数据结构笔记
文章目录
绪论
概念范围:数据>数据元素>数据项。
性质相同的数据元素集合叫做数据对象。
数据元素时数据的基本单位。
数据项是是数据的不可分割的最小单位。
逻辑结构: 1.集合结构 2.线性结构 3.树结构 4.图结构或网状结构
逻辑结构也可分为: 线性结构和非线性结构
存储结构分为 顺序存储结构 和 链式存储结构
计算复杂度时要注意一点,如果操作的次数可以得出具体数,比如for循环里要循环100次,这可以直接看作复杂度O(1)。
在相同规模n下,复杂度O(n)的算法在时间上总是优于复杂度O(2^n)。这是正确的,n不可以认为是1,因为如果是1,复杂度就不可以写成带n的了,只有n趋于很大时才可以写带n的复杂度,否则这个复杂度表达甚至不具有普适性。
算法原地工作指算法运行所需要的辅助空间相当于输入是一个常数。
顺序存储结构中数据元素之间的逻辑关系是由 存储位置 表示的
链式存储结构中数据元素之间的逻辑关系是由 指针 表示的
有序表和顺序表是两个东西,有序表是逻辑结构中的,顺序表是与存储结构有关的术语,同理,链表也是和存储结构有关的术语。
抽象数据类型的三个组成部分: 数据对象,数据关系和基本操作。
算法是解决问题的有限运算序列,而不是解决问题的计算方法,因为计算方法可能是需要无线长的时间的,这也就不可称之为算法。
高效性不是达到所需要的时间性能。而是指设计合理,执行效率高,达到时间性能即可这种片面的视角是无法准确描述高效性的。
空间复杂度是指在算法运行时需要的辅助运行空间,比如将数组a进行reverse。如果你用了辅助数组b,且b的大小是n,那么空间复杂度就是n了,如果你直接对数组a两头开始对元素进行交换,也就是说没有用额外的空间,那么空间复杂度就是O(1).
线性表
线性表的定义和特点
线性表的数据元素虽然不同,但同一线性表必定具有相同的特性,即属于同一数据对象,相邻数据元素存在序偶关系。
非空的线性表或线性结构的特点:
1.第一个元素和最后一个的元素唯一
2.除了第一个之外,结构中的每个数据元素均只有一个前驱。除了最后一个之外,结构中的每个数据元素均只有一个后继。
线性表的顺序存储结构是一种随机存取的存储结构
单链表的存储密度一定小于1,因为 存储密度=单链表数据项所占空间/结点所占空间,结点所占空间由数据项所占空间和存放后继结点地址的链域,存放后继结点地址的链域一定存在,故存储密度小于1。
创建一个包括n个节点的有序单链表的时间复杂度是 O(n^2)
所有数据通过指针的链接而组成单链表这是错误的,应该是所有的结点通过指针的链接而组成单链表
线性表的顺序表示和实现
定义顺序表的线性存储结构:
typedef struct
{
char no[20]//图书ISBN
char name[50];//图书名字
float price; //图书价格
}ElemType;
typedef struct
{
ElemType *elem; //存储空间的基地址
int length;
基本操作:
1.初始化
Status InitList(SqList &L)
{
L.elem=new ElemType[MAXSIZE];//分配内存;
if(!L.elem) exit (OVERFLOW);
L.length=0;
return OK;
}
2.取值
Status GetElem(SqList L,int i,ElemType &e)//取出第i个存入e
{
if(i<1||i>n) return ERROR;
e=L.elem[i-1];
return OK;
}
3.查找
int LocateElem(SqList L,Elemtype e)//查找e所在的位置
{
for(int i=0;i<L.length;i++)
{
if(L.elem[i]==e) return i+1;
}
return 0;
}
4.插入
Status ListInsert(SqList &L,int i,ElemType e)//在第i个插入e
{
if(i<1||i>L.length+1) retrun ERROR;//这里要注意i是可以等于L.length+1,因为可以把e添到最后
if(L.length==MAXSIZE) retrun ERROR;
for(int j=L.length-1;j>=i-1;j--)
{
L.elem[j+1]=L.elem[j];
}
L.elem[i-1]=e;
L.length++;
return OK;
}
5.删除
Status ListDelete(SqList &L,int i)//删除第i个elem
{
if(i>L.length||i<1) return ERROR;
for(int j=i;j<L.length;j++)
{
L.elem[j-1]=L.elem[j];
}
L.length++;
return OK;
}
线性表的链式表示和实现
定义顺序表的线性存储结构:
1.节点包括两个域:指针域和数据域。
2.结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻。
3.线性表的链式表示又称为非顺序映像或链式映像。
单链表/线性链表:结点只有一个指针域的链表。
双链表:有两个指针域的链表。
循环链表:首尾相接的链表称为循环链表。
6.链表有无头结点都是可以的。
有头结点的时候,当头节点的指针域为空时表示空表。
无头结点的时候头指针指向的节点为为空。
头节点优势:
便于首元结点的处理:首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其它位置一致,无须进行特殊处理;
便于空表和非空表的统一处理:无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了。
8.链表的查找是O(n)的,插入和删除是O(1)的,但是如果要在单链表中进行前插或删除操作,由于要从头查找前驱结点,所耗时间复杂度为O(n)。
定义:
typedef struct LNode
{
ElemType data;//数据域
struct LNode *next;//指针域
}LNode,*LinkList;
//这里的LNode *p 和LinkList p 意义相同。
//p是指针变量,表示结点地址。
//*p是结点变量,表示一个结点。
单链表基本操作:
1.初始化
(1)生成新结点作为头结点,用头指针L指向头结点。
(2)头节点的指针域置空。
Status InitList_L(LinkList &L)
{
L=new LNode; //开辟一个存放结点的存储空间,返回一个指向该存储空间的地址。
L->next=NULL; //L的指针域为空。
return OK;
}
2.销毁链表
Status DestroyList_L(LinkList &L)
{
LinkList p;
while(L)
{
p=L;
L=L->next;
delete p;//删除指针p所指的内容
}
return OK;
}
3.清空
Status ClearList(LinkList& L)//将L重置为空表
{
LinkList p,q;
p=L->next;//p指向第一个结点
while(p) //没到表尾
{
q=p->next;
delete p;
p=q;
}
L->next=NULL;
return OK;
}
4.求表长
int ListLength_L(LinkList L)
{
LinkList p;
p=L->next;
int i=0;
while(p)
{
i++;
p->next;
}
return i;
}
5.判断表是否为空
int ListEmpty(LinkList L)
{
if(L->next) return 0;
else return 1;
}
6.获取第i个结点e
Status GetElem_L(LinkList L,int i,ElemType &e)
{
LinkList p;
p=L->next;int j=1;//初始化
while(p&&j<i)
{
p=p->next;++j;
}
if(!p||j>i) return ERROR;//第i个结点不存在 j>i是为了防止i<=0
ElemType e=p->data;
return OK;
}
7.在线性表L中查找值为e的数据元素并返回地址
LNode *LocateELem_L(LinkList L,Elemtype e)
{
p=L->next;
while(p&&p->data!=e)
{
p=p->next;
}
return p;
}
8.在线性表L中查找值为e的数据元素并返回位置序号
int LocateElem_L(LinkList L,ElemType e)
{
p=p->next;
int j=0;
while(p&&p->data!=e)
{
j++;
p=p->next;
}
return j;
}
9.插入结点到第i个结点的位置上,即ai-1和ai之间
(1)找到ai-1存储位置p
(2)生成一个新结点s
(3)将新结点s的数据域置为x
(4)新结点s的指针域指向结点ai
(5)令结点p的指针域指向新结点s
Status ListInsert_L(LinkList &L,int i,ElemType e)
{
LinkList p=L;j=0;
while(p&&j<i-1) {p=p->next;++j;}
if(!p||j>i-1) return ERROR;
s=new LNode;
s->data=e;
s->next=p->next;
p->next=s;
return OK;
}
10.删除第i个结点
(1)找到ai-1存储位置p
(2)保存要删除的结点的值
(3)令p->next指向ai的直接后继结点
(4)释放结点ai的空间
Status ListDelete_L(LinkList &L,int i,ElemType &e)
{
LinkList p=L;int j=0;
while(p->next&&j<i-1)
{
j++;
p=p->next;
}
if(!(p->next)||j>i-1) return ERROR;
LinkList q=p->next;
p->next=q->next;
e=q->data;//保存删除节点的数据域;
delete q;
return OK;
}
11.单链表的建立:前插法
void CreatList_L(LinkList &L,int n)
{
L=new LNode;
L->next=NULL;//建立一个带头结点的单链表,L为头节点的地址
for(int i=1;i<=n;i++)
{
LinkList p=new LNode;
cin>>p->data;
p->next=L->next;L->next=p;//插入表头
}
}
12.单链表的建立:尾插法
void CreateList_L(LinkList &L,int n)
{
L=new LNode;
L->next=NULL;
LinkList r=L;//尾指针r指向头节点
for(int i=0;i<n;i++)
{
LinkList p=new LNode;
cin>>p->data;
p->next=NULL;r->next=p;//插入到尾部,记得插入的时候要先把p的next置为空
r=p; //r指向新的尾结点
}
}
循环链表基本操作:
1.终止条件的变化:由p!=NULL变为 p!=L ,由p->next!=NULL变为p->next!=L
2.对于循环链表来说,有时不给出头指针,而给出尾指针可以更方便的找到第一个和最后一个结点。
3.rear->next->next就是开始结点(即首元结点),rear为终端结点,终端结点下一个是头结点,头节点下一个是首元节点。
1.循环链表的合并
LinkList Connect(LinkList Ta,LinkList Tb)
{
LinkList p=Ta->next;//p存表头
Ta->next=Tb->next->next;//Tb表头连接Ta表头
delete Tb->next;//释放Tb表头结点
Tb->next=p;//修改指针
return Tb;
}
双向链表基本操作:
1.定义:
typedef struct DuLNode
{
ElemType data;
struct DuLNode *prior;
struct DuLNode *next;
}DuLNode,*DuLinkList;
2.插入
**执行顺序:**123随便换顺序,4是最后一个不能变。
Status ListInsert_DuL(DuLinkList &L,int i,ElemType e)
{
DuLinkList p=L->next;int j=0;
while(p&&j<i)
{
j++;
p=p->next;
}
if(!p||j>i) return ERROR;
s=new DuLNode;
s->data=e;
s->next=p;
s->prior=p->prior;
p->prior->next=s;
p->prior=s;
}
3.删除
Status ListDelete_Dul(DuLinkList &L,int i,ElemType e)
{
DuLinkList p=GetElem(L,i);
if(!p) return ERROR;
p->prior->next=p->next;
p->next->prior=p->prior;
delete p;
return OK;
}
线性表的进阶操作
线性表的合并:
比如 A=(7,5,3,11) B=(2,6,3) 合并后 A=(7,5,3,11,2,6). 去掉重复元素,且保持相对位置。
①分别获取LA的表长m和LB的表长n。
②从LB中第一个数据元素开始,循环n次执行以下操作。
从LB中查找第i个数据元素赋值给e;
在LA中查找元素e,如果不存在,则将e插在表LA的最后。
void MergeList(List &LA,List LB)//把B合并到A上
{
int m=ListLength(LA),n=ListLength(LB);
for(int i=1;i<=n;i++)
{
List e=GetElem(LB,i);
if(!LocateElem(LA,e)) ListInsert(LA,++m,e);//如果LA中不存在e,则直接插入到LA后方。
}
}
有序表的合并:
A=(3,5,8,11) B=(2,6,8,9,11,15,20) 合并后---->C=(2,3,5,6,8,8,9,11,11,15,20)
步骤:
①创建一个表长尾m+n的空表LC。
②指针pc初始化,指向LC的第一个元素。
③指针pa和pb初始化,分别指向LA和LB的第一个元素。
④当指针pa和pb均未到达相应表尾时,则依次比较pa和pb所指向的元素值,从LA或LB中摘取元素值较小的结点插入LC的最后。
⑤如果pb已到达LB的表尾,一次将LA的剩余元素插入LC的最后。
⑥如果pa已到达LA的表尾,一次将LB的剩余元素插入LC的最后。
1.顺序表的有序合并:
void MergeList_Sq(SqList LA,SqList LB,SqList &LC)
{
LC.Length=LB.Length+LA.Length;
pc=LC.elem; pb=LB.elem; pa=LA.elem;
pa_last=LA.elem+LA.length-1; pb_last=LB.elem+LB.length-1;
while((pa<=pa_last)&&(pb<=pb_last))
{
if(*pa<=*pb) *pc++=*pa++;
else *pc++=*pb++;
}
while(pa<=pa_last) *pc++=*pa++;
while(pb<=pb_last) *pc++=*pb++;
}
2.链式表的有序合并:
void MergeList_L(LinkList &LA,LinkList &LB,LinkList &LC)
{
pa=LA->next; pb=LB->next;
LC=LA;//用LA的头节点作为LC的头节点。
pc=LC;//pc指向LC
pc=LA->next;
while((pa!=nullptr)&&(pb!=nullptr))
{
if(pa->next<=pb->next) {pc->next=pa; pc=pc->next; pa=pa->next;}
else {pc->next=pb;pc=pc->next; pb=pb->next;}
}
pc->next=pa?pa:pb;//pa为空的话,pc->next=pb,否则为pa。
delete LB;
}
为什么只delete LB?
LA和LB的头指针都是用了所谓的「哨兵节点」,它们本身是空的节点,所以合并时并不参与。用哨兵节点做头指针是为了简化边界条件。
两个链表合并完之后,LA(LC)是新链表的头指针,仍是一个哨兵节点。这时原本用作LB头指针的这个哨兵节点就没有用了,所以要释放掉。
如果你创建一个链表的时候不使用哨兵节点做头指针,那合并完之后就不可以释放掉LB的头指针。
栈和队列
栈和队列的定义和特点:
1**. 栈**:后进先出 队列:先进先出
顺序栈:基本操作
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) return OVERFLOW;
S.top=S.base;
S.stackSize = MAXSIZE;
}
3.判空
bool StackEmpty(SqStack S)
{
if(S.top==S.base) return true;
else return false;
}
4.求顺序栈长度
int StackLength(SqStack S)
{
return S.top-S.base;
}
5.清空顺序栈
Status ClearStack(Stack S)
{
if(S.base) S.top=S.base;
return OK;
}
6.销毁顺序栈
Status DestroyStack(SqStack &S)
{
if(S.base)
{
delete S.base;
S.stacksize=0;
S.base=S.top=NULL
}
}
7.进栈
Status Push(SqStack &S,SElemType e)
{
if(S.top-S.base==S.stacksize)//栈满
return ERROR;
*S.top++=e;
return OK;
}
8.出栈
Status Pop(SqStack &S,SElemType &e)
{
if(S.top==S.base)//栈空
return ERROR;
e=*--S.top;
return OK;
}
9.取栈顶元素
Status GetTop(SqStack &S,SElemType &e)
{
if(S.top==S.base) return ERROR;//栈空
e=*(S.top-1);
return OK;
}
链栈:基本操作
1.定义
typedef struct StackNode
{
SElemType data;
struct StackNode *next;
}StackNode,*LinkStack;
LinkStack S;
2.初始化
void InitStack(LinkStack &S)
{
S=NULL;
}
3.判断链栈是否为空
Status StackEmpty(LinkStack S)
{
if(S=NULL) return ture;
else return false;
}
4.进栈
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d29pqRts-1588579749529)(https://s1.ax1x.com/2020/04/13/GjlxO0.png)]
Status Push(LinkStack &S,SElemType e)
{
p=new StackNode;
if(!p) exit(OVERFLOW);
p->data=e;p->next=S;S=p;
return OK;
}
5.出栈
Status Pop(LinkStack &S,SElemType &e)
{
if(S==NULL) return ERROR;
e=S->data;p=S;S=S->next;
delete p;
return OK;
}
6.取栈顶元素
SElemType GetTop(LinkStack S)
{
if(S==NULL) exit(1);
else return S->data;
}
队列(顺序):基本操作
1.队列的定义
#define M 100
Typedef struct
{
QElemType *base;
int front;
int rear;
}SqQueue;
2.一些问题
front=rear=0;//初始情况
front==rear//空队标志
base[rear++]=x;//入队
x=base[front++];//出队
但以上操作会有些问题:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SJ7Q28Xf-1588579749530)(https://s1.ax1x.com/2020/04/17/JEOmvV.png)]
解决方案:利用循环队列
base[0]接在base[M-1]之后
若rear+1=M
则rear=0;
入队操作
base[rear]=x;
rear=(rear+1)%M;
出队操作
x=base[front];
front=(front+1)%M;
队空:
front==rear
队满:
(rear+1)%M==front
3.队列初始化
Status InitQueue(SqQueue &Q)
{
Q.base=new QElemType[MAXQSIZE];
if(!Q.base) exit(OVERFLOW);
Q.front=Q.rear=0;
return OK;
}
4.求循环队列长度
int QueueLength(SqQueue Q)
{
return (Q.rear-Q.front+MAXQSIZE)%M;//记得取模
}
5.循环队列入队
Status EnQueue(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;
}
6.循环队列出队
Status DeQueue(SqQueue &Q,QElemType e)
{
if(Q.rear==Q.front) return ERROR;
e=Q.base[Q.front];
Q.front=(Q.rear+1)%MAXQSIZE;
return OK;
}
队列(链):基本操作
1.定义
typedef struct QNode
{
QElemType data;
struct Qnode *next;
}Qnode,*QueuePtr;
typedef struct
{
QueuePtr front;
QueuePtr rear;
}LinkQueue;
2.初始化
Status InitQueue (LinkQueue &Q)
{
Q.front=Q.rear=(QueuePtr) malloc(sizeof(QNode));
if(!Q.front) exit(OVERFLOW);//申请内存失败
Q.front->next=NULL;//头结点指向空,即首元结点不存在
return OK;
}
3.销毁链队列
Status DestroyQueue (LinkQueue &Q){
while(Q.front)
{
Q.rear=Q.front->next;//头节点指向尾结点
free(Q.front);
Q.front=Q.rear;
}
return OK;
}
4.判空
Status QueueEmpty(LinkQueue Q)
{
return (Q.front==Q.rear);//头结点和尾结点相同,即初始为空的状态。
}
5.求链队列头元素
Status GetHead(LinkQueue Q,QElemType &e)
{
if(Q.front==Q.rear) return ERROR;//队列为空 返回error
e=Q.front->next->data;//头结点指向的下一个结点为首元结点,首元结点中的data即为头元素。
return OK;
}
6.入队
Status EnQueue(LinkQueue &Q,QElemType e)
{
QueuePtr p=(QueuePtr)malloc(sizeof(QNode));//申请内存空间
if(!p) exit(OVERFLOW);//申请内存失败
p->data=e;p->next=NULL;//data
Q.rear->next=p;
}
7.出队
Status DeQueue(LinkQueue &Q,QElemType &e)
{
if(Q.front==Q.rear) return ERROR;
QueuePtr p=Q.front->next;
Q.front->next=p->next;
if(Q.rear==p) Q.rear=Q.front;
delete p;
return OK;
}
串(String)
存储
顺序存储
#define MAXSIZE 255//串的最大长度
typedef struct
{
char ch[MAXSIZE+1];
int length;
}SString;
堆存储
typedef struct
{
char *ch;
int length;
}SString;
链式存储
#define CHUNKSIZE 80
typedef struct Chunk
{
char ch[CHUNKSIZE];
struct Chunk *next;
}Chunk;
typedef struct
{
Chunk *head,*tail;//串的头指针和尾指针
int curlen;//当前长度
}LString;
BF算法
其实就是暴力匹配
int Index_BF(SString S,SString T,int pos)//返回模式T在 主串S中第pos个字符开始 第一次出现的位置。
{
int i=pos,j=1;
while(i<=S.length&&j<=T.length)
{
if(S.ch[i]==T.ch[j]){i++;j++;}
else
{
i=i-j+2;
j=1;
}
}
if(j>T.length) return (i-T.length);
else return 0;//匹配失败返回0
}
KMP算法
**next[j]:**当前模式种第j个字符与主串中相应字符失配时,在模式中需要重新和主串中该字符进行比较的字符的位置。
n e x t [ j ] = { 0 j=1( t 1 与 s i 比较不等时,下一步进行 t 1 与 s i + 1 的比较) m a x 模式串前后缀最长公共序列的长度 1 k=1(不存在相同的字串,下一步进行 t 1 与 s i 的比较) next[j]= \begin{cases} 0& \text{j=1($t_1$与$s_i$比较不等时,下一步进行$t_1$与$s_{i+1}$的比较)}\\ max& \text{模式串前后缀最长公共序列的长度}\\1& \text{k=1(不存在相同的字串,下一步进行$t_1$与$s_i$的比较)} \end{cases} next[j]=⎩⎪⎨⎪⎧0max1j=1(t1与si比较不等时,下一步进行t1与si+1的比较)模式串前后缀最长公共序列的长度k=1(不存在相同的字串,下一步进行t1与si的比较)
int index_kmp(SString S,SString T,int pos)
{
int i=pos,j=1;
while(i<=S.length&&j<=T.length)
{
if(j==0||S.ch[i]==T.ch[j]) {i++;j++;}
else j=next[j];
}
if(j>T.length) return i-T.length;//匹配成功
else return 0;//匹配失败
}
如何求next函数?
void get_next(SString T,int next[])
{
int i=1,j=0;
next[1]=0;
while(i<T.length)
{
if(j==0||T.ch[i]==T.ch[j]) {i++;j++;next[i]=j;}
else j=next[j];
}
}
如何求nextval函数?
void get_nextval(SString T,int nextval[])
{
int i=1,j=0;
next[1]=0;
while(i<T.length)
{
if(j==0||T.ch[i]==T.ch[j])
{
i++;j++;
if(T.ch[i]!=T.ch[j]) nextval[i]=j;
else nextval[i]=nextval[j];
next[i]=j;
}
else j=nextval[j];
}
}
中相应字符失配时,在模式中需要重新和主串中该字符进行比较的字符的位置。
n e x t [ j ] = { 0 j=1( t 1 与 s i 比较不等时,下一步进行 t 1 与 s i + 1 的比较) m a x 模式串前后缀最长公共序列的长度 1 k=1(不存在相同的字串,下一步进行 t 1 与 s i 的比较) next[j]= \begin{cases} 0& \text{j=1($t_1$与$s_i$比较不等时,下一步进行$t_1$与$s_{i+1}$的比较)}\\ max& \text{模式串前后缀最长公共序列的长度}\\1& \text{k=1(不存在相同的字串,下一步进行$t_1$与$s_i$的比较)} \end{cases} next[j]=⎩⎪⎨⎪⎧0max1j=1(t1与si比较不等时,下一步进行t1与si+1的比较)模式串前后缀最长公共序列的长度k=1(不存在相同的字串,下一步进行t1与si的比较)
int index_kmp(SString S,SString T,int pos)
{
int i=pos,j=1;
while(i<=S.length&&j<=T.length)
{
if(j==0||S.ch[i]==T.ch[j]) {i++;j++;}
else j=next[j];
}
if(j>T.length) return i-T.length;//匹配成功
else return 0;//匹配失败
}
如何求next函数?
void get_next(SString T,int next[])
{
int i=1,j=0;
next[1]=0;
while(i<T.length)
{
if(j==0||T.ch[i]==T.ch[j]) {i++;j++;next[i]=j;}
else j=next[j];
}
}
如何求nextval函数?
void get_nextval(SString T,int nextval[])
{
int i=1,j=0;
next[1]=0;
while(i<T.length)
{
if(j==0||T.ch[i]==T.ch[j])
{
i++;j++;
if(T.ch[i]!=T.ch[j]) nextval[i]=j;
else nextval[i]=nextval[j];
next[i]=j;
}
else j=nextval[j];
}
}