Cách sắp xếp một mảng trong Bash


139

Tôi có một mảng trong Bash, ví dụ:

array=(a c b f 3 5)

Tôi cần sắp xếp các mảng. Không chỉ hiển thị nội dung theo cách được sắp xếp, mà để có được một mảng mới với các thành phần được sắp xếp. Mảng được sắp xếp mới có thể là một mảng hoàn toàn mới hoặc cũ.

Câu trả lời:


208

Bạn không thực sự cần nhiều mã như vậy:

IFS=$'\n' sorted=($(sort <<<"${array[*]}"))
unset IFS

Hỗ trợ khoảng trắng trong các phần tử (miễn là nó không phải là dòng mới) hoạt động trong Bash 3.x.

ví dụ:

$ array=("a c" b f "3 5")
$ IFS=$'\n' sorted=($(sort <<<"${array[*]}")); unset IFS
$ printf "[%s]\n" "${sorted[@]}"
[3 5]
[a c]
[b]
[f]

Lưu ý: @sorontar đã chỉ ra rằng cần phải cẩn thận nếu các phần tử có chứa ký tự đại diện như *hoặc ?:

Phần được sắp xếp = ($ (...)) đang sử dụng toán tử "split and global". Bạn nên tắt global: set -fhoặc set -o noglobhoặc shopt -op noglobmột phần tử của mảng như *sẽ được mở rộng thành một danh sách các tệp.

Chuyện gì đang xảy ra:

Kết quả là một đỉnh cao sáu điều xảy ra theo thứ tự này:

  1. IFS=$'\n'
  2. "${array[*]}"
  3. <<<
  4. sort
  5. sorted=($(...))
  6. unset IFS

Đầu tiên IFS=$'\n'

Đây là một phần quan trọng trong hoạt động của chúng tôi có ảnh hưởng đến kết quả của 2 và 5 theo cách sau:

Được:

  • "${array[*]}" mở rộng đến mọi phần tử được giới hạn bởi ký tự đầu tiên của IFS
  • sorted=() tạo ra các yếu tố bằng cách phân chia trên mỗi ký tự của IFS

IFS=$'\n' thiết lập mọi thứ để các phần tử được mở rộng bằng cách sử dụng một dòng mới làm dấu phân cách và sau đó được tạo theo cách mà mỗi dòng trở thành một phần tử. (tức là Chia tách trên một dòng mới.)

Phân định theo một dòng mới rất quan trọng vì đó là cách sortvận hành (sắp xếp trên mỗi dòng). Việc phân tách chỉ bằng một dòng mới không quan trọng, nhưng cần bảo tồn các thành phần có chứa khoảng trắng hoặc tab.

Giá trị mặc định IFSmột khoảng trắng , một tab , theo sau là một dòng mới và sẽ không phù hợp với hoạt động của chúng tôi.

Tiếp theo, sort <<<"${array[*]}"phần

<<<, được gọi là các chuỗi ở đây , lấy sự mở rộng của "${array[*]}", như đã giải thích ở trên, và đưa nó vào đầu vào tiêu chuẩn của sort.

Với ví dụ của chúng tôi, sortđược cung cấp chuỗi sau đây:

a c
b
f
3 5

sort các loại , nó tạo ra:

3 5
a c
b
f

Tiếp theo, sorted=($(...))phần

Phần $(...), được gọi là thay thế lệnh , làm cho nội dung của nó ( sort <<<"${array[*]}) chạy như một lệnh bình thường, trong khi lấy đầu ra tiêu chuẩn kết quả là nghĩa đen đi đến đâu $(...).

Trong ví dụ của chúng tôi, điều này tạo ra một cái gì đó tương tự như viết đơn giản:

sorted=(3 5
a c
b
f
)

sorted sau đó trở thành một mảng được tạo bằng cách tách chữ này trên mỗi dòng mới.

cuối cùng unset IFS

Điều này đặt lại giá trị của giá trị IFSmặc định, và chỉ là thực hành tốt.

Đó là để đảm bảo chúng tôi không gây rắc rối với bất cứ điều gì dựa vào IFSkịch bản sau này. (Nếu không, chúng ta cần nhớ rằng chúng ta đã thay đổi mọi thứ xung quanh - điều gì đó có thể không thực tế đối với các tập lệnh phức tạp.)


2
@xxor không có IFS, nó sẽ chia các phần tử của bạn thành các phần nhỏ nếu chúng có khoảng trắng trong đó. Hãy thử ví dụ với IFS=$'\n' bỏ qua và xem!
antak

3
Rất đẹp. Bạn có thể giải thích cho người dùng bash trung bình cách giải pháp này hoạt động?
u32004

2
Bây giờ, với IFS, nó chia các phần tử của bạn thành các phần nhỏ nếu chúng chỉ có một loại khoảng trắng cụ thể trong đó. Tốt; không hoàn hảo :-)
Sự chuộc tội có giới hạn

7
unset IFScần thiết không? Tôi nghĩ rằng việc chuẩn bị trước IFS=một lệnh chỉ giới hạn thay đổi cho lệnh đó, tự động trở về giá trị trước đó.
Đánh dấu

10
@MarkH Điều đó là cần thiết vì sorted=()không phải là lệnh mà là gán biến thứ hai.
antak

35

Phản hồi ban đầu:

array=(a c b "f f" 3 5)
readarray -t sorted < <(for a in "${array[@]}"; do echo "$a"; done | sort)

đầu ra:

$ for a in "${sorted[@]}"; do echo "$a"; done
3
5
a
b
c
f f

Lưu ý phiên bản này đối phó với các giá trị có chứa các ký tự hoặc khoảng trắng đặc biệt ( ngoại trừ dòng mới)

Lưu ý readarray được hỗ trợ trong bash 4+.


Chỉnh sửa Dựa trên đề xuất của @Dimitre, tôi đã cập nhật nó thành:

readarray -t sorted < <(printf '%s\0' "${array[@]}" | sort -z | xargs -0n1)

có lợi ích thậm chí hiểu các yếu tố sắp xếp với các ký tự dòng mới được nhúng chính xác. Thật không may, như được báo hiệu chính xác bởi @ruakh, điều này không có nghĩa là kết quả của readarraynó sẽ đúng , bởi vì readarraykhông có tùy chọn nào để sử dụng NULthay vì các dòng mới thông thường như các dấu tách dòng.


5
Đẹp, cũng cần lưu ý rằng readarray có sẵn kể từ phiên bản 4 của bash. Nó có thể được rút ngắn một chút:readarray -t sorted < <(printf '%s\n' "${array[@]}" | sort)
Dimitre Radoulov

1
@Dimitre: Tôi đã lấy đề xuất của bạn và sửa lỗi xử lý khoảng trắng để làm việc với mọi thứ (sử dụng nullchar-delimiter trong nội bộ). Chúc mừng
sehe

1
Vâng, đây sort -zlà một cải tiến hữu ích, tôi cho rằng -ztùy chọn này là một phần mở rộng sắp xếp GNU.
Dimitre Radoulov

2
Nếu bạn muốn xử lý các dòng mới được nhúng, bạn có thể cuộn bản đọc của chính mình. Ví dụ : sorted=(); while read -d $'\0' elem; do sorted[${#sorted[@]}]=$elem; done < <(printf '%s\0' "${array[@]}" | sort -z). Điều này cũng hoạt động trong bạn đang sử dụng bash v3 thay vì bash v4, vì readarray không có sẵn trong bash v3.
Bob Bell

1
@ user1527227 Đó là chuyển hướng đầu vào ( <) kết hợp với thay thế quá trình <(...) . Hoặc để đặt nó bằng trực giác: bởi vì (printf "bla")không phải là một tập tin.
sehe

33

Đây là một triển khai quicksort Bash thuần túy:

#!/bin/bash

# quicksorts positional arguments
# return is in array qsort_ret
qsort() {
   local pivot i smaller=() larger=()
   qsort_ret=()
   (($#==0)) && return 0
   pivot=$1
   shift
   for i; do
      if (( i < pivot )); then
         smaller+=( "$i" )
      else
         larger+=( "$i" )
      fi
   done
   qsort "${smaller[@]}"
   smaller=( "${qsort_ret[@]}" )
   qsort "${larger[@]}"
   larger=( "${qsort_ret[@]}" )
   qsort_ret=( "${smaller[@]}" "$pivot" "${larger[@]}" )
}

Sử dụng như, ví dụ,

$ array=(a c b f 3 5)
$ qsort "${array[@]}"
$ declare -p qsort_ret
declare -a qsort_ret='([0]="3" [1]="5" [2]="a" [3]="b" [4]="c" [5]="f")'

Việc triển khai này là đệ quy, vì vậy đây là một quicksort lặp:

#!/bin/bash

# quicksorts positional arguments
# return is in array qsort_ret
# Note: iterative, NOT recursive! :)
qsort() {
   (($#==0)) && return 0
   local stack=( 0 $(($#-1)) ) beg end i pivot smaller larger
   qsort_ret=("$@")
   while ((${#stack[@]})); do
      beg=${stack[0]}
      end=${stack[1]}
      stack=( "${stack[@]:2}" )
      smaller=() larger=()
      pivot=${qsort_ret[beg]}
      for ((i=beg+1;i<=end;++i)); do
         if [[ "${qsort_ret[i]}" < "$pivot" ]]; then
            smaller+=( "${qsort_ret[i]}" )
         else
            larger+=( "${qsort_ret[i]}" )
         fi
      done
      qsort_ret=( "${qsort_ret[@]:0:beg}" "${smaller[@]}" "$pivot" "${larger[@]}" "${qsort_ret[@]:end+1}" )
      if ((${#smaller[@]}>=2)); then stack+=( "$beg" "$((beg+${#smaller[@]}-1))" ); fi
      if ((${#larger[@]}>=2)); then stack+=( "$((end-${#larger[@]}+1))" "$end" ); fi
   done
}

Trong cả hai trường hợp, bạn có thể thay đổi thứ tự bạn sử dụng: Tôi đã sử dụng so sánh chuỗi, nhưng bạn có thể sử dụng so sánh số học, so sánh thời gian sửa đổi tệp wrt, v.v. chỉ cần sử dụng thử nghiệm thích hợp; bạn thậm chí có thể làm cho nó chung chung hơn và để nó sử dụng một đối số đầu tiên là sử dụng hàm kiểm tra, ví dụ:

#!/bin/bash

# quicksorts positional arguments
# return is in array qsort_ret
# Note: iterative, NOT recursive! :)
# First argument is a function name that takes two arguments and compares them
qsort() {
   (($#<=1)) && return 0
   local compare_fun=$1
   shift
   local stack=( 0 $(($#-1)) ) beg end i pivot smaller larger
   qsort_ret=("$@")
   while ((${#stack[@]})); do
      beg=${stack[0]}
      end=${stack[1]}
      stack=( "${stack[@]:2}" )
      smaller=() larger=()
      pivot=${qsort_ret[beg]}
      for ((i=beg+1;i<=end;++i)); do
         if "$compare_fun" "${qsort_ret[i]}" "$pivot"; then
            smaller+=( "${qsort_ret[i]}" )
         else
            larger+=( "${qsort_ret[i]}" )
         fi
      done
      qsort_ret=( "${qsort_ret[@]:0:beg}" "${smaller[@]}" "$pivot" "${larger[@]}" "${qsort_ret[@]:end+1}" )
      if ((${#smaller[@]}>=2)); then stack+=( "$beg" "$((beg+${#smaller[@]}-1))" ); fi
      if ((${#larger[@]}>=2)); then stack+=( "$((end-${#larger[@]}+1))" "$end" ); fi
   done
}

Sau đó, bạn có thể có chức năng so sánh này:

compare_mtime() { [[ $1 -nt $2 ]]; }

Và sử dụng:

$ qsort compare_mtime *
$ declare -p qsort_ret

để có các tệp trong thư mục hiện tại được sắp xếp theo thời gian sửa đổi (mới nhất trước tiên).

GHI CHÚ. Các hàm này là Bash thuần túy! không có tiện ích bên ngoài, và không có subshells! chúng an toàn khi viết bất kỳ biểu tượng ngộ nghĩnh nào bạn có thể có (dấu cách, ký tự dòng mới, ký tự toàn cầu, v.v.).


1
Kudos cho Bashing ấn tượng cung cấp tính linh hoạt cao đối với các yếu tố đầu vào và tiêu chí sắp xếp. Nếu sắp xếp dựa trên dòng với các tùy chọn sắp xếp sortcung cấp là đủ, giải pháp sort+ read -asẽ nhanh hơn bắt đầu từ xung quanh, giả sử, 20 mục và ngày càng nhanh hơn và nhanh hơn đáng kể các yếu tố bạn đang xử lý. Ví dụ, trên iMac cuối năm 2012 của tôi chạy OSX 10.11.1 với Fusion Drive: mảng 100 phần tử: ca. 0,03 giây. ( qsort()) so với ca. 0,005 giây. ( sort+ read -a); Mảng 1000 phần tử: ca. 0,375 giây. ( qsort()) so với ca. 0,011 giây ( sort+ read -a).
mkuity0

Đẹp. Tôi nhớ sắp xếp nhanh chóng từ những ngày học đại học nhưng cũng sẽ nghiên cứu sắp xếp bong bóng. Đối với nhu cầu sắp xếp của tôi, tôi có khóa tạo thành phần tử thứ nhất và thứ hai theo sau là một phần tử dữ liệu (mà tôi có thể mở rộng sau). Mã của bạn có thể được cải thiện với số lượng phần tử chính (parm1) và số phần tử dữ liệu (parm2). Đối với OP, các tham số sẽ là 1 và 0. Đối với tôi, các tham số sẽ là 2 và 1. Về mọi mặt, câu trả lời của bạn có nhiều hứa hẹn nhất.
WinEunuuchs2Unix

1
Với một bộ dữ liệu các số nguyên chuỗi uncast tôi tìm thấy if [ "$i" -lt "$pivot" ]; thenđược yêu cầu nếu không thì "2" <"10" đã giải quyết trở lại đúng. Tôi tin rằng đây là POSIX so với Thuật ngữ; hoặc có lẽ Liên kết nội tuyến .
Trang2PagePro

27

Nếu bạn không cần xử lý các ký tự shell đặc biệt trong các thành phần mảng:

array=(a c b f 3 5)
sorted=($(printf '%s\n' "${array[@]}"|sort))

Với bash bạn sẽ cần một chương trình sắp xếp bên ngoài nào.

Với zsh, không cần chương trình bên ngoài và các ký tự shell đặc biệt dễ dàng xử lý:

% array=('a a' c b f 3 5); printf '%s\n' "${(o)array[@]}" 
3
5
a a
b
c
f

kshset -sđể sắp xếp ASCIIbetically .


Thông tin nền rất đẹp. Tôi gần như sẽ yêu cầu một bản demo về cách ksh sẽ sử dụng cờ set -s ... nhưng sau đó, một lần nữa, câu hỏi là trên bash, vì vậy đó sẽ là ngoài chủ đề
sehe

Điều này sẽ hoạt động với hầu hết các triển khai KornShell (ví dụ: ksh88pdksh ): set -A array x 'a a' d; set -s -- "${array[@]}"; set -A sorted "$@" Và, tất nhiên, lệnh set sẽ đặt lại các tham số vị trí hiện tại, nếu có.
Dimitre Radoulov

Bạn là một đài phun nước thực sự của kiến ​​thức vỏ. Tôi chắc chắn bạn phải có bộ nhớ nhiếp ảnh hoặc một cái gì đó, bởi vì loại khác biệt tinh tế này lảng tránh hầu hết các thành viên khác của loài người :), +1 cho gói thông tin đầy đủ
sehe

10

tl; dr :

Sắp xếp mảng a_invà lưu trữ kết quả trong a_out(các phần tử không được nhúng dòng mới [1] ):

Bash v4 +:

readarray -t a_out < <(printf '%s\n' "${a_in[@]}" | sort)

Bash v3:

IFS=$'\n' read -d '' -r -a a_out < <(printf '%s\n' "${a_in[@]}" | sort)

Ưu điểm so với giải pháp của antak :

  • Bạn không cần phải lo lắng về việc tạo hình ngẫu nhiên (giải thích ngẫu nhiên các thành phần mảng dưới dạng mẫu tên tệp), do đó không cần thêm lệnh để vô hiệu hóa tính năng tạo khối ( set -fset +fđể khôi phục lại sau này).

  • Bạn không cần phải lo lắng về việc đặt lại IFSvới unset IFS. [2]


Đọc tùy chọn: giải thích và mã mẫu

Ở trên kết hợp mã Bash với tiện ích bên ngoài sortcho một giải pháp hoạt động với các phần tử dòng đơn tùy ýsắp xếp từ vựng hoặc số (tùy chọn theo trường) :

  • Hiệu suất : Đối với khoảng 20 yếu tố trở lên , điều này sẽ nhanh hơn một giải pháp Bash thuần túy - đáng kể và ngày càng cao hơn khi bạn vượt qua khoảng 100 yếu tố.
    (Ngưỡng chính xác sẽ phụ thuộc vào đầu vào, máy và nền tảng cụ thể của bạn.)

    • Lý do nhanh là vì nó tránh được các vòng lặp Bash .
  • printf '%s\n' "${a_in[@]}" | sort thực hiện sắp xếp (theo từ vựng, theo mặc định - xem sortthông số POSIX ):

    • "${a_in[@]}"mở rộng một cách an toàn các phần tử của mảng a_indưới dạng các đối số riêng lẻ , bất cứ thứ gì chúng chứa (bao gồm cả khoảng trắng).

    • printf '%s\n' sau đó in từng đối số - tức là từng phần tử mảng - trên dòng riêng của nó, như hiện trạng.

  • Lưu ý việc sử dụng thay thế quy trình ( <(...)) để cung cấp đầu ra được sắp xếp làm đầu vào read/ readarray(thông qua chuyển hướng đến stdin, <), bởi vì read/ readarrayphải chạy trong trình bao hiện tại (không được chạy trong lớp con ) a_outđể hiển thị biến đầu ra đến trình bao hiện tại (để biến vẫn được xác định trong phần còn lại của tập lệnh).

  • Đọc sortkết quả đầu ra thành một biến mảng :

    • Bash v4 +: readarray -t a_outđọc đầu ra các dòng riêng lẻ bằng cách sortvào các phần tử của biến mảng a_out, mà không bao gồm dấu \nở mỗi phần tử ( -t).

    • Bash v3: readarraykhông tồn tại, do đó readphải được sử dụng:
      IFS=$'\n' read -d '' -r -a a_outbảo readđọc vào biến mảng ( -a) a_out, đọc toàn bộ đầu vào, trên các dòng ( -d ''), nhưng tách nó thành các phần tử mảng bằng các dòng mới ( IFS=$'\n'. $'\n', Tạo ra một dòng mới theo nghĩa đen (LF ), là một chuỗi được gọi là ANSI C-trích dẫn ).
      ( -r, một tùy chọn hầu như luôn luôn được sử dụng read, vô hiệu hóa việc xử lý các \ký tự không mong muốn .)

Mã mẫu được chú thích:

#!/usr/bin/env bash

# Define input array `a_in`:
# Note the element with embedded whitespace ('a c')and the element that looks like
# a glob ('*'), chosen to demonstrate that elements with line-internal whitespace
# and glob-like contents are correctly preserved.
a_in=( 'a c' b f 5 '*' 10 )

# Sort and store output in array `a_out`
# Saving back into `a_in` is also an option.
IFS=$'\n' read -d '' -r -a a_out < <(printf '%s\n' "${a_in[@]}" | sort)
# Bash 4.x: use the simpler `readarray -t`:
# readarray -t a_out < <(printf '%s\n' "${a_in[@]}" | sort)

# Print sorted output array, line by line:
printf '%s\n' "${a_out[@]}"

Do sử dụng sortkhông có tùy chọn, điều này mang lại sự sắp xếp từ vựng (chữ số sắp xếp trước chữ cái và chuỗi chữ số được xử lý theo từ vựng , không phải là số):

*
10
5
a c
b
f

Nếu bạn muốn sắp xếp số theo trường thứ 1, bạn sẽ sử dụng sort -k1,1nthay vì chỉ sort, sẽ mang lại (sắp xếp không phải là số trước số và sắp xếp số chính xác):

*
a c
b
f
5
10

[1] Để xử lý các phần tử có dòng mới được nhúng, hãy sử dụng biến thể sau (Bash v4 +, với GNU sort ) :
readarray -d '' -t a_out < <(printf '%s\0' "${a_in[@]}" | sort -z).
Câu trả lời hữu ích của Michał Górny có giải pháp Bash v3.

[2] Trong khi IFS được đặt trong biến thể Bash v3, thay đổi nằm trong phạm vi lệnh .
Ngược lại, những gì tiếp theo IFS=$'\n' trong câu trả lời của antak là một bài tập chứ không phải là một lệnh, trong trường hợp đó sự IFSthay đổi là toàn cầu .


8

Trong chuyến đi kéo dài 3 giờ từ Munich đến Frankfurt (mà tôi gặp khó khăn để tiếp cận vì Lễ hội tháng mười bắt đầu vào ngày mai) tôi đã suy nghĩ về bài đăng đầu tiên của mình. Sử dụng một mảng toàn cầu là một ý tưởng tốt hơn nhiều cho một hàm sắp xếp chung. Hàm sau xử lý các chuỗi tùy ý (dòng mới, khoảng trắng, v.v.):

declare BSORT=()
function bubble_sort()
{   #
    # @param [ARGUMENTS]...
    #
    # Sort all positional arguments and store them in global array BSORT.
    # Without arguments sort this array. Return the number of iterations made.
    #
    # Bubble sorting lets the heaviest element sink to the bottom.
    #
    (($# > 0)) && BSORT=("$@")
    local j=0 ubound=$((${#BSORT[*]} - 1))
    while ((ubound > 0))
    do
        local i=0
        while ((i < ubound))
        do
            if [ "${BSORT[$i]}" \> "${BSORT[$((i + 1))]}" ]
            then
                local t="${BSORT[$i]}"
                BSORT[$i]="${BSORT[$((i + 1))]}"
                BSORT[$((i + 1))]="$t"
            fi
            ((++i))
        done
        ((++j))
        ((--ubound))
    done
    echo $j
}

bubble_sort a c b 'z y' 3 5
echo ${BSORT[@]}

Bản in này:

3 5 a b c z y

Đầu ra tương tự được tạo ra từ

BSORT=(a c b 'z y' 3 5) 
bubble_sort
echo ${BSORT[@]}

Lưu ý rằng có lẽ Bash trong nội bộ sử dụng các con trỏ thông minh, do đó, hoạt động hoán đổi thể rẻ (mặc dù tôi nghi ngờ điều đó). Tuy nhiên, bubble_sortchứng minh rằng các chức năng nâng cao hơn như merge_sortcũng nằm trong phạm vi của ngôn ngữ shell.


5
Sắp xếp bong bóng? Wow .. Obama nói "sắp xếp bong bóng sẽ là con đường sai lầm" -> youtube.com/watch?v=k4RRi_ntQc8
Robottinosino

1
Chà, có vẻ như trong khi anh chàng O muốn thông minh, anh ta đã không cảm thấy rằng đây không phải là câu hỏi cơ hội 50/50. Người tiền nhiệm ở vị trí O-guys, hãy nói với anh ấy là anh chàng B, đã từng làm tốt hơn nhiều (Reynoldsburg, Ohio, tháng 10 năm 2000): "Tôi nghĩ rằng nếu bạn biết những gì bạn tin, thì việc trả lời câu hỏi sẽ dễ dàng hơn rất nhiều Tôi không thể trả lời câu hỏi của bạn. " Vì vậy, anh chàng B này thực sự biết điều gì đó về logic Boolean. Anh chàng O không.
Andreas Spindler

Hàm này có thể được thực hiện dễ dàng hơn bằng cách biến BSORT thành một mảng cục bộ với một tên miền cho bất kỳ mảng nào được sắp xếp. tức là local -n BSORT="$1"khi bắt đầu hàm. Sau đó, bạn có thể chạy bubble_sort myarrayđể sắp xếp myarray .
johnraff

7

Một giải pháp khác sử dụng bên ngoài sortvà đối phó với bất kỳ ký tự đặc biệt nào (ngoại trừ NULs :)). Nên hoạt động với bash-3.2 và GNU hoặc BSD sort(đáng buồn thay, POSIX không bao gồm -z).

local e new_array=()
while IFS= read -r -d '' e; do
    new_array+=( "${e}" )
done < <(printf "%s\0" "${array[@]}" | LC_ALL=C sort -z)

Đầu tiên nhìn vào chuyển hướng đầu vào ở cuối. Chúng tôi đang sử dụng tích printfhợp để viết ra các phần tử mảng, không kết thúc. Việc trích dẫn đảm bảo các phần tử mảng được truyền printfnguyên trạng và các chi tiết cụ thể của shell khiến nó sử dụng lại phần cuối của chuỗi định dạng cho mỗi tham số còn lại. Đó là, nó tương đương với một cái gì đó như:

for e in "${array[@]}"; do
    printf "%s\0" "${e}"
done

Danh sách phần tử kết thúc null sau đó được chuyển đến sort. Các -ztùy chọn gây ra nó để đọc các yếu tố null-chấm dứt, sắp xếp chúng và đầu ra null-chấm dứt là tốt. Nếu bạn chỉ cần lấy các yếu tố duy nhất, bạn có thể vượt qua -uvì nó dễ mang theo hơn uniq -z. Thứ LC_ALL=Ctự sắp xếp ổn định độc lập với miền địa phương - đôi khi hữu ích cho các tập lệnh. Nếu bạn muốn sorttôn trọng miền địa phương, hãy loại bỏ điều đó.

Cấu <()trúc có được bộ mô tả để đọc từ đường ống được sinh ra và <chuyển hướng đầu vào tiêu chuẩn của whilevòng lặp đến nó. Nếu bạn cần truy cập vào đầu vào tiêu chuẩn bên trong đường ống, bạn có thể sử dụng một mô tả khác - bài tập cho người đọc :).

Bây giờ, trở lại từ đầu. Đầu ra tích readhợp đọc từ stdin chuyển hướng. Đặt trống sẽ IFSvô hiệu hóa việc tách từ không cần thiết ở đây - kết quả là, readđọc toàn bộ 'dòng' đầu vào cho biến được cung cấp duy nhất. -rtùy chọn cũng vô hiệu hóa xử lý thoát không mong muốn ở đây. Cuối cùng, -d ''đặt dấu phân cách dòng thành NUL - nghĩa là bảo readđọc các chuỗi kết thúc bằng không.

Kết quả là, vòng lặp được thực thi một lần cho mỗi phần tử mảng kết thúc bằng 0 liên tiếp, với giá trị được lưu trữ trong e. Ví dụ chỉ đặt các mục trong một mảng khác nhưng bạn có thể thích xử lý chúng trực tiếp :).

Tất nhiên, đó chỉ là một trong nhiều cách để đạt được cùng một mục tiêu. Như tôi thấy, nó đơn giản hơn việc thực hiện thuật toán sắp xếp hoàn chỉnh trong bash và trong một số trường hợp, nó sẽ nhanh hơn. Nó xử lý tất cả các ký tự đặc biệt bao gồm các dòng mới và sẽ hoạt động trên hầu hết các hệ thống phổ biến. Quan trọng nhất, nó có thể dạy cho bạn một cái gì đó mới và tuyệt vời về bash :).


Giải pháp tuyệt vời và giải thích rất hữu ích, cảm ơn. Một tiện ích mở rộng: Không đặt IFS thành trống, khoảng trắng hàng đầu cũng sẽ bị loại bỏ - ngay cả khi không có phân tách từ nào được thực hiện.
Dirk Herrmann

Thay vì giới thiệu biến cục bộ evà đặt IFS trống, hãy sử dụng biến REPLY.
Robin A. Meade

2

thử cái này:

echo ${array[@]} | awk 'BEGIN{RS=" ";} {print $1}' | sort

Đầu ra sẽ là:

3
5
một
b
c
f

Vấn đề được giải quyết.


3
Nên chỉnh sửa điều này để đưa đầu ra vào một mảng mới để trả lời đầy đủ câu hỏi của anh ấy.
Peter Oram

2

Nếu bạn có thể tính một số nguyên duy nhất cho mỗi phần tử trong mảng, như sau:

tab='0123456789abcdefghijklmnopqrstuvwxyz'

# build the reversed ordinal map
for ((i = 0; i < ${#tab}; i++)); do
    declare -g ord_${tab:i:1}=$i
done

function sexy_int() {
    local sum=0
    local i ch ref
    for ((i = 0; i < ${#1}; i++)); do
        ch="${1:i:1}"
        ref="ord_$ch"
        (( sum += ${!ref} ))
    done
    return $sum
}

sexy_int hello
echo "hello -> $?"
sexy_int world
echo "world -> $?"

sau đó, bạn có thể sử dụng các số nguyên này làm chỉ mục mảng, vì Bash luôn sử dụng mảng thưa thớt, do đó không cần phải lo lắng về các chỉ mục không được sử dụng:

array=(a c b f 3 5)
for el in "${array[@]}"; do
    sexy_int "$el"
    sorted[$?]="$el"
done

echo "${sorted[@]}"
  • Ưu điểm. Nhanh.
  • Nhược điểm. Các phần tử trùng lặp được hợp nhất và không thể ánh xạ nội dung thành các số nguyên duy nhất 32 bit.

Kỹ thuật thú vị, tôi đã sử dụng một biến thể để tìm giá trị tối đa / tối thiểu mà không cần so sánh / sắp xếp rõ ràng. Nhưng , bổ sung không có trọng số mà không liên quan đến độ dài sẽ không hoạt động: "z" sắp xếp trước "aaaa", vì vậy bạn không thể sử dụng từ này cho các từ như bạn trình bày ở trên.
mr.spuratic

2

sắp xếp tối thiểu:

#!/bin/bash
array=(.....)
index_of_element1=0

while (( ${index_of_element1} < ${#array[@]} )); do

    element_1="${array[${index_of_element1}]}"

    index_of_element2=$((index_of_element1 + 1))
    index_of_min=${index_of_element1}

    min_element="${element_1}"

        for element_2 in "${array[@]:$((index_of_element1 + 1))}"; do
            min_element="`printf "%s\n%s" "${min_element}" "${element_2}" | sort | head -n+1`"      
            if [[ "${min_element}" == "${element_2}" ]]; then
                index_of_min=${index_of_element2}
            fi
            let index_of_element2++
        done

        array[${index_of_element1}]="${min_element}"
        array[${index_of_min}]="${element_1}"

    let index_of_element1++
done

1
array=(a c b f 3 5)
new_array=($(echo "${array[@]}" | sed 's/ /\n/g' | sort))    
echo ${new_array[@]}

nội dung tiếng vang của new_array sẽ là:

3 5 a b c f

1

Có một cách giải quyết cho vấn đề thông thường về không gian và dòng mới:

Sử dụng một nhân vật đó không phải là trong mảng ban đầu (như $'\1'hoặc $'\4'hoặc tương tự).

Hàm này hoàn thành công việc:

# Sort an Array may have spaces or newlines with a workaround (wa=$'\4')
sortarray(){ local wa=$'\4' IFS=''
             if [[ $* =~ [$wa] ]]; then
                 echo "$0: error: array contains the workaround char" >&2
                 exit 1
             fi

             set -f; local IFS=$'\n' x nl=$'\n'
             set -- $(printf '%s\n' "${@//$nl/$wa}" | sort -n)
             for    x
             do     sorted+=("${x//$wa/$nl}")
             done
       }

Điều này sẽ sắp xếp mảng:

$ array=( a b 'c d' $'e\nf' $'g\1h')
$ sortarray "${array[@]}"
$ printf '<%s>\n' "${sorted[@]}"
<a>
<b>
<c d>
<e
f>
<gh>

Điều này sẽ phàn nàn rằng mảng nguồn chứa ký tự giải pháp:

$ array=( a b 'c d' $'e\nf' $'g\4h')
$ sortarray "${array[@]}"
./script: error: array contains the workaround char

sự miêu tả

  • Chúng tôi đặt hai biến cục bộ wa(workaround char) và IFS null
  • Sau đó (với ifs null) chúng tôi kiểm tra toàn bộ mảng $*.
  • Không chứa bất kỳ char nào [[ $* =~ [$wa] ]].
  • Nếu có, hãy đưa ra một thông báo và báo hiệu lỗi: exit 1
  • Tránh mở rộng tên tệp: set -f
  • Đặt giá trị mới của IFS ( IFS=$'\n') một biến vòng lặp xvà một dòng mới var ( nl=$'\n').
  • Chúng tôi in tất cả các giá trị của các đối số nhận được (mảng đầu vào $@).
  • nhưng chúng tôi thay thế bất kỳ dòng mới bằng char giải pháp "${@//$nl/$wa}".
  • gửi những giá trị được sắp xếp sort -n.
  • và đặt lại tất cả các giá trị được sắp xếp trong các đối số vị trí set --.
  • Sau đó, chúng tôi chỉ định từng đối số một (để duy trì dòng mới).
  • trong một vòng lặp for x
  • đến một mảng mới: sorted+=(…)
  • bên trong trích dẫn để bảo tồn bất kỳ dòng mới hiện có.
  • Khôi phục cách giải quyết cho một dòng mới "${x//$wa/$nl}".
  • làm xong

1

Câu hỏi này có vẻ liên quan chặt chẽ. Và BTW, đây là một sự hợp nhất trong Bash (không có các quy trình bên ngoài):

mergesort() {
  local -n -r input_reference="$1"
  local -n output_reference="$2"
  local -r -i size="${#input_reference[@]}"
  local merge previous
  local -a -i runs indices
  local -i index previous_idx merged_idx \
           run_a_idx run_a_stop \
           run_b_idx run_b_stop

  output_reference=("${input_reference[@]}")
  if ((size == 0)); then return; fi

  previous="${output_reference[0]}"
  runs=(0)
  for ((index = 0;;)) do
    for ((++index;; ++index)); do
      if ((index >= size)); then break 2; fi
      if [[ "${output_reference[index]}" < "$previous" ]]; then break; fi
      previous="${output_reference[index]}"
    done
    previous="${output_reference[index]}"
    runs+=(index)
  done
  runs+=(size)

  while (("${#runs[@]}" > 2)); do
    indices=("${!runs[@]}")
    merge=("${output_reference[@]}")
    for ((index = 0; index < "${#indices[@]}" - 2; index += 2)); do
      merged_idx=runs[indices[index]]
      run_a_idx=merged_idx
      previous_idx=indices[$((index + 1))]
      run_a_stop=runs[previous_idx]
      run_b_idx=runs[previous_idx]
      run_b_stop=runs[indices[$((index + 2))]]
      unset runs[previous_idx]
      while ((run_a_idx < run_a_stop && run_b_idx < run_b_stop)); do
        if [[ "${merge[run_a_idx]}" < "${merge[run_b_idx]}" ]]; then
          output_reference[merged_idx++]="${merge[run_a_idx++]}"
        else
          output_reference[merged_idx++]="${merge[run_b_idx++]}"
        fi
      done
      while ((run_a_idx < run_a_stop)); do
        output_reference[merged_idx++]="${merge[run_a_idx++]}"
      done
      while ((run_b_idx < run_b_stop)); do
        output_reference[merged_idx++]="${merge[run_b_idx++]}"
      done
    done
  done
}

declare -ar input=({z..a}{z..a})
declare -a output

mergesort input output

echo "${input[@]}"
echo "${output[@]}"

0

Tôi không tin rằng bạn sẽ cần một chương trình sắp xếp bên ngoài ở Bash.

Đây là triển khai của tôi cho thuật toán sắp xếp bong bóng đơn giản.

function bubble_sort()
{   #
    # Sorts all positional arguments and echoes them back.
    #
    # Bubble sorting lets the heaviest (longest) element sink to the bottom.
    #
    local array=($@) max=$(($# - 1))
    while ((max > 0))
    do
        local i=0
        while ((i < max))
        do
            if [ ${array[$i]} \> ${array[$((i + 1))]} ]
            then
                local t=${array[$i]}
                array[$i]=${array[$((i + 1))]}
                array[$((i + 1))]=$t
            fi
            ((i += 1))
        done
        ((max -= 1))
    done
    echo ${array[@]}
}

array=(a c b f 3 5)
echo " input: ${array[@]}"
echo "output: $(bubble_sort ${array[@]})"

Điều này sẽ in:

 input: a c b f 3 5
output: 3 5 a b c f

Sắp xếp bong bóng là O(n^2). Tôi dường như nhớ lại hầu hết các thuật toán sắp xếp sử dụng O(n lg(n))cho đến hàng tá phần tử cuối cùng hoặc lâu hơn. Đối với các yếu tố cuối cùng, sắp xếp lựa chọn được sử dụng.
jww


-1

sorted=($(echo ${array[@]} | tr " " "\n" | sort))

Theo tinh thần của bash / linux, tôi sẽ sử dụng công cụ dòng lệnh tốt nhất cho mỗi bước. sortthực hiện công việc chính nhưng cần đầu vào được phân tách bằng dòng mới thay vì không gian, do đó, đường ống rất đơn giản ở trên chỉ đơn giản là:

Echo nội dung mảng -> thay thế không gian bằng dòng mới -> sắp xếp

$() là để lặp lại kết quả

($()) là đặt "kết quả lặp lại" trong một mảng

Lưu ý : như @sorontar đã đề cập trong một nhận xét cho một câu hỏi khác:

Phần được sắp xếp = ($ (...)) đang sử dụng toán tử "split and global". Bạn nên tắt global: set -f hoặc set -o noglob hoặc shopt -op noglob hoặc một phần tử của mảng như * sẽ được mở rộng thành một danh sách các tệp.


Theo tinh thần bash / linux : Tôi đoán bạn không hiểu gì về tinh thần cả. Mã của bạn bị hỏng hoàn toàn (mở rộng tên đường dẫn và tách từ). Điều này sẽ tốt hơn (Bash≥4) : mapfile -t sorted < <(printf '%s\n' "${array[@]}" | sort), nếu không sorted=(); while IFS= read -r line; do sorted+=( "$line" ); done < <(printf '%s\n' | sort).
gniourf_gniourf

Các antipotype bạn đang sử dụng là :: echo ${array[@]} | tr " " "\n"điều này sẽ bị phá vỡ nếu các trường của mảng chứa khoảng trắng và ký tự toàn cầu. Bên cạnh đó, nó sinh ra một lớp con và sử dụng một lệnh bên ngoài vô dụng. Và do echobị câm, nó sẽ bị hỏng nếu mảng của bạn bắt đầu bằng -e, -Ehoặc -n. Thay vào đó sử dụng : printf '%s\n' "${array[@]}". Các antipotype khác là: ($())đặt "kết quả lặp lại" trong một mảng . Chắc chắn không! đây là một antipotype khủng khiếp bị phá vỡ vì sự mở rộng tên đường dẫn (globalbing) và chia tách từ. Không bao giờ sử dụng kinh dị này.
gniourf_gniourf

Câu trả lời hàng đầu có "phản hạt khủng khiếp". Và cách để đi xuống để trả lời câu trả lời của người khác cho câu hỏi mà bạn tự trả lời.
michael
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.