编程语言
首页 > 编程语言> > 数据结构与算法之堆

数据结构与算法之堆

作者:互联网

封面

数据结构与算法系列

数据结构与算法之哈希表

数据结构与算法之跳跃表

数据结构与算法之字典树

数据结构与算法之2-3树

数据结构与算法之平衡二叉树

数据结构与算法之红黑树

带你手撸红黑树,小泉憋大招了

数据结构与算法之堆

数据结构与算法之十大经典排序

数据结构与算法之二分查找三模板

数据结构与算法之动态规划

数据结构与算法之回溯算法

数据结构与算法之Morris算法

数据结构与算法之贪心算法

数据结构与算法之拓扑排序

数据结构与算法之KMP算法

数据结构与算法之堆

前言

最近很久没更新了,一方面是手头活有点多了,有些忙了,业务代码得走起了,另一方面,生活上总有些小事情打断了节奏。总结下来,对,就是我太懒了。我承认,我不配,呜呜呜。
在这里插入图片描述

所以我来更新了。。。。。。
本期主讲的数据结构,他与栈经常成双入对,他就是堆,栈的好兄弟。

定义

老规矩,先给出堆的定义,以下是维基百科对堆的定义

In computer science, a heap is a specialized tree-based data structure which is essentially an almost complete tree that satisfies the heap property: in a max heap, for any given node C, if P is a parent node of C, then the key (the value) of P is greater than or equal to the key of C. In a min heap, the key of P is less than or equal to the key of C. The node at the “top” of the heap (with no parents) is called the root node.

基本就是,在完全二叉树的基础上,再加上一些特殊的性质。

性质

  1. 完全二叉树
    堆首先必须是一个完全二叉树,这个时候你肯定想问小泉,完全二叉树是啥子。
    待小泉画个图给大家xue微讲解一下。
    完全二叉树
    在图中,从根节点开始从上至下然后每层从左到右进行对节点进行标记序号,如果每个节点的序号与满二叉树(每层节点都达到最大个数)状态下的序号一致,则认为是完全二叉树。
    通俗来说,对于一个完全二叉树, 每一层节点需全不为空时,才可以有子节点,且必须先有左子节点再有右子节点。
    看图其实也可以了解到,满二叉树其实也是一种完全二叉树

  2. 节点值比子节点大/小
    最大堆:每一个节点值都要比其左右子节点值大
    最大堆

    最小堆:每一个节点值都要比其左右子节点值小
    最小堆

堆的构建

理论来说,堆的构建可以理解为节点的插入或者节点的下沉:
插入即新的节点插入到老节点的子节点进行上浮操作。
下沉即新节点每次从堆顶“插入”,进而每次都需要从堆顶进行下沉操作。
实际上,如果给出的是数组,我们可以把所给数组当作完全二叉树:
从叶子节点的父节点往“堆顶”进行下沉操作。如果你曾经看过Java优先队列(PriorityQueue,默认为小顶堆)的源码,你会发现,它的内部其实有着一个数组,初始容量为11。
在这里插入图片描述
在进行堆的一系列操作之前,先预热下,在使用堆时两个很重要很底层的操作:下沉和上浮。

下沉

下沉又称为堆化,当发现节点元素比子节点大(最小堆)或者比子节点小(最大堆)时,将节点值与较大子节点(最小堆)或者较小子节点(最大堆)的值相交换,即为沉操作。
用途:(1) 删除堆顶元素后重新形成堆 (2) 创建堆

void siftDown(int index) {
        if (index == size) return;
        int childIndex = 2 * index + 1;
        int tmp = heap[index];
        while(childIndex <= size) {
            if (childIndex + 1 <= size && heap[childIndex] < heap[childIndex + 1]) {
                childIndex++;
            }
            if (heap[childIndex]<= tmp) break;
            heap[index] = heap[childIndex];
            index = childIndex;
            childIndex = 2 * index + 1;
        }
        heap[index] = tmp;
    }

上浮

当发现节点元素比父节点小(最小堆)或者比父节点大(最大堆)时,将节点值与父节点值相交换,即为上浮操作。
用途:堆的插入

void siftUp(int index) {
        if (index ==1) return;
        int parentIndex = index/2;
        int tmp = heap[index];
        while (parentIndex >=1 && heap[parentIndex] < heap[index]) {
            heap[index] = heap[parentIndex];
            index = parentIndex;
            parentIndex = index / 2;
        }
        heap[index] = tmp;
    }

插入

对于插入的操作过程。谨记一点,先按照完全二叉树的形式将新节点当作叶子节点插入。然后再对该节点进行上浮操作。
具体事例如下:
向已经符合堆结构的[12, 10,9, 5, 6, 8]中插入13

  1. 先将13按照子节点插入到9的右子节点处,符合完全二叉树的性质。
  2. 对13进行上浮操作,与9做比较,最大堆的第二条性质,节点要比子节点大,因此与9互换,继续上浮。
  3. 重复2的操作直至无法上浮(无法上浮两种可能,一是达到堆顶,二是不满足上浮的条件)
    过程图剖解如下
    堆的插入

删除

对于删除的操作过程。谨记一点,查找到最后一个元素,将要删除的节点值与最后一个节点值互换,剔除最后一个节点。然后再对互换后的节点进行下沉操作。
具体事例如下:
在已经符合堆结构的[12, 10,9, 5, 6, 8]中删除12

  1. 先将12与8互换,再删除掉最后一个节点
  2. 对8进行下沉操作,与9,10做比较,最大堆的第二条性质,节点要比子节点大,因此与较大的子节点10互换,继续下沉。
  3. 重复2的操作直至无法下沉
    过程图剖解如下
    堆的删除

复杂度分析

操作时间复杂度时间复杂度
堆的创建O(N)O(N)
堆的插入O(logN)O(logN)
删除堆顶元素O(logN)O(logN)

其实堆的主要操作也就是这三项,插入堆,删除堆顶元素
堆这一数据结构相对来说,其实没有那么复杂但却是一些特定问题的好帮手——排序问题、前K个元素、第K个元素等等此类的问题。

应用

堆排序

由于堆的性质,最大堆的堆顶一定是最大值,最小堆的堆顶一定是最小值,因此删除堆顶后新堆顶是次大(或小)值,以此类推。

public class HeapSort {
    // 堆排序
    public static int[] heapSort(int[] nums) {
        int n = nums.length;
        int i,tmp;
        //构建大顶堆
        for(i=(n-2)/2;i>=0;i--) {//从只有一层子节点的父节点开始往树的根节点进行下沉操作
            shiftDown(nums,i,n-1);
        }
        //进行堆排序,删除堆顶,进行堆重构后堆顶依然是最大的
        for(i=n-1;i>=1;i--){
            //删除堆顶的过程是将最后一个节点值替换堆顶值,然后删除最后一个节点,其实也就是与最后一个节点互换
            tmp = nums[i];
            nums[i] = nums[0];
            nums[0] = tmp;
            shiftDown(nums,0,i-1);
        }
        return nums;
    }

    //小元素下沉操作
    public static void shiftDown(int[] nums, int parentIndex, int n) {
        //临时保存要下沉的元素
        int temp = nums[parentIndex];
        //左子节点的位置
        int childIndex = 2 * parentIndex + 1;
        while (childIndex <= n) {
            // 如果右子节点比左子节点大,则与右子节点交换
            if(childIndex + 1 <= n && nums[childIndex] < nums[childIndex + 1])
                childIndex++;
            if (nums[childIndex] <= temp ) break;//该子节点符合大顶堆特点
            //注意由于我们是从高度为1的节点进行堆排序的,所以不用担心节点子节点的子节点不符合堆特点
            // 父节点进行下沉
            nums[parentIndex] = nums[childIndex];
            parentIndex = childIndex;
            childIndex = 2 * parentIndex + 1;
        }
        nums[parentIndex] = temp;
    }

    public static void main(String[] args) {
        int[] a = {91,60,96,13,35,65,81,46,13,10,30,20,31,77,81,22};
        System.out.print("排序前数组a:\n");
        for(int i:a) {
            System.out.print(i);
            System.out.print(" ");
        }
        a=heapSort(a);
        System.out.print("\n排序后数组a:\n");
        for(int i:a) {
            System.out.print(i);
            System.out.print(" ");
        }
    }
}

Top K问题

Top K 问题是一类题的统称,主要是想要选取满足某一条件前K个最大化满足条件的元素。

题目

Leetcode 347. 前 K 个高频元素
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:
输入: nums = [1], k = 1
输出: [1]

解析

将所有元素加入到小顶堆中,如果超过了K个元素,那么每多一个比堆顶更加满足条件的元素就删除堆顶,然后插入元素。

代码

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        Map<Integer, Integer> map = new HashMap<Integer, Integer>();
        for (int num : nums) {
            map.put(num, map.getOrDefault(num, 0) + 1);
        }

        // int[] 的第一个元素代表数组的值,第二个元素代表了该值出现的次数
        PriorityQueue<int[]> queue = new PriorityQueue<int[]>(new Comparator<int[]>() {
            public int compare(int[] pre, int[] nex) {
                return pre[1] - nex[1];
            }
        });
        for (int key : map.keySet()) {
            int value = map.get(key);
            if (queue.size() == k) {
                if (queue.peek()[1] < value) {
                    queue.poll();
                    queue.add(new int[]{key, value});
                }
            } else {
                queue.add(new int[]{key, value});
            }
        }
        int[] kMax = new int[k];
        for (int i = 0; i < k; ++i) {
                kMax[i] = queue.poll()[0];
        }
        return kMax;
    }
}

总结

堆这一数据结构相对红黑树、B+树等结构要相对简单很多,掌握堆的两个性质、下沉和上浮的操作,构建起堆并不是什么难事,当然堆的构建过程了解了之后,更多的是如何去使用这一数据结构,Java当中就有集合类PriorityQueue(优先队列)作为堆提供给我们使用,以后带大家一起看一看它的源码吧~
今天的分享就到这里,希望对您有所帮助。

如有兴趣,可以关注我的公众号,每周和你一起修炼数据结构与算法。
在这里插入图片描述

标签:之堆,nums,int,算法,二叉树,数据结构,节点
来源: https://blog.csdn.net/LInthunder/article/details/115283192