数据结构:二叉树的顺序结构--堆

简介: 二叉树的顺序结构--堆详细解读,附带图解和完整代码

 朋友们、伙计们,我们又见面了,本期来给大家解读一下二叉树--堆的相关知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!

C 语 言 专 栏:C语言:从入门到精通

数据结构专栏:数据结构

个  人  主  页 :stackY、

目录

前言:

1.堆的概念及结构

2.堆的实现

2.1创建堆

2.2初始化堆

2.3向堆中插入数据

2.4向上调整算法

2.5判断堆是否为空

2.6删除堆中的数据

2.7向下调整算法

2.8有效元素个数、堆顶元素

2.9代码测试

2.10完整代码


紫蓝色几何渐变科技互联网微信公众号封面 (1).gif

前言:

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结 构存储。

现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统 虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

image.gif编辑

1.堆的概念及结构

如果有一个关键码的集合K = {},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:(i = 0,1,2,3,4,......n-1)被称为小堆或 (i = 0,1,2,3,4,......n-1)被称为大堆,将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

图示:

image.gif编辑

简单的来讲:

1.大堆就是每一个孩子节点都要比它的父亲节点小,这只是规定了父亲节点和孩子节点之间的大小关系,并没有规定兄弟节点之间的关系,两个兄弟节点的大小不确定

2. 小堆就是每一个孩子节点都要比它的父亲节点大,这只是规定了父亲节点和孩子节点之间的大小关系,并没有规定兄弟节点之间的关系,两个兄弟节点的大小不确定

因此堆并不一定是有序的。

堆的性质:

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

2.堆总是一棵完全二叉树

image.gif编辑

2.堆的实现

堆是一种特殊的完全二叉树,因此实现堆的思路就是使用顺序表,同样的我们也是分模块来写,创建头文件:Heap.h、源文件:Heap.c和Test.c,堆在这里主要实现的接口是:初始化、插入,删除(删除堆顶元素)、判空、有效元素个数、堆顶元素、销毁。话不多说我们直接开始:

注意:这里实现的是以小堆为例

2.1创建堆

堆的创建就是创建一个顺序表,然后再进行堆的一系列操作:

头文件:Heap.h

//创建堆
//顺序表
typedef int HPDataType;
typedef struct Heap
{
  HPDataType* a;
  int size = 0;  //有效元素个数
  int capacity;  //容量
}Heap;
image.gif

2.2初始化堆

这些接口实现都比较简单,直接上手即可:

头文件:Heap.h

//初始化
void HeapInit(Heap* php);
image.gif

源文件:Heap.c

//初始化
void HeapInit(Heap* php)
{
  php->a = NULL;  
  php->size = 0;
  php->capacity = 0;
}
image.gif

2.3向堆中插入数据

插入堆在这里插入的是堆的最后面,对应的也就是顺序表的最后一个位置,当插入进去之后还面临一个问题:我们创建的就是小堆,那么插入的数据如果小于它的父亲,就需要向上调整,如果调整之后还小于它新的父亲的,那么还是得继续向上调整,直到符合小根堆的逻辑

image.gif编辑

因此插入过程可以分为这么几步:

1. 先为堆开辟一块空间。

2. 检测容量,不够还需扩容。

3. 插入,判断是否需要进行向上调整。

头文件:Heap.h

//插入
void HeapPush(Heap* php, HPDataType x);
image.gif

源文件:Heap.c

//插入
void HeapPush(Heap* php, HPDataType x)
{
  assert(php);
  //插入数据的过程
  //容量不够则需要扩容
  //判断空间是否不足
  if (php->size == php->capacity)
  {
    int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
    //为堆开辟空间
    HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
    if (tmp == NULL)
    {
      perror("realloc fail");
      return;
    }
    php->capacity = newcapacity;
    php->a = tmp;
  }
  //插入数据
  php->a[php->size] = x;
  php->size++;
  //向上调整
  AdjustUp(php->a, php->size - 1);
}
image.gif

向上调整算法我们再来分装一个函数

2.4向上调整算法

向上调整我们的第一步首先要找它的父亲节点(在顺序表的逻辑下:如果它是左孩子,那么它的父亲节点的下标就是它的下标-1然后除2,如果它是右孩子,那么它的父亲节点的下标就是它的下标-2然后除2,但是呢?我们进行的是整数除法,没有余数,因此无论是左孩子还是右孩子都是使用-1除2的方法进行计算)。第二步判断孩子是否小于父亲,小于的话孩子和父亲就要交换位置,大于的话表示符合小堆逻辑,直接跳出即可,然后再让交换后的父亲节点变为新的孩子节点,然后再求新的父亲节点,依次类推,直到调整到堆顶,堆顶元素的下标为0,那么就表示孩子节点的下标为0时就不需要再调整了,退出即可:

image.gif编辑

因此孩子与父亲之间的关系可以总结为:

int Child = Parent*2+1;   //左孩子
int Child = Parent*2+2;  //右孩子
int Parent = (Child-1)/2;  //父亲
image.gif

源文件:Heap.c

//交换函数
void Swap(HPDataType* p1, HPDataType* p2)
{
  HPDataType tmp = *p1;
  *p1 = *p2;
  *p2 = tmp;
}
//向上调整算法
void AdjustUp(HPDataType* a, int child)
{
  //父节点
  int parent = (child - 1) / 2;
  //向上交换
  while (child > 0)
  {
    //判断父子大小关系
    if (a[child] < a[parent])
    {   
      //交换
      Swap(&a[child], &a[parent]);
      //更新父子关系
      child = parent;
      parent = (child - 1) / 2;
    }
    else
    {
      break;
    }
  }
}
image.gif

2.5判断堆是否为空

堆为空的条件就是堆中的有效元素个数为0。

头文件:Heap.h

//判空
bool HeapEmpty(Heap* php);
image.gif

源文件:Heap.c

//判空
bool HeapEmpty(Heap* php)
{
  assert(php);
  return php->size == 0;
}
image.gif

2.6删除堆中的数据

删除堆元素时并不是删除最后一个元素,这样删除并没有什么意义,因此删除堆中的元素时删除的是堆顶的元素,那么这里就有两种方法:

1. 和顺序表的头删一样,直接挪动数据进行覆盖,但是这样做时间复杂度高,而且挪动数据本来就是一件代价比较大的事情,再加上如果挪动数据进行覆盖的话就会将原本的父子关系打乱,父子关系打乱就有点扯淡了。

image.gif编辑

2. 将堆顶的数据和最后一个数据交换,然后删除掉最后一个数据,这时将剩下的元素进行向下调整。

image.gif编辑

所以删除数据可以分为这几步:

1. 先判断堆是否为空,为空就不能再删除。

2. 再交换顶部和最后的数据。

3. 再将最后一个元素删除。

4. 再判断是否需要向下调整。

头文件:Heap.h

//删除
void HeapPop(Heap* php);
image.gif

源文件:Heap.c

void HeapPop(Heap* php)
{
  assert(php);
  //判断堆是否为空
  assert(!HeapEmpty(php));
  //交换堆顶的数据和最后的数据
  Swap(&php->a[0], &php->a[php->size - 1]);
  //删除最后的数据
  php->size--;
  //向下调整
  AdjustDown(php->a, php->size, 0);
}
image.gif

向下调整算法我们再来分装一个函数

2.7向下调整算法

向下调整算法首先我们需要根据孩子与父亲的下标关系找到堆顶元素的左右孩子,然后找出左右孩子中最小的孩子,然后将其交换,然后再将孩子的位置更新为父节点,再继续找新的父亲节点的左右孩子的最小的节点,再进行交换,依次类推,直到交换到最后一个孩子节点(孩子节点小于等于总结点个数)。

找左右孩子中的最小的我们可以使用假设法,假设左孩子为最小的,然后进行判断,如果不合适,就给左孩子加1,从而得到右孩子,这里还需要注意,如果没有右孩子呢?那就只能和左孩子比较,也就不需要进行比较了(左孩子加一小于等于总结点个数)。

所以我们在设置向下调整算法的时候需要传堆、元素个数、堆顶的下标。

image.gif编辑

源文件:Heap.c

//向下调整
void AdjustDown(HPDataType* a, int n, int parent)
{
  //假设左孩子为左右孩子中最小的节点
  int child = parent * 2 + 1;
  while (child < n)  //当交换到最后一个孩子节点就停止
  {
    if (  child + 1 < n  //判断是否存在右孩子
      && a[child + 1] < a[child]) //判断假设的左孩子是否为最小的孩子
    {
      child++;   //若不符合就转化为右孩子
    }
    //判断孩子和父亲的大小关系
    if (a[child] < a[parent])
    {
      Swap(&a[child], &a[parent]);
      //更新父亲和孩子节点
      parent = child;
      child = parent * 2 + 1;
    }
    else
    {
      break;
    }
  }
}
image.gif

注意:

1. 这里的向上调整算法和向下调整算法只适用于堆的操作。

2. 向上调整算法和向下调整算法的时间复杂度是O(logN)。

向上调整和向下调整的最好情况是一次都不调整,最坏的情况是调整一个完全二叉树的高度次,又因为完全二叉树的结点个数为一个范围[ 2^(h-1), 2^h-1]。所以将高度于h于结点个数N结合在一起可以算出高度为[ logN+1,log(N+1)],因此算法的时间复杂度为O(logN)。

2.8有效元素个数、堆顶元素

1. 返回堆顶元素时先要判断堆是否为空,堆顶元素的下标就是0,因此直接返回下标为0的元素。

2. 有效元素的个数就是顺序表中的size,直接返回即可。

头文件:Heap.h

//有效元素个数
int HeapSize(Heap* php);
//获取堆顶元素
HPDataType HeapTop(Heap* php);
image.gif

源文件:Heap.c

//有效元素个数
int HeapSize(Heap* php)
{
  assert(php);
  return php->size;
}
//获取堆顶元素
HPDataType HeapTop(Heap* php)
{
  assert(php);
  //判断堆是否为空
  assert(!HeapEmpty(php));
  return php->a[0];
}
image.gif

2.9代码测试

插入测试:

写完了堆的基本接口之后我们可以调试通过监视窗口来观察一下,我们将一个数据依次插入到堆中,观察他们的变化:

源文件:Test.c

#include "Heap.h"
void HeapTest()
{
  Heap hp;
  //初始化
  HeapInit(&hp);
  //插入
  int arr[] = { 8,5,6,2,1,0,3,4,7,9 };
  for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
  {
    HeapPush(&hp, arr[i]);
  }
}
int main()
{
  HeapTest();
  return 0;
}
image.gif

image.gif编辑

删除测试:

#include "Heap.h"
void HeapTest()
{
  Heap hp;
  //初始化
  HeapInit(&hp);
  //插入
  int arr[] = { 8,5,6,2,1,0,3,4,7,9 };
  for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
  {
    HeapPush(&hp, arr[i]);
  }
  //删除
  HeapPop(&hp);
}
int main()
{
  HeapTest();
  return 0;
}
image.gif

image.gif编辑

2.10完整代码

头文件:Heap.h

#pragma once
//    堆
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
//创建堆
//顺序表
typedef int HPDataType;
typedef struct Heap
{
  HPDataType* a;
  int size;  //有效元素个数
  int capacity;  //容量
}Heap;
//初始化
void HeapInit(Heap* php);
//销毁
void HeapDestroy(Heap* php);
//插入
void HeapPush(Heap* php, HPDataType x);
//删除
void HeapPop(Heap* php);
//判空
bool HeapEmpty(Heap* php);
//有效元素个数
int HeapSize(Heap* php);
//获取堆顶元素
HPDataType HeapTop(Heap* php);
image.gif

源文件:Heap.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"
//初始化
void HeapInit(Heap* php)
{
  php->a = NULL;  
  php->size = 0;
  php->capacity = 0;
}
//销毁
void HeapDestroy(Heap* php)
{
  free(php->a);
  php->a = NULL;
  php->size = php->capacity = 0;
}
//交换函数
void Swap(HPDataType* p1, HPDataType* p2)
{
  HPDataType tmp = *p1;
  *p1 = *p2;
  *p2 = tmp;
}
//向上调整算法
void AdjustUp(HPDataType* a, int child)
{
  //父节点
  int parent = (child - 1) / 2;
  //向上交换
  while (child > 0)
  {
    //判断父子大小关系
    if (a[child] < a[parent])
    {   
      //交换
      Swap(&a[child], &a[parent]);
      //更新父子关系
      child = parent;
      parent = (child - 1) / 2;
    }
    else
    {
      break;
    }
  }
}
//插入
void HeapPush(Heap* php, HPDataType x)
{
  assert(php);
  //插入数据的过程
  //容量不够则需要扩容
  //判断空间是否不足
  if (php->size == php->capacity)
  {
    int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
    //为堆开辟空间
    HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
    if (tmp == NULL)
    {
      perror("realloc fail");
      return;
    }
    php->capacity = newcapacity;
    php->a = tmp;
  }
  //插入数据
  php->a[php->size] = x;
  php->size++;
  //向上调整
  AdjustUp(php->a, php->size - 1);
}
//向下调整
void AdjustDown(HPDataType* a, int n, int parent)
{
  //假设左孩子为左右孩子中最小的节点
  int child = parent * 2 + 1;
  while (child < n)  //当交换到最后一个孩子节点就停止
  {
    if (  child + 1 < n  //判断是否存在右孩子
      && a[child + 1] < a[child]) //判断假设的左孩子是否为最小的孩子
    {
      child++;   //若不符合就转化为右孩子
    }
    //判断孩子和父亲的大小关系
    if (a[child] < a[parent])
    {
      Swap(&a[child], &a[parent]);
      //更新父亲和孩子节点
      parent = child;
      child = parent * 2 + 1;
    }
    else
    {
      break;
    }
  }
}
//删除
void HeapPop(Heap* php)
{
  assert(php);
  //判断堆是否为空
  assert(!HeapEmpty(php));
  //交换堆顶的数据和最后的数据
  Swap(&php->a[0], &php->a[php->size - 1]);
  //删除最后的数据
  php->size--;
  //向下调整
  AdjustDown(php->a, php->size, 0);
}
//判空
bool HeapEmpty(Heap* php)
{
  assert(php);
  return php->size == 0;
}
//有效元素个数
int HeapSize(Heap* php)
{
  assert(php);
  return php->size;
}
//获取堆顶元素
HPDataType HeapTop(Heap* php)
{
  assert(php);
  //判断堆是否为空
  assert(!HeapEmpty(php));
  return php->a[0];
}
image.gif

源文件:Test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"
void HeapTest()
{
  Heap hp;
  //初始化
  HeapInit(&hp);
  //插入
  int arr[] = { 8,5,6,2,1,0,3,4,7,9 };
  for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
  {
    HeapPush(&hp, arr[i]);
  }
  //删除
  //HeapPop(&hp);
}
int main()
{
  HeapTest();
  return 0;
}
image.gif

朋友们、伙计们,美好的时光总是短暂的,我们本期的的分享就到此结束,最后看完别忘了留下你们弥足珍贵的三连喔,感谢大家的支持!

目录
相关文章
|
20天前
|
存储 算法 Java
散列表的数据结构以及对象在JVM堆中的存储过程
本文介绍了散列表的基本概念及其在JVM中的应用,详细讲解了散列表的结构、对象存储过程、Hashtable的扩容机制及与HashMap的区别。通过实例和图解,帮助读者理解散列表的工作原理和优化策略。
29 1
散列表的数据结构以及对象在JVM堆中的存储过程
|
22天前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
60 16
|
22天前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
78 8
|
1月前
|
存储 JavaScript 前端开发
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
71 1
|
1月前
|
存储 算法
探索数据结构:分支的世界之二叉树与堆
探索数据结构:分支的世界之二叉树与堆
|
1月前
探索顺序结构:栈的实现方式
探索顺序结构:栈的实现方式
|
23天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
111 9
|
14天前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
22 1
|
16天前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。
|
19天前
|
存储 JavaScript 前端开发
执行上下文和执行栈
执行上下文是JavaScript运行代码时的环境,每个执行上下文都有自己的变量对象、作用域链和this值。执行栈用于管理函数调用,每当调用一个函数,就会在栈中添加一个新的执行上下文。