📜  不相交集数据结构的链表表示(1)

📅  最后修改于: 2023-12-03 15:21:31.935000             🧑  作者: Mango

不相交集数据结构的链表表示

不相交集数据结构(Disjoint-set data structure)也被称为并查集(Union-Find)数据结构,是一种用于维护集合的数据结构,支持以下两种操作:

  • Find:查找某个元素所在的集合代表元素。
  • Union:将两个不相交的集合合并成一个集合。

通常的实现方式有两种,一种是使用数组,另一种是使用链表。在本篇文章中,我们将介绍如何使用链表实现不相交集数据结构。

算法实现

为了实现不相交集数据结构,我们需要用到一个链表节点结构体,其中包含以下成员:

  • parent:指向当前节点的父节点。
  • size:表示该节点所在集合的元素个数(只有集合代表元素节点的 size 才是准确的)。
struct Node {
    Node* parent;
    int size;
};

我们还需要定义一些操作函数,如下所示:

Find

给定一个元素节点 node,我们要找到其所在集合的代表元素。实现起来很简单,只需要向上遍历 node 的父节点,直到到达某个节点的父节点为 nullptr,这个节点就是集合的代表元素。

Node* Find(Node* node) {
    if (node->parent == nullptr) {
        return node;
    }
    Node* root = Find(node->parent);
    node->parent = root;  // 路径压缩优化
    return root;
}

注意到我们在 Find 过程中顺便做了路径压缩优化,将 node 到根节点路径上的所有节点的 parent 都指向根节点。

Union

给定两个元素节点 node1node2,我们需要将它们所在的两个集合合并成一个集合。具体地,我们需要找到它们所在集合的代表元素 root1root2,然后将其中一个代表元素的 parent 指向另一个代表元素。

按照启发式合并(合并时将树大小小的子树的根节点指向树大小大的子树的根节点)的做法,我们可以将集合元素个数少的集合并到元素个数多的集合上,这样可以减少树的高度,提高查询效率。

void Union(Node* node1, Node* node2) {
    Node* root1 = Find(node1);
    Node* root2 = Find(node2);

    // 如果已经在同一个集合中,则直接返回
    if (root1 == root2) {
        return;
    }

    // 启发式合并:将元素个数少的集合并到元素个数多的集合上
    if (root1->size < root2->size) {
        root1->parent = root2;
        root2->size += root1->size;
    } else {
        root2->parent = root1;
        root1->size += root2->size;
    }
}
MakeSet

给定一个元素节点 node,我们需要将其作为新集合的代表元素,创建一个新集合。

void MakeSet(Node* node) {
    node->parent = nullptr;
    node->size = 1;
}
示例

下面的代码演示了如何使用链表实现不相交集数据结构,以及如何使用该数据结构解决一道典型问题:求解连通块个数。

#include <iostream>
#include <vector>

struct Node {
    Node* parent;
    int size;
};

Node* Find(Node* node) {
    if (node->parent == nullptr) {
        return node;
    }
    Node* root = Find(node->parent);
    node->parent = root;  // 路径压缩优化
    return root;
}

void Union(Node* node1, Node* node2) {
    Node* root1 = Find(node1);
    Node* root2 = Find(node2);

    // 如果已经在同一个集合中,则直接返回
    if (root1 == root2) {
        return;
    }

    // 启发式合并:将元素个数少的集合并到元素个数多的集合上
    if (root1->size < root2->size) {
        root1->parent = root2;
        root2->size += root1->size;
    } else {
        root2->parent = root1;
        root1->size += root2->size;
    }
}

void MakeSet(Node* node) {
    node->parent = nullptr;
    node->size = 1;
}

int CountConnectedComponents(const std::vector<Node*>& nodes) {
    // 初始化不相交集
    for (auto node : nodes) {
        MakeSet(node);
    }
    // 对所有边进行 Union 操作
    for (int i = 0; i < nodes.size(); ++i) {
        Node* node1 = nodes[i];
        for (int j = i + 1; j < nodes.size(); ++j) {
            Node* node2 = nodes[j];
            if (/* Node i and Node j are connected */) {
                Union(node1, node2);
            }
        }
    }
    // 统计连通块个数
    int count = 0;
    for (Node* node : nodes) {
        if (node->parent == nullptr) {
            count++;
        }
    }
    return count;
}

int main() {
    // 构建一个无向图,共 5 个节点和 4 条边
    std::vector<Node*> nodes(5);
    for (int i = 0; i < nodes.size(); ++i) {
        nodes[i] = new Node;
    }
    Union(nodes[0], nodes[1]);
    Union(nodes[1], nodes[2]);
    Union(nodes[3], nodes[4]);
    std::cout << CountConnectedComponents(nodes) << std::endl;  // 输出 2
    return 0;
}
总结

本篇文章介绍了使用链表实现不相交集数据结构的算法,以及如何使用该数据结构解决一道典型问题:求解连通块个数。这种实现方式虽然效率不如数组实现,但是可以方便地支持动态添加节点。