一文带你看懂链表的奥秘 手把手教你实现链表的各项操作
很多人刚开始学习数据结构与算法这门课的时候都对链表有一些恐惧、懵逼,看不懂书上要表达的意思,照抄书上代码却又运行不起来,真的是10行代码9个error,把小伙伴们折磨的苦不堪言,今天博主就带领大家来探究一下线性表的顺序表达和链式表达,手把手的教大家实现链表的各种操作。
本文最后有完整的实现函数和完整的测试样例
什么的链表
说到链表,首先我们要知道链表是什么。很多小伙伴都知道链表,但是对顺序表都不太了解。其实链表是线性表的一种链式表达,而顺序表则是线性表的顺序表达。
那线性表又是什么呢?线性表是具有相同数据类型的n(n≥0)个元素的有限序列,n为表长即表的长度,当n为0时,为空表。
线性表一般有以下9个基本操作,其他的复杂操作一般依赖于这些基本操作来实现:
- InitList(&L):初始化表。构建一个空的线性表
- Length(L):求表长。返回线性表L的长度,即L中元素的个数。
- LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素。
- GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值。
- ListInsert(&L,i,e):插入操作。在表L中的第i个位置插入指定元素e。
- ListDelect(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。
- PrintList(L):输出操作。按前后顺序输出线性表L的所有元素值。
- Empty(L):判空操作。若L为空表,则返回true,否则返回false。
- DestroyList(&L):销毁操作。销毁线性表,并释放线性表L所占的内存空间。
实现操作之前的采坑指南
在实现各个操作之前,先做一些说明一些采坑指南,做一些基础的补充说明。
- 在函数参数中使用&,这是在C++中是一种方法,表示引用。如果传入变量,并且在函数内需要对变量进行改变,则需要使用引用&。当时,指针学的比较好的同学也可以使用指针来完成,这里为了简单,都将使用&,引用来进行。
- 在各个教材的代码中都会出现ElemType的数据类型,其实这是一个抽象的写法,这里的ElemType可以是任何一种数据类型,例如int,char,double等等,书上为了统一,都使用了ElemType这种抽象的写法,以至于很多同学照抄书上的代码会一直报错,运行不了。
- 线性表中的元素位序是从1开始的,而数组是从0开始。
- 基本操作的实现取决于采用哪种存储结构,储存结构不同,实现算法也不同
就先做这4点补充说明,我们下面进行实现,语音描述我们使用C++进行描述,但是没有使用过多的C++专用语法,只要有C基础的同学都能够看懂,不需要学过C++。
由于大家都对链表比较感兴趣,那本文我们就先说说链表。
链表的实现
链表,即线性表的链式表达。链式存储线性表时,不需要使用地址连续的存储单元,即它不要求逻辑上相邻的两个元素在物理位置上也相邻,它通过“链”建立起元素之间的逻辑关系,因此对线性表的插入、删除不需要移动元素,而只需要修改指针。
单链表的定义
线性表的链式存储又称作单链表,它是指通过一组任意存储单元来存储线性表中的数据元素。为了建立元素之间的线性关系,对于每个链表结点,除存放元素自身的信息外,还需要存放一个指向其后继的指针。
结点类型描述如下
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNoed,*LinkList;
其中data的数据类型可以是任意数据类型,例如int,char,或者一些结构体数据类型。。。下面实现过程中,我们统一使用int类型做演示。
typedef struct LNode{
int data;
struct LNode *next;
}LNoed,*LinkList;
创建单链表
初始化一个链表
int InitList(LinkList &L) {
L = (LNode *)malloc(sizeof(LNode)); //申请一个头结点
if (!L) return 0;
L->next = NULL; //头结点的指针域置空
return 1;
}
全部程序为:创建成功则输出1,否则输出0
#include<iostream>
using namespace std;
typedef struct LNode{
int data;
struct LNode *next;
}LNoed,*LinkList;
int InitList(LinkList &L) {
L = (LNode *)malloc(sizeof(LNode)); //申请一个头结点
if (!L) return 0;
L->next = NULL; //头结点的指针域置空
return 1;
}
int main(){
LinkList L;
cout<<InitList(L);
return 0;
}
向链表中插入数据
插入数据有很多种实现方法,这里提供4种实现方法供大家参考,其他的实现方法都和这类似。
- 一直插入,直到输入特定的值停止
1.1 头插法
1.2 尾插法 - 将一个特定的值插入到特定的位置
2.1 插在该位置之前
2.2 插在该位置之后
一直插入,直到输入特定的值停止 头插法实现
头插法是每次插入的数据都会插入到第一个数据的位置,其他的数据在其后面。
代码实现如下:直到输入6666,停止插入。
LinkList List_Insert_1(LinkList &L){
int mid;//用于接收输入
scanf("%d",&mid);//读取输入值
while(mid != 6666){//如果输入的不是6666的话,就一直插入
LNode *s = (LNode *)malloc(sizeof(LNode));//创建新结点,并分配空间
s->data = mid;//为新结点赋值
s->next = L->next;//新结点的下一个位置指向链表的第一个元素
L->next = s;//链表的下一个位置指向s
scanf("%d",&mid);//读取输入值
}
return L;
}
一直插入,直到输入特定的值停止 尾插法实现。
因为头插法插入的数据顺序是输入顺序的倒序,所有我们有时候需要使用尾插法。尾插法的话就需要一个指针来记录链表中最后一个元素的位置,来进行插入。代码实现如下:
LinkList List_Insert_2(LinkList &L){
int mid;//用于接收输入
scanf("%d",&mid);//读取输入值
LNode *r = L;//用于记录最后一个结点
while (r->next)//走到当前链表的最后
r = r->next;
while(mid != 6666){//如果输入的不是6666的话,就一直插入
LNode *s = (LNode *)malloc(sizeof(LNode));//创建新结点,并分配空间
s->data = mid;//为新结点赋值
s->next = NULL;//下一个位置为空
r->next = s;//将新结点插入到链表的最后
r = s;//链表最后的位置为s
scanf("%d",&mid);//读取输入值
}
return L;
}
将一个特定的值插入到特定的位置 插在该位置之前
//将一个特定的值插入到特定的位置 插在该位置之前
//在链表L中,在第i个位置前面插入数据e
LinkList List_Insert_3(LinkList &L,int i,int e){
int pos = 1;//用于记录位置
LNode *s = (LNode *)malloc(sizeof(LNode));//创建新结点,并分配空间
s->data = e;//赋值新结点
LNode *mid = L->next;//用来移动到指定位置
while (pos < i-1){
mid = mid->next;
pos++;
}
s->next = mid->next;
mid->next = s;
return L;
}
将一个特定的值插入到特定的位置 插在该位置之后
//将一个特定的值插入到特定的位置 插在该位置之后
//在链表L中,在第i个位置后面插入数据e
LinkList List_Insert_4(LinkList &L,int i,int e){
int pos = 1;//用于记录位置
LNode *s = (LNode *)malloc(sizeof(LNode));//创建新结点,并分配空间
s->data = e;//赋值新结点
LNode *mid = L->next;//用来移动到指定位置
while (pos < i){
mid = mid->next;
pos++;
}
s->next = mid->next;
mid->next = s;
return L;
}
打印链表中所有元素
为了同学们方便调试,验证自己的代码,我们先实现一下打印操作,这样便于同学们调试、验证
//打印链表中所有的元素
void Print_List(LinkList L){
LNode *mid = L->next;
while (mid){//只要mid不为空,则一直输出
printf("%d ",mid->data);//打印当前值
mid = mid->next;//走到下一个结点
}
cout<<endl;
}
按序号获取链表中结点的值
//按序号获取链表中结点的值
LNode *GetElem(LinkList L,int i){//获取链表L中第i个结点
int pos = 1;//用于记录序号,链表是从1开始计数的
LNode * mid = L->next;
if(i == 0)//i = 0时返回表头
return L;
if(i < 1)//i < 1时无效,返回NULL
return NULL;
while (pos < i && mid) {//移动到第i个结点
mid = mid->next;
pos++;
}
return mid;
}
按值查找链表中的结点
//按值查找表结点
//获取表中第一个值为e的结点
LNode *LocateElem(LinkList L,int e){
LNode * mid = L->next;
while (mid){
if(mid->data == e)
return mid;
mid = mid->next;
}
return NULL;
}
删除第i个结点
//删除第i个结点,返回删除的值,并且用e将删除的值传出来
int DeleteNode_1(LinkList &L,int i,int &e){
int pos = 1;//用于记录结点序号
LNode * mid = L->next;
while (pos < i-1 && mid){//移动到第i-1个结点
mid = mid->next;
pos++;
}
e = mid->next->data;//用e传输删除的值
LNode * mid_2 = mid->next;//指向第i个结点
mid->next = mid_2->next;//第i-1个结点直接指向第i+1个结点
free(mid_2);//释放第i个结点的内存
return e;//返回删除的值
}
求表长
//求表长
int List_Length(LinkList L){
int length = 0;
LNode * mid = L;
while (mid->next){
length++;
mid = mid->next;
}
return length;
}
完整的程序
完整的各个操作函数实现和测试函数如下:
#include<iostream>
using namespace std;
typedef struct LNode{
int data;
struct LNode *next;
}LNode,*LinkList;
//初始化一个单链表
int InitList(LinkList &L) {
L = (LNode *)malloc(sizeof(LNode)); //申请一个头结点
if (!L) return 0;
L->next = NULL; //头结点的指针域置空
return 1;
}
//一直插入,直到输入特定的值停止 头插法
LinkList List_Insert_1(LinkList &L){
int mid;//用于接收输入
scanf("%d",&mid);//读取输入值
while(mid != 6666){//如果输入的不是6666的话,就一直插入
LNode *s = (LNode *)malloc(sizeof(LNode));//创建新结点,并分配空间
s->data = mid;//为新结点赋值
s->next = L->next;//新结点的下一个位置指向链表的第一个元素
L->next = s;//链表的下一个位置指向s
scanf("%d",&mid);//读取输入值
}
return L;
}
//一直插入,直到输入特定的值停止 尾插法
LinkList List_Insert_2(LinkList &L){
int mid;//用于接收输入
scanf("%d",&mid);//读取输入值
LNode *r = L;//用于记录最后一个结点
while (r->next)//走到当前链表的最后
r = r->next;
while(mid != 6666){//如果输入的不是6666的话,就一直插入
LNode *s = (LNode *)malloc(sizeof(LNode));//创建新结点,并分配空间
s->data = mid;//为新结点赋值
s->next = NULL;//下一个位置为空
r->next = s;//将新结点插入到链表的最后
r = s;//链表最后的位置为s
scanf("%d",&mid);//读取输入值
}
return L;
}
//将一个特定的值插入到特定的位置 插在该位置之前
//在链表L中,在第i个位置前面插入数据e
LinkList List_Insert_3(LinkList &L,int i,int e){
int pos = 1;//用于记录位置
LNode *s = (LNode *)malloc(sizeof(LNode));//创建新结点,并分配空间
s->data = e;//赋值新结点
LNode *mid = L->next;//用来移动到指定位置
while (pos < i-1){
mid = mid->next;
pos++;
}
s->next = mid->next;
mid->next = s;
return L;
}
//将一个特定的值插入到特定的位置 插在该位置之后
//在链表L中,在第i个位置后面插入数据e
LinkList List_Insert_4(LinkList &L,int i,int e){
int pos = 1;//用于记录位置
LNode *s = (LNode *)malloc(sizeof(LNode));//创建新结点,并分配空间
s->data = e;//赋值新结点
LNode *mid = L->next;//用来移动到指定位置
while (pos < i){
mid = mid->next;
pos++;
}
s->next = mid->next;
mid->next = s;
return L;
}
//打印链表中所有的元素
void Print_List(LinkList L){
LNode *mid = L->next;
while (mid){//只要mid不为空,则一直输出
printf("%d ",mid->data);//打印当前值
mid = mid->next;//走到下一个结点
}
cout<<endl;
}
//按序号获取链表中结点的值
//获取链表L中第i个结点
LNode *GetElem(LinkList L,int i){
int pos = 1;//用于记录序号,链表是从1开始计数的
LNode * mid = L->next;
if(i == 0)//i = 0时返回表头
return L;
if(i < 1)//i < 1时无效,返回NULL
return NULL;
while (pos < i && mid) {//移动到第i个结点
mid = mid->next;
pos++;
}
return mid;
}
//按值查找表结点
//获取表中第一个值为e的结点
LNode *LocateElem(LinkList L,int e){
LNode * mid = L->next;
while (mid){
if(mid->data == e)
return mid;
mid = mid->next;
}
return NULL;
}
//删除第i个结点,返回删除的值,并且用e将删除的值传出来
int DeleteNode_1(LinkList &L,int i,int &e){
int pos = 1;//用于记录结点序号
LNode * mid = L->next;
while (pos < i-1 && mid){//移动到第i-1个结点
mid = mid->next;
pos++;
}
e = mid->next->data;//用e传输删除的值
LNode * mid_2 = mid->next;//指向第i个结点
mid->next = mid_2->next;//第i-1个结点直接指向第i+1个结点
free(mid_2);//释放第i个结点的内存
return e;//返回删除的值
}
//求表长
int List_Length(LinkList L){
int length = 0;
LNode * mid = L;
while (mid->next){
length++;
mid = mid->next;
}
return length;
}
int main(){
LinkList La,Lb;
//创建链表
cout<<"创建一个空链表 La"<<endl;
if(InitList(La))
cout<<"La创建成功"<<endl;
else
cout<<"La创建失败"<<endl;
cout<<"创建一个空链表 Lb"<<endl;
if(InitList(Lb))
cout<<"Lb创建成功"<<endl;
else
cout<<"Lb创建失败"<<endl;
cout<<"============================分隔符============================"<<endl<<endl;
//插入数据
//1. 一直插入数据,只到输入6666时停止,采用头插法。 向La中按顺序插入1 3 5 7 9 11 13 15
cout<<"一直插入数据,只到输入6666时停止,采用头插法。 向La中按顺序插入1 3 5 7 9 11 13 15"<<endl;
List_Insert_1(La);
//2. 一直插入数据,只到输入6666时停止,采用尾插法。 向La中按顺序插入2 4 6 8 10 12 14
cout<<"一直插入数据,只到输入6666时停止,采用尾插法。 向La中按顺序插入2 4 6 8 10 12 14"<<endl;
List_Insert_2(Lb);
//打印La
cout<<"La为:"<<endl;
Print_List(La);
//打印Lb
cout<<"Lb为:"<<endl;
Print_List(Lb);
//3. 在La第三个位置前插入333
cout<<"在La第三个位置前插入333"<<endl;
List_Insert_3(La,3,333);
//打印插入后的La
cout<<"插入后的La为:"<<endl;
Print_List(La);
//4. 在LB第三个位置后插入444
cout<<"在La第三个位置前插入333"<<endl;
List_Insert_4(Lb,3,444);
//打印插入后的Lb
cout<<"插入后的Lb为:"<<endl;
Print_List(Lb);
cout<<"============================分隔符============================"<<endl<<endl;
//获取La第2个值
cout<<"La的第2个值为: "<<GetElem(La,2)->data<<endl;
//获取Lb第3个值
cout<<"Lb的第3个值为: "<<GetElem(Lb,3)->data<<endl;
cout<<"============================分隔符============================"<<endl<<endl;
//获取La中值为5的结点
cout<<"La中值为5的结点的值为: "<<LocateElem(La,5)->data<<endl;
cout<<"============================分隔符============================"<<endl<<endl;
//删除La中第4个值
int e;
cout<<"La为:"<<endl;
Print_List(La);
cout<<"删除La中第4个值"<<endl;
DeleteNode_1(La,4,e);
cout<<"删除后的La为:"<<endl;
Print_List(La);
cout<<"Lb为:"<<endl;
Print_List(Lb);
cout<<"删除Lb中第6个值"<<endl;
DeleteNode_1(Lb,6,e);
cout<<"删除后的Lb为:"<<endl;
Print_List(Lb);
cout<<"============================分隔符============================"<<endl<<endl;
cout<<"La为:"<<endl;
Print_List(La);
cout<<"La的表长为:"<<List_Length(La)<<endl;
cout<<"Lb为:"<<endl;
Print_List(Lb);
cout<<"Lb的表长为:"<<List_Length(Lb)<<endl;
cout<<"============================分隔符============================"<<endl<<endl;
return 0;
}
程序运行的输入和输出为:
创建一个空链表 La
La创建成功
创建一个空链表 Lb
Lb创建成功
============================分隔符============================
一直插入数据,只到输入6666时停止,采用头插法。 向La中按顺序插入1 3 5 7 9 11 13 15
1 3 5 7 9 11 13 15 6666
一直插入数据,只到输入6666时停止,采用尾插法。 向La中按顺序插入2 4 6 8 10 12 14
2 4 6 8 10 12 14 6666
La为:
15 13 11 9 7 5 3 1
Lb为:
2 4 6 8 10 12 14
在La第三个位置前插入333
插入后的La为:
15 13 333 11 9 7 5 3 1
在La第三个位置前插入333
插入后的Lb为:
2 4 6 444 8 10 12 14
============================分隔符============================
La的第2个值为: 13
Lb的第3个值为: 6
============================分隔符============================
La中值为5的结点的值为: 5
============================分隔符============================
La为:
15 13 333 11 9 7 5 3 1
删除La中第4个值
删除后的La为:
15 13 333 9 7 5 3 1
Lb为:
2 4 6 444 8 10 12 14
删除Lb中第6个值
删除后的Lb为:
2 4 6 444 8 12 14
============================分隔符============================
La为:
15 13 333 9 7 5 3 1
La的表长为:8
Lb为:
2 4 6 444 8 12 14
Lb的表长为:7
============================分隔符============================
Process finished with exit code 0