文章目录
数据结构笔记整理
绪论
基本概念和术语
数据:能输入到计算机中并被计算机处理的符号的总称;
数据元素:数据的基本单位;
数据项:组成数据元素的、有独立含义的、不可分割的最小单位;
数据对象:性质相同的数据元素的集合;
数据结构:相互之间存在一种或多种特定关系的数据元素的集合;
数据类型:是一个值的集合和定义在这个值集上的一组操作的总称;
抽象数据类型:一般指由用户定义、表示应用问题的数据模型,以及定义在这个模型上的一组操作的总称;
(具体包括三部分:数据对象、数据对象上关系的集合以及对数据对象的基本操作的集合)
算法的定义及特性
定义
为了解决某类问题而规定的一个有限长的操作序列;
特性
1.有穷性
2.确定性
3.可行性
4.输入
5.输出;
评价算法优劣的标准
1.正确性
2.可读性
3.健壮性
4.高效性(包括时间和空间两个方面)
算法的时间复杂度
一个算法的执行时间大致上等于其所有语句执行时间总和,而语句的执行时间则为该条语句的重复执行次数和执行一次所需要时间的乘积;
问题规模是算法求解问题输入量的多少,一般用整数n表示;
语句频度一条语句重复执行的次数;
时间复杂度定义:随问题规模n的增大,算法执行时间的增长率称为时间复杂度;
最好时间复杂度:算法计算量可能达到的最小值;
最坏时间复杂度:算法计算量可能达到的最大值;
平均时间复杂度:算法在所有可能的情况下,按照输入实例以等概率出现时,算法计算量的加权平均值;
1.常量阶O(1)
int i=0,s=0;
for(i=0;i<100;i++){
s++; //语句频度为常数,无论怎么大,仍然是常数
}
2.线性阶O(n)
for(i=0;i<n;i++){
s++; //语句频度为n
}
3.对数阶O(log(2)n)
for(int i=1;i<n;i++){
i=i*2;
}
//2^n=x
//x=log(2)n
算法的空间复杂度
分析算法在实现是所需要的辅助空间;
int fac(int n){
if (n == 1)
return 1;
else
return n * fac(n - 1);
}
线性表
线性表的定义和特点
定义:由n个数据特性相同的元素构成的有限序列称为线性表;
特点:>存在唯一一个“第一个”数据元素;
存在唯一一个“最后一个”数据元素;
除上面两个外,每个元素有且只有一个前驱和一个后继;
线性表的顺序存储表示
定义:用一组地址连续的存储单元依次存储线性表的数据元素,这种存储结构的线性表称为顺序表;
特点:逻辑上相邻,物理次序也相邻;
顺序表中基本操作的实现
1.数据类型定义
#define MAXSIZE 100
typedef int ElemType; //定义数据元素的类型
typedef struct{
ElemType *elem; //存储空间基址
int length;
}Sqlist;
2.初始化
void InitList(SqList &L){
L.elem=new ElemType[MAXSIZE];
if(!L.elem) exit(0);//分配失败,退出
L.length=0;
}
3.取值
void GetElem(SqList L,int i,ElemType &e){
if(i<1||i>L.length) printf("Position Error!");
e=L.elem[i-1];
}
4.查找
int LocateElem(SqList L,Elemtype e){
for(i=0;i<L.length;i++){
if(L.elem[i]==e) return i+1;
}
return 0;
}
5.插入
void InsertList(Sqlist *L,int i,ElemType x){
int j;
if(i<1||i>L.length+1) printf("Position Error!");
if(L.length==MAXSIZE) printf("Position FULL!");
for(j=L->length-1;j>i-1;--j){
L->elem[j+1]=L->elem[j];
}
L->elem[i-1]=x;
++L->length;
}
6.删除
void DeleteList(Sqlist *L,int i){
int j;
if(i<=1||i>L->length){
printf("Position Error!");
}
for(j=L->length-1;j>i-1;--j){
L->elem[j-1]=L->elem[j];
}
--L->length;
}
性能分析:
1.预先分配空间;
2.存储密度=数据元素本身占用的存储量/结点结构占用的存储量;
3.查找效率高O(1);
4.插入删除效率低O(n);
PS.存储了第一个元素的位置通常称为线性表的起始位置或基地址,由此,知道了第一个元素的位置,任-数据元素都可随机存取,所以线性表的顺序存储结构是一种随机存取的数据结构;
线性表的链式存储表示
定义:用任意的存储单元存储线性表的数据元素,除了存储其本身的信息,还存储一个指示其后继的信息;
数据元素包括数据域和指针域;
单链表中基本操作的实现
1.数据类型定义
typedef int ElemType;
//定义链表类型
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode,*List;
2.初始化
void InitList(List &L){
L=new LNode;//生成新结点作为头结点,头指针L指向头结点
L->next=NULL;
}
3.输入、输出
void ListInput(List &L, int n) //链表数据的输入
{
int i=1;
List p, r;
r = L;
while (i<=n) {
p = new LNode;
cin >> p->data;
p->next = NULL;
r->next = p;
r = p;
i++;
}
}
void ListOutput(List L) //输出List
{
List p;
p = L->next;
cout << "The List is:"<<endl;
while (p != NULL) {
cout << p->data << " ";
p = p->next;
}
cout << endl;
}
4.查找
bool LocateElem(List L, int e){
List p;
p=L;
while(p){
if(p->data==e)
return true;
p=p->next;
}
return false;
}
5.插入
void InsertList(List &L,int i,ElemType e){
List p;
p=L;
int j;
while(p&&j<i-1){
p=p->next;
++j;
}
if(!p||j>i-1){
return ;
}
s=new LNode;
s->data=e;
s->next=p->next;
p->next=s;
}
6.删除
void DeleteList(List &L,int i,ElemType e){
List p;
p=L;
int j=0;
while(p->next&&j<i-1){
p=p->next;
++j;
}
if(!p||j>i-1){
return ;
}
q=new LNode;//临时保存地址结点,以备释放
q=p->next;
delete q;
}
性能分析:
1.元素个数没有限制;
2.存储密度=数据元素本身占用的存储量/结点结构占用的存储量;
3.查找效率低O(n);
4.插入删除效率高O(1);
PS.链表是非随机存取的存储结构,也称为顺序存取的存储结构;
首元结点、头结点、头指针概念说明;
首元结点:存储第一个数据元素的结点;
头结点:设在首元结点之前的一个结点,指针域指向首元结点;
头指针:指向链表中第一个结点的指针,有头结点指头结点,没有头结点,指首元结点;
PS.头结点作用:>便于首元结点的操作;
栈和队列
栈和队列是两种操作受限的线性表,可称为限定性的数据结构;
栈的定义和特点
栈是限定仅在表尾进行插入删除的线性表;
对栈来说,表尾端有其特殊含义,称为“栈顶(top)”,相应的,表头端为栈底(bottom);
不含任何元素的空表,称为“空栈”;
因此,栈又称为“后进先出”(Last In First Out);
栈的表示和操作实现
和线性表类似,栈有两种存储方法,分别称为顺序栈和链栈;
顺序栈
1.顺序栈的存储结构
#define MAXSIZE 100
typedef struct{
SElemType *base;//栈底指针
SElemType *top;//栈顶指针
int stacksize;//栈可用最大容量
}SqStack;
2.初始化
Status InitStack(SqStack &S){
S.base=new SElemType[MAXSIZE];//分配一个最大容量空间
if(!S.base) exit(OVERFLOW);//存储分配失败
S.top=S.base;
S.stacksize=MAXSIZE;
return OK;
}
3.入栈
Status Push(SqStack &S,SElemType e){
if(S.top-S.base==S.stacksize) return ERROR;//栈满
//*S.top++=e
*S.top=e;
S.top++;
return OK;
}
4.出栈
Status Pop(SqStack &S,SElemType &e){
if(S.top==S.base) return ERROR;//栈满
//e=*--S.top
--(S.top);
e=*S.top;
return OK;
}
5.取栈顶元素
SElemType GetTop(SqStack S){
if(S.top!=S.base)
return *(S.top-1);
}
链栈
通常链栈用单链表表示,只在链表头部进行操作;
以链表的头部作为栈顶较为方便,不需要附加一个头结点;
1.链栈的存储结构
typedef struct{
SElemType data;
struct StackNode *next;
}StackNode,*LinkStack;
2.初始化
Status InitStack(LinkStack &S){
S=NULL;
return OK;
}
3.入栈
Status Push(LinkStack &S,SElemType e){
StackNode *p;
p=new StackNode; // 生成新节点
p->data=e; // 将新节点数据域置为e
p->next=S; // 将新节点插入栈顶
S=p; // 修改栈顶指针
return OK;
}
4.出栈
Status Pop(LinkStack &S,SElemType &e){
if(S==NULL) return ERROR;
e=S->data;
StackNode *p;
p=S; //临时保存栈顶元素空间,以备释放
S=S->next; //修改栈顶指针
delete p;
return OK;
}
5.取栈顶元素
SElemType GetTop(LinkStack S){
if(S!=NULL){
return S->data;
}
}
栈与递归
栈有一个重要的应用就是在程序设计语言中实现递归;
遍历输出链表各个结点的递归算法
void TraverseList(LinkList p){
if(p==NULL) return ;
else{
cout<<p->data<<endl;
TraverseList(p->next);
}
}
队列的定义和特点
和栈相反,队列是一种先进先出(First In First Out)的线性表;
允许插入的一头称队尾,允许删除的称队头;
在队列的顺序存储结构中,除了用一组地址连续的存储单元依次存放从队列头到队列尾的元素之外,还设置两个指针,分别指向队列头元素以及队列尾元素,称为头指针(front)和尾指针(rear);
#define MAXQSIZE
typedef struct
{
QElemType *base;//存储空间的基地址
int front;
int rear;
}SqQueue;
在C语言中约定,初始化创建空队列时,令front=rear=0;
每当插入新的队列尾元素时,尾指针rear+1;
每当删除队列头元素时,头指针front+1;;
在非空队列中,头指针始终指向队头元素,尾指针始终指向队尾元素的下一个位置;
但这种受限制的操作,往往会造成空间没有占满,指针溢出的“假溢出”现象;
为了解决这种假溢出问题,将顺序队列变成一个环状的空间,称为循环队列;
头指针、尾指针以及队列元素之间的关系不变,只是在循环队列中,头指针尾指针的操作用“模”运算实现;
对于循环队列,不能以头指针、尾指针的值是否相同来判断队满还是队空,通常有两种处理方法:
1.少用一个元素空间;
队空:Q.front==Q.rear;
队满:(Q.rear+1)%MAXQSIZE==Q.front;
2.另设一个标识以区分队满、队空;
循环队列的操作
//1.初始化
Status InitQueue(SqQueue &Q){
Q.base=new QElemType[MAXQSIZE];
if(!Q.base) exit(OVERFLOW);
Q.front=Q.rear=0;
return OK;
}
//2.求队列长度
int QueueLength(SqQueue Q){
return (Q.rear-Q.front+MAXQSIZE)%MAXQSIZE;
}
//3.入队
Status InsertQueue(SqQueue &Q,QElemType e)
{
if ((Q.rear + 1) % MAXQSIZE == Q.front) return ERROR; //队列已满时,不执行入队操作
Q.base[Q.rear] = e; //将元素放入队列尾部
Q.rear = (Q.rear + 1) % MAXQSIZE; //尾部元素指向下一个空间位置,取模运算保证了索引不越界(余数一定小于除数)
return OK;
}
//4.出队
Status DeleteQueue(SqQueue &Q,QElemType &e)
{
if (Q.front == Q.rear) return ERROR; //空队列,直接返回
e = Q.base[Q.front]; //头部元素出队
Q.front = (Q.front + 1) % MAXSIZE;
return OK;
}
//5.取队头元素
QElemType GetHead(SqQueue Q){
if(Q.front!=Q.rear)
return Q.base[Q.front];
}
队列的链式表示和实现
队列的链式存储结构
typedef struct QNode
{
QElemType data;
struct QNode *next;
}QNode,*QueuePtr;
typedef struct
{
QueuePtr front;//队头指针
struct rear;//队尾指针
}LinkQueue;
//1.初始化
Status InitQueue(LinkQueue &Q){
Q.front=Q.rear=new QNode;
Q.front->next=NULL;
return OK;
}
//3.入队
Status InsertQueue(LinkQueue &Q,QElemType e)
{
p=new QNode;
p->data=e;
p->next=NULL;
Q.rear->next=p;
return OK;
}
//4.出队
Status DeleteQueue(LinkQueue &Q,QElemType &e)
{
if (Q.front == Q.rear) return ERROR; //空队列,直接返回
p=new QNode;
p=Q.front->next;
e=p->data;
Q.front->next=p->next;
if(Q.rear==p)
Q.rear=Q.front;
delete p;
return OK;
}
//5.取队头元素
QElemType GetHead(LinkQueue Q){
if(Q.front!=Q.rear)
return Q.front->next->data;
}
串、数组、和广义表
串的定义
串(string)是由零个或多个字符构成的有限序列,一般记为s=“a1 a2 a3…an”;
其中,s是串名,a1 a2 …an是串的值;
串中字符的数目n,称为串的长度;
零个字符的串称为空串,其长度为0;
串中任意个连续的字符组成的子序列称为该串的子串,包含子串的串相应的称为主串;
通常,称字符在序列中的序号为该字符在串中的位置;
称两个串是相等的,也就是说两个串长度相等,并且各个对应位置的字符都相等;
串的存储结构
与线性表类似,串也有两种基本存储结构:顺序存储和链式存储;
但考虑到存储效率和算法的方便性,串多采用顺序存储结构;
//-----串的定长顺序存储-----
#define MAXLINE 255 //串的最大长度
typedef struct{
char ch[MAXLEN+1]; //存储串的一维数组
int length; //串当前的长度
}SString;
这是一种静态的定义方式,在编译时刻就确定了串空间的大小;
在C语言中,存在一个称之为“堆”的自由存储区域,可以为每个新产生的串动态分配一块实际串长所需的存储空间,若分配成功,返回一个指向起始地址的指针,作为串的基址;
//-----串的堆式顺序存储-----
typedef struct{
char *ch; //非空串,按串长分配存储区,否则ch为NULL
int length; //串当前的长度
}HString;
串的模式匹配算法
子串的定位运算通常称为串的模式匹配或串匹配;
串的模式匹配设有两个字符串S和T,S为主串,T为子串;
著名的模式匹配算法是BF算法和KMP算法;
1.BF算法
BF算法,即暴力(Brute Force)算法;
BF算法的思想是:
1.将目标串S的第一个字符与模式串T的第一个字符进行匹配;
2.若相等,则继续比较S的第二个字符和 T的第二个字符;若不相等,则比较S的第二个字符和T的第一个字符;
3.依次比较下去,直到得出最后的匹配结果;
算法效率:
若主串长度位m,子串长度位n,则:
最好情况平均时间复杂度:O(m+n)
最坏情况平均时间复杂度:O(m*n)
2.KMP算法
KMP算法的改进在于:每当一次匹配过程出现字符比较不相等时,不需要回溯指针到i-j+2的位置,而是利用已经得到的部分匹配的结果,将模式串尽可能地向右滑动一段距离,继续进行比较;
什么是前缀,除了最后一个字符的所有子串。
什么是后缀,除了第一个字符的所有子串。
然后我们给出一个公共前后缀的概念:就是指长度最大且相等的前缀和后缀;
假设模式串中绿色的地方是j前面的子串的公共前后缀,当在j位置出现不匹配时,将前缀移动到后缀的位置,继续进行匹配;
而这个滑动的过程,需要依赖模式串的next[j]函数,由此来引出模式串的next函数的赋值规则:
数组
数组是由类型相同的数据元素构成的有序集合,每个元素称为数组元素,每个元素受n(n>=1)个线性关系的约束,每个元素在n个线性关系中的序号称为该元素的下标,可以通过下标访问数据元素;
因为数组中每个元素处于n个关系中,故称该数组为n维数组;
数组可以看作是线性表的推广,其特点是结构中元素本身可以说具有某种结构的数据,但属于同一数据类型;
数组的顺序存储
数组一般不做插入删除操作,一旦建立数组,则结构中的数据元素个数和元素之间的关系不再发生变动;
由于存储单元是一维的结构,而数组可能是多维的结构,则用一组连续存储单元存放数组的数据元素就有次序约定问题;
对应的二维数组有两种存储方式:一种是以列序列为主序列的存储方式,一种是行序列为主序列的存储方式;
假设每个数据元素占L个存储单元,则二维数组中任一元素a(ij)的存储位置可由下式确定:
L
O
C
(
i
,
j
)
=
L
O
C
(
0
,
0
)
+
(
n
∗
i
+
j
)
L
LOC(i,j)=LOC(0,0)+(n*i+j)L
LOC(i,j)=LOC(0,0)+(n∗i+j)L
特殊矩阵的压缩存储
所谓压缩存储,是指为多个值相同的元只分配一个存储空间,对零元不分配空间;
1.对称矩阵
对于对称矩阵,可为每一对对称元分配一个存储空间,则可将n*n个元素压缩到n(n+1)/2个元的空间中;
2.三角矩阵
只存储下或(上)三角包括主对角线的数据元素;
下标:0…n*(n+1)/2-1;
如果是下三角矩阵,A[i] [j]在B中位置k=(i+1)*i/2+j;
反之,A[i] [j]在B中位置k=(j+1)*j/2+i;
3.对角矩阵
矩阵半带宽:主对角线上下方各有b条对角线;
矩阵带宽:2*b+1;
只存储带状区内的元素;
除了第一行和最后一行各有b+1个元素,其余各行均有2*b+1个元素;
4.稀疏矩阵
矩阵A中有s个非零元素,稀疏因子e=S/m*n《=0.05,则为稀疏矩阵;
存储方式:只记录非0元素(节约空间,丧失随机存取功能);
顺序存储:三元表
稀疏矩阵的转置转换为对应三元表的转置;
快速转置算法:
链式存储:十字链表