目录
第一章 数据结构绪论
1.1 数据结构的基本概念
1.2 数据结构的三要素
1.2.1. 数据的逻辑结构
1.2.2. 数据的存储结构(物理结构)
1.2.3. 数据的运算
1.2.4. 数据类型和抽线数据类型
1.3 算法的基本概念
1.4 算法的时间复杂度
1.5 算法的空间复杂度
第二章 线性表
2.1 线性表的定义和基本操作
2.1.1 线性表的定义
2.1.2 线性表的基础操作
2.2 顺序表
2.2.1 顺序表的概念
2.2.2. 顺序表的实现编辑
2.2.3 顺序表的基本操作
2.3 线性表的链式表示
2.3.1. 单链表的基本概念
2.3.2. 单链表的实现
2.3.3. 单链表的插入
2.3.4. 单链表的删除
2.3.5. 单链表的查找
2.3.6. 单链表的建立
2.3.7. 双链表
2.3.8. 循环链表
2.3.9. 静态链表
2.3.10. 顺序表和链表的比较
第三章 栈和队列
3.1. 栈
3.1.1. 栈的基本概念
3.1.2. 栈的基本操作
3.1.3. 栈的顺序存储实现
3.1.4. 栈的链式存储
3.2. 队列
3.2.1. 队列的基本概念
3.2.2. 队列的基本操作
3.2.3. 队列的顺序存储实现
3.2.4. 队列的链式存储实现
3.2.5. 双端队列
3.3. 栈与队列的应用
3.3.1 栈在括号匹配中的应用
3.3.2. 栈在表达式求值中的应用
3.3.3. 栈在递归中的应用
3.3.4. 队列的应用
3.4. 特殊矩阵的压缩存储
3.4.1 数组的存储
3.4.2 对称矩阵的压缩存储
3.4.3 三角矩阵的压缩存储
3.4.4 三对角矩阵的压缩存储
3.4.5 稀疏矩阵的压缩存储
第四章 串
4.1. 串的基本概念
4.2. 串的基本操作
4.3. 串的存储实现
4.3.1 静态数组实现
4.3.2 基本操作的实现
4.4. 串的朴素模式匹配
4.5. KPM算法
第五章 图
5.1. 树的概念
5.1.1. 树的基本定义
5.1.2. 树的常考性质
5.2. 二叉树
5.2.1. 二叉树的定义
5.2.2. 特殊二叉树
5.2.3. 二叉树的性质
5.2.4. 二叉树存储实现
5.3. 二叉树的遍历和线索二叉树
5.3.1. 二叉树的先中后序遍历
5.3.2. 二叉树的层序遍历
5.3.3. 由遍历序列构造二叉树
5.3.4. 线索二叉树的概念
5.3.5. 二叉树的线索化
5.3.6. 在线索二叉树中找前驱/后继
5.4. 树和森林
5.4.1. 树的存储结构
5.4.2. 树和森林的遍历
5.5. 应用
5.5.1. 二叉排序树
5.5.2. 平衡二叉树
5.5.3. 哈夫曼树
第六章 图
6.1. 图的基本概念
6.2. 图的存储
6.2.1. 邻接矩阵
6.2.2. 邻接表
6.2.3. 十字链表、临接多重表
6.2.4. 图的基本操作
6.3. 图的遍历
6.3.1. 广度优先遍历
6.3.2. 深度优先遍历
6.4. 图的应用
6.4.1. 最小生成树
6.4.2. 无权图的单源最短路径问题——BFS算法
6.4.3. 单源最短路径问题——Dijkstra算法
6.4.4. 各顶点间的最短路径问题——Floyd算法
6.4.5. 有向⽆环图描述表达式
6.4.6. 拓扑排序
6.4.7. 关键路径
第七章 查找
7.1 查找概念
7.2 顺序查找
7.3 折半查找
7.4 分块查找
7.5 红黑树
7.5.1 为什么要发明红黑树?
7.5.2 红黑树的定义
7.5.3 红黑树的插入
7.6 B树和B+树
7.6.1 B树
7.6.2 B树的基本操作
7.6.3 B+树
7.6.4 B树和B+树的比较
7.7 散列查找及其性能分析
7.7.1 散列表的基本概念
7.7.2 散列查找及性能分析
第八章 排序
8.1. 排序的基本概念
8.2. 插入排序
8.2.1. 直接插入排序
8.2.2. 折半插入排序
8.2.3. 希尔排序
8.3. 交换排序
8.3.1. 冒泡排序
8.3.2. 快速排序
8.4. 选择排序
8.4.1. 简单选择排序
8.4.2. 堆排序
8.5. 归并排序
8.6. 基数排序
8.7. 内部排序算法总结
8.7.1. 内部排序算法比较
8.7.2. 内部排序算法的应用
8.8. 外部排序
8.8.1. 外部排序的基本概念和方法
8.8.2. 败者树
8.8.3. 置换-选择排序(生成初始归并段)
8.8.4. 最佳归并树
举例需要理解几点:
逻辑结构是指数据元素之间的逻辑关系,即从逻辑关系上描述数据。
逻辑结构包括:
如何用计算机表示数据元素的逻关系?
存储结构是指数据结构在计算机中的表示(又称映像),也称物理结构。
存储结构包括:
需要理解几点:
针对于某种逻辑结构,结合实际需求,定义基本运算。
例如:逻辑结构->线性结构
基本运算:
1.查找第i个数据元素
2.在第i个位置插入新的数据元素
3.删除第i个位置的数据元素......
数据类型是一个值的集合和定义在此集合上的一组操作的总称。例如:定义int整形,我们就可以把他们加减乘除等操作。
抽象数据类型(Abstract Data Type,ADT)是抽象数据组织及与之相关的操作。ADT 用数学化的语言定义数据的逻辑结构、定义运算。与具体的实现无关。
在探讨一种数据结构时理解几点:
程序 = 数据结构+算法
数据结构:如何用数据正确地描述现实世界的问题,并存入计算机。
算法:如何高效地处理这些数据,以解决实际问题
算法(Algorithm)是对特定问题求解步骤的一种描述,它是指令的有限序列,其中的每条指令表示一个或多个操作。
算法的特性:
我们可以类比:y = f(x)函数,其中x就是输出,y就是输出,这个函数就是算法。
好的算法达到的目标:
什么时候要传入参数的引用“&“-- 对参数的修改结果需要“带回来”看下面举例:
#include
void test(int x) //形参是实参的临时拷贝
{
x = 1024;
printf("test函数内部 x=%d\n",x);
}
int main()
{
int x = 1;
printf("调用test前 x=%d\n",x);
test(x); //这里的x改变了并没有传回来
printf("调用test后 x=%d\n",x);
return 0;
}
//输出为:
//调用test前 x=1
//test函数内部 x=1024
//调用test后 x=1
//请按任意键继续. . .
#include
void test(int &x) //把x的地址传到函数
{
x = 1024;
printf("test函数内部 x=%d\n",x);
}
int main()
{
int x = 1;
printf("调用test前 x=%d\n",x);
test(x); //这里的x通过函数传回来值改变了
printf("调用test后 x=%d\n",x);
return 0;
}
//输出为:
//调用test前 x=1
//test函数内部 x=1024
//调用test后 x=1024
//请按任意键继续. . .
我们看完线性表的逻辑结构和基本运算,现在继续学习物理结构:顺序表
//顺序表的实现--静态分配
#include
#define MaxSize 10 //定义表的最大长度
typedef struct{
int data[MaxSize]; //用静态的"数组"存放数据元素
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义(静态分配方式)
void InitList(SqList &L){
for(int i=0;i
//顺序表的实现——动态分配
#include
#include //malloc、free函数的头文件
#define InitSize 10 //默认的初始值
typedef struct{
int *data; //指示动态分配数组的指针
int MaxSize; //顺序表的最大容量
int length; //顺序表的当前长度
}SeqList;
void InitList(SeqList &L){ //初始化
//用malloc 函数申请一片连续的存储空间
L.data =(int*)malloc(InitSize*sizeof(int)) ;
L.length=0;
L.MaxSize=InitSize;
}
void IncreaseSize(SeqList &L,int len){ //增加动态数组的长度
int *p=L.data;
L.data=(int*)malloc((L.MaxSize+len)*sizeof(int));
for(int i=0;i
#define MaxSize 10 //定义最大长度
typedef struct{
int data[MaxSize]; //用静态的数组存放数据
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义
bool ListInsert(SqList &L, int i, int e){
if(i<1||i>L.length+1) //判断i的范围是否有效
return false;
if(L.length>=MaxSize) //当前存储空间已满,不能插入
return false;
for(int j=L.length; j>=i; j--){ //将第i个元素及其之后的元素后移
L.data[j]=L.data[j-1];
}
L.data[i-1]=e; //在位置i处放入e
L.length++; //长度加1
return true;
}
int main(){
SqList L; //声明一个顺序表
InitList(L);//初始化顺序表
//...此处省略一些代码;插入几个元素
ListInsert(L,3,3); //再顺序表L的第三行插入3
return 0;
}
#define MaxSize 10
typedef struct {
int data[MaxSize];
int length;
} SqList;
// 删除顺序表i位置的数据并存入e
bool ListDelete(SqList &L, int i, int &e) {
if (i < 1 || i > L.length) // 判断i的范围是否有效
return false;
e = L.data[i-1]; // 将被删除的元素赋值给e
for (int j = i; j < L.length; j++) //将第i个位置后的元素前移
L.data[j-1] = L.data[j];
L.length--;
return true;
}
int main() {
SqList L;
InitList(L);
int e = -1;
if (ListDelete(L, 3, e))
printf("已删除第3个元素,删除元素值为%d\n", e);
else
printf("位序i不合法,删除失败\n");
return 0;
}
// 静态分配的按位查找
#define MaxSize 10
typedef struct {
ElemType data[MaxSize];
int length;
}SqList;
ElemType GetElem(SqList L, int i) {
return L.data[i-1];
}
// 动态分配的按位查找
#define InitSize 10
typedef struct {
ElemType *data;
int MaxSize;
int length;
}SeqList;
ElemType GetElem(SeqList L, int i) {
return L.data[i-1];
}
#define InitSize 10 //定义最大长度
typedef struct{
ElemTyp *data; //用静态的“数组”存放数据元素
int Length; //顺序表的当前长度
}SqList;
//在顺序表L中查找第一个元素值等于e的元素,并返回其位序
int LocateElem(SqList L, ElemType e){
for(int i=0; i
以上我们看完顺序表的物理存储了,然后我们学习单链表
typedef struct LNode
{ //定义单链表结点类型
ElemType data; //数据域
struct LNode *next;//指针域
}LNode, *LinkList;
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
//初始化一个空的单链表
bool InitList(LinkList &L){
L = NULL; //空表,暂时还没有任何结点
return true;
}
void test(){
LinkList L; //声明一个指向单链表的头指针
//初始化一个空表
InitList(L);
...
}
//判断单链表是否为空
bool Empty(LinkList L){
return (L==NULL)
}
typedef struct LNode
{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
//初始化一个单链表(带头结点)
bool InitList(LinkList &L)
{
L = (LNode*) malloc(sizeof(LNode)); //头指针指向的结点——分配一个头结点(不存储数据)
if (L == NULL) //内存不足,分配失败
return false;
L -> next = NULL; //头结点之后暂时还没有结点
return true;
}
void test()
{
LinkList L; //声明一个指向单链表的指针: 头指针
//初始化一个空表
InitList(L);
//...
}
//判断单链表是否为空(带头结点)
bool Empty(LinkList L)
{
if (L->next == NULL)
return true;
else
return false;
}
带头结点和不带头结点的比较:
typedef struct LNode
{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
//在第i个位置插入元素e(带头结点)
bool ListInsert(LinkList &L, int i, ElemType e)
{
//判断i的合法性, i是位序号(从1开始)
if(i<1)
return False;
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p = L; //L指向头结点,头结点是第0个结点(不存数据)
//循环找到第i-1个结点
while(p!=NULL && jlengh, p最后会等于NULL
p = p->next; //p指向下一个结点
j++;
}
if (p==NULL) //如果p指针知道最后再往后就是NULL
return false;
//在第i-1个结点后插入新结点
LNode *s = (LNode *)malloc(sizeof(LNode)); //申请一个结点
s->data = e;
s->next = p->next;
p->next = s; //将结点s连到p后,后两步千万不能颠倒qwq
return true;
}
typedef struct LNode
{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
bool ListInsert(LinkList &L, int i, ElemType e)
{
if(i<1)
return false;
//插入到第1个位置时的操作有所不同!
if(i==1){
LNode *s = (LNode *)malloc(size of(LNode));
s->data =e;
s->next =L;
L=s; //头指针指向新结点
return true;
}
//i>1的情况与带头结点一样!唯一区别是j的初始值为1
LNode *p; //指针p指向当前扫描到的结点
int j=1; //当前p指向的是第几个结点
p = L; //L指向头结点,头结点是第0个结点(不存数据)
//循环找到第i-1个结点
while(p!=NULL && jlengh, p最后会等于NULL
p = p->next; //p指向下一个结点
j++;
}
if (p==NULL) //i值不合法
return false;
//在第i-1个结点后插入新结点
LNode *s = (LNode *)malloc(sizeof(LNode)); //申请一个结点
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
typedef struct LNode
{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
bool InsertNextNode(LNode *p, ElemType e)
{
if(p==NULL){
return false;
}
LNode *s = (LNode *)malloc(sizeof(LNode));
//某些情况下分配失败,比如内存不足
if(s==NULL)
return false;
s->data = e; //用结点s保存数据元素e
s->next = p->next;
p->next = s; //将结点s连到p之后
return true;
} //平均时间复杂度 = O(1)
//有了后插操作,那么在第i个位置上插入指定元素e的代码可以改成:
bool ListInsert(LinkList &L, int i, ElemType e)
{
if(i<1)
return False;
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p = L; //L指向头结点,头结点是第0个结点(不存数据)
//循环找到第i-1个结点
while(p!=NULL && jlengh, p最后4鸟会等于NULL
p = p->next; //p指向下一个结点
j++;
}
return InsertNextNode(p, e)
}
//前插操作:在p结点之前插入元素e
bool InsertPriorNode(LNode *p, ElenType e){
if(p==NULL)
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
if(s==NULL) //内存分配失败
return false;
//重点来了!
s->next = p->next;
p->next = s; //新结点s连到p之后
s->data = p->data; //将p中元素复制到s
p->data = e; //p中元素覆盖为e
return true;
}
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
bool ListDelete(LinkList &L, int i, ElenType &e){
if(i<1) return false;
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p = L; //L指向头结点,头结点是第0个结点(不存数据)
//循环找到第i-1个结点
while(p!=NULL && jlengh, p最后会等于NULL
p = p->next; //p指向下一个结点
j++;
}
if(p==NULL)
return false;
if(p->next == NULL) //第i-1个结点之后已无其他结点
return false;
LNode *q = p->next; //令q指向被删除的结点
e = q->data; //用e返回被删除元素的值
p->next = q->next; //将*q结点从链中“断开”
free(q) //释放结点的存储空间
return true;
}
bool DeleteNode(LNode *p){
if(p==NULL)
return false;
LNode *q = p->next; //令q指向*p的后继结点
p->data = p->next->data; //让p和后继结点交换数据域
p->next = q->next; //将*q结点从链中“断开”
free(q);
return true;
} //时间复杂度 = O(1)
LNode * GetElem(LinkList L, int i){
if(i<0) return NULL;
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p = L; //L指向头结点,头结点是第0个结点(不存数据)
while(p!=NULL && jnext;
j++;
}
return p; //返回p指针指向的值
}
LNode * LocateElem(LinkList L, ElemType e){
LNode *P = L->next; //p指向第一个结点
//从第一个结点开始查找数据域为e的结点
while(p!=NULL && p->data != e){
p = p->next;
}
return p; //找到后返回该结点指针,否则返回NULL
}
int Length(LinkList L){
int len=0; //统计表长
LNode *p = L;
while(p->next != NULL){
p = p->next;
len++;
}
return len;
}
// 使用尾插法建立单链表L
LinkList List_TailInsert(LinkList &L){
int x; //设ElemType为整型int
L = (LinkList)malloc(sizeof(LNode)); //建立头结点(初始化空表)
LNode *s, *r = L; //r为表尾指针
scanf("%d", &x); //输入要插入的结点的值
while(x!=9999){ //输入9999表示结束
s = (LNode *)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s; //r指针指向新的表尾结点
scanf("%d", &x);
}
r->next = NULL; //尾结点指针置空
return L;
}
LinkList List_HeadInsert(LinkList &L){ //逆向建立单链表
LNode *s;
int x;
L = (LinkList)malloc(sizeof(LNode)); //建立头结点
L->next = NULL; //初始为空链表,这步不能少!
scanf("%d", &x); //输入要插入的结点的值
while(x!=9999){ //输入9999表结束
s = (LNode *)malloc(sizeof(LNode)); //创建新结点
s->data = x;
s->next = L->next;
L->next = s; //将新结点插入表中,L为头指针
scanf("%d", &x);
}
return L;
}
LNode *Inverse(LNode *L)
{
LNode *p, *q;
p = L->next; //p指针指向第一个结点
L->next = NULL; //头结点指向NULL
while (p != NULL){
q = p;
p = p->next;
q->next = L->next;
L->next = q;
}
return L;
typedef struct DNode{ //定义双链表结点类型
ElemType data; //数据域
struct DNode *prior, *next; //前驱和后继指针
}DNode, *DLinklist;
typedef struct DNode{ //定义双链表结点类型
ElemType data; //数据域
struct DNode *prior, *next; //前驱和后继指针
}DNode, *DLinklist;
//初始化双链表
bool InitDLinkList(Dlinklist &L){
L = (DNode *)malloc(sizeof(DNode)); //分配一个头结点
if(L==NULL) //内存不足,分配失败
return false;
L->prior = NULL; //头结点的prior指针永远指向NULL
L->next = NULL; //头结点之后暂时还没有结点
return true;
}
void testDLinkList(){
//初始化双链表
DLinklist L; // 定义指向头结点的指针L
InitDLinkList(L); //申请一片空间用于存放头结点,指针L指向这个头结点
//...
}
//判断双链表是否为空
bool Empty(DLinklist L){
if(L->next == NULL) //判断头结点的next指针是否为空
return true;
else
return false;
}
bool InsertNextDNode(DNode *p, DNode *s){ //将结点 *s 插入到结点 *p之后
if(p==NULL || s==NULL) //非法参数
return false;
s->next = p->next;
if (p->next != NULL) //p不是最后一个结点=p有后继结点
p->next->prior = s;
s->prior = p;
p->next = s;
return true;
}
//删除p结点的后继结点
bool DeletNextDNode(DNode *p){
if(p==NULL) return false;
DNode *q =p->next; //找到p的后继结点q
if(q==NULL) return false; //p没有后继结点;
p->next = q->next;
if(q->next != NULL) //q结点不是最后一个结点
q->next->prior=p;
free(q);
return true;
}
//销毁一个双链表
bool DestoryList(DLinklist &L){
//循环释放各个数据结点
while(L->next != NULL){
DeletNextDNode(L); //删除头结点的后继结点
free(L); //释放头结点
L=NULL; //头指针指向NULL
}
}
while(p!=NULL){
//对结点p做相应处理,eg打印
p = p->prior;
}
后向遍历
while(p!=NULL){
//对结点p做相应处理,eg打印
p = p->next;
}
注意:双链表不可随机存取,按位查找和按值查找操作都只能用遍历的方式实现,时间复杂度为
typedef struct LNode{
ElemType data;
struct LNode *next;
}DNode, *Linklist;
/初始化一个循环单链表
bool InitList(LinkList &L){
L = (LNode *)malloc(sizeof(LNode)); //分配一个头结点
if(L==NULL) //内存不足,分配失败
return false;
L->next = L; //头结点next指针指向头结点
return true;
}
//判断循环单链表是否为空(终止条件为p或p->next是否等于头指针)
bool Empty(LinkList L){
if(L->next == L)
return true; //为空
else
return false;
}
//判断结点p是否为循环单链表的表尾结点
bool isTail(LinkList L, LNode *p){
if(p->next == L)
return true;
else
return false;
}
单链表和循环单链表的比较:
单链表:从一个结点出发只能找到该结点后续的各个结点;对链表的操作大多都在头部或者尾部;设立头指针,从头结点找到尾部的时间复杂度=O(n),即对表尾进行操作需要O(n)的时间复杂度;
循环单链表:从一个结点出发,可以找到其他任何一个结点;设立尾指针,从尾部找到头部的时间复杂度为O(1),即对表头和表尾进行操作都只需要O(1)的时间复杂度;
循环单链表优点:从表中任一节点出发均可找到表中其他结点。
typedef struct DNode{
ElemType data;
struct DNode *prior, *next;
}DNode, *DLinklist;
//初始化空的循环双链表
bool InitDLinkList(DLinklist &L){
L = (DNode *) malloc(sizeof(DNode)); //分配一个头结点
if(L==NULL) //内存不足,分配失败
return false;
L->prior = L; //头结点的prior指向头结点
L->next = L; //头结点的next指向头结点
}
void testDLinkList(){
//初始化循环单链表
DLinklist L;
InitDLinkList(L);
//...
}
//判断循环双链表是否为空
bool Empty(DLinklist L){
if(L->next == L)
return true;
else
return false;
}
//判断结点p是否为循环双链表的表尾结点
bool isTail(DLinklist L, DNode *p){
if(p->next == L)
return true;
else
return false;
}
bool InsertNextDNode(DNode *p, DNode *s){
s->next = p->next;
p->next->prior = s;
s->prior = p;
p->next = s;
//删除p的后继结点q
p->next = q->next;
q->next->prior = p;
free(q);
单链表:各个结点散落在内存中的各个角落,每个结点有指向下一个节点的指针(下一个结点在内存中的地址);
静态链表:用数组的方式来描述线性表的链式存储结构: 分配一整片连续的内存空间,各个结点集中安置,包括了——数据元素and下一个结点的数组下标(游标)
#define MaxSize 10 //静态链表的最大长度
struct Node{ //静态链表结构类型的定义
ElemType data; //存储数据元素
int next; //下一个元素的数组下标(游标)
};
//用数组定义多个连续存放的结点
void testSLinkList(){
struct Node a[MaxSize]; //数组a作为静态链表, 每一个数组元素的类型都是struct Node
//...
}
或者是:
#define MaxSize 10 //静态链表的最大长度
typedef struct{ //静态链表结构类型的定义
ELemType data; //存储数据元素
int next; //下一个元素的数组下标
}SLinkList[MaxSize];
void testSLinkList(){
SLinkList a;
}
相当于:
#define MaxSize 10 //静态链表的最大长度
struct Node{ //静态链表结构类型的定义
ElemType data; //存储数据元素
int next; //下一个元素的数组下标(游标)
};
typedef struct Node SLinkList[MaxSize]; //重命名struct Node,用SLinkList定义“一个长度为MaxSize的Node型数组;
【逻辑结构】
【存储结构】
顺序表:顺序存储
链表:链式存储
【基本操作 - 创建】
【基本操作 - 销毁】
【基本操作-增/删】
顺序表:插入/删除元素要将后续元素后移/前移;时间复杂度=O(n),时间开销主要来自于移动元素;
链表:插入/删除元素只需要修改指针;时间复杂度=O(n),时间开销主要来自查找目标元素
【基本操作-查】
顺序表
链表
顺序、链式、静态、动态四种存储方式的比较
顺序存储的固有特点:
链式存储的固有特点:
静态存储的固有特点:
动态存储的固有特点:
【顺序栈的定义】
#define MaxSize 10 //定义栈中元素的最大个数
typedef struct{
ElemType data[MaxSize]; //静态数组存放栈中元素
int top; //栈顶元素
}SqStack;
void testStack(){
SqStack S; //声明一个顺序栈(分配空间)
//连续的存储空间大小为 MaxSize*sizeof(ElemType)
}
【顺序栈的初始化】
#define MaxSize 10
typedef struct{
ElemType data[MaxSize];
int top;
}SqStack;
// 初始化栈
void InitStack(SqStack &S){
S.top = -1; //初始化栈顶指针
}
// 判断栈是否为空
bool StackEmpty(SqStack S){
if(S.top == -1)
return true;
else
return false;
}
【顺序栈的入栈出栈】
// 新元素进栈
bool Push(SqStack &S, ElemType x){ // 判断栈是否已满
if(S.top == MaxSize - 1)
return false;
S.data[++S.top] = x;
return true;
}
// 出栈
bool Pop(SqStack &x, ElemType &x){ // 判断栈是否为空
if(S.top == -1)
return false;
x = S.data[S.top--];
return true;
}
【读取栈顶元素】
// 读栈顶元素
bool GetTop(SqStack S, ElemType &x){
if(S.top == -1)
return false;
x = S.data[S.top];
return true;
}
S.data[++S.top] = x
【共享栈(两个栈共享同一片空间)】
#define MaxSize 10 //定义栈中元素的最大个数
typedef struct{
ElemType data[MaxSize]; //静态数组存放栈中元素
int top0; //0号栈栈顶指针
int top1; //1号栈栈顶指针
}ShStack;
//初始化栈
void InitSqStack(ShStack &S){
S.top0 = -1; //初始化栈顶指针
S.top1 = MaxSize;
}
【链栈的定义】
链表的头部作为栈顶,意味着:
因此,链栈实际上就是一个只能采用头插法插入或删除数据的链表;
栈的链式存储结构可描述为:
【链栈的定义】
typedef struct Linknode{
ElemType data; //数据域
Linknode *next; //指针域
}Linknode,*LiStack;
void testStack(){
LiStack L; //声明一个链栈
}
【链栈的初始化】
typedef struct Linknode{
ElemType data;
Linknode *next;
}Linknode,*LiStack;
// 初始化栈
bool InitStack(LiStack &L){
L = (Linknode *)malloc(sizeof(Linknode));
if(L == NULL)
return false;
L->next = NULL;
return true;
}
// 判断栈是否为空
bool isEmpty(LiStack &L){
if(L->next == NULL)
return true;
else
return false;
}
【入栈出栈】
// 新元素入栈
bool pushStack(LiStack &L,ElemType x){
Linknode *s = (Linknode *)malloc(sizeof(Linknode));
if(s == NULL)
return false;
s->data = x;
// 头插法
s->next = L->next;
L->next = s;
return true;
}
// 出栈
bool popStack(LiStack &L, int &x){
// 栈空不能出栈
if(L->next == NULL)
return false;
Linknode *s = L->next;
x = s->data;
L->next = s->next;
free(s);
return true;
}
【顺序队列的定义】
#define MaxSize 10; //定义队列中元素的最大个数
typedef struct{
ElemType data[MaxSize]; //用静态数组存放队列元素
int front, rear; //队头指针和队尾指针
}SqQueue;
void test{
SqQueue Q; //声明一个队列
}
【顺序队列的初始化】
#define MaxSize 10;
typedef struct{
ElemType data[MaxSize];
int front, rear;
}SqQueue;
// 初始化队列
void InitQueue(SqQueue &Q){
// 初始化时,队头、队尾指针指向0
// 队尾指针指向的是即将插入数据的数组下标
// 队头指针指向的是队头元素的数组下标
Q.rear = Q.front = 0;
}
// 判断队列是否为空
bool QueueEmpty(SqQueue Q){
if(Q.rear == Q.front)
return true;
else
return false;
}
【入队出队(循环队列)】
// 新元素入队
bool EnQueue(SqQueue &Q, ElemType x){
// 如果队列已满直接返回
if((Q.rear+1)%MaxSize == Q.front) //牺牲一个单元区分队空和队满
return false;
Q.data[Q.rear] = x;
Q.rear = (Q.rear+1)%MaxSize;
return true;
}
// 出队
bool DeQueue(SqQueue &Q, ElemType &x){
// 如果队列为空直接返回
if(Q.rear == Q.front)
return false;
x = Q.data[Q.front];
Q.front = (Q.front+1)%MaxSize;
return true;
}
【获得队头元素】
// 获取队头元素并存入x
bool GetHead(SqQueue &Q, ElemType &x){
if(Q.rear == Q.front)
return false;
x = Q.data[Q.front];
return true;
}
解决方法一:牺牲一个单元来区分队空和队满,即将(Q.rear+1)%MaxSize == Q.front作为判断队列是否已满的条件。(主流方法)
解决方法二:设置 size 变量记录队列长度。
#define MaxSize 10;
typedef struct{
ElemType data[MaxSize];
int front, rear;
int size;
}SqQueue;
// 初始化队列
void InitQueue(SqQueue &Q){
Q.rear = Q.front = 0;
Q.size = 0;
}
// 判断队列是否为空
bool QueueEmpty(SqQueue 0){
if(Q.size == 0)
return true;
else
return false;
}
// 新元素入队
bool EnQueue(SqQueue &Q, ElemType x){
if(Q.size == MaxSize)
return false;
Q.size++;
Q.data[Q.rear] = x;
Q.rear = (Q.rear+1)%MaxSize;
return true;
}
// 出队
bool DeQueue(SqQueue &Q, ElemType &x){
if(Q.size == 0)
return false;
Q.size--;
x = Q.data[Q.front];
Q.front = (Q.front+1)%MaxSize;
return true;
}
解决方法三:设置 tag 变量记录队列最近的操作。(tag=0
:最近进行的是删除操作;tag=1
:最近进行的是插入操作)
#define MaxSize 10;
typedef struct{
ElemType data[MaxSize];
int front, rear;
int tag;
}SqQueue;
// 初始化队列
void InitQueue(SqQueue &Q){
Q.rear = Q.front = 0;
Q.tag = 0;
}
// 判断队列是否为空,只有tag==0即出队的时候才可能为空
bool QueueEmpty(SqQueue 0){
if(Q.front == Q.rear && Q.tag == 0)
return true;
else
return false;
}
// 新元素入队
bool EnQueue(SqQueue &Q, ElemType x){
if(Q.rear == Q.front && tag == 1)
return false;
Q.data[Q.rear] = x;
Q.rear = (Q.rear+1)%MaxSize;
Q.tag = 1;
return true;
}
// 出队
bool DeQueue(SqQueue &Q, ElemType &x){
if(Q.rear == Q.front && tag == 0)
return false;
x = Q.data[Q.front];
Q.front = (Q.front+1)%MaxSize;
Q.tag = 0;
return true;
}
【链队列的定义】
// 链式队列结点
typedef struct LinkNode{
ElemType data;
struct LinkNode *next;
}
// 链式队列
typedef struct{
// 头指针和尾指针
LinkNode *front, *rear;
}LinkQueue;
【 链队列的初始化(带头结点)】
typedef struct LinkNode{
ElemType data;
struct LinkNode *next;
}LinkNode;
typedef struct{
LinkNode *front, *rear;
}LinkQueue;
// 初始化队列
void InitQueue(LinkQueue &Q){
// 初始化时,front、rear都指向头结点
Q.front = Q.rear = (LinkNode *)malloc(sizeof(LinkNode));
Q.front -> next = NULL;
}
// 判断队列是否为空
bool IsEmpty(LinkQueue Q){
if(Q.front == Q.rear)
return true;
else
return false;
}
【入队出队(带头结点)】
// 新元素入队
void EnQueue(LinkQueue &Q, ElemType x){
LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
Q.rear->next = s;
Q.rear = s;
}
// 队头元素出队
bool DeQueue(LinkQueue &Q, ElemType &x){
if(Q.front == Q.rear)
return false;
LinkNode *p = Q.front->next;
x = p->data;
Q.front->next = p->next;
// 如果p是最后一个结点,则将队头指针也指向NULL
if(Q.rear == p)
Q.rear = Q.front;
free(p);
return true;
}
【不带头结点的链队列操作】
typedef struct LinkNode{
ElemType data;
struct LinkNode *next;
}LinkNode;
typedef struct{
LinkNode *front, *rear;
}LinkQueue;
// 初始化队列
void InitQueue(LinkQueue &Q){
// 不带头结点的链队列初始化,头指针和尾指针都指向NULL
Q.front = NULL;
Q.rear = NULL;
}
// 判断队列是否为空
bool IsEmpty(LinkQueue Q){
if(Q.front == NULL)
return true;
else
return false;
}
// 新元素入队
void EnQueue(LinkQueue &Q, ElemType x){
LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
// 第一个元素入队时需要特别处理
if(Q.front == NULL){
Q.front = s;
Q.rear = s;
}else{
Q.rear->next = s;
Q.rear = s;
}
}
//队头元素出队
bool DeQueue(LinkQueue &Q, ElemType &x){
if(Q.front == NULL)
return false;
LinkNode *s = Q.front;
x = s->data;
if(Q.front == Q.rear){
Q.front = Q.rear = NULL;
}else{
Q.front = Q.front->next;
}
free(s);
return true;
}
双端队列定义
双端队列考点:判断输出序列的合法化
#define MaxSize 10
typedef struct{
char data[MaxSize];
int top;
}SqStack;
void InitStack(SqStack &S);
bool StackEmpty(SqStack &S);
bool Push(SqStack &S, char x);
bool Pop(SqStack &S, char &x);
// 判断长度为length的字符串str中的括号是否匹配
bool bracketCheck(char str[], int length){
SqStack S;
InitStack(S);
// 遍历str
for(int i=0; i
中缀表达式转后缀表达式-手算
步骤1: 确定中缀表达式中各个运算符的运算顺序
步骤2: 选择下一个运算符,按照[左操作数 右操作数 运算符]的方式组合成一个新的操作数
步骤3: 如果还有运算符没被处理,继续步骤2
“左优先”原则: 只要左边的运算符能先计算,就优先算左边的 (保证运算顺序唯一);
中缀:A + B - C * D / E + F
① ④ ② ③ ⑤
后缀:A B + C D * E / - F +
后缀表达式的计算—手算:
从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应的运算,合体为一个操作数
后缀表达式的计算—机算
用栈实现后缀表达式的计算(栈用来存放当前暂时不能确定运算次序的操作数)
步骤1: 从左往后扫描下一个元素,直到处理完所有元素;
步骤2: 若扫描到操作数,则压入栈,并回到步骤1;否则执行步骤3;
步骤3: 若扫描到运算符,则弹出两个栈顶元素,执行相应的运算,运算结果压回栈顶,回到步骤1;
中缀表达式转后缀表达式(机算)
初始化一个栈,用于保存暂时还不能确定运算顺序的运算符从左到右处理各个元素,直到末尾。可能遇到三种情况:
1.遇到操作数:直接加入后缀表达式。
2.遇到界限符:遇到“(”直接入栈;遇到“)”则依次弹出栈内运算符并加入后缀表达式,直到 弹出“(”为止。注意:“(”不加入后缀表达式。
3.遇到运算符:依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式, 若碰到“(” 或栈空则停止。之后再把当前运算符入栈。
#define MaxSize 40
typedef struct{
char data[MaxSize];
int top;
}SqStack;
typedef struct{
char data[MaxSize];
int front,rear;
}SqQueue;
void InitStack(SqStack &S);
bool StackEmpty(SqStack S);
bool Push(SqStack &S, char x);
bool Pop(SqStack &S, char &x);
void InitQueue(SqQueue &Q);
bool EnQueue(LQueue &Q, char x);
bool DeQueue(LQueue &Q, char &x);
bool QueueEmpty(SqQueue Q);
// 判断元素ch是否入栈
int JudgeEnStack(SqStack &S, char ch){
char tp = S.data[S->top];
// 如果ch是a~z则返回-1
if(ch >= 'a' && ch <= 'z')
return -1;
// 如果ch是+、-、*、/且栈顶元素优先级大于等于ch则返回0
else if(ch == '+' && (tp == '+' || tp == '-' || tp == '*' || tp == '/'))
return 0;
else if(ch == '-' && (tp == '+' || tp == '-' || tp == '*' || tp == '/'))
return 0;
else if(ch == '*' && (tp == '*' || tp == '/'))
return 0;
else if(ch == '/' && (tp == '*' || tp == '/'))
return 0;
// 如果ch是右括号则返回2
else if(ch == ')')
return 2;
// 其他情况ch入栈,返回1
else return 1;
}
// 中缀表达式转后缀表达式
int main(int argc, char const *argv[]) {
SqStack S;
SqQueue Q;
InitStack(S);
InitQueue(Q);
char ch;
printf("请输入表达式,以“#”结束:");
scanf("%c", &ch);
while (ch != '#'){
// 当栈为空时
if(StackEmpty(&S)){
// 如果输入的是数即a~z,直接入队
if(ch >= 'a' && ch <= 'z')
EnQueue(Q, ch);
// 如果输入的是运算符,直接入栈
else
Puch(S, ch);
}else{
// 当栈非空时,判断ch是否需要入栈
int n = JudgeEnStack(S, ch);
// 当输入是数字时直接入队
if(n == -1){
EnQueue(Q, ch);
}else if(n == 0){
// 当输入是运算符且运算符优先级不高于栈顶元素时
while (1){
// 取栈顶元素入队
char tp;
Pop(S, tp);
EnQueue(Q, tp);
// 再次判断是否需要入栈
n = JudgeEnStack(S, ch);
// 当栈头优先级低于输入运算符或者栈头为‘)’时,入栈并跳出循环
if(n != 0){
EnStack(S, ch);
break;
}
}
}else if(n == 2){
// 当出现‘)’时 将()中间的运算符全部出栈入队
while(1){
char tp;
Pop(S, tp);
if(tp == '(')
break;
else
EnQueue(Q, tp);
}
}else{
// 当运算符优先级高于栈顶元素或出现‘(’时直接入栈
Push(S, ch);
}
}
scanf("%c", &ch);
}
// 将最后栈中剩余的运算符出栈入队
while (!StackEmpty(S)){
char tp;
Pop(S, tp);
EnQueue(Q, tp);
}
// 输出队中元素
while (!QueueEmpety(Q)){
printf("%c ", DeQueue(Q));
}
return 0;
}
用栈实现中缀表达式的计算:
1.初始化两个栈,操作数栈和运算符栈;
2.若扫描到操作数,压入操作数栈;
3.若扫描到运算符或界限符,则按照“中缀转后缀”相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算,运算结果再压回操作数栈)
函数调用的特点:最后被调用的函数最先执行结束(LIFO)
函数调用时,需要用一个栈存储:
递归调用时,函数调用栈称为 “递归工作栈”:
缺点:太多层递归可能回导致栈溢出;适合用“递归”算法解决:可以把原始问题转换为属性相同,但规模较小的问题
一维数组的存储:各数组元素大小相同,且物理上连续存放。设起始地址为LOC,则数组元素的存放地址 = LOC + i * sizeof(ElemType) (0≤i<10)
二维数组的存储 :
1. M行N列的二维数组中,设起始地址为LOC,若按行优先存储,则的存储地址 =
2. M行N列的二维数组中,设起始地址为 LOC,若按列优先存储,则的存储地址 =
对称矩阵的压缩存储:若n阶方阵中任意一个元素,都有则该矩阵为对称矩阵,对于对称矩阵,只需存储主对角线+下三角区。若按照行优先原则将各元素存入一维数组中,即存入到数组中,那么数组共有个元素。对于k,有:
下三角矩阵:除了主对角线和下三角区,其余的元素都相同。
上三角矩阵:除了主对角线和上三角区,其余的元素都相同。
压缩存储策略:按行优先原则将主对角线+下三角区存入一维数组中,并在最后一个位置存储常量。即存入到数组中,那么数组共有个元素。对于k,有:
三对角矩阵,又称带状矩阵: 当时,有。对于三对角矩阵,按行优先原则,只存储带状部分,即存入到数组中,那么。若已知数组下标k,则 。
稀疏矩阵的非零元素远远少于矩阵元素的个数。压缩存储策略:
例:
S="HelloWorld!"
T='iPhone 11 Pro Max?'
假设有串 T = '', S = 'iPhone 11 Pro Max?', W = 'Pro'
静态数组实现(定长顺序存储)
#define MAXLEN 255 //预定义最大串长为255
typedef struct{
char ch[MAXLEN]; //静态数组实现(定长顺序存储)
//每个分量存储一个字符
//每个char字符占1B
int length; //串的实际长度
}SString;
动态数组实现( 堆分配存储)
typedef struct{
char *ch; //按串长分配存储区,ch指向串的基地址
int length; //串的实际长度
}HString;
HString S;
S.ch = (char*)malloc(MAXLEN *sizeof(char));
S.length = 0;
#define MAXLEN 255
typedef struct{
char ch[MAXLEN];
int length;
}SString;
// 1. 求子串
bool SubString(SString &Sub, SString S, int pos, int len){
//子串范围越界
if (pos+len-1 > S.length)
return false;
for (int i=pos; i
朴素模式匹配算法(简单模式匹配算法) 思想:
串的朴素模式匹配算法代码实现:
// 在主串S中找到与模式串T相同的子串并返回其位序,否则返回0
int Index(SString S, SString T){
int k=1;
int i=k, j=1;
while(i<=S.length && j<=T.length){
if(S.ch[i] == T.ch[j]){
++i; ++j;
}else{
k++; i=k; j=1;
}
}
if(j>T.length)
return k;
else
return 0;
}
或者不用k的方式:
int Index(SString S, SString T){
int i=1; //扫描主串S
int j=1; //扫描模式串T
while(i<=S.length && j<=T.length){
if(S.ch[i] == T.ch[j]){
++i;
++j; //继续比较后继字符
}
else{
i = i-j+2;
j=1; //指针后退重新开始匹配
}
}
if(j>T.length)
return i-T.length;
else
return 0;
}
时间复杂度:设模式串长度为m,主串长度为n
算法思想
求模式串的next数组
KPM 算法代码实现:
// 获取模式串T的next[]数组
void getNext(SString T, int next[]){
int i=1, j=0;
next[1]=0;
while(iT.length)
return i-T.length;
else
return 0;
}
int main() {
SString S={"ababcabcd", 9};
SString T={"bcd", 3};
printf("%d ", Index_KPM(S, T)); //输出9
}
KPM 算法的进一步优化:改进 next 数组:
void getNextval(SString T, int nextval[]){
int i=1,j=0;
nextval[1]=0;
while(i
树:n(n>=0)个节点的有限集合,是一种逻辑结构,当n=0时为空树,且非空树满足:
树是一种递归的数据结构
非空树特点:
基本术语
属性:
有序树和无序树
森林是m(>=0)棵互不相交的树的集合。
二叉树是n (n>=0)个结点的有限集合:
特点:
满二叉树:一棵深度为k且有个结点的二叉树称为满二叉树。特点:
完全二叉树:
深度为k的具有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号为1~n的结点一一对应时,称之为完全二叉树。特点:
二叉排序树:一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:
平衡二叉树:树上任一结点的左子树和右子树的深度之差不超过1。
常见考点1:设非空二叉树中度为0、1和2的结点个数分别为、,和,则(叶子结点比二分支结点多一个)
常见考点2:二叉树第层至多有个结点 (>=1);m叉树第层至多有个结点 (>=1)
常见考点3:高度为h的二叉树至多有个结点(满二叉树);高度为h的m叉树至多结点
常见考点4:具有 n 个(n>0)结点的完全二叉树的高度 h 为或者。
常见考点5:对于完全二叉树,可以由总结点数 n 推出度为 0、1 和 2 的结点个数、、
推导过程:
因为::所以为奇数
又因为:
所以:若完全二叉树有偶数n个节点,则为1;为;为
若完全二叉树有奇数n个节点,则为0;为;为
二叉树的顺序存储:
二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来;
几个重要常考的基本操作:
若完全二叉树中共有n个结点,则
#define MaxSize 100
struct TreeNode{
ElemType value; //结点中的数据元素
bool isEmpty; //结点是否为空
}
main(){
TreeNode t[MaxSize];
for (int i=0; i
链式存储
//二叉树的结点
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; //作为根节点的左孩子
二又树的递归特性:
【1】要么是个空二叉树
【2】要么就是由“根节点+左子树+右子树”组成的二叉树
【二叉树的先中后遍历】
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); //右孩子入队
}
}
由二叉树的遍历序列构造二叉树:
1. 前序+中序遍历序列
2. 后序+中序遍历序列
3. 层序+中序遍历序列
线索二叉树的概念与作用
//线索二叉树结点
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; // 左、右线索标志
}ThreadNode, *ThreadTree;
中序线索化的存储
先序线索化的存储
后序线索化的存储
中序线索化:
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; //处理遍历的最后一个结点
}
}
先序线索化:
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
,则 next 为 p 的右子树中最左下结点。// 找到以p为根的子树中,第一个被中序遍历的结点
ThreadNode *FirstNode(ThreadNode *p){
// 循环找到最左下结点(不一定是叶结点)
while(p->ltag==0)
p=p->lchild;
return p;
}
// 在中序线索二叉树中找到结点p的后继结点
ThreadNode *NextNode(ThreadNode *p){
// 右子树中最左下的结点
if(p->rtag==0)
return FirstNode(p->rchild);
else
return p->rchild;
}
// 对中序线索二叉树进行中序循环(非递归方法实现)
void InOrder(ThreadNode *T){
for(ThreadNode *p=FirstNode(T); p!=NULL; p=NextNode(p)){
visit(p);
}
}
中序线索二叉树找到指定结点 * p 的中序前驱 pre:
p->ltag==1
,则pre = p->lchild
;p->ltag==0
,则 next 为 p 的左子树中最右下结点。// 找到以p为根的子树中,最后一个被中序遍历的结点
ThreadNode *LastNode(ThreadNode *p){
// 循环找到最右下结点(不一定是叶结点)
while(p->rtag==0)
p=p->rchild;
return p;
}
// 在中序线索二叉树中找到结点p的前驱结点
ThreadNode *PreNode(ThreadNode *p){
// 左子树中最右下的结点
if(p->ltag==0)
return LastNode(p->lchild);
else
return p->lchild;
}
// 对中序线索二叉树进行中序循环(非递归方法实现)
void RevOrder(ThreadNode *T){
for(ThreadNode *p=LastNode(T); p!=NULL; p=PreNode(p))
visit(p);
}
先序线索二叉树找到指定结点 * p 的先序后继 next:
p->rtag==1
,则next = p->rchild
;p->rtag==0
:
先序线索二叉树找到指定结点 * p 的先序前驱 pre:
后序线索二叉树找到指定结点 * p 的后序前驱 pre:
后序线索二叉树找到指定结点 * p 的后序后继 next:
双亲表示法(顺序存储):每个结点中保存指向双亲的“指针”。
//数据域:存放结点本身信息。
//双亲域:指示本结点的双亲结点在数组中的位置。
#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)
查询:
①优点-查指定结点的双亲很方便;
②缺点-查指定结点的孩子只能从头遍历,空数据导致遍历更慢;
优点: 查指定结点的双亲很方便
缺点:查指定结点的孩子只能从头遍历
孩子表示法(顺序+链式存储)
孩子表示法:顺序存储各个节点,每个结点中保存孩子链表头指针。
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; //第一个孩子和右兄弟结点
}CSNode, *CSTree;
树的先根遍历
若树非空,先访问根结点,再依次对每棵子树进行先根遍历;(与对应二叉树的先序遍历序列相同)
树的先根遍历序列与这棵树相应二叉树的先序序列相同。
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); //访问根节点
}
}
层序遍历(队列实现)
森林的遍历
二又排序树,又称二叉查找树(BST,Binary Search Tree)棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:
【二叉排序树的查找】
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(keykey) //值小于根结点值,在左子树上查找
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
【二叉排序树的删除】
先搜索找到目标结点:
查找长度:在查找运算中,需要对比关键字的次数称为查找长度,反映了查找操作时间复杂度
平衡二叉树(Balanced Binary Tree),简称平衡树(AVL树)--上任一结点的左子树和右子树的高度之差不超过1。
结点的平衡因子 = 左子树高 - 右子树高
//平衡二叉树结点
typedef struct AVLNode{
int key; //数据域
int balance; //平衡因子
struct AVLNode *lchild; *rchild;
}AVLNode, *AVLTree;
平衡二叉树的插入
调整最小不平衡子树(LL):由于在结点 A 的左孩子(L)的左子树(L)上插入了新结点,A的平衡因子由1增至 2,导致以 A 为根的子树失去平衡,需要一次向右的旋转操作。将 A 的左孩子 B 向右上旋转代替 A 成为根结点,将 A 结点向右下旋转成为 B 的右子树的根结点,而 B 的原右子树则作为 A 结点的左子树。
调整最小不平衡子树(RR):由于在结点 A 的右孩子(R)的右子树(R)上插入了新结点,A的平衡因子由 -1 减至 -2,导致以 A 为根的子树失去平衡,需要一次向左的旋转操作。将 A 的右孩子 B 向左上旋转代替 A成为根结点,将 A 结点向左下旋转成为 B 的左子树的根结点,而 B 的原左子树则作为 A 结点的右子树。
调整最小不平衡子树(LR):由于在 A 的左孩子(L)的右子树(R)上插入新结点,A 的平衡因子由 1 增至 2,导致以 A 为根的子树失去平衡,需要进行两次旋转操作,先左旋转后右旋转。先将 A 结点的左孩子 B 的右子树的根结点 C 向左上旋转提升到 B 结点的位置,然后再把该 C 结点向右上旋转提升到 A 结点的位置。
调整最小不平衡子树(RL):由于在 A 的右孩子(R)的左子树(L)上插入新结点,A 的平衡因子由 -1 减至 -2,导致以 A 为根的子树失去平衡,需要进行两次旋转操作,先右旋转后左旋转。先将A 结点的右孩子 B的左子树的根结点 C 向右上旋转提升到 B 结点的位置,然后再把该 C 结点向左上旋转提升到 A 结点的位置。
查找效率分析:若树高为h,则最坏情况下,查找一个关键字最多需要对比 h 次,即查找操作的时间复杂度不可能超过 O(h)。
由于平衡二叉树上任一结点的左子树和右子树的高度之差不超过 1,假如以表示深度为 h 的平衡树中含有的最少结点数,则有:
;且有:
1、哈夫曼树定义
2、哈夫曼树的构造(重点)
给定n个权值分别为w1, W2,..., w,的结点,构造哈夫曼树的算法描述如下:
构造哈夫曼树的注意事项:
3、哈杜曼编码(重点)
曼树得到哈夫曼编码--字符集中的每个字符作为一个叶子结点,各个字符出现的频度作为结点的权值,根据之前介绍的方法构造哈夫曼树 。
(1)图G由顶点集V和边集E组成,记为G=(V,E),其中V(G)表示图G中顶点的有限非空集;E(G)表示图G中顶点之间的关系 (边) 集合。若V={V, V2,...,Vn},则用V表示图G中顶点的个数,也称图G的阶,E={(u, v) l uV, vV},用|E|表示图G中边的条数。
(2)无向图:若E是无向边 (简称边) 的有限集合时,则图G为无向图。边真是顶点的无序对,记为(v,w)或(w,v),因为(v,w)=(w,v),其中v、w是顶点。可以说顶点w和顶点v互为邻援点。边(v,w)依附于顶点w和v,或者说边(v,w)和顶点v、w相关联。
(3)有向图:若E是有向边(也称弧)的有限集合时,则图G为有向图弧是顶点的有序对,记为 (4)简单图:① 不存在重复边; ② 不存在顶点到自身的边。 (5)多重图:图G中某两个结点之间的边数多于一条,又允许顶点通过同一条边和自己关联,则G为多重图。 (6)顶点的度 (7)其他的概念 邻接矩阵法代码实现 性能分析: 邻接矩阵的性质:设图 G 的邻接阵为 (矩阵元素为0或1),则的元素等于由点到顶点的长度为的路径的数目。 代码实现 邻接表的特点: 邻接表对比邻接矩阵: 【十字链表】 【邻接多重表】 邻接多重表是无向图的另一种链式存储结构。 四种存储方法比较: Adjacent(G, x, y):判断图 G 是否存在边 < x , y > 或 ( x , y )。 1、广度优先遍历 (Breadth-First-Search,BFS)要点: 2、广度优先遍历用到的操作: 3、广度优先遍历代码实现: 4、复杂度分析: 5、⼴度优先⽣成树:⼴度优先⽣成树由⼴度优先遍历过程确定。由于邻接表的表示⽅式不唯⼀,因此基于邻接表的⼴度优先⽣成树也不唯⼀。 1、深度优先遍历(Depth First Search),也有称为深度优先搜索,简称为DFS。 代码实现: 2、复杂度分析: 3、深度优先遍历序列: 4、深度优先⽣成树: 5、深度优先⽣成深林: 1、生成树:连通图的生成树是包含图中全部顶点的一个极小连通子图。 若图中顶点数为 n,则它的生成树含有 n-1 条边。对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成一个回路。 2、最⼩⽣成树(最⼩代价树):对于一个带权连通无向图,生成树不同,每棵树的权(即树中所有边上的权值之和)也可能不同。设R为G的所有生成树的集合,若T为R中边的权值之和最小的生成树,则T称为G的最小生成树(Minimum-Spannino-Tree,MST).。 3、求最小生成树的两种方法 Prim算法(普里姆):从某一个顶点开始构建生成树;每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。时间复杂度: O(V2)适合用于边稠密图。 Kruskal算法(克鲁斯卡尔):每次选择一条权值最小的边,使这条边的两头连通(原本已经连通的就不选)直到所有结点都连通。时间复杂度: O(lEllog2lEl )适合用于边稀疏图。 1、使用 BFS算法求无权图的最短路径问题,需要使用三个数组 2、代码实现: 代码实现 Floyd算法:求出每⼀对顶点之间的最短路径,使⽤动态规划思想,将问题的求解分为多个阶段。 Floyd算法可以⽤于负权值带权图,但是不能解决带有“负权回路”的图(有负权值的边组成回路),这种图有可能没有最短路径。 Floyd算法使用到两个矩阵: 代码实现: 4.最短路径算法比较: 1、有向⽆环图:若⼀个有向图中不存在环,则称为有向⽆环图,简称 DAG图(Directed Acyclic Graph)。 DAG描述表达式:((a+b)*(b*(c+d))+(c+d)*e)*((c+d)*e) 2、有向无环图描述表达式的解题步骤: 1、AOV网(Activity on Vertex Network,用顶点表示活动的网):用DAG图(有向无环图)表示一个工程。顶点表示活动,有向边 2、拓扑排序:在图论中,由⼀个有向⽆环图的顶点组成的序列,当且仅当满⾜下列条件时,称为该图的⼀个拓扑排序: 3、拓扑排序的实现: 4、代码实现拓扑排序(邻接表实现): 1、AOE 网:在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如 完成活动所需的时间),称之为⽤边表示活动的⽹络,简称 AOE ⽹ (Activity On Edge NetWork)。 2、AOE⽹具有以下两个性质: 3、在 AOE ⽹中仅有⼀个⼊度为 0 的顶点,称为开始顶点(源点),它表示整个⼯程的开始; 也仅有⼀个出度为 0 的顶点,称为结束顶点(汇点),它表示整个⼯程的结束。 4、求和活动和事件的最早和最迟发生时间 5、关键活动、关键路径的特性: 如下举例: 顺序查找,又叫“线性查找”,通常用于线性表算法。 思想:从头到 jio 个找 (或者反过来也OK) 代码实现: 哨兵方式代码实现: 用查找判定树分析ASL 一个成功结点的查找长度=自身所在层数 一个失败结点的查找长度 =其父节点所在 层数默认情况下,各种失败情况或成功情况都等概率发生 【折半查找概念】 折半查找代码实现: 分块查找所针对的情况:块内无序、块间有序。 索引表及顺序表代码 B树,⼜称多路平衡查找树,B树中所有结点的孩⼦个数的最⼤值称为B树的阶,通常⽤m表示。⼀棵m阶B树或为空树,或为满⾜如下特性的m叉树: 所有的叶结点都出现在同⼀层次上,并且不带信息(可以视为外部结点或类似于折半查找判定树的查找失败结点,实际上这些结点不存在,指向这些结点的指针为空)。 m阶B树的核⼼特性: B树的⾼度:含 n 个关键字的 m叉B树,最⼩⾼度、最⼤⾼度是多少? B树的查找: B树的插入:将关键字 key 插入到B树的过程: B树的删除: 一棵m阶的B+树需满足以下条件: (1)m阶B树:结点中的n个关键字对应n+1棵子树; 散列函数的构造方法 散列查找执行步骤如下 平均查找长度(ASL):散列表查找成功的平均查找长度即找到表中已有表项的平均比较次数;散列表查找失败的平均查找长度即找不到待查的表项但能找到插入位置的平均比较次数。 代码实现(不带哨兵): 代码实现(带哨兵): 对链表进行插入排序代码实现: 代码实现: 希尔排序代码实现: 冒泡排序代码实现: 快速排序代码实现: 简单选择排序代码实现: 对链表进行简单选择排序: 堆排序代码实现: 堆的插入:对于大(或小)根堆,要插入的元素放到表尾,然后与父节点对比,若新元素比父节点更大(或小),则将二者互换。新元素就这样一路==“上升”==,直到无法继续上升为止。 堆的删除:被删除的元素用堆底元素替换,然后让该元素不断==“下坠”==,直到无法下坠为止。 代码实现:
① 对于无向图:顶点v的度是指依附于该顶点的边的条数,记为TD(v)。
② 对于有向图:入度是以顶点v为终点的有向边的数目,记为ID(v);出度是以顶点v为起点的有向边的数目,记为OD(v),顶点v的度等于其入度和出度之和,即TD(v) = ID(v) + OD(v)。
6.2. 图的存储
6.2.1. 邻接矩阵
#define MaxVertexNum 100 //顶点数目的最大值
typedef char VertexType; //顶点的数据类型
typedef int EdgeType; //带权图中边上权值的数据类型
typedef struct{
VertexType Vex[MaxVertexNum]; //顶点表
EdgeType Edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表
int vexnum, arcnum; //图的当前顶点数和弧树
}MGraph;
(1)空间复杂度:,只和顶点数相关,和实际的边数无关。
(2)适合用于存储稠密图。
(3)无向图的邻接矩阵是对称矩阵,可以压缩存储 (只存储上三角区或者下三角区)。6.2.2. 邻接表
#define MAXVEX 100 //图中顶点数目的最大值
type char VertexType; //顶点类型应由用户定义
typedef int EdgeType; //边上的权值类型应由用户定义
/*边表结点*/
typedef struct EdgeNode{
int adjvex; //该弧所指向的顶点的下标或者位置
EdgeType weight; //权值,对于非网图可以不需要
struct EdgeNode *next; //指向下一个邻接点
}EdgeNode;
/*顶点表结点*/
typedef struct VertexNode{
Vertex data; //顶点域,存储顶点信息
EdgeNode *firstedge //边表头指针
}VertexNode, AdjList[MAXVEX];
/*邻接表*/
typedef struct{
AdjList adjList;
int numVertexes, numEdges; //图中当前顶点数和边数
}
邻接矩阵
邻接表
空间复杂度
无向图 ;有向图
适用场景
存储稠密图
存储稀疏图
表示方式
唯一
不唯一
计算度、入度、出度
遍历对应行或列
计算有向图的度、入度不方便,其余很方便
找相邻的边
遍历对应行或列
找有向图的入边不方便,其余很方便
6.2.3. 十字链表、临接多重表
#define MAX_VERTEX_NUM 20 //最大顶点数量
struct EBox{ //边结点
int i,j; //该边依附的两个顶点的位置(一维数组下标)
EBox *ilink,*jlink; //分别指向依附这两个顶点的下一条边
InfoType info; //边的权值
};
struct VexBox{
VertexType data;
EBox *firstedge; //指向第一条依附该顶点的边
};
struct AMLGraph{
VexBox adjmulist[MAX_VERTEX_NUM];
int vexnum,edgenum; //无向图的当前顶点数和边数
};
6.2.4. 图的基本操作
Neighbors(G, x):列出图 G 中与结点 x 邻接的边。
InsertVertex(G, x):在图 G 中插入顶点 x 。
DeleteVertex(G, x):从图 G 中删除顶点 x。
AddEdge(G, x, y):若无向边 ( x , y ) 或有向边 < x , y > 不存在,则向图 G 中添加该边。
RemoveEdge(G, x, y):若无向边 ( x , y ) (x, y)(x,y) 或有向边 < x , y >
FirstNeighbor(G, x):求图 G 中顶点 x 的第一个邻接点,若有则返回顶点号。若 x 没有邻接点或图中不存在 x,则返回 -1。
NextNeighbor(G, x, y):假设图 G 中顶点 y 是顶点 x 的一个邻接点,返回除 y 之外顶点 x 的下一个邻接点的顶点号,若 y 是 x 的最后一个邻接点,则返回 -1。
Get_edge_value(G, x, y):获取图 G 中边 ( x , y )或 < x , y > 对应的权值。
Set_edge_value(G, x, y, v):设置图 G 中边 ( x , y )或 < x , y >对应的权值为 v。6.3. 图的遍历
6.3.1. 广度优先遍历
(1)找到与一个顶点相邻的所有顶点;
(2)标记哪些顶点被访问过;
(3)需要一个辅助队列。
FirstNeighbor(G, x):求图 G 中顶点 x 的第⼀个邻接点,若有则返回顶点号;若 x 没有邻接点或图中不存在 x,则返回 -1。
NextNeighbor(G, x, y):假设图 G 中顶点 y 是顶点 x 的⼀个邻接点,返回除 y 之外顶点 x 的下⼀个邻接点的顶点号,若 y 是 x 的最后⼀个邻接点,则返回 -1。/*邻接矩阵的广度遍历算法*/
void BFSTraverse(MGraph G){
int i, j;
Queue Q;
for(i = 0; i
(1)空间复杂度: 最坏情况,辅助队列大小为 。
(2)对于邻接矩阵存储的图,访问个顶点需要的时间,查找每个顶点的邻接点都需要 的时间,而总共有个顶点,时间复杂度为。
(3)对于邻接表存储的图,访问个顶点需要的时间,查找各个顶点的邻接点共需要的时间,时间复杂度为。6.3.2. 深度优先遍历
bool visited[MAX_VERTEX_NUM]; //访问标记数组
/*从顶点出发,深度优先遍历图G*/
void DFS(Graph G, int v){
int w;
visit(v); //访问顶点
visited[v] = TRUE; //设已访问标记
//FirstNeighbor(G,v):求图G中顶点v的第一个邻接点,若有则返回顶点号,否则返回-1。
//NextNeighbor(G,v,w):假设图G中顶点w是顶点v的一个邻接点,返回除w外顶点v
for(w = FirstNeighbor(G, v); w>=0; w=NextNeighor(G, v, w)){
if(!visited[w]){ //w为u的尚未访问的邻接顶点
DFS(G, w);
}
}
}
/*对图进行深度优先遍历*/
void DFSTraverse(MGraph G){
int v;
for(v=0; v
(1)空间复杂度主要来自来⾃函数调⽤栈,最坏情况下递归深度为 ;最好情况为。
(2)对于邻接矩阵存储的图,访问个顶点需要的时间,查找每个顶点的邻接点都需要 的时间,而总共有个顶点,时间复杂度为。
(3)对于邻接表存储的图,访问个顶点需要的时间,查找各个顶点的邻接点共需要的时间,时间复杂度为。
6.4. 图的应用
6.4.1. 最小生成树
Prim 算法(普⾥姆)
Kruskal 算法(克鲁斯卡尔)
时间复杂度
$O(
V
适用场景
稠密图
稀疏图
6.4.2. 无权图的单源最短路径问题——BFS算法
d[]
数组用于记录顶点 u 到其他顶点的最短路径。path[]
数组用于记录最短路径从那个顶点过来。visited[]
数组用于记录是否被访问过。#define MAX_LENGTH 2147483647 //地图中最大距离,表示正无穷
// 求顶点u到其他顶点的最短路径
void BFS_MIN_Disrance(Graph G,int u){
for(i=0; i
6.4.3. 单源最短路径问题——Dijkstra算法
final[]
数组用于标记各顶点是否已找到最短路径。dist[]
数组用于记录各顶点到源顶点的最短路径长度。path[]
数组用于记录各顶点现在最短路径上的前驱。#define MAX_LENGTH = 2147483647;
// 求顶点u到其他顶点的最短路径
void BFS_MIN_Disrance(Graph G,int u){
for(int i=0; i
6.4.4. 各顶点间的最短路径问题——Floyd算法
dist[][]
:目前各顶点间的最短路径。path[][]
:两个顶点之间的中转点。int dist[MaxVertexNum][MaxVertexNum];
int path[MaxVertexNum][MaxVertexNum];
void Floyd(MGraph G){
int i,j,k;
// 初始化部分
for(i=0;i
BFS算法
Dijkstra算法
Floyd算法
无权图
✔
✔
✔
带权图
✘
✔
✔
带负权值的图
✘
✘
✔
带负权回路的图
✘
✘
✘
时间复杂度
O(|V|^2)或(|V|+|E|)
O(|V|^2)
O(|V|^3)
通常⽤于
求⽆权图的单源最短路径
求带权图的单源最短路径
求带权图中各顶点间的最短路径
6.4.5. 有向⽆环图描述表达式
6.4.6. 拓扑排序
#define MaxVertexNum 100 //图中顶点数目最大值
typedef struct ArcNode{ //边表结点
int adjvex; //该弧所指向的顶点位置
struct ArcNode *nextarc; //指向下一条弧的指针
}ArcNode;
typedef struct VNode{ //顶点表结点
VertexType data; //顶点信息
ArcNode *firstarc; //指向第一条依附该顶点的弧的指针
}VNode,AdjList[MaxVertexNum];
typedef struct{
AdjList vertices; //邻接表
int vexnum,arcnum; //图的顶点数和弧数
}Graph; //Graph是以邻接表存储的图类型
// 对图G进行拓扑排序
bool TopologicalSort(Graph G){
InitStack(S); //初始化栈,存储入度为0的顶点
for(int i=0;i
6.4.7. 关键路径
1.求所有事件的最早发生时间ve(): 按拓扑排序序列,依次求各个顶点的 ve(k)即事件的关键路劲。
2.求所有事件的最迟发生时间 vl(): 按逆拓扑排序序列,依次求各个顶点的 vl(k)即从后往前推(最后一个点的最早和最迟相等)。
3.求所有活动的最早发生时间 e(): 若边 < vk,v; > 表示活动 ai,则有 e(i) = ve(k)。
4.求所有活动的最迟发生时间 1(): 若边 < Vk,v; > 表示活动 a,则有 l(i) = vlj)- Weight(vk,v;).
5.求所有活动的时间余量 d(): d() =l() - e(i)=最早-最迟。
若关键活动耗时增加,则整个⼯程的⼯期将增⻓。
缩短关键活动的时间,可以缩短整个⼯程的⼯期。
当缩短到⼀定程度时,关键活动可能会变成⾮关键活动。
可能有多条关键路径,只提⾼⼀条关键路径上的关键活动速度并不能缩短整个⼯程的⼯期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短⼯期的⽬的。
第七章 查找
7.1 查找概念
7.2 顺序查找
typedef struct{ //查找表的数据结构(顺序表)
ElemType *elem; //动态数组基址
int TableLen; //表的长度
}SSTable;
//顺序查找
int Search_Seq(SSTable ST,ElemType key){
int i;
for(i=0;i
typedef struct{ //查找表的数据结构(顺序表)
ElemType *elem; //动态数组基址
int TableLen; //表的长度
}SSTable;
//顺序查找
int Search_Seq(SSTable ST,ElemType key){
ST.elem[0]=key;
int i;
for(i=ST.TableLen;ST.elem[i]!=key;--i)
// 查找成功返回数组下标,否则返回0
return i;
}
7.3 折半查找
typedef struct{
ElemType *elem;
int TableLen;
}SSTable;
// 折半查找
int Binary_Search(SSTable L,ElemType key){
int low=0,high=L.TableLen,mid;
while(low<=high){
mid=(low+high)/2;
if(L.elem[mid]==key)
return mid;
else if(L.elem[mid]>key)
high=mid-1; //从前半部分继续查找
else
low=mid+1; //从后半部分继续查找
}
return -1;
}
7.4 分块查找
// 索引表
typedef struct{
ElemType maxValue;
int low,high;
}Index;
// 顺序表存储实际元素
ElemType List[100];
7.5 红黑树
7.5.1 为什么要发明红黑树?
红黑树:适用于频繁插入、删除的场景,实用性更强。7.5.2 红黑树的定义
7.5.3 红黑树的插入
7.6 B树和B+树
7.6.1 B树
7.6.2 B树的基本操作
7.6.3 B+树
7.6.4 B树和B+树的比较
m阶B+树:结点中的n个关键字对应n棵子树。
(2)m阶B树:根节点的关键字数n[1,m-1]。其他结点的关键字数n[[m/2]-1,m-1];
m阶B+树:根节点的关键字数n[1,m]其他结点的关键字数n[[m/2],m]。
(3)m阶B树:在B树中,各结点中包含的关键字是不重复的;
m阶B+树:在B+树中,叶结点包含全部关键字非叶结点中出现过的关键字也会出现在叶结点中。
(4)m阶B树:B树的结点中都包含了关键字对应的记录的存储地址;
m阶B+树:在B+树中,叶结点包含信息,所有非叶结点仅起索引作用,非叶结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。7.7 散列查找及其性能分析
7.7.1 散列表的基本概念
7.7.2 散列查找及性能分析
第八章 排序
8.1. 排序的基本概念
内部排序: 排序期间元素都在内存中——关注如何使时间、空间复杂度更低。
外部排序: 排序期间元素无法全部同时存在内存中,必须在排序的过程中根据要求不断地在内、外存之间移动——关注如何使时间、空间复杂度更低,如何使读/写磁盘次数更少。8.2. 插入排序
8.2.1. 直接插入排序
// 对A[]数组中共n个元素进行插入排序
void InsertSort(int A[],int n){
int i,j,temp;
for(i=1; i
// 对A[]数组中共n个元素进行插入排序
void InsertSort(int A[], int n){
int i,j;
for(i=2; i<=n; i++){
if(A[i]
//对链表L进行插入排序
void InsertSort(LinkList &L){
LNode *p=L->next, *pre;
LNode *r=p->next;
p->next=NULL;
p=r;
while(p!=NULL){
r=p->next;
pre=L;
while(pre->next!=NULL && pre->next->data
8.2.2. 折半插入排序
//对A[]数组中共n个元素进行折半插入排序
void InsertSort(int A[], int n){
int i,j,low,high,mid;
for(i=2; i<=n; i++){
A[0]=A[i]; //将A[i]暂存到A[0]
low=1; high=i-1;
while(low<=high){ //折半查找
mid=(low+high)/2;
if(A[mid]>A[0])
high=mid-1;
else
low=mid+1;
}
for(j=i-1; j>high+1; --j)
A[j+1]=A[j];
A[high+1]=A[0];
}
}
8.2.3. 希尔排序
// 对A[]数组共n个元素进行希尔排序
void ShellSort(ElemType A[], int n){
int d,i,j;
for(d=n/2; d>=1; d=d/2){ //步长d递减
for(i=d+1; i<=n; ++i){
if(A[i]0 && A[0]
8.3. 交换排序
8.3.1. 冒泡排序
// 交换a和b的值
void swap(int &a, int &b){
int temp=a;
a=b;
b=temp;
}
// 对A[]数组共n个元素进行冒泡排序
void BubbleSort(int A[], int n){
for(int i=0; i
8.3.2. 快速排序
// 用第一个元素将数组A[]划分为两个部分
int Partition(int A[], int low, int high){
int pivot = A[low];
while(low
8.4. 选择排序
8.4.1. 简单选择排序
// 交换a和b的值
void swap(int &a, int &b){
int temp = a;
a = b;
b = temp;
}
// 对A[]数组共n个元素进行选择排序
void SelectSort(int A[], int n){
for(int i=0; i
void selectSort(LinkList &L){
LNode *h=L,*p,*q,*r,*s;
L=NULL;
while(h!=NULL){
p=s=h; q=r=NULL;
while(p!=NULL){
if(p->data>s->data){
s=p; r=q;
}
q=p; p=p->next;
}
if(s==h)
h=h->next;
else
r->next=s->next;
s->next=L; L=s;
}
}
8.4.2. 堆排序
// 对初始序列建立大根堆
void BuildMaxHeap(int A[], int len){
for(int i=len/2; i>0; i--) //从后往前调整所有非终端结点
HeadAdjust(A, i, len);
}
// 将以k为根的子树调整为大根堆
void HeadAdjust(int A[], int k, int len){
A[0] = A[k];
for(int i=2*k; i<=len; i*=2){ //沿k较大的子结点向下调整
if(i
8.5. 归并排序
// 辅助数组B
int *B=(int *)malloc(n*sizeof(int));
// A[low,...,mid],A[mid+1,...,high]各自有序,将这两个部分归并
void Merge(int A[], int low, int mid, int high){
int i,j,k;
for(k=low; k<=high; k++)
B[k]=A[k];
for(i=low, j=mid+1, k=i; i<=mid && j<= high; k++){
if(B[i]<=B[j])
A[k]=B[i++];
else
A[k]=B[j++];
}
while(i<=mid)
A[k++]=B[i++];
while(j<=high)
A[k++]=B[j++];
}
// 递归操作
void MergeSort(int A[], int low, int high){
if(low
8.6. 基数排序
8.7. 内部排序算法总结
8.7.1. 内部排序算法比较
算法种类
最好时间复杂度
最坏时间复杂度
平均时间复杂度
空间复杂度
稳定性
直接插入排序
稳定
冒泡排序
稳定
简单选择排序
不稳定
希尔排序
不稳定
快速排序
不稳定
堆排序
不稳定
2路归并排序
稳定
基数排序
稳定
8.7.2. 内部排序算法的应用
8.8. 外部排序
8.8.1. 外部排序的基本概念和方法
① 跟据内存缓冲区大小,将外存上的文件分成 r 个子文件,依次读入内存并利用内部排序方法对它们进行排序,并将排序后得到的有序子文件重新写回外存(归并段)。
② 对这些归并段进行 S 趟 k 路归并,使归并段(有序子文件)逐渐由小到大,直至得到整个有序文件为止,其中(需要在内存中分配k个输入缓冲区和1个输出缓冲区)
① 把k个归并段的块读入k个输入缓冲区。
② 用“归并排序”的方法从k个归并段中选出几个最小记录暂存到输出缓冲区中。
③ 当输出缓冲区满时,写出外存。
①增加归并路数k,但是需要增加相应的输入缓冲区,且每次从k个归并段中选出一个最小元素需要对比 (k-1) 次。
② 减少初始归并段数量r。8.8.2. 败者树
8.8.3. 置换-选择排序(生成初始归并段)
8.8.4. 最佳归并树
① (初始归并段数量 -1) % (k-1)= 0,说明刚好可以构成严格k叉树,此时不需要添加虚段
② (初始归并段数量 -1) % (k-1) = 0,则需要补充(k-1) - u 个虚段