题目: 输入一个链表,输出该链表中倒数第k哥结点。
为了符合大多数人的习惯,本题从1开始计数,即链表的尾结点是倒数第1个结点。
例如一个链表有6个结点,从头结点开始它们的值依次是1,2,3,4,5,6.这个链表的倒数第3个结点是值为4的结点
为了得到第K个结点,很自然的想法是先走到链表的尾端,再从尾端回溯K步。可是我们从链表结点的定义可疑看出本题中的链表 是单向链表,单向链表的结点只有从前往后的指针而没有从后往前的指针,因此这种思路行不通。
既然不能从尾节点开始遍历这个链表,我们还是把思路回到头结点上来。 假设整个链表有N个结点,那么倒数第K哥结点就是从头结点开始的第n-k-1个结点。如果我们只要从头结点开始往后走n-k+1步就可疑了。如何得到节点 数n?这个不难,只需要从头开始遍历链表,没经过一个结点,计数器加1就行了。
也就是说我们需要遍历链表两次,第一次统计出链表中结点的个数,第二次就能找到倒数第k个结点。但是当我们把这个思路解释给面试官之后,他会告诉我们他期待的解法只需要遍历链表一次。
为了实现只遍历链表一次就能找到倒数第k个结点,我们可以定义两个指 针。第一个指针从链表的头指针开始遍历向前走k-1。第二个指针保持不动;从第k步开始,第二个指针也开化寺从链表的头指针开始遍历。由于两个指针的距离 保持在k-1,当第一个(走在前面的)指针到达链表的尾指结点时,第二个指针正好是倒数第k个结点。
不少人在面试前从网上看到过这道用两个指针遍历的思路来解答这道题, 因此听到面试官的这道题,他们心中一喜,很快就写出了代码。可是几天后等来的不是Offer,而是拒信,于是百思不得其解。其实原因很简单,就是自己的代 码不够鲁棒。面试官可以找出3种方法让这段代码崩溃。
1、输入Head指针为Null。由于代码会试图访问空指针指向的内存,程序会崩溃。
2、输入以Head为头结点的链表的结点总数少于k。由于在for循环中会在链表向前走k-1步,仍然会由于空指针造成崩溃。
3、输入的参数k为0.或负数,同样会造成程序的崩溃。
这么简单的代码存在3哥潜在崩溃的风险,我们可以想象当面试官看到这样的代码会是什么心情,最终他给出的是拒信而不是Offer。
以下我们给出Java版的代码:
package cglib;
class ListNode
{
int data;
ListNode nextNode;
}
public class DeleteNode {
public static void main(String[] args) {
ListNode head=new ListNode();
ListNode second=new ListNode();
ListNode third=new ListNode();
ListNode forth=new ListNode();
head.nextNode=second;
second.nextNode=third;
third.nextNode=forth;
head.data=1;
second.data=2;
third.data=3;
forth.data=4;
DeleteNode test=new DeleteNode();
//1->2->3->4
ListNode resultListNode=test.findKToTail(head, 3);
ListNode result = test.findKToTail(head, -1);
System.out.println(resultListNode.data);
System.out.println(result.data);
}
public ListNode findKToTail(ListNode head,int k){
if(head==null||k<=0){
return null;
}
ListNode resultNode=null;
ListNode headListNode=head;
for(int i = 0;i<k-1;i++){
System.out.println("i="+i);
if(headListNode.nextNode!=null){
System.out.println("最初headListNode.data="+headListNode.data);
headListNode=headListNode.nextNode;
System.out.println("headListNode.data="+headListNode.data);
}
else{
return null;
}
}
resultNode=head;
while(headListNode.nextNode!=null){//判断最后一个结点指向不为空
System.out.println("末尾不为0,resultNode.data="+resultNode.data);
resultNode=resultNode.nextNode;
System.out.println("resultNode.data="+resultNode.data);
System.out.println("原初headListNode.data="+headListNode.data);
headListNode=headListNode.nextNode;
System.out.println("headListNode.data="+headListNode.data);
}
return resultNode;
}
}
输出:
i=0
最初headListNode.data=1
headListNode.data=2
i=1
最初headListNode.data=2
headListNode.data=3
末尾不为0,resultNode.data=1
resultNode.data=2
原初headListNode.data=3
headListNode.data=4
2
Exception in thread "main" java.lang.NullPointerException
at cglib.DeleteNode.main(DeleteNode.java:26)
拓展1: 求链表的中间结点。如果链表中结点总数为奇数,返回中间结点;如果结点总数为偶数,返回中间两个结点的任意一个。(通过一次遍历解决这个问题)
解题思路:
首先想到的是先求解单链表的长度length,然后遍历length/2的距离即可查找到单链表的中间结点,但是此种方法需要遍历两次链表,即第一次遍历求解单链表的长度,第二次遍历根据索引获取中间结点。
如果是双向链表,可以首尾并行,利用两个指针一个从头到尾遍历,一个从尾到头遍历,当两个指针相遇时,就找到了中间元素。以此思想为基础,如果是单链表,也可以采用双指针的方式来实现中间结点的快速查找。
第一步,有两个指针同时从头开始遍历;第二步,一个快指针一次走两步,一个慢指针一次走一步;第三步,快指针先到链表尾部,而慢指针则恰好到达链表中部 (快指针到链表尾部时,当链表长度为奇数时,慢指针指向的即是链表中间指针,当链表长度为偶数时,慢指针指向的结点和慢指针指向结点的下一个结点都是链表 的中间结点)。
实现代码如下:
public class Solution {
public Node SearchMid(Node head){
Node p = head;
Node q = head;
while (p != null && p.next != null && p.next.next != null) {
p = p.next.next;
q = q.next;
}
return q;
}
}
拓展2:
判断一个单项链表是否形成了环形结构。(提示:速度不同的链表指针遍历,类似于操场跑步,当我们用一个指针遍历链表不能解决问题的时候,可以尝试利用两个指针来遍历链表,可以让其中一个指针遍历的速度快一些,比如一次在链表上走两步,或者让它先在链表上走若干步)
public class LinkListUtli { public static boolean hasCircle(LNode L) { if(L==null) return false;//单链表为空时,单链表没有环 if(L.next==null) return false;//单链表中只有头结点,而且头结点的next为空,单链表没有环 LNode p=L.next;//p表示从头结点开始每次往后走一步的指针 LNode q=L.next.next;//q表示从头结点开始每次往后走两步的指针 while(q!=null) //q不为空执行while循环 { if(p==q) return true;//p与q相等,单链表有环 p=p.next; q=q.next.next; } return false; } }
拓展3:
判断带头结点的单链表是否有环,并找出环的入口结点
链表形状类似数字 6 。
假设甩尾(在环外)长度为 a(结点个数),环内长度为 b 。
则总长度(也是总结点数)为 a+b 。
从头开始,0 base 编号。
将第 i 步访问的结点用 S(i) 表示。i = 0, 1 ...
当 i<a 时,S(i)=i ;
当 i≥a 时,S(i)=a+(i-a)%b 。
分析追赶过程:
两个指针分别前进,假定经过 x 步后,碰撞。则有:S(x)=S(2x)
由环的周期性有:2x=tb+x 。得到 x=tb 。
另,碰撞时,必须在环内,不可能在甩尾段,有 x>=a 。
连接点为从起点走 a 步,即 S(a)。
S(a) = S(tb+a) = S(x+a)。
得到结论:从碰撞点 x 前进 a 步即为连接点。
根据假设易知 S(a-1) 在甩尾段,S(a) 在环上,而 S(x+a) 必然在环上。所以可以发生碰撞。
而,同为前进 a 步,同为连接点,所以必然发生碰撞。
综上,从 x 点和从起点同步前进,第一个碰撞点就是连接点。
/
假设单链表的总长度为L,头结点到环入口的距离为a,环入口到快慢指针相遇的结点距离为x,环的长度为r,慢指针总共走了s步,则快指针走了2s步。另外,快指针要追上慢指针的话快指针至少要在环里面转了一圈多(假设转了n圈加x的距离),得到以下关系:
s = a + x;
2s = a + nr + x;
=>a + x = nr;
=>a = nr - x;
由上式可知:若在头结点和相遇结点分别设一指针,同步(单步)前进,则最后一定相遇在环入口结点,搞掂!
附图:
public static LNode searchEntranceNode(LNode L) { if(L==null) return null;//单链表为空时,单链表没有环 if(L.next==null) return null;//单链表中只有头结点,而且头结点的next为空,单链表没有环 LNode p=L.next;//p表示从头结点开始每次往后走一步的指针 LNode q=L.next.next;//q表示从头结点开始每次往后走两步的指针 while(q!=null) //q不为空执行while循环 { if(p==q) break;//p与q相等,单链表有环 p=p.next; q=q.next.next; } if(q==null) return null; //这里之所以没有像上面一样,先让p,q走一步再进入循环判断,是因为头结点可能就是环的入口结点 q=L; while(q!=null) { if(p==q) return p;//返回环中入口结点 p=p.next; q=q.next; } return null; }
拓展4: 判断带头结点的单链表是否有环,并求环的长度
解题思路:
设一个指针q指向环入口结点,让q往后移动直到q再次等于环的入口结点,此时q所走的总步数就是环的长度
//求单链表环的长度 public static int circleLength(LNode L) { LNode p=searchEntranceNode(L);//找到环的入口结点,前面返回的环入口 if(p==null) return 0;//不存在环时,返回0 LNode q=p.next;//入口的下一个节点 int length=1;//所以长度从1开始 while(p!=q) { length++; q=q.next; } return length;//返回环的长度 } }
拓展5: 判断两个链表是否相交,如果相交找出他们的交点。
仔细研究两个链表,如果他们相交的话,那么他们最后的一个节点一定是相同的,否则是不相交的。因此判断两个链表是否相交就很简单了,分别遍历到两个链表的尾部,然后判断他们是否相同,如果相同,则相交;否则不相交。示意图如下:
判断出两个链表相交后就是判断他们的交点了。假设第一个链表长度为len1,第二个问len2,然后找出长度较长的,让长度较长的链表指针向后移动|len1 - len2| (len1-len2的绝对值),然后在开始遍历两个链表,判断节点是否相同即可。
package cglib;
class ListNode
{
int data;
ListNode nextNode;
}
public class DeleteNode {
public static void main(String[] args) {
ListNode head=new ListNode();
ListNode second=new ListNode();
ListNode third=new ListNode();
ListNode forth=new ListNode();
head.nextNode=second;
second.nextNode=third;
third.nextNode=forth;
head.data=1;
second.data=2;
third.data=3;
forth.data=4;
ListNode head1=new ListNode();
ListNode second1=new ListNode();
ListNode third1=new ListNode();
head1.nextNode=second1;
//second1.nextNode=third1;
second1.nextNode=third;
head1.data=1;
second1.data=2;
//third1.data=3;
DeleteNode test=new DeleteNode();
//1->2->3->4
//1->2->3
ListNode resultListNode=test.findKToTail(head, head1);
System.out.println("交点值="+resultListNode.data);
}
public ListNode findKToTail(ListNode head1, ListNode head2)
{
if(null == head1 || null == head2)
{
return null;//如果有为空的链表,肯定是不相交的
}
ListNode p1;
ListNode p2;
p1 = head1;//第一个链表的 头结点
p2 = head2;//第二个链表的 头结点
int len1 = 0;//第一个链表的 长度
int len2 =0;//第二个链表的 长度
int diff = 0;//两个链表的长度差1
while(null != p1.nextNode)
{
p1 = p1.nextNode;
len1++;
}
while(null != p2.nextNode)
{
p2 = p2.nextNode;
len2++;
}
if(p1 != p2) //如果最后一个节点不相同,返回NULL,表示不相交
{
System.out.println("链表不相交");
return null;
}
diff = Math.abs(len1 - len2);
if(len1 > len2)//1234,123,前面已经判断相交了,现在求交点
{
p1 = head1;//以长链表为基准进行移动
p2 = head2;
}
else
{
p1 = head2;
p2 = head1;
}
for(int i=0; i<diff; i++)
{
p1 = p1.nextNode;//长链表先进行移动diff个距离
}
while(p1 != p2)//长链表先进行移动diff个距离,然后判断跟短链表是否一样,不一样继续两个一起往前
{
p1 = p1.nextNode;
p2 = p2.nextNode;
}
return p1;//这次一样的话肯定就是交点了
}
}
输出:交点值=3