菜单
×
   ❮   
HTML CSS JAVASCRIPT SQL PYTHON JAVA PHP HOW TO W3.CSS C C++ C# BOOTSTRAP REACT MYSQL JQUERY EXCEL XML DJANGO NUMPY PANDAS NODEJS R TYPESCRIPT ANGULAR GIT POSTGRESQL MONGODB ASP AI GO KOTLIN SASS VUE DSA GEN AI SCIPY AWS CYBERSECURITY DATA SCIENCE
     ❯   

DSA Kruskal 算法


Kruskal 算法

Kruskal 算法查找无向图中的最小生成树 (MST),或最小生成森林。


{{ msgDone }}

Kruskal 算法找到的 MST(或 MSTs)是连接所有顶点(或尽可能多)且总边权重最小的边集。

Kruskal 算法按从小到大的边权重顺序添加边到 MST(或最小生成森林)。

会形成环的边不会被添加到 MST 中。这些是上面动画中的红色闪烁线。

Kruskal 算法会检查图中的所有边,但上面的动画会在 MST 或最小生成森林完成时停止,这样您就不必等待最长的边被检查。

最小生成森林 当图中存在多个最小生成树时,就称之为最小生成森林。当图不连通时就会发生这种情况。在上面的动画中,您可以尝试通过使用复选框来验证这一点。

与 Prim 算法不同,Kruskal 算法可用于不连通的图,这意味着它可以找到多个 MST,这就是我们称之为最小生成森林的原因。

为了确定一条边是否会形成环,我们将在 Kruskal 算法中使用 并查集(Union-Find)环检测

工作原理

  1. 将图中的边按从小到大的边权重排序。
  2. 对于每条边,从权重最小的边开始
    1. 这条边会在当前的 MST 中形成环吗?
      • 如果否:将边添加为 MST 边。

手动演练

让我们在下面的图上手动运行 Kruskal 算法,以便在尝试编程之前理解详细的逐步操作。

前三条边被添加到 MST 中。这三条边具有最小的边权重且不形成任何环。

  • C-E,权重 2
  • D-E,权重 3
  • A-B,权重 4

之后,边 C-D(图中红色标出)无法添加,因为它会形成一个环。

{{ edge.weight }} {{el.name}}

Kruskal 算法接下来尝试添加到 MST 的四条边是:

  • E-G,权重 6
  • C-G,权重 7(未添加)
  • D-F,权重 7
  • B-C,权重 8

边 C-G(图中红色标出)无法添加到 MST,因为它会形成一个环。

{{ edge.weight }} {{el.name}}

正如您所见,此时 MST 已经创建完成,但 Kruskal 算法将继续运行,直到所有边都被测试是否可以添加到 MST 中。

Kruskal 算法尝试添加到 MST 的最后三条边是边权重最高的边:

  • A-C,权重 9(未添加)
  • A-G,权重 10(未添加)
  • F-G,权重 11(未添加)

这些边中的每一条都会在 MST 中形成一个环,所以它们无法添加。

{{ edge.weight }} {{el.name}}

Kruskal 算法现在已完成。

运行下面的模拟,查看 Kruskal 算法如何执行我们刚刚完成的手动步骤。

{{ edge.weight }} {{el.name}}
{{ msgDone }}

注意: 尽管 Kruskal 算法会检查图中的所有边,但此页面顶部的动画会在添加完最后一条 MST 或最小生成森林的边后立即停止,这样我们就不必查看所有无法添加的红色边。

这是可能的,因为对于连通图,只有一个 MST,并且当 MST 中的边数比图中的顶点数少 1 时(\(V-1\)),搜索就可以停止。对于不连通的图,我们的动画中有两个 MST,当 MST 的总边数达到 \(V-2\) 时,算法会停止。


Kruskal 算法的实现

为了让 Kruskal 算法找到最小生成树 (MST) 或最小生成森林,我们创建一个 Graph 类。稍后我们将使用这个 Graph 类中的方法来从上面的示例创建图,并在其上运行 Kruskal 算法。

class Graph:
    def __init__(self, size):
        self.size = size
        self.edges = []  # For storing edges as (weight, u, v)
        self.vertex_data = [''] * size  # Store vertex names

    def add_edge(self, u, v, weight):
        if 0 <= u < self.size and 0 <= v < self.size:
            self.edges.append((weight, u, v))  # Add edge with weight
            
    def add_vertex_data(self, vertex, data):
        if 0 <= vertex < self.size:
            self.vertex_data[vertex] = data

第 8 行和第 12 行: 检查输入参数 uvvertex 是否在可能的索引值范围内。

为了在 Kruskal 算法中进行并查集环检测,这两个方法 findunion 也在 Graph 类中定义。

    def find(self, parent, i):
        if parent[i] == i:
            return i
        return self.find(parent, parent[i])

    def union(self, parent, rank, x, y):
        xroot = self.find(parent, x)
        yroot = self.find(parent, y)
        if rank[xroot] < rank[yroot]:
            parent[xroot] = yroot
        elif rank[xroot] > rank[yroot]:
            parent[yroot] = xroot
        else:
            parent[yroot] = xroot
            rank[xroot] += 1

第 15-18 行: find 方法使用 parent 数组递归查找顶点的根。对于每个顶点,parent 数组保存一个指向该顶点父节点的指针(索引)。当 find 方法找到 parent 数组中指向自身的一个顶点时,就找到了根顶点。请继续阅读,了解 find 方法和 parent 数组如何在 kruskals_algorithm 方法中使用。

第 20-29 行: 当一条边被添加到 MST 时,union 方法使用 parent 数组来合并(union)两个树。 rank 数组保存了每个根顶点的树高度的粗略估计。合并两棵树时,秩较低的根会成为另一棵树的根节点的子节点。

以下是 Kruskal 算法作为 Graph 类中的方法实现的方式:

    def kruskals_algorithm(self):
        result = []  # MST
        i = 0 # edge counter

        self.edges = sorted(self.edges, key=lambda item: item[2])

        parent, rank = [], []

        for node in range(self.size):
            parent.append(node)
            rank.append(0)

        while i < len(self.edges):
            u, v, weight = self.edges[i]
            i += 1

            x = self.find(parent, u)
            y = self.find(parent, v)
            if x != y:
                result.append((u, v, weight))
                self.union(parent, rank, x, y)

        print("Edge \tWeight")
        for u, v, weight in result:
            print(f"{self.vertex_data[u]}-{self.vertex_data[v]} \t{weight}")

第 35 行: Kruskal 算法在尝试将边添加到 MST 之前,必须先对边进行排序。

第 40-41 行: 初始化 parentrank 数组。开始时,每个顶点都是自己的根(parent 数组中的每个元素都指向自身),并且每个顶点的初始高度为 0(rank 数组中的值为 0)。

第 44-45 行: 选择最小的边,并将 i 加 1,以便在下一次迭代中选择正确的边。

第 47-51 行: 如果当前边的两个端点 uv 具有不同的根 xy,则表示新边不会形成环,并且将合并这两个树。为了合并树,当前边被添加到 result 数组中,然后运行 union 方法以确保树被正确合并,从而使合并后的结果树只有一个根顶点。

现在,让我们创建“手动运行”上面显示的图,并在其上运行 Kruskal 算法。

示例

Python

class Graph:
    def __init__(self, size):
        self.size = size
        self.edges = []  # For storing edges as (weight, u, v)
        self.vertex_data = [''] * size  # Store vertex names

    def add_edge(self, u, v, weight):
        if 0 <= u < self.size and 0 <= v < self.size:
            self.edges.append((u, v, weight))  # Add edge with weight
            
    def add_vertex_data(self, vertex, data):
        if 0 <= vertex < self.size:
            self.vertex_data[vertex] = data

    def find(self, parent, i):
        if parent[i] == i:
            return i
        return self.find(parent, parent[i])

    def union(self, parent, rank, x, y):
        xroot = self.find(parent, x)
        yroot = self.find(parent, y)
        if rank[xroot] < rank[yroot]:
            parent[xroot] = yroot
        elif rank[xroot] > rank[yroot]:
            parent[yroot] = xroot
        else:
            parent[yroot] = xroot
            rank[xroot] += 1

    def kruskals_algorithm(self):
        result = []  # MST
        i = 0  # edge counter

        self.edges = sorted(self.edges, key=lambda item: item[2])

        parent, rank = [], []

        for node in range(self.size):
            parent.append(node)
            rank.append(0)

        while i < len(self.edges):
            u, v, weight = self.edges[i]
            i += 1
            
            x = self.find(parent, u)
            y = self.find(parent, v)
            if x != y:
                result.append((u, v, weight))
                self.union(parent, rank, x, y)

        print("Edge \tWeight")
        for u, v, weight in result:
            print(f"{self.vertex_data[u]}-{self.vertex_data[v]} \t{weight}")

g = Graph(7)
g.add_vertex_data(0, 'A')
g.add_vertex_data(1, 'B')
g.add_vertex_data(2, 'C')
g.add_vertex_data(3, 'D')
g.add_vertex_data(4, 'E')
g.add_vertex_data(5, 'F')
g.add_vertex_data(6, 'G')

g.add_edge(0, 1, 4)  #A-B,  4
g.add_edge(0, 6, 10) #A-G, 10
g.add_edge(0, 2, 9)  #A-C,  9
g.add_edge(1, 2, 8)  #B-C,  8
g.add_edge(2, 3, 5)  #C-D,  5
g.add_edge(2, 4, 2)  #C-E,  2
g.add_edge(2, 6, 7)  #C-G,  7
g.add_edge(3, 4, 3)  #D-E,  3
g.add_edge(3, 5, 7)  #D-F,  7
g.add_edge(4, 6, 6)  #E-G,  6
g.add_edge(5, 6, 11) #F-G, 11

print("Kruskal's Algorithm MST:")
g.kruskals_algorithm()
运行示例 »

Kruskal 算法的时间复杂度

有关时间复杂度的一般性解释,请访问此页面

令 \(E\) 为图中边的数量,Kruskal 算法的时间复杂度为:

\[ O( E \cdot log{E} ) \]

我们得到这个时间复杂度是因为在 Kruskal 开始添加边到 MST 之前,必须先对边进行排序。使用像 快速排序归并排序 这样的快速排序算法,仅此排序本身的时间复杂度为 \( O( E \cdot log{E} ) \)。

在对边进行排序之后,它们会逐一被检查,以确定是否会形成环,如果不会,则将其添加到 MST 中。

尽管使用 find 方法检查环的创建以及使用 union 方法将边包含到 MST 中看起来工作量很大,但这些仍然可以视为一个操作。之所以可以将它们视为一个操作,是因为它们花费的时间大约是常数时间。这意味着此操作所需的时间随着图的增长而增长非常缓慢,因此实际上不会影响整体时间复杂度。

由于 Kruskal 算法的时间复杂度仅随边的数量 \(E\) 而变化,因此它对于稀疏图(其中边数 \(E\) 与顶点数 \(V\) 的比率相对较低)尤其快速。



×

联系销售

如果您想将 W3Schools 服务用于教育机构、团队或企业,请发送电子邮件给我们
sales@w3schools.com

报告错误

如果您想报告错误,或想提出建议,请发送电子邮件给我们
help@w3schools.com

W3Schools 经过优化,旨在方便学习和培训。示例可能经过简化,以提高阅读和学习体验。教程、参考资料和示例会不断审查,以避免错误,但我们无法保证所有内容的完全正确性。使用 W3Schools 即表示您已阅读并接受我们的使用条款Cookie 和隐私政策

版权所有 1999-2024 Refsnes Data。保留所有权利。W3Schools 由 W3.CSS 提供支持