数据结构与算法之树(六)

1,759 阅读6分钟
  • 一种分层数据的抽象模型

  • 前端工作中常见的树包括:DOM树、级联选择、树形控件…

  • JS 中没有树,但是可以用 Object 和 Array 构建树

  • 树的常用操作:深度/广度优先遍历、先中后序遍历

1、普通树

const tree = {
  val: "a",
  children: [
    {
      val: "b",
      children: [
        {
          val: "d",
          children: [],
        },
        {
          val: "e",
          children: [],
        },
      ],
    },
    {
      val: "c",
      children: [
        {
          val: "f",
          children: [],
        },
        {
          val: "g",
          children: [],
        },
      ],
    },
  ],
}

深度优先遍历

  • 深度优先遍历:尽可能深的搜索树的分支,优先遍历其子节点,而非兄弟节点
  • 口诀:
    • 访问根节点
    • 对根节点的 children 挨个进行深度优先遍历
const dfs = (root) => {
    console.log("root value is", root.val);
    root.children.forEach(dfs);
}
dfs(tree);

广度优先遍历

  • 广度优先遍历:先访问离根节点最近的节点,优先遍历兄弟节点,再去遍历自己的子节点
  • 口诀:
    • 新建一个队列 并把根节点入队
    • 把队头出队并访问
    • 把队头的 children 挨个入队
    • 重复第二 、三步 直到队列为空
const bfs = (root) => {
    const q = [root];
    while(q.length > 0) {
        const n = q.shift();
        console.log(n.val);
        n.children.forEach(child => {
            q.push(child);
        })
    }
}
bfs(tree);

2、二叉树

  • 树中每个节点最多只能有两个子节点
  • 在JS 中通常用 Object 来模拟二叉树
const root = {
  val: "A",
  left: {
    val: "B",
    left: {
      val: "D",
      left: {
        val: "DD"
      },
      right: {
        val: "DDD"
      }
    },
    right: {
      val: "E",
    },
  },
  right: {
    val: "C",
    left: {
      val: "F",
    },
    right: {
      val: "G",
    },
  },
}

先序遍历

  • 访问根节点
  • 对根节点的子树进行先序遍历
  • 对根节点的子树进行先序遍历
// 先序遍历 递归
const preOrder1 = (tree) => {
    // 递归边界,root 为空
    if(!tree) return;
    // 输出当前遍历的结点值
    console.log(tree.val);
    // 递归遍历左子树
    preOrder1(tree.left);
    // 递归遍历右子树  
    preOrder1(tree.right);
}


	
// 先序遍历 非递归
const preOrder2 = (tree) => {
  if (!tree) return;

  // 新建一个栈
  const stack = [tree];

  while(stack.length) {
    const n = stack.pop();
    console.log(n.val);
    if (n.right) stack.push(n.right);
    if (n.left) stack.push(n.left);
  }
}

输出结果顺序为:

A
B
D
DD
DDD
E
C
F
G

中序遍历

  • 对根节点的子树进行中序遍历
  • 访问节点
  • 对根节点的子树进行中序遍历
// 中序遍历 递归
const inOrder1 = (tree) => {
    // 递归边界,root 为空
    if(!tree) return; 
    // 递归遍历左子树 
    inOrder1(tree.left)  
    // 输出当前遍历的结点值
    console.log(tree.val)  
    // 递归遍历右子树  
    inOrder1(tree.right)
}


// 中序遍历 非递归
const inOrder2 = (tree) => {
  if (!tree) return;

  // 新建一个栈
  const stack = [];

  // 先遍历所有的左节点
  let p = tree;
  while(stack.length || p) {
    while(p) {
      stack.push(p);
      p = p.left;
    }
    const n = stack.pop();
    console.log(n.val);
    p = n.right;
  }
}

输出结果顺序为:

DD
D
DDD
B
E
A
F
C
G

后序遍历

  • 对根节点的子树进行后序遍历
  • 对根节点的子树进行后序遍历
  • 访问节点
// 后序遍历 递归
const postOrder1 = (tree) => {
    // 递归边界,root 为空
    if(!tree) return;
    // 递归遍历左子树 
    postOrder1(tree.left);
    // 递归遍历右子树  
    postOrder1(tree.right);
    // 输出当前遍历的结点值
    console.log(tree.val); 
}


// 后序遍历 非递归
const postOrder2 = (tree) => {
  if (!tree) return

  const stack = [tree];
  const outputStack = [];

  while(stack.length) {
    const n = stack.pop();
    outputStack.push(n)

    if(n.left) stack.push(n.left);
    if(n.right) stack.push(n.right);
  }

  while(outputStack.length) {
    const n = outputStack.pop();
    console.log(n.val);
  }
};

输出结果顺序为:

DD
DDD
D
E
B
F
G
C
A

二叉树的最大深度

// 给一个二叉树,需要你找出其最大的深度,从根节点到叶子节点的距离

// 时间复杂度 O(n) n为树的节点数
// 空间复杂度 有一个递归调用的栈 所以为 O(n) n也是为二叉树的最大深度
const maxDepth = function(root) {
  let res = 0;
  // 使用深度优先遍历
  const dfs = (root, layer) => {
    if(!root) return;
    if(!root.left && !root.right) {
      // 没有叶子节点就把深度数量更新
      res = Math.max(res, layer);
    }
    dfs(root.left, layer + 1);
    dfs(root.right, layer + 1);
  }

  dfs(root, 1);
  return res;
};

二叉树的最小深度

解题思路:

  • 求最小深度,考虑使用广度优先遍历
  • 在广度优先遍历过程中,遇到叶子节点,停止遍历,返回节点层级

解题步骤

  • 广度优先遍历整棵树,并记录每个节点的层级
  • 遇到叶子节点,返回节点层级,停止遍历
// 时间复杂度O(n),n为树的节点数量
// 空间复杂度O(n),n为树的节点数量

const minDepth = function(root) {
  if(!root) return 0;
  // 使用广度优先遍历
  const q = [[root, 1]];

  while(q.length) {
      const [n, layer] = q.shift();
      // 如果是叶子节点直接返回深度
      if(!n.left && !n.right) {
          return layer;
      }
      if(n.left) q.push([n.left, layer + 1]);
      if(n.right) q.push([n.right, layer + 1]);
  }
};

LeetCode: 102. 二叉树的层序遍历

输出:[[3],[9,20],[15,7]]

解题思路:

  • 层序遍历顺序就是广度优先遍历
  • 不过在遍历时候需要记录当前节点所处的层级,方便将其添加到不同的数组中

解题步骤:

  • 广度优先遍历二叉树
  • 遍历过程中,记录每个节点的层级,并将其添加到不同的数组中
const levelOrder = function(root) {
  if(!root) return [];
  const q = [[root, 0]];
  const res = [];
  while(q.length) {
      const [n, layer] = q.shift();
      if(!res[layer]) {
          res.push([n.val]);
      }else {
          res[layer].push(n.val);
      } 
      if(n.left) q.push([n.left, layer + 1]);
      if(n.right) q.push([n.right, layer + 1]);
  }
  return res;
};

另外一种解法:

const levelOrder = function(root) {
  if(!root) return [];
  const q = [root];
  const res = [];
  while(q.length) {
      let len = q.length;
      res.push([]);
      // 循环每层节点
      while(len--) {
          const n = q.shift();
          res[res.length - 1].push(n.val);
          if(n.left) q.push(n.left);
          if(n.right) q.push(n.right);
      } 
  }
  return res;
};

上面两个解法

  • 时间复杂度O(n),n为树的节点数量

  • 空间复杂度O(n),n为树的节点数量

LeetCode: 94. 二叉树的中序遍历

  • 将我们上面中序遍历的打印变成往数组里添加元素即可
// 递归版
const inorderTraversal = function(root) {
  const res = [];
  const rec = (root) => {
    if(!root) return;
    rec(root.left);
    res.push(root.val);
    rec(root.right);
  }
  
  rec(root);
  return res;
};

// 非递归版
const inorderTraversal = function(root) {
  const res = [];
  const stack = [];
  let p = root;

  while(stack.length || p) {
    while(p) {
      stack.push(p);
      p = p.left;
    }
    const n = stack.pop();
    res.push(n.val);
    p = n.right;
  }
  
  return res;
};

LeetCode: 112. 路径总和

解题思路:

  • 在深度优先遍历的过程中,记录当前路径的节点值的和
  • 在叶子节点处,判断当前路径的节点值的和是否等于目标值

解题步骤:

  • 深度优先遍历二叉树,在叶子节点处,判断当前路径的节点值的和是否等于目标值,是就返回 true
  • 遍历结束,如果没有匹配,就返回 false
// 时间复杂度O(n),n为树的节点数量
// 空间复杂度O(n),n递归堆栈的高度,也就是树的高度,树的高度最小一条分叉的时候为n,如果全部都有两条分叉则为logn
const hasPathSum = function (root, targetSum) {
  if (!root) return false;
  let res = false;

  const dfs = (n, sum) => {
    // 在叶子节点判断是否有相等值
    if (!n.left && !n.right && targetSum === sum) {
      res = true;
    }
    // 记录每个节点路径的和
    if (n.left) dfs(n.left, sum + n.left.val);
    if (n.right) dfs(n.right, sum + n.right.val);
  };

  dfs(root, root.val);
  return res;
}

遍历JSON的所有节点值

const obj = { a: { b: { c: 3 } }, d: 4 };

const bfs = (n, path) => {
  console.log("path is", path, "n is", n);
  Object.keys(n).forEach((key) => {
    bfs(n[key], path.concat(key));
  });
}

bfs(obj, []);

打印结果:

path is []    n is {a: {…}, d: 4}
path is ['a']   n is b: {c: 3}
path is ['a', 'b']    n is {c: 3}
path is ['a', 'b', 'c']    n is 3
path is ['d']   n is 4	

渲染Antd中的树组件

const json = [
  { title: "一", key: "1", children: [{ title: "三", key: "3" }] },
  { title: "二", key: "2", children: [{ title: "四", key: "4" }] },
];
class Demo extends React.component {
  dfs = (n) => {
    return (
      <TreeNode title={n.title} key={n.key}>
        {n.children.map(this.dfs)}
      </TreeNode>
    );
  };
  render() {
    return <Tree>{json.map(this.dfs)}</Tree>;
  }
}