目录
一、引言
1.1 为什么我们需要链表
在学习链表之前,我们已经学过了顺序表,我们知道顺序表是为了弥补数组的缺陷而设计出来的,相应的,链表是为了弥补顺序表的缺陷而设计出来的。
我们来了解一下顺序表的缺陷:
1、空间不够了需要增容,增容是要付出代价的。
如上图所示明明同样是realloc,但是两次realloc返回的地址不一样。这是因为realloc扩容有两种方式:第一种为原地扩容,即第一次扩容,这是当arr后有足够的内存可以申请用来存放数据是会执行的扩容方式。第二种为异地扩容,即第二次扩容,当arr后没有足够的内存时,会重新在另一处地址为起点申请足够的空间,再将原空间的的数据拷贝到新空间后将原空间释放,最后返回新空间的首地址。在第二种的过程中,拷贝数据是会造成消耗的。
2、避免频繁扩容,我们在顺序表满了后基本都是按倍数扩容,可能就会导致一定的空间浪费。
3、顺序表要求数据从开始位置连续存储,那么我们在头部或者中间位置插入删除数据,就需要挪动数据,效率不高。
针对顺序表的缺陷,就设计出了链表。
而我们今天讲解的是链表中的单链表。
1.2 单链表的概念与结构
概念:单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象) + 指针(指示后继元素存储位置),元素就是存储数据的存储单元,指针就是连接每个结点的地址数据。
结构:单链表(Singly Linked List)是一种常用的数据结构,它由若干个节点组成,每个节点包含两部分:数据域和指针域。数据域用于存储数据,而指针域则用于指向下一个节点的地址。单链表中每个节点只有一个指针域,指向下一个节点,最后一个节点的指针域指向 NULL,表示链表的结尾。
二、单链表的实现
2.1 战前准备
在开始设计接口前,我们先需要完成对单链表的单位——节点的定义
//单链表存贮的数据类型
typedef int SLLDataType;
//创建单链表的节点
typedef struct SLLNode
{
SLLDataType data;
//用来存贮下一个节点的地址,用于后续来操作链表
struct SLLNode* next;
}Node;
2.2 特定位置节点的插入
2.2.1 节点的创建
在对链表进行节点的插入前,我们一定需要有可用于插入的节点,插入可以分为头插和尾插还有在指定位置插入多种逻辑,而创建节点就只有一种逻辑,一份相同代码不应在程序内多次出现,所以我们应封装一个函数来实现节点的创建。
//创造一个新的结点,并完成赋值操作
static Node* SLLNodeCreat(const SLLDataType val)
{
Node* newnode = (Node*)malloc(sizeof(Node));
if (newnode == NULL)
{
exit(-1);
}
//对新节点进行赋值
newnode->data = val;
newnode->next = NULL;
//返回新节点的地址,用于后续访问
return newnode;
}
static可以避免该函数被其他文件误用。
2.2.2 头插
单链表的头插相对简单,无论单链表在插入前是否有节点,都是一个逻辑。
void SLLPushFront(Node** pplist, const SLLDataType val)
{
assert(pplist);
assert(*pplist);
Node* newnode = SLLNodeCreat(val);
newnode->next = *pplist;
*pplist = newnode;
}
一定要先执行第一步,再执行第二步,如果反过来就会失去原本pplist所指向的地址。
2.2.3 尾插
尾插相对于比较麻烦,麻烦在于它有着两种逻辑,在不同情况下需要不同处理。
当链表内原本有节点时,运用尾插的逻辑;当链表内没有节点时,就是头插的逻辑了。
void SLLPushBack(Node** pplist,const SLLDataType val)
{
assert(pplist);
Node* newnode = SLLNodeCreat(val);
//分情况讨论
//当链表内没有数据时就相当于头插,要特殊处理
if (*pplist == NULL)
{
*pplist = newnode;
}
//当链表内有数据时要先找到链表的末尾
else
{
Node* tail = *pplist;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
要做到尾插,我们就需要找到链表的末尾。我们就可以定义一个节点指针来通过遍历来寻找链表的末尾。但是遍历什么时候停止呢?我们就要抓住末尾的特征:最后一个节点的next == NULL,我们就可以以这个特征来结束循环。
找到尾后的插入操作就大同小异了。因为newnode在创建的时候next就已经设置为NULL了,就不需要对其进行操作了。
2.3 特定位置节点的删除
2.3.1 头删
头删的逻辑相对简单,与头插相似。
头删在进行处理时同样就只有一个逻辑,无需特殊处理。
void SLLPopFront(Node** pplist)
{
assert(pplist);
assert(*pplist);
Node* next = (*pplist)->next;
free(*pplist);
*pplist = next;
}
在进行删除前,我们需要定义一个指针next来记录第二个节点的位置,这样才不会丢失后面的节点。
2.3.2 尾删
尾删和尾插有着一样的性质,在不同的情况下有着尾删和头删两种逻辑。
2.3.2.1 尾删思路一
但是尾删有点特别,因为在删除时,我们需要对倒数第二个节点的next进行改变,但是因为单链表的缺陷,我们无法从后一个节点找到前一个节点,所以我们需要引入一个新指针prev来记录倒数第二个节点的位置。
void SLLPopBack(Node** pplist)
{
assert(pplist);
assert(*pplist != NULL);
//当链表内没有数据时相当于头删,要特殊处理
if ((*pplist)->next == NULL)
{
free(*pplist);
*pplist = NULL;
}
else
{
//因为单链表的缺陷,从后一个节点无法找到前一个节点
//想要对前一个节点的next进行修改,就要记录上一个节点的地址
Node* tail = *pplist;
Node* prev = NULL;
//寻找链表的最后的一个节点
while (tail->next != NULL)
{
//记录上一个节点的地址
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
prev->next = NULL;
}
}
第二步操作将tail设为NULL是为安全考虑,其实没有这一步完全可行。
2.3.2.2 尾删思路二
尾删除了上述思路外还有一种思路:
void SLLPopBack(Node** pplist)
{
assert(pplist);
assert(*pplist != NULL);
if ((*pplist)->next == NULL)
{
free(*pplist);
*pplist = NULL;
}
else
{
Node* tail = *pplist;
//通过一下访问两个节点可以做到不用记录链表末尾的前一个节点
//tail最终停在了倒数第二个节点
while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
这种思路没有引入指针prev,而是通过一下读取两个节点,将tail停在了倒数第二个节点,而非倒数第一个节点。
两种思路都是可行的,可以依照自己的理解来选择。
2.4 打印单链表
我们可以通过循环来打印单链表,通过cur来遍历,当cur==NULL时便会停止,如果单链表内没有数据,即cur==plist==NULL则不会进入循环打印数据。
void SLLPrint(const Node* plist)
{
//保留assert就不能在单链表内没有数据时打印数据
//assert(plist);
const Node* curr = plist;
//直到指向链表的末端才停下
while (curr != NULL)
{
printf("%d->", curr->data);
curr = curr->next;
}
printf("NULL\n");
}
2.5 在指定位置插入或删除节点
2.5.1 寻找特定数据
单链表和顺序表不同,单链表各个节点的地址不一定时连续的,想要对单链表完成指定位置的插入和删除,就必须知道目标节点的位置。
Node* SLLFind(Node* plist, const SLLDataType val)
{
assert(plist);
//从一个节点开始找
Node* pos = plist;
while (pos)
{
if (pos->data == val)
{
//找到了就返回该节点的地址
return pos;
}
pos = pos->next;
}
//没找到就返回空指针
return NULL;
}
如果找到了,则返回目标节点的下标,若没找到就返回空指针。
如果想要查找重复元素,可以采用迭代(循环)的方法。
Node* pos = SLLFind(plist, 10);
while (pos != NULL)
{
//......
//你想要进行的操作
pos = SLLFind(plist, 10);
}
2.5.2 插入
2.5.2.1 在指定位置前插入节点
void SLLInsertBefore(Node** pplist, Node* pos, const SLLDataType val)
{
assert(pplist);
assert(pos);
Node* newnode = SLLNodeCreat(val);
//头插需要特殊处理
if (*pplist == pos)
{
newnode->next = *pplist;
*pplist = newnode;
}
else
{
//找到目标节点的前一个节点
Node* prev = *pplist;
while (prev->next != pos)
{
prev = prev->next;
}
newnode->next = pos;
prev->next = newnode;
}
}
在指定位置前插入会包含头插的情况,所以我们必须对头插的情况特殊处理。
并且它有着与尾插类似的窘境,我们必须要找到指定位置的前一个节点,然后改变其的next。我们同样需要prev来记录目标节点上个节点的位置,然后完成插入操作。
当prev->next==pos时,就说明prev已经到达了目标节点的上一个节点。
2.5.2.2 在指定位置后插入节点
单链表的缺陷限制了单链表从后往前寻找数据,但从前往后寻找数据并没有困难,所以在指定位置后插入节点比在指定位置前插入节点要轻松许多。
void SLLInsertAfter(Node* pos, const SLLDataType val)
{
assert(pos);
Node* newnode = SLLNodeCreat(val);
newnode->next = pos->next;
pos->next = newnode;
}
2.5.3 删除
2.5.3.1 删除指定位置的节点
void SLLErase(Node** pplist, Node* pos)
{
assert(pplist);
assert(*pplist);
//头删特殊处理
if (*pplist == pos)
{
*pplist = pos->next;
free(pos);
pos = NULL;
}
else
{
Node* prev = *pplist;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
删除指定位置的节点同样需要对头删进行特殊处理,也同样需要借助prev指针来记录上一个节点的地址。
2.5.3.2 删除指定位置后的节点
删除指定位置后的节点也同样相比删除指定位置的节点要轻松许多。
void SLLEraseAfter(Node* pos)
{
assert(pos);
//保证指定位置的后面有数据可删
assert(pos->next);
//记录pos->next的位置,用来销毁空间
Node* next = pos->next;
pos->next = next->next;
free(next);
next = NULL;
}
相比于另外两种指定位置的插入和删除,这里更推荐在指定位置后插入,在指定位置后删除,这两种更简单也更合理,符合单链表的特点。
2.6 销毁单链表
销毁单链表也需要我们从第一个节点开始,通过遍历找到每一个节点的地址后再释放空间。在这个过程中,我们同样需要记录将被销毁节点的下一个节点,用于后续节点的销毁。
void SLLDestory(Node** pplist)
{
assert(pplist);
assert(*pplist);
Node* curr = *pplist;
//记录下一个节点的地址,防止一个节点销毁后找不到下一个节点
Node* next = NULL;
while (curr)
{
next = curr->next;
free(curr);
curr = next;
}
//将plist设为空,指示该链表已经没有数据
*pplist = NULL;
}
在所有节点被销毁完毕后,将plist设为NULL,代表着这个单链表已经没有节点了。
三、小结
3.1 顺序表与单链表的对比
顺序表 | 单链表 | ||
优点 | 缺点 | 优点 | 缺点 |
支持随机访问。 | 空间不够了需要扩容,扩容是有消耗。 | 按需申请空间,不用了就释放空间(更合理的使用了空间)。 | 每个一个数据,都要存一个指针去链接后面数据节点。不支持随机访问(用下标直接访问第i个)。 |
头部或者中间位置的插入删除,需要挪动数据,挪动数据也是有消耗的。 | 头部中间插入删除数据,不需要挪动数据。 | ||
为了避免频繁扩容,一次一般都是按倍数扩容,可能存在一定空间浪费。 | 不存空间浪费。 |
注:有些算法是需要结构支持随机访问的,比如:二分查找和优化的快速排序,等等。
所以,顺序表和单链表各有其的好处,我们两种都需要学习,而不是单一的学习一种。
3.2 结语
今天我们讲解的是链表中的单链表,并且是没有哨兵位的单链表。但是我们可以发现,单链表也有很大的缺陷,如不能从后往前访问,这就需要我们来学习双向链表来解决这个问题。数据结构需要不断的学习和练习,理论要和实践并存。
以上为我个人对单链表的认识,如有错误还请各位指正!
四、源代码
4.1 main.c
#include "SinLinkedList.h"
static void SLLTest1()
{
Node* plist = NULL;
SLLPushBack(&plist, 1);
SLLPushBack(&plist, 2);
SLLPushBack(&plist, 3);
SLLPushBack(&plist, 4);
SLLPushBack(&plist, 5);
SLLPrint(plist);
SLLPopBack(&plist);
SLLPopBack(&plist);
SLLPopBack(&plist);
SLLPopBack(&plist);
//SLLPopBack(&plist);
//SLLPopBack(&plist);
//SLLPopBack(&plist);
//SLLPopBack(&plist);
//SLLPopBack(&plist);
SLLPushBack(&plist, 10);
SLLPushBack(&plist, 20);
SLLPrint(plist);
}
static void SLLTest2()
{
Node* plist = NULL;
SLLPushFront(&plist, 80);
SLLPushFront(&plist, 100);
SLLPushFront(&plist, 120);
SLLPushFront(&plist, 170);
SLLPrint(plist);
SLLPushBack(&plist, 1);
SLLPushBack(&plist, 2);
SLLPushBack(&plist, 3);
SLLPushBack(&plist, 4);
SLLPushBack(&plist, 5);
SLLPrint(plist);
SLLPushFront(&plist, 10);
SLLPushFront(&plist, 20);
SLLPushFront(&plist, 30);
SLLPushFront(&plist, 40);
SLLPushFront(&plist, 50);
SLLPrint(plist);
}
static void SLLTest3()
{
Node* plist = NULL;
SLLPushFront(&plist, 10);
SLLPushFront(&plist, 20);
SLLPushFront(&plist, 30);
SLLPushFront(&plist, 40);
SLLPrint(plist);
SLLPopBack(&plist);
//SLLPopBack(&plist);
SLLPopBack(&plist);
SLLPrint(plist);
SLLPushFront(&plist, 50);
SLLPushFront(&plist, 60);
SLLPushFront(&plist, 70);
SLLPushFront(&plist, 80);
SLLPrint(plist);
SLLPopFront(&plist);
SLLPopFront(&plist);
SLLPopFront(&plist);
SLLPrint(plist);
SLLPopFront(&plist);
//SLLPopFront(&plist);
SLLPopFront(&plist);
SLLPopFront(&plist);
SLLPushBack(&plist, 1);
SLLPushBack(&plist, 2);
SLLPushBack(&plist, 3);
SLLPrint(plist);
}
static void SLLTest4()
{
Node* plist = NULL;
SLLPushFront(&plist, 10);
SLLPushFront(&plist, 20);
SLLPushFront(&plist, 30);
SLLPushFront(&plist, 40);
SLLPrint(plist);
Node* pos = SLLFind(plist, 10);
if (pos)
{
printf("找到了,地址为%p\n", pos);
}
else
{
printf("没找到\n");
}
pos = SLLFind(plist, 70);
if (pos)
{
printf("找到了,地址为%p\n", pos);
}
else
{
printf("没找到\n");
}
}
static void SLLTest5()
{
Node* plist = NULL;
SLLPushBack(&plist, 10);
SLLPushBack(&plist, 20);
SLLPushBack(&plist, 30);
SLLPushBack(&plist, 40);
SLLPrint(plist);
Node* pos = SLLFind(plist, 40);
SLLInsertBefore(&plist, pos, 35);
SLLPrint(plist);
pos = SLLFind(plist, 10);
SLLInsertBefore(&plist, pos, 5);
SLLPrint(plist);
//pos = SLLFind(plist, 100);
//SLLInsertBefore(&plist, pos, 95);
pos = SLLFind(plist, 10);
SLLInsertAfter(pos, 15);
SLLPrint(plist);
pos = SLLFind(plist, 40);
SLLInsertAfter(pos, 45);
SLLPrint(plist);
}
static void SLLTest6()
{
Node* plist = NULL;
SLLPushBack(&plist, 10);
SLLPushBack(&plist, 20);
SLLPushBack(&plist, 30);
SLLPushBack(&plist, 40);
SLLPrint(plist);
Node* pos = SLLFind(plist, 40);
SLLErase(&plist, pos);
SLLPrint(plist);
pos = SLLFind(plist, 10);
SLLErase(&plist, pos);
SLLPrint(plist);
SLLPushBack(&plist, 70);
SLLPushBack(&plist, 80);
SLLPushBack(&plist, 90);
SLLPushBack(&plist, 100);
SLLPrint(plist);
pos = SLLFind(plist, 20);
SLLEraseAfter(pos);
SLLPrint(plist);
//pos = SLLFind(plist, 90);
//SLLEraseAfter(pos);
//SLLPrint(plist);
//pos = SLLFind(plist, 100);
//SLLEraseAfter(pos);
SLLPrint(plist);
}
static void SLLTest7()
{
Node* plist = NULL;
SLLPushBack(&plist, 10);
SLLPushBack(&plist, 20);
SLLPushBack(&plist, 30);
SLLPushBack(&plist, 40);
SLLPrint(plist);
SLLDestory(&plist);
SLLPopBack(&plist);
//Node* pos = SLLFind(plist, 10);
//while (pos != NULL)
//{
// //......
// //你想要进行的操作
// pos = SLLFind(plist, 10);
//}
}
int main()
{
//SLLTest1();
//SLLTest2();
//SLLTest3();
//SLLTest4();
//SLLTest5();
SLLTest6();
//SLLTest7();
return 0;
}
4.2 SinLinkedList.c
#include "SinLinkedList.h"
void SLLPrint(const Node* plist)
{
//保留assert就不能在单链表内没有数据时打印数据
//assert(plist);
const Node* curr = plist;
//直到指向链表的末端才停下
while (curr != NULL)
{
printf("%d->", curr->data);
curr = curr->next;
}
printf("NULL\n");
}
//创造一个新的结点,并完成赋值操作
static Node* SLLNodeCreat(const SLLDataType val)
{
Node* newnode = (Node*)malloc(sizeof(Node));
if (newnode == NULL)
{
exit(-1);
}
//对新节点进行赋值
newnode->data = val;
newnode->next = NULL;
//返回新节点的地址,用于后续访问
return newnode;
}
void SLLPushBack(Node** pplist,const SLLDataType val)
{
assert(pplist);
Node* newnode = SLLNodeCreat(val);
//分情况讨论
//当链表内没有数据时就相当于头插,要特殊处理
if (*pplist == NULL)
{
*pplist = newnode;
}
//当链表内有数据时要先找到链表的末尾
else
{
Node* tail = *pplist;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
void SLLPopBack(Node** pplist)
{
assert(pplist);
assert(*pplist != NULL);
//当链表内没有数据时相当于头删,要特殊处理
if ((*pplist)->next == NULL)
{
free(*pplist);
*pplist = NULL;
}
else
{
//因为单链表的缺陷,从后一个节点无法找到前一个节点
//想要对前一个节点的next进行修改,就要记录上一个节点的地址
Node* tail = *pplist;
Node* prev = NULL;
//寻找链表的最后的一个节点
while (tail->next != NULL)
{
//记录上一个节点的地址
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
prev->next = NULL;
}
}
//void SLLPopBack(Node** pplist)
//{
// assert(pplist);
// assert(*pplist != NULL);
//
// if ((*pplist)->next == NULL)
// {
// free(*pplist);
// *pplist = NULL;
// }
// else
// {
// Node* tail = *pplist;
//
// //通过一下访问两个节点可以做到不用记录链表末尾的前一个节点
// //tail最终停在了倒数第二个节点
// while (tail->next->next != NULL)
// {
// tail = tail->next;
// }
//
// free(tail->next);
// tail->next = NULL;
// }
//}
void SLLPushFront(Node** pplist, const SLLDataType val)
{
assert(pplist);
assert(*pplist);
Node* newnode = SLLNodeCreat(val);
newnode->next = *pplist;
*pplist = newnode;
}
void SLLPopFront(Node** pplist)
{
assert(pplist);
assert(*pplist);
Node* next = (*pplist)->next;
free(*pplist);
*pplist = next;
}
Node* SLLFind(Node* plist, const SLLDataType val)
{
assert(plist);
//从一个节点开始找
Node* pos = plist;
while (pos)
{
if (pos->data == val)
{
//找到了就返回该节点的地址
return pos;
}
pos = pos->next;
}
//没找到就返回空指针
return NULL;
}
void SLLInsertBefore(Node** pplist, Node* pos, const SLLDataType val)
{
assert(pplist);
assert(pos);
Node* newnode = SLLNodeCreat(val);
//头插需要特殊处理
if (*pplist == pos)
{
newnode->next = *pplist;
*pplist = newnode;
}
else
{
//找到目标节点的前一个节点
Node* prev = *pplist;
while (prev->next != pos)
{
prev = prev->next;
}
newnode->next = pos;
prev->next = newnode;
}
}
void SLLInsertAfter(Node* pos, const SLLDataType val)
{
assert(pos);
Node* newnode = SLLNodeCreat(val);
newnode->next = pos->next;
pos->next = newnode;
}
void SLLErase(Node** pplist, Node* pos)
{
assert(pplist);
assert(*pplist);
//头删特殊处理
if (*pplist == pos)
{
*pplist = pos->next;
free(pos);
pos = NULL;
}
else
{
Node* prev = *pplist;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
void SLLEraseAfter(Node* pos)
{
assert(pos);
//保证指定位置的后面有数据可删
assert(pos->next);
//记录pos->next的位置,用来销毁空间
Node* next = pos->next;
pos->next = next->next;
free(next);
next = NULL;
}
void SLLDestory(Node** pplist)
{
assert(pplist);
assert(*pplist);
Node* curr = *pplist;
//记录下一个节点的地址,防止一个节点销毁后找不到下一个节点
Node* next = NULL;
while (curr)
{
next = curr->next;
free(curr);
curr = next;
}
//将plist设为空,指示该链表已经没有数据
*pplist = NULL;
}
4.3 SinLinkedList.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
//单链表存贮的数据类型
typedef int SLLDataType;
//创建单链表的节点
typedef struct SLLNode
{
SLLDataType data;
//用来存贮下一个节点的地址,用于后续来操作链表
struct SLLNode* next;
}Node;
//依此打印单链表内的数据
extern void SLLPrint(const Node* plist);
//从单链表的尾部开始插入数据
extern void SLLPushBack(Node** pplist, const SLLDataType val);
//在单链表的尾部开始删除数据
extern void SLLPopBack(Node** pplist);
//从单链表的头部开始插入数据
extern void SLLPushFront(Node** pplist, const SLLDataType val);
//从单链表的头部开始删除数据
extern void SLLPopFront(Node** pplist);
//从单链表中查找数据
extern Node* SLLFind(Node* plist, const SLLDataType val);
//在指定位置的前面插入数据
extern void SLLInsertBefore(Node** pplist, Node* pos, const SLLDataType val);
//在指定位置后面插入数据
//相较于在前面插入数据,在后面插入数据更符合单链表的特性,也更加简单
extern void SLLInsertAfter(Node* pos, const SLLDataType val);
//删除指定数据
extern void SLLErase(Node** pplist, Node* pos);
//删除指定位置后面的数据
extern void SLLEraseAfter(Node* pos);
//销毁单链表
extern void SLLDestory(Node** pplist);