Structures de données et algorithmes (5) Algorithmes de tri

image-20220821224607720

Algorithme de tri

Félicitations à tous mes amis pour être venus à la dernière partie : Algorithmes de tri . L'étude des structures de données et des algorithmes touche à sa fin. La persistance est la victoire !

Les données d'un tableau sont à l'origine désordonnées, mais en raison des besoins, nous devons les organiser dans l'ordre. Pour trier le tableau, nous avons déjà expliqué le tri à bulles et le tri rapide (facultatif) dans l' article sur la programmation en langage C. ,

Avant de commencer, commençons par le tri à bulles.

Tri de base

Tri à bulles

Le tri des bulles a été expliqué dans le chapitre sur la programmation en langage C. Le cœur du tri des bulles est l'échange. Grâce à un échange continu, les grands éléments sont poussés petit à petit vers une extrémité. À chaque tour, le plus grand élément sera disposé à la position correspondante. , et enfin passer commande. Site Web de démonstration d'algorithme : https://visualgo.net/zh/sorting?slide=2-2

Supposons que la longueur du tableau soit N, le processus détaillé est :

  • Au total, N tours de tri sont effectués.
  • Chaque tour de tri commence à partir de l'élément le plus à gauche du tableau et compare les deux éléments. Si l'élément de gauche est supérieur à l'élément de droite, alors les positions des deux éléments sont échangées, sinon elles restent inchangées.
  • Chaque tour de tri poussera le plus gros des éléments restants vers l'extrême droite, et le tri suivant ne prendra plus en compte ces éléments qui se trouvent déjà à la position correspondante.

Par exemple, le tableau suivant :

image-20220904212453328

Puis lors du premier tour de tri, comparez d’abord les deux premiers éléments :

image-20220904212608834

Nous constatons que le premier est plus grand, nous devons donc l'échanger à ce moment-là. Après l'échange, continuez à comparer les deux éléments suivants vers l'arrière :

image-20220904212637156

On constate que ce dernier est plus grand et inchangé, continuez à regarder les deux derniers :

image-20220904212720898

À ce stade, le premier est plus grand, échange et continue de comparer les éléments suivants :

image-20220904212855292

Ce dernier est-il plus grand, continuez à échanger, puis comparez à l'envers :

image-20220904212942212

Ce dernier est encore plus grand. Nous constatons que tant qu'il s'agit du plus grand élément, il sera rejeté dans chaque comparaison :

image-20220904213034375

Enfin, le plus grand élément du tableau actuel est placé au premier plan et ce tour de tri est terminé. Comme le plus grand élément a été placé à la position correspondante, au deuxième tour, nous n'avons besoin de considérer que les éléments qui le précèdent, c'est-à-dire est Can:

image-20220904213115671

De cette façon, nous pouvons continuer à jeter le plus grand à l'extrême droite. Après les N derniers tours de tri, nous aurons un tableau ordonné.

Le code du programme est le suivant :

void bubbleSort(int arr[], int size){
    
    
    for (int i = 0; i < size; ++i) {
    
    
        for (int j = 0; j < size - i - 1; ++j) {
    
    
            //注意需要到N-1的位置就停止,因为要比较j和j+1
            //这里减去的i也就是已经排好的不需要考虑了
            if(arr[j] > arr[j + 1]) {
    
       //如果后面比前面的小,那么就交换
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
    }
}

C'est juste que ce code reste le tri à bulles le plus primitif, et nous pouvons l'optimiser :

  1. En fait, le tri ne nécessite pas N tours, mais N - 1 tours. Comme un seul élément n'est pas trié au dernier tour, cela équivaut à être trié, il n'est donc pas nécessaire de le considérer à nouveau.
  2. S'il n'y a pas d'échange pendant tout le cycle de tri, cela signifie que le tableau est déjà en ordre et qu'il n'y a aucune situation où le précédent est plus grand que le dernier.

Alors, améliorons-le :

void bubbleSort(int arr[], int size){
    
    
    for (int i = 0; i < size - 1; ++i) {
    
       //只需要size-1次即可
        _Bool flag = 1;   //这里使用一个标记,默认为1表示数组是有序的
        for (int j = 0; j < size - i - 1; ++j) {
    
    
            if(arr[j] > arr[j + 1]) {
    
    
                flag = 0;    //如果发生交换,说明不是有序的,把标记变成0
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
        if(flag) break;   //如果没有发生任何交换,flag一定是1,数组已经有序,所以说直接结束战斗
    }
}

De cette façon, nous avons fini d’écrire une version optimisée du tri à bulles.

Bien sûr, au final, nous devons introduire un concept supplémentaire : la stabilité du tri , alors qu'est-ce que la stabilité ? Si l’ordre de deux éléments de même taille reste inchangé avant et après le tri, l’algorithme de tri est stable. Le tri à bulles que nous venons d'introduire n'effectuera l'échange que si le premier est supérieur au second, cela n'affectera donc pas l'ordre des deux éléments initialement égaux. Par conséquent, le tri à bulles est un algorithme de tri stable .

tri par insertion

Introduisons un nouvel algorithme de tri, le tri par insertion, qui devrait être appelé tri par insertion directe pour être précis. Son idée de base est la même que lorsque nous jouons à Landlord.

image-20220904214541199

Je pense que vous y avez tous joué. Avant le début de chaque tour de jeu, nous devons tirer des cartes de la pile de cartes. Après avoir tiré les cartes, l'ordre des cartes dans nos mains peut être chaotique. Cela ne fonctionnera certainement pas . Il n'y a pas de cartes. Redressez-vous comment savoir quelles cartes en ont combien ? Afin de le rendre ordonné, nous insérerons les cartes nouvellement tirées dans les positions correspondantes selon l'ordre des cartes, afin de ne pas avoir à trier les cartes entre nos mains plus tard.

Le tri par insertion a en fait le même principe : par défaut, les cartes précédentes sont déjà triées (seule la première carte est dans l'ordre au début), et on va parcourir les parties restantes les unes à côté des autres, puis les insérer. position correspondante devant, adresse de démonstration d'animation : https://visualgo.net/zh/sorting

Supposons que la longueur du tableau soit N, le processus détaillé est :

  • Au total, N tours de tri sont effectués.
  • Chaque tour de tri sélectionnera un élément de l'arrière et le comparera avec les éléments précédemment triés de l'arrière vers l'avant jusqu'à ce qu'un élément pas plus grand que l'élément actuel soit rencontré, et l'élément actuel sera inséré devant cet élément.
  • Une fois qu'un élément est inséré, tous les éléments suivants sont reculés d'une position.
  • Lorsque tous les éléments suivants sont traversés et insérés dans les positions correspondantes, le tri est terminé.

Par exemple, le tableau suivant :

image-20220904212453328

À ce stade, nous supposons que le premier élément est déjà en ordre et nous commençons à examiner le deuxième élément :

image-20220904221510897

Retirez-le et comparez-le avec la séquence ordonnée précédente d'arrière en avant. La première comparaison est 4, et elle s'avère être plus petite que 4. Continuez à avancer et vous constatez que vous avez atteint la fin, vous pouvez donc placez-le simplement au premier plan. Remarque Avant de le placer au premier plan, reculez les éléments suivants pour libérer de l'espace :

image-20220904221648492

Insérez-le ensuite :

image-20220904221904359

Maintenant que les deux premiers éléments sont dans un état ordonné, continuons à examiner le troisième élément :

image-20220904221938583

Toujours en regardant de l'arrière vers l'avant, nous avons constaté que nous rencontrions 7 et 4 lorsque nous arrivions, nous l'avons donc mis directement à cette position :

image-20220904222022949

Maintenant que les trois premiers éléments sont tous en ordre, continuons à examiner le quatrième élément :

image-20220904222105375

En comparant successivement, nous avons constaté qu'aucun élément inférieur à 1 n'était trouvé à la fin, nous avons donc reculé les trois premiers éléments :

image-20220904222145903

Insérez 1 dans la position correspondante :

image-20220904222207544

Maintenant que les quatre premiers éléments sont tous dans un état ordonné, il ne nous reste plus qu'à terminer le parcours des éléments suivants de la même manière. Le résultat final est un tableau ordonné. Essayons d'écrire du code :

void insertSort(int arr[], int size){
    
    
    for (int i = 1; i < size; ++i) {
    
       //从第二个元素开始看
        int j = i, tmp = arr[i];   //j直接变成i,因为前面的都是有序的了,tmp相当于是抽出来的牌暂存一下
        while (j > 0 && arr[j - 1] > tmp) {
    
       //只要j>0并且前一个还大于当前待插入元素,就一直往前找
            arr[j] = arr[j - 1];   //找的过程中需要不断进行后移操作,把位置腾出来
            j--;
        }
        arr[j] = tmp;  //j最后在哪个位置,就是是哪个位置插入
    }
}

Bien entendu, ce code peut aussi être amélioré, car on passe trop de temps à comparer chacun pour trouver la position d'insertion, car la partie précédente des éléments est déjà dans un état ordonné, on peut envisager d'utiliser l'algorithme de recherche binaire pour trouver le position d'insertion correspondante. , ce qui permet de gagner du temps dans la recherche du point d'insertion :

int binarySearch(int arr[], int left, int right, int target){
    
    
    int mid;
    while (left <= right) {
    
    
        mid = (left + right) / 2;
        if(target == arr[mid]) return mid + 1;   //如果插入元素跟中间元素相等,直接返回后一位
        else if (target < arr[mid])  //如果大于待插入元素,说明插入位置肯定在左边
            right = mid - 1;   //范围划到左边
        else   
            left = mid + 1;   //范围划到右边
    }
    return left;   //不断划分范围,left也就是待插入位置了
}

void insertSort(int arr[], int size){
    
    
    for (int i = 1; i < size; ++i) {
    
    
        int tmp = arr[i];
        int j = binarySearch(arr, 0, i - 1, tmp);   //由二分搜索来确定插入位置
        for (int k = i; k > j; k--) arr[k] = arr[k - 1];   //依然是将后面的元素后移
        arr[j] = tmp;
    }
}

Enfin, discutons de la stabilité de l’algorithme de tri par insertion. Ensuite, le tri par insertion sans optimisation continue d'attendre pour trouver un élément qui n'est pas plus grand que l'élément à insérer. Par conséquent, lorsqu'un élément égal est rencontré, il ne sera inséré qu'après lui, et l'ordre d'origine des mêmes éléments ne sera pas modifié. On dit donc que le tri par insertion est également un algorithme de tri stable (mais il devient instable après l'utilisation ultérieure de l'optimisation de la recherche binaire. Par exemple, s'il y a deux éléments égaux consécutifs dans un tableau ordonné, et maintenant un autre égal L'élément égal arrive, celui du milieu vient d'être trouvé. C'est l'élément égal classé en premier et revient à la position suivante. L'élément nouvellement inséré poussera l'élément égal initialement classé deuxième vers l'arrière.)

tri par sélection

Regardons le dernier tri par sélection (pour être précis, il devrait s'agir d'un tri par sélection directe). Ce tri est également plus facile à comprendre. On va juste à l'arrière à chaque fois pour trouver le plus petit et on le met devant. Site de démonstration d'algorithme : https://visualgo.net/en/sorting

Supposons que la longueur du tableau soit N, le processus détaillé est :

  • Au total, N tours de tri sont effectués.
  • Chaque cycle de tri trouvera le plus petit élément parmi tous les éléments suivants, puis l'échangera avec la position suivante qui a été triée.
  • Après N tours d’échange, un tableau ordonné est obtenu.

Par exemple, le tableau suivant :

image-20220904212453328

Le premier tri nécessite de trouver le plus petit élément de tout le tableau et de l'échanger avec le premier élément :

image-20220905141347927

Après l'échange, le premier élément est déjà en ordre, et on continue à trouver le plus petit parmi les éléments restants :

image-20220905141426011

A ce moment-là, 2 se trouve en deuxième position. Faisons semblant de l'intervertir pour que les deux premiers éléments soient déjà dans l'ordre. Regardons le reste :

image-20220905141527050

À ce stade, on constate que 3 est le plus petit, il est donc directement permuté à la position du troisième élément :

image-20220905141629207

De cette façon, les trois premiers éléments sont tous ordonnés. En échangeant continuellement de cette manière, le tableau que nous obtenons finalement est un tableau ordonné. Essayons d'écrire du code :

void selectSort(int arr[], int size){
    
    
    for (int i = 0; i < size - 1; ++i) {
    
       //因为最后一个元素一定是在对应位置上的,所以只需要进行N - 1轮排序
        int min = i;   //记录一下当前最小的元素,默认是剩余元素中的第一个元素
        for (int j = i + 1; j < size; ++j)   //挨个遍历剩余的元素,如果遇到比当前记录的最小元素还小的元素,就更新
            if(arr[min] > arr[j])
                min = j;
        int tmp = arr[i];    //找出最小的元素之后,开始交换
        arr[i] = arr[min];
        arr[min] = tmp;
    }
}

Bien sûr, on peut aussi optimiser le tri de sélection, car à chaque fois qu'on a besoin de sélectionner le plus petit, autant sélectionner le plus grand, jeter le petit vers la gauche, et jeter le grand vers la droite, pour que l'on puisse peut avoir le double Le double de l'efficacité est atteint.

void swap(int * a, int * b){
    
    
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

void selectSort(int arr[], int size){
    
    
    int left = 0, right = size - 1;   //相当于左端和右端都是已经排好序的,中间是待排序的,所以说范围不断缩小
    while (left < right) {
    
    
        int min = left, max = right;
        for (int i = left; i <= right; i++) {
    
    
            if (arr[i] < arr[min]) min = i;   //同时找最小的和最大的
            if (arr[i] > arr[max]) max = i;
        }
        swap(&arr[max], &arr[right]);   //这里先把大的换到右边
        //注意大的换到右边之后,有可能被换出来的这个就是最小的,所以说需要判断一下
        //如果遍历完发现最小的就是当前右边排序的第一个元素
        //此时因为已经被换出来了,所以说需要将min改到换出来的那个位置
        if (min == right) min = max;
        swap(&arr[min], &arr[left]);   //接着把小的换到左边
        left++;    //这一轮完事之后,缩小范围
        right--;
    }
}

Enfin, analysons la stabilité du tri par sélection. Tout d'abord, le tri par sélection sélectionne le plus petit à chaque fois. Lors de l'insertion en avant, l'opération d'échange sera effectuée directement. Par exemple, la séquence d'origine est 3,3,1 et 1 est sélectionné à ce moment-là. C'est le plus petit élément et il est échangé avec les 3 premiers. Après l'échange, les 3 initialement classés en premier vont jusqu'à la fin, détruisant l'ordre d'origine. Par conséquent, le tri par sélection est un algorithme de tri instable .

Résumons les trois algorithmes de tri que nous avons appris ci-dessus. Supposons que la longueur du tableau à trier soitn :

  • Tri à bulles (version optimisée) :
    • Complexité temporelle dans le meilleur des cas : O ( n ) O(n)O ( n ) , s'il est ordonné, alors nous n'avons besoin que d'un seul parcours. Lorsque la marque détecte qu'aucun échange n'a eu lieu, il se terminera directement, donc cela pourra être fait une fois.
    • Complexité temporelle dans le pire des cas : O ( n 2 ) O(n^2)O ( n2 ), c'est-à-dire que chaque tour est rempli brusquement, comme un tableau complètement inversé.
    • **Complexité spatiale :** Étant donné qu'une seule variable est nécessaire pour stocker temporairement les variables qui doivent être échangées, la complexité spatiale est O ( 1 ) O (1)O ( 1 )
    • **Stabilité : **Stable
  • Tri par insertion:
    • Complexité temporelle dans le meilleur des cas : O ( n ) O(n)O ( n ) , s'il est ordonné, car la position d'insertion est également la même position, lorsque le tableau lui-même est ordonné, nous n'avons pas besoin de modifier d'autres éléments à chaque tour.
    • Complexité temporelle dans le pire des cas : O ( n 2 ) O(n^2)O ( n2 ), par exemple, un tableau complètement inversé sera comme ceci : à chaque tour, vous devez trouver complètement l'insertion avant.
    • Complexité spatiale : Une seule variable est nécessaire pour stocker les éléments extraits, la complexité spatiale est donc O ( 1 ) O (1)O ( 1 )
    • **Stabilité : **Stable
  • Sélectionnez le tri :
    • Complexité temporelle dans le meilleur des cas : O ( n 2 ) O(n^2)O ( n2 ), même si le tableau lui-même est ordonné, chaque tour doit toujours trouver les parties restantes une par une avant que le plus petit élément puisse être déterminé, donc l'ordre carré est toujours requis.
    • Complexité temporelle dans le pire des cas : O ( n 2 ) O(n^2)O ( n2 ), inutile d'en dire plus.
    • Complexité spatiale : Chaque tour n'a besoin d'enregistrer que la plus petite position de l'élément, donc la complexité spatiale est O ( 1 ) O (1)O ( 1 )
    • **Stabilité : **Instable

Le tableau est le suivant, n'oubliez pas :

Algorithme de tri le meilleur cas de scenario pire scénario complexité de l'espace la stabilité
Tri à bulles O (n) O(n)O ( n ) O ( n 2 ) O (n ^ 2)O ( n2 ) O (1) O(1)O ( 1 ) Stabiliser
tri par insertion O (n) O(n)O ( n ) O ( n 2 ) O (n ^ 2)O ( n2 ) O (1) O(1)O ( 1 ) Stabiliser
tri par sélection O ( n 2 ) O (n ^ 2)O ( n2 ) O ( n 2 ) O (n ^ 2)O ( n2 ) O (1) O(1)O ( 1 ) instable

Tri avancé

Plus tôt, nous avons introduit trois algorithmes de tri de base, et leur complexité temporelle moyenne a atteint O ( n 2 ) O(n^2)O ( n2 ), peut-on alors trouver un algorithme de tri plus rapide ? Dans cette partie, nous continuerons à présenter les versions avancées des trois algorithmes de tri précédents.

Tri rapide

Dans le chapitre de programmation en langage C, nous avons également introduit le tri rapide. Le tri rapide est une version avancée du tri à bulles. Dans le tri à bulles, la comparaison et l'échange d'éléments sont effectués entre des éléments adjacents. L'échange de chaque élément ne peut se déplacer que d'une seule position, donc le le nombre de comparaisons et de mouvements est élevé et l'efficacité est relativement faible. Dans le tri rapide, la comparaison et l'échange des éléments sont effectués des deux extrémités vers le milieu. Les éléments plus grands peuvent être permutés vers les positions ultérieures en un tour, tandis que les éléments plus petits peuvent être permutés vers les positions avant en un tour. Chaque mouvement se déplace plus loin. , il y a donc moins de comparaisons et de mouvements, et tout comme son nom, c'est plus rapide.

En fait, le but de chaque tour de tri rapide est de jeter les gros à droite du benchmark et les petits à gauche du benchmark.

Supposons que la longueur du tableau soit N, le processus détaillé est :

  • Au début, la plage de tri correspond à l'ensemble du tableau
  • Avant le tri, nous sélectionnons comme base le premier élément de toute la plage de tri et trions rapidement les éléments de la plage de tri.
  • Regardez d'abord de l'extrême droite vers la gauche et comparez tour à tour chaque élément avec l'élément de référence. S'il s'avère plus petit que l'élément de référence, échangez-le avec l'élément à la position de traversée gauche (la position de l'élément de référence au début), et conservez-le à ce moment La position actuelle parcourue à droite.
  • Après l'échange, l'élément est parcouru de gauche à droite. S'il s'avère plus grand que l'élément de base, il est échangé avec l'élément à la position traversée à droite précédemment réservée. La position actuelle à gauche est également conservée, et l'étape précédente est exécutée en boucle.
  • Lorsque les parcours gauche et droit entrent en collision, ce tour de tri rapide est terminé et la position finale au milieu est la position de l'élément de base.
  • En prenant la position de référence comme centre, divisez les côtés gauche et droit et effectuez un tri rapide de la même manière.

Par exemple, le tableau suivant :

image-20220904212453328

Tout d'abord, nous sélectionnons comme élément de base le premier élément 4. Initialement, les pointeurs gauche et droit sont situés aux deux extrémités :

image-20220905210056432

A ce moment-là, commencez à regarder de droite à gauche jusqu'à ce que vous rencontriez un élément plus petit que 4. Le premier est 6, ce qui n'est certainement pas le cas.

image-20220905210625181

À ce stade, continuez à comparer 3 et 4 et constatez qu'il est plus petit que 4. Ensuite, échangez directement 3 (en fait, écrasez-le directement) à la position de l'élément pointée par le pointeur gauche :

image-20220905210730105

À ce stade, nous nous tournons pour regarder de gauche à droite. Si nous rencontrons un élément plus grand que 4, nous l'échangeons vers le pointeur de droite. 3 n'est définitivement plus là, car il a juste ralenti, et puis il y a 2 :

image-20220905210851474

2 n'est pas aussi grand que 4, donc si nous continuons à regarder en arrière, 7 est plus grand que 4 en ce moment, alors continuez à échanger :

image-20220905211300102

Puis, il recommença à regarder de droite à gauche :

image-20220905211344027

A ce moment, 5 est plus grand que 4. Si on continue d'avancer, on constate que 1 est plus petit que 4, donc on continue à échanger :

image-20220905211427939

Puis il se tourne pour regarder de gauche à droite. A ce moment, les deux pointeurs entrent en collision, le tri se termine, et les positions pointées par les deux derniers pointeurs sont les positions des éléments de base :

image-20220905211543845

Après ce tour de tri rapide, le côté gauche n'est peut-être pas tout en ordre, mais il doit être plus petit que l'élément de base, et le côté droit doit être plus grand que l'élément de base. Ensuite, nous prenons le benchmark comme centre et le divisons en deux parties pour trier à nouveau rapidement :

image-20220905211741787

De cette façon, nous pouvons enfin mettre l'ensemble du tableau en ordre. Bien sûr, il existe d'autres façons de dire un tri rapide. Certaines d'entre elles consistent à trouver à la fois les parties gauche et droite, puis à les échanger. Ce que nous avons ici est de lancer éloignez-les dès qu'ils sont retrouvés. Maintenant que l'idée est claire, essayons d'implémenter le tri rapide :

void quickSort(int arr[], int start, int end){
    
    
    if(start >= end) return;    //范围不可能无限制的划分下去,要是范围划得都没了,肯定要结束了
    int left = start, right = end, pivot = arr[left];   //这里我们定义两个指向左右两个端点的指针,以及取出基准
    while (left < right) {
    
         //只要两个指针没相遇,就一直循环进行下面的操作
        while (left < right && arr[right] >= pivot) right--;   //从右向左看,直到遇到比基准小的
        arr[left] = arr[right];    //遇到比基准小的,就丢到左边去
        while (left < right && arr[left] <= pivot) left++;   //从左往右看,直到遇到比基准大的
        arr[right] = arr[left];    //遇到比基准大的,就丢到右边去
    }
    arr[left] = pivot;    //最后相遇的位置就是基准存放的位置了
    quickSort(arr, start, left - 1);   //不包含基准,划分左右两边,再次进行快速排序
    quickSort(arr, left + 1, end);
}

De cette façon, nous implémentons un tri rapide. Analysons la stabilité du tri rapide. Le tri rapide consiste à échanger directement des éléments plus petits ou plus grands que la référence. Par exemple, le tableau d'origine est : 2, 2, 1. À ce stade, le premier élément est utilisé comme référence . . D'abord, le 1 de droite sera renversé et deviendra : 1, 2, 1, puis de gauche à droite, car il ne sera modifié que lorsqu'il rencontrera un élément plus grand que la référence 2, donc la référence finale sera placée en dernière position : 1, 2, 2. A ce moment, 2 qui aurait dû être devant est passé à l'arrière. Par conséquent, l'algorithme de tri rapide est un algorithme de tri instable .

Tri rapide sur deux axes (facultatif)

Ici, nous devons ajouter une version améliorée supplémentaire du tri rapide, du tri rapide à deux axes . La classe d'outils de tableau du langage Java utilise cette méthode de tri pour trier les grands tableaux. Jetons un coup d'œil aux améliorations apportées par rapport au tri rapide. Tout d’abord, l’algorithme de tri rapide ordinaire peut ressembler à ceci lorsqu’il est confronté à des situations extrêmes :

image-20220906131959909

Il se trouve que l'ensemble du tableau est dans l'ordre inverse, cela équivaut donc à rechercher d'abord l'ensemble du tableau, puis à mettre 8 à la dernière position. À ce moment-là, le premier tour se termine :

image-20220906132112592

Puisque 8 va directement à l'extrême droite, il n'y a pas de moitié droite à ce moment-là, seulement la moitié gauche. A ce moment, la moitié gauche continue d'être triée rapidement :

image-20220906132244369

A ce moment, 1 est à nouveau le plus petit élément, donc lorsque le parcours est terminé, 1 est toujours à cette position. A ce moment, il n'y a pas de moitié gauche, seulement la moitié droite :

image-20220906132344525

A cette époque, le repère est 7, qui est le plus grand. C'est vraiment pas de chance. Après s'être arrangé, 7 est allé à l'extrême gauche, et il n'y a toujours pas de moitié droite :

image-20220906132437765

Nous avons constaté que dans ce cas extrême, chaque tour doit parcourir complètement toute la plage, et chaque tour aura un élément le plus grand ou le plus petit poussé des deux côtés. N'est-ce pas un tri à bulles ? Par conséquent, dans des cas extrêmes, le tri rapide dégénérera en tri à bulles, donc un tri rapide sélectionnera aléatoirement l'élément de référence. Afin de résoudre ce problème qui se produit dans les cas extrêmes, nous pouvons ajouter un autre élément de base, de sorte que même si une situation extrême se produit, à moins que les deux côtés ne soient des éléments minimum ou des éléments maximum, au moins une base puisse être segmentée normalement, et les situations extrêmes se produire La probabilité sera également considérablement réduite :

image-20220906132945691

À ce stade, le premier élément et le dernier élément sont tous deux utilisés comme éléments de référence et l'ensemble du retour est divisé en trois segments. En supposant que la ligne de base 1 est plus petite que la ligne de base 2, alors tous les éléments stockés dans le premier segment doivent être plus petits que ligne de base 1, et tous les éléments stockés dans le deuxième segment doivent être plus petits que la ligne de base 1. Il ne doit pas être inférieur à la base 1 et pas supérieur à la base 2. Tous les éléments stockés dans la troisième section doivent être supérieurs à la base 2 :

image-20220906133219853

Par conséquent, après avoir été divisés en trois segments, après chaque cycle de tri rapide à deux axes, les trois segments doivent être poursuivis avec un tri rapide à deux axes. Enfin, l'ensemble du tableau peut être commandé. Bien sûr, quelles quantités sont cet algorithme de tri Plus approprié pour ? Pour les tableaux relativement grands, si la quantité est relativement faible, étant donné que le tri rapide à deux axes doit effectuer tant d'opérations, il n'est en fait pas aussi rapide que le tri par insertion.

Simulons le fonctionnement du tri rapide sur deux axes :

image-20220906140255444

Tout d'abord, retirez le premier élément et le dernier élément comme deux repères, puis nous devons les comparer. Si le repère 1 est supérieur au repère 2, alors les deux repères doivent d'abord être échangés, mais ici parce que 4 est inférieur à 6 , il n'est pas nécessaire d'échanger.

À ce stade, nous devons créer trois pointeurs :

image-20220906140538076

Puisqu'il y a trois zones, la position du pointeur bleu et la zone à sa gauche sont toutes deux plus petites que le repère 1, la zone allant de la gauche du pointeur orange au pointeur bleu n'est pas plus petite que le repère 1 ni plus grande que le repère 2, le position du pointeur vert et La zone à droite est supérieure au repère 2, et la zone entre le pointeur orange et le pointeur vert est la zone à trier.

Tout d’abord, on part de l’élément pointé par le pointeur orange pour juger, qui peut être divisé en trois situations :

  • S'il est inférieur à la base 1, vous devez d'abord déplacer le pointeur bleu vers l'arrière, échanger les éléments vers le pointeur bleu, puis déplacer le pointeur orange vers l'arrière.
  • S’il n’est pas inférieur à la base 1 ni supérieur à la base 2, alors vous n’avez rien à faire, déplacez simplement le pointeur orange vers l’avant, car il se situe dans cette plage.
  • S'il est supérieur au repère 2, alors il doit être lancé vers la droite. Déplacez d'abord le pointeur droit vers la gauche et continuez à avancer pour en trouver un qui n'est pas plus grand que le repère 2, afin qu'il puisse être échangé en douceur.

Tout d'abord, jetons un coup d'oeil. À ce stade, le pointeur orange pointe vers 2, donc 2 est inférieur à la base 1. Nous devons d'abord déplacer le pointeur bleu vers l'arrière, puis échanger les éléments sur les pointeurs orange et bleu, mais ici parce qu'ils sont le même Un, donc il reste inchangé. À ce moment, les deux pointeurs ont reculé d'une position :

image-20220906141556398

De même, continuons à regarder l'élément pointé par le pointeur orange : à ce moment, il est 7, ce qui est supérieur à la base 2. Ensuite, il faut trouver un élément à droite qui n'est pas supérieur à la base 2 :

image-20220906141653453

Le pointeur vert cherche de droite à gauche, à ce moment il en trouve 3, et échange directement les éléments pointeur orange et pointeur bleu :

image-20220906141758610

Au tour suivant, continuez à regarder l'élément de pointeur orange. À ce moment-là, on constate qu'il est plus petit que la référence 1, alors déplacez d'abord le pointeur bleu vers l'avant et constatez qu'il et l'orange sont à nouveau ensemble. L'échange est le même que non. À ce moment, les deux pointeurs sont reculés d'une position :

image-20220906141926006

Le nouveau tour continue de regarder l'élément pointé par le pointeur orange. À ce stade, nous constatons que 1 est également plus petit que la ligne de base 1. Déplacez d'abord le pointeur bleu, puis échangez, puis déplacez le pointeur orange. Comme ci-dessus , échange la solitude :

image-20220906142041202

A ce moment, le pointeur orange pointe vers 8, qui est supérieur à la base 2. Ensuite, vous devez également en trouver un autre à droite qui n'est pas supérieur à la base 2 pour l'échange :

image-20220906142134949

A ce moment, trouvez-en 5, remplissez les conditions et échangez :

image-20220906142205055

Nous continuons à regarder le pointeur orange et constatons que l'élément du pointeur orange n'est pas inférieur à la base 1 ni supérieur à la base 2. Ensuite, selon les règles précédentes, il suffit de déplacer le pointeur orange vers l'avant :

image-20220906142303329

A ce moment, le pointeur orange et le pointeur vert entrent en collision, et il n'y a plus d'éléments à trier. Enfin, on échange les deux éléments de référence situés aux deux extrémités avec les pointeurs correspondants. La référence 1 est échangée avec le pointeur bleu, et la référence 2 est échangée avec le pointeur vert. :

image-20220906142445417

Les trois zones séparées à ce moment-là remplissent tout juste les conditions. Bien sûr, avec un peu de chance ici, l'ensemble du réseau sera en ordre. Cependant, selon l'itinéraire normal, nous devons continuer à effectuer un tri rapide sur deux axes sur le reste. trois zones. Enfin, le tri est terminé.

Essayons maintenant d'écrire le code pour le tri rapide sur deux axes :

void dualPivotQuickSort(int arr[], int start, int end) {
    
    
    if(start >= end) return;     //首先结束条件还是跟之前快速排序一样,因为不可能无限制地分下去,分到只剩一个或零个元素时该停止了
    if(arr[start] > arr[end])    //先把首尾两个基准进行比较,看看谁更大
        swap(&arr[start], &arr[end]);    //把大的换到后面去
    int pivot1 = arr[start], pivot2 = arr[end];    //取出两个基准元素
    int left = start, right = end, mid = left + 1;   //因为分了三块区域,此时需要三个指针来存放
    while (mid < right) {
    
        //因为左边冲在最前面的是mid指针,所以说跟之前一样,只要小于right说明mid到right之间还有没排序的元素
        if(arr[mid] < pivot1)     //如果mid所指向的元素小于基准1,说明需要放到最左边
            swap(&arr[++left], &arr[mid++]);   //直接跟最左边交换,然后left和mid都向前移动
        else if (arr[mid] <= pivot2) {
    
        //在如果不小于基准1但是小于基准2,说明在中间
            mid++;   //因为mid本身就是在中间的,所以说只需要向前缩小范围就行
        } else {
    
        //最后就是在右边的情况了
            while (arr[--right] > pivot2 && right > mid);  //此时我们需要找一个右边的位置来存放需要换过来的元素,注意先移动右边指针
            if(mid >= right) break;   //要是把剩余元素找完了都还没找到一个比基准2小的,那么就直接结束,本轮排序已经完成了
            swap(&arr[mid], &arr[right]);   //如果还有剩余元素,说明找到了,直接交换right指针和mid指针所指元素
        }
    }
    swap(&arr[start], &arr[left]);    //最后基准1跟left交换位置,正好左边的全部比基准1小
    swap(&arr[end], &arr[right]);     //最后基准2跟right交换位置,正好右边的全部比基准2大
    dualPivotQuickSort(arr, start, left - 1);    //继续对三个区域再次进行双轴快速排序
    dualPivotQuickSort(arr, left + 1, right - 1);
    dualPivotQuickSort(arr, right + 1, end);
}

Cette partie est uniquement facultative et n’est pas obligatoire.

Tri des collines

Le tri par colline est une version avancée du tri par insertion directe (le tri par colline est également appelé tri incrémentiel rétractable ). Bien que le tri par insertion soit facile à comprendre, dans les cas extrêmes, tous les éléments triés seront déplacés vers l'arrière (par exemple, si vous souhaitez simplement insérer (est un élément particulièrement petit)) Afin de résoudre ce problème, le tri Hill améliore le tri par insertion. Il regroupe l'ensemble du tableau en fonction de la taille du pas et compare d'abord les éléments les plus éloignés.

Cette taille de pas est déterminée par une séquence d'incréments. Cette séquence d'incréments est très critique. Un grand nombre d'études ont montré que lorsque la séquence d'incréments dlta[k] = 2^(t-k+1)-1(0<=k<=t<=(log2(n+1)))est n } {2}2nn 4 \frac {n} {4}4nn 8 \frac {n} {8}8n,...,1 de cette séquence incrémentielle.

Supposons que la longueur du tableau soit N, le processus détaillé est :

  • Trouvez d’abord la taille du pas initial, qui est n/2.
  • Nous regroupons l'ensemble du tableau selon la taille du pas, c'est-à-dire deux par deux (si n est un nombre impair, le premier groupe aura trois éléments)
  • Nous effectuons respectivement un tri par insertion au sein de ces groupes.
  • Une fois le tri terminé, nous regroupons les étapes par /2 et répétons les étapes ci-dessus jusqu'à ce que l'étape soit 1 et que la dernière passe de tri par insertion soit terminée.

Dans ce cas, comme l'ordre dans le groupe a été ajusté une fois, les petits éléments sont disposés le plus tôt possible. Même si de petits éléments doivent être insérés lors du dernier tri, il n'y aura pas trop d'éléments à reculer. . .

Prenons comme exemple le tableau suivant :

image-20220905223505975

Tout d'abord, la longueur du tableau est de 8. Divisez 2 directement pour obtenir 34. Ensuite, la taille du pas est de 4. Nous regroupons selon la taille du pas de 4 :

image-20220905223609936

Parmi eux, 4 et 8 sont le premier groupe, 2 et 5 sont le deuxième groupe, 7 et 3 sont le troisième groupe et 1 et 6 sont le quatrième groupe. Nous effectuons respectivement un tri par insertion au sein de ces quatre groupes. Après le tri dans le groupe, le résultat est :

image-20220905223659584

Vous pouvez voir que les petits éléments actuels avancent le plus possible, même s'ils ne sont pas encore en ordre. Ensuite, nous réduisons la taille du pas, 4/2=2, et divisons selon cette taille du pas :

image-20220905223804907

À ce stade, 4, 3, 8 et 7 forment un groupe, et 2, 1, 5 et 6 sont un groupe. Nous continuons à trier au sein de ces deux groupes et obtenons :

image-20220905224111803

Enfin, nous continuons à augmenter la taille du pas/2 pour obtenir 2/2 = 1. À ce stade, la taille du pas devient 1, ce qui équivaut à ce que le tableau entier soit un groupe. Nous effectuons à nouveau un tri par insertion. , nous constaterons que les petits éléments sont à gauche, et il sera très facile d'effectuer un tri par insertion à ce moment-là.

Essayons d'écrire du code maintenant :

void shellSort(int arr[], int size){
    
    
    int delta = size / 2;
    while (delta >= 1) {
    
    
        //这里依然是使用之前的插入排序,不过此时需要考虑分组了
        for (int i = delta; i < size; ++i) {
    
       //我们需要从delta开始,因为前delta个组的第一个元素默认是有序状态
            int j = i, tmp = arr[i];   //这里依然是把待插入的先抽出来
            while (j >= delta && arr[j - delta] > tmp) {
    
       
              	//注意这里比较需要按步长往回走,所以说是j - delta,此时j必须大于等于delta才可以,如果j - delta小于0说明前面没有元素了
                arr[j] = arr[j - delta];
                j -= delta;
            }
            arr[j] = tmp;
        }
        delta /= 2;    //分组插排完事之后,重新计算步长
    }
}

Bien que trois niveaux d'imbrication de boucles soient utilisés ici, la complexité temporelle réelle peut être supérieure à O ( n 2 ) O(n^2)O ( n2 )est encore petit, car il peut garantir que les petits éléments doivent se déplacer vers la gauche, donc le nombre de tri n'est en fait pas aussi grand que nous l'imaginions. Comme le processus de preuve est trop compliqué, il ne sera pas répertorié ici.

Alors, le tri Hill est-il stable ? Étant donné que nous regroupons maintenant par taille de pas, cela peut entraîner le déplacement de deux éléments identiques adjacents vers l'avant dans leur propre groupe. Par conséquent, le tri Hill est un algorithme de tri instable .

Tri en tas

Regardons le dernier point : le tri par tas est également un type de tri par sélection, mais il peut être plus rapide que le tri par sélection directe. Vous vous souvenez de la grande pile supérieure et de la petite pile supérieure que nous avons expliquées plus tôt ? Revoyons:

Pour un arbre binaire complet, si tous les nœuds pères de l'arbre sont plus petits que les nœuds enfants, nous appelons cela un petit tas racine (petit tas supérieur), et si tous les nœuds pères de l'arbre sont plus grands que les nœuds enfants, c'est un gros tas de racines.

Grâce au fait que le tas est un arbre binaire complet, nous pouvons facilement utiliser un tableau pour le représenter :

image-20220818110224673

En construisant un tas, nous pouvons saisir un tableau non ordonné en séquence, et la séquence finale stockée est une séquence organisée dans l'ordre. En profitant de cette propriété, nous pouvons facilement utiliser le tas pour le tri. Écrivons d'abord une petite pile supérieure :

typedef int E;
typedef struct MinHeap {
    
    
    E * arr;
    int size;
    int capacity;
} * Heap;

_Bool initHeap(Heap heap){
    
    
    heap->size = 0;
    heap->capacity = 10;
    heap->arr = malloc(sizeof (E) * heap->capacity);
    return heap->arr != NULL;
}

_Bool insert(Heap heap, E element){
    
    
    if(heap->size == heap->capacity) return 0;
    int index = ++heap->size;
    while (index > 1 && element < heap->arr[index / 2]) {
    
    
        heap->arr[index] = heap->arr[index / 2];
        index /= 2;
    }
    heap->arr[index] = element;
    return 1;
}

E delete(Heap heap){
    
    
    E max = heap->arr[1], e = heap->arr[heap->size--];
    int index = 1;
    while (index * 2 <= heap->size) {
    
    
        int child = index * 2;
        if(child < heap->size && heap->arr[child] > heap->arr[child + 1])
            child += 1;
        if(e <= heap->arr[child]) break;
        else heap->arr[index] = heap->arr[child];
        index = child;
    }
    heap->arr[index] = e;
    return max;
}

Il suffit ensuite d'insérer ces éléments dans le tas un par un, puis de les retirer un par un. Nous obtenons une séquence ordonnée :

int main(){
    
    
    int arr[] = {
    
    3, 5, 7, 2, 9, 0, 6, 1, 8, 4};

    struct MinHeap heap;    //先创建堆
    initHeap(&heap);
    for (int i = 0; i < 10; ++i)
        insert(&heap, arr[i]);   //直接把乱序的数组元素挨个插入
    for (int i = 0; i < 10; ++i)
        arr[i] = delete(&heap);    //然后再一个一个拿出来,就是按顺序的了

    for (int i = 0; i < 10; ++i)
        printf("%d ", arr[i]);
}

Le résultat final est :

image-20220906001134488

Bien que cela soit plus simple à utiliser, cela nécessite des O ( n ) O(n) supplémentairesO ( n ) l'espace est utilisé comme un tas, nous pouvons donc l'optimiser davantage et réduire son occupation de l'espace. Alors, comment optimiser ?Autant changer notre façon de penser et construire directement le tas pour le tableau donné.

Supposons que la longueur du tableau soit N, le processus détaillé est :

  • Redimensionnez d'abord le tableau donné dans un grand tas supérieur
  • Effectuez N tours de sélection, en sélectionnant à chaque fois l'élément en haut du tas chapiteau et en le stockant vers l'avant depuis la fin du tableau (en échangeant le haut du tas et le dernier élément du tas)
  • Une fois l'échange terminé, réajustez le nœud racine du tas afin qu'il continue à répondre aux propriétés d'un grand tas supérieur, puis répétez les opérations ci-dessus.
  • Lorsque les N tours sont terminés, vous obtenez un tableau disposé du plus petit au plus grand.

Nous transformons d’abord le tableau donné en un arbre binaire complet, en prenant le tableau suivant comme exemple :

image-20220906220020172

À ce stade, cet arbre binaire n’est pas encore un tas et notre objectif principal est de le transformer en un grand tas supérieur. Alors, comment transformer cet arbre binaire en un grand tas ? Nous devons seulement faire des ajustements en commençant par le dernier nœud non-feuille (dans l'ordre de haut en bas). Par exemple, 1 est le dernier nœud non-feuille à ce moment, donc à partir de 1, nous devons comparer. Si c'est le cas, Si le nœud enfant est plus grand que lui, alors le plus grand enfant doit être échangé. À ce stade, son nœud enfant 6 est supérieur à 1, il doit donc être échangé :

image-20220906221306519

Examinons ensuite l'avant-dernier nœud non-feuille, qui est 7. À ce stade, les deux enfants sont plus petits que lui, donc aucun ajustement n'est nécessaire. Regardons ensuite l'avant-dernier nœud non-feuille 2. , à ce stade, le deux enfants de 2, 6 et 8 ans, sont tous deux supérieurs à 2, alors on choisit le plus grand des deux enfants à échanger :

image-20220906221504364

Enfin, le seul nœud non-feuille qui reste est le nœud racine. À l'heure actuelle, les enfants gauche et droit de notre 4 sont supérieurs à 4, donc des ajustements doivent encore être faits :

[Échec du transfert d'image par lien externe. Le site source peut avoir un mécanisme anti-sangsue. Il est recommandé de sauvegarder l'image et de la télécharger directement (img-87a7s1F4-1662545089947)(/Users/nagocoler/Library/Application Support/typora-user -images/image-20220906221657599 .png)]

Après ajustement, ce n'est pas encore fini, car après avoir remplacé 4 à ce moment, il ne répond toujours pas aux propriétés d'un grand tas supérieur. À ce moment, l'enfant gauche de 4 est supérieur à 4, et nous devons continuer Regarder vers le bas:

image-20220906221833012

Après l’échange, l’arbre binaire entier satisfait désormais aux propriétés d’un grand tas supérieur, et notre premier ajustement initial est terminé.

À ce stade, nous commençons la deuxième étape. Nous devons échanger les éléments supérieurs du tas un par un, ce qui équivaut à retirer le plus grand à chaque fois jusqu'à ce qu'il soit terminé. Tout d'abord, échanger l'élément supérieur du tas et le dernier élément :

image-20220906222327297

A ce moment, le plus grand élément de tout le tableau a été disposé à la position correspondante, puis nous ne considérons plus le dernier élément. À ce moment, les éléments restants devant continuent d'être considérés comme un arbre binaire complet, et le Le nœud racine est à nouveau regroupé (seul le nœud racine doit être ajusté, car les autres nœuds non feuilles n'ont pas changé), afin qu'il continue de répondre aux propriétés d'un grand tas supérieur :

image-20220906222819554

Ce n’est pas encore fini, continuez à vous ajuster :

image-20220906222858752

A ce moment, le premier tour est terminé, puis le deuxième tour est répété. L'opération ci-dessus est répétée. Tout d'abord, l'élément supérieur du tas est toujours jeté à l'avant-dernière position, ce qui équivaut à placer l'avant-dernière position. dernier plus grand élément à la position correspondante :

image-20220906222934602

À ce stade, deux éléments ont été triés. De même, nous continuons à considérer les éléments restants comme un arbre binaire complet et continuons à effectuer des opérations de tas sur le nœud racine afin qu'il continue de satisfaire la propriété du grand tas supérieur :

image-20220906223110734

Au troisième tour, la même idée est utilisée, et la plus grande est remplacée par l'arrière :

image-20220906223326135

Après N tours de tri, chaque élément peut enfin être disposé dans la position correspondante. Sur la base des idées ci-dessus, essayons d'écrire du code :

//这个函数就是对start顶点位置的子树进行堆化
void makeHeap(int* arr, int start, int end) {
    
    
    while (start * 2 + 1 <= end) {
    
        //如果有子树,就一直往下,因为调整之后有可能子树又不满足性质了
        int child = start * 2 + 1;    //因为下标是从0开始,所以左孩子下标就是i * 2 + 1,右孩子下标就是i * 2 + 2
        if(child + 1 <= end && arr[child] < arr[child + 1])   //如果存在右孩子且右孩子比左孩子大
            child++;    //那就直接看右孩子
        if(arr[child] > arr[start])   //如果上面选出来的孩子,比父结点大,那么就需要交换,大的换上去,小的换下来
            swap(&arr[child], &arr[start]);
        start = child;   //继续按照同样的方式前往孩子结点进行调整
    }
}

void heapSort(int arr[], int size) {
    
    
    for(int i= size/2 - 1; i >= 0; i--)   //我们首选需要对所有非叶子结点进行一次堆化操作,需要从最后一个到第一个,这里size/2计算的位置刚好是最后一个非叶子结点
        makeHeap(arr, i, size - 1);
    for (int i = size - 1; i > 0; i--) {
    
       //接着我们需要一个一个把堆顶元素搬到后面,有序排列
        swap(&arr[i], &arr[0]);    //搬运实际上就是直接跟倒数第i个元素交换,这样,每次都能从堆顶取一个最大的过来
        makeHeap(arr, 0, i - 1);   //每次搬运完成后,因为堆底元素被换到堆顶了,所以需要再次对根结点重新进行堆化
    }
}

Enfin, analysons la stabilité du tri du tas. En fait, le tri du tas lui-même effectue également des sélections. À chaque fois, l'élément supérieur du tas est sélectionné et placé à l'arrière, mais le tas est toujours maintenu dynamiquement. En effet, lorsque des éléments seront retirés du haut du tas, ils seront échangés avec les feuilles du dessous, ce qui peut se produire :

image-20220906223706019

Par conséquent, le tri par tas est un algorithme de tri instable .

Enfin, résumons les propriétés pertinentes des trois algorithmes de tri ci-dessus :

Algorithme de tri le meilleur cas de scenario pire scénario complexité de l'espace la stabilité
Tri rapide O ( nlogn ) O(nlogn)O ( n connexion ) _ _ _ O ( n 2 ) O (n ^ 2)O ( n2 ) O (logn) O(logn)O ( connexion ) _ _ _ instable
Tri des collines O ( n 1.3 ) O(n^{1.3})O ( n1.3 ) O ( n 2 ) O (n ^ 2)O ( n2 ) O (1) O(1)O ( 1 ) instable
Tri en tas O ( nlogn ) O(nlogn)O ( n connexion ) _ _ _ O ( nlogn ) O(nlogn)O ( n connexion ) _ _ _ O (1) O(1)O ( 1 ) instable

Autres options de tri

En plus des nombreux algorithmes de tri que nous avons présentés précédemment, il existe également d’autres types d’algorithmes de tri. Jetons-les tous un coup d’œil.

tri par fusion

Le tri par fusion utilise l'idée de diviser et conquérir récursive pour diviser le tableau d'origine, puis trier d'abord les petits tableaux divisés, puis enfin les fusionner en un grand tableau ordonné. C'est toujours facile à comprendre :

image-20220906232451040

Prenons comme exemple le tableau suivant :

image-20220905223505975

Ne nous précipitons pas pour trier au début, divisons-le moitié-moitié :

image-20220907135544173

Continuez à diviser :

image-20220907135744253

Au final, cela deviendra des éléments un à un comme ceci :

image-20220907135927289

À ce stade, nous pouvons commencer à fusionner et à trier. Notez que la fusion ici n'est pas une simple fusion. Nous devons fusionner chaque élément dans l'ordre du plus petit au plus grand. Le premier groupe d'arbres 4 et 2. À ce stade, nous devons sélectionnez le plus petit parmi ces deux tableaux et déplacez-le vers l'avant :

image-20220907140219455

Une fois le tri terminé, nous continuons à fusionner vers le haut :

image-20220907141217008

Enfin, nous fusionnons les deux tableaux à leur taille d'origine :

image-20220907141442229

Enfin, vous obtiendrez un tableau ordonné.

En fait, cet algorithme de tri est également très efficace, mais il nécessite de sacrifier un espace de la taille du tableau d'origine pour trier les données décomposées. Le code est le suivant :

void merge(int arr[], int tmp[], int left, int leftEnd, int right, int rightEnd){
    
    
    int i = left, size = rightEnd - left + 1;   //这里需要保存一下当前范围长度,后面使用
    while (left <= leftEnd && right <= rightEnd) {
    
       //如果两边都还有,那么就看哪边小,下一个就存哪一边的
        if(arr[left] <= arr[right])   //如果左边的小,那么就将左边的存到下一个位置(这里i是从left开始的)
            tmp[i++] = arr[left++];   //操作完后记得对i和left都进行自增
        else
            tmp[i++] = arr[right++];
    }
    while (left <= leftEnd)    //如果右边看完了,只剩左边,直接把左边的存进去
        tmp[i++] = arr[left++];
    while (right <= rightEnd)   //同上
        tmp[i++] = arr[right++];
    for (int j = 0; j < size; ++j, rightEnd--)   //全部存到暂存空间中之后,暂存空间中的内容都是有序的了,此时挨个搬回原数组中(注意只能搬运范围内的)
        arr[rightEnd] = tmp[rightEnd];
}

void mergeSort(int arr[], int tmp[], int start, int end){
    
       //要进行归并排序需要提供数组和原数组大小的辅助空间
    if(start >= end) return;   //依然是使用递归,所以说如果范围太小,就不用看了
    int mid = (start + end) / 2;   //先找到中心位置,一会分两半
    mergeSort(arr, tmp, start, mid);   //对左半和右半分别进行归并排序
    mergeSort(arr, tmp, mid + 1, end);
    merge(arr, tmp, start, mid, mid + 1, end);  
  	//上面完事之后,左边和右边都是有序状态了,此时再对整个范围进行一次归并排序即可
}

Parce que le tri par fusion est également fusionné en fonction de la petite priorité à la fin. S'il rencontre l'égalité, le premier sera d'abord renvoyé au tableau d'origine, donc le premier est toujours classé en premier, donc le tri par fusion est également un tri stable algorithme .

Tri par compartiment et tri par base

Avant de commencer à expliquer le tri par compartiment, examinons d'abord le tri par comptage. Il nécessite que la longueur du tableau soit N et que la plage de valeurs des éléments du tableau soit comprise entre 0 et M-1 (M est inférieur ou égal à N)

Site Web de démonstration d'algorithme : https://visualgo.net/zh/sorting?slide=1

Par exemple, dans le tableau suivant, tous les éléments vont de 1 à 6 :

image-20220907142933725

Nous le parcourons d'abord et comptons le nombre d'occurrences de chaque élément. Une fois les statistiques terminées, nous pouvons savoir où stocker les éléments avec quelles valeurs​​après le tri :

image-20220907145336855

Analysons cela. Tout d'abord, il n'y a qu'un seul 1, donc il n'occupera qu'une seule position. Il n'y a qu'un seul 2, donc il n'occupera qu'une seule position, et ainsi de suite :

image-20220907145437992

Par conséquent, nous pouvons directement remplir ces valeurs une par une en fonction des résultats statistiques, et elles sont toujours stables. Il suffit de remplir quelques-unes dans l'ordre :

image-20220907145649061

Cela ne semble-t-il pas très simple, et il suffit de le parcourir une seule fois pour obtenir des statistiques ?

Bien sûr, il y a certainement des inconvénients :

  1. Lorsque la différence entre les valeurs maximales et minimales du tableau est trop grande, nous devons demander plus d'espace pour le comptage, cela ne convient donc pas au tri par comptage.
  2. Lorsque les valeurs des éléments du tableau ne sont pas discrètes (c'est-à-dire qu'elles ne sont pas des nombres entiers), il n'y a aucun moyen de les compter.

Examinons ensuite le tri par compartiment, qui est une extension du tri par comptage et l'idée est relativement simple. Cela nécessite également que la longueur du tableau soit N et que la plage de valeurs des éléments du tableau soit comprise entre 0 et M-1. (M est inférieur ou égal à N), par exemple, il y a maintenant 1 000 étudiants et nous devons maintenant trier ces étudiants en fonction de leurs scores. Étant donné que la plage de scores est comprise entre 0 et 100, nous pouvons créer 101 compartiments pour le stockage classifié. .

Par exemple, le tableau suivant :

image-20220907142933725

Ce tableau contient les éléments 1 à 6, nous pouvons donc créer 6 buckets pour les statistiques :

image-20220907143715938

De cette façon, nous n'avons besoin de parcourir qu'une seule fois pour classer tous les éléments et les jeter dans ces compartiments. Enfin, il nous suffit de parcourir ces compartiments dans l'ordre, puis de retirer les éléments et de les stocker afin d'obtenir un ordre ordonné. tableau:

image-20220907144255326

Cependant, bien que le tri par buckets soit également très rapide, il présente également les mêmes limites que le tri par comptage ci-dessus. Nous pouvons réduire le nombre de buckets en acceptant des éléments dans une certaine plage dans chaque bucket, mais cela entraînera une surcharge de temps supplémentaire.

Examinons enfin le tri par base. Le tri par base est toujours un algorithme de tri qui s'appuie sur des statistiques, mais il ne provoquera pas une application illimitée de l'espace auxiliaire car la plage est trop grande. L'idée est de séparer 10 nombres de base (de 0 à 9). Nous ne devons parcourir qu'une seule fois. Nous classons selon le nombre dans le chiffre des unités de chaque élément, car il y a maintenant 10 nombres de base, soit 10 A. seau. Une fois les unités terminées, regardez les dizaines et les centaines...

Site de démonstration d'algorithme : https://visualgo.net/zh/sorting

image-20220907152403435

Comptez d'abord par chiffres, puis triez, puis comptez par dizaines, puis triez. Le résultat final est le résultat final :

image-20220907152903020

Vient ensuite le chiffre des dizaines :

image-20220907153005797

Enfin, sortez-les à nouveau dans l'ordre :
image-20220907153139536

Tableau ordonné obtenu avec succès.

Enfin, résumons les propriétés pertinentes de tous les algorithmes de tri :

Algorithme de tri le meilleur cas de scenario pire scénario complexité de l'espace la stabilité
Tri à bulles O (n) O(n)O ( n ) O ( n 2 ) O (n ^ 2)O ( n2 ) O (1) O(1)O ( 1 ) Stabiliser
tri par insertion O (n) O(n)O ( n ) O ( n 2 ) O (n ^ 2)O ( n2 ) O (1) O(1)O ( 1 ) Stabiliser
tri par sélection O ( n 2 ) O (n ^ 2)O ( n2 ) O ( n 2 ) O (n ^ 2)O ( n2 ) O (1) O(1)O ( 1 ) instable
Tri rapide O ( nlogn ) O(nlogn)O ( n connexion ) _ _ _ O ( n 2 ) O (n ^ 2)O ( n2 ) O (logn) O(logn)O ( connexion ) _ _ _ instable
Tri des collines O ( n 1.3 ) O(n^{1.3})O ( n1.3 ) O ( n 2 ) O (n ^ 2)O ( n2 ) O (1) O(1)O ( 1 ) instable
Tri en tas O ( nlogn ) O(nlogn)O ( n connexion ) _ _ _ O ( nlogn ) O(nlogn)O ( n connexion ) _ _ _ O (1) O(1)O ( 1 ) instable
tri par fusion O ( nlogn ) O(nlogn)O ( n connexion ) _ _ _ O ( nlogn ) O(nlogn)O ( n connexion ) _ _ _ O (n) O(n)O ( n ) Stabiliser
tri par comptage O ( n + k ) O(n + k)O ( n+k ) O ( n + k ) O(n + k)O ( n+k ) OK OK)O ( k ) Stabiliser
tri au seau O ( n + k ) O(n + k)O ( n+k ) O ( n 2 ) O (n ^ 2)O ( n2 ) O (k + n) O(k + n)O ( k+n ) Stabiliser
Tri par base O ( n × k ) O(n \times k)O ( n×k ) O ( n × k ) O(n \times k)O ( n×k ) O (k + n) O(k+n)O ( k+n ) Stabiliser

tri des singes

Le tri des singes est plus bouddhiste, car tout dépend de la chance quant au moment où vous pourrez terminer le tri !

Le théorème du singe infini a été mentionné pour la première fois par Emile Borrell dans un livre sur les probabilités publié en 1909, qui introduisait le concept de « singes qui tapent ». Le théorème du singe infini est un exemple de la proposition d'uniformité nulle de Kolmogorov en théorie des probabilités. La signification générale est que si vous laissez un singe appuyer sur les touches d'une machine à écrire au hasard, et si vous continuez à les appuyer ainsi, tant que le temps atteint l'infini, le singe sera presque certainement capable de taper n'importe quel texte donné, même The Shakespeare's. un ensemble complet d’œuvres peut également être dactylographié.

Supposons qu'il existe un tableau de longueur N :

image-20220907154254943

Chaque fois que nous sélectionnons au hasard un élément du tableau et l'échangeons avec un élément aléatoire :

image-20220907154428792

Tant que vous avez de la chance, vous pourrez peut-être le faire en quelques fois seulement. Si vous n'avez pas de chance, vous ne pourrez peut-être pas organiser votre mariage tant que votre petit-fils ne sera pas marié.

le code s'affiche comme ci-dessous :

_Bool checkOrder(int arr[], int size){
    
    
    for (int i = 0; i < size - 1; ++i)
        if(arr[i] > arr[i + 1]) return 0;
    return 1;
}

int main(){
    
    
    int arr[] = {
    
    3,5, 7,2, 9, 0, 6,1, 8, 4}, size = 10;

    int counter = 0;
    while (1) {
    
    
        int a = rand() % size, b = rand() % size;
        swap(&arr[a], &arr[b]);
        if(checkOrder(arr, size)) break;
        counter++;
    }
    printf("在第 %d 次排序完成!", counter);
}

On constate qu'avec 10 éléments, le 7485618ème tri a été réussi :

image-20220907160219493

Mais je ne sais pas pourquoi les résultats du tri sont les mêmes à chaque fois. Peut-être que les nombres aléatoires ne sont pas assez aléatoires.

Algorithme de tri le meilleur cas de scenario pire scénario complexité de l'espace la stabilité
tri des singes O (1) O(1)O ( 1 ) O (1) O(1)O ( 1 ) instable

Je suppose que tu aimes

Origine blog.csdn.net/qq_25928447/article/details/126751213
conseillé
Classement