数据结构之“树”

542 阅读5分钟

树简介

树是什么?

  • 一种分层数据的抽象模型
  • 前端工作中常见的树包括:DOM树、级联选择,树形控件...
  • JS中没有树,但可以用Object和Array构建树。
  • 树的常用操作:深度/广度优先遍历、先中后序遍历

什么是深度/广度优先遍历?

  • 深度优先遍历:尽可能深的搜索树的分支
  • 广度优先遍历:先访问离根节点最近的节点

image.png

深度优先遍历算法口决

  • 访问根节点
  • 对根节点的children挨个进行深度优先遍历
const tree = {
    val: 'a',
    children: [
        {
            val: 'b',
            children: [
                {
                    val: 'd',
                    children: [],
                },
                {
                    val: 'e',
                    children: [],
                }
            ],
        },
        {
            val: 'c',
            children: [
                {
                    val: 'f',
                    children: [],
                },
                {
                    val: 'g',
                    children: [],
                }
            ],
        }
    ],
};
//递归法
const dfs = (root) =>{
    console.log(root.val);
    root.children.forEach(item=>{
        dfs(item);
    })
}

dfs(tree);
//打印: a b d e c f g

广度优先遍历算法口决

  • 新建一个队列,把根节点入队
  • 把队头出队并访问
  • 把队头的children挨个入队
  • 重复第二、三步,直到队列为空

image.png

const tree = {
    val: 'a',
    children: [
        {
            val: 'b',
            children: [
                {
                    val: 'd',
                    children: [],
                },
                {
                    val: 'e',
                    children: [],
                }
            ],
        },
        {
            val: 'c',
            children: [
                {
                    val: 'f',
                    children: [],
                },
                {
                    val: 'g',
                    children: [],
                }
            ],
        }
    ],
};
//递归法
const bfs = (root) =>{
    let q = [root];
    while(q.length > 0){            
        const n = q.shift();
        console.log(n.val);
        n.children.forEach(item=>{
            q.push(item);
        })
    }
}

bfs(tree);
//打印:abcdefg

二叉树是什么

  • 树中每个节点最多只能有两个子节点
  • 在JS中通常用Object来模拟二叉树
const binaryTree = {
    val : 1,
    left:{
        val : 2,
        left : null,
        right: null
    },
    right:{
        val:3,
        left:null,
        right:null
    }
}

先序遍历算法口决(根左右)

  • 访问根节点
  • 对根节点的左子树进行先序遍历
  • 对根节点的右子树进行先序遍历

image.png

const bt = {
    val: 1,
    left: {
        val: 2,
        left: {
            val: 4,
            left: null,
            right: null,
        },
        right: {
            val: 5,
            left: null,
            right: null,
        },
    },
    right: {
        val: 3,
        left: {
            val: 6,
            left: null,
            right: null,
        },
        right: {
            val: 7,
            left: null,
            right: null,
        },
    },
};
//递归方法
const preorder = (root) =>{
    if(!root){
        return;
    }
    console.log(root.val);
    preorder(root.left);
    preorder(root.right)
}
//非递归方法
const preorder2 = (root) =>{
    if(!root) { return;}
    const stack = [root];
    while(stack.length){
        const n = stack.pop();
        console.log(n.val);
        //因为栈先进后出,所以先入right,后入left
        if(n.right){
            stack.push(n.right);
        }
        if(n.left){
            stack.push(n.left);
        }
    }
}
preorder(bt)
//打印: 1245367

中序遍历算法口决(左根右)

  • 对根节点的左子树进行中序遍历
  • 访问根节点
  • 对根节点的右子树进行中序遍历
const bt = {
    val: 1,
    left: {
        val: 2,
        left: {
            val: 4,
            left: null,
            right: null,
        },
        right: {
            val: 5,
            left: null,
            right: null,
        },
    },
    right: {
        val: 3,
        left: {
            val: 6,
            left: null,
            right: null,
        },
        right: {
            val: 7,
            left: null,
            right: null,
        },
    },
};

const preorder = (root) =>{
    if(!root){
        return;
    }
    preorder(root.left);
    console.log(root.val);
    preorder(root.right)
}

const preorder2 = (root) =>{
    if(!root) { return;}
    const stack = [];
    let p = root;
    while(stack.length || p){
        while(p){
            stack.push(p);
            p = p.left;
        }
        const n = stack.pop();
        console.log(n.val);
        p = n.right;
    }
}

preorder(bt)
//打印: 4 2 5 1 6 3 7

后序遍历算法口决(根左右)

  • 对根节点的左子树进行后序遍历
  • 对根节点的右子树进行后序遍历
  • 访问根节点
const bt = {
    val: 1,
    left: {
        val: 2,
        left: {
            val: 4,
            left: null,
            right: null,
        },
        right: {
            val: 5,
            left: null,
            right: null,
        },
    },
    right: {
        val: 3,
        left: {
            val: 6,
            left: null,
            right: null,
        },
        right: {
            val: 7,
            left: null,
            right: null,
        },
    },
};

const preorder = (root) =>{
    if(!root){
        return;
    }
    preorder(root.left);
    preorder(root.right)
    console.log(root.val);
}
const preorder2 = (root) =>{
    if(!root) { return;}
    //左右根 --> 栈后进先出:根右左
    const stack = [root];
    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);
    }

}
preorder(bt)
//打印 : 4 5 2 6 7 3 1

LeetCode:104.二叉树的最大深度

image.png

解题思路

  • 求最大深度,考虑使用深度优先遍历
  • 在深度优先遍历过程中,记录每个节点所在的层级,找出最大的层级即可 解题步骤
  • 新建一个变量,记录最大深度
  • 深度优先遍历整棵树,并记录每个节点的层级,同时不断刷新最大深度这个变量
  • 遍历结束返回最大深度这个变量

解题coding:

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number}
 */
var maxDepth = function(root) {
    let res = 0;
    const dfs = (n , len)=>{
        if(!n){
            return ;
        }
        if(!n.left && !n.right){
            res = Math.max(res,len);
        }
        dfs(n.left, len + 1);
        dfs(n.right, len + 1);
    }
    dfs(root,1);
    return res;
};

LeetCode:111.二叉树的最小深度

image.png 解题思路

  • 求最小深度,考虑使用广度优先遍历
  • 在广度优先遍历过程中,遇到叶子节点,停止遍历,返回节点层级 解题步骤
  • 广度优先遍历整棵树,并记录每个节点的层级
  • 遇到叶子节点,返回节点层级,停止遍历 解题coding:
/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number}
 */
var minDepth = function(root) {
    // 广度优先遍历+层级
    if(!root){
        return 0;
    }
    let q = [[root,1]];
    while(q.length){
        let [n,len] = q.shift();
        if(!n.left && !n.right){
            return len;
        }
        if(n.left){
            q.push([n.left,len+1])
        }
        if(n.right){
            q.push([n.right,len+1])
        }
    }
};

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

image.png 解题思路

  • 层序遍历顺序就是广度优先遍历
  • 不过在遍历时候需要记录当前节点所处的层级,方便将其添加到不同的数组中 解题步骤
  • 广度优先遍历二叉树
  • 遍历过程中,记录每个节点的层级,并将其添加到不同的数组中 解题Coding:
/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
//方法一
var levelOrder2 = function(root) {
    if(!root){
        return []
    }
    let q = [[root,0]];
    const res = [];
    while(q.length){
        const [n,level] = q.shift();
        if(!res[level]){
            res.push([n.val]);
        }else{
            res[level].push(n.val);
        }
        n.left && q.push([n.left,level + 1]);
        n.right && q.push([n.right,level + 1]);
    }
    return res;

};
 //方法二
var 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);
            n.left && q.push(n.left);
            n.right && q.push(n.right);
        }
    }
    return res;
}

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

image.png

解题思路:就是上面介绍的中序遍历 (左中右)

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
/**递归*/
var inorderTraversal = function(root) {
    const res = [];
    const inorder = (root) => {
        if (!root) {
            return;
        }
        inorder(root.left);
        res.push(root.val);
        inorder(root.right);
    }
    inorder(root);
    return res;
};
/**迭代*/
var inorderTraversal = function(root) {
    const res = [];
    const stk = [];
    while (root || stk.length) {
        while (root) {
            stk.push(root);
            root = root.left;
        }
        root = stk.pop();
        res.push(root.val);
        root = root.right;
    }
    return res;
};


Leetcode:112.路径总和

解题思路

  • 在深度优先遍历的过程中,记录当前路径的节点值的和
  • 在叶子节点处,判断当前路径的节点值的和是否等于目标值 解题步骤
  • 深度优先遍历二叉树,在叶子节点处,判断当前路径的节点值的和是否等于目标值,是就返回true
  • 遍历结束,如果没有匹配,就返回false 解题coding
/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @param {number} targetSum
 * @return {boolean}
 */
var hasPathSum = function(root, targetSum) {
    if(!root){
        return false;
    }
    let res = false;
    const dfs = (n,val)=>{
        if(!n.left && !n.right && val === targetSum){
            res = true;
        }
        n.left && dfs(n.left, val + n.left.val);
        n.right && dfs(n.right, val + n.right.val);
    }
    dfs(root,root.val);
    return res;
};

前端与树:遍历JSON的所有节点值

const json = {
    a:{b:{c:1}},
    d:[1,2]
};
const dfs = (n,path) =>{
    console.log(n,path);
    Object.keys(n).forEach(k =>{
        console.log(k)
        dfs(n[k],path.concat(k));
    })
}
dfs(json,[])