数据结构之----并查集

307 阅读3分钟

朋友圈问题

有一个集合{0,1,2,3,4,5,6,7,8,9},共有9个元素,分别代表了1个人的编号,他们中有些人是朋友,下面的数组表达了这些人的朋友关系

var friends = [
    [0, 7],
    [1, 6],
    [4, 8],
    [8, 2],
    [9, 0],
    [3, 5],
    [1, 2]
];

约定朋友的朋友也是朋友,那么请问,在这个集合中,有几个朋友圈?同时请你提供一个is_friend(i, j)方法,可以用来判断i和j是不是朋友关系。

2. 并查集

在一些应用问题中,你需要将n个不同的元素划分成多个不相交的集合,就像上面的朋友圈问题。
并查集是一种非常简单但是非常有效的集合,它支持下面3种操作:

  • union(root1, root2) 把集合root2合并入集合root1中,要求是root1和root2互不相交
  • find(x) 搜索x所在的集合,返回该集合的名字
  • 初始化函数, 将s个元素初始化为s个只有一个元素的子集合

如果集合中有n个元素,可以用一个0~n-1个整数来表示这些元素,这个整数就是集合名,并查集的典型实现是采用数组,并用树形结构来表示元素及其所属子集的关系,回想一下堆,堆就是用数组来表达树结构,并查集是相同的道理,数组元素的索引就是这些元素的编号。

3. 实现

3.1 类定义

function UFSets(){
    var parent = [];
}

3.2 初始化

    this.init = function(size){
        parent = new Array(size);
        for(var i =0;i<size;i++){
            parent[i] = -1;
        }
    };

进行初始化的时候,数组里的每个元素都初始化为-1,这里有3个概念非常重要

  • 每个元素都是一个单独的集合,与其他集合互不相交
  • 对于刚刚初始化结束的并查集,每个元素是一个单独的集合,它的索引就是这个集合的集合名
  • 每个元素的值,就是其父节点所在的索引,由于刚刚初始化,每个元素的值都是-1,-1这个索引在数组中是不存在的,这恰好表明每个元素都没有父节点

3.3 find

find方法是搜索x所在的集合,并返回这个集合的名字

    this.find = function(item){
        while(parent[item] >=0){
            item = parent[item];
        }
        return item;
    };

对于一个刚刚初始化的并查集,你随便搜索一个数,比如3,那么方法返回的结果就是3,而在3.2 中已经明确指出,这个3就是它的集合名。

3.4 union(root1, root2)

合并两个不相交的集合,将root2合并到root1中,root1和root2是两个集合的集合名

    this.union = function(root1, root2){
        parent[root1] += parent[root2];
        parent[root2] = root1;
    };

必须强调一点,root1和root2是不相交的,这一点union方法自身没有做判断,需要你在应用的时候自己去判断。
仅仅是这简单的三个方法,一个并查集就写好了。

4.解决朋友圈问题

4.1 初始化并查集

我们集合实际问题,来理解并查集是如何工作的,下面的数组表达了朋友之间的关系

var friends = [
    [0, 7],
    [1, 6],
    [4, 8],
    [8, 2],
    [9, 0],
    [3, 5],
    [1, 2]
];

编号从0到9,那么我们初始化一个大小为10的数组,来表示这些元素集合

数组中有三个元素的值小于0,对应的森林结构有3棵树,一共有3个朋友圈。

4. 示例代码

function UFSets(){
    var parent = [];

    this.init = function(size){
        parent = new Array(size);
        for(var i =0;i<size;i++){
            parent[i] = -1;
        }
    };

    this.find = function(item){
        // 返回item所在集合的集合名
        while(parent[item] >=0){
            item = parent[item];
        }
        return item;
    };

    this.union = function(root1, root2){
        parent[root1] += parent[root2];
        parent[root2] = root1;

    };

    this.build_relation = function(i, j){
        //建立朋友关系
        var root1 = this.find(i);
        var root2 = this.find(j);
        // 不在同一个集合中,就合并到一起
        if(root1 != root2){
            this.union(root1, root2);
        }
    };

    this.is_friend = function(i, j){
        var root1 = this.find(i);
        var root2 = this.find(j);
        return root1 == root2;
    };

    this.get_friend_group_count = function(){
        var count = 0;
        for(var i= 0;i<parent.length;i++){
            if(parent[i]<0){
                count++;
            }
        }
        return count;
    };
};


var friends = [
    [0, 7],
    [1, 6],
    [4, 8],
    [8, 2],
    [9, 0],
    [3, 5],
    [1, 2]
];

var ufset = new UFSets();
ufset.init(10);

for(var i =0;i<friends.length; i++){
    var item = friends[i];
    ufset.build_relation(item[0], item[1]);
}

console.log("朋友圈个数为 "+ufset.get_friend_group_count());
console.log(ufset.is_friend(2, 6));
console.log(ufset.is_friend(6, 8));
console.log(ufset.is_friend(4, 8));
console.log(ufset.is_friend(9, 7));
console.log(ufset.is_friend(2, 4));
console.log(ufset.is_friend(2, 7));