Bash sıralama dizisi elemanların uzunluğuna göre mi?


9

Dizeler dizisi verildiğinde, diziyi her öğenin uzunluğuna göre sıralamak istiyorum.

Örneğin...

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

Sıralamak gerekir ...

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

(Bir bonus olarak, listenin aynı uzunlukta dizeleri alfabetik olarak sıralaması güzel olurdu. Yukarıdaki örnekte aynı uzunlukta olmalarına rağmen daha medium stringönce sıralanmıştı middle string. Ancak, bu, çözüm).

Dizi yerinde sıralanırsa (yani "dizi" değiştirilirse) veya yeni bir sıralanmış dizi oluşturulursa sorun yoktur.


1
Burada bazı ilginç cevaplar, dize uzunluğunu da test etmek için birini uyarlayabilmelisiniz stackoverflow.com/a/30576368/2876682
frostschutz

Yanıtlar:


12

Dizeler satırsonu içermiyorsa, aşağıdakilerin çalışması gerekir. Dizeleri, ikincil sıralama ölçütü olarak kullanarak dizinin dizinlerini uzunluğa göre sıralar.

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

Gerçek bir programlama diline geçmenin çözümü büyük ölçüde basitleştirebileceğini unutmayın, örneğin Perl'de,

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

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

1
Ruby'de: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- )

Bu işlem, işlem dizisinden sıralanan dizinin değerlerini okur.

İşlem ikamesi bir döngü içerir. Döngü, dizinin her bir öğesini, öğenin uzunluğu ve aradaki sekme karakteri ile başlayarak çıkarır.

Döngünün çıkış küçük en büyük sayısal olarak kriteri (alfabetik uzunlukları aynı ise, kullanım olup -k 2ryerine -k 2alfabetik sırasını tersine çevirmek için), ve bir sonuç olduğu gönderilir cutki dize uzunluklarına sahip sütun siler.

Test komut dosyasını ve ardından bir test çalıştırmasını sıralayın:

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

Bu, dizelerin yeni satır içermediğini varsayar. Yakın tarihli GNU sistemlerinde bash, null karakterini yeni satır yerine kayıt ayırıcı olarak kullanarak verilere gömülü yeni satırları destekleyebilirsiniz:

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

Burada, veri firar ile basılır \0, yerine yeni satır ilmeğine sortve cutbunların içinden nul ayrılmış hatlar okur -zGNU seçenekleri ve readarrayson olarak nul ayrılmış verileri okur -d ''.


3
Not -d '\0'aslında -d ''olarak bashkomutlara boş karakteri geçemez, hatta builtins. Ancak NUL üzerinde-d '' anlam sınırlayıcı olduğunu anlıyor . Bunun için bash 4.4+ ihtiyacınız olduğunu unutmayın.
Stéphane Chazelas

@ StéphaneChazelas Hayır, değil '\0', öyle $'\0'. Ve evet, (neredeyse tam olarak) 'e dönüşür ''. Ancak bu, diğer okuyuculara gerçek bir NUL sınırlayıcı kullanma niyetiyle iletişim kurmanın bir yoludur .
Isaac

4

Tamamen tekrarlamak istemiyorum zaten bash sıralama hakkında söylediklerinizi sadece, olabilir bash içinde sıralamak, ama belki atlatma. Aşağıda O (n 2 ) olan ve sadece küçük diziler için tolere edilebilen bir yerleştirme türünün sadece bash bir uygulamasıdır . Dizi öğelerini uzunluklarına göre azalan sırada yerinde sıralar. İkincil alfabetik sıralama yapmaz.

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

Bunun özel bir çözüm olduğuna dair kanıt olarak, çeşitli boyut dizilerindeki mevcut üç cevabın zamanlamalarını düşünün:

# 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 ve Kusalananda doğru fikre sahiptir: uzunlukları bir kez hesaplayın ve sıralama ve metin işleme için özel yardımcı programları kullanın.


4

Bir hackish? (karmaşık) ve diziyi uzunluğa göre sıralamak için hızlı bir satır yolu
( yeni satırlar ve seyrek diziler için güvenli ):

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

Bir satırda:

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

Yürütme sırasında

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

4

Bu aynı zamanda içinde yeni satır bulunan dizi öğelerini de işler; sorther elemanın sadece uzunluk ve indeksinden geçerek çalışır . O ile çalışmalıdır bashve 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[@]}"

Aynı uzunluktaki öğelerin sözlükbilimsel olarak da sıralanması gerekiyorsa, döngü şu şekilde değiştirilebilir:

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

Bu aynı zamanda sortdizgilere de geçecektir (yeni satırlar boşluk olarak değiştirilmiştir), ancak yine de kaynaktan dizine göre hedef diziye kopyalanacaktır. Her iki örnekte de, $(...)yalnızca sayı içeren satırları (ve /ilk örnekteki karakteri) görür , bu nedenle dizelerdeki karakterler veya boşluklar tarafından tetiklenmez.


Çoğaltamıyor. İkinci örnekte, $(...)komut ikamesi cut -d' ' -f1sıralamadan sonra yalnızca dizinleri (satırsonu ile ayrılmış sayıların listesi) görür . Bu tee /dev/tty, sonunda bir ile kolayca gösterilebilir $(...).
mosvy

Üzgünüm, kötülerim, özledim cut.
Stéphane Chazelas

@Isaac Yalnızca glob genişlemesine tabi olmayan rakamlar içerdiğinden ${!in[@]}veya ${#in[i]}/$ideğişken genişletmelerini alıntılamaya gerek yoktur ve boşluk, sekme, satırsonuna unset IFSsıfırlanır IFS. Aslında, alıntı yapmak zararlı olacaktır, çünkü bu alıntılamanın yararlı ve etkili olduğu ve ikinci örnekte IFSçıktısının ayarlanması ve / veya filtrelenmesinin sortgüvenli bir şekilde ortadan kaldırılabileceği yanlış izlenimi verecektir .
mosvy

@Isaac O mu değil eğer kırmak iniçeren "testing * here"ve shopt -s nullglobdöngü öncesinde ayarlanır.
mosvy

3

zshGeçişin bir seçenek olması durumunda , oradaki hackish bir yol (herhangi bir bayt dizisi içeren diziler için):

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

zshglob niteleyicileri aracılığıyla glob genişlemesi için sıralama düzenlerinin tanımlanmasına izin verir. Yani burada, biz globbing tarafından keyfi diziler için bunu yapmak için kandırma ediyoruz /, ancak yerine /dizinin (unsurları ile e'{reply=("$array[@]")}'daha sonra) ve numerically o(büyük harf ile ters içinde Sarf Okendi uzunluğuna bağlı olarak) öğelerini ( Oe'{REPLY=$#REPLY}').

Karakter sayısının uzunluğuna bağlı olduğunu unutmayın. Bayt sayısı için yerel ayarı C( LC_ALL=C) olarak ayarlayın.

Başka bir bash4.4+ yaklaşım (çok büyük bir dizi olmadığı varsayılarak):

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

(bu bayt cinsinden uzunluk ).

Eski sürümleriyle bashher zaman şunları yapabilirsiniz:

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

(bu da çalışmaya devam eder ki ksh93, zsh, yash, mksh).

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.