数据结构与算法
前言:本文主要记录数据结构与算法学习过程中的知识点,且基于C语言,获得更好的阅读体验请前往我的hexo博客
一.绪论
基本概念和术语:
-
数据:数据是指能输入到计算机中并能够被计算机处理的一切对象.
-
数据元素:数据元素是数据的基本单位.
-
数据项:一个数据元素可由若干数据项组成.
-
数据对象:数据对象是具有相同性质的数据元素的集合.
-
数据结构:数据结构是指互相之间存在着一种或多种关系的数据元素的集合.
逻辑结构:
1.集合结构:数据元素同属一个集合,单个数据元素之间没有任何关系
2.线性结构:类似于线性关系,数据元素之间是一对一的关系
3.树形结构:树形结构中的数据元素之间存在一对多的关系
4.图形结构:数据元素之间是多对多的关系
存储结构(物理结构):
(数据结构种类很多, 甚至你也可以发明自己的数据结构, 但是底层存储无非数组或者链表 ,那些多样化的数据结构, 究其源头, 都是在链表或者数组上的特殊操作 )
1.顺序存储:一段连续的内存空间
-
优点:随机访问
-
缺点:插入删除效率低,大小固定
2.链式存储:不连续的内存空间
-
优点:大小动态扩展,插入删除效率高
-
缺点:不能随机访问
3.索引:为了方便查找,整体无序,但索引块之间有序,需要额外空间存储索引表
-
优点:对顺序查找的一种改进,查找效率高
-
缺点:需额外空间存储索引
4.散列:选取某个函数,数据元素根据函数计算存储位置,可能存在多个数据元素存储在同一位置,引起地址冲突
-
优点:查找基于数据本身即可找到,查找效率高,存取效率高
-
缺点:存取随机,不便于顺序查找
队列,栈这两种数据结构既可以使⽤链表也可以使用数组实现.用数组实现,就要处理扩容缩容的问题; 用链表实现,则没有这个问题,但需要更多的内存空间存储节点指针
图的两种表示方法,邻接表就是链表,邻接矩阵就是二维数组.邻接矩阵判断连通性迅速,并可以进行矩阵运算解决⼀些问题,但是如果图⽐较稀疏的话很耗费空间.邻接表⽐较节省空间,但是很多操作的效率上肯定⽐不过邻接矩阵。
散列表就是通过散列函数把键映射到⼀个大数组里。 ⽽且对于解决散列冲突的方法, 拉链法需要链表特性, 操作简单, 但需要额外的空间存储指针; 线性探查法就需要数组特性, 以便连续寻址, 不需要指针的存储空间,但操作稍微复些。
影响算法运行时间的因素:
1.运行程序的计算机的机器指令的品质与速度
2.书写程序的语言(一般实现语言级别越高,其执行效率越低)
3.编译程序所生成目标代码的质量
4.问题的规模
大O表示法:用来表示时间复杂度函数的增长率的上界
时间复杂度:嵌套(求积),并列(求和)
空间复杂度:算法运行所需存储空间:
1.程序本身占用的空间
2.算法的输入,输出占用的空间
3.算法运行中占用的空间
评价一个算法的空间复杂度一般只考虑算法运行中所占用的临时空间.
对于一个算法,其时间复杂度和空间复杂度往往是相互影响的.
二.线性表
线性结构的特点是在数据元素的非空有限集合中,存在唯一的首元素和唯一的尾元素,首元素无直接前驱,尾元素无直接后继,集合中其他数据元素都有唯一的直接前驱和唯一的直接后继.线性表是最简单,最基本,也是最常用的一种线性结构.
线性表是具有相同数据类型的n(n>=0)个数据元素的有限序列.
线性表的顺序存储结构:
#define MAXSIZE 20 //存储空间初始分配量
typedef int ElemType; //ElemType类型根据实际情况而定,这里假设为int
typedef struct
{
ElemType data[MAXSIZE]; //数组存储数据元素,最大值为MAXSIZE
int length; //线性表当前长度
}SqList;
线性表顺序存储结构需要三个属性:
- 存储空间的起始位置
- 线性表的最大存储容量(数组的长度)
- 线性表的当前长度(小于等于数组长度)
顺序表的初始化:
SqList *init_SqList()
{
SqList *L;
L=(SqList *)malloc(sizeof(SqList)); //动态分配内存空间
if(!L) exit(1); //如果存储分配失败,运行exit()函数终止程序运行
L->length=0;
return L;
}
顺序表的插入运算:
SqList *Insert_SqList(SqList *L,int i,ElemType x)
{//在顺序表L第i个位置插入值为x的元素
if(L->length>=MAXSIZE-1)//判断表是否满
{
printf("顺序表已满!");
exit(1);
}
if(i<0||i>L->length+1)//判断插入位置是否合理
{
printf("插入位置i不合理!");
exit(1);
}
for(int m=L->length-1;m>=i-1;m--)
{
L->data[m+1]=L->data[m];//节点后移
}
L->data[i-1]=x;//新元素插入
L->length++;//表长加1
return L;
}
顺序表的删除运算:
SqList *Delete_SqList(SqList *L,int i,ElemType e)
{//删除顺序表L中第i个元素,删除元素的值保存在e中
if(i<0||i>L->length)
{
printf("插入位置i不合理!");
exit(1);
}
e=L->data[i-1];
for(i;i<=L->length-1;++i)
L->data[i-1]=L->data[i];
L->length--;
return L;
}
顺序表按值查找运算:
int LocateElem_Sq(SqList *L,ElemType x)
{//在顺序表中查找值为x的元素,查找成功返回元素存储位置
for(int i=0;i<length;i++)
{
if(L->data[i]==x)
return i;
}
return -1;
}
顺序表合并算法:
SqList *Merge_SqList(SqList *A,SqList *B)
{//将两个非递减次序排列的顺序表A和B合并为一个新的有序顺序表C
SqList *C;
int i,j,k;
i=0;j=0;k=0;
C=(SqList *)malloc(2*MAXSIZE*sizeof(SqList));
if(!C) exit(1);
C->length=A->length+B->length;
while(i<=A->length-1&&j<=B->length)
if(A->data[i]<B->data[j])
{
C->data[k]=A->data[i];k++;i++;
}
else
{
C->data[k]=B->data[j];k++;j++;
}
//前面部分是先将A,B中较短的填入C,后面再填入另一个
while(i<=A->length-1)
{
C->data[k]=A->data[i];k++;i++;
}
while(j<B->length-1)
{
C->data[k]=B->data[j];k++;j++;
}
return C;
}
线性表的链式存储结构:
1.单链表(动态链表)
静态链表是用类似于数组方法实现的,是顺序的存储结构,在物理地址上是连续的,而且需要预先分配地址空间大小。所以静态链表的初始长度一般是固定的,在做插入和删除操作时不需要移动元素,仅需修改指针
单链表节点数据类型:
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList;
头插入法建立单链表:
LinkList CreatList_L1()
{
LinkList L;
LinkList P;
int x;
L=(LinkList*)malloc(sizeof(Node));
L->next=NULL;
scanf("%d",&x);
while(x!=0)
{
p=(LinkList*)malloc(sizeof(Node));
p->data=x;
p->next=L->next;
L->next=p;
scanf("%d",&x);
}
return L;
}
尾插入法建立单链表:
LinkList CreatList_L2()
{
LinkList L,p,r;
int x;
r=L=(LinkList*)malloc(sizeof(Node));
L->next=NULL;
scanf("%d",&x);
while(x!=0)
{
p=(LinkList*)malloc(sizeof(Node));
p->data=x;
p->next=NULL;
r->next=p;
r=p;
}
return L;
}
求链表长度的算法:
int Listlength(LinkList L)
{
LinkList p;
int i=0;
p=L;
while(p->next!=NULL)
{
i++;
p=p->next;
}
return i;
}
查找操作:
1.按序号查找
LinkList Get_LinkList(LinkList L,int i)
{//在链表L中查找第i个元素,找到返回其指针,否则返回空
LinkList p;
p=L;
int j=0;
while(p->next!=NULL&&j<i)
{
p=p->next;
j++;
}
if(j==i)return p;
else return NULL;
}
2.按值查找
LinkList Locate_LinkList(LinkList L,ElemType x)
{
LinkList p;
p=L;
while(p->next!=NULL&&p->data!=x)
p=p->next;
return p;
}
插入运算:
LinkList ListInsert(LinkList L,int i,ElemType x)
{//在单链表第i个节点前插入新元素x
LinkList p,s;
int j=0;
p=L;
while(p->next!=NULL&&j<i-1)
{
p=p->next;
j++;
}
if(p==NULL||j>i-1)
{printf("参数i错误!");exit(1);}
s=(LinkList*)malloc(sizeof(Node));
s->data=x;
s->next=p->next;
p->next=s;
return L;
}
删除运算:
LinkList ListDelete(LinkList L,int i,ElemType *e)
{//删除单链表L中第i个元素
LinkList p,q;
int j=0;
p=L;
while(p->next!=NULL&&j<i-1)
{
p=p->next;
j++;
}
if(p->next=NULL||j>i-1)
{
printf("参数i错误!");
exit(1);
}
q=p->next;
p->next=q->next;
*e=q->data;
free(q);
return L;
}
有序链表归并算法:
LinkList Merge_LinkList(LinkList A,LinkList B)
{//A,B均为带头节点的单链表
LinkList C,p,q,s;
p=A->next;
q=B->next;
free(B);
C=A;
C->next=NULL;
while(p&&q)
{
if(p->data<q->data){s=p;p=p->next;}
else {s=q;q=q->next;}
s->next=C->next;
C->next=s;
}
if(p==NULL)p=q;
while(p)
{
s=p;
p=p->next;
s->next=C->next;
C->next=s;
}
}
2.循环链表
1.单向循环链表
单链表尾节点的指针域是空指针.而单向循环链表的最后一个节点的指针指向链表头节点.
对于单链表,从一已知节点只能访问该节点及其后继节点,无法访问该节点之前的节点;而对于单向循环链表,只要知道表中任一节点的地址,就可搜寻到所有其他节点的地址,遍历整个链表.
单向循环链表的数据类型定义与单链表相同.在单循环链表上的操作也与单链表基本相同,二者主要区别在于:判断是否达到表尾的条件不同.在单链表中,用指针域是否为NULL作为判断表尾节点的条件;而在循环链表中,则以节点指针域是否等于表头节点(头指针)作为判断到达表尾的条件.
2.双向链表
双向链表的结构:
typedef struct DuLnode
{
ElemType data;
struct DuLnode *prior,*next;
}DuLnode;
typedef DuLnode *DuLinkList;
双向链表的插入运算:
DuLinkList ListInsert_Dul(DuLinkList L,int i,ElemType x)
{//在双向链表的第i个节点前插入一个新元素x
DuLinkList p,s;
int j;
p=L;
j=0;
while(p!=NULL&&j<i)
{p=p->next;j++}
if(p==NULL||j<i)
{printf("参数i错误!");exit(1);}
if(!(s=(DuLinkList*)malloc(sizeof(DuLnode))))
exit(1);
s->data=x;
s->prior=p->prior;p->prior->next=s;
s->next=p;p->prior=s;
return L;
}
双向链表的删除运算:
DuLinkList ListDelete_Dul(DuLinkList L,int i,ElemType &e)
{//删除双向链表中第i数据元素
DuLinkList p;
int j;
p=L;
j=0;
while(p!=NULL&&j<i)
{p=p->next;j++;}
if(p==NULL||j>i)
{printf("参数i错误!");exit(1);}
else{
e=p->data;
p->prior->next=p->next;
p->next->prior=p->prior;
free(p);
return L;
}
}
三.栈与队列
栈和队列是在程序设计中被广泛使用的两种数据结构.由于从数据结构角度看,栈和队列是两种特殊的线性表.它们的逻辑结构和线性表相同,只是其运算规则较线性表有更多的限制,因此,也可以将栈和队列称为操作受限的线性表.
栈是限定仅在表尾进行插入和删除操作的线性表.队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表.
栈的定义
- 栈是一种特殊的线性表,是一种只允许在表的一端进行插入或删除操作的线性表.把栈中允许进行插入,删除操作的一端称为栈顶,栈的另一端称为栈底.
- 当栈中没有数据元素时,称之为空栈.栈顶是动态的,对栈顶位置的标记称为栈顶指针.栈的插入操作通常称为进栈(入栈或压栈),栈的删除操作通常称为退栈或出栈.
- 根据栈的定义,每次进栈的数据元素都放在当前栈顶元素之前而成为新的栈顶元素,每次退栈的数据元素都是当前栈顶元素.这样,最后进入栈的数据元素总是最先退出栈,因此,栈具有"后进先出"的特性,所以栈又称为后进先出的线性表,简称LIFO表.
栈的存储结构
栈有两种存储表示方法,即顺序存储和链式存储.顺序存储的栈称为顺序栈,链式存储的栈称为链式栈.
顺序栈
顺序栈的存储结构:
#define StackInitSize 100;
typedef int StackElementType;
typedef struct{
StackElementType data[StackInitSize];
int top;
}SeqStack;
顺序栈的初始化:
SeqStack *InitStack()
{
SeqStack *s;
s=(SeqStack*)malloc(sizeof(SeqStack));
if(s!=NULL)
{s->top=-1;
return s;}
else{printf("没有足够的内存空间,申请失败,程序运行终止!");
exit(1);}
}
判断栈空的函数:
int IsEmpty(SeqStack *s)
{
return(s->top==-1)?1:0;//栈空返回1,否则返回0
}
销毁栈的函数:
void DestoryStack(SeqStack *s)
{
free(s);
printf("栈已销毁!\n");
return;
}
进栈操作:
void Push(SeqStack *s,StackElementType x)
{
if(s->top==StackInitSize){
printf("栈满!栈发生上溢,程序停止运行!\n");
exit(1);
}
else{
s->top++;
s->data[s->top]=x;
}
return;
}
退栈操作:
StackElementType Pop(SeqStack *s)
{
StackElementType temp;
if(IsElpty(s)){
printf("栈空!栈发生下溢,程序停止运行!\n");
exit(0);
}
else{
temp=s->data[s->top];
s->top--;
return temp;
}
}
读取栈顶元素:
StackElementType GetTop(SeqStack *s)
{
if(IsEmpty(s))
{
printf("空栈,程序停止运行!");
exit(0);
}
else
return s->data[s->top];
}
栈浮动技术:
当一个程序中同时使用多个顺序栈时,为了防止上溢错误,需要为每个栈分配较大的存储空间.在多栈使用过程中通常会出现:在某一栈发生上溢的同时,其余栈尚有大量未用空间存在,这样不利于内存空间的共享,会降低内存空间的使用效率.如果将多个栈安排在同一个连续的存储空间中,这样多个栈共享存储空间,并使它们根据实际情况互相调节余缺.如此既节省了存储空间的开销,又降低了上溢现象发生的概率.这种多栈共享空间的技术,通常称为栈浮动技术.
当程序中同时使用两个栈时,两个栈可以共享同一存储空间.此时,将两个栈的栈底分别设在同一存储空间的两端,让两个栈各自向中间延伸.这样只有当整个共享空间被两个栈占满(两个栈的栈顶相遇)时,才会发生上溢.
两栈共享空间的结构:
typedef struct
{
StackElementType data[StackInitSize];
int top1;//栈1栈顶指针
int top2;//栈2栈顶指针
}SqDoubleStack;
对于两栈共享空间的进栈方法,我们除了要插入元素值参数外,还需要有一个判断是栈1还是栈2的栈号参数stackNumber
链式栈
链式栈的存储结构:
typedef int StackElementType;
typedef struct node{
StackElementType data;
struct node *next;
}LinkStack;
链式栈的进栈操作:
LinkStack *Push(LinkStack *top,StackElementType x)
{
LinkStack *p;
p=(LinkStack *)malloc(sizeof(LinkStack));
if(p){p->data=x;
p->next=top;
top=p;
return top;}
else{printf("内存不足,程序运行停止!");
exit(0);}
}
链式栈的退栈操作:
LinkStack *Pop(LinkStack *top,StackElementType *elem)
{
LinkStack *temp;
if(top)
{
temp=top;
*elem=top->data;
top=top->next;
free(temp);
return top;
}
else
return NULL;
}
链式栈读取栈顶元素:
StackElementType GetTop(LinkStack *top)
{
return top?top->data:NULL;
}