目录
引言
当解决字符串排列问题时,递归回溯算法是一种常见且有效的方法。尤其在处理包含重复字符的字符串时,避免生成重复排列成为一个关键考虑。本博客将介绍一个针对这一问题的优化算法,并解释其与传统深度优先搜索(DFS)回溯方法相比新增的内容。目的是为了题解更模板化。
题目链接:字符串的排列_牛客题霸_牛客网 (nowcoder.com)
话不多说先上代码:
import java.util.*;
public class Solution {
/**
* 生成字符串的所有排列
*
* @param str 输入的字符串
* @return 所有排列的列表
*/
public ArrayList<String> Permutation(String str) {
ArrayList<String> permutations = new ArrayList<>();
if (str == null || str.length() == 0) {
return permutations;
}
char[] chars = str.toCharArray();
Arrays.sort(chars); // 对字符数组排序,以便检测重复字符
boolean[] used = new boolean[str.length()]; // 标记字符是否被使用过
StringBuilder currentPermutation = new StringBuilder(); // 当前的排列
dfs(chars, used, currentPermutation, permutations);
return permutations;
}
/**
* 递归方法来生成排列
*
* @param chars 原始字符数组
* @param used 标记数组,标记字符是否被使用
* @param current 当前生成的排列
* @param permutations 保存所有生成的排列
*/
private void dfs(char[] chars, boolean[] used,
StringBuilder current, ArrayList<String> permutations) {
if (current.length() == chars.length) {
permutations.add(current.toString());
return;
}
for (int i = 0; i < chars.length; i++) {
if (!used[i]) {
// 检查重复字符
if (i > 0 && chars[i] == chars[i - 1] && !used[i - 1]) {
continue;
}
used[i] = true;
current.append(chars[i]);
dfs(chars, used, current, permutations);
// 回溯
current.deleteCharAt(current.length() - 1);
used[i] = false;
}
}
}
}
官方题解代码
import java.util.*;
public class Solution {
public ArrayList<String> Permutation(String str) {
ArrayList<String> res = new ArrayList<String>();
if(str == null || str.length() == 0)
return res;
//转字符数组
char[] charStr = str.toCharArray();
// 按字典序排序
Arrays.sort(charStr);
boolean[] vis = new boolean[str.length()];
//标记每个位置的字符是否被使用过
Arrays.fill(vis, false);
StringBuffer temp = new StringBuffer();
//递归获取
recursion(res, charStr, temp, vis);
return res;
}
public void recursion(ArrayList<String> res, char[] str,
StringBuffer temp, boolean[] vis){
//临时字符串满了加入输出
if(temp.length() == str.length){
res.add(new String(temp));
return;
}
//遍历所有元素选取一个加入
for(int i = 0; i < str.length; i++){
//如果该元素已经被加入了则不需要再加入了
if(vis[i])
continue;
if(i > 0 && str[i - 1] == str[i] && !vis[i - 1])
//当前的元素str[i]与同一层的前一个元素str[i-1]相同且str[i-1]已经用过了
continue;
//标记为使用过
vis[i] = true;
//加入临时字符串
temp.append(str[i]);
recursion(res, str, temp, vis);
//回溯
vis[i] = false;
temp.deleteCharAt(temp.length() - 1);
}
}
}
优化算法特点
这一优化算法的核心在于有效处理重复字符,以确保每个排列都是唯一的。其主要特点包括:
-
字符排序:首先对字符数组进行排序,使得相同的字符相邻,便于后续的重复检测。
-
重复字符检测:在递归过程中,通过检查相邻字符来避免重复。这是优化算法与传统DFS回溯的主要区别。
-
递归回溯框架:沿用了传统DFS回溯的框架,通过递归探索所有可能的排列组合。
-
效率和简洁:代码通过使用基本的数据结构并遵循清晰的逻辑,确保了高效性和易读性。
官方题解与本解法的比较
官方题解采用了一个更为复杂的方法,涉及多个数据结构和条件判断。与之相比,本文的解法有以下不同:
-
数据结构简化:本解法避免使用复杂的数据结构,如
StringBuffer
,从而简化代码。 -
增加了对重复字符的处理:本文解法通过对字符数组排序和逻辑检查避免了重复排列,而官方题解缺乏这种处理。
-
代码可读性更好:本解法的代码结构更清晰,逻辑更直观。
新增内容解析
与传统的DFS回溯相比,这种优化算法主要新增了对重复字符的处理逻辑:
1. 排序:
通过对输入字符串的字符进行排序,我们将相同的字符放置在一起。这是处理重复字符的前提步骤。排序后,相同的字符会被排列在一起,这使得我们可以通过比较相邻的字符来检测重复(判重)。
Arrays.sort(chars); // 对字符数组排序
2. 判重逻辑:在每次递归选择字符加入排列时,检查当前字符是否与前一个字符相同,且前一个字符是否已被使用。如果满足这些条件,我们跳过当前字符,从而避免生成重复的排列。
if (i > 0 && chars[i] == chars[i - 1] && !used[i - 1]) {
continue;
}
这种方法确保即使在输入字符串包含重复字符时,也能生成所有唯一的排列组合。
选择本解法的理由
选择这种解法的主要理由在于其能够有效处理输入字符串中的重复字符问题。在很多实际情况下,输入数据可能包含重复元素,因此一个能够识别并避免生成重复结果的算法显得尤为重要。此外,本文的解法在保持代码简洁性和易读性的同时,提供了高效的性能表现。
传统dfs框架:
"传统的DFS(深度优先搜索)框架" 指的是一种常用的递归方法,用于遍历或搜索树、图结构中的节点。在解决诸如路径寻找、排列组合等问题时,这种框架被广泛使用。其基本思想是从一个起点出发,探索尽可能深的分支,直到满足条件或达到叶子节点,然后回溯到上一个分支点继续探索其他可能的路径。
在字符串排列的问题中,这个框架可以被具体描述为:
-
初始化状态:通常包括选择列表(在字符串排列问题中是剩余未选择的字符)、路径记录(已经选择的字符序列)以及一些用于记录状态的变量(如布尔数组来标记字符是否被使用过)。
-
递归函数定义:定义一个递归函数来实现DFS,该函数通常包含当前选择状态的参数。
-
递归终止条件:当达到一个明确的结束点时(如在字符串排列问题中已选择字符数等于原字符串长度),记录或输出当前路径,然后返回。
-
遍历所有选择:在函数内部,遍历每个可用的选择(如在字符串排列问题中是所有未被选择的字符)。
-
做出选择:将当前的选择添加到路径中,并更改状态,如标记字符已被使用。
-
递归调用:以新的状态递归调用自身函数。
-
撤销选择(回溯):在返回前,撤销当前的选择,并恢复状态,以便进行下一个选择的探索。
这个框架的关键在于递归调用和回溯。通过递归,算法深入到每一条可能的路径,而通过回溯,它能够撤销之前的选择,回到上一个状态,从而探索新的可能性。这种方法非常适合处理需要遍历所有可能情况的问题,如排列、组合、括号生成等。
题目变形
这里引用acwing的一道题做示例
import java.io.*;
class Main{
static final int N = 10;
static int[] path = new int[N];
static boolean[] st = new boolean[N];
public static void main(String[] args)throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());
dfs(0,n);
}
private static void dfs(int u, int n) {
if (u == n) {
for(int i = 0; i < n; i ++) {
System.out.print(path[i] + " ");
}
System.out.println();
return ;
}
for(int i = 1; i <= n; ++i) {
if(!st[i]) {
path[u] = i;
st[i] = true;
dfs(u + 1, n);
st[i] = false;
}
}
}
}
输入:
3
输出:
1 2 3 1 3 2 2 1 3 2 3 1 3 1 2 3 2 1