题目
输入一个链表,输出该链表中倒数第k个结点。为了不引起歧义,我们规定最后一个节点为倒数第1个结点。
解析
预备知识
最简单想法肯定就是链表遍历了,分为2次遍历,第一遍历统计出链表的所有节点数,那么倒数第k个结点位于n - k + 1
位置处(注意此处说的位置从1开始计数),那么我们第二次遍历则从头结点开始,走n - k
步即可到达倒数第k个结点。
由于思路很简单,此处代码省略,感兴趣可以自行尝试,也可与我交流。
思路一
我们知道如果用一个指针,那么必须遍历2次才可得到答案。那么如何利用1次遍历就可以找到目标节点呢?
这里我们采用2个指针,初始化2个指针都指向头结点,之后让第二个指针先走k步,然后同时让2个指针前进,直到第二个指针指向空为止,也就是第二个指针指向了n + 1
的位置,这是我们第一个指针正好指向n - k + 1
的位置,也就是倒数第k个结点位置。
代码的实现要注意:
- 处理头结点为空的时候
- 可能k的值大于链表总结点数
我们这里图例展示求倒数第二个节点的过程:
/**
* 我的思路,用2个指针,一个先走k步,然后再一起走,当第二指针走到n位时,
* 第一个指针正好指向(n - k)位置
* @param head
* @param k
* @return
*/
public static ListNode FindKthToTail1(ListNode head, int k) {
if(head == null || k <= 0) {
return null;
}
ListNode q = head;
ListNode p = head;
for(int i = 1; i <= k; i++) {
if(p != null) {
p = p.next;
}else { // 说明k大于结点总数量了
return null;
}
}
while(p != null) {
q = q.next;
p = p.next;
}
return q;
}
其实上述代码不够简明,看了某大神的简明代码,如下:
/**
* 牛逼的写法
* @param head
* @param k
* @return
*/
public static ListNode FindKthToTail2(ListNode head, int k) {
if(head == null || k <= 0) {
return null;
}
ListNode q = head;
ListNode p = head;
int i = 0;
for(; p != null; i++) {
if(i > k) {
q = q.next;
}
p = p.next;
}
return i < k ? null : q;
}
note:需特别注意当k大于链表本身长度时的情况。
拓展
利用2个指针的变形的链表题目有很多,此处列举几个。
题目1
求链表的中间节点。当节点数量为奇数时,则返回中间节点。当节点数量为偶数时,则返回中间两个节点的第一个节点。
思路
这里虽然看起来跟上面倒数第k个结点类似,但是这里我们无法确定中间位置位于何处。所以要变换一下思路。
我们采取2个步长不一致的指针,第一个指针步长为1,第二个指针步长为2。当第二个指针指向链表末尾时,第一个指针正好指向链表中间位置。下面我具体说下过程:
- 我们保证第一个指针q永远指向奇数位置,第二个指针p用于指向偶数位置(2,4,6…)
- 当头指针为空,直接返回空
- 当链表总数为奇数时,假如是5,如果想结束的时候,第一个指针正好指向3位置,那么第二指针此时应该指向6位置(为空)。所以循环结束条件为 p == null
- 当链表总数为偶数时,假如是6,如果想结束的时候,第一个指针正好指向3位置,那么第二指针此时应该指向6位置(链表最后一个位置,它的下一个节点为空)。所以循环结束条件为 p.next == null
图示如下:
综上,代码就出来:
/**
* 寻找链表的中间节点
* 当节点数量为奇数时,则返回中间节点
* 当节点数量为偶数时,则返回中间两个节点的第一个节点
* @param head
* @return
*/
public static ListNode findMiddle(ListNode head) {
if(head == null) {
return null;
}
ListNode q = head;
ListNode p = head.next;
while(p != null && p.next != null) {
q = q.next;
p = p.next.next;
}
return q;
}
题目2
判断一个单向链表是否有环。
思路
同样此处的思路也是采用步长不一致的指针,第一个指针步长为1,第二个指针步长为2。如果存在环,速度快的指针必然会在某一刻追上慢的指针。如果不存在环, 速度快指针必然率先走到链表末尾。下面我具体说下过程:
- 因为可能给定链表无环,所以循环结束条件可以参照题目1的结束条件,可以有效避免空指针异常。
- 因为可能给定链表有环,所以需要额外添加循环结束条件,即第一个指针与第二个指针相等时,说明第二个指针追上了第一个指针,表明链表有环。
- 因为存在2种情况导致循环结束,所以需要额外的判断语句来判断是否是因为有环结束的。
综上,代码随之而出:
/**
* 判断单链表是否有环
* @param head
* @return
*/
public static boolean isCircle(ListNode head) {
if(head == null) {
return false;
}
ListNode q = head;
ListNode p = head.next;
while(p != null && p.next != null && p != q) {
q = q.next;
p = p.next.next;
}
return p != q ? false : true;
}
其实链表有环问题还有很多需要了解的,参照这篇博客。