目录
上一篇博客介绍完了线性结构中的顺序结构,本节开始介绍另一种线性结构:链表:
一般应用链表开发项目的情况比顺序表多,所以链表是我们开发的重点。
链式存储
俗称链表,将零散的结点连起来,比如当我们找到第一个结点的时候,我们可以通过某种方式找到第二个结点,以此类推。虽然它们不是连续的,但是我们能通过某种方式把它们连起来,我们也把这种结构称为线性结构。
链表的每个结点都有一个地址,并且地址是随机的,即使我们知道链表中第一个结点的地址,也无法找到第二个结点,而顺序表的地址是连续的,所以当我们知道顺序表中第一个结点的位置我们就能顺着首地址找到后面的位置。
因此我们需要把链表的每一个结点连起来,形成一条线。
如何连起来?
首先我们把这个结点分成两块
所谓的数据域就是保存数据的区域,指针域就是保存指针/地址的区域。我们把下一个结点的地址保存在上一个结点的指针域里面
然后0x500这个位置的结点也分成两块,它的指针域也存在着下一个结点的地址
其他的结点一样的分成两份,然后指针域都存放在下一个结点的地址,最后一个结点后面没有别的结点了,可以直接NULL空
这样就能连起来了
因此说链表也是线性结构的,只不过是内存是不连续的。这样的好处是不用一下子申请一块很大的内存。申请的内存越小,申请成功的几率也就越高。
第一个结点叫头结点,一般来说头结点不保存数据,真正保存数据是从第一个结点开始的。
当然有些地方会让头结点保存链表的长度,并且头结点不算进长度里面,比如上面的这个链表的长度是3。
并且上面我们讲的这种链表称为单向链表,简称单链表,就是只能沿着一个方向走下去,有些单链表可以有头结点,有些可以没有头结点,只不过有头结点更方便些,所以之后我们写的链表都是有头结点的。
我们一般都把链表的头结点的地址保存下来,叫头指针。只要我们知道头结点的地址,就能找到头结点,就能访问头结点的指针域,就能找到第一个结点,找到第一个结点也就能找到下一个结点,依次类推。
可想可知,头指针非常重要。它就像数组名,存放了数组首元素的地址。
那如何用程序的方式表示一个结点?
一个结点里边有数据,也有指针,所以我们可以用结构体来表示:
比如:
Struct Node
{
int data; //数据
struct node*next; //指针的类型取决于指针指向的元素的类型,而下一个结点也是用结构体来表示的,因此这个指针的类型就是struct node*
}
我们以后会经常写这样一段代码:
Struct Node *h=head;
h=h->next;
head代表头指针,h表示结构体的地址,h->next代表访问结构体中的next成员,而next成员指向的是下一个结点的地址,将下一个结点的地址赋值给h,就相当于指针h指向了下一个结点。
这两行代码一定要先搞明白,之后我们写代码的时候就直接这样写。
单链表的操作代码
搞明白链表的基本概念之后我们就开始写代码了:
我们还是分成三个文件来写,两个.c文件和一个.h文件
创建一个目录叫linklist
然后在此目录下创建link.h
单链表初始化操作
然后link.h中定义结构体和函数的返回值
更正图片:struct node *next;
再创建main.c文件,并初始化链表
然后再在link.c中定义init_link(&head)
链表无需和顺序表一样定义一个容量变量SIZE,因为链表是没有容量限制的,前提是内存够用。
写完这个初始化函数后要在link.h中声明一下
编译一下(注:*.c表示所有.c文件)
链表插入操作(笔试重点,建议背下来)
插入操作有三种情况:
第一是尾插法,就是在链表的末尾插入一个节点。
第二种是头插法,每次都在第一个结点的前面(注意第一个结点指的头结点后的一个结点)插入,这时第一个结点就变成第二个节点。
第三种是指定位置插入,是比较普遍应用的插入操作,比如在第二个结点的前面插入一个结点,这时第二个结点变成了第三个结点,但是这种方式不需要让第三个结点的位置往后挪动,只需要修改第一个结点的指针域指向的位置,然后被插入的结点的指针域填上那个被变成第三个结点的结点地址即可。
于是我们得出来一个结论:往任意一个位置插入节点,其实只需要修改前面那一个结点的指针域。
代码演示:
Link.c
在link.h中声明一下这个函数
Main.c
运行结果:
链表遍历操作
代码演示:
注意:如果遍历函数体中不允许用printf来打印的话,怎么办?
先在main函数的上面写:
再将show函数作为参数传过去给遍历的那个函数
然后我们在遍历函数的定义中通过p来调用show函数
最后可以在main.c中写一个换行
最后要记得在link.h中重新声明一个遍历函数
运行结果:
这样写的好处是如果你想在遍历的过程中修改某一个数字的话,只需要修改show函数,不用动traverse_link函数
判断链表是否为空
判断链表是否为空只需要判断头结点的指针域是否为空,如果不为空,而是存放了一个地址的话,说明头结点后面有指向另一个结点,所以不为空。
代码演示:
Link.c
在link.h中声明一下这个函数
Main.c
运行结果:
链表长度计算
Link.c
在link.h中声明一下这个函数
Main.c
运行结果
链表查找操作
和顺序表的查找一样,分为两种情况,就是给定位置查找这个位置上的数,还有一种是给定一个数,查找出这个数的位置。
Link.c
在link.h中声明一下这个函数
Main.c
运行结果:
15出现的位置是第1和第3
链表反转操作
笔试题21(最好记下来)
int func(LinkList A) //A是头指针
{
if (A==NULL) return 0; //入参判断
ListNode *p = A->next; //p指向第一个结点
A->next = NULL; //头结点的指针域变成空,链表断了
while (p != NULL) //p不等于空就进入循环
{
ListNode *q = p; //q也指向第一个结点
p = p->next; //p指向第二个结点
q->next = A->next; //头结点的Next此时是空,赋值给q的next,即让第一个结点的指针域也为空,那么链表又在第一个结点之后断开了
A->next = q; //将q赋值给A-next,即将第一个结点的位置赋值给头结点的指针域,此时头结点的Next指向第一个结点
}
return 1;
}
LinkList为结点指针,这段代码实现了什么功能?
第一次while循环让一个链表从第一个结点后面断开成两个链表,继续这样推理,最终当p等于空跳出循环后,现象时是头结点指向第三个结点,第三个结点指针第二个结点,第二个结点指向第一个结点,第一个结点的指针域位NULL,所以这段代码实现的是链表的反转。
可以自己尝试画图推断出结果。
我们把这段笔试代码加入我们的代码中
Link.c
在link.h中声明一下这个函数
Main.c
运行结果:
链表删除操作
比如我们要删掉第二结点,那么我们只需要将第二个节点的空间给释放掉,释放掉之后,原来的第三个结点就变成了第二个节点,所以我们需要将这个新的第一个结点的位置存放到第一个结点的指针中。结论就是如果我们要删除掉某一个结点,就需要将它后一个结点的位置移动到它前一个结点的指针域中。
代码演示:
Link.c
在头文件中声明一下这个函数
Link.c
在Link.h中声明一下这个函数
Main.c
运行结果:
删除代码、插入代码以及反转代码这三个代码在笔试中都非常重要,所以务必要熟练!
链表的前驱和后继
我们需要两个指针来完成,注意:第一个结点没有前驱,所以我们让指针k指向第二个节点,指针q指针第三个结点,指针k总是在q后面。
代码演示:
Link.c
在link.h中声明一下这个函数
Main.c
运行效果:
链表的后继操作同理。
链表清空操作
每次在释放一个结点之后,都需要将它后面的一个结点的地址放到它的前一个结点中再释放。比如开始要释放第一个结点,那么就需要先将第一个结点的指针域中存放的地址存放到头结点的指针域中,再释放掉第一个结点,后面以此类推。
代码演示:
Link.c
在Link.h中声明这个函数
main.c
运行结果:
链表销毁操作
刚刚的清空操作还剩下头结点,销毁操作就是把头结点也释放掉,并且头指针也变成空。
代码演示:
List.c
在list.h中声明这个函数
Main.c
运行结果:
完整代码
Link.c
#include "link.h"
#include <stdlib.h>//malloc的头文件
#include <stdio.h>//printf的头文件
//链表的初始化
int init_link(Node **h)//用指针接收指针的地址
{
if(NULL==h)//判断空指针
{
return FAILURE;
}
*h=(Node*)malloc(sizeof(Node)*1);//头指针的地址存在h里面,则*h=head(头指针),在头指针指向的位置申请空间
if(NULL==(*h))//如果返回值是空的话说明内存用完了
{
return FAILURE;
}
(*h)->next=NULL;//只初始化头节点,后面的节点为空
return SUCCESS;
}
//链表的插入操作
int insert_link(Node*h,int p,int n)//p接收的是要插入的位置
{
//入参判断
if(NULL==h)
{
return FAILURE;
}
Node *q=h;//将头节点的位置记录下来
int k=1;//记录下移动的次数
while(k<p && q)//保证移动的次数小于p,并且要保证q不等于NULL,因为当p不合法时,比如p大于链表的长度+1,那q=q->next有可能给q赋值了NULL,导致程序死掉
{
q=q->next;//把指针移动到要插入位置的前一个位置
k++;//k++后就不小于p了,退出循环
}
//判断位置是否合法
if(q==NULL||k>p)//位置太大或者位置太小(比如p=0)都不合法
{
return FAILURE;
}
Node *m=(Node*)malloc(sizeof(Node)*1);//申请节点
if(NULL==m)
{
return FAILURE;//内存不足,申请失败
}
m->data=n;//将num存放在数据域
m->next=q->next;//将前一个节点的指针域里面存放的地址搬到此节点的指针域
q->next=m;//将此解此节点的地址放到前一个节点的指针域
return SUCCESS;
}
//遍历链表
void traverse_link(Node*h,void (*p)(int))//结构体指针,和函数指针(接收时要指明返回值和参数)
{
//入参判断
if(NULL==h)
{
return;//程序不会向下走了
}
Node *q=h->next;//q指向第一个节点
while(q)//只要q不为空
{
p(q->data);//由于p指向show函数,可以通过p调用show函数
q=q->next;//q继续指向下一个节点
}
}
//判断链表是否为空
int empty_link(Node*h)
{
if(NULL==h)
{
return FAILURE;//这里是失败是因为传的参数错了
}
return (h->next==NULL)? SUCCESS:FAILURE;//如果是SUCCESS表示链表为空
}
//判断链表的长度
int length_link(Node*h)
{
if(NULL==h)
{
return FAILURE;//这里失败是因为传的参数错了
}
Node*q=h->next;//让q指向第一个节点
int length=0;
while(q)//只要q不为空
{
length++;//累计
q=q->next;//q指向下一个节点
}
//q为空时退出
return length;
}
//链表查找操作
int find_link(Node*h,int n,int *r)
{
if(NULL==h||NULL==r)
{
return FAILURE;
}
Node *q=h->next;//让q指向第一个节点
int i=0,len=0,flag=0;
while(q)//只要q不为空
{
len++;//长度累计
if(q->data==n)//如果找到n
{
r[i++]=len;
flag=1;//找到
}
q=q->next;
}
if(flag==1)
{
return SUCCESS;
}
else
{
return FAILURE;
}
}
//链表反转操作
int reverse_link(Node*h)
{
if(NULL==h)
return FAILURE;
Node *p=h->next;//让p指向第一个节点
h->next=NULL;//让头节点的指针域为空
while(p)
{
Node*q=p;//让q也指向第一个节点
p=p->next;//让p指向第二个节点
q->next=h->next;//让q即第一个节点的指针域为空
h->next=q;//把第一个节点的位置放到头节点的指针域中,即让h指向第一个节点
}
return SUCCESS;
}
//链表删除操作
int delete_link(Node*h,int p,int *num)
{
//入参判断
if(NULL==h||NULL==num)
{
return FAILURE;
}
Node *q=h;//将头节点的位置记录下来
int k=1;//记录下移动的次数
while(k<p && q)//保证移动的次数小于p,并且要保证q不等于NULL,因为当p不合法时,比如p大于链表的长度+1,那q=q->next有可能给q赋值了NULL,导致程序死掉
{
q=q->next;//把指针移动到要删除位置的前一个位置
k++;//k++后就不小于p了,退出循环
}
//判断位置是否合法
if(q==NULL||k>p||q->next==NULL)//位置太大或者位置太小(比如p=0)都不合法,当q->next指向的位置为NULL,就不合法,因为不能后面没有节点需要删除
{
return FAILURE;
}
//在释放掉p这个空间之前,先把它指针域中存放的下一个节点的地址挪到前一个节点的指针域中
Node*n=q->next;//此时q指向的是第一个节点,将第二个节点的位置存放在n中,n指向第二个节点
q->next=n->next;//将第三个节点的位置存放在第一个节点的指针域中
*num=n->data;//将第二个节点的数据赋值给num,即要删除的数据
free(n);//释放第二个节点
return SUCCESS;
}
//判断前驱
int prior_link(Node*h, int num, int*p)
{
//入参判断
if(NULL==h||NULL==p)
{
return FAILURE;
}
//判断一下,如果是空链表或者只有一个节点的话肯定不存在前驱
if(NULL==h->next||NULL==h->next->next)//h->next是第一个节点,h->next->next是第一个节点的指针域
{
return FAILURE;
}
Node *k=h->next;//让k指向第一个节点
Node *q=k->next;//让q指向第二个节点
while(q)//只要q不为空
{
if(q->data==num)//如果找到
{
*p=k->data;//记录下前驱
return SUCCESS;
}
q=q->next;//如果还没有找到,q继续往后走
k=k->next;//k也继续走
}
//如果当q等于NULL,退出while循环了还没有找到
return FAILURE;
}
//链表清空操作
int clear_link(Node*h)
{
//入参判断
if(NULL==h)
{
return FAILURE;
}
Node*q=h->next;//让q指向第一个节点
while(q)//只要q不为空
{
//释放q之前,先将指针域修改
h->next=q->next;//将第三个节点的地址存放到头节点
free(q);//释放第一个节点
q=h->next;//q指向第二个节点
}
return SUCCESS;
}
//链表销毁操作
int destroy_link(Node **h)//接收指针的地址
{
//入参判断
if(NULL==h)
{
return FAILURE;
}
//注意:在销毁操作前必须进行清空操作
if((*h)->next!=NULL)//说明头指针不为空,说明还没有执行清空操作
{
return FAILURE;
}
free(*h);//释放头节点所在的位置
*h=NULL;
return SUCCESS;
}
Link.h
#ifndef _LINK_H
#define _LINK_H
#define SUCCESS 1000
#define FAILURE 1001
//表示节点的结构体
typedef struct node
{
int data;
struct node *next;
}Node;//将结构体重命名为Node
int init_link(Node **h);
int insert_link(Node*h,int p,int n);
void traverse_link(Node*h,void (*p)(int));
int empty_link(Node*h);
int length_link(Node*h);
int find_link(Node*h,int n,int *r);
int reverse_link(Node*h);
int reverse_link(Node*h);
int delete_link(Node*h,int p,int *num);
int prior_link(Node*h, int num, int*p);
int clear_link(Node*h);
int destroy_link(Node **h);
#endif
Main.c
#include <stdio.h>
#include "link.h"
#include <stdlib.h>
#include <time.h>//设置随机数的头文件
void show(int x)
{
printf("%d ",x);
}
int main()
{
//定义一个头指针
Node *head=NULL;
//初始化链表
int ret=init_link(&head);
if(SUCCESS==ret)//如果返回值是SUCCESS
{
printf("链表初始化成功\n");
}
else
{
printf("链表初始化失败\n");
}
srand(time(NULL));//设置种子,记得包含头文件time.h
//插入元素
int i,num;
for(i=0;i<10;i++)//插入10个元素
{
num=rand()%20+1;//插入随机数,数字范围在0~20
ret=insert_link(head,i+1,num);//这种插入的方式就像一个尾插法,head是头指针,头指针存放的是链表头节点的地址,也相当于链表的位置,类似数组名的作用
if(SUCCESS==ret)
{
printf("在第%d个位置插入%d成功\n",i+1,num);
}
else
{
printf("插入%d失败\n",num);
}
}
//遍历链表
traverse_link(head,show);//将头指针传过去,类似传数组名,将函数名show(即函数的地址)也传过去
printf("\n");//换行
//判断链表是否为空
ret=empty_link(head);
if(SUCCESS==ret)
{
printf("链表为空\n");
}
else
{
printf("链表不为空\n");
}
//判断链表的长度
printf("链表的长度是%d\n",length_link(head));
//链表查找操作
num=rand()%20+1;//随机生成一个数
int res[10]={0};//用来存放num的所有节点的位置
ret=find_link(head,num,res);
if(SUCCESS==ret)
{
printf("%d 出现的位置是 ",num);
for(i=0;i<10 && res[i]!=0;i++)
{
printf("%d",res[i]);
}
printf("\n");
}
else
{
printf("%d不存在\n",num);
}
//反转链表
reverse_link(head);
//遍历链表,验证反转的结果
traverse_link(head,show);//将头指针传过去,类似传数组名,将函数名show(即函数的地址)也传过去
printf("\n");//换行
//链表的删除
int p;//用来保存删除的位置
for(i=0;i<5;i++)//删除5个节点
{
p=rand()%15;
ret=delete_link(head,p,&num);//p是要删除的位置,num是要删除的数
if(SUCCESS==ret)
{
printf("删除第%d个元素成功 %d\n",p,num);
}
else
{
printf("删除第%d个元素失败\n",p);
}
}
//遍历链表,验证删除的结果
traverse_link(head,show);//将头指针传过去,类似传数组名,将函数名show(即函数的地址)也传过去
printf("\n");//换行
//判断链表的前驱
int prior;
num=rand()%30;
ret=prior_link(head,num,&prior);
if(SUCCESS==ret)
{
printf("元素%d的前驱是%d\n",num,prior);
}
else
{
printf("元素%d不存在前驱\n",num);
}
//链表清空操作
ret=clear_link(head);
if(SUCCESS==ret)
{
printf("链表清空成功\n");
}
else
{
printf("链表清空失败\n");
}
//遍历链表,验证清空的结果
traverse_link(head,show);//将头指针传过去,类似传数组名,将函数名show(即函数的地址)也传过去
printf("\n");//换行
//链表销毁操作
ret=destroy_link(&head);
if(SUCCESS==ret)
{
printf("链表销毁成功\n");
}
else
{
printf("链表销毁失败\n");
}
return 0;
}
单链表的讲解就到这里,下一节来讲讲链表的分类!
QQ交流群:963138186
本篇就到这里,下篇继续!欢迎点击下方订阅本专栏↓↓↓