Comment trier un tableau dans Bash

139

J'ai un tableau dans Bash, par exemple:

array=(a c b f 3 5)

J'ai besoin de trier le tableau. Non seulement afficher le contenu de manière triée, mais pour obtenir un nouveau tableau avec les éléments triés. Le nouveau tableau trié peut être un tout nouveau ou l'ancien.

u32004
la source

Réponses:

208

Vous n'avez pas vraiment besoin de beaucoup de code:

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

Prend en charge les espaces dans les éléments (tant qu'il ne s'agit pas d'une nouvelle ligne), et fonctionne dans Bash 3.x.

par exemple:

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

Remarque: @sorontar a souligné que la prudence est de mise si les éléments contiennent des caractères génériques tels que *ou ?:

La partie triée = ($ (...)) utilise l'opérateur "split and glob". Vous devez désactiver glob: set -fou set -o noglobou shopt -op noglobou un élément du tableau comme *sera développé en une liste de fichiers.

Que ce passe-t-il:

Le résultat est un aboutissement de six choses qui se produisent dans cet ordre:

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

Premièrement la IFS=$'\n'

C'est une partie importante de notre opération qui affecte le résultat de 2 et 5 de la manière suivante:

Donné:

  • "${array[*]}" s'étend à chaque élément délimité par le premier caractère de IFS
  • sorted=() crée des éléments en divisant sur chaque caractère de IFS

IFS=$'\n' configure les choses de manière à ce que les éléments soient développés à l'aide d' une nouvelle ligne comme délimiteur, puis créés ultérieurement de manière à ce que chaque ligne devienne un élément. (c.-à-d. fractionnement sur une nouvelle ligne.)

La délimitation par une nouvelle ligne est importante car c'est ainsi que sortfonctionne (tri par ligne). Le fractionnement par une seule nouvelle ligne n'est pas aussi important, mais il est nécessaire de préserver les éléments qui contiennent des espaces ou des tabulations.

La valeur par défaut de IFSest un espace , une tabulation , suivi d' une nouvelle ligne , et serait impropre à notre fonctionnement.

Ensuite, la sort <<<"${array[*]}"partie

<<<, appelé ici chaînes , prend l'expansion de "${array[*]}", comme expliqué ci-dessus, et la nourrit dans l'entrée standard desort .

Avec notre exemple, sortest alimenté cette chaîne suivante:

a c
b
f
3 5

Depuis les sort tris , il produit:

3 5
a c
b
f

Ensuite, la sorted=($(...))partie

La $(...)partie, appelée substitution de commande , fait sort <<<"${array[*]}exécuter son contenu ( ) comme une commande normale, tout en prenant la sortie standard résultante comme le littéral qui va où que ce $(...)soit.

Dans notre exemple, cela produit quelque chose de similaire à la simple écriture:

sorted=(3 5
a c
b
f
)

sorted devient alors un tableau créé en divisant ce littéral à chaque nouvelle ligne.

Finalement, le unset IFS

Cela réinitialise la valeur de IFSà la valeur par défaut et constitue simplement une bonne pratique.

C'est pour nous assurer que nous ne causons aucun problème avec tout ce qui s'appuie IFSplus tard dans notre script. (Sinon, nous devons nous rappeler que nous avons changé les choses - ce qui pourrait être peu pratique pour les scripts complexes.)

antak
la source
2
@xxor sans le IFS, cela divisera vos éléments en petits morceaux s'ils contiennent des espaces blancs. Essayez le par exemple avec IFS=$'\n' omis et voyez!
antak
3
Très agréable. Pourriez-vous expliquer à l'utilisateur moyen de bash comment cette solution fonctionne?
u32004
2
Maintenant, avec le IFS, il divise vos éléments en petits morceaux s'ils ne contiennent qu'un seul type d'espaces blancs. Bien; pas parfait :-)
Expiation limitée
7
Est unset IFSnécessaire? Je pensais que le préfixe IFS=à une commande ne concernait que la modification de cette commande, revenant à sa valeur précédente automatiquement par la suite.
Mark H
10
@MarkH C'est nécessaire car ce sorted=()n'est pas une commande mais plutôt une deuxième affectation de variable.
antak le
35

Réponse originale:

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

production:

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

Notez que cette version gère les valeurs contenant des caractères spéciaux ou des espaces ( sauf les retours à la ligne)

Remarque readarray est pris en charge dans bash 4+.


Modifier Sur la base de la suggestion de @Dimitre, je l'avais mis à jour pour:

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

qui a l'avantage de même comprendre les éléments de tri avec des caractères de nouvelle ligne incorporés correctement. Malheureusement, comme correctement signalé par @ruakh, cela ne signifiait pas que le résultat de readarrayserait correct , car readarrayil n'y a pas d'option à utiliser à la NULplace de nouvelles lignes régulières comme séparateurs de ligne.

sehe
la source
5
Sympa, il faut aussi noter que readarray est disponible depuis la version 4 de bash. Il pourrait être un peu raccourci:readarray -t sorted < <(printf '%s\n' "${array[@]}" | sort)
Dimitre Radoulov
1
@Dimitre: J'ai pris votre suggestion et j'ai corrigé la gestion des espaces pour qu'elle fonctionne avec n'importe quoi (en utilisant des délimiteurs nullchar en interne). Vive
sehe
1
Oui, sort -zc'est une amélioration utile, je suppose que l' -zoption est une extension de tri GNU.
Dimitre Radoulov
2
Si vous souhaitez gérer les nouvelles lignes incorporées, vous pouvez générer votre propre readarray. Par exemple: sorted=(); while read -d $'\0' elem; do sorted[${#sorted[@]}]=$elem; done < <(printf '%s\0' "${array[@]}" | sort -z). Cela fonctionne également lorsque vous utilisez bash v3 au lieu de bash v4, car readarray n'est pas disponible dans bash v3.
Bob Bell
1
@ user1527227 C'est la redirection d'entrée ( <) combinée avec la substitution de processus <(...) . Ou pour le dire intuitivement: parce que ce (printf "bla")n'est pas un fichier.
sehe
33

Voici une implémentation pure de tri rapide Bash:

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

Utiliser comme, par exemple,

$ 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")'

Cette implémentation est récursive ... alors voici un tri rapide itératif:

#!/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
}

Dans les deux cas, vous pouvez changer l'ordre que vous utilisez: j'ai utilisé des comparaisons de chaînes, mais vous pouvez utiliser des comparaisons arithmétiques, comparer l'heure de modification du fichier, etc. utilisez simplement le test approprié; vous pouvez même le rendre plus générique et lui faire utiliser un premier argument qui est l'utilisation de la fonction de test, par exemple,

#!/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
}

Ensuite, vous pouvez avoir cette fonction de comparaison:

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

et utilise:

$ qsort compare_mtime *
$ declare -p qsort_ret

pour que les fichiers du dossier actuel soient triés par heure de modification (le plus récent en premier).

REMARQUE. Ces fonctions sont du pur Bash! pas d'utilitaires externes et pas de sous-shell! ils sont sûrs pour tous les symboles amusants que vous pourriez avoir (espaces, caractères de nouvelle ligne, caractères globaux, etc.).

gniourf_gniourf
la source
1
Félicitations pour l'impressionnant Bashing qui offre une grande flexibilité en ce qui concerne les éléments d'entrée et les critères de tri. Si le tri basé sur les lignes avec les options de tri proposées sortest suffisant, une solution sort+ read -asera plus rapide à partir d'environ, disons, 20 éléments, et de plus en plus rapidement avec plus d'éléments que vous traitez. Par exemple, sur mon iMac de fin 2012 sous OSX 10.11.1 avec un Fusion Drive: matrice de 100 éléments: env. 0,03 s sec. ( qsort()) contre ca. 0,005 s. ( sort+ read -a); Tableau de 1000 éléments: env. 0,375 s. ( qsort()) contre ca. 0,014 s ( sort+ read -a).
mklement0
Agréable. Je me souviens du tri rapide des années universitaires, mais je ferai également des recherches sur le tri des bulles. Pour mes besoins de tri, j'ai des premier et deuxième éléments formant la clé suivie d'un élément de données (que je pourrais développer plus tard). Votre code pourrait être amélioré avec le nombre d'éléments clés (parm1) et le nombre d'éléments de données (parm2). Pour OP, les paramètres seraient 1 et 0. Pour moi, les paramètres seraient 2 et 1. À tous égards, votre réponse est très prometteuse.
WinEunuuchs2Unix
1
Avec un ensemble de données d'entiers de chaîne non castés, j'ai trouvé que if [ "$i" -lt "$pivot" ]; thenc'était nécessaire, sinon le "2" <"10" résolu retournait vrai. Je crois que c'est POSIX contre lexicographique; ou peut-être Inline Link .
Page2PagePro
27

Si vous n'avez pas besoin de gérer des caractères shell spéciaux dans les éléments du tableau:

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

Avec bash, vous aurez de toute façon besoin d'un programme de tri externe.

Avec zsh, aucun programme externe n'est nécessaire et les caractères spéciaux du shell sont facilement gérés:

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

ksh doit set -strier ASCIIbétiquement .

Dimitre Radoulov
la source
Très belle information de fond. Je demande presque une démonstration sur la façon dont ksh utiliserait le drapeau de l'ensemble ... mais là encore, la question est sur bash, alors ce serait plutôt hors sujet
sehe
Cela devrait fonctionner avec la plupart des implémentations de KornShell (par exemple ksh88 et pdksh ): set -A array x 'a a' d; set -s -- "${array[@]}"; set -A sorted "$@" Et, bien sûr, la commande set réinitialisera les paramètres de position actuels, le cas échéant.
Dimitre Radoulov le
Vous êtes une véritable fontaine de connaissances sur les coquillages. Je suis sûr que vous devez avoir photographics mémoire ou quelque chose, parce que ce genre de différences subtiles élude la plupart des autres membres de l'espèce de l' homme :), +1 pour le package complet de l' info
sehe
10

tl; dr :

Trier le tableau a_inet stocker le résultat dans a_out(les éléments ne doivent pas avoir de sauts de ligne incorporés [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)

Avantages par rapport à la solution d'antak :

  • Vous n'avez pas à vous soucier du globbing accidentel (interprétation accidentelle des éléments du tableau en tant que modèles de nom de fichier), donc aucune commande supplémentaire n'est nécessaire pour désactiver le globbing ( set -fet set +fpour le restaurer plus tard).

  • Vous n'avez pas à vous soucier de la réinitialisation IFSavec unset IFS. [2]


Lecture facultative: explication et exemple de code

Ce qui précède combine le code Bash avec un utilitaire externe sortpour une solution qui fonctionne avec des éléments à une seule ligne arbitraires et un tri lexical ou numérique (éventuellement par champ) :

  • Performances : pour environ 20 éléments ou plus , ce sera plus rapide qu'une solution pure Bash - de manière significative et de plus en plus une fois que vous aurez dépassé les 100 éléments.
    (Les seuils exacts dépendront de votre entrée, de votre machine et de votre plate-forme spécifiques.)

    • La raison pour laquelle il est rapide est qu'il évite les boucles Bash .
  • printf '%s\n' "${a_in[@]}" | sort effectue le tri (lexicalement, par défaut - voir sortles spécifications POSIX de ):

    • "${a_in[@]}"se développe en toute sécurité aux éléments du tableau en a_intant qu'arguments individuels , quels qu'ils soient (y compris les espaces).

    • printf '%s\n' puis imprime chaque argument - c'est-à-dire, chaque élément du tableau - sur sa propre ligne, tel quel.

  • Notez l' utilisation d'une substitution de processus ( <(...)) pour fournir la sortie triée comme entrée vers read/ readarray(via la redirection vers stdin, <), car read/ readarraydoit s'exécuter dans le shell actuel (ne doit pas s'exécuter dans un sous - shell ) pour que la variable de sortie a_outsoit visible au shell courant (pour que la variable reste définie dans le reste du script).

  • Lecture de sortla sortie dans une variable tableau :

    • Bash v4 +: readarray -t a_outlit les lignes individuelles produites par sortdans les éléments de la variable de tableau a_out, sans inclure la fin \ndans chaque élément ( -t).

    • Bash v3: readarrayn'existe pas, donc readdoit être utilisé:
      IFS=$'\n' read -d '' -r -a a_outdit readde lire dans la -avariable array ( ) a_out, lisant l'entrée entière, à travers les lignes ( -d ''), mais en le divisant en éléments de tableau par des retours à la ligne ( IFS=$'\n'. $'\n', Ce qui produit une nouvelle ligne littérale (LF ), est une chaîne dite ANSI C-quoted string ).
      ( -r, une option qui devrait pratiquement toujours être utilisée avec read, désactive la gestion inattendue des \caractères.)

Exemple de code annoté:

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

En raison de l'utilisation de l' sortoption sans option, cela produit un tri lexical (les chiffres sont triés avant les lettres et les séquences de chiffres sont traitées lexicalement, pas comme des nombres):

*
10
5
a c
b
f

Si vous vouliez un tri numérique par le premier champ, vous utiliseriez sort -k1,1nau lieu de juste sort, ce qui donne (les non-nombres trient avant les nombres et les nombres trient correctement):

*
a c
b
f
5
10

[1] à des éléments de poignée avec des sauts de ligne incorporés, utiliser la variante suivante (Bash v4 +, avec GNU sort )
readarray -d '' -t a_out < <(printf '%s\0' "${a_in[@]}" | sort -z).
La réponse utile de Michał Górny a une solution Bash v3.

[2] Tant qu'il IFS est défini dans la variante Bash v3, le changement est limité à la commande .
En revanche, ce qui suit IFS=$'\n' dans la réponse d'Antak est une affectation plutôt qu'une commande, auquel cas le IFSchangement est global .

mklement0
la source
8

Dans le voyage de 3 heures en train de Munich à Francfort (que j'ai eu du mal à atteindre car l'Oktoberfest commence demain), je pensais à mon premier message. L'utilisation d'un tableau global est une bien meilleure idée pour une fonction de tri générale. La fonction suivante gère les chaînes arbitraires (retours à la ligne, blancs, etc.):

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

Cela imprime:

3 5 a b c z y

La même sortie est créée à partir de

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

Notez que Bash utilise probablement en interne des pointeurs intelligents, de sorte que l'opération d'échange pourrait être bon marché (bien que j'en doute). Cependant, bubble_sortdémontre que des fonctions plus avancées comme merge_sortsont également à la portée du langage shell.

Andreas Spindler
la source
5
Tri à bulles? Wow .. Obama dit que "le tri par bulles ne serait pas la bonne solution" -> youtube.com/watch?v=k4RRi_ntQc8
Robottinosino
1
Eh bien, il semble que si l'O-guy voulait être intelligent, il n'avait pas senti que ce n'était pas une question à 50/50. Un prédécesseur au poste de O-guy, disons-lui que le B-guy, a déjà fait beaucoup mieux (Reynoldsburg, Ohio, octobre 2000): "Je pense que si vous savez ce que vous croyez, il est beaucoup plus facile de répondre aux questions . Je ne peux pas répondre à votre question. " Donc, ce type B sait vraiment quelque chose sur la logique booléenne. Le O-guy ne le fait pas.
Andreas Spindler
La fonction pourrait être rendue plus facilement portable en faisant de BSORT un tableau local avec un nom de n'importe quel tableau à trier. c'est- local -n BSORT="$1"à- dire au début de la fonction. Ensuite, vous pouvez exécuter bubble_sort myarraypour trier mon tableau .
johnraff
7

Une autre solution qui utilise externe sortet gère tous les caractères spéciaux (sauf pour les NUL :)). Devrait fonctionner avec bash-3.2 et GNU ou BSD sort(malheureusement, POSIX n'inclut pas -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)

Regardez d'abord la redirection d'entrée à la fin. Nous utilisons printfintégré pour écrire les éléments du tableau, terminés par zéro. Les guillemets garantissent que les éléments du tableau sont passés tels quels, et les spécificités du shell le printffont réutiliser la dernière partie de la chaîne de format pour chaque paramètre restant. Autrement dit, c'est équivalent à quelque chose comme:

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

La liste d'éléments terminés par null est ensuite transmise à sort. L' -zoption l'oblige à lire les éléments terminés par NULL, à les trier et à afficher également les éléments terminés par NULL. Si vous n'aviez besoin que des éléments uniques, vous pouvez passer -ucar il est plus portable que uniq -z. Le LC_ALL=Cgarantit un ordre de tri stable indépendamment des paramètres régionaux - parfois utile pour les scripts. Si vous voulez que le sortrespecte les paramètres régionaux, supprimez-le.

La <()construction obtient le descripteur à lire à partir du pipeline généré et y <redirige l'entrée standard de la whileboucle. Si vous avez besoin d'accéder à l'entrée standard à l'intérieur du tube, vous pouvez utiliser un autre descripteur - exercice pour le lecteur :).

Maintenant, revenons au début. L' readintégré lit la sortie du stdin redirigé. La définition de vide IFSdésactive la division de mot qui n'est pas nécessaire ici - par conséquent, readlit toute la «ligne» d'entrée dans la variable fournie unique. -rL'option désactive également le traitement d'échappement qui n'est pas souhaité ici. Enfin, -d ''définit le délimiteur de ligne sur NUL - c'est-à-dire, indique readde lire les chaînes terminées par zéro.

En conséquence, la boucle est exécutée une fois pour chaque élément de tableau terminé par zéro successif, la valeur étant stockée dans e. L'exemple place simplement les éléments dans un autre tableau mais vous préférerez peut-être les traiter directement :).

Bien sûr, ce n'est qu'une des nombreuses façons d'atteindre le même objectif. Selon moi, c'est plus simple que d'implémenter un algorithme de tri complet dans bash et dans certains cas, ce sera plus rapide. Il gère tous les caractères spéciaux, y compris les retours à la ligne, et devrait fonctionner sur la plupart des systèmes courants. Plus important encore, cela peut vous apprendre quelque chose de nouveau et de génial à propos de bash :).

Michał Górny
la source
Excellente solution et explication très utile, merci. Une extension: sans définir IFS sur vide, les espaces de début seront également éliminés - même si autrement aucun découpage de mot n'a été effectué.
Dirk Herrmann le
Au lieu d'introduire une variable locale eet de définir un IFS vide, utilisez la variable REPLY.
Robin A. Meade
2

essaye ça:

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

La sortie sera:

3
5
une
b
c
F

Problème résolu.

rsingh
la source
3
Devrait éditer ceci pour mettre la sortie dans un nouveau tableau pour répondre pleinement à sa question.
Peter Oram
2

Si vous pouvez calculer un entier unique pour chaque élément du tableau, comme ceci:

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 -> $?"

alors, vous pouvez utiliser ces entiers comme index de tableau, car Bash utilise toujours un tableau fragmenté, donc pas besoin de vous soucier des index inutilisés:

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

echo "${sorted[@]}"
  • Avantages. Vite.
  • Les inconvénients. Les éléments dupliqués sont fusionnés et il peut être impossible de mapper le contenu sur des entiers uniques 32 bits.
Xiè Jìléi
la source
Technique intéressante, j'ai utilisé une variante pour trouver des valeurs max / min sans comparaison / tri explicite. Mais , une addition non pondérée sans tenir compte de la longueur ne fonctionnera pas: "z" trie avant "aaaa", vous ne pouvez donc pas l'utiliser pour les mots comme vous l'avez montré ci-dessus.
mr.spuratic
2

tri min:

#!/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
MathQues
la source
1
array=(a c b f 3 5)
new_array=($(echo "${array[@]}" | sed 's/ /\n/g' | sort))    
echo ${new_array[@]}

Le contenu de l'écho de new_array sera:

3 5 a b c f
blp
la source
1

Il existe une solution de contournement pour le problème habituel des espaces et des retours à la ligne:

Utilisez un caractère qui n'est pas dans le tableau d'origine (comme $'\1'ou$'\4' ou similaire).

Cette fonction fait le travail:

# 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
       }

Cela triera le tableau:

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

Cela se plaindra que le tableau source contient le caractère de contournement:

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

la description

  • Nous définissons deux variables locales wa(char de contournement) et un IFS nul
  • Ensuite (avec ifs null) nous testons que tout le tableau $*.
  • Ne contient aucun caractère de voisinage [[ $* =~ [$wa] ]].
  • Si c'est le cas, envoyez un message et signalez une erreur: exit 1
  • Évitez les extensions de nom de fichier: set -f
  • Définissez une nouvelle valeur de IFS ( IFS=$'\n'), une variable de boucle xet une nouvelle ligne var ( nl=$'\n').
  • Nous imprimons toutes les valeurs des arguments reçus (le tableau d'entrée $@).
  • mais nous remplaçons toute nouvelle ligne par le caractère de contournement "${@//$nl/$wa}".
  • envoyer ces valeurs à trier sort -n .
  • et replacez toutes les valeurs triées dans les arguments de position set --.
  • Ensuite, nous attribuons chaque argument un par un (pour conserver les nouvelles lignes).
  • en boucle for x
  • vers un nouveau tableau: sorted+=(…)
  • entre guillemets pour conserver toute nouvelle ligne existante.
  • restaurer la solution de contournement sur une nouvelle ligne "${x//$wa/$nl}".
  • terminé
Isaac
la source
1

Cette question semble étroitement liée. Et BTW, voici un mergesort dans Bash (sans processus externes):

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[@]}"
Andrej Podzimek
la source
0

Je ne suis pas convaincu que vous aurez besoin d'un programme de tri externe dans Bash.

Voici mon implémentation pour l'algorithme de tri à bulles simple.

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

Cela doit imprimer:

 input: a c b f 3 5
output: 3 5 a b c f
Andreas Spindler
la source
Le tri à bulles est O(n^2). Il me semble que la plupart des algorithmes de tri utilisent un O(n lg(n))jusqu'à la douzaine d'éléments finaux. Pour les éléments finaux, le tri par sélection est utilisé.
jww
0
a=(e b 'c d')
shuf -e "${a[@]}" | sort >/tmp/f
mapfile -t g </tmp/f
Steven Penny
la source
-1

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

Dans l'esprit de bash / linux, je dirigerais le meilleur outil de ligne de commande pour chaque étape. sortfait le travail principal mais a besoin d'une entrée séparée par une nouvelle ligne au lieu d'un espace, donc le pipeline très simple ci-dessus fait simplement:

Contenu du tableau d'écho -> remplacer l'espace par une nouvelle ligne -> trier

$() est de faire écho au résultat

($()) est de mettre le "résultat écho" dans un tableau

Remarque : comme @sorontar l'a mentionné dans un commentaire à une autre question:

La partie triée = ($ (...)) utilise l'opérateur "split and glob". Vous devez désactiver glob: set -f ou set -o noglob ou shopt -op noglob ou un élément du tableau comme * sera développé en une liste de fichiers.

Michael
la source
Dans l'esprit de bash / linux : je suppose que vous n'avez pas du tout compris l'esprit. Votre code est complètement cassé (extension du chemin et division des mots). Ce serait mieux (Bash≥4):, mapfile -t sorted < <(printf '%s\n' "${array[@]}" | sort)sinon sorted=(); while IFS= read -r line; do sorted+=( "$line" ); done < <(printf '%s\n' | sort).
gniourf_gniourf
Les antipatterns que vous utilisez sont:: echo ${array[@]} | tr " " "\n"cela se cassera si les champs du tableau contiennent des espaces et des caractères glob. De plus, il génère un sous-shell et utilise une commande externe inutile. Et en raison d' echoêtre stupide, il se cassera si votre tableau commence par -e, -Eou -n. Au lieu d' utiliser: printf '%s\n' "${array[@]}". L'autre antipattern est: ($())est de mettre le "résultat écho" dans un tableau . Certainement pas! c'est un horrible anti-modèle qui se brise à cause de l'expansion du chemin (globbing) et de la division des mots. N'utilisez jamais cette horreur.
gniourf_gniourf
La réponse la plus élevée a l '"horrible antipattern". Et il y a moyen de voter contre la réponse de quelqu'un d'autre à la question à laquelle vous avez vous-même répondu.
michael