数据结构与算法分析 第6章总结

6.3二叉堆


优先队列至少两种操作:插入insert等同于入队enqueue、删除最小者deleteMin等同于出队dequeue。

优先队列可以用于外部排序和贪婪算法的实现。

优先队列实现在第一种方法(简单)是在表头以O(1)时间执行插入操作,O(N)时间遍历链表删除最小元。或始终使链表处于排序状态,则insert代价O(N),deleteMin代价O(1)。

优先队列的第二种实现方法二叉查找树,insert和deleteMin都是O(lgn)。而查找树有许多不需要的类方法。

优先队列的第二种实现方法是使用(数据)二叉堆实现,不需要链且遍历简单,以O(logN)支持上两种操作。


堆具有结构性和堆序性两种性质。结构性指堆是完全二叉树(而叶子也满的是理想二叉树)。堆序性质指任意节点小于它的后裔。

数组保存二叉堆,根元素从索引1开始(索引0存值-1)。数据实现的二叉堆的任一位置i,左儿子2i,右儿子2i+1,父亲i/2的下取整。

//**********

二叉堆的API:空容项3种构造方法、堆一般方法、和3个内部实现方法:扩容、建堆、上下滤。构造函数创建的是无序堆,建堆函数用于维护堆序。


public classBinaryHeap> {

                  private static final int DEFAULT_CAPACITY = 10;

                  private int currentSize;

                  private AnyType[] array;


                  public BinaryHeap() {}

                  public BinaryHeap(int capacity) {}

                  public BinaryHeap(AnyType[] items) {}


                  public void insert(AnyType x) {}

                  public AnyType findMin() {}

                  public AnyType deleteMin() {}

                  public boolean isEmpty() {}

                  public void makeEmpty() {}


                  private void percolateDown(int hole) {}

                  private void buildHeap() {}

                  private void enlargeArray(int newSize) {}

}

// **********

新元素中堆中上滤percolate up,直到找到正确的位置。

上滤操作为:由插入位置向父延伸的插入排序。三步:抽牌、大牌下沉、放牌。

数组二叉堆的插入INSERT复杂度O(logN)。业已证明,执行一次插入平均需要2.607次比较,平均操作上移1.607层。

//**********

public void insert(AnyType x)

{

                  if(currentSize == array.length-1)

                                    enlargeArray(array.length*2+1);

                  //Percolate up

                  int hole = ++currentSize;

                  for(;hole>1&&array[hole/2].compareTo(x)>0;hole/=2)

                                    array[hole]=array[hole/2];

                  array[hole]=x;

}

//**********

优先队列的弹堆操作deleteMin:取根元素用于返回、最大元素补到根元素、再下滤。

下滤percolateDown操作:为向子延伸的插入排序。三步:抽牌、小牌小浮、放牌。注意浮牌的左儿子不出界为终止条件,原则选小儿子,右儿子小选右儿子。下滤操作的关键在于儿子选取。

上滤和下滤操作共同保证了堆序。

//**********

public AnyType deleteMin( ){

                  if(isEmpty())

                                    throw new UnderflowException();

                  AnyType minItem=array[1];

                  array[1]=array[currentSize--];

                  percolateDown(1);

                  return minItem;

}

percolateDown(hole){

                  int child;

                  AnyType eject=array[hole];

                  for(;hole*2

                                    child=hole*2;

                                    if(child!=currentSize&&array[child].compareTo(array[child+1])>0)

                                                      child++;

                                    if(array[child].compareTo(eject)<0)

                                                      array[hole]=array[child];

                                    else

                                                      break;                      

                  }

                  array[hole]=eject;

}

//**********

堆的其它操作--

行数据被封闭在类中,通过compareTo()比较关键字的值。降低关键字decreaseKey的值要上滤,增加关键字increaseKey的值要下滤。降低关键字的值可以用于提高程序的优先级。

删除delete为:将关键字置无穷,再下滤,array[currentSize]=NULL,currentSize--。

建堆buildHeap就是对非叶节点下滤。

//**********

public BinaryHeap( AnyType [ ]items ){

                  currentSize=items.length;

                  array=(AnyType[])new Comparable[(currentSize+2)*11/10];//数组留有10%的容量

                  int i=1;

                  for(AnyType item:items)

                                    array[i++]=item;

                  buildHeap();

}

private void buildHeap(){

                  for(int i=currentSize/2;i>0;i--)

                                    percolateDown(i);

}

//**********

理想二叉树是满叶的完全二叉树。高为h的理想二叉树,节点数2^(h+1)-1,节点高度和2^(h+1)-1-(h+1)。即理想二叉树高度和为:节点数-节点数的二进制表示法的1个数。


6.4优先队列的应用


选择问题:输入是N个元素以及一个整数k,找出第k个最大元素。

选择问题方法1:将元素读入数组并排序,返回第k个最大元素,复杂度O(N^2)。

方法2:将k个元素读入数组并排序,将其余元素放入数组中的正确位置,复杂度O(N*k),若k=N/2的上取整则复杂度为O(N^2)。

选择问题更高效的优先队列解决方法3:用O(N)时间buildHeap,O(logN)时间deleteMin共k次。算法复杂度O(N+klogN)。如果k=N并在元素离开堆时记录它们的值,相当于以O(NlogN)排序,即堆排序算法。

方法4:思路同方法3而使用堆代替数组。建堆O(k),处理其余元素(N-k)*O(logk),总复杂度O(Nlogk)。

其它方法:第7章的平均时间O(N)的方法,第10章的最坏时间O(N)的方法。


排队问题:顾客到达并排队直到k个出纳员服务,求顾客平均等待多久或所排队伍多长的统计问题。

方法:顾客等待的队伍实现为一个队列。每个时钟单位走到下一个事件时间。下一个事件要么是输入文件中下一顾客到达(插入insert),要么是顾客在一位出纳员处离开(弹堆deleteMin)。由于"事件将要发生的时间"(关键字)是可达的,只需要找出最近"将来发生事件"并处理这个事件。

6.5 d-堆

d堆的插入insert运行时间O(logd,N)。d堆的deleteMin需要找到d个儿子的最小者,deleteMin复杂度O(dlogd,N)。d为2的幂,这样才能通过移动二进制实现除法,大大减少运行时间。

d堆适用于insert次数远小于deleteMin次数的算法,也适用于队列太大而不能完全装入主存的情况(功能与B树相同)。

d堆的缺点是不能find()、堆合并merge困难。

6.6左式堆

左式堆具有二叉堆基本的堆结构性和堆序性,左式堆也是二叉树。

零路径长(npl null path length)定义为不具有两个节点的最短路径长。少于两个儿子的节点npl=0,null节点的npl=-1。

左式堆性质1:左儿子npl大于等于右儿子npl。左式堆是不平衡树,偏重于向左树增加深度。

左式堆性质2:节点npl比其儿子的npl的最小值大1。

左式堆其它:右路径有r个节点的左式堆必然至少有2^r-1个节点。


左式堆节点的数据结构为:数据、左右引用、零路径长。节点要声明为privatestatic以使对象内有效。左式堆不用存元素数currentSize。

//**********

左式堆API:

public classLeftistHeap>{

                  private Node root;

                  private static class Node{

                                    AnyType element;

                                    Node left;

                                    Node right;

                                    int npl;


                                    Node(AnyType theElement){

                                                      element=theElement;left=null;right=null;npl=0;

                                    }

                                    Node(AnyTypetheElement,Node lt,Node rt){

                                                      element=theElement;left=lt;right=rt;npl=0;

                                    }

                  }


                  private void swapChildren(Node t){}

                  private Nodemerge(Node h1,Node h2){}

                  private Nodemerge(Node h1,Node h2){}


                  public LeftistHeap(){root=null;}

                  public void insert(AnyType x){}

                  public AnyType findMin(){}

                  public AnyType deleteMin(){}

                  public boolean isEmpty(){return root==null;}

                  public void makeEmpty(){root=null;}

                  public void merge(LeftistHeap rhs){}

}

//**********

习惯将根元素较小的参数作为第一参数。

因而合并堆函数有3个,1个合并其它堆、1个处理参数为空堆(递归底)并将"小根堆"作为第一参数、1个为堆合并算法。

左式堆合并算法为:递归地使1参右子树与2参合并,并维护2个左式堆性质,递归底为1参单节点、1参空堆。

左式堆insert显然可以用左式堆merge实现。左式堆deleteMin为合并两个子树的堆,相比于二叉堆的最大元素"先置顶"再"下滤"。左式堆均以O(logN)支持merge/insert/deleteMin。

左式堆使用“链式的二叉堆建堆”实现。buildHeap的效果可以用递归地建立左右子树然后将根下滤得到。复杂度O(N)。未实现???

注意以下左式堆的实现中,用根节点表示左式堆,而非使用对象名来实现算法。

//**********

左式堆merge的基本情形:

private Nodemerge(Node h1,Node h2){

                  if(h1==null){

                                    return h2;

                  }

                  if(h2==null){

                                    return h1;

                  }

                  if(h1.element.compareTo(h2.element)<0){

                                    return merge1(h1,h2);

                  }else{

                                    return merge(h2,h1);

                  }

}


左式堆merge的一般情形:

privatemerge1(Node h1,Node h2){

                  if(h1.leftChild=null){

                                    h1.rightChild=h2;

                  }else{

                                    h1=merge(h1.rightChild,h2);

                                    if(h1.leftChild.npl

                                                      swapChildren(h1);

                                    h1.npl=h1.rightChild.npl;

                  }

                  return h1;

}


左式堆insert算法:

public void insert(AnyType x){

                  merge(new Node(x),root);

}


左式堆deleteMin算法:

public NodedeleteMin(AnyType x){

                  AnyType minItem=root.element;

                  root=merge(root.leftChild,root.rightChild);

                  return minItem;

}

//**********

6.7斜堆

斜堆是左式堆的自调节形式,类似于伸展树和AVL树的关系。斜堆具有堆序,而无结构限制。略。

6.8二项队列(二项队列不记代码,只要能读懂即可)

二项队列的merge/insert/deleteMin的最坏运行时间O(logN),而insert平均花费O(N)。

二项队列是保证堆序的树的集合,高度为k的树恰有2^k个节点。


二项队列的buildHeap算法:??可以先用插入代替

二项队列的merge算法:合并树操作是更新currentSize的。

二项队列的insert算法:就是合并。

二项队列的deleteMin算法:找到具有最小根的堆Bk,二项队列除去Bk形成堆集H',Bk除去根形成堆集H','合并H'和H''。前3步用时O(logN),最后一步用时O(logN)。

二项队列只能用对象名表示,而不能像二叉堆、左式堆那样用根点表示堆了。

//**********

二项队列是多叉树,节点使用左儿子右兄弟表示法。数据为节点数组theTrees和节点数currentSize。

二项队列的API:

public classBinomiaQueue>{

                  public static class Node{

                                    AnyType element;

                                    Node leftChild;

                                    Node nextSibling;

                                    Node(AnyType theElement){

                                                      element=theElement;leftChild=null;nextSibling=null;

                                    }

                                    Node(AnyType theElement,Nodelc,Node ns){

                                                      element=theElement;leftChild=lc;nextSibling=ns;

                                    }

                  }                

                  private static final int DEFAULT_TREES=1;

                  private int currentSize;

                  private Node[] theTrees;


                  private void combineTrees(Nodet1,Node t2){}

                  private void expandTrees(int newNumTrees){}

                  private int capacity(){}

                  private int findMinIndex(){}


                  public BinomiaQueue(){}

                  public BinomiaQueue(AnyType item){}

                  public void merge(BinomiaQueuerhs){};//二项队列的合并只实现了一个单项合并

                  public void insert(AnyType x){merge(newBinomiaQueue(x));}

                  public AnyType deleteMin(){}

                  public boolean isEmpty(){return currentSize==0;}

                  public void makeEmpty(){}

}



combineTrees的实现算法:使大根堆成为小根堆的左孩子。二项队列的优点在于它的堆合并算法简单。由于二项队列的堆的左孩子、右兄弟是单链表,因而combineTrees只需要修改t1的左孩子和t2的右兄弟。

private NodecombineTrees(Node t1,Node t2){

                  if(t1.element.compareTo(t2.element)>0)

                                    return combineTrees(t2,t1);

                  t2.nextSibling=t1.leftChild;

                  t1.leftChild=t1;

                  return t1;

}


二项队列merge算法:

public void merge(BinomiaQueuerhs){

                  if(this==rhs)//Avoid aliasing problems

                                    return;

                  currentSize+=rhs.currentSize;

                  if(currentSize>capacity()){

                                    intmaxLength=Math.max(theTrees.length,rhs.theTrees.length);

                                    expandTheTrees(maxLength+1);

                  }


                  Node carry=null;

                  for(int i=0;j=1;j<=currentSize;i++,j*=2){

                                    Nodet1=theTrees[i];

                                    Nodet2=i


                                    int whichCase=t1==null?0:1;

                                    whichCase+=t2==null?0:4;


                                    switch(whichCase){

                                    case 0://no trees

                                    case 1://only this

                                                      break;

                                    case 2://only rhs

                                                      theTrees[i]=t2;

                                                      rhs.theTrees[i]=null;

                                                      break;

                                    case 3://this and rhs

                                                      carry=combineTrees(t1,t2);

                                                      theTrees[i]=rhs.theTrees[i]=null;

                                                      break;

                                    case 4://only carry

                                                      theTrees[i]=carry;

                                                      carry=null;

                                    case 5://this and carry

                                                      carry=combineTrees(t1.carry);

                                                      theTrees[i]=null;

                                                      break;

                                    case 6://rhs and carry

                                                      carry=combineTrees(t2.carry);

                                                      rhs.theTrees[i]=null;

                                                      break;

                                    case 7://all trees

                                                      theTrees[i]=carry;

                                                      carry=combineTrees(t1,t2);

                                                      rhs.theTrees[i]=null;

                                                      break;                                        

                                    }                                  

                  }


                  for(int k=0;k

                                    rhs.theTrees[k]=null;

                  rhs.currentSize=0;

}


二项队列的去头子树的堆组,由于需要将根节点的右兄弟赋空,简单的方法是用计数的循还方式(不用链)。

二项队列的deleteMin算法:

                  public AnyType deleteMin(){

                                    if(isEmpty())

                                                      throw newUnderflowException();

                                    int minIndex=findMinIndex();

                                    AnyTypeminItem=theTrees[minIndex].element;

                                    NodedeletedTree=theTrees[minIndex].leftChild;


                                    //construct H''

                                    BinomiaQueuedeletedQueue=new BinomiaQueue();

                                    deletedQueue.expandTheTrees(minIndex+1);


                                    deletedQueue.currentSize=(1<

                                    for(intj=minIndex-1;j>=0;j--){

                                                      deletedQueue.theTrees[j]=deletedTree;

                                                      deletedTree=deletedTree.nextSibling;

                                                      deletedQueue.theTrees[j].nextSibling=null;

                                    }


                                    //construct H'

                                    theTrees[minIndex]=null;

                                    currentSize-=deletedQueue.currentSize+1;

                                    merge(deletedQueue);

                  }

//**********

&6.9标准库的优先队列

复习栈和队列的API:

[if !supportLists]l [endif]链表ArrayLIst增add()、删remove()、改set()、查get()、判空isEmpty()/size()

[if !supportLists]l [endif]栈Stack 压栈push(AnyType x)、弹栈pop()、栈空isEmpty()

[if !supportLists]l [endif]队列Queue由LinkedList实现,入队offer()、出队pull()、element()只返头部元素不出队。

ArrayBlockingQueue队列构造需指定容量

PriorityBlockingQueue基于堆数据结构对priorityQueue进行再次包装。

DelayQueue只有延迟期满的元素才能取出。队中元素都没满,则pull返回null。


Queue使用时要尽量避免Collection的add()和remove()方法,而是要使用offer()来加入元素,使用poll()来获取并移出元素。它们的优点是通过返回值可以判断成功与否,add()和remove()方法在失败的时候会抛出异常。 如果要使用前端而不移出该元素,使用element()或者peek()方法

[if !supportLists]l [endif]优先队列PriorityBlockingQueue类方法:isEmpty()、offer()、poll()、toArray()。优先队列需要将队列元素implementsComparable。

[if !supportLists]l [endif]索引优先队列(TreeMap)不必实现队列元素的Comparable接口,使用更方便。索引优队典型应用于Dijstra算法。

[if !supportLists]Ø [endif]HashMap只能实现键值查找的映射关系,只有"判长空置空添删容遍"功能(size/isEmpty/clear/put/remove/containsKey/conntainsValue/get/entrySet/keySet/values)

[if !supportLists]Ø [endif]而TreeMap实现了映射和优队功能:入队put、弹队pollFirstEntry/pollLastEntry、查firstEntry/lastEntry/ceilingEntry/floorEntry/higherEntry/lowerEntry/将Entry换成Key。

注意TreeMap的遍历用Map.Entry不存在TreeMap.Entry。

[if !supportLists]Ø [endif]TreeMap具有很强大查键功能,却不具有改键功能只能先remove后put。

你可能感兴趣的:(数据结构与算法分析 第6章总结)