1. 树
1.1 简介
树是一种数据结构,它是n(n>=0)个节点的有限集。n=0时称为空树。n>0时,有限集的元素构成一个具有层次感的数据结构。
区别于线性表一对一的元素关系,树中的节点是一对多的关系。树具有以下特点:
1.n>0时,根节点是唯一的,不可能存在多个根节点。
2.每个节点有零个至多个子节点;除了根节点外,每个节点有且仅有一个父节点。根节点没有父节点。
1.2 树的相关概念
树有许多相关的术语与概念,在学习树的结构之前,我们要熟悉这些概念。
1.子树:除了根节点外,每个子节点都可以分为多个不相交的子树。
2.孩子(Child)与双亲(Parent):若一个结点有子树,那么该结点称为子树根的"双亲",子树的根是该结点的"孩子"。在图一中,B、H是A的孩子,A是B、H的双亲。
3.兄弟(Sibling):具有相同双亲的节点互为兄弟,例如B与H互为兄弟。
4.节点的度(Degree):一个节点拥有子树的数目。例如A的度为2,B的度为1,C的度为3.
5.叶子(Leaf):没有子树,也即是度为0的节点。
6.分支节点:除了叶子节点之外的节点,也即是度不为0的节点。
7.内部节点:除了根节点(Ancestor)之外的分支节点。
8.层次(Level):根节点为第一层,其余节点的层次等于其双亲节点的层次加1.
9.树的高度(Depth):也称为树的深度,树中节点的最大层次。
10.有序树:树中节点各子树之间的次序是重要的,不可以随意交换位置。
11.无序树:树种节点各子树之间的次序是不重要的。可以随意交换位置。
12.森林:0或多棵互不相交的树的集合。例如下图中的两棵树为森林。
2. 二叉树
二叉树或者为空集,或者由一个根节点和两棵互不相交的、分别称为左子树和右子树的二叉树组成。
从定义可以看出一棵二叉树:
1.二叉树是有序树,区分左子树与右子树,不可以随意交换子树位置。
2.一个节点的子树数量取值范围为0,1,2。0代表该节点是叶子节点,1代表该节点只有左子树或只有右子树,2代表该节点有左右子树。
2.1 满二叉树(完美二叉树)
满二叉树要满足两个条件:
- 所有的节点都同时具有左子树和右子树。
- 所有的叶子节点都在同一层上。
在同样深度的二叉树中,满二叉树的节点数目是最多的,叶子数也是最多的。
2.3 完全二叉树
在一棵二叉树中,只有最下两层的度可以小于2,并且最下一层的叶子节点集中出现在靠左的若干位置上。
或者这样定义:对一棵具有n个节点的二叉树按层序从左到右编序,二叉树树某个节点的编序与同样位置的满二叉树节点的编序相同如果所有节点都满足这个条件,则二叉树为完全二叉树。
从定义可以看出: 满二叉树一定是完全二叉树;完全二叉树不一定是满二叉树。
2.4 二叉查找树
二叉排序树也称为二叉搜索树或二叉排序树。二叉排序树的节点包含键值key。二叉排序树或者是一棵空树,否则要求:
- 若它的左子树不为空,那么左子树上所有节点的key都小于根节点的key
- 若它的右子树不为空,那么右子树上所有节点的key都大于根节点的key
它的左右子树也分别为二叉排序树,根据定义,二叉查找树中没有重复key的节点。
在实际的应用中,二叉排序树的应用比较多,我们后面要讲的AVL树本身也是一种二叉排序树。
2.5 重要性质
3. 二叉树抽象数据类型
3.1 主要操作
- 类型名称:二叉树
- 数据对象集:一个有穷的结点集合,若不为空,则由根结点和其左、右二叉子树组成
- 操作集:BT ∈ BinTree,Item ∈ ElementType
主要操作有:
Boolean IsEmpty(BinTree BT):判别 BT 是否为空
void Traversal(BinTree BT):遍历,按某顺序访问每个结点
BinTree CreatBinTree():创建一个二叉树
常用的遍历方法有:
void PreOrderTraversal(BinTree BT):先序——根、左子树、右子树
void InOrderTraversal(BinTree BT):中序——左子树、根、右子树
void PostOrderTraversal(BinTree BT):后序——左子树、右子树、根
void LevelOrderTraversal(BinTree BT):层次遍历,从上到下、从左到右
3.2 顺序存储
按从上至下、从左到右顺序存储 n 个结点的完全二叉树的结点父子关系:
- 非根结点(序号 i > 1)的父结点的序号是 ⌊i/2⌋(向下取整)
- 结点(序号为 i)的左孩子结点的序号是 2i(若 2 i ≤ n,否则没有左孩子
- 结点(序号为 i)的右孩子结点的序号是 2i+1(若 2 i +1 ≤ n,否则没有右孩子
3.3 链表存储
4. 遍历操作
4.1 结点
typedef struct TreeNode *BinTree;
struct TreeNode{
int Data; // 存值
BinTree Left; // 左儿子结点
BinTree Right; // 右儿子结点
};
4.2. 先序遍历
遍历过程:访问根结点->先序遍历其左子树->先序遍历其右子树
4.2.1 递归实现
void PreOrderTraversal(BinTree BT){
if(BT){
printf("%d",BT->Data); // 打印根
PreOrderTraversal(BT->Left); // 进入左子树
PreOrderTraversal(BT->Right); // 进入右子树
}
}
4.2.2 非递归实现
void PreOrderTraversal(BinTree BT){
BinTree T = BT;
Stack S = CreateStack(); // 创建并初始化堆栈 S
while(T || !IsEmpty(S)){ // 当树不为空或堆栈不空
while(T){
Push(S,T); // 压栈,第一次遇到该结点
printf("%d",T->Data); // 访问结点
T = T->Left; // 遍历左子树
}
if(!IsEmpty(S)){ // 当堆栈不空
T = Pop(S); // 出栈,第二次遇到该结点
T = T->Right; // 访问右结点
}
}
}
4.3 中序遍历
递归过程:中序遍历其左子树->访问根结点->中序遍历其右子树
4.3.1 递归实现
void InOrderTraversal(BinTree BT){
if(BT){
InOrderTraversal(BT->Left); // 进入左子树
printf("%d",BT->Data); // 打印根
InOrderTraversal(BT->Right); // 进入右子树
}
}
4.3.2 非递归实现
void InOrderTraversal(BinTree BT){
BinTree T = BT;
Stack S = CreateStack(); // 创建并初始化堆栈 S
while(T || !IsEmpty(S)){ // 当树不为空或堆栈不空
while(T){
Push(S,T); // 压栈
T = T->Left; // 遍历左子树
}
if(!IsEmpty(S)){ // 当堆栈不空
T = Pop(S); // 出栈
printf("%d",T->Data); // 访问结点
T = T->Right; // 访问右结点
}
}
}
4.4 后序遍历
遍历过程:后序遍历其左子树->后序遍历其右子树->访问根结点
4.4.1 递归实现
void PostOrderTraversal(BinTree BT){
if(BT){
PostOrderTraversal(BT->Left); // 进入左子树
PostOrderTraversal(BT->Right); // 进入右子树
printf("%d",BT->Data); // 打印根
}
}
4.4.2 非递归实现
void PostOrderTraversal(BinTree BT){
BinTree T = BT;
Stack S = CreateStack(); // 创建并初始化堆栈 S
vector<BinTree> v; // 创建存储树结点的动态数组
Push(S,T);
while(!IsEmpty(S)){ // 当树不为空或堆栈不空
T = Pop(S);
v.push_back(T); // 出栈元素进数组
if(T->Left)
Push(S,T->Left);
if(T->Right)
Push(S,T->Right);
}
reverse(v.begin(),v.end()); // 逆转
for(int i=0;i<v.size();i++) // 输出数组元素
printf("%d",v[i]->Data);
}
4.5 层序遍历
遍历过程:从上至下,从左至右访问所有结点
队列实现过程:
根结点入队->从队列中取出一个元素->访问该元素所指结点
- 若该元素所指结点的左孩子结点非空,左孩子结点入队
- 若该元素所指结点的右孩子结点非空,右孩子结点入队
- 循环 1 - 4,直到队列中为空
void LevelOrderTraversal(BinTree BT){
queue<BinTree> q; // 创建队列
BinTree T;
if(!BT)
return;
q.push(BT); // BT 入队 ,此时bt是根节点
while(!q.empty()){
T = q.front(); // 访问队首元素
q.pop(); // 出队
printf("%d",T->Data);
if(T->Left) // 如果存在左儿子结点
q.push(T->Left); // 入队
if(T->Right)
q.push(T->Right);
}
}