桀骜

桀骜

假设我有一个关联数组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。它为遇到的各种值创建索引数组IX1IX2等等,并将这些名称保存在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

相关内容