Bash-Sortierarray nach Länge der Elemente?

9

Bei einem Array von Zeichenfolgen möchte ich das Array nach der Länge jedes Elements sortieren.

Zum Beispiel...

    array=(
    "tiny string"
    "the longest string in the list"
    "middle string"
    "medium string"
    "also a medium string"
    "short string"
    )

Sollte sortieren nach ...

    "the longest string in the list"
    "also a medium string"
    "medium string"
    "middle string"
    "short string"
    "tiny string"

(Als Bonus wäre es schön, wenn die Liste Zeichenfolgen gleicher Länge alphabetisch sortieren würde. Im obigen Beispiel medium stringwurde sie zuvor sortiert middle string, obwohl sie dieselbe Länge haben. Dies ist jedoch keine "harte" Anforderung, wenn dies die Lösung).

Es ist in Ordnung, wenn das Array direkt sortiert ist (dh "Array" wird geändert) oder wenn ein neues sortiertes Array erstellt wird.

PJ Singh
quelle
1
Einige interessante Antworten hier, sollten Sie in der Lage sein, eine anzupassen, um auch die Länge der Zeichenfolge zu testen stackoverflow.com/a/30576368/2876682
Frostschutz

Antworten:

12

Wenn die Zeichenfolgen keine Zeilenumbrüche enthalten, sollte Folgendes funktionieren. Es sortiert die Indizes des Arrays nach der Länge, wobei die Zeichenfolgen selbst als sekundäres Sortierkriterium verwendet werden.

#!/bin/bash
array=(
    "tiny string"
    "the longest string in the list"
    "middle string"
    "medium string"
    "also a medium string"
    "short string"
)
expected=(
    "the longest string in the list"
    "also a medium string"
    "medium string"
    "middle string"
    "short string"
    "tiny string"
)

indexes=( $(
    for i in "${!array[@]}" ; do
        printf '%s %s %s\n' $i "${#array[i]}" "${array[i]}"
    done | sort -nrk2,2 -rk3 | cut -f1 -d' '
))

for i in "${indexes[@]}" ; do
    sorted+=("${array[i]}")
done

diff <(echo "${expected[@]}") \
     <(echo "${sorted[@]}")

Beachten Sie, dass die Umstellung auf eine echte Programmiersprache die Lösung erheblich vereinfachen kann, z. B. in Perl

sort { length $b <=> length $a or $a cmp $b } @array
Choroba
quelle
1
In Python:sorted(array, key=lambda s: (len(s), s))
Wjandrea
1
In Ruby:array.sort { |a| a.size }
Dmitry Kudriavtsev
9
readarray -t array < <(
for str in "${array[@]}"; do
    printf '%d\t%s\n' "${#str}" "$str"
done | sort -k 1,1nr -k 2 | cut -f 2- )

Dies liest die Werte des sortierten Arrays aus einer Prozessersetzung.

Die Prozessersetzung enthält eine Schleife. Die Schleife gibt jedes Element des Arrays aus, dem die Länge des Elements und ein Tabulatorzeichen dazwischen vorangestellt sind.

Die Ausgabe der Schleife wird numerisch vom größten zum kleinsten sortiert (und alphabetisch, wenn die Längen gleich sind; -k 2ranstelle von -k 2, um die alphabetische Reihenfolge umzukehren), und das Ergebnis davon wird gesendet, an cutdas die Spalte mit den Zeichenfolgenlängen gelöscht wird.

Sortieren Sie das Testskript, gefolgt von einem Testlauf:

array=(
    "tiny string"
    "the longest string in the list"
    "middle string"
    "medium string"
    "also a medium string"
    "short string"
)

readarray -t array < <(
for str in "${array[@]}"; do
    printf '%d\t%s\n' "${#str}" "$str"
done | sort -k 1,1nr -k 2 | cut -f 2- )

printf '%s\n' "${array[@]}"
$ bash script.sh
the longest string in the list
also a medium string
medium string
middle string
short string
tiny string

Dies setzt voraus, dass die Zeichenfolgen keine Zeilenumbrüche enthalten. Auf GNU-Systemen mit einer aktuellen bashVersion können Sie eingebettete Zeilenumbrüche in den Daten unterstützen, indem Sie anstelle von Zeilenumbrüchen das Nullzeichen als Datensatztrennzeichen verwenden:

readarray -d '' -t array < <(
for str in "${array[@]}"; do
    printf '%d\t%s\0' "${#str}" "$str"
done | sort -z -k 1,1nr -k 2 | cut -z -f 2- )

Hier werden die Daten mit Nachlauf \0in der Schleife anstelle von Zeilenumbrüchen gedruckt, die sortund cutlesen nicht begrenzte Zeilen durch ihre -zGNU-Optionen und readarraylesen schließlich die nicht begrenzten Daten mit -d ''.

Kusalananda
quelle
3
Beachten Sie, dass -d '\0'in der Tat -d ''als bashnicht NUL - Zeichen auf Befehle passieren kann, auch seine builtins. Aber es versteht sich -d ''als Abgrenzung auf NUL . Beachten Sie, dass Sie dafür Bash 4.4+ benötigen.
Stéphane Chazelas
@ StéphaneChazelas Nein, das ist es nicht '\0', das ist es $'\0'. Und ja, es konvertiert (fast genau) in ''. Aber das ist ein Weg , um comunicate andere Leser der eigentlichen Absicht , ein NUL Trennzeichen verwenden.
Isaac
4

Ich werde nicht vollständig wiederholen, was ich bereits über das Sortieren in Bash gesagt habe. Sie können nur innerhalb von Bash sortieren, aber vielleicht sollten Sie es nicht. Im Folgenden finden Sie eine Nur-Bash-Implementierung einer Einfügungssortierung, die O (n 2 ) ist und daher nur für kleine Arrays toleriert werden kann. Es sortiert die vorhandenen Array-Elemente nach ihrer Länge in absteigender Reihenfolge. Es wird keine sekundäre alphabetische Sortierung durchgeführt.

array=(
    "tiny string"
    "the longest string in the list"
    "middle string"
    "medium string"
    "also a medium string"
    "short string"
    )

function sort_inplace {
  local i j tmp
  for ((i=0; i <= ${#array[@]} - 2; i++))
  do
    for ((j=i + 1; j <= ${#array[@]} - 1; j++))
    do
      local ivalue jvalue
        ivalue=${#array[i]}
        jvalue=${#array[j]}
        if [[ $ivalue < $jvalue ]]
        then
                tmp=${array[i]}
                array[i]=${array[j]}
                array[j]=$tmp
        fi
    done
  done
}

echo Initial:
declare -p array

sort_inplace

echo Sorted:
declare -p array

Betrachten Sie als Beweis dafür, dass dies eine spezielle Lösung ist, die Zeitabläufe der vorhandenen drei Antworten auf Arrays unterschiedlicher Größe:

# 6 elements
Choroba: 0m0.004s
Kusalananda: 0m0.004s
Jeff: 0m0.018s         ## already 4 times slower!

# 1000 elements
Choroba: 0m0.004s
Kusalananda: 0m0.004s
Jeff: 0m0.021s        ## up to 5 times slower, now!

5000 elements
Choroba: 0m0.004s
Kusalananda: 0m0.004s
Jeff: 0m0.019s

# 10000 elements
Choroba: 0m0.004s
Kusalananda: 0m0.006s
Jeff: 0m0.020s

# 99000 elements
Choroba: 0m0.015s
Kusalananda: 0m0.012s
Jeff: 0m0.119s

Choroba und Kusalananda haben die richtige Idee: Berechnen Sie die Längen einmal und verwenden Sie spezielle Dienstprogramme zum Sortieren und Verarbeiten von Text.

Jeff Schaller
quelle
4

Ein Hackish? (komplexe) und schnelle einzeilige Methode zum Sortieren des Arrays nach Länge
( sicher für Zeilenumbrüche und spärliche Arrays):

#!/bin/bash
in=(
    "tiny string"
    "the longest
        string also containing
        newlines"
    "middle string"
    "medium string"
    "also a medium string"
    "short string"
    "test * string"
    "*"
    "?"
    "[abc]"
)

readarray -td $'\0' sorted < <(
                    for i in "${in[@]}"
                    do     printf '%s %s\0' "${#i}" "$i";
                    done |
                            sort -bz -k1,1rn -k2 |
                            cut -zd " " -f2-
                    )

printf '%s\n' "${sorted[@]}"

In einer Zeile:

readarray -td $'\0' sorted < <(for i in "${in[@]}";do printf '%s %s\0' "${#i}" "$i"; done | sort -bz -k1,1rn -k2 | cut -zd " " -f2-)

Bei der Ausführung

$ ./script
the longest
        string also containing
        newlines
also a medium string
medium string
middle string
test * string
short string
tiny string
[abc]
?
*
Isaac
quelle
4

Dies behandelt auch Array-Elemente mit Zeilenumbrüchen. Es funktioniert, indem sortnur die Länge und der Index jedes Elements durchlaufen werden . Es sollte mit bashund funktionieren ksh.

in=(
    "tiny string"
    "the longest
        string also containing
        newlines"
    "middle string"
    "medium string"
    "also a medium string"
    "short string"
)
out=()

unset IFS
for a in $(for i in ${!in[@]}; do echo ${#in[i]}/$i; done | sort -rn); do
        out+=("${in[${a#*/}]}")
done

printf '"%s"\n' "${out[@]}"

Wenn die Elemente gleicher Länge auch lexikografisch sortiert werden müssen, kann die Schleife folgendermaßen geändert werden:

IFS='
'
for a in $(for i in ${!in[@]}; do printf '%s\n' "$i ${#in[i]} ${in[i]//$IFS/ }"; done | sort -k 2,2nr -k 3 | cut -d' ' -f1); do
        out+=("${in[$a]}")
done

Dies wird auch an sortdie Zeichenfolgen übergeben (wobei Zeilenumbrüche in Leerzeichen geändert werden), sie werden jedoch weiterhin von ihren Indizes von der Quelle in das Zielarray kopiert. In beiden Beispielen $(...)werden nur Zeilen angezeigt, die Zahlen enthalten (und das /Zeichen im ersten Beispiel), sodass es nicht durch Globbing-Zeichen oder Leerzeichen in den Zeichenfolgen ausgelöst wird.

Mosvy
quelle
Kann nicht reproduzieren. Im zweiten Beispiel werden bei der $(...)Befehlssubstitution aufgrund der Nachsortierung nur die Indizes (eine durch Zeilenumbrüche getrennte Liste von Zahlen) angezeigt cut -d' ' -f1. Dies konnte leicht durch ein tee /dev/ttyam Ende des $(...).
Mosvy
Entschuldigung, mein Schlimmes, ich habe das verpasst cut.
Stéphane Chazelas
@Isaac Es ist nicht erforderlich, die ${!in[@]}oder ${#in[i]}/$iVariablenerweiterungen in Anführungszeichen zu setzen, da sie nur Ziffern enthalten, die keiner Glob-Erweiterung unterliegen, und unset IFSdie IFSauf Leerzeichen, Tabulatoren und Zeilenumbrüche zurücksetzen. In der Tat wäre das Zitieren schädlich , da es den falschen Eindruck erweckt, dass ein solches Zitieren nützlich und effektiv ist und dass das Einstellen IFSund / oder Filtern der Ausgabe sortim zweiten Beispiel sicher beseitigt werden könnte.
Mosvy
@Isaac Es wird NICHT unterbrochen , wenn es inenthält "testing * here"und shopt -s nullglobvor der Schleife gesetzt wird.
Mosvy
3

Für den Fall, dass ein Wechsel zu zsheine Option ist, ein hackiger Weg dorthin (für Arrays, die eine beliebige Folge von Bytes enthalten):

array=('' blah $'x\ny\nz' $'x\0y' '1 2 3')
sorted_array=( /(e'{reply=("$array[@]")}'nOe'{REPLY=$#REPLY}') )

zshErmöglicht das Definieren von Sortierreihenfolgen für die Glob-Erweiterung über Glob-Qualifizierer. Hier täuschen wir es also vor, es für beliebige Arrays zu tun, indem wir es globalisieren /, aber durch /die Elemente des Arrays ersetzen ( e'{reply=("$array[@]")}') und dann die Elemente basierend auf ihrer Länge ( ) numerisch oumordnen (umgekehrt mit Großbuchstaben ).OOe'{REPLY=$#REPLY}'

Beachten Sie, dass dies auf der Länge der Anzahl der Zeichen basiert. Setzen Sie für die Anzahl der Bytes das Gebietsschema auf C( LC_ALL=C).

Ein bashweiterer Ansatz von 4.4+ (unter der Annahme eines nicht zu großen Arrays):

readarray -td '' sorted_array < <(
  perl -l0 -e 'print for sort {length $b <=> length $a} @ARGV
              ' -- "${array[@]}")

(das ist die Länge in Bytes ).

Mit älteren Versionen von bashkönnen Sie immer Folgendes tun:

eval "sorted_array=($(
    perl -l0 -e 'for (sort {length $b <=> length $a} @ARGV) {
      '"s/'/'\\\\''/g"'; printf " '\'%s\''", $_}' -- "${array[@]}"
  ))"

(was auch mit arbeiten würde ksh93, zsh, yash, mksh).

Stéphane Chazelas
quelle