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

news2024/11/24 4:23:36

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

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

🍚代码仓库:破浪晓梦

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

文章目录

  • 🍊树的概念及结构
    • 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.coloradmin.cn/o/792333.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL5.7 与 MariaDB10.1 审计插件兼容性验证

这是一篇关于发现 MariaDB 审计插件导致 MySQL 发生 crash 后&#xff0c;展开适配验证并进行故障处理的文章。 作者&#xff1a;官永强 爱可生DBA 团队成员&#xff0c;擅长 MySQL 运维方面的技能。热爱学习新知识&#xff0c;亦是个爱打游戏的宅男。 本文来源&#xff1a;原创…

20.1K Star!Notion的开源替代方案:AFFiNE

Notion这款笔记软件相信很多开发者都比较熟悉了&#xff0c;很多读者&#xff0c;包括我自己都用它来记录和管理自己的笔记。今天给大家推荐一个最近比较火的开源替代方案&#xff1a;AFFiNE。目前该开源项目已经斩获20.1K Star&#xff0c;热度非常的高&#xff0c;下面一起来…

Unhandled Exception: User denied permissions to access the device‘s location.

在写android app时&#xff0c;有的时候遇到这样的错误&#xff1a;未处理的异常:用户拒绝访问设备位置的权限。 即使加上了官网讲述的&#xff1a;下面两行代码任意一行&#xff0c;也不行。 <uses-permission android:name"android.permission.ACCESS_FINE_LOCATIO…

Java的第十四篇文章——文件和IO流

目录 学习目标 1. FIle类 1.1 File类的构造方法 1.2 FIle类的创建方法 1.3 File类的删除方法 1.4 File类的判断方法 1.4.1 绝对路径和相对路径 1.5 File类的获取的方法 1.6 File类的方法listFIles() 2. IO流对象 2.1 IO流对象的分类 2.1.1 按照操作的文件类型分类 …

【简单认识MySQL的MHA高可用配置】

文章目录 一、简介1、概述2、MHA 的组成3&#xff0e;MHA 的特点4、MHA工作原理 二、搭建MHA高可用数据库群集1.主从复制2.MHA配置 三、故障模拟四、故障修复步骤&#xff1a; 一、简介 1、概述 MHA&#xff08;Master High Availability&#xff09;是一套优秀的MySQL高可用…

Java网络编程基础概念

一、网络编程入门 1.1、软件结构 c/s结构&#xff1a;指客户端和服务器结构。如下图 B/S结构&#xff1a;指浏览器和服务器结构。如下图 共同点&#xff1a;无论哪一种架构&#xff0c;都离不开网络的支持。因此就设计到网络编程&#xff08;在一定的协议下&#xff0c;实…

android app控制ros机器人二

Ros-Mobile的使用基本熟悉&#xff0c;接下来熟悉代码&#xff0c;记录中间的问题。 GitHub - ROS-Mobile/ROS-Mobile-Android: Visualization and controlling application for Android 使用android studio打开项目后有bug。 BUG&#xff1a; 1.FAILURE: Build failed wit…

nvidia-smi输出的结果代表什么

nvidia-smi(NVIDIA System Management Interface) 是基于nvml的gpu的系统管理接口,主要用于显卡的管理和状态监控。 nvidia-smi简称NVSMI&#xff0c;提供监控GPU使用情况和更改GPU状态的功能&#xff0c;是一个跨平台工具&#xff0c;支持所有标准的NVIDIA驱动程序支持的Linu…

CMIP6数据处理及在气候变化、水文、生态等领域中的应用

气候变化对农业、生态系统、社会经济以及人类的生存与发展具有深远影响&#xff0c;是当前全球关注的核心议题之一。IPCC&#xff08;Intergovernmental Panel on Climate Change&#xff0c;政府间气候变化专门委员会&#xff09;的第六次评估报告明确&#xff1b;指出&#x…

低代码平台浅析:JNPF快速开发平台

目录 一、前言 二、低代码平台体验简述 三、关于平台 平台简介&#xff1a; 四、场景及用户分析 五、产品分析 1、着重讲一下JNPF的编辑器 2、业务流程 六、总结 一、前言 低代码平台能够改变了应用交付和管理的模式&#xff0c;大幅缩减交付周期&#xff0c;最终帮助业务加速创…

使用深度学习模型CNN进行实时情绪检测研究(Matlab代码实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f308;4 Matlab代码实现 &#x1f4a5;1 概述 使用深度学习模型CNN进行实时情绪检测是一种应用广泛的研究方向。下面是一个简要的步骤&#xff1a; 1. 数据收集和标注&#xff1a;收集包含…

华为OD机试真题 Java 实现【阿里巴巴找黄金宝箱(III)】【2023 B卷 100分】,附详细解题思路

目录 专栏导读一、题目描述二、输入描述三、输出描述四、解题思路五、Java算法源码六、效果展示1、输入2、输出3、说明4、那么问题来了&#xff0c;如果有两个满足的怎么办&#xff1f; 华为OD机试 2023B卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为O…

类和对象(下篇)

目录 1.再谈构造函数1.1构造函数体赋值1.2初始化列表1.3explicit关键字 2.Static成员2.1概念2.2特性 3.友元3.1友元函数3.2友元类 4.内部类5.再次理解类和对象总结 1.再谈构造函数 1.1构造函数体赋值 如下为对象赋初值构造函数体的语句&#xff0c;不能称作初始化。 class D…

Ubuntu18.04系统安装视频剪辑软件shotcut

Snap Store安装 使用的是最新的Ubuntu 18.04 LTS&#xff08;Bionic Beaver&#xff09;&#xff0c;其本身已安装Snap 如果没有安装&#xff0c;则可以使用以下命令安装SNAP $ sudo apt-get install snapd安装shotcut $ sudo snap install shotcut --classic启动shotcut $…

8.JAVA NIO核心之选择器(Selector)

highlight: arduino-light 选择器(Selector)概述 选择器Selector是 SelectableChannle 对象的多路复用器&#xff0c;Selector 可以同时监控多个SelectableChannel的 IO 状况&#xff0c;也就是说&#xff0c;利用 Selector可使一个单独的线程管理多个 Channel。Selector 是非阻…

shift语句的使用

[rootvm1 scripts]# help shift shift: shift [n]Shift positional parameters.Rename the positional parameters $N1,$N2 ... to $1,$2 ... If N isnot given, it is assumed to be 1.Exit Status:Returns success unless N is negative or greater than $#.说明&#xff1a…

SWMM模型:水文水动力模型在城市内涝、城市排水、海绵城市规划设计中深度应用

查看原文>>>最新水文水动力模型在城市内涝、城市排水、海绵城市规划设计中深度应用 随着计算机的广泛应用和各类模型软件的发展&#xff0c;将排水系统模型作为城市洪灾评价与防治的技术手段已经成为防洪防灾的重要技术途径。本文聚焦于综合利用GIS及CAD等工具高效地进…

Rust vs Go:常用语法对比(十二)

题图来自 Rust vs Go in 2023[1] 221. Remove all non-digits characters Create string t from string s, keeping only digit characters 0, 1, 2, 3, 4, 5, 6, 7, 8, 9. 删除所有非数字字符 package mainimport ( "fmt" "regexp")func main() { s : hei…

07 |「注解、反射、多线程、锁」

前言 函数 文章目录 前言一、注解二、反射1、作用 三、多线程1、基本概念2、实现方式1&#xff09;继承 Thread 类2&#xff09;实现Runnable接口3、常用 API 四、锁1、为什么2、锁 一、注解 注解不影响程序逻辑&#xff0c;但会被编译器在不同阶段&#xff08;编译、执行&…

【小白必看】利用Python生成个性化名单Word文档

文章目录 前言所需文件及文件格式说明excel数据如下word 模板如下文件目录格式及生成后的文件 导入所需的模块&#xff1a;打开 Excel 文件&#xff1a;选择工作表&#xff1a;获取数据列表&#xff1a;遍历数据并生成 Word 文档&#xff1a;完整代码结束语 前言 对于需要批量…