텍스트 에디터


description: 코딩 테스트의 기본 문제 중 하나인 에디터 구현입니다. 두 가지의 구현 방법이 있습니다. 자료구조 스택 또는 연결 리스트를 활용하는 방법입니다. 백준 사이트의 1406번 문제를 참조했습니다. 코드는 파이썬으로 작성되었습니다.

created: 2022-09-19
tags: code_test   algorithm   linked_list   stack  

  백준 1406번 텍스트 에디터 문제입니다. 이전에 따로 자료구조에 대해 정리를 해야할까 생각했지만 문제를 풀며 자료구조와 알고리즘의 활용 예시를 만들어 가는게 더 나을 것 같은 생각이 들었습니다. 문제는 온라인 프로그래밍 채점 사이트의 정답 비율이 30 % 보다 낮은 문제들을 이용하기로 했습니다.



문제


  한 줄로 된 간단한 에디터를 구현하려고 한다. 이 편집기는 영어 소문자만을 기록할 수 있는 편집기로, 최대 600,000 글자까지 입력할 수 있다.

  이 편집기에는 ‘커서’라는 것이 있는데, 커서는 문장의 맨 앞(첫 번째 문자의 왼쪽), 문장의 맨 뒤 (마지막 문자의 오른쪽), 또는 문장 중간 임의의 곳 (모든 연속된 두 문자 사이)에 위치할 수 있다. 즉 길이가 L인 문자열이 현재 편집기에 입력되어 있으면, 커서가 위치할 수 있는 곳은 L+1 가지 경우가 있다.

  이 편집기가 지원하는 명령어는 다음과 같다.

  1. L => 커서를 왼쪽으로 한 칸 옮김 (커서가 문장의 맨 앞이면 무시됨)
  2. D => 커서를 오른쪽으로 한 칸 옮김 (커서가 문장의 맨 뒤이면 무시됨)
  3. B => 커서 왼쪽에 있는 문자를 삭제함 (커서가 문장의 맨 앞이면 무시됨) 삭제로 인해 커서는 한 칸 왼쪽으로 이동한 것처럼 나타나지만, 실제로 커서의 오른쪽에 있던 문자는 그대로임
  4. P $ => $ 라는 문자를 커서 왼쪽에 추가함

  초기에 편집기에 입력되어 있는 문자열이 주어지고, 그 이후 입력한 명령어가 차례로 주어졌을 때, 모든 명령어를 수행하고 난 후 편집기에 입력되어 있는 문자열을 구하는 프로그램을 작성하시오. 단, 명령어가 수행되기 전에 커서는 문장의 맨 뒤에 위치하고 있다고 한다.



입출력


입력:

출력:


example
Input
Output

abcd
3
P x
L
P Y
    
abcdyx
    

abc
9
L
L
L
L
L
P x
L
B
P y
    
abcdyx
    

dmih
11
B
B
P x
L
B
B
B
P y
D
D
P z
    
yxz
    



Linked List 로 구현


  Linked List는 조회는 느리지만(O(n)) 삽입, 제거가 유리한(O(1)) 자료구조입니다. 정 반대의 장점을 가진 자료구조로는 List 가 있습니다. List 는 인덱스를 통해 조회를 매우 빠르게(O(1)) 할 수 있지만 데이터를 삽입, 제거하기 위해서는 기존의 데이터들을 모두 이동시키는 과정(O(n))이 필요하므로 굉장히 느립니다.

  만약 문제를 삽입, 제거가 느린 List 로 구현한다면 시간 내에 문제를 해결하지 못할 가능성이 큽니다. 또한 커서가 Linked List 내부에서 포인터의 역할을 하므로 List 의 인덱스를 활용한 조회는 필요가 없습니다. 즉, Linked List 를 활용하는 것이 좋은 방법입니다. 단, 문제의 특성상 커서가 오른쪽, 왼쪽 모두 이동하기 때문에 단방향 Singly Linked List 가 아닌 양방향 Doubly Linked List 를 활용해야 합니다.



그림 1. 마지막 위치 데이터 삽입



그림 2. 중간 위치 데이터 삽입


  Linked List에서 데이터를 삽입하려면 기존 데이터와 삽입할 데이터의 위치를 서로 업데이트하기만 하면 됩니다.



그림 3. 포인터 좌, 우 이동



그림 4. 포인터 좌, 우 이동 예외


  커서의 위치를 오른쪽, 왼쪽으로 이동하려면 참조를 따라가기만 하면 접근할 수 있습니다. 단 마지막 위치에 다다른다면 더 이상 이동할 수 없게 제어해야 합니다.



그림 5. 데이터 제거



그림 6. 데이터 제거 예외


  데이터를 제거하는 과정은 삽입의 역순입니다. 단, 왼쪽 끝 위치에 다다른다면 더 이상 제거할 수 없게 제어해야 합니다.


Linked List 구현 코드
class Node:

    def __init__(self, data):
        self._data = data
        self._pre = None
        self._post = None

    @property
    def data(self):
        return self._data

    @property
    def pre(self):
        return self._pre

    @pre.setter
    def pre(self, node):
        self._pre = node

    @property
    def post(self):
        return self._post

    @post.setter
    def post(self, node):
        self._post = node


class DoublyLinkedList:

    def __init__(self, input):
        self.head = Node(None)
        self.tail = Node(None)
        self._current = self.tail

        self.head.post = self.tail
        self.tail.pre = self.head

        for data in input:
            self.Insert(data)

    @staticmethod
    def Render(linked_list):
        linked_list.current = linked_list.head.post

        while (not linked_list.IsRightEnd):
            print(linked_list.current.data, end="")
            linked_list.current = linked_list.current.post

    @property
    def IsLeftEnd(self):
        return (self.current.pre == self.head)

    @property
    def IsRightEnd(self):
        return (self.current == self.tail)

    @property
    def current(self):
        return self._current

    @current.setter
    def current(self, node):
        self._current = node

    def Insert(self, data):
        new_node = Node(data)
        left_node = self.current.pre
        right_node = self.current

        left_node.post = new_node
        new_node.pre = left_node
        new_node.post = right_node
        right_node.pre = new_node

    def MoveLeft(self):

        if (not self.IsLeftEnd):
            self.current = self.current.pre

    def MoveRight(self):

        if (not self.IsRightEnd):
            self.current = self.current.post

    def Delete(self):

        if (not self.IsLeftEnd):
            target_node = self.current.pre
            left_node = target_node.pre
            right_node = self.current

            left_node.post = right_node
            target_node.pre = None
            target_node.post = None
            right_node.pre = left_node

            del (target_node)


def Solution(text, commands):

    def CRLF():
        print()

    def DataRender(editor):
        DoublyLinkedList.Render(editor)
        CRLF()

    editor = DoublyLinkedList(text)
    functions = {"L": editor.MoveLeft,
                 "D": editor.MoveRight,
                 "B": editor.Delete,
                 "P": editor.Insert}
    CRLF()

    for command in commands:
        print(command, end=" ")

        if (command[0] != "P"):
            functions[command[0]]()

        else:
            functions[command[0]](command[2])

        current = editor.current
        DataRender(editor)
        editor.current = current

    CRLF()

def main():
    text = input()
    m = int(input())
    commands = []

    for _ in range(m):
        commands.append(input())

    Solution(text, commands)

if __name__ == "__main__":
    main()



Stack 으로 구현


  Stack 도 자료의 추가와 제거가 O(1) 으로 굉장히 빠른 자료구조입니다. 2 개의 Stack 을 활용하면 커서의 위치 이동과 삽입, 제거를 더 간단하게 구현할 수 있습니다. 대신 Stack 의 크기를 미리 정해야 하하므로 메모리를 Linked List 에 비해 많이 사용하게 됩니다.


Stack 구현 코드
class Stack:

    def __init__(self, N):
        self.n = N
        self.top = 0
        self.buffer = [None] * self.n

    @staticmethod
    def Render(stack):
        print(stack.buffer, end=" ")

    @property
    def IsEmpty(self):
        return (self.top <= 0)

    @property
    def Peek(self):
        return self.buffer[self.top - 1]

    def Push(self, data):
        self.buffer[self.top] = data
        self.top += 1

    def Pop(self):
        self.top -= 1
        data = self.buffer[self.top]
        self.buffer[self.top] = None
        return data


class TwoStack:

    def __init__(self, input, extra):
        stack_length = len(input) + extra
        self.left_stack = Stack(stack_length)
        self.right_stack = Stack(stack_length)

        for data in input:
            self.Insert(data)

    @staticmethod
    def Render(two_stack):
        print("Left:", end=" ");
        Stack.Render(two_stack.left_stack)
        CRLF()

        print("Right:", end=" ");
        Stack.Render(two_stack.right_stack)
        CRLF()

    @property
    def IsLeftEnd(self):
        return (self.left_stack.IsEmpty)

    @property
    def IsRightEnd(self):
        return (self.right_stack.IsEmpty)

    def Insert(self, data):
        self.left_stack.Push(data)

    def MoveLeft(self):

        if (not self.IsLeftEnd):
            self.right_stack.Push(self.left_stack.Pop())

    def MoveRight(self):

        if (not self.IsRightEnd):
            self.left_stack.Push(self.right_stack.Pop())

    def Delete(self):

        if (not self.IsLeftEnd):
            self.left_stack.Pop()


def CRLF():
    print()

def Solution(text, commands):

    def DataRender(editor):
        TwoStack.Render(editor)

    editor = TwoStack(text, len(commands))
    functions = {"L": editor.MoveLeft,
                 "D": editor.MoveRight,
                 "B": editor.Delete,
                 "P": editor.Insert}
    CRLF()

    for command in commands:
        print(command)

        if (command[0] != "P"):
            functions[command[0]]()

        else:
            functions[command[0]](command[2])

        DataRender(editor)

    CRLF()

def main():
    text = input()
    m = int(input())
    commands = []

    for _ in range(m):
        commands.append(input())

    Solution(text, commands)

if __name__ == "__main__":
    main()



마치며


  Linked List 는 개인적으로 딥러닝 모델 커스터마이징 중 기존 형태에 새로운 Layer 를 추가할 때 사용한 경험이 있습니다. 나름 활용도가 높은 자료구조라 생각하며 딥러닝을 공부하시는 분이라면 굉장히 도움이 될 것입니다. 이만 포스트를 마치겠습니다. 감사합니다.



Reference URL