4 ms·
Just a heads up that this implementation has exponential time complexity on pathological patterns. Demo: https://tio.run/##tVddc6IwFH3nV0Q7WyXFIlrdUWr7uC/9B213
by hddqsb 4y ago
Just a heads up that this implementation has exponential time complexity on pathological patterns.
Demo: https://tio.run/##tVddc6IwFH3nV0Q7WyXFIlrdUWr7uC/9B213BkIQKoKFUGs7/nY3EFA@Emi3LQ@Oudyce@69h3ygwRKhw@HM9ZEXWxhcR8TyXPPSuZFKNjeomkLXX5ZtiOw2ODFJqgqiIA4RngOHkE00V9WlS5zYvETBWg2x5UbZr@kFpjqbYdPEeGSOpkNzfDWdTn@PzCvbGE80ZGvInOCZObHxTI1CpMbE9S7R2d1kTONA8IcCDCKy8zDYGITg0AdrgyAnYQegKrk@AYxsavaw30eBHxGAHCMEMJujgMQvG9xhX5FA9hSdGQ7zZf8T13ToB8iIsCy9pxO3juvh/gkOnJ@fJsjg/YgebV1KKve8Hz4WXyaIoAd786PliA0q4DmA9ggWi2ROESd/MqeLC130iqINBuW3@9LItUuRaSxNroGFmMS0DZoOaH/SqieNqOXQ51akGKnSt5y/phT5akptbvHJe1boV9YrmTuvnfwJlVfJY5zmQmZhhmkYP@BHMkNsrPSKHm4reuBTEdLgYt4XMMutYNomCiOoS9IHBCUWE0UCC3BSeyrWvz29pjHq@EMKZoVe0MpzFKmJlFih/PDQK3x1yZdwswAj3uQW1s3MxQxYe5P1Qij@PFGtjrkH2KNt5yT22BMlUdGNGCpbF4Yt1RDlekIR1etTTGhfxpwFciDMk63tRlgWqi70xb5V8Bw1eCJQ6Ju40Sz4TQIs4pgjkhSRuuuSePljqVA0sRPLQYCyFzLN1tEGknl0EnjBFocsN7mVSOadVEDsiwqeSP4M9/yccEG/2TYRgoHQKSkBSgTGsqQiQ@A6rfRXPsp3YaxOe73/a6Fo4dXKLQ@cN6NPpSkXTzSLU6PSdydOXyMltVu@6ciTbEeSiF4n2xpr/UrtwuNR89b/tV19X9nf6X41bzrHiXavbygfzdE2PA8QJwziZSlPC9tG7JE6swalVxTeaVX4h6ot1Hejrjuf1PXHqPyIDDiN5DfwdOwW7ePspJRfnH72usHNh/1yThylWxbjLtWO9VKpE9Jeqt4Q@ddD0TWwrNQMl39vUai5MJSVDCczs5GsZIhHamvD9ft5AA43ugJ1DWhCBC2IoQ2X0IEufIIr6ME19GEAN/AZhjCCBMbwBW7hK9zBt64u8a@3KZ5hmghZFsa2vVw6jus@Pa1Wnrde@34QbDbPz2EYRYTE8cvLdvv6utu9vRkZ4oaCELvfTdOfg1/Rg99VSgU@FjUvJNXYLejucNQFc9D1g26S/eHwDw https://tio.run/##tVddc6IwFH3nV0Q7WyXFIlrdUWr7uC/9B213BkIQKo...
pattern = "a*b*c*d*e*f*g*h*i*j*k*l*m*n*o*p*q*r*s*t*u*v*w*x*y*z"
text = "aabbccddeeffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzza"
running time = 1.6s
It should be relatively easy to fix this. Insight: If the pattern contains two or more wildcards (i.e. it has the form "<A>*<B>*<C>"), and you've found a match for "<A>*<B>", but you can't find a match for "*<C>", you can stop immediately without considering longer matches for the first wildcard. The reason is that a longer match for the first wildcard will mean you to start looking for "*<C>" later in the string, and that's strictly harder.
- ball_of_lint 4y agoThat might help, but I think it goes in the wrong direction - The proper way to implement a regex matcher if you are worried about performance against arbitrary patterns is to build a Finite State Machine. It's not as simple of code and involves knowing some computer science theory, but results in a matcher with better time complexity guarantees.
- ridiculous_fish 4y agoFinite state machines also have their cliffs, in particular bounded repetition. Rust's FA-based regex will explode with /.{12345}/, while v8's backtracking engine handles it no problem.
- burntsushi 4y agoYes, but please be careful here. The cliffs are categorically different. The cliffs of a finite state machine are still bounded to O(m * n), where m ~ len(regex) and n ~ len(haystack). The cliffs of a backtracker are exponential and difficult to predict. The repetition operator provokes the cliff because it represents short-hand syntax for easily increasing `m` to very large sizes. Indeed, by default, '.{12345}' will fail to compile because it blows the default limit. That only works because, as I said, the cliffs for finite automata based regex engines are far more predictable.
- hddqsb 4y ago[Note: This comment tree is about the Redis code, which uses glob-style patterns rather than regex. So in this comment and in my grandparent comment, "*" means "any string" rather than "zero or more of the previous pattern".] An NFA / DFA would be a perfectly valid way to implement an efficient matcher, but it's not the only way. For example, memoization (https://news.ycombinator.com/item?id=32434412#32436442 https://news.ycombinator.com/item?id=32434412#32436442) can be easily shown to have time complexity O(P * T) where P = len(pattern) and T = len(text), which is the same as a basic NFA implementation. In the case of the Redis code, my proposed fix requires much less code and memory than memoization or an NFA, and has time complexity O(P * T). It's slightly harder to prove this bound; here is a sketch of the proof: Matching against a pattern fragment that does not contain "*" takes O(len(pattern_fragment)). When a call to stringmatchlen() encounters a wildcard, it may recurse O(T) times (and then it returns). Each recursive call can be classified either "leaf" (if there is a match failure before the next wildcard) or "full" (if the match reaches the next wildcard). The insight I described leads to at most one "full" call at each level of the recursion (i.e. for each wildcard in the pattern). So for a particular pattern fragment, the number of "leaf" calls that process it is O(T). Hence the overall time complexity is O(P * T).