[外链图片转存中…(img-zSSPmsd4-1726034057397)]
暴力破解匹配
感觉就不行知道吧,这个题目巨复杂
一个是数组里面的长度,二是数组里面的字符串的长度
然后还有原串的长度
首先按照正常思路就是对words数组里的字符串进行排列组合
Ann
[外链图片转存中…(img-QwCf0eVu-1726034057398)]
再看这里的length
你看啊五千种可能的排列组合,一看就没戏
况且排列组合怎么个排列组合法呢?
怎么实现排列组合呢?
我想到的是通过
通过每一种组合的return 递归试试?
这个过程用List<String>
1 2 3
3 1 2
2 3 1
好吧这个自动化实现确实有点复杂
return List
这个地方用了回溯法
回溯的核心在于,当我们尝试所有可能的排列组合时,递归会不断深入,但在完成某条路径的探索后,需要“返回”到之前的状态,继续尝试其他未探索的路径。回溯的过程实际上是通过交换操作来“撤销”之前的选择,让数组恢复到上一层递归时的状态,这样我们才能继续进行其他可能的排列尝试。
回溯的概念:
在排列生成问题中,我们会逐步交换数组中的元素,形成不同的排列。当某一条路径递归到底(即生成了一个完整的排列)时,我们要回到上一级递归的状态,然后换另外一个元素进行排列。这就是“回溯”的过程。通过回溯,我们可以尝试每一个可能的组合。
回溯过程的详细解释:
为了更好地理解回溯过程,我将对下面的代码片段做详细讲解:
public static void permute(String[] words, int start, List<String> result) {
if (start == words.length - 1) {
// 如果到达最后一个元素,记录当前排列结果
StringBuilder sb = new StringBuilder();
for (String word : words) {
sb.append(word);
}
result.add(sb.toString());
} else {
for (int i = start; i < words.length; i++) {
swap(words, start, i); // 1. 交换当前层的元素
permute(words, start + 1, result); // 2. 递归调用处理下一层
swap(words, start, i); // 3. 回溯,撤销交换,恢复状态
}
}
}
public static void swap(String[] words, int i, int j) {
String temp = words[i];
words[i] = words[j];
words[j] = temp;
}
假设输入为words = {"ab", "cd", "ef"}
,让我们逐步跟踪递归和回溯的过程。
步骤 1: 第一层递归 (start = 0
)
- 初始状态:
words = {"ab", "cd", "ef"}
i = 0
,交换words[0]
和words[0]
(即没有变化),递归进入下一层。
步骤 2: 第二层递归 (start = 1
)
- 现在
start = 1
,仍保持words = {"ab", "cd", "ef"}
i = 1
,交换words[1]
和words[1]
(即没有变化),递归进入下一层。
步骤 3: 第三层递归 (start = 2
)
- 现在
start = 2
,仍保持words = {"ab", "cd", "ef"}
- 由于
start == words.length - 1
,生成一个排列:"abcdef"
,将其添加到结果列表中。
回溯步骤 1:
- 返回到第二层递归 (
start = 1
) - 回到
i = 1
时的状态,恢复原始顺序。这里没有交换操作,因为words[1]
和words[1]
本来就没变动。
第二层递归继续 (start = 1
):
i = 2
,交换words[1]
和words[2]
,现在数组变为{"ab", "ef", "cd"}
- 递归进入下一层处理。
第三层递归 (start = 2
):
- 生成一个排列:
"abefcd"
,将其添加到结果列表中。
回溯步骤 2:
- 现在我们回到第二层递归 (
start = 1
)。为了回溯,我们需要将之前交换的words[1]
和words[2]
再交换回来,以便恢复到之前的状态。 - 交换
words[1]
和words[2]
,恢复为{"ab", "cd", "ef"}
。
回溯到第一层递归 (start = 0
):
- 第一层递归继续,
i = 1
,交换words[0]
和words[1]
,现在数组变为{"cd", "ab", "ef"}
。 - 递归进入下一层。
第二层递归 (start = 1
):
- 现在
words = {"cd", "ab", "ef"}
,i = 1
,交换words[1]
和words[1]
(没有变化),递归进入下一层。
第三层递归 (start = 2
):
- 生成排列:
"cdabef"
,加入到结果列表。
回溯步骤 3:
- 回到第二层递归,继续尝试其他组合。
- 交换
words[1]
和words[2]
,得到{"cd", "ef", "ab"}
,递归处理。
第三层递归 (start = 2
):
- 生成排列:
"cdefab"
,加入到结果列表。
回溯步骤 4:
- 交换
words[1]
和words[2]
恢复为{"cd", "ab", "ef"}
,并回溯到第一层。
回到第一层递归 (start = 0
):
- 继续尝试新的组合,
i = 2
,交换words[0]
和words[2]
,得到{"ef", "cd", "ab"}
。 - 递归进入下一层。
第二层递归 (start = 1
):
- 交换
words[1]
和words[1]
,递归处理,生成排列"efcdab"
。
回溯步骤 5:
- 交换
words[1]
和words[2]
,得到{"ef", "ab", "cd"}
。 - 递归生成排列
"efabcd"
,完成最后一个排列。
最终结果:
- 所有的排列都生成完毕:
["abcdef", "abefcd", "cdabef", "cdefab", "efcdab", "efabcd"]
。
总结:
-
回溯的作用:回溯的作用是撤销递归中的选择。每次递归进入下一层时,会通过交换操作改变数组的顺序;当递归返回时,通过再次交换将数组恢复到原先的顺序,从而尝试其他可能的排列。
-
递归与交换:递归的每一层都会处理数组的一个位置,交换不同的元素到当前位置,然后递归进入下一层。这种逐步交换和恢复的过程确保了我们能够生成所有可能的排列。
-
回溯关键点:回溯发生在每次递归返回时。交换是生成不同排列的方式,而回溯则是通过再次交换,将数组恢复到之前的状态,以便进行下一次排列尝试。
第二种处理排列组合的方法
二哥那里的笔记用到的方法
1. Collections2.permutations(list)
Collections2.permutations(list)
是一个外部库(通常是 Guava 库)提供的用于生成一个集合的所有排列组合的方法。让我们来详细解析它的作用及实现。
Collections2
类
Collections2
是 Google Guava 库中的一个实用工具类,提供了一些额外的集合操作方法,比如生成集合的所有子集、生成集合的所有排列组合等。permutations(list)
是该类中的一个静态方法,用于生成给定集合的所有排列。
permutations(List<T> list)
方法
这个方法返回的是给定列表 list
的所有排列,它的返回值是一个包含所有排列的集合。例如,假设你有一个列表 ["ab", "cd", "ef"]
,permutations(list)
将返回所有可能的排列,例如:
[["ab", "cd", "ef"], ["ab", "ef", "cd"], ["cd", "ab", "ef"], ["cd", "ef", "ab"], ["ef", "ab", "cd"], ["ef", "cd", "ab"]]
每个排列是一个 List<String>
,方法返回的是一个 Collection<List<T>>
。
实现方式
-
Collections2.permutations(list)
的具体实现一般采用递归或回溯算法,通过不断交换列表中的元素来生成所有的排列。-
它会从列表中的第一个元素开始,和其他元素进行交换,然后递归处理剩下的元素。
-
当生成一个完整的排列时,它会将该排列添加到结果集中,并在此之后进行回溯,将元素恢复到之前的状态,继续生成其他排列。
-
-
由于
permutations(list)
会返回一个包含所有排列的集合,外层代码(如你提供的代码)可以遍历这些排列来生成相应的结果。
举个例子:
假设 list = ["ab", "cd", "ef"]
,那么 Collections2.permutations(list)
将返回:
[
["ab", "cd", "ef"],
["ab", "ef", "cd"],
["cd", "ab", "ef"],
["cd", "ef", "ab"],
["ef", "ab", "cd"],
["ef", "cd", "ab"]
]
每一个 List<String>
都是原列表的不同排列。
注意事项
- 这个方法会生成列表的所有排列组合,因此当列表的长度较大时,它生成的排列数目会非常多(即
n!
,其中n
是列表的长度)。因此,处理较大列表时要小心,可能会导致性能问题或内存不足。
2. Collections.addAll(list, words)
Collections.addAll()
是 Java 标准库中 Collections
类的一个实用方法,它的作用是将一个数组的所有元素批量添加到一个集合中。
方法定义
public static <T> boolean addAll(Collection<? super T> c, T... elements)
- 参数:
c
:目标集合,即你想将元素添加到的集合。在你的代码中,c
就是list
。elements
:你想要添加到集合中的元素。在你的代码中,elements
是words
数组。
作用
addAll
方法会将数组中的所有元素一个接一个地添加到集合中。如果所有元素成功添加,它返回 true
。
示例
假设你有以下代码:
String[] words = {"ab", "cd", "ef"};
List<String> list = new ArrayList<>();
Collections.addAll(list, words);
words
是一个字符串数组。list
是一个空的ArrayList
,用来存储数组中的元素。Collections.addAll(list, words)
将words
数组的所有元素批量添加到list
中。
添加前:
list = []
添加后:
list = ["ab", "cd", "ef"]
好处
Collections.addAll()
比起使用 for
循环逐个添加元素,更加简洁和高效:
// 用传统方式逐个添加
for (String word : words) {
list.add(word);
}
// 使用 Collections.addAll 更简洁
Collections.addAll(list, words);
总结
Collections2.permutations(list)
:来自 Guava 库,生成一个集合的所有排列,返回一个包含所有排列的集合。每个排列是原集合元素的不同顺序。Collections.addAll(list, words)
:Java 标准库中的方法,用于将数组中的所有元素批量添加到一个集合中,简化了数组到集合的转换。
希望这些解释能帮助你更好地理解这两个方法的作用和使用场景!
class Solution {
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> res = new ArrayList<>();
if (s == null || words == null || s.length() == 0 || words.length == 0) return res;
int wordLen = words[0].length(); // 单词的长度
int wordCount = words.length; // 单词的数量
// 统计words中每个单词应该出现的次数
Map<String, Integer> wordMap = new HashMap<>();
for (String word : words) {
wordMap.put(word, wordMap.getOrDefault(word, 0) + 1);
}
// 遍历所有的可能起始位置,0 到 wordLen - 1
for (int i = 0; i < wordLen; i++) {
int left = i; // 窗口左边界
int count = 0; // 窗口中符合条件的单词数量
Map<String, Integer> windowMap = new HashMap<>(); // 窗口中单词的计数
// 移动窗口的右边界
for (int j = i; j <= s.length() - wordLen; j += wordLen) {
String word = s.substring(j, j + wordLen);
// 如果word是有效的
if (wordMap.containsKey(word)) {
windowMap.put(word, windowMap.getOrDefault(word, 0) + 1);
count++;
// 如果窗口中该单词的数量超过了应有的数量,需要调整窗口的左边界
while (windowMap.get(word) > wordMap.get(word)) {
String leftWord = s.substring(left, left + wordLen);
windowMap.put(leftWord, windowMap.get(leftWord) - 1);
count--;
left += wordLen;
}
// 如果窗口中的单词数量正好等于words中的单词数量,记录起始位置
if (count == wordCount) {
res.add(left);
// 移动左边界以寻找新的匹配
String leftWord = s.substring(left, left + wordLen);
windowMap.put(leftWord, windowMap.get(leftWord) - 1);
count--;
left += wordLen;
}
} else {
// 如果word不是有效的,则重置窗口
windowMap.clear();
count = 0;
left = j + wordLen;
}
}
}
return res;
}
}