공부를 하면 할 수록, 기본 개념은 간단하며 소위 '기초'로 분류되는 주제이나 실제로 문제를 풀기에는 여간 까다롭지 않은 것이 연결 리스트라는 생각이 든다. 주변 엔지니어에게 경험을 물어봐도 이 주제를 어렵다고 평가하는 것을 들을 수 있었다. 대표적 문제를 풀면서 심층적으로 탐구해 보고자 한다.
정의
연결 리스트는 아래와 같이 정의된 노드들의 연결로 이루어진다 (Goodrich et al. [1])
class _Node:
def __init__(self, element=0, next=None, prev=None): # initialize node's fields
self._element = element # user's element
self._prev = prev # previous node reference
self._next = next # next node reference, 단일 연결리스트의 경우는 불필요
리스트의 뒤에 내용을 손쉽게 삽입하거나 삭제, 또는 값을 검색하기 위한 멤버함수(메소드) 등을 별도의 클래스를 만들어 정의할 수 있다. 이중 연결리스트와 이를 이용한 deque의 구현 코드를 소개한다. [1]
# Copyright 2013, Michael H. Goldwasser (GNU General Public License)
class _DoublyLinkedBase:
"""A base class providing a doubly linked list representation."""
#-------------------------- nested _Node class --------------------------
# nested _Node class
class _Node:
"""Lightweight, nonpublic class for storing a doubly linked node."""
__slots__ = '_element', '_prev', '_next' # streamline memory
def __init__(self, element, prev, next): # initialize node's fields
self._element = element # user's element
self._prev = prev # previous node reference
self._next = next # next node reference
#-------------------------- list constructor --------------------------
def __init__(self):
"""Create an empty list."""
self._header = self._Node(None, None, None)
self._trailer = self._Node(None, None, None)
self._header._next = self._trailer # trailer is after header
self._trailer._prev = self._header # header is before trailer
self._size = 0 # number of elements
#-------------------------- public accessors --------------------------
def __len__(self):
"""Return the number of elements in the list."""
return self._size
def is_empty(self):
"""Return True if list is empty."""
return self._size == 0
#-------------------------- nonpublic utilities --------------------------
def _insert_between(self, e, predecessor, successor):
"""Add element e between two existing nodes and return new node."""
newest = self._Node(e, predecessor, successor) # linked to neighbors
predecessor._next = newest
successor._prev = newest
self._size += 1
return newest
def _delete_node(self, node):
"""Delete nonsentinel node from the list and return its element."""
predecessor = node._prev
successor = node._next
predecessor._next = successor
successor._prev = predecessor
self._size -= 1
element = node._element # record deleted element
node._prev = node._next = node._element = None # deprecate node
return element # return deleted element
우선 이중 연결리스트의 안에는 nested class로서 _Node가 있다. 이처럼 클래스를 정의하기 위해 다른 클래스의 정의를 활용한 객체지향(OOP) 디자인 패턴을 합성(composition)이라고 부르기도 한다. 멤버함수의 정의에 _Node가 쓰이고 있는 것을 볼 수 있다. __slots__ 는 속도를 빠르게 하기 위한 장치로 생략 가능하다.
이를 이용한 Double-ended queue (deque) 의 구현은 다음과 같다. 앞의 _DoublyLinkedBase 클래스를 상속한 다음, 그 클래스의 nonpublic 멤버(field, method)를 적절한 인수(argument)를 넣어 호출하는 역할인 public method 들의 모음으로 구현이 되어 있다. 결국 deque는 이중 연결 리스트에 다름 아닌 것이다. deque는 collections.deque 에서 간편하게 사용할 수 있으나 연결리스트의 예로서 소개하였다.
class LinkedDeque(_DoublyLinkedBase): # note the use of inheritance
"""Double-ended queue implementation based on a doubly linked list."""
def first(self):
"""Return (but do not remove) the element at the front of the deque.
Raise Empty exception if the deque is empty.
"""
if self.is_empty():
raise Empty("Deque is empty")
return self._header._next._element # real item just after header
def last(self):
"""Return (but do not remove) the element at the back of the deque.
Raise Empty exception if the deque is empty.
"""
if self.is_empty():
raise Empty("Deque is empty")
return self._trailer._prev._element # real item just before trailer
def insert_first(self, e):
"""Add an element to the front of the deque."""
self._insert_between(e, self._header, self._header._next) # after header
def insert_last(self, e):
"""Add an element to the back of the deque."""
self._insert_between(e, self._trailer._prev, self._trailer) # before trailer
def delete_first(self):
"""Remove and return the element from the front of the deque.
Raise Empty exception if the deque is empty.
"""
if self.is_empty():
raise Empty("Deque is empty")
return self._delete_node(self._header._next) # use inherited method
def delete_last(self):
"""Remove and return the element from the back of the deque.
Raise Empty exception if the deque is empty.
"""
if self.is_empty():
raise Empty("Deque is empty")
return self._delete_node(self._trailer._prev) # use inherited method
연결리스트로 구현한 deque는 보는 바와 같이 queue의 앞과 뒤에서의 삽입, 삭제가 O(1) 만에 이루어진다.
문제
https://leetcode.com/problems/merge-two-sorted-lists/
Merge Two Sorted Lists - LeetCode
Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.
leetcode.com
정렬된 두 연결리스트(list 1, list 2)를 역시 정렬되도록 합칠 것. 즉 merge sort와 유사하다고 할 수 있다. 내용을 정리하면 다음 그림과 같다.
1. 빈 dummy head (DH) 를 만들고 tail (T) 가 이것을 가리키도록 최초 설정한다. DH는 리스트의 맨 처음을 굳건히 가리키기 위한 용도이다. 나중에 이것의 next를 리턴할 계획이다.
2. 어느 한 list가 존재할 때 까지 두 리스트의 값을 비교하면서 DH의 뒤에 계속 작은 값을 붙인다. 이 붙이는 역할을 tail을 움직이면서 수행하게 된다. 예를 들어 list 1의 첫 노드가 list 2의 첫 노드보다 더 작았다고 하자 그러면 이것을 먼저 DH의 뒤에 붙여야 한다 (merge sort의 merge 단계와 같다).
3. 주어진 리스트들의 길이가 같다는 보장은 없었으므로 2번 과정을 수행하고 나면 한 리스트의 남은 부분이 있을 수 있다. 이를 tail.next에 붙이기 위해 l1 또는 l2 중 존재하는 것을 붙인다.
4. 1에서 계획한대로 dh.next를 리턴한다.
만일 dh를 만들지 않으면 어떻게 될까. 두 리스트의 head node 중 작은 값을 선택한 다음 그것에서 출발하도록 하면 될까. 그러면 해당 리스트를 순회해야 할텐데, 그러려면 결국 포인터를 만들어야 한다. 그러므로 리스트의 경우는 dummy head를 만들어 푸는 것이 편리하다고 생각된다.
참고문헌
[1] Goodrich et al., 2013, Data Structures and Algorithms in Python, pp 270-276, Wiley
'알고리즘 > Graph' 카테고리의 다른 글
그래프 알고리즘 코드 정리 [1: 기본 탐색(traversal)] (0) | 2022.11.13 |
---|---|
Linked List Cycle (0) | 2022.05.30 |
위상정렬 (topological sorting) (0) | 2022.05.28 |
Connected Components with DFS (0) | 2022.05.13 |
DFS (스택)와 BFS (큐) 로 풀어보는 BST (0) | 2022.03.10 |