java学习 leetcode31 下一个排列
1.排列方法(按照全排列,数组,整数来回转换的思路)
package com.hmdp.leetcode;import java.util.*;
public class backtracking31 {public void nextPermutation(int[] nums) {// 1. 将当前数组转为字符串表示StringBuilder sb = new StringBuilder();for (int num : nums) {sb.append(num);}String value = sb.toString();// 2. 生成所有全排列,并存储为字符串Set<String> result = new HashSet<>();boolean[] used = new boolean[nums.length];backtrack(nums, new StringBuilder(), used, result);// 3. 排序所有排列List<String> sortedList = new ArrayList<>(result);Collections.sort(sortedList);// 4. 找到当前 value 的下一个排列for (int j = 0; j < sortedList.size(); j++) {if (sortedList.get(j).equals(value)) {int nextIndex = (j == sortedList.size() - 1) ? 0 : j + 1;String nextValue = sortedList.get(nextIndex);// 5. 写回原数组Arrays.fill(nums, 0);for (int k = 0; k < nextValue.length(); k++) {nums[k] = nextValue.charAt(k) - '0';}break;}}}// 回溯生成全排列(字符串形式)private void backtrack(int[] nums, StringBuilder path, boolean[] used, Set<String> result) {if (path.length() == nums.length) {result.add(path.toString());return;}for (int i = 0; i < nums.length; i++) {if (used[i]) continue;used[i] = true;path.append(nums[i]);backtrack(nums, path, used, result);path.deleteCharAt(path.length() - 1);used[i] = false;}}}
2.测试用例
import com.hmdp.leetcode.backtracking31;
import org.junit.jupiter.api.Test;import static org.junit.jupiter.api.Assertions.*;import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;public class SolutionTest {@Testpublic void testNextPermutation() {backtracking31 solution = new backtracking31();// Existing test casesint[] nums1 = {1, 2, 3};solution.nextPermutation(nums1);assertArrayEquals(new int[]{1, 3, 2}, nums1);int[] nums2 = {3, 2, 1};solution.nextPermutation(nums2);assertArrayEquals(new int[]{1, 2, 3}, nums2);int[] nums3 = {1, 1, 5};solution.nextPermutation(nums3);assertArrayEquals(new int[]{1, 5, 1}, nums3);int[] nums4 = {0, 0};solution.nextPermutation(nums4);assertArrayEquals(new int[]{0, 0}, nums4);int[] nums5 = {2, 2, 2};solution.nextPermutation(nums5);assertArrayEquals(new int[]{2, 2, 2}, nums5);int[] nums6 = {1, 3, 4, 2};solution.nextPermutation(nums6);assertArrayEquals(new int[]{1, 4, 2, 3}, nums6);int[] nums7 = {4, 3, 2, 1};solution.nextPermutation(nums7);assertArrayEquals(new int[]{1, 2, 3, 4}, nums7);int[] nums8 = {1, 1, 1, 1};solution.nextPermutation(nums8);assertArrayEquals(new int[]{1, 1, 1, 1}, nums8);int[] nums9 = {1, 2, 4, 3};solution.nextPermutation(nums9);assertArrayEquals(new int[]{1, 3, 2, 4}, nums9);// New test casesint[] nums10 = {0, 0, 4, 2, 1, 0};solution.nextPermutation(nums10);assertArrayEquals(new int[]{0, 1, 0, 0, 2, 4}, nums10);int[] nums11 = {6, 7, 5, 3, 5, 6, 2, 9, 1, 2, 7, 0, 9};solution.nextPermutation(nums11);assertArrayEquals(new int[]{6, 7, 5, 3, 5, 6, 2, 9, 1, 2, 9, 0, 7}, nums11);}
}
最后一个测试用例超时了,其他都是对的,就是思路好理解些
1. 溢出
✅ 问题原因:
当排列中的数字较大或排列长度较长时,直接使用整数类型(如
int
或long
)来表示排列可能会导致数值溢出。例如,在某些情况下,排列可能需要非常大的数值范围,而标准的整数类型无法容纳。
✅ 修复方式:
改用
String
表示排列:字符串可以表示任意长度和大小的排列,不会受到数值类型的限制。通过字符串处理,你可以避免数值溢出的问题。
2. 前导零丢失(这个确实有)
✅ 问题原因:
在使用整数类型表示排列时,前导零会被自动忽略。例如,排列
[0, 0, 4, 2, 1, 0]
转换为整数后会变成4210
,丢失了前导零。这会导致排列信息不完整,影响后续的排列生成和比较。
✅ 修复方式:
使用字符串处理,保留完整信息:字符串可以保留排列中的所有数字,包括前导零。通过字符串操作,你可以确保排列的完整性,避免前导零丢失的问题。
3. 效率低
✅ 问题原因:
使用回溯法或其他复杂算法生成全排列时,时间复杂度较高,尤其是在大规模输入的情况下。
回溯法的时间复杂度通常是指数级的,对于较大的输入规模,计算量会急剧增加,导致效率低下。
✅ 修复方式:
仅用于理解逻辑,不适用于大规模输入:在学习和理解排列生成的逻辑时,可以使用回溯法等方法。但在实际应用中,特别是面对大规模输入时,应选择更高效的算法或数据结构。
可以考虑使用迭代法或其他优化算法来提高效率。
4. 全排列写回错误(补上前导0,数组排序就有问题了)
✅ 问题原因:
在将全排列结果写回数组时,可能出现错误,导致排列顺序或内容不正确。
这可能是由于字符串和数组之间的转换不正确,或者在处理过程中出现了逻辑错误。
✅ 修复方式:
使用字符串逐位写回数组:在将字符串形式的排列写回数组时,逐位进行转换和赋值,确保每个字符都能正确地对应到数组中的相应位置。
例如,可以通过遍历字符串,将每个字符转换为整数,并依次写入数组中。
3.原地算法
public class Solution {public static void nextPermutation(int[] nums) {if (nums == null || nums.length <= 1) {return;}// Step 1: Find the first decreasing element from the end.int i = nums.length - 2;while (i >= 0 && nums[i] >= nums[i + 1]) {i--;}// If such an element is found, find the element to swap with.if (i >= 0) {int j = nums.length - 1;while (nums[j] <= nums[i]) {j--;}// Step 2: Swap the elements.swap(nums, i, j);}// Step 3: Reverse the sequence after the position i.reverse(nums, i + 1);}private static void swap(int[] nums, int i, int j) {int temp = nums[i];nums[i] = nums[j];nums[j] = temp;}private static void reverse(int[] nums, int start) {int end = nums.length - 1;while (start < end) {swap(nums, start, end);start++;end--;}}}
示例
假设我们有一个数组 nums = {1, 3, 5, 4, 2}
。我们的目标是找到这个排列的下一个字典序更大的排列。
步骤详解
寻找第一个降序对:
我们从后向前遍历数组,寻找第一个不符合升序的元素。
遍历顺序:
2 -> 4 -> 5
(直到遇到3
)。在位置
i=1
处发现3 < 5
,这是我们要找的第一个降序点。
寻找大于
nums[i]
的最小元素:从数组末尾开始向前查找,寻找第一个比
nums[1]=3
大的元素。查找顺序:
2 -> 4
(不满足条件),然后是4
,它比3
大,且是最接近的一个。找到索引
j=3
处的元素4
。
交换
nums[i]
和nums[j]
:交换
nums[1]
和nums[3]
,得到新的数组{1, 4, 5, 3, 2}
。
反转
i+1
到数组末尾的部分:反转
nums[2]
到nums[4]
,即{5, 3, 2}
。反转后的结果为
{2, 3, 5}
。最终数组变为
{1, 4, 2, 3, 5}
。
结果
原始数组 {1, 3, 5, 4, 2}
经过上述步骤后变成了 {1, 4, 2, 3, 5}
,这正是它的下一个字典序排列。
关键点总结
寻找降序对:从后向前找到第一个降序的位置,确保能找到一个比当前位置大的元素进行交换。
寻找交换元素:在降序部分中找到刚好大于当前元素的值进行交换,保证新排列是下一个更大的排列。
反转操作:将降序部分反转成升序,使得这部分成为最小的排列,从而保证整个数组是下一个字典序排列。