image.png
对于树这个结构,最常见的就是二叉树。我们除了需要了解二叉树的基本操作之外,还需要了解一些特殊的二叉树,比如二叉搜索树、平衡二叉树等,另外还要熟悉二叉树的遍历方式,比如前序遍历中序遍历后序遍历层序遍历。另外还要知道二叉树的常用遍历的方式:深度优先遍历广度优先遍历

1. 二叉树的概念

关于“树”,有三个比较相似的概念:高度(Height)、深度(Depth)、(Level)。它们的定义是这样的:

  • 节点的高度:节点到叶子节点的最长路径(边数)
  • 节点的深度:根节点到这个节点所经历的边的个数
  • 节点的层数:节点的深度 +1
  • 树的高度:根节点的高度

image.png

(1)二叉树

二叉树(binary tree)是一种特殊的树,它是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树。二叉树的递归定义为:二叉树是一棵空树,或者是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;左子树和右子树又同样都是二叉树。

二叉树具有以下特点:

  • 每个结点最多有两颗子树,结点的度最大为2(一个节点含有的子树的个数称为该节点的度)。
  • 左子树和右子树是有顺序的,顺序不能颠倒。
  • 即使某结点只有一个子树,也要区分左右子树。

存储二叉树有两种方法,

  • 基于指针或者引用的二叉链式存储法,
  • 基于数组的顺序存储法。

1)链式存储法
从下图可以看到,每个节点有三个字段,其中一个存储数据,另外两个是指向左右子节点的指针。我们只要拎住根节点,就可以通过左右子节点的指针,把整棵树都串起来。这种存储方式比较常用。大部分二叉树代码都是通过这种结构来实现的。
image.jpeg
2)顺序存储法
把根节点存储在下标 i = 1 的位置,那左子节点存储在下标 2 i = 2 的位置,右子节点存储在 2 i + 1 = 3 的位置。以此类推,B 节点的左子节点存储在 2 i = 2 2 = 4 的位置,右子节点存储在 2 i + 1 = 2 2 + 1 = 5 的位置。
image.jpeg
如果节点 X 存储在数组中下标为 i 的位置,下标为 2 i 的位置存储的就是左子节点,下标为 2 i + 1 的位置存储的就是右子节点。反过来,下标为 i/2 的位置存储就是它的父节点。通过这种方式,只要知道根节点存储的位置(一般情况下,为了方便计算子节点,根节点会存储在下标为 1 的位置),这样就可以通过下标计算,把整棵树都串起来。

不过,上面是一棵完全二叉树,所以仅仅“浪费”了一个下标为 0 的存储位置。如果是非完全二叉树,其实会浪费比较多的数组存储空间。看下面这个例子:
image.jpeg
所以,如果某棵二叉树是一棵完全二叉树,那用数组存储无疑是最节省内存的一种方式。因为数组的存储方式并不需要像链式存储法那样,要存储额外的左右子节点的指针。这也是为什么完全二叉树会单独拎出来的原因,也是为什么完全二叉树要求最后一层的子节点都靠左的原因。

(2)满二叉树

所有的分支结点都存在左子树和右子树,并且所有的叶子结点都在同一层上,这样就是满二叉树。就是完美圆满的意思,关键在于树的平衡。
数据结构与算法之二叉树篇 - 图6
根据满二叉树的定义,得到其特点为:

  1. 叶子只能出现在最下一层。
  2. 非叶子结点度一定是2。
  3. 在同样深度的二叉树中,满二叉树的结点个数最多,叶子树最多。

    (3)完全二叉树

    对一棵具有n个结点的二叉树按层序排号,如果编号为i的结点与同样深度的满二叉树编号为i结点在二叉树中位置完全相同,就是完全二叉树。满二叉树必须是完全二叉树,反过来不一定成立。其中关键点是按层序编号,然后对应查找。下图就是一个完全二叉树:
    数据结构与算法之二叉树篇 - 图7
    结合完全二叉树定义得到其特点:

  4. 叶子结点只能出现在最下一层(满二叉树继承而来)

  5. 最下层叶子结点一定集中在左 部连续位置。
  6. 倒数第二层,如有叶子节点,一定出现在右部连续位置。
  7. 同样结点树的二叉树,完全二叉树的深度最小(满二叉树也是对的)。

完全二叉树的性质:

  1. 具有 n 个结点的完全二叉树的深度为 K =「log2n」+1(取下整数)
  2. 有 n 个结点的完全二叉树各结点如果用顺序方式存储,则结点之间有如下关系: 若 i 为结点编号(从1开始编号)则 如果 i>1,则其父结点的编号为 i/2;
  3. 完全二叉树,如果 2 i <= n,则其左儿子(即左子树的根结点)的编号为2 i;若2 i > n,则无左儿子;如果 2 i + 1 <= n,则其右儿子的结点编号为 2 i + 1;若 2 i + 1 > n,则无右儿子。

    (4)二叉查找树

    二叉查找树是二叉树中最常用的一种类型,也叫二叉搜索树。顾名思义,二叉查找树是为了实现快速查找而生的。不过,它不仅仅支持快速查找一个数据,还支持快速插入、删除一个数据。

二叉查找树的根节点的值大于其左子树中任意一个节点的值,小于其右节点中任意一节点的值,这一规则适用于二叉查找树中的每一个节点。下图就是二叉查找树:
image.png
二叉排序树要么是空二叉树,要么具有如下特点:

  • 如果其根结点有左子树,那么左子树上所有结点的值都小于根结点的值;
  • 如果其根结点有右子树,那么右子树上所有结点的值都大小根结点的值;
  • 左右子树也要求都是二叉排序树;
  • 在二叉查找树中,会尽可能规避两个结点数值相等的情况;
  • 对二叉查找树进行中序遍历,就可以输出一个从小到大的有序数据队列。

在利用二叉查找树执行查找操作时,可以进行以下判断:

  • 首先判断根结点是否等于要查找的数据,如果是就返回。
  • 如果根结点大于要查找的数据,就在左子树中递归执行查找动作,直到叶子结点。
  • 如果根结点小于要查找的数据,就在右子树中递归执行查找动作,直到叶子结点。
  • 这样的“二分查找”所消耗的时间复杂度就可以降低为 O(logn)。

    (5)平衡二叉查找树(AVL)

    平衡二叉查找树具有如下几个性质:
  1. 可以是空树。
  2. 假如不是空树,任何一个结点的左子树与右子树都是平衡二叉树,并且高度之差的绝对值不超过1。

image.png
上图就是一棵平衡二叉树。

平衡二叉树是为了解决二叉查找树中出现链式结构(只有左子树或只有右子树)的情况,这样的情况出现后对我们的查找没有一点帮助,反而增加了维护的成本。

平衡因子使用两个字母来表示。第一个字母表示最小不平衡子树根结点的平衡因子,第二个字母表示最小不平衡子树较高子树的根结点的平衡因子。根据不同的情况使用不同的方法来调整失衡的子树。

2. 二叉树的操作

对于二叉树这个数据结构,只有解决了遍历问题,才能通过树来进行数据的增删查操作。所谓的二叉树遍历指的是:从树的根节点出发,按照某种次序依次访问二叉树中所有的结点,使得每个结点被访问仅且一次。

常见的二叉树遍历方式有前序遍历、中序遍历、后序遍历和层序遍历,每种遍历方法都可以用递归和迭代的方式来实现,这里的序指的是父结点的遍历顺序,前序就是先遍历父结点,中序就是中间遍历父结点,后序就是最后遍历父结点。其中前序遍历、中序遍历、后序遍历是基于深度优先遍历的,层序遍历是基于广度优先遍历的
image.png
上图中二叉树的结构及其编码:

  1. const root = {
  2. val: "A",
  3. left: {
  4. val: "B",
  5. left: {
  6. val: "D"
  7. },
  8. right: {
  9. val: "E"
  10. }
  11. },
  12. right: {
  13. val: "C",
  14. left: {
  15. val: "F"
  16. },
  17. right: {
  18. val: "G"
  19. }
  20. }
  21. };

(1)前序遍历

基本思想:先访问根结点,再先序遍历左子树,最后再先序遍历右子树,即根—左—右
遍历结果:A -> B -> D -> E -> C -> F

1)递归实现:

  1. function preorder(root){
  2. if(!root){
  3. return
  4. }
  5. console.log(root.val) // 打印当前遍历的节点
  6. preorder(root.left) // 递归遍历左子树
  7. preorder(root.right) // 递归遍历右子树
  8. }

2)非递归实现:
初始化一个栈和结果数组,将根节点放入栈中,当栈不为空时,重复下面的步骤:
(1)取出栈顶元素top,访问top
(2)若top的右子节点不为空,将top的右子节点放入栈中
(3)若top的左子节点不为空,将top的左子节点放入栈中
(4)将取出的栈顶元素top放入结果数组

  1. function preorder(root){
  2. if(!root){
  3. return [];
  4. }
  5. var result = []
  6. var stack = [root]
  7. while(stack.length){
  8. var top = stack.pop();
  9. if(top.right){
  10. stack.push(top.right);
  11. }
  12. if(top.left){
  13. stack.push(top.left);
  14. }
  15. result.push(top.val);
  16. }
  17. return result;
  18. }

(2)中序遍历

基本思想:先中序遍历左子树,然后再访问根结点,最后再中序遍历右子树,即左—根—右
遍历结果:D -> B -> E -> A -> C -> F

1)递归实现:

  1. function inorder(root) {
  2. if(!root) {
  3. return
  4. }
  5. inorder(root.left) // 递归遍历左子树
  6. console.log(root.val) // 打印当前遍历的结点
  7. inorder(root.right) // 递归遍历右子树
  8. }

2)非递归实现:
初始化一个栈和结果数组,当栈不为空时,重复下面的步骤:
(1)将根节点和所有的左子节点放入栈中,直到没有左子节点
(2)栈顶元素出栈,存入结果数组,将出栈的元素作为根节点
(3)查看该根节点右子节点是否有左子节点,若有就入栈,否则继续出栈

  1. function inorder(root) {
  2. if(!root){
  3. return [];
  4. }
  5. var result = []
  6. var stack = []
  7. while(stack.length || root){
  8. while(root){
  9. stack.push(root);
  10. root = root.left;
  11. }
  12. root = stack.pop();
  13. result.push(root.val)
  14. root = root.right;
  15. }
  16. return result;
  17. }

(3)后序遍历

基本思想:先后序遍历左子树,然后再后序遍历右子树,最后再访问根结点,即左—右—根
遍历结果:D -> E -> B -> F -> C -> A

1)递归实现:

  1. function postorder(root) {
  2. if(!root) {
  3. return
  4. }
  5. inorder(root.left) // 递归遍历左子树
  6. inorder(root.right) // 递归遍历右子树
  7. console.log(root.val) // 打印当前遍历的结点
  8. }

2)非递归实现:
初始化一个栈和结果数组,将根节点放入栈中,当栈不为空时,重复下面的步骤:
(1)取出栈顶元素top,访问top
(2)将取出的栈顶元素top放入结果数组的最开始
(3)若top的左子节点不为空,将top的左子节点放入栈中
(4)若top的右子节点不为空,将top的右子节点放入栈中

  1. function postorder(root) {
  2. if(!root){
  3. return [];
  4. }
  5. var result = []
  6. var stack = [root]
  7. while(stack.length){
  8. var top = stack.pop();
  9. result.unshift(top.val);
  10. if(top.left){
  11. stack.push(top.left);
  12. }
  13. if(top.right){
  14. stack.push(top.right);
  15. }
  16. }
  17. return result;
  18. }

(4)层序遍历

基本思想:层序遍历就是从上到下,从左到右打印二叉树的节点。
遍历结果:A -> B -> C -> D -> E -> F

创建一个数组存放结果,一个队列存放二叉树的节点,如果存放二叉树的队列不为空,就重复下面的步骤:
(1)将队列的第一个节点作为根节点,并放入结果数组中
(2)如果该根节点的左子树不为空,就将其放入队列中
(3)如果该根节点的右子树不为空,就将其放入队列中

基本实现:

  1. function levelTraversal(root){
  2. if(!root){
  3. return [];
  4. }
  5. var queue = [root];
  6. var result = [];
  7. while (queue.length){
  8. var node = queue.shift();
  9. result.push(node.val);
  10. if(node.left){
  11. queue.push(node.left);
  12. }
  13. if(node.right){
  14. queue.push(node.right);
  15. }
  16. }
  17. return result;
  18. }

(5)总结

可以看到,二叉树遍历过程中,每个结点都被访问了一次,其时间复杂度是 O(n)。接着,在找到位置后,执行增加和删除数据的操作时,只需要通过指针建立连接关系就可以了。对于没有任何特殊性质的二叉树而言,抛开遍历的时间复杂度以外,真正执行增加和删除操作的时间复杂度是 O(1)。树数据的查找操作和链表一样,都需要遍历每一个数据去判断,所以时间复杂度是 O(n)。

3. 经典题目:二叉树的遍历

(1)二叉树的前序遍历

给定一个二叉树,返回它的 前序 遍历。示例:

  1. 输入: [1,null,2,3]
  2. 1
  3. \
  4. 2
  5. /
  6. 3
  7. 输出: [1,2,3]

进阶: 递归算法很简单,你可以通过迭代算法完成吗?

迭代实现:初始化一个栈和结果数组,将根节点放入栈中,当栈不为空时,重复下面的步骤:
(1)取出栈顶元素top,访问top
(2)若top的右子节点不为空,将top的右子节点放入栈中
(3)若top的左子节点不为空,将top的左子节点放入栈中
(4)将取出的栈顶元素top放入结果数组

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val) {
  4. * this.val = val;
  5. * this.left = this.right = null;
  6. * }
  7. */
  8. /**
  9. * @param {TreeNode} root
  10. * @return {number[]}
  11. */
  12. // 迭代的写法:
  13. var preorderTraversal = function(root) {
  14. if(!root){
  15. return [];
  16. }
  17. var result = []
  18. var stack = [root]
  19. while(stack.length!==0){
  20. var top = stack.pop();
  21. if(top.right){
  22. stack.push(top.right);
  23. }
  24. if(top.left){
  25. stack.push(top.left);
  26. }
  27. result.push(top.val);
  28. }
  29. return result;
  30. };
  31. // 递归的写法:
  32. var preorderTraversal = function(root) {
  33. if(!root){
  34. return [];
  35. }
  36. var result = []
  37. var preorderTraversalNode = (node) => {
  38. if(node) {
  39. result.push(node.val)
  40. preorderTraversalNode(node.left)
  41. preorderTraversalNode(node.right)
  42. }
  43. }
  44. preorderTraversalNode(root)
  45. return result
  46. };

迭代的复杂度:

  • 时间复杂度,每个结点都入栈出栈一次,遍历整棵树的时间复杂度为 O(N),N表示二叉树的节点数。
  • 空间复杂度就是栈的最大使用空间,而这个空间是由树的高度决定的,所以空间复杂度就是 O(H),H 表示二叉树的高度。

递归的复杂度:

  • 时间复杂度,树上的每个结点都只访问一次,并且每次访问都只有一次压栈弹栈操作,所以复杂度为 O(N),N表示二叉树的节点数。
  • 空间复杂度,由于函数调用栈的深度与树的高度有关系,所以使用的空间为 O(H)。H 表示二叉树的高度。

    (2)二叉树的中序遍历

    给定一个二叉树,返回它的 中序 遍历。示例:
    1. 输入: [1,null,2,3]
    2. 1
    3. \
    4. 2
    5. /
    6. 3
    7. 输出: [1,3,2]
    进阶: 递归算法很简单,你可以通过迭代算法完成吗?

迭代实现:初始化一个栈和结果数组,当栈不为空时,重复下面的步骤:
(1)将根节点和所有的左子节点放入栈中,直到没有左子节点
(2)栈顶元素出栈,存入结果数组,将出栈的元素作为根节点
(3)查看该根节点右子节点是否有左子节点,若有就入栈,否则继续出栈

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val) {
  4. * this.val = val;
  5. * this.left = this.right = null;
  6. * }
  7. */
  8. /**
  9. * @param {TreeNode} root
  10. * @return {number[]}
  11. */
  12. // 迭代的实现:
  13. var inorderTraversal = function(root) {
  14. if(!root){
  15. return [];
  16. }
  17. var result = []
  18. var stack = []
  19. while(stack.length!==0||root){
  20. while(root){
  21. stack.push(root);
  22. root = root.left;
  23. }
  24. root = stack.pop();
  25. result.push(root.val)
  26. root = root.right;
  27. }
  28. return result;
  29. };
  30. // 递归的实现
  31. var inorderTraversal = function(root) {
  32. if(!root){
  33. return [];
  34. }
  35. var result = []
  36. var inorderTraversalNode = (node) => {
  37. if(node) {
  38. inorderTraversalNode(node.left)
  39. result.push(node.val)
  40. inorderTraversalNode(node.right)
  41. }
  42. }
  43. inorderTraversalNode(root)
  44. return result
  45. };

迭代的复杂度:

  • 时间复杂度,每个结点都入栈出栈一次,遍历整棵树的时间复杂度为 O(N),N表示二叉树的节点数。
  • 空间复杂度就是栈的最大使用空间,而这个空间是由树的高度决定的,所以空间复杂度就是 O(H),H 表示二叉树的高度。

递归的复杂度:

  • 时间复杂度,树上的每个结点都只访问一次,并且每次访问都只有一次压栈弹栈操作,所以复杂度为 O(N),N表示二叉树的节点数。
  • 空间复杂度,由于函数调用栈的深度与树的高度有关系,所以使用的空间为 O(H)。H 表示二叉树的高度。

    (3)二叉树的后序遍历

    给定一个二叉树,返回它的后序遍历。示例:
    1. 输入: [1,null,2,3]
    2. 1
    3. \
    4. 2
    5. /
    6. 3
    7. 输出: [3,2,1]
    进阶: 递归算法很简单,你可以通过迭代算法完成吗?

迭代实现:二叉树的后序遍历和前序遍历是一个相反的过程,所以基本思路和前序遍历类似。初始化一个栈和结果数组,将根节点放入栈中,当栈不为空时,重复下面的步骤:
(1)取出栈顶元素top,访问top
(2)将取出的栈顶元素top放入结果数组的最开始
(3)若top的左子节点不为空,将top的左子节点放入栈中
(4)若top的右子节点不为空,将top的右子节点放入栈中

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val) {
  4. * this.val = val;
  5. * this.left = this.right = null;
  6. * }
  7. */
  8. /**
  9. * @param {TreeNode} root
  10. * @return {number[]}
  11. */
  12. // 迭代的实现:
  13. var postorderTraversal = function(root) {
  14. if(!root){
  15. return [];
  16. }
  17. var result = []
  18. var stack = [root]
  19. while(stack.length!==0){
  20. var top = stack.pop();
  21. result.unshift(top.val);
  22. if(top.left){
  23. stack.push(top.left);
  24. }
  25. if(top.right){
  26. stack.push(top.right);
  27. }
  28. }
  29. return result;
  30. };
  31. // 递归的实现
  32. var postorderTraversal = function(root) {
  33. if(!root){
  34. return [];
  35. }
  36. var result = []
  37. var postorderTraversalNode = (node) => {
  38. if(node) {
  39. postorderTraversalNode(node.left)
  40. result.push(node.val)
  41. postorderTraversalNode(node.right)
  42. }
  43. }
  44. postorderTraversalNode(root)
  45. return result
  46. };

迭代的复杂度:

  • 时间复杂度,每个结点都入栈出栈一次,遍历整棵树的时间复杂度为 O(N),N表示二叉树的节点数。
  • 空间复杂度就是栈的最大使用空间,而这个空间是由树的高度决定的,所以空间复杂度就是 O(H),H 表示二叉树的高度。

递归的复杂度:

  • 时间复杂度,树上的每个结点都只访问一次,并且每次访问都只有一次压栈弹栈操作,所以复杂度为 O(N),N表示二叉树的节点数。
  • 空间复杂度,由于函数调用栈的深度与树的高度有关系,所以使用的空间为 O(H)。H 表示二叉树的高度。

    (4)二叉树的层序遍历

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

    1. 二叉树:[3,9,20,null,null,15,7],
    2. 3
    3. / \
    4. 9 20
    5. / \
    6. 15 7

    返回其层次遍历结果:

    1. [
    2. [3],
    3. [9,20],
    4. [15,7]
    5. ]

    创建一个数组存放结果,一个队列存放二叉树的节点,根据输出的要求,设置一个level,储存当前层数,如果存放二叉树的队列不为空,就重复下面的步骤:
    (1)将队列的第一个节点作为根节点,并放入当前层的结果数组中
    (2)如果该根节点的左子树不为空,就将其放入队列中
    (3)如果该根节点的右子树不为空,就将其放入队列中
    (4)遍历完该层之后,就遍历下一层

    1. /**
    2. * Definition for a binary tree node.
    3. * function TreeNode(val) {
    4. * this.val = val;
    5. * this.left = this.right = null;
    6. * }
    7. */
    8. /**
    9. * @param {TreeNode} root
    10. * @return {number[][]}
    11. */
    12. var levelOrder = function(root) {
    13. if(!root){
    14. return [];
    15. }
    16. var queue = [root];
    17. var result = [];
    18. var level = 0;
    19. while (queue.length!==0){
    20. result[level]=[];
    21. var levelNum = queue.length;
    22. while(levelNum--){
    23. var node = queue.shift();
    24. result[level].push(node.val);
    25. if(node.left){
    26. queue.push(node.left);
    27. }
    28. if(node.right){
    29. queue.push(node.right);
    30. }
    31. }
    32. level++;
    33. }
    34. return result;
    35. };

    复杂度分析:

  • 时间复杂度:每个点进队出队各一次,故渐进时间复杂度为 O(n),其中n是二叉树的节点数。

  • 空间复杂度:队列中元素的个数不超过 n 个,故渐进空间复杂度为 O(n),其中n是二叉树的节点数。

    (5)二叉树的层次遍历 II

    给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)。例如:给定二叉树 [3,9,20,null,null,15,7],
    1. 3
    2. / \
    3. 9 20
    4. / \
    5. 15 7
    返回其自底向上的层次遍历为:
    1. [
    2. [15,7],
    3. [9,20],
    4. [3]
    5. ]
    对于这道题目,对二叉树进行层序遍历,最直接的方法就是使用BFS(广度优先遍历)。

首先创建一个队列,将当前节点放进去,队列中的节点始终是当前层的节点。按顺序出列,加入结果中,并将当前节点的子节点加入到队列中。重复上述步骤,直到队列为空,就遍历完了整个二叉树。

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val) {
  4. * this.val = val;
  5. * this.left = this.right = null;
  6. * }
  7. */
  8. /**
  9. * @param {TreeNode} root
  10. * @return {number[][]}
  11. */
  12. var levelOrderBottom = function(root) {
  13. if(!root) {
  14. return []
  15. }
  16. const queue = []
  17. queue.push(root)
  18. const res = [] // 用来储存最后的结果
  19. while(queue.length){
  20. const subRes = [] // 用来储存每一层的节点值
  21. const levelSize = queue.length
  22. for(let i = 0; i < levelSize; i++){
  23. const cur = queue.shift()
  24. subRes.push(cur.val)
  25. if(cur.left){
  26. queue.push(cur.left)
  27. }
  28. if(cur.right){
  29. queue.push(cur.right)
  30. }
  31. }
  32. res.unshift(subRes)
  33. }
  34. return res
  35. };

复杂度分析:

  • 时间复杂度:O(n),其中 n 是二叉树中的节点数。每个节点访问一次,结果列表使用链表的结构时,在结果列表头部添加一层节点值的列表的时间复杂度是 O(1),因此总时间复杂度是 O(n)。
  • 空间复杂度:O(n),其中 n 是二叉树中的节点数。空间复杂度取决于队列开销,队列中的节点个数不会超过 n。

    (6)二叉树的锯齿形层序遍历

    给定一个二叉树,返回其节点值的锯齿形层次遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。例如:给定二叉树 [3,9,20,null,null,15,7]
    1. 3
    2. / \
    3. 9 20
    4. / \
    5. 15 7
    返回锯齿形层次遍历如下:
    1. [
    2. [3],
    3. [20,9],
    4. [15,7]
    5. ]
    可以采用递归的方式来解答,每一层都创建一个数组,奇数层从左往右依次插入数组,偶数层从右往左依次插入数组。

思路不是很难,这里我们使用i & 1来判断层数的奇偶:

  1. i & 1 == 1 // 奇数
  2. i & 1 == 0 // 偶数
  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val) {
  4. * this.val = val;
  5. * this.left = this.right = null;
  6. * }
  7. */
  8. /**
  9. * @param {TreeNode} root
  10. * @return {number[][]}
  11. */
  12. var zigzagLevelOrder = function(root) {
  13. const res = []
  14. function dfs(i, current){
  15. if(!current) return
  16. if(!Array.isArray(res[i])){
  17. res[i] = []
  18. }
  19. if(i & 1){
  20. res[i].unshift(current.val)
  21. }else{
  22. res[i].push(current.val)
  23. }
  24. dfs(i + 1, current.left)
  25. dfs(i + 1, current.right)
  26. }
  27. dfs(0, root)
  28. return res
  29. };

复杂度分析:

  • 时间复杂度:O(n),其中 n 为二叉树的节点数。每个节点会且仅会被遍历一次,时间复杂度为 O(n)。
  • 空间复杂度:O(n),其中 n 为二叉树的节点数。我们需要维护存储节点的队列和存储节点值的双端队列,空间复杂度为 O(n)。

    4. 经典题目:二叉树的属性

    (1)二叉树的完全性校验

    给定一个二叉树,确定它是否是一个完全二叉树百度百科中对完全二叉树的定义如下:

    若设二叉树的深度为 h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。(注:第 h 层可能包含 1~ 2 个节点。)

示例 1:
数据结构与算法之二叉树篇 - 图11

  1. 输入:[1,2,3,4,5,6]
  2. 输出:true
  3. 解释:最后一层前的每一层都是满的(即,结点值为 {1} {2,3} 的两层),且最后一层中的所有结点({4,5,6})都尽可能地向左。

示例 2:
数据结构与算法之二叉树篇 - 图12

  1. 输入:[1,2,3,4,5,null,7]
  2. 输出:false
  3. 解释:值为 7 的结点没有尽可能靠向左侧。

提示:树中将会有 1 到 100 个结点。

对于这道题目,我们可以使用层序遍历来解决。在层序遍历的过程中,需要用一个index来维护节点的索引,如果一个节点的index,那它的左孩子的索引是index * 2,右孩子的索引是index * 2 + 1

这里我们初始化一个队列,用来存储当前节点node和当前节点的索引值index。使用一个count来记录当前已经遍历到第几个节点。如果当前节点的索引值index和count + 1相等,那么说明当前节点的位置时正确的,就继续遍历,如果不相等,说明中间缺少了节点,直接返回false,结束遍历。

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val, left, right) {
  4. * this.val = (val===undefined ? 0 : val)
  5. * this.left = (left===undefined ? null : left)
  6. * this.right = (right===undefined ? null : right)
  7. * }
  8. */
  9. /**
  10. * @param {TreeNode} root
  11. * @return {boolean}
  12. */
  13. var isCompleteTree = function(root) {
  14. if(!root){
  15. return true
  16. }
  17. let count = 0
  18. const queue = [{ node: root, index: 1 }]
  19. while(queue.length){
  20. const temp = queue.shift()
  21. const node = temp.node
  22. const index = temp.index
  23. // 判断当前节点是否是正确的顺序值
  24. if(index !== ++count){
  25. return false
  26. }
  27. // 遍历当前节点的左右子树
  28. node.left && queue.push({node: node.left, index: index * 2})
  29. node.right && queue.push({node: node.right, index: index * 2 + 1})
  30. }
  31. return true
  32. };

复杂度分析:

  • 时间复杂度:O(n),这里最坏的情况就是我们需要遍历整棵二叉树,所以时间复杂度为O(n),其中n是二叉树的节点数;
  • 空间复杂度:O(1),我们需要初始化一个队列来保存当前遍历的节点,这个队列是一个常数空间,所以空间复杂度为O(1)。

    (2)二叉树中的最大路径和

    给定一个非空二叉树,返回其最大路径和。本题中,路径被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。
    示例 1:
    1. 输入:[1,2,3]
    2. 1
    3. / \
    4. 2 3
    5. 输出:6
    示例 2:
    1. 输入:[-10,9,20,null,null,15,7]
    2. -10
    3. / \
    4. 9 20
    5. / \
    6. 15 7
    7. 输出:42
    对于这道题目,我们可以使用递归遍历二叉树,我们需要的是最大的路径和,所以某个节点左右子树路径和和这个节点的值的和的最大值就是我们要求的解。

需要注意:

  • 一条从父节点延伸下来的路径,只能进入左子树或者右子树,不能同时进入左右子树。
  • 只有在最大贡献值大于 0 时,才会选取对应子节点。

    1. /**
    2. * Definition for a binary tree node.
    3. * function TreeNode(val) {
    4. * this.val = val;
    5. * this.left = this.right = null;
    6. * }
    7. */
    8. /**
    9. * @param {TreeNode} root
    10. * @return {number}
    11. */
    12. var maxPathSum = function(root) {
    13. let sum = Number.MIN_SAFE_INTEGER
    14. const dfs = (root) => {
    15. if(!root){
    16. return 0
    17. }
    18. // 计算左右子树的最大路径和
    19. const left = dfs(root.left)
    20. const right = dfs(root.right)
    21. // 计算总的最大路径和
    22. const maxSum = left + root.val + right
    23. sum = Math.max(sum, maxSum)
    24. // 返回当前计算出的最大路径
    25. const max = root.val + Math.max(left, right)
    26. return max < 0 ? 0 : max
    27. }
    28. dfs(root)
    29. return sum
    30. };

    复杂度分析:

  • 时间复杂度:O(N),其中 N 是二叉树中的节点个数。对每个节点访问不超过 2 次。

  • 空间复杂度:O(N),其中 N 是二叉树中的节点个数。空间复杂度主要取决于递归调用层数,最大层数等于二叉树的高度,最坏情况下,二叉树的高度等于二叉树中的节点个数。

    (3)二叉树的直径

    给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。示例:给定二叉树
    1. 1
    2. / \
    3. 2 3
    4. / \
    5. 4 5
    返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。

遇到二叉树的问题,我们在遍历时通常是采用深度优先遍历和广度优先遍历,这里需要求直径,我们就使用到了深度优先遍历。

从根节点进行遍历,在遍历到每个节点的时候,将其左右子树的最大深度加在一起,与结果res对比,并将最大的值赋值给热搜,这样使res一直保持是最大的值。最后返回res即可。

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val) {
  4. * this.val = val;
  5. * this.left = this.right = null;
  6. * }
  7. */
  8. /**
  9. * @param {TreeNode} root
  10. * @return {number}
  11. */
  12. var diameterOfBinaryTree = function(root) {
  13. let res = 0
  14. function depth(rootNode){
  15. if(!rootNode) return 0
  16. let l = depth(rootNode.left) // l为左子树的深度
  17. let r = depth(rootNode.right) // r为右子树的深度
  18. res = Math.max(res, l + r) // 计算最大直径l+r,更新res,能保持其一直是最大值
  19. return Math.max(l ,r) + 1 // 返回以该节点为根的子树的深度
  20. }
  21. depth(root)
  22. return res
  23. };

复杂度分析:

  • 时间复杂度:O(n),其中n为二叉树的节点数,这里需要遍历整棵二叉树,所以时间复杂度为O(n)。
  • 空间复杂度:O(h),其中h是二叉树的最大深度,是一个常数变量。

    (4)二叉树的所有路径

    给定一个二叉树,返回所有从根节点到叶子节点的路径。说明: 叶子节点是指没有子节点的节点。示例: ```javascript 输入:

    1 / \ 2 3 \ 5

输出: [“1->2->5”, “1->3”]

解释: 所有根节点到叶子节点的路径为: 1->2->5, 1->3

  1. 这个题目就是对二叉树数进行深度优先遍历,在遍历的过程中将当前节点的值存储在字符串中,直到没有子节点,就将这个遍历出的结果字符串存入结果数组中。
  2. ```javascript
  3. /**
  4. * Definition for a binary tree node.
  5. * function TreeNode(val) {
  6. * this.val = val;
  7. * this.left = this.right = null;
  8. * }
  9. */
  10. /**
  11. * @param {TreeNode} root
  12. * @return {string[]}
  13. */
  14. var binaryTreePaths = function(root) {
  15. if(!root) return []
  16. let res = []
  17. const buildPath = (root, resStr) => {
  18. if(!root.left && !root.right){
  19. resStr += root.val
  20. res.push(resStr)
  21. return
  22. }
  23. resStr += root.val + '->'
  24. if(root.left){
  25. buildPath(root.left, resStr)
  26. }
  27. if(root.right){
  28. buildPath(root.right, resStr)
  29. }
  30. }
  31. buildPath(root, '')
  32. return res
  33. };

复杂度分析

  • 时间复杂度:O(N2),其中 N 表示节点数目。在深度优先搜索中每个节点会被访问一次且只会被访问一次,每一次会对 path 变量进行拷贝构造,时间代价为 O(N),故时间复杂度为 O(N2);
  • 空间复杂度:O(N2),其中 N 表示节点数目。除答案数组外我们需要考虑递归调用的栈空间。在最坏情况下,当二叉树中每个节点只有一个孩子节点时,即整棵二叉树呈一个链状,此时递归的层数为 N,此时每一层的 path 变量的空间代价的总和的空间复杂度为 O(N2),最好情况下,当二叉树为平衡二叉树时,它的高度为 logN,此时空间复杂度为O((logN)2)。

    (5)对称的二叉树

    给定一个二叉树,检查它是否是镜像对称的。例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
    1. 1
    2. / \
    3. 2 2
    4. / \ / \
    5. 3 4 4 3
    但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:5r
    1. 1
    2. / \
    3. 2 2
    4. \ \
    5. 3 3
    进阶:你可以运用递归和迭代两种方法解决这个问题吗?

递归的思路比较简单,具体实现思路如下:

  • 首先判断当前树是否为空,空则直接返回true,否则就左子树的左子树和右子树的右子树是否相等
  • 如果左节点或者右节点为空时,就比较对应的右节点或左节点是否为空,为空则返回true,否则就返回false
  • 如果左右节点都不为空,就判断左节点的左节点和右节点的右节点是否相等
  • 如果相等,就传入该节点的子节点进行递归,否则就返回false

迭代方法需要借助队列来实现,具体实现思路如下:通过「同步移动」两个指针的方法来遍历这棵树,l 指针和 r 指针一开始都指向这棵树的根,随后 l 右移时,r 左移,l 左移时,r 右移。每次检查当前 l 和 r 节点的值是否相等,如果相等再判断左右子树是否对称。

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val) {
  4. * this.val = val;
  5. * this.left = this.right = null;
  6. * }
  7. */
  8. /**
  9. * @param {TreeNode} root
  10. * @return {boolean}
  11. */
  12. // 迭代的实现
  13. const isSymmetric = (root) => {
  14. if (!root) return true
  15. let queue = [root.left, root.right]
  16. while (queue.length > 0) {
  17. let node1 = queue.shift(), node2 = queue.shift()
  18. if (node1 === null && node2 === null) continue
  19. if (node1 === null || node2 === null || node1.val !== node2.val) return false
  20. queue.push(node1.left, node2.right, node1.right, node2.left)
  21. }
  22. return true
  23. }
  24. // 递归的实现
  25. var isSymmetric = function(root) {
  26. if(!root){
  27. return true
  28. }
  29. return isSameTree(root.left, root.right)
  30. };
  31. const isSameTree = (l, r) => {
  32. if(!l) return r === null
  33. if(!r) return l === null
  34. if(l.val !== r.val) return false
  35. return isSameTree(l.left, r.right,) && isSameTree(l.right, r.left)
  36. }

递归实现的复杂度分析:

  • 时间复杂度:这里遍历了这棵树,渐进时间复杂度为 O(n),其中n是这棵树的节点数。
  • 空间复杂度:这里的空间复杂度和递归使用的栈空间有关,这里递归层数不超过 n,故渐进空间复杂度为 O(n)。

迭代实现的复杂度分析:

  • 时间复杂度:这里遍历了这棵树,渐进时间复杂度为 O(n),其中n是这棵树的节点数。
  • 空间复杂度:这里需要用一个队列来维护节点,每个节点最多进队一次,出队一次,队列中最多不会超过 n 个点,故渐进空间复杂度为 O(n)。

    (6)二叉树的层平均值

    给定一个非空二叉树, 返回一个由每层节点平均值组成的数组。示例 1:
    1. 输入:
    2. 3
    3. / \
    4. 9 20
    5. / \
    6. 15 7
    7. 输出:[3, 14.5, 11]
    8. 解释:
    9. 0 层的平均值是 3 , 1层是 14.5 , 2层是 11 。因此返回 [3, 14.5, 11]
    提示:节点值的范围在32位有符号整数范围内。

这是一道比较简单的题目,就是二叉树的层序遍历。这里使用BFS(广度优先遍历),在遍历的过程中,将每层的节点值保存在队列中,然后将所有值出栈并相加。除以当前层的队列的长度就是这一层的平均值。将其放入结果中。重复上述步骤,直到遍历完整棵二叉树,返回最后的结果。

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val) {
  4. * this.val = val;
  5. * this.left = this.right = null;
  6. * }
  7. */
  8. /**
  9. * @param {TreeNode} root
  10. * @return {number[]}
  11. */
  12. var averageOfLevels = function(root) {
  13. if(!root){
  14. return []
  15. }
  16. const res = []
  17. const queue = []
  18. queue.push(root)
  19. while(queue.length){
  20. const len = queue.length
  21. let sum = 0
  22. for(let i = 0; i < len; i++){
  23. const cur = queue.shift()
  24. sum += cur.val
  25. if(cur.left){
  26. queue.push(cur.left)
  27. }
  28. if(cur.right){
  29. queue.push(cur.right)
  30. }
  31. }
  32. res.push(sum / len)
  33. }
  34. return res
  35. };

复杂度分析:

  • 时间复杂度:O(n),其中 n 是二叉树中的节点个数。广度优先搜索需要对每个节点访问一次,时间复杂度是 O(n)。需要对二叉树的每一层计算平均值,时间复杂度是 O(h),其中 h 是二叉树的高度,任何情况下都满足h≤n。因此总时间复杂度是 O(n)。
  • 空间复杂度:O(n),其中 n 是二叉树中的节点个数。空间复杂度取决于队列开销,队列中的节点个数不会超过 n。

    (7)二叉树的右视图

    给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。示例:

    1. 输入: [1,2,3,null,5,null,4]
    2. 输出: [1, 3, 4]
    3. 解释:
    4. 1 <---
    5. / \
    6. 2 3 <---
    7. \ \
    8. 5 4 <---

    对于二叉树的题目,最常用的方法就是深度优先遍历(DFS)和广度优先遍历(BFS)。下面就来用这两种方法来解决这个问题。

DFS:

  • 设置一个level,来保存当前遍历的二叉树的层级,初始值为0
  • 由于我们需要返回的是右视图的节点值,所以先遍历右节点的值,将右节点保存在结果数组中
  • 然后遍历左节点
  • 当结果数组的长度和二叉树当前的层级相同时,就将当前的节点值保存
  • 重复上述步骤,直至遍历完二叉树的所有节点

BFS:
使用广度优先遍历来遍历二叉树,这就相当于二叉树的层序遍历,对于每一层的遍历结果,取最后一个即可,这里我们使用队列来处理。

  • 初始化一个队列,将根节点加入到队列中
  • 当队列不为空的时候,就将队列的元素出队,将最后一个元素加入到结果数组中
  • 在元素出队列的时候,将元素的左右子节点分别加入到队列中
  • 重复上面的第二三步,直至队列为空 ```javascript /**
    • Definition for a binary tree node.
    • function TreeNode(val) {
    • this.val = val;
    • this.left = this.right = null;
    • } / /*
    • @param {TreeNode} root
    • @return {number[]} */

// DFS的实现: var rightSideView = function(root) { if(!root) return [] let res = [] dfs(root, 0, res) return res };

function dfs(root, level, res){ if(root){ if(res.length === level){ res.push(root.val) }

  1. dfs(root.right, level+1, res)
  2. dfs(root.left, level+1, res)
  3. }

}

// BFS的实现: var rightSideView = function(root) { if(!root) return [] let res = [] let queue = [root] while(queue.length > 0){ let len = queue.length

  1. while(len){
  2. let node = queue.shift()
  3. if(len === 1){
  4. res.push(node.val)
  5. }
  6. if(node.left){
  7. queue.push(node.left)
  8. }
  9. if(node.right){
  10. queue.push(node.right)
  11. }
  12. len--
  13. }
  14. }
  15. return res

};

  1. **DFS复杂度分析:**
  2. - 时间复杂度 : O(n)。其中n是二叉树的节点数,深度优先搜索最多访问每个结点一次,因此是线性复杂度。
  3. - 空间复杂度 : O(n)。其中n是二叉树的节点数,最坏情况下,栈内会包含接近树高度的结点数量,占用 O(n) 的空间。
  4. **BFS复杂度分析:**
  5. - 时间复杂度 : O(n)。其中n是二叉树的节点数,每个节点最多进队列一次,出队列一次,因此广度优先搜索的复杂度为线性。
  6. - 空间复杂度 : O(n)。其中n是二叉树的节点数,每个节点最多进队列一次,所以队列长度最大不不超过 n,所以这里的空间代价为 O(n)。
  7. <a name="DuuM2"></a>
  8. #### (8)完全二叉树的节点个数
  9. 给你一棵 **完全二叉树** 的根节点 `root` ,求出该树的节点个数。
  10. [完全二叉树](https://baike.baidu.com/item/%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91/7773232?fr=aladdin) 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 `h` 层,则该层包含 `1~ 2` 个节点。
  11. **示例 1:**<br />![](https://cdn.nlark.com/yuque/0/2021/jpeg/1500604/1616318342325-054b6120-a9cf-47f8-8526-82faca2672c2.jpeg#height=302&id=Qx43Y&originHeight=302&originWidth=372&originalType=binary&ratio=1&rotation=0&showTitle=false&size=0&status=done&style=shadow&title=&width=372)
  12. ```javascript
  13. 输入:root = [1,2,3,4,5,6]
  14. 输出:6

示例 2:

  1. 输入:root = []
  2. 输出:0

示例 3:

  1. 输入:root = [1]
  2. 输出:1

提示:

  • 树中节点的数目范围是[0, 5 * 10]
  • 0 <= Node.val <= 5 * 10
  • 题目数据保证输入的树是 完全二叉树

进阶: 遍历树来统计节点是一种时间复杂度为 O(n) 的简单解决方案。你可以设计一个更快的算法吗?

对于这道题目,我们可以使用深度优先遍历或者广度优先遍历来计算二叉树的节点数。

1)深度优先遍历: 深度优先遍历就很简单了,题中传入的是二叉树,需要返回的是二叉树,所以可以直接进行递归计算二叉树的节点数:

  • 如果子树为空节点数为 0
  • 如果子树存在左子树或右子树则节点数+1,继续递归分别求左子树节点数和右子树节点数

复杂度分析:

  • 时间复杂度:O(n),其中n是二叉树的节点数,我们需要遍历一遍整个二叉树;
  • 空间复杂度:O(h),其中h是二叉树的高度,递归的时间复杂度为O(h)。

2)广度优先遍历: 广度优先遍历往往是初始化一个对来保存当前层的节点,在对当前层的节点进行操作。对于这题目,我们只需要在节点进入队列的时候,结果加一即可。

复杂度分析:

  • 时间复杂度:O(n),其中n是二叉树的节点数,我们需要遍历一遍整个二叉树;
  • 空间复杂度:O(n),其中n是队列的长度,我们需要将每一层都放在队列中。

3)二分法
上面的两种方法的时间复杂度都是O(n),下面用二分法来解决,时间复杂度会降低。

我们知道,对于一个完全二叉树,它的所有子树都是完全二叉树,有的子树是满二叉树,满二叉树的的节点个数计算公式如下:2-1,其中h为当前树的高度。

那什么情况下就是满二叉树呢,我们知道,二叉树中有个树的深度的概念,指的就是根节点到这个节点所经历的边的个数, 所以我们只需要判断左右子树的高度手否相等来判断当前树是不是满二叉树。

如果不是满二叉树,那就是规模小一点的完全二叉树,就进行递归处理。

复杂度分析:

  • 时间复杂度:每次递归调用对应了一层树高,调用logN次,每次调用计算完全二叉树的高度需要O(logN),所以时间复杂度为O(logN)
  • 空间复杂度:O(1),我们只需要维护有限的额外空间。

广度优先遍历:

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val, left, right) {
  4. * this.val = (val===undefined ? 0 : val)
  5. * this.left = (left===undefined ? null : left)
  6. * this.right = (right===undefined ? null : right)
  7. * }
  8. */
  9. /**
  10. * @param {TreeNode} root
  11. * @return {number}
  12. */
  13. var countNodes = function(root) {
  14. if(!root){
  15. return 0
  16. }
  17. let queue = [root]
  18. let res = 1
  19. while(queue.length){
  20. let node = queue.shift()
  21. if(node.left){
  22. queue.push(node.left)
  23. res++
  24. }
  25. if(node.right){
  26. queue.push(node.right)
  27. res++
  28. }
  29. }
  30. return res
  31. };

深度优先遍历:

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val, left, right) {
  4. * this.val = (val===undefined ? 0 : val)
  5. * this.left = (left===undefined ? null : left)
  6. * this.right = (right===undefined ? null : right)
  7. * }
  8. */
  9. /**
  10. * @param {TreeNode} root
  11. * @return {number}
  12. */
  13. var countNodes = function(root) {
  14. if(!root){
  15. return 0
  16. }
  17. return 1 + countNodes(root.left) + countNodes(root.right)
  18. }

二分法:

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val, left, right) {
  4. * this.val = (val===undefined ? 0 : val)
  5. * this.left = (left===undefined ? null : left)
  6. * this.right = (right===undefined ? null : right)
  7. * }
  8. */
  9. /**
  10. * @param {TreeNode} root
  11. * @return {number}
  12. */
  13. var countNodes = function(root) {
  14. if(!root){
  15. return 0
  16. }
  17. let leftHeight = 0, rightHeight = 0
  18. let leftNode = root, rightNode = root
  19. while(leftNode){
  20. leftHeight++
  21. leftNode = leftNode.left
  22. }
  23. while(rightNode){
  24. rightHeight++
  25. rightNode = rightNode.right
  26. }
  27. if(leftHeight === rightHeight){
  28. return 2 ** leftHeight - 1
  29. }
  30. return 1 + countNodes(root.left) + countNodes(root.right)
  31. };

(9)左叶子之和

计算给定二叉树的所有左叶子之和。示例:

  1. 3
  2. / \
  3. 9 20
  4. / \
  5. 15 7

在这个二叉树中,有两个左叶子,分别是 9 和 15,所以返回 24

对于这道题目,我们可以对二叉树进行层序遍历, 初始化一个对来queue来保存当前层的元素,遍历队列中的元素,如果该节点的左子树不存在左右子树,说明它是一个左叶子节点,将其加在结果上。

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val) {
  4. * this.val = val;
  5. * this.left = this.right = null;
  6. * }
  7. */
  8. /**
  9. * @param {TreeNode} root
  10. * @return {number}
  11. */
  12. var sumOfLeftLeaves = function(root) {
  13. if(!root) return 0
  14. let res = 0
  15. const queue = [root]
  16. while(queue.length){
  17. const cur = queue.shift()
  18. if(cur.left){
  19. if(!cur.left.left && !cur.left.right){
  20. res += cur.left.val
  21. }
  22. queue.push(cur.left)
  23. }
  24. if(cur.right){
  25. queue.push(cur.right)
  26. }
  27. }
  28. return res
  29. };

复杂度分析:

  • 时间复杂度:O(n),最坏的情况下,也就是二叉树只有右子树,而形成一个链表的时候,我们需要遍历完整个二叉树,时间复杂度就是 O(n);
  • 空间复杂度:O(n),其中n表示队列的长度,这个长度永远小于等于二叉树的节点的数量。

    (10)找树左下角的值

    给定一个二叉树,在树的最后一行找到最左边的值。
    示例 1:
    1. 输入:
    2. 2
    3. / \
    4. 1 3
    5. 输出:1
    示例 2:
    1. 输入:
    2. 1
    3. / \
    4. 2 3
    5. / / \
    6. 4 5 6
    7. /
    8. 7
    9. 输出:7
    注意: 您可以假设树(即给定的根节点)不为 NULL

这里可以对二叉树进行层序遍历,而层序遍历就是基于广度优先遍历的。

在遍历的过程中,我们初始化一个队列来保存当前层的节点,这个过程中,需要先将根节点的右子节点加入到队列中,再将其左子节点加入到队列中。

这个过程中,将对列元素出队,并加入到res数组中,这样数组的最后一个值就是二叉树左下角的值。

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val, left, right) {
  4. * this.val = (val===undefined ? 0 : val)
  5. * this.left = (left===undefined ? null : left)
  6. * this.right = (right===undefined ? null : right)
  7. * }
  8. */
  9. /**
  10. * @param {TreeNode} root
  11. * @return {number}
  12. */
  13. var findBottomLeftValue = function(root) {
  14. const queue = [root]
  15. let res = []
  16. while(queue.length){
  17. const node = queue.shift()
  18. res.push(node.val)
  19. node.right && queue.push(node.right)
  20. node.left && queue.push(node.left)
  21. }
  22. return res[res.length - 1]
  23. };

复杂度分析:

  • 时间复杂度:O(n),其中n是二叉树的节点数,我们需要遍历整棵树;
  • 空间复杂度:O(n),其中n是二叉树的高度,我们需要初始化一个数组来保存二叉树的所有节点;

    (11)最大二叉树

    给定一个不含重复元素的整数数组 nums 。一个以此数组直接递归构建的 最大二叉树 定义如下:
  1. 二叉树的根是数组 nums 中的最大元素。
  2. 左子树是通过数组中 最大值左边部分 递归构造出的最大二叉树。
  3. 右子树是通过数组中 最大值右边部分 递归构造出的最大二叉树。

返回有给定数组 nums 构建的 最大二叉树

示例 1:
数据结构与算法之二叉树篇 - 图13

  1. 输入:nums = [3,2,1,6,0,5]
  2. 输出:[6,3,5,null,2,0,null,null,1]
  3. 解释:递归调用如下所示:
  4. - [3,2,1,6,0,5] 中的最大值是 6 ,左边部分是 [3,2,1] ,右边部分是 [0,5]
  5. - [3,2,1] 中的最大值是 3 ,左边部分是 [] ,右边部分是 [2,1]
  6. - 空数组,无子节点。
  7. - [2,1] 中的最大值是 2 ,左边部分是 [] ,右边部分是 [1]
  8. - 空数组,无子节点。
  9. - 只有一个元素,所以子节点是一个值为 1 的节点。
  10. - [0,5] 中的最大值是 5 ,左边部分是 [0] ,右边部分是 []
  11. - 只有一个元素,所以子节点是一个值为 0 的节点。
  12. - 空数组,无子节点。

示例 2:
数据结构与算法之二叉树篇 - 图14

  1. 输入:nums = [3,2,1]
  2. 输出:[3,null,2,null,1]

提示:

  • 1 <= nums.length <= 1000
  • 0 <= nums[i] <= 1000
  • nums 中的所有整数 互不相同

这道题目我们直接使用递归来实现:

  • 首先获取到数组中最大的值,来作为当前的根节点
  • 分别获取数组中最大值的左边的数组元素和右边的数组元素
  • 使用两个数组分别进行递归构建二叉树的左右子树

    1. /**
    2. * Definition for a binary tree node.
    3. * function TreeNode(val, left, right) {
    4. * this.val = (val===undefined ? 0 : val)
    5. * this.left = (left===undefined ? null : left)
    6. * this.right = (right===undefined ? null : right)
    7. * }
    8. */
    9. /**
    10. * @param {number[]} nums
    11. * @return {TreeNode}
    12. */
    13. var constructMaximumBinaryTree = function(nums) {
    14. if(nums.length === 0){
    15. return null
    16. }
    17. let max = Math.max(...nums)
    18. let root = new TreeNode(max)
    19. let leftArray = nums.slice(0, nums.indexOf(max))
    20. let rightArray = nums.slice(nums.indexOf(max) + 1)
    21. root.left = constructMaximumBinaryTree(leftArray)
    22. root.right = constructMaximumBinaryTree(rightArray)
    23. return root
    24. };

    复杂度分析:

  • 时间复杂度:O(n),一共递归了 n 次。每次递归寻找根节点时,需要遍历当前索引范围内所有元素找出最大值。一般情况下,每次遍历的复杂度为 O(logn),总复杂度为 O(nlogn)。最坏的情况下,数组 nums 有序,总的复杂度为 O(n)

  • 空间复杂度:O(n)。递归调用深度为 n。平均情况下,长度为 n 的数组递归调用深度为 O(logn)。

    (12)相同的树

    给定两个二叉树,编写一个函数来检验它们是否相同。如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

示例 1:

  1. 输入: 1 1
  2. / \ / \
  3. 2 3 2 3
  4. [1,2,3], [1,2,3]
  5. 输出: true

示例 2:

  1. 输入: 1 1
  2. / \
  3. 2 2
  4. [1,2], [1,null,2]
  5. 输出: false

示例 3:

  1. 输入: 1 1
  2. / \ / \
  3. 2 1 1 2
  4. [1,2,1], [1,1,2]
  5. 输出: false

我们只需要进行递归遍历两个树对应的节点,看看是否一致,一致的话就直接返回false。这里使用的是深度优先遍历来进行遍历操作。

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val, left, right) {
  4. * this.val = (val===undefined ? 0 : val)
  5. * this.left = (left===undefined ? null : left)
  6. * this.right = (right===undefined ? null : right)
  7. * }
  8. */
  9. /**
  10. * @param {TreeNode} p
  11. * @param {TreeNode} q
  12. * @return {boolean}
  13. */
  14. var isSameTree = function(p, q) {
  15. if(!p && !q){
  16. return true
  17. }
  18. if(p === null || q === null){
  19. return false
  20. }
  21. if(p.val !== q.val){
  22. return false
  23. }
  24. return isSameTree(p.left, q.left) && isSameTree(p.right, q.right)
  25. };

复杂度分析:

  • 时间复杂度:O(min(m,n)),其中 m 和 n 分别是两个二叉树的节点数。对两个二叉树同时进行深度优先搜索,只有当两个二叉树中的对应节点都不为空时才会访问到该节点,因此被访问到的节点数不会超过较小的二叉树的节点数。
  • 空间复杂度:O(min(m,n)),其中 m 和 n 分别是两个二叉树的节点数。空间复杂度取决于递归调用的层数,递归调用的层数不会超过较小的二叉树的最大高度,最坏情况下,二叉树的高度等于节点数。

    (13)出现次数最多的子树元素和

    给你一个二叉树的根结点,请你找出出现次数最多的子树元素和。一个结点的「子树元素和」定义为以该结点为根的二叉树上所有结点的元素之和(包括结点本身)。

你需要返回出现次数最多的子树元素和。如果有多个元素出现的次数相同,返回所有出现次数最多的子树元素和(不限顺序)。
示例 1:

  1. 输入:
  2. 5
  3. / \
  4. 2 -3
  5. 返回 [2, -3, 4],所有的值均只出现一次,以任意顺序返回所有值。

示例 2:

  1. 输入:
  2. 5
  3. / \
  4. 2 -5
  5. 返回 [2],只有 2 出现两次,-5 只出现 1 次。

提示: 假设任意子树元素和均可以用 32 位有符号整数表示。

这道题和二叉树的众数那道题是一样的思路:

  • 首先,先遍历出一次树, 求所有节点的子树和
  • 在遍历的过程中,使用map来记录每个元素出现的次数
  • 遍历完成之后,遍历map,找出次数最多的和,放在res中即可

    1. /**
    2. * Definition for a binary tree node.
    3. * function TreeNode(val) {
    4. * this.val = val;
    5. * this.left = this.right = null;
    6. * }
    7. */
    8. /**
    9. * @param {TreeNode} root
    10. * @return {number[]}
    11. */
    12. var findFrequentTreeSum = function(root) {
    13. let map = {}, res = [], max = 0
    14. const calcuSum = (root) => {
    15. if(!root){
    16. return 0
    17. }
    18. let left = calcuSum(root.left)
    19. let right = calcuSum(root.right)
    20. let sum = left + right + root.val
    21. // 将当前节点赋值为其所有子节点的和,方便后面进行计算
    22. root.val = sum
    23. map[sum] ? map[sum] += 1 : map[sum] = 1
    24. return root.val
    25. }
    26. calcuSum(root)
    27. for(let key in map){
    28. if(map[key] === max){
    29. res.push(key)
    30. }
    31. if(map[key] > max){
    32. max = map[key]
    33. res = [key]
    34. }
    35. }
    36. return res
    37. };

    复杂度分析:

  • 时间复杂度:O(n),其中n是这棵树的节点数量,我们需要遍历整棵树,来求每个节点的子树和。

  • 空间复杂度:O(n),其中n是这棵树的节点数量,这里需要的是递归的栈空间的空间代价。

    (14)二叉树最大宽度

    给定一个二叉树,编写一个函数来获取这个树的最大宽度。树的宽度是所有层中的最大宽度。这个二叉树与满二叉树(full binary tree) 结构相同,但一些节点为空。

每一层的宽度被定义为两个端点(该层最左和最右的非空节点,两端点间的null节点也计入长度)之间的长度。
示例 1:

  1. 输入:
  2. 1
  3. / \
  4. 3 2
  5. / \ \
  6. 5 3 9
  7. 输出: 4
  8. 解释: 最大值出现在树的第 3 层,宽度为 4 (5,3,null,9)。

示例 2:

  1. 输入:
  2. 1
  3. /
  4. 3
  5. / \
  6. 5 3
  7. 输出: 2
  8. 解释: 最大值出现在树的第 3 层,宽度为 2 (5,3)。

示例 3:

  1. 输入:
  2. 1
  3. / \
  4. 3 2
  5. /
  6. 5
  7. 输出: 2
  8. 解释: 最大值出现在树的第 2 层,宽度为 2 (3,2)。

示例 4:

  1. 输入:
  2. 1
  3. / \
  4. 3 2
  5. / \
  6. 5 9
  7. / \
  8. 6 7
  9. 输出: 8
  10. 解释: 最大值出现在树的第 4 层,宽度为 8 (6,null,null,null,null,null,null,7)。

注意: 答案在32位有符号整数的表示范围内。

这个题目,我们可以一层一层对二叉树进行遍历,初始化一个队列来保存这一层的节点,这个队列中保存着当前节点的节点值和索引值。

我们知道,一个节点的左子树的索引值是其索引值的两倍,即:left = index* 2,右子树的索引值是其索引值的两倍加一,即:right = index * 2 + 1。所以每层的宽度就是:right - left + 1,这样每一层的宽度值就求出来了,最大值也就自然而然的求出来了。

除此之外,我们还要考虑一种情况,就是二叉树深度特别深的时候,索引有可能就超出了数字的有效值。题目最后标明了:答案在32位有符号整数的表示范围内。也就是说最终答案那个最大宽度是不会超过32位有符号整数的。上面的想法是空节点也标注了索引,假如层数很多,但每层只有一个右节点的用例,空节点也计数就不行了,因为并没有限制层数。我们可以让同一层节点的索引先减去此层第一个节点的索引再来计算子节点的索引,这样每一层的索引都是从0开始的,从而解决数字大的问题。

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val, left, right) {
  4. * this.val = (val===undefined ? 0 : val)
  5. * this.left = (left===undefined ? null : left)
  6. * this.right = (right===undefined ? null : right)
  7. * }
  8. */
  9. /**
  10. * @param {TreeNode} root
  11. * @return {number}
  12. */
  13. var widthOfBinaryTree = function(root) {
  14. if(!root){
  15. return 0
  16. }
  17. const nodes = [{node: root, index: 0}]
  18. let res = 0
  19. while(nodes.length){
  20. let len = nodes.length
  21. const start = nodes[0].index
  22. const end = nodes[len - 1].index
  23. res = Math.max(res, end - start + 1)
  24. while(len--){
  25. let {node, index} = nodes.shift()
  26. index -= start
  27. node.left && nodes.push({ node: node.left, index: index * 2 })
  28. node.right && nodes.push({ node: node.right, index: index * 2 + 1 })
  29. }
  30. }
  31. return res
  32. };

复杂度分析:

  • 时间复杂度:O(n),其中n是二叉树的节点数,我们需要遍历完整个二叉树;
  • 空间复杂度:O(n),其中n是nodes栈的长度。

    (15)二叉树的最大深度

    给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。说明: 叶子节点是指没有子节点的节点。示例:给定二叉树 [3,9,20,null,null,15,7]

    1. 3
    2. / \
    3. 9 20
    4. / \
    5. 15 7

    递归实现:
    递归二叉树的节点,获取左子树和右子树的最大深度,比较后,返回最大深度,具体步骤如下:

  • 判断二叉树是否为空,空的直接返回 0,结束,非空二叉树继续

  • 分别递归计算左右子树的最大深度
  • 根据返回两者的两者数字,比较后的返回二叉树的最大深度

    1. /**
    2. * Definition for a binary tree node.
    3. * function TreeNode(val) {
    4. * this.val = val;
    5. * this.left = this.right = null;
    6. * }
    7. */
    8. /**
    9. * @param {TreeNode} root
    10. * @return {number}
    11. */
    12. var maxDepth = function(root) {
    13. if(!root){
    14. return 0;
    15. }else{
    16. var leftDepth = maxDepth(root.left)
    17. var rightDepth = maxDepth(root.right)
    18. return Math.max(leftDepth,rightDepth)+1
    19. }
    20. };

    复杂度分析:

  • 时间复杂度: O(n):通过递归的方式查询了树的所有子节点。查询花费 O(n) 的时间。

  • 空间复杂度: O(n):每次递归都需要创建新的临时空间,空间复杂度 O(n)。

    (16)二叉树的最小深度

    给定一个二叉树,找出其最小深度。最小深度是从根节点到最近叶子节点的最短路径上的节点数量。说明:叶子节点是指没有子节点的节点。示例 1:
    数据结构与算法之二叉树篇 - 图15

    1. 输入:root = [3,9,20,null,null,15,7]
    2. 输出:2

    示例 2:

    1. 输入:root = [2,null,3,null,4,null,5,null,6]
    2. 输出:5

    提示:

  • 树中节点数的范围在 [0, 105]

  • -1000 <= Node.val <= 1000

层序遍历实现:
设置一个level,表示当前的层数,然后对二叉树进行层序遍历,每增加一层,level就加一,直到某个节点没有左右子树,结束遍历,返回level

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val) {
  4. * this.val = val;
  5. * this.left = this.right = null;
  6. * }
  7. */
  8. /**
  9. * @param {TreeNode} root
  10. * @return {number}
  11. */
  12. var minDepth = function(root) {
  13. if(!root){
  14. return 0;
  15. }
  16. var level = 0;
  17. var queue = [root];
  18. while(queue.length){
  19. level += 1;
  20. var len = queue.length;
  21. while(len--){
  22. var node = queue.shift();
  23. if (!node.left&&!node.right){
  24. return level;
  25. }
  26. if(node.left){
  27. queue.push(node.left);
  28. }
  29. if(node.right){
  30. queue.push(node.right);
  31. }
  32. }
  33. }
  34. return level;
  35. };

复杂度分析:

  • 时间复杂度:O(n),其中 n 是树的节点数。对每个节点访问一次。
  • 空间复杂度:O(n),其中 n 是树的节点数。空间复杂度主要取决于队列的开销,队列中的元素个数不会超过树的节点数。

    (17)平衡二叉树

    给定一个二叉树,判断它是否是高度平衡的二叉树。本题中,一棵高度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。

示例 1:
image.png

  1. 输入:root = [3,9,20,null,null,15,7]
  2. 输出:true

示例 2:
image.png

  1. 输入:root = [1,2,2,3,3,null,null,4,4]
  2. 输出:false

示例 3:

  1. 输入:root = []
  2. 输出:true

复杂度分析:

  • 树中的节点数在范围 [0, 5000] 内
  • -104<= Node.val <= 104

    5. 经典题目:二叉树的操作

    (1)翻转二叉树

    翻转一棵二叉树。示例: ```javascript 输入: 4 / \ 2 7 / \ / \ 1 3 6 9

输出: 4 / \ 7 2 / \ / \ 9 6 3 1

  1. 通过翻转之后,二叉树的每一个左右子孩子都发生了交换,所有可以使用递归来实现:遍历每一个结点,并将每一个结点的左右孩子进行交换。
  2. ```javascript
  3. /**
  4. * Definition for a binary tree node.
  5. * function TreeNode(val) {
  6. * this.val = val;
  7. * this.left = this.right = null;
  8. * }
  9. */
  10. /**
  11. * @param {TreeNode} root
  12. * @return {TreeNode}
  13. */
  14. var invertTree = function(root) {
  15. if(!root){
  16. return root
  17. }
  18. // 递归获取左右子结点
  19. let right = invertTree(root.right)
  20. let left = invertTree(root.left)
  21. // 交换左右子结点
  22. root.right = left
  23. root.left =right
  24. return root
  25. };

复杂度分析:

  • 时间复杂度:O(N),其中 N 为二叉树节点的数目。需要遍历二叉树中的每一个节点,对每个节点而言,在常数时间内交换其两棵子树。
  • 空间复杂度:O(N)。使用的空间由递归栈的深度决定,它等于当前节点在二叉树中的高度。在平均情况下,二叉树的高度与节点个数为对数关系,即 O(logN)。而在最坏情况下,树形成链状,空间复杂度为 O(N)。

    (2)合并二叉树

    给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。示例 1:
    1. 输入:
    2. Tree 1 Tree 2
    3. 1 2
    4. / \ / \
    5. 3 2 1 3
    6. / \ \
    7. 5 4 7
    8. 输出:
    9. 合并后的树:
    10. 3
    11. / \
    12. 4 5
    13. / \ \
    14. 5 4 7
    注意: 合并必须从两个树的根节点开始。

这里可以使用递归的方式来计算,保持t1不便,将t2的节点往t1上加就可以了。

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val) {
  4. * this.val = val;
  5. * this.left = this.right = null;
  6. * }
  7. */
  8. /**
  9. * @param {TreeNode} t1
  10. * @param {TreeNode} t2
  11. * @return {TreeNode}
  12. */
  13. var mergeTrees = function(t1, t2) {
  14. if(!t1 && t2){
  15. return t2
  16. }
  17. if(t1 && !t2 || !t1 && !t2){
  18. return t1
  19. }
  20. t1.val += t2.val
  21. t1.left = mergeTrees(t1.left, t2.left)
  22. t1.right = mergeTrees(t1.right, t2.right)
  23. return t1
  24. };

复杂度分析:

  • 时间复杂度:O(min(m,n)),其中 m 和 n 分别是两个二叉树的节点个数。对两个二叉树同时进行深度优先搜索,只有当两个二叉树中的对应节点都不为空时才会对该节点进行显性合并操作,因此被访问到的节点数不会超过较小的二叉树的节点数。
  • 空间复杂度:O(min(m,n)),其中 m 和 n 分别是两个二叉树的节点个数。空间复杂度取决于递归调用的层数,递归调用的层数不会超过较小的二叉树的最大高度,最坏情况下,二叉树的高度等于节点数。

    (3)二叉树展开为链表

    给你二叉树的根结点 root ,请你将它展开为一个单链表:

展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。展开后的单链表应该与二叉树 先序遍历 顺序相同。

示例 1:
数据结构与算法之二叉树篇 - 图18

  1. 输入:root = [1,2,5,3,4,null,6]
  2. 输出:[1,null,2,null,3,null,4,null,5,null,6]

示例 2:

  1. 输入:root = []
  2. 输出:[]

示例 3:

  1. 输入:root = [0]
  2. 输出:[0]

提示:

  • 树中结点数在范围 [0, 2000] 内
  • -100 <= Node.val <= 100

进阶:你可以使用原地算法(O(1) 额外空间)展开这棵树吗?

题目中也说了,展开后的单链表与二叉树 先序遍历 顺序相同,所以我们可以先对二叉树进行先序遍历,然后将遍历的结果置位一条链表。这个链表相当于左子节点都是null,右子节点都是二叉树的值的二叉树。

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val, left, right) {
  4. * this.val = (val===undefined ? 0 : val)
  5. * this.left = (left===undefined ? null : left)
  6. * this.right = (right===undefined ? null : right)
  7. * }
  8. */
  9. /**
  10. * @param {TreeNode} root
  11. * @return {void} Do not return anything, modify root in-place instead.
  12. */
  13. var flatten = function(root) {
  14. // 前序遍历
  15. const fn = (root) => {
  16. if(!root){
  17. return
  18. }
  19. res.push(root)
  20. fn(root.left)
  21. fn(root.right)
  22. }
  23. let res = []
  24. fn(root)
  25. for(let i = 0; i < res.length - 1; i++){
  26. res[i].left = null
  27. res[i].right = res[i + 1]
  28. }
  29. };

复杂度分析:

  • 时间复杂度:O(n),其中 n 是二叉树的节点数。前序遍历的时间复杂度是 O(n),前序遍历之后,需要对每个节点更新左右子节点的信息,时间复杂度也是 O(n)。
  • 空间复杂度:O(n),其中 n 是二叉树的节点数。空间复杂度取决于栈(递归调用栈或者迭代中显性使用的栈)和存储前序遍历结果的列表的大小,栈内的元素个数不会超过 n,前序遍历列表中的元素个数是 n。

    (4)从前序与中序遍历序列构造二叉树

    根据一棵树的前序遍历与中序遍历构造二叉树。注意:你可以假设树中没有重复的元素。例如,给出

    1. 前序遍历 preorder = [3,9,20,15,7]
    2. 中序遍历 inorder = [9,3,15,20,7]

    返回如下的二叉树:

    1. 3
    2. / \
    3. 9 20
    4. / \
    5. 15 7

    先看下前序遍历和中序遍历的规律:

  • 前序遍历:根节点 + 左子树前序遍历 + 右子树前序遍历

  • 中序遍历:左子树中序遍历 + 根节点 + 右字数中序遍历

可以根据上面获得根据点判断,哪些是左子节点,哪些是右子节点,依次这样判断即可。

实现步骤如下:

  • 前序遍历找到根结点root
  • 找到root在中序遍历的位置 -> 左子树的长度和右子树的长度
  • 截取左子树的中序遍历、右子树的中序遍历
  • 截取左子树的前序遍历、右子树的前序遍历
  • 递归重建二叉树

    1. /**
    2. * Definition for a binary tree node.
    3. * function TreeNode(val) {
    4. * this.val = val;
    5. * this.left = this.right = null;
    6. * }
    7. */
    8. /**
    9. * @param {number[]} preorder
    10. * @param {number[]} inorder
    11. * @return {TreeNode}
    12. */
    13. var buildTree = function(preorder, inorder) {
    14. if(!inorder.length) return null
    15. let tmp = preorder[0]
    16. let mid = inorder.indexOf(tmp)
    17. let root = new TreeNode(tmp)
    18. root.left = buildTree(preorder.slice(1,mid+1),inorder.slice(0,mid))
    19. root.right = buildTree(preorder.slice(mid+1),inorder.slice(mid + 1))
    20. return root
    21. };

    复杂度分析:

  • 时间复杂度:O(n),其中 n 是树中的节点个数。

  • 空间复杂度:O(n),除去返回的答案需要的 O(n) 空间之外,还需要使用 O(n) 的空间存储哈希映射,以及 O(h)(其中 h 是树的高度)的空间表示递归时栈空间。这里 h

    (5)从中序与后序遍历序列构造二叉树

    根据一棵树的中序遍历与后序遍历构造二叉树。注意:你可以假设树中没有重复的元素。例如,给出

    1. 中序遍历 inorder = [9,3,15,20,7]
    2. 后序遍历 postorder = [9,15,7,20,3]

    返回如下的二叉树:

    1. 3
    2. / \
    3. 9 20
    4. / \
    5. 15 7

    先看下中序遍历和后序遍历的规律:

  • 中序遍历:左子树中序遍历 + 根节点 + 右字数中序遍历

  • 后序遍历:左子树后序遍历 + 右子树后序遍历 + 根节点

这个题目的思路也是使用递归:

  • 可以看到,后序遍历数组的最后一个值是二叉树的根节点,也就是示例中的7
  • 根据根节点的值,在中序遍历的数组中找到该值的索引,我就可以知道,3左边的是左子树的中序遍历的数组,3后面的是右子树的中序遍历的数组
  • 根据根节点的值,在后续遍历数组中找到的该值得索引,就可以知道,0到该索引的的值都是左子树的后序遍历的数组,后面的数值就是右子树的后序遍历的数组(需要排除根节点)
  • 根据上面得到的左右子树的中序遍历和后续遍历的结果进行递归操作

    1. /**
    2. * Definition for a binary tree node.
    3. * function TreeNode(val) {
    4. * this.val = val;
    5. * this.left = this.right = null;
    6. * }
    7. */
    8. /**
    9. * @param {number[]} inorder
    10. * @param {number[]} postorder
    11. * @return {TreeNode}
    12. */
    13. var buildTree = function(inorder, postorder) {
    14. if(!inorder.length) return null
    15. let len = postorder.length
    16. let tmp = postorder[len-1]
    17. let mid = inorder.indexOf(tmp)
    18. let root = new TreeNode(tmp)
    19. root.left = buildTree(inorder.slice(0,mid), postorder.slice(0,mid))
    20. root.right = buildTree(inorder.slice(mid + 1), postorder.slice(mid,len-1))
    21. return root
    22. };

    复杂度分析:

  • 时间复杂度:O(n),其中 n 是树中的节点个数。

  • 空间复杂度:O(n),除去返回的答案需要的 O(n) 空间之外,还需要使用 O(n) 的空间存储哈希映射,以及 O(h)(其中 h 是树的高度)的空间表示递归时栈空间。这里 h

    (6)从前序与后序遍历序列构造二叉树

    返回与给定的前序和后序遍历匹配的任何二叉树。其中,pre 和 post 遍历中的值是不同的正整数。示例:

    1. 输入:pre = [1,2,4,5,3,6,7], post = [4,5,2,6,7,3,1]
    2. 输出:[1,2,3,4,5,6,7]

    提示:

  • 1 <= pre.length == post.length <= 30

  • pre[] 和 post[] 都是 1, 2, …, pre.length 的排列
  • 每个输入保证至少有一个答案。如果有多个答案,可以返回其中一个。

先看下前序遍历和后序遍历的规律:

  • 前序遍历:根节点 + 左子树前序遍历 + 右子树前序遍历
  • 后序遍历:左子树后序遍历 + 右子树后序遍历 + 根节点

这个题目的思路也是使用递归:

  • 根据前序遍历的特点,就可以知道1是二叉树的根节点,那么紧跟其后的应该就是左节点,也就是2。
  • 在后序遍历中找到2对应的位置,我们就可以知道2及之前的数都是该二叉树的左节点的后序遍历数组,之后的数都是二叉树的右节点的后序遍历数组(需要除去根节点)
  • 根据左子树的长度在先序遍历中找到左子树和右子树的前序遍历值。
  • 进行递归操作,得出最后的二叉树。

    1. /**
    2. * Definition for a binary tree node.
    3. * function TreeNode(val) {
    4. * this.val = val;
    5. * this.left = this.right = null;
    6. * }
    7. */
    8. /**
    9. * @param {number[]} pre
    10. * @param {number[]} post
    11. * @return {TreeNode}
    12. */
    13. var constructFromPrePost = function(pre, post) {
    14. if(!pre.length) return null
    15. let tmp = pre[0];
    16. let index = post.indexOf(pre[1]);
    17. let root = new TreeNode(tmp);
    18. root.left = constructFromPrePost(pre.slice(1,index+2),post.slice(0,index+1));
    19. root.right = constructFromPrePost(pre.slice(index+2),post.slice(index+1,post.length-1));
    20. return root;
    21. };

    复杂度分析:

  • 时间复杂度:O(n),其中 n 是树中的节点个数。

  • 空间复杂度:O(n),除去返回的答案需要的 O(n) 空间之外,还需要使用 O(n) 的空间存储哈希映射,以及 O(h)(其中 h 是树的高度)的空间表示递归时栈空间。这里 h

    (7)填充每个节点的下一个右侧节点指针

    给定一个完美二叉树,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:
    1. struct Node {
    2. int val;
    3. Node *left;
    4. Node *right;
    5. Node *next;
    6. }
    填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。初始状态下,所有 next 指针都被设置为 NULL。

示例:
数据结构与算法之二叉树篇 - 图19

  1. 输入:{"$id":"1","left":{"$id":"2","left":{"$id":"3","left":null,"next":null,"right":null,"val":4},"next":null,"right":{"$id":"4","left":null,"next":null,"right":null,"val":5},"val":2},"next":null,"right":{"$id":"5","left":{"$id":"6","left":null,"next":null,"right":null,"val":6},"next":null,"right":{"$id":"7","left":null,"next":null,"right":null,"val":7},"val":3},"val":1}
  2. 输出:{"$id":"1","left":{"$id":"2","left":{"$id":"3","left":null,"next":{"$id":"4","left":null,"next":{"$id":"5","left":null,"next":{"$id":"6","left":null,"next":null,"right":null,"val":7},"right":null,"val":6},"right":null,"val":5},"right":null,"val":4},"next":{"$id":"7","left":{"$ref":"5"},"next":null,"right":{"$ref":"6"},"val":3},"right":{"$ref":"4"},"val":2},"next":null,"right":{"$ref":"7"},"val":1}

解释:给定二叉树如图 A 所示,你的函数应该填充它的每个 next 指针,以指向其下一个右侧节点,如图 B 所示。

提示:

  • 你只能使用常量级额外空间。
  • 使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度

这里我们使用递归的方式来解决这个问题。我们对二叉树进行先序遍历。我们要讨论两种情况:同一父节点的左右节点相连非同一父节点的左右节点相连,下面来看以下:

  • 第一步,对于同一父节点的左右节点相连,将左节点的值指向右节点
  • 第二部,对于非同一父节点的左右节点相连,以图中的5和6为例,我们通过5的父节点2,找到他的右节点3,再通过3找到其左节点,并将5和相连3相连。

对于上面的步骤,进行递归,直至遍历完所有的节点。

  1. /**
  2. * // Definition for a Node.
  3. * function Node(val, left, right, next) {
  4. * this.val = val === undefined ? null : val;
  5. * this.left = left === undefined ? null : left;
  6. * this.right = right === undefined ? null : right;
  7. * this.next = next === undefined ? null : next;
  8. * };
  9. */
  10. /**
  11. * @param {Node} root
  12. * @return {Node}
  13. */
  14. var connect = function(root) {
  15. if(!root) return null
  16. // 同一父节点的左右节点相连
  17. if(root.left && root.right){
  18. root.left.next = root.right
  19. }
  20. // 非同一父节点的左右节点相连
  21. if(root.right && root.next && root.next.left){
  22. root.right.next = root.next.left
  23. }
  24. // 递归遍历
  25. connect(root.left)
  26. connect(root.right)
  27. return root
  28. };

复杂度分析:

  • 时间复杂度:O(N),其中n是二叉树的节点的数量,每个节点只访问一次。
  • 空间复杂度:O(1),不需要存储额外的节点。

    (8)填充每个节点的下一个右侧节点指针 II

    给定一个二叉树:
    1. struct Node {
    2. int val;
    3. Node *left;
    4. Node *right;
    5. Node *next;
    6. }
    填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。初始状态下,所有 next 指针都被设置为 NULL

进阶:

  • 你只能使用常量级额外空间。
  • 使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度。

示例:
数据结构与算法之二叉树篇 - 图20
输入:root = [1,2,3,4,5,null,7]
输出:[1,#,2,3,#,4,5,7,#]
解释: 给定二叉树如图 A 所示,你的函数应该填充它的每个 next 指针,以指向其下一个右侧节点,如图 B 所示。序列化输出按层序遍历顺序(由 next 指针连接),’#’ 表示每层的末尾。

提示:

  • 树中的节点数小于 6000
  • -100 <= node.val <= 100

对于这道题目,我们可以对树进行层序遍历,树的层序遍历是基于广度优先遍历的,按照层的顺序进行遍历,我们需要舒适话一个队列queue,这个队列中保存着当前层的节点。

当队列不为空的时候就记录当前队列的的长度len,当遍历这一层的时候,修改这一层节点的 next 指针,这样就可以把每一层都组织成链表。

  1. /**
  2. * // Definition for a Node.
  3. * function Node(val, left, right, next) {
  4. * this.val = val === undefined ? null : val;
  5. * this.left = left === undefined ? null : left;
  6. * this.right = right === undefined ? null : right;
  7. * this.next = next === undefined ? null : next;
  8. * };
  9. */
  10. /**
  11. * @param {Node} root
  12. * @return {Node}
  13. */
  14. var connect = function(root) {
  15. if (!root) {
  16. return null;
  17. }
  18. const queue = [root];
  19. while (queue.length) {
  20. const len = queue.length;
  21. let last = null;
  22. for (let i = 1; i <= len; ++i) {
  23. let node = queue.shift();
  24. if (node.left) {
  25. queue.push(node.left);
  26. }
  27. if (node.right) {
  28. queue.push(node.right);
  29. }
  30. if (i !== 1) {
  31. last.next = node;
  32. }
  33. last = node;
  34. }
  35. }
  36. return root;
  37. };

复杂度分析:

  • 时间复杂度:O(N)。其中N是树的节点数,我们需要遍历这棵树上所有的点,时间复杂度为 O(N)。
  • 空间复杂度:O(N)。其中N是树的节点数,我们需要初始化一个队列,它的长度最大不超过树的节点数。

    (9)二叉树的序列化与反序列化

    序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。

请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。

示例:你可以将以下二叉树:

  1. 1
  2. / \
  3. 2 3
  4. / \
  5. 4 5
  6. 序列化为 "[1,2,3,null,null,4,5]"

提示: 这与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。

说明: 不要使用类的成员 / 全局 / 静态变量来存储状态,你的序列化和反序列化算法应该是无状态的。

对于这道二叉树的题目,我们能想到的最直接的方式就是深度优先宾利和广度优先遍历,这里就分别用两种方式来解答。

深度优先遍历:

  • 首先是序列化二叉树,可以定义一个遍历方法,先访问根节点,再访问左节点,最后访问右节点,将每个节点的值都存入数组,如果是null也要存入数组。
  • 之后是反序列化二叉树,也就是将数组转化为二叉树,因为数组是二叉树先序遍历的结果,所以我们就可以遍历数组,然后按照根节点、左子树、右子树的顺序复原二叉树

广度优先遍历:

  • 首先是序列化二叉树,广度优先遍历的遍历顺序是按照层级从上往下遍历(层序遍历),所以我们可以利用队列先进先出的特性,维持一个数组。先将根节点入队,再将左节点和右节点入队,递归即可。
  • 之后是反序列化二叉树,我们可以从数组中取出第一个元素生成根节点,将根节点加入队列,循环队列,将根节点的左右子树分别加入队列,循环此操作,直至队列为空。其中队列中的节点用于后面遍历其左右子节点。

1)深度优先遍历:

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val) {
  4. * this.val = val;
  5. * this.left = this.right = null;
  6. * }
  7. */
  8. /**
  9. * Encodes a tree to a single string.
  10. *
  11. * @param {TreeNode} root
  12. * @return {string}
  13. */
  14. var serialize = function(root) {
  15. const result = []
  16. function traverseNode(node){
  17. if(node === null){
  18. result.push(null)
  19. }else{
  20. result.push(node.val)
  21. traverseNode(node.left)
  22. traverseNode(node.right)
  23. }
  24. }
  25. traverseNode(root)
  26. return result
  27. };
  28. /**
  29. * Decodes your encoded data to tree.
  30. *
  31. * @param {string} data
  32. * @return {TreeNode}
  33. */
  34. var deserialize = function(data) {
  35. const len = data.length
  36. if(!len){
  37. return null
  38. }
  39. let i = 0
  40. function structure (){
  41. // 递归停止条件
  42. if(i >= len){
  43. return null
  44. }
  45. const val = data[i]
  46. i++
  47. if(val === null){
  48. return null
  49. }
  50. const node = new TreeNode(val)
  51. node.left = structure()
  52. node.right = structure()
  53. return node
  54. }
  55. return structure()
  56. };
  57. /**
  58. * Your functions will be called as such:
  59. * deserialize(serialize(root));
  60. */

复杂度分析:

  • 时间复杂度:在序列化和反序列化函数中,我们只访问每个节点一次,因此时间复杂度为 O(n),其中 n 是节点数,即树的大小。
  • 空间复杂度:在序列化和反序列化函数中,我们递归会使用栈空间,故渐进空间复杂度为 O(n)。

2)广度优先遍历:

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val) {
  4. * this.val = val;
  5. * this.left = this.right = null;
  6. * }
  7. */
  8. /**
  9. * Encodes a tree to a single string.
  10. *
  11. * @param {TreeNode} root
  12. * @return {string}
  13. */
  14. var serialize = function(root) {
  15. if(!root){
  16. return []
  17. }
  18. const result = []
  19. const queue = []
  20. queue.push(root)
  21. let node ;
  22. while(queue.length){
  23. node = queue.shift()
  24. result.push(node ? node.val : null)
  25. if(node){
  26. queue.push(node.left)
  27. queue.push(node.right)
  28. }
  29. }
  30. return result
  31. };
  32. /**
  33. * Decodes your encoded data to tree.
  34. *
  35. * @param {string} data
  36. * @return {TreeNode}
  37. */
  38. var deserialize = function(data) {
  39. const len = data.length
  40. if(!len){
  41. return null
  42. }
  43. const root = new TreeNode(data.shift())
  44. const queue = [root]
  45. while(queue.length){
  46. // 取出将要遍历的节点
  47. const node = queue.shift()
  48. if(!data.length){
  49. break
  50. }
  51. // 还原左节点
  52. const leftVal = data.shift()
  53. if(leftVal === null){
  54. node.left = null
  55. }else{
  56. node.left = new TreeNode(leftVal)
  57. queue.push(node.left)
  58. }
  59. if(!data.length){
  60. break
  61. }
  62. // 还原右节点
  63. const rightVal = data.shift()
  64. if(rightVal === null){
  65. node.right = null
  66. }else{
  67. node.right = new TreeNode(rightVal)
  68. queue.push(node.right)
  69. }
  70. }
  71. return root
  72. };
  73. /**
  74. * Your functions will be called as such:
  75. * deserialize(serialize(root));
  76. */

复杂度分析:

  • 时间复杂度:在序列化和反序列化函数中,我们只访问每个节点一次,因此时间复杂度为 O(n),其中 n 是节点数,即树的大小。
  • 空间复杂度:在序列化和反序列化函数中,我们递归会使用栈空间,故渐进空间复杂度为 O(n)。

    6. 经典题目:二叉搜索树的属性

    (1)验证二叉搜索树

    给定一个二叉树,判断其是否是一个有效的二叉搜索树。假设一个二叉搜索树具有如下特征:

  • 节点的左子树只包含小于当前节点的数。

  • 节点的右子树只包含大于当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。

示例 1:

  1. 输入:
  2. 2
  3. / \
  4. 1 3
  5. 输出: true

示例 2:

  1. 输入:
  2. 5
  3. / \
  4. 1 4
  5. / \
  6. 3 6
  7. 输出: false
  8. 解释: 输入为: [5,1,4,null,null,3,6]。
  9. 根节点的值为 5 ,但是其右子节点值为 4

首先使用DFS(深度优先遍历)递归遍历整棵树,检验每棵子树中是否都满足 左 < 根 < 右 这样的关系。

设定两个值:最大值和最小值分别为正无穷和负无穷,然后通过判断左孩子的值是否小于根节点,右孩子的值是否大于根节点来断定该二叉树是否是二叉搜索树。

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val) {
  4. * this.val = val;
  5. * this.left = this.right = null;
  6. * }
  7. */
  8. /**
  9. * @param {TreeNode} root
  10. * @return {boolean}
  11. */
  12. var isValidBST = function(root) {
  13. function dfs(root, minValue, maxValue){
  14. // 判断树为空的情况
  15. if(!root){
  16. return true
  17. }
  18. // 关键性的判断条件:左 < 根 < 右
  19. if(root.val <= minValue || root.val >= maxValue){
  20. return false
  21. }
  22. // 遍历左子树和右子树
  23. return dfs(root.left, minValue, root.val)&&dfs(root.right, root.val, maxValue)
  24. }
  25. // 对dfs遍历进行初始化
  26. return dfs(root, -Infinity, Infinity)
  27. };

复杂度分析:

  • 时间复杂度:O(n):在递归调用的时候二叉树的每个节点最多被访问一次,因此时间复杂度为 O(n)。
  • 空间复杂度:O(n):递归函数在递归过程中需要为每一层递归函数分配栈空间,所以这里需要额外的空间且该空间取决于递归的深度,即二叉树的高度。最坏情况下二叉树为一条链,树的高度为 nn ,递归最深达到 nn 层,故最坏情况下空间复杂度为 O(n)。

【方法二】中序遍历
使用二叉树的中序遍历来判断。我们知道:二叉搜索树的中序遍历是有序的。所以,直接对二叉树进行中序遍历,将得出的数组进行遍历,判断这个数组是否是有序的。

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val) {
  4. * this.val = val;
  5. * this.left = this.right = null;
  6. * }
  7. */
  8. /**
  9. * @param {TreeNode} root
  10. * @return {boolean}
  11. */
  12. var isValidBST = function(root) {
  13. const queue = []
  14. function dfs(root){
  15. if(!root){
  16. return true
  17. }
  18. if(root.left){
  19. dfs(root.left)
  20. }
  21. if(root){
  22. queue.push(root.val)
  23. }
  24. if(root.right){
  25. dfs(root.right)
  26. }
  27. }
  28. dfs(root)
  29. // 判断遍历的结果是否有序
  30. for(let i = 0; i<queue.length-1; i++){
  31. if(queue[i] >= queue[i+1]){
  32. return false
  33. }
  34. }
  35. return true
  36. };

复杂度分析:

  • 时间复杂度 : O(n),其中 n为二叉树的节点个数。二叉树的每个节点最多被访问一次,因此时间复杂度为 O(n)。
  • 空间复杂度 : O(n),其中 n为二叉树的节点个数。栈最多存储 n个节点,因此需要额外的 O(n)的空间。

    (2)二叉搜索树中第k小的元素

    给定一个二叉搜索树,编写一个函数 kthSmallest 来查找其中第 k 个最小的元素。说明:你可以假设 k 总是有效的,1 ≤ k ≤ 二叉搜索树元素个数。

示例 1:

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

示例 2:

  1. 输入: root = [5,3,6,2,4,null,null,1], k = 3
  2. 5
  3. / \
  4. 3 6
  5. / \
  6. 2 4
  7. /
  8. 1
  9. 输出: 3

进阶:如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化 kthSmallest 函数?

我们知道,二叉搜索树的左节点小于其父节点,右节点小于其右节点。这样二叉搜索树的中序遍历就是一个从小到大的有序序列。我们可以根据这个特性进行解答。

递归:对二叉搜索树进行中序遍历,遍历的原则就是先遍历左子树,然后遍历根节点,最后遍历左子树。在遍历过程中,将遍历的结果不断存入数组中,当遍历到第k个元素的时候,就终止遍历。

迭代:递归的方法也是利用的二叉搜索树的中序遍历:

  1. 初始化一个栈暂存树的节点
  2. 先遍历根节点,再遍历左子树,并保存在栈中
  3. 遍历完左子树之后,将栈中的元素的出栈,这样顺序就反过来了,变成了先成遍历的根节点,再遍历的左子树
  4. 在遍历的过程中,每遍历一次k就减一
  5. 遍历完左子树之后再遍历右子树
  6. 不断循环,直到k为0位置,返回当前的节点值。 ```javascript /**
    • 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} k
    • @return {number} */

// 递归的实现: var kthSmallest = function(root, k) { const result = [] function travel(node){ if(result.length >= k) return if(node.left){ travel(node.left) } result.push(node.val) if(node.right){ travel(node.right) } } travel(root) return result[k - 1] };

// 迭代的实现: let kthSmallest = function(root, k) { let stack = [] let node = root

  1. while(node || stack.length) {
  2. // 遍历左子树
  3. while(node) {
  4. stack.push(node)
  5. node = node.left
  6. }
  7. node = stack.pop()
  8. if(--k === 0) {
  9. return node.val
  10. }
  11. node = node.right
  12. }
  13. return null

}

  1. **递归的复杂度分析:**
  2. - 时间复杂度:O(n),其中n是二叉树的节点数,需要遍历了整个树。
  3. - 空间复杂度:O(n),用了一个数组存储中序序列。
  4. **迭代的复杂度分析:**
  5. - 时间复杂度:O(H+k),其中 H 指的是树的高度,由于开始遍历之前,要先向下达到叶,当树是一个平衡树时:复杂度为 O(logN+k)。当树是一个不平衡树时:复杂度为 O(N+k),此时所有的节点都在左子树。
  6. - 空间复杂度:O(H+k)。当树是一个平衡树时:O(logN+k)。当树是一个非平衡树时:O(N+k)。
  7. <a name="vSbln"></a>
  8. #### (3)二叉搜索树的第k大节点
  9. 给定一棵二叉搜索树,请找出其中第k大的节点。<br /> <br />**示例 1:**
  10. ```javascript
  11. 输入: root = [3,1,4,null,2], k = 1
  12. 3
  13. / \
  14. 1 4
  15. \
  16. 2
  17. 输出: 4

示例 2:

  1. 输入: root = [5,3,6,2,4,null,null,1], k = 3
  2. 5
  3. / \
  4. 3 6
  5. / \
  6. 2 4
  7. /
  8. 1
  9. 输出: 4

限制:1 ≤ k ≤ 二叉搜索树元素个数

我们知道,二叉搜索树的中序遍历的结果是一个有大到小的数组,所以我们可以倒中序遍历,然后将第结果的第k大节点返回即可。

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val) {
  4. * this.val = val;
  5. * this.left = this.right = null;
  6. * }
  7. */
  8. /**
  9. * @param {TreeNode} root
  10. * @param {number} k
  11. * @return {number}
  12. */
  13. var kthLargest = function(root, k) {
  14. let res = []
  15. const dfs = (root) => {
  16. if(!root){
  17. return
  18. }
  19. dfs(root.right)
  20. res.push(root.val)
  21. dfs(root.left)
  22. }
  23. dfs(root)
  24. return res[k - 1]
  25. };

复杂度分析:

  • 时间复杂度 O(n),最差的情况下,也就是当树退化为链表时(全部为右子节点),无论 k 的值大小,递归深度都为 n ,占用 O(n) 时间;
  • 空间复杂度 O(n),最差的情况下,也就是当树退化为链表时(全部为右子节点),系统使用 O(n) 大小的栈空间。

    (4)二叉搜索树的最小绝对差

    给你一棵所有节点为非负值的二叉搜索树,请你计算树中任意两节点的差的绝对值的最小值。示例: ```javascript 输入:

    1 \ 3 / 2

输出: 1

解释: 最小绝对差为 1,其中 2 和 1 的差的绝对值为 1(或者 2 和 3)。

  1. 提示:树中至少有 2 个节点。
  2. 这道题目比较简单,先回忆一下二叉搜索树的特征:左子树的值始终小于父节点的值,右子树的值始终大于父节点的值。还有很重要的一点:在二叉搜索树的遍历中,中序遍历出来的结果是一个升序的数组。
  3. 那我们就可以进行中序遍历,并比较相邻的节点的值,始终保持结果是当前最小的值。
  4. ```javascript
  5. /**
  6. * Definition for a binary tree node.
  7. * function TreeNode(val) {
  8. * this.val = val;
  9. * this.left = this.right = null;
  10. * }
  11. */
  12. /**
  13. * @param {TreeNode} root
  14. * @return {number}
  15. */
  16. var getMinimumDifference = function(root) {
  17. let pre = null
  18. let min = Infinity
  19. let inOrderTravel = (root) => {
  20. if(root){
  21. inOrderTravel(root.left)
  22. if(pre){
  23. min = Math.abs(root.val - pre.val) < min ? Math.abs(root.val - pre.val) : min
  24. }
  25. pre = root
  26. inOrderTravel(root.right)
  27. }
  28. }
  29. inOrderTravel(root)
  30. return min
  31. };

复杂度分析:

  • 时间复杂度:O(n),其中 n 为二叉搜索树节点数。每个节点在中序遍历中都会被访问一次且只会被访问一次,因此总时间复杂度为 O(n)。
  • 空间复杂度:O(n)。递归函数的空间复杂度取决于递归的栈深度,而栈深度在二叉搜索树为一条链的情况下会达到 O(n) 级别。

    (5)二叉搜索树中的众数

    给定一个有相同值的二叉搜索树(BST),找出 BST 中的所有众数(出现频率最高的元素)。假定 BST 有如下定义:

  • 结点左子树中所含结点的值小于等于当前结点的值

  • 结点右子树中所含结点的值大于等于当前结点的值
  • 左子树和右子树都是二叉搜索树

例如:给定 BST [1,null,2,2],

  1. 1
  2. \
  3. 2
  4. /
  5. 2
  6. 返回[2].

提示:如果众数超过1个,不需考虑输出顺序
进阶: 你可以不使用额外的空间吗?(假设由递归产生的隐式调用栈的开销不被计算在内)

对于这道题目,我们可以对二叉树进行深度优先遍历,在遍历过程中,初始化一个max,用来保存当前元素出现的最大的次数,将二叉树值与对应的次数保存在一个map中,最后我们遍历一遍这个map,将所有次数与max相等的值都保存在res中即可。

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val) {
  4. * this.val = val;
  5. * this.left = this.right = null;
  6. * }
  7. */
  8. /**
  9. * @param {TreeNode} root
  10. * @return {number[]}
  11. */
  12. var findMode = function(root) {
  13. let map = {}, max = 0, res = []
  14. const dfs = (root) => {
  15. if(!root){
  16. return []
  17. }
  18. map[root.val] ? map[root.val] += 1 : map[root.val] = 1;
  19. max = Math.max(max, map[root.val])
  20. dfs(root.left)
  21. dfs(root.right)
  22. }
  23. dfs(root)
  24. for(let key in map){
  25. if(max === map[key]){
  26. res.push(key)
  27. }
  28. }
  29. return res
  30. };

复杂度分析:

  • 时间复杂度:O(n),其中n是这棵树的节点数量,我们需要遍历整棵树。
  • 空间复杂度:O(n),其中n是这棵树的节点数量,这里需要的是递归的栈空间的空间代价。

    7. 经典题目:二叉搜索树的操作

    (1)将有序数组转换为二叉搜索树

    将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。
    示例:给定有序数组: [-10,-3,0,5,9],一个可能的答案是:[0,-3,9,-10,null,5],它可以表示下面这个高度平衡二叉搜索树:

    1. 0
    2. / \
    3. -3 9
    4. / /
    5. -10 5

    二分递归实现:
    将数组的值转化为一个高度平衡的二叉搜索树,我们只要找到中间的元素作为根节点,然后将中间元素的左边和右边分别二分,找出中间值作为子树的根节点,重复上述操作,直到遍历完整个数组为止即可。

  • 当数组长度为奇数时,以中间值作为根节点,形成的平衡二叉搜索树的两侧差值为0。

  • 当数组长度为偶数时,可以取中间两个元素的任意一个作为根节点的值,这样形成的平衡二叉树的两侧差值的绝对值为1。

    1. /**
    2. * Definition for a binary tree node.
    3. * function TreeNode(val) {
    4. * this.val = val;
    5. * this.left = this.right = null;
    6. * }
    7. */
    8. /**
    9. * @param {number[]} nums
    10. * @return {TreeNode}
    11. */
    12. var sortedArrayToBST = function(nums) {
    13. if(!nums.length){
    14. return null
    15. }
    16. function bst(low, high){
    17. // 遍历结束条件
    18. if(low > high){
    19. return null
    20. }
    21. // 取出当前子序列的中间元素的索引值
    22. const mid = Math.floor(low+(high-low)/2)
    23. // 将中间元素的值作为当前子树的根节点
    24. const cur = new TreeNode(nums[mid])
    25. // 递归构建左子树
    26. cur.left = bst(low, mid-1)
    27. // 递归构建右子树
    28. cur.right = bst(mid+1, high)
    29. return cur
    30. }
    31. return bst(0, nums.length-1)
    32. };

    复杂度分析:

  • 时间复杂度: O(log n):通过二分查找的方式递归查询了树的所有子节点。查询花费 O(log n) 的时间。

  • 空间复杂度: O(n):每次递归都需要创建新的临时空间,空间复杂度 O(n)。

    (2)二叉树搜索树中的搜索

    给定二叉搜索树(BST)的根节点和一个值。 你需要在BST中找到节点值等于给定值的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 NULL。例如,
    1. 给定二叉搜索树:
    2. 4
    3. / \
    4. 2 7
    5. / \
    6. 1 3
    7. 和值: 2
    你应该返回如下子树:
    1. 2
    2. / \
    3. 1 3
    在上述示例中,如果要找的值是 5,但因为没有节点值为 5,我们应该返回 NULL。 ```javascript /**
    • Definition for a binary tree node.
    • function TreeNode(val) {
    • this.val = val;
    • this.left = this.right = null;
    • } / /*
    • @param {TreeNode} root
    • @param {number} val
    • @return {TreeNode} */

// 迭代的实现: var searchBST = function(root, val) { while(root){ if(root.val === val){ return root }else if(root.val <val){ root = root.right }else{ root = root.left } } return null };

// 递归的实现: var searchBST = function(root, val) { if(!root){ return null } if(root.val === val){ return root }else if(root.val < val){ return searchBST(root.right, val) }else{ return searchBST(root.left, val) } };

  1. **迭代的复杂度分析:**
  2. - 时间复杂度:O(H),其中 H 是树高。平均时间复杂度为 O(logN),最坏时间复杂度为 O(N)。
  3. - 空间复杂度:O(1),恒定的额外空间。
  4. **递归的复杂度分析:**
  5. - 时间复杂度:O(H),其中 H 是树高。平均时间复杂度为 O(logN),最坏时间复杂度为 O(N)。
  6. - 空间复杂度:O(H),递归栈的深度为 H。平均情况下深度为 O(logN),最坏情况下深度为 O(N)。
  7. <a name="BSSpU"></a>
  8. #### (3)二叉搜索树中的插入操作
  9. 给定二叉搜索树(BST)的根节点和要插入树中的值,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 保证原始二叉搜索树中不存在新值。注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回任意有效的结果。
  10. 例如,
  11. ```javascript
  12. 给定二叉搜索树:
  13. 4
  14. / \
  15. 2 7
  16. / \
  17. 1 3
  18. 和 插入的值: 5

你可以返回这个二叉搜索树:

  1. 4
  2. / \
  3. 2 7
  4. / \ /
  5. 1 3 5

或者这个树也是有效的:

  1. 5
  2. / \
  3. 2 7
  4. / \
  5. 1 3
  6. \
  7. 4

二叉搜索树的性质:对于任意节点 root 而言,左子树(如果存在)上所有节点的值均小于 root.val,右子树(如果存在)上所有节点的值均大于 root.val,且它们都是二叉搜索树。

因此,当将val 插入到以root 为根的子树上时,根据 val 与 root.val 的大小关系,就可以确定要将val 插入到哪个子树中。

  • 如果该子树不为空,则问题转化成了将 val 插入到对应子树上。
  • 否则,在此处新建一个以 val 为值的节点,并链接到其父节点 root 上。 ```javascript /**
    • 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} val
    • @return {TreeNode} */

// 递归的实现: var insertIntoBST = function(root, val) { if(!root){ return new TreeNode(val) } if(val < root.val){ root.left = insertIntoBST(root.left,val) }else{ root.right = insertIntoBST(root.right,val) } return root };

// 迭代的实现: var insertIntoBST = function(root, val) { if(!root){ return new TreeNode(val) } let cur = root while(cur){ if(val > cur.val){ if(!cur.right){ cur.right = new TreeNode(val) return root }else{ cur = cur.right } }else{ if(!cur.left){ cur.left = new TreeNode(val) return root }else{ cur = cur.left } } } return root };

  1. **迭代的复杂度分析:**
  2. - 时间复杂度:O(N),其中 N 为树中节点数。最坏情况下,需要将值插入到树的最深的叶子结点上,而叶子节点最深为 O(N)。
  3. - 空间复杂度:O(1)。我们只使用了常数大小的空间。
  4. **递归的复杂度分析:**
  5. - 时间复杂度:O(N),其中 N 为树中节点数。最坏情况下,需要将值插入到树的最深的叶子结点上,而叶子节点最深为 O(N)。
  6. - 空间复杂度:O(1)。我们只使用了常数大小的空间。
  7. <a name="58Kl2"></a>
  8. #### (4)将二叉搜索树变平衡
  9. 给你一棵二叉搜索树,请你返回一棵 平衡后 的二叉搜索树,新生成的树应该与原来的树有着相同的节点值。如果一棵二叉搜索树中,每个节点的两棵子树高度差不超过 1 ,我们就称这棵二叉搜索树是 平衡的 。如果有多种构造方法,请你返回任意一种。
  10. 示例:<br />![](https://cdn.nlark.com/yuque/0/2021/png/1500604/1616245584956-c3625815-41a4-417d-9880-f12e9ab928f5.png#height=372&id=Tr40n&originHeight=372&originWidth=375&originalType=binary&ratio=1&rotation=0&showTitle=false&size=0&status=done&style=shadow&title=&width=375)<br />![](https://cdn.nlark.com/yuque/0/2021/png/1500604/1616245584888-093a911b-c865-4c6a-bf1e-5d134a26eeec.png#height=279&id=XvwBv&originHeight=279&originWidth=235&originalType=binary&ratio=1&rotation=0&showTitle=false&size=0&status=done&style=shadow&title=&width=235)
  11. ```javascript
  12. 输入:root = [1,null,2,null,3,null,4,null,null]
  13. 输出:[2,1,3,null,null,null,4]
  14. 解释:这不是唯一的正确答案,[3,1,4,null,2,null,null] 也是一个可行的构造方案。

提示:

  • 树节点的数目在 1 到 104 之间。
  • 树节点的值互不相同,且在 1 到 105 之间。

解题思路和上面的将一个有序数组转化为高度平衡的二叉搜索树问题类似。

我们知道,二叉搜索树的中序遍历是一个有序数组,那么就可以将题目给出的二叉搜索树进行中序遍历,将遍历得到的有序数组转化为平衡的二叉搜索树。其中后半部分和之前的解题思路完全一致。

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val) {
  4. * this.val = val;
  5. * this.left = this.right = null;
  6. * }
  7. */
  8. /**
  9. * @param {TreeNode} root
  10. * @return {TreeNode}
  11. */
  12. var balanceBST = function(root) {
  13. // 初始化一个数组用来储存中序遍历的结果
  14. const nums = []
  15. // 对二叉搜索树进行中序遍历
  16. function inorder(root){
  17. if(!root){
  18. return
  19. }
  20. inorder(root.left)
  21. nums.push(root.val)
  22. inorder(root.right)
  23. }
  24. // 将有序数组转化为高度平衡的二叉搜索树
  25. function buildAvl(low, high){
  26. if(low > high){
  27. return null
  28. }
  29. const mid = Math.floor(low+(high-low)/2)
  30. const cur = new TreeNode(nums[mid])
  31. cur.left = buildAvl(low, mid-1)
  32. cur.right = buildAvl(mid+1, high)
  33. return cur
  34. }
  35. inorder(root)
  36. return buildAvl(0, nums.length-1)
  37. };

复杂度分析:

  • 时间复杂度:由于每个节点最多被访问一次,因此总的时间复杂度为 O(N),其中 N 为链表长度。
  • 空间复杂度:虽然使用了递归,但是瓶颈不在栈空间,而是开辟的长度为 N 的 nums 数组,因此空间复杂度为 O(N),其中 N 为树的节点总数。

    (5)将有序链表转换为二叉搜索树

    给定一个单链表,其中的元素按升序排序,将其转换为高度平衡的二叉搜索树。本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。示例: ```javascript 给定的有序链表: [-10, -3, 0, 5, 9],

一个可能的答案是:[0, -3, 9, -10, null, 5], 它可以表示下面这个高度平衡二叉搜索树:

  1. 0
  2. / \

-3 9 / / -10 5

  1. 由于数组是有序递增排列的,所以我们可以从数组的中间开始查找。利用递归+二分法来解决这个问题。具体实现思路如下:
  2. - 找出数组的中间元素,作为二叉树的根节点的值
  3. - 二叉树的左节点是`0—mid-1`的中间坐标对应的元素
  4. - 二叉树的右节点是`mid+1—arr.length-1`的中间坐标对应元素
  5. - 按照上面的规律进行地递归,直到数组元素遍历完
  6. 需要注意的是,最后的结果可能不唯一。
  7. ```javascript
  8. /**
  9. * Definition for singly-linked list.
  10. * function ListNode(val, next) {
  11. * this.val = (val===undefined ? 0 : val)
  12. * this.next = (next===undefined ? null : next)
  13. * }
  14. */
  15. /**
  16. * Definition for a binary tree node.
  17. * function TreeNode(val, left, right) {
  18. * this.val = (val===undefined ? 0 : val)
  19. * this.left = (left===undefined ? null : left)
  20. * this.right = (right===undefined ? null : right)
  21. * }
  22. */
  23. /**
  24. * @param {ListNode} head
  25. * @return {TreeNode}
  26. */
  27. var sortedListToBST = function(head) {
  28. const arr=[];
  29. while(head){
  30. arr.push(head.val)
  31. head = head.next
  32. }
  33. const resTree=function(left, right){
  34. if(left > right) return null
  35. const mid = Math.floor(left + (right - left)/2);
  36. const res = new TreeNode(arr[mid]);
  37. res.left = resTree(left, mid-1);
  38. res.right = resTree(mid+1, right);
  39. return res
  40. }
  41. return resTree(0, arr.length-1)
  42. };

复杂度分析:

  • 时间复杂度:O(n),其中 n 是数组的长度。每个数字只访问一次。
  • 空间复杂度:O(logn),其中 n 是数组的长度。空间复杂度不考虑返回值,因此空间复杂度主要取决于递归栈的深度,递归栈的深度是 O(logn)。

    (6)把二叉搜索树转换为累加树

    给定一个二叉搜索树(Binary Search Tree),把它转换成为累加树(Greater Tree),使得每个节点的值是原来的节点值加上所有大于它的节点值之和。例如: ```javascript 输入: 原始二叉搜索树:
    1. 5
    2. / \
    3. 2 13

输出: 转换为累加树: 18 / \ 20 13

  1. 这也是一个简单题目,我们都知道二叉树的中序遍历结果是一个递增的数组。这里我们可以进行倒序进行中序遍历,这样遍历的出的数组就是递减的。
  2. 中序遍历的顺序是 **左子树→根节点→右子树。**所以可以先遍历右子树,再遍历根节点,最后遍历左子树。
  3. 这样的话,设置一个节点不断累加之前的值,那么在当前节点的值就会赋值成比他大的数的和。
  4. ```javascript
  5. /**
  6. * Definition for a binary tree node.
  7. * function TreeNode(val) {
  8. * this.val = val;
  9. * this.left = this.right = null;
  10. * }
  11. */
  12. /**
  13. * @param {TreeNode} root
  14. * @return {TreeNode}
  15. */
  16. var convertBST = function(root) {
  17. let sum = 0
  18. const inOrder = (root) => {
  19. if(!root){
  20. return
  21. }
  22. if(root.right){
  23. inOrder(root.right)
  24. }
  25. sum += root.val
  26. root.val = sum
  27. if(root.left){
  28. inOrder(root.left)
  29. }
  30. }
  31. inOrder(root)
  32. return root
  33. };

复杂度分析:

  • 时间复杂度:O(n),其中 n 是二叉搜索树的节点数。每一个节点恰好被遍历一次。
  • 空间复杂度:O(n),为递归过程中栈的开销,平均情况下为 O(logn),最坏情况下树呈现链状,为 O(n)。

    (7)修剪二叉搜索树

    给你二叉搜索树的根节点 root ,同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在[low, high]中。修剪树不应该改变保留在树中的元素的相对结构(即,如果没有被移除,原有的父代子代关系都应当保留)。 可以证明,存在唯一的答案。

所以结果应当返回修剪好的二叉搜索树的新的根节点。注意,根节点可能会根据给定的边界发生改变。

示例 1:
数据结构与算法之二叉树篇 - 图21

  1. 输入:root = [1,0,2], low = 1, high = 2
  2. 输出:[1,null,2]

示例 2:
数据结构与算法之二叉树篇 - 图22

  1. 输入:root = [3,0,4,null,2,null,null,1], low = 1, high = 3
  2. 输出:[3,2,null,1]

示例 3:

  1. 输入:root = [1], low = 1, high = 2
  2. 输出:[1]

示例 4:

  1. 输入:root = [1,null,2], low = 1, high = 3
  2. 输出:[1,null,2]

示例 5:

  1. 输入:root = [1,null,2], low = 2, high = 4
  2. 输出:[2]

提示:

  • 树中节点数在范围 [1, 10]
  • 0 <= Node.val <= 10
  • 树中每个节点的值都是唯一的
  • 题目数据保证输入是一棵有效的二叉搜索树
  • 0 <= low <= high <= 10

对于这道题目,我们可以使用递归来实现。

  • 如果当前结点小于下界,直接将修剪后的右子树替换当前节点并返回;
  • 如果当前结点大于上界,直接将修剪后的左子树替换当前节点并返回;
  • 如果当前节点在范围之内,就继续递归左右子树查找越界的节点。

    1. /**
    2. * Definition for a binary tree node.
    3. * function TreeNode(val, left, right) {
    4. * this.val = (val===undefined ? 0 : val)
    5. * this.left = (left===undefined ? null : left)
    6. * this.right = (right===undefined ? null : right)
    7. * }
    8. */
    9. /**
    10. * @param {TreeNode} root
    11. * @param {number} low
    12. * @param {number} high
    13. * @return {TreeNode}
    14. */
    15. var trimBST = function(root, low, high) {
    16. if(!root){
    17. return root
    18. }
    19. // 如果当前结点小于下界,直接将修剪后的右子树替换当前节点并返回
    20. if(root.val < low){
    21. return trimBST(root.right, low, high)
    22. }
    23. // 如果当前结点大于上界,直接将修剪后的左子树替换当前节点并返回
    24. if(root.val > high){
    25. return trimBST(root.left, low, high)
    26. }
    27. // 如果当前结点不越界,继续往左右子树进行递归
    28. root.left = trimBST(root.left, low, high)
    29. root.right = trimBST(root.right, low, high)
    30. return root
    31. };

    复杂度分析:

  • 时间复杂度:O(n),其中 n 是给定的树节点数。我们最多访问每个节点一次。

  • 空间复杂度:O(n),这里虽然没有使用任何额外的内存,但是在最差情况下,递归调用的栈可能与节点数一样大。

    (8)删除二叉搜索树中的节点

    给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。一般来说,删除节点可分为两个步骤:
  1. 首先找到需要删除的节点;
  2. 如果找到了,删除它。

说明: 要求算法时间复杂度为 O(h),h 为树的高度。
示例:

  1. root = [5,3,6,2,4,null,7]
  2. key = 3
  3. 5
  4. / \
  5. 3 6
  6. / \ \
  7. 2 4 7
  8. 给定需要删除的节点值是 3,所以我们首先找到 3 这个节点,然后删除它。
  9. 一个正确的答案是 [5,4,6,2,null,null,7], 如下图所示。
  10. 5
  11. / \
  12. 4 6
  13. / \
  14. 2 7
  15. 另一个正确答案是 [5,2,6,null,4,null,7]。
  16. 5
  17. / \
  18. 2 6
  19. \ \
  20. 4 7

我们知道,二叉搜索树的左子树总是比根节点小,右子树总是比根节点大,所以可以将根节点的值与要删除的 key 值对比,就知道要删除的值在哪个位置:

  • 如果key 和根节点相等,那么就删除当前的根节点,退出递归;
  • 如果key 比根节点值大,那么就要递归右子树去查找;
  • 如果key 比根节点值小,那么就要递归左子树去查找;

当我们找到需要删除的节点时,会有以下四种情况:

  • 待删除的节点的左右子节点均为空,那么就直接删除当前节点即可;
  • 待删除的节点存在左子节点,而右子节点为空,那么就当前节点设置为左子节点的值;
  • 待删除的节点存在右子节点,而左子节点为空,那么就当前节点设置为右子节点的值;
  • 带删除的节点同时存在左子子节点,那么就要找到比当前节点小的最大节点或者比当前节点大的最小节点)来替换掉当前的节点(下面代码中,我们是找的是比当前节点大的最小节点); ```javascript /**

    • 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} key
    • @return {TreeNode} */ var deleteNode = function(root, key) { if(!root){

      1. return root

      }

      if(root.val > key){

      1. root.left = deleteNode(root.left, key)

      }else if(root.val < key){

      1. root.right = deleteNode(root.right, key)

      }else{

      1. if(!root.left && !root.right){
      2. root = null
      3. }else if(root.left && !root.right){
      4. root = root.left
      5. }else if(!root.left && root.right){
      6. root = root.right
      7. }else if(root.left && root.right){
      8. let last = root.right
      9. while (last.left) {
      10. last = last.left
      11. }
      12. root.val = last.val
      13. root.right = deleteNode(root.right, last.val)
      14. }

      } return root

};

  1. **复杂度分析:**
  2. - 时间复杂度:O(logN)。在算法的执行过程中,我们一直在树上向左或向右移动。首先先用 O(H) 的时间找到要删除的节点,H指得是从根节点到要删除节点的高度。然后删除节点需要 O(H) 的时间,H指的是从要删除节点到替换节点的高度。由于 O(H+ H)=O(H),H 指得是树的高度,若树是一个平衡树,则 H = logN
  3. - 空间复杂度:O(H),递归时堆栈使用的空间,其中 H 是树的高度。
  4. <a name="2mKCf"></a>
  5. ### 8. 经典题目:二叉树的公共祖先
  6. <a name="viejZ"></a>
  7. #### (1)二叉树的最近公共祖先
  8. 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 pq,最近公共祖先表示为一个结点 x,满足 x pq 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
  9. 例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]<br />![](https://cdn.nlark.com/yuque/0/2021/png/1500604/1616247178129-9d563987-1012-4e8d-9d5b-93a82dae8d84.png#height=190&id=ij6bn&originHeight=190&originWidth=200&originalType=binary&ratio=1&rotation=0&showTitle=false&size=0&status=done&style=shadow&title=&width=200)<br />示例 1:
  10. ```javascript
  11. 输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
  12. 输出: 3
  13. 解释: 节点 5 和节点 1 的最近公共祖先是节点 3。

示例 2:

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

说明:

  • 所有节点的值都是唯一的。
  • p、q 为不同节点且均存在于给定的二叉树中。

对于二叉树的题目,经常用的就是递归遍历,这里我们也使用到了递归:

首先判断,如果树为空树或p、q中任意一节和根节点相等,那么p和q 的最近公共祖先节点就是根节点root。

如果树不为空树,并且p和q和根节点不相等,那么就递归遍历左右子树:

  • 如果p和q节点在左右子树的最近公共祖先节点都存在,说明p和q分布在左右子树上,则它们的最近公共祖先节点就是根节点root。
  • 如果只有一个子树递归有结果,说明p和q都在这个子树中,那么就返回该树的递归的结果。
  • 如果两个子树递归结果都为null,说明p和q都不在这俩子树中,那么就返回根节点root。

    1. /**
    2. * Definition for a binary tree node.
    3. * function TreeNode(val) {
    4. * this.val = val;
    5. * this.left = this.right = null;
    6. * }
    7. */
    8. /**
    9. * @param {TreeNode} root
    10. * @param {TreeNode} p
    11. * @param {TreeNode} q
    12. * @return {TreeNode}
    13. */
    14. var lowestCommonAncestor = function(root, p, q) {
    15. if(!root || root === p || root === q){
    16. return root
    17. }
    18. const left = lowestCommonAncestor(root.left, p, q)
    19. const right = lowestCommonAncestor(root.right, p, q)
    20. if(!left) return right
    21. if(!right) return left
    22. return root
    23. };

    复杂度分析:

  • 时间复杂度: O(n),其中 n 是二叉树节点的个数。这里遍历了二叉树的每个节点,所以时间复杂度为O(n)。

  • 空间复杂度: O(n),其中 n 是二叉树节点的个数。递归调用的栈深度取决于二叉树的高度,二叉树最坏情况下为一条链,此时高度为 n,所以空间复杂度为 O(n)。

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

    给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5]
数据结构与算法之二叉树篇 - 图23
示例 1:

  1. 输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
  2. 输出: 6
  3. 解释: 节点 2 和节点 8 的最近公共祖先是 6

示例 2:

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

说明:

  • 所有节点的值都是唯一的。
  • p、q 为不同节点且均存在于给定的二叉搜索树中。

对于这道题目,我们可以使用递归或者迭代来实现。

1)递归实现: 对于二叉树搜索树:

  • 如果 p.val 和 q.val 都比 root.val 小,则 p、q 肯定在 root 的左子树;
  • 如果 p.val 和 q.val 都比 root.val 大,则 p、q 肯定在 root 的右子树;
  • 如果 p.val 和 q.val 一个比 root.val 大,一个比 root.val 小,那说明这两个节点的最进公共祖先就是root;

2)迭代实现: 我们可以使用一个 while 循环,当 root 为 null 时就结束循环:

  • 如果 p.val 和 q.val 都比 root.val 小,则 p、q 肯定在 root 的左子树,root=root.left,遍历到 root 的左子节点。
  • 如果 p.val 和 q.val 都比 root.val 大,则 p、q 肯定在 root 的右子树,root=root.right,遍历到 root 的右子节点。
  • 其他情况,当前的 root 就是最近公共祖先,结束遍历,直接结束循环。

递归实现:

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val) {
  4. * this.val = val;
  5. * this.left = this.right = null;
  6. * }
  7. */
  8. /**
  9. * @param {TreeNode} root
  10. * @param {TreeNode} p
  11. * @param {TreeNode} q
  12. * @return {TreeNode}
  13. */
  14. var lowestCommonAncestor = function(root, p, q) {
  15. if(p.val < root.val && q.val < root.val){
  16. return lowestCommonAncestor(root.left, p, q)
  17. }
  18. if(p.val > root.val && q.val > root.val){
  19. return lowestCommonAncestor(root.right, p, q)
  20. }
  21. return root
  22. };

迭代实现:

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val) {
  4. * this.val = val;
  5. * this.left = this.right = null;
  6. * }
  7. */
  8. /**
  9. * @param {TreeNode} root
  10. * @param {TreeNode} p
  11. * @param {TreeNode} q
  12. * @return {TreeNode}
  13. */
  14. var lowestCommonAncestor = function(root, p, q) {
  15. while(root){
  16. if(p.val < root.val && q.val < root.val){
  17. root = root.left
  18. }else if(p.val > root.val && q.val > root.val){
  19. root = root.right
  20. }else{
  21. break
  22. }
  23. }
  24. return root
  25. };

迭代复杂度分析:

  • 时间复杂度:O(n),其中 n 是二叉搜索树的节点数。最坏的情况下,我们需要深度优先遍历整棵二叉树;
  • 空间复杂度:O(1),我们只需要常量空间来操作。

递归复杂度分析:

  • 时间复杂度:O(n),其中 n 是二叉搜索树的节点数。最坏的情况选,我们需要深度优先遍历整棵二叉树;
  • 空间复杂度:O(n),我们需要递归遍历这棵树,所以空间复杂度为O(n);