《剑指Offer:单链表操作入门——从“头删”开始破解面试》
🔥@晨非辰Tong:个人主页
👀专栏:《C语言》、《数据结构与算法》
💪学习阶段:C语言、数据结构与算法初学者
⏳“人理解迭代,神理解递归。”
引言:上篇我们初探了单链表的“不连续”之美,并实现了部分基础操作。本篇将作为单链表的终极篇章,彻底攻克其余核心功能,助你构建完整知识体系。
目录
二、单链表的实现(续)
2.5 单链表的头删
三、何时用顺序表,何时用单链表?
3.1 顺序表功能的复杂度
3.2 单链表功能的复杂度
四、链表其余功能
4.1 查找
4.2 在指定位置之前插入
4.3 在指定位置之后插入
4.4 删除pos节点
4.5 删除pos之后的节点
4.6 销毁链表
二、单链表的实现(续)
2.5 单链表的头删
--实现头删的算法思路很简单,不同于前面实现的尾删函数:
由于定义的 phead 指向的是首节点(保存首节点的地址),就需要改变 phead 的指向—>第2节点。
那么,要定义一个新的指针 next 将第2节点的地址保存(因为释放*pphead,会将首节点next指针保存的第2节点的地址销毁),接下来直接释放首节点,再将phead指向创建的next指针(*pphead)->next = next。
SList.c文件
include "SList.h"//定义_头删
void SLTPopFront(SLTNode** pphead)
{//链表为空不能删除assert(pphead && *pphead);//pphead为空—>解引用错误;*pphead为空->首节点为空(链表为空),无法删除//定义新指针,将第2个节点第地址存起来,释放phead指向的第一个的节点SLTNode* next = (*pphead)->next;//释放首节点,并重新指向新指针nextfree(*pphead);//(*pphead)存放的是首节点的地址(*pphead) = next;
}
test.c文件
include "SList.h"void test02()
{//创建空链表SLTNode* phead = NULL;//phead头指针,指向的首节点为空,代表链表没有节点//尾插SLTPushBack(&phead, 1);//为了使形参的变化影响实参,传地址SLTPushBack(&phead, 2);SLTPushBack(&phead, 3);SLTPushBack(&phead, 4);SLTPrint(phead);//头删SLTPopFront(&phead);SLTPrint(phead);SLTPopFront(&phead);SLTPrint(phead); SLTPopFront(&phead);SLTPrint(phead);SLTPopFront(&phead);SLTPrint(phead);}
int main()
{test02();return 0;
}
三、何时用顺序表,何时用单链表?
前面已经学习了如何判断算法的好坏:时间复杂度、空间复杂度!
下面通过对前面实现的顺序表、单链表插入、删除算法的复杂度计算来对比。
--复杂度主要关注时间复杂度
3.1 顺序表功能的复杂度
顺序表的尾插算法:
--程序中没有循环结构(循环次数未知),时间复杂度—>O(1)。
顺序表的头插算法:
--程序中调用了扩容函数(无循环),看函数主体——存在循环次数未知的循环结构,时间复杂度——>O(N)。
顺序表的尾删算法:
--代码没有循环结构,时间复杂度——>O(1)。
顺序表的头删算法:
--存在循环次数未知的循环结构,时间复杂度——>O(N)。
3.2 单链表功能的复杂度
单链表的尾插算法:
--存在循环次数未知的循环结构,时间复杂度——>O(N)。
单链表的头插算法:
--代码没有循环结构,时间复杂度——>O(1)。
单链表的尾删算法:
--存在循环次数未知的循环结构,时间复杂度——>O(N)。
单链表的头删算法:
--代码没有循环结构,时间复杂度——>O(1)。
通过对比发现:
- 频繁的对数据头部进行插入、删除:使用单链表;
- 频繁的对数据尾部进行插入、删除:使用顺序表。
四、链表其余功能
4.1 查找
SList.c
include "SList.c"//定义_查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{//创建指针接收首节点SLTNode* pcur = phead;while (pcur){if (pcur->data == x){return pcur;//返回找到的数值}pcur = pcur->next;}//未找到return NULL;//返回空
}//返回类型为SLTNode* ,因为最后程序返回的数据是这个类型
test.c
include "SList.h"void test02()
{//创建空链表SLTNode* phead = NULL;//phead头指针,指向的首节点为空,代表链表没有节点//尾插SLTPushBack(&phead, 1);//为了使形参的变化影响实参,传地址SLTPushBack(&phead, 2);SLTPushBack(&phead, 3);SLTPushBack(&phead, 4);SLTPrint(phead);//查找SLTNode* pos = SLTFind(phead, 4);if (pos){printf("找到了!\n");}else {printf("未找到!\n");}
}int main()
{test02();return 0;
}
--实现查找算法的思路很简单,需要遍历数组与目标查找数值进行对比。
4.2 在指定位置之前插入
SList.c文件
include "SList.h"//定义_在指定位置之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead && pos);//对二级指针解引用不能为空//新节点存储数据SLTNode* newcode = SLTBuyNode(x);//pos指向头节点if (pos == *pphead){//执行头插SLTPushFront(pphead, x);}else{//prev从首节点遍历寻找pos的前一个节点(next与pos的地址匹配)SLTNode* prev = *pphead;while (prev->next != pos){//没找到,移动到下一个节点prev = prev->next;}//跳出循环——找到了newcode->next = pos;//新节点的next指针指向posprev->next = newcode;//前一个节点next指针指向新节点}
}
对于在指定位置之前插入,定义中情况为头插,涉及到首节点的改变,所以要传首节点的地址过去。
test.c文件
include "SList.h"void test02()
{//创建空链表SLTNode* phead = NULL;//phead头指针,指向的首节点为空,代表链表没有节点//尾插SLTPushBack(&phead, 1);//为了使形参的变化影响实参,传地址SLTPushBack(&phead, 2);SLTPushBack(&phead, 3);SLTPushBack(&phead, 4);SLTPrint(phead);SLTNode* pos = SLTFind(phead, 4);//在4前面插入SLTInsert(&phead, pos, 100);SLTPrint(phead);//在首节点前插入pos = SLTFind(phead, 1);SLTInsert(&phead, pos, 100);SLTPrint(phead);}
int main()
{test02();return 0;
}
4.3 在指定位置之后插入
这种就是前面的第2种情况。
SList.c文件
include "SList.h"//定义_在指定位置之后插入
SLTNode* SLTInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newcode = SLTBuyNode(x);newcode->next = pos->next;//接收后节点的地址pos->next = newcode;}
4.4 删除pos节点
SList.c文件
include "SList.h"
//定义_删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead && *pphead);//如果pos刚好就是头结点if (pos == *pphead){SLTPopFront(pphead);}else {SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}//prev pos pos->nextprev->next = pos->next;free(pos);pos = NULL;}
}
算法思路:删除就是将节点空间释放,然后将pos前的节点与pos后的节点进行地址链接。
4.5 删除pos之后的节点
SList.c
include "SList.h"
//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos)
{assert(pos&&pos->next);//pos del del->nextSLTNode* del = pos->next;pos->next = del->next;free(del);del == NULL;
}
算法思路:找到pos节点后,创建新指针接收后节点的地址,再通过next指针进行地址链接。
4.6 销毁链表
SList.c
include "SList.h"//销毁
void SListDestroy(SLTNode** pphead)
{assert(pphead);SLTNode* pcur = *pphead;while (pcur){SLTNode* next = pcur->next;free(pcur);pcur = next;}*pphead = NULL;
}
算法思路:只需要循环释放pos节点空间,但需要新指针先将pos之后的节点地址保存,pos再移动。
回顾:
《面试高频数据结构:单链表与顺序表之争,读懂“不连续”之美背后的算法思想》
《算法面试“必杀技”:双指针法高效解决数组原地操作》
结语:至此,我们已经为单链表这座「数据结构大厦」打下了坚实的地基。是时候解锁新的关卡了——接下来,我们将进入功能更强大的双向链表,看它如何破解单链表遍历的效率困境,让插入删除更加游刃有余。