Java 全排列的几种实现方法

 更新时间:2024年11月26日 11:14:50   作者:飞滕人生TYF  
本文详细介绍了Java中全排列问题的几种实现方法,包括回溯法、字典序排列法和迭代法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

全排列问题是一个经典的算法问题,指的是对一个序列中的所有元素生成不重复的排列组合。以下是全排列问题在 Java 中的详细实现和讲解。

1. 全排列问题定义

输入: 给定一个序列或数组,找出所有元素的排列。
输出: 返回所有可能的排列。

示例:

输入:[1, 2, 3]

输出:

[1, 2, 3]
[1, 3, 2]
[2, 1, 3]
[2, 3, 1]
[3, 1, 2]
[3, 2, 1]

2. 常用算法

全排列的常见实现方法:

  • 回溯法(Backtracking)
  • 字典序排列法(Lexicographic Order)
  • 迭代法(非递归实现)

3. 使用回溯法解决全排列

回溯法是一种基于递归的搜索算法,它通过不断尝试并撤销之前的选择来生成所有可能的解。

3.1 回溯法实现(基础版)

适用于数组中无重复元素的全排列。

import java.util.ArrayList;
import java.util.List;

public class Permutations {
    public static List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        boolean[] used = new boolean[nums.length]; // 标记是否使用过
        List<Integer> current = new ArrayList<>();
        backtrack(nums, used, current, result);
        return result;
    }

    private static void backtrack(int[] nums, boolean[] used, List<Integer> current, List<List<Integer>> result) {
        // 终止条件:当前排列的大小等于数组长度
        if (current.size() == nums.length) {
            result.add(new ArrayList<>(current)); // 将当前排列加入结果
            return;
        }
        // 遍历每个元素
        for (int i = 0; i < nums.length; i++) {
            if (used[i]) { // 跳过已使用的元素
                continue;
            }
            // 做选择
            current.add(nums[i]);
            used[i] = true;
            // 递归
            backtrack(nums, used, current, result);
            // 撤销选择
            current.remove(current.size() - 1);
            used[i] = false;
        }
    }

    public static void main(String[] args) {
        int[] nums = {1, 2, 3};
        List<List<Integer>> permutations = permute(nums);
        System.out.println(permutations);
    }
}

输出结果

对于输入 [1, 2, 3]

[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]

3.2 回溯法(不使用标记数组)

通过交换数组中的元素,可以避免使用标记数组。

import java.util.ArrayList;
import java.util.List;

public class PermutationsSwap {
    public static List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        backtrack(nums, 0, result);
        return result;
    }

    private static void backtrack(int[] nums, int start, List<List<Integer>> result) {
        if (start == nums.length) {
            List<Integer> permutation = new ArrayList<>();
            for (int num : nums) {
                permutation.add(num);
            }
            result.add(permutation);
            return;
        }
        for (int i = start; i < nums.length; i++) {
            swap(nums, start, i); // 交换元素
            backtrack(nums, start + 1, result);
            swap(nums, start, i); // 撤销交换
        }
    }

    private static void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

    public static void main(String[] args) {
        int[] nums = {1, 2, 3};
        List<List<Integer>> permutations = permute(nums);
        System.out.println(permutations);
    }
}

3.3 回溯法处理重复元素

如果数组中包含重复元素(例如 [1, 1, 2]),我们需要对结果去重。可以通过对数组排序并在递归时跳过重复元素来实现。

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class PermutationsWithDuplicates {
    public static List<List<Integer>> permuteUnique(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        Arrays.sort(nums); // 排序以便跳过重复元素
        boolean[] used = new boolean[nums.length];
        backtrack(nums, used, new ArrayList<>(), result);
        return result;
    }

    private static void backtrack(int[] nums, boolean[] used, List<Integer> current, List<List<Integer>> result) {
        if (current.size() == nums.length) {
            result.add(new ArrayList<>(current));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (used[i]) {
                continue;
            }
            // 跳过相邻重复的元素
            if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
                continue;
            }
            current.add(nums[i]);
            used[i] = true;
            backtrack(nums, used, current, result);
            current.remove(current.size() - 1);
            used[i] = false;
        }
    }

    public static void main(String[] args) {
        int[] nums = {1, 1, 2};
        List<List<Integer>> permutations = permuteUnique(nums);
        System.out.println(permutations);
    }
}

输出结果

对于输入[1, 1, 2]

[[1, 1, 2], [1, 2, 1], [2, 1, 1]]

4. 字典序排列法

字典序法生成全排列的步骤:

  • 找到当前排列的“最后一个升序对”。
  • 从右侧找到比升序对中较小值大的最小值,交换这两个值。
  • 将右侧部分反转。

适用于需要按字典序输出排列的情况。

import java.util.Arrays;

public class PermutationsLexicographic {
    public static void nextPermutation(int[] nums) {
        int i = nums.length - 2;
        while (i >= 0 && nums[i] >= nums[i + 1]) { // 找到升序对
            i--;
        }
        if (i >= 0) {
            int j = nums.length - 1;
            while (j >= 0 && nums[j] <= nums[i]) {
                j--;
            }
            swap(nums, i, j); // 交换
        }
        reverse(nums, i + 1); // 反转后面的部分
    }

    private static void reverse(int[] nums, int start) {
        int i = start, j = nums.length - 1;
        while (i < j) {
            swap(nums, i, j);
            i++;
            j--;
        }
    }

    private static void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

    public static void main(String[] args) {
        int[] nums = {1, 2, 3};
        System.out.println(Arrays.toString(nums));
        for (int i = 0; i < 6; i++) {
            nextPermutation(nums);
            System.out.println(Arrays.toString(nums));
        }
    }
}

5. 总结

常见方法对比

方法适用场景特点
回溯法全排列(通用)最常用,适合生成所有排列,可以处理重复元素,通过递归和剪枝实现。
交换法无重复全排列不需要额外空间标记数组,通过交换实现排列,但对重复元素需要额外处理。
字典序排列法字典序输出排列按字典序生成排列,适合序列化输出;需要对输入序列进行排序作为初始状态。
迭代法生成排列的特定场景使用循环代替递归,但实现起来较为复杂,适合排列生成问题中的迭代优化。

性能分析

  • 时间复杂度: O ( n ! ) O(n!) O(n!),每种方法
    的时间复杂度都与排列数量成正比。
  • 空间复杂度
    • 使用标记数组的回溯法: O ( n ) O(n) O(n)
    • 使用交换法: O ( 1 ) O(1) O(1)(不包括递归栈)

全排列是算法中基础而重要的问题。回溯法是最常用的解决方式,而在实际开发中,根据不同的需求可以选择合适的方法来实现高效的排列生成。

到此这篇关于Java 全排列的几种实现方法的文章就介绍到这了,更多相关Java 全排列内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • springmvc配置线程池Executor做多线程并发操作的代码实例

    springmvc配置线程池Executor做多线程并发操作的代码实例

    今天小编就为大家分享一篇关于springmvc配置线程池Executor做多线程并发操作的代码实例,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-03-03
  • MyBatis源码分析之日志记录详解

    MyBatis源码分析之日志记录详解

    这篇文章主要给大家介绍了关于MyBatis源码分析之日志记录的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用MyBatis具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2018-11-11
  • SpringBoot实现单文件与多文件上传

    SpringBoot实现单文件与多文件上传

    本次例子不基于第三方存储(如七牛云对象存储、阿里云对象存储、腾讯云对象存储等),仅基于本地存储。本文主要内容如下:公共文件存储代码;单文件上传代码;多文件上传代码
    2021-05-05
  • mybatis plus or and 的合并写法实例

    mybatis plus or and 的合并写法实例

    这篇文章主要介绍了mybatis plus or and 的合并写法实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-02-02
  • MybatisPlusException:Failed to process,Error SQL异常报错的解决办法

    MybatisPlusException:Failed to process,Error SQL异常报错的解决办法

    这篇文章主要给大家介绍了关于MybatisPlusException:Failed to process,Error SQL异常报错的解决办法,文中通过实例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2023-03-03
  • Java中Executor接口用法总结

    Java中Executor接口用法总结

    这篇文章主要介绍了Java中Executor接口用法,较为详细的总结了Executor接口的定义、创建及用法,需要的朋友可以参考下
    2015-06-06
  • 整理java读书笔记十五之java中的内部类

    整理java读书笔记十五之java中的内部类

    内部类是指在一个外部类的内部再定义一个类。类名不需要和文件夹相同。本文给大家分享java读书笔记十五之java中的内部类,对java读书笔记相关知识感兴趣的朋友一起学习吧
    2015-12-12
  • JAVA设计模式之备忘录模式原理与用法详解

    JAVA设计模式之备忘录模式原理与用法详解

    这篇文章主要介绍了JAVA设计模式之备忘录模式,简单说明了备忘录模式的概念、原理并结合实例形式分析了java备忘录模式的具体定义及使用方法,需要的朋友可以参考下
    2017-08-08
  • SpringBoot实现application配置信息加密

    SpringBoot实现application配置信息加密

    在配置文件中,我们有开发环境配置和生产环境配置,而生产环境的配置信息是需要做好防护的,避免外泄,所以本文为大家整理了application配置信息加密的方法,需要的可以参考下
    2023-07-07
  • Java实现线程同步的四种方式总结

    Java实现线程同步的四种方式总结

    Java线程同步属于Java多线程与并发编程的核心点,需要重点掌握,下面我就来详解Java线程同步的4种主要的实现方式,需要的可以参考一下
    2022-09-09

最新评论