【Java】排序算法(思路及图解)

1.排序的概念

排序 :所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

稳定性 :假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

内部排序:数据元素全部放在内存中的排序。

外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

2.常见的排序算法的实现

(1)插入排序

插入排序分为 直接插入排序 和 希尔排序。

#直接插入排序

基本思想:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 (升序或者降序)。

思路图解:以数组集合{9,1,2,5,7,4,8,6,3,5}为例

根据以上的图片,代码思路如下:

思路(以升序为例):

  • 定义一个变量 i,从数组下标为 i=1的位置开始遍历 ,因为++第一个元素(下标0)默认是已排序的++ 。也就是说,外层循环的初始位置从i=1开始,在外层循环中 ,使用++变量tmp保存当前待插入的元素array[i]++ 。定义一个变量j,从当前元素i的前一个元素i-1,即j=i-1 的位置开始遍历,注意是向前遍历,也就是内层循环的初始位置从i-1开始,在内层循环中 ,如果 array[j]>tmp,那么我们就将 array[j] 这个较大的元素往后移动一位,即将 array[j] 移动到 j+1 的位置上( i 的位置上),array[j+1]=array[j] , 然后 j--,如果array[j]<=tmp,那直接将tmp放到array[j]的后一个位置,即array[j+1]=tmp,然后跳出循环。
  • 注意:在内层循环中,如果发现array[j] > tmp,则将array[j]的值向后移动到array[j+1]。如果发现array[j] <= tmp,则说明tmp应该插入到array[j]的后面,也就是array[j+1]的位置,然后我们执行array[j+1] = tmp并跳出内层循环。但是,有一种情况:如果一直比较到 j--(也就是所有已排序元素都比tmp大),那么内层循环会因为 j>=0 条件不满足而退出。此时,我们还没有执行插入操作。所以,在内层循环之后,我们需要将tmp放入正确的位置,这个位置就是 j+1(因为此时j为-1,j+1就是0,也就是数组的第一个位置)。所以,array[j+1]=tmp这一行代码包含了以上的两种情况,因此我们可以统一写在内层循环结束之后,也就是说在array[j]<=tmp的情况中,直接break跳出内层循环,不在这里赋值,这样可以避免重复赋值。
  • 总的来说,就是这种情况统一处理了:
  • * ++找到合适位置break的情况++
  • * ++比较到数组开头(j=-1)的情况++
java 复制代码
public static void insetSort(int[] array) {
    for(int i = 1; i < array.length; i++) {
        int tmp = array[i];//待插入的元素
        int j = i-1;//从当前元素的前一个开始比较
        //寻找插入位置并移动元素
        for(; j >= 0; j--) {
            //如果array[j]大于tmp,则将array[j]元素向右移动到j+1的位置,即i的位置
            if(array[j] > tmp) {
                array[j+1] = array[j];
            }else {
                //如果不大于tmp,则break跳出循环(注意,else这里只执行跳出循环,不赋值,统一到下面的一条语句赋值,否则在这里赋值会和下面的赋值重复操作)
                break;
            }
        }
        //由于array[j]不大于tmp,那么array[j+1]的位置放的就是tmp / 如果j<0的情况也通过这条语句赋值
        array[j+1] = tmp;
    }
}
直接插入排序的特性
  • 直接插入排序是一个稳定的排序:当遇到相等元素时(array[j] == tmp),执行break,不进行交换,相等元素的相对位置保持不变,从思路图中也可以看到,相同元素5它们的前后位置并没有颠倒。由此说明,本身如果是一个稳定的排序,那么可以实现为不稳定的排序(将array[j]>tmp这个条件改成array[j]>=tmp就可以实现将稳定变成不稳定,让相等元素也交换),但是如果本身是不稳定的排序,不能实现为稳定的排序
  • 时间复杂度: ------ 平均情况。 最坏的情况------,逆序的 5 4 3 2 1:对于第i个元素,需要比较i次,移动i次。总操作次数为:2*(1+2+3+...+n-1) = 2 * n(n-1)/2 = n(n-1)。最好的情况------,本身就是有序的 1 2 3 4 5:每次内层循环只需要比较一次就会break(或者进入内层循环的条件不满足,因为当前元素已经大于等于前一个元素)。所以,对于每个元素,我们只进行了常数次操作。
  • 空间复杂度:,算法只使用了固定数量的额外变量:i, j, tmp 等循环变量和临时变量,无论输入数组多大,这些变量的数量都是固定的,直接插入排序是原地排序:所有操作都在原数组上进行,不需要额外的数组空间。

#希尔排序(缩小增量排序)

基本思想:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。

希尔排序是对直接插入排序的优化,希尔排序的思想基于以下两点:

  • 1.直接插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
  • 2.但直接插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。

希尔排序 通过将比较的全部元素分为几个区域 来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步 。然后算法再取越来越小的步长 进行排序,最后一步就是普通的插入排序(直接插入排序),但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。

思路步骤:

  • 1.选择一个增量序列:通常初始增量gap为数组长度的一半,然后每次增量减半,直到增量为1。
  • 2.按增量序列个数k,对序列进行k趟排序。
  • 3.每趟排序,根据对应的增量gap,将待排序列分割成若干长度为m的子序列,分别对各子序列进行直接插入排序 。仅增量因子为1时,整个序列作为一个整体来处理,子序列长度即为整个序列的长度。

注意:当gap > 1时都是预排序 ,目的是让数组更接近于有序。希尔排序的分区是跳跃式的分区,根据步长gap的多少而分区。

  • 直接插入排序:每次只把当前元素和它紧挨着的前一个元素比较并插入,是"一步步挪动"。
  • 希尔排序:根据一个称为"步长"(Gap)的值,将整个数组分成若干逻辑上的子序列,然后在各个子序列内进行直接插入排序。这些子序列的成员在原数组中不是连续的 ,而是间隔了步长大小的距离,这就是"跳跃式"。

思路图解:以数组集合{9,1,2,5,7,4,8,6,3,5}为例

根据以上的图片,代码如下:

java 复制代码
public static void shellSort(int[] array) {
    //数组长度=gap
    int gap = array.length; 
    while(gap > 1) {
        //初始间隔为数组长度的一半,逐步缩小
        gap /= 2;
        //对每个子序列进行插入排序
        shell(array,gap);
    }
}
private static void shell(int[] array,int gap) {
    for(int i = gap;i < array.length; i++) {
        int tmp = array[i]; 
        int j = i-gap;
        for(;j >= 0;j -= gap) {
            //插入排序
            if(array[j] > tmp) {
                array[j+gap] = array[j];
            }else {
                break;
            }
        }
        array[j+gap] = tmp;
    }
}
希尔排序的特性
  • 希尔排序是一个不稳定的排序,上图中的最终结果与开始时数组进行对比,发现相同元素5发生了位置的改变。
  • 时间复杂度:希尔排序的时间复杂度不好计算,因为gap有多种取法,导致很难去计算,不过可以认为希尔排序时间复杂度区间大概在 ~ 之间,以上我们写的代码的时间复杂度是
  • 空间复杂度:

(2)选择排序

选择排序分为 直接选择排序 和 堆排序。

#直接选择排序

基本思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。

思路图解:

根据以上的图片,代码如下:

java 复制代码
public static selectSort(int[] array) {
    for(int i = 0;i < array.length; i++) {
        int minIndex = i;
        for(int j = i+1;j < array.length; j++) {
            if(array[j] < array[minIndex]) {
                minIndex = j;
            }
        }
        swap(array,i,minIndex);
    }
}
private static void swap(int[] array,int i,int j) {
    int tmp = array[i];
    array[i] = array[j];
    array[j] = tmp;
}
直接选择排序的特性
  • 直接选择排序是一个不稳定的排序。
  • 时间复杂度: ------ 和数组是否有序无关
  • 空间复杂度:
#优化直接选择排序

上述的排序方法中,即使数组是有序的,依然需要进行比较和交换,那么可以对直接选择排序进行优化,避免不必要的交换和比较。

思路:同时遍历选出最大元素和最小元素。

定义一个变量left,表示数组的第一个元素的下标,定义一个变量right,表示数组的最后一个元素的下标,在left<right的情况下,进入循环,++定义一个minIndex,用来存放数组中最小元素的下标,maxIndex用来存放数组中最大元素的下标++ ,初始时它们都为left(0),定义一个变量i,从数组的第二个元素开始遍历,即i=left+1(i要小于等于right),进入内层循环,如果array[i]<array[minIndex],那么更新最小元素的下标为i,即minIndex=i ,继续 i++ 遍历,如果array[i]>array[maxIndex],那么更新最大元素的下标为i,即maxIndex=i,i 遍历直到不符合条件,跳出内层循环,先让left和minIndex位置上的元素交换,这样left存放的就是数组中最小的元素,然后让right和maxIndex位置上的元素交换,这样right存放的就是数组中最大的元素,最后让left++,right--,开始新一轮的遍历,直到数组有序。

不过,上述的思路还有一个问题存在,这个问题通过以下的思路图呈现及解决:

根据上图,代码如下:

java 复制代码
public static void selectSort(int[] array) {
    int left = 0;
    int right = array.length-1;
    while(left < right) {
        for(int i = left+1;i <= right; i++) {
            int minIndex = left;
            int maxIndex = right;
            if(array[i] < array[minIndex]) {
                minIndex = i;
            }
            if(array[i] > array[maxIndex]) {
                maxIndex = i;
            }
        }
        swap(array,left,minIndex);
        if(maxIndex == left) {
            maxIndex = minIndex; 
        }
        swap(array,right,maxIndex);
        left++;
        right--;
    }
}

#堆排序

基本思想:堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。

在优先级队列的学习中,我们详细学过了关于堆的知识,其中包括了堆排序,这里不再详写,详情请看文章:https://blog.csdn.net/Zzzzmo_/article/details/155076023?spm=1001.2014.3001.5502

java 复制代码
public static void heapSort(int[] array) {
    //建大根堆
    createHeap(array);
    //排序
    int end = array.length-1;
    while(end > 0) {
        swap(array,0,end);
        siftDown(array,0,end);
        end--;
    }
}
private static void createHeap(int[] array) {
    for(int parent = (array.length-1-1)/2;parent >= 0; parent--;) {
        siftDown(array,parent,array,length);
    }
}
private static void siftDown(int[] array,int parent,int length) {
    int child = 2*parent+1;
    while(child < length) {
        if(child+1 < length && array[child] < array[child+1]) {
            child++;
        }
        if(array[child] > array[parent]) {
            swap(array,child,parent);
            parent = child;
            child = 2*parent+1;
        }else {
            break;
        }
    }
}
堆排序的特性
  • 堆排序是一个不稳定的排序。
  • 时间复杂度:
  • 空间复杂度:

(3)交换排序

交换排序分为 冒泡排序 和 快速排序。

所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置。

#冒泡排序

思路(以升序为例):

    1. 将数组中相邻元素从前往后依次进行比较,如果前一个元素比后一个元素大,则交换,一趟下来后最大元素 就在数组的末尾
    1. 依次从上上述过程,直到数组中所有的元素都排列好

思路图解:

根据上图,代码如下:

java 复制代码
public static void bubbleSort(int[] array) {
    //趟数:对于长度为n的数组,最多需要n-1趟排序
    for(int i = 0;i < array.length-1; i++) {
        //比较-执行相邻元素的比较和交换:趟排序后,最大的元素会"冒泡"到数组末尾
        // 因此每趟可以减少一次比较(减去i)
        for(int j = 0;j < array.length-1-i; j++) { 
            //如果当前元素大于后一个元素,交换它们(升序排序)
            if(array[j] > array[j+1]) {
                swap(array,j,j+1);
            }
        }
    }
}
冒泡排序的特性
  • 冒泡排序是一个稳定的排序(如果变成array[j]>=array[j+1]就会变成不稳定的排序)。
  • 时间复杂度:
  • 空间复杂度:
#优化冒泡排序

在进行冒泡排序过程中,可能会出现某一趟排序本身就是有序的,即在某一趟的排序中没有发生任何元素交换,说明数组已经完全有序,可以立即终止排序,那么可以利用一个布尔类型变量来完成这一优化:定义一个boolean类型的变量flag,每趟排序开始前将flag置为false,如果某一趟中进行了元素交换,那么就将flg改为true,如果并没有进行交换元素,那么flag一直就是false,立即终止排序。

java 复制代码
public static void bubbleSort(int[] array) {
    for(int i = 0;i < array,length-1; i++) {
        boolean flag = false;
        for(int j = 0;j < array.length-1-i; j++) {
            if(array[j] > array[j+1]) {
                swap(array,j,j+1);
                flag = true;
            }
        } 
        if(!flag) { //flag == false;
            break;
        }
    }
}

#快速排序

基本思想:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

根据以上的基本思想,我们可以发现,快速排序的思路与二叉树相似。

思路:以数组中第一个元素left为基准值,首先从后边 (数组最后一个元素right)开始找到比基准值小的元素 ,找到之后,停下来,此时的right下标位置上的元素就是比基准值小的元素 ;然后从前边 (数组第一个元素)开始找到比基准值大的元素 ,找到之后,停下来,此时的left下标位置上的元素就是比基准值大的元素 ;然后交换left和right位置上的元素,重复循环上述的操作交换元素,直到left和right相遇,停下来,交换基准值和left(right)位置上的元素,此时基准值的左序列都比基准值小,右序列都比基准值大。

思路图解:

如上图,完成了基准值的左序列都比基准值小,右序列都比基准值大的操作,那么接下来的步骤就和二叉树类似,继续去遍历左序列和右序列 ,执行的操作和上述的图片的过程相同,左序列遍历到left和right相遇,那么左序列的左序列和右序列又符合比左序列的基准值大或者小的规律,右序列也一样,那么这里可以使用递归思想。

从上述的两张图片中,都可以看出,这个pivot相遇点很重要,而从图中我们又可以知道,当子序列中只有一个元素的时候,那么就代表遍历结束,需要递归返回了,那么这个就是结束条件,即当left和right在同一个元素上(left==right)就代表递归结束,还有一种情况,例如上图,我们在遍历右序列的时候,发现右序列只有左序列而没有右序列,也就是说如果子序列为空的情况也代表递归结束要返回,即left>right时是空序列,要返回。

(为什么是left>right呢?在图中说过左序列中right=pivot-1,右序列中left=pivot+1)

快速排序递归实现的主框架

根据以上的所有分析,先写出主要的框架

java 复制代码
public static void quickSort(int[] array) {
    quick(array,0,array.length-1);
}
private static void quick(int[] array;int start,int end) {
    if(start >= end) {
        return;
    }
    int pivot = partition(array,start,end);
    quick(array,left,pivot-1);
    quick(array,pivot+1,right);
}

这个pivot相遇点很重要,有了它我们才可以进行后续的递归遍历操作,那么要找到这个相遇点,有以下的三种方法:(上述代码中 partition 方法,就是完成寻找相遇点pivot的任务)

#1Hoare划分法快速排序

该方法其实就是上述的思路图解的图片的过程------找基准值,然后从后往前遍历找比基准小的元素,从前往后找比基准值大的元素。直接依据分析写出代码即可:

java 复制代码
private static int partition(int[] array,int left,int right) {
    //基准值
    int tmp = array[left];    
    //记录基准值的下标位置
    int tmpLeft = left;
    while(left < right) {
        //找比tmp小的元素下标
        if(left < right && array[right] >= tmp) {
            right--;
        }
        //找比tmp大的元素下标位置
        if(left < right && array[left] <= tmp) {
            left++;
        }
        //交换left和right位置的元素
        swap(array,left,right);
    }
    //left和right相遇,交换基准值和相遇位置上的元素
    swap(array,left,tmpLeft);
    //返回相遇点的下标,即pivot
    return left;
}
  • 问题1:为什么是先从后往前找比基准值大的元素,再从前往后找比基准值小的元素,不能先从前往后找比基准值小的元素,然后从后往前找比基准值大的元素吗?

------ 不能,如果这样换完之后不符合基准值的左序列都比基准值小,右序列都比基准值大。如下图所示:

注意:也可以找数组中其他的元素作为基准值,但是一定要先从后往前找比基准值小的元素,再从前往后找比基准值大的元素。

  • 问题2:为什么array[right] >= tmp、array[left] <= tmp 要取等号?

--------- 示例:如下图,基准值是6,而left和right都是6,如果不取等号 ,那么left/right都进不去循环进行++或者--,那么就代码就会继续往下走,进行交换操作,交换完成之后,还是6和6与基准值6进行比较,会陷入死循环取等号就可以避免这种情况

#2挖坑法快速排序(一般快速排序都采用挖坑法)

思路:使用变量tmp记录基准值,在数组中从后往前找小于基准值的元素,当array[right]>=tmp时 ,即right位置上的元素大于等于tmp时,则right--直到找到小于tmp的元素,让right停下来 ,此时right位置上的元素就是比tmp小的元素,然后让此时right位置上的元素直接放到left位置上 ,即array[left]=array[right];如果array[left] <= tmp ,即left位置上的元素小于等于tmp时,则left-- ,直到找到大于tmp的元素,让left停下来,此时left位置上的元素就是比tmp小的元素,然后让此时left位置上的元素直接放到right位置上 ,即array[right] = array[left]。然后继续在满足left<right的情况下,循环遍历上述的操作。当跳出循环时,说明left和right相遇 了,将tmp放在left(right)位置上,然后返回left。之后的递归操作一样。

思路图解:

java 复制代码
private static void partition(int[] array,int left,int right) {
    int tmp = array[left];
    int tmpLeft = left;
    while(left < right) {
        if(left < right && array[right] >= tmp) {
            right--;
        }
        array[left] = array[right];

        if(left < right && array[left] <= tmp) {
            left++;
        }
        array[right] = array[left];
    }
    array[left] = tmp;
    return left;
}
#3.前后指针法快速排序(基本不用)

思路:定义一个prev变量,初始时是记录数组中第一个元素的下标位置left,定义一个变量cur,初始时是数组中第二个元素的下标位置left+1,在cur<=right 的情况下,进入循环,如果cur位置上的元素小于left位置上的元素并且prev的后一个位置的元素不等于cur位置上的元素,交换cur和prev位置上的元素 ,cur++。然后继续在满足cur<=right的情况下,重复上述的操作。当跳出循环时,说明cur走到了空,将此时的prev位置上的元素和left(0)位置上的元素交换然后返回prev。之后的递归操作一样。

思路图解:

java 复制代码
public static void partition(int[] aarray,int left,int right) {
    int prev = left;
    int cur = left+1;
    while(cur <= right) {
        if(array[cur] < array[left] && array[++prev] != array[cur]) {
            swap(array,prev,cur);
        }
        cur++;
    }
    swap(array,prev,left);
    return prev;
}
快速排序的特性
  • 快速排序是一个不稳定的排序。
  • 时间复杂度:最好情况:,每次分区都能将数组均匀分成两半,递归树的高度:log₂n,每层的工作量:O(n),总工作量:n × log₂n = O(nlog₂n)。最坏情况:,数组已经有序或逆序时,每次分区极度不平衡(一个子数组为空),递归树退化为链表,高度:n,每层工作量:n, n-1, n-2, ..., 1,总工作量:n(n+1)/2 = O(n²)。
  • 空间复杂度:最好情况: - 平衡分区时递归深度。最坏情况: - 不平衡分区时递归深度。
#优化快速排序(三数取中+小区间插入排序)

快速排序在最坏情况下,时间复杂度会退化到O(n^2),这通常发生在输入数组已经有序或接近有序的情况下,++此时分区操作可能极不平衡++ ,可能导致递归深度达到O(n)而引起栈溢出 ,因此使用三数取中法对快速排序进行优化,能够提高在快速排序过程中每次递归分区将数组均匀分区的概率,降低了最坏情况发生。

三数取中法使用的位置 :在quick方法中,每次寻找相遇点pivot之前,使用三数取中法寻找left和right下标的中间位置mid,即(left+right)/2,然后对比left、right和mid三个位置上的元素,找出最小的那个元素,然后返回这个最小元素的下标位置(可能是left/right/mid),使用midIndex变量接收这个下标位置,而midIndex下标位置上的元素要将其作为基准值,因此,找到minIndex后,每次让left位置上的元素和midIndex位置上的元素交换位置,保证基准值在前面的位置,接着才开始进行相遇点的寻找。

例如,如果没有三数取中法优化,那么像{1,2,3,4,5,6,7}这样的有序数组,在递归分区是极度不平衡,会有栈溢出的风险:

而如果采用三数取中法进行优化,又能够有效降低分区不平衡的概率:

思路:首先根据left和right求出中间下标位置,在array[left]<array[right] 的情况下,如果++array[mid]<array[left],那么中间位置是left,返回left;如果array[mid]>array[right],那么中间位置是right,返回right;如果上述的情况都不是,那么mid本身就是中间位置,返回mid。++ 在array[left]>array[right] 的情况下,如果a++rray[mid]>array[left],那么中间位置是left,返回left;如果array[mid]<array[right],那么中间位置是right,返回right;如果上述的情况都不是,那么mid本身就是中间位置,返回mid++。最后将left和mid交换位置。

java 复制代码
public static quick(int[] array,int start,int end) {
    if(start >= end) {
        return;
    }
    
    int midIndex = getMiddleNum(array,start,end);
    swap(array,start,midIndex);
    
    int pivot = partition(array,start,end);
    quick(array,start,pivot-1);
    quick(array,pivot+1,end);
}
private static int getMiddleNum(int[] array,int left,int right) {
    int mid = (left+right)/2;
    if(array[left] < array[right]) {
        if(array[mid] < array[left]) {
            return left;
        }else if(array[mid] > array[right]) {
            return right;
        }else {
            return mid;
        }
    }else {
        if(array[mid] > array[left]) {
            return left;
        }else if(array[mid] < array[right]) {
            rturn right;
        }else {
            return mid;
        }
    }
}

此外,快速排序是递归算法,递归调用的深度在最坏情况下可能达到O(n),也容易导致栈溢出 ,而且快速排序作为递归算法,对小的子区间递归的效率较低所以递归到小的子区间时,可以使用直接插入排序,这样就能减少调用递归的次数。(快速排序在递归时,那些小子数组往往已经基本排好序了,而直接插入排序对基本有序的数组排序特别快,因此使用直接插入排序)

java 复制代码
public static quick(int[] array,int start,int end) {
    if(start >= end) {
        return;
    }
    
    if(end-start+1 <= 7) {
        insetSortRange(array,strat,end);
        return;
    }
    
    int midIndex = getMiddleNum(array,start,end);
    swap(array,start,midIndex);
    
    int pivot = partition(array,start,end);
    quick(array,start,pivot-1);
    quick(array,pivot+1,end);
}
private static void insetSortRange(int[] array,int start,int end) {
    for(int i = start+1;i <= end; i++) {
        int tmp = array[i];
        int j = i+1;
        for(;j >= start; j--) {
            if(array[j] < tmp) {
                array[j+1] = array[j];
            }else {
                break;
            }
        }
        array[j+1] = tmp;
    }
}
#快速排序非递归

思路:使用 实现。首先还是要找到数组的相遇点pivot,然后判断pivot的左/右序列是否需要继续排序:如果pivot>start+1,则说明++左序列中的元素大于1++ ,表示此时的左序列还需要进行排序------首先先将左序列的第一个元素的下标先入栈,再将最后一个元素的下标pivot-1入栈;如果pivot>start+1,则说明++此时左序列中只有一个元素(至少要有2个元素才需要继续排序),那么就表示该左序列已经有序了,不执行入栈操作++ 。pivot的右序列和左序列的操作一样,判断pivot是否<end-1 ,如果是,入栈,如果不是,不执行入栈操作。然后在栈不为空的情况下,进入循环,更新此时的start和end的位置(将元素出栈)------------start和end可能是代表的是左序列也可能是右序列,寻找此时的序列的相遇点pivot,然后重复判断pivot的左/右序列是否已经有序,直到栈为空,此时快速排序结束,数组是有序的数组。

java 复制代码
public static void quickSort(int[] array) {
    quickNor(array,0,array.length-1);
}
private static void quickNor(int[] array,int start,int end) {
    Stack<Integer> stack = new Stack<>();

    int pivot = partition(array,strat,end);
    
    if(pivot > start+1) {
        stack.push(start);
        stack.push(pivot-1);
    }
    if(pivot < end-1) {
        stack.push(pivot+1);
        stack.push(end);
    }

    while(start < end) {
        end = stack.pop();
        start = stack.pop();
        
        pivot = partition(array,start,end);
        
        if(pivot > start+1) {
            stack.push(start);
            stack.push(pivot-1);
        }
        if(pivot < end-1) {
            stack.push(pivot+1);
            stack.push(end);
        }
    }
}

(4)归并排序

归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法 。将已有序的子序列合并,得到完全有序的序列;++即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并++。

思路:将待排序数组不断从中间位置分成两个子数组,递归地对每个子数组继续分解,直到每个子数组只有一个元素(或为空)[单个元素的数组是有序的],将两个已排序的子数组合并成一个有序数组:使用双指针法,比较两个子数组的元素,将较小的元素放入临时数组,最后将临时数组中的有序序列复制回原数组对应位置。

核心步骤:

java 复制代码
public static void mergeSort(int[] array) {
    mergeSortTmp(array,0,array.length-1);
}
private static void mergeSortTmp(int[] array,int left,int right) {
    if(left >= right) {
        return;
    }
    int mid = (left+right)/2;
    mergeSortTmp(array,left,mid);//递归遍历左半部分
    mergeSortTmp(array,mid+1,right);//递归遍历右半部分
    //左右两部分都已遍历完成,现在合并它们
    merge(array,left,mid,right);
}
private static void merge(int[] array,int left,int mid,int right) {
    //定义一个数组tmp,变量k记录它的下标,tmp数组用来存放有序的元素,tmp的大小取决于此时的子数组的大小,即right-left+1
    int[] tmp = new int[right-left+1];
    int k = 0;
    //s1,s2记录递归遍历时的mid的左右子数组的起始位置,左子数组的最后一个元素的位置e1=mid,右子数组e2=right
    int s1 = left;
    int s2 = mid+1;
    
    while(s1 <= mid && s2 <= right) {
        //如果左子数组的第一个元素s1小于等于右子数组的第一个元素s2,那么将s1位置上的元素放到tmp数组第一个元素位置上,然后k++,s1++
        if(array[s1] <= array[s2]) {
            tmp[k++] = array[s1++];
        }else {
            //如果左子数组的第一个元素s1大于右子数组的第一个元素s2,那么将s2位置上的元素放到tmp数组第一个元素位置上,然后k++,s2++
            tmp[k++] = array[s2++];
        }
    }
    //走到这里,如果左子数组中还有剩余的元素或者右子数组中还有剩余的元素,则直接放到tmp数组中,然后k++,s1++/s2++,直到数组中没有元素了
    while(s1 <= mid) {
        tmp[k++] = array[s1++];
    }
    while(s2 <= right) {
        tmp[k++] = array[s2++];
    }
    //到这里,tmp数组是有序的,将tmp中的元素复制回原数组array对应的位置上,注意下标位置的准确
    for(int i = 0;i < k; i++) {
        array[i+left] = tmp[i];
    }
}    

#归并排序的特性

  • 归并排序是一个稳定的排序(如果将array[s1] <= array[s2] 改成array[s1] < array[s2],就会变成不稳定的排序)。
  • 时间复杂度:O(n*log₂n) ------------------ 递归树的深度:log₂n 层,每层的工作量:合并操作总共需要 O(n)(每层都需要遍历所有元素),总工作量:层数 × 每层工作量 = log n × n = O(n log n)
  • 空间复杂度:O(n) ------------------ 每次合并都创建新数组

#归并排序非递归实现

思路:首先将每个元素视为一个长度为1的有序子数组,然后通过循环不断将相邻的两个有序子数组合并成更大的有序数组。在每一轮合并中,设定当前子数组的大小gap(初始为1),遍历整个数组,每次取出两个长度为gap的相邻子数组进行合并 。由于数组长度可能不是2的幂次,最后一个子数组可能不完整,因此需要特别处理边界 情况:当计算出的中间位置mid或右边界right超出数组范围时,将其调整为数组的最后一个下标 ,以确保合并操作不会越界。合并过程使用额外的临时数组存储排序结果,再写回原数组(就是merge方法的过程)。重复这一过程,每次将gap翻倍(gap *= 2 ),直到gap大于或等于数组长度,此时整个数组已完全有序。
注意:mid和right的计算方式:(以数组中每个元素视为一个长度为1的有序数组为例)

  • gap 表示当前每个有序子数组的长度(1)
  • 第一个子数组:从 left 开始,长度为 gap,所以终点是 left + gap - 1(0)
  • 第二个子数组:紧接第一个子数组,所以从 mid + 1 开始,长度也是 gap,所以终点是 (mid + 1) + gap - 1 = mid + gap (1)

归并排序采用二分法 的思想。每次合并后,有序子数组的长度翻倍,这正是算法时间复杂度 O(n log n) 的来源。

java 复制代码
public static void mergeSort(int[] array) {
    mergeSortTmp(array,0.array.length-1);
}
public static void mergeSortTmp(int[] array,int left,int right) {
    int gap = 1;
    while(gap < array.length) {
        for(int i = 0;i <= right; i = i+gap*2) { //i+gap*2 - 跳到下一个数组的left位置
            int left = i;
            int mid = left+gap-1;
            //检查边界    
            if(mid > array.length) {
                mid = array.length-1;
            }
            int right = mid+gap;
            //检查边界
            if(right > array.length) {
                right = array,length-1;
            }
            merge(array,left,mid,right);
        }
        gap *= 2;
    }
}

越界示例:

排序算法时间复杂度及稳定性分析

|--------|-------------------------------------------|-------------------------------------------|-------------------------------------------|-----------------------------------------------------------------------------|-----|
| 排序方法 | 最好 | 平均 | 最坏 | 空间复杂度 | 稳定性 |
| 冒泡排序 | | | | | 稳定 |
| 直接插入排序 | | | | | 稳定 |
| 直接选择排序 | | | | | 不稳定 |
| 希尔排序 | | | | | 不稳定 |
| 堆排序 | | | | | 不稳定 |
| 快速排序 | | | | ~ | 不稳定 |
| 归并排序 | | | | | 稳定 |

3.非基于比较的排序

计数排序

思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤

    1. 统计相同元素出现次数
    1. 根据统计的结果将序列回收到原来的序列中

思路:首先,遍历待排序数组,找出其中的最大值和最小值,确定数据的范围。然后创建一个计数数组,其大小为数据范围长度(最大值减最小值加1)。接着,第二次遍历原数组,统计每个元素出现的次数,并将统计结果存入计数数组的对应位置(通过元素值减去最小值映射到计数数组下标)。最后,遍历计数数组,按照计数数组中的统计次数,将元素按顺序重新填回原数组(通过下标加上最小值还原元素值),从而实现排序。

java 复制代码
public static void countSort(int[] array) {
    //初始时最大元素和最小元素都在0下标位置
    int maxVal = array[0];
    int minVal = array[0];
    //寻找数组中最大元素和最小元素的位置
    for(int i = 1;i < array.length; i++) {
        if(array[i] > maxVal) {
            maxVal = array[i];
        }
        if(array[i] < minVal) {
            minVal = array[i];
        }
    }
    //根据最大元素和最小元素的位置来确定计数数组count的大小
    int len = maxVal-minVal+1;
    int[] count = new int[len];
    //遍历原数组array,把array中的每个元素放到count数组中与元素对应的下标位置上进行计数,即array数组中当前元素有几个,那么count对应下标上
    //的元素就++,记录几个。
    for(int i = 0;i < array.length; i++) {
        int index = array[i]; 
        //如果array数组中的元素是2,3,5,4,1,1,那么就让count对应数字的下标index位置上++,但如果array中的元素是97,90,75,98,
        //这样的分散的数据,我们不可能将97放到count对应下标index=97位置上进行++,那样就要创建一个非常大的count数组,因此,可以
        //使用index-minVal这个方法,将97放到该下标位置上++
        count[index-minVal]++;
    }
    int index = 0;
    for(int i = 0;i < count.length; i++) {
        while(count[i] != 0) {
            array[index] = i+minVal;//最后记得加上minVal,将97还回去
            index++;
            count[i]--;
        }
    }
}

#计数排序的特性

  • 计数排序是一个稳定的排序。
  • 时间复杂度:O(n+k) ------ 范围越大,越慢 ,n为元素个数,k为数据范围。
  • 空间复杂度:O(n)

桶排序和基数排序了解即可。

相关推荐
人得思变~谁会嫌自己帅呢?1 小时前
希尔排序算法
数据结构·算法·排序算法
福尔摩斯张1 小时前
C语言文件操作详解(一):文件的打开与关闭(详细)
java·linux·运维·服务器·c语言·数据结构·算法
white-persist1 小时前
【攻防世界】reverse | answer_to_everything 详细题解 WP
c语言·开发语言·汇编·python·算法·网络安全·everything
Ynchen. ~1 小时前
[工程实战] 攻克“数据孤岛”:基于隐语纵向联邦学习的金融风控建模全解析
算法·金融·逻辑回归·隐语
程序员-King.1 小时前
day107—同向双指针—无重复字符的最长字串(LeetCode-3)
算法·leetcode·双指针
风掣长空1 小时前
【LeetCode】面试经典150题:合并两个有序数组
算法·leetcode·面试
im_AMBER1 小时前
Leetcode 69 正整数和负整数的最大计数
数据结构·笔记·学习·算法·leetcode
fufu03111 小时前
Linux环境下的C语言编程(三十六)
linux·c语言·开发语言·数据结构·算法
踢球的打工仔1 小时前
前端html(1)
前端·算法·html