diff --git a/src/algorithms/searching/RabinKarp.js b/src/algorithms/searching/RabinKarp.js new file mode 100644 index 0000000..2b7af7b --- /dev/null +++ b/src/algorithms/searching/RabinKarp.js @@ -0,0 +1,147 @@ +export function getRabinKarpSteps(text, pattern) { + const steps = []; + const n = text.length; + const m = pattern.length; + + if (m === 0 || n < m) { + return { steps, found: false }; + } + + const d = 256; + const q = 101; + let h = 1; + let pHash = 0; + let tHash = 0; + let found = false; + + for (let i = 0; i < m - 1; i++) { + h = (h * d) % q; + } + + steps.push({ + windowStart: 0, + compareIndex: -1, + status: 'calculating', + pHash: 0, + tHash: 0, + message: `Starting. Pattern length=${m}, Text length=${n}. Prime q=${q}. Multiplier h=${h}.` + }); + + for (let i = 0; i < m; i++) { + pHash = (d * pHash + pattern.charCodeAt(i)) % q; + tHash = (d * tHash + text.charCodeAt(i)) % q; + + steps.push({ + windowStart: 0, + compareIndex: i, + status: 'calculating', + pHash, + tHash, + message: `Calculating initial hashes. Index ${i}. Pattern Hash = ${pHash}, Window Hash = ${tHash}.` + }); + } + + for (let i = 0; i <= n - m; i++) { + steps.push({ + windowStart: i, + compareIndex: -1, + status: 'sliding', + pHash, + tHash, + message: `Sliding window to index ${i}. Pattern Hash = ${pHash}, Window Hash = ${tHash}.` + }); + + if (pHash === tHash) { + steps.push({ + windowStart: i, + compareIndex: 0, + status: 'hash_match', + pHash, + tHash, + message: `Hash Match Found! Verifying character by character...` + }); + + let j = 0; + for (j = 0; j < m; j++) { + steps.push({ + windowStart: i, + compareIndex: i + j, + patternCompareIndex: j, + status: 'verifying', + pHash, + tHash, + message: `Verifying: text[${i + j}] ('${text[i + j]}') === pattern[${j}] ('${pattern[j]}')?` + }); + + if (text[i + j] !== pattern[j]) { + steps.push({ + windowStart: i, + compareIndex: i + j, + patternCompareIndex: j, + status: 'spurious_hit', + pHash, + tHash, + message: `Spurious Hit! Mismatch found. Resuming search.` + }); + break; + } + } + + if (j === m) { + found = true; + steps.push({ + windowStart: i, + compareIndex: -1, + status: 'found', + pHash, + tHash, + message: `Pattern Found at index ${i}!` + }); + } + } + + if (i < n - m) { + const oldHash = tHash; + const charToRemove = text.charCodeAt(i); + const charToAdd = text.charCodeAt(i + m); + + tHash = (d * (tHash - charToRemove * h) + charToAdd) % q; + if (tHash < 0) { + tHash += q; + } + + steps.push({ + windowStart: i, + compareIndex: -1, + status: 'rolling', + removeIndex: i, + addIndex: i + m, + pHash, + tHash: oldHash, + message: `Rolling hash... Removing '${text[i]}' (val ${charToRemove}).` + }); + + steps.push({ + windowStart: i + 1, + compareIndex: -1, + status: 'rolling', + removeIndex: i, + addIndex: i + m, + pHash, + tHash, + message: `Rolling hash... Adding '${text[i + m]}' (val ${charToAdd}). New Window Hash = ${tHash}.` + }); + } + } + + steps.push({ + windowStart: -1, + compareIndex: -1, + status: found ? 'finished_found' : 'finished_not_found', + pHash, + tHash, + message: found ? "Search complete. Pattern was found." : "Search complete. Pattern was not found." + }); + + return { steps, found }; +} \ No newline at end of file diff --git a/src/components/searching/RabinKarp.jsx b/src/components/searching/RabinKarp.jsx new file mode 100644 index 0000000..ba05237 --- /dev/null +++ b/src/components/searching/RabinKarp.jsx @@ -0,0 +1,319 @@ +import React, { useState, useMemo, useEffect, useRef } from "react"; +// Assumes 'algorithms' is at 'src/algorithms/' +import { getRabinKarpSteps } from "../../algorithms/searching/RabinKarp"; + +const StringDisplay = ({ text, highlights = {} }) => { + return ( +
+ {text.split("").map((char, index) => { + let + bgColor = "bg-gray-700", + textColor = "text-gray-200", + borderColor = "border-gray-600"; + + if (highlights[index]) { + const type = highlights[index]; + if (type === "match") { + bgColor = "bg-green-600"; + textColor = "text-white"; + borderColor = "border-green-400"; + } else if (type === "spurious") { + bgColor = "bg-yellow-600"; + textColor = "text-white"; + borderColor = "border-yellow-400"; + } else if (type === "compare") { + bgColor = "bg-blue-600"; + textColor = "text-white"; + borderColor = "border-blue-400"; + } + } + + return ( +
+ {char} +
+ ); + })} +
+ ); +}; + +const PatternDisplay = ({ pattern, highlights = {} }) => { + return ( +
+ {pattern.split("").map((char, index) => { + let + bgColor = "bg-gray-700", + textColor = "text-gray-200", + borderColor = "border-gray-600"; + + if (highlights[index]) { + const type = highlights[index]; + if (type === "match") { + bgColor = "bg-green-600"; + textColor = "text-white"; + borderColor = "border-green-400"; + } else if (type === "mismatch") { + bgColor = "bg-red-600"; + textColor = "text-white"; + borderColor = "border-red-400"; + } + } + + return ( +
+ {char} +
+ ); + })} +
+ ); +}; + + +// Playback speeds in milliseconds +const SPEED_OPTIONS = { + "Slow": 1500, + "Medium": 500, + "Fast": 200, +}; + +export default function RabinKarp() { + const [text, setText] = useState("AABAACAADAABAABA"); + const [pattern, setPattern] = useState("AABA"); + const [steps, setSteps] = useState([]); + const [currentStep, setCurrentStep] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(SPEED_OPTIONS["Medium"]); + const timerRef = useRef(null); + + // --- Core Logic --- + + const handleCompute = () => { + if (!text || !pattern) { + alert("Text and Pattern cannot be empty."); + return; + } + if (pattern.length > text.length) { + alert("Pattern cannot be longer than the text."); + return; + } + + setIsPlaying(false); + const { steps: newSteps } = getRabinKarpSteps(text, pattern); + setSteps(newSteps); + setCurrentStep(0); + }; + + useEffect(() => { + handleCompute(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (isPlaying && currentStep < steps.length - 1) { + timerRef.current = setInterval(() => { + setCurrentStep((prevStep) => prevStep + 1); + }, speed); + } else if (currentStep === steps.length - 1) { + setIsPlaying(false); + } + + return () => { + clearInterval(timerRef.current); + }; + }, [isPlaying, currentStep, steps.length, speed]); + + // --- Handlers --- + + const togglePlay = () => { + if (currentStep === steps.length - 1) { + setCurrentStep(0); + setIsPlaying(true); + } else { + setIsPlaying(!isPlaying); + } + }; + + const handleNext = () => { + setIsPlaying(false); + if (currentStep < steps.length - 1) setCurrentStep(currentStep + 1); + }; + + const handlePrev = () => { + setIsPlaying(false); + if (currentStep > 0) setCurrentStep(currentStep - 1); + }; + + const currentState = useMemo(() => steps[currentStep] || {}, [steps, currentStep]); + const { status, textHighlights, patternHighlights, textHash, patternHash, message } = currentState; + + const isFinalStep = steps.length > 0 && currentStep === steps.length - 1; + + return ( +
+
+

+ Rabin-Karp String Search +

+ + {/* --- What is Rabin-Karp? --- */} +
+ + ❓ What is Rabin-Karp? + +
+

+ Rabin-Karp is a string matching algorithm that uses a **hashing function** to find a pattern in text. +

+
    +
  • It calculates a hash for the pattern and for each "window" of the text.
  • +
  • It uses a **"rolling hash"** to quickly calculate the next window's hash from the previous one.
  • +
  • If hashes match, it performs a character-by-character check to avoid **"spurious hits"** (where different strings have the same hash).
  • +
+
+
+ {/* --- End of What is Rabin-Karp? --- */} + + +
+
+ + setText(e.target.value)} + /> +
+ +
+ + setPattern(e.target.value)} + /> +
+ + +
+ + {steps.length > 0 ? ( + <> +
+ + +
+ + +
+ +
+ + +
+
+ +
+

+ Step {currentStep + 1} / {steps.length} +

+
+ + +
+
+

Current Action

+

+ {message || 'Starting...'} +

+
+ +
+
+

Window Hash

+

+ {textHash} +

+
+
+

Pattern Hash

+

+ {patternHash} +

+
+
+ +
+

Text

+ +
+ +
+

Pattern

+ +
+ + {isFinalStep && ( +
+

+ {status === 'finished_found' ? '🎉 Pattern Found! 🎉' : 'Search Complete. Pattern Not Found.'} +

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

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

+
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/src/pages/searching/RabinKarp.jsx b/src/pages/searching/RabinKarp.jsx new file mode 100644 index 0000000..6726b5f --- /dev/null +++ b/src/pages/searching/RabinKarp.jsx @@ -0,0 +1,6 @@ +import RabinKarpVisualizer from "../../components/searching/RabinKarp"; +import React from "react"; + +export default function RabinKarpPage() { + return ; +} \ No newline at end of file diff --git a/src/pages/searching/SearchingPage.jsx b/src/pages/searching/SearchingPage.jsx index 22b9346..79756f5 100644 --- a/src/pages/searching/SearchingPage.jsx +++ b/src/pages/searching/SearchingPage.jsx @@ -1,9 +1,9 @@ -// src/pages/SortingPage.jsx import React, { useState } from "react"; import LinearSearch from "./linearSearch"; import BinarySearch from "./BinarySearch"; +import RabinKarp from "./RabinKarp"; -export default function SortingPage() { +export default function SearchingPage() { const [selectedAlgo, setSelectedAlgo] = useState(""); const renderAlgorithm = () => { @@ -12,7 +12,9 @@ export default function SortingPage() { return ; case "BinarySearch": return ; - + case "RabinKarp": + return ; + default: return (
@@ -24,7 +26,6 @@ export default function SortingPage() { return (
- {/* Left Sidebar */}

Searching Panel @@ -37,8 +38,9 @@ export default function SortingPage() { className="w-full p-2 rounded bg-[#1e293b] text-white border border-gray-600 cursor-pointer" > - - + + +

- {/* Right Visualization Area */}
{renderAlgorithm()}
); -} +} \ No newline at end of file