0
点赞
收藏
分享

微信扫一扫

堆,系统堆结构,手动改写堆结构,堆结构远比比堆排序更重要

堆,系统堆结构,手动改写堆结构,堆结构远比比堆排序更重要!

提示:本文是堆排序的根基,但是堆结构,缺远比堆排序重要
今后有很多大厂的笔试面试题目,需要手动改写堆,本文就是最重要的基础知识


文章目录

系统堆结构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)大根堆,和小根堆,俩结构内部形式差不多,只需要插入和删除的过程中,有点细微的对比和改动就行。

举报

相关推荐

0 条评论