leetcode二叉树相关操作的总结

时间:2021-07-24 17:32:14

做了这么多关于二叉树的题,发现一些题目的本质来来回回就是那些操作,所以觉得需要进行总结,可以让之后的思路更明晰。


二叉树的操作,最普遍的就是递归,和前中后序遍历,层序遍历。前中后序遍历可以用递归也可以用自己的栈代替递归栈。层序遍历用一个队列辅助,每一层都是上一层所有节点的孩子。在这个过程中也可以记录每一层包含的节点及个数。


递归的解决问题的话,有三个要素:首先你要确定他们之间有递归关系,大规模的问题由小规模的问题通过一定的转换关系解决。递归不能无限递归下去,所以必须有个结束的条件。对于树来说一般是到达叶子节点;再有就是递归可以有一些退出递归的条件。


递归的解法一般都可以用非递归的方式代替。因为递归相当于系统调用栈进行了临时存储。那么我们就可以自己设置一个栈来实现系统栈的功能代替之。只是一些时候会比较麻烦。


如果是对整个树操作,其实就相当于对树的节点进行遍历,那么你的操作无非就对应了前序中序后序方式。对于这些方式其实访问节点的顺序也就确定下来了。visit(root.left) root.val visit(root.right)顺序就是二叉搜索树的节点值得从小到大的顺序。visit(root.right) root.val visit(root.left)就是反过来从大到小的顺序。

后序遍历大体是自底向上的,对于每一个子树来说,根的左子树自底向上,然后根的右子树自底向上。访问了该子树根以后就往上走。后序遍历访问每个节点时,要么它的右孩子为null,要么它的右孩子是刚刚访问的上一个节点。

先把最左边的一列按顺序入栈 然后开始循环 如果栈不为空,出栈,如果该节点的右孩子为空,或上一个访问的节点,那么就访问该节点。如果不是,就需要先后序访问它的右孩子,先把它再压入栈,然后r->r.right,然后再循环做孩子入栈 之后再递归循环


先序遍历相当于把最左边的一列先按顺序访问后入栈,然后出栈r->r.right找到他们的右孩子,右孩子不入栈,之后再用同样的方式访问它的右子树。

中序遍历类似于先序遍历,只是它把最左边的一列按顺序入栈,然后出栈访问,再r->r.right找到他们的右孩子,右孩子不入栈,之后再用同样的方式访问它的右子树。找到右孩子进入下一次循环,相当于对右孩子进行相同的中序遍历,下次循环,如果节点为空,没有右孩子,相当于上一个子树已经访问完。就出栈进入上一层,如果栈为空,说明这是最后一个右子树被访问完了。


关于二叉树非递归遍历方式点击打开链接写的比较详细


1.判断两个二叉树是否相同

二叉树是由根和左右两个子二叉树构成,所以判断两个二叉树是否相同就看他们根值是否相同,左子树和右子树是否同时相同。判断左右子树是否相同就又是同样的逻辑了。所以这就是典型的递归判断问题。递归的边界就是判断到叶子节点。叶子节点的特征就是root.left==null&&root.right==null;退出递归的条件就是两个树的根值不相同。代码如下:

public boolean isSameTree(TreeNode p, TreeNode q) {
if(p==null&&q==null)return true;
if((p==null)||(q==null))return false;
if(p.val==q.val){
return isSameTree(p.left,q.left)&&isSameTree(p.right,q.right);
}
return false;
}
非递归代码如下:你需要把两个节点同时压栈,因为操作时需要对应。java的话就用两个栈实现同时栈操作

public boolean isSameTree(TreeNode p, TreeNode q) {
Stack<TreeNode> stack_p = new Stack <> ();
Stack<TreeNode> stack_q = new Stack <> ();
if (p != null) stack_p.push( p ) ;
if (q != null) stack_q.push( q ) ;
while (!stack_p.isEmpty() && !stack_q.isEmpty()) {
TreeNode pn = stack_p.pop() ;
TreeNode qn = stack_q.pop() ;
if (pn.val != qn.val) return false ;
if (pn.right != null) stack_p.push(pn.right) ;
if (qn.right != null) stack_q.push(qn.right) ;
if (stack_p.size() != stack_q.size()) return false ;
if (pn.left != null) stack_p.push(pn.left) ;
if (qn.left != null) stack_q.push(qn.left) ;
if (stack_p.size() != stack_q.size()) return false ;
}
return stack_p.size() == stack_q.size() ;
}
2.判断一个二叉树是否对称

一个二叉树是对称的就要满足如下性质:它的左右子树是镜面对称,即中心对称。判断两个个树是否为镜面对称,则需要满足:根相同,左边的左子树跟右边的右子树镜面对称左边的右子树跟右边的左子树镜面对称。即以根垂直的线为轴线,轴对称。

public boolean isSymmetric(TreeNode root) {
if(root == null)return true;
return isMirror(root.left,root.right);
}
public boolean isMirror(TreeNode p,TreeNode q){
if(p==null&&q==null)return true;
if(p==null||q==null)return false;
if(p.val!=q.val)return false;
return isMirror(p.left,q.right)&&isMirror(p.right,q.left);
}
非递归方式:

public boolean isSymmetric(TreeNode root) {
if(root==null) return true;

Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode left, right;
if(root.left!=null){
if(root.right==null) return false;
stack.push(root.left);
stack.push(root.right);
}
else if(root.right!=null){
return false;
}

while(!stack.empty()){
if(stack.size()%2!=0) return false;
right = stack.pop();
left = stack.pop();
if(right.val!=left.val) return false;

if(left.left!=null){
if(right.right==null) return false;
stack.push(left.left);
stack.push(right.right);
}
else if(right.right!=null){
return false;
}

if(left.right!=null){
if(right.left==null) return false;
stack.push(left.right);
stack.push(right.left);
}
else if(right.left!=null){
return false;
}
}

return true;
}

3.求二叉树的最大深度(即深度)

该树的深度就是max(左子树,右子树)+1;递归即可,边界是叶子节点。

public int maxDepth(TreeNode root) {
if(root==null){
return 0;
}
return 1+Math.max(maxDepth(root.left),maxDepth(root.right));
}
非递归方式,dfs和bfs

dfs深度优先,你需要知道每一次栈操作时对应的深度,所以就用额外一个栈进行同步栈操作记录

public int maxDepth(TreeNode root) {
if(root == null) {
return 0;
}

Stack<TreeNode> stack = new Stack<>();
Stack<Integer> value = new Stack<>();
stack.push(root);
value.push(1);
int max = 0;
while(!stack.isEmpty()) {
TreeNode node = stack.pop();
int temp = value.pop();
max = Math.max(temp, max);
if(node.left != null) {
stack.push(node.left);
value.push(temp+1);
}
if(node.right != null) {
stack.push(node.right);
value.push(temp+1);
}
}
return max;
}
bfs
public int maxDepth(TreeNode root) {
if(root == null) {
return 0;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int count = 0;
while(!queue.isEmpty()) {
int size = queue.size();
while(size-- > 0) {
TreeNode node = queue.poll();
if(node.left != null) {
queue.offer(node.left);
}
if(node.right != null) {
queue.offer(node.right);
}
}
count++;
}
return count;
}
4.将一个有序数组转换为二叉搜索树BST

二叉搜索树的左子树中所有值比根小,右子树中所有值比根大。所以首先我们找到这个数组的中间值mid,它就是根。然后数组的左半部分构成左子树,右半部分构成右子树。如此递归下去,递归的边界就是当数组被分没的时候

public TreeNode sortedArrayToBST(int[] nums) {
return sortArray(nums,0,nums.length-1);
}
public TreeNode sortArray(int[] nums,int s,int e){
if(s > e)return null;
int mid = s + (e-s)/2;
TreeNode t = new TreeNode(nums[mid]);
t.left = sortArray(nums,s,mid-1);
t.right = sortArray(nums,mid+1,e);
return t;
}
非递归 压入的节点要跟所属数组的位置匹配上,所以用另外两个栈记录对应数组的首尾位置

public TreeNode sortedArrayToBST(int[] nums) {       
int len = nums.length;
if ( len == 0 ) { return null; }
TreeNode head = new TreeNode(0);
Stack<TreeNode> nodeStack = new Stack<TreeNode>() ;//要被对应序号创建的节点压入此栈
nodeStack.push(head);
Stack<Integer> leftIndexStack = new Stack<Integer>();
leftIndexStack.push(0);
Stack<Integer> rightIndexStack = new Stack<Integer>();
rightIndexStack.push(len-1);
while ( !nodeStack.isEmpty() ) {
TreeNode currNode = nodeStack.pop();
int left = leftIndexStack.pop();
int right = rightIndexStack.pop();
int mid = left + (right-left)/2; // avoid overflow
currNode.val = nums[mid];
if ( left <= mid-1 ) {
currNode.left = new TreeNode(0);
nodeStack.push(currNode.left);
leftIndexStack.push(left);
rightIndexStack.push(mid-1);
}
if ( mid+1 <= right ) {
currNode.right = new TreeNode(0);
nodeStack.push(currNode.right);
leftIndexStack.push(mid+1);
rightIndexStack.push(right);
}
}
return head;
}

5.将有序链表转为二叉搜索树

首先类似数组转换二叉树的思路,我们需要每次都找到链表的首节点,中间节点和尾节点。如何快捷的找到呢

while(fast!=null&&fast.next!=null){
fast = fast.next.next;
slow = slow.next;
}
fast每次跳两跳,slow每次跳一跳,所以当fast跳到最后的时候,slow刚好走了一半,也就是到达中间点,由于链表节点个数单双情况,fast最终指向最后一个节点或者null;由于需要递归的创建之后的节点,还需要mid-1,mid+1的位置,所以用pre记录slow的前一个节点,然后进行递归(head,pre)  slow (slow.next,fast),但是这里注意,因为你的判断条件在每一轮的fast.next上,所以在递归之前应该把链表断开。递归的结束边界是当最后一个节点创造树节点以后,fast=head,slow=head,fast.next==null;所以之后pre==null,所以head==null,再下一次递归head==null 结束return

public TreeNode sortedListToBST(ListNode head) {
ListNode tail = head;
if(head==null) return null;
while(tail.next!=null){
tail = tail.next;
}
return go(head,tail);
}
public TreeNode go(ListNode head,ListNode tail){
if(head==null)return null;
if(head==tail)return new TreeNode(head.val);
ListNode fast = head;
ListNode slow = head;
ListNode pre = null;
while(fast!=null&&fast.next!=null){
fast = fast.next.next;
pre = slow;
slow = slow.next;
}
if(pre!=null){
pre.next = null;
}else{
head = null;
}
TreeNode t = new TreeNode(slow.val);
t.left = go(head,pre);
t.right = go(slow.next,fast);
return t;
}
上述的创建过程相当于前序遍历创建节点。

如果我们知道一个树的总节点数n,已知它是高度平衡的那么这颗树是不是形状已经被确定下来了.用(0,n)(0,n/2)(n/2+1,n)...进行创建的数组形状就是最终的形状,只是需要再填入每个节点的值。那么我们用中序遍历

创建节点得顺序就相当于中序遍历那个树的顺序,而刚好中序遍历搜索二叉树的数值顺序就是升序1,2,3,4,5,6刚好符合链表的顺序,那就用对应链表节点值创建树节点吧

private ListNode node;

public TreeNode sortedListToBST(ListNode head) {
if(head == null){
return null;
}

int size = 0;
ListNode runner = head;
node = head;

while(runner != null){
runner = runner.next;
size ++;
}

return inorder(0, size - 1);
}

public TreeNode inorder(int start, int end){
if(start > end){
return null;
}

int mid = start + (end - start) / 2;
TreeNode left = inorder(start, mid - 1);

TreeNode treenode = new TreeNode(node.val);
treenode.left = left;
node = node.next;

TreeNode right = inorder(mid + 1, end);
treenode.right = right;

return treenode;
}


6.递归的方式实现层序遍历

public List<List<Integer>> levelOrderBottom(TreeNode root) {
List<List<Integer>> res = new ArrayList();
levelGo(res,root,0);
return res;
}
public void levelGo(List<List<Integer>> list,TreeNode t,int level){
if(t==null) return;
if(list.size()<=level){
list.add(new ArrayList<Integer>());
}
levelGo(list,t.left,level+1);
levelGo(list,t.right,level+1);
list.get(level).add(t.val);
}
层次反向遍历(从叶子层到根层)
public List<List<Integer>> levelOrderBottom(TreeNode root) {
List<List<Integer>> res = new ArrayList();
levelGo(res,root,1);
return res;
}
public void levelGo(List<List<Integer>> list,TreeNode t,int level){
if(t==null) return;
if(list.size()<level){
list.add(0,new ArrayList<Integer>());
}
levelGo(list,t.left,level+1);
levelGo(list,t.right,level+1);
list.get(list.size()-level).add(t.val);
}

都是在递归的过程中传递一个该节点所在的层数,根据其所在的层数放到对应相同位置上。相当于通过函数参数将参数中节点的信息也压栈,不管递归到了那一层,我都能通过函数参数获得当前节点对应在整个树中的层数。


7.判断一棵树是否平衡

这里平衡的定义是每个节点的两颗子树的高度差不超过1.

这个题一看直接就可以遍历树然后求每个节点的左右子树的高度,做判断。如果用一个递归的话:

int depth (TreeNode root) {
if (root == null) return 0;
return max (depth(root.left), depth (root.right)) + 1;
}

bool isBalanced (TreeNode root) {
if (root == NULL) return true;

int left=depth(root.left);
int right=depth(root.right);

return abs(left - right) <= 1 && isBalanced(root.left) && isBalanced(root.right);
}
这相当于前序遍历,那么分析一下可以发现,我基本上是从上向下遍历树的节点,而且每次都要重新计算一下下边节点的高度,越靠下的节点被计算的次数越多,趋于O(n方)了。这样的话我们可以通过自底向上bottom-up后序遍历来减少节点高度的重复计算。同时如果底下已经有子树不平衡了,那么就可以直接结束了,终止继续递归,没必要再计算了。所以定义一个函数,后序遍历树节点,如果左右子树满足平衡,就返回该节点的高度给其父节点,如果不平衡,直接返回一个负数(因为树的高度是非负数,这样可以刚好的区分),在获得左节点高度后,递归调用判断右节点之前就进行判断,终止递归。同样右子树判断之后,立马验证右子树是否满足平衡。满足再继续递归。

public boolean isBalanced(TreeNode root) {
if(root == null) return true;
return ban(root)!= -1;
}
public int ban(TreeNode t){
if(t == null)return 0;
int tl = ban(t.left);
if(tl == -1)return -1;
int tr = ban(t.right);
if(tr == -1)return -1;
if(tl - tr>1||tl-tr<-1) return -1;
return Math.max(tl,tr)+1;
}