数据结构与算法分析 第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。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容

  • 第4章树 对于大量数据链表访问太慢,而树支持以O(logN)平均时间支持各种操作。 树的概念:父亲、祖先、儿子、后...
    fjxCode阅读 601评论 0 0
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,637评论 18 139
  • 二月聽風春行早, 一襲夜雨龍抬頭。 少年哪識愁滋味, 笑執彩筆繞指柔。 二月初二,師徒三人燈下畫龍,各得其趣。東哥...
    風無意聽濤畫苑阅读 157评论 1 2
  • 一到周末 生活 便没了规律 想要添点儿生气 只能 出去走走 今天的武汉 天气蛮好 有风 微热 山不高 道不长 却也...
    司马庭深阅读 225评论 0 1