30串联所有单词的子串

[外链图片转存中…(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. 回溯的作用:回溯的作用是撤销递归中的选择。每次递归进入下一层时,会通过交换操作改变数组的顺序;当递归返回时,通过再次交换将数组恢复到原先的顺序,从而尝试其他可能的排列。

  2. 递归与交换:递归的每一层都会处理数组的一个位置,交换不同的元素到当前位置,然后递归进入下一层。这种逐步交换和恢复的过程确保了我们能够生成所有可能的排列。

  3. 回溯关键点:回溯发生在每次递归返回时。交换是生成不同排列的方式,而回溯则是通过再次交换,将数组恢复到之前的状态,以便进行下一次排列尝试。

第二种处理排列组合的方法

二哥那里的笔记用到的方法

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:你想要添加到集合中的元素。在你的代码中,elementswords 数组。
作用

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;

}

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Xia0Mo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值