Skip to content

Commit 1725c7b

Browse files
committed
refactor: Drive Linked List via an action log with staged insert/delete
Operations are now pure planners in lib/algorithms/linkedList.js that return a flat list of actions (mark, setNext, stageNode, lift, drop, removeNode, reorder, ...); the page is a generic executor that applies each action with a delay. Adding an operation no longer needs a new run-loop in the page. Insert and delete are now staged for clarity: - Insert: node floats above its slot, wires next then prev, drops in - Delete: node is flagged, lifts out, predecessor bypasses it, removed Also namespace SVG sibling keys to fix a duplicate-key warning (node ids collided with pointer-caption indices), and add a real thumbnail.
1 parent f200e02 commit 1725c7b

4 files changed

Lines changed: 288 additions & 157 deletions

File tree

public/images/linked-list.png

-172 KB
Loading

src/app/linked-list/canvas.jsx

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import Arrow from './arrow';
44
const VB_W = 240;
55
const VB_H = 90;
66
const PAD = 16;
7-
const Y = 48;
7+
const Y = 56; // resting row
8+
const LIFT = 26; // how high a staged node floats above the row
89

9-
export default function Canvas({ nodes, nextOf, prevOf, listType, nodeState = {}, pointers = [] }) {
10+
export default function Canvas({ nodes, nextOf, prevOf, listType, nodeState = {}, pointers = [], liftedId = null }) {
1011
const count = nodes.length;
1112
const spacing = count > 1
1213
? Math.min(46, (VB_W - 2 * PAD - NODE_W) / (count - 1))
@@ -15,12 +16,11 @@ export default function Canvas({ nodes, nextOf, prevOf, listType, nodeState = {}
1516
const startX = Math.max(PAD, (VB_W - totalW) / 2);
1617

1718
const posById = {};
18-
nodes.forEach((n, i) => { posById[n.id] = { x: startX + i * spacing, i }; });
19+
nodes.forEach((n, i) => {
20+
posById[n.id] = { x: startX + i * spacing, y: n.id === liftedId ? Y - LIFT : Y, i };
21+
});
1922

2023
const doubly = listType === 1;
21-
const nextY = doubly ? Y - 3 : Y;
22-
const prevY = Y + 3;
23-
2424
const tail = nodes[count - 1];
2525

2626
return (
@@ -42,12 +42,14 @@ export default function Canvas({ nodes, nextOf, prevOf, listType, nodeState = {}
4242
const src = posById[n.id];
4343
const dst = posById[target];
4444
if (!dst) return null;
45-
// forward (head->tail) points from the next-cell to the node's left;
45+
const y1 = src.y + (doubly ? -3 : 0);
46+
const y2 = dst.y + (doubly ? -3 : 0);
47+
// forward link exits the next-cell to the target's left edge;
4648
// a flipped (leftward) link during reverse points the other way.
4749
if (dst.i > src.i) {
48-
return <Arrow key={'nx' + n.id} x1={src.x + NODE_W} y1={nextY} x2={dst.x} y2={nextY} />;
50+
return <Arrow key={'nx' + n.id} x1={src.x + NODE_W} y1={y1} x2={dst.x} y2={y2} />;
4951
}
50-
return <Arrow key={'nx' + n.id} x1={src.x} y1={nextY} x2={dst.x + NODE_W} y2={nextY} />;
52+
return <Arrow key={'nx' + n.id} x1={src.x} y1={y1} x2={dst.x + NODE_W} y2={y2} />;
5153
})}
5254

5355
{/* prev pointers (doubly only) */}
@@ -56,45 +58,45 @@ export default function Canvas({ nodes, nextOf, prevOf, listType, nodeState = {}
5658
if (target == null) return null;
5759
const src = posById[n.id];
5860
const dst = posById[target];
59-
if (!dst || Math.abs(dst.i - src.i) !== 1) return null;
60-
return <Arrow key={'pv' + n.id} x1={src.x} y1={prevY} x2={dst.x + NODE_W} y2={prevY} color="#94a3b8" />;
61+
if (!dst) return null;
62+
return <Arrow key={'pv' + n.id} x1={src.x} y1={src.y + 3} x2={dst.x + NODE_W} y2={dst.y + 3} color="#94a3b8" />;
6163
})}
6264

6365
{/* null terminator after the tail */}
6466
{tail && nextOf[tail.id] == null && (
65-
<text x={posById[tail.id].x + NODE_W + 9} y={Y} textAnchor="middle" dominantBaseline="central"
67+
<text x={posById[tail.id].x + NODE_W + 9} y={posById[tail.id].y} textAnchor="middle" dominantBaseline="central"
6668
style={{ font: '4px sans-serif' }} fill="#94a3b8">null</text>
6769
)}
6870

6971
{/* null terminator before the head (doubly: head.prev = null) */}
7072
{doubly && nodes[0] && prevOf[nodes[0].id] == null && (
71-
<text x={posById[nodes[0].id].x - 9} y={Y} textAnchor="middle" dominantBaseline="central"
73+
<text x={posById[nodes[0].id].x - 9} y={posById[nodes[0].id].y} textAnchor="middle" dominantBaseline="central"
7274
style={{ font: '4px sans-serif' }} fill="#94a3b8">null</text>
7375
)}
7476

7577
{/* nodes */}
7678
{nodes.map((n) => (
77-
<Node key={n.id} x={posById[n.id].x} y={Y} value={n.value}
79+
<Node key={'nd' + n.id} x={posById[n.id].x} y={posById[n.id].y} value={n.value}
7880
listType={listType} state={nodeState[n.id]}
7981
isHead={prevOf[n.id] == null} isTail={nextOf[n.id] == null} />
8082
))}
8183

82-
{/* head / tail labels */}
83-
{nodes[0] && (
84+
{/* head / tail labels (skip while a node is lifted to avoid clutter) */}
85+
{nodes[0] && nodes[0].id !== liftedId && (
8486
<text x={posById[nodes[0].id].x + NODE_W / 2} y={Y - NODE_H / 2 - 4}
8587
textAnchor="middle" style={{ font: '3.5px sans-serif', fontWeight: 600 }} fill="#475569">head</text>
8688
)}
87-
{tail && count > 1 && (
89+
{tail && count > 1 && tail.id !== liftedId && (
8890
<text x={posById[tail.id].x + NODE_W / 2} y={Y - NODE_H / 2 - 4}
8991
textAnchor="middle" style={{ font: '3.5px sans-serif', fontWeight: 600 }} fill="#475569">tail</text>
9092
)}
9193

92-
{/* operation pointer captions (curr / prev / next / slow / fast) */}
94+
{/* operation pointer captions (curr / prev / next) */}
9395
{pointers.map((p, idx) => {
9496
if (p.nodeId == null || !posById[p.nodeId]) return null;
9597
const px = posById[p.nodeId].x + NODE_W / 2;
9698
return (
97-
<text key={idx} x={px} y={Y + NODE_H / 2 + 7} textAnchor="middle"
99+
<text key={'pt' + idx} x={px} y={Y + NODE_H / 2 + 7} textAnchor="middle"
98100
style={{ font: '4px sans-serif', fontWeight: 700 }} fill="#0f172a">
99101
{p.label}
100102
</text>

src/app/linked-list/page.jsx

Lines changed: 76 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,17 @@
22

33
import { useState, useRef } from 'react';
44
import Navbar from '@/components/navbar';
5-
import { buildList, linkify, makeNode, randomValues } from '@/lib/algorithms/linkedList';
5+
import {
6+
buildList,
7+
linkify,
8+
randomValues,
9+
reduceStructure,
10+
insertActions,
11+
deleteByValueActions,
12+
deleteByIndexActions,
13+
searchActions,
14+
reverseActions,
15+
} from '@/lib/algorithms/linkedList';
616
import Canvas from './canvas';
717
import Menu from './menu';
818

@@ -20,6 +30,7 @@ export default function LinkedList() {
2030
const [listType, setListType] = useState(0);
2131
const [nodeState, setNodeState] = useState({});
2232
const [pointers, setPointers] = useState([]);
33+
const [liftedId, setLiftedId] = useState(null);
2334
const [isRunning, setIsRunning] = useState(false);
2435

2536
const nodesRef = useRef(initial.nodes);
@@ -33,160 +44,91 @@ export default function LinkedList() {
3344
const indexRef = useRef('1');
3445

3546
// --- state appliers (keep refs and React state in sync) ---
36-
const applyNodes = (arr) => { nodesRef.current = arr; setNodes(arr); };
37-
const applyLinks = (nx, pv) => {
47+
const applyStructure = ({ nodes: n, nextOf: nx, prevOf: pv }) => {
48+
nodesRef.current = n; setNodes(n);
3849
nextOfRef.current = nx; setNextOf(nx);
3950
prevOfRef.current = pv; setPrevOf(pv);
4051
};
41-
const relink = (arr) => {
42-
const { nextOf: nx, prevOf: pv } = linkify(arr);
43-
applyNodes(arr);
44-
applyLinks(nx, pv);
52+
const relink = (arr) => applyStructure({ nodes: arr, ...linkify(arr) });
53+
54+
// Apply one action: visual actions touch marks/pointers; everything else is
55+
// a structural change handled by the lib reducer.
56+
const applyAction = (action) => {
57+
switch (action.type) {
58+
case 'mark':
59+
setNodeState((s) => ({ ...s, [action.id]: action.state }));
60+
break;
61+
case 'pointers':
62+
setPointers(action.items);
63+
break;
64+
case 'lift':
65+
setLiftedId(action.id);
66+
break;
67+
case 'drop':
68+
setLiftedId(null);
69+
break;
70+
case 'clear':
71+
setNodeState({});
72+
setPointers([]);
73+
setLiftedId(null);
74+
break;
75+
default:
76+
if (action.type === 'stageNode') setLiftedId(action.id);
77+
applyStructure(reduceStructure(
78+
{ nodes: nodesRef.current, nextOf: nextOfRef.current, prevOf: prevOfRef.current },
79+
action,
80+
));
81+
}
4582
};
46-
const mark = (id, kind) => setNodeState((s) => ({ ...s, [id]: kind }));
47-
const clearMarks = () => { setNodeState({}); setPointers([]); };
4883

49-
const begin = () => {
50-
if (isRunningRef.current) return false;
84+
const runActions = async (actions) => {
85+
if (!actions.length || isRunningRef.current) return;
5186
isRunningRef.current = true;
5287
setIsRunning(true);
53-
clearMarks();
54-
return true;
55-
};
56-
const finish = () => { isRunningRef.current = false; setIsRunning(false); };
57-
58-
// --- operations ---
59-
const runInsert = async (pos) => {
60-
if (!begin()) return;
61-
const arr = [...nodesRef.current];
62-
const idx = pos === 'head' ? 0
63-
: pos === 'tail' ? arr.length
64-
: Math.max(0, Math.min(Number(pos) || 0, arr.length));
65-
const value = Number(valueRef.current);
66-
67-
for (let i = 0; i < idx; i++) {
68-
mark(arr[i].id, 'active');
69-
await sleep(speedRef.current);
70-
mark(arr[i].id, 'done');
71-
}
72-
const node = makeNode(Number.isFinite(value) ? value : 0);
73-
arr.splice(idx, 0, node);
74-
relink(arr);
75-
mark(node.id, 'found');
76-
await sleep(speedRef.current);
77-
clearMarks();
78-
finish();
79-
};
80-
81-
const runDeleteValue = async (value) => {
82-
if (!begin()) return;
83-
const arr = [...nodesRef.current];
84-
let idx = -1;
85-
for (let i = 0; i < arr.length; i++) {
86-
mark(arr[i].id, 'active');
87-
await sleep(speedRef.current);
88-
if (arr[i].value === value) { idx = i; break; }
89-
mark(arr[i].id, 'done');
90-
}
91-
if (idx >= 0) {
92-
mark(arr[idx].id, 'remove');
93-
await sleep(speedRef.current);
94-
arr.splice(idx, 1);
95-
relink(arr);
96-
await sleep(speedRef.current);
97-
}
98-
clearMarks();
99-
finish();
100-
};
101-
102-
const runDeleteIndex = async (index) => {
103-
if (!begin()) return;
104-
const arr = [...nodesRef.current];
105-
const idx = Number(index);
106-
if (idx >= 0 && idx < arr.length) {
107-
for (let i = 0; i < idx; i++) {
108-
mark(arr[i].id, 'active');
109-
await sleep(speedRef.current);
110-
mark(arr[i].id, 'done');
111-
}
112-
mark(arr[idx].id, 'remove');
113-
await sleep(speedRef.current);
114-
arr.splice(idx, 1);
115-
relink(arr);
116-
await sleep(speedRef.current);
117-
}
118-
clearMarks();
119-
finish();
120-
};
121-
122-
const runSearch = async (value) => {
123-
if (!begin()) return;
124-
const arr = [...nodesRef.current];
125-
let found = false;
126-
for (let i = 0; i < arr.length; i++) {
127-
mark(arr[i].id, 'active');
128-
await sleep(speedRef.current);
129-
if (arr[i].value === value) { mark(arr[i].id, 'found'); found = true; break; }
130-
mark(arr[i].id, 'done');
131-
}
132-
await sleep(speedRef.current);
133-
if (found) await sleep(speedRef.current);
134-
clearMarks();
135-
finish();
136-
};
137-
138-
const runReverse = async () => {
139-
if (!begin()) return;
140-
const arr = [...nodesRef.current];
141-
if (arr.length < 2) { finish(); return; }
142-
143-
const temp = { ...linkify(arr).nextOf };
144-
let prev = null;
145-
for (let i = 0; i < arr.length; i++) {
146-
const curr = arr[i].id;
147-
const next = i + 1 < arr.length ? arr[i + 1].id : null;
148-
setPointers([
149-
...(prev != null ? [{ label: 'prev', nodeId: prev }] : []),
150-
{ label: 'curr', nodeId: curr },
151-
...(next != null ? [{ label: 'next', nodeId: next }] : []),
152-
]);
153-
mark(curr, 'active');
154-
await sleep(speedRef.current);
155-
temp[curr] = prev;
156-
applyLinks({ ...temp }, prevOfRef.current);
88+
setNodeState({});
89+
setPointers([]);
90+
setLiftedId(null);
91+
for (const action of actions) {
92+
applyAction(action);
15793
await sleep(speedRef.current);
158-
mark(curr, 'done');
159-
prev = curr;
16094
}
161-
// settle: physical order mirrors logical, forward links restored
162-
const reversed = [...arr].reverse();
163-
relink(reversed);
164-
setPointers([]);
165-
await sleep(speedRef.current);
166-
clearMarks();
167-
finish();
95+
isRunningRef.current = false;
96+
setIsRunning(false);
16897
};
16998

17099
const handleVisualize = () => {
171100
const op = operationRef.current;
172-
if (op === 0) runInsert('head');
173-
else if (op === 1) runInsert('tail');
174-
else if (op === 2) runInsert(indexRef.current);
175-
else if (op === 3) runDeleteValue(Number(valueRef.current));
176-
else if (op === 4) runDeleteIndex(indexRef.current);
177-
else if (op === 5) runSearch(Number(valueRef.current));
178-
else if (op === 6) runReverse();
101+
const list = {
102+
nodes: nodesRef.current,
103+
nextOf: nextOfRef.current,
104+
prevOf: prevOfRef.current,
105+
listType: listTypeRef.current,
106+
};
107+
const value = Number(valueRef.current);
108+
let actions = [];
109+
if (op === 0) actions = insertActions(list, 'head', value);
110+
else if (op === 1) actions = insertActions(list, 'tail', value);
111+
else if (op === 2) actions = insertActions(list, indexRef.current, value);
112+
else if (op === 3) actions = deleteByValueActions(list, value);
113+
else if (op === 4) actions = deleteByIndexActions(list, indexRef.current);
114+
else if (op === 5) actions = searchActions(list, value);
115+
else if (op === 6) actions = reverseActions(list);
116+
runActions(actions);
179117
};
180118

181119
const handleRandomize = () => {
182120
if (isRunningRef.current) return;
183-
clearMarks();
121+
setNodeState({});
122+
setPointers([]);
123+
setLiftedId(null);
184124
relink(buildList(randomValues(5)).nodes);
185125
};
186126

187127
const handleReset = () => {
188128
if (isRunningRef.current) return;
189-
clearMarks();
129+
setNodeState({});
130+
setPointers([]);
131+
setLiftedId(null);
190132
relink(nodesRef.current.map((n) => ({ ...n })));
191133
};
192134

@@ -214,6 +156,7 @@ export default function LinkedList() {
214156
listType={listType}
215157
nodeState={nodeState}
216158
pointers={pointers}
159+
liftedId={liftedId}
217160
/>
218161
</div>
219162
</div>

0 commit comments

Comments
 (0)