Knuth–Morris–Pratt illustrated
cambridge.org
cambridge.org
Consider the normal naive way to search for a string in a document. First you search for the next instance of the first letter of your search string, then try to match the entire string at that position. If the match fails then you advance your search position by one character and repeat (looking for next occurrence of first letter, etc).
The Knuth-Morris-Pratt algorithm improves upon this naive approach by usually advancing by more than one character after a failed match, thereby speeding up the search. It does this by taking advantage of its knowledge of the search string and the position at which the match failed.
To get the idea, consider searching for the string "explosion" .. first we find the next "e", then try matching the rest of the word. Say we match "explo" then fail (perhaps the document had "explode"), so now we want to start over and find the next "e" in the document... What KMP would do here is note that the document matched the first 5 letters "explo", none of which (other than 1st letter) are an "e", so it can advance by 5 characters, not just 1, to start looking for the next "e".
The amounts it can advance at each failed match position are pre-calculated to be efficient.
But still, it all comes down to how far can you advance at any given match failure position.
Wouldn't knowing that require having already run the search?
And I can certainly implement the Z algorithm in a few mins while I struggle to implement the KMP off the top of my head.
Edit: perhaps a bit better article on this https://www.geeksforgeeks.org/z-algorithm-linear-time-patter...
Like KMP, it involves a preliminary step of analyzing the search term(s), and from them it builds a directed graph representing rules for what to do while consuming the stream of document-characters.
To find all occurrences, it's something like O(document_length + all_target_words_combined_length + number_of_hits) .
If Go is more your speed, here's an implementation in Go: https://git.lukeshu.com/btrfs-progs-ng/tree/cmd/btrfs-rec/in... (though it is slightly complicated by the fact that it allows 'substr' to include fixed-length wildcards).
These examples use pretty minimal features of Haskell, mostly expressions, function definitions with argument pattern matching, and algebraic type definitions. These would take an hour or two to get acquainted with. Nothing fancy is used in the code, in particular, nothing outright monadic :)
For comparison, you can check out a Lisp version at the end. It's much longer, and Lisp (well, Racket here) is usually a very expressive language.
-- The any function determines if some element of the input list satisfies the given predicate:
any f [] = False
any f (x:xt) = f x || any f xt
In the meantime here's a more palatable version of 'any' for the 99% of programmers who are put off by the confusing Haskell: // Returns whether any elements of this stream match the provided predicate. May not evaluate the predicate on all elements if not necessary for determining the result. If the stream is empty then false is returned and the predicate is not evaluated.
public final boolean anyMatch(Predicate<? super P_OUT> predicate) {
return evaluate(MatchOps.makeRef(predicate, MatchOps.MatchKind.ANY));
}
final <R> R evaluate(TerminalOp<E_OUT, R> terminalOp) {
assert getOutputShape() == terminalOp.inputShape();
if (linkedOrConsumed)
throw new IllegalStateException(MSG_STREAM_LINKED);
linkedOrConsumed = true;
return isParallel()
? terminalOp.evaluateParallel(this, sourceSpliterator(terminalOp.getOpFlags()))
: terminalOp.evaluateSequential(this, sourceSpliterator(terminalOp.getOpFlags()));
}
public static <T> TerminalOp<T, Boolean> makeRef(Predicate<? super T> predicate,
MatchKind matchKind) {
Objects.requireNonNull(predicate);
Objects.requireNonNull(matchKind);
class MatchSink extends BooleanTerminalSink<T> {
MatchSink() {
super(matchKind);
}
@Override
public void accept(T t) {
if (!stop && predicate.test(t) == matchKind.stopOnPredicateMatches) {
stop = true;
value = matchKind.shortCircuitResult;
}
}
}
return new MatchOp<>(StreamShape.REFERENCE, matchKind, MatchSink::new);
}Anything lowercase is a function/variable, and will appear somewhere on the left-hand-side of an equals.
Anything uppercase is a Type/constructor. In this case I think they've only used a Tree, which they defined themselves (either a Nil or a Node).
any and scanl would typically be imports, but they've defined these in full.
They also defined init themselves unrelated to the one in the stdlib.
I abandoned this project without completing KMP. If anyone is interested in this sort of algorithm explanations, I'd love to collaborate / finish this project.
You want to grow the window as big as possible to match the substring. The data structure you naturally come up with to do checks efficiently here is the one you use in KMP.
https://hn.algolia.com/?dateRange=pastWeek&page=0&prefix=tru...
And mehulashah's comment that the thread claims was posted 7 hours ago was also posted two days ago, according to Algolia
https://hn.algolia.com/?dateRange=pastWeek&page=0&prefix=tru...
I'm aware of the second chance pool, but I don't think it can be that
https://news.ycombinator.com/pool
My recent post: https://news.ycombinator.com/item?id=40037466
shows with the second chance repost time (12 hours ago), but was actually first posted ~ 24 hours ago.
It sank w/out notice then, this morning I woke to find it was suddenly active and "recently" posted.
That's the second chance pool effect for you :-)
My submissions page shows the first submission time, the "actual" submission comments page shows the second post time.
There's a fair bit of lispy magic going on around here; dang and other mods can merge comments on seperate submissions, view comment voting history, and generally do a wide range of back end housekeeping | forensic | anti troll operations and views .. very probably nothing really changes .. just our end user perception of the HN world is filtered by transformation.
I haven't implemented KMP but I might try after reading this.
BM beats it easily, not talking about the best two-way search algorithms, as implemented in musl.
Usually Thierry Lecroqs site has all the graphical descriptions of string search algos, just the latest are missing. https://www-igm.univ-mlv.fr/~lecroq/string/index.html
And for these related problems:
- fastest regexp search
- fastest search, matching minimum edit distance
For regex, you can't really distill it down to one single fastest algorithm.
It's somewhat similar even for substring search. But certainly, the fastest algorithms are going to be the ones that make use of SIMD in some way.
Also if you look at algorithms like Boyer-Moore, they effectively DO skip spaces, but do so in a manner that is language / content agnostic. (https://www-igm.univ-mlv.fr/~lecroq/string/node14.html)