Skip to content

Commit af178ff

Browse files
sozelfistvil02
andauthored
Refactor Ternary Search (#815)
* ref: refactor ternary search * chore: replace if-else by match with guard * test: verify test data --------- Co-authored-by: Piotr Idzik <[email protected]>
1 parent 3139471 commit af178ff

File tree

1 file changed

+170
-66
lines changed

1 file changed

+170
-66
lines changed

src/searching/ternary_search.rs

Lines changed: 170 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,195 @@
1+
//! This module provides an implementation of a ternary search algorithm that
2+
//! works for both ascending and descending ordered arrays. The ternary search
3+
//! function returns the index of the target element if it is found, or `None`
4+
//! if the target is not present in the array.
5+
16
use std::cmp::Ordering;
27

3-
pub fn ternary_search<T: Ord>(
4-
target: &T,
5-
list: &[T],
6-
mut start: usize,
7-
mut end: usize,
8-
) -> Option<usize> {
9-
if list.is_empty() {
8+
/// Performs a ternary search for a specified item within a sorted array.
9+
///
10+
/// This function can handle both ascending and descending ordered arrays. It
11+
/// takes a reference to the item to search for and a slice of the array. If
12+
/// the item is found, it returns the index of the item within the array. If
13+
/// the item is not found, it returns `None`.
14+
///
15+
/// # Parameters
16+
///
17+
/// - `item`: A reference to the item to search for.
18+
/// - `arr`: A slice of the sorted array in which to search.
19+
///
20+
/// # Returns
21+
///
22+
/// An `Option<usize>` which is:
23+
/// - `Some(index)` if the item is found at the given index.
24+
/// - `None` if the item is not found in the array.
25+
pub fn ternary_search<T: Ord>(item: &T, arr: &[T]) -> Option<usize> {
26+
if arr.is_empty() {
1027
return None;
1128
}
1229

13-
while start <= end {
14-
let mid1: usize = start + (end - start) / 3;
15-
let mid2: usize = end - (end - start) / 3;
30+
let is_asc = is_asc_arr(arr);
31+
let mut left = 0;
32+
let mut right = arr.len() - 1;
1633

17-
match target.cmp(&list[mid1]) {
18-
Ordering::Less => end = mid1 - 1,
19-
Ordering::Equal => return Some(mid1),
20-
Ordering::Greater => match target.cmp(&list[mid2]) {
21-
Ordering::Greater => start = mid2 + 1,
22-
Ordering::Equal => return Some(mid2),
23-
Ordering::Less => {
24-
start = mid1 + 1;
25-
end = mid2 - 1;
26-
}
27-
},
34+
while left <= right {
35+
if match_compare(item, arr, &mut left, &mut right, is_asc) {
36+
return Some(left);
2837
}
2938
}
3039

3140
None
3241
}
3342

34-
#[cfg(test)]
35-
mod tests {
36-
use super::*;
43+
/// Compares the item with two middle elements of the current search range and
44+
/// updates the search bounds accordingly. This function handles both ascending
45+
/// and descending ordered arrays. It calculates two middle indices of the
46+
/// current search range and compares the item with the elements at these
47+
/// indices. It then updates the search bounds (`left` and `right`) based on
48+
/// the result of these comparisons. If the item is found, it returns `true`.
49+
///
50+
/// # Parameters
51+
///
52+
/// - `item`: A reference to the item to search for.
53+
/// - `arr`: A slice of the array in which to search.
54+
/// - `left`: A mutable reference to the left bound of the search range.
55+
/// - `right`: A mutable reference to the right bound of the search range.
56+
/// - `is_asc`: A boolean indicating whether the array is sorted in ascending order.
57+
///
58+
/// # Returns
59+
///
60+
/// A `bool` indicating:
61+
/// - `true` if the item was found in the array.
62+
/// - `false` if the item was not found in the array.
63+
fn match_compare<T: Ord>(
64+
item: &T,
65+
arr: &[T],
66+
left: &mut usize,
67+
right: &mut usize,
68+
is_asc: bool,
69+
) -> bool {
70+
let first_mid = *left + (*right - *left) / 3;
71+
let second_mid = *right - (*right - *left) / 3;
3772

38-
#[test]
39-
fn returns_none_if_empty_list() {
40-
let index = ternary_search(&"a", &[], 1, 10);
41-
assert_eq!(index, None);
73+
// Handling the edge case where the search narrows down to a single element
74+
if first_mid == second_mid && first_mid == *left {
75+
return match &arr[*left] {
76+
x if x == item => true,
77+
_ => {
78+
*left += 1;
79+
false
80+
}
81+
};
4282
}
4383

44-
#[test]
45-
fn returns_none_if_range_is_invalid() {
46-
let index = ternary_search(&1, &[1, 2, 3], 2, 1);
47-
assert_eq!(index, None);
48-
}
84+
let cmp_first_mid = item.cmp(&arr[first_mid]);
85+
let cmp_second_mid = item.cmp(&arr[second_mid]);
4986

50-
#[test]
51-
fn returns_index_if_list_has_one_item() {
52-
let index = ternary_search(&1, &[1], 0, 1);
53-
assert_eq!(index, Some(0));
54-
}
55-
56-
#[test]
57-
fn returns_first_index() {
58-
let index = ternary_search(&1, &[1, 2, 3], 0, 2);
59-
assert_eq!(index, Some(0));
87+
match (is_asc, cmp_first_mid, cmp_second_mid) {
88+
// If the item matches either midpoint, it returns the index
89+
(_, Ordering::Equal, _) => {
90+
*left = first_mid;
91+
return true;
92+
}
93+
(_, _, Ordering::Equal) => {
94+
*left = second_mid;
95+
return true;
96+
}
97+
// If the item is smaller than the element at first_mid (in ascending order)
98+
// or greater than it (in descending order), it narrows the search to the first third.
99+
(true, Ordering::Less, _) | (false, Ordering::Greater, _) => {
100+
*right = first_mid.saturating_sub(1)
101+
}
102+
// If the item is greater than the element at second_mid (in ascending order)
103+
// or smaller than it (in descending order), it narrows the search to the last third.
104+
(true, _, Ordering::Greater) | (false, _, Ordering::Less) => *left = second_mid + 1,
105+
// Otherwise, it searches the middle third.
106+
(_, _, _) => {
107+
*left = first_mid + 1;
108+
*right = second_mid - 1;
109+
}
60110
}
61111

62-
#[test]
63-
fn returns_first_index_if_end_out_of_bounds() {
64-
let index = ternary_search(&1, &[1, 2, 3], 0, 3);
65-
assert_eq!(index, Some(0));
66-
}
112+
false
113+
}
67114

68-
#[test]
69-
fn returns_last_index() {
70-
let index = ternary_search(&3, &[1, 2, 3], 0, 2);
71-
assert_eq!(index, Some(2));
72-
}
115+
/// Determines if the given array is sorted in ascending order.
116+
///
117+
/// This helper function checks if the first element of the array is less than the
118+
/// last element, indicating an ascending order. It returns `false` if the array
119+
/// has fewer than two elements.
120+
///
121+
/// # Parameters
122+
///
123+
/// - `arr`: A slice of the array to check.
124+
///
125+
/// # Returns
126+
///
127+
/// A `bool` indicating whether the array is sorted in ascending order.
128+
fn is_asc_arr<T: Ord>(arr: &[T]) -> bool {
129+
arr.len() > 1 && arr[0] < arr[arr.len() - 1]
130+
}
73131

74-
#[test]
75-
fn returns_last_index_if_end_out_of_bounds() {
76-
let index = ternary_search(&3, &[1, 2, 3], 0, 3);
77-
assert_eq!(index, Some(2));
78-
}
132+
#[cfg(test)]
133+
mod tests {
134+
use super::*;
79135

80-
#[test]
81-
fn returns_middle_index() {
82-
let index = ternary_search(&2, &[1, 2, 3], 0, 2);
83-
assert_eq!(index, Some(1));
136+
macro_rules! test_cases {
137+
($($name:ident: $test_case:expr,)*) => {
138+
$(
139+
#[test]
140+
fn $name() {
141+
let (item, arr, expected) = $test_case;
142+
if let Some(expected_index) = expected {
143+
assert_eq!(arr[expected_index], item);
144+
}
145+
assert_eq!(ternary_search(&item, arr), expected);
146+
}
147+
)*
148+
};
84149
}
85150

86-
#[test]
87-
fn returns_middle_index_if_end_out_of_bounds() {
88-
let index = ternary_search(&2, &[1, 2, 3], 0, 3);
89-
assert_eq!(index, Some(1));
151+
test_cases! {
152+
empty: ("a", &[] as &[&str], None),
153+
one_item_found: ("a", &["a"], Some(0)),
154+
one_item_not_found: ("b", &["a"], None),
155+
search_two_elements_found_at_start: (1, &[1, 2], Some(0)),
156+
search_two_elements_found_at_end: (2, &[1, 2], Some(1)),
157+
search_two_elements_not_found_start: (0, &[1, 2], None),
158+
search_two_elements_not_found_end: (3, &[1, 2], None),
159+
search_three_elements_found_start: (1, &[1, 2, 3], Some(0)),
160+
search_three_elements_found_middle: (2, &[1, 2, 3], Some(1)),
161+
search_three_elements_found_end: (3, &[1, 2, 3], Some(2)),
162+
search_three_elements_not_found_start: (0, &[1, 2, 3], None),
163+
search_three_elements_not_found_end: (4, &[1, 2, 3], None),
164+
search_strings_asc_start: ("a", &["a", "b", "c", "d", "google", "zoo"], Some(0)),
165+
search_strings_asc_middle: ("google", &["a", "b", "c", "d", "google", "zoo"], Some(4)),
166+
search_strings_asc_last: ("zoo", &["a", "b", "c", "d", "google", "zoo"], Some(5)),
167+
search_strings_asc_not_found: ("x", &["a", "b", "c", "d", "google", "zoo"], None),
168+
search_strings_desc_start: ("zoo", &["zoo", "google", "d", "c", "b", "a"], Some(0)),
169+
search_strings_desc_middle: ("google", &["zoo", "google", "d", "c", "b", "a"], Some(1)),
170+
search_strings_desc_last: ("a", &["zoo", "google", "d", "c", "b", "a"], Some(5)),
171+
search_strings_desc_not_found: ("x", &["zoo", "google", "d", "c", "b", "a"], None),
172+
search_ints_asc_start: (1, &[1, 2, 3, 4], Some(0)),
173+
search_ints_asc_middle: (3, &[1, 2, 3, 4], Some(2)),
174+
search_ints_asc_end: (4, &[1, 2, 3, 4], Some(3)),
175+
search_ints_asc_not_found: (5, &[1, 2, 3, 4], None),
176+
search_ints_desc_start: (4, &[4, 3, 2, 1], Some(0)),
177+
search_ints_desc_middle: (3, &[4, 3, 2, 1], Some(1)),
178+
search_ints_desc_end: (1, &[4, 3, 2, 1], Some(3)),
179+
search_ints_desc_not_found: (5, &[4, 3, 2, 1], None),
180+
with_gaps_0: (0, &[1, 3, 8, 11], None),
181+
with_gaps_1: (1, &[1, 3, 8, 11], Some(0)),
182+
with_gaps_2: (2, &[1, 3, 8, 11], None),
183+
with_gaps_3: (3, &[1, 3, 8, 11], Some(1)),
184+
with_gaps_4: (4, &[1, 3, 8, 10], None),
185+
with_gaps_5: (5, &[1, 3, 8, 10], None),
186+
with_gaps_6: (6, &[1, 3, 8, 10], None),
187+
with_gaps_7: (7, &[1, 3, 8, 11], None),
188+
with_gaps_8: (8, &[1, 3, 8, 11], Some(2)),
189+
with_gaps_9: (9, &[1, 3, 8, 11], None),
190+
with_gaps_10: (10, &[1, 3, 8, 11], None),
191+
with_gaps_11: (11, &[1, 3, 8, 11], Some(3)),
192+
with_gaps_12: (12, &[1, 3, 8, 11], None),
193+
with_gaps_13: (13, &[1, 3, 8, 11], None),
90194
}
91195
}

0 commit comments

Comments
 (0)