目录
- 1.删除单链表中的元素
- 1.1 删除排序链表中的重复元素
- 1.2 删除排序链表中的重复元素Ⅱ
- 1.3 移除链表元素
- 2.反转链表
- 2.1 反转链表
- 2.2 反转链表Ⅱ
- 3.查找链表中结点
- 3.1 链表的中间结点
- 3.2 链表中倒数第k个节点
- 4.回文链表
- 5.相交链表
- 6.合并链表
知识补充:
递归三要素:
- 大问题能拆成若干个小问题的解。
- 拆分后的子问题和原问题除了数据规模不同,思路完全相同。
- 存在问题的终止条件,不借助任何外部函数的特殊值,直接得出答案。
链表问题三大常用方法:
- 虚拟头节点法
- 递归
- 快慢指针法
1.删除单链表中的元素
1.1 删除排序链表中的重复元素
题目链接: 83.删除链表中的重复元素
题目内容:给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次返回已排序的链表 。
示例:
输入:head = [1,1,2,3,3]
输出:[1,2,3]
解法1:迭代
要找重复元素,这里使用两个引用:prev,cur
- base case:若链表为空或只有一个元素,肯定不会有重复元素,直接返回即可。
- 当链表长度大于1时,给定一个虚拟头结点,让prev指向虚拟头结点,cur指向后一个节点。判断这两个节点的值是否相同。
- 若不相同,则prev=prev.next。
- 若相同,prev.next=cur.next,使prev指向相同元素节点的下一个节点,cur=cur.next,再继续判断,重复以上过程。
- 最后返回dummyHead.next即可。
public ListNode deleteDuplicates(ListNode head) {//base caseif(head==null || head.next==null){return head;}//此时链表一定两个节点//虚拟头节点ListNode dummyhead=new ListNode(-101);dummyhead.next=head;//prev指向的一定不是重复元素ListNode prev=dummyhead;//双指针比较元素是否重复ListNode cur=prev.next;while(cur!=null){if(prev.val==cur.val){prev.next=cur.next;}else{prev=prev.next;}cur=cur.next;}return dummyhead.next;}
解法2:递归
链表是天然的递归结构。
首先,明确deleteDuplicates()
方法的作用是删除所有重复的元素,得到一个所有元素只出现一次的已排序的链表 。
那么,整个链表就可以分为 head+除head以外的所有节点
即head+deleteDuplicates(head.next)
此时deleteDuplicates(head.next)
已经是一个没有重复元素的链表,那么只需要判断head和此链表的head的值是否相同。
步骤:
- 边界条件,若链表为空或只有一个元素,肯定不会有重复元素,直接返回即可。
- 要删除重复元素,先把以head.next为头结点的子链表中的所有重复元素删除完。
- 最后比较head和head.next的值。
public ListNode deleteDuplicates(ListNode head) {//base caseif(head==null || head.next==null){return head;}head.next=deleteDuplicates(head.next);return head.val==head.next.val ? head.next:head;}
1.2 删除排序链表中的重复元素Ⅱ
题目链接: 82.删除排序链表中的重复元素Ⅱ
题目内容: 给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。
示例:
输入:head = [1,2,3,3,4,4,5]
输出:[1,2,5]
本题和上一题不同的地方在于,只留下不同的元素,重复元素一个不留。
解法1:迭代
这里使用三指针法:prev,cur,sec
- prev一定指向不同元素
- sec一定指定cur的下一个
步骤:
- 边界条件,若链表为空或只有一个元素,肯定不会有重复元素,直接返回即可。
- 当
sec==null
时,循环结束,没有可比较的对象了。- 当
cur.val != sec.val
,prev=prev.next
,prev向后移动。- 当
cur.val==sec.val
时,循环判断,sec一直向后移动,直到cur.val != sec.val
,此时prev.next指向sec。- cur移动到sec的位置。在新一轮循环中,让sec=cur.next。继续判断。
public ListNode deleteDuplicates(ListNode head) {//base caseif(head==null || head.next==null){return head;}//虚拟头结点ListNode dummyHead=new ListNode(-101);dummyHead.next=head;ListNode prev=dummyHead;ListNode cur=prev.next;while(cur!=null){ListNode sec=cur.next;if(sec==null){break;}if(cur.val!=sec.val){prev=prev.next;}else{while( sec!=null && cur.val==sec.val){sec=sec.next;}//此时,sec和cur一定不相等prev.next=sec;}cur=sec;}return dummyHead.next;}
解法2:递归
链表可以看作head以head.next为头结点的子链表的组合。
思路:
- 边界条件,若链表为空或只有一个元素,肯定不会有重复元素,直接返回即可。
- 若
head.val !=head.next.val
,head.next直接指向以head.next为头结点的链表。- 若
head.val ==head.next.val
,此时头结点就是重复的节点,先处理头结点的情况,给定一个引用newHead等于head.next。判断head.val==newHead.val
,若相等,让newHead不停向后移动,直到不相等。此时newHead一定不是重复元素,返回deleteDuplicates(newHead)
。
public ListNode deleteDuplicates(ListNode head) {//base caseif(head==null || head.next==null){return head;}if(head.val !=head.next.val){head.next=deleteDuplicates(head.next);return head;}else{//头结点就是重复的节点,先处理完头结点的情况ListNode newHead=head.next;while(newHead!=null && head.val==newHead.val){newHead=newHead.next;}//此时newHead一定不是重复元素return(deleteDuplicates(newHead));}}
1.3 移除链表元素
题目链接: 203.移除链表元素
题目内容: 给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
示例:
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
解法1:迭代
思路:
- 边界条件,如何head为空,直接返回null。
- 给定一个虚拟头结点,连接head。给定一个引用prev指向虚拟头结点。(注意:prev一定指向值不为val的节点)
- 遍历链表。若
prev.next.val==val
,prev直接指向prev.next的下一个节点。- 若不相等,prev直接向后移动。
public ListNode removeElements(ListNode head, int val) {//1.base caseif(head==null){return null;}//虚拟头节点ListNode dummyHead=new ListNode();dummyHead.next=head;//prev一定指向值不为val的节点ListNode prev=dummyHead;while(prev.next!=null){if(prev.next.val==val){prev.next=prev.next.next;}else{prev=prev.next;}}return dummyHead.next;}
解法2:递归
链表可以看作head以head.next为头结点的子链表的组合。
思路:
- 边界条件,如何head为空,直接返回null。
removeElements(head.next,val)
一定是值不为val的链表。head.next直接指向removeElements(head.next,val)
。- 此时只需判断头节点的值是否为val。若是,返回head.next,否则返回head。
public ListNode removeElements(ListNode head, int val) {//base caseif(head==null){return null;}head.next=removeElements(head.next,val);return head.val==val ? head.next:head;}
2.反转链表
2.1 反转链表
题目链接:206.反转链表
题目内容: 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
解法1:头插法
思路:若题目没有空间限制,则可以遍历原链表,不断产生新节点,在头插到新链表中,最后返回新链表。
public ListNode reverseList(ListNode head) {//base case if(head==null || head.next==null){return head;}//虚拟头结点ListNode dummyHead=new ListNode(-1);//不断遍历原链表,产生新节点,头插到新链表while(head!=null){ListNode cur=new ListNode(head.val);cur.next=dummyHead.next;dummyHead.next=cur;head=head.next;}return dummyHead.next;}
解法2:原地反转
思路:
- 原先prev是指向cur的,反过来,让cur指向prev就实现了反转。
- 但是cur.next指向prev之后,剩下的链表就断开了找不到了,所以需要先用一个next记住原链表cur的下一个节点。
当cur为null时,prev刚好在最后一个节点,直接返回prev即可。
public ListNode reverseList(ListNode head) {//base caseif(head==null || head.next==null){return null;}ListNode prev=null;ListNode cur=head;while(cur!=null){ListNode next=cur.next;cur.next=prev;prev=cur;cur=next;}return prev;}
解法3:递归
思路:
reverseList(ListNode head)
的作用是得到一个反转后的链表。则可以把链表分为头节点和以head.next为头节点的反转后的链表。
此时只需要将head连接到子链表的尾端即可。
public ListNode reverseList(ListNode head) {if(head==null || head.next==null){return head;}ListNode next=head.next;ListNode newHead=reverseList(head.next);//拼接当前头节点和转后的子链表head.next=null;next.next=head;return newHead;}
2.2 反转链表Ⅱ
题目链接:92.反转链表Ⅱ
解法:
思路:
- 给定两个引用,记住left的前一个节点和right的后一个节点。
- 先将待反转的区域反转
- 把 pre 的 next 指针指向反转以后的链表头节点,把反转以后的链表的尾节点的 next 指针指向 succ。
public ListNode reverseBetween(ListNode head, int left, int right) {//base caseif(head==null || head.next==null){return head;}ListNode dummyHead=new ListNode(-1);dummyHead.next=head;ListNode prev=dummyHead;//从temp开始走,来到left的前一个节点for(int i=0;i<left-1;i++){prev=prev.next;}//从prev开始走,一直到right节点ListNode rightNode=prev;for(int i=0;i<right-left+1;i++){rightNode=rightNode.next;}//截取要反转的链表ListNode next=rightNode.next;ListNode leftNode=prev.next;//切断连接prev.next=null;rightNode.next=null;//反转链表子区间reverseList(leftNode);//接回原链表prev.next=rightNode;leftNode.next=next;return dummyHead.next;}public ListNode reverseList(ListNode head) {ListNode prev=null;ListNode cur=head;while(cur!=null){ListNode next=cur.next;cur.next=prev;prev=cur;cur=next;}return prev;}
3.查找链表中结点
3.1 链表的中间结点
题目链接:876.链表的中间结点
题目内容: 给你单链表的头结点 head ,请你找出并返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
示例:
输入:head = [1,2,3,4,5]
输出:[3,4,5]
解释:链表只有一个中间结点,值为 3 。
解法1:按长度查找
步骤:
- 遍历原链表,得到链表的长度
- 若长度为奇数,直接走 长度/2 的步数。
- 若长度为偶数,也是直接走 长度/2 的步数。
public ListNode middleNode(ListNode head) {int count=0;ListNode temp=head;while(temp!=null){temp=temp.next;count++;}int n=count/2;while(n>0){head=head.next;n--;}return head;}
解法2:快慢指针法
思路:
- 两个指针,low 和 fast
- low每走一步,fast就走两步
- 当fast走到终点,low就停在我们想要的位置
public ListNode middleNode(ListNode head) {ListNode low=head;ListNode fast=head;while(fast!=null && fast.next!=null){low=low.next;fast=fast.next.next;}return low;}
3.2 链表中倒数第k个节点
题目链接:剑指Offer 22.链表中倒数第k个结点
题目内容: 输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
示例:
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
解法1:按长度查找
思路:
- 遍历链表,得到链表的长度n。
- 倒数第k个结点就是正数第n-k个结点。
public ListNode getKthFromEnd(ListNode head, int k) {if(head==null || k<=0){return null;}ListNode temp=head;int n=0;while(temp!=null){temp=temp.next;n++;}int count=n-k;while(count>0){head=head.next;count--;}return head;}
解法2:快慢指针法
思路:
- 给定两个引用sec,fir
- fir先向前走k步。
- sec 和 fir 同时向前走,当fir为null时,sec刚好指向倒数dik个结点。
public ListNode getKthFromEnd(ListNode head, int k) {if(head==null || k<=0){return null;}ListNode fir=head;ListNode sec=head;for(int i=0;i<k;i++){//判断k>链表长度的情况if(fir==null){return null;}fir=fir.next;}while(fir!=null){fir=fir.next;sec=sec.next;}return sec;}
4.回文链表
题目链接:234.回文链表
题目内容: 给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
示例:
输入:head = [1,2,2,1]
输出:true
解法
思路:
- 判断回文链表其实就是判断对称链表。
- 将链表一份为二,l1为头节点到中间结点的链表,l2为中间结点到尾节点的链表。
- 将l2反转,与l1的值进行对比。遍历,直到其中一条链表走到null,若有不一样的值,则不为回文链表。反之,则是回文链表。
public boolean isPalindrome(ListNode head) {if(head==null || head.next==null){return true;}ListNode middleNode=middleNode(head);//反转中间结点之后的链表ListNode l2=reverseList(middleNode);//找反例while(head!=null && l2!=null){if(head.val != l2.val){return false;}head=head.next;l2=l2.next;}return true;}//查找中间节点public ListNode middleNode(ListNode head) {int count=0;ListNode temp=head;while(temp!=null){temp=temp.next;count++;}int n=count/2;while(n>0){head=head.next;n--;}return head;}//反转链表public ListNode reverseList(ListNode head) {if(head==null || head.next==null){return head;}ListNode next=head.next;ListNode newHead=reverseList(head.next);//拼接当前头节点和转后的子链表head.next=null;next.next=head;return newHead;}
5.相交链表
题目链接:160.相交链表
题目内容: 给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。
示例:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at ‘8’
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
— 请注意相交节点的值不为 1,因为在链表 A 和链表 B 之中值为 1 的节点 (A 中第二个节点和 B 中第三个节点) 是不同的节点。换句话说,它们在内存中指向两个不同的位置,而链表 A 和链表 B 中值为 8 的节点 (A 中第三个节点,B 中第四个节点) 在内存中指向相同的位置。
解法:
如图所示:
- listA的长度=x+z
- listB的长度=y+z
但若让A和B都走一遍对方的路程:即 x+z+y=y+z+x。此时它们的路程一定相等,走完之后一定会相交。
过程图举例:
步骤:
- 引入两个引用listA,listB。
- listA走到终点后倒过来走listB。
- listB走到终点后倒过来走listA。
- 若不存在交点,则listA和listB最后都指向null。
- 若存在交点,则两个链表走完全程后一定指向交点。
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {//引入两个引用ListNode listA=headA;ListNode listB=headB;while(listA!=listB){listA = listA==null ? headB:listA.next;listB = listB==null ? headA:listB.next;}return listA;}
6.合并链表
题目链接:21.合并两个有序链表
题目内容: 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
解法1:虚拟头节点法
思路:
- 给定一个虚拟头结点dummyHead;
- 遍历两个链表。比较结点值的大小,将较小值拼接在新链表后面。
图解:
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {if(list1==null){return list2;}if(list2==null){return list1;}ListNode dummyHead=new ListNode(-1);ListNode tail=dummyHead;//取较小值拼接在链表的尾部while(list1!=null && list2!=null){if(list1.val<list2.val){tail.next=list1;tail=list1;list1=list1.next;}else{tail.next=list2;tail=list2;list2=list2.next;}}//此时,至少一个链表为Nullif(list1==null){tail.next=list2;}if(list2==null){tail.next=list1;}return dummyHead.next;}
解法2:递归
思路:
- 已知
mergeTwoLists(ListNode list1, ListNode list2)
方法的作用是将两个升序链表合并为一个新的 升序 链表并返回。- 如果list1.val<list2.val,此题可以看作
list1
+mergeTwoLists(ListNode list1.next, ListNode list2)
- 如果list2.val<list1.val,此题可以看作list2+
mergeTwoLists(ListNode list1, ListNode list2.next)
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {if(list1==null){return list2;}if(list2==null){return list1;}if(list1.val<=list2.val){list1.next=mergeTwoLists(list1.next,list2);return list1;}else{list2.next=mergeTwoLists(list1,list2.next);return list2;}}