해당 문제는 프로그래머스에서 풀어볼 수 있기에 문제 설명은 생략하겠습니다.
초기에 문제를 읽고 들었던 구현 방법은 hashmap 을 트리형태로 연결한 Trie 자료 구조였습니다.
이 문제의 경우 시간복잡도를 줄이는 과정에서 굉장히 생각을 많이 해야하는 문제입니다. 트라이 자료구조의 시간복잡도는 O(m) 여기서 m 은 검색 키워드의 길이입니다. 위 검색 키워드 제한사항으로 주어진 전체 검색 키워드 길이의 합은 1,000,000 이하로 단순히 트라이 자료구조로 사전적인 검색만 하게된다면 통과할 수 있는 기준입니다.
하지만 문제는 와일드카드 문자인 '?' 를 사용할 수 있다는 점입니다. 알파벳은 26개의 영단어로 구성되며, '?'만 들어 있는 '??????'도 가능한 키워드 임을 생각하면 최악의 경우 26^1,000,000 이라는 말도 안되는 연산량이 필요할 수 있습니다. (물론 전체 가사 단어 길이의 합 또한 1,000,000 이하로 이 경우 연산량은 1,000,000 * 1,000,000 이 최대 연산량이지만 이 정도만 되도 시간초과가 발생하기엔 충분합니다.)
그렇기 때문에 이를 해결하는 것이 관건이라고 볼 수 있습니다.
Trie 자료 구조는 class 를 통해 다양한 방식으로 만들어 볼 수 있는데, 만약 시간복잡도를 생각하지 않고 Trie 자료 구조를 단순히 만드는 문제였다면 아래처럼 직관적으로 만들었을 것 같습니다.
from collections import defaultdict
class Node:
def __init__(self):
self.isDone = False
self.dict = defaultdict(Node)
class Trie:
def __init__(self):
self.node = Node()
self.cnt = 0
def insert(self, node : Node, word) -> None:
if len(word) == 0:
node.isDone = True
return
self.insert(node.dict[word[0]], word[1:])
def add(self, word) -> None:
self.insert(self.node, word)
return
def checkQuery(self, node: Node, query) -> None:
if not query:
if node.isDone:
self.cnt += 1
return
q = query[0]
if q == "?":
for v in node.dict.keys():
self.checkQuery(node.dict[v], query[1:])
else:
self.checkQuery(node.dict[q], query[1:])
def check(self, query)->int:
self.cnt = 0
self.checkQuery(self.node, query)
return self.cnt
def solution(words, queries):
answer = []
t = Trie()
for word in words:
t.add(word)
for query in queries:
cnt = t.check(query)
answer.append(cnt)
return answer
하지만 이 문제에선 이 과정으로 문제를 풀면 당연히 시간초과에 걸리게되고, 주어진 조건을 보고 적당히 가지치기를 해주어야합니다.
문제의 핵심 해결 키워드는 위 문구로 와일드카드 문자인 '?' 는 한 개 이상 존재하며 접미사 또는 접두사 하나로만 주어진다는 규칙이 있습니다.
와일드카드 검색은 최소화 해야하므로 위 문제를 풀기위해 세 가지 경우의 수로 나누어 문제를 해결하였다.
이렇게 할 경우 Trie 자료 구조는 2개 딕셔너리 1개를 초기 구성해야하는 번거로움이 있지만, 값을 비교할 때 길이가 같은 값만 비교하여 빠르고, 와일드 카드 문자가 나오기 직전까지만 비교하므로 매우 빠르게 값을 처리할 수 있습니다.
위에서 언급하였던대로 접두사, 접미사를 구분하기 위한 2가지 Trie 구조와 길이만을 빠르게 비교하기 위한 Dictionary 하나를 만들 예정이다. 하지만 여기서 Trie 구조를 잡을 때도 길이가 다르다면 알파벳 확인이 무의미 하므로 빠른 확인을 위해 Dictionary (key 값은 length) 안에 Trie 자료구조를 사용하는 방식으로 진행하였다.
먼저 Trie 자료 구조를 통해 가사의 딕셔너리를 초기 구성할 때 prefixDict 와 suffixDict, lenDict 3가지로 나누었습니다.
from collections import defaultdict
class Trie():
def __init__(self):
self.root = {}
def insert(self, word):
cnode = self.root
for s in word:
if s not in cnode:
cnode[s] = [ {}, 0 ]
cnode[s][1] += 1
cnode = cnode[s][0]
def solution(words, queries):
answer = []
prefixDict = defaultdict(Trie)
suffixDict = defaultdict(Trie)
lenDict = defaultdict(int)
for word in words:
prefixDict[len(word)].insert(word)
suffixDict[len(word)].insert(word[::-1])
lenDict[len(word)] += 1
for query in queries:
if query[0] == "?" and query[-1] == "?":
a = lenDict[len(query)]
elif query[0] == "?":
a = suffixDict[len(query)].find(query[::-1])
elif query[-1] == "?":
a = prefixDict[len(query)].find(query)
쿼리 탐색을 진행하게 될 때에는 먼저 '??????' 와 같이 접두사, 접미사 구분이 없이 전체가 와일드카드로 구성된 형태인지 파악한 후 lenDict 를 이용하여 빠르게 결과를 판단하고 접미사, 접두사 구분이 된 쿼리라면 이를 나누어 각각 만들어둔 Trie 자료구조에 length 길이 파악 후 결과를 파악한다.
from collections import defaultdict
class Trie():
def __init__(self):
self.root = {}
def insert(self, word):
cnode = self.root
for s in word:
if s not in cnode:
cnode[s] = [ {}, 0 ]
cnode[s][1] += 1
cnode = cnode[s][0]
def find(self, word) -> int:
result = 0
cnode = self.root
for s in word:
if s == "?":
return result
else:
if s in cnode:
result = cnode[s][1]
cnode = cnode[s][0]
else:
return 0
return result
def solution(words, queries):
answer = []
prefixDict = defaultdict(Trie)
suffixDict = defaultdict(Trie)
lenDict = defaultdict(int)
for word in words:
prefixDict[len(word)].insert(word)
suffixDict[len(word)].insert(word[::-1])
lenDict[len(word)] += 1
for query in queries:
if query[0] == "?" and query[-1] == "?":
a = lenDict[len(query)]
elif query[0] == "?":
a = suffixDict[len(query)].find(query[::-1])
elif query[-1] == "?":
a = prefixDict[len(query)].find(query)
answer.append(a)
return answer
해당 문제에선 길이가 정해져 있었고, 시간 활용을 위하여 length 를 먼저 파악하는 코드를 구현하였지만, 사실 Trie 자료 구조는 접두사(prefix) 트리 라고 불릴 만큼 앞에서 부터 원하는 결과를 파악하기 위해 계속해서 탐색해 나가는 자료 구조에 가까워 length 를 정하지 않는 것이 일반적인 것 같습니다.
Trie 자료 구조
Trie는 문자열을 효율적으로 저장하고 검색할 수 있는 트리 기반의 자료구조이다. 주로 사전(dictionary)와 같은 애플리케이션에서 사용되며, 접두사(prefix) 트리라고도 불린다.
트라이의 주요 특징
일반적인 Trie 구조
Trie 자료 구조를 새롭게 배우게 되었다, 그 동안 비슷한 문제를 풀며 이게 트라이 자료구조인지도 제대로 모르고 그저 dictionary 의 재귀적인 구현을 통해 풀어야 할 것 같다는 느낌으로 풀어왔었는데, 단순 구현 문제가 아닌 트라이 자료구조 + 시간복잡도 해결 방법을 묻는 문제를 만나니 다른 사람들의 여러 풀이를 보고 트라이 자료 구조에 대해서도 공부하는 시간이 되었습니다.
여러 자료구조들을 좀 더 자세히 찾아보고 python 에서 동작 과정도 알아보는 시간이 필요할 거 같다고 느꼈습니다.
네이버 검색엔진에선 접두사, 접미사를 넘어서 가운데에서도 등장할 수 있고 띄어쓰기도 알아서 변경하는 모습을 보이는데 Trie 자료구조를 바탕으로 구현되었을 것이라 생각합니다.
만약 위 자동완성을 나보고 구현하라고 한다면?
방식으로 구현할 것 같은데, 올바른 방법인지는 잘 모르겠습니다.
(혹시 잘 아시는 고수분이 있다면 댓글 달아주시면 제 공부에 많은 도움이 될 것 같습니다!)