Skip to content

Commit 2b5e10a

Browse files
committed
Add binary search
1 parent a8c6ed1 commit 2b5e10a

File tree

6 files changed

+308
-2
lines changed

6 files changed

+308
-2
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//: Playground - noun: a place where people can play
2+
3+
// An unsorted array of numbers
4+
let numbers = [11, 59, 3, 2, 53, 17, 31, 7, 19, 67, 47, 13, 37, 61, 29, 43, 5, 41, 23]
5+
6+
// Binary search requires that the array is sorted from low to high
7+
let sorted = numbers.sort()
8+
9+
/*
10+
The recursive version of binary search.
11+
*/
12+
func binarySearch<T: Comparable>(a: [T], key: T, range: Range<Int>) -> Int? {
13+
if range.startIndex >= range.endIndex {
14+
return nil
15+
} else {
16+
let midIndex = range.startIndex + (range.endIndex - range.startIndex) / 2
17+
if a[midIndex] > key {
18+
return binarySearch(a, key: key, range: range.startIndex ..< midIndex)
19+
} else if a[midIndex] < key {
20+
return binarySearch(a, key: key, range: midIndex + 1 ..< range.endIndex)
21+
} else {
22+
return midIndex
23+
}
24+
}
25+
}
26+
27+
binarySearch(sorted, key: 2, range: 0 ..< sorted.count) // gives 0
28+
binarySearch(sorted, key: 67, range: 0 ..< sorted.count) // gives 18
29+
binarySearch(sorted, key: 43, range: 0 ..< sorted.count) // gives 13
30+
binarySearch(sorted, key: 42, range: 0 ..< sorted.count) // nil
31+
32+
/*
33+
The iterative version of binary search.
34+
35+
Notice how similar these functions are. The difference is that this one
36+
uses a while loop, while the other calls itself recursively.
37+
*/
38+
func binarySearch<T: Comparable>(a: [T], key: T) -> Int? {
39+
var range = 0..<a.count
40+
while range.startIndex < range.endIndex {
41+
let midIndex = range.startIndex + (range.endIndex - range.startIndex) / 2
42+
if a[midIndex] == key {
43+
return midIndex
44+
} else if a[midIndex] < key {
45+
range.startIndex = midIndex + 1
46+
} else {
47+
range.endIndex = midIndex
48+
}
49+
}
50+
return nil
51+
}
52+
53+
binarySearch(sorted, key: 2) // gives 0
54+
binarySearch(sorted, key: 67) // gives 18
55+
binarySearch(sorted, key: 43) // gives 13
56+
binarySearch(sorted, key: 42) // nil
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>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Timeline
3+
version = "3.0">
4+
<TimelineItems>
5+
</TimelineItems>
6+
</Timeline>

Binary Search/BinarySearch.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
Binary Search
3+
4+
Recursively splits the array in half until the value is found.
5+
6+
If there is more than one occurrence of the search key in the array, then
7+
there is no guarantee which one it finds.
8+
9+
Note: The array must be sorted!
10+
*/
11+
func binarySearch<T: Comparable>(a: [T], key: T) -> Int? {
12+
var range = 0..<a.count
13+
while range.startIndex < range.endIndex {
14+
let midIndex = range.startIndex + (range.endIndex - range.startIndex) / 2
15+
if a[midIndex] == key {
16+
return midIndex
17+
} else if a[midIndex] < key {
18+
range.startIndex = midIndex + 1
19+
} else {
20+
range.endIndex = midIndex
21+
}
22+
}
23+
return nil
24+
}

Binary Search/README.markdown

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
# Binary Search
2+
3+
Goal: Quickly find an element in an array.
4+
5+
Let's say you have an array of numbers and you want to determine whether a specific number is in that array, and if so, at which index.
6+
7+
In most cases, Swift's `indexOf()` function is good enough for that:
8+
9+
```swift
10+
let numbers = [11, 59, 3, 2, 53, 17, 31, 7, 19, 67, 47, 13, 37, 61, 29, 43, 5, 41, 23]
11+
12+
numbers.indexOf(43) // returns 15
13+
```
14+
15+
The built-in `indexOf()` function performs a [linear search](../Linear Search/). In code that looks something like this:
16+
17+
```swift
18+
func linearSearch<T: Equatable>(a: [T], _ key: T) -> Int? {
19+
for i in 0 ..< a.count {
20+
if a[i] == key {
21+
return i
22+
}
23+
}
24+
return nil
25+
}
26+
```
27+
28+
And you'd use it like this:
29+
30+
```swift
31+
linearSearch(numbers, 43) // returns 15
32+
```
33+
34+
So what's the problem? `linearSearch()` loops through the entire array from the beginning until it finds the element you're looking for. In the worst case, the value isn't in the array and all that work is done for nothing.
35+
36+
On average, the linear search algorithm needs to look at half the values in the array. If your array is large enough, this starts to become very slow!
37+
38+
## Divide and conquer
39+
40+
The classic way to speed this up is to use a *binary search*. The trick is to keep splitting the array in half until the value is found.
41+
42+
For an array of size `n`, the performance is not **O(n)** as with linear search but only **O(log n)**. To put that in perspective, binary search on an array with 1,000,000 elements only takes about 20 steps to find what you're looking for, because `log_2(1,000,000) = 19.9`. And for an array with a billion elements it only takes 30 steps. (Then again, when was the last time you used an array with a billion items?)
43+
44+
Sounds great, but there is one downside to using binary search: the array must be sorted. In practice, this usually isn't a problem.
45+
46+
Here's how binary search works:
47+
48+
- Split the array in half and determine whether the thing you're looking for, known as the *search key*, is in the left half or in the right half.
49+
- How do you determine in which half the search key is? This is why you sorted the array first, so you can do a simple less-than or greater-than comparison.
50+
- If the search key is in the left half, you repeat the process there: split the left half into two even smaller pieces and look in which piece the search key must lie. (Likewise for when it's the right half.)
51+
- This repeats until the search key is found. If the array cannot be split up any further, you must regrettably conclude that the search key is not present in the array.
52+
53+
Now you know why it's called a "binary" search: in every step it splits the array into two halves. This process of *divide-and-conquer* is what allows it to quickly narrow down where the search key must be.
54+
55+
## The code
56+
57+
Here is a recursive implementation of binary search in Swift:
58+
59+
```swift
60+
func binarySearch<T: Comparable>(a: [T], key: T, range: Range<Int>) -> Int? {
61+
if range.startIndex >= range.endIndex {
62+
// If we get here, then the search key is not present in the array.
63+
return nil
64+
65+
} else {
66+
// Calculate where to split the array.
67+
let midIndex = range.startIndex + (range.endIndex - range.startIndex) / 2
68+
69+
// Is the search key in the left half?
70+
if a[midIndex] > key {
71+
return binarySearch(a, key: key, range: range.startIndex ..< midIndex)
72+
73+
// Is the search key in the right half?
74+
} else if a[midIndex] < key {
75+
return binarySearch(a, key: key, range: midIndex + 1 ..< range.endIndex)
76+
77+
// If we get here, then we've found the search key!
78+
} else {
79+
return midIndex
80+
}
81+
}
82+
}
83+
```
84+
85+
To try this out, copy the code to a playground and do:
86+
87+
```swift
88+
let numbers = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67]
89+
90+
binarySearch(numbers, key: 43, range: 0 ..< numbers.count) // gives 13
91+
```
92+
93+
Note that the `numbers` array is sorted. The binary search algorithm does not work otherwise!
94+
95+
I said that binary search works by splitting the array in half, but we don't actually create two new arrays. Instead, we keep track of these splits using a Swift `Range` object.
96+
97+
Initially, this range covers the entire array, `0 ..< numbers.count`. As we split the array, the range becomes smaller and smaller.
98+
99+
> **Note:** One thing to be aware of is that `range.endIndex` always points one beyond the last element. In the example, the range is `0..<19` because there are 19 numbers in the array, and so `range.startIndex = 0` and `range.endIndex = 19`. But in our array the last element is at index 18, since we start counting from 0. Just keep this in mind when working with ranges: the `endIndex` is always one more than the index of the last element.
100+
101+
## Stepping through the example
102+
103+
It might be useful to look at how the algorithm works in detail.
104+
105+
The array from the above example consists of 19 numbers and looks like this when sorted:
106+
107+
[ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67 ]
108+
109+
We're trying to determine if the number `43` is in this array.
110+
111+
To split the array in half, we need to know the index of the object in the middle. That's determined by this line:
112+
113+
```swift
114+
let midIndex = range.startIndex + (range.endIndex - range.startIndex) / 2
115+
```
116+
117+
Initially, the range has `startIndex = 0` and `endIndex = 19`. Filling in these values, we find that `midIndex` is `0 + (19 - 0)/2 = 19/2 = 9`. It's actually `9.5` but because we're using integers, the answer is rounded down.
118+
119+
In the next figure, the `*` shows the middle item. As you can see, the number of items on each side is the same, so we're split right down the middle.
120+
121+
[ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67 ]
122+
*
123+
124+
Now binary search will determine which half to use. The relevant section from the code is:
125+
126+
```swift
127+
if a[midIndex] > key {
128+
// use left half
129+
} else if a[midIndex] < key {
130+
// use right half
131+
} else {
132+
return midIndex
133+
}
134+
```
135+
136+
In this case, `a[midIndex] = 29`. That's less than the search key, so we can safely conclude that the search key will never be in the left half of the array, since that only contains numbers smaller than `29`. Hence, the search key must be in the right half somewhere (or not in the array at all).
137+
138+
Now we can simply repeat the binary search, but on the array interval from `midIndex + 1` to `range.endIndex`:
139+
140+
[ x, x, x, x, x, x, x, x, x, x | 31, 37, 41, 43, 47, 53, 59, 61, 67 ]
141+
142+
Since we no longer need to concern ourselves with the left half of the array, I've marked that with `x`'s. From now on we'll only look at the right half, which starts at array index 10.
143+
144+
We calculate the index of the new middle element: `midIndex = 10 + (19 - 10)/2 = 14`, and split the array down the middle again.
145+
146+
[ x, x, x, x, x, x, x, x, x, x | 31, 37, 41, 43, 47, 53, 59, 61, 67 ]
147+
*
148+
149+
As you can see, `a[14]` is indeed the middle element of the array's right half.
150+
151+
Is the search key greater or smaller than `a[14]`? It's smaller because `43 < 47`. This time we're taking the left half and ignore the larger numbers on the right:
152+
153+
[ x, x, x, x, x, x, x, x, x, x | 31, 37, 41, 43 | x, x, x, x, x ]
154+
155+
The new `midIndex` is here:
156+
157+
[ x, x, x, x, x, x, x, x, x, x | 31, 37, 41, 43 | x, x, x, x, x ]
158+
*
159+
160+
The search key is greater than `37`, so continue with the right side:
161+
162+
[ x, x, x, x, x, x, x, x, x, x | x, x | 41, 43 | x, x, x, x, x ]
163+
*
164+
165+
Again, the search key is greater, so split once more and take the right side:
166+
167+
[ x, x, x, x, x, x, x, x, x, x | x, x | x | 43 | x, x, x, x, x ]
168+
*
169+
170+
And now we're done. The search key equals the array element we're looking at, so we've finally found what we were looking for. Number `43` is at array index `13`. w00t!
171+
172+
It may have seemed like a lot of work, but in reality it only took four steps to find the search key in the array, which sounds about right because `log_2(19) = 4.23`. With a linear search, it would have taken 14 steps.
173+
174+
What would happen if we were to look for `42` instead of `43`? In that case, we can't split up the array any further.The `range.endIndex` becomes smaller than `range.startIndex` and that tells the algorithm the search key is not in the array and it returns `nil`.
175+
176+
> **Note:** Many implementations of binary search calculate `midIndex = (start + end) / 2`. This contains a subtle bug that only appears with very large arrays, because `start + end` may overflow the maximum number an integer can hold. This situation is unlikely to happen on a 64-bit CPU, but it definitely can on 32-bit machines.
177+
178+
## Iterative vs recursive
179+
180+
Binary search is recursive in nature because you apply the same logic over and over again to smaller and smaller subarrays. However, that does not mean you must implement `binarySearch()` as a recursive function. It's often more efficient to convert a recursive algorithm into an iterative version, using a simple loop instead of lots of recursive function calls.
181+
182+
Here is an iterative implementation of binary search in Swift:
183+
184+
```swift
185+
func binarySearch<T: Comparable>(a: [T], key: T) -> Int? {
186+
var range = 0..<a.count
187+
while range.startIndex < range.endIndex {
188+
let midIndex = range.startIndex + (range.endIndex - range.startIndex) / 2
189+
if a[midIndex] == key {
190+
return midIndex
191+
} else if a[midIndex] < key {
192+
range.startIndex = midIndex + 1
193+
} else {
194+
range.endIndex = midIndex
195+
}
196+
}
197+
return nil
198+
}
199+
```
200+
201+
As you can see, the code is very similar to the recursive version. The main difference is in the use of the `while` loop.
202+
203+
Use it like this:
204+
205+
```swift
206+
let numbers = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67]
207+
208+
binarySearch(numbers, key: 43) // gives 13
209+
```
210+
211+
## The end
212+
213+
Is it a problem that the array must be sorted first? It depends. Keep in mind that sorting takes time -- the combination of binary search plus sorting may be slower than doing a simple linear search. Binary search shines in situations where you sort just once and then do many searches.
214+
215+
See also [Wikipedia](https://en.wikipedia.org/wiki/Binary_search_algorithm).
216+
217+
*Written by Matthijs Hollemans*

README.markdown

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@ If you're new to algorithms and data structures, here are a few good ones to sta
2828

2929
### Searching
3030

31-
- Linear Search
32-
- Binary Search
31+
- [Binary Search](Binary Search/). Quickly find elements in a sorted array.
3332
- Count Occurrences
3433
- Select Minimum / Maximum
3534
- Select k-th Largest Element

0 commit comments

Comments
 (0)