数据结构与算法⑪(第四章_中)堆的分步构建

简介: 数据结构与算法⑪(第四章_中)堆的分步构建
【百度百科】堆(Heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做 一棵完全二叉树的数组对象

完全二叉树的性质就是堆的性质,堆是完全二叉树的顺序结构存储。

① 堆总是一棵完全二叉树

② 堆中的某个节点的值总是不大于或不小于其父节点的值。

堆的逻辑结构是完全二叉树,物理(存储)结构是数组

1.完整Heap.h

和以前学的数据结构一样,先定义出堆的结构体,然后实现接口函数。

先给出Heap.h的代码:

#pragma once
 
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
 
typedef int HPDataType;
 
typedef struct Heap 
{
    HPDataType* array;  //指向动态开辟的数组
    int size;           //有效数据的个数
    int capacity;       //容量空间的大小
} Heap;
 
void HeapInit(Heap* php);//初始化堆
void HeapDestroy(Heap* php);//堆的销毁
void HeapPrint(Heap* php);//堆的打印
bool HeapEmpty(Heap* php);//判断堆是否为空
HPDataType HeapTop(Heap* php);//返回堆顶数据
int HeapSize(Heap* php);//统计堆的个数
void HeapCheckCapacity(Heap* php);//检查容量
void Swap(HPDataType* px, HPDataType* py);//交换函数
 
void HeapPush(Heap* php, HPDataType x);//堆的插入
 
void BigAdjustUp(int* arr, int child);//大根堆上调
 
void SmallAdjustUp(int* arr, int child);//小根堆上调
 
void HeapPop(Heap* php);//堆的删除
 
void SmallAdjustDown(int* arr, int n, int parent);//小根堆下调
 
void BigAdjustDown(int* arr, int n, int parent);//大根堆下调

2.八个简单函数

这八个函数前几篇数据结构文章都讲过类似的了,直接放出来

void HeapInit(Heap* php)//初始化堆
{
    assert(php);
    php->array = NULL;
    php->size = php->capacity = 0;
}
 
void HeapDestroy(Heap* php) //堆的销毁
{
    assert(php);
    free(php->array);
    php->capacity = php->size = 0;
}
 
void HeapPrint(Heap* php) //堆的打印
{
    for (int i = 0; i < php->size; i++) 
    {
        printf("%d ", php->array[i]);
    }
    printf("\n");
}
 
bool HeapEmpty(Heap* php)//判断堆是否为空
{
    assert(php);
 
    return php->size == 0; // 如果为size为0则表示堆为空
}
 
HPDataType HeapTop(Heap* php) //返回堆顶数据
{
    assert(php);
    assert(!HeapEmpty(php));
 
    return php->array[0];
}
 
int HeapSize(Heap* php) //统计堆的个数
{
    assert(php);
 
    return php->size;
}
 
void HeapCheckCapacity(Heap* php) //检查容量 写过很多次了
{
    if (php->size == php->capacity) 
    {
        int newCapacity = php->capacity == 0 ? 4 : (php->capacity * 2); 
   HPDataType* tmpArray = (HPDataType*)realloc(php->array, sizeof(HPDataType) * newCapacity);
        if (tmpArray == NULL)
        {  
            printf("realloc failed!\n");
            exit(-1);
        }
        //更新他们的大小
        php->array = tmpArray;
        php->capacity = newCapacity;
    }
}
 
void Swap(HPDataType* px, HPDataType* py) //交换函数
{
    HPDataType tmp = *px;
    *px = *py;
    *py = tmp;
}

3.大堆的插入(HeapPush)

void HeapPush(HP* php, HPDataType x) 
{
    assert(php);  
    // 写到这里写一个检查是否需要增容
    HeapCheckCapacity(php);
    // 插入数据
    php->array[php->size] = x;
    php->size++;
    // 写到这里写一个向上调整 传入目标数组,和插入的数据(即 size - 1)。
    AdjustUp(php->array, php->size - 1); 
}

插入的核心思路:

① 先将元素插入到堆的末尾,即最后一个孩子之后。

② 插入之后如果堆的性质遭到破坏,就将新插入的节点顺着其的父亲往上调整到合适位置。

直到调到符合堆的性质为止。


根据堆的性质,如果不满足大堆和小堆,出现子大于父或父大于子的情况,

为了保证插入之后堆还是堆,我们就需要进行自下往上的调整。

堆插入数据对其他节点没有影响,只是可能会影响从它到根节点路径上节点的关系。

比如下面的情况:新插入的为 60,子大于父(60 > 56 ),这时就需要交换。

先把父亲赋值给孩子,再把孩子赋值给父亲,再让父亲往上走,判断是否比父亲大,如果大就再进行交换。 为了搞定这些情况,我们就需要写一个 "向上调整" 的算法(最坏调到根停止):

3.1大堆向上调整(BigAdjustUp)

void BigAdjustUp(int* arr, int child) //大根堆上调
{
    assert(arr);
    // 首先根据公式计算算出父亲的下标
    int parent = (child - 1) / 2;
    // 最坏情况:调到根,child=parent 当child为根节点时结束(根节点永远是0)
    while (child > 0) //不能写parent >= 0
    {                 //为什么呢?最后一次往上走时,
                     //child = parent;  此时child = 0
                     //parent = (child - 1) / 2;
                     //(0 - 1) / 2 = 0  这么一来parent仍然会是0
                     //导致parent根本就不会小于0
        if (arr[child] > arr[parent]) // 如果孩子大于父亲(不符合堆的性质)
        {  
            // 交换他们的值
            Swap(&arr[child], &arr[parent]);
            // 往上走
            child = parent;
            parent = (child - 1) / 2;
        }
        else// 如果孩子小于父亲(符合堆的性质)
        {  
            break;// 跳出循环
        }
    }
}

3.2小堆向上调整(SmallAdjustUp)

如果我们想改成小堆向上调整呢?

只需要改变一下大于小于判断条件即可:

void SmallAdjustUp(int* arr, int child) //小堆上调
{
    assert(arr);
    // 首先根据公式计算算出父亲的下标
    int parent = (child - 1) / 2;
    // 最坏情况:调到根,child=parent 当child为根节点时结束(根节点永远是0)
    while (child > 0)
    {
        if (arr[child] < arr[parent])  // 如果孩子小于父亲(不符合小堆的性质)
        { 
            Swap(&arr[child], &arr[parent]);
            // 往上走
            child = parent;
            parent = (child - 1) / 2;
        }
        else  // 如果孩子小于父亲(符合堆的性质)
        {  
            break;
        }
    }
}

4.堆的删除(HeapPop)

因为在讲堆的插入时我们用大堆演示的,我们这里先用小堆来演示。

删除的核心思路:删除堆,删除的是堆顶的数据。就是删除这个树的根。

如果直接挪动数据把堆顶数据覆盖,那么堆的结构就乱了,还要走一遍插入的逻辑,效率太低

所以有人想出一种很巧妙的方法:

将堆顶的数据跟最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。

(向下调整算法堆排序要使用,使用前提是左子树和右子树都是大堆/小堆)

① 将堆顶元素与堆中最后一个元素进行交换。

② 删除堆中的最后一个元素。

③ 将堆顶元素向下调整到满足堆特性为止。

void HeapPop(HP* php)
 {
    assert(php);
    assert(!HeapIsEmpty(php));
    // 删除数据
    Swap(&php->array[0], &php->array[php->size - 1]);
    php->size--;
    // 写到这里写一个向下调整 传入目标数组,,数组的大小,调整位置的起始位置
    SmalljustDown(php->array, php->size, 0);
}

4.1小堆向下调整(SmallAdjustDown)

向下调整,把它调整成堆,跟左右孩子中小的那个交换。

结束条件(有一个成立即可):

① 父<=小的孩子,则停止。

② 调整到叶子(因为叶子特征为没有孩子,左孩子下标超出数组范围,就不存在了)。

//小根堆下调 左右子树为小根堆,根节点不满足时使用  左右子树不满足就对其进行下调
void SmallAdjustDown(int* arr, int n, int parent) 
{
    int child = parent * 2 + 1; // 默认为左孩子
    while (child < n) // 叶子内
    { 
        // 选出左右孩子中小的那一个
        if (child + 1 < n && arr[child + 1] < arr[child]) //左孩子+1为右孩子
        {    //如果 child + 1 比 n 大,就说明没有右孩子,默认是对的
            child++;//左孩子+1更新为右孩子
        }
        if (arr[child] < arr[parent])// 如果孩子小于父亲(不符合小堆的性质)
        { 
            // 交换它们的值
            Swap(&arr[child], &arr[parent]);
            // 往下走
            parent = child;
            child = parent * 2 + 1;
        }
        else // 如果孩子大于父亲(符合小堆的性质)
        { 
            break;
        }
    }
}

如果我们想改成大堆呢?

也只需要改变一下大于小于判断条件即可:

① 选出左右孩子大的那一个。

② 小堆是所有父亲都小于孩子,大堆则是所有父亲都要大于孩子,我们来改一下:

4.2大堆向下调整(BigAdjustDown)

void BigAdjustDown(int* arr, int n, int parent) //大堆下调
{
    int child = parent * 2 + 1; // 默认为左孩子
    while (child < n) { // 叶子内
        // 选出左右孩子中大的那一个
        if (child + 1 < n && arr[child + 1] > arr[child]) 
        {
            child++;
        }
        if (arr[child] > arr[parent]) // 如果孩子大于父亲(不符合大堆的性质)
        {
            // 交换它们的值
            Swap(&arr[child], &arr[parent]);
            // 往下走
            parent = child;
            child = parent * 2 + 1;
        }
        else  // 如果孩子小于父亲(符合大堆的性质)
        {
            break;
        }
    }
}

5.堆的构建完整代码

Heap.h

#pragma once
 
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
 
typedef int HPDataType;
 
typedef struct Heap 
{
    HPDataType* array;  //指向动态开辟的数组
    int size;           //有效数据的个数
    int capacity;       //容量空间的大小
} Heap;
 
void HeapInit(Heap* php);//初始化堆
void HeapDestroy(Heap* php);//堆的销毁
void HeapPrint(Heap* php);//堆的打印
bool HeapEmpty(Heap* php);//判断堆是否为空
HPDataType HeapTop(Heap* php);//返回堆顶数据
int HeapSize(Heap* php);//统计堆的个数
void HeapCheckCapacity(Heap* php);//检查容量
void Swap(HPDataType* px, HPDataType* py);//交换函数
 
void HeapPush(Heap* php, HPDataType x);//堆的插入
 
void BigAdjustUp(int* arr, int child);//大根堆上调
 
void SmallAdjustUp(int* arr, int child);//小根堆上调
 
void HeapPop(Heap* php);//堆的删除
 
void SmallAdjustDown(int* arr, int n, int parent);//小根堆下调
 
void BigAdjustDown(int* arr, int n, int parent);//大根堆下调

Heap.c

#define _CRT_SECURE_NO_WARNINGS 1
 
#include "Heap.h"
 
void HeapInit(Heap* php)//初始化堆
{
    assert(php);
    php->array = NULL;
    php->size = php->capacity = 0;
}
 
void HeapDestroy(Heap* php) //堆的销毁
{
    assert(php);
    free(php->array);
    php->capacity = php->size = 0;
}
 
void HeapPrint(Heap* php) //堆的打印
{
    for (int i = 0; i < php->size; i++) 
    {
        printf("%d ", php->array[i]);
    }
    printf("\n");
}
 
bool HeapEmpty(Heap* php)//判断堆是否为空
{
    assert(php);
 
    return php->size == 0; // 如果为size为0则表示堆为空
}
 
 
HPDataType HeapTop(Heap* php) //返回堆顶数据
{
    assert(php);
    assert(!HeapEmpty(php));
 
    return php->array[0];
}
 
int HeapSize(Heap* php) //统计堆的个数
{
    assert(php);
 
    return php->size;
}
 
void HeapCheckCapacity(Heap* php) //检查容量  写过很多次了
{
    if (php->size == php->capacity) 
    {
        int newCapacity = php->capacity == 0 ? 4 : (php->capacity * 2); //第一次给4,其他情况扩2倍
        HPDataType* tmpArray = (HPDataType*)realloc(php->array, sizeof(HPDataType) * newCapacity); // 数组扩容
        if (tmpArray == NULL)
        {  //检查realloc
            printf("realloc failed!\n");
            exit(-1);
        }
        //更新他们的大小
        php->array = tmpArray;
        php->capacity = newCapacity;
    }
}
 
void Swap(HPDataType* px, HPDataType* py) //交换函数
{
    HPDataType tmp = *px;
    *px = *py;
    *py = tmp;
}
 
void BigAdjustUp(int* arr, int child) //大根堆上调
{
    assert(arr);
    // 首先根据公式计算算出父亲的下标
    int parent = (child - 1) / 2;
    // 最坏情况:调到根,child=parent 当child为根节点时结束(根节点永远是0)
    while (child > 0) //不能写parent >= 0
    {                 //为什么呢?最后一次往上走时,
                     //child = parent;  此时child = 0
                     //parent = (child - 1) / 2;
                     //(0 - 1) / 2 = 0  这么一来parent仍然会是0
                     //导致parent根本就不会小于0
        if (arr[child] > arr[parent]) // 如果孩子大于父亲(不符合大堆的性质)
        {  
            // 交换他们的值
            Swap(&arr[child], &arr[parent]);
            // 往上走
            child = parent;
            parent = (child - 1) / 2;
        }
        else// 如果孩子小于父亲(符合堆的性质)
        {  
            break;// 跳出循环
        }
    }
}
 
void SmallAdjustUp(int* arr, int child) //小堆上调
{
    assert(arr);
    // 首先根据公式计算算出父亲的下标
    int parent = (child - 1) / 2;
    // 最坏情况:调到根,child=parent 当child为根节点时结束(根节点永远是0)
    while (child > 0)
    {
        if (arr[child] < arr[parent])  // 如果孩子小于父亲(不符合小堆的性质)
        { 
            Swap(&arr[child], &arr[parent]);
            // 往上走
            child = parent;
            parent = (child - 1) / 2;
        }
        else  // 如果孩子小于父亲(符合堆的性质)
        {  
            break;
        }
    }
}
 
void HeapPush(Heap* php, HPDataType x) 
{
    assert(php);
    // 检查是否需要扩容
    HeapCheckCapacity(php);
    // 插入数据
    php->array[php->size] = x;
    php->size++;
    // 向上调整 [目标数组,调整位置的起始位置(刚插入的数据)]
    BigAdjustUp(php->array, php->size - 1);
}
 
 
//小根堆下调  左右子树为小根堆,根节点不满足时使用  左右子树不满足就对其进行下调
void SmallAdjustDown(int* arr, int n, int parent) 
{
    int child = parent * 2 + 1; // 默认为左孩子
    while (child < n) // 叶子内
    { 
        // 选出左右孩子中小的那一个
        if (child + 1 < n && arr[child + 1] < arr[child]) //左孩子+1为右孩子
        {    //如果 child + 1 比 n 大,就说明没有右孩子,默认是对的
            child++;//左孩子+1更新为右孩子
        }
        if (arr[child] < arr[parent])// 如果孩子小于父亲(不符合小堆的性质)
        { 
            // 交换它们的值
            Swap(&arr[child], &arr[parent]);
            // 往下走
            parent = child;
            child = parent * 2 + 1;
        }
        else // 如果孩子大于父亲(符合小堆的性质)
        { 
            break;
        }
    }
}
 
void BigAdjustDown(int* arr, int n, int parent) //大根堆下调
{
    int child = parent * 2 + 1; // 默认为左孩子
    while (child < n) // 叶子内
    { 
        // 选出左右孩子中大的那一个
        if (child + 1 < n && arr[child + 1] > arr[child]) 
        {
            child++;
        }
        if (arr[child] > arr[parent]) { // 如果孩子大于父亲(不符合大堆的性质)
            // 交换它们的值
            Swap(&arr[child], &arr[parent]);
            // 往下走
            parent = child;
            child = parent * 2 + 1;
        }
        else  // 如果孩子小于父亲(符合大堆的性质)
        {
            break;
        }
    }
}
 
void HeapPop(Heap* php) 
{
    assert(php);
    assert(!HeapEmpty(php));
    // 删除数据
    Swap(&php->array[0], &php->array[php->size - 1]);
    php->size--;
    // 向下调整 [目标数组,数组的大小,调整位置的起始位置]
    //SmallAdjustDown(php->array, php->size, 0);
    BigAdjustDown(php->array, php->size, 0);
}

Test.c

#include"Heap.h"
 
int main()
{
    Heap h;
    HeapInit(&h);
    int arr[] = { 15,18,19,25,34,28,65,27,49,37 };
    for (int i = 0;i < sizeof(arr) / sizeof(arr[0]);i++)
    {
        HeapPush(&h, arr[i]);
    }
    HeapPrint(&h);
 
    HeapPop(&h);
    HeapPrint(&h);
 
    HeapPush(&h, 100);
    HeapPush(&h, 40);
    HeapPrint(&h);
    HeapPop(&h);
    HeapPrint(&h);
 
    return 0;
}

本篇完。

目录
相关文章
|
10天前
|
存储 算法 Java
散列表的数据结构以及对象在JVM堆中的存储过程
本文介绍了散列表的基本概念及其在JVM中的应用,详细讲解了散列表的结构、对象存储过程、Hashtable的扩容机制及与HashMap的区别。通过实例和图解,帮助读者理解散列表的工作原理和优化策略。
23 1
散列表的数据结构以及对象在JVM堆中的存储过程
|
12天前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
55 16
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
56 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
1月前
|
存储 JavaScript 前端开发
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
67 1
|
2月前
|
存储 Java
【数据结构】优先级队列(堆)从实现到应用详解
本文介绍了优先级队列的概念及其底层数据结构——堆。优先级队列根据元素的优先级而非插入顺序进行出队操作。JDK1.8中的`PriorityQueue`使用堆实现,堆分为大根堆和小根堆。大根堆中每个节点的值都不小于其子节点的值,小根堆则相反。文章详细讲解了如何通过数组模拟实现堆,并提供了创建、插入、删除以及获取堆顶元素的具体步骤。此外,还介绍了堆排序及解决Top K问题的应用,并展示了Java中`PriorityQueue`的基本用法和注意事项。
51 5
【数据结构】优先级队列(堆)从实现到应用详解
|
1月前
|
存储 算法 调度
数据结构--二叉树的顺序实现(堆实现)
数据结构--二叉树的顺序实现(堆实现)
|
1月前
|
存储 算法 分布式数据库
【初阶数据结构】理解堆的特性与应用:深入探索完全二叉树的独特魅力
【初阶数据结构】理解堆的特性与应用:深入探索完全二叉树的独特魅力
|
1月前
|
存储 算法
探索数据结构:分支的世界之二叉树与堆
探索数据结构:分支的世界之二叉树与堆
|
1月前
|
存储 算法 Java
【用Java学习数据结构系列】用堆实现优先级队列
【用Java学习数据结构系列】用堆实现优先级队列
29 0
|
1月前
|
存储 算法
【数据结构】二叉树——顺序结构——堆及其实现
【数据结构】二叉树——顺序结构——堆及其实现