web-dev-qa-db-ja.com

要素の長さに従って配列をバッシュソートしますか?

文字列の配列がある場合、各要素の長さに従って配列をソートしたいと思います。

例えば...

    array=(
    "tiny string"
    "the longest string in the list"
    "middle string"
    "medium string"
    "also a medium string"
    "short string"
    )

ソートする必要があります...

    "the longest string in the list"
    "also a medium string"
    "medium string"
    "middle string"
    "short string"
    "tiny string"

(おまけとして、リストが同じ長さの文字列をアルファベット順に並べ替えるといいですね。上記の例では、同じ長さでもmedium stringmiddle stringの前に並べ替えられました。しかし、それは「難しい」要件(ソリューションが複雑すぎる場合)。

配列がインプレースで並べ替えられている(つまり、「配列」が変更されている)場合、または新しい並べ替えられた配列が作成される場合は、問題ありません。

9
PJ Singh

文字列に改行が含まれていない場合は、次のように動作します。文字列自体を2次ソート基準として使用して、配列のインデックスを長さでソートします。

#!/bin/bash
array=(
    "tiny string"
    "the longest string in the list"
    "middle string"
    "medium string"
    "also a medium string"
    "short string"
)
expected=(
    "the longest string in the list"
    "also a medium string"
    "medium string"
    "middle string"
    "short string"
    "tiny string"
)

indexes=( $(
    for i in "${!array[@]}" ; do
        printf '%s %s %s\n' $i "${#array[i]}" "${array[i]}"
    done | sort -nrk2,2 -rk3 | cut -f1 -d' '
))

for i in "${indexes[@]}" ; do
    sorted+=("${array[i]}")
done

diff <(echo "${expected[@]}") \
     <(echo "${sorted[@]}")

実際のプログラミング言語に移行すると、ソリューションを大幅に簡略化できることに注意してください。 Perlでは、次のことができます

sort { length $b <=> length $a or $a cmp $b } @array
12
choroba
readarray -t array < <(
for str in "${array[@]}"; do
    printf '%d\t%s\n' "${#str}" "$str"
done | sort -k 1,1nr -k 2 | cut -f 2- )

これは、プロセス置換からソートされた配列の値を読み取ります。

プロセス置換にはループが含まれています。ループは、要素の長さとその間のタブ文字が前に付加された配列の各要素を出力します。

ループの出力は、最大から最小に数値でソートされます(長さが同じ場合はアルファベット順です。-k 2rの代わりに-k 2を使用して、アルファベット順を逆にします)およびthatcutに送信され、文字列の長さの列が削除されます。

テストスクリプトの後にテスト実行を並べ替えます。

array=(
    "tiny string"
    "the longest string in the list"
    "middle string"
    "medium string"
    "also a medium string"
    "short string"
)

readarray -t array < <(
for str in "${array[@]}"; do
    printf '%d\t%s\n' "${#str}" "$str"
done | sort -k 1,1nr -k 2 | cut -f 2- )

printf '%s\n' "${array[@]}"
$ bash script.sh
the longest string in the list
also a medium string
medium string
middle string
short string
tiny string

これは、文字列に改行が含まれていないことを前提としています。最近のbashを使用するGNUシステムでは、改行の代わりにヌル文字をレコード区切りとして使用することにより、データに埋め込まれた改行をサポートできます。

readarray -d '' -t array < <(
for str in "${array[@]}"; do
    printf '%d\t%s\0' "${#str}" "$str"
done | sort -z -k 1,1nr -k 2 | cut -z -f 2- )

ここでは、データはループ内で改行ではなく末尾の\0で出力され、sortおよびcut-z GNUオプションとreadarrayは、-d ''を使用してヌル区切りデータを最後に読み取ります。

9
Kusalananda

私は完全には繰り返さない bashでのソートについてすでに述べた 、あなただけがcanbash内でソートするが、多分あなたはすべきではない。以下は、挿入ソートのbashのみの実装で、O(n2)、したがって、これは小さなアレイでのみ許容されます。配列要素を長さの大きい順に並べ替えます。二次的なアルファベット順の並べ替えは行いません。

array=(
    "tiny string"
    "the longest string in the list"
    "middle string"
    "medium string"
    "also a medium string"
    "short string"
    )

function sort_inplace {
  local i j tmp
  for ((i=0; i <= ${#array[@]} - 2; i++))
  do
    for ((j=i + 1; j <= ${#array[@]} - 1; j++))
    do
      local ivalue jvalue
        ivalue=${#array[i]}
        jvalue=${#array[j]}
        if [[ $ivalue < $jvalue ]]
        then
                tmp=${array[i]}
                array[i]=${array[j]}
                array[j]=$tmp
        fi
    done
  done
}

echo Initial:
declare -p array

sort_inplace

echo Sorted:
declare -p array

これが特殊なソリューションであることの証拠として、さまざまなサイズのアレイに対する既存の3つの回答のタイミングを検討してください。

# 6 elements
Choroba: 0m0.004s
Kusalananda: 0m0.004s
Jeff: 0m0.018s         ## already 4 times slower!

# 1000 elements
Choroba: 0m0.004s
Kusalananda: 0m0.004s
Jeff: 0m0.021s        ## up to 5 times slower, now!

5000 elements
Choroba: 0m0.004s
Kusalananda: 0m0.004s
Jeff: 0m0.019s

# 10000 elements
Choroba: 0m0.004s
Kusalananda: 0m0.006s
Jeff: 0m0.020s

# 99000 elements
Choroba: 0m0.015s
Kusalananda: 0m0.012s
Jeff: 0m0.119s

ChorobaKusalananda は正しい考えを持っています:長さを一度計算し、ソートとテキスト処理に専用のユーティリティを使用します。

4
Jeff Schaller

これは、改行を含む配列要素も処理します。各要素の長さとインデックスのみをsortを通過させることで機能します。 bashおよびkshで動作するはずです。

_in=(
    "tiny string"
    "the longest
        string also containing
        newlines"
    "middle string"
    "medium string"
    "also a medium string"
    "short string"
)
out=()

unset IFS
for a in $(for i in ${!in[@]}; do echo ${#in[i]}/$i; done | sort -rn); do
        out+=("${in[${a#*/}]}")
done

printf '"%s"\n' "${out[@]}"
_

同じ長さの要素も辞書順にソートする必要がある場合は、ループを次のように変更できます。

_IFS='
'
for a in $(for i in ${!in[@]}; do printf '%s\n' "$i ${#in[i]} ${in[i]//$IFS/ }"; done | sort -k 2,2nr -k 3 | cut -d' ' -f1); do
        out+=("${in[$a]}")
done
_

これはsortにも文字列を渡します(改行はスペースに変更されます)が、それらのインデックスによって、コピー元からコピー先の配列にコピーされます。どちらの例でも、$(...)は数値(および最初の例の_/_文字)を含む行のみを表示するため、文字列内の文字またはスペースのグロビングによってトリップされることはありません。

4
mosvy

ハッカっぽい? (複雑)配列を長さで並べ替える高速な1行の方法
改行に対して安全およびスパース配列):

#!/bin/bash
in=(
    "tiny string"
    "the longest
        string also containing
        newlines"
    "middle string"
    "medium string"
    "also a medium string"
    "short string"
    "test * string"
    "*"
    "?"
    "[abc]"
)

readarray -td $'\0' sorted < <(
                    for i in "${in[@]}"
                    do     printf '%s %s\0' "${#i}" "$i";
                    done |
                            sort -bz -k1,1rn -k2 |
                            cut -zd " " -f2-
                    )

printf '%s\n' "${sorted[@]}"

1行で:

readarray -td $'\0' sorted < <(for i in "${in[@]}";do printf '%s %s\0' "${#i}" "$i"; done | sort -bz -k1,1rn -k2 | cut -zd " " -f2-)

実行時

$ ./script
the longest
        string also containing
        newlines
also a medium string
medium string
middle string
test * string
short string
tiny string
[abc]
?
*
4
Isaac

zshへの切り替えがオプションの場合、そこにはハックな方法があります(バイトのシーケンスを含む配列の場合):

_array=('' blah $'x\ny\nz' $'x\0y' '1 2 3')
sorted_array=( /(e'{reply=("$array[@]")}'nOe'{REPLY=$#REPLY}') )
_

zshを使用すると、グロブ修飾子を使用して、グロブ拡張のソート順を定義できます。したがって、ここでは、_/_をグロブすることにより、任意の配列に対してそれを行うようにだましていますが、_/_を配列の要素(e'{reply=("$array[@]")}')に置き換えてから、numerically order(大文字のOとは逆に)長さに基づいて要素(_Oe'{REPLY=$#REPLY}'_)。

文字数での長さに基づいていることに注意してください。バイト数については、ロケールをC(_LC_ALL=C_)に設定します。

別のbash 4.4+アプローチ(配列が大きすぎないと仮定):

_readarray -td '' sorted_array < <(
  Perl -l0 -e 'print for sort {length $b <=> length $a} @ARGV
              ' -- "${array[@]}")
_

(それはbytesの長さです)。

古いバージョンのbashでは、いつでも次のことができます。

_eval "sorted_array=($(
    Perl -l0 -e 'for (sort {length $b <=> length $a} @ARGV) {
      '"s/'/'\\\\''/g"'; printf " '\'%s\''", $_}' -- "${array[@]}"
  ))"
_

(これは_ksh93_、zshyashmkshでも機能します)。

3