diff --git a/DIRECTORY.md b/DIRECTORY.md index bb28da3a..84cf928e 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -371,6 +371,8 @@ * [Test Plates Between Candles](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/search/binary_search/plates_between_candles/test_plates_between_candles.py) * Rotated Sorted Array * [Test Search Rotated Sorted Array](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/search/binary_search/rotated_sorted_array/test_search_rotated_sorted_array.py) + * Search 2D Matrix + * [Test Search 2D Matrix](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/search/binary_search/search_2d_matrix/test_search_2d_matrix.py) * Search Range * [Test Search Range](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/search/binary_search/search_range/test_search_range.py) * Single Non Duplicate diff --git a/algorithms/search/binary_search/search_2d_matrix/README.md b/algorithms/search/binary_search/search_2d_matrix/README.md new file mode 100644 index 00000000..f7ec1f9d --- /dev/null +++ b/algorithms/search/binary_search/search_2d_matrix/README.md @@ -0,0 +1,14 @@ +# Search a 2D Matrix + +Given a two dimensional integer array matrix with m rows and n columns, determine whether the integer target exists in +matrix. Return true if target is present, otherwise return false. + +The array matrix is sorted such that each row is in nondecreasing order, and the first element of each row is greater +than the last element of the previous row. + +## Constraints + +- m == matrix.length +- n == matrix[i].length +- 1 <= m,n <= 100 +- -10^4 <= matrix[i][j], target <= 10^4 diff --git a/algorithms/search/binary_search/search_2d_matrix/__init__.py b/algorithms/search/binary_search/search_2d_matrix/__init__.py new file mode 100644 index 00000000..bef02886 --- /dev/null +++ b/algorithms/search/binary_search/search_2d_matrix/__init__.py @@ -0,0 +1,69 @@ +from typing import List + + +def search_matrix(matrix: List[List[int]], target: int) -> bool: + """ + Search a 2D matrix for a target value. If the target value is found, return True, otherwise return False. + Args: + matrix (List[List[int]]): 2D matrix to search in. + target (int): Target value to search for. + Returns: + bool: True if the target value is found in the matrix, False otherwise. + """ + if not matrix: + return False + if not matrix[0]: + return False + + n_rows = len(matrix) + n_cols = len(matrix[0]) + + left, right = 0, n_rows * n_cols - 1 + while left <= right: + mid = (left + right) // 2 + row, col = mid // n_cols, mid % n_cols + if matrix[row][col] == target: + return True + elif matrix[row][col] < target: + left = mid + 1 + else: + right = mid - 1 + return False + + +def search_matrix_2(matrix: List[List[int]], target: int) -> bool: + """ + Search a 2D matrix for a target value. If the target value is found, return True, otherwise return False. + Args: + matrix (List[List[int]]): 2D matrix to search in. + target (int): Target value to search for. + Returns: + bool: True if the target value is found in the matrix, False otherwise. + """ + # Get dimensions of the matrix + num_rows, num_cols = len(matrix), len(matrix[0]) + + # Initialize binary search boundaries + # Treat the 2D matrix as a flattened 1D array + left, right = 0, num_rows * num_cols - 1 + first_true_index = -1 + + # Binary search using the template: find first index where element >= target + while left <= right: + mid = (left + right) // 2 + + # Convert 1D index to 2D coordinates + row, col = divmod(mid, num_cols) + + # Feasible condition: matrix[row][col] >= target + if matrix[row][col] >= target: + first_true_index = mid + right = mid - 1 + else: + left = mid + 1 + + # Check if first_true_index points to the target + if first_true_index == -1: + return False + row, col = divmod(first_true_index, num_cols) + return matrix[row][col] == target diff --git a/algorithms/search/binary_search/search_2d_matrix/test_search_2d_matrix.py b/algorithms/search/binary_search/search_2d_matrix/test_search_2d_matrix.py new file mode 100644 index 00000000..d36b3012 --- /dev/null +++ b/algorithms/search/binary_search/search_2d_matrix/test_search_2d_matrix.py @@ -0,0 +1,39 @@ +import unittest +from typing import List +from parameterized import parameterized +from utils.test_utils import custom_test_name_func +from algorithms.search.binary_search.search_2d_matrix import ( + search_matrix, + search_matrix_2, +) + +SEARCH_2D_MATRIX_TEST_CASES = [ + ([[-8, -3, 1, 4], [7, 9, 13, 18], [21, 26, 31, 40]], 13, True), + ([[-6]], 5, False), + ([[-5, -2, 0], [3, 6, 10]], -9, False), + ([[-10, -4, 2, 9, 15, 22]], 9, True), + ([[-9], [-1], [5], [12], [20]], 6, False), + ([[0]], 0, True), + ([[1, 3, 5, 7], [10, 11, 16, 20], [23, 30, 34, 60]], 3, True), + ([[1, 3, 5, 7], [10, 11, 16, 20], [23, 30, 34, 60]], 13, False), +] + + +class Search2DMatrixTestCase(unittest.TestCase): + @parameterized.expand(SEARCH_2D_MATRIX_TEST_CASES, name_func=custom_test_name_func) + def test_search_2d_matrix( + self, matrix: List[List[int]], target: int, expected: bool + ): + actual = search_matrix(matrix, target) + self.assertEqual(expected, actual) + + @parameterized.expand(SEARCH_2D_MATRIX_TEST_CASES, name_func=custom_test_name_func) + def test_search_2d_matrix_2( + self, matrix: List[List[int]], target: int, expected: bool + ): + actual = search_matrix_2(matrix, target) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main() diff --git a/poetry.lock b/poetry.lock index dc0ed8b3..0c740274 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "black" @@ -6,6 +6,7 @@ version = "25.11.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "black-25.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec311e22458eec32a807f029b2646f661e6859c3f61bc6d9ffb67958779f392e"}, {file = "black-25.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1032639c90208c15711334d681de2e24821af0575573db2810b0763bcd62e0f0"}, @@ -55,6 +56,7 @@ version = "8.3.1" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" +groups = ["dev"] files = [ {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, @@ -69,10 +71,12 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev", "test"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {dev = "platform_system == \"Windows\"", test = "sys_platform == \"win32\""} [[package]] name = "iniconfig" @@ -80,6 +84,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["test"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -91,6 +96,7 @@ version = "1.1.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, @@ -102,6 +108,7 @@ version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["dev", "test"] files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, @@ -113,6 +120,7 @@ version = "0.9.0" description = "Parameterized testing with any Python test framework" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "parameterized-0.9.0-py2.py3-none-any.whl", hash = "sha256:4e0758e3d41bea3bbd05ec14fc2c24736723f243b28d702081aef438c9372b1b"}, {file = "parameterized-0.9.0.tar.gz", hash = "sha256:7fc905272cefa4f364c1a3429cbbe9c0f98b793988efb5bf90aac80f08db09b1"}, @@ -127,6 +135,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -138,6 +147,7 @@ version = "4.5.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.10" +groups = ["dev"] files = [ {file = "platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3"}, {file = "platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312"}, @@ -154,6 +164,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -169,6 +180,7 @@ version = "9.0.0" description = "Get CPU info with pure Python" optional = false python-versions = "*" +groups = ["test"] files = [ {file = "py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690"}, {file = "py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5"}, @@ -180,6 +192,7 @@ version = "8.3.5" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, @@ -200,6 +213,7 @@ version = "5.1.0" description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer." optional = false python-versions = ">=3.9" +groups = ["test"] files = [ {file = "pytest-benchmark-5.1.0.tar.gz", hash = "sha256:9ea661cdc292e8231f7cd4c10b0319e56a2118e2c09d9f50e1b3d150d2aca105"}, {file = "pytest_benchmark-5.1.0-py3-none-any.whl", hash = "sha256:922de2dfa3033c227c96da942d1878191afa135a29485fb942e85dff1c592c89"}, @@ -220,6 +234,7 @@ version = "0.3.0" description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3"}, {file = "pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a"}, @@ -234,6 +249,7 @@ version = "0.3.7" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0e8377cccb2f07abd25e84fc5b2cbe48eeb0fea9f1719cad7caedb061d70e5ce"}, {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:15a4d1cc1e64e556fa0d67bfd388fed416b7f3b26d5d1c3e7d192c897e39ba4b"}, @@ -255,6 +271,6 @@ files = [ ] [metadata] -lock-version = "2.0" -python-versions = "^3.11" -content-hash = "4484a420e200cd1d0c3cdcfe56e39afed7dcc2666889169211e9742516ef6c07" +lock-version = "2.1" +python-versions = "^3.14" +content-hash = "0a17bfa0d4108043aab772d4e4222cd1edac040e6c410a86fde12794a83a2acf"