Array di ordinamento di Bash in base alla lunghezza degli elementi?


9

Dato un array di stringhe, vorrei ordinare l'array in base alla lunghezza di ciascun elemento.

Per esempio...

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

Dovrebbe ordinare per ...

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

(Come bonus, sarebbe bello se l'elenco ordinasse le stringhe della stessa lunghezza, in ordine alfabetico. Nell'esempio sopra è medium stringstato ordinato prima middle stringanche se hanno la stessa lunghezza. Ma questo non è un requisito "difficile", se complica il soluzione).

Va bene se l'array viene ordinato sul posto (ovvero viene modificato "array") o se viene creato un nuovo array ordinato.


1
alcune risposte interessanti qui, si dovrebbe essere in grado di adattarsi a un test per la lunghezza della stringa così stackoverflow.com/a/30576368/2876682
frostschutz

Risposte:


12

Se le stringhe non contengono newline, dovrebbe funzionare quanto segue. Ordina gli indici dell'array per lunghezza, usando le stringhe stesse come criterio di ordinamento secondario.

#!/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[@]}")

Nota che passare a un vero linguaggio di programmazione può semplificare notevolmente la soluzione, ad esempio in Perl, puoi semplicemente

sort { length $b <=> length $a or $a cmp $b } @array

1
In Python:sorted(array, key=lambda s: (len(s), s))
wjandrea,

1
In Ruby:array.sort { |a| a.size }
Dmitry Kudriavtsev il

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- )

Questo legge i valori dell'array ordinato da una sostituzione di processo.

La sostituzione del processo contiene un ciclo. Il loop emette ciascun elemento dell'array anteposto dalla lunghezza dell'elemento e un carattere di tabulazione nel mezzo.

L'uscita del loop è ordinato numericamente dal più grande al più piccolo (e alfabetico se le lunghezze sono uguali, l'uso -k 2ral posto di -k 2invertire l'ordine alfabetico) e il risultato di che viene inviato cutche elimina la colonna con le lunghezze delle corde.

Ordina lo script di test seguito da un'esecuzione di test:

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

Ciò presuppone che le stringhe non contengano newline. Sui sistemi GNU con un recente bash, è possibile supportare newline incorporate nei dati usando il carattere nul come separatore del record anziché newline:

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- )

Qui, i dati vengono stampati con un trascinamento \0nel ciclo anziché con le nuove righe, sorte cutlegge righe delimitate da null tramite le loro -zopzioni GNU e readarrayinfine legge i dati delimitati da null -d ''.


3
Si noti che -d '\0'in realtà -d ''come bashnon può passare caratteri NUL ai comandi, anche i suoi comandi incorporati. Ma capisce -d ''come delimit di significato su NUL . Nota che hai bisogno di bash 4.4+ per questo.
Stéphane Chazelas,

@ StéphaneChazelas No, non lo è '\0', lo è $'\0'. E sì, si converte (quasi esattamente) in ''. Ma questo è un modo per comunicare ad altri lettori l'intento reale di usare un delimitatore NUL.
Isaac,

4

Non ripeterò completamente quello che ho già detto sull'ordinamento in bash , solo tu puoi ordinare in bash, ma forse non dovresti. Di seguito è un'implementazione solo bash di un ordinamento di inserzione, che è O (n 2 ), e quindi è tollerabile solo per piccoli array. Ordina gli elementi dell'array sul posto in base alla loro lunghezza, in ordine decrescente. Non esegue un ordinamento alfabetico secondario.

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

A riprova del fatto che si tratta di una soluzione specializzata, considerare i tempi delle tre risposte esistenti su matrici di varie dimensioni:

# 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 e Kusalananda hanno l'idea giusta: calcolare una volta le lunghezze e utilizzare utility dedicate per l'ordinamento e l'elaborazione del testo.


4

Un hacker? (complesso) e veloce modo a una riga per ordinare l'array in base alla lunghezza
( sicuro per newline e array sparsi):

#!/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[@]}"

Su una riga:

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

In esecuzione

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

4

Questo gestisce anche gli elementi dell'array con newline in essi; funziona passando sortsolo attraverso la lunghezza e l'indice di ciascun elemento. Dovrebbe funzionare con bashe 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[@]}"

Se anche gli elementi della stessa lunghezza devono essere ordinati lessicograficamente, il loop potrebbe essere modificato in questo modo:

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

Questo passerà anche alle sortstringhe (con le nuove righe modificate in spazi), ma verrebbero comunque copiate dalla matrice all'array di destinazione dai loro indici. In entrambi gli esempi, $(...)vedranno solo le righe che contengono numeri (e il /carattere nel primo esempio), quindi non verrà fatto scattare da caratteri o spazi traballanti nelle stringhe.


Impossibile riprodurre. Nel secondo esempio, la $(...)sostituzione del comando vede solo gli indici (un elenco di numeri separati da newline), a causa del cut -d' ' -f1dopo l'ordinamento. Questo potrebbe essere facilmente dimostrato da un tee /dev/ttyalla fine del $(...).
mosvy,

Scusa, mio ​​male, ho perso il cut.
Stéphane Chazelas,

@Isaac Non è necessario citare le espansioni variabili ${!in[@]}o ${#in[i]}/$iperché contengono solo cifre che non sono soggette all'espansione glob e unset IFSreimposteranno lo IFSspazio, la scheda, la nuova riga. In effetti, la loro citazione sarebbe dannosa , perché darebbe la falsa impressione che tale citazione sia utile ed efficace e che l'impostazione IFSe / o il filtro dell'output del sortsecondo esempio possano essere tranquillamente eliminati.
mosvy,

@Isaac NON si interrompe se incontiene "testing * here"ed shopt -s nullglobè impostato prima del ciclo.
mosvy,

3

Nel caso in cui il passaggio a zshsia un'opzione, un modo hacker lì (per array contenenti qualsiasi sequenza di byte):

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

zshconsente di definire gli ordinamenti per la sua espansione glob tramite qualificatori glob. Quindi, qui, lo stiamo ingannando per farlo per matrici arbitrarie facendo globbing /, ma sostituendoli /con gli elementi dell'array ( e'{reply=("$array[@]")}') e quindi nranterando umericamente o(al contrario con le maiuscole O) gli elementi in base alla loro lunghezza ( Oe'{REPLY=$#REPLY}').

Si noti che si basa sulla lunghezza in numero di caratteri. Per il numero di byte, impostare la locale su C( LC_ALL=C).

Un altro bashapproccio 4.4+ (supponendo un array non troppo grande):

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

(che è la lunghezza in byte ).

Con le versioni precedenti di bash, puoi sempre fare:

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

(che sarebbe anche lavorare con ksh93, zsh, yash, mksh).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.