写给前端的并查集思想

94 阅读3分钟

一、啥是并查集?

它是一种方法,在某种规则的限制下,把N个单独的有相同性质节点划分到一个集合里。

二、用来解决什么问题?

能用 分拨方式 解决的问题都可以用这个思想解决。

分拨顾名思义,谁和谁一伙,谁和谁好了,这个圈里有多少人,那个圈里有多少人的。为了干成一件事,每个圈里都要贡献一份力啥的。

三、讲解思想

图1.png

上面这张图里一共有10个节点,每个节点的连通性如图所示。现在我们对连通性描述如下: 0-1是直接相连0-3是间接相连2-3也是间接相连0-4不相连

我们现在使用二维数组(arr)来表示上图里的节点与连通性,arr[i][j] 的取值有2个,分别是0、1。现在对取值描述如下:

1:i节点与j节点直接相连。
0:i节点与j节点非直接相连。

根据上图我们知道二维数组(arr)的值是下面这样:

let arr = [
  // 0  1  2  3  4  5  6  7  8  9        colIndex
    [1, 1, 1, 0, 0, 0, 0, 0, 0, 0], // 0 rowIndex
    [1, 1, 0, 1, 0, 0, 0, 0, 0, 0], // 1
    [1, 0, 1, 0, 0, 0, 0, 0, 0, 0], // 2
    [0, 1, 0, 1, 0, 0, 0, 0, 0, 0], // 3
    [0, 0, 0, 0, 1, 0, 0, 0, 1, 0], // 4
    [0, 0, 0, 0, 0, 1, 1, 1, 0, 0], // 5
    [0, 0, 0, 0, 0, 1, 1, 0, 0, 0], // 6
    [0, 0, 0, 0, 0, 1, 0, 1, 0, 0], // 7
    [0, 0, 0, 0, 1, 0, 0, 0, 1, 0], // 8
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], // 9 
]

目前为止,我们可以通过二维数组来表示上图的节点与联通性,那么问题来了,根据这个二维数组,我们如何得知有几个集合呢?

起初,我们10个节点各自为战,所以此时总共有10个集合。

我们声明一个root数组,用于存储这10个节点与它的连通性。

let arr = [...];
let root = [];
for (let index = 0; index < arr.length; index++){
    root.push(index);
}

root数组代表的含义:索引代表我们的节点,索引对应的值代表该节点的最祖先元素

所以此时,再经过一系列的操作处理之后,我们的root数组的数据应该是这样的:

   // root索引:0  1  2  3  4  5  6  7  8  9
    let root = [0, 0, 0, 0, 4, 5, 5, 5, 4, 9];

正好也与我们的图里的节点对应上了:

图1.png

好了,现在我们来看看 一系列操作 是怎么操作数据。

  • 首先,遍历二维数组arr,如果arr[i][j] === 1,那么说明i与j是相连的,那么此时root[y]的值就应该更改为i的祖先元素。这个过程我们叫做“并操作(union函数)”。

  • 其次,我们如何找 i 的祖先元素(这个过程叫做“查操作(find函数)”)?。

我们以节点3为例,来演示下在节点3上进行的操作。
1、首先arr[1][3] === 1,意味着节点3与节点1是相连的,所以roor[3] = find(1)。


2、find函数是用来干啥的?上面已经说的很清楚了,root里存储的值都是祖先元素,那么find函数就是查找相应元素的祖先元素。


3、find(1) 就是查找节点1的祖先元素,在arr没有被遍历结束之前,我们还不知道节点1是否有祖先元素,以及祖先元素是否被更改。


4、在第3步我们可知,在遍历过程中,节点的祖先元素是可以被更改的(或者说图与图之间是可以相连的),所以在我们进行并操作的时候,需要遍历root数组,要把那些被牵连的节点的对应值都更改掉。因为并操作的对象可以是根与根之间。

3.1、并操作

// 并操作
union(x, y){ // 将x、y节点联通
    let rootX = find(x);
    let rootY = find(y);
    if (rootX !== rootY){
        for (let index = 0; index < root.length; index++){
            if (root[index] === rootY){
                root[index] = rootX;
            }
        }
    }
}

3.2、查操作

let arr = [...];
let root = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
function find(suoyin){
    return root[suoyin];
}

3.3、完整打通

let arr = [...];
let root = new Array(arr.length).map( (item, index) => index );

for (let rowIndex = 0; rowIndex < arr.length; rowIndex++){
    for (let colIndex = 0; colIndex < arr.length; colIndex++){
        if (arr[rowIndex][colIndex] === 1){ // 进行并操作
            if (rowIndex <= colIndex){      // 我们并操作的原则是大节点向小节点靠拢,如果不这么做的话会造成循环重复。
                union(rowIndex, colIndex);
            } else {
                union(colIndex, rowIndex);
            }
        }
    }
}

console.log('root:', root);

此时root里的元素就都是被分拨了,数组里有多少个不重复的元素就有多少个集合。

四、最后

好啦,基本的并查集思想到这里就结束啦,如果在上述过程中出现了什么错误,欢迎指正,那么我们下回再见啦。