Bash сортувати масив відповідно до довжини елементів?


9

Враховуючи масив рядків, я хотів би сортувати масив відповідно до довжини кожного елемента.

Наприклад...

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

Сортувати за ...

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

(Як бонус, було б непогано, якби список сортував рядки однакової довжини за алфавітом. У наведеному вище прикладі medium stringбуло відсортовано раніше, middle stringхоча вони однакової довжини. Але це не є "жорсткою" вимогою, якщо це над ускладнює рішення).

Гаразд, якщо масив відсортований на місці (тобто "масив" модифікований) або якщо створено новий відсортований масив.


1
деякі цікаві відповіді тут, ви повинні бути в змозі пристосувати один до випробування для довжини рядка, а stackoverflow.com/a/30576368/2876682
frostschutz

Відповіді:


12

Якщо рядки не містять нових рядків, слід працювати наступним чином. Він сортує індекси масиву за довжиною, використовуючи самі рядки як критерій вторинного сортування.

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

Зауважте, що перехід на справжню мову програмування може значно спростити рішення, наприклад, в Perl, ви можете просто

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

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

1
В Рубі:array.sort { |a| a.size }
Дмитро Кудрявцев

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

Це зчитує значення відсортованого масиву з підстановки процесу.

Процес заміщення містить цикл. Цикл виводить кожен елемент масиву, попередньо визначений довжиною елемента та символом вкладки між ними.

Виведення циклу сортується за чисельністю від найбільшого до найменшого (і в алфавітному порядку, якщо довжини однакові; використовувати -k 2rзамість того, -k 2щоб змінити алфавітний порядок), і результат цього надсилається, до cutякого видаляється стовпець із довжиною рядків.

Сортувати тестовий сценарій з подальшим тестовим запуском:

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

Це передбачає, що рядки не містять нових рядків. У системах GNU з останнім часом bashви можете підтримувати вбудовані нові рядки в дані, використовуючи нуль-символ як роздільник записів замість нового рядка:

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

Тут дані друкуються з \0контуром у циклі замість нових рядків, sortа також cutзчитують рядки з обмеженими нулями через їх -zпараметри GNU і, readarrayнарешті, зчитують дані, обмежені нулем -d ''.


3
Зауважте, що -d '\0'насправді -d ''так bashне можна передавати символи NUL командам, навіть її вбудованим. Але це розуміє -d ''як значення, що обмежується на NUL . Зауважте, що для цього вам потрібно bash 4.4+.
Стефан Хазелас

@ StéphaneChazelas Ні, це не '\0'так $'\0'. І так, він перетворюється (майже точно) на ''. Але це спосіб донести до інших читачів фактичний намір використовувати роздільник NUL.
Ісаак

4

Я не буду повністю повторювати те, що я вже говорив про сортування по bash , просто ви можете сортувати в bash, але, можливо, не варто. Нижче наведено реалізацію типу "bash", яка є O (n 2 ), і так допустимо лише для малих масивів. Він сортує елементи масиву на місці за їх довжиною, у порядку зменшення. Це не робить другорядний алфавітний сорт.

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

Як доказ того, що це спеціалізоване рішення, розгляньте терміни існуючих трьох відповідей на масиви різних розмірів:

# 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 та Kusalananda є правильна ідея: обчислити довжини один раз і використовувати спеціальні утиліти для сортування та обробки тексту.


4

Хакіш? (складний) та швидкий однорядковий спосіб сортувати масив за довжиною
( безпечно для нових рядків та розріджених масивів):

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

По одному рядку:

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

На виконання

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

4

Це також обробляє елементи масиву з новими рядками в них; він працює, пропускаючи sortлише довжину та індекс кожного елемента. Він повинен працювати з bashі 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[@]}"

Якщо елементи однакової довжини також мають бути відсортовані лексикографічно, цикл можна змінити так:

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

Це також перейде до sortрядків (з новими рядками змінено на пробіли), але вони все одно будуть скопійовані з джерела в масив призначення їх індексами. В обох прикладах $(...)заповіт побачить лише рядки, що містять цифри (та /символ у першому прикладі), тому він не буде зіткнутий глобусними символами чи пробілами у рядках.


Неможливо відтворити. У другому прикладі $(...)заміна команди бачить лише індекси (список номерів, розділених новими рядками) через cut -d' ' -f1сортування після. Це можна легко продемонструвати в tee /dev/ttyкінці $(...).
mosvy

Вибачте, мій поганий, я пропустив cut.
Стефан Шазелас

@Isaac Не потрібно цитувати розширення ${!in[@]}або ${#in[i]}/$iзмінну, тому що вони містять лише цифри, які не підлягають глобальному розширенню, і unset IFSбуде скинуто IFSпробіл, вкладку, новий рядок. Насправді, цитування їх було б шкідливим , оскільки воно створить помилкове враження, що таке цитування є корисним та ефективним, і що налаштування IFSта / або фільтрація результатів sortу другому прикладі можна було б безпечно виконувати.
mosvy

@Isaac Він НЕ зламається, якщо inмістить, "testing * here"і shopt -s nullglobвстановлений перед циклом.
mosvy

3

У випадку, якщо перехід на zsh- це варіант, хакітський шлях туди (для масивів, що містять будь-яку послідовність байтів):

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

zshдозволяє визначати порядки сортування для його глобального розширення за допомогою глобальних класифікаторів. Отже, тут ми обманюємо це, щоб зробити це для довільних масивів шляхом поглинання /, але замінивши /на елементи масиву ( e'{reply=("$array[@]")}'), а потім nумерно order (у зворотному регістрі O) на основі їх довжини ( Oe'{REPLY=$#REPLY}').

Зауважте, що вона базується на довжині в кількості символів. Для кількості байтів встановіть локаль на C( LC_ALL=C).

Ще один bashпідхід 4.4+ (припускаючи не надто великий масив):

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

(така довжина в байтах ).

З більш старими версіями bashви завжди можете:

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

(Який також буде працювати з ksh93, zsh, yash, mksh).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.