你想要的排序算法,都在这!

308 阅读8分钟

写在前面

最近写代码的时候用到堆排序解决一些问题,然后写的过程中发现有点手生,就想着记录一些,然后又琢磨着干脆直接整理一份排序的文章,附带对排序的分析以及实现代码,给自己练练手。
所有代码都搞懂熟练,你还会怕排序么?如果觉得有所帮助,记得点个关注和点个赞哦

冒泡排序

冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果他们的顺序(如从大到小、首字母从A到Z)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素已经排序完成。

实现原理

  • 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  • 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
  • 针对所有的元素重复以上的步骤,除了最后一个。
  • 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

在这里插入图片描述

算法稳定性

冒泡排序总的平均时间复杂度为 O ( N 2 ) O(N^2) O(N2) 。冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,是不会再交换的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法

代码实现

public void bubbleSort(int[] sourceArray){
    for (int i = 1; i < sourceArray.length; i++){
        // 设定一个标记,若为true,则表示此次循环没有进行交换,也就是待排序列已经有序,排序已经完成。
        boolean flag = true;
        for (int j = 0; j < sourceArray.length - i; j++){
            if (sourceArray[j] > sourceArray[j + 1]){
                sourceArray[j] = sourceArray[j] ^ sourceArray[j + 1];
                sourceArray[j + 1] = sourceArray[j] ^ sourceArray[j + 1];
                sourceArray[j] = sourceArray[j] ^ sourceArray[j + 1];
                flag = false;
            }
        }

        if (flag){
            break;
        }
    }
}

快速排序

快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),简称快排,一种排序算法,最早由东尼·霍尔提出。快速排序通常明显比其他算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地达成。

实现原理

  • 从数列中挑出一个元素,称为 “基准”(pivot),
  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

在这里插入图片描述

算法稳定性

在平均状况下,排序个项目要 O ( n l o g n ) O(nlogn) O(nlogn) ,事实上,快速排序 O ( n l o g n ) O(nlogn) O(nlogn) 通常明显比其他算法更快。交换 a[r]a[mid],完成一趟快速排序。在中枢元素和 a[r] 交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为5 3 3 4 3 8 9 10 11,现在中枢元素 53(第5个元素,下标从1开始计)交换就会把序列中,三个元素 3 的稳定性打乱(就是最后一个三到了最前面),所以快速排序是一个不稳定的排序算法

代码实现

public void quickSort(int[] sourceArray, int left, int right){
    if (left >= right) return;
    int l = left;
    int r = right;
    int mid = sourceArray[left];
    while (l < r) {
        while (l < r && sourceArray[r] >= mid)
            r--;
        if (l < r)
            sourceArray[l++] = sourceArray[r];
        while (l < r && sourceArray[l] < mid)
            l++;
        if (l < r)
            sourceArray[r--] = sourceArray[l];
    }
    sourceArray[l] = mid;
    quickSort(sourceArray, left, l - 1);
    quickSort(sourceArray, l + 1, right);
}

快速选择排序

这里单独提一下一个混合的算法,是基于快速排序而来的,类似对快速排序进行剪枝,用于计算第 K K K大(小)的数。快速选择算法的平均时间复杂度为 O ( N ) O(N) O(N)。就像快速排序那样,本算法也是 Tony Hoare 发明的,因此也被称为 Hoare选择算法。本方法大致上与快速排序相同。简便起见,注意到第 k 个最大元素也就是第 N - k 个最小元素,因此可以用第 k 小算法来解决本问题。

首先,我们选择一个枢轴,并在线性时间内定义其在排序数组中的位置。这可以通过 划分算法 的帮助来完成。

为了实现划分,沿着数组移动,将每个元素与枢轴进行比较,并将小于枢轴的所有元素移动到枢轴的左侧。

这样,在输出的数组中,枢轴达到其合适位置。所有小于枢轴的元素都在其左侧,所有大于或等于的元素都在其右侧。这样,数组就被分成了两部分。如果是快速排序算法,会在这里递归地对两部分进行快速排序,时间复杂度为 O ( N l o g N ) O(Nlog N) O(NlogN)。而在这里,由于知道要找的第 N - k 小的元素在哪部分中,我们不需要对两部分都做处理,这样就将平均时间复杂度下降到 O ( N ) O(N) O(N),最终的算法十分直接了当 :

  • 随机选择一个枢轴。
  • 使用划分算法将枢轴放在数组中的合适位置 pos。将小于枢轴的元素移到左边,大于等于枢轴的元素移到右边。
  • 比较 pos 和 N - k 以决定在哪边继续递归处理。

在这里插入图片描述

选择排序

选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。 选择排序是不稳定的排序方法。

实现原理

  • 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
  • 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
  • 重复第二步,直到所有元素均排序完毕。

在这里插入图片描述

算法稳定性

选择排序是 O ( n 2 ) O(n^2) O(n2) 的时间复杂度,在一趟选择,如果一个元素比当前元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列 5 8 5 2 9,我们知道第一遍选择第 1 个元素 5 会和 2 交换,那么原序列中两个 5 的相对前后顺序就被破坏了,所以选择排序是一个不稳定的排序算法

代码实现

public void selectSort(int[] sourceArray){
    // 总共要经过 N-1 轮比较
    for (int i = 0; i < sourceArray.length - 1; i++){
        int cur = i;
        // 每轮需要比较的次数 N-i
        for (int j = i + 1; j < sourceArray.length; j++){
            if (sourceArray[cur] > sourceArray[j]) {
                // 记录目前能找到的最小值元素的下标
                cur = j;
            }
        }

        // 将找到的最小值和i位置所在的值进行交换
        if (i != cur){
            sourceArray[i] = sourceArray[i] ^ sourceArray[cur];
            sourceArray[cur] = sourceArray[i] ^ sourceArray[cur];
            sourceArray[i] = sourceArray[i] ^ sourceArray[cur];
        }
    }
}

插入排序

插入排序(英语:Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到 O ( 1 ) O(1) O(1) 的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

实现原理

  • 从第一个元素开始,该元素可以认为已经被排序
  • 取出下一个元素,在已经排序的元素序列中从后向前扫描
  • 如果该元素(已排序)大于新元素,将该元素移到下一位置
  • 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
  • 将新元素插入到该位置后
  • 重复步骤2~5

在这里插入图片描述

算法稳定性

平均来说插入排序算法复杂度为 O ( n 2 ) O(n^2) O(n2) ,如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的

代码实现

public void insertSort(int[] sourceArray){
    // 从下标为1的元素开始选择合适的位置插入,因为下标为0的只有一个元素,默认是有序的
    for (int i = 1; i < sourceArray.length; i++){
        // 记录要插入的数据
        int temp = sourceArray[i];
        // 从已经排序的序列最右边的开始比较,找到比其小的数
        int j = i;
        while (j > 0 && temp < sourceArray[j - 1]){
            sourceArray[j] = sourceArray[j - 1];
            j--;
        }

        // 存在比其小的数,插入
        if (j != i) sourceArray[j] = temp;
    }
}

归并排序

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

实现原理

  • 第一步:申请空间,使其大小为两个已经 排序序列之和,该空间用来存放合并后的序列
  • 第二步:设定两个 指针,最初位置分别为两个已经排序序列的起始位置
  • 第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
  • 重复步骤3直到某一指针超出序列尾
  • 将另一序列剩下的所有元素直接复制到合并序列尾

在这里插入图片描述

算法稳定性

始终都是 O ( n l o g n ) O(nlogn) O(nlogn) 的时间复杂度,代价是需要额外的内存空间。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定性。那么,在短的有序序列合并的过程中,稳定性是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法

代码实现

public int[] mergeSort(int[] sourceArray){
    if (sourceArray.length < 2) return sourceArray;
    int mid = (int) Math.floor(sourceArray.length / 2);
    int[] left = Arrays.copyOfRange(sourceArray, 0, mid);
    int[] right = Arrays.copyOfRange(sourceArray, mid, sourceArray.length);

    return merge(mergeSort(left), mergeSort(right));
}

private int[] merge(int[] left, int[] right){
    int[] nums = new int[left.length + right.length];
    int l = 0;
    int r = 0;
    int i = 0;
    while (l < left.length && r < right.length){
        if (left[l] < right[r]) {
            nums[i] = left[l];
            l++;
        }else {
            nums[i] = right[r];
            r++;
        }
        i++;
    }

    while (l < left.length){
        nums[i] = left[l];
        l++;i++;
    }

    while (r < right.length){
        nums[i] = right[r];
        r++;i++;
    }

    return nums;
}

基数排序

基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog®m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。

实现原理

假设原来有一串数值如下所示:73, 22, 93, 43, 55, 14, 28, 65, 39, 81,首先根据个位数的数值,在走访数值时将它们分配至编号 09 的桶子中:

0
1 81
2 22
3 73 93 43
4 14
5 55 65
6
7
8 28
9 39

接下来将这些桶子中的数值重新串接起来,成为以下的数列:81, 22, 73, 93, 43, 14, 55, 65, 28, 39,接着再进行一次分配,这次是根据十位数来分配:

0
1 14
2 22 28
3 39
4 43
5 55
6 65
7 73
8 81
9 93

接下来将这些桶子中的数值重新串接起来,成为以下的数列:14, 22, 28, 39, 43, 55, 65, 73, 81, 93,这时候整个数列已经排序完毕;如果排序的对象有三位数以上,则持续进行以上的动作直至最高位数为止。LSD的基数排序适用于位数小的数列,如果位数多的话,使用MSD的效率会比较好。MSD的方式与LSD相反,是由高位数为基底开始进行分配,但在分配之后并不马上合并回一个数组中,而是在每个“桶子”中建立“子桶”,将每个桶子中的数值按照下一数位的值分配到“子桶”中。在进行完最低位数的分配后再合并回单一的数组中。
在这里插入图片描述

算法稳定性

基数排序的时间复杂度为 O ( n l o g ( r ) m ) O (nlog(r)m) O(nlog(r)m) ,有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法

代码实现

public void radixSort(int[] sourceArray){
    int maxDigit = getMaxDigit(sourceArray);
    int mod = 10;
    int dev = 1;

    for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10){
        int[][] counter = new int[mod * 2][0];
        for (int j = 0; j < sourceArray.length; j++){
            int bucket = ((sourceArray[j] % mod) / dev) + mod;
            counter[bucket] = arrayAppend(counter[bucket], sourceArray[j]);
        }

        int pos = 0;
        for (int[] bucket : counter){
            for (int value : bucket){
                sourceArray[pos++] = value;
            }
        }
    }
}

private int getMaxDigit(int[] arr){
    int maxValue = getMaxValue(arr);
    return getNumLength(maxValue);
}

private int getMaxValue(int[] arr){
    int maxValue = arr[0];
    for (int value : arr){
        if (maxValue < value){
            maxValue = value;
        }
    }
    return maxValue;
}

private int getNumLength(long num){
    if (num == 0){
        return 1;
    }
    int length = 0;
    for (long temp = num; temp != 0; temp /= 10){
        length++;
    }
    return length;
}

private int[] arrayAppend(int[] arr, int value){
    arr = Arrays.copyOf(arr, arr.length + 1);
    arr[arr.length - 1] = value;
    return arr;
}

希尔排序(shell)

希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因D.L.Shell于1959年提出而得名。

实现原理

先取一个小于 n 的整数 d1 作为第一个增量,把文件的全部记录分组。所有距离为 d1 的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量 d2=1(<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。
在这里插入图片描述

算法稳定性

希尔排序的时间复杂度会比 O ( n 2 ) O(n^2) O(n2) 好一些,由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的

代码实现

public void shellSort(int[] sourceArray){
    int gap = sourceArray.length;

    while (true){
        gap /= 2;
        for (int i = 0; i < gap; i++){
            for (int j = i + gap; j < sourceArray.length; j += gap){
                int temp = sourceArray[j];
                int k = j - gap;
                while (k >= 0 && sourceArray[k] > temp){
                    sourceArray[k + gap] = sourceArray[k];
                    k -= gap;
                }
                sourceArray[k + gap] = temp;
            }
        }
        if (gap == 1) break;
    }
}

堆排序

堆排序(英语:Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

实现原理

  • 创建一个堆 H[0……n-1];
  • 把堆首(最大值)和堆尾互换;
  • 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
  • 重复步骤 2,直到堆的尺寸为 1。

在这里插入图片描述

算法稳定性

堆排序的平均时间复杂度为 O ( n l o g n ) Ο(nlogn) O(nlogn) 。我们知道堆的结构是节点 i 的孩子为 2 * i2 * i + 1 节点,大顶堆要求父节点大于等于其 2 个子节点,小顶堆要求父节点小于等于其 2 个子节点。在一个长为 n 的序列,堆排序的过程是从第 n / 2 开始和其子节点共 3 个值选择最大(大顶堆)或者最小(小顶堆),这 3 个元素之间的选择当然不会破坏稳定性。但当为 n / 2 - 1, n / 2 - 2, ... 1这些个父节点选择元素时,就会破坏稳定性。有可能第 n / 2 个父节点交换把后面一个元素交换过去了,而第 n / 2 - 1 个父节点把后面一个相同的元素没 有交换,那么这 2 个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法

代码实现

public void heapSort(int[] sourceArray){
    int len = sourceArray.length;
    buildMaxHeap(sourceArray, len);
    for (int i = len - 1; i > 0; i--){
        swap(sourceArray, 0, i);
        len--;
        heapify(sourceArray, 0, len);
    }
}

private void buildMaxHeap(int[] arr, int len){
    for (int i = (int) Math.floor(len / 2); i >= 0; i--){
        heapify(arr, i, len);
    }
}

private void heapify(int[] arr, int i, int len){
    int left = 2 * i + 1;
    int right = 2 * i + 2;
    int largest = i;

    if (left < len && arr[left] > arr[largest]){
        largest = left;
    }

    if (right < len && arr[right] > arr[largest]){
        largest = right;
    }

    if (largest != i){
        swap(arr, i, largest);
        heapify(arr, largest, len);
    }
}

private void swap(int[] arr, int i, int j){
    arr[i] = arr[i] ^ arr[j];
    arr[j] = arr[i] ^ arr[j];
    arr[i] = arr[i] ^ arr[j];
}

计数排序

计数排序(Counting sort)是一种稳定的线性时间排序算法。计数排序使用一个额外的数组 C C C,其中第 i i i 个元素是待排序数组 A A A中值等于 i i i的元素的个数。然后根据数组 C C C来将 A A A中的元素排到正确的位置。

实现原理

  • 找出待排序的数组中最大和最小的元素
  • 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
  • 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
  • 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1

在这里插入图片描述

算法稳定性

计数排序的时间复杂度是 O ( n + k ) O(n + k) O(n+k) ,由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上 1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序 0100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要反向填充目标数组,以及将每个数字的统计减去 1 的原因。所以计数排序是稳定的

代码实现

private static void countingSort(int[] arr, int maxValue) {
    int bucketLen = maxValue + 1;
    int[] bucket = new int[bucketLen];

    for (int value : arr) {
        bucket[value]++;
    }

    int sortedIndex = 0;
    for (int j = 0; j < bucketLen; j++) {
        while (bucket[j] > 0) {
            arr[sortedIndex++] = j;
            bucket[j]--;
        }
    }
}

private static int getMaxValue(int[] arr) {
    int maxValue = arr[0];
    for (int value : arr) {
        if (maxValue < value) {
            maxValue = value;
        }
    }
    return maxValue;
}