数据结构——树

目录

1. 树的概念与结构

①树的相关概念

Ⅰ. 什么是树

Ⅱ. 树中的相关概念

②树的表示法

③树的应用

2. 二叉树的概念与结构

①二叉树的概念

②特殊二叉树

③二叉树性质及证明

④二叉树性质相关习题 

3. 堆

①堆的概念及结构

②堆的实现

⑴堆的向上调整法

⑵堆的向下调整法

⑶堆的插入与删除

⑷堆的创建

⑸堆的接口与实现 

③堆的应用

⑴堆排序

⑵Top-K问题

4. 二叉树的链式结构

①二叉树的遍历

⑴前序遍历

⑵中序遍历

⑶后序遍历

⑷层序遍历

②求节点个数以及高度等

⑴二叉树节点个数

⑵二叉树的高度

⑶二叉树第k层节点个数

⑷二叉树查找值为x的节点


1. 树的概念与结构

①树的相关概念

Ⅰ. 什么是树

树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。之所以称之为树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
1. 有一个 特殊的结点,称为根结点 ,根节点没有前驱结点
2. 除根节点外,其余结点被分成 M(M>0) 个互不相交的集合 T1 T2 …… Tm ,其中每一个集合 Ti(1<= i<= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有 0 个或多个后继
3. 因此, 树是递归定义 的。

随便举例一棵树,如

 注:树形结构中,子树之间不能有交集,否则就不是树形结构

Ⅱ. 树中的相关概念

我们以下面这个树为例

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

②树的表示法

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

表示方法如下

typedef int DataType;
struct Node
{
	struct Node* _firstChild1;  // 第一个孩子结点
	struct Node* _pNextBrother; // 指向其下一个兄弟结点
	DataType _data;             // 结点中的数据域
};

 我们以如下一棵树为例

左孩子右兄弟表示为

③树的应用

表示文件系统的目录树结构

windos中的文件也是以树的形式储存的

2. 二叉树的概念与结构

①二叉树的概念

二叉树是一种树形结构,每个节点最多只能有两个子节点,并且左右子节点的顺序是确定的。一个二叉树可以为空,或者由一个根节点和左右两个子二叉树组成。其中左子树和右子树也分别是二叉树。

从上图中我们不难看出: 

1. 二叉树不存在度大于 2 的结点
2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

因此,对于任意的二叉树都是由以下几种情况复合而成的

②特殊二叉树

1. 满二叉树 :一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K ,且结点总数是,则它就是满二叉树。
2. 完全二叉树 :完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为 K的,有n 个结点的二叉树,当且仅当其每一个结点都与深度为 K 的满二叉树中编号从 1 n 的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。

③二叉树性质及证明

1. 若规定根节点的层数为 1 ,则一棵非空二叉树的 第h 层上最多有 2^(h - 1) 个结点.
2. 若规定根节点的层数为 1 ,则 深度为 h 的二叉树的最大结点数是 2^h - 1.
3. 对任何一棵二叉树 , 如果度为 0 其叶结点个数为N0 , 度为 2 的分支结点个数为N2 , 则有 N0 = N2+ 1

假设对于任意一棵具有 k + 1 个节点的二叉树,都满足N0​=N2​+1。

对于一颗具有 k+1 个节点的二叉树,我们可以将其分为根节点和左右两棵子树。设左子树有 n1​ 个节点,右子树有 n2​ 个节点。

那么该二叉树的 N0​ 就是左子树的 N0​ 与右子树的 N0​ 之和,即 N0​=N01​+N02​。

同理 N2​ 也可以拆分为左右两个子树,即 N2​=N21​+N22​。

根据二叉树的性质,除了根节点,每个节点的度数最多为 2。我们将 N0​ 表示为叶节点数目,那么左子树和右子树的 n0​ 和 n2​ 就可以表示为:

左子树:n01​=N01​+1,n21​=N01​

右子树:n02​=N02​+1,n22​=N02​

总结起来,我们得到以下三式:

N0​=n01​+n02​=N01​+N02​+2

N2​=n21​+n22​=N01​+N02​

代入 N0​=N2​+1,得到

N01​+N02​+2=N01​+N02​+1

因此,对于任意一颗具有 k+1 个节点的二叉树都成立。

4. 若规定根节点的层数为 1 ,具有 n 个结点的满二叉树的深度 h= log(n + 1). (ps:是log 2为底,n+1 为对数 )
5. 对于具有 n 个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从 0 开始编号,则对于序号为i 的结点有:
1. i>0 i 位置节点的双亲序号: (i-1)/2 i=0 i 为根节点编号,无双亲节点
2. 2i+1<n ,左孩子序号: 2i+1 2i+1>=n 否则无左孩子
3. 2i+2<n ,右孩子序号: 2i+2 2i+2>=n 否则无右孩子

图示如下 

证明

1. 若 i>0,则 i 位置结点的双亲序号为 (i−1)/2。

首先我们可以根据完全二叉树的定义,得出父节点编号一定小于当前节点编号,且父节点在当前节点的上一级。又因为当前节点是从上至下从左至右编号的完全二叉树中的第 i 个节点。那么它的父节点肯定是处于序号为 (i−1)/2 的位置,这就证明了该结论。

2. 若 2i+1<n,则左孩子序号为 2i+1,否则无左孩子。

由完全二叉树的定义,一个节点的左子节点编号为 2i+1,右子节点编号为 2i+2。

证明:

当 2i+1<n 时,按照完全二叉树的定义,该节点一定有左孩子,左孩子的编号为 2i+1。当 2i+1≥n 时,也就是说该节点没有左孩子,因为编号最大不超过n−1。

3. 若 2i+2<n,则右孩子序号为 2i+2,否则无右孩子。

与第二个结论的证明类似,当 2i+2<n 时,按照完全二叉树的定义,该节点一定有右孩子,右孩子的编号为 2i+2。当 2i+2≥n 时,该节点没有右孩子,因为编号最大不超过 n−1。

综上所述,我们证明了对于具有 n 个结点的完全二叉树,编号为 i 的节点满足的三个性质,即若 i>0,则它的双亲节点序号为 (i−1)/2;若 2i+1<n,则它的左孩子序号为 2i+1;若 2i+2<n,则它的右孩子序号为 2i+2。

④二叉树性质相关习题 

1. 某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为( )
A 不存在这样的二叉树
B 200
C 198
D 199

解:

∵N0 = N2 + 1

∴N0 = 200,因此答案选B 

2. 在具有 2n 个结点的完全二叉树中,叶子结点个数为( )
A n
B n+1
C n-1
D n/2

解:叶子结点即度为0的结点,将其转化为计算N0大小即可

∵2*n = N0 + N1 + N2, N0 = N2 + 1,又∵在完全二叉树中度为1的节点取值仅有0/1,

∴当N1 = 0时,2*n = 2*N0+1有N0=(2*n-1)/2,无匹配选项;当N1=1时,2*n=2*N0+2有N0=n-1,因此答案选C

3. 一棵完全二叉树的节点数位为 531 个,那么这棵树的高度为( )
A 11
B 10
C 8
D 12

解:

∵2^10 = 1024,又∵一棵满二叉树的结点数为2^h - 1,

∴一棵高度为10的满二叉树的节点数为1023,而一棵高度为9的满二叉树的节点数为511

∵531∈[512,1023]

∴这是一棵高度为10的树

4. 一个具有 767 个节点的完全二叉树,其叶子节点个数为()
A 383
B 384
C 385
D 386

解:

∵767 = N0 + N1 + N2,N0 = N2 + 1

∴766 = 2*N0 + N1,又∵N0不能是小数,∴N1=0

∴N0=383,因此选A

3. 堆

①堆的概念及结构

堆是一种特殊的树状数据结构,具有以下两个特点:

  • 堆是一棵完全二叉树。
  • 堆中任何一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。

这个定义中提到的"大于等于"或"小于等于",就是指堆的排序方式。如果父节点的值大于等于子节点的值,就称为"大根堆",反之则是"小根堆"。

如下图所示

②堆的实现

根据上面的结我们可以定义堆的自定义类型如下

typedef int HPDataType;

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

⑴堆的向上调整法

在知道了堆的结构后,当我们需要向堆中插入元素时,应该选择的是尾插,但是尾插之后不能保证此时依旧满足堆的性质,(以大根堆为例)如

此时受到影响的可能是最后一个节点的所有祖先,即

代码如下

void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//大根堆
void AdjustUp(HPDataType* a, int child)
{
	//当前节点为child
	//通过child找到parent
	int parent = (child - 1) / 2;

	//用parent作判断条件不够严谨,因为如果child为0时,
	//parent=(0-1)/2=-0.5(C语言向下取整,因此parent=0,while的判断条件没有改变)
	//while (parent >= 0)
	//当child=0时,已经没有必要再向上调整,循环结束
	while(child > 0)
	{
		//因为是大根堆,因此当孩子大于父亲时,需要交换父子位置
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

⑵堆的向下调整法

那当我们从堆中取出或删除元素时,应该做到的是取出(删除)堆顶元素(即首元素),但是如果我们直接将该位置元素直接删除,那么就会导致1. 原来的父子关系被打乱;2. 依次挪动数据导致效率低下。如图所示

因此,我们在此最好的选择是将堆顶元素和最后一个元素交换,这样既解决了效率又保持了相对的父子关系,唯一需要进行调整的就是大根堆性质的所决定的父子关系

代码如下

void AdjustDown(HPDataType* a, int parent, int n)
{
	// 通过父节点找到左右子节点
	int lchild = 2 * parent + 1;
	int rchild = 2 * parent + 2;

	// 如果左孩子所处位置已经不在堆中就结束循环
	// 表示当前父节点为叶子结点,无需向下调整
	while (lchild < n)
	{		
		// 假设左孩子为较大的孩子
		int mchild = lchild;
		// 当右孩子存在时,如果右孩子比左孩子大,就将右孩子改为较大的孩子
		if (rchild < n && a[mchild] < a[rchild])
		{
			mchild = rchild;
		}

		// 不断向下调整
		if (a[parent] < a[mchild])
		{
			Swap(&a[parent], &a[mchild]);
			parent = mchild;
			lchild = 2 * parent + 1;
			rchild = 2 * parent + 2;
		}
		else
		{
			break;
		}
	}
}

⑶堆的插入与删除

由于堆是由数组模拟实现的,因此在堆中插入数据一般选择尾插(选择尾插有两个好处:1. 不会破坏原来的父子关系; 2. 效率高),它的代码实现如下

// 堆的插入
void HeapPush(Heap* hp, HPDataType x)
{
	assert(hp);

	// 检测数组是否需要进行扩容
	if (hp->size == hp->capacity)
	{
		HPDataType* tmp = (HPDataType*)realloc(hp->a, sizeof(HPDataType) * hp->capacity * 2);
		if (tmp == NULL)
		{
			perror("malloc fail");
			return;
		}

		hp->a = tmp;
		hp->capacity *= 2;
	}

	hp->a[hp->size] = x;
	hp->size++;

	// 传入的是最后一个有效元素的下标,即size-1
	AdjustUp(hp->a, hp->size - 1);
}

类似的,堆数据的取出(删除)一般选择将首元素与尾元素交换并将size-1表示有效元素的减少,

它的代码实现如下

// 堆的判空
int HeapEmpty(Heap* hp)
{
	assert(hp);

	return hp->size == 0;
}

// 堆的删除
void HeapPop(Heap* hp)
{
	assert(hp);
    // 若堆为空就不应该再被允许删除数据
	assert(!HeapEmpty(hp));

	Swap(&hp->a[0], &hp->a[hp->size - 1]);
	hp->size--;

	// 需要传入堆有效数据个数
	AdjustDown(hp->a, 0, hp->size - 1);
}

⑷堆的创建

堆的创建有两种形式:

第一种是创建一个空堆,不断向其中插入数据构成一个堆,其代码实现如下

// 堆的创建(一)
void HeapInit(Heap* hp)
{
	assert(hp);

	HPDataType* a = (HPDataType*)malloc(sizeof(HPDataType) * 4);
	if (a == NULL)
	{
		perror("malloc fail");
		return;
	}

	hp->a = a;
	hp->size = 0;
	hp->capacity = 4;
}

第二种是对一个已经存在的数组建堆,这里有两种建堆方式,

一种是向上调整法建堆,代码实现如下

void HeapCreatUp(Heap* hp, HPDataType* a, int n)
{
	assert(hp);

	// 向上调整法建堆 -- 时间复杂度: O(N*logN)
	// 从头开始向后(下)遍历,将每个元素都向上调整一次
	for (int i = 0; i < n; i++)
	{
		AdjustUp(a, i);
	}

}

此建堆方式的时间复杂度为O(N*logN),证明如下

另一种是向下调整法建堆,代码实现如下

void HeapCreatDown(Heap* hp, HPDataType* a, int n)
{
	assert(hp);

	// 向下调整法建堆 -- 时间复杂度: O(N)
	// 第一个n-1是为了找到数组最后一个元素的下标
	// 第二个-1是为了找到该元素父亲
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, i, n);
	}

}

此建堆方式的时间复杂度为O(N),证明如下 

⑸堆的接口与实现 

接口如下

// 堆的创建
void HeapCreat(Heap* hp, HPDataType* a, int n);

// 堆的初始化
void HeapInit(Heap* hp);

// 堆的销毁
void HeapDestory(Heap* hp);

// 堆的插入
void HeapPush(Heap* hp, HPDataType x);

// 堆的删除
void HeapPop(Heap* hp);

// 取堆顶的数据
HPDataType HeapTop(Heap* hp);

// 堆的数据个数
int HeapSize(Heap* hp);

// 堆的判空
int HeapEmpty(Heap* hp);

实现如下

// 堆的创建
void HeapCreat(Heap* hp, HPDataType* a, int n)
{
	assert(hp);

	// 向上调整法建堆 -- 时间复杂度: O(N*logN)
	// 从头开始向后(下)遍历,将每个元素都向上调整一次
	for (int i = 0; i < n; i++)
	{
		AdjustUp(a, i);
	}

	// 向下调整法建堆 -- 时间复杂度: O(N)
	// 第一个n-1是为了找到数组最后一个元素的下标
	// 第二个-1是为了找到该元素父亲
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, i, n);
	}
}
// 堆的初始化
void HeapInit(Heap* hp)
{
	assert(hp);

	HPDataType* a = (HPDataType*)malloc(sizeof(HPDataType) * 4);
	if (a == NULL)
	{
		perror("malloc fail");
		return;
	}

	hp->a = a;
	hp->size = 0;
	hp->capacity = 4;
}
// 堆的销毁
void HeapDestory(Heap* hp)
{
	assert(hp);

	free(hp->a);
	free(hp);
}
// 堆的插入
void HeapPush(Heap* hp, HPDataType x)
{
	assert(hp);

	// 检测数组是否需要进行扩容
	if (hp->size == hp->capacity)
	{
		HPDataType* tmp = (HPDataType*)realloc(hp->a, sizeof(HPDataType) * hp->capacity * 2);
		if (tmp == NULL)
		{
			perror("malloc fail");
			return;
		}

		hp->a = tmp;
		hp->capacity *= 2;
	}

	hp->a[hp->size] = x;
	hp->size++;

	// 传入的是最后一个有效元素的下标,即size-1
	AdjustUp(hp->a, hp->size - 1);
}
// 堆的判空
int HeapEmpty(Heap* hp)
{
	assert(hp);

	return hp->size == 0;
}
// 堆的删除
void HeapPop(Heap* hp)
{
	assert(hp);
	// 若堆为空就不应该再被允许删除数据
	assert(!HeapEmpty(hp));

	Swap(&hp->a[0], &hp->a[hp->size - 1]);
	hp->size--;

	// 需要传入堆有效数据个数
	AdjustDown(hp->a, 0, hp->size - 1);
}
// 取堆顶的数据
HPDataType HeapTop(Heap* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp));

	return hp->a[0];
}
// 堆的数据个数
int HeapSize(Heap* hp)
{
	assert(hp);

	return hp->size;
}

③堆的应用

在发明了堆的结构后,它也被大量投入到其他应用中

⑴堆排序

详见堆排序

⑵Top-K问题

Top-k问题是一类需要找出前k个最大(最小)元素的问题,其中k通常是一个小于总数的正整数。

分析:

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都无法全部加载到内存中)。
而最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆
求前k个最大(小)的元素,建小(大)堆
2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
通俗来讲,即为了求k个最大(小)的树,先设定一个k个大小的小堆,将之后的每个元素与小堆的堆顶元素相比较,如果大于堆顶元素就替换其进堆,这样到最后就能保证堆里的元素始终大于堆顶,而堆顶又始终大于堆外的数据,即找到了最大的K个元素

 代码实现

// TopK问题:找出N个数里面最大/最小的前K个问题。

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void AdjustDown(int* a, int parent, int n)
{
	// 通过父节点找到左右子节点
	int lchild = 2 * parent + 1;
	int rchild = 2 * parent + 2;

	// 如果左孩子所处位置已经不在堆中就结束循环
	// 表示当前父节点为叶子结点,无需向下调整
	while (lchild < n)
	{
		// 假设左孩子为较大的孩子
		int mchild = lchild;
		// 当右孩子存在时,如果右孩子比左孩子大,就将右孩子改为较大的孩子
		if (rchild < n && a[mchild] > a[rchild])
		{
			mchild = rchild;
		}

		// 不断向下调整
		if (a[parent] > a[mchild])
		{
			Swap(&a[parent], &a[mchild]);
			parent = mchild;
			lchild = 2 * parent + 1;
			rchild = 2 * parent + 2;
		}
		else
		{
			break;
		}
	}
}

// 堆的创建
void HeapCreat(int* a, int n)
{
	// 向下调整法建堆 -- 时间复杂度: O(N)
	// 第一个n-1是为了找到数组最后一个元素的下标
	// 第二个-1是为了找到该元素父亲
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, i, n);
	}
}


// 此处假设寻找K个最大数据
void TestTopk(int* a, int n, int k)
{
	// 1.建一个有k个数据的小堆
	int* tmp = (int*)malloc(sizeof(int) * k);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}

	int i = 0;
	for (i = 0; i < k; i++)
	{
		tmp[i] = a[i];
	}

	HeapCreat(tmp, k);

	// 2.将这之后的n-k个数据都与堆顶元素比较
	while (i != n)
	{
		if (a[i] > tmp[0])
		{
			Swap(&a[i], &tmp[0]);
			AdjustDown(tmp, 0, k);
		}

		i++;
	}

	for (i = 0; i < k; i++)
	{
		printf("%d ", tmp[i]);
	}
}

测试如下

4. 二叉树的链式结构

①二叉树的遍历

二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
二叉树的遍历又分为递归结构和非递归结构
1. 递归结构:前序/中序/后序遍历
2. 非递归结构:层序遍历

注:

深度优先遍历(DFS):如二叉树的前序遍历等,一般用递归实现

广度优先遍历(BFS):如二叉树的层序遍历,一般用队列实现 

在这里,我们使用如下一颗二叉树来举例

模拟实现它的代码如下

typedef int BTDataType;
typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;

BTNode* BuyNode(BTDataType n)
{
	BTNode* a = (BTNode*)malloc(sizeof(BTNode));
	if (a == NULL)
	{
		perror("malloc fail");
		return NULL;
	}

	a->data = n;
	a->left = NULL;
	a->right = NULL;

}

BTNode* CreatBinaryTree()
{
	BTNode* node1 = BuyNode(1);
	BTNode* node2 = BuyNode(2);
	BTNode* node3 = BuyNode(3);
	BTNode* node4 = BuyNode(4);
	BTNode* node5 = BuyNode(5);
	BTNode* node6 = BuyNode(6);

	node1->left = node2;
	node1->right = node4;
	node2->left = node3;
	node4->left = node5;
	node4->right = node6;

	return node1;
}

⑴前序遍历

前序遍历的形式是:根 左子树 右子树

对上面那棵树前序遍历有

从1处开始遍历,先访问根1,再访问{左子树2,此时将2视作新的根,先访问根2[再访问左子树3,然后(3先访问左子树NULL,再访问右子树NULL),再访问右子树NULL],再访问右子树4,然后4[访问左子树5,5先(访问左子树NULL,再访问右子树NULL),再访问右子树6,6先(访问左子树NULL,再访问右子树NULL)]}

代码实现

// 二叉树前序遍历
void PreOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}

	printf("%d ", root->data);
	PreOrder(root->left);
	PreOrder(root->right);
}

运行和原图验证有

⑵中序遍历

前序遍历的形式是:左子树 根 右子树

对上面那棵树前序遍历有

在遍历时,记住“遇到根不能访问,先访问它的左子树”即可 

代码实现

// 二叉树中序遍历
void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}

	InOrder(root->left);
	printf("%d ", root->data);
	InOrder(root->right);
}

验证有

⑶后序遍历

前序遍历的形式是:左子树 右子树 根

对上面那棵树前序遍历有

 在遍历时,记住“遇到根和右子树都不能访问,先访问它的左子树,访问完左子树先访问右子树,再访问根”即可 

代码实现

// 二叉树后序遍历
void PostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}

	PostOrder(root->left);
	PostOrder(root->right);
	printf("%d ", root->data);
}

验证有

⑷层序遍历

层序遍历的形式为:从根节点开始向下的每一层从左到右依次遍历

对上面那棵树层序遍历有

在实现层序遍历时,我们通常需要使用队列这一数据结构,将根节点入队列中,这之后每次遍历到根节点后,使其出队列,并将其左右子节点入队列,其示意图如下 

代码实现(在这里使用队列,将其typedef的内容改为struct BinaryTreeNode*)

// 层序遍历
void LevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);

	QueuePush(&q, root);

	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		printf("%d ", front->data);

		if (front->left)
			QueuePush(&q, front->left);
		if (front->right)
			QueuePush(&q, front->right);
	}

	QueueDestroy(&q);
}

int main()
{
	BTNode* root = CreatBinaryTree();
	LevelOrder(root);

	return 0;
}

运行如下

②求节点个数以及高度等

在此依旧使用上述二叉树,即

⑴二叉树节点个数

// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}

	return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}

验证有

⑵二叉树的高度

// 二叉树的高度
int BinaryHeight(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}

	// 记录左右的高度防止重复访问
	int leftHeight = BinaryHeight(root->left) + 1;
	int rightHeight = BinaryHeight(root->right) + 1;

	return leftHeight > rightHeight ? leftHeight : rightHeight;
}

验证有

⑶二叉树第k层节点个数

// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
	if (root == NULL)
	{
		return 0;
	}

	if (k == 1)
	{
		if (root)
			return 1;
	}

	return BinaryTreeLevelKSize(root->left, k-1) 
		+ BinaryTreeLevelKSize(root->right, k-1);
}

验证有

⑷二叉树查找值为x的节点

// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;

	if (root->data == x)
		return root;

	BTNode* lval = BinaryTreeFind(root->left, x);
	if (lval)
		return lval;

	BTNode* rval = BinaryTreeFind(root->right, x);
	if (rval)
		return rval;

	return NULL;
}

验证有

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值