【数据结构】二叉树


1.树

1.1 树的概念与结构

树是一种非线性的数据结构,它是由n(n>0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来很像一棵倒挂的树,也就是说它是根朝上,叶朝下的。

  • 有一个特殊的结点,称为根结点,根结点没有前驱结点。
  • 除根结点外,其余结点被分为M(M>0)个互不相交的集合T1、T2、.....、Tm,其中每一个集合又是一棵结构与树类似的子树。每棵子树的根节点有且只有一个前驱,可以有0个或多个后继。因此树是递归的定义的。

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

非树形结构:

  • 子树是不相交的,如果存在相交就是图了。
  • 除了根结点外,每个结点有且只有一个父结点
  • 一棵N个结点的树有N-1条边

1.2 树相关术语

父结点/双亲结点:若一个结点含有子结点,则这个结点称为其子结点的父结点;如图,A是B的父结点。

子结点/孩子结点:一个结点含有的子树的根节点称为该结点的子结点;如上图,B是A的孩子结点。

结点的度:一个结点有几个孩子,他的度就是多少,比如A的度为6,B的度为0,F的度为2.

树的度:一棵树中,最大的结点的度称为树的度;如上图,树的度为6.

叶子结点/终端结点:度为0的结点称为叶结点,如上图:B、C、H、I...等结点为叶节点。

分支结点/非终端结点:度不为 0 的结点; 如上图: DEFG... 等结点为分支结点。

兄弟结点:具有相同父结点的结点互称为兄弟结点(亲兄弟); 如上图: BC 是兄弟结点

结点的层次:从根开始定义起,根为第 1 层,根的子结点为第 2 层,以此类推;

树的高度或深度:树中结点的最大层次; 如上图:树的高度为 4。

结点的祖先:从根到该结点所经分⽀上的所有结点;如上图: A 是所有结点的祖先。

路径:⼀条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列;比如A到Q的路径为: A-E-J-Q;H到Q的路径H-D-A-E-J-Q。

子孙:以某结点为根的子树中任⼀结点都称为该结点的子孙。如上图:所有结点都是A的子孙。

森林:mm>0) 棵互不相交的树的集合称为森林。

1.3 树的表示

孩子兄弟表示法:

树结构相对于线性表复杂了很多。这里有一种孩子兄弟表示法。

struct TreeNode
{
    struct Node* child;//左边开始的第一个孩子结点
    struct Node* brother;//指向其右边的下一个兄弟结点
    int data;//节点中的数据
};

2.二叉树

2.1 概念与结构

在树形结构中最常用的就是二叉树,一棵二叉树是结点的一个有限集合,该集合由一个根结点加上两棵分别称为左子树和右子树的二叉树组成或者为空。

二叉树具备以下特点:

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

注意:对于任意的二叉树都是由以下的几种情况复合成的:

2.2 特殊的二叉树

2.2.1 满二叉树

一个二叉树,如果每一层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为k,且结点总数是2^k - 1,则它就是满二叉树。

2.2.2 完全二叉树

完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。假设有一棵二叉树有k层,如果它的前k-1层都排满,第k层未排满,且第k层是按照从左到右的顺序排的,就是完全二叉树。特别地,当第k层也排满了,则为满二叉树,满二叉树也是完全二叉树的一种。满二叉树一定是完全二叉树,而完全二叉树不一定是满二叉树。

二叉树性质:

根据满二叉树的特点可知:

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

2)若规定根结点的层数为1,则深度为h的二叉树的最大结点是2^h-1

3)若规定根结点的层数为1,具有n个结点的满二叉树的深度h = log2(n+1)。(log以2为底,n+1为对数) 

2.3 二叉树存储结构

二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。

2.3.1 顺序结构

顺序结构就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会一空间的浪费,完全二叉树更适合使用顺序结构存储。

现实中通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

2.3.2 链式结构

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

3.实现顺序结构二叉树

一般堆使用顺序结构的数组来存储数据,堆是一种特殊的二叉树,具有二叉树的特性的同时,还具备其他的特性。

3.1 堆的概念与结构

根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。

 

堆具有以下性质

  • 堆中某个结点的值总是不大于或不小于其父结点的值;
  • 堆总是一棵完全二叉树。

二叉树性质:

  • 对于具有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,否则无右孩子 

3.2 堆的实现

堆底层结构为数组,因此定义堆的结构为:

typedef int HPDataType;

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

//初始化堆
void HPInit(HP* php);

//用给定数组初始化堆
void HPInitArray(HP* php,HPDataType* a,int n);

//堆的销毁
void HPDestroy(HP* php);

//堆的插入
void HPPush(HP* php,HPDataType x);

//堆的删除
HPDataType HPTop(HP* php);

//删除堆顶的数据
void HPTop(HP* php);

//判空
bool HPEmpty(HP* php);

//求size
int HPSize(HP* php);

//向上调整算法
void AdjustUp(HPDataType* a,int child);

//向下调整算法
void AdjustDown(HPDataType* a,int n ,int parent);

3.2.1 向上调整算法

向上调整算法:

  • 先将元素插入到堆的末尾,即最后一个孩子之后
  • 插入之后如果堆的性质遭到破坏,将新插入的结点顺着其双亲往上调整到合适的位置即可
void AdjustUp(HPDataType* arr, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
        //建大堆,>
        //建小堆,<
		if (arr[child] < arr[parent])
		{
			Swap(&arr[parent], &arr[child]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

用于: 

1.堆的插入:将新数据插入到数组的尾上,再进行向上调整算法,直到满足堆;

2.堆排序中向上调整算法建堆,排升序建大堆;排降序建小堆。 决定建的是大堆or小堆只需改变if条件中的大于or小于号。

3.向上调整算法建堆的时间复杂度:O(n*log2n).    log以2为底n的对数

3.2.2 向下调整算法

向下调整算法:

  • 将堆顶元素与最后一个元素交换
  • 删除堆中最后一个元素
  • 将堆顶元素向下调整到满足堆特性为止
void Adjustdown(HPDataType* arr, int parent, int n)//n是结点个数
{
	int child = parent * 2 + 1;//左孩子
	while (child < n)
	{
		if (child + 1 < n && arr[child + 1] < arr[child])
		{
			child++;
		}
		if (arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

用于: 

1.堆的删除:删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据交换,然后删除数组最后一个数据,再将换到堆顶的数据进行数据的向下调整;

2.在堆排序中向下调整算法建堆,前提是保证左右子树都满足需要建的堆的特性。(比如需要建的是大堆必须保证左右子树都已经是大堆)

3.向下调整算法建堆的时间复杂度:O(n),所以在建堆时推荐使用向下调整算法建堆。

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

3.3 堆的应用

3.3.1 堆排序

用向上/向下调整算法建堆排序:

1.向上调整算法建堆排序:

a.排升序--建大堆(大堆小堆取决于if条件中是>还是<)

b.排降序--建小堆(大堆小堆取决于if条件中是>还是<)

2.向下调整算法建堆排序:
先从最后一个元素开始向上找到其父节点,然后用向下调整算法前先保证没棵子树都已经满足需要建的堆的特性,再从根节点向下遍历。(小堆大堆也是改两个if中的条件)

3.3.2 TOP-K问题

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

思路:
1.若求前k个最大的元素,则建小堆;若求前k个最小的元素,则建大堆

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

时间复杂度:O(n) = k + (n-k)log 2 k(log以2为底k的对数)

4.实现链式结构二叉树

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

4.1 前中后序遍历

4.1.1 遍历规则

按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:

1)前序遍历:访问根结点的操作发生在遍历其左右子树之前,访问顺序为:根结点,左子树,右子树。简记为:根左右。

2)中序遍历:访问根结点的操作发生在遍历其左右子树之间,访问顺序为:左子树,根结点,右子树。简记为:左根右。

3)后序遍历:访问根结点的操作发生在遍历其左右子树之后,访问顺序为:左子树,右子树,根结点。

4.1.2 代码实现


//前序遍历 根左右
void PreOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	printf("%d ", root->data);
	PreOrder(root->left);
	PreOrder(root->right);
}

//中序遍历 左根右
void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	InOrder(root->left);
	printf("%d ", root->left);
	InOrder(root->right);
}

//后序遍历 左右根
void PostOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	PostOrder(root->left);
	PostOrder(root->right);
	printf("%d ", root->data);
}

4.2 结点个数以及高度等

1.二叉树的结点个数

//二叉树的结点个数 
//结点个数=1(根结点)+左子树结点个数+右子树结点个数
int BTNodeSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	return 1 + BTNodeSize(root->left) + BTNodeSize(root->right);
}

2.二叉树的叶子结点个数

//二叉树的叶子结点个数
//左子树叶子结点 + 右子树叶子结点
//叶子结点:度为0的结点(无左右孩子)
int BTTreeLeafSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	if (root->left == NULL && root->right == NULL)
	{
		return 1;
	}
	return BTTreeLeafSize(root->left) + BTTreeLeafSize(root->right);
}

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

//二叉树第k层结点个数
// 左子树第k层结点个数+右子树第k层结点个数
//每次递归k--,作为找到层数的条件,当k=1时即递归到对应层
int BTTreeLevelKSize(BTNode* root, int k)
{
	if (root == NULL)
	{
		return;
	}
	if (k == 1)
	{
		return 1;
	}
	return BTTreeLevelKSize(root->left, k - 1) + BTTreeLevelKSize(root->right, k - 1);
}

4.二叉树的高度/深度

//二叉树的深度/高度
//1+左子树高度/右子树高度(哪边大取哪边)
int BinaryTreeHigh(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	int lefthigh = BinaryTreeHigh(root->left);
	int righthigh = BinaryTreeHigh(root->right);
	return lefthigh > righthigh ? 1 + lefthigh : 1 + righthigh;
}

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

//二叉树查找值为x的结点
//找根结点,如果根结点找到x直接返回
//再递归找左节点和右结点
BTNode* BinaryTreeFind(BTNode* root,BTDataType x)
{
	if (root == NULL)
	{
		return NULL;
	}
	if (root->data == x)
	{
		return root;
	}
	BTNode* leftfind = BinaryTreeFind(root->left,x);
	if (leftfind)
		return leftfind;
	BTNode* rightfind = BinaryTreeFind(root->right, x);
	if (rightfind)
		return rightfind;
	return NULL;
}

6.二叉树的销毁

//二叉树销毁
//销毁左子树+销毁右子树+销毁根结点
void BinaryTreeDestory(BTNode** root)
{
	if (*root == NULL)
	{
		return;
	}
	BinaryTreeDestory(&(*root)->left);
	BinaryTreeDestory(&(*root)->right);
	free(*root);
	*root = NULL;
}

4.3 层序遍历

除了先序遍历,中序遍历,后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根结点所在层数为1,层序遍历就是从所在二叉树的根结点出发,首先访问第一层的根结点,然后从左到右访问第二层上的结点,接着是第三层的结点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。

实现层序遍历需要额外借助数据结构:队列。

思路:创建一个队列数据结构并将其初始化后,将根结点入队列,只要队列中不为空,取队头打印出值,然后出队列,只要左右结点不为空就入队列。

代码实现:

//层序遍历:借助数据结构----队列
void LevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		//取队头,打印
		BTNode* front = QueueFront(&q);
		printf("%d ", front->data);
		QueuePop(&q);
		//队头节点的左右孩子入队列
		if (front->left)
			QueuePush(&q, front->left);
		if (front->right)
			QueuePush(&q, front->right);
	}
	//队列为空
	QueueDestroy(&q);
}

4.4 判断是否为完全二叉树

思路:借助队列的结构,首先将根结点入队列,只要队列不为空就出队列,根结点出队列后再将其左右子树结点入队列,只要队列不为空就出队列。

如果是完全二叉树,跳出一个循环之后(出完所有非空结点)队列中剩下的全是NULL结点;如果不是完全二叉树,跳出一个循环之后队列中还有非空结点。

代码实现:

//判断二叉树是否为完全二叉树:借助数据结构----队列
//如果是完全二叉树,跳出一个循环之后队列中剩下的全是NULL结点
//如果不是完全二叉树,跳出一个循环之后队列中还有非空结点
bool BinaryTreeComplete(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	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* front = QueueFront(&q);
		QueuePop(&q);
		if (front != NULL)
		{
			QueueDestroy(&q);
			return false;
		}
	}
	QueueDestroy(&q);
	return true;
}

5.二叉树算法题

5.1 单值二叉树

原题链接: 965. 单值二叉树 - 力扣(LeetCode)

思路:利用递归。

先确定根结点是否存在,如果根结点不存在直接返回true。在根结点存在的情况下,分别用左右子树与根结点的值比较看是否相同,如果不相同直接返回false作为递归结束的条件。

代码实现:

bool isUnivalTree(struct TreeNode* root)
{
    if(root == NULL)
    {
        return true;
    }
    if(root->left && root->left->val != root->val)
    {
        return false;
    }
    if(root->right && root->right->val != root->val)
    {
        return false;
    }
    return isUnivalTree(root->left) && isUnivalTree(root->right);
}

5.2 相同的树

原题链接:100. 相同的树 - 力扣(LeetCode)

思路:先不用比较值是否相同,首先要保证两棵树的结构是相同的,最后再看值是否相同。

如果两棵树都是空树,则为相同的树;如果其一为空,则返回false;代码如果未进入前两个循环,则说明两棵树都不为空,再去匹配值是否相同,值如果相同就返回true,反之返回false。

 代码实现: 

bool isSameTree(struct TreeNode* p, struct TreeNode* q)
{
    //两棵树都为空
    if(p == NULL && q == NULL)
    {
        return true;
    }
    //其一为空
    if(p == NULL || q == NULL)
    {
        return false;
    }
    //说明都不为空
    //这里if条件不能写成等于,否则无法递归后面的结点
    if(p->val != q->val)
    {
        return false;
    }
    return isSameTree(p->left,q->left) && isSameTree(p->right,q->right);
}

拓展:101. 对称二叉树 - 力扣(LeetCode)

思路:首先结构也要相同,确保了结构相同后再确定值是否相同。

代码实现: 

bool isSameTree(struct TreeNode* p, struct TreeNode* q)
{
    //两棵树都为空
    if(p == NULL && q == NULL)
    {
        return true;
    }
    //其一为空
    if(p == NULL || q == NULL)
    {
        return false;
    }
    //说明都不为空
    //这里if条件不能写成等于,否则无法递归后面的结点
    if(p->val != q->val)
    {
        return false;
    }
    return isSameTree(p->right,q->left) && isSameTree(p->left,q->right);
}

bool isSymmetric(struct TreeNode* root) 
{
    return isSameTree(root->right,root->left);
}

5.3 另一棵树的子树

原题链接:572. 另一棵树的子树 - 力扣(LeetCode)

思路:

  代码实现:

5.4 二叉树遍历

原题链接:144. 二叉树的前序遍历 - 力扣(LeetCode)

思路:

 代码实现:

5.5 二叉树的构建及遍历

原题链接:二叉树遍历_牛客题霸_牛客网

思路:

代码实现: 

6.二叉树选择题

二叉树性质:对任何一棵二叉树,如果度为0,其叶节点个数为n0,度为2的分支结点个数为n2,则有:n0 = n2 + 1。

1.在具有2n个结点的完全二叉树中,叶子结点个数为?

A.n      B.n+1      C.n-1      D.n/2

【解析】设此完全二叉树度为1的结点有n1个,度为2的结点有n2个,度为0的结点有n0个。

则有:n0 + n1 + n2 = 2n;

又因为二叉树性质n0 = n2 + 1,有:n2 = n0 - 1.

则1式可变为:2n0 + n1 - 1 = 2n

易知在完全二叉树中度为1的结点为0或1个。

当为0个时,2n0 - 1 = 2n,结点不可能有0.5个,则度为1的结点有1个

所以n1 = 1,2n0 = 2n。度为0的结点(即叶子结点)有n个。答案选A。

2.一棵完全二叉树的结点数为531个,那么这棵树的高度为?

A.11      B.10      C.8      D.12

【解析】由二叉树的性质:若二叉树结点为n,则其高度h = log2n(log以2为底n的对数)

由题意得结点数为531个,则二叉树高度h = log2 531

而2^9 = 512,则二叉树高度应该为10.选B

3.一个具有767个结点的完全二叉树,其叶子结点个数为?

A.383      B.384      C.385      D.386

【解析】与1为同类题

设此完全二叉树度为1的结点有n1个,度为2的结点有n2个,度为0的结点有n0个。

则有:n0 + n1 + n2 = 767;

又因为二叉树性质n0 = n2 + 1,有:n2 = n0 - 1.

则1式可变为:2n0 + n1 - 1 = 767

易知在完全二叉树中度为1的结点为0或1个。

当度为1的结点有1个时,2n0  = 767,得n0 = 383.5;不可能有0.5个结点;

所以度为1的结点为0个,2n0 = 768,得n0 = 384.

所以度为0即叶子结点的个数为384个,选B。

4.设一棵二叉树的中序遍历序列:badce,后序遍历序列:bdeca。则二叉树前序遍历序列为?

A.abcde      B.decab      C.debac      D.abcde

【解析】因为后序遍历最后遍历根结点,易知根结点为a。

而中序遍历的遍历顺序为左根右,a又为根结点,则可知b为a的左子树。

再看后序遍历,a已经是根结点,而b又是a左子树的根结点,则c为a的右子树的根结点。且c的左右子树分别为d和e。

所以根据描述画出对应的二叉树,所对应的前序遍历为:abcde。选A。

5.某二叉树的后序遍历序列与中序遍历序列相同,均为ABCDEF,则按层次输出(同一层次从左到右)的序列为?

A.FEDCBA      B.CBAFED      C.DEFCBA      D.ABCDEF

【解析】与4为同类题。根据后序遍历推出根结点为F。

又由中序遍历推出无右子树,左子树为E,E为左子树根结点,以此类推可得:E的左子树为D,D的左子树为C,C的左子树为B,B的左子树为A。依照以上的推理画出对应的二叉树并按层次遍历的序列为:FEDCBA。选A。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值