class 36 二叉树高频题目 - 上 (不含有树形dp)

在这里插入图片描述

1. BFS 的两种方式

如下图, 是一个二叉树. 我们需要按照层的方式来遍历这棵树.
在这里插入图片描述

在这里插入图片描述

1.1 使用 JDK 自带的类实现(链表实现, 经典 BFS)

  1. 首先我们实现一个队列, 这个队列从头进, 从尾出.
  2. 然后将根节点放入其中,
    1. 然后将放入的节点弹出,
    2. 然后继续验证弹出的节点有没有左孩子, 若是有, 将左孩子压入队列中.
    3. 然后继续验证弹出的节点有没有右孩子, 若是有, 将右孩子压入队列中.
  3. 因为我们需要案按照层的顺序收集节点, 所以我们需要设置一个 hash表 用来记录对应的节点和所在的层数, 还需要一个双层的 List 结构, 用来记录每一层的树节点.
  4. 然后继续实现上述的流程, 只要是当前节点的左孩子和右孩子, 那对应的层数肯定就是当前节点的下一层.

1.2 代码实例

public static List<List<Integer>> levelOrder1(TreeNode root) {  
    List<List<Integer>> ans = new ArrayList<>();  
    if (root != null) {  
       Queue<TreeNode> queue = new LinkedList<>();         // 用链表来模拟队列.  
       HashMap<TreeNode, Integer> levels = new HashMap<>();  // 用哈希表表示节点在第几层.  
       queue.add(root);      // 将“根节点”放入队列中.  
       levels.put(root, 0);  // 然后利用哈希表获取当前节点:根节点的层数.  
       while (!queue.isEmpty()) {  // 队列中只要不是null, 这个循环就不停止.  
          TreeNode cur = queue.poll();  // 将弹出的节点设置为 cur.          int level = levels.get(cur);  // 通过这个key(节点), 获得对应的value(层数).  
          if (ans.size() == level) {  // 若是ans的大小 == 层数, 说明此时我没有对应下标的数组.  
             ans.add(new ArrayList<>()); // 此时在其中新建一个数组  
          }  
          ans.get(level).add(cur.val); // 此时找到ans下标为层数的数组, 然后在其中加入对应的值.  
          if (cur.left != null) {   // 若是弹出节点的左孩子不是null,  
             queue.add(cur.left);  // 将弹出节点的左孩子加入到队列中,  
             levels.put(cur.left, level + 1); // 将这个左孩子加入到hash表中的key, 层数加入到value.  
          }  
          if (cur.right != null) { // 和上面是一样的, 只是将左孩子换成了右孩子了.  
             queue.add(cur.right);  
             levels.put(cur.right, level + 1);  
          }  
       }  
    }  
    return ans;  // 最后返回ans.  
}

1.3 自己用数组实现队列一次处理一层, 逻辑实现

可以自己用数组实现一个队列, 然后设置 l 和 r , 将 r 设置为将数字加入的位置, l 设置为数字出去的位置,

  1. 将当前节点加入到数组(队列)中, 得到此时队列的长度是 size, 重复下面的行为 size 遍.
  2. 先弹出当前节点, 然后有左孩子就加入左孩子, 有右孩子就加入右孩子.
  3. 然后将弹出来的所有节点都加入到对应的数组中去. (当然, 现在只弹出了一个节点, 后续加入了一层很多节点之后, 我们直接将所有的节点都加入到其中), 这样会非常高效.

1.4 代码实例

public static List<List<Integer>> levelOrder2(TreeNode root) {  
    List<List<Integer>> ans = new ArrayList<>();  
    if (root != null) {  // 保证这个树不是null.  
       l = r = 0;       // 开始的时候, 没有任何东西加入到这个数组中.  
       queue[r++] = root;   // 直接将这个二叉树的根节点加入到数组中.  
       while (l < r) { // 队列里还有东西  
          int size = r - l;    // size表示队列的大小.  
          ArrayList<Integer> list = new ArrayList<Integer>();  // list 保存此时的层数的节点.  
          for (int i = 0; i < size; i++) {  // 如下操作执行 size 遍. 经过完整的for循环之后, 会将所有的节点都加入当list中.  
             TreeNode cur = queue[l++];    // 将弹出的节点设置为当前节点.  
             list.add(cur.val);            // 将这个弹出了的节点放到list中.  
             if (cur.left != null) {       // 判断当前节点的左孩子是不是null  
                queue[r++] = cur.left;    // 若不是null, 将其加入到队列中.  
             }  
             if (cur.right != null) {      // 判断当前节点的左孩子是不是null  
                queue[r++] = cur.right;   // 若不是null, 将其加入到队列中.  
             }  
          }  
          ans.add(list);       // 最后将这一层的节点都处理好了, 最后直接加入到ans中.  
       }  
    }  
    return ans;  // 最后直接返回ans就行.  
}

2. 题目二:锯齿状遍历

在这里插入图片描述

2.1 逻辑实现

还是利用了上面优化了之后的 BFS, 唯一有点不一样的就是对应的遍历的顺序是不同的.

  1. 我们设置一个 bool 类型的变量, 这样方便我们进行不同顺序的遍历,
  2. 根据不同的 reverse 是 false 和 true 将树节点按照不同的顺序放入到队列中,
  3. 将一层的所有树节点放入到队列中后, 然后依次弹出队列, 然后将下一层的节点放到队列中.
  4. 然后继续将 list 加入到 ans 中, 然后反转 reverse 的状态.

2.2 代码实例

我们单独将这段代码拿出来说明一下, 这段代码的功能是将这一层的节点按照要求的顺序收集

reverse == true 的时候, 我们收集的顺序是:从右往左收集, 所以此时 i == r - 1, j == -1, 因为我们需要的是从右往左收集, 所以我们需要从右边开始收集, 只需要执行 size 次就能收集好这一层的所有树节点了,

这个 for循环 设计的非常精妙, i 表示从队列的哪里开始收集, 而 j 表示是回退还是前进, k 表示次数, i += j 这个设计同时满足了前进回退的需要, 毕竟 i 和 j 是同时满足的, i 在左边开始收集的时候, j肯定前进是 所以 j == 1, i 在右边开始收集的时候, j肯定是回退, 所以 j == -1.

for (int i = reverse ? r - 1 : l, j = reverse ? -1 : 1, k = 0; k < size; i += j, k++) {  
    TreeNode cur = queue[i];  
    list.add(cur.val);       
}

但是需要注意的是:所有的树节点进入队列的方式已经确定了, 我们只是通过上面的 for循环 实现了将不同的树节点从左往右还是从右往左放入 list 中的操作.

public static int MAXN = 2001;  
  
public static TreeNode[] queue = new TreeNode[MAXN];  
  
public static int l, r;

public static List<List<Integer>> zigzagLevelOrder(TreeNode root) {  
    List<List<Integer>> ans = new ArrayList<>();  
    if (root != null) {  
       l = r = 0;  
       queue[r++] = root;  
       // false 代表从左往右  
       // true 代表从右往左  
       boolean reverse = false;   
       while (l < r) {  
          int size = r - l;  
          ArrayList<Integer> list = new ArrayList<Integer>();  
          // reverse == false, 左 -> 右, l....r-1, 收集size个  
          // reverse == true,  右 -> 左, r-1....l, 收集size个  
          // 左 -> 右, i = i + 1  
          // 右 -> 左, i = i - 1  
          for (int i = reverse ? r - 1 : l, j = reverse ? -1 : 1, k = 0; k < size; i += j, k++) {  
             TreeNode cur = queue[i];  
             list.add(cur.val);  
          }  
          for (int i = 0; i < size; i++) {  
             TreeNode cur = queue[l++];  
             if (cur.left != null) {  
                queue[r++] = cur.left;  
             }  
             if (cur.right != null) {  
                queue[r++] = cur.right;  
             }  
          }  
          ans.add(list);  
          reverse = !reverse;  
       }  
    }  
    return ans;  
}

3. 题目三:最大特殊宽度

在这里插入图片描述

3.1 逻辑实现

这里需要我们对“堆结构”有一些了解, 我们这里有一些巧思.

  1. 我们首先对树节点进行编号, 假设 root(根节点) 的编号是:1, 然后那对应的左孩子的编号是:1 * 2, 右孩子编号是:1 * 2 + 1, 这个方式和堆结构很相似, 而且将 root 设置为 0 也是可以的, 这样就和堆结构完全一样了, 所以就直接看自己就行了.
  2. 这样的好处是, 可以省去层的麻烦, 我只要编号就行了, 也不用管是不是 null, 我只需要直到队列中最左边的编号和最右边的编号的差值就行了. 我只要每一层都遍历完了之后, 最大的那一个差值.

3.2 代码实例

public static int MAXN = 3001;  
  
public static TreeNode[] nq = new TreeNode[MAXN];  // nodeQueue 用来存储树节点.  
  
public static int[] iq = new int[MAXN];   // idQueue 用来存储节点的编号  
  
public static int l, r;  
  
public static int widthOfBinaryTree(TreeNode root) {  
    int ans = 1;  
    l = r = 0;  
    nq[r] = root;  // 先将根节点放入到 节点队列中.  
    iq[r++] = 1;   // 并且同时将对应的编号放入到 编号队列中. 我们给根节点设置的编号是:1.  
    while (l < r) {  
       int size = r - l;  
       ans = Math.max(ans, iq[r - 1] - iq[l] + 1);  // 哪一层的最大深度要大一点.  
       for (int i = 0; i < size; i++) {  
          TreeNode node = nq[l];  
          int id = iq[l++];  
          if (node.left != null) {  
             nq[r] = node.left;  
             iq[r++] = id * 2;    
          }  
          if (node.right != null) {  
             nq[r] = node.right;  
             iq[r++] = id * 2 + 1;  
          }  
       }  
    }  
    return ans;  
}

4. 题目四:最大深度和最小深度

4.1 最大深度

在这里插入图片描述

4.1.1 代码实例

这个不难理解, 直接给代码实例解释

我们在执行下面的代码的时候, 有不理解的地方, 可以直接画一下递归决策图, 这样就非常好理解了.
比如下图的树, 我们要找这个二叉树的最大深度, 我们只需要画一下递归决策图就能完全理解了.
在这里插入图片描述
在这里插入图片描述

public static int maxDepth(TreeNode root) {  
    return root == null ? 0 : Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; 
}

4.2 最小深度

注意:这里的最小深度是必须要到达叶节点, 比如从根节点 root 开始, root.left == null, root.right != null, 那么这个二叉树的最小深度只能是右边的树节点了.

4.2.1 代码实例

这个同样的不难的, 我们还是直接上代码

首先规定两个特殊情况, 一种是当前根节点为 null, 直接返回 0, 因为这棵树都不存在.
另一种是当前只有一个根节点 root, 返回 1, 因为此时 root 这个节点是根节点的也是叶节点.

为了规避在上面“注意”中情况, 我们需要在开始遍历二叉树之前, 将左右两边的深度分别都设置为:Integer.MAX_VALUE.

然后执行后面的代码, 将树左右两边的叶节点都遍历一遍, 然后进行对比, 拿出其中的最小值. 最后返回.
这个行为还是一个递归实现, 只要画一下递归决策图就能更好理解了.

注意:if (root == null) { return 0; }, 这个情况的判断只会在最开始的时候判断, 后续是永远不会重新判断的, 因为我们后续进入递归的条件就是该节点的左孩子不是 null, 或者该节点的右孩子不是 null, 这也就导致 2 <- 1 -> null 这种情况下, ldeep == 1, rdeep == Integer.MAX_VALUE. 所以 1 这个节点最后的返回值是:2.

public int minDepth(TreeNode root) {  // 最小深度肯定不是 1, 因为要到叶子节点上.  
    if (root == null) {  
       // 当前的树是空树  
       return 0;  
    }  
    if (root.left == null && root.right == null) {  
       // 当前root是叶节点 (又是根节点)  
       return 1;  
    }  
    int ldeep = Integer.MAX_VALUE; // 为了防止一边是null, 导致最后的结果是 1, 因为要到叶子节点上.  
    int rdeep = Integer.MAX_VALUE;  
    if (root.left != null) {  
       ldeep = minDepth(root.left);  
    }  
    if (root.right != null) {  
       rdeep = minDepth(root.right);  
    }  
    return Math.min(ldeep, rdeep) + 1;  
}

5. 题目五:二叉树的序列化和反序列化

序列化就是将这个二叉树转为字符串的形式, 反序列化就是将这个字符串转为二叉树的形式,

比如下面这棵树转为字符串的就是按照先序的方式序列化的过程, 反序列化就是通过右边转为左边二叉树的过程. null# 代替, 中间用 , 分隔开.
在这里插入图片描述

序列化和反序列化有先序和后序两种方式, 没有中序的方式, 因为中序有可能将两个不同的二叉树混淆.

// 二叉树可以通过先序、后序或者按层遍历的方式序列化和反序列化  
// 但是,二叉树无法通过中序遍历的方式实现序列化和反序列化  
// 因为不同的两棵树,可能得到同样的中序序列,即便补了空位置也可能一样。  
// 比如如下两棵树  
//         __2  
//        /  
//       1  
//       和  
//       1__  
//          \  
//           2  
// 补足空位置的中序遍历结果都是{ null, 1, null, 2, null}

5.1 逻辑实现

5.1.1 序列化的逻辑实现

这个的逻辑实现很好想, 遍历整个二叉树, 然后若是遇见 null 就将字符串中加入 #,, 若是遇见非 null 的时候, 就向字符串中加入对应的 val.

5.1.2 反序列化的逻辑实现

将字符串用 , 进行分割, 然后根据字符找到对应的值, 最后从顶部到底部建立好二叉树.

5.2 代码实现

5.2.1 序列化的代码实例

这里我们利用了“先序遍历”的“递归”方式, 遍历了整个二叉树, 然后利用 StringBuilder 类, 将二叉树序列化了.

public String serialize(TreeNode root) {  
    StringBuilder builder = new StringBuilder();  
    f(root, builder);  
    return builder.toString();  
}  
  
void f(TreeNode root, StringBuilder builder) {  
    if (root == null) {  
       builder.append("#,");  
    } else {  
       builder.append(root.val + ",");  
       f(root.left, builder);  
       f(root.right, builder);  
    }  
}

5.2.2 反序列化的代码实例

这里需要注意的是:我们需要一个变量 cnt 记录字符串消费到哪里了, 这样就和序列化的过程相反了, 将 String数组中 所有的字符都对应好一个二叉树节点进行建立.

public TreeNode deserialize(String data) {  
    String[] vals = data.split(",");  
    cnt = 0;  
    return g(vals);  
}  
  
// 当前数组消费到哪了  
public static int cnt;  
  
TreeNode g(String[] vals) {  
    String cur = vals[cnt++];  
    if (cur.equals("#")) {  
       return null;  
    } else {  
       TreeNode head = new TreeNode(Integer.parseInt(cur));  
       head.left = g(vals);  
       head.right = g(vals);  
       return head;  
    }  
}

6. 题目六:按照层的方式进行序列化和反序列化

这个方式也是很好理解的, 具体的说明我们直接看代码实例

6.1 序列化

public static int MAXN = 10001;  
  
public static TreeNode[] queue = new TreeNode[MAXN];  
  
public static int l, r;  
  
public String serialize(TreeNode root) {  
    StringBuilder builder = new StringBuilder();  
    if (root != null) {  
       builder.append(root.val + ",");     // 将根节点的值放入 builder 中.实现序列化过程.
       l = 0;  
       r = 0;  
       queue[r++] = root;  
       while (l < r) {                    
          TreeNode cur = queue[l++];          // 将节点弹出.
          if (cur.left != null) {  
             builder.append(cur.left.val + ",");  // 将弹出节点的左边节点序列化
             queue[r++] = cur.left;               // 然后将左边节点放入到队列中.
          } else {  
             builder.append("#,");   // 左边节点为null, 不加入队列, 直接序列化
          }  
          if (cur.right != null) {  
             builder.append(cur.right.val + ",");  // 这里和上面是一样的.
             queue[r++] = cur.right;  
          } else {  
             builder.append("#,");  
          }  
       }  
    }  
    return builder.toString();  
}

6.2 反序列化

public TreeNode deserialize(String data) {  
    if (data.isEmpty()) {  // 先判断是不是null, 若是, 直接返回null
       return null;  
    }  
    String[] nodes = data.split(",");   // 将字符串分割
    int index = 0;  
    TreeNode root = generate(nodes[index++]);  // 直接建立好根节点.
    l = 0;  
    r = 0;  
    queue[r++] = root;  // 将根节点加入到队列中,
    while (l < r) {  
       TreeNode cur = queue[l++];  
       cur.left = generate(nodes[index++]);  // 这里判断左孩子是不是null
       cur.right = generate(nodes[index++]);  // 这里判断右孩子是不是null
       if (cur.left != null) {  
          queue[r++] = cur.left; 
       }  
       if (cur.right != null) {  
          queue[r++] = cur.right;  
       }  
    }  
    return root;  
}  
  
private TreeNode generate(String val) {  
	// 这个函数用来创建二叉树节点, 或者直接返回一个null的二叉树节点.
    return val.equals("#") ? null : new TreeNode(Integer.parseInt(val));  
}

7. 题目七:先序和中序结构重构二叉树

7.1 逻辑实现

在这里插入图片描述

我们假设需要上图中的两个数组, 实现一个二叉树, 我们可以利用递归的方式建立一个二叉树.

这棵二叉树的先序数组和中序数组一定是相互对应的, 按照上图为例子, 在 f 函数中, [0, 4]范围上的数组上是整棵树. 从这两个数组中建立好整棵树的头结点.
所以我们设计的函数 f(先序数组, 0, 4, 中序数组, 0, 4). 通过这个函数建立好根结点 a, 然后继续通过 f 函数建立 a 的左孩子:f(先序数组, 1, 2, 中序数组, 0, 1), 右孩子也是一样.

假设先序数组中范围是:[l1, r1], 中序数组中范围是:[l2, r2]. 先序数组中, 头结点肯定是:l1, 那么中序数组中对应的可能是:k, 然后我们建立左孩子, 左孩子的建立肯定是从 l1 + 1 开始, 中序的范围是 [l2, k - 1], 这样先序数组中对应的范围是:[l1, l1 + k - l2], 建立右孩子的过程也是一样的.

我们主要是通过一个范围, 然后根据对应的数组范围, 创建好对应的节点.

7.2 代码实例

这个方法中, 使用 HashMap 的意义是:先将所有的中序数组中所有的数字放好, 这样后续方便取用. 不用每一次寻找 k 的时候, 都要遍历一遍.

public static TreeNode buildTree(int[] pre, int[] in) {  
    if (pre == null || in == null || pre.length != in.length) {  
       return null;  
    }  
    HashMap<Integer, Integer> map = new HashMap<>();  
    for (int i = 0; i < in.length; i++) {  
       map.put(in[i], i);  
    }  
    return f(pre, 0, pre.length - 1, in, 0, in.length - 1, map);  
}  
  
public static TreeNode f(int[] pre, int l1, int r1, int[] in, int l2, int r2, HashMap<Integer, Integer> map) {  
    if (l1 > r1) {  
       return null;  
    }  
    TreeNode head = new TreeNode(pre[l1]);  
    if (l1 == r1) {  
       return head;  
    }  
    int k = map.get(pre[l1]);  
    // pre : l1(........)[.......r1]  
    // in  : (l2......)k[........r2]    // (...)是左树对应,[...]是右树的对应  
    head.left = f(pre, l1 + 1, l1 + k - l2, in, l2, k - 1, map);  
    head.right = f(pre, l1 + k - l2 + 1, r1, in, k + 1, r2, map);  
    return head;  
}

8. 题目八:判断完全二叉树

判断完全二叉树之前, 我们先确定一个概念:满二叉树,

满二叉树的概念是:一棵二叉树中, 所有的节点都是满的, 需要保证除了叶节点之外的每一个树节点都有左孩子和右孩子. 如下图.
在这里插入图片描述

那么完全二叉树就是将这个满二叉树从后往前砍, 如下图. 那怕是最后砍的只剩下 a 这一个根节点了, 也是一个完全二叉树.

或者说:完全二叉树的编号必须的连续的, 不能跳过.

在这里插入图片描述

8.1 逻辑实现

我们还是使用 BFS 进行遍历, 但是要一个一个节点地进行判断, 当我们遇见每一个节点的时候, 需要判断以下两个情况:

  1. 若是一个节点只有右孩子, 没有左孩子. 说明这个树不是完全二叉树.
  2. 若是发现一个节点的孩子不全, 那么从这个节点开始, 之后的所有节点, 必须全部都是叶节点. 若只要有一个不是叶子节点就说明不是一个完全二叉树.
    若是遇见上述两个情况中任意一个情况的二叉树, 不是完全二叉树.

8.2 代码实例

// 提交以下的方法  
// 如果测试数据量变大了就修改这个值  
public static int MAXN = 101;  
  
public static TreeNode[] queue = new TreeNode[MAXN];  
  
public static int l, r;  
  
public static boolean isCompleteTree(TreeNode root) {  
    if (root == null) {  
       return true;  
    }  
    l = r = 0;  
    queue[r++] = root;  
    // 是否遇到过左右两个孩子不双全的节点  
    boolean leaf = false;  
    while (l < r) {  
       TreeNode cur = queue[l++];  
       if ((cur.left == null && cur.right != null) || (leaf && cur.left != null)) {  
          return false;  // 这一段代码是用来判断是不是满足上述的两种情况的.
       }  
       if (cur.left != null) {  
          queue[r++] = cur.left;  
       }  
       if (cur.right != null) {  
          queue[r++] = cur.right;  
       }  
       if (cur.left == null || cur.right == null) {  
          leaf = true;  // 判断遇见了两个孩子全不全.
       }  
    }  
    return true;  
}
原来的判断条件是:
if ((cur.left == null && cur.right != null) || (leaf && (cur.left != null || cur.right != null))) {  
    return false;  
}

上面的代码可以进行简化:
if ((cur.left == null && cur.right != null) || (leaf && cur.left != null)) {  
  return false;  // 这一段代码是用来判断是不是满足上述的两种情况的.
}  

原理:无论是什么样的情况, 第一个代码执行到(cur.right != null)的时候, 已经判断好了只能是:false, 因为cur.right只有null和非null两种情况, 只要是第一个判断方式没有实现, 那只能说明cur.right 是 null, 这个时候已经不用判断了.

9. 题目九:求完全二叉树的节点个数

要求:需要保证时间复杂度必须低于 O(n).

9.1 逻辑实现

  1. 首先我们从根节点开始按照最左端的顺序往下遍历, 这样能得到这棵树有几层.
  2. 然后继续从根节点的右孩子开始遍历, 还是从最左端开始遍历,
  3. 若是到达了最深层, 说明左边的这棵二叉树一定是满二叉树,
    1. 此时左树的节点个数是:2^高度 - 1, 右边我们可以进行递归调用:f(右),
    2. 所以最后的节点个数为:2^高度 - 1 + 1 + f(右). 最后加一是因为需要加上根节点(对于当前的树来说).
  4. 若是没有到达最深层, 说明右边的这棵二叉树一定是满二叉树, 只是高度不同.
    1. 说明右树的节点个数位:2^高度 - 1, 然后我们可以递归调用 f(左).
    2. 以最后的节点个数为:2^高度 - 1 + 1 + f(左). 最后加一是因为需要加上根节点 (对于当前的树来说).

当然这里遇见了递归实现, 需要自己去画一下递归逻辑图, 这个是必不可少的.

9.2 代码实例

public static int countNodes(TreeNode head) {  
    if (head == null) {  
       return 0;  
    }  
    return f(head, 1, mostLeft(head, 1));  
}  
  
// cur : 当前来到的节点  
// level :  当前来到的节点在第几层  
// h : 整棵树的高度,不是cur这棵子树的高度, h是永远确定了的.  
// 求 : cur这棵子树上有多少节点  
public static int f(TreeNode cur, int level, int h) {  
    if (level == h) {  
       return 1;  
    }  
    if (mostLeft(cur.right, level + 1) == h) {  
       // cur右树上的最左节点,扎到了最深层  
       return (1 << (h - level)) + f(cur.right, level + 1, h);  
    } else {  
       // cur右树上的最左节点,没扎到最深层  
       return (1 << (h - level - 1)) + f(cur.left, level + 1, h);  
    }  
}  
  
// 当前节点是cur,并且它在level层  
// 返回从cur开始不停往左,能扎到几层  
public static int mostLeft(TreeNode cur, int level) {  
    while (cur != null) {  
       level++;  
       cur = cur.left;  
    }  
    return level - 1;  
}

9.3 时间复杂度分析

这个方法的逻辑实现就是从根节点开始, 选择其中一侧, 遍历这一侧的最左侧的所有节点, 所以假设这棵二叉树的高度为:h, 所以第一次是:h, 第二次是:h - 1 … 最后一次是 0, 所以时间复杂度是:O(h^2), 那么 h = log(n), n为二叉树的节点个数, 所以最后的时间复杂度是:O(log(n)^2), 这个的时间复杂度要比 O(n) 快很多,

比如 n = 2^32, 使用这个方法我们只需要 32^2 = 1024 但是遍历整个二叉树需要 O(2^32), 远远小于 O(n).

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/901424.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

便捷之选:微信小程序驱动的停车场管理系统

作者介绍&#xff1a;✌️大厂全栈码农|毕设实战开发&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。 &#x1f345;获取源码联系方式请查看文末&#x1f345; 推荐订阅精彩专栏 &#x1f447;&#x1f3fb; 避免错过下次更新 Springboot项目精选实战案例 更多项目…

跨境支付,哪些国产数据库能接得住?

最近有一个非常重大的事件&#xff0c;那就是10月22日-24日的金砖国家会议。金砖国家领导人第十六次会晤是金砖国家进一步凝聚共识、以实际行动推动“全球南方”共同发展进步的重要机遇。 酝酿已久的金砖跨境支付体系&#xff0c;也在这次峰会中正式推出。金砖国家的支付系统一…

国内大语言模型哪家更好用?

大家好&#xff0c;我是袁庭新。 过去一年&#xff0c;AI大语言模型在爆发式增长&#xff0c;呈现百家争鸣之态。国内外相关厂商积极布局&#xff0c;并相继推出自家研发的智能化产品。 我在工作中已习惯借助AI来辅助完成些编码、创作、文生图等任务&#xff0c;甚至对它们产…

【计网】从零开始认识IP协议 --- 认识网络层,认识IP报头结构

从零开始认识IP协议 1 网络层协议1.1 初步认识IP协议1.2 初步理解IP地址 2 IP协议报头3 初步理解网段划分 1 网络层协议 1.1 初步认识IP协议 我们已经熟悉了传输层中的UDP和TCP协议&#xff0c;接下来我们来接触网络层的协议&#xff1a; 网络层在计算机网络中的意义主要体现…

线段树

文章目录 1 线段树概念2 线段树操作2.1 建树2.2 区间修改2.3 区间查询2.4 练习题目 3 线段树进阶3.1 乘法线段树 * 补充&#xff1a;快读快写4 End 1 线段树概念 线段树 ( S e g m e n t T r e e ) (Segment\ Tree) (Segment Tree) 是 O I OI OI 中的常用算法。线段树是一种…

PHP-FPM 性能配置优化

4 核 8 G 服务器大约可以开启 500 个 PHP-FPM&#xff0c;极限吞吐量在 580 qps &#xff08;Query Per Second 每秒查询数&#xff09;左右。 Nginx php-fpm 是怎么工作的&#xff1f; php-fpm 全称是 PHP FastCGI Process Manager 的简称&#xff0c;从名字可得知&#xff…

基于SSM的冰淇淋在线购买网站【附源码】

基于SSM的冰淇淋在线购买网站 效果如下&#xff1a; 系统首页界面 用户登录界面 冰淇淋页面 每日秒杀页面 个人中心界面 管理员登录界面 管理员功能界面 口味管理界面 冰淇淋管理界面 每日秒杀管理界面 视频教学管理界面 研究背景 近些年&#xff0c;随着中国经济发展&#…

订购 Claude AI 的第二天 它独自完成 文字转语音 flask应用

图二里&#xff0c;删除几个无关的 chats 全程我做的工作&#xff1a;向 AI 提要求&#xff0c;copy / paste 代码&#xff0c;在venv验证运行&#xff0c;向 AI 反馈&#xff0c;总共用了3个 chats.&#xff08;图中的只有一个 Chat&#xff0c; 删掉的另外两个: Python 库安…

海外云手机实现高效的海外社交媒体营销

随着全球化的深入发展&#xff0c;越来越多的中国企业走向国际市场&#xff0c;尤其是B2B外贸企业&#xff0c;海外社交媒体营销已成为其扩大市场的重要手段。在复杂多变的海外市场环境中&#xff0c;如何有效提高营销效率并降低运营风险&#xff0c;成为了众多企业的首要任务。…

计算机网络(十二) —— 高级IO

#1024程序员节 | 征文# 目录 一&#xff0c;预备 1.1 重新理解IO 1.2 五种IO模型 1.3 非阻塞IO 二&#xff0c;select 2.1 关于select 2.2 select接口参数解释 2.3 timeval结构体和fd_set类型 2.4 socket就绪条件 2.5 select基本工作流程 2.6 简单select的服务器代…

【mysql进阶】4-8 临时表空间

临时表空间 - Temporary Tablespaces 1 什么是临时表&#xff1f; ✅ 解答问题 临时表存储的是临时数据&#xff0c;不能永久的存储数据&#xff0c;⼀般在复杂的查询或计算过程中⽤来存储过渡的中间结果&#xff0c;MySQL在执⾏查询与计算的过程中会⾃动⽣成临时表&#xff0c…

C++ 抛异常

目录 一.抛异常与运行崩溃的区别 1.运行崩溃 2.抛异常 二.抛异常机制存在的意义 1.清晰的处理错误 2.结构化的错误管理 3.跨函数传递错误信息 4.异常对象多态性 三.抛异常的使用方法 1.抛出异常 (throw) 2.捕获异常 (catch) 3.标准异常类 四.抛异常的处理机制 1.抛…

2024“源鲁杯“高校网络安全技能大赛-Misc-WP

Round 1 hide_png 题目给了一张图片&#xff0c;flag就在图片上&#xff0c;不过不太明显&#xff0c;写个python脚本处理一下 from PIL import Image ​ # 打开图像并转换为RGB模式 img Image.open("./attachments.png").convert("RGB") ​ # 获取图像…

rabbitmq 使用注意事项

1&#xff0c;注意开启的端口号&#xff0c;一共四个端口号&#xff0c;1883是mqtt连接的端口号&#xff0c;如果没开&#xff0c;是连接不上的需要手动起mqtt插件。 //开始mqtt插件服务 rabbitmq-plugins enable rabbitmq_mqtt 2&#xff0c;15672端口是http网页登录的管理后…

Next Stack技术联盟成立:打造新一代基础软件技术栈

北京&#xff0c;2024 年 10 月 —— 在全球数字化浪潮的推动下&#xff0c;中国基础软件产业迎来了前所未有的创新机遇与挑战。为应对这一时代任务并推动中国基础软件的全球化进程&#xff0c;观测云携手多家领先技术企业正式宣布成立 Next Stack 技术联盟。这一联盟旨在汇聚国…

接口测试(五)jmeter——get请求

一、get请求——短信验证码&#xff08;示例仅供参考&#xff09; 1. get请求&#xff1a;传参数据直接拼接在地址后面&#xff0c;jmeter不需要设置请求头content-type 注&#xff1a;短信验证码接口&#xff0c;返回结果中不会返回短信验证码&#xff0c;是存在数据库表中&a…

Maven项目管理工具-初始+环境配置

1. Maven的概念 1.1. 什么是Maven Maven是跨平台的项目管理工具。主要服务于基于Java平台的项目构建&#xff0c;依赖管理和项目信息管理。 理想的项目构建&#xff1a;高度自动化&#xff0c;跨平台&#xff0c;可重用的组件&#xff0c;标准化的流程 maven能够自动下载依…

Mybatis-plus-入门

Mybatis-plus-入门 1&#xff1a;介绍 mybatis-plus的官网&#xff1a;MyBatis-Plus &#x1f680; 为简化开发而生 2: 快速入门 步骤&#xff1a; 1&#xff1a;引入依赖&#xff1a; <dependency><groupId>com.baomidou</groupId><artifactId>my…

STM32使用硬件I2C读写AT24C02 EEPROM(一)

文章目录 一、软件准备配置I2C接口&#xff1a;生成工程代码&#xff1a; 二、编写驱动程序初始化I2C接口&#xff1a;编写读写函数&#xff1a; 三、调试与测试 前面讲到使用软件模拟i2c读写AT24C02&#xff0c;这篇文章使用stm32 提供的硬件i2c读写&#xff0c;看看怎么回事 …

gin入门教程(3):创建第一个 HTTP 服务器

首先设置golang github代理&#xff0c;可解决拉取git包的时候&#xff0c;无法拉取的问题&#xff1a; export GOPROXYhttps://goproxy.io再查看自己的go版本&#xff1a; go version我这里的版本是&#xff1a;go1.23.2 linux/arm64 准备工作做好之后就可以进行开发了 3.…