Bash sort array menurut panjang elemen?

9

Diberikan array string, saya ingin mengurutkan array sesuai dengan panjang setiap elemen.

Sebagai contoh...

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

Harus menyortir ke ...

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

(Sebagai bonus, alangkah baiknya jika daftar mengurutkan string dengan panjang yang sama, menurut abjad. Dalam contoh di atas medium stringdisortir sebelum middle stringmeskipun mereka memiliki panjang yang sama. Tapi itu bukan persyaratan "keras", jika terlalu mempersulit larutan).

OK jika array diurutkan di tempat (yaitu "array" dimodifikasi) atau jika array diurutkan baru dibuat.

PJ Singh
sumber
1
beberapa jawaban menarik di sini, Anda harus dapat mengadaptasi satu untuk menguji panjang string juga stackoverflow.com/a/30576368/2876682
frostschutz

Jawaban:

12

Jika string tidak mengandung baris baru, berikut ini akan berfungsi. Ini mengurutkan indeks array dengan panjang, menggunakan string sendiri sebagai kriteria pengurutan sekunder.

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

Perhatikan bahwa pindah ke bahasa pemrograman nyata dapat sangat menyederhanakan solusi, misalnya dalam Perl, Anda bisa saja

sort { length $b <=> length $a or $a cmp $b } @array
choroba
sumber
1
Dengan Python:sorted(array, key=lambda s: (len(s), s))
wjandrea
1
Dalam 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- )

Ini membaca nilai array yang diurutkan dari subtitusi proses.

Substitusi proses berisi loop. Loop menghasilkan setiap elemen array yang diawali dengan panjang elemen dan karakter tab di antaranya.

Output dari loop diurutkan secara numerik dari terbesar ke terkecil (dan abjad jika panjang yang sama, penggunaan -k 2rdi tempat -k 2untuk membalik urutan abjad) dan hasil yang dikirim ke cutyang menghapus kolom dengan panjang tali.

Sortir skrip uji yang diikuti oleh uji coba:

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

Ini mengasumsikan bahwa string tidak mengandung baris baru. Pada sistem GNU dengan yang baru-baru ini bash, Anda dapat mendukung baris baru yang disematkan dalam data dengan menggunakan karakter nul sebagai pemisah rekaman, bukan baris baru:

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

Di sini, data dicetak dengan mengekor \0dalam lingkaran alih-alih baris baru, sortdan cutmembaca garis nul-delimited melalui -zopsi GNU mereka dan readarrayakhirnya membaca data nul-delimited dengan -d ''.

Kusalananda
sumber
3
Perhatikan bahwa -d '\0'sebenarnya -d ''karena bashtidak dapat meneruskan karakter NUL ke perintah, bahkan bawaannya. Tapi itu dipahami -d ''sebagai makna pembatasan pada NUL . Perhatikan bahwa Anda memerlukan bash 4.4+ untuk itu.
Stéphane Chazelas
@ StéphaneChazelas Tidak, tidak '\0', bukan $'\0'. Dan ya, itu mengkonversi (hampir persis) ke ''. Tapi itu adalah cara untuk mengkomunikasikan kepada pembaca lain niat sebenarnya menggunakan pembatas NUL.
Isaac
4

Saya tidak akan sepenuhnya mengulangi apa yang telah saya katakan tentang mengurutkan dalam bash , hanya Anda dapat mengurutkan dalam bash, tapi mungkin Anda tidak boleh. Di bawah ini adalah implementasi bash-only dari jenis penyisipan, yaitu O (n 2 ), dan hanya dapat ditoleransi untuk array kecil. Ini mengurutkan elemen array di tempat dengan panjangnya, dalam urutan menurun. Itu tidak melakukan semacam abjad sekunder.

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

Sebagai bukti bahwa ini adalah solusi khusus, pertimbangkan timing tiga jawaban yang ada pada berbagai ukuran array:

# 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 dan Kusalananda memiliki ide yang tepat: hitung panjang sekali dan gunakan utilitas khusus untuk menyortir dan memproses teks.

Jeff Schaller
sumber
4

Peretasan? (kompleks) dan cara satu baris yang cepat untuk mengurutkan array menurut panjangnya
( aman untuk baris baru dan array jarang):

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

Pada satu baris:

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

Saat eksekusi

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

Ini juga menangani elemen array dengan baris baru di dalamnya; ini bekerja dengan sorthanya melewati panjang dan indeks setiap elemen. Ini harus bekerja dengan bashdan 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[@]}"

Jika elemen-elemen dengan panjang yang sama juga harus diurutkan secara leksikografis, loop dapat diubah seperti ini:

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

Ini juga akan diteruskan ke sortstring (dengan baris baru berubah menjadi spasi), tetapi mereka akan tetap disalin dari sumber ke array tujuan dengan indeks mereka. Dalam kedua contoh, $(...)hanya akan melihat garis yang berisi angka (dan /karakter dalam contoh pertama), sehingga tidak akan tersandung oleh karakter globbing atau spasi di string.

mosvy
sumber
Tidak dapat mereproduksi Dalam contoh kedua, $(...)substitusi perintah hanya melihat indeks (daftar angka yang dipisahkan oleh baris baru), karena cut -d' ' -f1pengurutan setelahnya. Ini dapat dengan mudah ditunjukkan oleh tee /dev/ttypada akhir $(...).
Mosvy
Maaf, salah saya, saya melewatkan cut.
Stéphane Chazelas
@Isaac Tidak perlu mengutip ${!in[@]}atau ${#in[i]}/$iekspansi variabel karena mereka hanya berisi digit yang tidak tunduk pada ekspansi glob dan unset IFSakan mengatur ulang IFSruang, tab, baris baru. Bahkan, mengutipnya akan berbahaya , karena akan memberikan kesan yang salah bahwa mengutip seperti itu bermanfaat dan efektif, dan bahwa pengaturan IFSdan / atau penyaringan output sortpada contoh kedua dapat dengan aman dihilangkan dengan aman.
Mosvy
@Isaac TIDAK rusak jika inberisi "testing * here"dan shopt -s nullglobdiatur sebelum loop.
Mosvy
3

Dalam kasus beralih ke zshadalah opsi, cara hackish sana (untuk array berisi urutan byte):

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

zshmemungkinkan menentukan perintah sortir untuk ekspansi glob melalui kualifikasi glob. Jadi di sini, kami menipu untuk melakukannya untuk array sewenang-wenang dengan globbing on /, tetapi mengganti /dengan elemen array ( e'{reply=("$array[@]")}') dan kemudian secara nkasar rder o(terbalik dengan huruf besar O) elemen berdasarkan panjangnya ( Oe'{REPLY=$#REPLY}').

Perhatikan bahwa ini didasarkan pada panjangnya jumlah karakter. Untuk jumlah byte, atur lokal ke C( LC_ALL=C).

bashPendekatan 4.4+ lainnya (dengan asumsi array tidak terlalu besar):

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

(itu panjang dalam byte ).

Dengan versi yang lebih lama bash, Anda selalu dapat melakukan:

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

(yang juga akan bekerja dengan ksh93, zsh, yash, mksh).

Stéphane Chazelas
sumber