上篇讲到顺序表,我们发现顺序表存在以下问题:
- 中间/头部插入数据,时间复杂度为 O ( n ) O(n) O(n)
- 增容需要
realloc
,有时会申请新的空间,拷贝数据,释放旧空间,会有不小的消耗。 - 2倍增容依然会使大量空间被浪费。
那么如何解决这些问题呢?下面我们来看看单链表。
什么是链表
链表是一种在物理存储单元上非连续的存储结构,数据元素的逻辑顺序是通过链表中的指针链接实现的。单链表的每一个结点由两部分组成,数据域和指针域(存放指向下一个结点的指针),最后一个结点存放的指针为NULL
第一个结点就是头结点(head)
链表还分为单向链表和双向链表,循环链表或非循环链表。
本篇文章主要介绍单向非循环链表。
链表的实现
链表有时我们会在头结点之前再加一个虚拟头结点(数据域不存放有效数据,指针域指针指向头结点),虚拟头结点带和不带有什么区别呢?我们在下面的实现过程中体会。
头文件SList.h
以下是链表结构的定义和要实现的功能
#pragma once
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SListNode, SLN;
SListNode* SListInit();//初始化(创建带虚拟头结点的链表)
void SListPrint(SListNode* phead);//打印
SListNode* BuySListNode(SLTDataType x);//动态申请一个结点
void SListPushBack(SListNode** pphead, SLTDataType x);//尾插
void SListPushFront(SListNode** pphead, SLTDataType x);//头插
void SListPopBack(SListNode** pphead);//尾删
void SListPopFront(SListNode** pphead);//头删
SListNode* SListFind(SListNode* phead, SLTDataType x);//查找
void SListInsert(SListNode** pphead, SListNode* pos, SLTDataType x);//插入pos位置
void SListErase(SListNode** pphead, SListNode* pos);//删除pos位置
void SListInsertAfter(SListNode* pos, SLTDataType x);//插入pos位置之后
void SListEraseAfter(SListNode* pos);//删除pos位置之后
void SListDestroy(SListNode** pphead);//销毁
注意到有的地方传二级指针,有的地方传一级指针,这是传值调用和传址调用的区别,如果要修改实参指针的指向,应该传址。
函数实现SList.c
带虚拟头结点的要初始化
初始化
虚拟头结点,我们给它起名dummyHead
SListNode* SListInit()
{
SListNode* dummyHead = (SListNode*)malloc(sizeof(SListNode));
if (dummyHead == NULL)
{
printf("malloc fail\n");
exit(-1);
}
dummyHead->data = 0;
dummyHead->next = NULL;
return dummyHead;
}
申请结点
SListNode* BuySListNode(SLTDataType x)
{
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
if (newnode == NULL)
{
printf("malloc fail\n");
exit(-1);
}
else
{
newnode->data = x;
newnode->next = NULL;
}
return newnode;
}
头插
无虚拟头结点:
void SListPushFront(SListNode** pphead, SLTDataType x)
{
assert(pphead);
SListNode* newnode = BuySListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
有虚拟头结点:
void SListPushFront2(SListNode* dummyHead, SLTDataType x)
{
assert(dummyHead);
SListNode* newnode = BuySListNode(x);
newnode->next = dummyHead->next;
dummyHead->next = newnode;
}
有虚拟头结点的不用传二级指针,它的前插其实是插入到了dummyHead
的后面,没有改变dummyHead
的指向
头删
无虚拟头结点:
void SListPopFront(SListNode** pphead)
{
assert(pphead);
if (*pphead == NULL)
{
return;
}
else
{
SListNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
}
有虚拟头结点:
void SListPopFront2(SListNode* dummyHead)
{
assert(dummyHead);
if (dummyHead->next == NULL)
{
printf("No delete elements\n");
return;
}
SListNode* tmp = dummyHead->next;
dummyHead->next = dummyHead->next->next;
free(tmp);
}
得判断dummyHead
的下一结点是不是NULL
注意操作顺序不能颠倒,得确保被赋值的结点之前指向的结点还能被找到。
尾插
void SListPushBack(SListNode** pphead, SLTDataType x)
{
assert(pphead);
SListNode* newnode = BuySListNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
// 找尾
SListNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
这个有无虚拟头结点都可以用。
尾删
无虚拟头结点:
需要分三种情况:空,只有一个结点,有多个结点。
多个结点可以用双指针,也可以直接找倒数第二个结点,但这两种方法都无法适用空和只有一个结点的情况,只能单独处理。
void SListPopBack(SListNode** pphead)
{
assert(pphead);
// 1、空
// 2、一个节点
// 3、多个节点
if (*pphead == NULL)
{
return;
}
else if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
//SListNode* prev = NULL;
//SListNode* tail = *pphead;
//while (tail->next != NULL)
//{
// prev = tail;
// tail = tail->next;
//}
//free(tail);
//tail = NULL;
//prev->next = NULL;
SListNode* tail = *pphead;
while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
有虚拟头结点:
void SListPopBack2(SListNode* dummyHead)
{
assert(dummyHead);
if (dummyHead->next == NULL)
{
printf("No delete elements\n");
return;
}
SListNode* tail = dummyHead;
while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
只要单独处理为空的情况就可以了。
细心的读者一定发现上面画的两个动图都是针对有虚拟头结点的情况,那是因为向中间插入删除稍微难一点。
但是正因虚拟头结点的头插头删本质上就是向中间插入删除,所以在写一般插入删除的时候,头插头删就不用单独考虑。
在实际做题的时候,也建议先加上虚拟头结点。
插入
无虚拟头结点:
void SListInsert(SListNode** pphead, SListNode* pos, SLTDataType x)
{
assert(pphead);
assert(pos);
// 1、pos是第一个节点
// 2、pos不是第一个节点
if (pos == *pphead)
{
SListPushFront(pphead, x);
}
else
{
SListNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SListNode* newnode = BuySListNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
有虚拟头结点:
void SListInsert2(SListNode* dummyHead, SListNode* pos, SLTDataType x)
{
assert(dummyHead);
assert(pos);
SListNode* prev = dummyHead;
while (prev->next != pos)
{
prev = prev->next;
}
SListNode* newnode = BuySListNode(x);
prev->next = newnode;
newnode->next = pos;
}
删除
无虚拟头结点:
void SListErase(SListNode** pphead, SListNode* pos)
{
assert(pphead);
assert(pos);
if (*pphead == pos)
{
SListPopFront(pphead);
}
else
{
SListNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
有虚拟头结点:
void SListErase2(SListNode* dummyHead, SListNode* pos)
{
SListNode* prev = dummyHead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
直接就是无虚拟头结点的else部分,我都快懒得复制了😴
打印
void SListPrint(SListNode* phead)
{
SListNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
查找
查找某值所在结点指针,找不到返回NULL
SListNode* SListFind(SListNode* phead, SLTDataType x)
{
SListNode* cur = phead;
while (cur != NULL)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
注意带虚拟头结点的要传真正的头结点,或者函数内先cur = dummyHead->next;
一下。
销毁
void SListDestroy(SListNode** pphead)
{
assert(pphead);
SListNode* cur = *pphead;
while (cur)
{
SListNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
插删后面
最后再写两个无虚拟头结点的情况下在pos后插入删除的函数。
void SListInsertAfter(SListNode* pos, SLTDataType x)
{
assert(pos);
SListNode* newnode = BuySListNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
void SListEraseAfter(SListNode* pos)
{
assert(pos);
SListNode* next = pos->next;
if (next)
{
pos->next = next->next;
free(next);
next = NULL;
}
}
连头结点都不用传了,很好插。
完整代码
#include "SList.h"
SListNode* SListInit()
{
SListNode* dummyHead = (SListNode*)malloc(sizeof(SListNode));
if (dummyHead == NULL)
{
printf("malloc fail\n");
exit(-1);
}
dummyHead->data = 0;
dummyHead->next = NULL;
return dummyHead;
}
void SListPrint(SListNode* phead)
{
SListNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
SListNode* BuySListNode(SLTDataType x)
{
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
if (newnode == NULL)
{
printf("malloc fail\n");
exit(-1);
}
else
{
newnode->data = x;
newnode->next = NULL;
}
return newnode;
}
void SListPushBack(SListNode** pphead, SLTDataType x)
{
assert(pphead);
SListNode* newnode = BuySListNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
// 找尾
SListNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
void SListPushFront(SListNode** pphead, SLTDataType x)
{
assert(pphead);
SListNode* newnode = BuySListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
void SListPushFront2(SListNode* dummyHead, SLTDataType x)
{
assert(dummyHead);
SListNode* newnode = BuySListNode(x);
newnode->next = dummyHead->next;
dummyHead->next = newnode;
}
void SListPopBack(SListNode** pphead)
{
assert(pphead);
// 1、空
// 2、一个节点
// 3、多个节点
if (*pphead == NULL)
{
return;
}
else if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
//SListNode* prev = NULL;
//SListNode* tail = *pphead;
//while (tail->next != NULL)
//{
// prev = tail;
// tail = tail->next;
//}
//free(tail);
//tail = NULL;
//prev->next = NULL;
SListNode* tail = *pphead;
while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
void SListPopBack2(SListNode* dummyHead)
{
assert(dummyHead);
if (dummyHead->next == NULL)
{
printf("No delete elements\n");
return;
}
SListNode* tail = dummyHead;
while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
void SListPopFront(SListNode** pphead)
{
assert(pphead);
if (*pphead == NULL)
{
return;
}
else
{
SListNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
}
void SListPopFront2(SListNode* dummyHead)
{
assert(dummyHead);
if (dummyHead->next == NULL)
{
printf("No delete elements\n");
return;
}
SListNode* tmp = dummyHead->next;
dummyHead->next = dummyHead->next->next;
free(tmp);
}
SListNode* SListFind(SListNode* phead, SLTDataType x)
{
SListNode* cur = phead;
while (cur != NULL)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
// 在pos位置之前插入
void SListInsert(SListNode** pphead, SListNode* pos, SLTDataType x)
{
assert(pphead);
assert(pos);
// 1、pos是第一个节点
// 2、pos不是第一个节点
if (pos == *pphead)
{
SListPushFront(pphead, x);
}
else
{
SListNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SListNode* newnode = BuySListNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
void SListInsert2(SListNode* dummyHead, SListNode* pos, SLTDataType x)
{
assert(dummyHead);
assert(pos);
SListNode* prev = dummyHead;
while (prev->next != pos)
{
prev = prev->next;
}
SListNode* newnode = BuySListNode(x);
prev->next = newnode;
newnode->next = pos;
}
void SListInsertAfter(SListNode* pos, SLTDataType x)
{
assert(pos);
SListNode* newnode = BuySListNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
// 删除pos 位置
void SListErase(SListNode** pphead, SListNode* pos)
{
assert(pphead);
assert(pos);
if (*pphead == pos)
{
SListPopFront(pphead);
}
else
{
SListNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
void SListErase2(SListNode* dummyHead, SListNode* pos)
{
SListNode* prev = dummyHead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
void SListEraseAfter(SListNode* pos)
{
assert(pos);
SListNode* next = pos->next;
if (next)
{
pos->next = next->next;
free(next);
next = NULL;
}
}
void SListDestroy(SListNode** pphead)
{
assert(pphead);
SListNode* cur = *pphead;
while (cur)
{
SListNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
测试test.c
带虚拟头结点的
#include "SList.h"
void menu()
{
printf("***********************\n");
printf("**** 1.头插 2.头删 ****\n");
printf("**** 3.尾插 4.尾删 ****\n");
printf("**** 5.插入 6.删除 ****\n");
printf("**** 7.打印 8.查找 ****\n");
printf("**** 0.退出 ****\n");
printf("***********************\n");
}
int main()
{
SListNode* s = SListInit();
int option = 0;
SLTDataType x;
size_t pos;
do
{
menu();
printf("请选择:");
scanf("%d", &option);
switch (option)
{
case 1:
printf("输入要插入的值:");
scanf("%d", &x);
SListPushFront2(s, x);
printf("插入成功\n");
break;
case 2:
SListPopFront2(s);
printf("删除成功\n");
break;
case 3:
printf("输入要插入的值:");
scanf("%d", &x);
SListPushBack(&s, x);
printf("插入成功\n");
break;
case 4:
SListPopBack2(s);
printf("删除成功\n");
break;
case 5:
printf("输入要插入的位置后一个结点的值:");
scanf("%u", &pos);
printf("输入要插入的值:");
scanf("%d", &x);
SListInsert2(s, SListFind(s, pos), x);
printf("插入成功\n");
break;
case 6:
printf("输入要删除结点的值:");
scanf("%u", &pos);
SListErase2(s, SListFind(s, pos));
printf("删除成功\n");
break;
case 7:
SListPrint(s->next);
break;
case 8:
printf("输入要查找的值:");
scanf("%d", &x);
SListNode* p = SListFind(s, x);
if (p == NULL)
{
printf("未找到\n");
}
else
{
printf("地址为:%p\n", p);
}
break;
}
} while (option);
printf("退出程序\n");
SListDestroy(&s);
return 0;
}
总结
单链表:
- 优点:单链表增删简单,时间复杂度只有 O ( 1 ) O(1) O(1),空间利用率高,一个数据一个结点,不用可立马释放。
- 缺点:不支持随机访问,并且只能从前往后遍历,只能找到后继,不能找到前驱。
双向链表的结构要复杂一些,但是效率进一步提高,后续我们也会实现的。
😊单链表就到这里啦,看到这儿,你应该已经学会了单链表的增删查改,虚拟头结点带和不带的区别,往后还有许多OJ题要多练,学数据结构就是要多画图,多思考,多练习。