一.定义
一种线性存储结构,元素不连续存放,每个节点包括两部分:数据域 + 后继指针,指针指向下一个结点;最后一个节点指针为NULL。
二.节点结构和性质

**补充-头节点:**可以设置虚拟头节点,简化边界处理;头指针指向链表开头。
单链表的性质
1.链表在逻辑上是连续的,物理结构上不一定连续。
2.结点一般是从堆上申请的。
3.从堆上申请的空间,是按照一定策略分配出来的,每次申请的空间可能连续,可能不连续。
三.链表新节点的创建
SLTNode* SLTbuyNode(SLTDataType x)
{
//根据x创建节点
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail!");
exit(1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
四.单链表结点的定义
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;//存储的结构
struct SLTNode* next;//指向下一个结点
}SLTNode;
五.单链表的打印
void SLTPrint(SLTNode* phead)
{
SLTNode* pcur = phead;
while (pcur)//pcur != NULL
{
printf("%d ->", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
六.单链表的尾插 - 时间复杂度O(N)
直接把新节点插到链表最后面即可:
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTbuyNode(x);
//链表为空
if (*pphead == NULL)
{
*pphead = newnode;
}
else // 链表不为空才去找尾
{
//找尾
SLTNode* ptail = *pphead;
while (ptail->next)
{
ptail = ptail->next;
}
ptail->next = newnode;
}
}
七.单链表的头插 - 时间复杂度O(1)
把新结点插到链表最开头,新节点变头节点。
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
//创建新节点,存入数据x。
SLTNode* newnode = SLTbuyNode(x);
//新节点next指向原来链表的第一个节点。
//这里的*phead是原始头指针,代表链表旧首节点
newnode->next = *pphead;
//更新头指针,让头指针指向新节点,新节点成为新的表头
*pphead = newnode;
}
八.单链表的尾删 - 时间复杂度O(N)
删除链表的最后一个结点,单链表只能从头往后走,不能从尾巴往回走,不光要删除尾结点,还要删除倒数第二个节点,把它的next置为NULL。
void SLTPopBack(SLTNode** pphead)
{
assert(pphead);
//只有一个节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
return;
}
SLTNode* prev = NULL;
SLTNode* ptail = *pphead;
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
prev->next = NULL;
free(ptail);
ptail = NULL;
}
九.单链表的头删 - 时间复杂度O(1)
删除链表第一个结点(头结点)。
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
十.单链表的查找
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
十一.在指定位置pos前插入数据
在 pos 结点前面插入新结点。单链表没有向前访问的能力,无法直接拿到 pos 的前一个结点,所以必须从链表头部开始循环遍历,找到 pos 的前驱结点 prev。如果 pos 本身就是链表的头结点,前面没有结点,直接复用头插逻辑。找到前驱后,让prev->next指向新结点,再让新结点的next指向 pos,完成链表衔接。整段操作需要遍历链表,时间复杂度 O (N)。如果不单独处理 pos 等于头结点的情况,循环会找不到前驱,代码直接出错。
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && pos);
//当pos指向第一个节点,是头插
if (pos == *pphead)
{
SLTPushFront(pphead,x);
return;
}
SLTNode* newnode = SLTbuyNode(x);
//找到pos的前一个节点
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = newnode;
newnode->next = pos;
}
十二.在指定位置后插入数据.
在 pos 结点后面插入新结点,不需要寻找前驱,效率更高。首先将新结点的 next 指向 pos 原本的后继结点,保存住 pos 后面整条链表;再修改 pos 的 next 指向新结点,完成连接。指针顺序不能调换,如果先修改 pos 的 next,pos 原本后面结点的地址就会丢失,造成链表断裂。该操作没有循环,时间复杂度 O (1),也不存在特殊边界情况,是单链表更推荐的插入方式。
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = SLTbuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
十三.单链表删除pos位置的结点
先找pos的前一个节点吗,让pos结点的前一个结点的next指针指向pos结点的下一个节点。
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && pos);
//pos就是头节点 - 头删
if (pos == *pphead)
{
SLTPopFront(pphead);
return;
}
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
十四.删除链表pos位置之后的节点
该函数删除 pos 后面的结点,无需寻找前驱,没有循环,时间复杂度 O (1)。断言保证 pos 不为空,且 pos 后面确实存在结点。先用 del 保存待删除的后继结点,让 pos 直接连接到 del 的下一个结点,跳过被删节点,再释放 del 内存。只能删掉 pos 后面的节点,不能删除 pos 自身;若 pos 是倒数第二个结点,就可以删掉链表尾结点。函数内部把 del 置空,只作用于局部变量。
void SLTEraseAfter(SLTNode* pos)
{
assert(pos && pos->next);
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
十五.销毁链表
void SListDestroy(SLTNode** pphead)
{
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
}