From 769bbbb95980590e0722ed484903f7a0d3efa0e8 Mon Sep 17 00:00:00 2001 From: colinleach Date: Fri, 5 Apr 2024 11:24:45 -0700 Subject: [PATCH 1/5] [Matching Brackets] draft approaches --- .../matching-brackets/.approaches/config.json | 30 +++ .../.approaches/introduction.md | 73 +++++++ .../repeated-substitution/content.md | 53 +++++ .../repeated-substitution/snippet.txt | 5 + .../.approaches/stack-match/content.md | 44 +++++ .../.approaches/stack-match/snippet.txt | 8 + .../matching-brackets/.articles/config.json | 14 ++ .../.articles/performance/code/Benchmark.py | 184 ++++++++++++++++++ .../performance/code/run_times.feather | Bin 0 -> 3018 bytes .../.articles/performance/content.md | 41 ++++ .../.articles/performance/snippet.md | 3 + 11 files changed, 455 insertions(+) create mode 100644 exercises/practice/matching-brackets/.approaches/config.json create mode 100644 exercises/practice/matching-brackets/.approaches/introduction.md create mode 100644 exercises/practice/matching-brackets/.approaches/repeated-substitution/content.md create mode 100644 exercises/practice/matching-brackets/.approaches/repeated-substitution/snippet.txt create mode 100644 exercises/practice/matching-brackets/.approaches/stack-match/content.md create mode 100644 exercises/practice/matching-brackets/.approaches/stack-match/snippet.txt create mode 100644 exercises/practice/matching-brackets/.articles/config.json create mode 100644 exercises/practice/matching-brackets/.articles/performance/code/Benchmark.py create mode 100644 exercises/practice/matching-brackets/.articles/performance/code/run_times.feather create mode 100644 exercises/practice/matching-brackets/.articles/performance/content.md create mode 100644 exercises/practice/matching-brackets/.articles/performance/snippet.md diff --git a/exercises/practice/matching-brackets/.approaches/config.json b/exercises/practice/matching-brackets/.approaches/config.json new file mode 100644 index 0000000000..295cbca6e3 --- /dev/null +++ b/exercises/practice/matching-brackets/.approaches/config.json @@ -0,0 +1,30 @@ +{ + "introduction": { + "authors": [ + "colinleach", + "BethanyG" + ] + }, + "approaches": [ + { + "uuid": "449c828e-ce19-4930-83ab-071eb2821388", + "slug": "stack-match", + "title": "Stack Match", + "blurb": "Maintain context during stream processing by use of a stack.", + "authors": [ + "colinleach", + "BethanyG" + ] + }, + { + "uuid": "b4c42162-751b-42c8-9368-eed9c3f4e4c8", + "slug": "repeated-substitution", + "title": "Repeated Substitution", + "blurb": "Use substring replacement to iteratively simplify the string.", + "authors": [ + "colinleach", + "BethanyG" + ] + } + ] +} diff --git a/exercises/practice/matching-brackets/.approaches/introduction.md b/exercises/practice/matching-brackets/.approaches/introduction.md new file mode 100644 index 0000000000..83e2d22412 --- /dev/null +++ b/exercises/practice/matching-brackets/.approaches/introduction.md @@ -0,0 +1,73 @@ +# Introduction + +The aim in this exercise is to determine whether opening and closing brackets are properly paired. + +The brackets may be nested deeply (think Lisp code) and/or dispersed among a lot of other text (think complex LaTeX documents). + +Community solutions fall into two main groups: + +- Those which make a single pass through the input string, maintaining necessary context. +- Those which repeatedly make global substitutions within the text. + +## Single-pass approaches + +```python +def is_paired(input_string): + bracket_map = {"]" : "[", "}": "{", ")":"("} + tracking = [] + + for element in input_string: + if element in bracket_map.values(): + tracking.append(element) + if element in bracket_map: + if not tracking or (tracking.pop() != bracket_map[element]): + return False + return not tracking +``` + +The key in this approach is to maintain context by pushing open brackets onto some sort of stack, then checking if a closing bracket pairs with the top item on the stack. + +See [stack-match][stack-match] approaches for details. + +## Repeated-substitution approaches + +```python +def is_paired(text): + text = "".join([x for x in text if x in "()[]{}"]) + while "()" in text or "[]" in text or "{}" in text: + text = text.replace("()","").replace("[]", "").replace("{}","") + return not text +``` + +In this case, we first remove any non-bracket characters, then use a loop to repeatedly remove inner bracket pairs. + +See [repeated-substitution][repeated-substitution] approaches for details. + +## Other approaches + +Functional languages prizing immutibility are likely to use techniques such as `foldl()` or recursive matching, as discussed on the [Scala track][scala]. + +This is possible in a dynamic scripting language like Python, but certainly unidiomatic and probably inefficient. + +For anyone really wanting to go down that route, Python has [`functools.reduce()`][reduce] for folds and added [structural pattern matching][pattern-matching] in Python 3.10. + +Recursion is not highly optimised and there is no tail recursion, but the default stack depth of 1000 should be more than enough for this problem. + +## Which approach to use + +For short, well-defined input strings such as those in the tests, repeated-substitution allows a passing solution in very few lines. + +Stack-match is a single-pass approach which allows stream processing, scales linearly with text length and will remain performant for very large inputs. + +Examining the community solutions published for this exercise, it is clear that the highest-rep Python programmers generally prefer stack-match, avoiding lots of string copying. + +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! + +See the [performance article][article-performance] for more details. + +[stack-match]: https://exercism.org/tracks/python/exercises/matching-brackets/approaches/stack-match +[repeated-substitution]: https://exercism.org/tracks/python/exercises/matching-brackets/approaches/repeated-substitution +[article-performance]:https://exercism.org/tracks/python/exercises/matching-brackets/articles/performance +[scala]: https://exercism.org/tracks/scala/exercises/matching-brackets/dig_deeper +[reduce]: https://docs.python.org/3/library/functools.html#functools.reduce +[pattern-matching]: https://docs.python.org/3/whatsnew/3.10.html#pep-634-structural-pattern-matching diff --git a/exercises/practice/matching-brackets/.approaches/repeated-substitution/content.md b/exercises/practice/matching-brackets/.approaches/repeated-substitution/content.md new file mode 100644 index 0000000000..883d38936e --- /dev/null +++ b/exercises/practice/matching-brackets/.approaches/repeated-substitution/content.md @@ -0,0 +1,53 @@ +# Repeated Substitution + +```python +def is_paired(text): + text = "".join([x for x in text if x in "()[]{}"]) + while "()" in text or "[]" in text or "{}" in text: + text = text.replace("()","").replace("[]", "").replace("{}","") + return not text +``` + +In this approach, the steps are: + +- Remove all non-brackets characters. +- Iteratively remove all bracket pairs: this reduces nesting in the string from the inside outwards. +- Test for an empty string, meaning all brackets are paired. + +The code above spells out the approach particularly clearly, but there are (of course) several possible variants. + +```python +def is_paired(input_string): + symbols = "".join(char for char in input_string if char in "{}[]()") + while (pair := next((pair for pair in ("{}", "[]", "()") if pair in symbols), False)): + symbols = symbols.replace(pair, "") + return not symbols +``` + +The second solution above does essentially the same thing, but using a generator expression assigned with a [walrus operator][walrus] `:=` (introduced in Python 3.8). + +Regex enthusiasts can modify the approach further, using `re.sub()` instead of `string.replace()`: + +```python +import re + +def is_paired(str_: str) -> bool: + str_ = re.sub(r'[^{}\[\]()]', '', str_) + while str_ != (str_ := re.sub(r'{\}|\[]|\(\)', '', str_)): + pass + return not bool(str_) +``` + +It is even possible to combine regexes and recursion in the same solution, though not everyone would view this as idiomatic Python: + +```python +import re + +def is_paired(input_string): + replaced = re.sub(r"[^\[\(\{\}\)\]]|\{\}|\(\)|\[\]", "", input_string) + return not input_string if input_string == replaced else is_paired(replaced) +``` + +Note that both solutions using regular expressions ran slightly *slower* than `string.replace()` solutions in benchmarking, so adding this type of complexity brings no benefit in this problem. + +[walrus]: ***Can we merge the `walrus-operator` concept?*** \ No newline at end of file diff --git a/exercises/practice/matching-brackets/.approaches/repeated-substitution/snippet.txt b/exercises/practice/matching-brackets/.approaches/repeated-substitution/snippet.txt new file mode 100644 index 0000000000..d84715f9d6 --- /dev/null +++ b/exercises/practice/matching-brackets/.approaches/repeated-substitution/snippet.txt @@ -0,0 +1,5 @@ +def is_paired(text): + text = "".join([x for x in text if x in "()[]{}"]) + while "()" in text or "[]" in text or "{}" in text: + text = text.replace("()","").replace("[]", "").replace("{}","") + return not text \ No newline at end of file diff --git a/exercises/practice/matching-brackets/.approaches/stack-match/content.md b/exercises/practice/matching-brackets/.approaches/stack-match/content.md new file mode 100644 index 0000000000..d5faebe8ae --- /dev/null +++ b/exercises/practice/matching-brackets/.approaches/stack-match/content.md @@ -0,0 +1,44 @@ +# Stack Match + +```python +def is_paired(input_string): + bracket_map = {"]" : "[", "}": "{", ")":"("} + stack = [] + + for element in input_string: + if element in bracket_map.values(): + stack.append(element) + if element in bracket_map: + if not stack or (stack.pop() != bracket_map[element]): + return False + return not stack +``` + +The point of this approach is to maintain a context of which brackets are currently open: + +- If a left bracket is found, push it onto the stack. +- If a right bracket is found, and it pairs with the top item on the stack, pop the stack and contine. +- If there is a mismatch, for example `'['` with `'}'` or no left bracket on the stack, we can immediately terminate and return `False`. +- When all the input text is processed, determine if the stack is empty, meaning all left brackets were matched. + +In Python, a `list` is a good implementation of a stack: it has `append()` (equivalent to push) and `pop()` methods built in. + +Some solutions use `collections.deque()` as an alternative implementation, though this has no clear advantage and near-identical runtime performance. + +The code above searches `bracket_map` for left brackets, meaning the keys of the dictionary, and `bracket_map.values()` for the right brackets. + +Other solutions created two sets of left and right brackets explicitly, or just searched a string representation: + +```python + if element in ']})': +``` + +Such changes made little difference to code length or readability, but ran about 5-fold faster than the purely dictionary-based solution. + +At the end, success is an empty stack, tested above by using the False-like quality of `[]` (as Python programmers often do). + +To be more explicit, we could alternatively use an equality: + +```python + return stack == [] +``` diff --git a/exercises/practice/matching-brackets/.approaches/stack-match/snippet.txt b/exercises/practice/matching-brackets/.approaches/stack-match/snippet.txt new file mode 100644 index 0000000000..55efb4785e --- /dev/null +++ b/exercises/practice/matching-brackets/.approaches/stack-match/snippet.txt @@ -0,0 +1,8 @@ + bracket_map = {"]" : "[", "}": "{", ")":"("} + tracking = [] + for element in input_string: + if element in bracket_map.values(): tracking.append(element) + if element in bracket_map: + if not tracking or (tracking.pop() != bracket_map[element]): + return False + return not tracking \ No newline at end of file diff --git a/exercises/practice/matching-brackets/.articles/config.json b/exercises/practice/matching-brackets/.articles/config.json new file mode 100644 index 0000000000..0a5a8856a3 --- /dev/null +++ b/exercises/practice/matching-brackets/.articles/config.json @@ -0,0 +1,14 @@ +{ + "articles": [ + { + "uuid": "af7a43b5-c135-4809-9fb8-d84cdd5138d5", + "slug": "performance", + "title": "Performance", + "blurb": "Compare a variety of solutions using benchmarking data.", + "authors": [ + "colinleach", + "BethanyG" + ] + } + ] +} diff --git a/exercises/practice/matching-brackets/.articles/performance/code/Benchmark.py b/exercises/practice/matching-brackets/.articles/performance/code/Benchmark.py new file mode 100644 index 0000000000..b2bd9b28f1 --- /dev/null +++ b/exercises/practice/matching-brackets/.articles/performance/code/Benchmark.py @@ -0,0 +1,184 @@ +import timeit + +import pandas as pd +import numpy as np +import requests + + +# ------------ FUNCTIONS TO TIME ------------- # + +def stack_match1(input_string): + bracket_map = {"]" : "[", "}": "{", ")":"("} + tracking = [] + + for element in input_string: + if element in bracket_map.values(): + tracking.append(element) + if element in bracket_map: + if not tracking or (tracking.pop() != bracket_map[element]): + return False + return not tracking + + +def stack_match2(input_string): + opening = {'[', '{', '('} + closing = {']', '}', ')'} + pairs = {('[', ']'), ('{', '}'), ('(', ')')} + stack = list() + + for char in input_string: + if char in opening: + stack.append(char) + elif char in closing: + if not stack or (stack.pop(), char) not in pairs: + return False + return stack == [] + + + +def stack_match3(input_string): + BRACKETS = {'(': ')', '[': ']', '{': '}'} + END_BRACKETS = {')', ']', '}'} + + stack = [] + + def is_valid(char): + return stack and stack.pop() == char + + for char in input_string: + if char in BRACKETS: + stack.append(BRACKETS[char]) + elif char in END_BRACKETS and not is_valid(char): + return False + + return not stack + + +def stack_match4(input_string): + stack = [] + r = {')': '(', ']': '[', '}': '{'} + for c in input_string: + if c in '[{(': + stack.append(c) + if c in ']})': + if not stack: + return False + if stack[-1] == r[c]: + stack.pop() + else: + return False + return not stack + + +from collections import deque +from typing import Deque + + +def stack_match5(text: str) -> bool: + """ + Determine if the given text properly closes any opened brackets. + """ + PUSH = {"[": "]", "{": "}", "(": ")"} + PULL = set(PUSH.values()) + + stack: Deque[str] = deque() + for char in text: + if char in PUSH: + stack.append(PUSH[char]) + elif char in PULL: + if not stack or char != stack.pop(): + return False + return not stack + + +def repeated_substitution1(text): + text = "".join([x for x in text if x in "()[]{}"]) + while "()" in text or "[]" in text or "{}" in text: + text = text.replace("()","").replace("[]", "").replace("{}","") + return not text + + +def repeated_substitution2(input_string): + symbols = "".join(c for c in input_string if c in "{}[]()") + while (pair := next((pair for pair in ("{}", "[]", "()") if pair in symbols), False)): + symbols = symbols.replace(pair, "") + return not symbols + + +import re + +def repeated_substitution3(str_: str) -> bool: + str_ = re.sub(r'[^{}\[\]()]', '', str_) + while str_ != (str_ := re.sub(r'{\}|\[]|\(\)', '', str_)): + pass + return not bool(str_) + + +def repeated_substitution4(input_string): + replaced = re.sub(r"[^\[\(\{\}\)\]]|\{\}|\(\)|\[\]", "", input_string) + return not input_string if input_string == replaced else repeated_substitution4(replaced) + +## ---------END FUNCTIONS TO BE TIMED-------------------- ## + +## -------- Timing Code Starts Here ---------------------## + +def get_file(url): + resp = requests.get(url) + return resp.text + +short = "\\left(\\begin{array}{cc} \\frac{1}{3} & x\\\\ \\mathrm{e}^{x} &... x^2 \\end{array}\\right)" +mars_moons = get_file("https://raw.githubusercontent.com/colinleach/PTYS516/main/term_paper/term_paper.tex") +galaxy_cnn = get_file("https://raw.githubusercontent.com/colinleach/proj502/main/project_report/report.tex") + + +# Input Data Setup +inputs = [short, mars_moons, galaxy_cnn] + +# Ensure the code doesn't terminate early with a mismatch +assert all([stack_match1(txt) for txt in inputs]) + +# #Set up columns and rows for Pandas Data Frame +col_headers = ['short', 'mars_moons', 'galaxy_cnn'] +row_headers = [ + "stack_match1", + "stack_match2", + "stack_match3", + "stack_match4", + "stack_match5", + + "repeated_substitution1", + "repeated_substitution2", + "repeated_substitution3", + "repeated_substitution4" + ] + +# Empty dataframe will be filled in one cell at a time later +df = pd.DataFrame(np.nan, index=row_headers, columns=col_headers) + +# Function List to Call When Timing +functions = [stack_match1, stack_match2, stack_match3, stack_match4, stack_match5, + repeated_substitution1, repeated_substitution2, repeated_substitution3, repeated_substitution4] + +# Run timings using timeit.autorange(). Run Each Set 3 Times. +for function, title in zip(functions, row_headers): + timings = [[ + timeit.Timer(lambda: function(data), globals=globals()).autorange()[1] / + timeit.Timer(lambda: function(data), globals=globals()).autorange()[0] + for data in inputs] for rounds in range(3)] + + # Only the fastest Cycle counts. + timing_result = min(timings) + + print(f'{title}', f'Timings : {timing_result}') + # Insert results into the dataframe + df.loc[title, col_headers[0]:col_headers[-1]] = timing_result + +# Save the data to avoid constantly regenerating it +df.to_feather('run_times.feather') +print("\nDataframe saved to './run_times.feather'") + +# The next bit is useful for `introduction.md` +pd.options.display.float_format = '{:,.2e}'.format +print('\nDataframe in Markdown format:\n') +print(df.to_markdown(floatfmt=".2e")) + diff --git a/exercises/practice/matching-brackets/.articles/performance/code/run_times.feather b/exercises/practice/matching-brackets/.articles/performance/code/run_times.feather new file mode 100644 index 0000000000000000000000000000000000000000..72ef3124185e4bfb6b7e03ba43c83a667d6b1a6d GIT binary patch literal 3018 zcmeGeTWAzl^t#!^RhAIJrlBaVL%@g{vzu)iMCWdZ#we&Eq^Z!z5K9m;?I5IAh^is^s1($cWK2y6+dQ8Ol4Q~pxkpx{8c7lkZT1MJ zgvpY*V6ULEm<&tQq@sXCI4Ub4DW7KAl8jUw1%;#<4Qe4-0BJQDi>DSCT35FmG>{&X z4HjYs1Myx#RzW%qwwl!_37t-mhGx=T!iHN3i6?u+i$u?iYipGpO|6*Tq7&*@1 zCexe2Cu@U3jOhs}rfG0$_RZUP@7VBRrm&+aDX6N84d?UTt>Hq~Tp(EBFl=_;?$-H1 z^GnucW@tJO1SQ&~Gd+cI5Kl2(*N))cdt^Nk)l{TV?BrItM*p=g*3!I z=Ervc20#D+@d)G^WKA*VT9rkEA6W&kC>J>2$=6*q*A?NqHJlC~7?`8kdm!WzmvfNE zuaVCN6Cn>Q`7*K^a<_ym=h<9l<@~Zm_KLwCkA09udCYM_Ah}_Hs`+{-2&*}Ub)f?I zSV`PmD=Bhv-zTtWEXb?@TUa?v89XoCAL>|P$>0x90naa&mc!;#$s%;x7#TdnpRn;W zc!uxE@ef5aIJMSwTKq#6j=w^_*?oNz-fZb}3i~QL14DogjL=u-?tS}I+&_GO=asQ> zF%+2lcgAsBd{umlZGHH?IFL4;e{g3z?Q(qkv~&~GmP z_{WCnI{MYik*U9qe?sS~I({8}(M~sZoSj|Ue$YIb0{Xw)acc0t-ErFAJ??h=u#2{w z9`@DTYoc9c2Ukq~9iS&0ItPcQn&{~4ld&R~pI+bn=iOUZ{WKg7&(-(0(T=lsABm;y zrv9q@^8s`KDcF8004f1$0Ja0P0z?4%04@OVk(pTM+XTdi34;Z{kYdaTM!atFp%0)I z!0#sWu8(y&4pl)8Nr_}v!iXA40}6n*oH*9`;Ip>2oU|atZ|TSQ$0_Eh8zLAp#0?la zh!KL0;T|wnFoarg0wZ9>(}XSVFuRv}Zng<7^<0mTt@YTGY`rh%F_Hh`$ziF8P literal 0 HcmV?d00001 diff --git a/exercises/practice/matching-brackets/.articles/performance/content.md b/exercises/practice/matching-brackets/.articles/performance/content.md new file mode 100644 index 0000000000..191b41df0f --- /dev/null +++ b/exercises/practice/matching-brackets/.articles/performance/content.md @@ -0,0 +1,41 @@ +# Performance + +All functions were tested on three inputs, a short string from the stadard tests plus two scientific papers in $\LaTeX$ format. + +Python reported these string lengths: + +``` + short: 84 + mars_moons: 34836 + galaxy_cnn: 31468 +``` + +A total of 9 community solutions were tested: 5 variants of stack-match and 4 of repeated-substitution. + +Full details are in the [benchmark code][benchmark-code], including URLs for the downloaded papers. + +Results are summarized in the table below, with all times in seconds. + + +| | short | mars_moons | galaxy_cnn | +|:-----------------------|:--------:|:------------:|:------------:| +| stack_match1 | 5.64e-06 | 21.9e-04 | 39.7e-04 | +| stack_match2 | 1.71e-06 | 7.38e-04 | 6.64e-04 | +| stack_match3 | 1.79e-06 | 7.72e-04 | 6.95e-04 | +| stack_match4 | 1.77e-06 | 5.92e-04 | 5.18e-04 | +| stack_match5 | 1.70e-06 | 7.79e-04 | 6.97e-04 | +| repeated_substitution1 | 1.20e-06 | 3.50e-04 | 3.06e-04 | +| repeated_substitution2 | 1.86e-06 | 3.58e-04 | 3.15e-04 | +| repeated_substitution3 | 4.27e-06 | 14.0e-04 | 12.5e-04 | +| repeated_substitution4 | 4.96e-06 | 14.9e-04 | 13.5e-04 | + +Overall, most of these solutions had fairly similar performance, and runtime scaled similarly with input length. + +There is certainly no evidence for either class of solutions being systematically better than the other. + +The slowest was `stack_match1`, which did a lot of lookups in dictionary keys and values. Searching instead in sets or strings gave a small but perhaps useful improvement. + +Among the repeated-substitution codes, the first two used standard Python string operations, running slightly faster than the second two which use regular expressions. + + +[benchmark-code]: https://github.com/exercism/python/blob/main/exercises/practice/matching-brackets/.articles/performance/code/Benchmark.py diff --git a/exercises/practice/matching-brackets/.articles/performance/snippet.md b/exercises/practice/matching-brackets/.articles/performance/snippet.md new file mode 100644 index 0000000000..1479ad508e --- /dev/null +++ b/exercises/practice/matching-brackets/.articles/performance/snippet.md @@ -0,0 +1,3 @@ +# Performance + +Compare a variety of solutions using benchmarking data. From 7d1f5bd96da17441b75267288599230ce5b59674 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Mon, 29 Jul 2024 17:27:40 -0700 Subject: [PATCH 2/5] [Matching Brackets] Approaches Review & Edits --- .../.approaches/introduction.md | 41 +++++++++++-------- .../repeated-substitution/content.md | 31 ++++++++++---- .../repeated-substitution/snippet.txt | 2 +- .../.approaches/stack-match/content.md | 19 +++++---- .../.articles/performance/code/Benchmark.py | 2 +- .../.articles/performance/content.md | 8 ++-- 6 files changed, 61 insertions(+), 42 deletions(-) diff --git a/exercises/practice/matching-brackets/.approaches/introduction.md b/exercises/practice/matching-brackets/.approaches/introduction.md index 83e2d22412..744bb050f5 100644 --- a/exercises/practice/matching-brackets/.approaches/introduction.md +++ b/exercises/practice/matching-brackets/.approaches/introduction.md @@ -1,13 +1,14 @@ # Introduction -The aim in this exercise is to determine whether opening and closing brackets are properly paired. +The aim in this exercise is to determine whether opening and closing brackets are properly paired within the input text. -The brackets may be nested deeply (think Lisp code) and/or dispersed among a lot of other text (think complex LaTeX documents). +These brackets may be nested deeply (think Lisp code) and/or dispersed among a lot of other text (think complex LaTeX documents). Community solutions fall into two main groups: -- Those which make a single pass through the input string, maintaining necessary context. -- Those which repeatedly make global substitutions within the text. +1. Those which make a single pass or loop through the input string, maintaining necessary context for matching. +2. Those which repeatedly make global substitutions within the text for context. + ## Single-pass approaches @@ -25,49 +26,53 @@ def is_paired(input_string): return not tracking ``` -The key in this approach is to maintain context by pushing open brackets onto some sort of stack, then checking if a closing bracket pairs with the top item on the stack. +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. See [stack-match][stack-match] approaches for details. + ## Repeated-substitution approaches ```python def is_paired(text): - text = "".join([x for x in text if x in "()[]{}"]) + text = "".join(item for item in text if item in "()[]{}") while "()" in text or "[]" in text or "{}" in text: text = text.replace("()","").replace("[]", "").replace("{}","") return not text ``` -In this case, we first remove any non-bracket characters, then use a loop to repeatedly remove inner bracket pairs. +In this approach, we first remove any non-bracket characters, then use a loop to repeatedly remove inner bracket pairs. See [repeated-substitution][repeated-substitution] approaches for details. + ## Other approaches -Functional languages prizing immutibility are likely to use techniques such as `foldl()` or recursive matching, as discussed on the [Scala track][scala]. +Languages prizing immutibility are likely to use techniques such as `foldl()` or recursive matching, as discussed on the [Scala track][scala]. -This is possible in a dynamic scripting language like Python, but certainly unidiomatic and probably inefficient. +This is possible in Python, but can read as unidiomatic and will (likely) result in inefficient code if not done carefully. -For anyone really wanting to go down that route, Python has [`functools.reduce()`][reduce] for folds and added [structural pattern matching][pattern-matching] in Python 3.10. +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. + +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. -Recursion is not highly optimised and there is no tail recursion, but the default stack depth of 1000 should be more than enough for this problem. ## Which approach to use -For short, well-defined input strings such as those in the tests, repeated-substitution allows a passing solution in very few lines. +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. +But as input grows, this method becomes could become less and less performant, due to the multiple passes and changes needed to determine matches. -Stack-match is a single-pass approach which allows stream processing, scales linearly with text length and will remain performant for very large inputs. +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. -Examining the community solutions published for this exercise, it is clear that the highest-rep Python programmers generally prefer stack-match, avoiding lots of string copying. +Examining the community solutions published for this exercise, it is clear that many programmers prefer the stack-match method, avoiding the repeated string copying of the substitution approach. 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! See the [performance article][article-performance] for more details. -[stack-match]: https://exercism.org/tracks/python/exercises/matching-brackets/approaches/stack-match -[repeated-substitution]: https://exercism.org/tracks/python/exercises/matching-brackets/approaches/repeated-substitution [article-performance]:https://exercism.org/tracks/python/exercises/matching-brackets/articles/performance -[scala]: https://exercism.org/tracks/scala/exercises/matching-brackets/dig_deeper -[reduce]: https://docs.python.org/3/library/functools.html#functools.reduce [pattern-matching]: https://docs.python.org/3/whatsnew/3.10.html#pep-634-structural-pattern-matching +[reduce]: https://docs.python.org/3/library/functools.html#functools.reduce +[repeated-substitution]: https://exercism.org/tracks/python/exercises/matching-brackets/approaches/repeated-substitution +[scala]: https://exercism.org/tracks/scala/exercises/matching-brackets/dig_deeper +[stack-match]: https://exercism.org/tracks/python/exercises/matching-brackets/approaches/stack-match diff --git a/exercises/practice/matching-brackets/.approaches/repeated-substitution/content.md b/exercises/practice/matching-brackets/.approaches/repeated-substitution/content.md index 883d38936e..301d734320 100644 --- a/exercises/practice/matching-brackets/.approaches/repeated-substitution/content.md +++ b/exercises/practice/matching-brackets/.approaches/repeated-substitution/content.md @@ -1,8 +1,9 @@ # Repeated Substitution + ```python def is_paired(text): - text = "".join([x for x in text if x in "()[]{}"]) + text = "".join([element for element in text if element in "()[]{}"]) while "()" in text or "[]" in text or "{}" in text: text = text.replace("()","").replace("[]", "").replace("{}","") return not text @@ -10,12 +11,17 @@ def is_paired(text): In this approach, the steps are: -- Remove all non-brackets characters. -- Iteratively remove all bracket pairs: this reduces nesting in the string from the inside outwards. -- Test for an empty string, meaning all brackets are paired. +1. Remove all non-bracket characters from the input string. +2. Iteratively remove all remaining bracket pairs: this reduces nesting in the string from the inside outwards. +3. Test for a now empty string, meaning all brackets have been paired. + The code above spells out the approach particularly clearly, but there are (of course) several possible variants. + +## Variation 1: Walrus Operator within a Generator Expression + + ```python def is_paired(input_string): symbols = "".join(char for char in input_string if char in "{}[]()") @@ -24,9 +30,12 @@ def is_paired(input_string): return not symbols ``` -The second solution above does essentially the same thing, but using a generator expression assigned with a [walrus operator][walrus] `:=` (introduced in Python 3.8). +The second solution above does essentially the same thing as the initial approach, but uses a generator expression assigned with a [walrus operator][walrus] `:=` (_introduced in Python 3.8_). -Regex enthusiasts can modify the approach further, using `re.sub()` instead of `string.replace()`: + +## Variation 2: Regex + +Regex enthusiasts can modify the previous approach, using `re.sub()` instead of `string.replace()`: ```python import re @@ -38,7 +47,11 @@ def is_paired(str_: str) -> bool: return not bool(str_) ``` -It is even possible to combine regexes and recursion in the same solution, though not everyone would view this as idiomatic Python: +## Variation 3: Regex and Recursion + + +It is possible to combine regexes & recursion in the same solution, though not everyone would view this as idiomatic Python: + ```python import re @@ -48,6 +61,6 @@ def is_paired(input_string): return not input_string if input_string == replaced else is_paired(replaced) ``` -Note that both solutions using regular expressions ran slightly *slower* than `string.replace()` solutions in benchmarking, so adding this type of complexity brings no benefit in this problem. +Note that solutions using regular expressions ran slightly *slower* than `string.replace()` solutions in benchmarking, so adding this type of complexity brings no benefit to this problem. -[walrus]: ***Can we merge the `walrus-operator` concept?*** \ No newline at end of file +[walrus]: https://martinheinz.dev/blog/79/ diff --git a/exercises/practice/matching-brackets/.approaches/repeated-substitution/snippet.txt b/exercises/practice/matching-brackets/.approaches/repeated-substitution/snippet.txt index d84715f9d6..0699d54575 100644 --- a/exercises/practice/matching-brackets/.approaches/repeated-substitution/snippet.txt +++ b/exercises/practice/matching-brackets/.approaches/repeated-substitution/snippet.txt @@ -1,5 +1,5 @@ def is_paired(text): - text = "".join([x for x in text if x in "()[]{}"]) + text = "".join([element for element in text if element in "()[]{}"]) while "()" in text or "[]" in text or "{}" in text: text = text.replace("()","").replace("[]", "").replace("{}","") return not text \ No newline at end of file diff --git a/exercises/practice/matching-brackets/.approaches/stack-match/content.md b/exercises/practice/matching-brackets/.approaches/stack-match/content.md index d5faebe8ae..6ceaf107e8 100644 --- a/exercises/practice/matching-brackets/.approaches/stack-match/content.md +++ b/exercises/practice/matching-brackets/.approaches/stack-match/content.md @@ -1,5 +1,6 @@ # Stack Match + ```python def is_paired(input_string): bracket_map = {"]" : "[", "}": "{", ")":"("} @@ -14,28 +15,28 @@ def is_paired(input_string): return not stack ``` -The point of this approach is to maintain a context of which brackets are currently open: +The point of this approach is to maintain a context of which bracket sets are currently open: - If a left bracket is found, push it onto the stack. -- If a right bracket is found, and it pairs with the top item on the stack, pop the stack and contine. -- If there is a mismatch, for example `'['` with `'}'` or no left bracket on the stack, we can immediately terminate and return `False`. +- If a right bracket is found, and it pairs with the top item on the stack, pop the bracket off the stack and continue. +- If there is a mismatch, for example `'['` with `'}'` or no left bracket on the stack, the code can immediately terminate and return `False`. - When all the input text is processed, determine if the stack is empty, meaning all left brackets were matched. -In Python, a `list` is a good implementation of a stack: it has `append()` (equivalent to push) and `pop()` methods built in. +In Python, a `list` is a good implementation of a stack: it has `list.append()` (equivalent to a "push") and `lsit.pop()` methods built in. -Some solutions use `collections.deque()` as an alternative implementation, though this has no clear advantage and near-identical runtime performance. +Some solutions use `collections.deque()` as an alternative implementation, though this has no clear advantage (_since the code only uses appends to the right-hand side_) and near-identical runtime performance. -The code above searches `bracket_map` for left brackets, meaning the keys of the dictionary, and `bracket_map.values()` for the right brackets. +The code above searches `bracket_map` for left brackets, meaning the _keys_ of the dictionary, and the line `bracket_map.values()` is used to search for the right brackets. -Other solutions created two sets of left and right brackets explicitly, or just searched a string representation: +Other solutions created two sets of left and right brackets explicitly, or searched a string representation: ```python if element in ']})': ``` -Such changes made little difference to code length or readability, but ran about 5-fold faster than the purely dictionary-based solution. +Such changes made little difference to code length or readability, but ran about 5-fold faster than the dictionary-based solution. -At the end, success is an empty stack, tested above by using the False-like quality of `[]` (as Python programmers often do). +At the end, success is an empty stack, tested above by using the False-y quality of `[]` (_as Python programmers often do_). To be more explicit, we could alternatively use an equality: diff --git a/exercises/practice/matching-brackets/.articles/performance/code/Benchmark.py b/exercises/practice/matching-brackets/.articles/performance/code/Benchmark.py index b2bd9b28f1..1ca6ff0025 100644 --- a/exercises/practice/matching-brackets/.articles/performance/code/Benchmark.py +++ b/exercises/practice/matching-brackets/.articles/performance/code/Benchmark.py @@ -92,7 +92,7 @@ def stack_match5(text: str) -> bool: def repeated_substitution1(text): - text = "".join([x for x in text if x in "()[]{}"]) + text = "".join(x for x in text if x in "()[]{}") while "()" in text or "[]" in text or "{}" in text: text = text.replace("()","").replace("[]", "").replace("{}","") return not text diff --git a/exercises/practice/matching-brackets/.articles/performance/content.md b/exercises/practice/matching-brackets/.articles/performance/content.md index 191b41df0f..4df3081ee6 100644 --- a/exercises/practice/matching-brackets/.articles/performance/content.md +++ b/exercises/practice/matching-brackets/.articles/performance/content.md @@ -1,6 +1,6 @@ # Performance -All functions were tested on three inputs, a short string from the stadard tests plus two scientific papers in $\LaTeX$ format. +All functions were tested on three inputs, a short string from the exercise tests plus two scientific papers in $\LaTeX$ format. Python reported these string lengths: @@ -11,10 +11,8 @@ Python reported these string lengths: ``` A total of 9 community solutions were tested: 5 variants of stack-match and 4 of repeated-substitution. - Full details are in the [benchmark code][benchmark-code], including URLs for the downloaded papers. - -Results are summarized in the table below, with all times in seconds. +Results are summarized in the table below, with all times in seconds: | | short | mars_moons | galaxy_cnn | @@ -29,6 +27,8 @@ Results are summarized in the table below, with all times in seconds. | repeated_substitution3 | 4.27e-06 | 14.0e-04 | 12.5e-04 | | repeated_substitution4 | 4.96e-06 | 14.9e-04 | 13.5e-04 | + + Overall, most of these solutions had fairly similar performance, and runtime scaled similarly with input length. There is certainly no evidence for either class of solutions being systematically better than the other. From d42bbf5e4ab3ed8d7c316199009888755e0401de Mon Sep 17 00:00:00 2001 From: BethanyG Date: Wed, 31 Jul 2024 17:20:46 -0700 Subject: [PATCH 3/5] Additional grammar and spelling edits --- .../.approaches/introduction.md | 6 ++--- .../repeated-substitution/content.md | 23 ++++++++++--------- .../repeated-substitution/snippet.txt | 2 +- .../.approaches/stack-match/content.md | 18 +++++++++------ 4 files changed, 27 insertions(+), 22 deletions(-) diff --git a/exercises/practice/matching-brackets/.approaches/introduction.md b/exercises/practice/matching-brackets/.approaches/introduction.md index 744bb050f5..0096dac45c 100644 --- a/exercises/practice/matching-brackets/.approaches/introduction.md +++ b/exercises/practice/matching-brackets/.approaches/introduction.md @@ -60,13 +60,13 @@ Recursion is not highly optimised in Python and there is no tail call optimizati ## Which approach to use 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. -But as input grows, this method becomes could become less and less performant, due to the multiple passes and changes needed to determine matches. +But as input grows, this method could become less and less performant, due to the multiple passes and changes needed to determine matches. 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. -Examining the community solutions published for this exercise, it is clear that many programmers prefer the stack-match method, avoiding the repeated string copying of the substitution approach. +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. -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! +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! See the [performance article][article-performance] for more details. diff --git a/exercises/practice/matching-brackets/.approaches/repeated-substitution/content.md b/exercises/practice/matching-brackets/.approaches/repeated-substitution/content.md index 301d734320..2c8c17d637 100644 --- a/exercises/practice/matching-brackets/.approaches/repeated-substitution/content.md +++ b/exercises/practice/matching-brackets/.approaches/repeated-substitution/content.md @@ -11,7 +11,7 @@ def is_paired(text): In this approach, the steps are: -1. Remove all non-bracket characters from the input string. +1. Remove all non-bracket characters from the input string (_as done through the filter clause in the list-comprehension above_). 2. Iteratively remove all remaining bracket pairs: this reduces nesting in the string from the inside outwards. 3. Test for a now empty string, meaning all brackets have been paired. @@ -30,27 +30,28 @@ def is_paired(input_string): return not symbols ``` -The second solution above does essentially the same thing as the initial approach, but uses a generator expression assigned with a [walrus operator][walrus] `:=` (_introduced in Python 3.8_). +The second solution above does essentially the same thing as the initial approach, but uses a generator expression assigned with a [walrus operator][walrus] `:=` (_introduced in Python 3.8_) in the `while-loop` test. -## Variation 2: Regex +## Variation 2: Regex Substitution in a While Loop -Regex enthusiasts can modify the previous approach, using `re.sub()` instead of `string.replace()`: +Regex enthusiasts can modify the previous approach, using `re.sub()` instead of `string.replace()` in the `while-loop` test: ```python import re -def is_paired(str_: str) -> bool: - str_ = re.sub(r'[^{}\[\]()]', '', str_) - while str_ != (str_ := re.sub(r'{\}|\[]|\(\)', '', str_)): - pass - return not bool(str_) +def is_paired(text: str) -> bool: + text = re.sub(r'[^{}\[\]()]', '', text) + while text != (text := re.sub(r'{\}|\[]|\(\)', '', text)): + continue + return not bool(text) ``` -## Variation 3: Regex and Recursion +## Variation 3: Regex Substitution and Recursion -It is possible to combine regexes & recursion in the same solution, though not everyone would view this as idiomatic Python: + +It is possible to combine `re.sub()` and recursion in the same solution, though not everyone would view this as idiomatic Python: ```python diff --git a/exercises/practice/matching-brackets/.approaches/repeated-substitution/snippet.txt b/exercises/practice/matching-brackets/.approaches/repeated-substitution/snippet.txt index 0699d54575..0fa6d54abd 100644 --- a/exercises/practice/matching-brackets/.approaches/repeated-substitution/snippet.txt +++ b/exercises/practice/matching-brackets/.approaches/repeated-substitution/snippet.txt @@ -1,5 +1,5 @@ def is_paired(text): - text = "".join([element for element in text if element in "()[]{}"]) + text = "".join(element for element in text if element in "()[]{}") while "()" in text or "[]" in text or "{}" in text: text = text.replace("()","").replace("[]", "").replace("{}","") return not text \ No newline at end of file diff --git a/exercises/practice/matching-brackets/.approaches/stack-match/content.md b/exercises/practice/matching-brackets/.approaches/stack-match/content.md index 6ceaf107e8..3ca53baf0d 100644 --- a/exercises/practice/matching-brackets/.approaches/stack-match/content.md +++ b/exercises/practice/matching-brackets/.approaches/stack-match/content.md @@ -15,18 +15,18 @@ def is_paired(input_string): return not stack ``` -The point of this approach is to maintain a context of which bracket sets are currently open: +The point of this approach is to maintain a context of which bracket sets are currently "open": -- If a left bracket is found, push it onto the stack. -- If a right bracket is found, and it pairs with the top item on the stack, pop the bracket off the stack and continue. -- If there is a mismatch, for example `'['` with `'}'` or no left bracket on the stack, the code can immediately terminate and return `False`. +- If a left bracket is found, push it onto the stack (_append it to the `list`_). +- If a right bracket is found, **and** it pairs with the last item placed on the stack, pop the bracket off the stack and continue. +- If there is a mismatch, for example `'['` with `'}'` or there is no left bracket on the stack, the code can immediately terminate and return `False`. - When all the input text is processed, determine if the stack is empty, meaning all left brackets were matched. -In Python, a `list` is a good implementation of a stack: it has `list.append()` (equivalent to a "push") and `lsit.pop()` methods built in. +In Python, a [`list`][concept:python/lists]() is a good implementation of a stack: it has [`list.append()`][list-append] (_equivalent to a "push"_) and [`lsit.pop()`][list-pop] methods built in. -Some solutions use `collections.deque()` as an alternative implementation, though this has no clear advantage (_since the code only uses appends to the right-hand side_) and near-identical runtime performance. +Some solutions use [`collections.deque()`][collections-deque] as an alternative implementation, though this has no clear advantage (_since the code only uses appends to the right-hand side_) and near-identical runtime performance. -The code above searches `bracket_map` for left brackets, meaning the _keys_ of the dictionary, and the line `bracket_map.values()` is used to search for the right brackets. +The default iteration for a dictionary is over the _keys_, so the code above uses a plain `bracket_map` to search for left brackets, while `bracket_map.values()` is used to search for right brackets. Other solutions created two sets of left and right brackets explicitly, or searched a string representation: @@ -43,3 +43,7 @@ To be more explicit, we could alternatively use an equality: ```python return stack == [] ``` + +[list-append]: https://docs.python.org/3/tutorial/datastructures.html#more-on-lists +[list-pop]: https://docs.python.org/3/tutorial/datastructures.html#more-on-lists +[collections-deque]: https://docs.python.org/3/library/collections.html#collections.deque From 77fff3865f115ef5ebc1d55d1e831aecc1272f6d Mon Sep 17 00:00:00 2001 From: BethanyG Date: Wed, 31 Jul 2024 18:23:50 -0700 Subject: [PATCH 4/5] Final Edits Hopefully, the final edits. :smile: --- .../.approaches/stack-match/content.md | 3 ++- .../.approaches/stack-match/snippet.txt | 6 +++--- .../matching-brackets/.articles/performance/content.md | 10 +++++----- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/exercises/practice/matching-brackets/.approaches/stack-match/content.md b/exercises/practice/matching-brackets/.approaches/stack-match/content.md index 3ca53baf0d..b07a798833 100644 --- a/exercises/practice/matching-brackets/.approaches/stack-match/content.md +++ b/exercises/practice/matching-brackets/.approaches/stack-match/content.md @@ -36,7 +36,7 @@ Other solutions created two sets of left and right brackets explicitly, or searc Such changes made little difference to code length or readability, but ran about 5-fold faster than the dictionary-based solution. -At the end, success is an empty stack, tested above by using the False-y quality of `[]` (_as Python programmers often do_). +At the end, success is an empty stack, tested above by using the [False-y quality][falsey] of `[]` (_as Python programmers often do_). To be more explicit, we could alternatively use an equality: @@ -47,3 +47,4 @@ To be more explicit, we could alternatively use an equality: [list-append]: https://docs.python.org/3/tutorial/datastructures.html#more-on-lists [list-pop]: https://docs.python.org/3/tutorial/datastructures.html#more-on-lists [collections-deque]: https://docs.python.org/3/library/collections.html#collections.deque +[falsey]: https://docs.python.org/3/library/stdtypes.html#truth-value-testing diff --git a/exercises/practice/matching-brackets/.approaches/stack-match/snippet.txt b/exercises/practice/matching-brackets/.approaches/stack-match/snippet.txt index 55efb4785e..571b6792a6 100644 --- a/exercises/practice/matching-brackets/.approaches/stack-match/snippet.txt +++ b/exercises/practice/matching-brackets/.approaches/stack-match/snippet.txt @@ -1,8 +1,8 @@ bracket_map = {"]" : "[", "}": "{", ")":"("} - tracking = [] + stack = [] for element in input_string: if element in bracket_map.values(): tracking.append(element) if element in bracket_map: - if not tracking or (tracking.pop() != bracket_map[element]): + if not stack or (stack.pop() != bracket_map[element]): return False - return not tracking \ No newline at end of file + return not stack \ No newline at end of file diff --git a/exercises/practice/matching-brackets/.articles/performance/content.md b/exercises/practice/matching-brackets/.articles/performance/content.md index 4df3081ee6..0d34786e73 100644 --- a/exercises/practice/matching-brackets/.articles/performance/content.md +++ b/exercises/practice/matching-brackets/.articles/performance/content.md @@ -17,25 +17,25 @@ Results are summarized in the table below, with all times in seconds: | | short | mars_moons | galaxy_cnn | |:-----------------------|:--------:|:------------:|:------------:| -| stack_match1 | 5.64e-06 | 21.9e-04 | 39.7e-04 | +| stack_match4 | 1.77e-06 | 5.92e-04 | 5.18e-04 | | stack_match2 | 1.71e-06 | 7.38e-04 | 6.64e-04 | | stack_match3 | 1.79e-06 | 7.72e-04 | 6.95e-04 | -| stack_match4 | 1.77e-06 | 5.92e-04 | 5.18e-04 | | stack_match5 | 1.70e-06 | 7.79e-04 | 6.97e-04 | +| stack_match1 | 5.64e-06 | 21.9e-04 | 39.7e-04 | | repeated_substitution1 | 1.20e-06 | 3.50e-04 | 3.06e-04 | | repeated_substitution2 | 1.86e-06 | 3.58e-04 | 3.15e-04 | | repeated_substitution3 | 4.27e-06 | 14.0e-04 | 12.5e-04 | | repeated_substitution4 | 4.96e-06 | 14.9e-04 | 13.5e-04 | - Overall, most of these solutions had fairly similar performance, and runtime scaled similarly with input length. There is certainly no evidence for either class of solutions being systematically better than the other. -The slowest was `stack_match1`, which did a lot of lookups in dictionary keys and values. Searching instead in sets or strings gave a small but perhaps useful improvement. +The slowest was `stack_match1`, which did a lot of lookups in dictionary. +keys and values. Searching instead in sets or strings gave a small but perhaps useful improvement. -Among the repeated-substitution codes, the first two used standard Python string operations, running slightly faster than the second two which use regular expressions. +Among the repeated-substitution solutions, the first two used standard Python string operations, running slightly faster than the second two which use regular expressions. [benchmark-code]: https://github.com/exercism/python/blob/main/exercises/practice/matching-brackets/.articles/performance/code/Benchmark.py From a01b50f30c31e38672ca5c70cc74ac8adf49c049 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Wed, 31 Jul 2024 18:32:57 -0700 Subject: [PATCH 5/5] Un crossed left vs right --- .../matching-brackets/.approaches/stack-match/content.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/matching-brackets/.approaches/stack-match/content.md b/exercises/practice/matching-brackets/.approaches/stack-match/content.md index b07a798833..9619e83390 100644 --- a/exercises/practice/matching-brackets/.approaches/stack-match/content.md +++ b/exercises/practice/matching-brackets/.approaches/stack-match/content.md @@ -26,7 +26,7 @@ In Python, a [`list`][concept:python/lists]() is a good implementation of a stac Some solutions use [`collections.deque()`][collections-deque] as an alternative implementation, though this has no clear advantage (_since the code only uses appends to the right-hand side_) and near-identical runtime performance. -The default iteration for a dictionary is over the _keys_, so the code above uses a plain `bracket_map` to search for left brackets, while `bracket_map.values()` is used to search for right brackets. +The default iteration for a dictionary is over the _keys_, so the code above uses a plain `bracket_map` to search for right brackets, while `bracket_map.values()` is used to search for left brackets. Other solutions created two sets of left and right brackets explicitly, or searched a string representation: