探索数据结构小紫书之树和二叉树

432 阅读4分钟

持续更新中......

基本概念

树是n个结点的有限集,树的结构定义是一个递归定义

在任意一颗非空树中:

  1. 有且仅有一个特定的称为根的结点
  2. 当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1,T2,T3...,其中每一个集合本身又是一棵树,并且称为根的子数

接下来我们来研究一种称为二叉树的抽象数据类型

二叉树是一种树型结构,它的特点是每个结点至多只有两颗子树,(即二叉树中不存在度大于2的结点),并且,二叉树的子树有左右之分,其次序不能任意颠倒

各种操作

二叉树的前中后序遍历

首先,大致了解一下什么是二叉树的前中后序遍历

  • 前序:根结点->左子树->右子树
  • 中序:左子树->根结点->右子树
  • 后序:左子树->右子树->根结点

看个图,更好理解 image.png

这里专门说一下后序遍历

后序遍历首先遍历左子树,然后遍历右子树,最后访问根结点,在遍历左、右子树时,仍然先遍历左子树,然后遍历右子树,最后遍历根结点。即:

若二叉树为空则结束返回,否则:

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

已知前序遍历和中序遍历,就能确定后序遍历。

解法:递归三步走

给你二叉树的根节点 root ,返回它节点值的 前序遍历。

//递归  3步走
//前序
var preorderTraversal = function(root) {
 let res=[];
     //1.确定递归函数的参数和返回值
 const dfs=function(root){
     //2.确定终止条件
     if(root===null)return ;
     //3.确定单层递归的逻辑 中 左 右
     res.push(root.val);//先序遍历所以从父节点开始
     dfs(root.left);//递归左子树
     dfs(root.right);//递归右子树
 }
 //只使用一个参数 使用闭包进行存储结果
 dfs(root);
 return res;
};

给你二叉树的根节点 root ,返回它节点值的 中序遍历。

//中序
var inorderTraversal = function(root) {
    let res=[];
    const dfs=function(root){
        if(root===null){
            return ;
        }
        dfs(root.left);//左中右
        res.push(root.val);
        dfs(root.right);
    }
    dfs(root);
    return res;
};

给你二叉树的根节点 root ,返回它节点值的 后序遍历。

//后序
var postorderTraversal = function(root) {
    let res=[];
    const dfs=function(root){
        if(root===null){
            return ;
        }
        dfs(root.left);//左右中
        dfs(root.right);
        res.push(root.val);
    }
    dfs(root);
    return res;
};

二叉树的层序遍历

给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。

示例: 二叉树:[3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7

返回其层序遍历结果:

[
  [3],
  [9,20],
  [15,7]
]

解法:层序遍历+队列思想

var levelOrder = function(root) {
    const ret = [];
    if (!root) {
        return ret;
    }
    const q = [];
    q.push(root);
    while (q.length !== 0) {
        const currentLevelSize = q.length;//常量不能改变 每换一层就出一次for 来一次while q数组长度更新
        ret.push([]);//没换一层就添加一个新的数组
        for (let i = 1; i <= currentLevelSize; ++i) {
            const node = q.shift();//shift删除数组第一个元素,返回删除的元素(改变原数组)
            ret[ret.length - 1].push(node.val);//最后一个数组添加
            if (node.left) q.push(node.left);//左
            if (node.right) q.push(node.right);//右
        }
    } 
    return ret;
};

路径总和

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum ,判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。

叶子节点 是指没有子节点的节点。

示例: image.png

输入:root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22 输出:true

解法:递归

//妙啊!!递归
var hasPathSum = function(root, targetSum) {
    if(!root){//到最后一个节点之后
        return false
    }
    if(!root.left&&!root.right){
        return root.val===targetSum//到叶子节点了
    }
    return hasPathSum(root.left,targetSum-root.val) || hasPathSum(root.right,targetSum-root.val)//左或右只要有一边能找到就行
};

二叉树的最大深度

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

示例:

    3
   / \
  9  20
    /  \
   15   7

给定二叉树 [3,9,20,null,null,15,7],

返回它的最大深度 3 。

解法:递归

递归三部曲

  1. 确定递归函数的参数和返回值
  2. 确定终止条件
  3. 确定单层递归的逻辑
var maxDepth = function(root) {
    let k=0
    if(!root) return k
    if(maxDepth(root.left) || maxDepth(root.right)){//参数
        k++//单层递归的逻辑  根左右的顺序,任何一个不为空就继续 根左右 遍历
    }
    return k//返回值
}

翻转二叉树

翻转一棵二叉树。

示例:

输入:

     4
   /   \
  2     7
 / \   / \
1   3 6   9

输出:

     4
   /   \
  7     2
 / \   / \
9   6 3   1

这道题背后有个小故事

image.png

但是它真的很难吗,并不是,这只是一道简单题,让我们一探究竟

解法:BFS 队列 广度优先遍历 自顶向下

// 用层序遍历的方式去遍历二叉树。
// 根节点先入列,然后出列,出列就 “做事”,交换它的左右子节点(左右子树)。
// 并让左右子节点入列,往后,这些子节点出列,也被翻转。
// 直到队列为空,就遍历完所有的节点,翻转了所有子树。

var invertTree = function(root) {
    let queue=[root]//根节点先入列
    while(queue.length>0){
        let cur=queue.pop()//然后出列,最后一次把 null放进去了
        if(cur===null) continue;//直到队列为空,就遍历完所有的节点,翻转了所有子树 回到while,判断false,直接跳出while
        [cur.left,cur.right]=[cur.right,cur.left]//出列就 “做事”,交换它的左右子节点(左右子树)
        queue.unshift(cur.left)//并让左右子节点入列,往后,这些子节点出列,也被翻转
        queue.unshift(cur.right)
    }
    return root
};

特别要注意的点,continue的使用,跳出当前循环,最后一次是压入了 null 队列长度=1