文章目录
剑指offer 22 链表中倒数第K个节点
输入一个链表,输出该链表中倒数第k
个节点。为了符合大多数人的习惯,本题从1
开始计数,即链表的尾节点是倒数第1
个节点。
例如,一个链表有 6
个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6
。这个链表的倒数第 3
个节点是值为 4
的节点。
示例:
给定一个链表: 1->2->3->4->5, 和 k = 2. k = 2. k=2.
返回链表 4->5.
解题思路:
假设链表共有m
个节点,则从头节点走到尾节点需要m-1
步(如五个手指间有四个空隙),如果走到尾结点的后一个null
节点,则是走m
步。倒数第k
个,说明前面还有m-k
个,那倒数第k
个就是顺数的第m-k+1
个,需要走m-k
步才能到它。用快慢指针,让快指针先走k
步,随后让快慢指针一起走,当快指针走到尾部的后一节点null
的时候,慢指针刚好走了m-k
步,也就是刚好到倒数第k
个节点的位置。
Java代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
if(head == null || k < 0) return head;
ListNode fast = head;
ListNode slow = head;
for(int i = 0;i < k;i++){//快指针先走k步
fast = fast.next;
}
//现在快慢指针一起走,快指针指向null时,慢指针刚好指向倒数第k个节点
while(fast != null){
fast = fast.next;
slow = slow.next;
}
return slow;
}
}
也可以先计算出链表总共有多少节点,然后从头节点走m - k步即可,不过相对快慢指针遍历了两遍链表
Go代码
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func kthToLast(head *ListNode, k int) int {
// 假设共m个节点,则倒数第k个,前面还有m - k个节点,则倒数第k个是顺数的第m - k + 1个节点
// 从头节点走m - k 步可以走到它
if head == nil || k <= 0 {
return -1
}
cur := head
m := 0
for cur != nil {
m++
cur = cur.Next
}
cur = head
for i := 0;i < m - k;i++ {
cur = cur.Next
}
return cur.Val
}
扩展题一:LeetCode 19 删除链表的倒数第N个节点
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
进阶:你能尝试使用一趟扫描实现吗?
示例 1:
输入: head = [1,2,3,4,5], n = 2
输出: [1,2,3,5]
示例 2:
输入: head = [1], n = 1
输出: []
示例 3:
输入: head = [1,2], n = 1
输出: [1]
提示:
- 链表中结点的数目为
sz
1 <= sz <= 30
0 <= Node.val <= 100
1 <= n <= sz
解题思路:
首先需要明确,对于单链表的删除或者新增操作,我们需要找到待处理节点的前一个节点才好操作。
由于有可能要删除的是倒数最后一个结点,也即第一个结点,故我们可以建立一个虚拟头结点dummy
,然后设定快慢指针,fast
和slow
,让fast
先走n
步,slow
不动,然后fast
和slow
同时走,直到fast
走到尽头(注意这里是最后一个不为空的节点,而不是null,和上面那题有点区别,因为是要找到要删除的节点的前一个节点,所以少走一步
)。
假设加上虚拟头结点共有m
个结点(则从虚拟头结点走到尾节点需要m-1
步,如5
根手指,中间只有4
个间隙),倒数第n
个,说明前面还有m-n
个,那倒数第k
个就是顺数的第m-n+1
个,需要走m-n
步才能到它,但是删除节点,我们要找的是待删除节点的前一节点,所以应该是走m-1-n
步。而fast
走n
步后,还剩下m-1-n
步走到尾结点,此时fast
和slow
再都走m-1-n
步,fast
到达尽头尾节点,而slow
走了m-1-n
步,刚好是待删除节点的前一个节点,所以这个位置刚好是我们所要删除结点的前一位置。如图所示:
现在两个指针同时走,fast
走到尾结点走了m-1-n
步,故慢指针也是走了m-1-n
步,还差n
步才到尾结点,因此慢指针刚好指着我们需要的倒数第n
个结点的前一结点
,因为还差n
步才到尾节点,说明当前节点到尾节点的节点个数总和是n+1
,也即当前节点是倒数第n+1
个节点,也即倒数第n
个节点的前一节点。
注意:上述走法即使没有虚拟头节点,也是同样的走法,因为我们是按总节点个数m中夹着m-1步操作的,即有没有虚拟头结点,不影响走法,也是快指针也走n步,然后快慢指针同时走,只是走到尾节点的时候,快慢两个指针同时都比有虚拟头节点的少走一步而已,这里加了虚拟头节点,是因为可能要找的是真实头节点的前一节点。
Java代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
//由于可能要删除的是头结点,故建立虚拟头结点,用于统一操作
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode fast = dummy,slow = dummy;
//快指针先走n步
for(int i = 0;i < n;i++){
fast = fast.next;
}
//现在快慢指针一起走
while(fast.next != null){
slow = slow.next;
fast = fast.next;
}
slow.next = slow.next.next;
return dummy.next;
}
}
Go代码
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func removeNthFromEnd(head *ListNode, n int) *ListNode {
// 假设共有m个节点,倒数第n个,说明前面还有m-n个,则它是顺数的第m - n + 1个
// 要删除,需要找他的前一个节点,是顺数第m - n 个
// 从第一个节点走到尾部需要m - 1步,走m - n - 1步可以到待删除节点的前一节点
// 所以 快指针先走n步,剩下m - n - 1步快慢指针一起走
// 这样的话,快指针到尾节点的时候,慢指针能刚好到待删除节点的前一节点
if head == nil || n <= 0 {
return head
}
// 要删除的可能是头节点,故建立一个虚拟头节点
dummy := &ListNode{}
dummy.Next = head
slow,fast := dummy,dummy
// 因为要用到fast.Next,所以还是判断一下fast!=nil比较好,能避免空指针
// 不写fast != nil的判断也能过,是因为题目保证了输入数据的合理性,但是工作中取值时,尽量先进行非空判断
for i:=0;i < n && fast != nil;i++ {
fast = fast.Next
}
for fast != nil && fast.Next != nil {
fast = fast.Next
slow = slow.Next
}
slow.Next = slow.Next.Next
return dummy.Next
}
扩展题二:LeetCode 876 求链表的中间节点
给定一个头结点为 head
的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
示例 1:
输入:[1,2,3,4,5]
输出: 此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.
示例 2:
输入: [1,2,3,4,5,6]
输出: 此列表中的结点 4 (序列化形式:[4,5,6])
由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。
提示:
给定链表的结点数介于 1
和 100
之间。
解题思路:
同样是用快慢指针,这里由于是求中间节点,所以可以让快指针每次走两步,慢指针每次走一步
,当链表长度是奇数
时,快指针走到尾结点时,慢指针刚好指着中间节点。当链表长度是偶数
时,快指针指着链表尾结点下一节点的null
时,慢指针指着中间节点的第二个节点。
Java代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode middleNode(ListNode head) {
if(head == null || head.next == null) return head;
ListNode fast = head;
ListNode slow = head;
//由于要用到fast.next.next,所以一定要判断一下fast.next!=null,否则出现空指针异常
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
}
Go代码
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func middleNode(head *ListNode) *ListNode {
// 快慢指针,快的一次走两步,慢的一次走一步
// m个节点,中间有m-1步,所以m是奇数时,快指针走到尾节点时,慢指针刚好在中间节点
// m是偶数时,快指针是走到尾节点后面的nil时,慢节点在中间节点,可以对照图模拟一下
if head == nil {
return head
}
slow,fast := head,head
for fast != nil {
// 奇数情况
if fast.Next == nil {
break
}
slow = slow.Next
fast = fast.Next.Next
}
return slow
}
扩展题三:2095. 删除链表的中间节点
给你一个链表的头节点 head
。删除 链表的 中间节点
,并返回修改后的链表的头节点 head
。
长度为 n
链表的中间节点是从头数起第⌊n / 2⌋
个节点(下标从0
开始),其中 ⌊x⌋
表示小于或等于x
的最大整数。
对于 n = 1、2、3、4
和 5
的情况,中间节点的下标分别是 0、1、1、2
和 2
。
示例 1:
输入:head = [1,3,4,7,1,2,6]
输出:[1,3,4,1,2,6]
解释:
上图表示给出的链表。节点的下标分别标注在每个节点的下方。
由于 n = 7 ,值为 7 的节点 3 是中间节点,用红色标注。
返回结果为移除节点后的新链表。
示例 2:
输入:head = [1,2,3,4]
输出:[1,2,4]
解释:
上图表示给出的链表。
对于 n = 4 ,值为 3 的节点 2 是中间节点,用红色标注。
示例 3:
输入:head = [2,1]
输出:[2]
解释:
上图表示给出的链表。
对于 n = 2 ,值为 1 的节点 1 是中间节点,用红色标注。
值为 2 的节点 0 是移除节点 1 后剩下的唯一一个节点。
提示:
- 链表中节点的数目在范围 [1, 10^5] 内
- 1 <= Node.val <= 10^5
解题思路:
相对上题找中间节点,多了一个要求,就是删除。要删除节点,所以需要一个指针记录前一个节点。然后要删除的可能是头节点,所以用一个虚拟头结点的套路。
Go代码
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func deleteMiddle(head *ListNode) *ListNode {
// 快慢指针,快的一次走两步,慢的一次走一步
// m个节点,中间有m-1步,所以m是奇数时,快指针走到尾节点时,慢指针刚好在中间节点
// m是偶数时,快指针是走到尾节点后面的nil时,慢节点在中间节点,可以对照图模拟一下
// 要删除节点,所以需要一个指针记录前一个节点
if head == nil {
return head
}
dummy := &ListNode{}
dummy.Next = head
pre := dummy
slow,fast := head,head
for fast != nil {
// 奇数情况
if fast.Next == nil {
break
}
pre = pre.Next
slow = slow.Next
fast = fast.Next.Next
}
pre.Next = pre.Next.Next
return dummy.Next
}
举一反三
当我们用一个指针遍历链表不能解决问题的时候,可以尝试用两个指针来遍历链表。可以让其中一个指针遍历的速度快一些,比如一次在链表中走两步,或者让它先在链表上走若干步。
双指针、快慢指针两个思想很重要,数组和链表问题经常用到。