剑指Offer-树部分

重建二叉树

输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

例如,给出
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
    3
   / \
  9  20
    /  \
   15   7

解题思路

首先前序/后序遍历 + 中序遍历可以重建二叉树。题目考察的就是前序+中序来重建二叉树,后序+中序的思路是类似的。
例子与思路
假设有二叉树如下:

    1
   / \
  2   3
 / \
4   5

它的前序遍历的顺序是:1 2 4 5 3。中序遍历的顺序是:4 2 5 1 3
因为前序遍历的第一个元素就是当前二叉树的根节点。那么,这个值就可以将中序遍历分成 2 个部分。在以上面的例子,中序遍历就被分成了 4 2 5 和 3 两个部分。4 2 5就是左子树,3就是右子树。
最后,根据左右子树,继续递归即可

var buildTree = function(preorder, inorder) {
    if(!preorder.length || !inorder.length){
        return null
    };
    let rootVal = preorder[0];
    let node = new TreeNode(rootVal);
    let i = 0;
    for(;i<inorder.length;i++){
        if(rootVal == inorder[i]){
            break
        }
    };
    node.left = buildTree(preorder.slice(1,i+1),inorder.slice(0,i));
    node.right = buildTree(preorder.slice(i+1),inorder.slice(i+1));
    return node
};

树的子结构

输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)
B是A的子结构, 即 A中有出现和B相同的结构和节点值

例如:
给定的树 A:
     3
    / \
   4   5
  / \
 1   2
给定的树 B4 
  /
 1
返回 true,因为 BA 的一个子树拥有相同的结构和节点值。
示例 1:
输入:A = [1,2,3], B = [3,1]
输出:false
示例 2:
输入:A = [3,4,5,1,2], B = [4,1]
输出:true

解题思路

递归
设计两个函数:
isSubStructure 的职能:判断 B 是否是 A 的子结构。是,返回 true;否则,尝试 A 的左右子树
isSubTree 的职能:封装“判断 B 是否是 A 的子结构”的具体逻辑。

var isSubStructure = function(A, B) {
    if(!A || !B) {
        return false;
    }
    return (isSubTree(A,B) || isSubStructure(A.left,B) || isSubStructure(A.right,B))
};
var isSubTree = function(A,B){
    if(!B){
        return true
    }
    if(!A){
        return false
    }
    if(A.val != B.val){
        return false
    }
    return isSubTree(A.left,B.left) && isSubTree(A.right,B.right)
}

二叉树的镜像

请完成一个函数,输入一个二叉树,该函数输出它的镜像。

例如输入:

     4
   /   \
  2     7
 / \   / \
1   3 6   9
镜像输出:
     4
   /   \
  7     2
 / \   / \
9   6 3   1
示例 1:
输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]

解题思路1

左右子树镜像递归

var mirrorTree = function(root) {
    if (!root) {
        return null;
    }
    // 交换当前节点的左右节点
    const leftCopy = root.left;
    root.left = root.right;
    root.right = leftCopy;

    // 对左右子树做相同操作
    mirrorTree(root.left);
    mirrorTree(root.right);

    return root;
};

解题思路2

一句话完成,也是递归

var mirrorTree = function(root) {
    return root == null? null : new TreeNode(root.val,mirrorTree(root.right),mirrorTree(root.left))
};

对称的二叉树

请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。

例如,二叉树 [1,2,2,3,4,4,3] 是对称的。

    1
   / \
  2   2
 / \ / \
3  4 4  3
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:

    1
   / \
  2   2
   \   \
   3    3
示例 1:
输入:root = [1,2,2,3,4,4,3]
输出:true
示例 2:
输入:root = [1,2,2,null,3,null,3]
输出:false

解题思路

递归

var isSymmetric = function(root) {
    if(!root) return true;
    var check = function(left,right){
        if(!left && !right) return true;
        if(!left || !right) return false;
        if(left.val != right.val) return false;
        return check(left.left,right.right) && check(left.right,right.left)
    }
    return check(root.left,root.right)
};

从上到下打印二叉树

从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。

例如:
给定二叉树: [3,9,20,null,null,15,7],
    3
   / \
  9  20
    /  \
   15   7
返回:
[3,9,20,15,7]

解题思路

层序遍历需要使用一个队列来存储有用的节点。整体的思路如下:
将 root 放入队列
取出队首元素,将 val 放入返回的数组中
检查队首元素的子节点,若不为空,则将子节点放入队列
检查队列是否为空,为空,结束并返回数组;不为空,回到第二步
时间复杂度和空间复杂度是 O(N)。代码实现如下

var levelOrder = function(root) {
    if (!root) {
        return [];
    }
    const data = [];
    const queue = [root];
    while (queue.length) {
        const first = queue.shift();
        data.push(first.val);
        first.left && queue.push(first.left);
        first.right && queue.push(first.right);
    }
    return data;
};

从上到下打印二叉树 II

从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。

例如:
给定二叉树: [3,9,20,null,null,15,7],
    3
   / \
  9  20
    /  \
   15   7
返回其层次遍历结果:
[
  [3],
  [9,20],
  [15,7]
]

解题思路

按层打印,因此增加层这个概念来限制

var levelOrder = function(root) {
    if(!root) return [];
    let data = [];
    let queue = [root];
    let level = 0;
    while(queue.length){
        data[level] = []
        levelNum = queue.length;
        while(levelNum--){
            let first = queue.shift();
            data[level].push(first.val);
            first.left && queue.push(first.left);
            first.right && queue.push(first.right);
        }
       level++ 
    }
    return data
};

从上到下打印二叉树 III

请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。

例如:
给定二叉树: [3,9,20,null,null,15,7],
    3
   / \
  9  20
    /  \
   15   7
返回其层次遍历结果:
[
  [3],
  [20,9],
  [15,7]
]

解题思路

跟上体类似,只不过加了个左右添加的判断

在这里插入代码片var levelOrder = function(root) {
    if(!root) return [];
    let data = [];
    let queue = [root];
    let level = 0;
    while(queue.length){
        data[level] = [];
        levelNum = queue.length;
        while(levelNum--){
            let first = queue.shift();
            level%2==0?data[level].push(first.val):data[level].unshift(first.val);
            first.left && queue.push(first.left);
            first.right && queue.push(first.right);
        }
        level++
    }
    return data
};

二叉树中和为某一值的路径

输入一棵二叉树和一个整数,打印出二叉树中节点值的和为输入整数的所有路径。从树的根节点开始往下一直到叶节点所经过的节点形成一条路径。

示例:
给定如下二叉树,以及目标和 target = 225
             / \
            4   8
           /   / \
          11  13  4
         /  \    / \
        7    2  5   1
返回:
[
   [5,4,11,2],
   [5,8,4,5]
]

解题思路1

前序遍历(递归)
算法实现思路是:
每次来到新节点,将节点放入当前保存的路径.检查节点是否是叶节点:是:将路径放入结果中;不是:继续遍历左子树和右子树
上面整个过程就是一个前序遍历,但在遍历的过程中,动态地维护了当前路径与总和的信息。

var pathSum = function(root, sum) {
    if (!root) {
        return [];
    }
    const pathes = [];
    __pathSum(root, sum, pathes, []);
    return pathes;
};

function __pathSum(root, sum, pathes, path) {
    if (!root) {
        return;
    }

    path = [...path, root.val]; // 深拷贝

    if (!root.left && !root.right && root.val === sum) {
        pathes.push(path);
        return;
    }

    __pathSum(root.left, sum - root.val, pathes, path);
    __pathSum(root.right, sum - root.val, pathes, path);
}

解题思路2

这里可以借助栈,改造成非递归前序遍历。从而减少运行时间。
为了方便表示节点信息,栈中的元素是形如(node, 剩余路径和, 节点路径组成的数组)这样的元祖。
整体的处理流程是:
取出栈顶的元祖:节点、剩余路径和、路径
如果当前节点是叶节点,且剩余路径和等于节点的 val,那么将路径放入结果中
如果右节点不为空,将(右节点,剩余路径和 - 右节点值,路径+右节点)放入栈中
如果左节点不为空,处理过程和右节点一样
注意,为什么先处理右节点而不是左节点?先遍历左和右都可以。但是因为用的***的 oj 平台,这里要求“数组长度大的数组靠前”,先遍历左节点就是 oc 不了。

var pathSum = function(root, target) {
    if(!root) return [];
    let stack = [[root,target,[root.val]]];
    let pathes = [];
    while(stack.length){
        let [node,target,path] = stack.pop();
        if(!node.left && !node.right && node.val == target){
            pathes.push(path)
        }
        if(node.right){
            stack.push([node.right,target-node.val,[...path,node.right.val]])
        };
        if(node.left){
            stack.push([node.left,target-node.val,[...path,node.left.val]])
        };
    }
    return pathes
};

序列化二叉树

请实现两个函数,分别用来序列化和反序列化二叉树。

示例: 
你可以将以下二叉树:
    1
   / \
  2   3
     / \
    4   5
序列化为 "[1,2,3,null,null,4,5]"

解题思路1(DFS)

(序列化)
首先对其进行序列化生成字符串,以X代替null
在这里插入图片描述
(反序列)
传入由序列化字符串转成的 list 数组。
逐个 pop 出 list 的首项,构建当前子树的根节点,顺着 list,构建顺序是根节点 > 左子树 > 右子树。
如果弹出的字符为 “X”,则返回 null 节点。
如果弹出的字符是数值,则创建root节点,并递归构建root的左右子树,最后返回root。
在这里插入图片描述

//序列化
const serialize = (root) => {
  if (root == null) {                  // 遍历到 null 节点
    return 'X';
  }
  const left = serialize(root.left);   // 左子树的序列化结果
  const right = serialize(root.right); // 右子树的序列化结果
  return root.val + ',' + left + ','+ right; // 按  根,左,右  拼接字符串
};
//反序列化
const deserialize = (data) => {
  const list = data.split(',');   // split成数组

  const buildTree = (list) => {   // 基于list构建当前子树
    const rootVal = list.shift(); // 弹出首项,获取它的“数据”
    if (rootVal == "X") {         // 是X,返回null节点
      return null;
    }
    const root = new TreeNode(rootVal); // 不是X,则创建节点
    root.left = buildTree(list);        // 递归构建左子树
    root.right = buildTree(list);       // 递归构建右子树
    return root;                        // 返回当前构建好的root
  };

  return buildTree(list); // 构建的入口
};

解题思路2(BFS)

(序列化)
维护一个队列,初始让根节点入列,考察出列节点:
如果出列的节点是 null,将符号 ‘X’ 推入 res 数组。
如果出列的节点是数值,将节点值推入数组 res,并将它的左右子节点入列。
子节点 null 也要入列,它对应 “X”,要被记录,只是它没有子节点可入列。
入列、出列…直到队列为空,就遍历完所有节点,res构建完毕,转成字符串就好。
在这里插入图片描述
(反序列化)
依然先转成list数组,用一个指针 cursor 从第二项开始扫描。
起初,用list[0]构建根节点,并让根节点入列。
节点出列,此时 cursor 指向它的左子节点值,cursor+1 指向它的右子节点值。
如果子节点值是数值,则创建节点,并认出列的父亲,同时自己也是父亲,入列。
如果子节点值为 ‘X’,什么都不用做,因为出列的父亲的 left 和 right 本来就是 null
可见,所有的真实节点都会在队列里走一遍,出列就带出儿子入列
在这里插入图片描述

 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */

/**
 * Encodes a tree to a single string.
 *
 * @param {TreeNode} root
 * @return {string}
 */
const serialize = (root) => {
  const queue = [root];
  let res = [];
  while (queue.length) {
    const node = queue.shift(); 
    if (node) {                
      res.push(node.val);       
      queue.push(node.left);    
      queue.push(node.right);    
    } else {                   
      res.push('X');           
    }
  }
  return res.join(',');  
}


/**
 * Decodes your encoded data to tree.
 *
 * @param {string} data
 * @return {TreeNode}
 */
const deserialize = (data) => {
  if (data == 'X') return null;

  const list = data.split(','); 

  const root = new TreeNode(list[0]); 
  const queue = [root];          
  let cursor = 1;              

  while (cursor < list.length) { 
    const node = queue.shift();  

    const leftVal = list[cursor];      
    const rightVal = list[cursor + 1]; 

    if (leftVal != 'X') {              
      const leftNode = new TreeNode(leftVal); 
      node.left = leftNode;                   
      queue.push(leftNode);                   
    }
    if (rightVal != 'X') {
      const rightNode = new TreeNode(rightVal);
      node.right = rightNode;
      queue.push(rightNode);
    }
    cursor += 2; 
  }
  return root;  
};

二叉搜索树的第k大节点

给定一棵二叉搜索树,请找出其中第k大的节点

示例 1:
输入: root = [3,1,4,null,2], k = 1
   3
  / \
 1   4
  \
   2
输出: 4
示例 2:
输入: root = [5,3,6,2,4,null,null,1], k = 3
       5
      / \
     3   6
    / \
   2   4
  /
 1
输出: 4

解题思路1

利用从上到下遍历二叉树的思想

var kthLargest = function(root, k) {
    let queue = [root];
    let res = [];
    while(queue.length){
        let first = queue.shift();
        res.push(first.val);
        first.left && queue.push(first.left);
        first.right && queue.push(first.right)
    };
    function sortNumber(a,b){
        return b-a
    }
    return res.sort(sortNumber)[k-1];
};

解题思路2

递归

var kthLargest = function(root, k) {
    let setArray = new Set()
    const dfs = function(node) {
        if (node === null) {
            return
        }
        setArray.add(node.val)
        dfs(node.left)
        dfs(node.right)
    }
    dfs(root)
    let array = [...setArray]
    array.sort((a,b) => {
        return b - a
    })
    return array[k - 1]
};

解题思路3

二叉搜索树,中序遍历的数组结果刚好是排好序的,故利用反中序遍历。直接遍历到第 k 大的值就停止遍历

var kthLargest = function(root, k) {
    // 反中序遍历,记录数值第k个值返回
    let num = 0
    let result = null
    const dfs = function(node) {
        if (node === null) {
            return
        }
        dfs(node.right)
        num++
        if (num === k) {
            result = node.val
            return
        }
        dfs(node.left)
    }
    dfs(root)
    return result
};

二叉树的深度

输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。

例如:
给定二叉树 [3,9,20,null,null,15,7]3
   / \
  9  20
    /  \
   15   7
返回它的最大深度 3

解题思路1

利用从上到下遍历二叉树的思想

var maxDepth = function(root) {
    if(!root) return [];
    let queue = [root];
    let level = 0;
    while(queue.length){
        let levelNum = queue.length
        while(levelNum--){
            let node = queue.shift();
            node.left && queue.push(node.left);
            node.right && queue.push(node.right)
        }
        level += 1;
    }
    return level
};

解题思路2

递归

var maxDepth = function(root) {
         return root?Math.max(maxDepth(root.left),maxDepth(root.right))+1:0;
};

平衡二叉树

输入一棵二叉树的根节点,判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。

示例 1:
给定二叉树 [3,9,20,null,null,15,7]
    3
   / \
  9  20
    /  \
   15   7
返回 true 。
示例 2:
给定二叉树 [1,2,2,3,3,null,null,4,4]
       1
      / \
     2   2
    / \
   3   3
  / \
 4   4
返回 false

解题思路1

递归(深度优先搜索)
自底而上

var isBalanced = function(root) {
  if(!root) return true;
  let left = dfs(root.left);
  let right = dfs(root.right);
  if(Math.abs(left-right)>1) return false;
  return isBalanced(root.left) && isBalanced(root.right)
};
var dfs = function(root){
    if(!root) return 0;
    return Math.max(dfs(root.left),dfs(root.right)) + 1
}

解题思路2

广度优先搜索

var isBalanced = function(root) {
  if(!root) return true
  let queue = [ root ]
  let nodes = []
  while ( queue.length ) {
    let node = queue.shift()
    nodes.unshift(node)
    node.left && queue.push( node.left )
    node.right && queue.push( node.right )
  }
  for(let node of nodes){
    let left = node.left ? node.left.val : 0
    let right = node.right ? node.right.val : 0
    if(Math.abs(left - right) > 1) return false
    node.val = Math.max(left, right) + 1         // 当前节点值变为最大深度
  }
  return true
};

二叉搜索树的最近公共祖先

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]
在这里插入图片描述

示例 1:
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
输出: 6 
解释: 节点 2 和节点 8 的最近公共祖先是 6。
示例 2:
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
输出: 2
解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。

解题思路1

可以看出该题核心在于节点本身比其左子树节点大,比其右子树节点小
递归版本

var lowestCommonAncestor = function(root, p, q) {
    if ((root.val - p.val) * (root.val - q.val) <= 0) return root
    if (p.val < root.val) return lowestCommonAncestor(root.left, p, q)
    return lowestCommonAncestor(root.right, p, q)
};

解题思路2

广度优先搜索版本

var lowestCommonAncestor = function (root, p, q) {
    if (!root) {
        return null
    }
    if (p.val === q.val) {
        return q
    }
    while (root) {
        if (root.val < q.val && root.val < p.val) {
            root = root.right
        }
        if (root.val > q.val && root.val > p.val) {
            root = root.left
        }
        else {
            return root
        }
    }
};

二叉树的公共祖先

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]
在这里插入图片描述

示例 1:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。
示例 2:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。

解题思路

这个跟上一题一个不同点是各个节点之间没有规律性
定义左子树left与右子树right
如果满足 left&&right || ((root.val == p || root.val == q) && (left || right))
即该节点左子树包含q或者p且右子树包含p或q 或者该节点为p或q且其下左右任意一个子树包含p或q 说明该节点为最深公共祖先。
判断是否该节点的左右树包含p或q的条件应该是 left || right|| (root.val === p.val || root.val === q.val) 即该节点左子树包含或右子树包含或本身值为p或q
这里体现递归的思想

var lowestCommonAncestor = function(root, p, q) {
    let result;
    let dfs = (root,p,q) => {
        if(!root) return false;
        let left = dfs(root.left,p,q);
        let right = dfs(root.right,p,q);
        if(left&&right ||((root.val == p || root.val == q) && (left || right))){
            result = root
        }
        return left || right || (root.val === p.val || root.val === q.val)
    }
    dfs(root, p, q);
    return result
};
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值