数据结构基础(数组、线性表、栈和队列)
基础知识
数据结构基本概念
1)数据
数据元素:数据基本单位,可由若干数据项组成;
数据对象:性质相同的数据元素的集合;
数据类型:包括原子类型,结构类型,抽象数据类型;
抽象数据类型(ADT):通常用(数据对象,数据关系,基本操作集)这样的三元组表示;
数据结构:包括逻辑结构、存储结构、数据的运算;
2)数据的逻辑结构(独立于计算机的,与存储结构无关)
包括:线性结构(线性表,栈,队列),非线性结构(树,图,集合);
3)数据的存储结构(物理结构,不独立于计算机)
顺序存储:逻辑上相邻的元素存储在物理位置上也相邻的存储单元里;
链式存储:用指示元素存储地址的指针表示元素之间的逻辑关系;
索引存储:建立附加索引表,索引项一般为(关键字,地址);
散列存储:即hash存储,由关键字直接计算出地址;
4)数据的运算:包括运算的定义与实现;
算法和算法评价
1)算法:对特定问题求解步骤的一种描述,它是指令的有限序列,其中每一条指令表示一个或多个操作;
2)五个特性:有穷性,确定性,可行性,输入,输出;
3)时间复杂度:设算法中所有语句的频度之和为T(n),算法中最深层循环内语句的频度为f(n),则T(n)=O(f(n));
频度:语句在算法中被重复执行的次数;T(n)是问题规模n的函数;
最好的时间复杂度,平均时间复杂度,最坏时间复杂度; => 一般考虑的是最坏时间复杂度;
O(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n)
4)空间复杂度:算法所耗费的存储空间,一般为除输入和程序之外的辅助空间;若算法原地工作,则辅助空间为常量O(1);
5)有序表:关键字有序的线性表,属于逻辑结构;
6)附加说明:
将两个长度分别为m和n的升序链表,合并为一个长度为m+n的降序链表,最坏情况下的时间复杂度为O(max{m,n}),因为比较次数为2*max{m,n}-1(插空时);
相同规模n下,复杂度为O(n)的算法在时间上总是优于复杂度为O(2^n)的算法;
所谓时间复杂度,是指最坏情况下,估算算法执行时间的一个上界;
同一个算法,实现语言的级别越高,执行效率就越低;
线性表
线性表是一种逻辑结构,顺序表和链表是存储结构
线性表的定义和基本操作
1)线性表:具有相同数据类型的n个数据元素的有限序列;L=(a1,a2,...,ai,ai+1,...,an);
除第一个元素外,每个元素有且仅有一个直接前驱,除最后一个元素外,每个元素有且仅有一个直接后继;
特征:有限个数据元素;逻辑结构;元素有先后次序(前驱后继关系);数据类型相同;
2)基本操作:
InitList(&L):初始化
Length(&L):求表长
LocateElem(L,e):按值查找
GetElem(L,e):按位查找
ListInsert(&L,i,e):插入
ListDelete(&L,i,&e):删除,用e返回删除元素的值
PrintList(L):输出
Empty(L):判空
DestroyList(&L):销毁
线性表的顺序表示
顺序表的定义
1)顺序表:线性表的顺序存储,特点是表中元素的逻辑顺序与其物理顺序相同
2)线性表的顺序存储类型描述
1.静态分配(数据大小、空间固定)
#define MaxSize 50 //定义线性表的最大长度 typedef struct { ElemType data[MaxSize]; //顺序表的元素 int length; //顺序表的当前长度 }SqList; //顺序表的类型定义
2.动态分配(动态分配依然是顺序存储结构,随机存取方式,只是分配的空间大小可以在运行时决定)
#define InitSize 100 //表长度的初始定义 typedef struct { ElemType *data; //动态分配数组的指针 int MaxSize,length; //定义数组的最大容量和当前个数 }SqList; //动态分配数组顺序表的类型定义C语言
C语言动态分配初始语句 :L.data=(ElemType*)malloc(sizeof(ElemType)*InitSize);
C++动态分配初始语句 :L.data=New ElemType(InitSize);
3)顺序表的特点:
随机存取,随机访问,即首地址+元素序号可以在O(1)时间内找到指定元素;
存储密度高,每个结点只存储数据元素;
逻辑上相邻,物理上也相邻,插入和删除需移动大量元素;
顺序表上基本操作的实现(插入,删除,查找)
1)插入操作
//将元素e插入到顺序表的第i个位置 bool ListInsert(SqList &L,int i, ElemType e) { if(i<1 || i>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; }
最好的情况:在表尾插入(i=n+1),则后移语句不执行,时间复杂度为O(1);
最坏的情况:在表头插入(i=1),则元素后移执行n次,时间复杂度为O(n);
平均情况:在第i个位置插入,元素后移执行n/2次,平均时间复杂度为O(n);
2)删除操作
//删除顺序表中第i个位置的元素 bool ListDelete(SqList &L,int i,ElemType 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; }
最好情况:删除表尾元素(i=n),无需前移元素,时间复杂度为O(1);
最坏情况:删除表头元素(i=1),前移元素n-1次,时间复杂度为O(n);
平均情况:删除第i个位置的元素,前移(n-1)/2次,平均时间复杂度为O(n);
3)按值查找(顺序查找)
//查找顺序表中值为e的元素,若查找成功,则返回位序,否则返回0; int LocateElem(SqList L, ElemType e){ for(int i=0;i<L.length;i++){ if(L.data[i]==e){ return i+1; } return 0; }
最好情况:查找元素在表头,仅需比较一次,时间复杂度为O(1);
最坏情况:查找元素在表尾(或不存在)时,需比较n次,时间复杂度为O(n);
平均情况:查找元素在第i个位置,需比较(n+1)/2次,平均时间复杂度为O(n);
线性表的链式表示
不需使用地址连续的存储单元,即不要求逻辑上相邻的两个元素在物理位置上也相邻,通过“链”建立起数据元素之间的逻辑关系;
单链表
1)定义:线性表的链式存储;每个链表节点,除了存放元素自身的信息外,还需要存放一个指向其后继的指针;
单链表中结点类型描述:
typedef struct LNode{ //定义单链表结点类型 ElemType data; //数据域 struct LNode *next; //指针域 }LNode,*LinkList;
优点:解决顺序表需要大量连续存储空间的缺点;
缺点:由于附加指针域,所以也浪费空间;
特点:非随机存储结构,查找特定结点时,需要从表头开始遍历,依次查找;
2)通常用"头指针"标识一个单链表L,头指针为"NULL"时表示一个空表;
头结点:为操作方便,单链表第一个结点之前附加一个结点,称为头结点,头结点的指针域指向第一个结点,数据域可以不设信息,也可记录表长等相关信息;
头结点&头指针:
1.不论是否有头结点,头指针始终指向链表的第一个结点;
2.头结点是带头结点链表的第一个结点,结点内通常不存储任何信息;
头结点的优点:
1.链表的第一个位置操作与其他位置一致;
2.头指针是指向头结点(或链表第一个结点)的非空指针(空表中头结点的指针域为空),所以空表非空表处理一致;
单链表基本操作的实现
1)采用头插法建立单链表
//从空表开始生成新结点 //将新结点插入到当前链表的表头,即头结点之后 LinkList CreateList1(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为头指针 L->next = s; scanf("%d",&x); } return L; }
特点: 读入数据的顺序与生成链表中元素顺序相反;设单链表长为n,则时间复杂度为O(n).
2)采用尾插法建立单链表
将新结点插入到当前链表的表尾上,需要增加一个尾指针r,使其始终指向当前链表的尾结点.(即最后一个结点为r,会更新)
LinkList CreateList2(LinkList &L){ int x; //元素类型为整型 L=(LinkList)malloc(sizeof(LNode)); //创建头结点 LNode *s,*r=L; scanf("%d",&x); //输入结点值 while(x != 9999) { //输入9999表示结束 s=(LNode*)malloc(sizeof(LNode)); //创建新结点 s->data = x; r->next = s; //将新结点插入表中,r为表尾指针 r = s->next; //r指向新的表尾结点 scanf("%d",&x); } r->next = NULL; //尾结点指针置空 return L; }
时间复杂度为O(n),单链表长为n的情况下
3)按序号查找结点值
//取出单链表L(带头结点)中第i个位置的结点指针(元素从第一个位置开始) LNode *GetElem(LinkList L,int i){ int j=1; //计数,初始为1 LNode *p=L->next; //将头结点指针赋给p if(i==0){ return L; //返回头结点 } if(i<0){ return NULL; //无效,返回NULL } while(p && j<i){ //从第1个结点开始查找第i个结点 p = p->next; j++; } return p; //返回第i个结点的指针 }
4)按值查找表结点
//查找单链表L(带头结点)中数据域值等于e的结点指针,否则返回NULL LNode *LocateElem(LinkList L, ElemType e){ LNode *p=L->next; //将头结点的指针赋给p while(p!=NULL && p->data != e){ //从第1个结点开始查找data域为e的结点 p = p->next; } return p; //指到后返回该结点指针,否则返回NULL }
5)插入结点操作
//将值为x的新结点插入到单链表的第i个位置上 p = GetElem(L,i-1); //查找前驱结点 s->next = p->next; p->next = s;
时间复杂度为O(n)(主要是GetElem(L,i-1)所花费的),若在固定结点后插入,时间复杂度为O(1).
//将*s插入到*p之前 s->next = p->next; //s插入到p之后 p->next = s; temp=p->data; //*s,*p交换数据域 p->data = s->data; s->data = temp; //时间复杂度为O(1)
6)删除结点操作
//将单链表的第i个结点删除 p=GetElem(L,i-1); //查找删除位置的前驱结点 q=p->next; //令q指向被删除结点 p->next = q->next; //将*q结点从链中断开 free(q); //释放结点的存储空间
时间复杂度为O(n)(主要用于GetElem(L,i-1));
//仅知p的后继结点,删除*p q=p->next; //删除后继 p->next = q->next; //将后继数据域赋给p free(q);
7)求表长操作
计算单链表中数据结点(不含头结点)的个数,时间复杂度为O(n)(设置计数器变量),单链表的长度是不包括头结点的;
双链表
1)单链表:只能从前往后顺序遍历,访问前驱的时间复杂度为O(n),访问后继为O(1);
2)双链表:有两个指针prior和next,分别指向其前驱结点和后继结点;
3)双链表中结点类型描述
typedef struct DNode{ //定义双链表结点类型 ElemType data; //数据域 struct DNode *prior,*next; //前驱和后继指针 }DNode,*DLinkList;
4)双链表的插入操作
//在*p所指结点之后插入结点*s,时间复杂度为O(1) s->next = p->next; p->next->prior = s; p->next = s; //前两行代码必须在该代码之前 s->prior = p;
5)双链表的删除操作
//删除双链表中结点*p的后继结点*q,时间复杂度为O(1) p->next = q->next; q->next->prior = p; free(q); //释放结点空间
循环链表
1)循环单链表:与单链表的区别在于,最后一个结点的指针不是NULL,而是指向头结点;
表尾结点*r的next域指向L,表中没有指针域为NULL的结点;
判空条件:头结点的指针是否等于头指针;
因为循环单链表是一个环,所以在任何位置上的插入和删除操作都等价,无需判断表尾;
单链表:只能从表头结点开始往后顺序遍历整个链表;循环单链表:可以从表中任一结点开始遍历整个链表;
仅设立尾指针的循环单链表,设r是尾指针,r->next即为头指针,对表头表尾操作都只需O(1)的时间复杂度;
3)循环双链表
表尾结点的next指向头结点,头结点的prior指向表尾结点;
循环双链表L中,当结点*p为尾结点时,p->next=L;当为空表时,L->prior=L, L->next=L;
静态链表
1)借助数组来描述线性表的链式存储结构,结点也有数据域data和指针域next。此处指针为结点的相对地址(数组下标),静态链表也要预先分配一块连续的存储空间;
2)静态链表结构类型描述
#define MaxSize 50 //静态链表的最大长度 typedef struct { //静态链表结构类型的定义 ElemType data; //存储数据元素 int next; //下一个元素的数组下标 }SLinkList[MaxSize];
静态链表以next=-1作为其结束的标志(设单链表使用方便)
顺序表和链表的比较
1)如何选取存储结构
存储考虑:当线性表长度或存储规模难以估计 =>链表;
运算考虑:按序号访问 =>顺序表;插入、删除=>链表;
环境考虑:顺序表基于数组,链表基于指针;
栈和队列
栈
基本概念
1)栈:只允许在一端进行插入或删除操作的线性表;
2)栈顶:线性表允许进行插入和删除的那一端;
3)栈底:固定的,不允许进行插入和删除的那一端;
4)空栈:不含任何元素的空表;
5)栈特点:后进先出;
6)基本操作:
InitStack(&S):初始化一个空栈S;
StackEmpty(S):判断栈是否为空,空返回true,不空返回false;
Push(&S,x):进栈;
Pop(&S,x):出栈
GetTop(S,&x):读栈顶元素
ClearStack(&S):销毁栈,并释放栈S占用的存储空间;
栈的顺序存储结构(顺序栈)
1)顺序栈:栈的顺序存储,利用一组地址连续的存储单元存放自栈底至栈顶的数据元素,同时附设一个指针top指示当前栈顶的位置;
2)栈的顺序存储类型描述:
#define MaxSize 50 //定义栈中元素的最大个数 typedef struct{ ElemType data[MaxSize]; //存放栈中元素 int top; //栈顶指针 }SqStack;
初始栈顶指针S.top=-1; 栈顶元素S.data[S.top];栈空:S.top=-1;栈满:S.top=MaxSize-1;栈长S.top+1;进栈+1,出栈-1;
3)顺序栈的基本运算
1.初始化
void InitStack(&S){ S.top=-1; //初始化栈顶指针 }
2.判栈空
bool StackEmpty(S){ if(S.top==-1){ //栈空 return true; } else{ //不空 return false; }
3.进栈
bool Push(SqStack &S,ElemType x){ if(S.top=MaxSize-1){ //栈满,报错 return false; } S.data[++S.top]=x; //进栈,指针加1再进栈 return true; }
4.出栈
bool Pop(SqStack &S,ElemType &x){ if(S.top==-1){ //栈空,报错 return false; } x=S.data[S.top--]; //先出栈,指针再减1 return true; }
5.读栈顶元素
bool GetTop(SqStack S,ElemType &x){ if(S.top==-1){ //栈空,报错 return false; } x=S.data[S.top]; //x记录栈顶元素 return true; }
注意初始条件,S.top=-1表示栈空,S.top=0表示指向第一个元素;
4)共享栈(能有效利用存储空间)
将栈底设置在共享空间两端,固定不变;栈顶向共享空间的中间延伸;
栈的链式存储结构(链栈)
1)通常用单链表实现,规定所有操作在单链表表头进行,规定链栈没有头结点,Lhead指向栈顶元素;
2)栈的链式存储类型描述
typedef struct Linknode { ElemType data; //数据域 struct Linknode *next; //指针域 }*LiStack; //栈类型定义
队列
队列基本概念
1)队列:简称队,只允许在表的一端进行插入,而在表的另一端进行删除的线性表,先进先出(FIFO);
2)队头:允许删除的一端,删除元素称为出队或离队;
3)队尾:允许插入的一段,插入元素称为入队或进队;
4)空队列:不含任何元素的空表;
5)基本操作:
InitQueue(&Q):初始化队列;
QueueEmpty(&Q):判队空;
EnQueue(&Q,x):入队;
DeQueue(&Q,&x):出队;
GetHead(Q,&x):读队头元素,将值赋给x;
队列的顺序存储结构
==>队列的顺序存储
1)队列的顺序存储:分配一块连续的存储单元存放队列中的元素,并设两个指针,front指向当前队头元素的位置,rear指向当前队尾元素的位置。
2)队列顺序存储类型描述:
#define MaxSize 50 //定义队列中元素的最大个数 typedef struct{ ElemType data[MaxSize]; //存放队列元素 int front,rear; //队头指针和队尾指针 }SqQueue;
3)
初始状态 (队空):Q.front = Q.rear = 0;
进队:先送值到队尾,再队尾指针+1;
出队:先取队头值,再队头指针+1;
==>循环队列
1)把存储队列元素的表从逻辑上看作一个环;
初始:Q.front = Q.rear = 0;
入队:队尾指针进1取模,Q.rear=(Q.rear+1)%MaxSize;
出队:队首指针进1取模,Q.front=(Q.front+1)%MaxSize;
队列长度:(Q.rear+MaxSize-Q.front)%MaxSize;
2)队空队满判定方法
1.牺牲一个单元,入队时少用一个单元,front在rear下一个位置则队满;
队空:Q.front == Q.rear;
队满:Q.front == (Q.rear+1)%MaxSize;
队列长度:(Q.rear+MaxSize-Q.front)%MaxSize;
2.增设表示元素个数的数据成员
队空:Q.size=0;
队满:Q.size=MaxSize;
3.增设数据成员tag
队空:tag=0,删除导致Q.front=Q.rear;
队满:tag=1,插入导致Q.front=Q.rear;
==>循环队列的操作(采用“牺牲一个单元”判定法则判队空队满)
1)初始化
void InitQueue(&Q){ Q.front = Q.rear =0; //初始化队首、队尾指针 }
2)判队空
bool IsEmpty(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)队列的链式表示称为链式队列,同时带有队头指针和队尾指针的单链表,头指针指向队头结点,尾指针指向队尾结点;
2)队列链式存储类型描述
typedef struct { //链式队列结点 ElemType data; struct LinkNode *next; }LinkNode; typedef struct{ //链式队列 LinkNode *front,*rear; //链式队列队头和队尾指针 }LinkQueue;
队空:Q.front=NULL and Q.rear=NULL(若是带头结点的链式队列,只需Q.rear=Q.front即可)
入队:新结点插入链表尾部,即Q.rear指向新结点
出队:取队头元素,Q.front指向下一结点(若该结点为最后一个结点,则令Q.front,Q.rear均为NULL)
3)链式队列的优点:不存在存储分配不合理和溢出问题,适合数据元素变动较大的问题;
==>链式队列基本操作
1)初始化
void InitQueue(LinkQueue &Q){ Q.front = Q.rear =(LinkNode *)malloc(sizeof(LinkNode)); // 建立头结点 Q.front->next = NULL; //初始为空 }
2)判队空
bool IsEmpty(LinkQueue Q){ if(Q.rear==Q.front){ return true; } else{ return false; } }
3)入队
void EnQueue(LinkQueue &Q,ElemType x){ s=(LinkNode *)malloc(sizeof(LinkNode)); //创建新结点 s->data=x; s->next = NULL; Q.rear->next = s; //插入链表尾部 Q.rear=s; }
4)出队
bool DeQueue(LinkQueue &Q,ElemType &x){ if(Q.front==Q.rear){ //队空 return false; } p=Q.front->next; x=p->data; Q.front->next = p->next; if(Q.rear ==p){ //若原队列只有一个结点,则删除后变空 Q.rear = Q.front; } free(p); return true; }
双端队列
1)允许两端都可以进行入队和出队操作的队列(元素逻辑结构仍是线性结构)
进队:前端进的在后端进的前面
出队:先出的元素排在后出元素的前面
2)输出受限的双端队列:允许在一端进行插入和删除,另一端只允许插入的双端队列;
3)输入受限的双端队列:允许在一端进行插入和删除,另一端只允许删除的双端队列;
不能通过输入受限的双端队列得到的是4,2,3,1和4,2,1,3;
不能通过输出受限的双端队列得到的是4,2,3,1和4,1,3,2;
栈和队列的应用
栈的应用
1)栈在括号匹配的应用:
设空栈=>左括号进栈=>右括号,则消除栈中与之最近的左括号(匹配则继续,不匹配则退出);
2)栈在表达式求值中的应用:后缀表达式求值
操作数进栈=>操作符,从栈中取最上的操作数计算,并将结果压入栈中=>重复上述操作;
3)栈在递归中的应用
递归=>非递归,需要借助栈;
队列的应用
1)队列在层次遍历中的应用
根结点入队;队空则结束遍历重复第三步;队列第一个结点出队并访问,若有左孩子,则将左孩子入队,若有右孩子,则将右孩子入队,返回第二步;
2)队列在计算机系统中的应用
打印机的数据缓冲区(主机和外设速度不匹配的问题);
CPU资源竞争(队首用户先使用)(多用户引起的资源竞争问题);
特殊矩阵的压缩存储
数组
数组是线性表的推广,一维数组是一个线性表,二维数组可看作线性表的线性表;数组一旦定义,维数和维界不可改变,除初始化和销毁外,只能存取和修改元素;
数组的存储结构
一维数组: A[0,1,...,n-1]
二维数组:按行优先和按列优先
矩阵的压缩存储
压缩矩阵:多个值相同的元素只分配一个存储空间;
1)对称矩阵
2)三角矩阵
3)三对角矩阵
4)稀疏矩阵
-----------------------------------
时间原因,此篇停更,掌握基本概念后,刷题才是硬道理!!!
参考资料:
《王道数据结构》