本博客旨在结局利用有序数组和有序链表构造平衡二叉树(下文使用AVL树代指)问题。
直接通过旋转来构造AVL树似乎是一个不错的选择,但是稍加分析就会发现,这样平白无故做了许多毫无意义的旋转。因为直接通过旋转调整二叉查找树(下文使用BST代指)并没有利用数组或链表本身是有序的信息,进行了大量无意义的操作。
下面通过leetcode两道例题来说明这个问题。
重点的问题在于:如何利用数组的有序信息呢?
首先我们先来观察一个有序数组和一棵AVL树所呈现的关系
我们可以发现数组中间元素恰好是对应AVL树的root节点(若数组长为偶数,可取中间偏左或偏右元,此时左右子树高度差为1)
我们只需要选取中间元素,构造头节点,在递归地构造其左右子树即可。
class Solution { public TreeNode sortedArrayToBST(int[] nums) { return build(nums, 0, nums.length - 1); } private TreeNode build(int[] nums, int l, int r) { // 为什么采用l小于r而不是等于来判断呢? // 我们这里是采用的闭区间的写法,当然可以采用闭开区间的写法,也就是等于时返回null // 循环和递归函数的边界条件一定要多加注意 if (l > r) return null; // 防止因为l和r过大造成溢出,不过这道题数组长度不会那么大,但最好还是养成这样的习惯 int mid = l + (r - l) / 2; // 其实这是一个中序遍历,先建立root节点,在递归地建立左子树和右子树 TreeNode node = new TreeNode(nums[mid]); node.left = build(nums, l, mid - 1); node.right = build(nums, mid + 1, r); // 建立好后返回root节点即可 return node; } }
递归函数是如何设计的呢?
时间复杂度:这道题本质上是使用先序遍历的方式构造一棵树,每个节点都被构造一次且路过三次,所以时间复杂度显然为O(N)
空间复杂度:递归调用栈深度为树的深度,由于构造的树为AVL树,其深度不超过lg(N),递归调用栈深度也不超过O(lgN),故空间复杂度为O(lgN)
这道题是108题的兄弟版本,数组可以随机访问元素,因此我们可以轻而易举地得到中间元素,但是链表不再具备这个特性。因此我们需要采取其他的方法来解决这个问题。
方法1不给出代码,其实相比于108题只是多了一步构造数组罢了。
class Solution { public TreeNode sortedListToBST(ListNode head) { return build(head, null); } private TreeNode build(ListNode l, ListNode r) { if (l == r) return null; ListNode slow = l, fast = slow; // 快慢指针寻找中间节点,可以说是本做法的核心,注意循环的条件 while (fast != r && fast.next != r) { slow = slow.next; fast = fast.next.next; } // 其实这是一个先序遍历的过程,在过程AVL树 TreeNode node = new TreeNode(slow.val); node.left = build(l, slow); node.right = build(slow.next, r); return node; } }
递归函数设计说明:
方法2缺陷:快慢指针寻找中间元素所需时间复杂度是o(lgN),我们每次在构造子树root节点时均需要使用它一次,这无疑造成了一些浪费。让我们回到108题所示图片中(把那个数组想象为一个链表),AVL树中序遍历产生的序列与链表是一致的。可以利用这个特点改进方法2吗?
这里附上一份详细的参考链接
class Solution { // 递归过程中各个函数均维护一个链表头,故将其设为实例变量 ListNode listHead; public TreeNode sortedListToBST(ListNode head) { listHead = head; int length = getLength(head); return build(0, length - 1); } // 一定要多注意该函数参数的设计,参数是整数索引!不再像方法2那样是ListNode引用 // 一定要好好理解这个函数 private TreeNode build(int left, int right) { if (left > right) return null; TreeNode node = new TreeNode(); int mid = left + (right - left) / 2; node.left = build(left, mid - 1); node.val = listHead.val; listHead = listHead.next; node.right = build(mid + 1, right); return node; } // 辅助函数,作用是遍历链表,统计其长度 private int getLength(ListNode head) { int counter = 0; while (head != null) { counter++; head = head.next; } return counter; } }
递归函数的说明: