昨天晚上十一点多吧,我在公司楼下抽烟呢,手机群里有人突然甩了个算法题,说“拆分二叉搜索树”,我第一反应还以为是那种把树砍两半的暴力活……后来一想不对,这玩意其实特别像线上做灰度:一棵 BST,给你个阈值 V,你得把它拆成两棵树,一棵全是 <= V,另一棵全是 > V,而且还得保持 BST 结构不乱,节点也不能丢。
你们知道 BST 那个味儿吧,左边都小,右边都大,所以拆的时候千万别去全树扫描,那就跟线上全量扫库一样,迟早出事。正确姿势是顺着“阈值”这条线走:
- 如果当前节点
root.val <= V,那它肯定应该留在“小树”里;但它的右子树里可能有一部分跑到“大树”去,所以只需要去拆右子树。拆完以后,小树这边的 root.right 接回“右子树拆出来的小半边”,大树那半边单独返回。 - 反过来如果
root.val > V,这个节点就得留在“大树”里;但它左子树里可能有一些其实 <= V,所以去拆左子树。拆完以后,大树这边的 root.left 接回“左子树拆出来的大半边”,小树那半边返回。
说白了就是递归一路下去,每次只动一条边,复杂度基本就是树高,平衡树接近 O(log n),最坏退化成链表才 O(n),但也比你傻乎乎全遍历强太多了。
代码我就按工程里最常见的写法来,返回一个长度为 2 的数组:res[0] 是小树根,res[1] 是大树根。对了,别用什么 Pair 啊三方包啊,面试官看了容易皱眉,直接数组最省事。
publicclassSplitBST{staticclassTreeNode{int val; TreeNode left, right; TreeNode(int v) { this.val = v; } }// 返回结果:res[0] = <= V 的BST根,res[1] = > V 的BST根publicstatic TreeNode[] splitBST(TreeNode root, int V) {if (root == null) returnnew TreeNode[]{null, null};if (root.val <= V) {// root 属于小树,去拆右子树 TreeNode[] rightSplit = splitBST(root.right, V); root.right = rightSplit[0]; // 右子树里 <=V 的部分接回小树returnnew TreeNode[]{root, rightSplit[1]}; // >V 的部分就是大树 } else {// root 属于大树,去拆左子树 TreeNode[] leftSplit = splitBST(root.left, V); root.left = leftSplit[1]; // 左子树里 >V 的部分接回大树returnnew TreeNode[]{leftSplit[0], root}; // <=V 的部分就是小树 } }// 随手写个小测试,别太在意输出格式,主要看结构不乱、节点不丢publicstaticvoidmain(String[] args){/* 4 / \ 2 6 / \ / \ 1 3 5 7 */ TreeNode root = new TreeNode(4); root.left = new TreeNode(2); root.right = new TreeNode(6); root.left.left = new TreeNode(1); root.left.right = new TreeNode(3); root.right.left = new TreeNode(5); root.right.right = new TreeNode(7); TreeNode[] res = splitBST(root, 2);// res[0] 应该包含 1,2;res[1] 应该包含 3,4,5,6,7(并保持BST) System.out.println(res[0].val); // 大概率是2 System.out.println(res[1].val); // 大概率是4 }}
你要是把这个思路带到日常写业务,其实挺像“按条件把一坨数据拆成两条链路”,关键点就是:别乱动不该动的部分,只沿着可能跨界的那条边去处理。昨天群里还有人问“能不能迭代写”,能写啊,但递归其实更像你脑子里那棵树的形状,写错概率更低……哎我先不说了,刚有人 @我 说线上又有个慢 SQL,要去看下日志了。