Skip to content

绘制一棵漂亮的树

1. 问题

  • 棵树 T,我们要做的就是试着把它画出来
tsx
class DrawTree {
  constructor(tree, depth = 0) {
    this.x = -1
    this.y = depth
    this.tree = tree
    this.children = tree.children.map((child) => {
      return new DrawTree(child, depth + 1)
    })
  }
}

原则

  • 原则 1:树的边不应该交叉

  • 原则 2:相同深度的节点应该绘制在同一水平线,这能让树的结构更清晰

-原则 3:树应该尽可能画的紧凑一点

  • 原则4:父节点应该位于子节点中间

2. 解决方案

自下而上 尽可能紧凑的树 满足原则 3

tsx
// 代码3
const nexts = []
const minimum_ws = (tree, depth = 0) => {
  if (nexts[depth] === undefined) {
    nexts[depth] = 0
  }
  tree.x = nexts[depth]
  tree.y = depth
  nexts[depth] += 1
  tree.children.forEach((child) => {
    minimum_ws(child, depth + 1)
  })
}

原则 4:父节点应该位于子节点中间

Mods 和 Rockers

tsx
class DrawTree {
  constructor(tree, depth = 0) {
    this.x = -1 // 节点的x坐标(初始为-1,表示未确定)
    this.y = depth // 节点的y坐标(即深度)
    this.tree = tree // 原始树节点
    this.children = tree.children.map((child) => {
      return new DrawTree(child, depth + 1) // 递归创建子节点
    })
    this.mod = 0 // 调整值,用于后续布局微调
  }

  setup = (tree, depth = 0, nexts = {}, offset = {}) => {
    // 先递归处理所有子节点(后序遍历)
    tree.children.forEach((child) => {
      setup(child, depth + 1, nexts, offset)
    })

    tree.y = depth // 设置当前节点的深度

    let place // 临时位置变量

    // 根据子节点数量决定当前节点的x位置
    if (childrenLength <= 0) {
      // 叶子节点:放在当前深度层的最左可用位置
      place = nexts[depth] || 0
      tree.x = place
    } else if (childrenLength === 1) {
      // 只有一个子节点:放在子节点左边一格
      place = tree.children[0].x - 1
    } else {
      // 多个子节点:放在第一个和最后一个子节点的中间位置
      let s = tree.children[0].x + tree.children[childrenLength - 1].x
      place = s / 2
    }

    // 计算并记录当前深度层需要的偏移量
    offset[depth] = Math.max(offset[depth] || 0, (nexts[depth] || 0) - place)

    // 如果有子节点,应用计算出的偏移量
    if (childrenLength > 0) {
      tree.x = place + offset[depth]
    }

    // 更新当前深度层的下一个可用位置
    if (nexts[depth] === undefined) {
      nexts[depth] = 0
    }
    nexts[depth] += 2

    tree.mod = offset[depth] // 记录当前节点的调整值
  }

  addmods = (tree, modsum = 0) => {
    // 应用累计的调整值到当前节点
    tree.x = tree.x + modsum
    modsum += tree.mod // 累计调整值

    // 递归应用到所有子节点
    tree.children.forEach((child) => {
      addmods(child, modsum)
    })
  }

  layout = (tree) => {
    setup(tree) // 计算初步布局
    addmods(tree) // 应用调整值
    return tree // 返回布局好的树
  }
}

树作为 Block 块

  • 上面算法问题 ,相同的树结构,当放在树的不同位置时,可能会绘制出不同的结构。为了解决这个问题,

  • 原则 5:同一个子树无论在树的哪个位置,绘制的结果都应该相同

思路

对树进行后序遍历 如果一个节点是叶子节点,那么给它一个值为 0 的 x 坐标 否则,在不产生冲突的情况下,将它的右子树尽可能靠近左子树 使用与前面相同的 mod 方式,在 O(n)时间内移动树 将节点放置在其子节点中间 再遍历一次树,将累积的 mode 值加到 x 坐标上

树形轮廓算法解析与可视化

关键点

  • 计算树的轮廓(左轮廓或右轮廓)

  • 根据两棵树的轮廓计算它们之间的最小安全距离,避免重叠

核心概念解析

树的轮廓

  • 左轮廓:树每一层最左侧节点的 x 坐标组成的数组

  • 右轮廓:树每一层最右侧节点的 x 坐标组成的数组

tsx
    A
   / \
  B   C
 /   / \
D   E   F

// 假设各节点的x坐标为:A(1), B(0), C(2), D(-1), E(1), F(3)

// 左轮廓:[1 (A), 0 (B), -1 (D)]

// 右轮廓:[1 (A), 2 (C), 3 (F)]
tsx
// 代码7

// 小于
const lt = (a, b) => {
  return a < b
}

// 大于
const gt = (a, b) => {
  return a > b
}

// 将两个数组按位置配对
// [a, b, c],[d, e, f] => [[a, d], [b, e], [c, f]]
const zip = (a, b) => {
  let len = Math.min(a.length, b.length)
  let arr = []
  for (let i = 0; i < len; i++) {
    arr.push([a[i], b[i]])
  }
  return arr
}
const push_right = (left, right) => {
  // 左边树的右轮廓
  let wl = contour(left, lt)
  // 右边树的左轮廓
  let wr = contour(right, gt)
  let res = zip(wl, wr)
  let arr = res.map((item) => {
    return item[0] - item[1]
  })
  return Math.max(...arr) + 1
}
// 获取一棵树的轮廓
const contour = (tree, comp, level = 0, cont = null) => {
  // 根节点只有一个,所以直接添加
  if (cont === null) {
    cont = [tree.x]
  } else if (cont.length < level + 1) {
    // 该层级尚未添加,直接添加
    cont.push(tree.x)
  } else if (comp(cont[level], tree.x)) {
    // 该层级已经有值,所以进行比较
    cont[level] = tree.x
  }
  tree.children.forEach((child) => {
    contour(child, comp, level + 1, cont)
  })
  return cont
}

树形布局中的线程优化技术解析

线程概念解析

在树形布局上下文中,线程(thread)是一种特殊的指针,它连接同一层级但不具有父子关系的节点,用于快速访问轮廓节点而不需要遍历整个子树。

线程的特点:

  • 非父子关系:连接同一层级的节点

  • 优化遍历:避免全树扫描,快速找到轮廓边界

  • 虚线表示:在可视化中用虚线表示(实线表示真正的父子关系)

tsx
// 代码8
// 获取节点的右轮廓下一个节点
const nextright = (tree) => {
  if (tree.thread) {
    // 如果有线程,直接跳转
    return tree.thread
  } else if (tree.children.length > 0) {
    // 否则取最右子节点
    return tree.children[tree.children.length - 1]
  } else {
    // 叶子节点没有下一个
    return null
  }
}

// 获取节点的左轮廓下一个节点
const nextleft = (tree) => {
  if (tree.thread) {
    // 如果有线程,直接跳转
    return tree.thread
  } else if (tree.children.length > 0) {
    // 否则取最左子节点
    return tree.children[0]
  } else {
    // 叶子节点没有下一个
    return null
  }
}
const contour = (left, right, max_offset = 0, left_outer = null, right_outer = null) => {
  // 初始化外层节点
  if (left_outer === null) left_outer = left
  if (right_outer === null) right_outer = right

  // 计算当前层的最大偏移需求
  if (left.x - right.x > max_offset) {
    max_offset = left.x - right.x
  }

  // 获取下一层的内外层节点
  let lo = nextleft(left) // 左树左轮廓下一节点
  let li = nextright(left) // 左树右轮廓下一节点
  let ri = nextleft(right) // 右树左轮廓下一节点
  let ro = nextright(right) // 右树右轮廓下一节点

  // 如果还有内层节点,继续递归
  if (li && ri) {
    return contour(li, ri, max_offset, lo, ro)
  }

  return max_offset + 1 // 返回需要右推的距离(+1保证不重叠)
}