Skip to content

Commit b1c8105

Browse files
authored
Merge pull request #21 from kareman/binary-search-recursion
Simplify binary search using recursion.
2 parents 68c2606 + f53d5f0 commit b1c8105

3 files changed

Lines changed: 172 additions & 19 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Scheme
3+
LastUpgradeVersion = "0900"
4+
version = "1.3">
5+
<BuildAction
6+
parallelizeBuildables = "YES"
7+
buildImplicitDependencies = "YES">
8+
<BuildActionEntries>
9+
<BuildActionEntry
10+
buildForTesting = "YES"
11+
buildForRunning = "YES"
12+
buildForProfiling = "YES"
13+
buildForArchiving = "YES"
14+
buildForAnalyzing = "YES">
15+
<BuildableReference
16+
BuildableIdentifier = "primary"
17+
BlueprintIdentifier = "52D6DA0E1BF000BD002C0205"
18+
BuildableName = "SortedArray.framework"
19+
BlueprintName = "SortedArray-macOS"
20+
ReferencedContainer = "container:SortedArray.xcodeproj">
21+
</BuildableReference>
22+
</BuildActionEntry>
23+
</BuildActionEntries>
24+
</BuildAction>
25+
<TestAction
26+
buildConfiguration = "Release"
27+
selectedDebuggerIdentifier = ""
28+
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
29+
disableMainThreadChecker = "YES"
30+
language = ""
31+
shouldUseLaunchSchemeArgsEnv = "YES">
32+
<Testables>
33+
<TestableReference
34+
skipped = "NO">
35+
<BuildableReference
36+
BuildableIdentifier = "primary"
37+
BlueprintIdentifier = "DD7502791C68FCFC006590AF"
38+
BuildableName = "SortedArray-macOS Tests.xctest"
39+
BlueprintName = "SortedArray-macOS Tests"
40+
ReferencedContainer = "container:SortedArray.xcodeproj">
41+
</BuildableReference>
42+
<SkippedTests>
43+
<Test
44+
Identifier = "SortedArrayTests">
45+
</Test>
46+
</SkippedTests>
47+
</TestableReference>
48+
</Testables>
49+
<MacroExpansion>
50+
<BuildableReference
51+
BuildableIdentifier = "primary"
52+
BlueprintIdentifier = "52D6DA0E1BF000BD002C0205"
53+
BuildableName = "SortedArray.framework"
54+
BlueprintName = "SortedArray-macOS"
55+
ReferencedContainer = "container:SortedArray.xcodeproj">
56+
</BuildableReference>
57+
</MacroExpansion>
58+
<AdditionalOptions>
59+
</AdditionalOptions>
60+
</TestAction>
61+
<LaunchAction
62+
buildConfiguration = "Release"
63+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
64+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
65+
language = ""
66+
launchStyle = "0"
67+
useCustomWorkingDirectory = "NO"
68+
ignoresPersistentStateOnLaunch = "NO"
69+
debugDocumentVersioning = "YES"
70+
debugServiceExtension = "internal"
71+
allowLocationSimulation = "YES">
72+
<MacroExpansion>
73+
<BuildableReference
74+
BuildableIdentifier = "primary"
75+
BlueprintIdentifier = "52D6DA0E1BF000BD002C0205"
76+
BuildableName = "SortedArray.framework"
77+
BlueprintName = "SortedArray-macOS"
78+
ReferencedContainer = "container:SortedArray.xcodeproj">
79+
</BuildableReference>
80+
</MacroExpansion>
81+
<AdditionalOptions>
82+
</AdditionalOptions>
83+
</LaunchAction>
84+
<ProfileAction
85+
buildConfiguration = "Release"
86+
shouldUseLaunchSchemeArgsEnv = "YES"
87+
savedToolIdentifier = ""
88+
useCustomWorkingDirectory = "NO"
89+
debugDocumentVersioning = "YES">
90+
<MacroExpansion>
91+
<BuildableReference
92+
BuildableIdentifier = "primary"
93+
BlueprintIdentifier = "52D6DA0E1BF000BD002C0205"
94+
BuildableName = "SortedArray.framework"
95+
BlueprintName = "SortedArray-macOS"
96+
ReferencedContainer = "container:SortedArray.xcodeproj">
97+
</BuildableReference>
98+
</MacroExpansion>
99+
</ProfileAction>
100+
<AnalyzeAction
101+
buildConfiguration = "Debug">
102+
</AnalyzeAction>
103+
<ArchiveAction
104+
buildConfiguration = "Release"
105+
revealArchiveInOrganizer = "YES">
106+
</ArchiveAction>
107+
</Scheme>

Sources/SortedArray.swift

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,13 @@ fileprivate enum Match<Index: Comparable> {
358358
case notFound(insertAt: Index)
359359
}
360360

361+
extension Range where Bound == Int {
362+
var middle: Int? {
363+
guard !isEmpty else { return nil }
364+
return lowerBound + count / 2
365+
}
366+
}
367+
361368
extension SortedArray {
362369
/// Searches the array for `element` using binary search.
363370
///
@@ -375,26 +382,15 @@ extension SortedArray {
375382
}
376383

377384
fileprivate func search(for element: Element, in range: Range<Index>) -> Match<Index> {
378-
guard !range.isEmpty else { return .notFound(insertAt: range.upperBound) }
379-
var left = range.lowerBound
380-
var right = index(before: range.upperBound)
381-
382-
while left <= right {
383-
let dist = distance(from: left, to: right)
384-
let mid = index(left, offsetBy: dist/2)
385-
let candidate = self[mid]
386-
387-
switch compare(candidate, element) {
388-
case .orderedAscending:
389-
left = index(after: mid)
390-
case .orderedDescending:
391-
right = index(before: mid)
392-
case .orderedSame:
393-
return .found(at: mid)
394-
}
385+
guard let middle = range.middle else { return .notFound(insertAt: range.upperBound) }
386+
switch compare(element, self[middle]) {
387+
case .orderedDescending:
388+
return search(for: element, in: index(after: middle)..<range.upperBound)
389+
case .orderedAscending:
390+
return search(for: element, in: range.lowerBound..<middle)
391+
case .orderedSame:
392+
return .found(at: middle)
395393
}
396-
// Not found. left is the index where this element should be placed if it were inserted.
397-
return .notFound(insertAt: left)
398394
}
399395
}
400396

Tests/SortedArrayTests/PerformanceTests.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,52 @@ class PerformanceTests: XCTestCase {
2929
XCTAssertEqual(sut.lastIndex(of: 3), 999_999)
3030
}
3131
}
32+
33+
func testPerformanceOfIndexOfObject() {
34+
let sourceArray = Array(repeating: Box(500), count: 1_000_000) + Array(repeating: Box(40), count: 1_000_000) + Array(repeating: Box(3), count: 1_000_000)
35+
let sut = SortedArray(unsorted: sourceArray)
36+
measure {
37+
XCTAssertEqual(sut.index(of: Box(500)), 2_000_000)
38+
}
39+
}
40+
41+
func testPerformanceOfLastIndexOfObject() {
42+
let sourceArray = Array(repeating: Box(500), count: 1_000_000) + Array(repeating: Box(40), count: 1_000_000) + Array(repeating: Box(3), count: 1_000_000)
43+
let sut = SortedArray(unsorted: sourceArray)
44+
measure {
45+
XCTAssertEqual(sut.lastIndex(of: Box(3)), 999_999)
46+
}
47+
}
48+
49+
func testPerformanceOfIndexOfObjectInRange() {
50+
let sut = SortedArray(sorted: (0..<3_000_000).lazy.map(Box.init))
51+
measure {
52+
XCTAssertEqual(sut.index(of: Box(500)), 500)
53+
}
54+
}
55+
56+
func testPerformanceOfLastIndexOfObjectInRange() {
57+
let sut = SortedArray(sorted: (0..<3_000_000).lazy.map(Box.init))
58+
XCTAssertEqual(sut.lastIndex(of: Box(1_999_999)), 1_999_999)
59+
measure {
60+
XCTAssertEqual(sut.lastIndex(of: Box(1_999_999)), 1_999_999)
61+
}
62+
}
63+
}
64+
65+
class Box<T: Comparable>: Comparable {
66+
static func <(lhs: Box<T>, rhs: Box<T>) -> Bool {
67+
return lhs.value < rhs.value
68+
}
69+
70+
static func ==(lhs: Box<T>, rhs: Box<T>) -> Bool {
71+
return lhs.value == rhs.value
72+
}
73+
74+
let value: T
75+
init(_ value: T) {
76+
self.value = value
77+
}
3278
}
3379

3480
extension PerformanceTests {
@@ -37,6 +83,10 @@ extension PerformanceTests {
3783
("testLinuxTestSuiteIncludesAllTests", testLinuxTestSuiteIncludesAllTests),
3884
("testPerformanceOfIndexOf", testPerformanceOfIndexOf),
3985
("testPerformanceOfLastIndexOf", testPerformanceOfLastIndexOf),
86+
("testPerformanceOfIndexOfObject", testPerformanceOfIndexOfObject),
87+
("testPerformanceOfLastIndexOfObject", testPerformanceOfLastIndexOfObject),
88+
("testPerformanceOfIndexOfObjectInRange", testPerformanceOfIndexOfObjectInRange),
89+
("testPerformanceOfLastIndexOfObjectInRange", testPerformanceOfLastIndexOfObjectInRange),
4090
]
4191
}
4292
}

0 commit comments

Comments
 (0)