《王道》数据结构笔记整理2024_2024版王道数据结构pdf,2024软件测试面试笔试总结

51 阅读45分钟

##### 2.3.6双链表


双链表中节点类型的描述:`



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; }


**双链表的插入操作**  
 后插操作  
 InsertNextDNode(p, s): 在p结点后插入s结点



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节点的后继节点



//删除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; }


注意:双链表不可随机存取,按位查找和按值查找操作都只能用遍历的方式实现,时间复杂度为O(n)  
 ![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c20144c988f34025835f5ef6094d49cf~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=FDPL%2FBWntIZni3La3foEz13KoZY%3D)


##### 2.3.7循环链表


**1.循环单链表**  
 **最后一个结点的指针不是NULL,而是指向头结点**



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)的时间复杂度;


==优点:==从表中任一节点出发均可找到表中其他结点。


**2.循环双链表**  
 **表头结点的prior指向表尾结点,表尾结点的next指向头结点**



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);


**双向循环链表:**  
 和单链的循环表类似,双向链表也可以有循环表,让头结点的前驱指针指向链表的最后一个结点,让最后一个结点的后继指针指向头结点。  
 ![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/40a9e10612894ae5af493ec41aaa64e4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=5XEl4EorXKHgvNLbeOjl6G%2FFkXI%3D)  
 结构定义:



typedef struct DuLNode{ Elemtype data; struct DulNode *prior,*next;

} DuLNode,*DuLinkList;


##### 2.3.8静态链表


**1. 定义:**


* 单链表:各个结点散落在内存中的各个角落,每个结点有指向下一个节点的指针(下一个结点在内存中的地址);
* 静态链表:用数组的方式来描述线性表的链式存储结构: 分配一整片连续的内存空间,各个结点集中安置,包括了——数据元素and下一个结点的数组下标(游标)


	+ 其中数组下标为0的结点充当"头结点"
	+ 游标为-1表示已经到达表尾
	+ 若每个数据元素为4B,每个游标为4B,则每个结点共8B;假设起始地址为addr,则数据下标为2的存放地址为:addr+8\*2
	+ **注意:** 数组下标——物理顺序,位序——逻辑顺序;
	+ 优点:增、删操作不需要大量移动元素;
	+ 缺点:不能随机存取,只能从头结点开始依次往后查找,容量固定不变!


**2.静态链表用代码表示:**



#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型数组;


注意:SLinkList a 强调a是静态链表;struct Node a 强调a是一个Node型数组;


**3.静态链表基本操作的实现**


* 初始化静态链表:把a[0]的next设为-1
* 查找某个位序(不是数组下标,位序是各个结点在逻辑上的顺序)的结点:从头结点出发挨个往后遍历结点,时间复杂度O=(n)
* 在位序为i上插入结点:① 找到一个空的结点,存入数据元素;② 从头结点出发找到位序为i-1的结点;③修改新结点的next;④ 修改i-1号结点的next;
* 删除某个结点:① 从头结点出发找到前驱结点;② 修改前驱节点的游标;③ 被删除节点next设为-2;


##### 2.3.9 顺序表和链表的比较


**1.逻辑结构**


* 顺序表和链表都属于线性表,都是线性结构


**2.存储结构**


* 顺序表:顺序存储


	+ 优点:支持随机存取,存储密度高
	+ 缺点:大片连续空间分配不方便,改变容量不方便
* 链表:链式存储


	+ 优点:离散的小空间分配方便,改变容量方便
	+ 缺点:不可随机存取,存储密度低


**3. 基本操作 - 创建**


* 顺序表:需要预分配大片连续空间。若分配空间过小,则之后不方便拓展容量;若分配空间过大,则浪费内存资源;
* 静态分配:静态数组,容量不可改变
* 动态分配:动态数组,容量可以改变,但是需要移动大量元素,时间代价高(malloc(),free())
* 链表:只需要分配一个头结点或者只声明一个头指针


**4. 基本操作 - 销毁**


* 顺序表:修改 Length = 0


	+ 静态数组——系统自动回收空间



typedef struct{ ElemType *data; int MaxSize; int length; }SeqList;


* 动态分配:动态数组——需要手动free()



//创 L.data = (ELemType *)malloc(sizeof(ElemType) *InitSize) //销 free(L.data);

//!malloc() 和 free() 必须成对出现


**5.基本操作-增/删**


* 顺序表:插入/删除元素要将后续元素后移/前移;时间复杂度=O(n),时间开销主要来自于移动元素;
* 链表:插入/删除元素只需要修改指针;时间复杂度=O(n),时间开销主要来自查找目标元素


**6.基本操作-查**


* 顺序表


	+ 按位查找:O(1)
* 按值查找:O(n),若表内元素有序,可在O(log2n)时间内找到
* 链表


	+ 按位查找:O(n)
	+ 按值查找:O(n)


##### 2.3.10顺序、链式、静态、动态四种存储方式的比较


1. 顺序存储的固有特点:  
 逻辑顺序与物理顺序一直,本质上是用数组存储线性表的各个元素(即随机存取);存储密度大,存储空间利用率高。
2. 链式存储的固有特点:  
 元素之间的关系采用这些元素所在的节点的“指针”信息表示(插、删不需要移动节点)。
3. 静态存储的固有特点:  
 在程序运行的过程中不要考虑追加内存的分配问题。
4. 动态存储的固有特点:  
 可动态分配内存;有效的利用内存资源,使程序具有可扩展性。


##### 2.3.11链表的逆置算法


思路:先将链表一个一个的断开,再将断开的链表插入到原来的队列中  
 ![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/efc56cdde9c64197a1aaace1bb32acea~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=ifmlhmqQ2LQrvC4wAOqr4NCSSP8%3D)


### 第三章:栈和队列


#### 3.1栈(stack)


##### 3.1.1栈的基本概念


### 标题


**1.栈的定义**


* 栈是特殊的线性表:只允许在一端进行插入或删- 除操作, 其逻辑结构与普通线性表相同;
* 栈顶(Top):允许进行插入和删除的一端 (最上面的为栈顶元素);
* 栈底(Bottom):固定的,不允许进行插入和删除的一端 (最下面的为栈底元素);
* 空栈:不含任何元素的空表;
* 特点:后进先出(后进栈的元素先出栈);
* 缺点:栈的大小不可变,解决方法——共享栈;


**2.栈的基本操作**


**“创建&销毁”**


* InitStack(&S) 初始化栈:构造一个空栈S,分配内存空间;
* DestroyStack(&S) 销毁栈:销毁并释放栈S所占用的内存空间;


**“增&删”**


* Push(&S, x) 进栈:若栈S未满,则将x加入使其成为新栈顶;
* Pop(&S, &x) 出栈:若栈S非空,则弹出(删除)栈顶元素,并用x返回;  
 **“查&其他”**
* GetTop(S, &x) 读取栈顶元素:若栈S非空,则用x-返回栈顶元素;(栈的使用场景大多只访问栈顶元素);
* StackEmpty(S) 判空: 断一个栈S是否为空,若S为空,则返回true,否则返回false;


##### 3.1.2 栈的顺序存储


**1.顺序栈的定义**



#define MaxSize 10 //定义栈中元素的最大个数

typedef struct{ ElemType data[MaxSize]; //静态数组存放栈中元素 int top; //栈顶元素 }SqStack;

void testStack(){ SqStack S; //声明一个顺序栈(分配空间) //连续的存储空间大小为 MaxSize*sizeof(ElemType) }


**2.顺序栈的基本操作**



#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.top = S.top + 1;    //指针先加1
S.data[S.top] = x;    //新元素入栈

/\*

S.data[++S.top] = x; */ return true; }

//出栈 bool Pop(SqStack &x, ElemType &x){ if(S.top == -1) //栈空 return false;

x = S.data[S.top];       //先出栈
S.top = S.top - 1;       //栈顶指针减1
return true;

/\*

x = S.data[S.top--]; */

//只是逻辑上的删除,数据依然残留在内存里

}

//读栈顶元素 bool GetTop(SqStack S, ElemType &x){ if(S.top == -1) return false;

x = S.data[S.top];      //x记录栈顶元素
return true; 

}

void testStack(){ SqStack S; //声明一个顺序栈(分配空间) InitStack(S); //... }


\*\*注意:\*\*也可以初始化时定义 S.top = 0 :top指针指向下一个可以插入元素的位置(栈顶元素的后一个位置);


* 进栈操作 :栈不满时,栈顶指针先加1,再送值到栈顶元素。`S.data[S.top++] = x;`
* 出栈操作:栈非空时,先取栈顶元素值,再将栈顶指针减1。`x = S.data[–S.top];
* 栈空条件:S.top==-1
* 栈满条件:S.top==MaxSize-1
* 栈长:S.top+1


**3.共享栈**  
 \*\*定义:\*\*利用栈底位置相对不变的特性,可以让两个顺序栈共享一个一维数组空间,将两个栈的栈底 分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸。


* 存取数据的时间复杂度均为O(1)



#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;
}


栈满条件:`top1-top0=1`


##### 3.1.3栈的链式存储


1.定义:采用链式存储的栈称为链栈。  
 2.优点:链栈的优点是便于多个栈共享存储空间和提高其效率,且不存在栈满上溢的情况。  
 3.特点:


* 进栈和出栈都只能在栈顶一端进行(链头作为栈顶)
* 链表的头部作为栈顶,意味着:  
 1. 在实现数据"入栈"操作时,需要将数据从链表的头部插入;  
 2. 在实现数据"出栈"操作时,需要删除链表头部的首元节点;  
 **因此,链栈实际上就是一个只能采用头插法插入或删除数据的链表;**  
 栈的链式存储结构可描述为:



typedef struct Linknode{ ElemType data; //数据域 struct Linknode *next; //指针域 }*LiStack; //栈类型的定义


4. 栈的基本操作:


* 初始化
* 进栈
* 出栈
* 获取栈顶元素
* 判空、判满  
 **带头结点的链栈基本操作:**



#include<stdio.h>

struct Linknode{ int data; //数据域 Linknode *next; //指针域 }Linknode,*LiStack;

typedef Linknode *Node; //结点结构体指针变量 typedef Node List; //结点结构体头指针变量

//1. 初始化 void InitStack(LiStack &L){ //L为头指针 L = new Linknode; L->next = NULL; }

//2.判栈空 bool isEmpty(LiStack &L){ if(L->next == NULL){ return true; } else return false; }

//3. 进栈(:链栈基本上不会出现栈满的情况) void pushStack(LiStack &L, int x){ Linknode s; //创建存储新元素的结点 s = new Linknode; s->data = x;

//头插法
s->next = L->next;
L->next = s;

}

//4.出栈 bool popStack(LiStack &L, int &x){ Linknode s; if(L->next == NULL) //栈空不能出栈 return false;

s = L->next;
x = s->data;
L->next = L->next->next;
delete(s);

return true;

}


**不带头结点的链栈基本操作:**



#include<stdio.h>

struct Linknode{ int data; //数据域 Linknode *next; //指针域 }Linknode,*LiStack;

typedef Linknode *Node; //结点结构体指针变量 typedef Node List; //结点结构体头指针变量

//1.初始化 void initStack(LiStack &L){ L=NULL; }

//2.判栈空 bool isEmpty(LiStack &L){ if(L == NULL) return true; else teturn false; }

//3.进栈 void pushStack(LiStack &L, int x){ Linknode s; //创建存储新元素的结点 s = new Linknode;

s->next = L;
L = s;

}

//4.出栈 bool popStack(LiStack &L, int &x){ Linknode s; if(L = NULL) //栈空不出栈 return false;

s = L;
x = s->data;
L = L->next;
delete(s);

return true;

}


#### 3.2队列(Queue)


##### 3.2.1队列的基本概念


1.定义:队列(Queue)简称队,是一种操作受限的线性表,只允许在表的一端进行插入,而在表的另一端进行删除。  
 2.特点


* 队列是操作受限的线性表,只允许在一端进行插入 (入队),另一端进行删除 (出队)
* 操作特性:先进先出 FIFO
* 队头:允许删除的一端
* 队尾:允许插入的一端
* 空队列:不含任何元素的空表  
 3.队列的基本操作  
 **“创建&销毁”**
* `InitQueue(&Q):` 初始化队列,构造一个空列表Q
* `DestroyQueue(&Q):` 销毁队列,并释放队列Q所占用的内存空间  
 **“增&删”**
* `EnQueue(&Q, x):` 入队,若队列Q未满,将x加入,使之成为新的队尾
* `DeQueue(&Q, &x):` 出队,若队列Q非空,删除队头元素,并用x返回  
 **“查&其他”**
* `GetHead(Q,&x):` 读队头元素,若队列Q非空,则将队头元素赋值给x
* `QueueEmpty(Q):` 判队列空,若队列Q为空,则返回


##### 3.2.2队列的顺序存储结构


* 队头指针:指向队头元素
* 队尾指针:指向队尾元素的下一个位置


1. 队列存储的基本操作



//队列的顺序存储类型

define MaxSize 10; //定义队列中元素的最大个数

typedef struct{ ElemType data[MaxSize]; //用静态数组存放队列元素 //连续的存储空间,大小为——MaxSize*sizeof(ElemType) int front, rear; //队头指针和队尾指针 }SqQueue;

//初始化队列 void InitQueue(SqQueue &Q){ //初始化时,队头、队尾指针指向0 Q.rear = Q.front = 0; }

void test{ SqQueue Q; //声明一个队列 InitQueue(Q); //... }

// 判空 bool QueueEmpty(SqQueue 0){ if(Q.rear == Q.front) //判空条件后 return true; else return false; }


2. 循环队列  
 定义:将循环队列臆造为一个环状的空间,即把存储队列元素的表从逻辑上视为一个环,称为循环队列。  
 基本操作:



a%b == a除以b的余数

初始:Q.front = Q.rear = 0;

队首指针进1:Q.front = (Q.front + 1) % MaxSize

队尾指针进1:Q.rear = (Q.rear + 1) % MaxSize —— 队尾指针后移,当移到最后一个后,下次移动会到第一个位置

队列长度:(Q.rear + MaxSize - Q.front) % MaxSize


**区分队空还是队满的情况:**  
 **方案一: 牺牲一个单元来区分队空和队满**  
 队尾指针的再下一个位置就是队头,即 `(Q.rear+1)%MaxSize == Q.front`


* 循环队列——入队:只能从队尾插入(判满使用方案一)



bool EnQueue(SqQueue &Q, ElemType x){ if((Q.rear+1)%MaxSize == Q.front) //队满 return false; Q.data[Q.rear] = x; //将x插入队尾 Q.rear = (Q.rear + 1) % MaxSize; //队尾指针加1取模

return true;

}


* 循环队列——出队:只能让队头元素出队



//出队,删除一个队头元素,用x返回 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;

}


* 循环队列——获得队头元素



bool GetHead(SqQueue &Q, ElemType &x){ if(Q.rear == Q.front) //队空报错 return false;

x = Q.data[Q.front];
return true;

}


**方案二: 不牺牲存储空间,设置size**  
 定义一个变量 `size`用于记录队列此时记录了几个数据元素,初始化 `size = 0`,进队成功 `size++`,出队成功`size--`,根据size的值判断队满与队空


队满条件:`size == MaxSize`


队空条件:`size == 0`



define MaxSize 10;

typedef struct{ ElemType data[MaxSize];
int front, rear;
int size; //队列当前长度 }SqQueue;

//初始化队列 void InitQueue(SqQueue &Q){ Q.rear = Q.front = 0; size = 0; }


**方案三: 不牺牲存储空间,设置tag**  
 定义一个变量 `tag`,`tag = 0` --最近进行的是删除操作;`tag = 1` --最近进行的是插入操作;


每次删除操作成功时,都令`tag = 0`;只有删除操作,才可能导致队空;  
 每次插入操作成功时,都令`tag = 1`;只有插入操作,才可能导致队满;  
 队满条件:`Q.front == Q.rear && tag == 1`


队空条件:`Q.front == Q.rear && tag == 0`



define MaxSize 10;

typedef struct{ ElemType data[MaxSize];
int front, rear;
int tag; //最近进行的是删除or插入 }SqQueue;


##### 3.2.3队列的链式存储结构


1.定义:队列的链式表示称为链队列,它实际上是一个同时带有队头指针和队尾指针的单链表。  
 链队列:用链表表示的队列,是限制仅在表头删除和表尾插入的单链表。  
 队列的链式存储类型可描述为:



typedef struct LinkNode{ //链式队列结点 ElemType data; struct LinkNode *next; }

typedef struct{ //链式队列 LinkNode *front, *rear; //队列的队头和队尾指针 }LinkQueue;


2.链式队列的基本操作——**带头结点**


* 初始化 & 判空



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) //也可用 Q.front -> next == NULL return true; else return false; }


* 入队操作



//新元素入队 (表尾进行) void EnQueue(LinkQueue &Q, ElemType x){ LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode)); //申请一个新结点 s->data = x; s->next = NULL; //s作为最后一个结点,指针域指向NULL Q.rear->next = s; //新结点插入到当前的rear之后 Q.rear = s; //表尾指针指向新的表尾 }


* 出队操作



//队头元素出队 bool DeQueue(LinkQueue &Q, ElemType &x){ if(Q.front == Q.rear) return false; //空队

LinkNode \*p = Q.front->next;         //p指针指向即将删除的结点 (头结点所指向的结点)
x = p->data;
Q.front->next = p->next;             //修改头结点的next指针
if(Q.rear == p)                      //此次是最后一个结点出队
    Q.rear = Q.front;                //修改rear指针
free(p);                             //释放结点空间

return true;

}


* 队列满的条件  
 顺序存储:预分配存储空间


链式存储:一般不会队满,除非内存不足


* 计算链队长度 (遍历链队)  
 设置一个`int length` 记录链式队列长度
* 初始化 & 判空
* 入队操作



//新元素入队 (表尾进行) 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;           //新结点插入到rear结点之后
    Q.rear = s;                 //修改rear指针指向新的表尾结点
}

}


##### 3.2.4双端队列


1.定义:双端队列是指允许两端都可以进行入队和出队操作的队列


* 双端队列允许从两端插入、两端删除的线性表;
* 如果只使用其中一端的插入、删除操作,则等同于栈;
* 输入受限的双端队列:允许一端插入,两端删除的线性表;
* 输出受限的双端队列:允许两端插入,一端删除的线性表;


![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c6fa155143754301a9c019d9a1efb23d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=9%2BVIXqc7p3nHECJ%2BkJHMylDmJSk%3D)


##### 3.2.5循环队列


利用一组地址连续的存储单元依次存放队列中的数据元素。因为队头和队尾的位置是变化的。所以:设头、尾指针。


求循环队列的长度:  
 ![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ccb97bdd22314a48b1e210abfe59ed3d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=2jtr2wAgCF16dkwlCnRq9ZgQYuQ%3D)


#### 3.3栈的应用


##### 3.3.1栈在括号匹配中的应用


用栈实现括号匹配


* ((())) 最后出现的左括号最先被匹配 (栈的特性—LIFO);
* 遇到左括号就入栈;
* 遇到右括号,就“消耗”一个左括号 (出栈);  
 匹配失败情况:
* 扫描到右括号且栈空,则该右括号单身;
* 扫描完所有括号后,栈非空,则该左括号单身;
* 左右括号不匹配;


代码:



#define MaxSize 10

typedef struct{ char data[MaxSize]; int top; } SqStack;

//初始化栈 InitStack(SqStack &S)

//判断栈是否为空 bool StackEmpty(SqStack &S)

//新元素入栈 bool Push(SqStack &S, char x)

//栈顶元素出栈,用x返回 bool Pop(SqStack &S, char &x)

bool bracketCheck(char str[], int length){ SqStack S; //声明 InitStack(S); //初始化栈

for(int i=0; i<length; i++){
    if(str[i] == '(' || str[i] == '[' || str[i] == '{'){
        Push(S, str[i]);       //扫描到左括号,入栈
    }else{
        if(StackEmpty(S))      //扫描到右括号,且当前栈空
            return false;      //匹配失败
        
        char topElem;          //存储栈顶元素
        Pop(S, topElem);       //栈顶元素出栈

        if(str[i] == ')' && topElem != '(' )
            return false;

        if(str[i] == ']' && topElem != '[' )
            return false;

        if(str[i] == '}' && topElem != '{' )
            return false;       
    }
}

StackEmpty(S);                //栈空说明匹配成功

}


##### 3.3.2栈在表达式求值中的应用


**1. 中缀表达式 (需要界限符)**  
 运算符在两个操作数中间:



① a + b ② a + b - c ③ a + b - c*d ④ ((15 ÷ (7-(1+1)))×3)-(2+(1+1)) ⑤ A + B × (C - D) - E ÷ F


**2. 后缀表达式 (逆波兰表达式)**  
 运算符在两个操作数后面:



① a b + ② ab+ c - / a bc- + ③ ab+ cd* - ④ 15 7 1 1 + - ÷ 3 × 2 1 1 + + - ⑤ A B C D - × + E F ÷ - (机算结果) A B C D - × E F ÷ - + (不选择)


中缀表达式转后缀表达式-手算  
 步骤1: 确定中缀表达式中各个运算符的运算顺序


步骤2: 选择下一个运算符,按照[左操作数 右操作数 运算符]的方式组合成一个新的操作数


步骤3: 如果还有运算符没被处理,继续步骤2


“左优先”原则: 只要左边的运算符能先计算,就优先算左边的 (保证运算顺序唯一);



中缀:A + B - C * D / E + F ① ④ ② ③ ⑤
后缀:A B + C D * E / - F +


**重点:中缀表达式转后缀表达式-机算**  
 初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。从左到右处理各个元素,直到末尾。可能遇到三种情况:


遇到操作数: 直接加入后缀表达式。  
 遇到界限符: 遇到 ‘(’ 直接入栈; 遇到 ‘)’ 则依次弹出栈内运算符并加入后缀表达式,直到弹出 ‘(’ 为止。`注意: '(' 不加入后缀表达式。`  
 遇到运算符: 依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到 ‘(’ 或栈空则停止。之后再把当前运算符入栈。  
 按上述方法处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式。


后缀表达式的计算—手算:  
 从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应的运算,合体为一个操作数;



注意: 两个操作数的左右顺序


**重点:后缀表达式的计算—机算**  
 用栈实现后缀表达式的计算(栈用来存放当前暂时不能确定运算次序的操作数)


步骤1: 从左往后扫描下一个元素,直到处理完所有元素;


步骤2: 若扫描到操作数,则压入栈,并回到步骤1;否则执行步骤3;


步骤3: 若扫描到运算符,则弹出两个栈顶元素,执行相应的运算,运算结果压回栈顶,回到步骤1;


注意: 先出栈的是“右操作数”


**3.前缀表达式 (波兰表达式)**  
 运算符在两个操作数前面:



① + a b ② - +ab c ③ - +ab *cd


中缀表达式转前缀表达式—手算  
 步骤1: 确定中缀表达式中各个运算符的运算顺序


步骤2: 选择下一个运算符,按照[运算符 左操作数 右操作数]的方式组合成一个新的操作数


步骤3: 如果还有运算符没被处理,就继续执行步骤2


“右优先”原则: 只要右边的运算符能先计算,就优先算右边的;



中缀:A + B * (C - D) - E / F ⑤ ③ ② ④ ① 前缀:+ A - * B - C D / E F


前缀表达式的计算—机算  
 用栈实现前缀表达式的计算


步骤1: 从右往左扫描下一个元素,直到处理完所有元素;


步骤2: 若扫描到操作数则压入栈,并回到步骤1,否则执行步骤3


步骤3: 若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到步骤1;


注意: 先出栈的是“左操作数”


**4.中缀表达式的计算(用栈实现)**  
 两个算法的结合: 中缀转后缀 + 后缀表达式的求值


初始化两个栈,操作数栈 和运算符栈


若扫描到操作数,压人操作数栈


若扫描到运算符或界限符,则按照“中缀转后缀”相同的逻辑压入运算符栈 (期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈项元素并执行相应运算,运算结果再压回操作数栈)


##### 3.3.3栈在递归中的应用


函数调用的特点:最后被调用的函数最先执行结束(LIFO)


函数调用时,需要用一个栈存储:


* 调用返回地址
* 实参
* 局部变量


递归调用时,函数调用栈称为 “递归工作栈”:


* 每进入一层递归,就将递归调用所需信息压入栈顶;
* 每退出一层递归,就从栈顶弹出相应信息;


\*\*缺点:\*\*太多层递归可能回导致栈溢出;


适合用“递归”算法解决:可以把原始问题转换为属性相同,但规模较小的问题;


#### 3.4特殊矩阵的压缩存储


矩阵定义: 一个由m\*n个元素排成的m行(横向)n列(纵向)的表。  
 矩阵的常规存储:将矩阵描述为一个二维数组。


##### 3.4.1数组的存储结构


1.一维数组



Elemtype a[10];


各数组元素大小相同,物理上连续存放;


起始地址:`LOC`


数组下标:默认从0开始!


数组元素 `a[i]` 的存放地址 = `LOC + i × sizeof(ElemType)`


2.二维数组



Elemtype b[2][4]; //2行4列的二维数组


行优先/列优先存储优点:实现随机存储


起始地址:`LOC`


M行N列的二维数组 `b[M][N]` 中,`b[i][j]`的存储地址:


行优先存储: `LOC + (i×N + j) × sizeof(ElemType)`  
 列优先存储:`LOC + (j×M + i) × sizeof(ElemType)`


##### 3.4.2普通矩阵的存储


二维数组存储:


* 描述矩阵元素时,行、列号通常从1开始;
* 描述数组时,通常下标从 0 开始;


##### 3.4.3特殊矩阵的存储


特殊矩阵——压缩存储空间(只存有用的数据)


矩阵的压缩存储:为多个相同的非零元素只分配一个存储空间;对零元素不分配空间。


1. 对称矩阵(方阵)


在一个n阶方阵A中,若元素满足下述性值:![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2b5f7d6eb6344e93a984cb4b1dfb673b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=EtS%2B%2FJjqpEGsPCAYK%2F%2BzAzZDwHw%3D)  
 则称A为对称矩阵。  
 ![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/51ff486a969f4a829e26ee1ce04526bc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=e8N%2Foon830TCamdTd%2B%2BCX8Btaew%3D)


2. 三角矩阵(方阵)  
 以主对角线划分,三角矩阵有上(下)三角两种。上(下)三角矩阵的下(上)三角(不含主对角线)中的元素均为常数。在大多数情况下,三角矩阵常数为零。


![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e68c29c01cad42aa94d4d5512dab35ea~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=Xt0zZIejR3qp06fwEWUuKwmEAP4%3D)


3. 三对角矩阵(方阵)  
 ![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a94f01865b034c8ba87eb1d5c73bba25~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=YNgw%2B0eDrk4TeMMzqqZiFVvGyZo%3D)  
 对角矩阵可按行优先顺序或对角线的顺序,将其压缩存储到一维数组中,且也能找到每个非零元素和向量下标的对应关系。
4. 稀疏矩阵  
 设在m*n的矩阵中有t个非零元素,令c=t/(m*n),当c<=0.05时称为稀疏矩阵。  
 压缩存储原则:存各非零元的值、行列位置和矩阵的行列数。


* 顺序存储——三元组


![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7903fc32e76049a9ba291fed8ae36e0c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=jKHM28PkKzA8Ej%2Bo9Z8WcJJFRJg%3D)


* 链式存储——十字链表法  
 优点:它能够灵活得插入因运算而产生的新的非零元素,删除因运算而产生的新的零元素,实现矩阵的运算。  
 十字链表中结点的结构示意图:  
 ![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5b211762a06f40b29e5d15efe435a503~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=JTYSVmo4FATYWZPw1lZt1rKtdSU%3D)  
 right:用于链接同一行中的下一个非零元素;  
 down:用于链接同一列中的下一个非零元素。  
 ![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a9a7c9945177487580248b4f41d5ee7c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=Es7M9c%2BmAQENVZf2UHlTXaTX94U%3D)


### 第四章:串


#### 4.1串的定义和实现


##### 4.1.1串的定义


1. 串: 零个或多个字符组成的有限序列,如 `S = 'iPhone 11 Pro Max?';`
2. 串名:S是串名;
3. 串的长度:串中字符的个数n;
4. 空串:n=0时的串;
5. 子串:串中任意多个连续的字符组成的子序列称为该串的子串;
6. 主串:包含子串的串;
7. 字符在主串中的位置:某个字符在串中的序号(从1开始);
8. 子串在主串中的位置:子串的第一个字符在主串中的位置;
9. 空串 V.S 空格串:  
 M = ‘’ 是空串;  
 N = ’ ’ 是空格串;
10. 串 V.S 线性表:  
 串是特殊的线性表,数据元素之间呈线性关系(逻辑结构相似);  
 串的数据对象限定为字符集:中文字符、英文字符、数字字符、标点字符…  
 串的基本操作,如增删改除通常以子串为操作对象


##### 4.1.2串的基本操作


假设有串 `T = ''`, `S = 'iPhone 11 Pro Max?'`, `W = 'Pro'`


* `StrAssign(&T, chars)`: 赋值操作,把串T赋值为chars;
* `StrCopy(&T, S)`: 复制操作,把串S复制得到串T
* `StrEmpty(S)`: 判空操作,若S为空串,则返回TRUE,否则返回False;
* `StrLength(S)`: 求串长,返回串S的元素个数;
* `ClearString(&S)`: 清空操作,将S清为空串;
* `DestroyString(&S)`: 销毁串,将串S销毁——回收存储空间;
* `Concat(&T, S1, S2)`: 串联联接,用T返回由S1和S2联接而成的新串———可能会导致存储空间的扩展;  
 例:



Concat(&T, S, W)

T = ‘iPhone 11 Pro Max?Pro’


* `SubString(&Sub, S, pos, len)`: 求子串,用Sub返回串S的第pos个字符起长度为len的子串;



SubString(&T, S, 4, 6)

T = ‘one 11’


* `Index(S, T)`: 定位操作,若主串S中存在与串T值相同的子串,则返回它再主串S中第一次出现的位置,否则函数值为0* `StrCompare(S, T):` 串的比较操作,参照英文词典排序方式;若S > T,返回值>0; S = T,返回值=0 (需要两个串完全相同) ; S < T,返回值<0;


##### 4.1.3串的存储结构


1定长顺序存储表示



#define MAXLEN 255 //预定义最大串长为255

typedef struct{ char ch[MAXLEN]; //静态数组实现(定长顺序存储) //每个分量存储一个字符 //每个char字符占1B int length; //串的实际长度 }SString;


* 串长的两种表示法:
* 方案一:用一个额外的变量length来存放串的长度(保留ch[0]);
* 方案二:用ch[0]充当length;  
 优点:字符的位序和数组下标相同;
* 方案三:没有length变量,以字符’\0’表示结尾(对应ASCII码的0);  
 缺点:需要从头到尾遍历;
* \*\*方案四——最终使用方案:\*\*ch[0]废弃不用,声明int型变量length来存放串的长度(方案一与方案二的结合)
* 基本操作实现(基于方案四)



#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<pos+len; i++)
    Sub.cn[i-pos+1] = S.ch[i];

Sub.length = len;

return true;

}

// 2. 比较两个串的大小 int StrCompare(SString S, SString T){ for (int i; i<S.length && i<T.length; i++){ if(S.ch[i] != T.ch[i]) return S.ch[i] - T.ch[i]; } //扫描过的所有字符都相同,则长度长的串更大 return S.length - T.length; }

// 3. 定位操作 int Index(SString S, SString T){ int i=1; n = StrLength(S); m = StrLength(T); SString sub; //用于暂存子串

while(i<=n-m+1){
    SubString(Sub,S,i,m);
    if(StrCompare(Sub,T)!=0)
        ++i;
    else 
        return i;    // 返回子串在主串中的位置
}
return 0;            //S中不存在与T相等的子串

}


2.堆分配存储表示  
 \*\*堆存储结构的特点:\*\*仍以一组空间足够大的、地址连续的存储单元依次存放字符序列,但它们的存储空间实在程序执行过程种动态分配的 。  
 通常,C语言提供的串类型就是以这种存储方式实现的。由动态分配函数malloc()分配一块实际串长所需要的存储空间(“堆”),如果分配成功,则返回此空间的起始地址,作为串的基址。由free()释放串不再需要的空间,


\*\*堆存储结构的优点:\*\*堆存储结构既有顺序存储结构的特点,处理(随机取子串)方便,操作中对串长又没有任何限制,更显灵活,因此在串处理的应用程序中常被采用。



//动态数组实现 typedef struct{ char *ch; //按串长分配存储区,ch指向串的基地址 int length; //串的长度 }HString;

HString S; S.ch = (char *) malloc(MAXLINE * sizeof(char)); //基地址指针指向连续空间的起始位置 //malloc()需要手动free() S.length;


3.串的链式存储



typedef struct StringNode{ char ch; //每个结点存1个字符 struct StringNode *next; }StringNode, * String;


问题:存储密度低,每个字符1B,每个指针4B;  
 解决方案:每一个链表的结点存储多个字符——每个结点称为块——块链结构



typedef struct StringNode{ char ch[4]; //每个结点存多个个字符 struct StringNode *next; }StringNode, * String;


结合链表思考优缺点


* 存储分配角度:链式存储的字符串无需占用连续空间,存储空间分配更灵活;
* 操作角度:若要在字符串中插入或删除某些字符,则顺序存储方式需要移动大量字符,而链式存储不用;
* 若要按位序查找字符,则顺序存储支持随机访问,而链式存储只支持顺序访问;


#### 4.2串的模式匹配


**模式匹配**:子串的定位操作称为串的模式,它求的是子串(常称模式串)在主串中的位置。


##### 4.2.1朴素模式匹配算法



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; }


**时间复杂度分析:**


* 主串长度为n,模式串长度为m  
 最多比较`n-m+1`个子串  
 最坏时间复杂度 = `O(nm)`  
 每个子串都要对比m个字符(对比到最后一个字符才匹配不上),共要对比n-m+1个子串,复杂度 = `O((n-m+1)m) = O(nm - m^2 + m) = O(nm)`  
 PS:大多数时候,n>>m  
 最好时间复杂度 = `O(n)`  
 每个子串的第一个字符就匹配失败,共要对比n-m+1个子串,复杂度 = `O(n-m+1) = O(n)`


##### 4.2.2改进的模式匹配算法——KMP算法


* 不匹配的字符之前,一定是和模式串一致的;
* 根据模式串T,求出next数组(只与模式串有关,与主串无关),利用next数组进行匹配,当匹配失败时,主串的指针 i 不再回溯!  
 next数组是根据子串求出来的,当前面的字符串已知时如果有重复的,从当前的字符匹配即可。


1. 求next数组


* 作用:当模式串的第j个字符失配时,从模式串的第next[j]继续往后匹配;
* 对于任何模式串,当第1个字符不匹配时,只能匹配下一个子串,因此,next[1] = 0——表示模式串应右移一位,主串当前指针后移一位,再和模式串的第一字符进行比较;
* 对于任何模式串,当第2个字符不匹配时,应尝试匹配模式串的第一个字符,因此,next[2] = 0;  
 例:对于串 T = `'abaabc'`  
 ![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5b4ebf7708e84d48871528b16e4db678~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=Jn0dVai2sjLybeUKwqa7tLuT%2BVE%3D)


2. 利用`next数组`进行模式匹配



int Index_KMP(SString S, SString T, int next[]){ int i=1; //主串 int j=1; //模式串 while(i<S.length && j<=T.length){ if(j==0 || S.ch[i]==T.ch[j]){ //第一个元素匹配失败时 ++j; ++i; //继续比较后继字符 } else j=next[j] //模式串向右移动 } if(j>T.length) return i-T.length; //匹配成功 }


3. 时间复杂度分析


* 求next数组时间复杂度 = O(m)
* 模式匹配过程最坏时间复杂度 = O(n)
* KMP算法的最坏时间复杂度 = O(m+n)


**next数组的求法:**



> 
> 我们能确定next数组第一二位一定分别为0,1,后面求解每一位的next值时,根据前一位进行比较。  
>  从第三位开始,将前一位与其next值对应的内容进行比较,  
>  如果相等,则该位的next值就是前一位的next值加上1;  
>  如果不等,向前继续寻找next值对应的内容来与前一位进行比较,  
>  直到找到某个位上内容的next值对应的内容与前一位相等为止,  
>  则这个位对应的值加上1即为需求的next值;  
>  如果找到第一位都没有找到与前一位相等的内容,那么求解的位上的next值为1。
> 
> 
> 


注意下标都是从1开始的  
 传送门:<https://blog.csdn.net/m0_37482190/article/details/86667059>


### 第五章:树


#### 5.1树的基本概念


##### 5.1.1树的定义


树是n个结点的有限集。


* 空树:n=0
* 根结点、分支结点、叶子结点
* 非空树的特性
* 子树


##### 5.1.2基本术语


1. 结点之间的关系描述
	1. 祖先、子孙、双亲、兄弟…结点  
	 2. 路径、路径长度
2. 结点、树的属性描述  
 1. 结点的层次(深度)——从上往下  
 2. 结点的高度——从下往上  
 3. 树的高度——总共多少层  
 4. **结点的度**——有几个孩子  
 5. **树的度**——各结点的度的最大值
3. 有序树、无序树
4. 森林


##### 5.1.3树的性质


1. 树中的结点数等于所有结点的度数之和加1。
2. 度为m的树第i层上至多有m^i-1个结点
3. 度为m的数、m叉数的区别  
 ![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bdcbdb2839644cfe915f0c102dbd3c82~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=cMzUUGdtYCQX8lhKGMqKHtuy6Hs%3D)


#### 5.2二叉树的概念


##### 5.2.1 二叉树的定义与特性


**定义:**  
 二叉树是n(n>=0)个结点的有限集,它或者是空集(n=0),或者由一个根结点及两颗互不相交的分别称作这个根的左子树和右子树的二叉树组成。


**特点:**


1. 每个结点最多有俩孩子(二叉树中不存在度大于2的结点)。
2. 二叉树可以是空集合,根可以有空的左子树和空的右子树。
3. 二叉树有左右之分,次序不能颠倒。


**二叉树的性质:**  
 1.在二叉树的第i层上至多有2^(i-1)个结点(i>1)。  
 2.深度为k的二叉树至多有2^k-1个结点(k>=1)。  
 3.对任何一颗二叉树T,如果其叶子数为n0,度为2的结点数为n2,则n0=n2+1.  
 4. 具有n个结点的完全二叉树的深度为(log2N)+1。  
 5. ![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b9041a3d5f7d47acb9accdfa3f38b568~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=%2BD%2FYQtmkiibvSi15sd8lU9TmTlk%3D)


**注意:二叉树不是树的特殊情况,它们是两个概念。**


##### 5.2.2几种特殊的二叉树


1. 满二叉树:一颗深度为k且有2^k-1个结点的二叉树称为满二叉树。每一层上的结点数都达到最大。叶子全部在最低层。
2. 完全二叉树:深度为k的具有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号为1~n的结点一 一对应时,称之为完全二叉树。
3. 二叉排序树
4. 平衡二叉树


##### 5.2.3二叉树的存储结构


1. 顺序存储  
 二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来;



#define MaxSize 100

struct TreeNode{ ElemType value; //结点中的数据元素 bool isEmpty; //结点是否为空 }

main(){ TreeNode t[MaxSize]; for (int i=0; i<MaxSize; i++){ t[i].isEmpty = true; } }


2. 链式存储



//二叉树的结点

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二叉树的遍历


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); //递归遍历右子树 } }


2. 中序遍历(左根右)


* 若二叉树为空,不用操作
* 若二叉树非空:
	+ 先序遍历左子树
	+ 访问根节点
	+ 先序遍历右子树



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); //递归遍历右子树 } }


3. 后续遍历(左右根)


* 若二叉树为空,不用操作
* 若二叉树非空:  
 - 先序遍历左子树  
 - 先序遍历右子树  
 - 访问根节点



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); //访问根结点 } }


4. 二叉树的层次遍历  
 算法思想:


* 初始化一个辅助队列
* 根节点入队
* 若队列非空,则队头结点出队,访问该结点,依次将其左、右孩子插入队尾(如果有的话)
* 重复以上操作直至队列为空



//二叉树的结点(链式存储) 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); //右孩子入队 } }


5. 由遍历序列构造二叉树


* 先序序列 + 中序序列
* 后序序列 + 中序序列
* 层序序列 + 中序序列  
 **key**: 找到树的根节点,并根据中序序列划分左右子树,再找到左右子树根节点、


##### 5.3.2线索二叉树


1. 线索二叉树的概念与作用  
 在二叉树的结点上加上线索的二叉树称为线索二叉树,对二叉树以某种遍历方式(如先序、中序、后序或层次等)进行遍历,使其变为线索二叉树的过程称为对二叉树进行线索化。
2. 线索二叉树的存储结构


* 中序线索二叉树——线索指向中序前驱、中序后继



//线索二叉树结点 typedef struct ThreadNode{ ElemType data; struct ThreadNode *lchild, *rchild; int ltag, rtag; // 左、右线索标志 }ThreadNode, *ThreadTree;




> 

> tag == 0: 指针指向孩子

> 

> 

> 





> 

> tag == 1: 指针是“线索”

> 

> 

> 



* 先序线索二叉树——线索指向先序前驱、先序后继
* 后序线索二叉树——线索指向后序前驱、后序后继


3. 二叉树的线索话


* 中序线索化



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; //处理遍历的最后一个结点 } }


4. 线索二叉树中找前驱、后继


* 中序线索二叉树找中序后继:在中序线索二叉树中找到指定节点 \*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没有后序后继;


![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/aba7e83076cb47fdb7ee773f0090f48c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=PuvMazhyKIyGIm19NwDVrOCkxgs%3D)


#### 5.4树、森林


##### 5.4.1树的存储结构


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)  
 查询:①优点-查指定结点的双亲很方便;②缺点-查指定结点的孩子只能从头遍历,空数据导致遍历更慢;


2. 孩子表示法(顺序+链式)  
 孩子链表:把每个结点的孩子结点排列起来,看成是一个线性表,用单链表存储,则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;


3. 孩子兄弟表示法(链式)



typedef struct CSNode{ ElemType data; //数据域 struct CSNode *firstchild, *nextsibling; //第一个孩子和右兄弟指针, *firstchild 看作左指针,*nextsibling看作右指针 }CSNode. *CSTree;


##### 5.4.2树、森林与二叉树的转换


本质:森林中各个树的根结点之间视为兄弟关系


**将树转换成二叉树:**


1. 加线:在兄弟之间加一连线
2. 抹线:对每个结点去除其与孩子之间的关系(第一孩子除外)
3. 旋转:以树的根结点为轴心,顺时针转45度  
 (兄弟相连留长子)  
 ![                                                                                                                                                                                                                                                                                                                                                    ](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/cd6ca597de294d95aacaaabf2d066586~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=XWibDz%2BhWwP19H0c3yd5dHoMv5M%3D)


##### 5.4.3树、森林的遍历


1. 树的遍历


* 先根遍历:若树非空,先访问根结点,再依次对每棵子树进行先根遍历;(与对应二叉树的先序遍历序列相同)



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); //访问根节点 } }


* 层序遍历(队列实现):



> 
> 若树非空,则根结点入队;  
>  若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队;  
>  重复以上操作直至队尾为空;
> 
> 
> 


2. 森林的遍历


* 先序遍历:等同于依次对各个树进行**先根遍历**;也可以先转换成与之对应的二叉树,对二叉树进行先序遍历;
* 中序遍历:等同于依次对各个树进行**后根遍历**;也可以先转换成与之对应的二叉树,对二叉树进行中序遍历;


#### 5.5树与二叉树的应用


##### 5.5.1二叉排序树(BST)


1. 二叉排序树的定义  
 左子树结点值<跟结点值<右子树结点值
2. 查找操作



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); }


3. 插入操作



//在二叉排序树中插入关键字为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); }


4. 二叉排序树的构造



//按照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++; } }


5. 删除操作
6. 查找效率分析



> 
> 查找长度:查找运算中,需要对比关键字的次数,反映了查找操作时间复杂度;  
>  查找成功的平均查找长度ASL  
>  查找失败的平均查找长度ASL
> 
> 
> 


##### 5.5.2平衡二叉树(AVL)


1. 平衡二叉树的定义  
 在插入和删除二叉树的结点时,要保证任意结点的左右子树的高度差的绝对值不超过1,将这样的树称为**平衡二叉树**

//平衡二叉树结点 typedef struct AVLNode{ int key; //数据域 int balance; //平衡因子 struct AVLNode *lchild; *rchild; }AVLNode, *AVLTree;


2. 平衡二叉树的插入
3. 插入新节点后如何调整“不平衡”问题  
 调整最小不平衡子树



> 
> LL: 在A结点的左孩子的左子树中插入导致不平衡  
>  调整: A的左孩子结点右上旋  
>  RR: 在A结点的右孩子的右子树中插入导致不平衡  
>  调整: A的右孩子结点左上旋  
>  LR: 在A结点的左孩子的右子树中插入导致不平衡  
>  调整: A的左孩子的右孩子,先左上旋再右上旋  
>  RL: 在A结点的右孩子的左子树中插入导致不平衡  
>  调整: A的右孩子的左孩子,先右上旋再左上旋
> 
> 
> 


4. 平衡二叉树的查找与效率分析  
 若树高为h,则最坏情况下,查找一个关键字最多需要对比h次,即查找操作的时间复杂度不可能超过O(h);


#### 5.5.3哈夫曼树


1. 带权路径长度:从根节点到该结点之间的路径长度与该节点的权的乘积。
2. 哈夫曼树的定义:带权路径最短的树。
3. 哈夫曼树的构造(重点):构造森林全是根,选用两小造新树,删除两小添新人,重复2、3剩单根。
4. 哈杜曼编码(重点):  
 ![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f74ffd52cd884932b14f24c0fd6cb2e7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=%2Bg%2BFrTnwbTEnzEGZhgLjrDLM9RE%3D)


:由哈夫曼树得到的二进制前缀编码称为哈夫曼编码。


### 第六章 图


#### 6.1图的基本概念


* 图是一种非线性结构
* 图的特点:


1. 顶点之间的关系是任意的
2. 图中任意两个顶点之间都可能相关
3. 顶点的前驱和后继个数无限制


* 定义:图是一种数据元素间存在多对多关系的数据结构加上一组基本操作构成的抽象数据类型。  
 生成树:所有顶点均由边连接在一起但不存在回路的图。


#### 6.2图的存储结构


##### 6.2.1数组表示法(邻接矩阵表示法)


![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ebbbefa45042494e8633afacbca1338f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=CAxcSpCEEbELTHE9aJnIdOatOIE%3D)  
 ![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5cb9df5124394af2be65093d68ae4876~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=01zwZ9sAe6FsWLDF84wlBP%2BmFgg%3D)


##### 6.2.2 邻接表(类似于数的孩子链表表示法)


![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/13e1be7b6001445fb2aad46eb368b4df~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=3o8QAghaPGTJn759NRZHT93z5AE%3D)  
 ![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dfc1689159324804b464d06be6f795d8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=601xX7CPAGx48bzAlG5RVHBNx0o%3D)


##### 6.2.3十字链表


![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d3d98f450797471ea9131f2801cfa905~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=5RNbJv%2BDFKpaB1unUr3MaYMgfBM%3D)


#### 6.3图的遍历


定义:从图的任意指定顶点出发,依照某种规则去访问图中所有顶点,且每个顶点仅被访问一次,这一过程叫图的遍历。  
 方式:


* 深度优先遍历方法(Depth\_First Search——DFS)
* 广度优先遍历法(Breadth\_Frist Search——BFS)


#### 6.4最小生成树


普里姆(Prim)算法。  
 ![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/271945ac4953476b843a9cc507a400b4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=uGooWdSVNRaVIlpgBj9YnjA3RQ8%3D)


#### 6.5最短路径


迪杰斯特拉:


![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8478fe3b8cda4b6a862fe9d7e310dbc2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=8dSrgdgIJqgb%2B46sku3TyhGl7zU%3D)


#### 6.6AOV网络与拓扑排序


#### 6.7 AOE网络与关键路径


### 第七章 查找


#### 7.1查找表相关概念


* 查找表:由同一类型的数据元素(或记录)构成的集合。对查找表进行的经常操作为:查找、检索、增加、删除。
* 静态查找表:对查找表只进行前两种操作。
* 动态查找表:不仅限于前两种操作。
* 关键字:数据元素中某个数据项的值,用以标识一个数据元素,如果是唯一标识,则称为主关键字。
* 查找是否成功:根据给定的值,在查找表中确定一个其关键字等于给定值的元素,如果表中存在这样元素,则称查找成功,否则,不成功。  
 折半查找:  
 ![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ec9063341d8d48beafca582e271205c1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=knvoVi%2FdsAt%2BAUE%2FfDwjlDPUzRE%3D)  
 索引查找:  
 ![在这里插入图片描述](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1876a38a832347eba15eb4b4ed23f074~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MzIxMjA3NDIwNDUy:q75.awebp?rk3s=f64ab15b&x-expires=1772342228&x-signature=e9A1trQHOJCsdmvGPFHFAeaCBzc%3D)


### 第八章 排序


#### 8.1排序的基本概念


1. 排序:重新排列表中的元素,使表中元素满足按关键字有序的过程(关键字可以相同)
2. 排序算法的评价指标:时间复杂度、空间复杂度;
3. 排序算法的稳定性:关键字相同的元素在排序之后相对位置不变,称为稳定的;(选择题考查)  
 Q: 稳定的排序算法一定比不稳定的好?  
 A: 不一定,要看实际需求;
4. 排序算法的分类:  
 **内部排序:** 数据都在内存——关注如何使时间、空间复杂度更低;  
 **外部排序:** 数据太多,无法全部放入内存——关注如何使时间、空间复杂度更低,如何使读/写磁盘次数更少;


#### 8.2 插入排序


##### 8.2.1直接插入排序


1. **算法思想:** 每次将一个待排序的记录按其关键字大小,插入(依次对比、移动)到前面已经排好序的子序列中,直到全部记录插入完成
2. **代码实现:**


* 不带“哨兵”



void InsertSort(int A[], int n){ //A中共n个数据元素 int i,j,temp; for(i=1; i<n; i++) if(A[i]<A[i-1]){ //A[i]关键字小于前驱 temp = A[i];
for(j=i-1; j>=0 && A[j]>temp; --j) A[j+1] = A[j]; //所有大于temp的元素都向后挪 A[j+1] = temp; //复制到插入位置 } }


* 带“哨兵” ,优点:不用每轮循环都判断j>=0



void InsertSort(int A[], int n){ //A中从1开始存储,0放哨兵 int i,j; for(i=1; i<n; i++) if(A[i]<A[i-1]){
A[0] = A[i]; //复制为哨兵 for(j=i-1; A[0] < A[j]; --j) //从后往前查找待插入位置 A[j+1] = A[j]; //向后挪动 A[j+1] = A[0]; //复制到插入位置 } }


3. 算法效率分析



> 
> 空间复杂度:O(1)  
>  时间复杂度:主要来自于对比关键字、移动关键字,若有n个元素,则需要n-1躺处理  
>  最好情况: 原本为有序,共n-1趟处理,每一趟都只需要对比1次关键字,不需要移动元素,共对比n-1次 —— O(n)  
>  最差情况: 原本为逆序 —— O(n²)  
>  平均情况: O(n²)  
>  算法稳定性:稳定
> 
> 
> 


4. 对链表进行插入排序  
 移动元素的次数变少了,因为只需要修改指针,不需要依次右移;  
 但是关键字对比的次数依然是O(n²)数量级,因此整体看来时间复杂度仍然是O(n²)


##### 8.2.2折半插入排序


1. 思路: 先用折半查找找到应该插入的位置,再移动元素;
2. 为了保证稳定性,当查找到和插入元素关键字一样的元素时,应该继续在这个元素的右半部分继续查找以确认位置; 即当 A[mid] == A[0] 时,应继续在mid所指位置右边寻找插入位置
3. 当low>high时,折半查找停止,应将[low,i-1]or[high+1,i-1]内的元素全部右移,并将A[0]复制到low所指的位置;
4. 代码实现



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]
}

}


5. 与直接插入排序相比,比较关键字的次数减少了,但是移动元素的次数没有变,时间复杂度仍然是O(n²)


##### 8.2.3希尔排序


1. 思路: 先追求表中元素的部分有序,再逐渐逼近全局有序;
2. 更适用于基本有序的排序表和数据量不大的排序表,仅适用于线性表为顺序存储的情况
3. 代码实现:



void ShellSort(ElemType A[], int n){ //A[0]为暂存单元 for(dk=n/2; dk>=1; dk=dk/2) //步长递减(看题目要求,一般是1/2 for(i=dk+1; i<=n; ++i) if(A[i]<A[i-dk]){ A[0]=A[i]; for(j=i-dk; j>0&&A[0]<A[j];j-=dk) A[j+dk]=A[j]; //记录后移,查找插入的位置 A[j+dk]=A[0;] //插入 } }

img img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

了解详情》docs.qq.com/doc/DSlVlZExWQ0FRSE9H