剑指 Offer 22. 链表中倒数第k个节点 - 力扣(LeetCode)
发布:2021年8月2日22:34:09
问题描述及示例
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
示例:
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
我的题解
我的题解1(双层while
循环)
主要思路比较直接,用双层while
循环来完成逐个遍历节点并计算当前遍历节点到链尾的距离length
。其中,外层while
循环用于遍历链表节点,而内层while
循环用于计算当前节点到链尾的距离。
用p
指针来充当内层循环中的头指针,计算距离length
的过程其实也是一种遍历节点的过程。只不过遍历的过程中会同时记录遍历过的节点数量从而达到计算当前节点距离链尾的长度的目的。如果计算得到的长度为k
,则说明当前所遍历的节点(也就是head
当前的指向)就是我们要找的那个节点。
注意:
p
指针大概就是充当了一个“侦查兵”的角色。每次head
移动到下一个节点时,就让p
去探探前面的路有多长。
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @param {number} k
* @return {ListNode}
*/
var getKthFromEnd = function (head, k) {
// p指针初始化指向为链首
let p = head;
// 外层while循环遍历链表节点
while (head) {
// 每次开始计算当前节点到链尾的距离时,都要将length置0
let length = 0;
// 内层 while 循环用于计算当前节点到链尾的距离
while (p) {
length++;
p = p.next;
}
// 上面计算的长度与k相等,则说明找到了我们想要的节点,返回当前节点
if (length === k) {
return head;
}
// 如果上面的长度不匹配,则head指向下一个节点,开始重复上面的过程
head = head.next;
// 当然也要让侦查小兵p指针回到司令官head的身边以便下一次让p做同样的探路任务
p = head;
}
return null;
};
提交记录
208 / 208 个通过测试用例
状态:通过
执行用时:76 ms, 在所有 JavaScript 提交中击败了85.02%的用户
内存消耗:39.1 MB, 在所有 JavaScript 提交中击败了76.93%的用户
时间:2021/08/02 22:37
上面的解法中用到了双层while
循环。这就有种类似穷举的感觉,但不是真正意义上的穷举,因为每次 计算完length
,都会进行一次判断来决定是否返回,如果k
值较大,那么head
就不用移动很多次了,反之就会导致多次计算路径。这个复杂度应该和双层for
循环是差不多的道理,所以应该也是 O(n2)。
我的题解2(只计算一次链表长度)
更新:2021年8月3日10:00:30
突然想到既然是要算链表长度,那其实只算一次就可以了。那就是直接把整个链表的长度给算出来,然后再由链表长度和k
算出head
指针要往后走几步不就可以了吗?
var getKthFromEnd = function (head, k) {
let p = head;
let length = 0;
// 这个while循环用于计算整个链表的长度
while (p) {
length++;
p = p.next;
}
// 这个while循环用于让head指针到达目标位置
while(length - k > 0) {
head = head.next;
k++;
}
return head;
};
提交记录
208 / 208 个通过测试用例
状态:通过
执行用时:72 ms, 在所有 JavaScript 提交中击败了 90.83% 的用户
内存消耗:39.2 MB, 在所有 JavaScript 提交中击败了 55.81% 的用户
时间:2021/08/03 09:58
可以看到,修改之后的执行用时有了改善,但是不知道为啥内存消耗反而还表现差了点,按理来说内存消耗应该和上面的差不多的,有的时候同样的提交好几次的表现都不一样。em……所以还是按理论分析吧。上面的程序中将原来的双层嵌套的while
循环变成了两个顺序执行的while
循环,所以理论上来说的话,这种解法的时间表现应该要好一点,而空间表现应该和之前的一样。
其实在这种解法之前,还有一个小版本。那就是我用一个变量接收了length - k
的值,这就导致其空间表现远不如之前的程序。
一开始是用diff
变量接收length - k
的值,然后用while
循环控制head
向前走的步数。
var getKthFromEnd = function (head, k) {
// 计算链表长度的代码和上面的是一样的
// ……
let diff = length - k;
while (diff > 0) {
head = head.next;
diff--;
}
return head;
};
执行用时:72 ms, 在所有 JavaScript 提交中击败了 91.27% 的用户
内存消耗:39.4 MB, 在所有 JavaScript 提交中击败了 16.29% 的用户
时间:2021/08/02 23:30
然后是尝试用for
循环控制head
向前走的步数。(感觉和上面的那个没啥大差别,哈哈)
var getKthFromEnd = function (head, k) {
// 计算链表长度的代码和上面的是一样的
// ……
for(let i = 0; i < length - k; i++) {
head = head.next;
}
return head;
};
执行用时:72 ms, 在所有 JavaScript 提交中击败了 91.27% 的用户
内存消耗:39.4 MB, 在所有 JavaScript 提交中击败了 9.25% 的用户
时间:2021/08/02 23:34
后来我才想到其实不用特意弄一个变量来接收length - k
的值,直接控制k
自增或者length
就可以了。
我的题解3(不用计算链表长度——双指针)
更新:2021年8月3日10:59:46
我昨天一直在想一种不用计算链表长度的解法,但是如果不计算链表长度的话,又怎么确定该head
该往前走几步呢?我们只能从传入的参数k
知道一个距离值,我想应该还是要用双指针,但是总感觉有什么条件还没有用上,一时间没有想出好的解决办法。然后在看官方的精选题解时,看到评论区有一位题友【@sdjdd】的评论,于是茅塞顿开。感谢这位题友。
var getKthFromEnd = function (head, k) {
let p = head;
// 先让p指针向前走k-1步
while (k - 1 > 0) {
p = p.next;
k--;
}
// 再让p指针继续往前走,同时head指针开始带着p指针同步往前走,
// 当p到达链尾时,则head已到达目标位置,注意下面while循环的结束条件
while (p.next) {
head = head.next;
p = p.next;
}
return head;
};
解法思路我觉得挺巧妙的,首先是让一个指针p
先往前走k-1
步,然后让p
指针和head
指针同时走,并且保持两个指针的相对距离是不变的k-1
(这里的“距离”可以理解为:走得较晚的head
指针往前跨k-1
步就可以和p
指针指向一致了),然后当走得较早p
指向链尾时,就可以保证head
指针离链尾的距离为k
。
就像
head
指针和p
指针之间用一根长度为k-1
的小棍给连接起来了。head
指针拿着这个棍子推着p
往前走,碰到墙(链尾)时就知道要停下来了。
其实之前我说总感觉有啥条件没用上,这里指的就是指针p
向前移动的结束条件,由这个条件再加上已知的k
,就可以控制head
指针最终指向的节点到链表的距离为k
。
官方题解
更新:2021年7月29日18:43:21
因为我考虑到著作权归属问题,所以【官方题解】部分我不再粘贴具体的代码了,可到下方的链接中查看。
更新:2021年8月2日22:39:11
参考:面试题22. 链表中倒数第 k 个节点(双指针,清晰图解) - 链表中倒数第k个节点 - 力扣(LeetCode)
注意:上面的题解并不是官方题解的,而是官方的【精选】题解
【更新结束】
有关参考
暂无