Leetcode 第 173. Binary Search Tree Iterator 题,题目难度 Medium。
一. 题目要求
实现一个二叉搜索树迭代器。你将使用二叉搜索树的根节点初始化迭代器。
调用 next() 将返回二叉搜索树中的下一个最小的数。
示例:
BSTIterator iterator = new BSTIterator(root);
iterator.next(); // 返回 3
iterator.next(); // 返回 7
iterator.hasNext(); // 返回 true
iterator.next(); // 返回 9
iterator.hasNext(); // 返回 true
iterator.next(); // 返回 15
iterator.hasNext(); // 返回 true
iterator.next(); // 返回 20
iterator.hasNext(); // 返回 false
- next() 和 hasNext() 操作的时间复杂度是 O(1),并使用 O(h) 内存,其中 h 是树的高度。
二. 解题思路 & 代码
本题考察的是对二叉搜索树的遍历,最重要的一点就是明白二叉搜索树的特点,这里引用极客时间《数据结构与算法之美》中的表述:
在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值
题目要求我们的迭代器每次迭代返回的都是二叉树中的最小值,对于二叉搜索树可以采用中序遍历的方式将元素从小到大遍历一遍。
解法 1. 中序遍历 & List 迭代
每次 next 取值和 hasNext 判断没法直接通过 操作二叉树来实现,因此需要进行数据结构的转换,这里我们使用 ArrayList,通过中序遍历将二叉树的值以从小到大的顺序存到 List 中,然后接下来的迭代针对 List 操作就非常方便了。
实现代码如下:
class BSTIterator {
private List<TreeNode> nodeList = new ArrayList<>();
private int len = 0;
private int index = 0;
public BSTIterator(TreeNode root) {
this.inOrder(root);
len = nodeList.size();
}
// 中序遍历
private void inOrder(TreeNode node) {
if (node == null) {
return;
}
inOrder(node.left);
nodeList.add(node);
inOrder(node.right);
}
public int next() {
return nodeList.get(index ++).val;
}
public boolean hasNext() {
return index < len;
}
}
上面代码实际运行效率为 15ms 左右,beats 90.49 % of java。
解法 2. 中序遍历 & 二叉树构建
上面的解法运行效率已经算不错了,但是有一个还可以改进的地方:
- List 需要占用额外的存储空间,空间复杂度为 O(N),不符合题目的要求。
为了不占用额外的存储空间,就只能在二叉树本身进行操作。回到二叉搜索树的特点,如果我们二叉搜索树进行如下变动,则二叉树就变成了类似链表的结构,
此时遍历就非常方便了。
上述变换核心逻辑是:
- 父节点与左子节点互换,父节点变为左子节点的左子节点
- 父节点与右子节点互换,右子节点变为其原父节点的父节点
实现代码如下:
public class BSTIterator {
TreeNode current;
public BSTIterator(TreeNode root) {
current = root;
if (root != null) {
// 创建临时根节点,其 left 节点即为二叉搜索树中的最小左子节点
TreeNode head = new TreeNode(0);
TreeNode lastOne = build(head, root);
lastOne.left = null;
// 从 最小左子节点开始遍历
current = head.left;
}
}
private TreeNode build(TreeNode listNode, TreeNode root) {
if (root == null) return listNode;
listNode = build(listNode, root.left);
listNode.left = root;
listNode = root;
return build(listNode, root.right);
}
public boolean hasNext() {
return current != null;
}
public int next() {
int res = current.val;
current = current.left;
return res;
}
}
示例中的二叉树变换之后如下:
实际运行效率和第一版差不多,但是省去了额外的存储空间,另外也节省了 ArrayList 插入和长度变化时的时间,整体上性能还是有所优化的。
三. 解题后记
皓哥在《左耳听风》中提到,算法题一般分两类:
-
基础算法题:考察常用的数据结构、解题套路比如递归、DFS 等,通过做这类题可以加深对数据结构和算法的掌握。
-
编程题:这些题的 Edge Case 和 Corner Case 有很多。需要想清楚再干,做这些题可以非常好地训练对各种情况的考虑,以及你对程序代码组织的掌控。
这道题就是典型的基础算法题,首先要对二叉搜索树的特性有所了解,然后理解其前中后序遍历与递归的使用,最后在理清变换思路实现代码。
解题时尤其要注意的是针对迭代的性能优化,不要解出来就算了,第一版我们使用了一个额外的 List,如果要遍历的二叉树过大的话可能会导致内存溢出,因此第二版降低空间复杂度的尝试是非常有必要的。
我是 AhriJ邹同学,前后端、小程序、DevOps 都搞的炸栈工程师。博客持续更新,欢迎小伙伴关注或与我私信交流,互相学习,共同进步。