1. 考研数据结构所需的程序语言基础
1. 变量类型
1. 基本类型
-
数值类型:short\int\long\float\double
考研数据结构中常用的有两种:int(存储整数),float(存储小数)
-
字符类型:char(存储字符,如A,B,C)
用法:
int a;
int b=1;//赋值,初始化
int a,b,c,d=2;
//存取
a=1;
d=b;
2. 指针型
存放变量地址的变量类型
int *p1=&A;//定义了整型指针变量p1,并将其初始化为整型变量A的地址 &:取地址符
float *p2=&B;
char *p3=&C;
p1=&D; //把D的地址赋值给p1,也就是p1指向D
E=*p1;//取p1指向的值赋值给E
NULL:是指不指向任何地址的特殊标记,其值为0。初始化指针时常用
int *p4=NULL;
#include <iostream>
using namespace std;
int main()
{
int a;
int b = 10;
a = 11;
cout << "a=" << a << endl;
cout << "b=" << b << endl;
int *p = &a;
cout << "*p=" << *p << endl;
*p = *p + 1;
cout << "a:" << a << endl;
p = NULL;
cout << "p:" << p << endl;
return 0;
}
//结果
a=11
b=10
*p=11
a:12
p:00000000
3. 构造类型
-
数组:相同类型的变量排成一列所构成的变量集
int B[100];//长度为100,存储范围为0~99 int A[5]={1,2,3,4,5}; a=A[0];//a=1 A[3]=b;//A[3]与b存值相同
-
结构体:不同的类型变量组合在一起来解决问题
typedef struct { int a; float b; char c; ...... }结构体名; typedef struct 结构体名 { int a; float b; char c; struct 结构体名 *d; ...... }结构体名;
typedef struct { int a; float b; char c; ...... }S; S s; s.a=1; s.b=1.111; s.c='A'; R=s.a;
4. void
在考研数据结构中,主要用来定义没有返回值的函数
void F()
{
...
...
return;
...
}
2. 控制语句
1. 判断语句
if(条件)
{
...
...
}
if(条件)
{
...
}
else
{
...
}
if(条件)
{
...
}
else if(条件)
{
...
}
...
else //在这里的else块可以不出现
{
...
}
条件:可以是一个表达式
eg:a==b;
a+b
1+1
!a
可以是一个变量或字面值
eg: a
124
‘a’
2. 循环语句
for(int i=0;i<N;++i)
{
...
}
while(条件)
{
...
}
//break 和continue
//break退出循环,continue结束本次循环
3. 函数
返回值类型 函数名(参数定义列表)
{
...
}
//函数调用方法
函数名(参数列表);
int add(int a,int b)
{
return a+b;
}
//调用
result = add(1,2);
void F()
{
...
}
//调用
F();
int result =0;
void getResult(int r)
{
++r;
}
//调用
getResult(result);//result值不变
int result =0;
void getResult(int &r) //引用型
{
++r;
}
//调用
getResult(result);//result值改变
int *p=NULL;
void getResult(int *&P)
{
P=q;
}
//调用
getResult(p);
//整型变量
int result =0;
void getResult (int *r)
{
++(*r); //取值后自增
}
//调用
getResult(&result); //取地址,进行改变
//指针型变量
int *p=NULL;
void getResult(int **p)
{
*P=q;
}
//调用
getResult(&p);
6. 逻辑结构和存储结构
1. 逻辑结构
- 集合:没关系
- 线性表:一对一关系(线性结构)元素前驱和后继是一对一关系
- 树:一对多关系(分支结构)
- 图:多对多关系(存在环路)
2. 存储关系
- 顺序结构:连续的存储空间,适用于存储线性结构
- 链式结构:存储单元之间的关系通过link连接,存储单元不仅存储数据,还存储了指向另外一个单元的地址
typdef struct LNode
{
int data; //定义数据类型
struct LNode *next;
}LNode;
LNode *L;
L=(LNode*)malloc(sizeof(LNode)); //malloc用以分配合适的内存空间,并返回空间地址,LNode*强制转换
A->next=B;
B->next=C;
- 支持随机存取(Random Access):顺序结构
7. 算法分析基础
1. 时间复杂度
- 简化问题:将加、减、乘、除、比较和赋值视作耗时相同的运算
- 最好、最坏与平均情况:对于输入数据,所执行的基本次数最多的情况,为最坏情况,如果没有特殊说明,我们要求的都是最坏情况
1. 普通函数时间复杂度分析
-
一次循环运行时间是循环内语句的运行时间乘以循环次数
嵌套循环运行时间为最内层语句执行次数乘以总循环次数
并列的两个循环运行时间与执行次数数量级大的那个相同
2. 递归函数时间复杂度分析
2. 空间复杂度
2.1 基础知识
1. 线性表的逻辑结构
1. def:
线性表是具有相同特性数据元素的有限序列。
相同特性 | 有限 | 序列 |
---|---|---|
把同一类事物归类,方便批量处理 | 表中元素个数为n,n有限大,n可以为0 | 表中元素排成一列,体现在一对一的逻辑特性(每个元素有且仅有一个前驱和一个后继) |
线性表是具有n个数据元素的有序序列,数据元素由数据项组成
2. 存储结构
1 顺序存储结构
-
顺序存储结构的代码实现
int A[maxSize]; //maxSize取线性表能取到的最大长度 int length=0;
2 链式存储结构
- 链式存储结构的代码实现
typedef struct LNode
{
int data;
struct LNode *next;
}LNode;
LNode *L;
L=(LNode*)malloc(sizeof(LNode));
A->next=B;
B->next=C;
C->next=D;
D->next=NULL;
-
含有头结点的链表
头结点:头指针所指向的不含任何数据信息的结点
开始结点:链表中第一个存数据信息的结点
尾结点:链表中最后一个存数据信息的结点
判空条件:
Head->next==NULL; 为真,空链表
-
不含头结点的链表
判空条件:
Head==NULL; 为真,空链表
-
链式存储结构:双链表
typedef struct DNode
{
int data;
struct DNode *next;
struct DLode *prior; //添加了一个前驱指针prior
}DNode;
DNode *L;
L=(DNode*)malloc(sizeof(DNode));
A->next=B;
B->next=C;
C->next=D;
D->prior=C;
C->prior=B;
B->prior=A;
-
链式存储结构(循环单链表)
判空条件:
Head->next==Head; //为真,空链表
在单循环链表中设置尾指针而不是头指针的原因:
尾指针是指向终端节点的指针,用它来表示单循环链表可以使得查找链表的开始结点和终端节点都很方便。
设一个带头节点的单循环链表,其尾指针是rear,则开始结点和终端结点分贝为指针rear指向的后继节点的后继节点和指针rear所指的结点,即rear->next->next 和 rear,查找均为时间为O(1)。
若用头指针来表示该链表,查找开始结点为O(1),终端结点为O(n)
链式存储结构(循环双链表)
判空条件:
Head->next=Head; 或 Head->prior=Head; //为真,空链表
带头节点的循环链表里没有空指针
习题
#include<iostream>
using namespace std;
#include <math.h>
#define min 0.001
#define maxSize 100 //辅助记录数组长度
//参数列表:两个数组,以及传入的数组长度
int compare(float A[], int An, float B[], int Bn) //返回值为0,-1或者1
{
int i = 0; //从头到位扫描顺序表
while (i < An && i < Bn) //当i=An||Bn,表示有个表已经扫描完毕,停止扫描
{
if (fabs(A[i] - B[i] < min)) //不用==是因为float型数据不好判断,用取绝对值函数fabs来衡量数据是否相等
{
++i; //i后移一位
}
else
break;
}
if (i >= An && i >= Bn) //A=B
return 0;
//(i >= An && i < Bn)表示i扫描完了A表,却未扫描完B表,
//A[i] < B[i],表示A除去相等的部分的第一个元素小于B除去相等部分的第一个元素
else if ((i >= An && i < Bn) || A[i] < B[i])
return -1; //-1返回A小于B
else
return 1;
}
int main()
{
float A[maxSize] = { 1,2,3,4 };
float B[maxSize] = { 1,2,3,4,5,3,6 };
cout << compare(A, 4, B, 6) << endl;
return 0;
}
单链表 | 双链表 | 循环单链表 | 循环双链表 |
---|---|---|---|
有头结点的单链表:Head->nextNULL; 为真,空链表 无头结点的单链表:HeadNULL; 为真,空链表 | 与单链表相同 | Head->next==Head; //为真,空链表 | Head->next=Head; 或 Head->prior=Head; //为真,空链表 |
循环单链表和单链表所占内存空间相同(要求两个表结点个数相同)
2.2 考点分析
1. 两种存储结构的特性对比
-
在顺序表中插入或删除元素可能会导致移动大量元素的连带操作(插入或者删除操作发生在表尾位置例外),而链表不会,链表只需要修改几个指针。
-
为了尽可能的弥补上一条中单链表的不足,开发了双链表、循环链表、单链表和循环双链表等存储结构,这些存储结构可以在仅知道链表中任一个结点地址的情况下推知其余所有结点的地址,但任然不支持随机存取。
-
有时候还会给链表定义一个额外的指针,最常见的是表尾指针,它指向链表中最后一个指针,可以借助它来提高某些常用操作的执行效率。
-
线性表采用顺序存储结构,必须占用一片连续的存储单元,而采用链式存储结构不需要这样
-
从表整体来看,一般顺序表存储空间利用率低于链表,而从单个存储空间来看,顺序表存储空间利用率要高于链表。
顺序表 | 链式表 |
---|---|
占用一片连续的存储单元 | 不需要占用一片连续的存储空间 |
一般插入和删除元素繁琐 | 便于插入和删除元素 |
支持随机存取 | 不支持随机存取 |
从单个存储空间来看,顺序表存储空间利用率要高 | 从表整体来看,一般链表存储空间利用率高(不必申请一段连续的存储空间) |
存储密度大(不用分配存储空间用于指针存放) | 存储密度小 |
存储密度:结点数据本身所占的存储量和整个结点结构所占的存储量之比
2. 元素移动次数计算和静态链表
1. 顺序表插入元素
假设一个顺序表中有n个元素,则可供插入的位置有n+1个(假设最后还有个位置)
- 在任意位置插入元素的概率为:p=1/(n+1);
- 在 i 位置(i的取值:0~n)之前插入元素,需要移动(n-i)个元素;
- 插入元素平均要移动的元素个数为:n/2;
- 时间复杂度:O(n);
2. 顺序表删除元素
- 在任一位置删除元素的概率为:p=1/n;
- 在 i 位置(i的取值:0~n-1)删除元素,需要移动n-1-i 个元素
- 删除元素平均要移动的元素个数为:(n-1/2)
- 时间复杂度:O(n);
//删除第i到j个元素,包括i和j元素
//主要思想:用后j+1到最后一个元素去覆盖前j-i+1个元素
void delete(Slist &L,int i,intj)
{
int k,delta;
delta=j-i+1;
for(k=j+1;k<L.length;++k)
{
L.data[k-dalte]=L.data.[k];
}
L.length=L.length-delta;
}
3. 静态链表
存储空间被一次性分配,不便于利用零散的存储空间
静态链表中的指针是数组下标,指示的是链表中下一个元素在数组中的地址
静态链表结点类型定义
typedef struct
{
int a;
float b;
char c;
......
}结构体名;
typedef struct
{
int data;
int next;
}SLNode;
SLNode Slink[maxSize];
int p=Ad0; //定义一个指针,指针用数组下标表示;
SLink[p].data; //取p指针指向的结点值,类比p->data;
SLink[p].next; //取p后继结点指针,类比p->next;
//在p后插入结点q
SLink[q].next=SLink[p].next;
SLink[p].next=q;
//类比:q->next=p->next;
//p->next=q;
3. 线性表元素插入和删除
1. 单链表
单链表结点插入代码示例:(在p结点后插入s结点)
s->next=p->next;
p->next=s;
单链表结点删除代码示例:(在p结点后删除s结点)
p->next=s->next;
free(s);
插入特殊情况:
- 在含有头结点的线性表的表头位置插入(即在头结点之后插入元素)
s->next=head->next;
head->next=s;
- 在不含有头结点的线性表的表头位置插入(在第一个元素之前插入元素)
s->next=head->next;
head=s; //使得s结点成为新的开始结点
头结点:头指针所指向的不含任何数据信息的结点
开始结点:链表中第一个存数据信息的结点
尾结点:链表中最后一个存数据信息的结点
删除特殊情况:
- 在含有头结点的线性表的删除第一个结点(即删除头结点)
head = head->next;
free(p);
- 给链表设置头结点,可以使得在第一个数据结点之前插入一个新结点和删除第一个数据结点的操作同表中部结点的这些操作统一起来,方便写代码
- 带头结点的链表,其头指针不随操作而改变,可以减少错误
2. 双链表
双链表结点插入代码示例:(在p结点后插入s结点)
s->next=p->next;
s->prior=p; //s的前驱结点是p
p->next=s; //p的后继结点是s
s->next->prior=s; //s的后继结点的前驱结点是s
双链表结点删除代码示例:(在p结点后删除s结点)
s->prior->next=s->next; //s的前驱结点的后继结点设置为s的后继结点
s->next->prior=s->prior; //s的后继结点的前驱结点设置为s的前驱结点
free(s);
3. 顺序表
顺序表中插入元素注意事项
- 可插入下标位置p的取值范围是:0到length
- 当表长length等于数组长度maxSize的时候,不可以在插入元素
- 移动元素从后往前进行
//创建一个顺序表
int sqList[maxSize]={1,2,3,4...,n};
int length=n;
//为了防止插入操作引起length发生变化,int &length标记length为引用型
//函数参数按照下行规方法定义为数组,无需加&符号,即可在函数体内部直接改变所传入数组的元素值,可以理解为,只要函数参数定义为数组,就是定义的引用型数组
//int p 插入位置
//int e 插入元素
int insertElem(int sqList[],int &length,int p,int e)
{
if(p<0)||p>length||length=maxSize)
return 0; //插入数据失败并且返回0
for(int i=length-1;i>=p;--i) //从最后一位到从插入位置结束,向后移动一位
sqList[i+1]=sqList[i];
sqList[p]=e; //p位置放上插入元素e
++length; //更新表长
return 1;
}
顺序表中删除元素注意事项
- 可删除元素下标p的取值范围是:0到length-1;
- 当表长length等于0的时候,不可以再删除元素
- 想要清空表:length=0;
- 移动元素从前往后移
//int &e 用来存放被删除的那个结点的值
//int p;删除结点的位置
int deleteElem(int sqList[],int &length,int p,int &e)
{
if(p<0||p>length-1)
return 0; //传入数据不合法,返回0
e=sqList[p]; //保存删除的元素
for(int i=p;i<length-1;i++)
sqList[i]=sqList[i+1];
--length; 更新表长
return 1;
}
设计一个算法,从一给定的顺序表L中删除下标i到j(i<j,包括i,j)的所有元素
#include <iostream>
using namespace std;
//设计一个算法,从一给定的顺序表L中删除下标i到j(i<j,包括i,j)的所有元素
//int i, int j,删除元素的范围
void del(int arr[], int &length, int i, int j)
{
int k, delta; //delta要删除的元素的个数
delta = j - i + 1;
for (k = j + 1; k < length; ++k) //从删除的最后一个元素的后面开始扫描到表尾,将这些元素搬运到前面
arr[k - delta] = arr[k];
length -= delta;
}
int main()
{
int A[] = { 1,2,3,4,5,6,7,8,9,10 };
int length = 10;
del(A, length, 3, 5);
for (int i = 0; i < length; ++i)
cout << A[i] << " ";
cout << endl;
cout << length << endl;
system("pause");
return 0;
}
有一个非递减非空单链表,设计算法删除值域重复的结点
void del(LNode *L)
{
LNode *p = L->next, *q; //p始终指向我们要删除结点的前驱
while (p->next != NULL)
{
if (p->data == p->next->data) //p的数据域等于p的后继的数据域
{
q = p->next; //p的后继指针复制给指针q
p->next = q->next; //p->next=p->next->next
free(q); //释放q,也就是p的后继节点
}
else
p = p->next; //p后移一位处理后续结点
}
}
设计算法,将一个头节点为A的单链表,分解为两个单链表A和B,使得A链表中只含有原来链表中data域为奇数的结点,而B链表中只含有原来链表中data域为偶数的结点,且保持原来的相对顺序
//设计算法,将一个头节点为A的单链表,
//分解为两个单链表A和B,使得A链表中只含有原来链表中data域为奇数的结点,
//而B链表中只含有原来链表中data域为偶数的结点,且保持原来的相对顺序
void split(LNode *A, LNode *B)
{
LNode *p, *q, *r;
B = (LNode*)malloc(sizeof(LNode));
B->next = NULL;
r = B; //申请了一个头结点空间,并让r和B都指向它
p = A; //p指针指向A链表的头结点
while (p->next != NULL)
{
if (p->next->data % 2 == 0)
{
q = p->next;
p->next = q->next;
q->next = NULL;
//将q结点插入到B链表中
r->next = q;
r = q;
}
else
p = p->next;
}
}
4. 建表
1. 顺序表建表
代码示例
int A[maxSize];
int length; //描述顺序表的长度
//数组和长度定义为引用值,因为要改变
int createList(int A[], int &length)
{
cin >> length;
if (length > maxSize)
return 0; //作为建表失败的标记
for (int i = 0; i < length; ++i)
cin >> A[i];
return 1;
}
2. 链表建表
1. 头插法代码示例
//头插法建表
using namespace std;
void createLinkListH(LNode *&head)
{
head = (LNode*)malloc(sizeof(LNode));
head->next = NULL; //申请一个头结点空间
LNode *p = NULL; //p指针接受新插入的结点的指针
int n;
//有using namespace std,就不要 std::
cin >> n; //scanf("%d",&n); //从键盘输入结点个数
for (int i = 0; i < n; ++i)
{
p = (LNode*)malloc(sizeof(LNode));
p->next = NULL;
cin >> p->data; //scanf("%d",&(p->data));
//向头结点插入新的结点
p->next = head->next; //可省略
head->next = p;
}
}
2. 尾插法建表代码示例
//尾插法建表
//head 定义为引用指针型
using namespace std;
void createLinkListR(LNode *&head)
{
head = (LNode*)malloc(sizeof(LNode));
head->next = NULL; //申请一个头结点空间
LNode *p = NULL; //p指针接受新插入的结点的指针
LNode *r = head; //r指针始终指向尾部结点的指针
int n;
//有using namespace std,就不要 std::
cin >> n; //scanf("%d",&n); //从键盘输入结点个数
for (int i = 0; i < n; ++i)
{
p = (LNode*)malloc(sizeof(LNode));
p->next = NULL;
cin >> p->data; //scanf("%d",&(p->data));
//向尾结点插入新的结点
p->next = r->next; //可省略
r->next = p;
r = p; //对r指针的维护,让r结点始终指向当前的最新的尾部节点
}
}
键盘输入n个英文字母,输入格式为nc1,c2,…cn,其中n表示字母的个数,请编程以这些输入数据建立一个单链表,并要求将字母不重复的存入链表。
解:输入一个单词,扫描其在链表中是都出现,如果出现,就什么都不做,否则,根据这个单词构造结点插入到链表中
代码示例:
using namespace std;
//不需要返回值,所以函数类型为void
//函数参数是引用指针型
void createLinkNoSameElem(LNode *&head)
{
head = (LNode *)malloc(sizeof(LNode));
head->next = NULL; //建立一个头结点
LNode *p;
int n; //输入元素个数
char ch; //接收字符
cin >> n;
for (int i = 0; i < n; ++i)
{
cin >> ch;
p = head->next;
while (p != NULL)
{
if (p->data == ch)
break;
p = p->next;
}
if (p == NULL)
{
p = (LNode*)malloc(sizeof(LNode)); //申请一个结点空间
p->data = ch; //给p结点数据域赋值 ch
//将p结点插入到头结点之后
p->next = head->next;
head->next = p;
}
}
}
5.逆置
顺序表的逆置代码示例
//主要流程
for (int i = left, j = right; i < j; ++i, --j)
{
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
void reverse(SList &L)
{
int i,j;
int temp;
for(i=0,j=L.length-1;i<j;++i,--j)
{
temp=L.data[i];
L.data[i]=L.data[j];
L.data[j]=temp;
}
}
链表的逆置代码示例
//链表:逆置p->next 到q之间的结点
while (p->next != q) //直到p结点的后继结点为q结束循环,完成逆置
{
t = p->next; //将p的下一个结点设为t
p->next = t->next; 删除t结点但是不释放
//将t结点插入到q结点之后
t->next = q->next;
q->next = t;
}
eg:将一长度为n的数组的前端k(k<n)个元素逆置后移动到数组后端,要求原数组中数据不丢失
void reverse(int a[], int left, int right, int k)
{
int temp;
//i < j,为了限制k大于表长的一半,left + k>j,重复逆置的发生
for (int i = left, j = right; i < left + k && i < j; ++i, --j) // i < left + k i扫描前k个位置
{
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
eg:将一长度为n的数组的前端k(k<n)个元素保持原序移动到数组后端,要求原数组中数据不丢失
//将一长度为n的数组的前端k(k<n)个元素保持原序移动到数组后端,要求原数组中数据不丢失
void moveToEnd(int a[], int n, int k)
{
reverse(a, 0, k - 1, k); //将前k个元素逆置
reverse(a, 0, n - 1, k); //将全体元素进逆置,逆置的范围是前k个
}
eg:将R中的元素(X0,X1…Xn-1),经过移动过后变成:(Xp,Xp+1,…Xn-1,X0,X1…Xp-1),即循环左移p(0<p<n)个位置
//时间复杂度:O(n);
//空间复杂度:O(1),因为存储空间只有一个a[]
void reverse(int a[], int left, int right, int k)
{
int temp;
//i < j,为了限制k大于表长的一半,left + k>j,重复逆置的发生
for (int i = left, j = right; i < left + k && i < j; ++i, --j) // i < left + k i扫描前k个位置
{
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
void moveP(int a[], int n, int p)
{
reverse(a, 0, p - 1, p); //逆置前p个元素
reverse(a, p, n - 1, n - p); //逆置p后面的元素
reverse(a, 0, n - 1, n); //逆置整个表
}
- 时间复杂度注意:
- 一次循环运行时间是循环内语句的运行时间乘以循环次数
- 嵌套循环运行时间为最内层语句执行次数乘以总循环次数
- 并列的两个循环运行时间与执行次数数量级大的相同
- 原地算法:辅助空间相对于输入数据量而言是个常数的算法,即空间复杂度为:O(1)
6.取最值
1. 在线性表中取最值
//求最小值
int min = a[0];
int minIdx = 0; //标记永远是最大元素的那个下标
for (int i = 0; i < n; ++i)
{
if (min> a[i])
{
min = a[i];
minIdx = i;
}
}
//求最大值
int max = a[0];
int maxIdx = 0; //标记永远是最大元素的那个下标
for (int i = 0; i < n; ++i)
{
if (max < a[i])
{
max = a[i];
maxIdx = i;
}
}
2. 在链表中求最值
//在链表中求最大值
LNode *p, *q;
int max = head->next->data;
q = p = head->next;
while (p->next != NULL)
{
if (max < p->data)
{
max = p->data;
q = p; //用q来标记最大元素的下标
}
p = p->next; //指针后移一位
}
//在链表中求最小值
LNode *p, *q;
int min = head->next->data;
q = p = head->next;
while (p->next != NULL)
{
if (min > p->data)
{
min = p->data;
q = p; //用q来标记最小元素的下标
}
p = p->next; //指针后移一位
}
eg:一双链表非空,由head指针指出,结点构造为{llink,data,rlink},请设计一个将结点数据域data值最大的那个结点(最大值结点只有一个)移动到链表前面的 算法,要求不得重新申请新节点空间。
typedef struct DLNode
{
int data;
struct DLNode *llink;
struct DLNode *rlink;
}DLNode;
//不需要返回类型
void maxFirst(DLNode *head)
{
DLNde *p = head->rlink;
//用指针q标记最大元素
DLNode *q = p;
int max = p->data;
//找最值,对于双链表而言只走其中一条
while (p->next != NULL)
{
if (max < p->data) {
max = p->data;
q = p;
}
p = p->next;
}
//"删除"
DLNode *l = q->llink, *r = q->rlink;
l->rlink = r;
if (r != NULL) //判断q指针是否为最后一个指针,如果r == NULL,表明q是尾结点,就不用定义其右指针
r->llink = 1;
//插入
q->next = head;
q->rlink = head->rlink;
head->rlink = q;
q->rlink->rllink = q;
}
eg:假定采用带头结点的单链表保存单词,当两个单词有相同的后缀时,可共享相同的后缀存储空间,设str1和str2分别指向两个单词所在单链表的头结点,链表结点结构为{data,next},设计一个时间是尽可能高效的算法,找出str1 和str2 所指向的两个链表共同后缀的起始位置
//假定采用带头结点的单链表保存单词,当两个单词有相同的后缀时,可共享相同的后缀存储空间
//设str1和str2分别指向两个单词所在单链表的头结点,链表结点结构为{data,next}
//谁一个时间是尽可能高效的算法,找出str1 和str2 所指向的两个链表共同后缀的起始位置
//基本思想:
/* 1.分别求出str1和str2 所指的两个链表的长度m和n
2.令指针p,q分贝指向str1和str2 的头结点,若m>n,则使p指向链表中的第m-n+1个结点
若m<n,则使q指向链表中n-m+1的个结点;即使指针p和q所指的结点到表尾的长度相等
3.将指针p和q同步向后移动,并判断他们是否指向同一个结点,如若指向的是同一个结点,则该结点为所求的共同后缀的起始位置
*/
LNode *findFirstCommon(LNode *str1, LNode *str2)
{
int len1 = 0;
int len2 = 0;
LNode *p = str1->next, *q = str2->next;
//求第一个链表长
whlie(p != NULL)
{
len1++;
p = p->next;
}
//求第二个链表长
while (q != NULL)
{
len2++;
q = q->next;
}
//若len1 > len2,则使p指向链表中的第len1-len2+1个结点
//若len1 < len2,则使q指向链表中的第len2-len1+1个结点;
for (p = str1->next; len1 > len2; len1--)
p = p->next;
for (q = str2->next; len1 < len2; len2--)
q = q->next;
//直到找到p==q,即他们指向同一个结点,退出循环
while (p != NULL && p != q)
{
p = p->next;
q = q->next;
}
return p;
}
7. 划分
1. 以表头数据作为枢轴,以枢轴作为划分标准
//参数列表为数组和数组长度
void partition(int a[], int n)
{
int temp;
//i为第一个元素的下标,j为最后一个元素的下标
int i = 0;
int j = n - 1;
temp = a[i];
while (i < j)
{
//从最后向前进行扫描,找出第一个小于temp的值退出循环,此时j值更新为此值的下标值
while (i < j &&a[j] >= temp)
--j;
//执行完上面while语句以后,j值可能等于i,所以要添加if语句进行判断
if (i<j)
{
//将j放到a[0]位置,a[0]值不会被覆盖,因为已经提前放在temp中
a[i] = a[j];
//i下标位置更新
++i;
}
//从前向后进行扫描,找出第一个大于temp的值退出循环,此时i更新为此值的下标
while (i < j&& a[i] < temp)
++i;
//执行完上面while语句以后,i值可能等于j,所以要添加if语句进行判断
if (i<j)
{
a[j] = a[i];
--j;
}
}
a[i] = temp;
}
2. 任意指定值作为比较标准
temp=X
比较的标准:comp=Y
- 效果是把数组元素以Y为界限分成了前后两个部分,前半部分小于Y,后半部分大于Y
- i和j最终指向的值是X,而不是Y
//参数列表为数组和数组长度和给定的比较标准
void partition(int a[], int n,int comp)
{
int temp;
//i为第一个元素的下标,j为最后一个元素的下标
int i = 0;
int j = n - 1;
temp = a[i];
while (i < j)
{
//从最后向前进行扫描,找出第一个小于comp的值退出循环,此时j值更新为此值的下标值
while (i < j &&a[j] >= comp)
--j;
//执行完上面while语句以后,j值可能等于i,所以要添加if语句进行判断
if (i < j)
{
//将j放到a[0]位置,a[0]值不会被覆盖,因为已经提前放在temp中
a[i] = a[j];
//i下标位置更新
++i;
}
//从前向后进行扫描,找出第一个大于comp的值退出循环,此时i更新为此值的下标
while (i < j&& a[i] < comp)
++i;
//执行完上面while语句以后,i值可能等于j,所以要添加if语句进行判断
if (i < j)
{
a[j] = a[i];
--j;
}
}
a[i] = temp;
}
3. 给定数组中的第k个元素作为枢轴进行划分,以枢轴作为划分标准
void partition(int a[], int n ,int k)
{
int temp;
//i为第一个元素的下标,j为最后一个元素的下标
int i = 0;
int j = n - 1;
//交换第一个元素和第k个元素,即设置a[k]作为枢轴
temp = a[0];
a[0]=a[k];
a[k] = temp;
//与1执行过程相同
temp = a[i];
while (i < j)
{
//从最后向前进行扫描,找出第一个小于temp的值退出循环,此时j值更新为此值的下标值
while (i < j &&a[j] >= temp)
--j;
//执行完上面while语句以后,j值可能等于i,所以要添加if语句进行判断
if (i < j)
{
//将j放到a[0]位置,a[0]值不会被覆盖,因为已经提前放在temp中
a[i] = a[j];
//i下标位置更新
++i;
}
//从前向后进行扫描,找出第一个大于temp的值退出循环,此时i更新为此值的下标
while (i < j&& a[i] < temp)
++i;
//执行完上面while语句以后,i值可能等于j,所以要添加if语句进行判断
if (i < j)
{
a[j] = a[i];
--j;
}
}
a[i] = temp;
}
8. 归并
1. 顺序表的归并
//函数传入参数为两个待归并的数组以及他们的长度和归并完成的新数组
void mergearray(int a[], int m, int b[], int n, int c[])
{
int i = 0;
int j = 0;
int k = 0;
while (i < m&&j < n)
{
//如果a数组中元素小于b数组中的元素,将a数组中元素放到c数组中
//否则,将b数组中元素放到c数组中
if (a[i] < b[j])
{
c[k] = a[i];
k++;
i++;
}
else
c[k++] = b[j++];//后置递增:先进行表达式运算,后让变量加一
}
//下面循环只有一个会执行,假设a数组已经全部放在了c数组,那么此时i=m,结果将执行把j后续元素放入c中的操作
while (i < m)
c[k++] = a[i++];
while (j < n)
c[k++] = b[j++];
}
2. 链表的归并
//升序归并 尾插法
//函数传入参数为两个待归并的链表的头指针,因为新生成的链表的头指针会被赋,所以要设置成引用型,指向新生成的链表的头结点
void merge(LNode *A, LNode *B, LNode *&C)
{
//定义p,q指针指向第一个元素数据
LNode *p = A->next;
LNode *q = B->next;
//指针r指向尾部
LNode *r;
//取A作为头结点指针赋值给新链表头结点C指针
C = A;
C->next = NULL;
free(B);
//未归并前,头尾结点指向同一个位置
r = C;
//只要p和q均不是尾结点,循环继续
while (p != NULL && q != NULL)
{
if (p->next <= q->next)
{
//将p插入新的链表
r->next = p;
//p和r指针均后移一位,即r始终指向尾结点
p = p->next;
r = r->next;
}
else
{
r->next = q;
q = q->next;
r = r->next;
}
}
//把剩余元素直接插入到新链表
if (p != NULL)
r->next = p;
if (q != NULL)
r->next = q;
}
//降序归并 头插法
//函数传入参数为两个待归并的链表的头指针,因为新生成的链表的头指针会被赋,所以要设置成引用型,指向新生成的链表的头结点
void mergeR(LNode *A, LNode *B, LNode *&C)
{
//定义p,q指针指向第一个元素数据
LNode *p = A->next;
LNode *q = B->next;
//指针s指向当前选出的最小结点
LNode *s;
C = A;
C->next = NULL;
free(B);
//只要p和q均不是尾结点,循环继续
while (p != NULL && q != NULL)
{
if (p->next <= q->next)
{
s = p;
p = p->next;
//s指针插入链表的头部
s->next = C->next;
C->next = s;
}
else
{
s = q;
q = q->next;
s->next = C->next;
C->next = s;
}
}
//把剩余元素直接插入到新链表
//当p不是尾结点
while (p != NULL)
{
s = p;
p = p->next;
s->next = C->next;
C->next = s;
}
while (q != NULL)
{
s = q;
q = q->next;
s->next = C->next;
C->next = s;
}
}
9. 补充例题
//有一个线性表,采用带头结点的单链表来存储,设计一个算法将其逆置,要求不能建立新结点,能通过表中已知结点重新组合来完成
//传递的参数列表为头指针
void reverse(LNode *L)
{
LNode *p = L->next;
LNode *q;
L->next = NULL;
//遍历整个链表
while (p->next != NULL)
{
//q为p指向结点的后继节点
q = q->next;
//将p头插法插入L后,第一次插入时,p->next = L->next==NULL
p->next = L->next;
L->next = p;
//将p后移一位,进行更新
p = q;
}
}
3.1 基础知识
1. 栈
1. 逻辑结构
栈(stack)是一种只能在一端进行插入或者删除操作的线性表;
线性表 | 一端 |
---|---|
栈的逻辑结构属于线性表,只不过在操作上加了一些约束。线性表是具有n个数据元素的有序序列 | 可以插入或者删除元素的一端叫做栈顶,另一端叫做栈底。 |
先进后出
2. 存储结构
1. 顺序栈
//栈中元素是从0—top。top范围之外的元素任然在数组中,但不属于栈中元素
int stack[maxSize];
//top起到指针作用,但其实是数组下标
int top=-1;
//元素入栈,先把4放入top位置,top指针再后移一位
stack[++top]=4;
//元素出栈,先取出top元素,再将top指针前移一位
x==stack[top--];
栈空和栈满
栈空状态下不能进行出栈操作,栈满状态下不能进行入栈操作
//栈空
top==-1;
//栈满
top==maxSize-1;
2. 链栈
LNode *head=(LNode *)malloc(sizeof(LNode));
head->next=NULL;
LNode *top=NULL;
//元素A入栈
LNode *head=(LNode *)malloc(sizeof(LNode));
head->next=NULL;
//用top指针始终指向新插入的元素
top=((LNode *)malloc(sizeof(LNode)));
top->next=NULL;
top->data='A';
top->next=head->next;
head->next=top;
//元素B入栈
LNode *head=(LNode *)malloc(sizeof(LNode));
head->next=NULL;
//top始终指向栈顶元素结点(头结点的后继结点)
x=top->data;
head->next=top->next;
free(top);
top=head->next;
//为真,则栈空,只要有足够的内存,栈就不会满
head->next==NULL;
2. 队列
1. 逻辑结构
def:队列是一种插入元素只能在一端能进,删除元素只能在另一端进行的线性表。
线性表 | 一端 | 另一端 |
---|---|---|
队列的逻辑结构属于线性表,只不过是在操作上加上了一些约束。线性表是具有n个数据元素的有序序列 | 可以插入元素的一端叫做队尾(Rear) | 可以删除元素的一端叫做队头(Front) |
先进先出
2. 存储结构
1. 顺序队
int queue[maxSize];
int front =0,rear=0;
插入元素:
//入队
//rear先后移一位,再赋值x
//rear始终指向队尾元素
queue[++rear]=x;
删除元素:
//出队
//先front自增,再取自增后指向的元素赋值给x
//front并不指向队头元素,他指向的下一个元素才是队头元素
x=queue[++front];
假溢出
可修改出入队操作
int queue[maxSize];
int front =0,rear=0;
//入队
rear=(rear+1)%maxSize;
queue[rear]=x;
//出队
front=(front+1)%maxSize;
x=queue[front];
//队空
front ==rear;
//队满
front==(rear+1)%maxSize;
2. 链队
front指向队头指针,头指针始终指向第一个元素
//rear尾指针始终指向尾部最后一个结点
//队空
front->next==NULL;
typedef struct
{
LNode * front;
LNode * rear;
}queue;
3.2 考点分析
1. 输出序列
由出栈序列判断容量
-
入栈序列:a1a2a3a4a5…an
出栈序列:p1p2p3p4p5…pn
栈容量至少是?
-
入栈序列:1、2、…、n
出栈序列:p1p2p3p4p5…pn
-
若p1=n
-
若pi=n(i<i<n)
则:pi>pi+1>…>pn,即入栈是升序,一次性出栈是降序,pi出栈时,pi+1还未出栈
-
若i<j<k 时
-
若1、2、3则无3,1,2
如果最后入栈的最先出栈,那么出栈顺序一定是固定的逆序出栈
-
-
Catalan number:
Cn=(2n)!/ [(n+1)!n!]
n个数按照某种顺序入栈,并且可在任意时刻出栈,不同出栈序列的个数为Cn
2. 各种表达式之间的转换
- 中缀表达式: a+b
- 前缀表达式(波兰式):+ab
- 后缀表达式(逆波兰式):ab+
转换方法:
1. 中缀转
(a+b)*c+d-(e+g)*h
//转前缀表达式
((a+b)*c)+d-((e+g)*h)
(((a+b)*c)+d)-((e+g)*h)
((((a+b)*c)+d)-((e+g)*h))
-(+(*(+(ab)c)d)(*+(eg)h)) //去掉括号
-+*+abcd*+egh
//转后缀表达式
((((a+b)*c)+d)-((e+g)*h))
((((ab)+c)*d)+((eg)+h)*)-
ab+c*d+eg+h*-
2. 后缀转中缀
ab+c*d+eg+h*-
(a+b)c*d+eg+h*-
((a+b)*c)d+eg+h*-
(((a+b)*c)+d)eg+h*-
(((a+b)*c)+d)(e+g)h*-
(((a+b)*c)+d)((e+g)*h)-
((((a+b)*c)+d)-((e+g)*h))
((a+b)*c)+d-((e+g)*h)
3. 后缀转前缀
ab+c*d+eg+h*-
(+ab)c*d+eg+h*-
(*(+ab)c)d+eg+h*-
(+(*(+ab)c)d)eg+h*-
(+(*(+ab)c)d)(+eg)h*-
(+(*(+ab)c)d)(*(+eg)h)-
-((+(*(+ab)c)d)(*(+eg)h))
-+*+abcd*+egh
3. 用栈实现表达式的转换
1. 中缀转后缀
-
从左到右扫描表达式,遇到操作数直接写出来
-
遇到运算符就入栈,
-
在入栈之前,要把当前扫描的运算符与栈顶运算符做比较,如果当前扫描的运算符其运算优先级小于或等于栈顶运算符,就把栈顶运算符出,写入当前的结果表达式中
-
栈空的时候直接入栈
-
对于表达式中有括号的情况,左括号直接入栈,当栈顶元素是左括号时,此后扫描到的运算符都入栈,当扫描到右括号时,则执行一系列的出栈操作,把当前栈中从栈顶到左括号的元素全部出栈,并将其写入结果表达式中,括号不写,
-
当扫描完中缀表达式的所有元素后,若栈中还有元素,则全部出栈,并将其写入结果表达式中
void infixToPostFix(char infix[], char s2[], int &top2)
{
//辅助栈
char s1[maxSize];
int top1 = -1;
while (infix[i] != '\0')
{
//判断是否是字符表达式
if ('0' < infix[i] && infix[i] <= '9')
{
//直接入结果栈
s2[++top2] = infix[i];
++i;
}
//对左括号的判定,如果扫描的是左括号,入辅助栈s2
else if (infix[i] == '(')
{
s1[++top1] = '(';
++i;
}
//
else if (infix[i] == '+' || infix[i] == '_' || infix[i] == '*' || infix[i] == '/')
{
//如果辅助栈空 或者栈顶元素是左括号 或者此时辅助栈中的扫描的优先级大于栈顶元素的优先级
//入s1栈,且i增加一位
if (top1 == -1 || s1[top1] == '(' || getPriority(infix[i]) > getPriority(s1[top1]))
{
s1[++top1] = infix[i];
++i;
}
//否则,入结果栈
else
s2[++top2] = s1[top1--];
}
else if (infix[i] == ')')
{
while (s1[top1] != '(')
s2[++top2] = s1[top1--];
--top1;
++i;
}
}
//将辅助栈中所有元素出栈压入结果栈中
while (top1 != -1)
s2[++top2] = s2[top1--];
}
2. 中缀转前缀
- 从右到左扫描表达式,遇到操作数直接写出来
- 遇到运算符就入栈,
- 在入栈之前,要把当前扫描的运算符与栈顶运算符做比较,如果当前扫描的运算符其运算优先级小于栈顶运算符,就把栈顶运算符出,写入当前的结果表达式中
- 栈空的时候直接入栈
- 对于表达式中有括号的情况,右括号直接入栈,当栈顶元素是右括号时,此后扫描到的运算符都入栈,当扫描到左括号时,则执行一系列的出栈操作,把当前栈中从栈顶到左括号的元素全部出栈,并将其写入结果表达式中,括号不写
- 当扫描完中缀表达式的所有元素后,若栈中还有元素,则全部出栈,并将其写入结果表达式中
中缀转前缀 | 中缀转后缀 |
---|---|
当前读取运算符的优先级小于栈顶运算符优先级则出栈 | 当前读取运算符优先级小于或等于栈顶运算符优先级则出栈 |
void infixToPostFix(char infix[], char s2[], int &top2)
{
//辅助栈
char s1[maxSize];
int top1 = -1;
int i = len - 1;
while (i>=0)
{
//判断是否是字符表达式
if ('0' < infix[i] && infix[i] <= '9')
{
//直接入结果栈
s2[++top2] = infix[i];
--i;
}
//对左括号的判定,如果扫描的是左括号,入辅助栈s2
else if (infix[i] == ')')
{
s1[++top1] = ')';
--i;
}
//
else if (infix[i] == '+' || infix[i] == '_' || infix[i] == '*' || infix[i] == '/')
{
//如果辅助栈空 或者栈顶元素是左括号 或者此时辅助栈中的扫描的优先级大于等于栈顶元素的优先级入辅助栈
//入s1栈,且i增加一位
if (top1 == -1 || s1[top1] == ')' || getPriority(infix[i]) >= getPriority(s1[top1]))
{
s1[++top1] = infix[i];
--i;
}
//否则,入结果栈
else
s2[++top2] = s1[top1--];
}
else if (infix[i] == '(')
{
while (s1[top1] != ')')
s2[++top2] = s1[top1--];
--top1;
--i;
}
}
//将辅助栈中所有元素出栈压入结果栈中
while (top1 != -1)
s2[++top2] = s2[top1--];
}
3. 后缀转前缀
- 每扫描一个运算符,就把运算符所对应的两个表达式移到运算符的后面
4. 用栈实现表达式的计算
1. 用栈求中缀表达式值
- 需要两个栈,一个栈s1来暂存操作数,另外一个s2暂存运算符
- 从左到右扫描要求值的表达式,当扫描到操作数的时候入s1栈,当扫描到运算符的时候,准备入s2栈
- 如果s2栈为空或者s2栈顶元素为左括号,则运算符直接入s2栈。
- 如果当前扫描到的运算符其优先级大于栈顶运算符的优先级,则入s2栈
- 如果当前扫描到的运算符其优先级小于等于栈顶运算符的优先级,则出栈,每次出栈一个运算符,就从s1栈出栈两个操作数,出栈的两个操作数和s2出栈的运算符进行运算,第一次出栈的操作数在右,第二次出栈的操作数在左。并将计算结果入栈s1。
- 直到最新的栈顶运算符的优先级小于扫描的运算符的优先级,将此运算符入栈,
- 遇到右括号,s2从栈顶到左括号的所有运算符出栈,并对每一个出栈的运算符做一次计算,并将结果压入s1栈
- 当整个表达式扫描完毕,s2栈中所有运算符出栈,并进行计算,并将结算结果压入s1栈
- 最终s1栈的栈顶就是求值结果
int getPriority(char op)
{
//通过将加减设置为0.乘除设置为1来设置运算符优先级
if (op == '+' || op == '-')
return 0;
else
return 1;
}
int calSub(float opand1, char op, float opand2, float &result)
{
//进行运算符的具体操作
if (op == '+')
result = opand1 + opand2;
if (op == '-')
result = opand1 - opand2;
if (op == '*')
result = opand1 * opand2;
if (op == '/')
{
//fabs为取绝对值,MIN为宏,设置为非常小的正数,可以看作为0
if (fabs(opand2) < MIN)
{
return 0;
}
else
{
result = opand1 / opand2;
}
}
return 1;
}
//就从s1栈出栈两个操作数,出栈的两个操作数和s2出栈的运算符进行运算,第一次出栈的操作数在右,第二次出栈的操作数在左。并将计算结果入栈s1。
int calStackTopTwo(float s1[], int &top1, float s2[], int &top2)
{
float opand1, opand2, result;
char op;
int flag;
//先出栈的为第二个操作数,后出栈的为第一个操作数
opand2 = s1[top1--];
opand1 = s1[top1--];
op = s2[top2--];
flag = calSub(opand1, op, opand2, result);
if (flag == 0)
{
puts("ERRPR");
}
s1[++top1] = result;
return flag;
}
float calInfix(char exp[])
{
//建立两个栈
float s1[maxSize];
int top1 = -1;
float s2[maxSize];
int top2 = -1;
//扫描表达式,当扫描到‘\0’表示扫描结束
while (exp[i] != '\0')
{
if ('0' <= exp[i] && exp[i] <= '9')
{
//前置递增:先让变量加一 然后再进行表达式的运算
//进行字符型转换成数值型
s1[++top1] = exp[i] - '0';
++i;
}
else if (exp[i]=='(')
{
s2[++top2] = '(';
++i;
}
else if (exp[i] == '+' || exp[i] == '-' || exp[i] == '*' || exp[i] == '/')
{
if (top2 == -1 || s2[top2] == '(' || getPriority(exp[i]) > getPriority(s2[top2]))
{
s2[++top2] = exp[i];
++i;
}
else
{
int flag = calStackTopTwo(s1, top1, s2, top2);
if (flag == 0)
return 0;
}
}
else if (exp[i] == ')')
{
while (s2[top2 != '('])
{
int flag = calStackTopTwo(s1, top1, s2, top2);
if (flag == 0)
return 0;
}
--top2;
++i;
}
}
while (top2 != -1)
{
int flag = calStackTopTwo(s1, top1, s2, top2);
if (flag == 0)
return 0;
}
return s1[top1];
}
2. 用栈求后缀表达式值
- 从左至右扫描表达式
- 当遇到操作数时,入栈,当遇到运算符时,从栈顶弹出两个操作数,第一次出栈的操作数在右边,第二此出栈的操作数在左边,将结果压栈。直至表达式被扫描完成
int calSub(float opand1, char op, float opand2, float &result)
{
if (op == '+')
result = opand1 + opand2;
if (op == '-')
result = opand1 - opand2;
if (op == '*')
result = opand1 * opand2;
if (op == '/')
{
//fabs为取绝对值,MIN为宏,设置为非常小的正数,可以看作为0
if (fabs(opand2) < MIN)
{
return 0;
}
else
{
result = opand1 / opand2;
}
}
return 1;
}
float calPostFix(char exp[])
{
float s[maxSize];
int top = -1;
int i = 0;
for(int i=0;exp[i] != '\0';++i)
{
if ('0' <= exp[i] && exp[i] <= '9')
{
s[++top] = exp[i] = '0';
}
else
{
float opnd1, opnd2, result;
char op;
int flag; //作为判断是否成功的标志
opnd2 = s[top--];
opnd1 = s[top--];
op = exp[i];
flag = calSub(opnd1, op, opnd2, result);
if (flag == 0)
{
puts("ERROR");
break;
}
s[++top] = result;
}
}
return s[top];
}
3. 用栈求前缀表达式值
- 从右至左扫描表达式
int calSub(float opand1, char op, float opand2, float &result)
{
if (op == '+')
result = opand1 + opand2;
if (op == '-')
result = opand1 - opand2;
if (op == '*')
result = opand1 * opand2;
if (op == '/')
{
//fabs为取绝对值,MIN为宏,设置为非常小的正数,可以看作为0
if (fabs(opand2) < MIN)
{
return 0;
}
else
{
result = opand1 / opand2;
}
}
return 1;
}
float calPreFix(char exp[],int len)
{
float s[maxSize];
int top = -1;
//从右往左扫描
for (int i = len - 1; i >= 0; --i)
{
if ('0' <= exp[i] && exp[i] <= '9')
{
s[++top] = exp[i] - '0';
}
else
{
float opnd1, opnd2, result;
char op;
int flag; //作为判断是否成功的标志
//先出栈的为第一个操作数,后出栈的为第二个操作数
opnd1 = s[top--];
opnd2 = s[top--];
op = exp[i];
flag = calSub(opnd1, op, opnd2, result);
if (flag == 0)
{
puts("ERROR");
return 0;
}
s[++top] = result;
}
}
return s[top];
}
5. 循环队列的配置问题
1. 正常配置
//入队
rear=(rear+1)%maxSize;
queue[rear]=x;
//出队
front=(front+1)%maxSize;
x=queue[front];
先移动rear指针,再入队元素
先移动front指针,再取元素
rear指针始终指向队尾元素,front指针指向队头元素的下一个元素
队中元素有多少个
//当rear>front时
rear-front
//当rear<front时
rear-front+maxSize
合并上述两种情况
(rear-front+maxSize)%maxSize
2. 非正常配置
先入队元素,再移动rear指针
先取元素,再移动front指针
front指向队头元素,rear指向队尾元素的下一个元素
队中元素有多少个
//当rear>front时
rear-front
//当rear<front时
rear-front+maxSize
合并上述两种情况
(rear-front+maxSize)%maxSize
//队满
front==(rear+2)%maxSize;
队中元素有多少个
//当rear>front时
rear-front+1
//当rear<front时
rear-front+1+maxSize
合并上述两种情况
(rear-front+1+maxSize)%maxSize
6. 双端队列
三种:毫无限制,输入限制,输出限制
eg:设有一个双端队列,元素进入该队列的顺序为1,2,3,4;试分别求出满足下列条件的输出序列
- 不可能通过输入受限的双端队列输出的序列是?
从分析
操作:
对内情况:
出队序列:
-
不可能通过输出受限的双端队列输出的序列是?
-
即不能由输入受限的双端队列得到,也不能由输出受限的双端队列得到的输出序列是?
栈形式下序列数:
Cn=(2n)!/ [(n+1)!n!]
C4=14
栈形式下不可能的序列数=4!-14=10
7. 栈的扩展
1. 共享栈
int stack[maxSize];
//top1最初指向第一个单元的前一个位置
//top2指向这片存储空间最后一个元素的后一个位置
int top1=-1,top2=maxSize;
//一般将其合并为一个二维数组
int top[2]={1,maxSize};
top[0]==-1 //为真,则s1空
top[2]==maxSize //为真,则s2空
stack[++top[0]]=x; //s1入栈
stack[--top[1]]=x; //s2入栈
top[0]+1=top[1]; //为真,则栈满
2. 用栈来模拟队列
用两个栈来模拟队列,两个栈大小相同
-
入队规则:
- 若s1未满,则元素直接入s1;
- 若s1满,s2空,则将s1元素全部出栈并入s2,腾出位置再入s1;
-
出队规则:
- 若s2不空,则直接从s2中直接出栈
- 若s2空,则将s1中元素全部出栈并入s2中,然后从s2中出栈
-
队满:s1满而s2不空,则不能继续入队,即为队满状态
-
队空:s1和s2均为空
8. 括号匹配
用栈来模拟情况
int isMatched(char left, char right)
{
if (left == '('&&right == ')')
return 1;
else if (left == '['&&right == ']')
return 1;
else if (left == '{'&&right == '}')
return 1;
else
return 0;
}
//用int型返回参数是否匹配
int isParenthesesBalanced(char exp[])
{
char s[maxSize];
int top = -1;
//前置递增:先让变量加一 然后再进行表达式的运算
for (int i = 0; exp[i] != '\0'; ++i)
{
//左括号直接入栈
if (exp[i] == '(' || exp[i] == '[' || exp[i] == '{')
s[++top] = exp[i];
//右括号进行判定
if (exp[i] == ')' || exp[i] == ']' || exp[i] == '}')
{
//如果此时栈空,表明不匹配
if (top == -1)
return 0;
//如果栈不空,出栈一个元素,用isMatched函数来判定两元素是否匹配
char left = s[--top];
if (isMatched(left, exp[i]) == 0)
return 0;
}
}
//执行完如果栈内还有元素,表明不匹配
if (top > -1)
return 0;
return 1;
}
解方程
//求解方程
int calF(int m)
{
//对于累乘,初值cum设置为1,对于累加,初值设置为0
int cum = 1;
int s[maxSize];
int top = -1;
while (m != 0)
{
s[++top] = m;
m = m / 3;
}
while (top != -1)
cum *= s[top--];
return cum;
}
4.1 基础知识
1. 串的逻辑结果
def:串是特殊的线性表
2. 串的存储结构
2.1 串的顺序存储结构
最常用的两种
//定长存储结构
typedef struct
{
char str[maxSize+1];
int length;
}Str;
//变长存储结构
typedef struct
{
char *ch;
int length;
}Str;
-
定长存储结构
- 串的长度固定
- maxSize为已经定义的常量,表示串的最大长度
- str数组长度定义为maxSize+1,是因为多出了一个’\0’作为结束字符,’\0’不作为串的本身内容,但占有串的一个存储空间
- 适合于长度不变的串
- 便于查找和删除
-
变长存储结构
Str S; S.length=L; //给一个串分配一定规模(L+1)存储空间,并用ch指针指向这片空间的首地址 S.ch=(char*)malloc((L+1)*sizeof(char)); //L+1是因为多出了一个'\0'作为结束字符 S.ch[length范围内的位置]=某字符变量; 某字符变量=S.ch[length范围内的位置]; free(S.ch); //free函数的参数是串的首地址,而malloc函数返回值为串的首地址
-
- 适合长度改变的串
- 基本采用变长存储结构
3. 赋值操作
typedef struct
{
char *ch;
int length;
}Str;
//str定义成引用型,是因为str会改变
//定义了一个字符型指针ch,指向我们定义的串的首地址
int strAssign(Str &str, char *ch)
{
//如果我们要赋值的串有值,即已经指向一片存储空间,则释放存储空间
if (str.ch) //如果串为空,则str.ch=NULL,为假,if不执行,如果串不空,则str.ch!=NULL,执行if语句块
free(str.ch); //释放存储空间
int len = 0;
char *c = ch; //c开始指向一片连续空间的首地址
while (*c)
{
++len;
++c;
}
if (len == 0)
{
str.ch = NULL;
str.length = 0;
return 1; //用1来标记赋值成功
}
else
{
str.ch = (char*)malloc(sizeof(char)*(len + 1));
if (str.ch == NULL) //分配空间失败,malloc会返回一个NULL值,此时赋值一定失败
return 0; //用0来标记赋值失败
else
{
//开始赋值
c = ch;
//i <= len是因为最后一个结束字符'\0'也要被赋值
for (int i = 0; i <= len; ++i, ++c)
str.ch[i] = *c;
str.length = len;
return 1; 用1来标记赋值成功
}
}
}
4. 取串长度
int strLength(Str str)
{
return str.length;
}
5. 串比较
设两串C1和C2中的待比较字符分别为a和b
- 如果a的ASCII码小于b的ASCII码,则返回C1小于C2标记(一个负数);
- 如果a的ASCII码大于b的ASCII码,则返回C1大于C2标记(一个正数);
- 如果a的ASCII码等于b的ASCII码,按之前的情况比较两串中的下一对字符;
- 经过上述步骤没有比较出C1和C2大小的情况,先结束的串为较小串,两串同时结束则返回两串相等标记(0)。
typedef struct
{
char *ch;
int length;
}Str;
int strCompare(Str s1, Str s2)
{
for (int i = 0; i < s1.length&&i < s2.length; ++i)
{
if (s1.ch[i] != s2.ch[i])
//实际上是用两个字符的ASCII码来做减法
//如果a的ASCII码小于b的ASCII码,则返回C1小于C2标记(一个负数)
//如果a的ASCII码大于b的ASCII码,则返回C1大于C2标记(一个正数)
return s1.ch[i] - s2.ch[i];
}
//经过上述步骤没有比较出C1和C2大小的情况,先结束的串为较小串,两串同时结束则返回两串相等标记(0)
return s1.length - s2.length;
}
6. 串连接
typedef struct
{
char *ch;
int length;
}Str;
int concat(Str &str, Str str1, Str str2)
{
如果我们要赋值的串有值,即已经指向一片存储空间,则释放存储空间
if (str.ch != NULL)
{
free(str.ch);
str.ch = NULL;
}
//多申请一个存储空间是因为要放置结束标记
str.ch = (char *)malloc(sizeof(char)*(str1.length + str2.length + 1));
//如果存储空间分配失败
if (!str.ch)
return 0;
int i = 0;
while (i < str1.length)
{
str.ch[i] = str1.ch[i];
++i;
}
int j = 0;
while (j <= str2.length)
{
str.ch[i + j] = str2.ch[j];
++j;
}
str.length = str1.length + str2.length;
return 1;
}
7. 取子串
typedef struct
{
char *ch;
int length;
}Str;
//在str中取一段从pos开始长度为len 的子串放置到substr中
int subString(Str &substr, Str str, int pos, int len)
{
if (pos < 0 || pos >= str.length || len<0 || len>str.length - pos)
return 0;
if (substr.ch)
{
free(substr.ch);
substr.ch = NULL;
}
if (len == 0)
{
substr.ch = NULL;
substr.length = 0;
return 1;
}
else
{
str.ch = (char *)malloc(sizeof(char)*(len+ 1));
int i = pos;
int j = 0;
while (i < pos + len)
{
substr.ch[j] = str.ch[i];
++i;
++j;
}
substr.ch[j] = '\0';
substr.length = len;
return 1;
}
}
8. 清空串
int clearString(Str &str)
{
if(str.ch)
{
free(str.ch);
str.ch=NULL;
}
str.length=0;
return 1;
}
4.2 KMP算法
快速的从一个主串中找出一个想要的子串
- 第一位:0
- 第二位:1
- 第n位(n>=3):比较前n-1位,得出最长前后缀匹配长度k,项k+1
求解next数组的几个概念
- 前缀:包含首位字符但不包含末位字符的子串
- 后缀:包含末位字符但不包含首位字符的子串
- next数组定义:当主串于与模式串的某一位字符不匹配时,模式串要回退的位置
- next[j]:其值=第j位字符前面j-1位字符组成的子串的前后缀重和字符数+1
j:1 2 3 4 5 6 7 8
P:a b a a b c a a
Next[j]:0 1 1 2 2 3 1 2
当j=1时,规定next[1]=0;
当j=2时,j 前子串位 ‘a’ ,next[2]=1;
当j=3时,j 前子串位 ‘a b’ ,next[3]=1;
当j=4时,j 前子串位 ‘a b a’ ,next[4]=2;
当j=5时,j 前子串位 ‘a b a a’ ,next[5]=2;
当j=6时,j 前子串位 ‘a b a a b’ ,next[6]=3;
当j=7时,j 前子串位 'a b a a b c a ,next[7]=1;
当j=8时,j 前子串位 ‘a b a a b c a a’ ,next[8]=2;
规律:
- next[j]的值每次最多增加1
- 模式串的最后一位字符不影响next数组的结果
//length为串ch的长度
int GetNext(char ch[], int length, int next[])
{
next[1] = 0;
next[2] = 1;
int i = 1;
int j = 0; //i为当前主串正在匹配的字符位置,也是next数组的索引
while (i < length)
{
if (j == 0 ||ch[i] == ch[j])
//先自增再运算表达式
//相当于 i=i+1;j=j+1;next[i]=j;
next[++i] = ++j;
else
j = next[j];
}
}
- 如果Pj != Pnext[j],那么next[j+1]可能的次大值为next[next[j]]+1]
5.1 数组
- 一维数组 (a0,a1,a2,a3…,an-1)
dataType a[n]; //其中dataType为数据类型,如int型
- 二维数组:数组元素全为一维数组的数组
dataType a[m][n]; //其中dataType为数据类型,如int型
-
- 行优先存储:先存完一行,再存列
- 列优先存储:先存完一列,再存行
5.2 矩阵
int A[m][n];
-
特殊矩阵和稀疏矩阵
相同的元素或零元素在矩阵中的分布存在一定规律的矩阵称之为特殊矩阵,反之称为稀疏矩阵。
2.1 特殊矩阵
1. 对称矩阵
矩阵中的元素满足ai,j=aj,i 的矩阵称之为对称矩阵(矩阵必须为方阵)
以主对角线对称相等
-
假设有一个n*n的对称数组,第一个元素是a0,0 用对称矩阵存储,则an-1,n-1 的下标为:
(1+n)*n/2-1
2. 三角矩阵
-
假设有一个n*n的数组,第一个元素是a0,0 用上三角矩阵存储,则an-1,n-1 的下标为:
(1+n)n/2-1
3. 对角矩阵
-
对于一个按照行优先存储的三对角矩阵,求出第 i 行带状区域内第一个元素 x 在一维数组中的下标,假设c存在数组的最后一位
-
当i 等于1时,带状区域的第一个元素为矩阵中的第一个元素,其在一维数组中下标为0;
-
当i 大于1时,第i行之前的元素个数为2+(i-2)3 ,则带状区域内第一个元素x 在一维数组中的下标为
2+(i-2)*3
-
2.2 稀疏矩阵的顺序存储结构
三元组表示法
二维数组表示三元组:
float trimat[maxSize+1][3]; //maxSize为定义的三元组可能存储元素的最大个数,maxSize+1是因为第一行要存储矩阵基本信息[非0元素个数,矩阵行数,矩阵列数]
float val=trimat[k][0]; //取元素值
int i=(int)trimat[k][1]; //取行标
int j=(int)trimat[k][2]; //取列标
用结构体来表示三元组:
typedef struct
{
float val;
int i,j;
}Trimat;
Trimat trimat[maxSize+1];
float val=trimat[k].val;
int i=trimat[k].i;
int j=trimat[k].j;
2.3 稀疏矩阵的链式存储结构
1. 邻接表表示法
- 每条链表所在行标保存了整条链表中所有元素的行标信息
- 每条链表中的结点保存了对应元素的值和列表信息
2. 十字链表表示法
5.3 广义表
1. 逻辑结构
- A=(),A 是一个空表,长度为0,深度为1
- B(d,e),B的元素全为原子,d和e,长度为2,深度为1
- C(b,(c,d)),C有俩个元素,分别是原子b和另外一个广义表(c,d) ,长度为2,深度为2
- D=(B,C),D的元素全是广义表,B和C,长度为2,深度为3
所以:一个广义表的子表可以是其他已定义的广义表的引用
- E=(a,E),E有两个元素,原子a和它本身,长度为2,由此可见一个广义表可以是递归定义的。展开E可以得到(a,(a,(a,(a,…)))),是一个无限深是广义表。
长度:表中最上层元素的个数
深度:表中括号的最大层数
当广义表非空时:
表头(Head):第一个元素为广义表的表尾,可能是元素也可能是广义表
表尾(Tail):其余元素组成的表是广义表的表尾,只可能是广义表
GetHead(B)=d; //取表头
GetTail(B)=(e); //取表尾
2. 存储结构
1. 头尾链表存储结构
2. 扩展线性表存储结构
6.1 基础知识
1.1 树的基础知识
1. 树的逻辑结构及其相关名词
-
树:非线性的数据结构,是一种分支结构是若干结点的集合,是由唯一的根节点和若干棵互不关联的子树组成
-
结点:体现数据信息的以及逻辑关系的单元
-
结点的度:结点所引出的分支个数
-
树的度:树中各结点度的最大值
-
叶子结点:又叫终端结点,度为0 的结点,即没有分支的结点
-
孩子结点:结点的子树的根
-
双亲结点:孩子结点的上一层结点
-
祖先:从根到某节点的的路径上的所有结点,都是这个结点的祖先
-
子孙:以某结点为根的子树中的所有结点,都是这个结点的子孙
-
兄弟节点:有同一个双亲结点的结点称为兄弟结点
-
堂兄弟结点:彼此的双亲结点互为兄弟结点
-
树的高度:又称深度,指的是树的最大层次数
-
结点的深度和高度
- 结点的深度是从根结点到该结点路径上的结点个数
- 从某结点往下走可能到达多个叶子结点,对应了多条通往这些叶子结点的路径,其中最长的那条路径上结点的个数即该结点在树中的高度
- 根结点的高度为树的高度
2. 树的存储结构
2.1 顺序存储结构
typedef struct
{
int data; //存储数据域
int pIdx; //存储双亲在树中的位置
}TNode;
TNode tree[maxSize];
tree[0].data = A1;
tree[0].pIdx = -1;
tree[1].data = A2;
tree[1].pIdx =0;
tree[2].data = A3;
tree[2].pIdx = 0;
tree[3].data = A4;
tree[3].data = 0;
tree[4].data = A5;
tree[4].data = 1;
tree[5].pIdx = A6;
tree[5].pIdx = 1;
//简化形式
//双亲存储结构
int tree[maxSize];
tree[0]=-1; //下标为0的结点为根结点
tree[1]=0;
tree[2]=0;
tree[3]=0;
tree[4]=1; //下标为4的结点的父节点的下标为1
tree[5]=1;
2.2 链式存储结构
//链表结点类型
//孩子存储结构
typedef struct Branch
{
//Branch为指向其孩子结点是数组下标
int cIdx; //其孩子结点的数组下标
Branch* next;
}Branch;
typedef struct
{
int data;
Branch* first; //指向了存储了所有孩子结点在数组中位置的链表的第一个结点的指针
}TNode;
1.2 二叉树
1. 二叉树的逻辑结构
1. 二叉树的基本知识
对一般树加了一些约束:
-
每个结点最多有两颗子树
-
子树有左右次序之分
-
满二叉树:除了最底层结点之外的所有结点都有左右两个孩子
- 若有n层则共有(2^n-1)个结点
- 第n层有2^(n-1)个结点
-
完全二叉树:对于一颗满二叉树,从右至左逐个删除结点得到的树,可以把满二叉树看成一种特殊的完全二叉树
对于满二叉树:
高为1:2^1-1个结点
高为2:2^2-1个结点
高为3:2^3-1个结点
……
高为h:2^h-1个结点
求完全二叉树的高度(h):
2(h-1)-1<n<=2h-1
2(h-1)<=n<2h
h-1<=log2n<h
向下取整
h=[log2n]+1
2(h-1)-1<n<=2h-1
2(h-1)<n+1<=2h
h-1<log2(n+1)<=h
向上取整
h=[log2(n+1)]
2. 二叉树的一些性质
-
总分支数=总结点数-1(适用于所有树)
-
叶结点数 为N0;
单分支结点数为 N1;
双分支结点数为 N2;
- 总结点数=N0+N1+N2;
- 总分支数:N1+2N2
- 总分支数=总结点数-1
解得:N0=N2+1 即叶子结点数=双分支结点数+1
eg:空指针个数为:结点总数+1
-
叶结点数 为N0;
单分支结点数为 N1;
双分支结点数为 N2;
三分支结点数为N3;
……
m分支结点树为Nm;
- 总分支数=总结点数-1
- 总结点数=N0+N1+N2+N3+…+Nm
- 总分支数=N1+2N2+3N3+…mNm
解得:N0=1+N2+2N3+…+(m-1)Nm
2. 二叉树的存储结构
1. 顺序存储结构
- 对于完全二叉树:(从0 开始编号)
- 父结点位置为i;
- 左孩子结点位置为2i+1;
- 右孩子结点位置为2i+2;
有局限性,只适用于存储完全二叉树
2. 链式存储结构
二叉链表存储结构
//每个结点由三个域组成,左边指针域指向左孩子,中间数据域,右边指针域指向右孩子
typedef struct BTNode
{
int data;
struct BTNode* lChild; //指向其左孩子的指针
struct BTNode* rChild; //指向其右孩子的指针
}BTNode;
//二叉链表存储结构
A1->lChild = A2;
A1->rChild = A3;
A2->lChild = A4;
A2->rChild = NULL;
A3->lChild = A5;
A3->rChild = NULL;
//对于树
typedef struct BTNode
{
int data;
struct BTNode* child;
struct BTNode* sibling; //指向自己的兄弟结点
}BTNode;
//链接
A1->child = A2;
A1->sibling=NULL;
A2->child = A5;
A2->sibling = A3;
A3->sibling = A4;
A4->sibling = NULL;
//取A1孩子结点A3
//印证了知道树中的结点可以找到任意一个孩子结点
A1->child->sibling;
1.3 树和二叉树的互相准换
树的孩子结点之间没有次序之分
森林:多棵树放在一起称之为森林
1.4 遍历
按照某种规则访问这个结构中的所有结点
1. 二叉树的遍历:
1.1 广度(层次)优先遍历
按照其层次优先顺序,一层结点未遍历完不进行下一层的遍历
1.2 深度优先遍历(先序,中序,后序)
-
第一次来到某个结点时访问,所得序列为先序遍历序列
-
第二次来到某个结点时访问,所得序列为中序访问序列
-
第三次来到某个结点时访问,所得序列为后序访问序列
-
先序遍历:先访问根结点,然后先遍历左子树,最后遍历右子树
-
中序遍历:先中序遍历左子树,然后访问根结点,最后中序遍历右子树
-
后序遍历:先后序遍历左子树,然后后续遍历右子树,最后访问根结点
2. 树的遍历
2.1 广度(层次优先遍历)
2.2 深度优先遍历
树的叶子结点只有一个空分支,代表其没有孩子结点
- 第一次来到某个结点时访问,所得序列为先序遍历序列
- 最后一次来到某个结点时访问,所得序列为后序访问序列
对于树的结点,不一定都会经历两次,也不一定经历三次,所以会有歧义,不规定中序遍历
- 如果一颗二叉树是由某个树转换而来,那对二叉树树进行先序遍历就相当于对原来的树进行先序遍历
- 如果一颗二叉树是由某个树转换而来,那对二叉树树进行中序遍历就相当于对原来的树进行后续遍历
3. 森林的遍历
-
先序遍历:先序遍历森林中的每一棵树
-
后续遍历:后序遍历森林中的每一颗树
-
如果一颗二叉树是由某个森林转换而来,那对二叉树树进行先序遍历就相当于对原来的森林进行先序遍历
-
如果一颗二叉树是由某个森林转换而来,那对二叉树树进行中序遍历就相当于对原来的森林进行后续遍历
1.5 递归基础
直接递归调用:在函数体内对自己进行调用
对于所有函数都有:
保护现场:存档
恢复现场:读档
而对于递归函数,将保护现场一个个压栈,恢复现场对应出栈操作
1.6 二叉树的深度优先遍历(递归)
void r(BTNode *p)
{
if (p != NULL)
{
//先序遍历
r(p->lChild);
//中序遍历
r(p->rChild);
//后序遍历
}
}
//先序遍历
void r(BTNode *p)
{
if (p != NULL)
{
visit(p);
r(p->lChild);
r(p->rChild);
}
}
//中序遍历
void r(BTNode *p)
{
if (p != NULL)
{
r(p->lChild);
visit(p);
r(p->rChild);
}
}
//后序遍历
void r(BTNode *p)
{
if (p != NULL)
{
r(p->lChild);
r(p->rChild);
visit(p);
}
}
1.7 二叉树深度优先遍历(非递归)
1. 先序遍历非递归化
- 从根结点开始入栈一个元素
- 不停的执行以下操作:
- 如果栈不空,就出栈一个元素,并对其进行访问,并访问其左右孩子
- 若左右孩子存在,则依次入栈,右孩子先入栈,左孩子后入栈
- 若没有左右孩子则继续出栈一个元素
- 如果进行出栈操作后栈为空,表明遍历结束
typedef struct BTNode
{
int data;
struct BTNode* lChild; //指向其左孩子的指针
struct BTNode* rChild; //指向其右孩子的指针
}BTNode;
//参数列表为根结点的指针
void preorder(BTNode *bt)
{
//首先判断根结点是否为空
if (bt != NULL)
{
//建立一个栈
BTNode *Stack[maxSize];
int top = -1;
BTNode *p = NULL;
Stack[++top] = bt;
while (top != -1)
{
//先出栈一个元素,并对其进行访问
p = Stack[top--];
Visit(p);
//监测其左右孩子是否存在
//如果存在则入栈
//先入栈右孩子,再入栈左孩子
if (p->rChild != NULL)
Stack[++top] = p->rChild;
if (p->lChild != NULL)
Stack[++top] = p->lChild;
}
}
}
2. 后序遍历非递归化
-
先序遍历:先访问根结点,然后先遍历左子树,最后遍历右子树
-
后序遍历:先后序遍历左子树,然后后续遍历右子树,最后访问根结点
-
逆后续遍历序列:先遍历根,再遍历右子树,最后遍历左子树
//后序遍历序列
typedef struct BTNode
{
int data;
struct BTNode* lChild; //指向其左孩子的指针
struct BTNode* rChild; //指向其右孩子的指针
}BTNode;
//参数列表为根结点的指针
void preorder2(BTNode *bt)
{
//首先判断根结点是否为空
if (bt != NULL)
{
//建立两个栈,一个是辅助栈,一个是将结果序列逆序的栈
BTNode *Stack1[maxSize];
int top1 = -1;
BTNode *Stack2[maxSize];
int top2 = -1;
BTNode *p = NULL;
Stack1[++top1] = bt;
while (top1 != -1)
{
//先出栈一个元素,并对其进行访问
p = Stack1[top1--];
Stack2[++top2] = p;
Visit(p);
//监测其左右孩子是否存在
//如果存在则入栈
//先入栈左孩子,再入栈右孩子
if (p->lChild != NULL)
Stack1[++top1] = p->lChild;
if (p->rChild != NULL)
Stack1[++top1] = p->rChild;
}
while (top2 != -1)
{
p = Stack2[top2--];
Visit(p);
}
}
}
3. 中序遍历非递归化
-
中序遍历:先中序遍历左子树,然后访问根结点,最后中序遍历右子树
-
从根结点开始入栈一个元素
-
从根结点开始访问其左分支,边访问边将其历经的结点入栈
-
直到不能继续向左走为止,此时出栈一个结点,并访问
-
从这个结点开始,先往右访问一次,然后继续上述操作
//后序遍历
void ino(BTNode *bt)
{
if (bt != NULL)
{
BTNode *Stack[maxSize];
int top = -1;
BTNode *p = NULL;
p = bt;
while (top != -1||p!=NULL) //执行完if语句以后可能栈空,但如果p的右子树存在,即p!=NULL(执行完if语句后p = p->rChild),还应该继续遍历
{
while (p != NULL)
{
Stack[++top] = p;
p = p->lChild;
}
if (top != -1)
{
p = Stack[top--];
Visit(p);
p = p->rChild;
}
}
}
}
1.8 二叉树层次遍历
- 从根结点开始,根结点先入队,出队访问值然后监测其左右孩子是否存在
- 左孩子先入队,右孩子后入队
- 若存在,则入队
- 重复上述步骤直到队空为止
typedef struct BTNode
{
int data;
struct BTNode *lChild;
struct BTNode *rChild;
}BTNode;
void level(BTNode *bt)
{
//当树不空的时候进行遍历
if (bt != NULL)
{
//建立一个队列,队内元素类型为BTNode
int front, rear;
BTNode *que[maxSize];
front = rear = 0;
//p作为遍历指针
BTNode *p;
//让根结点入队
rear = (rear + 1) % maxSize;
que[rear] = bt;
while (front != rear)
{
//保存出队一个元素,并对其进行访问
front = (front + 1) % maxSize;
p = que[front];
Visit(p);
//判断其左右孩子是否存在,若存在,则入队
//左孩子先入队,右孩子后入队
if (p->lChild != NULL)
{
rear = (rear + 1) % maxSize;
que[rear] = p->lChild;
}
if (p->rChild != NULL)
{
rear = (rear + 1) % maxSize;
que[rear] = p->rChild;
}
}
}
}
1.9 树的深度遍历
//链表结点类型
//孩子存储结构
typedef struct Branch
{
//Branch为指向其孩子结点是数组下标
int cIdx; //其孩子结点的数组下标
Branch* next;
}Branch;
typedef struct
{
int data;
Branch* first; //指向了存储了所有孩子结点在数组中位置的链表的第一个结点的指针
}TNode;
//先序遍历
//添加一个树的结点的索引数组参数
void preOrder(TNode *p, TNode tree[])
{
if (p != NULL)
{
Visit(p);
//定义一个Branch类型的指针q让其指向当前链表的第一个结点
Branch *q;
q = p->first;
//让q遍历整个链表
while (q != NULL)
{
preOrder(&tree[q->cIdx], tree);
q = p->next;
}
}
}
//后序遍历
//添加一个树的结点的索引数组参数
void preOrder(TNode *p, TNode tree[])
{
if (p != NULL)
{
//定义一个Branch类型的指针q让其指向当前链表的第一个结点
Branch *q;
q = p->first;
//让q遍历整个链表
while (q != NULL)
{
preOrder(&tree[q->cIdx], tree);
q = p->next;
}
Visit(p);
}
}
1.10 树的广度(层次)遍历
//广度遍历
void level(TNode *tn, TNode tree[])
{
//定义一个队列
int front, rear;
TNode *que[maxSize];
front = rear = 0;
//p作为遍历指针
TNode *p;
if (tn != NULL)
{
rear = (rear + 1) % maxSize;
que[rear] = tn;
while (front != rear)
{
front = (front + 1) % maxSize;
p = que[front];
Visit(p);
Branch *q = p->first;
while (q != NULL)
{
rear = (reat + 1) % maxSize;
que[rear] = &tree[q->cIdx];
q = q->next;
}
}
}
}
6.2 考点
1. 中序线索二叉树
1).逻辑结构
如果一个结点有左空指针,则左空指针指向其前驱,若存在右空指针,则右空指针指向其后继
2).存储结构
typedef struct TBTNode
{
int data;
int lTag; //lTag==0时,lTag指向的是左孩子,lTag==1时,lTag指向的是前驱
int tTag; //rTag==0时,rTag指向的是右孩子,rTag==1时,rTag指向的是后继
TBTNode *lChild;
TBTNode *rChild;
}TBTNode;
//用pre指针来始终指向当前访问结点的前驱结点,也就是p指针的指向结点的前驱结点
void inThread(TBTNode *p, TBTNode *&pre)
{
if (p != NULL)
{
inThread(p->lChild, pre);
if (p->lChild == NULL)
{
//如果左孩子为空,直接让其指向其前驱结点
//并将lTag置为1,表明此时其指向前驱结点
p->lChild = pre;
p->lTag = 1;
}
//监测pre结点
if (pre != NULL && pre->rChild == NULL)
{
//如果右孩子为空,让其指向pre结点的后继结点即p所指结点
//并将pre的rTag = 1,表明其右指针指向其后继结点
pre->rChild = p;
pre->rTag = 1;
}
//因为上一步执行完毕,让pre沿着其右孩子指针向后移,为了保证pre指针始终指向p的前驱,令pre = p;
pre = p;
inThread(p->rChild, pre);
}
}
- 从根节点一直向左走,走到空指针为止,则此空指针所在的结点就是遍历序列的第一个结点
- 从根结点一直向右走,走到空指针为止,则此空指针所在的结点就是遍历序列的最后一个结点
2. 前序线索二叉树
//前序二叉树线索化
//用pre指针来始终指向当前访问结点的前驱结点,也就是p指针的指向结点的前驱结点
void preThread(TBTNode *p, TBTNode *&pre)
{
if (p != NULL)
{
if (p->lChild == NULL)
{
//如果左孩子为空,直接让其指向其前驱结点
//并将lTag置为1,表明此时其指向前驱结点
p->lChild = pre;
p->lTag = 1;
}
//监测pre结点
if (pre != NULL && pre->rChild == NULL)
{
//如果右孩子为空,让其指向pre结点的后继结点即p所指结点
//并将pre的rTag = 1,表明其右指针指向其后继结点
pre->rChild = p;
pre->rTag = 1;
}
//因为上一步执行完毕,让pre沿着其右孩子指针向后移,为了保证pre指针始终指向p的前驱,令pre = p;
pre = p;
if(p->lTag==0)
preThread(p->lChild, pre);
if(p->rTag==0)
preThread(p->rChild, pre);
}
}
- 如果一个结点的左指针不为空,并且左指针不是线索,则左指针指向其后继,是线索的,指向其前驱
- 如果一个结点的左指针为空,右指针不为空,则其右指针指向的是其后继结点
- 先序线索排列的本质就是找后继
//先序线索二叉树的遍历操作
void preOrder(TBTNode *tbt)
{
if (tbt != NULL)
{
TBTNode *p = tbt;
while (p != NULL)
{
while (p->lTag == 0)
{
Visit(p);
p = p->lChild;
}
Visit(p);
p = p->rChild;
}
}
}
3. 后续线索二叉树
//线索链接
void postThread(TBNode *p, TBNode *&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;
}
}
如何找一个结点的后继
- 若结点X是二叉树的根,则其后继为空
- 若结点X是其双亲结点的右孩子,或是其双亲结点的左孩子且其双亲没有右子树,则其后继即为双亲结点
- 若结点X是其双亲结点的左孩子,且其双亲结点有右子树,则其后继为双亲右子树上按后序遍历出的第一个结点
三种线索二叉树的比较
前序 | 中序 | 后序 |
---|---|---|
找某个结点的后继简单 | 找某个结点的后继很简单 | 找某个结点的后继困难 |
找某个结点的前驱困难 | 找某个结点的前驱很简单 | 找某个结点的前驱困难 |
6.4 哈夫曼树
1. 哈夫曼二叉树
-
路径:指从树中一个结点到另一个结点的分支所构成的路线
-
路径长度:指路径上的分支数目
-
树的路径长度:指从根到每个结点的路径长度之和
-
带权路径长度:结点具有权值,从该结点到根之间的路径长度乘以结点的权值,就是该结点的带权路径长度
-
树的带权路径长度(WPL):指树中所有叶子结点的带权路径长度之和
-
哈夫曼树的特点
- 权值越大的结点距离根结点越近
- 树中没有度为1的结点(结点的度:结点所引出的分支个数)这类树又叫做正则(严格)二叉树
- 树的带权路径长度最短
2. 哈夫曼n叉树
每次选择三个权值最小的结点构造哈夫曼三叉树(不够补充权值为0 的结点)
6.5 二叉树的确定
根据遍历序列确定二叉树
1. 已知先序和中序遍历序列
-
已知先序和中序遍历序列,能确定一颗二叉树
-
通过先序遍历序列可以找到根结点,
-
通过中序遍历序列可以把遍历序列划分成两部分
typedef struct BTNode
{
int data;
struct BTNode *lChild;
struct BTNode *rChild;
}BTNode;
//pre[]先序访问序列, in[]中序访问序列
//参数列表:把两个遍历序列传进数组,为每个数组定义控制范围
BTNode *CreateBT(char pre[], char in[], int L1, int R1, int L2,int R2)
{
//定义递归出口:处理的数组范围为0
if (L1 > R1)
return NULL;
//为根结点申请一个存储单元
BTNode *s = (BTNode)malloc(sizeof(BTNode));
s->lChild = s->rChild = NULL;
//根结点的数据值为pre数组的L1结点,即先序访问序列的第一个结点
s -> data = pre[L1];
//从中序遍历序列中找出根结点的位置
int i;
for (i = L2; i <= R2; ++i)
{
if (in[i] == pre[L1])
break;
}
//先序遍历序列的范围为:L1 + 1至L1 + i - L2
//中序遍历序列的范围为:L2至i - 1(划分好的左子树的 范围)
s->lChild = CreateBT(pre, in, L1 + 1, L1 + i - L2, L2, i - 1);
s->rChild = CreateBT(pre, in, L1 + i - L2 + 1, R1, i + 1, R2);
//返回建立的树的根指针
return s;
}
2. 已知后序和中序遍历序列
后序遍历和前序遍历相似,只不过后序遍历序列的根结点在最后一位
//已知后序和中序访问序列
BTNode *CreateBT2(char post[], char in[], int L1, int R1, int L2, int R2)
{
if (L1 > R1)
return NULL;
//为根结点申请一个存储单元
BTNode *s = (BTNode)malloc(sizeof(BTNode));
s->lChild = s->rChild = NULL;
//根结点的数据值为post数组的R1结点,即最后一个结点
s -> data = post[R1];
int i;
for (i = L2; i <= R2; ++i)
{
if (in[i] == post[R1])
break;
}
s->lChild = CreateBT2(post, in, L1, L1 + i - L2 - 1, L2, i - 1);
s->rChild = CreateBT2(post, in, L1 + i - L2, R1 - 1, i + 1, R2);
return s;
}
3. 已知层次遍历序列和中序遍历序列
与先序遍历序列划分类似,只不过先序遍历序列划分的是连续的,层次遍历序列划分是不连续的
- 把两部分不连续的结点,并让他们保持层次遍历序列中的次序,存在两个数组中
//已知层次访问序列和中序访问序列
int search(char arr[], char key, int L, int R)
{
int idx;
for (idx = L; idx <= R; ++idx)
if (arr[idx] == key)
return idx;
return -1;
}
void getSubLevel(char subLevel[],
char level[],
char in[],
int n,
int L,
int R)
{
int k = 0;
for (int i = 0; i < n; ++i)
if (search(in, level[i], L, R) != -1)
subLevel[k++] = level[i];
}
BTNode *CreateBT3(char level[], char in[], int n, int L, int R)
{
if (L > R)
return NULL;
//为根结点申请一个存储单元,当前处理的结点为层次访问序列的第一个结点
BTNode *s = (BTNode)malloc(sizeof(BTNode));
s->lChild = s->rChild = NULL;
s->data = level[0];
//在中序访问子序列中找出当前子树根结点的位置
int i = search(in, level[0], L, R);
//通过中序访问的左右子树来进行元素划分
int LN = i - L;
char Llevel[LN];
int RN = R - i;
char Rlevel[RN];
getSubLevel(Llevel, level, in, n, L, i - 1);
getSubLevel(Rlevel, level, in, n, i + 1, R);
s->lChild = CreateBT3(Llevel, in, LN, L, i - 1);
s->rChild = CreateBT3(Rlevel, in, RN, i + 1, R);
return s;
}
已知先序和后序遍历序列不能确定二叉树
6.6 二叉树的估计和二叉树存储表达式
1. 二叉树的估计
- 前序遍历和后序遍历结果相同的二叉树为:空树或者只有根结点的树
- 前序遍历和中序遍历结果相同的二叉树为:没有左子树的树(所有结点都没有左子树)
前序和后序 遍历结果相同的二叉树为 | TLR LRT | T 空树或者只有根结点的树 |
前序和中序 遍历结果相同的二叉树为 | TLR LTR | TR 没有左子树的树(所有结点都没有左子树) |
中序和后序 遍历结果相同的二叉树为 | LTR LRT | LT 没有右子树的树(所有结点都没有右子树) |
前序和后序 遍历结果相反的二叉树为 | TLR LRT | TL或者TR,没有左子树的树或者没有右子树的树 |
前序和中序 遍历结果相反的二叉树为 | TLR LTR | TL 没有右子树的树(所有结点都没有右子树) |
中序和后序 遍历结果相反的二叉树为 | LTR LRT | TR 没有左子树的树(所有结点都没有左子树) |
2. 二叉树表达式
用二叉树来存储一个算数表达式
利用栈来建立二叉树
先序遍历得到前缀表达式;
后序遍历得到后缀表达式;
利用二叉树的后序遍历
int calSub(float opand1, char op, float opand2, float &result)
{
if (op == '+')
result = opand1 + opand2;
if (op == '-')
result = opand1 - opand2;
if (op == '*')
result = opand1 * opand2;
if (op == '/')
{
//fabs为取绝对值,MIN为宏,设置为非常小的正数,可以看作为0
if (fabs(opand2) < MIN)
{
return 0;
}
else
{
result = opand1 / opand2;
}
}
return 1;
}
typedef struct BTNode
{
int data;
struct BTNode *lChild;
struct BTNode *rChild;
}BTNode;
float cal(BTNode *root)
{
//如果只有根结点,则返回根结点的数值
if (root->lChild == NULL && root->rChild == NULL)
return root->data - '0';
else
{
float opand1 = cal(root->lChild);
float opand2 = cal(root->rChild);
//先左子树再右子树再根:后序遍历序列
float result;
calSub(opand1, root->data, opand2, result);
return result;
}
}
7.1 图的基础知识
1. 逻辑结构和基本概念
树是分支结构一对多关系、
图是多对多关系
一些基本概念
名称 | 概念 |
---|---|
图 | 是由顶点的有穷集合V和边的集合E组成 |
无向图 | 每条边都没有方向 边:(A1,A3) |
有向图 | 每条边(弧)都有方向 弧:<A1,A3> |
顶点的度 | 和某个顶点有关的边的条数 |
顶点的入度 | 在有向图中,指向某顶点 的边的条数称为该顶点的入度 |
顶点的出度 | 在有向图中,由某顶点发出的边的条数称为顶点的出度 |
简单图 | 在图中,不存在某顶点到其自身的边,且不存在重复的边的图 |
无向完全图 | 在无向图中,若任意两顶点之间存在边,则称之为无向完全图,这种图边的个数为:n(n-1)/2,n为顶点数 |
有向完全图 | 在有向图中,若任意两项之间存在方向相反的两条边,这种图边的个数为:n(n-1) ,n为顶点数 |
带权图 | 在图中,若边含有权值,则一般称之为网 |
路径 | 相邻顶点序偶所构成的序列,路径长度是路径上边的个数 |
环 | 第一个顶点到最后顶点相同的路径称之为回路或者环 |
简单路径 | 序列中顶点不重复出现的路径称之为简单路径 |
简单环 | 序列中除了第一个和最后一个顶点,其余顶点都不重复出现的环称之为简单环 |
连通 | 无向图中,如果Vi到Vj有路径,则称Vi和Vj连通 |
连通图 | 如果图中任意一对顶点连通,则此图为连通图 |
连通分量 | 无向图中的极大连通子图称之为连通分量 |
强连通图 | 在有向图中,每一对Vi到Vj,从Vi到Vj和从Vj到Vi都存在路径 |
强连通分量 | 有向图的极大连通子图称之为强连通分量 |
2. 存储结构
1. 顺序存储结构
-
有向图矩阵对称
-
无向图矩阵不对称
-
对于一行中1的个数:为所发出边的条数(出度)
-
对于所在的列中1的个数:指向它的边的条数(入度)
-
用二维数组的行标、列标来代表顶点信息
- 行标代表起点
- 列标代表终点
-
用数组中所存的数据来代表顶点之间的关系
-
对于带权图:如果两个元素之间存在关系,则数组中存放的数据为其权数,没关系记为∞
-
矩阵对角线位置为其到自身的关系,可以设置为0(表明自己到自己的为0)也可以设置为∞,表明没有自身到自身(考试考简单图,没环自己)
-
带权图
float MGraph[5][5]
for (int i = 0; i < 5; ++i)
for (int j = 0; j < 5; ++j)
//进行初始化,即给每个边赋一个很大的值,表明顶点之间没有关系
MGraph[i][j] = MAX;
- 邻接矩阵
//多出的顶点数组,行标和列标和对应一位数组的数组下标来表述顶点之间的关系
char vertex[5] = { 'A','B','C','D','E' };
//Edge只存储边信息
float Edge[5][5];
for (int i = 0; i < 5; ++i)
for (int j = 0; j < 5; ++j)
Edge[i][j] = MAX;
2. 链式存储结构
1. 邻接表、十字链表(有向图)和邻接多重表(无向图)存储法
- 有向图
- 邻接表
typedef struct ArcNode //针对分支设计的数据结构
{
int adjV; //邻接顶点,就是这条边所对应的顶点
struct ArcNode * next; //指向下一个边结点的指针
}ArcNode;
typedef struct //针对顶点设计的数据结构
{
int data;
ArcNode *first; //取顶点对应的第一条边
}VNode;
typedef struct //图结构体
{
VNode adjList[maxSize]; //将图的顶点数据放在一个数组中
int n, e; //顶点和边的个数
}AGraph;
- 逆邻接表
每一个链表所引出的链表所对应的边是指向这个顶点的,和邻接表相反
入边:指向某个顶点的边,反之称之为出边
- 访问入边利用逆邻接表合适,访问出边利用邻接表合适
- 十字链表
-
找入边:边结点的end域与顶点所在的数组下标相同
-
找出边:边结点的start域与顶点所在的数组下标相同
-
无向图
- 邻接多重表:消除多余 的边界点,又能正确存储结点之间的关系(对于无向图)
十字链表和邻接多重表分别是对有向图和无向图邻接表的改进
7.2 考点
1. 深度优先遍历(DFS)
//针对分支设计的数据结构
typedef struct ArcNode
{
//邻接顶点,就是这条边所对应的顶点
int adjV;
//指向下一个边结点的指针
struct ArcNode * next;
}ArcNode;
typedef struct
{
int data;
ArcNode *first;
}VNode;
typedef struct
{
VNode adjList[maxSize];
int n, e;
}AGraph;
//v为顶点下标
//初始标记数组 visit[v]全为0
void DFS(int v, AGraph *G)
{
visit[v] = 1; //作为访问标记
Visit(v);
ArcNode *q = G->adjList[v].first; //访问v引出的第一个条边
while (q != NULL) //以q的邻接点为起点做邻接遍历
{
if (visit[q->adjV] == 0) //用q->adjV取q指向的顶点数组下标,用visit数组访问是否为0,为0 表示未访问
DFS(q->adjV,G);
q = q->next; //让q指向与顶点数组相关的另一条边上
}
}
2. 广度优先遍历(BFS)
typedef struct ArcNode //边结构体设计
{
int adjv;
struct ArcNode *next;
}ArcNode;
typedef struct //顶点结构体设计
{
int data;
ArcNode *first;
}VNode;
typedef struct //图结构体设计
{
VNode adjList[maxsize];
int n, e;
}AGraph;
void bfs(AGraph *G, int v, int visit[maxSize])
{
ArcNode *p;
int que[maxSize], front = 0, rear = 0;
int j;
Visit[v];
visit[v] = 1;
rear = (rear + 1) % maxSize; //将顶点数组下标入队
que[rear] = v;
while (front != rear)
{
front = (front + 1) % maxSize;
j = que[front];
p = G->adjList[j].first;
while (p != NULL)
{
if (visit[p->adjv == 0])
{
Visit(p->adjv);
visit[p->adjv] = 1;
rear = (rear + 1) % maxSize;
que[rear] = p->adjv;
}
p = p->next;
}
}
}
以顶点为中心开始扩散访问其邻接顶点,都访问过则访问其邻接顶点,以邻接顶点为中心继续扩散
3. 最小生成树
1. 生成树和最小生成树
-
由图的所有顶点和部分构成的一颗树叫做该图的生成树
-
在图中通过删除某些边把图中的所有消除后剩余的部分就是生成树
-
一个图可以导出多个生成树
-
构成这颗树的所有分支的权值和最小的树叫做最小生成树
2. Prim算法
-
以顶点为操作单位
-
从图中任取一个顶点,把它当成一棵树
-
从与这棵树相接的边中选取一条最短(权值最小)的边,将这条边与其所连接的顶点也并入这棵树中
-
重复以上操作直至所有顶点并入树中
-
在找侯选边时,考虑当前生成树中的顶点与图中剩余顶点的边
代码示例:
float MGraph[5][5];
for (int i = 0; i < 5; ++i)
for (int j = 0; j < 5; ++j)
//进行初始化,即给每个边赋一个很大的值,表明顶点之间没有关系
MGraph[i][j] = MAX;
//顶点个数n 带权图MGraph 构造最小生成树的第一个顶点 浮点引用型数据sum记录最小代价和
void Prim(int n, float MGraph[][n], int v0, float&sum)
{
//lowCost[n] 数组用来记录当前顶点到顶点 n 的最小权值
//vSet[n]==1,表明该顶点已经被并入生成树中,初始为0
int lowCost[n], vSet[n];
//用v来指向当前刚并入的顶点
int v, k, min;
for (int i = 0; i < n; ++i)
{
//取第一个顶点到其余顶点i的权值放入lowCost[i]数组
lowCost[i] = MGraph[v0][i];
//vSet[i] = 0,表明该顶点未并入
vSet[i] = 0;
}
v = v0; //v始终指向当前刚并入的顶点
vSet[v] = 1;
sum = 0;
//循环n-1个,因为最开始n个顶点访问完还有n-1个顶点
for (int i = 0; i < n - 1; ++i)
{
//min初值设置成∞
min = INF;
for (int j = 0; j < n; ++j)
{
//找出未访问且权值最小的顶点的数组下标k
if (vSet[j] == 0 && lowCost[j] < min)
{
min = lowCost[j];
k = j;
}
vSet[k] = 1;
v = k; //k并入生成树
//更新最小权值和
sum += min;
//更新lowCost数组的值
for (int j = 0; j < n; ++j)
//未访问且权值小于当前lowCost数组中所存最小权值,则更新该权值
if (vSet[j] == 0 && MGraph[v][j] < lowCost[j])
lowCost[j] = MGraph[v][j];
}
}
}
3. Kruskal算法
-
以边为操作的主要单位,每次并入一条边
-
每次选未被并入的且并入后不会产生环的最短(权值最小)的那条边并入
-
并查集
Kruskal算法代码
//图的存储结构设计
typedef struct
{
int a, b; //a,b表述边对应的两个顶点
int w; //w表示边的权重
}Road;
Road road[maxSize];
int v[maxSize];
//取根节点下标值 v[]存放的是顶点和顶点存的数组下标值
int getRoot(int p)
{
//只有当p值等于p存的下标值时,p才是根节点
while (p != v[p])
p = v[p];
return p;
}
void Kruskal(Road road[], int n, int e, int &sum)
{
int a, b;
sum = 0;
for (int i = 0; i < n; ++i)
v[i] = i;
//把存储边的数组按照边的权值大小从小到大进行排序
sort(road, e);
for (int i = 0; i < e; ++i)
{
a = getRoot(road[i].a);
b = getRoot(road[i].b);
//a,b根节点不同,表示a,b在不同的树中
if (a != b)
{
v[a] = b;
sum += road[i].w;
}
}
}
4. 最短路径
1. Dijkstra算法
可以求某一顶点到图中其余各顶点的最短路径
dist[v]>dist[Vpre]+MGraph[Vpre][V]
更新 dist[v] 为dist[Vpre]+MGraph[Vpre][V]
更新 path[v]为 Vpre
//参数列表(顶点个数,边信息,起始顶点,存最短路径长度,存最短路径)
void Dijkstra(int n, float MGraph[][n], int vo, int dist[], int path[])
{
//对dist[]和path set[]数组进行初始化
int set[maxSize];
int min, v;
for (int i = 0; i < n; ++i)
{
dist[i] = MGraph[v0][i];
set[i] = 0;
if (MGraph[v0][i] < INF)
path[i] = v0;
else
path[i] = -1;
}
set[vo] = 1;
path[vo] = -1;
//除去自己,本来n个顶点现在n-1个顶点,循环n-1次
for (int i = 0; i < n - 1; ++i)
{
//找出距离起点最近的顶点并入树
min = INF;
for (int j=0;j<n;++j)
if (set[j] == 0 && dist[j] < min)
{
v = j;
min = dist[j];
}
set[v] = 1;
//更新dist和path数组
for (int j = 0; i < n; ++j)
{
//需要更新的顶点是未并入的顶点
if (set[j] == 0 && dist[v] + MGraph[v][j] < dist[j])
{
dist[j] = dist[v] + MGraph[v][j];
path[j] = v;
}
}
}
}
-
dist[]:起点到其余各顶点的最短路径长度
-
path[]:顶点到其所在的最短路径上的前一个顶点的数组下标信息
-1:表明当前顶点在其最短路径上没有前一个结点
-
set[]:用来标记顶点是否并入最短路径
-
当我们检测从起点经过中介点,再由中介点到被测顶点时,由中介点到被测点是一条直接的边,如果无直接的边,则视其距离为∞
更新dist[]和path[]基本流程
-
如果起点到未被并入的顶点距离>生成树起点经过刚被并入顶点到未被并入的顶点
更新dist[]值为 生成树起点经过刚被并入顶点到未被并入的顶点
更新path[]值为刚并入的顶点
-
从目前还未并入最短路径的顶点中选出一个距离起点最近的顶点,并入生成树中,set值改为1
2. Floyd算法
求解任意两点之间的最短路径
A[][] //存储了任意两个顶点当前的最短路径长度
path[][] //存储了任意两个顶点所在最短路径上的中间点
执行过程:
对于每个顶点v,和任一顶点对(i,j),i!=j,v!=i,v!=j,
如果A[i][j]>A[i][v]+A[v][j],
则将A[i][j]更新为A[i][v]+A[v][j]的值,
并且将path[i][j]改为v
基本流程
//寻找从u到v的最短路径
void printPath(int u, int v, int path[][max])
{
if(path[u][v]==-1)
直接输出
else
{
int mid = path[u][v];
printPath(u, mid, path);
printPath(mid, v, path);
}
}
代码示例
//顶点个数
void Floyd(int n, float MGraph[][], int Path[][n])
{
int i, j, v;
int A[n][n];
for(i=0;i<n;++i)
for (j = 0, j < n, ++j)
{
A[i][j] = MGraph[i][j];
Path[i][j] = -1;
}
//选出所有的v
for(v=0;v<n;++v)
//选出所有的顶点对,对当前的v进行检测
for(i=0;i<n;++i)
if (A[i][j] > A[i][v] + A[v][j])
{
A[i][j] = A[i][v] + A[v][j];
Path[i][j] = v;
}
}
5. 拓扑排序
对于AOV图:活动在顶点上的网
- 从有向图中选择一个入度为0的顶点输出
- 删除1 中的顶点,并且删除从该顶点出发的全部边
- 重复上述两步,直到剩余的图不存在入度为0的顶点为止
这样得到的序列就是拓扑排序序列
constexpr auto maxSize = 10000;
typedef struct ArcNode
{
int adjV; //邻接顶点,就是这条边所对应的顶点
struct ArcNode * next; //指向下一个边结点的指针
}ArcNode;
typedef struct
{
int data;
//用count来标记此顶点的入度
int count;
ArcNode* first;
}VNode;
typedef struct
{
VNode adjList[maxSize];
int n, e; //顶点和边的个数
}AGraph;
//返回值为int,是因为要返回是否成功
//有环肯定失败
int TopSort(AGraph *G)
{
//i,j 是循环变量,n是用来统计以及输出的拓扑序列的顶点的个数,可以帮助我们判断拓扑排序是否成功
int i, j, n = 0;
//用栈来保存当前图中入度为0的顶点
int stack[maxSize], top = -1;
ArcNode *p;
//假设入度count已知
for (i = 0; i < G->n; ++i)
if (G->adjList[i].count == 0)
stack[++top] = i;
while (top != -1)
{
i = stack[top--];
++n;
std::cout << i << "";
p = G->adjList[i].first;
while (p != NULL)
{
j = p->adjV;
--(G->adjList[j].count);
if (G->adjList[j].count == 0)
stack[++top] = j;
p = p->next;
}
}
if (n == G->n)
return 1;
else
return 0;
}
- 从有向图中选择一个出度为0的顶点输出
- 删除1 中的顶点,并且删除指向该顶点的全部边
- 重复上述两步,直到剩余的图不存在出度为0的顶点为止
这样得到的序列就是逆拓扑排序序列
//针对分支设计的数据结构
typedef struct ArcNode
{
//邻接顶点,就是这条边所对应的顶点
int adjV;
//指向下一个边结点的指针
struct ArcNode * next;
}ArcNode;
typedef struct
{
int data;
ArcNode *first;
}VNode;
typedef struct
{
VNode adjList[maxSize];
int n, e;
}AGraph;
//v为顶点下标
//初始标记数组 visit[v]全为0
void DFS(int v, AGraph *G)
{
visit[v] = 1; //作为访问标记
ArcNode *q = G->adjList[v].first; //访问v引出的第一个条边
while (q != NULL) //以q的邻接点为起点做邻接遍历
{
if (visit[q->adjV] == 0) //用q->adjV取q指向的顶点数组下标,用visit数组访问是否为0,为0 表示未访问
DFS(q->adjV, G);
q = q->next; //让q指向与顶点数组相关的另一条边上
}
Visit(v);
}
6. 关键路径
AOE网:活动在边上的网
- 边的长度:活动持续事件
- 顶点:活动的开始或者结束事件
- 关键路径:在AOE网中,从源点开始到汇点结束的最大路径长度的那条路径
- 关键活动:活动最早发生时间和活动最迟发生时间重合的活动
- 关键路径:所有的关键活动,构成了关键路径
-
事件最早发生时间:
-
求出图的拓扑排序序列和逆拓扑排序序列
-
根据拓扑排序序列找出每个事件的最早发生时间,
如果某个事件由多条边指向它,或者说有多个活动导致它的发生,其中最晚发生的那个活动是这个事件的最早发生时间
-
-
事件最迟发生时间
-
根据逆拓扑排序序列找出每个事件的最迟发生时间
-
首先确定最后一个事件的最迟发生时间为最早发生时间,才能确定其他活动的最迟发生时间
对于某个事件如果有多条事件引出,有多条后继事件发生,它的最迟发生时间是根据所有后继事件所得出的最迟发生事件中的最早的那个
-
-
活动最早发生时间
发出这些活动的事件的最早发生时间就是活动的最早发生时间,因为AOE网中引出活动的事件就是活动的开始
-
活动最迟发生时间
- 它所指向的事件的最迟发生时间 - 活动持续时间
8.1 直接插入,简单选择排序和气泡排序
1. 直接插入排序
sort:排序和分类
关键字:排序的对象
初始序列越接近升序排列,比较次数越少,效率越高
//arr[]数组用以存储我们排序的结构,n表示我们排序的个数
void insertSort(int arr[], int n)
{
//temp用来暂存关键字
int temp, i, j;
//第一个关键字下标为0,把它当成有序序列,外层循环从无序序列也就是第二个关键字也就是下标1开始
for (i = 1; i < n; ++i)
{
temp = arr[i];
//i是无序序列的第一个元素,j表示i左边的那个元素,也就是有序序列的最后一个关键字
j = i - 1;
//从j 开始到 0结束表示从右往左扫描整个有序序列
while (j >= 0 && temp < arr[j])
{
arr[j + 1] = arr[j];
--j;
}
arr[j + 1] = temp;
}
}
2. 简单选择排序
void selectSort(int arr[], int n)
{
int i, j, k;
int temp;
for (i = 0; i < n; ++i)
{
//取无序序列的最小值
k = i;
for (j = i + 1; j < n; ++j)
{
if (arr[k] > arr[j])
k = j;
}
//将最小值插入有序序列的最右端
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
3. 冒泡排序
void bubleSort(int arr[], int n)
{
int i, j, flag;
int temp;
//扫描的范围为 n-1至1
for (i = n - 1; i >= 1; --i)
{
flag = 0;
//j从1开始是因为0没有左边一个元素无法进行比较
for (j = 1; j <= i; ++j)
{
if (arr[j - 1] >> arr[j])
{
temp = arr[j];
arr[j] = arr[j - 1];
arr[j - 1] = temp;
//当j和j左边的关键字发生交换,就将flag的关键字置为1
flag = 1;
}
//如果扫描完一遍发现flag值为0,说明没有发生交换,表明排序已经完成
if (flag == 0)
return;
}
}
}
8.2 希尔排序
直接插入排序的改进
不能保证每趟排序至少能将一个关键字放在最终位置上
void shellSort(int arr[], int n)
{
int temp;
for (int gap = n / 2; gap > 0; gap /= 2)
{
//gap之前的关键字都是其所在子序列的关键字,gap是无序序列的第一个关键字,gap之后的关键字落在无序序列
//gap==1时,就是直接插入排序
for (int i = gap; i < n; ++i)
{
temp = arr[i];
int j;
for (j = i; j > gap&&arr[j - gap] > temp; j -= gap)
arr[j] = arr[j - gap];
arr[j] = temp;
}
}
}
8.3 快速排序
类比于线性表的划分
执行完一次快速排序以后,枢轴左边的都比枢轴小,枢轴右边的都比枢轴大x
序列越无序,效率越高
//arr[]用来存放关键字,low-high表示处理子序列的范围
void quickSort(int arr[], int low, int high)
{
int temp;
int i = low;
int j = high;
//子序列长度大于1的时候进行划分
if (low < high)
{
temp = arr[low];
while (i < j)
{
while (j > i&&arr[j] >= temp)
--j;
if (i < j)
{
arr[i] == arr[j];
++i;
}
while (i < j&&arr[i] < temp)
++i;
if (i < j)
{
arr[j] = arr[i];
--j;
}
}
arr[i] = temp;
quickSort(arr, low, i - 1);
quickSort(arr, i + 1, high);
}
}
过程:
- 初始i,j指向序列的两端,temp保存枢轴关键字
- j 先移动,一边左移一边找比枢轴小的关键字,找到就将其复制到 i 所指的位置上,i 向右移动一位
- i 从左往右扫描,找第一个大于枢轴的关键字,找到就将其复制到 j 所指的位置, j 向左移动一位
- 重复以上操作直至 i>=j 结束
- 将枢轴元素赋值给i 指向的元素
8.4 堆排序
简单选择排序的改进
-
堆的逻辑结构
- 逻辑上属于完全二叉树
- 大顶堆:对于树中任意结点的关键字的值都不小于其孩子结点的关键字的值
- 小顶堆:对于树中任意结点的关键字的值都不大于其孩子结点的关键字的值
-
堆的存储结构
- 父结点位置为i
- 左孩子结点位置为2i+1
- 右孩子结点位置为2i+2
- 从0开始编号,共有n个结点,最后一个非叶结点编号为
-
建大顶堆(把较大的换上去,较小的换下来)
- 首先找出完全二叉树的最后一个非叶结点p( (n/2)向下取整)-1
- 观察p所指的结点以及其孩子结点的关键字的值,如果p的关键字的值比起孩子结点a中最大的的关键字的值要小,则交换p与a
- 如果不大,则p-1
-
插入结点
-
删除结点
- 把要删除的结点取出
- 将堆中最后一个结点a取出放在被删除的位置
- 然后调整a的位置使其恢复大顶堆
//参数列表:要调整的数组,需要操作的范围
void sift(int arr[], int low, int high)
{
//i是父节点,j是父节点的左孩子
int i = low, j = 2 * i + 1;
//用temp来暂存我们要调整的关键字的值
int temp = arr[i];
while (j <= high)
{
//j < high 反映了i有左右孩子,左孩子的值要小于右孩子的值,j后移一位
//目标是使关键字有左右孩子时,让j指向值较大的那个孩子
if (j < high&&arr[j] < arr[j + 1])
++j;
if (temp < arr[j])
{
arr[i] = arr[j];
//i和j都下移一位
i = j;
j = 2 * i + 1;
}
else
break;
}
arr[i] = temp;
}
void heapSort(int arr[], int n)
{
int i;
int temp;
//从最后一个非叶节点开始向上进行
for (i = n / 2 - 1; i >= 0; --i)
//调整范围为从最后一个非叶结点到最后一个结点
sift(arr, i, n - 1);
for (i = n - 1; i > 0; --i)
{
temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
sift(arr, 0, i - 1);
}
}
8.5 归并排序
把所有长度为1 的有序序列两两一组然后逐渐归并成和原序列长度相同的一个有序序列的过程
//可以实现在任意子序列进行归并操作
//规定从low到mid为一个带归并的有序子表,从mid+1到high之间的关键字为带归并的另外一个有序子表
void merge(int arr[], int low, int mid, int high)
{
int i, j, k;
//n1和n2使带归并的两个子表的长度
//从low到mid的关键字的个数
int n1 = mid - low + 1;
//从mid+1到high的关键字的个数
int n2 = high - mid;
//把划分的子表暂存在两个数组中
int L[n1], R[n2];
for (i = 0; i < n1; i++)
L[i] = arr[low + i];
for (j = 0; j < n2; j++)
R[j] = arr[mid + 1 + j];
i = 0;
j = 0;
k = low;
while (i < n1&&j < n2)
{
//取L[i]和R[j]中较小的值赋值给arr[k]
if (L[i] <= R[j])
arr[k] = L[i++];
else
arr[k] = R[j++];
k++;
}
//如果在L[i]和R[j]其中有一个遍历结束,则将剩下的值赋给arr
while (i < n1)
arr[k++] = L[i++];
while (j < n2)
arr[k++] = R[j++];
}
void mergeSort(int arr[], int low, int high)
{
if (low < high)
{
int mid = (low + high) / 2;
mergeSort(arr, low, mid);
mergeSort(arr, mid + 1, high);
merge(arr, low, mid, high);
}
}
8.6 基数排序
通过不停的分配和收集来实现排序
从下往上出关键字
先出来的关键字在左,后出来的关键字在右
不需要进行关键字的比较
排序算法 | 时间复杂度 |
---|---|
起泡排序 | n^2 |
快速排序 | nlog2n |
堆排序 | nlog2n |
简单选择排序 | n^2 |
归并排序 | nlog2n |
8.7 稳定性分析
-
稳定性:如果某个算法对某个序列进行排序后,这个序列中所有相同的关键字的次序能够保持不变
排序后这些相同关键字的相对顺序和初始序列中顺序是一样的
这样的算法是稳定的排序算法
1. 起泡排序
if(arr[j+1]>arr[j]) //交换
是稳定的
2. 简单选择排序
是不确定的
- 基于关键字交换的简单选择排序
if(arr[k]>arr[j])
k=j;
是不稳定的
使用于顺序存储结构
-
基于关键字插入的简单选择排序
适用于链表存储结构,不会造成大量关键字的移动
if(p->data>q->data)
p=q;
是稳定的
3.直接插入排序
是稳定的
4. 快速排序
while(j>i&&arr[j]>=temp)
--j;
if(i<j)
{
arr[i]=arr[j];
++i;
}
while(i<j&& arr[i]<temp)
++i;
if(i<j)
{
arr[j]=arr[i];
--j;
}
是不稳定的
5. 希尔排序
是不稳定的
6. 归并排序
while(i<n1&&j<n2)
{
if(L[i]<=R[j])
arr[k]=L[i++];
else
arr[k]=R[j++];
k++;
}
是稳定的
7. 堆排序
if(j<high&&arr[j]<arr[j+1])
++j;
if(temp<arr[j])
{
arr[i]=arr[j];
i=j;
j=2*i+1;
}
是不稳定的
8. 基数排序
是稳定的
9. 总结
排序算法 | 稳定性 |
---|---|
起泡排序 | 稳定 |
简单选择排序 | 不确定 |
直接插入排序 | 稳定 |
快速排序 | 不稳定 |
希尔排序 | 不稳定 |
归并排序 | 稳定 |
堆排序 | 不稳定 |
基数排序 | 稳定 |
“考研痛苦,快些选一堆好友来聊天吧”
“快”指快速排序
“些”指希尔排序
“选”指基于关键字交换的简单选择排序简单选择排序
“堆”指堆排序
这四种排序是不稳定的
8.8 时间复杂度
排序算法 | 时间复杂度 | 时间复杂度最好 | 最差 | 空间复杂度 |
---|---|---|---|---|
直接插入排序 | O(n^2) | O(n) | O(n^2) | O(1) |
折半插入排序 | O(n^2) | O(nlog2n) | O(n^2) | O(1) |
简单选择排序 | O(n^2) | O(1) | ||
冒泡排序 | O(n^2) | O(n) | O(n^2) | O(1) |
希尔排序 | O(n^2) | O(1) | ||
快速排序 | O(nlog2n) | O(nlog2n) | O(n^2) | O(log2n) |
堆排序 | O(nlog2n) | O(1) | ||
归并排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(n) |
基数排序 | O(d(n+rd)) | O(rd) |
- 基数排序:
- n:序列中的关键字数
- d:关键字的关键字位数
- rd:关键字基的个数,自然数是10,英文字符是26
以上排序可以称之为内部排序
8.9 外部排序(多路归并排序)
-
在关键字规模远大于内存所能容纳的规模时候,可以利用外部排序
-
最耗费事件的操纵:IO操作,与初始归并段的长度有关,初始归并段越长,需要归并的次数越少,每一个关键字需要执行的IO操作次数越少
-
文件有m个初始归并段,采用k路归并的时,所需要的归并趟数是 (logkm)向下取整
1. 置换-选择排序
产生的是长度不等的初始归并段
2. 最佳归并树
构造哈夫曼树实现
3. 败者树
1). 是一种数据结构
2). 建树
9.1 顺序查找
可以无视序列的有序性,序列有序或者无序都可以进行查找
记录:数据的组织方式
关键字:唯一区分一个记录的标记
1.ASL(平均查找长度)
-
描述查找的效率,分为查找成功的平均查找长度和查找失败的平均查找长度
-
查找成功ASL:
pi=1/n ASL1=c1*p1+......ci*pi+......cn*pn
-
查找失败ASL
ASL2=n
//作用于顺序表 int Search(int arr[], int n, int key) { int i; for (i = 0; i < n; ++i) if (arr[i] = key) return 1; //查找失败 return -1; } //作用于链表 typedef struct LNode { int data; struct LNode *next; }LNode; LNode* Search(LNode* head, int key) { LNode* p = head->next; while (p != NULL) { if (p->data == key) return p; p = p->next; } //查找失败 return NULL; }
9.2 折半查找
对于一个有序序列可以进行折半查找
mid=((low+high)向下取整)
//参数列表(数组,查找范围,查找关键字)
int BSearch(int arr[], int low, int high, int key)
{
while (low <= high)
{
int mid = (low + high) / 2;
if (arr[mid] == key)
return mid;
else if (arr[mid] > key)
high = mid - 1;
else
low = mid + 1;
}
//查找失败
return -1;
}
9.3 分块查找(索引顺序查找)
具有一定的有序性
块内无序,快间有序
typedef struct
{
int maxKey; //块中挑选出的最大的关键字
int low, high; //结构体变量描述的关键字的范围
}indexElem;
//定义一个索引元素数组
indexElem index[5];
//定义了一个关键字数组
int keys[15];
//在索引表(由索引元素构成的表)上以折半查找,在块内进行顺序查找
9.4 二叉排序树
1. 排序二叉树
-
树中的结点是关键字
-
对于树中每一个结点
- 如果该结点有左子树,则左子树上的关键字全都小于该结点的关键字值
- 如果该结点有右子树,则右子树上的关键字值全都大于该结点关键字值
二叉排序树或者空树,或者满足以下性质的二叉树
- 若它的左子树不空,则左子树上所有关键字的值均小于根关键字的值
- 若它的右子树不空,则右子树上所有关键字的值均大于根关键字的值
- 左右子树又各是一颗二叉排序树
2. 查找关键字
-
p从根节点开始,将p所指的关键字值与待查找的关键字的值进行对比,相等则表明已经找到
-
如果不等
- 若 待查找的关键字的值<p 所指的关键字值,则让p沿着左分支走一步
- 若 待查找的关键字的值>p 所指的关键字值,则让p沿着右分支走一步
-
继续重复移动,直到p来到空指针,表明查找失败,即表明待查找的关键字不在二叉排序树中
typedef struct BTNode { int key; BTNode *lChild; BTNode *rChild; }BTNode;
BTNode* BSTSearch(BTNode* p, int key)
{
while (p != NULL)
{
if (key == p->key)
//查找成功
return p;
else if (key < p->key)
p = p->lChild;
else
p = p->rChild;
}
//查找失败
return NULL;
}
//递归查找
BTNode* BSTSearch2(BTNode * p, int key)
{
if (p == NULL)
return NULL;
else
{
if (p->key == key)
return p;
else if (key < p->key)
//先BSTSearch2(p->lChild, key)再return
return BSTSearch2(p->lChild, key);
else
return BSTSearch2(p->rChild, key);
}
}
## 3. 插入和删除
~~~c
//插入
//参数列表(*&p指针引用型)
int BSTInsert(BTNode *&p, int key)
{
//如果p为空,表明找到了待插入的位置
if (p == NULL)
{
p = (BTNode*)malloc(sizeof(BTNode)); //申请结点空间进行插入操作
p->lChild = p->rChild = NULL;
p->key = key;
//查找成功标志
return 1;
}
else
{
//如果待插入元素已经在二叉排序树中,则没必要插入,记录插入失败
if (key == p->key)
//插入失败标志
return 0;
else if (key < p->key)
return BSTInsert(p->lChild, key);
else
return BSTInsert(p->rChild, key);
}
}
删除关键字三种情况
-
p结点为叶子结点直接删除
-
p结点只有右子树而无左子树,或者只有左子树没有右子树
只需要将p删除,然后将p的子树直接连接在原来p与其双亲结点f相连的指针即可
-
p结点有左右子树
-
沿着p的左子树根结点的右指针一直向右走,直到来到其右子树的最后一个结点r(或者沿着p的右子树根结点的左指针一直向左走,直到来到其左子树的最左边的一个结点)
-
然后将p中关键字用r中关键字替代
-
最后判断,如果r是叶子节点,直接删除
如果r是非叶子结点,按照2中方法删除r
9.5 平衡二叉树(AVL树)
1. 基础定义
-
二叉排序树越矮,查找效率越高,所以查找效率最高的是平衡二叉树
-
是一颗二叉排序树
-
平衡因子BF:左子树高度-右子树高度
-
平衡二叉树中各个结点的平衡因子绝对值不大于1
2. 平衡调整
- LL(Left-Left)调整/右单旋转调整:在左孩子的左子树插入结点导致不平衡的调整
- RR(Right-Right)调整/左单旋转调整:在右孩子的右子树上插入结点导致不平衡的调整
- LR(Left-Right)调整/先左后右双旋转调整:在左孩子的右子树上插入结点导致不平衡的调整
- RL(Right-Left)调整/先右后左双旋转调整:在右孩子的左子树上插入结点导致不平衡的调整
平衡调整过程
-
先找到平衡因子更改的结点
-
从找到的结点中挑选一个中间结点作为根结点,重新构造一颗二叉排序树
-
调整
-
设Nh表示高度为h的平衡二叉树中含有的最少结点数,则有
N0=0;
N1=1;
N2=2;
…
Nh=Nh-1+Nh-2+1
-
当每个非叶结点的平衡因子均为0,即每个非终端结点都有左右子树,且高度相等,这样的平衡二叉树被称为满二叉树,而高度为k的满二叉树的结点为 2^k-1
9.6 B树和B+树
1. B树
-
是平衡二叉树的扩展
-
m阶B树每个结点最多有m个分支(子树);
最少分支数要看是否为根结点,如果是根结点且不是叶子结点则至少有2个分支
非根非叶结点至少有((m/2)向上取整)个分支
-
有n(k<=n<=m)个分支的结点有n-1个关键字,它们按递增序列排序
k=2(根结点)
((m/2)向上取整)(非根结点)
具有n个关键字的结点含有n+1个分支
-
结点内的关键字互不相等
-
叶结点处于同一层
可以用空指针表示,是查找失败到达的位置
-
B树的阶数是人为规定的,不会应为结点中关键字的个数的改变而改变
2. 查找和插入关键字
结点结构体
n | Key1 | … | Keyn-1 | Keyn |
---|---|---|---|---|
p0 | p1 | … | pn-1 | pn |
n:关键字的个数
p0:指向当前结点的孩子结点的指针
- pi所指向的结点中所有关键字的值小于Keyi+1,大于Keyi
从一颗空树开始逐个插入关键字的操作就是建树
3. 删除关键字
4. B+树
-
在B+树中,具有n个关键字的结点含有n个分支,而在B树种,具有n个关键字的结点含有n+1个分支
-
在B+树中,每个结点(根节点除外)中的关键字个数n的取值范围为((m/2)向上取整)<=n<=m
而在B树中,它们的气质范围分别是((m/2)向上取整)<=n<=m-1和1<=1<=m-1
-
B+树是一个关键字对应一个分支
B树是一个关键字的空位置对应一个分支
-
在B+树中叶子结点包含信息,并且包含了全部关键字,叶子结点引出的指针指向记录
-
在B+树中有一个指针指向关键字最小的叶子结点,所有叶子结点链接成一个线性链表,而B树没有
也就是说B+树可以进行顺序查找
9.7 散列表(Hash table)
Hash查找法的时间复杂度为 O(1)
根据关键字的值来计算出关键字在表中的地址
哈希函数:ad=H(key)
冲突处理
Hi(key)=(H(key)+i)Mod len
i属于1-len-1,len为表长
计算查找不成功的平均查找长度:查找不成功的位置是Hash函数所能映射到的位置,而不是表中所有位置
1. 常见的Hash函数
1. 直接定址
取关键字或者关键字的某个线性函数为Hash地址,即:
H(key)=key
或者
H(key)=a*key+b //其中a和b为常数
2. 数字分析
适用于关键字位数比较多(可以有目的性的选择一些产生冲突少的段为地址)且表中可能的关键字都是已知的情况
分析的原则是尽量取冲突较少的位数段
3.平方取中
取关键字平方后的中间几位作为Hash地址。
一个数平方后的中间几位数和数的每一位都有关,由此得到的Hash地址随机性更大,不容易产生冲突
但是要求关键字较少
4. 除留余数
取关键字被某个不大于Hash表表长m的数p除后所得的余数为Hash地址,即
H(key)=key Mod p (p<=len)
在本方法中,p的选择很重要,一般p选择小于或者等于表长的最大素数,这样可以减少冲突
2. 常见冲突处理方法
冲突是不可避免的
1. 开放定址
- 线性探测法:
Hi(key)=(H(key)+i)Mod len
i属于1-len-1,len为表长
特点:可以探测到表中所有位置但是易产生堆积问题
若有k个关键字互为同义词,若有线性探测法把k个关键字存入Hash表,至少要进行k(k+1)/2次检测
- 平方探测法
设发生冲突的地址为d,则探测的新地址为:
d+1^2、d-1^2、d+2^2、d-2^2、d+3^2、d-3^2...
特点:不可以探测到表中所有位置(至少可以探测到一半位置)但是不易产生堆积问题
2. 链地址法
-
链地址法可以避免堆积问题
-
堆积现象不能被完全避免,因为在有些情况下,无法使用链地址法
-
增大装填因子,空闲位置变少,更容易发生冲突
装填因子a=n/len n是关键字个数,len是表长
10.1 线性结构算法分析
1. 顺序存储结构
-
线性顺序表访问元素的时间复杂度为 O(1):因为顺序表支持随机访问
-
线性顺序表查找元素的时间复杂度为O(n)
-
线性顺序表删除元素的时间复杂度为O(n)
特殊情况下在表尾删除元素,不需要移动其他元素位置,时间复杂度为O(1)
-
线性顺序表插入元素的时间复杂度为O(n)
特殊情况下在表尾插入元素,不需要移动其他元素位置,时间复杂度为O(1)
线性结构 | 访问 | 查找 | 删除 | 插入 |
---|---|---|---|---|
顺序存储结构 | O(1) | O(n) | O(n) | O(n) |
链式存储结构 | O(n) | O(n) | O(1) | O(1) |
折半查找 | O(logn)/O(log2n) |
2. 链式存储结构
-
线性表链式存储结构访问元素的时间复杂度为O(n)
-
线性表链式存储结构顺序查找元素(顺序:从左到右扫描整个链表)的时间复杂度为O(n)
-
线性表链式存储结构插入元素(插入元素位置已知,不需要进行访问)的时间复杂度为O(1)
-
线性表链式存储结构删除元素(删除元素位置已知,不需要进行访问)的时间复杂度为O(1)
3. 折半查找
int BSearch(int arr[],int low, int high,int key)
{
while(low<=high)
{
int mid=(low+high)/2;
if(arr[mid]==key)
return mid;
else if(arr[mid]>key)
high=mid-1;
else
low=mid+1;
}
return -1;
}
T(n)=T(n/2)+c
折半查找时间复杂度O(logn) O(log2n)
10.2 非线性结构算法分析
非线性结构 | 时间复杂度 |
---|---|
二叉树递归遍历 | O(n) |
二叉树非递归遍历 | O(n) |
二叉树层次遍历 | O(n) |
图用邻接矩阵存储,遍历 | O(n^2) |
图用邻接表存储,遍历 | O(n+e) |
普里姆算法 | O(n^2) |
克鲁斯卡尔算法 | O(eloge) |
迪杰斯特拉算法 | O(n^2) |
最短路径(弗洛依德)算法 | O(n^3) |
1. 二叉树递归遍历相关时间复杂度
void r(BTNode * p)
{
if(p!=NULL)
{
visit(p);
r(p->lchild);
r(p->rchild);
}
}
T(n)=T(x)+T(y)+c
T(n)=T(n-1)+1c
二叉树递归遍历的时间复杂度为O(n)
2. 二叉树先序遍历非递归
void preorderNonrecursion(BTNode * bt)
{
if(bt!=NULL)
{
BTNode *Stack[maxSize];
int top=-1;
BTNode *p;
Stack[++top]=bt;
while(top!=-1)
{
p=Stack[top--];
Visit(p); //关键代码段
if(p->rChild!=NULL)
Stack[++top]=p->rChild;
if(p->lChild!=NULL)
Stack[++top]=p->lChild;
}
}
}
二叉树非递归遍历的时间复杂度为O(n)
3. 二叉树的层次遍历代码
void level(BTNode* p)
{
if(bt!=NULL)
{
int front,rear;
BTNode *que[maxSize];
front=rear=0;
BTNode *p;
rear=(rear+1)%maxSize;
que[rear]=bt;
while(front!=rear)
{
front =(front +1)%maxSize;
p=que[front];
Visit(p); //选择
if(p->lChild!=NULL)
{
rear=(rear+1)%maxSize;
que[rear]=p->lChild;
}
if(p->rChild!=NULL)
{
rear=(rear+1)%maxSize;
que[rear]=p->rChild;
}
}
}
}
二叉树层次遍历时间复杂度为O(n)
4. 图
- 图用邻接矩阵存储,遍历时间复杂度为O(n^2)
- 图用邻接表存储,遍历时间复杂度为O(n+e) (扫描顶点一次,顺着顶点扫描链表一次)
- 普里姆算法时间复杂度为O(n^2)
- 克鲁斯卡尔算法时间复杂度为O(eloge)
- 迪杰斯特拉算法时间复杂度为O(n^2)
- 最短路径(弗洛依德)算法时间复杂度为O(n^3)
10.3 汉诺塔问题算法分析
//参数列表(n:有n个圆盘需要搬运,char a,char b,char c表示三个圆盘的编号,表示盘子从a经过b调整移动到c)
void hanoi(int n,char a,char b,char c)
{
if(n==1)
{
cout<<a<<"->"<<c<<cout;
}
else
{
//把n-1个盘子从a经过辅助空间c搬运到b上
hanoi(n-1,a,c,b);
//把a中剩下的最后一个盘子直接搬运到c上并打印出
cout<<a<<"->"<<c<<endl;
hanoi(n-1,b,a,c);
}
}
T(n)=2T(n-1)+c
T(1)=b
汉诺塔问题的时间复杂度为O(2^n)
10.4 排序算法时间复杂度分析
排序算法 | 最好情况时间复杂度 | 最坏情况时间复杂度 | 平均情况时间复杂度 |
---|---|---|---|
直接插入排序 | O(n) | O(n^2) | O(n^2) |
简单选择排序 | O(n^2) | O(n^2) | O(n^2) |
气泡排序 | O(n) | O(n^2) | O(n^2) |
快速排序 | O(nlogn) | O(n^2) | O(nlogn) |
二路归并排序 | O(nlogn) | O(nlogn) | O(nlogn) |
希尔排序 | O(n2)/O(n1.5) | O(n2)/O(n1.5) | O(n2)/O(n1.5) |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) |
基数排序 | O(d(n+rd)) | O(d(n+rd)) | O(d(n+rd)) |
1. 简单排序算法时间复杂度分析
1. 直接插入排序
void insertSort(int arr[],int n)
{
int temp,i,j;
for(i=1;i<n;++i)
{
temp=arr[i];
j=i-1;
while(j>=0&&temp<arr[j])
{
arr[j+1]=arr[j];
--j;
}
arr[j+1]=temp;
}
}
- 最好情况外层循环执行n-1 次,内存循环不执行,因此直接插入排序最好的情况时间复杂度为 O(n)
- 最坏情况下,内层执行i次,因此直接插入排序最坏的情况时间复杂度为 O(n^2)
2. 简单选择排序
void selectSort(int arr[],int n)
{
int i,j,k;
int temp;
for(i=0;i<n;++i)
{
k=i;
for(j=i+1;j<n;++j)
if(arr[k]>arr[j])
k=j;
temp=arr[i];
arr[i]=arr[k];
arr[k]=temp;
}
}
-
无论好坏的情况下,内层循环执行 i次,外层循环执行 n次
因此时间复杂度为 O(n^2)
3. 气泡排序
- 最好情况即有序序列,时间复杂度为O(n)
- 最坏情况即逆序序列,时间复杂度为O(n^2)
- 平均时间复杂度为O(n^2)
2. 复杂排序算法时间复杂度分析
1. 快速排序
//arr[]用来存放关键字,low-high表示处理子序列的范围
void quickSort(int arr[], int low, int high)
{
int temp;
int i = low;
int j = high;
//子序列长度大于1的时候进行划分
if (low < high)
{
temp = arr[low];
while (i < j)
{
while (j > i&&arr[j] >= temp)
--j;
if (i < j)
{
arr[i] == arr[j];
++i;
}
while (i < j&&arr[i] < temp)
++i;
if (i < j)
{
arr[j] = arr[i];
--j;
}
}
arr[i] = temp;
quickSort(arr, low, i - 1); //规模为i
quickSort(arr, i + 1, high); //规模为n-i-1
}
}
-
内外循环一共执行n次
-
T(0)=T(I)=c
T(n)=T(i)+T(n-i-1)+cn
-
最好情况下时间复杂度为O(nlogn)
-
最坏情况下时间复杂度为O(n^2)
-
平均情况下时间复杂度为O(nlogn)
2. 二路归并排序
void mergeSort(int arr[], int low, int high)
{
if (low < high)
{
int mid = (low + high) / 2;
mergeSort(arr, low, mid);
mergeSort(arr, mid + 1, high);
merge(arr, low, mid, high);
}
}
-
归并排序最好最坏和平均情况下时间复杂度都是O(nlogn)
-
空间复杂度为O(n)
3. 希尔排序
- 每次将增量除以2向下取整,其中n为序列长度,此时时间复杂度为O(n^2)
- 其中k为大于等于1的整数,2k+1小于待排序序列,增量序列末位的1是额外添加的,此时时间复杂度为O(n1.5)
- 希尔排序是直接插入排序的升级,所以空间复杂度为O(1)
4. 堆排序
void heapSort(int arr[], int n)
{
int i;
int temp;
//从最后一个非叶节点开始向上进行
for (i = n / 2 - 1; i >= 0; --i)
//调整范围为从最后一个非叶结点到最后一个结点
sift(arr, i, n - 1);
for (i = n - 1; i > 0; --i)
{
temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
sift(arr, 0, i - 1);
}
}
-
对一个有n个结点的堆,一轮调整的次数最多为logn
-
堆排序时间复杂度最好最坏以及平均情况下都是O(nlogn)
-
空间复杂度为O(1)
-
对于建树过程,
//从最后一个非叶节点开始向上进行 for (i = n / 2 - 1; i >= 0; --i) //调整范围为从最后一个非叶结点到最后一个结点 sift(arr, i, n - 1);
T(n)=2T(n/2)+logn
即建堆的时间复杂度为O(n)
5. 基数排序
-
时间复杂度最好、最坏、平均都是: O(d(n+rd))
-
空间复杂度最好、最坏、平均都是:O(rd)
-
n:序列中的关键字数
-
d:关键字的关键字位数
-
rd:关键字基的个数,自然数是10,英文字符是26
-
3. 算法空间复杂度分析
空间复杂度:是对一个算法在运行过程中临时占用存储空间大小的度量
排序算法 | 空间复杂度 |
---|---|
直接插入排序 | O(1) |
简单选择排序 | O(1) |
气泡排序 | O(1) |
快速排序 | O(log2n) |
二路归并排序 | O(n) |
希尔排序 | O(1) |
堆排序 | O(1) |
基数排序 | O(rd) |
简单排序算法空间复杂度都是O(1),都是原地算法