diff --git a/DIRECTORY.md b/DIRECTORY.md index d4d9bcb0..020746ad 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -466,6 +466,8 @@ * [Test Append Chars To Make Subsequence](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/two_pointers/append_chars_to_make_subsequence/test_append_chars_to_make_subsequence.py) * Array 3 Pointers * [Test Array 3 Pointers](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/two_pointers/array_3_pointers/test_array_3_pointers.py) + * Backspace String Compare + * [Test Backspace String Compare](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/two_pointers/backspace_string_compare/test_backspace_string_compare.py) * Container With Most Water * [Test Container With Most Water](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/two_pointers/container_with_most_water/test_container_with_most_water.py) * Count Pairs diff --git a/algorithms/two_pointers/backspace_string_compare/README.md b/algorithms/two_pointers/backspace_string_compare/README.md new file mode 100644 index 00000000..d98b97e2 --- /dev/null +++ b/algorithms/two_pointers/backspace_string_compare/README.md @@ -0,0 +1,95 @@ +# Backspace String Compare + +Given two strings s and t, return true if they are equal when both are typed into empty text editors. '#' means a +backspace character. + +Note that after backspacing an empty text, the text will continue empty. + +## Examples + +![Example 1](./images/examples/backspace_string_compare_example_1.png) +![Example 2](./images/examples/backspace_string_compare_example_2.png) +![Example 3](./images/examples/backspace_string_compare_example_3.png) +![Example 4](./images/examples/backspace_string_compare_example_4.png) + +Example 5: + +```text +Input: s = "ab#c", t = "ad#c" +Output: true +Explanation: Both s and t become "ac". +``` + +Example 6: + +```text +Input: s = "ab##", t = "c#d#" +Output: true +Explanation: Both s and t become "". +``` + +Example 7: +```text +Input: s = "a#c", t = "b" +Output: false +Explanation: s becomes "c" while t becomes "b". +``` + +## Constraints + +- 1 <= s.length, t.length <= 200 +- s and t only contain lowercase letters and '#' characters. + +> Follow up: Can you solve it in O(n) time and O(1) space? + +## Topics + +- Two Pointers +- String +- Stack +- Simulation + +## Solution(s) + +- [Two Pointers](#two-pointers) +- [Build String](#build-string) + +### Two Pointers + +When writing a character, it may or may not be part of the final string depending on how many backspace keystrokes occur +in the future. + +If instead we iterate through the string in reverse, then we will know how many backspace characters we have seen, and +therefore whether the result includes our character. + +The key insight is that backspace characters affect only the characters to their left, which means if we traverse both +strings from right to left, we can determine which characters are truly “visible” (i.e., not cancelled by a '#') and +compare them on the fly without ever building the final strings. We maintain two pointers, one for each string, and a +skip counter for each that tracks how many upcoming characters should be skipped due to pending backspaces. Whenever both +pointers land on a valid character simultaneously, we compare them directly and move on. + +#### Algorithm + +Iterate through the string in reverse. If we see a backspace character, the next non-backspace character is skipped. If +a character isn't skipped, it is part of the final answer. + +#### Complexity Analysis + +- **Time Complexity**: O(m + n), where m, n are the lengths of `s` and `t` respectively. Because each character in `s` + (of length `m`) and each character in `t` of length `n` is visited at most once by its respective pointer, making the + total work proportional to the combined length of both strings +- **Space Complexity**: O(1). This is because only a fixed number of variables(the pointers) are used regardless of the + input sizes, with no auxiliary data structures or string reconstruction required. + +### Build String + +Let's individually build the result of each string (build(S) and build(T)), then compare if they are equal. + +Algorithm + +To build the result of a string build(S), we'll use a stack based approach, simulating the result of each keystroke. + +#### Complexity Analysis + +- Time Complexity: O(M+N), where M,N are the lengths of S and T respectively. +- Space Complexity: O(M+N). diff --git a/algorithms/two_pointers/backspace_string_compare/__init__.py b/algorithms/two_pointers/backspace_string_compare/__init__.py new file mode 100644 index 00000000..95cb2344 --- /dev/null +++ b/algorithms/two_pointers/backspace_string_compare/__init__.py @@ -0,0 +1,82 @@ +from typing import Generator, Any +from itertools import zip_longest + + +def backspace_compare_two_pointers(s: str, t: str) -> bool: + def f(word: str) -> Generator[str, Any, None]: + skip = 0 + for x in reversed(word): + if x == "#": + skip += 1 + elif skip: + skip -= 1 + else: + yield x + + return all(x == y for x, y in zip_longest(f(s), f(t))) + + +def backspace_compare_two_pointers_2(s: str, t: str) -> bool: + # Initialize pointers at the end of each string + pointer_s, pointer_t = len(s) - 1, len(t) - 1 + # Track pending backspaces for each string + skip_s, skip_t = 0, 0 + + # Continue while there are characters left to process in either string + while pointer_s >= 0 or pointer_t >= 0: + # Advance pointer in s to find the next valid character + while pointer_s >= 0: + if s[pointer_s] == "#": + # Increment skip count and move pointer left + skip_s += 1 + pointer_s -= 1 + elif skip_s > 0: + # This character is cancelled by a backspace; consume skip and move left + skip_s -= 1 + pointer_s -= 1 + else: + # Found a valid character in s + break + + # Advance pointer in t to find the next valid character + while pointer_t >= 0: + if t[pointer_t] == "#": + # Increment skip count and move pointer left + skip_t += 1 + pointer_t -= 1 + elif skip_t > 0: + # This character is cancelled by a backspace; consume skip and move left + skip_t -= 1 + pointer_t -= 1 + else: + # Found a valid character in t + break + + # Compare the current valid characters from both strings + if pointer_s >= 0 and pointer_t >= 0: + # If characters differ, strings are not equal + if s[pointer_s] != t[pointer_t]: + return False + elif pointer_s >= 0 or pointer_t >= 0: + # One string has characters remaining while the other is exhausted + return False + + # Move both pointers left to continue comparison + pointer_s -= 1 + pointer_t -= 1 + + # All characters matched + return True + + +def backspace_compare_build_string(s: str, t: str) -> bool: + def build(word: str) -> str: + ans = [] + for c in word: + if c != "#": + ans.append(c) + elif ans: + ans.pop() + return "".join(ans) + + return build(s) == build(t) diff --git a/algorithms/two_pointers/backspace_string_compare/images/examples/backspace_string_compare_example_1.png b/algorithms/two_pointers/backspace_string_compare/images/examples/backspace_string_compare_example_1.png new file mode 100644 index 00000000..4e520442 Binary files /dev/null and b/algorithms/two_pointers/backspace_string_compare/images/examples/backspace_string_compare_example_1.png differ diff --git a/algorithms/two_pointers/backspace_string_compare/images/examples/backspace_string_compare_example_2.png b/algorithms/two_pointers/backspace_string_compare/images/examples/backspace_string_compare_example_2.png new file mode 100644 index 00000000..7aae9a23 Binary files /dev/null and b/algorithms/two_pointers/backspace_string_compare/images/examples/backspace_string_compare_example_2.png differ diff --git a/algorithms/two_pointers/backspace_string_compare/images/examples/backspace_string_compare_example_3.png b/algorithms/two_pointers/backspace_string_compare/images/examples/backspace_string_compare_example_3.png new file mode 100644 index 00000000..bb61e43a Binary files /dev/null and b/algorithms/two_pointers/backspace_string_compare/images/examples/backspace_string_compare_example_3.png differ diff --git a/algorithms/two_pointers/backspace_string_compare/images/examples/backspace_string_compare_example_4.png b/algorithms/two_pointers/backspace_string_compare/images/examples/backspace_string_compare_example_4.png new file mode 100644 index 00000000..75b855dd Binary files /dev/null and b/algorithms/two_pointers/backspace_string_compare/images/examples/backspace_string_compare_example_4.png differ diff --git a/algorithms/two_pointers/backspace_string_compare/test_backspace_string_compare.py b/algorithms/two_pointers/backspace_string_compare/test_backspace_string_compare.py new file mode 100644 index 00000000..4fa5db9e --- /dev/null +++ b/algorithms/two_pointers/backspace_string_compare/test_backspace_string_compare.py @@ -0,0 +1,54 @@ +import unittest +from parameterized import parameterized +from utils.test_utils import custom_test_name_func +from algorithms.two_pointers.backspace_string_compare import ( + backspace_compare_two_pointers, + backspace_compare_two_pointers_2, + backspace_compare_build_string, +) + +BACKSPACE_STRING_COMPARE_TEST_CASES = [ + ("x##y", "y", True), + ("abc###", "def###", True), + ("hello#world", "hellworld", True), + ("a##b##c", "c", True), + ("coding", "coding", True), + ("ab#c", "ac", True), + ("ab#c", "bc", False), + ("abc###", "xyz###", True), + ("#", "#", True), + ("ab#c", "ad#c", True), + ("ab##", "c#d#", True), + ("a#c", "b", False), +] + + +class BackspaceStringCompareTestCase(unittest.TestCase): + @parameterized.expand( + BACKSPACE_STRING_COMPARE_TEST_CASES, name_func=custom_test_name_func + ) + def test_backspace_string_compare_two_pointers( + self, s: str, t: str, expected: bool + ): + actual = backspace_compare_two_pointers(s, t) + self.assertEqual(expected, actual) + + @parameterized.expand( + BACKSPACE_STRING_COMPARE_TEST_CASES, name_func=custom_test_name_func + ) + def test_backspace_string_compare_two_pointers_2( + self, s: str, t: str, expected: bool + ): + actual = backspace_compare_two_pointers_2(s, t) + self.assertEqual(expected, actual) + + @parameterized.expand( + BACKSPACE_STRING_COMPARE_TEST_CASES, name_func=custom_test_name_func + ) + def test_backspace_compare_build_string(self, s: str, t: str, expected: bool): + actual = backspace_compare_build_string(s, t) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main()