题目描述:
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

思路:
链表中出现交换字眼时,就要考虑是否要使用虚拟头节点了,本题也不例外,依旧使用了虚拟头节点这一重要技巧。我们定义一个虚拟头节点dummy,用cur指针指向dummy,现在是cur—>1—>2—>3,我们要让cur指向2,再让2指向1,最后让1指向3,如图:

这样就实现两个节点的交换了,注意我们没有更改节点里的值,只是改变了对应的顺序。接下来用一个while循环不断重复这个过程,直到cur的下一个或者下下一个节点为null时终止循环(因为链表节点个数可能是偶数也可能是奇数,偶数时cur.next为null,奇数时cur.next.next为null),再返回dummy的下一个节点,也就是新链表的头节点,大功告成。
难点:
本题有两个值得注意的地方,一个是while括号内的条件,一定是cur.next在前,cur.next.next在后,假设cur.next已经为null了,如果cur.next.next的判断在前就会变成对一个空指针进行判断,这会造成空指针异常报错。其次就是要提前存储两个节点的值,以上图为例就是1和3,如果不提前存储,2就没法指向1了,1也没法指向3,因为此时cur.next被赋值了以后,我们就找不到1和3节点了,昨天的题中也有类似的思路。
代码示例:
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var swapPairs = function(head) {
let dummy = new ListNode(0,head)
let cur = dummy
while(cur.next && cur.next.next){
let temp = cur.next
let temp1 =cur.next.next.next
cur.next = cur.next.next
cur.next.next = temp
temp.next = temp1
cur = cur.next.next
}
return dummy.next
};
题目描述:
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

思路:
虚拟头节点不必多说,本题的重头戏在于如何找到这个倒数第n个节点的前一个节点,这就要用到双指针了,如果要删除第n个节点,我们就让快指针先移动n+1个节点,再让快慢指针同时移动,直到快指针指向null,此时的慢指针就指向了删除节点的上一个节点。下面阐述具体步骤:
第一步 ,定义fast指针与slow指指针,指向虚拟头节点:

第二步,让fast走n+1步,方便后续同时移动到末尾后能让slow指向删除节点的前一个节点,再让fast和slow同时移动,直到fast指向null


最后直接将slow的下一个节点删除,这里操作与之前的删除题一致,不再过多赘述。

难点:
本题的难点在于理解为什么要让fast走n+1步,其实走n步也可以,那后面while判断的条件就得改成fast.next !== null,要知道我们移动fast的目的就是为了控制slow指针,使它能刚好移动到被删除节点的前一个,数学推导的话自己画一个简单的图就能更快看懂了。还有就是老生常谈的空指针异常的问题,最后一步慢指针可能指向了链表的最后一个节点,那么此时slow.next就是一个空指针,如果此时继续上面的删除操作,就会触发空指针异常报错,这里我用了一个简单的if语句判断去解决这个问题。
代码示例:
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} n
* @return {ListNode}
*/
var removeNthFromEnd = function (head, n) {
let dummy = new ListNode(0, head)
let fast = dummy
let slow = dummy
for(let i = 0 ; i <= n ; i++){
fast = fast.next
}
while (fast !== null) {
fast = fast.next
slow = slow.next
}
if (slow !== null) {
slow.next = slow.next.next
}
return dummy.next
};
题目描述:
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
图示两个链表在节点 c1 开始相交:

题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
思路:
本题的关键在于求出两个链表交点的指针,这里很重要,是指针相等而非数值相等。二者最大的区别在于指针相等代表后续节点都相等,数值相等仅代表此节点相等。这题我们使用双指针解法,让两条链表的末尾对齐,那么为什么是末尾而不是头对齐呢,举个简单的例子:listA = (1,2,3,6,7)listB = (4,5,6,7),如果我们同时从1和4开始逐个比较,永远找不到相交点,对齐末尾的意义便在于此,我们让两个指针从距离末尾相同位置开始遍历,这样它们会同时到达相交点(如果存在)。这是一种巧妙的归一化处理,把两个不同长度的链表问题转化成两个相同“有效长度”链表的问题。下面是详细的解题步骤:
首先我们让curA指向链表A的头节点,curB指向链表B的头节点

然后分别求出两个链表的长度,求出两个链表长度的差值,用一个for循环不断移动curA,使它移动到与curB到末尾相同距离的位置,如图:

最后用一个while循环判断curA和curB是否相等,不相等就同时移动curA与curB,只需要在循环结束后加一个return curA即可,无需return null,如果循环一直未找到curA === curB直到cur===null时,循环会终止,此时返回得curA就是null
难点:
本题必须理解curA ===curB ,而非curA.val === curB.val这一点,链表相交的概念可得搞清楚了。其次就是处理长度不一致的情况,这里使用了对齐末尾法,其实也可以使用双指针交替遍历。最后就是边界情况处理的难点,在头尾节点相交,或一个链表为空,亦或者是链表有环(本题假设无环),这都是需要我们去考虑的情况。
代码示例:
var getListLen = function(head) {
let cur = head
let count = 0
while(cur){
count++
cur = cur.next
}
return count
}
var getIntersectionNode = function(headA, headB) {
let curA =headA
let curB = headB
let lenA = getListLen(headA)
let lenB = getListLen(headB)
if(lenA < lenB){
[lenA,lenB]=[lenB,lenA];
[curA,curB]=[curB,curA];
}
let i = lenA - lenB
while(i-- > 0){
curA =curA.next
}
while(curA && curA !== curB){
curA = curA.next
curB = curB.next
}
return curA
};
题目描述:
题意: 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示:

那么相遇时: slow指针走过的节点数为: x + y, fast指针走过的节点数:x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。
因为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同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。
动画如下:

就是这么简单的实现过程,关键是你要能用数学推导出:头节点和相遇点到环入口距离相等,这一结论,只要此结论一出,相信后续敲代码不成问题。
补充:关于为什么slow的步数是x+y而不是x+y+若干环长度。因为,首先,slow进环要比fast慢,slow进环的时候,fast一定在环的任意一个位置,如图:

slow一定没有走到环入口3,而fast已经到环入口3了,这说明slow开始走的那一环已经和fast相遇了。
代码示例:
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var detectCycle = function (head) {
let fast = head
let slow = head
while (fast !== null && fast.next !== null) {
slow = slow.next
fast = fast.next.next
if (fast === slow) {
let index1 = head
let index2 = fast
while (index1 !== index2) {
index1 = index1.next
index2 = index2.next
}
return index1
}
}
return null
};
链表章节总结:这两天重温链表,深刻的让我体会到了我一刷链表的时候到底是有多水,很多时候对着题解看两下,敲出来就算过了,没深挖过背后的原理。就好比链表相交这道题,其实我之前完全没理解指针相等和数值相等的区别,草草过了力扣就略过了这道题,这也是为什么我今天在这道题中反复提及二者的区别,就是为了警醒自己,也是为了自己未来三刷回顾笔记时能想起来原来我被这个点卡住过。虚拟头节点的使用倒是没啥,这个一刷时用多了就理解了,只是现在发现基本上只有增删改的时候用,查就不怎么用得到,算是对它的理解有了小小的加深吧。环形链表这道题,包括昨天的设计链表,都是一刷时就比较痛苦的题,侧重点不同,前者的数学推导让我苦恼,后者的功能繁多让我debug到头秃,二刷其实也没多大改善,因为换了语言,我甚至觉得设计链表更难了哈哈,基本语法还是得过过关,明天就到哈希表了,今天也是借着这个休息日追上群里录友们的进度了,可喜可贺呀可喜可贺。

被折叠的 条评论
为什么被折叠?



