Bootstrap

数据结构:二叉树

一、树概念及结构

1、树的概念

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

简单来说,就是形如下面这样的:

89b0a0f4c637488e95cf3e8a969f874c.png

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

d6e2e999b9b6446ea298f0b7ccca701f.png

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)棵互不相交的树的集合称为森林;

 3、树的表示

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

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

86e2a51c89e54a90b6310971e987358f.png

4、树在实际中的运用

在表示文件系统时,我们用的就是树形结构:

9ef74d830270419b9cfeb7b37deb35ef.png

二、二叉树概念及结构

1、二叉树的概念及结构

二叉树是一种特殊的树,它的每个父亲节点最多只有两个孩子,例如:

c2d4e000316b4135a5868ae0a83a879f.png

 

任意一棵二叉树都是由以下几种情况复合而成的:

 084dc085c1f34781b19b6aab5fb227dd.png

2、完全二叉树与满二叉树

  • 满二叉树的最后一层的所有节点均没有孩子,除最后一层外的每一层的每个节点均有两个孩子。
  • 完全二叉树是由满二叉树而引出来的,完全二叉树的最后一层节点是连续的,除最后一层和倒数第二层以外的每个节点均有两个孩子。

3、二叉树的性质

1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有eq?2%5E%7Bi-1%7D 个结点.

2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是eq?2%5Eh-1

3. 对任何一棵二叉树, 如果度为0其叶结点个数为eq?n_%7B0%7D, 度为2的分支结点个数为 ,则有eq?n_%7B0%7D%3Dn_%7B2%7D&plus;1

4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度,eq?h%3D%5Clog_%7B2%7D%28n&plus;1%29

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. 某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为()
A 不存在这样的二叉树
B 200
C 198
D 199
 
2.下列数据结构中,不适合采用顺序存储结构的是()
A 非完全二叉树
B 堆
C 队列
D 栈
 
3.在具有 2n 个结点的完全二叉树中,叶子结点个数为()
A n
B n+1
C n-1
D n/2
 
4.一棵完全二叉树的节点数位为531个,那么这棵树的高度为()
A 11
B 10
C 8
D 12
 
5.一个具有767个节点的完全二叉树,其叶子节点个数为()
A 383
B 384
C 385
D 386
 
答案:
1.B
2.A
3.A
4.B
5.B
 

4、二叉树的存储结构

1. 顺序存储

顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,关于堆我们后面的章节会专门讲解。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树

2c95e3fcbba24a48ad0d5937f81b60c5.png

2. 链式存储

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

96e92c8ba80c407aa1eab1e8268c246f.png

typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
	struct BinTreeNode* _pLeft; // 指向当前节点左孩子
	struct BinTreeNode* _pRight; // 指向当前节点右孩子
	BTDataType _data; // 当前节点值域
}
// 三叉链
struct BinaryTreeNode
{
	struct BinTreeNode* _pParent; // 指向当前节点的双亲
	struct BinTreeNode* _pLeft; // 指向当前节点左孩子
	struct BinTreeNode* _pRight; // 指向当前节点右孩子
	BTDataType _data; // 当前节点值域
};

三、二叉树的顺序结构及实现

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储。

1、堆的概念及结构

堆是一种特殊的完全二叉树,它分为大根堆和小根堆,简称大堆和小堆。

  • 大根堆:根节点最大,且每个节点的孩子都比自己小。
  • 小根堆:根节点最小,且每个节点的孩子都比自己大。

1.下列关键字序列为堆的是:()

A 100,60,70,50,32,65

B 60,70,65,50,32,100

C 65,100,70,32,50,60

D 70,65,100,32,50,60

E 32,50,100,70,65,60

F 50,100,70,65,60,32

2.已知小根堆为8,15,10,21,34,16,12,删除关键字 8 之后需重建堆,在此过程中,关键字之间的比较次数是()。

A 1

B 2

C 3

D 4

3.一组记录排序码为(5 11 7 2 3 17),则利用堆排序方法建立的初始堆为

A(11 5 7 2 3 17)

B(11 5 7 2 17 3)

C(17 11 7 2 3 5)

D(17 11 7 5 3 2)

E(17 7 11 3 5 2)

F(17 7 11 3 2 5)

4.最小堆[0,3,2,5,7,4,6,8],在删除堆顶元素0之后,其结果是()

A[3,2,5,7,4,6,8]

B[2,3,5,7,4,6,8]

C[2,3,4,5,7,8,6]

D[2,3,4,5,6,7,8]

  1. A
  2. C
  3. C
  4. C

2、堆的实现

1.堆的插入

堆的插入采用的是向上调整法,即:

首先直接在数组末尾插入数据,然后比较其与父亲节点的大小,若比父亲小(假设为小堆)则与父亲交换,然后再与新父亲节点比较,以此类推,直到比父亲大,或者变成根节点为止。

 

d23e6f98a1884ee087711c356c4e2d3c.png

大堆则把上述的小改为大即可。

2.堆的删除

删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法,即:

先将根节点和最后一个节点交换,然后删除最后一个节点,再比较根节点与左右孩子的大小,若存在孩子比它小(假设为小堆),则与最小的孩子交换,再比较它与新孩子的大小,直到两个孩子都比它大,或者为空为止。

89a1bda811514fb7ab2807f400d770c4.png

3.堆的创建

有了前面的知识,堆的创建就很简单了,只要不断插入数据即可,这里我们重点讨论一下建堆的时间复杂度。

cb8a1f63cf5948e0b3ba115c32ba0d5a.png

因此,建堆的时间复杂度为O(N)。

4.堆的代码实现

//Heap.h
#include<stdio.h>
#include<malloc.h>
#include<assert.h>
#include<stdbool.h>
typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}Heap;

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);
// 堆的判空
bool HeapEmpty(Heap* hp);
//Heap.c
#include"Heap.h"

void HeapInit(Heap* hp)
{
	hp->a = NULL;
	hp->capacity = 0;
	hp->size = 0;
}
// 堆的销毁
void HeapDestory(Heap* hp)
{
	free(hp->a);
	free(hp);
	hp = NULL;
}
// 堆的插入
void HeapPush(Heap* hp, HPDataType x)
{
	//扩容
	if (hp->size == hp->capacity)
	{
		int newcapacity = hp->size == 0 ? 4 : hp->capacity * 2;
		int* tmp = (int*)realloc(hp->a,newcapacity * sizeof(HPDataType));
		if (tmp == NULL)
		{
			perror("realloc error");
		}
		hp->a = tmp;
		hp->capacity = newcapacity;
	}
	//插入数据
	hp->a[hp->size] = x;
	//向上调整
	int child = hp->size;
	int parent = (child - 1) / 2;
	while (parent >= 0)
	{
		if (hp->a[child] > hp->a[parent])
		{
			HPDataType tmp = hp->a[child];
			hp->a[child] = hp->a[parent];
			hp->a[parent] = tmp;
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
	hp->size++;
}
// 堆的删除
void HeapPop(Heap* hp)
{
	assert(hp);
	//交换堆顶和堆底
	HPDataType tmp = hp->a[0];
	hp->a[0] = hp->a[hp->size - 1];
	hp->a[hp->size - 1] = tmp;
	hp->size--;
	//向下调整
	int parent = 0;
	int maxchild = parent * 2 + 1;
	while (maxchild <= hp->size - 1)
	{
		if (maxchild == hp->size - 1)
			;
		else if (hp->a[maxchild] < hp->a[maxchild + 1])
		{
			maxchild++;
		}
		if (hp->a[parent] < hp->a[maxchild])
		{
			HPDataType tmp = hp->a[parent];
			hp->a[parent] = hp->a[maxchild];
			hp->a[maxchild] = tmp;
			parent = maxchild;
			maxchild = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
// 取堆顶的数据
HPDataType HeapTop(Heap* hp)
{
	assert(hp);
	return hp->a[0];
}
// 堆的数据个数
int HeapSize(Heap* hp)
{
	return hp->size;
}
// 堆的判空
bool HeapEmpty(Heap* hp)
{
	return hp->size == 0;
}

3、堆的应用

1.堆排序

堆排序即利用堆的思想来进行排序,总共分为两个步骤:

1. 建堆

升序:建大堆

降序:建小堆

2. 利用堆删除思想来进行排序

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

4aa0ecc56ccf4546b8952095e56298dd.png

 堆排序的代码实现:

#include<stdio.h>
#define HeapType int

//向下调整
void Adjustdown(HeapType* a, int pos, int num)
{
	while (pos * 2 + 1 <= num-1)
	{
		int maxchild = pos * 2 + 1;
		if (maxchild == num-1)
			;
		else if (a[maxchild] < a[maxchild + 1])
			maxchild++;
		if (a[pos] < a[maxchild])
		{
			HeapType tmp = a[pos];
			a[pos] = a[maxchild];
			a[maxchild] = tmp;
			pos = maxchild;
		}
		else
			break;
	}
}



void Heapsort(HeapType* a, int num)
{
	//建堆
	for (int i = (num - 2) / 2; i >= 0; i--)
	{
		Adjustdown(a, i, num);
	}
	//排序
	for (int i = num - 1; i >= 0; i--)//6
	{
		HeapType tmp = a[i];
		a[i] = a[0];
		a[0] = tmp;
		Adjustdown(a, 0, i);
	}
}

2.TopK问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。

比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

最佳的方式就是用堆来解决,基本思路如下:

1. 用数据集合中前K个元素来建堆

前k个最大的元素,则建小堆

前k个最小的元素,则建大堆

2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

TopK问题的代码实现:

void AdjustDown(int* a, int pos, int size)
{
	while (pos * 2 + 1 <= size - 1)
	{
		int minchild = pos * 2 + 1;
		if (minchild == size - 1)
			;
		else if (a[minchild] > a[minchild + 1])
			minchild++;
		if (a[pos] > a[minchild])
		{
			int tmp = a[pos];
			a[pos] = a[minchild];
			a[minchild] = tmp;
			pos = minchild;
		}
		else
			break;
	}
}

void topk(int k)
{

	//建堆
	int* a = (int*)malloc(k * sizeof(int));
	if (a == NULL)
	{
		perror("malloc error");
		return;
	}
	for (int i = 0; i < k; i++)
	{
		fscanf(pf, "%d", a + i);

	}
	for (int i = (k - 2) / 2; i >= 0; i--)
	{
		AdjustDown(a, i, k);
	}
	//遍历
	for (int i = k; i < 10000; i++)
	{
		int tmp = 0;
		fscanf(pf, "%d", &tmp);
		if (tmp > a[0])
		{
			a[0] = tmp;
			AdjustDown(a, 0, k);
		}
	}
	for (int i = 0; i < k; i++)
	{
		printf("%d ", a[i]);
	}
}

四、二叉树链式结构的实现

1、二叉树的遍历

1.前序遍历

前序遍历(先序遍历),即先访问根节点,再访问左子树,再访问右子树。

前序遍历代码:

void BinaryTreePrevOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("# ");
		return;
	}
	else
	{
		printf("%c ", root->val);
		BinaryTreePrevOrder(root->left);
		BinaryTreePrevOrder(root->right);
	}
}

2.中序遍历

中序遍历,即先访问左子树,再访问根节点,再访问右子树。

中序遍历代码:

void BinaryTreeInOrder(BTNode* root)
{
	if (root == NULL)
		return;
	else
	{
		BinaryTreeInOrder(root->left);
		printf("%d ", root->val);
		BinaryTreeInOrder(root->right);
	}
}

3.后序遍历

后序遍历,即先访问左右子树,再访问根节点。

后续遍历的代码:

void BinaryTreePostOrder(BTNode* root)
{
	if (root == NULL)
		return;
	else
	{
		BinaryTreePostOrder(root->left);
		BinaryTreePostOrder(root->right);
		printf("%d ", root->val);
	}
}

4.层序遍历

 首先访问第一层的树根节点,然后从左到右访问第2层 上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。  

层序遍历思路:

利用队列,先将根节点入队列,然后出队列,并将出队列的节点的左右孩子分别入队列,出队列的节点即为遍历到的数据。以此类推,直到队列为空。

层序遍历代码实现:

void BinaryTreeLevelOrder(BTNode* root)
{
	Queue* pq = (Queue*)malloc(sizeof(Queue));
	if (pq == NULL)
	{
		perror("malloc error");
	}
	Initqueue(pq);
	Pushqueue(pq, root);
	while (QueueEmpty(pq)) 
	{
		BTNode* tmp = Topqueue(pq);
		Popqueue(pq);
		if (tmp != NULL)
		{
			Pushqueue(pq, tmp->left);
			Pushqueue(pq, tmp->right);
			printf("%c ", tmp->val);
		}
		else
			printf("# ");
	}
}

2、练习

选择:

1.某完全二叉树按层次输出(同一层从左到右)的序列为 ABCDEFGH 。该完全二叉树的前序序列为)
A ABDHECFG
B ABCDEFGH
C HDBEAFCG
D HDEBFGCA
 
2.二叉树的先序遍历和中序遍历如下:先序遍历:EFHIGJK;中序遍历:HFIEJKG.则二叉树根结点为()
A E
B F
C G
D H
 
3.设一课二叉树的中序遍历序列:badce,后序遍历序列:bdeca,则二叉树前序遍历序列为
A adbce
B decab
C debac
D abcde
 
4.某二叉树的后序遍历序列与中序遍历序列相同,均为 ABCDEF ,则按层次输出(同一层从左到右)的序列
 
A FEDCBA
B CBAFED
C DEFCBA
D ABCDEF
 
答案:
1.A
2.A
3.D
4.A
 
尝试实现下列函数:
// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, DataType x);

代码实现:

// 二叉树节点个数
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)
		return 1;
	return  BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, DataType x)
{
	if (root == NULL)
		return NULL;
	if (root->val == x)
		return root;
	BTNode* tmp = BinaryTreeFind(root->left, x);
	if (tmp != NULL)
		return tmp;
	return BinaryTreeFind(root->right, x);
}

二叉树基础oj练习:

单值二叉树

相同的树

对称二叉树

二叉树的前序遍历

二叉树的中序遍历

二叉树的后序遍历

另一棵树的子树

3、二叉树的创建和销毁

二叉树的构建与遍历

// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi);
// 二叉树销毁
void BinaryTreeDestory(BTNode** root);
// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root);

代码实现:

// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(DataType* a, int n, int* pi)
{
	BTNode* root = NULL;
	if (*pi < n)
	{
		if (a[*pi] == '#')
		{
			(*pi)++;
			return NULL;
		}
		root = CreatNode(a[*pi]);
		(*pi)++;
		root->left = BinaryTreeCreate(a, n, pi);
		root->right = BinaryTreeCreate(a, n, pi);
	}
	return root;
}


// 二叉树销毁
void BinaryTreeDestory(BTNode** root)
{
	if (*root == NULL)
		return;
	BinaryTreeDestory(&(*root)->left);
	BinaryTreeDestory(&(*root)->right);
	free(*root);
}
// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root)
{
	int flag = 0;
	Queue* pq = (Queue*)malloc(sizeof(Queue));
	if (pq == NULL)
	{
		perror("malloc error");
	}
	Initqueue(pq);
	Pushqueue(pq, root);
	while (QueueEmpty(pq))
	{
		BTNode* tmp = Topqueue(pq);
		Popqueue(pq);
		if (tmp == NULL)
			break;
		Pushqueue(pq, tmp->left);
		Pushqueue(pq, tmp->right);
	}
	while (QueueEmpty(pq))
	{
		BTNode* tmp = Topqueue(pq);
		if (tmp != NULL)
			return 0;
		Popqueue(pq);
	}
	return 1;
}

 

 

;