1268. 搜索推荐系统

1268. 搜索推荐系统

1268. 搜索推荐系统

难度中等

给你一个产品数组 products 和一个字符串 searchWordproducts 数组中每个产品都是一个字符串。

请你设计一个推荐系统,在依次输入单词 searchWord 的每一个字母后,推荐 products 数组中前缀与 searchWord 相同的最多三个产品。如果前缀相同的可推荐产品超过三个,请按字典序返回最小的三个。

请你以二维列表的形式,返回在输入 searchWord 每个字母后相应的推荐产品的列表。

示例 1:

输入:products = ["mobile","mouse","moneypot","monitor","mousepad"], searchWord = "mouse"
输出:[
["mobile","moneypot","monitor"],
["mobile","moneypot","monitor"],
["mouse","mousepad"],
["mouse","mousepad"],
["mouse","mousepad"]
]
解释:按字典序排序后的产品列表是 ["mobile","moneypot","monitor","mouse","mousepad"]
输入 m 和 mo,由于所有产品的前缀都相同,所以系统返回字典序最小的三个产品 ["mobile","moneypot","monitor"]
输入 mou, mous 和 mouse 后系统都返回 ["mouse","mousepad"]

示例 2:

输入:products = ["havana"], searchWord = "havana"
输出:[["havana"],["havana"],["havana"],["havana"],["havana"],["havana"]]

示例 3:

输入:products = ["bags","baggage","banner","box","cloths"], searchWord = "bags"
输出:[["baggage","bags","banner"],["baggage","bags","banner"],["baggage","bags"],["bags"]]

示例 4:

输入:products = ["havana"], searchWord = "tatiana"
输出:[[],[],[],[],[],[],[]]

提示:

  • 1 <= products.length <= 1000
  • 1 <= Σ products[i].length <= 2 * 10^4
  • products[i] 中所有的字符都是小写英文字母。
  • 1 <= searchWord.length <= 1000
  • searchWord 中所有字符都是小写英文字母。

函数签名:

func suggestedProducts(products []string, searchWord string) [][]string

分析

Trie 树

Trie 树存储所有单词,遍历 searchWord 前缀做查询。

为了在树上迅速获得单词,可以给节点加一个 word 属性,插入单词的时候,最后一个节点的 word 属性即为该单词。

const (
	letters = 26
	
	resultLimit = 3
)

func suggestedProducts(products []string, searchWord string) [][]string {
	trie := &Trie{links: make([]*Trie, letters)}
	for _, product := range products {
		trie.insert(product)
	}
	res := make([][]string, len(searchWord))
	cur := trie
	for i := range searchWord {
		index := searchWord[i] - 'a'
		if cur.links[index] == nil {
			break
		}
		cur = cur.links[index]
		res[i] = cur.getAllWords(resultLimit)
	}
	return res
}

type Trie struct {
	links []*Trie
	word  string
}

func (t *Trie) insert(word string) {
	cur := t
	for _, char := range word {
		index := char - 'a'
		if cur.links[index] == nil {
			cur.links[index] = &Trie{links: make([]*Trie, letters)}
		}
		cur = cur.links[index]
	}
	cur.word = word
}

func (t *Trie) getAllWords(limit int) []string {
	var res []string
	var dfs func(cur *Trie)
	dfs = func(cur *Trie) {
		if cur.word != "" {
			res = append(res, cur.word)
		}
		if len(res) == limit {
			return
		}
		for _, next := range cur.links {
			if len(res) == limit {
				return
			}
			if next == nil {
				continue
			}
			dfs(next)
		}
	}
	dfs(t)
	return res
}

时间复杂度:O(C+S),其中 C 指所有单词长度和,SsearchWord 的长度。

空间复杂度:O(C)

二分搜索

可以先按照字典序将所有产品排序,对于 searchWord的每个前缀,可以用二分搜索找到第一个不小于该前缀的产品,然后最多检查三个产品得到这个前缀对应的结果。

func suggestedProducts(products []string, searchWord string) [][]string {
	sort.Strings(products)
	res := make([][]string, len(searchWord))
	for i := 0; i < len(searchWord); i++ {
		prefix := searchWord[:i+1]
		index := sort.Search(len(products), func(i int) bool {
			return products[i] >= prefix
		})
		if index == len(products) {
			break
		}
		end := min(index+3, len(products))
		for k := index; k < end && strings.HasPrefix(products[k], prefix); k++ {
			res[i] = append(res[i], products[k])
		}
		products = products[index:] // 前边的部分确定可以排除,后续不再需要
	}
	return res
}

实测二分搜索要比 Trie 树的解法更快且更省内存~