二叉树基础
树的基本算法
树的搜索算法从搜索的形式上来看,可以分为深度优先搜索和广度优先搜索。
1. 深度优先搜索
深度优先搜索算法从遍历根节点的次序来看,可以分为先序遍历、中序遍历和后序遍历算法。
1.1 先序遍历
// 先序遍历递归实现
public void recursivePreOrder(TreeNode root) {
if (root == null)
return;
visit(root);
recursivePreOrder(root.left);
recursivePreOrder(root.right);
}
// 先序遍历非递归实现,方式一
public void preOrder(TreeNode root) {
if (root == null)
return;
Deque<TreeNode> stack = new Deque<>();
stack.push(root);
while (!stack.empty()) {
// root为栈顶元素
root = stack.pop();
visit(root);
// 先入栈右孩子节点,再入栈左孩子节点,才能保证出栈遍历的顺序是先左后右
if (root.right != null)
stack.push(root.right);
if (root.left != null)
stack.push(root.left);
}
}
// 先序遍历非递归实现,方式二
public void preOrder(TreeNode root) {
if (root == null)
return;
Deque<TreeNode> stack = new Deque<>();
while (!stack.empty() || root != null) {
// 开始时,从根节点开始,并循环向左下遍历,并入栈
// 先访问再入栈,因为先序遍历,后续入栈的节点还需访问其右子节点
while (root != null) {
visit(root);
stack.push(root);
root = root.left;
}
// 如果root空了,而栈未空,则说明遍历到的节点没有左孩子,出栈检查其是否有右子节点
// 如果没有右子节点,则表示遍历到叶子节点,出栈,并检查上一层的右孩子...
root = stack.pop();
root = root.right;
/* 此处将while改为if,while外部代码放入else中也可以
if (root != null) {
visit(root);
stack.push(root);
root = root.left;
} else {
root = stack.pop();
root = root.right;
}
*/
}
}
1.2 中序遍历
// 中序遍历递归实现
public void recursivePreOrder(TreeNode root) {
if (root == null)
return;
recursivePreOrder(root.left);
visit(root);
recursivePreOrder(root.right);
}
// 中序遍历非递归实现
public void inOrder(TreeNode root) {
if (root == null)
return;
Deque<TreeNode> stack = new Deque<>();
while (!stack.empty() || root != null) {
// 先将左下方的节点都入栈
while (root != null) {
stack.push(root);
root = root.left;
}
root = stack.pop();
visit(root);
root = root.right;
/* 此处将while改为if,while外部代码放入else中也可以
if (root != null) {
stack.push(root);
root = root.left;
} else {
root = stack.pop();
visit(root);
root = root.right;
}
*/
}
}
// Morris中序遍历,可以将中序遍历的非递归算法的空间复杂度降低为O(1)
/*
Morris中序遍历的思想:
在进行中序遍历时,一定先遍历左子树,然后遍历当前节点,最后遍历右子树。在常规方法中,用递归回溯或者是栈来保证遍历完左子树的方式,使得左子树遍历结束后可以再回到当前节点,但这需要额外的空间代价。
因此Morris的思想就是遍历完左子树回到当前节点时,空间复杂度可以为常数级。
在遍历时,目的是让当前的节点是在遍历完当前点的前驱之后被遍历,可以考虑修改它的前驱节点的right指针。
1. 当前节点的前驱节点的right指针可能本来就指向当前节点(前驱是当前节点的父节点)
2. 当前节点的前驱节点是当前节点左子树最右下的节点。
如果是第 2 种情况,则希望遍历完前驱节点后,可以回到当前节点,因此将前驱节点的right域指向当前节点。
Morris中序遍历最重要的步骤就是,寻找当前节点的前驱节点,使前驱节点的right指向当前节点,并且此中序遍历寻找下一个点始终是通过转移到right指针指向的节点来完成的。
1. 如果当前节点没有左子树,则遍历这个点,然后跳转到当前节点的右子树。(相当于右子树的根节点与当前节点是情况1,当前节点的前驱节点就是当前节点的父节点)
2. 如果当前节点有左子树,那么它的前驱节点一定在左子树上,我们可以在左子树上一直向右行走,找到当前点的前驱节点。
- 如果前驱节点没有右子树,就将前驱节点的right指针指向当前节点。这一步是为了在遍历完前驱节点后能找到前驱节点的后继,也就是当前节点。
- 如果前驱节点的右子树为当前节点(前驱节点在当前节点左子树的最右下角的情况),说明前驱节点已经被遍历过并被修改了right指针,这个时候重新将前驱的右孩子设置为空,遍历当前的点,然后跳转到当前节点的右子树。
具体算法流程:
1. 如果x无左子节点,先访问x节点,再访问x的右子节点,即x=x.right。
2. 如果x有左子节点,则找到x左子树上最右的节点(即左子树中序遍历的最后一个节点,x在中序遍历中的前驱节点),记为 predecessor。根据predecessor的右子节点是否为空,进行如下操作。
- 如果predecessor的右子节点为空,则将其右子节点指向x,然后访问x的左子节点,即x=x.left。
- 如果predecessor的右子节点不为空,则此时其右子节点是指向x的,说明已经遍历完x的左子树,将predecessor的右子域置空,将x的值加入答案数组,然后访问x的右子节点,即x=x.right。
3. 重复上述操作,直至访问完整棵树。
*/
public void inorderTraversal(TreeNode root) {
TreeNode predecessor = null;
while (root != null) {
// 为有左子树的前驱节点寻找前驱
if (root.left != null) {
// predecessor 节点就是当前root节点向左走一步,然后一直向右走至无法走为止
// 目的是找到当前节点的前驱节点
predecessor = root.left;
while (predecessor.right != null && predecessor.right != root) {
// 判断predecessor.right != root是为了防止当前前驱节点已经更新过right域
// 如果不判断,则会导致predecessor可能会抵达当前节点的右子树
predecessor = predecessor.right;
}
// 让predecessor的右指针指向root,继续遍历左子树
if (predecessor.right == null) {
predecessor.right = root;
root = root.left; // 继续遍历左子树
} else { // 说明左子树已经访问完了,我们需要断开前驱节点的right域与当前节点的链接
// 左子树最右下节点的后继只有可能是当前节点,因此当predecessor.right!=null时,
// 只有可能是又遍历回当前节点了,因此此时遍历当前节点,且断开前驱节点的后继
visit(root);
predecessor.right = null;
root = root.right; // 继续遍历右子树
}
}
// 如果没有左孩子,则直接访问当前节点,并遍历子树
else {
visit(root);
root = root.right;
}
}
return;
}
1.3 后序优先
// 后序遍历递归实现
public void recursivePreOrder(TreeNode root) {
if (root == null)
return;
recursivePreOrder(root.left);
recursivePreOrder(root.right);
visit(root);
}
// 后续遍历非递归实现
public void postOrderTraverse(TreeNode root) {
TreeNode cur, pre = null;
// cur是当前节点,pre是刚刚访问过的上一个节点
Deque<TreeNode> stack = new Deque<>();
stack.push(root);
// stack存储的是未访问的
while (!stack.empty()) {
cur = stack.peek();
// 满足两个条件之一:1.当前节点的左右子节点都为空;2.上一个访问的节点存在,并访问的是当前节点的左右子节点其中之一
if ((cur.left == null && cur.right == null) || (pre != null && (pre == cur.left || pre == cur.right))) {
// 访问此节点,将此节点出栈,更新已访问的上一个节点
visit(cur);
stack.pop();
pre = cur;
} else { // 此节点存在左右节点,且都不是刚刚访问过的
if (cur.right != null)
stack.push(cur.right);
if (cur.left != null)
stack.push(cur.left);
}
}
/*
1.根节点入栈,然后根节点存在左右子树,且pre为空,则此时以此将右节点和左节点入栈,栈:[root, root.right, root.left]
2.当前cur=root.left,假如此节点是叶子节点,则访问root.left,pre=root.left,且此节点出栈,则栈:[root, root.right]
3.假设root.right有子节点,则子节点先入栈
4....如此循环
*/
}
public void postOrder_1(TreeNode root) {
if (root == null)
return;
Deque<TreeNode> stack = new Deque<>();
TreeNode pre = root;
while (!stack.empty() || root != null) {
// 将以此root为根的子树的左列全部元素入栈
while (root != null) {
stack.push(root);
root = root.left;
}
// 现在的栈顶元素可能是叶节点或者某最下端子树的根节点,赋予root
root = stack.peek().right;
if (root == null || root == pre) {
//若栈顶节点的右节点为空或者已经visit过,则按顺序应该访问栈顶节点
root = stack.pop();
visit(root);
//pre用来标记已经visit过此节点
pre = root;
root = null;
}
}
}
public void postOrder_2(TreeNode root) {
if (root == null)
return;
Deque<TreeNode> stack = new Deque<>();
TreeNode pre = root; // pre表示
while (root != null) {
// 从root节点开始,依此将左侧满足左子节点不为空的节点入栈
while (root.left != null) {
stack.push(root);
root = root.left;
} // 经过此步之后,root指向原root为根的子树最左一边最下的叶子节点
// 只有满足遍历到的节点不为空且其右子节点为空或刚访问过,才访问root,更新刚刚访问过的节点
while (root != null && (root.right == null || root.right == pre)) {
visit(root);
pre = root;
// 更新root
if (stack.empty())
return;
root = stack.pop();
}
// 经过上述两个while,说明此节点是以原先root为根的子树的最左下叶子节点,且该节点包括右节点且右子节点并不是刚刚访问的,因此还不到访问这个节点,先入栈,然后遍历此节点的右子节点
stack.push(root);
root = root.right;
}
}
// 双栈法实现后序遍历的非递归方法
public void postOrder_3(TreeNode root) {
if (root == null)
return;
Deque<TreeNode> stack = new Deque<TreeNode>();
Deque<TreeNode> result = new Deque<TreeNode>(); // result同
// 将所有的节点按照根、右、左的
while (!stack.empty() || root != null) {
while (root != null) {
stack.push(root);
result.push(root);
root = root.right;
}
if (!stack.empty())
// 这里的pop的目的之一是为了达到stack.empty()而跳出循环的作用,因此不能只用一个栈
root = stack.pop().left;
}
while (!result.empty()) {
root = result.pop();
visit(root);
}
}
2. 广度优先搜索
层次遍历
// 层次遍历的非递归实现
// 令辅助队列中留存的节点都在树中的同一层。
public void levelOrder(TreeNode root) {
if (root == null)
return;
Deque<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
while (!queue.isEmpty()) {
root = queue.poll();
if (root.left != null)
queue.offer(root.left);
if (root.right != null)
queue.offer(root.right);
visit(root); // 这个visit可以在while中queue.poll之后的任意地方
}
}
// 广度优先遍历,就是层次遍历然后按结果保存
public List<Integer> BFS(TreeNode root){
Deque<TreeNode> queue = new ArrayDeque<>();
List<Integer> list = new ArrayList<>();
if(root == null)
return list;
queue.offer(root);
while (!queue.isEmpty()){
root = queue.poll();
if(root.left != null)
queue.offer(root.left);
if(root.right != null)
queue.offer(root.right);
list.add(root.val);
}
return list;
}
// 层次遍历的递归实现
public void recurLevelOrder(TreeNode root) {
if (root == null)
return;
int depth = maxDepth(root);
// 从上到下按层访问树的节点。如果要倒序访问只需修改此处顺序
for (int i = 1; i <= depth; i++)
visitNodeAtDepth(root, i);
}
public void visitNodeAtDepth(TreeNode root, int depth) { // 按特定层次访问的节点
if (root == null || depth < 1)
return;
// 因为要按顺序访问(打印),所以要规定必须到某一层才能visit,即递归过程中depth减为1才输出
if (depth == 1) {
visit(root);
return;
}
// 每次都要遍历判定depth之上的所有层,只有到达depth层才visit
visitNodeAtDepth(root.left, depth - 1);
visitNodeAtDepth(root.right, depth - 1);
}
// 计算树的深度方式一
public int maxDepth(TreeNode root) { // 计算树的深度,从1开始
if (root == null)
return 0;
return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
树的一些性质
对于任意一颗子树,其前序遍历的形式总是 [ 根节点 , [ 左子树的前序遍历结果 , 右子树的前序遍历结果 ] ],中序遍历的形式总是 [ [ 左子树的前序遍历结果 ] , 根节点 , [ 右子树的前序遍历结果 ] ],后序遍历的形式总是 [ [ 左子树的后序遍历结果 ] , [ 右子树的后序遍历结果 ] , 根节点 ]。
树的解题方法总结
1. 计算树的深度
计算树的最大深度
// 计算树的深度方式一
public int maxDepth(TreeNode root) { // 计算树的深度,从1开始
if (root == null)
return 0;
return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
// 计算树的深度方式二
public int getTreeDepth(Node node) {
if (node.lChild == null && node.rChild == null) {
return 1; // 只有一个节点,深度为1
}
int left = 0, right = 0; // left、right表示左右子树的深度
// 递归计算左右子树的深度
if (node.lChild != null) {
left = getTreeDepth(node.lChild);
}
if (node.rChild != null) {
right = getTreeDepth(node.rChild);
}
// 左右子树的最大高度+1(表示根节点)
return left > right ? left + 1 : right + 1;
}
计算树的最小深度
由于树的最小深度是指根节点到叶子节点的最小深度,而直接使用最大深度类似的方式求出的可能是只有单子节点的节点的空域所在的深度,并不是树的叶子节点的深度。
// 最小深度方式一
// 需要判断遍历的节点是空节点则返回 0,是叶子节点则返回 1,否则是有子节点的情形,只递归遍历非空的分支。
public int minDepth(TreeNode root) {
if (root == null) // 空树,实质上只用于判断根节点,后续不会判断
return 0;
if (root.left == null && root.right == null) // 当前节点为叶子节点
return 1;
int minSubDepth = Integer.MAX_VALUE;
// 因为当左右子树都为空时,此节点才是叶子节点,才可以被计算在最小深度的计算结果内
// 此时左右子树不全为空,只计算不为空的子树的高度
if (root.left != null)
minSubDepth = Math.min(minDepth(root.left), minSubDepth);
if (root.right != null)
minSubDepth = Math.min(minDepth(root.right), minSubDepth);
return minSubDepth + 1;
}
// 最小深度方式二
public int minDepth(TreeNode root) {
if (root == null)
return 0;
Deque<TreeNode> deque = new LinkedList<>();
deque.offer(root);
int depth = 0; // 记录当前遍历到的深度
while (!deque.isEmpty()) {
++depth;
int levelNum = deque.size(); // 表示树的当前层节点的个数,也即队列的前levelNum个节点
TreeNode node;
while (levelNum > 0) {
// node作为当前子树的根节点
node = deque.poll();
// 当节点的左右子节点都为空,则遇到了层次遍历的第一个叶子节点,此节点的深度一定是树的最小深度
if (node.left == null && node.right == null)
return depth;
// node的left和right不全是null
if (node.left != null)
deque.offer(node.left);
if (node.right != null)
deque.offer(node.right);
--levelNum;
}
}
return depth;
}
2. 按层输出树的每一层节点值
// 按层输出节点值方式一
public void levelOrder(TreeNode root) {
if (root == null)
return;
Deque<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
while (!queue.isEmpty()) {
int levelNum = queue.size(); // levelNum表示这一层有多少节点
for (int i = 0; i < levelNum; i++) { // 按数量将这一层的节点进行出队并访问
root = queue.poll();
if (root.left != null)
queue.offer(root.left);
if (root.right != null)
queue.offer(root.right);
visit(root); // System.out.print(root.val + " ");
}
System.out.println();
}
}
// 按层输出节点值方式二
public void levelOrder(TreeNode root) {
if (root == null)
return;
Deque<TreeNode> queue = new ArrayDeque<>();
// lineUp表示遍历的此一层的节点个数,lineDown是即将遍历的下一层的节点个数
int lineCur = 1, lineNext = 0;
queue.offer(root);
while (!queue.isEmpty()) {
root = queue.poll();
visit(root); // System.out.print(root.val + " ");
// 更新下一行的节点的值
if (root.left != null){
queue.offer(root.left);
lineNext++;
}
if (root.right != null){
queue.offer(root.right);
lineNext++;
}
if (--lineUp == 0) { // lineUp在每判断一次时都会先减
// 此一层的节点都遍历输出完毕,需要换层且换行输出
lineCur = lineNext;
lineNext = 0;
System.out.println();
}
}
}
3. 递归的倒序层次遍历
public List<List<Integer>> recursiveLevelOrderBottom(TreeNode root) {
// 用来保存层次遍历的倒序(从深层到浅层)
LinkedList<List<Integer>> lists = new LinkedList<List<>>();
addToList(lists, root, 1);
return lists;
}
//将depth层的p节点保存至list
public void addToList(LinkedList<List<Integer>> lists, TreeNode root, int depth) {
if (root == null)
return;
if (lists.size() < depth) // 如果到了新的一层,则lists中新增一个空的list
lists.addFirst(new LinkedList<Integer>());
// 由于不用输出只是保存,可以使用get控制保存在哪一层,所以不用将同一层的节点一同放入
// 由于是逆序,因此深度为depth的节点放入第lists.size()-depth个列表中
lists.get(lists.size() - depth).add(root.val); // 将节点值加入到的list中
// 分别在lists中保存下一层
addToList(lists, root.left, depth + 1);
addToList(lists, root.right, depth + 1);
}
// 如果是要打印,则需要使用一个辅助栈,栈中每一个元素都是保存每一层的节点的List,
// 将节点全部遍历完成之后,再按出栈的顺序打印