1.树的概念
1.1 树的基本定义
树:n(n>=0)个节点的有限集合,是一种逻辑结构,当n=0时为空树,且非空树满足:
- 有且仅有一个特定的称为根的节点.
- 当n>1时,其余结点可分为m (m >0) 个互不相交的有限集合
,其中每个集合本身又是一棵树,并且称为根结点的子树。
树是一种递归的数据结构
非空树特点:
- 有且仅有一个根节点
- 没有后继的结点称为“叶子结点”(或终端节点)
- 有后继的结点称为“分支结点” (或非终端结点)
- 除了根节点外,任何一个结点都有且仅有一个前驱
- 每个结点可以有0个或多个后继
1.2 基本术语
- 祖先结点:自己的之上都是祖先节点。
- 子孙结点:自己的之下都是子孙节点。
- 双亲结点 (父节点) :和自己相连的上一个就是父节点。
- 孩子结点:和自己相连的下面一个。
- 兄弟结点:我自己同一个父节点的。
- 堂兄弟结点:同一层的节点。
属性:
- 结点的层次(深度)--从上往下数,默认从 1开始
- 结点的高度-一从下往上数
- 树的高度 (深度)-一总共多少层
- 结点的度--有几个孩子(分支),叶子结点 的度=0
- 树的度一-各结点的度的最大值
- 两个结点之间的路径——从上往下经历的结点
- 结点间路径长度——经过的边的的条数
- 树的路径长度——树根到每个结点路径长度之和
有序树和无序树
- 有序树--逻辑上看,树中结点的各子树从左至右是有次序的,不能互换
- 无序树--逻辑上看,树中结点的各子树从左至右是无次序的,可以互换
选用有序树还是无序树具体看你用树存什么,是否需要用结点的左右位置反映某些逻辑关系
1.3.森林
森林是m(>=0)棵互不相交的树的集合。
m可为0——空森林
2. 树的常考性质
2.1 常见考点
- 常见考点1:结点数=总度数+1
结点的度——结点有几个孩子(分支),最后再加上根结点
-
常见考点2:度为m的树、m叉树 的区别
树的度——各结点的度的最大值,m叉树——每个结点最多只能有m个孩子的树
-
常见考点3:度为m的树第 i 层至多有
个结点(i≥1) m叉树第 i 层至多有
个结点(i≥1)
- 常见考点4: 高度为h的m叉树至多有
个结点。
等比数列求和公式:
- 常见考点5: 高度为h的m叉树至少有h个结点;高度为h、度为m的树至少有h+m-1个结点。
- 常见考点6: 具有n个结点的m叉树的最小高度为
(考点4反解)
高度最小的情况——所有结点都有m个孩子
2.2 树的形态
相似树和等价树
如果两棵树中各个结点的位置都相对应,可以说这两棵树相似;如果两棵树不仅相似,而且对应结点上的数据也相同,就可以说这两棵树等价
问题相当于当给定 N 个结点时,可以构建多少种形态不同的树(互不相似)
每一棵普通树都可以转化一棵没有右子树的二叉树(孩子兄弟表示法的转化后),所以对于N个结点的树来说,树的形态改变是因为除了根结点之外的其它结点改变形态得到的,所以,N个结点构建的形态不同的树与之对应的是N-1个结点构建的形态不同的二叉树。而n个结点的二叉树
如果 表示 N 个结点构建的形态不同的树的数量,
表示 N 个结点构建的形态不同的二叉树的数量,则两者之间有这样的关系:
3. 二叉树
3.1 二叉树的定义
3.1.1 定义和特点
二叉树是n (n>=0)个结点的有限集合
- 或者为空二叉树,即n =0。
- 或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树。
特点:
- 每个结点至多只有两棵子树
- 左右子树不能颠倒 (二叉树是有序树)
- 二叉树可以是空集合,根可以有空的左子树和空的右子树
注意区别:度 为2的有序树(至少有一个结点度为2)
二叉树的五种状态
3.1.2 Catalan数——n个节点能组成多少种二叉树
思想:递归+组合
当n=1时,只有1个根节点,则只能组成1种形态的二叉树,令n个节点可组成的二叉树数量表示为h(n), 则h(1)=1;
当n=2时,1个根节点固定,还有n-1个节点,可以作为左子树,也可以作为右子树, 即:h(2)=h(0)*h(1)+h(1)*h(0)=2,则能组成2种形态的二叉树。这里h(0)表示空,所以只能算一种形态,即h(0)=1;
当n=3时,1个根节点固定,还有n-1=2个节点,可以在左子树或右子树, 即:h(3)=h(0)*h(2)+h(1)*h(1)+h(2)*h(0)=5,则能组成5种形态的二叉树。
以此类推,当n>=2时,可组成的二叉树数量为h(n)=h(0)*h(n-1)+h(1)*h(n-2)+...+h(n-1)*h(0)种。
即符合Catalan数的定义,可直接利用通项公式得出结果。
递归式: h(n)=h(n-1)*(4*n-2)/(n+1);
该递推关系的解为:
3.1.3 其他使用Catalan数解决的问题
(1)矩阵连乘: P=a1×a2×a3×……×an,依据乘法结合律,不改变其顺序,只用括号表示成对的乘积,试问有几种括号化的方案?
(2)一个栈(无穷大)的进栈序列为1,2,3,..n,有多少个不同的出栈序列?
(3)有2n个人排成一行进入剧场。入场费5元。其中只有n个人有一张5元钞票,另外n人只有10元钞票,剧院无其它钞票,问有多少中方法使得只要有10元的人买票,售票处就有5元的钞票找零?(将持5元者到达视作将5元入栈,持10元者到达视作使栈中某5元出栈)
(4)将一个凸多边形区域分成三角形区域的方法数?
(5)在圆上选择2n个点,将这些点成对连接起来,使得所得到的n条线段不相交的方法数。
(6)一位大城市的律师在她住所以北n个街区和以东n个街区处工作。每天她走2n个街区去上班。如果她从不穿越(但可以碰到)从家到办公室的对角线,那么有多少条可能的道路?
3.2 特殊二叉树
3.2.1 满二叉树:
一棵深度为k且有个结点的二叉树称为满二叉树。特点:
- 每一层上的结点数都达到最大,不存在度为 1 的结点
- 叶子全部在最低层。
- 按层序从1开始编号,结点i的左孩子为 2i,右孩子为 2i+1;结点i的父节点为
(向下取整)
- i<=
为分支结点,i>
为叶子结点
3.2.2 完全二叉树:
深度为k的具有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号为1~n的结点一一对应时,称之为完全二叉树。特点:
- 只有最后两层可能有叶子结点,
- 最多只有一个度为1的结点(0个或1个),如果某结点只有一个孩子, 那么一定是左孩子
- 按层序从1开始编号,结点i的左孩子为 2i,右孩子为 2i+1;结点i的父节点为[i/2]
- i<=
为分支结点,i>
为叶子结点
3.2.3 二叉排序树:
一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:
- 左子树上所有结点的关键字均小于根结点的关键字;
- 右子树上所有结点的关键字均大于根结点的关键字;
- 左子树和右子树又各是一棵二叉排序树。
二叉排序树可用于元 素的排序、搜索
3.2.4 平衡二叉树:
树上任一结点的左子树和右子树的深度之差不超过1。
平衡二叉树能有 更高的搜索效率
3.3.5 严格二叉树
节点的度只有0和2两种。则高为h的严格二叉树最多有2h-1个结点。n个结点的严格二叉树 最大深度为(n+1)/2。
3.3 二叉树的性质
3.3.1 二叉树的常考性质
常见考点1:设非空二叉树中度为0、1和2的结点个数分别为、
,和
,则
(叶子结点比二分支结点多一个)
常见考点2:二叉树第层至多有
个结点 (
>=1);m叉树第
层至多有
个结点 (
>=1)
常见考点3:高度为h的二叉树至多有个结点(满二叉树);高度为h的m叉树至多
结点
3.3.2 完全二叉树的常考性质
常见考点1:(注意此处是完全二叉树才具备的)
常见考点2:对于完全二叉树,可以由总结点数 n 推出度为 0、1 和 2 的结点个数、
、
推导过程:
因为::所以
为奇数
又因为:
所以:若完全二叉树有偶数n个节点,则为1;
为
;
为
若完全二叉树有奇数n个节点,则为0;
为
;
为
3.4二叉树存储实现
3.4.1 二叉树的顺序存储:
二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来;
可以让第一个位置空缺,保 证数组下标和结点编号一致
定义一个长度为 MaxSize 的数组 t ,按照从上至下、从左至右的顺序依次存储完全二叉树中的各个结点,代码实现如下
#define MaxSize 100
struct TreeNode{
ElemType value; //结点中的数据元素
bool isEmpty; //结点是否为空
}
// 初始化时所有结点标记为空
void InItTree(TreeNode t[])
{
for (int i=0; i<MaxSize; i++){
t[i].isEmpty = true;
}
}
main(){
TreeNode t[MaxSize];
InItTree(t);
}
几个重要常考的基本操作:
- i 的左孩子:2i
- i 的右孩子:2i+1
- i 的父节点:i/2
- i 所在的层次:
或者
若完全二叉树中共有n个结点,则
- 判断i是否有左孩子?
?
- 判断i是否有右孩子?
?
- 判断i是否是叶子/分支结点?
?
注:如果不是完全二叉树,依然按层序将各节点顺序存储,那么无法从结点编号反映 出结点间的逻辑关系
二叉树的顺序存储中,一定要把二叉 树的结点编号与完全二叉树对应起来
最坏情况:高度为 h 且只有 h 个结点的单 支树(所有结点只有右孩子),也至少需 要 个存储单元
结论:二叉树的顺序存储结构,只适合存 储完全二叉树
3.4.2 二叉树的链式存储
包含数据域和左右孩子指针,n个结点的二叉链表共有 n+1 个空链域,可以用于构造 线索二叉树
//二叉树的结点
struct ElemType{
int value;
};
typedef struct BiTnode{
ElemType data; //数据域
struct BiTNode *lchild, *rchild; //左、右孩子指针
}BiTNode, *BiTree;
//定义一棵空树
BiTree root = NULL;
//插入根节点
root = (BiTree) malloc (sizeof(BiTNode));
root->data = {1};
root->lchild = NULL;
root->rchild = NULL;
//插入新结点
BiTNode *p = (BiTree) malloc (sizeof(BiTNode));
p->data = {2};
p->lchild = NULL;
p->rchild = NULL;
root->lchild = p; //作为根节点的左孩子
优点:找到指定结点 p 的左/右孩子——超简单
缺点:找指定结点 p 的 父结点只能从根开始遍历寻找
解决:三叉链表——方便 找父结点(根据实际需求决定要不要加父结点指针)
struct ElemType{
int value;
};
typedef struct BiTnode{
ElemType data; //数据域
struct BiTNode *lchild, *rchild; //左、右孩子指针
struct BiTNode *parent; // 父结点指针
}BiTNode, *BiTree;
4.二叉树的先中后序遍历
- 遍历:按照某种次序把所有结点都访问一遍。
- 层次遍历:基于树的层次特性确定的次序规则
二又树的递归特性:
- 要么是个空二叉树
- 要么就是由“根节点+左子树+右子树”组成的二叉树
4.1二叉树的先中后遍历
4.1.1 二叉树先中后序遍历的递归实现
- 先序遍历:根左右(NLR)
typedef struct BiTnode{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
void PreOrder(BiTree T){
if(T!=NULL){
visit(T); //访问根结点
PreOrder(T->lchild); //递归遍历左子树
PreOrder(T->rchild); //递归遍历右子树
}
}
C++实现
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
vec.push_back(cur->val); // 中
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
traversal(root, result);
return result;
}
};
- 中序遍历:左根右 (LNR)
typedef struct BiTnode{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
void InOrder(BiTree T){
if(T!=NULL){
InOrder(T->lchild); //递归遍历左子树
visit(T); //访问根结点
InOrder(T->rchild); //递归遍历右子树
}
}
- 后序遍历:左右根(LRN)
typedef struct BiTnode{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
void PostOrder(BiTree T){
if(T!=NULL){
PostOrder(T->lchild); //递归遍历左子树
PostOrder(T->rchild); //递归遍历右子树
visit(T); //访问根结点
}
}
4.1.2 用栈实现了二叉树先中后序的迭代遍历(非递归)。
基本思路:将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记,只有空节点弹出的时候,才将下一个节点放进结果集。如何标记呢,就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。
迭代法中序遍历
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root != NULL) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
if (node != NULL) {
st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
if (node->right) st.push(node->right); // 添加右节点(空节点不入栈)
st.push(node); // 添加中节点
st.push(NULL); // 中节点访问过,但是还没有处理,加入空节点做为标记。
if (node->left) st.push(node->left); // 添加左节点(空节点不入栈)
} else { // 只有遇到空节点的时候,才将下一个节点放进结果集
st.pop(); // 将空节点弹出
node = st.top(); // 重新取出栈中元素
st.pop();
result.push_back(node->val); // 加入到结果集
}
}
return result;
}
迭代法前序遍历 (注意此时我们和中序遍历相比仅仅改变了两行代码的顺序)
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root != NULL) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
if (node != NULL) {
st.pop();
if (node->right) st.push(node->right); // 右
if (node->left) st.push(node->left); // 左
st.push(node); // 中
st.push(NULL);
} else {
st.pop();
node = st.top();
st.pop();
result.push_back(node->val);
}
}
return result;
}
迭代法后序遍历
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root != NULL) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
if (node != NULL) {
st.pop();
st.push(node); // 中
st.push(NULL);
if (node->right) st.push(node->right); // 右
if (node->left) st.push(node->left); // 左
} else {
st.pop();
node = st.top();
st.pop();
result.push_back(node->val);
}
}
return result;
}
};
4.2 二叉树的层序遍历
算法思想:
- 1.初始化一个辅助队列
- 2.根结点入队
- 3.若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果有的话)
- 4.重复3直至队列为空
//二叉树的结点(链式存储)
typedef struct BiTnode{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
//链式队列结点
typedef struct LinkNode{
BiTNode *data; // 存结点指针而不是结点
typedef LinkNode *next;
}LinkNode;
typedef struct{
LinkNode *front, *rear;
}LinkQueue;
//层序遍历
void LevelOrder(BiTree T){
LinkQueue Q;
InitQueue (Q); //初始化辅助队列
BiTree p;
EnQueue(Q,T); //将根节点入队
while(!isEmpty(Q)){ //队列不空则循环
DeQueue(Q,p); //队头结点出队
visit(p); //访问出队结点
if(p->lchild != NULL)
EnQueue(Q,p->lchild); //左孩子入队
if(p->rchild != NULL)
EnQueue(Q,p->rchild); //右孩子入队
}
}
C++实现
队列法
vector<vector<int>> levelOrder(TreeNode* root) {
queue<TreeNode*> que;
if (root != NULL) que.push(root);
vector<vector<int>> result;
while (!que.empty()) {
int size = que.size();
vector<int> vec;
// 这里一定要使用固定大小size,不要使用que.size(),因为que.size是不断变化的
for (int i = 0; i < size; i++) {
TreeNode* node = que.front();
que.pop();
vec.push_back(node->val);
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
result.push_back(vec);
}
return result;
}
递归法
void order(TreeNode* cur, vector<vector<int>>& result, int depth)
{
if (cur == nullptr) return;
if (result.size() == depth) result.push_back(vector<int>());
result[depth].push_back(cur->val);
order(cur->left, result, depth + 1);
order(cur->right, result, depth + 1);
}
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> result;
int depth = 0;
order(root, result, depth);
return result;
}
4.3 由遍历序列构造二叉树
- 一个前序遍历序列可能对应多种二叉树形态。
- 同理,一个后序遍历序列、一个中序遍历序列、一个层序遍历序列也可能对应多种二叉树形态。
结论:若只给出一棵二叉树的 前/中/后/层序遍历序列 中的一种,不能唯一确定一棵二叉树。
由二叉树的遍历序列构造二叉树:
4.3.1 前序+中序遍历序列
由 前序+中序遍历序列 构造二叉树:由前序遍历的遍历顺序(根节点、左子树、右子树)可推出根节点,由根节点在中序遍历序列中的位置即可推出左子树与右子树分别有哪些结点。
4.3.2 后序+中序遍历序列
由 后序+中序遍历序列 构造二叉树:由后序遍历的遍历顺序(左子树、右子树、根节点)可推出根节点,由根节点在中序遍历序列中的位置即可推出左子树与右子树分别有哪些结点。
4.3.3 层序+中序遍历序列
由 层序+中序遍历序列 构造二叉树:由层序遍历的遍历顺序(层级遍历)可推出根节点,由根节点在中序遍历序列中的位置即可推出左子树与右子树分别有哪些结点。
前序、后序、层序 序列的两两组合无法唯一 确定一科二叉树