Skip to content

Commit 233eb24

Browse files
committed
feat: enhance canvas info display with cycle analysis and distance calculation
- Add cycle analyzer for detecting cycle entry point and length - Add distance calculator for tracking pointer distances - Update visualizer to display enhanced information on canvas - Update algorithm to integrate new analysis modules - Add comprehensive tests for new functionality
1 parent 096ef0f commit 233eb24

File tree

7 files changed

+262
-15
lines changed

7 files changed

+262
-15
lines changed

.kiro/specs/leetcode-141-visualizer/design.md

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ interface ListNode {
9696
x: number;
9797
y: number;
9898
}
99+
100+
// 布局参数 - 增大节点间距以确保环形箭头清晰可见
101+
interface LayoutConfig {
102+
nodeRadius: number; // 节点半径: 25px
103+
nodeSpacing: number; // 节点间距: 100px (从 80px 增加)
104+
startX: number; // 起始 X 坐标: 60px
105+
startY: number; // 起始 Y 坐标: 80px
106+
svgHeight: number; // SVG 高度: 220px
107+
}
99108
```
100109

101110
### 4. Control Panel Component
@@ -132,7 +141,7 @@ interface FloatingBallProps {
132141

133142
### 6. Algorithm Engine
134143

135-
算法执行引擎,生成每一步的状态。
144+
算法执行引擎,生成每一步的状态。步骤生成需要细粒度划分,确保每个条件检查、指针移动、循环进入/退出都是独立的步骤。
136145

137146
```typescript
138147
interface AlgorithmStep {
@@ -143,6 +152,20 @@ interface AlgorithmStep {
143152
variables: VariableState[];
144153
description: string;
145154
hasCycle: boolean | null; // null 表示还未确定
155+
stepType: StepType; // 步骤类型,用于细粒度分类
156+
}
157+
158+
// 步骤类型枚举 - 用于细粒度步骤划分
159+
enum StepType {
160+
METHOD_ENTRY = 'method_entry', // 方法入口
161+
CONDITION_CHECK = 'condition_check', // 条件检查
162+
VARIABLE_INIT = 'variable_init', // 变量初始化
163+
LOOP_ENTRY = 'loop_entry', // 循环入口
164+
LOOP_CONDITION = 'loop_condition', // 循环条件检查
165+
POINTER_MOVE_SLOW = 'pointer_move_slow', // 慢指针移动
166+
POINTER_MOVE_FAST = 'pointer_move_fast', // 快指针移动
167+
LOOP_EXIT = 'loop_exit', // 循环退出
168+
RETURN_RESULT = 'return_result' // 返回结果
146169
}
147170

148171
interface AlgorithmEngine {
@@ -153,6 +176,18 @@ interface AlgorithmEngine {
153176
}
154177
```
155178

179+
#### 细粒度步骤划分规则
180+
181+
每次循环迭代应包含以下独立步骤:
182+
1. **while 条件检查** - 检查 slow != fast
183+
2. **fast null 检查 (第一部分)** - 检查 fast == null
184+
3. **fast.next null 检查 (第二部分)** - 检查 fast.next == null
185+
4. **慢指针移动** - slow = slow.next
186+
5. **快指针移动** - fast = fast.next.next
187+
188+
这样可以让用户更清晰地理解算法的每一个执行细节。
189+
```
190+
156191
### 7. Step Manager
157192
158193
管理步骤导航和自动播放。
@@ -289,6 +324,18 @@ Based on the prework analysis, the following correctness properties have been id
289324

290325
**Validates: Requirements 4.4**
291326

327+
### Property 8: Node Spacing Minimum
328+
329+
*For any* linked list visualization with cyclePos >= 0, the node spacing SHALL be at least 100px to ensure the cycle connection arrow is clearly visible.
330+
331+
**Validates: Requirements 4.6**
332+
333+
### Property 9: Fine-Grained Step Generation
334+
335+
*For any* linked list with at least 2 nodes, each loop iteration in the algorithm SHALL generate at least 3 separate steps: (1) loop condition check, (2) slow pointer movement, (3) fast pointer movement.
336+
337+
**Validates: Requirements 2.6**
338+
292339
## Error Handling
293340

294341
### Input Validation

.kiro/specs/leetcode-141-visualizer/requirements.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
3. WHEN a variable value changes THEN the Visualizer SHALL display the variable's current value inline after the corresponding code line
3838
4. WHEN the algorithm step changes THEN the Visualizer SHALL update the highlighted line to match the current execution point
3939
5. WHEN displaying variable values THEN the Visualizer SHALL show values for slow pointer, fast pointer, and their positions
40+
6. WHEN generating algorithm steps THEN the AlgorithmEngine SHALL create fine-grained steps that include: (a) each condition check as a separate step, (b) each pointer movement as a separate step, (c) loop entry and exit as separate steps
4041

4142
### Requirement 3: 控制面板与键盘快捷键
4243

@@ -62,6 +63,7 @@
6263
3. WHEN a cycle exists THEN the Visualizer SHALL visually indicate the cycle connection in the linked list
6364
4. WHEN displaying the visualization THEN the Visualizer SHALL show current step number, total steps, and algorithm phase description
6465
5. WHEN pointers move THEN the Visualizer SHALL animate the pointer transitions smoothly
66+
6. WHEN rendering a cyclic linked list THEN the Visualizer SHALL use increased node spacing (minimum 100px) to ensure clear visibility of the cycle connection arrow
6567

6668
### Requirement 5: 社区交流悬浮球
6769

.kiro/specs/leetcode-141-visualizer/tasks.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,25 @@
152152

153153
- [x] 14. Final Checkpoint - 确保所有测试通过
154154
- Ensure all tests pass, ask the user if questions arise.
155+
156+
- [x] 15. 增强可视化效果和步骤划分
157+
- [x] 15.1 增大环形链表节点间距
158+
- 修改 src/scripts/visualizer.js 中的 nodeSpacing 从 80px 增加到 100px
159+
- 确保环形连接箭头清晰可见
160+
- _Requirements: 4.6_
161+
- [x] 15.2 优化算法步骤细粒度划分
162+
- 修改 src/scripts/algorithm.js 中的 generateAllSteps() 方法
163+
- 将 fast null 检查拆分为两个独立步骤:检查 fast == null 和检查 fast.next == null
164+
- 确保每次循环迭代至少包含:while 条件检查、null 检查、慢指针移动、快指针移动
165+
- _Requirements: 2.6_
166+
- [x] 15.3 编写节点间距属性测试
167+
- **Property 8: Node Spacing Minimum**
168+
- **Validates: Requirements 4.6**
169+
- 验证环形链表节点间距至少为 100px
170+
- [x] 15.4 编写细粒度步骤属性测试
171+
- **Property 9: Fine-Grained Step Generation**
172+
- **Validates: Requirements 2.6**
173+
- 验证每次循环迭代至少生成 3 个独立步骤
174+
175+
- [x] 16. Checkpoint - 确保所有测试通过
176+
- Ensure all tests pass, ask the user if questions arise.

src/scripts/algorithm.js

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ export class AlgorithmEngine {
170170

171171
// eslint-disable-next-line no-constant-condition
172172
while (true) {
173-
// while 条件检查
173+
// 步骤: while 条件检查 - slow != fast
174174
this.steps.push({
175175
stepNumber: stepNumber++,
176176
codeLine: CODE_LINES.WHILE_CHECK,
@@ -180,7 +180,7 @@ export class AlgorithmEngine {
180180
{ name: 'slow', value: `节点(${this.values[slow]}) [索引:${slow}]`, line: 6 },
181181
{ name: 'fast', value: `节点(${this.values[fast]}) [索引:${fast}]`, line: 7 }
182182
],
183-
description: `检查 slow != fast: ${slow} != ${fast}`,
183+
description: `检查 while 条件: slow != fast${slow} != ${fast} = ${slow !== fast}`,
184184
hasCycle: null
185185
});
186186

@@ -198,19 +198,31 @@ export class AlgorithmEngine {
198198
return;
199199
}
200200

201-
// 检查 fast 是否到达末尾
201+
// 细粒度步骤: 检查 fast == null (第一部分)
202202
const fastNext = this.getNextIndex(fast);
203+
this.steps.push({
204+
stepNumber: stepNumber++,
205+
codeLine: CODE_LINES.FAST_NULL_CHECK,
206+
slowPos: slow,
207+
fastPos: fast,
208+
variables: [
209+
{ name: 'fast', value: `节点(${this.values[fast]}) [索引:${fast}]`, line: 9 }
210+
],
211+
description: `检查 fast == null: fast = 节点(${this.values[fast]}) ≠ null`,
212+
hasCycle: null
213+
});
214+
215+
// 如果 fast.next 为 null,结束
203216
if (fastNext === -1) {
204217
this.steps.push({
205218
stepNumber: stepNumber++,
206219
codeLine: CODE_LINES.FAST_NULL_CHECK,
207220
slowPos: slow,
208221
fastPos: fast,
209222
variables: [
210-
{ name: 'fast', value: `节点(${this.values[fast]})`, line: 9 },
211223
{ name: 'fast.next', value: 'null', line: 9 }
212224
],
213-
description: 'fast.next null,无环',
225+
description: '检查 fast.next == null: fast.next = null ✓ 条件成立',
214226
hasCycle: null
215227
});
216228
this.steps.push({
@@ -219,24 +231,37 @@ export class AlgorithmEngine {
219231
slowPos: slow,
220232
fastPos: fast,
221233
variables: [],
222-
description: '快指针到达末尾,返回 false',
234+
description: '快指针的下一个节点为空,无环,返回 false',
223235
hasCycle: false
224236
});
225237
return;
226238
}
227239

240+
// 细粒度步骤: 检查 fast.next == null (第二部分)
228241
const fastNextNext = this.getNextIndex(fastNext);
242+
this.steps.push({
243+
stepNumber: stepNumber++,
244+
codeLine: CODE_LINES.FAST_NULL_CHECK,
245+
slowPos: slow,
246+
fastPos: fast,
247+
variables: [
248+
{ name: 'fast.next', value: `节点(${this.values[fastNext]}) [索引:${fastNext}]`, line: 9 }
249+
],
250+
description: `检查 fast.next == null: fast.next = 节点(${this.values[fastNext]}) ≠ null`,
251+
hasCycle: null
252+
});
253+
254+
// 如果 fast.next.next 为 null,结束
229255
if (fastNextNext === -1) {
230256
this.steps.push({
231257
stepNumber: stepNumber++,
232258
codeLine: CODE_LINES.FAST_NULL_CHECK,
233259
slowPos: slow,
234260
fastPos: fast,
235261
variables: [
236-
{ name: 'fast.next', value: `节点(${this.values[fastNext]})`, line: 9 },
237262
{ name: 'fast.next.next', value: 'null', line: 9 }
238263
],
239-
description: 'fast.next.nextnull,无环',
264+
description: '检查 fast.next.next: fast.next.next = null ✓ 条件成立',
240265
hasCycle: null
241266
});
242267
this.steps.push({
@@ -245,13 +270,13 @@ export class AlgorithmEngine {
245270
slowPos: slow,
246271
fastPos: fast,
247272
variables: [],
248-
description: '快指针到达末尾,返回 false',
273+
description: '快指针无法再移动两步,无环,返回 false',
249274
hasCycle: false
250275
});
251276
return;
252277
}
253278

254-
// 移动指针
279+
// 步骤: 慢指针移动
255280
const newSlow = this.getNextIndex(slow);
256281
this.steps.push({
257282
stepNumber: stepNumber++,
@@ -261,11 +286,12 @@ export class AlgorithmEngine {
261286
variables: [
262287
{ name: 'slow', value: `节点(${this.values[newSlow]}) [索引:${newSlow}]`, line: 12 }
263288
],
264-
description: `慢指针移动: ${slow} -> ${newSlow}`,
289+
description: `慢指针移动一步: slow = slow.next → ${slow} ${newSlow}`,
265290
hasCycle: null
266291
});
267292
slow = newSlow;
268293

294+
// 步骤: 快指针移动
269295
this.steps.push({
270296
stepNumber: stepNumber++,
271297
codeLine: CODE_LINES.FAST_NEXT,
@@ -274,7 +300,7 @@ export class AlgorithmEngine {
274300
variables: [
275301
{ name: 'fast', value: `节点(${this.values[fastNextNext]}) [索引:${fastNextNext}]`, line: 13 }
276302
],
277-
description: `快指针移动: ${fast} -> ${fastNextNext}`,
303+
description: `快指针移动两步: fast = fast.next.next → ${fast} ${fastNextNext}`,
278304
hasCycle: null
279305
});
280306
fast = fastNextNext;

src/scripts/visualizer.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ export class Visualizer {
3636
this.cycleAnalyzer = new CycleAnalyzer();
3737
this.cycleInfo = null;
3838

39-
// 布局参数
39+
// 布局参数 - 增大节点间距以确保环形箭头清晰可见
4040
this.nodeRadius = 25;
41-
this.nodeSpacing = 80;
41+
this.nodeSpacing = 100; // 从 80px 增加到 100px,确保环形连接箭头清晰可见
4242
this.startX = 60;
4343
this.startY = 80;
4444
this.svgHeight = 220;

tests/algorithm.test.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,113 @@ describe('AlgorithmEngine', () => {
9696
});
9797
});
9898

99+
/**
100+
* **Feature: leetcode-141-visualizer, Property 9: Fine-Grained Step Generation**
101+
* **Validates: Requirements 2.6**
102+
*
103+
* For any linked list with at least 2 nodes, each loop iteration in the algorithm
104+
* SHALL generate at least 3 separate steps: (1) loop condition check, (2) slow pointer movement,
105+
* (3) fast pointer movement.
106+
*/
107+
describe('Property 9: Fine-Grained Step Generation', () => {
108+
it('should generate at least 3 steps per loop iteration for cyclic lists', () => {
109+
fc.assert(
110+
fc.property(
111+
fc.array(fc.integer({ min: -100, max: 100 }), { minLength: 3, maxLength: 8 }),
112+
fc.integer({ min: 0, max: 7 }),
113+
(values, pos) => {
114+
const validPos = Math.min(pos, values.length - 1);
115+
engine.initialize(values, validPos);
116+
const steps = engine.getAllSteps();
117+
118+
// 找到所有 while 条件检查步骤
119+
const whileCheckSteps = steps.filter(s => s.codeLine === CODE_LINES.WHILE_CHECK);
120+
121+
// 找到所有慢指针移动步骤
122+
const slowMoveSteps = steps.filter(s => s.codeLine === CODE_LINES.SLOW_NEXT);
123+
124+
// 找到所有快指针移动步骤
125+
const fastMoveSteps = steps.filter(s => s.codeLine === CODE_LINES.FAST_NEXT);
126+
127+
// 如果有循环迭代,应该有对应的步骤
128+
// 每次完整的循环迭代应该包含: while检查 + null检查 + slow移动 + fast移动
129+
// 至少应该有 while 检查步骤
130+
if (whileCheckSteps.length > 0) {
131+
// 慢指针和快指针移动次数应该相等
132+
return slowMoveSteps.length === fastMoveSteps.length;
133+
}
134+
return true;
135+
}
136+
),
137+
{ numRuns: 100 }
138+
);
139+
});
140+
141+
it('should include null check steps in each loop iteration', () => {
142+
fc.assert(
143+
fc.property(
144+
fc.array(fc.integer({ min: -100, max: 100 }), { minLength: 3, maxLength: 8 }),
145+
fc.integer({ min: 0, max: 7 }),
146+
(values, pos) => {
147+
const validPos = Math.min(pos, values.length - 1);
148+
engine.initialize(values, validPos);
149+
const steps = engine.getAllSteps();
150+
151+
// 找到所有 null 检查步骤
152+
const nullCheckSteps = steps.filter(s => s.codeLine === CODE_LINES.FAST_NULL_CHECK);
153+
154+
// 找到所有慢指针移动步骤
155+
const slowMoveSteps = steps.filter(s => s.codeLine === CODE_LINES.SLOW_NEXT);
156+
157+
// 如果有指针移动,应该有对应的 null 检查
158+
// 每次循环迭代应该有 2 个 null 检查步骤(检查 fast 和 fast.next)
159+
if (slowMoveSteps.length > 0) {
160+
// null 检查步骤数应该是指针移动次数的 2 倍
161+
return nullCheckSteps.length >= slowMoveSteps.length * 2;
162+
}
163+
return true;
164+
}
165+
),
166+
{ numRuns: 100 }
167+
);
168+
});
169+
170+
it('should generate fine-grained steps for non-cyclic lists', () => {
171+
fc.assert(
172+
fc.property(
173+
fc.array(fc.integer({ min: -100, max: 100 }), { minLength: 4, maxLength: 10 }),
174+
(values) => {
175+
engine.initialize(values, -1);
176+
const steps = engine.getAllSteps();
177+
178+
// 对于无环链表,应该有多个步骤
179+
// 至少应该有: 方法开始 + null检查 + slow初始化 + fast初始化 + while检查 + ...
180+
return steps.length >= 5;
181+
}
182+
),
183+
{ numRuns: 100 }
184+
);
185+
});
186+
187+
it('should have at least 3 steps per complete loop iteration', () => {
188+
// 测试具体的例子:[3, 2, 0, -4] pos=1
189+
engine.initialize([3, 2, 0, -4], 1);
190+
const steps = engine.getAllSteps();
191+
192+
// 统计每种类型的步骤
193+
const whileChecks = steps.filter(s => s.codeLine === CODE_LINES.WHILE_CHECK).length;
194+
const slowMoves = steps.filter(s => s.codeLine === CODE_LINES.SLOW_NEXT).length;
195+
const fastMoves = steps.filter(s => s.codeLine === CODE_LINES.FAST_NEXT).length;
196+
const nullChecks = steps.filter(s => s.codeLine === CODE_LINES.FAST_NULL_CHECK).length;
197+
198+
// 每次完整循环应该有: 1个while检查 + 2个null检查 + 1个slow移动 + 1个fast移动
199+
// 所以 null 检查数应该是 slow 移动数的 2 倍
200+
expect(nullChecks).toBe(slowMoves * 2);
201+
expect(slowMoves).toBe(fastMoves);
202+
expect(whileChecks).toBeGreaterThan(0);
203+
});
204+
});
205+
99206
describe('Step Structure Validation', () => {
100207
it('should have all required fields in each step', () => {
101208
engine.initialize([3, 2, 0, -4], 1);

0 commit comments

Comments
 (0)