【相当困难】设计一个没有扩容负担的堆结构-Java

文章介绍了一种无需扩容的堆结构实现,通过完全二叉树形式,保证了添加和删除元素的时间复杂度不超过O(logN)。该结构支持小根堆和大根堆,提供了getHead、getSize、add和popHead等方法,并详细阐述了add和popHead的实现逻辑,涉及节点的插入、调整以及堆的维护过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

分享一个大牛的人工智能教程。零基础!通俗易懂!风趣幽默!希望你也加入到人工智能的队伍中来!请轻击人工智能教程大家好!欢迎来到我的网站! 人工智能被认为是一种拯救世界、终结世界的技术。毋庸置疑,人工智能时代就要来临了,科… 继续阅读 前言https://www.captainai.net/troubleshooter

package live.every.day.ProgrammingDesign.CodingInterviewGuide.Other;

import java.util.Comparator;

/**
 * 设计一个没有扩容负担的堆结构
 *
 * 【题目】
 * 堆结构一般是使用固定长度的数组结构来实现的。这样的实现虽然足够经典,但存在扩容的负担,比如不断向堆中增加元素,使得固定
 * 数组快耗尽时,就不得不申请一个更大的固定数组,然后把原来数组中的对象复制到新的数组里完成堆的扩容,所以,如果扩容时堆中
 * 的元素个数为N,那么扩容行为的时间复杂度为O(N)。请设计一种没有扩容负担的堆结构,即在任何时刻有关堆的操作时间复杂度都不
 * 超过O(logN)。
 *
 * 【要求】
 * 1.没有扩容的负担。
 * 2.可以生成小根堆,也可以生成大根堆。
 * 3.包含getHead方法,返回当前堆顶的值。
 * 4.包含getSize方法,返回当前堆的大小。
 * 5.包含add(x)方法,即向堆中新加元素x,操作后依然是小根堆/大根堆。
 * 6.包含popHead方法,即删除并返回堆顶的值,操作后依然是小根堆/大根堆。
 * 7.如果堆中的节点个数为N,那么各个方法的时间复杂度为:
 * getHead:O(1)。
 * getSize:O(1)。
 * add:O(logN)。
 * popHead:O(logN)。
 *
 * 【难度】
 * 相当困难
 *
 * 【解答】
 * 本题的设计方法有很多,本文提供的方法实际上是实现了完全二叉树结构,并含有堆的调整过程。二叉树的节点类型如下,比经典的二
 * 叉树节点多一条指向父节点的parent指针。
 * 本书实现的堆结构叫MyHeap类,MyHeap中有四个重要的组成部分。
 * •head:Node类型的变量,表示当前堆的头节点。
 * •last:Node类型的变量,表示当前堆的堆尾节点,也就是最后一排的最右节点。
 * •size:整型变量,表示当前推的大小。
 * •comp:继承了Comparator接口的比较器类型的变量。在构造MyHeap实例时由用户定义,通过定义堆中元素的比较方式,自然可以
 * 将推实现成大根堆或小根推。comp变量是在构造时一经设定就不能更改。
 * 所有堆的操作在执行时,变量head、last和size都能够正确更新是MyHeap类实现的重点。其中getHead方法和getSize方法是很
 * 容易实现的,就是直接取值返回即可。那么接下来就重点介绍add方法和popHead方法的实现细节。
 *
 * add方法的实现。如果想要把元素value加入到堆中,首先生成二叉树节点类型的实例,即new Node<value的类型>(value),假设
 * 生成的节点为newNode。把newNode加到二叉树上的具体过程如下:
 * 1.如果size==0,说明当前的堆没有节点,三个变量简单赋值即可。
 * 2.如果size>0,说明当前的堆有节点,此时想要加上newNode的困难在于,不知道newNode应该加到二叉树的什么位置。此时利用
 * last的位置来找到newNode应该加的位置。
 * 1)last具体在堆中的什么位置特别关键,具体有如下三种情况:
 * 情况一,last是当前层的最后一个节点,也就是当前层已经满,无法再加新的节点,那么newNode应该加在新一层最左的位置。
 * 情況二,如果last是last父节点的左孩子,那么newNode应该加在last父节点的右孩子的位置。
 * 情况三,如果last既不是情況一,也不是情况二,则参见图9-7。
 * 图9-7代表情况三,即当前层并没有添加满,但是last的父节点(比如图中的D节点)已经添加满,此时需要一个向上寻找的过程。先以
 * last作为当前节点,然后看看当前节点是不是当前节点的父节点的左孩子,如果不是,就一直向上。比如图9-7中的节点1,它不是其
 * 父节点的左孩子,那么向上寻找开始,节点又成为当前节点。此时发现节点D是其父节点(即节点B)的左孩子,此时寻找结束。新节点
 * newNode应该加在节点B的右子树的最左节点的左孩子的位置上,即节点E的左孩子位置。下面再举一例,如图9-8所示。
 * 图9-8中last节点是节点K,如何找到newNode应该加的位置呢?和图9-7的方式相同,也是往上寻找的过程。开始时当前节点为节点
 * K,发现它不是其父节点(E)的左孩子,那么节点E变成当前节点,发现也不是其父节点(B)的左孩子,那么节点B变成当前节点,发现节
 * 点B是其父节点A的左孩子,此时向上的过程停止。新节点newNode应该加在节点A的右子树的最左节点的左孩子的位置上,即节点F的
 * 左孩子位置。
 * 2)加完newNode之后,newNode就成为新的last,令last=newNode,同时size++。
 * 3)此时的last节点就是新加节点,虽然加在了二叉树上,但还没有经历建堆的调整过程。比如,如果整个堆是大根堆,而新加节点的
 * 值又很大,按道理,这个节点应该经历向上交换的过程,所以最后应该从last节点向上经历堆的调整过程,即heapInsert过程。同时
 * 需要特别注意的是,在交换的过程中,last和head的值可能会变化,如图9-9所示。
 * 假设加上新节点(值为8的节点)之后的完全二叉树如图9-9所示,很明显,last节点需要往上调整的过程。调整之后的二叉树应该为图
 * 9-10。
 * 如果在经历调整之后,新加的节点最后没有占据头节点的位置,那么head的值当然是不用改变的,但如果最后占据了头节点的位置,则
 * head的值应该调整,比如图9-10中head的值应该变为节点8。同理,如果在经历调整时发现,新加的节点并不比它的父节点大,说明
 * 新加的节点不需要向上移动,那么last的值当然还是新加的节点,但如果新加的节点需要向上移动,比如图9-10,那么last的值也需
 * 要调整,应该设为新加的节点的父节点(图9-10中的节点6)。只有head和last在调整的每一步都正确地更新,整个设计才能不出错。
 * 具体请参看如下代码中MyHeap类实现的heapInsertModify方法。
 *
 * popHead方法的实现。删除堆顶节点并返回堆顶的值,具体过程如下:
 * 1.如果size==0,说明当前堆为空,直接返回null,也不需要任何调整。
 * 2.如果size==1,说明当前堆里只有一个节点,返回节点值并将堆清空,即如下代码。
 * 3.如果size>1,把当前堆顶节点记为res,把最后一个元素(last)放在堆顶位置作为新的头,同时从头部开始进行堆的调整,使其继
 * 续是大根/小根堆,最后返回res.value即可。话虽如此,但是这个过程还是要保证head和last的正确更新,具体细节如下:
 * 1)先把堆中最后一个节点(last)和整个堆结构断开,记为oldLast。因为oldLast要放在头节点的位置,所以last的值应该变成
 * oldLast节点之前的那个节点,同样有三种情况。
 * 情况一,如果oldLast在断开之前是其所在层的最左节点,那么在断开之后,last应该变为上一层的最右节点。
 * 情况二,如果oldLast在断开之前是oldLast的父节点的右孩子,那么在断开之后,last应该变为oldLast的父节点的左孩子。
 * 情况三,除情况一和情况二外,还有一种情况,如图9-11所示。
 * 图9-11代表了情况三,即oldLast并不是当前层的最左节点,也不是其父节点的右孩子,此时需要一个向上寻找的过程。先以
 * oldLast作为当前节点,然后看当前节点是不是当前节点的父节点的右孩子,如果不是,就一直向上。比如,图9-11中的节点J,它不
 * 是其父节点的右孩子,那么向上寻找开始,节点E成为当前节点,此时发现节点E是其父节点(即节点B)的右孩子,寻找结束。last节点
 * 应该设成节点B的左子树的最右节点(即节点I)。我们再举一例,如图9-12所示。
 * 图9-12中的oldLast节点是节点L,如何设置last节点的值呢?和图9-11的方式相同,也是往上寻找的过程。开始时当前节点为节点
 * L,发现它不是其父节点(F)的右孩子,那么节点F变成当前节点,发现也不是其父节点(C)的右孩子,那么节点C变成当前节点,发现节
 * 点C是其父节点A的右孩子,此时向上的过程停止。Last节点应该设成节点A的左子树的最右节点,即节点K。步骤1)的具体过程请参看
 * MyHeap类实现的popLastAndSetPreviousLast方法。
 * 2)断开oldLast节点后,堆中的元素少了一个,所以size减1。如果size在减1之后有size==1,说明一开始堆的大小为2,断开
 * oldLast之后堆中只剩一个头节点。那么此时令oldLast作为新的头节点,并返回旧的头节点的值即可,代码如下。
 * 3)如果断开oldLast节点后,size依然大于1。那么将oldLast设成新的头节点,然后从堆顶开始往下调整堆结构,即heapify的过
 * 程,此时依然要注意head和last可能改变的情况,因为调整的过程中新的头节点(即oldLast)还可能会移动,使得head和last位置
 * 上的节点发生变化,具体过程请参看MyHeap类实现的heapify方法。
 *
 * MyHeap类的设计就介绍完了,与经典堆结构是一个数组结构不同的是,MyHeap类是一个完全二叉树结构,所以两个相邻节点在交换位
 * 置时的处理会更复杂,都考虑彼此的拓扑关系,才能做到正确地进行交换。具体请参看MyHeap类实现的swapClosedTwoNodes方法。
 * 当然也可以不进行结构上的交换,而只是交换两个节点的值,即Node.value。
 * add和popHead方法的所有操作都是在完全二叉树的一条或两条路径上进行的操作,所以每一个操作的代价都是完全二叉树的高度级别,
 * 一个节点数为N的完全二叉树高度为O(logN),所以add和popHead方法的时间复杂度为O(logN)。MyHeap类的全部实现如下:
 *
 * @author Created by LiveEveryDay
 */

public class DesignOneNoCapacityExpansionBurdenHeapStructure {

    public static class Node<K> {
        public K value;
        public Node<K> left;
        public Node<K> right;
        public Node<K> parent;

        public Node(K data) {
            value = data;
        }
    }

    public static class MyHeap<K> {
        private Node<K> head; // 堆头节点
        private Node<K> last; // 堆尾节点
        private long size; // 当前堆的大小
        private final Comparator<K> comp; // 大根堆或小根堆

        public MyHeap(Comparator<K> compare) {
            head = null;
            last = null;
            size = 0;
            comp = compare; // 基于比较器决定是大根堆还是小根堆
        }

        public K getHead() {
            return head == null ? null : head.value;
        }

        public long getSize() {
            return size;
        }

        public boolean isEmpty() {
            return size == 0 ? true : false;
        }

        // 添加一个新节点到堆中
        public void add(K value) {
            Node<K> newNode = new Node<>(value);
            if (size == 0) {
                head = newNode;
                last = newNode;
                size++;
                return;
            }
            Node<K> node = last;
            Node<K> parent = node.parent;
            // 找到正确的位置并插入到新节点
            while (parent != null && node != parent.left) {
                node = parent;
                parent = node.parent;
            }
            Node<K> nodeToAdd = null;
            if (parent == null) {
                nodeToAdd = mostLeft(head);
                nodeToAdd.left = newNode;
                newNode.parent = nodeToAdd;
            } else if (parent.right == null) {
                parent.right = newNode;
                newNode.parent = parent;
            } else {
                nodeToAdd = mostLeft(parent.right);
                nodeToAdd.left = newNode;
                newNode.parent = nodeToAdd;
            }
            last = newNode;
            // 建堆过程及其调整
            heapInsertModify();
            size++;
        }

        public K popHead() {
            if (size == 0) {
                return null;
            }
            Node<K> res = head;
            if (size == 1) {
                head = null;
                last = null;
                size--;
                return res.value;
            }
            Node<K> oldLast = popLastAndSetPreviousLast();
            // 如果弹出堆尾节点后,堆的大小等于1的处理
            if (size == 1) {
                head = oldLast;
                last = oldLast;
                return res.value;
            }
            // 如果弹出堆尾节点后,堆的大小大于1的处理
            Node<K> headLeft = res.left;
            Node<K> headRight = res.right;
            oldLast.left = headLeft;
            if (headLeft != null) {
                headLeft.parent = oldLast;
            }
            oldLast.right = headRight;
            if (headRight != null) {
                headRight.parent = oldLast;
            }
            res.left = null;
            res.right = null;
            head = oldLast;
            // 堆heapify过程
            heapify(oldLast);
            return res.value;
        }

        // 找到以node为头的子树中,最左的节点
        private Node<K> mostLeft(Node<K> node) {
            while (node.left != null) {
                node = node.left;
            }
            return node;
        }

        // 找到node为头的子树中,最右的节点
        private Node<K> mostRight(Node<K> node) {
            while (node.right != null) {
                node = node.right;
            }
            return node;
        }

        // 建堆及调整的过程
        private void heapInsertModify() {
            Node<K> node = last;
            Node<K> parent = node.parent;
            if (parent != null && comp.compare(node.value, parent.value) < 0) {
                last = parent;
            }
            while (parent != null && comp.compare(node.value, parent.value) < 0) {
                swapClosedTwoNodes(node, parent);
                parent = node.parent;
            }
            if (head.parent != null) {
                head = head.parent;
            }
        }

        // 堆heapify过程
        private void heapify(Node<K> node) {
            Node<K> left = node.left;
            Node<K> right = node.right;
            Node<K> most = node;
            while (left != null) {
                if (left != null && comp.compare(left.value, most.value) < 0) {
                    most = left;
                }
                if (right != null && comp.compare(right.value, most.value) < 0) {
                    most = right;
                }
                if (most != node) {
                    swapClosedTwoNodes(most, node);
                } else {
                    break;
                }
                left = node.left;
                right = node.right;
                most = node;
            }
            if (node.parent == last) {
                last = node;
            }
            while (node.parent != null) {
                node = node.parent;
            }
            head = node;
        }

        // 交换相邻的两个节点
        private void swapClosedTwoNodes(Node<K> node, Node<K> parent) {
            if (node == null || parent == null) {
                return;
            }
            Node<K> parentParent = parent.parent;
            Node<K> parentLeft = parent.left;
            Node<K> parentRight = parent.right;
            Node<K> nodeLeft = node.left;
            Node<K> nodeRight = node.right;
            node.parent = parentParent;
            if (parentParent != null) {
                if (parent == parentParent.left) {
                    parentParent.left = node;
                } else {
                    parentParent.right = node;
                }
            }
            parent.parent = node;
            if (nodeLeft != null) {
                nodeLeft.parent = parent;
            }
            if (nodeRight != null) {
                nodeRight.parent = parent;
            }
            if (node == parent.left) {
                node.left = parent;
                node.right = parentRight;
                if (parentRight != null) {
                    parentRight.parent = node;
                }
            } else {
                node.left = parentLeft;
                node.right = parent;
                if (parentLeft != null) {
                    parentLeft.parent = node;
                }
            }
            parent.left = nodeLeft;
            parent.right = nodeRight;
        }

        // 在树中弹出堆尾节点后,找到原来的倒数第二个节点设置成新的队尾节点
        private Node<K> popLastAndSetPreviousLast() {
            Node<K> node = last;
            Node<K> parent = node.parent;
            while (parent != null && node != parent.right) {
                node = parent;
                parent = node.parent;
            }
            if (parent == null) {
                node = last;
                parent = node.parent;
                node.parent = null;
                if (node == parent.left) {
                    parent.left = null;
                } else {
                    parent.right = null;
                }
                last = mostRight(head);
            } else {
                Node<K> newLast = mostRight(parent.left);
                node = last;
                parent = node.parent;
                node.parent = null;
                if (node == parent.left) {
                    parent.left = null;
                } else {
                    parent.right = null;
                }
                last = newLast;
            }
            size--;
            return node;
        }
    }

    public static void main(String[] args) {
        Comparator<Integer> comparator = Comparator.naturalOrder();
        MyHeap<Integer> myHeap = new MyHeap<>(comparator);
        myHeap.add(3);
        myHeap.add(4);
        myHeap.add(6);
        myHeap.add(7);
        myHeap.add(5);
        myHeap.add(6);
        myHeap.add(8);
        myHeap.popHead();
        System.out.printf("Size: %d%n", myHeap.getSize());
        System.out.printf("Head: %d%n", myHeap.getHead());
    }

}

// ------ Output ------
/*
Size: 6
Head: 4
*/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值