线性表相关基础知识详见之前的文章
数据结构之线性表1:顺序表 https://blog.youkuaiyun.com/qq_45629864/article/details/107889870
链表概念
以链式结构存储的线性表就是链表
存储结构:存储器中位置任意,逻辑上相邻的数据元素在物理上不一定相邻,可能连续可能不连续。
存取方式:顺序存取
结点分为数据域和指针域
- 数据域:存储元素数值
- 指针域:存储后继结点的位置
由若干个结点由指针链组成一个链表
头指针指向第一个元素地址,并且由头指针唯一确定,因此单链表可以用头指针名字命名
最后一个结点指向空:p->next=NULL
辨析头结点和头指针:
- 头指针:指向链表第一个结点的指针
- 头结点:为了方便操作,在第一个元素之前附设的一个结点,可以啥都不存也可以存表长(统计表长时候头结点不计入)
- 首元结点:链表中存储第一个数据元素的结点
也可以像这样:H是头指针(仅有指针域),A是首元结点。ps:这种方式不推荐,绝大多数都带头结点
链表类型
- 单链表:结点只有一个指针域指向后继元素
- 双链表:结点由两个指针域,指向前驱和后继
- 循环链表:收尾相接的链表
存储类型描述
typedef struct Node{
ElemType data;
struct Node *next;
}Node, *LinkList;
由于next指向的变量是Node型,所以struct Node *next,和第一行一样,可以看做照着嵌套声明*next(就像指向int型声明用int *p一样),这个类型就叫Node
*Linklist 是指向结构结点的指针类型,定义的时候就不用给变量前面加 * 号了
Node *L
就等于Linklist L
更优的使用方法:将数据项都包进ElemType这一类型内,然后在链表结构体中使用。
定义链表和指针
定义链表:Linklist L
定义结点:*Node p
分辨空表
默认是带头结点的单链表,若L->next==NULL就是空表
建立单链表
尾插法
将申请的新结点放在上一个结点后面,类比正常排队
LinkList CreatListTail(LinkList L){//尾插法创建结点
LinkList L;
Node *s,*r; //创建指向结点的指针
int flag=1; //flag=1时正常输入,flag=0时结束输入
int c;
r=L;
while(flag){
scanf("%d",&c);
if(c!=99999){
s=(Node*)malloc(sizeof(Node));
s->data=c;
r->next=s; // 尾结点指向新结点,连接起来
r=s; //将尾指针移向最后结点
}
else{
flag=0;
r->next=NULL;
}
}
}
主体解读:当flag=1,也就是c!=99999时候,申请Node类型的新结点,并用指针s指向,将输入值c放入s指向的新结点的数据域,然后让原本指向头结点的尾指针r,他的指针域指向新结点s(其实就是连起来),之后再将尾指针挪到新结点上(就能一直指向最后一个结点), 输入结束后,指向尾巴的指针r再将指针域指向NULL(完成尾指针该做的任务)。
头插法
将申请的新结点放在第一个结点前面(头结点之后),类比每个人都插队到最前面
LinkList CreatListHead(LinkList L){//头插法创建结点
Node *s;
int c;
int flag=1;
while(flag){
scanf("%d",&c);
if(c!=99999){
s=(Node*)malloc(sizeof(Node));
s->data=c;
s->next=L->next; //新结点指向头结点指向的结点
L->next=s; //头结点指向新结点
}
else{
flag=0;
}
}
}
主体解读:当flag=1,也就是c!=99999时候,申请Node类型的新结点,并用指针s指向,将输入值c放入s指向的新结点的数据域,让新结点指向头结点指向的结点,完成连接,再把头结点指向改为指向新结点,完成新结点插入头结点和首元结点之间 输入结束后,直接完成。
求单链表长度
本质就是数结点个数,指针从第一个元素指到最后一个结点(最后一个结点.next=NULL)。
本题计算带头结点的单链表长度
int ListLength(LinkList L){ //返回长度
Node *p;
p=L->next; //p指向第一个元素
int i=0;
while(p!=NULL){ //当所指不为NULL
p=p->next; //指向下一个
i++;
}
return i;
}
由于最后结点是隐式位置,不知道具体值,所以用while语句。
当指向NULL时,不进循环,不加i,保存下来前值。
查找算法
按序号查找
默认单链表带头结点,每找一个结点计数+1,到达需要的序号。
Node *GetNum(LinkList L,int e){ //在表L中查找序号为e的结点
int j=0;
Node *p;
p=L; //指向头结点
while((p->next!=NULL)&&j<e) //未到达表尾&&未数到
{
p=p->next;
j++;
}
if(e==j){ //找到了
return p;
}
else
{
return NULL; //i<=0或者i>表长
}
}
主体解读:在:**未到达表尾&&计数小于序号(没数到)**这两个条件同时满足时循环,每次指针后移一个结点,并计数器+1,移动一次记一个数。
需要注意:最初p指向头结点,p后移一位到第一个结点,j=1。
按值查找
一个一个结点找相同的值,并返回这个结点
Node *GetElem(LinkList L,int e){ //在表L中查找值为e的结点并返回
Node *p;
p=L->next; //指向首元结点
while(p!=NULL){ //数过尾结点后退出循环
if(e!=p->data){ //未找到就后移
p=p->next;
}
else
return p;
}
}
插入操作
在带头结点的单链表L中的第i个元素之前插入一个数据元素e
LinkList Insert(LinkList L,int i,int e){//在表L的第i个元素之前插入值为e的结点
Node *p,*s;
int k=0;
p=L;
while(p!=NULL&&k<i-1){ //因为i之前,所以数到i-1个停下来
p=p->next;
k++;
}
if(p==NULL){ //若p都找到空了还没有找到位置i,则不合理
printf("插入位置不合适");
return ERROR;
}
s=(Node*)malloc(sizeof(Node));
s->data=e; //将值放入新结点s
s->next=p->next; //新结点指向定位指针p的下一个结点
p->next=s; //p指向新结点完成插入
}
主体解读:首先使用类似上面的查找算法,定位插入位置i的前一位,同时检测插入位置是否合法。申请新结点s,将值e放入s->data,新结点s指向定位指针p的下一项,定位结点p指向s完成连接
删除操作
单链表L中删除第i个结点,并将内部元素保存出来
LinkList Dele(LinkList L,int i,int e){//删除表L中第i个结点,并将内部元素保存在e里面
Node *p,*r;
int k=0;
p=L;
while(p->next!=NULL&&k<i-1){ //定位到被删除结点的前一个,不能定位在尾结点
p=p->next;
k++;
}
if(k!=i-1){
/*言下之意上面的循环中是因为p->next=NULL所以跳出,也就是说删除结点在尾结点后面,结点不存在*/
printf("删除结点的位置不合理");
return 0;
}
r=p->next; //r指向要被销毁的结点
p->next=r->next; //i-1结点指向销毁结点后一个(跨过销毁结点)
e=r->data;
free(r); //释放空间
}
主体解读:通过定位指针p定位到需要删除结点的前一项,但是需要注意不能定位到尾指针(尾指针下一项不存在),删除指针r指向定位指针p下一结点(需要删除的结点),定位指针p再指向r的下一结点(跨过需要删除的结点),将删除结点中元素保存出来,最后释放删除结点r。
求两集合之差
集合A用单链表LA表示,集合B用单链表LB表示,
求A-B,即属于A集合而不属于B集合的元素。(去除AB共有的元素)
void Difference(LinkList LA,LinkList LB){
Node *pre,*p,*q; //后面两个分别是LA,LB指示器
Node *r; //销毁指针
pre=LA; //当前处理结点的前一个结点(删除时使用)
p=LA->next; //当前处理结点
while(p!=NULL) //p未指向空
{ q=LB->next; //q定位在LB第一个结点
while(q!=NULL&&q->data!=p->data) //q未指向空 且 所指结点的元素值不相同就循环
{ q=q->next;}
if(q!=NULL) //上面的循环因为两值相等而退出,还没走完就找到相同的
{ r=p;
pre->next=p->next;
p=p->next;
free(r);
}
else //没有找到相同,正常后移
{ pre=p;
p=p->next;
}
}
}
整体思路是LA中删除操作,LB中查找操作,LA每定位一个结点,LB要走完全部结点。
主体解读:由于我们会删除LA中重复结点,所以p指向LA处理结点,pre指向p前一个结点,当p在LA中未指向空,则q在LB中从头查找,当q未指向空且没找到相同,q不断后移。若因为出现相同值而退出循环,则删除LA定位到的元素,否则p后移。
合并操作
两个递增的有序链表,将其合并成一个有序链表
void MergeList(LinkList &LA,LinkList &LB,LinkList &LC){
Node *pa,*pb,*pc;
pa=LA->next; //pa指向LA第一个元素
pb=LB->next; //pb指向LB第一个元素
pc=LC=LA; //用LA的头结点作为LC的头结点
while(pa!=NULL&&pb!=NULL) //当这两个都没有数到尾结点
{ if(pa->data<=pb->data)
{ pc->next=pa; //pc接上较小结点
pc=pa; //pc移动到较小结点
pa=pa->next; //pa后移一位
}
else
{ pc->next=pb; //pc接上较小结点
pc=pb; //pc移动到较小结点
pb=pb->next; //pb后移一位
}
}
if(pa!=NULL)
{ pc->next=pa;
}
else
{ pc->next=pb;
}
free(LB); //释放LB的头结点
}
主体解读:和顺序表合并类似,但是LC
销毁操作
从头结点开始,依次释放所有结点
void Destroy(LinkList L){//从头销毁所有结点
Node *p;
while(L!=NULL)
{
p=L;
L=L->next;
free(p);
}
}
将头指针不断后移,p定位并删除头指针的前一项
主体解读:当头指针L未指向空,p指向L的位置,L后移一位,删除p指向的结点,直到头指针L指向空
清空操作
清除链表L内所有结点,变成只有头指针和头结点的空链表
LinkList Clear(LinkList L){
Node *p,*q;
p=L->next;
while(p){
q=p->next;
free(p);
p=q;
}
L->next=NULL;
}
主体思想:用q保存p的下一个位置,删除p后,两个一块前移,直到p指向空。
摘自: 王卓老师:www.bilibili.com/read/cv3285…
摘自:耿国华老师 www.bilibili.com/video/BV1kx…