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
choroba
источник
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версиями вы можете поддерживать встроенные символы новой строки в данных, используя символ nul в качестве разделителя записей вместо символа новой строки:

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

Чороба и Кусалананда имеют правильную идею: вычислить длины один раз и использовать специальные утилиты для сортировки и обработки текста.

Джефф Шаллер
источник
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строки (с символами новой строки, замененными на пробелы), но они все равно будут скопированы из исходного массива в целевой по своим индексам. В обоих примерах, $(...)будут видны только строки, содержащие цифры (и /символ в первом примере), поэтому он не будет отключен с помощью символов или пробелов в строках.

mosvy
источник
Не могу воспроизвести. Во втором примере $(...)команда подстановки видит только индексы (список чисел, разделенных новыми строками) из- cut -d' ' -f1за сортировки после. Это может быть легко продемонстрировано tee /dev/ttyв конце $(...).
Мосви
Извини, мой плохой, я пропустил cut.
Стефан Шазелас
@Isaac Нет необходимости заключать в кавычки ${!in[@]}или ${#in[i]}/$iрасширения переменных, потому что они содержат только цифры, которые не подлежат расширению glob, и unset IFSбудут сброшены в IFSпробел, табуляцию, перевод строки. На самом деле, цитирование их было бы вредно , потому что это создаст ложное впечатление, что такое цитирование полезно и эффективно, и что настройка IFSи / или фильтрация вывода sortво втором примере может быть безопасно устранена.
Мосвы
@Isaac НЕ ломается, если inсодержится "testing * here"и shopt -s nullglobустанавливается перед циклом.
Мосвы
3

В случае, если переключение на zshявляется опцией, есть хакерский способ (для массивов, содержащих любую последовательность байтов):

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

zshпозволяет определять порядки сортировки для его расширения глоба через квалификаторы глоба. Так вот, мы обманывая его , чтобы сделать это для любых массивов, подстановки на /, но заменяя /с элементами массива ( e'{reply=("$array[@]")}') , а затем numerically 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).

Стефан Шазелас
источник