From bdaf9eff207444a3a5ccb5b4fe1f5cd7acdceac1 Mon Sep 17 00:00:00 2001 From: Truong Nhan Nguyen Date: Mon, 23 Sep 2024 14:07:47 +0700 Subject: [PATCH 1/3] ref: refactor ternary search --- src/searching/ternary_search.rs | 224 +++++++++++++++++++++++--------- 1 file changed, 159 insertions(+), 65 deletions(-) diff --git a/src/searching/ternary_search.rs b/src/searching/ternary_search.rs index 8cf975463fc..6e4478dc4bf 100644 --- a/src/searching/ternary_search.rs +++ b/src/searching/ternary_search.rs @@ -1,91 +1,185 @@ +//! This module provides an implementation of a ternary search algorithm that +//! works for both ascending and descending ordered arrays. The ternary search +//! function returns the index of the target element if it is found, or `None` +//! if the target is not present in the array. + use std::cmp::Ordering; -pub fn ternary_search( - target: &T, - list: &[T], - mut start: usize, - mut end: usize, -) -> Option { - if list.is_empty() { +/// Performs a ternary search for a specified item within a sorted array. +/// +/// This function can handle both ascending and descending ordered arrays. It +/// takes a reference to the item to search for and a slice of the array. If +/// the item is found, it returns the index of the item within the array. If +/// the item is not found, it returns `None`. +/// +/// # Parameters +/// +/// - `item`: A reference to the item to search for. +/// - `arr`: A slice of the sorted array in which to search. +/// +/// # Returns +/// +/// An `Option` which is: +/// - `Some(index)` if the item is found at the given index. +/// - `None` if the item is not found in the array. +pub fn ternary_search(item: &T, arr: &[T]) -> Option { + if arr.is_empty() { return None; } - while start <= end { - let mid1: usize = start + (end - start) / 3; - let mid2: usize = end - (end - start) / 3; + let is_asc = is_asc_arr(arr); + let mut left = 0; + let mut right = arr.len() - 1; - match target.cmp(&list[mid1]) { - Ordering::Less => end = mid1 - 1, - Ordering::Equal => return Some(mid1), - Ordering::Greater => match target.cmp(&list[mid2]) { - Ordering::Greater => start = mid2 + 1, - Ordering::Equal => return Some(mid2), - Ordering::Less => { - start = mid1 + 1; - end = mid2 - 1; - } - }, + while left <= right { + if match_compare(item, arr, &mut left, &mut right, is_asc) { + return Some(left); } } None } -#[cfg(test)] -mod tests { - use super::*; +/// Compares the item with two middle elements of the current search range and +/// updates the search bounds accordingly. This function handles both ascending +/// and descending ordered arrays. It calculates two middle indices of the +/// current search range and compares the item with the elements at these +/// indices. It then updates the search bounds (`left` and `right`) based on +/// the result of these comparisons. If the item is found, it returns `true`. +/// +/// # Parameters +/// +/// - `item`: A reference to the item to search for. +/// - `arr`: A slice of the array in which to search. +/// - `left`: A mutable reference to the left bound of the search range. +/// - `right`: A mutable reference to the right bound of the search range. +/// - `is_asc`: A boolean indicating whether the array is sorted in ascending order. +/// +/// # Returns +/// +/// A `bool` indicating: +/// - `true` if the item was found in the array. +/// - `false` if the item was not found in the array. +fn match_compare( + item: &T, + arr: &[T], + left: &mut usize, + right: &mut usize, + is_asc: bool, +) -> bool { + let first_mid = *left + (*right - *left) / 3; + let second_mid = *right - (*right - *left) / 3; - #[test] - fn returns_none_if_empty_list() { - let index = ternary_search(&"a", &[], 1, 10); - assert_eq!(index, None); - } + // Handling the edge case where the search narrows down to a single element + if first_mid == second_mid && first_mid == *left { + if arr[*left] != *item { + *left += 1; + return false; + } - #[test] - fn returns_none_if_range_is_invalid() { - let index = ternary_search(&1, &[1, 2, 3], 2, 1); - assert_eq!(index, None); + return true; } - #[test] - fn returns_index_if_list_has_one_item() { - let index = ternary_search(&1, &[1], 0, 1); - assert_eq!(index, Some(0)); - } + let cmp_first_mid = item.cmp(&arr[first_mid]); + let cmp_second_mid = item.cmp(&arr[second_mid]); - #[test] - fn returns_first_index() { - let index = ternary_search(&1, &[1, 2, 3], 0, 2); - assert_eq!(index, Some(0)); + match (is_asc, cmp_first_mid, cmp_second_mid) { + (_, Ordering::Equal, _) => { + *left = first_mid; + return true; + } + (_, _, Ordering::Equal) => { + *left = second_mid; + return true; + } + (true, Ordering::Less, _) | (false, Ordering::Greater, _) => { + *right = first_mid.saturating_sub(1) + } + (true, _, Ordering::Greater) | (false, _, Ordering::Less) => *left = second_mid + 1, + (_, _, _) => { + *left = first_mid + 1; + *right = second_mid - 1; + } } - #[test] - fn returns_first_index_if_end_out_of_bounds() { - let index = ternary_search(&1, &[1, 2, 3], 0, 3); - assert_eq!(index, Some(0)); - } + false +} - #[test] - fn returns_last_index() { - let index = ternary_search(&3, &[1, 2, 3], 0, 2); - assert_eq!(index, Some(2)); - } +/// Determines if the given array is sorted in ascending order. +/// +/// This helper function checks if the first element of the array is less than the +/// last element, indicating an ascending order. It returns `false` if the array +/// has fewer than two elements. +/// +/// # Parameters +/// +/// - `arr`: A slice of the array to check. +/// +/// # Returns +/// +/// A `bool` indicating whether the array is sorted in ascending order. +fn is_asc_arr(arr: &[T]) -> bool { + arr.len() > 1 && arr[0] < arr[arr.len() - 1] +} - #[test] - fn returns_last_index_if_end_out_of_bounds() { - let index = ternary_search(&3, &[1, 2, 3], 0, 3); - assert_eq!(index, Some(2)); - } +#[cfg(test)] +mod tests { + use super::*; - #[test] - fn returns_middle_index() { - let index = ternary_search(&2, &[1, 2, 3], 0, 2); - assert_eq!(index, Some(1)); + macro_rules! test_cases { + ($($name:ident: $test_case:expr,)*) => { + $( + #[test] + fn $name() { + let (item, arr, expected) = $test_case; + assert_eq!(ternary_search(&item, arr), expected); + } + )* + }; } - #[test] - fn returns_middle_index_if_end_out_of_bounds() { - let index = ternary_search(&2, &[1, 2, 3], 0, 3); - assert_eq!(index, Some(1)); + test_cases! { + empty: ("a", &[] as &[&str], None), + one_item_found: ("a", &["a"], Some(0)), + one_item_not_found: ("b", &["a"], None), + search_two_elements_found_at_start: (1, &[1, 2], Some(0)), + search_two_elements_found_at_end: (2, &[1, 2], Some(1)), + search_two_elements_not_found_start: (0, &[1, 2], None), + search_two_elements_not_found_end: (3, &[1, 2], None), + search_three_elements_found_start: (1, &[1, 2, 3], Some(0)), + search_three_elements_found_middle: (2, &[1, 2, 3], Some(1)), + search_three_elements_found_end: (3, &[1, 2, 3], Some(2)), + search_three_elements_not_found_start: (0, &[1, 2, 3], None), + search_three_elements_not_found_end: (4, &[1, 2, 3], None), + search_strings_asc_start: ("a", &["a", "b", "c", "d", "google", "zoo"], Some(0)), + search_strings_asc_middle: ("google", &["a", "b", "c", "d", "google", "zoo"], Some(4)), + search_strings_asc_last: ("zoo", &["a", "b", "c", "d", "google", "zoo"], Some(5)), + search_strings_asc_not_found: ("x", &["a", "b", "c", "d", "google", "zoo"], None), + search_strings_desc_start: ("zoo", &["zoo", "google", "d", "c", "b", "a"], Some(0)), + search_strings_desc_middle: ("google", &["zoo", "google", "d", "c", "b", "a"], Some(1)), + search_strings_desc_last: ("a", &["zoo", "google", "d", "c", "b", "a"], Some(5)), + search_strings_desc_not_found: ("x", &["zoo", "google", "d", "c", "b", "a"], None), + search_ints_asc_start: (1, &[1, 2, 3, 4], Some(0)), + search_ints_asc_middle: (3, &[1, 2, 3, 4], Some(2)), + search_ints_asc_end: (4, &[1, 2, 3, 4], Some(3)), + search_ints_asc_not_found: (5, &[1, 2, 3, 4], None), + search_ints_desc_start: (4, &[4, 3, 2, 1], Some(0)), + search_ints_desc_middle: (3, &[4, 3, 2, 1], Some(1)), + search_ints_desc_end: (1, &[4, 3, 2, 1], Some(3)), + search_ints_desc_not_found: (5, &[4, 3, 2, 1], None), + with_gaps_0: (0, &[1, 3, 8, 11], None), + with_gaps_1: (1, &[1, 3, 8, 11], Some(0)), + with_gaps_2: (2, &[1, 3, 8, 11], None), + with_gaps_3: (3, &[1, 3, 8, 11], Some(1)), + with_gaps_4: (4, &[1, 3, 8, 10], None), + with_gaps_5: (5, &[1, 3, 8, 10], None), + with_gaps_6: (6, &[1, 3, 8, 10], None), + with_gaps_7: (7, &[1, 3, 8, 11], None), + with_gaps_8: (8, &[1, 3, 8, 11], Some(2)), + with_gaps_9: (9, &[1, 3, 8, 11], None), + with_gaps_10: (10, &[1, 3, 8, 11], None), + with_gaps_11: (11, &[1, 3, 8, 11], Some(3)), + with_gaps_12: (12, &[1, 3, 8, 11], None), + with_gaps_13: (13, &[1, 3, 8, 11], None), } } From 507e514d5b6a1e2ac44f9aa553a34b6ba6cc4e39 Mon Sep 17 00:00:00 2001 From: Truong Nhan Nguyen Date: Sat, 12 Oct 2024 09:09:18 +0700 Subject: [PATCH 2/3] chore: replace if-else by match with guard --- src/searching/ternary_search.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/searching/ternary_search.rs b/src/searching/ternary_search.rs index 6e4478dc4bf..3bf4eb38ccc 100644 --- a/src/searching/ternary_search.rs +++ b/src/searching/ternary_search.rs @@ -72,18 +72,20 @@ fn match_compare( // Handling the edge case where the search narrows down to a single element if first_mid == second_mid && first_mid == *left { - if arr[*left] != *item { - *left += 1; - return false; - } - - return true; + return match &arr[*left] { + x if x == item => true, + _ => { + *left += 1; + false + } + }; } let cmp_first_mid = item.cmp(&arr[first_mid]); let cmp_second_mid = item.cmp(&arr[second_mid]); match (is_asc, cmp_first_mid, cmp_second_mid) { + // If the item matches either midpoint, it returns the index (_, Ordering::Equal, _) => { *left = first_mid; return true; @@ -92,10 +94,15 @@ fn match_compare( *left = second_mid; return true; } + // If the item is smaller than the element at first_mid (in ascending order) + // or greater than it (in descending order), it narrows the search to the first third. (true, Ordering::Less, _) | (false, Ordering::Greater, _) => { *right = first_mid.saturating_sub(1) } + // If the item is greater than the element at second_mid (in ascending order) + // or smaller than it (in descending order), it narrows the search to the last third. (true, _, Ordering::Greater) | (false, _, Ordering::Less) => *left = second_mid + 1, + // Otherwise, it searches the middle third. (_, _, _) => { *left = first_mid + 1; *right = second_mid - 1; From ec6bbdc4cd2943ca50d3de925535c8cc21c65ff9 Mon Sep 17 00:00:00 2001 From: Piotr Idzik <65706193+vil02@users.noreply.github.com> Date: Wed, 16 Oct 2024 19:52:28 +0200 Subject: [PATCH 3/3] test: verify test data --- src/searching/ternary_search.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/searching/ternary_search.rs b/src/searching/ternary_search.rs index 3bf4eb38ccc..cb9b5bee477 100644 --- a/src/searching/ternary_search.rs +++ b/src/searching/ternary_search.rs @@ -139,6 +139,9 @@ mod tests { #[test] fn $name() { let (item, arr, expected) = $test_case; + if let Some(expected_index) = expected { + assert_eq!(arr[expected_index], item); + } assert_eq!(ternary_search(&item, arr), expected); } )*