任一节点X的零路径长(null path length)npl(X)定义为到从X到一个不具有两个儿子的节点的最短路径的长。
不具有两个儿子的节点即是叶子节点和单孩子节点。
所以叶子节点和单孩子节点的npl都是0,因为自身到自身的距离为0.
规定空节点即null,则npl(null)为-1.此规定也为以下定理服务。
定理:任一节点的零路径长比它的两个儿子的零路径长的最小值大1,所以说叶子节点和单孩子节点的零路径长为0,因为这两种节点都有一个孩子是空节点,即儿子中npl最小值为-1,根据定理,这两种节点的npl=-1+1=0.
左式堆的性质:对于堆中的每一个节点X,左儿子的零路径长是大于等于右儿子的零路径长。
代码如下,注释帮助理解:
// LeftistHeap class
//
// CONSTRUCTION: with a negative infinity sentinel
//
// ******************PUBLIC OPERATIONS*********************
// void insert( x ) --> Insert x
// Comparable deleteMin( )--> Return and remove smallest item
// Comparable findMin( ) --> Return smallest item
// boolean isEmpty( ) --> Return true if empty; else false
// void makeEmpty( ) --> Remove all items
// void merge( rhs ) --> Absorb rhs into this heap
// ******************ERRORS********************************
// Throws UnderflowException as appropriate
/**
* Implements a leftist heap.
* Note that all "matching" is based on the compareTo method.
* @author Mark Allen Weiss
*/
public class LeftistHeap<AnyType extends Comparable<? super AnyType>>
{
/**
* Construct the leftist heap.
*/
public LeftistHeap( )
{
root = null;//构造函数,根为空
}
/**
* Merge rhs into the priority queue.
* rhs becomes empty. rhs must be different from this.
* @param rhs the other leftist heap.
*/
public void merge( LeftistHeap<AnyType> rhs )
{
if( this == rhs ) // Avoid aliasing problems如果是同样的两个堆
return;
root = merge( root, rhs.root );//重载进入参数为两个节点的函数。为俩个根节点
rhs.root = null;//此时传进来的rhs已经被合并,直接引用置空
}
/**
* Internal method to merge two roots.
* Deals with deviant cases and calls recursive merge1.
*/
private LeftistNode<AnyType> merge( LeftistNode<AnyType> h1, LeftistNode<AnyType> h2 )
{
if( h1 == null )//如果是第一次进入则是排除掉某个堆为空的情况
return h2; //如果不是第一次,那就可能是递归过程的终点之一
if( h2 == null )
return h1; //递归终点之一
if( h1.element.compareTo( h2.element ) < 0 )//将两个根进行比较,小的根为第一个参数,大的为第二个参数
return merge1( h1, h2 );//进入到实际的合并函数中
else
return merge1( h2, h1 );
}
/**
* Internal method to merge two roots.
* Assumes trees are not empty, and h1's root contains smallest item.
*/
private LeftistNode<AnyType> merge1( LeftistNode<AnyType> h1, LeftistNode<AnyType> h2 )
{
if( h1.left == null ) // Single node如果左子树为空,那么根据左式堆的性质,肯定是个单节点
h1.left = h2; // Other fields in h1 already accurate h2已经满足左式堆了,递归终点之一
//这个终点,其实就是,h1是小的根,肯定是把h2往h1上放,而h1.left == null其实就是h1为单节点,而这是
//merge1函数的唯一特殊情况
else
{
h1.right = merge( h1.right, h2 );
if( h1.left.npl < h1.right.npl ) //合并后如果左比右的零路径长小
swapChildren( h1 );
h1.npl = h1.right.npl + 1;//因为性质,节点的零路径长的等于两儿子节点的零路径长的最小值加1,而
//而左儿子的零路径长是>=右儿子的零路径长的,所以直接按右儿子的零路径长加1就好
}
return h1;
}
/**
* Swaps t's two children.
*/
private static <AnyType> void swapChildren( LeftistNode<AnyType> t )
{ //交换左右子树
LeftistNode<AnyType> tmp = t.left;
t.left = t.right;
t.right = tmp;
}
/**
* Insert into the priority queue, maintaining heap order.
* @param x the item to insert.
*/
public void insert( AnyType x )
{//插入单节点就是特殊的合并,直接调用真实的merge函数
root = merge( new LeftistNode<>( x ), root );//参数顺序并不重要,因为merge函数会进行比较的
}
/**
* Find the smallest item in the priority queue.
* @return the smallest item, or throw UnderflowException if empty.
*/
public AnyType findMin( )
{
if( isEmpty( ) )
throw new UnderflowException( );
return root.element;
}
/**
* Remove the smallest item from the priority queue.
* @return the smallest item, or throw UnderflowException if empty.
*/
public AnyType deleteMin( )
{
if( isEmpty( ) )
throw new UnderflowException( );
//删除就是删除根,再合并左右子树
AnyType minItem = root.element;
root = merge( root.left, root.right );
return minItem;
}
/**
* Test if the priority queue is logically empty.
* @return true if empty, false otherwise.
*/
public boolean isEmpty( )
{
return root == null;
}
/**
* Make the priority queue logically empty.
*/
public void makeEmpty( )
{
root = null;
}
private static class LeftistNode<AnyType>
{
// Constructors
LeftistNode( AnyType theElement )
{
this( theElement, null, null );
}
LeftistNode( AnyType theElement, LeftistNode<AnyType> lt, LeftistNode<AnyType> rt )
{
element = theElement;
left = lt;
right = rt;
npl = 0;
}
AnyType element; // The data in the node
LeftistNode<AnyType> left; // Left child
LeftistNode<AnyType> right; // Right child
int npl; // null path length
}
private LeftistNode<AnyType> root; // root
public static void main( String [ ] args )
{
int numItems = 100;
LeftistHeap<Integer> h = new LeftistHeap<>( );
LeftistHeap<Integer> h1 = new LeftistHeap<>( );
int i = 37;
for( i = 37; i != 0; i = ( i + 37 ) % numItems )
if( i % 2 == 0 )
h1.insert( i );
else
h.insert( i );
h.merge( h1 );//合并,最终你会发现堆里的数为1-99,这些数
for( i = 1; i < numItems; i++ )
if( h.deleteMin( ) == i )
System.out.println( "Oops! " + i );//你会发现根为1,删除掉根后,根为2,以此类推
}
}
在说一下主函数的for循环,看似很奇怪,循环出现的数字是99个看似乱序的数字,实际上观察后你会发现:
1.这些肯定是1-99的这99个数字的乱序。
2.第一次出现的数和最后一次的数加起来等于100,第二次和倒数第二次的数也是,以此类推。
其实可以这么理解:
数字以37的步伐开始填充1-99这99个位置(因为每次循环后就会37+37),第一次填充的数就是第一次循环的数,想象位置首尾相连,最终会把这些位置都填充完。
为什么出现的数没有0和100呢,是因为:0根本不可能,因为都是一直在加的,而加的数都是正数。100出现时是循环的终点,在最后一次循环,System.out.println(i),即输出了最后一个i值,然后执行i = ( i + 37 ) % numItems,而这里i+37加起来肯定等于100,因为是终点。所以最后输出的数肯定100-37即63.
如果你把循环里的37都改成别的数,分两种情况:
1.如果别的数不能被100整除,那么出现的数就是99个,即某种顺序的1-99
2.如果别的数能被100整除,那么出现的数的个数就是,(100/改的数)-1。道理很简单,因为能被整除,所以不会填充过程从尾跳到首的情况。
下面贴一下合并的图解过程:
这里可能有人会问,最后结果中,12节点的npl会不会比7节点的小,这样不就不符合左式堆了吗。
实际上它们是相同的,符合要求,npl都是1。各位可以根据定理:任一节点的零路径长比它的两个儿子的零路径长的,最小值大1,来验算。