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

Answers:


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

这将从流程替换中读取已排序数组的值。

进程替换包含一个循环。循环输出该数组的每个元素,该元素的前面是元素的长度和一个制表符。

循环的输出按数字从大到小的顺序排序(如果长度相同,则按字母顺序排序;使用-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

假定字符串不包含换行符。在具有new的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以循环结尾而不是换行符的形式打印,sortcut通过其-zGNU选项readarray读取以nul 分隔的行,最后使用读取以nul分隔的数据-d ''


3
请注意,-d '\0'实际上是-d ''因为bash无法将NUL字符传递给命令,甚至不能将其内置命令传递给命令。但是它确实理解-d ''对NUL的含义界定。请注意,您需要为此使用bash 4.4+。
斯特凡Chazelas

@StéphaneChazelas不,不是'\0',是$'\0'。是的,它将(几乎完全)转换为''。但是,这是一种能够comunicate给其他读者的实际意图使用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

ChorobaKusalananda的想法正确:一次计算长度,并使用专用的工具进行排序和文本处理。


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末尾的a可以很容易地证明这一点$(...)
mosvy

抱歉,我不好,我错过了cut
斯特凡Chazelas

@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允许通过glob限定符为其glob扩展定义排序顺序。因此,在这里,我们欺骗性地通过遍历on来对任意数组进行处理/,但替换/为array(e'{reply=("$array[@]")}')的元素,然后根据其长度()n总括地递归o(大写相反)。OOe'{REPLY=$#REPLY}'

请注意,它基于字符数的长度。对于字节数,将语言环境设置为CLC_ALL=C)。

另一种bash4.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[@]}"
  ))"

(这也将一起工作ksh93zshyashmksh)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.