二叉树详解

目录

1.树的概念与结构

1.1 树的概念

1.2 树的相关概念

1.3 树的表示

1.4 树在实际中的运用(表示文件系统的目录树结构)

2. 二叉树的概念和结构

2.1 二叉树的概念

2.2 特殊的二叉树

2.3 二叉树的性质

2.4 二叉树的存储结构

3. 二叉树的顺序结构及实现

3.1 二叉树的顺序结构

3.2 堆的概念及结构

3.3 堆的实现

3.3.1 堆的向下调整算法

3.3.2 堆的创建

3.3.3 建堆时间复杂度

3.3.4 堆的插入

3.3.4 堆的删除

3.3.5 堆的实现

3.4 堆的应用

3.4.1 堆排序

3.4.2 Topk问题

4. 二叉树链式结构的实现

4.1 二叉树的创建

4.2 二叉树的遍历

4.3 二叉树的节点数以及高度

4.4 二叉树的节点查找

4.5 判断是否为完全二叉树

4.6 二叉树的销毁


1.树的概念与结构

1.1 树的概念

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

1.2 树的相关概念

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

1.3 树的表示

树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了, 既然保存值域,也要保存结点和结点之间
的关系 ,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法
等。我们这里就简单的了解其中最常用的 孩子兄弟表示法
typedef int DataType;
struct Node
{
 struct Node* firstChild1; // 第一个孩子结点
 struct Node* pNextBrother; // 指向其下一个兄弟结点
 DataType data; // 结点中的数据域
}

这样我们就可以通过一个结点的brother找到它所有的孩子。

1.4 树在实际中的运用(表示文件系统的目录树结构)

2. 二叉树的概念和结构

2.1 二叉树的概念

一棵二叉树是结点的一个有限集合,该集合 :
1. 或者为空
2. 由一个根结点加上两棵别称为左子树和右子树的二叉树组成
3. 二叉树不存在度大于 2 的结点也就是二叉树的度为2
4. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

2.2 特殊的二叉树

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

2.3 二叉树的性质

1. 若规定根结点的层数为1,则一棵非空二叉树的i层上最多有2^(i-1)

个结点 .
2. 若规定根结点的层数为 1 ,则 深度为 h 的二叉树的最大结点数是2^h-1
3. 对任何一棵二叉树 , 如果度为 0 其叶结点个数为N0  , 度为 2 的分支结点个数为N2  , 则有 N0 = N2+1
/*
* 假设二叉树有N个结点
* 从总结点数角度考虑:N = n0 + n1 + n2 ①
*
* 从边的角度考虑,N个结点的任意二叉树,总共有N-1条边
* 因为二叉树中每个结点都有双亲,根结点没有双亲,每个节点向上与其双亲之间存在一条边
* 因此N个结点的二叉树总共有N-1条边
*
* 因为度为0的结点没有孩子,故度为0的结点不产生边; 度为1的结点只有一个孩子,故每个度为1的结
点* * 产生一条边; 度为2的结点有2个孩子,故每个度为2的结点产生两条边,所以总边数为:
n1+2*n2
* 故从边的角度考虑:N-1 = n1 + 2*n2 ②
* 结合① 和 ②得:n0 + n1 + n2 = n1 + 2*n2 - 1
* 即:n0 = n2 + 1
*/

4.若规定根结点的层数为1,具有n个结点的满二叉树的深度,h=log2(n+1)(ps: log2(n+1)是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否则无右孩子

2.4 二叉树的存储结构

二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
1. 顺序存储
顺序结构存储就是使用 数组来存储 ,一般使用数组 只适合表示完全二叉树 ,因为不是完全二叉树会有空
间的浪费。而现实中使用中只有堆才会使用数组来存储,堆就是用数组存储的 二叉树顺
序存储在物理上是一个数组,在逻辑上是一颗二叉树。
2. 链式存储
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是
链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所
在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面课程
学到高阶数据结构如红黑树等会用到三叉链。
如下图所示:
三叉链表就是多了一个parent指针指向该节点的父节点。
typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
 struct BinTreeNode* left; // 指向当前结点左孩子
 struct BinTreeNode* right; // 指向当前结点右孩子
 BTDataType data; // 当前结点值域
}
// 三叉链
struct BinaryTreeNode
{
 struct BinTreeNode* parent; // 指向当前结点的双亲
 struct BinTreeNode* left; // 指向当前结点左孩子
 struct BinTreeNode* right; // 指向当前结点右孩子
 BTDataType data; // 当前结点值域
};

3. 二叉树的顺序结构及实现

3.1 二叉树的顺序结构

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结
构存储。 现实中我们通常把堆 ( 一种二叉树 ) 使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统
虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
上图就是为什么完全二叉树可以用顺序结构

3.2 堆的概念及结构

3.3 堆的实现

3.3.1 堆的向下调整算法

现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根结点开始的向下调整算法可以把它调整
成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
每次只能调整一个结点且满足调整结点的左右子树满足堆的结构如下图所示

3.3.2 堆的创建

堆分为大堆和小堆向上调整算法这里我们不演示了时间复杂度不如向下调整算法下面我们会进行分析的

下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算
法,把它构建成一个堆。根结点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子结点的
子树开始调整,一直调整到根结点的树,就可以调整成堆。
倒数第一个非叶子结点就是最后一个叶子结点的父节点为什么从这开始呢?当我们从这里调整是因为这时候满足该节点的左右子树是堆。这样一个一个调就可以完成了建堆的过程

3.3.3 建堆时间复杂度

这里我们需要注意当我们得到t与h的关系后要把h换成n的表达式因为时间复杂度看的是n个节点调整完需要移动的总步数

向下调整我们可以看到结点越多的层需要调整的次数少;而向上调整结点越多的层数需要调整的次数多因此建堆我们采用向上调整算法。

3.3.4 堆的插入

3.3.4 堆的删除

3.3.5 堆的实现

typedef int HPDataType;
typedef struct Heap
{
 HPDataType* _a;
 int _size;
 int _capacity; 
}Heap;
// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n);
// 堆的销毁
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 AddjustDown(HPDataType* a, int n, int parent)
{
	//建立大堆
	assert(a);
	int child = 2 * parent + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child + 1] > a[child])
		{
			child++;
		}
		if (a[parent] < a[child])
		{
			int tem = a[parent];
			a[parent] = a[child];
			a[child] = tem;
			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}

// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n)
{
	assert(hp);
	assert(a);
	hp->_capacity = n;
	hp->_size = n;
	hp->_a = (HPDataType*)malloc(sizeof(HPDataType)*n);
	for (int i = (n - 2) / 2; i >= 0; i--)
	{
		AddjustDown(a, n, i);
	}
	//接下来把这个数组拷贝一份到堆中
	for (int i = 0; i < n; i++)
	{
		hp->_a[i] = a[i];
	}

}

堆的销毁:

// 堆的销毁
void HeapDestory(Heap* hp)
{
	assert(hp);
	free(hp->_a);
	hp->_a = NULL;
	hp->_size = 0;
	hp->_capacity = 0;
}
堆的插入:
//向上调整
AddjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	while (parent >= 0)
	{
		if (a[child] > a[parent])
		{
			int tem = a[parent];
			a[parent] = a[child];
			a[child] = tem;
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}
// 堆的插入
void HeapPush(Heap* hp, HPDataType x)
{
	assert(hp);
	//插入之前看看是否需要扩容
	if (hp->_size == hp->_capacity)
	{
		int newcapacity = hp->_capacity == 0 ? 4 : 2 * hp->_capacity;
		HPDataType* ret = (HPDataType*)realloc(hp->_a, sizeof(HPDataType) * newcapacity);
		if (ret == NULL)
		{
			perror("realloc");
			return;
		}
		hp->_a = ret;
		hp->_capacity = newcapacity;
	}
	hp->_a[(hp->_size)++] = x;
	//下面进行向上调整为大堆
	AddjustUp(hp->_a, hp->_size - 1);

}

堆的删除:

// 堆的删除
void HeapPop(Heap* hp)
{
	assert(hp);
	assert(hp->_size);
	int tem = hp->_a[0];
	hp->_a[0] = hp->_a[hp->_size-1];
	hp->_a[hp->_size-1] = tem;
	hp->_size--;
	AddjustDown(hp->_a, hp->_size, 0);
}

取堆顶的数据

// 取堆顶的数据
HPDataType HeapTop(Heap* hp)
{
	assert(hp && hp->_size);
	return hp->_a[0];
}
// 堆的数据个数
int HeapSize(Heap* hp)
{
	assert(hp);
	return hp->_size;
}

// 堆的判空
int HeapEmpty(Heap* hp)
{
	assert(hp);
	return hp->_size == 0;
}

3.4 堆的应用

3.4.1 堆排序

堆排序就是利用了堆的删除这里以大堆为例,每次把堆顶数据与堆尾交换然后再删除在进行向下调整这样就实现了对数组的排序

这里升序用大堆;降序用小堆。

void HeapSort(HPDataType* a, int n)
{
	//建立大堆
	for (int i = (n - 2) / 2; i >= 0; i--)
	{
		AddjustDown(a, n, i);
	}
	int end = n - 1;
	while (end >= 0)
	{
		int tem = a[0];
		a[0] = a[end];
		a[end] = tem;
		AddjustDown(a, end, 0);
		end--;
	}
}

3.4.2 Topk问题

TOP-K 问题:即求数据结合中前 K 个最大的元素或者最小的元素,一般情况下数据量都比较大
比如:专业前 10 名、世界 500 强、富豪榜、游戏中前 100 的活跃玩家等。
对于 Top-K 问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了 ( 可能
数据都不能一下子全部加载到内存中 ) 。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前 K 个元素来建堆
k 个最大的元素,则建小堆
k 个最小的元素,则建大堆
2. 用剩余的 N-K 个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余 N-K 个元素依次与堆顶元素比完之后,堆中剩余的 K 个元素就是所求的前 K 个最小或者最大的元素。
void DateCreate()
{
	FILE* input = fopen("data.txt", "w");
	if (input == NULL)
	{
		perror("input");
		return;
	}
	for (int i = 0; i < 1000000; i++)
	{
		int x = rand() % 100 + i;
		fprintf(input, "%d\n", x);
	}
	fclose(input);
	input = NULL;
}
int main()
{
	srand((unsigned int)time(NULL));
	//1.产生数据
	DateCreate();
	//2.读数据
	int n = 0;
	printf("请输入你要找几个最小的数:\n");
	scanf("%d", &n);
	int* arr = (int*)malloc(sizeof(int) * n);
	FILE* input = fopen("data.txt", "r");
	if (input == NULL)
		return;
	for (int i = 0; i < n; i++)
	{
		fscanf(input, "%d\n", &arr[i]);	
	}
	//下面进行建大堆过程找小的数进堆
	for (int i = (n - 2) / 2; i >= 0; i--)
	{
		AddjustDown(arr, n, i);
	}
	//与堆顶元素比较如果小就进堆
	int tem;
	while (fscanf(input, "%d", &tem) != EOF)
	{
		if (tem < arr[0])
		{
			arr[0] = tem;
			AddjustDown(arr, n, 0);
		}
	}
	fclose(input);
	input = NULL;
	for (int i = 0; i < n; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

4. 二叉树链式结构的实现

4.1 二叉树的创建

首先是树节点的定义

typedef char BTDataType;

typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi)
{
	if (*pi >= n || a[(*pi)] == '#')
	{
		(*pi)++;
		return NULL;
	}
	BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
	newnode->data = a[(*pi)++];
	newnode->left = BinaryTreeCreate(a, n, pi);
	newnode->right = BinaryTreeCreate(a, n, pi);
	return newnode;
}

这里创建一个树的方法就是把该树的前序写成数组按照前序构建树

n是数组元素的个数,pi就是元素下标的地址

4.2 二叉树的遍历

// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("# ");
		return;
	}
	printf("%c ", root->data);
	BinaryTreePrevOrder(root->left);
	BinaryTreePrevOrder(root->right);

}
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("Null ");
		return;
	}
	BinaryTreeInOrder(root->left);
	printf("%d ", root->data);
	BinaryTreeInOrder(root->right);
}
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("Null ");
		return;
	}	
	BinaryTreePostOrder(root->left);
	BinaryTreePostOrder(root->right);
	printf("%d ", root->data);
}
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root)
{
	//大致思路就是把非空结点入队出队时把他的孩子全部入队
	//直到队列没有树的结点
	Queue queue;
	QueueInit(&queue);
	if (root == NULL)
		return;
		QueuePush(&queue, root);
	while (!QueueEmpty(&queue))
	{
		BTNode* top = QueueFront(&queue);
		printf("%c ", top->data);
		QueuePop(&queue);
		if(top->left)
		QueuePush(&queue, top->left);
		if(top->right)
		QueuePush(&queue, top->right);
	}
	QueueDestroy(&queue);
}

在层序遍历这里我们需要用到队列把非空节点入队出队时让该节点的孩子节点入队知道队列为空

这里我们要知道队列里面的元素类型是指向树节点的指针。

4.3 二叉树的节点数以及高度

// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
	
	return root == NULL?0: BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	if (root->left == NULL && root->right == NULL)
	{
		return 1;
	}
	return BinaryTreeLeafSize(root->left)+BinaryTreeLeafSize(root->right);
}
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
	if (root == NULL)
	{
		return 0;
	}
	if (k == 1 || k == 0)
	{
		return k;
	}
	 return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}

4.4 二叉树的节点查找

// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
	{
		return NULL;
	}
	if (root->data == x)
	{
		return root;
	}
	     BTNode* ret = BinaryTreeFind(root->left, x);
		 if (ret)
		 {
			 return ret;
		 }
	return BinaryTreeFind(root->right, x);
}

4.5 判断是否为完全二叉树

 //判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root == NULL)
	{
		return false;
	}
	QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front == NULL)
		{
			break;
		}
		QueuePush(&q, front->left);
		QueuePush(&q, front->right);
	}
	while (!QueueEmpty(&q))
	{
		BTNode* n1 = QueueFront(&q);
		QueuePop(&q);
		if (n1 != NULL)
		{
			return false;
		}
	}
	QueueDestroy(&q);
	return true;


}

这里的思路就是把空树也入队列当遇到第一个空节点停止让后便利该队列看看是否有非空节点

如果没有就是完全二叉树有一个就不是完全二叉树。当我们遇到第一个空节点时,说明前面的非空节点已经出队列了,也就是前面非空节点的孩子都入队了所以一定可以判断出是否为完全二叉树。

4.6 二叉树的销毁

// 二叉树销毁                                                                                                                                                                        rrse
void BinaryTreeDestory(BTNode** root)
{
	//*root是指向树节点的一个指针
	if ((*root) == NULL)
	{
		return;
	}
	BinaryTreeDestory(&((*root)->left));
	BinaryTreeDestory(&((*root)->right));
	free(*root);
	*root = NULL;
}

二叉树的销毁这里我们传入的是二级指针因为我们要改变根节点指针的指向,如果你传入一级指针就要在调用该函数后在手动置为NULL;这里我们用的是后续删除,如果用前序我们要先保存左右字数节点的指针在删掉根节点,后续删除显然更简单。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值