赞
踩
本章内容只是数据结构概述,并不在考研大纲范围内。
一个语句的频度是指该语句在算法中被重复执行的次数。算法中所有语句的频度之和记为T(n),它是该算法问题规模n的函数,时间复杂度主要分析T(n)的数量级。
T(n)=O(f(n))
加法规则:O(f(n))+O(g(n))=O(max(f(n),g(n)))
乘法规则:O(f(n))×O(g(n))=O(f(n)×g(n))
常见的渐进时间复杂度:“常对幂指阶”
O(1)<O(log2n)<O(n)<O(nlog2n)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n)
判断时间复杂度的方法:
1、循环主体的变量参与循环条件的判断:在用于递推实现的算法中,首先找出基本运算的执行次数x与问题规模n之间的关系式,解得x=f(n),f(n)最高次幂为k,则算法的时间复杂度为O(n^k)。
2、循环主体中的变量与循环条件无关:此类题可采用数学归纳法或直接累计循环次数。多层循环时从内到外分析,忽略单步语句、条件判断语句、只关注主体语句的执行次数。此类问题又分为递归程序和非递归程序:
- 递归程序:一般使用公式进行递推。时间复杂度分析如下:
T(n)=1+T(n-1)=1+1+T(n-2)=...=n-1+T(1)- 非递归程序:分析较为简单可以直接累计次数。
算法的空间复杂度S(n)定义为该算法所需的存储空间,它是问题规模n的函数,记为
S(n)=O(g(n))
一个程序在执行时除需要存储空间来存放本身所用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为实现计算所需信息的辅助空间。若输入数据所占空间只取决于问题本身,和算法无关,则只需要分析除输入和程序之外的额外空间。
算法原地工作:是指算法所需的辅助空间为常量,即O(1)
本章很有可能考察算法题哦,但是因为其代码量较小,故而需要追求最优的性能(思想)。
静态实现:
- #define MaxSize //定义线性表的最大长度
- typedef struct{
- ElemType data[MaxSize]; //顺序表的元素
- int length; //顺序表的当前长度
- }SqList; //顺序表的类型定义
动态实现:
- #define InitSize 10 //表长度的初始定义
-
- typedef struct {
- ElemType *data; //指示动态分配数组的指针
- int MaxSize; //顺序表的最大容量
- int length; //顺序表的当前长度
- }SeqList; //动态分配数组顺序表的类型定义
C++的初始动态分配语句为:
L.data=new ElemType[InitSize];
1.顺序表的初始化
静态分配和动态分配的初始化操作是不同的,静态分配在声明一个顺序表时,就已经为其分配了数据空间,因此初始化时只需要将顺序表当前的长度设为0。
- //SqList L; //声明一个顺序表
- void InitList(SqList &L) {
- L.length = 0; //顺序表初始长度为0
- }
动态分配的初始化为顺序表分配一个预定义大小的数组空间,并将顺序表的当前长度设为0。
- void InitList(SqList &L) {
- L.data = (int *)malloc(InitSize * sizeof(int));//分配存储空间
- L.length = 0; //顺序表的初始长度为0
- L.MaxSize = InitSize; //初始存储容量
- }
2.插入操作
- bool ListInsert(SqList &L, int i, int e) {
- if (i < 1 || i > L.length+1) //判断i的范围是否有效
- return false;
- if (L.length >= MaxSize) //判断存储空间是否已满
- return false;
- for (int j = L.length; j >= i; j--) //将第i个元素之后的元素后移
- L.data[j] = L.data[j-1];
- L.data[i-1] = e; //在位置i处放入e
- L.length++; //线性表长度加1
- return true;
- }
3.删除操作
- bool ListDelete(SqList &L, int i, int &e) {
- if (i < 1 || i > L.length) //判断i的范围是否有效
- return false;
- e = L.data[i-1]; //将被删除的元素赋值给e
- for (int j = i; j < L.length; j++) //将第i个位置后的元素前移
- L.data[j-1] = L.data[j];
- L.length--; //线性表长度减1
- return true;
- }
4.按值查找(顺序查找)
- int LocateElem(SqList L, ElemType e) {
- for (int i = 0; i < L.length; i++)
- if (L.data[i] == e)
- return i+1; //数组下标为i的元素值等于e,返回其位序i+1
- return 0; //退出循环,说明查找失败
- }
(插入、删除、查找)时间复杂度:
是指通过一组任意的存储单元来存储线性表中的数据元素。为了建立数据元素之间的线性关系,对每个链表节点,除存放自身的信息之外,还需要存放一个指向其后继的指针。
单链表中节点类型的描述如下:
- typedef struct LNode{ //定义单链表结点类型
- ElemType data; //数据域
- struct LNode *next; //指针域
- }LNode, *LinkList;
特点:
实现方式:
带头结点的单链表在初始化时,需要创建一个头结点,并让头指针指向头结点,头结点的next域初始化为NULL。
- //带头结点的单链表的初始化
- bool InitList(LinkList &L){
- L = (LNode *)malloc(sizeof(LNode));//创建头结点
- if (L == NULL) //内存不足,分配失败
- return false;
- L->next = NULL; //头结点之后暂时还没有结点
- return true;
- }
-
- //不带头结点的单链表的初始化
- bool InitList(LinkList &L){
- L = NULL; //空表,暂时还没有任何结点
- return true;
- }
- int Length(LinkList L){
- int len=0; //计数变量,初始为0
- LNode *p=L;
- while(p->next!=NULL){
- p=p->next;
- len++; //每访问一个结点,计数加1
- }
- return len;
- }
求表长的时间复杂度是O(n)。另需注意的是,因为单链表的长度是不包括头结点的,因此不带头系结点和带头结点的单链表在求表长操作上会略有不同。
- LNode * GetElem(LinkList L, int i){
- if(i<0)
- return NULL;
- LNode *p=L; //指针p当前扫描的结点
- int j=0; //记录当前结点的位序,头结点是第0个结点
- while(p!=NULL && j<i){ //循环找到第i个结点
- p = p->next;
- j++;
- }
- return p; //返回第i个结点的指针或NULL
- }
该操作的时间复杂度为O(n) 。
- LNode * LocateElem(LinkList L, ElemType e){
- LNode *p = L->next;
- while(p!=NULL && p->data != e){ //从第一个结点开始查找数据域为e的结点
- p = p->next;
- }
- return p; //找到后返回该结点指针,否则返回NULL
- }
该操作的时间复杂度为O(n) 。
1.按位序插入(不带头结点)
- bool ListInsert(LinkList &L, int i, ElemType e){
- if(i<1)//判断i的合法性
- return false;
- if(i==1){//需要判断插入的位置是否是第1个
- LNode *s = (LNode *)malloc(size of(LNode));
- s->data = e;
- s->next = L;
- L = s; //头指针指向新结点
- return true;
- }
-
- //i>1的情况与带头结点一样,唯一区别是j的初始值为1
- LNode *p = L; //指针p指向当前扫描到的结点
- int j = 1; //当前p指向的是第几个结点
-
- //循环找到第i-1个结点
- while(p!=NULL && j<i-1){ //如果i>lengh,p最后会等于NULL
- p = p->next;
- j++;
- }
-
- if (p == NULL) //p值为NULL说明i值不合法
- return false;
- //在第i-1个结点后插入新结点
- LNode *s = (LNode *)malloc(sizeof(LNode));
- s->data = e;
- s->next = p->next;
- p->next = s;
- return true;
- }
该操作的时间复杂度为O(n) 。
前插操作是指在某结点的前面插入一个新结点,后插操作的定义刚好与之相反。
- s->next = p->next;
- p->next = s;
- e = p->data; // 交换两个结点中的数据
- s->data = p->data;
- p->data = e;
2.采用头插法建立单链表
该方法从一个空表开始,生成新结点,并将读取到的数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头(头结点之后)。
- LinkList List_HeadInsert(LinkList &L){ //逆向建立单链表
- LNode *s;
- int x; //设元素类型为整型
- L = (LinkList)malloc(sizeof(LNode)); //创建头结点
- L->next = NULL; //初始为空链表(这步很关键)
- scanf("%d", &x); //输入要插入的结点的值(数据域)
- while(x!=9999){ //输入9999表示结束
- s = (LNode *)malloc(sizeof(LNode)); //创建新结点
- s->data = x;
- s->next = L->next;
- L->next = s; //将新结点插入表中,L为头指针
- scanf("%d", &x);
- }
- return L;
- }
采用头插法建立单链表时,读入数据的顺序与生成的链表中元素的顺序时相反的。可用来实现链表的逆置。
- // 将链表L中的数据逆置并返回
- LNode *List_Inverse(LNode *L){
- LNode *p, *q;
- p = L->next; //p指针指向第一个结点
- L->next = NULL; //头结点置空
- while (p != NULL){ //依次判断p结点中的数据并采用头插法插到L链表中
- q = p;
- p = p->next;
- q->next = L->next;
- L->next = q;
- }
- return L;
- }
3.采用尾插法建立单链表
头插法建立单链表的算法虽然简单,但生成的链表中结点的次序和输入数据的顺序不一样。若希望两者次序一样,则可以采用尾插法。该方法将新结点插入到当前链表的表尾,为此必须增加一个尾指针r,使其始终指向当前链表的尾结点。
- LinkList List_TailInsert(LinkList &L){ //正向建立单链表
- int x; //设ElemType为整型int
- L = (LinkList)malloc(sizeof(LNode)); //建立头结点(初始化空表)
- LNode *s, *r = L; //r为表尾指针
- scanf("%d", &x); //输入要插入的结点的值
- while(x!=9999){ //输入9999表示结束
- s = (LNode *)malloc(sizeof(LNode));
- s->data = x;
- r->next = s;
- r = s; //r指针指向新的表尾结点
- scanf("%d", &x);
- }
- r->next = NULL; //尾结点指针置空
- return L;
- }
- bool ListDelete(LinkList &L, int i, ElemType &e){
- if(i<1)
- return false;
- LNode *p=L; //指针p指向当前扫描到的结点
- int j=0; //记录当前结点的位序,头结点是第0个结点
-
- while(p!=NULL && j<i-1){ //循环找到第i-1个结点
- //如果i>lengh,p和p的后继结点会等于NULL
- p = p->next;
- j++;
- }
- if(p==NULL)
- return false;
- if(p->next == NULL)
- return false;
-
- LNode *q = p->next; //令q指向被删除的结点(暂时存储)
- e = q->data; //用e返回元素的值
- p->next = q->next; //将*q结点从链中“断开”
- free(q) //释放结点的存储空间
- return true;
- }
同插入算法一样,该算法的主要时间也耗费在查找操作上,时间复杂度为O(n)。当链表不带头结点时,需要判断被删结点是否为首结点,若是,则要做特殊处理,将头指针L指向新的首结点。反之,则是相同的。
拓展:删除指定结点*p
通常的做法是先从链表的头结点开始顺序查找到其前驱,然后执行删除操作。但其实,删除结点*p的操作可用删除其后继来实现,实质就是将其后继的值赋予其自身,然后再删除后继,也能使时间复杂度为O(1)。
- *q = p->next; //令q指向p的后继结点
- //如果p是最后一个结点,则q指向NULL,继续执行就会报错
- p->data = q->data; //用后继结点的数据域覆盖
- p->next = q->next; //将*q结点从链中“断开”
- free(q); //释放后继结点的存储空间
注:单链表是整个链表的基础,一定要熟练掌握单链表的基本操作算法。
为了克服单链表的缺点(要访问某个结点的前驱(插入、删除操作时),只能从头开始遍历)引入了双链表,双链表结点中有两个指针prior和next,分别指向直接前驱和直接后继。
双链表中结点类型的描述如下:
- typedef struct DNode{ //定义双链表结点类型
- ElemType data; //数据域
- struct DNode *prior,*next; //前驱和后继指针
- }DNode, *DLinklist;
双链表再单链表的基础上增加了一个指向其前驱的指针prior,因此双链表的按值查找和按位查找与单链表的相同。但是再插入和删除操作的实现上,与单链表有着较大的差异。这是因为“链”变化时也需要对指针prior做出修改,其关键是保证在修改的过程中不断链。(时间复杂度也仅为O(1))
1. 双链表的插入操作
- s->next = p->next; //将结点*s插入到结点*p之后
- p->next->prior = s;
- s->prior = p;
- p->next = s;
注:上诉代码的语句顺序不是唯一的,但也不是任意的,第一句必须在第四句之前,否则*p的后继结点的指针就会丢掉,导致插入失败。
2. 双链表的删除操作
- p->next = q->next;
- q->next->prior=p;
- free(q);
1. 循环单链表
循环单链表与单链表的区别在于,表中最后一个结点的指针不是NULL,而是改为指向头结点,从而整个链表形成一个环。(故循环单链表的判空条件不是头结点的指针是否为空,而是尾结点的指针是否等于头指针)首尾相等
2. 循环双链表
与循环单链表类似。在循环双链表中就只是把头结点的prior指针还要指向其表尾结点。
用数组的方式实现的链表。分配一整片连续的内存空间(与顺序表相同),各个结点集中安置,每个结点包括了数据元素和下一个结点的数组下标。
- #define MaxSize 10 //静态链表的最大长度
- struct Node{ //静态链表结构类型的定义
- ElemType data; //存储数据元素
- int next; //下一个元素的数组下标
- } SLinkList[MaxSize];
注:静态链表以 next ==-1作为其结束的标志。静态链表的插入、删除操作与动态链表的相同。只需要修改指针,而不需要移动元素。总体来说,静态链表没有单链表使用起来方便,buttt在一些不支持指针的高级语言中,这是一种非常巧妙地设计方法。
1. 存取(读/写)方式
顺序表可以顺序存取,也可也随机存取,链表只能从表头开始依次顺序存取。
2. 逻辑结构与物理结构
采用顺序存储时,逻辑上相邻的元素,对应的物理存储位置也相邻。而采用链式存储时,逻辑上相邻的元素,物理存储位置不一定相邻,对应的逻辑关系是通过指针链接来表示的。
3. 查找、插入和删除操作
查找 | 按位查找 | 按值查找 |
顺序表 | O(1) | O(n) | O(log2n)若元素有序,(二分法) |
链表 | O(n) | O(n) |
顺序表的插入、删除操作,平均需要移动半个表长的元素。
链表的插入、删除操作,只需修改相关结点的指针域即可。
4. 空间分配
顺序表一旦确定了就无法更改。
栈的本质上就是一种受限的线性表,故而它也有两种存储方式。
1. 顺序栈的实现
- #define MaxSize 10 //定义栈中元素的最大个数
- typedef struct{
- ElemType data[MaxSize]; //静态数组存放栈中元素
- int top; //栈顶元素
- }SqStack;
注:方法不唯一,也可以是初始设置栈顶指针S.top=0;进栈时先将值送到栈顶,栈顶指针再加1;出栈时,栈顶指针先减1,再取栈顶元素;空条件是s.top==0;栈满条件是S.top==MaxSize。
根本区别就在于栈顶指针是否有指向实体元素。
2. 顺序栈的基本操作
- // 初始化栈
- void InitStack(SqStack &S){
- S.top = -1; //初始化栈顶指针
- }
-
- // 判断栈是否为空
- bool StackEmpty(SqStack S){
- if(S.top == -1)
- return true;
- return false;
- }
-
- // 进栈
- bool Push(SqStack &S, ElemType x){ // 判断栈是否已满
- if(S.top == MaxSize - 1)
- return false;
- S.data[++S.top] = x; //指针先加1,再入栈
- return true;
- }
-
- // 出栈
- bool Pop(SqStack &x, ElemType &x){ // 判断栈是否为空
- if(S.top == -1)
- return false;
- x = S.data[S.top--]; //先出栈,指针再减1
- return true;
- }
-
- // 读栈顶元素
- bool GetTop(SqStack S, ElemType &x){
- if(S.top == -1)
- return false;
- x = S.data[S.top]; //x记录栈顶元素
- return true;
- }
注(Again):这里的top指向的是栈顶元素,如果S.top=0,则相对应的栈空、栈满、入栈操作和出栈操作也会发生变化。
3. 共享栈
利用栈底位置相对不变的特性,可以让两个顺序栈共享一个一维数组空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶相共享空间的中间延伸。
- #define MaxSize 10 //定义栈中元素的最大个数
- typedef struct{
- ElemType data[MaxSize]; //静态数组存放栈中元素
- int top0; //0号栈栈顶指针
- int top1; //1号栈栈顶指针
- }ShStack;
-
- // 初始化栈
- void InitSqStack(ShStack &S){
- S.top0 = -1;
- S.top1 = MaxSize;
- }
注:两个栈的栈顶指针都指向栈顶元素,top0=-1时为0号栈为空,top1=MaxSize时1号栈为空;当且仅当两个栈顶指针相邻(top1-top0=1)时,判断为栈满。当0号栈进栈时top0先加1再赋值(1号栈则相反,先减1再赋值);出栈时则刚好相反。
采用链式存储,优点是便于多个栈共享存储空间和提高其效率,且不存在栈满上溢的情况。通常采用单链表实现,并规定所有操作都是在单链表的表头进行的。
- typedef struct Linknode{
- ElemType data; //数据域
- Linknode *next; //指针域
- }LiStack;
采用链式存储便于结点的插入和删除。操作与链表相似,入栈和出栈的操作都在链表的表头进行。
注:带表头和不带表头的链栈,具体的实现会有所不同。
基本操作:
注:栈和队列是受限的线性表,因此不是任何对线性表的操作都可以作为栈和队列的操作。比如,不可以随便读取栈或队列中间的某个数据。
1. 队列的顺序存储
队列的顺序实现是指分配一块连续的存储单元存放队列的元素,并附设了两个指针:队头指针front指向对头元素,队尾指针rear指向队尾元素的下一个位置(这个不一定哦,不同教材可能对这两个指针的定义都不一样,做习题的时候需要擦亮自己的bigeyes)
- #define MaxSize 10; //定义队列中元素的最大个数
-
- typedef struct{
- ElemType data[MaxSize]; //用数组存放队列元素
- int front, rear; //队头指针和队尾指针
- }SqQueue;
注:Q.rear==MaxSize不能作为队列满的条件,会产生假溢出。
2. 循环队列
将顺序队列臆造成一个环状的空间,即把存储队列元素的表从逻辑上视为一个环,称为循环队列。
当队首指针Q.front=MaxSize-1后,再前进一个位置就自动到0,这可以利用除法取余运算(%)来实现。
注:循环队列中不能仅仅使用 Q.front==Q.rear 作为判断队空/队满的条件。
故产生了以下三种解决方式。
- 牺牲一个单元来区分队空和队满,即将 (Q.rear+1)%MaxSize == Q.front) 作为判断队列是否已满的条件。(主流方法)Q.front==Q.rear作为判断队空的条件。
- 类型中增设size数据成员,表示元素个数。删除成功size减1,插入成功size加1。队空时Q.size==0;队满时Q.size==MaxSize,两种情况都有Q.front==Q.rear。
- 类型中增设tag数据成员,以区分队满还是队空。删除成功置tag=0,若导致Q.front==Q.rear,则为队空;插入成功置tag=1,若导致Q.front==Q.rear,则为队满。
3. 循环队列的操作
1)初始化
- void InitQueue(SqQueue &Q){
- Q.rear = Q.front = 0; //初始化队首、队尾指针
- }
2)判队空
- bool QueueEmpty(SqQueue Q){
- if(Q.rear == Q.front)
- return true;
- else
- return false;
- }
3)入队
- bool EnQueue(SqQueue &Q, ElemType x){
- if((Q.rear+1)%MaxSize == Q.front) //牺牲一个单元区分队空和队满
- return false;
- Q.data[Q.rear] = x;
- Q.rear = (Q.rear+1)%MaxSize; //队尾指针加1取模
- return true;
- }
4)出队
- bool DeQueue(SqQueue &Q, ElemType &x){
- if(Q.rear == Q.front) //队空则报错
- return false;
- x = Q.data[Q.front];
- Q.front = (Q.front+1)%MaxSize; //队头指针加1取模
- return true;
- }
1. 队列的链式存储
队列的链式结构表示称为链队列,它实际上是一个同时有队头指针和队尾指针的单链表,头指针指向对头结点,尾指针指向队尾结点,即单链表的最后一个结点。
- // 链式队列结点
- typedef struct LinkNode{
- ElemType data;
- struct LinkNode *next;
- }
-
- // 链式队列
- typedef struct{
- // 头指针和尾指针
- LinkNode *front, *rear;
- }LinkQueue;
注:不带头结点时,当 Q.front==NULL 且 Q.rear==NULL 时,链式队列为空。
2. 链式存储的基本操作
1)初始化
- void InitQueue(LinkQueue &Q){
- //初始化时,front、rear都指向头结点
- Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode));
- Q.front -> next = NULL; //初始为空
- }
2)判队空
- //判断队列是否为空
- bool IsEmpty(LinkQueue Q){
- if(Q.front == Q.rear) //也可用 Q.front -> next == NULL
- return true;
- else
- return false;
- }
3)入队操作
- //新元素入队 (表尾进行)
- void EnQueue(LinkQueue &Q, ElemType x){
- LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode)); //申请一个新结点
- s->data = x;
- s->next = NULL; //s作为最后一个结点,指针域指向NULL
- Q.rear->next = s; //新结点插入到当前的rear之后
- Q.rear = s; //表尾指针指向新的表尾
- }
4)出队操作
- //队头元素出队
- bool DeQueue(LinkQueue &Q, ElemType &x){
- if(Q.front == Q.rear)
- return false; //空队
-
- LinkNode *p = Q.front->next; //p指针指向即将删除的结点 (头结点所指向的结点)
- x = p->data;
- Q.front->next = p->next; //修改头结点的next指针
- if(Q.rear == p) //此次是最后一个结点出队
- Q.rear = Q.front; //修改rear指针
- free(p); //释放结点空间
-
- return true;
- }
定义:双端队列是指允许两端都可以进行入队和出队操作的队列。
((())) 最后出现的左括号最先被匹配 (栈的特性—LIFO);
遇到左括号就入栈;
遇到右括号,就“消耗”一个左括号 (出栈);
匹配失败情况:
扫描到右括号且栈空,则该右括号单身;
扫描完所有括号后,栈非空,则该左括号单身;
左右括号不匹配;
1、算术表达式
2、中缀表达式转后缀表达式
手算方法:
- 按照运算符的运算规则对所有的运算单位加括号。
- 将运算符移至对应括号的后面,相当于按“左操作数 右操作数 运算符“重新组合。
- 去除所有的括号。
计算方法:
在计算机中,中缀表达式转后缀表达式时需要借助一个栈,用于保存暂时还不能确定运算顺序的运算符。从左到右依次扫描中缀表达式中的每一项,具体转化过程如下:
- 遇到操作数。直接加入后缀表达式。
- 遇到界限符,若为“(”,则直接入栈;若为“)”,则依次弹出栈中的运算符,并加入后缀表达式,直到弹出“(”为止。注意,“(”直接删除,不加入后缀表达式。
- 遇到运算符。若其优先级高于除“(”外的栈顶运算符,则直接入栈。否者,从栈顶开始,依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,直到遇到一个优先级低于它的运算符或遇到“(”为止,之后将当前运算符入栈。
按上诉方法扫描所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式。
eg:A+B*(C-D)-E/F(会手算的就行了)
3、后缀表达式求值
通过后缀表示计算表达式的值的过程:
- 从左往右依次扫描表达式的每一项,若该项是操作数,则将其压入栈中;
- 若该项是操作符<op>,则从栈中退出两个操作数Y和X,形成运算指令X<op>Y,并将计算结果压入栈中。
后缀表达式 ABCD-*+EF 求值过程如下
在信息处理中有一大类问题需要逐层或逐行处理。这类问题的解决方法往往是在处理当前层或当前行时就对下一层或下一行做预处理,把处理顺序安排好,等到当前层或当前行处理完毕,就可以处理下一层或下一行。使用队列是为了保存下一步的处理顺序。
队列在计算机系统中的应用非常广泛,以下仅从两个方面来阐述:第一个方面是解决主机与外部设备之间数度不匹配的问题,第二个方面是解决由多用户引起的资源竞争问题。
由n(n>=1)个相同类型的数据元素构成的有限序列,每个数据元素称为一个数组元素,每个元素在n个线性关系中的序号称为该元素的下标,下标的取值范围称为数组的维界。
注:数组和线性表之间的关系:数组是线性表的推广。
一维数组:
二维数组:
设二维数组的行下标和列下标的范围分别是[0,h1]与[0,h2]。
注:行就找列,列就找行。
压缩存储: 多个值相同的元素只分配一个空间,0不分配空间。
特殊矩阵的压缩存储方法:找出特殊矩阵中值相同的矩阵元素的分布规律,把那些呈现规律性分布的、值相同的多个矩阵元素压缩存储到一个存储空间内。
1、对称矩阵
注:矩阵元素aij通常是从1开始的。压缩完的尺寸为[n(n+1)/2]。
元素下标之间的关系:
2、三角矩阵
下三角矩阵:
元素下标之间的对应关系为:
下三角矩阵在内存中的压缩形式如下:
注:压缩完的尺寸为[n(n+1)/2+1]
上三角矩阵:
元素下标之间的对应关系为:
上三角矩阵在内存中的压缩形式如下:
注:以上推导均假设数组的下标从0开始。直接记也很困难,最好还是会自己推导。
3、三对角矩阵
对角矩阵也称为带状矩阵。
三对角矩阵A也可以采用压缩存储,将3条对角线上的元素按行优先方式存放在一维数组B中,且a1,1存放与B[0]中,其存储方式如图所示。
k=2i+j-3(三对角矩阵压缩存储的下标对应关系)
i=[(k+1)/3+1](向下取整)
矩阵中非零元素的个数t,相对矩阵元素的个数s来说非常少,即s>>t的矩阵称为稀疏矩阵。
压缩存储策略:
'\0'
初始化。)1. 定长顺序表示
串的顺序存储结构是用一组地址连续的存储单元来存储字符序列的。按照预定义的大小,为每个定义的串变量分配一个固定长度的存储区。一般用定长数组来定义。
- #define MAXLEN 255 //预定义最大串长为255
- typedef struct {
- char ch[MAXLEN]; //每个分量存储一个字符
- //存储串的一维数组,ch[0]~ch[255],共256个;
- //通常情况下ch[0]存放串的长度,或者闲置不用,真正串的类容从ch[1]开始
- int length; //串的实际长度
- }SString;
注:串的实际长度只能小于或等于MAXLEN,超过预定长度的串指会被舍去,称为截断。
串长有两种表示方法:一是如上诉定义描述的那样,用一个额外变量len来存放串的长度;二是在串值后面加一个不计入串长的结束标记字符"\0",此时串长为隐含值。
为了克服截断,可以采用动态分配的方法。
2. 堆分配存储表示
堆分配存储表示仍然以一组地址连续的存储单元存放串值的字符序列,但他们的存储空间是在程序执行的过程中动态分配得到的。
- typedef struct st {
- char *ch; //按串长分配存储区,ch指向串存放的起始地址
- int length; //串的长度
- }HString;
注:在C语言中存在一个称之为”堆“的自由存储区,并用malloc()和free()函数来完成动态存储管理。
3. 块链存储表示
串的块链存储,指的是使用链表结构存储字符串。由于串的特殊性(每个元素只有一个字符),在具体实现时,每个结点即可以存放一个字符,也可以存放多个字符。每个结点称为块,故整个链表称为块链结构。
1. 暴力匹配算法
- int Index(SString S, SString T){
- int i=1; //扫描主串S
- int j=1; //扫描模式串T
- while(i<=S.length && j<=T.length){
- if(S.ch[i] == T.ch[j]){
- ++i;
- ++j; //继续比较后继字符
- }
- else{
- i = i-j+2;
- j=1; //指针后退重新开始匹配
- }
- }
- if(j>T.length)
- //匹配成功则返回与模式T中第一个字符相等的字符在主串S中的序号,否则匹配不成功,函数值为0。
- return i-T.length;
- else
- return 0;
- }
注:主串长度为n,模式串长度为m,所以最多会比较n-m+1个子串。
最坏时间复杂度 = O(nm):每个子串都要对比m个字符(对比到最后一个字符才匹配不上),共要对比n-m+1个子串,复杂度 = O((n-m+1)m) = O(nm - m^2 + m) = O(nm)
buttttt大多数时候,n>>m
最好时间复杂度 = O(n):每个子串的第一个字符就匹配失败,共要对比n-m+1个子串,复杂度 = O(n-m+1) = O(n)
其实很容易从上述的暴力匹配中看出为什么效率较低。
在暴力匹配中,每趟匹配失败都是模式串后移一位再从头开始比较、而某趟已匹配相等的字符串序列是模式串的某个前缀,这种频繁的重复相当于模式串不断的进行自我比较,这也就是低效率的根源。
于是就出现了next数组,原理不过多赘述。
- void get_next(SString T,int next[]){
- int i=1,j=0;
- next[1]=0;
- while(i<T.length){
- if(j==0||T.length()){
- ++i;
- ++j;
- next[i]=j; //若pi=pj,则next[j+1]=next[j]+1
- }
- else{
- j=next[j]; //否则令j=next[j],循环继续
- }
- }
next数组的求解方法:
- next数组第一二位一定分别为0,1,后面求解每一位的next值时,根据前一位进行比较。
- 从第三位开始,将前一位与其next值对应的内容进行比较,
- 如果相等,则该位的next值就是前一位的next值加上1;
- 如果不等,向前继续寻找next值对应的内容来与前一位进行比较,
- 直到找到某个位上内容的next值对应的内容与前一位相等为止,
- 则这个位对应的值加上1即为需求的next值;
- 如果找到第一位都没有找到与前一位相等的内容,那么求解的位上的next值为1
公式:next[i]=匹配字符数+1
巴拉巴拉讲这么多不如直接看样例蛤
next数组第一二位一定分别为0,1:
看第三位,按照next数组求解方法。第三位a的前一位是第二位的b,b的next值是1对应内容是a,b与a不同,则继续向前寻找next值对应的内容与第二位的b进行比较。但是找到第一位都没有找到与第二位的b相等的内容,所以第三位a的next值为1,则:
看第四位的b,b的前一位a的next值1对应内容为a,相同,所以该位b的next值就是前一位a的next(第三位的a)值加上1,即为2:
看第五位a,a的前一位b的next值2对应内容为b,相等,所以第五位a的next值就是其前一位b的next值加上1,即为3:
看第六位a,a的前一位a的next值3对应内容为a,相等,所以该位a的next值就是前一位a的next值加上1,即为4:
看第七位a,a的前一位a的next值4对应内容为b,不相等,向前继续寻找next值对应的内容来与前一位进行比较,b的next值2对应的内容为b,依旧不相等,继续向前寻找,第二位b的next值1对应内容为a,相等。因为是在第二位b处实现的相等,所以第七位a的next值为第二位b的next值上加1,即为2:
后续就都是按照上述的过程,就不多赘述了,让我321直接上成品。
注:值的注意的是有的教材中所写的下标是从0开始的那我们就要整体减1(也就是有的题目要考虑两解)
利用next数组进行模式匹配
- int Index_KMP(SString S, SString T, int next[]){
- int i=1; //主串
- int j=1; //模式串
- while(i<S.length && j<=T.length){
- if(j==0 || S.ch[i]==T.ch[j]){ //第一个元素匹配失败时
- ++j;
- ++i; //继续比较后继字符
- }
- else
- j=next[j] //模式串向右移动
- }
- if(j>T.length)
- return i-T.length; //匹配成功
- }
注:尽管普通模式匹配的时间复杂度O(mn),KMP算法的时间复杂度是O(m+n),但在一般情况下,普通模式匹配的实际执行时间近似O(m+n),因此至今还在被采用
KMP算法仅在主串与子串有很多“部分匹配”时才显得比普通算法快得多,其主要优点时主串不回溯。
基本公式
注:本人手瓢上面的next也一样模式串中i均是j,后续也没有改。(imlazyboy)
nextval数组规定的第一项为0。
先将其next[i]所对应的值标在旁边,方便计算。
先找出不相等的(Pj != Pnextval[j])则nextval数组的值就等于相对应的next数组的值。
那接下来看相等的,nextval数组则等于相对应的next数组所指位置的nextval数组。
- void get_nextval(SString T, int nextval []){
- int i = 1;
- nextval[1] = 0;
- int j = 0;
- while(i<T.length){
- if (j == 0 || T.ch[i] == T.ch[j]){
- ++i; ++j;
- if (T[i] != T[j]){
- nextval[i] = j
- } else{
- nextval[i] = nextval[j]
- }
- }else{
- j = nextval[j];
- }
- }
- }
树是n(n>=0)个结点的有限集。
定义:
二叉树是n(n>=0)个结点的有限集,它或者是空集(n=0),或者由一个根结点及两颗互不相交的分别称作这个根的左子树和右子树的二叉树组成。
特点:
特殊的二叉树:
注:满二叉树是一种特殊的完全二叉树。
二叉树的性质:
1、顺序存储结构
二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来。(用一组连续的存储单元依次自上向下、自左向右存储完全二叉树上的结点元素,即将完全二叉树上编号为i的结点元素存储在一维数组下标为i-1的分量中。)
- #define MaxSize 100
-
- struct TreeNode{
- ElemType value; //结点中的数据元素
- bool isEmpty; //结点是否为空
- }
-
- main(){
- TreeNode t[MaxSize];
- for (int i=0; i<MaxSize; i++){
- t[i].isEmpty = true;
- }
- }
注:建议从数组下标1开始存储树中的结点,保证数组下标和结点编号一致。
依据二叉树的性质,完全二叉树和满二叉树采用顺序存储比较合适。
2、链式存储结构
由于顺序存储的空间利用率较低,因此二叉树一般都采用链式存储链式存储结构,用链表结点来存储二叉树中的每个结点。二叉链表结构如下。
- //二叉树的结点
-
- struct ElemType{
- int value;
- };
-
- typedef struct BiTnode{
- ElemType data; //数据域
- struct BiTNode *lchild, *rchild; //左、右孩子指针
- }BiTNode, *BiTree;
-
- //定义一棵空树
- BiTree root = NULL;
-
- //插入根节点
- root = (BiTree) malloc (sizeof(BiTNode));
- root -> data = {1};
- root -> lchild = NULL;
- root -> rchild = NULL;
-
- //插入新结点
- BiTNode *p = (BiTree) malloc (sizeof(BiTNode));
- p -> data = {2};
- p -> lchild = NULL;
- p -> rchild = NULL;
- root -> lchild = p; //作为根节点的左孩子
注:使用不同的存储结构时,实现二叉树操作的算法也会不同,因此要根据实际应用场合(二叉树的形态和需要进行的运算)来选择合适的存储结构。
是指按某条搜索路径访问树中每个结点,使得每个结点均被访问一次,而且仅被访问一次。由于二叉树是一种非线性结构,每个结点都可能有两棵子树,因此需要寻找一种规律,一遍使二叉树上的结点能排列在一个线性结构上,进而便于遍历。
- typedef struct BiTnode{
- ElemType data;
- struct BiTNode *lchild, *rchild;
- }BiTNode, *BiTree;
-
- void PreOrder(BiTree T){
- if(T!=NULL){
- visit(T); //访问根结点
- PreOrder(T->lchild); //递归遍历左子树
- PreOrder(T->rchild); //递归遍历右子树
- }
- }
-
- typedef struct BiTnode{
- ElemType data;
- struct BiTNode *lchild, *rchild;
- }BiTNode, *BiTree;
-
- void InOrder(BiTree T){
- if(T!=NULL){
- InOrder(T->lchild); //递归遍历左子树
- visit(T); //访问根结点
- InOrder(T->rchild); //递归遍历右子树
- }
- }
-
- typedef struct BiTnode{
- ElemType data;
- struct BiTNode *lchild, *rchild;
- }BiTNode, *BiTree;
-
- void PostOrder(BiTree T){
- if(T!=NULL){
- PostOrder(T->lchild); //递归遍历左子树
- PostOrder(T->rchild); //递归遍历右子树
- visit(T); //访问根结点
- }
- }
-
4、层次遍历
算法思想:
- //层序遍历
- void LevelOrder(BiTree T){
- LinkQueue Q;
- InitQueue (Q); //初始化辅助队列
- BiTree p;
- EnQueue(Q,T); //将根节点入队
- while(!isEmpty(Q)){ //队列不空则循环
- DeQueue(Q,p); //队头结点出队
- visit(p); //访问出队结点
- if(p->lchild != NULL)
- EnQueue(Q,p->lchild); //若左孩子不空,左孩子入队
- if(p->rchild != NULL)
- EnQueue(Q,p->rchild); //若右孩子不空,右孩子入队
- }
- }
5、 由遍历序列构造二叉树
若已知中序遍历,再给出其他三种遍历序列中的任意一种,就可以唯一地确定一棵二叉树。
- //线索二叉树结点
- typedef struct ThreadNode{
- ElemType data;
- struct ThreadNode *lchild, *rchild;
- int ltag, rtag; // 左、右线索标志
- }ThreadNode, *ThreadTree;
中序线索二叉树的构造
二叉树的线索化是将二叉链表中的空指针改为指向前驱或者后继的线索。而前驱或后继的信息只有在遍历时才能得到,因此线索化实际上就是遍历一遍二叉树。
- void InThread(ThreadTreeNode p, ThreadTreeNode pre) {
- if (p!=NULL) {
- /*递归线索化左子树*/
- InThread(p->lchild,pre);
-
- if (p->lchild == NULL) {
- p->ltag = 1;
- p->lchild = pre; //指向前驱结点
- }
- if (pre!=NULL&&pre->rchild==NULL) {
- pre->rchild = p; //指向后继结点
- pre->rtag = 1;
- }
-
- /*递归线索化右子树*/
- InThread(p->rchild, pre);
- }
- }
-
- //2.主过程算法
- void CreateInThread(ThreadTreeNode T) {
- ThreadTreeNode pre = NULL;
- if (T!=NULL) { //非空二叉树,进行线索化
- InThread(T,pre); //遍历线索化二叉树
- pre->rchild = NULL; //处理遍历的最后一个结点
- pre->rtag = 1;
- }
- }
为了方便,可以在二叉树的线索链表上也添加一个头结点,令其lchild域的指针指向二叉树的根结点,其rchild域的指针指向中序遍历时访问的最后一个结点;令二叉树中序序列中的第一个结点的lchild域指针和最后一个结点的rchild域指针均指向头结点。这好比为二叉树建立了一个双向线索链表,方便从前往后或从后往前对线索二叉树进行遍历。
中序线索二叉树的遍历
中序线索二叉树的结点隐含了线索二叉树的前驱和后继信息。在对其进行遍历时,只要先找到序列中的第一个结点,然后依次找结点的后继,直至其后继为空。在中序线索二叉树中找结点后继的规律是:若其右标志为“1”,则右链为线索,指示其后继,否则遍历右子树中第一个访问的结点(右子树中最左下的结点)为其后继。
1)求中序线索二叉树的中序序列中的第一个结点:
- ThreadNode *Firstnode(ThreadNode *p){
- while (p->ltag==0) { //结点有左树时,寻找最左端的树
- p = p->lchild; //找到最左下结点(不一定是叶结点哦)
- }
- return p;
- }
2)求中序线索二叉树结点p在中序序列下的后继:
- ThreadNode* Nextnode(ThreadNode *p){
- if(p->rtag==0)
- return Firstnode(p->rchild);
- else
- return p->rchild;
- }
3)不含头结点的中序线索二叉树的中序遍历的算法:
- void Inorder(ThreadNode *T){
- for(ThreadNode *p=Firstnode(T);p!=NULL;p=Nextnode(p))
- //printf("%c",p->data);
- visit(p);
- }
先序线索二叉树的构造
先序线索二叉树的构造和中序线索化的构造基本一样,唯一需要注意就是防止出现转圈问题。
- // 通过先序遍历对二叉树线索化
- void PreThread(ThreadTree p,ThreadTree &pre){
- if(p!=NULL){
- if(p->lchild==NULL){
- p->lchild = pre;
- p->ltag = 1;
- }
-
- if(pre!=NULL && pre->rchild==NULL){
- pre->rchild = p;
- pre->rtag = 1;
- }
- pre = p;
- if(p->ltag == 0) // 防止转圈问题
- PreThread(p->lchild,pre);
-
- PreThread(p->rchild,pre);
- }
- }
-
- void CreatePreThread(ThreadTree T){
- ThreadTree pre = NULL;
- if(T!=NULL){
- PreThread(T,pre);
- if(pre->rchild==NULL){
- pre->rtag = 1;
- }
- }
- }
后序线索二叉树的构造
- // 通过后序遍历对二叉树线索化
- void PostThread(ThreadTree p,ThreadTree &pre){
- if(p!=NULL){
- PostThread(p->lchild,pre);
- PostThread(p->rchild,pre);
-
- if(p->lchild == NULL){
- p->lchild = pre;
- p->ltag = 1;
- }
-
- if(pre!=NULL && pre->rchild==NULL){
- pre->rchild = p;
- pre->rtag = 1;
- }
- pre = p;
- }
- }
-
- void CreatePostThread(ThreadTree T){
- ThreadTree pre = NULL;
- if(T!=NULL){
- PostThread(T,pre);
- if(pre->rchild==NULL){
- pre->rtag = 1;
- }
- }
- }
在线索二叉树中找前驱和后继
- 中序线索二叉树
- 结点p的中序前驱结点
- 若p的ltag=1,lchild指向其前驱;
- 若p的ltag=0,p的前驱是以该结点为根的左子树上按中序遍历的最后一个结点。
- 结点p的中序后继结点
- 若p的rtag=1,rchild指向其后继;
- 若p的rtag=0,p的后继是以该结点为根的右子树上按中序遍历的第一个结点。
- 后序线索二叉树
- 查找结点*p的前驱
- 若结点*p无左子树,则p->lchild指向其前驱
- 若结点*p有左子树,当其右子树为空时,其左子树的根(即p->lrchild)为其后序前驱。当其右子树非空时,其右子树的根(即p->rchild)为其后序前驱。
- 查找结点*p的后继
- 若结点*p为根,则无后继;
- 若结点*p为其双亲的右孩子,则其后继为其双亲;
- 若结点*p为其双亲的左孩子,且双亲无右子女,则其后继为其双亲;
- 若结点*p为其双亲的左孩子,且双亲有右子女,则结点*p的后继是其双亲的右子树中按后序遍历的第一个结点。
所以,求后序线索二叉树中结点的后继要知道其双亲的信息,要使用栈,所以说后序线索二叉树是不完善的。
- 先序线索二叉树
- 在先序线索二叉树中查找结点的后继较容易,而查找前驱要知道其双亲的信息,要使用栈,所以说先序线索二叉树也是不完善的。
注:
一、前序与后序
- 序列相同
- 空树或者只有根节点的二叉树。
- 序列相反
- 当且仅当二叉树中只有一个叶子节点。
- 二叉树的高度和其节点个数相同。
二、前序与中序
- 序列相同
- 空树或缺左子树的单支二叉树。
- 序列相反
- 二叉树为空或者只有一个节点。
- 若二叉树不为空,缺右子树的单支二叉树(则任意节点没有右孩子)。
三、中序与后序
- 序列相同
- 空树或者缺右子树的单支二叉树。
- 序列相反
- 任意节点没有左孩子节点。
任何一颗二叉树的叶结点在前序、中序、后序遍历序列中的相对次序是不发生改变的。
1. 双亲表示法(顺序存储)
采用一组连续空间来存储每个结点,同时在每个结点中增设一个伪指针,指示其双亲结点在数组中的位置。(根结点下标为0,其伪指针域为-1)
- #define MAX_TREE_SIZE 100 //树中最多结点数
- typedef struct{ //树的结点定义
- ElemType data; //数据元素(存放结点本身信息。)
- int parent; //双亲位置域(指示本结点的双亲结点在数组中的位置。)
- }PTNode;
-
- typedef struct{ //树的类型定义
- PTNode nodes[MAX_TREE_SIZE]; //双亲表示
- int n; //结点数
- }PTree;
Traits:
2. 孩子表示法
将每个结点的孩子结点视为一个线性表,且以单链表作为存储结构,则n个结点就有n个孩子链表(叶结点的孩子链表为空表)。而n个头结点又组成一个线性表,用顺序表(含n个元素的结构数组)存储。
- struct CTNode{
- int child; //孩子结点在数组中的位置
- struct CTNode *next; // 下一个孩子
- };
-
- typedef struct{
- ElemType data;
- struct CTNode *firstChild; // 第一个孩子
- }CTBox;
-
- typedef struct{
- CTBox nodes[MAX_TREE_SIZE];
- int n, r; // 结点数和根的位置
- }CTree;
与双亲表示法相反,孩子表示法寻找孩子的操作非常方便,而寻找双亲的操作需要遍历n个结点中孩子链表指针域所指向的n个孩子链表。
3. 孩子兄弟表示法(二叉树表示法)
即以二叉链表作为树的存储结构。
- typedef struct CSNode{
- ElemType data; //数据域
- struct CSNode *firstchild, *nextsibling; //第一个孩子和右兄弟指针
- }CSNode. *CSTree;
1. 树转换为二叉树
- 加线:在兄弟之间加一连线
- 抹线:对每个结点去除其与孩子之间的关系(第一孩子除外)
- 旋转:以树的根结点为轴心,顺时针转45度
口诀就是:兄弟相连留长子。
注:树的每个分支结点的所有子结点中的最右子结点无右孩子,根节点转换后也没有右孩子,因此对应二叉树中无右孩子的结点个数=分支结点数+1
2. 森林转换为二叉树
- 把每棵树转换为二叉树。
- 第一棵二叉树不动,从第二棵二叉树开始,依次把后一颗二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。当所有的二叉树连接起来后,就得到了森林转换来的二叉树
3. 二叉树转为树
- 若某结点是其双亲的左孩子,则把该结点的右孩子、右孩子的右孩子等都与该结点的双亲结点用连线连起来
- 删除原二叉树中所有双亲结点与右孩子结点之间的连线
- 整理由前两步得到的树,即以该结点为轴心,逆时针转动45°。
4. 二叉树转换为森林
- 从根结点开始,若右孩子存在,则把右孩子结点的连线删除,在查看分离后的二叉树,若右孩子存在,则连线删除……,直到所有的右孩子连线都删除为止,得到分离后的二叉树。
- 在将每棵分离后的二叉树转换为树即可。
注:判断一棵二叉树能够转换成一棵树还是森林,标准很简答,那就是只要这课二叉树的根结点有没有右孩子,有就是森林,没有就是一棵树。
1. 树的遍历
2. 森林的遍历
注:有可能会考察定长编码和哈夫曼编码的比较。
- // 并查集的结构定义
- #define SIZE 100
- int UFset[SIZE]; // 集合元素数组(双亲指针数组)
-
- // 初始化(s[] 即为并查集)
- void Initial(int s[]){
- for(int i = 0; i < SIZE; i++){ //每个自成单元素集合
- s[i] = -1;
- }
- }
-
- // Find操作:在s[] 中查找并返回包含元素 x 的树的根
- // 最坏时间复杂度:O(n) (即结点排列形似成竖直状的单链表)
- int Find(int s[], int x){
- while(s[x] >= 0) x = s[x]; // 循环寻找 x 的根
- return x; // 根的s[] < 0
- }
-
- // Union操作:求两个不相交子集合的并集
- // 时间复杂度:O(1)
- void Union(int s[], int Root1, int Root2){
- if(Root1 == Root2) return; // 要求 Root1 和 Root2 是不同的,且表示子集合的名字
- s[Root2] = Root1; // 将 Root2 连接到另一根 Root1 下面
- }
- // Find操作的优化:
- // 压缩路径(先找到根结点,再将查找路径上所有结点都挂到根结点下)
- int Find(int s[], int x){
- int root = x;
- while(s[root] >= 0) root = s[root]; // 循环找到根
- while(x != root){ // 压缩路径
- int t = x; // t 指向 x 的父结点
- s[x] = root; // x 直接挂到根结点下
- x = t; // 继续使 x 的父结点也挂到根结点下
- }
- return root;
- }
-
- 通过Find操作的“压缩路径”优化后,可使集合树的深度不超过O(a(n))(a(n)是一个增长及其缓慢的函数,对于其常见的正整数n,通常a(n)<=4)
-
-
- // Union操作的优化:小树并入大树
- void Union(int s[], int Root1, int Root2){
- if(Root1 == Root2) return;
- if(Root2 > Root1){ // Root2 结点数更少
- s[Root1] += s[Root2]; //累加结点总数
- s[Root2] = Root1; // 小树合并到大树
- }
- else{ // Root1 结点数更少
- s[Root2] += s[Root1]; //累加结点总数
- s[Root1] = Root2; // 小树合并到大树
- }
- }
-
- 采用这种方法构造得成的集合树,其深度不超过[log2n]+1([]向下取整)
在无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的。
若图G中任意两个顶点都是连通的,则称图G为连通图,否则称为非连通图。
无向图中的极大连通子图称为连通分量。若一个图有n个顶点,并且边数小于n−1,则此图必是非连通图。(也有极小连通子图:既要保持图连通又要使得边数最少的子图)
(左边两张是无向图,右边三张都是左边的连通分量)
强连通图、强连通分量(有向图):
在有向图中,若从顶点v到顶点w和从顶点w到项点v之间都有路径,则称这两个顶点是强连通的。若图中任何一对顶点都是强连通的,则称此图为强连通图。
有向图中的极大强连通子图称为有向图的强连通分量。
(右边是左边的强连通分量)
生成树、生成森林:
连通图的生成树是包含图中全部顶点的一个极小连通子图。若图中顶点数为n,则它的生成树含有n−1条边。对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成一个回路。
在非连通图中,连通分量的生成树构成了非连通图的生成森林。
顶点的度,入度和出度:图中每个顶点的度定义为以该项点为一个端点的边的数目。
无向图的全部顶点的度的和等于边数的2倍。
对于有向图,顶点v的度分为入度和出度,入度是以顶点v为终点的有向边的数目,而出度是以顶点v为起点的有向边的数目。
有向图的全部顶点的入度之和与出度之和相等,并且等于边数。这是因为每条有向边都有一个起点和终点。
边的权和网:在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值。这种边上带有权值的图称为带权图,也称网。
稠密图、稀疏图:边数很少的图称为稀疏图,反之称为稠密图。稀疏和稠密本身是模糊的概念,稀疏图和稠密图常常是相对而言的。
路径、路径长度和回路:
某个顶点到另一个顶点之间的顶点序列被称为路径。
路径上边的数目称为路径长度。
第一个顶点和最后一个顶点相同的路径称为回路或环。
简单路径、简单回路:
在路径序列中,顶点不重复出现的路径称为简单路径。
除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。
距离:从顶点u出发到顶点v的最短路径若存在,则此路径的长度称为从u到v的距离。若从u到v根本不存在路径,则记该距离为无穷(∞)。
有向树:一个顶点的入度为0、其余顶点的入度均为1的有向图,称为有向树。
是指用一个一维数组存储图中顶点的信息,用一个二维数组存储图中边的信息(即各顶点之间的邻接关系)
- #define MaxVertexNum 100 //项目数目的最大值
- typedef char VertexType; //顶点对应的数据类型
- typedef int EdgeType; //边对应的数据类型
- typedef struct{
- VertexTypr Vex[MaxVertexNum]; //顶点集
- EdgeType Edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表
- int vexnum,arcnum; //图的当前顶点数和边数
- }MGraph;
Traits:
- 无向图的邻接矩阵一定是一个对称矩阵(且唯一)。因此,在实际存储邻接矩阵时只需存储上(或下)三角矩阵的元素。
- 对于无向图,邻接矩阵的第i行(或第i列)非零元素(或非无穷元素)的个数正好是顶点i的度。
- 对于有向图,邻接矩阵的第 i 行非零元素(或非无穷元素)的个数正好是顶点 i 的出度;第 i 列非零元素(或非无穷元素)的个数正好是顶点 i 的入度。
- 用邻接矩阵存储图,很容易确定图中任意两个顶点之间是否有边相连。但是,要确定图中有多少边,则必须按行、按列对每个元素进行检测,所花费的时间代价很大
- 稠密图适合使用邻接矩阵的存储表示
- 设图G的邻接矩阵为A, An 的元素An[i][j]等于由顶点i到顶点j的长度为n的路径的数目。
- #define MaxVertexNum 100 //图中顶点数目的最大值
- typedef struct ArcNode{ //边表结点
- int adjvex; //该弧所指向的顶点的位置
- struct ArcNode *next; //指向下一条弧的指针
- //InfoType info; //网的边权值
- }ArcNode;
- typedef struct VNode{ //顶点表结点
- VertexType data; //顶点信息
- ArcNode *first; //指向第一条依附该顶点的弧的指针
- }VNode,AdjList[MaxVertexNum];
- typedef struct{
- AdjList vertices; //邻接表
- int vexnum,arcnum; //图的顶点数和弧数
- }ALGraph; //ALGraph是以邻接表存储的图类型
Traits:
- 若G为无向图,则所需要的存储空间为O(|V|+2|E|);
若G为有向图,则所需要的存储空间为O(|V|+|E|);
前者的倍数2是因为无向图中,每条边在邻接表中出现两次- 对于稀疏图,采用邻接表表示法将极大地洁身存储空间
- 在邻接表中,给定一顶点,能很容易地找出它的所有邻边,因为只需要读取它的邻接表。
- 在有向图的邻接表示中,求一个给定顶点的出度只需要计算其邻接表中的节点个数;但是求其顶点的入度则需要遍历所有的邻接表。(可用逆邻接表,也能直接计算出入度)
- 图的邻接表表示并不唯一,因为在每个顶点对应的单链表中,各边结点的链接次序可以是任意的,它取决于建立邻接表的算法和边的输入次序。
注:邻接表:反映的是顶点出度的情况。
逆邻接表:反映的是顶点的入度情况。
有向图的一种链式存储方式,表中对应于有向图的每条弧有一个结点,对应于每个顶点也有一个结点。
注:其实就是邻接表和逆邻接表的结合。
画法:先画邻接表,再补全对应结点的域(增加弧结点的域),最后“自己指向自己”。
无向图的一种链式存储结构。
(边)
(顶点)
注:编组连
从图的某个顶点出发,依次访问图中所有的顶点,每个顶点被访问一次且仅访问一次。
防止多次访问某一个顶点的思路:设置辅助数组visited[n],用来标记每个被访问的顶点,
初始化状态为visited[n]=0;如果顶点被访问到,则修改辅助数组的值 :visited[i]=1
广度优先搜索(Breadth First Search,BFS)类似于树的按层次遍历的过程。广度优先搜索遍历的过程如下:
Key:
- //BFS伪代码
- bool visited[Max_vertex_num]; //访问标记数组
- void BFSTraverse(Graph G) {
- for (int i = 0; i < G.vexnum; ++i)
- visited[i] = false; //访问数组初始化
- InitQueue(Q); //初始化辅助队列Q
- for (int i = 0; i < G.vexnum; ++i){
- if(!visited[i])
- BFS(G, i); //对每个连通分量调用一次BFS
- }
- }
- //BFS算法求解单源最短路径问题
-
- const int Int_max = 0x7fffffff;
- void BFS_mindist(Graph G, int u) {
- for (int i = 0; i < G.vexnum; ++i)
- d[i] = Int_max; // 初始化初始路径长度
- visited[u] = true;
- d[u] = 0;
- EnQueue(Q, u);
- while(!isEmpty(Q)) {
- DeQueue(Q, u); // 队头出列
- for (w = FirstNeighbor(G, u); w >= 0; w = NextNeighbor(G, u, w))
- {
- if(!visited[w]) { // 访问尚未访问的邻接顶点
- visited[w] = true;
- d[w] = d[u] + 1; // 路径长度+1
- EnQueue(Q, w); // 顶点入队
- } // if
- }// for
- }// while
- }
深度优先搜索遍历连通图是一个递归的过程。为了在遍历过程中便于区分顶点是否已被访问,需附设访问标志数组visited[n],其初值为“false”,一旦某个顶点被访问,则其相应的分量置为“true”。
深度优先搜索遍历连通图的算法步骤为:
- bool visited[MAX_VERTEX_NUM];
- void DFSTraverse(MGraph G){
- int v;
- for(v=0; v<G.vexnum; ++v){
- visited[v] = FALSE; //初始化已访问标记数据
- }
- for(v=0; v<G.vexnum; ++v){ //从v=0开始遍历
- if(!visited[v]){
- DFS(G, v);
- }
- }
- }
若是非连通图,上述遍历过程执行之后,图中一定还有顶点未被访问。因此,需要从图中另选一个未被访问的顶点作为起始点,重复上述深度优先搜索过程,直到图中所有顶点均被访问过为止。这样,要实现对非连通图的遍历,就需要循环调用上述算法。
基本概念:
Prim算法(与点有关):
Kruskal算法(与边有关):
用额外两个数组分别存储该顶点的前驱和距离源顶点的距离
- #define MaxVertexNum 100
-
- void BFS(Graph G, int v){
- Queue Q;
- InitQueue(Q);
- EnQueue(v);
- int q, p;
- //dist数组用来存放当前顶点到源顶点的距离,pre存放当前顶点的前驱顶点
- int dist[MaxVertexNum] = { 0 }, pre[MaxVertexNum] = { 0 };
- bool visited[MaxVertexNum] = { false };
- //源顶点的前驱置为-1
- pre[v] = -1;
- //源顶点标记已访问
- visited[v] = true;
- //cur标记当前距离顶点的距离
- int cur = 0;
- while(!IsEmpty(Q)){
- DeQueue(Q, p);
- cur++;
- for (q = FirstNeighbor(G, p); q >= 0; q = NextNeighbor(G, p, q)){
- if (!visited[q]) {
- //将q设置为已标记
- visited[q] = true;
- //入队q
- EnQueue(Q, p);
- //更新q距离源顶点的距离
- dist[q] = cur;
- //更新q的前驱
- pre[q] = p;
- }//if
- }//for
- }//while
- }
注:BFS其实就是把无权图当做全部边权为1的带权图来进行处理的。(同样这就是它的局限性)
注:Dijkstra算法并不适用于图中边权为负值的情况。
Dijkstra算法是贪心算法,因此得到的未必是最优解。
这其实就是一个递归(贪心))的算法了,也就是把一个大问题先拆分成几个小问题。
需要新建两个二维数组:
Core code
- for(k=1;k<=n;k++)
- for(i=1;i<=n;i++)
- for(j=1;j<=n;j++) {
- if(d[i][k]+d[k][j]<d[i][j]) {
- d[i][j]=d[i][k]+d[k][j];
- path[i][j]=path[i][k];
- }
- }
注:带负权回路的图很有可能没有最小生成树。
DAG(Directed Acyclic Graph):有向无环图(有向图中不存在回路)
解题方法(咸鱼学长版):
- bool TopologicalSort(Graph G)
- {
- InitStack(G);
- for(int i=0; i<G.vexnum; ++i)
- if(indegree[i] == 0)
- Push(S,i); // 将所有入度为0的顶点入队
-
- int count = 0; // 计数,记录当前已经输出的顶点数
- while(!IsEmpty())
- {
- Pop(S,i); // 从队列中取出一个顶点
- print[count++]=i; // 输出该顶点
- // 将所有i指向的顶点的入度减1,并将入度减为0的顶点入栈
- for(p=G.vertices[i].firstarc;p;p->nextarc){
- v=p->adjvex;
- if(!(--indegree[v))
- Push(S,v); // 若入度为0,则入栈
- }
-
- if(count<G.vexnum)
- return false; // 没有输出全部顶点,有向图中有回路
- else
- return true; // 拓扑排序成功
- }
- //逆拓扑排序
- #define MaxVertexNum 100
-
- bool visited[MaxVertNum] = {false};
-
- void DPS_Init(Graph G){
- for (int i = 0; i < G.verNum; i++){
- if (!visited[i]) DFS(G, i);
- }
- }
-
- void DFS(Graph G, int v){
- visited[v] = true;
- int w;
- for (w = FirstNeighbor(G, v); w >= 0; w = NextNeighbor(G, v, w){
- if (!visited[w]) DFS(G, w);
- }
- cout << v << endl;
- }
AOE网:在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销。
求解方法:
特性:
采用不同存储结构时,各种图算法的时间复杂度:
又称为线性查找,它对顺序表和链表都是适用的。
基本思想:其实就是挨个找(从头到jio or 反着来都可以)。
代码实现:
- typedef struct{ //查找表的数据结构(顺序表)
- ElemType *elem; //动态数组基址
- int TableLen; //表的长度
- }SSTable;
-
- //顺序查找
- int Search_Seq(SSTable ST,ElemType key){
- int i;
- for(i=0;i<ST.TableLen && ST.elem[i]!=key;++i);
- // 查找成功返回数组下标,否则返回-1
- return i=ST.TableLen? -1 : i;
- }
-
-
- //哨兵版
- typedef struct{ //查找表的数据结构(顺序表)
- ElemType *elem; //动态数组基址
- int TableLen; //表的长度
- }SSTable;
-
- //顺序查找
- int Search_Seq(SSTable ST,ElemType key){
- ST.elem[0]=key;
- int i;
- for(i=ST.TableLen;ST.elem[i]!=key;--i)
- // 查找成功返回数组下标,否则返回0
- return i;
- }
注:
- 一个成功结点的查找长度 = 自身所在层数
- 一个失败结点的查找长度 = 其父节点所在
- 层数默认情况下,各种失败情况或成功情况都等概率发生
又称二分查找,它仅适用于有序的顺序表。
基础思想:
代码实现:
- typedef struct{
- ElemType *elem;
- int TableLen;
- }SSTable;
-
- // 折半查找
- int Binary_Search(SSTable L,ElemType key){
- int low=0,high=L.TableLen,mid;
- while(low<=high){
- mid=(low+high)/2;
- if(L.elem[mid]==key)
- return mid;
- else if(L.elem[mid]>key)
- high=mid-1; //从前半部分继续查找
- else
- low=mid+1; //从后半部分继续查找
- }
- return -1;
- }
折半查找判定树的构造(爱考): 取决于mid的取值是向上取整还是向下取整。
时间复杂度:O(log2n)
又称为“索引顺序查找”,块内⽆序、块间有序。
基础思想:
查找效率分析(ASL):ASL = 查索引表的平均查找长度+查分块的平均查找长度
(设n个记录均匀分为b块,每块s个记录)
注:
分块查找的优点:
分块查找的缺点:
构造一颗二叉排序树的目的并不是排序,而是提高查找、插入和删除关键字的速度,这种非线性结构也有利于插入和删除的实现。
定义:二叉排序树又称为二叉查找树,特点为:左子树节点值<根节点值<右子树节点值(因此对二叉排序树进行中序遍历,可得到一个递增的有序序列。)
查找:从根节点开始,沿某个分支逐层向下比较的过程:若BST非空,则先将给定值与根节点的关键字比较,若相同则成功;若小于根节点关键字则沿其左子树根节点向下进行类似操作;若大于根节点关键字则沿其右子树根节点向下进行类似操作。
- typedef struct BSTNode{
- int key;
- struct BSTNode *lchild, *rchild;
- }BSTNode, *BSTree;
-
- //查找(非递归)
- BSTNode *BST_Search(BiTree T, ElemType key)
- {
- while(T != NULL && key != T->data) //若树空或等于根结点数,则结束循环
- {
- if(key<T->data) //小于,则在左子树上查找
- T = T->lchild;
- eles //大于,则在右子树查找
- T = T->rchild;
- }
- return T; //失败会返回NULL
- }
-
- //查找(递归)
- BSTNode *BST_Search(BiTree T, ElemType key)
- {
- if(T == NULL)
- return NULL;
- if(key == T->key)
- return T;
- else if(key < T->key)
- return BST_Search(T->lchild, key);
- else if
- return BST_Search(T->rchild, key);
- }
BST是一种动态生成的树表,通过查找比较不断生成。
Step:
- //最坏时间复杂度O(h)
- int BST_Insert(BiTree &T, KeyType key)
- {
- if(T == NULL)
- {
- T = (BiTree)malloc(sizeof(BSTNode));
- T->data = k;
- T->lchild = T->rchild = NULL;
- return 1;
- }
- else if(key == T->data)
- return 0;
- eles if(key<T->data)
- return BST_Insert(T->lchild, key);
- else
- return BST_Insert(T->rchild, key);
- }
构造:从空树开始以此输入元素调用插入函数,逐渐构造出一棵树。
- void Creat_BST(BiTree &T, KeyType str[], int n)
- {
- T = NULL;
- int i = 0;
- while(i<n)
- {
- BST_Insert(T, str[i++]);
- }
- }
注: BST的生成与输入的顺序相关,相同数据按不同顺序输入得到的BST树也不同(不同的关键字序列可能得到相同的BST,也可能得到不同的BST)。
删除:
需要保证删除该节点后其左右子树接到原树,且仍保持BST特征。
注:情况3中的直接后继或前驱为中序遍历中的后继或前驱,可能为左子树最右下节点或右子树最左下节点(值与根节点最接近,不打破大小排序关系)
效率分析:(与树高相关)
定义:在插入和删除节点时保证任意结点的左右子树高度差的绝对值小于1
平衡因子:结点左子树的高 - 右子树的高(只能是 -1,0,1)
插入:为保证树的平衡,在插入节点时需检查插入路径上的节点是否因为此次的插入而出现不平衡,如果发生则找到距离插入节点最近的不平衡节点A(最小不平衡子树),对以A为根的树进行调整使其达到平衡。
Step:
- LL型(右单旋——父变爷,爷变右兄)
- RR型(左单旋——父变爷,爷变左兄)
- LR型(左右双旋——子变爷,父左子,爷右子)
- RL型(右左双旋——子变爷,父右子,爷左子)
注:看书上的可能会很绕啊,但其实就是想想怎么凑成“平衡”。
删除:这个就和操作很类似,同样需要进行平衡性的检查。
Step:
- 用BST方法对节点X进行删除
- 若出现不平衡现象则从节点X向上进行回溯(向上找最小不平衡子树),找到第一个不平衡节点A,B为节点A最近的孩子节点,C是B最近的孩子节点(找“个头”最高的儿孙节点),然后以A为根,利用与插入相同的四种调整方法进行判断调整(LL、RR、LR、RL)
- 如果还是不平衡就继续向上,重复第2步
注:删除和插入对结点的调整方式大差不差,buttt插入仅针对最小不平衡子树A,而删除可能要一直“回溯”。
查找:AVL中查找操作与BST相同,因此查找时与给定值比较的关键字个数不超过树高,因此,含n个节点的AVL树的最大深度为,因此AVL的平均查找长度为
红黑树不是AVL喔~是BST(二叉排序树)
注:根黑叶黑不红红,黑路数同左根右
性质:
- 从根节点到叶节点的最长路径不大于最短路径的 2 倍
- 有n个内部节点的红黑树的高度 h<=2log2(n+1)
- 红黑树的黑高(根节点到叶节点路径上黑色节点数量)至少为 h/2
- 红黑树查找操作时间复杂度 = O(log2n)
- 若根节点黑高为h,则内部节点最少为 2^h-1
插入(困难点):新插入红黑树的结点初始着为红色
- 若新节点是根节点,染为黑色;
- 若插入后仍满足红黑树定义(仅需查看是否满足不红红,其他三条都不会被打破),若满足则插入结束,若不满足,根据其叔叔(父节点的兄弟节点)的颜色进行相应调整:
- 黑叔:旋转+染色(染色就是换成和之前不同的颜色)
- LL型:右单旋,父换爷+染色(指给父和原爷节点染色)
- RR型:左单旋,父换爷+染色
- LR型:左右双旋,儿换爷+染色(指给儿和原爷节点染色)
- RL型:右左双旋,儿换爷+染色
- 红叔:染色+变新——叔父爷染色,爷变成新节点
删除(基本不考):后面再补!
注:
所谓m阶B树是所有结点的平衡因子均等于0的m路平衡查找树。(绝对平衡的树)
⼀棵m阶B树或为空树,或为满⾜如下特性的m叉树:
- 树中每个结点⾄多有 m 棵⼦树,即⾄多含有 m-1 个关键字。
- 若根结点不是终端结点,则⾄少有两棵⼦树。
- 除根结点外的所有⾮叶结点⾄少有 ⌈m/2⌉ 棵⼦树,即⾄少含有 ⌈m/2⌉ -1 个关键字。(为了保证查找效率,每个结点的关键字不能太少)
- 所有⾮叶结点的结构如下:
- 其中,Ki(i = 1, 2,…, n)为结点的关键字,且满⾜K1 < K2 <…< Kn;Pi(i = 0,1,…, n)为指向⼦树根结点的指针,且指针Pi-1所指⼦树中所有结点的关键字均⼩于Ki,Pi所指⼦树中所有结点的关键字均⼤于 Ki, n(⌈m/2⌉-1≤n≤m-1)为结点中关键字的个数。
- 所有的叶结点都出现在同⼀层次上,并且不带信息(可以视为外部结点或类似于折半查找判定树的查找失败结点,实际上这些结点不存在,指向这些结点的指针为空)。
注:在408真题中将B树的叶结点定义为最底层的终端结点。(失败结点还要再下一层)
m阶B树的核⼼特性(学长总结):
- 根节点的⼦树数∈[2,m],关键字数∈[1,m−1]。
- 其他结点的⼦树数∈[⌈m/2⌉,m];关键字数∈[⌈m/2⌉-1,m−1]。
- 对任⼀结点,其所有⼦树⾼度都相同。
- 关键字的值:⼦树0 < 关键字1 < ⼦树1 < 关键字2 < ⼦树2 <…. (类⽐⼆叉查找树左<中<右)
- 含 n 个关键字的 m叉B树,最小高度、最大高度
基本操作:
B树的查找:B树的查找操作与二叉查找树类似。
B树常存储在磁盘上,因此前一个查找操作在磁盘上进行,后一个查找操作在内存中进行。在B树中查找到某个结点后,先在有序表中进行查找,若找到则查找成功,否则按照对应指针信息到所指的子树中去查找。查找到叶子结点(对应指针为空指针),则说明树中没有对应的关键字,查找失败。
B树的插入:
注:根结点的分裂会导致B树高度加1
B树的删除:
注:其实就是在过程中要一直保持核心特性(2 and 4)
B+树是应数据库所需而出现的一种B树的变形树。
一棵m阶的B+树需满足以下条件:
- 每个分支节点最多有m棵子树(孩子结点)。
- 非叶根结点至少有两颗子树,其他每个分支结点至少有⌈m/2⌉棵子树。
- 结点的子树个数与关键字个数相同。
- 所有叶子结点包含所有关键字及指向相应记录的指针,叶子结点中将关键字按大小排列,并且相邻叶子结点按大小顺序相互链接起来。(说明B+树支持顺序查找)
- 所有分支节点仅包含它的各个节点中关键字的最大值及指向其子节点的指针。
常见的几种散列函数:
散列查找执行步骤如下:
平均查找长度(ASL):
散列表查找成功的平均查找长度即找到表中已有表项的平均比较次数;
装填因子:一般计为 α,即一个表的装满程度。α= 散列表长度m/表中记录数n
排序:就是重新排列表中的元素,使表中的元素按关键字有序的过程。
算法思想:每次将一个待排序的记录按其关键字大小,插入到前面已经排好序的子序列中,直到全部记录插入完成。
实现代码:
- // 对A[]数组中共n个元素进行插入排序
- void InsertSort(int A[],int n){
- int i,j,temp;
- for(i=1; i<n; i++){
- if(A[i]<A[i-1]){ //如果A[i]关键字小于前驱
- temp=A[i];
- for(j=i-1; j>=0 && A[j]>temp; --j)
- A[j+1]=A[j]; //所有大于temp的元素都向后挪
- A[j+1]=temp;
- }
- }
- }
-
- //带哨兵
- // 对A[]数组中共n个元素进行插入排序
- void InsertSort(int A[], int n){
- int i,j;
- for(i=2; i<=n; i++){
- if(A[i]<A[i-1]){
- A[0]=A[i]; //复制为哨兵,A[0]不放元素
- for(j=i-1; A[0]<A[j]; --j)
- A[j+1]=A[j];
- A[j+1]=A[0];
- }
- }
- }
-
- //链表
- void InsertSort(LinkList &L){
- LNode *p=L->next, *pre;
- LNode *r=p->next;
- p->next=NULL;
- p=r;
- while(p!=NULL){
- r=p->next;
- pre=L;
- while(pre->next!=NULL && pre->next->data<p->data)
- pre=pre->next;
- p->next=pre->next;
- pre->next=p;
- p=r;
- }
- }
算法性能分析:
算法思路: 每次将一个待排序的记录按其关键字大小,使用折半查找找到前面子序列中应该插入的位置并插入,直到全部记录插入完成。
实现代码:
- //对A[]数组中共n个元素进行折半插入排序
- void InsertSort(int A[], int n){
- int i,j,low,high,mid;
- for(i=2; i<=n; i++){ //依次将A[2]~A[n]插入前面的已排序序列
- A[0]=A[i]; //将A[i]暂存到A[0]
- low=1; high=i-1;
- while(low<=high){ //折半查找
- mid=(low+high)/2; //取中间点
- if(A[mid]>A[0])
- high=mid-1;
- else
- low=mid+1;
- }
- for(j=i-1; j>high+1; --j)
- A[j+1]=A[j]; //统一后移元素,空出插入位置
- A[high+1]=A[0];
- }
- }
注:为了保证算法的稳定性,当查找到和插入元素关键字一样的元素时,应该在这个元素的右半部分继续查找以确认位置。即当 A[mid] == A[0] 时,应继续在mid所指位置右边寻找插入位置。
算法性能分析:
算法思路:先追求表中元素的部分有序,再逐渐逼近全局有序,以减小插入排序算法的时间复杂度。
先将待排序表分割成若干形如L[i,i+d,i+2d,...,i+kd]的“特殊”子表,对各个子表分别进行直接插入排序。缩小增量d,重复上述过程,直到 d=1 为止。
实现代码:
- // 对A[]数组共n个元素进行希尔排序
- void ShellSort(ElemType A[], int n){
- int d,i,j;
- for(d=n/2; d>=1; d=d/2){ //步长d递减(无统一规定)
- for(i=d+1; i<=n; ++i){
- if(A[i]<A[i-d]){
- A[0]=A[i]; //A[0]做暂存单元,不是哨兵
- for(j=i-d; j>0 && A[0]<A[j]; j-=d)
- A[j+d]=A[j]; //记录后移,查找移动的位置
- A[j+d]=A[0]; //移动
- }
- }
- }
- }
算法性能分析:
算法思路:从后往前(或从前往后)两两比较相邻元素的值,若为逆序(即A[i−1]>A[i]),则交换它们,直到序列比较完。如此重复最多 n-1 次冒泡就能将所有元素排好序。
注:为保证稳定性,关键字相同的元素不交换。
实现代码:
- // 交换a和b的值
- void swap(int &a, int &b){
- int temp=a;
- a=b;
- b=temp;
- }
-
- // 对A[]数组共n个元素进行冒泡排序
- void BubbleSort(int A[], int n){
- for(int i=0; i<n-1; i++){
- bool flag = false; //标识本趟冒泡是否发生交换
- for(int j=n-1; j>i; j--){
- if(A[j-1]>A[j]){
- swap(A[j-1],A[j]);
- flag=true;
- }
- }
- if(flag==false)
- return; //若本趟遍历没有发生交换,说明已经有序
- }
- }
算法性能分析:
顾名思义,快速排序是所有内部排序算法中性能最优的排序算法。
算法思路:(分治法)在待排序表 L[1...n] 中任选一个元素 pivot 作为枢轴(通常取首元素),通过一趟排序将待排序表分为独立的两部分 L[1...k−1] 和 L[k−1...n]。使得L[1...k−1] 中的所有元素小于 pivot,L[k−1...n] 中的所有元素大于等于 pivot,则 pivot 放在了其最终位置 L[k] 上。重复此过程直到每部分内只有一个元素或空为止。
注:
实现代码:
- // 用第一个元素将数组A[]划分为两个部分
- 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];
- while(low<high && A[low]<=pivot)
- ++low; //比枢轴大的元素移动到右端
- A[high] = A[low];
- }
- A[low] = pivot; //枢轴元素存放到最终元素
- return low; //放回存放枢轴的最终位置
- }
-
- // 对A[]数组的low到high进行快速排序
- void QuickSort(int A[], int low, int high){
- if(low<high){ //递归跳出的条件
- int pivotpos = Partition(A, low, high); //划分
- QuickSort(A, low, pivotpos - 1);
- QuickSort(A, pivotpos + 1, high);
- }
- }
算法性能分析:
选择排序:每一趟在待排元素中选取关键字最小(或最大)的元素加入有序子序列。
算法思路:每一趟在待排序元素中选取关键字最小的元素与待排序元素中的第一个元素交换位置。
实现代码:
- // 交换a和b的值
- void swap(int &a, int &b){
- int temp = a;
- a = b;
- b = temp;
- }
-
- // 对A[]数组共n个元素进行选择排序
- void SelectSort(int A[], int n){
- for(int i=0; i<n-1; i++){ //一共进行n-1趟,i指向待排序序列中第一个元素
- int min = i;
- for(int j=i+1; j<n; j++){ //在A[i...n-1]中选择最小的元素
- if(A[j]<A[min])
- min = j;
- }
- if(min!=i)
- swap(A[i], A[min]);
- }
- }
算法性能分析:
链表的实现代码:
- void selectSort(LinkList &L){
- LNode *h=L,*p,*q,*r,*s;
- L=NULL;
- while(h!=NULL){
- p=s=h; q=r=NULL;
- while(p!=NULL){
- if(p->data>s->data){
- s=p; r=q;
- }
- q=p; p=p->next;
- }
- if(s==h)
- h=h->next;
- else
- r->next=s->next;
- s->next=L; L=s;
- }
- }
堆的基本概念:
(大根堆)
算法思路:首先将存放在 L[1...n] 中的n个元素建成初始堆,由于堆本身的特点,堆顶元素就是最大值。将堆顶元素与堆底元素交换,这样待排序列的最大元素已经找到了排序后的位置。此时剩下的元素已不满足大根堆的性质,堆被破坏,将堆顶元素下坠使其继续保持大根堆的性质,如此重复直到堆中仅剩一个元素为止。
实现代码(包括堆的初始化):
- // 对初始序列建立大根堆
- void BuildMaxHeap(int A[], int len){
- for(int i=len/2; i>0; i--) //从后往前调整所有非终端结点
- HeadAdjust(A, i, len);
- }
-
- // 将以k为根的子树调整为大根堆
- void HeadAdjust(int A[], int k, int len){
- A[0] = A[k]; //A[0]暂存子树的根结点
- for(int i=2*k; i<=len; i*=2){ //沿k较大的子结点向下调整
- if(i<len && A[i]<A[i+1])
- i++;
- if(A[0] >= A[i]) //筛选结束
- break;
- else{
- A[k] = A[i]; //将A[i]调整至双亲结点上
- k=i; //修改k值,以便继续向下筛选
- }
- }
- A[k] = A[0] //被筛选结点的值放入最终位置
- }
-
- // 交换a和b的值
- void swap(int &a, int &b){
- int temp = a;
- a = b;
- b = temp;
- }
-
- // 对长为len的数组A[]进行堆排序
- void HeapSort(int A[], int len){
- BuildMaxHeap(A, len); //初始建立大根堆
- for(int i=len; i>1; i--){ //n-1趟的交换和建堆过程
- swap(A[i], A[1]);
- HeadAdjust(A,1,i-1);
- }
- }
算法性能分析:
注:
注:基于大根堆排序得到“递增序列”,基于小根堆的堆排序得到“递减序列”
归并:把两个或多个已经有序的序列合并成一个。
算法思想:把待排序表看作 n 个有序的长度为1的子表,然后两两合并,得到 ⌈n/2⌉ 个长度为2或1的有序表……如此重复直到合并成一个长度为n的有序表为止。
实现代码:
- // 辅助数组B
- int *B=(int *)malloc(n*sizeof(int));
-
- // A[low,...,mid],A[mid+1,...,high]各自有序,将这两个部分归并
- void Merge(int A[], int low, int mid, int high){
- int i,j,k;
- for(k=low; k<=high; k++)
- B[k]=A[k]; //将A中所有元素复制到B中
- for(i=low, j=mid+1, k=i; i<=mid && j<= high; k++){
- if(B[i]<=B[j]) //将较小值复制到A中
- A[k]=B[i++];
- else
- A[k]=B[j++];
- }
- while(i<=mid)
- A[k++]=B[i++];
- while(j<=high)
- A[k++]=B[j++];
- }
-
- // 递归操作
- void MergeSort(int A[], int low, int high){
- if(low<high){
- int mid = (low+high)/2; //从中间划分
- MergeSort(A, low, mid); //对左半部分归并排序
- MergeSort(A, mid+1, high); //对右半部分归并排序
- Merge(A,low,mid,high); //归并
- }
- }
算法性能分析:
算法思想:把整个关键字拆分为d位,按照各个关键字位权重递增的次序(比如:个、十、百),做d趟“分配”和“收集”,若当前处理关键字位可能取得r个值,则需要建立r个队列。
注:应该就只考手算。
算法性能分析:
基数排序擅长处理的问题:
注:比如身份证号分配和中文名排序,但别教条化如果要给10亿个人的身份证号排序的话,基数排序又会变得合适了。
算法思想:通过计数每个元素的出现次数来进行排序,适用于整数或有限范围内的非负整数排序。
算法性能分析:
注:简选、Shell、Quick、堆排4不稳
外部排序:对大文件进行排序时,因为文件中的记录很多、信息量庞大,无法将整个文件复制进内存中进行排序。因此需要将待排序的记录存储在外存上,排序时再把数据一部分一部分地调入内存进行排序,在排序过程中需要多次进行内存和外存之间的交换。
注:
k路平衡归并:
- 最多只能有k个段归并为一个;
- 每一趟归并中,若有m个归并段参与归并,则经过处理得到 ⌈m/k⌉(向上取整)个新的归并段。
注:k叉归并的最佳归并树一定是严格k叉树,即树中只有度为k、度为0的结点。
k叉归并和k叉归并平衡树不一样喔。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。