Skip to content

Commit f16db24

Browse files
BrianLusinaBrianLusina
authored andcommitted
feat(puzzles): find bitonic peak using binary search
This adds a new function to find a bitonic peak in a given list of integers. This is a variation of find_peak_element and achieves the same outcome with the same time and space complexity of O(1) and O(log(n)). Additional tests and comments have been added for clarity to make the algorithm easier to understand and reason about.
1 parent 2786311 commit f16db24

File tree

6 files changed

+175
-5
lines changed

6 files changed

+175
-5
lines changed

puzzles/search/binary_search/find_peak_element/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Find Peak Element
1+
# Find Peak Element(or Find Bitonic Peak)
22

33
A peak element is an element that is strictly greater than its neighbors.
44

puzzles/search/binary_search/find_peak_element/__init__.py

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from typing import List
1+
from typing import List, Optional
22

33

4-
def find_peak_element(nums: List[int]) -> int:
4+
def find_peak_element(nums: List[int]) -> Optional[int]:
55
"""Finds a peak element's index in the provided list of integers
66
77
Algorithm:
@@ -25,14 +25,21 @@ def find_peak_element(nums: List[int]) -> int:
2525
Returns:
2626
int: index of a peak element(i.e. element that is greater than its adjacent neighbours)
2727
"""
28+
# we require at least 3 numbers to form a bitonic peak
29+
if len(nums) < 3:
30+
return None
31+
2832
left = 0
2933
right = len(nums) - 1
3034
peak_index = -1
3135

3236
while left <= right:
3337
mid = (left + right) >> 1
3438

35-
# check that the potential peak element is within bounds or is greater than both its neighbours
39+
# check that the potential peak element is within bounds or is greater than both its neighbours. The
40+
# `mid==len(nums)-1` check is to ensure we are not at the end of the list. The potential_peak > nums[mid + 1]
41+
# checks if the peak is greater than the next number. If either condition evaluates to true, the peak index is
42+
# set to the middle and the right boundary is moved to the middle minus 1, this removes the right half
3643
potential_peak = nums[mid]
3744
if mid == len(nums) - 1 or potential_peak > nums[mid + 1]:
3845
peak_index = mid
@@ -41,3 +48,51 @@ def find_peak_element(nums: List[int]) -> int:
4148
left = mid + 1
4249

4350
return peak_index
51+
52+
53+
def find_bitonic_peak(nums: List[int]) -> Optional[int]:
54+
"""
55+
Finds the bitonic peak or the peak element in a list of numbers. A bitonic peak is an integer whose value is greater
56+
thant its neighbours on both the immediate left and the immediate right.
57+
58+
Complexity:
59+
60+
- Time complexity O(log n): Where n is the number of elements in the nums vector.
61+
- Space Complexity O(1): Since it uses a constant amount of extra space.
62+
63+
Args:
64+
nums (List): list of integers
65+
Returns:
66+
int: index of a peak element(i.e. element that is greater than its adjacent neighbours)
67+
"""
68+
# we require at least 3 numbers to form a bitonic peak
69+
if len(nums) < 3:
70+
return None
71+
72+
left = 0
73+
right = len(nums) - 1
74+
75+
while left <= right:
76+
mid = (left + right) >> 1
77+
78+
# we assign the mid_left and mid_right based on the middle index. If the middle index - 1(i.e. left of the middle
79+
# is greater than or equal to 0, we assign that value to mid_left, else we assign it a negative infinity value.
80+
# the negative infinity indicates that the value is out of bounds of the list.
81+
# On the other hand, if the middle index + 1 (i.e. the right of the middle index) is less than the length of the
82+
# list, then we assign it that value to the mid_right. If not, we give the mid_right a value of positive infinity
83+
# indicating that this is out of bounds of the list
84+
mid_left = nums[mid - 1] if mid - 1 >= 0 else float("-inf")
85+
mid_right = nums[mid + 1] if mid + 1 < len(nums) else float("inf")
86+
87+
# if the mid_left is less than the middle value and the mid_right is greater than the middle value, we move the
88+
# left pointer to 1 value greater than the middle index
89+
if mid_left < nums[mid] < mid_right:
90+
left = mid + 1
91+
elif mid_left > nums[mid] > mid_right:
92+
# we move the right pointer to middle index minus 1
93+
right = mid - 1
94+
elif mid_left < nums[mid] and mid_right < nums[mid]:
95+
# else we return the middle value
96+
return mid
97+
98+
return None

puzzles/search/binary_search/find_peak_element/test_find_peak_element.py

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import unittest
2-
from . import find_peak_element
2+
from . import find_peak_element, find_bitonic_peak
33

44

55
class FindPeakElementTestCase(unittest.TestCase):
@@ -88,6 +88,121 @@ def test_9(self):
8888
actual = find_peak_element(nums)
8989
self.assertEqual(expected, actual)
9090

91+
def test_10(self):
92+
"""should return 3 from [1, 2, 3, 4, 1]"""
93+
nums = [1, 2, 3, 4, 1]
94+
expected = 3
95+
actual = find_peak_element(nums)
96+
self.assertEqual(expected, actual)
97+
98+
def test_11(self):
99+
"""should return 3 from [1, 6, 5, 4, 3, 2, 1]"""
100+
nums = [1, 6, 5, 4, 3, 2, 1]
101+
expected = 1
102+
actual = find_peak_element(nums)
103+
self.assertEqual(expected, actual)
104+
105+
106+
class FindBitonicPeakTestCases(unittest.TestCase):
107+
def test_1(self):
108+
"""should return 2 for nums = [1,2,3,1]"""
109+
nums = [1, 2, 3, 1]
110+
expected = 2
111+
actual = find_bitonic_peak(nums)
112+
self.assertEqual(expected, actual)
113+
114+
def test_2(self):
115+
"""should return either 5 or 1 for nums = [1,2,1,3,5,6,4]"""
116+
nums = [1, 2, 1, 3, 5, 6, 4]
117+
expected_5 = 5
118+
expected_1 = 1
119+
actual = find_bitonic_peak(nums)
120+
self.assertIn(actual, [expected_1, expected_5])
121+
122+
def test_3(self):
123+
"""should return either 3 for nums = [0, 1, 2, 3, 2, 1, 0]"""
124+
nums = [0, 1, 2, 3, 2, 1, 0]
125+
expected = 3
126+
actual = find_bitonic_peak(nums)
127+
self.assertEqual(actual, expected)
128+
129+
def test_4(self):
130+
"""should return 1 from 0 10 3 2 1 0"""
131+
nums = [0, 10, 3, 2, 1, 0]
132+
expected = 1
133+
actual = find_bitonic_peak(nums)
134+
self.assertEqual(expected, actual)
135+
136+
def test_5(self):
137+
"""should return 1 from 0 10 0"""
138+
nums = [0, 10, 0]
139+
expected = 1
140+
actual = find_bitonic_peak(nums)
141+
self.assertEqual(expected, actual)
142+
143+
def test_6(self):
144+
"""should return 16 from 0 1 2 12 22 32 42 52 62 72 82 92 102 112 122 132 133 132 111 0"""
145+
nums = [
146+
0,
147+
1,
148+
2,
149+
12,
150+
22,
151+
32,
152+
42,
153+
52,
154+
62,
155+
72,
156+
82,
157+
92,
158+
102,
159+
112,
160+
122,
161+
132,
162+
133,
163+
132,
164+
111,
165+
0,
166+
]
167+
expected = 16
168+
actual = find_bitonic_peak(nums)
169+
self.assertEqual(expected, actual)
170+
171+
def test_7(self):
172+
"""should return 1 from 0, 10, 5, 2"""
173+
nums = [0, 10, 5, 2]
174+
expected = 1
175+
actual = find_bitonic_peak(nums)
176+
self.assertEqual(expected, actual)
177+
178+
def test_8(self):
179+
"""should return 1 from 0, 2, 1, 0"""
180+
nums = [0, 2, 1, 0]
181+
expected = 1
182+
actual = find_bitonic_peak(nums)
183+
self.assertEqual(expected, actual)
184+
185+
def test_9(self):
186+
"""should return 1 from 0, 1, 0"""
187+
nums = [0, 1, 0]
188+
expected = 1
189+
actual = find_bitonic_peak(nums)
190+
self.assertEqual(expected, actual)
191+
192+
def test_10(self):
193+
"""should return 3 from [1, 2, 3, 4, 1]"""
194+
nums = [1, 2, 3, 4, 1]
195+
expected = 3
196+
actual = find_bitonic_peak(nums)
197+
self.assertEqual(expected, actual)
198+
199+
def test_11(self):
200+
"""should return 3 from [1, 6, 5, 4, 3, 2, 1]"""
201+
nums = [1, 6, 5, 4, 3, 2, 1]
202+
expected = 1
203+
actual = find_bitonic_peak(nums)
204+
self.assertEqual(expected, actual)
205+
91206

92207
if __name__ == "__main__":
93208
unittest.main()

0 commit comments

Comments
 (0)