数据结构初阶之单链表
1.单链表的实现
我们不再过的的废话,直接来看关于单链表的一些功能函数:
voidSLTPrint(SLTNode* phead);voidSLTPushBack(SLTNode** phead, SLTDataType x);voidSLTPushFront(SLTNode** phead, SLTDataType x);voidSTLPopBack(SLTNode** phead);voidSLTPopFront(SLTNode** phead); SLTNode*SLTFind(SLTNode* phead, SLTDataType x);voidSLTInsert(SLTNode** phead, SLTNode* pos, SLTDataType x);voidSLTInsertAfter(SLTNode* pos, SLTDataType x);voidSLTErase(SLTNode** phead, SLTNode* pos);voidSLTEraseAfter(SLTNode* pos);voidSListDesTroy(SLTNode** phead);这些是涉及我们这次要实现的函数功能,我们来一一解决它们
不过在此之前,我们首先看一下关于单链表的实现:
1.1实现
typedefint SLTDataType;typedefstructSListNode{ SLTDataType data;structSListNode* next;}SLTNode;上面的是前置条件,在链表中我们存储data,并存储指向下一个节点的指针,
可视化理解:
一个节点在内存中的样子:
┌───────────────────────┐ │ Node │ ├───────────┬───────────┤ │ data │ next │ │ (值) │ (指针) │ │ e.g.10 │ 0x123456 │ └───────────┴───────────┘ 多个节点连接成链表:
节点1(地址:0x1000) 节点2(地址:0x2000) 节点3(地址:0x3000) ┌───────────┬───────────┐ ┌───────────┬───────────┐ ┌───────────┬───────────┐ │ data:1 │ next:0x2000│→ │ data:2 │ next:0x3000│→ │ data:3 │ next:NULL │ └───────────┴───────────┘ └───────────┴───────────┘ └───────────┴───────────┘ voidtest1(){ 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);}链表节点存储两个关键信息:
- 数据值(data):节点的实际内容
- 指向下一个节点的指针(next):将节点连接成链的关键
2单链表函数
2.1单链表打印
voidSLTPrint(SLTNode* phead){ SLTNode* pcur = phead;while(pcur){printf("%d->", pcur->data); pcur = pcur->next;}printf("NULL\n");}我们首先定义pcur指向头节点,while循环,依次打印节点中的data值即可
2.2 单链表的尾插
voidSLTPushBack(SLTNode** phead, SLTDataType x){assert(phead); SLTNode* newnode =SLTBuyNode(x);if(*phead ==NULL){*phead = newnode;}else{ SLTNode* ptail =*phead;while(ptail->next){ ptail = ptail->next;} ptail->next = newnode;}}首先,我们传过来的二级指针不能为空,但是一级指针是可以的,毕竟传过来的链表为空,那我尾插也没什么问题,我们定义新节点为newnode,那么可能会有人说SLTBuyNode(x)是什么玩意,这个其实是因为在函数功能中我们总会用到申请空间的一个功能,所以我们干脆再写一个函数完成这个功能,如下:
SLTNode*SLTBuyNode(SLTDataType x){ SLTNode* newnode =(SLTNode*)malloc(sizeof(SLTNode));if(newnode ==NULL){perror("malloc fail");exit(1);} newnode->data = x; newnode->next =NULL;}好了,继续我们的思路,如果我们传过来的链表为空,那么走if函数中,直接把新申请的给我们的空链表就可以了,如果不为空链表,那么定义个ptail节点,进入while循环中,直到找到最后一个节点,然后把ptail->next = newnode即可
其实这里还有一个问题,为什么这里要传二级指针呢?
2.2.1为什么要传二级指针?
答案只有一个!那就是如果传一级指针,那么我们的形参并不会影响实参,这种传递只是传值,它们之间互不影响罢了,可能到这里还是不明白为什么,没关系,我们这里来详细探讨下:
场景一:链表为空时插入第一个节点
SLTNode* plist =NULL;// plist本身是个指针,现在是NULLSLTPushBack(plist,1);// 如果传plist,只是传值如果SLTPushBack参数是SLTNode* phead
- phead是plist的副本
- 你在函数里phead = newnode,只是改了副本
- 函数返回后,外面的plist还是NULL 😢
场景二:用二级指针
voidSLTPushBack(SLTNode** pphead, SLTDataType x){// pphead 是 &plist,*pphead 就是 plist// 修改 *pphead = newnode,就是修改了 plist 本身}"第一个节点"和"指向第一个节点的指针"是什么?
实际内存结构: ┌─────┬─────────────┐ ┌─────┬─────────────┐ │ data│ next │ │ data│ next │ │ 1 │ 0x1000─────▶│ │ 2 │ 0x2000─────▶│ └─────┴─────────────┘ └─────┴─────────────┘ 地址:0x1000 地址:0x2000 plist(指针变量): ┌─────────────┐ │ 值:0x1000 │ ← 指向第一个节点 └─────────────┘ 地址:0x3000(假设) - 第一个节点:内存地址为0x1000的那个结构体
- 指向第一个节点的指针:变量plist,它存的值是0x1000
- 指向第一个节点的指针的地址:&plist,值是0x3000
形象比喻: - 一级指针 = 你家的门牌号(知道房子在哪)
- 二级指针 = 知道你家门牌号写在哪个本子上
- 要换房子(空链表插入)时,得找到记门牌号的本子(二级指针),改上面的门牌号
简单记忆:
- 如果函数要修改链表头指针(空链表插入、删除第一个节点等),传二级指针&plist
- 如果函数只是遍历/读取,传一级指针plist
为什么难理解?
因为指针就像"遥控器",你手里拿着遥控器(一级指针)。如果你想在另一个房间(函数里)换台(修改链表头),得有人把遥控器给你。但如果只是告诉你现在播什么节目(读取),就不需要给遥控器。
如果有些人到这里还是不太理解,那我们不妨再做个对照实验:
场景1:修改普通变量
voidchangeNumber(int x){ x =100;// 修改副本}intmain(){int a =10;changeNumber(a);// 传值printf("%d", a);// 输出:10,a没变!}这里大家都明白:传值只是传了副本。
场景2:修改指针指向的值
voidchangeValue(int* p){*p =100;// 通过指针修改原值}intmain(){int a =10;changeValue(&a);// 传地址printf("%d", a);// 输出:100,a变了!}这里也简单:用一级指针修改指针指向的数据。
场景3:修改指针本身(这才是你的困惑)
// 这个函数想改变指针本身(让它指向新地址)voidchangePointer(int* p){ p =malloc(sizeof(int));// 这只能改变局部副本p*p =200;}intmain(){int* ptr =NULL;// ptr是一个指针changePointer(ptr);// 想改变ptr,让它指向新内存// 问题:ptr仍然是NULL!}关键来了:ptr本身是一个指针变量,它存着一个地址。当我们说"想修改ptr",意思是修改ptr这个变量里存的地址值。
- ptr = 0x1000 → 改成 ptr = 0x2000
- 但ptr作为参数传给函数时,函数得到的是ptr值的副本
- 就像int a = 10传值给函数一样
可视化内存变化:
调用前: plist:[NULL] 地址0x1000 调用SLTPushBack(plist,1): 在栈上创建局部变量phead:[NULL] 地址0x2000(是plist的副本) 在函数里:phead = newnode phead:[0x3000] 地址0x2000(修改了局部变量) 函数返回:phead销毁 plist:[NULL] 地址0x1000(原指针没变!)总结成一句话:
- 你想修改谁的"值",就要传谁的"地址"。
- 想修改int a的值 → 传&a (一级指针)
- 想修改int* ptr的值(这个值是个地址) → 传&ptr (二级指针)
在我们的例子中:
- plist是一个SLTNode*类型的一级指针
- 你想修改plist这个指针变量的值(从NULL改成新节点地址)
- 所以要传plist的地址,即&plist,这就是二级指针
再打个比方:
- plist = 一个信封,里面写着一个地址(现在是空的)
- SLTPushBack(plist, x) = 我给你这个信封的复印件
- 你在复印件上写新地址,不影响我的原信封
- SLTPushBack(&plist, x) = 我给你我放信封的抽屉位置
- 你打开抽屉,直接在我的原信封上写新地址
现在大家明白了嘛?
2.3单链表的头插
voidSLTPushFront(SLTNode** phead, SLTDataType x){assert(phead); SLTNode* newnode =SLTBuyNode(x); newnode->next =*phead;*phead = newnode;}首先还是二级指针不能为空,我让我定义的新节点指向头节点,再让*phead指向新节点,这样新节点就为头节点了
2.4单链表的尾删
voidSTLPopBack(SLTNode** phead){assert(phead &&*phead);//链表中只有一个节点if((*phead)->next ==NULL){free((*phead));*phead =NULL;}else{ SLTNode* prev =*phead; SLTNode* ptail =*phead;while(ptail->next){ prev = ptail; ptail = ptail->next;}free(ptail); prev->next =NULL; ptail =NULL;}}我们想要尾删是不是需要找到最后一个节点呢?我们这里定义两个指针,当while循环,ptail->next结束后就找到了尾节点,但是我们还需要找到尾节点的前一个节点,为什么呢?如果我们找到尾节点直接释放掉的话,那我们尾节点的前一个节点指向哪里呢?对吧,所以我们需要找到尾节点的前一个节点,当找到这两个节点时,我把尾节点释放掉,前一个节点的next指向空指针,再把尾节点置为空即可。
2.5单链表的头删
voidSLTPopFront(SLTNode** phead){assert(phead &&*phead); SLTNode* next =(*phead)->next;free(*phead);*phead = next;}这个其实很简单,首先定义个节点指向头节点的下一个节点,然后释放掉头节点,使下一个节点成为头节点即可。
2.6单链表的查找
SLTNode*SLTFind(SLTNode* phead, SLTDataType x){ SLTNode* pcur = phead;while(pcur){if(pcur->data == x){return pcur;} pcur = pcur->next;}returnNULL;}这个没啥好讲,很简单,过。
2.7单链表关于在指定位置之前插入数据
voidSLTInsert(SLTNode** phead, SLTNode* pos, SLTDataType x){assert(phead &&*phead);assert(pos);if(*phead == pos){SLTPushFront(phead,x);}else{ SLTNode* newnode =SLTBuyNode(x); SLTNode* prev =*phead;while(prev->next != pos){ prev = prev->next;} newnode->next = pos; prev->next = newnode;}}其实这里就是把前面综合一点而已,既然我们想要再pos节点之前插入数据,那么就先找到pos的前一个节点,然后把新节点的next指向pos,pos的前一个节点的next指向新节点即可,这样不就插入进去了嘛。

但是当我们注意,*phead == pos时这样的代码是行不通的,所以我们需要单独来讨论一下即可。
2.8单链表关于在指定位置之后插入数据
voidSLTInsertAfter(SLTNode* pos, SLTDataType x){assert(pos); SLTNode* newnode =SLTBuyNode(x); newnode->next = pos->next; pos->next = newnode;}这个比刚才的更简单,因为在后,所以直接省一个参数,我们把新节点next指向pos->next即可,pos->next = newnode就可以啦,如图:

2.9单链表关于删除pos节点
voidSLTErase(SLTNode** phead, SLTNode* pos){assert(phead &&*phead);assert(pos);if(pos ==*phead){//头删 SLTNode* next =(*phead)->next;free(pos);(*phead)= next;}else{ SLTNode* pcur =*phead;while(pcur->next != pos){ pcur = pcur->next;} pcur->next = pos->next;free(pos); pos =NULL;}}后面的跟前面都大同小异,就不说了。
2.10单链表关于删除pos之后的节点
voidSLTEraseAfter(SLTNode* pos){assert(pos&&pos->next); SLTNode* del = pos->next; pos->next = del->next;free(del); del =NULL;}2.11单链表关于销毁单链表
voidSListDesTroy(SLTNode** phead){assert(phead &&*phead); SLTNode* pcur =*phead;while(pcur){ SLTNode* next = pcur->next;free(pcur); pcur = next;}*phead =NULL;}