树和图的遍历方式

1,055 阅读5分钟

树,包括图,在遍历时都存在两种方式:深度优先遍历和广度优先遍历。 树,一定有一个根节点,而图,没有根节点,但图中的任意节点都可以作为根节点使用(当然该节点一定要有边,否则没有意义)

树的遍历

树的深度优先遍历

核心思想:优先访问子节点而非兄弟节点,直到子节点的递归调用栈清空,再访问兄弟节点。

  1. 访问当前节点
  2. 将当前节点的children作为子树的根节点递归访问

使用递归的方式

// 传入树的根节点,开始深度优先遍历,每个节点的子节点用children数组来表示
function dfs(root) {
  // 访问节点数据
  console.log(root.val);
  if (root.children.length > 0) {
    for(let i = 0; i < root.children.length; i ++) {
      dfs(root.children[i]);
    }
    // 或者使用root.children.forEach(dfs)来替换上面的for循环
  }
}

树的广度优先遍历

核心思想:优先访问兄弟节点,为了保证访问顺序,使用队列来存储要访问的节点。

  1. 新建队列,将根节点入队
  2. 队列出队,访问出队节点
  3. 将出队节点的children依次入队
  4. 重复2和3,直到队列为空

使用队列的方式

// 传入树的根节点,开始深度优先遍历
function bfs(root) {
  const queue = [];
  queue.push(root);
  while(queue.length > 0) {
    const node = queue.shift();
    console.log(node.val);
    node.children && node.children.forEach(child => queue.push(child))
  }
}

在打包模块时,可以使用广度优先遍历从入口文件触发,构建模块的依赖图谱。

图的遍历

图与树的遍历很类似,但最明显的一点在于,树是有指向性(方向性)的,而无向图没有方向性,因此在遍历无向图时,需要额外的操作就是标记已访问的节点(可以使用一个集合来标记已访问过的节点),防止节点出现循环访问的情况(即节点的访问形成一个圈变为死循环)。

另外,作为遍历算法的起始点,不同的起始点可能导致最后遍历到的节点情况不同,这与图本身有关。

图的深度优先遍历

  1. 访问当前节点
  2. 将当前节点的未访问的相邻节点递归访问 如图所示

image.png

使用递归的方式

// 这里的图使用邻接表的方式来表示
const graph = {
   0: [1, 2], // 节点0与节点1,节点2相邻
   1: [2],
   2: [0, 3],
   3: [3]
}
// 存储已访问的节点的集合
const visited = new Set()
// 传入图的任意节点,开始深度优先遍历
function dfs(node) {
  // 访问节点数据
  console.log(node);
  // 加入已访问的节点集合
  visited.add(node)
  graph[node].forEach(n => {
    // 如果没有访问过该节点,则递归访问该节点
    if (!visited.has(n)) {
      dfs(n);
    }
  })
}

图的广度优先遍历

核心思想:优先访问兄弟节点,为了保证访问顺序,使用队列来存储要访问的节点。

  1. 新建队列,将根节点入队
  2. 队列出队,访问出队节点
  3. 将出队节点的没有被访问过的相邻节点依次入队
  4. 重复2和3,直到队列为空

使用队列的方式

// 存储已访问的节点的集合
const visited = new Set()
// 传入树的根节点,开始深度优先遍历
function bfs(node) {
  const queue = [];
  queue.push(node);
  visited.add(node);
  while(queue.length > 0) {
    const n = queue.shift();
    console.log(n);
    // 加入已访问集合的代码放在这里不合适,因为某个节点可能是其他几个节点的相邻节点,只要该节点没有出队被加入已访问集合,可能会被多次加入待访问的队列
    // visited.add(n);
    graph[n].forEach(c => {
      if (!visited.has(c)) {
        queue.push(c);
        // 加入待访问队列中,同时加入已访问队列里,因为待访问队列可以说是100%要访问到
        visited.add(c);
      }
    })
  }
}

二叉树的先中后序遍历

二叉树的子节点使用left和right来指定,而不在使用children来指定。

特点

  • 先、中、后指的是根节点的访问顺序
  • 左子树一定在右子树之前访问

递归型的遍历

先(前)序遍历

  1. 访问根节点
  2. 对左子树先(前)序遍历
  3. 对右子树先(前)序遍历
// 先序遍历
function preOrder(root) {
  // 递归终止条件
  if (!root) return;
  console.log(root.val);
  preOrder(root.left);
  preOrder(root.right);
}

中序遍历

  1. 对左子树中序遍历
  2. 访问根节点
  3. 对右子树中序遍历
// 中序遍历
function inOrder(root) {
  if (!root) return;
  inOrder(root.left);
  console.log(root.val);
  inOrder(root.right);
}

后序遍历

  1. 对左子树后序遍历
  2. 对右子树后序遍历
  3. 访问根节点
// 后序遍历
function postOrder(root) {
  if (!root) return;
  postOrder(root.left);
  postOrder(root.right);
  console.log(root.val)
}

非递归型的遍历

所有的递归都可以用栈来代替,因为递归的底层就是使用栈来实现的。

先序遍历

  1. 访问根节点
  2. 对左子树先序遍历
  3. 对右子树先序遍历
function preOrder(root) {
  if (!root) {return;}
  const stack = [];
  stack.push(root);
  while(stack.length > 0) {
    // 出栈
    const node = stack.pop();
    console.log(node.val);
    // 将左右子树入栈, 先让右子树入栈,再让左子树入栈,因为我们要先访问左子树,再访问右子树
    if (node.right) {
      stack.push(node.right);
    }
    if (node.left) {
      stack.push(node.left);
    }
  }
}

中序遍历

  1. 对左子树中序遍历
  2. 访问根节点
  3. 对右子树中序遍历

中序遍历的顺序跟React Fiber树的构建过程很相似

// 中序遍历
function inOrder(root) {
  if (!root) {return;}
  const stack = [];
  let p = root;
  while(p || stack.length > 0) {
    while(p) {
      // 先将左子树不断入栈
      stack.push(p);
      p = p.left;
    }
    // 所有左节点都入栈后,开始出栈访问
    const node = stack.pop();
    console.log(node.val);
    // 访问完栈顶左节点后,将其右节点入栈
    p = node.right;
  }
}

后序遍历

  1. 对左子树后序遍历
  2. 对右子树后序遍历
  3. 访问根节点

后序遍历是左 -> 右 -> 根,前序遍历是 根 -> 左 -> 右,因此将前序遍历改造为 根 -> 右 -> 左,然后利用栈的先进后出,逆序访问即可。 需要两个栈:

  • 递归栈:模拟遍历过程中的递归函数调用
  • 访问栈:存储访问节点,在递归栈中节点出栈时,将节点存储到访问栈中,提供最终的节点访问顺序。
// 后序遍历
function postOrder(root) {
  if (!root) {return;}
  const stack = [];
  const outPutStack = [];
  stack.push(root);
  // 对前序遍历进行改造,前序遍历是 根 -> 左 -> 右,后续遍历是左 -> 右 -> 根,
  // 将前序遍历改造成 根 -> 右 -> 左,然后逆序输出访问即可
  while(stack.length > 0) {
    // 出栈
    const node = stack.pop();
    // 加入到访问栈中
    outPutStack.push(node)
    // 前序遍历改造,先将左子树入栈,再将右子树入栈, 根 -> 右 -> 左
    if (node.left) {
      stack.push(node.left);
    }
    if (node.right) {
      stack.push(node.right);
    }
  }
  while(outPutStack.length > 0) {
    const node = outPutStack.pop();
    console.log(node.val);
  }
}