Skip to content

Commit d699d35

Browse files
authored
Add AdjacencyList data structure (#209)
1 parent 7233878 commit d699d35

File tree

3 files changed

+264
-0
lines changed

3 files changed

+264
-0
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
//
2+
// AdjacencyList.swift
3+
// DataStructures
4+
//
5+
// Created by Benjamin Wetherfield on 5/23/19.
6+
//
7+
8+
// Adjacency list representation of a graph
9+
public struct AdjacencyList<Node: Hashable> {
10+
11+
// MARK: - Instance Properties
12+
13+
// Stores the set of nodes adjacent to each node
14+
public let adjacencies: [Node: Set<Node>]
15+
16+
// MARK: - Initializers
17+
18+
public init(_ adjacencies: [Node: Set<Node>]) {
19+
self.adjacencies = adjacencies
20+
}
21+
22+
// Safe initializer that ensures there are no hanging nodes inside of a "value" `Set` that are not within
23+
// the "keys" of `adjancencies`.
24+
public init(safe adjacencies: [Node: Set<Node>]) {
25+
self.adjacencies = adjacencies.reduce(into: [Node: Set<Node>]()) { completed, pair in
26+
let (node, neighbors) = pair
27+
completed[node] = neighbors
28+
neighbors.forEach { neighbor in
29+
if !completed.keys.contains(neighbor) { completed[neighbor] = [] }
30+
}
31+
}
32+
}
33+
}
34+
35+
extension AdjacencyList {
36+
37+
// MARK: - Instance Methods
38+
39+
// Tarjan's algorithm to find strongly connected components
40+
func getStronglyConnectedComponent ()
41+
-> (Node) -> Set<Node> {
42+
43+
func reducer(
44+
_ map: inout [Node: Set<Node>],
45+
_ pair: (key: Node, value: Set<Node>)
46+
) -> () {
47+
48+
func strongConnect (
49+
_ map: inout [Node: Set<Node>],
50+
_ indexAt: inout [Node: Int],
51+
_ lowLinkAt: inout [Node: Int],
52+
_ counter: inout Int,
53+
_ stack: inout Stack<Node>,
54+
_ stackSet: inout Set<Node>,
55+
_ pair: (key: Node, value: Set<Node>)
56+
) -> () {
57+
let (cursor, neighbors) = pair
58+
indexAt[cursor] = counter
59+
lowLinkAt[cursor] = counter
60+
counter += 1
61+
stack.push(cursor)
62+
stackSet.insert(cursor)
63+
64+
neighbors.forEach { neighbor in
65+
if !indexAt.keys.contains(neighbor) {
66+
strongConnect(&map,&indexAt, &lowLinkAt, &counter, &stack, &stackSet,
67+
(key: neighbor, value: adjacencies[neighbor]!))
68+
lowLinkAt[cursor] = min(lowLinkAt[cursor]!, lowLinkAt[neighbor]!)
69+
} else if stackSet.contains(neighbor) {
70+
lowLinkAt[cursor] = min(lowLinkAt[cursor]!, indexAt[neighbor]!)
71+
}
72+
}
73+
74+
if indexAt[cursor]! == lowLinkAt[cursor]! {
75+
var tempSet: Set<Node> = []
76+
while stack.top! != cursor {
77+
let next = stack.pop()!
78+
tempSet.insert(next)
79+
stackSet.remove(next)
80+
}
81+
tempSet.insert(stack.pop()!)
82+
stackSet.remove(cursor)
83+
tempSet.forEach { node in map[node] = tempSet }
84+
}
85+
}
86+
87+
var indexAt: [Node: Int] = [:]
88+
var lowLinkAt: [Node: Int] = [:]
89+
var counter: Int = 0
90+
var stack: Stack<Node> = []
91+
var stackSet: Set<Node> = []
92+
93+
let _ = strongConnect(&map, &indexAt, &lowLinkAt, &counter, &stack, &stackSet, pair)
94+
}
95+
96+
let map = adjacencies.reduce(into: [:], reducer)
97+
return { map[$0]! }
98+
}
99+
100+
// Group nodes according to the set-forming function `nodeClumper` and return the resulting
101+
// `AdjacencyList`, removing self-loops that arise.
102+
func clumpify (via nodeClumper: @escaping (Node) -> Set<Node>) -> AdjacencyList<Set<Node>> {
103+
return AdjacencyList<Set<Node>>(
104+
adjacencies.reduce(into: [Set<Node>: Set<Set<Node>>]()) { list, adjacencyPair in
105+
let (node, adjacentNodes) = adjacencyPair
106+
let clump = nodeClumper(node)
107+
let adjacentClumps = Set(adjacentNodes.map(nodeClumper))
108+
if let existingSet = list[clump] {
109+
list[clump] = existingSet.union(adjacentClumps)
110+
} else {
111+
list[clump] = adjacentClumps
112+
}
113+
list[clump]!.remove(clump)
114+
})
115+
}
116+
117+
// Group nodes according to the function that sends a node to its strongly connected component, as found
118+
// by the implementation of Tarjan's algorithm, hence forming a Directed Acyclic Graph (DAG) version of
119+
// original `AdjacencyList` (`self`).
120+
public func DAGify () -> AdjacencyList<Set<Node>> {
121+
return clumpify (via: getStronglyConnectedComponent())
122+
}
123+
124+
// Determines whether the graph represented by the `AdjacencyList` contains a cycle.
125+
public func containsCycle () -> Bool {
126+
127+
var flag = false
128+
129+
func cycleSearch(_ visited: inout Set<Node>, _ keyValue: (key: Node, value: Set<Node>)) {
130+
131+
func depthFirstSearch (
132+
_ visited: inout Set<Node>,
133+
_ active: inout Set<Node>,
134+
_ keyValue: (key: Node, value: Set<Node>)
135+
) -> Bool
136+
{
137+
let (node, neighbors) = keyValue
138+
if visited.contains(node) { return false }
139+
visited.insert(node)
140+
active.insert(node)
141+
let foundCycle = neighbors.reduce(false) { outcome, neighbor in
142+
if active.contains(neighbor) {
143+
return true
144+
} else if !visited.contains(neighbor) {
145+
return outcome ||
146+
depthFirstSearch(&visited, &active, (neighbor, adjacencies[neighbor]!))
147+
} else {
148+
return outcome
149+
}
150+
}
151+
active.remove(node)
152+
return foundCycle
153+
}
154+
155+
var active: Set<Node> = []
156+
if flag == true { return }
157+
if depthFirstSearch(&visited, &active, keyValue) {
158+
flag = true
159+
}
160+
}
161+
162+
let _ = adjacencies.reduce(into: [], cycleSearch)
163+
return flag
164+
}
165+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//
2+
// AdjacencyListTests.swift
3+
// DataStructuresTests
4+
//
5+
// Created by Benjamin Wetherfield on 5/23/19.
6+
//
7+
8+
import XCTest
9+
import DataStructures
10+
11+
class AdjacencyListTests: XCTestCase {
12+
13+
func testSafeInitializer() {
14+
let adjacencyList = AdjacencyList<Int>(safe: [1:[2,3,4]])
15+
XCTAssertEqual(adjacencyList.adjacencies[1],[2,3,4])
16+
XCTAssertEqual(adjacencyList.adjacencies[2],[])
17+
XCTAssertEqual(adjacencyList.adjacencies[3],[])
18+
XCTAssertEqual(adjacencyList.adjacencies[4],[])
19+
}
20+
21+
func testSimpleCycle() {
22+
let adjacencyList = AdjacencyList<Int>([1:[1]])
23+
XCTAssertTrue(adjacencyList.containsCycle())
24+
}
25+
26+
func testSimpleNonCycle() {
27+
let adjacencyList = AdjacencyList<Int>([1:[]])
28+
XCTAssertFalse(adjacencyList.containsCycle())
29+
}
30+
31+
func testSimpleCycleTwoNodes() {
32+
let adjacencyList = AdjacencyList<Int>([1:[2], 2:[1]])
33+
XCTAssertTrue(adjacencyList.containsCycle())
34+
}
35+
36+
func testSimpleCycleThreeNodesMixed() {
37+
let adjacencyList = AdjacencyList<Int>([1:[2], 2:[1], 3: [1,2]])
38+
XCTAssertTrue(adjacencyList.containsCycle())
39+
}
40+
41+
func testSimpleCycleDisjoint() {
42+
let adjacencyList = AdjacencyList<Int>([1:[], 2:[3], 3:[4], 4:[2]])
43+
XCTAssertTrue(adjacencyList.containsCycle())
44+
}
45+
46+
func testSimpleNoCycleDisjoint() {
47+
let adjacencyList = AdjacencyList<Int>([1:[2], 2:[], 3:[4], 4:[5], 5:[]])
48+
XCTAssertFalse(adjacencyList.containsCycle())
49+
}
50+
51+
func testDAGifySimpleCycle() {
52+
let adjacencyList = AdjacencyList<Int>([1:[1]])
53+
XCTAssertFalse(adjacencyList.DAGify().containsCycle())
54+
}
55+
56+
func testDAGifySimpleNonCycle() {
57+
let adjacencyList = AdjacencyList<Int>([1:[]])
58+
XCTAssertFalse(adjacencyList.DAGify().containsCycle())
59+
}
60+
61+
func testDAGifySimpleCycleTwoNodes() {
62+
let adjacencyList = AdjacencyList<Int>([1:[2], 2:[1]])
63+
XCTAssertFalse(adjacencyList.DAGify().containsCycle())
64+
}
65+
66+
func testDAGifySimpleCycleThreeNodesMixed() {
67+
let adjacencyList = AdjacencyList<Int>([1:[2], 2:[1], 3: [1,2]])
68+
XCTAssertFalse(adjacencyList.DAGify().containsCycle())
69+
}
70+
71+
func testDAGifySimpleCycleDisjoint() {
72+
let adjacencyList = AdjacencyList<Int>([1:[], 2:[3], 3:[4], 4:[2]])
73+
XCTAssertFalse(adjacencyList.DAGify().containsCycle())
74+
}
75+
76+
func testDAGifySimpleNoCycleDisjoint() {
77+
let adjacencyList = AdjacencyList<Int>([1:[2], 2:[], 3:[4], 4:[5], 5:[]])
78+
XCTAssertFalse(adjacencyList.DAGify().containsCycle())
79+
}
80+
}

Tests/DataStructuresTests/XCTestManifests.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,24 @@ extension AVLTreeTests {
1212
]
1313
}
1414

15+
extension AdjacencyListTests {
16+
static let __allTests = [
17+
("testDAGifySimpleCycle", testDAGifySimpleCycle),
18+
("testDAGifySimpleCycleDisjoint", testDAGifySimpleCycleDisjoint),
19+
("testDAGifySimpleCycleThreeNodesMixed", testDAGifySimpleCycleThreeNodesMixed),
20+
("testDAGifySimpleCycleTwoNodes", testDAGifySimpleCycleTwoNodes),
21+
("testDAGifySimpleNoCycleDisjoint", testDAGifySimpleNoCycleDisjoint),
22+
("testDAGifySimpleNonCycle", testDAGifySimpleNonCycle),
23+
("testSafeInitializer", testSafeInitializer),
24+
("testSimpleCycle", testSimpleCycle),
25+
("testSimpleCycleDisjoint", testSimpleCycleDisjoint),
26+
("testSimpleCycleThreeNodesMixed", testSimpleCycleThreeNodesMixed),
27+
("testSimpleCycleTwoNodes", testSimpleCycleTwoNodes),
28+
("testSimpleNoCycleDisjoint", testSimpleNoCycleDisjoint),
29+
("testSimpleNonCycle", testSimpleNonCycle),
30+
]
31+
}
32+
1533
extension ArrayExtensionsTests {
1634
static let __allTests = [
1735
("testInsertingAtIndexAtBeginning", testInsertingAtIndexAtBeginning),
@@ -573,6 +591,7 @@ extension ZipToLongestTests {
573591
public func __allTests() -> [XCTestCaseEntry] {
574592
return [
575593
testCase(AVLTreeTests.__allTests),
594+
testCase(AdjacencyListTests.__allTests),
576595
testCase(ArrayExtensionsTests.__allTests),
577596
testCase(BimapTests.__allTests),
578597
testCase(BinaryHeapTests.__allTests),

0 commit comments

Comments
 (0)