堆的链式实现
1,堆的特征
1)堆是一颗完全二叉树,这跟前面的二叉树,二叉查找树均不同,这意味着,建堆的时候,堆的插入位置是按顺序固定的
2)每个节点小于或等于其左右子节点(最小堆)
3)堆中的任意一颗子树也是一个堆,注意概念都是递归定义的
2,堆实现的接口
为了便于来测试写的堆是否正确,除了实现接口中的操作外,还实现了前面二叉树里面的3种遍历迭代器(用来检测堆是否正确),实现方法完全一样,直接复制过来的,在堆的学习中,不再详述,只实现堆作为堆特有的操作。
package Heap;
import Tree.BinaryTreeADT;
public interface HeapADT{
public void addElement(Comparable element);
public Comparable findMin();
public Comparable removeMin();
}
3,堆结点的定义
除了跟二叉树一样要维护一个左右指针外,还要维护一个parent指针,在add操作分析里将说明之。
package Heap;
import Tree.BinaryTreeNode;
public class HeapNode{
private Comparable element;
private HeapNode left,right,parent;
public HeapNode(Comparable element)
{
this.element = element;
left = null;
right = null;
parent = null;
}
public Comparable getElement()
{
return element;
}
public void setElement(Comparable element)
{
this.element = element;
}
public HeapNode getLeftNode()
{
return left;
}
public void setLeftNode(HeapNode left)
{
this.left = left;
}
public HeapNode getRightNode()
{
return right;
}
public void setRightNode(HeapNode right)
{
this.right = right;
}
public HeapNode getParentNode()
{
return parent;
}
public void setParentNode(HeapNode parent)
{
this.parent = parent;
}
}
4,堆的实现分析
1)实例变量和构造方法
private HeapNode root;
private int count;
private HeapNode lastNode;
public LinkedHeap()
{
root = null;
count = 0;
lastNode = null;
}
public LinkedHeap(Comparable element)
{
HeapNode node = new HeapNode(element);
root = node;
count++;
lastNode = root;
}
在后面的分析中会介绍为什么要记录lastNode
2)堆的插入(建堆)---public void addElement(Comparable element)
---addElement将给定元素添加到合适的位置上,并维护堆的完整性和有序性
---因为堆是一颗完全二叉树,所以插入位置时固定的,即下一个空闲位置,可能有两种情况,h层满或者还在h层中
----以上分析表明:插入节点由2个步骤组成,a,将元素插入到固定位置。 b,调整堆的顺序
a,插入到固定位置,为了便于实现,我们记录lastNode为当前堆中的最后一个结点,如果lastNode是一个左节点(自己定义的一个概念,即为某个节点的左子节点),那么下一个插入位置肯定是对应的右节点(在本层中)。如果lastNode是一个右节点,情况就复杂一点,下一个插入结点可能在本层中,也可能是下一层的第一个结点:从lastNode往上找父节点,直到找到找到一个左节点,那么这个左节点的父节点的右子树上的最左结点就是下一个要插入的位置(而且插入元素是插入为它的左结点)。或者一直找到了root也没有找到一个左节点,那么说明h层已满,下一个插入位置在h+1层第一个。
b,自底向上调整堆,这是由于这个,才记录了一个parent指针。
不断地将新插入的结点与其父节点比较,如果小于父节点,就交换双方的位置,直到新值大于其父节点或者到达root。
将这两个过程分离开,定义为2个方法:
public void addElement(Comparable element) {//2个private支持方法
// TODO Auto-generated method stub
HeapNode node = new HeapNode(element);
if(isEmpty())//当前树为空,还没有任何结点
{
root = node;
lastNode = root;
count++;
}
else if(lastNode == root)//只有1个结点root
{
root.setLeftNode(node);
node.setParentNode(root);
lastNode = node;
count++;
}
else
{
HeapNode insertNode = getInsertNode();//得到要插入在之下的那个结点
if(insertNode.getLeftNode() == null)
{
insertNode.setLeftNode(node);
node.setParentNode(insertNode);
lastNode = node;
count++;
}
else
{
insertNode.setRightNode(node);
node.setParentNode(insertNode);
lastNode = node;
count++;
}
//upAdjustHeap();//从lastnode向上调整堆
}
upAdjustHeap();//从lastnode向上调整堆
}
private HeapNode getInsertNode(){//至少有2个结点的情况下求插入结点
}
private void upAdjustHeap(){//从lastNode往上调整堆
}
这是一种很好的解决问题的思路,当一个过程复杂的时候,把它分解为一个有意义的小过程,然后来调用,不仅显得条理清晰,而且复用性好,在具体的实现中,可以先把主方法写好,支持方法写成空方法,写完后来思考支持方法怎么去实现。
下面是这2个方法的具体实现,在上面的分析中已经比较清楚了,
求插入到的那个节点(即插入为谁的子节点)
private HeapNode getInsertNode(){//至少有2个结点的情况下求插入结点
HeapNode result = null;
if(lastNode.getParentNode().getLeftNode() == lastNode)//lastNode是一个左节点
result = lastNode.getParentNode();
else //lastNode是一个右节点
{
HeapNode temp = lastNode;//用temp来往上找是某个节点的左节点的节点
while(temp != root && temp.getParentNode().getRightNode() == temp)
temp = temp.getParentNode();//此时temp是一个左节点了或者到了root
if(temp == root)//h层已满
{
result = root;
while(result.getLeftNode() != null)
result = result.getLeftNode();
}
else
{
result = temp.getParentNode().getRightNode();//上述temp的父节点的右子树的根节点
while(result.getLeftNode() != null)
result = result.getLeftNode();
}
}
return result;
}
这个还可以根据堆的特征做一些改进,比如while那里,其实根据堆的特征可知不用while也可以找到(完全树决定了2层之间的距离,不需要往下找许多)
自下往上调整堆:
private void upAdjustHeap(){//从lastNode往上调整堆
HeapNode temp = lastNode;
while(temp != root)
{
if(temp.getElement().compareTo(temp.getParentNode().getElement()) < 0)
{
Comparable ele = temp.getElement();
temp.setElement(temp.getParentNode().getElement());
//temp.getParentNode().setElement(temp.getElement());//temp值已改变
temp.getParentNode().setElement(ele);
temp = temp.getParentNode();
}
else break;
}
}
没什么好说了,就是从下往上不断地跟父节点比较,交换
3)删除堆顶元素(最小值)
由于堆的完全二叉树特性,删除堆顶元素后,也是分两个步骤来处理:a,将lastNode放到堆顶来,b,从堆顶向下调整堆
a,将lastNode放到堆顶,由于要维护lastNode,我们在将lastNode的值放到堆顶后,还要找到新的lastNode,即原来的倒数第二个节点。这个过程类似于上面的寻找插入节点,但不太一样。
b,从堆顶向下调整堆:注意的是从上往下调整和从下往上调整不一样,往下调整有2个子节点,如果2个子节点都比父节点小,要和更小的一个交换,这个十分重要,否则交换上去没有保持堆的顺序。
public Comparable removeMin() {
Comparable result = null;
if(isEmpty())
{
System.out.println("当前树为空");
return null;
}
else if(size() == 1) //如果只有一个节点
{
result = root.getElement();
root = null;
lastNode = null;
count--;
}
else //至少有2个节点
{
result = root.getElement();
root.setElement(lastNode.getElement());
HeapNode newLastNode = getNewLastNode();//得到删除lastNode后的新的lastNode
//lastNode = null;//??????????????????可以将最后一个元素置为null吗 不能!!!!!
if(lastNode.getParentNode().getLeftNode() == lastNode)//正确的方法
lastNode.getParentNode().setLeftNode(null);
else lastNode.getParentNode().setRightNode(null);
lastNode = newLastNode;
//downAdjustHeap();//从堆顶向下调整堆
count--;
}
downAdjustHeap(root);//从堆顶向下调整堆
return result;
}
private HeapNode getNewLastNode(){//至少有2个节点的情况下返回当前堆的倒数第二个节点
return newLastNode;
}
private void downAdjustHeap(HeapNode root){//从堆顶自上向下调整堆
}
得到倒数第二个节点的方法:
private HeapNode getNewLastNode(){//至少有2个节点的情况下返回当前堆的倒数第二个节点
HeapNode newLastNode = null;
if(lastNode.getParentNode().getRightNode() == lastNode) //lastNode是右节点
newLastNode = lastNode.getParentNode().getLeftNode();
else //lastNode是左节点
{
HeapNode temp = lastNode;
while(temp != root && temp.getParentNode().getLeftNode() == temp)
temp = temp.getParentNode(); //此时temp是一个右节点或者到达root
if(temp == root) //说明此时newLastNode在上一层最后一个
{
while(temp.getRightNode() != null)
temp = temp.getRightNode();
newLastNode = temp;
}
else
{
newLastNode = temp.getParentNode().getLeftNode();
while(newLastNode.getRightNode() != null)
newLastNode = newLastNode.getRightNode();
}
}
return newLastNode;
}
一个递归的调整以某个结点为根的堆的方法:
private void downAdjustHeap(HeapNode root){//从堆顶自上向下调整堆
HeapNode temp = root;
HeapNode left = temp.getLeftNode();
HeapNode right = temp.getRightNode();
if(left != null && right != null) //左右都存在
{
if(left.getElement().compareTo(right.getElement()) < 0) //左比右小(如果左右都比根小,要交换较小的一个)
{
if(left.getElement().compareTo(temp.getElement()) < 0)
{
Comparable ele = temp.getElement();
temp.setElement(temp.getLeftNode().getElement());
temp.getLeftNode().setElement(ele);
downAdjustHeap(temp.getLeftNode()); //递归对子树调整
}
else return;
}
else //右比左小
{
if(right.getElement().compareTo(temp.getElement()) < 0)
{
Comparable ele = temp.getElement();
temp.setElement(temp.getRightNode().getElement());
temp.getRightNode().setElement(ele);
downAdjustHeap(temp.getRightNode());
}
return;
}
}
else if(left != null && right == null) //左存在,右不存在(不可能左不存在右存在)
{
if(left.getElement().compareTo(temp.getElement()) < 0)
{
Comparable ele = temp.getElement();
temp.setElement(temp.getLeftNode().getElement());
temp.getLeftNode().setElement(ele);
downAdjustHeap(temp.getLeftNode()); //也可以不需要再调整,直接return
}
else return;
}
else return; //都不存在
}
调整某个元素为根的堆,这是一个递归的方法,交换它跟它的子节点后,再来调整以它的子节点为根的堆。
复杂的地方在于怎么去分情况,一个根节点是可能有2个子节点的,刚开始写了许多if else 非常混乱,后来想清楚了,按照子节点的个数来分类会显得很清晰,
另外,我看了下书上的实现方法,非常不好,不是用的递归,if else更是很多,情况分类非常多,代码可读性很差,觉得写成递归,且这样分类会显得好多了
5,完整代码及测试
package Heap;
import java.util.Iterator;
import Queue.LinkedQueue;
import Tree.BinaryTreeNode;
public class LinkedHeap implements HeapADT {
private HeapNode root;
private int count;
private HeapNode lastNode;
public LinkedHeap()
{
root = null;
count = 0;
lastNode = null;
}
public LinkedHeap(Comparable element)
{
HeapNode node = new HeapNode(element);
root = node;
count++;
lastNode = root;
}
public void addElement(Comparable element) {//2个private支持方法
// TODO Auto-generated method stub
HeapNode node = new HeapNode(element);
if(isEmpty())//当前树为空,还没有任何结点
{
root = node;
lastNode = root;
count++;
}
else if(lastNode == root)//只有1个结点root
{
root.setLeftNode(node);
node.setParentNode(root);
lastNode = node;
count++;
}
else
{
HeapNode insertNode = getInsertNode();//得到要插入在之下的那个结点
if(insertNode.getLeftNode() == null)
{
insertNode.setLeftNode(node);
node.setParentNode(insertNode);
lastNode = node;
count++;
}
else
{
insertNode.setRightNode(node);
node.setParentNode(insertNode);
lastNode = node;
count++;
}
//upAdjustHeap();//从lastnode向上调整堆
}
upAdjustHeap();//从lastnode向上调整堆
}
private HeapNode getInsertNode(){//至少有2个结点的情况下求插入结点
HeapNode result = null;
if(lastNode.getParentNode().getLeftNode() == lastNode)//lastNode是一个左节点
result = lastNode.getParentNode();
else //lastNode是一个右节点
{
HeapNode temp = lastNode;//用temp来往上找是某个节点的左节点的节点
while(temp != root && temp.getParentNode().getRightNode() == temp)
temp = temp.getParentNode();//此时temp是一个左节点了或者到了root
if(temp == root)//h层已满
{
result = root;
while(result.getLeftNode() != null)
result = result.getLeftNode();
}
else
{
result = temp.getParentNode().getRightNode();//上述temp的父节点的右子树的根节点
while(result.getLeftNode() != null)
result = result.getLeftNode();
}
}
return result;
}
private void upAdjustHeap(){//从lastNode往上调整堆
HeapNode temp = lastNode;
while(temp != root)
{
if(temp.getElement().compareTo(temp.getParentNode().getElement()) < 0)
{
Comparable ele = temp.getElement();
temp.setElement(temp.getParentNode().getElement());
//temp.getParentNode().setElement(temp.getElement());//temp值已改变
temp.getParentNode().setElement(ele);
temp = temp.getParentNode();
}
else break;
}
}
public Comparable removeMin() {
Comparable result = null;
if(isEmpty())
{
System.out.println("当前树为空");
return null;
}
else if(size() == 1) //如果只有一个节点
{
result = root.getElement();
root = null;
lastNode = null;
count--;
}
else //至少有2个节点
{
result = root.getElement();
root.setElement(lastNode.getElement());
HeapNode newLastNode = getNewLastNode();//得到删除lastNode后的新的lastNode
//lastNode = null;//??????????????????可以将最后一个元素置为null吗 不能!!!!!
if(lastNode.getParentNode().getLeftNode() == lastNode)//正确的方法
lastNode.getParentNode().setLeftNode(null);
else lastNode.getParentNode().setRightNode(null);
lastNode = newLastNode;
//downAdjustHeap();//从堆顶向下调整堆
count--;
}
downAdjustHeap(root);//从堆顶向下调整堆
return result;
}
private HeapNode getNewLastNode(){//至少有2个节点的情况下返回当前堆的倒数第二个节点
HeapNode newLastNode = null;
if(lastNode.getParentNode().getRightNode() == lastNode) //lastNode是右节点
newLastNode = lastNode.getParentNode().getLeftNode();
else //lastNode是左节点
{
HeapNode temp = lastNode;
while(temp != root && temp.getParentNode().getLeftNode() == temp)
temp = temp.getParentNode(); //此时temp是一个右节点或者到达root
if(temp == root) //说明此时newLastNode在上一层最后一个
{
while(temp.getRightNode() != null)
temp = temp.getRightNode();
newLastNode = temp;
}
else
{
newLastNode = temp.getParentNode().getLeftNode();
while(newLastNode.getRightNode() != null)
newLastNode = newLastNode.getRightNode();
}
}
return newLastNode;
}
private void downAdjustHeap(HeapNode root){//从堆顶自上向下调整堆
HeapNode temp = root;
HeapNode left = temp.getLeftNode();
HeapNode right = temp.getRightNode();
if(left != null && right != null) //左右都存在
{
if(left.getElement().compareTo(right.getElement()) < 0) //左比右小(如果左右都比根小,要交换较小的一个)
{
if(left.getElement().compareTo(temp.getElement()) < 0)
{
Comparable ele = temp.getElement();
temp.setElement(temp.getLeftNode().getElement());
temp.getLeftNode().setElement(ele);
downAdjustHeap(temp.getLeftNode()); //递归对子树调整
}
else return;
}
else //右比左小
{
if(right.getElement().compareTo(temp.getElement()) < 0)
{
Comparable ele = temp.getElement();
temp.setElement(temp.getRightNode().getElement());
temp.getRightNode().setElement(ele);
downAdjustHeap(temp.getRightNode());
}
return;
}
}
else if(left != null && right == null) //左存在,右不存在(不可能左不存在右存在)
{
if(left.getElement().compareTo(temp.getElement()) < 0)
{
Comparable ele = temp.getElement();
temp.setElement(temp.getLeftNode().getElement());
temp.getLeftNode().setElement(ele);
downAdjustHeap(temp.getLeftNode()); //也可以不需要再调整,直接return
}
else return;
}
else return; //都不存在
}
public Comparable findMin() {
return root.getElement();
}
//只保留3个迭代器方法,可以测试堆的方法就行
public int size() {
return count;
}
public boolean isEmpty() {
return (count == 0);
}
public Iterator iteratorInorder() {
LinkedQueue queue = new LinkedQueue();
inorder(root,queue);//将以root为根的树按序进队
return queue.iterator();//返回队列的迭代器
}
private void inorder(HeapNode node,LinkedQueue queue){
if(node != null)
{
inorder(node.getLeftNode(),queue);
queue.enqueue(node.getElement());
inorder(node.getRightNode(),queue);
}
}
public Iterator PreInorder() {
LinkedQueue queue = new LinkedQueue();
preorder(root,queue);
return queue.iterator();
}
private void preorder(HeapNode node,LinkedQueue queue){
if(node != null)
{
queue.enqueue(node.getElement());
preorder(node.getLeftNode(),queue);
preorder(node.getRightNode(),queue);
}
}
public Iterator PostInorder() {
LinkedQueue queue = new LinkedQueue();
postorder(root,queue);
return queue.iterator();
}
private void postorder(HeapNode node,LinkedQueue queue){
if(node != null)
{
postorder(node.getLeftNode(),queue);
postorder(node.getRightNode(),queue);
queue.enqueue(node.getElement());
}
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
LinkedHeap heap = new LinkedHeap();
heap.addElement(12);
heap.addElement(17);
heap.addElement(11);
heap.addElement(4);
heap.addElement(10);
heap.addElement(6);
heap.addElement(8);
heap.addElement(22);
heap.addElement(16);
heap.addElement(5);
heap.addElement(2);
heap.addElement(9);
heap.addElement(7);
heap.addElement(20);
heap.addElement(28);
heap.addElement(13);
heap.addElement(15);
System.out.println("堆的大小为: " + heap.size());
System.out.println("堆为空吗?: " + heap.isEmpty());
System.out.println("\n\n中序序列为: ");
Iterator it = heap.iteratorInorder();
while(it.hasNext())
System.out.print(it.next() + " ");
System.out.println("\n前序序列为: ");
it = heap.PreInorder();
while(it.hasNext())
System.out.print(it.next() + " ");
System.out.println("\n后序序列为: ");
it = heap.PostInorder();
while(it.hasNext())
System.out.print(it.next() + " ");
heap.removeMin();
System.out.println("\n\n\n删除最小值后前序序列为: ");
it = heap.PreInorder();
while(it.hasNext())
System.out.print(it.next() + " ");
heap.removeMin();
System.out.println("\n接着删除最小值后前序序列为: ");
it = heap.PreInorder();
while(it.hasNext())
System.out.print(it.next() + " ");
heap.removeMin();
System.out.println("\n接着删除最小值后前序序列为: ");
it = heap.PreInorder();
while(it.hasNext())
System.out.print(it.next() + " ");
}
}
堆的形状也是跟插入顺序有关的,构造了下面的堆:
结果:
堆的大小为: 17
堆为空吗?: false
中序序列为:
22 15 16 13 17 4 11 5 10 2 12 7 9 6 20 8 28
前序序列为:
2 4 13 15 22 16 17 5 11 10 6 7 12 9 8 20 28
后序序列为:
22 16 15 17 13 11 10 5 4 12 9 7 20 28 8 6 2
删除最小值后前序序列为:
4 5 13 15 22 17 10 11 16 6 7 12 9 8 20 28
接着删除最小值后前序序列为:
5 10 13 15 17 11 22 16 6 7 12 9 8 20 28
接着删除最小值后前序序列为:
6 10 13 15 17 11 22 16 7 9 12 28 8 20
总结:堆的实现还是非常复杂的,一定要多想几遍建堆(插入)和删除堆顶这2个操作
前者插入位置是固定的,包含2个过程:将插入元素放到找到的位置上,(这个位置需要一个方法去找),然后从下往上调整堆
后者将最后一个元素替代堆顶,删除最后一个元素(删除后要设置新的最后一个元素,即需要一个方法去找原来的倒数第二个元素),然后自上向下调整堆
这里有2个调整堆的方法,是不一样的,从下往上只需要跟父节点比较即可,从上往下就复杂许多,它有2个子节点