2024王道数据结构考研笔记专栏将持续更新,欢迎 点此 收藏,共同交流学习…
文章目录
- 第五章:树
- 5.1树的基本概念
- 5.1.1树的定义
- 5.1.2基本术语
- 5.1.3树的性质
- 5.2二叉树的概念
- 5.2.1 二叉树的定义与特性
- 5.2.2几种特殊的二叉树
- 5.2.3二叉树的存储结构
- 5.3二叉树的遍历和线索二叉树
- 5.3.1二叉树的遍历
- 5.3.2线索二叉树
- 5.4树、森林
- 5.4.1树的存储结构
- 5.4.2树、森林与二叉树的转换
- 5.4.3树、森林的遍历
- 5.5树与二叉树的应用
- 5.5.1二叉排序树(BST)
- 5.5.2平衡二叉树(AVL)
- 5.5.3哈夫曼树
- 第六章 图
- 6.1图的基本概念
- 6.2图的存储结构
- 6.2.1数组表示法(邻接矩阵表示法)
- 6.2.2 邻接表(类似于数的孩子链表表示法)
- 6.2.3十字链表
- 6.3图的遍历
- 6.4最小生成树
- 6.5最短路径
- 6.6AOV网络与拓扑排序
- 6.7 AOE网络与关键路径
第五章:树
5.1树的基本概念
5.1.1树的定义
树是n个结点的有限集。
- 空树:n=0
- 根结点、分支结点、叶子结点
- 非空树的特性
- 子树
5.1.2基本术语
- 结点之间的关系描述
- 祖先、子孙、双亲、兄弟…结点
- 路径、路径长度
- 结点、树的属性描述
1. 结点的层次(深度)——从上往下
2. 结点的高度——从下往上
3. 树的高度——总共多少层
4. 结点的度——有几个孩子
5. 树的度——各结点的度的最大值 - 有序树、无序树
- 森林
5.1.3树的性质
- 树中的结点数等于所有结点的度数之和加1。
- 度为m的树第i层上至多有m^i-1个结点
- 度为m的数、m叉数的区别
5.2二叉树的概念
5.2.1 二叉树的定义与特性
定义:
二叉树是n(n>=0)个结点的有限集,它或者是空集(n=0),或者由一个根结点及两颗互不相交的分别称作这个根的左子树和右子树的二叉树组成。
特点:
- 每个结点最多有俩孩子(二叉树中不存在度大于2的结点)。
- 二叉树可以是空集合,根可以有空的左子树和空的右子树。
- 二叉树有左右之分,次序不能颠倒。
二叉树的性质:
-
在二叉树的第i层上至多有2^(i-1)个结点(i>1)。
-
深度为k的二叉树至多有2^k-1个结点(k>=1)。
-
对任何一颗二叉树T,如果其叶子数为n0,度为2的结点数为n2,则n0=n2+1.
-
具有n个结点的完全二叉树的深度为(log2N)+1。
注意:二叉树不是树的特殊情况,它们是两个概念。
5.2.2几种特殊的二叉树
- 满二叉树:一颗深度为k且有2^k-1个结点的二叉树称为满二叉树。每一层上的结点数都达到最大。叶子全部在最低层。
- 完全二叉树:深度为k的具有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号为1~n的结点一 一对应时,称之为完全二叉树。
- 二叉排序树
- 平衡二叉树
5.2.3二叉树的存储结构
- 顺序存储
二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来;
#define MaxSize 100
struct TreeNode{
ElemType value; //结点中的数据元素
bool isEmpty; //结点是否为空
}
main(){
TreeNode t[MaxSize];
for (int i=0; i<MaxSize; i++){
t[i].isEmpty = true;
}
}
- 链式存储
//二叉树的结点
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的父节点;只能从根结点开始遍历,也可以使用三叉链表
typedef struct BiTnode{
ElemType data; //数据域
struct BiTNode *lchild, *rchild; //左、右孩子指针
struct BiTNode *parent; //父节点指针
}BiTNode, *BiTree;
- n个结点的二叉链表共有n+1个空链域
5.3二叉树的遍历和线索二叉树
5.3.1二叉树的遍历
- 先序遍历(根左右)
- 若二叉树为空,不用操作
- 若二叉树非空:
- 访问根节点
- 先序遍历左子树
- 先序遍历右子树
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); //递归遍历右子树
}
}
- 中序遍历(左根右)
- 若二叉树为空,不用操作
- 若二叉树非空:
- 先序遍历左子树
- 访问根节点
- 先序遍历右子树
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); //访问根结点
}
}
- 二叉树的层次遍历
算法思想:
- 初始化一个辅助队列
- 根节点入队
- 若队列非空,则队头结点出队,访问该结点,依次将其左、右孩子插入队尾(如果有的话)
- 重复以上操作直至队列为空
//二叉树的结点(链式存储)
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); //右孩子入队
}
}
- 由遍历序列构造二叉树
- 先序序列 + 中序序列
- 后序序列 + 中序序列
- 层序序列 + 中序序列
key: 找到树的根节点,并根据中序序列划分左右子树,再找到左右子树根节点、
5.3.2线索二叉树
- 线索二叉树的概念与作用
在二叉树的结点上加上线索的二叉树称为线索二叉树,对二叉树以某种遍历方式(如先序、中序、后序或层次等)进行遍历,使其变为线索二叉树的过程称为对二叉树进行线索化。 - 线索二叉树的存储结构
- 中序线索二叉树——线索指向中序前驱、中序后继
//线索二叉树结点
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; // 左、右线索标志
}ThreadNode, *ThreadTree;
tag == 0: 指针指向孩子
tag == 1: 指针是“线索”
-
先序线索二叉树——线索指向先序前驱、先序后继
-
后序线索二叉树——线索指向后序前驱、后序后继
- 二叉树的线索话
- 中序线索化
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; // 左、右线索标志
}ThreadNode, *ThreadTree;
//全局变量pre, 指向当前访问的结点的前驱
TreadNode *pre=NULL;
void InThread(ThreadTree T){
if(T!=NULL){
InThread(T->lchild); //中序遍历左子树
visit(T); //访问根节点
InThread(T->rchild); //中序遍历右子树
}
}
void visit(ThreadNode *q){
if(q->lchid = NULL){ //左子树为空,建立前驱线索
q->lchild = pre;
q->ltag = 1;
}
if(pre!=NULL && pre->rchild = NULL){
pre->rchild = q; //建立前驱结点的后继线索
pre->rtag = 1;
}
pre = q;
}
//中序线索化二叉树T
void CreateInThread(ThreadTree T){
pre = NULL; //pre初始为NULL
if(T!=NULL);{ //非空二叉树才能进行线索化
InThread(T); //中序线索化二叉树
if(pre->rchild == NULL)
pre->rtag=1; //处理遍历的最后一个结点
}
}
- 先序线索化
注意【转圈】问题,当ltag==0时,才能对左子树先序线索化
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; // 左、右线索标志
}ThreadNode, *ThreadTree;
//全局变量pre, 指向当前访问的结点的前驱
TreadNode *pre=NULL;
//先序遍历二叉树,一边遍历一边线索化
void PreThread(ThreadTree T){
if(T!=NULL){
visit(T);
if(T->ltag == 0) //lchild不是前驱线索
PreThread(T->lchild);
PreThread(T->rchild);
}
}
void visit(ThreadNode *q){
if(q->lchid = NULL){ //左子树为空,建立前驱线索
q->lchild = pre;
q->ltag = 1;
}
if(pre!=NULL && pre->rchild = NULL){
pre->rchild = q; //建立前驱结点的后继线索
pre->rtag = 1;
}
pre = q;
}
//先序线索化二叉树T
void CreateInThread(ThreadTree T){
pre = NULL; //pre初始为NULL
if(T!=NULL);{ //非空二叉树才能进行线索化
PreThread(T); //先序线索化二叉树
if(pre->rchild == NULL)
pre->rtag=1; //处理遍历的最后一个结点
}
}
- 后序线索化
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; // 左、右线索标志
}ThreadNode, *ThreadTree;
//全局变量pre, 指向当前访问的结点的前驱
TreadNode *pre=NULL;
//先序遍历二叉树,一边遍历一边线索化
void PostThread(ThreadTree T){
if(T!=NULL){
PostThread(T->lchild);
PostThread(T->rchild);
visit(T); //访问根节点
}
}
void visit(ThreadNode *q){
if(q->lchid = NULL){ //左子树为空,建立前驱线索
q->lchild = pre;
q->ltag = 1;
}
if(pre!=NULL && pre->rchild = NULL){
pre->rchild = q; //建立前驱结点的后继线索
pre->rtag = 1;
}
pre = q;
}
//先序线索化二叉树T
void CreateInThread(ThreadTree T){
pre = NULL; //pre初始为NULL
if(T!=NULL);{ //非空二叉树才能进行线索化
PostThread(T); //后序线索化二叉树
if(pre->rchild == NULL)
pre->rtag=1; //处理遍历的最后一个结点
}
}
- 线索二叉树中找前驱、后继
- 中序线索二叉树找中序后继:在中序线索二叉树中找到指定节点 *p 的中序后继 next
若 p->rtag == 1, 则 next = p->rchild;
若 p->rtag == 0, 则 p 必有右孩子, 则 next = p的右子树中最左下结点;
//1. 找到以P为根的子树中,第一个被中序遍历的结点
ThreadNode *Firstnode(ThreadNode *p){
//循环找到最左下的结点(不一定是叶结点)
while(p->ltag == 0)
p=p->lchild;
return p;
}
//2. 在中序线索二叉树中找到结点p的后继结点
ThreadNode *Nextnode(ThreadNode *p){
//右子树最左下结点
if(p->rtag==0)
return Firstnode(p->rchild);
else
return p->rchild; //rtag==1,直接返回后继线索
}
//3. 对中序线索二叉树进行中序遍历
void Inorder(ThreadNode *T){ //T为根节点指针
for(ThreadNode *p = Firstnode(T); p!=NULL; p = Nextnode(p))
visit(p);
}
- 先序线索二叉树找先序后继:在先序线索二叉树中找到指定节点 *p 的先序后继 next
若
p->rtag == 1, 则 next = p->rchild; 若 p->rtag == 0
, 则 p 必有右孩子(左孩子不知道)case1: 若p有左孩子 ——— 根 左 右 / 根 (根 左 右) 右
case2: 若p没有左孩子 ——— 根 右 / 根 (*根 *左 右)
- 先序线索二叉树找先序前驱:在先序线索二叉树中找到指定节点 *p 的先序前驱pre
若 p->ltag == 1, 则 next = p->lchild;
若 p->ltag == 0, 则 p
必有左孩子,但是先序遍历中,左右子树的结点只可能是根的后继,不可能是前驱,所以不能从左右孩子里寻找p的先序前驱,(除非从头开始遍历/三叉链表case1: 如果能够找到p的父节点,且p是左孩子 —— p的父节点就是p的前驱;
case2: 如果能够找到p的父节点,且p是右孩子,且其左兄弟为空 —— p的父节点就是p的前驱;
case3: 如果能够找到p的父节点,且p是右孩子,且其左兄弟非空 ——
p的前驱为左兄弟子树中最后一个被先序遍历到的结点(根节点出发,先往右,右没有往左,找到最下一层的结点);case4: p没有父节点,即p为根节点,则p没有先序前驱
- 后序线索二叉树找后序前驱:在后序线索二叉树中找到指定节点 *p 的后序前驱pre
若 p->ltag == 1, 则 next = p->lchild;
若 p->ltag == 0, 则 p 必有左孩子(不知道有没有右孩子)
case1: 若p有右孩子 ——— 左 右 根 / 左 (左 右 根) 根
case2: 若p没有右孩子 ——— 左 根 (左子树按后序遍历,最后一个结点,p的左孩子)
- 后序线索二叉树找后序后继:在后序线索二叉树中找到指定节点 *p 的后序后继next
若 p->rtag == 1, 则 next = p->rchild;
若 p->rtag == 0, 则 p 必有右孩子, 左孩子不知道, 但是在后序遍历中,左右子树中的结点只有可能是根的前驱,而不可能是根的后继,所以找不到后继,(除非从头开始遍历/三叉链表
case1: 如果能找到p的父节点,且p是右孩子 —— p的父节点即为其后继
case2: 如果能找到p的父节点,且p是左孩子,其右兄弟为空 —— p的父节点即为其后继
case3: 如果能找到p的父节点,且p是左孩子,其右兄弟非空 —— p的后继为其右兄弟子树中第一个被后序遍历的结点;
case4: p没有父节点,即p为根节点,则p没有后序后继;
5.4树、森林
5.4.1树的存储结构
- 双亲表示法(顺序存储):每个结点中保存指向双亲的指针
数据域:存放结点本身信息。
双亲域:指示本结点的双亲结点在数组中的位置。
#define MAX_TREE_SIZE 100 //树中最多结点数
typedef struct{ //树的结点定义
ElemType data;
int parent; //双亲位置域
}PTNode;
typedef struct{ //树的类型定义
PTNode nodes[MAX_TREE_SIZE]; //双亲表示
int n; //结点数
}PTree;
增:新增数据元素,无需按逻辑上的次序存储;(需要更改结点数n)
删(叶子结点):① 将伪指针域设置为-1;②用后面的数据填补;(需要更改结点数n)
查询:①优点-查指定结点的双亲很方便;②缺点-查指定结点的孩子只能从头遍历,空数据导致遍历更慢;
- 孩子表示法(顺序+链式)
孩子链表:把每个结点的孩子结点排列起来,看成是一个线性表,用单链表存储,则n个结点有n个孩子链表(叶子的孩子链表为空表)。而n个头结点又组成一个线性表,用顺序表(含n个元素的结构数组)存储。
struct CTNode{
int child; //孩子结点在数组中的位置
struct CTNode *next; // 下一个孩子
};
typedef struct{
ElemType data;
struct CTNode *firstChild; // 第一个孩子
}CTBox;
typedef struct{
CTBox nodes[MAX_TREE_SIZE];
int n, r; // 结点数和根的位置
}CTree;
- 孩子兄弟表示法(链式)
typedef struct CSNode{
ElemType data; //数据域
struct CSNode *firstchild, *nextsibling; //第一个孩子和右兄弟指针, *firstchild 看作左指针,*nextsibling看作右指针
}CSNode. *CSTree;
5.4.2树、森林与二叉树的转换
本质:森林中各个树的根结点之间视为兄弟关系
将树转换成二叉树:
- 加线:在兄弟之间加一连线
- 抹线:对每个结点去除其与孩子之间的关系(第一孩子除外)
- 旋转:以树的根结点为轴心,顺时针转45度
(兄弟相连留长子)
5.4.3树、森林的遍历
- 树的遍历
- 先根遍历:若树非空,先访问根结点,再依次对每棵子树进行先根遍历;(与对应二叉树的先序遍历序列相同)
void PreOrder(TreeNode *R){
if(R!=NULL){
visit(R); //访问根节点
while(R还有下一个子树T)
PreOrder(T); //先跟遍历下一个子树
}
}
- 后根遍历:若树非空,先依次对每棵子树进行后根遍历,最后再返回根节点;(与对应二叉树的中序遍历序列相同)
void PostOrder(TreeNode *R){
if(R!=NULL){
while(R还有下一个子树T)
PostOrder(T); //后跟遍历下一个子树
visit(R); //访问根节点
}
}
- 层序遍历(队列实现):
若树非空,则根结点入队;
若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队;
重复以上操作直至队尾为空;
- 森林的遍历
- 先序遍历:等同于依次对各个树进行先根遍历;也可以先转换成与之对应的二叉树,对二叉树进行先序遍历;
- 中序遍历:等同于依次对各个树进行后根遍历;也可以先转换成与之对应的二叉树,对二叉树进行中序遍历;
5.5树与二叉树的应用
5.5.1二叉排序树(BST)
- 二叉排序树的定义
左子树结点值<跟结点值<右子树结点值 - 查找操作
typedef struct BSTNode{
int key;
struct BSTNode *lchild, *rchild;
}BSTNode, *BSTree;
//在二叉排序树中查找值为key的结点(非递归)
//最坏空间复杂度:O(1)
BSTNode *BST_Search(BSTree T, int key){
while(T!=NULL && key!=T->key){ //若树空或等于跟结点值,则结束循环
if(key<T->key) //值小于根结点值,在左子树上查找
T = T->lchild;
else //值大于根结点值,在右子树上查找
T = T->rchild;
}
return T;
}
//在二叉排序树中查找值为key的结点(递归)
//最坏空间复杂度:O(h)
BSTNode *BSTSearch(BSTree T, int key){
if(T == NULL)
return NULL;
if(Kry == T->key)
return T;
else if(key < T->key)
return BSTSearch(T->lchild, key);
else
return BSTSearch(T->rchild, key);
}
- 插入操作
//在二叉排序树中插入关键字为k的新结点(递归)
//最坏空间复杂度:O(h)
int BST_Insert(BSTree &T, int k){
if(T==NULL){ //原树为空,新插入的结点为根结点
T = (BSTree)malloc(sizeof(BSTNode));
T->key = k;
T->lchild = T->rchild = NULL;
return 1; //插入成功
}
else if(K == T->key) //树中存在相同关键字的结点,插入失败
return 0;
else if(k < T->key)
return BST_Insert(T->lchild,k);
else
return BST_Insert(T->rchild,k);
}
- 二叉排序树的构造
//按照str[]中的关键字序列建立二叉排序树
void Crear_BST(BSTree &T, int str[], int n){
T = NULL; //初始时T为空树
int i=0;
while(i<n){
BST_Insert(T,str[i]); //依次将每个关键字插入到二叉排序树中
i++;
}
}
- 删除操作
- 查找效率分析
查找长度:查找运算中,需要对比关键字的次数,反映了查找操作时间复杂度;
查找成功的平均查找长度ASL
查找失败的平均查找长度ASL
5.5.2平衡二叉树(AVL)
- 平衡二叉树的定义
在插入和删除二叉树的结点时,要保证任意结点的左右子树的高度差的绝对值不超过1,将这样的树称为平衡二叉树。
//平衡二叉树结点
typedef struct AVLNode{
int key; //数据域
int balance; //平衡因子
struct AVLNode *lchild; *rchild;
}AVLNode, *AVLTree;
- 平衡二叉树的插入
- 插入新节点后如何调整“不平衡”问题
调整最小不平衡子树
LL: 在A结点的左孩子的左子树中插入导致不平衡
调整: A的左孩子结点右上旋
RR: 在A结点的右孩子的右子树中插入导致不平衡
调整: A的右孩子结点左上旋
LR: 在A结点的左孩子的右子树中插入导致不平衡
调整: A的左孩子的右孩子,先左上旋再右上旋
RL: 在A结点的右孩子的左子树中插入导致不平衡
调整: A的右孩子的左孩子,先右上旋再左上旋
- 平衡二叉树的查找与效率分析
若树高为h,则最坏情况下,查找一个关键字最多需要对比h次,即查找操作的时间复杂度不可能超过O(h);
5.5.3哈夫曼树
- 带权路径长度:从根节点到该结点之间的路径长度与该节点的权的乘积。
- 哈夫曼树的定义:带权路径最短的树。
- 哈夫曼树的构造(重点):构造森林全是根,选用两小造新树,删除两小添新人,重复2、3剩单根。
- 哈杜曼编码(重点):
由哈夫曼树得到的二进制前缀编码称为哈夫曼编码。
第六章 图
6.1图的基本概念
- 图是一种非线性结构
- 图的特点:
- 顶点之间的关系是任意的
- 图中任意两个顶点之间都可能相关
- 顶点的前驱和后继个数无限制
- 定义:图是一种数据元素间存在多对多关系的数据结构加上一组基本操作构成的抽象数据类型。
生成树:所有顶点均由边连接在一起但不存在回路的图。
6.2图的存储结构
6.2.1数组表示法(邻接矩阵表示法)
6.2.2 邻接表(类似于数的孩子链表表示法)
6.2.3十字链表
6.3图的遍历
定义:从图的任意指定顶点出发,依照某种规则去访问图中所有顶点,且每个顶点仅被访问一次,这一过程叫图的遍历。
方式:
- 深度优先遍历方法(Depth_First Search——DFS)
- 广度优先遍历法(Breadth_Frist Search——BFS)
6.4最小生成树
普里姆(Prim)算法。
6.5最短路径
迪杰斯特拉: