数据结构与算法系列
数据结构与算法之哈希表
数据结构与算法之跳跃表
数据结构与算法之字典树
数据结构与算法之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.
基本就是,在完全二叉树
的基础上,再加上一些特殊的性质。
性质
-
完全二叉树
堆首先必须是一个完全二叉树,这个时候你肯定想问小泉,完全二叉树是啥子。
待小泉画个图给大家xue微讲解一下。
在图中,从根节点开始从上至下然后每层从左到右进行对节点进行标记序号,如果每个节点的序号与满二叉树(每层节点都达到最大个数)状态下的序号一致,则认为是完全二叉树。
通俗来说,对于一个完全二叉树, 每一层节点需全不为空时,才可以有子节点,且必须先有左子节点再有右子节点。
看图其实也可以了解到,满二叉树其实也是一种完全二叉树。 -
节点值比子节点大/小
最大堆:每一个节点值都要比其左右子节点值大
最小堆:每一个节点值都要比其左右子节点值小
堆的构建
理论来说,堆的构建可以理解为节点的插入或者节点的下沉:
插入即新的节点插入到老节点的子节点进行上浮操作。
下沉即新节点每次从堆顶“插入”,进而每次都需要从堆顶进行下沉操作。
实际上,如果给出的是数组,我们可以把所给数组当作完全二叉树:
从叶子节点的父节点往“堆顶”进行下沉操作。如果你曾经看过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
- 先将13按照子节点插入到9的右子节点处,符合完全二叉树的性质。
- 对13进行上浮操作,与9做比较,最大堆的第二条性质,节点要比子节点大,因此与9互换,继续上浮。
- 重复2的操作直至无法上浮(无法上浮两种可能,一是达到堆顶,二是不满足上浮的条件)
过程图剖解如下
删除
对于删除的操作过程。谨记一点,查找到最后一个元素,将要删除的节点值与最后一个节点值互换,剔除最后一个节点。然后再对互换后的节点进行下沉操作。
具体事例如下:
在已经符合堆结构的[12, 10,9, 5, 6, 8]中删除12
- 先将12与8互换,再删除掉最后一个节点
- 对8进行下沉操作,与9,10做比较,最大堆的第二条性质,节点要比子节点大,因此与较大的子节点10互换,继续下沉。
- 重复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(优先队列)作为堆提供给我们使用,以后带大家一起看一看它的源码吧~
今天的分享就到这里,希望对您有所帮助。
如有兴趣,可以关注我的公众号,每周和你一起修炼数据结构与算法。
目前小泉也在建设自己的网站,有兴趣也可关注下:小泉的开发修炼之路