算法 堆排序(递归和非递归方式讲解)

算法 堆排序

@author:Jingdai
@date:2020.10.29

刷 LeetCode 215 题,用到堆排序,现总结一下堆排序的知识。

基础知识

  • 完全二叉树

    在说堆之前,必须先要说一下完全二叉树。注意和满二叉树区别,满二叉树每一层都必须是满的,而完全二叉树最后一层可以是不满的(其它层也必须是满的),同时最后一层的叶子节点必须都在左边,从左往右排。如下图是完全二叉树和非完全二叉树的示例。

在这里插入图片描述

  • 堆是一种特殊的完全二叉树,分为大根堆和小根堆。对于大根堆,除根节点外每个节点的值小于等于其父节点的值;小根堆正好相反。

思路

了解堆之后,看一下如何使用堆来排序,一般排序使用大根堆。

先看几个性质,当用一个数组 array 来表示一个完全二叉树时,树的根节点为 array[0],对于一个数组下标为 i 的节点,它的父节点数组下标为 (i-1)/2 ,它的左孩子数组下标为 2*i+1,它的右孩子数组下标为 2*i+2

而堆排序可以分为 2 步,构建初始大根堆和维护大根堆。

首先我们看如何维护大根堆。对于某一个节点 i当它的左子树为一个大根堆且右子树为一个大根堆时,对该节点进行建堆的过程就称为维护大根堆。如下图,对下标为 1 的的节点(棕色)进行维护大根堆(它的左子树和右子树都是大根堆),首先找出左子树根、右子树根和自己的最大值,然后和自己的值进行交换。

在这里插入图片描述

但是如图发现下标为3的节点又变得不是大根堆了,需要递归第3节点进行维护大根堆。

在这里插入图片描述

这里下标为4的节点不用变,因为它没有变,所以它还是保持自己大根堆的性质不会变。这里只要注意一点就行,就是对某个节点维护大根堆时,该节点的左右子树必须是大根堆。看下面的维护堆的代码片段。

public static void heapify(int[] array, int index, int lastElementIndex){
     
     
       
   int leftChild = 2 * index + 1;
   int rightChild = 2 * index + 2;

   int maxValueIndex = index;
   if (leftChild <= lastElementIndex && array[leftChild] > array[maxValueIndex]) {
     
     
       maxValueIndex = leftChild;
   }
   if (rightChild <= lastElementIndex && array[rightChild] > array[maxValueIndex]) {
     
     
       maxValueIndex = rightChild;
   }

   if (maxValueIndex != index) {
     
     
       // swap
       int temp = array[index];
       array[index] = array[maxValueIndex];
       array[maxValueIndex] = temp;

       // recurse
       heapify(array, maxValueIndex, lastElementIndex);
   }
   // else // is already a heap, just return
}

这里的 heapify 代码是可以优化的,现在代码中需要递归的调用,但是我们都知道非递归的方式优于递归的方式,所以这里可以直接在交换后将当前节点换成 maxValueIndex ,然后继续之前同样的操作,直到当前节点下标和最大节点下标相等,代表已经维护好最大堆,则退出循环,就可以把递归改成循环,看下面优化后的代码。

public static void heapify(int[] array, int index, int lastElementIndex){
     
     

    int leftChild;
    int rightChild;
    int maxValueIndex;

    while (true) {
     
     
        leftChild = 2 * index + 1;
        rightChild = 2 * index + 2;

        maxValueIndex = index;
        if (leftChild <= lastElementIndex 
            && array[leftChild] > array[maxValueIndex]) {
     
     
            maxValueIndex = leftChild;
        }
        if (rightChild <= lastElementIndex 
            && array[rightChild] > array[maxValueIndex]) {
     
     
            maxValueIndex = rightChild;
        }

        if (maxValueIndex != index) {
     
     
            // swap
            int temp = array[index];
            array[index] = array[maxValueIndex];
            array[maxValueIndex] = temp;

            index = maxValueIndex;
        } else {
     
     
            break;
        }
    }
}

然后我们看构建初始大根堆,如果不仔细思考可能会想到直接从最后一个节点开始到根节点依次维护大根堆,这样当遍历每个节点时它的左右子树已经维护过了,一定是大根堆,可以满足第一步维护大根堆的条件。这样做没有错,但是可以优化一下,因为你会发现从后往前遍历维护大根堆时,后面开始的节点都是叶子节点,叶子节点一定是大根堆,不用维护,所以可以直接从最后一个非叶子节点开始,这样就简化了很多。

那问题又来了,最后一个非叶子节点的下标是多少呢?这里有一个定理,对于用数组表示的完全二叉树,最后一个非叶子节点的下标为 (lastIndex-1)/2其实最后一个叶子的父节点不就是最后一个非叶子节点嘛,所以我们可以直接从这个节点开始维护大根堆,就构建了一个初始大根堆,看下面构建大根堆的代码。

public static void buildHeap(int[] array) {
     
     
   int lastElementIndex = array.length - 1;
   for (int i = (lastElementIndex - 1) / 2; i >= 0; i--) {
     
     
       heapify(array, i, lastElementIndex);
   }
}

之后就是堆排序了,第一次构建完大根堆后,大根堆最大值是树根,就可以将根节点和最后一个节点交换位置,代表已经找到了最大的数。那下一步呢?对前 n-1 个数再重新构建大根堆找第二小的数吗?当然不是,这样做的话复杂度就变成 O(n^2) 了,这么麻烦还是 O(n^2),岂不是没人用了。当交换完根节点后,其实你会发现根节点的左子树和右子树还是大根堆,所以可以直接对根节点维护大根堆就行了,而不用重新建堆,维护完之后再与倒数第二个数进行交换,然后再维护,再交换…一直到最后排序完。

看下面的排序的代码片段。

public static void heapSort(int[] array) {
     
     
   buildHeap(array);

   for (int i = array.length - 1; i >= 1; i--) {
     
     
       // swap
       int temp = array[i];
       array[i] = array[0];
       array[0] = temp;

       heapify(array, 0, i-1);
   }
}

第一次建堆的时间复杂度是 O(n) ,而每次维护堆的时间复杂度是 O(lgn) ,需要维护 n-1 次,所以这样最后的时间复杂度是 O(nlgn)

完整的代码如下。

代码

public static void main(String[] args){
     
     

    int[] array = {
     
     4, 5, 9, 2, 1, 4, 1, 3, 5, 6, 7};
    heapSort(array);
    System.out.println(Arrays.toString(array));

}

public static void heapSort(int[] array) {
     
     
    buildHeap(array);

    for (int i = array.length - 1; i >= 1; i--) {
     
     
        // swap
        int temp = array[i];
        array[i] = array[0];
        array[0] = temp;

        heapify(array, 0, i-1);
    }
}

public static void buildHeap(int[] array) {
     
     
    int lastElementIndex = array.length - 1;
    for (int i = (lastElementIndex - 1) / 2; i >= 0; i--) {
     
     
        heapify(array, i, lastElementIndex);
    }
}

public static void heapify(int[] array, int index, int lastElementIndex){
     
     

    int leftChild;
    int rightChild;
    int maxValueIndex;

    while (true) {
     
     
        leftChild = 2 * index + 1;
        rightChild = 2 * index + 2;

        maxValueIndex = index;
        if (leftChild <= lastElementIndex 
            && array[leftChild] > array[maxValueIndex]) {
     
     
            maxValueIndex = leftChild;
        }
        if (rightChild <= lastElementIndex 
            && array[rightChild] > array[maxValueIndex]) {
     
     
            maxValueIndex = rightChild;
        }

        if (maxValueIndex != index) {
     
     
            // swap
            int temp = array[index];
            array[index] = array[maxValueIndex];
            array[maxValueIndex] = temp;

            index = maxValueIndex;
        } else {
     
     
            break;
        }
    }
}

猜你喜欢

转载自blog.csdn.net/qq_41512783/article/details/109355341