|
| 1 | +# Introduction |
| 2 | + |
| 3 | +The aim in this exercise is to determine whether opening and closing brackets are properly paired within the input text. |
| 4 | + |
| 5 | +These brackets may be nested deeply (think Lisp code) and/or dispersed among a lot of other text (think complex LaTeX documents). |
| 6 | + |
| 7 | +Community solutions fall into two main groups: |
| 8 | + |
| 9 | +1. Those which make a single pass or loop through the input string, maintaining necessary context for matching. |
| 10 | +2. Those which repeatedly make global substitutions within the text for context. |
| 11 | + |
| 12 | + |
| 13 | +## Single-pass approaches |
| 14 | + |
| 15 | +```python |
| 16 | +def is_paired(input_string): |
| 17 | + bracket_map = {"]" : "[", "}": "{", ")":"("} |
| 18 | + tracking = [] |
| 19 | + |
| 20 | + for element in input_string: |
| 21 | + if element in bracket_map.values(): |
| 22 | + tracking.append(element) |
| 23 | + if element in bracket_map: |
| 24 | + if not tracking or (tracking.pop() != bracket_map[element]): |
| 25 | + return False |
| 26 | + return not tracking |
| 27 | +``` |
| 28 | + |
| 29 | +The key in this approach is to maintain context by pushing open brackets onto some sort of stack (_in this case appending to a `list`_), then checking if there is a corresponding closing bracket to pair with the top stack item. |
| 30 | + |
| 31 | +See [stack-match][stack-match] approaches for details. |
| 32 | + |
| 33 | + |
| 34 | +## Repeated-substitution approaches |
| 35 | + |
| 36 | +```python |
| 37 | +def is_paired(text): |
| 38 | + text = "".join(item for item in text if item in "()[]{}") |
| 39 | + while "()" in text or "[]" in text or "{}" in text: |
| 40 | + text = text.replace("()","").replace("[]", "").replace("{}","") |
| 41 | + return not text |
| 42 | +``` |
| 43 | + |
| 44 | +In this approach, we first remove any non-bracket characters, then use a loop to repeatedly remove inner bracket pairs. |
| 45 | + |
| 46 | +See [repeated-substitution][repeated-substitution] approaches for details. |
| 47 | + |
| 48 | + |
| 49 | +## Other approaches |
| 50 | + |
| 51 | +Languages prizing immutibility are likely to use techniques such as `foldl()` or recursive matching, as discussed on the [Scala track][scala]. |
| 52 | + |
| 53 | +This is possible in Python, but can read as unidiomatic and will (likely) result in inefficient code if not done carefully. |
| 54 | + |
| 55 | +For anyone wanting to go down the functional-style path, Python has [`functools.reduce()`][reduce] for folds and added [structural pattern matching][pattern-matching] in Python 3.10. |
| 56 | + |
| 57 | +Recursion is not highly optimised in Python and there is no tail call optimization, but the default stack depth of 1000 should be more than enough for solving this problem recursively. |
| 58 | + |
| 59 | + |
| 60 | +## Which approach to use |
| 61 | + |
| 62 | +For short, well-defined input strings such as those currently in the test file, repeated-substitution allows a passing solution in very few lines of code. |
| 63 | +But as input grows, this method could become less and less performant, due to the multiple passes and changes needed to determine matches. |
| 64 | + |
| 65 | +The single-pass strategy of the stack-match approach allows for stream processing, scales linearly (_`O(n)` time complexity_) with text length, and will remain performant for very large inputs. |
| 66 | + |
| 67 | +Examining the community solutions published for this exercise, it is clear that many programmers prefer the stack-match method which avoids the repeated string copying of the substitution approach. |
| 68 | + |
| 69 | +Thus it is interesting and perhaps humbling to note that repeated-substitution is **_at least_** as fast in benchmarking, even with large (>30 kB) input strings! |
| 70 | + |
| 71 | +See the [performance article][article-performance] for more details. |
| 72 | + |
| 73 | +[article-performance]:https://exercism.org/tracks/python/exercises/matching-brackets/articles/performance |
| 74 | +[pattern-matching]: https://docs.python.org/3/whatsnew/3.10.html#pep-634-structural-pattern-matching |
| 75 | +[reduce]: https://docs.python.org/3/library/functools.html#functools.reduce |
| 76 | +[repeated-substitution]: https://exercism.org/tracks/python/exercises/matching-brackets/approaches/repeated-substitution |
| 77 | +[scala]: https://exercism.org/tracks/scala/exercises/matching-brackets/dig_deeper |
| 78 | +[stack-match]: https://exercism.org/tracks/python/exercises/matching-brackets/approaches/stack-match |
0 commit comments