commit 9f7bd903a36e9efcd60601dd56fc5c1480d7646a Author: 岩岩 <68364461+oxyanyano@users.noreply.github.com> Date: Mon Mar 28 16:49:12 2022 +0800 Add files via upload diff --git a/1.1线性表.md b/1.1线性表.md new file mode 100644 index 0000000..371f0ec --- /dev/null +++ b/1.1线性表.md @@ -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); //判空操作 +``` + +### 三、存储结构 + +`顺序存储`和`链式存储` + +### 四、线性表分类 + +线性表的顺序存储:顺序表 + +线性表的链式存储:单双链表、循环链表 + +静态链表比较特殊:逻辑上离散,物理上连续 diff --git a/1.2顺序表——线性表的顺序存储.md b/1.2顺序表——线性表的顺序存储.md new file mode 100644 index 0000000..829e92c --- /dev/null +++ b/1.2顺序表——线性表的顺序存储.md @@ -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; iL.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; jL.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 +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<=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; iL.length){ + return -1; + }else{ + x = L.data[i-1]; + for(int j=i; jnext = 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; + } +} +``` + +#### 建立单链表 + +##### 头插法建立单链表 + +用于链表的逆置 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210417202853466.png) + +```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; +} +``` + +##### 尾插法建立单链表 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210417204450775.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDE2MjM2MQ==,size_16,color_FFFFFF,t_70) + +```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 && jnext; + 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<data<<" "; + p = p->next; + } + cout< +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<data<<" "; + p = p->next; + } + cout<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 && jnext; + 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."<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<<"第三个结点的值为:"<data<next->data<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) + +![插入操作](https://img-blog.csdnimg.cn/20210418105409128.jpg) + +```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) + +![删除操作](https://img-blog.csdnimg.cn/20210418104109509.png) + +```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 && jnext; + 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<data<<" "; + p = p->next; + } + cout<next != NULL){ + DeleteNextNode(L); + } + free(L); //释放头结点 + L = NULL; +} +``` + +### 四、完整代码 + +```c +#include +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<data<<" "; + p = p->next; + } + cout<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 && jnext; + 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."<next; + p->next = q->next; + q->next->prior = p; + free(q); +} + +//判空操作 +bool Empty(DLinkList L){ + if(L->next == NULL){ + cout<<"L is null"<next->data<prior->data<data<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 && jnext; + 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; +} +``` + +#### 求表长、遍历、销毁 + +与单双链表一样 + diff --git a/1.6静态链表.md b/1.6静态链表.md new file mode 100644 index 0000000..cf6b991 --- /dev/null +++ b/1.6静态链表.md @@ -0,0 +1,77 @@ +## 静态链表—— Static Linked List + +### 一、静态链表的定义 + +`静态链表`借助`数组`来描述线性表的链式存储结构,结点也有`数据域data`和`指针域next`,这里的指针是结点的`相对地址(数组下标)`,又称`游标`。和顺序表一样,静态链表也需要预先分配一块连续的内存空间。 + +![静态链表](https://img-blog.csdnimg.cn/20210418162136494.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDE2MjM2MQ==,size_16,color_FFFFFF,t_70) + +### 二、静态链表的特点 + +顺序表:逻辑上连续,物理上连续 + +静态链表:逻辑上离散,物理上连续 + +单双链表:逻辑上离散,物理上离散 + +静态链表比顺序表好。比单双链表差 + +`优点`:增加、删除操作不需要移动大量元素 + +`缺点`:①不能随机存取。只能从头结点遍历找 + +​ ②容量不可变 + +### 三、静态链表上的操作 + +#### 静态链表的类型描述 + +```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 + +#### 查找 + +从头结点出发挨个往后遍历结点 + +#### 求表长 + +从头结点出发挨个往后遍历结点 + +#### 遍历 + +从头结点出发挨个往后遍历结点 \ No newline at end of file diff --git a/2.1栈.md b/2.1栈.md new file mode 100644 index 0000000..d002542 --- /dev/null +++ b/2.1栈.md @@ -0,0 +1,36 @@ +## 栈—— Stack + +### 一、栈的定义 + +`栈`是线性表结构的一种,但是栈结构的插入与删除操作都只能从同一端进行,所以栈结构是一种受限制的线性表结构,数据的插入与删除符合LIFO的原则(也就是`后进先出`,`先进后出`)。 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200317151355505.png#pic_center) + +### 二、栈的基本操作 + +`注`:参数代“&”表示:方法运行完后,对参数修改的结果要`“带回来”` + +对数据的操作:创销,增删查改 + +```c +InitStack(&S); //初始化表:构造一个空的栈S,分配内存空间 +DestoryStack(&S); //销毁操作:销毁栈,并释放栈S所占用的内存空间 + +Push(&S,x); //进栈,若栈S未满,则将x加入使之成为新栈 +Pop(&S,&x); //出栈,若栈S非空,则弹出栈顶元素,并用x返回 + +GetTop(S,&x); //读栈顶元素,若栈S非空,则将x返回栈顶元素 + +//其它常用操作 +StackEmpty(S); //判空操作 +``` + +### 三、存储结构 + +`顺序存储`和`链式存储` + +### 四、栈分类 + +栈的顺序存储:顺序栈 + +栈的链式存储:链栈 diff --git a/2.2顺序栈——栈的顺序存储.md b/2.2顺序栈——栈的顺序存储.md new file mode 100644 index 0000000..7706d1c --- /dev/null +++ b/2.2顺序栈——栈的顺序存储.md @@ -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; +} +``` diff --git a/2.3链栈——栈的链式存储.md b/2.3链栈——栈的链式存储.md new file mode 100644 index 0000000..7acc8f1 --- /dev/null +++ b/2.3链栈——栈的链式存储.md @@ -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; +} +``` \ No newline at end of file diff --git a/2.4栈的应用.md b/2.4栈的应用.md new file mode 100644 index 0000000..503405d --- /dev/null +++ b/2.4栈的应用.md @@ -0,0 +1,63 @@ +## 栈的应用 + +### 一、表达式求值(中缀表达式求值) + +三种表达式:`中缀表达式`、`后缀表达式`、`前缀表达式` + +中缀表达式:有界限符 + +后缀和前缀表达式:无界限符 + +`注`:方法一和方法二是分开的,方法三是中缀转后缀和后缀计算同时进行,一般用方法三解决 + +#### 方法一:中缀表达式转后缀表达式,再用后缀表达式求值 + +##### 1.中缀表达式转后缀表达式(用`栈`保存`运算符`) + +中缀表达式`从左往右`扫描: + +①遇到`操作数`,直接压入栈。 + +②遇到`界限符`,遇到“(”,直接压入栈;遇到“)”,依次弹出栈内运算符`从左往右`加入表达式,直到弹出“(”结束。 + +③遇到`运算符`,依次弹出比这个运算符优先级高的所有运算符,将此运算符压入栈。 + +##### 2.后缀表达式求值(用`栈`存`运算结果`) + +后缀表达式`从左往右`扫描: + +①遇到`操作数`,直接压入栈。 + +②遇到`运算符`,弹出两个元素,执行相应运算,将运算结果压回栈顶 + +#### 方法二:中缀表达式转前缀表达式,再用前缀表达式求值 + +##### 1.中缀表达式转前缀表达式(用`栈`保存`运算符`) + +中缀表达式`从右往左`扫描: + +①遇到`操作数`,直接压入栈。 + +②遇到`界限符`,遇到“(”,直接压入栈;遇到“)”,依次弹出栈内运算符`从右往左`加入表达式,直到弹出“(”结束。 + +③遇到`运算符`,依次弹出比这个运算符优先级高的所有运算符,将此运算符压入栈。 + +##### 2.前缀表达式求值(用`栈`存`运算结果`) + +前缀表达式`从右往左`扫描: + +①遇到`操作数`,直接压入栈。 + +②遇到`运算符`,弹出两个元素,执行相应运算,将运算结果压回栈顶 + +#### 方法三:中缀转后缀和后缀计算同时进行(两个栈) + +用`两个栈`:分别存`操作数`和`运算符` + +①遇到`操作数`,直接压入操作数栈。 + +②遇到`运算符`或`界限符`,按“中缀转后缀”的逻辑压入运算符栈,每当弹出一个运算符时,相应地弹出两个操作数,执行相应运算,将运算结果压回操作数栈 + +### 二、递归 + +递归的背后是栈的应用 \ No newline at end of file diff --git a/3.1队列.md b/3.1队列.md new file mode 100644 index 0000000..8b8ac10 --- /dev/null +++ b/3.1队列.md @@ -0,0 +1,36 @@ +## 队列—— Queue + +### 一、队列的定义 + +`队列`是只允许在一端进行插入,在另一端进行删除的线性表(`先进先出`,`后进后出`) + +![img](http://c.biancheng.net/cpp/uploads/allimg/140713/1-140G31P93Y96.jpg) + +### 二、队列的基本操作 + +`注`:参数代“&”表示:方法运行完后,对参数修改的结果要`“带回来”` + +对数据的操作:创销,增删查改 + +```c +InitQueue(&Q); //初始化队列:构造一个空队列Q,分配内存空间 +DestoryQueue(&Q); //销毁操作:销毁队列,并释放队列Q所占用的内存空间 + +EnQueue(&Q,x); //入队,若队列Q未满,则将x加入使之成为新的队尾 +DeQueue(&Q,&x); //出队,若队列Q非空,则删除队头元素,并用x返回 + +GetHead(Q,&x); //读队头元素,若队列Q非空,则将x返回队头元素 + +//其它常用操作 +QueueEmpty(Q); //判空操作 +``` + +### 三、存储结构 + +`顺序存储`和`链式存储` + +### 四、队列分类 + +队列的顺序存储:顺序队列 + +队列的链式存储:链式队列 \ No newline at end of file diff --git a/3.2顺序队列(循环队列)——队列的顺序存储.md b/3.2顺序队列(循环队列)——队列的顺序存储.md new file mode 100644 index 0000000..7de1c15 --- /dev/null +++ b/3.2顺序队列(循环队列)——队列的顺序存储.md @@ -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 diff --git a/3.3链队列——队列的链式存储.md b/3.3链队列——队列的链式存储.md new file mode 100644 index 0000000..059a95f --- /dev/null +++ b/3.3链队列——队列的链式存储.md @@ -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; +} +``` + +### 五、队列已满 + +链队一般不会队满 \ No newline at end of file diff --git a/3.4双端队列——队列的改进.md b/3.4双端队列——队列的改进.md new file mode 100644 index 0000000..b152b6a --- /dev/null +++ b/3.4双端队列——队列的改进.md @@ -0,0 +1,20 @@ +## 双端队列—— Double-ended Queue + +### 一、双端队列的定义 + +`线性表`:任何位置插入删除 + +栈、队列、双端队列都是只能两端插入删除的线性表 + +`栈`:一端插入删除 + +`共享栈`:栈的变种,两端为栈底,中间为栈顶,只能两端向中间插入删除 + +`队列`:一端插入,另一端删除 + +`双端队列`:两端都可插入删除 + +`输入受限的双端队列`:一端插入,两端删除 + +`输出受限的双端队列`:两端插入,一端删除 + diff --git a/3.5队列的应用.md b/3.5队列的应用.md new file mode 100644 index 0000000..0a18f89 --- /dev/null +++ b/3.5队列的应用.md @@ -0,0 +1,11 @@ +## 队列的应用 + +### 一、树的层次遍历 + +### 二、图的广度优先遍历 + +### 三、操作系统中的应用 + +多个进程争抢有限的系统资源时,采用`先来先服务算法(FCFS)` + +例子:CPU资源分配;打印数据缓冲区 \ No newline at end of file diff --git a/3.6特殊矩阵的压缩存储.md b/3.6特殊矩阵的压缩存储.md new file mode 100644 index 0000000..ed20c81 --- /dev/null +++ b/3.6特殊矩阵的压缩存储.md @@ -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`,注:行列从1开始 + +| i(行) | j(列) | v(值) | +| ------- | ------- | ------- | +| 1 | 3 | 4 | +| 1 | 6 | 5 | +| 2 | 2 | 3 | + +`方法二:链式存储——十字链表法` + +![1637932175187](F:\408数据结构\图片\1637932175187.png) diff --git a/4.1串.md b/4.1串.md new file mode 100644 index 0000000..529a837 --- /dev/null +++ b/4.1串.md @@ -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;若SS.length) return false; + for(int i=pos; iT,则返回值>0;若S=T,则返回值=0;若ST.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] + +![img](https://images0.cnblogs.com/blog/416010/201308/17084056-66930855432b4357bafbf8d6c76c1840.png) + +```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]; + } + } +} +``` + diff --git a/5.1树.md b/5.1树.md new file mode 100644 index 0000000..e44fe19 --- /dev/null +++ b/5.1树.md @@ -0,0 +1,61 @@ +## 树 —— Tree + +### 一、树的定义: + +`树`是η(n≥0)个结点的有限集合,n≡o时,称为空树,这是一种特殊情况。在任意一棵非空树中应满足: +①有且仅有一个特定的称为根的`结点`。 +②当n>1时,其余结点可分为m(m>0)个互不相交的有限集合T1,T2…,Tn,其中每个集合本身又是一棵树,并且称为根结点的`子树`。 + +![img](https://img-blog.csdn.net/20180801094313847?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpZXJtaW5nX18=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) + +### 二、树的术语 + +`父结点`:若一个结点含有子结点,则这个结点称为其子结点的父结点 +`子结点`:一个结点含有的子树的根结点称为该结点的子结点 +`兄弟结点`:拥有共同父结点的结点互称为兄弟结点 +`祖先`:对任意结点x,从根结点到结点x的所有结点都是x的祖先(结点x也是自己的祖先) +`后代`:对任意结点x,从结点x到叶子结点的所有结点都是x的后代(结点x也是自己的后代) + +`两结点的路径`:对任意结点x,从结点x到结点y的`从上到下`的路 +`两结点的路径长度`:对任意结点x,从结点x到结点y经过的边数 + +`结点的层次`:对任意结点x,从根结点到结点x的`从上到下`经过的边数 +`结点高度`:对任意结点x,叶子结点到x结点的路径长度就是结点x的`高度` +`树的深度`:一棵树中结点的最大深度就是树的深度,也称为`高度` + +`结点的度`:有几个子结点(分支) +`树的度`:各结点的度的最大值 +`叶子结点`:度为零的结点就是叶子结点 + +`森林`:m颗互不相交的树构成的集合就是森林 + +### 三、树的分类 + +有序树和无序树 + +`有序树`一一逻辑上看,树中结点的各子树从左至右是有次序的,不能互换 + +![image-20211108105809688](C:\Users\Dell\AppData\Roaming\Typora\typora-user-images\image-20211108105809688.png) + +`无序树`一一逻辑上看,树中结点的各子树从左至右是无次序的,可以互换 + +![image-20211108105824182](C:\Users\Dell\AppData\Roaming\Typora\typora-user-images\image-20211108105824182.png) + +### 四、树的性质 + +考点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$$ diff --git a/5.2二叉树.md b/5.2二叉树.md new file mode 100644 index 0000000..3b24098 --- /dev/null +++ b/5.2二叉树.md @@ -0,0 +1,277 @@ +## 二叉树 —— Binary Tree + +### 一、二叉树的定义: + +`二叉树`是n(n≥0)个结点的有限集合 +①或者为空二叉树,即n=0。 +②或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树 +特点:①每个结点至多只有两棵子树 ②左右子树不能颠倒(二叉树是有序树) + +![二叉树示意图](http://data.biancheng.net/uploads/allimg/181226/2-1Q226195I0M1.gif) + +### 二、二叉树的五种状态 + +![img](https://upload-images.jianshu.io/upload_images/10852306-cecc753794d38154.png?imageMogr2/auto-orient/strip|imageView2/2/w/337/format/webp) + +### 三、特殊的二叉树 + +`满二叉树`:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。 + +`特点`: +①只有最后一层有叶子结点 +②不存在度为1的结点 +③按层序从1开始编号,结点i的左孩子为2i,右孩子为2+1;结点i的父节点为i/2向下取整(如果有的话) + +![满二叉树示意图](http://data.biancheng.net/uploads/allimg/181226/2-1Q226195949495.gif) + +`完全二叉树`:当且仅当每个结点都与相同高度的满二叉树的编号一一对应 + +特点: +①只有最后两层可能有叶子结点 +②最多只有一个度为1的结点 +③按层序从1开始编号,结点i的左孩子为2i,右孩子为2+1;结点i的父节点为i/2向下取整(如果有的话) +④i ≤ n/2向下取整为分支结点,i ≥ n/2向下取整为叶子结点 + +![完全二叉树示意图](http://data.biancheng.net/uploads/allimg/181226/2-1Q22620003J18.gif) + +`二叉排序树`:一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:左子树上所有结点的关键字均小于根结点的关键字;右子树上的所有结点的关键字均大于根结点的关键字。 + +![二叉排序树](https://bkimg.cdn.bcebos.com/pic/94cad1c8a786c9179df9bed6c93d70cf3ac75763?x-bce-process=image/resize,m_lfit,w_536,limit_1/format,f_jpg) + +`平衡二叉树`:树上任一结点的左子树和右子树的深度之差不超过1。 + +![img](https://img0.baidu.com/it/u=1329084275,842118972&fm=26&fmt=auto) + +### 四、二叉树的性质 + +几个重要常考的基本操作: + +- i的左孩子——2i + +- i的右孩子——2i+1 +- i的父节点——i/2向下取整 +- i所在的层次 ——log2(n+1)向上取整 或 log2n向下取整+1 + +若`完全二叉树`中共有n个结点(非完全二叉树不行),则 + +- 判断i是否有左孩子?——2i≤n则有 +- ·判断是否有右孩子?——2i+1n/2向下取整是叶子结点,idata = {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) + +#### 代码:(用二叉树的链式存储) + +![img](https://pic2.zhimg.com/80/v2-55f42c78e51c0de9d1b942278ee1cfc1_720w.jpg) + +先序遍历:每个结点都会被路过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; + } +} +``` + +### 九、二叉树的层次遍历(层序遍历) + +![img](https://pic4.zhimg.com/80/v2-e9eb296238feba1867dc2b7b6deec257_720w.jpg) + +算法思想: +①初始化一个辅助队列(链队列) +②根结点入队 +③若队列非空,则队头结点出队,访问该结点并将其左、右孩子插入队尾(如果有的话) +④重复③直至队列为空 + +#### 链队列 + +```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.层序+中序遍历序列 + +关键是有非中序确定根结点是谁,再将根结点代入中序得左右子树,依次类推。 diff --git a/5.3线索二叉树.md b/5.3线索二叉树.md new file mode 100644 index 0000000..a6e6885 --- /dev/null +++ b/5.3线索二叉树.md @@ -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为左孩子` diff --git a/5.4树的存储结构.md b/5.4树的存储结构.md new file mode 100644 index 0000000..a793597 --- /dev/null +++ b/5.4树的存储结构.md @@ -0,0 +1,136 @@ +## 树的存储结构 + +### 一、存储结构 + +`顺序存储`和`链式存储` + +方法: +`双亲表示法`(顺序存储) +`孩子表示法`(顺序+链式存储) +`孩子兄弟表示法`(链式存储) + +### 二、双亲表示法(顺序存储) + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20200205124743184.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dteTAyMTdf,size_16,color_FFFFFF,t_70) + +双亲表示法:`顺序存储`结点数据,结点中保存父结点在数组中的下标 + +`优点`:找父节点方便。 +`缺点`:找孩子不方便。 + +`注`:双亲表示法与二叉树的顺序存储不一样,双亲表示法也可表示二叉树 + +#### 类型描述 + +`结点`包括`数据`和`父亲下标`, +`树`包括`结点数组`和`结点个数` + +```c +#define MAX_TREE_SIZE 100 //树中最多结点数 +typedef struct{ //树的结点定义 + ElemType data; //数据元素 + int parent; //双亲位置域 +}PTNode; +typedef struct{ //树的类型定义 + PTNode nodes[MAX_TREE_SIZE]; //双亲表示 + int n; //结点数 +}PTree; +``` + +#### 增加一个结点 + +新增元素,无需按逻辑次序存储,可以放到删除结点留下的存储空间里 + +#### 删除一个结点 + +方案一:数据取出,双亲指针改为-1 + +方案二:用存储空间中最后一个存的结点把要删的结点覆盖 + +#### 查找一个结点 + +找父结点方便、找孩子不方便。 + +空数据导致遍历慢。 + +### 二、孩子表示法(顺序+链式存储) + +![在这里插入图片描述](https://img-blog.csdnimg.cn/2020020513261515.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dteTAyMTdf,size_16,color_FFFFFF,t_70) + +孩子表示法:`顺序存储`结点数据,结点中保存孩子`链表`头指针(`链式存储`) + +`优点`:找孩子方便。 +`缺点`:找父节点不方便。 + +#### 类型描述 + +`孩子结点`包括`孩子下标`和`下一个孩子指针`, +`数组`包括`数据`和`孩子结点`, +`树`包括`数组`和`数组元素(结点)个数`及`根的下标` + +```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; +``` + +#### 增加一个结点 + +新增元素,父结点后新增一个孩子结点,数组中加一个数组元素 + +#### 删除一个结点 + +父结点后的链表中将此结点删除 + +数组中: +①若此结点后无链表,则直接删除 +②若此结点后有链表,再处理子树 + +#### 查找一个结点 + +按图一行一行遍历 + +找孩子结点方便,找父结点不方便 + +### 三、孩子兄弟表示法(顺序+链式存储) + +![点击查看源网页](https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fwww.pianshen.com%2Fimages%2F453%2Fcc517c2353eb55fee933453aec89c3d5.png&refer=http%3A%2F%2Fwww.pianshen.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1639041091&t=4bf03e7d98ff4795bf1faac99745914e) + +孩子兄弟表示法:用`二叉链表`存储`树`——`两个指针`:`第一个孩子`和`右兄弟` + +用此方法存储的树,形态上和`二叉树`类似 + +#### 类型描述 + +由二叉树的链式存储(二叉链表)改变而来 + +```c +typedef struct CSNode{ + Elemtype data; //数据域 + struct CSTNode *firstchild, *nextsibling; //第一个孩子和右兄弟指针 +}CSTNode, *CSTree; +``` + +#### 应用:树和二叉树的转换 + +![1637759667705](F:\408数据结构\图片\1637759667705.png) + +### 四、森林和二叉树的转换 + +本质:用`二叉链表`存储`森林` + +将森林的根结点连起来,视为兄弟关系 + +![1637759667694](F:\408数据结构\图片\1637759667694.png) + +![1637759667683](F:\408数据结构\图片\1637759667683.jpg) diff --git a/5.5二叉排序树.md b/5.5二叉排序树.md new file mode 100644 index 0000000..a53d431 --- /dev/null +++ b/5.5二叉排序树.md @@ -0,0 +1,200 @@ +## 二叉排序树——Binary Search Tree + +### 一、二叉排序树的定义 + +`二叉排序树`,又称`二叉查找树`(`BST`, `Binary Search Tree`) + +定义:一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树: +`左子树`上所有结点的关键字均`小于` `根结点`的关键字; +`右子树`上所有结点的关键字均`大于` `根结点`的关键字。 +左子树和右子树又各是一棵二叉排序树。 + +`左子树结点值<根结点值<右子树结点值` +进行`中序遍历`,可以得到一个`递增的有序序列` + +`作用`:元素的有序组织、`搜索`。 + +![二叉排序树](https://bkimg.cdn.bcebos.com/pic/94cad1c8a786c9179df9bed6c93d70cf3ac75763?x-bce-process=image/resize,m_lfit,w_536,limit_1/format,f_jpg) + +### 二、二叉排序树的存储结构(用链式存储) + +二叉排序树的类型表述(与二叉树一样) + +```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或(x,y)。 +Neighbors(G,x); //列出图G中与结点x邻接的边。 +InsertVertex(G,x); //在图G中插入顶点x。 +DeleteVertex(G,x); //从图G中删除顶点x。 +AddEdge(G,x,y); //若无向边(x,y)或有向边不存在,则向图G中添加该边。 +RemoveEdge(G,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)或对应的权值。 +Set_edge_value(G,x,y,v); //设置图G中边(x,y)或对应的权值为v。 +``` + +图的遍历中直接调用FirstNeighbor(G,x);和NextNeighbor(G,x,y); \ No newline at end of file diff --git a/6.4图的遍历(BFS、DFS).md b/6.4图的遍历(BFS、DFS).md new file mode 100644 index 0000000..a6e8ecd --- /dev/null +++ b/6.4图的遍历(BFS、DFS).md @@ -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=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=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 diff --git a/6.5最小生成树(Prim算法、Kruskal算法).md b/6.5最小生成树(Prim算法、Kruskal算法).md new file mode 100644 index 0000000..a267841 --- /dev/null +++ b/6.5最小生成树(Prim算法、Kruskal算法).md @@ -0,0 +1,47 @@ +## 最小生成树 + +### 一、最小生成树的概念 + +连通图生成树,非连通图生成森林 + +`生成树`是包含图中全部顶点的一个`极小连通子图` +`特性`:图中有n个顶点,则它的生成树含有n-1条边。 +去除一条边会变成非连通图;加上一条边会变成一个回路 + +之前学过`广度优先生成树`和`深度优先生成树`。 + +`最小生成树`,也叫`最小代价树`。在带权连通无向图的所有生成树中,所有边的代价和最小。 + +最小生成树可能很多,但边的权值之和总是唯一且最小的。 + +`最小生成的边=顶点数-1` + +![这里写图片描述](https://img-blog.csdn.net/20160714130435508) + +### 二、Prim算法(普利姆算法) + +此算法可以称为`“加点法”`,每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。 + +实现思想:以最低代价加入`点`。 +用两个数组: +①`isJoin数组`:标记`各结点`是否已加入树。 +②`lowCost数组`:各节点加入树的最低代价。 + +每轮遍历**isJoin**数组,第一遍找到**lowCost**最低的顶点,然后加入;第二遍循环遍历更新各点的**lowCost**值。 +则`时间复杂度`=$$O(|V|^2)$$,适合`边稠密图` + +![1638175026761](F:\408数据结构\图片\1638175026761.png) + +![1638175026757](F:\408数据结构\图片\1638175026757.png) + +### 三、Kruskal算法(克鲁斯卡尔算法) + +此算法可以称为`“加边法”`,初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。 + +实现思想:以最低代价加入`边`。 +用一个表: +(weight(边权),Vertex1(点1),Vertex2(点2)) +用`并查集`检查下一个边的两个点是否已连接。 + +判断两个点是否已连接需要$$O(log_2|E|)$$,工执行e轮。 +则`时间复杂度`=$$O(|E|log_2|E|)$$,适合`边稀疏图` diff --git a/6.6最短路径(BFS、Dijkstra算法、Floyd算法).md b/6.6最短路径(BFS、Dijkstra算法、Floyd算法).md new file mode 100644 index 0000000..2857aa7 --- /dev/null +++ b/6.6最短路径(BFS、Dijkstra算法、Floyd算法).md @@ -0,0 +1,123 @@ +## 最短路径 + +![1638172106501](F:\408数据结构\图片\1638172106501.png) + +![1638178148141](F:\408数据结构\图片\1638178148141.png) + +### 一、BFS算法(无权图) + +由`广度优先算法`求最短路径: + +```c +//图的广度优先搜索(用图的邻接矩阵、领接表都可以,只是FirstNeighbor和NextNeighbor函数实现不一样) +bool visited[MAX_VERTEX_NUM]; //访问标记数组 +void BFSTraverse(Graph G){ //对图G进行广度优先搜索 + for(v=0; v=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=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算法的局限性:不适用负权值带权图`。 + +![1638175026752](F:\408数据结构\图片\1638175026752.png) + +![1638175026747](F:\408数据结构\图片\1638175026747.png) + +### 三、Floyd算法(弗洛伊德算法)(带权图、无权图) + +实现方式: +用两个二维数组: +①`A数组`:记录各顶点之间目前的最短路径长度 +②`path数组`:记录两点之间的第一个中转点。 + +以某一点为中转点遍历二维数组,更新两个数组,将所有点作为中转点都遍历一遍,形成三重循环 +则`时间复杂度`=$$O(|V|^3)$$,`空间复杂度`=$$O(|V|^2)$$。 + +`Floyd算法的局限性:不适用负权值带权图`。 + +```c +//省略初始化A和path数组 +//Floyd算法的核心 +for(int k=0; k A[i][k] + A[k][j]){ //以Vk作为中转点的路径更短 + A[i][j] = A[i][k] + A[k][j]; //更新最短路径长度 + path[i][j] = k; //中转点 + } + } + } +} +``` + diff --git a/6.7有向无环图.md b/6.7有向无环图.md new file mode 100644 index 0000000..7dd4dfe --- /dev/null +++ b/6.7有向无环图.md @@ -0,0 +1,11 @@ +## 有向无环图——Directed Acyclic Graph + +### 一、有向无环图定义 + +`有向无环图`:有向图中不存在环,又称`DAG图`。 + +### 二、有向无环图描述表达式 + +由表达式画`最少顶点`的有向无环图: + +![1638191188272](F:\408数据结构\图片\1638191188272.png) \ No newline at end of file diff --git a/6.8拓扑排序.md b/6.8拓扑排序.md new file mode 100644 index 0000000..8581e66 --- /dev/null +++ b/6.8拓扑排序.md @@ -0,0 +1,80 @@ +## 拓扑排序 + +### 一、AOV网 + +`AOV网`(Activity On Vertex NetWork,用`顶点表示活动`的网): +用DAG图(`有向无环图`)表示一个工程。 +`顶点表示活动`,边$$$$表示活动$$V_i$$必须先于活动$$V_j$$进行 + +![uTools_1638194953695](F:\408数据结构\图片\uTools_1638194953695.png) + +### 二、拓扑排序 + +有向无环图中当且仅当满足下列条件时,称为该图的一个拓扑排序: +①每个顶点出现且只出现一次 +②若顶点A在序列中排在B的前面,则在图中不存在从B到A的路径 + +每个AOV网有多个拓扑排序序列。 + +`拓扑排序:找到做事的先后顺序。` + +拓扑排序的实现: +①从AOV网中选择一个没有前驱(入度为0)的顶点并输出。 +② 从网中删除该顶点和所有以它为起点的有向边。 +③ 重复①和②直到当前的AOV网为空或当前网中不存在无前驱的顶点为止。 + +#### 图的类型描述: + +![1637934445539](F:\408数据结构\图片\1637934445539.jpg) + +```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; inextarc){//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; //拓扑排序成功 +} +``` + +![1638194223550](F:\408数据结构\图片\1638194223550.jpg) + +![1638194223556](F:\408数据结构\图片\1638194223556.png) \ No newline at end of file diff --git a/6.9关键路径.md b/6.9关键路径.md new file mode 100644 index 0000000..7fd7e33 --- /dev/null +++ b/6.9关键路径.md @@ -0,0 +1,45 @@ +## 关键路径 + +### 一、AOE网 + +`AOE网`(Activity On Edge NetWork,用`边表示活动`的网): +`带权有向图`中,`顶点表示事件`,`有向边表示活动`,`边上的权值表示完成该活动的开销`。 + +![uTools_1638194800008](F:\408数据结构\图片\uTools_1638194800008.png) + +AOE网具有以下两个性质: +①只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始; +② 只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生。 +另外,有些活动是可以并行进行的 + +在AOE网中仅有一个入度为0的顶点,称为`开始顶点(源点)`,它表示整个工程的开始; +也仅有一个出度为0的顶点,称为`结束顶点(汇点)`,它表示整个工程的结束。 + +### 二、关键路径 + +`从源点到汇点`的`有向路径`可能有多条,所有路径中,具有`最大路径长度`的路径称为 +`关键路径`,而把`关键路径上的活动`称为`关键活动` + +`特性`: +若关键活动耗时增加,则整个工程的工期将增长 +缩短关键活动的时间,可以缩短整个工程的工期 +当缩短到一定程度时,关键活动可能会变成非关键活动 + +可能有多条关键路径,只提高一条关键路径上的关键活动速度并不能缩短整个工程的工期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短工期的目的。 + +`计算`: +事件最早、最迟发生时间 +活动最早、最迟开始时间 +活动的时间余量=活动最迟开始-最早开始 + +![uTools_1638195067535](F:\408数据结构\图片\uTools_1638195067535.png) + +![uTools_1638195535394](F:\408数据结构\图片\uTools_1638195535394.png) + +![uTools_1638195638068](F:\408数据结构\图片\uTools_1638195638068.png) + +![uTools_1638195751296](F:\408数据结构\图片\uTools_1638195751296.png) + +![uTools_1638195723961](F:\408数据结构\图片\uTools_1638195723961.png) + +![uTools_1638195438535](F:\408数据结构\图片\uTools_1638195438535.png) \ No newline at end of file diff --git a/7.1查找.md b/7.1查找.md new file mode 100644 index 0000000..e1b58f5 --- /dev/null +++ b/7.1查找.md @@ -0,0 +1,33 @@ +## 查找——Search + +### 一、查找的基本概念 + +`查找`:在数据集合中寻找满足某种条件的数据元素的过程称为查找。 +`查找表`(查找结构):用于查找的数据集合称为查找表,它由同一类型的数据元素(或记录)组成。 +`关键字`:数据元素中唯一标识该元素的某个数据项的值,使用基于关键字的查找,查找结果应该是唯一的。 + +![uTools_1638241191170](F:\408数据结构\图片\uTools_1638241191170.png) + +![uTools_1638241230449](F:\408数据结构\图片\uTools_1638241230449.png) + +### 二、对查找表的常见操作 + +查找和插入、删除 + +`静态查找表`只关注`查找速度`,`动态查找表`既关注`查找速度`又关注`插入删除是否方便`。 + +### 三、查找算法的评价指标 + +`查找长度`:对比关键字的次数 + +`平均查找长度(ASL)`:对比关键字次数的平均值。 + +$$ASL=\sum_{i=1}^{n}{P_iC_i}$$——查找概率×查找长度的总和 + +`ASL反映了查找算法的时间复杂度。` + +#### 查找成功和查找失败的平均查找长度: + +![1638242765366](F:\408数据结构\图片\1638242765366.jpg) + +![1638242765362](F:\408数据结构\图片\1638242765362.jpg) \ No newline at end of file diff --git a/7.2顺序查找.md b/7.2顺序查找.md new file mode 100644 index 0000000..a77e0fc --- /dev/null +++ b/7.2顺序查找.md @@ -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 key){ + high = mid - 1; //从前半部分继续查 + }else{ + low = mid + 1; //从后半部分继续查 + } + } + return -1; //查找失败,返回-1 +} +``` + +### 三、查找效率分析 + +![uTools_1638256115837](F:\408数据结构\图片\uTools_1638256115837.png) + +### 四、折半查找判定树的构造 + +#### 构造: + +![uTools_1638256209356](F:\408数据结构\图片\uTools_1638256209356.png) + +![uTools_1638256305668](F:\408数据结构\图片\uTools_1638256305668.png) + +![uTools_1638256324365](F:\408数据结构\图片\uTools_1638256324365.png) + +#### 特性: + +![uTools_1638256454841](F:\408数据结构\图片\uTools_1638256454841.png) + +查找表有n个关键字,则失败结点有n+1个 + +![uTools_1638256551359](F:\408数据结构\图片\uTools_1638256551359.png) + +与`折半查找判定树`的高度h有关。高度越小,查找效率越高 + +最好情况,平均查找长度=$$O(log_2n)$$ + +最坏情况,平均查找长度=$$O(n)$$ + +则`时间复杂度`=$$O(log_2n)$$ \ No newline at end of file diff --git a/7.4分块查找.md b/7.4分块查找.md new file mode 100644 index 0000000..b89cac1 --- /dev/null +++ b/7.4分块查找.md @@ -0,0 +1,33 @@ +## 分块查找——Block Search + +### 一、分块查找的定义 + +`分块查找`,又叫`索引顺序查找`。 + +### 二、折半查找的实现 + +`算法思想`:用一个`索引表`给数据归类。 + +算法过程: +①在`索引表`中确定待查记录所属的分块(`可顺序、可折半`) +②在`块内顺序查找` + +![uTools_1638257539381](F:\408数据结构\图片\uTools_1638257539381.png) + +#### 用折半查找索引表: + +若索引表中不包含目标关键字,则折半查找索引表`最终停在low>high`,要`在low所指分块中查找` + +### 三、查找效率分析 + +![uTools_1638258212238](F:\408数据结构\图片\uTools_1638258212238.png) + +![uTools_1638258279430](F:\408数据结构\图片\uTools_1638258279430.png) + +### 四、分块查找的优化 + +上面的分块查找对插入删除不友好。 + +改进:索引表为顺序表,查找表为链表。 + +![1638258852038](F:\408数据结构\图片\1638258852038.jpg) \ No newline at end of file diff --git a/7.5B树.md b/7.5B树.md new file mode 100644 index 0000000..33a265c --- /dev/null +++ b/7.5B树.md @@ -0,0 +1,104 @@ +## B树——B-Tree + +![uTools_1638266668727](F:\408数据结构\图片\uTools_1638266668727.png) + +### 一、B树的定义 + +`B树`,又名`多路平衡查找树`(`m叉查找树`) + +数据库索引技术里大量使用者B树和B+树的数据结构. + +B树是由`二叉排序树`升级为`m叉查找树` + +![uTools_1638263634505](F:\408数据结构\图片\uTools_1638263634505.png) + +![uTools_1638263678360](F:\408数据结构\图片\uTools_1638263678360.png) + +![1638263378425](F:\408数据结构\图片\1638263378425.png) + +#### 核心特征 + +![uTools_1638263546216](F:\408数据结构\图片\uTools_1638263546216.png) + +### 二、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叉查找树 + +![uTools_1638262837463](F:\408数据结构\图片\uTools_1638262837463.png) + +### 三、查找效率分析 + +#### 保证查找效率: + +每个结点的关键字太少,导致树变高,查找效率就会下降。 + +![uTools_1638262966104](F:\408数据结构\图片\uTools_1638262966104.png) + +![uTools_1638263106933](F:\408数据结构\图片\uTools_1638263106933.png) + +#### B树的高度 + +最小高度: + +![uTools_1638263978037](F:\408数据结构\图片\uTools_1638263978037.png) + +最大高度: + +![uTools_1638264247752](F:\408数据结构\图片\uTools_1638264247752.png) + +综上,高度为: + +![uTools_1638264294898](F:\408数据结构\图片\uTools_1638264294898.png) + +### 四、B树的插入 + +插入满一个结点后,从中间拆开,让中间位置($$\lceil m/2 \rceil$$)向上产生父结点,两边成为其孩子结点,依次类推 + +![B tree](https://images0.cnblogs.com/blog/94031/201403/290047064066682.png) + +![img](https://files.cnblogs.com/yangecnu/btreebuild.gif) + +### 五、B树的删除 + +情况一:删除`非终端结点`中的关键字 + +![1638265769766](F:\408数据结构\图片\1638265769766.png) + +![1638265769770](F:\408数据结构\图片\1638265769770.png) + +情况一:删除`终端结点`中的关键字 + +①删除33时右边结点够借: + +![1638265769762](F:\408数据结构\图片\1638265769762.png) + +![1638265769749](F:\408数据结构\图片\1638265769749.jpg) + +![1638265769758](F:\408数据结构\图片\1638265769758.png) + +![1638265769753](F:\408数据结构\图片\1638265769753.png) + +②删除49时,右边的结点不够借: + +解决方法:将`父结点中夹的关键字`取下与左右孩子`合并`。 + +![1638265769745](F:\408数据结构\图片\1638265769745.jpg) + diff --git a/7.6B+树.md b/7.6B+树.md new file mode 100644 index 0000000..e22af5d --- /dev/null +++ b/7.6B+树.md @@ -0,0 +1,40 @@ +## B+树——B-Plus-Tree + +### 一、B树的定义 + +`B+树`,又名`多级分块查找`。 + +数据库索引技术里大量使用者B树和B+树的数据结构. + +B+树是由`分块查找`升级为`查找树` + +B树任何一层都可以找到,因为每一层都是数据 +而B+树除叶子结点外,其余都是分类,只有最底层才会指向数据,必须找到最底层才能知道是否成功。 + +B+树有两种查找方式:①分块查找:从根结点查 ②顺序查找:从P开始横着查。 + +![uTools_1638279630033](F:\408数据结构\图片\uTools_1638279630033.png) + +![uTools_1638279791118](F:\408数据结构\图片\uTools_1638279791118.png) + +### 二、B+树的实现 + +无 + +### 三、查找效率分析 + +与B树一样 + +### 四、B+树的插入 + +![B Plus tree](https://images0.cnblogs.com/blog/94031/201403/290050048129679.png) + +![img](https://files.cnblogs.com/yangecnu/Bplustreebuild.gif) + +### 五、B+树的删除 + +与B树一样 + +### 六、B树与B+树的对比 + +![1638280509471](F:\408数据结构\图片\1638280509471.jpg) diff --git a/7.7散列查找(哈希查找).md b/7.7散列查找(哈希查找).md new file mode 100644 index 0000000..2bdab15 --- /dev/null +++ b/7.7散列查找(哈希查找).md @@ -0,0 +1,113 @@ +## 散列查找(哈希查找)——Hash Search + +![uTools_1638349184596](F:\408数据结构\图片\uTools_1638349184596.png) + +### 一、哈希查找的定义 + +`散列表`(Hash Table),又名`哈希表`,是一种数据结构。 + +`特点`:数据元素的`关键字与其存储地址直接相关`。 + +通过`散列函数(哈希函数)`将关键字与存储地址一一映射。 + +散列查找是典型的`“用空间换时间”的算法` + +`装填因子α` = 表中记录个数/散列表表长 + +`查找效率`:取决于`散列函数`、`处理冲突的方法`、`装填因子α` + +![1638281466946](F:\408数据结构\图片\1638281466946.jpg) + +#### 处理冲突的方法——拉链法 + +![uTools_1638349467195](F:\408数据结构\图片\uTools_1638349467195.png) + +### 二、常见的散列函数(哈希函数) + +冲突是由散列函数导致的,`冲突越多,查找效率越低` + +散列函数的设计目的:让不同的关键字的冲突尽可能少。 + +①除留余数法 +②直接定址法 +③数字分析法 +④平方取中法 + +处理冲突的方法:以拉链法为主。 + +#### 1.除留余数法 + +$$H(key)=key\mod{p}$$ + +除数p取法:散列表表长为m,取一个不大于m但最接近或等于m的`质数p` + +查找方法:当p=13时,查找66,66%13=1,则在a[1]下的链表中寻找。 + +`查找效率分析`: + +![1638350479082](F:\408数据结构\图片\1638350479082.jpg) + +#### 2.直接定址法 + +$$H(key)=key$$ 或 $$H(key)=a*key+b$$ + +这种计算最简单,适合`关键字分布连续`的情况 + +![uTools_1638351041435](F:\408数据结构\图片\uTools_1638351041435.png) + +#### 3.数字分析法 + +选取数码`分布较为均匀的若干位`作为散列地址。 + +![1638351182580](F:\408数据结构\图片\1638351182580.png) + +#### 4.平方取中法 + +取`关键字的平方值的中间几位`作为散列地址。 + +具体取多少位要视实际情况而定。这种方法得到的`散列地址与关键字的每位都有关系`,因此使得 +散列地址分布比较均匀,适用于关键字的每位取值都不够均匀或均小于散列地址所需的位数。 + +![uTools_1638351362463](F:\408数据结构\图片\uTools_1638351362463.png) + +### 三、处理冲突的方法 + +①拉链法 +②开放定址法 +③再散列法 + +#### 1.拉链法 + +上面讲了 + +#### 2.开放定址法 + +![uTools_1638353226182](F:\408数据结构\图片\uTools_1638353226182.png) + +`d的不同取法`: + +##### ①线性探测法 + +![uTools_1638352055034](F:\408数据结构\图片\uTools_1638352055034.png) + +![uTools_1638352323225](F:\408数据结构\图片\uTools_1638352323225.png) + +![uTools_1638352359885](F:\408数据结构\图片\uTools_1638352359885.png) + +![uTools_1638352393458](F:\408数据结构\图片\uTools_1638352393458.png) + +##### ②平方探测法 + +散列表长必须是$$4j+3$$ + +![uTools_1638352677985](F:\408数据结构\图片\uTools_1638352677985.png) + +![1638352873925](F:\408数据结构\图片\1638352873925.png) + +##### ③伪随机序列法 + +d取随机值 + +#### 3.再散列法 + +![uTools_1638353359841](F:\408数据结构\图片\uTools_1638353359841.png) diff --git a/8.1排序.md b/8.1排序.md new file mode 100644 index 0000000..abcaa3c --- /dev/null +++ b/8.1排序.md @@ -0,0 +1,29 @@ +## 排序——Sort + +![1638363129298](F:\408数据结构\图片\1638363129298.png) + +### 一、排序的定义 + +`排序`,就是重新排列表中的元素,使表中的元素满足`按关键字有序`的过程。 + +![uTools_1638362274133](F:\408数据结构\图片\uTools_1638362274133.png) + +### 二、排序算法的应用 + +对数据进行大小排序 + +### 三、排序的算法评价指标 + +时间复杂度、空间复杂度、`稳定性` + +![uTools_1638362879122](F:\408数据结构\图片\uTools_1638362879122.png) + +### 四、排序算法的分类 + +![1638363129303](F:\408数据结构\图片\1638363129303.png) + +## 常见排序算法与其时间复杂度: + +![aHR0cHM6Ly9pbWFnZXMyMDE4LmNuYmxvZ3MuY29tL2Jsb2cvODQ5NTg5LzIwMTgwNC84NDk1ODktMjAxODA0MDIxMzI1MzAzNDItOTgwMTIxNDA5LnBuZw](F:\408数据结构\图片\aHR0cHM6Ly9pbWFnZXMyMDE4LmNuYmxvZ3MuY29tL2Jsb2cvODQ5NTg5LzIwMTgwNC84NDk1ODktMjAxODA0MDIxMzI1MzAzNDItOTgwMTIxNDA5LnBuZw.png) + +![20210408130104795](F:\408数据结构\图片\20210408130104795.png) \ No newline at end of file diff --git a/8.2插入排序(稳定).md b/8.2插入排序(稳定).md new file mode 100644 index 0000000..3e94e39 --- /dev/null +++ b/8.2插入排序(稳定).md @@ -0,0 +1,95 @@ +## 插入排序——Insertion Sort + +![uTools_1638366099368](F:\408数据结构\图片\uTools_1638366099368.png) + +### 一、算法思想: + +每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列中,直到全部记录插入完成。 + +![img](https://images2017.cnblogs.com/blog/849589/201710/849589-20171015225645277-1151100000.gif) + +### 二、代码实现: + +普通: + +```c +// 直接插入排序 +void InsertSort(int A[], int n){ + int i, j, temp; + for(i=1; i=0 && A[j]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[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)$$ \ No newline at end of file diff --git a/8.3希尔排序(不稳定).md b/8.3希尔排序(不稳定).md new file mode 100644 index 0000000..5569543 --- /dev/null +++ b/8.3希尔排序(不稳定).md @@ -0,0 +1,52 @@ +## 希尔排序——Shell's Sort + +![uTools_1638366303933](F:\408数据结构\图片\uTools_1638366303933.png) + +`希尔排序`又叫`缩小增量排序`。 + +1959年Shell发明,第一个突破$$O(n^2)$$的排序算法,是`简单插入排序的改进版`。它与插入排序的不同之处在于,它会`优先比较距离较远的元素`。 + +### 一、算法思想: + +先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述: + +- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1; +- 按增量序列个数k,对序列进行k 趟排序; +- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。 + +![img](https://images2018.cnblogs.com/blog/849589/201803/849589-20180331170017421-364506073.gif) + +### 二、代码实现: + +在简单插入排序外加了`步长变化` + +```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]0 && A[0]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)$$ + +算法稳定性:`稳定` + +顺序表和链表都可以。 \ No newline at end of file diff --git a/8.5快速排序(不稳定).md b/8.5快速排序(不稳定).md new file mode 100644 index 0000000..328f3e0 --- /dev/null +++ b/8.5快速排序(不稳定).md @@ -0,0 +1,62 @@ +## 快速排序——Quick Sort + +![uTools_1638450353573](F:\408数据结构\图片\uTools_1638450353573.png) + +![uTools_1638451646054](F:\408数据结构\图片\uTools_1638451646054.png) + +`快速排序`的基本思想:通过一趟排序将待排记录`分隔成独立的两部分`,其中一部分记录的关键字均比另一部分的关键字小,则`可分别对这两部分记录继续进行排序`,以达到整个序列有序。 + +`确定中间数的位置` + +### 一、算法思想: + +快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下: + +- 从数列中挑出一个元素,称为 “基准”(pivot); +- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作; +- 递归地(recursive)把小于基准值元素的子数列和大于 + +![img](https://images2017.cnblogs.com/blog/849589/201710/849589-20171015230936371-1413523412.gif) + +### 二、代码实现: + +将第一个作为枢轴,后面的元素与枢轴比较,一此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=pivot ) --high; + A[Low] = A[high]; //比枢轴小的元素移动到左端 + while(low=左、右。 + +小根堆:完全二叉树中,根<=左、右。 + +### 一、算法思想: + +- 将初始待排序关键字序列(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,则整个排序过程完成。 + +![img](https://images2017.cnblogs.com/blog/849589/201710/849589-20171015231308699-356134237.gif) + +### 二、代码实现: + +#### 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= 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)$$ + +算法稳定性:`不稳定` diff --git a/8.8归并排序(稳定).md b/8.8归并排序(稳定).md new file mode 100644 index 0000000..6904b65 --- /dev/null +++ b/8.8归并排序(稳定).md @@ -0,0 +1,63 @@ +## 归并排序——Merge Sort + +![uTools_1638535460811](F:\408数据结构\图片\uTools_1638535460811.png) + +`归并排序`是建立在归并操作上的一种有效的排序算法。该算法是采用`分治法`(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。 + +归并:把两个或多个已经有序的序列合并成一个。 + +m路归并:将m个有序的序列合并成一个,每选出一个元素需要对比关键字m-1次。 + +2路归并: + +![uTools_1638535201796](F:\408数据结构\图片\uTools_1638535201796.png) + +### 一、算法思想: + +- 把长度为n的输入序列分成两个长度为n/2的子序列; +- 对这两个子序列分别采用归并排序; +- 将两个排序好的子序列合并成一个最终的排序序列。 + +![img](https://images2017.cnblogs.com/blog/849589/201710/849589-20171015230557043-37375010.gif) + +### 二、代码实现: + +```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