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