一、前言
二叉树完结后因为学校的事情太多一直没来得及更新关于数据结构的系列博客 , 今天就来继续更新一个新的结构—堆 , 堆(优先级队列) , 名字是队列 , 本质上是一种特殊的二叉树
二、二叉树的顺序存储
2.1 存储方式
前面我们在写一个普通的二叉树时 , 使用的左右孩子表示法 , 除此之外也可以使用数组来表示树 , 使用数组存储这个树的层序遍历结果(需要存储空节点) , 这种方式一般只适合表示完全二叉树,因为对于非完全二叉树来说会有空间的浪费 , 这种方式的主要用法就是堆
2.2 下标关系(很重要)
根据数组下标确定节点之间父子关系 :
① 父节点和左子树的关系
- 左子树的下标=根节点下标*2+1
- 根节点的下标=(左子树下标-1)/2
②父节点和右子树的关系
- 右子树下标=根节点下标*2+2
- 根节点下标=(右子树下标-2)/2
一个根节点左右子树下标相邻 , 根据下标求根节点下标 , 都可以用当前子节点的下标-1/2不需要区分左右子树
三、堆
3.1 概念
① 堆物理上是保存在数组中
② 通过数组表示树可能会浪费很多空间 , 所以之前用链表 , 对于一种特殊的树 , 使用数组就不会浪费(完全二叉树)
③ 对于树中的任意节点 , 满足根节点小于左右子树的值(小堆) , 满足根节点大于左右子树的值(大堆) , 堆就是通过数组的形式来存储
④ 堆最大的用处 快速找到一个数中的最大值 最小值(根节点)
3.2 向下调整(前提是左右子树都是堆)
要找到一个序列前K个最大值 , 就需要知道这个K个元素构成的集合中最小值是谁 , 需要用一个最小堆来找到这个最小值 , 一旦堆中的元素发生改变(插入/删除)都需要调整堆的结构 , 让堆的规则不被破坏掉
规则 :
① 先设定根节点为当前节点
② 找到当前节点的左右子树的值(通过下标来获取到)
③ 比较左右子树的值,找出谁更小,用child标记更小的值
④ 比较child和parent谁大谁小 , 如果child比parent小 , 就不符合小堆的规则 , 进行交换 ; 如果child比parent大 , 符合小堆的规则,不需要交换 , 整个调整也就结束了
处理完一个节点之后从当前child出发,循环刚才的过程
代码 :
//从倒数第一个非叶子节点开始从后往前遍历数组
// 针对每个位置,依次向下调整
//上面就是在建堆
//调整一个堆
//通过size指定array中那些元素是有效的堆元素
//index表示从哪个位置的下标开始调整
public static void shiftDown(int[] array,int size,int index){
int parent =index;
int child=2*parent+1;//根据parent下标找到左子树下标
//时间复杂度O(logN) size固定 child每次*2
//size是8循环3次 size 16 循环4次 size 32 循环5次
while(child<size){
//比较左右子树 找到较小值
if(child+1<size && array[child+1]<array[child]){
child=child+1;
}
//经过上面的比较 已经不知道child是左还是右
//只是最小
//再比较child和parent
if(array[child]<array[parent]){
//不符合小堆原则,交换父子节点
int temp=array[child];
array[child]=array[parent];
array[parent]=temp;
}else{
//调整完毕,不需要继续
break;
}
//更新parent和child 处理下一层数据
parent=child;
child=parent*2+1;
}
3.3 向上调整
public static void shiftUp(int[] array,int index){
int child=index;
int parent=(child-1)/2;
while(child>0){
if(array[parent]<array[child]){
//当前不符合大堆要求
int temp=array[parent];
array[parent]=array[child];
array[child]=temp;
}else{
//发现当前父节点比子节点大,说明已经符合堆结构
break;
}
child=parent;
parent=(child-1)/2;
}
}
3.4 建堆
建堆可以基于向下调整,也可以基于向上调整
基于向下调整来建堆 :
public static void createHeap(int[] array,int size){
for(int i=(size-1-1)/2;i>=0;i--){
shiftDown(array,size,i);
}
}
基于向上调整来建堆 :
public static void createHeap(int[] array,int size){
for(int i=size-1;i>0;i--){
shiftDown(array,i);
}
}
3.5 优先级队列
public class MyPriority {
//array看起来是个数组,其实是个堆结构
private int[] array=new int[100];
private int size=0;
public void offer(int x){
array[size]=x;
size++;
//把新加入的元素进行向上调整
//针对child和parent进行比较,看看是否满足当前堆的条件
//当前是一个大堆,要求parent的值要比child大,如果parent没有child大,就交换元素
//进行向上调整直接比较父子,不需要比较左右子树
shiftUp(array,size-1);
}
public int poll(){
//下标为0的元素就是队首元素
//删掉的同时,剩下的结构仍然是个堆
//直接把数组中的最后一个元素赋值到堆顶元素,同时size--
//接下来从根节点出发 进行向下调整
int oldValue=array[0];
array[0]=array[size-1];
size--;
shiftDown(array,size,0);
return oldValue;
}
public int peek(){
return array[0];
}
public boolean isEmpty(){
return size==0;
}
public static void main(String[] args) {
MyPriority queue=new MyPriority();
queue.offer(9);
queue.offer(5);
queue.offer(2);
queue.offer(7);
queue.offer(3);
queue.offer(6);
queue.offer(8);
while (!queue.isEmpty()){
//poll就是有序顺序
//优先队列 每次poll的元素就是优先级最高/最低的元素取出来
//poll N次就是堆排序
int cur=queue.poll();
System.out.println(cur);
}
}
}
四、标准库中的堆
标准库中的优先队列是小堆
public static void main(String[] args) {
//标准库中的优先队列是小堆
PriorityQueue<Integer> queue = new PriorityQueue<>();
queue.offer(9);
queue.offer(5);
queue.offer(2);
queue.offer(7);
queue.offer(3);
queue.offer(6);
while(!queue.isEmpty()){
int cur=queue.poll();
System.out.println(cur);
}
}
五、TopK问题
给定N个数字 找出前M大的数字
解决方法1 : 用一个数组保存这个数字 , 直接在数组上建大堆,循环1000次进行取堆顶和调整
解决方法2 : 2.先取集合中前1000个元素放在一个数组中 , 建立一个小堆(堆顶元素就是这个堆最小的) , 再遍历集合中数字和守门员比较 , 比守门员大就删掉守门员去入堆调整堆 所有元素遍历完 , 就是前1000大的
方法1的内存可能放不下 , 方法2的运行效率也更高
100亿数字=>400亿字节=>8G=>80亿是40G
tips:定时器:工作中经常会用到的一个组件=>本质上就是topK问题.底层原理就是基于堆数据结构
六、面试题
给定两个以升序排序的整型数组nums1和nums2 , 以及一个整数K 定义一对值(u,v) , 其中第一个元素来自nums1 , 第二个元素来自nums2 , 找到和最小的K对数字(u1,v1)(uk,vk)
class Pair implements Comparable<Pair>{
public int n1;
public int n2;
public int sum;
public Pair(int n1, int n2) {
this.n1 = n1;
this.n2 = n2;
this.sum = n1+n2;
}
@Override
public int compareTo(Pair o) {
//this比other小返回<0
//this比other大 返回大于0
//this和other相等 返回0
//直接用sum值衡量Pair大小
return this.sum-o.sum;
}
}
class Solution{
//1.获取所有数对
//2.数对都放在优先队列中
//3.再从优先队列中取到前K对数对即可
//需要把数对放在类中 优先队列保存这个类的对象就好
//[
// [1,1],
// [1,2]
//]
// 返回值的二维数组中,每一行是一个数对(每一行只有两个元素)
//一共有K行
public List<List<Integer>> kSmallestPairs(int[] nums1,int[] nums2,int k){
List<List<Integer>> result = new ArrayList<>();
if(nums1.length==0||nums2.length==0||k<=0){
return result;
}
//当前是需要前K小的元素 建立小堆 TOPK的第一种解决方式
//Pair可以放在优先队列中要实现Comparable接口,可以去比较大小
PriorityQueue<Pair> queue=new PriorityQueue<>();
//把可能数对获取到加入队列
for(int i=0;i<nums1.length;i++){
for(int j=0;j<nums2.length;j++){
queue.offer(new Pair(nums1[i],nums2[j]));
/*if(queue.size()>k){
//利用k限制queue的大小
//循环结束之后queue中就只剩下三个最大的元素
//但是题目要求取最小,就直接建一个存放所有元素的小堆,循环去取堆顶 topk的第一种解法
queue.poll();
}*/
}
}
//循环结束之后,此时所有的数对都在队列中,循环取出k个较小元素即可
for(int i=0;i<k&&!queue.isEmpty();i++){
Pair cur=queue.poll();
List<Integer> temp=new ArrayList<>();
temp.add(cur.n1);
temp.add(cur.n2);
result.add(temp);
}
return result;
}
}