C语言每日练习之二叉堆

目录
  • 一、堆的概念
    • 1、概述
    • 2、定义
    • 3、性质
    • 4、作用
  • 二、堆的存储结构
    • 1、根结点编号
    • 2、孩子结点编号
    • 3、父结点编号
    • 4、数据域
    • 5、堆的数据结构
  • 三、堆的常用接口
    • 1、元素比较
    • 2、交换元素
    • 3、空判定
    • 4、满判定
    • 5、上浮操作
    • 6、下沉操作
  • 四、堆的创建
    • 1、算法描述
    • 2、动画演示
    • 3、源码详解
  • 五、堆元素的插入
    • 1、算法描述
    • 2、动画演示
    • 3、源码详解
  • 五、堆元素的删除
    • 1、算法描述
    • 2、动画演示
    • 3、源码详解
  • 总结

一、堆的概念

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) 对堆顶元素执行下沉操作;

总结

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

(0)

相关推荐

  • python下实现二叉堆以及堆排序的示例

    堆是一种特殊的树形结构, 堆中的数据存储满足一定的堆序.堆排序是一种选择排序, 其算法复杂度, 时间复杂度相对于其他的排序算法都有很大的优势. 堆分为大头堆和小头堆, 正如其名, 大头堆的第一个元素是最大的, 每个有子结点的父结点, 其数据值都比其子结点的值要大.小头堆则相反. 我大概讲解下建一个树形堆的算法过程: 找到N/2 位置的数组数据, 从这个位置开始, 找到该节点的左子结点的索引, 先比较这个结点的下的子结点, 找到最大的那个, 将最大的子结点的索引赋值给左子结点, 然后将最大的子结点

  • 彻底搞定堆排序:二叉堆

    目录 二叉堆 插入 删除 构建 二叉堆代码实现 总结 二叉堆 什么是二叉堆 二叉堆本质上是一种完全二叉树,它分为两个类型 最大堆:最大堆的任何一个父节点的值,都大于等于它的左.右孩子节点的值(堆顶就是整个堆的最大元素) 最小堆:最小堆的任何一个父节点的值,都小于等于它的左.右孩子节点的值(堆顶就是整个堆的最小元素) 二叉堆的根节点叫做堆顶 二叉堆的基本操作 插入节点 删除节点 构建二叉堆 这几种操作都基于堆的自我调整,所谓堆自我调整,就是把一个不符合堆的完全二叉树,调整成一个堆,下面以最小堆为例

  • Java语言实现二叉堆的打印代码分享

    二叉堆是一种特殊的堆,二叉堆是完全二元树(二叉树)或者是近似完全二元树(二叉树).二叉堆有两种:最大堆和最小堆.最大堆:父结点的键值总是大于或等于任何一个子节点的键值:最小堆:父结点的键值总是小于或等于任何一个子节点的键值. 打印二叉堆:利用层级关系 我这里是先将堆排序,然后在sort里执行了打印堆的方法printAsTree() public class MaxHeap<T extends Comparable<? super T>> { private T[] data; pr

  • java编程实现优先队列的二叉堆代码分享

    这里主要介绍的是优先队列的二叉堆Java实现,代码如下: package practice; import edu.princeton.cs.algs4.StdRandom; public class TestMain { public static void main(String[] args) { int[] a = new int[20]; for (int i = 0; i < a.length; i++) { int temp = (int)(StdRandom.random()*1

  • PHP利用二叉堆实现TopK-算法的方法详解

    前言 在以往工作或者面试的时候常会碰到一个问题,如何实现海量TopN,就是在一个非常大的结果集里面快速找到最大的前10或前100个数,同时要保证内存和速度的效率,我们可能第一个想法就是利用排序,然后截取前10或前100,而排序对于量不是特别大的时候没有任何问题,但只要量特别大是根本不可能完成这个任务的,比如在一个数组或者文本文件里有几亿个数,这样是根本无法全部读入内存的,所以利用排序解决这个问题并不是最好的,所以我们这里就用php去实现一个小顶堆来解决这个问题. 二叉堆 二叉堆是一种特殊的堆,二

  • C语言每日练习之二叉堆

    目录 一.堆的概念 1.概述 2.定义 3.性质 4.作用 二.堆的存储结构 1.根结点编号 2.孩子结点编号 3.父结点编号 4.数据域 5.堆的数据结构 三.堆的常用接口 1.元素比较 2.交换元素 3.空判定 4.满判定 5.上浮操作 6.下沉操作 四.堆的创建 1.算法描述 2.动画演示 3.源码详解 五.堆元素的插入 1.算法描述 2.动画演示 3.源码详解 五.堆元素的删除 1.算法描述 2.动画演示 3.源码详解 总结 一.堆的概念 1.概述 堆是计算机科学中一类特殊的数据结构的统

  • 理解二叉堆数据结构及Swift的堆排序算法实现示例

    二叉堆的性质 1.二叉堆是一颗完全二叉树,最后一层的叶子从左到右排列,其它的每一层都是满的 2.最小堆父结点小于等于其每一个子结点的键值,最大堆则相反 3.每个结点的左子树或者右子树都是一个二叉堆 下面是一个最小堆: 堆的存储 通常堆是通过一维数组来实现的.在起始数组为 0 的情形中: 1.父节点i的左子节点在位置 (2*i+1); 2.父节点i的右子节点在位置 (2*i+2); 3.子节点i的父节点在位置 floor((i-1)/2); 维持堆的性质 我们以最大堆来介绍(后续会分别给出最大堆和

  • Python实现二叉堆

    优先队列的二叉堆实现 在前面的章节里我们学习了"先进先出"(FIFO)的数据结构:队列(Queue).队列有一种变体叫做"优先队列"(Priority Queue).优先队列的出队(Dequeue)操作和队列一样,都是从队首出队.但在优先队列的内部,元素的次序却是由"优先级"来决定:高优先级的元素排在队首,而低优先级的元素则排在后面.这样,优先队列的入队(Enqueue)操作就比较复杂,需要将元素根据优先级尽量排到队列前面.我们将会发现,对于下一

  • Java实现二叉堆、大顶堆和小顶堆

    目录 什么是二叉堆 什么是大顶堆.小顶堆 建堆 程序实现 建立大顶堆 逻辑过程 程序实现 建立小顶堆 逻辑过程 程序实现 从堆顶取数据并重构大小顶堆 什么是二叉堆 二叉堆就是完全二叉树,或者是靠近完全二叉树结构的二叉树.在二叉树建树时采取前序建树就是建立的完全二叉树.也就是二叉堆.所以二叉堆的建堆过程理论上讲和前序建树一样. 什么是大顶堆.小顶堆 二叉堆本质上是一棵近完全的二叉树,那么大顶堆和小顶堆必然也是满足这个结构要求的.在此之上,大顶堆要求对于一个节点来说,它的左右节点都比它小:小顶堆要求

  • Java 数据结构与算法系列精讲之二叉堆

    目录 概述 优先队列 二叉堆 二叉堆实现 获取索引 添加元素 siftUp 完整代码 概述 从今天开始, 小白我将带大家开启 Java 数据结构 & 算法的新篇章. 优先队列 优先队列 (Priority Queue) 和队列一样, 是一种先进先出的数据结构. 优先队列中的每个元素有各自的优先级, 优先级最高的元素最先得到服务. 如图: 二叉堆 二叉堆 (Binary Heap) 是一种特殊的堆, 二叉堆具有堆的性质和二叉树的性质. 二叉堆中的任意一节点的值总是大于等于其孩子节点值. 如图: 二

随机推荐