为什么awk似乎会将数组随机化?

4
如果您查看此awk测试的输出,您会发现awk中的数组似乎以某种随机模式打印出来。对于相同数量的输入,它似乎是以相同的顺序。为什么会这样做呢?
echo "one two three four five six" | awk '{for (i=1;i<=NF;i++) a[i]=$i} END {for (j in a) print j,a[j]}'
4 four
5 five
6 six
1 one
2 two
3 three

echo "P04637 1A1U 1AIE 1C26 1DT7 1GZH 1H26 1HS5 1JSP 1KZY 1MA3 1OLG 1OLH 1PES 1PET 1SAE 1SAF 1SAK 1SAL 1TSR 1TUP 1UOL 1XQH 1YC5 1YCQ" | awk '{for (i=1;i<=NF;i++) a[i]=$i} END {for (j in a) print j,a[j]}'
17 1SAF
4 1C26
18 1SAK
5 1DT7
19 1SAL
6 1GZH
7 1H26
8 1HS5
9 1JSP
10 1KZY
20 1TSR
11 1MA3
21 1TUP
12 1OLG
22 1UOL
13 1OLH
23 1XQH
14 1PES
1 P04637
24 1YC5
15 1PET
2 1A1U
25 1YCQ
16 1SAE
3 1AIE

Why does it do so, is there rule for this?


如果您想要固定顺序,请勿使用“x in y”,而是使用“for/while”,因为“x in y”无法保持原始顺序。 - Kent
4个回答

9

在GNU Awk用户指南中的8.数组-->8.5扫描数组的所有元素一节中,当涉及到for (value in array)语法时:

通过此语句访问数组元素的顺序由awk内部排列数组元素的方式决定,无法控制或更改。如果循环体中的语句添加了新元素到数组中,则可能会导致问题;不可预测for循环是否会到达它们。同样地,在循环中更改变量可能会产生奇怪的结果。最好避免这些情况。


如果您想按照存储顺序打印数组,则必须使用经典的for循环:

for (j=1; j<=NF; j++) print j,a[j]

例子:

$ awk '{for (i=1;i<=NF;i++) a[i]=$i} END {for (j=1; j<=NF; j++) print j,a[j]}' <<< "P04637 1A1U 1AIE 1C26 1DT7 1GZH 1H26 1HS5 1JSP 1KZY 1MA3 1OLG 1OLH 1PES 1PET 1SAE 1SAF 1SAK 1SAL 1TSR 1TUP 1UOL 1XQH 1YC5 1YCQ"
1 P04637
2 1A1U
3 1AIE
4 1C26
5 1DT7
6 1GZH
7 1H26
8 1HS5
9 1JSP
10 1KZY
11 1MA3
12 1OLG
13 1OLH
14 1PES
15 1PET
16 1SAE
17 1SAF
18 1SAK
19 1SAL
20 1TSR
21 1TUP
22 1UOL
23 1XQH
24 1YC5
25 1YCQ

代码和参考资料都很好,但这并不能解释为什么,这就是Jotne所问的。 - Chris Seymour
@Jotne 绝对不是,微妙之处不在于 value in array顺序。 如果顺序不重要,这是一个完全有效的构造。 - Chris Seymour
我想我错过了Jotne的评论。嗯,当然,for (i in a) 对于许多情况非常有用。如果顺序很重要,则无用。 - fedorqui
@fedorqui 不小心删掉了 :) 问题是我是否应该避免使用value in array - Jotne
没问题,@Jotne :) 正如sudo_O聪明地说的那样,你不应该这样做。它是一个非常有用的工具,只有在顺序很重要时才需要避免使用。 - fedorqui

5
Awk使用哈希表来实现关联数组。这只是这种特定数据结构的固有属性。特定元素存储在数组中的位置取决于该值的哈希。还需要考虑哈希表的实现方式。如果它在内存方面高效,它将使用模数函数或其他方法限制每个键存储的范围。您还可能会得到不同键的相互冲突的哈希值,因此会发生链接,这又会影响插入的顺序,具体取决于哪个键首先插入。
当适当地用于循环遍历每个键时,构造函数(key in array)是完全可以的,但您不能保证顺序,并且在循环中不应更新array,因为您可能会错误地多次处理array[key]。
关于哈希表的良好描述可在《Think Complexity》一书中找到。

4
问题在于您使用的获取数组索引的运算符,而不是数组存储在哈希表中的事实。 in运算符以随机(看起来)的顺序提供数组索引(这默认与哈希表相关,但这是一种实现选择,可以修改)。
显式以数字递增顺序提供数组索引的for循环也操作相同的哈希表,但无论如何都会按特定顺序生成输出。
这只是获得数组索引的两种不同方法,两种方法都适用于哈希表。
请查阅man awk并查找in运算符。
如果您想使用in运算符控制输出顺序,则可以通过填充PROCINFO["sorted_in"]来使用GNU awk(从4.0版本开始)。有关详细信息,请参见http://www.gnu.org/software/gawk/manual/gawk.html#Controlling-Array-Traversal
一些常见的访问数组索引的方法:

要以您不关心的顺序打印数组元素:

{a[$1]=$0} END{for (i in a) print i, a[i]}

如果数组的索引是数字且连续的,从1开始,要按照索引的数字顺序打印数组元素:

{a[++i]=$0} END{for (i=1;i in a;i++) print i, a[i]}

如果索引是数字但不连续的,可以按索引的数字顺序打印数组元素:

{a[$1]=$0; min=($1<min?$1:min); max=($1>max?$1:max)} END{for (i=min;i<=max;i++) if (i in a) print i, a[i]}

为了按照输入中看到的顺序打印数组元素:
{a[$1]=$0; b[++max]=$1} END{for (i=1;i <= max;i++) print b[i], a[b[i]]}

使用gawk 4.0+按照指定的索引顺序打印数组元素:

BEGIN{PROCINFO["sorted_in"]=whatever} {a[$1]=$0} END{for (i in a) print i, a[i]}

如果需要其他功能,请编写自己的代码或参考 gawk 中的 asort()asorti()


1
很好的概述。 在(i=1;i in a;i++)中,您是否使用in a代替i<=length(a),因为POSIX规范不支持数组的length()函数,或者还有其他优点? 关于whateverPROCINFO["sorted_in"]支持的值包括"@ind_str_asc""@ind_num_asc""@val_type_asc""@val_str_asc""@val_num_asc""@ind_str_desc""@ind_num_desc""@val_type_desc""@val_str_desc""@val_num_desc""@unsorted",或自定义比较函数的名称。 - mklement0
1
是的,我使用in a比gawk支持length(a)更久了,所以我已经习惯了它,并且它是可移植的,而且出于效率考虑(以及美观),我讨厌在循环中每次迭代都调用相同参数的相同函数来产生相同结果的想法。如果我不打算使用in a,那么我会在循环外部设置一个变量并进行测试。谢谢。 - Ed Morton
明白了,谢谢。但是在每次迭代中调用in a比调用length(a)更有效吗?无论如何,我明白了提前将数组大小缓存到变量中的要点。 - mklement0
1
我认为 in alength(a) 更有效率,因为 in a 只是内置的哈希查找,而 length(a) 则需要调用函数并计算数组中元素的数量。现在我感到很有动力去测试它。在一个包含 1,000,000 个元素的数组上进行了 3 次计时运行后发现,in 稍微快于 length,但仅仅只差一点:0m0.421s vs 0m0.437s - Ed Morton

0
如果你正在使用gawk或mawk,你也可以设置一个环境变量WHINY_USERS,它会在迭代之前对索引进行排序。
示例:
echo "one two three four five six" | WHINY_USERS=true awk '{for (i=1;i<=NF;i++) a[i]=$i} END {for (j in a) print j,a[j]}'
1 one
2 two
3 three
4 four
5 five
6 six

来自 mawk 的手册:

WHINY_USERS

这是一个未记录在案的 gawk 特性。它告诉 mawk 在开始迭代数组元素之前对数组索引进行排序。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接