Skip to content
282 changes: 226 additions & 56 deletions src/searching/saddleback_search.rs
Original file line number Diff line number Diff line change
@@ -1,85 +1,255 @@
// Saddleback search is a technique used to find an element in a sorted 2D matrix in O(m + n) time,
// where m is the number of rows, and n is the number of columns. It works by starting from the
// top-right corner of the matrix and moving left or down based on the comparison of the current
// element with the target element.
use std::cmp::Ordering;

pub fn saddleback_search(matrix: &[Vec<i32>], element: i32) -> (usize, usize) {
// Initialize left and right indices
/// Custom error type to represent errors related to matrix validation.
#[derive(Debug, PartialEq, Eq)]
pub enum MatrixError {
NonRectangularInput,
NotSorted,
}

/// Checks if the given matrix (vector of vectors) is sorted row-wise and column-wise.
///
/// # Arguments
///
/// * `matrix` - A vector of vectors representing the matrix to check.
///
/// # Returns
///
/// Returns `true` if the matrix is sorted both row-wise and column-wise. Otherwise, returns `false`.
fn is_sorted(matrix: &[Vec<isize>]) -> bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn is_sorted(matrix: &[Vec<isize>]) -> bool {
pub fn is_sorted(matrix: &[Vec<isize>]) -> bool {

Imagine that you have some matrix. You know that you will query the position of many elements and you want to check if the matrix is soared only once.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand this point. By the way, do you know how can we express the tests here? Suppose, we have two functions, one for validate matrix, one for saddleback search like this:

fn validate_matrix(matrix: &[Vec<isize>]) -> Result<(), MatrixError> {
    // Check if all rows have the same length (rectangular matrix).
    if matrix.iter().any(|row| row.len() != matrix[0].len()) {
        return Err(MatrixError::NonRectangularInput);
    }

    // Check if matrix is sorted.
    if !is_sorted(matrix) {
        return Err(MatrixError::NotSorted);
    }

    Ok(())
}

and

fn saddleback_search(matrix: &[Vec<isize>], element: isize) -> Option<(usize, usize)> {
    if matrix.is_empty() || matrix.iter().all(|row| row.is_empty()) {
        return None;
    }

    let mut left_index = 0;
    let mut right_index = matrix[0].len() - 1;

    while left_index < matrix.len() {
        match element.cmp(&matrix[left_index][right_index]) {
            Ordering::Equal => return Some((left_index, right_index)),
            Ordering::Greater => {
                left_index += 1;
            }
            Ordering::Less => {
                if right_index == 0 {
                    break;
                } else {
                    right_index -= 1;
                }
            }
        }
    }

    None
}

Can you propose how we can write tests, @vil02?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would create a function checking if the input is rectangular and simply use it in both is_sorted and saddleback_search.

Regarding testing strategy: it should be enough to check saddleback_search (as it is now) and is_sorted.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you suggest the detailed changes?

let rows = matrix.len();
let cols = matrix[0].len();
Comment on lines +20 to +21
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to check if the input is rectangular.

Copy link
Contributor Author

@sozelfist sozelfist Oct 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can move the logic that checks the matrix form and is_sorted into a single function, for example, validate_matrix

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above: I would introduce a new function checking if the input is rectangular and use it in both public functions.


// Check if rows are sorted.
for row in matrix {
if row.windows(2).any(|w| w[0] > w[1]) {
return false;
}
}

// Check if columns are sorted.
for col in 0..cols {
for row in 1..rows {
if matrix[row - 1][col] > matrix[row][col] {
return false;
}
}
}

true
}

/// Performs Saddleback search on a sorted matrix represented as a vector of vectors.
///
/// The Saddleback search algorithm finds the position of a target element in a matrix where
/// each row and each column is sorted in ascending order. The search starts from the top-right
/// corner of the matrix and moves left or down based on comparisons with the target element.
///
/// Optionally, the matrix can be validated for being sorted before the search starts.
///
/// # Arguments
///
/// * `matrix` - A vector of vectors representing the sorted matrix.
/// * `element` - The target element to search for.
/// * `check_sorted` - If true, verifies that the matrix is sorted before performing the search.
///
/// # Returns
///
/// Returns `Ok(Some((row, column)))` where both indices are 0-based if the element is found.
/// Returns `Ok(None)` if the element is not found.
/// Returns `Err(MatrixError)` if the matrix is not rectangular or not sorted.
pub fn saddleback_search(
matrix: &[Vec<isize>],
element: isize,
check_sorted: bool,
) -> Result<Option<(usize, usize)>, MatrixError> {
if matrix.is_empty() || matrix.iter().all(|row| row.is_empty()) {
return Ok(None);
}

if matrix.iter().any(|row| row.len() != matrix[0].len()) {
return Err(MatrixError::NonRectangularInput);
}

if check_sorted && !is_sorted(matrix) {
return Err(MatrixError::NotSorted);
}

let mut left_index = 0;
let mut right_index = matrix[0].len() - 1;

// Start searching
while left_index < matrix.len() {
match element.cmp(&matrix[left_index][right_index]) {
// If the current element matches the target element, return its position (indices are 1-based)
Ordering::Equal => return (left_index + 1, right_index + 1),
Ordering::Equal => return Ok(Some((left_index, right_index))),
Ordering::Greater => {
// If the target element is greater, move to the next row (downwards)
left_index += 1;
}
Ordering::Less => {
// If the target element is smaller, move to the previous column (leftwards)
if right_index == 0 {
break; // If we reach the left-most column, exit the loop
break;
} else {
right_index -= 1;
}
}
}
}

// If the element is not found, return (0, 0)
(0, 0)
Ok(None)
}

#[cfg(test)]
mod tests {
use super::*;

// Test when the element is not present in the matrix
#[test]
fn test_element_not_found() {
let matrix = vec![vec![1, 10, 100], vec![2, 20, 200], vec![3, 30, 300]];
assert_eq!(saddleback_search(&matrix, 123), (0, 0));
}

// Test when the element is at the top-left corner of the matrix
#[test]
fn test_element_at_top_left() {
let matrix = vec![vec![1, 10, 100], vec![2, 20, 200], vec![3, 30, 300]];
assert_eq!(saddleback_search(&matrix, 1), (1, 1));
}

// Test when the element is at the bottom-right corner of the matrix
#[test]
fn test_element_at_bottom_right() {
let matrix = vec![vec![1, 10, 100], vec![2, 20, 200], vec![3, 30, 300]];
assert_eq!(saddleback_search(&matrix, 300), (3, 3));
}

// Test when the element is at the top-right corner of the matrix
#[test]
fn test_element_at_top_right() {
let matrix = vec![vec![1, 10, 100], vec![2, 20, 200], vec![3, 30, 300]];
assert_eq!(saddleback_search(&matrix, 100), (1, 3));
}

// Test when the element is at the bottom-left corner of the matrix
#[test]
fn test_element_at_bottom_left() {
let matrix = vec![vec![1, 10, 100], vec![2, 20, 200], vec![3, 30, 300]];
assert_eq!(saddleback_search(&matrix, 3), (3, 1));
macro_rules! saddleback_tests {
($($name:ident: $tc:expr,)*) => {
$(
#[test]
fn $name() {
let (matrix, element, expected) = $tc;
assert_eq!(saddleback_search(&matrix, element, true), expected);
}
)*
};
}

// Additional test case: Element in the middle of the matrix
#[test]
fn test_element_in_middle() {
let matrix = vec![
vec![1, 10, 100, 1000],
vec![2, 20, 200, 2000],
vec![3, 30, 300, 3000],
];
assert_eq!(saddleback_search(&matrix, 200), (2, 3));
saddleback_tests! {
test_element_not_found: (
vec![
vec![1, 10, 100],
vec![2, 20, 200],
vec![3, 30, 300]
],
123,
Ok(None::<(usize, usize)>),
),
test_element_at_top_left: (
vec![
vec![1, 10, 100],
vec![2, 20, 200],
vec![3, 30, 300]
],
1,
Ok(Some((0, 0))),
),
test_element_at_bottom_right: (
vec![
vec![1, 10, 100],
vec![2, 20, 200],
vec![3, 30, 300]
],
300,
Ok(Some((2, 2))),
),
test_element_at_top_right: (
vec![
vec![1, 10, 100],
vec![2, 20, 200],
vec![3, 30, 300]
],
100,
Ok(Some((0, 2))),
),
test_element_at_bottom_left: (
vec![
vec![1, 10, 100],
vec![2, 20, 200],
vec![3, 30, 300]
],
3,
Ok(Some((2, 0))),
),
test_element_in_middle: (
vec![
vec![1, 10, 100, 1000],
vec![2, 20, 200, 2000],
vec![3, 30, 300, 3000],
],
200,
Ok(Some((1, 2))),
),
test_element_smaller_than_min: (
vec![
vec![1, 10, 100],
vec![2, 20, 200],
vec![3, 30, 300],
],
0,
Ok(None::<(usize, usize)>),
),
test_horizontal: (
vec![
vec![1, 10, 100],
],
100,
Ok(Some((0, 2))),
),
test_vertical: (
vec![
vec![1],
vec![2],
vec![3],
],
2,
Ok(Some((1, 0))),
),
test_single_element: (
vec![
vec![1],
],
1,
Ok(Some((0, 0))),
),
test_empty_matrix: (
vec![],
1,
Ok(None::<(usize, usize)>),
),
test_non_rectangular_matrix: (
vec![
vec![1, 10, 100],
vec![2, 20],
vec![3, 30, 300],
],
20,
Err::<Option<(usize, usize)>, MatrixError>(MatrixError::NonRectangularInput),
),
test_empty_row: (
vec![
vec![1, 2, 3],
vec![],
vec![4, 5, 6],
],
3,
Err::<Option<(usize, usize)>, MatrixError>(MatrixError::NonRectangularInput),
),
test_full_empty_rows: (
vec![
vec![],
vec![],
vec![],
vec![],
],
1,
Ok(None::<(usize, usize)>),
),
test_unsorted_matrix_row_wise: (
vec![
vec![1, 10, 100],
vec![20, 200, 2],
vec![3, 30, 300],
],
200,
Err::<Option<(usize, usize)>, MatrixError>(MatrixError::NotSorted),
),
test_unsorted_matrix_column_wise: (
vec![
vec![1, 10, 100],
vec![2, 20, 30],
vec![3, 15, 300],
],
200,
Err::<Option<(usize, usize)>, MatrixError>(MatrixError::NotSorted),
),
}
}