文章简述
大家好,本篇是个人的第4篇文章。
承接第3篇文章《开启算法之路,还原题目,用debug调试搞懂每一道题》,本篇文章继续分享关于链表的算法题目。
本篇文章共有5道题目
一,反转链表(经典题目)
1.1.1 题目分析
反转链表是经典的题目,题中信息描述很清晰,给定一个单链表,将其反转。
先说说有什么思路呢?从题中给的案例输出结果看,是不是只需要将输入的链表的指针改成相反方向,就可以得到要输出的结果。
就好比如下图所示:
但是问题来了,我们是单链表,是没办法将下个节点直接指向该节点的上个节点。
因此就需要定义一个辅助指针,用来指向该节点的上个节点,这样就能完成,如下图所示。
那按照我们上面分析也就是将cur指针指向pre节点就可以了。
注意:此处有坑
当我们将当前节点【cur】指向上一个节点【pre】的时候,如何将指针向下移动呢?
此时的节点【cur】已经指向了上一个节点【pre】了,所以我们还需要一个临时变量去保存当前节点的下个节点,具体为什么这么做,我们在下面代码演示的时候debug看下过程。
接着我们上面的步骤,将指针向下移动,如图所示。
移动指针后,再将当前节点的next指针指向上一个节点。
最后当前节点没有下个节点的时候,就结束遍历,如图所示。
1.1.2 代码分析
按照套路,先初始化节点对象。
class ListNode {
int val;
ListNode next;
ListNode() {
}
ListNode(int val) {
this.val = val;
}
ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
@Override
public String toString() {
return "ListNode{" +
"val=" + val +
'}';
}
}
创建单链表结构。
// 创建单链表
ListNode l1 = new ListNode(1);
ListNode l2 = new ListNode(2);
ListNode l3 = new ListNode(3);
ListNode l4 = new ListNode(4);
ListNode l5 = new ListNode(5);
NodeFun nodeFun = new NodeFun();
nodeFun.add(l1);
nodeFun.add(l2);
nodeFun.add(l3);
nodeFun.add(l4);
// 返回创建的链表
ListNode node = nodeFun.add(l5);
反转链表的代码。
public ListNode reverseListIteration(ListNode head) {
// 定义上节点辅助指针
ListNode pre = null;
// 定义当前节点辅助指针
ListNode cur = head;
// 循环当前节点不为空
while (null != cur) {
// 临时变量保存当前节点的下个节点
ListNode temp = cur.next;
// 当前节点的next指向上节点
cur.next = pre;
// 上节点向下移动
pre = cur;
// 当前节点指向下个节点
cur = cur.next;
}
return pre;
}
1.1.3 debug调试
节点初始化完成了,按照分析我们定义了2个节点,如上图第一次遍历【pre】节点是null,【cur】从第一个节点开始。
下一步debug调试我们先不急,回顾之前说的一个问题,为什么要将当前节点的下一个节点用临时变量保存,那我们直接看debug调试。
第一次遍历的时候,修改完指针后当前节点已经指向上一个节点了,再看上述题目分析的图解。
这就是为啥要先把当前节点的下个节点缓存起来。
上图debug我们看出,【cur】当前节点的指针已经指向null,下一步就是移动指针指向下一个节点。
我们再接着进行debug调试,按照上述分析,第二步循环就是将节点【2】指向上一个节点【1】,如下图所示。
现在当前节点【cur】已经指向【2】,那它的下个节点就是【1】,如下图所示。
经过上面的两步循环,成功的将指针进行了反转,剩下的节点循环也就如出一辙了。
当循环到最后节点【5】时,下个节点为null,此时结束while循环,而节点【5】也是指向了上一个节点【4】。
最后我们再看下运行结果。
二,回文链表
1.2.1 题目分析
如果做过字符串的算法题,里面有个回文字符串的题目。没错,它俩的意思是一样的。
看题目描述得知一个链表是不是回文链表,就是看链表就是看链表正读和反读是不是一样的。
假如说,我们拿到了后半部分链表,再将其反转。去和链表的前半部分比较,值相等就是回文链表了。
注意:
这种方式会破坏原链表的结构,为保证题目的一致性,最后再将链表再重新拼接
另外一种解题方式为:将整个链表节点遍历保存到数组中,而数组是有下标,并可以直接获取数组的大小,那么只需从数组的首尾去判断即可
反转链表上一道题我们已经分享了,现在重点是如何获取后半部分的链表。
我们再说说快慢指针的思想,通常我们定义2个指针,一个移动快,一个移动慢。详细的案例可以参考本人上一篇文章《开启算法之路,还原题目,用debug调试搞懂每一道题》,有一道关于快慢指针的题目。
定义慢指针每次移动1个节点,快指针每次移动2个节点,当然我们是需要保证快节点的下下个
个节点不为空。
slow = slow.next;
fast = fast.next.next;
其实快慢指针的思想就是,假设链表是一个回文链表,快指针比慢指针是多走一步,当快指针走完的时候,慢指针也就刚好走到该链表的一半。
上图中slow指针正好走到链表的一半,此时也就得到链表的后半部分了,即slow.next
。
1.2.2 代码分析
老套路,先创建一个回文链表。
ListNode l1 = new ListNode(1);
ListNode l2 = new ListNode(2);
ListNode l3 = new ListNode(2);
ListNode l4 = new ListNode(1);
NodeFun nodeFun = new NodeFun();
nodeFun.add(l1);
nodeFun.add(l2);
nodeFun.add(l3);
ListNode head = nodeFun.add(l4);
获取后半部分链表代码。
private ListNode endOfFirstHalf(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while (fast.next != null && fast.next.next != null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
反转链表的代码与上题目是一样的。
最后将两个链表进行判断是否是一样的。
// 判断是否回文
ListNode p1 = head;
ListNode p2 = secondHalfStart;
boolean flag = true;
while (flag && p2 != null) {
if (p1.val != p2.val) {
flag = false;
}
p1 = p1.next;
p2 = p2.next;
}
1.2.3 debug调试
先获取链表的后半部分。
debug开始循环后,fast直接走到链表的第3个节点【2】
slow.next就是链表的后半部分,再将后半部分进行链表反转
最后我们也就得到如下2个链表。
最后将这2个链表进行比较是否相等,相等则是回文链表。
三,链表的中间节点
1.3.1 题目分析
获取链表的中间节点乍一看和回文链表中使用快慢指针获取后半链表有点类似呢?
没错,这波操作是类似的,但也并不是完全一样,其主要思想还是快慢指针。
换句话说,如果你已理解了上面的题,那这道题也就不是什么事了。话不多说,先来分析一波。
同样我们还是定义slow慢指针每次移动一个节点,fast快指针每次移动2个节点。
那么fast快指针移动到最后节点时,slow慢指针也就是要返回的链表。
我想,你是不是有个疑问。就是为什么慢指针是移动一个节点,快节点移动2个节点?如果是偶数个节点,这个规则还正确吗!那就验证下。
为了方便,就继续上面节点的遍历。
题目中描述,如果有2个中间节点,返回第二个节点
,所以返回节点【4,5,6】也就符合要求了
1.3.2 代码分析
创建链表结构。
ListNode l1 = new ListNode(1);
ListNode l2 = new ListNode(2);
ListNode l3 = new ListNode(3);
ListNode l4 = new ListNode(4);
ListNode l5 = new ListNode(5);
NodeFun nodeFun = new NodeFun();
nodeFun.add(l1);
nodeFun.add(l2);
nodeFun.add(l3);
nodeFun.add(l4);
ListNode head = nodeFun.add(l5);
获取后半部分链表代码。
// 快慢指针
ListNode slow = head;
ListNode fast = head;
while(fast != null && fast.next != null){
//移动指针
fast = fast.next.next;
slow = slow.next;
}
return slow;
1.3.3 debug调试
快指针移动到节点【3】,慢指针移动到节点【2】
接着再走一步,快指针移动到节点【5】,慢节点移动到节点【3】,到此也就满足题意的要求了。
四,链表中倒数第k个节点
1.4.1 题目分析
这道题要求就是返回倒数K个节点,最笨的办法就是参考上面链表反转,先将链表反转。获取前K个节点,将获取的节点再次进行反转即可得到题目要求。
但是显然这种方式只能满足答案输出,经过上面的3道题目,有没有得到什么启发呢?
是的,这道题依然可以使用双指针解决,是不是感觉双指针可以解决所有的链表问题了(QAQ)。
再仔细一想,是不是感觉和上一道《链表的中间节点》题目很类似?获取链表的中间节点是返回后半部分节点,而本道题是要求返回指定K个节点。
那就直接说结论吧,同样是定义快慢指针。只不过在上道题中快指针是每次移动2个节点,本道题中给定的K,就是快指针移动的节点个数。
同样初始化指针都在首节点,如果我们先将fast指针移动K个节点。
到此才算初始化节点完成,剩下的操作就是遍历剩下的链表,直到fast指针指向最后一个节点。
一直遍历到fast节点为null,此时返回slow指针所指引的节点。
1.4.2 代码分析
初始化链表,由于和前几道题的操作是一样的,此处就不在展示。
获取倒数第K个节点的代码。
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode slow = head;
ListNode fast = head;
// 先将快指针向前移动K
while (k-- > 0) {
fast = fast.next;
}
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
1.4.3 debug调试
按照上面图解分析,fast快指针指向节点【3】的时候才算真正初始化快慢指针完成。
当快指针指向节点【5】时,slow慢节点指向节点【3】
注意:中间省略了一步,即慢指针指向节点【2】时,快指针指向节点【4】
节点【5】是最后一个节点,再次进入while循环。
最后一次循环时,慢指针指向了4,快指针下一个节点已经为null,此时结束循环。
五,移除重复节点
1.5.1 题目分析
这道题和上一篇中的题目【删除排序链表中的重复元素】是一样的,简单的做法即利用Set集合保存未重复的节点,再遍历链表判断是否已存在Set集合中。
因此本道题就不在多分析,直接贴上代码。
1.5.2 代码分析
Set<Integer> set = new HashSet<>();
ListNode temp = head;
while(temp != null && temp.next != null){
set.add(temp.val);
if(set.contains(temp.next.val)){
temp.next = temp.next.next;
}else{
temp = temp.next;
}
}
return head;
}
六,总结
本次文章共分享总结5道题目,仔细分析有没有发现这些题套路都是一样的。都利用了双指针的思想,通过一定的规则移动快慢指针获取指定链表节点。
本次的5道题目和上次的3道题目,基本已经包含了链表简单题目的所有类型。当你把本篇文章的题目看完后,关于链表的简单题目你也已经做完了。
本人已经将链表的所有简单题目刷完,总结出来的结论即套路都是一样的。简单来说,大部分的题目都可以利用双指针,递归,数组来完成。
在下篇文章中会对链表的简单题目做一个小总结。
最后,求关注
原创不易,每一篇都是用心在写。如果对您有帮助,就请一键三连(关注,点赞,再转发)
我是杨小鑫,坚持写作,分享更多有意义的文章。
感谢您的阅读,期待与您相识!