검색 엔진에서 자동완성을 하려면 어떻게 해야할까요?




class Node {
constructor(value = '') {
this.value = value;
this.children = new Map();
}
}
class Trie {
// Trie를 생성하면 루트로 빈 노드를 생성
constructor() {
this.root = new Node();
}
// 문자열을 추가하면 탐색을 위해서 루트부터 시작
insert(string) {
let currentNode = this.root;
// 문자열을 앞에서 부터 하나씩 자르면서 순회
for (const char of string) {
// 만약 현재 노드에서 자른 문자열을 간선으로 가지고 있지 않다면 새 노드를 추가
if (!currentNode.children.has(char)) {
currentNode.children.set(char, new Node(currentNode.value + char));
}
// 다음 정점으로 이동
currentNode = currentNode.children.get(char);
}
}
// 문자열이 존재하는지 체크
has(string) {
let currentNode = this.root;
for (const char of string) {
if (!currentNode.children.has(char)) {
return false;
}
currentNode = currentNode.children.get(char);
}
return true;
}
}
const trie = new Trie();
trie.insert('cat');
trie.insert('can');
console.log(trie.has('cat')); // true
console.log(trie.has('can')); // true
console.log(trie.has('cap')); // false
사실 문제 이름부터 자동완성이기 때문에 바로 Trie를 떠올릴 수 있습니다. 거기에 문제 내용까지 살펴보면 자동완성 기능이 되어야 최소 입력 글자를 알 수 있기에 이 문제에선 Trie가 가장 효율적인 자료구조라는 것을 알 수 있습니다.
Trie 구조를 만들면서 하위에 어떤 문자들이 있는지 미리 알아야 셀 수 있습니다. 예를 들어, guild를 찾을 때 gu만 입력해도 된다는 것을 알기 위해 Trie 구조에 해당 정보들을 넣어놔야 합니다.
다음과 같이 Trie 구조를 구성할 수 있습니다.
그럼 Trie 구조가 다음과 같이 구성됩니다.
[3, "g"]
/ \
[1, "u"] [2, "o"]
| |
[1, "i"] [1, "n"]
| |
[1, "l"] [1, "e"]
|
[1, "d"]
Trie 구조가 완성되었다면 이후 각 단어들을 찾으며 카운팅이 1이라면 이후 글자를 입력하지 않아도 된다는 것을 알 수 있기 때문에 그 지점에서 카운팅을 멈추면 됩니다.
위 알고리즘을 구현하면 다음과 같습니다.
function makeTrie(words) {
const root = {}; // 먼저 루트 노드를 설정할 변수를 만든다.
for (const word of words) { // Trie를 구성하기 위한 루프를 돌린다.
let current = root; // 루프부터 시작
for (const letter of word) { // 단어의 글자를 하나씩 춫출한 후
// 값을 넣는다. 리스트의 첫 번째 값은 학습된 단어가 몇 개인지를 카운팅하고
// 두 번째 값은 트리 구조로 이용할 노드 값으로 사용한다.
if (!current[letter]) current[letter] = [0, {}];
current[letter][0] = 1 + (current[letter][0] || 0); // 카운팅을 위해 1 더해준다.
current = current[letter][1]; // current는 letter에 해당되는 노드로 이동한다.
}
}
return root; // 반환
}
function solution(words) {
let answer = 0;
const trie = makeTrie(words); // Trie 자료구조를 만들어준다.
for (const word of words) { // 입력받은 수 만큼 루프
let count = 0; // 카운팅을 위한 변수
let current = trie; // 루트부터 시작
for (const [index, letter] of [...word].entries()) {
count += 1;
if (current[letter][0] <= 1) { // 단어가 하나 이하로 남을 경우 종료
break;
}
current = current[letter][1]; // 다음 노드로 이동
}
answer += count; // 카운팅을 더해준다
}
return answer; // 반환
}