面試演算法:連結串列成環的檢測
在有關連結串列的面試演算法題中,檢測連結串列是否有環是常見的題目。給定一個連結串列,要求你判斷連結串列是否存在迴圈,如果有,給出環的長度,要求演算法的時間複雜度是O(N), 空間複雜度是O(1).
如果大家對圖論有了解的話,那麼就會知道深度優先搜尋,在進行深度優先搜尋時,每遍歷一個節點,演算法會給該節點設定一個標誌位,當節點被訪問後,該標誌位設定成true, 因此當搜尋過程中,如果發現當前節點的標誌位已經設定成true的話,那表明圖中含有環。
現在問題是,題目要求空間複雜度是O(1), 也就是我們不能分配多餘的空間作為節點的標誌位,因此,像深度優先搜尋那樣通過標誌位判斷佇列中是否含有環的做法是行不通的。本節我們看看,如何在沒有標誌位的條件下,判斷一個佇列中是否有環。我們以下圖的佇列為例子進行說明:
上面的佇列中,含有一個環,環的長度是6,也就是6個節點形成了一個圓環。
如果佇列中含有圓環,那麼對佇列的遍歷會有什麼影響呢。試想有一個圓形跑道,甲乙兩人同時在跑道的起點,如果甲的速度是乙的兩倍,那麼當乙跑完半圈時,甲跑完一圈回到起點,乙跑完一圈回到起點時,甲跑完兩圈也回到起點,這樣的話,甲乙重新在起點相遇。
採用上面的思路,如果我們使用兩個指標分別從佇列的起點出發,一個指標前進一次遍歷1個節點,另一個指標前進一次,遍歷2個節點,如果佇列中有環,那麼我們可以確信,前進若干次之後,兩個指標會相遇,於是演算法就是讓兩個指標同時前進,如果兩個指標能相遇的話,那麼可以斷定連結串列中有環。還是以上圖為例,假設兩個指標h1, h2同時處於佇列頭節點,h1前進一次經過一個節點,h2前進一次,經過兩個節點,我們用stepCount來統計節點前進的次數,當兩個節點前進3次,也就是stepCount= 3 的時候,兩個指標同時進入到連結串列的環中:
依照順時針次序,指標h2與h1間的距離就是2,我們用d來表示,如果h2要追上h1的話,也就是說,前進指定次數後,h2和h1重合,那麼h2經歷過的節點數,等於h1經過的節點數加上環的長度,再加上當前距離d,如果用circleLength來表示環的長度,那麼以下的公式成立:
2 * stepCount - (1*stepCount + d) = k*circleLength
k是任意整數,當k = 1 時,兩個指標第一次相遇,當k = 2時兩個指標第二次相遇,以此類推。由此,如果我們讓兩個指標不停的前進,那麼兩個指標將不停的相遇。
假設使得兩指標第一次相遇需要前進的次數用t1來表示,於是有:
2*t1 - (t1 + d ) = 1*circleLength (1)
如果兩指標第二次相遇需要前進的次數用t2來表示,於是有:
2*t2 - (t2 + d) = 2*circleLenth (2)
用(2) 減去 (1) 有:
t2 - t1 = circleLength
也就是說,只要我們記錄第一次相遇時前進的次數,還有第2次相遇時前進的總次數,把兩個次數相減,就能得到佇列中,環的長度了。
具體程式碼實現如下:
public class ListUtility {
private Node tail;
Node createList(int nodeNum) {
if (nodeNum <= 0) {
return null;
}
Node head = null;
int val = 0;
Node node = null;
while (nodeNum > 0) {
if (head == null) {
head = new Node();
head.val = val;
node = head;
tail = head;
} else {
node.next = new Node();
node = node.next;
node.val = val;
node.next = null;
tail = node;
}
val++;
nodeNum--;
}
return head;
}
public Node createCycleList(int totalNodeNum, int circleNodeNum) {
if (totalNodeNum < circleNodeNum) {
return null;
}
Node head = createList(totalNodeNum);
Node temp = head;
int stepCount = totalNodeNum - circleNodeNum;
while (stepCount > 0) {
temp = temp.next;
stepCount--;
}
tail.next = temp;
return head;
}
public void printList(Node head) {
while (head != null && head.visited == false) {
System.out.print(head.val + " -> ");
head.visited = true;
head = head.next;
}
System.out.println("null");
}
}
createCycleList用於構造給定環長度的連結串列,例如要構造圖示例子的連結串列只要呼叫createCycleList(10, 6)就可以了。
public class CircleList {
private Node stepOne;
private Node stepTwo;
private int stepCount = 0;
private int visitCount = 0;
int lenOfFirstVisit = 0;
int lenOfSecondVisit = 0;
public int getCircleLength(Node head) {
stepOne = head;
stepTwo = head;
lenOfFirstVisit = 0;
lenOfSecondVisit = 0;
do {
if ( goOneStep() == false || goTwoStep() == false) {
break;
}
stepCount++;
if (stepOne == stepTwo) {
visitCount++;
if (visitCount == 1) {
lenOfFirstVisit = stepCount;
}
if (visitCount == 2) {
lenOfSecondVisit = stepCount;
}
}
} while(visitCount < 2);
return lenOfSecondVisit - lenOfFirstVisit;
}
private boolean goOneStep() {
if (stepOne == null || stepOne.next == null) {
return false;
}
stepOne = stepOne.next;
return true;
}
private boolean goTwoStep() {
if (stepTwo == null || stepTwo.next == null || stepTwo.next.next == null) {
return false;
}
stepTwo = stepTwo.next.next;
return true;
}
}
getCircleLength()用於檢驗給定的連結串列是否含有環,如果有的話,返回連結串列環的長度,也就是環中有幾個節點,如果輸入給定圖示連結串列,那麼返回的值是6.它的演算法跟我們前面描述的一樣,首先使用兩個指標stepOne, stepTwo先指向連結串列的頭節點,stepCount用來記錄前進的次數,stepOne一次前進遍歷一個節點,stepTwo前進一次遍歷兩個節點。在do…while 迴圈中,分別讓兩個指標同時前進,直到兩個指標相遇,或者某個指標指向空元素,也就是null為止,第一次相遇時,使用變數lenOfFirstVisit記錄前進的次數,第二次相遇時,使用變數lenOfSecondVisit來記錄,根據上面的演算法推論,兩次相遇時前進次數的差值,就是佇列中環的長度。
在演算法中,由於我們沒有分配任何新的空間,所以演算法的複雜度為O(1), 在遍歷過程中,只要兩個指標相遇第二次,那麼演算法離開終止,這樣的話,如果佇列中的節點不在圓環中的話,那麼它只會被兩個指標遍歷一次,如果在圓環中,那麼節點最多被兩個指標遍歷兩次,因此演算法的時間複雜度是O(N).
我們看看演算法的執行結果:
public class LinkList {
public static void main(String[] args) {
ListUtility util = new ListUtility();
Node head = util.createCycleList(10, 6);
CircleList cl = new CircleList();
int circleLen = cl.getCircleLength(head);
System.out.println("length of list circle is: " + circleLen);
head = util.createList(10);
circleLen = cl.getCircleLength(head);
System.out.println("length of list circle is: " + circleLen);
}
}
我們先構造一個長度為10,同時含有6個節點的環的連結串列,然後使用演算法計算環的長度,接著再構造一個長度為10, 但是沒有環的連結串列,然後再計算這個連結串列環的長度,最後執行結果如下:
length of list circle is: 6
length of list circle is: 0
也就是說,我們的演算法,對於含有環的連結串列,它能夠準確的計算環的長度,對於沒有環的連結串列,它也能準確的判斷出該連結串列沒有環,於是返回長度為0,這就證明了,我們的演算法理論和實現是正確的,更詳細的講解請參看視訊:
如何進入google,演算法面試技能全面提升指南