强调:文中的所有ElemType指代任意变量,例如char、float、int甚至其它结构体变量;部分源码使用的是char,均经过作者验证可直接食用
单链表的建立
单链表的建立其实就是一个结点插入的过程,首先构建链表的结点结构体,并进行初始化,然后再不断插入结点,我们需要解决的问题是:
- 用什么插入方式?
- 在哪个位置插入?
- 程序如何设计:要不要头结点?单链表的长度是多少?链表每个元素内容又是什么?
[!插入方式的选用]
我们首先回答是选用哪种插入方式的问题?如果只看结果的话,构建单链表无所谓其哪种插入方式,只要能实现单链表的建立,都可以选用。但不同的插入方式有不同特点,在实际使用中,面对同一个问题的编程难易程度不一样,时间复杂度或者空间复杂度也会不一样。
回顾我们关于单链表的插入问题,我们一般有按位序插入和指定结点的前插和后插。按位序插入,位序说明本身就链表就已经存在,一般是用在某一个单链表的某个位置进行插入,指定结点插入分前插和后插,由于单链表的不可逆性,前插操作需要找到前驱结点,这个过程需要经过类似于按位序插入的遍历结点的过程,比较复杂,而指定结点的后插只需要指定结点,一切顺理成章,操作简单,时间复杂度低。
[!] 插入位置的选用
在哪个位置插入,无非就是,头部、尾部和中间。每次我们都将结点插入到头部,相当于一个倒序插入,而每次插入到尾部即为顺序插入,如果插入到中间则是无序,无序的线性表一般来说没有意义,因此我们用的最多的是头部插入和尾部插入,同时尾部插入又是最常用的插入方式。
[!如何设计程序]
当我们建立链表的时候,我们需要解决三个问题,第一个是要不要头结点?另一个是链表有多长?最后一个是链表的结点数据是什么?其实在这里就会出现很多结果,例如:有头结点和没有头结点,长度已知和长度未知都是有可能的,每一个结点数据是什么,是传递结点还是传递结点数据,理论上各有写法。
但在实际的应用中,我们希望我们的程序,结构简单,逻辑清晰,还有就是可拓展性强。考虑到这些,我们一般设计成一个动态长度,且传递结点元素的有头结点的单链表。
有头结点的在程序设计上相对简单,动态长度在一定程度上能够实现静态长度的功能,传递结点数据,比传递结点更具有实际应用意义也兼容传递结点的功能(此段有疑惑可以在评论区讨论或者私信我,在此不作大篇幅的论述)
结合上述回答,在此,我们设计尾插和头插两个单链表建立的程序
//首先,无论用什么方法,我们先建立单链表的结点结构体
typedef struct LNode{
//定义单链表结点类型
char data;//每一个结点存放一个数据元素
struct LNode *next;//指针指向下一个结点
}LNode,*LinkList;
LinkList List_TailInsert(LinkList *L) {
char flag = 's'; // 链表创建标志,s表示开始/继续创建链表结点
char x; // 结点数据,ElemType指代任意数据类型,例如int
*L = (LinkList)malloc(sizeof(LNode)); // 创建头结点
if (*L == NULL) {
printf("空间申请失败\n");
exit(1);//终止程序
}
(*L)->next = NULL;
LNode *s, *r