假设我有一个关联数组bash
,
declare -A hash
hash=(
["foo"]=aa
["bar"]=bb
["baz"]=aa
["quux"]=bb
["wibble"]=cc
["wobble"]=aa
)
其中键和值对我来说都是未知的(实际数据是从外部源读取的)。
我如何创建一个对应于相同值的键数组,以便我可以在所有唯一值的循环中执行
printf 'Value "%s" is present with the following keys: %s\n' "$value" "${keys[*]}"
并获取输出(不一定按此顺序)
Value "aa" is present with the following keys: foo baz wobble
Value "bb" is present with the following keys: bar quux
Value "cc" is present with the following keys: wibble
重要的是,键作为单独的元素存储在keys
数组中,因此不需要从文本字符串中解析它们。
我可以做类似的事情
declare -A seen
seen=()
for value in "${hash[@]}"; do
if [ -n "${seen[$value]}" ]; then
continue
fi
keys=()
for key in "${!hash[@]}"; do
if [ "${hash[$key]}" = "$value" ]; then
keys+=( "$key" )
fi
done
printf 'Value "%s" is present with the following keys: %s\n' \
"$value" "${keys[*]}"
seen[$value]=1
done
但双循环的效率似乎有点低。
我是否遗漏了一段数组语法bash
?
在eg中这样做会让zsh
我获得更强大的数组操作工具吗?
在 Perl 中,我会做
my %hash = (
'foo' => 'aa',
'bar' => 'bb',
'baz' => 'aa',
'quux' => 'bb',
'wibble' => 'cc',
'wobble' => 'aa'
);
my %keys;
while ( my ( $key, $value ) = each(%hash) ) {
push( @{ $keys{$value} }, $key );
}
foreach my $value ( keys(%keys) ) {
printf( "Value \"%s\" is present with the following keys: %s\n",
$value, join( " ", @{ $keys{$value} } ) );
}
但bash
关联数组不能容纳数组......
我也对任何可能使用某种形式的间接索引的老式解决方案感兴趣(在读取我上面所说的值时构建一组索引数组hash
?)。感觉应该有一种方法可以在线性时间内做到这一点。
答案1
桀骜
反转键 <=> 值
在 中zsh
,定义哈希的主要语法类似于hash=(k1 v1 k2 v2...)
(perl
较新的版本还支持笨拙的 ksh93/bash 语法以实现兼容性,尽管在引用键时存在变化)
keys=( "${(@k)hash}" )
values=( "${(@v)hash}" )
typeset -A reversed
reversed=( "${(@)values:^keys}" ) # array zipping operator
或者使用Oa
参数扩展标志来反转键+值列表的顺序:
typeset -A reversed
reversed=( "${(@kvOa)hash}" )
或使用循环:
for k v ( "${(@kv}hash}" ) reversed[$v]=$k
和 双引号@
是为了保留空键和值(注意bash
关联数组不支持空键)。由于关联数组中元素的扩展没有特定的顺序,如果 的多个元素$hash
具有相同的值(最终将成为 中的键$reversed
),您无法判断哪个键将用作 中的值$reversed
。
为你的循环
您可以使用R
哈希下标标志来获取基于值而不是键的元素,并结合e
精确(而不是通配符)匹配,然后使用k
参数扩展标志获取这些元素的键:
for value ("${(@u)hash}")
print -r "elements with '$value' as value: ${(@k)hash[(Re)$value]}"
你的 Perl 方法
zsh
(与 相反ksh93
)不支持数组数组,但它的变量可以包含 NUL 字节,因此如果元素不包含 NUL 字节,则可以使用它来分隔元素,或者使用${(q)var}
/${(Q)${(z)var}}
对列表进行编码/解码使用引用。
typeset -A seen
for k v ("${(@kv)hash}")
seen[$v]+=" ${(q)k}"
for k v ("${(@kv)seen}")
print -r "elements with '$k' as value: ${(Q@)${(z)v}}"
克什93
ksh93 是 1993 年第一个引入关联数组的 shell。整体赋值的语法意味着以编程方式执行此操作非常困难zsh
,但至少在 ksh93 中它ksh93
支持复杂的嵌套数据结构,这在某种程度上是合理的。
特别是,这里 ksh93 支持数组作为哈希元素的值,因此您可以执行以下操作:
typeset -A seen
for k in "${!hash[@]}"; do
seen[${hash[$k]}]+=("$k")
done
for k in "${!seen[@]}"; do
print -r "elements with '$k' as value ${x[$k][@]}"
done
巴什
bash
几十年后添加了对关联数组的支持,复制了 ksh93 语法,但没有复制其他高级数据结构,并且没有 zsh 的任何高级参数扩展运算符。
在 中bash
,您可以使用引用列表zsh 中提到的使用printf %q
或使用较新版本的方法${var@Q}
。
typeset -A seen
for k in "${!hash[@]}"; do
printf -v quoted_k %q "$k"
seen[${hash[$k]}]+=" $quoted_k"
done
for k in "${!seen[@]}"; do
eval "elements=(${seen[$k]})"
echo -E "elements with '$k' as value: ${elements[@]}"
done
然而,如前所述,关联数组不支持空值作为键,因此如果某些值为空,则bash
关联数组将不起作用。$hash
您可以选择将空字符串替换为某些占位符,例如,<EMPTY>
或者在键前添加一些您稍后将删除以供显示的字符。
答案2
我相信您知道,绊脚石是在将索引数组的名称作为(另一个)变量的值时获取索引数组的整个值。我不能用少于中间值的格式来做到这一点${v[@]}
,然后对其使用 eval 。所以,这是这种方法:
declare -A keys
N=0 # counter for the index variables IX1, IX2, IX3, ...
for key in "${!hash[@]}"; do
value="${hash[$key]}"
if [ -z "${keys[$value]}" ] ; then N=$((N+1)) ; keys[$value]=IX$N ; fi
index="${keys[$value]}" # 'index' is now name of index variable
X="\${$index[@]}"
eval "$index=( $X $key )" # adding next key to it
done
for value in "${!keys[@]}" ; do
index=${keys[$value]}
X="\${$index[@]}"
printf "Value %s is present with the following keys: %s\n" \
"$value" "$(eval echo "$X")"
done
这是针对 Linux 的bash
。它为遇到的各种值创建索引数组IX1
,IX2
等等,并将这些名称保存在keys
值的关联数组中。因此,${keys[$value]}
是保存该值的键的索引数组的名称。然后X
设置为值集合的变量“访问短语”,允许eval echo "$X"
转换为带有空格分隔的那些值。例如,如果一个值有索引 array IX2
,那么X
将是 string ${IX2[@]}
。
我相信zsh
在不支持数组数组方面是类似的,所以它可能需要类似的解决方案。恕我直言,访问短语zsh
稍微清晰一些。
答案3
这是另一种方法 - 将数据存储在两个索引数组中。其中之一具有唯一值,第二个可以包含重复/重复值。人们可以构造关联数组,其中将第二个数组中的重复元素作为键,并将第一个数组中的相应条目作为由空格分隔的值。
下面的代码避免使用eval
并且仅使用for
循环
代码
source=("foo" "bar" "baz" "quux" "wibble" "wobble")
destination=("aa" "bb" "aa" "bb" "cc" "aa")
declare -A inverted_array
# Printout formatted arrays with headers
printf '%-10s %-20s %-30s\n' "Index" "Destination" "Source"
for ((i = ((${#source[@]} - 1)); i >= 0; i--)); do
source_i="${source["$i"]}"
destination_i="${destination["$i"]}"
printf '%-10s %-20s %-30s\n' "$i" "$destination_i" "$source_i"
tempstring="${inverted_array["$destination_i"]}"
inverted_array["$destination_i"]="$source_i"" ""$tempstring"
done
echo
printf '%-10s %-20s\n' "Key" "Value"
# Remove the last space from the every element of the resulted array and print it formatted
for index in "${!inverted_array[@]}"; do
removespace="${inverted_array[$index]}"
removespace=${removespace%" "}
inverted_array["$index"]="$removespace"
printf '%-10s %-20s\n' "$index" "${inverted_array["$index"]}"
done
echo
输出:
Index Destination Source
5 aa wobble
4 cc wibble
3 bb quux
2 aa baz
1 bb bar
0 aa foo
Key Value
bb bar quux
aa foo baz wobble
cc wibble
PS 为了进一步扩展/演示上面的示例 - 下面是生成两个数组的代码。其中一个 -source
包含 5 个字符长的随机字符,第二个 -destination
仅包含随机一个字符 0-9a-f 作为值。
生成两个索引数组(每个数组包含 100 个元素)的代码:
for ((i = 0; i < 100; i++)); do
source+=("$(tr -dc 'a-zA-Z' </dev/urandom | head -c 5)")
destination+=("$(tr -dc '0-9a-f' </dev/urandom | head -c 1)")
done
使用上面的代码创建关联数组,结果如下:
Key Value
9 soxRg PmUZv eOmkR cFuie wmlsO EdNdM XuloF SSfjE oHfnc FcIKE
8 hLRpa eXODM wRGkh MwZUW lfWaE WQiwU IHGjj nNEcg
7 Pdxmd ywPZQ lPQIx TKawd VTyqR
6 lIwla Docxu Dimnz ovywP HwzQv
5 ObezH tyFNS BqnWp CFlMk dDkYC
4 rNzLM GVLXH AgZSL ionEp tngzQ
3 yRfqn IdTne
2 sMSxm WKmGm ELjOL pqxqw stWnL
1 yxycd EAGRg WxBle ItLNz WUdVu shUaC qDNIO xIwdM
0 OXdHh VQcsT AFvFq sgrYK AQrjZ
f uXJor IkwDr AOGSK hYMGE PQQfu tUjbh NwrVi iqZKO hHLYU
e XhMpB TCCFr ATbxa
d ReqMh lbxFx bGivd YCGtv lAtZj
c Kvthr itbaF wIbaf LwUiB VTInv xvWbC gpyRZ
b riimt EkLbv QYpZq kgvTi tOJRH jZykW pRuMD FJVXZ xipDx wkCMN
a REJnb Xtunv raimk SemnZ xMwno EXwKi sekmg WUKhx
答案4
使用乐(以前称为 Perl_6)
my %hash = (
'foo' => 'aa',
'bar' => 'bb',
'baz' => 'aa',
'quux' => 'bb',
'wibble' => 'cc',
'wobble' => 'aa'
);
my %inverted = %hash.classify( { .value }, :as{ .key } );
for %inverted.kv -> $k, $v {
printf( "Value \"%s\" is present with the following keys: %s\n",
$k, $v ) };
输出:
Value "aa" is present with the following keys: wobble baz foo
Value "bb" is present with the following keys: bar quux
Value "cc" is present with the following keys: wibble
简而言之,这里工作的关键是使用 Raku 的classify
例程完成,该例程%hash
根据元素的.value
组件测试元素,并将等效值分类:as
为.key
。
完成大部分工作的单行代码如下(可以在 Raku REPL 中运行):
.say for %hash.classify: {.value}, :as{.key};
cc => [wibble]
aa => [baz wobble foo]
bb => [quux bar]
classify
附录(1):Raku 具有类似于 的功能categorize
。对于上面的代码,classify
可以替换为categorize
具有相同的结果。
附录(2):如果你想%hash
从 重建原始对象%inverted
,你可以调用invert
它的例程。根据文档:invert
“和之间的区别antipairs
在于 invert 将列表值扩展为多个对。”
https://docs.raku.org/routine/classify
https://docs.raku.org/routine/categorize
https://docs.raku.org/routine/invert
https://raku.org