Skip to content

Commit 8a3cd16

Browse files
committed
fix: 修复比较标签底部文字被节点遮挡的问题
1 parent 4ebf7ee commit 8a3cd16

File tree

4 files changed

+131
-72
lines changed

4 files changed

+131
-72
lines changed

src/algorithm/diameterAlgorithm.ts

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,55 @@ export function generateAlgorithmSteps(root: D3TreeNode | null): AlgorithmStep[]
185185
return 0;
186186
}
187187

188+
// ========== 处理 isNull 的 D3TreeNode(可视化用的空节点) ==========
189+
if (node.isNull) {
190+
const currentPath = [...path, node.id];
191+
192+
// 进入空节点
193+
steps.push(createStep(
194+
stepIndex++,
195+
`📥 递归进入 NULL 节点`,
196+
node.id, currentPath, [], [...diameterPath], globalDiameter,
197+
[
198+
{ name: 'diameter', value: String(globalDiameter), line: 2 },
199+
{ name: 'node', value: 'null', line: 9 },
200+
],
201+
9,
202+
'recursion-enter',
203+
{ toNodeId: node.id, value: 'null' }
204+
));
205+
206+
// 检查 node == null,返回 0
207+
steps.push(createStep(
208+
stepIndex++,
209+
`🔍 检查 node == null 为 true,返回深度 0`,
210+
node.id, currentPath, [], [...diameterPath], globalDiameter,
211+
[
212+
{ name: 'diameter', value: String(globalDiameter), line: 2 },
213+
{ name: 'return', value: '0', line: 11 },
214+
],
215+
10,
216+
'return-value',
217+
{ fromNodeId: node.id, toNodeId: parentId || undefined, value: 0 }
218+
));
219+
220+
// 退出空节点
221+
steps.push(createStep(
222+
stepIndex++,
223+
`📤 递归退出 NULL 节点,返回深度 0`,
224+
node.id, currentPath, [], [...diameterPath], globalDiameter,
225+
[
226+
{ name: 'diameter', value: String(globalDiameter), line: 2 },
227+
{ name: 'return', value: '0', line: 11 },
228+
],
229+
11,
230+
'recursion-exit',
231+
{ fromNodeId: node.id, toNodeId: parentId || undefined, value: 0 }
232+
));
233+
234+
return 0;
235+
}
236+
188237
const currentPath = [...path, node.id];
189238

190239
// ========== 递归进入 ==========
@@ -199,11 +248,12 @@ export function generateAlgorithmSteps(root: D3TreeNode | null): AlgorithmStep[]
199248
],
200249
9,
201250
'recursion-enter',
202-
{ toNodeId: node.id, value: node.val }
251+
{ toNodeId: node.id, value: node.val ?? 'null' }
203252
));
204253

205254
// ========== 处理左子树 ==========
206255
// 递归调用depth(node.left),进入左子树
256+
const leftChildLabel = node.left ? (node.left.isNull ? 'null' : `node=${node.left.val}`) : 'null';
207257
steps.push(createStep(
208258
stepIndex++,
209259
`⬇️ 递归调用depth(node.left),进入左子树`,
@@ -216,7 +266,7 @@ export function generateAlgorithmSteps(root: D3TreeNode | null): AlgorithmStep[]
216266
],
217267
13,
218268
'param-pass',
219-
{ fromNodeId: node.id, toNodeId: node.left?.id, value: node.left ? `node=${node.left.val}` : 'null' }
269+
{ fromNodeId: node.id, toNodeId: node.left?.id, value: leftChildLabel }
220270
));
221271

222272
// 递归计算左子树深度
@@ -241,6 +291,7 @@ export function generateAlgorithmSteps(root: D3TreeNode | null): AlgorithmStep[]
241291

242292
// ========== 处理右子树 ==========
243293
// 递归调用depth(node.right),进入右子树
294+
const rightChildLabel = node.right ? (node.right.isNull ? 'null' : `node=${node.right.val}`) : 'null';
244295
steps.push(createStep(
245296
stepIndex++,
246297
`⬇️ 递归调用depth(node.right),进入右子树`,
@@ -254,7 +305,7 @@ export function generateAlgorithmSteps(root: D3TreeNode | null): AlgorithmStep[]
254305
],
255306
14,
256307
'param-pass',
257-
{ fromNodeId: node.id, toNodeId: node.right?.id, value: node.right ? `node=${node.right.val}` : 'null' }
308+
{ fromNodeId: node.id, toNodeId: node.right?.id, value: rightChildLabel }
258309
));
259310

260311
// 递归计算右子树深度

src/components/AlgorithmModal/AlgorithmModal.tsx

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,24 +131,47 @@ export function AlgorithmModal({ isOpen, onClose }: AlgorithmModalProps) {
131131
<section className="algo-section">
132132
<h3>💻 代码实现</h3>
133133
<div className="code-box">
134-
<pre>{`class Solution {
134+
<pre>{`/**
135+
* 二叉树直径计算
136+
*
137+
* 思路:通过深度优先搜索(DFS)遍历每个节点,
138+
* 计算经过每个节点的最长路径,取最大值
139+
*/
140+
class Solution {
141+
// 全局变量:记录遍历过程中发现的最大直径
135142
private int diameter = 0;
136143
144+
/**
145+
* 主方法:计算二叉树的直径
146+
* @param root 二叉树的根节点
147+
* @return 二叉树的直径(最长路径的边数)
148+
*/
137149
public int diameterOfBinaryTree(TreeNode root) {
138-
depth(root);
150+
depth(root); // 通过计算深度的过程,顺便更新直径
139151
return diameter;
140152
}
141153
154+
/**
155+
* 辅助方法:计算以 node 为根的子树深度
156+
* 同时在遍历过程中更新全局最大直径
157+
*
158+
* @param node 当前节点
159+
* @return 以该节点为根的子树深度
160+
*/
142161
private int depth(TreeNode node) {
162+
// 递归终止条件:空节点深度为0
143163
if (node == null) return 0;
144164
165+
// 递归计算左子树深度
145166
int leftDepth = depth(node.left);
167+
// 递归计算右子树深度
146168
int rightDepth = depth(node.right);
147169
148-
// 更新直径:经过当前节点的最长路径
170+
// 关键步骤:更新直径
171+
// 经过当前节点的路径长度 = 左子树深度 + 右子树深度
149172
diameter = Math.max(diameter, leftDepth + rightDepth);
150173
151-
// 返回当前子树的深度
174+
// 返回当前子树的深度 = 较深子树的深度 + 1
152175
return Math.max(leftDepth, rightDepth) + 1;
153176
}
154177
}`}</pre>

src/components/TreeVisualization/TreeVisualization.tsx

Lines changed: 31 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -38,26 +38,27 @@ const ZOOM_STEP = 0.2;
3838
/**
3939
* 根据动画类型获取对应的颜色
4040
*
41-
* 不同的动画类型使用不同的颜色,便于用户区分:
42-
* - 递归进入:青色
43-
* - 递归退出:红色
44-
* - 返回值传递:紫色
45-
* - 比较操作:黄色
46-
* - 更新直径:绿色
47-
* - 参数传递:蓝色
41+
* 简化的颜色方案,相关操作使用相同颜色:
42+
* - 递归进入/退出:青色(表示递归调用栈的变化)
43+
* - 参数传递/返回值传递:紫色(表示数据在节点间流动)
44+
* - 比较/更新直径:黄色(表示计算和更新操作)
4845
*
4946
* @param type - 动画类型
5047
* @returns 对应的颜色值
5148
*/
5249
function getAnimationColor(type: AnimationType): string {
5350
switch (type) {
54-
case 'recursion-enter': return '#4ecdc4'; // 青色 - 递归进入
55-
case 'recursion-exit': return '#ff6b6b'; // 红色 - 递归退出
56-
case 'return-value': return '#a78bfa'; // 紫色 - 返回值传递
57-
case 'compare': return '#fbbf24'; // 黄色 - 比较操作
58-
case 'update-diameter': return '#22c55e'; // 绿色 - 更新直径
59-
case 'param-pass': return '#60a5fa'; // 蓝色 - 参数传递
60-
default: return '#ffa116'; // 橙色 - 默认
51+
case 'recursion-enter': // 递归进入
52+
case 'recursion-exit': // 递归退出
53+
return '#4ecdc4'; // 青色 - 递归调用栈变化
54+
case 'param-pass': // 参数传递
55+
case 'return-value': // 返回值传递
56+
return '#a78bfa'; // 紫色 - 数据流动
57+
case 'compare': // 比较操作
58+
case 'update-diameter': // 更新直径
59+
return '#fbbf24'; // 黄色 - 计算和更新
60+
default:
61+
return '#ffa116'; // 橙色 - 默认
6162
}
6263
}
6364

@@ -222,6 +223,7 @@ export function TreeVisualization({ root, currentStep }: TreeVisualizationProps)
222223
.attr('stroke-dasharray', '4,4'); // 虚线样式
223224

224225
// 绘制连接到实际节点的边(实线)
226+
// 边的颜色保持不变,不随动画类型改变
225227
g.selectAll('.edge-real')
226228
.data(edges.filter(e => !e[2])) // 只选择连接到实际节点的边
227229
.enter()
@@ -231,16 +233,8 @@ export function TreeVisualization({ root, currentStep }: TreeVisualizationProps)
231233
.attr('y1', d => d[0].y)
232234
.attr('x2', d => d[1].x)
233235
.attr('y2', d => d[1].y)
234-
.attr('stroke', d => {
235-
const edgeKey = `${d[0].id}-${d[1].id}`;
236-
if (highlightedEdges.has(edgeKey)) return getAnimationColor(animationType);
237-
return 'rgba(255, 255, 255, 0.3)';
238-
})
239-
.attr('stroke-width', d => {
240-
const edgeKey = `${d[0].id}-${d[1].id}`;
241-
if (highlightedEdges.has(edgeKey)) return 3;
242-
return 2;
243-
});
236+
.attr('stroke', 'rgba(255, 255, 255, 0.3)')
237+
.attr('stroke-width', 2);
244238

245239
// ========== 绘制空节点(NULL节点) ==========
246240
const nullNodeGroups = g.selectAll('.node-null')
@@ -341,9 +335,10 @@ export function TreeVisualization({ root, currentStep }: TreeVisualizationProps)
341335
const toNode = nodesMap.get(animationData.toNodeId);
342336

343337
if (fromNode && toNode) {
344-
const isUpward = animationType === 'return-value';
345-
const startNode = isUpward ? fromNode : toNode;
346-
const endNode = isUpward ? toNode : fromNode;
338+
// 参数传递:从父节点(from)到子节点(to),箭头向下
339+
// 返回值:从子节点(from)到父节点(to),箭头向上
340+
const startNode = fromNode;
341+
const endNode = toNode;
347342

348343
// 计算路径偏移,避免与边重叠
349344
const dx = endNode.x - startNode.x;
@@ -359,7 +354,7 @@ export function TreeVisualization({ root, currentStep }: TreeVisualizationProps)
359354
.attr('stroke-width', 2)
360355
.attr('fill', 'none')
361356
.attr('stroke-dasharray', '5,5')
362-
.attr('marker-end', `url(#arrow-${isUpward ? 2 : 3})`);
357+
.attr('marker-end', `url(#arrow-2)`);
363358

364359
// 路径动画
365360
const totalLength = path.node()?.getTotalLength() || 0;
@@ -399,8 +394,9 @@ export function TreeVisualization({ root, currentStep }: TreeVisualizationProps)
399394
.text(valueText);
400395

401396
// 在目标节点上方添加状态标签
402-
const targetNode = isUpward ? toNode : fromNode;
403-
const stateText = isUpward ? '返回值' : '参数传递';
397+
const isParamPass = animationType === 'param-pass';
398+
const targetNode = endNode;
399+
const stateText = isParamPass ? '参数传递' : '返回值';
404400
const stateLabelWidth = 60;
405401

406402
g.append('rect')
@@ -433,9 +429,9 @@ export function TreeVisualization({ root, currentStep }: TreeVisualizationProps)
433429
g.append('rect')
434430
.attr('class', 'compare-label-bg')
435431
.attr('x', currentNode.x - 55)
436-
.attr('y', currentNode.y - 75)
432+
.attr('y', currentNode.y - 85)
437433
.attr('width', 110)
438-
.attr('height', 45)
434+
.attr('height', 55)
439435
.attr('rx', 6)
440436
.attr('fill', animColor)
441437
.attr('opacity', 0.95);
@@ -444,7 +440,7 @@ export function TreeVisualization({ root, currentStep }: TreeVisualizationProps)
444440
g.append('text')
445441
.attr('class', 'compare-title')
446442
.attr('x', currentNode.x)
447-
.attr('y', currentNode.y - 58)
443+
.attr('y', currentNode.y - 66)
448444
.attr('text-anchor', 'middle')
449445
.attr('fill', '#1a1a2e')
450446
.attr('font-size', '10px')
@@ -455,7 +451,7 @@ export function TreeVisualization({ root, currentStep }: TreeVisualizationProps)
455451
g.append('text')
456452
.attr('class', 'compare-content')
457453
.attr('x', currentNode.x)
458-
.attr('y', currentNode.y - 42)
454+
.attr('y', currentNode.y - 48)
459455
.attr('text-anchor', 'middle')
460456
.attr('fill', '#1a1a2e')
461457
.attr('font-size', '13px')
@@ -466,7 +462,7 @@ export function TreeVisualization({ root, currentStep }: TreeVisualizationProps)
466462
g.append('text')
467463
.attr('class', 'compare-result')
468464
.attr('x', currentNode.x)
469-
.attr('y', currentNode.y - 28)
465+
.attr('y', currentNode.y - 32)
470466
.attr('text-anchor', 'middle')
471467
.attr('fill', '#1a1a2e')
472468
.attr('font-size', '10px')

src/utils/treeUtils.ts

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -125,32 +125,21 @@ export function convertToD3Tree(
125125

126126
// 递归转换子节点
127127
// 左子节点ID后缀为'-L',右子节点ID后缀为'-R'
128-
const hasLeftChild = node.left !== null;
129-
const hasRightChild = node.right !== null;
130-
const isLeaf = !hasLeftChild && !hasRightChild;
131-
132-
// 如果是叶子节点,不创建空子节点
133-
if (isLeaf) {
134-
d3Node.left = null;
135-
d3Node.right = null;
136-
} else {
137-
// 非叶子节点,递归处理子节点
138-
// 如果某一侧没有子节点,会创建一个空节点来显示 NULL
139-
d3Node.left = convertToD3Tree(
140-
node.left,
141-
`${id}-L`,
142-
depth + 1,
143-
d3Node,
144-
includeNullNodes
145-
);
146-
d3Node.right = convertToD3Tree(
147-
node.right,
148-
`${id}-R`,
149-
depth + 1,
150-
d3Node,
151-
includeNullNodes
152-
);
153-
}
128+
// 所有节点(包括叶子节点)都显示其空孩子,这样递归到 null 返回 0 的过程才完整
129+
d3Node.left = convertToD3Tree(
130+
node.left,
131+
`${id}-L`,
132+
depth + 1,
133+
d3Node,
134+
includeNullNodes
135+
);
136+
d3Node.right = convertToD3Tree(
137+
node.right,
138+
`${id}-R`,
139+
depth + 1,
140+
d3Node,
141+
includeNullNodes
142+
);
154143

155144
return d3Node;
156145
}
@@ -179,10 +168,10 @@ export function calculateTreeLayout(
179168
if (!root) return;
180169

181170
const maxDepth = getMaxDepth(root);
182-
// 节点之间的最小水平间距
183-
const nodeSpacing = 60;
184-
// 层级之间的垂直间距,根据树的深度自适应
185-
const levelHeight = Math.min(80, (height - 150) / (maxDepth + 1));
171+
// 节点之间的最小水平间距(增大间距让树更舒展)
172+
const nodeSpacing = 100;
173+
// 层级之间的垂直间距,根据树的深度自适应(增大垂直间距)
174+
const levelHeight = Math.min(120, (height - 100) / (maxDepth + 1));
186175

187176
// 用于追踪下一个可用的x坐标
188177
let nextX = 0;

0 commit comments

Comments
 (0)