一、题目描述
整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。
- 例如,
arr = [1,2,3]
,以下这些都可以视作arr
的排列:[1,2,3]
、[1,3,2]
、[3,1,2]
、[2,3,1]
。
整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。
- 例如,
arr = [1,2,3]
的下一个排列是[1,3,2]
。 - 类似地,
arr = [2,3,1]
的下一个排列是[3,1,2]
。 - 而
arr = [3,2,1]
的下一个排列是[1,2,3]
,因为[3,2,1]
不存在一个字典序更大的排列。
给你一个整数数组 nums
,找出 nums
的下一个排列。
必须 原地 修改,只允许使用额外常数空间。
示例 1:
输入:nums = [1,2,3] 输出:[1,3,2]
示例 2:
输入:nums = [3,2,1] 输出:[1,2,3]
示例 3:
输入:nums = [1,1,5] 输出:[1,5,1]
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 100
二、解题思路
- 从数组的末尾开始向前遍历,找到第一个递减的子序列的起始点。这个点就是我们需要进行交换的元素之一。
- 在找到的递减子序列之后,找到比当前元素大的最小元素。这个元素将成为交换的第二个元素。
- 交换这两个元素。
- 反转递减子序列之后的子数组,以确保它是递增的,这是为了确保新的排列是字典序更大的排列。
三、具体代码
class Solution {
public void nextPermutation(int[] nums) {
if (nums == null || nums.length <= 1) {
return; // 如果数组为空或只有一个元素,不需要改变
}
// 1. 从后向前找到第一个递减的子序列的起始点
int i = nums.length - 2;
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
// 如果i为-1,说明数组已经是递增的,需要反转整个数组
if (i == -1) {
reverse(nums, 0, nums.length - 1);
} else {
// 2. 在递减子序列之后找到比当前元素大的最小元素
int j = nums.length - 1;
while (nums[j] <= nums[i]) {
j--;
}
swap(nums, i, j); // 交换这两个元素
// 3. 反转递减子序列之后的子数组
reverse(nums, i + 1, nums.length - 1);
}
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
private void reverse(int[] nums, int start, int end) {
while (start < end) {
swap(nums, start, end);
start++;
end--;
}
}
}
四、时间复杂度和空间复杂度
1. 时间复杂度
- 在最坏的情况下,我们需要遍历整个数组来找到第一个递减的子序列的起始点,这需要 O(n) 时间,其中 n 是数组的长度。
- 接下来,我们可能需要再次遍历数组来找到比当前元素大的最小元素,这同样需要 O(n) 时间。
- 最后,我们可能需要反转数组的一部分,这部分的时间复杂度是 O(n),但因为这部分只涉及数组的后半部分,所以实际上这部分的时间复杂度是 O(n/2)。
- 综合考虑,最坏情况下的总时间复杂度是 O(n) + O(n) + O(n/2) = O(n)。
- 这是因为在实际应用中,我们通常不会执行所有的操作,而且数组的反转操作不会每次都发生。
- 因此,我们可以认为这段代码的平均时间复杂度是 O(n)。
2. 空间复杂度
- 这段代码使用了额外的常数空间来存储临时变量(例如,在
swap
方法中交换元素时使用的temp
变量)。 swap
和reverse
方法都是原地操作,没有使用额外的数组或数据结构。- 因此,空间复杂度是 O(1),即常数空间复杂度。
五、总结知识点
-
数组操作:代码处理的是一个整数数组,涉及到数组的遍历、元素交换和反转等基本操作。
-
原地算法:整个
nextPermutation
方法是在原数组上进行操作的,没有使用额外的数组来存储中间结果。这是算法设计中的一个重要概念,可以节省空间。 -
递增和递减序列:代码中判断数组是否已经是递增序列,这是通过比较相邻元素的大小来实现的。如果数组已经是递增的,那么它的下一个排列就是递减序列。
-
交换操作:
swap
方法用于交换数组中的两个元素。这是原地排序和排列操作中常用的技巧。 -
反转数组:
reverse
方法用于反转数组的一部分。在找到下一个排列的过程中,如果需要将递增序列转换为递减序列,就需要反转数组的一部分。 -
循环控制:代码中使用了
while
循环来控制遍历数组的过程,这是处理序列问题时常见的控制结构。 -
条件判断:代码中使用了
if
语句来进行条件判断,例如判断数组是否需要反转,以及寻找合适的元素进行交换。 -
递归思想:虽然这段代码没有直接使用递归,但是理解递归对于解决排列和组合问题很有帮助。在某些情况下,可以通过递归的方式来生成所有可能的排列。
-
边界条件处理:在处理数组时,需要考虑边界条件,例如数组为空或只有一个元素的情况,以及在反转数组时的起始和结束索引。
以上就是解决这个问题的详细步骤,希望能够为各位提供启发和帮助。