【数据结构】(8) 二叉树
一、树形结构
1、什么是树形结构
- 根节点没有前驱,其它节点只有一个前驱(双亲/父结点)。
- 所有节点可以有 0 ~ 多个后继,即分支(孩子结点)。
- 每个结点作为子树的根节点,这些子树互不相交。
2、关于树概念词
节点的度:节点的分支数。
树的度:树中最大的结点的度。
节点的层次:根为第一层。
树的高度:最大层次。
兄弟结点:同父的结点:
堂兄弟结点:同爷爷的结点。
节点的祖先:从根到该结点路径上的所有结点。
子孙:某节点为根,其子树中任意结点。
森林:互不相交的树集合。
3、树的表示形式
很多种,简单了解最常用(这种表示法,方便将树转为二叉树)的孩子兄弟表示法:
class Node {int data; // 存数据Node firstChild; // 存第一个孩子Node firstBrother; // 存第一个兄弟
}
二、二叉树
1、什么是二叉树
- 树的度为2。
- 子树有左右之分,不能颠倒。
2、特殊的二叉树
- 满二叉树:若k表示结点层数,则每层结点数都等于 2^(k-1) 的树就是满二叉树。
- 完全二叉树:每个结点编号与满二叉树中每个节点编号一一对应的树。
3、二叉树性质
3.1、性质
- 第 k 层最多有 2^(k-1) 个结点。
- 高度为 k 的二叉树最多有 (2^k) - 1 个结点。
- 任意二叉树,叶子节点个数 n0 和有两个度的结点个数 n2 有关系:n0 = n2+1。
推导:n = n0+n1+n2 个结点,除了根节点都有前驱,故一共 n-1 个度。n-1 = 0*n0+1*n1+2*n2。
联合两个式子得到:n0 = n2 + 1。
- n 个节点的完全二叉树深度为 log(n+1) 上取整。
推导:n = (2^k)-1,k = log(n+1)。完全二叉树结点数肯定小于等于满二叉树结点数,所以 n 可能不够,结果就上取整。
- 从根节点开始(编号0),从左往右给每个结点编号:
① 双亲编号: i=0,根节点,无双亲;i>0,双亲编号=(i-1)/2。
② 左孩子编号:若 2*i+1 < n,左孩子编号 2*i+1;否则没左孩子。
③ 右孩子编号:若 2*i+2 < n,右孩子编号 2*i+2;否则没右孩子。
3.2、练习题
4、二叉树的遍历
4.1、遍历方式
前序:根>>左>>右,ABDCEF。
中序:左>>根>>右,DBAECF。
后序:左>>右>>根,DBEFCA。
层序:从上到下,从左往右,ABCDEF。
4.2、遍历序列转二叉树
如果想根据遍历序列获得二叉树,则必须是两种遍历序列,并且其中一种必须是中序遍历。因为,前、后、层序遍历序列能得知每个子二叉树的根;中序遍历能得知某根的左右子树。
4.3、代码(递归)
二叉树的存储结构有顺序存储和链式存储,本文只讨论了链式存储。顺序存储请看我写的优先级队列(堆)部分。
二叉树的表示形式:
public class BinaryTree {public static class TreeNode {char value;TreeNode left;TreeNode right;public TreeNode(char value) {this.value = value;}}private TreeNode root;............
}
构造一棵二叉树(主要为了测试,这种构造方式不可取):
public void createTree() {TreeNode A = new TreeNode('A');TreeNode B = new TreeNode('B');TreeNode C = new TreeNode('C');TreeNode D = new TreeNode('D');TreeNode E = new TreeNode('E');TreeNode F = new TreeNode('F');A.left = B;A.right = C;B.left = D;C.left = E;C.right = F;root = A;}
前序遍历:
public static void preOrder(TreeNode root) {if (root == null) {return;}System.out.print(root.value + " ");preOrder(root.left);preOrder(root.right);}
中序遍历:
public static void inOrder(TreeNode root) {if (root == null) {return;}inOrder(root.left);System.out.print(root.value + " ");inOrder(root.right);}
后序遍历:
public static void postOrder(TreeNode root) {if (root == null) {return;}postOrder(root.left);postOrder(root.right);System.out.print(root.value + " ");}
4.4、代码(非递归):
前序遍历:144. 二叉树的前序遍历 - 力扣(LeetCode)
分析:深度优先遍历,左子树遍历完后,转到右子树,需要获得父亲结点,属于后进先出,用栈实现。
对于非空树:当前处理子树的根 cur 的初始值为 root。
step1:树不为空,树根 cur 入栈,并打印。若左子树不为空,更新子树 cur 为左子树,重复step1。直到遇到左子树为空,即 cur 为空停止上述循环。转到step2,开始判断右子树。
step2:弹出栈顶元素,即左子树的父结点,用于转到右子树,若右子树为空,重复step2。直到栈为空,且右子树仍为空时,树遍历完成;或者遇到右子树不为空,更新子树 cur 为右子树,重复 step1,开始遍历右子树。
class Solution {public List<Integer> preorderTraversal(TreeNode root) {Stack<TreeNode> stack = new Stack<>(); // 栈,用于左子树遍历完后,获取父节点,转到遍历右子树List<Integer> list = new ArrayList<>(); // 打印序列// 空树,不打印if(root == null) {return list;}TreeNode cur = root; // 当前处理子树的根。对于非空树,初始值为 root。// 栈不为空,说明还有右子树需要判断// cur不为空,说明还有非空右子树需要遍历while(!stack.isEmpty() || cur != null) { // step1// 一直是 push 操作,不需要判断栈空// 处理情况1:直到左子树为空,退出循环,再转到 step2// 处理情况2:右子树不为空,需要遍历右子树,转到 step1while(cur != null) {stack.push(cur); // 当前处理子树的根节点入栈list.add(cur.val); // 打印当前处理子树的根结点cur = cur.left; // 转到左子树}// step2cur = stack.pop(); // 弹出栈顶元素cur = cur.right; // 转到右子树}return list;}
}
也可以用 ArrayDeque 替代 Stack,区别是 Stack 的方法有 synchronized 关键字修饰,在多线程环境中,用锁实现同步。比如:有一个 Stack 对象 p1,当一个线程使用了 p1调用的同步方法,那么 p1 对象的同步方法就会被锁住,使得其它的线程不能调用 p1 的同步方法;直到那个线程使用完后解锁,其它线程才能够使用。因此,在多线程环境中,Stack 相较于 ArrayDeque 是线程安全的。
中序遍历:94. 二叉树的中序遍历 - 力扣(LeetCode)
分析:左根右,根保存在栈中。先遍历左子树,左子树遍历完后,弹出根时打印根,最后遍历右子树。依然是用栈保存父节点,用于转向右子树。
方法与前序遍历类似,只不过打印根的位置不同。
class Solution {public List<Integer> inorderTraversal(TreeNode root) {Stack<TreeNode> stack = new Stack<>();List<Integer> list = new ArrayList<>(); if(root == null) {return list;}TreeNode cur = root; while(!stack.isEmpty() || cur != null) { while(cur != null) {stack.push(cur); cur = cur.left; }cur = stack.pop(); list.add(cur.val); // 弹出父节点(根结点)时,才打印根cur = cur.right; }return list;}
}
后续遍历:145. 二叉树的后序遍历 - 力扣(LeetCode)
分析:左右根,左子树遍历完后,父节点不能弹出栈,右子树遍历完后,父节点再弹出栈打印。
我们现在需要思考,右子树遍历完的标志是什么。比如上图这棵树,在遍历到 F 时,栈中元素应该为:A, C, F。当前父节点 F 的左子树为 null 不遍历,右子树也为 null 不遍历,弹出根 F 并打印,此时栈为:A,C。当前父节点 C 的右子树是F,F已遍历完,需要弹出根C并打印......
可以观察到,当前父节点(栈顶元素)的右子树为空,或者父节点的右孩子是上一次弹出的栈顶元素时, 表示右子树遍历结束。
class Solution {public List<Integer> postorderTraversal(TreeNode root) {Stack<TreeNode> stack = new Stack<>();List<Integer> list = new ArrayList<>(); if(root == null) {return list;}TreeNode cur = root; TreeNode pre = null; // 存储上一次遍历完成的子树的根while(!stack.isEmpty() || cur != null) { while(cur != null) {stack.push(cur); cur = cur.left; }TreeNode top = stack.peek(); // 查看父节点元素,继续判断是否要遍历右子树// 父节点的右孩子为空,或者父节点的右孩子是上一次遍历完的子树(上一次弹出的根),则表示其右子树遍历完成// cur 不能改变,仍为 null,表示右子树已经遍历完了,不需要再在step1中遍历。所以用 top 暂存父节点if(top.right == null || top.right == pre) {stack.pop(); // 弹出父节点list.add(top.val); // 打印父节点pre = top; // 保存遍历完的子树的根} else { // 右子树没遍历完,继续遍历父节点的右子树cur = top.right; }}return list;}
}
层序遍历:使用队列,根节点开始,从上往下,从左到右,先进先出。每弹出一个结点,打印该节点,让后将其孩子全部依次入队。直到队列为空结束。
public void levelOrder() {Queue<TreeNode> queue = new LinkedList<>(); // 队列if (root == null) { // 空树return;}queue.offer(root); // 根节点入队while (!queue.isEmpty()) {TreeNode node = queue.poll(); // 出队System.out.print(node.value + " "); // 打印if (node.left!= null) { // 左孩子不为空,入队queue.offer(node.left);}if (node.right!= null) { // 右孩子不为空,入队queue.offer(node.right);}}}
5、二叉树的基本操作
5.1、求树中节点个数
遍历思想:把遍历中的打印结点,换为计数结点。
private int count; public int size() {count = 0;size(root);return count;}public void size(TreeNode root) {if (root == null) {return;}count++;size(root.left);size(root.right);}
子问题思想:树的结点树=左数结点数+右数结点数+1。
public static int size2(TreeNode root) {if (root == null) { // 空树,0个结点return 0;}return size2(root.left) + size2(root.right) + 1;}
5.2、求叶子节点个数
遍历思想:把打印结点换为,是叶子结点就计数。
private int leafCount;public int leafCount() {leafCount = 0;leafCount(root);return leafCount;}public void leafCount(TreeNode root) {if (root == null) {return;}if (root.left == null && root.right == null) {leafCount++;} leafCount(root.left);leafCount(root.right);}
子问题思想:树的叶子结点个数=左子树叶子节点个数+右子树叶子节点个数。
public static int leafCount2(TreeNode root) {if (root == null) { // 空树,0个叶子节点return 0;}if (root.left == null && root.right == null) { // 只有一个根结点,1个叶子节点return 1;}return leafCount2(root.left) + leafCount2(root.right);}
5.3、求第K层结点个数
遍历思想:把打印结点换成,是第 K 层结点就计数。(遍历整棵树。)
private int levelCount;public int levelCount(int k) {levelCount = 0;levelCount(root, k);return levelCount;}public void levelCount(TreeNode root, int k) {if (root == null) {return;}if (k == 1) {levelCount++;}levelCount(root.left, k - 1);levelCount(root.right, k - 1);}
子问题思想:树的第K层结点个数=左子树的第K-1层节点个数+右子树的第K-1层节点个数。(如果K合法,到第K层为止,效率比遍历思想更高。)
public static int levelCount2(TreeNode root, int k) {if (root == null) { // 空树,没有节点return 0;}if (k == 1) { // 树的第一层结点为根,1个节点return 1;}return levelCount2(root.left, k - 1) + levelCount2(root.right, k - 1);}
5.3、求二叉树的高度
子问题思想:树的高度=max(左子树高度,右子树高度)+1。
时间复杂度=调用递归函数的次数=O(n)。
空间复杂度,因为调用到叶子节点后,就会释放空间,再遍历另一个分叉,因此空间复杂度是树的高度=O(logN)。
public static int height(TreeNode root) {if (root == null) { // 空树,深度为0return 0;}return Math.max(height(root.left), height(root.right)) + 1;}
5.4、检测 value 是否在树中
子问题思想:是根,返回根节点地址;搜索左子树,返回值不为空,返回地址;搜索右子树,返回值不为空,返回地址;否者,返回 nll。
最坏时间复杂度:O(N)
public static TreeNode findValue(TreeNode root, char value) {if (root == null) { // 树为空,没有结点return null; }if (root.value == value) { // 是根节点return root;}TreeNode left = findValue(root.left, value); // 搜索左子树if (left!= null) { // 找到了return left;}return findValue(root.right, value); // 搜索右子树}
5.5、判断树是否为完全二叉树
思路:空树必为完全二叉树。层序遍历树,空结点要入队。遇到第一个空结点,如果是完全二叉树,则在队列中,这个空结点的右边必全是空结点;如果不是完全二叉树,在队列中,其右必存在不为空结点。
故遇到空结点后,马上在队列中判断其后是否都为空,是则为完全二叉树;否则不为。
public static boolean isComplete(TreeNode root) {if(root == null) { // 空树return true;}Queue<TreeNode> queue = new LinkedList<>(); // 队列queue.offer(root); // 根节点入队while (!queue.isEmpty()) { // 层序遍历树,寻找树中第一个空结点TreeNode node = queue.poll(); // 弹出一个结点if (node != null) {queue.offer(node.left); // 左孩子入队queue.offer(node.right); // 右孩子入队} else { // 遇到空结点,退出循环break;}}// 判断空结点后是否都为空姐点while (!queue.isEmpty()) {if (queue.poll() != null) { // 还有非空结点,不是完全二叉树return false;}}return true;}
6、二叉树相关OJ题
6.1、分层包装
102. 二叉树的层序遍历 - 力扣(LeetCode)
分析:在正常前序遍历的基础上,根节点入队,得到初始队列。>> 依次出队,包装为一层,添加到树中。其孩子依次入队,作为下一层。>> 重复上述操作,直到下一层为空。
class Solution {public List<List<Integer>> levelOrder(TreeNode root) {List<List<Integer>> list = new ArrayList<>();if(root == null) { // 空树return list;}// 初始,根节点入队Queue<TreeNode> queue = new LinkedList<>(); // 下一层队列queue.offer(root); while(!queue.isEmpty()) { // 直到下一层为空List<Integer> curLevel = new ArrayList<>(); // 当前层 int size = queue.size(); // 处理下一层,下一层作为当前层的大小while(size-- != 0) { // 遍历队列,包装为层,将每个结点的孩子放入下一层TreeNode node = queue.poll(); // 弹出一个结点curLevel.add(node.val); // 包装到当前层中if(node.left != null) { // 左孩子放入下一层queue.offer(node.left);}if(node.right != null) { // 右孩子放入下一层queue.offer(node.right);}}list.add(curLevel);}return list;}
}
6.2、两颗相同的树
100. 相同的树 - 力扣(LeetCode)
分析:子问题思想。两根为空,必相同。一根为空,一根不为空,必不相同。两根不为空,两值不同,必不相同;两值相同,两棵树的左子树相同且右子树相同,则相同,否则不同。
时间复杂度:O(min(n, m)),最坏情况,结点数最少的那棵树遍历完,就知道是否相同了。
class Solution {public boolean isSameTree(TreeNode p, TreeNode q) {if(p == null && q == null) { // 两空树,相同return true;}if((p == null && q != null) || (p != null && q == null)) { // 一根空,一根不空,不相同return false;}// 两根不为空if(p.val != q.val) { // 值不相同,不相同return false;}// 两根相同,左、右子树相同才相同;否则不同return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);}
}
6.3、另一棵树的子树
572. 另一棵树的子树 - 力扣(LeetCode)
分析:子问题思想。树为空,没有子树;子树与树相同,是子树;子树与树不相同,子树是不是树的左子树的子树,是则是子树;子树是不是树的右子树的子树,是则是子树;否则不是子树。
时间复杂度:O(m*n),每遇树中一个结点,就遍历子树是否与之相等。m 个结点比较 m 次,每次比较 n 个子树结点。
class Solution {public boolean isSameTree(TreeNode p, TreeNode q) {if(p == null && q == null) { // 两空树,相同return true;}if((p == null && q != null) || (p != null && q == null)) { // 一根空,一根不空,不相同return false;}// 两根不为空if(p.val != q.val) { // 值不相同,不相同return false;}// 两根相同,左、右子树相同才相同;否则不同return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);}public boolean isSubtree(TreeNode root, TreeNode subRoot) {if(root == null) { // 树为空,没有子树return false;}if (isSameTree(root, subRoot)) { // 树和子树相同,是树的子树return true;}if(isSubtree(root.left, subRoot)) { // 是左子树的子树,是树的子树return true;}return isSubtree(root.right, subRoot); // 是右子树的子树,是树的子树;否则不是树的子树}
}
6.4、翻转二叉树
226. 翻转二叉树 - 力扣(LeetCode)
分析:如果为空或者只有一个根节点,不翻转;否则,左子树和右子树交换,继续翻转左子树、右子树。
class Solution {public TreeNode invertTree(TreeNode root) {// 如果树为空,或者只有一个根节点,不翻转if(root == null || (root.left == null && root.right == null)) {return root;}// 否则左子树和右子树交换TreeNode tmp = root.left;root.left = root.right;root.right = tmp;// 翻转左子树、右子树invertTree(root.left);invertTree(root.right);return root;}
}
6.5、判断平衡二叉树
110. 平衡二叉树 - 力扣(LeetCode)
平衡二叉树:每一棵子树的左右子树的高度差不超过1。
分析:空树,是平衡二叉树。左子树是平衡二叉树,且右子树是平衡二叉树,左子树的高度和右子树的高度差的绝对值≤1,是平衡二叉树;否则不是。
时间复杂度:O(N^2),对于树中每个结点,都要求其左、右子树的高度;每次求子树高度,都会遍历整棵子树。
class Solution {public int height(TreeNode root) {if (root == null) { // 空树,深度为0return 0;}return Math.max(height(root.left), height(root.right)) + 1;}public boolean isBalanced(TreeNode root) {// 空树是平衡二叉树if(root == null) {return true;}// 左子树不是平衡二叉树,或者右子树不是平衡二叉树,则树不是平衡二叉树if(!isBalanced(root.left) || !isBalanced(root.right)) {return false;}// 左右子树高度差的绝对值≤1,是平衡二叉树,否则不是return Math.abs(height(root.left) - height(root.right)) <= 1;}
}
改进:其实,求树的高度就包含了求子树的高度;但是上面的代码重复求了子树的高度。改进思路就是,我们只需求一次树的高度,在此过程中必然历经,求子树、子子树高度,在求到它们高度的之后,马上判断它们的高度差的绝对值是否≤1,它们是否是平衡二叉树。
时间复杂度:O(N)。只求了一次树的高度,遍历了所有结点。
class Solution {/*** 返回值:-1 表示不为平衡二叉树,大于 -1 表示树的高度。*/private int height(TreeNode root) {if (root == null) { // 空树,深度为0return 0;}// 求左、右子树高度int leftH = height(root.left);int rightH = height(root.right);// 求完高度,马上判断是否符合平衡二叉树的要求// 左、右子树是否为平衡二叉树if(leftH == -1 || rightH == -1) {return -1;}// 左右子树高度差是否符合要求if(Math.abs(leftH - rightH) > 1) {return -1;}// 左、右子树符合平衡二叉树要求,返回树的高度return Math.max(leftH, rightH) + 1;}public boolean isBalanced(TreeNode root) {// 空树是平衡二叉树if(root == null) {return true;}// 如果返回值 > -1,表示是树高度,是平衡二叉树;否则不是return height(root) > -1;}
}
6.6、判断对称二叉树
101. 对称二叉树 - 力扣(LeetCode)
分析:子问题思路。空树,对称。非空树,对于左、右子树根节点,都为空,则对称;一个为空、一个不为空,则不对称;都不为空,但是值不相等,则不对称;都不为空,且值相等,左根的左子树与右根的右子树对称,且左根的右子树与右根的左子树对称,则树对称;否则不对称。
class Solution {public boolean isSymmetric(TreeNode root) {if(root == null) { // 空树,对称return true;}return isSymmetricChild(root.left, root.right);}public boolean isSymmetricChild(TreeNode leftRoot, TreeNode rightRoot) {// 左根、右根都为空,对称if(leftRoot == null && rightRoot == null) {return true;}// 左、右根其中一个不为空,另一个为空,不对称if((leftRoot == null && rightRoot != null) || (leftRoot != null && rightRoot == null)) {return false;}// 左右根不为空,但值不等,不对称if(leftRoot != null && rightRoot != null && leftRoot.val != rightRoot.val) {return false;}// 左根的左子树、右根的右子树对称,且左根的右子树、右根的左子树对称,才对称return isSymmetricChild(leftRoot.left, rightRoot.right) && isSymmetricChild(leftRoot.right, rightRoot.left);}
}
6.7、先序构建,中序遍历
二叉树遍历_牛客题霸_牛客网
分析:重点在根据先序遍历序列构建二叉树。因为这里告诉了空结点,所以只有一个先序遍历序列也能构建二叉树。
遍历思想。先序遍历,如果根节点是'#',则返回 null;如果根节点不是'#',则创建结点,到下一个字符,创建左子树、创建右子树,最后返回根节点。
import java.util.Scanner;class TreeNode {char value;TreeNode left;TreeNode right;public TreeNode(char value) {this.value = value;}
}public class Main {public static int i; // 记录字符下标// 先序创建树public static TreeNode createTree(String str) {TreeNode root = null;if (str.charAt(i) != '#') {root = new TreeNode(str.charAt(i)); // 创建新节点i++;// 创建左、右子树root.left = createTree(str);root.right = createTree(str);} else {i++;}return root;}// 中序遍历public static void inOrder(TreeNode root) {if (root == null) {return;}inOrder(root.left);System.out.print(root.value + " ");inOrder(root.right);}public static void main(String[] args) {Scanner in = new Scanner(System.in);while (in.hasNextLine()) {String str = in.nextLine();i = 0;TreeNode root = createTree(str);inOrder(root);}}
}
6.8、最近公共祖先
236. 二叉树的最近公共祖先 - 力扣(LeetCode)
分析:根据题目,树中必有p、q两节点。如果 p 在左子树中,q 在右子树中,则 root 为最近公共祖先。
如果 p、q 中存在一个是根结点,则 root 为最近公共祖先。
如果 p、q 都在左子树中,进入左子树找最近公共祖先。
如果 p、q 都在右子树中,进入右子树找最近公共祖先。
时间复杂度:O(N)
class Solution {public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {// 如果为空树,没有最近公共祖先// 必须加此判断,遇到叶子结点,递归函数才能返回 nullif(root == null) {return null;}// 其中一个是根节点,最近公共祖先是根if(p == root || q == root) {return root;}TreeNode leftFather = lowestCommonAncestor(root.left, p, q);TreeNode rightFather = lowestCommonAncestor(root.right, p, q);// p、q 分布在左、右两子树中,最近公共祖先是根if(leftFather != null && rightFather != null) {return root;}// 最近公共祖先在左子树中if(leftFather != null) {return leftFather;}// 最近公共祖先在右子树中if(rightFather != null) {return rightFather;}// 树中不存在 q、preturn null;}
}
思路2:找到根节点分别到 p、q 的路径,对比路径,找到最近公共祖先。
p 的路径:3、5、6。size=3
q 的路径:3、5、2、4。size=4
q 路径出栈 4-3=1 个元素,得到 3、5、2。
p 、q 同时出栈,不相同则继续同时出栈,相同则找到最近公共祖先。
时间复杂度:O(N^2),找了两次路径。
class Solution {public boolean getPath(TreeNode root, TreeNode target, Stack<TreeNode> stack) {if(root == null) { // 空树,没有路径return false;}stack.push(root); // 根节点入栈if(root == target) { // 找到目标节点,退出return true;}boolean found = getPath(root.left, target, stack); // 在左子树中寻找路径if(found) { // 找到路径,退出return true;}found = getPath(root.right, target, stack); // 在右子树中寻找路径if(found) { // 找到路径,退出return true;}stack.pop(); // 路径没找到,根结点出栈return false;}public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {Stack<TreeNode> stack = new Stack<>();Stack<TreeNode> stack2 = new Stack<>();getPath(root, p, stack); // 寻找 p 的路径getPath(root, q, stack2); // 寻找 q 的路径// 计算路径长度差,长的路径先出栈差的绝对值长度int len = stack.size() - stack2.size();if(len < 0) { // 长度差为负,交换两个栈Stack<TreeNode> temp = stack;stack = stack2;stack2 = temp;len = -len;}// 弹出栈中元素,直到长度差为0while(len-- != 0) {stack.pop();}// 弹出栈中元素,不相同则继续弹,相同则为最近公共祖先while(!stack.isEmpty() && !stack2.isEmpty() && stack.peek() != stack2.peek()) {stack.pop();stack2.pop();}return stack.isEmpty()? null : stack.peek(); // 栈为空,没有最近公共祖先}
}
6.9、根据前序、中序构造树
105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)
分析:前序遍历从左往右,找根节点,创建根节点。再在中序遍历中找根节点的位置,区分左右子树。再构建左子树,构建右子树。
class Solution {private int preIndex; // 先序序列的根下标public TreeNode buildTree(int[] preorder, int[] inorder) {return buildTree(preorder, inorder, 0, inorder.length-1);}private int findIndex(int[] inorder, int low, int high, int rootVal) {for(int i = low; i <= high; i++) {if(inorder[i] == rootVal) {return i;}}return -1;}// low、high 为在中序序列中子树的起始、终止下标private TreeNode buildTree(int[] preorder, int[] inorder, int low, int high) {if(low > high) { // 没有结点了,返回空return null;}int rootVal = preorder[preIndex]; // 根TreeNode root = new TreeNode(rootVal); // 创建根结点preIndex++;int inIndex = findIndex(inorder, low, high, rootVal); // 在中序序列中找根的位置// 构建左、右子树root.left = buildTree(preorder, inorder, low, inIndex-1);root.right = buildTree(preorder, inorder, inIndex+1, high);return root;}
}
6.10、根据中序、后序遍历构造树
106. 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode)
分析:方法与 6.9 大致相同,只不过后序序列是从后往前遍历根。后序:左右根,倒着遍历,会先创建右子树。
class Solution {private int postIndex; // 后序序列的根下标public TreeNode buildTree(int[] inorder, int[] postorder) {postIndex = postorder.length-1;return buildTree(postorder, inorder, 0, inorder.length-1);}private int findIndex(int[] inorder, int low, int high, int rootVal) {for(int i = low; i <= high; i++) {if(inorder[i] == rootVal) {return i;}}return -1;}// low、high 为在中序序列中子树的起始、终止下标private TreeNode buildTree(int[] postorder, int[] inorder, int low, int high) {if(low > high) { // 没有结点了,返回空return null;}int rootVal = postorder[postIndex]; // 根TreeNode root = new TreeNode(rootVal); // 创建根结点postIndex--;int inIndex = findIndex(inorder, low, high, rootVal); // 在中序序列中找根的位置// 构建左、右子树root.right = buildTree(postorder, inorder, inIndex+1, high);root.left = buildTree(postorder, inorder, low, inIndex-1);return root;}
}
6.11、根据二叉树创建字符串
分析:前序遍历,同一棵子树中的结点会被()包在一起。空树,不打印。非空树,打印根节点,如果左右孩子都为空,什么都不打印。如果左孩子不为空,右孩子为空,打印(,遍历左子树,打印)。如果左孩子为空,右孩子不为空,打印()(,遍历右子树,打印)。如果左右孩子都不为空,打印(,遍历左子树,打印)(,遍历右子树,打印)。
class Solution {public String tree2str(TreeNode root) {StringBuilder str = new StringBuilder();tree2str(root, str);return str.toString();}public void tree2str(TreeNode root, StringBuilder str) {// 空树,不打印if (root == null) {return;}str.append(root.val); // 打印根节点// 如果左孩子不为空,打印(,遍历左子树,打印)if (root.left != null) {str.append('(');tree2str(root.left, str);str.append(')');// 如果右孩子为空,什么都不打印// 如果右孩子不为空,打印(,遍历右子树,打印)// if(root.right != null) {// str.append('(');// tree2str(root.right, str);// str.append(')');// }} else {// 如果右子树为空,什么都不打印// 如果右子树不为空,打印(),打印(,遍历右子树,打印)if (root.right != null) {str.append("()");// str.append('(');// tree2str(root.right, str);// str.append(')');}}if (root.right != null) {str.append('(');tree2str(root.right, str);str.append(')');}}
}
相关文章:
【数据结构】(8) 二叉树
一、树形结构 1、什么是树形结构 根节点没有前驱,其它节点只有一个前驱(双亲/父结点)。所有节点可以有 0 ~ 多个后继,即分支(孩子结点)。每个结点作为子树的根节点,这些子树互不相交。 2、关于…...
前端大屏适配方案:从设计到实现的全流程指南
引言 随着数据可视化需求的增长,大屏展示项目在前端开发中越来越常见。然而,大屏开发面临独特的挑战: 屏幕分辨率多样:从1080P到4K甚至8K,如何保证清晰度?布局复杂:多图表、多组件如何合理排列…...
10. Hbase Compaction命令
一. 什么是Compaction 在 HBase 中,频繁进行数据插入、更新和删除操作会生成许多小的 HFile,当 HFile 数量增多时,会影响HBase的读写性能。此外,垃圾数据的存在也会增加存储需求。因此,定期进行 Compact操作ÿ…...
完善sql盲注中的其他函数 dnslog+sqlmap外带数据
2. 布尔盲注 布尔盲注是通过观察应用程序的响应(如页面内容、HTTP 状态码等)来判断查询条件是否为真。 <?php // 数据库连接配置 $host localhost; $dbname testdb; $user root; $password password; // 创建数据库连接 $conn new mysqli($ho…...
Python 识别图片和扫描PDF中的文字
目录 工具与设置 Python 识别图片中的文字 Python 识别图片中的文字及其坐标位置 Python 识别扫描PDF中的文字 注意事项 在处理扫描的PDF和图片时,文字信息往往无法直接编辑、搜索或复制,这给信息提取和分析带来了诸多不便。手动录入信息不仅耗时费…...
Java 有哪些锁,他们的区别是什么
Java 锁的分类 Java 中的锁可以从多个维度进行分类: 悲观锁 vs. 乐观锁公平锁 vs. 非公平锁独占锁 (互斥锁) vs. 共享锁 (读写锁)可重入锁 vs. 不可重入锁自旋锁偏向锁 vs. 轻量级锁 vs. 重量级锁 (JVM 锁优化) 1. synchronized 关键字: 类型: 悲观锁…...
CSS实现单行、多行文本溢出显示省略号(…)
在网页设计中,我们常常遇到这样的情况:文本内容太长,无法完全显示在一个固定的区域内。为了让界面看起来更整洁,我们可以使用省略号(…)来表示内容溢出。这不仅能提升用户体验,还能避免内容溢出…...
网络协议/MQTT Paho.MQTT客户端库接口基础知识
开源c版mqtt客户端:https://github.com/eclipse-paho/paho.mqtt.cMQTT 客户端与服务器之间支持的通信协议主要包括: 协议地址格式加密默认端口适用场景服务器地址示例TCPtcp://不加密1883局域网或对安全性要求不高的场景tcp://localhost:1883TLS/SSLssl://加密8883对安全性要…...
VSCode C/C++ 开发环境完整配置及常见问题(自用)
这里主要记录了一些与配置相关的内容。由于网上教程众多,部分解决方法并不能完全契合我遇到的问题,因此我选择以自己偏好的方式,对 VSCode 进行完整的配置,并记录在使用过程中遇到的问题及解决方案。后续内容也会持续更新和完善。…...
深入解析 Go 中的 `io.Pipe()`:实现高效的并发通信
在 Go 语言中,io.Pipe() 是一个强大且灵活的工具,用于在不同的 goroutine 之间实现高效的同步和通信。它通过创建一对连接的 I/O 流,允许数据在管道的两端安全地传递。本文将详细介绍 io.Pipe() 的工作原理、主要特点、使用方法以及一些实际应…...
【Kubernetes】常用命令全解析:从入门到实战(中)
🐇明明跟你说过:个人主页 🏅个人专栏:《Kubernetes航线图:从船长到K8s掌舵者》 🏅 🔖行路有良友,便是天堂🔖 目录 一、引言 1、什么是k8s 2、K8s的核心功能 二、资…...
嵌入式八股文面试题(二)C语言算法
相关概念请查看文章:C语言概念。 1. 如何实现一个简单的内存池? 简单实现: #include <stdio.h> #include <stdlib.h>//内存块 typedef struct MemoryBlock {void *data; // 内存块起始地址struct MemoryBlock *next; // 下一个内…...
Proxmox VE 8.3 qm 方式导入ESXi Linux OVA UEFI模式虚拟机
前言 实现esxi ova uefi 虚拟机导入到pve,Linux UEFI 都支持 创建一个105虚拟机 qm 参数使用参考,以下可以根据自己的实际情况执行调整 esxi 导出虚拟机参考 #vmid (100 - 999999999) vmid=105# qm vm name...
人工智能浪潮下脑力劳动的变革与重塑:挑战、机遇与应对策略
一、引言 1.1 研究背景与意义 近年来,人工智能技术发展迅猛,已成为全球科技领域的焦点。从图像识别、语音识别到自然语言处理,从智能家居、智能交通到智能医疗,人工智能技术的应用几乎涵盖了我们生活的方方面面,给人…...
【线性代数】1行列式
1. 行列式的概念 行列式的符号表示: 行列式的计算结果:一个数 计算模型1:二阶行列式 二阶行列式: 三阶行列式: n阶行列式: 🍎计算行列式 计算模型2:上三角形行列式 上三角形行列式特征:主对角线下皆为0。 上三角形行列式: 化上三角形通用方法:主对角线下,…...
厘米和磅的转换关系
在排版和设计领域,厘米(cm)和磅(pt)都是常用的长度度量单位,它们之间的转换关系基于特定的换算标准,下面为你详细介绍: 基本换算关系 磅是印刷行业常用的长度单位,1英寸…...
vant4 van-list组件的使用
<van-listv-if"joblist && joblist.length > 0"v-model:loading"loading":finished"finished":immediate-check"false"finished-text"没有更多了"load"onLoad">// 加载 const loading ref(fals…...
QT 异步编程之多线程
一、概述 1、在进行桌面应用程序开发的时候,假设应用程序在某些情况下需要处理比较复制的逻辑,如果只有一个线程去处理,就会导致窗口卡顿,无法处理用户的相关操作。这种情况下就需要使用多线程,其中一个线程处理窗口事…...
HCIA项目实践---OSPF的知识和原理总结
9.5 OSPF 9.5.1 从哪些角度评判一个动态路由协议的好坏? (1)选路佳(是否会出环) OSPF 协议采用链路状态算法,通过收集网络拓扑信息来计算最短路径,从根本上避免了路由环路的产生。 (…...
DNS污染:网络世界的“隐形劫持”与防御
在互联网的底层架构中,DNS(域名系统)如同数字世界的“导航员”,将用户输入的域名翻译成机器可读的IP地址。然而,DNS污染(DNS Poisoning)正像一场无声的“地址篡改”危机,威胁着全球网…...
Unity Shader Feature
Shader Feature 设置Keyword //0:Red 1:Green 2:Blue Mat.SetInt(“_Color”,0); 需要在创建时进行设置,运行时不可设置 Shader "Unlit/KeywordEnum" {Properties{[KeywordEnum(Red,Green,Blue)] _Color("Color",int) 0}SubShader{Pass{HLSL…...
Java-数据结构-栈与队列(常考面试题与单调栈)
在上一篇的学习中,我们学习了栈和队列的基本知识,以及它们对应都有哪些方法,在什么应用场景下如何使用,并且还对它们进行了模拟实现,而其实对于栈和队列的相关知识还远不止于此,而今天我们就对栈与队列进行…...
Python Pandas(11):Pandas 数据可视化
数据可视化是数据分析中的重要环节,它帮助我们更好地理解和解释数据的模式、趋势和关系。通过图形、图表等形式,数据可视化将复杂的数字和统计信息转化为易于理解的图像,从而便于做出决策。Pandas 提供了与 Matplotlib 和 Seaborn 等可视化库…...
wordpress模板文件结构超详解
wordpress网站建设中,主题的制作是最为核心的环节。了解模板文件结构是模板制作的第一步,本文所讲的模板文件结构包括两部分,一是指以文件名为概念的文件结构,二是指文件内容的代码结构。 一、如何使模板文件起作用 ↑ wordpres…...
大脑神经网络与机器神经网络的区别
大脑神经网络(生物神经网络)与机器神经网络(人工神经网络,ANN)虽然名称相似,但在结构、功能、学习机制等方面存在显著差异。以下是两者的主要区别: 1. 基础结构与组成 大脑神经网络: 由 生物神经元(约860亿个)通过突触连接形成动态网络。 神经元通过电化学信号(动作…...
【H5自适应】高端科技类pbootcms网站模板 – 三级栏目、下载与招聘功能支持
(H5自适应)高端大气的科技类pbootcms网站模板 带三级栏目、下载和招聘功能 后台地址:您的域名/admin.php 后台账号:admin 后台密码:123456 为了提升系统安全,请将后台文件admin.php的文件名修改一下。修改之后,后台…...
SQL-leetcode—1661. 每台机器的进程平均运行时间
1661. 每台机器的进程平均运行时间 表: Activity ----------------------- | Column Name | Type | ----------------------- | machine_id | int | | process_id | int | | activity_type | enum | | timestamp | float | ----------------------- 该表展示了一家工厂网站的…...
C++ Primer 跳转语句
欢迎阅读我的 【CPrimer】专栏 专栏简介:本专栏主要面向C初学者,解释C的一些基本概念和基础语言特性,涉及C标准库的用法,面向对象特性,泛型特性高级用法。通过使用标准库中定义的抽象设施,使你更加适应高级…...
清华大学:DeepSeek 如何赋能职场应用(35 页 PDF)
原来已经分享过清华大学的 DeepSeek:从入门到精通(100页PDF) 现在又来第二弹:《DeepSeek 如何赋能职场应用?从提示语技巧到多场景应用》 PDF里介绍了 DeepSeek 这一人工智能工具及其在职场中的应用,从基础…...
idea 错误: 找不到或无法加载主类 @C:\Users\admin\AppData\Local\Temp\idea_arg_file1549212448
idea 错误: 找不到或无法加载主类 C:\Users\admin\AppData\Local\Temp\idea_arg_file1549212448 该错误往往和左下角爱弹出的如下提示是一个意思 Error running ‘PayV3Test1.testTransferBatchesBatchId’ Error running PayV3Test1.testTransferBatchesBatchId. Command lin…...
开发指南098-logback-spring.xml说明
可执行的工程src\main\resources目录有logback-spring.xml文件用于配置日志。配置日志有些容易犯晕的地方,这里列出: 1、<logger>标签的优先级高于<root>标签:所以,如果<logger>标签指定了某个具体的包或类的…...
【SpringBoot3.x+】slf4j-log4j12依赖引入打印日志报错的两种解决方法
最开始引入了1.7.5版本的slf4j-log4j依赖包,但是控制台不报错也不显示日志 在https://mvnrepository.com/找到最新的2.0.16版本之后出现报错: 进入提示的slf4j网站中可以找到从2.0.0版本开始,slf4j-log4j已经被slf4j-reload4j取代࿱…...
【STM32】H743的以太网MAC控制器的一个特殊功能
调试743的MAC,翻阅手册的时候,发现了一个有意思的功能 混杂模式 H743的MAC控制器,可以设置为混杂模式,这就意味着它可以做一些网络监控的应用,譬如连接具备端口镜像功能的交换机,然后直接代替PC实现网络数据…...
Java LinkedList(单列集合)
LinkedList 是 Java 中实现了 List 接口的一个类,它属于 java.util 包。与 ArrayList 不同,LinkedList 是基于双向链表实现的,适合于频繁进行插入和删除操作的场景。 1. LinkedList 的基本特性 基于链表实现:LinkedList 使用双向…...
docker compose快速部署kafka-connect集群
先部署kafka集群,启动 参考:docker compose部署kafka集群-CSDN博客 创建timezone文件,内容填写Asia/Shanghai 再部署kafka-connect集群 networks: net: external: true services: kafka-connect1: restart: always image:…...
docker 部署nginx,nginx 504
遇到问题 原因: 因为用的docker 部署nginx, docker 应用与服务之间的端口未开放,导致访问不到服务。...
RealClip正式发布:重新定义轻量化数字内容交互体验
在移动互联网流量红利逐渐见顶的当下,用户对即时性、碎片化娱乐与交互体验的需求持续攀升。轻量化小游戏、VR互动、数字孪生、工业仿真等内容形态迅速崛起,但开发者却面临两大核心矛盾:如何将高性能互动内容轻量化嵌入现有应用中?…...
SQLMesh系列教程-2:SQLMesh入门项目实战(上篇)
假设你已经了解SQLMesh是什么,以及其他应用场景。如果没有,我建议你先阅读《SQLMesh系列教程-1:数据工程师的高效利器-SQLMesh》。 在本文中,我们将完成一个小项目或教程,以帮助你开始使用SQLMesh。你可以选择一步一步…...
把 DeepSeek1.5b 部署在显卡小于4G的电脑上
这里写自定义目录标题 介绍准备安装 Ollama查看CUDA需要版本安装CudaToolkit检查Cuda是否装好设置Ollama环境变量验证是否跑在GPU上ollama如何导入本地下载的模型安装及配置docker安装open-webui启动open-webui开始对话 调整gpu精度 介绍 Deepseek1.5b能够运行在只用cpu和gpu内…...
#渗透测试#批量漏洞挖掘#29网课交单平台 SQL注入
免责声明 本教程仅为合法的教学目的而准备,严禁用于任何形式的违法犯罪活动及其他商业行为,在使用本教程前,您应确保该行为符合当地的法律法规,继续阅读即表示您需自行承担所有操作的后果,如有异议,请立即停止本文章读。 目录 1. 漏洞原理 2. 漏洞定位 3. 攻击验证示…...
试试DeepSeek写prompt+stable diffusion生成漫画
#deepseek #stable diffusion 模型:dreamshaperXL_v21TurboDPMSDE.safetensors 一、情节拟定 漫画情节由deepseek自编自导,画幅为四张。 Prompt 1: 魔法觉醒 "一个平凡的少年在阁楼发现一本古老的魔法书,书页散发着微弱的蓝光。画…...
java面试题之 int和Integer的区别
int和Integer的区别 1、Integer是int的包装类,int则是java的一种基本数据类型 2、Integer变量必须实例化后才能使用,而int变量不需要 3、Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;…...
Spring Bean的生命周期
1、对象实例化 2、属性设置 3、初始化 4、使用 5、销毁 示例代码如下: import org.springframework.stereotype.Component;Component public class SpringBeanA {public SpringBeanA() {System.out.println("第一步:实例化(spring对象&#x…...
Vue 发送 PDF 文件链接到 WinForm 程序进行打印
Vue 发送 PDF 文件链接到 WinForm 程序进行打印的完整流程如下: 1. Vue 端 Vue 通过 fetch 或 axios 发送 PDF 文件的 URL 给 WinForms 程序(WinForms 需要开启一个本地 API)。 <template><div><button click"sendPri…...
Vue笔记(十)
一、AI的基本认知 二、ChatGPT的基本使用 三、AI插件--Copilot入门 1.Copilot是由OpenAI和GitHub合作开发的AI编程辅助插件,基于大量代码训练,能根据上下文自动生成代码建议。 2.安装与配置:在常用代码编辑器(如Visual Studio Cod…...
使用LangChainV3.0加载PDF文件并进行总结
LangChain目前已经更新到了V3版本,之前一直使用的V1版本,有很多方法都需要自己去封装,这次重新看了V3版本的API文档,很多方法都十分便利,调用方法简单明了十分方便,下面就来展示下这次对于PDF文件加载的优化…...
玩转大语言模型——使用Kiln AI可视化环境进行大语言模型微调数据合成
系列文章目录 玩转大语言模型——使用langchain和Ollama本地部署大语言模型 玩转大语言模型——三分钟教你用langchain提示词工程获得猫娘女友 玩转大语言模型——ollama导入huggingface下载的模型 玩转大语言模型——langchain调用ollama视觉多模态语言模型 玩转大语言模型—…...
EasyRTC智能硬件:小体积,大能量,开启音视频互动新体验
在万物互联的时代,智能硬件正以前所未有的速度融入我们的生活。然而,受限于硬件性能和网络环境,许多智能硬件在音视频互动体验上仍存在延迟高、卡顿、回声等问题,严重影响了用户的使用体验。 EasyRTC智能硬件,凭借其强…...
vue知识点5
1.如何让组件里的样式与其他组件互相不干扰 scope范围的意思 <style scope> </style> 2.vue的生命周期 创建 挂载 更新 销毁 3.vue的四个生命周期详解 创建beforeCreate,created 挂载 beforeMount,mounted 更新 beforeUpdate,updated 销毁 beforeDest…...
qt的QSizePolicy的使用
使用 QSizePolicy 设置控件的伸缩因子 在 Qt 中,QSizePolicy 控制 控件如何在布局中伸缩。如果想要影响控件的大小调整行为,可以通过 QSizePolicy::setHorizontalStretch() 和 QSizePolicy::setVerticalStretch() 设置伸缩因子。 基本用法 假设我们有一个…...