排序算法详解

参考链接:https://blog.csdn.net/luo1454925298/article/details/105560466

排序算法性能比较:

image-20220111174402413

时间复杂度计算

递推公式法

归并排序的递推公式是:

merge_sort(p…r) = merge(merge_sort(p…q), merge_sort(q+1…r))
终止条件:
p >= r 不用再继续分解

我们假设对n个元素排序的时间是T(n),那分解成两个子数组排序的时间是T ( n/2 ) 。merge函数合并两个子数组的时间复杂度是O(n)。所以归并排序时间复杂度计算公式就是:

T(n) = 2 * T(n/2) + n ,n > 2

T(1) = c

继续计算T(n)

T(n)=2*T(n/2)+n
     =2*(2*T(n/4)+n/2)+n=4*T(n/4)+2n
     =4*(2*T(n/8)+n/4)+2n=8*T(n/8)+3n
     =8*(2*T(n/16)+n/8)+3n=16*T(n/16)+4n
     ...
     =2^k*T(n/2^k)+k*n

image-20220224220124746的时候,image-20220224220141342代入上边的式子:image-20220224220228289用大O表示法,image-20220224220251733

递归树法

递归的思想就是将大问题分解为小问题来求解。然后再将小问题分解成小小问题。这样一层层分解直到问题不能再分解。如果我们把这一层层的分解过程画成图,其实就是一棵树。我们把它叫做递归树。
参看下图,括号中的数字表示问题的规模。

在这里插入图片描述

归并排序比较耗时的操作是合并,也就是将两个小数组合并成一个大数组。其他操作的代价很低,可以记为常数L。
从图中看出每一层的耗时是相同的,都为n。现在我们只要知道这棵树的高度h,就可以得到总的时间复杂度:O(h*n)。

从图中能看到这是一颗满二叉树。满二叉树的高度大约是image-20220224220450050所以归并排序的时间复杂度就是image-20220224220504863

一,冒泡排序

时间复杂度:O(n的平方),稳定

基本思想:

通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。

在一轮的循环之后,可以让未排序的最大元素上浮到右侧。在一轮循环中,如果没有发生交换,那么说明数组已经是有序的,此时可以直接退出。

步骤如下:

  • 将序列当中的左右元素,依次比较,保证右边的元素始终大于左边的元素;
    ( 第一轮结束后,序列最后一个元素一定是当前序列的最大值;)

  • 对序列当中剩下的n-1个元素再次执行步骤1。

  • 对于长度为n的序列,一共需要执行n-1轮比较

i从0开始增加,不断的把arr.length-1-i之前的最大值滚动过来,放置到arr.length-1-i位置上去,保证arr.length-1-i位置及之后的都是顺序的

代码如下:

public void bubbleSort(){
    int[] arr = {3, 9, -1, 10, 20};
    // 临时变量
    int temp = 0;
    // 标识变量,表示是否进行过交换,如果在一次遍历过后flag还是false,说明整个遍历没有遇到逆序的情况,就不用再进行后边的操作了
    boolean flag = false;
    for (int i = arr.length - 1; i > 0; i--) {
        //固定i个
        for (int j = 0; j < i; j++) {
            // 如果前面的数比后面的数大,则交换
            if (arr[j] > arr[j + 1]) {
                flag = true;
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
        // 在一趟排序中,一次交换都没有发生过
        if (!flag) {
            break;
        } else {
            // 重置flag!!!, 进行下次判断
            flag = false;
        }

    }
    System.out.println(Arrays.toString(arr));
}

二,选择排序

时间复杂度:O(n的平方),不稳定

它的运行时间与输入无关,这个特点使得它对一个已经排序的数组也需要这么多的比较和交换操作。

基本思想:

选择排序(select sorting)也是一种简单的排序方法。它的基本思想是:第一次从arr[0]arr[n-1]中选取最小值,与arr[0]交换,第二次从arr[1]arr[n-1]中选取最小值,与arr[1]交换,第三次从arr[2]arr[n-1]中选取最小值,与arr[2]交换,…,第i次从arr[i-1]arr[n-1]中选取最小值,与arr[i-1]交换,…, 第n-1次从arr[n-2]~arr[n-1]中选取最小值,与arr[n-2]交换,总共通过n-1次,得到一个按排序码从小到大排列的有序序列。

代码如下:

public void selectSort() {
    int[] arr = {3, 9, -1, 10, 20};
    for (int i = 0;i < arr.length;i++) {
        int minVal = arr[i];
        int minIndex = i;
        // 从i之后的位置找最小值然后和arr[i]交换
        for (int j = i + 1;j < arr.length;j++) {
            if (minVal > arr[j]) {
                minVal = arr[j];
                minIndex = j;
            }
        }
        if (minIndex != i) {
            // 最小值和arr[i]交换
            arr[minIndex] = arr[i];
            arr[i] = minVal;
        }
    }

    System.out.println(Arrays.toString(arr));
}

三,插入排序

时间复杂度:O(n的平方),稳定

插入排序的时间复杂度取决于数组的初始顺序,如果数组已经部分有序了,那么逆序较少,需要的交换次数也就较少,时间复杂度较低。

基本思想

插入排序(Insertion Sorting)的基本思想是:把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。
image-20220109173406011

代码如下:

public void insertSort() {
    int[] arr = {3, 9, -1, 10, 20};
    for (int i = 1;i < arr.length;i++) {
        // 待插入值:arr[i]
        int insertVal = arr[i];
        // 指向arr[i]的前面这个数的下标
        int insertIndex = i - 1;
        // 从i-1往前遍历,遇到比insertVal大的值,就后移,保证i之前都是顺序的
        while (insertIndex >= 0 && arr[insertIndex] > insertVal) {
            // 未到达指定位置时,(前方)比它(insertVal)大的值向后移,而新添加的最后一个数已给insertVal。
            arr[insertIndex + 1] = arr[insertIndex];
            insertIndex--;
        }
        // 最后找到适合insertVal插入的位置插入
        arr[insertIndex + 1] = insertVal;
    }
    System.out.println(Arrays.toString(arr));
}

四,希尔排序

时间复杂度:O(n log(n) )书上说的是希尔排序的分析是一个复杂的问题,以为它的时间是所取“增量”序列的函数,这涉及到一些数学上尚未解决的难题,N^1.3是一个比较快的实现,不稳定

shell排序的时间复杂度是依赖于 argument sequence 的,所以你用不同的序列,时间复杂度不同
N^1.3是一个比较快的实现

基本思想

希尔排序使用插入排序对间隔 h 的序列进行排序。通过不断减小 h,最后令 h=1,就可以使得整个数组是有序的。希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

image-20220109195244074

在gap不断除以2的过程中,每一步都是i从gap开始往后递增,然后将i之前的通过插入排序以gap间隔的数组保证顺序。

代码如下:

public void shellSort() {
    int[] arr = {8,9,1,7,2,3,5,4,6,0};
    // 共gap组
    for (int gap = arr.length / 2;gap > 0;gap /= 2) {
        // 在gap不断除以2的过程中,每一步都是i从gap开始往后递增,然后将i之前的通过**插入排序**以gap间隔的数组保证顺序。
        for (int i = gap;i < arr.length;i++) {
            int insertVal = arr[i];
            int insertIndex = i - gap;
            // 这里是以gap为单位递减的,因为是通过gap来分组的
            while (insertIndex >= 0 && arr[insertIndex] > insertVal) {
                arr[insertIndex + gap] = arr[insertIndex];
                insertIndex -= gap;
            }
            arr[insertIndex + gap] = insertVal;
        }
    }

    System.out.println(Arrays.toString(arr));
}

五、归并排序

时间复杂度:O(n log(n) ),稳定

基本思想

归并排序的思想是将数组分成两部分,分别进行排序,然后归并起来。归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题(divide)成一些小的问题然后递归求解,而(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。

image-20220110102840085

可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程。

合并相邻有序子序列:

再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤

image-20220110103132342

image-20220110103146320

mergeSort方法用于递归将数组分成两半进行递归调用mergeSort方法,然后将两边排好序的数组调用merge方法进行合并

merge方法用于将两边排好序的数组进行合并,比较两边数组的数据哪个小,小的数值暂存到temp数组中,最后将temp数组的数值更新到arr对应的范围区间内

代码如下:

public class MergeSort {
    
    /**
     * @description: 分+合方法
     * @param: arr
     * @param: left
     * @param: right
     * @param: temp:一个用于暂存数据的临时数组用于数据比较交换暂时存放
     * @return: void
     * @author: RealGang
     * @date: 2022/1/10
     */
    public  void mergeSort(int[] arr,int left,int  right,int[] temp) {
        // 分解到只有一个
        if (left < right) {
            int mid = (left + right)/2;
            // 分成两半,向左递归进行分解
            mergeSort(arr,left,mid,temp);
            // 分成两半,向右递归进行分解
            mergeSort(arr,mid+1,right,temp);
            merge(arr,left,mid,right,temp);
        }
    }

    /**
     * @description: 通过比较左半部分和右半部分的数据哪个小,小的放入到temp数组中,然后相应指针后移
     * @param: arr
     * @param: left
     * @param: mid
     * @param: right
     * @param: temp:一个用于暂存数据的临时数组用于数据比较交换暂时存放
     * @return: void
     * @author: RealGang
     * @date: 2022/1/10
     */
    public void merge(int[] arr,int left,int mid,int right,int[] temp) {
        int i = left;
        int j = mid + 1;
        // 指向temp数组的当前索引
        int index = 0;
        // 直到左右两边有一边处理完毕为止
        while (i <= mid && j <= right) {
            // 如果左边的数据小,放入到temp数组中,并且相应指针后移
            if (arr[i] < arr[j]) {
                temp[index] = arr[i];
                index++;
                i++;
            } else {
                // 如果右边的数据小,放入到temp数组中,并且相应指针后移
                temp[index] = arr[j];
                index++;
                j++;
            }
        }
        // 把有剩余数据的一边的数据依次全部填充到temp
        while (i <= mid) {
            temp[index] = arr[i];
            index++;
            i++;
        }
        while (j <= right) {
            temp[index] = arr[j];
            index++;
            j++;
        }
        //将temp数组的刚才暂存的元素拷贝到arr,注意:并不是每次都拷贝所有
        index = 0;
        int tempLeft = left;
        while (tempLeft <= right) {
            arr[tempLeft] = temp[index];
            index++;
            tempLeft++;
        }
    }

    public static void main(String[] args) {
        int[] arr = {8, 4, 5, 7, 1, 3, 6, 2};
        int[] temp = new int[arr.length];
        MergeSort mergeSort = new MergeSort();
        mergeSort.mergeSort(arr,0,arr.length - 1,temp);
        System.out.println(Arrays.toString(arr));
    }
}

六、快速排序

时间复杂度:O(n log(n) ),不稳定

基本思想

排序算法的思想非常简单,在待排序的数列中,我们首先要找一个数字作为基准数(这只是个专用名词)。为了方便,我们一般选择第 1 个数字作为基准数(其实选择第几个并没有关系)。接下来我们需要把这个待排序的数列中小于基准数的元素移动到待排序的数列的左边,把大于基准数的元素移动到待排序的数列的右边。这时,左右两个分区的元素就相对有序了;接着把两个分区的元素分别按照上面两种方法继续对每个分区找出基准数,然后移动,直到各个分区只有一个数时为止。

image-20220110111410685

这是典型的分治思想,即分治法。下面我们对一个实际例子进行算法描述,讲解快速排序的排序步骤。

以 47、29、71、99、78、19、24、47 的待排序的数列为例进行排序,为了方便区分两个 47,我们对后面的 47 增加一个下画线,即待排序的数列为 47、29、71、99、78、19、24、47。

首先我们需要在数列中选择一个基准数,我们一般会选择中间的一个数或者头尾的数,这里直接选择第 1 个数 47 作为基准数,接着把比 47 小的数字移动到左边,把比 47 大的数字移动到右边,对于相等的数字不做移动。所以实际上我们需要找到中间的某个位置 k,这样 k 左边的值全部比 k 上的值小,k 右边的值全部比 k 上的值大。

接下来开始移动元素。怎么移动呢?其实冒泡排序也涉及对元素的移动,但是那样移动起来很累,比如把最后一个元素移动到第 1 个,就需要比较 n-1 次,同时交换 n-1 次,效率很低。其实,只需把第 1 个元素和最后一个元素交换就好了,这种思想是不是在排序时可以借鉴呢?之前说快速排序就是对冒泡排序的一个改进,就是这个原因。

快速排序的操作是这样的:首先从数列的右边开始往左边找,我们设这个下标为 i,也就是进行减减操作(i--),找到第 1 个比基准数小的值,让它与基准值交换;接着从左边开始往右边找,设这个下标为 j,然后执行加加操作(j++),找到第 1 个比基准数大的值,让它与基准值交换;然后继续寻找,直到 i 与 j 相遇时结束,最后基准值所在的位置即 k 的位置,也就是说 k 左边的值均比 k 上的值小,而 k 右边的值都比 k 上的值大。

所以对于上面的数列 47、29、71、99、78、19、24、47,进行第 1 趟第 1 个交换的排序情况如下,第 1 次的操作情况如图 1 所示。

image-20220111170712206

交换之后,j 移动到了下标为 6 的位置,对 i 继续扫描,如图 2 所示。

image-20220111170737976

此时交换后的数列变为 24、29、47、99、78、19、71、47。接下来我们继续对 i、j 进行操作,如图 3 所示,继续进行 i-- 及 j++ 的比较操作。

image-20220111170753456

进行了这两次 i、j 的移动、比较、交换之后,我们最终得到的数列是 24、29、19、47、78、99、71、47。接下来我们继续进行 i-- 的操作,发现在 i 为 4 时比 47 大不用交换,在 i 为 3 时与 j 相遇,这时就不需要继续移动、比较了,已经找到 k 了,并且 k 的值为 3。我们可以确认一下当前的数列是不是 k 左边的值都比 47 小,而 k 右边的值都比 47 大(由于要保持相对位置不变,所以 47 同样在基准值 47 的右边)。

47 这个值已经落到了它该在的位置,第 1 趟排序完成了。接下来就是以 k 为基准,分为两部分,然后在左右两部分分别执行上述排序操作,最后数据会分为 4 部分;接着对每部分进行操作,直到每部分都只有一个值为止。

接下来进行第 2 趟排序,现在左边部分为 24、29、19,我们选择第 1 个数 24 作为基准数,接着进行 i--、j++ 的操作,我们发现 i 最初的值为 19,比 24 这个基准值小,所以与基准值进行交换,得到的数列为 19、29、24;当 j 为 1 时,我们发现 29 比 24 大,所以与基准值进行交换,得到的数列 19、24、29,此时 i 为 2,j 为 1;继续 i-- 时发现 i 为 1,与 j 相遇,左边部分的数列的 k 为 1,并且左右两部分分别只有一个元素,此时第 2 轮排序的左边部分的排序结束,同时左边部分的所有数据都排序完成。

我们接着看右边部分的排序,待排序的数列为 78、99、71、47,我们同样选择第 1 个值 78 为基准值,接下来进行 i 与 j 的移动与比较,发现 47 比 78 小,进行交换,得到的数列 47、99、71、78;从左往右发现 99 比基准值 78 大,进行交换,得到的数列为 47、78、71、99;继续从右向左看,发现 71 比基准值 78 小,进行交换,得到的数列为 47、71、78、99。此时 i 在整体数组中的下标为 6,j 为 5,若继续 j++ 则与 i 相遇,所以完成此轮排序。

此时右边数列的 k 为 6,一般会是相遇的位置,也就是基准值所在的位置,这时数列又被分为两部分,左边是 47、71,右边是 99,需要继续对左边部分的数据进行排序,虽然只有两个数据,但我们还是继续按照快速排序的思想操作一下,选择 47 作为基准数,将i进行从右向左的移动、比较,发现 i 与 j 相等时没有产生移动,完成第 2 轮排序。

至此,所有排序都已经完成,最终数列的结果是 19、24、29、47、47、71、78、99。

代码如下:

public class QuickSort {
    public void quickSort(int[] arr,int left,int right) {
        if (left < right) {
            // 以第一个数值作为基准key
            int key = arr[left];
            // i指向左侧,j指向右侧,这里i指向的必须满足小于key,否则就就赋值给arr[j],同理j相反
            int i = left;
            int j = right;
            // 最终i==j的时候,arr[i]左边的数值都要小于key,右边的数值都要大于key
            while (i < j) {
                // 先移动j指针,找到一个小于key的数值
                while (i < j && arr[j] > key) {
                    j--;
                }
                // 如果找到一个小于key的数值,该值应该放到左边,所以赋值给arr[i],并且i++;这里不用i,j位置数值互换,因为在最后会把arr[i]的位置的数值替换为key
                if (i < j) {
                    arr[i] = arr[j];
                    i++;
                }
                // 再移动i指针,找到一个大于key的数值
                while (i < j && arr[i] < key) {
                    i++;
                }
                // 如果找到一个大于key的数值,该值应该放到右边,所以赋值给arr[j],并且j--;这里不用i,j位置数值互换,因为在最后会把arr[i]的位置的数值替换为key
                if (i < j) {
                    arr[j] = arr[i];
                    j--;
                }
            }
            // 把arr[i]的位置的数值替换为key,此时arr[i]左边的数值都小于key,arr[i]右边的数值都大于key
            arr[i] = key;
            // 对arr[i]左边的数组和右边的数组分别递归进行上述操作,直到递归的区间只有一个数值为止
            quickSort(arr,left,i - 1);
            quickSort(arr,i + 1,right);
        }
    }

    public static void main(String[] args) {
        int[] arr = {47,29,71,99,78,19,24,47};
        QuickSort quickSort = new QuickSort();
        quickSort.quickSort(arr,0,arr.length - 1);
        System.out.println(Arrays.toString(arr));
    }
}

七、基数排序(桶排序)

时间复杂度:O(d(r + n)),稳定,r代表关键字的基数,n代表关键字的个数

基本思想

  1. 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
  2. 这样说明,比较难理解,下面我们看一个图文解释,理解基数排序的步骤

image-20220111180814935

image-20220111180854858

image-20220111180918143

代码如下:

public class RadixSort {

    public void radixSort(int[] arr) {
        int maxVal = arr[0];
        for (int i = 0;i < arr.length;i++) {
            if (maxVal < arr[i]) {
                maxVal = arr[i];
            }
        }
        // 最大数的位数
        int maxLength = String.valueOf(maxVal).length();
        // 桶,第i行代表某一位上是i的数值的列表
        int[][] bucket = new int[10][arr.length];
        // 记录arr数组在某一位上是0-9的数值的个数,bucketElementCounts[i]代表某一位数是i的上边的数值有多少个,也就是记录的bucket[i]的数组实际数值的个数,方便遍历bucket[i]
        int[] bucketElementCounts = new int[10];

        // 这里从个位一直遍历到最大数的位数,n 记录个十百千
        for (int i = 0,n = 1;i < maxLength;i++,n *= 10) {
            // 遍历arr数组,获取arr[j]在某一位上的数值a,然后放到bucket[a]的里边,但是要知道bucket[a]里边放了几个元素了,所以通过bucketElementCounts[digitOfElement]记录
            for (int j = 0;j < arr.length;j++) {
                int digitOfElement = (arr[j] / n) % 10;
                bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];

                bucketElementCounts[digitOfElement]++;
            }

            int index = 0;
            // 遍历完一趟arr之后,要对arr根据某一位数的数值大小进行一次重排序,从0到9开始,找某一位数是0的有多少个,然后再遍历bucket[0]上的数组,这种遍历赋值顺序保证了arr是按照这一位数大小排序
            for (int k = 0;k < bucketElementCounts.length;k++) {
                if (bucketElementCounts[k] != 0) {
                    // bucketElementCounts[k]记录的是bucket[k]的实际数值的个数,然后遍历bucket[k]去赋值给arr
                    for (int t = 0;t < bucketElementCounts[k];t++) {
                        arr[index] = bucket[k][t];
                        index++;
                    }
                }
                bucketElementCounts[k] = 0;
            }

        }
        System.out.println(Arrays.toString(arr));
    }

    public static void main(String[] args) {
        int[] arr = {9847,429,781,99,78,19,24,47};
        RadixSort radixSort = new RadixSort();
        radixSort.radixSort(arr);
    }
}

八,堆排序(树结构实际应用)

时间复杂度:O(n log(n) ),不稳定

基本思想

利用大顶堆(小顶堆)堆顶记录的是最大关键字(最小关键字)这一特性,使得每次从无序中选择最大记录(最小记录)变得简单。

① 将待排序的序列构造成一个最大堆,此时序列的最大值为根节点
② 依次将根节点与待排序序列的最后一个元素交换
③ 再维护从根节点到该元素的前一个节点为最大堆,如此往复,最终得到一个递增序列

我的理解:大顶堆的根节点一定是最大值,每次把数组调整成为大顶堆,然后将根节点(arr[0])与无序区间的最后一位交换,此时无序区间长度减一,有序区间长度加一,然后再把无序区间调整为大顶堆,直到最后无序区间长度为0

步骤:

实现堆排序需要解决两个问题:

1、如何由一个无序序列建成一个堆?
2、如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?

假设给定一个组无序数列{100,5,3,11,6,8,7},带着问题,我们对其进行堆排序操作进行分步操作说明。

image-20220111204009673

  1. 创建最大堆

    ①首先我们将数组我们将数组从上至下按顺序排列,转换成二叉树:一个无序堆。每一个三角关系都是一个堆,上面是父节点,下面两个分叉是子节点,两个子节点俗称左孩子、右孩子;

    image-20220111204112914

    ②转换成无序堆之后,我们要努力让这个无序堆变成最大堆(或是最小堆),即每个堆里都实现父节点的值都大于任何一个子节点的值。

    image-20220111204130579

    ③从最后一个堆开始,即左下角那个没有右孩子的那个堆开始;首先对比左右孩子,由于这个堆没有右孩子,所以只能用左孩子,左孩子的值比父节点的值小所以不需要交换。如果发生交换,要检测子节点是否为其他堆的父节点,如果是,递归进行同样的操作。

    ④第二次对比红色三角形内的堆,取较大的子节点,右孩子8胜出,和父节点比较,右孩子8大于父节点3,升级做父节点,与3交换位置,3的位置没有子节点,这个堆建成最大堆。

    image-20220111204229803

    ⑤对黄色三角形内堆进行排序,过程和上面一样,最终是右孩子33升为父节点,被交换的右孩子下面也没有子节点,所以直接结束对比。

    ⑥最顶部绿色的堆,堆顶100比左右孩子都大,所以不用交换,至此最大堆创建完成。

    image-20220111204251199

  2. 堆排序(最大堆调整)

    ①首先将堆顶元素100交换至最底部7的位置,7升至堆顶,100所在的底部位置即为有序区,有序区不参与之后的任何对比。

    image-20220111204415037

    ②在7升至顶部之后,对顶部重新做最大堆调整,左孩子33代替7的位置。

    image-20220111204440372

    ③在7被交换下来后,下面还有子节点,所以需要继续与子节点对比,左孩子11比7大,所以11与7交换位置,交换位置后7下面为有序区,不参与对比,所以本轮结束,无序区再次形成一个最大堆。

    image-20220111204546764

    ④将最大堆堆顶33交换至堆末尾,扩大有序区;

    image-20220111204606435

    ⑤不断建立最大堆,并且扩大有序区,最终全部有序。

    image-20220111204620652

代码如下:

public class HeapSort {

    public void sort(int[] arr) {
        /*
         *  第一步:将数组堆化
         *  beginIndex = 第一个非叶子节点((len - 1)/2),然后递减。
         *  从第一个非叶子节点开始即可。无需从最后一个叶子节点开始。
         *  叶子节点可以看作已符合堆要求的节点,根节点就是它自己且自己以下值为最大。
         */
        int len = arr.length;
        int beginIndex = (len - 1)/2;
        // 从第一个非叶子结点开始递减,把以该节点为父节点的下边的子树转化为大顶堆
        for (int i = beginIndex;i >= 0;i--) {
            maxHeapify(arr,i,len - 1);
        }

        /*
         * 第二步:对堆化数据排序
         * 经过上述步骤之后,整个数组已经是一个大顶堆了,就可以通过根节点找到最大值交换到末尾
         * 每次都是移出最顶层的根节点A[0],与最尾部节点位置调换,同时遍历长度 - 1,此时后边的有序区间长度加一,前边的无序区间长度减一。
         * 然后重新整理被换到根节点的末尾元素,使其符合堆的特性。
         * 直至未排序的堆长度为 0。
         */
        for (int i = len - 1;i > 0;i--) {
            // 将arr[0]与i上的位置数值交换之后,i上边的数值就变成了了无序区最大的值,然后i-1,无序数组的长度也减一,继续把无序数组转化为大顶堆
            swap(arr,0,i);
            maxHeapify(arr,0,i - 1);
        }
    }

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

    /**
     * 调整索引为 index 处的数据,使其符合大顶堆的特性。
     *
     * @param index 需要堆化处理的数据的索引
     * @param lastIndex 未排序的堆(数组)的末尾索引
     */
    private void maxHeapify(int[] arr,int index,int lastIndex) {
        // 左子节点索引
        int left = index * 2 + 1;
        // 右子节点索引
        int right = left + 1;
        // 子节点值最大索引,默认左子节点。
        int maxIndex = left;
        // 左子节点索引超出计算范围,直接返回。
        if (left > lastIndex) {
            return;
        }
        // 先判断左右子节点,哪个较大。
        if (right <= lastIndex && arr[right] > arr[left]) {
            maxIndex = right;
        }
        if (arr[maxIndex] > arr[index]) {
            // 如果父节点被子节点调换,则需要继续判断 换下后的父节点是否符合堆的特性,将调换后的子节点及其下的子树调整为大顶堆
            swap(arr,index,maxIndex);
            maxHeapify(arr,maxIndex,lastIndex);
        }
    }

    public static void main(String[] args) {
        int[] arr = {47,29,71,99,78,19,24,47};
        HeapSort heapSort = new HeapSort();
        heapSort.sort(arr);
        System.out.println(Arrays.toString(arr));
    }
}

复杂度分析

  • 平均时间复杂度:O(nlogn)
  • 最佳时间复杂度:O(nlogn)
  • 最差时间复杂度:O(nlogn)
  • 稳定性:不稳定

堆排序其实也是一种选择排序,是一种树形选择排序。只不过直接选择排序中,为了从R[1…n]中选择最大记录,需比较n-1次,然后从R[1…n-2]中选择最大记录需比较n-2次。事实上这n-2次比较中有很多已经在前面的n-1次比较中已经做过,而树形选择排序恰好利用树形的特点保存了部分前面的比较结果,因此可以减少比较次数。对于n个关键字序列,最坏情况下每个节点需比较log2(n)次,因此其最坏情况下时间复杂度为nlogn。堆排序为不稳定排序,不适合记录较少的排序。

九、计数排序

https://zhuanlan.zhihu.com/p/137576551

十、桶排序

https://zhuanlan.zhihu.com/p/125737294

外排序

外部排序

有时,待排序的文件很大,计算机内存不能容纳整个文件,这时候对文件就不能使用内部排序了(这里做一下说明,其实所有的排序都是在内存中做的,这里说的内部排序是指待排序的内容在内存中就可以完成,而外部排序是指待排序的内容不能在内存中一下子完成,它需要做内外存的内容交换),外部排序常采用的排序方法也是归并排序,这种归并方法由两个不同的阶段组成:

1、采用适当的内部排序方法对输入文件的每个片段进行排序,将排好序的片段(成为归并段)写到外部存储器中(通常由一个可用的磁盘作为临时缓冲区),这样临时缓冲区中的每个归并段的内容是有序的。

2、利用归并算法,归并第一阶段生成的归并段,直到只剩下一个归并段为止。

例如要对外存中4500个记录进行归并,而内存大小只能容纳750个记录,在第一阶段,我们可以每次读取750个记录进行排序,这样可以分六次读取,进行排序,可以得到六个有序的归并段,如下图:

img

每个归并段的大小是750个记录,记住,这些归并段已经全部写到临时缓冲区(由一个可用的磁盘充当)内了,这是第一步的排序结果。

完成第二步该怎么做呢?这时候归并算法就有用处了,算法描述如下:

1、将内存空间划分为三份每份大小250个记录,其中两个用作输入缓冲区,另外一个用作输出缓冲区。首先对Segment_1和Segment_2进行归并,先从每个归并段中读取250个记录到输入缓冲区,对其归并,归并结果放到输出缓冲区,当输出缓冲区满后,将其写到临时缓冲区内,如果某个输入缓冲区空了,则从相应的归并段中再读取250个记录进行继续归并,反复以上步骤,直至Segment_1和Segment_2全都排好序,形成一个大小为1500的记录,然后对Segment_3和Segment_4、Segment_5和Segment_6进行同样的操作。

2、对归并好的大小为1500的记录进行如同步骤1一样的操作,进行继续排序,直至最后形成大小为4500的归并段,至此,排序结束。

可以用一个图示表示上述算法的归并效果:

img

以上对外部排序如何使用归并算法进行排序进行了简要总结,提高外部排序需要考虑以下问题:

1、如何减少排序所需的归并趟数

2、如果高效利用程序缓冲区,使得输入、输出和CPU运行尽可能地重叠。

3、如何生成初始归并段(Segment)和如何对归并段进行归并。

posted @ 2022-01-11 21:25  RealGang  阅读(188)  评论(0编辑  收藏  举报