「✍ Canvas」绘制一颗二叉树

860 阅读2分钟

前言

最近在刷一些二叉树的题,发现调试起来实在是麻烦又不够直观,要是可以随机生成一颗二叉树并渲染,解完题后实时查看树的变化就方便多了。

发现有篇文章实现的挺好:juejin.cn/post/696209…

生成二叉树


  class TreeNode {
    constructor(val) {
      this.val = val
      this.left = null
      this.right = null
    }
  }

  class FullBinaryTree {
    constructor(level) {
      const _Root = this.getFullBinaryTree(level)
      this.getRoot = () => {
        return _Root
      }
    }

    getFullBinaryTree(level) {
      debugger
      let index = 0;
      function fullTree(n) {
        if (n === 0) return null;
        let root = new TreeNode(index++);
        root.left = fullTree((n - 1) / 2);
        root.right = fullTree((n - 1) / 2);
        return root;
      }
      // 2的level次方 -1 = 总节点数
      return fullTree(Math.pow(2, level) - 1);
    }
  }
  
  const Root = new FullBinaryTree(3).getRoot() // 获取一个深度为3的树的根结点

基于这颗树可以再写一些工具方法

计算深度

const getTreeDepth = (root) => {
    let depth = 0 // 当前深度
    let max = 0 // 当前最大深度
    const solve = (node) => {
      if (!node) {
        if (depth >= max) {
          max = depth
        }
        return 0
      }

      depth++

      solve(node.left)
      solve(node.right)

      depth-- // 回到当前节点,深度减回来

      return max
    }

    return solve(root)
}

渲染器(Render)

用canvas作为渲染器

initSize : 初始化canvas

renderNode: 绘制圆形节点,填充节点值

renderLine: 连接两个节点

class CanvasRender {
  constructor(container) {
    this.canvas = document.createElement('canvas');
    this.container = container;
    this.ctx = this.canvas.getContext('2d');
  }

  initSize(w, h) {
    this.canvas.width = w;
    this.canvas.height = h;
    this.container.appendChild(this.canvas)
  }

  setStyle(ctx) {
    ctx.font = "20px serif";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
  }

  renderNode(x, y, r, text) {
    this.ctx.beginPath();
    this.ctx.arc(x, y, r, 0, Math.PI * 2); // 绘制圆
    this.ctx.stroke();
    this.setStyle(this.ctx) // 设置样式
    this.ctx.fillText(text, x, y); // 填充节点的value
  }

  renderLine(x1, y1, x2, y2) {
    this.ctx.moveTo(x1, y1);
    this.ctx.lineTo(x2, y2);
    this.ctx.stroke();
  }
}

绘制器

class TreeDrawer {
  constructor(render) {
    this.render = render;
  }
  layout(root, nodeW, nodeH) {
    
    function computeWidth(root) {
      if (!root) return 0;
      if (!(root.left || root.right)) {
        root.width = nodeW;
        return root.width;
      }
      root.width = computeWidth(root.left) + computeWidth(root.right) + nodeW;
      return root.width;
    }

    function computePosition(root, left, right, curY = nodeH) {
      if (!root) return;
      let x;
      if (root.left) {
        x = left + root.left.width + nodeW;
      } else {
        x = left + nodeW;
      }
      root.position = [x, curY];
      computePosition(root.left, left, x, curY + nodeH);
      computePosition(root.right, x, right, curY + nodeH);

    }

    computeWidth(root);
    computePosition(root, 0, root.width);
    return root.width;
  }

  draw(root, nodeW = 40, nodeH = 40) {
    let depth = getTreeDepth(root) // 获取深度
    let width = this.layout(root, nodeW, nodeH); // canvas宽度
    let height = depth * nodeH + nodeH; // canvas高度
    this.render.initSize(width + nodeW, height);

    const getVector = (x, y, x1, y1) => {
      let dis = Math.sqrt((x - x1) ** 2 + (y - y1) ** 2);
      return [
        (x1 - x) / dis,
        (y1 - y) / dis
      ]
    }
    const linkNode = (root, child) => {
      let [px, py] = root.position;
      let [cx, cy] = child.position;
      let [dx, dy] = getVector(px, py, cx, cy);
      this.render.renderLine(px + (nodeW / 2) * dx, py + (nodeW / 2) * dy,
        cx - (nodeW / 2) * dx, cy - (nodeW / 2) * dy)
    }

    const drawNode = (root) => {
      let [x, y] = root.position;
      this.render.renderNode(x, y, nodeW / 2, root.val);
    }
    function draw(root) {
      if (!root) return;
      drawNode(root);
      if (root.left) linkNode(root, root.left);
      if (root.right) linkNode(root, root.right);
      draw(root.left);
      draw(root.right);
    }
    draw(root);
  }
}