文章简述
你们好,本篇是我的的第4篇文章。java
承接第3篇文章《开启算法之路,还原题目,用debug调试搞懂每一道题》,本篇文章继续分享关于链表的算法题目。node
本篇文章共有5道题目算法
一,反转链表(经典题目)
1.1.1 题目分析
反转链表是经典的题目,题中信息描述很清晰,给定一个单链表,将其反转。数组
先说说有什么思路呢?从题中给的案例输出结果看,是否是只须要将输入的链表的指针改为相反方向,就能够获得要输出的结果。缓存
就比如以下图所示:ide
可是问题来了,咱们是单链表,是没办法将下个节点直接指向该节点的上个节点。this
所以就须要定义一个辅助指针,用来指向该节点的上个节点,这样就能完成,以下图所示。debug
那按照咱们上面分析也就是将cur指针指向pre节点就能够了。3d
注意:此处有坑
指针
当咱们将当前节点【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道题目,基本已经包含了链表简单题目的全部类型。当你把本篇文章的题目看完后,关于链表的简单题目你也已经作完了。
本人已经将链表的全部简单题目刷完,总结出来的结论即套路都是同样的。简单来讲,大部分的题目均可以利用双指针,递归,数组来完成。
在下篇文章中会对链表的简单题目作一个小总结。
最后,求关注
原创不易,每一篇都是用心在写。若是对您有帮助,就请一键三连(关注,点赞,再转发)
我是杨小鑫,坚持写做,分享更多有意义的文章。
感谢您的阅读,期待与您相识!