数据结构与算法之字典树和并查集

121 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 13 天,点击查看活动详情

作者: 千石
支持:点赞、收藏、评论
欢迎各位在评论区交流

前言

本文内容来自我平时学习的一些积累,如有错误,还请指正

在题目实战部分,我将代码实现和代码解释设置在了解题思路的下方,方便各位作为参考刷题

一些话

本文内容来自我平时学习的一些积累,如有错误,还请指正

在题目实战部分,我将代码实现和代码解释设置在了解题思路的下方,方便各位作为参考刷题

题目练习步骤:

  1. 给自己10分钟,读题并思考解题思路
  2. 有了思路以后开始写代码,如果在上一步骤中没有思路则停止思考并且看该题题解
  3. 在看懂题解(暂时没看懂也没关系)的思路后,背诵默写题解,直至能熟练写出来
  4. 隔一段时间,再次尝试写这道题目

前置知识

Trie树(字典树)是一种树形结构,常用于存储字符串集合,它以字符串的公共前缀为边。

基本实现: Trie树的每个节点都代表一个字符串的一个前缀,根节点代表空字符串,子节点代表父节点字符串加上一个字符,每个节点通过指针与其他节点相连。

  • 常见操作:

    1. 插入字符串:从根节点开始,依次扫描字符串的每一个字符,如果当前节点的子节点中存在该字符,就转到该子节点;如果不存在该字符的子节点,就创建一个新的子节点。
    2. 查询字符串:从根节点开始,依次扫描字符串的每一个字符,如果当前节点的子节点中不存在该字符,说明该字符串不存在;如果到达一个节点的末尾,且该节点是一个单词结尾标识,说明该字符串存在。
  • 特性:

    1. 存储效率高:Trie树以公共前缀为边,节省了大量的内存空间。
    2. 查询效率高:Trie树从根节点开始依次扫描字符,最多只需要扫描几次就能判断字符串是否存在,查询效率非常高。
    3. 支持前缀查询:Trie树可以通过查询以某个前缀为开头的所有字符串。
    4. 可以动态插入:Trie树可以动态的插入新的字符串,而不需要重建整棵树。

在实际应用中,Trie树常用于字符串搜索、字典树等场景,由于其优秀的存储和查询性能,是一种非常有效的数据结构。

题目

题目一:212. 单词搜索 II

image.png

思路:

使用字典树来存储 words 列表中的所有单词,然后对于二维字符网格 board 中的每个位置,以该位置为起点,使用DFS算法在字典树上进行搜索,如果找到一个单词,将其加入结果列表中。

  1. 先将所有单词都加入字典树中
  2. 对于每个单元格,在字典树上查询以该单元格字母为开头的单词,如果存在,则使用并查集判断该单词是否已经被遍历过,如果没有被遍历过,则从该单元格开始向其周围进行深度优先遍历,查询是否能够构成一个单词。

复杂度分析

时间复杂度分析:对于每个单词的查询时间复杂度为O(L),其中L为单词长度,并且每个单词只会被遍历一次,因此总的时间复杂度为O(sum(L))

代码实现

class Node:
    def __init__(self, wordId=-1):
        self.wordId = wordId
        self.next = collections.defaultdict(Node)
        
class Solution:
    def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
        direct = [(-1, 0), (0, 1), (1, 0), (0, -1)]
        r, c, res = len(board), len(board[0]), []
        
        root = Node()
        for i, word in enumerate(words):
            node = root
            for k in word:
                node = node.next[k]
            node.wordId = i
        
        def dfs(i, j, node):
            if i < 0 or i >= r or j < 0 or j >= c\
                or board[i][j] not in node.next:
                return
            
            tmp, board[i][j] = board[i][j], '$'
            node = node.next[tmp]
            if node.wordId != -1:
                res.append(words[node.wordId])
                node.wordId = -1
            
            for x, y in direct:
                nx, ny = x + i, y + j
                dfs(nx, ny, node)
            board[i][j] = tmp
   
        for i in range(r):
            for j in range(c):  
                dfs(i, j, root)
        return res

题目二:208. 实现 Trie (前缀树)

image.png

代码分析

class Trie:
    def __init__(self):
        self.root = {}
        self.end_of_word = "#"
        
    def insert(self, word: str) -> None:
        node = self.root
        for char in word:
            if char not in node:
                node[char] = {}
            node = node[char]
        node[self.end_of_word] = True
        
    def search(self, word: str) -> bool:
        node = self.root
        for char in word:
            if char not in node:
                return False
            node = node[char]
        return self.end_of_word in node
        
    def startsWith(self, prefix: str) -> bool:
        node = self.root
        for char in prefix:
            if char not in node:
                return False
            node = node[char]
        return True

复杂度分析

时间复杂度:

  • insert() 和 search() 方法的时间复杂度都是 O(n),其中 n 是插入的字符串的长度。
  • startsWith() 的时间复杂度为 O(n),其中 n 是前缀的长度。

空间复杂度:

  • 对于一个长度为 n 的字符串集,Trie 的空间复杂度为 O(26^n),其中 26 是字母表的大小。