我的编程空间,编程开发者的网络收藏夹
学习永远不晚

C语言每日练习之二叉堆

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

C语言每日练习之二叉堆

一、堆的概念

1、概述

堆是计算机科学中一类特殊的数据结构的统称。实现有很多,例如:大顶堆,小顶堆,斐波那契堆,左偏堆,斜堆 等等。从子结点个数上可以分为二叉堆,N叉堆等等。本文将介绍的是 二叉堆。

2、定义

二叉堆本质是一棵完全二叉树,所以每次元素的插入删除都能保证 O ( l o g 2 n ) O(log_2n) O(log2​n)。根据堆的偏序规则,分为 小顶堆 和 大顶堆。小顶堆,顾名思义,根结点的关键字最小;大顶堆则相反。如图所示,表示的是一个大顶堆。

3、性质

以大顶堆为例,它总是满足下列性质:
1)空树是一个大顶堆;

2)大顶堆中某个结点的关键字 小于等于 其父结点的关键字;

3)大顶堆是一棵完全二叉树。有关完全二叉树的内容,可以参考:画解完全二叉树。

如下图所示,任意一个从叶子结点到根结点的路径总是一个单调不降的序列。

小顶堆只要把上文中的 小于等于 替换成 大于等于 即可。

4、作用

还是以大顶堆为例,堆能够在 O ( 1 ) O(1) O(1) 的时间内,获得 关键字 最大的元素。并且能够在 O ( l o g 2 n ) O(log_2n) O(log2​n) 的时间内执行插入和删除。一般用来做 优先队列 的实现。

二、堆的存储结构

学习堆的过程中,我们能够学到一种新的表示形式。就是:利用 数组 来表示 链式结构。怎么理解这句话呢?

由于堆本身是一棵完全二叉树,所以我们可以把每个结点,按照层序映射到一个顺序存储的数组中,然后利用每个结点在数组中的下标,来确定结点之间的关系。

如图所示,描述的是堆结点下标和结点之间的关系,结点上的数字代表的是 数组下标。从左往右按照层序进行连续递增。

1、根结点编号

根结点的编号,看作者的喜好。可以用 0 或者 1。本文的作者是 C语言 出身,所以更倾向于选择 0 作为根结点的编号(因为用 1 作为根结点编号的话,数组的第 0 个元素就浪费了)。

我们可以用一个宏定义来实现它的定义,如下:

#define root 0

2、孩子结点编号

那么,根结点的两个左右子树的编号,就分别为 1 和 2 了。以此类推,按照层序进行编号的话,1 的左右子树编号为 3 和 4;2 的左右子树编号为 5 和 6。

根据数学归纳法,对于编号为 i i i 的结点,它的左子树编号为 2 i + 1 2i+1 2i+1,右子树编号为 2 i + 2 2i+2 2i+2。用宏定义实现如下:

#define lson(idx) (2*idx+1)
#define rson(idx) (2*idx+2)

由于这里涉及到乘 2,所以我们还可以用左移位运算来优化乘法运算,如下:

#define lson(idx) (idx << 1|1)
#define rson(idx) ((idx + 1) << 1)

3、父结点编号

同样,父结点编号也可以通过数学归纳法得出,当结点编号为 i i i 时,它的父结点编号为 i − 1 2 \frac {i-1} {2} 2i−1​,利用C语言实现如下:

#define parent(idx) ((idx - 1) / 2)

这里涉及到除 2,可以利用右移运算符进行优化,如下:

#define parent(idx) ((idx - 1) >> 1)

这里利用补码的性质,根结点的父结点得到的值为 -1;

4、数据域

堆数据元素的数据域可以定义两个:关键字 和 值,其中关键字一般是整数,方便进行比较确定大小关系;值则是用于展示用,可以是任意类型,可以用typedef struct进行定义如下:

typedef struct {
    int key;      // (1)
    void *any;    // (2)
}DataType;
  • (1) 关键字;
  • (2) 值,定义成一个空指针,可以用来表示任意类型;

5、堆的数据结构

由于堆本质上是一棵完全二叉树,所以将它一一映射到数组后,一定是连续的。我们可以用一个数组来代表一个堆,在C语言中的数组拥有一个固定长度,可以用一个Heap结构体表示如下:

typedef struct {
    DataType *data;  // (1)
    int size;        // (2)
    int capacity;    // (3)
}Heap;
  • (1) 堆元素所在数组的首地址;
  • (2) 堆元素个数;
  • (3) 堆的最大元素个数;

三、堆的常用接口

1、元素比较

两个堆元素的比较可以采用一个比较函数compareData来完成,比较过程就是对关键字key进行比较的过程,以大顶堆为例:

a. 大于返回 -1,代表需要执行交换;

b. 小于返回 1,代表需要执行交换;

c. 等于返回 0,代表需要执行交换;

int compareData(const DataType* a, const DataType* b) {
    if(a->key > b->key) {
        return -1;
    }else if(a->key < b->key) {
        return 1;
    }
    return 0;
}

2、交换元素

交换两个元素的位置,也是堆这种数据结构中很常见的操作,C语言实现也比较简单,如下:

void swap(DataType* a, DataType* b) {
    DataType tmp = *a;
    *a = *b;
    *b = tmp;
}

3、空判定

空判定是一个查询接口,即询问堆是否是空的,实现如下:

bool HeapIsEmpty(Heap *heap) {
    return heap->size == 0;
}

4、满判定

满判定是一个查询接口,即询问堆是否是满的,实现如下:

bool heapIsFull(Heap *heap) {
    return heap->size == heap->capacity;
}

5、上浮操作

对于大顶堆而言,从它叶子结点到根结点的元素关键字一定是单调不降的,如果某个元素出现了比它的父结点大的情况,就需要进行上浮操作。

上浮操作就是对 当前结点 和 父结点 进行比较,如果它的关键字比父结点大(compareData返回-1的情况),将它和父结点进行交换,继续上浮操作;否则,终止上浮操作。

如图所示,代表的是一个关键字为 95 的结点,通过不断上浮,到达根结点的过程。上浮完毕以后,它还是一个大顶堆。

上浮过程的 C语言 实现如下:

void heapShiftUp(Heap* heap, int curr) {               // (1)
    int par = parent(curr);                            // (2)
    while(par >= root) {                               // (3)
        if( compareData( &heap->data[curr], &heap->data[par] ) < 0 ) {
            swap(&heap->data[curr], &heap->data[par]); // (4) 
            curr = par;
            par = parent(curr);
        }else {
            break;                                     // (5) 
        }
    }
}

(1) heapShiftUp这个接口是一个内部接口,所以用小写驼峰区分,用于实现对堆中元素进行插入的时候的上浮操作;

(2) curr表示需要进行上浮操作的结点在堆中的编号,par表示curr的父结点编号;

(3) 如果已经是根结点,则无须进行上浮操作;

(4) 子结点的关键字 大于 父结点的关键字,则执行交换,并且更新新的 当前结点 和 父结点编号;

(5) 否则,说明已经正确归位,上浮操作结束,跳出循环;

6、下沉操作

对于大顶堆而言,从它 根结点 到 叶子结点 的元素关键字一定是单调不增的,如果某个元素出现了比它的某个子结点小的情况,就需要进行下沉操作。

下沉操作就是对 当前结点 和 关键字相对较小的子结点 进行比较,如果它的关键字比子结点小,将它和这个子结点进行交换,继续下沉操作;否则,终止下沉操作。

如图所示,代表的是一个关键字为 19 的结点,通过不断下沉,到达叶子结点的过程。下沉完毕以后,它还是一个大顶堆。

下沉过程的 C语言 实现如下:

void heapShiftDown(Heap* heap, int curr) {            // (1)    int son = lson(curr);                             // (2)    while(son < heap->size) {        if( rson(curr) < heap->size ) {            if( compareData( &heap->data[rson(curr)], &heap->data[son] ) < 0 ) {                son = rson(curr);                     // (3)             }                }        if( compareData( &heap->data[son], &heap->data[curr] ) < 0 ) {            swap(&heap->data[son], &heap->data[curr]); // (4)            curr = son;            son = lson(curr);        }else {            break;                                     // (5)         }    }}void heapShiftDown(Heap* heap, int curr) {            // (1)
    int son = lson(curr);                             // (2)
    while(son < heap->size) {
        if( rson(curr) < heap->size ) {
            if( compareData( &heap->data[rson(curr)], &heap->data[son] ) < 0 ) {
                son = rson(curr);                     // (3) 
            }        
        }
        if( compareData( &heap->data[son], &heap->data[curr] ) < 0 ) {
            swap(&heap->data[son], &heap->data[curr]); // (4)
            curr = son;
            son = lson(curr);
        }else {
            break;                                     // (5) 
        }
    }
}

(1) heapShiftDown这个接口是一个内部接口,所以用小写驼峰区分,用于对堆中元素进行删除的时候的下沉调整;

(2) curr表示需要进行下沉操作的结点在堆中的编号,son表示curr的左儿子结点编号;

(3) 始终选择关键字更小的子结点;

(4) 子结点的值小于父结点,则执行交换;

(5) 否则,说明已经正确归位,下沉操作结束,跳出循环;

四、堆的创建

1、算法描述

通过给定的数据集合,创建堆。可以先创建堆数组的内存空间,然后一个一个执行堆的插入操作。插入操作的具体实现,会在下文继续讲解。

2、动画演示

3、源码详解

Heap* HeapCreate(DataType *data, int dataSize, int maxSize) {    // (1)
    int i;
    Heap *h = (Heap *)malloc( sizeof(Heap) );                    // (2)
    h->data = (DataType *)malloc( sizeof(DataType) * maxSize );  // (3)
    h->size = 0;                                                 // (4)
    h->capacity = maxSize;                                       // (5)
    for(i = 0; i < dataSize; ++i) {
        HeapPush(h, data[i]);                                    // (6)
    }
    return h;                                                    // (7)
}

(1) 给定一个元素个数为dataSize的数组data,创建一个最大元素个数为maxSize的堆并返回堆的结构体指针;

(2) 利用malloc申请堆的结构体的内存;

(3) 利用malloc申请存储堆数据的数组的内存空间;

(4) 初始化空堆;

(5) 初始化堆最大元素个数为maxSize

(6) 遍历数组执行堆的插入操作,插入的具体实现HeapPush接下来会讲到;

(7) 最后,返回堆的结构体指针;

五、堆元素的插入

1、算法描述

堆元素的插入过程,就是先将元素插入堆数组的最后一个位置,然后执行上浮操作;

2、动画演示

在这里插入图片描述

3、源码详解

bool HeapPop(Heap *heap) {
    if(HeapIsEmpty(heap)) {
        return false;                               // (1)
    }
    heap->data[root] = heap->data[ --heap->size ];  // (2)
    heapShiftDown(heap, root);                      // (3)
    return true;
}

(1) 堆已满,不能进行插入;

(2) 插入堆数组的最后一个位置;

(3) 对最后一个位置的 堆元素 执行上浮操作;

五、堆元素的删除

1、算法描述

堆元素的删除,只能对堆顶元素进行操作,可以将数组的最后一个元素放到堆顶,然后对堆顶元素进行下沉操作。

2、动画演示

在这里插入图片描述

3、源码详解

bool HeapPop(Heap *heap) {
    if(HeapIsEmpty(heap)) {
        return false;                               // (1)
    }
    heap->data[root] = heap->data[ --heap->size ];  // (2)
    heapShiftDown(heap, root);                      // (3)
    return true;
}
  • (1) 堆已空,无法执行删除;
  • (2) 将堆数组的最后一个元素放入堆顶,相当于删除了堆顶元素;
  • (3) 对堆顶元素执行下沉操作;

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注编程网的更多内容!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

C语言每日练习之二叉堆

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

C语言每日一练------Day(6)

本专栏为c语言练习专栏,适合刚刚学完c语言的初学者。本专栏每天会不定时更新,通过每天练习,进一步对c语言的重难点知识进行更深入的学习。 今日练习题关键字:整数转换 异或 💓博主csdn个人主页:小小unic
2023-08-30

C语言每日一练------Day(5)

本专栏为c语言练习专栏,适合刚刚学完c语言的初学者。本专栏每天会不定时更新,通过每天练习,进一步对c语言的重难点知识进行更深入的学习。 今日练习题关键字:错误的集合 密码检查 💓博主csdn个人主页:小小u
2023-08-30

C语言如何解决二叉堆问题

这篇文章给大家分享的是有关C语言如何解决二叉堆问题的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。一、堆的概念1、概述堆是计算机科学中一类特殊的数据结构的统称。实现有很多,例如:大顶堆,小顶堆,斐波那契堆,左偏堆,
2023-06-26

C语言之二叉树的遍历

这篇文章主要介绍了C语言中二叉树的遍历:前序、中序、后序,认识二叉树结构最简单的方式就是遍历二叉树,感兴趣的小伙伴可以参考阅读本文
2023-05-14

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录