【数据结构入门指南】二叉树顺序结构: 堆及实现(全程配图,非常经典)

简介: 【数据结构入门指南】二叉树顺序结构: 堆及实现(全程配图,非常经典)

一、前言:二叉树的顺序结构

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

 

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


二、堆的概念及结构

堆是一种特殊的数据结构,可以将堆分为大顶堆和小顶堆两种形式。堆中的每个节点都有一个值,并且满足以下两个条件:

 

①:堆的父节点的值总是大于或等于其子节点的值(大顶堆)或者小于或等于其子节点的值(小顶堆)。

②:堆是完全二叉树,即除了最底层外,其他层的节点都是满的,并且最底层的节点都尽量靠左排列。

大(小)堆示意图:


三、堆的实现(本篇博客以实现小堆为例)

3.1 准备工作

由于堆是通过数组来实现的,所以我们也和顺序表一样,首先要创建一个结构体来存储:数组指针 + 容量 + 存储数据大小

代码实现:

typedef int HPDataType;
typedef struct Heap
{
  HPDataType* a;//数组指针
  int size;//存储数据大小
  int capacity;//容量
}HP;

3.2 初始化

初始化有两种方式:

①:初始化时为数组开辟一定大小空间。②:直接将数组指针置为NULL,插入数据过程中在进一步处理。(本篇博客采用第二种)

代码实现:

void HPInit(HP* php)
{
  assert(php);
  php->a = NULL;
  php->capacity = 0;
  php->size = 0;
}

3.3 堆的插入

【代码思路】:

①:在插入数据前,我们首先要判断是否要扩容的问题。由于前面初始化时我们直接置空,所以我们先判断容量是否为空。如果为空开4个空间,否则空间扩大到原来的2倍。(为空时,第一次具体开辟多少空间读者可自行选择,本篇博客开4)

②:接下来就是插入数据了!但有一个问题,直接插入数据可能会破坏堆的结构,所以我们采用了向上调整算法

代码:

void HPPush(HP* php, HPDataType x)
{
  assert(php);
  //扩容
  if (php->size == php->capacity)
  {
    int newcapacity = php->size == 0 ? 4 : php->capacity * 2;
    HPDataType* tmp = (HPDataType*)realloc(php->a,sizeof(HPDataType) * newcapacity);
    if (tmp == NULL)
    {
      perror("malloc fail");
      exit(-1);
    }
    php->a = tmp;
    php->capacity = newcapacity;
  }
  
  //插入数据
  php->a[php->size] = x;
  php->size++;
  //向上调整
  AdjustUp(php->a, php->size - 1);
}

3.3.1 向上调整算法

【代码思路】:由于插入数据时,影响的只是插入数据到根节点间的关系。所以我们只需将插入数据通过双亲节点调到合适位置即可

 

Tips:父节点和子节点关系

  • leftchild = parent *2 + 1
  • rightchild = parent * 2 + 2
  • parent = (child - 1)/2

代码:

void Swap(HPDataType* p1, HPDataType* p2)
{
  HPDataType tmp = *p1;
  *p1 = *p2;
  *p2 = tmp;
}
void AdjustUp(HPDataType* a, int child)
{
  assert(a);
  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;
    }   
  }
}

3.4 堆的删除

【代码思路】:堆的删除默认是头删。删除有两种思路:

 

①:将根节点删除,再将其余数据全部向前移动。但这样做会造成一个问题:挪动覆盖导致父子节点的关系全乱了,需要重新建堆。为了删个数据,重新建堆,得不偿失,所以我们采用下面这种方法。

 

②:将堆顶的1数据和最后一个数据交换,在删除最后一个数据。堆顶数据在通过向下调整算法调到合适位置即可

代码:

void HPPop(HP* php)
{
  assert(php);
  assert(!HPEmpty(php));//非空,还有数据可删。该接口后面实现
  
  Swap(&php->a[0], &php->a[php->size - 1]);//交换
  php->size--;//删除
  //向下调整
  AdjustDown(php->a, php->size, 0);
}

3.4.1 向下调整算法

【代码思路】:向下调整算法有一个前提:左右子树必须是一个堆,才能调整。同时还要注意是调大堆还是小堆。

调小堆:堆顶元素和孩子中最小的节点比较,如果父节点大于较小的子节点子,两者交换。不断向下调整到合适位置。(调大堆,和较大孩子比较)

代码

//这里博主在多说一句,为什么向下调整算法不直接传结构体指针,
//而是分别传数组指针和数据大小呢?
//这里是博主有意为之的,目的在于让向下调整算法通用性更强,在做其他题时,也可以直接调用此接口
void AdjustDown(HPDataType* a, int n, int parent)
{
  assert(a);
  int child = parent * 2 + 1;//假设左孩子更小
  while (child < n)
  {
    //如果右孩子存在,且右孩子更小,则左孩子加1移到右孩子
    if ((child + 1) < n && a[child] > a[child + 1])
    {
      child++;
    }
    if (a[parent] > a[child])//交换
    {
      Swap(&a[parent], &a[child]);
      parent = child;
      child = parent * 2 + 1;
    }
    else
    {
      //父节点小于较小的子节点,说明已经调到合适位置,此时break跳出
      break;
    }
  }
}

3.5 堆的判空(接下的过于简单直接给出代码)

bool HPEmpty(HP* php)
{
  return php->size == 0;
}

3.6 取堆顶的数据

assert(php);
  assert(!HPEmpty(php));//断言堆中还有元素
  return php->a[0];

3.7 堆的个数

int HPSize(HP* php)
{
  assert(php);
  return php->size;
}

3.8 堆的销毁

void HPDestory(HP* php)
{
  assert(php);
  php->a = NULL;
  php->capacity = php->size = 0;
}

四、所有代码

Heap.h

#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int HPDataType;
typedef struct Heap
{
  HPDataType* a;
  int size;
  int capacity;
}HP;
void HPInit(HP* php);//初始化
void HPPush(HP* php, HPDataType x);//插入数据
void HPPop(HP* php);//删除数据
int HPTop(HP* php);//堆顶元素
bool HPEmpty(HP* php);//判空
int HPSize(HP* php);//数据个数
void HPDestory(HP* php);//销毁

Heap.c

#include "Heap.h"
void HPInit(HP* php)
{
  assert(php);
  php->a = NULL;
  php->capacity = 0;
  php->size = 0;
}
void Swap(HPDataType* p1, HPDataType* p2)
{
  HPDataType tmp = *p1;
  *p1 = *p2;
  *p2 = tmp;
}
void AdjustUp(HPDataType* a, int child)
{
  assert(a);
  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 AdjustDown(HPDataType* a, int n, int parent)
{
  assert(a);
  int child = parent * 2 + 1;
  while (child < n)
  {
    if ((child + 1) < n && a[child] > a[child + 1])
    {
      child++;
    }
    if (a[parent] > a[child])
    {
      Swap(&a[parent], &a[child]);
      parent = child;
      child = parent * 2 + 1;
    }
    else
    {
      break;
    }
  }
}
void HPPush(HP* php, HPDataType x)
{
  assert(php);
  //扩容
  if (php->size == php->capacity)
  {
    int newcapacity = php->size == 0 ? 4 : php->capacity * 2;
    HPDataType* tmp = (HPDataType*)realloc(php->a,sizeof(HPDataType) * newcapacity);
    if (tmp == NULL)
    {
      perror("malloc fail");
      exit(-1);
    }
    php->a = tmp;
    php->capacity = newcapacity;
  }
  php->a[php->size] = x;
  php->size++;
  //向上调整
  AdjustUp(php->a, php->size - 1);
}
bool HPEmpty(HP* php)
{
  return php->size == 0;
}
void HPPop(HP* php)
{
  assert(php);
  assert(!HPEmpty(php));
  
  Swap(&php->a[0], &php->a[php->size - 1]);
  php->size--;
  //向下调整
  AdjustDown(php->a, php->size, 0);
}
int HPTop(HP* php)
{
  assert(php);
  assert(!HPEmpty(php));
  return php->a[0];
}
int HPSize(HP* php)
{
  assert(php);
  return php->size;
}
void HPDestory(HP* php)
{
  assert(php);
  php->a = NULL;
  php->capacity = php->size = 0;
}


相关文章
|
1月前
|
存储 算法 Java
散列表的数据结构以及对象在JVM堆中的存储过程
本文介绍了散列表的基本概念及其在JVM中的应用,详细讲解了散列表的结构、对象存储过程、Hashtable的扩容机制及与HashMap的区别。通过实例和图解,帮助读者理解散列表的工作原理和优化策略。
43 1
散列表的数据结构以及对象在JVM堆中的存储过程
|
4天前
|
数据库
数据结构中二叉树,哈希表,顺序表,链表的比较补充
二叉搜索树,哈希表,顺序表,链表的特点的比较
数据结构中二叉树,哈希表,顺序表,链表的比较补充
|
1月前
|
机器学习/深度学习 存储 算法
数据结构实验之二叉树实验基础
本实验旨在掌握二叉树的基本特性和遍历算法,包括先序、中序、后序的递归与非递归遍历方法。通过编程实践,加深对二叉树结构的理解,学习如何计算二叉树的深度、叶子节点数等属性。实验内容涉及创建二叉树、实现各种遍历算法及求解特定节点数量。
89 4
|
1月前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
136 8
|
1月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
222 9
|
1月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
37 1
|
1月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
56 5
|
1月前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。
|
1月前
|
存储 JavaScript 前端开发
执行上下文和执行栈
执行上下文是JavaScript运行代码时的环境,每个执行上下文都有自己的变量对象、作用域链和this值。执行栈用于管理函数调用,每当调用一个函数,就会在栈中添加一个新的执行上下文。
|
1月前
|
存储
系统调用处理程序在内核栈中保存了哪些上下文信息?
【10月更文挑战第29天】系统调用处理程序在内核栈中保存的这些上下文信息对于保证系统调用的正确执行和用户程序的正常恢复至关重要。通过准确地保存和恢复这些信息,操作系统能够实现用户模式和内核模式之间的无缝切换,为用户程序提供稳定、可靠的系统服务。
52 4