업데이트:

태그: , , ,

카테고리:

연결리스트란

연결리스트는 파이썬의 리스트와는 다르게 key값과 link값으로 이루어져있다.

리스트는 각 인덱스가 메모리가 할당되어 각 원소의 주소값을 가지게된다. 그래서 상수시간 O(1) 내에 연산이 가능하다.

하지만 연결리스트는 각 메모리가 원소의 주소(key값) + 다음값이 저장된 주소(link값) 이 저장되어 있다.
key값과 link값이 함께 저장되어 있는 것을 Node라고 부른다.

class Node:
    def __init__(self, key = None): 
        self.key = key #노드의 키값
        self.next = None #노드의 링크값, 별도로 설정하지 않는다면 초기값은 none

    def __str__(self):
        return str(self.key) #키값 호출 시, print(self.key) 대신에 print(self) 로 바로 키값출력이 가능하도록 하는 메서드



특징

단점

연결리스트는 배열처럼 인덱스로 접근이 불가능하다.
따라서 우리가 만약에 연결리스트의 3번 인덱스 값을 찾고싶다면, head노드(노드의 맨 첫번째 값)의 링크부터 따라서 들어가야하므로 head->1->2->3으로 총 3번의 계산이 필요하다.
만약 100번 인덱스 값을 찾고 싶다면, 100번의 계산이 필요하므로 연결리스트의 시간복잡도는 O(N)이 된다.

장점

연결리스트의 장점도 있다.
일반적인 리스트가 insert를 하게되면 새로운 값을 맨 앞에 넣는다는 가정하에, 뒤의 모든 값을 밀어내야하므로 시간복잡도는 O(N)인데 반해,
연결리스트는 head node를 새로 만들어 link값을 이전 head node의 주소를 가리키면 되므로 시간 복잡도는 O(1)이다.



한방향 연결리스트

  1. 노드들이 self.next 링크를 따라 한방향으로 연결된 리스트이다.
  2. 맨 앞의 노드를 head.node라 하고, head node를 알면 그 뒤의 모든 노드들을 찾을 수 있기때문에 연결리스트를 대표하는 노드이다.
  3. 맨 마지막 노드는 self.next가 none을 저장한다.
class SinglyLinkedList:
    def __init__(self): #리스트 생성시 자동호출메서드
        self.head = None #초기 head값은 none
        self.size = 0 #리스트의 노드의 개수

    def __iter__(self): #이터레이터 호출시 자동호출함수()
        v = self.head #v에 head node 키값설정
        while v != None: #head node가 none이 아닐때(빈 리스트가 아닐때)
            yield v
            v = v.next

    def __str__(self): #str호출시 자동호출함수
        return "->".join(str(v) for v in self) #인스턴스 내의 

    def __len__(self):
        return self.size #인스턴스의 노드개수 리턴



지원연산

pushfront

  1. 현재 head node 앞에 새로운 node을 삽입해야하므로, 새로운 노드가 head node가 된다.
  2. 새로운 node의 next값은 이전의 head node가 된다.
  3. 연결리스트의 전체 사이즈가 1 커진다.
def pushfront(self, key):
    new_node = Node(key) #key값 가진 새로운 노드 생성
    new_node.next = self.head #새로운 노드의 next는 원래 head값인 self.head임
    self.head = new_node #self.head 는 새로운 노등니 new_node가 된다.
    self.size += 1



pushback

  1. 빈 리스트에 pushback은 새로운 노드는 head node가 된다.
  2. .next가 none인 노드가 tail node가 된다. tail node 다음 값으로 주어진 node를 생성해 넣어야한다.
  3. 이를 찾기 위해서는 head node로 먼저 접근해 .next가 none인 노드를 찾는 과정이 필요하다.
def pushback(self, key):
    new_node = Node(key)
    if self.size == 0: #현재 연결리스트가 비어있는 경우
        self.head = new_node #연결리스트의 head node는 new_node가 된다.
    
    else:
        tail = self.head #tail node찾기 위해 tail에 self.head를 지정
        while tail.next != None: #tail.next 가 none이 아니라면
            tail = tail.next #tail에 tail.next(self.head.next)를 지정하고 다시 while문 반복
        tail.next = new_node #while문 끝나면 tail에 연결리스트의 마지막값이 들어와있으므로 다음값에 새로운 노드 new_node 연결
    self.size += 1



popfront

  1. 빈 리스트의 경우, popfront 불가능
  2. popfront 이후 self.head는 self.head.next가 되어야한다.
  3. popfront는 원래 self.head를 리턴해야한다.
def popfront(self):
        if self.size == 0:
            return None
        else:
            x = self.head #|node(4)를 x 변수에 복사
            key = x.key#|key 는 node(4)의 key인 4
            self.head = x.next #|node(4).next = node(5)이므로 self.head 는 node(5)가 된다.
            del x #|node(4) 객체 자체를 메모리에서 제거
            self.size -= 1
            return key



popback

  1. popback은 맨 뒤 tail의 key값을 리턴해야한다.
  2. tail의 이전값을 prev값으로 설정하는데, prev.next는 None이된다(.next가 none이라는건 리스트의 마지막 노드라는 의미)
  3. 리스트의 노드가 없을때, 1개일때, 2개 이상일때로 나뉜다.
  4. 노드가 없을때는 연산을 실행하지 않는다.
  5. 노드가 1개일때, self.head이자 tail의 key값을 리턴하면된다, 그리고 리스트가 비워질 것이므로 self.head를 none으로 설정한다.
  6. 노드가 2개 이상이라면, tail은 삭제될 것이므로 prev.next = none이 되어야한다.
def popback(self):
    if self.size == 0: #노드가 0개인 경우
        return None

    else:
        prev, tail = None, self.head #prev에 tail을 지정해나가면서 tail.next가 none인걸 찾는다.
        while tail.next != None: #노드가 1개인 경우
            prev = tail
            tail = tail.next

        if prev == None:  #while 연산이 끝났는데, prev가 none인건 노드가 1개일때밖에 없다.
            self.head == None #pop하면 리스트가 비워지므로 self.head를 None으로 만들어 
        else:
            prev.next = None #노드가 2개이상인 경우, prev 노드가 마지막노드가 되므로 prev의 next(링크)를 지운다.

        key = tail.key
        del tail
        self.size -= 1
        return key



  1. 찾고자하는 key값을 가진 node를 리턴해야한다. 없으면 none을 리턴한다.
  2. 1가지 방법은 while루프를 부르는 방법이있고,
  3. 다른 방법은 for 루프를 돌리는거다.
def search(self, key):
    x = self.head #연결리스트의 헤드노드를 x에 할당
    while x != None: #x가 none 아닌동안, 즉 연결리스트의 헤드부터 끝까지 
        if x.key = key: #만약 x의 키값이 찾고자하는 키값이라면
            return x #x 노드를 리턴한다.
        x = x.next #찾고자하는 키값이 아니라면 x에 다음 노드를 할당한다.
    return x (or None)#x가 none이되면 즉, 연결리스트의 끝값 +1이되면 x는 none이되는데, (x.next가 none이므로) none을 리턴하게된다.



remove

delete의 경우 3가지로 나뉜다

  1. 연결리스트의 길이가 0인 경우
  2. 지우고자하는 값이 headnode인 경우->popfront
  3. 연결리스트의 길이가 0이 아니면서 지우고자하는 값이 headnode가 아니라면 연결리스트에서 중간, 뒤에 있는 값을 의미한다.
  4. 3의 경우, popback처럼 prev찾아야하며, prev.next를 tail.next에 연결하면 지우고자하는 키값을 가진 노드는 연결리스트에서 배제된다.
  5. 배제된 노드는 삭제하고(tail), popfront와 달리 리스트의 사이즈를 줄이는 명령이 없으므로 별도로 self..size -= 1을 실행해줘야한다.
def remove(self, key):
        if self.size == 0: #연결리스트 길이 0인 경우
            return None

        elif self.head.key == key: #리스트의 헤드노드와 찾고자하는 노드의 키가 같은경우
            self.popfront() #popfront로 다음 노드를 헤드노드로 변경, 본래 헤드노드삭제

        else:
            prev, tail = None, self.head #초기값설정
            while tail.next != None: #tail.next가 none이 아닌동안
                prev = tail #prev와 tail을 한칸씩 옮겨가기
                tail = tail.next
                if tail.key == key: #만약 tail.key와 찾고자하는 키가 같다면
                    prev.next = tail.next #remove할 node의 링크를 지워버리면된다.
                    del tail #tail node를 리스트 상에서 삭제
                    self.size -= 1 #한개 삭제되었으므로 리스트 길이 1개 줄인다.
                    break



reverse

  1. 각 노드의 .next를 이전노드로 설정해야한다.
  2. prev, tail로 작서앟자.
  3. while문 탈출하고나선 리스트의 head노드를 변경해주자.
def reverse(self):
    a, b = None, self.head

    while b:
        if b:
            c = b.next #c에 b의 다음노드를 복사
            b.next = a #b의 링크를 반대로 설정
        a = b
        b = c

iter에 대해서

def __iter__(self): #
        v = self.head #
        while v != None: #
            yield v
            v = v.next

위 함수를 genarator라고 하는데, 연결리스트의 인스턴스에게 for문을 사용할 수 있게 해준다.
원래 연결리스트클래스의 인스턴스는 iterator가 없다면

for i in linkedlist:
    print(i)

를 할 경우, 에러가 나게 된다. 연결리스트에서는 일반적인 배열처럼 iter를 할 원소가 없기 때문이다.
이를 가능하게 해주는게 스페셜메서드로 __iter__(self)이다.
그리고 이 메서드처럼 yield가 있는 함수를 generator라고 부른다.



  1. 파이썬에서 인스턴스에 for문을 돌린다고 인지를 하게되면
  2. 인스턴스에 선언된 클래스에서 __iter__(self)를 자동으로 호출한다.
  3. __iter__는 yeild로 v를 리턴한다.
  4. while 문에서 v는 자동으로 다음 값이 할당된다.
  5. yeild된 값은 for문의 원소로 리턴이 가능하게된다.
  6. 다시 for문이 호출되고, 다음 v가 리턴되게된다.
  7. 만약 while문이 종료되고 __iter__(self)에서 yeild가 명령되지 않으면 자동으로 StopIterator Errormessage가 출력되면서 for문이 종료된다.

참고자료

개인적인 공부를 위한 글이며, 모든 저작권은 신천수 교수님께 있습니다.
자세한 강의 내용은 신천수 교수님 강의를 참고하시면 좋을 것 같습니다.
😇 신천수 교수님 자료구조 강의

댓글남기기