-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add AdjacencyList data structure (#209)
- Loading branch information
1 parent
7233878
commit d699d35
Showing
3 changed files
with
264 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
// | ||
// AdjacencyList.swift | ||
// DataStructures | ||
// | ||
// Created by Benjamin Wetherfield on 5/23/19. | ||
// | ||
|
||
// Adjacency list representation of a graph | ||
public struct AdjacencyList<Node: Hashable> { | ||
|
||
// MARK: - Instance Properties | ||
|
||
// Stores the set of nodes adjacent to each node | ||
public let adjacencies: [Node: Set<Node>] | ||
|
||
// MARK: - Initializers | ||
|
||
public init(_ adjacencies: [Node: Set<Node>]) { | ||
self.adjacencies = adjacencies | ||
} | ||
|
||
// Safe initializer that ensures there are no hanging nodes inside of a "value" `Set` that are not within | ||
// the "keys" of `adjancencies`. | ||
public init(safe adjacencies: [Node: Set<Node>]) { | ||
self.adjacencies = adjacencies.reduce(into: [Node: Set<Node>]()) { completed, pair in | ||
let (node, neighbors) = pair | ||
completed[node] = neighbors | ||
neighbors.forEach { neighbor in | ||
if !completed.keys.contains(neighbor) { completed[neighbor] = [] } | ||
} | ||
} | ||
} | ||
} | ||
|
||
extension AdjacencyList { | ||
|
||
// MARK: - Instance Methods | ||
|
||
// Tarjan's algorithm to find strongly connected components | ||
func getStronglyConnectedComponent () | ||
-> (Node) -> Set<Node> { | ||
|
||
func reducer( | ||
_ map: inout [Node: Set<Node>], | ||
_ pair: (key: Node, value: Set<Node>) | ||
) -> () { | ||
|
||
func strongConnect ( | ||
_ map: inout [Node: Set<Node>], | ||
_ indexAt: inout [Node: Int], | ||
_ lowLinkAt: inout [Node: Int], | ||
_ counter: inout Int, | ||
_ stack: inout Stack<Node>, | ||
_ stackSet: inout Set<Node>, | ||
_ pair: (key: Node, value: Set<Node>) | ||
) -> () { | ||
let (cursor, neighbors) = pair | ||
indexAt[cursor] = counter | ||
lowLinkAt[cursor] = counter | ||
counter += 1 | ||
stack.push(cursor) | ||
stackSet.insert(cursor) | ||
|
||
neighbors.forEach { neighbor in | ||
if !indexAt.keys.contains(neighbor) { | ||
strongConnect(&map,&indexAt, &lowLinkAt, &counter, &stack, &stackSet, | ||
(key: neighbor, value: adjacencies[neighbor]!)) | ||
lowLinkAt[cursor] = min(lowLinkAt[cursor]!, lowLinkAt[neighbor]!) | ||
} else if stackSet.contains(neighbor) { | ||
lowLinkAt[cursor] = min(lowLinkAt[cursor]!, indexAt[neighbor]!) | ||
} | ||
} | ||
|
||
if indexAt[cursor]! == lowLinkAt[cursor]! { | ||
var tempSet: Set<Node> = [] | ||
while stack.top! != cursor { | ||
let next = stack.pop()! | ||
tempSet.insert(next) | ||
stackSet.remove(next) | ||
} | ||
tempSet.insert(stack.pop()!) | ||
stackSet.remove(cursor) | ||
tempSet.forEach { node in map[node] = tempSet } | ||
} | ||
} | ||
|
||
var indexAt: [Node: Int] = [:] | ||
var lowLinkAt: [Node: Int] = [:] | ||
var counter: Int = 0 | ||
var stack: Stack<Node> = [] | ||
var stackSet: Set<Node> = [] | ||
|
||
let _ = strongConnect(&map, &indexAt, &lowLinkAt, &counter, &stack, &stackSet, pair) | ||
} | ||
|
||
let map = adjacencies.reduce(into: [:], reducer) | ||
return { map[$0]! } | ||
} | ||
|
||
// Group nodes according to the set-forming function `nodeClumper` and return the resulting | ||
// `AdjacencyList`, removing self-loops that arise. | ||
func clumpify (via nodeClumper: @escaping (Node) -> Set<Node>) -> AdjacencyList<Set<Node>> { | ||
return AdjacencyList<Set<Node>>( | ||
adjacencies.reduce(into: [Set<Node>: Set<Set<Node>>]()) { list, adjacencyPair in | ||
let (node, adjacentNodes) = adjacencyPair | ||
let clump = nodeClumper(node) | ||
let adjacentClumps = Set(adjacentNodes.map(nodeClumper)) | ||
if let existingSet = list[clump] { | ||
list[clump] = existingSet.union(adjacentClumps) | ||
} else { | ||
list[clump] = adjacentClumps | ||
} | ||
list[clump]!.remove(clump) | ||
}) | ||
} | ||
|
||
// Group nodes according to the function that sends a node to its strongly connected component, as found | ||
// by the implementation of Tarjan's algorithm, hence forming a Directed Acyclic Graph (DAG) version of | ||
// original `AdjacencyList` (`self`). | ||
public func DAGify () -> AdjacencyList<Set<Node>> { | ||
return clumpify (via: getStronglyConnectedComponent()) | ||
} | ||
|
||
// Determines whether the graph represented by the `AdjacencyList` contains a cycle. | ||
public func containsCycle () -> Bool { | ||
|
||
var flag = false | ||
|
||
func cycleSearch(_ visited: inout Set<Node>, _ keyValue: (key: Node, value: Set<Node>)) { | ||
|
||
func depthFirstSearch ( | ||
_ visited: inout Set<Node>, | ||
_ active: inout Set<Node>, | ||
_ keyValue: (key: Node, value: Set<Node>) | ||
) -> Bool | ||
{ | ||
let (node, neighbors) = keyValue | ||
if visited.contains(node) { return false } | ||
visited.insert(node) | ||
active.insert(node) | ||
let foundCycle = neighbors.reduce(false) { outcome, neighbor in | ||
if active.contains(neighbor) { | ||
return true | ||
} else if !visited.contains(neighbor) { | ||
return outcome || | ||
depthFirstSearch(&visited, &active, (neighbor, adjacencies[neighbor]!)) | ||
} else { | ||
return outcome | ||
} | ||
} | ||
active.remove(node) | ||
return foundCycle | ||
} | ||
|
||
var active: Set<Node> = [] | ||
if flag == true { return } | ||
if depthFirstSearch(&visited, &active, keyValue) { | ||
flag = true | ||
} | ||
} | ||
|
||
let _ = adjacencies.reduce(into: [], cycleSearch) | ||
return flag | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
// | ||
// AdjacencyListTests.swift | ||
// DataStructuresTests | ||
// | ||
// Created by Benjamin Wetherfield on 5/23/19. | ||
// | ||
|
||
import XCTest | ||
import DataStructures | ||
|
||
class AdjacencyListTests: XCTestCase { | ||
|
||
func testSafeInitializer() { | ||
let adjacencyList = AdjacencyList<Int>(safe: [1:[2,3,4]]) | ||
XCTAssertEqual(adjacencyList.adjacencies[1],[2,3,4]) | ||
XCTAssertEqual(adjacencyList.adjacencies[2],[]) | ||
XCTAssertEqual(adjacencyList.adjacencies[3],[]) | ||
XCTAssertEqual(adjacencyList.adjacencies[4],[]) | ||
} | ||
|
||
func testSimpleCycle() { | ||
let adjacencyList = AdjacencyList<Int>([1:[1]]) | ||
XCTAssertTrue(adjacencyList.containsCycle()) | ||
} | ||
|
||
func testSimpleNonCycle() { | ||
let adjacencyList = AdjacencyList<Int>([1:[]]) | ||
XCTAssertFalse(adjacencyList.containsCycle()) | ||
} | ||
|
||
func testSimpleCycleTwoNodes() { | ||
let adjacencyList = AdjacencyList<Int>([1:[2], 2:[1]]) | ||
XCTAssertTrue(adjacencyList.containsCycle()) | ||
} | ||
|
||
func testSimpleCycleThreeNodesMixed() { | ||
let adjacencyList = AdjacencyList<Int>([1:[2], 2:[1], 3: [1,2]]) | ||
XCTAssertTrue(adjacencyList.containsCycle()) | ||
} | ||
|
||
func testSimpleCycleDisjoint() { | ||
let adjacencyList = AdjacencyList<Int>([1:[], 2:[3], 3:[4], 4:[2]]) | ||
XCTAssertTrue(adjacencyList.containsCycle()) | ||
} | ||
|
||
func testSimpleNoCycleDisjoint() { | ||
let adjacencyList = AdjacencyList<Int>([1:[2], 2:[], 3:[4], 4:[5], 5:[]]) | ||
XCTAssertFalse(adjacencyList.containsCycle()) | ||
} | ||
|
||
func testDAGifySimpleCycle() { | ||
let adjacencyList = AdjacencyList<Int>([1:[1]]) | ||
XCTAssertFalse(adjacencyList.DAGify().containsCycle()) | ||
} | ||
|
||
func testDAGifySimpleNonCycle() { | ||
let adjacencyList = AdjacencyList<Int>([1:[]]) | ||
XCTAssertFalse(adjacencyList.DAGify().containsCycle()) | ||
} | ||
|
||
func testDAGifySimpleCycleTwoNodes() { | ||
let adjacencyList = AdjacencyList<Int>([1:[2], 2:[1]]) | ||
XCTAssertFalse(adjacencyList.DAGify().containsCycle()) | ||
} | ||
|
||
func testDAGifySimpleCycleThreeNodesMixed() { | ||
let adjacencyList = AdjacencyList<Int>([1:[2], 2:[1], 3: [1,2]]) | ||
XCTAssertFalse(adjacencyList.DAGify().containsCycle()) | ||
} | ||
|
||
func testDAGifySimpleCycleDisjoint() { | ||
let adjacencyList = AdjacencyList<Int>([1:[], 2:[3], 3:[4], 4:[2]]) | ||
XCTAssertFalse(adjacencyList.DAGify().containsCycle()) | ||
} | ||
|
||
func testDAGifySimpleNoCycleDisjoint() { | ||
let adjacencyList = AdjacencyList<Int>([1:[2], 2:[], 3:[4], 4:[5], 5:[]]) | ||
XCTAssertFalse(adjacencyList.DAGify().containsCycle()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters