树、二叉树(C语言版)详解

🍕博客主页:️自信不孤单

🍬文章专栏:数据结构与算法

🍚代码仓库:破浪晓梦

🍭欢迎关注:欢迎大家点赞收藏+关注

文章目录

  • 🍊树的概念及结构
    • 1. 树的概念
    • 2. 树的相关概念
    • 3.树的性质
    • 4. 树的存储结构
      • 4.1 双亲表示法
      • 4.2 孩子表示法
      • 4.3 孩子兄弟表示法
    • 5. 树在实际中的应用
  • 🍓二叉树概念及结构
    • 1. 二叉树的概念
    • 2. 特殊的二叉树
      • 2.1 斜树
      • 2.2 满二叉树
      • 2.3 完全二叉树
    • 3. 二叉树的性质
    • 4. 二叉树的存储结构
      • 1. 二叉树的顺序结构
      • 2. 二叉树的链式结构
  • 🍒二叉树的顺序结构实现
    • 1. 堆的概念及结构
    • 2. 堆的性质
    • 3. 堆的实现
      • 3.1 初始化堆
      • 3.2 🍚堆的向上调整算法
      • 3.3 🍚堆的向下调整算法
      • 3.4 堆的插入
      • 3.5 堆的删除
      • 3.6 获取堆顶数据
      • 3.7 获取堆中数据个数
      • 3.8 堆的判空
      • 3.9 堆的销毁
    • 4. 堆的应用
      • 4.1 堆排序
      • 4.2 Top-K问题
  • 🍥二叉树的链式结构实现
    • 1. 遍历二叉树
      • 1.1 前序遍历二叉树
      • 1.2 中序遍历二叉树
      • 1.3 后序遍历二叉树
      • 1.4 层序遍历二叉树
    • 2. 通过前序遍历的数组构建二叉树
    • 3. 二叉树结点个数
    • 4. 二叉树叶子结点个数
    • 5. 二叉树第k层结点个数
    • 6. 二叉树查找值为x的结点
    • 7. 二叉树的深度
    • 8. 判断二叉树是否是完全二叉树
    • 9. 二叉树的销毁


🍊树的概念及结构

1. 树的概念

树是n(n>=0)个结点的有限集。当n = 0时,称为空树。在任意一棵非空树中应满足:

  1. 有且仅有一个特定的称为根的结点。
  2. 当n>1时,其余节点可分为m(m>0)个互不相交的有限集T1,T2,…,Tm,其中每个集合本身又是一棵树,并且称为根的子树。

因此,树是递归定义的。树作为一种逻辑结构,同时也是一种分层结构,具有以下两个特点:

  1. 树的根结点没有前驱,除根结点外的所有结点有且只有一个前驱。
  2. 树中所有结点可以有零个或多个后继。

注意:树形结构中,子树之间不能有交集,否则就不是树形结构了,因此n个结点的树中有n-1条边。

在这里插入图片描述

2. 树的相关概念

在这里插入图片描述

  1. 节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6。
  2. 叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I…等节点为叶节点。
  3. 非终端节点或分支节点:度不为0的节点; 如上图:D、E、F、G…等节点为分支节点。
  4. 双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点。
  5. 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点。
  6. 兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点。
  7. 树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6。
  8. 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
  9. 树的高度或深度:树中节点的最大层次; 如上图:树的高度为4。
  10. 堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点。
  11. 节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先。
  12. 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙。
  13. 森林:由m(m>0)棵互不相交的树的集合称为森林;

3.树的性质

  1. 树中的结点数等于所有结点的度数加 1 1 1

  2. 度为 m m m的树中第 i i i层上至多有 m ( n − 1 ) m^{(n-1)} m(n1)个结点( i ⩾ 1 i \geqslant 1 i1 )。

  3. 高度为 h h h m m m叉树至多有 ( m h − 1 ) / ( m − 1 ) (m^h - 1)/(m - 1) (mh1)/(m1)个结点。

  4. 具有 n n n个结点的 m m m叉树的最小高度为 [ log ⁡ 2 ( n ( m − 1 ) + 1 ) ] [\log_2 (n(m - 1) + 1)] [log2(n(m1)+1)]

4. 树的存储结构

树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既要保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。这里我们来介绍几种表示方法:

4.1 双亲表示法

这里以数组的方式为例,在每个结点中,存储该结点的数据信息 data 以及双亲的位置 parent。
以下是双亲表示法的结点结构定义代码:

/*树的双亲表示法结点结构定义*/
#define MAX_TREE_SIZE 10
typedef int TElemType; // 树结点的数据类型
/*结点结构*/
typedef struct PTNode
{
	TElemType data; // 结点数据
	int parent; // 双亲位置
}PTNode;
/*树结构*/
typedef struct
{
	PTNode nodes[MAX_TREE_SIZE]; // 结点数组
	int r, n; // 根的位置和结点数
}PTree;

这样的存储结构可以根据结点的 parent 的值很容易找到它的双亲结点,直到 parent 为 -1 时,表示找到了树结点的根。可如果我们要想知道,某结点的孩子是什么,需要遍历整个结构。

4.2 孩子表示法

把每个结点的孩子结点排列起来,以单链表作存储结构,则 n 个结点有 n 个孩子链表,如果是叶子结点则此单链表为空。然后 n 个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中。如下图:

在这里插入图片描述

设计两个结点,一个孩子结点,一个表头结点。孩子结点中 child 是数据域,用来存储某个结点在表头数组中的下标。next 是指针域,用来存储指向某结点的下一个孩子结点的指针。表头结点中 data 是数据域,存储某结点的数据信息。firstchild 是头指针域,存储该结点的孩子链表的头指针。
以下是孩子表示法的结点结构定义代码:

/*树的孩子表示法结构定义*/
#define MAX_TREE_SIZE 10
typedef int TElemType; // 树结点的数据类型
/*孩子结点*/
typedef struct CTNode
{
	int child;
	struct CTNode* next;
}*ChildPtr;
/*表头结点*/
typedef struct
{
	TElemType data;
	ChildPtr firstchild;
}CTBox;
/*树结构*/
typedef struct
{
	CTBox nodes[MAX_TREE_SIZE]; // 结点数组
	int r, n; // 根的位置和结点数
}

这样的存储结构对于我们要查找某个结点的某个孩子,只需要查找这个结点的孩子单链表即可。对于遍历整棵树也是很方便的,对头结点的数组循环即可。但是我们要想找到某个结点的双亲,还需要遍历整个结构。

4.3 孩子兄弟表示法

任意一棵树, 它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。 因此,我们设置两个指针,分别指向该结点的第一个孩子和该结点的右兄弟。如下图:

在这里插入图片描述

以下是孩子表示法的结点结构定义代码:

/*树的孩子兄弟表示法结构定义*/
typedef int TElemType; //树结点的数据类型
typedef struct CSNode
{
	TElemtype data;
	struct CSNode* firstchild, * rightsib;
} CSNode, * CSTree;

5. 树在实际中的应用

在这里插入图片描述

🍓二叉树概念及结构

1. 二叉树的概念

二叉树是一种树形结构,其特点是每个结点至多只有两棵子树(即二叉树中不存在度大于2的结点),并且二叉树的子树有左右之分,其次序不能任意颠倒。

二叉树是 n (n≥0) 个结点的有限集合:

  1. 或者为空二叉树,即n=0。
  2. 或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树。

二叉树的5种基本形态如图所示:

在这里插入图片描述

2. 特殊的二叉树

2.1 斜树

所有的结点都只有左子树的二叉树叫左斜树。所有结点都是只有右子树的二叉树叫右斜树。这两者统称为斜树。

2.2 满二叉树

在这里插入图片描述

一棵高度为 h h h,且含有个 2 h − 1 2^h-1 2h1结点的二叉树称为满二叉树,即树中的每层都含有最多的结点。满二叉树的叶子结点都集中在二叉树的最下一层,并且除叶子结点之外的每个结点度数均为 2 2 2。可以对满二叉树按层序编号:约定编号从根结点(根结点编号为 1 1 1)起,自上而下,自左向右。这样,每个结点对应一个编号,对于编号为 i 的结点,若有双亲,则其双亲为 i / 2 i/2 i/2,若有左孩子,则左孩子为 2 i 2i 2i;若有右孩子,则右孩子为 2 i + 1 2i+1 2i+1

2.3 完全二叉树

在这里插入图片描述

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

  1. i ⩽ n / 2 i \leqslant n/2 in/2,则结点 i i i为分支结点,否则为叶子结点。

  2. 叶子结点只可能在层次最大的两层上出现。对于最大层次中的叶子结点,都依次排列在该层最左边的位置上。

  3. 若有度为 1 1 1的结点,则只可能有一个,且该结点只有左孩子而无右孩子(重要特征)。

  4. 按层序编号后,一旦出现某结点(编号为 i i i)为叶子结点或只有左孩子,则编号大于 i i i的结点均为叶子结点。

  5. n n n为奇数,则每个分支结点都有左孩子和右孩子;若为偶数,则编号最大的分支结点(编号为 n / 2 n/2 n/2)只有左孩子,没有右孩子,其余分支结点左、右孩子都有。

3. 二叉树的性质

  1. 任意一棵树,若结点数量为 n n n,则边的数量为 n − 1 n-1 n1
  2. 非空二叉树上的叶子结点数等于度为 2 2 2的结点数加 1 1 1,即 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1
  3. 非空二叉树上第 k k k层上至多有 2 k − 1 2^{k-1} 2k1个结点( k ⩾ 1 k \geqslant 1 k1)。
  4. 高度为 h h h的二叉树至多有 2 h − 1 2^h-1 2h1个结点( h ⩾ 1 h \geqslant 1 h1)。
  5. 对完全二叉树按从上到下、从左到右的顺序依次编号 0 , 1 , … , n 0,1,…,n 0,1,,n,则有以下关系:
  • i > 0 i>0 i>0时,结点 i i i的双亲的编号为 ( i − 1 ) / 2 (i-1)/2 (i1)/2,即当 i i i为奇数时, 它是双亲的左孩子;当 i i i为偶数时,它是双亲的右孩子。
  • 2 i + 1 < n 2i+1<n 2i+1<n时,结点的左孩子编号为 2 i + 1 2i+1 2i+1,否则无左孩子。
  • 2 i + 2 < n 2i+2<n 2i+2<n时,结点 i i i的右孩子编号为 2 i + 2 2i+2 2i+2,否则无右孩子。
  • 结点 i i i所在层次(深度)为 [ log ⁡ 2 ( i + 1 ) ] + 1 [\log_2 (i+1)]+1 [log2(i+1)]+1。(方括号表示向下取整)
  1. 具有 n n n n > 0 n>0 n>0)个结点的完全二叉树的高度为 [ log ⁡ 2 n ] + 1 [\log_2 n]+1 [log2n]+1。(方括号表示向下取整)
  2. 具有 n n n n > 0 n>0 n>0)个结点的满二叉树的高度为 log ⁡ 2 ( n + 1 ) \log_2 (n+1) log2(n+1)

4. 二叉树的存储结构

1. 二叉树的顺序结构

二叉树的顺序存储是指用一组地址连续的存储单元依次自上而下、自左至右存储完全二叉树上的结点元素,即将完全二叉树上编号为 i i i的结点元素存储在一维数组下标为 i − 1 i-1 i1的分量中。

依据二叉树的性质,使用顺序结构数组只适合表示完全二叉树和满二叉树,树中结点的序号可以唯一地反映结点之间的逻辑关系,这样既能最大可能地节省存储空间,又能利用数组元素的下标值确定结点在二叉树中的位置,以及结点之间的关系。在实际应用中,堆的实现就很好的体现了这一结构。二叉树的顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。

在这里插入图片描述

2. 二叉树的链式结构

二叉树的链式存储结构是指用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。我们称这样的链表叫做二叉链表。

在含有 n n n个结点的二叉链表中,含有 n + 1 n+1 n+1个空链域。

在这里插入图片描述

/*二叉树的二叉链表结点构造定义*/
/*结点结构*/
typedef int TElemType;
typedef struct BTNode {
	TElemType data;	// 结点数据
	struct BTNode* lchild, * rchild;	// 左右孩子指针
} BTNode, * BTree;

🍒二叉树的顺序结构实现

1. 堆的概念及结构

如果有一个关键码的集合 K = { k 0 , k 1 , k 2 , … , k n − 1 } K=\{k_0,k_1, k_2,…,k_{n-1}\} K={k0k1k2kn1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足: K i ⩽ K 2 ∗ i + 1 Ki \leqslant K_{2*i+1} KiK2i+1 K i ⩽ K 2 ∗ i + 2 Ki \leqslant K_{2*i+2} KiK2i+2 K i ⩾ K 2 ∗ i + 1 Ki \geqslant K_{2*i+1} KiK2i+1 K i ⩾ K 2 ∗ i + 2 Ki \geqslant K_{2*i+2} KiK2i+2)(其中 i = 0 , 1 , 2 , … i = 0,1,2,… i=0,1,2,),则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

在这里插入图片描述

2. 堆的性质

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

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

3. 堆的实现

首先创建两个文件来实现堆:

  1. Heap.h(节点的声明、接口函数声明、头文件的包含)
  2. Heap.c(堆接口函数的实现)

如图:

在这里插入图片描述

Heap.h 文件内容如下:

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int HPDataType;
typedef struct Heap
{
	HPDataType*  a;
	int size;
	int capacity;
}Heap;

// 堆的初始化
void HeapInit(Heap* php);
// 堆的向上调整
void AdjustUp(HPDataType* a, int child);
// 堆的向下调整
void AdjustDown(HPDataType* a, int n, int parent);
// 堆的插入
void HeapPush(Heap* php, HPDataType x);
// 堆的删除
void HeapPop(Heap* php);
// 获取堆顶数据
HPDataType HeapTop(Heap* php);
// 获取堆中数据个数
int HeapSize(Heap* php);
// 堆的判空
int HeapEmpty(Heap* php);
// 堆的销毁
void HeapDestory(Heap* php);

接下来我们在 Heap.c 文件中实现各个接口函数。

3.1 初始化堆

对堆进行置空。

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

3.2 🍚堆的向上调整算法

当我们在堆尾插入数据时,需要进行调整使其仍然是堆,就要用到堆的向上调整算法。

以向上调整算法的基本思想(以建小堆为例):

  1. 将目标结点与其父结点比较。

  2. 若目标结点的值比其父结点的值小,则交换目标结点与其父结点的位置,并将原目标结点的父结点当作新的目标结点继续进行向上调整。若目标结点的值比其父结点的值大,则停止向上调整,此时该树已经是小堆了。

图解:

在这里插入图片描述

代码实现:

// 交换数据
void Swap(HPDataType* p1, HPDataType* p2)
{
	assert(p1);
	assert(p2);
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void AdjustUp(HPDataType*  a, int child)
{
	while (child > 0)
	{
		int parent = (child - 1) / 2;
		if (a[parent] > a[child])
		{
			Swap(&a[parent], &a[child]);
			child = parent;
		}
		else
		{
			break;
		}
	}
}

拓展:

向上调整一次的时间复杂度为 O ( log ⁡ N ) O(\log N) O(logN)

向上调整建堆的思路:

  • 从第一个结点开始,依次向上调整即可。

向上调整建堆的时间复杂度为 O ( N ∗ log ⁡ N ) O(N*\log N) O(NlogN)

向上调整建堆的代码实现:

for (int i = 0; i < n; i++)
{
	AdjustUp(a, i);
}

3.3 🍚堆的向下调整算法

现在我们给出一个数组,逻辑上看作一棵完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。

使用向下调整算法需要满足一个前提:

  1. 若想将其调整为小堆,那么根结点的左右子树必须都为小堆。

  2. 若想将其调整为大堆,那么根结点的左右子树必须都为大堆。

向下调整算法的基本思想(以建小堆为例):

  1. 从根结点处开始,选出左右孩子中值较小的孩子。
  2. 让小的孩子与其父亲进行比较。若小的孩子比父亲还小,则该孩子与其父亲的位置进行交换。并将原来小的孩子的位置当成父亲继续向下进行调整,直到调整到叶子结点为止。若小的孩子比父亲大,则不需处理了,调整完成,整个树已经是小堆了。

图解:

在这里插入图片描述

代码实现:

// 交换数据
void Swap(HPDataType* p1, HPDataType* p2)
{
	assert(p1);
	assert(p2);
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void AdjustDown(HPDataType* a, int n, int parent)
{
	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;
		}
	}
}

拓展:

向下调整一次的时间复杂度为 O ( log ⁡ N ) O(\log N) O(logN)

使用堆的向下调整算法需要满足其根结点的左右子树均为大堆或是小堆才行,那么如何才能将一个任意完全二叉树调整为堆呢?

答案很简单,我们只需要从倒数第一个非叶子结点开始,从后往前,按下标,依次作为根去向下调整即可。

向下调整建堆的代码实现:

for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
    AdjustDown(a, n, i);
}

那么向下调整建堆的时间复杂度为多少呢?

当结点数无穷大时,完全二叉树与其层数相同的满二叉树相比较来说,它们相差的结点数可以忽略不计,所以计算时间复杂度的时候我们可以将完全二叉树看作与其层数相同的满二叉树来进行计算。

在这里插入图片描述

建堆过程中最坏情况交换的次数:

T ( n ) = 1 × ( h − 1 ) + 2 × ( h − 2 ) + 2 h − 2 × 1 T(n) = 1 \times (h-1)+2 \times (h-2)+2^{h-2} \times 1 T(n)=1×(h1)+2×(h2)+2h2×1

通过求和我们得出

T ( n ) = 2 h − h − 1 T(n)=2^h-h-1 T(n)=2hh1

由二叉树的性质得 N = 2 h − 1 N=2^h-1 N=2h1

h = log ⁡ 2 ( N + 1 ) h=\log_2(N+1) h=log2(N+1)

所以 T ( n ) = N − log ⁡ 2 ( N + 1 ) T(n)=N-\log_2(N+1) T(n)=Nlog2(N+1)

用大O的渐近表示法:

T ( N ) = O ( N ) T(N)=O(N) T(N)=O(N)

最终推出向下调整建堆的时间复杂度为 O ( N ) O(N) O(N)

3.4 堆的插入

先判断扩容, 然后将结点插到堆数组的最后,再进行向上调整操作。

void HeapPush(Heap* php, HPDataType x)
{

	assert(php);
	if (php->capacity == php->size)
	{
		int NewCapacity = php->capacity == 0 ? 5 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a, NewCapacity * sizeof(HPDataType));
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		php->a = tmp;
		php->capacity = NewCapacity;
	}
	php->size++;
	php->a[php->size - 1] = x;
	AdjustUp(php->a, php->size - 1);
}

3.5 堆的删除

堆的删除是指删除堆顶元素。

实现思路:

如果非空就将堆顶结点与最后一个结点交换,然后去掉最后一个结点,将堆顶结点进行向下调整操作。

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);
}

3.6 获取堆顶数据

断言判空,返回堆顶元素的值。

HPDataType HeapTop(Heap* php)
{
	assert(php);
	assert(!HeapEmpty(php));
	return php->a[0];
}

3.7 获取堆中数据个数

返回堆中数据个数。

int HeapSize(Heap* php)
{
	assert(php);
	return php->size;
}

3.8 堆的判空

堆非空返回ture,否则返回false。

int HeapEmpty(Heap* php)
{
	assert(php);
	return php->size == 0;
}

3.9 堆的销毁

释放动态开辟好的内存,并对数据进行置空。

void HeapDestory(Heap* php)
{
	assert(php);
	free(php->a);
	php->a = NULL;
	php->capacity = php->size = 0;
}

4. 堆的应用

堆的应用相对广泛,我们在此仅介绍堆排序Top-K问题

4.1 堆排序

堆排序是利用堆的思想进行的排序,是一种选择排序。时间复杂度为 O ( N ∗ log ⁡ N ) O(N*\log N) O(NlogN),它也是一种不稳定排序。

实现分为两步:

  1. 建堆。
  • 升序:建大堆
  • 降序:建小堆
  1. 利用堆删除思想进行排序。

建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。

实现具体思路(以升序为例):

  1. 将待排序的 n n n个元素构建成一个大堆。此时,整个序列的最大值就是堆顶的根节点。
  2. 将其与末尾元素进行交换,此时末尾元素为最大值。
  3. 然后将剩余 n − 1 n-1 n1个元素通过向下调整的重新建成大堆,这样会得到 n n n个元素的次大值。如此反复执行,便能得到一个有序序列了。

堆排序动图演示网站

代码实现:

void HeapSort(HPDataType* a, int n)
{
    // 向下调整建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}
    
	for (int i = n - 1; i >= 0; i--)
	{
		Swap(&a[0], &a[i]);
		AdjustDown(a, i, 0);
	}
}

4.2 Top-K问题

求数据集合中前K个最大元素或最小元素,一般情况下数据量都比较大。

基本思路:

  1. 用数据集合中前 K K K个元素来建堆。
    求前 k k k个最大的元素,则建小堆。
    求前 k k k个最小的元素,则建大堆。

  2. 用剩余的 N − K N-K NK个元素依次与堆顶元素来比较,不满足则替换堆顶元素。

剩余 N − K N-K NK个元素依次与堆顶元素比完之后,堆中剩余的 K K K个元素就是所求的前 K K K个最小或者最大的元素。

接下来,我们通过文件操作的方式来实现。首先在文件中造10000个随机数据,然后再执行以上思路。

代码实现:

// 造数据
void CreateNDate()
{
	int n = 10000;
	srand((unsigned int)time(0));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}
    
	for (size_t i = 0; i < n; ++i)
	{
		int x = rand() % 1000000;
		fprintf(fin, "%d\n", x);
	}
    
	fclose(fin);
}

//打印最大的前K个数据
void PrintTopK(int k)
{
	const char* file = "data.txt";
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen error");
		return;
	}
    
	int* kminheap = (int*)malloc(sizeof(int) * k);
	if (kminheap == NULL)
	{
		perror("malloc error");
		return;
	}
    
    // 存入前k个数据
	for (int i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &kminheap[i]);
	}
    
	// 建小堆
	for (int i = (k-1-1)/2; i >= 0; i--)
	{
		AdjustDown(kminheap, k, i);
	}
    
    // 比较
	int val = 0;
	while (!feof(fout))
	{
		fscanf(fout, "%d", &val);
		if (val > kminheap[0])
		{
			kminheap[0] = val;
			AdjustDown(kminheap, k, 0);
		}
	}
    
    // 打印
	for (int i = 0; i < k; i++)
	{
		printf("%d ", kminheap[i]);
	}
	printf("\n");
}

🍥二叉树的链式结构实现

根据二叉树的概念,我们发现链式结构能更清晰地表示二叉树。二叉树每个结点最多有两个孩子,所以我们设计一个数据域和两个指针域,数据域存放该节点的数据,指针域存放左右孩子指针。

二叉树链式结构的结点定义代码:

/*二叉树的二叉链表结点构造定义*/
/*结点结构*/
typedef char TElemType;
typedef struct BTNode {
	TElemType data;	// 结点数据
	struct BTNode* lchild, * rchild;	// 左右孩子指针
} BTNode;

1. 遍历二叉树

动图演示点这里!!!

1.1 前序遍历二叉树

前序遍历:先根 再左 再右

void BinaryTreePrevOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	printf("%c ", root->data);
	BinaryTreePrevOrder(root->lchild);
	BinaryTreePrevOrder(root->rchild);
}

1.2 中序遍历二叉树

中序遍历:先左 再根 再右

void BinaryTreeInOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	BinaryTreeInOrder(root->lchild);
	printf("%c ", root->data);
	BinaryTreeInOrder(root->rchild);
}

1.3 后序遍历二叉树

后序遍历:先左 再右 再根

void BinaryTreePostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	BinaryTreePostOrder(root->lchild);
	BinaryTreePostOrder(root->rchild);
	printf("%c ", root->data);
}

1.4 层序遍历二叉树

从上到下从左往右依次遍历

我们发现层序遍历的特点是根据根、左、右的顺序遍历每个结点。于是我们想到让根结点入队列,出队列时再将该节点的左右孩子依次入队列,直到队列为空则层序遍历结束。

队列结点及各接口函数定义

void BinaryTreeLevelOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	Queue* qu = (Queue*)malloc(sizeof(Queue));
	QueueInit(qu);
	QueuePush(qu, root);
	while (!QueueEmpty(qu))
	{
		BTNode* ret = QueueFront(qu);
		if (ret)
		{
			printf("%c ", ret->data);
			QueuePush(qu, ret->lchild);
			QueuePush(qu, ret->rchild);
		}
		else
			printf("N ");
		QueuePop(qu);
	}
	QueueDestroy(qu);
	free(qu);
	printf("\n");
}

2. 通过前序遍历的数组构建二叉树

给定前序数组(其中空节点用#表示),通过前序递归开辟并连接结点。

BTNode* BTBuyNode(TElemType x)
{
	BTNode* ret = (BTNode*)malloc(sizeof(BTNode));
	if (!ret)
	{
		perror("malloc fail");
		return NULL;
	}
	ret->lchild = NULL;
	ret->rchild = NULL;
	ret->data = x;
	return ret;
}

BTNode* BinaryTreeCreate(TElemType* a, int n, int* pi)
{
	if (*pi == n)
	{
		return NULL;
	}

	if (a[*pi] == '#')
	{
		(*pi)++;
		return NULL;
	}

	BTNode* root = BTBuyNode(a[*pi]);
	(*pi)++;

	root->lchild = BinaryTreeCreate(a, n, pi);
	root->rchild = BinaryTreeCreate(a, n, pi);

	return root;
}

3. 二叉树结点个数

int BinaryTreeSize(BTNode* root)
{
	return root == NULL ? 0 : BinaryTreeSize(root->lchild) +
		BinaryTreeSize(root->rchild) + 1;
}

4. 二叉树叶子结点个数

int BinaryTreeLeafSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	if (root->lchild == NULL && root->rchild == NULL)
	{
		return 1;
	}

	return BinaryTreeLeafSize(root->lchild) +
		BinaryTreeLeafSize(root->rchild);
}

5. 二叉树第k层结点个数

int BinaryTreeLevelKSize(BTNode* root, int k)
{
	if (root == NULL)
	{
		return 0;
	}

	if (k == 1)
	{
		return 1;
	}
	return BinaryTreeLevelKSize(root->lchild, k - 1) +
		BinaryTreeLevelKSize(root->rchild, k - 1);
}

6. 二叉树查找值为x的结点

BTNode* BinaryTreeFind(BTNode* root, TElemType x)
{
	if (root == NULL)
	{
		return NULL;
	}
	if (root->data == x)
	{
		return root;
	}

	BTNode* l = BinaryTreeFind(root->lchild, x);
	if (l)
	{
		return l;
	}

	BTNode* r = BinaryTreeFind(root->rchild, x);
	if (r)
	{
		return r;
	}

	return NULL;
}

7. 二叉树的深度

int BinaryTreeMaxDepth(BTNode* root) {
	if (root == NULL)
		return 0;
	int l = BinaryTreeMaxDepth(root->lchild);
	int r = BinaryTreeMaxDepth(root->rchild);
	return l > r ? l + 1 : r + 1;
}

8. 判断二叉树是否是完全二叉树

通过层序遍历的方式找到第一个空节点,如果后面还有非空结点就不是完全二叉树,否则为完全二叉树。

bool BinaryTreeComplete(BTNode* root)
{
	if (root == NULL)
		return true;
	Queue* qu = (Queue*)malloc(sizeof(Queue));
	QueueInit(qu);
	QueuePush(qu, root);
	while (!QueueEmpty(qu))
	{
		BTNode* ret = QueueFront(qu);
		QueuePop(qu);
		if (ret)
		{
			QueuePush(qu, ret->lchild);
			QueuePush(qu, ret->rchild);
		}
		else
			break;
	}
	int result = true;
	while (!QueueEmpty(qu))
	{
		BTNode* ret = QueueFront(qu);
		if (ret)
		{
			result = false;
			break;
		}
		QueuePop(qu);
	}
	QueueDestroy(qu);
	free(qu);
	return result;
}

9. 二叉树的销毁

void BinaryTreeDestory(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	BinaryTreeDestory(root->lchild);
	BinaryTreeDestory(root->rchild);
	free(root);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/45936.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

算法通关村第一关-链表黄金挑战笔记|环的入口

解决链表环入口问题 文章目录 解决链表环入口问题前言链表中环的问题Hash和集合的解法&#xff1a;快慢指针实现解决&#xff1a; 解题思路&#xff1a;Hash或者使用集合的方式实现快慢指针&#xff08;这里使用三次刚好解决&#xff09; 总结 前言 提示&#xff1a;无论今天过…

C++笔记之使用普通指针和shared_ptr在堆上申请类对象的各种写法

C笔记之使用普通指针和shared_ptr在堆上申请类对象的各种写法 code review! 文章目录 C笔记之使用普通指针和shared_ptr在堆上申请类对象的各种写法1.几种不同的写法2.ChatGpt回答 1.几种不同的写法 注&#xff1a;使用普通指针申请堆内存&#xff0c;其实是应该有delete的&…

Redis常用数据类型和使用场景

Redis目前支持5种数据类型&#xff0c;分别是&#xff1a; String&#xff08;字符串&#xff09; List&#xff08;列表&#xff09; Hash&#xff08;字典&#xff09; Set&#xff08;集合&#xff09; Sorted Set&#xff08;有序集合&#xff09; 下面就分别介绍这五…

得物词分发平台技术架构建设与演进

前言 在文章开始前先介绍下导购&#xff0c;导购通常是指帮助消费者在购物过程中做出最佳决策的人或系统。在电商网站中&#xff0c;导购可以引导用户关注热卖商品或促销活动等&#xff0c;帮助用户更好地进行购物。导购的目的是为了提高用户的购物体验&#xff0c;促进销售额…

抽象工厂模式——产品族的创建

1、简介 1.1、简介 抽象工厂模式为创建一组对象提供了一种解决方案。与工厂方法模式相比&#xff0c;抽象工厂模式中的具体工厂不只是创建一种产品&#xff0c;它负责创建一族产品 1.2、定义 抽象工厂模式&#xff08;Abstract Factory Pattern&#xff09;&#xff1a;提供…

4、非线性数据结构

上一节课我们讲了线性数据结构&#xff0c;这一节我们说下非线性数据结构。 非线性数据结构&#xff0c;从字面意思来看&#xff0c;就是指不是线性的结构。线性结构的特点是只有一个前驱和一个后继。 那么非线性结构的特点就是有多个前驱或后继了。 如果只存在一个没有前驱的…

Python爬虫基础

文章目录 Python学习记录Python基础爬虫&#xff1a;代码&#xff1a;运行结果&#xff1a; Python学习记录 Python基础爬虫&#xff1a; 代码&#xff1a; import urllib.request import random import chardet#请求头列表 us["Mozilla/4.0 (compatible; MSIE 6.0; Wi…

(学习笔记-IP)IP基础知识

基本认识 IP在TCP/IP参考模型中处于第三层&#xff0c;也就是网络层。 网络层的主要作用是&#xff1a;实现主机与主机之间的通信&#xff0c;也叫点对点的通信。 网络层与数据链路层的关系&#xff1a; MAC的作用是实现直连的两个设备之间通信&#xff0c;而IP负责没有直连的…

消息队列- 背景知识

这里写目录标题 前言消息队列消息队列的作用常见的消息队列消息队列的核心概念BrokerServer核心概念消息队列的核心API消息队列与消费者之间的工作模式交换机的类型消息队列的持久化 总结 前言 消息队列,不知道大家是否陌生,如果说消息队列感到陌生的话, 有一个模型肯定大家都…

Nginx下载和安装教程、Nginx目录结构、Nginx具体应用

1、Nginx概述 Nginx是一款轻量级的开源Web服务器软件&#xff0c;也是一种反向代理服务器。它以其高性能和灵活性而被广泛应用于互联网领域。本文将介绍Nginx的概述、下载和安装以及目录结构。 &#xff08;1&#xff09;Nginx介绍 Nginx最初由Igor Sysoev开发&#xff0c;目…

Pycharm工具Python开发自动添加注释(详细)

方法自动添加参数注释 定义了一个函数&#xff0c;在函数下面敲入了三个双引号后&#xff0c;enter回车并没有自动出现注释&#xff0c;如图&#xff1a; 解决办法 Pycharm中依次打开File —> Settings —> Tools —> Python Integrated Tools&#xff0c;如图&…

C++笔记之对指针类型的变量进行+1操作

C笔记之对指针类型的变量进行1操作 在C中&#xff0c;对指针类型的变量进行"1"操作会根据指针的数据类型而有所不同。这涉及到指针的算术运算&#xff0c;C中的指针算术运算是根据指针所指向的数据类型的大小来进行的。 code review! 文章目录 C笔记之对指针类型的…

最受欢迎的12个Python开源框架,还没用过你就OUT了!!!

今天给大家带来了12个在GitHub等开源网站中最受欢迎的Python开源框架。如果你正在学习python&#xff0c;那么这12个开源框架&#xff0c;千万别错过&#xff0c;这些框架包括事件I/O&#xff0c;OLAP&#xff0c;Web开发&#xff0c;高性能网络通信&#xff0c;测试&#xff0…

zookeeper-3.7.1集群

1.下载&解压安装包apache-zookeeper-3.7.1-bin.tar.gz 解压到/app/ &改名zookeeper-3.7.1 [rootnode1 app]# tar -zxvf apache-zookeeper-3.7.1-bin.tar.gz -C /app/ [rootnode1 app]# mv apache-zookeeper-3.7.1-bin zookeeper-3.7.1 ---- 删除docs [rootnode1…

五步快速搭建个性化外卖小程序商城

随着人们生活节奏的加快&#xff0c;外卖行业蓬勃发展。为了满足用户的需求&#xff0c;许多企业开始使用小程序商城来提供外卖服务。那么&#xff0c;如何制作一个功能完善、用户友好的外卖小程序商城呢&#xff1f;下面就来为大家详细介绍一下制作的步骤。 首先&#xff0c;我…

Docker consul容器服务更新与发现

Docker consul容器服务更新与发现 一、什么事服务注册与发现二、什么是consul三、consul部署1、consul服务器2、registrator服务器3、consul-template 一、什么事服务注册与发现 服务注册与发现是微服务架构中不可或缺的重要组件。起初服务都是单节点的&#xff0c;不保障高可…

Vue3+Vite+TypeScript常用项目模块详解

目录 1.Vue3ViteTypeScript 概述 1.1 vue3 1.1.1 Vue3 概述 1.1.2 vue3的现状与发展趋势 1.2 Vite 1.2.1 现实问题 1.2 搭建vite项目 1.3 TypeScript 1.3.1 TypeScript 定义 1.3.2 TypeScript 基本数据类型 1.3.3 TypeScript语法简单介绍 2. 项目配置简单概述 2.…

【CEEMDAN-WOA-LSTM】完备集合经验模态分解-鲸鱼优化-长短时记忆神经网络研究(Python代码实现)

目录 &#x1f4a5;1 概述 1.1 完备集合经验模态分解原理 1.2 鲸鱼优化 1.3 LSTM &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f308;4 Python代码实现 &#x1f4a5;1 概述 1.1 完备集合经验模态分解原理 早期的 EMD 方法具有较强的自适应性&#xff0c;能够有…

【弹力设计篇】聊聊限流设计

为什么需要限流 对于一个后端系统来说&#xff0c;其处理能力是有限的&#xff0c;会受到业务流程&#xff0c;系统配置等的限制&#xff0c;QPS和TPS有一个上限值&#xff0c;比如一个订单系统1分钟可以处理100个请求。当1分钟超过100个请求的时候&#xff0c;我们为了保证系…

5.python设计模式【单例模式】

内容&#xff1a;保证一个类只有一个实例&#xff0c;并提供一个访问它的全局访问点角色&#xff1a; 单例&#xff08;Singleton&#xff09; UML图 举个例子&#xff1a; 需求&#xff1a;一个类只能实例化一个对象&#xff0c;不能实例化多个对象 from abc import abstract…
最新文章