前端算法入门之路(四)(二叉树)

109 阅读4分钟

船长表情包镇楼

图片名称

二叉树

  1. 每个节点度最多为2
  2. 度为0的节点比度为2的节点多1个

遍历

  • 前序遍历:根左右
  • 中序遍历:左根右
  • 后序遍历:左右根

相关二叉树

  • 完全二叉树:仅最后一层右侧取缺少子节点的二叉树
  • 满二叉树:没有度为1节点的二叉树
  • 完美二叉树:最后一层都没有孩子的满二叉树

A3651F25-237E-44ea-9C6F-5FA09BD1187A.png

完全二叉树

  1. 编号为i的子节点
    左孩子编号:2i
    右孩子编号:2
    i+1
  2. 可以用连续空间存储(数组)
    一维与二维的相通

树结构的深入理解

  • 节点代表集合,边代表关系,子节点代表性质互不相同的子集

设计理解递归程序

  1. 数学归纳法-> 结构归纳法
  2. 赋予递归函数一个明确的意义
  3. 思考边界条件
  4. 实现递归过程

船长金句:谁会在乎递归函数是怎么执行的呢,我们只要关注这个函数的意义即可!

LeetCode肝题

  1. LeetCode 144. 二叉树的前序遍历
// 赋予递归函数一个明确的意义:前序遍历一棵树
// 不要展开思考递归函数
var preorder = function(root, ans) {
    if(root === null) return null // 边界条件
    ans.push(root.val)  // 根
    preorder(root.left, ans)   // 左 递归遍历左子树
    preorder(root.right, ans)   // 右 递归遍历右子树
}
var preorderTraversal = function(root) {
    let ans = []
    preorder(root, ans)
    return ans
};
    1. N 叉树的前序遍历
// 赋予递归函数一个明确的意义:前序遍历一棵树
var __preorder = function(root, ans) {
    if(!root.children) return // 边界条件
    ans.push(root.val)
    for(let i = 0; i<root.children.length; i++) {
        __preorder(root.children[i], ans)       // 递归遍历子树
    }
};
var preorder = function(root) {
    if (!root) return []
    let ans = []
    __preorder(root, ans)
    return ans
};
    1. 翻转二叉树
// 赋予递归函数一个明确的意义:翻转一棵树(交换左右子树)
var reverse = function(root) {
    if (!root) return  // 边界条件
    let a = root.right
    root.right = root.left
    root.left = a
    reverse(root.left)  // 递归翻转左子树
    reverse(root.right)  // 递归翻转右子树
}
var invertTree = function(root) {
    reverse(root)
    return root
};
  1. 剑指 Offer 32 - II. 从上到下打印二叉树 II
// 赋予递归函数一个明确的意义:翻转一棵树(交换左右子树)
var getResult = function(root, k, ans) {
    if (!root) return  // 边界条件
    if (k == ans.length) ans.push([])    // 每到新的一层push一个空数组
    ans[k].push(root.val)    // 答案集合,第k层依次放置答案
    getResult(root.left, k + 1, ans)  // 递归遍历左子树
    getResult(root.right, k + 1, ans)  // 递归遍历右子树
}
var levelOrder = function(root) {
    let ans = []
    getResult(root, 0, ans)
    return ans
};
    1. 二叉树的层序遍历 II
// 思路同上,最终反转一下数组的值
var getResult = function(root, k, ans) {
    if (!root) return
    if (k == ans.length) ans.push([])
    ans[k].push(root.val)
    getResult(root.left, k+1, ans)
    getResult(root.right, k+1, ans)
}
var resver = function(arr) {
    for(let i=0, j=arr.length-1; i<j;i++,j--) {
        [arr[i],arr[j]] = [arr[j], arr[i]]
    }
}
var levelOrderBottom = function(root) {
    if (!root) return []
    let ans = []
    getResult(root, 0, ans)
    resver(ans)
    return ans
};
    1. 二叉树的锯齿形层序遍历
// 思路同上,反转奇数项数组的值
var getResult = function(root, k, ans) {
    if (!root) return
    if (k ==ans.length) ans.push([])
    ans[k].push(root.val)
    getResult(root.left, k+1, ans)
    getResult(root.right, k+1, ans)
}
var resves = function(arr) {
    for(let i=0,j=arr.length-1;i<j;i++,j--) {
        [arr[i], arr[j]] = [arr[j], arr[i]]
    }
}
var zigzagLevelOrder = function(root) {
    if (!root) return []
    let ans = []
    getResult(root, 0, ans)
    for(let i = 0; i<ans.length; i++) {
        if (i%2==1) {
            resves(ans[i])
        }
    }
    return ans
};
    1. 平衡二叉树
// 赋予递归函数一个明确的意义:
// 返回左右子树较高的树的高度,如果左右子树高度相差大于1,则返回一个负数,证明树不平衡
var getHeight = function(root) {
    if (!root) return 0
    let l = getHeight(root.left)
    let r = getHeight(root.right)
    if (l<0 || r<0) return -2
    if (Math.abs(l - r) > 1) return -2
    return Math.max(l, r) + 1
}
var isBalanced = function(root) {
    if (!root) return true
    return getHeight(root) > 0
};
    1. 路径总和
// 赋予递归函数一个明确的意义:寻找节点值等于targetSum的节点
var hasPathSum = function(root, targetSum) {
    if(!root) return false  // 边界值判断
    // 到达叶子节点时,如果节点值等于targetSum则返回true
    if (!root.left && !root.right) return root.val == targetSum
    // 非叶子节点时,如果左子树能找到节点值等于targetSum的节点,返回true
    if (root.left && hasPathSum(root.left, targetSum - root.val)) return true
    // 非叶子节点时,如果右子树能找到节点值等于targetSum的节点,返回true
    if (root.right && hasPathSum(root.right, targetSum - root.val)) return true
    // 最后则证明左右子树都找不到该节点,返回false
    return false
};
    1. 从前序与中序遍历序列构造二叉树 核心图解
      image.png
// 赋予递归函数一个明确的意义:通过前序和中序遍历构建和返回一棵树
var buildTree = function(preorder, inorder) {
    if(preorder.length==0) return null  // 边界值判断
    let pos = inorder.findIndex(item => item == preorder[0])  // 获取根节点下标
    let l_pre = [],l_in = [],r_pre = [],r_in = []  //左子树前、中序遍历数组,右子树前、中序遍历数组
    for(let i = 0; i < pos; i++) {
        l_pre.push(preorder[i+1])
        l_in.push(inorder[i])
    }
    for(let i = pos+1; i < preorder.length; i++) {
        r_pre.push(preorder[i])
        r_in.push(inorder[i])
    }
    let root = new TreeNode(preorder[0])
    root.left = buildTree(l_pre, l_in)   // 递归构建左子树
    root.right = buildTree(r_pre, r_in)   // 递归构建右子树
    return root
};
    1. 完全二叉树的结点个数
// 赋予递归函数一个明确的意义:查询完全二叉树的结点个数
var countNodes = function(root) {
    if(!root) return 0
    // 树的节点个数 = 左子树的数量 + 右子树的数量 + 1
    return countNodes(root.left) + countNodes(root.right) + 1
};
  1. 剑指 Offer 54. 二叉搜索树的第 k 大结点
    二叉搜索树:右子树的值总是大于根节点,左子树的值总是小于根节
    所以二叉搜索树的中序遍历结果是一个有序数组
// 获取前序遍历结果
var inorder = function(root, ans) {
    if (!root) return
    root.left && inorder(root.left,ans)
    ans.push(root.val)
    root.right && inorder(root.right,ans)
}
var kthLargest = function(root, k) {
    let ans = []
    inorder(root, ans)
    return ans[ans.length -k]
};
  1. 剑指 Offer 26. 树的子结构
// 赋予递归函数一个明确的意义:A树B树是否完全相同
var is_match = function(A, B) {
    if (B == null) return true  // 如果B树为空则说明相同
    if (A == null) return false  // 如果A树为空则说明不相同
    if (A.val != B.val) return false  // 如果值不相等则说明不相同
    return is_match(A.left, B.left) && is_match(A.right, B.right)    // 比较左子树和右子树
}
// 赋予递归函数一个明确的意义:B树是否是A树的子树
var isSubStructure = function(A, B) {
    if (A == null) return false
    if (B == null) return false
    if (A.val == B.val && is_match(A, B)) return true    // 如果当前值相等且两树相同,返回true
    return isSubStructure(A.left, B) || isSubStructure(A.right, B)    // 否则递归比较A的左子树与B或者A的右子树与B
};
    1. 二叉树最大宽度
// 层序给树的每个节点一个编号, 根节点为i,左子树为2i,右子树为2i+1
var widthOfBinaryTree = function(root) {
    if (!root) return 0
    // 定义一个最大值和一个二维数组,数组存的是每一层的编号和节点
    let max = 1, que = [[0, root]]
    while(que.length) {
        let smaller = que[0][0], bigger = que[que.length - 1][0]
        max = Math.max(max, bigger - smaller + 1)
        let arr = []
        for (const [index, item] of que) {
            // 左子树的序号可以等价于(父节点的序号-父节点层最小序号)*2
            // 右子树的序号可以等价于(父节点的序号-父节点层最小序号)*2 + 1
            item.left && arr.push([2*(index - smaller), item.left])
            item.right && arr.push([2*(index - smaller)+1, item.right])
        }
        que = arr
    }
    return max
};