Leetcode总结(树)

本文是关于LeetCode中树型题目的总结,包括 Convert Sorted List to Binary Search Tree、Flatten Binary Tree to Linked List等8道题目。文章强调了递归在解决树问题中的重要性,介绍了将递归转化为递推公式的思想,以及如何通过数学归纳法解决这些问题。
摘要由CSDN通过智能技术生成

Leetcode题型总结之 树

Table of Contents

总结

#109. Convert Sorted List to Binary Search Tree

#114. Flatten Binary Tree to Linked List

#404. Sum of Left Leaves

#222. Count Complete Tree Nodes

#337. House Robber III

#450. Delete Node in a BST

#662. Maximum Width of Binary Tree

655. Print Binary Tree

701. Insert into a Binary Search Tree


总结

Why tree?  因为不够熟练,对递归,搜索算法以及分治比较抵触

 

Leetcode树的类型题目,总结来说就两点dfs和bfs,而dfs就基本是递归+循环

// check whether the root is null

// search left child

// search right child

 

递归的意义:

实际上,递归就是一个数学归纳法。

1.知道初始值的是多少

2.知道前一个值与后一个值的递推关系

所有的树类型的题都是类似这种方法求解。

 

推荐这个文章:https://juejin.im/post/5ec225e26fb9a043761ce4d8 

文章里面说的关于递归我十分赞同

回到递归,在学习递归的过程中,最大的陷阱就是人肉递归。人脑是很难把整个“递”“归”过程毫无差错的想清楚的。但是计算机恰好擅长做重复的事情,那我们便无须跳入细节,利用数学归纳法的思想,将其抽象成一个递推公式。相信它可以完成这个任务,其他的交给计算机就好了。

所需要做的是把递归转变成递推公式,运用数学归纳法,不需要跳入细节,之后相信计算机会完成这个算法就行了。

 

接下来的题基本就靠这个方法迎刃而解

 


 

#109. Convert Sorted List to Binary Search Tree

这题是把一个排好序的List转化成BST

通过观察我们发现,如果一个tree是一个BST,那么它的左右子树也同样是一个BST

如果我们把一个List转变成BST,那么也就相当于List的前半段变成BST,List后半段也变成BST,然后List的中间作为BST的root就好了。

 

于是套用数学归纳法的公式:

原子操作: 将List中间值作为根节点(并将List分为前后两半)

递推:执行fun(List前半段),执行fun(List后半段)

 

最后找到这个List中间值的方法是用快慢指针

代码如下

class Solution {

    public TreeNode sortedListTOBST(ListNode head) {
        
        // edge case
        if (head == null) return null;
        if (head.next == null) return new TreeNode(head.val); 
        
        //找到中间值之前的node,才能将List从中间斩断
        ListNode preMid = findPreMid(head);     
        ListNode mid = preMid.next;
        preMid.next = null;
           
        //原子操作
        TreeNode node = new TreeNode(mid.val);
        //递推
        node.left = sortedListTOBST(head);
        node.right = sortedListTOBST(mid.next);
        return node;
    }

    public findPreMid(ListNode head) {
        //O(n)
        //找到中间值,并时刻记录中间值之前的node
        ListNode slow = head, fast = head.next, preMid = slow;
        while (fast != null &&. fast.next != null) {
            preMid = slow;
            slow = slow.next;
            fast = fast.next.next;
        }    
        return preMid;
    }    

}

 

本题的时间复杂度是O(nlogn),递归调用树的次数是O(logn),每次找到中间值时时间为O(n)

 


 

#114. Flatten Binary Tree to Linked List

这题是将一个二叉树按照inorder顺序“铺平”

很显然,把树抽象为一个root + 左子树 + 右子树的模型

第一步就是左子树的right 指向右子树

第二步就是root的right指向左子树

这里有个trick就是利用pre指针

套用数学归纳法就是:

1. 原子操作:找到树叶

2. 递推:fun(右子树) 再 fun(左子树),随后将root.right指向pre,root.left指向空,最后pre指向root自己

 

代码如下

class Solution {
    TreeNode pre = null;
    
    public void flatten(TreeNode root) {
        //原子操作
        if (root == null) return;

        
        flatten(root.right);
        flatten(root.left);
        
        //指针指向前一个
        root.right = pre;
        root.left = null;
        pre = root;
    }
}

 

本题时间复杂度为O(n),显然每个节点都遍历过

 


 

#404. Sum of Left Leaves

计算左树叶

这题虽然是easy,但是我觉得是一个很典型的递归问题

这题就是设置一个flag,是不是左子树

1. 原子操作:如果flag是左子树且为树叶,则sum++

2. 递推:若left 不为空,fun(左子树, true),  若right不为空,func(右子树, false)

 

代码如下

class Solution {
    int sum = 0;
    
    public int sumOfLeftLeaves(TreeNode root) {
        //edge case
        if (root == null) return 0;
        if (root.left != null) helper(root.left, true);
        if (root.right != null) helper(root.right, false);
        return sum;
    }
    
    public void helper(TreeNode root, boolean isLeft) {
        //递推
        if (root.left != null) helper(root.left, true);
        if (root.right != null) helper(root.right, false);
        
        //原子操作
        if (isLeft && root.left == null && root.right == null) {
            sum += root.val;
        }
    }
}

 

本题的时间复杂度为O(n),考虑最坏情况(成一条线的树)

 


 

#222. Count Complete Tree Nodes

计算一个完全二叉树的节点个数,这题就是分两种情况,第一种左子树高度和右子树高度相等,就是一个完全二叉树了,利用公式2 ^ 层数 - 1就可以算出。如果不是,分别计算左子树和右子树,用同样的方法计算两个子树节点再加上root本身即可。

所以根据递归

1. 原子操作: 如果左右子树高度一致,直接计算2 ^ n - 1

2. 递推:节点数 = fun(左子树) + fun(右子树) + 1

代码如下

class Solution {
    public int leftHeight(TreeNode root) {
        int height = 0;
        while (root != null) {
            height++;
            root = root.left;
        }
        return height;
    }
    
    public int rightHeight(TreeNode root) {
        int height = 0;
        while (root != null) {
            height++;
            root = root.right;
        }
        return height;
    }
    
    public int countNodes(TreeNode root) {
        if (root == null) return 0;
        int left = leftHeight(root);
        int right = rightHeight(root);
        if (left == right) {
            return (1 << left) - 1;
        } else {
            return 1 + countNodes(root.left) + countNodes(root.right);
        }
    }
}

本题的时间复杂度需要思考一下,因为计算左右子树高度的操作不是O(n),而是O(logn)。

调用次数是O(logn)次,如下图所示

最终时间复杂度为O((logn)^2)


 

#337. House Robber III

本题是计算一个树状结构的房屋,如果小偷来偷他们家,前提是不能连着偷只能隔一个或两个,怎么偷。(小偷有这么聪明还做啥小偷

之前我以为这并不是一道树的题目,是一个动归或贪心。直接想当然贪心方法隔一个偷一个,但没想到如果隔两个偷的话有时候会更多

这题也可以拆分成两个子问题 -> 偷这家或不偷这家

1. 如果偷这家,那么这家的left和right就不能考虑了,也就是跳过root.left, 和root.right,直接考虑左左,左右,右左,右右,也就是fun(root.left.left) + fun(root.left.right) + fun(root.right.left) + fun(root.right.right)

2. 如果不偷这家,那么就考虑下家吧,也就是跳过本次root, 考虑左右子树,也就是

fun(root.left) + func(root.right)

以上就是递推

而原子操作就是if root == null, 则返回0

考虑root.left和root.right是否为空之后,写出代码如下

class Solution {
    public int rob(TreeNode root) {
        if (root == null) return 0;
        int rootVal = root.val;
        int rootNext = 0;
        if (root.left != null) {
            rootVal += rob(root.left.left) + rob(root.left.right);
            rootNext += rob(root.left);
        }
        if (root.right != null) {
            rootVal += rob(root.right.left) + rob(root.right.right);
            rootNext += rob(root.right);
        }
        int maxVal = Math.max(rootVal, rootNext);
        return maxVal;
    }
}

这里看到discussion后有一个结合动归思维的点,就是加上HashMap存储。

因为有些重复节点可能会被算很多次,一旦这个节点知道了它最大值是多少,那么就不必再次计算了

也就有以下代码

class Solution {
    Map<TreeNode, Integer> map = new HashMap<>();
    public int rob(TreeNode root) {
        if (root == null) return 0;
        if (map.containsKey(root)) return map.get(root);
        int rootVal = root.val;
        int rootNext = 0;
        if (root.left != null) {
            rootVal += rob(root.left.left) + rob(root.left.right);
            rootNext += rob(root.left);
        }
        if (root.right != null) {
            rootVal += rob(root.right.left) + rob(root.right.right);
            rootNext += rob(root.right);
        }
        int maxVal = Math.max(rootVal, rootNext);
        map.put(root, maxVal);
        return maxVal;
    }
}

速度直接从500多ms,变为2ms

时间复杂度,很明显是O(n)

 

但我看了一下排名第一的代码

class Solution {
    public int rob(TreeNode root) {
        int[] res = helper(root);
        return Math.max(res[0], res[1]);
    }
    
    public int[] helper(TreeNode root) {
        if (root == null) return new int[]{0, 0};
        int[] robLeft = helper(root.left);
        int[] robRight = helper(root.right);
        int robIt = root.val + robLeft[1] + robRight[1];
        int nrobIt = Math.max(robLeft[0], robLeft[1]) + Math.max(robRight[0], robRight[1]);
        return new int[]{robIt, nrobIt};
    }
}

这样的方法速度可以到0ms

这种方法优点就是调用次数,我自己的方法中,fun调用了6次,而这个方法只调用2次

 


 

#450. Delete Node in a BST

本题题意很简单,就是在一个BST中删除一个node。

我们知道删除BST中一个node会出现三种情况:

1: 左子树为空:这种情况删掉了当前节点root.right顶替即可

2: 右子树为空:与上种情况同理,直接用root.left顶替即可

3: 如果两边都不为空:那删除了当前节点的话,需要用左子树中的最大值节点来顶替,或者用右子树的最小值节点来顶替

所以本题有两种解法

递归思路如下:

1. 原子操作:遇到左/右子树为空的node,直接删除,用左/右子树顶替

2. 递推:遇到两边不为空,那删除当前node,并用左子树最大值赋值给当前node赋值(leftMaxVal),
然后就变为了执行fun(root.left, leftMaxVal) (即删除左子树中的leftMaxVal)

代码如下

class Solution {
    public TreeNode deleteNode(TreeNode root, int key) {
        if (root == null) return null;
        if (root.val > key) {
            root.left = deleteNode(root.left, key);
        } else if (root.val < key) {
            root.right = deleteNode(root.right, key);
        } else { //Find the root whose val is key
            if (root.left == null) {
                return root.right;
            } else if (root.right == null) {
                return root.left;
            } else {
                TreeNode p = root.left;
                // Search for maxVal at left children

                while (p.right != null) {
                    p = p.right;
                }
                root.val = p.val;
                root.left = deleteNode(root.left, root.val);

                /* alternative method
                // Search for minVal at right children
                TreeNode p = root.right;
                while (p.left != null) {
                    p = p.left;
                }
                root.val = p.val;
                root.right = deleteNode(root.right, root.val);

                */
            }
        }
        return root;
    }
    
}

很显然,该算法最差情况就是删除根节点root,这样的话时间复杂度仍然为O(logN),(搜索时层数最大为O(logN))

 


#662. Maximum Width of Binary Tree

题意是说计算一个二叉树的最大宽度,也就是计算同一层内, 最左子树的位置 - 最右子树的位置 + 1

加一个变量pos,如果将二叉树画成这样:

很明显,对于一个节点node来说,位置为pos,则其左子树等于2 * pos,右子树为2 * pos + 1

这样递归操作就有:

1. 原子操作:若当前节点为空,不做任何操作;若不为空则计算该节点到最左节点的距离

2. 递归:fun(左子树, 2 * pos), fun(右子树, 2 * pos + 1)

这里增加一个Map,储存每一层最左节点的pos

代码如下:

class Solution {
    int maxWidth = 1;
    Map<Integer, Integer> map = new HashMap<>();
    public int widthOfBinaryTree(TreeNode root) {
        if (root == null) return 0;
        helper(root, 0, 0);
        return maxWidth;
    }
    
    public void helper(TreeNode root, int depth, int pos) {
        if (root == null) return;
        if (!map.containsKey(depth)) {
            map.put(depth, pos);
        } else {
            maxWidth = Math.max(maxWidth, pos - map.get(depth) + 1);
        }
       
        helper(root.left, depth + 1, pos * 2);
        helper(root.right, depth + 1, pos * 2 + 1);
        
    }
}

时间复杂度为O(N),空间复杂度(Map的使用)为O(N)

 

同样也可以用BFS做,增加一个queue用于存储,每遍历一层就记录最左节点的pos

还需要一个类Node,因为BFS无法传pos给子树,所以干脆建一个新类Node包含pos和TreeNode本身

代码如下

class Solution {
    public int widthOfBinaryTree(TreeNode root) {
        if (root == null) return 0;
        Queue<Node> queue = new LinkedList<>();
        queue.offer(new Node(root, 0));
        int maxWidth = 1;
        while (!queue.isEmpty()) {
            int len = queue.size();
            int leftMost = 0;
            for (int i = 0; i < len; i++) {
                Node temp = queue.poll();
                if (temp.node.left != null) {
                    queue.offer(new Node(temp.node.left, temp.pos * 2));
                }
                if (temp.node.right != null) {
                    queue.offer(new Node(temp.node.right, temp.pos * 2 + 1));
                }
                if (i == 0) {
                    leftMost = temp.pos;
                } else {
                    maxWidth = Math.max(maxWidth, temp.pos - leftMost + 1);
                }
            }
        }
        return maxWidth;
    }
}

class Node {
    TreeNode node;
    int pos;
    Node(TreeNode node, int pos) {
        this.node = node;
        this.pos = pos;
    }
}

时间复杂度为O(N), 空间复杂度(queue)为O(N)

 


 

655. Print Binary Tree

题意为打印二叉树,看似很容易,其实坑也挺多。一开始思路是边dfs边打印,但是这种方法很麻烦,也很难想。

这题最佳方法是一开始就打印一个空树(得到树的高度h,再建立2^h - 1 * h的矩阵)里面填满空字符串“”,
之后再用二分查找的方法填写二叉树

填写的递归操作为:

1. 原子操作:若为空则不操作,若不为空在board[][mid]填

2. 递推:fun(左子树, h + 1,  start,  mid -1), fun(右子树,  h + 1,  mid + 1,  end);

代码如下:

class Solution {
    public List<List<String>> printTree(TreeNode root) {
        int height = getHeight(root);
        int width = (1 << height) - 1;
        String[][] board = new String[height][width];
        for (String[] b : board) Arrays.fill(b, "");
        List<List<String>> list = new ArrayList<>();
        setBoardValue(root, board, 0, width - 1, 0);
        for (String[] b : board) list.add(Arrays.asList(b));
        return list;
    }
     
    public void setBoardValue(TreeNode root, String[][] board, int start, int end, int height) {
        if (root == null) return;
        int mid = (start + end) / 2;
        board[height][mid] = Integer.toString(root.val);
        setBoardValue(root.left, board, start, mid - 1, height + 1);
        setBoardValue(root.right, board, mid + 1, end, height + 1);
    }
    
    public int getHeight(TreeNode root) {
        if (root == null) return 0;
        return Math.max(getHeight(root.left), getHeight(root.right)) + 1;
    } 
    
    
}

时间复杂度为O(N),空间复杂度为O(N)


701. Insert into a Binary Search Tree​​​​​​​

题意即将一个数插入BST中,

很简单,如果这个值小一点,就往左子树插,相当于问“将一个数插入左子树的BST中”;

同理,若大一点,就往右子树插,若相等,随便往哪插

递归操作为:

1. 原子操作:若节点为空, 直接创建这个val的TreeNode

2. 递推:若val较小,找左子树,即fun(root.left, val),否则找右子树, fun(root.right, val)

代码如下:

class Solution {
    public TreeNode insertIntoBST(TreeNode root, int val) {
        if (root == null) return new TreeNode(val);
        if (val < root.val) {
            root.left = insertIntoBST(root.left, val);
        } else {
            root.right = insertIntoBST(root.right, val);
        }
        return root;
    }
}

时间复杂度为O(N), 空间复杂度为O(N)


本博客将持续更新Leetcode树问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值