算法重拾-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;
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容