具体描述
钢铁洪流与黎明之歌 这是一个关于挣扎、反抗与重塑的世界。 在曾经被宏伟的“统一帝国”所统治的土地上,如今只剩下破碎的城邦和星罗棋布的独立领地。帝国,这个以秩序与进步为名,却以铁腕与压迫为实的庞大机器,在一次不为人知的灾难后轰然崩塌,留下了遍地疮痍和无尽的迷茫。权力真空催生了无数割据势力,他们或继承帝国的残余,或凭借武力崛起,在这片失落的土地上勾心斗角,战争从未停歇。 故事的开端,我们跟随的是来自北方边陲小城“埃兰”的青年,艾伦·维克多。埃兰,一个被遗忘在帝国边缘的角落,却因其丰富的矿藏和战略地理位置,成为了各方势力觊觎的焦点。艾伦,一个在贫困中长大的孤儿,靠着一把锈迹斑斑的旧刀和惊人的战斗直觉勉强度日。他的生活,充leetcode.com/problems/next-permutation/ 一、 核心思路:逆序寻找与局部翻转 “下一个排列”问题的核心在于,我们需要找到当前排列序列的“下一个”字典序上的排列。这就像我们在对数字进行排序一样,比如 123 的下一个是 132,132 的下一个是 213。 要实现这一点,我们不能简单地对整个序列进行排序,因为那样只会得到最小的排列。我们需要找到一个方法,在保持序列整体“向前”推进的前提下,进行最小的改变。 思考一下,当我们找到一个排列时,如果它已经是字典序上最大的排列(例如 321),那么就不存在“下一个”排列了。反之,只要不是最大的排列,就一定存在下一个。 关键在于,我们如何找到这个“下一个”? 1. 从右往左寻找第一个“下降”的元素: 我们需要找到序列中从右往左第一个满足 `nums[i] < nums[i+1]` 的位置 `i`。 为什么是下降? 因为从右往左,如果 `nums[i] >= nums[i+1]`,意味着这一段序列(`nums[i]` 及其右侧)已经是降序排列了。降序排列是当前数字段字典序上最大的排列。要找到下一个更大的排列,我们必须改变 `nums[i]` 或者其左侧的某个元素。 找到 `i` 的意义: `nums[i]` 是我们需要“替换”的元素,以使得整体序列变大。而 `nums[i+1]` 及之后的所有元素,由于是从右往左第一个下降的位置,因此 `nums[i+1]` 及其右侧的子序列 (`nums[i+1:]`) 本身是降序排列的。 2. 如果找不到这样的 `i` (即整个序列是降序的): 这意味着当前排列是所有可能排列中字典序最大的。根据题目要求,此时应该将序列重排为字典序最小的排列,也就是升序排列。这可以通过反转整个序列来实现。 3. 从右往左寻找第一个比 `nums[i]` 大的元素: 在找到 `i` 之后,我们从 `i+1` 的位置开始,从右往左寻找第一个 `nums[j]` 满足 `nums[j] > nums[i]`。 为什么是比 `nums[i]` 大? 我们要将 `nums[i]` 替换成一个比它大的数,以使得整体排列变大。同时,为了找到“下一个”排列,我们希望这个替换的数尽可能小,以便于后续部分能够重排成最小。 为什么从右往左找? 因为 `nums[i+1:]` 是降序的,从右往左找到的第一个比 `nums[i]` 大的数,就是 `nums[i+1:]` 中比 `nums[i]` 小的数中最大的一个。这确保了我们找到了一个最小的替换,能够产生下一个排列。 4. 交换 `nums[i]` 和 `nums[j]`: 将找到的 `nums[i]` 和 `nums[j]` 进行交换。此时,`nums[i]` 的位置已经变成了一个更大的数,而 `nums[j]` 原来的位置被 `nums[i]` 占据。 5. 反转 `nums[i+1:]` 子序列: 在交换之后,`nums[i]` 的位置上的数已经增大,但 `nums[i+1]` 及其右侧的子序列,尽管交换了 `nums[j]`,但由于 `nums[i+1:]` 原本是降序排列的,交换后这个子序列仍然是降序的(或者说,是比交换前“略微”大一些,但仍然是该段数字的较大排列)。为了得到“下一个”排列,我们希望 `nums[i+1:]` 这一部分尽可能小,也就是升序排列。因此,我们需要将 `nums[i+1:]` 这个子序列进行反转。 为什么反转? 因为在交换 `nums[i]` 和 `nums[j]` 之前,`nums[i+1:]` 是降序排列的。交换后,`nums[i+1]` 位置上的数(原 `nums[j]`) 保持了其“较大的”特性,而 `nums[i+1:]` 中的其他元素(包括原 `nums[i]` 到了 `nums[j]` 的位置)依然保持了原有的相对大小关系,但整体上仍然是倾向于较大的排列。通过将 `nums[i+1:]` 反转,我们可以将其变为升序排列,从而得到字典序上最小的下一个排列。 总结一下算法步骤: 1. 从数组的右侧开始,找到第一个不满足 `nums[i] < nums[i+1]` 的索引 `i`。 2. 如果不存在这样的 `i`,说明数组已经按照降序排列,是最大的排列。将整个数组反转,得到最小的排列,然后结束。 3. 从数组的右侧开始,找到第一个满足 `nums[j] > nums[i]` 的索引 `j`。 4. 交换 `nums[i]` 和 `nums[j]`。 5. 反转从 `i+1` 到数组末尾的所有元素。 举例说明: 输入: `[1, 2, 3]` 1. 从右往左找 `i`: `3 > 2`,不满足 `nums[i] < nums[i+1]`。 `2 > 1`,不满足 `nums[i] < nums[i+1]`。 `i` 最终找到 `1` (索引为 0),满足 `nums[0] < nums[1]` (1 < 2)。所以 `i = 0`。 2. 从右往左找 `j` 满足 `nums[j] > nums[i]` (即 `nums[j] > 1`): `nums[2] = 3`,`3 > 1`。所以 `j = 2`。 3. 交换 `nums[i]` 和 `nums[j]`:交换 `nums[0]` (1) 和 `nums[2]` (3)。数组变为 `[3, 2, 1]`。 4. 反转 `nums[i+1:]` (即 `nums[1:]`):反转 `[2, 1]`。变为 `[1, 2]`。 最终结果:`[3, 1, 2]`。 输入: `[3, 2, 1]` 1. 从右往左找 `i`: `1 < 2`,不满足 `nums[i] < nums[i+1]`。 `2 < 3`,不满足 `nums[i] < nums[i+1]`。 没有找到满足 `nums[i] < nums[i+1]` 的 `i`。 2. 此时说明数组是降序的,是最大的排列。将整个数组反转:`[1, 2, 3]`。 最终结果:`[1, 2, 3]`。 输入: `[1, 1, 5]` 1. 从右往左找 `i`: `5 > 1`,不满足 `nums[i] < nums[i+1]`。 `1 < 1`,不满足 `nums[i] < nums[i+1]`。 `i` 最终找到 `1` (索引为 0),满足 `nums[0] < nums[1]` (1 < 1) 是 错误 的。 正确的判断是:从右往左,找到第一个 `nums[i] < nums[i+1]`。 `i = 1`: `nums[1]=1`, `nums[2]=5`. `1 < 5`. 所以 `i = 1`. 2. 从右往左找 `j` 满足 `nums[j] > nums[i]` (即 `nums[j] > 1`): `nums[2] = 5`. `5 > 1`. 所以 `j = 2`. 3. 交换 `nums[i]` 和 `nums[j]`:交换 `nums[1]` (1) 和 `nums[2]` (5)。数组变为 `[1, 5, 1]`。 4. 反转 `nums[i+1:]` (即 `nums[2:]`):反转 `[1]`。数组不变。 最终结果:`[1, 5, 1]`。 二、 代码实现 ```python from typing import List class Solution: def nextPermutation(self, nums: List[int]) -> None: """ Do not return anything, modify nums in-place instead. """ n = len(nums) 1. 从右往左寻找第一个下降点 i,使得 nums[i] < nums[i+1] i = n - 2 while i >= 0 and nums[i] >= nums[i+1]: i -= 1 2. 如果不存在这样的 i,说明整个数组是降序排列的(最大的排列), 将其反转为升序排列(最小的排列)。 if i == -1: nums.reverse() return 3. 从右往左寻找第一个比 nums[i] 大的数 j,使得 nums[j] > nums[i] j = n - 1 while nums[j] <= nums[i]: j -= 1 4. 交换 nums[i] 和 nums[j] nums[i], nums[j] = nums[j], nums[i] 5. 反转从 i+1 到数组末尾的所有元素 这是因为在交换后,nums[i+1:] 仍然是降序排列的, 将其反转会得到升序排列,从而得到最小的下一个排列。 left, right = i + 1, n - 1 while left < right: nums[left], nums[right] = nums[right], nums[left] left += 1 right -= 1 ``` 三、 复杂度分析 时间复杂度: O(n)。 第一步寻找 `i` 最多遍历整个数组一次。 第二步寻找 `j` 最多遍历数组剩余部分一次。 第三步反转子序列最多遍历数组剩余部分一半。 总体而言,每个元素最多被访问常数次,因此时间复杂度为 O(n)。 空间复杂度: O(1)。 该算法是在原地修改 `nums` 数组,没有使用额外的辅助空间。 四、 几个关键点和容易出错的地方 1. “下降点”的定义: 寻找 `i` 时,条件是 `nums[i] < nums[i+1]`。一旦找到,就找到了第一个“打破”降序的地方。 2. `i == -1` 的情况: 当整个数组已经是降序排列时,`i` 会变成 `-1`。此时需要将整个数组反转成升序。 3. 寻找 `j` 的条件: 寻找 `j` 时,条件是 `nums[j] > nums[i]`。并且需要从右往左找,以保证找到的是“刚刚好”比 `nums[i]` 大的数。 4. 反转 `i+1` 后的子序列: 这是使结果成为“下一个”字典序排列的关键。因为 `nums[i+1:]` 原本是降序排列的,交换 `nums[i]` 和 `nums[j]` 后,`nums[i+1:]` 中的元素仍然保持了一种“相对较大”的顺序。通过将其反转成升序,可以使得这一段的排列达到最小,从而保证整个数组是“下一个”最小的排列。 5. 原地修改: 题目要求原地修改 `nums`,所以不能创建新的数组来存储结果。 五、 进一步思考 回文数问题: “下一个排列”这个算法的思想,在某些需要生成特定序列或者进行组合优化的场景下可能会有所启发。 全排列生成: 这个算法是生成字典序全排列的基础。通过循环调用 `nextPermutation` 直到回到初始排列,就可以生成所有全排列。 去重问题: 如果输入数组存在重复元素,上述算法仍然能正确工作,生成的是下一个字典序排列,而不是下一个不重复的排列。例如 `[1, 1, 5]` 的下一个是 `[1, 5, 1]`。 这个算法的设计非常巧妙,充分利用了序列的局部特性来构建全局的下一个排列。理解了“下降点”和“反转”的含义,就掌握了解决这个问题的核心。