Below is a sorted list of integers. The classic two-pointer algorithm fixes each element a and scans the rest with two pointers — one from the left, one from the right — looking for b and c such that a + b + c = 0. Click Step to advance one outer iteration, or Run to finish instantly.
<p class="hint">A sorted list of integers. The two-pointer algorithm fixes <b>a</b> and scans for <b>b + c = −a</b> with left/right pointers. Click <b>Step</b> to advance one outer iteration, or <b>Run</b> to finish. A found triple is highlighted in green.</p>
<div id="array-row" class="array-row"></div>
<div class="info-row">
<span id="info-a" class="badge badge-a">a = —</span>
<span id="info-b" class="badge badge-b">b = —</span>
<span id="info-c" class="badge badge-c">c = —</span>
<span id="info-cmp" class="badge badge-cmp">comparisons: 0</span>
</div>
<div id="status" class="status">Press Step or Run to start.</div>
<div class="btns">
<button id="btn-step" type="button">Step</button>
<button id="btn-run" type="button">Run</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.array-row { display: flex; gap: 5px; flex-wrap: wrap; margin: .5rem 0 .6rem; }
.cell {
width: 44px; height: 44px; display: flex; align-items: center; justify-content: center;
font: 700 14px ui-monospace, monospace; border-radius: 8px;
border: 2px solid #c5cdd6; background: #edf1f5; color: #2b3a4a;
transition: background .15s, border-color .15s;
}
.cell.fixed { background: #dbe4ff; border-color: #7091e6; color: #1d3557; }
.cell.left { background: #fff3cd; border-color: #f0a500; color: #5a3e00; }
.cell.right { background: #fde8c8; border-color: #e07b39; color: #5a2500; }
.cell.found { background: #c3f0ca; border-color: #2e9e51; color: #0a4020; }
.info-row { display: flex; gap: .4rem; flex-wrap: wrap; margin: .3rem 0; }
.badge { font: 600 12px ui-monospace, monospace; padding: .2rem .55rem; border-radius: 6px; }
.badge-a { background: #dbe4ff; color: #1d3557; }
.badge-b { background: #fff3cd; color: #5a3e00; }
.badge-c { background: #fde8c8; color: #5a2500; }
.badge-cmp { background: #f0f0f0; color: #333; }
.status { font-size: .95rem; font-weight: 600; margin: .55rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .95rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// 3SUM two-pointer algorithm:
// Given sorted array A, for each i, use pointers L=i+1, R=n-1 to find A[L]+A[R]==-A[i].
// Time: O(n^2). We step through outer iterations one at a time so the user can watch.
const NUMS = [-7, -4, -3, 0, 2, 3, 5, 7]; // sorted; has triple (-4,-3,7) and (-3,0,3)
const n = NUMS.length;
let i, L, R, comparisons, done, foundTriple;
function reset() {
i = 0; L = 1; R = n - 1; comparisons = 0; done = false; foundTriple = null;
document.getElementById('btn-step').disabled = false;
document.getElementById('btn-run').disabled = false;
setStatus('Press Step or Run to start.', '');
updateBadges(-1, -1, -1);
render(-1, -1, -1, []);
}
function render(fi, lb, rb, highlight) {
const row = document.getElementById('array-row');
row.innerHTML = '';
NUMS.forEach((v, idx) => {
const d = document.createElement('div');
d.className = 'cell';
if (highlight.includes(idx)) d.className += ' found';
else if (idx === fi) d.className += ' fixed';
else if (idx === lb) d.className += ' left';
else if (idx === rb) d.className += ' right';
d.textContent = v >= 0 ? '+' + v : '' + v;
row.appendChild(d);
});
}
function updateBadges(fi, lb, rb) {
document.getElementById('info-a').textContent = fi >= 0 ? fmt('a = %v%', { v: NUMS[fi] }) : 'a = —';
document.getElementById('info-b').textContent = lb >= 0 ? fmt('b = %v%', { v: NUMS[lb] }) : 'b = —';
document.getElementById('info-c').textContent = rb >= 0 ? fmt('c = %v%', { v: NUMS[rb] }) : 'c = —';
document.getElementById('info-cmp').textContent = fmt('comparisons: %comparisons%', { comparisons: comparisons });
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t; s.className = 'status ' + cls;
}
// Advance ONE full outer iteration (one fixed element i).
// Returns true if the outer loop has more work, false when done.
function stepOuter() {
if (done) return;
if (i >= n - 2) { done = true; return; }
const a = NUMS[i];
L = i + 1;
R = n - 1;
while (L < R) {
comparisons++;
const s = a + NUMS[L] + NUMS[R];
if (s === 0) {
foundTriple = [i, L, R];
done = true;
return;
} else if (s < 0) {
L++;
} else {
R--;
}
}
i++;
}
document.getElementById('btn-step').addEventListener('click', () => {
if (done) return;
stepOuter();
if (foundTriple) {
render(-1, -1, -1, foundTriple);
updateBadges(foundTriple[0], foundTriple[1], foundTriple[2]);
setStatus(fmt('Found: %v% + %v2% + %v3% = 0 (%comparisons% comparisons)', { v: NUMS[foundTriple[0]], v2: NUMS[foundTriple[1]], v3: NUMS[foundTriple[2]], comparisons: comparisons }), 'ok');
document.getElementById('btn-step').disabled = true;
document.getElementById('btn-run').disabled = true;
console.log('3SUM found:', NUMS[foundTriple[0]], NUMS[foundTriple[1]], NUMS[foundTriple[2]], '| comparisons:', comparisons);
} else if (done) {
render(-1, -1, -1, []);
updateBadges(-1, -1, -1);
setStatus(fmt('No zero-sum triple found. (%comparisons% comparisons)', { comparisons: comparisons }), 'bad');
document.getElementById('btn-step').disabled = true;
document.getElementById('btn-run').disabled = true;
} else {
render(i - 1, L, R, []);
updateBadges(i - 1, L, R);
setStatus(fmt('Fixing a = %v%, scanning with two pointers…', { v: NUMS[i - 1] }), '');
}
});
document.getElementById('btn-run').addEventListener('click', () => {
if (done) return;
while (!done) stepOuter();
if (foundTriple) {
render(-1, -1, -1, foundTriple);
updateBadges(foundTriple[0], foundTriple[1], foundTriple[2]);
setStatus(fmt('Found: %v% + %v2% + %v3% = 0 (%comparisons% comparisons)', { v: NUMS[foundTriple[0]], v2: NUMS[foundTriple[1]], v3: NUMS[foundTriple[2]], comparisons: comparisons }), 'ok');
console.log('3SUM found:', NUMS[foundTriple[0]], NUMS[foundTriple[1]], NUMS[foundTriple[2]], '| comparisons:', comparisons);
} else {
render(-1, -1, -1, []);
updateBadges(-1, -1, -1);
setStatus(fmt('No zero-sum triple found. (%comparisons% comparisons)', { comparisons: comparisons }), 'bad');
}
document.getElementById('btn-step').disabled = true;
document.getElementById('btn-run').disabled = true;
});
document.getElementById('btn-reset').addEventListener('click', reset);
reset();
<p class="hint">A sorted list of integers. The two-pointer algorithm fixes <b>a</b> and scans for <b>b + c = −a</b> with left/right pointers. Click <b>Step</b> to advance one outer iteration, or <b>Run</b> to finish. A found triple is highlighted in green.</p>
<div id="array-row" class="array-row"></div>
<div class="info-row">
<span id="info-a" class="badge badge-a">a = —</span>
<span id="info-b" class="badge badge-b">b = —</span>
<span id="info-c" class="badge badge-c">c = —</span>
<span id="info-cmp" class="badge badge-cmp">comparisons: 0</span>
</div>
<div id="status" class="status">Press Step or Run to start.</div>
<div class="btns">
<button id="btn-step" type="button">Step</button>
<button id="btn-run" type="button">Run</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.array-row { display: flex; gap: 5px; flex-wrap: wrap; margin: .5rem 0 .6rem; }
.cell {
width: 44px; height: 44px; display: flex; align-items: center; justify-content: center;
font: 700 14px ui-monospace, monospace; border-radius: 8px;
border: 2px solid #c5cdd6; background: #edf1f5; color: #2b3a4a;
transition: background .15s, border-color .15s;
}
.cell.fixed { background: #dbe4ff; border-color: #7091e6; color: #1d3557; }
.cell.left { background: #fff3cd; border-color: #f0a500; color: #5a3e00; }
.cell.right { background: #fde8c8; border-color: #e07b39; color: #5a2500; }
.cell.found { background: #c3f0ca; border-color: #2e9e51; color: #0a4020; }
.info-row { display: flex; gap: .4rem; flex-wrap: wrap; margin: .3rem 0; }
.badge { font: 600 12px ui-monospace, monospace; padding: .2rem .55rem; border-radius: 6px; }
.badge-a { background: #dbe4ff; color: #1d3557; }
.badge-b { background: #fff3cd; color: #5a3e00; }
.badge-c { background: #fde8c8; color: #5a2500; }
.badge-cmp { background: #f0f0f0; color: #333; }
.status { font-size: .95rem; font-weight: 600; margin: .55rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .95rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// 3SUM two-pointer algorithm:
// Given sorted array A, for each i, use pointers L=i+1, R=n-1 to find A[L]+A[R]==-A[i].
// Time: O(n^2). We step through outer iterations one at a time so the user can watch.
const NUMS = [-7, -4, -3, 0, 2, 3, 5, 7]; // sorted; has triple (-4,-3,7) and (-3,0,3)
const n = NUMS.length;
let i, L, R, comparisons, done, foundTriple;
function reset() {
i = 0; L = 1; R = n - 1; comparisons = 0; done = false; foundTriple = null;
document.getElementById('btn-step').disabled = false;
document.getElementById('btn-run').disabled = false;
setStatus('Press Step or Run to start.', '');
updateBadges(-1, -1, -1);
render(-1, -1, -1, []);
}
function render(fi, lb, rb, highlight) {
const row = document.getElementById('array-row');
row.innerHTML = '';
NUMS.forEach((v, idx) => {
const d = document.createElement('div');
d.className = 'cell';
if (highlight.includes(idx)) d.className += ' found';
else if (idx === fi) d.className += ' fixed';
else if (idx === lb) d.className += ' left';
else if (idx === rb) d.className += ' right';
d.textContent = v >= 0 ? '+' + v : '' + v;
row.appendChild(d);
});
}
function updateBadges(fi, lb, rb) {
document.getElementById('info-a').textContent = fi >= 0 ? fmt('a = %v%', { v: NUMS[fi] }) : 'a = —';
document.getElementById('info-b').textContent = lb >= 0 ? fmt('b = %v%', { v: NUMS[lb] }) : 'b = —';
document.getElementById('info-c').textContent = rb >= 0 ? fmt('c = %v%', { v: NUMS[rb] }) : 'c = —';
document.getElementById('info-cmp').textContent = fmt('comparisons: %comparisons%', { comparisons: comparisons });
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t; s.className = 'status ' + cls;
}
// Advance ONE full outer iteration (one fixed element i).
// Returns true if the outer loop has more work, false when done.
function stepOuter() {
if (done) return;
if (i >= n - 2) { done = true; return; }
const a = NUMS[i];
L = i + 1;
R = n - 1;
while (L < R) {
comparisons++;
const s = a + NUMS[L] + NUMS[R];
if (s === 0) {
foundTriple = [i, L, R];
done = true;
return;
} else if (s < 0) {
L++;
} else {
R--;
}
}
i++;
}
document.getElementById('btn-step').addEventListener('click', () => {
if (done) return;
stepOuter();
if (foundTriple) {
render(-1, -1, -1, foundTriple);
updateBadges(foundTriple[0], foundTriple[1], foundTriple[2]);
setStatus(fmt('Found: %v% + %v2% + %v3% = 0 (%comparisons% comparisons)', { v: NUMS[foundTriple[0]], v2: NUMS[foundTriple[1]], v3: NUMS[foundTriple[2]], comparisons: comparisons }), 'ok');
document.getElementById('btn-step').disabled = true;
document.getElementById('btn-run').disabled = true;
console.log('3SUM found:', NUMS[foundTriple[0]], NUMS[foundTriple[1]], NUMS[foundTriple[2]], '| comparisons:', comparisons);
} else if (done) {
render(-1, -1, -1, []);
updateBadges(-1, -1, -1);
setStatus(fmt('No zero-sum triple found. (%comparisons% comparisons)', { comparisons: comparisons }), 'bad');
document.getElementById('btn-step').disabled = true;
document.getElementById('btn-run').disabled = true;
} else {
render(i - 1, L, R, []);
updateBadges(i - 1, L, R);
setStatus(fmt('Fixing a = %v%, scanning with two pointers…', { v: NUMS[i - 1] }), '');
}
});
document.getElementById('btn-run').addEventListener('click', () => {
if (done) return;
while (!done) stepOuter();
if (foundTriple) {
render(-1, -1, -1, foundTriple);
updateBadges(foundTriple[0], foundTriple[1], foundTriple[2]);
setStatus(fmt('Found: %v% + %v2% + %v3% = 0 (%comparisons% comparisons)', { v: NUMS[foundTriple[0]], v2: NUMS[foundTriple[1]], v3: NUMS[foundTriple[2]], comparisons: comparisons }), 'ok');
console.log('3SUM found:', NUMS[foundTriple[0]], NUMS[foundTriple[1]], NUMS[foundTriple[2]], '| comparisons:', comparisons);
} else {
render(-1, -1, -1, []);
updateBadges(-1, -1, -1);
setStatus(fmt('No zero-sum triple found. (%comparisons% comparisons)', { comparisons: comparisons }), 'bad');
}
document.getElementById('btn-step').disabled = true;
document.getElementById('btn-run').disabled = true;
});
document.getElementById('btn-reset').addEventListener('click', reset);
reset();
Count the comparisons. For n numbers the algorithm makes at most n × n pointer steps — hence O(n2). Each time you add one more number, the worst-case work grows by an entire extra row. The open question is whether a fundamentally smarter strategy could trim that to, say, O(n1.9) — and so far, nobody has found one.
Comments
Loading comments...