如何快速确认链表上是否存在环

这个问题遇到过很多次了,但是并不是说每次都很清楚。所以这次用golang的代码来实现一遍,加深理解与记忆。如果一个链表上不存在环,那么一定能够遍历完链表中所有的Node节点;如果存在环,那么可以想象成存在一个圆形操场。在一个圆里面,如果有两个人,行走的速度不一样,那么一定会有相遇的那一刻。最佳的解法

这个问题遇到过很多次了,但是并不是说每次都很清楚。所以这次用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/

Read more

Volcano 与 Kubernetes GPU 调度学习笔记

本笔记系统整理 Volcano 调度器、Kubernetes 调度框架、GPU Device Plugin、HAMi 等云原生 AI 调度领域的核心知识,适合用于学习、复习和工程实践参考。 目录 * 第一部分:Volcano 入门 * 1. Volcano 是什么 * 2. 安装与快速使用 * 3. 核心特性一览 * 第二部分:Volcano 整体架构 * 4. Volcano 解决的核心问题 * 5. 整体架构与数据流 * 6. 三层抽象模型 * 第三部分:Volcano 核心实现原理 * 7. Session 机制 * 8. Gang Scheduling 实现 * 9. Queue 与 DRF 公平调度

容器镜像(4):镜像的常用工具箱

容器镜像(4):镜像的常用工具箱

前几篇在讲多架构镜像时已经用过 skopeo 和 crane 做镜像复制,这篇系统整理这两个工具的完整能力,同时介绍几个日常操作镜像时同样好用的工具。 一、skopeo:不依赖 Daemon 的镜像瑞士军刀 skopeo 的核心价值是绕过 Docker daemon,直接与 Registry API 交互。上一篇用它做镜像复制和离线传输,但它的能力远不止于此。 1.1 安装 # Ubuntu / Debian sudo apt install -y skopeo skopeo --version # skopeo version 1.15.1 1.2 inspect:免拉取检查镜像元数据 docker inspect 需要先把镜像拉到本地,skopeo inspect 直接向 Registry

容器镜像(3):多架构镜像构建

容器镜像(3):多架构镜像构建

一、什么是多架构镜像 1.1 OCI Image Index 上一篇介绍了单平台镜像的结构:一个 Manifest 指向 Config 和若干 Layer blob。多架构镜像在此之上多了一层——OCI Image Index(也叫 Manifest List),是一个轻量的索引文件,把多个单平台 Manifest 组织在一起: $ docker manifest inspect golang:1.22-alpine { "schemaVersion": 2, "mediaType": "application/vnd.oci.image.index.v1+json", "manifests&

容器镜像(2):containerd 视角下的镜像

容器镜像(2):containerd 视角下的镜像

一、为什么需要了解 containerd 如果你只用 docker run 跑容器,从来不关心底层,那可以不了解 containerd。但如果你在用 Kubernetes,或者想真正理解"容器运行时"是什么,containerd 是绕不开的。 事实上,当你执行 docker run 的时候,containerd 早就在后台悄悄工作了——Docker 从 1.11 版本开始,就把核心运行时剥离出来交给 containerd 负责。 1.1 Docker 的架构演变 早期的 Docker(1.10 及之前)是一个"大一统"的单体程序:一个 dockerd