|
| 1 | +# 题目描述(简单难度) |
| 2 | + |
| 3 | + |
| 4 | + |
| 5 | +给一个升序数组,生成一个平衡二叉搜索树。平衡二叉树定义如下: |
| 6 | + |
| 7 | +> 它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。 |
| 8 | +
|
| 9 | +二叉搜索树定义如下: |
| 10 | + |
| 11 | +> 1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值; |
| 12 | +> 2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值; |
| 13 | +> 3. 任意节点的左、右子树也分别为二叉查找树; |
| 14 | +> 4. 没有键值相等的节点。 |
| 15 | +
|
| 16 | +# 解法一 递归 |
| 17 | + |
| 18 | +如果做了 [98 题](<https://leetcode.wang/leetCode-98-Validate-Binary-Search-Tree.html>) 和 [99 题](<https://leetcode.wang/leetcode-99-Recover-Binary-Search-Tree.html>),那么又看到这里的升序数组,然后应该会想到一个点,二叉搜索树的中序遍历刚好可以输出一个升序数组。 |
| 19 | + |
| 20 | +所以题目给出的升序数组就是二叉搜索树的中序遍历。 |
| 21 | + |
| 22 | +根据中序遍历还原一颗树,又想到了 [105 题](<https://leetcode.wang/leetcode-105-Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal.html>) 和 [106 题](<https://leetcode.wang/leetcode-106-Construct-Binary-Tree-from-Inorder-and-Postorder-Traversal.html>),通过中序遍历加前序遍历或者中序遍历加后序遍历来还原一棵树。前序(后序)遍历的作用呢?提供根节点!然后根据根节点,就可以递归的生成左右子树。 |
| 23 | + |
| 24 | +这里的话怎么知道根节点呢?平衡二叉树,既然要做到平衡,我们只要把根节点选为数组的中点即可。 |
| 25 | + |
| 26 | +综上,和之前一样,找到了根节点,然后把数组一分为二,进入递归即可。注意这里的边界情况,包括左边界,不包括右边界。 |
| 27 | + |
| 28 | +```java |
| 29 | +public TreeNode sortedArrayToBST(int[] nums) { |
| 30 | + return sortedArrayToBST(nums, 0, nums.length); |
| 31 | +} |
| 32 | + |
| 33 | +private TreeNode sortedArrayToBST(int[] nums, int start, int end) { |
| 34 | + if (start == end) { |
| 35 | + return null; |
| 36 | + } |
| 37 | + int mid = (start + end) >>> 1; |
| 38 | + TreeNode root = new TreeNode(nums[mid]); |
| 39 | + root.left = sortedArrayToBST(nums, start, mid); |
| 40 | + root.right = sortedArrayToBST(nums, mid + 1, end); |
| 41 | + |
| 42 | + return root; |
| 43 | +} |
| 44 | +``` |
| 45 | + |
| 46 | +# 解法二 栈 DFS |
| 47 | + |
| 48 | +递归都可以转为迭代的形式。 |
| 49 | + |
| 50 | +一部分递归算法,可以转成动态规划,实现空间换时间,例如 [5题](<https://leetcode.windliang.cc/leetCode-5-Longest-Palindromic-Substring.html>),[10题](<https://leetcode.windliang.cc/leetCode-10-Regular-Expression-Matching.html>),[53题](<https://leetcode.windliang.cc/leetCode-53-Maximum-Subarray.html?h=%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92>),[72题](<https://leetcode.wang/leetCode-72-Edit-Distance.html>),从自顶向下再向顶改为了自底向上。 |
| 51 | + |
| 52 | +一部分递归算法,只是可以用栈去模仿递归的过程,对于时间或空间的复杂度没有任何好处,比如这道题,唯一好处可能就是能让我们更清楚的了解递归的过程吧。 |
| 53 | + |
| 54 | +自己之前对于这种完全模仿递归思路写成迭代,一直也没写过,今天也就试试吧。 |
| 55 | + |
| 56 | +思路的话,我们本质上就是在模拟递归,递归其实就是压栈出栈的过程,我们需要用一个栈去把递归的参数存起来。这里的话,就是函数的参数 `start`,`end`,以及内部定义的 `root`。为了方便,我们就定义一个类。 |
| 57 | + |
| 58 | +```java |
| 59 | +class MyTreeNode { |
| 60 | + TreeNode root; |
| 61 | + int start; |
| 62 | + int end |
| 63 | + MyTreeNode(TreeNode r, int s, int e) { |
| 64 | + this.root = r; |
| 65 | + this.start = s; |
| 66 | + this.end = e; |
| 67 | + } |
| 68 | +} |
| 69 | +``` |
| 70 | + |
| 71 | +第一步,我们把根节点存起来。 |
| 72 | + |
| 73 | +```java |
| 74 | +Stack<MyTreeNode> rootStack = new Stack<>(); |
| 75 | +int start = 0; |
| 76 | +int end = nums.length; |
| 77 | +int mid = (start + end) >>> 1; |
| 78 | +TreeNode root = new TreeNode(nums[mid]); |
| 79 | +TreeNode curRoot = root; |
| 80 | +rootStack.push(new MyTreeNode(root, start, end)); |
| 81 | +``` |
| 82 | + |
| 83 | +然后开始递归的过程,就是不停的生成左子树。因为要生成左子树,`end - start` 表示当前树的可用数字的个数,因为根节点已经用去 1 个了,所以为了生成左子树,个数肯定需要大于 1。 |
| 84 | + |
| 85 | +```java |
| 86 | +while (end - start > 1) { |
| 87 | + mid = (start + end) >>> 1; //当前根节点 |
| 88 | + end = mid;//左子树的结尾 |
| 89 | + mid = (start + end) >>> 1;//左子树的中点 |
| 90 | + curRoot.left = new TreeNode(nums[mid]); |
| 91 | + curRoot = curRoot.left; |
| 92 | + rootStack.push(new MyTreeNode(curRoot, start, end)); |
| 93 | +} |
| 94 | +``` |
| 95 | + |
| 96 | +在递归中,返回 `null` 以后,开始生成右子树。这里的话,当 `end - start <= 1` ,也就是无法生成左子树了,我们就可以出栈,来生成右子树。 |
| 97 | + |
| 98 | +```java |
| 99 | +MyTreeNode myNode = rootStack.pop(); |
| 100 | +//当前作为根节点的 start end 以及 mid |
| 101 | +start = myNode.start; |
| 102 | +end = myNode.end; |
| 103 | +mid = (start + end) >>> 1; |
| 104 | +start = mid + 1; //右子树的 start |
| 105 | +curRoot = myNode.root; //当前根节点 |
| 106 | +if (start < end) { //判断当前范围内是否有数 |
| 107 | + mid = (start + end) >>> 1; //右子树的 mid |
| 108 | + curRoot.right = new TreeNode(nums[mid]); |
| 109 | + curRoot = curRoot.right; |
| 110 | + rootStack.push(new MyTreeNode(curRoot, start, end)); |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +然后把上边几块内容组合起来就可以了。 |
| 115 | + |
| 116 | +```java |
| 117 | +class MyTreeNode { |
| 118 | + TreeNode root; |
| 119 | + int start; |
| 120 | + int end; |
| 121 | + |
| 122 | + MyTreeNode(TreeNode r, int s, int e) { |
| 123 | + this.root = r; |
| 124 | + this.start = s; |
| 125 | + this.end = e; |
| 126 | + } |
| 127 | +} |
| 128 | +public TreeNode sortedArrayToBST(int[] nums) { |
| 129 | + if (nums.length == 0) { |
| 130 | + return null; |
| 131 | + } |
| 132 | + Stack<MyTreeNode> rootStack = new Stack<>(); |
| 133 | + int start = 0; |
| 134 | + int end = nums.length; |
| 135 | + int mid = (start + end) >>> 1; |
| 136 | + TreeNode root = new TreeNode(nums[mid]); |
| 137 | + TreeNode curRoot = root; |
| 138 | + rootStack.push(new MyTreeNode(root, start, end)); |
| 139 | + while (end - start > 1 || !rootStack.isEmpty()) { |
| 140 | + //考虑左子树 |
| 141 | + while (end - start > 1) { |
| 142 | + mid = (start + end) >>> 1; //当前根节点 |
| 143 | + end = mid;//左子树的结尾 |
| 144 | + mid = (start + end) >>> 1;//左子树的中点 |
| 145 | + curRoot.left = new TreeNode(nums[mid]); |
| 146 | + curRoot = curRoot.left; |
| 147 | + rootStack.push(new MyTreeNode(curRoot, start, end)); |
| 148 | + } |
| 149 | + //出栈考虑右子树 |
| 150 | + MyTreeNode myNode = rootStack.pop(); |
| 151 | + //当前作为根节点的 start end 以及 mid |
| 152 | + start = myNode.start; |
| 153 | + end = myNode.end; |
| 154 | + mid = (start + end) >>> 1; |
| 155 | + start = mid + 1; //右子树的 start |
| 156 | + curRoot = myNode.root; //当前根节点 |
| 157 | + if (start < end) { //判断当前范围内是否有数 |
| 158 | + mid = (start + end) >>> 1; //右子树的 mid |
| 159 | + curRoot.right = new TreeNode(nums[mid]); |
| 160 | + curRoot = curRoot.right; |
| 161 | + rootStack.push(new MyTreeNode(curRoot, start, end)); |
| 162 | + } |
| 163 | + |
| 164 | + } |
| 165 | + |
| 166 | + return root; |
| 167 | +} |
| 168 | +``` |
| 169 | + |
| 170 | +# 解法三 队列 BFS |
| 171 | + |
| 172 | +参考 [这里](<https://leetcode.com/problems/convert-sorted-array-to-binary-search-tree/discuss/35218/Java-Iterative-Solution>)。 和递归的思路基本一样,不停的划分范围。 |
| 173 | + |
| 174 | +```java |
| 175 | +class MyTreeNode { |
| 176 | + TreeNode root; |
| 177 | + int start; |
| 178 | + int end; |
| 179 | + |
| 180 | + MyTreeNode(TreeNode r, int s, int e) { |
| 181 | + this.root = r; |
| 182 | + this.start = s; |
| 183 | + this.end = e; |
| 184 | + } |
| 185 | +} |
| 186 | +public TreeNode sortedArrayToBST3(int[] nums) { |
| 187 | + if (nums.length == 0) { |
| 188 | + return null; |
| 189 | + } |
| 190 | + Queue<MyTreeNode> rootQueue = new LinkedList<>(); |
| 191 | + TreeNode root = new TreeNode(0); |
| 192 | + rootQueue.offer(new MyTreeNode(root, 0, nums.length)); |
| 193 | + while (!rootQueue.isEmpty()) { |
| 194 | + MyTreeNode myRoot = rootQueue.poll(); |
| 195 | + int start = myRoot.start; |
| 196 | + int end = myRoot.end; |
| 197 | + int mid = (start + end) >>> 1; |
| 198 | + TreeNode curRoot = myRoot.root; |
| 199 | + curRoot.val = nums[mid]; |
| 200 | + if (start < mid) { |
| 201 | + curRoot.left = new TreeNode(0); |
| 202 | + rootQueue.offer(new MyTreeNode(curRoot.left, start, mid)); |
| 203 | + } |
| 204 | + if (mid + 1 < end) { |
| 205 | + curRoot.right = new TreeNode(0); |
| 206 | + rootQueue.offer(new MyTreeNode(curRoot.right, mid + 1, end)); |
| 207 | + } |
| 208 | + } |
| 209 | + |
| 210 | + return root; |
| 211 | +} |
| 212 | +``` |
| 213 | + |
| 214 | +最巧妙的地方是它先生成 `left` 和 `right` 但不进行赋值,只是把范围传过去,然后出队的时候再进行赋值。这样最开始的根节点也无需单独考虑了。 |
| 215 | + |
| 216 | +# 扩展 求中点 |
| 217 | + |
| 218 | +前几天和同学发现个有趣的事情,分享一下。 |
| 219 | + |
| 220 | +首先假设我们的变量都是 `int` 值。 |
| 221 | + |
| 222 | +二分查找中我们需要根据 `start` 和 `end` 求中点,正常情况下加起来除以 2 即可。 |
| 223 | + |
| 224 | +```java |
| 225 | +int mid = (start + end) / 2 |
| 226 | +``` |
| 227 | + |
| 228 | +但这样有一个缺点,我们知道`int`的最大值是 `Integer.MAX_VALUE` ,也就是`2147483647`。那么有一个问题,如果 `start = 2147483645`,`end = = 2147483645`,虽然 `start` 和 `end`都没有超出最大值,但是如果利用上边的公式,加起来的话就会造成溢出,从而导致`mid`计算错误。 |
| 229 | + |
| 230 | +解决的一个方案就是利用数学上的技巧,我们可以加一个 `start` 再减一个 `start` 将公式变形。 |
| 231 | + |
| 232 | +```java |
| 233 | +(start + end) / 2 = (start + end + start - start) / 2 = start + (end - start) / 2 |
| 234 | +``` |
| 235 | + |
| 236 | +这样的话,就解决了上边的问题。 |
| 237 | + |
| 238 | +然后当时和同学看到`jdk`源码中,求`mid`的方法如下 |
| 239 | + |
| 240 | +```java |
| 241 | +int mid = (start + end) >>> 1 |
| 242 | +``` |
| 243 | + |
| 244 | +它通过移位实现了除以 2,但。。。这样难道不会导致溢出吗? |
| 245 | + |
| 246 | +首先大家可以补一下 [补码](https://mp.weixin.qq.com/s/uvcQHJi6AXhPDJL-6JWUkw) 的知识。 |
| 247 | + |
| 248 | +其实问题的关键就是这里了`>>>` ,我们知道还有一种右移是`>>`。区别在于`>>`为有符号右移,右移以后最高位保持原来的最高位。而`>>>`这个右移的话最高位补 0。 |
| 249 | + |
| 250 | +所以这里其实利用到了整数的补码形式,最高位其实是符号位,所以当 `start + end`溢出的时候,其实本质上只是符号位收到了进位,而`>>>`这个右移可以带着符号位右移,所以之前的信息没有丢掉。 |
| 251 | + |
| 252 | +但`>>`有符号右移就会出现问题了,事实上 JDK6 之前都用的`>>`,这个 BUG 在 java 里竟然隐藏了十年之久。 |
| 253 | + |
| 254 | +# 总 |
| 255 | + |
| 256 | +经过这么多的分析,大家估计体会到了递归的魅力了吧,简洁而优雅。另外的两种迭代的实现,可以让我们更清楚的了解递归到底发生了什么。关于求中点,大家以后就用`>>>`吧,比`start + (end - start) / 2`简洁不少,还能给别人科普一下补码的知识。 |
0 commit comments