Skip to content

Commit 84e306b

Browse files
committed
Adds BoyerMooreHorspool & Breadth First Search
1 parent a83afba commit 84e306b

File tree

14 files changed

+322
-0
lines changed

14 files changed

+322
-0
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
//: Playground - noun: a place where people can play
2+
3+
/*
4+
Boyer-Moore string search
5+
6+
This code is based on the article "Faster String Searches" by Costas Menico
7+
from Dr Dobb's magazine, July 1989.
8+
http://www.drdobbs.com/database/faster-string-searches/184408171
9+
*/
10+
extension String {
11+
func index(of pattern: String, usingHorspoolImprovement: Bool = false) -> Index? {
12+
// Cache the length of the search pattern because we're going to
13+
// use it a few times and it's expensive to calculate.
14+
let patternLength = pattern.count
15+
guard patternLength > 0, patternLength <= self.count else { return nil }
16+
17+
// Make the skip table. This table determines how far we skip ahead
18+
// when a character from the pattern is found.
19+
var skipTable = [Character: Int]()
20+
for (i, c) in pattern.enumerated() {
21+
skipTable[c] = patternLength - i - 1
22+
}
23+
24+
// This points at the last character in the pattern.
25+
let p = pattern.index(before: pattern.endIndex)
26+
let lastChar = pattern[p]
27+
28+
// The pattern is scanned right-to-left, so skip ahead in the string by
29+
// the length of the pattern. (Minus 1 because startIndex already points
30+
// at the first character in the source string.)
31+
var i = index(startIndex, offsetBy: patternLength - 1)
32+
33+
// This is a helper function that steps backwards through both strings
34+
// until we find a character that doesn’t match, or until we’ve reached
35+
// the beginning of the pattern.
36+
func backwards() -> Index? {
37+
var q = p
38+
var j = i
39+
while q > pattern.startIndex {
40+
j = index(before: j)
41+
q = index(before: q)
42+
if self[j] != pattern[q] { return nil }
43+
}
44+
return j
45+
}
46+
47+
// The main loop. Keep going until the end of the string is reached.
48+
while i < endIndex {
49+
let c = self[i]
50+
51+
// Does the current character match the last character from the pattern?
52+
if c == lastChar {
53+
54+
// There is a possible match. Do a brute-force search backwards.
55+
if let k = backwards() { return k }
56+
57+
if !usingHorspoolImprovement {
58+
// If no match, we can only safely skip one character ahead.
59+
i = index(after: i)
60+
} else {
61+
// Ensure to jump at least one character (this is needed because the first
62+
// character is in the skipTable, and `skipTable[lastChar] = 0`)
63+
let jumpOffset = max(skipTable[c] ?? patternLength, 1)
64+
i = index(i, offsetBy: jumpOffset, limitedBy: endIndex) ?? endIndex
65+
}
66+
} else {
67+
// The characters are not equal, so skip ahead. The amount to skip is
68+
// determined by the skip table. If the character is not present in the
69+
// pattern, we can skip ahead by the full pattern length. However, if
70+
// the character *is* present in the pattern, there may be a match up
71+
// ahead and we can't skip as far.
72+
i = index(i, offsetBy: skipTable[c] ?? patternLength, limitedBy: endIndex) ?? endIndex
73+
}
74+
}
75+
return nil
76+
}
77+
}
78+
79+
// A few simple tests
80+
81+
let str = "Hello, World"
82+
str.index(of: "World") // 7
83+
84+
let animals = "🐶🐔🐷🐮🐱"
85+
animals.index(of: "🐮") // 6
86+
87+
let lorem = "Lorem ipsum dolor sit amet"
88+
lorem.index(of: "sit", usingHorspoolImprovement: true) // 18
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2+
<playground version='5.0' target-platform='osx'>
3+
<timeline fileName='timeline.xctimeline'/>
4+
</playground>

SwiftAlgorithmClub/BoyerMooreHorspool.playground/playground.xcworkspace/contents.xcworkspacedata

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>IDEDidComputeMac32BitWarning</key>
6+
<true/>
7+
</dict>
8+
</plist>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Timeline
3+
version = "3.0">
4+
<TimelineItems>
5+
<LoggerValueHistoryTimelineItem
6+
documentLocation = "file:///Users/neifmetus/Documents/Projects/swift-algorithm-club/Boyer-Moore-Horspool/BoyerMooreHorspool.playground#CharacterRangeLen=0&amp;CharacterRangeLoc=75&amp;EndingColumnNumber=21&amp;EndingLineNumber=2&amp;StartingColumnNumber=21&amp;StartingLineNumber=2&amp;Timestamp=560891520.806637"
7+
selectedRepresentationIndex = "0"
8+
shouldTrackSuperviewWidth = "NO">
9+
</LoggerValueHistoryTimelineItem>
10+
<LoggerValueHistoryTimelineItem
11+
documentLocation = "file:///Users/neifmetus/Documents/Projects/swift-algorithm-club/Boyer-Moore-Horspool/BoyerMooreHorspool.playground#CharacterRangeLen=0&amp;CharacterRangeLoc=75&amp;EndingColumnNumber=21&amp;EndingLineNumber=2&amp;StartingColumnNumber=21&amp;StartingLineNumber=2&amp;Timestamp=560891520.806855"
12+
selectedRepresentationIndex = "0"
13+
shouldTrackSuperviewWidth = "NO">
14+
</LoggerValueHistoryTimelineItem>
15+
<LoggerValueHistoryTimelineItem
16+
documentLocation = "file:///Users/neifmetus/Documents/Projects/swift-algorithm-club/Boyer-Moore-Horspool/BoyerMooreHorspool.playground#CharacterRangeLen=0&amp;CharacterRangeLoc=75&amp;EndingColumnNumber=21&amp;EndingLineNumber=2&amp;StartingColumnNumber=21&amp;StartingLineNumber=2&amp;Timestamp=560891520.807023"
17+
selectedRepresentationIndex = "0"
18+
shouldTrackSuperviewWidth = "NO">
19+
</LoggerValueHistoryTimelineItem>
20+
</TimelineItems>
21+
</Timeline>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
func breadthFirstSearch(_ graph: Graph, source: Node) -> [String] {
2+
var queue = Queue<Node>()
3+
queue.enqueue(source)
4+
5+
var nodesExplored = [source.label]
6+
source.visited = true
7+
8+
while let node = queue.dequeue() {
9+
for edge in node.neighbors {
10+
let neighborNode = edge.neighbor
11+
if !neighborNode.visited {
12+
queue.enqueue(neighborNode)
13+
neighborNode.visited = true
14+
nodesExplored.append(neighborNode.label)
15+
}
16+
}
17+
}
18+
19+
return nodesExplored
20+
}
21+
22+
let graph = Graph()
23+
24+
let nodeA = graph.addNode("a")
25+
let nodeB = graph.addNode("b")
26+
let nodeC = graph.addNode("c")
27+
let nodeD = graph.addNode("d")
28+
let nodeE = graph.addNode("e")
29+
let nodeF = graph.addNode("f")
30+
let nodeG = graph.addNode("g")
31+
let nodeH = graph.addNode("h")
32+
33+
graph.addEdge(nodeA, neighbor: nodeB)
34+
graph.addEdge(nodeA, neighbor: nodeC)
35+
graph.addEdge(nodeB, neighbor: nodeD)
36+
graph.addEdge(nodeB, neighbor: nodeE)
37+
graph.addEdge(nodeC, neighbor: nodeF)
38+
graph.addEdge(nodeC, neighbor: nodeG)
39+
graph.addEdge(nodeE, neighbor: nodeH)
40+
graph.addEdge(nodeE, neighbor: nodeF)
41+
graph.addEdge(nodeF, neighbor: nodeG)
42+
43+
let nodesExplored = breadthFirstSearch(graph, source: nodeA)
44+
print(nodesExplored)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
public class Edge: Equatable {
2+
public var neighbor: Node
3+
4+
public init(_ neighbor: Node) {
5+
self.neighbor = neighbor
6+
}
7+
}
8+
9+
public func == (_ lhs: Edge, rhs: Edge) -> Bool {
10+
return lhs.neighbor == rhs.neighbor
11+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
public class Graph: CustomStringConvertible, Equatable {
2+
public private(set) var nodes: [Node]
3+
4+
public init() {
5+
self.nodes = []
6+
}
7+
8+
@discardableResult public func addNode(_ label: String) -> Node {
9+
let node = Node(label)
10+
nodes.append(node)
11+
return node
12+
}
13+
14+
public func addEdge(_ source: Node, neighbor: Node) {
15+
let edge = Edge(neighbor)
16+
source.neighbors.append(edge)
17+
}
18+
19+
public var description: String {
20+
var description = ""
21+
22+
for node in nodes where !node.neighbors.isEmpty {
23+
description += "[node: \(node.label) edges: \(node.neighbors.map { $0.neighbor.label})]"
24+
}
25+
return description
26+
}
27+
28+
public func findNodeWithLabel(_ label: String) -> Node {
29+
return nodes.filter { $0.label == label }.first!
30+
}
31+
32+
public func duplicate() -> Graph {
33+
let duplicated = Graph()
34+
35+
for node in nodes {
36+
duplicated.addNode(node.label)
37+
}
38+
39+
for node in nodes {
40+
for edge in node.neighbors {
41+
let source = duplicated.findNodeWithLabel(node.label)
42+
let neighbour = duplicated.findNodeWithLabel(edge.neighbor.label)
43+
duplicated.addEdge(source, neighbor: neighbour)
44+
}
45+
}
46+
47+
return duplicated
48+
}
49+
}
50+
51+
public func == (_ lhs: Graph, rhs: Graph) -> Bool {
52+
return lhs.nodes == rhs.nodes
53+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
public class Node: CustomStringConvertible, Equatable {
2+
public var neighbors: [Edge]
3+
4+
public private(set) var label: String
5+
public var distance: Int?
6+
public var visited: Bool
7+
8+
public init(_ label: String) {
9+
self.label = label
10+
neighbors = []
11+
visited = false
12+
}
13+
14+
public var description: String {
15+
if let distance = distance {
16+
return "Node(label: \(label), distance: \(distance))"
17+
}
18+
return "Node(label: \(label), distance: infinity)"
19+
}
20+
21+
public var hasDistance: Bool {
22+
return distance != nil
23+
}
24+
25+
public func remove(_ edge: Edge) {
26+
neighbors.remove(at: neighbors.firstIndex { $0 === edge }!)
27+
}
28+
}
29+
30+
public func == (_ lhs: Node, rhs: Node) -> Bool {
31+
return lhs.label == rhs.label && lhs.neighbors == rhs.neighbors
32+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
public struct Queue<T> {
2+
private var array: [T]
3+
4+
public init() {
5+
array = []
6+
}
7+
8+
public var isEmpty: Bool {
9+
return array.isEmpty
10+
}
11+
12+
public var count: Int {
13+
return array.count
14+
}
15+
16+
public mutating func enqueue(_ element: T) {
17+
array.append(element)
18+
}
19+
20+
public mutating func dequeue() -> T? {
21+
if isEmpty {
22+
return nil
23+
} else {
24+
return array.removeFirst()
25+
}
26+
}
27+
28+
public func peek() -> T? {
29+
return array.first
30+
}
31+
}

0 commit comments

Comments
 (0)