前情提要
大家在学习C语言的过程中,单链表是一部分非常重要的部分,但是往往我们在学校听老师上课时或者自己看书后,却依旧有着很多的问号,所以在接下来的文章中,我将会从我一个学生的角度出发,对单链表进行详细的讲解,有什么问题也希望大家多多提出,那么接下来我们正式开始~
目录
1.1 什么是链表:
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
-
这是百度百科上对于链表的解释。其中,如果你赶时间的话,只需要把我标注出来黄色的部分看一遍就行了。总而言之,链表是一种存储结构,由许多的结点组成,每一个结点里有一个数据,还有一个指向下一个结点的指针。
-
有可能你觉得还是很抽象不懂,那么我们接下来用图片的形式来将链表的样子完全呈现出来,我的C语言老师兼数据结构老师经常用一个例子来形容链表,那就是——风筝:
-
那么我们怎样用一个龙风筝去理解链表呢?让我们回到链表的几个基础概念中,并且将其对应着图中的风筝一一解读出来:
-
链表的头结点:图中风筝的龙头。
-
链表的尾结点:图中风筝的龙尾。
-
链表的结点:风筝上龙的身子。
-
链表一个结点存储的数据:图2风筝中不同样子的脸谱
-
链表一个结点存储的指针:图2中一个脸谱连接下一个脸谱之间的风筝骨架
由此我们便将链表的一些基本概念与风筝联系了起来,这时我们再将两者放在一起进行对比:
- 这样把两张图放在一起会不会让你基本上明白了链表长什么样?链表由什么构成?
1.2 链表的结点——风筝的骨架
- 那么问题来了,组成一个链表最重要的部分是什么呢?那就是——结点(Node)
- 在整个链表中, 一个结点(Node)起着保存数据和连接节点的作用,一个链表是由许多个结点相连而组成的,那么,我们怎么用C语言代码来实现一个结点的功能呢? 答案如下:
//以下代码可以看作是一个结点的内部结构
typedef struct MyList{
int data; //用来保存一个结点的数据
struct MyList* next;//用来保存该结点指向下一个结点的指针
}Node,*ListNode; //struct MyList=Node 用来简化定义结构体类型的过程
//struct MyList*=ListNode用来简化定义结构体指针的过程
- 单链表中我们通过结构体变量来定义一个个的结点,同时通过next指针,来将这些分散的结点一个个串起来,就像制作风筝一样,next指针就像是胶水一般,将龙风筝的骨架一个一个的粘在一起,可以这样想,你做完一个风筝结点,就将它与上一个你做完的风筝结点连接起来,当你做完时,所有的结点都连起来了,就形成了一整个龙风筝。
//如何创造一个结点的代码,可以看作是制作一个风筝结点的过程
ListNode NewNode;
NewNode=(ListNode) malloc(sizeof(Node));
//malloc的全称是memory allocation,中文叫动态内存分配,
//用于申请一块连续的指定大小的内存块区域
//以void*类型返回分配的内存区域地址
//简单来说就是我们向计算机申请一块类型为ListNode而大小为Node的区域给结点
//这也是动态单链表的具体体现,能够动态申请内存给结点
- 通过以上两段代码我们便知晓了结点结构体中内部构造和创建一个结点的具体方法,但是,不同的结点也有不同的作用,接下来我将对各种结点的作用再次进行详细的讲述
1.3 链表的头结点、尾结点——龙头、龙尾
- 首先我想先问大家一个问题,一个龙风筝,如果没有龙头,那么它还算是一个风筝吗?
- 答案当然是,它还是一个风筝,它还能够在天空中飞翔,只是没有头比较丑而已,那么我们也可以想,一个链表的头尾结点对它是不是很重要呢,还是跟风筝一样?
1.3.1 头结点——龙头(有头单链表和无头链表的的区别)
数据结构中,在单链表的第一个结点之前附设一个结点,它没有直接前驱,称之为头结点。
头结点的数据域可以不存储任何信息,头结点的指针域存储指向第一个结点的指针(即第一个元素结点的存储位置)。 头结点的作用是使所有链表(包括空表)的头指针非空,并使对单链表的插入、删除操作不需要区分是否为空表或是否在第一个位置进行,从而与其他位置的插入、删除操作一致。
- 这是百度百科对于头结点的解释,同样,如果你赶时间,只需要看我标记的部分即可。
- 我们上文说过,一个风筝或者链表的结点,应该是包含着一个“数据”和一个指向下一个的结点的“指针”,但是我们可以看到百度百科的解释中,头结点的数据域可以不存储任何信息,也就是说,在单链表中分为头结点的数据域可以存储信息和头结点的数据域可以不存储信息两种情况,也就是有头单链表和无头单链表两种情况
- 从图中,我们可以清晰地看出有头和无头链表的具体区别了,就在于头结点的数据域是否存储信息。
- 而本文之后讲述的内容都将基于有头单链表,理由就是有头链表与无头链表相比,有头链表虽然浪费空间,但易理解,在边界处较好处理,不容易出错,在插入删除等操作时不需要对头结点进行特殊操作。
1.3.2 尾结点——龙尾
数据结构中,尾结点是指链表中最后一个节点,即存储最后一个元素的节点,与之对应的是头结点,在链表的第一个结点之前附设一个结点。在单链表中,尾结点的指针一般为空,即没有保存其他节点的存储位置信息。但在双向链表中,尾结点一般指向链表中第一个节点=。
- 这里是百度百科对于尾结点的解释,这里其实一句话就已经说清楚尾结点的含义了,尾结点就是一个风筝或者链表的最后一个结点,因为它已经是最后一个结点,所以的next指针指向NULL,也就是空。
1.4 空链表的概念(有头链表)
- 前文我们说过了如果一个龙风筝没有龙头,但是有风筝的各个结点,它还是能够飞在天空中的,但是问题又来了,如果一个必定带头的龙风筝(有头链表)只有一个龙头,而没有各个结点组成的身子,那么这个龙风筝能不能飞上天空呢?(非空链表)
- 答案是不能的,因为在有头链表中,头结点是不存储任何数据的,而我想借此说明的问题就是,在有头链表中,什么样才算是空链表的状态
ListNode Head;
Head->next=NULL;
-
当我们要创建一个有头链表时,我们需要对链表进行初始化,而创建一个头结点,同时将头结点的next指向NULL就完成了这一步。
-
我们可以理解为,在我们正式制作龙风筝之前,我们先做了一个龙头,有了这个龙头,之后我们制作出的风筝的结点都可以接在这个龙头后面,但是只有单独一个龙头的时候,它是没有办法飞上天的,也就是说一个龙头是构不成一个风筝的。
-
但是我们制作出了这个龙头,也就是这个有头链表的头结点,就相当于是完成了初始化,这为下一步我们正式创建链表打下了基础。
1.5 有头单链表的创建并打印(又名风筝的制作过程)
1.5.1 有头单链表的初始化
- 有了以上的内容,我们便可以正式开始下一步有头单链表的创建。
- 首先,我们先定义一个创建链表的函数
#include<stdio.h>
#include<stdlib.h>
typedef struct MyList{
int data;
struct MyList* next;
}Node,*ListNode;
ListNode CreatList()
{
}
- 将我们上文创建头结点的代码写入
#include<stdio.h>
#include<stdlib.h>
typedef struct MyList{
int data;
struct MyList* next;
}Node,*ListNode;
ListNode CreatList()
{
ListNode Head;
Head=(ListNode) malloc(sizeof(Node));
Head->next=NULL;
Head->data=0;
}
- 这时候,我们想要创建的链表就有了头结点,相当于制作出了龙风筝的龙头,接下来我们可以开始思考怎样将一个又一个的结点接上龙头。
1.5.2 头结点和其他结点的连接(尾插法创建单链表)
- 正如真正做风筝一样,每次我们新接入的结点,实际上都是作为当前链表的尾结点接入的,我们这里通过图片来进一步理解:
- 所以只建立一个头结点对于我们创建单链表还是不够的,我们还需要一个尾结点和一个用来插入数据的新结点,因此我们可以再次完善我们的代码:
#include<stdio.h>
#include<stdlib.h>
typedef struct MyList{
int data;
struct MyList* next;
}Node,*ListNode;
ListNode CreatList()
{
ListNode NewNode,Head,Tail;
Head=(ListNode) malloc(sizeof(Node));
Head->next=NULL;
Head->data=0;
Tail=Head;//当没有其他结点时,此时的头结点也就是尾结点
}
- 最后还有最重要的一步需要我们去理解,就是将结点作为尾巴不断地插入链表中,我们应该怎么用代码去实现呢,这里先放出尾插法的代码,我们再来慢慢理解:
ListNode NewNode=(ListNode) malloc(sizeof(Node));//malloc函数
//创建新结点
scanf("%d",&x); //输入的x将作为新结点的数据
NewNode->data=x; //将x赋予新结点的数据域
NewNode->next=NULL; //将新结点的指针域指向空,因为此时它在
//链表的最后,在它之后没有结点了
Tail->next=NewNode;//将旧的尾结点的next指针指向新的结点
//因为此时的旧尾结点已经不是在链表的最后了
Tail=NewNode; //让新结点成为尾结点
- 在代码块中,我对尾插法的核心部分已经进行了详解,总而言之就是通过不停地对尾结点赋予新结点,让整个链表开始不断地边长。
- 此时,我们就得到了完整的用尾插法创建单链表的代码:
#include<stdio.h>
#include<stdlib.h>
typedef struct MyList{
int data;
struct MyList* next;
}Node,*ListNode;
//************尾插法创建单链表*************//
ListNode CreatList()
{
int n,i;
ListNode NewNode,Head,Tail;
Head=(ListNode) malloc(sizeof(Node));
Head->next=NULL;
Head->data=0;
Tail=Head;//当没有其他结点时,此时的头结点也就是尾结点
printf("请输入n的值:");
scanf("%d",&n);
for(i=1;i<=n;i++)
{
int x;
ListNode NewNode=(ListNode) malloc(sizeof(Node));//malloc函数
//创建新结点
printf("请输入第%d个值:",i);
scanf("%d",&x); //输入的x将作为新结点的数据
NewNode->data=x; //将x赋予新结点的数据域
NewNode->next=NULL; //将新结点的指针域指向空,因为此时它在
//链表的最后,在它之后没有结点了
Tail->next=NewNode;//将旧的尾结点的next指针指向新的结点
//因为此时的旧尾结点已经不是在链表的最后了
Tail=NewNode; //让新结点成为尾结点
}
return Head;
}
//************打印创建以后的单链表*************//
void Print(ListNode L)
{
ListNode TempElem=L->next;
while(TempElem)
{
printf("%d ",TempElem->data);
TempElem=TempElem->next;
}
printf("\n");
}
int main()
{
int n,m;
ListNode Head=NULL;
Head=CreatList();
Print(Head);
}
- 这里我们还在代码中加入了打印我们创建链表的部分,其实只要我们能够成功创建出链表后,打印链表只不过是对我们创建链表的一种遍历而已,相信看完代码大家都能够理解其中的意思。