算法重拾-1、二插堆

一.概念

二插堆,是一种数据结构。用于解决任务优先级队列的性能问题。比如任务有优先级,我每次处理最高优先级的任务,每次会产生新的任务加入到队列中,并且每次取出最高优先级的任务去处理,并从队列中删除。我可以用一个有序动态数组去解决,插入每次需要 O(n)的时间,查询和删除都是O(1)。那么有没有更好的结构,优于这个效率,BBST是一个,但数据结构实现太复杂。有一个很好的简单的数据结构,就是二插堆,可以实现 插入、删除和获取最大值都是O(logn)。

二插堆在逻辑上是一个完全二叉树。在物理上是用数组来存储。结构的关系见下图:


截屏2023-12-26 18.22.30.png

根据图的观察,节点的序号具有如下规律:(假设当前节点序号为i,数组长度为n)
1.左子节点为 2i + 1,右子节点为 2i+2。
(如果序号超出数组长度n,则不存在相应左右子节点)
2.最后一个子节点(即非叶子节点) 为 n/2
3.父节点为 (i-1)/2

二插堆的特点:任何子节点的值都小于或等于父节点,最大值是根节点。

二.实现

实现涉及到三个关键的方法:
1.初始化,也叫原地建堆,一般有两种方案,从下至上的下滤和从上至下的上滤。一般选取前者。
(从一个任意数组转为满足二插堆性质的数组)
2.插入,要求从最后一个叶子结点下一个节点插入
3.删除,删除根节点

1.插入

插入核心思路是,插入到最后一个叶子节点。然后从此节点开始,找父节点,如果父节点大于子节点,则停止,否则交互父子节点,从父节点位置继续上滤。
想象下:上滤就是把底层的大的值,冒泡到上层,直到合适的位置,保证上一层的子节点严格大于下一层的子节点即可。

    public void add(E element) {
        elementNotNullCheck(element);
        ensureCapacity(size+1);
        elements[size++] = element;
        siftUp(size-1);
    }

    // 让index的元素上滤
    private void siftUp(int index){
        E e = elements[index];
        while (index > 0){
            int pindex = (index - 1) >> 1;
            E p = elements[pindex];
            if(compare(e,p) <= 0) return;

            E tpm = elements[index];
            elements[index] = elements[pindex];
            elements[pindex] = tpm;

            index = pindex;

        }
    }

2.删除

删除即是删除堆顶,把最后一个元素放到根节点,然后堆长度-1,之后从根节点开始下滤。
所谓下滤,就是找到当前节点的左右子节点。如果有比当前节点大的,则找到左右子节点中最大的,和当前节点交互,并且从这个交换的子节点继续向下这一过程,直到子节点成为叶子节点为止。如果没有比当前节点大的,则停止,这就是合适的下滤位置了。这个过程保证了二插堆的性质,保证了所有高层的子节点都大于底一层的,且顶部是最大的。可以想想下,下滤就是把顶部新节点流动到合适的位置。

    public E remove() {
        emptyCheck();
        E root =  elements[0];
        elements[0] = elements[size - 1];
        elements[size-1] = null;
        size--;
        siftDown(0);
        return root;
    }

    private void siftDown(int index){
        E element = elements[index];

        int half = size >> 1;
        while ( index < half){// 第一个叶子节点索引=非叶子节点数量
            // 找出左右子树节点中较大的一个,并上移。
            int childIndex = 2*index + 1;
            E child = elements[childIndex];

            int rightIndex = childIndex + 1;
//            System.out.printf("比较元素"+childIndex+"和"+rightIndex);
            if(rightIndex < size && compare(elements[rightIndex],child) > 0){
                childIndex = rightIndex;
                child = elements[rightIndex];
            }

            if(compare(element,child) >= 0) break;
            elements[index] = child;
            index = childIndex;
        }
        elements[index] = element;

    }

3.原地建堆

原地建堆,就是从最后一个子节点(非叶子几点)向根节点方向,逐渐下滤,下滤可保证每个子树是二叉堆,从而保证树增大的过程中,始终满足二插堆性质。

public BinartHeap(E[] elements,Comparator<E> comparator){
        this.comparator = comparator;

        if(elements == null || elements.length == 0){
            this.elements = (E[])new Object[DEFAULT_CAPACITY];
        }else{
            size = elements.length;
            int capacity = Math.max(elements.length,DEFAULT_CAPACITY);
            this.elements = (E[]) new Object[capacity];
            for (int i = 0; i < elements.length; i++) {
                this.elements[i] = elements[i];
            }
            heapify();
        }
    }

    // 批量建堆
    private void heapify(){
        for (int i = (size >> 1) - 1; i >=0; i--) {
            siftDown(i);
        }
    }

4.替换

一般情况想到,先删除,再增加。
实际可以直接替换堆顶元素,进行下滤。

    public E replace(E element) {
//        E root = remove();
//        add(element);
//        return root;
        elementNotNullCheck(element);
        E root = null;
        if(size == 0){
            elements[0] = element;
            size++;
        }else{
            root = elements[0];
            elements[0] = element;
            siftDown(0);
        }
        return root;
    }

三、堆排序

思路:就是原地建堆,之后把队首最大元素放到队尾,队尾元素放到堆首,进行下滤操作,堆长度-1。循环倒数第二个元素重复操作。这样队尾元素从后往前依次形成最大,次大,次次大,这种结构,完成了从小到大的排序。

    protected void sort() {
        // 原地建堆
        heapSize = array.length;
        for (int i = (heapSize >> 1) - 1; i >= 0; i--) {
            siftDown(i);
        }

        while (heapSize > 1) {
            // 交换堆顶元素和尾部元素
            swap(0, --heapSize);

            // 对0位置进行siftDown(恢复堆的性质)
            siftDown(0);
        }
    }

四、刷题

leetcode 持续补充

1.215.数组中第k个最大元素

思路:原地建堆,然后删除k-1个元素,堆顶就是最大。
注意:书写过程,按照自己的理解,考虑所有分支。主要是siftdown写法。
堆难在

public static int findKthLargest1(int[] nums, int k) {
        // 1、原地建堆,从最后一个非叶子节点(n/2)开始siftdown
        for(int i = nums.length/2;i>=0;i--){
            siftDown(nums,i,nums.length);
        }
        // 2、删除堆顶元素k-1次,删除后用最后一个元素siftDown
        int heapSize = nums.length;
        for (int i = 0; i < k-1; i++) {
            remove(nums,heapSize-i);
        }

        return nums[0];
    }

    public static void siftDown(int[] nums,int index,int heapSize){
        while (index< heapSize/2){
            int indexValue = nums[index];
            // 是否有左右子节点,以及左右子节点找到最大的,和index比较
            int  lchildIndex = index * 2 + 1;
            if(lchildIndex >= heapSize){
                break;
            }else{
                int lchildValue = nums[lchildIndex];
                int  rightIndex = index * 2 + 2;
                if(rightIndex >= heapSize){
                    // 比较index和左子节点,如果index较大,则break,否则swap,并且index更新为做子节点的index。
                    if(lchildValue > indexValue){
                        swap(nums,lchildIndex,index);
                        index = lchildIndex;
                    }else{
                        break;
                    }
                }else{
                    // 比较index和左右子节点,逻辑相同
                    int rchildValue = nums[rightIndex];

                    int largestindex = index;
                    int largestValue = indexValue;
                    if(lchildValue > indexValue){
                        largestindex = lchildIndex;
                        largestValue = lchildValue;
                    }

                    if(rchildValue > largestValue){
                        largestindex = rightIndex;
                        largestValue = rchildValue;
                    }
                    if(largestindex == index){
                        break;
                    }else{
                        swap(nums,index,largestindex);
                        index = largestindex;
                    }
                }
            }

        }
    }

    public static void remove(int[]nums,int heapSize){
        swap(nums,heapSize-1,0);
        siftDown(nums,0,heapSize -1);
    }

    public static void swap(int[] nums,int i,int j){
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335

推荐阅读更多精彩内容