Bash sắp xếp mảng theo chiều dài của các phần tử?


9

Đưa ra một chuỗi các chuỗi, tôi muốn sắp xếp mảng theo độ dài của mỗi phần tử.

Ví dụ...

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

Nên sắp xếp để ...

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

(Như một phần thưởng, sẽ rất tuyệt nếu danh sách sắp xếp các chuỗi có cùng độ dài, theo thứ tự bảng chữ cái. Trong ví dụ trên medium stringđã được sắp xếp trước middle stringmặc dù chúng có cùng độ dài. Nhưng đó không phải là một yêu cầu "khó", nếu nó quá phức tạp giải pháp).

Sẽ ổn nếu mảng được sắp xếp tại chỗ (nghĩa là "mảng" được sửa đổi) hoặc nếu một mảng được sắp xếp mới được tạo.


1
Một số câu trả lời thú vị ở đây, bạn sẽ có thể điều chỉnh một câu để kiểm tra độ dài chuỗi cũng như stackoverflow.com/a/30576368/2876682
frostschutz 17/11/18

Câu trả lời:


12

Nếu các chuỗi không chứa dòng mới, phần dưới đây sẽ hoạt động. Nó sắp xếp các chỉ số của mảng theo độ dài, sử dụng chính các chuỗi làm tiêu chí sắp xếp thứ cấp.

#!/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[@]}")

Lưu ý rằng việc chuyển sang ngôn ngữ lập trình thực có thể đơn giản hóa rất nhiều giải pháp, ví dụ như trong Perl, bạn chỉ có thể

sort { length $b <=> length $a or $a cmp $b } @array

1
Trong Python:sorted(array, key=lambda s: (len(s), s))
wjandrea

1
Trong 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- )

Điều này đọc các giá trị của mảng được sắp xếp từ một sự thay thế quá trình.

Sự thay thế quá trình chứa một vòng lặp. Vòng lặp xuất ra từng phần tử của mảng được đặt trước bởi độ dài của phần tử và ký tự tab ở giữa.

Đầu ra của vòng lặp được sắp xếp theo số lượng từ lớn nhất đến nhỏ nhất (và theo thứ tự chữ cái nếu độ dài là như nhau; sử dụng -k 2rthay -k 2cho thứ tự bảng chữ cái) và kết quả của được gửi tới để cutxóa cột với độ dài chuỗi.

Sắp xếp tập lệnh kiểm tra theo sau là một lần chạy thử:

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

Điều này giả định rằng các chuỗi không chứa dòng mới. Trên các hệ thống GNU gần đây bash, bạn có thể hỗ trợ các dòng mới được nhúng trong dữ liệu bằng cách sử dụng ký tự nul làm dấu tách bản ghi thay vì dòng mới:

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- )

Ở đây, dữ liệu được in với dấu vết \0trong vòng lặp thay vì dòng mới, sortcutđọc các dòng được phân định bằng nul thông qua các -ztùy chọn GNU và readarraycuối cùng đọc dữ liệu được phân định bằng nul -d ''.


3
Lưu ý rằng -d '\0'trên thực tế -d ''bashkhông thể chuyển các ký tự NUL cho các lệnh, ngay cả các phần tử của nó. Nhưng nó hiểu -d ''như là phân định ý nghĩa trên NUL . Lưu ý rằng bạn cần bash 4.4+ cho điều đó.
Stéphane Chazelas

@ StéphaneChazelas Không, không phải '\0'vậy $'\0'. Và vâng, nó chuyển đổi (gần như chính xác) thành ''. Nhưng đó là một cách để comunicate với độc giả khác thực tế ý định sử dụng một delimiter NUL.
Isaac

4

Tôi sẽ không hoàn toàn lặp lại những gì tôi đã nói về việc sắp xếp trong bash , chỉ là bạn có thể sắp xếp trong bash, nhưng có lẽ bạn không nên. Dưới đây là một triển khai chỉ bash của một loại chèn, đó là O (n 2 ), và do đó chỉ chấp nhận được đối với các mảng nhỏ. Nó sắp xếp các phần tử mảng tại chỗ theo chiều dài của chúng, theo thứ tự giảm dần. Nó không làm một loại chữ cái thứ cấp.

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

Bằng chứng là đây là một giải pháp chuyên biệt, hãy xem xét thời gian của ba câu trả lời hiện có trên các mảng kích thước khác nhau:

# 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 có ý tưởng đúng: tính toán độ dài một lần và sử dụng các tiện ích chuyên dụng để sắp xếp và xử lý văn bản.


4

Một tin tặc? (phức tạp) và cách nhanh một dòng để sắp xếp mảng theo chiều dài
( an toàn cho dòng mới và mảng thưa):

#!/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[@]}"

Trên một dòng:

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

Thực hiện

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

4

Điều này cũng xử lý các phần tử mảng với dòng mới trong đó; nó hoạt động bằng cách chỉ đi qua sortchiều dài và chỉ số của từng yếu tố. Nó nên làm việc với bashksh.

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[@]}"

Nếu các phần tử có cùng độ dài cũng phải được sắp xếp theo từ vựng, vòng lặp có thể được thay đổi như sau:

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

Điều này cũng sẽ chuyển đến sortcác chuỗi (với các dòng mới được thay đổi thành khoảng trắng), nhưng chúng vẫn sẽ được sao chép từ nguồn sang mảng đích bởi các chỉ mục của chúng. Trong cả hai ví dụ, $(...)sẽ chỉ thấy các dòng chứa số (và /ký tự trong ví dụ đầu tiên), do đó, nó sẽ không bị vấp bởi các ký tự hoặc khoảng trắng trong chuỗi.


Không thể tái tạo. Trong ví dụ thứ hai, sự $(...)thay thế lệnh chỉ nhìn thấy các chỉ mục (một danh sách các số được phân tách bằng dòng mới), bởi vì cut -d' ' -f1sau khi sắp xếp. Điều này có thể dễ dàng được chứng minh bởi một tee /dev/ttyở cuối của $(...).
mosvy

Xin lỗi, xấu của tôi, tôi đã bỏ lỡ cut.
Stéphane Chazelas

@Isaac Không cần trích dẫn ${!in[@]}hoặc ${#in[i]}/$imở rộng biến vì chúng chỉ chứa các chữ số không thuộc đối tượng mở rộng toàn cầu và unset IFSsẽ đặt lại IFSkhông gian, tab, dòng mới. Trong thực tế, trích dẫn chúng sẽ có hại , bởi vì nó sẽ gây ấn tượng sai rằng trích dẫn đó là hữu ích và hiệu quả, và việc thiết lập IFSvà / hoặc lọc đầu ra của sortví dụ thứ hai có thể được thực hiện một cách an toàn.
mosvy

@Isaac Nó KHÔNG phá vỡ nếu inchứa "testing * here"shopt -s nullglobđược thiết lập trước khi vòng lặp.
mosvy

3

Trong trường hợp chuyển sang zshlà một tùy chọn, một cách hackish ở đó (đối với các mảng chứa bất kỳ chuỗi byte nào):

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

zshcho phép xác định các đơn đặt hàng sắp xếp để mở rộng toàn cầu thông qua vòng loại toàn cầu. Vì vậy, ở đây, chúng tôi lừa nó để làm điều đó cho các mảng tùy ý bằng cách nhập vào /, nhưng thay thế /bằng các phần tử của mảng ( e'{reply=("$array[@]")}') và sau đó nrderally o(ngược lại với chữ hoa O) các phần tử dựa trên chiều dài của chúng ( Oe'{REPLY=$#REPLY}').

Lưu ý rằng nó dựa trên độ dài số lượng ký tự. Đối với số byte, đặt ngôn ngữ thành C( LC_ALL=C).

Một bashcách tiếp cận 4.4+ khác (giả sử một mảng không quá lớn):

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

(đó là độ dài tính bằng byte ).

Với các phiên bản cũ hơn bash, bạn luôn có thể làm:

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

(mà cũng sẽ làm việc với ksh93, zsh, yash, mksh).

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.