Add disjoint set.

This commit is contained in:
Oleksii Trekhleb 2018-05-03 16:49:46 +03:00
parent de5b771b4b
commit 2e76caa9d6
6 changed files with 463 additions and 0 deletions

View File

@ -20,6 +20,7 @@
* Segment Tree or Interval Tree
* Binary Indexed Tree or Fenwick Tree
9. [Graph](https://github.com/trekhleb/javascript-algorithms/tree/master/src/data-structures/graph) (both directed and undirected)
9. [Disjoint Set](https://github.com/trekhleb/javascript-algorithms/tree/master/src/data-structures/disjoint-set)
## Algorithms

View File

@ -0,0 +1,93 @@
import DisjointSetItem from './DisjointSetItem';
export default class DisjointSet {
/**
* @param {function(value: *)} [keyCallback]
*/
constructor(keyCallback) {
this.keyCallback = keyCallback;
this.items = {};
}
/**
* @param {*} itemValue
* @return {DisjointSet}
*/
makeSet(itemValue) {
const disjointSetItem = new DisjointSetItem(itemValue, this.keyCallback);
if (!this.items[disjointSetItem.getKey()]) {
// Add new item only in case if it not presented yet.
this.items[disjointSetItem.getKey()] = disjointSetItem;
}
return this;
}
/**
* @param {*} itemValue
* @return {(string|null)}
*/
find(itemValue) {
const templateDisjointItem = new DisjointSetItem(itemValue, this.keyCallback);
// Try to find item itself;
const requiredDisjointItem = this.items[templateDisjointItem.getKey()];
if (!requiredDisjointItem) {
return null;
}
return requiredDisjointItem.getRoot().getKey();
}
/**
* @param {*} valueA
* @param {*} valueB
* @return {DisjointSet}
*/
union(valueA, valueB) {
const rootKeyA = this.find(valueA);
const rootKeyB = this.find(valueB);
if (rootKeyA === null || rootKeyB === null) {
throw new Error('One or two values are not in sets');
}
if (rootKeyA === rootKeyB) {
// In case if both elements are already in the same set then just return its key.
return this;
}
const rootA = this.items[rootKeyA];
const rootB = this.items[rootKeyB];
if (rootA.getAncestorsCount() < rootB.getAncestorsCount()) {
// If rootB's tree is bigger then make rootB to be a new root.
rootB.addChild(rootA);
return rootB.getKey();
}
// If rootA's tree is bigger then make rootA to be a new root.
rootA.addChild(rootB);
return this;
}
/**
* @param {*} valueA
* @param {*} valueB
* @return {boolean}
*/
inSameSet(valueA, valueB) {
const rootKeyA = this.find(valueA);
const rootKeyB = this.find(valueB);
if (rootKeyA === null || rootKeyB === null) {
throw new Error('One or two values are not in sets');
}
return rootKeyA === rootKeyB;
}
}

View File

@ -0,0 +1,94 @@
export default class DisjointSetItem {
/**
* @param {*} value
* @param {function(value: *)} [keyCallback]
*/
constructor(value, keyCallback) {
this.value = value;
this.keyCallback = keyCallback;
/** @var {DisjointSetItem} this.parent */
this.parent = null;
this.children = {};
}
/**
* @return {*}
*/
getKey() {
// Allow user to define custom key generator.
if (this.keyCallback) {
return this.keyCallback(this.value);
}
// Otherwise use value as a key by default.
return this.value;
}
/**
* @return {DisjointSetItem}
*/
getRoot() {
return this.isRoot() ? this : this.parent.getRoot();
}
/**
* @return {boolean}
*/
isRoot() {
return this.parent === null;
}
/**
* @return {number}
*/
getAncestorsCount() {
if (this.getChildren().length === 0) {
return 0;
}
let count = 0;
/** @var {DisjointSetItem} child */
this.getChildren().forEach((child) => {
// Count child itself.
count += 1;
// Also add all children of current child.
count += child.getAncestorsCount();
});
return count;
}
/**
* @return {DisjointSetItem[]}
*/
getChildren() {
return Object.values(this.children);
}
/**
* @param {DisjointSetItem} parentItem
* @param {boolean} forceSettingParentChild
* @return {DisjointSetItem}
*/
setParent(parentItem, forceSettingParentChild = true) {
this.parent = parentItem;
if (forceSettingParentChild) {
parentItem.addChild(this);
}
return this;
}
/**
* @param {DisjointSetItem} childItem
* @return {DisjointSetItem}
*/
addChild(childItem) {
this.children[childItem.getKey()] = childItem;
childItem.setParent(this, false);
return this;
}
}

View File

@ -0,0 +1,20 @@
# Disjoint Set
**Disjoint-set** data structure (also called a unionfind data structure or mergefind set) is a data
structure that tracks a set of elements partitioned into a number of disjoint (non-overlapping) subsets.
It provides near-constant-time operations (bounded by the inverse Ackermann function) to *add new sets*,
to *merge existing sets*, and to *determine whether elements are in the same set*.
In addition to many other uses (see the Applications section), disjoint-sets play a key role in Kruskal's algorithm for finding the minimum spanning tree of a graph.
![disjoint set](https://upload.wikimedia.org/wikipedia/commons/6/67/Dsu_disjoint_sets_init.svg)
*MakeSet* creates 8 singletons.
![disjoint set](https://upload.wikimedia.org/wikipedia/commons/a/ac/Dsu_disjoint_sets_final.svg)
After some operations of *Union*, some sets are grouped together.
## References
- [Wikipedia](https://en.wikipedia.org/wiki/Disjoint-set_data_structure)
- [By Abdul Bari on YouTube](https://www.youtube.com/watch?v=wU6udHRIkcc)

View File

@ -0,0 +1,140 @@
import DisjointSet from '../DisjointSet';
describe('DisjointSet', () => {
it('should throw error when trying to union and check not existing sets', () => {
function mergeNotExistingSets() {
const disjointSet = new DisjointSet();
disjointSet.union('A', 'B');
}
function checkNotExistingSets() {
const disjointSet = new DisjointSet();
disjointSet.inSameSet('A', 'B');
}
expect(mergeNotExistingSets).toThrow();
expect(checkNotExistingSets).toThrow();
});
it('should do basic manipulations on disjoint set', () => {
const disjointSet = new DisjointSet();
expect(disjointSet.find('A')).toBeNull();
expect(disjointSet.find('B')).toBeNull();
disjointSet.makeSet('A');
expect(disjointSet.find('A')).toBe('A');
expect(disjointSet.find('B')).toBeNull();
disjointSet.makeSet('B');
expect(disjointSet.find('A')).toBe('A');
expect(disjointSet.find('B')).toBe('B');
disjointSet.makeSet('C');
expect(disjointSet.inSameSet('A', 'B')).toBeFalsy();
disjointSet.union('A', 'B');
expect(disjointSet.find('A')).toBe('A');
expect(disjointSet.find('B')).toBe('A');
expect(disjointSet.inSameSet('A', 'B')).toBeTruthy();
expect(disjointSet.inSameSet('B', 'A')).toBeTruthy();
expect(disjointSet.inSameSet('A', 'C')).toBeFalsy();
disjointSet.union('A', 'A');
disjointSet.union('B', 'C');
expect(disjointSet.find('A')).toBe('A');
expect(disjointSet.find('B')).toBe('A');
expect(disjointSet.find('C')).toBe('A');
expect(disjointSet.inSameSet('A', 'B')).toBeTruthy();
expect(disjointSet.inSameSet('B', 'C')).toBeTruthy();
expect(disjointSet.inSameSet('A', 'C')).toBeTruthy();
disjointSet
.makeSet('E')
.makeSet('F')
.makeSet('G')
.makeSet('H')
.makeSet('I');
disjointSet
.union('E', 'F')
.union('F', 'G')
.union('G', 'H')
.union('H', 'I');
expect(disjointSet.inSameSet('A', 'I')).toBeFalsy();
expect(disjointSet.inSameSet('E', 'I')).toBeTruthy();
disjointSet.union('I', 'C');
expect(disjointSet.find('I')).toBe('E');
expect(disjointSet.inSameSet('A', 'I')).toBeTruthy();
});
it('should union smaller set with bigger one making bigger one to be new root', () => {
const disjointSet = new DisjointSet();
disjointSet
.makeSet('A')
.makeSet('B')
.makeSet('C')
.union('B', 'C')
.union('A', 'C');
expect(disjointSet.find('A')).toBe('B');
});
it('should do basic manipulations on disjoint set with custom key extractor', () => {
const keyExtractor = value => value.key;
const disjointSet = new DisjointSet(keyExtractor);
const itemA = { key: 'A', value: 1 };
const itemB = { key: 'B', value: 2 };
const itemC = { key: 'C', value: 3 };
expect(disjointSet.find(itemA)).toBeNull();
expect(disjointSet.find(itemB)).toBeNull();
disjointSet.makeSet(itemA);
expect(disjointSet.find(itemA)).toBe('A');
expect(disjointSet.find(itemB)).toBeNull();
disjointSet.makeSet(itemB);
expect(disjointSet.find(itemA)).toBe('A');
expect(disjointSet.find(itemB)).toBe('B');
disjointSet.makeSet(itemC);
expect(disjointSet.inSameSet(itemA, itemB)).toBeFalsy();
disjointSet.union(itemA, itemB);
expect(disjointSet.find(itemA)).toBe('A');
expect(disjointSet.find(itemB)).toBe('A');
expect(disjointSet.inSameSet(itemA, itemB)).toBeTruthy();
expect(disjointSet.inSameSet(itemB, itemA)).toBeTruthy();
expect(disjointSet.inSameSet(itemA, itemC)).toBeFalsy();
disjointSet.union(itemA, itemC);
expect(disjointSet.find(itemA)).toBe('A');
expect(disjointSet.find(itemB)).toBe('A');
expect(disjointSet.find(itemC)).toBe('A');
expect(disjointSet.inSameSet(itemA, itemB)).toBeTruthy();
expect(disjointSet.inSameSet(itemB, itemC)).toBeTruthy();
expect(disjointSet.inSameSet(itemA, itemC)).toBeTruthy();
});
});

View File

@ -0,0 +1,115 @@
import DisjointSetItem from '../DisjointSetItem';
describe('DisjointSetItem', () => {
it('should do basic manipulation with disjoint set item', () => {
const itemA = new DisjointSetItem('A');
const itemB = new DisjointSetItem('B');
const itemC = new DisjointSetItem('C');
const itemD = new DisjointSetItem('D');
expect(itemA.getAncestorsCount()).toBe(0);
expect(itemA.getChildren()).toEqual([]);
expect(itemA.getKey()).toBe('A');
expect(itemA.getRoot()).toEqual(itemA);
expect(itemA.isRoot()).toBeTruthy();
expect(itemB.isRoot()).toBeTruthy();
itemA.addChild(itemB);
itemD.setParent(itemC);
expect(itemA.getAncestorsCount()).toBe(1);
expect(itemC.getAncestorsCount()).toBe(1);
expect(itemB.getAncestorsCount()).toBe(0);
expect(itemD.getAncestorsCount()).toBe(0);
expect(itemA.getChildren().length).toBe(1);
expect(itemC.getChildren().length).toBe(1);
expect(itemA.getChildren()[0]).toEqual(itemB);
expect(itemC.getChildren()[0]).toEqual(itemD);
expect(itemB.getChildren().length).toBe(0);
expect(itemD.getChildren().length).toBe(0);
expect(itemA.getRoot()).toEqual(itemA);
expect(itemB.getRoot()).toEqual(itemA);
expect(itemC.getRoot()).toEqual(itemC);
expect(itemD.getRoot()).toEqual(itemC);
expect(itemA.isRoot()).toBeTruthy();
expect(itemB.isRoot()).toBeFalsy();
expect(itemC.isRoot()).toBeTruthy();
expect(itemD.isRoot()).toBeFalsy();
itemA.addChild(itemC);
expect(itemA.isRoot()).toBeTruthy();
expect(itemB.isRoot()).toBeFalsy();
expect(itemC.isRoot()).toBeFalsy();
expect(itemD.isRoot()).toBeFalsy();
expect(itemA.getAncestorsCount()).toEqual(3);
expect(itemB.getAncestorsCount()).toEqual(0);
expect(itemC.getAncestorsCount()).toEqual(1);
});
it('should do basic manipulation with disjoint set item with custom key extractor', () => {
const keyExtractor = (value) => {
return value.key;
};
const itemA = new DisjointSetItem({ key: 'A', value: 1 }, keyExtractor);
const itemB = new DisjointSetItem({ key: 'B', value: 2 }, keyExtractor);
const itemC = new DisjointSetItem({ key: 'C', value: 3 }, keyExtractor);
const itemD = new DisjointSetItem({ key: 'D', value: 4 }, keyExtractor);
expect(itemA.getAncestorsCount()).toBe(0);
expect(itemA.getChildren()).toEqual([]);
expect(itemA.getKey()).toBe('A');
expect(itemA.getRoot()).toEqual(itemA);
expect(itemA.isRoot()).toBeTruthy();
expect(itemB.isRoot()).toBeTruthy();
itemA.addChild(itemB);
itemD.setParent(itemC);
expect(itemA.getAncestorsCount()).toBe(1);
expect(itemC.getAncestorsCount()).toBe(1);
expect(itemB.getAncestorsCount()).toBe(0);
expect(itemD.getAncestorsCount()).toBe(0);
expect(itemA.getChildren().length).toBe(1);
expect(itemC.getChildren().length).toBe(1);
expect(itemA.getChildren()[0]).toEqual(itemB);
expect(itemC.getChildren()[0]).toEqual(itemD);
expect(itemB.getChildren().length).toBe(0);
expect(itemD.getChildren().length).toBe(0);
expect(itemA.getRoot()).toEqual(itemA);
expect(itemB.getRoot()).toEqual(itemA);
expect(itemC.getRoot()).toEqual(itemC);
expect(itemD.getRoot()).toEqual(itemC);
expect(itemA.isRoot()).toBeTruthy();
expect(itemB.isRoot()).toBeFalsy();
expect(itemC.isRoot()).toBeTruthy();
expect(itemD.isRoot()).toBeFalsy();
itemA.addChild(itemC);
expect(itemA.isRoot()).toBeTruthy();
expect(itemB.isRoot()).toBeFalsy();
expect(itemC.isRoot()).toBeFalsy();
expect(itemD.isRoot()).toBeFalsy();
expect(itemA.getAncestorsCount()).toEqual(3);
expect(itemB.getAncestorsCount()).toEqual(0);
expect(itemC.getAncestorsCount()).toEqual(1);
});
});