什么是并查集?谜底就在谜面上,并查集并的是多个集合,查的是元素是否在集合中,并查集主要是解决连通性问题,主要是用于维护连通关系和查询连通关系,但是由于并查集可以根据不同的使用场景进行灵活使用所以没有固定的模板,不能像堆栈等数据结构一样直接固化在编程语言中,那么什么是连通性呢?
连通关系:具有传递性,比如a==b,b==c,可以推导出a==c,此时a、b、c具有连通性它们属于同一个集合。
实际案例:朋友圈具有传递性,a跟b是朋友,b跟c是朋友,那么可以推导出a跟c也是朋友,a、b、c同属于一个朋友圈。
根据并查集的主要功能主要对应两个方法find和merge,根据优化程度可分为QuickFind、QuickUnion、WeightQuickUnion、WeightQuickUnionWithPathCompress。
QuickFind:
优点:查找速度比较快
缺点:合并速度比较慢
class UnionSet {
constructor(n) {
this.colors = [];
this.n = n;
//初始化每个点的颜色,每一个点都用下标表示不同的颜色
for (let i = 0; i < n; i++) {
this.colors.push(i);
}
}
//时间复杂度O(1)
find(x) {
return this.colors[x];
}
//时间复杂度O(n)
merge(a, b) {
const ca = this.colors[a];
if (this.colors[b] === ca) return;
for (let i = 0; i < this.n; i++) {
if (this.colors[i] === ca) {
this.colors[i] = this.colors[b];
}
}
}
}
const unionSet = new UnionSet(10);
unionSet.merge(0, 1);
unionSet.merge(1, 3);
unionSet.merge(2, 3);
//0,1,2,3属于同一个集合它们都拥有相同的颜色3
console.log(
unionSet.find(0),
unionSet.find(1),
unionSet.find(2),
unionSet.find(3)
);
QuickUnion:
特点:使用树形结构来表示节点之间的连通关系
缺点:当树形结构退化为链表时查询和合并效率比较低
class UnionSet {
constructor(n) {
this.n = n;
this.root = [];
//初始化的时候节点的根节点就是它自己
for (let i = 0; i < n; i++) {
this.root[i] = i;
}
}
//时间复杂度跟树高有关
find(x) {
if (this.root[x] === x) return x;
return this.find(this.root[x]);
}
//时间复杂度跟树高有关
merge(a, b) {
//如果两个点的对应集合的根节点如果相同那么这两个点已经处于同一个集合,此时没必要再进行合并操作
if (this.find(a) === this.find(b)) return;
this.root[this.find(a)] = b;
}
}
WeightQuickUnion:
背景:在QuickUnion的基础上进行的优化,为了比较是根据树节点数量还是根据树高来进行merge效率更高,我们需要一个评价指标:节点的平均查找次数。
平均查找次数=所有节点的查找次数/节点数量
我们假设有a、b两棵树,a的节点数量为sa,b的节点数量为sb,a的树高为la,b的树高为lb,由此a、b两棵树分情况如下:
a作为b的root时平均查找次数为:la+lb+sb/sa+sb
b作为a的root时平均查找次数为:la+lb+sa/sa+sb
由此得出结论:当sb小的时候以a作为b的root平均查找次数更小,效率更优,即:a、b两棵树谁的节点数量更多谁作为root
优点:按秩优化后查找和合并效率都比较高
class UnionSet {
constructor(n) {
this.n = n;
this.root = [];
this.size = [];
for (let i = 0; i < n; i++) {
this.root[i] = i;
this.size[i] = 1;
}
}
//时间复杂度O(lgN)
find(x) {
if (this.root[x] === x) return x;
return this.find(this.root[x]);
}
//时间复杂度O(lgN)
merge(a, b) {
const ra = this.find(a),
rb = this.find(b);
if (ra === rb) return;
if (this.size[ra] < this.size[rb]) {
this.root[ra] = rb;
this.size[ra] += this.size[rb];
} else {
this.root[rb] = ra;
this.size[rb] += this.size[ra];
}
}
}
WeightQuickUnionWithPathCompress: 特点:增加了路径压缩
class UnionSet {
constructor(n) {
this.n = n;
this.root = [];
this.size = [];
for (let i = 0; i < n; i++) {
this.root[i] = i;
this.size[i] = 1;
}
}
//时间复杂度接近于O(1)
find(x) {
return (this.root[x] = this.root[x] === x ? x : this.find(this.root[x]));
}
//时间复杂度接近于O(1)
merge(a, b) {
const ra = this.find(a),
rb = this.find(b);
if (ra === rb) return;
if (this.size[ra] < this.size[rb]) {
this.root[ra] = rb;
this.size[ra] += this.size[rb];
} else {
this.root[rb] = ra;
this.size[rb] += this.size[ra];
}
}
}
工程版:
特点:效率很高的同时代码最简洁
class UnionSet {
constructor(n) {
this.n = n;
this.root = [];
for (let i = 0; i < n; i++) {
this.root[i] = i;
}
}
//时间复杂度接近于O(1)
find(x) {
return (this.root[x] = this.root[x] === x ? x : this.find(this.root[x]));
}
//时间复杂度接近于O(1)
merge(a, b) {
this.father[this.find(a)] = this.find(b);
}
应用场景:
leetcode:547题
解题思路:相互连接的城市构成一个集合即省份,集合的数量即为省份的数量
/**
* @param {number[][]} isConnected
* @return {number}
*/
class UnionSet {
constructor(n) {
this.n = n;
this.father = [];
for (let i = 0; i < n; i++) {
this.father[i] = i;
}
}
find(x) {
return (this.father[x] =
this.father[x] == x ? x : this.find(this.father[x]));
}
merge(a, b) {
this.father[this.find(a)] = this.find(b);
}
}
var findCircleNum = function (isConnected) {
const length = isConnected.length;
const unionSet = new UnionSet(length);
for (let i = 0; i < length; i++) {
for (let j = 0; j < length; j++) {
if (isConnected[i][j] === 1) {
unionSet.merge(i, j);
}
}
}
let ans = 0;
for (let i = 0; i < length; i++) {
if (unionSet.find(i) === i) {
ans++;
}
}
return ans;
};
执行结果:
leetcode:990题
解题思路:将26个英文字母转化为下标index,先根据==关系确定并查集,然后再扫描方程发现在并查集中有不等关系则直接返回false
/**
* @param {string[]} equations
* @return {boolean}
*/
class UnionSet{
constructor(n){
this.n = n;
this.father = [];
for(let i=0; i<n; i++){
this.father[i] = i;
}
}
find(x){
return (this.father[x] = this.father[x] == x? x : this.find(this.father[x]));
}
merge(a,b){
this.father[this.find(a)] = this.find(b);
}
}
var equationsPossible = function(equations) {
const unionSet = new UnionSet(26);
for(let i=0; i<equations.length; i++){
if(equations[i][1] === '!') continue;
let a = equations[i][0].charCodeAt(0) - 'a'.charCodeAt(0);
let b = equations[i][3].charCodeAt(0) - 'a'.charCodeAt(0);
unionSet.merge(a,b);
}
for(let i=0; i<equations.length; i++){
if(equations[i][1] === '=') continue;
let a = equations[i][0].charCodeAt(0) - 'a'.charCodeAt(0);
let b = equations[i][3].charCodeAt(0) - 'a'.charCodeAt(0);
if(unionSet.find(a) === unionSet.find(b)) return false;
}
return true;
};
执行结果:
leetcode:200题
解题思路:扫描grid将上、左都为1的岛屿视为连通关系,根据index建立连通关系,最后扫描判断岛屿数量
/**
* @param {character[][]} grid
* @return {number}
*/
class UnionSet{
constructor(n){
this.n = n;
this.father = [];
for(let i=0; i<n; i++)
this.father[i] = i;
}
find(x){
return (this.father[x] = this.father[x] === x? x:this.find(this.father[x]));
}
merge(a,b){
this.father[this.find(a)] = this.find(b);
}
}
var numIslands = function(grid) {
const m = grid.length;
const n = grid[0].length;
const getIndex = (i,j)=>{
return i * n+j;
}
const unionSet = new UnionSet(m * n);
for(let i=0; i<m; i++){
for(let j=0; j<n; j++){
if(grid[i][j] === '0') continue;
if(i>0&&grid[i-1][j] === '1'){
unionSet.merge(getIndex(i-1,j),getIndex(i,j))
}
if(j>0&&grid[i][j-1] === '1'){
unionSet.merge(getIndex(i,j-1),getIndex(i,j))
}
}
}
let ans = 0;
for(let i=0; i<m; i++){
for(let j=0; j<n; j++){
if(grid[i][j] === '1'&& unionSet.find(getIndex(i,j)) === getIndex(i,j)) ans++;
}
}
return ans;
};
执行结果:
leetcode:684题
解题思路:先根据输入条件扫描生成并查集,扫描过程中如果发现并查集中的同一个集合包含这两个元素则视为冗余连接
/**
* @param {number[][]} edges
* @return {number[]}
*/
class UnionSet {
constructor(n){
this.n = n;
this.father = [];
for(let i=0; i<n; i++){
this.father[i] = i;
}
}
find(x){
return (this.father[x] = this.father[x] == x ? x: this.find(this.father[x]));
}
merge(a,b){
this.father[this.find(a)] = this.find(b);
}
}
var findRedundantConnection = function(edges) {
let unionSet = new UnionSet(edges.length);
for(let i=0; i<edges.length; i++){
if(unionSet.find(edges[i][0]) === unionSet.find(edges[i][1])) return edges[i];
else unionSet.merge(edges[i][0],edges[i][1]);
}
};
执行结果:
leetcode:1319题
解题思路:并查集集合数量-1即为操作次数
/**
* @param {number} n
* @param {number[][]} connections
* @return {number}
*/
class UnionSet{
constructor(n){
this.n = n;
this.father = [];
for(let i=0; i<n; i++){
this.father[i] = i;
}
}
find(x){
return (this.father[x] = this.father[x] == x ? x : this.find(this.father[x]));
}
merge(a,b){
this.father[this.find(a)] = this.find(b);
}
}
var makeConnected = function(n, connections) {
if(connections.length<n-1) return -1;
const unionSet = new UnionSet(n);
for(let i=0; i<connections.length; i++){
unionSet.merge(connections[i][0],connections[i][1]);
}
let ans = 0;
for(let i=0; i<n; i++){
if(unionSet.find(i) === i) ans++;
}
return ans -1 ;
};
执行结果:
leetcode:947题
解题思路:
/**
* @param {number[][]} stones
* @return {number}
*/
class UnionSet{
constructor(n){
this.father = [];
for(let i=0; i<n; i++){
this.father[i] = i;
}
}
find(x){
return (this.father[x] = this.father[x] === x ? x : this.find(this.father[x]));
}
merge(a,b){
this.father[this.find(a)] = this.find(b);
}
}
var removeStones = function(stones) {
const unionSet = new UnionSet(stones.length);
const index_x = new Map();
const index_y = new Map();
for(let i=0; i<stones.length; i++){
const x = stones[i][0];
const y = stones[i][1];
if(index_x.has(x)){
unionSet.merge(i,index_x.get(x));
}
if(index_y.has(y)){
unionSet.merge(i,index_y.get(y));
}
index_x.set(x,i);
index_y.set(y,i);
}
let ans = 0;
for(let i=0; i<stones.length; i++){
if(unionSet.find(i) === i) ans++;
}
return stones.length - ans;
};
执行结果:
leetcode:1202题
解题思路:小根堆+并查集
/**
* @param {string} s
* @param {number[][]} pairs
* @return {string}
*/
class UnionSet {
constructor(n) {
this.father = [];
for (let i = 0; i < n; i++) {
this.father[i] = i;
}
}
find(x) {
return (this.father[x] =
this.father[x] === x ? x : this.find(this.father[x]));
}
merge(a, b) {
this.father[this.find(a)] = this.find(b);
}
}
class PriorityQueue {
constructor() {
this.data = [];
}
add(x) {
this.data.push(x);
this.up(this.data.length - 1);
}
up(index) {
while (index > 0) {
const parentIndex = (index - 1) >> 1;
if (this.data[parentIndex] >= this.data[index]) {
this.swap(parentIndex, index);
index = parentIndex;
} else break;
}
}
swap(i, j) {
[this.data[i], this.data[j]] = [this.data[j], this.data[i]];
}
down(index) {
let cnt = (this.data.length - 2) >> 1,
small,
smallIndex;
while (index <= cnt) {
const l = index * 2 + 1,
r = l + 1;
if (this.data[r] === undefined || this.data[l] < this.data[r]) {
small = this.data[l];
smallIndex = l;
} else {
small = this.data[r];
smallIndex = r;
}
if (this.data[index] >= small) {
this.swap(index, smallIndex);
index = smallIndex;
} else break;
}
}
shift() {
let res = this.data[0];
this.data[0] = this.data.pop();
this.down(0);
return res;
}
}
var smallestStringWithSwaps = function (s, pairs) {
const unionSet = new UnionSet(s.length);
const priorityQueueList = [];
for (let i = 0; i < s.length; i++) {
priorityQueueList.push(new PriorityQueue());
}
for (let i = 0; i < pairs.length; i++) {
const a = pairs[i][0];
const b = pairs[i][1];
unionSet.merge(a, b);
}
for (let i = 0; i < s.length; i++) {
priorityQueueList[unionSet.find(i)].add(s[i]);
}
let res = "";
for (let i = 0; i < s.length; i++) {
res += priorityQueueList[unionSet.find(i)].shift();
}
return res;
};
执行结果:
欢迎关注我公众号:"波仔学前端"持续更新前端技术