【算法】深度优先与广度优先遍历

298 阅读3分钟

测试数据:

graph TD
1 --> 1-1
1 --> 1-2
1 --> 1-3
1-2 --> 2
2 --> 2-1
2 --> 2-2
const treeData = {
    id: '1',
    title: '节点1',
    children: [
        {
            id: '1-1',
            title: '节点1-1'
        },
        {
            id: '1-2',
            title: '节点1-2',
            children: [
                {
                    id: '2',
                    title: '节点2',
                    children: [
                        {
                            id: '2-1',
                            title: '节点2-1'
                        },
                        {
                            id: '2-2',
                            title: '节点2-2'
                        }
                    ]
                }
            ]
        },
        {
            id: '1-3',
            title: '节点1-3'
        }
    ]
};

(一)深度优先遍历

image.png
深度优先遍历用的是(先进后出),栈对数组尾部进行操作

1. 递归实现

  • 思路:
    • 从图中一个未访问的顶点 V 开始,沿着一条路一直走到底
    • 看该节点 是否还有除该子节点以外的节点,没有继续回退到父节点,有则遍历该子节点之外的其他节点
  • 优缺点: 递归的表达性很好,也很容易理解,不过如果层级过深,很容易导致栈溢出
const dfsWithRecursion = (node, nodeList) => {
    if (node) {
        nodeList.push(node.id); //存入当前节点的数据
        const children = node.children;
        if (children && children.length) {
            for (let i = 0; i < children.length; i++) {
                dfsWithRecursion(children[i], nodeList);//递归调用
            }
        }
    }
    console.log('【 nodes 】-68', nodeList);
    // 结果:['1', '1-1', '1-2', '2', '2-1', '2-2', '1-3']
    return nodeList;
};

2. 非递归实现

  • 思路: 对二叉树来说,是先序遍历(先遍历当前节点 -> 再遍历左节点 -> 再遍历右节点)
    • ① 对于每个节点来说,先遍历当前节点(将当前节点弹栈),然后把右节点压栈,再压左节点(这样弹栈的时候会先拿到左节点遍历,符合深度优先遍历要求)。
    • ② 弹栈,拿到栈顶的节点,如果节点不为空,重复步骤 ①,如果为空,结束遍历。
  • 优点: 不用担心递归那样层级过深导致的栈溢出问题
  • 前置知识点:
    | 方法名 | 作用 | | --- | --- | | push | 向数组的末尾添加一个或更多元素,并返回新的长度 | | pop | 把数组的最后一个元素从其中删除,并返回最后一个元素的值|
const dfsWithNoRecursion = node => {
    const nodes = []; //保存遍历到的数据
    const stack = []; //存储栈
    if (node) {
        stack.push(node); //将最顶部的节点入栈
        // 栈不为空
        while (stack.length) {
            // 返回最后一个元素的值,即栈顶元素
            let topItem = stack.pop(); //每次取出当前最顶部的节点(即最后入栈的节点)
            nodes.push(topItem.id); //存入当前节点的数据
            const children = topItem.children;
            if (children && children.length) {
                //(根据先进后出的顺序),
                // 存的时候:同一层级从右到左依次入栈
                // 取的时候:同一层级从左到右依次出栈
                for (let i = children.length - 1; i >= 0; i--) {
                    stack.push(children[i]);
                }
            }
        }
    }
    console.log('【 nodes 】-68', nodes);
    // 结果:['1', '1-1', '1-2', '2', '2-1', '2-2', '1-3']
    return nodes;
};

(二)广度优先遍历

image.png
广度优先遍历用的是用队列(先进先出) 来实现,队列对数组首部进行操作

非递归实现

  • 思路: 指的是从图的一个未遍历的节点出发,先遍历这个节点的相邻节点,再依次遍历每个相邻节点的相邻节点。
  • 优点: 不用担心递归那样层级过深导致的栈溢出问题
  • 前置知识点:
    | 方法名 | 作用 | | --- | --- | | unshift | 向数组的开头添加一个或更多元素,并返回新的长度 | | shift | 把数组的第一个元素从其中删除,并返回第一个元素的值。 |
const bfsWithNoRecursion = node => {
    const nodeList = []; //保存遍历到的数据
    if (node) {
        const queue  = []; //存储队列
        queue.unshift(node);  //进入队列
        // 队列不为空
        while (queue.length) {
            let firstItem = queue.shift(); //离开队列
            nodeList.push(firstItem.id); //存入当前节点的数据
            const children = firstItem.children;
            if (children && children.length) {
                //(根据先进先出的顺序),
                for (let i = 0; i < children.length; i++) {
                    queue.push(children[i]); // 依次将该层的子元素放到队列中
                }
            }
        }
    }
    console.log('【 nodeList 】-68', nodeList);
    // 结果:['1', '1-1', '1-2', '1-3', '2', '2-1', '2-2']
    return nodeList;
};