最大堆(建立、删除、插入和堆排序)

关于最大堆

什么是最大堆和最小堆?最大(小)堆是指在树中,存在一个结点并且该结点有儿子结点,该结点的data域值都不小于(大于)其儿子结点的data域值,而且它是一个彻底二叉树(不是满二叉树)。注意区分选择树,由于选择树(selection tree)概念和最小堆有些相似,他们都有一个特色是“树中的根结点都表示树中的最小元素结点”。同理最大堆的根结点是树中元素最大的。那么来看具体的看一下它长什么样?(最小堆这里省略)node

 
图-1

 

这里须要注意的是:在多个子树中,并非说其中一个子树的父结点必定大于另外一个子树的儿子结点。最大堆是树结构,并且必定要是彻底二叉树。数组

最大堆ADT

那么咱们在作最大堆的抽象数据类型(ADT)时就须要考虑三个操做:
(1)、建立一个最大堆;
(2)、最大堆的插入操做;
(3)、最大堆的删除操做;
最大堆ADT以下:函数

struct Max_Heap {
  object: 由多个元素组成的彻底二叉树,其每一个结点都不小于该结点的子结点关键字值
  functions:
    其中heap∈Max_Heap,n,max_size∈int,Element为堆中的元素类型,item∈ Element
    Max_Heap createHeap(max_size)       := 建立一个总容量不大于max_size的空堆
    void max_heap_insert(heap, item ,n) := 插入一个元素到heap中
    Element max_heap_delete(heap,n)     := if(heap不为空) {return 被删除的元素 }else{return NULL}
}
///其中:=符号组读做“定义为”

最大堆内存表现形式

咱们只是简单的定义了最大堆的ADT,为了可以用代码实现它就必需要考虑最大堆的内存表现形式。从最大堆的定义中,咱们知道不论是对最大堆作插入仍是删除操做,咱们必需要保证插入或者删除完成以后,该二叉树仍然是一个彻底二叉树。基于此,咱们就必需要去操做某一个结点的父结点。
  第一种方式,咱们使用链表的方式来实现,那么咱们须要添加一个额外的指针来指向该结点的父结点。此时就包括了左子结点指针、右子结点指针和父结点指针,那么空链的数目有多是很大的,好比叶子结点的左右子结点指针和根结点的父结点指针,因此不选择这种实现方式(关于用链表实现通常二叉树时处理左右子结点指针的问题在线索二叉树中有说起)。
  第二种方式,使用数组实现,在二叉树进行遍历的方法分为:先序遍历、中序遍历、后序遍历和层序遍历。咱们能够经过层序遍历的方式将二叉树结点存储在数组中,因为最大堆是彻底二叉树不会存在数组的空间浪费。那么来看看层序遍历是怎么作的?对下图的最大堆进行层序遍历:学习

 
 
 
 
层序遍历流程变化
从这里能够看出最后获得的顺序和上面图中所标的顺序是同样的。
  那么对于数组咱们怎么操做父结点和左右子结点呢?对于彻底二叉树采用顺序存储表示,那么对于任意一个下标为i(1 ≤ i ≤ n)的结点:
(1)、父结点为: i / 2(i ≠ 1),若i = 1,则i是根节点。
(2)、左子结点: 2i(2i ≤ n), 若不知足则无左子结点。
(3)、右子结点: 2i + 1(2i + 1 ≤ n),若不知足则无右子结点。
 
 

 

最终咱们选择数组做为最大堆的内存表现形式。编码

基本定义:spa

#define MAX_ELEMENTS 20
#define HEAP_FULL(n) (MAX_ELEMENTS - 1 == n)
#define HEAP_EMPTY(n) (!n)
typedef struct {
    int key;
}element;
element heap[MAX_ELEMENTS];

下面来看看最大堆的插入、删除和建立这三个最基本的操做。指针

最大堆的插入

最大堆的插入操做能够简单当作是“结点上浮”。当咱们在向最大堆中插入一个结点咱们必须知足彻底二叉树的标准,那么被插入结点的位置的是固定的。并且要知足父结点关键字值不小于子结点关键字值,那么咱们就须要去移动父结点和子结点的相互位置关系。具体的位置变化,能够看看下面我画的一个简单的图。code

void insert_max_heap(element item ,int *n){
    if(HEAP_FULL(*n)){
      return;
    }
    int i = ++(*n);
    for(;(i != 1) && (item.key > heap[i/2].key);i = i / 2){/// i ≠ 1是由于数组的第一个元素并无保存堆结点
      heap[i] = heap[i/2];/// 这里其实和递归操做相似,就是去找父结点
    }
    heap[i] = item;
}

 

 
最大堆的插入

因为堆是一棵彻底二叉树,存在n个元素,那么他的高度为: log2(n+1),这就说明代码中的for循环会执行 O(log2(n))次。所以插入函数的时间复杂度为: O(log2(n))

 

最大堆的删除

最大堆的删除操做,老是从堆的根结点删除元素。一样根元素被删除以后为了可以保证该树仍是一个彻底二叉树,咱们须要来移动彻底二叉树的最后一个结点,让其继续符合彻底二叉树的定义,从这里能够看做是最大堆最后一个结点的下沉(也就是下文提到的结点1)操做。例如在下面的最大堆中执行删除操做:排序

 
 

如今对上面👆最大堆作删除, 对于最大堆的删除,咱们不能本身进行选择删除某一个结点,咱们只能删除堆的根结点。(⚠️⚠️⚠️)

 

  • 第一步,咱们删除上图中的根结点20;
  • 当删除根结点20以后明显不是一个彻底二叉树,更确切地说被分红了两棵树。
  • 咱们须要移动子树的某一个结点来充当该树的根节点,那么在(15,2,14,10,1)这些结点中移动哪个呢?显然是移动结点1,若是移动了其余结点(好比14,10)就再也不是一个彻底二叉树了。

对上面三步图示以下:递归

 
 
 
 

显然如今看来该二叉树虽然是一个彻底二叉树,可是它并不符合最大堆的相关定义,咱们的目的是要在删除完成以后,该彻底二叉树依然是最大堆。所以就须要咱们来作一些相关的操做!

1)、此时在结点(15,2)中选择较大的一个和1作比较,即15 > 1的,因此15上浮到以前的20的结点处。
2)、同第1步相似,找出(14,10)之间较大的和1作比较,即14>1的,因此14上浮到原来15所处的结点。
3)、由于原来14的结点是叶子结点,因此将1放在原来14所处的结点处。
 
 
element delete_max_heap(int *n){
  int parent, child;
  element temp, item;
  temp = heap[--*n];
  item = heap[1];
  parent = 1,child=2;
  for(;child <= *n; child = child * 2){
   if( (child < *n) && heap[child].key < heap[child+1].key){/// 这一步是为了看当前结点是左子结点大仍是右子结点大,而后选择较大的那个子结点
        child++;
      }
      if(temp.key >= heap[child].key){
        break;
      }
      heap[parent] = heap[child];///这就是上图中第二步和第三步中黄色部分操做
      parent = child;/// 这其实就是一个递归操做,让parent指向当前子树的根结点
   }
  heap[parent] = temp;
  return item;
}

同最大堆的插入操做相似,一样包含n个元素的最大堆,其高度为:log2(n+1),其时间复杂度为:O(log2(n))

总结:由此能够看出,在已经肯定的最大堆中作删除操做,被删除的元素是固定的,须要被移动的结点也是固定的,这里我说的被移动的元素是指最初的移动,即最大堆的最后一个元素。移动方式为从最大的结点开始比较。

最大堆的建立

为何要把最大堆的建立放在最后来说?由于在堆的建立过程当中,有两个方法。会分别用到最大堆的插入和最大堆的删除原理。建立最大堆有两种方法:
(1)、先建立一个空堆,而后根据元素一个一个去插入结点。因为插入操做的时间复杂度为O(log2(n)),那么n个元素插入进去,总的时间复杂度为O(n * log2(n))
(2)、将这n个元素先顺序放入一个二叉树中造成一个彻底二叉树,而后来调整各个结点的位置来知足最大堆的特性。
如今咱们就来试一试第二种方法来建立一个最大堆:假如咱们有12个元素分别为:

{79,66,43,83,30,87,38,55,91,72,49,9}

将上诉15个数字放入一个二叉树中,确切地说是放入一个彻底二叉树中,以下:

 
 

可是这明显不符合最大堆的定义,因此咱们须要让该彻底二叉树转换成最大堆!怎么转换成一个最大堆呢?
  最大堆有一个特色就是 其各个子树都是一个最大堆,那么咱们就能够从把最小子树转换成一个最大堆,而后依次转换它的父节点对应的子树,直到最后的根节点所在的整个彻底二叉树变成最大堆。那么从哪个子树开始调整?

 

咱们从该彻底二叉树中的最后一个非叶子节点为根节点的子树进行调整,而后依次去找倒数第二个倒数第三个非叶子节点...

具体步骤

在作最大堆的建立具体步骤中,咱们会用到最大堆删除操做中结点位置互换的原理,即关键字值较小的结点会作下沉操做

  • 1)、就如同上面所说找到二叉树中倒数第一个非叶子结点87,而后看以该非叶子结点为根结点的子树。查看该子树是否知足最大堆要求,很明显目前该子树知足最大堆,因此咱们不须要移动结点。该子树最大移动次数为1
     
     
  • 2)、如今来到结点30,明显该子树不知足最大堆。在该结点的子结点较大的为72,因此结点72和结点30进行位置互换。该子树最大移动次数为1
     
     
  • 3)、一样对结点83作相似的操做。该子树最大移动次数为1
     
     
  • 4)、如今来到结点43,该结点的子结点有{87,38,9},对该子树作一样操做。因为结点43多是其子树结点中最小的,因此该子树最大移动次数为2
     
     
  • 5)、结点66一样操做,该子树最大移动次数为2
     
     
  • 6)、最后来到根结点79,该二叉树最高深度为4,因此该子树最大移动次数为3
     
     

自此经过上诉步骤建立的最大堆为:

 
 
 

因此从上面能够看出,该二叉树总的须要移动结点次数最大为:10

代码实现

void create_max_heap(void){
        int total = (*heap).key;
        /// 求倒数第一个非叶子结点
        int child = 2,parent = 1;
        for (int node = total/2; node>0; node--) {
            parent = node;
            child = 2*node;
            int max_node = 2*node+1;
            element temp = *(heap+parent);
            for (; child <= total; child *= 2,max_node = 2*parent+1) {
                if (child+1 <= total && (*(heap+child)).key < (*(heap+child+1)).key) {
                    child++;
                }
                if (temp.key > (*(heap+child)).key) {
                    break;
                }
                *(heap+parent) = *(heap+child);
                parent = child;
            }
            *(heap+parent) = temp;
        }
    }

/**
 *
 * @param heap  最大堆;
 * @param items 输入的数据源
 * @return 1成功,0失败
 */
int create_binary_tree(element *heap,int items[MAX_ELEMENTS]){
    int total;
    if (!items) {
        return 0;
    }
    element *temp = heap;
    heap++;
    for (total = 1; *items;total++,(heap)++,items = items + 1) {
        element ele = {*items};
        element temp_key = {total};
        *temp = temp_key;
        *heap = ele;
    }
    return 1;
}
///函数调用
int items[MAX_ELEMENTS] = {79,66,43,83,30,87,38,55,91,72,49,9};
element *position = heap;
create_binary_tree(position, items);
for (int i = 0; (*(heap+i)).key > 0; i++) {
  printf("binary tree element is %d\n",(*(heap + i)).key);
}
create_max_heap();
for (int i = 0; (*(heap+i)).key > 0; i++) {
  printf("heap element is %d\n",(*(heap + i)).key);
}

上诉代码在我机器上可以成功的构建一个最大堆。因为该彻底二叉树存在n个元素,那么他的高度为:log2(n+1),这就说明代码中的for循环会执行O(log2(n))次。所以其我理解的平均运行时间为:O(log2(n))。而其上界为当该二叉树为满二叉树时其时间复杂度为O((n)。

堆排序

堆排序要比空间复杂度为O(n)的归并排序要慢一些,可是要比空间复杂度为O(1)的归并排序要快!
  经过上面最大堆建立一节中咱们可以建立一个最大堆。出于该最大堆太大,我将其进行缩小以便进行画图演示。

 
 

最大堆的排序过程实际上是和最大堆的删除操做相似,因为最大堆的删除只能在根结点进行,当将根结点删除完成以后,就是将剩下的结点进行整理让其符合最大堆的标准。

 

  • 1)、把最大堆根结点91“删除”,第一次排序图示:
     
     

    进过这一次排序以后,91就处在最终的正确位置上,因此咱们只须要对余下的最大堆进行操做!这里须要注意一点:

⚠️⚠️⚠️注意,关于对余下进行最大堆操做时:
并不须要像建立最大堆时,从倒数第一个非叶子结点开始。由于在咱们只是对第一个和最后一个结点进行了交换,因此只有根结点的顺序不知足最大堆的约束,咱们只须要对第一个元素进行处理便可

  • 2)、继续对结点87进行相同的操做:

     
     

    一样,87的位置肯定。

     

  • 3)、如今咱们来肯定结点83的位置:

     
     

     

  • 4)、通过上诉步骤就不难理解堆排序的原理所在,最后排序结果以下:

     
     
     

通过上诉多个步骤以后,最终的排序结果以下:

[3八、4三、7二、7九、8三、8七、91]

很明显这是一个正确的从小到大的顺序。

编码实现

这里须要对上面的代码进行一些修改!由于在排序中,咱们的第0个元素是不用去放一个哨兵的,咱们的元素从原来的第一个位置改成从第0个位置开始放置元素。

void __swap(element *lhs,element *rhs){
    element temp = *lhs;
    *lhs = *rhs;
    *rhs = temp;
}

int create_binarytree(element *heap, int items[MAX_SIZE], int n){
    if (n <= 0) return 0;
    for (int i = 0; i < n; i++,heap++) {
        element value = {items[i]};
        *heap = value;
    }
    return 1;
}

void adapt_maxheap(element *heap ,int node ,int n){
    int parent = node - 1 < 0 ? 0 : node - 1;
    int child = 2 * parent + 1;/// 由于没有哨兵,因此在数组中的关系由原来的:parent = 2 * child => parent = 2 * child + 1
    int max_node = max_node = 2*parent+2 < n - 1 ? 2*parent+2 : n - 1;
    element temp = *(heap + parent);
    for (;child <= max_node; parent = child,child = child * 2 + 1,max_node = 2*parent+2 < n - 1 ? 2*parent+2 : n - 1) {
        if ((heap + child)->key <= (heap + child + 1)->key && child + 1 < n) {
            child++;
        }
        if ((heap + child)->key < temp.key) {
            break;
        }
        *(heap + parent) = *(heap + child);
    }
    *(heap + parent) = temp;
}

int create_maxheap(element *heap ,int n){

    for (int node = n/2; node > 0; node--) {
        adapt_maxheap(heap, node, n);
    }
    return 1;
}

void heap_sort(element *heap ,int n){
    ///建立一个最大堆
    create_maxheap(heap, n);
    ///进行排序过程
    int i = n - 1;
    while (i >= 0) {
        __swap(heap+0, heap + i);/// 将第一个和最后一个进行交换
        adapt_maxheap(heap, 0, i--);///将总的元素个数减一,适配成最大堆,这里只须要对首元素进行最大堆的操做
    }
}

调用:

/// 堆排序
int n = 7;
int items[7] = {87,79,38,83,72,43,91};
element heap[7];
create_binarytree(heap, items, n);
heap_sort(heap, n);///38,43,72,79,83,87,91

在实现堆排序时最须要注意的就是当没有哨兵以后,父结点和左右孩子结点之间的关系发生了变化:

parent = 2 * child + 1;///左孩子
parent = 2 * child + 2;///右孩子

关于对排序相关的知识点已经整理完了。其时间复杂度和归并排序的时间时间复杂度是同样的O(N*LogN)

结束语

当咱们在作和彻底二叉树有关的操做时,对于彻底二叉树采用顺序存储表示,须要记住对于任意一个下标为i(1 ≤ i ≤ n)的结点:父结点为:i / 2(i ≠ 1),若i = 1,则i是根节点。左子结点:2i(2i ≤ n), 若不知足则无左子结点。右子结点:2i + 1(2i + 1 ≤ n),若不知足则无右子结点。   关于最大堆的相关操做(插入、删除、建立和排序)已经一一学习完毕。这些操做中,删除、建立和排序思想很是相似,都是操做结点下沉。而插入操做相反,相似上浮的操做!

相关文章
相关标签/搜索