二叉堆与堆排序(详细图解)

二叉堆

二叉堆是一种特殊的树,有两种特性:

  • 二叉堆是一个完全二叉树
  • 二叉堆中任意一个父节点的值都大于等于(或小于等于)其左右孩子节点的值。

根据第二条特性,可以将二叉堆分为两类:

  • 最大堆:父节点的值总是大于或等于左右孩子节点的值。
    在这里插入图片描述

  • 最小堆:父节点的值总是小于或等于左右孩子节点的值。
    在这里插入图片描述

二叉堆的实现

1.往堆中插入一个节点

由于二叉堆具有完全二叉树的特性,所以我们插入节点时,应该保证它任然是一个完全二叉树。所以,在插入的时候,我们把新节点插入二叉堆的最后一个位置。例如,在下图中插入7。
在这里插入图片描述
然后在对二叉堆进行调整,使其满足任意一个父节点都大于等于它的左右孩子节点。对此,我们可以对新插入的节点进行上浮操作,即和父节点交换位置。

7和2相比,7大于2,上浮。
在这里插入图片描述
7和5比,7大于5,上浮。
在这里插入图片描述
7与8比,7小于8,插入完成。
在这里插入图片描述

2.删除节点

对于二叉堆,删除节点,我们一般是删除根节点。和插入操作一样,由于要保证二叉堆完全二叉树的特性,在删除根节点后,将二叉堆的最后一个元素替换到根节点上。
在这里插入图片描述
把8删除后,将2替换上来。
在这里插入图片描述
由于要保证最大堆任意父节点都大于等于其左右孩子节点的值,对根节点进行下沉操作。
首先比较根节点的左右孩子节点的值,7比6大,在将2与7比较,2小于7,下沉。
在这里插入图片描述
比较左右孩子节点的值,4大于2,父节点2的值小于4,2与4交换。
在这里插入图片描述
节点2已经不存在左右孩子节点,删除操作结束。
在这里插入图片描述

3.构建二叉堆

二叉树的构建一般是基于链表实现,但是二叉堆是采用数组的方式来存储。
在这里插入图片描述
这个二叉堆对应的数组是:
在这里插入图片描述
所以如果知道一个节点的位置,就能推导出其左右孩子节点的位置。假如一个节点的下标是n,则其左孩子节点的下标为:2n+1,右孩子节点的下标为:2n+2。

最大堆的代码实现:

public class BinaryHeap {
    
    

    /**
     * 上浮操作,对插入的节点进行上浮
     *
     * @param arr
     * @param length 数组的长度
     */
    public static void upAdjust(int[] arr, int length) {
    
    
        // 标记插入的节点
        int child = length - 1;
        // 插入节点的父节点
        int parent = (child - 1) / 2;
        // 保存插入节点的值
        int temp = arr[child];

        // 进行上浮
        while (child > 0 && temp < arr[parent]) {
    
    
            // 单向赋值
            arr[child] = arr[parent];
            // 孩子节点和父节点向上移
            child = parent;
            parent = (child - 1) / 2;
        }

        // 循环结束,child的下标即时要插入的位置
        arr[child] = temp;
    }

    /**
     * 下沉操作
     *
     * @param arr
     * @param parent 要下沉元素的下标
     * @param length 数组的长度
     */
    public static void downAdjust(int[] arr, int parent, int length) {
    
    
        // 存储下沉元素的值
        int temp = arr[parent];
        // 下沉节点的孩子节点
        int child = parent * 2 + 1;

        while (child < length) {
    
    
            // 如果右孩子节点比左孩子节点大,定位到右孩子节点
            if (child + 1 < length && arr[child] < arr[child + 1]) child++;
            // 如果父节点大于等于孩子节点,退出循环
            if (temp >= arr[child]) break;

            // 单向赋值
            arr[parent] = arr[child];
            // 父节点和孩子节点向下移
            parent = child;
            child = parent * 2 + 1;
        }

        // 退出循环表示找到正确的位置
        arr[parent] = temp;
    }

    // 构建二叉堆
    public static void buildHead(int[] arr) {
    
    
        // 从最后一个非叶子节点开始下沉
        for (int i = (length - 2) / 2; i >= 0; i--) {
    
    
            downAdjust(arr, i, arr.length);
        }
    }
}

堆排序

堆排序是基于二叉堆实现的,要实现堆排序,需要先构造二叉堆。在大小为n的二叉堆构建完成后,将堆顶的元素与最后一个元素交换,将剩下的n-1个元素看成一个新的二叉堆,然后在对根节点进行下沉操作,重复此过程,即能完成堆排序。图解如下:
在这里插入图片描述
将堆顶元素9和最后一个元素2交换。
在这里插入图片描述对2进行下沉操作。
在这里插入图片描述
将7与1进行交换,对1进行下沉操作。
在这里插入图片描述
将6与2进行交换,对2进行下沉操作。下沉操作时,若左右孩子节点相同时,选取左右孩子节点交换都可以。
在这里插入图片描述
将5与3进行交换,对3进行下沉操作。
在这里插入图片描述
以此类推…
在这里插入图片描述
这样,堆排序就完成了。

代码实现如下:

import java.util.Arrays;

public class HeapSort{
    
    

	 /**
     * 下沉操作
     * 
     * @param arr
     * @param parent  要下沉元素的下标
     * @param length  数组的长度
     */
    public static void downAdjust(int[] arr, int parent, int length) {
    
    
        // 保存要下沉元素的值
        int temp = arr[parent];
        // 定位左孩子节点的位置
        int child = parent * 2 + 1;

        while (child < length) {
    
    
            // 如果右孩子节点比左孩子节点大,则定位到右孩子节点
            if (child + 1 < length && arr[child] < arr[child + 1]) child++;
            // 如果父节点的值大于等于孩子节点的值,结束下沉操作
            if (temp >= arr[child]) break;

            // 单向赋值
            arr[parent] = arr[child];
            // 将父节点和孩子节点向下移
            parent = child;
            child = parent * 2 + 1;
        }

        // 循环结束表示定位到了正确位置
        arr[parent] = temp;
    }

    public static void heapSort(int[] arr) {
    
    
        if (arr == null || arr.length < 2) return;

        // 构建二叉堆,从最后一个非叶子节点开始下沉
        for (int i = (arr.length - 2) / 2; i >= 0; i--) {
    
    
            downAdjust(arr, i, arr.length);
        }

        // 进行堆排序
        for (int i = arr.length - 1; i > 0; i--) {
    
    
            // 将堆顶的元素和最后一个元素交换
            int temp = arr[0];
            arr[0] = arr[i];
            arr[i] = temp;
            // 下沉操作,每次数组的长度减一
            downAdjust(arr, 0, i);
        }
    }

	// 测试类
	public static void main(String[] args) {
    
    
        int[] a = new int[10];
        for (int i = 0; i < a.length; i++) {
    
    
            a[i] = (int) (Math.random() * 20);
        }

        System.out.println("排序前:" + Arrays.toString(a));
        heapSort(a);
        System.out.println("排序后:" + Arrays.toString(a));
    }
}    

运行结果

排序前:[15, 19, 14, 8, 11, 4, 17, 16, 7, 19]
排序后:[4, 7, 8, 11, 14, 15, 16, 17, 19, 19]

性质:①时间复杂度分析:在建堆的过程中,时间复杂度是O(n),排序过程中的时间复杂度是O(nlogn),所以,堆排序的整体时间复杂度是O(nlogn)。

②空间复杂度:O(1)     ③原地排序     ④非稳定排序

猜你喜欢

转载自blog.csdn.net/qq_46122005/article/details/110440480