web-dev-qa-db-ja.com

2つのリストを減算する方法(高速)?

2つのリストを減算する高速な方法は何ですか1。リストは小さいかもしれませんが、シェルでの直接的な方法かもしれません。または、リストが長くなる可能性があります。外部ツールの方が速いかもしれません。

次の2つのリストがあるとします。

list1=( 1 2 3 4 5 6 7 8 9 10 11 12 )
list2=( 1 2 3   5   7 8 9    11 12 )

List1からlist2のすべての要素を削除して、次と同等の結果のリスト(listr)を取得する方法:

listr=( 4 6 10 )

リストが大きい場合(大量のメモリを使用する可能性がある場合)も、リストはファイル内にある可能性があります。

この質問を簡潔にするために、私はすべてのアルゴリズムをコミュニティの回答に入れています。

そこで行われた複数のテストを読んでください。

マルチセット

元の質問は、繰り返しなしで、list2の完全なリスト(list1)の欠落している要素を見つけることを目的としていました。

ただし、リストが次の場合:

list1=( a a b b b c     d d   )
list2=(     b b   c c c d d e )

そして multiset 減算の定義は このページでは のようになり、期待される結果は次のとおりです。

listr= ( a a b )

アルゴリズム1と3のみが正しく機能します。
アルゴリズム2も4もこれを行うことはできません。
アルゴリズム5(comm)は、comm -23
アルゴリズム6(zsh)が失敗します。私はそれを動作させる方法を知りません。
アルゴリズム7(通信)。上記のように、-23機能します。

対称差の設定 定義のすべてのアルゴリズムを分析したわけではありません。

listr=( a a b c c e )

だが comm -3 list1.txt list2.txt | tr -d ' \t'機能します。


1 はい、テキストファイル(行のリスト)をシェルで処理することは悪い考えであることを知っています。設計上、遅いです。
しかし 避けられないように見える場合 があります。
私(私たち)は提案を受け入れています。

8
Isaac

commを使用すると、両方のリストに共通するものをすべて削除できます。

listr=($(comm -3 <(printf "%s\n" "${list1[@]}" | sort) <(printf "%s\n" "${list2[@]}" | sort) | sort -n))

これにより、両方のリストがcommが予期する順序で並べ替えられ、それらが比較され、どちらかのリストに固有のアイテムのみが出力され、数値順に再度並べ替えられます。

両方のリストが辞書式にソートされている場合( LC_COLLATE )、再度ソートすることを避けることができます:

listr=($(comm --nocheck-order -3 <(printf "%s\n" "${list1[@]}") <(printf "%s\n" "${list2[@]}")))

これは、比較する必要のある値がファイルに保存されている場合にもうまく機能します。

9
Stephen Kitt
#!/bin/zsh
list1=( 1 2 3 4 5 6 7 8 9 10 11 12 )
list2=( 1 2 3   5   7 8 9    11 12 )
listr=("${(@)list1:|list2}")
typeset -p listr
6
llua

概要:

  • 長いリストの場合、リストがすでにソートされている場合は、comm(alg7)が最速です。

  • Zshソリューションは(はるかに)最も高速ですifファイルが読み込まれない場合、つまり、リストが「メモリ内」にある場合。ただし、これはファイルから値を読み取る必要がある他のすべてのソリューションとの公平な比較ではありません。元のコード(ファイルを読み取る時間をテストから回避する)を、ファイルを読み取る時間も含むコードに変更しました。


これはコミュニティの回答です。各回答の時間のみを報告することを目的としています。

[〜#〜] do [〜#〜]ソリューション/オプションを編集して追加し、すべてを比較してください。


アルゴリズムのリスト

  • alg1:単純なループソリューション。
  • alg2:外部sortおよびuniq -uの使用
  • alg3:bashで文字列を処理します。
  • alg4:ソートされたリストで-vに参加(ありがとう @ Kusalananda
  • alg5:comm(ありがとう @ Stephen Kitt
  • alg6:zsh(ありがとう @ Llua
  • alg7:commですが、すでにソートされたファイルにあります(ありがとう @ Stephen Kitt

Zshマニュアルからのメモ:

$ {name:| arrayname}
arraynameが配列変数の名前(内容ではなくN.B.)の場合、arraynameに含まれる要素はすべて、nameの置換から削除されます。

テスト中

これを解決するにはいくつかの方法があるため、答えを(公平に)テストするための一般的なフレームワークが必要です。

いくつかのガイドライン(不当なものを見つけた場合は変更してください):

  • 適度な精度を得るのに十分な繰り返しを測定します。
  • 使用するシェルの内側を測定します(シェルのロード/アンロードは避けてください)。
  • 印刷しないか、/ dev/nullにリダイレクトすることにより、出力オーバーヘッドを回避します。

テストコード:

#!/bin/bash
alg1(){
         arr=( "${list1[@]}" )
         for i in "${list2[@]}"; do
             for j in "${!arr[@]}"; do
         if [[ "$i" == "${arr[j]}" ]]; then
             unset arr["$j"]
             break
         fi
             done
     done
     printf '%s ' "${arr[@]}"; echo
}

alg2(){
         arr=($({ printf '%s\n' "${list1[@]}" "${list2[@]}"; } | sort | uniq -u))
         printf '%s ' "${arr[@]}"; echo
}

alg3(){
         a=" $(printf '%s ' ${list1[@]})" # Watch the space at the start!!.
         for i in "${list2[@]}"; do
         a=${a/ "$i" / };
     done
     printf '%s\n' "$a"
}

alg4(){  join -v 1 list1.txt list2.txt ; }

alg5(){  #listr=$(
                    comm -3 <(printf "%s\n" "${list1[@]}" | sort) \
                            <(printf "%s\n" "${list2[@]}" | sort) |
                sort -n
     #)
      }

alg6(){ zsh -c '  alg6(){
                           list1=( $(cat list1.txt) )
                           list2=( $(cat list2.txt) )
                           listr=("${(@)list1:|list2}")
                           typeset -p listr
                        }
                  TIMEFMT="%E %U %S"
                  time ( for ((k=0;k<'"$1"';k++)); do alg6; done; )
                '
      }
#: <<-\_comment_
alg7(){ comm -23 list1.txt list2.txt; }

try(){ for ((k=0;k<$1;k++)); do "$2"; done; }

#list1=( 1 2 3 4 5 6 7 8 9 10 11 12 )
#list2=( 1 2 3   5   7 8 9    11 12 )

#list1=( a a b b b c     d d   )
#list2=(     b b   c c c d d e )

size=1000000
list1=( "0" $(seq 1 "$size") )
list2=( "${list1[@]}" ); unset "list2[123]" "list2[234]" "list2[345]"

printf '%s\n' "${list1[@]}" | sort >list1.txt
printf '%s\n' "${list2[@]}" | sort >list2.txt

repeats=${1:-10}; shift
out=${1:-no}    ; shift
(($#==0)) && set -- alg{1..7}

TIMEFORMAT='%R %U %S'
for   i
do    printf '%s ' "$i"
      if [[ $out == no ]]; then
      [[ $i != alg6 ]] &&
          time try "$repeats" "$i" >/dev/null ||
          time alg6 "$repeats" >/dev/null
      else
      [[ $i != alg6 ]] &&
          time try "$repeats" "$i"            ||
          time alg6 "$repeats"
      fi
done

結果:

短いリスト(コードで提示):

$ ./script
alg1 2.056 0.806 1.237
alg2 3.478 3.126 1.756
alg3 0.999 0.728 0.304
alg4 1.186 0.780 0.434
alg5 5.234 1.926 1.722
alg6 2.71s 1.64s 1.26s
     2.758 1.637 1.265
alg7 1.156 0.799 0.422

Alg6の時間は、zshによって報告された時間であり、その後bashによって報告された時間です。
また、ファイルの読み取りを関数から外部に削除すると、zshの実行時間は本当に短くなります(0.050)。

長いリスト

500個の値(10回の繰り返し)のみのリストをテストすると、alg1は非常に非効率的であることがわかります。さらなるテストからの削除:

alg1 4.149 3.471 0.657
alg2 0.145 0.055 0.063
alg3 0.219 0.180 0.009
alg4 0.039 0.015 0.003
alg5 0.149 0.018 0.027
alg6 0.06s 0.02s 0.02s
     0.087 0.030 0.018
alg7 0.033 0.008 0.008

5kの値(10回の繰り返し)をテストすると、alg3も非効率的であることがわかります。

alg2 0.590 0.526 0.187
alg3 12.957 12.888 0.044
alg4 0.098 0.047 0.008
alg5 0.654 0.028 0.036
alg6 0.16s 0.12s 0.04s
     0.211 0.118 0.044
alg7 0.038 0.022 0.014

5万個の値のテスト(10回の繰り返し):

alg2 6.487 5.838 1.611
alg4 0.488 0.469 0.019
alg5 5.073 0.250 0.056
alg6 1.42s 1.20s 0.21s
     1.467 1.206 0.216
alg7 0.271 0.247 0.014

500kの場合(10回の繰り返し)

alg4 5.471 5.269 0.156
alg6 15.14s 13.33s 1.91s
     15.215 13.335 1.926
alg7 2.833 2.655 0.138

1Mの場合(10回の繰り返し)

alg4 11.127 10.804 0.251
alg7 5.772 5.525 0.230
2
Isaac