上一篇博客中,我们大致的讲解了单链表相关的各种接口。接下来我们通过例题来运用类似的思想,达到学以致用的目的。
1.移除链表元素
203. 移除链表元素 - 力扣(LeetCode)
没有说明头结点是什么,默认就是第一个元素,而不是哨兵位。
思路一:
遍历链表,利用删除节点的操作,遇到节点就删除
typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) {
ListNode* pcur=head;
ListNode* prev=NULL;
while(pcur){
if(pcur->val==val){
//SLTErase(pcur);
if(prev){
prev->next=pcur->next;
free(pcur);
pcur=prev->next;
}else{//开头为空的情况
head=head->next;
pcur=head;
}
}else{
prev=pcur;
pcur=pcur->next;
}
}
return head;
}
整体思路不难,但具体操作有很多细节需要注意,比较繁琐:在遍历时,为了保证能让该节点的前后节点相连,必须要用一个prev比pcur一直慢走一步。同时,如果prev为空时,删除的操作还不一样。
思路二:
使用新的链表指针NewHead和NewTail
注意,此处并没有开辟新的空间,我们只是利用两个指针串联了所有符合要求的节点
将符合条件的5转移过来之后,5的后面看似是NULL,实在5的next指向的是6,需要在最后给ptail指向的节点的next赋值为NULL。又有可能出现空链表的特殊测试用例情况(用例2),所以我们再用if判断一下,保证newTail的next是存在的
typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) {
ListNode* NewHead;
ListNode* NewTail;//最终会走到尾巴,但其实际作用是遍历链表
NewHead=NewTail=NULL;
ListNode* pcur=head;
while(pcur){
if(pcur->val!=val){
if(NewHead==NULL){//第一次
NewTail=NewHead=pcur;
}else{
NewTail->next=pcur;
NewTail=pcur;
}
}
pcur=pcur->next;
}
if(NewTail){
NewTail->next=NULL;
}
return NewHead;
}
2.链表的中间节点
876. 链表的中间结点 - 力扣(LeetCode)
非常简单的题目(计数一次之后直接找中点即可),但重点不是如何通过该题目,而是学习新思想:快慢指针
先定义两个指针 fast slow
核心思想就是:slow走一步,fast走两步
不过,任然需要分类考虑偶数和奇数的不同情况:从头开始走,总数为偶数时fast走到末尾的NULL,总数为奇数时fast走到最后一个节点。
typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head) {
ListNode* fast;
ListNode* slow;
fast=slow=head;
while(fast&&fast->next){
fast=fast->next->next;
slow=slow->next;
}
return slow;
}
!!注意:如果调换while里面的两个条件的顺序,将发生 执行错误 ,原因是当有偶数个时,我们最后fast会停在NULL的位置,无法通过NULL找到其next,所以先判断fast如果fast就已经不满足条件了,由于c语言短路的特性,将不再判断fast->next,从而避免出错。
3.反转链表
206. 反转链表 - 力扣(LeetCode)
思路一:遍历原链表结点,并且创建新链表,遍历时每个结点都进行头插到新链表
思路二:使用三个指针n1 n2 n3,并依次赋值NULL head head->next
分别记录:前驱节点 当前节点 后继节点
改变节点的指针指向,以此来达到反转的目的。具体流程如下:
我们可以先直接让n2(节点一)的next指向n1(NULL),并且由于n3保存了节点2的地址所以不用担心丢失节点2,然后
n1=n2;
n2=n3;
n3=n3->next;
此时就变成了:节点一指向NULL,n1指向节点一,n2指向节点二,n3指向节点三,一轮循环结束,已经成功逆置了第一个元素,让他成为next指向NULL的目标链表的最后一个元素 。我们继续这个操作,仍然让n2指向n1,再
n1=n2;
n2=n3;
n3=n3->next;
..................... 依次循环
不过细心的你一定发现,我们再使用n3->next之前最好先判断n3是否为空,免得出现“执行错误”。
n1=n2;
n2=n3;
if(n3)
n3=n3->next;
直到最后一次时
最后一次的特征是:n2=n3;执行后,n2为空,这就成为了我们while循环里的条件。
typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head) {
ListNode* n1=NULL;
ListNode* n2=head;
if(head==NULL){
return head;
}
ListNode* n3=head->next;
while(n2){
n2->next=n1;
n1=n2;
n2=n3;
if(n3)
n3=n3->next;
}
return n1;
}
4.合并两个有序链表
21. 合并两个有序链表 - 力扣(LeetCode)
有多种多样的实现思路,比如将l2与l1比较,比较到合适位置就将l2的元素搬运到l1中并插入。
但是为了体现一种新思想:使用 哨兵位
我们此处只着重介绍一种思路:创建新链表
大致思路就是,先创建两个节点newHead newTail 指针,再用l1 l2两个链表指针分别遍历两个链表,分别比较l1 l2的数据,谁小就把谁放进新链表,当任意一个指针指向空(已经跑出了链表时),就结束循环,将指向非空的指针所指向的剩下的内容(最小的一些数据),接到新链表中。
面对这种ONLINE JUDGE题目,我们不难发现。他会给一些抽象的特殊测试用例,如此题中的case2 3
就像高中时数学大题做不出来就先做特殊情况蹭分数的想法一样,先写出两个特殊情况:
然后完成我们的逻辑
typedef struct ListNode ListNode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
//用两个变量代替,避免最后找不到list1和list2
ListNode* l1=list1;
ListNode* l2=list2;
ListNode* newHead=NULL;
ListNode* newTail=NULL;
if(l1==NULL){
return list2;
}
if(l2==NULL){
return list1;
}
while(l1&&l2){
if(l1->val<l2->val){
if(newHead==NULL){
newHead=newTail=l1;
}else{
newTail->next=l1;
newTail=newTail->next;
}
l1=l1->next;
}else{
if(newHead==NULL){
newHead=newTail=l2;
}else{
newTail->next=l2;
newTail=newTail->next;
}
l2=l2->next;
}
}
//退出循环,此时应该有一个指针指向空,我们将剩下的一个指向的小数据们接在新链表末尾
if(l1){
newTail->next=l1;
}
if(l2){
newTail->next=l2;
}
return newHead;
}
细心的朋友会发现,此时我们并没有用到我们的哨兵位:
无论l1->val和l2->val的大小,我们都必须先判断newHead和newTail是否为空(两个判断一个即可),冗杂,这就体现哨兵位的优点了:
我们在newHead指向的新链表的第一个元素插入一个哨兵位,里面不放元素或者放置无效元素,避免检查新指针是否为空的这一步,优化代码
也就是将newHead newTail赋值为NULL的两步修改为
ListNode* newHead=(ListNode*)malloc(sizeof(struct ListNode));
ListNode* newTail=newHead;
有效减少while中的内容
最后由于哨兵位(newHead)不再使用,为了避免出函数后内存泄漏,应当执行free(不需要置NULL,因为出函数后该变量自动就销毁了)
5.分割链表
面试题 02.04. 分割链表 - 力扣(LeetCode)
思路一:创建一个新链表,小于x的头插,大于或等于x的尾差
思路二:大小链表法
(大小链表分别定义一个头一个尾,lessHead和greaterHead分别作为哨兵位)
typedef struct ListNode ListNode;
struct ListNode* partition(struct ListNode* head, int x){
if(head==NULL){
return head;
}
ListNode* pcur=head;
ListNode* lessTail,*lessHead;
ListNode* greaterHead,*greaterTail;
//将哨兵位赋值给大小链表
lessTail=lessHead=(ListNode*)malloc(sizeof(struct ListNode));
greaterHead=greaterTail=(ListNode*)malloc(sizeof(struct ListNode));
while(pcur){
if(pcur->val>=x){
greaterTail->next=pcur;
greaterTail=greaterTail->next;
}else{
lessTail->next=pcur;
lessTail=lessTail->next;
}
pcur=pcur->next;
}
//链接大小链表,注意给末尾赋NULL
greaterTail->next=NULL;
lessTail->next=greaterHead->next;
ListNode* ret=lessHead->next;
free(lessHead);
free(greaterHead);
return ret;
}
注意:当我们把原本不在最后一个位置的节点放在新链表的最后一个节点的时候,一定注意其next是不是指向空,否则就可能出现死循环等错误。
并且链接大小链表的两个句子不能交换,否则greaterTail和greaterHead若为空,就又会发生执行错误。
6.著名的约瑟夫问题
环形链表的约瑟夫问题_牛客题霸_牛客网 (nowcoder.com)
ListNode* SLTBuyNode(int x){
//创建单个的节点
ListNode* p=(ListNode*)malloc(sizeof(ListNode));
p->val=x;
p->next=NULL;
return p;
}
ListNode* SLTCreat(int n){
ListNode* phead=SLTBuyNode(1);
ListNode* ptail=phead;
for(int i=2;i<=n;i++){
ptail->next=SLTBuyNode(i);
ptail=ptail->next;
}
//让首尾相连
ptail->next=phead;
return ptail;//因为后继需要执行删除操作,需要前驱结点prev,
//所有我们直接返回尾结点来作为prev
}
注意:这道题貌似没有定义节点的结构体,但是其实已经定义了,输入LIstNode的时候有一样的名字出现
其次:为什么要返回ptail? 先看主程序代码
int ysf(int n, int m ) {
ListNode* prev=SLTCreat(n);
ListNode* pcur=prev->next;
//开始遍历
for(int count=1;pcur->next!=pcur;){
if(count==m){
//删除该节点,并且count回到1,做好前后相连工作
count=1;
prev->next=pcur->next;
free(pcur);
pcur=prev->next;
}else{
//向后移动
count++;
prev=pcur;
pcur=pcur->next;
}
}
return pcur->val;
}
正如前文所说,因为需要prev指针来记录前驱节点,在m=1的情况,我们如果返回头结点给phead,正常情况就会给ptail赋值为NULL,那么就无法执行prev->next的语句。
总的来说,为了避免节点指针为空又使用他们找自己的元素,出现执行出错的情况,我们应该合理使用哨兵位(如第4、5题)和循环链表(如此题)的特点