使数组按非递减顺序排列 图解题解
这道题到底在问什么
- 输入
- nums=[5,3,4,4,7,3,6,11,8,5,11]
- 输出
- 3
- 输入
- nums=[4,5,7,7,13]
- 输出
- 0 (本来就非递减,不用删)
最优解:一步一步想明白
- 3记牢这一句:每一轮基于本轮开始的数组同时删,谁的左邻比自己大谁就被删。下面从第一轮开始,一对一对地扫。
- 4准备逐轮删除这是初始数组 5,3,4,4,7,3,6。从左往右看,3 比前面的 5 小,是个下降;7 后面又掉到 3,也是下降。只要还存在这种下降,数组就不是非递减,就得继续删。灰色代表已删除(现在一个都没有),红色代表本轮待删,现在开始第一轮。
- 5第 1 轮 · 扫描相邻对第 1 轮开始。当前还活着的是 5, 3, 4, 4, 7, 3, 6 这 7 个数(灰色的是前几轮已经删掉的,固定留在原位当占位)。我拿这一轮开始时的这串数,从左往右一对一对地看,谁的左邻比自己大,就把谁标红等着删。
- 6第 1 轮 · 第 1 对看这一对:左边绿色的基准要么是保留的邻居、要么已经标红,右边紫色是 3。左邻的值 5 比 3 大,构成下降,所以 3 这一步要被删,把它标红。注意它标红后并不马上消失,还要继续给它右边的数当比较基准。
- 7第 1 轮 · 第 2 对看这一对:左边是 3,右边紫色是 4。3 没有比 4 大,这里不是下降,4 本轮保留,不标红。继续看下一对。
- 8第 1 轮 · 第 3 对看这一对:左边是 4,右边紫色是 4。4 没有比 4 大,这里不是下降,4 本轮保留,不标红。继续看下一对。
- 9第 1 轮 · 第 4 对看这一对:左边是 4,右边紫色是 7。4 没有比 7 大,这里不是下降,7 本轮保留,不标红。继续看下一对。
- 10第 1 轮 · 第 5 对看这一对:左边绿色的基准要么是保留的邻居、要么已经标红,右边紫色是 3。左邻的值 7 比 3 大,构成下降,所以 3 这一步要被删,把它标红。注意它标红后并不马上消失,还要继续给它右边的数当比较基准。
- 11第 1 轮 · 第 6 对看这一对:左边是 3,右边紫色是 6。3 没有比 6 大,这里不是下降,6 本轮保留,不标红。继续看下一对。
- 12第 1 轮完成第 1 轮结算:刚才标红的 3, 3 这 2 个数同时被删,一起灰掉。剩下绿色的 5, 4, 4, 7, 6 是本轮的幸存者。已完成 1 轮。再看看它还降不降,降就接着来下一轮。
- 13第 2 轮 · 扫描相邻对第 2 轮开始。当前还活着的是 5, 4, 4, 7, 6 这 5 个数(灰色的是前几轮已经删掉的,固定留在原位当占位)。我拿这一轮开始时的这串数,从左往右一对一对地看,谁的左邻比自己大,就把谁标红等着删。
- 14第 2 轮 · 第 1 对看这一对:左边绿色的基准要么是保留的邻居、要么已经标红,右边紫色是 4。左邻的值 5 比 4 大,构成下降,所以 4 这一步要被删,把它标红。注意它标红后并不马上消失,还要继续给它右边的数当比较基准。
- 15第 2 轮 · 第 2 对看这一对:左边是 4,右边紫色是 4。4 没有比 4 大,这里不是下降,4 本轮保留,不标红。继续看下一对。
- 16第 2 轮 · 第 3 对看这一对:左边是 4,右边紫色是 7。4 没有比 7 大,这里不是下降,7 本轮保留,不标红。继续看下一对。
- 17第 2 轮 · 第 4 对看这一对:左边绿色的基准要么是保留的邻居、要么已经标红,右边紫色是 6。左邻的值 7 比 6 大,构成下降,所以 6 这一步要被删,把它标红。注意它标红后并不马上消失,还要继续给它右边的数当比较基准。
- 18第 2 轮完成第 2 轮结算:刚才标红的 4, 6 这 2 个数同时被删,一起灰掉。剩下绿色的 5, 4, 7 是本轮的幸存者。已完成 2 轮。再看看它还降不降,降就接着来下一轮。
- 19第 3 轮 · 扫描相邻对第 3 轮开始。当前还活着的是 5, 4, 7 这 3 个数(灰色的是前几轮已经删掉的,固定留在原位当占位)。我拿这一轮开始时的这串数,从左往右一对一对地看,谁的左邻比自己大,就把谁标红等着删。
- 20第 3 轮 · 第 1 对看这一对:左边绿色的基准要么是保留的邻居、要么已经标红,右边紫色是 4。左邻的值 5 比 4 大,构成下降,所以 4 这一步要被删,把它标红。注意它标红后并不马上消失,还要继续给它右边的数当比较基准。
- 21第 3 轮 · 第 2 对看这一对:左边是 4,右边紫色是 7。4 没有比 7 大,这里不是下降,7 本轮保留,不标红。继续看下一对。
- 22第 3 轮完成第 3 轮结算:刚才标红的 4 这 1 个数同时被删,一起灰掉。剩下绿色的 5, 7 是本轮的幸存者。已完成 3 轮。再看看它还降不降,降就接着来下一轮。
- 23第 4 轮 · 扫描相邻对第 4 轮开始。当前还活着的是 5, 7 这 2 个数(灰色的是前几轮已经删掉的,固定留在原位当占位)。我拿这一轮开始时的这串数,从左往右一对一对地看,谁的左邻比自己大,就把谁标红等着删。
- 24第 4 轮 · 第 1 对看这一对:左边是 5,右边紫色是 7。5 没有比 7 大,这里不是下降,7 本轮保留,不标红。继续看下一对。
- 25结束 · 答案 = 3这一轮从头扫到尾,没有任何一对是左大右小,一个红都没标出来。说明剩下的 5, 7 已经是非递减了,操作到此为止。回头数一数,真正发生删除的一共是 3 轮,所以答案就是 3。
⚠️ 容易写错的地方
✗ 错:一轮里删一个就立刻更新数组、再接着判定
✓ 对:一轮基于本轮开始的数组,把所有该删的同时删
题目规定同步删除,谁该删只看本轮起始时的左邻,边删边判会漏删或多删,轮数就错了
✗ 错:直接照抄逐轮模拟提交
✓ 对:大数据必须换单调栈加 dp 的 O(n) 解
n 最大到十万,逐轮删最坏 O(n 的平方),会超时;单调栈把它压到线性
✗ 错:以为答案等于被删元素的总个数
✓ 对:答案是删除的轮数,不是被删个数
一轮能同时删掉很多个,要数的是执行了几步(几轮),不是删了多少数
✗ 错:dp[i] 理解成 i 右边比它小的元素个数
✓ 对:dp[i] 是 i 把够得着的小数全吃完花的轮数
被吃的小数如果自己还带着「消化时间」,会把 i 的完成时刻往后推,取的是较大值不是简单计数
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def totalSteps(self, nums: List[int]) -> int:
stk = []
ans, n = 0, len(nums)
dp = [0] * n
for i in range(n - 1, -1, -1):
while stk and nums[i] > nums[stk[-1]]:
dp[i] = max(dp[i] + 1, dp[stk.pop()])
stk.append(i)
return max(dp)C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int totalSteps(vector<int>& nums) {
stack<int> stk;
int ans = 0, n = nums.size();
vector<int> dp(n);
for (int i = n - 1; i >= 0; --i) {
while (!stk.empty() && nums[i] > nums[stk.top()]) {
dp[i] = max(dp[i] + 1, dp[stk.top()]);
ans = max(ans, dp[i]);
stk.pop();
}
stk.push(i);
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int totalSteps(int[] nums) {
Deque<Integer> stk = new ArrayDeque<>();
int ans = 0;
int n = nums.length;
int[] dp = new int[n];
for (int i = n - 1; i >= 0; --i) {
while (!stk.isEmpty() && nums[i] > nums[stk.peek()]) {
dp[i] = Math.max(dp[i] + 1, dp[stk.pop()]);
ans = Math.max(ans, dp[i]);
}
stk.push(i);
}
return ans;
}
}复杂度
时间
O(n)
单调栈解法里每个下标只入栈一次、出栈一次,遍历一遍即得答案。动画演示的逐轮删除是理解用的暴力法,最坏要 O(n) 轮、每轮 O(n),是 O(n 的平方),数据大时会超时,所以正解用单调栈
空间
O(n)
按峰值算。一个下标栈最坏装下接近 n 个元素(整个数组从左到右非递减、递增时,右往左遍历时前面的小数吃不掉更大的栈顶,于是全留在栈里),再加一个长度 n 的 dp 数组,合起来是 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 使数组按非递减顺序排列 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
最直接的暴力怎么做,为什么会超时?+
就是动画演示的逐轮模拟:每一轮扫一遍数组,把所有左邻比自己大的元素同时删掉,直到某一轮没得删。每轮 O(n),最坏要 O(n) 轮(比如一个大数排最前、后面一路小数,每轮只能吃掉一个),整体 O(n 的平方)。n 到十万时平方就是百亿级,必然超时,所以要优化。
单调栈解法为什么从右往左遍历?+
一个元素会在第几轮被删,取决于它左边第一个比它大、还没被吃掉的元素什么时候够到它。从右往左扫,用一个栈维护那些「还在等着被左边更大的数吃掉」的较小元素;遇到一个较大的 nums[i],就把栈里比它小的逐个弹出并累计吃掉它们要花的轮数,正好把每个元素的删除时刻算准。
答案到底取的是什么?+
取的是 dp 数组的最大值。dp[i] 是元素 i 把右边够得着的小数全吃完所花的轮数,整个数组要等到最能吃、吃得最久的那个元素也停手,才彻底不再变化。所以总步数就是所有 dp[i] 里的最大值。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 使数组按非递减顺序排列 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。