目录
嘿嘿:上次实现单链表已经是两个月之前的事儿了,今儿拿出来再写,思路还是在,但确实忘掉了一些细节,一上午出现了各种小问题,全都是自己一个个调出来的,这种快感不是别的什么事情可以代替的。同时也发现,当初觉得想不到的东西,现在看也就是理所当然,这就是螺旋式上升吧🐏!
0. 引
上文说到顺序表缺陷 ——
❄️1. 空间不够需要增容,增容是要付出代价的。
❄️2. 为了避免频繁扩容,我们基本都是按倍数去扩(比如扩2倍),这又可能会造成一定的空间浪费。
❄️3. 顺序表要求数据从开始位置保持连续,那么中间和头部的插入和删除,都需要挪动数据,时间复杂度为O(N),效率不高。
为此,我们引入了链表。
1. 链表的概念和结构
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
- 逻辑图 —— 想象出来的形象方式表示
- 物理图 —— 内存中的真实存储 —— 通过指针来存储下一个节点的地址,并不连续
2. 链表的分类
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
- 单向或双向
- 带头不带头
- 循环非循环
当然我们最常用的还是这两种 ——
- 无头单向非循环链表
- 结构简单,但缺陷还是很多(一会儿实现的时候你就知道啦),单纯单链表的增删查改意义不大。
- 但是还是要写,可能正因为它的缺陷,很多oj题考察的都是单链表
- 更多的是做更复杂数据结构的子结构,比如哈希桶、邻接表。
- 带头双向循环链表
- 结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。
- 另外这个结构虽然复杂,却也是无死角的结构,用代码实现时会发现结构会带来很多优势,非常爽,后面我们代码实现了就知道了。
3. 链表的实现
文末领取头文件大家自己写,自己写自己调时候思路是最清晰的,具体实现中每个思路和要注意的小点我都会写好(尤其是它第一次出现的时候)(这链表电脑画图老累了,主要是这个破箭头,ipad上写字还丑,我把我草纸的图弄进来了哦吼吼)红笔写的是边界,耦合色写的是变化过程哈哈。
3.1 打印、申请新节点、销毁
3.1.1 打印
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
3.1.2 申请新节点
//申请新节点
SLTNode* BuyListNode(SListDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
printf("malloc failed\n");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
3.1.3 销毁
- 顺序表中申请的内存是连续的(
realloc
),因此一次释放就行;而链表的空间申请时候就不连续,必须遍历回收。 - free时,data和next都会被置成随机值,因此要记录next
void SListDestroy(SLTNode** pphead)
{
assert(pphead);
// 链表的空间必须遍历回收,因为申请时候就不连续
SLTNode* cur = *pphead;
SLTNode* next = cur->next;// 记录下一个节点的位置
while (next)
{
free(cur);
cur = next;
next = cur->next;
}
*pphead = NULL;
}
3.2 尾插、尾删
3.2.1 尾插
- 为什么要传二级指针?尾插、包括后面的尾删头插头删、前面的销毁都需要传二级指针,而打印、查找就不需要。
🍓实际上,二级指针就是为了处理首节点的改变(pList是链表的地址,类型为SLNode*
),后面操纵的都是结构体就不需要。众所周知形参是实参的一份临时拷贝,形参的改变不会影响实参,因此必须传实参地址(在这里类型为SLNode**
)以改变实参。
- 解释一下
assert(pphead);
,像这种一定不为空的要断言一下,方便查错(传错)
void SListPushBack(SLTNode** pphead, SListDataType x)
{
assert(pphead);
SLTNode* newnode = BuyListNode(x);
//1.空链表 - 尾插头结点的地址pList发生改变,因此需要传pList的地址
//单拎出来处理
if (*pphead == NULL)
{
*pphead = newnode;
return;
}
//2.找尾
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
3.2.2 尾删
- 考虑链表为空 —— 断言结束(STL采取的就是这样粗暴的方式),毕竟,就是你用错了
- 其余细节过程都在图中
void SListPopBack(SLTNode** pphead)
{
assert(pphead);
//1.空链表
assert(*pphead);
//2.删的只剩一个节点时
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
return;
}
//3.
//找尾的上一个
SLTNode* prevtail = *pphead;
SLTNode* tail = prevtail->next;
while (tail->next != NULL)
{
prevtail = tail;
tail = tail->next;
}
free(tail);
prevtail->next = NULL;
}
3.3 头插、头删
3.3.1 头插
void SListPushFront(SLTNode** pphead, SListDataType x)
{
assert(pphead);
SLTNode* newnode = BuyListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
3.3.2 头删
void SListPopFront(SLTNode** pphead)
{
assert(pphead);
//1.删空
assert(*pphead);
//2.
SLTNode* headnext = (*pphead)->next;
free(*pphead);
*pphead = headnext;
}
3.4 查找、任意位置插入、任意位置删除
3.4.1 查找
SLTNode* SListFind(SLTNode* phead, SListDataType x)
{
SLTNode* cur = phead;
if (phead == NULL)
{
//处理空链表,不然后面解引用可是又错了
return NULL;
}
while (cur)
{
if (cur->data == x)
{
return cur;
}
else
{
cur = cur->next;
}
}
return NULL;//走到这儿还没找到
}
3.4.2 任意位置插入
//在pos位置之前去插入一个节点--pos位置一般是find来的
void SListInsert(SLTNode** pphead, SLTNode* pos, SListDataType x)
{
assert(pphead);
assert(pos);
SLTNode* newnode = BuyListNode(x);
//1. 头插
if (*pphead == pos)
{
newnode->next = *pphead;
*pphead = newnode;
}
//2.
SLTNode* posPrev = *pphead;
while (posPrev->next != pos)
{
posPrev = posPrev->next;
}
posPrev->next = newnode;
newnode->next = pos;
}
//在pos后边插入--这个更适合也更简单高效
//STL- insert_after
void SListInsertAfter(SLTNode* pos, SListDataType x)
{
assert(pos);
SLTNode* newnode = BuyListNode(x);
SLTNode* posNext = pos->next;
pos->next = newnode;
newnode->next = posNext;
}
3.4.3 任意位置删除
void SListErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
//删空
assert(*pphead);
//1.头删
//带哨兵位的头的 -- 可以合并 头删与中间删 -- 逻辑结构一样,不会删完
if (*pphead == pos)
{
*pphead = pos->next;
free(pos);
return;//自己调出来的
}
SLTNode* posPrev = *pphead;
while (posPrev->next != pos)
{
posPrev = posPrev->next;
}
posPrev->next = pos->next;
free(pos);//注意顺序,对于我很简单啦
//pos不置空也无所谓,毕竟在函数中是形参,形参的改变不会影响实参,处于好习惯,还是置空比较好
}
当然也可以实现删除pos后面的节点,这个功能很奇葩哈哈给pos不删pos
void SListEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);//一个节点
SLTNode* posNext = pos->next;
pos->next = posNext->next;
free(posNext);
//next = NULL;//出作用域即销毁,是野指针,但别人拿不到
}
4. 关于单链表的思考
4.1 链表优点
链表弥补了顺序表的缺陷
- 按需申请空间,不用了就释放(更合理的使用了空间),不存在空间浪费
- 头部和中间插入删除数据,不需要挪动数据
4.1 链表缺点
- 每一个数据,都要存一个指针链接后面的数据结点
- 不支持随机访问(用下标直接访问第i个元素),而有些算法,需要随机访问,如:二分查找、优化的快排
附录
SingleList.h
#pragma once
//无头单向非循环链表
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SListDataType;
typedef struct SListNode
{
SListDataType data;
struct SListNode* next;
}SLTNode;
void SListPrint(SLTNode* phead);
//销毁
void SListDestroy(SLTNode** pphead);
//尾插
void SListPushBack(SLTNode** pphead, SListDataType x);
//尾删
void SListPopBack(SLTNode** pphead);
//头插
void SListPushFront(SLTNode** pphead, SListDataType x);
//头删
void SListPopFront(SLTNode** pphead);
//查找
SLTNode* SListFind(SLTNode* phead, SListDataType x);
//在pos位置之前去插入一个节点--pos位置一般是find来的
void SListInsert(SLTNode** pphead, SLTNode* pos, SListDataType x);
//任意位置删
void SListErase(SLTNode** pphead, SLTNode* pos);
//在pos后边插入--这个更适合也更简单高效
void SListInsertAfter(SLTNode* pos, SListDataType x);
void SListEraseAfter(SLTNode* pos);
//void SListInsert(SLTNode* phead, SLTNode* pos,SListDataType x);
//void SListErase(SLTNode* phead, int pos);
SingleList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
void SListDestroy(SLTNode** pphead)
{
// 这个链表是
// 链表的空间必须遍历回收,因为申请时候就不连续
SLTNode* cur = *pphead;
SLTNode* next = cur->next;// 记录下一个节点的位置
while (next)
{
free(cur);
cur = next;
next = cur->next;
}
*pphead = NULL;
}
//申请新节点
SLTNode* BuyListNode(SListDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
printf("malloc failed\n");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
void SListPushBack(SLTNode** pphead, SListDataType x)
{
assert(pphead);
SLTNode* newnode = BuyListNode(x);
//1.空链表 - 尾插头结点的地址pList发生改变,因此需要传pList的地址
// **pphead
if (*pphead == NULL)
{
*pphead = newnode;
return;
}
//找尾
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
void SListPopBack(SLTNode** pphead)
{
assert(pphead);
//1.空链表
assert(*pphead);
//2.删的只剩一个节点时
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
return;
}
//3.普遍情况
//找尾的上一个
SLTNode* prevtail = *pphead;
SLTNode* tail = prevtail->next;
while (tail->next != NULL)
{
prevtail = tail;
tail = tail->next;
}
free(tail);
prevtail->next = NULL;
}
void SListPushFront(SLTNode** pphead, SListDataType x)
{
assert(pphead);
SLTNode* newnode = BuyListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
void SListPopFront(SLTNode** pphead)
{
assert(pphead);
//1.删空
assert(*pphead);
//2.
SLTNode* headnext = (*pphead)->next;
free(*pphead);
*pphead = headnext;
}
SLTNode* SListFind(SLTNode* phead, SListDataType x)
{
SLTNode* cur = phead;
if (phead == NULL)
{
//处理空链表,不然后面解引用可是又错了
return NULL;
}
while (cur)
{
if (cur->data == x)
{
return cur;
}
else
{
cur = cur->next;
}
}
return NULL;//走到这儿还没找到
}
//在pos位置之前去插入一个节点--pos位置一般是find来的
void SListInsert(SLTNode** pphead, SLTNode* pos, SListDataType x)
{
assert(pphead);
assert(pos);
SLTNode* newnode = BuyListNode(x);
//1. 头插
if (*pphead == pos)
{
newnode->next = *pphead;
*pphead = newnode;
}
//2.
SLTNode* posPrev = *pphead;
while (posPrev->next != pos)
{
posPrev = posPrev->next;
}
posPrev->next = newnode;
newnode->next = pos;
}
//在pos后边插入--这个更适合也更简单高效
//STL- insert_after
void SListInsertAfter(SLTNode* pos, SListDataType x)
{
assert(pos);
SLTNode* newnode = BuyListNode(x);
SLTNode* posNext = pos->next;
pos->next = newnode;
newnode->next = posNext;
}
//任意位置删
void SListErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
//删空
assert(*pphead);
//1.头删
//带哨兵位的头的 -- 可以合并 头删与中间删 -- 逻辑结构一样,不会删完
if (*pphead == pos)
{
*pphead = pos->next;
free(pos);
return;//自己调出来的
}
SLTNode* posPrev = *pphead;
while (posPrev->next != pos)
{
posPrev = posPrev->next;
}
posPrev->next = pos->next;
free(pos);//注意顺序,对于我很简单啦
//pos不置空也无所谓,毕竟在函数中是形参,形参的改变不会影响实参,处于好习惯,还是置空比较好
}
void SListEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);//一个节点
SLTNode* posNext = pos->next;
pos->next = posNext->next;
free(posNext);
//next = NULL;//出作用域即销毁,是野指针,但别人拿不到
}
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
//测试尾插尾删
void testSingleList1()
{
//指向单链表的指针 - 本质是存储这个单链表头结点的地址的指针变量
SLTNode* pList;
pList = NULL;//这个错是我调出来的
SListPushBack(&pList, 1);
SListPushBack(&pList, 2);
SListPushBack(&pList, 3);
SListPushBack(&pList, 4);
SListPrint(pList);
SListPopBack(&pList);
SListPopBack(&pList);
SListPopBack(&pList);
/*SListPopBack(&pList);
SListPopBack(&pList);*/
SListPrint(pList);
SListDestroy(&pList);
}
//测试头插头删
void testSingleList2()
{
SLTNode* pList;
pList = NULL;
SListPushFront(&pList, 1);
SListPushFront(&pList, 2);
SListPushFront(&pList, 3);
SListPrint(pList);
SListPopFront(&pList);
SListPopFront(&pList);
/*SListPopFront(&pList);
SListPopFront(&pList);*/
SListPrint(pList);
SListDestroy(&pList);
}
//测试任意位置的插入、查找
void testSingleList3()
{
SLTNode* pList;
pList = NULL;
SListPushBack(&pList, 1);
SListPushBack(&pList, 2);
SListPushBack(&pList, 4);
SListPushBack(&pList, 5);
SLTNode* ret = SListFind(pList, 2);
if (ret == NULL)
{
printf("not found\n");
}
else
{
printf("%d\n", ret->data);
}
//SListInsert(&pList, ret, 0);//测试头插
//SListInsert(&pList, ret, 3);
SListInsertAfter(ret, 3);
SListPrint(pList);
SListDestroy(&pList);
}
//测试任意位置删
void testSingleList4()
{
SLTNode* pList;
pList = NULL;
SListPushBack(&pList, 1);
SListPushBack(&pList, 2);
SListPushBack(&pList, 4);
SListPushBack(&pList, 5);
SListPrint(pList);
SLTNode* ret = SListFind(pList, 1);
if (ret == NULL)
{
printf("not found\n");
}
else
{
printf("%d\n", ret->data);
}
//SListErase(&pList, ret);
//SListErase(&pList, ret);
SListEraseAfter(ret);
SListPrint(pList);
SListDestroy(&pList);
}
int main()
{
//testSingleList1();
//testSingleList2();
//testSingleList3();
testSingleList4();
return 0;
}
下一篇来带头双向循环链表
本文完