春招打卡 | 串联所有单词的子串

132 阅读2分钟

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

题目描述

题目来自力扣,描述如下:

给定一个字符串 s 和一些 长度相同 的单词 words 。找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。

注意子串要与 words 中的单词完全匹配,中间不能有其他字符 ,但不需要考虑 words 中单词串联的顺序。

示例 1

输入:s = "barfoothefoobarman", words = ["foo","bar"]
输出:[0,9]
解释:
从索引 0 和 9 开始的子串分别是 "barfoo" 和 "foobar" 。
输出的顺序不重要, [9,0] 也是有效答案。

示例 2

输入:s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"]
输出:[]

思路分析

本题采用滑动窗口+哈希表的解法实现,虽然不是最佳解法,但这种解法相对简单。

主要思路,先对特殊情况进行判断:

  • 长度为 0 的 words,直接返回空切片。
  • 其他情况,描述如下:

已知 words 中的单词长度都一样,我们可以先计算单词的总长度(单词个数 * 一个单词长度)记为 allWordLen

然后在 s 上,逐个取 allWordLen 个长度的子字符串,有了“子字符串”,我们接下来需要逐个匹配 words 中的单词是否都在其中,由于题目要求:单词完全匹配,中间不能有其他字符。 所以匹配过的字符不能再用于比较。我们先把 words 中的单词都存在一个 map 中(记为 map1),为了防止同一个单词出现多次的情况,我们对 map 的单词计数,因为类型为:map[string]int

在每次比较“子字符串”时,会有一个新的 map 存储此次单词的匹配情况(记为 map2),从“子字符串”上截取一个单词长度,存入 map2 并计数,如果这个单词也在 map1 中,那么继续遍历,下一次比较的 offset 是 oneLen(即一个单词长度)。 如果不在,则跳过这个“子字符串”,继续取下一次的“子字符串”来比较。直到顺序取完所有的“子字符串”。

AC 代码

func findSubstring(s string, words []string) []int {
   res := make([]int, 0)
   wordNum := len(words)
   if wordNum < 1 {
      return res
   }
   oneLen := len(words[0])
   allWordLen := len(words) * oneLen
   // map1 存储所有的 words
   map1 := make(map[string]int, len(words))
   for _, word := range words {
      map1[word] += 1
   }
   // 可以利用滑动窗口比较的次数
   cmpNum := len(s) - allWordLen + 1
   for i := 0; i < cmpNum; i++ {
      cmpStr := s[i : allWordLen+i]
      tmpNum := 0
      hasWordMap := make(map[string]int, 0)
      // 比较单词是否是否都能匹配上
      for j := 0; j < len(cmpStr)-oneLen+1; j += oneLen {
         tmpOneWord := cmpStr[j : oneLen+j]
         if _, ok := map1[tmpOneWord]; ok {
            hasWordMap[tmpOneWord] += 1
            if hasWordMap[tmpOneWord] > map1[tmpOneWord] {
               break
            }
         } else {
            break
         }
         tmpNum += 1
      }
      if tmpNum == wordNum {
         res = append(res, i)
      }
   }

   return res
}


func TestFindSubstring(t *testing.T) {
   got := findSubstring("barfoothefoobarman", []string{"foo", "bar"})
   expected := []int{0, 9}
   if !reflect.DeepEqual(got, expected) {
      t.Error("err 01")
      return
   }
   t.Log("--end--")
}

总结

解题时如果用的上 map 结构,是一种很好的辅助工具,因为 map 的存取复杂度是常数级,效率很高。

复杂度分析

  • 循环遍历获取“子字符串”,循环长度依 s 的长度(记为 m)而定;第二层的单词匹配,根据 words 的长度(记为 n)而定。所以时间复杂度是 O(m*n)
  • 空间复杂度:map 的存储空间由 words 的长度而定,因此为 O(n)