【问题标题】:Bash. The quickest and efficient array search重击。最快高效的数组搜索
【发布时间】:2017-07-07 22:31:05
【问题描述】:

我需要在 bash 进程中多次执行数组搜索。我需要知道什么是最快速有效的方法。我知道该怎么做。问题的重点在于如何以最快的方式做到这一点。现在,我正在这样做:

#!/bin/bash

array_test=("text1" "text2" "text3" "text4" "text5")
text_to_search="text4"

START=$(date +%s.%N)

for item in "${array_test[@]}"; do
    if [ ${item} = "${text_to_search}" ]; then
        echo "found!!"
        break
    fi
done

END=$(date +%s.%N)
DIFF=$(echo "$END - $START" | bc)
echo $DIFF

通过这段代码,我们可以测量时间。

假设我们有 300 个或更多的数组。有更快的方法吗?我需要提高性能。谢谢。

编辑我正在使用 bash 4.2 。真正的数组有换行符:

array_test=(
"text1"
"text2"
"text3"
"text4"
"text5"
)

【问题讨论】:

  • 10,000,000 个元素只需 30 秒!
  • 你的数组可以保持排序吗?您的搜索可以按排序顺序执行吗?您是否尝试过关联数组来真正让 Bash 进行检索?
  • 是的,数组按字母顺序排序,现在运行良好,但由于调试模式的原因,我计划对脚本中的每个函数进行搜索。我认为如果我这样做可能对性能不利。我会检查提案。谢谢。
  • 您可以通过使用 C 风格的 for 循环来迭代索引(假设数组不太稀疏)来获得不错的速度:for((i=0; i<=${#array[@]}; i++)); do item=${array[i]}; ...; done。这是因为 shell 在开始搜索之前不需要完全展开数组。
  • 数组声明中的换行符不会改变数组的内容(与元素本身内部的换行符相反,这是完全不同的事情)。

标签: arrays bash performance search


【解决方案1】:

最快版本(用于大型阵列)

array[*] 中使用grep -qFx
-q 禁止输出并存在于第一个匹配项中。
-F 搜索固定字符串而不是正则表达式。
-x 搜索整行而不是子字符串。这可确保搜索字符串 b 与元素 abc 不匹配。

if ( IFS=$'\n'; echo "${array[*]}" ) | grep -qFx "$text_to_search"; then
    echo "found!"
fi

假设:数组和要搜索的文本都不包含换行符。

也可以使用换行符搜索文本,只要有一个已知的未使用字符可以存储在 bash 变量中。我发现\x1E(ASCII 控制字符»记录分隔符«)效果很好。改编后的版本如下:

export d=$'\x1E' # unused character, here "Record Seperator"
if ( IFS="$d"; echo "$d${array[*]}$d" ) | grep -qF "$d$text_to_search$d"; then
    echo "found!"
fi

理论上,您可以通过在数组切片上使用多个并行grep 来进一步加快速度。然而,这太快了(见下面的结果),你可能永远不会遇到并行搜索得到回报的场景。

测试

我使用了一个大小为1'000'000的数组,生成如下:

size=$((10 ** 6))
array_test=($(seq -f 'text%.0f' 1 "$size"))

(顺便说一句:使用{1..1000000}seq数量级

搜索模式是所述数组的最后一个条目

text_to_search="text$size"

测试了三种搜索方式

  1. 您使用for 循环的方法
  2. printf %s\\n "array[@]" | grep -qFx
  3. (IFS=$'\n'; echo "array[*]";) | grep -qFx

结果如下:

  1. 65.5
  2. 59.3
  3. 00.4 秒(是的,小数点前是零)

【讨论】:

  • 你确定最后一个测试没有很快完成,因为${array_test[*]} 扩展为一个字符串,而 grep 在第一个匹配项上返回并且没有其他行可以匹配?根据我所做的快速测试,我很确定这是正在发生的事情。
  • 我将以不同的方式陈述我的评论:您的快速解决方案适用于您实际上不需要匹配数组元素的情况,并且匹配包含所有元素值的单个字符串就可以了。在某些情况下,这可能是可能的,但是您必须真正知道自己在做什么,并且一旦您的数组发生更改,您就必须重新进行大扩展(以构建大字符串),因此可能会失去性能优势如果数组被大量写入和读取。
  • @Fred 你是对的,所描述的方法(正如答案中已经指出的那样)做出了强有力的假设,但在所描述的场景中得到了解决。答案已更新,现在更安全,速度提高了两倍(!)(可能是由于管道 | 而不是 <<<)。
  • 我喜欢你的回答,但真正的数组有换行符。我编辑了我的问题。
  • 如果你也想要索引怎么办?
【解决方案2】:

如果您非常关心性能,也许 (a) Bash 不是最好的工具,并且 (b) 您应该尝试不同的方法并在您的数据上进行测试。话虽这么说,也许关联数组可以帮助您。

试试这个:

#!/bin/bash

declare -A array_test()
array_test=(["text1"]="" ["text2"]="" ["text3"]="" ["text4"]="" ["text5"]="")
text_to_search="text4"

if
  [[ ${array_test[$text_to_search]+found} ]]
then
  echo "Found!"
fi

请注意,在这种情况下,我使用键但为空值构建关联数组(将值设置为与键相同的值并占用更多内存并没有真正的用处)。

另一种方法是对数组进行排序,并使用某种二进制搜索。当然,这将涉及更多代码,如果 Bash 关联数组得到有效实现,可能会更慢。但是,再一次,没有什么比对实际数据进行测试来验证性能假设更好的了。

如果您有一个在键中包含信息的关联数组,您可以使用扩展来使用它们,就像使用值一样:

for key in "${!array[@]}"
do
  do_something_with_the key
done

此外,您可以使用循环构建数组,如果您从文件或命令的输出中读取元素,这将非常有用。举个例子:

declare -A array_test=()
while IFS= read -r key
do
  array_test[$key]=
done < <(command_with_output)

请注意,设置为 null(空)值的元素与未设置的元素不同。您可以通过以下扩展轻松看到:

declare -A array_test=()
array_test[existing_key]=
echo ${array_test[existing_key]+found} # Echoes "found"
echo ${array_test[missing_key]+found}  # Echoes nothing

"${var+value}" 扩展使用很有用,因为如果未设置变量,它会扩展为空,如果已设置,则扩展为 value。使用 set -u 捕获扩展未设置变量的尝试时不会产生错误。

【讨论】:

  • 您可以使用循环 declare -A array; for i in ${array_test[@]}; do array[$i]=1; done 创建关联数组。
  • @SLePort 我会在我的回答中添加这个,对于不熟悉关联数组的人来说可能并不明显。
  • 非常重要的引用数组扩展"${array_test[@]}" -- 否则包含空格的元素将不会被视为一个单元。
  • bash 4.3 及更高版本中,-v 运算符适用于数组索引:if [[ -v array_test[$text_to_search] ]]
  • 我编辑了问题,我正在使用 bash 4.2 谢谢!!
【解决方案3】:

如果您的数组元素都不包含空格,则可以使用模式匹配

array_test=("text1" "text2" "text3" "text4" "text5")
text_to_search="text4"

[[ " ${array_test[*]} " == *"  $text_to_search "*]] && echo found || echo not found

引号和空格在那里非常重要。

在 bash 中,在 [[ ]] 双括号内,== 运算符是 模式匹配 运算符,而不仅仅是字符串相等。

【讨论】:

  • 好主意,但你搞混了。要搜索的文本(不是扩展数组)应该被* 包围,并且搜索模式必须在== 的右侧。正确版本:[[ " ${array[*]} " == *\ $text\ * ]].
  • 在更改IFS 时,您的想法也可以与空格一起使用。
【解决方案4】:

基于有用的@Fred 答案使用关联数组的具体示例:

script.sh

#!/bin/bash

read -a array_test <<< $(seq 1 10000)
text_to_search="9999"

function from_associative_array {
  declare -A array
  for constant in ${array_test[@]}
  do
      array[$constant]=1
  done
  [[ ${array[$text_to_search]} ]] && echo  "Found in associative array";
}

function from_indexed_array {
  for item in "${array_test[@]}"; do
      if [ ${item} = "${text_to_search}" ]; then
          echo "Found in indexed array"
          break
      fi
  done
}

time from_indexed_array
time from_associative_array

$ bash script.sh
Found in indexed array

real    0m0.611s
user    0m0.604s
sys     0m0.004s

Found in associative array

real    0m0.297s
user    0m0.296s
sys     0m0.000s

【讨论】:

  • 我不认为你的测试在做同样的事情。第一个测试分配 10000 个元素并搜索一个,但第二个测试检索 10000 个元素。您应该首先构建数组,并且只对查找部分进行计时。您可以对两个数组的创建时间进行基准测试(关联应该更慢),但这与基准创建+查找一个选项并仅查找另一个选项不是“公平”的比较。
  • 顺便说一下,索引数组搜索(按照这里的编码方式完成)的复杂度是 O(n),而关联数组应该是 O(log n),这意味着数据集越大,差异越大。但是对于一个小数组,关联数组的性能可能比索引数组差。一般来说,由于在低数据量下的性能影响通常不是问题,因此更好的“大数据集”算法可能是最好的选择,特别是如果它可以简单地编码(即使用语言结构或标准库)。
  • @Fred 创建关联数组是第一个测试的一部分,因为初始数组是索引数组。在 OP 代码中,所有负载都在循环测试中,只需将所有值设置为 1 即可创建关联数组更快。那就是说你是对的,使用小数组循环索引数组会更快。但是对于 100 长度的数组,增益将为 0.001 毫秒。谁在乎...
  • @Fred 如果您误解了结果,请重新阅读我的答案。这里的关联数组测试更快。
  • 我必须假设 OP 从一开始就将数组构建为关联数组,但这最终不是我的决定。请注意,您可以分配一个空值(而不是 1)来创建元素,它也可以正常工作。我从你的测试中了解到 AA 更快,我只是指出测试的一部分比另一部分做得更多。
猜你喜欢
  • 1970-01-01
  • 2011-05-11
  • 2014-06-10
  • 2014-03-09
  • 1970-01-01
  • 2011-09-01
  • 2012-04-19
  • 2014-10-31
  • 2015-01-01
相关资源
最近更新 更多