by Yngie

연결된 리스트 (Linked List)

|

본 게시물은 문일철 교수님의 데이터 구조 및 분석을 참조하여 작성하였습니다.

Linked List

지난 시간에 알아본 배열(Array) 은 그 자료 구조가 가진 선형 구조 때문에 요소를 삽입과 삭제하는 과정에서 무조건 $N$ 번의 Operation이 필요했습니다. 이번에 알아볼 연결된 리스트(Linked list)는 삽입과 삭제에서 발생하는 비효율을 개선하기 위해서 만들어진 자료구조입니다. 참고로 여기에 등장하는 연결된 리스트와 파이썬에 쓰이는 리스트(List)는 큰 관련이 없으니 다른 자료구조로 생각해주시면 되겠습니다.

연결된 리스트의 기본 구조

연결된 리스트(Linked List) 는 노드(Node)와 레퍼런스(Reference)로 되어있습니다. 연결된 리스트가 이런 형식을 택한 이유는 인덱스로 구성된 선형 구조를 피하기 위해서 입니다. 연결된 리스트의 노드는 두 가지 변수로 이루어져 있습니다. 첫 번째는 해당 노드에 저장되는 값을 가리키는 레퍼런스를 저장하는 변수입니다. 그리고 두 번째 변수는 다음 노드를 가리키는 레퍼런스를 저장하는 변수입니다. 아래 그림은 간단한 연결된 리스트의 구조를 이미지로 나타낸 것입니다. 이미지의 연결된 리스트는 총 4개의 노드로 구성되어 있는 것을 볼 수 있습니다.

linkedlist0

이미지 출처 : 데이터 구조 및 분석 수업자료

연결된 리스트에는 헤드(Head)와 테일(Tail)이라는 스페셜 노드가 있습니다. 위 이미지에서도 이를 확인할 수 있습니다. 헤드는 연결된 리스트 맨 앞에 위치하는 노드이고, 테일은 연결된 리스트 맨 뒤에 위치하는 노드입니다. 두 노드 모두 값을 가리키는 레퍼런스가 없으며, 헤드는 다음 노드를 가리키는 레퍼런스를 가지고 있고 테일은 그마저도 가지고 있지 않습니다. 대신 테일은 다른 노드의 레퍼런스를 받지만 헤드는 다른 노드의 레퍼런스를 받지 않습니다. 이 두 스페셜 노드가 연결된 리스트를 구성하는 데 있어서 필수적인 요소는 아니지만, 연결된 리스트의 주줏돌과 같은 역할을 담당함으로써 삽입과 삭제를 더욱 용이하게 만드는 역할을 합니다.

아래는 연결된 리스트를 만드는 파이썬 코드입니다.

class Node:
    nodeNext = None
    nodePrev = ''
    objValue = ''
    binHead = False
    binTail = False

    def __init__(self, objValue='', nodeNext=None, binHead=False, binTail=False):
        self.nodeNext = nodeNext
        self.objValue = objValue
        self.binHead = binHead
        self.binTail = binTail

    def getValue(self):
        return self.objValue
    def setValue(self, objValue):
        self.objValue = objValue

    def getNext(self):
        return self.nodeNext
    def setNext(self, nodeNext):
        self.nodeNext = nodeNext

    def isHead(self):
        return self.binHead
    def isTail(self):
        return self.binTail


node1 = Node(objValue='a')
nodeTail = Node(binTail=True)
nodeHead = Node(binHead=True, nodeNext=node1)

연결된 리스트에서의 탐색(Search)

연결된 리스트에서 요소를 탐색하는 과정은 배열과 크게 다르지 않습니다. 연결된 리스트는 배열에서 삽입과 삭제만을 개선하기 위한 자료구조이기 때문이지요. 비록 접근 방법은 다르지만 요소를 하나하나 확인한다는 점에서는 별 차이가 없습니다. 배열에서는 요소를 탐색할 때 인덱스로 접근했지만 연결된 리스트는 인덱스를 사용하지 않기 때문에 노드와 레퍼런스로 접근합니다. 아래 그림을 보며 설명을 이어가겠습니다.

search_in_ll

가장 먼저 헤드를 찾은 뒤 헤드의 레퍼런스가 가리키는 노드로 이동합니다. 그리고 이동한 노드가 참조하는 값을 비교한 뒤 맞으면 거기서 함수를 끝내고 아니면 레퍼런스를 따라 다음 노드로 이동합니다. 이 과정을 찾고자 하는 요소가 나올 때까지 반복하면 됩니다. 만약 요소가 없다면 테일이 나올 때까지 반복한 후 False를 반환하면 됩니다.

연결된 리스트는 탐색 과정이 비슷하기 때문에 발생하는 Operation 횟수 역시 배열과 동일합니다. 내부 요소의 개수가 $N$ 인 연결된 리스트에서 리스트 내에 없는 요소를 탐색하는 경우에는 $N$ 회, 리스트 내에 있는 요소를 탐색하는 경우에는 최대 $N$ 회가 됩니다.

연결된 리스트에서의 삽입(Insert)

이제부터 알아볼 삽입과 삭제는 연결된 리스트의 구조가 빛을 발하는 부분입니다. 배열에서 $N$ 회 필요했던 Operation을 연결된 리스트에서는 획기적으로 줄일 수 있습니다. 연결된 리스트에서는 단 $3$ 번의 Operation 만으로 요소를 삽입하고 삭제할 수 있습니다. 먼저 요소를 삽입하는 과정부터 알아봅시다. 배열에서 우리가 요소를 넣을 인덱스를 지정해주었던 것처럼, 연결된 리스트에서도 요소가 들어갈 자리는 미리 지정해 준다는 가정 하에서 시작합니다.

삽입의 첫 번째 절차는 요소를 삽입하려는 위치 다음에 있는 노드인 Node_next 를 미리 저장하는 것입니다. 다음으로는 노드 삽입할 위치 이전에 있는 노드인 Node_prev 의 레퍼런스가 가리키는 노드를 Node_next 에서 새로운 노드인 Node_new 로 업데이트 해줍니다. 마지막으로 Node_new 의 레퍼런스가 가리키는 노드를 Node_next 로 바꿔주면 삽입 절차가 끝나게 됩니다.

연결된 리스트에서는 이 $3$ 번의 Operation 만으로 자료구조 내에 요소를 삽입할 수 있습니다. 정해진 길이없이 노드와 레퍼런스만 조작해주면 되기 때문입니다. 아래는 연결된 리스트에서 요소를 삽입하는 과정을 이미지로 나타낸 것입니다.

linkedlist1

이미지 출처 : 데이터 구조 및 분석 수업자료

연결된 리스트에서의 삭제(Delete)

연결된 리스트에서는 삭제를 단 2번의 Operation 만으로 수행할 수 있습니다. 삭제 역시 우리가 삭제할 요소의 자리를 지정한다는 가정 하에서 시작합니다.

첫 번째 Operation은 삭제하려는 요소의 레퍼런스를 받는 노드인 Node_next 를 저장하는 것입니다. 이 과정은 삽입에서 했던 것과 동일합니다. 두 번째 Operation은 Node_prev 의 레퍼런스가 가리키는 노드를 Node_remove 에서 Node_next 로 변경해줍니다. 연결된 리스트에서는 두 번의 Operation만을 거치면 삭제 과정이 완료됩니다.

그런데 우리가 다루어주지 않는 한 노드가 있습니다. 바로 Node_remove 인데요. 이 노드는 이제 어떻게 처리되는 것일까요? 파이썬에는 이런 부분을 처리하는 가비지 콜렉터(Garbage collector)라는 장치가 있습니다. (헤드 노드를 제외한다면) 연결된 리스트에서 어떤 레퍼런스의 지목도 받지 못하는 노드는 바로 이 가비지 콜렉터에 의해서 제거됩니다.

linkedlist2

이미지 출처 : 데이터 구조 및 분석 수업자료

연결된 리스트의 탐색, 삽입, 삭제 구현하기

아래는 연결된 리스트에서 요소를 삽입하고 삭제하는 과정을 파이썬 코드로 구현한 것입니다. 이전에 구현했던 Node 클래스를 활용할 수 있습니다.

class SinglyLinkedList:
    nodeHead = ''
    nodeTail = ''
    size = 0

    def __init__(self):
        self.nodeTail = Node(binTail=True)
        self.nodeHead = Node(binTail=True, nodeNext=self.nodeTail)

    def insertAt(self, objInsert, idxInsert):
        nodeNew = Node(objValue=objInsert)
        nodePrev = self.get(idxInsert-1)
        nodeNext = nodePrev.getNext()
        nodePrev.setNext(nodeNew)
        nodeNew.setNext(nodeNext)
        self.size = self.size+1

    def removeAt(self, idxRemove):
        nodePrev = self.get(idxRemove-1)
        nodeRemove = nodePrev.getNext()
        nodeNext = nodeRemove.getNext()
        nodePrev.setNext(nodeNext)
        self.size = self.size-1
        return nodeRemove.getValue()

    def get(self, idxRemove):
        nodeReturn = self.nodeHead
        
        for itr in range(idxRetrieve+1):
            nodeReturn = nodeReturn.getNext()
        return nodeReturn

    def printStatus(self):
        nodeCurrent = self.nodeHead

        while nodeCurrent.getNext().isTail() == False:
            nodeCurrent = nodeCurrent.getNext()
            print(nodeCurrent.getValue(), end=" ")
        print("")

    def getSize(self):
        return self.size
    

list1 = SinglyLinkedList()
list1.insertAt('a',0)
list1.insertAt('b',1)
list1.insertAt('d',2)
list1.insertAt('e',3)
list1.insertAt('f',4)
list1.printStatus()
>>> a b d e f

list1.insertAt('c',2)
list1.printStatus()
>>> a b c d e f

list1.removeAt(3)
list1.printStatus()
>>> a b c e f

연결된 리스트의 의의

연결된 리스트는 배열이 가진 선형 구조의 한계를 극복하고 삽입과 삭제를 매우 빠르게 실행할 수 있도록 하는 자료구조입니다. 또한 이후에 배울 스택(Stack)과 큐(Queue) 의 기반이 되는 자료구조이기도 합니다.



Comments