今天的题目对我来说是蛮有难度的 很多题第一眼看并没有什么思路 不过综合来看 无论数组还是链表都需要经常去往双指针的方面去想
两两交换链表中的节点
思路
对于很多链表操作的问题 我们大多数对操作的节点都需要知道他的上一个节点 而针对头指针的情况 我们通常需要设置一个虚拟头节点 这题也不例外
操作步骤如下:
- 定义prev = 虚拟头节点,cur = head
- prev的next等于cur.next
- 用temp保存一下cur.next.next 因为一会他就要指cur了
- cur.next.next = cur , cur.next = temp
- 移动prev = cur , cur = cur.next
代码
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode dummyhead = new ListNode(-1, head);
ListNode prev = dummyhead;
ListNode cur = dummyhead.next;
ListNode temp;
while (prev.next!=null&&prev.next.next!=null) {
prev.next = cur.next;
temp = cur.next.next;
cur.next.next = cur;
cur.next = temp;
prev = cur;
cur = cur.next;
}
return dummyhead.next;
}
}
对于while循环的判断 这里说一下:针对偶数个节点 我们两两交换正常每个节点都能操作成功 而奇数个节点 最后一个节点我们不用操作 而因此while里面两个判断条件 (prev.next!=null&&prev.next.next!=null)注意第一个条件写在前面 否则如果是四个节点 prev.next.next会引发空指针异常
删除链表的倒数第N个节点
思路
这道题的关键就是如何去找到倒数第N个节点然后进行删除 这里笔者最初用了最暴力的解法:先遍历链表得到链表的长度 然后就好做了 一个for循环即可找到要删除的节点的前一个节点
不过这样很显然需要最多遍历两次链表 而是否可以遍历一次链表就可完成操作呢?这里还是用到了快慢指针
顺便一提 以后关于想到快慢指针的时候 可以多想一想这个题怎么利用快慢指针一个走得快一个走的慢的特性完成我们单个指针无法完成的事情
比如这道题:
- 定义fast指针和slow指针,初始值为虚拟头结点,如图:
-
fast首先走n + 1步 ,为什么是n+1呢,因为只有这样同时移动的时候slow才能指向删除节点的上一个节点(方便做删除操作),如图:
-
fast和slow同时移动,直到fast指向末尾,如题:
-
删除slow指向的下一个节点,如图:
代码
public class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummyNode = new ListNode(0);
dummyNode.next = head;
ListNode fastIndex = dummyNode;
ListNode slowIndex = dummyNode;
// 只要快慢指针相差 n 个结点即可
for (int i = 0; i <= n; i++) {
fastIndex = fastIndex.next;
}
while (fastIndex != null) {
fastIndex = fastIndex.next;
slowIndex = slowIndex.next;
}
//此时 slowIndex 的位置就是待删除元素的前一个位置。
//具体情况可自己画一个链表长度为 3 的图来模拟代码来理解
slowIndex.next = slowIndex.next.next;
return dummyNode.next;
}
}
利用快慢指针的特性 可以帮助我们找到要删除的节点
链表相交
思路
这题初看的时候完全没有任何思路 后来看到这样一句话才豁然开朗:较长的那个链表只会在最后n个节点内有交点,所以在最后那一块找就行,不用整个链表遍历浪费时间和内存。 什么意思呢? 就是如果两个链表如果相交 从交点到最后的节点一定都是一样的 又因为两个链表有长度之差 只需要把二者链表的尾端对齐 然后遍历看两个节点相等(肯定不会在前面有相交的 长度也不够啊)
把题理解透还是关键啊
代码
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
int lenA = 0;
int lenB = 0;
//需要分别获得两个链表的长度 判断一下哪个是长链表
for (ListNode node = headA; node != null; node = node.next) {
lenA++;
}
for (ListNode node = headB; node != null; node = node.next) {
lenB++;
}
if (lenA < lenB) {
return IntersectionNode(headB, lenB, headA, lenA);
} else {
return IntersectionNode(headA, lenA, headB, lenB);
}
}
public ListNode IntersectionNode(ListNode curA, int lenA, ListNode curB, int lenB) {
for (int i = 0; i < lenA - lenB; i++) { //移动指针到使其对齐
curA = curA.next;
}
while (curA != null && curB != null) {
if (curA == curB) { //节点相同就退出
return curA;
}
curA = curA.next;
curB = curB.next;
}
return null;
}
}
环形链表||
思路
这道题相当于分为两问 首先判断有没有环 其次是去找环的入口
判断有没有环 还是采用快慢指针的方式 让快指针一次移动两个单位 而慢指针一次移动一个单位 如果存在环 当慢指针进入环时 快指针一定已经进入环了 那么在环中快指针相对于慢指针一次移动一个单位 他们肯定可以碰上
为什么快指针一次移动两个单位 而不是三个或者四个单位呢 假设一次移动三个单位 那在环中 相对于慢指针一次移动两个单位 有可能把慢指针就给略过去了
流程如图所示:
那怎么去寻找环的入口呢 这里就涉及到数学推导了
假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示:
那么相遇时: slow指针走过的节点数为: x + y
, fast指针走过的节点数:x + y + n (y + z)
,n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。
为什么slow指针走过的节点数是x+y而不是x+ny呢 可以想像一下 当slow达到环形入口的时候 fast一定是在环的某一处 当fast追上slow时 最长花费的时间也不过(环的长度/2)那么slow指针最多也就能走一半环的长度 一定是在slow没走完一圈就相遇了
因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:
(x + y) * 2 = x + y + n (y + z)
两边消掉一个(x+y): x + y = n (y + z)
因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。
所以要求x ,将x单独放在左面:x = n (y + z) - y
,
再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z
注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。
这个公式说明什么呢?
先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。
当 n为1的时候,公式就化解为 x = z
,
这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。
也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。
让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。
动画如下:
那么 n如果大于1是什么情况呢,就是fast指针在环形转n圈之后才遇到 slow指针。
其实这种情况和n为1的时候 效果是一样的,一样可以通过这个方法找到 环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。
代码
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null) { //偶数的话会走到最后一个null 奇数个数的话会走到倒数第二个节点 走到最后都没成证明无环
slow = slow.next;
fast = fast.next.next;
if (fast == slow) {
ListNode index1 = fast;
ListNode index2 = head;
while (index1 != index2) {
index1 = index1.next;
index2 = index2.next;
}
return index1;
}
}
return null;
}
}
这里还有一种不用数学推导也更容易理解的方式:
public class Solution {
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) {
return null;
}
ListNode slow = head, fast = head;
// 如果有坏,在环里相当于fast每次多走一步肯定最后会相遇
while (fast != null && fast.next != null && slow != fast) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
break;
}
}
// 无环返回
if (fast == null || fast.next == null) {
return null;
}
// 计算一下这个环的大小
int cnt = 1;
ListNode node = fast.next;
//从相遇节点的下一个节点开始直到碰上相遇节点
while (node != slow) {
node = node.next;
cnt++;
}
// 让s1ow和fast回归头节点,假设总长度为L,坏长度为x,我们可以先让fast走x步
// 因为fast比slow多走了一个环的长度,所以他们一步一步走相遇的时候一定是在入口!
slow = head;
fast = head;
for (int i = 0; i < cnt; i++) {
fast = fast.next;
}
while (slow != fast) {
fast = fast.next;
slow = slow.next;
}
return fast;
}
}