Skip to content

Commit f41326d

Browse files
committed
Add Disjoint Set collection
1 parent c9dbb56 commit f41326d

File tree

8 files changed

+516
-5
lines changed

8 files changed

+516
-5
lines changed

README.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,50 @@ Alternatively you can pull it from the central Maven repositories:
1010
<dependency>
1111
<groupId>io.github.hextriclosan</groupId>
1212
<artifactId>algorithm</artifactId>
13-
<version>0.0.3</version>
13+
<version>0.0.4</version>
1414
</dependency>
1515
```
1616

1717
#### Gradle
1818
```groovy
19-
implementation 'io.github.hextriclosan:algorithm:0.0.3'
19+
implementation 'io.github.hextriclosan:algorithm:0.0.4'
2020
```
2121

2222
## Basic usage
2323

24+
### Collections
25+
26+
##### Disjoint Set
27+
Disjoint Set a.k.a. Union-Find data structure implementation
28+
```java
29+
DisjointSet<String> disjointSet = new DisjointSet<>();
30+
disjointSet.makeSets(List.of("New York", "Los Angeles", "Chicago", "Houston"));
31+
32+
record Edge(String firstCity, String secondCity, int distance) {
33+
}
34+
35+
List<Edge> edges = List.of(
36+
new Edge("New York", "Los Angeles", 2445),
37+
new Edge("New York", "Chicago", 790),
38+
new Edge("New York", "Houston", 1628),
39+
new Edge("Los Angeles", "Chicago", 2015),
40+
new Edge("Los Angeles", "Houston", 1547),
41+
new Edge("Chicago", "Houston", 1092)
42+
);
43+
44+
edges.stream()
45+
.sorted(Comparator.comparingInt(Edge::distance))
46+
.filter(edge -> disjointSet.find(edge.firstCity()) != disjointSet.find(edge.secondCity()))
47+
.forEach(edge -> {
48+
disjointSet.union(edge.firstCity(), edge.secondCity());
49+
System.out.println(edge);
50+
});
51+
// prints out
52+
// Edge[firstCity=New York, secondCity=Chicago, distance=790]
53+
// Edge[firstCity=Chicago, secondCity=Houston, distance=1092]
54+
// Edge[firstCity=Los Angeles, secondCity=Houston, distance=1547]
55+
```
56+
2457
### Comparators
2558

2659
##### Lexicographical Comparator

pom.xml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>io.github.hextriclosan</groupId>
88
<artifactId>algorithm</artifactId>
9-
<version>0.0.4-SNAPSHOT</version>
9+
<version>0.0.4</version>
1010
<packaging>jar</packaging>
1111

1212
<name>Java Algorithms</name>
@@ -40,11 +40,21 @@
4040
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
4141
</properties>
4242

43+
<dependencyManagement>
44+
<dependencies>
45+
<dependency>
46+
<groupId>org.junit</groupId>
47+
<artifactId>junit-bom</artifactId>
48+
<version>5.10.3</version>
49+
<type>pom</type>
50+
<scope>import</scope>
51+
</dependency>
52+
</dependencies>
53+
</dependencyManagement>
4354
<dependencies>
4455
<dependency>
4556
<groupId>org.junit.jupiter</groupId>
46-
<artifactId>junit-jupiter-engine</artifactId>
47-
<version>5.10.3</version>
57+
<artifactId>junit-jupiter</artifactId>
4858
<scope>test</scope>
4959
</dependency>
5060
</dependencies>
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package io.github.hextriclosan.algorithm.collections;
2+
3+
import io.github.hextriclosan.algorithm.collections.disjointset.FindCompressStrategy;
4+
import io.github.hextriclosan.algorithm.collections.disjointset.FullCompression;
5+
6+
import java.util.Collection;
7+
import java.util.HashMap;
8+
import java.util.Map;
9+
import java.util.Objects;
10+
11+
/**
12+
* A data structure that maintains a collection of disjoint (non-overlapping) sets.
13+
* It supports efficient union and find operations, as well as strategies for path compression and ranking.
14+
*
15+
* @param <E> the type of elements stored in the Disjoint Set
16+
*/
17+
public class DisjointSet<E> {
18+
private final Map<E, E> parentByElement;
19+
private final Map<E, Integer> rankByElement;
20+
private final FindCompressStrategy<E> findCompressStrategy;
21+
22+
/**
23+
* Constructs a DisjointSet with default initializations using {@link FullCompression} strategy.
24+
*/
25+
public DisjointSet() {
26+
this(new HashMap<>(), new HashMap<>(), new FullCompression<>());
27+
}
28+
29+
/**
30+
* Constructs a DisjointSet with a specified path compression strategy.
31+
*
32+
* @param findCompressStrategy the strategy used for path compression and find operations
33+
*/
34+
public DisjointSet(FindCompressStrategy<E> findCompressStrategy) {
35+
this(new HashMap<>(), new HashMap<>(), findCompressStrategy);
36+
}
37+
38+
/**
39+
* Constructs a DisjointSet with custom initial parent and rank mappings, along with a specified path compression strategy.
40+
* This constructor allows the user to recreate a DisjointSet with a particular state, including predefined parent-to-element
41+
* relationships, rank values, and a customized path compression strategy.
42+
*
43+
* @param parentByElement the initial mapping of elements to their parent elements
44+
* @param rankByElement the initial mapping of elements to their ranks (or sizes)
45+
* @param findCompressStrategy the strategy used for path compression and find operations
46+
* @throws NullPointerException if any of the parameters are null
47+
*/
48+
public DisjointSet(Map<E, E> parentByElement, Map<E, Integer> rankByElement, FindCompressStrategy<E> findCompressStrategy) {
49+
Objects.requireNonNull(parentByElement, "parentByElement");
50+
Objects.requireNonNull(rankByElement, "rankByElement");
51+
Objects.requireNonNull(findCompressStrategy, "findCompressStrategy");
52+
this.parentByElement = parentByElement;
53+
this.rankByElement = rankByElement;
54+
this.findCompressStrategy = findCompressStrategy;
55+
}
56+
57+
/**
58+
* Creates a new set with the specified element in the Disjoint Set structure.
59+
* If the element is already present, no action is taken.
60+
*
61+
* @param element the element to initialize as a new set
62+
* @throws NullPointerException if the specified element is null
63+
*/
64+
public void makeSet(E element) {
65+
Objects.requireNonNull(element, "element");
66+
if (!parentByElement.containsKey(element)) {
67+
parentByElement.put(element, element);
68+
rankByElement.put(element, 0);
69+
}
70+
}
71+
72+
/**
73+
* Creates new sets for each element in the specified collection in the Disjoint Set structure.
74+
* If an element is already present, no action is taken for that element.
75+
*
76+
* @param elements the collection of elements to initialize as new sets
77+
* @throws NullPointerException if the specified collection is null or contains null elements
78+
*/
79+
public void makeSets(Collection<E> elements) {
80+
Objects.requireNonNull(elements, "elements");
81+
for (E element : elements) {
82+
makeSet(element);
83+
}
84+
}
85+
86+
/**
87+
* Unites the sets that contain the specified elements into a single set.
88+
*
89+
* @param first the first element
90+
* @param second the second element
91+
* @throws NullPointerException if either of the elements is null
92+
*/
93+
public void union(E first, E second) {
94+
Objects.requireNonNull(first, "first");
95+
Objects.requireNonNull(second, "second");
96+
E firstRoot = find(first);
97+
E secondRoot = find(second);
98+
99+
if (firstRoot == secondRoot) {
100+
return;
101+
}
102+
103+
int firstRootRank = rankByElement.get(firstRoot);
104+
int secondRootRank = rankByElement.get(secondRoot);
105+
106+
if (firstRootRank > secondRootRank) {
107+
parentByElement.put(secondRoot, firstRoot);
108+
} else {
109+
parentByElement.put(firstRoot, secondRoot);
110+
if (firstRootRank == secondRootRank) {
111+
rankByElement.put(secondRoot, secondRootRank + 1);
112+
}
113+
}
114+
}
115+
116+
/**
117+
* Finds the representative (root) of the set containing the specified element,
118+
* using the configured find and path compression strategy.
119+
*
120+
* @param element the element to find
121+
* @return the representative (root) of the set containing the element
122+
* @throws NullPointerException if the element is null
123+
*/
124+
public E find(E element) {
125+
Objects.requireNonNull(element, "element");
126+
return findCompressStrategy.apply(parentByElement, element);
127+
}
128+
129+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package io.github.hextriclosan.algorithm.collections.disjointset;
2+
3+
import java.util.Map;
4+
import java.util.function.BiFunction;
5+
6+
/**
7+
* A strategy interface for the find and path compression operation in the {@link io.github.hextriclosan.algorithm.collections.DisjointSet}.
8+
* This interface extends the {@link BiFunction} interface, taking a map of element-to-parent relationships and
9+
* an element, and returning the representative (or root) of the subset containing the given element.
10+
*
11+
* @param <E> the type of elements in the Disjoint Set
12+
*/
13+
public interface FindCompressStrategy<E> extends BiFunction<Map<E, E>, E, E> {
14+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package io.github.hextriclosan.algorithm.collections.disjointset;
2+
3+
import java.util.Map;
4+
5+
/**
6+
* An implementation of the {@link FindCompressStrategy} interface that performs full path compression.
7+
* This strategy ensures that all nodes along the path from the given element to the root are directly
8+
* connected to the root, optimizing future find operations.
9+
*
10+
* @param <E> the type of elements in the Disjoint Set
11+
*/
12+
public class FullCompression<E> implements FindCompressStrategy<E> {
13+
14+
/**
15+
* Applies the full path compression strategy to find the root of the subset containing the specified element.
16+
* If the element is not found in the map, an {@link IllegalArgumentException} is thrown.
17+
*
18+
* @param parentByElement the map representing the parent relationship of elements in the disjoint set
19+
* @param element the element for which to find the root
20+
* @return the root of the subset containing the specified element
21+
* @throws IllegalArgumentException if the element is not found in the disjoint set
22+
*/
23+
@Override
24+
public E apply(Map<E, E> parentByElement, E element) {
25+
if (!parentByElement.containsKey(element)) {
26+
throw new IllegalArgumentException("Element not found in the disjoint set");
27+
}
28+
29+
E old = element;
30+
E ancestor = parentByElement.get(element);
31+
while (ancestor != element) {
32+
element = ancestor;
33+
ancestor = parentByElement.get(element);
34+
}
35+
36+
element = parentByElement.get(old);
37+
while (ancestor != element) {
38+
parentByElement.put(old, ancestor);
39+
old = element;
40+
element = parentByElement.get(old);
41+
}
42+
43+
return ancestor;
44+
}
45+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package io.github.hextriclosan.algorithm.collections.disjointset;
2+
3+
import java.util.Map;
4+
5+
/**
6+
* An implementation of the {@link FindCompressStrategy} interface that performs path halving compression.
7+
* This strategy optimizes the find operation by halving the path length during path compression.
8+
*
9+
* @param <E> the type of elements in the Disjoint Set
10+
*/
11+
public class PathHalvingCompression<E> implements FindCompressStrategy<E> {
12+
13+
/**
14+
* Applies the path halving compression strategy to find the root of the subset containing the specified element.
15+
* If the element is not found in the map, an {@link IllegalArgumentException} is thrown.
16+
*
17+
* @param parentByElement the map representing the parent relationship of elements in the disjoint set
18+
* @param element the element for which to find the root
19+
* @return the root of the subset containing the specified element
20+
* @throws IllegalArgumentException if the element is not found in the disjoint set
21+
*/
22+
@Override
23+
public E apply(Map<E, E> parentByElement, E element) {
24+
if (!parentByElement.containsKey(element)) {
25+
throw new IllegalArgumentException("Element not found in the disjoint set");
26+
}
27+
28+
E parent = parentByElement.get(element);
29+
E grandparent = parentByElement.get(parent);
30+
while (parent != grandparent) {
31+
parentByElement.put(element, grandparent);
32+
element = grandparent;
33+
parent = parentByElement.get(element);
34+
grandparent = parentByElement.get(parent);
35+
}
36+
37+
return parent;
38+
}
39+
}

0 commit comments

Comments
 (0)