目录
0.绪论
数据结构的三要素:逻辑结构、存储结构、数据运算
逻辑图:
逻辑结构:
其中:集合结构、树结构、图结构为非线性结构
存储(物理)结构
存储结构:顺序存储、链式存储、索引存储、散列存储
顺序存储:逻辑上相邻的元素存储在物理位置也相邻的存储单元中
链式存储:逻辑上相邻但物理位置上不要求相邻
索引存储:在存储元素信息的同时建立索引表(关键字,地址)
散列(哈希)存储:根据元素的关键字直接计算出元素的存储地址
数据的运算
施加在数据上的运算
算法和算法的评价
算法的5个基本要素:有穷性、确定性、可行性、输入、输出
算法的评价:时间复杂度和空间复杂度
时间复杂度:算法中所有语句被重复执行的次数之和的数量级,用T(n)表示,例如:
语句执行次数之和为an^3+bn^2+cn,则时间复杂度T(n)=O(n^3)
x=x+1; //时间复杂度为O(1),称为常数阶
for(i=1; i<=n; i++){ //时间复杂度为O(n),称为线性阶 x=x+1; }
for(i=1; i<=n; i++){ //时间复杂度为O(n2),称为平方阶 for(j=1; j<=n; j++){ x=x+1; } }
for ( i=1; i < n; i++ ) { y = y+1; //语句1 for ( j=0; j<=(2*n); j++ ) x++; //语句2 } //语句1频度为(n-1);语句2频度为(n-1)x(2n+1)=2n2-n-1,因此时间复杂度T(n)=2n2-2=O(n2)。
i=1; //语句1 while (i<=n){ i=i*2; //语句2 } //语句1频度为1;语句2频度需要进一步计算 假设while循环执行了x次,则当1*2*2*2...*2>n时结束循环,即2^x>n,则x=log2(n) 所以时间复杂度T(n)=log2(n)+1=O(logn)
常用的时间复杂度按照耗费的时间从小到大依次是:
O(1)<O(logn)<O(n)<O(nlogn)<O(n²)<O(n³)<O(2ⁿ)<O(n!)
空间复杂度:一个程序在执行过程中临时使用的存储空间的大小
1.线性表
定义:一个线性表是n个具有相同特性的数据元素的有限序列,线性表的元素仅限于原子项,原子是结构上不可分割的成分
1.1顺序存储结构:把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构,通常用数组来描述顺序存储结构,下同
分类:数组
//线性表的顺序存储 #include <stdio.h> #include <malloc.h> //malloc函数 #include <stdlib.h> //exit函数 typedef struct{ int *elem;//指向数组第一个元素的指针,也就是数组名 int length; //线性表当前长度 int listsize; //线性表最大长度 } SqList; //初始化 void initSqList(SqList *sq){ int num; printf("请输入线性表最大长度:"); scanf("%d",&num); sq->elem=(int *)malloc(num*sizeof(int));//为线性表分配地址 if(!(sq->elem)){ printf("系统分配内存空间失败!\n"); exit(-1); } sq->length=0; sq->listsize=num; return; } //在第i个位置插入数据e,这里SqList使用的是引用传递 void insertData(SqList *sq,int i,int e){ int j,temp; if(i>((sq->length)+1)||i<1){ printf("插入失败\n"); exit(-1); } for(j=sq->length;j>i-1;j--){ sq->elem[j]=sq->elem[j-1];//插入点之后的元素后移 } sq->elem[i-1]=e; (sq->length)++; } //遍历 void findAll(SqList L){ for(int i=0;i<L.length;i++){ printf("%d ",L.elem[i]); } printf("\n"); } //查找第i个元素的值,这里SqList使用的是值传递 int findById(SqList L,int i){ if(i<1||i>(L.length+1)){ printf("查找失败\n"); exit(-1); }else{ return L.elem[i-1]; } } //删除第i个元素,并返回删除的值 int delete(SqList *L,int i){ int j,temp; if(i<1||i>(L->length)){ printf("删除失败\n"); exit(-1); } temp=L->elem[i-1]; for(j=i-1;j<L->length-1;j++){ L->elem[j]=L->elem[j+1]; } L->length--; return temp; } //判断表是否为空 bool isEmpty(SqList L){ if(L.length==0){ return true; }else{ return false; } } //表置空 void clearList(SqList *L){ L->length=0; return ; } //销毁表 void Destory(SqList *L){ free(L); L->elem=NULL; L->length=0; L->listsize=0; } int main() { SqList L; initSqList(&L); insertData(&L,1,10); insertData(&L,2,9); insertData(&L,3,8); insertData(&L,4,7); insertData(&L,5,6); insertData(&L,5,55); insertData(&L,2,22); printf("length:%d\n",L.length); findAll(L); printf("%d\n",findById(L,2)); printf("%d\n",delete(&L,2)); findAll(L); return 0; }
小知识点:
值传递:值传递的特点是单向传递,即主调函数调用时给形参分配存储单元,把实参的值传递给形参,在调用结束后,形参的存储单元被释放,而形参值的任何变化都不会影响到实参的值,实参的存储单元仍保留并维持数值不变。
地址传递:这种方式使用数组名或者指针作为函数参数,传递的是该数组的首地址或指针的值。因此在数组名或指针作函数参数时所进行的传送只是地址传送,形参在取得该首地址之后,与实参共同拥有一段内存空间,形参的变化也就是实参的变化。
1.2链式存储结构:结点在存储器中的位置是随意的,即逻辑上相邻的数据元素在物理上不一定相邻(链表)分类:单(向)链表、单向循环链表、双(向)链表、双向循环链表
单链表:
优点:解决顺序表需要大量连续存储单元的缺点
缺点:不能直接找到表中某个特定的结点,需要从表头遍历查找
头节点的数据域可不设任何信息,头节点的指针域(也即头指针)指向第一个元素结点
不管带不带头结点,头指针始终指向链表的第一个结点;
这里要说一个困扰我很长时间的问题:任何指针的所占内存大小是固定的,如32位机器指针都是4个字节,所以假设我们的机器是32位系统,我们定义的链表结点中LinkList指针类型就是4个字节,而LNode结点类型就是8个字节(int占4个字节,struct LNode占4个字节)
#include <stdlib.h> #include <stdio.h> #include <stdbool.h> /* *typedef struct LNode LNode:定义新的LNode变量 LNode *typedef struct LNode *LinkList(typedef struct LNode* LinkList):定义新的指向LNode变量的指针 LinkList *可以用下面的去帮助理解 *typedef struct int ElemType:定义新的整型变量 ElemType *typedef struct int* ElemTypePtr:定义新的指向整型变量的指针 ElemTypePtr */ typedef struct LNode{ int data;//存放数据 struct LNode *next;//指向下一节点的指针(即存放后续结点的地址) }LNode,*LinkList; //单链表初始化,带头节点(头节点不保存数据) LinkList initList(){ int i; int length;//单链表有效节点个数 int val;//节点的值 LinkList pHead=(LinkList)malloc(sizeof(LNode));//pHead始终指向头节点,malloc函数分配成功后会返回一个指针对象(即地址),创建头结点 if(pHead==NULL){ printf("分配失败!\n"); exit(-1); }else{ LinkList pTail=pHead;//pTail始终指向尾接点 pTail->next=NULL; printf("请输入有效节点个数:");//有效节点个数不包含头节点 scanf("%d",&length); for(i=0;i<length;i++){ printf("请输入第%d个节点的值:",i+1); scanf("%d",&val); LNode *pNew=(LNode*)malloc(sizeof(LNode));//LinkList pNew=(LinkList)malloc(sizeof(LNode)),其中LinkList就等于LNode*,LinkList pNew等价于LNode *pNew pNew->data=val; pTail->next=pNew; pNew->next=NULL; pTail=pNew; } return pHead; } } //求链表的长度 int getLength(LinkList pHead){ int i; LinkList pTail; pTail=pHead; for(i=0;pTail->next!=NULL;i++){ pTail=pTail->next; } return i; } //判断链表是否为空 bool isEmpty(LinkList pHead){ if(pHead->next==NULL){ return true; }else{ return false; } } //销毁链表 void destory(LinkList pHead){ LinkList p; while(pHead!=NULL){ p=pHead->next; free(pHead); pHead=p; } printf("销毁完毕!\n"); } //在第i个位置插入元素e void insertData(LinkList pHead,int i,int e){ int j; int temp; LinkList pTail; LinkList pTemp; LinkList pt; pt=(LinkList)malloc(sizeof(LNode)); pt->data=e; pTail=pHead; for(j=0;j<i-1;j++){ pTail=pTail->next; } pTemp=pTail->next; pTail->next=pt; pt->next=pTemp; } //删除第i个位置的节点并返回其值 int deleteById(LinkList pHead,int i){ LinkList pTail; pTail=pHead; int j; int data; LinkList pM; for(j=0;j<i-1;j++){ pTail=pTail->next; } pM=pTail->next->next; data=pTail->next->data; pTail->next=pM; return data; } //遍历链表 void findAll(LinkList pHead){ int i; LinkList pTail; pTail=pHead; for(i=0;i<getLength(pHead);i++){ pTail=pTail->next; printf("第%d个元素的值为%d ",i+1,pTail->data); } } int findById(LinkList pHead,int i){ if(i>getLength(pHead)){ printf("超出范围!\n"); exit(-1); }else{ int j; LinkList pTail; pTail=pHead; for(j=0;j<i;j++){ pTail=pTail->next; } return pTail->data; } } int main() { LinkList pHead=initList(); findAll(pHead); printf("\n"); printf("第%d个位置的元素为%d\n",2,findById(pHead,2)); insertData(pHead,2,67); printf("插入元素后第%d个位置的元素为%d\n",2,findById(pHead,2)); printf("删除第%d个位置元素,其值为:%d\n",3,deleteById(pHead,3)); findAll(pHead); return 0; }
单向循环链表:
循环单链表(单向循环链表)与单链表的区别在于:表中最后一个结点的指针不是NULL,而改为指向头结点,从而整个链表形成一个环。
在循环单链表中可从表中任意一个结点处开始遍历整个链表
#include <stdlib.h> #include <stdio.h> #include <stdbool.h> typedef struct LNode{ int data;//存放数据 struct LNode *next;//指向下一节点的指针 }LNode,*LinkList;//结点和头指针 //单向循环链表初始化,带头节点(头节点不保存数据) LinkList initList(){ int i; int length;//单链表有效节点个数 int val;//节点的值 LinkList pHead=(LinkList)malloc(sizeof(LNode));//pHead始终指向头节点 if(pHead==NULL){ printf("分配失败!\n"); exit(-1); }else{ LinkList pTail=pHead;//pTail始终指向尾接点 pTail->next=NULL; printf("请输入有效节点个数:");//有效节点个数不包含头节点 scanf("%d",&length); for(i=0;i<length;i++){ printf("请输入第%d个节点的值:",i+1); scanf("%d",&val); LinkList pNew=(LinkList)malloc(sizeof(LNode)); pNew->data=val; pTail->next=pNew; pNew->next=NULL; pTail=pNew; } pTail->next=pHead; return pHead; } } void insert(LinkList pHead,int i,int data){ LinkList p,pNew,pTemp; p=pHead; pNew=(LinkList)malloc(sizeof(LNode)); pNew->data=data; int j; for(j=0;j<i-1;j++){ p=p->next; } pTemp=p->next; p->next=pNew; pNew->next=pTemp; } int deletebyId(LinkList pHead,int i){ LinkList p,pTemp; int data; p=pHead; for(int j=0;j<i-1;j++){ p=p->next; } pTemp=p->next; p->next=pTemp->next; data=pTemp->data; free(pTemp); return data; } //遍历链表 void findAll(LinkList pHead){ int i; LinkList pTail; pTail=pHead; for(i=0;pTail->next!=pHead;i++){ pTail=pTail->next; printf("%d ",pTail->data); } printf("\n"); } int getLength(LinkList pHead){ LinkList p=pHead; int i; for(i=0;p->next!=pHead;i++){ p=p->next; } return i; } void destory(LinkList pHead){ LinkList p=pHead->next,pTemp; while(p!=pHead){ pTemp=p->next; printf("%d ",p->data); free(p); p=pTemp; } free(pHead); printf("%d ",pHead->data); } bool isEmpty(LinkList pHead){ if(pHead->next==pHead){ return true; }else{ return false; } } void main(){ LinkList pHead=initList(); printf("length:%d\n",getLength(pHead)); findAll(pHead); insert(pHead,2,50); printf("length:%d\n",getLength(pHead)); findAll(pHead); deletebyId(pHead,1); printf("%s\n",isEmpty(pHead)==1?"是":"否"); findAll(pHead); destory(pHead); }
双链表:
#include <stdlib.h> #include <stdio.h> #include <stdbool.h> //定义双链表结点 typedef struct DNode{ int data;//数据域 struct DNode *prev;//前驱指针 struct DNode *next;//后继指针 }DNode,*DLinkList; //初始化双链表,带头结点 DLinkList initList(){ DLinkList p,pNew,pHead; pHead=(DLinkList)malloc(sizeof(DNode)); if(pHead==NULL){ printf("分配失败!\n"); exit(-1); }else{ int i,length,val; pHead->prev=NULL; pHead->next=NULL; p=pHead; printf("请输入有效节点个数:");//有效节点个数不包含头节点 scanf("%d",&length); for(i=0;i<length;i++){ printf("请输入第%d个节点的值:",i+1); scanf("%d",&val); pNew=(DLinkList)malloc(sizeof(DNode)); pNew->data=val; pNew->next=NULL; p->next=pNew; pNew->prev=p; p=pNew; } return pHead; } } //遍历双链表 void findAll(DLinkList phead){ DLinkList p; p=phead; p=p->next; while(p!=NULL){ printf("%d ",p->data); p=p->next; } printf("\n"); } //插入 void insert(DLinkList pHead,int i,int data){ DLinkList p,pNew; pNew=(DLinkList)malloc(sizeof(DNode)); pNew->data=data; p=pHead; int j; for(j=0;j<i-1;j++){ p=p->next; } pNew->next=p->next; p->next->prev=pNew; p->next=pNew; pNew->prev=p; } //删除 int deleteById(DLinkList pHead,int i){ DLinkList p; p=pHead; for(int j=0;j<i;j++){ p=p->next;//令p指向第i个节点 } if(p->next!=NULL){ p->prev->next=p->next; p->next->prev=p->prev; }else{ p->prev->next=NULL; } int data=p->data; free(p); return data; } //判空 bool isEmpty(DLinkList pHead){ DLinkList p=pHead->next; if(p==NULL){ return true; }else{ return false; } } //获取长度 int getLength(DLinkList pHead){ int i; DLinkList p=pHead; for(i=0;p->next!=NULL;i++){ p=p->next; } return i; } //销毁链表 void destory(DLinkList pHead){ DLinkList p; while(pHead!=NULL){ p=pHead->next; free(pHead); pHead=p; } } int main(){ DLinkList pHead=initList(); printf("链表是否为空?%s\n",isEmpty(pHead)==1?"是":"否"); printf("链表长度为:%d\n",getLength(pHead)); findAll(pHead); insert(pHead,2,67); findAll(pHead); printf("删除结点的值为:%d\n",deleteById(pHead,1)); findAll(pHead); }
int main(){ pNode pHead=initList(); printf("链表是否为空?%s\n",isEmpty(pHead)==1?"是":"否"); printf("链表长度为:%d\n",getLength(pHead)); findAll(pHead); printf("删除结点的值为:%d\n",deleteById(pHead,1)); printf("链表是否为空?%s\n",isEmpty(pHead)==1?"是":"否"); printf("链表长度为:%d\n",getLength(pHead)); findAll(pHead); }
双向循环链表:
一下俩种画法都对(前者没有画头指针,应该是有头指针的(如图二))
循环双链表(双向循环链表):与双链表的区别在于——头结点的前驱指针指向尾结点,尾结点的后继指针指向头结点
#include <stdlib.h> #include <stdio.h> #include <stdbool.h> //定义双链表结点 typedef struct Node{ int data;//数据域 struct Node *prev;//前驱指针 struct Node *next;//后继指针 }Node,*DoubleLinkList;//结点和头指针 //初始化双向循环链表,带头结点 DoubleLinkList initList(){ DoubleLinkList pHead,p,pNew; pHead=(DoubleLinkList)malloc(sizeof(Node)); if(pHead==NULL){ printf("分配失败!\n"); exit(-1); }else{ int i,length,val; p=pHead; printf("请输入有效节点个数:");//有效节点个数不包含头节点 scanf("%d",&length); for(i=0;i<length;i++){ printf("请输入第%d个节点的值:",i+1); scanf("%d",&val); pNew=(DoubleLinkList)malloc(sizeof(Node)); pNew->data=val; pNew->prev=p; pNew->next=pHead; p->next=pNew; pHead->prev=pNew; p=pNew; } return pHead; } } void insert(DoubleLinkList pHead,int i,int data){ DoubleLinkList p=pHead,pNew; pNew=(DoubleLinkList)malloc(sizeof(Node)); pNew->data=data; for(int j=0;j<i-1;j++){ p=p->next; } pNew->next=p->next; p->next->prev=pNew; p->next=pNew; pNew->prev=p; } int deletebyId(DoubleLinkList pHead,int i){ DoubleLinkList p=pHead; for(int j=0;j<i;j++){ p=p->next; } p->prev->next=p->next; p->next->prev=p->prev; int data=p->data; free(p); return data; } //遍历双向循环链表 void findAll(DoubleLinkList phead){ DoubleLinkList p; p=phead; p=p->next; while(p!=phead){ printf("%d ",p->data); p=p->next; } printf("\n"); } int getLength(DoubleLinkList pHead){ DoubleLinkList p=pHead->next; int i; for(i=0;p!=pHead;i++){ p=p->next; } return i; } void destory(DoubleLinkList pHead){ DoubleLinkList p=pHead->next,pTemp; while(p!=pHead){ pTemp=p->next; printf("%d ",p->data); free(p); p=pTemp; } printf("%d ",pHead->data); free(pHead); } bool isEmpty(DoubleLinkList pHead){ if(pHead->next==pHead){ return true; }else{ return false; } } void main(){ DoubleLinkList pHead=initList(); printf("length:%d\n",getLength(pHead)); findAll(pHead); insert(pHead,2,65); findAll(pHead); printf("%d\n",deletebyId(pHead,3)); findAll(pHead); printf("%s\n",isEmpty(pHead)==1?"是":"否"); destory(pHead); }
静态链表
静态链表:借助数组描述线性表链式结构,结点也有数据域和指针域,只是指针域是数组下标,需要预先分配一块连续的内存空间。
#define MaxSize 50 //静态链表的最大长度 typedef struct { int data; //存储的数据 int next; //下一元素的宿主下标,静态链表以next=-1作为结束的标志 }SLinkList[MaxSize];
顺序表和链表的比较:
存取方式:顺序表可以顺序存取,也可随机存取;链表只能从表头顺序存取。
逻辑与物理结构:顺序表逻辑上相邻的元素物理存储位置也相邻;链表逻辑上相邻的元素物理存储位置不一定相邻;
查找、插入和删除操作:按值查找(顺序表无序)两者时间复杂度均为O(n),(顺序表有序)采用折半查找顺序表的复杂度会降低为O(log以2为底(n));按序号查找,顺序表O(1),链表O(n)。插入/删除:顺序表平均移动半个表长长度,链表则只需修改指针域即可。
空间分配:顺序表在静态存储分配情况下不能扩充;动态分配情况下虽可扩充但需要移动大量的元素,效率低,且内存中没有特别大的连续存储空间,从而导致分配失败;链表则有空间即可随时分配。
如何选择?
基于存储的考虑:表长度不确定用链表;
基于运算的考虑:经常做按序号查找的操作用顺序表;经常做插入和删除操作用链表
几种常用算法设计技巧:
链表:头插法、尾插法、逆置法、归并法、双指针法等
逆置法图解(使用三个指针实现)
typedef struct LNode{ int data; struct LNode *next; }LNode,*LinkList; //迭代逆置法,head 为无头节点链表的头指针 LinkList reverseList(LinkList L) { if (L== NULL || L->next == NULL) return head;//如果是空链表则直接返回 else { LinkList prev = NULL; LinkList now = L; LinkList next = L->next; while (1){ now->next = prev;//now指针指向节点的指针域要与prev指针指向一样 if (next== NULL)//判断 next是否为 NULL,如果成立则已经找到原链表尾,退出循环 break; prev= now;// prev,now,next三个指针都向后移动一个节点准备逆置下一个节点 now= next; next= next->next; } L= now;//最后head头指针的指向改为和mid指针相同。 return L; } }
双指针法(适用场景):
- 寻找链表的倒数第 k 个元素(快指针先走 k 步,然后快慢指针开始同速前)
- 寻找链表的中点(快指针一次前进两步,慢指针一次前进一步);
- 判断链表是否有环(快指针每次前进两步,慢指针每次前进一步,如果含有环,快指针最终会和慢指针相遇);
- 奇偶排序(仅适用于数组:调整数组使奇数全部都位于偶数前面,一个指针p从开头往后,另一个指针q从后往前,当p遇到偶数时停止,当q遇到奇数时停止,然后交换p和q,然后继续,直到p=q时结束)
顺序表:经常结合排序和查找的几种算法进行设计,如归并排序、二分查找等
2. 栈和队列
2.1:栈
定义:先进后出,后进先出,只要满足此条件就可以称为栈,所以我们把数组和链表加上限制条件后便成了栈(栈是一种线性表,但只允许在一端进行插入和删除操作,n个不同元素进栈。出栈元素不同的排列数为)
主要方法:初始化、进栈、出栈、判空、遍历、清空等
分类:静态栈(顺序存储)、动态栈(链式存储)
应用:数制转换(如:10进制转8进制)、括号匹配检验、行编辑器、迷宫求解、表达式求值、中缀表达式转后缀表达式、递归实现
顺序栈 :将顺序表(数组)的一端作为栈底,另一端为栈顶
#define MAXSIZE 20 //栈中元素最大个数 #include <stdlib.h> #include <stdio.h> #include <stdbool.h> typedef struct{ int data[MAXSIZE]; //栈中元素 int top; //栈顶指针 }SqStack; //初始化顺序栈 void initStack(SqStack *s){ s->top=-1; } //获取栈顶元素 int getTop(SqStack *s,int x){ if(s->top==-1){ return false; } x=s->data[s->top]; return x; } //元素入栈 bool push(SqStack *s,int x){ if(s->top==MAXSIZE){ return false; } s->top++; s->data[s->top]=x; return true; } //元素出栈 int pop(SqStack *s,int x){ if(s->top==-1){ return false; } x=s->data[s->top]; s->top--; return x; } //获取元素个数 int getLength(SqStack *s){ if(s->top==-1){ return 0; }else{ return s->top; } } //判空 bool isEmpty(SqStack s){ if(s.top==-1){ return true; }else{ return false; } } void clearStack(SqStack *s){ s->top=-1; }
#define INCREMENT 10 //存储空间增量 #include <stdlib.h> #include <stdbool.h> typedef struct{ int *base;//栈底指针 int *top;//栈顶指针,栈顶指针指向栈顶元素的上一个位置 int size;//可用容量 }SqStack; //初始化顺序栈 void initStack(SqStack *s){ int num; printf("请输入线性表最大长度:"); scanf("%d",&num); s->base=(int*)malloc(num*sizeof(int)); if(!(s->base)){ printf("系统分配内存空间失败!\n"); exit(-1); } s->top=s->base; s->size=num; } //获取栈顶元素 int getTop(SqStack *s){ if(s->top==s->base){ printf("栈空!"); return -1; } int *temp; temp=s->top; return *(temp-1);//指针的每一次递增/递减,它其实会指向下一个/上一个元素的存储单元 } //元素入栈 void push(SqStack *s,int data){ if((s->top)-(s->base)>=s->size){ printf("扩容"); s->base=(int*)realloc(s->base,(s->size+INCREMENT)* sizeof(int)); //更改由malloc()函数分配的内存空间的大小 s->top=s->base+s->size;//base的位置上移栈长度得到栈顶元素位置 s->size+=INCREMENT; } *s->top=data; s->top++; } //元素出栈 int pop(SqStack *s){ if(s->base==s->top){ printf("栈空!"); return -1; } s->top--; int e=*(s->top);//printf(s->top)输出结果为地址13046832,s->top--后,输出结果为13046828 return e; } //获取元素个数 int getLength(SqStack *s){ if(s->base==s->top){ return 0; }else{ return s->top-s->base; } } //遍历栈 void findAll(SqStack *s){ if(s->top==s->base){ printf("栈空!"); return ; } int* temp=s->base; while(temp<s->top){ printf("%d ",*(temp++)); } printf("\n"); } //判空 bool isEmpty(SqStack s){ if(s.base==s.top){ return true; }else{ return false; } } void clearStack(SqStack *s){ s->top=s->base; } void destoryStack(SqStack *s){ s->base=NULL; s->top=NULL; s->size=0; free(s->base); free(s->top); printf("销毁成功!"); } void main(){ SqStack S; initStack(&S); printf("请输入有效元素个数:"); int num; scanf("%d",&num); for(int i=0;i<num;i++){ int val; printf("请输入第%d个元素值:",i+1); scanf("%d",&val); push(&S,val); } printf("遍历结果"); findAll(&S); printf("%s\n",isEmpty(S)==1?"是":"否"); int length=getLength(&S); printf("length:%d\n",length); for(int i=0;i<length;i++){ printf("%d ",pop(&S)); } printf("\n"); printf("%s\n",isEmpty(S)==1?"是":"否"); destoryStack(&S); }
void main(){ SqStack S; initStack(&S); printf("请输入有效元素个数:"); int num; scanf("%d",&num); for(int i=0;i<num;i++){ int val; printf("请输入第%d个元素值:",i+1); scanf("%d",&val); push(&S,val); } printf("遍历结果"); findAll(&S); printf("栈顶元素为:%d\n",getTop(&S)); printf("%s\n",isEmpty(S)==1?"是":"否"); printf("清空栈...\n"); clearStack(&S); int length=getLength(&S); printf("length:%d\n",length); for(int i=0;i<length;i++){ printf("%d ",pop(&S)); } printf("\n"); printf("%s\n",isEmpty(S)==1?"是":"否"); destoryStack(&S); }
共享栈:
让俩个顺序栈共享一个一个一维数组空间,将俩个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸,图中栈顶指针指向的是栈顶元素。共享栈是为了更有效的利用存储空间,且存取效率没有影响。
链栈:将链表的头部作为栈顶,尾部作为栈底
或
#include <stdlib.h> #include <stdio.h> #include <stdbool.h> typedef struct LNode{ int data; struct linkStack *next; }LNode,*linkStack; typedef struct Stack{ linkStack top;//栈顶指针 int size; }LinkStack; //初始化空栈,带头结点 LinkStack initLinkStack(LinkStack *S){ S->top=(linkStack)malloc(sizeof(LNode)); if(S->top==NULL){ printf("分配失败!"); exit(-1); } S->top->next=NULL; S->size=0; } bool isEmpty(LinkStack *S){ if(S->top->next==NULL){ return true; }else{ return false; } } int getLength(LinkStack *S){ return S->size; } //入栈 void push(LinkStack *S,int data){ printf("元素入栈:%d\n",data); linkStack pNew=(linkStack)malloc(sizeof(LNode)); pNew->data=data; pNew->next=S->top; S->top=pNew; S->size++; } //出栈 int pop(LinkStack *S){ linkStack pNew; int e=S->top->data; pNew=S->top; S->top=pNew->next; free(pNew); S->size--; printf("元素出栈:%d,栈顶元素:%d\n",e,S->top->data); return e; } //获取栈顶元素 int getTop(LinkStack *S){ if(isEmpty(S)){ printf("栈空"); exit(1); } return S->top->data; } //销毁 void destory(LinkStack *S){ linkStack p; while(S->top!=NULL){ p=S->top; S->top=p->next; printf("%d ",p->data); free(p); } S->size=0; } void main(){ LinkStack S; initLinkStack(&S); printf("Length:%d\n",getLength(&S)); push(&S,1); push(&S,2); push(&S,3); push(&S,4); printf("Length:%d\n",getLength(&S)); printf("栈空?%s\n",isEmpty(&S)==1?"是":"否"); printf("栈顶元素为:%d\n",getTop(&S)); int length=getLength(&S); for(int i=0;i<length;i++){ pop(&S); } printf(getLength(&S)); printf("栈空?%s\n",isEmpty(&S)==1?"是":"否"); printf("栈顶元素为:%d",getTop(&S)); }
2.2:队列
定义:先进先出,后进后出,只要满足此条件就可以称为队列。(队列是一种操作受限的线性表,只允许再一端进行插入而在另一端进行删除,插入元素叫入队,删除元素叫出队)
分类:单向对列(按存储结构又可分为顺序队列和链式队列)、双向(端)队列、循环队列(顺序存储,链式没有太大使用意义)
应用:银行排队办理业务、线程池的应用、
线程池工作原理:
- 当有任务提交到线程池,线程池首先会根据核心线程数创建线程来处理这些任务
- 如果核心线程处理不过来,就把任务放到一个阻塞队列等待,当核心线程空闲时从队列把任务取出
- 如果任务再继续来,阻塞队列放不下了,就会创建一些临时线程来处理新加的任务而不是取队列中的任务(临时线程即不超过线程池最大线程数限制的新建线程)
- 如果临时线程也用完了,就开启拒绝策略。
顺序队列
#define MaxSize 20 #include <stdlib.h> #include <stdio.h> #include <stdbool.h> typedef struct Squeue{ int data[MaxSize]; int front;//队头索引 int rear;//队尾索引 }Squeue; //初始化顺序队列 void initSqueue(Squeue *qu){ qu->front=qu->rear=0; } bool isFull(Squeue *qu){ // int length= sizeof(qu->data)/ sizeof(int); int length=qu->rear-qu->rear; if(length>=MaxSize){ return true; }else{ return false; } } bool isEmpty(Squeue *qu){ // int length= sizeof(qu->data)/ sizeof(int); int length=qu->rear-qu->front; if(length==0){ return true; }else{ return false; } } //入队,入队时只有rear在移动 int push(Squeue *qu,int data){ if(isFull(qu)){ printf("队满"); exit(-1); } printf("入队:%d\n",data); qu->data[qu->rear++]=data; } //出队 int pop(Squeue *qu){ if(isEmpty(qu)){ printf("队空"); exit(-1); } int e=qu->data[qu->front]; qu->front++; return e; } int getLength(Squeue *qu){ if(isEmpty(qu)){ return 0; } if(isFull(qu)){ return MaxSize; } return qu->rear-qu->front; } void clearQueue(Squeue *qu){ qu->front=0; qu->rear=0; } void main(){ Squeue qu; initSqueue(&qu); push(&qu,1); push(&qu,2); push(&qu,3); push(&qu,4); clearQueue(&qu); push(&qu,11); push(&qu,22); printf("出队:%d\n",pop(&qu)); printf("Length:%d\n",getLength(&qu)); int length=getLength(&qu); for(int i=0;i<length;i++){ printf("出队:%d\n",pop(&qu)); } }
顺序队列经过一系列的入队出队操作后, 两个指针最终会到达数组的末端处,虽然队中已没有了元素,如果不进行初始化,仍然无法继续插入元素,这就是所谓的“假溢出”。 为了解决假溢出的问题,可以将数组弄成一个环状,让rear和front指针沿着环走,这样就不会出现无法继续走 下去的情况,这样就产生了顺序循环队列,且有如下性质:
为了区分队空和堆满有三种方法,一般使用第一种
#define MaxSize 6 #include <stdlib.h> #include <stdio.h> #include <stdbool.h> typedef struct Squeue{ int data[MaxSize]; int front;//队头索引 int rear;//队尾索引 }Squeue; //初始化顺序循环队列 void initSqueue(Squeue *qu){ for(int i=0;i<MaxSize;i++){ qu->data[i]=0; } qu->front=qu->rear=0; } bool isFull(Squeue *qu){ if((qu->rear+1)%MaxSize==qu->front){ return true; }else{ return false; } } bool isEmpty(Squeue *qu){ if(qu->rear==qu->front){ return true; }else{ return false; } } //入队,入队时只有rear在移动 int push(Squeue *qu,int data){ if(isFull(qu)){ printf("队满请先将部分元素出队"); exit(-1); } printf("入队:%d\n",data); qu->rear=(qu->rear+1)%MaxSize;//队尾指针加1取模 qu->data[qu->rear]=data; printf("front:%d,rear:%d\n",qu->front,qu->rear); } //出队 int pop(Squeue *qu){ if(isEmpty(qu)){ printf("队空"); exit(-1); } qu->front=(qu->front+1)%MaxSize;//队头指针加1取模 int e=qu->data[qu->front]; qu->data[qu->front]=0;//队头指针内容置零代表无元素 printf("出队:%d\n",e); printf("front:%d,rear:%d\n",qu->front,qu->rear); return e; } int getLength(Squeue *qu){ if(isEmpty(qu)){ return 0; } if(isFull(qu)){ return MaxSize-1; } return (qu->rear + MaxSize - qu->front)%MaxSize;//如果rear<front:rear+maxsize-front;如果rear>front:rear-front;此表达式为俩者的结合写法 } void findAll(Squeue *qu){ for(int i=0;i<MaxSize;i++){ printf("%d:%d ",i,qu->data[i]); } printf("\n"); } void main(){ Squeue qu; initSqueue(&qu); push(&qu,10); push(&qu,20); push(&qu,30); push(&qu,40); push(&qu,11); findAll(&qu); pop(&qu); pop(&qu); findAll(&qu); push(&qu,12); findAll(&qu); pop(&qu); pop(&qu); push(&qu,66); findAll(&qu); int length=getLength(&qu); for(int i=0;i<length;i++){ pop(&qu); } }
链式队列(不会出现连续存储空间不足或溢出的问题)
#include <stdlib.h> #include <stdbool.h> typedef struct Node{ //链队列结点 int data; struct Node *next; }Node,*pNode; typedef struct Queue{ //链队列 pNode front; pNode rear; }Queue; void initQueue(Queue *qu){ pNode pHead=(pNode)malloc(sizeof(Node)); if(pHead==NULL){ exit(-1); } pHead->next=NULL; qu->front=qu->rear=pHead; } bool isEmpty(Queue *qu){ if(qu->front==qu->rear){ return true; }else{ return false; } } void push(Queue *qu,int data){ pNode pNew=(pNode)malloc(sizeof(Node)); pNew->data=data; pNew->next=NULL; qu->rear=pNew; } int pop(Queue *qu){ if(isEmpty(qu)){ printf("队空"); exit(-1); } pNode p=qu->front->next; int e=p->data; qu->front->next=p->next; if(qu->front->next==NULL){//如果原队列只有头结点时 qu->rear=qu->front; } free(p); return e; } int getLength(Queue *qu){ int i; pNode p=qu->front->next; for(i=0;p!=NULL;i++){ p=p->next; } return i; } void findAll(Queue *qu){ pNode p=qu->front->next; while(p!=NULL){ printf("%d ",p->data); p=p->next; } printf("\n"); } void clearQueue(Queue *qu){ pNode p=qu->front->next,q; while(p==NULL){ q=p->next; free(p); p=q; } qu->front=qu->rear; qu->front->next=NULL; } void destory(Queue *qu){ pNode p; while (qu->front){ p=qu->front->next; free(qu->front); qu->front=p; } } void main(){ Queue qu; initQueue(&qu); push(&qu,1); push(&qu,2); push(&qu,3); push(&qu,4); push(&qu,5); printf("%d\n",getLength(&qu)); findAll(&qu); int length=getLength(&qu); for(int i=0;i<length;i++){ printf("出队:%d\n",pop(&qu)); } }
双端队列:由于应用不多,这里只写原理
在双端队列入队时中,前端的元素排在后端进入元素的前面,后端进入的元素排在前端进入的元素的后面。在双端队列出队时,先出的元素排在后出的元素前面
类型:一种是数据只能从一段加入而可以从两端取数据 ,另一种是可从两端加入但只从一端取数据
2.3:总结
栈和队列是两种特殊的线性表
3.串
定义:由零个或多个字符组成的有限序列,是数据元素为单个字符的特殊线性表(也就是我们通常认识的字符串)
分类:定长顺序存储串、堆分配存储串(动态分配长度)、块链存储串(即链式存储)
基本操作:赋值操作、连接操作、求串长、窜的比较和求子串
空串和空白串的区别:
空串(Null String)是指长度为零的串
空白串(Blank String)是指包含一个或多个空白字符‘ ’(空格键)的字符串
定长顺序存储串(固定长度的数组构建的字符串)
#include <string.h> #include <stdlib.h> #include <stdio.h> #define MAXSIZE 255 //定长顺序存储 typedef struct{ char ch[MAXSIZE]; int length; }HString; void initStr(HString *str,char *chars){ int len=strlen(chars); if(len>MAXSIZE){ printf("字段长度过长!"); exit(-1); } for(int i=0;i<len;i++){ str->ch[i]=chars[i]; } str->length=len; } void display(HString *str){ int len=str->length; for(int i=0;i<len;i++){ printf("%c",str->ch[i]); } printf("\n"); } void main(){ HString str; char chars[]="abcdefg"; initStr(&str,chars); display(&str); }
堆分配存储串(使用malloc或realloc动态分配的数组构建的字符串)
#include <stdio.h> #include <stdlib.h> #include <string.h> //堆分配存储串 typedef struct{ char *ch; int length; }HString; //初始化 void initString(HString *str){ str->ch=NULL; str->length=0; } //输出串 void printString(HString *str){ if(str->length==0){ printf("空"); } printf("长度:%d,",str->length); for(int i=0;i<str->length;i++){ printf("%c",str->ch[i]); } printf("\n"); } //使用char数组给串赋值 void assign(HString *str,char *chars){ int len=strlen(chars); str->ch=(char*)malloc(len*sizeof(char)); if(str->ch==NULL){ printf("分配失败"); exit(-1); } for(int i=0;i<len;i++){ str->ch[i]=chars[i]; } str->length=len; } void copy(HString *str,HString *newStr){ newStr->ch=(char*)malloc(sizeof(char)*str->length); for(int i=0;i<str->length;i++){ newStr->ch[i]=str->ch[i]; } newStr->length=str->length; } int compare(HString *str1,HString *str2){ for(int i=0;i<str1->length&&i<str2->length;i++){ if(str1->ch[i]!=str2->ch[i]){//相等的字符跳过,直到不等的字符 return str1->ch[i]-str2->ch[i]; } } return str1->length-str2->length;//字符都相等,比较字符串的长度 } void compareResult(int result){ if(result>0){ printf("前者大于后者\n"); }else if(result<0){ printf("后者大于前者\n"); } else{ printf("相等\n"); } } //字符串拼接 void contact(HString *str1,HString *str2,HString *newStr){ int len=(str1->length)+(str2->length); initString(newStr); newStr->ch=(char*)malloc(sizeof(char)*len); for(int i=0;i<str1->length;i++){ newStr->ch[i]=str1->ch[i]; } for(int i=0;i<str2->length;i++){ newStr->ch[i+str1->length]=str2->ch[i]; } newStr->length=len; } //字符串截取 void substring(HString *str,HString *newStr,int pos,int len){ initString(newStr); newStr->ch=(char*)malloc(sizeof(char)*len); for(int i=0;i<len;i++){ newStr->ch[i]=str->ch[i+pos]; } newStr->length=len; } //返回子串在父串中的位置,即子串首字母在母串中的位置 int getPos(HString *parentStr,HString *childStr){ HString *str=(HString*)malloc(sizeof(HString)); initString(str); int len=childStr->length; int pos; for(pos=0;pos+len<parentStr->length;pos++){ substring(parentStr,str,pos,len); printf("第%d次截取到临时串为:",pos); printString(str); if(!compare(str,childStr)){ return pos; } } return 0; } //删除指定个字符 void delete(HString *str,int pos,int len){ for(int i=pos;i<str->length-len;i++){ str->ch[i]=str->ch[i+len]; } str->length=str->length-len; } //插入字符 void insert(HString *str,int pos,HString *insertStr){ int len=str->length+insertStr->length; str->ch=(char*)realloc(str->ch, sizeof(char)*len);//重新调整str的容量 for(int i=len-1;i>=pos+insertStr->length-1;i--){ str->ch[i]=str->ch[i-(insertStr->length)]; } for(int i=0;i<insertStr->length;i++){ str->ch[i+pos]=insertStr->ch[i]; } str->length=len; } //将str中的oldstr替换成newstr void replace(HString *oldstr,HString *newstr,HString *str){ int pos=getPos(str,oldstr); delete(str,pos,oldstr->length); insert(str,pos,newstr); } void main(){ HString str; initString(&str); char chars[]="abcdefg"; assign(&str,chars); printString(&str); HString newStr; initString(&newStr); copy(&str,&newStr); printString(&newStr); char newchars1[]="def"; HString newStr2; assign(&newStr2,newchars1); compareResult(compare(&str,&newStr)); compareResult(compare(&str,&newStr2)); HString str3; contact(&str,&newStr,&str3); printString(&str3); HString str4; substring(&str,&str4,2,3); printString(&str4); printf("============================\n"); printString(&str3); printString(&str4); printf("位置:%d",getPos(&str3,&str4)); printf("\n"); delete(&str,2,3); printf("str:"); printString(&str); printf("插入后:"); insert(&str,2,&str4); printString(&str); HString str5; initString(&str5); char chars2[]="www"; assign(&str5,chars2); replace(&str4,&str5,&str); printf("str替换后为:"); printString(&str); }
块链存储(使用链表来存储,每个结点存储一个字符或多个字符):
//块链存储(使用链表来存储,每个结点存储一个字符或多个字符) #include <stdio.h> #include <stdlib.h> #include <string.h> #define linkNum 3 //单个节点可存储数据个数 typedef struct Node{ char arr[linkNum]; struct Node *next; }Node,*pNode; pNode initLinkString(char *chars){ int len=strlen(chars); int nodeNum=len/linkNum;//所需结点个数 if(len%nodeNum!=0){ nodeNum++; } pNode pHead=(pNode)malloc(sizeof(Node)); pHead->next=NULL; pNode p=pHead; for(int i=0;i<nodeNum;i++){ int j=0; for(;j<linkNum;j++){ if(i*linkNum+j<len){ p->arr[j]=chars[i*linkNum+j]; }else{ p->arr[j]='#'; } } if(i*linkNum+j<len){ pNode pNew=(pNode)malloc(sizeof(Node)); pNew->next=NULL; p->next=pNew; p=pNew; } } return pHead; } void displayLinkString(pNode pHead){ pNode p=pHead; while(p!=NULL){ for(int i=0;i<linkNum;i++){ printf("%c",p->arr[i]); } p=p->next; } } void main(){ char chars[]="helloworld!"; pNode pHead=initLinkString(chars); displayLinkString(pHead); }
4.数组和广义表
4.1:数组
定义:用一组连续的内存空间来存储一组具有相同类型的数据。(一维数组可视为线性表)
分类:一维数组、二维数组、多维数组
特殊矩阵:对此矩阵、上(下)三角矩阵、对角矩阵。
压缩存储:多个形同的元素只分配一个存储空间,对零元素不分配存储空间。其目的是为了节省空间。
应用:对称矩阵的压缩存储、上(下)三角矩阵的压缩存储、稀疏矩阵(矩阵中包含大量0元素)的压缩存储
对称矩阵的压缩存储:将a[1,1]到a[n,n]存放到一个长度为n(n+1)/2的一维数组中。
元素a[i,j]在一维数组中的下标为:1+2+....+(i-1) +j-1 (以下三角区和主对角线元素为例)
其中:1+2+...+(i-1)为前(i-1)行元素,(j)代表第i行元素中a[i,j]之前(含本身)的元素有j个。
因为一维数组从0开始最后再减去1。
下三角矩阵:与对称矩阵类似,不同之处在于需要多存一个零元素。一维数组长度为:n(n+1)/2+1。即存储完下三角区和对角线元素后,再存一个零元素。
元素a[i,j]在一维数组中的下标为:1+2+....+(i-1) +j-1(以下三角区和主对角线元素为例)
而a[j,i]始终为数组最后的那一元素,下标为n(n+1)/2
上三角矩阵:计算比较麻烦了解就好了,但是可以使用对称性来求,即
上三角矩阵的a[i,j]下标+把该上三角矩阵转化成下三角矩阵后的b[j,i]的下标=n(n-1)/2-1
三对角矩阵: 所有非零元素都保存在以主对角线为中心的3条对角线上,其他为零元素。
a[i,j]的下标为:3*(i-1)-1 + w-1,其中w为1或2或3,若i<j则为3,若i=j则为2,若i>j,则为1
-1因为数组下标从0开始。
通过这个等式也可以在已知下标的基础上求i和j
稀疏矩阵:非零元素很少,零元素居多。通常我们使用三元组去保存数据,即
(行标,列标,元素值)
稀疏矩阵存储——三元组顺序表压缩存储,三元组顺序表每次提取指定元素都需要遍历整个数组,运行效率很低
#define number 20 typedef struct { int i,j;//行索引,列索引 int data; }Node; typedef struct { Node data[number];//存储所有非0元素 int lines,lists,num;//行数、列数、非0元素个数 }TSMatrix; void display(TSMatrix M){ for(int i=1;i<=M.lines;i++){ for(int j=1;j<=M.lists;j++){ int value=0; for(int k=0;k<M.num;k++){ if(i==M.data[k].i&&j==M.data[k].j){ printf("%d ",M.data[k].data); value=1; break; } } if(value==0){ printf("0 "); } } printf("\n"); } } void main(){ TSMatrix M; M.lines=3; M.lists=3; M.num=3; M.data[0].i=1; M.data[0].j=1; M.data[0].data=1; M.data[1].i=2; M.data[1].j=3; M.data[1].data=5; M.data[2].i=3; M.data[2].j=1; M.data[2].data=3; display(M); }
二维数组按行优先存储与按列优先存储
行逻辑链接顺序存储:
#include <stdio.h> #define MAXSIZE 12500 #define MAXRC 100 typedef struct { int i,j; int data; }Node;//三元组结点 //行逻辑链接顺序存储 typedef struct { Node data[MAXSIZE+1]; int rpos[MAXRC+1]; int lines,lists,num; }RLSMatrix; void display(RLSMatrix M){ for(int i=1;i<=M.lines;i++){ for(int j=1;j<=M.lists;j++){ int value=0; if(i+1 <=M.lines){ for(int k=M.rpos[i];k<M.rpos[i+1];k++){ if(i == M.data[k].i && j == M.data[k].j){ printf("%d ",M.data[k].data); value=1; break; } } if(value==0){ printf("0 "); } }else{ for(int k=M.rpos[i];k<=M.num;k++){ if(i == M.data[k].i && j == M.data[k].j){ printf("%d ",M.data[k].data); value=1; break; } } if(value==0){ printf("0 "); } } } printf("\n"); } } void main(){ /*0 3 0 5*/ /*0 0 1 0*/ /*2 0 0 0*/ //(1,2,3)|(1,4,5)|(2,3,1)|(3,1,2) RLSMatrix M; M.num = 4; M.lines = 3; M.lists = 4; M.rpos[1] = 1;//第一行第一个非0元素在一维数组的第1个位置 M.rpos[2] = 3;//第二行第一个非0元素在一维数组的第3个位置 M.rpos[3] = 4;//第三行第一个非0元素在一维数组的第4个位置 M.data[1].data = 3; M.data[1].i = 1; M.data[1].j = 2; M.data[2].data = 5; M.data[2].i = 1; M.data[2].j = 4; M.data[3].data = 1; M.data[3].i = 2; M.data[3].j = 3; M.data[4].data = 2; M.data[4].i = 3; M.data[4].j = 1; //输出矩阵 display(M); }
十字链表法存储:
数组 "不利于插入和删除数据" 的特点,以上两种压缩存储方式都不适合解决类似 "向矩阵中添加或删除非 0 元素" 的问题
#include <stdlib.h> #include <stdio.h> typedef struct OLNode{ int i,j,data;//三元组 struct OLNode *right,*down;//右指针、下指针(右指针指向同行结点,下指针指向下一行结点) }OLNode,*OLink; //十字链表法存储 typedef struct { OLink *rhead,*chead;//行链表数组,列链表数组 int rows,cols,nums;//行数、列数、非零元素个数 }CrossList; CrossList createMatrix(CrossList M){ int rows,cols,nums; printf("输入行数、列数、非0元素个数:"); scanf("%d%d%d",&rows,&cols,&nums); M.rows=rows; M.cols=cols; M.nums=nums; M.rhead=(OLink*)malloc(sizeof(OLNode)*(rows+1)); M.chead=(OLink*)malloc(sizeof(OLNode)*(cols+1)); if(M.rhead==NULL||M.chead==NULL){ printf("初始化失败!"); exit(-1); } int i; for(i=1;i<=rows;i++){ M.rhead[i]=NULL; }//把行数组的各项都赋值为空 int j; for(j=1;j<=cols;j++){ M.chead[j]=NULL; }//把列数组的各项都赋值为空 int data; OLink p,q; while (i!=0){ printf("请输入一个结点的三元信息(行号、列号、值):"); scanf("%d%d%d",&i,&j,&data); p=(OLNode*)malloc(sizeof(OLNode)); if(p==NULL){ printf("初始化三元组失败"); exit(-1); } p->i=i; p->j=j; p->data=data; //找到行的位置 if(M.rhead[i]==NULL||M.rhead[i]->j>j){ p->right=M.rhead[i]; M.rhead[i]=p; }else{ for(q=M.rhead[i];(q->right)&&q->right->j<j;q=q->right); p->right=q->right; q->right=p; } //找到列的位置 if(M.chead[j]==NULL||M.chead[j]->i>i){ p->down=M.chead[j]; M.chead[j]=p; }else{ for(q=M.chead[j];(q->down)&&q->down->i<i;q=q->down); p->down=q->down; q->down=p; } } return M; } void display(CrossList M){ for (int i = 1; i <= M.cols; i++) { if (NULL != M.chead[i]) { OLink p = M.chead[i]; while (NULL != p) { printf("%d %d %d\n", p->i, p->j, p->data); p = p->down; } } } } void main(){ CrossList M; M.rhead=NULL; M.chead=NULL; M=createMatrix(M); display(M); }
逻辑结构图
4.2:广义表
为什么会出现广义表?因为数组只能存储同种类型的元素,如 {1,2,3} 或 {{1,2,3},{4,5,6}} 。
但如果我们想存储这样的数据,如 {1,{1,2,3}} 则不行了,因此我们用广义表来存储
定义:n个元素的序列,广义表的元素为:或者是原子项,或者是一个广义表
LS = (a1,a2,…,an),LS 代表广义表的名称,ai表示广义表存储的数据, ai 既可以代表单个元素(原子),也可以代表另一个广义表(子表)
存储:链式存储(如果使用顺序存储则需要操作n为数组(如: {1,{2,{3,4}}}要使用三维数组)造成存储空间的浪费)
广义表节点结构:
tag 标记位用于区分此节点是原子还是子表,通常原子的 tag 值为 0,子表的 tag 值为 1
hp 指针用于连接本子表中存储的原子或子表
tp 指针用于连接广义表中下一个原子或子表
#include <stdlib.h> typedef struct GLNode{ int tag; union{ char atom; struct { struct GLNode *hp,*next;//hp指向指向原子节点,next指向下一个节点 }ptr; }; }*Glist; //{a,{b,c,d}} Glist createGlist(Glist C){ //头指针 C=(Glist)malloc(sizeof(Glist)); //定义原子节点 C->tag=1; C->ptr.hp=(Glist)malloc(sizeof(Glist)); C->ptr.hp->tag=0; C->ptr.hp->atom='a'; C->ptr.next=(Glist)malloc(sizeof(Glist)); C->ptr.next->tag=1; C->ptr.next->ptr.hp=(Glist)malloc(sizeof(Glist)); C->ptr.next->ptr.next=NULL; C->ptr.next->ptr.hp->tag=1; C->ptr.next->ptr.hp->ptr.hp=(Glist)malloc(sizeof(Glist)); C->ptr.next->ptr.hp->ptr.hp->tag=0; C->ptr.next->ptr.hp->ptr.hp->atom='b'; C->ptr.next->ptr.hp->ptr.next=(Glist)malloc(sizeof(Glist)); C->ptr.next->ptr.hp->ptr.next->tag=1; C->ptr.next->ptr.hp->ptr.next->ptr.hp=(Glist)malloc(sizeof(Glist)); C->ptr.next->ptr.hp->ptr.next->ptr.hp->tag=0; C->ptr.next->ptr.hp->ptr.next->ptr.hp->atom='c'; C->ptr.next->ptr.hp->ptr.next->ptr.next=(Glist)malloc(sizeof(Glist)); C->ptr.next->ptr.hp->ptr.next->ptr.next->tag=1; C->ptr.next->ptr.hp->ptr.next->ptr.next->ptr.hp=(Glist)malloc(sizeof(Glist)); C->ptr.next->ptr.hp->ptr.next->ptr.next->ptr.hp->tag=0; C->ptr.next->ptr.hp->ptr.next->ptr.next->ptr.hp->atom='d'; C->ptr.next->ptr.hp->ptr.next->ptr.next->ptr.next=NULL; }
另一种节点结构:
tag 标记位用于区分此节点是原子还是子表,通常原子的 tag 值为 0,子表的 tag 值为 1
action:标识原子值
hp 指针用于连接本子表中存储的原子或子表
tp 指针用于连接广义表中下一个原子或子表
#include <stdlib.h> //这种存储方式更简单一些 typedef struct GLNode{ int tag; union{ int atom; struct GLNode *hp; }; struct GLNode *tp; }*Glist; Glist creatGlist(Glist C){ C=(Glist)malloc(sizeof(Glist)); C->tag=1; C->hp=(Glist)malloc(sizeof(Glist)); C->tp=NULL; //表头原子a C->hp->tag=0; C->atom='a'; C->hp->tp=(Glist)malloc(sizeof(Glist)); C->hp->tp->tag=1; C->hp->tp->hp=(Glist)malloc(sizeof(Glist)); C->hp->tp->tp=NULL; //原子b C->hp->tp->hp->tag=0; C->hp->tp->hp->atom='b'; C->hp->tp->hp->tp=(Glist)malloc(sizeof(Glist)); //原子c C->hp->tp->hp->tp->tag=0; C->hp->tp->hp->tp->atom='c'; C->hp->tp->hp->tp->tp=(Glist)malloc(sizeof(Glist)); //原子d C->hp->tp->hp->tp->tp->tag=0; C->hp->tp->hp->tp->tp->atom='d'; C->hp->tp->hp->tp->tp->tp=NULL; return C; }
广义表深度和长度的计算:
广义表的长度: 若广义表不空,则广义表所包含的元素的个数,叫广义表的长度。
广义表的深度: 广义表中括号的最大层数叫广义表的深度。
广义表的表头:当广义表LS非空时,称第一个元素为LS的表头;
广义表表尾:称广义表LS中除去表头后其余元素组成的广义表为LS的表尾。
举例:LS=((),a,b,(a,b,c),(a,(a,b),c)),
其中表头为子表LSH = (),表尾为子表LST = (a,b,(a,b,c),(a,(a,b),c))
广义表LS的长度:5,其中5哥元素分别为:()、a、b、(a,b,c)、(a,(a,b),c),所以长度为5
广义表LS的深度:3,其中括号的层数依次是2,1,1,2,3,所以其最大层数为3,即深度为3
5.树和二叉树
5.1:树
定义:它是由n(n>=1)个有限结点组成一个具有层次关系的集合
相关概念:根结点和叶子结点、子树和空树、结点的度(结点有多少分支)和层(根节点为第一层依此类推到叶子结点)、树的度(树内结点度的最大值)和层、有序树(规定结点从左到右看)和无序树(没有规定)
树的基本知识:
结点的度:结点的孩子个数称为结点的度。如:B的度为2,D的度为3
树的度:树中结点的最大度数。如:此树的度为3
分支结点:度大于0的结点。
叶子结点:度为0的结点。
树的高度:4
路径:俩个结点之间所经过的结点序列。如ABEL是一个路径(树的路径是自顶向下的)
路径长度:路径上经过的边的个数,如ABEL路径的长度为:3
树的性质:
树中的结点数=所有结点度数之和+1。
度为m的树中第i层最多有个结点(i≥1)。
高度为h的m叉树最多有个结点。
有n个结点的m叉树的最小高度为。
例题:
树的存储:
存储方式:双亲表示法、孩子表示法、孩子兄弟表示法
操作:树转二叉树、遍历(先根遍历、后根遍历)
双亲表示法:采用一组连续的存储空间存储每个结点,每个结点增设一个伪指针(保存的是数组下标),其中根结点下标为0且伪指针为-1,其余结点层序序列存入数组。
优点:可快速得到每个结点的双亲结点。
缺点:求已知结点的孩子结点不方便,需要遍历整个结构。
#define MAX_SIZE 100//宏定义树中结点的最大数量 #include<stdio.h> #include<stdlib.h> typedef struct Snode{ int data;//树中结点的数据类型 int parent;//结点的父结点在数组中的位置下标 }PTNode; typedef struct { PTNode tnode[MAX_SIZE];//存放树中所有结点 int n;//结点数 }PTree; PTree InitPNode(PTree tree) { int i,j; char ch; printf("请输出节点个数:\n"); scanf("%d",&(tree.n)); printf("请输入结点的值其双亲位于数组中的位置下标:\n"); for(i=0; i<tree.n; i++) { fflush(stdin); scanf("%c %d",&ch,&j); tree.tnode[i].data = ch; tree.tnode[i].parent = j; } return tree; } void FindParent(PTree tree) { char a; int isfind = 0; printf("请输入要查询的结点值:\n"); fflush(stdin); scanf("%c",&a); for(int i =0;i<tree.n;i++){ if(tree.tnode[i].data == a){ isfind=1; int ad=tree.tnode[i].parent; printf("%c的父节点为 %c,存储位置下标为 %d",a,tree.tnode[ad].data,ad); break; } } if(isfind == 0){ printf("树中无此节点"); } } void main(){ PTree tree; tree = InitPNode(tree); FindParent(tree); }
孩子表示法:采用的是 "顺序表+链表" 的组合结构,即n个结点有n个孩子链表(叶子节点的孩子链表为空表)
#include <stdio.h> #include <stdlib.h> //定义孩子链表的每个结点 typedef struct CTNode{ int child;//孩子结点的下标 struct CTNode *next;//指向孩子结点的指针 }ChildPtr; typedef struct { int data;//头节点保存的值 ChildPtr *firstchild;//孩子链表的头指针 }CTBox; typedef struct { CTBox nodes[20];//保存头节点的数组 int n,r;//n代表结点数量,r代表根的位置 }CTree; CTree initTree(CTree tree){ printf("输入节点数量:\n"); scanf("%d",&(tree.n)); for(int i=0;i<tree.n;i++){ printf("输入第 %d 个节点的值:\n",i+1); fflush(stdin); scanf("%c",&(tree.nodes[i].data)); tree.nodes[i].firstchild=(ChildPtr*)malloc(sizeof(ChildPtr)); tree.nodes[i].firstchild->next=NULL; printf("输入节点 %c 的孩子节点数量:\n",tree.nodes[i].data); int Num; scanf("%d",&Num); if(Num!=0){ ChildPtr *p = tree.nodes[i].firstchild; for(int j = 0 ;j<Num;j++){ ChildPtr * newEle=(ChildPtr*)malloc(sizeof(ChildPtr)); newEle->next=NULL; printf("输入第 %d 个孩子节点在顺序表中的位置",j+1); scanf("%d",&(newEle->child)); p->next= newEle; p=p->next; } } } return tree; } void findKids(CTree tree,char a){ int hasKids=0; for(int i=0;i<tree.n;i++){ if(tree.nodes[i].data==a){ ChildPtr * p=tree.nodes[i].firstchild->next; while(p){ hasKids = 1; printf("%c ",tree.nodes[p->child].data); p=p->next; } break; } } if(hasKids==0){ printf("此节点为叶子节点"); } } void main(){ CTree tree; tree = initTree(tree); //默认数根节点位于数组notes[0]处 tree.r=0; printf("找出节点 F 的所有孩子节点:"); findKids(tree,'F'); }
优点:求已知结点的孩子结点比较方便
缺点:求已知结点的双亲结点麻烦,需要遍历n个孩子链表
孩子兄弟表示法:以二叉链表作为树的存储结构。每个结点包括:结点值、指向该结点孩子结点的指针、指向该结点兄弟结点的指针。
typedef struct CSNode{ char data; struct CSNode *firstchild,*nextsibling;//指向第一个孩子的指针、指向第一个兄弟的指针 }CSNode,*CSTree;
优点:易于查找已知结点的孩子结点。
缺点:不方便查找已知结点的双亲结点,但可在结点定义中增设parent域指向其父节点从而方便查询。
5.2:二叉树
定义:树中各结点的度不超过2的有序树
性质:;叶子结点数=度为2的结点数+1
性质:
叶子节点数=度为2的结点数+1 (即:n0=n2+1,由结点总数=分支数+1关系推导出)
第k层有个结点;
高度为h的二叉树最多有个节点(K为树的深度,等于树的层)
在完全二叉树中,结点i所在层为(i为从上到下从左到右编号)
n个结点的完全二叉树高度为
操作:创建节点、插入节点、查找节点、删除节点、遍历二叉树(先序、中序、后序)、销毁二叉树、求节点或叶子节点个数、求树高、求路径、交换节点的左右孩子
分类:完全二叉树、满二叉树(除了叶子结点每个结点的度都为2)、哈夫曼树(带权路径长度最小的二叉树又叫最优二叉树)、平衡二叉树(又叫AVL树、任意节点的子树的高度差都小于等于1)、线索二叉树(加上结点前趋后继信息的二叉树)、二叉排序树(又叫二叉查找树)、红黑树(解决二叉排序树插入新节点导致的不平衡)、B-树(B树)、B+树(对B-树的简化)
满二叉树:高度为h的满二叉树,含有2^h-1个结点。
完全二叉树:高度为h,当且仅当每个结点都与高度为h的满二叉树编号一一对应
二叉排序树:左子树所有结点关键字<根结点关键字,右子树所有结点关键字>根节点关键字
平衡二叉树:|左子树深度-右子树深度|≤1
存储结构
1.顺序存储结构(仅仅适用于满或完全二叉树,普通二叉树想顺序存储需要补成完全二叉树才可)
完全二叉树的顺序存储:
顺序表还原完全二叉树:根据第i结点,其左孩子是2^i,右孩子是2^i+1来构建。
这种存储结构建议从数组下标1开始,若从0开始则无法用性质4计算出孩子结点在数组的位置
2.链式存储结构
二叉链表法(每个节点存储左子树和右子树)
二叉树链式存储:因为普通二叉树使用顺序存储空间利用率低,所以我们提出了链式存储
其中Lchild:指向左孩子结点的指针、data:保存数据,Rchild指向右孩子结点的指针
#include <stdlib.h> //定义二叉数结点 typedef struct BiTNode{ int data; struct BiTNode *lchild,*rchild; }BiTNode,*BiTree; BiTree createBiTree(){ BiTree T=(BiTree)malloc(sizeof(BiTree)); T->data=1; T->lchild=(BiTNode*)malloc(sizeof(BiTNode)); T->lchild->data=2; T->rchild=(BiTNode*)malloc(sizeof(BiTNode)); T->rchild->data=3; T->rchild->lchild=NULL; T->rchild->rchild=NULL; T->lchild->lchild=(BiTNode*)malloc(sizeof(BiTNode)); T->lchild->lchild->data=4; T->lchild->rchild=NULL; T->lchild->lchild->lchild=NULL; T->lchild->lchild->rchild=NULL; return T; } void main(){ BiTree T=createBiTree(); printf("%d",T->lchild->lchild->data); }
三叉链表法(左子树、右子树、父节点)
三叉链表: 方便查找某节点的父节点
二叉树的遍历:先序遍历(根-左-右)、中序遍历(左-根-右)、后序遍历(左-右-根)
#include <stdlib.h> #include <stdio.h> typedef struct BiTNode{ int data; struct BiTNode *left,*right; }BiTNode; //先序 void preOrder(BiTNode *node){ if(node!=NULL){ printf("%d ",node->data); preOrder(node->left); preOrder(node->right); } } //中序 void midOrder(BiTNode *node){ if(node!=NULL){ midOrder(node->left); printf("%d ",node->data); midOrder(node->right); } } //后序 void endOrder(BiTNode *node){ if(node!=NULL){ endOrder(node->left); endOrder(node->right); printf("%d ",node->data); } } void main(){ BiTNode n1; BiTNode n2; BiTNode n3; BiTNode n4; n1.data=1; n2.data=2; n3.data=3; n4.data=4; n1.left=&n2; n1.right=&n3; n2.left=&n4; n2.right=NULL; n3.left=NULL; n3.right=NULL; n4.left=NULL; n4.right=NULL; printf("先序遍历的结果为:"); preOrder(&n1); putchar('\n'); printf("中序遍历的结果为:"); midOrder(&n1); putchar('\n'); printf("后序遍历的结果为:"); endOrder(&n1); putchar('\n'); }
三种遍历每个结点都访问一次且仅访问一次,故时间复杂度为O(n);空间复杂度最坏情况下,二叉树是n个结点n层的单支树,递归工作栈深度为n,此时空间复杂度为O(n)
二叉树层序遍历
遍历过程描述:
- 根节点入队
- 若队空(即所有结点已处理完毕)则遍历结束,否则重复操作3
- 队列中第一个结点出队,若其有左孩子,则左孩子入队;若其有右孩子,则有孩子入队,返回2
#define MaxSize 20 #include <stdlib.h> #include <stdio.h> #include <stdbool.h> //定义二叉数结点 typedef struct BiTNode{ int data; struct BiTNode *lchild,*rchild; }BiTNode,*BiTree; typedef struct Squeue{ BiTNode data[MaxSize]; int front;//队头索引 int rear;//队尾索引 }Squeue; //初始化顺序队列 void initSqueue(Squeue *qu){ qu->front=qu->rear=0; } bool isFull(Squeue *qu){ int length=qu->rear-qu->rear; if(length>=MaxSize){ return true; }else{ return false; } } bool isEmpty(Squeue *qu){ int length=qu->rear-qu->front; if(length==0){ return true; }else{ return false; } } //入队,入队时只有rear在移动 void push(Squeue *qu,BiTNode data){ if(isFull(qu)){ printf("队满"); exit(-1); } printf("入队:%d\n",data.data); qu->data[qu->rear++]=data; } BiTNode pop(Squeue *qu){ if(isEmpty(qu)){ printf("队空"); exit(-1); } BiTNode e=qu->data[qu->front]; qu->front++; printf("出队:%d\n",e.data); return e; } BiTree createBiTree(){//1 23 4 BiTree T=(BiTree)malloc(sizeof(BiTree)); T->data=1; T->lchild=(BiTree)malloc(sizeof(BiTNode)); T->lchild->data=2; T->rchild=(BiTree)malloc(sizeof(BiTNode)); T->rchild->data=3; T->rchild->lchild=NULL; T->rchild->rchild=NULL; T->lchild->lchild=(BiTNode*)malloc(sizeof(BiTNode)); T->lchild->lchild->data=4; T->lchild->rchild=NULL; T->lchild->lchild->lchild=NULL; T->lchild->lchild->rchild=NULL; return T; } void levelOrder(BiTree T,int size,Squeue *Q){ int p[size]; int i=0; if(T==NULL){ return; } push(Q,*T); while(!isEmpty(Q)){ BiTNode node=pop(Q); p[i]=node.data; i++; if(node.lchild!=NULL){ push(Q,*node.lchild); } if(node.rchild!=NULL){ push(Q,*node.rchild); } } for(int j=0;j<4;j++){ printf("%d",p[j]); } } void main(){ Squeue Q; initSqueue(&Q); BiTree T=createBiTree(); levelOrder(T,4,&Q); }
typedef struct BiTNode { int data; struct BiTNode *lchild,*rchild; }BiTNode,*BiTree; //求已知结点的双亲结点 BiTNode* parent(BiTree T,int x) { BiTNode *ans; if(T==NULL) return NULL; if(T->lchild==NULL&&T->rchild==NULL) return NULL else { if(T->lchild->data==x||T->rchild->data==x) return T; else { ans=parent(T->lchild,x); if(ans) return ans; ans=parent(T->rchild,x); if(ans) return ans; return NULL; } } }
/*求二叉树的深度 ,递归方式*/ int getDepth(BiTreeNode *root){ int left, right; if (root == NULL) //递归出口 return 0; else{ left = getDepth(root->leftChild); //递归 right = getDepth(root->rightChild); return left > right ? (left+1) : (right+1); //返回较深的一棵子树 } }
typedef struct BiTNode{ char data; struct BiTNode *left, *right; }BiTNode, *BiTree; //求二叉树的叶子节点个数 int getLeafNum(BiTNode *root){ if (NULL == root){ return 0; } if (NULL == root->left && NULL == root->right){ return 1; } return getLeafNum(root->left)+ getLeafNum(root->right);; }
//判断两颗二叉树是否相同,相同即:二叉树结构相同且二叉树对应节点值相同 bool isEqual(BTree T1,BTree T2) { if(T1 == NULL && T2 == NULL) return true; if(!T1||!T2) return false; if(T1->data == T2->data) return isEqual(T1->lc,T2->lc) && isEqual(T1->rc,T2->rc);//判断左右子树是否都相等 else return false; }
由遍历序列构建二叉树:
1.由先序和中序序列可以唯一地构建一颗二叉树
2.由后序和中序序列可以唯一地构建一颗二叉树
3.由层序和中序序列可以唯一地构建一颗二叉树
线索二叉树:
我们发现使用链式存储时,结点会有许多空指针,这样不利于资源的有效利用,且无法轻松得到某结点的前驱和后继,所以我们提出了线索二叉树的概念:
当 ltag = 0 时, lchild 指向结点的左孩子;
当 ltag = 1 时, lchild 指向结点的直接前驱;
当 rtag = 0 时, rchild 指向结点的右孩子;
当 rtag = 1 时, rchild 指向结点的直接后继三种不同的线索二叉树:
typedef struct ThreadNode{ int data; //数据域 struct ThreadNode *lChild,*rChild;//左、右孩子指针 int ltag,rtag;//左、右线索标志 }ThreadNode,*ThreadTree;
构造线索二叉树
即将二叉树中的空指针改为前驱和后继,故需要遍历一次二叉树。;例如中序线索二叉树的建立,即使用中序遍历对二叉树进行线索化:
#include "stdlib.h" typedef struct ThreadNode{ int data; //数据域 struct ThreadNode *lChild,*rChild;//左、右孩子指针 int ltag,rtag;//左、右线索标志 }ThreadNode,*ThreadTree; ThreadTree pre = NULL;//保存上一次访问过的结点 ThreadTree createThreadTree(){ ThreadTree T=(ThreadTree)malloc(sizeof(ThreadTree)); T->data=1; T->lChild=(ThreadNode*)malloc(sizeof(ThreadNode)); T->lChild->data=2; T->rChild=(ThreadNode*)malloc(sizeof(ThreadNode)); T->rChild->data=3; T->rChild->lChild=NULL; T->rChild->rChild=NULL; T->lChild->lChild=(ThreadNode*)malloc(sizeof(ThreadNode)); T->lChild->lChild->data=4; T->lChild->rChild=NULL; T->lChild->lChild->lChild=NULL; T->lChild->lChild->rChild=NULL; return T; } void thread(ThreadNode *p){ if(p!=NULL){ thread(p->lChild); if(p->lChild==NULL){ p->lChild=pre; p->ltag=1; } if(pre!=NULL&&pre->rChild==NULL){ pre->rChild=p; pre->rtag=1; } pre=p; thread(p->rChild); } } void InThread(ThreadTree T){ if(T!=NULL){ thread(T); pre->rChild=NULL; pre->rtag=1; } } void main(){ ThreadTree T=createThreadTree(); InThread(T); printf("结点为:%d",T->lChild->lChild->rChild->data); }
中序线索二叉树遍历:
//获取该数中中序序列第一个(最左下)结点 ThreadNode* getFirstNode(ThreadNode *p){ while(p->ltag!=1){//中序第一个结点(即最左下结点)的ltag一定为1 p=p->lChild; } return p; } //获取该结点的后继结点 ThreadNode* getNextNode(ThreadNode *p){ if(p->rtag==0){ return getFirstNode(p->rChild);//获取右子树中的最左下结点 }else{ return p->rChild;//直接返回右指针所指向结点 } } //遍历二叉树 void InOrder(ThreadTree T){ ThreadNode *temp=getFirstNode(T); for(ThreadNode *p=temp;p!=NULL;p=getNextNode(p)){ printf("%d",p->data); } } void main(){ ThreadTree T=createThreadTree(); InThread(T); printf("结点为:%d\n",T->lChild->lChild->rChild->data); InOrder(T); }
带头结点的线索二叉树:这样就省去了ltag和rtag了,且头节点的做指针指向根结点,右指针指向中序最后一个结点;中序第一个结点左指针和中序最后一个结点右指针都指向头结点
先序线索二叉树和后序线索二叉树
先序线索二叉树:已知一结点,若其有左孩子则其为后继结点;若其无左孩子但有右孩子则右孩子为后继;如果该结点既无左孩子又无有孩子则其线索化后右指针为后继。
后续线索二叉树:已知一结点,若此结点为二叉树的根则其后继为空;若此结点是双亲的右孩子或者是双亲的左孩子且双亲无右子树则其后继为双亲;若此结点是双亲的左孩子且双亲有右子树则后继为双亲右子树后序遍历序列第一个结点。
(做题技巧:其实不需要记住上面的话,只需要把二叉树画出,然后根据是哪种遍历,直接判断某结点的后继为谁就指向谁就可以了,如下图a的D结点,如果是后续线索二叉树,则D的右指针指向什么?答:根据后续遍历结果为CDBFA,则D的右指针指向B)
回朔法
定义:解决问题时,每进行一步,都是抱着试试看的态度,如果发现当前选择并不是最好的,或者这么走下去肯定达不到目标,立刻做回退操作重新选择。这种走不通就回退再走的方法就是回溯法。
回溯法与递归是有区别的:回溯法在列举过程如果发现当前情况根本不可能存在,就停止后续的所有工作,返回上一步进行新的尝试。递归是从问题的结果出发,不断地调用自己的思想就是递归,回溯和递归唯一的联系就是,回溯法可以用递归思想实现
使用回溯法解决问题的过程,实际上是建立一棵“状态树”的过程,回溯法的求解过程实质上是先序遍历“状态树”的过程。在某些情况下,回溯法解决问题的过程中创建的状态树并不都是满二叉树,因为在试探的过程中,有时会发现此种情况下,再往下进行没有意义,所以会放弃这条死路,回溯到上一步
5.3:森林
定义:由多个互不相交的树组成的集合称为森林
操作:森林转二叉树、二叉树转森林、树和森林的遍历(先序遍历、后序遍历)
树转为二叉树:二叉树和树均可以用二叉链表表示且一一对应,故一棵树可以找到一个孩子兄弟二叉树与之对应。转换规则:左孩子右兄弟
森林转二叉树:与树转为二叉树类似,其中每棵树的根节点视为兄弟结点。转换规则:左孩子右兄弟
树和森林的遍历
树的遍历:
先根遍历:先访问根结点,再依次遍历没课子树,其遍历序列和对应的孩子兄弟二叉树先序序列相同。
后根遍历:先遍历每棵子树,再访问根节点,其遍历序列和对应的孩子兄弟二叉树的中序序列相同。
例如:此树的先根遍历序列为:ABEFCDG,后根遍历序列为:EFBCGDA
层次遍历:按层序依次访问各结点。如上图:ABCDEFG
森林的遍历:
先序遍历森林:从第一棵树开始先序遍历遍历即可,与对应的孩子兄弟二叉树的先序遍历序列一致。
中序遍历森林(有的教材也叫后序遍历森林):从第一棵树开始中序遍历遍历即可,与对应的孩子兄弟二叉树的中序遍历序列一致。
例如:此森林的先序遍历序列为:ABCDEFGHI,中序遍历序列为:BCDAFEHIG
5.4 树和二叉树的应用
1.二叉排序树(BST)(也叫二叉搜索树或二叉查找树)
定义:
- 二叉排序树中,如果其根结点有左子树,那么左子树上所有结点的值都小于根结点的值;
- 二叉排序树中,如果其根结点有右子树,那么右子树上所有结点的值都大小根结点的值;
- 二叉排序树的左右子树也要求都是二叉排序树;
所以对二叉排序树进行中序遍历可得到一个递增的有序序列。
查找结点:
从根结点开始逐层向下比较,若关键字比较相等则查找成功,若给定值小于根结点值则在左子树查找,若大于则在右子树查找。
插入结点:
二叉排序树是动态生成的,在查找过程中不存在目标关键字则插入。若原二叉树为空,则直接插入,若关键字小于根结点值则插入左子树,若关键字大于根结点则插入右子树。
#include <stdbool.h> #include <stdio.h> #include <stdlib.h> typedef struct BiNode{ int data; struct BiNode *left; struct BiNode *right; }BiNode,*BiTree; //使用二叉排序树查找关键字(递归方式) BiTree SearchBSTRecursion(BiTree T,int key){ if(T==NULL||key==T->data){ return T; }else if(key<T->data){ SearchBSTRecursion(T->left,key); }else{ SearchBSTRecursion(T->right,key); } } //使用二叉排序树查找关键字(非递归方式) BiTree SearchBSTNoRecursion(BiTree T,int key){ while(T!=NULL&&key!=T->data){ if(key<T->data){ T=T->left; }else{ T=T->right; } } return T; } //使用二叉排序树查找关键字,若不存在则用指针保存失败位置 bool SearchBST(BiTree T,int key,BiTree f,BiTree *p){ if(!T){ *p=f;//令 p 指针指向查找过程中最后一个叶子结点 return false; }else if(key==T->data){ *p=T;//令 p 指针指向该关键字 return true; }else if(key<T->data){ return SearchBST(T->left,key,T,p); }else{ return SearchBST(T->right,key,T,p); } } //二叉排序树中插入关键字 bool insertBST(BiTree *T,int e){ BiTree p=NULL; if(!SearchBST((*T),e,NULL,&p)){ //如果查找不成功 BiTree s=(BiTree)malloc(sizeof(BiTree)); s->data=e; s->left=s->right=NULL; if(!p){//如果 p 为NULL,说明该二叉排序树为空树,此时插入的结点为整棵树的根结点 *T=s; }else if(e<p->data){//如果 p 不为 NULL,则 p 指向的为查找失败的最后一个叶子结点,只需要通过比较 p 和 e 的值确定 s 到底是 p 的左孩子还是右孩子 p->left=s; }else{ p->right=s; } return true; } return false; } //中序遍历 void order(BiTree t){ if(t==NULL){ return; } order(t->left); printf("%d ",t->data); order(t->right); } void main(){ int i; int a[5]={3,4,2,5,9}; BiTree T=NULL; for(i=0;i<5;i++){ insertBST(&T,a[i]); } printf("中序遍历:"); order(T); }
这里我要再写另一种写法(更好理解)且不使用BiTree的写法,因为我老容易搞晕BiTree和BiNode的关系
#include <stdbool.h> #include <stdio.h> #include <stdlib.h> typedef struct BiNode{ int data; struct BiNode *left; struct BiNode *right; }BiNode; bool insert(BiNode **T,int key){ if((*T)==NULL){ (*T)=(BiNode *)malloc(sizeof(BiNode)); (*T)->data=key; (*T)->left=(*T)->right=NULL; return true; }else if(key==(*T)->data){ return false; }else if(key<(*T)->data){ return insert(&(*T)->left,key); }else{ return insert(&(*T)->right,key); } } //中序遍历 void order(BiNode *t){ if(t==NULL){ return; } order(t->left); printf("%d ",t->data); order(t->right); } void main(){ int i; int a[5]={3,4,2,5,9}; BiNode* T=NULL; for(i=0;i<5;i++){ insert(&T,a[i]);//注意这里的&T代表的是指针的地址,所以再insert方法中应使用**T去接收 } printf("中序遍历:"); order(T); }
删除结点:
使用二叉排序树表示的动态查找表中删除某个数据元素时,需要在成功删除该结点的同时,依旧使这棵树为二叉排序树,假设要删除的为结点 p,则对于二叉排序树来说,需要根据结点 p 所在不同的位置作不同的操作,有以下 3 种可能:
- 若被删除结点p是叶结点,则直接删除
- 若被删结点 p 只有左子树或者只有右子树,将其左子树或者右子树直接变为结点 p 父结点的子树即可;
- 若被删结点 p 左右子树都有,则令p的直接后继(或直接前驱)替代p,然后从树中删除整个直接后继(或直接前驱)然后就转换成了情况1或情况2。
情况一: 略
情况二:
只有左子树
只有右子树
情况三:既有左子树又有右子树,则找到直接后继81,用81替换掉78的值,然后删除81;
删除81变成只有右子树的情况,让其子树成为父结点的子树即可。
查找效率:
如下图a,在等概率的情况下,查找成功的平均查找长度为:
ASL=(1+2*2+3*4+4*3)/10=2.9
如下图b,在等概率的情况下,查找成功的平均查找长度为:
ASL=(1+2+3+4+5+6+7+8+9+10)/10=5.5
二叉排序树查找与二分查找的比较:
- 从查找过程看,相似。
- 从平均执行时间看,二分法查找对象是是有序顺序表,时间是; 二叉排序树无需移动结点,时间为;
- 从判定树看:二分法判定树唯一;二叉排序树不唯一(相同关键字插入顺序不同则二叉排序树不同)。
- 适用范围:二分法适用于静态查找表;二叉排序树适用于动态查找表。
平衡二叉树:
背景:为避免树的高度过高,降低二叉排序树的性能(平均查找时间),规定在插入和删除结点时,要保证任意结点的左右子树高度差的绝对值不超过1,将这样的二叉树称为平衡二叉树。(如左边二叉树,左子树高度为2,右子树高度为2,右边二叉树。右子树高度为5)
定义:
平衡二叉树,又称为 AVL 树。具有以下性质
- 每棵子树的左子树和右子树的高度差不能超过 1;
- 其左子树和右子树都是平衡二叉树;
平衡因子:左子树与右子树高度差,取值只可能是:0、1 和 -1。
插入:
插入结点时,首先检查是否此次插入会导致不平衡,若是则找到离插入点最近且平衡因子绝对值大于1的结点A,然后对A为根的子树进行插入和调整平衡操作。
我们以序列
{13,24,37,90,53}
构建平衡二叉树为例平衡调整操作介绍:
单向右旋平衡处理(LL型):A结点左孩子(L)的左子树(L)插入新节点导致的不平衡,需要进行一次右旋操作(B向右旋转到A位置,B的右子树给A)
单向左旋平衡处理(RR型):A结点右孩子(R)的右子树(R)插入新节点导致的不平衡,需要进行一次左旋操作(B向左旋转到A位置,B的左子树给A)
先左后右双向旋转平衡处理(LR型):A结点左孩子(L)的右子树(R)插入新结点导致的不平衡,需要进行先左旋后右旋的操作(C转到B位置,C再转到A位置且C的左子树给B,)
左旋转:
右旋转:
先右后左双向旋转平衡处理(RL型):A结点右孩子(R)的左子树(L)插入新结点导致的不平衡,需要进行先右旋后左旋的操作(C转到B位置,C再转到A位置且C的左子树给A)
注意:
A代表最开始平衡因子为2的点;
新结点插入C的左子树还是右子树不影响旋转过程 ;
为什么LR时插入结点在B,而RL时插入结点在A,因为平衡二叉树也是一颗二叉排序树,看下面的例子会更清晰:序列{15,3,7,10,9,8}依次插入
如从i->j的过程 ,8与其他数字的大小关系决定了他的位置
若改一下序列为{4,7,10,2,6,5},可以看到5的位置与上述情况的不同
补充:在做题过程中发现先左后右(先右后左)还有其他的情况
左孩子右子树插入导致不平衡
右孩子左子树插入导致不平衡
查找:
平衡二叉树的查找和二叉排序树一致,含有n个结点的平衡二叉树最大深度为 (即),故平均查找长度为
平衡二叉树结点最少时:
如图h=4时,n=4+2+1
哈夫曼树(最优二叉树)和哈夫曼编码:
结点的权:树中结点常常被赋予一个表示某种意义的值,称为该结点的权。图 1 中结点 a 的权为 7,结点 b 的权为 5。
结点的带权路径长度:指的是从根结点到任意结点的路径长度(经过的边数)与该结点的权的乘积,图 1 中结点 b 的带权路径长度为 2 * 5 = 10 。
树的带权路径长度:为树中所有叶子结点的带权路径长度之和。通常记作 “WPL”,图 1 中所示的这颗树的带权路径长度为:WPL = 7 * 1 + 5 * 2 + 2 * 3 + 4 * 3。
图 1 中从根结点到结点 c 的路径长度为 3
哈夫曼树的定义:在含有n个带权叶结点的二叉树中,带权路径长度最小的二叉树称为哈夫曼树,又叫最优二叉树。例如:
构建哈夫曼树的方法(给定n个权值分别为w1,w2....wn的结点):
- 将n个结点分别作为只含一个结点的二叉树构成森林F。
- 构造一个新结点,从F中选取两颗根节点权值最小的树作为新结点的左右子树,将新结点的权值赋值为左右子树权值之和。
- 从F中删除刚才选出的两颗树,同时将新得到的树加入F中。
- 重复2和3,直至F中只剩一棵树为止
哈夫曼树的特点:
- 每个初始结点都最终成为叶结点,权值最小的结点路径长度最大
- 构造过程中新建了n-1个结点,哈夫曼树结点总数为2n-1
- 哈夫曼树不存在度为1的结点(每次都选俩个作为新结点的孩子)
哈夫曼编码:
哈夫曼编码就是在哈夫曼树的基础上构建的,哈夫曼树中的每一个子树,统一规定其左孩子标记为 0 ,右孩子标记为 1。(哈夫曼编码用于在数据通信中表示不同的字符,是一种可变长度编码。哈夫曼编码不唯一,因为也可以左孩子用1表示,右孩子用0表示。若干相同的结点构造出的哈夫曼树不同但带权路径长度相同)
此哈夫曼树的带权路径长度为:WPL=1*45+3*(13+12+16)+4*(5+9)=224,若使用3位固定长度编码要300位,哈夫曼编码压缩了25%的数据。
使用程序求哈夫曼编码有两种方法:
- 从叶子结点一直找到根结点,逆向记录途中经过的标记。例如,图 3 中字符 c 的哈夫曼编码从结点 c 开始一直找到根结点,结果为:0 1 1 ,所以字符 c 的哈夫曼编码为:1 1 0(逆序输出)
- 从根结点出发,一直到叶子结点,记录途中经过的标记。例如,求图 3 中字符 c 的哈夫曼编码,就从根结点开始,依次为:1 1 0
#include <stdlib.h> #include <string.h> #include <stdio.h> typedef struct { int weight;//结点权重 int parent,left,right;//父节点、左孩子、右孩子在数组中的下标 }HTNode,*HuffmanTree; typedef char **HuffmanCode;//存储哈夫曼编码 //需要每次根据各个结点的权重值,筛选出其中值最小的两个结点,end表示HT数组中存放结点的最终位置,s1和s2传递的是HT数组中权重值最小的两个结点在数组中的位置 void select(HuffmanTree HT,int end,int *s1,int *s2){ int min1,min2; int i=1; while(HT[i].parent!=0&&i<=end){ i++; } min1=HT[i].weight; *s1=i; i++; while(HT[i].parent!=0&&i<=end){ i++; } //俩个结点比较大小,min1为较小的 if(HT[i].weight<min1){ min2=min1; *s2=*s1; min1=HT[i].weight; *s1=i; }else{ min2=HT[i].weight; *s2=i; } //俩个结点和后续的所有未构成树的结点比较 for(int j=i+1;j<=end;j++){ if(HT[j].parent!=0){ continue; } if(HT[j].weight<min1){ min2=min1; min1=HT[j].weight; *s2=*s1; *s1=j; }else if(HT[j].weight>=min1&&HT[j].weight<min2){ min2=HT[j].weight; *s2=j; } } } //HT为存储哈夫曼树的数组,w为存储结点权值的数组,n为结点个数 void createHuffmanTree(HuffmanTree *HT,int *w,int n){ if(n<=1) return; int m=2*n-1; *HT=(HuffmanTree)malloc((m+1)* sizeof(HTNode)); HuffmanTree p= *HT; //初始化结点 for(int i=1;i<=n;i++){ (p+i)->weight=*(w+i-1); (p+i)->parent=0; (p+i)->left=0; (p+i)->right=0; } //初始化哈夫曼树中除叶子结点外的结点 for(int i=n+1;i<=m;i++){ (p+i)->weight = 0; (p+i)->parent = 0; (p+i)->left = 0; (p+i)->right = 0; } //构建 for(int i=n+1;i<=m;i++){ int s1,s2; select(*HT,i-1,&s1,&s2); (*HT)[s1].parent=(*HT)[s2].parent=i; (*HT)[i].left=s1; (*HT)[i].right=s2; (*HT)[i].weight=(*HT)[s1].weight+(*HT)[s2].weight; } } //HT为哈夫曼树,HC为存储结点哈夫曼编码的二维动态数组,n为结点的个数 void HuffmanCoding(HuffmanTree HT, HuffmanCode *HC,int n){ *HC = (HuffmanCode) malloc((n+1) * sizeof(char *)); int m=2*n-1; int p=m; int cdlen=0; char *cd = (char *)malloc(n*sizeof(char)); //将各个结点的权重用于记录访问结点的次数,首先初始化为0 for (int i=1; i<=m; i++) { HT[i].weight=0; } //一开始 p 初始化为 m,也就是从树根开始。一直到p为0 while (p) { //如果当前结点一次没有访问,进入这个if语句 if (HT[p].weight==0) { HT[p].weight=1;//重置访问次数为1 //如果有左孩子,则访问左孩子,并且存储走过的标记为0 if (HT[p].left!=0) { p=HT[p].left; cd[cdlen++]='0'; } //当前结点没有左孩子,也没有右孩子,说明为叶子结点,直接记录哈夫曼编码 else if(HT[p].right==0){ (*HC)[p]=(char*)malloc((cdlen+1)*sizeof(char)); cd[cdlen]='\0'; strcpy((*HC)[p], cd); } } //如果weight为1,说明访问过一次,即是从其左孩子返回的 else if(HT[p].weight==1){ HT[p].weight=2;//设置访问次数为2 //如果有右孩子,遍历右孩子,记录标记值 1 if (HT[p].right!=0) { p=HT[p].right; cd[cdlen++]='1'; } } //如果访问次数为 2,说明左右孩子都遍历完了,返回父结点 else{ HT[p].weight=0; p=HT[p].parent; --cdlen; } } } //打印哈夫曼编码的函数 void PrintHuffmanCode(HuffmanCode htable,int *w,int n) { printf("Huffman code : \n"); for(int i = 1; i <= n; i++) printf("%d code = %s\n",w[i-1], htable[i]); } void main(){ int w[5] = {2, 8, 7, 6, 5}; int n = 5; HuffmanTree htree; HuffmanCode htable; createHuffmanTree(&htree, w, n); HuffmanCoding(htree, &htable, n); PrintHuffmanCode(htable,w, n); }
补充:有的题中是用过字符的频数来构建哈夫曼树的,例如:A、B、C、D、E五个字符,出现的频率分别为2,5,3,3,4,然后我们根据构建时将俩个频率小的进行组合即可...,即
WPL=5*2+3*2+3*3+3*2+4*2=39
二叉树遍历和遍历过程中对结点的各种操作是重点。容易结合递归算法和利用栈和队列的非递归算法。请认真练习如下的例题:
6.图
图的基本概念
图的定义:由顶点的集合和顶点之间边的集合组成,通常表示为:G(V,E),G表示一个图,V是图中顶点的集合,E是图中边的集合(线性表可以是空表,树可以是空树,但图不可以是空图,必须有顶点,边可以没有)(数据之间的关系有 3 种,分别是 "一对一"、"一对多" 和 "多对多",前两种关系的数据可分别用线性表和树结构存储,"多对多"逻辑关系数据的结构用图结构存储)。
有向图:E是有向边(也叫弧)的有限集合,则图G为有向图。用<2,3>表示有向图中2到3带方向的线。例如:
无向图:E是无向边(简称边)的有限集合,则图G为无向图。用(1,2)表示无向图中连接1与2之间的线。例如:
简单图:一个图不存在重复边;不存在顶点到自身的边,称图为简单图。以上的俩个图均为简单图。
多重图:俩个顶点之间的边数大于1条,并可以有顶点和自身的边,则图称为多重图。
完全图(也叫简单完全图):n个结点和n(n-1)/2的条边的无向图称为完全图;n个结点n(n-1)条边的有向图称为有向完全图。G2和G3分别为完全图和有向完全图。
子图:有俩个图G1=(V1,E1)和G2=(V2,E2),其中V2是V1的子集,E2是E1的子集,则G2是G1的子图。 (并非V1和V2的任何子集都构成图,有的组合可能不是图)
连通图:在无向图中,若顶点A到顶点B有路径则称A和B连通。若无向图G中任意俩个顶点都连通的则称G为连通图。(如图4,A和C也是连通的)(无向图中,n个结点连通图至少n-1条边)
连通分量:无向图中的极大连通子图称为连通分量。图G4有三个连通分量。但G4不是连通图。(一个图有n个顶点,边数<n-1,则此图是非连通的)
强连通:在有向图中,从A到B,从B到A都有路径(可以是多条路径组合),则称A和B是强连通的。
强连通图:若有向图中任意两个顶点都是强连通的,则称为强连通图。(有向图中,n个结点强连通图至少n条边)
强连通分量:有向图中极大强连通子图称为强连通分量。(n个顶点的有向图至少需要n条边,才能是强连通图)
生成树:连通图中包含所有顶点的极小连通子图称为生成树。(生成树中n个结点有n-1条边)(极大连通子图要求包含所有原图的边;极小连通子图既要保证连通又要边数最少)
连通图中的生成树必须满足以下 2 个条件:
- 包含连通图中所有的顶点;
- 任意两顶点之间有且仅有一条通路;
生成森林:生成森林是对应非连通图来说的,非连通图可分解为多个连通分量,而每个连通分量又各自对应多个生成树,因此是由多棵生成树组成的生成森林。
顶点的度:无向图中,顶点的度即关联的边数;如G2中顶点1的度为3。无向图全部顶点的度之和=边数*2。有向图中,顶点的度分为入度和出度,入度指以顶点为终点的有向边数;出度指以顶点为起点的有向边数。如G1中顶点2的入度为1出度为2。有向图顶点的度为入度和出度的和。有向图全部顶点的入度之和=全部顶点的出度之和=边数。
边的权/网:在图中,每条边都可以标上具有某种意义的数值,该数值称为边的权值。这种有权值的图称为带权图,也叫网。
稀疏图/稠密图:边数很少的图称为稀疏图。反之为稠密图。稀疏和稠密的判断条件是:e<nlogn,其中 e 表示图中边(或弧)的数量,n 表示图中顶点的数量。如果式子成立,则为稀疏图;反之为稠密图。
路径/路径长度/回路(环):A到D的一条路径指顶点序列ABCD。路径上边的数目称为路径长度。第一个顶点和最后一个顶点相同的路径称为回路或环。(n个结点的图有大于n-1条边,则必存在环)
简单路径/简单回路:顶点不重复出现的路径称为简单路径。除第一个顶点和最后一个顶点,其余顶点不重复出现的回路称为简单回路。
距离:从A到D的最短路径的长度称为A到D的距离。若最短路径不存才,则记为无穷(∞)
有向树:一个顶点的入度为0,其余顶点的入度为1的有向图称为有向树
图的存储和基本操作
邻接矩阵法:用一个一维数组存储顶点的信息,用一个二维数组存储边的信息,这个二维数组称为邻接矩阵。设邻接矩阵为A[i][j],图中结点编号为v1,v2,...vn,则有
若是带权图,则邻接矩阵中保存对应的权值,若俩个顶点不相连则用0或∞表示
注:无向图的邻接矩阵是对称的;邻接矩阵的空间复杂度为O(n^2),n为顶点数。
#define MAXVertexNum 100; typedef struct { char Vex[MAXVertexNum];//顶点表 int Edge[MAXVertexNum][MAXVertexNum];//邻接矩阵表或边表 int vexnum,arcnum;//当前顶点个数和边数 }MGraph;
邻接矩阵存储表示法特点:
- 无向图的邻接矩阵是一个对称矩阵,实际存储时只需存上三角元素即可。
- 无向图的邻接矩阵第i行非零元素个数为顶点vi的度
- 有向图的邻接矩阵第i行非零元素个数为顶点vi的出度;第i列非零元素个数为顶点vi的入度
- 邻接矩阵便于确定俩个顶点是否相连,但不方便求边数
- 稠密图适合使用邻接矩阵法存储
算法:
(引用:图的邻接矩阵实现C语言_学习笔记,仅供参考-CSDN博客
/* 顶点数据类型为char,边上权值类型为int */ #include <stdio.h> #include <stdlib.h> #define MAXVEX 100 //最大顶点数 #define INFINITY 99999 //用99999表示无限大 //********************邻接矩阵存储结构代码******************** typedef struct { char verx[MAXVEX]; //顶点表 int arc[MAXVEX][MAXVEX]; //边表 int numVertexes, numEdges; //图中当前顶点数和边数 }MGraph; //********************建立无向图的邻接矩阵表示******************** void CreateMGraph(MGraph* G) { int i, j, k, w; printf("输入顶点数和边数\n"); scanf("%d%d", &G->numVertexes, &G->numEdges); getchar(); //获取缓冲区的回车符 for (i = 0; i < G->numVertexes; i++) //读入顶点信息 { printf("输入第%d个顶点信息\n",i+1); scanf("%c",&G->verx[i]); getchar(); //获取缓冲区的回车符 } for (i = 0; i < G->numVertexes; i++) //矩阵初始化 for (j = 0; j < G->numVertexes; j++) if (i == j) //如果i==j;矩阵的主对角线都为0 G->arc[i][j] = 0; else G->arc[i][j] = INFINITY; //否则,矩阵初始化,都是无穷大 for (k = 0; k < G->numEdges; k++) //建立邻接矩阵 { printf("输入边(vi,vj)上的下标i,下标j和权w:\n"); scanf("%d%d%d", &i, &j, &w); //输入边(vi,vj)上的权值w G->arc[i][j] = w; G->arc[j][i] = G->arc[i][j]; //因为无向图矩阵是对称的 } } //********************邻接矩阵的输出******************** void prt(MGraph G) { int i,j; for (i = 0; i < G.numVertexes; i++) { for (j = 0; j < G.numVertexes; j++) printf("%-10d", G.arc[i][j]); printf("\n"); } } int main() { MGraph G; CreateMGraph(&G); prt(G); }
邻接表法:对图中每个顶点建立一个单链表,链表中保存顶点对应的边,这个单链表称为边表,边表的头指针和顶点的数据信息采用顺序存储(称为顶点表)。即邻接表中有俩种结点:顶点表结点和边表结点。(n个顶点e条边的无向图,顶点表大小为n,所有链表结点为2e)
#define MaxVertNum 100 //边表结点(链式存储) typedef struct ArcNode{ int adjvex;//弧所指向的顶点位置 struct ArcNode *next;//指向边表结点 }ArcNode; //顶点表结点(顺序存储) typedef struct VNode{ int data;//顶点信息 ArcNode *first;//指向边表第一个结点 }VNode,AdjList[MaxVertNum]; //邻接表 typedef struct{ AdjList vertices;//邻接表 int verxnum,arcnum;//图的顶点数和弧数 }ALGraph;
特点:
当G为无向图时,邻接表的空间复杂度为O(结点数+2*边数),因为无向图中每条边在边表中会出现2次;当G为有向图时,邻接表的空间复杂度为O(结点数+边数);
邻接表存储适用于稀疏图的存储
邻接表中,可以很容易地找到它的所有邻边,但要确定俩个结点是否存在边则不容易。
邻接表求有向图的出度容易,但求入度则需要遍历全表。这时可以采用逆邻接表的存储方式求入度。
图的邻接表不唯一,因为每个顶点对应单链表中结点顺序是任意的,它取决于建立邻接表的算法和边的输入顺序。
算法:
/* 顶点数据类型为char,边上权值类型为int */ #include <stdio.h> #include <stdlib.h> #define MAXVEX 100 //最大顶点数 #define INFINITY 99999 //用99999表示无限大 //****************************边表结点**************************** typedef struct EdgeNode { int adjvex; //邻接点域,储存该顶点对应的下标 //int weight; //用于储存权值,非网图不需要 struct EdgeNode* next; //链域,指向下一个邻接结点 }EdgeNode; //****************************顶点表结点**************************** typedef struct VertexNode { char data; //顶点域存放顶点信息 EdgeNode* firstedge; //边表头指针 }VertexNode, AdjList[MAXVEX]; //****************************邻接表结构**************************** typedef struct GraphAdjList { AdjList adjlist; //顶点表结点数组 int numVertexes, numEdges; //图中当前顶点数和边数 }GraphAdjList; //**************************无向图邻接表的创建************************* void CreateALGraph(GraphAdjList* G) { int i, j, k; EdgeNode* e; printf("输入顶点数和边数\n"); scanf("%d%d", &G->numVertexes, &G->numEdges); getchar(); //获取缓冲区的 回车符 for (i = 0; i < G->numVertexes; i++) //读入顶点信息,建立顶点表 { printf("输入第%d个顶点值\n", i + 1); scanf("%c", &G->adjlist[i].data); //输入顶点信息 getchar(); //获取缓冲区的 回车符 G->adjlist[i].firstedge = NULL; //将边表置为空表 } for (k = 0; k < G->numEdges; k++) //建立边表 { printf("输入第%d条边(vi,vj)的下标:\n",k+1); scanf("%d%d", &i, &j); e = (EdgeNode*)malloc(sizeof(EdgeNode)); //向内存申请空间 if (e == NULL) { printf("内存申请失败\n"); exit(0); } e->adjvex = j; //这三步,类似于单链表的头插法 e->next = G->adjlist[i].firstedge; G->adjlist[i].firstedge = e; e = (EdgeNode*)malloc(sizeof(EdgeNode)); if (e == NULL) { printf("内存申请失败\n"); exit(0); } e->adjvex = i; e->next = G->adjlist[j].firstedge; G->adjlist[j].firstedge = e; } } //********************邻接矩阵的输出******************** void prt(GraphAdjList G) { EdgeNode* p; int i; for (i = 0; i < G.numVertexes; i++) //i小于当前顶点数 { p = G.adjlist[i].firstedge; //p指向第一个边表结点 while (p != NULL) { printf("边%c-%c\t", G.adjlist[i].data, G.adjlist[p->adjvex].data); p = p->next; //p指向下一个边表结点 } printf("\n"); } } int main() { GraphAdjList G; CreateALGraph(&G); prt(G); return 0; }
十字链表
定义:十字链表是有向图的一种链式存储结构,每条弧对应一个结点,每个顶点也对应一个结点。
这里顶点结点是顺序存储的
上面的图可能看的不太明白,结合下面的图就明白了(上面的图没有写出info域)
特点:
在十字链表中,既容易找到以Vi为尾的弧,又容易找到以Vi为头的弧,因为容易得到顶点的出度和入度。十字链表的存储形式是不唯一的,但都对一同一个有向图。(特殊矩阵的存储中有具体讲过十字链表)
邻接多重表
定义:邻接多重表是无向图的一种存储结构,每条边用一个结点表示
每个顶点也用一个结点表示
上述图表省略了mark域和info域,且由于是无向图,例如c和b之间的边可写成(null,2,add,1,add2,null)也可写成(null,1,add3,2,add4,null),图中是前者
邻接多重表与邻接表的差别在于:同一条边在邻接表中用俩个结点表示;而在邻接多重表中只用一个点表示。
图的基本操作
图的遍历
广度优先搜索(BFS——Breadth-First-Search)
基本思想:首先访问起始顶点v,然后依次访问v的各未访问过的邻接点v2,v3,...vi,然后依次访问v2,v3...vi的所有未被访问的邻接点,直到所有顶点都被访问过为止。
过程描述:首先从a开始访问,a入队,然后a出队并访问a的未被访问过的邻接点b、c、d,然后b、c、d入队;取出队头元素b,访问b的邻接点e并入队,c出队(因为e已经访问过了。不会再访问),d出队,访问d的邻接点f并入队;然后e出队,f出队(b、c、d已访问过),循环结束。
#define MAX_VERTEX_NUM 6 #include <stdlib.h> #include <stdbool.h> #include <stdio.h> bool visited[MAX_VERTEX_NUM]; typedef struct Graph{ char vertex[MAX_VERTEX_NUM]; int edge[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; int vexnum,arcnum; }Graph; typedef struct Node{ int data; struct Node *next; }Node,*pNode; typedef struct Queue{ pNode front; pNode rear; }Queue; void initQueue(Queue *qu){ pNode pHead=(pNode)malloc(sizeof(Node));//创建头结点,phead为指向头结点的指针 if(pHead==NULL){ exit(-1); } pHead->next=NULL; qu->front=qu->rear=pHead; } bool isEmpty(Queue *qu){ if(qu->front==qu->rear){ return true; }else{ return false; } } void push(Queue *qu,int data){ pNode pNew=(pNode)malloc(sizeof(Node)); pNew->data=data; pNew->next=NULL; qu->rear->next=pNew;//qu->rear是头结点 qu->rear=pNew; } int pop(Queue *qu){ if(isEmpty(qu)){ printf("栈空"); exit(-1); } pNode p=qu->front->next; qu->front->next=p->next; int e=p->data; if(qu->front->next==NULL){//如果原队列只有头结点时 qu->rear=qu->front; } free(p); return e; } void printGraph(Graph G){ printf("顶点序列为:"); for(int v=0;v<G.vexnum;v++){ printf("%c",G.vertex[v]); } printf("\n"); printf("边的二维数组关系为:\n"); for(int i=0;i<G.vexnum;i++){ for(int j=0;j<G.vexnum;j++){ printf("%d",G.edge[i][j]); } printf("\n"); } printf("\n"); } int Location(Graph G,char e){ for(int v=0;v<G.vexnum;v++){ if(G.vertex[v]==e){ return v; } } return -1; } void createGraph(Graph *G,int vexnum,int arcnum,char vertex[],char arc[]){ G->vexnum=vexnum; G->arcnum=arcnum; for(int k=0;k<G->vexnum;k++){ G->vertex[k]=vertex[k];//顶点表赋值 } for(int i=0;i<G->vexnum;i++){ for(int j=0;j<G->vexnum;j++){ G->edge[i][j]=0;//初始化边表 } } int t=0; int p,q; for(int k=0;k<G->arcnum;k++){ char m,n; m=arc[t++]; n=arc[t++]; t++; p=Location(*G,m);//获取m对应的顶点序号 q=Location(*G,n);//获取n对应的顶点序号 G->edge[p][q]=1; G->edge[q][p]=1; } } void visit(Graph G,int v){ printf("访问的是%c结点\n",G.vertex[v]); } //求第i行的第一个邻接顶点 int FirstNeighbor(Graph G,int i){ if(i<0||i>=G.vexnum){ return -1; } for(int v=0;v<G.vexnum;v++){ if(G.edge[i][v]==1){ return v; } } return -1; } //求从i行j列开始的下一个邻接顶点 int NextNeighbor(Graph G,int i,int j){ if(i<0||i>=G.vexnum){ return -1; } if(j<0||j>G.vexnum){ return -1; } for(int v=j+1;v<G.vexnum;v++){ if(G.edge[i][v]==1){ return v; } } return -1; } void BFS(Graph G,int v,Queue *qu){ visit(G,v); visited[v]=true; push(qu,v);//顶点入队 while(!isEmpty(qu)){ v=pop(qu);//顶点出队 for(int w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)){ if(!visited[w]){ visit(G,w);//如果顶点没有被访问过则访问 visited[w]=true; push(qu,w);//顶点入队 } } } } void BFSTraverse(Graph G){ for(int i=0;i<G.vexnum;i++){ visited[i]=false;//初始化访问记录表 } Queue Q; initQueue(&Q);//初始化辅助队列 for(int i=0;i<G.vexnum;i++){ if(!visited[i]){ BFS(G,i,&Q);//若顶点未被访问则执行遍历操作 } } } void main(){ Graph G; char vert[]={'a','b','c','d','e','f'}; char arc[]="ab,ac,ad,be,ce,df"; createGraph(&G,6,6,vert,arc); printGraph(G); BFSTraverse(G); }
图的广度优先搜索与二叉树的层序遍历是完全一致的,都用到队列。
BFS算法需要借助一个辅助队列,n个顶点均需入队一次,故空间复杂度为O(顶点个数);采用邻接表存储的时间复杂度为O(顶点数+边数),采用邻接矩阵方法存储的时间复杂度为O(顶点数的平方)
BFS算法求单源最短路径问题:
最短路径:在非带权图中,顶点u到顶点v的任何路径中边数最少的路径。若顶点u到顶点v没有通路则最短路径为无穷大(∞)。BFS可以求解单源最短路径问题,因为BFS总是由近到远来遍历每个顶点的。
int d[MAX_VERTEX_NUM]; //其余代码和上面BFS的代码都类似就不贴出来了 void BFSMinDistance(Graph G,int u,Queue *Q){ visited[u]=true; d[u]=0; push(Q,u); while(!isEmpty(Q)){ u=pop(Q); for(int w=FirstNeighbor(G,u);w>=0;w=NextNeighbor(G,u,w)){ if(!visited[w]){ visited[w]=true; d[w]=d[u]+1; push(Q,w); } } } } void oneOriginMinDistance(Graph G){ for(int i=0;i<G.vexnum;i++){ visited[i]=false;//初始化访问记录表 } for(int i=0;i<G.vexnum;i++){ d[i]=infinity; } Queue Q; initQueue(&Q);//初始化辅助队列 for(int i=0;i<G.vexnum;i++){ if(!visited[i]){ BFSMinDistance(G,i,&Q);//求第一个结点到其他结点的最短路径 } } }
广度优先生成树:
在广度遍历过程中,我们可以得到一颗遍历树,称为广度优先生成树。由于邻接表的存储表示不唯一,故其广度优先生成树也不唯一。图中第一个访问结点为2
深度优先搜索(DFS——Depth-First-Search)
基本思想:
首先访问某一起始顶点v,然后访问与v邻接且未被访问的任一顶点w1,再访问w1邻接且未被访问的任一顶点w2,...重复上述过程。当不能再继续访问时,依次退回到最近被访问的顶点,继续访问其邻接且未被访问的顶点,直到所有顶点均被访问。
//其他相关代码均与BFS算法一致,这里不再贴出 void DFS(Graph G,int v){ visit(G,v); visited[v]=true; for(int w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)){ if(!visited[w]){ DFS(G,w); } } } void DFSTraverse(Graph G){ for(int i=0;i<G.vexnum;i++){ visited[i]=false;//初始化访问记录表 } for(int i=0;i<G.vexnum;i++){ if(!visited[i]){ DFS(G,i);//若顶点未被访问则执行遍历操作 } } } void main(){ Graph G; char vert[]={'a','b','c','d','e','f'}; char arc[]="ab,ac,ad,be,ce,df"; createGraph(&G,6,6,vert,arc); printGraph(G); DFSTraverse(G); }
基于邻接矩阵的DFS遍历序列和BFS序列是唯一的;基于邻接表的DFS和BFS序列是不唯一的。
DFS算法需借助递归工作栈,其空间复杂度为O(顶点数);基于邻接矩阵的DFS算法的时间复杂度为O(顶点数的平方),基于邻接表的DFS算法的时间复杂度为O(顶点数+边数)
深度优先生成树和生成森林
对连通图使用DFS才能生成深度优先生成树,否则生成深度优先生成森林。基于邻接表的生成树不唯一。
使用图的遍历判断图的连通性
原理:对于无向图,若无向图连通,则从一点出发一次遍历就能访问图中所有顶点;若无向图不连通,则从某一点出发,一次遍历只能该点所在连通分量的所有点,其他顶点需要执行多次遍历访问。对于有向图,若初始点到其他顶点都有路径则可一次遍历,否则不能一次遍历。
图的应用
最小生成树(Minimum-Spanning-Tree,MST):
一个带权连通无向图G,权值之和最小的生成树为最小生成树。
最小生成树不唯一(因为权值之和可能相等);最小生成树虽不唯一但其权值之和是唯一的最小的;最小生成树的边数=顶点数-1
每次加入一条边最终形成目标树,以下俩种具体算法都是应用了这个生成思想
Prim(普里姆)算法
初始时,从图中任取一顶点加入树T,然后选择一个离T中顶点集合最近的顶点,并将该顶点和相应的边加入T,每次操作后T中顶点数和边数都增加1。以此类推,直到所有的顶点都加入到T中,便得到了最小生成树。
算法设计:设最小生成树的顶点集合为U1,最小生成树边的集合为T1,原图顶点集合为U2,原图边的集合为T2
- 任选图中一个顶点加入U1
- 选取U1中点与U2中点连接成最小的邻边,将该边在U2中的顶点移动到U1,将此边加入T1中
- 重复步骤2直到边数=顶点数-1
#define MAX_VALUE 9999 #include "test9_3.h" //定义最短边 typedef struct { char adjvex;//最短边对应的顶点 int lowcost;//最短边的权值 }shortEdge; //查找节点名对应的数组下标,如a对应0 int LocateVex(Graph G,char v){ for(int i=0;i<G.vexnum;i++){ if(v==G.vertex[i]){ return i; } } return -1; } void printValueGraph(Graph G){ printf("顶点序列为:"); for(int v=0;v<G.vexnum;v++){ printf("%c",G.vertex[v]); } printf("\n"); printf("边的二维数组关系为:\n"); for(int i=0;i<G.vexnum;i++){ for(int j=0;j<G.vexnum;j++){ printf("%4d ",G.edge[i][j]); } printf("\n"); } printf("\n"); } void createValueGraph(Graph *G,int vexnum,int arcnum,char vertex[],char arc[],int val[]){ G->vexnum=vexnum; G->arcnum=arcnum; for(int k=0;k<G->vexnum;k++){ G->vertex[k]=vertex[k];//顶点表赋值 } for(int i=0;i<G->vexnum;i++){ for(int j=0;j<G->vexnum;j++){ G->edge[i][j]=MAX_VALUE;//初始化边表 } } int t=0; int p,q; for(int k=0;k<G->arcnum;k++){ char m,n; m=arc[t++]; n=arc[t++];//从arc边关系数组中获取对应的顶点名称 t++; p=Location(*G,m);//获取m对应的顶点序号,如传入为a则返回0 q=Location(*G,n);//获取n对应的顶点序号 G->edge[p][q]=val[k]; G->edge[q][p]=val[k]; } } //返回最短边关联顶点的下标,如这里a与其他个点,比较后选出顶点b最短,返回1 int minimal(Graph G,shortEdge *shortEdge){ int location; int min=MAX_VALUE; for(int i=0;i<G.vexnum;i++){ int lowcost=shortEdge[i].lowcost; if(lowcost<min&&lowcost!=0){ min=lowcost; location=i; } } return location; } void MiniSpanTree_Prim(Graph G,char start){ shortEdge shortEdge[MAX_VERTEX_NUM]; int k=LocateVex(G,start); for(int i=0;i<G.vexnum;i++){//初始化短边表 shortEdge[i].adjvex=start;//每条边都与start相关联 shortEdge[i].lowcost=G.edge[k][i]; } shortEdge[k].lowcost=0; for(int i=0;i<G.vexnum-1;i++){ k=minimal(G,shortEdge); printf("%c->%c,%d\n",shortEdge[k].adjvex,G.vertex[k],shortEdge[k].lowcost);//输出生成树的边及权值 shortEdge[k].lowcost=0; for(int j=0;j<G.vexnum;j++){ if(G.edge[k][j]<shortEdge[j].lowcost){ shortEdge[j].lowcost=G.edge[k][j]; shortEdge[j].adjvex=G.vertex[k]; } } } } void main(){ Graph G; char vert[]={'a','b','c','d','e','f'}; char arc[]="ab,ac,ad,be,ce,df"; int val[]={15,35,57,9,17,90}; createValueGraph(&G,6,6,vert,arc,val); printValueGraph(G); MiniSpanTree_Prim(G,'a'); }
Prim算法的时间复杂度为O(顶点数的平方),适用于求边稠密的图的最小生成树。
Krasual(克鲁斯卡尔)算法
该算法是一种按权值递增次序来选择边构造最小生成树的方法。初始时为只有n个顶点的非连通图T,每个顶点自成一个连通分量,然后按边的权值由小到大选择未被选择过且权值最小的边,若该边关联的顶点落在不同的连通分量上,将边加入T,否则舍弃此边选择下一权值最小的边。依次类推,直到所有顶点都在一个连通分量上。(如何理解绿字部分:比如下图中有两个权值为5的边,v3和v4的边属于同一连通分量,故舍弃这条边)
算法设计:设最小生成树的顶点集合为U1,最小生成树边的集合为T1,原图顶点集合为U2,原图边的集合为T2
- 将原图中的顶点加入到T1
- 任选两个顶点连线使得边的权值最小,若把该边加入到T1中不会形成回路(形成回路就不是树了)则加入,否则不加入
- 重复步骤2直到边数=顶点数-1
#define MAX_VERTEX_NUM 6 typedef struct { char begin; char end; int weight; }Edge;//定义边 typedef struct { char vertex[MAX_VERTEX_NUM];//顶点数组 Edge edge[MAX_VERTEX_NUM];//自定义边数组 int vexnum,arcnum; }EdgeGraph; int Location(EdgeGraph G,char e){ for(int v=0;v<G.vexnum;v++){ if(G.vertex[v]==e){ return v; } } return -1; } //查找节点名对应的数组下标,如a对应0 int LocateVex(EdgeGraph G,char v){ for(int i=0;i<G.vexnum;i++){ if(v==G.vertex[i]){ return i; } } return -1; } void printValueGraph(EdgeGraph G){ printf("顶点序列为:"); for(int v=0;v<G.vexnum;v++){ printf("%c",G.vertex[v]); } printf("\n"); printf("边的二维数组关系为:\n"); for(int i=0;i<G.vexnum;i++){ printf("%c->%c,%d\n",G.edge[i].begin,G.edge[i].end,G.edge[i].weight); } printf("\n"); } void createValueGraph(EdgeGraph *G,int vexnum,int arcnum,char vertex[],char arc[],int val[]){ G->vexnum=vexnum; G->arcnum=arcnum; for(int k=0;k<G->vexnum;k++){ G->vertex[k]=vertex[k];//顶点表赋值 } for(int i=0;i<G->vexnum;i++){ G->edge[i].begin=0;//初始化边表 G->edge[i].end=0;//初始化边表 G->edge[i].weight=MAX_VERTEX_NUM;//初始化边表 } int t=0; int p,q; for(int k=0;k<G->arcnum;k++){ char m,n; m=arc[t++]; n=arc[t++];//从arc边关系数组中获取对应的顶点名称 t++; // p=Location(*G,m);//获取m对应的顶点序号,如传入为a则返回0 q=Location(*G,n);//获取n对应的顶点序号 G->edge[k].begin=m; G->edge[k].end=n; G->edge[k].weight=val[k]; } } //冒泡排序 void sort(EdgeGraph *G){ Edge temp; for(int i=0;i<G->arcnum-1;i++){ for(int j=i+1;j<G->arcnum;j++){ if(G->edge[i].weight>G->edge[j].weight){ temp=G->edge[i]; G->edge[i]=G->edge[j]; G->edge[j]=temp; } } } } void print(EdgeGraph G){ for(int i=0;i<G.arcnum;i++){ printf("%c->%c,%d\n",G.edge[i].begin,G.edge[i].end,G.edge[i].weight); } } //查找根节点 int FindRoot(int t,int parent[]){ while(parent[t]>-1){// t=parent[t]; } return t; } void MiniSpanTree_Kruskal(EdgeGraph *G){ int root1,root2; int LocVex1,LocVex2; int parent[MAX_VERTEX_NUM];//用于查找顶点的双亲、两个顶点是否有共同的双亲、生成树是否有环 sort(G); printValueGraph(*G); for(int i=0;i<G->vexnum;i++){ parent[i]=-1;//初始化parent数组,-1代表没有双亲节点+ } printf("最小生成树为:\n"); int num=0; for(int i=0;i<G->arcnum;i++){ LocVex1=LocateVex(*G,G->edge[i].begin); LocVex2=LocateVex(*G,G->edge[i].end); root1=FindRoot(LocVex1,parent); root2=FindRoot(LocVex2,parent); if(root1!=root2){//若不相等故不成环 printf("%c->%c,%d\n",G->edge[i].begin,G->edge[i].end,G->edge[i].weight); parent[root2]=root1; num++; if(num==G->vexnum-1){//若num=顶点+1,代表树生成完毕,提前返回 return; } } } } void main(){ EdgeGraph G; char vert[]={'a','b','c','d','e','f'}; char arc[]="ab,ac,ad,be,ce,df"; int val[]={15,35,57,9,17,90}; createValueGraph(&G,6,6,vert,arc,val); MiniSpanTree_Kruskal(&G); }
Kruskal算法中,采用堆来存放边的集合,因此每次选择权值最小边的时间复杂度为O(log(边数)),采用并查集的数据结构来描述T,从而构造T的时间复杂度为O(边数*log(边数)),kruskal算法适用于边稀疏而顶点比较多的图。
最短路径
广度优先搜索查找最短路径是针对无权图的,当图是带权图时,把一个顶点v0到图中任一顶点vi的一条路径(可能多条)所在边的权值之和称为带权路径长度。把带权路径长度最短的路径称为最短路径(从某顶点出发,沿图的边到达另一顶点所经过的路径中,各边上权值之和最小的一条路径叫做最短路径)。
带权有向图G的最短路径问题分为两类:一是单源最短路径(求图中某一顶点到其他顶点的最短路径,可用Dijkstra算法求解);二是求每对顶点间的最短路径(可用Floyd算法求解)。
迪杰斯塔拉(Dijkstra)算法——单源最短路径
过程如下:
- 初始时,集合S为{0},dist[i]=arcs[0][i](i=1,2,...n-1);
- 选择dist[i]中值最小的顶点加入到S;
- 修改从v0出发到其他顶点的最短路径(dist[k]=dist[j]+arcs[j][k])
- 重复2-3,知道所有顶点都加入到S。
步骤3的更新过程:如图,当S={v0,v1}时,dist[1]=3,dist[2]=7,则更新后dist[2]=4
Dijkstra使用邻接矩阵和邻接表,时间复杂度均为O(顶点数^2),且Dijkstra不适用于边上有负权值的情况。
#include<stdio.h> #include<stdlib.h> #define MaxVexNum 50 #define MaxInt 32767 #define MaxEdgeNum 50 //邻接矩阵 typedef int VertexType; typedef int EdgeType; typedef struct AMGraph{ VertexType vexs[MaxVexNum];//顶点表 EdgeType arcs[MaxVexNum][MaxVexNum];//邻接矩阵表 int vexnum,edgenum;//顶点数,边数 }AMGraph; void createGraph(AMGraph *g){//创建无向图 printf("请输入顶点数:"); scanf("%d",&g->vexnum); printf("\n请输入边数:"); scanf("%d",&g->edgenum); //初始化顶点表 for(int i=0;i<g->vexnum;i++){ g->vexs[i]=i; } for(int i=0;i<g->vexnum;i++){ for(int j=0;j<g->vexnum;j++){ g->arcs[i][j]=MaxInt; if(i==j) g->arcs[i][j]=0; } } printf("请输入边的信息以及边的权值(顶点是0~n-1)\n"); for(int i=0;i<g->edgenum;i++){ int x,y,w; scanf("%d%d%d",&x,&y,&w); g->arcs[x][y]=w; //g.arcs[y][x]=w; } } void PrintGraph(AMGraph g){ printf("邻接矩阵为:\n"); for(int i=0;i<g.vexnum;i++) { printf(" %d",g.vexs[i]); } printf("\n"); for(int i=0;i<g.vexnum;i++){ printf("%d ",g.vexs[i]); for(int j=0;j<g.vexnum;j++){ if(g.arcs[i][j]==32767){ printf("∞ "); }else{ printf("%d ",g.arcs[i][j]); } } printf("\n"); } } //Dijkstra算法,求单源最短路径 void Dijkstra(AMGraph g,int dist[],int path[],int v0){ int n=g.vexnum,v; int set[n];//set数组用于记录该顶点是否归并 //第一步:初始化 for(int i=0;i<n;i++){ set[i]=0; dist[i]=g.arcs[v0][i]; if(dist[i]<MaxInt){//若距离小于MaxInt说明两点之间有路可通 path[i]=v0;//则更新路径i的前驱为v }else{ path[i]=-1; //表示这两点之间没有边 } } set[v0]=1;//将初始顶点并入 path[v0]=-1;//初始顶点没有前驱 //第二步 for(int i=1;i<n;i++){//共n-1个顶点 int min=MaxInt; //第二步:从i=1开始依次选一个距离顶点的最近顶点 for(int j=0;j<n;j++){ if(set[j]==0&&dist[j]<min){ v=j; min=dist[j]; } } //将顶点并入 set[v]=1; //第三步:在将新结点并入后,其初始顶点v0到各顶点的距离将会发生变化,所以需要更新dist[]数组 for(int j=0;j<n;j++){ if(set[j]==0&&dist[v]+g.arcs[v][j]<dist[j]){ dist[j]=dist[v]+g.arcs[v][j]; path[j]=v; } } } //输出 printf(" "); for(int i=0;i<n;i++) printf("%3d ",i); printf("\ndist[]:"); for(int i=0;i<n;i++) printf("%3d ",dist[i]); printf("\npath[]:"); for(int i=0;i<n;i++) printf("%3d ",path[i]); } int main(){ AMGraph g; createGraph(&g); int dist[g.vexnum]; int path[g.vexnum]; Dijkstra(g,dist,path,0); }
Dijkstra算法和Prim算法的比较:
- 给一堆村子之间修路,保证花费最小,用prim算法;从一个村子到其他所有村子修路,并且希望花费最小,用Dijkstra。
- prim适用于无向连通图;Dijkstra则适用于有向图;
- 松弛条件不一样:prim是从S到V-S进行松弛(V是所有节点集合,S是加入最小生成树的集合);而Dijkstra的松弛操作是从单源点到其他所有节点的松弛。
弗洛伊德(Floyd)算法——所有顶点间的最短路径
已知一个权值为正数的带权有向图,任取俩个不相同的顶点,求他们之间的最短路径。
其中:
D(-1)[i][j]=arcs[i][j](arcs是原图的邻接矩阵)
D(0)[i][j]:代表将v0作为中间顶点时,vi到vj的最短路径长度(如图v2到v1的中间顶点为v0)
D(k)[i][j]:代表将vk作为中间顶点时,vi到vj的最短路径长度
D(n-1)[i][j]:代表vi到vj的最短路径长度(n为原图中顶点个数)(如图D(2)即为算法最终结果)
P(0)[i][j]:代表将v0作为中间顶点时,vi到vj的最短路径
P(k)[i][j]:代表将vk作为中间顶点时,vi到vj的最短路径
P(n-1)[i][j]:代表vi到vj的最短路径(n为原图中顶点个数)
Floyd算法的时间复杂度为O(顶点数^3),允许图中带有负权值的边,但不允许有带负权值边组成的回路,且适用于带权无向图。也可多次使用Dijkstra算法求任意俩个顶点的最短路径。
#include<stdio.h> #include<stdlib.h> #define MaxVexNum 50 #define MaxInt 32767 #define MaxEdgeNum 50 //邻接矩阵 typedef int VertexType; typedef int EdgeType; typedef struct AMGraph{ VertexType vexs[MaxVexNum];//顶点表 EdgeType arcs[MaxVexNum][MaxVexNum];//邻接矩阵表 int vexnum,edgenum;//顶点数,边数 }AMGraph; void createGraph(AMGraph *g){//创建无向图 printf("请输入顶点数:"); scanf("%d",&g->vexnum); printf("\n请输入边数:"); scanf("%d",&g->edgenum); //初始化顶点表 for(int i=0;i<g->vexnum;i++){ g->vexs[i]=i; } for(int i=0;i<g->vexnum;i++){ for(int j=0;j<g->vexnum;j++){ g->arcs[i][j]=MaxInt; if(i==j) g->arcs[i][j]=0; } } printf("请输入边的信息以及边的权值(顶点是0~n-1)\n"); for(int i=0;i<g->edgenum;i++){ int x,y,w; scanf("%d%d%d",&x,&y,&w); g->arcs[x][y]=w; //g.arcs[y][x]=w; } } void PrintGraph(AMGraph g){ printf("邻接矩阵为:\n"); for(int i=0;i<g.vexnum;i++) { printf(" %d",g.vexs[i]); } printf("\n"); for(int i=0;i<g.vexnum;i++){ printf("%d ",g.vexs[i]); for(int j=0;j<g.vexnum;j++){ if(g.arcs[i][j]==32767){ printf("%2s ","--"); }else{ printf("%2d ",g.arcs[i][j]); } } printf("\n"); } } //Floyd算法 //递归输出两个顶点直接最短路径 void printPath(int u,int v,int path[][MaxVexNum]){ if(path[u][v]==-1){ printf("[%d %d] ",u,v); }else{ int mid=path[u][v]; printPath(u,mid,path); printPath(mid,v,path); } } void Floyd(AMGraph g,int path[][MaxVexNum]){ int n=g.vexnum; int A[n][n]; //第一步:初始化path[][]和A[][]数组 for(int i=0;i<n;i++){ for(int j=0;j<n;j++){ A[i][j]=g.arcs[i][j]; path[i][j]=-1; } } //第二步:三重循环,寻找最短路径 for(int v=0;v<n;v++){//第一层是代表中间结点 for(int i=0;i<n;i++){ for(int j=0;j<n;j++){ if(A[i][j]>A[i][v]+A[v][j]){ A[i][j]=A[i][v]+A[v][j]; path[i][j]=v; } } } } } int main(){ AMGraph g; createGraph(&g); PrintGraph(g); int path[MaxVexNum][MaxVexNum]; Floyd(g,path); printPath(0,4,path); }
有向无环图描述表达式
定义:若一个有向图中不存在环,则称其为有向无环图。简称DAG图。
有向无环图是描述表达式的有效工具:如((a+b)*(b*(c+d))+(c+d)*e)*((c+d)*e)既可以用二叉树表示,又可以用有向无环图表示。但有向无环图可以对相同的子式共享,从而节省存储空间。( 如(c+d)和(c+d)*e )
拓扑排序
AOV网:把顶点表示活动、边表示活动间先后关系的有向无环图称做顶点活动网(Activity On Vertex network),简称AOV网。(边的关系解释:vi是vj的直接前驱,vj是vi的直接后继,且不能以自己本身作为直接前驱和直接后继)。而拓扑排序就是用于判断顶点活动网中是否存在环!
拓扑排序序列满足:
- 每个顶点出现且只出现一次;
- 若顶点A排在B前,则图中不存在B到A的路径。
判定给定AOV网是否存在环的方法:
- 网中的顶点都在其拓扑序列中则一定不存在环。
拓扑排序是对有向无环图的顶点的一种排序,拓扑排序的思想是:
- 从AOV网中选择一个没有前驱(入度为0)的顶点并输出;
- 从AOV网中删去此顶点和以它为起点的有向边;
- 重复上述两步,直到输出全部顶点或者不存在无前驱(入度为0)的顶点为止(后面的情况表明图中有环)
每轮选择入度为0的顶点并输出,然后删除该顶点和以它为起点的有向边,重复操作,知道所有的顶点均被输出或由于存在环而结束。
#include "stdio.h" #include "stdlib.h" #include "io.h" #include "math.h" #include "time.h" #define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 #define MAXEDGE 20 #define MAXVEX 14 typedef struct LNode{ int data; struct linkStack *next; }LNode,*linkStack; typedef struct Stack{ linkStack top;//栈顶指针 int size; }LinkStack; typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */ /* 邻接矩阵结构 */ typedef struct { int vexs[MAXVEX]; int arc[MAXVEX][MAXVEX]; int numVertexes, numEdges; }MGraph; /* 邻接表结构****************** */ typedef struct EdgeNode /* 边表结点 */ { int adjvex; /* 邻接点域,存储该顶点对应的下标 */ int weight; /* 用于存储权值,对于非网图可以不需要 */ struct EdgeNode *next; /* 链域,指向下一个邻接点 */ }EdgeNode; typedef struct VertexNode /* 顶点表结点 */ { int in; /* 顶点入度 */ int data; /* 顶点域,存储顶点信息 */ EdgeNode *firstedge;/* 边表头指针 */ }VertexNode, AdjList[MAXVEX]; typedef struct { AdjList adjList; int numVertexes,numEdges; /* 图中当前顶点数和边数 */ }graphAdjList,*GraphAdjList; /* **************************** */ void CreateMGraph(MGraph *G)/* 构件图 */ { int i, j; /* printf("请输入边数和顶点数:"); */ G->numEdges=MAXEDGE; G->numVertexes=MAXVEX; for (i = 0; i < G->numVertexes; i++)/* 初始化图 */ { G->vexs[i]=i; } for (i = 0; i < G->numVertexes; i++)/* 初始化图 */ { for ( j = 0; j < G->numVertexes; j++) { G->arc[i][j]=0; } } G->arc[0][4]=1; G->arc[0][5]=1; G->arc[0][11]=1; G->arc[1][2]=1; G->arc[1][4]=1; G->arc[1][8]=1; G->arc[2][5]=1; G->arc[2][6]=1; G->arc[2][9]=1; G->arc[3][2]=1; G->arc[3][13]=1; G->arc[4][7]=1; G->arc[5][8]=1; G->arc[5][12]=1; G->arc[6][5]=1; G->arc[8][7]=1; G->arc[9][10]=1; G->arc[9][11]=1; G->arc[10][13]=1; G->arc[12][9]=1; } /* 利用邻接矩阵构建邻接表 */ void CreateALGraph(MGraph G,GraphAdjList *GL) { int i,j; EdgeNode *e; *GL = (GraphAdjList)malloc(sizeof(graphAdjList)); (*GL)->numVertexes=G.numVertexes; (*GL)->numEdges=G.numEdges; for(i= 0;i <G.numVertexes;i++) /* 读入顶点信息,建立顶点表 */ { (*GL)->adjList[i].in=0; (*GL)->adjList[i].data=G.vexs[i]; (*GL)->adjList[i].firstedge=NULL; /* 将边表置为空表 */ } for(i=0;i<G.numVertexes;i++) /* 建立边表 */ { for(j=0;j<G.numVertexes;j++) { if (G.arc[i][j]==1) { e=(EdgeNode *)malloc(sizeof(EdgeNode)); e->adjvex=j; /* 邻接序号为j */ e->next=(*GL)->adjList[i].firstedge; /* 将当前顶点上的指向的结点指针赋值给e */ (*GL)->adjList[i].firstedge=e; /* 将当前顶点的指针指向e */ (*GL)->adjList[j].in++; } } } } //初始化链栈,带头结点 LinkStack initLinkStack(LinkStack *S){ S->top=(linkStack)malloc(sizeof(LNode)); if(S->top==NULL){ printf("分配失败!"); exit(-1); } S->top->next=NULL; S->size=0; } bool isEmpty(LinkStack *S){ if(S->top->next==NULL){ return true; }else{ return false; } } //入栈 void push(LinkStack *S,int data){ // printf("元素入栈:%d\n",data); linkStack pNew=(linkStack)malloc(sizeof(LNode)); pNew->data=data; pNew->next=S->top; S->top=pNew; S->size++; } //出栈 int pop(LinkStack *S){ linkStack pNew; int e=S->top->data; pNew=S->top; S->top=pNew->next; free(pNew); S->size--; // printf("元素出栈:%d,栈顶元素:%d\n",e,S->top->data); return e; } /* 拓扑排序,若GL无回路,则输出拓扑排序序列并返回1,若有回路返回0。 */ Status TopologicalSort(GraphAdjList GL) { LinkStack S; initLinkStack(&S); for(int i=0;i<GL->numVertexes;i++){ if(GL->adjList[i].in == 0){ push(&S,i);//将所有度为0的顶点入栈 } } int count=0; EdgeNode *p; int k; while(!isEmpty(&S)){ int i=pop(&S); printf("%d -> ",GL->adjList[i].data); count++; for(p=GL->adjList[i].firstedge;p;p=p->next){//遍历边表 k=p->adjvex;//获取顶点下表 if(!(--(GL->adjList[k].in))){//入度减1后为0则入栈 push(&S,k); } } } printf("\n"); if(count<GL->numVertexes){//若有环则count数会小于节点总数 return ERROR; }else{ return OK; } } int main(void) { MGraph G; GraphAdjList GL; int result; CreateMGraph(&G); CreateALGraph(G,&GL); result=TopologicalSort(GL); printf("result:%d",result); return 0; }
若一个顶点有多个直接后继,则拓扑排序的结果通常不唯一;若每个顶点有唯一的前驱后继关系,则结果唯一;对于一般图,若邻接矩阵是三角矩阵,则存在拓扑序列,否则不一定存在拓扑序列;
关键路径
AOE网:把边表示活动,顶点表示事件,权表示活动持续时间的带权有向无环图称做边活动网(Activity on Edge),简称AOE网。(与AOV网区别开,AOV网边无权值,边仅表示顶点的前驱后继关系)
AOE网的性质:
- 只有顶点A代表的事件发生后,从A出发的各有向边代表的活动才能开始。
- 只有进入顶点B的各有向边代表的活动都结束时,顶点B代表的事件才能发生。
- 在AOE网中只有一个入度为0的顶点,称为开始顶点。也只有一个出度为0的顶点,称为结束顶点。
关键路径:在AOE网中,从开始顶点到完成顶点所有路径中,最大路径长度的路径称为关键路径。完成整个工程的最短时间就是关键路径的长度,也即关键路径上各活动的花销总和。
关键活动:关键路径上的活动称为关键活动。
事件Vk的最早(early)发生时间表示:ve(k)
事件Vk的最迟(late)发生时间表示:vl(k)
活动ai的最早开始时间表示:e(i)
活动ai的最迟开始时间表示:l(i)
V1事件:表示整个工程开始;V9事件:表示整个工程结束;V5事件:表示a4,a5已经完成,a7,a8可以开始
首先完成 Ve(j)、Vl(j)、e(i)、l(i) 4 种统计信息的准备工作。其中代表从vi到vj的边的权值
求事件的最晚开始时间是从前往后算的 ;求事件的最晚开始时间是从后往前算的
然后根据事件的俩个时间来算活动的时间 ,其中设活动 ai 用弧 < j, k > 表示,其持续时间记为:wj,k
最后根据l-e=0的情况确定出关键活动,进而确定出关键路径。
#include <stdio.h> #include <stdlib.h> #define MAX_VERTEX_NUM 20//最大顶点个数 #define VertexType int//顶点数据的类型 typedef enum{false,true} bool; //建立全局变量,保存边的最早开始时间 VertexType ve[MAX_VERTEX_NUM]; //建立全局变量,保存边的最晚开始时间 VertexType vl[MAX_VERTEX_NUM]; typedef struct ArcNode{ int adjvex;//邻接点在数组中的位置下标 struct ArcNode * nextarc;//指向下一个邻接点的指针 VertexType dut; }ArcNode; typedef struct VNode{ VertexType data;//顶点的数据域 ArcNode * firstarc;//指向邻接点的指针 }VNode,AdjList[MAX_VERTEX_NUM];//存储各链表头结点的数组 typedef struct { AdjList vertices;//图中顶点及各邻接点数组 int vexnum,arcnum;//记录图中顶点数和边或弧数 }ALGraph; //找到顶点对应在邻接表数组中的位置下标 int LocateVex(ALGraph G,VertexType u){ for (int i=0; i<G.vexnum; i++) { if (G.vertices[i].data==u) { return i; } } return -1; } //创建AOE网,构建邻接表 void CreateAOE(ALGraph **G){ *G=(ALGraph*)malloc(sizeof(ALGraph)); scanf("%d,%d",&((*G)->vexnum),&((*G)->arcnum)); for (int i=0; i<(*G)->vexnum; i++) { scanf("%d",&((*G)->vertices[i].data)); (*G)->vertices[i].firstarc=NULL; } VertexType initial,end,dut; for (int i=0; i<(*G)->arcnum; i++) { scanf("%d,%d,%d",&initial,&end,&dut); ArcNode *p=(ArcNode*)malloc(sizeof(ArcNode)); p->adjvex=LocateVex(*(*G), end); p->nextarc=NULL; p->dut=dut; int locate=LocateVex(*(*G), initial); p->nextarc=(*G)->vertices[locate].firstarc; (*G)->vertices[locate].firstarc=p; } } //结构体定义栈结构 typedef struct stack{ VertexType data; struct stack * next; }stack; stack *T; //初始化栈结构 void initStack(stack* *S){ (*S)=(stack*)malloc(sizeof(stack)); (*S)->next=NULL; } //判断栈是否为空 bool StackEmpty(stack S){ if (S.next==NULL) { return true; } return false; } //进栈,以头插法将新结点插入到链表中 void push(stack *S,VertexType u){ stack *p=(stack*)malloc(sizeof(stack)); p->data=u; p->next=NULL; p->next=S->next; S->next=p; } //弹栈函数,删除链表首元结点的同时,释放该空间,并将该结点中的数据域通过地址传值给变量i; void pop(stack *S,VertexType *i){ stack *p=S->next; *i=p->data; S->next=S->next->next; free(p); } //统计各顶点的入度 void FindInDegree(ALGraph G,int indegree[]){ //初始化数组,默认初始值全部为0 for (int i=0; i<G.vexnum; i++) { indegree[i]=0; } //遍历邻接表,根据各链表中结点的数据域存储的各顶点位置下标,在indegree数组相应位置+1 for (int i=0; i<G.vexnum; i++) { ArcNode *p=G.vertices[i].firstarc; while (p) { indegree[p->adjvex]++; p=p->nextarc; } } } bool TopologicalOrder(ALGraph G){ int indegree[G.vexnum];//创建记录各顶点入度的数组 FindInDegree(G,indegree);//统计各顶点的入度 //建立栈结构,程序中使用的是链表 stack *S; //初始化栈 initStack(&S); for (int i=0; i<G.vexnum; i++) { ve[i]=0; } //查找度为0的顶点,作为起始点 for (int i=0; i<G.vexnum; i++) { if (!indegree[i]) { push(S, i); } } int count=0; //栈为空为结束标志 while (!StackEmpty(*S)) { int index; //弹栈,并记录栈中保存的顶点所在邻接表数组中的位置 pop(S,&index); //压栈,为求各边的最晚开始时间做准备 push(T, index); ++count; //依次查找跟该顶点相链接的顶点,如果初始入度为1,当删除前一个顶点后,该顶点入度为0 for (ArcNode *p=G.vertices[index].firstarc; p ; p=p->nextarc) { VertexType k=p->adjvex; if (!(--indegree[k])) { //顶点入度为0,入栈 push(S, k); } //如果边的源点的最长路径长度加上边的权值比汇点的最长路径长度还长,就覆盖ve数组中对应位置的值,最终结束时,ve数组中存储的就是各顶点的最长路径长度。 if (ve[index]+p->dut>ve[k]) { ve[k]=ve[index]+p->dut; } } } //如果count值小于顶点数量,表明有向图有环 if (count<G.vexnum) { printf("该图有回路"); return false; } return true; } //求各顶点的最晚发生时间并计算出各边的最早和最晚开始时间 void CriticalPath(ALGraph G){ if (!TopologicalOrder(G)) { return ; } for (int i=0 ; i<G.vexnum ; i++) { vl[i]=ve[G.vexnum-1]; } int j,k; while (!StackEmpty(*T)) { pop(T, &j); for (ArcNode* p=G.vertices[j].firstarc ; p ; p=p->nextarc) { k=p->adjvex; //构建Vl数组,在初始化时,Vl数组中每个单元都是18,如果每个边的汇点-边的权值比源点值小,就保存更小的。 if (vl[k]-p->dut<vl[j]) { vl[j] = vl[k]-p->dut; } } } for (j = 0; j < G.vexnum; j++) { for (ArcNode*p = G.vertices[j].firstarc; p ;p = p->nextarc) { k = p->adjvex; //求各边的最早开始时间e[i],等于ve数组中相应源点存储的值 int ee = ve[j]; //求各边的最晚开始时间l[i],等于汇点在vl数组中存储的值减改边的权值 int el = vl[k]-p->dut; //判断e[i]和l[i]是否相等,如果相等,该边就是关键活动,相应的用*标记;反之,边后边没标记 char tag = (ee==el)?'*':' '; printf("%3d%3d%3d%3d%3d%2c\n",j,k,p->dut,ee,el,tag); } } } int main(){ ALGraph *G; CreateAOE(&G);//创建AOE网 initStack(&T); TopologicalOrder(*G); CriticalPath(*G); return 0; }
可通过加快关键活动来缩短工程工期,但不是随便缩短的(有可能把关键活动变成非关键活动) 所有关键活动提前完成,则整个工程提前完成;但只有任一关键活动提前完成,不能缩短整个工程工期
关键路径不唯一(如上面例题我们得到的是俩条关键路径),只有加快所有关键路径上的关键活动才能缩短工期(只加快一条是不能缩短的)
7.查找
流程图
基本概念
查找表:由同一类型的数据元素组成的数据集合。4种常见查找表操作:(1)查询某元素是否在查找表中;(2)查询满足条件的数据元素的各属性;(3)插入数据;(4)删除数据;
静态/动态查找表:只涉及操作(1)和(2),无需动态修改查找表,此类查找表称为静态查找表,适合静态查找表的查找方法:顺序查找、折半查找、分块查找等。若需要动态插入和删除的查找表称为动态查找表。适合动态查找表的查找方法:二叉排序树查找(包含二叉平衡树和B树)、散列查找等。
关键字:在查找表查找某个特定元素时,前提是需要知道这个元素的一些属性,这些属性都可以称为关键字。
关键字又细分为主关键字和次关键字。若某个关键字可以唯一地识别一个数据元素时,称这个关键字为主关键字,例如学生的学号就具有唯一性;反之,像学生姓名、年龄这类的关键字,由于不具有唯一性,称为次关键字。
平均查找长度(Average Search Length,用 ASL 表示):所有查找过程中关键字的比较次数的平均值(是衡量算法效率的最主要指标,平均查找长度(ASL) =(查找第i个元素的概率Pi * 找到第i个元素的比较次数Ci )求和
静态查找(在查找表中只做查找操作,而不改动表中数据元素,称此类查找表为静态查找表,如顺序查找、折半查找、分块查找)
顺序查找:从表中的最后一个数据元素开始,逐个同记录的关键字做比较,如果匹配成功,则查找成功;反之,如果直到表中第一个关键字查找完也没有成功匹配,则查找失败。
在初始化创建查找表时,将所有的数据元素存储在数组中,并把第一个位置留给用于查找的关键字,这个位置保存的元素称为哨兵。之后,顺序表遍历从没有哨兵的一端依次进行查询,如果查找表中有用户需要的数据,则程序输出该位置;反之,程序会运行至哨兵所在位置,此时匹配成功,程序停止运行,结果是查找失败
#include <stdlib.h> typedef struct{ int key; int value; //...如果需要,可以添加其他属性 }ElemType; typedef struct { ElemType *elem;//保存元素的数组 int length;//表长度 }SSTable; void createSSTable(SSTable *st){ int length; printf("请输入数据长度:\n"); scanf("%d",&length); st->length=length; st->elem=(ElemType*)malloc((length+1)*sizeof(ElemType)); printf("请输入表中元素:\n"); for(int i=1;i<=length;i++){ scanf("%d",&(st->elem[i].key)); } } void printSSTable(SSTable st){ for(int i=0;i<=st.length;i++){ printf("%d ",st.elem[i].key); } } void Search_seq(SSTable st,int key){ st.elem[0].key=key;//哨兵位保存待查询关键字 int i; for(i=st.length;st.elem[i].key!=key;i--); if(i==0){ printf("查找失败"); }else{ printf("数据%d在查找表的位置为:%d",key,i); } } void main(){ SSTable st; createSSTable(&st);//1 5 4 2 7 9 printf("请输入需要查找的关键字:\n"); int key; scanf("%d",&key); Search_seq(st,key); }
无序线性表的顺序查找(如数据为1 5 4 2 7 9)
ASL(成功)=(6+5+4+3+2+1)/6=7/2,其中Ci=n-i+1,ASL=(n+1)/2
ASL(失败)=(n个7相加)/n=7,其中Ci=n+1=7,ASL=n+1,n为失败节点数
有序线性表表的顺序查找(如数据为:1 2 4 5 7 9)
ASL(成功)=(6+5+4+3+2+1)/6=7/2,其中Ci=n-i+1,ASL=(n+1)/2,和无序时相同
ASL(失败)=(1+2+3+4+5+6+6)/7,详细分析看下图(设落在7个失败区间的结点数分别为x1,x2,x3,x4,x5,x6,x7)
顺序查找缺点是:当n较大时,平均查找查找长度较大效率低;优点是:既可以顺序存储也可以链式存储,无论关键字是否有序均可应用。
折半查找(二分查找)
只适用于有序的顺序表,所以使用折半查找前需要对数据进行排序。
基本思想:首先将给定值key与中间元素比较,若相等则查找成功返回存储位置;若不等则从前半部分或后半部分继续进行同样的查找,直到找到或没有所查元素为止。
对静态查找表
{5,13,19,21,37,56,64,75,80,88,92}
采用折半查找算法查找关键字为 21 的过程为:当第三次做判断时,发现 mid 就是关键字 21 ,查找结束。
在做查找的过程中,如果 low 指针和 high 指针的中间位置在计算时位于两个关键字中间,即求得 mid 的位置不是整数,需要统一做取整操作。
#include <stdio.h> #include <stdlib.h> typedef struct { int key;//查找表中每个数据元素的值 //如果需要,还可以添加其他属性 }ElemType; typedef struct{ ElemType *elem;//存放查找表中数据元素的数组 int length;//记录查找表中数据的总数量 }SSTable; void createSSTable(SSTable *st){ int length; printf("请输入数据长度:\n"); scanf("%d",&length); st->length=length; st->elem=(ElemType*)malloc((length+1)*sizeof(ElemType)); printf("请输入表中元素:\n"); for(int i=1;i<=length;i++){ scanf("%d",&(st->elem[i].key)); } } void printSSTable(SSTable st){ for(int i=0;i<=st.length;i++){ printf("%d ",st.elem[i].key); } } //折半查找算法 int Search_Bin(SSTable st,int key){ int low=1; int high=st.length; int mid; while(low<=high){ mid=(low+high)/2;//取中间的位置,C语言除法运算结果为小数时,默认取小的整数 if(st.elem[mid].key==key){ return mid; }else if(st.elem[mid].key>key){ high=mid-1; }else{ low=mid+1; } } return 0; } int main(int argc, const char * argv[]) { SSTable *st; Create(&st, 11); getchar(); printf("请输入查找数据的关键字:\n"); int key; scanf("%d",&key); int location=Search_Bin(st, key); //如果返回值为 0,则证明查找表中未查到 key 值, if (location==0) { printf("查找表中无该元素"); }else{ printf("数据在查找表中的位置为:%d",location); } return 0; }
折半查找过程可用如图二叉树来描述,此二叉树称为判定树。图中方形叶结点为查找失败结点。从判定树可看出,查找成功时查找长度为从根结点到目的结点路径上的结点数,而查找不成功时长度为从根结点到对应失败结点父节点(即方形叶子结点的父节点)的路径上结点数。
ASL(成功)=(1*1+2*2+3*4+4*4)/11,即(第i层结点个数*层高之和)/结点总数
其中
ASL(失败)=(3*4+8*4)/12,即(第i层失败结点数*父节点层高之和)/失败结点数
折半查找的时间复杂度为,比顺序查找效率要高。折半查找不适合于链式存储结构(因为链式存储时,程序无法根据low指针和high指针计算出mid的位置)
分块查找
分块查找又叫索引顺序查找,吸收了顺序查找和折半查找各自优点,既有动态结构又适合快速查找(块间有序,块内无序,块间折半,块内线性)
基本思想:将查找表分为若干块,块内元素无序,块间有序(即第一块中最大关键字小于第二块中所有关键字),然后再建立一个索引表,索引表中含有各块中最大关键字和各块中第一个元素的地址,索引表按关键字有序排列。
分块查找分两步:第一步在索引表确定待查记录所在的块,可以顺序查找也可折半查找;第二步在块内顺序查找(块内无序故不能折半查找)。
分块查找的平均查找长度=索引表平均查找长度+块内平均查找长度。如图18个数据,分3块,每块6个元素,则
均使用顺序查找时ASL(成功)=(1+2+3)/3+(1+2+3+4+5+6)/6,(1+2+3)/3=[3*(3+1)/2]/3,后同
折半加顺序查找时ASL(成功)=(1*1+2*2+3*4)/7+[6*(6+1)/2]/6
动态查找(在查找表中做查找操作的同时进行插入/修改/删除数据的操作,称此类表为动态查找表,,例如:二叉排序树的查找、平衡二叉树的查找、B树/B+树查找)
二叉排序树:
见二叉树的应用
二叉平衡树:
见二叉树的应用
B/B+树:
B树及其基本操作(B-树就是B树)
B即Balanced,平衡的意思。B树又称多路平衡查找树,B树中所有结点的孩子个数最大值称为B树的阶级,用m表示。
带叶子结点(F)形式
不带叶子结点形式
一棵m阶的B树,特性如下:
- 树中每个结点至多有m棵子树,至多有m-1个关键字
- 若根结点不是叶子结点,则至少有两棵子树
- 除根结点外的所有非叶子结点至少有⌈m/2⌉棵子树,至少⌈m/2⌉-1个关键字
- 所有的叶结点都出现在同一层次上,并且不带信息(可视为外部结点或折半查找判定树的失败结点,实际上这些结点不存在,指向这些结点的指针为空)。
- 所有非叶结点的结构如下:其中Ki为结点关键字,且满足K1<K2<...<Kn;Pi为指向子树根结点的指针,且P(i-1)指向子树的所有结点的关键字均小于Ki,Pi指向子树的所有结点的关键字均大于Ki,n为结点中关键字个数
B树的高度
B树的大部分操作所需的磁盘存取次数与B树的高度成正比(不包含叶子结点那一层),一颗高度为h的m阶B树中关键字个数满足n<=m^h-1,即
若每个结点保存最少的关键字时B树高度最大,n+1≥2(⌈m/2⌉)^(h-1),
例如:一颗3阶B树共有8个关键字,其高度范围为
3阶B树至少有多少个结点?至多有多少个结点?
第一层有且仅有1个
第二层有2-3个
第三层有2^2-3^2个
...
第k层有2^(k-1)-3^(k-1)个
B树的查找
B树的查找包含两个基本操作:
- 在B树中找结点(B树常存储在磁盘上,这一操作在磁盘上进行)
- 在结点内查找关键字(这一操作在内存中进行,即找到结点后将节点信息存入内存,然后采用顺序查找或折半查找。查找到目标结点后,先在有序表中进行查找,若找到则查找成功,否则按照对应的指针信息,到所指的子树中去查找)(如:上图查找42时,先从根结点开始,比较42>22,则关键字在22的右子树上,再次比较36<42<45,则关键字在中间子树上,比较后查找到42,查找成功,否则查找到叶子节点,查找失败)
B树的插入
B树的插入过程如下:
- 定位。利用B树的查找算法,找出插入该关键字的最低层中的某个非叶结点。
- 插入。在B树中,当插入后的结点关键字个数小于m,则可以直接插入;当插入后结点关键字个数大于m-1时,必须对结点进行分裂。
结点分裂的方法
当某结点中关键字超出最大限制数量后,从中间位置将关键字分为两部分,左边关键字放在原结点,右边关键字放到新结点,中间位置关键字放到原结点的父节点。若父节点因此也超过了关键字上限,则继续分裂操作,直至传到根节点,进而B树高度+1。一棵3阶B树的分裂过程及方法如图所示(3阶树的所有结点中最多有2个关键字)
B树的删除
当被删除的关键字k不在终端结点(最低层非叶结点)中时:可以用k的前驱(或后继)k'来替换k,然后在相应的结点中删除k',然后转变成删除关键字在终端结点的情形。
当被删除的关键字k在终端结点(最低层非叶结点)中时,有三种情况:
- 直接删除关键字。若被删关键字所在结点的关键字个数 >= ⌈m/2⌉,表面删除后仍满足B树定义,则直接删除该关键字。
- 兄弟够借。若被删关键字所在结点的关键字个数=⌈m/2⌉-1,且与此结点相邻的左(右)兄弟结点的关键字个数 >= ⌈m/2⌉,则需要调整该结点、左(右)兄弟结点以及其双亲结点(父子换位法),以达到新的平衡。如下图,4阶B树删除65,右兄弟关键字个数>=⌈m/2⌉=2,将71取代65的位置,将74调整到71的位置。
- 兄弟不够借。若被删关键字所在结点的关键字个数=⌈m/2⌉-1,且与此结点相邻的左(右)兄弟结点的关键字个数 = ⌈m/2⌉-1,则将关键字删除后与左(右)兄弟结点以及其双亲结点中的关键字进行合并。如下图,4阶B树删除5,它及其右兄弟结点关键字个数=⌈m/2⌉-1=1,故删除5后将60合并到65结点中。(在合并的过程中,若此时导致其父结点的关键字个数也不满足B树的定义,则继续进行这种合并操作。若最终使得根结点被合并,B树高度减1。
B+树的基本概念
B+树是因数据库所需出现的一种B树的变形树。B+树把数据都存储在叶结点,而内部结点只存关键字和孩子指针
一颗m阶B+树满足下列条件:
- 每个分支结点最多有m棵子树。
- 非叶根结点至少有两棵子树,其他每个分支结点至少有⌈m/2⌉棵子树。
- 结点的子树个数与关键字个数相等。
- 所有叶结点包含全部关键字及指向相应记录的指针,叶结点中将关键字按大小顺序排列,并且相邻叶结点按大小顺序相互链接起来。
- 所有分支结点中仅包含它的各个子结点中关键字的最大值及指向其子结点的指针。
B树与B+树的区别
- 在B+树中,具有n个关键字的结点只含有n棵子树,即每个关键字对应一棵子树。在B树中,具有n个关键字的结点含有(n+1)棵子树。
- 在B+树中,每个结点的关键字个数n的范围是 ⌈m/2⌉ <= n <= m(根结点:1 <= n <= m);在B树中,每个结点的关键字个数n的范围是 ⌈m/2⌉-1 <= n <= m-1(根结点:1 <= n <= m-1)。
- 在B+树中,叶节点包含信息,所有非叶结点仅起到索引作用,非叶结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。
- 在B+树中,叶结点包含了全部关键字,即在非叶结点中出现的关键字也会出现在叶结点中。而在B树中,叶结点包含的关键字和其他结点包含的关键字是不重复的。
图为4阶B+树。双亲结点关键字是其子树中最大关键字的副本。B+树有两个头指针:一个指向根结点,另一个指向关键字最小的叶节点。因此,B+树科进行两种查找运算:一种是从最小关键字开始的顺序查找,另一种是从根结点开始的多路查找。
B+树查找过程中,非叶节点上关键字等于给定值是并不终止,而是继续向下查找,直到叶节点上的该关键字为止 。(即无论查找成功与否,都是一条从根结点到叶结点的路径)
B+树的插入操作(与B树类似,简单了解即可)
(1)若为空树,创建一个叶子结点,记录插入,这个叶子结点也是根结点,插入操作结束
(2)针对叶子类型结点:根据关键字找到叶子结点,然后插入记录,插入后若当前结点关键字个数<=m-1,则插入结束。否则,将这个叶子结点分裂成为左右两个叶子结点,左叶子结点包括前m/2个关键字,右叶子结点包括剩下的,将第m/2+1个记录上移到父结点(必须索引结点)。进位到父结点的关键字左孩子指针指向左结点,右孩子结点指针指向右结点,本身就指向父结点。
(3)针对索引类型结点:若当前结点的关键字个数<=m-1,则插入结束。否则,将这个索引类型结点分裂成两个索引结点,左索引结点关键字包含前(m-1)/2个,右索引结点包含m-((m-1)/2)个,将第m/2个关键字上移到父结点中,类同第2步。B+树的删除操作(与B树类似,简单了解即可)
如果叶子结点中没有要删除的关键字,则删除失败,否则执行下面步骤
(1)删除叶子结点中对应的关键字。删除后若结点关键字个数>=ceil(m/2)-1,删除操作结束,否则执行下一步
(2)若兄弟结点的关键字个数>ceil(m/2)-1,向兄弟结点借一个关键字,同时用借的关键字替换(当前结点与父结点相同的关键字),删除结束,否则执行下一步。
(3)当前结点与兄弟结点合并成新的叶子结点,并删除父结点中的关键字(孩子指针变成一个指针,正好指向新的叶子结点),当前结点指向父结点(索引结点)。执行下一步。
(4)若索引结点的关键字个数>=ceil(m/2)-1,则删除操作结束,否则执行下一步。
(5)若兄弟结点关键字个数>ceil(m/2)-1,父结点关键字下移,兄弟结点关键字上移,删除结束,否则执行下一步。
(6)当前结点和兄弟结点及其父结点关键字下移合并成新的结点,将新结点指向父结点,重复第4步。
散列表
散列函数:把查找表的关键字映射到对应的地址的函数,记为Hash(key)=Addr(addr可以是数组下标、索引或内存地址等)。
冲突:散列函数可能会把两个不同关键字映射到同一地址,这种情况叫冲突。一方面,设计得好的散列函数应尽量减少冲突;另一方面,冲突是不可避免的,所以还要设计好处理冲突的方法。(即k1≠k2,而f(k1)=f(k2))
散列表(也叫哈希表):建立了关键字和存储地址间的直接映射关系,即若关键字为k,则其值存放在f(k)的存储位置上,由此,不需比较便可直接取得所查记录的数据结构。
哈希函数的构造方法:
直接定址法:Hash(key) = a•key + b (a、b为常数) —— 以关键码key的某个线性函数值为哈希地址
优点:不会产生冲突。缺点:要占用连续地址空间,若关键字分布不连续则空位较多,浪费存储空间。
除留余数法:Hash(key) = key % p(p是一个整数) —— 以关键码除以p的余数作为哈希地址
方法:若设计的哈希表长为m,取 ≤m 且距离最近的质数为p(20以内的质数:2,3 ,5,7,11,13,17,19),关键是选取p,从而减少冲突
随机数法:选择一个随机数,取关键字的随机函数值为它的散列地址,即Hash(key)=random(key),random为随机函数,适用于关键字长度不等时。
数字分析法:例如:H(81346532)=43,H(81301367)=06,取第四位和第七位作为地址,适用于已知关键字集合
平方取中法:例如:2589的平方值为6702921,可以取中间的029为地址,适用于关键字每位取值都不够均匀或均小于散列地址所需位数
折叠法:是将关键字从左到右分割成位数相等的几部分(注意最后一部分位数不够时可以短些)然后将这几部分叠加求和并按散列表表长,取后几位作为散列地址。(如关键字是 9876543210,散列表表长为三位则我们可以将它分为四组 987|654|321|0,然后将它们叠加求和 987+654+321+0=1962,再取后 3 位得到散列地址即为 962)
处理冲突:
开放定址法(又叫再散列法)
Hash地址=(Hash(key)+增量序列)% 表长,例子中增量序列使用线性探测法(di:0,1,2,3,4..m-1)
增量序列的确定方法:
- 线性探测法:di=0,1,2,3...k,缺点:容易造成元素在相邻地址聚集
- 平方探测法:di=0^2,1^2,(-1)^2,2^2....k^2,(-k)^2,缺点:不能探测到散列表上所有单元
- 再散列法:di=Hash2(key),即当第一个散列函数发生冲突时,再利用第二个散列函数计算地址增量。
- 伪随机序列法:di=伪随机序列
查找成功平均查找长度:(1+2+1+1+1+4+1+2+2)/9
例2:(36,15,22,40,63)
,哈希函数为:H(K)=K MOD 7。
K为关键字,用线性探测法再散列法处理冲突。查找成功平均查找长度:(1+2+3 + 1 + 1)/5 = 1.6
查找失败平均查找长度:(5 + 4 + 3 + 2 + 1 + 2 + 1) / 7
如何理解查找失败:现在我的哈希表已经把这5个数据填进去了,当我取一个除了这5个数之外的数来查才会失败,比如我查数字7是否在这个表中,我通过哈希函数得到他应该在地址0里面,结果发现0地址已经被63占据了,那么我根据线性探测原则,我去1地址再找,发现被36占据了,以此类推,直到我找到地址4,发现里面没有数据,说明这个哈希表没有我要找的7,即我查询失败了,期间我查询比较了5次,所以地址0的查询失败次数为5次
程序代码演示
#include <stdlib.h> #define HASH_SIZE 20 #define NULL_VALUE -32768 typedef struct{ int *elem;//保存数据的数组首地址 int count;//当前数据元素个数 int sizeindex;//当前容量,即数组elem长度 }HashTable; void init(HashTable *H){ H->count=0; H.sizeindex=HASH_SIZE H->elem=(int*)malloc(sizeof(int)*HASH_SIZE); if(H->elem==NULL){ printf("动态内存分配失败"); exit(-1); } for(int i=0;i<HASH_SIZE;i++){ H->elem[i]=NULL_VALUE; } } //定义散列函数 int Hash(int key){ return key % 19; } //将元素插入散列表 void insert(HashTable *H,int e){ int addr=Hash(e); while(H->elem[addr]!=NULL_VALUE){ addr=(addr+1) % 19;//如果地址对应元素不为空(已被占用)则使用开放定地址法解决冲突 } H->elem[addr]=e; H->count++; } //遍历散列表 void printHashTable(HashTable H){ for(int i=0;i<HASH_SIZE;i++){ printf("%d ",H.elem[i]); } } int SearchHash(HashTable H,int key){ int addr=Hash(key); while(H.elem[addr]!=NULL_VALUE && H.elem[addr]!=key){ addr=(addr+1) % 19;//使用开放定地址法解决冲突 } if(H.elem[addr]==key){ return addr; }else{ return -1; } } void main(){ HashTable H; init(&H); int arr[12]={19,14,23,1,68,20,84,27,55,11,10,79}; for(int i=0;i<12;i++){ insert(&H,arr[i]); } printHashTable(H); printf("\n请输入要查找的元素:\n"); int x; scanf("%d",&x); int result=SearchHash(H,x); if(result==-1){ printf("元素不存在"); }else{ printf("%d元素在哈希表的位置是:%d",x,result); } }
再哈希法:同时构造多个不同的哈希函数Hashi(i=1,2,3...k),当Hash1冲突时使用Hash2,直到不冲突。优点:数据不易产生聚集;缺点:增加了计算时间
链地址法:将所有哈希地址为 i (即地址冲突的)的元素构成一个单链表,并将单链表的头指针存在哈希表的第 i 个单元中
平均查找长度ASL(成功)=(1 * 7 + 2 * 4 + 3 * 1) / 12 = 1.5 (查找成功平均长度=总查找成功次数 / 关键字个数)
建立公共溢出区:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素一律填入溢出表(了解即可)
散列表的查找过程:
初始Addr=Hash(key),
步骤1:然后检查地址为Addr的位置是否有记录,若无则查找失败;若有则比较它与key的值,若相等则返回查找成功标识,否则执行步骤2。
步骤2:用给定的冲突处理方法计算下一散列地址,并更新Addr,转入步骤1。
散列表由于冲突的产生,仍然是一个给定值与关键字比较的过程,因此仍需要用平均查找长度来衡量查找效率。散列表的查找效率取决于三个因素:散列函数、处理冲突的方法和填装因子。(装填因子a = (哈希表中的记录数) / (哈希表的长度),a越大说明表装得越满,发生冲突可能越大,反之则越小)
8.内部排序
框架图
基本概念
内部排序:在排序期间全部元素存放在内存中的排序。内部排序在执行过程中要进行两种操作:比较和移动,通过比较确定大小关系从而移动到有序的位置。并非所有内部排序都需要比较操作,如基数排序就不需要。内部排序算法的性能取决于时间复杂度和空间复杂度
外部排序:在排序期间元素无法全部同时存放在内存中,必须在排序过程中根据需要不断在内外村之间移动的排序。(即参加排序的记录数量很大,需要借助外存)
排序的稳定性:在待排序表中A、B两个关键字值相等,若排序后A、B的先后次序保持不变,则称这种排序算法是稳定的,反之称为不稳定的。算法是否具有稳定性不能衡量一个算法的优劣,因为如果待排序表中关键字不允许重复,则选择排序算法的稳定性无关紧要。
8.1 插入类排序
基本思想:将一个记录按关键字大小插入已排好的序列。
直接插入排序:将第
i
个记录插入到i-1
个已排好序的记录中(在查找插入位置时,采用的是顺序查找的方式),执行过程:
- 查出第i个元素在i-1个元素列表中的插入位置k(找位置)。
- 将k位置及以后的所有元素后移一个位置(腾空间)。
- 将第i个元素复制到k的位置(放进去)。
将L0位置设置为哨兵位,然后将序列中的第1个记录看成是一个有序的子序列,然后从第2个记录起逐个进行插入,直至整个序列按关键字非递减排列为止。
#include "stdlib.h" typedef struct { int *list; int length; }SqList; void initSqList(SqList *L,int *arr,int length){ L->list=(int *)malloc(sizeof(int)*length); for(int i=1;i<length;i++){ L->list[i]=arr[i-1]; } L->length=length; } void printAll(SqList L){ for(int i=0;i<L.length;i++){ printf("%d ",L.list[i]); } } void insertSort(SqList *L){ int i,j; for(i=2;i<=L->length;i++){ if(L->list[i] < L->list[i-1]){//若list[i]关键字小于其前驱,将list[i]插入 L->list[0]=L->list[i];//先把待插入元素放到哨兵位 for(j=i-1;L->list[0]<L->list[j];j--){//从第i-1位开始从后往前依次与待插入元素比较,若目标元素大于待插入元素,则该元素后移一位,直到找到比待插入元素小的,也就找到了插入位 L->list[j+1]=L->list[j]; } L->list[j+1]=L->list[0];//将元素复制到插入位 } } } void main(){ SqList L; int arr[8] = {49, 38, 65, 97, 76, 13, 27, 49}; initSqList(&L, arr, 9); printf("初始序列为:"); printAll(L); insertSort(&L); printf("\n排序后序列为:"); printAll(L); }
空间复杂度:O(1)
时间复杂度:O(n^2)(平均情况下)
稳定性:稳定的
适用性:适用于顺序表或链表(大部分排序算法仅适用于顺序存储的线性表,这个是特殊)
折半插入排序:插入时采用折半查找,插入第i个元素到r[i]到r[i-1]之间(直接插入排序是边比较边移动进行的,而折半插入排序是先比较后统一移动,即比较和移动分离)(只适用于顺序表)
//排序 #include <stdio.h> void print(int a[], int n ,int i){ printf("第%d躺:",i); for(int j=0; j<n; j++){ printf("%2d ",a[j]); } printf("\n"); } void BInsertSort(int *a,int size){ int low,high,mid; for(int i=2;i<=size;i++){ a[0]=a[i];//将待插入元素保存到哨兵位 low=1; high=i-1; while(low<=high){//采用折半查找法判断插入位置,最终变量 low 表示插入位置 mid=(low+high)/2; if(a[mid]>a[0]){ high=mid-1; }else{ low=mid+1; } } for(int j=i;j>=low;j--){ a[j]=a[j-1]; } a[low]=a[0];//插入元素 print(a, 9, i); } } void main(){ int a[9] = {65535,34,10,17,5,22,43,19,26}; BInsertSort(a, 8); }
也可以将第0位设为哨兵位去求,但我觉得这样搞容易乱,不如直接设一个temp变量
特点:减少了比较元素的次数,但元素移动次数没有变,故时间复杂度仍为:O(n^2)
稳定性:稳定
希尔排序: 先将整个待排记录序列分割成若干子序列, 然后对各子表分别进行直接插入排序, 当整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。(为什么叫希尔排序?因为这种方法是名叫希尔的人提出的)
如何分割:依次取分割增量为
d1=n/2,d(i+1)=di/2 (i=2,3,4..)
,直到di=1为止,如下图:d1=4,将数据分为4组,每组2个元素,然后组内进行直接插入排序;
d2=2,将数据分为2组,每组4个元素,然后组内进行直接插入排序;
d3=1,将数据分为1组,每组8个元素,然后组内进行直接插入排序,最后得到结果;
void print(int *a,int size,int i){ printf("di=%d时:",i); for(int i=1;i<=size;i++){ printf("%2d ",a[i]); } printf("\n"); } void ShellSort(int *a,int n){ int j; for(int di=n/2;di>=1;di=di/2){ for(int i=di+1;i<=n;i++){ if(a[i]<a[i-di]){ a[0]=a[i]; for(j=i-di; j>0 && a[0]<a[j]; j=j-di){ a[j+di]=a[j]; } a[j+di]=a[0]; } } print(a,8,di); } } void main(){ int a[]={65535,46,55,13,42,94,17,05,70};//a[0]是暂存单元 ShellSort(a,8); print(a,8); }
空间复杂度:O(1)
时间复杂度:O(n^(1.3))或O(n^2),后者为最坏情况下
稳定性:不稳定(例如 4 3 3 3 3 3 3 3 ,经过排序后变为3 3 3 3 3 3 3 4)
适用性:顺序存储结构的线性表
8.2:交换类排序
交换:根据两个关键字比较结果来交换俩个数据在序列中的位置。
冒泡排序
基本思想:第一趟冒泡——从后往前(或从前往后)两两比较相邻元素的值,若为逆序(与结果要求大小顺序相反)则交换,直到序列比较完,此时将最小元素排在了第一个位置(这里假设结果要求升序排列);下一趟冒泡,上一次确定的最小元素不参与比较,剩余元素继续比较并交换,然后剩余元素中的最小元素拍到第二位,重复执行直到所有元素都有序。(最多n-1趟),如序列{49,38,65,97,76,13,27,49}的冒泡排序过程为:
#include "stdbool.h" void print(int *a,int size){ for(int i=0;i<size;i++){ printf("%d ",a[i]); } printf("\n"); } void BubbleSort(int *a,int size){ bool flag;//是否发生交换 int temp;//临时变量 for(int i=0;i<size-1;i++){ flag=false; for(int j=size-1;j>i;j--){ if(a[j]<a[j-1]){ //交换 temp=a[j]; a[j]=a[j-1]; a[j-1]=temp; flag=true; } } print(a,size); if(flag==false){//某次遍历后没有发生交换,即序列已经变得有序,则不需要再执行后续操作,直接结束 return; } } } void main(){ int a[]={49,38,65,97,76,13,27,49}; BubbleSort(a,8); }
注意到:这次冒泡排序排了5趟,因为加了flag变量判断是否已经有序,进而不做多余的操作。冒泡排序最坏情况下最多需要排n-1次
空间复杂度:O(1)
时间复杂度:最好情况下(序列本身即有序,比较次数n-1,移动(或者叫交换)次数0,时间复杂度为O(n),最坏情况下(序列本身逆序,比较次数n(n-1)/2,移动次数3*n(n-1)/2)(交换分3步),时间复杂度为O(n^2),平均时间复杂度O(n^2))
稳定性:稳定
快速排序
基本思想(基于分治法):
- 先从待排数列中任取出一个数作为基准数p(通常取首元素)
- 通过一趟排序,将序列中大于等于数全放到它的右边,小于p的数全放到它的左边
- 然后再对两个左边序列和右边序列重复第一二步,直到每部分只有一个元素或空为止
以一个数组作为示例(取序列第一个数为基准数用X表示,附设俩个指针i和j,分别指向首段和末端):
初始时,i = 0; j = 7; X = 49
指针j从后往前搜索找到第一个小于基准数的元素27,将27交换到i所指位置
指针i从前往后找到第一个大于基准数的元素65,将65交换到j所指位置
指针j继续往前搜索找到小于基准数的元素13,将13交换到i所指位置
指针i继续往后搜索找到大于基准数的元素97,将97交换到j所指位置
指针j继续往前搜索小于基准数的元素,直至j==i,此时i之后的元素均大于等于49,i之前元素均小于49,将基准数49放到i所指的位置,至此一趟排序结束,原序列被分成了前后两个子序列。
然后按照同样的方法对两个子序列继续递归进行快速排序,直到待排序列只有一个元素或空
最终得到结果为:13,27,38,49,49,65,76,97
代码:
void print(int *a,int size){ for(int i=0;i<size;i++){ printf("%d ",a[i]); } printf("\n"); } //返回分区位置 int partition(int *a,int low,int high){ int pivot=a[low]; while(low<high){ while(low<high&&a[high]>=pivot){ high--;//从后往前寻找比基准数小的数的位置 } a[low]=a[high];//找到后交换到low所指元素 while(low<high&&a[low]<=pivot){ low++;//从前往后寻找比基准数大的数的位置 } a[high]=a[low];//找到后交换到high所指元素 } a[low]=pivot; print(a,8); return low; } void QuickSort(int *a,int low,int high){ if(low<high){ int m=partition(a,low,high); QuickSort(a,low,m-1); QuickSort(a,m+1,high); } } void main(){ int a[]={49,38,65,97,76,13,27,49}; QuickSort(a,0,7); }
空间复杂度:快速排序是递归调用,需要一个递归栈,最好情况下栈深度为,最坏情况下栈深度为n,故平均复杂度为(最坏情况左右两个区域分别包含n-1和0个元素)
时间复杂度:最坏时间复杂度为O(n^2),即初始排序数列有序时需要进行n-1趟;最好情况下时间复杂度为;平均时间复杂度为;故快速排序是所有内部排序中时间复杂度最优的排序算法。
稳定性:不稳定(假设初始序列位46663,经过一趟快排之后变为34666,相对位置发生了变化)
8.3:选择类排序
选择排序的基本思想:
简单选择排序
算法思想:对于具有 n 个记录的无序表遍历 n-1 次,第 i 次从无序表中第 i 个记录开始,找出后续关键字中最小的记录,然后放置在第 i 的位置上,例如对无序表
{56,12,80,91,20}
采用简单选择排序算法进行排序,具体过程如下:第一次遍历时,从下标为 0 的位置(即 56 )开始,找出关键字值最小的记录(12),将其(12)同下标为 0 的关键字 (56) 交换位置,得到:
第二次遍历时,从下标为 1 的位置(即 56) 开始,找出最小值 (20),将其(20)同下标为 1 的关键字( 56 )互换位置,得到:
第三次遍历时,从下标为 2 的位置(即 80) 开始,找出最小值( 56),将其(56)同下标为 3 的关键字( 80) 互换位置,得到:
第四次遍历时,从下标为 3 的位置(即 91 )开始,找出最小是( 80 ),将其(80)同下标为 4 的关键字 (91) 互换位置,得到:
到此简单选择排序算法完成,无序表变为有序表
void print(int *a,int size){ for(int i=0;i<size;i++){ printf("%d ",a[i]); } printf("\n"); } void SimpleSelectSort(int *a,int size){ int min; for(int i=0;i<size-1;i++){ min=i;//记录最小元素位置 for(int j=i+1;j<size;j++){//找到"从最小位置的下一位置开始"的后续元素中最小元素 if(a[j]<a[min]){ min=j; } } if(min!=i){//如果在查找过程中有更新最小元素位置 int temp=a[min]; a[min]=a[i]; a[i]=temp; } print(a,size); } } void main(){ int a[]={56,12,80,91,20}; SimpleSelectSort(a,5); }
稳定性:不稳定(例如:8 8 7,排序后为7 8 8)
树形选择排序
算法思想: 树形选择排序(又称“锦标赛排序”),是一种按照锦标赛的思想进行选择排序的方法,即所有记录采取两两分组,筛选出较小(较大)的值;然后从筛选出的较小(较大)值中再两两分组选出更小(更大)值,依次类推,直到最后选出一个最小(最大)值。同样可以采用此方式筛选出次小(次大)值等
例如对无序表
{49,38,65,97,76,13,27,49}
采用树形选择的方式排序,过程如下:第一次遍历,选择出最小值为13
第二次遍历,由于关键字 13 已经筛选完成,需要将关键字 13 改为“最大值”,选择出最小值为27
依此类推,得到从小到大的序列:13,27,38,49,49,65,76,97,该算法的时间复杂度为
O(nlogn)
堆排序
堆的含义:在含有 n 个元素的序列中,如果序列中的元素满足下面其中一种关系时,此序列可以称之为堆。(也可以理解为:在完全二叉树中,每个根结点的值都必须不小于(或者不大于)左右孩子结点的值)
- ki ≥ k(2i) 且 ki ≥ k(2i+1)(n个记录中,第 i 个关键字的值大于第 2*i 个关键字,同时也大于第 2*i+1 个关键字,即每个根结点的值大于左右孩子结点的值)或
- ki ≤ k(2i) 且 ki ≤ k(2i+1)(n个记录中,第 i 个关键字的值小于第 2*i 个关键字,同时也小于第 2*i+1 个关键字,即每个根结点的值小于左右孩子结点的值)
满足条件1的称为大根堆(大顶堆),其最大元素在根结点。
满足条件2的称为小根堆(小顶堆),其最小元素在根结点。
以无序表{87,45,78,32,17,65,53,09}为例,为一个大根堆
以无序表
{49,38,65,97,76,13,27,49}为例,为一个小根堆
堆排序的算法思想:通过将无序表转化为堆,由于堆的性质,堆顶元素即为最大值(最小值),输出堆顶元素后,通常将堆底元素放到堆顶,此时堆的性质被破坏,将堆顶元素以下的元素调整以继续保持大顶堆(小顶堆)的性质,之后再输出堆顶元素,如此重复,直到堆中只剩一个元素为止。
堆排序过程需要解决的两个问题:
1.如何将得到的无序序列转化为一个初始堆?
n个结点的完全二叉树,最后一个结点是⌊n/2⌋个结点的孩子结点。对第⌊n/2⌋个结点为根的子树进行筛选(即:如对于大根堆,若根结点的关键字小于左右孩子较大者,则交换),使孩子树称为堆,之后依次往前对个结点为根的子树进行筛选操作,若破坏了下一级的堆,则重新筛选下一级的堆,直到根子树构成堆,反复调整,直到根结点。例如无序表{53,17,78,09,45,65,87,32}建立堆的过程如图:(注意开始位置为完全二叉树的第⌊n/2⌋=8/2=4个结点,之后依次为第3个,第2个,第1个)
2.输出堆顶元素后,如何调整剩余元素构建一个新的堆?
输出堆顶元素后,将堆的最后一个元素放到堆顶,此时堆的性质被破坏,向下进行筛选
代码:
#include "stdio.h" void print(int *a,int size){ for(int i=1;i<=size;i++){ printf("%d ",a[i]); } printf("\n"); } //堆的调整 void HeadAdjust(int *a,int root,int size){ a[0]=a[root];//暂存子树的根结点 for(int i=root*2;i<=size;i=i*2){//根结点位置是root时,其左孩子位置一定是root*2 if(i<size&&a[i]<a[i+1]){//获取数值较大的孩子结点下标 i++; } if(a[0]>=a[i]){//如果如果子树根结点值大于等于其较大孩子结点值,结束循环 break; }else{ a[root]=a[i];//如果子树根结点值小于其较大孩子结点值,则将子树根值更新为该结点值 root=i;//然后以该结点为新子树的根继续调整 } } a[root]=a[0]; print(a,8); } void BuildMaxHeap(int *a,int size){ for(int i=size/2;i>0;i--){ printf("调整第%d个结点:",i); HeadAdjust(a,i,size); } } void HeapSort(int *a,int size){ printf("大根堆的构建开始\n"); BuildMaxHeap(a,size); printf("得到大顶堆序列为:"); print(a,size); printf("\n"); int temp; for(int i=size;i>1;i--){ temp=a[1]; a[1]=a[i]; a[i]=temp; printf("第%d个元素归位的结果为:",i); HeadAdjust(a,1,i-1);//把剩余i-1个元素整理成堆 } printf("最终结果为:"); print(a,size); } void main(){ int a[]={65535,53,17,78,9,45,65,87,32}; HeapSort(a,8); }
这里要注意一点:如果按照大顶堆逐个输出顺序结果为:87,78,65,53,45,32,17,9。如果按原数组输出则为上图最终结果。
堆的插入操作:先将新结点放在堆末端,再对堆向上执行调整操作(如下图)
适用情况:适合关键字较多的情况,如1亿个数中选出100个最大值:首先使用大小为100的数组,读入100个数,建立小顶堆,然后依次读入剩余数,若该数小于堆顶则舍弃,否则用该数取代堆顶并重新调整堆,待数去全部读取完毕,则堆中100个数即为所求。
空间复杂度:O(1)
时间复杂度:建堆时间复杂度O(n),之后每次调整时间复杂度O(h) (其中h为完全二叉树的树高),综述最好、最坏、平均时间复杂度均为
稳定性:不稳定(例如4 6 6 6 3,经过排序后为3 4 6 6 6)
8.4:归并排序
归并的含义:将两个或两个以上的有序表组合成一个新的有序表。
二路归并排序
算法思想:假定待排序表含有n个记录,则将其视为n个有序的子表,每个子表的长度为1,然后两两归并,得到⌈n/2⌉个长度为2或1的有序表(因为n有可能为奇数);继续两两归并......如此重复,直到合并成一个长度为n的游戏表为止。例如对序列{49,38,65,97,76,13,27}进行的归并排序:
代码:
#include <stdlib.h> #include <stdio.h> #define SIZE 7 int *b; void print(int *a,int size){ for(int i=0;i<size;i++){ printf("%d ",a[i]); } printf("\n"); } void create(){ b=(int*)malloc(sizeof(int)*(SIZE+1)); } //归并相邻的两个有序表 void Merge(int *a,int low,int mid,int high){ int i,j,k; for(k=low;k<=high;k++){ b[k]=a[k];//将a中所有元素复制到B中 } i=low;//分别从俩个子表的第一个数开始比较 j=mid+1; for(k=i;i<=mid&&j<=high;k++){ if(b[i]<=b[j]){ a[k]=b[i];//将较小的数放到a表对应位置,然后取得较小数的子表后移一位再次比较,直到某一子表数字不够比较为止 i++; }else{ a[k]=b[j]; j++; } } while(i<=mid){//若右子表数字不够比则将左子表数剩余部分全部复制到a表对应为止,下同 a[k]=b[i]; k++; i++; } while(j<=high){ a[k]=b[j]; k++; j++; } if(low<3&&high>3){ printf("最终合并:"); }else if(high<=3){ printf("左半边归并:"); }else { printf("右半边归并:"); } print(a,7); } 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); } } void main(){ int a[]={49,38,65,97,76,13,27}; create(); MergeSort(a,0,6); }
注意:这里是按每次归并输出的,即下图标红的6次
而在一趟排序操作中,调用了⌈n/2⌉次merge算法,总共需要进行趟
空间复杂度:辅助数组B长度为n,故空间复杂度为O(n)0
时间复杂度:(每趟时间复杂度为O(n),需要进行趟)
稳定性:稳定
k路归并时执行趟数为:(N为元素个数)
8.5:基数排序
排序思想:基于关键字各位的大小进行排序(通常采用链式存储来进行)
分配:把Q0,Q1....Qr-1各队列置空,依次考察个位、十位...关键字大小,将相同值的入队(r为基数,例如十进制时:r=10,十六进制时:r=16)
收集:把Q0,Q1....Qr-1各队首尾相连,得到新的结点序列,从而形成新的线性表。
例如对以下10个数进行排序:(基数为10,每个关键字是1000以内的正整数)
第一趟:选择最低位关键字相等的记录(即:个位)分配到同一队列
然后进行收集操作,得到新的线性表
第二趟:选择次低位关键字相等的记录(即:十位)分配到同一队列
然后进行收集操作 ,得到新的线性表
第三趟:选择最高位关键字相等的记录(即:百位)分配到同一队列
然后进行收集操作 ,得到新的线性表,至此排序结束。
代码:
#include <stdlib.h> #include <stdio.h> #include <stdbool.h> #include <xmath.h> //定义链式结点 typedef struct LinkNode{ int data; struct LinkNode *next; }LinkNode; //定义队列 typedef struct{ LinkNode *front,*rear; }LinkQueue; void initQueue(LinkQueue *Q){ Q->front=(LinkNode*)malloc(sizeof(LinkNode));//定义头结点 Q->rear=Q->front; Q->front->next=NULL; } bool isEmpty(LinkQueue *Q){ if(Q->rear==Q->front){ return true; }else{ return false; } } //入队 void enQueue(LinkQueue *Q,int e){ LinkNode* p=(LinkNode*)malloc(sizeof(LinkNode)); p->data=e; p->next=NULL; Q->rear->next=p; Q->rear=p; } //出队 int deQueue(LinkQueue *Q){ if(isEmpty(Q)){ exit(-1); } LinkNode* p; p=Q->front->next; int e=p->data; Q->front->next=p->next; if(Q->rear==p){//如果是最后一个结点出队则初始化rear指针 Q->rear=Q->front; } free(p); return e; } void print(int *a,int size){ for(int i=0;i<size;i++){ printf("%d ",a[i]); } printf("\n"); } //取个位/十位/百位/...的值,i==1时取个位数,i=2时取十位数,i=3时取百位数 int GetNum(int a,int i){ int n; if(i==1){ n=a%10; }else{ int k=(int)pow(10,i-1);//pow(a,b)的意思是:求a的b次方 n=a/k%10; } return n; } //获取序列中最大数的位数 int GetMaxLength(int *a,int size){ if(size==0){ return -1; } int index=1; for(int i=0;i<size;i++){ int temp=1; int r=10; while(a[i]/r>0){ temp++; r=r*10; } if(index<temp){ index=temp; } } return index; } void BaseSort(int a[],LinkQueue *Q,int NumberLength,int size){ for(int i=1;i<=NumberLength;i++){//分配过程 for(int j=0;j<size;j++){ int k=GetNum(a[j],i); enQueue(&Q[k],a[j]); } for(int j=0,p=0;j<=9&&p<size;j++){//收集过程 while(!isEmpty(&Q[j])){ int x; x=deQueue(&Q[j]);//逐个从队列中取出数据放入到a数组中。形成新的序列 a[p]=x; p++; } } print(a,size); } } void main(){ LinkQueue Q[10];//声明一个队列数组 for(int i=0;i<10;i++){ initQueue(&Q[i]);//逐个初始化队列数组 } int a[]={278,109,63,930,589,184,505,269,8,83}; int size= sizeof(a)/ sizeof(int);//获取基数(也就是几进制)本例的结果是:40/4=10 int NumberLength=GetMaxLength(a,size); BaseSort(a,Q,NumberLength,size); }
空间效率:空间复杂度O(r),(一趟排序需要的辅助空间为r,即r个队列,r个队头指针和r个队尾指针,且后续趟数会重复使用这些空间)
时间复杂度:需要进行d趟分配和收集,每趟分配需要O(n),每趟收集需要O(r),故时间复杂度为O(d(n+r)),即与序列的初始态无关(d是序列中最大数的位数,r是基数,n是元素个数)
稳定性:稳定
8.6:内部排序算法的比较
列表比较:
基数排序 分配和收集 O(d(n+r)) O(d(n+r)) O(d(n+r)) O(r) 稳定 总结规律:
- 直接插入排序、折半插入排序、简单选择排序、冒泡排序——平均时间复杂度均为O(n^2)
- 直接插入排序、折半插入排序、希尔排序、冒泡排序、简单选择排序、堆排序——空间复杂度均为O(1)
- 直接插入排序、折半插入排序、冒泡排序、归并排序和基数排序——是稳定的排序方法
其他补充:
元素的移动次数与关键字的初始排列次序无关的是:
基数排序
。元素的比较次数与初始序列无关是:
选择排序
。算法的时间复杂度与初始序列无关的是:
简单选择排序
。(1)简单排序法一般只用于 n 较小的情况(例如 n<30)。当序列中的记录“基本有序” 时,直接插入排序是最佳的排序方法。如果记录中的数据较多,则应采用移动次数较少 的简单选择排序法。
(2)快速排序、堆排序和归并排序的平均时间复杂度均为 O(nlogn),但实验结果表明,就平均时间性能而言,快速排序是所有排序方法中最好的。遗憾的是,快速排序在最坏情况下的时间性能为 O(n^2)。堆排序和归并排序的最坏时间复杂度仍为 O(nlogn),当 n 较大时,归并排序的时间性能优于堆排序,但它所需的辅助空间最多。
(3)可以将简单排序法与性能较好的排序方法结合使用。例如,在快速排序中,当划分 子区间的长度小于某值时,可以转而调用直接插入排序法;或者先将待排序序列划分成 若干子序列,分别进行直接插入排序,然后再利用归并排序法,将有序子序列合并成一 个完整的有序序列。
(4)基数排序的时间复杂度可以写成 O(dn)。因此,它最适用于 n 值很大而关键字的位 数 d 较小的序列。当 d 远小于 n 时,其时间复杂度接近 O(n)。
(5)从排序的稳定性上来看,在所有简单排序法中,简单选择排序是不稳定的,其他各 种简单排序法都是稳定的。然而,在那些时间性能较好的排序方法中,希尔排序、快速 排序、堆排序都是不稳定的,只有归并排序、基数排序是稳定的。
n比较小的时候,适合 插入排序和选择排序
基本有序的时候,适合 直接插入排序和冒泡排序初始数据基本反序,则选用 选择排序
n很大但是关键字的位数较少时,适合 链式基数排序
n很大的时候,适合 快速排序 堆排序 归并排序
无序的时候,适合 快速排序
稳定的排序:冒泡排序 插入排序 归并排序 基数排序
复杂度是O(nlogn):快速排序 堆排序 归并排序
辅助空间(大 次大):归并排序 快速排序
好坏情况一样:简单选择(n^2),堆排序(nlogn),归并排序(nlogn)
最好是O(n)的:插入排序 冒泡排序
内部排序算法的应用
选取排序算法需要考虑的因素
- 待排序元素个数n
- 元素本身信息量大小
- 关键字的结构和分布情况
- 稳定性的要求
- 语言工具的条件,存储结构及辅助空间大小
选择排序的过程:
- 若n较小,可采用直接插入排或简单选择排,若记录本身信息量大则再特定选择简单选择排
- 若关键字基本有序,则可采用选直接插入或冒泡排序
- 中等规模元素序列n<=1000,希尔排序是一种很好的选择
- 若n较大,则可采用O(nlogn)的排序算法:快速排、堆排序或归并排序;快排针对关键字随机分布时时间最短,而堆排序所需辅助空间少于快排,但以上两着都不稳定;若要求稳定则选择归并排;若n很大但关键字位数较少则可采用基数排序
外部排序
外部排序定义:针对大文件精选的排序,需将待排序的记录存储在外存上,排序时一部分一部分地加入到内存进行排序(需多次进行内存和外存的交换)(在外部排序中时间代价主要考虑访问磁盘的次数(即I/O次数)。
外部排序的算法思想:外部排序一般采用归并排序法,它包括两个阶段
- 根据内存缓冲区大小,将外存上的文件分成若干子文件,依次读入到内存并利用内部排序对其进行排序,然后将排序后得到的有序子文件(也叫归并段)重新写回外存。
- 对归并段进行逐趟归并,使归并段逐渐由小到大,直至整体有序为止。
举例说明:给你一个包含20亿个int类型整数的文件,计算机的内存只有2GB,怎么给它们排序?一个int数占4个字节,20个亿需要80亿字节,大概占用8GB的内存,而计算机只有2GB的内存,数据都装不下!可以把8GB分割成4个2GB的数据来排,然后在把他们拼凑回去。如下图:
排序的时候可以选择快速排序或归并排序等算法。接着把两个小的有序子串合并成一个大的有序子串。
更加好理解抽象化例子:这里我们具体解释一下如何把2个2G字串合并成4G的(我们假设排序的数有12个{70.81,2,86,3,24,8,87,17,46,30,64},内存一次只能装3个数,则把数据分成4份依次加载到内存):
(1)分4次,每次读入3个数到内存,然后使用内部排序排好,再让写入到外存
(2)然后把字串两两和并:分别读入不同段的两个数字到内存,选择较小的写出到外存,然后再读入新数字替换掉之前写出的数,依次类推,直到合并成含有6个int的有序子串
........
(3)最后把2个6个int有序子串同样方法,依次读入内存选择最小者写入外存,进而合并成12个int数据
按照以上方法,每个数据都从硬盘读了3次,写了3次
增加归并路数k能减少归并趟数,刚才我们采用两两合并(2路归并)方法,现在我们采用4个有序字串进行合并(4路归并)的方法,这样我们每个数据读写次数就各是2次了
(1)第一步同上没有变化
(2)第二部,分别读入2,3,8,30,然后选择较小数写入到外存;然后再读入70复制到2的位置,再次选择较小数写入外存,直到所有数合并完毕,按照此方法,每个数都读写了2次,这样会大大节省时间
外排总时间=内排时间+外存信息读写时间+内部归并所需时间,其中外存读写时间远远大于内排和内部归并时间,因此增大归并路数,可减少归并趟数,从而减少磁盘I/O次数。
败者树
增加归并路数k能减少归并趟数,进而减少I/O次数,但内部归并的时间会增加,为了使内部归并不受归并路数k的影响,引入败者树。使用败者树后,内部归并的次数与k无关了,只要内存空间允许,增大k就能有效地减少I/O次数,提高外派速度。
置换-选择排序
减少初始归并段个数也可减少归并趟数,比如4个字串需归并3次,2个字串需归并1次,我们还是以{70,81,2,86,3,24,8,87,17,46,30,64}为例:
我们取3个读入到内存,然后选出最小数写入子串p1中,然后再读入一个数覆盖上次已写入的数的位置,选择最小数且比p1中其他数大的数写入子串p1(这个选择过程可用堆排序),一直重复,直到p1存放结束
...
....
直到没有符合条件的数存在为止(通过这种方法每个外存字串存了4个数(之前的方法是存3个数)),然后使用p2来继续存放
最后只生成了两个有序字串
最佳归并树
文件经过置换-选择排序后,得到的使长度不等的初始归并段,如何将长度不等的初始归并段归并排序,使得I/O次数最少?假设置换-选择得到9个初始归并段,其长度为9,30,12,18,3,17,2,6,24,做3路平衡归并
9.补充
声明:文中引用了王道数据结构书中的内容