欢迎各位大佬交流!!!
通过对经典链表OJ题目的练习,不仅能加深对链表的理解,更能体会链表的精妙之处!
目录
1、移除链表元素
看到题目之后,我们第一个思路就是遍历一遍链表,遇到某个节点的val满足条件就删除此节点;
思路一:
当我们要删除某一个节点时,肯定要暂存此节点的下一个节点;
那么不妨创建出cur、prev两个节点,一个用来遍历节点,另一个来记录cur的前一个节点地址
那如果头节点的val就等于val呢?显然要更改头节点,那么我们提前处理这种情况,保证cur和prev不受影响;
尝试写代码:
先提前处理头节点的情况
cpp
if(head && head->val == val)
{
//更改头节点
head = head->next;
}
接着创建两个指针用来遍历,同时分情况处理
cpp
while(cur)
{
if(cur->val == val)
{
//删除cur
struct ListNode* next = cur->next;
prev->next = cur->next;
free(cur);
cur = next;
}
else
{
//同时向后走
prev = cur;
cur = cur->next;
}
}
整合一下
cpp
struct ListNode* removeElements(struct ListNode* head, int val)
{
//思路一:创建两个指针
//先处理头节点
while(head && head->val == val)
{
//更改头节点
head = head->next;
}
struct ListNode* cur = head;
struct ListNode* prev = head;
while(cur)
{
if(cur->val == val)
{
//删除cur
struct ListNode* next = cur->next;
prev->next = cur->next;
free(cur);
cur = next;
}
else
{
//同时向后走
prev = cur;
cur = cur->next;
}
}
return head;
}
现在来运行一下

程序出错了,提示也并不是很明显,我们再来思考一下;
首先我们的遍历节点这段代码肯定是没有问题的;
那么就要往前看,我们再思考一下;
如果给定的链表前两个节点的val都等于val呢?而我们的if只能判断一次,因此我们将if改为while再试一次

在此基础上,我们还可以适当优化一下;
我们可不可以将所有节点的判断都看成是普通节点的判断?
就是创建一个虚拟头节点
将此虚拟头节点当做给定链表头节点,后续所有节点都当做普通节点来处理
cpp
//创建虚拟头节点
struct ListNode* dummyhead = (struct ListNode*)malloc(sizeof(struct ListNode));
dummyhead->next = head;
创建出虚拟头节点之后,依旧用双指针的方法进行遍历即可
在最终返回时,一定要返回虚拟头节点的下一个节点!!!
cpp
struct ListNode* removeElements(struct ListNode* head, int val)
{
//创建虚拟头节点
struct ListNode* dummyhead = (struct ListNode*)malloc(sizeof(struct ListNode));
dummyhead->next = head;
struct ListNode* cur = dummyhead->next;
struct ListNode* prev = dummyhead;
while(cur)
{
if(cur->val == val)
{
//满足条件,删除
prev->next = cur->next;
free(cur);
cur = prev->next;
}
else
{
prev = cur;
cur = cur->next;
}
}
return dummyhead->next;
}
提交一下:

思路二:
我们换一种角度来思考,重新新建一个链表怎么样?如果某个节点的val != val,此时就尾插到新链表中,最后返回新链表的头节点即可
首先创建出新链表的头节点、遍历原链表的cur节点
有了思路一的铺垫,思路二就显得简单的多
cpp
struct ListNode* removeElements(struct ListNode* head, int val)
{
struct ListNode* cur = head;
struct ListNode* newhead = NULL;
struct ListNode* newtail = NULL;
while(cur)
{
if(cur->val != val)
{
//加入到新链表中
if(newhead == NULL)
{
//新链表的第一个节点,既是头节点,也是尾节点
newhead = newtail = cur;
}
else
{
//尾插即可
newtail->next = cur;
newtail = cur;
}
}
cur = cur->next;
}
return newhead;
}
此时,我们提交一下后发现测试用例一无法通过

我们看到预期结果时1,2,3,4,5,而我们的输出结果却是1,2,3,4,5,6 最终多了一个节点
这是为何?

当我们处理到5节点时,newtail = cur,继续向后走,发现最后一个节点的val与题目所给val相等;此时往后走就退出了循环
而newtail的下一个节点不还是连接着节点6吗?
因此,只需将newtail的next置为空即可满足题意
cpp
struct ListNode* removeElements(struct ListNode* head, int val)
{
struct ListNode* cur = head;
struct ListNode* newhead = NULL;
struct ListNode* newtail = NULL;
while(cur)
{
if(cur->val != val)
{
//加入到新链表中
if(newhead == NULL)
{
//新链表的第一个节点,既是头节点,也是尾节点
newhead = newtail = cur;
}
else
{
//尾插即可
newtail->next = cur;
newtail = cur;
}
}
cur = cur->next;
}
if(newtail != NULL) newtail->next = NULL;
return newhead;
}
提交一下

没有问题!!!
AC了
2、反转链表
反转链表,就是将链表逆置
思路一:
首先能够想到的就是创建两个指针,遍历一遍链表;
同时在逆置的过程中创建next指针,用来控制cur及暂存节点
cpp
struct ListNode* reverseList(struct ListNode* head)
{
//直接将prev置为空,当处理第一个元素时,就会将其next置为空,恰好作为反转链表后的尾节点
struct ListNode* prev = NULL;
struct ListNode* cur = head;
while(cur)
{
struct ListNode* next = cur->next;
cur->next = prev;
prev = cur;
cur = next;
}
return prev;
}

没有问题,这种迭代法很简洁高效
思路二:
不妨再想一想,如果用和上道题思路二的想法,新建一个链表;
同时将原链表的每一个节点都头插到新链表中,最终返回新链表的头节点
cpp
struct ListNode* reverseList(struct ListNode* head)
{
struct ListNode* newtail,*newhead = NULL;
struct ListNode* cur = head;
while(cur)
{
if(newhead == NULL)
{
//既是头节点,也是尾节点
newhead = newtail = cur;
newtail->next = NULL;
cur = cur->next;
}
else
{
//将每个节点头插到新链表中即可
struct ListNode* next = cur->next;
cur->next = newhead;
newhead = cur;
cur = next;
}
}
return newhead;
}
此时我们提交一下

会发现出错了,看用例发现只输出了一个结果,说明没连接上;
而我们头插的逻辑又是没问题的,那么问题就在于这个既是头节点又是尾节点的处理中;
cpp
newhead = newtail = cur;
newtail->next = NULL;
cur = cur->next;
我们将newtail->next = NULL之后,不就相当于cur->next = NULL,接着cur = cur->next,不就相当于直接将cur置为空了;
那么循环肯定进不来了,导致只会输出一个结果
因此我们只需暂存下一个节点的信息,然后再赋值给cur即可
cpp
struct ListNode* reverseList(struct ListNode* head)
{
struct ListNode* newtail,*newhead = NULL;
struct ListNode* cur = head;
while(cur)
{
struct ListNode* next = cur->next;
if(newhead == NULL)
{
//既是头节点,也是尾节点
newhead = newtail = cur;
newtail->next = NULL;
cur = cur->next;
}
else
{
//将每个节点头插到新链表中即可
cur->next = newhead;
newhead = cur;
}
cur = next;
}
return newhead;
}

3、合并两个有序链表
双指针法:
题目中明确表示两个链表均递增,因此很容易想到双指针遍历两个链表,根据curA->val 与 curB->val来确定添加节点的先后顺序;
首先创建两个cur指针,接着创建newhead和newtail用来表示新链表的头节点和尾节点;
核心目的就是通过双指针的遍历,至少会将一个链表全部遍历完,后续再处理剩下的节点时直接挂上对应的cur节点即可
cpp
struct ListNode* curA = list1;
struct ListNode* curB = list2;
struct ListNode* newhead = NULL,*newtail = NULL;
while(curA && curB)
{
//至少遍历完一个链表
if(curA->val < curB->val)
{
struct ListNode* next = curA->next;
//将curA尾插到链表中
if(newtail == NULL)
{
//既是头节点也是尾节点
newhead = newtail = curA;
}
else
{
//尾插即可
newtail->next = curA;
newtail = curA;
}
curA = next;
}
else
{
struct ListNode* next = curB->next;
//将curB尾插到链表中
if(newtail == NULL)
{
//既是头节点也是尾节点
newhead = newtail = curB;
}
else
{
//尾插即可
newtail->next = curB;
newtail = curB;
}
curB = next;
}
}
当上述代码执行完之后,再处理未遍历完的链表;
此时我们可以选择用两个while循环,一个处理curA,一个处理curB
cpp
if(newtail == NULL)
{
//说明至少一个链表是空的
//因此直接返回那个非空的链表即可
return curA ? curA : curB;
}
if(curA)
{
//尾插到newtail中
newtail->next = curA;
}
if(curB)
{
//尾插到newtail中
newtail->next = curB;
}
这样就将剩下的节点处理完了;
既然逻辑是如果curA不为空,就将curA挂在newtail的后面,否则就将curB挂在newtail的后面;
不妨也用三目操作符进行优化
cpp
newtail->next = curA ? curA : curB;
简洁、高效

再此基础上,如果我们增加虚拟头节点呢?
是不是会让逻辑更加清晰?
来尝试写代码
cpp
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
struct ListNode* dummyhead = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* cur = dummyhead;
struct ListNode* newtail = NULL;
while(list1 && list2)
{
if(list1->val < list2->val)
{
cur->next = list1;
list1 = list1->next;
}
else
{
cur->next = list2;
list2 = list2->next;
}
cur = cur->next;
}
cur->next = list1 ? list1 : list2;
return dummyhead->next;
}
和之前不同的是,我们不再创建curA curB 而是直接使用list1、list2;
创建虚拟头指针,并用cur遍历新的链表;
最终返回dummyhead的下一个节点即可

4、链表的中间节点
思路一:
遍历一遍链表,同时利用cnt记录总的节点个数
然后循环cnt次,直接返回即可
cpp
struct ListNode* middleNode(struct ListNode* head)
{
int cnt = 0;
struct ListNode* cur = head;
//获取总节点个数
while(cur)
{
cnt++;
cur = cur->next;
}
int ans = cnt / 2 + 1;
//由于直接指向头节点,因此会少走一步
ans -= 1;
struct ListNode* ret = head;
while(ans--)
{
ret = ret->next;
}
return ret;
}

还有没有其他方法了?
下面我们用一种新的方法,也是利用两个指针;
通过快慢指针,只需遍历一遍链表即可
思路二:
定义两个指针,一个快指针,一个慢指针;
快指针一次走两步,慢指针一次走一步;
当快指针走到终点时,慢指针正好到达链表中间的位置
由于快指针一次走两步,因此循环结束的条件应该是fast && fasr->next != NULL;
下面来实现代码
cpp
struct ListNode* middleNode(struct ListNode* head)
{
struct ListNode* fast = head;
struct ListNode* slow = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}

如有不足之处恳请指出!!!
谢谢大家!!!