From 79874b2130dbfe5c0d973ce1bd1f66fc5680a6e2 Mon Sep 17 00:00:00 2001 From: PresenceOP-Coder Date: Sun, 9 Nov 2025 08:40:10 +0530 Subject: [PATCH] feat: implemented kmpseach algo --- .../dynamic-programming/kmpSearchAlgo.js | 161 ++++++++ .../kmpSearchAlgovisual.jsx | 389 ++++++++++++++++++ .../DyanmicProgrammingPage.jsx | 8 + src/pages/dynamic-programming/KMPSearch.jsx | 6 + 4 files changed, 564 insertions(+) create mode 100644 src/algorithms/dynamic-programming/kmpSearchAlgo.js create mode 100644 src/components/dynamic-programming/kmpSearchAlgovisual.jsx create mode 100644 src/pages/dynamic-programming/KMPSearch.jsx diff --git a/src/algorithms/dynamic-programming/kmpSearchAlgo.js b/src/algorithms/dynamic-programming/kmpSearchAlgo.js new file mode 100644 index 0000000..136dcf4 --- /dev/null +++ b/src/algorithms/dynamic-programming/kmpSearchAlgo.js @@ -0,0 +1,161 @@ +export function computeLPSSteps(pattern) { + const m = pattern.length; + const lps = Array(m).fill(0); + const steps = []; + let len = 0; + let i = 1; + + steps.push({ + lps: [...lps], + i: 1, + len: 0, + message: "Initializing LPS table. lps[0] is always 0. Starting from i = 1.", + highlight: { i: 1, len: 0, lps: -1 }, + }); + + while (i < m) { + steps.push({ + lps: [...lps], + i: i, + len: len, + message: `Comparing pattern[${i}] ('${pattern[i]}') and pattern[${len}] ('${pattern[len]}').`, + highlight: { i: i, len: len, lps: -1 }, + }); + + if (pattern[i] === pattern[len]) { + len++; + lps[i] = len; + steps.push({ + lps: [...lps], + i: i, + len: len, + message: `Match! lps[${i}] = ${len}. Increment i and len.`, + highlight: { i: i, len: len, lps: i, state: 'match' }, + }); + i++; + } else { + if (len !== 0) { + len = lps[len - 1]; + steps.push({ + lps: [...lps], + i: i, + len: len, + message: `Mismatch. Falling back: len = lps[${len - 1}] = ${len}.`, + highlight: { i: i, len: len, lps: -1, state: 'mismatch' }, + }); + } else { + lps[i] = 0; + steps.push({ + lps: [...lps], + i: i, + len: len, + message: `Mismatch. No fallback. lps[${i}] = 0. Increment i.`, + highlight: { i: i, len: len, lps: i, state: 'mismatch' }, + }); + i++; + } + } + } + + steps.push({ + lps: [...lps], + i: m, + len: len, + message: "LPS table computation complete.", + highlight: { i: m, len: len, lps: -1, state: 'done' }, + }); + return { steps, lpsTable: lps }; + } + + export function kmpSearchSteps(text, pattern, lps) { + const n = text.length; + const m = pattern.length; + const steps = []; + const matches = []; + let i = 0; + let j = 0; + + steps.push({ + i: i, + j: j, + matches: [...matches], + message: "Starting KMP search. i = 0 (text), j = 0 (pattern).", + highlight: { i: 0, j: 0, state: 'idle' } + }); + + while (i < n) { + if (j === 0) { + steps.push({ + i: i, + j: j, + matches: [...matches], + message: `Comparing text[${i}] ('${text[i]}') and pattern[${j}] ('${pattern[j]}').`, + highlight: { i: i, j: j, state: 'compare' } + }); + } else { + steps.push({ + i: i, + j: j, + matches: [...matches], + message: `Comparing text[${i}] ('${text[i]}') and pattern[${j}] ('${pattern[j]}').`, + highlight: { i: i, j: j, state: 'compare' } + }); + } + + + if (pattern[j] === text[i]) { + i++; + j++; + } + + if (j === m) { + const matchIndex = i - j; + matches.push(matchIndex); + steps.push({ + i: i, + j: j, + matches: [...matches], + message: `PATTERN FOUND! at index ${matchIndex}.`, + highlight: { i: i, j: j, state: 'match', matchIndex: matchIndex } + }); + j = lps[j - 1]; + steps.push({ + i: i, + j: j, + matches: [...matches], + message: `Jumping pattern pointer using LPS: j = lps[${m - 1}] = ${j}.`, + highlight: { i: i, j: j, state: 'jump' } + }); + } else if (i < n && pattern[j] !== text[i]) { + if (j !== 0) { + j = lps[j - 1]; + steps.push({ + i: i, + j: j, + matches: [...matches], + message: `Mismatch. Jumping pattern pointer using LPS: j = lps[${j - 1}] = ${j}.`, + highlight: { i: i, j: j, state: 'jump' } + }); + } else { + i++; + steps.push({ + i: i, + j: j, + matches: [...matches], + message: `Mismatch at j=0. Incrementing text pointer i to ${i}.`, + highlight: { i: i, j: j, state: 'mismatch' } + }); + } + } + } + + steps.push({ + i: n, + j: j, + matches: [...matches], + message: `Search complete. ${matches.length} match(es) found.`, + highlight: { i: n, j: j, state: 'done' } + }); + + return { steps, matches }; + } \ No newline at end of file diff --git a/src/components/dynamic-programming/kmpSearchAlgovisual.jsx b/src/components/dynamic-programming/kmpSearchAlgovisual.jsx new file mode 100644 index 0000000..fcdb7d6 --- /dev/null +++ b/src/components/dynamic-programming/kmpSearchAlgovisual.jsx @@ -0,0 +1,389 @@ +import React, { useState, useMemo, useEffect, useRef } from "react"; +import { computeLPSSteps, kmpSearchSteps } from "@/algorithms/string/kmpAlgo"; + +const LPSDisplay = ({ pattern, lpsState }) => { + const { lps, highlight, message } = lpsState; + const { i, len, state } = highlight; + + return ( +
+

Phase 1: Building LPS Table

+
+
+
Pattern:
+ {pattern.split("").map((char, index) => ( +
+ {char} +
+ ))} +
+
+
+ {pattern.split("").map((_, index) => ( +
+ {index === i && i} + {index === len && len} +
+ ))} +
+ +
+
LPS:
+ {lps.map((value, index) => ( +
+ {value} +
+ ))} +
+
+
+

Current Action

+

{message}

+
+
+ ); +}; + +const SearchDisplay = ({ text, pattern, searchState }) => { + const { i, j, matches, highlight, message } = searchState; + const { state, matchIndex } = highlight; + const m = pattern.length; + const patternOffset = i - j; + + return ( +
+

Phase 2: Searching Text

+
+
+
Text:
+ {text.split("").map((char, index) => ( +
+ {char} +
+ ))} +
+
+
+ {text.split("").map((_, index) => ( +
+ {index === i && i} +
+ ))} +
+ +
+
Pattern:
+ {pattern.split("").map((char, index) => ( +
+ {char} +
+ ))} +
+
+
+ {pattern.split("").map((_, index) => ( +
+ {index === j && j} +
+ ))} +
+
+
+

Current Action

+

{message}

+
+
+ ); +}; + +const SPEED_OPTIONS = { + "Slow": 1500, + "Medium": 500, + "Fast": 200, +}; + +export default function KMPVisualizer() { + const [text, setText] = useState("ABABDABACDABABCABAB"); + const [pattern, setPattern] = useState("ABABCABAB"); + + const [lpsSteps, setLpsSteps] = useState([]); + const [searchSteps, setSearchSteps] = useState([]); + const [lpsTable, setLpsTable] = useState([]); + const [matches, setMatches] = useState([]); + + const [currentStep, setCurrentStep] = useState(0); + const [visualizationPhase, setVisualizationPhase] = useState("idle"); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(SPEED_OPTIONS["Medium"]); + const timerRef = useRef(null); + + const handleCompute = () => { + setIsPlaying(false); + clearInterval(timerRef.current); + + if (!pattern) { + alert("Please enter a pattern."); + return; + } + + const { steps: newLpsSteps, lpsTable: newLpsTable } = computeLPSSteps(pattern); + const { steps: newSearchSteps, matches: newMatches } = kmpSearchSteps(text, pattern, newLpsTable); + + setLpsSteps(newLpsSteps); + setLpsTable(newLpsTable); + setSearchSteps(newSearchSteps); + setMatches(newMatches); + + setCurrentStep(0); + setVisualizationPhase("lps"); + }; + + useEffect(() => { + handleCompute(); + }, []); + + useEffect(() => { + if (isPlaying) { + timerRef.current = setInterval(() => { + handleNext(true); + }, speed); + } else { + clearInterval(timerRef.current); + } + return () => clearInterval(timerRef.current); + }, [isPlaying, speed, currentStep, visualizationPhase]); + + const togglePlay = () => { + if (visualizationPhase === 'done') { + setCurrentStep(0); + setVisualizationPhase('lps'); + setIsPlaying(true); + } else { + setIsPlaying(!isPlaying); + } + }; + + const handleNext = (isAutoPlay = false) => { + if (!isAutoPlay) setIsPlaying(false); + + if (visualizationPhase === 'lps') { + if (currentStep < lpsSteps.length - 1) { + setCurrentStep(currentStep + 1); + } else { + setCurrentStep(0); + setVisualizationPhase('search'); + } + } else if (visualizationPhase === 'search') { + if (currentStep < searchSteps.length - 1) { + setCurrentStep(currentStep + 1); + } else { + setVisualizationPhase('done'); + setIsPlaying(false); + } + } + }; + + const handlePrev = () => { + setIsPlaying(false); + + if (visualizationPhase === 'search') { + if (currentStep > 0) { + setCurrentStep(currentStep - 1); + } else { + setCurrentStep(lpsSteps.length - 1); + setVisualizationPhase('lps'); + } + } else if (visualizationPhase === 'lps') { + if (currentStep > 0) { + setCurrentStep(currentStep - 1); + } + } + }; + + const handleTextChange = (e) => { + setText(e.target.value); + setVisualizationPhase('idle'); + } + + const handlePatternChange = (e) => { + setPattern(e.target.value); + setVisualizationPhase('idle'); + } + + const currentLpsState = useMemo(() => lpsSteps[currentStep] || {}, [lpsSteps, currentStep]); + const currentSearchState = useMemo(() => searchSteps[currentStep] || {}, [searchSteps, currentStep]); + + const isFinalStep = visualizationPhase === 'done'; + + return ( +
+
+

+ KMP String Search +

+ +
+ + ❓ What is KMP Search Algorithm? + +
+

+ The Knuth-Morris-Pratt (KMP) algorithm is an efficient string searching algorithm that finds all occurrences of a pattern in a text string. + It avoids unnecessary character comparisons by using information from previous matches. +

+
    +
  • + Phase 1 - LPS Table (Longest Proper Prefix which is also Suffix): Preprocesses the pattern to build an LPS array that stores the length of the longest proper prefix that is also a suffix for each position in the pattern. This helps determine how far we can skip characters when a mismatch occurs. +
  • +
  • + Phase 2 - Pattern Searching: Uses the LPS table to search for the pattern in the text. When a mismatch occurs, instead of starting from the beginning of the pattern, it uses the LPS values to skip unnecessary comparisons, making the search linear time O(n + m) instead of O(n*m). +
  • +
+
+
+ +
+
+ + +
+
+ + +
+ +
+ + {visualizationPhase !== 'idle' ? ( + <> +
+ + +
+ + +
+ +
+ + +
+
+ +
+

+ {visualizationPhase === 'lps' && `Phase 1 (LPS): Step ${currentStep + 1} / ${lpsSteps.length}`} + {visualizationPhase === 'search' && `Phase 2 (Search): Step ${currentStep + 1} / ${searchSteps.length}`} + {visualizationPhase === 'done' && `Visualization Complete!`} +

+
+ +
+ {visualizationPhase === 'lps' && ( + + )} + {(visualizationPhase === 'search' || visualizationPhase === 'done') && ( + + )} + + {isFinalStep && ( +
+

+ 🎉 + Search Finished: {matches.length} match(es) found at indices: {matches.join(', ')} +

+
+ )} +
+ + ) : ( +
+

Enter text and a pattern, then click "Re-Visualize" to begin.

+
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/src/pages/dynamic-programming/DyanmicProgrammingPage.jsx b/src/pages/dynamic-programming/DyanmicProgrammingPage.jsx index 63f93ac..e263170 100644 --- a/src/pages/dynamic-programming/DyanmicProgrammingPage.jsx +++ b/src/pages/dynamic-programming/DyanmicProgrammingPage.jsx @@ -10,6 +10,7 @@ import LISPage from "./LIS"; import CoinChange from "./CoinChange"; import RodCutting from "./RodCutting"; +import KMPSearch from "./KMPSearch"; export default function DynamicProgrammingPage() { const [selectedAlgo, setSelectedAlgo] = useState(""); @@ -71,6 +72,12 @@ export default function DynamicProgrammingPage() { ); + case "KMPSearch": + return ( +
+ +
+ ); default: return (
@@ -138,6 +145,7 @@ export default function DynamicProgrammingPage() { +