那天半夜一点多,产品小哥给我打电话,语音那头一顿噼里啪啦: “东哥东哥,那个……二叉那个数…树,啥来着,BST,对,修剪一下,页面老卡了。” 我人都懵了,还以为又是数据库锁表,结果一看,是道算法题。
大概意思你们懂的哈:给你一棵二叉搜索树,节点都是 int,顺序是左小右大,然后给你一个区间 [low, high],让你把不在这个区间的节点都剪掉,还得保证这货还是棵合法的二叉搜索树。 听着就像爸妈让你“清理朋友圈”:小于 200 的发红包,大于 1000 的借钱,统统删了,中间的留着 🤷♂️
我当时脑子里第一个画面,就是拿剪刀在树上咔咔乱剪,后来一想不对啊,这是“二叉搜索树”诶,它有秩序的。既然它是有序的,那就可以偷懒。比如当前节点值小于 low,那它左子树肯定更小嘛,整个左边都没救了,可以一刀全砍;反过来大于 high 就整个砍右边,对吧。
困得眼睛都睁不开了,我还是把代码敲出来了,大概长这样:
// 这个 TreeNode 就是力扣那种标准写法classTreeNode{int val; TreeNode left; TreeNode right; TreeNode(int x) {this.val = x; }}classSolution{public TreeNode trimBST(TreeNode root, int low, int high){// 1. 空树就别折腾了if (root == null) {returnnull; }// 2. 当前节点太小了,整棵左子树都比它更小,直接扔左边if (root.val < low) {// 相当于从右子树开始重新当根return trimBST(root.right, low, high); }// 3. 当前节点太大了,整棵右子树都比它更大,直接扔右边if (root.val > high) {// 从左子树接着找合适的根return trimBST(root.left, low, high); }// 4. 落在区间里,那就递归修剪它的左右子树 root.left = trimBST(root.left, low, high); root.right = trimBST(root.right, low, high);return root; }}
你看,其实逻辑特别生活化:
< low:这人太“抠”了,朋友圈左边那一挂全是更抠的,拉黑一整片;> high:这人太“豪”了,右边一群更豪的,同理一锅端;- 落在
[low, high] 中间的,就是那种“能一起吃饭 AA 的正常朋友”,留下来,把他左右两边再筛一遍。
这里最容易搞错的地方,真不是递归,而是那两个 return trimBST(root.right...) 和 return trimBST(root.left...)。 很多同学一开始会下意识写成:
if (root.val < low) { root = root.right;}
这样就废了,因为你只是把指针往右挪了一步,并没有继续往下“递归修剪”。类似你把垃圾从卧室扫到客厅,门一关,就以为打扫完了,结果爸妈一进来:这谁教你的代码风格……
我自己为了验证这玩意没写崩,还随手写了个中序打印看下修剪后的结果,顺便再感叹一下 BST 中序遍历是真的省心:
publicvoidprintInorder(TreeNode root){if (root == null) return; printInorder(root.left); System.out.print(root.val + " "); printInorder(root.right);}
修剪完再中序一遍,你会发现: 1)还是从小到大排好序的; 2)所有值都在 [low, high] 里面; 3)不在区间的节点,要么整支子树被砍掉,要么被它子树里更合适的节点“顶上来”。
那天把代码提上去,产品在群里说了一句:“哦,原来修剪树不是前端缩下高度就行啊。”我手机这边翻了个白眼,差点语音怼回去:兄弟这是算法不是 CSS 啊……
差不多就这样,我再多说两句就要开始讲 TCP 抓包和 1024 卡包那个妖孽 bug 了,算了算了,外卖到了我先去拿个饭。