<div class="controls">
<label>Pattern <input id="pat" type="text" value="ABABC" maxlength="12" spellcheck="false"></label>
<label>Text <input id="txt" type="text" value="ABABABABC" maxlength="40" spellcheck="false"></label>
<div class="btns">
<button id="btnStep" type="button">Step</button>
<button id="btnRun" type="button">Run all</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
</div>
<section class="panel">
<h3>Failure function <span class="label">f[i] = longest prefix = suffix of pattern[0..i]</span></h3>
<div id="failTable" class="fail-table"></div>
</section>
<section class="panel">
<h3>Search <span class="label">text pointer <em>i</em> never goes back</span></h3>
<div id="searchViz" class="search-viz"></div>
<div class="status" id="status">Press Step or Run all.</div>
</section>
<div id="matches" class="matches"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 15px; }
.controls { display: flex; flex-wrap: wrap; gap: .6rem .9rem; align-items: flex-end; margin-bottom: .8rem; }
label { display: flex; flex-direction: column; gap: .2rem; font-size: .82rem; font-weight: 600; color: #555; }
input { font: 600 15px ui-monospace, monospace; padding: .35rem .6rem; border: 1.5px solid #bcc; border-radius: 6px; width: 180px; }
.btns { display: flex; gap: .5rem; align-items: flex-end; }
button { font: 600 14px system-ui; padding: .4rem .85rem; border: 1.5px solid #1d3557; background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.panel { margin-bottom: .8rem; }
h3 { font-size: .88rem; font-weight: 700; color: #1d3557; margin: 0 0 .3rem; }
.label { font-weight: 400; color: #666; font-size: .78rem; margin-left: .4rem; }
/* Failure function table */
.fail-table { display: flex; gap: 3px; flex-wrap: nowrap; }
.ft-col { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.ft-idx { font-size: .72rem; color: #999; width: 32px; text-align: center; }
.ft-char { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;
font: 700 14px ui-monospace, monospace; border-radius: 5px;
background: #e8eef3; border: 1.5px solid #c8d5e0; color: #1d3557; }
.ft-val { width: 32px; height: 24px; display: flex; align-items: center; justify-content: center;
font: 600 12px ui-monospace, monospace; color: #c0392b; }
/* Search viz */
.search-viz { position: relative; overflow-x: auto; padding-bottom: .4rem; }
.sv-row { display: flex; gap: 3px; margin-bottom: 3px; }
.sv-cell { width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;
font: 600 13px ui-monospace, monospace; border-radius: 5px; flex-shrink: 0; }
.sv-cell.txt { background: #e8eef3; border: 1.5px solid #c8d5e0; color: #1d3557; }
.sv-cell.pat { background: transparent; border: 1.5px solid transparent; color: #555; }
.sv-cell.match { background: #c8f5d8; border-color: #3cb371; color: #1a6a3a; }
.sv-cell.mismatch { background: #fde; border-color: #e05; color: #900; }
.sv-cell.active { border-color: #e67e22; background: #fff3e0; }
.sv-cell.done { background: #d0ebff; border-color: #3a86ff; color: #1a4a8a; }
.sv-cell.empty { border-color: transparent; }
.status { font-size: .92rem; font-weight: 600; margin: .3rem 0; min-height: 1.3em; color: #333; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.matches { font-size: .88rem; color: #1d3557; font-weight: 600; min-height: 1.2em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// KMP: build failure function then search text for pattern.
// The text pointer i never decreases — that’s the whole point.
function buildFailure(pat) {
const m = pat.length, f = new Array(m).fill(0);
let k = 0;
for (let i = 1; i < m; i++) {
while (k > 0 && pat[k] !== pat[i]) k = f[k - 1];
if (pat[k] === pat[i]) k++;
f[i] = k;
}
return f;
}
function kmpSearch(txt, pat, f) {
// Returns array of step objects for animation
const steps = [], matches = [];
const n = txt.length, m = pat.length;
if (!m) return { steps, matches };
let j = 0; // pattern pointer
for (let i = 0; i < n; i++) {
while (j > 0 && pat[j] !== txt[i]) {
steps.push({ i, j, type: 'mismatch', prevJ: j, newJ: f[j - 1], matches: [...matches] });
j = f[j - 1];
}
if (pat[j] === txt[i]) {
steps.push({ i, j, type: 'match', matches: [...matches] });
j++;
} else {
steps.push({ i, j: 0, type: 'nomatch', matches: [...matches] });
}
if (j === m) {
const start = i - m + 1;
matches.push(start);
steps.push({ i, j, type: 'found', start, matches: [...matches] });
j = f[j - 1];
}
}
return { steps, matches };
}
// ---------- State ----------
let pat = '', txt = '', f = [], steps = [], stepIdx = -1, allMatches = [];
let running = false;
function init() {
pat = document.getElementById('pat').value.toUpperCase();
txt = document.getElementById('txt').value.toUpperCase();
f = buildFailure(pat);
const res = kmpSearch(txt, pat, f);
steps = res.steps; allMatches = [];
stepIdx = -1;
renderFailTable();
renderSearch(-1, null, []);
document.getElementById('status').textContent = 'Press Step or Run all.';
document.getElementById('status').className = 'status';
document.getElementById('matches').textContent = '';
}
function renderFailTable() {
const el = document.getElementById('failTable');
el.innerHTML = '';
for (let i = 0; i < pat.length; i++) {
const col = document.createElement('div'); col.className = 'ft-col';
col.innerHTML = `<div class="ft-idx">${i}</div><div class="ft-char">${pat[i]}</div><div class="ft-val">${f[i]}</div>`;
el.appendChild(col);
}
}
function renderSearch(iPos, jPos, matches) {
const el = document.getElementById('searchViz'); el.innerHTML = '';
if (!txt.length) return;
// Row 1: text
const txtRow = document.createElement('div'); txtRow.className = 'sv-row';
for (let i = 0; i < txt.length; i++) {
const c = document.createElement('div');
let cls = 'sv-cell txt';
if (i === iPos) cls += ' active';
if (matches.includes('text_' + i)) cls += ' done';
c.className = cls; c.textContent = txt[i];
txtRow.appendChild(c);
}
el.appendChild(txtRow);
// Row 2: aligned pattern (show pattern aligned at iPos - jPos)
if (jPos !== null && jPos >= 0 && iPos >= 0) {
const offset = iPos - jPos;
const patRow = document.createElement('div'); patRow.className = 'sv-row';
for (let i = 0; i < txt.length; i++) {
const pi = i - offset;
const c = document.createElement('div');
if (pi >= 0 && pi < pat.length) {
let cls = 'sv-cell pat';
if (pi < jPos) cls += ' match';
if (pi === jPos) cls += ' active';
c.className = cls; c.textContent = pat[pi];
} else {
c.className = 'sv-cell empty'; c.textContent = '';
}
patRow.appendChild(c);
}
el.appendChild(patRow);
}
// highlight match positions in text
if (matches.length) {
for (const start of matches) {
for (let k = 0; k < pat.length; k++) {
const cell = txtRow.children[start + k];
if (cell) { cell.classList.add('done'); }
}
}
}
}
function applyStep() {
if (stepIdx >= steps.length - 1) return false;
stepIdx++;
const s = steps[stepIdx];
allMatches = s.matches.slice();
const statusEl = document.getElementById('status');
const matchEl = document.getElementById('matches');
if (s.type === 'match') {
renderSearch(s.i, s.j, allMatches);
statusEl.textContent = fmt('text[%i%]=’%v%’ = pattern[%j%]=’%v2%’ — match, advance both.', { i: s.i, v: txt[s.i], j: s.j, v2: pat[s.j] });
statusEl.className = 'status';
} else if (s.type === 'mismatch') {
renderSearch(s.i, s.j, allMatches);
statusEl.textContent = fmt('Mismatch at text[%i%] vs pattern[%j%]. Jump pattern to f[%v%]=%newJ% (no text backtrack!).', { i: s.i, j: s.j, v: s.j-1, newJ: s.newJ });
statusEl.className = 'status bad';
} else if (s.type === 'nomatch') {
renderSearch(s.i, 0, allMatches);
statusEl.textContent = fmt('text[%i%]=’%v%’ ≠ pattern[0]=’%v2%’ — skip.', { i: s.i, v: txt[s.i], v2: pat[0] });
statusEl.className = 'status bad';
} else if (s.type === 'found') {
renderSearch(s.i, s.j, allMatches);
statusEl.textContent = fmt('✓ Pattern found at position %start%!', { start: s.start });
statusEl.className = 'status ok';
}
matchEl.textContent = allMatches.length
? fmt('Matches found at: %map%', { map: allMatches.map(p => 'position ' + p).join(', ') })
: '';
if (stepIdx === steps.length - 1 && s.type !== 'found') {
if (!allMatches.length) {
statusEl.textContent = 'Search complete — pattern not found.';
statusEl.className = 'status bad';
} else {
statusEl.textContent = fmt('Search complete — %length% match(es):: positions %allMatches%.', { length: allMatches.length, allMatches: allMatches.join(', ') });
statusEl.className = 'status ok';
}
}
return true;
}
document.getElementById('btnStep').addEventListener('click', () => { if (!running) applyStep(); });
document.getElementById('btnRun').addEventListener('click', async () => {
if (running) return;
running = true;
while (applyStep()) await new Promise(r => setTimeout(r, 280));
running = false;
});
document.getElementById('btnReset').addEventListener('click', init);
document.getElementById('pat').addEventListener('input', init);
document.getElementById('txt').addEventListener('input', init);
init();
<div class="controls">
<label>Pattern <input id="pat" type="text" value="ABABC" maxlength="12" spellcheck="false"></label>
<label>Text <input id="txt" type="text" value="ABABABABC" maxlength="40" spellcheck="false"></label>
<div class="btns">
<button id="btnStep" type="button">Step</button>
<button id="btnRun" type="button">Run all</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
</div>
<section class="panel">
<h3>Failure function <span class="label">f[i] = longest prefix = suffix of pattern[0..i]</span></h3>
<div id="failTable" class="fail-table"></div>
</section>
<section class="panel">
<h3>Search <span class="label">text pointer <em>i</em> never goes back</span></h3>
<div id="searchViz" class="search-viz"></div>
<div class="status" id="status">Press Step or Run all.</div>
</section>
<div id="matches" class="matches"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 15px; }
.controls { display: flex; flex-wrap: wrap; gap: .6rem .9rem; align-items: flex-end; margin-bottom: .8rem; }
label { display: flex; flex-direction: column; gap: .2rem; font-size: .82rem; font-weight: 600; color: #555; }
input { font: 600 15px ui-monospace, monospace; padding: .35rem .6rem; border: 1.5px solid #bcc; border-radius: 6px; width: 180px; }
.btns { display: flex; gap: .5rem; align-items: flex-end; }
button { font: 600 14px system-ui; padding: .4rem .85rem; border: 1.5px solid #1d3557; background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.panel { margin-bottom: .8rem; }
h3 { font-size: .88rem; font-weight: 700; color: #1d3557; margin: 0 0 .3rem; }
.label { font-weight: 400; color: #666; font-size: .78rem; margin-left: .4rem; }
/* Failure function table */
.fail-table { display: flex; gap: 3px; flex-wrap: nowrap; }
.ft-col { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.ft-idx { font-size: .72rem; color: #999; width: 32px; text-align: center; }
.ft-char { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;
font: 700 14px ui-monospace, monospace; border-radius: 5px;
background: #e8eef3; border: 1.5px solid #c8d5e0; color: #1d3557; }
.ft-val { width: 32px; height: 24px; display: flex; align-items: center; justify-content: center;
font: 600 12px ui-monospace, monospace; color: #c0392b; }
/* Search viz */
.search-viz { position: relative; overflow-x: auto; padding-bottom: .4rem; }
.sv-row { display: flex; gap: 3px; margin-bottom: 3px; }
.sv-cell { width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;
font: 600 13px ui-monospace, monospace; border-radius: 5px; flex-shrink: 0; }
.sv-cell.txt { background: #e8eef3; border: 1.5px solid #c8d5e0; color: #1d3557; }
.sv-cell.pat { background: transparent; border: 1.5px solid transparent; color: #555; }
.sv-cell.match { background: #c8f5d8; border-color: #3cb371; color: #1a6a3a; }
.sv-cell.mismatch { background: #fde; border-color: #e05; color: #900; }
.sv-cell.active { border-color: #e67e22; background: #fff3e0; }
.sv-cell.done { background: #d0ebff; border-color: #3a86ff; color: #1a4a8a; }
.sv-cell.empty { border-color: transparent; }
.status { font-size: .92rem; font-weight: 600; margin: .3rem 0; min-height: 1.3em; color: #333; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.matches { font-size: .88rem; color: #1d3557; font-weight: 600; min-height: 1.2em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// KMP: build failure function then search text for pattern.
// The text pointer i never decreases — that’s the whole point.
function buildFailure(pat) {
const m = pat.length, f = new Array(m).fill(0);
let k = 0;
for (let i = 1; i < m; i++) {
while (k > 0 && pat[k] !== pat[i]) k = f[k - 1];
if (pat[k] === pat[i]) k++;
f[i] = k;
}
return f;
}
function kmpSearch(txt, pat, f) {
// Returns array of step objects for animation
const steps = [], matches = [];
const n = txt.length, m = pat.length;
if (!m) return { steps, matches };
let j = 0; // pattern pointer
for (let i = 0; i < n; i++) {
while (j > 0 && pat[j] !== txt[i]) {
steps.push({ i, j, type: 'mismatch', prevJ: j, newJ: f[j - 1], matches: [...matches] });
j = f[j - 1];
}
if (pat[j] === txt[i]) {
steps.push({ i, j, type: 'match', matches: [...matches] });
j++;
} else {
steps.push({ i, j: 0, type: 'nomatch', matches: [...matches] });
}
if (j === m) {
const start = i - m + 1;
matches.push(start);
steps.push({ i, j, type: 'found', start, matches: [...matches] });
j = f[j - 1];
}
}
return { steps, matches };
}
// ---------- State ----------
let pat = '', txt = '', f = [], steps = [], stepIdx = -1, allMatches = [];
let running = false;
function init() {
pat = document.getElementById('pat').value.toUpperCase();
txt = document.getElementById('txt').value.toUpperCase();
f = buildFailure(pat);
const res = kmpSearch(txt, pat, f);
steps = res.steps; allMatches = [];
stepIdx = -1;
renderFailTable();
renderSearch(-1, null, []);
document.getElementById('status').textContent = 'Press Step or Run all.';
document.getElementById('status').className = 'status';
document.getElementById('matches').textContent = '';
}
function renderFailTable() {
const el = document.getElementById('failTable');
el.innerHTML = '';
for (let i = 0; i < pat.length; i++) {
const col = document.createElement('div'); col.className = 'ft-col';
col.innerHTML = `<div class="ft-idx">${i}</div><div class="ft-char">${pat[i]}</div><div class="ft-val">${f[i]}</div>`;
el.appendChild(col);
}
}
function renderSearch(iPos, jPos, matches) {
const el = document.getElementById('searchViz'); el.innerHTML = '';
if (!txt.length) return;
// Row 1: text
const txtRow = document.createElement('div'); txtRow.className = 'sv-row';
for (let i = 0; i < txt.length; i++) {
const c = document.createElement('div');
let cls = 'sv-cell txt';
if (i === iPos) cls += ' active';
if (matches.includes('text_' + i)) cls += ' done';
c.className = cls; c.textContent = txt[i];
txtRow.appendChild(c);
}
el.appendChild(txtRow);
// Row 2: aligned pattern (show pattern aligned at iPos - jPos)
if (jPos !== null && jPos >= 0 && iPos >= 0) {
const offset = iPos - jPos;
const patRow = document.createElement('div'); patRow.className = 'sv-row';
for (let i = 0; i < txt.length; i++) {
const pi = i - offset;
const c = document.createElement('div');
if (pi >= 0 && pi < pat.length) {
let cls = 'sv-cell pat';
if (pi < jPos) cls += ' match';
if (pi === jPos) cls += ' active';
c.className = cls; c.textContent = pat[pi];
} else {
c.className = 'sv-cell empty'; c.textContent = '';
}
patRow.appendChild(c);
}
el.appendChild(patRow);
}
// highlight match positions in text
if (matches.length) {
for (const start of matches) {
for (let k = 0; k < pat.length; k++) {
const cell = txtRow.children[start + k];
if (cell) { cell.classList.add('done'); }
}
}
}
}
function applyStep() {
if (stepIdx >= steps.length - 1) return false;
stepIdx++;
const s = steps[stepIdx];
allMatches = s.matches.slice();
const statusEl = document.getElementById('status');
const matchEl = document.getElementById('matches');
if (s.type === 'match') {
renderSearch(s.i, s.j, allMatches);
statusEl.textContent = fmt('text[%i%]=’%v%’ = pattern[%j%]=’%v2%’ — match, advance both.', { i: s.i, v: txt[s.i], j: s.j, v2: pat[s.j] });
statusEl.className = 'status';
} else if (s.type === 'mismatch') {
renderSearch(s.i, s.j, allMatches);
statusEl.textContent = fmt('Mismatch at text[%i%] vs pattern[%j%]. Jump pattern to f[%v%]=%newJ% (no text backtrack!).', { i: s.i, j: s.j, v: s.j-1, newJ: s.newJ });
statusEl.className = 'status bad';
} else if (s.type === 'nomatch') {
renderSearch(s.i, 0, allMatches);
statusEl.textContent = fmt('text[%i%]=’%v%’ ≠ pattern[0]=’%v2%’ — skip.', { i: s.i, v: txt[s.i], v2: pat[0] });
statusEl.className = 'status bad';
} else if (s.type === 'found') {
renderSearch(s.i, s.j, allMatches);
statusEl.textContent = fmt('✓ Pattern found at position %start%!', { start: s.start });
statusEl.className = 'status ok';
}
matchEl.textContent = allMatches.length
? fmt('Matches found at: %map%', { map: allMatches.map(p => 'position ' + p).join(', ') })
: '';
if (stepIdx === steps.length - 1 && s.type !== 'found') {
if (!allMatches.length) {
statusEl.textContent = 'Search complete — pattern not found.';
statusEl.className = 'status bad';
} else {
statusEl.textContent = fmt('Search complete — %length% match(es):: positions %allMatches%.', { length: allMatches.length, allMatches: allMatches.join(', ') });
statusEl.className = 'status ok';
}
}
return true;
}
document.getElementById('btnStep').addEventListener('click', () => { if (!running) applyStep(); });
document.getElementById('btnRun').addEventListener('click', async () => {
if (running) return;
running = true;
while (applyStep()) await new Promise(r => setTimeout(r, 280));
running = false;
});
document.getElementById('btnReset').addEventListener('click', init);
document.getElementById('pat').addEventListener('input', init);
document.getElementById('txt').addEventListener('input', init);
init();
Comments
Loading comments...