数据结构:⼆叉树(1)

目录

前言

树部分知识:

一.树的概念和结构

二.树的一些相关术语和定义

三.树的实现结构(了解部分)

四、树的应用场景

二叉树部分知识讲解:

一.二叉树概念与结构

二.特殊二叉树类型

1.满二叉树

2.完全二叉树

3.性质补充

三、⼆叉树存储结构

顺序结构:

​编辑应用:

链式结构:

四、堆的概念与结构

1.实现顺序结构⼆叉树:

2.堆的概念与结构

(重点)

3.堆的实现

五、堆的实现代码部分

1.堆的初始化:(本次实现选取大堆为例)

2.堆的销毁:

[3.堆的插入数据 :](#3.堆的插入数据 :)

[4.堆打印值 :](#4.堆打印值 :)

六、现有的堆代码的测试

总结

前言

本篇文章将讲解数据结构:⼆叉树的知识,内容包括:

  1. **树讲解:**树的概念与结构、树相关术语、树的表示方法、树形结构实际运⽤场景。
  2. **⼆叉树:**概念与结构、特殊的⼆叉树讲解、⼆叉树存储结构、实现顺序结构⼆叉树、实现链式结构⼆叉树、⼆叉树算法题等模块。

由于内容较多,所以知识会被分多篇文章来介绍。

树部分知识:

一.树的概念和结构

树是⼀种⾮线性的数据结构,它是由 n ( n>=0 ) 个有限结点组成⼀个具有层次关系的集合。把它叫做树是因为它看起来像⼀棵倒挂的树,也就是说它是根朝上,⽽叶朝下的。

例:

  1. 有⼀个特殊的结点,称为根结点,根结点没有前驱结点。(可看图中的A节点)
  2. 除根结点外,其余结点被分成 M(M>0) 个互不相交的集合 T1、T2、......、Tm ,其中每一个集合 Ti(1 <= i <= m) ⼜是⼀棵结构与树类似的子树。每棵子树的根结点有且只有⼀个前驱,可以有 0 个或多个后继。因此,树是递归定义的。
  3. 注:树形结构中,⼦树之间不能有交集,否则就不是树形结构。

根据上述定义: 我们可以得知:

图中的树都不是树(子树之间一定没有交集,否则就不是树形结构了)

  • ⼦树是不相交的(如果存在相交就是图了,图以后数据结构知识会有讲解)
  • 除了根结点外,每个结点有且仅有⼀个⽗结点。
  • ⼀棵N个结点的树有N-1条边。

二.树的一些相关术语和定义

接下来,将介绍树的相关术语和定义知识:

例:

  1. 父结点/双亲结点:若一个结点含有子结点,则这个结点称为其子结点的父结点; 如上图:A是B的父结点
  2. 子结点/孩子结点:一个结点含有的子树的根结点称为该结点的子结点; 如上图:B是A的孩子结点
  3. 结点的度:一个结点有几个孩子,他的度就是多少;比如A的度为6,F的度为2,K的度为0
  4. 树的度:⼀棵树中,最大的结点的度称为树的度; 如上图:树的度为 6
  5. 叶子结点/终端结点:度为 0 的结点称为叶结点; 如上图: B、C、H、I... 等结点为叶结点
  6. 分支结点/非终端结点:度不为 0 的结点; 如上图: D、E、F、G... 等结点为分支结点
  7. 兄弟结点:具有相同父结点的结点互称为兄弟结点(亲兄弟); 如上图: B、C 是兄弟结点
  8. 结点的层次:从根开始定义起,根为第 1 层,根的子结点为第 2 层,以此类推;
  9. 树的高度或深度:树中结点的最大层次; 如上图:树的高度为 4
  10. 结点的祖先:从根到该结点所经分支上的所有结点;如上图: A 是所有结点的祖先
  11. 路径:一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列;比如A到Q的路径为:A-E-J-Q;H到Q的路径H-D-A-E-J-Q
  12. 子孙:以某结点为根的子树中任一结点都称为该结点的子孙。如上图:所有结点都是A的子孙
  13. 森林:由 m(m>0) 棵互不相交的树的集合称为森林;

三.树的实现结构(了解部分)

树结构相对线性表就⽐较复杂了,要存储表⽰起来就⽐较⿇烦了,既需要保存值域部分,也要保存结点和结点之间的关系,实际中树有很多种表⽰⽅式,接下来,就将对结构一一介绍:

  1. 双亲表示法

  2. 孩子表示法

  3. 孩子双亲表示法

双亲表示法

核心思想 :用数组存储每个节点,每个节点记录其双亲节点的数组下标

代码例:

cpp 复制代码
#define MAX_TREE_SIZE 100
typedef struct PTNode {
    int data;       // 数据域
    int parent;     // 双亲节点的数组下标(根节点为-1)
} PTNode;
typedef struct {
    PTNode nodes[MAX_TREE_SIZE];  // 节点数组
    int r, n;                     // 根节点位置、总节点数
} PTree;
  • 优点:查找双亲节点极快(O(1))。
  • 缺点:查找孩子节点需遍历整个数组(O(n))。
  • 适用场景:频繁查询父节点的场景。

孩子表示法

核心思想 :每个节点的所有孩子节点用单链表存储,再用数组记录每个节点的数据和孩子链表的头指针。

代码例:

cpp 复制代码
typedef struct CTNode {
    int child;               // 孩子节点的数组下标
    struct CTNode* next;     // 指向下一个孩子
} ChildPtr;
typedef struct {
    int data;                // 数据域
    ChildPtr* firstchild;    // 孩子链表头指针
} CTBox;
typedef struct {
    CTBox nodes[MAX_TREE_SIZE];  // 节点数组
    int r, n;                     // 根节点位置、总节点数
} CTree;
  • 优点:查找孩子节点高效(直接遍历链表)。
  • 缺点:查找双亲节点需遍历所有节点(O(n))。
  • 适用场景:频繁查询子节点的场景(如多叉树的遍历)。

孩子双亲表示法

核心思想 :结合前两种方法,每个节点同时记录双亲下标孩子链表头指针,兼顾双亲与孩子的查询效率。

代码例:

cpp 复制代码
typedef struct CTNode {
    int child;               // 孩子节点的数组下标
    struct CTNode* next;     // 指向下一个孩子
} ChildPtr;
typedef struct {
    int data;                // 数据域
    int parent;              // 双亲节点的数组下标
    ChildPtr* firstchild;    // 孩子链表头指针
} CTBox;
typedef struct {
    CTBox nodes[MAX_TREE_SIZE];  // 节点数组
    int r, n;                     // 根节点位置、总节点数
} CTree;

图例:

  • 优点:同时支持O(1)查找双亲、O(k)查找孩子(k为孩子数)。
  • 缺点:结构较复杂,内存占用略高。
  • 适用场景:需频繁查询双亲与孩子的场景(如复杂层次结构的管理系统)。
特性 双亲表示法 孩子表示法 孩子双亲表示法
查找双亲 O(1) O(n) O(1)
查找孩子 O(n) O(k) O(k)
内存占用
结构复杂度 简单 中等 复杂

其中最常⽤的孩⼦兄弟表示法。此部分作为了解部分,在实际应用中很少使用这种复杂的树结果,像**⼆叉树这种结构的应用相对来说更广泛。**

四、树的应用场景

  1. ⽂件系统是计算机存储和管理⽂件的⼀种⽅式,它利⽤树形结构来组织和管理⽂件和⽂件夹。在⽂件系统中,树结构被⼴泛应⽤,它通过⽗结点和⼦结点之间的关系来表⽰不同层级的⽂件和⽂件夹之间的关联。

例:

二叉树部分知识讲解:

一.二叉树概念与结构

在树形结构中,我们最常⽤的就是⼆叉树,定义为:

二叉树是由n(n≥0)个节点组成的有限集合,满足:

  • 空集合(空二叉树);
  • 或由一个根节点和两棵互不相交的左子树、右子树组成,左、右子树本身也是二叉树。

基本形态:

  1. 基本形态

    二叉树有5种基本形态:

    • 空二叉树
    • 仅含根节点
    • 根节点+左子树
    • 根节点+右子树
    • 根节点+左子树+右子树

举例图例:

补充说明:

1.⼆叉树不存在度⼤于 2 的结点。

2. ⼆叉树的⼦树有左右之分,次序不能颠倒,因此⼆叉树是有序树。

二.特殊二叉树类型

特殊二叉树是在普通二叉树基础上,通过结构约束功能特性 形成的特定形态,本文讲解常见的特殊二叉树类型,包括满二叉树、完全二叉树。

以下是核心类型的定义与特点:

1.满二叉树

一个二叉树如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树,也就是说,如果一个二叉树层次为k,其结点总数就是2^k-1,则它就是满二叉树。

注:判断方法:

  1. 深度为k且节点总数为2ᵏ - 1的二叉树,每一层节点数达到最大值(第i层有2^(i-1)个节点),所有叶子节点集中在最底层。
  2. 每个节点要么是叶子节点(度为0),要么有两个子节点(度为2),不存在度为1的节点。
  3. 结构对称,节点数必为奇数,叶子节点仅在最底层。

示例:

每一层的节点个数都达到最大。

2.完全二叉树

完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而来的。对于深度为k的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。注意满二叉树是一种特殊的完全二叉树。

完全二叉树注意:

  1. 叶子节点仅在最后两层,且最底层叶子靠左连续排列;(因为完全二叉树会注重节点的顺序)
  2. 若有度为1的节点,最多1个且仅含左孩子;
  3. 满二叉树是完全二叉树的特殊情况,但反之不成立。

例:

3.性质补充

  1. 若规定根结点的层数为 1 ,则⼀棵非空二叉树的第i层上最多有 2 ^(i−1) 个结点
  2. 若规定根结点的层数为 1 ,则深度为 h 的二叉树的最大结点数是 2 ^h − 1
  3. 若规定根结点的层数为 1 ,具有 n 个结点的满二叉树的深度 h = log(n + 1) ( log以2为底, n+1 为对数)

三、⼆叉树存储结构

⼆叉树⼀般可以使⽤两种结构存储,⼀种顺序结构,⼀种链式结构。

实现结构取决于树的数据情况,接下来,将讲解该部分知识:

顺序结构:

顺序结构存储实际上就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费,完全二叉树更适合使用顺序结构存储。

至于为什么完全二叉树更适合使用顺序结构存储,与它的值的结构有关, 顺序结构存储实际上就是使用数组来存储,为了访问到各个节点,需要将所有的节点存入数组中,但如果树中有很多的空节点,不存储任何值,我们也需要为空节点开辟空间,那样会造成空间上的浪费的。

例:

应用:

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

堆结构,在下面会有所讲解,所以先说到这。

链式结构:

⼆叉树的链式存储结构是指,⽤链表来表⽰⼀棵⼆叉树,即⽤链来指⽰元素的逻辑关系。通常的⽅法 是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别⽤来给出该结点左孩⼦和右孩 ⼦所在的链结点的存储地址。链式结构⼜分为⼆叉链和三叉链,当前我们学习中⼀般都是⼆叉链。到后⾯讲解⾼阶数据结构如红⿊树等会⽤到三叉链。所以目前专心讲解二叉树知识。

例:

接下来,将讲解该方面知识:

四、堆的概念与结构

1.实现顺序结构⼆叉树:

⼀般堆使⽤顺序结构的数组来存储数据,堆是⼀种特殊的⼆叉树,具有⼆叉树的特性的同时,还具备其他的特性。

2.堆的概念与结构

概念:

如果有⼀个关键码的集合 0 1 K = {k ,k ,k ,... , k } 2 式存储,在⼀个⼀维数组中,并满⾜: K = ( i K2∗i+1 K <= 且 i K2∗i+2 ), i = 0 、 1 、 2... ,则称为⼩堆(或⼤堆)。将根结点最⼤的堆叫做最⼤堆或⼤根堆,根结点最⼩的堆 叫做最⼩堆或⼩根堆。

例:

(重点)

  • 简单来说:大根堆的根节点值最大,小根堆的根节点值最小。

  • 堆中某个结点的值总是不⼤于或不⼩于其⽗结点的值;

  • 堆总是⼀棵完全⼆叉树。

  • 性质:对于具有 n 个结点的完全⼆叉树,如果按照从上⾄下从左⾄右的数组顺序对所有结点从 0 开始编号,则对于序号为 i 的结点有:

    若i>0,i的位置结点的双亲序号:(i-1)/2;i=0,i为根结点编号,无双亲结点

    若2i+1<n,左孩子序号:2i+1,2i+1>=n,则无左孩子

    若2i+2<n,右孩子序号:2i+2,2i+2>=n,则无右孩子

3.堆的实现

堆底层结构为数组,因此定义堆的结构也与顺序表相同:

cpp 复制代码
typedef int type;
typedef struct Heap
{
	type* a;
	int size;
	int capacity;
}HP;

操作方面,也与顺序表相同,不同点是,堆是有大根堆,小根堆的结构要求的,所以实现时也不是那么容易。

五、堆的实现代码部分

1.堆的初始化:(本次实现选取大堆为例)

堆的初始化是创建一个空堆并设置其初始容量、指针等基本属性的过程,通常分为大顶堆 (父节点值≥子节点)和小顶堆 (父节点值≤子节点)两种类型。本次实现选取大堆为例。

void HPInit(HP* h);

实现代码:

cpp 复制代码
void HPInit(HP* h)
{
	assert(h);
	h->a = NULL;
	h->capacity = h->size = 0;
}

讲解:

由于,堆的底层是顺序表,所以,初始化的实现与顺序表的实现基本一致。

cpp 复制代码
void HPInit(HP* h)  // 函数:初始化堆,参数为堆结构体指针h
{
    assert(h);      // 断言指针h非空,避免空指针访问
    h->a = NULL;    // 初始化动态数组指针为NULL(未分配内存)
    h->capacity = h->size = 0;  // 初始化容量和元素个数为0
}

2.堆的销毁:

堆的销毁函数核心目标是释放动态分配的内存,避免内存泄漏,并将堆重置为空状态,实现也与顺序表一样。

函数:

void HPDestroy(HP* h)

代码:

cpp 复制代码
void HPDestory(HP* h)
{
	assert(h);
	if (h->a)
	{
		free(h->a);
	}
	h->capacity = h->size = 0;
}

讲解:

接下来,将逐行讲解:

cpp 复制代码
void HPDestroy(HP* h)  // 函数:销毁堆,参数为堆结构体指针h
{
    assert(h);          // 【强制检查】断言h非空,避免空指针访问
    if (h->a)           // 若动态数组指针a非空(即已分配内存)
    {
        free(h->a);     // 释放a指向的动态内存
    }
    h->capacity = h->size = 0;  // 重置容量和元素个数为0
}

3.堆的插入数据 :

注意:堆的插入,是有大根堆、小根堆要求的,所以每当我们插入一个值时,都要进行堆调整。

需遵循**"先插入元素,再向上调整"**的核心逻辑。

至于:向上调整,举例:

未插入之前:

插入值:

调整之后:

在每次插入之前,我们认为是符合该(大根堆或小根堆)结构 的状态,插如的值如果使树不符合该(大根堆或小根堆)结构,则需让新插入的元素"上浮"到正确位置,恢复堆的性质。

所应用的函数:

void HPPush(HP* h, type x) //插入函数
void AdjustUp(type* a, int child) //向上调整算法
void swap(int* a, int* b); //交换值函数

代码:(由于只有入堆会引起扩容,所以扩容代码,我将其写在了插入函数内)

cpp 复制代码
void swap(int* a, int* b)
{
    int t = *a;
    *a = *b;
    *b = t;
}
void AdjustUp(type* a, int child)
{
    assert(a);
    int parent = (child - 1) / 2;
    while (child > 0)
    {
        if (a[parent] < a[child])
        {
            swap(&a[parent], &a[child]);
            child = parent;
            parent = (child - 1) / 2;
        }
        else
        {
            break;
        }

    }
}
void HPPush(HP* h, type x)
{
    assert(h);
    if (h->capacity == h->size)
    {
        int n = h->capacity == 0 ? 4 : 2 * h->capacity;
        type* tmp = (type*)realloc(h->a, n*sizeof(type));
        if (tmp == NULL)
        {
            perror("realloc failed");
            return;
        }
        h->a = tmp;
        h->capacity = n;
    }
    h->a[h->size++] = x;
    AdjustUp(h->a, h->size - 1);
}

讲解:

堆的插入操作实现 ,包含swap(交换)、AdjustUp(向上调整)和HPPush(堆插入)三个函数,核心是通过"扩容→插入→调整"维持堆的性质(以大顶堆为例)。

  • swap 函数:交换两个整数的辅助函数
  • AdjustUp 函数:向上调整
cpp 复制代码
void AdjustUp(type* a, int child) {
    assert(a);                  // 检查数组指针非空
    int parent = (child - 1) / 2; // 父节点下标 = (子节点下标-1) // 2(整数除法)
    while (child > 0) {         // 子节点未到根节点(下标0)
        if (a[parent] < a[child]) { // 大顶堆条件:父节点 < 子节点 → 破坏堆性质
            swap(&a[parent], &a[child]); // 交换父子节点,让大值上去
            child = parent;             // 子节点上移到父节点位置
            parent = (child - 1) / 2;   // 更新父节点下标
        } else {
            break; // 父节点 >= 子节点 → 满足堆性质,停止调整
        }
    }
}
  • HPPush 函数:堆的插入入口
cpp 复制代码
void HPPush(HP* h, type x)
{
    assert(h);                  // 检查堆指针非空
    // 1. 扩容:容量不足时翻倍(初始容量为0则扩为4)
    if (h->capacity == h->size)
    {
        int n = h->capacity == 0 ? 4 : 2 * h->capacity;
        type* tmp = (type*)realloc(h->a,  n*sizeof(type)); // 重新分配内存
      if (tmp == NULL) {
    perror("realloc failed"); // 打印错误原因
    return; // 或返回错误码,避免程序崩溃
}
h->a = tmp;
        h->capacity = n;        // 更新容量
    }
    // 2. 插入元素:放到数组末尾(堆的最后一个叶子节点)
    h->a[h->size++] = x;
    // 3. 向上调整:让新元素上浮到正确位置
    AdjustUp(h->a, h->size - 1);
}

例:

4.堆打印值 :

堆打印值实际上与顺序表的值的打印一样,只是,结果是按照堆的值的顺序。

函数:

void HPPrint(HP* h);

代码:

cpp 复制代码
void HPPrint(HP* h)
{
	assert(h);
	int i;
	for (i = 0; i < h->size; i++)
	{
		printf("%d  ", h->a[i]);
	}
	printf("\n");
}

讲解:

前面文章已讲过,这里不过多讲解。

六、现有的堆代码的测试

接下来,我将展示我已经写完的代码部分:

本代码分三个文件完成:

1.h

cpp 复制代码
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int type;
typedef struct Heap
{
	type* a;
	int size;
	int capacity;
}HP;
void HPInit(HP* h);
void HPDestory(HP* h);
void HPPush(HP* h, type x);
void AdjustUp(type* a,int child);
void swap(int* a, int* b);
void HPPrint(HP* h);

1.c

cpp 复制代码
#include"1.h"
void HPInit(HP* h)
{
	assert(h);
	h->a = NULL;
	h->capacity = h->size = 0;
}
void HPDestory(HP* h)
{
	assert(h);
	if (h->a)
	{
		free(h->a);
	}
	h->capacity = h->size = 0;
} 
void swap(int* a, int* b)
{
	int t = *a;
	*a = *b;
	*b = t;
}
void AdjustUp(type* a, int child)
{
	assert(a);
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[parent] < a[child])
		{
			swap(&a[parent], &a[child]);
			child = parent;
			parent = (child - 1) / 2;
	}
		else
		{
			break;
		}

	}
}
void HPPush(HP* h, type x)
{
	assert(h);
	if (h->capacity == h->size)
	{
		int n = h->capacity == 0 ? 4 : 2 * h->capacity;
		type* tmp = (type*)realloc(h->a, n*sizeof(type));
		if (tmp == NULL) {
			perror("realloc failed"); 
			return; 
		}
		h->a = tmp;
		h->capacity = n;
	}
	h->a[h->size++] = x;
	AdjustUp(h->a, h->size - 1);
}
void HPPrint(HP* h)
{
	assert(h);
	int i;
	for (i = 0; i < h->size; i++)
	{
		printf("%d  ", h->a[i]);
	}
	printf("\n");
}

main.c

cpp 复制代码
#include"1.h"
void test()
{
	HP h;
	HPInit(&h);
	HPPush(&h, 10);  //10
	HPPush(&h, 20);  //20 10
	HPPush(&h, 6);   //20 10 6
	HPPrint(&h);
	HPPush(&h, 30);  //30  20  6  10
	HPPrint(&h);
	HPPush(&h, 60);  //60  30  6  10  20
	HPPrint(&h);
	HPDestory(&h);
}

int main()
{
	test();
}

测试结果:

总结

以上就是今天要讲的内容,本篇文章涉及的知识点为:

  1. **树讲解:**树的概念与结构、树相关术语、树的表示方法、树形结构实际运⽤场景。
  2. **⼆叉树:**概念与结构、特殊的⼆叉树讲解、⼆叉树存储结构、实现顺序结构⼆叉树

的相关内容,为本章节知识的内容,希望大家能喜欢我的文章,谢谢各位,,接下来的内容我会很快更新。

相关推荐
zore_c2 小时前
【数据结构】二叉树初阶——超详解!!!(包含二叉树的实现)
c语言·开发语言·数据结构·经验分享·笔记·算法·链表
laocooon5238578862 小时前
《21天学通C语言》第一天
c语言·开发语言
代码游侠2 小时前
学习笔记——网络基础
linux·c语言·网络·笔记·学习·算法
枫叶丹42 小时前
【Qt开发】Qt事件(二)-> QKeyEvent 按键事件
c语言·开发语言·数据库·c++·qt·microsoft
superman超哥11 小时前
仓颉语言中元组的使用:深度剖析与工程实践
c语言·开发语言·c++·python·仓颉
LYFlied12 小时前
【每日算法】LeetCode 153. 寻找旋转排序数组中的最小值
数据结构·算法·leetcode·面试·职场和发展
charlie11451419113 小时前
现代嵌入式C++教程:C++98——从C向C++的演化(2)
c语言·开发语言·c++·学习·嵌入式·教程·现代c++
雨季余静13 小时前
c语言 gb2312转utf-8,带码表,直接使用。
c语言·c语言utf8·c语言gb2312·c语言gbk·c语言gb18030·gb2312转utf8·gbk转utf8
2401_8904430213 小时前
Linux 基础IO
linux·c语言