数据结构与算法----->数据结构----->堆

♀尐吖头ヾ 提交于 2020-01-22 04:18:25

13.1堆的概念

    • 堆是一种二叉树,是一种特殊的二叉树
    • 节点的子节点的关键字

13.2 java程序实现堆这种数据结构

 13.2.1编程思路:

  step1,首先了解堆的概念

      • 堆是完全二叉树
      • 使用数组来存储堆元素,熟记数组元素下标和子节点、父节点的关系
        • 如果堆中某个节点的数组下标为x,那么

        •   
      • 熟记“”堆条件“”        

  step2,依据堆的概念,设计class Heap中成员函数——fun:删除堆中关键值最大的节点

      • 编程思路  
        •      
      • java代码
        •   

   step3,依据堆的概念,设计class Heap中的成员函数——fun:往现有的堆中插入一个新的节点

      • 编程思路:
        • 向上筛选,使得新插入的节点关键值小于父节点关键值,大于子节点关键值,使得新的二叉树重新满足“”堆条件“”

          • 只需要将新节点关键值和其“”暂时位置“”上的父节点的关键值相比较,并根据需要交换位置即可

        •   
      • java代码:
        • trickleUp(int index)函数将新节点向上筛选使得重新满足堆条件,该函数的参数是节点在存储堆元素的数组中的位置下标

        •  应注意上述程序中插入的一个新的节点之后,数组大小currentSize++

        •       

   step4,依据堆的概念,设计class Heap中的成员函数——fun:改变的堆中某个节点的值

      • 编程思路:
        • 如果该节点的关键值降低了,就向下筛选使得新的二叉树重新满足堆条件
        • 如果该节点的关键值上升了,就向上筛选  
      • java代码:  
        •  

13.2.2 用java所实现的Heap:class (完整程序) 

    • Node.java
      /**
       * @description: 堆中节点类,由于堆是使用数组存储的二叉树,所以堆的节点类中不必有引用,引用之类的属性是不必要的*/
      package tree;
      
      import POJO.Student;
      
      public class HeapNode {
          private String key;//关键字
          private double data;//数据项(基本数据类型)
          private Student stu;//数据项(POJO对象)
          
          public HeapNode(){}
          
          public HeapNode(String key,double data,Student stu){
              this.key=key;
              this.data=data;
              this.stu=stu;
          }
          //输出节点中的数据内容
          public void displayNode(){
              System.out.println("节点关键字key="+this.key);
              System.out.println("节点数据内容:data:double="+this.data+"    Student.name="+this.stu.getName());
          }
          public String getKey(){
              return this.key;
          }
          public void setKey(String key){
              this.key=key;
          }
          public double getData(){
              return this.data;
          }
      }

      Heap.java

      /**
       * @author lxrm
       * @date 20161024
       * @description
       * 堆:这是树的应用,堆是一种特殊的二叉树
       *         堆是完全二叉树
       *         堆是用数组实现的二叉树
       *         (堆条件)堆这种特殊的二叉树必须满足一个条件,即树中父节点的关键值必须大于其子节点的关键值*/
      package tree;
      
      import POJO.Student;
      
      public class Heap {
          private HeapNode heapArray[];//堆数组,用于存储堆中各个节点
          private int maxSize;//堆中可存储的最大元素个数
          private int currentSize;//堆中现有元素个数
          /**
           *@param size:int 堆数组大小(由于堆这种二叉树的所有节点是使用数组来存储的,所以要在开始就初始化相应的数组,该参数是数组大小,也是堆中元素最大个数 
           **/
          public Heap(int size){
              heapArray=new HeapNode[size];
              maxSize=size;
              currentSize=0;
          }
          /**
           * @description:向堆中插入数据
           * 编程思路:step1,将待插入数据封装成Node对象(节点对象)
           *           step2,将待插入节点对象放到堆最底层最末尾位置(往堆中插入新的节点,但是现在还不满足堆的条件)
           *                     先判断当前堆数组是否已满,若已满则不能插入新的节点
           *                     若堆未满,则将待插入节点放置在堆数组末尾位置(末尾第一个空位置)
           *           step3,使新插入的节点沿着路径上升,直到满足堆的条件(即使得父节点关键值大于子节点关键值)*/
          public void insert(String key,double data,Student stu){
              HeapNode newNode=new HeapNode(key,data,stu);//step1
              if(currentSize==maxSize){//step2
                  System.out.println("堆已满,无法插入新的节点");
                  return;
              }
              heapArray[currentSize++]=newNode;
              this.tricleUpNode(currentSize-1);//step3,将乱序节点heapArray[currentSize]沿路径上升放置到正确的位置,使乱序二叉树重新满足堆条件
          }
          /**
           * @description:删除堆中优先级最高的元素,并将该元素作为返回值返回*/
          public HeapNode deleteRoot(){
              HeapNode root=heapArray[0];//根元素,下面的程序会将根节点删除,所以如果后面还想使用该节点中的数据,就要先将该节点保存下来
              heapArray[0]=heapArray[currentSize-1];//step1:将根节点删除,并将堆中最后一个节点放到根节点的位置(这样使得删除根节点后的堆还是完全二叉树)
              currentSize--;//step2:删掉了一个节点,所以堆数组的当前大小要减一
              this.tricleDownNode(0);//step3:此步骤将前面所得无序完全二叉树重新满足堆条件
              return root;
          }
          /**
           * @description:这个函数用于更改某个节点的关键值大小,且在更改后自动重新排序,以使其重新满足堆条件(父节点关键值比两个子节点的关键值都大)
           * @param index:int 待改动节点的索引值(即该节点在堆数组中的下标值)
           * @param targetKey:String 目的关键字值
           * */
          public void changeKey(int index,String targetKey){
              if(index<0||index>=currentSize){
                  System.out.println("给出的index超出范围,index应该在[0,"+currentSize+")中取值");
                  return; 
              }
              String initKey=heapArray[index].getKey();
              heapArray[index].setKey(targetKey);
              if(targetKey.compareTo(initKey)>0){
                  this.tricleUpNode(index);
              }else{
                  this.tricleDownNode(index);
              }
          }
          /**
           * @description:这个函数用于使参数节点沿着"树路径"上升,直至这个树重新满足"堆条件"(堆条件:父节点关键字值大于他的子节点的关键字的值)
           * @param node:HeapNode 乱序节点,本函数可以使这个乱序节点最终被放置在正确的位置,从而使得“乱序完全二叉树”编程符合堆条件的堆
           * @param index:int 乱序节点在数组中的位置,也即乱序节点在堆数组中的下标
           * 编程思路:从乱序节点开始,沿着树路径上升,查找乱序节点应该被放置的正确位置
           *              树路径是这样的,乱序节点-->乱序节点的父节点-->乱序节点的父节点的父节点(用数组实现的堆中parentIndex=(childIndex-1)/2)
           *              终止条件:父节点下标<0,或者途中某个父节点的关键值大于乱序节点的关键值
           * */
          private void tricleUpNode(int index){
              HeapNode tmp=heapArray[index];//暂时存储乱序节点的地址值
              int currentIndex=index;//从当前节点开始沿着“树路径”上升,查找乱序节点应该被放置的正确位置
              int parentIndex=(currentIndex-1)/2;//父节点数组下标
              
              //如果父节点的关键值比乱序节点的关键值小,则父节点下移
              while((parentIndex>=0)&&(heapArray[parentIndex].getKey().compareTo(heapArray[currentIndex].getKey())<0)){
                  heapArray[currentIndex]=heapArray[parentIndex];
                  currentIndex=parentIndex;
                  parentIndex=(parentIndex-1)/2;
              }
              heapArray[currentIndex]=tmp;
          }
          /**
           * @description:从无序节点开始,沿着树路径“向下查找”参数节点应该被放置的位置,并且将参数节点放置到正确的位置,使得乱序的完全二叉树重新满足堆条件
           * @param index:int 无序节点在数组中的位置,数组下标
           * 编程思路:从参数节点(无序节点)开始,沿着树路径向下查找,直至找到无序节点应该被放置的正确位置
           *           从当前层到下一层有两条路径,应该选择哪条路径呢(答:应该选择关键值较大的子节点构成实际行走的路径)
           *         终止条件:当前层节点作为父节点,比左子节点和右子节点关键值都大,则当前层节点位置就是乱序节点应该被放置的正确位置
           *         终止条件:查找到了堆末尾位置*/
          private void tricleDownNode(int index){
              HeapNode tmp=heapArray[index];//step1:暂时存储无序节点对象
              int currentIndex=index;
              int leftChildIndex=2*currentIndex+1;
              int rightChildIndex=leftChildIndex+1;//这样写可以比乘法少些运算量,节省运行时间
              while(leftChildIndex<currentSize){//至少有一个子节点时(也即至少拥有左子节点)
                  boolean flagLeftChild=tmp.getKey().compareTo(heapArray[leftChildIndex].getKey())>0;//当前节点关键值比其左子节点的关键值大,该值为true
                  
                  if(rightChildIndex<currentSize){//拥有两个子节点时
                      boolean flagRightChild=tmp.getKey().compareTo(heapArray[rightChildIndex].getKey())>0;//当前节点的关键值比其右子节点的关键值大,该值为true
                      if(flagLeftChild&&flagRightChild){//当乱序节点关键值比heapArray[currentIndex]左子节点和右子节点都大时,currentIndex就是乱序节点应该被放置的位置
                          break;
                      }else{
                          if(heapArray[leftChildIndex].getKey().compareTo(heapArray[rightChildIndex].getKey())>0){
                              heapArray[currentIndex]=heapArray[leftChildIndex];
                              currentIndex=leftChildIndex;
                          }else{
                              heapArray[currentIndex]=heapArray[rightChildIndex];
                              currentIndex=rightChildIndex;
                          }
                          leftChildIndex=2*currentIndex+1;
                          rightChildIndex=leftChildIndex+1;
                      }
                  }else{//仅拥有一个子节点时(即只拥有左子节点时)
                      if(flagLeftChild){//如果当前节点关键值比左子节点关键值大,就跳出while循环,currentIndex就是乱序节点应该插入的位置
                          break;
                      }else{
                          heapArray[currentIndex]=heapArray[leftChildIndex];
                          currentIndex=leftChildIndex;
                          break;//这里已经是堆末尾,所以可以跳出while循环
                      }
                  }    
              }//end while
              heapArray[currentIndex]=tmp;
          }//end tricleDownNode()
          //堆是否为空
          public boolean isEmpty(){
              return currentSize==0;
          }
          //堆是否已满
          public boolean isFull(){
              return currentSize==maxSize;
          }
          //将堆展示在屏幕上
          public void displayHeap(){
              int layerNum=1;//每层节点数目
              int index=0;//堆中元素索引(也即堆数组下标)
              //展示堆数组
              System.out.println("heapArray:");
              for(;index<currentSize;index++){
                  System.out.print(heapArray[index].getKey()+"  ");
              }
              /*
              //以树的形式展示堆(有待改进)
              while(true){
                  for(int i=0;i<layerNum;i++){
                      if(heapArray[index].getKey()!=null){
                          System.out.print(heapArray[index].getKey()+" ");
                      }
                      
                      index++;
                      if(index>=currentSize-1){
                          return;
                      }
                  }
                  System.out.println("index="+index);
                  layerNum=2*layerNum;
              }//end while()
              */
          }
          
      }

       

        

13.3 堆的特点(性质)

    • 堆是一种特殊的二叉树,它的父节点的关键值大于子节点的关键值,但是不能保证右子节点的关键值大于左子节点的关键值,所以没有一种遍历方法能够按照一定的顺序(升序或者降序)顺序访问树中所有节点。
    • 但是堆能保证,按照从根节点到任何一个叶子节点的路径,关键值都是按照降序排列的。
    • 堆,除了以上所述无法进行按序遍历之外,还有一些缺点,如:不便于按照关键值查找特定的节点。
    • 因为不易查找到指定关键值的节点,所以也不易删除某个指定关键值的节点,因为删除之前要先定位该节点。
    • 堆的时间复杂度:
      • 查找指定关键值的节点O(N)      在存储堆元素的数组中依次查找,所以时间复杂度为O(N)
      • 删除指定关键值的节点O(N)    查找定位+删除+移动子树
      • 往堆中插入一个新的节点O(logN)    先插入到堆的末尾,然后向上筛选新插入的节点,使得重新满足“堆条件”
      • 删除堆中关键值最大的节点O(logN)    删除根节点,然后将最后一个节点作为根节点,再向下筛选使得重新满足“堆条件”
    • 已有的堆中先删除一个节点,紧接着再将该节点插入堆中,得到的新的堆可能和之前的堆不一样。如:堆中删除最大关键值节点(即根节点)后再将该节点插入到堆中,得到的新的堆和之前的堆是不一样的。
    • 同样的一组节点,形成的堆可能不一样,最后形成的堆和节点的插入顺序有关。
    • 如何扩展堆:
      • 由于堆是使用数组存储的完全二叉树,不停地往堆中插入新的元素的情况下,有可能会导致存储堆元素的数组溢出,这时就要扩展堆数组。
      • 需要创建一个新的堆数组,应使新的堆数组的容量为之前的二倍,并且将原有数组内容copy到新的数组中即可。
      • 为了避免上述手动扩展过程,可以不使用数组来存储堆元素,而是使用Vector类来存储堆元素,因为Vector可以自动扩展容量。 

13.4 堆的应用场景 

 场景一:堆可用于实现优先级队列。(用堆实现的优先级队列,往队列插入新队员和删除队首元素的时间复杂度都是O(logN))

    概述:优先级队列有多种实现方法,如使用有序数组就可以实现优先级队列,这种方法中元素的插入时间为O(N),因为要移动数组元素。

         根据上面的叙述可知,使用有序数组实现的优先级队列,新元素的插入较慢,所以考虑使用其他方法实现优先级队列。

       可以使用堆来实现优先级队列。

    堆实现优先级队列的方法:

       往队列中插入新的元素就用Heap类中的insert()函数

         删除队首元素就用Heap类中的deleteRoot()函数

场景二:使用堆实现堆排序

    概述:借助于堆,将一个无序数据集按照从大到小的顺序排列

    堆排序的实现:将无序数据依次插入堆中

             然后使用Heap类中的deleteRoot()函数不停地删除堆顶部的元素

    堆排序的时间复杂度:O(N*logN)

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!