目录
一、初识单链表
单链表是如何构造的:
单链表如何解决顺序表中的问题:
二、单链表的初始定义
三、尾插和头插
3.1 新建结点CreateNode
3.2 打印SLTPrint
3.3 尾插SLTPushBack
3.4 头插SLTPushFront
四、尾删和头删
4.1 尾删SLTPopBack
4.2 头删SLTPopFront
五、某位置前插入删除
5.1 查找SLTFind
5.2 某位置前插入SLTInsert
5.3 某位置删除SLTErase
六、某位置后插入删除
七、 assert断言
7.1 SLTPrint断言
7.2 SLTPushBack 和 SLTPushFront 断言
7.3 SLTPopBack 和 SLTPopFront 断言
7.4 SLTInsert 和 SLTErase 断言
顺序表的问题:
1.尾部插入效率还不错,头部或者中间插入删除,需要挪动数据,效率低下
2. size 满了后只能扩容,扩容是有一定消耗的;扩容一般存在一定的空间浪费
我们总结了顺序表的问题,下面我们学习的单链表就可以让这些问题迎刃而解。
学习要求:掌握C语言的指针、二级指针、动态开辟内存的知识
一、初识单链表
我们首先来回顾一下顺序表中的结构体成员:
顺序表中单个结构体存储着顺序表的表头指针、整个顺序表的数据容量、实际存储的数据容量......
单链表是如何构造的:
链表每个结点会存储一个数据,同时会存储一个指针,这个指针指向下一个结点。
单链表如何解决顺序表中的问题:
链表的每一个结点都是 malloc 出来的,他们的地址并不是连续分配的,如果从头部或者中间插入删除,只需要改动单个结点,无需挪动整个 table ;因为链表的结点是一个一个开辟的,所以也不存在顺序表的空间浪费。
下面我们来看一下链表实际的结构:
在这里我们强调不需要太过于关注链表的物理结构,我们的注意点应该集中在其是个结构体。
二、单链表的初始定义
这里还是和顺序表一样的套路,把它当成一个工程去做,当然就要分文件啦。
其次,在 .h 文件中我们要做我们的准备工作:
接下来我们来看一下我们需要对链表做的操作:
三、尾插和头插
在学习尾插之前,我们需要先思考一个问题,为什么在我们的操作中有时候需要传指针而有时候需要传二级指针呢(下面以尾插为例)?
如果要探索这个问题的答案,我们一定要知道我们在尾插时是需要改变指针的值的。如果我们调用函数需要改变 int 的值,我们是不是需要传 int* 来改变我们的实参,而在这里我们需要改变的是SLTDataType* 的值,所以就需要我们传入指针的地址来对指针的值进行修改。
3.1 新建结点CreateNode
我们在这还需要写一个函数,这个函数可以说是链表插入元素的精髓了,我们在前面了解到了链表插入元素是一个结点一个结点地 malloc 出来的,所以我们在这里写一个 CreateNode 函数来创建我们的新结点,可以极大地减少我们下面的代码量。
3.2 打印SLTPrint
为了方便后续的检测,我们也要先把显示链表内容的函数定义出来:
3.3 尾插SLTPushBack
3.4 头插SLTPushFront
这里要注意的是我们先让 NewNode 作为头结点来指向 *pphead (即phead) 以此来找到之前第一个结点的地址,然后我们再将 *pphead 指向 NewNode,在以后遇到需要头插的题目,我们也要使用这种顺序。
四、尾删和头删
4.1 尾删SLTPopBack
尾删肯定要找到最后一个元素,让指向最后一个元素的指针直接指向 NULL ,然后再 free 掉最后一个结点,再查找会后一个结点时,我们需要改变的是指向它地址的指针的值,所以我们就要用 tail->next->next != NULL 作为寻找条件:
欸,我们从图中和代码中不难发现,当 tail 的后两个元素为 NULL 时才能进行判断,那么如果链表中只有一个结点(tail->next == NULL),这时候应该怎么办呢?当然是单独进行判断啦!
下面我们来看一下完整代码:
但是需要注意的是,如果我们在一开始就定义了 tail 指针指向 *pphead ,当 tail == NULL 时, free 掉 tail ,这样的写法在我们删掉最后一个结点时会在 Print() 函数中报错,错误代码如下:
这是为什么呢?原因是如果提前用 tail 接收 *pphead,那么在 free(tail) 后也应该将 *pphead 置为空,如果仅仅将 tail 置空,那我们的 *pphead 就成了野指针,这样当然是不可取的。
4.2 头删SLTPopFront
头删就相对简单很多,只需要改变 *pphead 的指向,再 free 就好。
五、某位置前插入删除
在学习某位置插入删除前,我们需要知道的是我们的“某位置”应该如何定位,比较某个结点与我们的目标值?显然是不行的,当有多个结点的值重复时这个想法自然就会被推翻,那我们应该怎么办呢?我们也要设计一个查找函数,将第一个查找到的结点地址返回,再将该地址传入我们的插入删除函数中。
5.1 查找SLTFind
当我们遍历完整个链表却没找到想要的元素时,说明链表中没有该元素,返回 NULL 。
5.2 某位置前插入SLTInsert
首先我们要判断这里是不是头插,如果是头插,我们直接调用头插函数,如果不是,我们继续我们的移形换影大法,插入结点。
在改变 cur->next 前,我们要用临时变量 tmp 接收 cur 下一个结点的地址,这样才能在后面找到。
5.3 某位置删除SLTErase
因为我们的 SLTFind 函数可以定位到 val==x 的当前结点,所以我们可以用这一特点删除指定位置的元素。当我们要删除的是第一个结点时,我们同样可以调用头删函数。
六、某位置后插入删除
这里思路比较简单,我们来简单看一下代码:
七、 assert断言
最后我们当然不能忘记断言,下面我们一起来看看每个函数中需要的断言:
7.1 SLTPrint断言
SLTPrint不用断言,因为我们已经设置了当链表为空时只需打印 NULL
7.2 SLTPushBack 和 SLTPushFront 断言
这两个函数只需要断言 pphead 这个二级指针,因为我们是允许链表为空时的头插尾插的。
7.3 SLTPopBack 和 SLTPopFront 断言
这两个函数不仅要断言 pphead 还要断言 *pphead ,因为当链表中有结点时才可以Pop
7.4 SLTInsert 和 SLTErase 断言
插入和删除函数不仅需要像上面两个函数一样断言 pphead 和 *pphead ,还要断言 pos 。
下面是我的单链表源代码库,包含了对每个函数测试用的代码,需要的uu可以自行查看:
手撕单链表 - Gitee.com