如何在 Bash 中对数组进行排序
Posted
技术标签:
【中文标题】如何在 Bash 中对数组进行排序【英文标题】:How to sort an array in Bash 【发布时间】:2011-11-18 12:52:15 【问题描述】:我在 Bash 中有一个数组,例如:
array=(a c b f 3 5)
我需要对数组进行排序。不仅以排序的方式显示内容,还可以获得一个包含排序元素的新数组。新排序的数组可以是全新的也可以是旧的。
【问题讨论】:
【参考方案1】:你真的不需要那么多代码:
IFS=$'\n' sorted=($(sort <<<"$array[*]"))
unset IFS
支持元素中的空格(只要它不是换行符),并且在 Bash 3.x 中有效。
例如:
$ array=("a c" b f "3 5")
$ IFS=$'\n' sorted=($(sort <<<"$array[*]")); unset IFS
$ printf "[%s]\n" "$sorted[@]"
[3 5]
[a c]
[b]
[f]
注意: @sorontar 有 pointed out,如果元素包含诸如 *
或 ?
之类的通配符,则需要注意:
sorted=($(...)) 部分使用“split and glob”运算符。您应该关闭 glob:
set -f
或set -o noglob
或shopt -op noglob
或像*
这样的数组元素将扩展为文件列表。
发生了什么:
结果是按此顺序发生的六件事的高潮:
IFS=$'\n'
"$array[*]"
<<<
sort
sorted=($(...))
unset IFS
首先,IFS=$'\n'
这是我们操作的一个重要部分,它通过以下方式影响 2 和 5 的结果:
给定:
"$array[*]"
扩展到由IFS
的第一个字符分隔的每个元素
sorted=()
通过拆分 IFS
的每个字符来创建元素
IFS=$'\n'
sets things up 以便使用 新行 作为分隔符扩展元素,然后以每行成为一个元素的方式创建。 (即在新行上拆分。)
用新行分隔很重要,因为这就是sort
的操作方式(按行排序)。 仅分割一个新行并不重要,但需要保留包含空格或制表符的元素。
IFS
的默认值是 一个空格,一个制表符,后跟 一个新行,不适合我们的操作。
接下来,sort <<<"$array[*]"
部分
<<<
,称为here strings,采用上述"$array[*]"
的扩展,并将其输入sort
的标准输入。
在我们的示例中,sort
被输入以下字符串:
a c
b
f
3 5
由于sort
排序,它产生:
3 5
a c
b
f
接下来,sorted=($(...))
部分
$(...)
部分,称为command substitution,导致其内容 (sort <<<"$array[*]
) 作为正常命令运行,同时将生成的 标准输出 作为文字,随处可见 @ 987654358@是。
在我们的示例中,这会产生类似于简单编写的内容:
sorted=(3 5
a c
b
f
)
sorted
然后变成一个数组,通过在每一新行上拆分这个文字来创建。
最后,unset IFS
这会将IFS
的值重置为默认值,这是一种很好的做法。
这是为了确保我们不会在脚本后面依赖于 IFS
的任何内容造成麻烦。 (否则我们需要记住我们已经改变了一些东西——这对于复杂的脚本可能是不切实际的。)
【讨论】:
@xx 或没有IFS
,如果元素中有空格,它会将元素分成小块。尝试省略IFS=$'\n'
的例如,看看!
非常好。您能为普通的 bash 用户解释一下这个解决方案是如何工作的吗?
现在,使用IFS
,如果元素中只有一种特定类型的空白,它会将您的元素分成小块。好的;不完美:-)
unset IFS
有必要吗?我认为将IFS=
添加到命令的范围内仅对该命令的更改,之后自动返回到其先前的值。
@MarkH 这是必要的,因为sorted=()
不是命令,而是第二个变量赋值。【参考方案2】:
原始回复:
array=(a c b "f f" 3 5)
readarray -t sorted < <(for a in "$array[@]"; do echo "$a"; done | sort)
输出:
$ for a in "$sorted[@]"; do echo "$a"; done
3
5
a
b
c
f f
注意此版本处理包含特殊字符或空格的值(除了换行符)
注意 bash 4+ 支持 readarray。
编辑根据@Dimitre 的建议,我已将其更新为:
readarray -t sorted < <(printf '%s\0' "$array[@]" | sort -z | xargs -0n1)
它的好处是甚至可以理解正确嵌入换行符的 排序 元素。不幸的是,正如@ruakh 正确表示的那样,这并不意味着readarray
的结果将是正确,因为readarray
没有选择使用NUL
而不是常规的换行符 作为行分隔符。
【讨论】:
很好,还应该注意 readarray 从 bash 版本 4 开始可用。可以缩短一点:readarray -t sorted < <(printf '%s\n' "$array[@]" | sort)
@Dimitre:我采纳了您的建议并修复了空白处理以适用于任何内容(在内部使用 nullchar 分隔符)。干杯
是的,sort -z
是一个有用的改进,我想-z
选项是 GNU 排序扩展。
如果你想处理嵌入的换行符,你可以滚动你自己的 readarray。例如:sorted=(); while read -d $'\0' elem; do sorted[$#sorted[@]]=$elem; done < <(printf '%s\0' "$array[@]" | sort -z)
。这也适用于您使用 bash v3 而不是 bash v4,因为 readarray 在 bash v3 中不可用。
@user1527227 它是输入重定向 (<
) 结合 进程替换 <(...)
。或者直观地说:因为(printf "bla")
不是文件。【参考方案3】:
这是一个纯 Bash 快速排序实现:
#!/bin/bash
# quicksorts positional arguments
# return is in array qsort_ret
qsort()
local pivot i smaller=() larger=()
qsort_ret=()
(($#==0)) && return 0
pivot=$1
shift
for i; do
# This sorts strings lexicographically.
if [[ $i < $pivot ]]; then
smaller+=( "$i" )
else
larger+=( "$i" )
fi
done
qsort "$smaller[@]"
smaller=( "$qsort_ret[@]" )
qsort "$larger[@]"
larger=( "$qsort_ret[@]" )
qsort_ret=( "$smaller[@]" "$pivot" "$larger[@]" )
用作,例如,
$ array=(a c b f 3 5)
$ qsort "$array[@]"
$ declare -p qsort_ret
declare -a qsort_ret='([0]="3" [1]="5" [2]="a" [3]="b" [4]="c" [5]="f")'
这个实现是递归的……所以这里是一个迭代快速排序:
#!/bin/bash
# quicksorts positional arguments
# return is in array qsort_ret
# Note: iterative, NOT recursive! :)
qsort()
(($#==0)) && return 0
local stack=( 0 $(($#-1)) ) beg end i pivot smaller larger
qsort_ret=("$@")
while (($#stack[@])); do
beg=$stack[0]
end=$stack[1]
stack=( "$stack[@]:2" )
smaller=() larger=()
pivot=$qsort_ret[beg]
for ((i=beg+1;i<=end;++i)); do
if [[ "$qsort_ret[i]" < "$pivot" ]]; then
smaller+=( "$qsort_ret[i]" )
else
larger+=( "$qsort_ret[i]" )
fi
done
qsort_ret=( "$qsort_ret[@]:0:beg" "$smaller[@]" "$pivot" "$larger[@]" "$qsort_ret[@]:end+1" )
if (($#smaller[@]>=2)); then stack+=( "$beg" "$((beg+$#smaller[@]-1))" ); fi
if (($#larger[@]>=2)); then stack+=( "$((end-$#larger[@]+1))" "$end" ); fi
done
在这两种情况下,你都可以改变你使用的顺序:我用的是字符串比较,但是你可以用算术比较,比较wrt文件修改时间等等,只要使用适当的测试即可;你甚至可以让它更通用,并让它使用第一个参数,即测试函数使用,例如,
#!/bin/bash
# quicksorts positional arguments
# return is in array qsort_ret
# Note: iterative, NOT recursive! :)
# First argument is a function name that takes two arguments and compares them
qsort()
(($#<=1)) && return 0
local compare_fun=$1
shift
local stack=( 0 $(($#-1)) ) beg end i pivot smaller larger
qsort_ret=("$@")
while (($#stack[@])); do
beg=$stack[0]
end=$stack[1]
stack=( "$stack[@]:2" )
smaller=() larger=()
pivot=$qsort_ret[beg]
for ((i=beg+1;i<=end;++i)); do
if "$compare_fun" "$qsort_ret[i]" "$pivot"; then
smaller+=( "$qsort_ret[i]" )
else
larger+=( "$qsort_ret[i]" )
fi
done
qsort_ret=( "$qsort_ret[@]:0:beg" "$smaller[@]" "$pivot" "$larger[@]" "$qsort_ret[@]:end+1" )
if (($#smaller[@]>=2)); then stack+=( "$beg" "$((beg+$#smaller[@]-1))" ); fi
if (($#larger[@]>=2)); then stack+=( "$((end-$#larger[@]+1))" "$end" ); fi
done
那么就可以有这个比较功能了:
compare_mtime() [[ $1 -nt $2 ]];
并使用:
$ qsort compare_mtime *
$ declare -p qsort_ret
使当前文件夹中的文件按修改时间排序(最新的优先)。
注意。这些函数是纯 Bash!没有外部实用程序,也没有子shell!对于您可能拥有的任何有趣符号(空格、换行符、全局字符等),它们都是安全的。
注意2。测试[[ $i < $pivot ]]
是正确的。它使用字典字符串比较。如果您的数组只包含整数并且您想按数字排序,请改用((i < pivot))
。
请不要编辑这个答案来改变它。它已经被编辑(并回滚)了几次。我这里给出的测试是正确的,并且与示例中给出的输出相对应:示例同时使用了字符串和数字,目的是按字典顺序对其进行排序。在这种情况下使用((i < pivot))
是错误的。
【讨论】:
感谢令人印象深刻的 Bashing,它在输入元素和排序标准方面提供了极大的灵活性。如果使用sort
提供的排序选项进行基于行的排序就足够了,那么sort
+ read -a
解决方案将从大约20 个项目开始更快,并且随着您处理的元素越多,速度会越来越快和。例如,在我 2012 年末运行 OSX 10.11.1 和 Fusion Drive 的 iMac 上:100 元素数组:ca。 0.03s 秒。 (qsort()
) 与 ca。 0.005 秒。 (sort
+ read -a
); 1000 元素数组:大约0.375 秒。 (qsort()
) 与 ca。 0.014 秒 (sort
+ read -a
)。
不错。我记得大学时代的快速排序,但也会研究冒泡排序。对于我的排序需要,我有第一个和第二个元素形成键,然后是一个数据元素(我稍后可能会扩展)。您的代码可以通过关键元素的数量 (parm1) 和数据元素的数量 (parm2) 来改进。对于 OP,参数是 1 和 0。对我来说,参数是 2 和 1。无论如何,你的答案最有希望。
使用我发现的未转换字符串整数数据集if [ "$i" -lt "$pivot" ]; then
是必需的,否则解析的“2”Inline Link.【参考方案4】:
如果不需要处理数组元素中的特殊shell字符:
array=(a c b f 3 5)
sorted=($(printf '%s\n' "$array[@]"|sort))
使用 bash 无论如何你都需要一个外部排序程序。
使用 zsh 不需要外部程序,并且可以轻松处理特殊的 shell 字符:
% array=('a a' c b f 3 5); printf '%s\n' "$(o)array[@]"
3
5
a a
b
c
f
ksh 有 set -s
以 ASCII 顺序排序。
【讨论】:
非常好的背景信息。我几乎会要求一个关于 ksh 如何使用 set -s 标志的演示......但话说回来,问题是关于 bash,所以这将是相当离题的 这应该适用于大多数 KornShell 实现(例如 ksh88 和 pdksh):set -A array x 'a a' d; set -s -- "$array[@]"; set -A sorted "$@"
并且,当然,set 命令会重置当前的位置参数,如果有的话。
你是贝壳知识的源泉。我敢肯定你一定有过照片的记忆或其他东西,因为这种细微的差异是人类物种的大多数其他成员所无法做到的 :),+1 以获得完整的信息包【参考方案5】:
tl;dr:
对数组a_in
进行排序并将结果存储在a_out
中(元素不能嵌入换行符[1]
):
Bash v4+:
readarray -t a_out < <(printf '%s\n' "$a_in[@]" | sort)
Bash v3:
IFS=$'\n' read -d '' -r -a a_out < <(printf '%s\n' "$a_in[@]" | sort)
优于antak's solution:
您不必担心意外的通配(将数组元素意外解释为文件名模式),因此不需要额外的命令来禁用通配(set -f
和set +f
稍后恢复它)。
您不必担心将IFS
重置为unset IFS
。[2]
选读:解释和示例代码
以上将 Bash 代码与外部实用程序 sort
结合在一起,以提供一种适用于任意单行元素和的解决方案>词汇或数字排序(可选按字段):
性能:对于大约 20 个或更多元素,这将比纯 Bash 解决方案更快 -一旦超过 100 个元素,就会显着增加。(确切的阈值取决于您的具体输入、机器和平台。)
它快速的原因是它避免了 Bash 循环。printf '%s\n' "$a_in[@]" | sort
执行排序(在词汇上,默认情况下 - 请参阅sort
's POSIX spec):
"$a_in[@]"
安全地扩展为数组a_in
的元素作为单个参数,无论它们包含什么(包括空格)。
printf '%s\n'
然后按原样在其自己的行上打印每个参数 - 即每个数组元素。
read
/ readarray
必须在 current shell 中运行(不得在 subshell 中运行),才能使输出变量 a_out
成为对当前 shell 可见(以便变量在脚本的其余部分中保持定义)。
将sort
的输出读入一个数组变量:
Bash v4+:readarray -t a_out
将 sort
输出的各个行读取到数组变量 a_out
的元素中,而不包括每个元素中的尾随 \n
(-t
)。
Bash v3:readarray
不存在,因此必须使用 read
:IFS=$'\n' read -d '' -r -a a_out
告诉 read
读入数组 (-a
) 变量 a_out
,读取整个输入,跨行 (-d ''
),但通过换行符将其拆分为数组元素(IFS=$'\n'
。$'\n'
,产生文字换行符 (LF),即所谓的ANSI C-quoted string)。
(-r
,实际上应该始终与read
一起使用的选项,禁用对\
字符的意外处理。)
带注释的示例代码:
#!/usr/bin/env bash
# Define input array `a_in`:
# Note the element with embedded whitespace ('a c')and the element that looks like
# a glob ('*'), chosen to demonstrate that elements with line-internal whitespace
# and glob-like contents are correctly preserved.
a_in=( 'a c' b f 5 '*' 10 )
# Sort and store output in array `a_out`
# Saving back into `a_in` is also an option.
IFS=$'\n' read -d '' -r -a a_out < <(printf '%s\n' "$a_in[@]" | sort)
# Bash 4.x: use the simpler `readarray -t`:
# readarray -t a_out < <(printf '%s\n' "$a_in[@]" | sort)
# Print sorted output array, line by line:
printf '%s\n' "$a_out[@]"
由于使用不带选项的sort
,这会产生词法排序(数字在字母之前排序,并且数字序列按词法处理,而不是数字):
*
10
5
a c
b
f
如果您希望 numerical 按第一个字段排序,您可以使用 sort -k1,1n
而不是仅使用 sort
,这会产生(非数字在数字之前排序,并且数字正确排序):
*
a c
b
f
5
10
[1] 要处理带有嵌入换行符的元素,请使用以下变体(Bash v4+,带有 GNU sort
):readarray -d '' -t a_out < <(printf '%s\0' "$a_in[@]" | sort -z)
.@987654325 @ 有一个 Bash v3 解决方案。
[2] 虽然在 Bash v3 变体中设置了 IFS
,但更改仅限于命令。
相比之下,在 antak 的回答中,IFS=$'\n'
后面的内容是 assignment 而不是命令,在这种情况下,IFS
的更改是全局。
【讨论】:
【参考方案6】:在从慕尼黑到法兰克福的 3 小时火车旅行中(因为慕尼黑啤酒节明天开始,我很难到达)我正在考虑我的第一篇文章。对于通用排序函数,使用全局数组是一个更好的主意。以下函数处理任意字符串(换行符、空格等):
declare BSORT=()
function bubble_sort()
#
# @param [ARGUMENTS]...
#
# Sort all positional arguments and store them in global array BSORT.
# Without arguments sort this array. Return the number of iterations made.
#
# Bubble sorting lets the heaviest element sink to the bottom.
#
(($# > 0)) && BSORT=("$@")
local j=0 ubound=$(($#BSORT[*] - 1))
while ((ubound > 0))
do
local i=0
while ((i < ubound))
do
if [ "$BSORT[$i]" \> "$BSORT[$((i + 1))]" ]
then
local t="$BSORT[$i]"
BSORT[$i]="$BSORT[$((i + 1))]"
BSORT[$((i + 1))]="$t"
fi
((++i))
done
((++j))
((--ubound))
done
echo $j
bubble_sort a c b 'z y' 3 5
echo $BSORT[@]
打印出来:
3 5 a b c z y
同样的输出是从
创建的BSORT=(a c b 'z y' 3 5)
bubble_sort
echo $BSORT[@]
请注意,Bash 内部可能使用智能指针,因此交换操作可能很便宜(尽管我对此表示怀疑)。但是,bubble_sort
表明,像 merge_sort
这样的更高级的功能也在 shell 语言的范围内。
【讨论】:
冒泡排序?哇.. 奥巴马说“冒泡排序是错误的方法”-> youtube.com/watch?v=k4RRi_ntQc8 好吧,看起来 O-guy 想要聪明一点,但他并没有感觉到这不是一个 50/50 的机会问题。作为 O-guy 的前任,让我们告诉他 B-guy,曾经做得更好(俄亥俄州雷诺兹堡,2000 年 10 月):“我认为,如果您知道自己的信仰,那么回答问题会容易得多. 我无法回答你的问题。”所以这个B-guy真的对布尔逻辑有所了解。 O-guy 没有。 通过使 BSORT 成为一个带有 nameref 的本地数组到要排序的任何数组,可以使函数更容易移植。即local -n BSORT="$1"
在函数的开头。然后你可以运行bubble_sort myarray
对myarray进行排序。【参考方案7】:
另一种使用外部sort
并处理任何 特殊字符的解决方案(NUL 除外:))。应该适用于 bash-3.2 和 GNU 或 BSD sort
(遗憾的是,POSIX 不包括 -z
)。
local e new_array=()
while IFS= read -r -d '' e; do
new_array+=( "$e" )
done < <(printf "%s\0" "$array[@]" | LC_ALL=C sort -z)
先看最后的输入重定向。我们使用内置的printf
来写出以零结尾的数组元素。引用确保数组元素按原样传递,并且 shell printf
的细节导致它为每个剩余参数重用格式字符串的最后一部分。也就是说,它相当于:
for e in "$array[@]"; do
printf "%s\0" "$e"
done
然后将以空结尾的元素列表传递给sort
。 -z
选项使其读取以空字符结尾的元素,对它们进行排序并输出以空字符结尾的元素。如果您只需要获取唯一元素,则可以传递-u
,因为它比uniq -z
更便携。 LC_ALL=C
确保独立于语言环境的稳定排序顺序——有时对脚本很有用。如果您希望 sort
尊重语言环境,请将其删除。
<()
构造获取要从生成的管道读取的描述符,<
将while
循环的标准输入重定向到它。如果你需要访问管道内的标准输入,你可以使用另一个描述符——读者练习:)。
现在,回到开头。 read
内置从重定向的标准输入读取输出。将IFS
设置为空会禁用分词,这在此处是不必要的——因此,read
会将输入的整个“行”读取到单个提供的变量。 -r
选项也会禁用此处不需要的转义处理。最后,-d ''
将行分隔符设置为 NUL——也就是说,告诉read
读取以零结尾的字符串。
因此,对于每个连续的以零结尾的数组元素执行一次循环,值存储在e
中。该示例只是将项目放在另一个数组中,但您可能更喜欢直接处理它们:)。
当然,这只是实现同一目标的众多方法之一。正如我所看到的,它比在 bash 中实现完整的排序算法更简单,并且在某些情况下它会更快。它处理所有特殊字符,包括换行符,并且应该适用于大多数常见系统。最重要的是,它可能会教你一些关于 bash 的新奇事物 :)。
【讨论】:
很好的解决方案和非常有用的解释,谢谢。一个扩展:如果不将 IFS 设置为空,则前导空格也将被消除 - 即使没有进行分词。 不要引入局部变量e
并设置空 IFS,而是使用 REPLY 变量。【参考方案8】:
试试这个:
echo $array[@] | awk 'BEGINRS=" "; print $1' | sort
输出将是:
3 5 一种 b C F问题解决了。
【讨论】:
应该编辑这个以将输出放入一个新数组中以完全回答他的问题。【参考方案9】:如果你可以为数组中的每个元素计算一个唯一的整数,像这样:
tab='0123456789abcdefghijklmnopqrstuvwxyz'
# build the reversed ordinal map
for ((i = 0; i < $#tab; i++)); do
declare -g ord_$tab:i:1=$i
done
function sexy_int()
local sum=0
local i ch ref
for ((i = 0; i < $#1; i++)); do
ch="$1:i:1"
ref="ord_$ch"
(( sum += $!ref ))
done
return $sum
sexy_int hello
echo "hello -> $?"
sexy_int world
echo "world -> $?"
那么,你可以使用这些整数作为数组索引,因为 Bash 总是使用稀疏数组,所以不用担心未使用的索引:
array=(a c b f 3 5)
for el in "$array[@]"; do
sexy_int "$el"
sorted[$?]="$el"
done
echo "$sorted[@]"
优点。快。
缺点。重复的元素被合并,并且不可能将内容映射到 32 位唯一整数。
【讨论】:
有趣的技术,我使用了一种变体来查找最大/最小值,而无需显式比较/排序。 但是,不考虑长度的非加权加法不起作用:“z”排在“aaaa”之前,所以你不能将它用于上面显示的单词。【参考方案10】:最小排序:
#!/bin/bash
array=(.....)
index_of_element1=0
while (( $index_of_element1 < $#array[@] )); do
element_1="$array[$index_of_element1]"
index_of_element2=$((index_of_element1 + 1))
index_of_min=$index_of_element1
min_element="$element_1"
for element_2 in "$array[@]:$((index_of_element1 + 1))"; do
min_element="`printf "%s\n%s" "$min_element" "$element_2" | sort | head -n+1`"
if [[ "$min_element" == "$element_2" ]]; then
index_of_min=$index_of_element2
fi
let index_of_element2++
done
array[$index_of_element1]="$min_element"
array[$index_of_min]="$element_1"
let index_of_element1++
done
【讨论】:
【参考方案11】:array=(a c b f 3 5)
new_array=($(echo "$array[@]" | sed 's/ /\n/g' | sort))
echo $new_array[@]
new_array 的回显内容为:
3 5 a b c f
【讨论】:
【参考方案12】:对于常见的空格和换行问题有一个解决方法:
使用不在原始数组中的字符(如$'\1'
或$'\4'
或类似字符)。
这个函数完成了工作:
# Sort an Array may have spaces or newlines with a workaround (wa=$'\4')
sortarray() local wa=$'\4' IFS=''
if [[ $* =~ [$wa] ]]; then
echo "$0: error: array contains the workaround char" >&2
exit 1
fi
set -f; local IFS=$'\n' x nl=$'\n'
set -- $(printf '%s\n' "$@//$nl/$wa" | sort -n)
for x
do sorted+=("$x//$wa/$nl")
done
这将对数组进行排序:
$ array=( a b 'c d' $'e\nf' $'g\1h')
$ sortarray "$array[@]"
$ printf '<%s>\n' "$sorted[@]"
<a>
<b>
<c d>
<e
f>
<gh>
这将抱怨源数组包含解决方法字符:
$ array=( a b 'c d' $'e\nf' $'g\4h')
$ sortarray "$array[@]"
./script: error: array contains the workaround char
说明
我们设置了两个局部变量wa
(workaround char)和一个空 IFS
然后(ifs 为 null)我们测试整个数组 $*
。
不包含任何 woraround char [[ $* =~ [$wa] ]]
。
如果是,请发出消息并发出错误信号:exit 1
避免文件名扩展:set -f
设置 IFS 的新值 (IFS=$'\n'
)、循环变量 x
和换行符 (nl=$'\n'
)。
我们打印接收到的参数的所有值(输入数组$@
)。
但我们将任何新行替换为解决方法 char "$@//$nl/$wa"
。
发送这些值以进行排序sort -n
。
并将所有排序值放回位置参数set --
。
然后我们一一分配每个参数(以保留换行符)。
在循环中for x
到一个新数组:sorted+=(…)
在引号内保留任何现有的换行符。
将解决方法恢复为换行符"$x//$wa/$nl"
。
完成
【讨论】:
【参考方案13】:This question 看起来密切相关。顺便说一句,这是 Bash 中的合并排序(没有外部进程):
mergesort()
local -n -r input_reference="$1"
local -n output_reference="$2"
local -r -i size="$#input_reference[@]"
local merge previous
local -a -i runs indices
local -i index previous_idx merged_idx \
run_a_idx run_a_stop \
run_b_idx run_b_stop
output_reference=("$input_reference[@]")
if ((size == 0)); then return; fi
previous="$output_reference[0]"
runs=(0)
for ((index = 0;;)) do
for ((++index;; ++index)); do
if ((index >= size)); then break 2; fi
if [[ "$output_reference[index]" < "$previous" ]]; then break; fi
previous="$output_reference[index]"
done
previous="$output_reference[index]"
runs+=(index)
done
runs+=(size)
while (("$#runs[@]" > 2)); do
indices=("$!runs[@]")
merge=("$output_reference[@]")
for ((index = 0; index < "$#indices[@]" - 2; index += 2)); do
merged_idx=runs[indices[index]]
run_a_idx=merged_idx
previous_idx=indices[$((index + 1))]
run_a_stop=runs[previous_idx]
run_b_idx=runs[previous_idx]
run_b_stop=runs[indices[$((index + 2))]]
unset runs[previous_idx]
while ((run_a_idx < run_a_stop && run_b_idx < run_b_stop)); do
if [[ "$merge[run_a_idx]" < "$merge[run_b_idx]" ]]; then
output_reference[merged_idx++]="$merge[run_a_idx++]"
else
output_reference[merged_idx++]="$merge[run_b_idx++]"
fi
done
while ((run_a_idx < run_a_stop)); do
output_reference[merged_idx++]="$merge[run_a_idx++]"
done
while ((run_b_idx < run_b_stop)); do
output_reference[merged_idx++]="$merge[run_b_idx++]"
done
done
done
declare -ar input=(z..az..a)
declare -a output
mergesort input output
echo "$input[@]"
echo "$output[@]"
【讨论】:
【参考方案14】:我不相信您需要在 Bash 中使用外部排序程序。
这是我对简单冒泡排序算法的实现。
function bubble_sort()
#
# Sorts all positional arguments and echoes them back.
#
# Bubble sorting lets the heaviest (longest) element sink to the bottom.
#
local array=($@) max=$(($# - 1))
while ((max > 0))
do
local i=0
while ((i < max))
do
if [ $array[$i] \> $array[$((i + 1))] ]
then
local t=$array[$i]
array[$i]=$array[$((i + 1))]
array[$((i + 1))]=$t
fi
((i += 1))
done
((max -= 1))
done
echo $array[@]
array=(a c b f 3 5)
echo " input: $array[@]"
echo "output: $(bubble_sort $array[@])"
这应该打印:
input: a c b f 3 5
output: 3 5 a b c f
【讨论】:
冒泡排序是O(n^2)
。我似乎记得大多数排序算法使用 O(n lg(n))
直到最后十几个元素。对于最终元素,使用选择排序。【参考方案15】:
a=(e b 'c d')
shuf -e "$a[@]" | sort >/tmp/f
mapfile -t g </tmp/f
【讨论】:
【参考方案16】:非常感谢在我之前回答的人。使用他们出色的输入、bash 文档和其他方面的想法,这对我来说是完美的,无需 IFS 更改
array=("a \n c" b f "3 5")
在 bash 中使用进程替换和读取数组 > v4.4 WITH EOL 字符
readarray -t sorted < <(sort < <(printf '%s\n' "$array[@]"))
在 bash 中使用进程替换和读取数组 > v4.4 WITH NULL 字符
readarray -td '' sorted < <(sort -z < <(printf '%s\0' "$array[@]"))
最后我们验证
printf "[%s]\n" "$sorted[@]"
输出是
[3 5]
[a \n c]
[b]
[f]
请让我知道这是否是对嵌入式 /n 的正确测试,因为两种解决方案都会产生相同的结果,但第一个解决方案不应该与嵌入式 /n 一起正常工作
【讨论】:
【参考方案17】:array=(z 'b c'); set "$array[@]"; printf '%s\n' "$@"; \
| sort \
| mapfile -t array; declare -p array
declare -a array=([0]="b c" [1]="z")
打开一个内联函数 ...
以获取一组新的位置参数(例如 $1
、$2
等)。
将数组复制到位置参数。 (例如,set "$array[@]"
会将第 n 个数组参数复制到第 n 个位置参数。注意引号保留了数组元素中可能包含的空格)。
打印每个位置参数(例如,printf '%s\n' "$@"
将在自己的行上打印每个位置参数。同样,请注意引号保留每个位置参数中可能包含的空白)。
然后sort
做它的事。
使用 mapfile 将流读入数组(例如,mapfile -t array
将每一行读入变量 array
,-t
忽略每行中的 \n
)。
转储数组以显示其已排序。
作为一个函数:
set +m
shopt -s lastpipe
sort_array()
declare -n ref=$1
set "$ref[@]"
printf '%s\n' "$@"
| sort \
| mapfile -t $ref
然后
array=(z y x); sort_array array; declare -p array
declare -a array=([0]="x" [1]="y" [2]="z")
我期待着被所有 UNIX 大师撕碎! :)
【讨论】:
你测试过代码吗?这是行不通的。$ref
是运行mapfile
的子shell 的本地,它不会影响父shell。可能是,您在 zsh 或 fish 下测试了代码 - 问题是关于 bash。【参考方案18】:
sorted=($(echo $array[@] | tr " " "\n" | sort))
本着 bash / linux 的精神,我会为每个步骤提供最好的命令行工具。 sort
完成主要工作,但需要用换行符而不是空格分隔输入,所以上面非常简单的管道就可以了:
回显数组内容 --> 用换行符替换空格 --> 排序
$()
是回显结果
($())
是将“回显结果”放入数组中
注意:正如@sorontar 在comment 中提到的另一个问题:
sorted=($(...)) 部分使用“split and glob”运算符。您应该关闭 glob:set -f 或 set -o noglob 或 shopt -op noglob 或像 * 这样的数组元素将扩展为文件列表。
【讨论】:
本着 bash / linux 的精神:我猜你根本没有理解这种精神。您的代码已完全损坏(路径名扩展和分词)。这会更好(Bash≥4):mapfile -t sorted < <(printf '%s\n' "$array[@]" | sort)
,否则sorted=(); while IFS= read -r line; do sorted+=( "$line" ); done < <(printf '%s\n' | sort)
。
您正在使用的反模式是:echo $array[@] | tr " " "\n"
:如果数组的字段包含空格和全局字符,这将中断。此外,它会生成一个子外壳并使用无用的外部命令。由于echo
很笨,如果您的数组以-e
、-E
或-n
开头,它将中断。改为使用:printf '%s\n' "$array[@]"
。另一个反模式是:($())
是将“回显结果”放入数组中。当然不是!这是一个可怕的反模式,由于路径名扩展(通配符)和分词而中断。永远不要使用这种恐怖。
最佳答案是“可怕的反模式”。以及对别人对您自己回答的问题的回答投反对票的方式。
关于陷阱的精彩讨论!以上是关于如何在 Bash 中对数组进行排序的主要内容,如果未能解决你的问题,请参考以下文章