第五章 树和二叉树
树的基本概念
- 树:有且仅有一个特定根结点;树是递归的也是分层的
- 度:树中一个结点的孩子结点称为该节点的度,树中结点最大的度数称为树的度
- 深度:从上往下;高度:从下往上
- 有序树:格子数从左到右有次序,不能互换;
- 路径:树中两个结点之间的路径是由这两个结点所经过的结点序列构成,长度为经过边的个数
- 森林:m棵不相交的树的集合
- 性质:树中的结点数等于所有结点的度数之和加一;度为m的树中第i层上最多mi-1个结点;
高度为h的m叉树至多(mh-1)/m-1个结点;具有n个结点的m叉树最小高度为logm(n(m-1)+1);
二叉树的概念
二叉树的定义及其主要特性
- 定义:每个结点至多两颗子树,子树有左右之分不能电脑顺序
- 满二叉树:树中每一层都含有最多的结点
- 完全二叉树:满二叉树缺了最后几个叶子结点
- 二叉排序树:左子树上所有结点的关键字均小于根节点的关键字,右子树上所有结点的关键字均大于根节点的关键字,左右子树各是一颗二叉排序树
- 平衡二叉树:任一根节点的左右子树深度之差不超过1
- 二叉树的性质:非空二叉树上叶子结点数等于度为2的结点数+1,其余和前面相同
二叉树的存储结构
- 顺序存储结构:适用于完全二叉树和满二叉树,节省存储空间
- 链式存储结构:二叉树一般采取链式存储结构(data、lchild、rchild)
二叉树的遍历和线索二叉树
- 二叉树的遍历:按照某条搜索路径访问树中的每个结点,使得每个结点均被访问一次,且仅被访问一次
- 先序遍历(NLR)//递归算法
先访问根节点,再左子树,再右子树
//先序遍历
void PreOrder(BiTree T) {
if (T != NULL) {
visit(T);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
- 中序遍历(LNR)
先访问左子树,再根节点,再右子树
void PreOrder(BiTree T) {
if (T != NULL) {
PreOrder(T->lchild);
visit(T);
PreOrder(T->rchild);
}
- 后序遍历(LRN)
先访问左子树,再右子树,再根节点
void PreOrder(BiTree T) {
if (T != NULL) {
PreOrder(T->lchild);
PreOrder(T->rchild);
visit(T);
}
时间复杂度和空间复杂度均为O(n);
递归工作栈的深度恰好为树的深度;
- 递归算法和非递归算法的转化
//中序遍历的非递归
void InOrder2(BiTree T) {
InitStack(S); //初始化栈s
BiTree p = T; //p是遍历指针
while (p || p!IsEmpty(S)) {//栈不空或者p不空的时候循环
if (p) { //一路向左
push(p); //入栈顶
p = p->lchild; //左孩子不空,一路向左走
}
else {
pop(p); //出栈,转向右子树
visit(p);
p = p->rchild; //栈顶元素出栈,访问该节点,向右子树走
}
}
}
//先序遍历的非递归
void InOrder3(BiTree T) {
InitStack(S); //初始化栈s
BiTree p = T; //p为遍历指针
while (p || IsEmpty(S)) {
if (p) {
visit(p);
Push(S, p);
p = p->lchild;
}
else {
Pop(S, p);
p = p->rchild;
}
}
}
- 层次遍历
借助队列
//层次遍历
void LevelOrder(BiTree T) {
InitQueue(Q);
BiTree p = T;
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);
}
}
}
由遍历确定二叉树:先序和中序、后序和中序、层序和中序
线索二叉树
传统二叉链表存储仅能体现父子关系,无法直接得到结点在遍历中的前驱或者后继
线索二叉树可以查找结点前驱和后继,规定:若无左子树,则lchild指向其前驱结点,若无右子树,rchild指向后继结点
- 中序线索二叉树的构造
线索化的实质是遍历一遍二叉树
//中序线索二叉树的构造
void InThread(Thread& p, Thread& pre) {
//pre指向刚刚访问过的结点,p指向正在访问的结点,即pre是p的前驱
if (p != NULL) {
InThread(p->lchild, pre);
if (p->lchild == NULL) {
p->child = pre;
p->ltag = 1;
}
if (pre != NULL && pre->rchild == NULL) {
pre->rchild = p;
pre->rtag = 1;
}
pre = p;
InThread(p->rchild, pre);
}
}
void Ct = reateInThrea(ThreadTree T) {
Thread pre = NULL;
if (T) {
InThread(T, pre);
pre->rchild = NULL;
pre->rtag = 1;
}
}
- 中序线索二叉树的遍历
找到序列中的第一个结点,然后依次找到结点的后继,直到后继为空
树、森林
树的存储结构
- 双亲表示法:选择一组连续的空间存储每个结点,在每个结点中增设一个伪指针,指示双亲结点在数组中的位置。
树的顺序存储结构中,数组下标代表结点编号,下标中所存的内容表示结点之间的关系;
二叉树的顺序存储结构中,数组下标表示结点编号也表示各节点之间的关系; - 孩子表示法:将每个结点的孩子结点都用单链表链接起来形成一个线性结构
- 孩子兄弟表示法:又名二叉树表示法。以二叉链表作为树的存储结构,每个结点都包括:结点值、指向结点第一个孩子结点的指针、指向结点下一个兄弟结点的指针
树、森林与二叉树的转换
给定一棵树可以找到唯一的一颗二叉树与之对应
树转化成二叉树:结点的孩子左子树,结点的兄弟右子树
森林转化成二叉树:把所有二叉树的根链接起来,剩下的和之前序列一样
树、森林的遍历
树与森林先根遍历:对应二叉树的先序排列;
树后根遍历、森林中序遍历:对应二叉树的中序排列
树层次遍历:对应二叉树的层次遍历
树的应用——并查集
Union(S,Root1,Root2):把s中子集合S1并入子集合S2;
Find(S,x):查找集合s中单元素x所在的子集合,并返回该集合的名字
Initial(S):将集合s中每个元素都初始化为只有一个单元素的子集合
树和二叉树的应用
二叉排序树(BST)
- 二叉排序树或是一颗空树或具有左子树小于根节点小于右子树的性质的树
- 二叉排序树的查找:从根节点开始,沿着某个分支逐层向下比较的过程
//二叉排序树的非递归查找算法
BSTNode* BST_search(BiTree T, ElemType key) {
while (T != NULL && T->data != key) {
if (T->data < key)
T = T->rchild;
else
T = T->lchid;
}
return T;
}
- 二叉排序树的插入
若原二叉排序树为空,则直接插入结点,否则若关键字k小于根结点值则插入到左子树,若关键字k大于根节点,则插入到右子树
//二叉排序树的插入算法
int BST_Insert(BiTree& T, KeyType k) {
if (T == NULL) {
T = (Bitree)malloc(sizeof(BSTNode));
T->key = k;
T->lchild = T->rchild = NULL;
ruturn 1; //返回1 插入成功
}
else if (k == T->key)
return 0;
else if(k < T->key)
return BST_Insert(T->lchild, k);
else
return BST_Insert(T->lchild, k);
}
- 二叉排序树的构造
//二叉排序树的构造
void Creat_BST(BiTree& T, KeyType str[i], int n) {
T = NULL;
int i = 0;
while (i <n) {
BST_insert(T, str[i]);
i++;
}
}
- 二叉排序树的删除
把被删除结点从存储二叉排序树的链表上摘下来,将因此删除结点而断开的二叉链表重新组合起来
找直接后继(右子树中最左下的)或者直接前驱(左子树中最右下的) - 二叉排序树的查找效率分析
左右子树高度之差绝对值不超过1最好,O(logn)
若全部是倾斜单只树则是O(n)
排序二叉树的查找不唯一
平衡二叉树(AVL)
- 左右子树高度差的绝对值不超过1,平衡因子只可能为-1,0,1
- 平衡二叉树的插入(LL、RR、LR、RL)
- 平衡二叉树的查找 O(logn)