不问花开几许
只向浅笑安然
烟雨长虹,孤鹜齐飞的个人主页
个人专栏
期待小伙伴们的支持与关注!!!
目录
使用链表的原因
单链表的实现索引
定义链表的结构体
链表的功能
为节点分配动态内存空间
打印单链表
单链表的头插
代码测试#
关于单链表使用二级指针的原因
单链表的尾插
代码测试#
单链表的头删
编辑
代码测试#
单链表的尾删
代码测试#
单链表的查找
代码测试#
在指定位置之前插入节点
代码测试#
在指定位置之后插入节点
代码测试#
删除指定位置的节点
代码测试#
删除指定位置之后的节点
代码测试#
销毁链表
代码测试#
顺序表和链表的区别
关于链表的OJ题
移除链表元素
思路#
链表的中间节点
思路#
反转链表
思路#
总结#
顺序表的局限性#
<1>中间或头部的插入删除,时间复杂度为O(N)
<2>增容需要申请新空间,拷贝数据,释放旧空间,会有不小的消耗
<3>增容一般是呈2倍的增长,势必会有一定的空间浪费
例如:当前容量为100,满了以后增容到 200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间
因为使用顺序表有种种弊端,所以我们程序猿开发了链表。
链表的简介#
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的 指针链接 次序实现的
<1>从上图可看出,链式结构在逻辑上是连续的,但是在物理上不一定连续
<2>现实中的结点一般都是从堆上申请出来的
<3>从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续
链表的种类:
虽然有这么多的链表的结构,但是我们实际中最常⽤还是两种结构:
单链表 和 双向带头循环链表
<1>单链表:结构简单,⼀般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多
<2>带头双向循环链表:结构最复杂,⼀般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势
我们本篇文章主要讲 单链表 的实现以及几道OJ题,下篇会更新双向循环链表
单链表的实现索引
<1>单链表的头插
<2>单链表的尾插
<3>单链表的头删
<4>单链表的尾删
<5>在指定位置pos之前插入节点
<6>在指定位置pos之后插入节点
<7>在指定位置pos之后删除节点
<8>删除指定位置pos节点
<9>销毁链表
链表的结构跟火车车厢相似,我们需要根据乘客的数量来增删我们车的数量,但其过程不会影响其他车厢,每节车厢都是独立存在的。
车厢是独立存在的,且每节车厢都有车门。想象⼀下这样的场景,假设每节车厢的车门都是锁上的状态,需要不同的钥匙才能解锁,每次只能携带⼀把钥匙的情况下如何从车头走到车尾,答案是:每节车厢里都放⼀把下⼀节车厢的钥匙
在前⼀个节点存放下⼀个节点的地址,就可以访问下一个节点
typedef int SListType; //将数据类型重命名,防止以后换类型所带来的麻烦 typedef struct SList { SListType data; //数据域-车厢号 struct SList* next; //指针域-指向下一节车厢的钥匙 }SL;
为节点分配动态内存空间
后面我们要在单链表中进行头插和尾插,为了方便和减少代码的重复度,我们分装一个函数用来专门创建新结点
SL* SLMalloc(SListType x) //接收SListType类型的值 { SL* newNode = (SL*)malloc(sizeof(SL)); //malloc分配动态内存空间 assert(newNode); //判断有没有开辟成功 newNode->data = x; //为该节点的数据域赋值 newNode->next = NULL; //若该节点的下一个节点不存在则置为空 return newNode; //返回空间 }
打印单链表
我们写代码最好是写一点测一点,不要最后才调试,所以我们先把打印接口先规划好
void Print(SL** head) { SL* put = *head; //定义一个结构体指针put指向头节点 while (put) //遍历put打印节点数据 { printf("%d->", put->data); put = put->next; } printf("NULL\n"); //最后一个节点指向的是NULL }
单链表的头插
void SLFront(SL** head, SListType x) { assert(head); //判断链表为不为空 SL* newNode = SLMalloc(x); //开辟动态空间 newNode->next = *head; //将新空间的指针域指向原头节点 *head = newNode; //让newNode成为新的头节点 }
<1>判断链表为不为空
<2>新开辟节点的指针域指向原头节点
<3>让新节点newNode成为新的头节点
代码测试#
这样我们的头插就完成啦
关于单链表使用二级指针的原因
因为单链表没有哨兵位(带头空节点),而我们单链表的头节点是要存储数据的
如果仅仅只是传一级指针,那就是 传值调用 ,形参不影响实参
所以为了改变我们的头节点,我们要选择 传址调用 ,就要传二级指针
单链表的尾插
void SLPush_Back(SL** head, SListType x) { assert(head); //判断链表为不为空 SL* newNode = SLMalloc(x);//开辟动态内存空间 if (*head == NULL) //如果链表为空,newNode就直接是头节点 { *head = newNode; return; } //如果链表不为空 SL* pur = *head; //定义一个指针pur遍历到链表的最后一个节点 while (pur->next) { pur = pur->next; } pur->next = newNode; //将链表最后一个节点的指针域指向新节点 }
<1>因为尾插是要与前一个节点链接的,所以要判断链表是否为空的情况
<2>如果为空,newNode就直接是头节点
<3>如果不为空,我们就定义一个指针pur遍历到链表的最后一个节点
<4>并将最后一个节点的指针域指向新开辟的节点
代码测试#
单链表的头删
void SLPopFront(SL** head) { assert(head); //判断链表为不为空 assert(*head); //判断头指针为不为空 SL* next = (*head)->next; //我们定义一个next指针接收头节点的下一个节点的指针 free(*head); //释放原头节点 *head = next; //将next设置为新节点 }
<1>判断链表和头节点是否为空
<2>先让头指针指向下一个节点
<3>在释放掉原头指针
代码测试#
单链表的尾删
void SLPopBack(SL** head) { assert(head); assert(*head); if ((*head)->next == NULL) //如果头指针后面没有节点就释放头指针 { free(head); head = NULL; return; } SL* pulic = *head; //定义一个指针用来遍历最后一个节点 SL* puvie = NULL; while (pulic->next) { puvie = pulic; //最后一个节点的前驱节点 pulic = pulic->next; } puvie->next = NULL; //断掉最后一个节点和前驱节点的联系 free(pulic); //释放最后一个节点 pulic = NULL; }
<1>判断链表和头指针是否为空
<2>判断头指针后面的节点是否为空
<3>如果头指针后面的节点为空就释放头指针
<4>若不为空就定义两个指针puvie先指向NULL、pulic遍历找到尾节点的前驱节点
<5>将尾节点的前驱节点置为NULL,并释放最后一个节点
代码测试#
单链表的查找
SL* SLFind(SL** head, SListType x) { SL* put = *head; //定义一个指针put指向头节点 while (put) //遍历put找到要查找的目标节点 { if (put->data == x) { return put; } put = put->next; } return NULL; //若没有找到就置为空 }
代码测试#
在指定位置之前插入节点
void SLInsert(SL** head, SL* pos, SListType x) { assert(head); //判断链表为不为空 assert(*head); //判断头节点为不为空 assert(pos); //判断pos是否合法 SL* newNode = SLMalloc(x); if (pos == *head) //如果pos指向头指针,就头插 { SLFront(head,x); return; } SL* pur = *head; //如果pos不是头指针,就定义一个指针pur指向头节点 while (pur->next != pos) //遍历pur到pos的前驱节点 { pur = pur->next; } pur->next = newNode; //将pur的指针域指向新节点 newNode->next = pos; //新节点的指针域指向pos }
<1>判断头节点和链表是否为空,pos是否合法
<2>如果pos指向头节点,就头插
<3>如果pos不指向头节点,就定义一个指针pur遍历到pos的前驱节点
<4>将pur的指针域指向新节点,新节点的指针域指向pos,完成牵手任务
代码测试#
在指定位置之后插入节点
void SLInsertAfter(SL* pos, SListType x) { assert(pos); //判断pos是否合法 SL* newNode = SLMalloc(x); newNode->next = pos->next; //新节点的指针域存储pos的下一个节点 pos->next = newNode; //pos的指针域存储新节点的指针 }
<1>判断pos位置是否合法
<2>先将新节点的指针域存储pos的下一个节点
<3>pos的指针域存储新节点的指针
注意:<2>、<3>的顺序不能交换,不然找不到3节点以及之后的节点
代码测试#
删除指定位置的节点
void FreePos(SL**head,SL* pos) { assert(head); //判断链表为不为空 assert(*head); //判断头节点为不为空 assert(pos); //判断pos是否合法 if (*head == pos) //如果pos指向头节点就进行头删 { SLPopFront(head); return; } SL* pur = *head; //如果pos不指向头节点,就定义一个指针pur while (pur->next != pos) //遍历pur指向pos前驱节点的位置 { pur = pur->next; } pur->next = pos->next; //pur的指针域指向pos的下一个节点 free(pos); //牵手成功就删掉pos的联系 pos = NULL; }
<1>判断头节点、链表是否为空,pos是否合法
<2>如果pos指向头节点就进行头删
<3>如果pos不指向头节点就定义一个指针pur
<4>遍历pur指向pos前驱节点的位置,让pur的指针域指向pos的下一个节点
<5>释放pos节点
代码测试#
删除指定位置之后的节点
void FreePosRight(SL* pos) { assert(pos); //判断pos是否合法 assert(pos->next); //判断pos之后的节点是否为空 SL* pur = pos->next; //定义一个指针pur指向要释放的节点 pos->next = pos->next->next;//pos在于要删除的节点的下一个节点完成牵手 free(pur); //释放pur的所在节点的空间 pur = NULL; }
<1>判断pos是否合法
<2>判断pos之后的节点是否为NULL
<3>定义一个指针pur指向要被(分手)释放的节点
<4>pos在与该节点的下一个节点完成牵手
<5>在pos与将要(分手)释放的节点的下一个节点完成牵手后删除该节点的联系方式
<6>之后我们在释放该节点的空间就好了
代码测试#
销毁链表
void SListDesTroy(SL** head) { assert(head); //判断链表为不为空 assert(*head); //判断头节点为不为空 SL* put = *head; //定义一个指针指向头节点 while (put) //循环销毁链表 { SL* next = put->next; free(put); put = next; } *head = NULL; }
<1>判断头节点和链表是否为空
<2>定义一个指针put指向头节点
<3>遍历put循环释放链表的空间达到销毁的目的
代码测试#
这样我们的单链表的主要功能就已经实现啦!!!
接下来我们在题目中找找灵感
不同点 | 顺序表 | 链表 |
存储空间上 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
随机访问 | 支持:O(1) | 不支持:O(N) |
任意位置插入或者删除元素 | 可能需要搬移元素,效率低 O(N) | 只需修改指针指向 |
插入 | 动态顺序表,空间不够时需要扩容 | 没有容量的概念 |
应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |
缓存利用率 | 高 | 低 |
移除链表元素
题目链接:移除链表元素
给你一个链表的头节点
head
和一个整数val
,请你删除链表中所有满足Node.val == val
的节点,并返回 新的头节点 。输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5]示例 2:
输入:head = [], val = 1 输出:[]示例 3:
输入:head = [7,7,7,7], val = 7 输出:[]提示:
- 列表中的节点数目在范围
[0, 104]
内1 <= Node.val <= 50
0 <= val <= 50
思路#
三指针的思想:快慢双指针+一个遍历链表的指针
创建新链表,遍历原链表中不是val的节点,并尾插到新链表中
<1>定义三个指针pur、left、right
<2>pur指向原链表的头节点,left、right先置为NULL
<3>创建一个新链表,将不是val的节点尾插到新链表
<4>尾插完后right往后走,准备接收尾插节点
<5>遇到val时,pur直接跳过,并不进行尾插操作
<6>直到pur->next = NULL时跳出循环
<7>注意:遍历完原链表后right->next一定要置为NULL,因为节点5还存着节点val的地址
<8>最后返回头节点 left 便可完成此题
typedef struct ListNode ListNode;//结构体的重命名,为了以下书写简单 struct ListNode* removeElements(struct ListNode* head, int val) { ListNode *left,*right; //定义新链表的快慢指针 left = right = NULL; ListNode* pur = head; //在定义一个指针指向头节点 while(pur) { if(pur->val != val) //遍历pur判断是不是val,不是val就尾插 { if(left ==NULL) //判断头节点为不为空 { left = right = pur; //若为空新插入的节点就是头节点 } else { //尾插 right->next = pur; right = right->next; } } pur = pur->next; //是val就继续遍历 } if(right) { right->next = NULL; //断掉尾节点和其他节点的联系 } return left; }
链表的中间节点
题目链接:链表的中间节点
给你单链表的头结点
head
,请你找出并返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
输入:head = [1,2,3,4,5] 输出:[3,4,5] 解释:链表只有一个中间结点,值为 3 。输入:head = [1,2,3,4,5,6] 输出:[4,5,6] 解释:该链表有两个中间结点,值分别为 3 和 4 ,返回第二个结点。提示:
- 链表的结点数范围是
[1, 100]
1 <= Node.val <= 100
思路#
快慢双指针:定义两个指针left、right分别指向头节点,left走一步,right走两步
链表节点是偶数的情况:
链表节点是奇数的情况:
如图所示
<1>left走一步right走两步,到 right 或者 right->next 为NULL的情况下left刚好是链表的中间节点
<2>这个时候我们只要返回 left 所在节点就可以啦
typedef struct ListNode ListNode; struct ListNode* middleNode(struct ListNode* head) { ListNode *left,*right; //定义快慢指针 left = right = head; //快慢指针都指向头节点 while(right&&right->next)//这里分奇偶讨论,奇数是right,偶数是right->next,不为空就进行循环 { left = left->next; //慢指针走一步 right = right->next->next;//快指针走两步 } return left; }
反转链表
题目链接:反转链表
给你单链表的头节点
head
,请你反转链表,并返回反转后的链表。输入:head = [1,2,3,4,5] 输出:[5,4,3,2,1]输入:head = [1,2] 输出:[2,1]示例 3:
输入:head = [] 输出:[]提示:
- 链表中节点的数目范围是
[0, 5000]
-5000 <= Node.val <= 5000
思路#
三指针:创建三个指针,分别记录前驱节点,当前节点,后继节点,改变原链表的指针方向
<1>创建三个指针,分别记录前驱节点n1,当前节点n2,后继节点n3
<2>让n2指向原链表的头节点,n1指向NULL、n3指向n2的下一个节点
<3>开始让头节点与下一个节点断开并指向NULL成为我们新创建链表的尾节点
<4>完成上述操作后,n1、n2、n3分别往后走直到n2指向NULL为止
<5>这样原来的尾节点成为头节点,原来的头节点成为尾节点,链表被反转了
typedef struct ListNode ListNode; struct ListNode* reverseList(struct ListNode* head) { if(head == NULL)//判断链表是否为空 { return head; } ListNode *n1,*n2,*n3; n1 = NULL,n2 = head,n3 = head->next; while(n2) //如果n2不为空就进入循环 { n2->next = n1; //修改n2的指向 n1 = n2; //交换位置达成移动的效果 n2 = n3; if(n3) //判断n3是否为空 n3 = n3->next; //n3往后走 } return n1; }
总结#
<1>在遇到不会写的链表接口时最好画图模拟
<2>链表在分手后记得断掉联系方式
<3>链表题很多涉及到指针,所以最先要往双指针的方向去想
C++算法中两夫妻的故事-双指针