堆,系统堆结构,手动改写堆结构,堆结构远比比堆排序更重要!
提示:本文是堆排序的根基,但是堆结构,缺远比堆排序重要
今后有很多大厂的笔试面试题目,需要手动改写堆,本文就是最重要的基础知识
文章目录
系统堆结构PriorityQueue
说:堆
肯定不能单独说“堆”
得说是小根堆,还是大根堆
(1)小根堆,一颗完全二叉树,每一个节点x,均小于等于左右子;
(2)大跟对,一颗完全二叉树,每一个节点x,均大于等于左右子;
完全二叉树
所谓完全二叉树,就是二叉树的节点,从左往右是依次变满的过程:
也就是说,任意一个节点x,可以有左子,没有右子(因为它仍然时从左往右变满的过程)
可以有左子和右子(从左往右变满了)
但是不能没有左子,就有了右子(长子没有,老二老三何来?)【从左往右没有变满】叫不完全二叉树
系统堆默认小根堆
PriorityQueue是系统的堆类名称,默认是小根堆
//复习堆结构
public static void test2(){
PriorityQueue<Integer> heap = new PriorityQueue();
//默认小根堆,大厂的笔试题经常用,用来做贪心的题目
heap.add(5);
heap.add(2);
heap.add(3);
heap.add(1);
heap.add(4);
System.out.println(heap.peek());//堆顶,1
while (!heap.isEmpty()){
System.out.print(heap.poll() +" ");//不断地弹出堆顶的值,小根堆,升序的
}
}
直观看看系统的堆是啥样的:
1
1 2 3 4 5
看到,你放入了乱序的结构,它给你组织成了有序的结构
系统堆底层的实现是数组,只不过下标模拟二叉树
堆在底层是如何组织的呢?
堆的本质:用数组模拟组织成的完全二叉树
堆不是真的二叉树结构(二叉树的内部是真实的节点,每个节点有left和right指针)【堆在逻辑上是一个二叉树】
堆是用数组模拟的,二叉树的下标可以对应数组下标,所以用下标模拟的二叉树,数实际存放在数组中
比如:heap数组存放1 2 3 4 5
逻辑上可以这么组织一个完全二叉树:
如果heap数组中i=0位置可以代表头结点head
则数组heap的位置i的左子,在二叉树中下标是:2i+1,右子的下标是2i+2
这非常容易推算的,因此,堆必须保证严格的完全二叉树的特点,这样才能让下标从左往右递推
堆的二叉树中任意节点x的下标是i的话,它的父节点下标应该是多少?(i-1)/2
那么堆当初申请的时候,我们会设定一个最大的heap的尺寸,叫limit,也可以叫N
比如堆可能大为N=100;
而,我们实际存的数可能没有那么多,我们叫heapSize;
上面那个案例中,显然heapSize=5;我们只放了5个
手动改写堆结构
为什么系统堆有了,还要手动改写堆结构呢???
系统堆结构,只有add,poll,peek,isEmpty函数
顺便,系统堆结构会自动给你排序为大根堆或者小根堆
但是,系统堆结构,无法快速索引某一个x在哪个位置,如果非要索引,至少你还得**o(log(n))**的复杂度去寻找
如果我们希望,能不能让堆在o(1)速度知道x在哪个下标?
现在的系统堆结构是不能的,因此需要咱们手动改写堆,通过另外拿一个数组来存下标的方式,o(1)速度找到它。
系统为啥不给你写呢???
由于系统并不知道你要不要用这个下标信息【大部分情况下,咱并不用这个下标】
所以,系统干脆就不给你准备额外的空间存储下标信息,因为太浪费空间了!!
一旦你要用这个额外的信息,不好意思,你自己手动改写堆结构吧!!【这可是大厂的面试题中经常考的知识】
既然要手动改写堆结构,那咱就必须搞清楚,堆的结构到底是怎么实现的???
【我是说核心思想怎么实现?代码怎么写的?你都的会】
每次最基本的使用add,poll,peek,isEmpty 函数,顺便,系统堆结构会自动给你排序为大根堆或者小根堆
这个排序功能怎么实现?这是今天我们必须要学会的
往后要怎么加额外信息,咱再说。
堆结构的手动实现:思想与代码
(1)isEmpty()
简单,判断heapSize=0吗?是就为空,否则返回false
(2)peek()
简单,堆顶吗不就是,自然返回heap[0]就行了
我们说过,堆顶就是数组中的0位置,头结点,实际上数组存的,逻辑上是一个完全二叉树,两者下标是对应的关系。
(3)重头戏:如何给堆添加一个新值:add(x)
【不放设咱们要设计一个大根堆(因为小根堆也一样)】
现在heap中已经有了1 2 3了,请你添加4,怎么做?
我们这么做:
1)先把新来的x=4放入heapSize位置,代表x加入了堆尾部,挂在了3位置
2)通过一个函数检查x,有必要的话,让它从堆的heapSize位置上浮,冒到上面去!heapInsert(heap, heapSize)
看见没,通过检查上浮,让3位置的4,先跟1位置的2交换,再检查,让1位置的4根0位置的3交换,最终4上浮到堆顶
这样才能保证我们的堆是一个大根堆
最终heap被调整为标准的大根堆!!
heapInsert(heap, i)怎么实现呢?
很简单!
对比i位置的父节点parent,如果x>a,则在数组中交换i和parent位置;【然后让继续对比i=parent,看看还能不能上浮?能就循环上浮……】
否则不管了,当前就是一个很完美的大根堆。——就这么easy
OK,加入新元素的事情,就搞定了。
下面看弹出堆顶那个元素呢?
(4)弹出堆顶:poll()
自然heapSize减了1。也就是说heap数组尾部少了一个数,让原来heapSize-1那个位置失效!
弹出堆顶之后,咱还要保持原来的现状!大根堆,咋做呢?
我们这样:
1)既然0位置要失效,干脆让0和heapSize-1位置交换,即收尾交换,此时heapSize–,说明4已经失效了
2)此时呢,显然又不是大根堆了,自然咱们要利用一个函数检查,让i位置比较小的数,往下沉,让大的数上来,保证大根堆
heapify(arr,i,heapSize):
目前heap中存储了heapSize个数,需要把i那个数,检查下沉到下面,让大数上来,形成大根堆
咋做呢?
如果i有左子a(left=2i+1),有右子b(right=2i+2)
那我们先对比a和b谁大,位置就给largest
然后再对比largest和i,看看x大还是largest上的数大?小的位置给largest
这样的话,我们就找到了x,a和b中最大的位置是谁了
如果largest还是i没变,那不要下沉了
如果largest不是i,而确实变为a或者b那的位置了,则i和largest位置一定要交换,代表小的数下沉了
【然后继续检查i=largest和下面的点,看看还需要下沉吗?需要就循环下沉】
这样就保证了heap一直是一个大根堆
到此,我们就全部手动实现了大根堆结构!!
下面我们手撕这个大根堆的实现的代码!!!
//复习手动实现大根堆结构:
public static class BigHeap{
//底层是数组
private int[] heap;//不许外面的人修改
public int N;//最大容量
public int heapSize;//实际容量
public BigHeap(int limit){
N = limit;
heap = new int[N];
heapSize = 0;//最开始啥也没有
}
//几个函数
//isEmpty()
public boolean isEmpty(){
return heapSize == 0;
}
//peek()
public int peek(){
if (heapSize == 0) throw new RuntimeException("根本没有数!");
return heap[0];//堆顶,数组头结点
}
//heapInsert:从i位置开始检查上浮
public void heapInsert(int[] arr, int i){
//想往上浮,先看父节点在不在?,而且父节点不是0,是0的话,已经在堆顶了
//检查i位置,是不是更大,更大需要上浮的
int parent = (i - 1) >> 1;
while (parent >= 0 && arr[i] > arr[parent]){
swap(arr, i, parent);
i = parent;//循环检查需要上浮吗?
parent = (i - 1) >> 1;//更新此时i的parent
}
}
//add(x)
public void add(int value){
//规则:先放heap屁股,然后检查上浮的事情
heap[heapSize] = value;
heapInsert(heap, heapSize++);//从heapSize那开始检查,最后实际容量++
}
//heapify:从i位置检查下沉
public void heapify(int[] arr, int i, int heapSize){
//目前arr中实际容量为heapSize,咱咱这个范围内,检查i位置需不需要下沉
//对于大根堆来说,那就是小的需要下沉,咱们要找i和左右子谁更大,大就不下沉,小就要下沉
int left = (i << 1) | 1;
int right = left + 1;//左右子的下标
int largest = i;//暂时让最大就是i吧
while (left < heapSize){
//只有左子有就行
//先对比左右子谁更大,右边更大?给右边,否则就是左边暂时最大
largest = (right < heapSize && arr[right] > arr[left]) ? right : left;
//然后对比i
largest = arr[largest] > arr[i] ? largest : i;//左右子不大,那i不变
if (largest == i) break;//仍然时i不变,那不用下沉了,i最大
//否则真的要让i下沉,largest位置上来
swap(arr, i, largest);
//继续循环对比
i = largest;
left = (i << 1) | 1;
right = left + 1;//跟心此时i的左右子的下标
}
}
//poll()
public int poll(){
//弹出堆顶的结果
int ans = heap[0];
//先交换收尾,然后检查下沉不下沉,小的需要下沉,大的上来
swap(heap, 0, --heapSize);//当然实际容量必定--
heapify(heap, 0, heapSize);//在实际容量范围内,检查0位置是否需要下沉?
return ans;
}
}
//测试一下:
public static void test3(){
BigHeap heap = new BigHeap(10);
System.out.println(heap.isEmpty());//空
heap.add(1);
System.out.println(heap.peek());
heap.add(2);
System.out.println(heap.peek());
heap.add(3);
System.out.println(heap.peek());
heap.add(4);
System.out.println(heap.peek());//每次来一个更大的,堆顶就展示更大的
System.out.println("---------------");
heap.poll();
System.out.println(heap.peek());
heap.poll();
System.out.println(heap.peek());
heap.poll();
System.out.println(heap.peek());
heap.poll();
System.out.println(heap.isEmpty());//又空了
}
public static void main(String[] args) {
// checker();
// test();
// test2();
test3();
}
看结果
true
1
2
3
4
---------------
3
2
1
true
同理,咱们可以手动改写小根堆
小根堆,无非就是堆顶更小,所以呢,咱们只需要改动两个地方:
(1)heapInsert(int[] arr, int i):查i位置,和parent对比,谁更小,需要上浮【大根堆是谁更大,需要上浮】
(2)heapify(int[] arr, int i, int heapSize):查i位置,和左右子left和right对比,谁更小,小的保留,谁更大?大的下沉。【与大根堆不同,大根堆中小的下沉,大的上浮】
就改这两点,其余跟大根堆一模一样
//复习手动实现 小 根堆结构:
public static class SmallHeap{
//底层是数组
private int[] heap;//不许外面的人修改
public int N;//最大容量
public int heapSize;//实际容量
public SmallHeap(int limit){
N = limit;
heap = new int[N];
heapSize = 0;//最开始啥也没有
}
//几个函数
//isEmpty()
public boolean isEmpty(){
return heapSize == 0;
}
//peek()
public int peek(){
if (heapSize == 0) throw new RuntimeException("根本没有数!");
return heap[0];//堆顶,数组头结点
}
//heapInsert:从i位置开始检查上浮
public void heapInsert(int[] arr, int i){
//想往上浮,先看父节点在不在?,而且父节点不是0,是0的话,已经在堆顶了
//检查i位置,是不是更小,更小的需要上浮
int parent = (i - 1) >> 1;
while (parent >= 0 && arr[i] < arr[parent]){//与大根堆不同的唯一点!!,小才上浮
swap(arr, i, parent);
i = parent;//循环检查需要上浮吗?
parent = (i - 1) >> 1;//更新此时i的parent
}
}
//add(x)
public void add(int value){
//规则:先放heap屁股,然后检查上浮的事情
heap[heapSize] = value;
heapInsert(heap, heapSize++);//从heapSize那开始检查,最后实际容量++
}
//heapify:从i位置检查下沉
public void heapify(int[] arr, int i, int heapSize){
//目前arr中实际容量为heapSize,咱咱这个范围内,检查i位置需不需要下沉
//对于大根堆来说,那就是小的需要下沉,咱们要找i和左右子谁更大,大就不下沉,小就要下沉
int left = (i << 1) | 1;
int right = left + 1;//左右子的下标
int smallest = i;//暂时让最小就是i吧----小根堆,得找最小值哦!!!
while (left < heapSize){
//只有左子有就行
//先对比左右子谁更小,右边更小?给右边,否则就是左边暂时最小
smallest = (right < heapSize && arr[right] < arr[left]) ? right : left;
//然后对比i
smallest = arr[smallest] < arr[i] ? smallest : i;//左右子不小,那i不变
if (smallest == i) break;//仍然i不变,那不用下沉了,i最小
//否则真的要让i下沉,smallest位置上来
swap(arr, i, smallest);
//继续循环对比
i = smallest;
left = (i << 1) | 1;
right = left + 1;//跟心此时i的左右子的下标
}
}
//poll()
public int poll(){
//弹出堆顶的结果
int ans = heap[0];
//先交换收尾,然后检查下沉不下沉,大的需要下沉,小的上来
swap(heap, 0, --heapSize);//当然实际容量必定--
heapify(heap, 0, heapSize);//在实际容量范围内,检查0位置是否需要下沉?
return ans;
}
}
//测试一下:
public static void test4(){
SmallHeap heap = new SmallHeap(10);
System.out.println(heap.isEmpty());//空
heap.add(4);
System.out.println(heap.peek());
heap.add(3);
System.out.println(heap.peek());
heap.add(2);
System.out.println(heap.peek());
heap.add(1);
System.out.println(heap.peek());//每次来一个更小的,堆顶就展示更小的
System.out.println("---------------");
heap.poll();
System.out.println(heap.peek());
heap.poll();
System.out.println(heap.peek());
heap.poll();
System.out.println(heap.peek());
heap.poll();
System.out.println(heap.isEmpty());//又空了
}
public static void main(String[] args) {
// checker();
// test();
// test2();
// test3();
test4();
}
看:
true
4
3
2
1
---------------
2
3
4
true
下一篇文章,咱们在这基础上,专门讲一下堆排序,堆排序很简单。
总结
提示:重要经验:
1)堆结构非常重要!!!堆结构远比堆排序重要。
2)大根堆,和小根堆,俩结构内部形式差不多,只需要插入和删除的过程中,有点细微的对比和改动就行。