内部排序算法思路与实现【附图解&复杂度分析】


内部排序

  排序也称排序算法(Sort Algorithm),排序是将一组数据,依指定的顺序进行排列的过程。排序算法分为两类:

  1. 内部排序:将需要处理的所有数据都加载到内部存储器中进行排序。
  2. 外部排序:数据量过大,无法全部加载到内存中,需要借助外部存储进行排序。

  排序算法根据稳定性可以分为:

  1. 稳定排序:相同元素在排序后的前后相对顺序保持不变。比如{5’,2,5’’},排序后为{2,5’,5’’},{5’} {5’’} 相对顺序没有改变。
  2. 非稳定排序: 相同元素在排序后的前后相对顺序可能发生了变化。比如{5’,2,5’’},排序后为{2,5’’,5’},{5’’} {5’} 相对顺序发生了改变。

1. 直接插入排序

插入排序包括直接插入排序和希尔排序。
对于插入排序,特征是进行元素间的比较。

算法思想
  对于待排序的数组,构建有序序列,对于无序序列中的每个元素,在有序序列中从后向前寻找到相应的位置进行插入。
  例如,对于有序序列 {38,49,65,97} ,要插入元素 56,显然 56 要插入在 65 之前,则将 {65} , {97} 向后移动一位,将 {56} 插入在 {65} 之前,排序后数组为 {38,49,56,65,97}。

算法图解:对数组 [4,2,2,78,5,45] 进行直接插入排列:
直接插入排序
算法稳定性:在直接插入排序中,对于相同的数据,可以设置条件确定插入位置,不必改变先后顺序。比如上例中,两个元素2的相对前后顺序没有变化。算法稳定

代码实现

	/**
     * 简单插入排序:稳定排序
     * @param arr 传入的数组
     */
    public static void insertSort(int[] arr){
        int len=arr.length;
        for(int i=1;i<len;i++){
            int cur=arr[i];
            //在有序序列中查找合适的插入位置
            int j;
            for(j=i-1;j>=0&&arr[j]>cur;j--){
                arr[j+1]=arr[j];
            }
            //插入该元素
            arr[j+1]=cur;
        }
    }

复杂度分析

时间复杂度 O(N2)
  最好情况为数组升序排列,只需要比较 n-1 次即可,时间复杂度 O(N)。
  最坏情况为数组降序排列,需要比较 1+2+…+(n-1)=n×(n-1)/2 次,交换 n-1 次,时间复杂度为 O(N2)。
  插入排序的平均时间复杂度为 O(N2)。

空间复杂度 O(1)
  直接插入排序使用常数级别的额外空间。

算法特征

  1. 适合少量数据的排序;算法稳定。
  2. 插入排序在对几乎已经排好序的数据操作时,效率高,可以达到线性排序的效率。
  3. 插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。


2. 希尔排序

算法思想
  希尔排序又称缩小增量排序,是插入排序的改进形式。
  考虑到直接插入排序在序列几乎排好序时效率达到线性排序级别,希尔排序依据不断递减的步长将待排序序列划分为若干子序列,对每个子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全部记录依次进行直接插入排序。

算法图解:对数组 [49’,38,65,97,76,13,27,49’’,55,04] 进行希尔排序:
希尔排序-1

希尔排序-2

算法稳定性:一次插入排序是稳定的,但是希尔排序对数据进行分组处理,分别进行插入排序,相同元素可能划分在不同组中,从而相对前后顺序会发生改变。比如上例中49’’ 与49’ 划分在不同组中,排序后顺序发生了变化。希尔排序不稳定

代码实现

    /**
     * 希尔排序:缩小增量排序,步长每次模2递减
     * @param arr 待排序数组
     */
    public static void shellSort(int[] arr){
        int len=arr.length;
        int gap=len/2;
        //步长递减
        while(gap>=1) {
            /* 每次分为gap组 */
            for (int i = 0; i < gap; i++) {
                //对于每一组,进行直接插入排序
                for (int j = i + gap; j < len; j += gap) {
                    int cur = arr[j];
                    int pre = j - gap;
                    while (pre >= 0 && arr[pre] > cur) {
                        arr[pre + gap] = arr[pre];
                        pre -= gap;
                    }
                    arr[pre+gap] = cur;
                }
            }
            gap/=2;
        }
    }

复杂度分析

时间复杂度 O(N log2 N)
  希尔排序可以取不同的增量序列,相应的时间复杂度也不同;希尔排序最后一轮的增量必须为 1,保证排序完成后数组一定有序。
  步长序列为 n/2i 时,最坏情况下退化为直接插入排序,时间复杂度为 O(N2)。
  步长序列为 2k-1 时,最坏情况下时间复杂度为 O(N3/2)。
  步长序列为 2i3j 时,最坏情况下时间复杂度为 O(N log2 N)。
  希尔排序的渐进时间复杂度为O(N log2 N)。

空间复杂度 O(1)
  希尔排序使用常数级别的额外空间。



3. 简单选择排序

选择排序包简单选择排序和堆排序。
对于选择排序,特征是进行元素间的比较与交换。

算法思想
  核心思想是比较,交换。在数组元素的比较中找到一个最小的元素,将其放在起始位置;之后每次从未排序序列中找到最小元素,将其放到已排序序列的末尾(即与未排序序列的首元素进行交换),直到所有元素均排序完毕。

算法图解:对数组 [4,5’,78,5’’,17,1] 进行简单选择排序:
简单选择排序

算法稳定性
  在寻找最小元素,进行元素交换的过程中,可能导致相同元素的前后顺序发生变换。
  例如:对于序列 (7) 2 5 9 3 4 [7] 1,用直接选择排序算法进行排序时候, (7) 和 1 调换, (7) 就跑到了 [7] 的后面了,原来的次序改变了,这样就不稳定了。如上例中的 5’5’’ 顺序改变。
  选择排序不稳定

代码实现

    /**
     * 简单选择排序,不稳定
     * @param arr 待排序数组
     */
    public static void selectSort(int[] arr){
        int len=arr.length;
        for(int i=0;i<len-1;i++){
            //已排序序列为[0,i),在未排序序列[i,len-1]中寻找最小元素下标
            int min=i;
            for(int j=i+1;j<len;j++){
                if(arr[j]<arr[min]){
                    min=j;
                }
            }
            //交换当前元素arr[i]与最小元素arr[min]
            if(i!=min){
                int temp=arr[i];
                arr[i]=arr[min];
                arr[min]=temp;
            }
        }
    }

复杂度分析

时间复杂度 O(N2)
  无论初始序列如何,简单选择排序在每次寻找最小值的过程中总要与相邻元素进行比较,比较次数为 1+2+…+(n-1)= n×(n-1)/2,每次比较可能发生交换,最坏情况下交换次数为 n-1 。所以简单选择排序的时间复杂度为 O(N2)

空间复杂度 O(1)
  简单选择排序使用常数级别的额外空间。



4. 堆排序


  堆的结构为完全二叉树,分为大顶堆和小顶堆:

  1. 大顶堆:每个结点的值都大于或等于其左右子结点的值,即 arr[i] ≥ arr[2×i+1] && arr[i] ≥ arr[2×i+2] ,在堆排序中用来实现升序排列
  2. 小顶堆:每个结点的值都小于或等于其左右子结点的值,即 arr[i] ≤ arr[2×i+1] && arr[i] ≤ arr[2×i+2] ,在堆排序中用来实现降序排列

算法思想
  堆排序属于选择排序。
  考虑将包含 N 个元素的数组进行升序排列,堆排序分为两步:

  1. 建堆。先将待排序的数组建成大顶堆,使得每个父节点都大于等于它的左右子节点,时间复杂度为 O(N)
  2. 堆调整。将堆顶最大值与数组末尾元素交换,调整堆顶元素使得剩下的n-1个元素仍构成大顶堆,重复步骤2,直到得到一个有序序列,时间复杂度为 O(N log N)

算法特征:首先构造大顶堆,之后进行 n-1 次循环,每次循环可以确定一个最大值放在当前序列末尾。

算法图解:对数组 [4,6,2,5,9’,9’’,1] 进行堆排序:

1.建堆
对于 N 个节点的完全二叉树,非叶子结点个数为 N/2 ,进行 N/2 次堆调整,最终构建的大顶堆为 [9’,6,9’’,5,4,2,1]。

建堆

2.堆调整
对于步骤1构建的大顶堆 [9’,6,9’’,5,4,2,1],此时堆顶元素为整个数组最大值,将其与末尾元素交换,从堆顶开始维护堆,重新寻找剩余元素中的最大值,将其交换到堆顶。

堆调整-1
堆调整-2

算法稳定性
  堆排序属于选择排序,特征是进行元素间的比较交换,在构造堆和调整堆的过程中,为了维护堆顶元素,可能导致相同元素的前后顺序发生变换。
  如上例中对于数组 [4,6,2,5,9’,9’’,1] 进行堆排序,在建堆后 9’ 为堆顶元素,进行第 1 次堆调整后,把 9’ 作为排好序的元素放在数组末尾,导致 9’9’’ 的相对顺序改变。
  堆排序不稳定

代码实现

    /**
     * 堆排序,不稳定排序
     * @param arr 待排序数组
     */
    public static void heapSort(int[] arr){
        //step1:建造大顶堆
        int len=arr.length;
        for(int start=len/2-1;start>=0;start--){
            maxHeapify(arr,start,len);
        }
        //step2:堆调整
        for(int j=len-1;j>0;j--){
            //交换arr[0],arr[j]
            int temp=arr[0];
            arr[0]=arr[j];
            arr[j]=temp;
            //堆调整
            maxHeapify(arr,0,j);
        }
    }

    /**
     * 调整索引start处的数据,使以其为堆顶元素的堆符合大顶堆的性质
     * @param arr 待排序数组
     * @param start 起始处理坐标
     * @param end 终止处理坐标
     */
    public static void maxHeapify(int[] arr,int start,int end){
        //记录堆顶元素下标
        int pre=start;
        for(int j=2*start+1;j<end;j=j*2+1){
            //当前节点arr[pre]的左右子节点的较大者
            if(j+1<end&&arr[j]<arr[j+1]){
                j++;
            }
            //若子节点大于父节点,交换arr[j],arr[pre],调整堆顶元素
            if(arr[j]>arr[pre]){
                int temp=arr[j];
                arr[j]=arr[pre];
                arr[pre]=temp;
            }
            //更新父节点
            pre=j;
        }
    }

复杂度分析

时间复杂度 O(N log N)
  堆排序的时间复杂度= 建堆+堆调整= O(N) + O(N log N) = O(N log N)

<1> 建堆时间复杂度 O(N)
:建立大顶堆
思路:假设有n个节点,那么根据堆是完全二叉树的结构,堆的最后一个非叶子结点下标为 n/2-1
处理过程

  1. 从最后一个非叶子结点开始,找出其叶子结点的最大值,若大于原来的非叶子节点,则交换。
  2. 非叶子结点下标依次递减,直到处理完所有的非叶子结点,完成大顶堆的构建。

建堆的时间复杂度为什么是O(N)?
  假设n个节点构成了一棵完全二叉树,那么二叉树深度为 h = log2 n 。由于叶子结点默认有序,我们从下标最大的非叶子结点开始考虑。
  那么第 h 层有 2(h-1) 个数据,交换次数为1 ;第 h-1 层有 2(h-2) 个数据,交换次数为 2,…,第 3 层有2(3-1) 个数据,交换次数为 h-2;第 2 层有 2(2-1) 个数据,交换次数为 h-1;第 1 层有 1 个数据,交换次数为 h
设总交换次数为s
  式1:s=h×2(0)+(h-1)×2(1)+(h-2)×2(2)+…+3×2(h-3)+2×2(h-2)+1×2(h-1)
  式1×2得到式2:
  式2:2s=h×2(1)+(h-1)×2(2)+(h-2)×2(3)+…+3×2(h-2)+2×2(h-1)+1×2(h)
  错位相减,式2-式1:
  s=2(1)+2(2)+2(3)+…+2(h-1)+(2(h)-h)=2×2(h)-h-2=2×n-log2 n-2=O(N)
  所以建堆的时间复杂度为 O(N)

<2> 堆调整时间复杂度 O(N log N)
  注意到前面已经建好了大顶堆,其时间复杂度为O(N)。现在计算的是每次将堆顶元素与末尾数据交换,进行堆调整的过程。
  假设n个节点构成了一棵完全二叉树,那么二叉树深度为 h = log2 n 。注意到我们每次都从根节点 arr[0] 向下判断,不同的是判断的深度在不断递减。即代码:maxHeapify(int[] arr,int start,int end),每次从根节点arr[0]开始调整堆,但是决定结束深度的end在不断变小
  那么第 h 层有 2(h-1) 个数据,交换次数为 h;第 h-1 层有 2(h-2) 个数据,交换次数为 h-1,…,第 3 层有 2(3-1) 个数据,交换次数为 3;第 2层有 2(2-1) 个数据,交换次数为 2;第 1 层有一个数据,交换次数为 1
设总交换次数为s,
  式1:s=1×2(0)+2×2(1)+3×2(2)+…+(h-2)×2(h-3)+(h-1)×2(h-2)+h×2(h-1)
  式1×2
  式2:2×s=1×2(1)+2×2(2)+3×2(3)+…+(h-2)×2(h-2)+(h-1)×2(h-1)+h×2(h)
  错位相减法,式2-式1
  s=h×2(h)-[2(1)+2(2)+2(3)+…+2(h-1)]-1=h×2(h)-2(h)-3=nlogn-n-3=O(nlog n)
  所以堆调整的时间复杂度为 O(N log N)

空间复杂度 O(1)
  堆排序不要任何辅助数组,只需要几个辅助变量,所占空间是常数,所以空间复杂度为O(1)。



5. 冒泡排序

冒泡排序思路包括冒泡排序和快速排序。
冒泡排序的特征是交换。

算法思路:对于待排序序列从前向后遍历,依次比较相邻元素的值,若当前元素大于下一个元素,则把两者进行交换。每一趟遍历把当前未排序序列中的最大值交换到序列最后一位。

算法图解:对数组 [4,2’,78,2’’,45,1] 进行冒泡排序:
冒泡排序

算法稳定性:冒泡排序进行的是相邻元素间的比较交换,所以对于相同元素,不会改变其前后顺序。冒泡排序稳定

算法特征

  1. 冒泡排序每次将前面的最大值比较交换到后面的位置,但是比选择排序更糟糕的是,一旦出现了前面数据大于后面数据,就要不断进行交换的过程。
  2. 一共进行 n-1 次大的循环,n 为数组长度。每次未排序序列的长度在不断变小,第 1 趟比较交换 n-1 次,第 2 次 n-2 次…,第 n-1 次比较交换 1 次。
  3. 冒泡排序稳定

代码实现

    /**
     * 冒泡排序,稳定排序
     * @param arr 待排序数组
     */
    public static void bubbleSort(int[] arr){
        int len=arr.length;
        for(int i=0;i<len-1;i++){
            //标记本次遍历是否进行了交换
            boolean flag=false;
            for(int j=0;j<len-i-1;j++){
                //交换arr[j],arr[j+1]
                if(arr[j]>arr[j+1]){
                    int temp=arr[j];
                    arr[j]=arr[j+1];
                    arr[j+1]=temp;
                    flag=true;
                }
            }
            //该次没有进行元素交换,数组有序,提前终止
            if(!flag){
                break;
            }
        }
    }

复杂度分析

时间复杂度 O(N2)
  最好情况下,数组正序,冒泡排序进行 n-1 次比较即刻终止,时间复杂度 O(N)。
  最坏情况下,数组逆序,冒泡排序需要进行 n-1 次遍历,每次未排序序列的长度在不断变小,第 1 趟比较交换 n-1 次,第 2 趟比较交换 n-2 次…,第 n-1 趟比较交换 1 次。所以总的 比较次数1+2+3+…+(n-1)=n×(n-1)/2,每次比较都要进行交换,交换次数 同样为 1+2+3+…+(n-1)=n×(n-1)/2时间复杂度为 O(N2)
  简单冒泡排序的平均时间复杂度为 O(N2)

空间复杂度 O(1)
  简单冒泡排序使用常数级别的额外空间。



6. 快速排序

快速排序对冒泡排序做出改进。

6.1 随机化快排

算法思路

  1. 快速排序基于分治法,使用递归实现。
  2. 快速排序的特征是每次找一个 基准值 pivot,对待排序序列进行一次遍历,将 小于 pivot的元素放在左半部分 ,将 大于等于 pivot的元素放在右半部分。在本次处理结束后 ,基准值pivot处于中间位置。这一操作称为 分区(partition)
  3. 之后,对左右两部分序列分别进行递归处理

算法图解:对数组[4,3’,5,2,6,1,3’’]进行快速排序:
快速排序

算法稳定性:在数组元素与基准元素进行交换时,可能会导致相同元素的前后顺序发生变化。比如上例中当基准值为 4 时,进行一轮交换,元素 3’’ 被交换到 3’ 前面,导致最终结果中 3’’ 与 3’ 前后顺序发生变化。快速排序不稳定

代码实现

   /**
     * 快速排序,不稳定排序
     * @param arr 待排序数组
     * @param left 左边界
     * @param right 右边界,[left,right]
     */
    public static void quickSort(int[] arr,int left,int right){
        if(left>=right){
            return;
        }
        int mid=partition(arr,left,right);
        //左递归
        quickSort(arr,left,mid-1);
        //右递归
        quickSort(arr,mid+1,right);
    }

    /**
     * 快速排序分片,根据基准值将序列划分为两部分
     * @param arr 待排序数组
     * @param left 左边界
     * @param right 右边界
     * @return 返回基准值的下标
     */
    public static int partition(int[] arr,int left,int right){
        //基准元素下标
        int pivot=left;
        //维护基准值右边第一个位置
        int index=left+1;
        for(int i=index;i<=right;i++){
            //若当前元素arr[i]<arr[pivot],则交换arr[index],arr[i]
            if(arr[i]<arr[pivot]){
                swap(arr,i,index);
                index++;
            }
        }
        //交换基准值到其位置index-1
        swap(arr,pivot,index-1);
        //返回基准值下标
        return index-1;
    }

    /**
     * 置换函数:交换数组两个数据
     * @param arr 数组
     * @param i 下标i
     * @param j 下标j
     */
    public static void swap(int[] arr,int i,int j){
        if(i==j){
            return;
        }
        int temp=arr[i];
        arr[i]=arr[j];
        arr[j]=temp;
    }
    

复杂度分析

时间复杂度 O(N log N)

<1> 最好情况下时间复杂度 O(N log N)
  每次分片得到的两个子序列长度接近相等,这样对于 N 个元素的数组,递归树的深度不超过 log2 N +1 ,时间复杂度最小。
  分析思路 1:对于递归树的每一层,都需要遍历 n 个元素,根据各自的基准值进行元素交换,递归树共有 log n 层,时间复杂度为 O(N log N)
  分析思路 2:对于 n 个元素,设算法的时间复杂度为 T(n),显然 T(n) 包括一次遍历当前层元素的时间复杂度 O(n),以及递归处理长度接近 n/2 的左右子序列需要的时间 T(n/2)。即算法的时间复杂度递归公式为:T(n)=2×T(n/2)+O(n),并且T(1)=1
  递推分析
  T(n) =2×T(n/2)+O(n)
    =2×[2T(n/4) +O(n/2)]+1O(n)= 4T(n/4)+2O(n)
    =4×[2T(n/8) +O(n/4)]+2O(n)= 8T(n/8)+3O(n)
    =8×[2T(n/16)+O(n/8)]+3O(n)=16T(n/16)+4O(n)
    =… …
    =nT(n/n)+kO(n) [当 k=log2n 时]
    =n+n log n
    =O(n log n)
  所以最好情况下快速排序的时间复杂度为 O(N log N)。

<2> 最坏情况下时间复杂度 O(N2)
  最坏情况下,每次分片得到的两个子序列长度分别为 n0-10 (n0为当前待排序子序列的长度),这样对于 N 个元素的数组,递归树退化为链表,深度为 N,时间复杂度最大。
  例如,对于升序或降序数组进行排序时,如果每次分片时默认的基准值都为当前区间左端点,导致划分的左右子区间长度分别为 n0-1 和 0,就会出现最坏情况
  最坏情况下,算法的时间复杂度递归公式为:T(n)=T(n-1)+T(0)+O(n)=T(n-1)+O(n),其中 T(0)=O(1)
  递推分析
  T(n) =T(n-1)+O(n)
    =T(n-2)+O(n-1)+O(n)
    =T(n-3)+O(n-2)+O(n-1)+O(n)
    =… …
    =T(0)+O(1)+O(2)+O(3)…+O(n-2)+O(n-1)+O(n)
    =n×(n+1)/2
    =O(n2)
  快速排序的最差时间复杂度为 O(N2)

<3> 快速排序期望时间复杂度 O(N log N)
  通常情况下认为,在同数量级 O(N log N) 的排序算法中,快速排序的平均性能最好。

空间复杂度 O(log N)
  对于原地分割的快速排序版本:在最好情况下,递归树深度为 log n,每一层在原数组上修改,只使用了常数空间存储变量,每次递归只返回基准值索引,空间复杂度为 O(log N)。在最坏情况下,递归树退化为链表,深度为 n,空间复杂度为 O(n)。 快速排序平均空间复杂度为 O(log N)



6.2 双路快排

出现原因

  1. 对于上述随机化快速排序,如果数组中包含大量的重复元素,在分片时由于左半部分序列均 小于 基准值,右半部分均 大于等于 基准值,左右序列长度极不平衡,甚至会导致递归树退化为链表,效率低下。
  2. 双路快排用来改进这种不平衡现象,使用两个索引遍历数组,使 小于等于 基准值的元素在左序列,大于等于 基准值的元素在右序列,尽量平衡左右序列。

算法思路

  1. 双路快速排序算法是随机化快速排序的改进版本,基于分治法,使用递归实现。
  2. 快速排序的特征是每次找一个 基准值 pivot,对待排序序列进行一次遍历,将 小于等于 pivot的元素放在左半部分 ,将 大于等于 pivot的元素放在右半部分。在本次处理结束后 ,基准值pivot处于中间位置。这一操作称为 分区(partition)
  3. 之后,对左右两部分序列分别进行递归处理

代码实现

1.双路快排形式1:赋值法

 /**
     * 2.1 双路快排-赋值
     * @param arr 数组
     * @param left 左边界
     * @param right 右边界
     */
    public static void quickSortTwoWays(int[] arr,int left,int right){
        if(left>=right){
            return;
        }
        int mid=partitionTwoWays(arr,left,right);
        //左右递归
        quickSortTwoWays(arr,left,mid-1);
        quickSortTwoWays(arr,mid+1,right);
    }

    /**
     * 2.1 双路快排分片-赋值
     * @param arr 数组
     * @param left 左边界
     * @param right 右边界
     * @return 返回基准值索引
     */
    public static int partitionTwoWays(int[] arr,int left,int right){
        //随机在[left,right]范围内选择一个数作为基准值
        swap(arr,left,(int)(Math.random()*(right-left+1))+left);
        int l=left,r=right;
        int pivot=arr[left];
        while(l<r){
            while(l<r&&arr[r]>pivot){
                r--;
            }
            if(l<r){
                arr[l++]=arr[r];
            }
            while(l<r&&arr[l]<pivot){
                l++;
            }
            if(l<r){
                arr[r--]=arr[l];
            }
        }
        arr[l]=pivot;
        return l;
    }
    public static void swap(int[] arr,int i,int j){
        if(i==j){
            return;
        }
        int temp=arr[i];
        arr[i]=arr[j];
        arr[j]=temp;
    }

2.双路快排形式2:交换法

   /**
     * 2.2 双路快速排序-交换
     * 左半部分小于等于基准值,右半部分大于等于基准值
     * @param arr 待排序数组
     * @param left 左边界索引
     * @param right 右边界索引
     */
    public static void quickSortTwo(int[] arr,int left,int right){
        if(left>=right){
            return;
        }
        int mid=partitionTwo(arr,left,right);

        //左右递归
        quickSortTwo(arr,left,mid-1);
        quickSortTwo(arr,mid+1,right);
    }

    /**
     * 2.2 双路快排分片-交换
     * @param arr 待排序数组
     * @param left 左边界索引
     * @param right 右边界索引
     * @return 返回基准值下标
     */
    public static int partitionTwo(int[] arr,int left,int right){
        //随机在[left,right]范围内选择一个数作为基准值
        swap(arr,left,(int)(Math.random()*(right-left+1))+left);
        int pivot=arr[left];
        int i=left+1,j=right;
        while(true){
            while(i<=right&&arr[i]<pivot){
                i++;
            }
            while(j>=left+1&&arr[j]>pivot){
                j--;
            }
            if(i>j){
                break;
            }
            swap(arr,i,j);
            i++;
            j--;
        }
        swap(arr,left,j);
        return j;
    }
    public static void swap(int[] arr,int i,int j){
        if(i==j){
            return;
        }
        int temp=arr[i];
        arr[i]=arr[j];
        arr[j]=temp;
    }   

复杂度分析

时间复杂度 O(N log N)
  对于递归树的每一层,都需要遍历 n 个元素,根据各自的基准值进行元素交换,递归树共有 log n 层,时间复杂度为 O(N log N)

空间复杂度 O(log N)
  在最好情况下,递归树深度为 log n,每一层在原数组上修改,只使用了常数空间存储变量,每次递归只返回基准值索引,空间复杂度为 O(log N)。在最坏情况下,递归树退化为链表,深度为 n,空间复杂度为 O(n)。 快速排序平均空间复杂度为 O(log N)



6.3 三路快排

算法思路

  1. 对于上述双路快排,如果数组中包含大量的重复元素,在分片时只是尽量让左右序列长度取得平衡,实际效果不一定好。
  2. 三路快排用来改进这种现象,集中存放重复出现的基准值,具体使用三个索引遍历数组,使 小于 基准值的元素在左序列,等于 基准值的元素在中间序列,大于 基准值的元素在右序列,之后对左右子序列进行递归处理。

代码实现

    /**
     * 3.三路快排
     * @param arr 待排序数组
     * @param left 左边界
     * @param right 右边界
     */
    public static void quickSortThree(int[] arr,int left,int right){
        if(left<right){
            int[] temp=partitionThree(arr,left,right);
            quickSortThree(arr,left,temp[0]);
            quickSortThree(arr,temp[1],right);
        }
    }

    /**
     * 3.三路快排分片
     * @param arr 数组
     * @param left 左边界
     * @param right 右边界
     * @return 返回两个中间临界值lt,gt
     */
    public static int[] partitionThree(int[] arr,int left,int right){
        //选择基准数
        int v=arr[left];
        //中间两个临界值
        int lt=left;
        int gt=right+1;
        //i为扫描元素
        int i=left+1;
        /**
         * |  v  |    <v   |   |   ==v   |    | ... |     |   >v    |
         * |     |         |   |         |    |     |     |         |
         * left  [left+1 lt]   [lt+1  i-1]    i     gt-1  [gt  right]
         */
        //arr[left+1,lt]<v
        //arr[lt+1,i-1]==v
        //arr[gt,right]>v
        while(i<gt){
            //小于,交换arr[i],arr[lt+1]
            if(arr[i]<v){
                swap(arr,i,lt+1);
                lt++;
                i++;
            }else if(arr[i]>v) {
                //大于,交换arr[i],arr[gt-1]
                swap(arr,i,gt-1);
                gt--;
            }else {
                //等于,i++
                i++;
            }
        }
        //交换left,lt
        swap(arr,left,lt);
        lt--;
        //返回两个中间边界
        return new int[]{lt,gt};
    }
    
    public static void swap(int[] arr,int i,int j){
        if(i==j){
            return;
        }
        int temp=arr[i];
        arr[i]=arr[j];
        arr[j]=temp;
    }

复杂度分析

时间复杂度 O(N log N)
  对于递归树的每一层,都需要遍历 n 个元素,根据各自的基准值进行元素交换,递归树共有 log n 层,时间复杂度为 O(N log N)

空间复杂度 O(log N)
  在最好情况下,递归树深度为 log n,每一层在原数组上修改,只使用了常数空间存储变量,每次递归只返回中间两个基准值索引,空间复杂度为 O(log N)。在最坏情况下,递归树退化为链表,深度为 n,空间复杂度为 O(n)。 快速排序平均空间复杂度为 O(log N)



7. 归并排序

算法思路

  1. 归并排序基于分治法,分为 分割合并 两个阶段。
  2. 在分割阶段,不断将当前待排序序列分为前后长度相同的两部分,对于两个子序列进行递归分割,直到子序列长度为1。
  3. 在合并阶段,对于相邻的子序列进行两两合并,其中元素按升序排列,并回溯处理,不断合并相邻序列,直到整个序列有序。

算法图解:对数组 [4,3’,5,2’,6,1,3’’,2’’] 进行归并排序:
归并排序

算法稳定性:对于 N 个元素的数组,进行 log N 次分割,每次分割不改变数组元素位置, 进行 log N 次合并,每次合并都是相邻两个数组的合并,不改变相同元素的前后顺序。归并排序稳定

代码实现

归并排序递归版

    /**
     * 归并排序:分,将大问题分成小问题,进行递归
     * @param arr 数组
     * @param left 左边界
     * @param right 右边界
     */
    public static void mergeSort(int[] arr,int left,int right){
        if(left>=right){
            return;
        }
        int mid=(left+right)/2;
        //归
        mergeSort(arr,left,mid);
        mergeSort(arr,mid+1,right);
        //并
        mergeHelp(arr,left,right,mid);
    }

    /**
     * 归并排序:治,将分的小问题逐步合并,每一次回溯计算一次答案,回溯结束后把答案合并在一起
     * @param arr 数组
     * @param left 左边界
     * @param right 右边界
     * @param mid 中间值
     */
    public static void mergeHelp(int[] arr,int left,int right,int mid){
        int[] temp=new int[right-left+1];
        int l=left,r=mid+1;
        int index=0;
        //[l,mid]与[mid+1,r]
        while(l<=mid&&r<=right){
            temp[index++]=(arr[l]<=arr[r])?arr[l++]:arr[r++];
        }
        while(l<=mid){
            temp[index++]=arr[l++];
        }
        while(r<=right){
            temp[index++]=arr[r++];
        }
        //将整个临时数组作为排序后结果,复制到目标数组arr中,其中目标数组从left开始
        System.arraycopy(temp,0,arr,left,temp.length);
    }

复杂度分析

时间复杂度 O(N log N)
  无论长度为 N 的初始序列如何,归并排序都要进行 log N 次分割,分割过程不进行比较操作,再进行 log N 次合并,每次合并都要对整个数组进行一次遍历,比较一层的元素大小,时间复杂度 O(N),总共有 N 层,归并排序时间复杂度为 O(N log N)

空间复杂度 O(N)
  归并排序在每一层的合并过程中,需要一个长度为 N 的辅助数组记录该层合并结果,在每一层回溯后,该数组即释放,所以归并排序的空间复杂度为 O(n)



8. 基数排序

算法思路

  1. 基数排序是一种非比较型排序算法。
  2. 基数排序将数字分割成不同的位,高位补零,从低位到高位进行处理。
  3. 对于十进制数的比较,建立10个桶 count[10] ,对于每一位,统计数字0-9的数;之后计算 count[10] 的前缀和,用来确定元素的新位置;根据前缀和,完成该数位的排序。
  4. 一直到最高位统计结束,即可得到排序结果。

算法图解:对数组 [3’,12,9,3’’,24,6,100,87,33,4] 进行基数排序:
基数排序

算法稳定性:如图所示,某数位数字相同的元素在同一个桶中,通过 正序 计算桶 count[ ] 的前缀和,逆序 确定元素的新位置,可以使相同数字的前后顺序保持不变。基数排序稳定

代码实现

    /**
     * 基数排序,稳定排序
     * @param arr 待排序数组
     */
    public static void radixSort(int[] arr){
        int len=arr.length;
        //建立辅助数组,存放临时结果
        int[] buf=new int[len];
        //建立10进制数的桶,用来统计当前位置数字0-9的数目
        int[] count=new int[10];

        //获取数组中最大值
        int maxValue=Integer.MIN_VALUE;
        for(int n:arr){
            if(n>maxValue){
                maxValue=n;
            }
        }
        //获取最大值的位数d
        int d=(maxValue+"").length();
        long exp=1;

        for(int i=0;i<d;i++){
            //每次清空10个桶
            Arrays.fill(count,0);
            //1.遍历数组arr[],统计当前数字位0-9的个数
            for (int value : arr) {
                int digit = (value / (int) exp) % 10;
                count[digit]++;
            }
            //2.统计各个桶中元素的数目和
            for(int j=1;j<10;j++){
                count[j]+=count[j-1];
            }
            //3.逆序遍历数组arr[],根据桶中数字位置,将当前元素加入临时数组buf[]
            for(int j=len-1;j>=0;j--){
                int digit=(arr[j]/(int)exp)%10;
                buf[count[digit]-1]=arr[j];
                count[digit]--;
            }
            //将临时数组复制到原数组,更新exp
            System.arraycopy(buf,0,arr,0,len);
            exp*=10;
        }
    }

复杂度分析

时间复杂度 O(d (N+k) )
  对于长度为 N 的 k 进制数组进行基数排序,若数组中最大值的位数为 d ,那么需要进行 d 次循环。每次循环需要遍历两次长度为 N 的数组,遍历两次长度为 k 的桶,所以一次循环的时间复杂度为 O(N+k) ,一共进行 d 轮循环,所以基数排序时间复杂度为 O(d (N+k) )

空间复杂度 O(N+k)
  对于长度为 N 的 k 进制数组进行基数排序,需要建立长度为 N 的辅助数组,用来记录每一次遍历后排序的结果;还需要建立长度为 k 的桶,用来记录每一次该数字位中各个数字的数目,所以基数排序的空间复杂度为 O(N+k)



内部排序算法复杂度对比

算法稳定性

  1. 不稳定排序:快(快速)、希(希尔)、选(选择)、堆(堆排序)。
  2. 稳定排序:冒(冒泡)、插(插入)、归(归并)、基(基数)。

算法复杂度

排序算法最好时间复杂度最坏时间复杂度平均时间复杂度空间复杂度稳定性
插入O(N)O(N2)O(N2)O(1)稳定
希尔O(N log2 N)O(N2)O(N log2 N)O(1)不稳定
选择O(N2)O(N2)O(N2)O(1)不稳定
O(N log N)O(N log N)O(N log N)O(1)不稳定
冒泡O(N)O(N2)O(N2)O(1)稳定
快速O(N log N)O(N2)O(N log N)O(log N)不稳定
归并O(N log N)O(N log N)O(N log N)O(N)稳定
基数O(d (N+k) )O(d (N+k) )O(d (N+k) )O(N+k)稳定


参考资料

  1. 《数据结构:C语言版》严蔚敏,吴伟民编著,清华大学出版社。
  2. 《算法导论(第3版)》Thomas H.Cormen 等著,机械工业出版社。
  3. 维基百科,排序算法。
  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
对于这个问题,我可以给出一个简单实现的代码示例,供参考。以下是Python代码: ```python import random import time # 直接插入排序 def insertion_sort(arr): n = len(arr) comparisons = 0 exchanges = 0 for i in range(1, n): key = arr[i] j = i - 1 while j &gt;= 0 and arr[j] &gt; key: comparisons += 1 arr[j + 1] = arr[j] j -= 1 exchanges += 1 arr[j + 1] = key exchanges += 1 return comparisons, exchanges # 希尔排序 def shell_sort(arr): n = len(arr) comparisons = 0 exchanges = 0 gap = n // 2 while gap &gt; 0: for i in range(gap, n): temp = arr[i] j = i while j &gt;= gap and arr[j - gap] &gt; temp: comparisons += 1 arr[j] = arr[j - gap] j -= gap exchanges += 1 arr[j] = temp exchanges += 1 gap //= 2 return comparisons, exchanges # 直接选择排序 def selection_sort(arr): n = len(arr) comparisons = 0 exchanges = 0 for i in range(n - 1): min_idx = i for j in range(i + 1, n): comparisons += 1 if arr[j] &lt; arr[min_idx]: min_idx = j arr[i], arr[min_idx] = arr[min_idx], arr[i] exchanges += 1 return comparisons, exchanges # 堆排序 def heap_sort(arr): def heapify(arr, n, i): largest = i l = 2 * i + 1 r = 2 * i + 2 if l &lt; n and arr[l] &gt; arr[largest]: comparisons += 1 largest = l if r &lt; n and arr[r] &gt; arr[largest]: comparisons += 1 largest = r if largest != i: arr[i], arr[largest] = arr[largest], arr[i] exchanges += 1 heapify(arr, n, largest) n = len(arr) comparisons = 0 exchanges = 0 for i in range(n // 2 - 1, -1, -1): heapify(arr, n, i) for i in range(n - 1, 0, -1): arr[i], arr[0] = arr[0], arr[i] exchanges += 1 heapify(arr, i, 0) return comparisons, exchanges # 冒泡排序 def bubble_sort(arr): n = len(arr) comparisons = 0 exchanges = 0 for i in range(n): for j in range(n - i - 1): comparisons += 1 if arr[j] &gt; arr[j + 1]: arr[j], arr[j + 1] = arr[j + 1], arr[j] exchanges += 1 return comparisons, exchanges # 快速排序 def quick_sort(arr): def partition(arr, low, high): i = low - 1 pivot = arr[high] for j in range(low, high): if arr[j] &lt;= pivot: i += 1 arr[i], arr[j] = arr[j], arr[i] exchanges += 1 comparisons += 1 arr[i + 1], arr[high] = arr[high], arr[i + 1] exchanges += 1 return i + 1 def quick_sort_helper(arr, low, high): if low &lt; high: pi = partition(arr, low, high) quick_sort_helper(arr, low, pi - 1) quick_sort_helper(arr, pi + 1, high) n = len(arr) comparisons = 0 exchanges = 0 quick_sort_helper(arr, 0, n - 1) return comparisons, exchanges # 归并排序 def merge_sort(arr): def merge(arr, l, m, r): n1 = m - l + 1 n2 = r - m L = [0] * n1 R = [0] * n2 for i in range(n1): L[i] = arr[l + i] for i in range(n2): R[i] = arr[m + 1 + i] i = 0 j = 0 k = l comparisons = 0 exchanges = 0 while i &lt; n1 and j &lt; n2: comparisons += 1 if L[i] &lt;= R[j]: arr[k] = L[i] i += 1 else: arr[k] = R[j] j += 1 k += 1 exchanges += 1 while i &lt; n1: arr[k] = L[i] i += 1 k += 1 exchanges += 1 while j &lt; n2: arr[k] = R[j] j += 1 k += 1 exchanges += 1 return comparisons, exchanges def merge_sort_helper(arr, l, r): comparisons = 0 exchanges = 0 if l &lt; r: m = (l + r) // 2 comparisons_l, exchanges_l = merge_sort_helper(arr, l, m) comparisons += comparisons_l exchanges += exchanges_l comparisons_r, exchanges_r = merge_sort_helper(arr, m + 1, r) comparisons += comparisons_r exchanges += exchanges_r comparisons_merge, exchanges_merge = merge(arr, l, m, r) comparisons += comparisons_merge exchanges += exchanges_merge else: comparisons = 0 exchanges = 0 return comparisons, exchanges n = len(arr) return merge_sort_helper(arr, 0, n - 1) # 基数排序 def radix_sort(arr): def counting_sort(arr, exp): n = len(arr) output = [0] * n count = [0] * 10 for i in range(n): idx = (arr[i] // exp) % 10 count[idx] += 1 for i in range(1, 10): count[i] += count[i - 1] i = n - 1 while i &gt;= 0: idx = (arr[i] // exp) % 10 output[count[idx] - 1] = arr[i] count[idx] -= 1 i -= 1 i = 0 for i in range(n): arr[i] = output[i] return n - 1, n - 1 max_val = max(arr) exp = 1 comparisons = 0 exchanges = 0 while max_val // exp &gt; 0: comparisons_round, exchanges_round = counting_sort(arr, exp) comparisons += comparisons_round exchanges += exchanges_round exp *= 10 return comparisons, exchanges # 生成随机数 def generate_random_data(n): return [random.randint(0, 99) for _ in range(n)] # 生成100000个随机数 data = [random.randint(0, 9999) for _ in range(100000)] # 选择排序算法进行演示 algorithms = [ (&quot;直接插入排序&quot;, insertion_sort), (&quot;希尔排序&quot;, shell_sort), (&quot;直接选择排序&quot;, selection_sort), (&quot;堆排序&quot;, heap_sort), (&quot;冒泡排序&quot;, bubble_sort), (&quot;快速排序&quot;, quick_sort), (&quot;归并排序&quot;, merge_sort), (&quot;基数排序&quot;, radix_sort) ] print(&quot;排序算法效率比较平台&quot;) print(&quot;======================&quot;) # 功能1:通过随机数函数生成15个(0-99)的整数,在系统里通过选择不同的排序算法进行演示并显示每一趟的排序结果,同时给出排序算法的关键字比较次数和移动次数 print(&quot;演示排序算法&quot;) print(&quot;----------------------&quot;) data_15 = generate_random_data(15) for name, algorithm in algorithms: arr = data_15.copy() comparisons, exchanges = algorithm(arr) print(name) print(&quot;排序前:&quot;, data_15) print(&quot;排序后:&quot;, arr) print(&quot;关键字比较次数:&quot;, comparisons) print(&quot;移动次数:&quot;, exchanges) print(&quot;----------------------&quot;) # 功能2:通过随机数函数生成100000个(0-9999)的整数,同时输出各排序算法的具体计算时间 print(&quot;计算排序算法时间&quot;) print(&quot;----------------------&quot;) for name, algorithm in algorithms: arr = data.copy() start_time = time.time() algorithm(arr) end_time = time.time() print(name) print(&quot;计算时间:&quot;, end_time - start_time, &quot;秒&quot;) print(&quot;----------------------&quot;) ``` 以上代码实现了各种排序算法,并演示了如何使用这些算法进行排序,并计算出每个算法的关键字比较次数和移动次数。同时,还演示了如何使用这些算法对100000个随机数进行排序,并输出它们的具体计算时间。需要注意的是,这只是一个简单的示例,实际上还有很多需要考虑的细节和问题,例如算法的优化、内存的管理等等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

VoidTaoist

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值