基础数据结构与算法之堆-C语言实现

0-概述

,又称优先队列(Priority Queue),是一种特殊的队列,取出元素的顺序是按照元素的优先权(关键字)的大小,而不是元素进入队列的先后顺序。

一般用完全二叉树实现堆。堆可分为大顶堆小顶堆两种,顾名思义,大顶堆就是对于任意子树,根节点都比左右子节点大;小顶堆就是对于任意子树,根节点都比左右子节点小,二者的实现方式相同,逻辑结构都是完全二叉树,采用数组存储。

下面是4个堆的例子,前2个是大顶堆,后2个是小顶堆:

在这里插入图片描述

堆的主要操作有:

  1. 创建堆
  2. 向堆中插入元素
  3. 删除堆顶元素
  4. 建堆(将一个无序数组排成堆)

相关函数声明如下:

//创建一个容量为MaxSize的最大堆
MaxHeap Create(int MaxSize);
//向堆中插入元素
void Insert(MaxHeap H, ElementType item);
// Pop掉大顶堆的根节点:从根节点开始,用最大堆中的最后一个元素向上过滤下层节点(比该元素大的上移)
ElementType DeleteMax(MaxHeap H);
//建堆:从最后一个有儿子的节点开始不断的向前迭代将其子树调整为堆
void buildMaxHeap(MaxHeap H);
//向下过滤节点
void percDown(MaxHeap H, int n);

其中percDown是堆操作的核心函数,用于向下过滤节点

下面是堆操作的实现:

1-堆结构的定义

首先是堆结构的定义:

//哨兵值
#define MAXDATA 99999
typedef int ElementType;
struct HeapStruct {
    ElementType* Data;
    int Size;
    int Capacity;
};
//定义一个最大堆类型:指向HeapStruct的指针
typedef struct HeapStruct* MaxHeap;
  1. 宏定义一个极大的哨兵值99999,存放在堆中数组中的0号位置,方便操作(跳出循环)。
  2. 堆结构中包含三个分量:指向堆区的数组指针,堆的当前大小,堆的容量
  3. 定义一个大顶堆MaxHeap类型为指向堆结构的指针

2-创造堆

堆的创造很简单,就是给结构中的指针开辟空间,然后将成员变量做初始化,最后将哨兵设置好,代码如下:

MaxHeap Create(int MaxSize) {
    MaxHeap H = malloc(sizeof(struct HeapStruct));
    //为数组开空间,0号元素存的是哨兵,所以要+1
    H->Data = malloc((MaxSize + 1) * sizeof(struct HeapStruct));
    H->Size = 0;
    H->Capacity = MaxSize;
    //哨兵
    H->Data[0] = MAXDATA;
    return H;
}

3-向堆中插入元素

一个大顶堆的插入过程如下图所示:
在这里插入图片描述

堆采用数组存储,那么执行插入操作自然而然就插在最后一个位置,但是插入后很可能破坏堆的有序性,所以需要进行位置调整:比较当前节点是否小于父节点,若小于,则当前位置就正好可以插入,满足堆的有序性。否则,若当前节点大于父节点while (item > H->Data[i / 2]),就让父节点下来到当前位置i自己上去i/2的位置,代码实现如下:

void Insert(MaxHeap H, ElementType item) {
    if (isFull(H)) {
        printf("堆已满\n");
        return;
    }
    //刚开始假设插入的位置在数组的最后
    int i = ++H->Size;
    //只要item比父节点大:
    while (item > H->Data[i / 2]) {
        //父节点下来
        H->Data[i] = H->Data[i / 2];
        //我上去
        i = i / 2;
    }
    //跳出循环时i锚定了合适的插入位置,赋值
    H->Data[i] = item;
    return;
}

4-删除堆顶元素

一个大顶堆的删除过程如下图所示:
在这里插入图片描述

首先我们将堆顶元素存入MaxItem,然后将数组中最后一个元素存入tmp,并将Size--,接下来我们需要重新调整堆,就是要给tmp找到合适的存放位置(由Parent指向)。

Parent初始时指向根节点,只要Parent还有左孩子while (Parent * 2 <= H->Size),就执行循环:

  1. Child指向Parent的左孩子;
  2. 如果Parent有右孩子,且右孩子比左孩子更大,就让Child指向右孩子,现在Child已经指向了Parent的左右孩子中较大者
  3. 如果tmpChild大,说明tmp可以放在这,break;
  4. 否则,应该让Child上来,自己下去;

代码实现如下:

ElementType DeleteMax(MaxHeap H) {
    if (isEmpty(H)) {
        printf("堆为空\n");
        return H->Data[0];
    }
    ElementType MaxItem = H->Data[1];
    ElementType tmp = H->Data[H->Size--];
    int Parent = 1;
    int Child;
    //算法核心:给tmp找到合适的位置
    while (Parent * 2 <= H->Size) {
        // Child指向左孩子
        Child = 2 * Parent;
        // Child指向左右孩子中最大者
        if ((Child != H->Size) && H->Data[Child] < H->Data[Child + 1]) {
            Child++;
        }
        //如果tmp>左右孩子最大者,说明tmp在这里坐得住,跳出循环
        if (tmp > H->Data[Child]) {
            break;
        } else {
            //让孩子上来
            H->Data[Parent] = H->Data[Child];
            //自己下去
            Parent = Child;
        }
    }
    H->Data[Parent] = tmp;
    return MaxItem;
}

5-建堆(将一个无序数组排成堆)

作为树的一种,堆也是一种递归的结构:对于任何一个节点,其左子树是一个堆,右子树也是一个堆,以此类推,直到叶子节点。

建堆的过程buildMaxHeap也类似于一个递归的过程:从最后一个有孩子的节点开始,将当前子树调整为堆,然后向前迭代,不断的将当前子树调整为堆,直到根节点。

建堆的核心在于调整节点使当前子树为堆,这在上面“删除堆顶元素”中已经提到过了,就是对于当前节点,要不断的跟左右孩子比较,寻找合适的插入位置,也就是上面的循环体中内容。

代码实现如下:

void buildMaxHeap(MaxHeap H) {
    //从最后一个有儿子的节点开始
    for (int i = H->Size / 2; i > 0; i--) {
        percDown(H, i);
    }
}

void percDown(MaxHeap H, int n) {
    ElementType top;
    int Child;
    int Parent = n;
    top = H->Data[n];
    //向下过滤
    while (Parent * 2 <= H->Size) {
        Child = Parent * 2;
        if (Child != H->Size && H->Data[Child] < H->Data[Child + 1]) {
            Child++;
        }
        if (top > H->Data[Child]) {
            break;
        } else {
            H->Data[Parent] = H->Data[Child];
            Parent = Child;
        }
    }
    H->Data[Parent] = top;
}

6-源代码

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值