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


9

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

例えば...

    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 string前に並べ替えられmiddle stringていました。しかし、解決)。

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


1
以上、ここでいくつかの興味深い答えは、あなたがよくとして文字列の長さをテストするために1を適合させることができるはずstackoverflow.com/a/30576368/2876682
frostschutz

回答:


12

文字列に改行が含まれていない場合は、次のように動作します。ストリング自体を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

1
Pythonの場合:sorted(array, key=lambda s: (len(s), s))
wjandrea 2018年

1
Rubyの場合:array.sort { |a| a.size }
Dmitry Kudriavtsev

9
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アルファベット順を逆にする)の結果それに送信されるcut文字列の長さを有するカラムを削除しています。

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

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

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

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ループの末尾に改行ではなく印刷され、sortcut-zGNUオプションを介してヌル区切りの行をreadarray読み取り、最後にでヌル区切りのデータを読み取ります-d ''


3
組み込みであっても-d '\0'、実際-d ''bashはコマンドにNUL文字を渡すことができないことに注意してください。しかし、それはNULの区切り-d ''を意味すると理解しています。そのためにはbash 4.4以降が必要です。
ステファンChazelas

StéphaneChazelasません@、それはないが'\0'、それがあります$'\0'。そして、はい、それは(ほぼ正確に)に変換され''ます。しかし、それはNUL区切り文字を使用する実際の意図を他の読者に伝える方法です。
Isaac

4

bashでの並べ替えについてすでに述べた内容を完全に繰り返すことはしませんが、bash内で並べ替えることができます、そうすべきではないかもしれません。以下は、挿入ソートのbashのみの実装です。これはO(n 2)であり、小さい配列に対してのみ許容されます。配列要素を、長さの大きい順に並べ替えます。2次的なアルファベット順の並べ替えは行いません。

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

ハッカっぽい?(複雑)配列を長さで並べ替える高速な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

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


再現できません。2番目の例では、並べ替え後の$(...)ため、コマンド置換ではインデックス(改行で区切られた数値のリスト)のみが表示されcut -d' ' -f1ます。これはtee /dev/tty、の最後にあるで簡単に説明できます$(...)
mosvy

申し訳ありませんが、私の悪い、私は逃したcut
ステファンChazelas

@Isaac ${!in[@]}または${#in[i]}/$i変数展開は、グロブ展開の対象とならない数字のみを含みunset IFSIFSはスペース、タブ、改行にリセットされるため、引用符を付ける必要はありません。実際、それらを引用すると有害です。なぜなら、そのような引用は有用で効果的であるという誤った印象を与え、2番目の例のIFS出力の設定やフィルタリングをsort安全に行うことができるからです。
2018年

@Isaacが含まれていてループの前に設定されている場合は壊れませin"testing * here"shopt -s nullglob
2018年

3

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

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

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

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

別のbash4.4+アプローチ(配列が大きすぎないと想定):

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

(それはバイトの長さです)。

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

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

(もで動作することになりますksh93zshyashmksh)。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.