1. 链表的概念及结构
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
链表的结构跟火车车厢相似,淡季时车次的车厢会相应减少,旺季时车次的车厢会额外增加几节。只需要将火车里的某节车厢去掉或加上,不会影响其他车厢,每节车厢都是独立存在的。
车厢是独立存在,且每节车厢都有车门。想象一下这样的场景,假设每节车厢的车门都是锁上的状态,需要不同的钥匙才能解锁,每次只能携带一把钥匙的情况下如何从车头走到车尾?
最简单的做法:每节车厢里都放一把下一节车厢的钥匙。
在链表里,每节“车厢”是什么样的呢?
与顺序表不同的是,链表里的每节“车厢”都是独立申请下来的空间,我们称之为“结点/节点”。节点的组成主要有两个部分:当前节点要保存的数据和保存下一个节点的地址(指针变量)。图中指针变量 plist 保存的是第一个节点的地址,我们称 plist 此时“指向”第一个节点,如果我们希望 plist “指向”第二个节点时,只需要修改 plist 保存的内容为 0x0012FFA0。
为什么还需要指针变量来保存下一个节点的位置?
链表中每个节点都是独立申请的(即需要插入数据时才去申请一块节点的空间),我们需要通过指针变量来保存下一个节点位置才能从当前节点找到下一个节点。
假设当前保存的节点为整型:
// 定义节点的结构
// 数据 + 指向下一个节点的指针
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data; // 存储的节点数据
struct SListNode* next; // 指针变量用来保存下一个节点的地址
}SLTNode;
当我们想要保存一个整型数据时,实际是向操作系统申请了一块内存,这个内存不仅要保存整型数据,也需要保存下一个节点的地址(当下一个节点为空时保存的地址为空)。
当我们想要从第一个节点走到最后一个节点时,只需要在前一个节点拿上下一个节点的地址(下一个节点的钥匙)就可以了。
给定的链表结构中,如何实现节点从头到尾的打印?
补充:
1. 链式结构在逻辑上是连续的,在物理结构上不一定连续;
2. 节点一般是从堆上申请的;
3. 从堆上申请来的空间,是按照一定策略分配出来的,每次申请的空间可能是连续,可能不连续。
2. 单链表的实现
2.1 链表的打印
// 链表的打印
void SLTPrint(SLTNode* phead)
{
SLTNode* pcur = phead;
while (pcur) // pcur != NULL
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
测试程序:首先我们得先开辟新的空间,然后创建几个新的节点,再把所创建的新节点关联起来,最后调用打印函数接口,打印数据(注意:最后一个节点存储的下一个节点的地址要为NULL)
void SListTest01()
{
// 链表是由一个一个的节点组成
// 创建几个节点
SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
node1->data = 1;
SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
node2->data = 2;
SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
node3->data = 3;
SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
node4->data = 4;
// 将四个节点连接起来
node1->next = node2;
node2->next = node3;
node3->next = node4;
node4->next = NULL;
// 调用链表的打印
SLTNode* plist = node1;
SLTPrint(plist);
}
int main()
{
SListTest01();
return 0;
}
运行结果:
2.2 链表的尾插
// 链表的尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
如果我们要在链表的最后一个节点尾插一个新的节点,首先要先找到尾节点,再将尾节点和新节点连接起来。不过,在插入之前我们得先申请一个新的节点空间。这里需要注意的一个点:我们形参要改变实参必须要传地址(即要用指针来接收),那为什么形参不用一级指针而用二级指针呢?因为,我们创建的节点是一个结构体指针 SLTNode* ,所以我们得用二级指针来接收。
知道了原理,我们接下来就可以编写程序,首先得判断头节点的地址 &plist (pphead)不能为空,然后申请一个新的节点空间。好,接下来我们要分两种情况:1. 链表是空链表;2. 链表是非空链表。空链表:我们直接把新节点作为头节点;非空链表:我们就正常的尾插。
尾插接口函数:
// 申请节点函数
SLTNode* SLTBuyNode(SLTDataType x)
{
SLTNode* newNode = (SLTNode*)malloc(sizeof(SLTNode));
if (newNode == NULL)
{
perror("malloc fail!");
exit(1); // 正常退出是0,非正常退出是非0
}
newNode->data = x;
newNode->next = NULL;
return newNode;
}
// 链表的尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead); // pphead 不能为NULL
// *pphead 就是指向第一个节点的指针
// 空链表和非空链表
// 申请新的节点
SLTNode* newNode = SLTBuyNode(x);
if (*pphead == NULL)
{
*pphead = newNode;
}
else
{
// 找尾
SLTNode* ptail = *pphead;
while (ptail->next) // ptail->next != NULL
{
ptail = ptail->next;
}
// ptail指向的就是尾节点
ptail->next = newNode;
}
}
测试程序:尾插 1 2 3 4
void SListTest02()
{
SLTNode* plist = NULL;
// 测试尾插
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPrint(plist);
}
int main()
{
SListTest02();
return 0;
}
运行结果:
2.3 链表的头插
// 链表的头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);
头插我们也得分两种情况:1. 链表是空链表;2. 链表是非空链表。
首先得判断头节点的地址 &plist (pphead)不能为空,然后申请一个新的节点空间。让新节点 newNode 的下一个节点的地址 newNode->next 指向头节点 *pphead ,再新节点 newNode 作为新的头节点 *pphead。
头插接口函数:
// 链表的头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
// 空链表和非空链表都能使用此方法
assert(pphead); // pphead 不能为NULL
// 申请新的节点
SLTNode* newNode = SLTBuyNode(x);
newNode->next = *pphead;
*pphead = newNode;
}
测试程序:尾插 1 2 3 4,再头插 6 7 8
void SListTest02()
{
SLTNode* plist = NULL;
// 测试尾插
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPrint(plist);
// 测试头插
SLTPushFront(&plist, 6);
SLTPrint(plist);
SLTPushFront(&plist, 7);
SLTPrint(plist);
SLTPushFront(&plist, 8);
SLTPrint(plist);
}
int main()
{
SListTest02();
return 0;
}
运行结果:
我们测试一下空链表是不是也可行:
void SListTest02()
{
SLTNode* plist = NULL;
// 测试头插
SLTPushFront(&plist, 6);
SLTPrint(plist);
SLTPushFront(&plist, 7);
SLTPrint(plist);
SLTPushFront(&plist, 8);
SLTPrint(plist);
}
int main()
{
SListTest02();
return 0;
}
运行结果:可行!
2.3 链表的尾删
// 链表的尾删
void SLTPopBack(SLTNode** pphead);
链表节点的删除不是单纯把要删除的节点给free掉,而是要找最后一个节点的前一个节点,把这个节点的下一个节点的地址置为NULL。
尾删我们也要分两种情况:1. 只有单节点;2. 多节点。单节点:我们直接把头节点给释放掉,然后置为空。多节点:我们先找到最后一个节点,怎么找呢?定义一个 ptail 结构体指针,遍历链表,当 patil->next 为 NULL ,说明此时的 ptail 就是尾节点。我们还要找尾节点的前一个节点,这时我们就要定义一个 prev 结构体指针,把找尾节点的 prev = ptail 保存一下,最后 patil = NULL,说明前一次找的就是尾节点的前一个节点。
// 链表的尾删
void SLTPopBack(SLTNode** pphead)
{
// 链表不能为空
assert(pphead && *pphead);
// 链表只有一个节点
if ((*pphead)->next == NULL) // -> 优先级高于 *
{
free(*pphead);
*pphead = NULL;
}
else
{
// 链表有多个节点
SLTNode* prev = *pphead;
SLTNode* ptail = *pphead;
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
free(ptail);
ptail = NULL;
prev->next = NULL;
}
}
测试程序:尾插 1 2 3 4,然后尾删,一直删到只剩一个节点,多节点和单节点一起判断
void SListTest02()
{
SLTNode* plist = NULL;
// 测试尾插
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPrint(plist);
// 测试尾删
SLTPopBack(&plist);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPrint(plist);
}
int main()
{
SListTest02();
return 0;
}
运行结果:
2.4 链表的头删
// 链表的头删
void SLTPopFront(SLTNode** pphead);
在释放头节点之前,我们要先用一个指针 next 保存头节点 *pphead 下一个节点的地址,然后释放头节点,再把 next 指针作为新的头节点。
// 链表的头删
void SLTPopFront(SLTNode** pphead)
{
// 链表不能为空
assert(pphead && *pphead);
// 多个节点和单个节点都能使用
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
测试程序:
void SListTest02()
{
SLTNode* plist = NULL;
// 测试尾插
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPrint(plist);
// 测试头删
SLTPopFront(&plist);
SLTPrint(plist);
SLTPopFront(&plist);
SLTPrint(plist);
SLTPopFront(&plist);
SLTPrint(plist);
SLTPopFront(&plist);
SLTPrint(plist);
}
int main()
{
SListTest02();
return 0;
}
运行结果:尾插 1 2 3 4,然后头删,一直删到只剩一个节点,多节点和单节点一起判断
2.5 链表的查找
// 链表的查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
遍历链表数据,找到了,就返回对应的结构体指针,没有找到,就返回NULL。
// 链表的查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
测试程序:
void SListTest02()
{
SLTNode* plist = NULL;
// 测试尾插
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPrint(plist);
// 测试查找
SLTNode* find = SLTFind(plist, 3);
if (find == NULL)
{
printf("没有找到!\n");
}
else
{
printf("找到了!\n");
}
}
int main()
{
SListTest02();
return 0;
}
运行结果: