연결 리스트 : 초보자도 파이썬으로 이해하는 자료구조

기본적으로 “연결 리스트”는 요소가 노드에 저장되는 선형 데이터 구조입니다. 배열과 달리 연결된 목록에는 연속적인 메모리 공간이 필요하지 않습니다. 대신, 각 요소(노드)에는 데이터와 시퀀스의 다음 노드에 대한 참조(링크 또는 포인터)가 포함됩니다.

노드

연결 리스트는 “데이터 + 포인터”를 포함하는 컨테이너인 노드로 구성되어 있습니다.

데이터: 정수, 문자 또는 기타 데이터 유형과 같이 저장하려는 실제 정보입니다.

다음 포인터: 시퀀스의 다음 노드를 가리키는 링크 또는 포인터입니다. 마지막 노드에서 이 포인터는 일반적으로 목록의 끝을 나타내는 NULL을 가리킵니다.

연결 리스트의 장점

동적 크기

연결 리스트는 런타임 중에 실제 데이터 크기에 맞춰 동적으로 늘어나거나 줄어들 수 있습니다.

효율적인 삽입 및 삭제

연결 리스트에서 요소를 추가하거나 제거하는 것은 배열과 비교할 때 효율적입니다.

파이썬에서 연결 리스트

Python에는 목록, 튜플, 집합 및 사전과 같은 내장 데이터 구조가 있는 것과 같은 방식의 내장 연결 리스트 데이터 구조는 없습니다…!

Python에 내장된 ‘리스트’은 연결 리스트가 아닌 동적 배열입니다.

유사한 기능을 제공하지만 다르게 구현되어 인덱스 및 동적 크기 조정을 통해 요소에 대한 지속적인 액세스를 제공합니다.

파이썬에서 연결 리스트 구현

파이썬에서 직접 linked list를 구현하려면 아래와 같이 구현할 수 있습니다.

class Node:
    # 노드 클래스 정의: 연결 리스트의 각 요소(노드)를 나타냄
    def __init__(self, data):
        self.data = data  # 노드가 저장할 데이터
        self.next = None  # 다음 노드를 가리키는 포인터, 초기값은 None

class LinkedList:
    # 연결 리스트 클래스 정의
    def __init__(self):
        self.head = None  # 리스트의 시작 노드를 가리키는 head, 초기값은 None

    def print_list(self):
        # 리스트의 모든 요소를 출력하는 메소드
        current = self.head  # 현재 위치를 리스트의 시작으로 설정
        while current:
            # 리스트의 끝에 도달할 때까지 반복
            print(current.data, end=" -> ")  # 현재 노드의 데이터 출력
            current = current.next  # 다음 노드로 이동
        print("None")  # 리스트의 끝 표시

    def clear(self):
        # 리스트의 모든 요소를 제거하는 메소드
        self.head = None  # head를 None으로 설정하여 리스트 초기화

    def insert_at_index(self, index, data):
        # 특정 인덱스에 새로운 노드를 삽입하는 메소드
        new_node = Node(data)  # 새로운 노드 생성
        if index == 0:
            # 리스트의 시작 부분에 삽입하는 경우
            new_node.next = self.head  # 새 노드의 다음 노드를 현재 head로 설정
            self.head = new_node  # 새 노드를 새로운 head로 설정
            return
        current = self.head
        for i in range(index - 1):
            # 삽입할 위치의 바로 이전 노드까지 이동
            if current is None:
                raise IndexError("Index out of bounds")  # 인덱스 범위 초과 예외 처리
            current = current.next
        new_node.next = current.next  # 새 노드의 다음 노드를 현재 노드의 다음 노드로 설정
        current.next = new_node  # 현재 노드의 다음 노드를 새 노드로 설정

    def append(self, data):
        # 리스트의 끝에 새로운 노드를 추가하는 메소드
        new_node = Node(data)  # 새로운 노드 생성
        if self.head is None:
            # 리스트가 비어있는 경우
            self.head = new_node  # 새 노드를 head로 설정
            return
        last = self.head
        while last.next:
            # 리스트의 마지막 노드를 찾을 때까지 이동
            last = last.next
        last.next = new_node  # 마지막 노드의 다음 노드를 새 노드로 설정

    def delete_at_index(self, index):
        # 특정 인덱스의 노드를 삭제하는 메소드
        if self.head is None:
            # 리스트가 비어있는 경우
            return
        if index == 0:
            # 첫 번째 노드를 삭제하는 경우
            self.head = self.head.next  # head를 다음 노드로 업데이트
            return
        current = self.head
        for i in range(index - 1):
            # 삭제할 노드의 바로 이전 노드까지 이동
            if current.next is None:
                raise IndexError("Index out of bounds")  # 인덱스 범위 초과 예외 처리
            current = current.next
        current.next = current.next.next if current.next else None  # 삭제할 노드를 연결 리스트에서 제거

    def pop(self):
        # 리스트의 마지막 요소를 삭제하는 메소드
        if self.head is None:
            # 리스트가 비어있는 경우
            return
        if self.head.next is None:
            # 리스트에 요소가 하나만 있는 경우
            self.head = None
            return
        current = self.head
        while current.next.next:
            # 마지막 요소의 바로 이전 요소까지 이동
            current = current.next
        current.next = None  # 마지막 요소를 제거

    def get(self, index):
        # 특정 인덱스의 노드의 데이터를 반환하는 메소드
        current = self.head
        for i in range(index):
            # 지정된 인덱스까지 이동
            if current is None:
                raise IndexError("Index out of bounds")  # 인덱스 범위 초과 예외 처리
            current = current.next
        return current.data if current else None  # 인덱스의 노드 데이터 반환

# 사용 예시
ll = LinkedList()
ll.append(1)
ll.append(2)
ll.insert_at_index(1, 3)
ll.print_list()  # 1 -> 3 -> 2 -> None
print("Element at index 1:", ll.get(1))  # 출력: 3
ll.delete_at_index(1)
ll.print_list()  # 출력: 1 -> 2 -> None
ll.pop()
ll.print_list()  # 출력: 1 -> None
ll.clear()
ll.print_list()  # 출력: None

배열 vs 연결 리스트

측면정적 배열동적 배열연결 리스트
크기 조절크기가 고정됨.크기를 동적으로 조절할 수 있음.크기를 동적으로 조절할 수 있음.
메모리 사용량크기가 고정되어 있어 예측 가능.크기에 따라 메모리 할당이 동적으로 조절됨.노드마다 추가적인 포인터로 인한 메모리 오버헤드.
요소 추가 및 삭제비효율적. 삽입 또는 삭제 시 다른 요소를 이동해야 함.동적으로 크기를 조절하므로 삽입 및 삭제가 효율적.노드를 추가하거나 삭제할 때 노드만 조작하면 되므로 효율적.
접근 시간인덱스를 통한 직접적인 접근이 가능.인덱스를 통한 접근이 가능하며, 동적 크기 조절에 따른 성능 저하 가능.다음 노드를 찾아가야 하므로 일반적으로 접근 속도가 느림.
메모리 단편화단편화 없음.메모리 할당 및 해제로 인한 단편화 가능.노드 추가 및 삭제로 인한 단편화 가능.
구현의 복잡성간단하고 직관적.동적 크기 조절 및 메모리 관리로 인한 구현이 복잡.노드 간의 연결 및 메모리 할당 관리로 인한 구현이 복잡.
용도크기가 고정되어 있고 빠른 접근이 필요한 경우.크기의 동적 변경이 필요하며 삽입 및 삭제가 빈번한 경우.크기가 동적으로 변하며, 삽입 및 삭제가 자주 발생하는 경우.

참고하면 좋은 글

Data Structure : Introduction to Linked List – Codeforwin

Leave a Comment

목차