LeetCode 556. 下一个更大元素 III
题目描述

题意分析
重新排列正整数
n的全部十进制数位,找出严格大于n的最小整数。每个数位的使用次数必须保持不变,不能新增或删除数位。“更大”与“最小”要同时满足:原数本身不算答案,任意一个更大的排列也不一定是答案。不存在更大排列,或最小的更大排列超过 32 位有符号整数上限时,都返回
-1。
解法:下一排列
核心思路
[!blue]
数位数相同时,两个整数的大小由从左到右第一个不同的数位决定。因此,要让结果刚好变大,应尽量保留高位,把必须增大的位置推到最右侧。
从右向左找到第一个满足
digits[pivot] < digits[pivot + 1]的位置。它右边的后缀已经是非递增排列,也就是这些数位能组成的最大排列;只调整后缀不可能让原数变大,所以必须增大pivot。若找不到该位置,整个数已经是最大排列,直接返回-1。确定位置后,还要让这一位增加得尽量少。后缀非递增,从右向左找到的第一个严格大于
digits[pivot]的数位,就是最小的可用替代值。交换后,后缀仍然非递增:交换位置右侧的数位都不大于旧的枢轴值,因此放回旧值不会破坏顺序。最后反转后缀,把它变成非递减排列,也就是新高位确定后的最小后缀。这样依次保证“增大位置最靠右、该位增幅最小、剩余部分最小”,得到的正是下一个更大排列。先按 64 位整数解析,再检查 32 位上限,避免范围判断之前就发生溢出。
解题步骤
- 将
n转为字符数组,从倒数第二位向左寻找digits[pivot] < digits[pivot + 1]。- 若
pivot < 0,说明全部数位非递增,没有更大排列,返回-1。- 从末位向左找第一个严格大于枢轴的数位,与枢轴交换。
- 反转
[pivot + 1, end],使这个后缀按非递减顺序排列。- 将结果解析成 64 位整数;超过 $2^{31}-1$ 时返回
-1,否则返回该整数。
代码实现
class Solution {
public int nextGreaterElement(int n) {
char[] digits = String.valueOf(n).toCharArray();
int pivot = digits.length - 2;
while (pivot >= 0 && digits[pivot] >= digits[pivot + 1]) {
pivot--;
}
if (pivot < 0) {
return -1;
}
// 后缀非递增,从右找第一个更大值,才能使高位增幅最小。
int successor = digits.length - 1;
while (digits[successor] <= digits[pivot]) {
successor--;
}
swap(digits, pivot, successor);
// 高位提升后,把后缀恢复为最小排列。
reverse(digits, pivot + 1, digits.length - 1);
long value = Long.parseLong(new String(digits));
return value > Integer.MAX_VALUE ? -1 : (int) value;
}
private void reverse(char[] digits, int left, int right) {
while (left < right) {
swap(digits, left++, right--);
}
}
private void swap(char[] digits, int i, int j) {
char temp = digits[i];
digits[i] = digits[j];
digits[j] = temp;
}
}
import "strconv"
func nextGreaterElement(n int) int {
digits := []byte(strconv.Itoa(n))
pivot := len(digits) - 2
for pivot >= 0 && digits[pivot] >= digits[pivot+1] {
pivot--
}
if pivot < 0 {
return -1
}
// 后缀非递增,从右找第一个更大值,才能使高位增幅最小。
successor := len(digits) - 1
for digits[successor] <= digits[pivot] {
successor--
}
digits[pivot], digits[successor] = digits[successor], digits[pivot]
// 高位提升后,把后缀恢复为最小排列。
for left, right := pivot+1, len(digits)-1; left < right; left, right = left+1, right-1 {
digits[left], digits[right] = digits[right], digits[left]
}
value, _ := strconv.ParseInt(string(digits), 10, 64)
if value > 1<<31-1 {
return -1
}
return int(value)
}
复杂度分析
- 时间复杂度:$O(d)$,其中 $d$ 是十进制数位数。寻找枢轴、寻找交换对象、反转和解析各扫描至多数位一次。
- 空间复杂度:$O(d)$,用于字符数组和转换后的字符串;交换与反转本身只用常数空间。
关键点总结
[!green]
- 最长非递增后缀无法通过内部重排变大,它前面的数位才是最靠右的可增大位置。
- 从后缀右端找第一个更大数位,保证枢轴只增加必要的最小幅度。
- 交换后后缀仍非递增,反转即可得到最小后缀,无需额外排序。
- 数位重排可能突破整数上限;恰好等于上限仍是合法结果。
易错点总结
[!yellow]
- 寻找枢轴时必须跳过大于或等于后一位的位置。相等不构成上升关系,不能作为增大入口。
- 交换对象必须严格大于枢轴,且在后缀中尽量小;随意选择更大数位会跳过真正的下一排列。
- 交换后不反转后缀,只能保证结果变大,不能保证它最小。
- 无枢轴时必须先返回,不能继续用负下标访问数组,也不能像循环排列问题那样返回最小排列。
- 先转为 32 位整数再判断范围,已经来不及防止溢出;应使用 64 位中间值,并用
>而非>=判断上限。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 31. 下一个排列 | 中等 | 把整数各位当作数组后,直接寻找下一字典序排列,再检查结果是否超出整数范围。 |
| 7. 整数反转 | 中等 | 同样在十进制重组时注意32位边界,原题反转全部数位,本题只求最接近的更大重排。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!