1. 题号和题目名称
- 单词搜索 II
2. 题目叙述
给定一个 m x n
二维字符网格 board
和一个单词(字符串)列表 words
,找出所有同时在二维网格和字典中出现的单词。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。
3. 模式识别
本题可以结合字典树(Trie 树)和回溯算法来解决。字典树用于高效地存储和查找单词列表,回溯算法用于在二维网格中搜索可能的单词路径。
4. 考点分析
- 字典树的构建与应用:需要构建字典树来存储单词列表,以便在网格搜索过程中快速判断是否匹配到单词。
- 回溯算法:在二维网格中进行深度优先搜索(DFS),尝试所有可能的路径,同时要注意避免重复使用单元格。
- 剪枝优化:在搜索过程中,及时剪掉不可能产生有效单词的分支,提高搜索效率。
5. 所有解法
解法一:朴素回溯
- 思路:对于每个单词,在二维网格中进行回溯搜索,检查是否能找到该单词。时间复杂度较高,因为对于每个单词都要进行一次完整的网格搜索。
解法二:字典树 + 回溯
- 思路:首先构建字典树来存储所有单词,然后在二维网格的每个单元格上启动回溯搜索,利用字典树来指导搜索过程,一旦发现当前路径无法构成有效单词,就停止搜索,避免不必要的计算。这是本题的最优解法。
6. 最优解法(字典树 + 回溯)的 C 语言代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#define ALPHABET_SIZE 26
// 定义字典树节点结构体
typedef struct TrieNode {
struct TrieNode *children[ALPHABET_SIZE]; // 子节点数组,每个元素对应一个字母
bool isEndOfWord; // 标记该节点是否为一个单词的结尾
char *word; // 存储完整的单词
} TrieNode;
// 创建一个新的字典树节点
TrieNode* createNode() {
TrieNode* node = (TrieNode*)malloc(sizeof(TrieNode));
for (int i = 0; i < ALPHABET_SIZE; i++) {
node->children[i] = NULL; // 初始化子节点数组为 NULL
}
node->isEndOfWord = false; // 初始标记不是单词结尾
node->word = NULL; // 初始时不存储单词
return node;
}
// 向字典树中添加单词
void insert(TrieNode* root, char *word) {
TrieNode* current = root;
for (int i = 0; word[i] != '\0'; i++) {
int index = word[i] - 'a'; // 计算字符对应的索引
if (current->children[index] == NULL) {
current->children[index] = createNode(); // 如果子节点不存在,创建新节点
}
current = current->children[index]; // 移动到下一个节点
}
current->isEndOfWord = true; // 标记单词结尾
current->word = strdup(word); // 存储完整的单词
}
// 回溯搜索函数
void backtrack(char** board, int row, int col, int m, int n, TrieNode* node, char ***result, int *resultSize) {
if (row < 0 || row >= m || col < 0 || col >= n || board[row][col] == '#') {
return; // 越界或已访问过,返回
}
char originalChar = board[row][col];
int index = originalChar - 'a';
TrieNode* child = node->children[index];
if (child == NULL) {
return; // 该路径无法构成有效单词,返回
}
if (child->isEndOfWord) {
// 找到一个单词
(*result)[(*resultSize)++] = child->word;
child->isEndOfWord = false; // 避免重复添加
}
board[row][col] = '#'; // 标记为已访问
// 向四个方向进行搜索
backtrack(board, row - 1, col, m, n, child, result, resultSize); // 上
backtrack(board, row + 1, col, m, n, child, result, resultSize); // 下
backtrack(board, row, col - 1, m, n, child, result, resultSize); // 左
backtrack(board, row, col + 1, m, n, child, result, resultSize); // 右
board[row][col] = originalChar; // 恢复原始字符
}
// 主函数,查找所有匹配的单词
char **findWords(char** board, int boardSize, int* boardColSize, char **words, int wordsSize, int* returnSize) {
TrieNode* root = createNode();
// 构建字典树
for (int i = 0; i < wordsSize; i++) {
insert(root, words[i]);
}
char **result = (char**)malloc(wordsSize * sizeof(char*));
*returnSize = 0;
// 在二维网格的每个单元格上启动回溯搜索
for (int i = 0; i < boardSize; i++) {
for (int j = 0; j < boardColSize[i]; j++) {
backtrack(board, i, j, boardSize, boardColSize[i], root, &result, returnSize);
}
}
return result;
}
// 释放字典树的内存
void freeTrie(TrieNode* node) {
if (node == NULL) {
return;
}
for (int i = 0; i < ALPHABET_SIZE; i++) {
freeTrie(node->children[i]); // 递归释放子节点
}
if (node->word != NULL) {
free(node->word); // 释放存储的单词
}
free(node); // 释放当前节点
}
// 以下是测试代码示例
int main() {
char *board[] = {"oaan", "etae", "ihkr", "iflv"};
int boardSize = 4;
int boardColSize[] = {4, 4, 4, 4};
char *words[] = {"oath", "pea", "eat", "rain"};
int wordsSize = 4;
int returnSize;
char **result = findWords(board, boardSize, boardColSize, words, wordsSize, &returnSize);
for (int i = 0; i < returnSize; i++) {
printf("%s\n", result[i]);
}
// 释放结果数组的内存
for (int i = 0; i < returnSize; i++) {
free(result[i]);
}
free(result);
return 0;
}
7. 复杂度分析
- 时间复杂度: O ( m ∗ n ∗ 3 L ) O(m * n * 3^L) O(m∗n∗3L),其中 m m m 和 n n n 分别是二维网格的行数和列数, L L L 是单词的最大长度。在最坏情况下,对于每个单元格,都有 3 个方向可以继续搜索(因为不能回到上一个单元格),并且需要对每个单元格启动搜索。
- 空间复杂度: O ( k ∗ L ) O(k * L) O(k∗L),其中 k k k 是单词的数量, L L L 是单词的最大长度。主要用于存储字典树的节点。