8大排序算法介绍-java实现

排序算法

为什么要从低效率 O ( n 2 ) 的复杂度高的算法学起?
1. 低效率的算法是高效算法的基础,求解思路简单明了,易于接受,通过从基础开始能够加深对问题的理解;
2. 只有学习了基础的算法,在遇到问题时,才能有最基本的解决问题的思路,再慢慢改进;
3. 高效率的算法也有着自身的不足,并非所有场合都是需要高效率的算法,而基础的算法,编码简单,易于实现,在一些特殊情况下,简单算法更有效;
4. 简单的排序算法可以作为一些复杂排序算法的子过程,用来改进复杂的算法;
5. 算法就是解决问题的方法,算法思想就是解决问题的思路,简单的算法思路直接来源于生活中解决问题的思路,具有直观、容易理解的特点。
* 注意:假定:
* ①要排序的数据都是整数;
* ②数据存储在数组中;
* ③数据以升序排列。

1.冒泡排序

冒泡排序

  • 最基本的排序,思想最直白简单:对一组数据,多次遍历,每次遍历过程中比较相邻元素,把较大的值交换到后面;
  • 每轮都是把最大值向上,就像大的气泡上升一样,所以叫冒泡排序。
//当开始第k次遍历时,不需要考虑后面的k-1个元素,已经排好序了
public static void BubbleSort(int[] nums) {
    int tmp=0;
    // 第k轮遍历,把倒数第k个位置的数固定
    for(int k = 1; k < nums.length; k++){
        //只对前n-k个数进行排序
        for(int i= 0; i < nums.length - k; i++){
            if(nums[i] > nums[i+1]){
                tmp = nums[i+1];
                nums[i+1] = nums[i];
                nums[i] = tmp;
            }
        }
    }
}
// 改进:如果在第k次遍历没有发生交换,说明已经是有序的,就不需要再进行k+1次以及后续的遍历
public static void BubbleSort(int[] nums) {
    int tmp=0;
    //创建一个标记,表明是否需要继续遍历
    boolean needNextPass = true;
    for(int k=1; k < nums.length && needNextPass; k++){
        //在第k轮开始时,把标记设为false
        needNextPass = false;
        for(int i = 0; i < nums.length - k; i++){
            if(nums[i] > nums[i+1]){
                tmp = nums[i+1];
                nums[i+1] = nums[i];
                nums[i] = tmp;
                // 如果进行了交换,说明还需要继续遍历和排序
                needNextPass = true;
            }
        }
    }
}
  • 在最佳情况下,对于有序数组,冒泡排序只需要遍历一次就可以确定数组已经排好序,不需要进行下一次遍历,比较了n-1次,时间复杂度为: O ( n )
  • 在最差情况下,冒泡排序需要遍历 n 1 轮,第k轮遍历需要比较 n k 次,最后一次比较1次,所以平均和最差情况下的时间复杂度为:
    ( n 1 ) + ( n 2 ) + . . . + 1 = n ( n 1 ) 2 = O ( n 2 )
  • 额外需要的辅助空间为: O ( 1 )

2.选择排序

选择排序

  • 回顾“冒泡排序”:每一轮遍历中,只确定了一个“最大值”,但是每次交换要进行3次赋值操作,能否减少赋值的操作次数?
  • 选择排序:每一轮遍历开始时,“选择”一个“最值下标”,然后在遍历过程中,改变最值下标,遍历结束了再把最值和首位位置的数据交换,每一次改变的只是选择的最值下标,所以叫选择排序。
public static void SelectionSort(int[] list) {
    for(int k = 0; k < nums.length - 1; k++){
        // 因为是把最值向后移动,所以每次假设最值下标是0
        int minIndex = k;
        for(int i = k + 1; i < nums.length; i++){
            if(nums[i] < nums[minIndex]){
                minIndex = i;
            }
        }
        //如果最小值的下标不等于刚开始的初始值,就交换这两个位置的值
        int minTemp = nums[minIndex];
        if(minIndex != k){
            nums[minIndex] = nums[k];
            nums[k] = minTemp;
        }
    }
}
  • 最佳情况下,即使数组原本是有序的,每次遍历只是选择出一个最值的下标,而不是对相邻的数据进行交换,不能确认剩下的部分已经有序,依然需要进行比较;
  • k , ( k = 0... n 2 ) 轮遍历中,需要比较和赋值 n k 1 次,第 k , ( k = 0... n 1 ) 轮遍历结束后,前 k + 1 个数都是有序的,最后一次遍历比较和交换1次,时间复杂度为:
    ( n 1 ) + ( n 2 ) + . . . + 1 = n ( n 1 ) 2 = O ( n 2 )

    最佳、平均和最差时间复杂度都是: O ( n 2 )
  • 需要额外辅助空间: O ( 1 )

3.插入排序

插入排序

  • 插入排序:假设前面的子线性表是有序的,重复地把后面的元素插入到前面这个有序的子表中,并保持子表的有序,直到所有数据都插入完成。
  • 算法流程:
    1. 从下标为i(i=1,2,…,n-1)的数据开始,假设前i个数组成一个有序子表;
    2. 把数据nums[i]保存在一个临时变量currElem中,并把currElem插入已经排好序的线性子表nums[0,…,i-1]中;
    3. nums[i]插入时保证子表的有序性:
      • 从下标 k = i 1 开始,向前依次比较nums[k]与currElem的大小:
      • 如果nums[i-1] > currElem,就把nums[i-1]赋值给list[i];
      • 接着比较list[i-2]和currElem,如果list[i-2]>currElem,就把list[i-2]赋值给list[i-1],依次类推;
      • ……
      • 直到list[k] <= currElem 或者 k<0,那么把currElem赋值给list[k+1];
    4. 直到所有元素,都添加到子表中,且整个子表都是有序的。
public static void InsertionSort(int[] ]nums){
    for(int i = 1; i < nums.length; i++){
        int currElem = nums[i];
        //把大于当前值的数据后移
        int k;
        for(k = i - 1; k >= 0 && nums[k] > currElem; k-- ){
            nums[k+1] = nums[k];
        }
        nums[k+1] = currElem;
    }
}
  • 最佳情况下,数据是有序的,不满足内层循环条件 n u m s [ k ] > c u r r E l e m ,使得内层循环不会执行,只执行外层循环,遍历了一遍数组。这是插入排序的重要特性:输入的数据重复率越高、有序性越高,插入排序的性能越好,当数据有序性很强的时候,插入排序的效果比 O ( n l o g n ) 的算法更好,对完全有序的数据,最优可以达到 O ( n )
  • 最差情况下,在第i次遍历中,为了把num[i]插入到子表中,需要进行i次比较和i次移动,平均和最差时间复杂度为:
    2 [ ( n 1 ) + ( n 2 ) + . . . + 1 ] = n ( n 1 ) = O ( n 2 )
  • 需要额外辅助空间: O ( 1 )

4.希尔排序

希尔排序

  • 定理1:N个互异数的数组的平均逆序数是 N ( N 1 ) 4
  • 定理2:通过交换相邻元素进行排序的任何算法平均都需要 Ω ( N 2 ) 的时间。
  • 一个排序算法通过删除逆序得以向前进行,为了提高效率,它每次交换应该删除不止一个逆序。在插入排序算法中,每次有序子表只增加1个节点,无序部分的逆序数只减小1。如果比较相隔较远的数,使得数移动时能跨过多个元素,则进行一次比较就可能消除多个逆序数。
  • Shell排序:对相隔一定增量距离 [ h k , h k 1 , . . . , h 1 ] 的元素进行插入排序,所用的增量随着算法的进行而减少 ( h k > h k 1 > , . . . , > h 1 ) ,只要 h 1 == 1 任何增量序列都是可行的。
  • 希尔排序的重要性质:在使用 h k 增量排序后,所有相隔 h k 的元素都是有序的,对于每一个 i 有: n u m [ i ] n u m [ i + h k ] ,此时称文件是 h k 排序的,而且在后续的排序过程中,将保持它的 h k 排序性。
  • 一个流行的(不是最好)的增量序列是: h k = N 2 , h k 1 = h k 2
public static void ShellSort(int[] nums){
    for( int h = nums.length / 2; h >= 1; h >> 1 ){
        for(int i = h; i < nums.length; i++){
            int tmp = nums[i];
            int j;
            for( j = i - h; j >= 0 && nums[j] > tmp; j -= h){
                nums[j+h] = nums[j];
            }
            nums[j+h] = tmp;
        }
    }
}
  • 希尔排序的核心部分是插入排序,不同的是,插入排序每次的增量是1,希尔排序的增量是h,对 h k , h k + 1 , . . . , N 1 中的每一个位置 i 处的元素,放置到 i , i h k , i 2 h k , . . . 中的正确位置上去,如下图案例所示;
    希尔排序案例
  • 增量序列对希尔排序的性能影响很大,最佳情况下,有序数组的时间复杂度是 O ( n ) ,平均情况是 O ( n 1.3 ) ,最差情况下是 O ( n 2 )
  • 需要额外辅助空间: O ( 1 )
  • 希尔排序的复杂度和增量序列是相关的
    • n 2 i 序列的时间复杂度(最坏情形)为 O ( n 2 )
    • 2 i 1 序列的时间复杂度(最坏情形)为 O ( n 1.5 )
    • 最好的一个序列是{1,5,19,41,109,…},其最坏情形运行时间为 O ( n 1.3 )

5.归并排序

归并排序

  • 归并排序:使用“分治思想”对数组进行排序,把数组分成两部分,对每部分递归地应用归并排序,在两部分排好序后,把它们归并成一个较大的数组。
  • 归并排序的思想是先分组,再合并:持续把数组划分为子数组,直到每个子数组只包含一个元素;然后把这些小的子数组合并,在合并的过程中排序,组成较大的有序子数组,最后形成一个有序的完成数组。
public static void MergeSort(int[] nums){
    int[] tmpArr = new int[nums.length];
    mergeSort(nums, tmpArr, 0, nums.length-1);
}
//归并排序函数
public static void mergeSort(int[] nums, int[] tmpArr, int left, int right){
    if(left < right){
        int mid = (right - left) / 2 + left;
        //递归划分子数组
        mergeSort(nums, tmpArr, left, mid);
        mergeSort(nums, tmpArr, mid+1, right);
        //合并
        merge(nums, tmpArr, left, mid, right);
    }
}
//合并函数,在合并的过程中把子数组排序
public static void merge(int[] nums, int[] tmpArr, int left, int mid, int right){
    int cur1 = left, cur2 = mid +1, cur3 = left;
    //把两个子数组中的数依次比较,较小的数在合并数组中放在前面
    while(cur1 <= mid && cur2 <= right){
        if(nums[cur1] < nums[cur2]){
            tmpArr[cur3++] = nums[cur1++];
        }else{
            tmpArr[cur3++] = nums[cur2++];
        }
    }
    //如果左子数组还有剩余,全部添加进临时数组
    while(cur1 <= mid){
        tmpArr[cur3++] = nums[cur1++];
    }
    //如果右子数组还有剩余,全部添加进临时数组
    while(cur2 <= right){
        tmpArr[cur3++] = nums[cur2++];
    }
    //把临时数组复制进原数组
    for(int i = left; i <= right; i++){
        nums[i] = tmpArr[i];
    }
}
  • 假设N是2的幂,总是可以分成相等的两个部分,N=1时,归并所需的时间是常数1;对N个数归并排序的用时等于完成两个N/2数组的归并排序所用的时间和加上合并的时间:
    T ( 1 ) = 1

    T ( N ) = 2 T ( N / 2 ) + N

    其中, T ( N / 2 ) = 2 T ( N / 4 ) + N / 2 , T ( N / 4 ) = 2 T ( N / 8 ) + N / 4 , . . . ,最终可得:
    T ( N ) = 2 m T ( 1 ) + m N , m = l o g 2 N

    T ( N ) = N + N l o g 2 N

    可以这里理解,对N个数进行2分,一共可以分 l o g 2 N 层,每层的归并时间为N,一共 N l o g 2 N
  • 最佳、平均和最差的时间复杂度为 O ( N l o g 2 N )
  • 空间复杂度为 O ( N )

6.快速排序

快速排序

  • 快速排序:在数组中先选择一个称为“主元(pivot)”的元素,把数组分成两部分,使得第一部分中的所有元素都小于等于主元,第二部分中的所有元素都大于主元;对第一部分递归地使用快速排序,然后再对第二部分递归地使用快速排序。
  • 主元的选择会影响算法的性能,最好的主元能够把数组均分成两个部分,可以选择第一个元素作为主元。
public static void QuickSort(int[] nums){
    quickSort(nums, 0, nums.length - 1);
}
// 快速排序函数
private static void quickSort(int[] nums, int first, int last){
    if(last > first){
        int pivotIndex = partition(nums, first, last);
        quickSort(nums, first, pivotIndex - 1);
        quickSort(nums, pivotIndex + 1, last);
    }
}
// 分组函数以及排序
private static void partition(int[] nums, int first, int last){
    int pivot = nums[first];
    int low = first + 1, high = last;
    // 第一步,遍历数组,把所有小于等于pivot的数移动到它左边,把所有大于pivot的数移动到它右边
    while(low < high){
        //这里的两个内层循环条件都是low<=high,因为两个下标时分开进行更新的:
        //如果把内层循环条件改为:low < high,会出现以下情况:
        //对于一个子数组0478,pivot=5,先更新low到nums[low] = 7,再更新high到nums[high]=7,两个下标都无法再继续
        //nums[high]的值不小于等于pivot,在后续的交换中会出错,不满足快速排序的要求
        while(low <= high && nums[low] <= pivot){ low++; }
        while(low <= high &&  nums[high] > pivot){ high--; }
        //如果low<high并进行到这个语句块,表示此时:nums[low]>pivot且nums[high]<=pivot,把两者交换
        if(low < high){
            int tmp = nums[low];
            nums[low] = nums[high];
            nums[high] = tmp;
        }
    }
    //当遍历完所有数据后,如果nums[high]>=pivot,就减小high,直到nums[high]<pivot
    while(high > first && nums[high] >= pivot){
        high --;
    }
    //交换pivot和nums[high],返回pivot的下标
    if(pivot > nums[high]){
        nums[first] = nums[high];
        nums[high] = pivot;
        return high;
    }else{
        return first;
    }
}

快速排序的另一个版本:三路快速排序

pass
  • 在最佳情况下,每次把数组划分成规模大致相等的两部分,设 T ( n ) 表示使用快速排序算法对包含 n 个元素的数组排序所需的时间,类似归并排序有:
    T ( n ) = 2 T ( n 2 ) + n

    最终得到: T ( n ) = O ( n l o g 2 n )
  • 最坏情况下,划分n个元素需要比较和移动n次,因此划分的时间为 O ( n ) ,每次主元会把数组划分成一个大的子数组和一个空数组,大的子数组是在上次的子数组的基础上减1,算法复杂度为:
    ( n 1 ) + ( n 2 ) + . . . + 2 + 1 = O ( n 2 )
  • 空间复杂度 O ( n l o g 2 n )
  • 归并排序和快速排序都使用了“分而治之”和“递归”的思想方法,对于归并排序,大量的工作是在划分之后,把两个子线性表排序合并部分;对于快速排序,大量的工作是划分成两个子数组并保持子数组元素满足条件。
  • 最差情况下,归并好于快速排序,因为归并排序一直在等分数组,而快速排序的划分和主元有关;平均情况下两者相等,归并需要额外空间,快速排序不需要。

7.堆排序

堆排序

  • 堆排序:先把所有数据都输入进一个 二叉堆,然后从堆中不断移除最大值,形成一个有序序列。
  • 二叉堆是什么?
    ①是一个完全二叉树(除了最后一层外,其他每一层都是满节点的二叉树,且在最后一层上只缺少右边的若干结点)
    ②是一个堆,每一个父节点的值都大于等于它的子节点
  • 使用数组来保存二叉堆中的数据,根节点在数组中的下标为0,下标为i的节点的左子节点下标为 2 i + 1 ,右子节点的下标为 2 i + 2 ,父节点为 ( i 1 ) / 2
    堆的存储
  • 上述关系证明
    从数组保存堆的数据可以看出,数组中数据的组织形式和广度优先遍历的输出相同,是一个队列的输出。假设下标为i的节点处于二叉堆的第 k ( k = 0 , 1 , 2 , . . . ) 层,那么:
    • 前面 0 , 1 , . . . , k 1 层的节点数和为 2 k 1 个,在数组中的下标为 [ 0 , . . , 2 k 2 ]
    • 第k层的节点数为 2 k 个,在数组中的下标为 [ 2 k 1 , . . . , 2 k + 2 k 2 ] ,其中位于i前面的节点有 i ( 2 k 1 ) = i + 1 2 k
    • 根据广度遍历队列的规律,每一层的所有节点先进队列,再把该层的所有节点的子节点添进队列,所以第k层所有节点后面还有 2 ( i + 1 2 k ) 个节点,然后才是当前节点的左子节点
    • 当前节点的左子节点的下标为: 2 k + 2 k 2 + 2 ( i + 1 2 k ) + 1 = 2 i + 1
    • 证明完毕
  • 二叉堆的操作:添加新节点和删除根节点(最大数)之后,都要调整堆的结构,保证依然符合二叉堆的性质

    • 堆排序添加节点

    堆排序添加节点

    • 堆排序删除根节点

    堆排序删除根节点

//建立堆的类
public class Heap<E extends Comparable<E>>{
    //用来保存数据的列表
    private java.util.ArrayList<E> list = new java.util.ArrayList<>();
    public Heap(){  }
    public Heap(E[] objects){
        for(E element : objects){
            list.add(element);
        }
    }
    //添加新节点进堆
    public void add(E newElement){
        list.add(newElement);
        int currIndex = list.size() - 1;
        //如果等于0,表示只有一个根节点,不需要重构
        while(currIndex > 0){
            int parentIndex = (currIndex - 1) / 2;
            if(list.get(parentIndex).compareTo(list.get(currIndex)) < 0){
                E tmp = list.get(parentIndex);
                list.set(parentIndex, list.get(currIndex));
                list.set(currIndex, tmp);
            }else{
                break;
            }
            currIndex = parentIndex;
        }
    }
    //从堆中删除最大数-根节点
    public E remove(){
        if(list.size() == 0){
            return null;
        }
        E root = list.get(0);
        list.set(0, list.get(list.size() -1));
        list.remove(list.size()-1);

        int currIndex = 0;
        while(currIndex < list.size()){
            int currLeft = 2*currIndex +1, currRight = 2*currIndex +2;
            //如果左子节点的下标大于等于列表长度,说明没有子节点
            if(currLeft >= list.size()) { break; }
            //如果右子节点存在,比较左子结点和右子节点的值,找到最大值,否则最大值就是左子结点
            int maxIndex = currLeft;
            if(currRight < list.size()){
                maxIndex = list.get(currRight).compareTo(list.get(currLeft)) > 0 ? currRight : currLeft;
            }
            //如果当前节点小于子节点的最大值,就交换
            if(list.get(currIndex).compareTo(list.get(maxIndex)) < 0){
                E tmp = list.get(maxIndex);
                list.set(maxIndex, list.get(currIndex));
                list.set(currIndex, tmp);
                currIndex = maxIndex;
            }else{
                break;
            }
        }
        return root;
    }
    //返回节点数
    public int getSize(){
        return list.size();
    }
}
//堆排序函数
public static <E extends Comparable<E>> void HeapSort(E[] list){
    Heap<E> heap = new Heap<>();

    for(E e:list){
        heap.add(e);
    }
    for(int i = list.length - 1; i >= 0; i--){
        list[i] = heap.remove();
    }
}
  • 复杂度分析:
    • 设h表示包含n个元素的堆的高度,由于堆是一个完全二叉树,所以第一层有1个节点,第2层有2个节点,…,第k层有 2 k 1 个节点,第h-1层有 2 h 2 个节点,第h层至少有一个节点最多有 2 h 1 个节点,因此:
      1 + 2 + . . . + 2 h 2 < n 1 + 2 + . . . + 2 h 2 + 2 h 1

      2 h 1 1 < n 2 h 1

      2 h 1 < n + 1 2 h

      h 1 < l o g 2 ( n + 1 ) h

      这样有 l o g 2 ( n + 1 ) h < l o g 2 ( n + 1 ) + 1 ,堆的高度为 O ( l o g 2 n )
    • add方法会从叶子节点遍历到根节点的路径,因此添加一个节点需要h步,建立一个包含n个节点的二叉堆需要 O ( n l o g 2 n ) 的时间;
    • remove方法从根节点遍历到叶子节点的路径,因此从二叉堆中删除根节点后需要h步重建二叉堆,一共调用了n次remove方法,产生一个有序数组的时间为 O ( n l o g 2 n ) .
  • 堆排序的最佳、平均和最差时间复杂度都是 O ( n l o g 2 n ) ,空间复杂度为 O ( 1 ) ,空间效率高于归并排序。
  • 堆适用于对动态数据进行实时排序,例如,操作系统把执行时间划成时间片,在每一个时间片内动态选择优先级最高的任务进行执行。在操作系统执行了某个任务之后,新的任务会到来,旧的任务的优先级也会改变,可以先建立一个堆,把动态变化的数据保存进去,然后在这个数据结构上对数据进行排序和取出。
  • 对于“选出前K最值”类问题,可以建立一个容量为K的堆,遍历数组一次添加进堆。

8.桶排序和基数排序

桶排序

  • 前面的算法都可以用在任何数值类型的数据,比如整数、浮点数、字符串以及任何可比较的对象上,经证明 基于比较的排序算法的复杂度不会好于 O ( n l o g 2 n )
  • 但是如果数据都是整数,那么可以使用桶排序,而无序进行比较。
  • 桶排序:假设N个整数范围在[0,M],建立M+1个分别标记为[0,1,2,…,M]的“桶”,把数据i放在桶i中,标记为i的桶中都放着具有相同值的元素。
//M是额外的信息
public static void BucketSort(int[] nums, int M){
    int[] bucket = new int[M+1];
    //这里是计算每个元素出现的次数
    for(int num : nums){ buckets[num]++; }

    int k =0;
    for(int i = 0; i < bucket.length; i++){
        if(bucket[i] != 0){
            for(int j=1; j <= bucket[i]; j++){
                nums[k++] = i;
            }
        }
    }
}
  • 桶排序的时间复杂度为 O ( N + M ) ,空间复杂度为 O ( M )
  • 当输入是大量的小范围数据时,使用桶排序是很合算的,比前面几种算法更方便;如果输入少量的大范围数据就不合算了,需要使用另一种排序算法:基数排序
  • 基数排序:可以看作是桶排序的一种变种,只不过这时的桶和数据范围无关,而是固定的10个(标记为0~9);
  • 排序过程:
    1. 遍历所有数据,把最后一位值是i的数据放在标记为i的桶中,然后从桶中依次取出,那么数据都是按照最后一位有序的;
    2. 遍历所有数据,把倒数第二位是i的数据放在标记为i的桶中,然后从桶中依次取出,那么数据都是按照倒数第二位有序的;
      • ……
    3. 第k趟之后,元素按照倒数第k位有序,最终结束时,所有数据都是有序的
//需要直到额外信息,L是最大数的位数个数
public static void RadixSort(int[] nums, int L){
    //初始化桶,每个桶是一个列表
    ArrayList<Integer>[] buckets = new ArrayList<>[10];
    for(int i = 0; i < buckets.length; i++){
        buckets[i] = new ArrayList<>();
    }

    for(int k = 1; k <= L; k++){
        //从倒数第k位开始,把每一个数依次添加进对应的桶中
        for(int num : nums){
            buckets[getDigit(num, k)].add(num);
        }
        //把每个桶中的数据再取出来
        int idx = 0;
        for(ArrayList<Integer> bucket : buckets ){
            for(int j = 0; j < bucket.size(); j++){
                nums[idx++] = bucket.get(j);
            }
            bucket.clear();
        }
    }
}
//取出倒数第k位上的数字,范围在[0,9]之间
private int getDigit(int num, int k){
    int val;
    while(k > 0){
        val = num % 10;
        num /= 10;
        k--;
    }
    return val;
}
  • 基数排序的时间复杂度为 O ( p ( N + b ) ) ,空间复杂度为 O ( b p + N ) ,其中p是数据的最大位数,N是数据个数,b是桶的个数;
  • 基数排序的一个应用是,如果所有字符串都有相同的长度L,对每个字符串使用桶,那么可以实现在 O ( N L ) 时间内的基数排序。

排序算法的稳定性

排序算法的稳定性:若待排序的序列中,存在多个相等的数据,经过排序, 这些数据的的相对位置保持不变,则称该算法是稳定的;若经排序后,记录的相对次序发生了改变,则称该算法是不稳定的。
* 稳定性的好处:排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。另外,如果排序算法稳定,可以避免多余的比较。
* 稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
* 不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。

冒泡排序的稳定性

选择排序的稳定性

插入排序的稳定性

如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。

希尔排序的稳定性

希尔排序时一种不稳定的排序,两个相等的数A==B,A在B前面,在减小增量的过程中,B如果被排到A的前面,在后续的插入排序中A会被排在B的后一位。

归并排序的稳定性

归并排序中,两个相等的数A==B,A在B前面,如果A和B被划分进同一个子数组,那么在合并时,A在B前面,如果A被划分进左数组,B被划分进右数组,那么合并后A依然在B前面。

快速排序的稳定性

快速排序是不稳定的,两个相等的数A==B,A在B前面,如果A被选择为主元,那么A会被移动到B的后面

堆排序的稳定性

堆排序是不稳定的,在建堆的时候是稳定的,在出堆的时候是不稳定的

桶排序和基数排序的稳定性

两者都是稳定的

2018.05.07–版本1
2018.05.08–版本2

猜你喜欢

转载自blog.csdn.net/smj19920225/article/details/80231294
今日推荐