【问题标题】:Check if an indexed array in bash is sparse or dense检查 bash 中的索引数组是稀疏还是密集
【发布时间】:2021-10-17 21:44:41
【问题描述】:

我在 bash 中有一个动态生成的索引数组,想知道它是 sparse 还是 dense
如果在最后一个条目之前存在未设置的索引,则数组是稀疏的。否则数组是密集的。

检查应该适用于所有情况,即使是空数组、非常大的数组(展开时超过 ARG_MAX),当然还有具有任意条目的数组(例如空条目或包含 *\、空格的条目和换行符)。后者应该相当简单,因为您可能不想扩展数组的值。

理想情况下,检查应该是高效且可移植的。

这里有一些用于检查您的解决方案的基本测试用例。 您的检查可以使用硬编码的全局变量名称 a 以与旧 bash 版本兼容。对于 bash 4.3 及更高版本,您可能需要改用 local -n isDense_array="$1",以便您可以指定要检查的数组。

isDense() {
  # INSERT YOUR CHECK HERE
  # if array `a` is dense, return 0 (success)
  # if array `a` is sparse, return any of 1-255 (failure) 
}
test() {
  isDense && result=dense || result=sparse 
  [[ "$result" = "$expected" ]] ||
  echo "Test in line $BASH_LINENO failed: $expected array considered $result"
}

expected=dense
a=(); test
a=(''); test
a=(x x x); test

expected=sparse
a=([1]=x); test
a=([1]=); test
a=([0]=x [2]=x); test
a=([4]=x [5]=x [6]=x); test
a=([0]=x [3]=x [4]=x [13]=x); test

要对您的检查进行基准测试,您可以使用

a=($(seq 9999999))
time {
  isDense
  unset 'a[0]'; isDense
  a[0]=1; unset 'a[9999998]'; isDense
  a=([0]=x [9999999999]=x); isDense
}

【问题讨论】:

    标签: arrays bash sparse-matrix


    【解决方案1】:

    方法

    非空密集数组的索引从0${#a[*]}-1。由于鸽巢原理,稀疏数组的last索引必须大于等于${#a[@]}

    Bash 脚本

    为了获得最后一个索引,我们假设索引列表${!a[@]} 是按升序排列的。 Bash 的手册没有指定任何顺序,但是(至少对于 bash 5 及以下版本)实现保证了这个顺序(在源代码文件array.c 中搜索array_keys_to_word_list)。

    isDense() {
      [[ "${#a[*]}" = 0 || " ${!a[*]}" == *" $((${#a[*]}-1))" ]]
    }
    

    对于小型数组,这非常有效。对于大型数组,由于${!a[*]},检查有点慢。该问题的基准测试耗时 9.8 秒。

    可加载的 Bash 内置

    这个答案中的方法只需要 last 索引。但是 bash 只允许使用 ${!a[*]} 提取 all 索引,这是不必要的缓慢。在内部,bash 知道最后一个索引是什么。因此,如果您愿意,您可以编写一个可加载的内置函数来访问 bash 的内部数据结构。

    当然,这不是一个真正实用的解决方案。如果性能真的那么重要,你不应该使用 bash 脚本。尽管如此,我还是写了一个这样的内置函数。

    Loadable bash builtin

    上述内置函数的空间和时间复杂度与数组的大小和结构无关。检查isdense a 应该和b=1 一样快。

    【讨论】:

    • isDense() { local IFS=,; (( i = (${!a[*]}), i && i == ${#a[*]} - 1 )); } 在极端情况下会稍微快一些。
    • 感谢您的建议。对于基准测试,这使用 9.399s 而不是 9.586s。但它也会导致a=() 的语法错误并导致测试a=('') 失败。
    • 我喜欢你写了一个内置的。我一直想尝试使用内置函数,它们似乎是一个很棒的(阅读:完全不切实际,仅限高级用户)功能,并没有得到很多使用。
    • 可能有一个内置函数返回数组的 max(index);或者可能是 nth from the end 索引(例如,将允许反向遍历索引 - 而不是打印/排序 ${!arr[@]});
    • @markp-fuso 检索最后一个数组索引很容易。写一个 maxindex 内置实际上是我的第一个计划,但后来我想,如果我可以一次实现所有东西,为什么要停在那里。另一方面,nth from the end 需要在内置函数中进行迭代,并且对于此任务不是必需的。
    【解决方案2】:

    更新:(重新)在 Ubuntu 20 虚拟机中运行测试(比之前在 cygwin/bash 环境中的测试要好得多;时间更接近 Socowi 报告的时间)


    注意:我在数组中填充了 1000 万个条目 ({0..9999999})。

    使用与 Socowi 相同的假设 ...

    要获取数组的最后一个索引,我们假设索引列表 ${!a[@]} 是按升序排列的。 Bash 的手册没有指定任何顺序,但是(至少对于 bash 5 及以下版本)实现保证了这个顺序

    ...我们可以对typeset -p a的输出做同样的假设,即输出是按索引排序的。


    查找最后一个索引:

    $ a[9999999]='[abcdef]'
    $ typeset -p a | tail -c 40
    9999998]="9999998" [9999999]="[abcdef]")
    

    第一次尝试使用awk 剥离最后一个索引:

    $ typeset -p a | awk '{delim="[][]"; split($NF,arr,delim); print arr[2]}'
    9999999
    

    这非常慢(cygwin/bash 需要 > 3.5 分钟),同时以 100%(单个)cpu 利用率运行并消耗约 4 GB 内存。

    第二次尝试使用tail -c 来限制提供给awk 的数据:

    $ typeset -p a | tail -c 40 | awk '{delim="[][]"; split($NF,arr,delim); print arr[2]}'
    9999999
    

    这以大约 1.6 秒(ubuntu/bash)的(相对)惊人的速度出现。

    第三次尝试在使用 awk 解析之前将最后一个数组值保存在局部变量中

    注意事项:

    • 正如 Socowi 所指出的(在 cmets 中),最后一个元素的内容可能包含多个字符(空格、括号、单/双引号等),这使得解析非常复杂
    • 一种解决方法是将最后一个数组值保存在一个变量中,解析typeset -p 输出,然后将值放回去
    • 可以通过array[-1] 访问最后一个数组值(需要bash 4.3+

    这也以大约 1.6 秒(ubuntu/bash)的(相对)惊人的速度出现:

    $ lastv="${a[-1]}"
    $ a[-1]=""
    $ typeset -p a | tail -c 40 |  awk -F'[][]' '{print $(NF-1)}'
    9999999
    $ a[-1]="${lastv}"
    

    功能:

    这为我们提供了以下isDense() 函数:

    初始函数

    unset -f isDense
    
    isDense() {
      local last=$(typeset -p a | tail -c 40 | awk '{delim="[][]"; split($NF,arr,delim); print arr[2]}')
      [[ "${#a[@]}" = 0 || "${#a[@]}" -ne "${last}" ]]
    }
    

    最新函数(变量=最后一个数组值)

    unset -f isDense
    
    isDense() {
      local lastv="${a[-1]}"
      a[-1]=""
      local last=$(typeset -p a | tail -c 40 | awk -F'[][]' '{print $(NF-1)}')
      [[ "${#a[@]}" = 0 || "${#a[@]}" -ne "${last}" ]]
      rc=$?
      a[-1]="${lastv}"
      return "${rc}"
    }
    

    基准测试:

    • 从问题(减去第 4 次测试 - 我厌倦了等待重新填充我的 1000 万个条目数组)这意味着 ...
    • 请记住,基准/测试调用 isDense() 3 次
    • ubuntu/bash 中运行每个函数几次,这是最好的时间...

    Socowi 函数:

    real    0m11.717s
    user    0m9.486s
    sys     0m1.982s
    

    oguz ismail 的功能:

    real    0m10.450s
    user    0m9.899s
    sys     0m0.546s
    

    我最初的typeset|tail-c|awk函数:

    real    0m4.514s
    user    0m3.574s
    sys     0m1.442s
    

    Socowi 的declare|tr|tail-n 函数的最新测试(变量=最后一个数组值):

    real    0m5.306s
    user    0m4.130s
    sys     0m2.670s
    

    使用原始typeset|tail-c|awk 函数的最新测试(变量=最后一个数组值):

    real    0m4.305s
    user    0m3.247s
    sys     0m1.761s
    

    【讨论】:

    • 有趣。尽管typeset -p a 必须打印比echo "${!a[*]}" 更多的时间,但在a=($(seq ...)) 的情况下,前者只需要后者的一小部分时间。但是,对于较长的值,typeset 变得越来越慢,而 ${!a[*]} 仅受条目数的影响。试试a=($(seq -f $(yes|tr -d \\n|head -c999)%f 999999)); time typeset -p a >/dev/null; time true "${!a[@]}"。但主要问题是,awk 可能会解析值而不是索引,例如在 a=([1]='[0]') 的情况下。
    • sure re: speed ... 这取决于 ...话又说回来,谁在他们的头脑中会(实际上)期望从 10+ 百万的条目中获得“出色”的表现bash数组?!?!?! :-) 至于awk 解析问题...我已经修改了逻辑以解决数组值中括号的问题...
    • 用于提取最后一个元素的新 awk 脚本也可以被欺骗。它为a=([1]=' [0]') 返回0。要使其健壮,需要解析 bash 引号,包括 "…"'…'$'…'。但即便如此,[lastIndex]="lastValue") 也必须少于 100 个字符。我找到了 bash 4.2 及更高版本的替代方案:last=("${a[-1]}"); last=$(declare -p last); last="${last#*=*0}n"; last=$(declare -p a | head -c -${#last} | tr \[ \\n | tail -n 1)。这仅比 tail -c100 | awk 慢一点(基准测试为 5.0s 而不是 4.7s)但安全。
    • 如果您可以临时修改数组a 并拥有bash 4.3 或更高版本,我之前的建议可以大大改进:tmp=${a[-1]}; a[-1]=; last=$(declare -p a | tr \[ \\n | tail -n 1); last=${last%]*}; a[-1]=$tmp。基准测试只需要 2.0 秒,而且更便携,因为它不需要 head -c(posix 未指定)。
    • 对于那些想知道为什么typeset/declare -p a${!a[*]} 快的人:两者都是通过遍历数组来实现的,但是declare 将结果存储在char* 缓冲区中,而${!使用make_word_list 创建一个链表,然后将其反转,然后转换为char *。所以后者至少迭代同一个数组三遍。有趣的是,这个过程对于[*][@] 是相同的。如果你写for i in "${!a[@]}" 之类的东西,bash 将创建一个单词表,将其转换为单个字符串,然后再次将该字符串解析为一个单词表。
    【解决方案3】:
    isDense() {
      local -rn array="$1"
      ((${#array[@]})) || return 0
      local -ai indices=("${!array[@]}")
      ((indices[-1] + 1 == ${#array[@]}))
    }
    

    遵循独立的调用约定:

    test() {
      local result
      isDense "$1" && result=dense || result=sparse 
      [[ "$result" = "$2" ]] ||
      echo "Test in line $BASH_LINENO failed: $2 array considered $result"
    }
    
    a=(); test a dense
    a=(''); test a dense
    a=(x x x); test a dense
    
    a=([1]=x); test a sparse
    a=([1]=); test a sparse
    a=([0]=x [2]=x); test a sparse
    a=([4]=x [5]=x [6]=x); test a sparse
    a=([0]=x [3]=x [4]=x [13]=x); test a sparse
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-08-16
      • 2017-11-08
      • 2018-04-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-03
      相关资源
      最近更新 更多