非递减数列这个事儿啊,我是前两天刚被人半夜电话拎起来问的,语气特别急:“东哥东哥,那个啥,数组是不是只改一个就能变成不下降的,怎么写?”我人都还在床上呢,脑子一片浆糊,只能一边翻身一边想算法。
你先脑补个场景吧,就像考试成绩单那种数组:[4, 2, 3],老师说我只愿意给你改一次分,看能不能把这串分数改成“后面不比前面低”的,那就是所谓“非递减数列”。也就是说得满足 nums[i] <= nums[i+1],全程不能出现下坡,只能持平或者上坡。
一开始我也想过那种比较蠢的办法:暴力枚举,每个位置都试着改一改,看最后是不是非递减。想想就知道完蛋,面试官当场给你请出门口那种,时间复杂度直接炸。后来清醒一点,拿纸随便写了几个例子,我念给你听:
[4, 2, 3] 这种,改一个 4 变成 2 或者改 2 变成 4,都能搞定,答案 true。[3, 4, 2, 3] 就有点恶心了,你无论改哪个,都会还有一个地方是下坡,所以要 false。
关键点就一个:你从左往右扫数组,找到“下坡”的地方,也就是 nums[i] < nums[i-1] 的位置,看这种情况出现几次。如果多于一次,铁定就不行了,只能改一个嘛。
但是只数次数还不够,难点在于:出现这一次下坡的时候,你到底是“怪前面”还是“怪后面”?也就是你究竟改 nums[i-1] 还是改 nums[i]。
那天我在工位上给同事讲,直接在 IDEA 里敲了个最小版的 Java 写法,大概长这样:
publicbooleancheckPossibility(int[] nums){int changed = 0; // 记录你动手改的次数for (int i = 1; i < nums.length; i++) {if (nums[i] < nums[i - 1]) { // 出现“下坡” changed++;if (changed > 1) {returnfalse; // 下坡超过一次,直接GG }if (i - 2 >= 0 && nums[i] < nums[i - 2]) {// 说明当前这个太小,拉它上去 nums[i] = nums[i - 1]; } else {// 否则就把前面的压下来 nums[i - 1] = nums[i]; } } }returntrue;}
你感受一下这个 if 里那段分支,其实就是人脑在纠结:“这锅到底谁背”。举个更具体的:
比如 [3, 4, 2, 3],当你走到 i = 2 这一位,也就是数值 2,发现 2 < 4,这里有个“下坡”。 这时候再往前看一眼 nums[i-2] 是 3。 如果你把 4 改小,改成 2,那数组变 [3, 2, 2, 3],前面又变成了 3 > 2,前面直接崩掉。 所以这里就得让 2 抬起来,抬到和 4 一样大,改成 [3, 4, 4, 3]。虽然后面还有问题,但至少不会让前面的历史更糟。
换个例子 [4, 2, 3],当你走到 i = 1 的时候,前面没有 i-2 了,那就简单粗暴,把前面的 4 改成 2 就好了,变成 [2, 2, 3],一切和谐。
所以那句判断:
if (i - 2 >= 0 && nums[i] < nums[i - 2]) { nums[i] = nums[i - 1];} else { nums[i - 1] = nums[i];}
背后小心思是这样的: 如果当前这个数 nums[i] 小到连再往前一个 nums[i-2] 都顶不住,那说明“现在的人问题大”,只好把它拉高; 否则就说明“前任太高调”,把 nums[i-1] 拉低一点,和当前对齐就行。
我当时还顺手写了个小 main 跑一跑,免得嘴上说的都对,代码一跑寄了:
publicstaticvoidmain(String[] args){int[][] tests = { {4, 2, 3}, {3, 4, 2, 3}, {1, 2, 3, 3}, {5, 7, 1, 8} }; NonDecreasingArray s = new NonDecreasingArray();for (int[] arr : tests) { System.out.println(Arrays.toString(arr) + " -> " + s.checkPossibility(arr.clone())); }}
打印出来大概就是这种感觉:
[5, 7, 1, 8] -> true (把 7 改成 1 就行)
这里有个小细节,注意我调用的时候用了 arr.clone(),因为算法里是会真的修改数组的。在面试里如果不 clone,考官可能还会问你一句“为啥要改原数组”,你就又得扯一堆“节省空间”“O(1) 额外空间”这种官方话术。这个和数据库那些默认配置不改就翻车的感觉很像,你以为“反正能跑”,结果线上一压测直接跪⌄⌄⌄
这题整体复杂度挺亲民的,就是单次 for 循环,时间 O(n),空间上只用几个临时变量,算是那种写着写着很顺手的题,比我当年选 MySQL 还是 Postgres 纠结半天要幸福多了。
顺带提醒一句,写这种一遍扫描的题,日志别乱打,我之前排查 TCP 那个 1024 卡包的坑,一堆打印害得我眼睛都花了,看半天才发现问题其实就卡在一个字节上,你数组越界、索引写错一个,一样能给你搞出玄学 bug。
反正这个非递减数列你要是能把“只允许一次犯错,然后聪明地补救”这个思路吃透,后面遇到那种“最多改 K 次”的、或者“删掉一个数让它变好看”的题,脑子里套路都是一样的。行了,我先去喝口水,眼睛有点花,剩下的你自己多敲两遍代码就记住了。