題目: 輸入一個鏈表,輸出該鏈表中倒數第k哥結點。
爲了符合大多數人的習慣,本題從1開始計數,即鏈表的尾結點是倒數第1個結點。html
例如一個鏈表有6個結點,從頭結點開始它們的值依次是1,2,3,4,5,6.這個鏈表的倒數第3個結點是值爲4的結點java
爲了獲得第K個結點,很天然的想法是先走到鏈表的尾端,再從尾端回溯K步。但是咱們從鏈表結點的定義可疑看出本題中的鏈表 是單向鏈表,單向鏈表的結點只有從前日後的指針而沒有從後往前的指針,所以這種思路行不通。面試
既然不能從尾節點開始遍歷這個鏈表,咱們仍是把思路回到頭結點上來。 假設整個鏈表有N個結點,那麼倒數第K哥結點就是從頭結點開始的第n-k-1個結點。若是咱們只要從頭結點開始日後走n-k+1步就可疑了。如何獲得節點 數n?這個不難,只須要從頭開始遍歷鏈表,沒通過一個結點,計數器加1就好了。url
也就是說咱們須要遍歷鏈表兩次,第一次統計出鏈表中結點的個數,第二次就能找到倒數第k個結點。可是當咱們把這個思路解釋給面試官以後,他會告訴咱們他期待的解法只須要遍歷鏈表一次。spa
爲了實現只遍歷鏈表一次就能找到倒數第k個結點,咱們能夠定義兩個指 針。第一個指針從鏈表的頭指針開始遍歷向前走k-1。第二個指針保持不動;從第k步開始,第二個指針也開化寺從鏈表的頭指針開始遍歷。因爲兩個指針的距離 保持在k-1,當第一個(走在前面的)指針到達鏈表的尾指結點時,第二個指針正好是倒數第k個結點。.net
很多人在面試前從網上看到過這道用兩個指針遍歷的思路來解答這道題, 所以聽到面試官的這道題,他們心中一喜,很快就寫出了代碼。但是幾天後等來的不是Offer,而是拒信,因而百思不得其解。其實緣由很簡單,就是本身的代 碼不夠魯棒。面試官能夠找出3種方法讓這段代碼崩潰。指針
一、輸入Head指針爲Null。因爲代碼會試圖訪問空指針指向的內存,程序會崩潰。code
二、輸入以Head爲頭結點的鏈表的結點總數少於k。因爲在for循環中會在鏈表向前走k-1步,仍然會因爲空指針形成崩潰。htm
三、輸入的參數k爲0.或負數,一樣會形成程序的崩潰。blog
這麼簡單的代碼存在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