在上一篇文章中,我们学习了链表中的"单向链表",但学可不代表就是学会了,能够运用链表的地方比比皆是,解题方法也是层出不穷,今天就让我们巩固一下"单向链表"的知识吧~
第一题:相交链表
📚 思路提示:
想要判断链式结构中是否存在环,就要证明两者有相交,所以就要找到两者的相交点。而两个单链表的长度时常有所不同,所以遍历时就可能出现错过相交点的情况,因此我们最好想一个"特殊的起始点",即为"较长链表的中间结点,并且从该结点开始到最后的长度等于较短链表的长度"。
想要拿到这个结点并不难,我们只需要求出两个链表各自的长度,再求出链表长度的差值,这个差值为几,就让较长链表向后走几步,走完后较长链表剩下的部分链表长度,就正好等于较短链表的长度了~
⭐ 图解:
我们只需要注意,遍历完两个链表后仍然没有相交点即为"非相交链表"就好了~
📖 代码示例:
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode curA = headA;
int lena = 0;
ListNode curB = headB;
int lenb = 0;
while(curA != null){
curA = curA.next;
lena++;
}
while(curB != null){
curB = curB.next;
lenb++;
}
int num = Math.max(lena,lenb) - Math.min(lena,lenb);
if(lena > lenb){
while(num-- > 0){
headA = headA.next;
}
}else {
while(num-- > 0){
headB = headB.next;
}
}
while(headA != headB){
headA = headA.next;
headB = headB.next;
if(headA == null || headB == null){
return null;
}
}
return headA;
}
}
第二题:反转链表
📚 思路提示:想要做到指定区间内反转单链表,我们需要先学会如何整体反转单链表。
关于单链表我们知道,结点只能一直向前走,不能往回走,而且想要两个结点互换位置,就代表至少需要两个结点来进行操作,而我们不仅仅想交换两个结点,交换后我们还需要让结点继续往后走,直到将需要的范围全部进行反转才可以。
而想要交换后再让结点继续往后走,只用两个结点就会出现问题了,比如上图中我们想将" 2 "和" 3 "的位置相互交换,所以交换后" 3 "的下一个就应该是" 2 ",而这样就会出现"丢失原链表"的问题,所以我们还需要第三个链表用来确保结点能够正常的后移~
(为防止访问越界,我们可以将第三个链表放入循环内部定义)
我们来演示一下整体反转单链表是何种过程。
⭐ 图解:
这就是整体反转单链表的全过程了,还是比较好理解的~
(记得把反转链表的末尾加上null)
📖 代码示例:
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null){
return head;
}
ListNode cur = head;
ListNode curn = head.next;
while(curn != null){
ListNode curN = curn.next;
curn.next = cur;
cur = curn;
curn = curN;
}
head.next = null;
return cur;
}
}
第三题:反转链表 II
📚 思路提示:学会了链表的反转之后,我们来尝试一下这道题,本质上虽然还是反转链表,但是相比上一题,这一题需要考虑的就比较多了。
首先我们先设置一个计数器,从头开始遍历链表,每移动一个结点,计数器相应加一,直到计数器与left相等时,我们开始反转链表~并且在反转链表的途中,left还要时刻加一,直到计数器与right相等时,我们的反转链表工作结束~
(这里就不画图演示了,和上一题是一样的~)
还记得嘛?上一题虽然主要考察反转链表,但是就算会了反转链表,如果忘记了将 " head = null " 也还是没有用的~而 " head = null " 是因为将链表整体反转后,我们的尾结点就不再是之前的尾结点了,也正是因为如此,新的尾结点后面便不是 "null" ,而是" 原链表中第二个结点 ",这是因为 "新的尾结点是原来的头结点,原头结点的next就是第二个结点"~
上面就是一种情况,所代表的是"从头到尾全部反转",即"left == 1 && right == 链表长度"
(时间复杂度为O(n)的情况下无法拿到链表长度,所以我们可以通过"反转列表尾结点的next是否为null"来确认right是否为链表长度~)
而就这两种因素来说,能够组成四种不同的组合,这也就是解题的关键了。
⭐ 图解:
(以下对应操作中,lcur代表"反转链表首结点的前一结点",Lcur代表"反转链表首结点")
📕 (left == 1 && curn != null)
📖 对应操作:head.next = curn;
📕 (left != 1 && curn == null)
📖 对应操作:lcur.next = cur;
Lcur.next = null;
📕 (left != 1 && curn != null)
📖 对应操作:lcur.next = cur;
Lcur.next = curn;
📕 (left == 1 && curn == null)
📖 对应操作:Lcur.next = null;
📖 代码示例:
class Solution {
public ListNode reverseBetween(ListNode head, int m, int n) {
if (head == null || head.next == null || m == n) {
return head;
}
int left = 1;
ListNode cur = head;
// 用于保存反转链表首结点的前一结点
ListNode lcur = null;
// 用于保存反转链表首结点
ListNode Lcur = null;
while (cur != null && left != m) {
//获取反转链表首结点的前一结点
if (left == m - 1) {
lcur = cur;
}
cur = cur.next;
left++;
}
//获取反转链表首结点
Lcur = cur;
ListNode curn = cur.next;
while (curn != null && left != n) {
ListNode curN = curn.next;
curn.next = cur;
cur = curn;
curn = curN;
left++;
}
if(m == 1 && curn != null){
head.next = curn;
}else if(m != 1 && curn == null){
lcur.next = cur;
Lcur.next = null;
}else if(m != 1 && curn != null){
lcur.next = cur;
Lcur.next = curn;
}else{
Lcur.next = null;
}
if(m != 1){
return head;
}
return cur;
}
}
第四题:链表的中间结点
📚 思路提示:这题要求的是中间结点,相信聪明的你稍微思考一下就有思路咯~对的,其实就是使用快慢指针,我们只需要创建两个结点," slow = head " 和 " fast = head ",当 fast 走到末尾的时候,slow所处的位置就是中间结点了。
⭐ 图解:
📖 代码示例:
class Solution {
public ListNode middleNode(ListNode head) {
if(head == null){
return null;
}
ListNode slow = head;
ListNode fast = head;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
}
第五题:链表的回文结构
📚 思路提示:回文结构相信对大家来说都不陌生吧~这是一种" 向前和向后读都相同的序列 "。
或许大家心里想的已经很简单了吧~可能想着"全反转一遍,再和原来的链表对比一下~",其实并没有这么简单,我们注意,题目要求我们设计时间复杂度为O(n)的解题方法,这也就代表我们必须只遍历一次链表就判断出它是否为"回文结构"。
虽然不能"全反转一遍",但反转在这题中确实是非常至关重要的一步~相信经过上两道题的练习,大家已经能够流畅的反转链表了~
因为回文结构 "从前和向后读都相同",也就代表了后半部分的反转链表应该等于前半部分的链表,所以此题中我们需要反转的部分是"链表的后半部分"~
而想要拿到后半部分,我们就要找到中间节点,这也是我们的上一题呀~这回思路通畅了吧~看看图解吧!
⭐ 图解:
📕 寻找中间结点
📕 反转链表
这里大家就能看出一些问题了~没错,奇数个结点和偶数个结点的情况是不同的~所以我们对比的时候采用这这样的策略:
📕 最后对前后链表进行对比时,将最后一对结点拿出循环单独进行对比
📖 代码示例:
class Solution {
public boolean isPalindrome(ListNode head) {
if(head == null || head.next == null){
return true;
}
ListNode slow = head;
ListNode fast = head;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
//保存初始反转结点
ListNode c = slow;
ListNode cur = slow.next;
while(cur != null){
ListNode curn = cur.next;
cur.next = slow;
slow = cur;
cur = curn;
}
//将新的尾结点next = null
c.next = null;
//保存反转后反转链表表头
cur = slow;
//slow.next != null 是将最后一对结点保留
//用于后续对"奇数个结点"和"偶数个结点"进行区分
while(slow.next != null){
//值不相同则不是回文链表
if(head.val != slow.val){
return false;
}
slow = slow.next;
head = head.next;
}
//head == cur 代表奇数个结点
//head.val == slow.val 代表偶数个结点
if(head == cur || head.val == slow.val){
return true;
}
return false;
}
}
第六题:环形链表
📚 思路提示:这题并不难,我们只需要创建一对快慢指针,在两者都不等于null的情况下一直循环,如果在某一刻两个结点相遇了,就代表这是一个环形链表~
⭐ 图解:
📖 代码示例:
public class Solution {
public boolean hasCycle(ListNode head) {
if(head == null){
return false;
}
ListNode slow = head;
ListNode fast = head;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
if(slow == fast){
return true;
}
}
return false;
}
}
第七题:环形链表 II
📚 思路提示:这题感觉属于是一个数学题...我们直接看图解:
⭐ 图解:
而最后得到的 X = Y 则代表"相遇点到入口的距离" = "头结点到入口的距离",这样一来问题也就迎刃而解了,我们只需要找到两个结点的相遇点,然后相遇点的结点与头结点同时一步一步的移动,直到两者相遇,这个结点就是入口结点~
📖 代码示例:
public class Solution {
public ListNode detectCycle(ListNode head) {
if(head == null || head.next == null){
return null;
}
ListNode slow = head.next;
ListNode fast = head.next.next;
while(slow != fast){
if(fast == null || fast.next == null){
return null;
}
slow = slow.next;
fast = fast.next.next;
}
slow = head;
while(slow != fast){
slow = slow.next;
fast = fast.next;
}
return slow;
}
}
那么这次关于链表的练习题就为大家分享到这里啦,作者能力有限,如果有讲得不清晰或者不正确的地方,还请大家在评论区多多指出,我也会虚心学习的!那我们下次再见哦~