mirror of
https://github.com/oxyanyano/2022-WangDao-CS-DS-Notes.git
synced 2026-05-01 22:20:03 +08:00
Add files via upload
This commit is contained in:
41
1.1线性表.md
Normal file
41
1.1线性表.md
Normal file
@@ -0,0 +1,41 @@
|
||||
## 线性表 —— Linear List
|
||||
|
||||
### 一、线性表的定义:
|
||||
|
||||
`线性表`是n个具有相同特性的数据元素的有限序列。
|
||||
|
||||
|
||||
|
||||
### 二、线性表的基本操作
|
||||
|
||||
`注`:参数代“&”表示:方法运行完后,对参数修改的结果要`“带回来”`
|
||||
|
||||
对数据的操作:创销,增删查改
|
||||
|
||||
```c
|
||||
InitList(&L); //初始化表:构造一个空的线性表L,分配内存空间
|
||||
DestoryList(&L); //销毁操作:销毁线性表,并释放线性表L所占用的内存空间
|
||||
|
||||
ListInsert(&L,i,e); //插入操作:在表L中第i个位置上查入指定元素e
|
||||
ListDelete(&L,i,&e); //删除操作:删除表L中第i个位置的元素,/*并用e反回删除元素的值*/
|
||||
|
||||
LocateElem(L,e); //按值查找操作
|
||||
GetElem(L,i); //按位查找操作
|
||||
|
||||
//其它常用操作
|
||||
Length(L); //求表长
|
||||
Print(L); //输出操作
|
||||
Empty(L); //判空操作
|
||||
```
|
||||
|
||||
### 三、存储结构
|
||||
|
||||
`顺序存储`和`链式存储`
|
||||
|
||||
### 四、线性表分类
|
||||
|
||||
线性表的顺序存储:顺序表
|
||||
|
||||
线性表的链式存储:单双链表、循环链表
|
||||
|
||||
静态链表比较特殊:逻辑上离散,物理上连续
|
||||
421
1.2顺序表——线性表的顺序存储.md
Normal file
421
1.2顺序表——线性表的顺序存储.md
Normal file
@@ -0,0 +1,421 @@
|
||||
## 顺序表—— Sequence List
|
||||
|
||||
### 一、顺序表的定义
|
||||
|
||||
`顺序表`:线性表的`顺序存储`,它是用一组地址连续的存储单元依次存储线性表中的数据元素,使得**逻辑上相邻的两个元素在物理位置上也相邻**。
|
||||
|
||||
### 二、顺序表的特点
|
||||
|
||||
①随机访问:可直接通过下标访问
|
||||
|
||||
②存储密度高:每个节点只能存数据元素
|
||||
|
||||
③拓展容量不方便:即便采用动态分配方式,迁移数据时时间复杂度也比较高
|
||||
|
||||
④插入、删除操作不方便:需要移动大量元素
|
||||
|
||||
### 三、顺序表的实现方式
|
||||
|
||||
实现方式:`静态分配`和`动态分配`
|
||||
|
||||
### 四、静态分配的顺序表上的操作
|
||||
|
||||
#### 静态分配的顺序表的优缺点
|
||||
|
||||
`缺点`:顺序表的表长确定后无法修改,存满了就存不了了
|
||||
|
||||
#### 顺序表的类型描述
|
||||
|
||||
```C
|
||||
#define MaxSize 10; //定义最大长度
|
||||
typedef struct{
|
||||
int data[MaxSize]; //“静态”的数组存数据,存int数据
|
||||
int length; //顺序表的当前长度
|
||||
}SqList;
|
||||
```
|
||||
|
||||
#### 初始化
|
||||
|
||||
```c
|
||||
//初始化
|
||||
void InitList(SqList &L){
|
||||
for(int i=0; i<MaxSize; i++){
|
||||
L.data=[i]=0; //将所有元素都设为默认值0
|
||||
}
|
||||
L.length=0; //顺序表长度初始为0
|
||||
}
|
||||
```
|
||||
|
||||
#### 插入
|
||||
|
||||
```c
|
||||
//插入操作:在顺序表L的第i个(位序)上插入x
|
||||
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--)
|
||||
L.data[j]=L.data[j-1]; //将第i个及后面的元素后移
|
||||
L.data[i-1]=e; //将e放到第i个位置
|
||||
L.length++; //长度+1
|
||||
}
|
||||
```
|
||||
|
||||
##### 插入的时间复杂度:
|
||||
|
||||
最好情况:插到表尾,不需移动元素,循环0次,`最好时间复杂度`=O(1)
|
||||
|
||||
最坏情况:插到表头,移动n个元素,循环n次,`最坏时间复杂度`=O(n)
|
||||
|
||||
平均情况:设插入概率为p=1/n+1,则循环np+(n-1)p+…+1p=n/2,`平均时间复杂度`=O(n)
|
||||
|
||||
#### 删除
|
||||
|
||||
```c
|
||||
//删除操作:删除顺序表L中第i个元素并返回其元素值
|
||||
bool ListDelete(SqList &L, int i,int &e){
|
||||
if(i<1||i>L.length+1){ //判断i的范围是否有效
|
||||
return false;
|
||||
}else{
|
||||
e = L.data[i-1]; //将被删除的元素赋值给e
|
||||
for(int j=i; j<L.length; j++){
|
||||
L.data[j]=L.data[j-1]; //将第i个后面的元素前移
|
||||
}
|
||||
L.length--; //长度-1
|
||||
return ture;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### 删除的时间复杂度:
|
||||
|
||||
最好情况:删除表尾,不需移动元素,循环0次,`最好时间复杂度`=O(1)
|
||||
|
||||
最坏情况:删除表头,移动n-1个元素,循环n次,`最坏时间复杂度`=O(n)
|
||||
|
||||
平均情况:设删除概率为p=1/n,则循环(n-1)p+(n-2)p+…+1p=(n-1)/2,`平均时间复杂度`=O(n)
|
||||
|
||||
#### 查找
|
||||
|
||||
##### 按位查找
|
||||
|
||||
```c
|
||||
//按位查找:返回顺序表中第i个元素的元素值
|
||||
int GetElem(Sqlist L, int i){
|
||||
return L.data[i-1];
|
||||
}
|
||||
```
|
||||
|
||||
###### 按位查找的时间复杂度
|
||||
|
||||
时间复杂度=O(1)
|
||||
|
||||
##### 按值查找
|
||||
|
||||
```c
|
||||
//按值查找:返回顺序表L中第一个值为x的元素的位置
|
||||
int LocateElem(Sqlist L, int e){
|
||||
for(int i=0; i<L.length; i++){
|
||||
if(L.data[i] == e)
|
||||
return i+1; //返回元素位置
|
||||
}
|
||||
return -1; //查找失败,返回-1
|
||||
}
|
||||
```
|
||||
|
||||
###### 按值查找的时间复杂度
|
||||
|
||||
最好情况:目标在表头,循环1次,`最好时间复杂度`=O(1)
|
||||
|
||||
最坏情况:目标在表尾,循环n次,`最坏时间复杂度`=O(n)
|
||||
|
||||
平均情况:设删除概率为p=1/n,则循环(n-1)p+(n-2)p+…+1p=(n+1)/2,`平均时间复杂度`=O(n)
|
||||
|
||||
#### 无销毁
|
||||
|
||||
系统自动销毁
|
||||
|
||||
### 五、动态分配的顺序表上的操作
|
||||
|
||||
#### 动态分配的顺序表的优缺点:
|
||||
|
||||
`优点`:可以动态增加长度
|
||||
|
||||
`缺点`:动态增加长度中的迁移工作时间开销大
|
||||
|
||||
#### 顺序表的类型描述
|
||||
|
||||
```C
|
||||
typedef struct{
|
||||
int *data; //指向“动态”分配的数组的指针
|
||||
int MaxSize; //顺序表的最大长度
|
||||
int length; //顺序表的当前长度
|
||||
}SqList;
|
||||
```
|
||||
|
||||
#### 初始化
|
||||
|
||||
```c
|
||||
//初始化
|
||||
void InitList(SqList &L){
|
||||
//用malloc函数申请一片连续的存储空间
|
||||
L.data=(int *)malloc(InitSize*sizeof(int));
|
||||
L.MaxSize=InitSize;
|
||||
L.length=0; //顺序表长度初始为0
|
||||
}
|
||||
```
|
||||
|
||||
#### 动态增加数组长度
|
||||
|
||||
方法:借用一个指针指向原来顺序表,新建一个更大的顺序表,将原数据迁移过来,并更改顺序表大小,最后释放原顺序表空间
|
||||
|
||||
```c
|
||||
//动态分配
|
||||
void IncreaseSize(SqlList &L, int len){
|
||||
int *p=L.data;
|
||||
L.data=(int *)malloc((L.InitSize+len)*sizeof(int));
|
||||
for(int i=0; i<L.length; i++){
|
||||
L.data[i]=p[i]; //将数据迁移至新区域
|
||||
}
|
||||
L.MaxSize=L.MaxSize+len; //顺序表最大长度+len
|
||||
free(p); //释放原来的内存空间
|
||||
}
|
||||
```
|
||||
|
||||
#### 插入
|
||||
|
||||
在静态分配的基础上,如果容量不够,则动态增加
|
||||
|
||||
```c
|
||||
//插入操作:在顺序表L的第i个(位序)上插入x
|
||||
bool ListInsert(SqList &L, int i,int e){
|
||||
if(i<1||i>L.length+1) //判断i的范围是否有效
|
||||
return false;
|
||||
if(L.length>=MaxSize) //当存储空间已满时,动态增加数组长度
|
||||
IncreaseSize(L,10);
|
||||
for(int j=L.length; j>=i; j--)
|
||||
L.data[j]=L.data[j-1]; //将第i个及后面的元素后移
|
||||
L.data[i-1]=e; //将e放到第i个位置
|
||||
L.length++; //长度+1
|
||||
}
|
||||
```
|
||||
|
||||
##### 插入的时间复杂度:
|
||||
|
||||
最好情况:插到表尾,不需移动元素,循环0次,`最好时间复杂度`=O(1)
|
||||
|
||||
最坏情况:插到表头,移动n个元素,循环n次,`最坏时间复杂度`=O(n)
|
||||
|
||||
平均情况:设插入概率为p=1/n+1,则循环np+(n-1)p+…+1p=n/2,`平均时间复杂度`=O(n)
|
||||
|
||||
#### 删除(与静态一样)
|
||||
|
||||
```c
|
||||
//删除操作:删除顺序表L中第i个元素并返回其元素值
|
||||
bool ListDelete(SqList &L, int i,int &e){
|
||||
if(i<1||i>L.length+1){ //判断i的范围是否有效
|
||||
return false;
|
||||
}else{
|
||||
e = L.data[i-1]; //将被删除的元素赋值给e
|
||||
for(int j=i; j<L.length; j++){
|
||||
L.data[j]=L.data[j-1]; //将第i个后面的元素前移
|
||||
}
|
||||
L.length--; //长度-1
|
||||
return ture;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### 删除的时间复杂度:
|
||||
|
||||
最好情况:删除表尾,不需移动元素,循环0次,`最好时间复杂度`=O(1)
|
||||
|
||||
最坏情况:删除表头,移动n-1个元素,循环n次,`最坏时间复杂度`=O(n)
|
||||
|
||||
平均情况:设删除概率为p=1/n,则循环(n-1)p+(n-2)p+…+1p=(n-1)/2,`平均时间复杂度`=O(n)
|
||||
|
||||
#### 查找(与静态一样)
|
||||
|
||||
##### 按位查找
|
||||
|
||||
```c
|
||||
//按位查找:返回顺序表中第i个元素的元素值
|
||||
int GetElem(Sqlist L, int i){
|
||||
return L.data[i-1];
|
||||
}
|
||||
```
|
||||
|
||||
###### 按位查找的时间复杂度
|
||||
|
||||
时间复杂度=O(1)
|
||||
|
||||
##### 按值查找
|
||||
|
||||
```c
|
||||
//按值查找:返回顺序表L中第一个值为x的元素的位置
|
||||
int LocateElem(Sqlist L, int e){
|
||||
for(int i=0; i<L.length; i++){
|
||||
if(L.data[i] == e)
|
||||
return i+1; //返回元素位置
|
||||
}
|
||||
return -1; //查找失败,返回-1
|
||||
}
|
||||
```
|
||||
|
||||
###### 按值查找的时间复杂度
|
||||
|
||||
最好情况:目标在表头,循环1次,`最好时间复杂度`=O(1)
|
||||
|
||||
最坏情况:目标在表尾,循环n次,`最坏时间复杂度`=O(n)
|
||||
|
||||
平均情况:设删除概率为p=1/n,则循环(n-1)p+(n-2)p+…+1p=(n+1)/2,`平均时间复杂度`=O(n)
|
||||
|
||||
#### 销毁
|
||||
|
||||
```c
|
||||
//销毁操作
|
||||
void DestroyList(Sqlist &L){
|
||||
free(L.data);
|
||||
L.length = 0;
|
||||
L.MaxSize = 0;
|
||||
L.data = nullptr; //令其为空指针
|
||||
}
|
||||
```
|
||||
|
||||
### 六、共同的操作
|
||||
|
||||
#### 求表长
|
||||
|
||||
```c
|
||||
//求表长
|
||||
int Length(Sqlist L){
|
||||
return L.length;
|
||||
}
|
||||
```
|
||||
|
||||
#### 遍历
|
||||
|
||||
```c
|
||||
//遍历操作
|
||||
void PrintList(Sqlist L){
|
||||
for(int i=0; i<L.length; i++){
|
||||
cout<<L.data[i]<<" ";
|
||||
}
|
||||
cout<<endl;
|
||||
}
|
||||
```
|
||||
|
||||
#### 判空
|
||||
|
||||
```c
|
||||
//判空操作
|
||||
int Empty(Sqlist L){
|
||||
return L.length==0? 1 : 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 七、完整代码
|
||||
|
||||
```c
|
||||
//动态分配的顺序表的完整代码
|
||||
#include<bits/stdc++.h>
|
||||
using namespace std;
|
||||
|
||||
#define MaxSize 100 //定义线性表的最大长度
|
||||
typedef struct Sqlist{
|
||||
int data[MaxSize];
|
||||
int length;
|
||||
}Sqlist;
|
||||
|
||||
//初始化
|
||||
void Init(Sqlist &L){
|
||||
L.length = 0;
|
||||
}
|
||||
|
||||
//求表长
|
||||
int Length(Sqlist L){
|
||||
return L.length;
|
||||
}
|
||||
|
||||
//插入操作:在表中第i个位置上插入x
|
||||
void Insert(Sqlist &L, int i, int x){
|
||||
if(i<1 || i>L.length+1 || L.length >= MaxSize){
|
||||
cout<<x<<" Insert failed."<<endl;
|
||||
return;
|
||||
}
|
||||
for(int j=L.length; j>=i; j--){ //第i个及以后的元素后移
|
||||
L.data[j] = L.data[j-1];
|
||||
}
|
||||
L.data[i-1]=x;
|
||||
L.length++;
|
||||
}
|
||||
|
||||
//遍历操作
|
||||
void PrintList(Sqlist L){
|
||||
cout<<"L: ";
|
||||
for(int i=0; i<L.length; i++){
|
||||
cout<<L.data[i]<<" ";
|
||||
}
|
||||
cout<<endl;
|
||||
}
|
||||
|
||||
//按值查找:返回顺序表L中第一个值为x的元素的位置
|
||||
int LocateElem(Sqlist L, int x){
|
||||
for(int i=0; i<L.length; i++){
|
||||
if(L.data[i] == x)
|
||||
return i+1; //返回元素位置
|
||||
}
|
||||
return -1; //查找失败,返回-1
|
||||
}
|
||||
|
||||
//按位查找:返回顺序表中第i个元素的元素值
|
||||
int GetElem(Sqlist L, int i){
|
||||
return L.data[i-1];
|
||||
}
|
||||
|
||||
//删除操作:删除顺序表L中第i个元素并返回其元素值
|
||||
int Delete(Sqlist &L, int i, int &x){
|
||||
if(i<1 || i>L.length){
|
||||
return -1;
|
||||
}else{
|
||||
x = L.data[i-1];
|
||||
for(int j=i; j<L.length; j++){
|
||||
L.data[j-1] = L.data[j];
|
||||
}
|
||||
L.length--;
|
||||
return x;
|
||||
}
|
||||
}
|
||||
|
||||
//判空操作
|
||||
int Empty(Sqlist L){
|
||||
return L.length==0? 1 : 0;
|
||||
}
|
||||
|
||||
int main(){
|
||||
Sqlist L;
|
||||
Init(L);
|
||||
Insert(L, 1, 50);
|
||||
Insert(L, 2, 60);
|
||||
Insert(L, 1, 40);
|
||||
Insert(L, 1, 666);
|
||||
Insert(L, 5, 70);
|
||||
Insert(L, 6, 40);
|
||||
Insert(L, 7, 100);
|
||||
cout<<"长度:"<<Length(L)<<endl;
|
||||
PrintList(L);
|
||||
int x;
|
||||
cout<<"第三个元素是:"<<GetElem(L,3)<<endl;
|
||||
cout<<"40在第"<<LocateElem(L,40)<<"位"<<endl;
|
||||
cout<<"删除"<<Delete(L,6,x)<<endl;
|
||||
PrintList(L);
|
||||
if(!Empty(L)){
|
||||
cout<<"L不为空"<<endl;
|
||||
}else{
|
||||
cout<<"L为空"<<endl;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
447
1.3单链表——线性表的链式存储.md
Normal file
447
1.3单链表——线性表的链式存储.md
Normal file
@@ -0,0 +1,447 @@
|
||||
## 单链表—— Singly Linked List
|
||||
|
||||
### 一、单链表的定义
|
||||
|
||||
`单链表`:线性表的`链式存储`,它是通过一组任意的存储单元来存储线性表中的数据元素,不需要使用地址连续的存储单元,因此它不要求在逻辑上相邻的两个元素在物理位置上也相邻。
|
||||
|
||||
### 二、单链表的特点
|
||||
|
||||
①不能随机访问:遍历查找访问
|
||||
|
||||
②存储密度不高:每个节点既要存数据元素又要存指针
|
||||
|
||||
③拓展容量方便:直接用建立单链表拓展
|
||||
|
||||
④插入、删除操作方便:知道位置直接插入和删除
|
||||
|
||||
### 三、单链表的实现方式
|
||||
|
||||
实现方式:`不带头结点`和`带头结点`,一般带头结点比不带头结点好
|
||||
|
||||
带头结点:写操作代码方便,一般用带头结点,不明确的都是带头结点的
|
||||
|
||||
不带头结点:写操作代码麻烦,要区分第一个数据和后续数据的处理
|
||||
|
||||
`注`:这两种方式主要是:类型描述相同,初始化和判空不同
|
||||
|
||||
### 四、单链表上的操作
|
||||
|
||||
#### 单链表的类型描述
|
||||
|
||||
```C
|
||||
typedef struct LNode{ //定义单链表结点类型
|
||||
int data; //数据域,可以是别的各种数据类型,本文统一用int类型
|
||||
struct LNode *next; //指针域
|
||||
}LNode, *LinkList;
|
||||
```
|
||||
|
||||
#### 初始化和判空
|
||||
|
||||

|
||||
|
||||
##### 不带头结点的初始化和判空
|
||||
|
||||
```c
|
||||
//初始化
|
||||
void InitList(LinkList &L){
|
||||
L = NULL;
|
||||
L->next = NULL;
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
//判空操作
|
||||
bool Empty(LinkList L){
|
||||
if(L == NULL){
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### 带头结点的初始化和判空
|
||||
|
||||
```c
|
||||
//初始化
|
||||
void InitList(LinkList &L){
|
||||
L = (LNode *)malloc(sizeof(LinkList));
|
||||
L->next = NULL;
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
//判空操作
|
||||
bool Empty(LinkList L){
|
||||
if(L->next == NULL){
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 建立单链表
|
||||
|
||||
##### 头插法建立单链表
|
||||
|
||||
用于链表的逆置
|
||||
|
||||

|
||||
|
||||
```c
|
||||
//头插法建立单链表
|
||||
LinkList HeadInsert(LinkList &L){
|
||||
InitList(L); //初始化
|
||||
int x;
|
||||
cin>>x;
|
||||
while(x!=9999){ //输入9999表示结束
|
||||
LNode *s = (LNode *)malloc(sizeof(LNode));
|
||||
s->data = x;
|
||||
s->next = L->next;
|
||||
L->next = s;
|
||||
cin>>x;
|
||||
}
|
||||
return L;
|
||||
}
|
||||
```
|
||||
|
||||
##### 尾插法建立单链表
|
||||
|
||||

|
||||
|
||||
```c
|
||||
//尾插法建立单链表
|
||||
LinkList TailInsert(LinkList &L){
|
||||
InitList(L);
|
||||
LNode *s,*r=L;
|
||||
int x;
|
||||
cin>>x;
|
||||
while(x!=9999){
|
||||
s = (LNode *)malloc(sizeof(LNode));
|
||||
s->data = x;
|
||||
r->next = s;
|
||||
r = s;
|
||||
cin>>x;
|
||||
}
|
||||
r->next = NULL;
|
||||
return L;
|
||||
}
|
||||
```
|
||||
|
||||
#### 插入
|
||||
|
||||
`时间复杂度`=O(1)
|
||||
|
||||
##### 带头结点的插入
|
||||
|
||||
```c
|
||||
//将x插入到单链表L的第i个位置上
|
||||
bool Insert(LinkList &L, int i, int e){
|
||||
if(i<1) return false;
|
||||
LNode *p = GetElem(L,i-1); //查找第i个位置
|
||||
return InsertNextNode(p, e); //用后插操作,插在p后面
|
||||
}
|
||||
```
|
||||
|
||||
##### 不带头结点的插入
|
||||
|
||||
```c
|
||||
//将x插入到单链表L的第i个位置上
|
||||
bool Insert(LinkList &L, int i, int e){
|
||||
if(i<1) return false;
|
||||
if(i==1){
|
||||
LNode *s = (LNode *)malloc(sizeof(LNode));
|
||||
s->data = e;
|
||||
s->next = L;
|
||||
L = s;
|
||||
return true;
|
||||
}
|
||||
LNode *p = GetElem(L,i-1); //查找第i个位置
|
||||
return InsertNextNode(p, e); //用后插操作,插在p后面
|
||||
}
|
||||
```
|
||||
|
||||
##### 指定结点的后插操作
|
||||
|
||||
```c
|
||||
//后插操作:在p结点之后插入元素e
|
||||
bool InsertNextNode(LNode *p, int e){
|
||||
if(p==NULL) return false;
|
||||
LNode *s = (LNode *)malloc(sizeof(LNode));
|
||||
if(s==NULL) return false;
|
||||
s->data = e;
|
||||
s->next = p->next;
|
||||
p->next = s;
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
##### 指定结点的前插操作
|
||||
|
||||
还是插在p后面,只不过让p和插入结点的值交换
|
||||
|
||||
```c
|
||||
//前插操作:在p结点之前插入元素e
|
||||
bool InsertPriorNode(LNode *p, int 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->data = p->data;
|
||||
p->data = e;
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 删除
|
||||
|
||||
##### 按位序删除
|
||||
|
||||
```c
|
||||
//删除操作:将单链表中的第i个结点删除
|
||||
bool Delete(LinkList &L, int i int &e){
|
||||
if(i<1 || i>Length(L))
|
||||
return false;
|
||||
LNode *p = GetElem(L,i-1); //查找第i个位置
|
||||
LNode *q = p->next;
|
||||
e = q->data;
|
||||
p->next = q->next;
|
||||
free(q);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
###### 按位序删除的时间复杂度:
|
||||
|
||||
最好情况:删除第一个,不需查找位置,循环0次,`最好时间复杂度`=O(1)
|
||||
|
||||
最坏情况:删除最后一个,需查找第n位,循环n次,`最坏时间复杂度`=O(n)
|
||||
|
||||
平均情况:删除任意一个,`平均时间复杂度`=O(n)
|
||||
|
||||
##### 指定结点的删除
|
||||
|
||||
`时间复杂度`=O(n)
|
||||
|
||||
方法:p的后一个为q,p指向q的下一个,把q的值给p,最后释放q
|
||||
|
||||
```c
|
||||
//删除指定结点p
|
||||
bool Delete(LNode *p){
|
||||
if(p==NULL) return false;
|
||||
LNode *q = p->next;
|
||||
p->data = q->data
|
||||
p->next = q->next;
|
||||
free(q);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 查找
|
||||
|
||||
##### 按位查找
|
||||
|
||||
`平均时间复杂度`=O(n)
|
||||
|
||||
```c
|
||||
//按位查找:查找在单链表L中第i个位置的结点
|
||||
LNode *GetElem(LinkList L, int i){
|
||||
int j=0;
|
||||
LNode *p = L;
|
||||
if(i<0) return NULL;
|
||||
while(p && j<i){
|
||||
p = p->next;
|
||||
j++;
|
||||
}
|
||||
return p; //如果i大于表长,p=NULL,直接返回p即可
|
||||
}
|
||||
```
|
||||
|
||||
##### 按值查找
|
||||
|
||||
`平均时间复杂度`=O(n)
|
||||
|
||||
```c
|
||||
//按值查找:查找e在L中的位置
|
||||
LNode *LocateElem(LinkList L, int e){
|
||||
LNode *p = L->next;
|
||||
while(p && p->data != e){
|
||||
p = p->next;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
```
|
||||
|
||||
#### 求表长
|
||||
|
||||
`平均时间复杂度`=O(n)
|
||||
|
||||
```c
|
||||
//求表的长度
|
||||
int Length(LinkList L){
|
||||
int len = 0;
|
||||
LNode *p = L;
|
||||
while(P->next){
|
||||
p = p->next;
|
||||
len++;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
```
|
||||
|
||||
#### 遍历
|
||||
|
||||
```c
|
||||
//遍历操作
|
||||
void PrintList(LinkList L){
|
||||
LNode *p = L->next;
|
||||
while(p){
|
||||
cout<<p->data<<" ";
|
||||
p = p->next;
|
||||
}
|
||||
cout<<endl;
|
||||
}
|
||||
```
|
||||
|
||||
### 五、完整代码
|
||||
|
||||
```c
|
||||
#include<bits/stdc++.h>
|
||||
using namespace std;
|
||||
|
||||
typedef struct LNode{
|
||||
int data;
|
||||
struct LNode *next;
|
||||
}LNode, *LinkList;
|
||||
|
||||
//初始化
|
||||
void InitList(LinkList &L){
|
||||
L = (LNode *)malloc(sizeof(LinkList));
|
||||
L->next = NULL;
|
||||
}
|
||||
|
||||
//遍历操作
|
||||
void PrintList(LinkList L){
|
||||
LNode *p = L->next;
|
||||
while(p){
|
||||
cout<<p->data<<" ";
|
||||
p = p->next;
|
||||
}
|
||||
cout<<endl;
|
||||
}
|
||||
|
||||
//求单链表的长度
|
||||
int Length(LinkList L){
|
||||
LNode *p = L->next;
|
||||
int len = 0;
|
||||
while(p){
|
||||
len++;
|
||||
p = p->next;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
//头插法建立单链表
|
||||
LinkList HeadInsert(LinkList &L){
|
||||
InitList(L); //初始化
|
||||
int x;
|
||||
cin>>x;
|
||||
while(x!=9999){
|
||||
LNode *s = (LNode *)malloc(sizeof(LNode));
|
||||
s->data = x;
|
||||
s->next = L->next;
|
||||
L->next = s;
|
||||
cin>>x;
|
||||
}
|
||||
return L;
|
||||
}
|
||||
|
||||
//尾插法建立单链表
|
||||
LinkList TailInsert(LinkList &L){
|
||||
InitList(L);
|
||||
LNode *s,*r=L;
|
||||
int x;
|
||||
cin>>x;
|
||||
while(x!=9999){
|
||||
s = (LNode *)malloc(sizeof(LNode));
|
||||
s->data = x;
|
||||
r->next = s;
|
||||
r = s;
|
||||
cin>>x;
|
||||
}
|
||||
r->next = NULL;
|
||||
return L;
|
||||
}
|
||||
|
||||
//按值查找:查找x在L中的位置
|
||||
LNode *LocateElem(LinkList L, int x){
|
||||
LNode *p = L->next;
|
||||
while(p && p->data != x){
|
||||
p = p->next;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
//按位查找:查找在单链表L中第i个位置的结点
|
||||
LNode *GetElem(LinkList L, int i){
|
||||
int j=1;
|
||||
LNode *p = L->next;
|
||||
if(i==0)return L;
|
||||
if(i<1)return NULL;
|
||||
while(p && j<i){
|
||||
p = p->next;
|
||||
j++;
|
||||
}
|
||||
return p; //如果i大于表长,p=NULL,直接返回p即可
|
||||
}
|
||||
|
||||
//将x插入到单链表L的第i个位置上
|
||||
void Insert(LinkList &L, int i, int x){
|
||||
LNode *p = GetElem(L,i-1);
|
||||
LNode *s = (LNode *)malloc(sizeof(LNode));
|
||||
s->data = x;
|
||||
s->next = p->next;
|
||||
p->next = s;
|
||||
}
|
||||
|
||||
//删除操作:将单链表中的第i个结点删除
|
||||
void Delete(LinkList &L, int i){
|
||||
if(i<1 || i>Length(L)){
|
||||
cout<<"delete failed: index is wrong."<<endl;
|
||||
return;
|
||||
}
|
||||
LNode *p = GetElem(L,i-1);
|
||||
LNode *q = p->next;
|
||||
p->next = q->next;
|
||||
free(q);
|
||||
}
|
||||
|
||||
|
||||
int main(){
|
||||
//初始化,尾插法建立单链表
|
||||
LinkList L = TailInsert(L);
|
||||
//插入:在第二个位置插入结点,数据域为888,并遍历单链表
|
||||
Insert(L,2,888);
|
||||
cout<<"在第二个位置插入888: ";
|
||||
PrintList(L);
|
||||
//删除:删除第四个结点
|
||||
Delete(L,4);
|
||||
cout<<"删除第四个结点后:";
|
||||
PrintList(L);
|
||||
//按位查找:查找第三个结点,并输出其数据域的值
|
||||
LNode *p = GetElem(L,3);
|
||||
cout<<"第三个结点的值为:"<<p->data<<endl;
|
||||
//按值查找:查找数据域为2的结点的指针
|
||||
LNode *q = LocateElem(L,2);
|
||||
cout<<"数据为2的结点的下一个结点的值为:"<<q->next->data<<endl;
|
||||
//输出单链表的长度
|
||||
cout<<"单链表的长度:"<<Length(L)<<endl;
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
#### 运行结果:
|
||||
|
||||

|
||||
431
1.4双链表——单链表的改进.md
Normal file
431
1.4双链表——单链表的改进.md
Normal file
@@ -0,0 +1,431 @@
|
||||
## 双链表—— Double Linked List
|
||||
|
||||
### 一、单链表的定义
|
||||
|
||||
单链表的结点中只有一个指向其后继的指针,使得单链表要访问某个结点的前驱结点时,只能从头开始遍历,访问后驱结点的复杂度为O(1),访问前驱结点的复杂度为O(n)。为了克服上述缺点,引入了双链表。
|
||||
|
||||
`双链表`的结点中有两个指针`prior`和`next`,分别指向前驱结点和后继结点。
|
||||
|
||||
### 二、双链表的实现方式
|
||||
|
||||
实现方式:`不带头结点`和`带头结点`,一般带头结点比不带头结点好
|
||||
|
||||
带头结点:写操作代码方便,一般用带头结点,不明确的都是带头结点的
|
||||
|
||||
不带头结点:写操作代码麻烦,要区分第一个数据和后续数据的处理
|
||||
|
||||
### 三、双链表上的操作(带头结点)
|
||||
|
||||
#### 双链表的类型描述
|
||||
|
||||
```C
|
||||
typedef struct DNode{
|
||||
int data; //数据域
|
||||
struct DNode *prior,*next; //前驱和后继指针
|
||||
}DNode, *DLinkList;
|
||||
```
|
||||
|
||||
#### 初始化
|
||||
|
||||

|
||||
|
||||
```c
|
||||
//初始化
|
||||
bool InitList(DLinkList &L){
|
||||
L = (LNode *)malloc(sizeof(DLinkList));
|
||||
if(L == NULL) return false;
|
||||
L->prior = NULL;
|
||||
L->next = NULL;
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 判空
|
||||
|
||||
```c
|
||||
//判空操作
|
||||
bool Empty(DLinkList L){
|
||||
if(L->next == NULL){
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 建立双链表
|
||||
|
||||
##### 头插法建立双链表
|
||||
|
||||
用于链表的逆置
|
||||
|
||||
```c
|
||||
//头插法建立双链表
|
||||
DLinkList HeadInsert(DLinkList &L){
|
||||
InitList(L); //初始化
|
||||
int x;
|
||||
cin>>x;
|
||||
while(x!=9999){
|
||||
DNode *s = (DNode *)malloc(sizeof(DNode));
|
||||
s->data = x;
|
||||
if(L->next == NULL){
|
||||
s->next = NULL;
|
||||
s->prior = L;
|
||||
L->next = s;
|
||||
}else{
|
||||
s->next = L->next;
|
||||
L->next->prior = s;
|
||||
s->prior = L;
|
||||
L->next = s;
|
||||
}
|
||||
cin>>x;
|
||||
}
|
||||
return L;
|
||||
}
|
||||
```
|
||||
|
||||
##### 尾插法建立单链表
|
||||
|
||||
```c
|
||||
//尾插法建立双链表
|
||||
DLinkList TailInsert(DLinkList &L){
|
||||
InitList(L);
|
||||
DNode *s,*r=L;
|
||||
int x;
|
||||
cin>>x;
|
||||
while(x!=9999){
|
||||
s = (DNode *)malloc(sizeof(DNode));
|
||||
s->data = x;
|
||||
s->next = NULL;
|
||||
s->prior = r;
|
||||
r->next = s;
|
||||
r = s;
|
||||
cin>>x;
|
||||
}
|
||||
return L;
|
||||
}
|
||||
```
|
||||
|
||||
#### 插入
|
||||
|
||||
`时间复杂度`=O(1)
|
||||
|
||||

|
||||
|
||||
```c
|
||||
//将x插入到双链表L中*p结点的下一个结点
|
||||
bool Insert(DNode *p, int x){
|
||||
DNode *s = (DNode *)malloc(sizeof(DNode));
|
||||
if(p == NULL||s==NULL) return false;
|
||||
s->data = x;
|
||||
s->next = p->next; //1
|
||||
if(p->next != NULL) p->next->prior = s; //2
|
||||
s->prior = p; //3
|
||||
p->next = s; //4
|
||||
return true;
|
||||
}
|
||||
|
||||
//在双链表L中*p结点后插入s结点
|
||||
bool Insert(DNode *p, DNode *s){
|
||||
if(p == NULL||s==NULL) return false;
|
||||
s->next = p->next; //1
|
||||
if(p->next != NULL) p->next->prior = s; //2
|
||||
s->prior = p; //3
|
||||
p->next = s; //4
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 删除
|
||||
|
||||
##### 按位序删除
|
||||
|
||||
`时间复杂度`=O(n)
|
||||
|
||||

|
||||
|
||||
```c
|
||||
//删除操作:将双链表中的第i个结点删除
|
||||
bool Delete(DLinkList &L, int i){
|
||||
if(i<1 || i>Length(L)) return false;
|
||||
DNode *p = GetElem(L,i-1);
|
||||
if(p == NULL) return false;
|
||||
DNode *q = p->next;
|
||||
p->next = q->next; //1
|
||||
if(p->next != NULL) q->next->prior = p; //2
|
||||
free(q);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
##### 指定结点的删除
|
||||
|
||||
`时间复杂度`=O(1)
|
||||
|
||||
```c
|
||||
//删除操作:删除双链表中的p结点的后继结点
|
||||
void DeleteNextNode(DNode *p){
|
||||
if(p == NULL) return false;
|
||||
DNode *q = p->next;
|
||||
if(q == NULL) return false;
|
||||
p->next = q->next; //1
|
||||
if(p->next != NULL) q->next->prior = p; //2
|
||||
free(q);
|
||||
}
|
||||
```
|
||||
|
||||
#### 查找(与单链表完全一样)
|
||||
|
||||
##### 按位查找
|
||||
|
||||
`平均时间复杂度`=O(n)
|
||||
|
||||
```c
|
||||
//按位查找:查找在单链表L中第i个位置的结点
|
||||
DNode *GetElem(DLinkList L, int i){
|
||||
int j=0;
|
||||
DNode *p = L;
|
||||
if(i<0) return NULL;
|
||||
while(p && j<i){
|
||||
p = p->next;
|
||||
j++;
|
||||
}
|
||||
return p; //如果i大于表长,p=NULL,直接返回p即可
|
||||
}
|
||||
```
|
||||
|
||||
##### 按值查找
|
||||
|
||||
`平均时间复杂度`=O(n)
|
||||
|
||||
```c
|
||||
//按值查找:查找e在L中的位置
|
||||
DNode *LocateElem(DLinkList L, int e){
|
||||
DNode *p = L->next;
|
||||
while(p && p->data != e){
|
||||
p = p->next;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
```
|
||||
|
||||
#### 求表长(与单链表完全一样)
|
||||
|
||||
`平均时间复杂度`=O(n)
|
||||
|
||||
```c
|
||||
//求表的长度
|
||||
int Length(DLinkList L){
|
||||
int len = 0;
|
||||
DNode *p = L;
|
||||
while(P->next){
|
||||
p = p->next;
|
||||
len++;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
```
|
||||
|
||||
#### 遍历(与单链表完全一样)
|
||||
|
||||
```c
|
||||
//遍历操作
|
||||
void PrintList(DLinkList L){
|
||||
DNode *p = L->next;
|
||||
while(p){
|
||||
cout<<p->data<<" ";
|
||||
p = p->next;
|
||||
}
|
||||
cout<<endl;
|
||||
}
|
||||
```
|
||||
|
||||
#### 销毁
|
||||
|
||||
```c
|
||||
//销毁操作
|
||||
void DestoryList(DLinkList L){
|
||||
//循环释放各点的数据结点
|
||||
while(L->next != NULL){
|
||||
DeleteNextNode(L);
|
||||
}
|
||||
free(L); //释放头结点
|
||||
L = NULL;
|
||||
}
|
||||
```
|
||||
|
||||
### 四、完整代码
|
||||
|
||||
```c
|
||||
#include<bits/stdc++.h>
|
||||
using namespace std;
|
||||
|
||||
typedef struct DNode{
|
||||
int data;
|
||||
struct DNode *prior,*next;
|
||||
}DNode, *DLinkList;
|
||||
|
||||
//初始化
|
||||
void InitList(DLinkList &L){
|
||||
L = (DNode *)malloc(sizeof(DLinkList));
|
||||
L->prior = NULL;
|
||||
L->next = NULL;
|
||||
}
|
||||
|
||||
//遍历操作
|
||||
void PrintList(DLinkList L){
|
||||
DNode *p = L->next;
|
||||
while(p){
|
||||
cout<<p->data<<" ";
|
||||
p = p->next;
|
||||
}
|
||||
cout<<endl;
|
||||
}
|
||||
|
||||
//求双链表的长度
|
||||
int Length(DLinkList L){
|
||||
DNode *p = L->next;
|
||||
int len = 0;
|
||||
while(p){
|
||||
len++;
|
||||
p = p->next;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
//头插法建立双链表
|
||||
DLinkList HeadInsert(DLinkList &L){
|
||||
InitList(L); //初始化
|
||||
int x;
|
||||
cin>>x;
|
||||
while(x!=9999){
|
||||
DNode *s = (DNode *)malloc(sizeof(DNode));
|
||||
s->data = x;
|
||||
if(L->next == NULL){
|
||||
s->next = NULL;
|
||||
s->prior = L;
|
||||
L->next = s;
|
||||
}else{
|
||||
s->next = L->next;
|
||||
L->next->prior = s;
|
||||
s->prior = L;
|
||||
L->next = s;
|
||||
}
|
||||
cin>>x;
|
||||
}
|
||||
return L;
|
||||
}
|
||||
|
||||
//尾插法建立双链表
|
||||
DLinkList TailInsert(DLinkList &L){
|
||||
InitList(L);
|
||||
DNode *s,*r=L;
|
||||
int x;
|
||||
cin>>x;
|
||||
while(x!=9999){
|
||||
s = (DNode *)malloc(sizeof(DNode));
|
||||
s->data = x;
|
||||
s->next = NULL;
|
||||
s->prior = r;
|
||||
r->next = s;
|
||||
r = s;
|
||||
cin>>x;
|
||||
}
|
||||
return L;
|
||||
}
|
||||
|
||||
//按值查找:查找x在L中的位置
|
||||
DNode *LocateElem(DLinkList L, int x){
|
||||
DNode *p = L->next;
|
||||
while(p && p->data != x){
|
||||
p = p->next;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
//按位查找:查找在双链表L中第i个位置的结点
|
||||
DNode *GetElem(DLinkList L, int i){
|
||||
int j=1;
|
||||
DNode *p = L->next;
|
||||
if(i==0)return L;
|
||||
if(i<1)return NULL;
|
||||
while(p && j<i){
|
||||
p = p->next;
|
||||
j++;
|
||||
}
|
||||
return p; //如果i大于表长,p=NULL,直接返回p即可
|
||||
}
|
||||
|
||||
//将x插入到双链表L中*p结点的下一个结点
|
||||
void Insert(DLinkList &L, DNode *p, int x){
|
||||
DNode *s = (DNode *)malloc(sizeof(DNode));
|
||||
s->data = x;
|
||||
s->next = p->next;
|
||||
p->next->prior = s;
|
||||
s->prior = p;
|
||||
p->next = s;
|
||||
}
|
||||
|
||||
//删除操作:将双链表中的第i个结点删除
|
||||
void Delete(DLinkList &L, int i){
|
||||
if(i<1 || i>Length(L)){
|
||||
cout<<"delete failed: index is wrong."<<endl;
|
||||
return;
|
||||
}
|
||||
DNode *p = GetElem(L,i-1);
|
||||
DNode *q = p->next;
|
||||
p->next = q->next;
|
||||
q->next->prior = p;
|
||||
free(q);
|
||||
}
|
||||
|
||||
//判空操作
|
||||
bool Empty(DLinkList L){
|
||||
if(L->next == NULL){
|
||||
cout<<"L is null"<<endl;
|
||||
return true;
|
||||
}else{
|
||||
cout<<"L is not null"<<endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int main(){
|
||||
//尾插法建立双链表,并遍历单链表
|
||||
DLinkList L = TailInsert(L);
|
||||
cout<<"L: ";
|
||||
PrintList(L);
|
||||
|
||||
DNode *p;
|
||||
//按值查找
|
||||
p = LocateElem(L,2);
|
||||
cout<<"值为2的结点的下一个结点值是:"<<p->next->data<<endl;
|
||||
cout<<"值为2的结点的上一个结点值是:"<<p->prior->data<<endl;
|
||||
//按位查找
|
||||
p = GetElem(L,3);
|
||||
cout<<"第三个结点值是:"<<p->data<<endl;
|
||||
|
||||
//插入操作
|
||||
Insert(L,p,7);
|
||||
cout<<"在第三个结点后面插入值为7的结点后L: ";
|
||||
PrintList(L);
|
||||
|
||||
//删除操作
|
||||
Delete(L, 5);
|
||||
cout<<"删除第五个结点后L: ";
|
||||
PrintList(L);
|
||||
|
||||
//求表长
|
||||
cout<<"表长为:"<<Length(L)<<endl;;
|
||||
|
||||
//判空
|
||||
Empty(L);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
#### 运行结果:
|
||||
|
||||

|
||||
|
||||
238
1.5循环链表——单双链表的改进.md
Normal file
238
1.5循环链表——单双链表的改进.md
Normal file
@@ -0,0 +1,238 @@
|
||||
## 循环链表—— Double Linked List
|
||||
|
||||
### 一、循环链表的定义
|
||||
|
||||
`循环链表`:一般包括循环循环链表和循环循环链表,如下图所示
|
||||
|
||||

|
||||
|
||||
### 二、循环链表的实现方式
|
||||
|
||||
实现方式:`不带头结点`和`带头结点`,一般带头结点比不带头结点好
|
||||
|
||||
带头结点:写操作代码方便,一般用带头结点,不明确的都是带头结点的
|
||||
|
||||
不带头结点:写操作代码麻烦,要区分第一个数据和后续数据的处理
|
||||
|
||||
### 三、循环单链表上的操作(带头结点)
|
||||
|
||||
#### 循环单链表的类型描述(与单链表一样)
|
||||
|
||||
```C
|
||||
typedef struct LNode{ //定义循环链表结点类型
|
||||
int data; //数据域,可以是别的各种数据类型,本文统一用int类型
|
||||
struct LNode *next; //指针域
|
||||
}LNode, *LinkList;;
|
||||
```
|
||||
|
||||
#### 初始化和判空(与单链表不一样)
|
||||
|
||||
L->next = NULL改为L->next = L
|
||||
|
||||
```c
|
||||
//初始化
|
||||
void InitList(LinkList &L){
|
||||
L = (LNode *)malloc(sizeof(LinkList));
|
||||
L->next = L;
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
//判空操作
|
||||
bool Empty(LinkList L){
|
||||
if(L->next == L){
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 判断表尾结点
|
||||
|
||||
```c
|
||||
//判断结点p是否是表尾结点
|
||||
bool isTail(LinkList L){
|
||||
if(p->next == L){
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 插入、删除
|
||||
|
||||
循环单链表的插入、删除算法与单链表几乎一样,不同的是如果在表尾进行,那么要让单链表继续**保持循环的性质**,即**让尾结点的next域指向头结点**。
|
||||
|
||||
`时间复杂度`=O(1)
|
||||
|
||||
#### 查找
|
||||
|
||||
单链表的查找是从表头开始,找到表尾停止。而循环单链表中,若从表头开始查找,那与单链表的操作一致。若从表的任意位置开始查找,那么需要一个计数变量sum,当sum=表长时还未找到,则查找失败,退出循环。
|
||||
|
||||
#### 求表长
|
||||
|
||||
与单链表一样
|
||||
|
||||
#### 遍历
|
||||
|
||||
单链表只能从表头结点开始往后顺序遍历整个链表,循环单链表可以从表中的`任意一个结点开始`遍历整个链表。
|
||||
|
||||
### 四、循环双链表上的操作(带头结点)
|
||||
|
||||
#### 循环链表的类型描述
|
||||
|
||||
```C
|
||||
typedef struct DNode{
|
||||
int data; //数据域
|
||||
struct DNode *prior,*next; //前驱和后继指针
|
||||
}DNode, *DLinkList;
|
||||
```
|
||||
|
||||
#### 初始化和判空
|
||||
|
||||
L->next = NULL改为L->next = L
|
||||
|
||||
L->prior = NULL改为L->prior = L
|
||||
|
||||
```c
|
||||
//初始化
|
||||
bool InitList(DLinkList &L){
|
||||
L = (LNode *)malloc(sizeof(DLinkList));
|
||||
if(L == NULL) return false;
|
||||
L->prior = L;
|
||||
L->next = L;
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
//判空操作
|
||||
bool Empty(DLinkList L){
|
||||
if(L->next == L){
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 判断表尾结点
|
||||
|
||||
```c
|
||||
//判断结点p是否是表尾结点
|
||||
bool isTail(DLinkList L){
|
||||
if(L->next == L){
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 插入
|
||||
|
||||
循环双链表的插入、删除算法与双链表几乎一样,不同的是如果在表尾进行,那么要让双链表继续**保持循环的性质**,即**让尾结点的next域指向头结点,同时让头结点的prior域指向尾结点**。
|
||||
|
||||
`注`:p->next->prior = s; 循环时不判断
|
||||
|
||||
`时间复杂度`=O(1)
|
||||
|
||||
```c
|
||||
//将x插入到循环链表L中*p结点的下一个结点
|
||||
bool Insert(DNode *p, int x){
|
||||
DNode *s = (DNode *)malloc(sizeof(DNode));
|
||||
if(p == NULL||s==NULL) return false;
|
||||
s->data = x;
|
||||
s->next = p->next; //1
|
||||
p->next->prior = s; //2
|
||||
s->prior = p; //3
|
||||
p->next = s; //4
|
||||
return true;
|
||||
}
|
||||
|
||||
//在循环链表L中*p结点后插入s结点
|
||||
bool Insert(DNode *p, DNode *s){
|
||||
s->next = p->next; //1
|
||||
p->next->prior = s; //2
|
||||
s->prior = p; //3
|
||||
p->next = s; //4
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 删除
|
||||
|
||||
`注`:p->next->prior = s; 循环时不判断
|
||||
|
||||
##### 按位序删除
|
||||
|
||||
`时间复杂度`=O(n)
|
||||
|
||||
```c
|
||||
//删除操作:将循环链表中的第i个结点删除
|
||||
bool Delete(DLinkList &L, int i){
|
||||
if(i<1 || i>Length(L)) return false;
|
||||
DNode *p = GetElem(L,i-1);
|
||||
DNode *q = p->next;
|
||||
p->next = q->next; //1
|
||||
q->next->prior = p; //2
|
||||
free(q);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
##### 指定结点的删除
|
||||
|
||||
`时间复杂度`=O(1)
|
||||
|
||||
```c
|
||||
//删除操作:删除循环链表中的p结点的后继结点
|
||||
void DeleteNextNode(DNode *p){
|
||||
DNode *q = p->next;
|
||||
p->next = q->next; //1
|
||||
q->next->prior = p; //2
|
||||
free(q);
|
||||
}
|
||||
```
|
||||
|
||||
#### 查找(与单双链表完全一样)
|
||||
|
||||
##### 按位查找
|
||||
|
||||
`平均时间复杂度`=O(n)
|
||||
|
||||
```c
|
||||
//按位查找:查找在循环链表L中第i个位置的结点
|
||||
DNode *GetElem(DLinkList L, int i){
|
||||
int j=0;
|
||||
DNode *p = L;
|
||||
if(i<0) return NULL;
|
||||
while(p && j<i){
|
||||
p = p->next;
|
||||
j++;
|
||||
}
|
||||
return p; //如果i大于表长,p=NULL,直接返回p即可
|
||||
}
|
||||
```
|
||||
|
||||
##### 按值查找
|
||||
|
||||
`平均时间复杂度`=O(n)
|
||||
|
||||
```c
|
||||
//按值查找:查找e在L中的位置
|
||||
DNode *LocateElem(DLinkList L, int e){
|
||||
DNode *p = L->next;
|
||||
while(p && p->data != e){
|
||||
p = p->next;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
```
|
||||
|
||||
#### 求表长、遍历、销毁
|
||||
|
||||
与单双链表一样
|
||||
|
||||
77
1.6静态链表.md
Normal file
77
1.6静态链表.md
Normal file
@@ -0,0 +1,77 @@
|
||||
## 静态链表—— Static Linked List
|
||||
|
||||
### 一、静态链表的定义
|
||||
|
||||
`静态链表`借助`数组`来描述线性表的链式存储结构,结点也有`数据域data`和`指针域next`,这里的指针是结点的`相对地址(数组下标)`,又称`游标`。和顺序表一样,静态链表也需要预先分配一块连续的内存空间。
|
||||
|
||||

|
||||
|
||||
### 二、静态链表的特点
|
||||
|
||||
顺序表:逻辑上连续,物理上连续
|
||||
|
||||
静态链表:逻辑上离散,物理上连续
|
||||
|
||||
单双链表:逻辑上离散,物理上离散
|
||||
|
||||
静态链表比顺序表好。比单双链表差
|
||||
|
||||
`优点`:增加、删除操作不需要移动大量元素
|
||||
|
||||
`缺点`:①不能随机存取。只能从头结点遍历找
|
||||
|
||||
②容量不可变
|
||||
|
||||
### 三、静态链表上的操作
|
||||
|
||||
#### 静态链表的类型描述
|
||||
|
||||
```C
|
||||
#define MaxSize 50 //静态链表的最大长度
|
||||
typedef struct SLinkList{
|
||||
ElemType data; //数据元素
|
||||
int next; //下一个元素的数组下标
|
||||
}SLinkList[MaxSize];
|
||||
```
|
||||
|
||||
#### 初始化
|
||||
|
||||
a[0]的next设为-1
|
||||
|
||||
其它的next设为-2
|
||||
|
||||
#### 判空
|
||||
|
||||
它的next为-2
|
||||
|
||||
则此结点为空
|
||||
|
||||
#### 插入
|
||||
|
||||
①找一个空的结点,存入数据
|
||||
|
||||
②从头结点出发找到位序为i-1的结点
|
||||
|
||||
③修改新结点的next
|
||||
|
||||
④修改i-1号结点的next
|
||||
|
||||
#### 删除
|
||||
|
||||
②从头结点出发找到前驱结点
|
||||
|
||||
③修改前驱结点的next
|
||||
|
||||
④被删除结点的next设为-2
|
||||
|
||||
#### 查找
|
||||
|
||||
从头结点出发挨个往后遍历结点
|
||||
|
||||
#### 求表长
|
||||
|
||||
从头结点出发挨个往后遍历结点
|
||||
|
||||
#### 遍历
|
||||
|
||||
从头结点出发挨个往后遍历结点
|
||||
36
2.1栈.md
Normal file
36
2.1栈.md
Normal file
@@ -0,0 +1,36 @@
|
||||
## 栈—— Stack
|
||||
|
||||
### 一、栈的定义
|
||||
|
||||
`栈`是线性表结构的一种,但是栈结构的插入与删除操作都只能从同一端进行,所以栈结构是一种受限制的线性表结构,数据的插入与删除符合LIFO的原则(也就是`后进先出`,`先进后出`)。
|
||||
|
||||

|
||||
|
||||
### 二、栈的基本操作
|
||||
|
||||
`注`:参数代“&”表示:方法运行完后,对参数修改的结果要`“带回来”`
|
||||
|
||||
对数据的操作:创销,增删查改
|
||||
|
||||
```c
|
||||
InitStack(&S); //初始化表:构造一个空的栈S,分配内存空间
|
||||
DestoryStack(&S); //销毁操作:销毁栈,并释放栈S所占用的内存空间
|
||||
|
||||
Push(&S,x); //进栈,若栈S未满,则将x加入使之成为新栈
|
||||
Pop(&S,&x); //出栈,若栈S非空,则弹出栈顶元素,并用x返回
|
||||
|
||||
GetTop(S,&x); //读栈顶元素,若栈S非空,则将x返回栈顶元素
|
||||
|
||||
//其它常用操作
|
||||
StackEmpty(S); //判空操作
|
||||
```
|
||||
|
||||
### 三、存储结构
|
||||
|
||||
`顺序存储`和`链式存储`
|
||||
|
||||
### 四、栈分类
|
||||
|
||||
栈的顺序存储:顺序栈
|
||||
|
||||
栈的链式存储:链栈
|
||||
148
2.2顺序栈——栈的顺序存储.md
Normal file
148
2.2顺序栈——栈的顺序存储.md
Normal file
@@ -0,0 +1,148 @@
|
||||
## 顺序栈—— Sequence Stack
|
||||
|
||||
### 一、顺序栈的定义
|
||||
|
||||
`顺序栈`:栈的`顺序存储`。
|
||||
|
||||
### 二、顺序栈的实现方式
|
||||
|
||||
实现方式:`静态分配`和`动态分配`
|
||||
|
||||
### 三、静态分配的顺序栈上的操作
|
||||
|
||||
#### 顺序栈的类型描述
|
||||
|
||||
```C
|
||||
#define MaxSize 10
|
||||
typedef struct{
|
||||
Elemtype data[MaxSize];//静态数组存放栈中元素
|
||||
int top; //栈顶指针
|
||||
}SqStack;
|
||||
```
|
||||
|
||||
#### 初始化
|
||||
|
||||
```c
|
||||
//初始化一个栈
|
||||
void InitStack(SqStack &S){
|
||||
S.top = -1; //初始化栈顶指针
|
||||
}
|
||||
```
|
||||
|
||||
#### 判栈空
|
||||
|
||||
```c
|
||||
bool StackEmpty(SqStack S){
|
||||
if(S.top == -1){
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 入栈
|
||||
|
||||
```c
|
||||
//向栈中压入元素e
|
||||
bool Push(SqStack &S,Elemtype e){
|
||||
if(S.Top == MaxSize-1) return false;
|
||||
S.Top = S.Top + 1; //栈顶指针向上移动
|
||||
S.data[S.top] = e;
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 出栈
|
||||
|
||||
```c
|
||||
//栈顶元素出栈
|
||||
bool Pop(Stack &S,Elemtype &e){
|
||||
if(S.Top == -1) return false;
|
||||
e = S.data[S.Top]; //x为栈顶元素,栈顶指针下移一个位置
|
||||
S.Top = S.Top - 1;
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取栈顶元素
|
||||
|
||||
```c
|
||||
//获取栈顶元素e
|
||||
bool GetTop(Stack &S,Elemtype &e){
|
||||
if(S.Top==S.Base) return false;
|
||||
e = S.data[S.Top];
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### 四、动态分配的顺序栈上的操作
|
||||
|
||||
#### 顺序栈的类型描述
|
||||
|
||||
```C
|
||||
typedef struct Stack{
|
||||
Elemtype *Top;//指向栈顶元素的上一个
|
||||
Elemtype *Base;//指向栈底
|
||||
int stacksize;//当前栈的空间大小
|
||||
}SqStack
|
||||
```
|
||||
|
||||
#### 初始化
|
||||
|
||||
```c
|
||||
//初始化一个栈
|
||||
bool InitStack(SqStack &S){
|
||||
S.Base=(Elemtype *)malloc(MAXSIZE*sizeof(Elemtype));
|
||||
if(!S.Base) return false;
|
||||
S.Top=S.Base;
|
||||
S.stacksize=MAXSIZE;
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 判栈空
|
||||
|
||||
```c
|
||||
bool StackEmpty(SqStack S){
|
||||
if(S.Top == S.Base){
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 入栈
|
||||
|
||||
```c
|
||||
//向栈中压入元素e
|
||||
bool Push(SqStack &S,Elemtype e){
|
||||
if(S.Top-S.Base==S.stacksize) return false;
|
||||
*S.Top=e;
|
||||
S.Top++; //栈顶指针向上移动
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 出栈
|
||||
|
||||
```c
|
||||
//栈顶元素出栈
|
||||
bool Pop(Stack &S,Eletype &x){
|
||||
if(S.Top==S.Base) return false;
|
||||
x=*--S.Top; //x为栈顶元素,栈顶指针下移一个位置
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取栈顶元素
|
||||
|
||||
```c
|
||||
//获取栈顶元素e
|
||||
bool GetTop(Stack &S,Elemtype &e){
|
||||
if(S.Top==S.Base) return false;
|
||||
e = *(S.Top-1);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
166
2.3链栈——栈的链式存储.md
Normal file
166
2.3链栈——栈的链式存储.md
Normal file
@@ -0,0 +1,166 @@
|
||||
## 链栈—— Linked Stack
|
||||
|
||||
### 一、链栈的定义
|
||||
|
||||
`链栈`:栈的`链式存储`。
|
||||
|
||||
### 二、链栈的实现方式
|
||||
|
||||
实现方式:`不带头结点`和`带头结点`,一般带头结点比不带头结点好
|
||||
|
||||
不带头结点:写操作代码麻烦,要区分第一个数据和后续数据的处理
|
||||
|
||||
带头结点:写操作代码方便,一般用带头结点,不明确的都是带头结点的
|
||||
|
||||
`注`:这两种方式:只有类型描述一样,初始化不一样,
|
||||
|
||||
判空、入栈、出栈、取栈顶元素不一样,不带头节点是s,带头结点是s->next,因为链栈以`链头`为`栈顶`
|
||||
|
||||
### 三、不带头结点的链栈上的操作(与不带头结点的单链表一样)
|
||||
|
||||
`链头`为`栈顶`
|
||||
|
||||
#### 链栈的类型描述
|
||||
|
||||
```C
|
||||
typedef struct LNode{ //定义单链表结点类型
|
||||
int data; //数据域,可以是别的各种数据类型,本文统一用int类型
|
||||
struct LNode *next; //指针域
|
||||
}LNode, *LinkStack;
|
||||
```
|
||||
|
||||
#### 初始化
|
||||
|
||||
```c
|
||||
//初始化
|
||||
void InitStack(LinkStack &S){
|
||||
S = NULL;
|
||||
S->next = NULL;
|
||||
}
|
||||
```
|
||||
|
||||
#### 判栈空
|
||||
|
||||
```c
|
||||
//判空操作
|
||||
bool StackEmpty(LinkStack S){
|
||||
if(S == NULL){
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 入栈(与单链表插入一样)
|
||||
|
||||
```c
|
||||
bool push(LNode *s, int x){
|
||||
if(s==NULL) return false;
|
||||
LNode *p = (LNode*)malloc(sizeof(LNode));
|
||||
if(p==NULL) return false;
|
||||
p->next = NULL;
|
||||
p->data = x;
|
||||
p->next = s;
|
||||
s = p;
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 出栈(与单链表删除一样)
|
||||
|
||||
```c
|
||||
bool Delete(LNode *s){
|
||||
if(s==NULL) return false;
|
||||
LNode *q = s;
|
||||
s->data = q->data
|
||||
s = q->next;
|
||||
free(q);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取栈顶元素(与单链表删除一样)
|
||||
|
||||
```c
|
||||
//获取栈顶元素e
|
||||
bool GetTop(LNode &s,Elemtype &e){
|
||||
if(s == NULL) return false;
|
||||
e = s->data;
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### 四、带头结点的链栈上的操作
|
||||
|
||||
s都改为s->next,类型描述和初始化例外
|
||||
|
||||
#### 链栈的类型描述
|
||||
|
||||
```C
|
||||
typedef struct LNode{ //定义单链表结点类型
|
||||
int data; //数据域,可以是别的各种数据类型,本文统一用int类型
|
||||
struct LNode *next; //指针域
|
||||
}LNode, *LinkStack;
|
||||
```
|
||||
|
||||
#### 初始化
|
||||
|
||||
```c
|
||||
//初始化
|
||||
void InitStack(LinkStack &S){
|
||||
S = (LNode*)malloc(sizeof(LNode));
|
||||
S->next = NULL;
|
||||
}
|
||||
```
|
||||
|
||||
#### 判栈空
|
||||
|
||||
```c
|
||||
bool StackEmpty(LinkStack S){
|
||||
if(S->next == NULL){
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 入栈(与单链表插入一样)
|
||||
|
||||
```c
|
||||
bool push(LNode *s, int x){
|
||||
if(s==NULL) return false;
|
||||
LNode *p = (LNode*)malloc(sizeof(LNode));
|
||||
if(p==NULL) return false;
|
||||
p->next = NULL;
|
||||
p->data = x;
|
||||
p->next = s->next;
|
||||
s->next = p;
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 出栈(与单链表删除一样)
|
||||
|
||||
```c
|
||||
bool Delete(LNode *s){
|
||||
if(s==NULL) return false;
|
||||
LNode *q = s->next;
|
||||
s->data = q->data
|
||||
s->next = q->next;
|
||||
free(q);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取栈顶元素
|
||||
|
||||
```c
|
||||
//获取栈顶元素e
|
||||
bool GetTop(LNode &s,Elemtype &e){
|
||||
if(s->next == NULL) return false;
|
||||
e = s->next->data;
|
||||
return true;
|
||||
}
|
||||
```
|
||||
63
2.4栈的应用.md
Normal file
63
2.4栈的应用.md
Normal file
@@ -0,0 +1,63 @@
|
||||
## 栈的应用
|
||||
|
||||
### 一、表达式求值(中缀表达式求值)
|
||||
|
||||
三种表达式:`中缀表达式`、`后缀表达式`、`前缀表达式`
|
||||
|
||||
中缀表达式:有界限符
|
||||
|
||||
后缀和前缀表达式:无界限符
|
||||
|
||||
`注`:方法一和方法二是分开的,方法三是中缀转后缀和后缀计算同时进行,一般用方法三解决
|
||||
|
||||
#### 方法一:中缀表达式转后缀表达式,再用后缀表达式求值
|
||||
|
||||
##### 1.中缀表达式转后缀表达式(用`栈`保存`运算符`)
|
||||
|
||||
中缀表达式`从左往右`扫描:
|
||||
|
||||
①遇到`操作数`,直接压入栈。
|
||||
|
||||
②遇到`界限符`,遇到“(”,直接压入栈;遇到“)”,依次弹出栈内运算符`从左往右`加入表达式,直到弹出“(”结束。
|
||||
|
||||
③遇到`运算符`,依次弹出比这个运算符优先级高的所有运算符,将此运算符压入栈。
|
||||
|
||||
##### 2.后缀表达式求值(用`栈`存`运算结果`)
|
||||
|
||||
后缀表达式`从左往右`扫描:
|
||||
|
||||
①遇到`操作数`,直接压入栈。
|
||||
|
||||
②遇到`运算符`,弹出两个元素,执行相应运算,将运算结果压回栈顶
|
||||
|
||||
#### 方法二:中缀表达式转前缀表达式,再用前缀表达式求值
|
||||
|
||||
##### 1.中缀表达式转前缀表达式(用`栈`保存`运算符`)
|
||||
|
||||
中缀表达式`从右往左`扫描:
|
||||
|
||||
①遇到`操作数`,直接压入栈。
|
||||
|
||||
②遇到`界限符`,遇到“(”,直接压入栈;遇到“)”,依次弹出栈内运算符`从右往左`加入表达式,直到弹出“(”结束。
|
||||
|
||||
③遇到`运算符`,依次弹出比这个运算符优先级高的所有运算符,将此运算符压入栈。
|
||||
|
||||
##### 2.前缀表达式求值(用`栈`存`运算结果`)
|
||||
|
||||
前缀表达式`从右往左`扫描:
|
||||
|
||||
①遇到`操作数`,直接压入栈。
|
||||
|
||||
②遇到`运算符`,弹出两个元素,执行相应运算,将运算结果压回栈顶
|
||||
|
||||
#### 方法三:中缀转后缀和后缀计算同时进行(两个栈)
|
||||
|
||||
用`两个栈`:分别存`操作数`和`运算符`
|
||||
|
||||
①遇到`操作数`,直接压入操作数栈。
|
||||
|
||||
②遇到`运算符`或`界限符`,按“中缀转后缀”的逻辑压入运算符栈,每当弹出一个运算符时,相应地弹出两个操作数,执行相应运算,将运算结果压回操作数栈
|
||||
|
||||
### 二、递归
|
||||
|
||||
递归的背后是栈的应用
|
||||
36
3.1队列.md
Normal file
36
3.1队列.md
Normal file
@@ -0,0 +1,36 @@
|
||||
## 队列—— Queue
|
||||
|
||||
### 一、队列的定义
|
||||
|
||||
`队列`是只允许在一端进行插入,在另一端进行删除的线性表(`先进先出`,`后进后出`)
|
||||
|
||||

|
||||
|
||||
### 二、队列的基本操作
|
||||
|
||||
`注`:参数代“&”表示:方法运行完后,对参数修改的结果要`“带回来”`
|
||||
|
||||
对数据的操作:创销,增删查改
|
||||
|
||||
```c
|
||||
InitQueue(&Q); //初始化队列:构造一个空队列Q,分配内存空间
|
||||
DestoryQueue(&Q); //销毁操作:销毁队列,并释放队列Q所占用的内存空间
|
||||
|
||||
EnQueue(&Q,x); //入队,若队列Q未满,则将x加入使之成为新的队尾
|
||||
DeQueue(&Q,&x); //出队,若队列Q非空,则删除队头元素,并用x返回
|
||||
|
||||
GetHead(Q,&x); //读队头元素,若队列Q非空,则将x返回队头元素
|
||||
|
||||
//其它常用操作
|
||||
QueueEmpty(Q); //判空操作
|
||||
```
|
||||
|
||||
### 三、存储结构
|
||||
|
||||
`顺序存储`和`链式存储`
|
||||
|
||||
### 四、队列分类
|
||||
|
||||
队列的顺序存储:顺序队列
|
||||
|
||||
队列的链式存储:链式队列
|
||||
143
3.2顺序队列(循环队列)——队列的顺序存储.md
Normal file
143
3.2顺序队列(循环队列)——队列的顺序存储.md
Normal file
@@ -0,0 +1,143 @@
|
||||
## 顺序队列—— Sequence Queue
|
||||
|
||||
### 一、顺序队列的定义
|
||||
|
||||
`顺序队列`:队列的`顺序存储`。
|
||||
|
||||
### 二、顺序队列的实现方式
|
||||
|
||||
实现方式:`静态分配`和`动态分配`
|
||||
|
||||
`注`:这两种方式:只是类型描述和初始化不一样,判空、入队、出队、取队头操作都一样
|
||||
|
||||
### 三、顺序队列上的操作
|
||||
|
||||
以`循环队列`为主
|
||||
|
||||
#### 静态分配的顺序队列:类型描述和初始化
|
||||
|
||||
```C
|
||||
#define MaxSize 10
|
||||
typedef struct{
|
||||
Elemtype data[MaxSize];//静态数组存放队列中元素
|
||||
Elemtype front,rear; //队列顶指针
|
||||
}SqQueue;
|
||||
```
|
||||
|
||||
```c
|
||||
//初始化一个队列
|
||||
void InitStack(SqQueue &Q){
|
||||
Q.rear = Q.front = 0; //初始化队列顶指针
|
||||
}
|
||||
```
|
||||
|
||||
#### 动态分配的顺序队列:类型描述和初始化
|
||||
|
||||
```C
|
||||
typedef struct{
|
||||
Elemtype *base; //静态数组存放队列中元素
|
||||
Elemtype front,rear; //队列顶指针
|
||||
}SqQueue;
|
||||
```
|
||||
|
||||
```c
|
||||
//初始化一个队列
|
||||
bool InitQueue(SqQueue &Q){
|
||||
Q.base=(Elemtype *)malloc(MAXQSIZE*sizeof(Elemtype));
|
||||
if(!Q.Base) return false;
|
||||
Q.front = Q.rear = 0;
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 判队列空
|
||||
|
||||
```c
|
||||
//判空
|
||||
bool StackEmpty(SqStack S){
|
||||
if(Q.rear == Q.front){ //队列已空
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 入队(循环队列)
|
||||
|
||||
```c
|
||||
//入队
|
||||
bool EnQueue(SqQueue &Q,Elemtype e){
|
||||
if((Q.rear+1)%MAXSIZE == Q.front) return false; //队列已满
|
||||
Q.data[Q.rear] = e; //e为队尾元素
|
||||
Q.rear = (Q.rear + 1)%MaxSize; //队尾指针后移
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 出队(循环队列)
|
||||
|
||||
```c
|
||||
//出队
|
||||
bool DeQueue(SqQueue &Q,Elemtype &e){
|
||||
if(Q.rear == Q.front) return false;
|
||||
e = Q.data[Q.front]; //e为队头元素
|
||||
Q.front=(Q.front+1)%MAXQSIZE; //队头指针后移
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取队头元素
|
||||
|
||||
```c
|
||||
//获取队头元素
|
||||
bool GetHead(SqQueue &Q,Elemtype &e){
|
||||
if(Q.rear == Q.front) return false;
|
||||
e = Q.data[Q.front]; //e为队列顶元素
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 队列已空/已满
|
||||
|
||||
方案一:
|
||||
|
||||
```c
|
||||
#define MaxSize 10
|
||||
typedef struct{
|
||||
Elemtype data[MaxSize];//静态数组存放队列中元素
|
||||
int front,rear; //队列顶指针
|
||||
}SqQueue;
|
||||
```
|
||||
|
||||
已空:Q.rear == Q.front 已满:(Q.rear+1)%MAXSIZE == Q.front
|
||||
|
||||
方案二:
|
||||
|
||||
```c
|
||||
#define MaxSize 10
|
||||
typedef struct{
|
||||
Elemtype data[MaxSize];//静态数组存放队列中元素
|
||||
int front,rear; //队列顶指针
|
||||
int size; //队列当前长度
|
||||
}SqQueue;
|
||||
```
|
||||
|
||||
已空:size == 0 已满:size == MAXSIZE
|
||||
|
||||
方案三:
|
||||
|
||||
```c
|
||||
#define MaxSize 10
|
||||
typedef struct{
|
||||
Elemtype data[MaxSize];//静态数组存放队列中元素
|
||||
int front,rear; //队列顶指针
|
||||
int tag; //记录最近是删除还是插入,0是删除,1是插入
|
||||
}SqQueue;
|
||||
```
|
||||
|
||||
已空:Q.rear == Q.front && tag = 0 已满:Q.rear == Q.front && tag = 1
|
||||
|
||||
#### 元素的个数
|
||||
|
||||
(Q.rear+MAXSIZE-Q.front)%MAXSIZE == Q.front
|
||||
183
3.3链队列——队列的链式存储.md
Normal file
183
3.3链队列——队列的链式存储.md
Normal file
@@ -0,0 +1,183 @@
|
||||
## 链队列—— Linked Queue
|
||||
|
||||
### 一、链队列的定义
|
||||
|
||||
`链队列`:队列的`链存储`。
|
||||
|
||||
### 二、链队列的实现方式
|
||||
|
||||
实现方式:`不带头结点`和`带头结点`,一般带头结点比不带头结点好
|
||||
|
||||
`注`:这两种方式:类型描述相同,初始化和判空不同
|
||||
|
||||
入队,不带头节点要对第一个特殊处理
|
||||
|
||||
出队,取队头元素不一样,不带头节点是Q.front,带头结点是Q.front->next,因为链队以`链头`为`队头`
|
||||
|
||||
### 三、不带头结点的链队列上的操作
|
||||
|
||||
#### 链队列的类型描述
|
||||
|
||||
```C
|
||||
typedef struct LNode{ //定义单链表结点类型
|
||||
ElemType data; //数据域,可以是别的各种数据类型,本文统一用int类型
|
||||
struct LNode *next; //指针域
|
||||
}LNode;
|
||||
typedef struct{
|
||||
LNode *front, *rear;
|
||||
}LinkQueue;
|
||||
```
|
||||
|
||||
#### 初始化
|
||||
|
||||
```C
|
||||
//初始化一个队列
|
||||
bool InitQueue(LinkQueue &Q){
|
||||
//初始时,front, rear都指向NULL
|
||||
Q.front = NULL;
|
||||
Q.rear = NULL;
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 判空
|
||||
|
||||
```c
|
||||
//判空
|
||||
bool StackEmpty(LinkQueue S){
|
||||
if(Q.front == NULL){ //队列已空
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 入队
|
||||
|
||||
```c
|
||||
//入队
|
||||
void EnQueue(LinkQueue &Q,Elemtype e){
|
||||
LNode *s = (LNode *)malloc(sizeof(LNode));
|
||||
s->data = e; //e为队尾元素
|
||||
s->next = NULL;
|
||||
//对第一个特殊处理
|
||||
if(Q.front == NULL){
|
||||
Q.front = s;
|
||||
Q.rear = s;
|
||||
}
|
||||
Q.rear->next = s; //新结点插入到rear后
|
||||
Q.rear s; //队尾指针后移
|
||||
}
|
||||
```
|
||||
|
||||
#### 出队
|
||||
|
||||
```c
|
||||
//出队
|
||||
bool DeQueue(LinkQueue &Q,Elemtype &e){
|
||||
if(Q.rear == NULL) return false;
|
||||
LNode *p = Q.front;
|
||||
e = p->data; //e为队头元素
|
||||
Q.front = p->next; //队头指针后移
|
||||
if(Q.rear == p){ //最后一个结点出队
|
||||
Q.front = NULL;
|
||||
Q.rear = NULL;
|
||||
}
|
||||
free(p);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取队头元素
|
||||
|
||||
```c
|
||||
//获取队头元素
|
||||
bool GetHead(SqQueue &Q,Elemtype &e){
|
||||
if(Q.rear == Q.front) return false;
|
||||
e = Q.data[Q.front]; //e为队列顶元素
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### 四、带头结点的链队列上的操作
|
||||
|
||||
#### 链队列的类型描述
|
||||
|
||||
```C
|
||||
typedef struct LNode{ //定义单链表结点类型
|
||||
ElemType data; //数据域,可以是别的各种数据类型,本文统一用int类型
|
||||
struct LNode *next; //指针域
|
||||
}LNode;
|
||||
typedef struct{
|
||||
LNode *front, *rear;
|
||||
}LinkQueue;
|
||||
```
|
||||
|
||||
#### 初始化
|
||||
|
||||
```c
|
||||
//初始化一个队列
|
||||
bool InitQueue(LinkQueue &Q){
|
||||
////初始时,front, rear都指向头结点
|
||||
Q.front = Q.rear = (LNode *)malloc(sizeof(LNode));
|
||||
Q.front = Q.rear = NULL;
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 判空
|
||||
|
||||
```c
|
||||
//判空
|
||||
bool StackEmpty(SqStack S){
|
||||
if(Q.rear == Q.front){ //队列已空
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
//或
|
||||
//判空
|
||||
bool StackEmpty(SqStack S){
|
||||
if(Q.front->next == NULL){ //队列已空
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 入队(循环队列)
|
||||
|
||||
```c
|
||||
//入队
|
||||
void EnQueue(LinkQueue &Q,Elemtype e){
|
||||
LNode *s = (LNode *)malloc(sizeof(LNode));
|
||||
s->data = e; //e为队尾元素
|
||||
s->next = NULL;
|
||||
Q->rear->next = s; //新结点插入到rear后
|
||||
Q.rear s; //队尾指针后移
|
||||
}
|
||||
```
|
||||
|
||||
#### 出队
|
||||
|
||||
```c
|
||||
//出队
|
||||
bool DeQueue(LinkQueue &Q,Elemtype &e){
|
||||
if(Q.rear == NULL) return false;
|
||||
LNode *p = Q.front->next;
|
||||
e = p->data; //e为队头元素
|
||||
Q.front->next = p->next; //队头指针后移
|
||||
if(Q.rear == p){ //最后一个结点出队
|
||||
Q.front = Q.rear;
|
||||
}
|
||||
free(p);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### 五、队列已满
|
||||
|
||||
链队一般不会队满
|
||||
20
3.4双端队列——队列的改进.md
Normal file
20
3.4双端队列——队列的改进.md
Normal file
@@ -0,0 +1,20 @@
|
||||
## 双端队列—— Double-ended Queue
|
||||
|
||||
### 一、双端队列的定义
|
||||
|
||||
`线性表`:任何位置插入删除
|
||||
|
||||
栈、队列、双端队列都是只能两端插入删除的线性表
|
||||
|
||||
`栈`:一端插入删除
|
||||
|
||||
`共享栈`:栈的变种,两端为栈底,中间为栈顶,只能两端向中间插入删除
|
||||
|
||||
`队列`:一端插入,另一端删除
|
||||
|
||||
`双端队列`:两端都可插入删除
|
||||
|
||||
`输入受限的双端队列`:一端插入,两端删除
|
||||
|
||||
`输出受限的双端队列`:两端插入,一端删除
|
||||
|
||||
11
3.5队列的应用.md
Normal file
11
3.5队列的应用.md
Normal file
@@ -0,0 +1,11 @@
|
||||
## 队列的应用
|
||||
|
||||
### 一、树的层次遍历
|
||||
|
||||
### 二、图的广度优先遍历
|
||||
|
||||
### 三、操作系统中的应用
|
||||
|
||||
多个进程争抢有限的系统资源时,采用`先来先服务算法(FCFS)`
|
||||
|
||||
例子:CPU资源分配;打印数据缓冲区
|
||||
69
3.6特殊矩阵的压缩存储.md
Normal file
69
3.6特殊矩阵的压缩存储.md
Normal file
@@ -0,0 +1,69 @@
|
||||
## 特殊矩阵的压缩存储
|
||||
|
||||
### 一、数组的存储结构
|
||||
|
||||
`一维数组`:$$a[N]$$
|
||||
逻辑上连续存放,物理上(内存中)也连续存放
|
||||
数组元素$$a[i]$$的`物理地址=LOC+i*sizeof(ElemType)`
|
||||
|
||||
`二维数组`:$$a[N][M]$$
|
||||
逻辑上是n行n列的矩阵,物理上(内存中)是`行优先存储`和`列优先存储`的连续存放
|
||||
`行优先存储`:数组元素$$a[i][j]$$的`物理地址=LOC+(i*N+j)*sizeof(ElemType)`
|
||||
`列优先存储`:数组元素$$a[i][j]$$的`物理地址=LOC+(J*M+i)*sizeof(ElemType)`
|
||||
|
||||
普通矩阵的存储可用二维数组存储。
|
||||
|
||||
### 二、特殊矩阵的存储
|
||||
|
||||
①对称矩阵
|
||||
②三角矩阵
|
||||
③三对角矩阵
|
||||
④稀疏矩阵
|
||||
|
||||
#### 2.1对称矩阵的压缩存储
|
||||
|
||||
`对称矩阵`:$$a_{i,j}=a_{j,i}$$
|
||||
方法:一维数组$$a[N]$$只存主对角线+下三角区(或主对角线+上三角区)
|
||||
存储数组的大小:$$N=\frac{n(n+1)}{2}$$
|
||||
数组下标范围:$$0$$ ~ $$\frac{n(n+1)}{2}-1$$
|
||||
`行优先存储`:数组下标:$$k=\begin{cases} \frac{i(i-1)}{2}+j-1, \quad i \geq j(下三角区和主对角线元素)\\ \frac{j(j-1)}{2}+i-1, \quad i<j(上三角区元素a_{i,j}=a_{j,i}) \end{cases}$$
|
||||
|
||||
#### 2.2三角矩阵的压缩存储
|
||||
|
||||
①`下三角矩阵`:除主对角线和下三角区,其余的元素都相等
|
||||
方法:一维数组$$a[N]$$存主对角线+下三角区,在最后多加一个位置存常其余相等元素
|
||||
存储数组的大小:$$N=\frac{n(n+1)}{2}+1$$
|
||||
数组下标范围:$$0$$ ~ $$\frac{n(n+1)}{2}$$
|
||||
`行优先存储`:数组下标:$$k=\begin{cases} \frac{i(i-1)}{2}+j-1, \quad i \geq j(下三角区和主对角线元素)\\ \frac{n(n+1)}{2}, \quad\quad\quad\quad i<j(上三角区元素) \end{cases}$$
|
||||
|
||||
②`上三角矩阵`:除主对角线和上三角区,其余的元素都相等
|
||||
方法:一维数组$$a[N]$$存主对角线+下三角区,在最后多加一个位置存常其余相等元素
|
||||
存储数组的大小:$$N=\frac{n(n+1)}{2}+1$$
|
||||
数组下标范围:$$0$$ ~ $$\frac{n(n+1)}{2}$$
|
||||
`行优先存储`:数组下标:$$k=\begin{cases} \frac{(i-1)(2n-i+2)}{2}+(j-i), \quad i \geq j(下三角区和主对角线元素)\\ \frac{n(n+1)}{2}, \quad\quad\quad\quad\quad\quad\quad~ i<j(上三角区元素) \end{cases}$$
|
||||
|
||||
#### 2.3三对角矩阵的压缩存储
|
||||
|
||||
`三对角矩阵`,又称`带状矩阵`。
|
||||
方法:一维数组$$a[N]$$存带状部分
|
||||
存储数组的大小:$$N=3n-3+1$$
|
||||
数组下标范围:$$0$$ ~ $$3n-3$$
|
||||
`行优先存储`:
|
||||
`i和j计算数组下标k`:$$k=2i+j-3$$
|
||||
`数组下标k计算i和j`:$$i=\lceil (k+2)/3 \rceil$$,$$j=k-2i+3$$
|
||||
|
||||
#### 2.4稀疏矩阵的压缩存储
|
||||
|
||||
`稀疏矩阵`:非零元素的个数远远少于矩阵元素的个数。
|
||||
|
||||
`方法一:顺序存储——三元组<行,列,值>`,注:行列从1开始
|
||||
|
||||
| i(行) | j(列) | v(值) |
|
||||
| ------- | ------- | ------- |
|
||||
| 1 | 3 | 4 |
|
||||
| 1 | 6 | 5 |
|
||||
| 2 | 2 | 3 |
|
||||
|
||||
`方法二:链式存储——十字链表法`
|
||||
|
||||

|
||||
28
4.1串.md
Normal file
28
4.1串.md
Normal file
@@ -0,0 +1,28 @@
|
||||
## 串,即字符串——String
|
||||
|
||||
### 一、串的定义
|
||||
|
||||
`串`是一种特殊的`线性表`,串的`数据对象`限定为`字符集`
|
||||
|
||||
### 二、串的基本操作
|
||||
|
||||
`注`:参数代“&”表示:方法运行完后,对参数修改的结果要`“带回来”`
|
||||
|
||||
对数据的操作:创销,增删查改
|
||||
|
||||
```c
|
||||
StrAssign(&τ chars);//赋值操作。把串T赋值为chars
|
||||
StrCopy(&TS); //复制操作。由串S复制得到串T。
|
||||
StrEmpty(S); //判空操作。若S为空串,则返回TRUE,否则返回 FALSE。
|
||||
StrEngth(S); //求串长。返回串S的元素个数
|
||||
ClearString(&S); //清空操作。将S清为空串。
|
||||
Destroystring(&S); //销毁串。将串S销毁(回收存储空间)。
|
||||
Concat(&TS1, S2); //串联接。用T返回由S1和S2联接而成的新串
|
||||
SubString(&sub,S, pos, len); //求子串。用Sub返回串S的第pos个字符起长度为|en的子串。
|
||||
ndex(S, T); //定位操作。若主串S中存在与串T值相同的子串,则返回它在主串S中第一次出现的位置;否则函数值为0。
|
||||
StrCompare(S,T); //比较操作。若S>T,则返回值>0;若S=T,则返回值=0;若S<T,则返回值<0。
|
||||
```
|
||||
|
||||
### 三、存储结构
|
||||
|
||||
`顺序存储`和`链式存储`
|
||||
121
4.2串的存储结构.md
Normal file
121
4.2串的存储结构.md
Normal file
@@ -0,0 +1,121 @@
|
||||
## 串的存储结构
|
||||
|
||||
### 一、串的存储结构
|
||||
|
||||
`顺序存储`和`链式存储`
|
||||
|
||||
### 二、串的顺序存储的实现方式
|
||||
|
||||
实现方式:`静态分配`和`动态分配`,一般用动态分配
|
||||
|
||||
#### 串的类型描述:
|
||||
|
||||
##### 静态分配:SString
|
||||
|
||||
```C
|
||||
#define MAXLEN 255; //定义最大长度
|
||||
typedef struct{
|
||||
char ch[MAXLEN]; //“静态”的数组存数据,存字符
|
||||
int length; //串的实际长度
|
||||
}SString;
|
||||
```
|
||||
|
||||
##### 动态分配:HString
|
||||
|
||||
```c
|
||||
typedef struct{
|
||||
char *ch; //指向“动态”分配的串的基地址
|
||||
int length; //顺序表的当前长度
|
||||
}HString;
|
||||
```
|
||||
|
||||
#### 静态分配的顺序存储的串的优缺点
|
||||
|
||||
`缺点`:串的顺序存储的表长确定后无法修改,存满了就存不了了
|
||||
|
||||
#### 动态分配的顺序表的优缺点:
|
||||
|
||||
`优点`:可以动态增加长度
|
||||
|
||||
`缺点`:动态增加长度中的迁移工作时间开销大
|
||||
|
||||
### 三、串的链式存储的实现方式
|
||||
|
||||
实现方式:`不带头结点`和`带头结点`,一般用带头结点
|
||||
|
||||
`不带头结点`和`带头结点`的类型描述相同,初始化和判空不同
|
||||
|
||||
#### 串的类型描述:
|
||||
|
||||
分为:`单个分配`和`堆分配`,一般用堆分配
|
||||
|
||||
单个分配存储密度低,堆分配存储密度高
|
||||
|
||||
##### 单个分配:
|
||||
|
||||
```c
|
||||
typedef struct StringNode{
|
||||
char ch; //每个结点存1个字符
|
||||
struct StringNode *next;
|
||||
}StringNode, *String;
|
||||
```
|
||||
|
||||
##### 堆分配:
|
||||
|
||||
```c
|
||||
typedef struct StringNode{
|
||||
char ch[4]; //每个结点存4个字符
|
||||
struct StringNode *next;
|
||||
}StringNode, *String;
|
||||
```
|
||||
|
||||
### 四、串的上的操作
|
||||
|
||||
以静态分配的顺序串为主
|
||||
|
||||
#### 求子串
|
||||
|
||||
```c
|
||||
//求子串
|
||||
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.ch[i-pos+1] =S.ch[i];
|
||||
}
|
||||
Sub.length = len;
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 字符串比较
|
||||
|
||||
```c
|
||||
//字符串比较操作。若S>T,则返回值>0;若S=T,则返回值=0;若S<T,则返回值<0。
|
||||
int StrCompare(SString S, SString T){
|
||||
for(int i=1; i<S.length && T.length; i++){
|
||||
if(S.ch[i]!=T.ch[i]) return S.ch[i]-T[i];
|
||||
}
|
||||
//扫描过所有字符都相等,则长度更长的串更大
|
||||
return S.length-T.length;
|
||||
}
|
||||
```
|
||||
|
||||
#### 定位操作
|
||||
|
||||
方法:在S中依次按顺序取m长子串,判断是否与T相同
|
||||
|
||||
```c
|
||||
//定位操作
|
||||
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相同的子串
|
||||
}
|
||||
```
|
||||
|
||||
112
4.3字符串模式匹配.md
Normal file
112
4.3字符串模式匹配.md
Normal file
@@ -0,0 +1,112 @@
|
||||
## 字符串模式匹配
|
||||
|
||||
`字符串模式匹配`:在`主串`中找到与`模式串`相同的子串,并返回其所在位置。
|
||||
|
||||
`子串`:一定能在主串中找到的串
|
||||
|
||||
`模式串`:不一定能在主串中找到的串
|
||||
|
||||
方法:`朴素模式匹配算法`和`KMP算法`
|
||||
|
||||
### 一、朴素模式匹配算法
|
||||
|
||||
#### 1.用串的定位操作:
|
||||
|
||||
方法:在S中依次按顺序取m长子串,判断是否与T相同
|
||||
|
||||
```c
|
||||
//定位操作
|
||||
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
|
||||
int Index(SString S, SString T){
|
||||
int i = 1, j = 1;
|
||||
while(i<=S.length && j<=T.length){ //跳出循环情况:j>T.length,匹配成功
|
||||
if(S.ch[i]==T.ch[i]){ //i>S.length,匹配失败
|
||||
++i; ++j; //继续比较后面的字符
|
||||
}else{ //匹配失败,指针i、j都后退,匹配下一个
|
||||
i = i-j+2;
|
||||
j = 1;
|
||||
}
|
||||
}
|
||||
if(j > T.length){ //j>T.length,匹配成功
|
||||
return i-T.length;
|
||||
}else{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 朴素模式匹配算法的时间复杂度:
|
||||
|
||||
设主串长度为n,模式串长度为m,则
|
||||
|
||||
最好情况:每次匹配第一个字符就匹配失败,直到最后才匹配成功,循环n次,`最好时间复杂度`=O(n)
|
||||
|
||||
最坏情况:到最后也没找到,主串移动n个元素,模式串移动m个元素,`最坏时间复杂度`=O(mn)
|
||||
|
||||
### 二、KMP算法
|
||||
|
||||
精髓:利用好已经匹配过的模式串信息,建立一个next数组,表示j的回溯
|
||||
|
||||
```c
|
||||
int Index(SString S, SString T){
|
||||
int i = 1, j = 1;
|
||||
while(i<=S.length && j<=T.length){ //跳出循环情况:j>T.length,匹配成功
|
||||
if(S.ch[i]==T.ch[i]){ //i>S.length,匹配失败
|
||||
++i; ++j; //继续比较后面的字符
|
||||
}else{ //匹配失败,指针i不变,j后退,匹配下一个
|
||||
j=next[j];
|
||||
}
|
||||
}
|
||||
if(j > T.length){ //j>T.length,匹配成功
|
||||
return i-T.length;
|
||||
}else{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### KMP算法的时间复杂度:
|
||||
|
||||
设主串长度为n,模式串长度为m,则
|
||||
|
||||
最好情况:每次匹配第一个字符就匹配失败,直到最后才匹配成功,循环n次,`最好时间复杂度`=O(n)
|
||||
|
||||
最坏情况:到最后也没找到,主串移动n个元素,模式串移动m个元素,`最坏时间复杂度`=O(m+n)
|
||||
|
||||
#### next数组的建立
|
||||
|
||||
P[0 ~ k-1] == P[j-k ~ j-1]
|
||||
|
||||

|
||||
|
||||
```c
|
||||
//next数组的建立
|
||||
int getNext(String ps){
|
||||
char[] p = ps.toCharArray();
|
||||
int[] next = new int[p.length];
|
||||
next[1] = 0;
|
||||
int j = 1, k = 0;
|
||||
while (j < p.length){
|
||||
if (k == 0 || p[j] == p[k]) {
|
||||
next[++j] = ++k;
|
||||
}else{
|
||||
k = next[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
61
5.1树.md
Normal file
61
5.1树.md
Normal file
@@ -0,0 +1,61 @@
|
||||
## 树 —— Tree
|
||||
|
||||
### 一、树的定义:
|
||||
|
||||
`树`是η(n≥0)个结点的有限集合,n≡o时,称为空树,这是一种特殊情况。在任意一棵非空树中应满足:
|
||||
①有且仅有一个特定的称为根的`结点`。
|
||||
②当n>1时,其余结点可分为m(m>0)个互不相交的有限集合T1,T2…,Tn,其中每个集合本身又是一棵树,并且称为根结点的`子树`。
|
||||
|
||||

|
||||
|
||||
### 二、树的术语
|
||||
|
||||
`父结点`:若一个结点含有子结点,则这个结点称为其子结点的父结点
|
||||
`子结点`:一个结点含有的子树的根结点称为该结点的子结点
|
||||
`兄弟结点`:拥有共同父结点的结点互称为兄弟结点
|
||||
`祖先`:对任意结点x,从根结点到结点x的所有结点都是x的祖先(结点x也是自己的祖先)
|
||||
`后代`:对任意结点x,从结点x到叶子结点的所有结点都是x的后代(结点x也是自己的后代)
|
||||
|
||||
`两结点的路径`:对任意结点x,从结点x到结点y的`从上到下`的路
|
||||
`两结点的路径长度`:对任意结点x,从结点x到结点y经过的边数
|
||||
|
||||
`结点的层次`:对任意结点x,从根结点到结点x的`从上到下`经过的边数
|
||||
`结点高度`:对任意结点x,叶子结点到x结点的路径长度就是结点x的`高度`
|
||||
`树的深度`:一棵树中结点的最大深度就是树的深度,也称为`高度`
|
||||
|
||||
`结点的度`:有几个子结点(分支)
|
||||
`树的度`:各结点的度的最大值
|
||||
`叶子结点`:度为零的结点就是叶子结点
|
||||
|
||||
`森林`:m颗互不相交的树构成的集合就是森林
|
||||
|
||||
### 三、树的分类
|
||||
|
||||
有序树和无序树
|
||||
|
||||
`有序树`一一逻辑上看,树中结点的各子树从左至右是有次序的,不能互换
|
||||
|
||||

|
||||
|
||||
`无序树`一一逻辑上看,树中结点的各子树从左至右是无次序的,可以互换
|
||||
|
||||

|
||||
|
||||
### 四、树的性质
|
||||
|
||||
考点1:结点数=总度数+1
|
||||
|
||||
考点2:度为m的树和m叉树
|
||||
`度为m的树`:至少有一个结点度=m,一定是非空树
|
||||
`m叉树`:允许所有结点的度都≤m,可以是空树
|
||||
|
||||
考点3:度为m的树第i层至多有几个结点? $$m^{i-1}$$
|
||||
|
||||
考点4:高度为h的m叉树至多有几个结点? $$\frac{(m^{h}-1)}{(m-1)}$$
|
||||
|
||||
考点5:
|
||||
高度为h的m叉树至少有多少个结点? $$h$$
|
||||
|
||||
高度为h、度为m的树至少有多少个结点? $$h+m-1$$
|
||||
|
||||
考点6:具有n个结点的m叉树的最小高度为? $$log_m\lceil (n(m-1)+1) \rceil$$
|
||||
277
5.2二叉树.md
Normal file
277
5.2二叉树.md
Normal file
@@ -0,0 +1,277 @@
|
||||
## 二叉树 —— Binary Tree
|
||||
|
||||
### 一、二叉树的定义:
|
||||
|
||||
`二叉树`是n(n≥0)个结点的有限集合
|
||||
①或者为空二叉树,即n=0。
|
||||
②或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树
|
||||
特点:①每个结点至多只有两棵子树 ②左右子树不能颠倒(二叉树是有序树)
|
||||
|
||||

|
||||
|
||||
### 二、二叉树的五种状态
|
||||
|
||||

|
||||
|
||||
### 三、特殊的二叉树
|
||||
|
||||
`满二叉树`:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。
|
||||
|
||||
`特点`:
|
||||
①只有最后一层有叶子结点
|
||||
②不存在度为1的结点
|
||||
③按层序从1开始编号,结点i的左孩子为2i,右孩子为2+1;结点i的父节点为i/2向下取整(如果有的话)
|
||||
|
||||

|
||||
|
||||
`完全二叉树`:当且仅当每个结点都与相同高度的满二叉树的编号一一对应
|
||||
|
||||
特点:
|
||||
①只有最后两层可能有叶子结点
|
||||
②最多只有一个度为1的结点
|
||||
③按层序从1开始编号,结点i的左孩子为2i,右孩子为2+1;结点i的父节点为i/2向下取整(如果有的话)
|
||||
④i ≤ n/2向下取整为分支结点,i ≥ n/2向下取整为叶子结点
|
||||
|
||||

|
||||
|
||||
`二叉排序树`:一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:左子树上所有结点的关键字均小于根结点的关键字;右子树上的所有结点的关键字均大于根结点的关键字。
|
||||
|
||||

|
||||
|
||||
`平衡二叉树`:树上任一结点的左子树和右子树的深度之差不超过1。
|
||||
|
||||

|
||||
|
||||
### 四、二叉树的性质
|
||||
|
||||
几个重要常考的基本操作:
|
||||
|
||||
- i的左孩子——2i
|
||||
|
||||
- i的右孩子——2i+1
|
||||
- i的父节点——i/2向下取整
|
||||
- i所在的层次 ——log2(n+1)向上取整 或 log2n向下取整+1
|
||||
|
||||
若`完全二叉树`中共有n个结点(非完全二叉树不行),则
|
||||
|
||||
- 判断i是否有左孩子?——2i≤n则有
|
||||
- ·判断是否有右孩子?——2i+1<n则有
|
||||
- 判断i是否是叶子/分支结点?——i>n/2向下取整是叶子结点,i<n/2向下取整是分支结点
|
||||
|
||||
### 五、存储结构
|
||||
|
||||
`顺序存储`和`链式存储`,一般用链式存储
|
||||
|
||||
### 六、二叉树的顺序存储
|
||||
|
||||
只适合存完全二叉树,普通二叉树会浪费很多空间
|
||||
|
||||
#### 二叉树的类型描述
|
||||
|
||||
```c
|
||||
#define MaxSize 10
|
||||
struct TreeNode{
|
||||
Elemtype value; //结点中的数据元素
|
||||
bool isEmpty; //结点是否为空
|
||||
};
|
||||
|
||||
TreeNode t[MaxSize];
|
||||
```
|
||||
|
||||
#### 初始化
|
||||
|
||||
```c
|
||||
//初始化
|
||||
bool initBTree(TreeNode &t){
|
||||
for(int i=0; i<MaxSize; i++){
|
||||
t[i].isEmpty = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 七、二叉树的链式存储(二叉链表)
|
||||
|
||||
#### 二叉树的类型表述
|
||||
|
||||
```c
|
||||
typedef struct TreeNode{
|
||||
Elemtype data; //数据域
|
||||
struct BiTNode *lchide, *rchild; //左、右孩子指针
|
||||
}BiTNode, *BiTree;
|
||||
```
|
||||
|
||||
`注`:n个结点的二叉链表共有n+1个空链域
|
||||
|
||||
因为n个结点,有n-1个有指向;2n个指针,则指向NULL的个数=2n-(n-1)=n+1
|
||||
|
||||
#### 初始化(部分代码)
|
||||
|
||||
```c
|
||||
//数据域
|
||||
struct Elemtype{
|
||||
int value;
|
||||
};
|
||||
|
||||
//定义一颗空树
|
||||
BiTree root = NULL;
|
||||
|
||||
//插入根结点
|
||||
root = (BiTree)malloc(sizeof(BiTNode));
|
||||
root->data = {1};
|
||||
root->lchild = NULL;
|
||||
root->rchild = NULL;
|
||||
|
||||
//插入新结点
|
||||
BiTNode *p = (BiTNode *)malloc(sizeof(BiTNode));
|
||||
p->data = {2};
|
||||
p->lchild = NULL;
|
||||
p->rchild = NULL;
|
||||
root->lchild = p; //作为根结点的左孩子
|
||||
```
|
||||
|
||||
#### 找结点p的父节点很难,解决:
|
||||
|
||||
```c
|
||||
typedef struct TreeNode{
|
||||
Elemtype data; //数据域
|
||||
struct BiTNode *lchide, *rchild; //左、右孩子指针
|
||||
struct BiTNode *parent; //父节点指针
|
||||
}BiTNode, *BiTree;
|
||||
```
|
||||
|
||||
### 八、二叉树的遍历
|
||||
|
||||
先序遍历、中序遍历、后序遍历
|
||||
|
||||
`先序遍历`:根左右(NLR)
|
||||
|
||||
`中序遍历`:左根右(LNR)
|
||||
|
||||
`后序遍历`:左右根(LRN)
|
||||
|
||||
#### 代码:(用二叉树的链式存储)
|
||||
|
||||

|
||||
|
||||
先序遍历:每个结点都会被路过3次,第一次路过时访问结点
|
||||
|
||||
`空间复杂度`=O(h)
|
||||
|
||||
```c
|
||||
//先序遍历
|
||||
void PreOrder(BiTree T){
|
||||
if(T!=NULL){
|
||||
visit(T); //访问根结点
|
||||
PreOrder(T->lchild); //递归遍历左子树
|
||||
PreOrder(T->rchild); //递归遍历右子树
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
中序遍历:每个结点都会被路过3次,第二次路过时访问结点
|
||||
|
||||
`空间复杂度`=O(h)
|
||||
|
||||
```c
|
||||
//中序遍历
|
||||
void InOrder(BiTree T){
|
||||
if(T!=NULL){
|
||||
InOrder(T->lchild); //递归遍历左子树
|
||||
visit(T); //访问根结点
|
||||
InOrder(T->rchild); //递归遍历右子树
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
后序遍历:每个结点都会被路过3次,第三次路过时访问结点
|
||||
|
||||
`空间复杂度`=O(h)
|
||||
|
||||
```c
|
||||
//后序遍历
|
||||
void PostOrder(BiTree T){
|
||||
if(T!=NULL){
|
||||
PostOrder(T->lchild); //递归遍历左子树
|
||||
PostOrder(T->rchild); //递归遍历右子树
|
||||
visit(T); //访问根结点
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 应用:
|
||||
|
||||
1.算数表达式的`分析树`
|
||||
|
||||
`先序遍历`->前缀表达式
|
||||
|
||||
`中序遍历`->中缀表达式(需要加界限符)
|
||||
|
||||
`后序遍历`->后缀表达式
|
||||
|
||||
2.求树的深度
|
||||
|
||||
```c
|
||||
//求树的深度
|
||||
void treeDepth(BiTree T){
|
||||
if(T!=NULL){
|
||||
return 0;
|
||||
}else{
|
||||
int l = treeDepth(T->lchild); //左子树高度
|
||||
int r = treeDepth(T->rchild); //右子树高度
|
||||
//树的深度=Max(左子树深度,右子树深度)+1
|
||||
return l>r ? l+1 : r+1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 九、二叉树的层次遍历(层序遍历)
|
||||
|
||||

|
||||
|
||||
算法思想:
|
||||
①初始化一个辅助队列(链队列)
|
||||
②根结点入队
|
||||
③若队列非空,则队头结点出队,访问该结点并将其左、右孩子插入队尾(如果有的话)
|
||||
④重复③直至队列为空
|
||||
|
||||
#### 链队列
|
||||
|
||||
```C
|
||||
//类型描述
|
||||
typedef struct LNode{ //定义单链表结点类型
|
||||
ElemType data; //数据域,可以是别的各种数据类型,本文统一用int类型
|
||||
struct LNode *next; //指针域
|
||||
}LNode;
|
||||
typedef struct{
|
||||
LNode *front, *rear;
|
||||
}LinkQueue;
|
||||
```
|
||||
|
||||
#### 代码:(用二叉树的链式存储)
|
||||
|
||||
```c
|
||||
//层次遍历
|
||||
void LevelOrder(BiTree T){
|
||||
LinkQueue Q;
|
||||
InitQueue(Q);
|
||||
BiTree p; //T为根结点,p也是根结点,保证根出队后可以指向孩子,因为T出队后,T->lchild无效
|
||||
EnQueue(Q, T); //根结点入队
|
||||
while(!isEmpty(Q)){ //队列不空则循环
|
||||
DeQueue(Q, T); //根结点出队
|
||||
if(p->lchild!=NULL)
|
||||
EnQueue(Q, p->lchild); //左结点入队
|
||||
if(p->rchild!=NULL)
|
||||
EnQueue(Q, p->rchild); //右结点入队
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 十、由遍历序列构造二叉树
|
||||
|
||||
1.前序+中序遍历序列
|
||||
|
||||
2.后序+中序遍历序列
|
||||
|
||||
3.层序+中序遍历序列
|
||||
|
||||
关键是有非中序确定根结点是谁,再将根结点代入中序得左右子树,依次类推。
|
||||
312
5.3线索二叉树.md
Normal file
312
5.3线索二叉树.md
Normal file
@@ -0,0 +1,312 @@
|
||||
## 线索二叉树——Threaded Binary Tree
|
||||
|
||||
### 一、线索二叉树定义
|
||||
|
||||
背景:为解决遍历只能从根结点开始这个问题,因为普通二叉树找前驱和后继很麻烦
|
||||
|
||||
`线索二叉树`在二叉树的结点上加上`线索`的二叉树。
|
||||
|
||||
### 二、线索二叉树的存储结构
|
||||
|
||||
由二叉树的链式存储改进而来
|
||||
|
||||
#### 二叉树的类型表述
|
||||
|
||||
```c
|
||||
typedef struct TreeNode{
|
||||
Elemtype data; //数据域
|
||||
struct BiTNode *lchide, *rchild; //左、右孩子指针
|
||||
}BiTNode, *BiTree;
|
||||
```
|
||||
|
||||
#### 线索二叉树的类型表述
|
||||
|
||||
```c
|
||||
typedef struct TreeNode{
|
||||
Elemtype data; //数据域
|
||||
struct BiTNode *lchide, *rchild; //左、右孩子指针
|
||||
int ltag, rtag; //左、右线索标志
|
||||
}ThreadNode, *ThreadTree;
|
||||
```
|
||||
|
||||
`tag==0`:表示指针指向`孩子`
|
||||
`tag==1`:表示指针指向`线索`
|
||||
|
||||
### 三、线索二叉树的分类
|
||||
|
||||
`中序线索二叉树`、`先序线索二叉树`、`后续线索二叉树`
|
||||
|
||||
### 四、二叉树线索化
|
||||
|
||||
`对二叉树进行线索化`:对二叉树以某种遍历方式(如先序、中序、后序或层次等)进行遍历,使其变为线索二叉树的过程。
|
||||
|
||||
#### 中序线索化
|
||||
|
||||
```c
|
||||
//全局变量pre,指向当前访问结点的前驱
|
||||
ThreadNode *pre = NULL;
|
||||
|
||||
//中序线索化二叉树T(三种一样,只是调用线索化函数不同)
|
||||
void CreateThread(ThreadTree T){
|
||||
pre = NULL; //pre初始为NULL
|
||||
if(T != NULL){ //非空二叉树才能线索化
|
||||
InTread(T); //中序线索化二叉树
|
||||
if(pre->rchild == NULL){
|
||||
pre->rtag = 1; //处理遍历的最后一个结点
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//中序遍历二叉树,一边遍历,一边线索化
|
||||
void InOrder(ThreadTree T){
|
||||
if(T!=NULL){
|
||||
InOrder(T->lchild); //递归遍历左子树
|
||||
visit(T); //访问根结点
|
||||
InOrder(T->rchild); //递归遍历右子树
|
||||
}
|
||||
}
|
||||
|
||||
//访问结点,顺便线索化(三种一样)
|
||||
void visit(TheadNode *q){
|
||||
if(q->lchild == NULL){//左子树为空,建立前驱线索
|
||||
q->lchild = pre;
|
||||
q->ltag = 1;
|
||||
}
|
||||
if(pre!=NULL && pre->rchild==NULL){
|
||||
pre->rchild = q; //建立前驱结点的后继线索
|
||||
pre->rtage = 1;
|
||||
}
|
||||
pre = q;
|
||||
}
|
||||
```
|
||||
|
||||
#### 先序线索化
|
||||
|
||||
```c
|
||||
//全局变量pre,指向当前访问结点的前驱
|
||||
ThreadNode *pre = NULL;
|
||||
|
||||
//先序线索化二叉树T(三种一样,只是调用线索化函数不同)
|
||||
void CreateThread(ThreadTree T){
|
||||
pre = NULL; //pre初始为NULL
|
||||
if(T != NULL){ //非空二叉树才能线索化
|
||||
PreTread(T); //先序线索化二叉树
|
||||
if(pre->rchild == NULL){
|
||||
pre->rtag = 1; //处理遍历的最后一个结点
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//中序遍历二叉树,一边遍历,一边线索化
|
||||
void PreOrder(ThreadTree T){
|
||||
if(T!=NULL){
|
||||
visit(T); //访问根结点
|
||||
if(T->ltag == 0) //lchild不是前驱线索,是线索还遍历则无限循环
|
||||
InOrder(T->lchild); //递归遍历左子树
|
||||
InOrder(T->rchild); //递归遍历右子树
|
||||
}
|
||||
}
|
||||
|
||||
//访问结点,顺便线索化(三种一样)
|
||||
void visit(TheadNode *q){
|
||||
if(q->lchild == NULL){//左子树为空,建立前驱线索
|
||||
q->lchild = pre;
|
||||
q->ltag = 1;
|
||||
}
|
||||
if(pre!=NULL && pre->rchild==NULL){
|
||||
pre->rchild = q; //建立前驱结点的后继线索
|
||||
pre->rtage = 1;
|
||||
}
|
||||
pre = q;
|
||||
}
|
||||
```
|
||||
|
||||
#### 后续线索化
|
||||
|
||||
```c
|
||||
//全局变量pre,指向当前访问结点的前驱
|
||||
ThreadNode *pre = NULL;
|
||||
|
||||
//后序线索化二叉树T(三种一样,只是调用线索化函数不同)
|
||||
void CreateThread(ThreadTree T){
|
||||
pre = NULL; //pre初始为NULL
|
||||
if(T != NULL){ //非空二叉树才能线索化
|
||||
PostTread(T); //后序线索化二叉树
|
||||
if(pre->rchild == NULL){
|
||||
pre->rtag = 1; //处理遍历的最后一个结点
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//中序遍历二叉树,一边遍历,一边线索化
|
||||
void PostOrder(ThreadTree T){
|
||||
if(T!=NULL){
|
||||
InOrder(T->lchild); //递归遍历左子树
|
||||
InOrder(T->rchild); //递归遍历右子树
|
||||
visit(T); //访问根结点
|
||||
}
|
||||
}
|
||||
|
||||
//访问结点,顺便线索化(三种一样)
|
||||
void visit(TheadNode *q){
|
||||
if(q->lchild == NULL){//左子树为空,建立前驱线索
|
||||
q->lchild = pre;
|
||||
q->ltag = 1;
|
||||
}
|
||||
if(pre!=NULL && pre->rchild==NULL){
|
||||
pre->rchild = q; //建立前驱结点的后继线索
|
||||
pre->rtage = 1;
|
||||
}
|
||||
pre = q;
|
||||
}
|
||||
```
|
||||
|
||||
### 五、线索二叉树找前驱和后继
|
||||
|
||||
#### 1.1中序线索二叉树找中序后继
|
||||
|
||||
在`中序线索二叉树`中找指定结点*p的`中序后继next`
|
||||
|
||||
算法思想:
|
||||
|
||||
(1)若`p->rtag == 1`,则next为后继线索,即`next = p->rchild`。
|
||||
|
||||
(2)若`p->rtag == 0`,则p必有右孩子,`next为p的右子树中最左下结点`。
|
||||
|
||||
```c
|
||||
//找到以P为根的子树中,第一个被中序遍历的结点
|
||||
ThreadNode *Firstnode(ThreadNode *p){
|
||||
//循环找到最左下结点(不一定是叶子结点)
|
||||
while(p->ltag == 0) p = p->lchild;
|
||||
return p;
|
||||
}
|
||||
|
||||
//在中序线索二叉树中找到结点p的后继结点
|
||||
TheadNode *Nextnode(ThreadNode *p){
|
||||
//右子树中最左下结点
|
||||
if(p->rtag == 0) return Fistnode(p->rchild);
|
||||
else return p->rchild; //rtag=1直接返回后继线索
|
||||
}
|
||||
```
|
||||
|
||||
##### 应用:中序线索二叉树的中序遍历
|
||||
|
||||
`空间复杂度`=O(1)
|
||||
|
||||
```c
|
||||
//对中序线索二又树进行中序遍历(利用线索实现的非递算法)
|
||||
void Inorder(ThreadNode *T){
|
||||
for (ThreadNode *p=Firstnode(T); p!=NULL; p=Nextnode(p))
|
||||
visit (p);
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2中序线索二叉树找中序前驱
|
||||
|
||||
在`中序线索二叉树`中找指定结点*p的`中序前驱pre`
|
||||
|
||||
算法思想:
|
||||
|
||||
(1)若`p->rtag == 1`,则pre为前驱线索,即`pre = p->lchild`。
|
||||
|
||||
(2)若`p->rtag == 0`,则p必有左孩子,`pre为p的左子树中最右下结点`。
|
||||
|
||||
```c
|
||||
//找到以P为根的子树中,最后一个被中序遍历的结点
|
||||
ThreadNode *Lastnode(ThreadNode *p){
|
||||
//循环找到最右下结点(不一定是叶子结点)
|
||||
while(p->rtag == 0) p = p->rchild;
|
||||
return p;
|
||||
}
|
||||
|
||||
//在中序线索二叉树中找到结点p的前驱结点
|
||||
TheadNode *Prenode(ThreadNode *p){
|
||||
//右子树中最左下结点
|
||||
if(p->rtag == 0) return Lastnode(p->lchild);
|
||||
else return p->lchild; //rtag=1直接返回前驱线索
|
||||
}
|
||||
```
|
||||
|
||||
##### 应用:中序线索二叉树的逆向中序遍历
|
||||
|
||||
`空间复杂度`=O(1)
|
||||
|
||||
```c
|
||||
//对中序线索二又树进行逆向中序遍历(利用线索实现的非递算法)
|
||||
void Inorder(ThreadNode *T){
|
||||
for (ThreadNode *p=Lastnode(T); p!=NULL; p=Prenode(p))
|
||||
visit (p);
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.1先序线索二叉树找先序后继
|
||||
|
||||
在`先序线索二叉树`中找指定结点*p的`先序后继next`
|
||||
|
||||
先序遍历:根左右。
|
||||
|
||||
算法思想:
|
||||
|
||||
(1)若`p->rtag == 1`,则pre为前驱线索,即`next = p->rchild`。
|
||||
|
||||
(2)若`p->rtag == 0`,则
|
||||
|
||||
①若`p有左孩子`,则`next为左孩子`
|
||||
|
||||
②若`p没有左孩子`,则`next为右孩子`
|
||||
|
||||
#### 2.2先序线索二叉树找先序前驱
|
||||
|
||||
在`先序线索二叉树`中找指定结点*p的`先序前驱pre`
|
||||
|
||||
由于先序遍历:根左右。p结点的左右子树中的结点只能是根的后继,不可能是前驱,因此`无法找前驱`
|
||||
|
||||
`解决`:改用三叉链表,三个指针:指向父结点、左孩子结点和右孩子结点
|
||||
|
||||
算法思想:
|
||||
|
||||
(1)若`p->rtag == 1`,则pre为前驱线索,即`pre = p->lchild`。
|
||||
|
||||
(2)若`p->rtag == 0`,则
|
||||
|
||||
①若`能找到p的父结点`且`p是左孩子`,则`pre为父结点`
|
||||
|
||||
②若`能找到p的父结点`且`p是右孩子`,其`左兄弟为空`,则`pre为父结点`
|
||||
|
||||
②若`能找到p的父结点`且`p是右孩子`,其`左兄弟为非空`,则`pre为左兄弟子树中最后一个被先序遍历的结点`。
|
||||
|
||||
#### 3.1后序线索二叉树找后序后继
|
||||
|
||||
在`后序线索二叉树`中找指定结点*p的`后序后继next`
|
||||
|
||||
由于后序遍历:左右根。p结点的左右子树中的结点只能是根的后前驱,不可能是后继,因此`无法找前后继
|
||||
|
||||
`解决`:改用三叉链表,三个指针:指向父结点、左孩子结点和右孩子结点
|
||||
|
||||
算法思想:
|
||||
|
||||
(1)若`p->rtag == 1`,则next为后继线索,即`next = p->rchild`。
|
||||
|
||||
(2)若`p->rtag == 0`,则
|
||||
|
||||
①若`能找到p的父结点`且`p是右孩子`,则`next为父结点`
|
||||
|
||||
②若`能找到p的父结点`且`p是左孩子`,其`右兄弟为空`,则`next为父结点`
|
||||
|
||||
②若`能找到p的父结点`且`p是左孩子`,其`右兄弟为非空`,则`next为左兄弟子树中第一个被后续序遍历的结点`。
|
||||
|
||||
#### 3.2后序线索二叉树找后序前驱
|
||||
|
||||
在`后序线索二叉树`中找指定结点*p的`后序前驱pre`
|
||||
|
||||
先序遍历:左右根。
|
||||
|
||||
算法思想:
|
||||
|
||||
(1)若`p->rtag == 1`,则pre为前驱线索,即`pre = p->lchild`。
|
||||
|
||||
(2)若`p->rtag == 0`,则
|
||||
|
||||
①若`p有右孩子`,则`pre为右孩子`
|
||||
|
||||
②若`p没有右孩子`,则`pre为左孩子`
|
||||
136
5.4树的存储结构.md
Normal file
136
5.4树的存储结构.md
Normal file
@@ -0,0 +1,136 @@
|
||||
## 树的存储结构
|
||||
|
||||
### 一、存储结构
|
||||
|
||||
`顺序存储`和`链式存储`
|
||||
|
||||
方法:
|
||||
`双亲表示法`(顺序存储)
|
||||
`孩子表示法`(顺序+链式存储)
|
||||
`孩子兄弟表示法`(链式存储)
|
||||
|
||||
### 二、双亲表示法(顺序存储)
|
||||
|
||||

|
||||
|
||||
双亲表示法:`顺序存储`结点数据,结点中保存父结点在数组中的下标
|
||||
|
||||
`优点`:找父节点方便。
|
||||
`缺点`:找孩子不方便。
|
||||
|
||||
`注`:双亲表示法与二叉树的顺序存储不一样,双亲表示法也可表示二叉树
|
||||
|
||||
#### 类型描述
|
||||
|
||||
`结点`包括`数据`和`父亲下标`,
|
||||
`树`包括`结点数组`和`结点个数`
|
||||
|
||||
```c
|
||||
#define MAX_TREE_SIZE 100 //树中最多结点数
|
||||
typedef struct{ //树的结点定义
|
||||
ElemType data; //数据元素
|
||||
int parent; //双亲位置域
|
||||
}PTNode;
|
||||
typedef struct{ //树的类型定义
|
||||
PTNode nodes[MAX_TREE_SIZE]; //双亲表示
|
||||
int n; //结点数
|
||||
}PTree;
|
||||
```
|
||||
|
||||
#### 增加一个结点
|
||||
|
||||
新增元素,无需按逻辑次序存储,可以放到删除结点留下的存储空间里
|
||||
|
||||
#### 删除一个结点
|
||||
|
||||
方案一:数据取出,双亲指针改为-1
|
||||
|
||||
方案二:用存储空间中最后一个存的结点把要删的结点覆盖
|
||||
|
||||
#### 查找一个结点
|
||||
|
||||
找父结点方便、找孩子不方便。
|
||||
|
||||
空数据导致遍历慢。
|
||||
|
||||
### 二、孩子表示法(顺序+链式存储)
|
||||
|
||||

|
||||
|
||||
孩子表示法:`顺序存储`结点数据,结点中保存孩子`链表`头指针(`链式存储`)
|
||||
|
||||
`优点`:找孩子方便。
|
||||
`缺点`:找父节点不方便。
|
||||
|
||||
#### 类型描述
|
||||
|
||||
`孩子结点`包括`孩子下标`和`下一个孩子指针`,
|
||||
`数组`包括`数据`和`孩子结点`,
|
||||
`树`包括`数组`和`数组元素(结点)个数`及`根的下标`
|
||||
|
||||
```c
|
||||
#define MAX_TREE_SIZE 100 //树中最多结点数
|
||||
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;
|
||||
```
|
||||
|
||||
#### 增加一个结点
|
||||
|
||||
新增元素,父结点后新增一个孩子结点,数组中加一个数组元素
|
||||
|
||||
#### 删除一个结点
|
||||
|
||||
父结点后的链表中将此结点删除
|
||||
|
||||
数组中:
|
||||
①若此结点后无链表,则直接删除
|
||||
②若此结点后有链表,再处理子树
|
||||
|
||||
#### 查找一个结点
|
||||
|
||||
按图一行一行遍历
|
||||
|
||||
找孩子结点方便,找父结点不方便
|
||||
|
||||
### 三、孩子兄弟表示法(顺序+链式存储)
|
||||
|
||||

|
||||
|
||||
孩子兄弟表示法:用`二叉链表`存储`树`——`两个指针`:`第一个孩子`和`右兄弟`
|
||||
|
||||
用此方法存储的树,形态上和`二叉树`类似
|
||||
|
||||
#### 类型描述
|
||||
|
||||
由二叉树的链式存储(二叉链表)改变而来
|
||||
|
||||
```c
|
||||
typedef struct CSNode{
|
||||
Elemtype data; //数据域
|
||||
struct CSTNode *firstchild, *nextsibling; //第一个孩子和右兄弟指针
|
||||
}CSTNode, *CSTree;
|
||||
```
|
||||
|
||||
#### 应用:树和二叉树的转换
|
||||
|
||||

|
||||
|
||||
### 四、森林和二叉树的转换
|
||||
|
||||
本质:用`二叉链表`存储`森林`
|
||||
|
||||
将森林的根结点连起来,视为兄弟关系
|
||||
|
||||

|
||||
|
||||

|
||||
200
5.5二叉排序树.md
Normal file
200
5.5二叉排序树.md
Normal file
@@ -0,0 +1,200 @@
|
||||
## 二叉排序树——Binary Search Tree
|
||||
|
||||
### 一、二叉排序树的定义
|
||||
|
||||
`二叉排序树`,又称`二叉查找树`(`BST`, `Binary Search Tree`)
|
||||
|
||||
定义:一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:
|
||||
`左子树`上所有结点的关键字均`小于` `根结点`的关键字;
|
||||
`右子树`上所有结点的关键字均`大于` `根结点`的关键字。
|
||||
左子树和右子树又各是一棵二叉排序树。
|
||||
|
||||
`左子树结点值<根结点值<右子树结点值`
|
||||
进行`中序遍历`,可以得到一个`递增的有序序列`
|
||||
|
||||
`作用`:元素的有序组织、`搜索`。
|
||||
|
||||

|
||||
|
||||
### 二、二叉排序树的存储结构(用链式存储)
|
||||
|
||||
二叉排序树的类型表述(与二叉树一样)
|
||||
|
||||
```c
|
||||
typedef struct TreeNode{
|
||||
int data; //数据域
|
||||
struct BiTNode *lchide, *rchild; //左、右孩子指针
|
||||
}BSTNode, *BSTree;
|
||||
```
|
||||
|
||||
### 三、二叉排序树的查找
|
||||
|
||||
非递归好于递归
|
||||
|
||||
算法思想:
|
||||
若树非空,目标值与根结点的值比较:
|
||||
若相等,则查找成功。
|
||||
若小于根结点,则在左子树上查找,否则在右子树上查找。
|
||||
查找成功,返回结点指针;查找失败返回NULL。
|
||||
|
||||
##### 递归查找
|
||||
|
||||
`最坏空间复杂度`=O(h)
|
||||
|
||||
```c
|
||||
//在二叉排序树中查找值为key的结点(递归实现)
|
||||
BSTNode *BST_Search(BSTree T,int key){
|
||||
if(T == NULL) return NULL;
|
||||
if(key == T->data) return T;
|
||||
else if(key < T->data)
|
||||
return BST_Search( T->lchild, key);
|
||||
else
|
||||
return BST_Search( T->rchild, key);
|
||||
}
|
||||
```
|
||||
|
||||
##### 非递归查找
|
||||
|
||||
`最坏空间复杂度`=O(1)
|
||||
|
||||
```c
|
||||
//在二叉排序树中查找值为key的结点
|
||||
BSTNode *NoRBST_Search(BSTree T,int key){
|
||||
while(T!=NULL && key!=T->data){
|
||||
if(key < T->data) T = T->lchild;
|
||||
else T = T->rchild;
|
||||
}
|
||||
return T;
|
||||
}
|
||||
```
|
||||
|
||||
### 四、二叉排序树的插入
|
||||
|
||||
非递归好于递归
|
||||
|
||||
算法思想:
|
||||
若原二叉排序树为空,则直接插入结点;
|
||||
否则,若关键字k小于根结点值,则插入到左子树,若关键字k大于根结点值,则插入到右子树
|
||||
|
||||
#### 递归插入
|
||||
|
||||
`最坏空间复杂度`=O(h)
|
||||
|
||||
```c
|
||||
//在二叉排序树插入关键字为k的新结点(递归实现)
|
||||
int BST_Insert(BSTree &T,int key){
|
||||
if(T == NULL){ //树为空,则插入根结点
|
||||
T = (BSTree)malloc(sizeof(BSTNode));
|
||||
T->data = k;
|
||||
T->lchild = T->rchild = NULL;
|
||||
return 1;
|
||||
}
|
||||
if(key == T->data){ //树中存在相同关键字的结点,插入失败
|
||||
return 0;
|
||||
}else if(key < T->data){
|
||||
return BST_Insert( T->lchild, key); //插入到T的左子树
|
||||
}else{
|
||||
return BST_Insert( T->rchild, key); //插入到T的右子树
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 非递归插入
|
||||
|
||||
`最坏空间复杂度`=O(1)
|
||||
|
||||
```c
|
||||
//二叉排序树非递归插入,最坏空间复杂度Sn=O(1)
|
||||
int NoRBST_Insert(BSTree &T, int key) {
|
||||
BSTNode *pre = NULL;//二叉树里最后一个结点
|
||||
if (T == NULL) { //原来树为空,申请结点将其插入进去
|
||||
T = (BSTree)malloc(sizeof(BSTNode));
|
||||
T->data = key;
|
||||
T->lchild = T->rchild = NULL;
|
||||
return 1; //成功插入
|
||||
}
|
||||
|
||||
//获取到最后一个结点
|
||||
while(T != NULL) {
|
||||
if(key == T->data) {
|
||||
return 0; //二叉排序树里不可能存在相同的结点,插入失败
|
||||
} else if(key < T->data) {
|
||||
pre = T;
|
||||
T = T->lchild;//遍历左孩子
|
||||
} else {
|
||||
pre = T;
|
||||
T = T->rchild;//遍历右孩子
|
||||
}
|
||||
}
|
||||
|
||||
//插入到左子树
|
||||
if (key < pre->data) {
|
||||
T = (BSTree)malloc(sizeof(BSTNode));
|
||||
T->data = key;
|
||||
pre->lchild = T;
|
||||
} else { //到了此步已经不存在等于的情况
|
||||
//插入到右子树
|
||||
T = (BSTree)malloc(sizeof(BSTNode));
|
||||
T->data = key;
|
||||
pre->rchild = T;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 五、二叉排序树的构造
|
||||
|
||||
```c
|
||||
//按照str[]中的关键字序建立二叉排序树
|
||||
void Creat_BST(BSTree &T, int str[],int n){
|
||||
T=NULL;
|
||||
int i=0;
|
||||
while(i<n){
|
||||
BST_Insert(T,str[i]);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 六、二叉排序树的删除
|
||||
|
||||
先搜索找到目标结点z:
|
||||
|
||||
①若被删除结点z是`叶子结点`,则`直接删除`,不会破坏二叉排序树的性质。
|
||||
|
||||

|
||||
|
||||
②若结点z只有一颗左子树或右子树,则让z的子树分为z父结点的子树,代替z的位置
|
||||
|
||||

|
||||
|
||||
③若结点z有左、右两棵子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况。
|
||||
|
||||
直接后继代替
|
||||
|
||||

|
||||
|
||||
直接前驱代替
|
||||
|
||||

|
||||
|
||||
### 七、查找效率的分析
|
||||
|
||||
查找长度:对比关键字的次数,反映了查找操作的时间复杂度。
|
||||
|
||||
与高度h有关。高度越小,查找效率越高
|
||||
|
||||
平衡二叉树的查找效率最高,也是最好情况
|
||||
|
||||
最好情况,平均查找长度=O(log~2~n)
|
||||
|
||||
最坏情况,平均查找长度=O(n)
|
||||
|
||||
#### 平均查找长度计算
|
||||
|
||||
查找`成功`的平均查找长度:
|
||||
|
||||

|
||||
|
||||
查找`失败`的平均查找长度:
|
||||
|
||||

|
||||
46
5.6平衡二叉树.md
Normal file
46
5.6平衡二叉树.md
Normal file
@@ -0,0 +1,46 @@
|
||||
## 平衡二叉树——Balanced Binary Tree
|
||||
|
||||
### 一、平衡二叉树的定义
|
||||
|
||||
`平衡二叉树`,又被称为`AVL树`(有别于AVL算法),且具有以下性质:
|
||||
|
||||
它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
|
||||
|
||||
AVL是两个人的名字,ASL是平均查找长度,不一样
|
||||
|
||||
`结点的平衡因子=左子树高-右子树高`
|
||||
|
||||
平衡二叉树结点的平衡因子的值只可能是-1、0、1。
|
||||
|
||||

|
||||
|
||||
### 二、平衡二叉树的存储结构(用链式存储)
|
||||
|
||||
平衡二叉树的类型表述(与二叉树不一样)
|
||||
|
||||
```c
|
||||
typedef struct AVLNode{
|
||||
int key; //数据域
|
||||
int balance; //平衡因子
|
||||
struct AVLNode *lchide, *rchild; //左、右孩子指针
|
||||
}AVLNode, *AVLTree;
|
||||
```
|
||||
|
||||
### 三、平衡二叉树的插入
|
||||
|
||||
在平衡的二叉排序树中插入一个结点导致不平衡,如何调整平衡?
|
||||
解决方法:调整`“最小不平衡树”`
|
||||
|
||||
四种调整方法:
|
||||
①LL:在A的`左孩子的左子树`中插入导致A的不平衡,将A的`左孩子右上旋`。
|
||||
②RR:在A的`右孩子的右子树`中插入导致A的不平衡,将A的`右孩子左上旋`。
|
||||
③LR:在A的`左孩子的右子树`中插入导致A的不平衡,将A的`左孩子的右孩子,先左上旋再右上旋`。
|
||||
④RL:在A的`右孩子的左子树`中插入导致A的不平衡,将A的`右孩子的左孩子,先右上旋再左上旋`。
|
||||
|
||||
### 四、查找效率分析
|
||||
|
||||
`平均查找的时间复杂度`=O(log~2~h)
|
||||
|
||||
### 五、高度为h的平衡二叉树的最少结点数
|
||||
|
||||
`递推公式`:$$n_h=n_{h-1}+n_{h-2}+1$$
|
||||
29
5.7哈夫曼树.md
Normal file
29
5.7哈夫曼树.md
Normal file
@@ -0,0 +1,29 @@
|
||||
## 哈夫曼树——Huffman Tree
|
||||
|
||||
### 一、带权路径长度
|
||||
|
||||
结点的`权`:有某种现实含义的数值(如:结点的重要性等)
|
||||
|
||||
`结点的带权路径长度=该结点的路径长度×该结点的权值`
|
||||
|
||||
`树的带权路径长度`=所有`叶结点`的`带权路径长度之和`
|
||||
|
||||
### 二、哈夫曼树的定义
|
||||
|
||||
给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的`带权路径长度达到最小`,称这样的二叉树为`最优二叉树`,也称为`哈夫曼树`(Huffman Tree)。
|
||||
|
||||
哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
|
||||
|
||||

|
||||
|
||||
### 三、哈夫曼树的构造
|
||||
|
||||
哈夫曼树构造的树可以不同,但带权路径长度相同
|
||||
|
||||
### 四、哈夫曼编码
|
||||
|
||||
①固定长度编码:平衡二叉树
|
||||
|
||||
②可变长度编码:哈夫曼树(最优二叉树)
|
||||
|
||||

|
||||
69
6.1图.md
Normal file
69
6.1图.md
Normal file
@@ -0,0 +1,69 @@
|
||||
## 图——Graph
|
||||
|
||||
### 一、图的定义
|
||||
|
||||
`图G`由`顶点集V`和`边集E`组成,记$$G=(V,E)$$。
|
||||
|
||||
$$|V|$$表示图G中顶点的个数,也称图G的阶。
|
||||
|
||||
$$|E|$$表示图G中边的条数。
|
||||
|
||||
注:线性表可以是空表,树可以是空树,但`图不可以为空`,即V一定是非空集
|
||||
|
||||
### 二、图的分类
|
||||
|
||||
①有向图和无向图。
|
||||
|
||||

|
||||
|
||||
②简单图和多重图
|
||||
|
||||

|
||||
|
||||
### 三、顶点的度,入度、出度
|
||||
|
||||
无向图:
|
||||
顶点的度=连该顶点的边的条数
|
||||
无入度出度概念
|
||||
|
||||
有向图:
|
||||
入度=指向该点的边的条数
|
||||
出度=从该点指向其它点的边的条数
|
||||
顶点的度=连该顶点的边的条数=入度出度之和
|
||||
|
||||
### 四、顶点与顶点之间的关系
|
||||
|
||||

|
||||
|
||||
### 五、连通图、强连通图
|
||||
|
||||

|
||||
|
||||
### 六、子图、生成子图
|
||||
|
||||
子图:点集是子集,边集是子集
|
||||
生成子图:点集不变,边集是子集。
|
||||
|
||||
### 七、连通分量、强连通分量
|
||||
|
||||
连通分量:无向图的极大连通子图
|
||||
强连通分量:有向图的极大连通子图
|
||||
|
||||
### 八、生成树、生成森林
|
||||
|
||||
连通图可以生成树
|
||||
非连通图可以生成森林
|
||||
|
||||
### 九、特殊的图
|
||||
|
||||
①无向完全图、有向完全图
|
||||
无向完全图:无向图中任意两个顶点之间都存在边
|
||||
有向完全图:有向图中任意两个顶点之间都存在相反的两条弧
|
||||
|
||||
②稀疏图、稠密图
|
||||
稀疏图:边很少的图
|
||||
稠密图:边很多的图
|
||||
|
||||
③树、有向树
|
||||
树:不存在回路、且连通的无向图
|
||||
有向树:一个顶点的入度为0,其余顶点的入度为1 的有向图
|
||||
120
6.2图的存储结构.md
Normal file
120
6.2图的存储结构.md
Normal file
@@ -0,0 +1,120 @@
|
||||
## 图的存储结构
|
||||
|
||||
### 一、图的存储结构
|
||||
|
||||
①领接矩阵:顺序存储(一维数组存点的数据,二维数组存边的连接情况)(存储无向图、有向图)
|
||||
②邻接表:顺序+链式存储(顺序存点的数据,链存连接该点的边)(存储有向图、无向图)
|
||||
③十字链表:链式存储(存储有向图)
|
||||
④邻接多重表:链式存储(存储无向图)
|
||||
|
||||

|
||||
|
||||
### 二、邻接矩阵法
|
||||
|
||||
`空间复杂度`=$$O(|V|^2)$$,`适合存稠密图`
|
||||
|
||||
`无向图的邻接矩阵`是`对称矩阵`,可以`压缩存储`,见3.6
|
||||
|
||||
`性质`:$$A$$为图$$G$$的邻接矩阵,则$$A^n$$的元素$$A^n[i][j]$$=`顶点i到顶点j的长度为n的路径的数目`
|
||||
|
||||
`计算度、入度、出度`:必须遍历对应的行或列。
|
||||
`找相邻的边`:必须遍历对应的行或列。
|
||||
|
||||
#### 1.1普通图的领接矩阵法:
|
||||
|
||||

|
||||
|
||||
```c
|
||||
#define MaxVertexNum 100 //顶点数目最大值
|
||||
typedef struct{
|
||||
char Vex[MaxVertexNum]; //顶点表:存每个点的数据
|
||||
int Edge[MaxVertexNum][MaxVertexNum];//邻接矩阵,边表:存边的连接情况
|
||||
int vexnum, arcnum; //图当前的顶点数和边数(弧数)
|
||||
}MGraph;
|
||||
```
|
||||
|
||||
边可以是int,bool或枚举型变量。
|
||||
|
||||
#### 1.2普通图的度、入度、出度
|
||||
|
||||
某点的度:某点的边数
|
||||
|
||||
无向图:
|
||||
`第i个结点的度`=`第i行(第i列)`的`非零元素`个数。
|
||||
|
||||
有向图:
|
||||
`第i个结点的出度`=`第i行`的`非零元素`个数。
|
||||
`第i个结点的入度`=`第i列`的`非零元素`个数。
|
||||
`第i个结点的度`=`第i行、第i列`的`非零元素`个数`之和`。
|
||||
|
||||
求顶点的度、入度、出度的`时间复杂度`=O($$|V|$$)
|
||||
|
||||
#### 2.带权图的领接矩阵法
|
||||
|
||||

|
||||
|
||||
```c
|
||||
#define MaxVertexNum 100 //顶点数目最大值
|
||||
#define INFINITY 4294967295 //宏定义常量“无穷”,4294967295为最大的int值
|
||||
typedef char VertexType; //顶点的数据类型
|
||||
typedef int EdgeType; //边的数据类型
|
||||
typedef struct{
|
||||
VertexType Vex[MaxVertexNum]; //顶点表:存每个点的数据
|
||||
EdgeType Edge[MaxVertexNum][MaxVertexNum];//邻接矩阵,边表:存边的连接情况
|
||||
int vexnum, arcnum; //图当前的顶点数和边数(弧数)
|
||||
}MGraph;
|
||||
```
|
||||
|
||||
### 三、邻接表法
|
||||
|
||||
`空间复杂度`:无向图为$$O(|V|+2|E|)$$,有向图为$$O(|V|+|E|)$$,`适合存稀疏图`
|
||||
`表示方式不唯一`
|
||||
|
||||
邻接矩阵降低了空间复杂度,但使操作不方便了:
|
||||
`计算度、入度、出度`:计算有向图的度、入度不方便,其余很方便。
|
||||
`找相邻的边`:找有向图的入边不方便。
|
||||
|
||||
#### 图的类型描述:
|
||||
|
||||
与树的孩子表示法很像
|
||||
|
||||

|
||||
|
||||
```c
|
||||
#define MaxVertexNum 100 //顶点数目最大值
|
||||
//"边(弧)"
|
||||
typedef struct ArcNode{
|
||||
int adjvex; //边(弧)指向那个结点
|
||||
struct ArcNode *next; //指向下一条弧的指针
|
||||
//InfoType info; //边权值
|
||||
}ArcNode;
|
||||
//"顶点"
|
||||
typedef struct VNode{
|
||||
VertexType data; //顶点数据
|
||||
ArcNode *first; //顶点指向的第一条边
|
||||
}VNode, AdjList[MaxVertexNum];
|
||||
//用领接表存储图
|
||||
typedef struct{
|
||||
AdjList vertices; //顶点数组
|
||||
int vernum, arcnum; ////图当前的顶点数和边数(弧数)
|
||||
}ALGraph;
|
||||
```
|
||||
|
||||
### 四、十字链表法
|
||||
|
||||
`空间复杂度`:$$O(|V|+|E|)$$,与邻接表法一样
|
||||
|
||||
解决了邻接表法的找入边难的问题。
|
||||
|
||||

|
||||
|
||||
### 五、邻接多重表
|
||||
|
||||
`空间复杂度`:$$O(|V|+|E|)$$,比邻接表法的$$O(|V|+2|E|)$$好
|
||||
|
||||
解决了邻接表法存两遍边的空间浪费。
|
||||
|
||||
删除边、删除结点等操作很方便。
|
||||
|
||||

|
||||
|
||||
17
6.3图的基本操作.md
Normal file
17
6.3图的基本操作.md
Normal file
@@ -0,0 +1,17 @@
|
||||
## 图的基本操作
|
||||
|
||||
```c
|
||||
//图的基本操作
|
||||
Adjacent(G,x,y); //判断图G是否存在边<x,y>或(x,y)。
|
||||
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>存在,则从图G中删除该边。
|
||||
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。
|
||||
```
|
||||
|
||||
图的遍历中直接调用FirstNeighbor(G,x);和NextNeighbor(G,x,y);
|
||||
176
6.4图的遍历(BFS、DFS).md
Normal file
176
6.4图的遍历(BFS、DFS).md
Normal file
@@ -0,0 +1,176 @@
|
||||
## 图的遍历
|
||||
|
||||
### 一、图的遍历
|
||||
|
||||
广度优先遍历、深度优先遍历
|
||||
|
||||
### 二、广度优先遍历(BFS)
|
||||
|
||||
与`树`的广度优先搜索(`层次遍历`)很像。
|
||||
|
||||
需要一个`辅助链队列`。
|
||||
|
||||
树的层次遍历的算法思想:
|
||||
①初始化一个辅助队列(链队列)
|
||||
②根结点入队
|
||||
③若队列非空,则队头结点出队,访问该结点并将其`左、右孩子`插入队尾(如果有的话)
|
||||
④重复③直至队列为空
|
||||
|
||||
图的广度优先搜索的算法思想:
|
||||
①初始化一个辅助队列(链队列)
|
||||
②结点入队
|
||||
③若队列非空,则队头结点出队,访问该结点并将其`相邻顶点`插入队尾(如果有的话)
|
||||
④重复③直至队列为空
|
||||
|
||||
树没有回路,不可能搜到已访问结点
|
||||
图有可能搜索到已访问的结点
|
||||
解决方法:`用一个数组标记顶点的访问`
|
||||
|
||||
#### 链队列
|
||||
|
||||
```C
|
||||
//类型描述
|
||||
typedef struct LNode{ //定义单链表结点类型
|
||||
ElemType data; //数据域,可以是别的各种数据类型,本文统一用int类型
|
||||
struct LNode *next; //指针域
|
||||
}LNode;
|
||||
typedef struct{
|
||||
LNode *front, *rear;
|
||||
}LinkQueue;
|
||||
```
|
||||
|
||||
#### 代码
|
||||
|
||||
```c
|
||||
//树的层次遍历(广度优先搜索)(用二叉树的链式存储)
|
||||
void LevelOrder(BiTree T){
|
||||
LinkQueue Q;
|
||||
InitQueue(Q);
|
||||
BiTree p; //T为根结点,p也是根结点,保证根出队后可以指向孩子,因为T出队后,T->lchild无效
|
||||
EnQueue(Q, T); //根结点入队
|
||||
while(!isEmpty(Q)){ //队列不空则循环
|
||||
DeQueue(Q, T); //根结点出队
|
||||
if(p->lchild!=NULL)
|
||||
EnQueue(Q, p->lchild); //左结点入队
|
||||
if(p->rchild!=NULL)
|
||||
EnQueue(Q, p->rchild); //右结点入队
|
||||
}
|
||||
}
|
||||
|
||||
//图的广度优先搜索(用图的邻接矩阵、领接表都可以,只是FirstNeighbor和NextNeighbor函数实现不一样)
|
||||
bool visited[MAX_VERTEX_NUM]; //访问标记数组
|
||||
void BFSTraverse(Graph G){ //对图G进行广度优先搜索
|
||||
for(v=0; v<G.vexnum; ++v){
|
||||
visited[v] = false; //初始化访问标记数组
|
||||
}
|
||||
InitQueue(Q); //初始化辅助队列Q
|
||||
for(v=0; v<G.vexnum; ++v){
|
||||
if(!visited[v]){ //对每个连通分量调用一次BFS
|
||||
BFS(G,v); //vi没访问过,从vi开始BFS
|
||||
}
|
||||
}
|
||||
}
|
||||
void BFS(Graph G, int v){
|
||||
visit(v); //访问初始顶点v
|
||||
visited[v] = true; //对v做已访问标记
|
||||
EnQueue(Q, v); //顶点v入队
|
||||
while(!isEmpty(Q)){ //队列不空则循环
|
||||
DeQueue(Q, v); //顶点v出队
|
||||
for(w=FirstNeighbor(G,v); w>=0; w=NextNeighbor(G,v,w)){//检测v所有的邻接顶点
|
||||
if(!visited[w]){ //w为v尚未访问的邻接顶点
|
||||
visit(w); //访问顶点w
|
||||
visited[w] = true; //对w做已访问标记
|
||||
EnQueue(Q, w); //顶点w入队
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 复杂度分析
|
||||
|
||||
`空间复杂度`=$$O(|V|)$$
|
||||
|
||||
`时间复杂度`:
|
||||
`邻接矩阵`=$$O(|V|^2)$$,`邻接表`=$$O(|V|+|E|)$$
|
||||
|
||||
原理:
|
||||
邻接矩阵:访问点=$$O(|V|)$$,访问边=$$O(|V|^2)$$,时间复杂度=$$O(|V|)+O(|V|^2)=O(|V|^2)$$
|
||||
邻接表:访问点=$$O(|V|)$$,访问无向边=$$O(2|E|)$$,访问无向边=$$O(|E|)$$,
|
||||
`无向图`时间复杂度=$$O(|V|)+O(2|E|)=O(|V|+|E|)$$
|
||||
`有向图`时间复杂度=$$O(|V|)+O(|E|)=O(|V|+|E|)$$
|
||||
|
||||
#### 广度优先生成树、森林
|
||||
|
||||
连通图生成树,非连通图生森林
|
||||
|
||||
由于领接矩阵表示法唯一,领接表法表示不唯一
|
||||
导致邻接矩阵生成树唯一,领接表生成树不唯一
|
||||
|
||||
### 三、深度优先遍历(DFS)
|
||||
|
||||
#### 代码
|
||||
|
||||
与`树`的`先序遍历`很像。
|
||||
|
||||
```c
|
||||
//树的先序遍历(深度优先遍历)(用二叉树的链式存储)
|
||||
void PreOrder(BiTree T){
|
||||
if(T!=NULL){
|
||||
visit(T); //访问根结点
|
||||
PreOrder(T->lchild); //递归遍历左子树
|
||||
PreOrder(T->rchild); //递归遍历右子树
|
||||
}
|
||||
}
|
||||
|
||||
//图的深度优先搜索(用图的邻接矩阵、领接表都可以,只是FirstNeighbor和NextNeighbor函数实现不一样)
|
||||
bool visited[MAX_VERTEX_NUM]; //访问标记数组
|
||||
void BFSTraverse(Graph G){ //对图G进行广度优先搜索
|
||||
for(v=0; v<G.vexnum; ++v){
|
||||
visited[v] = false; //初始化访问标记数组
|
||||
}
|
||||
for(v=0; v<G.vexnum; ++v){
|
||||
if(!visited[v]){ //对每个连通分量调用一次BFS
|
||||
BFS(G,v); //vi没访问过,从vi开始BFS
|
||||
}
|
||||
}
|
||||
}
|
||||
void DFS(Graph G, int v){
|
||||
visit(v); //访问初始顶点v
|
||||
visited[v] = true; //对v做已访问标记
|
||||
for(w=FirstNeighbor(G,v); w>=0; w=NextNeighbor(G,v,w)){//检测v所有的邻接顶点
|
||||
if(!visit[w]){ //w为v尚未访问的邻接顶点
|
||||
DFS(G, w);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 复杂度分析(与BFS一样)
|
||||
|
||||
`空间复杂度`=$$O(|V|)$$,来自递归工作站
|
||||
|
||||
`时间复杂度`:
|
||||
`邻接矩阵`=$$O(|V|^2)$$,`邻接表`=$$O(|V|+|E|)$$
|
||||
|
||||
原理:
|
||||
邻接矩阵:访问点=$$O(|V|)$$,访问边=$$O(|V|^2)$$,时间复杂度=$$O(|V|)+O(|V|^2)=O(|V|^2)$$
|
||||
邻接表:访问点=$$O(|V|)$$,访问无向边=$$O(2|E|)$$,访问无向边=$$O(|E|)$$,
|
||||
`无向图`时间复杂度=$$O(|V|)+O(2|E|)=O(|V|+|E|)$$
|
||||
`有向图`时间复杂度=$$O(|V|)+O(|E|)=O(|V|+|E|)$$
|
||||
|
||||
#### 深度优先生成树、森林(与BFS一样)
|
||||
|
||||
连通图生成树,非连通图生森林
|
||||
|
||||
由于领接矩阵表示法唯一,领接表法表示不唯一
|
||||
导致邻接矩阵生成树唯一,领接表生成树不唯一
|
||||
|
||||
### 四、图的遍历与图的连通性
|
||||
|
||||
`无向图`进行BFS/DFS遍历:`调用BFS/DFS次数=连通分量数`
|
||||
连通图只需调用一次BFS/DFS
|
||||
|
||||
`有向图`进行BFS/DFS遍历:要具体分析
|
||||
若起始顶点到其它顶点都有路径,则只需调用一次
|
||||
强连通图从任意结点都只需调用一次BFS/DFS
|
||||
47
6.5最小生成树(Prim算法、Kruskal算法).md
Normal file
47
6.5最小生成树(Prim算法、Kruskal算法).md
Normal file
@@ -0,0 +1,47 @@
|
||||
## 最小生成树
|
||||
|
||||
### 一、最小生成树的概念
|
||||
|
||||
连通图生成树,非连通图生成森林
|
||||
|
||||
`生成树`是包含图中全部顶点的一个`极小连通子图`
|
||||
`特性`:图中有n个顶点,则它的生成树含有n-1条边。
|
||||
去除一条边会变成非连通图;加上一条边会变成一个回路
|
||||
|
||||
之前学过`广度优先生成树`和`深度优先生成树`。
|
||||
|
||||
`最小生成树`,也叫`最小代价树`。在带权连通无向图的所有生成树中,所有边的代价和最小。
|
||||
|
||||
最小生成树可能很多,但边的权值之和总是唯一且最小的。
|
||||
|
||||
`最小生成的边=顶点数-1`
|
||||
|
||||

|
||||
|
||||
### 二、Prim算法(普利姆算法)
|
||||
|
||||
此算法可以称为`“加点法”`,每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。
|
||||
|
||||
实现思想:以最低代价加入`点`。
|
||||
用两个数组:
|
||||
①`isJoin数组`:标记`各结点`是否已加入树。
|
||||
②`lowCost数组`:各节点加入树的最低代价。
|
||||
|
||||
每轮遍历**isJoin**数组,第一遍找到**lowCost**最低的顶点,然后加入;第二遍循环遍历更新各点的**lowCost**值。
|
||||
则`时间复杂度`=$$O(|V|^2)$$,适合`边稠密图`
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### 三、Kruskal算法(克鲁斯卡尔算法)
|
||||
|
||||
此算法可以称为`“加边法”`,初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。
|
||||
|
||||
实现思想:以最低代价加入`边`。
|
||||
用一个表:
|
||||
(weight(边权),Vertex1(点1),Vertex2(点2))
|
||||
用`并查集`检查下一个边的两个点是否已连接。
|
||||
|
||||
判断两个点是否已连接需要$$O(log_2|E|)$$,工执行e轮。
|
||||
则`时间复杂度`=$$O(|E|log_2|E|)$$,适合`边稀疏图`
|
||||
123
6.6最短路径(BFS、Dijkstra算法、Floyd算法).md
Normal file
123
6.6最短路径(BFS、Dijkstra算法、Floyd算法).md
Normal file
@@ -0,0 +1,123 @@
|
||||
## 最短路径
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### 一、BFS算法(无权图)
|
||||
|
||||
由`广度优先算法`求最短路径:
|
||||
|
||||
```c
|
||||
//图的广度优先搜索(用图的邻接矩阵、领接表都可以,只是FirstNeighbor和NextNeighbor函数实现不一样)
|
||||
bool visited[MAX_VERTEX_NUM]; //访问标记数组
|
||||
void BFSTraverse(Graph G){ //对图G进行广度优先搜索
|
||||
for(v=0; v<G.vexnum; ++v){
|
||||
visited[v] = false; //初始化访问标记数组
|
||||
}
|
||||
InitQueue(Q); //初始化辅助队列Q
|
||||
for(v=0; v<G.vexnum; ++v){
|
||||
if(!visited[v]){ //对每个连通分量调用一次BFS
|
||||
BFS(G,v); //vi没访问过,从vi开始BFS
|
||||
}
|
||||
}
|
||||
}
|
||||
void BFS(Graph G, int v){
|
||||
visit(v); //访问初始顶点v
|
||||
visited[v] = true; //对v做已访问标记
|
||||
EnQueue(Q, v); //顶点v入队
|
||||
while(!isEmpty(Q)){ //队列不空则循环
|
||||
DeQueue(Q, v); //顶点v出队
|
||||
for(w=FirstNeighbor(G,v); w>=0; w=NextNeighbor(G,v,w)){//检测v所有的邻接顶点
|
||||
if(!visited[w]){ //w为v尚未访问的邻接顶点
|
||||
visit(w); //访问顶点w
|
||||
visited[w] = true; //对w做已访问标记
|
||||
EnQueue(Q, w); //顶点w入队
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
实现方式:
|
||||
用两个数组:
|
||||
①`d数组`:记录各点的路径长度。
|
||||
②`path数组`:记录各点的前驱。
|
||||
|
||||
`时间复杂度`:
|
||||
|
||||
`邻接矩阵`=$$O(|V|^2)$$,`邻接表`=$$O(|V|+|E|)$$
|
||||
|
||||
```c
|
||||
//用BFS求顶点U到其它顶点的最短路径(只改了visit函数调用的两行)
|
||||
#define INFINITY 4294967295 //宏定义常量“无穷”,4294967295为最大的int值
|
||||
bool visited[MAX_VERTEX_NUM]; //访问标记数组
|
||||
void BFS(Graph G, int v){
|
||||
//d[i]表示从u到i结点的最短路径
|
||||
for(i=0; i<G.vexnum; ++i){
|
||||
d[i] = INFINITY; //初始化路径长度
|
||||
path[i] = -1; //最短路径从哪个顶点过来
|
||||
}
|
||||
d[u] = 0; //从u开始
|
||||
visited[v] = true; //对v做已访问标记
|
||||
EnQueue(Q, v); //顶点v入队
|
||||
while(!isEmpty(Q)){ //队列不空则循环
|
||||
DeQueue(Q, v); //顶点v出队
|
||||
for(w=FirstNeighbor(G,v); w>=0; w=NextNeighbor(G,v,w)){//检测v所有的邻接顶点
|
||||
if(!visited[w]){ //w为v尚未访问的邻接顶点
|
||||
d[w] = d[u] + 1; //路径长度加1
|
||||
path[w] = u; //最短路径应从u到w
|
||||
visited[w] = true; //对w做已访问标记
|
||||
EnQueue(Q, w); //顶点w入队
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 二、Dijkstra算法(迪杰斯特拉算法)(带权图、无权图)
|
||||
|
||||
`BFS算法的局限性:不适用带权图`
|
||||
|
||||
实现方式:
|
||||
用两个数组:
|
||||
①`final数组`:记录各顶点是否已找到最短路径。
|
||||
②`dist数组`:记录各点的最短路径长度。
|
||||
③`path数组`:记录各点路径上的前驱。
|
||||
|
||||
每轮遍历**final**数组,第一遍找到**dist**最低的顶点,然后加入;第二遍循环遍历更新各点的**dist**值和**path**值。
|
||||
则`时间复杂度`=$$O(|V|^2)$$
|
||||
|
||||
`Dijkstra算法的局限性:不适用负权值带权图`。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### 三、Floyd算法(弗洛伊德算法)(带权图、无权图)
|
||||
|
||||
实现方式:
|
||||
用两个二维数组:
|
||||
①`A数组`:记录各顶点之间目前的最短路径长度
|
||||
②`path数组`:记录两点之间的第一个中转点。
|
||||
|
||||
以某一点为中转点遍历二维数组,更新两个数组,将所有点作为中转点都遍历一遍,形成三重循环
|
||||
则`时间复杂度`=$$O(|V|^3)$$,`空间复杂度`=$$O(|V|^2)$$。
|
||||
|
||||
`Floyd算法的局限性:不适用负权值带权图`。
|
||||
|
||||
```c
|
||||
//省略初始化A和path数组
|
||||
//Floyd算法的核心
|
||||
for(int k=0; k<n; k++){ //考虑以Vk作为中转点
|
||||
for(int i=0; i<n; i++){ //遍历整个矩阵,i为行号,j为列号
|
||||
for(int i=0; i<n; i++){
|
||||
if(A[i][j] > A[i][k] + A[k][j]){ //以Vk作为中转点的路径更短
|
||||
A[i][j] = A[i][k] + A[k][j]; //更新最短路径长度
|
||||
path[i][j] = k; //中转点
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
11
6.7有向无环图.md
Normal file
11
6.7有向无环图.md
Normal file
@@ -0,0 +1,11 @@
|
||||
## 有向无环图——Directed Acyclic Graph
|
||||
|
||||
### 一、有向无环图定义
|
||||
|
||||
`有向无环图`:有向图中不存在环,又称`DAG图`。
|
||||
|
||||
### 二、有向无环图描述表达式
|
||||
|
||||
由表达式画`最少顶点`的有向无环图:
|
||||
|
||||

|
||||
80
6.8拓扑排序.md
Normal file
80
6.8拓扑排序.md
Normal file
@@ -0,0 +1,80 @@
|
||||
## 拓扑排序
|
||||
|
||||
### 一、AOV网
|
||||
|
||||
`AOV网`(Activity On Vertex NetWork,用`顶点表示活动`的网):
|
||||
用DAG图(`有向无环图`)表示一个工程。
|
||||
`顶点表示活动`,边$$<V_i,V_j>$$表示活动$$V_i$$必须先于活动$$V_j$$进行
|
||||
|
||||

|
||||
|
||||
### 二、拓扑排序
|
||||
|
||||
有向无环图中当且仅当满足下列条件时,称为该图的一个拓扑排序:
|
||||
①每个顶点出现且只出现一次
|
||||
②若顶点A在序列中排在B的前面,则在图中不存在从B到A的路径
|
||||
|
||||
每个AOV网有多个拓扑排序序列。
|
||||
|
||||
`拓扑排序:找到做事的先后顺序。`
|
||||
|
||||
拓扑排序的实现:
|
||||
①从AOV网中选择一个没有前驱(入度为0)的顶点并输出。
|
||||
② 从网中删除该顶点和所有以它为起点的有向边。
|
||||
③ 重复①和②直到当前的AOV网为空或当前网中不存在无前驱的顶点为止。
|
||||
|
||||
#### 图的类型描述:
|
||||
|
||||

|
||||
|
||||
```c
|
||||
#define MaxVertexNum 100 //顶点数目最大值
|
||||
//"边(弧)"
|
||||
typedef struct ArcNode{
|
||||
int adjvex; //边(弧)指向那个结点
|
||||
struct ArcNode *next; //指向下一条弧的指针
|
||||
//InfoType info; //边权值
|
||||
}ArcNode;
|
||||
//"顶点"
|
||||
typedef struct VNode{
|
||||
VertexType data; //顶点数据
|
||||
ArcNode *first; //顶点指向的第一条边
|
||||
}VNode, AdjList[MaxVertexNum];
|
||||
//用领接表存储图
|
||||
typedef struct{
|
||||
AdjList vertices; //顶点数组
|
||||
int vernum, arcnum; ////图当前的顶点数和边数(弧数)
|
||||
}ALGraph;
|
||||
```
|
||||
|
||||
### 代码
|
||||
|
||||
```c
|
||||
bool TopologicalSort(Graph G){
|
||||
InitStack(S); //初始化栈,存储入度为0的顶点
|
||||
for(int i=0; i<G.vexnum; i++){
|
||||
if(indegree[i] == 0){
|
||||
Push(S,i); //将所有入度为0的顶点进栈
|
||||
}
|
||||
}
|
||||
int count = 0; //计数,记录当前已经输出的顶点数
|
||||
while(!IsEmpty(S)){ //栈不空,则存在入度为0的顶点
|
||||
Pop(S,i); //栈顶元素出栈
|
||||
print[count++] = i; //输出顶点i
|
||||
for(p=G.vertices[i].firstarc; p; p=p->nextarc){//p是顶点第一个指向的结点,p存在则循环
|
||||
//将所有i指向的顶点的入度减1,并且将入度减为0的顶点压入栈s
|
||||
v = p->adjvex; //v是结点p中存的顶点号
|
||||
if(!(--indegree[v])) //入度先减1,再判断是否为0
|
||||
Push(S,v); //入度为0,则入栈
|
||||
}
|
||||
}
|
||||
if(count < G.vexnum)
|
||||
return false; //排序失败,有向图中有回路
|
||||
else
|
||||
return true; //拓扑排序成功
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||

|
||||
45
6.9关键路径.md
Normal file
45
6.9关键路径.md
Normal file
@@ -0,0 +1,45 @@
|
||||
## 关键路径
|
||||
|
||||
### 一、AOE网
|
||||
|
||||
`AOE网`(Activity On Edge NetWork,用`边表示活动`的网):
|
||||
`带权有向图`中,`顶点表示事件`,`有向边表示活动`,`边上的权值表示完成该活动的开销`。
|
||||
|
||||

|
||||
|
||||
AOE网具有以下两个性质:
|
||||
①只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始;
|
||||
② 只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生。
|
||||
另外,有些活动是可以并行进行的
|
||||
|
||||
在AOE网中仅有一个入度为0的顶点,称为`开始顶点(源点)`,它表示整个工程的开始;
|
||||
也仅有一个出度为0的顶点,称为`结束顶点(汇点)`,它表示整个工程的结束。
|
||||
|
||||
### 二、关键路径
|
||||
|
||||
`从源点到汇点`的`有向路径`可能有多条,所有路径中,具有`最大路径长度`的路径称为
|
||||
`关键路径`,而把`关键路径上的活动`称为`关键活动`
|
||||
|
||||
`特性`:
|
||||
若关键活动耗时增加,则整个工程的工期将增长
|
||||
缩短关键活动的时间,可以缩短整个工程的工期
|
||||
当缩短到一定程度时,关键活动可能会变成非关键活动
|
||||
|
||||
可能有多条关键路径,只提高一条关键路径上的关键活动速度并不能缩短整个工程的工期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短工期的目的。
|
||||
|
||||
`计算`:
|
||||
事件最早、最迟发生时间
|
||||
活动最早、最迟开始时间
|
||||
活动的时间余量=活动最迟开始-最早开始
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
33
7.1查找.md
Normal file
33
7.1查找.md
Normal file
@@ -0,0 +1,33 @@
|
||||
## 查找——Search
|
||||
|
||||
### 一、查找的基本概念
|
||||
|
||||
`查找`:在数据集合中寻找满足某种条件的数据元素的过程称为查找。
|
||||
`查找表`(查找结构):用于查找的数据集合称为查找表,它由同一类型的数据元素(或记录)组成。
|
||||
`关键字`:数据元素中唯一标识该元素的某个数据项的值,使用基于关键字的查找,查找结果应该是唯一的。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### 二、对查找表的常见操作
|
||||
|
||||
查找和插入、删除
|
||||
|
||||
`静态查找表`只关注`查找速度`,`动态查找表`既关注`查找速度`又关注`插入删除是否方便`。
|
||||
|
||||
### 三、查找算法的评价指标
|
||||
|
||||
`查找长度`:对比关键字的次数
|
||||
|
||||
`平均查找长度(ASL)`:对比关键字次数的平均值。
|
||||
|
||||
$$ASL=\sum_{i=1}^{n}{P_iC_i}$$——查找概率×查找长度的总和
|
||||
|
||||
`ASL反映了查找算法的时间复杂度。`
|
||||
|
||||
#### 查找成功和查找失败的平均查找长度:
|
||||
|
||||

|
||||
|
||||

|
||||
58
7.2顺序查找.md
Normal file
58
7.2顺序查找.md
Normal file
@@ -0,0 +1,58 @@
|
||||
## 顺序查找——Sequential Search
|
||||
|
||||
### 一、顺序查找的定义
|
||||
|
||||
`顺序查找`,又叫`线性查找`。
|
||||
|
||||
### 二、顺序查找的实现
|
||||
|
||||
`算法思想`:从头挨个查找。
|
||||
|
||||
普通代码:
|
||||
|
||||
```c
|
||||
//查找表的数据结构(动态分配的顺序表)
|
||||
typedef struct{
|
||||
ElemType *elem; //指向“动态”分配的数组的指针
|
||||
int TableLen; //查找表的当前长度
|
||||
}SSTable;
|
||||
//顺序查找
|
||||
int Search_Seq(SSTable ST, ElemType key){
|
||||
int i;
|
||||
for(i=0; i<ST.TableLen && ST.elem[i]!=key; ++i){//从前往后找,判断是否越界
|
||||
return i==ST.TableLen? -1 : i; //查找成功,则返回元素下标;查找失败,则返回-1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
有哨兵的代码:不用判断越界,效率更高
|
||||
|
||||
```c
|
||||
//查找表的数据结构(动态分配的顺序表)
|
||||
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){//从后往前找,不用判断越界
|
||||
return i; //查找成功,则返回元素下标;查找失败,则返回0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 三、查找效率分析
|
||||
|
||||
`查找成功`:$$ASL_{成功}=\frac{1}{n}+\frac{2}{n}+\cdots+\frac{n}{n}=\frac{n+1}{2}$$,时间复杂度=$$O(n)$$
|
||||
|
||||
`查找成功`:$$ASL_{失败}=n+1$$,时间复杂度=$$O(n)$$
|
||||
|
||||
则,`时间复杂度`=$$O(n)$$
|
||||
|
||||
### 四、顺序查找的优化
|
||||
|
||||
若查找表是`有序表`,可`优化查找失败`的ASL
|
||||
|
||||
若查找元素的`查找概率不同`,将`概率高的放前面`排序,可`优化查找成功`的ASL
|
||||
64
7.3折半查找.md
Normal file
64
7.3折半查找.md
Normal file
@@ -0,0 +1,64 @@
|
||||
## 折半查找——Binary Search
|
||||
|
||||
### 一、折半查找的定义
|
||||
|
||||
`折半查找`,又叫`二分查找`。仅适用于`有序的顺序表`。
|
||||
|
||||
### 二、折半查找的实现
|
||||
|
||||
`算法思想`:每次从中间分,判断自己是哪一半
|
||||
|
||||
普通代码:
|
||||
|
||||
```c
|
||||
//查找表的数据结构(动态分配的顺序表)
|
||||
typedef struct{
|
||||
ElemType *elem; //指向“动态”分配的数组的指针
|
||||
int TableLen; //查找表的当前长度
|
||||
}SSTable;
|
||||
//折半查找
|
||||
int Binary_Search(SSTable L, ElemType key){
|
||||
int low =0, high = L.TableLen-1, 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; //查找失败,返回-1
|
||||
}
|
||||
```
|
||||
|
||||
### 三、查找效率分析
|
||||
|
||||

|
||||
|
||||
### 四、折半查找判定树的构造
|
||||
|
||||
#### 构造:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
#### 特性:
|
||||
|
||||

|
||||
|
||||
查找表有n个关键字,则失败结点有n+1个
|
||||
|
||||

|
||||
|
||||
与`折半查找判定树`的高度h有关。高度越小,查找效率越高
|
||||
|
||||
最好情况,平均查找长度=$$O(log_2n)$$
|
||||
|
||||
最坏情况,平均查找长度=$$O(n)$$
|
||||
|
||||
则`时间复杂度`=$$O(log_2n)$$
|
||||
33
7.4分块查找.md
Normal file
33
7.4分块查找.md
Normal file
@@ -0,0 +1,33 @@
|
||||
## 分块查找——Block Search
|
||||
|
||||
### 一、分块查找的定义
|
||||
|
||||
`分块查找`,又叫`索引顺序查找`。
|
||||
|
||||
### 二、折半查找的实现
|
||||
|
||||
`算法思想`:用一个`索引表`给数据归类。
|
||||
|
||||
算法过程:
|
||||
①在`索引表`中确定待查记录所属的分块(`可顺序、可折半`)
|
||||
②在`块内顺序查找`
|
||||
|
||||

|
||||
|
||||
#### 用折半查找索引表:
|
||||
|
||||
若索引表中不包含目标关键字,则折半查找索引表`最终停在low>high`,要`在low所指分块中查找`
|
||||
|
||||
### 三、查找效率分析
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### 四、分块查找的优化
|
||||
|
||||
上面的分块查找对插入删除不友好。
|
||||
|
||||
改进:索引表为顺序表,查找表为链表。
|
||||
|
||||

|
||||
104
7.5B树.md
Normal file
104
7.5B树.md
Normal file
@@ -0,0 +1,104 @@
|
||||
## B树——B-Tree
|
||||
|
||||

|
||||
|
||||
### 一、B树的定义
|
||||
|
||||
`B树`,又名`多路平衡查找树`(`m叉查找树`)
|
||||
|
||||
数据库索引技术里大量使用者B树和B+树的数据结构.
|
||||
|
||||
B树是由`二叉排序树`升级为`m叉查找树`
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
#### 核心特征
|
||||
|
||||

|
||||
|
||||
### 二、B树的实现
|
||||
|
||||
#### 二叉排序树的类型表述(与二叉树一样)(二叉树的链式存储)
|
||||
|
||||
```c
|
||||
typedef struct TreeNode{
|
||||
int data; //数据域
|
||||
struct BiTNode *lchide, *rchild; //左、右孩子指针
|
||||
}BSTNode, *BSTree;
|
||||
```
|
||||
|
||||
#### B树的类型表述(m叉查找树)
|
||||
|
||||
```c
|
||||
typedef struct TreeNode{
|
||||
int datas[m-1]; //zui
|
||||
struct BiTNode *child[m]; //左、右孩子指针
|
||||
}BSTNode, *BSTree;
|
||||
```
|
||||
|
||||
#### 5叉查找树
|
||||
|
||||

|
||||
|
||||
### 三、查找效率分析
|
||||
|
||||
#### 保证查找效率:
|
||||
|
||||
每个结点的关键字太少,导致树变高,查找效率就会下降。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
#### B树的高度
|
||||
|
||||
最小高度:
|
||||
|
||||

|
||||
|
||||
最大高度:
|
||||
|
||||

|
||||
|
||||
综上,高度为:
|
||||
|
||||

|
||||
|
||||
### 四、B树的插入
|
||||
|
||||
插入满一个结点后,从中间拆开,让中间位置($$\lceil m/2 \rceil$$)向上产生父结点,两边成为其孩子结点,依次类推
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### 五、B树的删除
|
||||
|
||||
情况一:删除`非终端结点`中的关键字
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
情况一:删除`终端结点`中的关键字
|
||||
|
||||
①删除33时右边结点够借:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
②删除49时,右边的结点不够借:
|
||||
|
||||
解决方法:将`父结点中夹的关键字`取下与左右孩子`合并`。
|
||||
|
||||

|
||||
|
||||
40
7.6B+树.md
Normal file
40
7.6B+树.md
Normal file
@@ -0,0 +1,40 @@
|
||||
## B+树——B-Plus-Tree
|
||||
|
||||
### 一、B树的定义
|
||||
|
||||
`B+树`,又名`多级分块查找`。
|
||||
|
||||
数据库索引技术里大量使用者B树和B+树的数据结构.
|
||||
|
||||
B+树是由`分块查找`升级为`查找树`
|
||||
|
||||
B树任何一层都可以找到,因为每一层都是数据
|
||||
而B+树除叶子结点外,其余都是分类,只有最底层才会指向数据,必须找到最底层才能知道是否成功。
|
||||
|
||||
B+树有两种查找方式:①分块查找:从根结点查 ②顺序查找:从P开始横着查。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### 二、B+树的实现
|
||||
|
||||
无
|
||||
|
||||
### 三、查找效率分析
|
||||
|
||||
与B树一样
|
||||
|
||||
### 四、B+树的插入
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### 五、B+树的删除
|
||||
|
||||
与B树一样
|
||||
|
||||
### 六、B树与B+树的对比
|
||||
|
||||

|
||||
113
7.7散列查找(哈希查找).md
Normal file
113
7.7散列查找(哈希查找).md
Normal file
@@ -0,0 +1,113 @@
|
||||
## 散列查找(哈希查找)——Hash Search
|
||||
|
||||

|
||||
|
||||
### 一、哈希查找的定义
|
||||
|
||||
`散列表`(Hash Table),又名`哈希表`,是一种数据结构。
|
||||
|
||||
`特点`:数据元素的`关键字与其存储地址直接相关`。
|
||||
|
||||
通过`散列函数(哈希函数)`将关键字与存储地址一一映射。
|
||||
|
||||
散列查找是典型的`“用空间换时间”的算法`
|
||||
|
||||
`装填因子α` = 表中记录个数/散列表表长
|
||||
|
||||
`查找效率`:取决于`散列函数`、`处理冲突的方法`、`装填因子α`
|
||||
|
||||

|
||||
|
||||
#### 处理冲突的方法——拉链法
|
||||
|
||||

|
||||
|
||||
### 二、常见的散列函数(哈希函数)
|
||||
|
||||
冲突是由散列函数导致的,`冲突越多,查找效率越低`
|
||||
|
||||
散列函数的设计目的:让不同的关键字的冲突尽可能少。
|
||||
|
||||
①除留余数法
|
||||
②直接定址法
|
||||
③数字分析法
|
||||
④平方取中法
|
||||
|
||||
处理冲突的方法:以拉链法为主。
|
||||
|
||||
#### 1.除留余数法
|
||||
|
||||
$$H(key)=key\mod{p}$$
|
||||
|
||||
除数p取法:散列表表长为m,取一个不大于m但最接近或等于m的`质数p`
|
||||
|
||||
查找方法:当p=13时,查找66,66%13=1,则在a[1]下的链表中寻找。
|
||||
|
||||
`查找效率分析`:
|
||||
|
||||

|
||||
|
||||
#### 2.直接定址法
|
||||
|
||||
$$H(key)=key$$ 或 $$H(key)=a*key+b$$
|
||||
|
||||
这种计算最简单,适合`关键字分布连续`的情况
|
||||
|
||||

|
||||
|
||||
#### 3.数字分析法
|
||||
|
||||
选取数码`分布较为均匀的若干位`作为散列地址。
|
||||
|
||||

|
||||
|
||||
#### 4.平方取中法
|
||||
|
||||
取`关键字的平方值的中间几位`作为散列地址。
|
||||
|
||||
具体取多少位要视实际情况而定。这种方法得到的`散列地址与关键字的每位都有关系`,因此使得
|
||||
散列地址分布比较均匀,适用于关键字的每位取值都不够均匀或均小于散列地址所需的位数。
|
||||
|
||||

|
||||
|
||||
### 三、处理冲突的方法
|
||||
|
||||
①拉链法
|
||||
②开放定址法
|
||||
③再散列法
|
||||
|
||||
#### 1.拉链法
|
||||
|
||||
上面讲了
|
||||
|
||||
#### 2.开放定址法
|
||||
|
||||

|
||||
|
||||
`d的不同取法`:
|
||||
|
||||
##### ①线性探测法
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
##### ②平方探测法
|
||||
|
||||
散列表长必须是$$4j+3$$
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
##### ③伪随机序列法
|
||||
|
||||
d取随机值
|
||||
|
||||
#### 3.再散列法
|
||||
|
||||

|
||||
29
8.1排序.md
Normal file
29
8.1排序.md
Normal file
@@ -0,0 +1,29 @@
|
||||
## 排序——Sort
|
||||
|
||||

|
||||
|
||||
### 一、排序的定义
|
||||
|
||||
`排序`,就是重新排列表中的元素,使表中的元素满足`按关键字有序`的过程。
|
||||
|
||||

|
||||
|
||||
### 二、排序算法的应用
|
||||
|
||||
对数据进行大小排序
|
||||
|
||||
### 三、排序的算法评价指标
|
||||
|
||||
时间复杂度、空间复杂度、`稳定性`
|
||||
|
||||

|
||||
|
||||
### 四、排序算法的分类
|
||||
|
||||

|
||||
|
||||
## 常见排序算法与其时间复杂度:
|
||||
|
||||

|
||||
|
||||

|
||||
95
8.2插入排序(稳定).md
Normal file
95
8.2插入排序(稳定).md
Normal file
@@ -0,0 +1,95 @@
|
||||
## 插入排序——Insertion Sort
|
||||
|
||||

|
||||
|
||||
### 一、算法思想:
|
||||
|
||||
每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列中,直到全部记录插入完成。
|
||||
|
||||

|
||||
|
||||
### 二、代码实现:
|
||||
|
||||
普通:
|
||||
|
||||
```c
|
||||
// 直接插入排序
|
||||
void InsertSort(int A[], int n){
|
||||
int i, j, temp;
|
||||
for(i=1; i<n; i++){ //讲个元素插入已排好的序列中
|
||||
if(A[i]<A[i-1]){ //若A[i]的关键字小于前驱
|
||||
temp = A[i]; //用temp暂存A[i]
|
||||
for(j=i-1; i>=0 && A[j]<temp; --j){ //检查所有前面已排好序的元素
|
||||
A[j+1] = A[j]; //所有大于temp的都后移
|
||||
}
|
||||
A[j+1] = temp; //复制到插入位置
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
带哨兵:
|
||||
|
||||
```c
|
||||
// 直接插入排序(带哨兵)
|
||||
void InsertSort(int A[], int n){
|
||||
int i, j;
|
||||
for(i=2; i<=n; i++){ //讲个元素插入已排好的序列中
|
||||
if(A[i]<A[i-1]){ //若A[i]的关键字小于前驱
|
||||
A[0] = A[i]; //复制为哨兵,A[0]作为哨兵
|
||||
for(j=i-1; A[0]<A[j]; --j){ //从后往前查找待插入位置
|
||||
A[j+1] = A[j]; //所有大于A[0]的都后移
|
||||
}
|
||||
A[j+1] = A[0]; //复制到插入位置
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 三、算法效率分析
|
||||
|
||||
`空间复杂度`=$$O(1)$$,因为需要的辅助变量为int i,j,temp,
|
||||
|
||||
时间复杂度:
|
||||
最好情况=$$n-1$$,时间复杂度=$$O(n)$$
|
||||
最坏情况=$$2+3+\cdots+n+(n+1)=\frac{n(n+3)}{2}$$,时间复杂度=$$O(n^2)$$
|
||||
平均`时间复杂度`=$$O(n^2)$$
|
||||
|
||||
算法稳定性:`稳定`
|
||||
|
||||
### 四、优化——折半插入排序
|
||||
|
||||
折半查找找出插入的位置
|
||||
|
||||
当A[mid]=A[0]时,将[mid,i-1]内的元素全部后移,并将A[0]复制到mid所在位置。
|
||||
|
||||
Low>high时停止,将[low,i-1]内的元素全部后移,并将A[0]复制到low所在位置。
|
||||
|
||||
带哨兵:
|
||||
|
||||
```c
|
||||
// 直接插入排序(带哨兵)
|
||||
void InsertSort(int A[], int n){
|
||||
int i, j, low,high,mid;
|
||||
for(i=2; i<=n; i++){ //讲个元素插入已排好的序列中
|
||||
if(A[i]<A[i-1]){ //若A[i]的关键字小于前驱
|
||||
A[0] = 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[0]的都后移
|
||||
}
|
||||
A[high+1] = A[0]; //复制到插入位置
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
虽然对比关键字次数变少,但时间复杂度的数量级依然没变。
|
||||
|
||||
`时间复杂度`=$$O(n^2)$$
|
||||
52
8.3希尔排序(不稳定).md
Normal file
52
8.3希尔排序(不稳定).md
Normal file
@@ -0,0 +1,52 @@
|
||||
## 希尔排序——Shell's Sort
|
||||
|
||||

|
||||
|
||||
`希尔排序`又叫`缩小增量排序`。
|
||||
|
||||
1959年Shell发明,第一个突破$$O(n^2)$$的排序算法,是`简单插入排序的改进版`。它与插入排序的不同之处在于,它会`优先比较距离较远的元素`。
|
||||
|
||||
### 一、算法思想:
|
||||
|
||||
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
|
||||
|
||||
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
|
||||
- 按增量序列个数k,对序列进行k 趟排序;
|
||||
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
|
||||
|
||||

|
||||
|
||||
### 二、代码实现:
|
||||
|
||||
在简单插入排序外加了`步长变化`
|
||||
|
||||
```c
|
||||
//希尔排序
|
||||
void InsertSort(int A[], int n){
|
||||
int d, i, j;
|
||||
for(d=n/2; d>1; d=d/2){ //步长变化
|
||||
for(i=d+1; i<=n; ++i){ //讲个元素插入已排好的序列中
|
||||
if(A[i]<A[i-d]){ //若A[i]的关键字小于前驱
|
||||
A[0] = A[i]; //复制为哨兵,A[0]作为哨兵
|
||||
for(j=i-d; j>0 && A[0]<A[j]; j-=d){ //从后往前查找待插入位置
|
||||
A[j+d] = A[j]; //所有大于A[0]的都后移
|
||||
}
|
||||
A[j+d] = A[0]; //复制到插入位置
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 三、算法效率分析
|
||||
|
||||
`空间复杂度`=$$O(1)$$,因为需要的辅助变量为int d,i,j,
|
||||
|
||||
时间复杂度:
|
||||
无法计算,`时间复杂度`大概为=$$O(n^{1.3})$$
|
||||
|
||||
算法稳定性:`不稳定`
|
||||
|
||||
`仅适用于顺序表,不适用于链表`
|
||||
|
||||

|
||||
58
8.4冒泡排序(稳定).md
Normal file
58
8.4冒泡排序(稳定).md
Normal file
@@ -0,0 +1,58 @@
|
||||
## 冒泡排序——Bubble Sort
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
`冒泡排序`是一种简单的排序算法。它重复地走访过要排序的数列,`一次比较两个元素`,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为`越小的元素会经由交换慢慢“浮”到数列的顶端`。
|
||||
|
||||
`确定最小数或最大数的位置`
|
||||
|
||||
### 一、算法思想:
|
||||
|
||||
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
|
||||
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
|
||||
- 针对所有的元素重复以上的步骤,除了最后一个;
|
||||
- 重复步骤1~3,直到排序完成。
|
||||
|
||||

|
||||
|
||||
### 二、代码实现:
|
||||
|
||||
从后往前两两比较,逆序则交换,比较完可将最小的放前面,将第一个去除,比较剩下的,再确定次小,以此类推。
|
||||
|
||||
```c
|
||||
//交换
|
||||
void swap((int &a, int &b){
|
||||
int temp = a;
|
||||
a = b;
|
||||
b = temp;
|
||||
}
|
||||
//冒泡排序
|
||||
void BubbleSort(int A[],int n){
|
||||
for(int i=0; i<n; i++){
|
||||
bool flag = false;
|
||||
for(int j=n-1; j>i; j--) //一趟冒泡排序:从后往前两两比较
|
||||
if(A[j-1]>A[j]){ //若逆序
|
||||
swqp(A[j-1],A[j]); //则交换
|
||||
flag = true;
|
||||
}
|
||||
if(flag == false){
|
||||
return; //本趟遍历后没有发生交换,说明表已经有序
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 三、算法效率分析
|
||||
|
||||
`空间复杂度`=$$O(1)$$,因为需要的辅助变量为bool flag
|
||||
|
||||
时间复杂度:
|
||||
最好情况=$$n-1$$,时间复杂度=$$O(n)$$
|
||||
最坏情况=$$(n-1)+(n-2)+\cdots+2+1=\frac{n(n-1)}{2}$$,时间复杂度=$$O(n^2)$$
|
||||
平均`时间复杂度`=$$O(n^2)$$
|
||||
|
||||
算法稳定性:`稳定`
|
||||
|
||||
顺序表和链表都可以。
|
||||
62
8.5快速排序(不稳定).md
Normal file
62
8.5快速排序(不稳定).md
Normal file
@@ -0,0 +1,62 @@
|
||||
## 快速排序——Quick Sort
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
`快速排序`的基本思想:通过一趟排序将待排记录`分隔成独立的两部分`,其中一部分记录的关键字均比另一部分的关键字小,则`可分别对这两部分记录继续进行排序`,以达到整个序列有序。
|
||||
|
||||
`确定中间数的位置`
|
||||
|
||||
### 一、算法思想:
|
||||
|
||||
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
|
||||
|
||||
- 从数列中挑出一个元素,称为 “基准”(pivot);
|
||||
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
|
||||
- 递归地(recursive)把小于基准值元素的子数列和大于
|
||||
|
||||

|
||||
|
||||
### 二、代码实现:
|
||||
|
||||
将第一个作为枢轴,后面的元素与枢轴比较,一此low,一high比较,low的大则放到high处,high的小则放到low处,直到low=high,找到枢轴位置。
|
||||
|
||||
按枢轴分成两部分,分别做上面的方法,以此类推(递归)。
|
||||
|
||||
```c
|
||||
//用第一个元素将待排序序列划分成左右两个部分
|
||||
int Partition(int A[],int low,int high){
|
||||
int pivot = A[Low]; //第一个元素作为枢轴
|
||||
while(low < high){ //用10w、high搜索枢轴的最终位置
|
||||
while(low<high && A[high]>=pivot ) --high;
|
||||
A[Low] = A[high]; //比枢轴小的元素移动到左端
|
||||
while(low<high && A[Low]<=pivot)++low;
|
||||
A[high] = A[low]; //比枢轴大的元素移动到右端
|
||||
}
|
||||
A[low] = pivot; //枢轴元素存放到最终位置
|
||||
return low; //返回存放枢轴的最终位置
|
||||
}
|
||||
//快速排序
|
||||
void QuickSort(int A[],int low,int high){
|
||||
if(low<high){ //递归跳出的条件
|
||||
int pivotpos =Partition(A,Low,high); //划分
|
||||
QuickSort(A,low,pivotpos-1); //划分左子表
|
||||
QuickSort(A,pivotpos+1,high); //划分右子表
|
||||
}
|
||||
```
|
||||
|
||||
### 三、算法效率分析
|
||||
|
||||
与递归深度有关
|
||||
|
||||
`空间复杂度`=$$O(递归深度)$$
|
||||
最好空间复杂度=$$O(log_2n)$$
|
||||
最坏 空间复杂度=$$O(n)$$
|
||||
|
||||
时间复杂度=$$O(n*递归深度)$$
|
||||
最好时间复杂度=$$O(nlog_2n)$$
|
||||
最坏时间复杂度=$$O(n^2)$$
|
||||
平均`时间复杂度`=$$O(nlog_2n)$$
|
||||
|
||||
算法稳定性:`不稳定`
|
||||
42
8.6简单选择排序(稳定).md
Normal file
42
8.6简单选择排序(稳定).md
Normal file
@@ -0,0 +1,42 @@
|
||||
## 简单选择排序——Select Sort
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
`选择排序`是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
|
||||
|
||||
### 一、算法思想:
|
||||
|
||||
n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:
|
||||
|
||||
- 初始状态:无序区为R[1..n],有序区为空;
|
||||
- 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
|
||||
- n-1趟结束,数组有序化了。
|
||||
|
||||

|
||||
|
||||
### 二、代码实现:
|
||||
|
||||
```c
|
||||
//简单选择排序
|
||||
void SelectSort(int A[], int n){
|
||||
for(int i=0; i<n-1; i++){
|
||||
int min = i;
|
||||
for(int j=i+1;j<n;j++){
|
||||
if(A[j]<A[min]) min = j;
|
||||
}
|
||||
if(min!=i) swap(A[i],A[min]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 三、算法效率分析
|
||||
|
||||
`空间复杂度`=$$O(1)$$,因为需要的辅助变量为int i,j
|
||||
|
||||
时间复杂度=$$(n-1)+(n-2)+\cdots+2+1=\frac{n(n-1)}{2}$$,`时间复杂度`=$$O(n^2)$$
|
||||
|
||||
算法稳定性:`稳定`
|
||||
|
||||
顺序表和链表都可以
|
||||
83
8.7堆排序(不稳定).md
Normal file
83
8.7堆排序(不稳定).md
Normal file
@@ -0,0 +1,83 @@
|
||||
## 堆排序——Heap Sort
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
`堆排序`是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
|
||||
|
||||

|
||||
|
||||
大根堆:完全二叉树中,根>=左、右。
|
||||
|
||||
小根堆:完全二叉树中,根<=左、右。
|
||||
|
||||
### 一、算法思想:
|
||||
|
||||
- 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
|
||||
- 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
|
||||
- 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
|
||||
|
||||

|
||||
|
||||
### 二、代码实现:
|
||||
|
||||
#### 1.先建立大根堆:
|
||||
|
||||
①建立大根堆,只需检查所有非终端结点是否满足大根堆要求。顺序存储的二叉树中非终端结点编号为$$i<\lfloor n/2 \rfloor$$
|
||||
②从$$i=\lfloor n/2 \rfloor$$开始,从后往前处理非终端结点,判断第i个结点与它的孩子结点2i,2i+1是否满足大根堆要求。不满足,则根与最大的孩子互换。
|
||||
③换了的孩子还要继续判断与它的孩子是否满足,依次往下判断。直到没有可以换的。(`小元素不断下坠`)
|
||||
|
||||
#### 2.基于大根堆进行排序
|
||||
|
||||
①大根堆可知最前面是最大的,则交换最前与最后元素
|
||||
②排除最后元素,重新建立大根堆,建好后再将第一个元素与最后一个元素交换(排除最后已确定的元素),以此类推。
|
||||
|
||||
```c
|
||||
//建立大根堆(处理所有的非终端结点)(初始调整范围)
|
||||
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]; //k指向根结点,用A[0]暂存
|
||||
for(int i=2*k; i<=len; i*=2){ //沿key较大的子结点下下筛选
|
||||
if(i<len && A[i]<A[i+1]) i++; //右孩子更大,则i++
|
||||
if(A[0] >= A[i]) break; //满足根>左、右孩子
|
||||
else{
|
||||
A[k] = A[i]; //将大的孩子成为根
|
||||
k = i; //k指向新的根
|
||||
}
|
||||
}
|
||||
A[k] = A[0]; //被筛选结点的值放入最终位置
|
||||
}
|
||||
//堆排序的完整逻辑
|
||||
void HeapSort(int A[],int len){
|
||||
BuildMaxHeap(A,len); //初始建立大根堆
|
||||
for(int i=len; i>1; i--){ //找n-1次最大元素
|
||||
swap(A[i],A[1]); //堆顶元素与堆顶元素交换
|
||||
HeadAdjust(A,1,i-1); //交换后只有A[1]不满足大根堆要求,则调整A[1]即可
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 三、算法效率分析
|
||||
|
||||
`空间复杂度`=$$O(1)$$
|
||||
|
||||
时间复杂度:
|
||||
|
||||
建堆:
|
||||
一个结点,每下坠一层,最多只对比关键字两次。
|
||||
树高为$$h$$,在i层的结点最多下坠$$h-i$$层,则对比关键字$$2*(h-i)$$
|
||||
第一层对比$$2^0*2*(h-1)$$,第二层对比$$2^1*2*(h-2)$$,则第i层对比$$2^{i-1}*2*(h-i)$$,共h-1层
|
||||
求和后不超过$$4n$$,则`建堆的时间复杂度`=$$O(n)$$
|
||||
|
||||
下坠:
|
||||
n-1次下坠,每次最多下h层,因$$h=log_2n$$,则`下坠的时间复杂度`=$$O(nlog_2n)$$
|
||||
|
||||
则堆排序的`时间复杂度`=$$O(n)$$+$$O(nlog_2n)$$=$$O(nlog_2n)$$
|
||||
|
||||
算法稳定性:`不稳定`
|
||||
63
8.8归并排序(稳定).md
Normal file
63
8.8归并排序(稳定).md
Normal file
@@ -0,0 +1,63 @@
|
||||
## 归并排序——Merge Sort
|
||||
|
||||

|
||||
|
||||
`归并排序`是建立在归并操作上的一种有效的排序算法。该算法是采用`分治法`(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
|
||||
|
||||
归并:把两个或多个已经有序的序列合并成一个。
|
||||
|
||||
m路归并:将m个有序的序列合并成一个,每选出一个元素需要对比关键字m-1次。
|
||||
|
||||
2路归并:
|
||||
|
||||

|
||||
|
||||
### 一、算法思想:
|
||||
|
||||
- 把长度为n的输入序列分成两个长度为n/2的子序列;
|
||||
- 对这两个子序列分别采用归并排序;
|
||||
- 将两个排序好的子序列合并成一个最终的排序序列。
|
||||
|
||||

|
||||
|
||||
### 二、代码实现:
|
||||
|
||||
```c
|
||||
int *B = (int *)malloc(n*sizeof(int)); //辅助数组B
|
||||
//将两个有序数组归并(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++]; //将较小的复制到A中,先赋值,再将指针后移。
|
||||
else A[k] = B[j++];
|
||||
}
|
||||
while(j<=mid) A[k++] = B[i++]; //先赋值,再将指针后移。
|
||||
while(j<=high) A[k++] = B[j++]; //先赋值,再将指针后移。
|
||||
}
|
||||
//归并排序
|
||||
void MergeSort(int A[],int low,int high){
|
||||
if(low<high){
|
||||
int mid = (low+high)/2; //从中间划分
|
||||
MergeSort(A,low,mid); //对左半部分归并排序
|
||||
MergeSort(A,mid+1,high); //对右半部分归并排序
|
||||
Merge(A,low,mid,high); //归并
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 三、算法效率分析
|
||||
|
||||
`空间复杂度`=$$O(n)$$,因为需要的辅助数组B
|
||||
|
||||
时间复杂度:
|
||||
n个元素进行二路归并,需进行$$\lceil log_2n \rceil$$趟
|
||||
每趟归并的时间复杂度=$$O(n)$$
|
||||
`时间复杂度`=$$O(nlog_2n)$$
|
||||
|
||||
算法稳定性:`稳定`
|
||||
|
||||
顺序表和链表都可以。
|
||||
71
8.9基数排序(稳定).md
Normal file
71
8.9基数排序(稳定).md
Normal file
@@ -0,0 +1,71 @@
|
||||
## 基数排序——Radix Sort
|
||||
|
||||

|
||||
|
||||
`基数排序`是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
|
||||
|
||||
`r为基数`,基数是每一位可以取的数的范围,例如10进制的一位数可以取0~9,则`十进制的基数r=10`。
|
||||
|
||||
d为关键字个数,也是趟数,例如`10进制的一位数为一个d`。
|
||||
|
||||
n为元素个数
|
||||
|
||||
基数排序擅长树立d小,r小,n大的元素序列进行排序。
|
||||
|
||||

|
||||
|
||||
### 一、算法思想:
|
||||
|
||||
- 取得数组中的最大数,并取得位数;
|
||||
- arr为原始数组,从最低位开始取每个位组成radix数组;
|
||||
- 对radix进行计数排序(利用计数排序适用于小范围数的
|
||||
|
||||

|
||||
|
||||
### 二、代码实现:
|
||||
|
||||
需一个辅助链队列来实现
|
||||
|
||||

|
||||
|
||||
```c
|
||||
int *B = (int *)malloc(n*sizeof(int)); //辅助数组B
|
||||
//将两个有序数组归并(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++]; //将较小的复制到A中,先赋值,再将指针后移。
|
||||
else A[k] = B[j++];
|
||||
}
|
||||
while(j<=mid) A[k++] = B[i++]; //先赋值,再将指针后移。
|
||||
while(j<=high) A[k++] = B[j++]; //先赋值,再将指针后移。
|
||||
}
|
||||
//归并排序
|
||||
void MergeSort(int A[],int low,int high){
|
||||
if(low<high){
|
||||
int mid = (low+high)/2; //从中间划分
|
||||
MergeSort(A,low,mid); //对左半部分归并排序
|
||||
MergeSort(A,mid+1,high); //对右半部分归并排序
|
||||
Merge(A,low,mid,high); //归并
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 三、算法效率分析
|
||||
|
||||
空间、时间复杂度`取决于基数r`
|
||||
|
||||
`空间复杂度`=$$O(r)$$,
|
||||
因为需要r个辅助队列,链队列是增加指针域,则每个链队列的空间复杂度=$$O(1)$$
|
||||
|
||||
`时间复杂度`=$$O(d(n+r))$$
|
||||
|
||||
算法稳定性:`稳定`
|
||||
|
||||
### 四、基数排序的应用
|
||||
|
||||

|
||||
Reference in New Issue
Block a user