From 2e76caa9d66b521c0caaf8af5bc14f1ac5279bc2 Mon Sep 17 00:00:00 2001 From: Oleksii Trekhleb Date: Thu, 3 May 2018 16:49:46 +0300 Subject: [PATCH] Add disjoint set. --- README.md | 1 + .../disjoint-set/DisjointSet.js | 93 ++++++++++++ .../disjoint-set/DisjointSetItem.js | 94 ++++++++++++ src/data-structures/disjoint-set/README.md | 20 +++ .../disjoint-set/__test__/DisjointSet.test.js | 140 ++++++++++++++++++ .../__test__/DisjointSetItem.test.js | 115 ++++++++++++++ 6 files changed, 463 insertions(+) create mode 100644 src/data-structures/disjoint-set/DisjointSet.js create mode 100644 src/data-structures/disjoint-set/DisjointSetItem.js create mode 100644 src/data-structures/disjoint-set/README.md create mode 100644 src/data-structures/disjoint-set/__test__/DisjointSet.test.js create mode 100644 src/data-structures/disjoint-set/__test__/DisjointSetItem.test.js diff --git a/README.md b/README.md index 3ecdefc5..faa95c89 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/data-structures/disjoint-set/DisjointSet.js b/src/data-structures/disjoint-set/DisjointSet.js new file mode 100644 index 00000000..c7f786c0 --- /dev/null +++ b/src/data-structures/disjoint-set/DisjointSet.js @@ -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; + } +} diff --git a/src/data-structures/disjoint-set/DisjointSetItem.js b/src/data-structures/disjoint-set/DisjointSetItem.js new file mode 100644 index 00000000..fece064f --- /dev/null +++ b/src/data-structures/disjoint-set/DisjointSetItem.js @@ -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; + } +} diff --git a/src/data-structures/disjoint-set/README.md b/src/data-structures/disjoint-set/README.md new file mode 100644 index 00000000..b5d5bfcd --- /dev/null +++ b/src/data-structures/disjoint-set/README.md @@ -0,0 +1,20 @@ +# Disjoint Set + +**Disjoint-set** data structure (also called a union–find data structure or merge–find 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) diff --git a/src/data-structures/disjoint-set/__test__/DisjointSet.test.js b/src/data-structures/disjoint-set/__test__/DisjointSet.test.js new file mode 100644 index 00000000..c62e6393 --- /dev/null +++ b/src/data-structures/disjoint-set/__test__/DisjointSet.test.js @@ -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(); + }); +}); diff --git a/src/data-structures/disjoint-set/__test__/DisjointSetItem.test.js b/src/data-structures/disjoint-set/__test__/DisjointSetItem.test.js new file mode 100644 index 00000000..c3f1000d --- /dev/null +++ b/src/data-structures/disjoint-set/__test__/DisjointSetItem.test.js @@ -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); + }); +});