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

关于最大堆

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

图-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),若不满足则无右子结点。

最终我们选择数组作为最大堆的内存表现形式。

基本定义:

#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];

下面来看看最大堆的插入、删除和创建这三个最基本的操作。

最大堆的插入

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

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)、经过上诉步骤就不难理解堆排序的原理所在,最后排序结果如下:


经过上诉多个步骤之后,最终的排序结果如下:

[38、43、72、79、83、87、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),若不满足则无右子结点。
  关于最大堆的相关操作(插入、删除、创建和排序)已经一一学习完毕。这些操作中,删除、创建和排序思想非常类似,都是操作结点下沉。而插入操作相反,类似上浮的操作!

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

推荐阅读更多精彩内容

  • 第一章 绪论 什么是数据结构? 数据结构的定义:数据结构是相互之间存在一种或多种特定关系的数据元素的集合。 第二章...
    SeanCheney阅读 5,660评论 0 19
  • B树的定义 一棵m阶的B树满足下列条件: 树中每个结点至多有m个孩子。 除根结点和叶子结点外,其它每个结点至少有m...
    文档随手记阅读 13,012评论 0 25
  • 因为之前就复习完数据结构了,所以为了保持记忆,整理了一份复习纲要,复习的时候可以看着纲要想具体内容。 树 树的基本...
    牛富贵儿阅读 6,421评论 3 10
  • 数据结构与算法--从平衡二叉树(AVL)到红黑树 上节学习了二叉查找树。算法的性能取决于树的形状,而树的形状取决于...
    sunhaiyu阅读 7,579评论 4 32
  • 最近看了一篇新闻,讲的是上海取消了义务教育阶段的听力磁带,配图是一个小学生举着一本教科书,不禁思绪一转,发现自己离...
    577c06a08b67阅读 132评论 0 0