Skip to main content

Prefix and Suffix Search


Prefix and Suffix Search: Design a special dictionary that searches the words in it by a prefix and a suffix.

Implement the WordFilter class:

  • WordFilter(string[] words) Initializes the object with the words in the dictionary.
  • f(string pref, string suff) Returns the index of the word in the dictionary, which has the prefix pref and the suffix suff. If there is more than one valid index, return the largest of them. If there is no such word in the dictionary, return -1.
Example 1:
Input
["WordFilter", "f"]
[[["apple"]], ["a", "e"]]
Output
[null, 0]
Explanation
WordFilter wordFilter = new WordFilter(["apple"]);
wordFilter.f("a", "e"); // return 0,
because the word at index 0 has prefix = "a" and suffix = "e".

Constraints:
  • 1 <= words.length <= 10^4
  • 1 <= words[i].length <= 7
  • 1 <= pref.length, suff.length <= 7
  • words[i], pref and suff consist of lowercase English letters only.
  • At most 10^4 calls will be made to the function f.

Try this Problem on your own or check similar problems:

  1. Design Add and Search Words Data Structure
Solution:
class WordFilter {
class Trie {
public class TrieNode{
TrieNode[] children;
int weight;
public TrieNode(){
children = new TrieNode[27];
weight = 0;
}
}

TrieNode root;
public Trie() {
root = new TrieNode();
}

public void insert(String word, int weight) {
for (int i = 0; i < word.length(); ++i) {
TrieNode iter = root;
iter.weight = weight;
for (int j = i; j < 2 * word.length() - 1; ++j) {
int k = word.charAt(j % word.length()) - 'a';
if (iter.children[k] == null)
iter.children[k] = new TrieNode();
iter = iter.children[k];
iter.weight = weight;
}
}
}

public int search(String word){
TrieNode iter = root;
for (int i = 0; i < word.length(); ++i) {
char letter = word.charAt(i);
if (iter.children[letter - 'a'] == null) return -1;
iter = iter.children[letter - 'a'];
}
return iter.weight;
}

}
Trie trie = new Trie();

public WordFilter(String[] words) {
for (int i = 0; i < words.length; ++i) {
trie.insert(words[i] + '{', i);
}
}

public int f(String pref, String suff) {
return trie.search(suff + '{' + pref);
}
}

/**
* Your WordFilter object will be instantiated and called as such:
* WordFilter obj = new WordFilter(words);
* int param_1 = obj.f(pref,suff);
*/

Time/Space Complexity:
  • Time Complexity: O(nk^2 + Qk)
  • Space Complexity: O(nk^2)

Explanation:

We can have two tries one for inserting all prefixes and the other inserting suffixes. We also track indices of words Set<Integer> wordIdx for each of the nodes, so to find a word with maximum index with certain prefix and suffix we can search for prefix and suffix in respective tries and find the intersection of the two sets of words (which start with prefix, and end in suffix) and return maximum element of that intersection. This will lead to TLE if the sets become large.

class WordFilter {
class Trie {
public class TrieNode{
TrieNode[] children;
boolean isWord = false;
Set<Integer> wordIdx;
public TrieNode(){
children = new TrieNode[26];
wordIdx = new HashSet<>();
}
}

TrieNode suffixRoot, prefixRoot;
public Trie() {
suffixRoot = new TrieNode();
prefixRoot = new TrieNode();
}

public void insert(String word, int idx, boolean isSuffixRoot) {
TrieNode iter = isSuffixRoot ? suffixRoot : prefixRoot;
for(int i = 0; i < word.length(); ++i){
char letter = word.charAt(i);
if(iter.children[letter - 'a'] == null){
iter.children[letter - 'a'] = new TrieNode();
}
iter = iter.children[letter - 'a'];
iter.wordIdx.add(idx);
}
iter.isWord = true;
iter.wordIdx.add(idx);
}

private TrieNode searchPrefix(String prefix, boolean isSuffixRoot){
TrieNode iter = isSuffixRoot ? suffixRoot : prefixRoot;
for(int i = 0; i < prefix.length(); ++i){
char letter = prefix.charAt(i);
if(iter.children[letter - 'a'] == null){
return null;
}
iter = iter.children[letter - 'a'];
}
return iter;
}

public Set<Integer> startsWith(String prefix, boolean suffixLookup) {
TrieNode node = searchPrefix(prefix, suffixLookup);
return node == null ? null : node.wordIdx;
}
}

Trie trie = new Trie();

private String reverseString(String s){
StringBuilder sb = new StringBuilder();
sb.append(s);
sb.reverse();
return sb.toString();
}

public WordFilter(String[] words) {
for(int i = 0; i < words.length; ++i){
trie.insert(words[i], i, false);
trie.insert(reverseString(words[i]), i, true);
}
}

public int f(String pref, String suff) {
Set<Integer> wordWithPrefIdx = trie.startsWith(pref, false);
Set<Integer> wordWithSuffIdx = trie.startsWith(reverseString(suff), true);

if(wordWithPrefIdx == null || wordWithSuffIdx == null) return -1;

wordWithPrefIdx.retainAll(wordWithSuffIdx);

return wordWithPrefIdx.isEmpty() ? -1 : Collections.max(wordWithPrefIdx);
}
}

Space complexity is the size of the tries O(nk) number of words and the average (or maximum) length of words, while we have O(nk + Q(n + k)) for time complexity since it takes O(nk) to build the tree and then for each query Q we would have the time required to search for k-length word and time required to traverse the set of words containing certain prefix/suffix leading to O(n + k) (the set can containing all words that have been inserted in the trie) total time complexity.

To save time on read/queries since they will be far more frequent that write/create operation (only once) we can spend more time on the initialization. If we have for example word "cat", we could insert it together with all of its suffixes separated by special (non-lowercase English letter '{'), so we would insert "{cat", "t{cat", "at{cat", "cat{cat", then on query we would query for "t{ca" if the required suffix is "t" and prefix "ca". This leads to a k time complexity for each query Q, while we need more space and time to build the trie in this case O(nk^2) for each word we spend k^2 (k average length of the word) since we're inserting each suffix and there are k (length of word) of those. We use the weight to overwrite the last index of the word that has same path in the trie as one of the previous words, so we can always return the maximum index.