如何快速確認鏈表上是否存在環

這個問題遇到過很多次了,但是並不是說每次都很清楚。所以這次用golang的代碼來實現一遍,加深理解與記憶。

如果一個鏈表上不存在環,那麼一定能夠遍歷完鏈表中所有的Node節點;如果存在環,那麼可以想象成存在一個圓形操場。在一個圓裏面,如果有兩個人,行走的速度不一樣,那麼一定會有相遇的那一刻。最佳的解法便是基於此想法,當然也有其他的解法,比如說將每個遍歷的節點放進一個set裏面,如果存在過,那麼就存在環。

數據結構:

type Node struct {
	index int
	next *Node
}

構建鏈表,隨機選擇是否生成環

func createNodeList() *Node{
	rand.Seed(time.Now().Unix())
	length := rand.Intn(100) + 1
	
	head := new(Node)
	var tail *Node
	
	// 構建鏈表
	for idx ,curNode := 1, head; idx <= length; idx ++ {
		curNode.index = idx
		if idx != length {
			curNode.next = new(Node)
			curNode = curNode.next
		} else {
			tail = curNode
		}
	}
	
	buildRing := rand.Intn(2)
	if buildRing == 0 {
		// 構建環
		enterRingIndex := rand.Intn(length) + 1
		enterRingNode := head
		for idx:= 2; idx <= enterRingIndex; idx ++{
			enterRingNode = enterRingNode.next
		}
		tail.next = enterRingNode
		fmt.Println("length : ", length, ", enter ring index : ", enterRingIndex, ", Ring length : ", length - enterRingIndex + 1)
	} else {
		fmt.Println("don't build ring")
	}
	
	return head
}

輸出鏈表,用於debug

func outputNodeList(head * Node) {
	for head != nil {
		fmt.Print(head.index, " ")
		head = head.next
	}
}

使用兩個下標,用於抽象表示圓形操場中的兩個人,leader走前面,每次走兩步,follower走後面,每次走一步,如果相遇,返回相遇時的Node節點。

func nodeListExistRing(head * Node) *Node {
	leader, follower := head, head
	for leader != nil && follower != nil {
		leader = leader.next
		if leader == nil {
			return nil
		}
		leader = leader.next
		
		follower = follower.next
		
		if (leader == follower) {
			return leader
		}
	}
	return nil
}

如果鏈表中存在環,那麼環的長度就等於:follower再走一圈時走過的步長。

func calRingLength(meetNode * Node) int {
	curNode := meetNode
	curNode = curNode.next
	count := 1
	for ; curNode != meetNode; count ++ {
		curNode = curNode.next;
	}
	return count
}

如果鏈表存在環,那麼入環點等於將leader指向head後,並以每次一步的速度往前,直到與follower再次相遇,此時的節點便是入環點。

如何證明上面的步驟是正確的?

也就是說,入環時走過的路程,等於S2加上(S1+S2)的整數倍,如果leader步長變成1,並且從A點出發到達B點,那麼走過的路程D,一定會等於S2+(n-1)(S1 + S2),也就是說一定會在B點相遇。B點即入環點。

func calEnterRingIndex(meetNode * Node, head * Node) int {
	var enterRingIndex = 0;
	for head != meetNode {
		head = head.next
		meetNode = meetNode.next
		enterRingIndex ++
	}
	return enterRingIndex + 1
}

啓動類,將前面過程,按照想要的邏輯拼裝在一起。

func main() {
	head := createNodeList()
	meetNode := nodeListExistRing(head)
	if meetNode == nil {
		outputNodeList(head)
	} else {
		fmt.Println("RING EXIST!", ", Ring length: ", calRingLength(meetNode), ", Enter Ring Index : ", calEnterRingIndex(meetNode, head))
	}
}

附錄:

文中公式、圖及推算過程的latex源碼:

\begin{tikzpicture}
    % \draw[dashed, file=blue!20]{0, 0} circle(0.5cm);
    \draw(0, 0) circle (1.5cm);
    \coordinate[label = above:$O$] (O) at (0, 0);
    \node at(0, 0)[circle,fill,inner sep=1pt]{};

    \coordinate[label = right:$C$] (C) at (-30:1.5);
    \node at(C)[circle,fill,inner sep=1pt]{};

    \coordinate[label = below right:$S_1$] (S1) at (-60:1.5);
    \coordinate[label = left:$S_2$] (S2) at (120:1.5);

    \coordinate[label = below: $B$] (B) at (-90:1.5);
    \node at(B)[circle,fill,inner sep=1pt]{};

    \coordinate[label = below: $A$] (A) at (-7, -1.5);
    \node at(A)[circle,fill,inner sep=1pt]{};

    \draw (A) -- (B) node[above, midway] (line) {$D$};
\end{tikzpicture}

設n爲leader與follower在相遇前,比follower多走的圈數,那麼便存在
\[ 2(D + S_1) = D + S_1 + n * (S_1 + S_2) \]

化簡得:
\[ 2*D + 2*S_1 = D + S_1 + n * (S_1 + S_2) \]
\[ D + S_1 = n * (S_1 + S_2) \]
\[ D = n * (S_1 + S_2) - S_1 \]
\[ D = (n - 1) * (S_1 + S_2) + S_1 + S_2 - S_1 \]

即:
\[ D = (n - 1) * (S_1 + S_2) + S_2 \]

參考:https://zouyu4524.github.io/blog/tikz-plot/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章