本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。
为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。
由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。
给你两个字符串数组 queries 和 dictionary 。数组中所有单词都只包含小写英文字母,且长度都相同。
一次 编辑 中,你可以从 queries 中选择一个单词,将任意一个字母修改成任何其他字母。从 queries 中找到所有满足以下条件的字符串:不超过 两次编辑内,字符串与 dictionary 中某个字符串相同。
请你返回 queries 中的单词列表,这些单词距离 dictionary 中的单词 编辑次数 不超过 两次 。单词返回的顺序需要与 queries 中原本顺序相同。
示例 1:
java
输入:queries = ["word","note","ants","wood"], dictionary = ["wood","joke","moat"]
输出:["word","note","wood"]
解释:
- 将 "word" 中的 'r' 换成 'o' ,得到 dictionary 中的单词 "wood" 。
- 将 "note" 中的 'n' 换成 'j' 且将 't' 换成 'k' ,得到 "joke" 。
- "ants" 需要超过 2 次编辑才能得到 dictionary 中的单词。
- "wood" 不需要修改(0 次编辑),就得到 dictionary 中相同的单词。
所以我们返回 ["word","note","wood"] 。
示例 2:
java
输入:queries = ["yes"], dictionary = ["not"]
输出:[]
解释:
"yes" 需要超过 2 次编辑才能得到 "not" 。
所以我们返回空数组。
提示:
1 <= queries.length, dictionary.length <= 100n == queries[i].length == dictionary[j].length1 <= n <= 100- 所有
queries[i]和dictionary[j]都只包含小写英文字母。
方法1 暴力
对每个 q = q u e r i e s [ i ] q = queries[i] q=queries[i] ,遍历 d i c t i o n a r y dictionary dictionary 中的字符串 s s s ,判断 q , s q, s q,s 是否至多有两个位置的字母不同。
java
class Solution {
public List<String> twoEditWords(String[] queries, String[] dictionary) {
List<String> ans = new ArrayList<>();
for (String q : queries) {
for (String s : dictionary) {
int cnt = 0;
for (int i = 0; i < s.length() && cnt <= 2; i++) {
if (q.charAt(i) != s.charAt(i)) {
cnt++;
}
}
if (cnt <= 2) {
ans.add(q);
break;
}
}
}
return ans;
}
}
cpp
class Solution {
public:
vector<string> twoEditWords(vector<string>& queries, vector<string>& dictionary) {
vector<string> ans;
for (auto& q : queries) {
for (auto& s : dictionary) {
int cnt = 0;
for (int i = 0; i < s.size() && cnt <= 2; i++) {
if (q[i] != s[i]) {
cnt++;
}
}
if (cnt <= 2) {
ans.push_back(q);
break;
}
}
}
return ans;
}
};
python
class Solution:
def twoEditWords(self, queries: List[str], dictionary: List[str]) -> List[str]:
ans = []
for q in queries:
for s in dictionary:
if sum(x != y for x, y in zip(q, s)) <= 2:
ans.append(q)
break
return ans
rust
impl Solution {
pub fn two_edit_words(queries: Vec<String>, dictionary: Vec<String>) -> Vec<String> {
let mut ans = vec![];
for q in queries {
for s in &dictionary {
let mut cnt = 0;
for (a, b) in q.bytes().zip(s.bytes()) {
if a != b {
cnt += 1;
if cnt > 2 {
break;
}
}
}
if cnt <= 2 {
ans.push(q);
break;
}
}
}
ans
}
}
go
func twoEditWords(queries, dictionary []string) (ans []string) {
for _, q := range queries {
next:
for _, s := range dictionary {
cnt := 0
for i := range s {
if q[i] != s[i] {
cnt++
if cnt > 2 {
continue next
}
}
}
ans = append(ans, q)
break
}
}
return
}
js
var twoEditWords = function(queries, dictionary) {
const ans = [];
for (const q of queries) {
for (const s of dictionary) {
let cnt = 0;
for (let i = 0; i < s.length && cnt <= 2; i++) {
if (q[i] !== s[i]) {
cnt++;
}
}
if (cnt <= 2) {
ans.push(q);
break;
}
}
}
return ans;
};
c
char** twoEditWords(char** queries, int queriesSize, char** dictionary, int dictionarySize, int* returnSize) {
char** ans = malloc(queriesSize * sizeof(char*));
*returnSize = 0;
for (int i = 0; i < queriesSize; i++) {
char* q = queries[i];
for (int j = 0; j < dictionarySize; j++) {
char* s = dictionary[j];
int cnt = 0;
for (int k = 0; s[k] && cnt <= 2; k++) {
if (q[k] != s[k]) {
cnt++;
}
}
if (cnt <= 2) {
ans[(*returnSize)++] = q;
break;
}
}
}
return ans;
}
复杂度分析:
- 时间复杂度: O ( q d n ) O(qdn) O(qdn) , q q q 是 q u e r i e s queries queries 的长度, d d d 是 d i c t i o n a r y dictionary dictionary 的长度, n n n 是 q u e r i e s [ i ] queries[i] queries[i] 的长度。题目保证所有字符串长度相等。
- 空间复杂度: O ( 1 ) O(1) O(1) 。返回值不计入。
方法2 字典树
可将 d i c t i o n a r y dictionary dictionary 中所有的单词插入字典树,对每个 q u e r i e s [ i ] queries[i] queries[i] 做DFS,过程中维护修改次数 c n t cnt cnt ,从而实现字典树上【最多 2 2 2 次修改】的匹配搜索。
定义状态 d f s ( i , n o d e , c n t ) dfs(i, node, cnt) dfs(i,node,cnt) , i i i 表示当前匹配到第 i i i 个字符, n o d e node node 表示当前所在字典树的节点, c n t cnt cnt 表示已经修改 c n t cnt cnt 次。对第 i i i 个字符 q u e r i e s [ i ] queries[i] queries[i] :
- 如果字典树中存在 n o d e . c h i l d r e n [ q u e r i e s [ i ] ] node.children[queries[i]] node.children[queries[i]] ,不进行修改。下一步搜索状态 d f s ( i + 1 , n o d e . c h i l d r e n [ q u e r i e s [ i ] ] , c n t ) dfs(i + 1, node.children[queries[i]], cnt) dfs(i+1,node.children[queries[i]],cnt) 。
- 如果字典树中不存在 n o d e . c h i l d r e n [ q u e r i e s [ i ] ] node.children[queries[i]] node.children[queries[i]] ,且 c n t < 2 cnt <2 cnt<2 ,进行修改,枚举所有 c ≠ q u e r i e s [ i ] c \ne queries[i] c=queries[i] ,下一步搜索状态 d f s ( i + 1 , n o d e . c h i l d r e n [ c ] , c n t + 1 ) dfs(i + 1, node.children[c], cnt+1) dfs(i+1,node.children[c],cnt+1) 。
搜索过程中,我们可进行一些剪枝,比如某条路径找到合法解就提前终止。
java
class Solution {
static class TrieNode {
TrieNode[] child = new TrieNode[26];
boolean isEnd = false;
}
TrieNode root = new TrieNode();
void insert(String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
int idx = c - 'a';
if (node.child[idx] == null)
node.child[idx] = new TrieNode();
node = node.child[idx];
}
node.isEnd = true;
}
boolean dfs(String word, int i, TrieNode node, int cnt) {
if (cnt > 2 || node == null)
return false;
if (i == word.length())
return node.isEnd;
int idx = word.charAt(i) - 'a';
// 不修改
if (node.child[idx] != null) {
if (dfs(word, i + 1, node.child[idx], cnt))
return true;
}
// 修改
if (cnt < 2) {
for (int c = 0; c < 26; c++) {
if (c == idx)
continue;
if (node.child[c] != null) {
if (dfs(word, i + 1, node.child[c], cnt + 1))
return true;
}
}
}
return false;
}
public List<String> twoEditWords(String[] queries, String[] dictionary) {
for (String w : dictionary)
insert(w);
List<String> res = new ArrayList<>();
for (String q : queries) {
if (dfs(q, 0, root, 0)) {
res.add(q);
}
}
return res;
}
}
cpp
struct TrieNode {
TrieNode* child[26];
bool isEnd;
TrieNode() {
memset(child, 0, sizeof(child));
isEnd = false;
}
};
class Solution {
public:
TrieNode* root = new TrieNode();
void insert(string& word) {
TrieNode* node = root;
for (char c : word) {
int idx = c - 'a';
if (!node->child[idx])
node->child[idx] = new TrieNode();
node = node->child[idx];
}
node->isEnd = true;
}
bool dfs(string& word, int i, TrieNode* node, int cnt) {
if (cnt > 2)
return false;
if (!node)
return false;
if (i == word.size()) {
return node->isEnd;
}
int idx = word[i] - 'a';
// 不修改
if (node->child[idx]) {
if (dfs(word, i + 1, node->child[idx], cnt))
return true;
}
// 修改
if (cnt < 2) {
for (int c = 0; c < 26; c++) {
if (c == idx)
continue;
if (node->child[c]) {
if (dfs(word, i + 1, node->child[c], cnt + 1))
return true;
}
}
}
return false;
}
vector<string> twoEditWords(vector<string>& queries,
vector<string>& dictionary) {
for (auto& w : dictionary)
insert(w);
vector<string> res;
for (auto& q : queries) {
if (dfs(q, 0, root, 0)) {
res.push_back(q);
}
}
return res;
}
};
python
class TrieNode:
def __init__(self):
self.child = [None] * 26
self.isEnd = False
class Solution:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
node = self.root
for c in word:
idx = ord(c) - ord('a')
if not node.child[idx]:
node.child[idx] = TrieNode()
node = node.child[idx]
node.isEnd = True
def dfs(self, word, i, node, cnt):
if cnt > 2 or not node:
return False
if i == len(word):
return node.isEnd
idx = ord(word[i]) - ord('a')
# 不修改
if node.child[idx] and self.dfs(word, i + 1, node.child[idx], cnt):
return True
# 修改
if cnt < 2:
for c in range(26):
if c == idx:
continue
if node.child[c] and self.dfs(word, i + 1, node.child[c], cnt + 1):
return True
return False
def twoEditWords(self, queries, dictionary):
for w in dictionary:
self.insert(w)
res = []
for q in queries:
if self.dfs(q, 0, self.root, 0):
res.append(q)
return res
rust
struct TrieNode {
child: Vec<Option<Box<TrieNode>>>,
is_end: bool,
}
impl TrieNode {
fn new() -> Self {
let mut child = Vec::with_capacity(26);
for _ in 0..26 {
child.push(None);
}
Self {
child,
is_end: false,
}
}
}
impl Solution {
fn insert(root: &mut TrieNode, word: &str) {
let mut node = root;
for c in word.chars() {
let idx = (c as u8 - b'a') as usize;
if node.child[idx].is_none() {
node.child[idx] = Some(Box::new(TrieNode::new()));
}
node = node.child[idx].as_mut().unwrap();
}
node.is_end = true;
}
fn dfs(word: &[u8], i: usize, node: &TrieNode, cnt: i32) -> bool {
if cnt > 2 {
return false;
}
if i == word.len() {
return node.is_end;
}
let idx = (word[i] - b'a') as usize;
// 不修改
if let Some(ref next) = node.child[idx] {
if Self::dfs(word, i + 1, next, cnt) {
return true;
}
}
// 修改
if cnt < 2 {
for c in 0..26 {
if c == idx {
continue;
}
if let Some(ref next) = node.child[c] {
if Self::dfs(word, i + 1, next, cnt + 1) {
return true;
}
}
}
}
false
}
pub fn two_edit_words(queries: Vec<String>, dictionary: Vec<String>) -> Vec<String> {
let mut root = TrieNode::new();
for w in &dictionary {
Self::insert(&mut root, w);
}
let mut res = vec![];
for q in &queries {
if Self::dfs(q.as_bytes(), 0, &root, 0) {
res.push(q.clone());
}
}
res
}
}
go
type TrieNode struct {
child [26]*TrieNode
isEnd bool
}
var root = &TrieNode{}
func insert(word string) {
node := root
for _, c := range word {
idx := c - 'a'
if node.child[idx] == nil {
node.child[idx] = &TrieNode{}
}
node = node.child[idx]
}
node.isEnd = true
}
func dfs(word string, i int, node *TrieNode, cnt int) bool {
if cnt > 2 || node == nil {
return false
}
if i == len(word) {
return node.isEnd
}
idx := word[i] - 'a'
// 不修改
if node.child[idx] != nil && dfs(word, i+1, node.child[idx], cnt) {
return true
}
// 修改
if cnt < 2 {
for c := 0; c < 26; c++ {
if byte(c) == idx {
continue
}
if node.child[c] != nil && dfs(word, i+1, node.child[c], cnt+1) {
return true
}
}
}
return false
}
func twoEditWords(queries []string, dictionary []string) []string {
root = &TrieNode{}
for _, w := range dictionary {
insert(w)
}
var res []string
for _, q := range queries {
if dfs(q, 0, root, 0) {
res = append(res, q)
}
}
return res
}
csharp
public class Solution {
class TrieNode {
public TrieNode[] child = new TrieNode[26];
public bool isEnd = false;
}
TrieNode root = new TrieNode();
void Insert(string word) {
var node = root;
foreach (char c in word) {
int idx = c - 'a';
if (node.child[idx] == null)
node.child[idx] = new TrieNode();
node = node.child[idx];
}
node.isEnd = true;
}
bool Dfs(string word, int i, TrieNode node, int cnt) {
if (cnt > 2 || node == null)
return false;
if (i == word.Length)
return node.isEnd;
int idx = word[i] - 'a';
// 不修改
if (node.child[idx] != null) {
if (Dfs(word, i + 1, node.child[idx], cnt))
return true;
}
// 修改
if (cnt < 2) {
for (int c = 0; c < 26; c++) {
if (c == idx) continue;
if (node.child[c] != null) {
if (Dfs(word, i + 1, node.child[c], cnt + 1))
return true;
}
}
}
return false;
}
public IList<string> TwoEditWords(string[] queries, string[] dictionary) {
foreach (var w in dictionary)
Insert(w);
var res = new List<string>();
foreach (var q in queries) {
if (Dfs(q, 0, root, 0)) {
res.Add(q);
}
}
return res;
}
}
js
class TrieNode {
constructor() {
this.child = new Array(26).fill(null);
this.isEnd = false;
}
}
var twoEditWords = function(queries, dictionary) {
const root = new TrieNode();
function insert(word) {
let node = root;
for (let c of word) {
let idx = c.charCodeAt(0) - 97;
if (!node.child[idx]) node.child[idx] = new TrieNode();
node = node.child[idx];
}
node.isEnd = true;
}
function dfs(word, i, node, cnt) {
if (cnt > 2 || !node) return false;
if (i === word.length) return node.isEnd;
let idx = word.charCodeAt(i) - 97;
// 修改
if (node.child[idx] && dfs(word, i + 1, node.child[idx], cnt))
return true;
// 不修改
if (cnt < 2) {
for (let c = 0; c < 26; c++) {
if (c === idx) continue;
if (node.child[c] && dfs(word, i + 1, node.child[c], cnt + 1))
return true;
}
}
return false;
}
for (let w of dictionary) insert(w);
const res = [];
for (let q of queries) {
if (dfs(q, 0, root, 0)) res.push(q);
}
return res;
};
ts
class TrieNode {
child: (TrieNode | null)[] = new Array(26).fill(null);
isEnd: boolean = false;
}
function twoEditWords(queries: string[], dictionary: string[]): string[] {
const root = new TrieNode();
function insert(word: string) {
let node = root;
for (const c of word) {
const idx = c.charCodeAt(0) - 97;
if (!node.child[idx]) node.child[idx] = new TrieNode();
node = node.child[idx]!;
}
node.isEnd = true;
}
function dfs(word: string, i: number, node: TrieNode | null, cnt: number): boolean {
if (cnt > 2 || !node) return false;
if (i === word.length) return node.isEnd;
const idx = word.charCodeAt(i) - 97;
// 不修改
if (node.child[idx] && dfs(word, i + 1, node.child[idx], cnt))
return true;
// 修改
if (cnt < 2) {
for (let c = 0; c < 26; c++) {
if (c === idx) continue;
if (node.child[c] && dfs(word, i + 1, node.child[c], cnt + 1))
return true;
}
}
return false;
}
for (const w of dictionary) insert(w);
const res: string[] = [];
for (const q of queries) {
if (dfs(q, 0, root, 0)) res.push(q);
}
return res;
}
c
typedef struct TrieNode {
struct TrieNode* child[26];
bool isEnd;
} TrieNode;
TrieNode* newNode() {
TrieNode* node = (TrieNode*)malloc(sizeof(TrieNode));
memset(node->child, 0, sizeof(node->child));
node->isEnd = false;
return node;
}
void insert(TrieNode* root, char* word) {
TrieNode* node = root;
for (int i = 0; word[i]; i++) {
int idx = word[i] - 'a';
if (!node->child[idx])
node->child[idx] = newNode();
node = node->child[idx];
}
node->isEnd = true;
}
bool dfs(char* word, int i, TrieNode* node, int cnt) {
if (cnt > 2 || !node)
return false;
if (word[i] == '\0')
return node->isEnd;
int idx = word[i] - 'a';
// 不修改
if (node->child[idx] && dfs(word, i + 1, node->child[idx], cnt))
return true;
// 修改
if (cnt < 2) {
for (int c = 0; c < 26; c++) {
if (c == idx) continue;
if (node->child[c] && dfs(word, i + 1, node->child[c], cnt + 1))
return true;
}
}
return false;
}
char** twoEditWords(char** queries, int queriesSize,
char** dictionary, int dictionarySize,
int* returnSize) {
TrieNode* root = newNode();
for (int i = 0; i < dictionarySize; i++)
insert(root, dictionary[i]);
char** res = (char**)malloc(sizeof(char*) * queriesSize);
int cnt = 0;
for (int i = 0; i < queriesSize; i++) {
if (dfs(queries[i], 0, root, 0)) {
res[cnt++] = queries[i];
}
}
*returnSize = cnt;
return res;
}
复杂度分析:
- 时间复杂度: O ( d n + q n 2 ⋅ 25 2 ) O(dn + qn^2 \cdot 25^2) O(dn+qn2⋅252) , q q q 是 q u e r i e s queries queries 的长度, d d d 是 d i c t i o n a r y dictionary dictionary 的长度, n n n 是 q u e r i e s [ i ] queries[i] queries[i] 的长度。建字典树需要 O ( d n ) O(dn) O(dn) ,查询时对每个字母都有修改和不修改两种选择,选择修改位置有 C n 2 = n 2 C^2_n = n^2 Cn2=n2 种选择,其中不修改有 1 1 1 条分支,修改有 25 25 25 条分支,最多修改两次,所以有 25 2 25^2 252 种选择。
- 空间复杂度: O ( d n ) O(dn) O(dn) ,即为字典树占用的空间。