树:n(n>=0)个节点的有限集合,是一种逻辑结构,当n=0时为空树,且非空树满足:
树是一种递归的数据结构
非空树特点:
属性:
有序树和无序树
选用有序树还是无序树具体看你用树存什么,是否需要用结点的左右位置反映某些逻辑关系
森林是m(>=0)棵互不相交的树的集合。
m可为0——空森林
结点的度——结点有几个孩子(分支),最后再加上根结点
常见考点2:度为m的树、m叉树 的区别
树的度——各结点的度的最大值,m叉树——每个结点最多只能有m个孩子的树
常见考点3:度为m的树第 i 层至多有 mi-1 个结点(i≥1) m叉树第 i 层至多有 mi-1 个结点(i≥1)
等比数列求和公式:
高度最小的情况——所有结点都有m个孩子
二叉树是n (n>=0)个结点的有限集合
特点:
注意区别:度 为2的有序树(至少有一个结点度为2)
二叉树的五种状态
一棵深度为k且有个结点的二叉树称为满二叉树。特点:
深度为k的具有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号为1~n的结点一一对应时,称之为完全二叉树。特点:
一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:
二叉排序树可用于元 素的排序、搜索
树上任一结点的左子树和右子树的深度之差不超过1。
平衡二叉树能有 更高的搜索效率
常见考点1:设非空二叉树中度为0、1和2的结点个数分别为、,和,则(叶子结点比二分支结点多一个)
常见考点2:二叉树第层至多有个结点 (>=1);m叉树第层至多有个结点 (>=1)
常见考点3:高度为h的二叉树至多有个结点(满二叉树);高度为h的m叉树至多结点
常见考点2:对于完全二叉树,可以由总结点数 n 推出度为 0、1 和 2 的结点个数、、
推导过程:
因为::所以为奇数
又因为:
所以:若完全二叉树有偶数n个节点,则为1;为;为
若完全二叉树有奇数n个节点,则为0;为;为
二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来;
可以让第一个位置空缺,保 证数组下标和结点编号一致
定义一个长度为 MaxSize 的数组 t ,按照从上至下、从左至右的顺序依次存储完全二叉树中的各个结点,代码实现如下
#define MaxSize 100
struct TreeNode{
ElemType value; //结点中的数据元素
bool isEmpty; //结点是否为空
}
// 初始化时所有结点标记为空
void InItTree(TreeNode t[])
{
for (int i=0; i
几个重要常考的基本操作:
若完全二叉树中共有n个结点,则
注:如果不是完全二叉树,依然按层序将各节点顺序存储,那么无法从结点编号反映 出结点间的逻辑关系
二叉树的顺序存储中,一定要把二叉 树的结点编号与完全二叉树对应起来
最坏情况:高度为 h 且只有 h 个结点的单 支树(所有结点只有右孩子),也至少需 要 2h-1 个存储单元
结论:二叉树的顺序存储结构,只适合存 储完全二叉树
包含数据域和左右孩子指针,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;
二又树的递归特性:
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& vec) {
if (cur == NULL) return;
vec.push_back(cur->val); // 中
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
}
vector preorderTraversal(TreeNode* root) {
vector result;
traversal(root, result);
return result;
}
};
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); //递归遍历右子树
}
}
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); //访问根结点
}
}
基本思路:将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记,只有空节点弹出的时候,才将下一个节点放进结果集。如何标记呢,就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。
迭代法中序遍历
vector inorderTraversal(TreeNode* root) {
vector result;
stack 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 preorderTraversal(TreeNode* root) {
vector result;
stack 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 postorderTraversal(TreeNode* root) {
vector result;
stack 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;
}
};
算法思想:
//二叉树的结点(链式存储)
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> levelOrder(TreeNode* root) {
queue que;
if (root != NULL) que.push(root);
vector> result;
while (!que.empty()) {
int size = que.size();
vector 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>& result, int depth)
{
if (cur == nullptr) return;
if (result.size() == depth) result.push_back(vector());
result[depth].push_back(cur->val);
order(cur->left, result, depth + 1);
order(cur->right, result, depth + 1);
}
vector> levelOrder(TreeNode* root) {
vector> result;
int depth = 0;
order(root, result, depth);
return result;
}
结论:若只给出一棵二叉树的 前/中/后/层序遍历序列 中的一种,不能唯一确定一棵二叉树。
由二叉树的遍历序列构造二叉树:
由 前序+中序遍历序列 构造二叉树:由前序遍历的遍历顺序(根节点、左子树、右子树)可推出根节点,由根节点在中序遍历序列中的位置即可推出左子树与右子树分别有哪些结点。
由 后序+中序遍历序列 构造二叉树:由后序遍历的遍历顺序(左子树、右子树、根节点)可推出根节点,由根节点在中序遍历序列中的位置即可推出左子树与右子树分别有哪些结点。
由 层序+中序遍历序列 构造二叉树:由层序遍历的遍历顺序(层级遍历)可推出根节点,由根节点在中序遍历序列中的位置即可推出左子树与右子树分别有哪些结点。
前序、后序、层序 序列的两两组合无法唯一 确定一科二叉树