저번 시간에는 큐 자료구조에 대해 알아보았는데요.
오늘은 링크드 리스트에 대한 내용을 알아보도록 하겠습니다.
링크드 리스트(Linked List)는 각 요소(Node)가 데이터와 다음 요소를 가리키는 포인터를 가지고 있는 선형 데이터 구조입니다. 배열과 달리, 링크드 리스트의 요소들은 메모리상에서 연속적으로 위치하지 않으며, 각 요소는 다른 요소의 주소를 포함하여 리스트를 형성합니다.
이제 파이썬으로 단일 연결 리스트(Singly Linked List)를 구현하는 방법을 살펴보겠습니다.
class Node:
def __init__(self,data):
self.data = data # 노드가 저장하는 데이터
self.next = None # 다음 노드를 가리키는 포인터
class LinkedList:
def __init__(self,data):
self.head = Node(data) # 링크드 리스트의 첫번째 데이터
def append(self,data):
if self.head is None:
self.head = Node(data)
else:
node = self.head
while node.next:
node = node.next
node.next = Node(data)
def print_linked_list(self):
current = self.head
while current:
print(current.data,end=' -> ')
current = current.next
print('None')
def delete(self,data):
if self.head == '':
print('This data is not in our linked list')
return
if self.head.data == data:
tmp = self.head
self.head = self.head.next
tmp = None
else:
current = self.head
while current.next:
if current.next.data == data:
tmp = current.next
current.next = current.next.next
tmp = None
return
else:
current = current.next
def insert_before(self,target_data,data): # target_data 앞에 data를 삽입하는 메서드
if self.head is None:
print('Linked List is empty')
return
if self.head.data == target_data:
new_node = Node(data)
new_node.next = self.head
self.head = new_node
return
current = self.head
while current.next and current.next.data != target_data:
current = current.next
if current.next is None:
print(f"Node with data {target_data} not found.")
else: # current.next가 target_data일 때
new_node = Node(data)
new_node.next = current.next
current.next = new_node
def insert_after(self,target_data,data): # target_data 뒤에 data를 삽입하는 메서드
if self.head is None:
print('Linked List is empty')
return
current = self.head
while current is not None:
if current.data == target_data:
new_node = Node(data)
new_node.next = current.next
current.next = new_node
return
current = current.next
# target_data를 찾지 못한 경우
print(f'Node with data {target_data} not found.')
# 링크드 리스트 사용 예제
if __name__ == '__main__':
ll = LinkedList(0)
# 1부터 11 넣기
for i in range(1,11):
ll.append(i)
# 결과 출력
ll.print_linked_list() # 0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 -> None
ll.delete(5)
ll.print_linked_list() # 0 -> 1 -> 2 -> 3 -> 4 -> 6 -> 7 -> 8 -> 9 -> 10 -> None
# 5 앞에 99 삽입
ll.insert_before(5, 99) # Node with data 5 not found.
ll.print_linked_list() # 0 -> 1 -> 2 -> 3 -> 4 -> 6 -> 7 -> 8 -> 9 -> 10 -> None
# 6 앞에 99 삽입
ll.insert_before(6, 99)
ll.print_linked_list() # 0 -> 1 -> 2 -> 3 -> 4 -> 99 -> 6 -> 7 -> 8 -> 9 -> 10 -> None
# 0 뒤에 15 삽입 (헤드 노드 뒤에 삽입하는 경우)
ll.insert_after(0, 15)
ll.print_linked_list() # 0 -> 15 -> 1 -> 2 -> 3 -> 4 -> 99 -> 6 -> 7 -> 8 -> 9 -> 10 -> None
# 8 뒤에 77 삽입
ll.insert_after(8,77)
ll.print_linked_list() # 0 -> 15 -> 1 -> 2 -> 3 -> 4 -> 99 -> 6 -> 7 -> 8 -> 77 -> 9 -> 10 -> None
위의 코드에서 링크드 리스트의 기본 연산인 삽입(append)과 출력(print_list)을 구현했습니다. 다음으로 추가할 수 있는 연산은 다음과 같습니다:
위에서 파이썬으로 링크드 리스트를 구현하는 방법을 살펴보았습니다. 이제 각 메서드의 동작을 하나씩 자세히 설명하며, 어떻게 링크드 리스트가 작동하는지 이해해 보도록 하겠습니다.
링크드 리스트는 Node
라는 기본 단위로 구성됩니다. Node
클래스는 각 노드가 저장하는 데이터와 다음 노드를 가리키는 포인터를 가지고 있습니다.
class Node:
def __init__(self, data):
self.data = data # 노드가 저장하는 데이터
self.next = None # 다음 노드를 가리키는 포인터
data
: 노드가 저장하는 실제 데이터입니다.next
: 다음 노드를 가리키는 포인터입니다. 새 노드를 생성할 때는 이 포인터가 None
으로 설정됩니다.LinkedList
클래스는 전체 리스트를 관리합니다. 이 클래스는 리스트의 첫 번째 노드인 헤드를 저장하고, 여러 가지 리스트 연산을 제공합니다.
__init__
메서드리스트의 첫 번째 노드를 초기화합니다. 이 메서드는 리스트가 생성될 때 호출됩니다.
def __init__(self, data):
self.head = Node(data) # 링크드 리스트의 첫 번째 노드
self.head
는 리스트의 시작을 가리키는 중요한 포인터입니다. 초기화할 때 리스트의 첫 번째 데이터를 받아 이 데이터를 가지는 첫 번째 노드를 생성합니다.append
메서드리스트의 끝에 새로운 노드를 추가하는 메서드입니다.
def append(self, data):
if self.head is None:
self.head = Node(data)
else:
node = self.head
while node.next:
node = node.next
node.next = Node(data)
next
포인터를 새로운 노드로 설정합니다.print_linked_list
메서드리스트의 모든 노드를 출력하는 메서드입니다.
def print_linked_list(self):
current = self.head
while current:
print(current.data, end=' -> ')
current = current.next
print('None')
next
포인터를 따라가며 리스트의 끝까지 출력합니다. 리스트의 마지막에는 None
을 출력하여 리스트의 끝임을 나타냅니다.delete
메서드특정 데이터를 가진 노드를 삭제하는 메서드입니다.
def delete(self, data):
if self.head is None:
print('This data is not in our linked list')
return
if self.head.data == data:
tmp = self.head
self.head = self.head.next
tmp = None
else:
current = self.head
while current.next:
if current.next.data == data:
tmp = current.next
current.next = current.next.next
tmp = None
return
else:
current = current.next
insert_before
메서드특정 노드 앞에 새로운 노드를 삽입하는 메서드입니다.
def insert_before(self, target_data, data): # target_data 앞에 data를 삽입하는 메서드
if self.head is None:
print('Linked List is empty')
return
if self.head.data == target_data:
new_node = Node(data)
new_node.next = self.head
self.head = new_node
return
current = self.head
while current.next and current.next.data != target_data:
current = current.next
if current.next is None:
print(f"Node with data {target_data} not found.")
else: # current.next가 target_data일 때
new_node = Node(data)
new_node.next = current.next
current.next = new_node
target_data
가 헤드 노드의 데이터와 일치하면, 새로운 노드를 헤드로 설정합니다.target_data
를 가진 노드를 찾을 때까지 리스트를 순회하고, 해당 노드 앞에 새로운 노드를 삽입합니다.insert_after
메서드특정 노드 뒤에 새로운 노드를 삽입하는 메서드입니다.
def insert_after(self, target_data, data): # target_data 뒤에 data를 삽입하는 메서드
if self.head is None:
print('Linked List is empty')
return
current = self.head
while current is not None:
if current.data == target_data:
new_node = Node(data)
new_node.next = current.next
current.next = new_node
return
current = current.next
print(f'Node with data {target_data} not found.')
target_data
를 가진 노드를 찾을 때까지 리스트를 순회하고, 해당 노드 뒤에 새로운 노드를 삽입합니다.마지막으로, 구현한 메서드들을 활용하여 링크드 리스트를 어떻게 사용하는지 예제를 통해 살펴보겠습니다.
# 링크드 리스트 사용 예제
if __name__ == '__main__':
ll = LinkedList(0)
# 1부터 10까지 데이터를 삽입
for i in range(1, 11):
ll.append(i)
# 리스트 출력
ll.print_linked_list() # 0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 -> None
# 특정 노드 삭제
ll.delete(5)
ll.print_linked_list() # 0 -> 1 -> 2 -> 3 -> 4 -> 6 -> 7 -> 8 -> 9 -> 10 -> None
# 특정 노드 앞에 데이터 삽입
ll.insert_before(6, 99)
ll.print_linked_list() # 0 -> 1 -> 2 -> 3 -> 4 -> 99 -> 6 -> 7 -> 8 -> 9 -> 10 -> None
# 특정 노드 뒤에 데이터 삽입
ll.insert_after(8, 77)
ll.print_linked_list() # 0 -> 1 -> 2 -> 3 -> 4 -> 99 -> 6 -> 7 -> 8 -> 77 -> 9 -> 10 -> None
이 코드는 링크드 리스트의 다양한 연산을 보여주는 좋은 예제입니다. 삽입, 삭제, 그리고 리스트 출력과 같은 기본적인 연산들이 어떻게 작동하는지 확인할 수 있습니다.
흐음,, 좀 복잡하죠? 하지만 한 발 더 남았습니다.😔
할 수 있습니다!! 다시 집중해볼까요?
이중 연결 리스트는 단일 연결 리스트와 비슷하지만, 각 노드가 두 개의 포인터(next
와 prev
)를 가지는 구조로, 양방향으로 리스트를 순회할 수 있습니다. 이중 연결 리스트는 삽입과 삭제 작업이 양쪽 방향에서 모두 가능하기 때문에 더 유연한 데이터 구조를 제공합니다.
next
와 prev
)를 가지기 때문에 메모리 사용이 더 큽니다.이제 이중 연결 리스트를 파이썬으로 구현하는 방법을 살펴보겠습니다.
class Node:
def __init__(self, data):
self.data = data # 노드가 저장하는 데이터
self.next = None # 다음 노드를 가리키는 포인터
self.prev = None # 이전 노드를 가리키는 포인터
class DoublyLinkedList:
def __init__(self, data):
self.head = Node(data) # 리스트의 첫 번째 노드
def append(self, data):
if self.head is None:
self.head = Node(data)
else:
node = self.head
while node.next:
node = node.next
new_node = Node(data)
node.next = new_node
new_node.prev = node # 새 노드의 prev를 마지막 노드로 설정
def print_linked_list(self):
current = self.head
while current:
print(current.data, end=' <-> ')
current = current.next
print('None')
def delete(self, data):
if self.head is None:
print('This data is not in our linked list')
return
if self.head.data == data:
tmp = self.head
self.head = self.head.next
if self.head:
self.head.prev = None # 헤드 노드의 prev를 None으로 설정
tmp = None
else:
current = self.head
while current.next:
if current.next.data == data:
tmp = current.next
current.next = current.next.next
if current.next:
current.next.prev = current # 삭제된 노드의 다음 노드의 prev를 현재 노드로 설정
tmp = None
return
else:
current = current.next
def insert_before(self, target_data, data):
if self.head is None:
print('Linked List is empty')
return
if self.head.data == target_data:
new_node = Node(data)
new_node.next = self.head
self.head.prev = new_node
self.head = new_node
return
current = self.head
while current.next and current.next.data != target_data:
current = current.next
if current.next is None:
print(f"Node with data {target_data} not found.")
else:
new_node = Node(data)
new_node.next = current.next
new_node.prev = current
current.next.prev = new_node
current.next = new_node
def insert_after(self, target_data, data):
if self.head is None:
print('Linked List is empty')
return
current = self.head
while current is not None:
if current.data == target_data:
new_node = Node(data)
new_node.next = current.next
new_node.prev = current
if current.next:
current.next.prev = new_node
current.next = new_node
return
current = current.next
print(f'Node with data {target_data} not found.')
위 코드에서는 이중 연결 리스트의 기본 연산을 구현했습니다. 이제 각 메서드가 어떻게 작동하는지 살펴보겠습니다.
append
메서드리스트의 끝에 새로운 노드를 추가합니다.
def append(self, data):
if self.head is None:
self.head = Node(data)
else:
node = self.head
while node.next:
node = node.next
new_node = Node(data)
node.next = new_node
new_node.prev = node # 새 노드의 prev를 마지막 노드로 설정
prev
포인터는 마지막 노드를 가리킵니다.print_linked_list
메서드리스트의 모든 노드를 출력합니다.
def print_linked_list(self):
current = self.head
while current:
print(current.data, end=' <-> ')
current = current.next
print('None')
<->
를 사용합니다.delete
메서드특정 데이터를 가진 노드를 삭제합니다.
def delete(self, data):
if self.head is None:
print('This data is not in our linked list')
return
if self.head.data == data:
tmp = self.head
self.head = self.head.next
if self.head:
self.head.prev = None # 헤드 노드의 prev를 None으로 설정
tmp = None
else:
current = self.head
while current.next:
if current.next.data == data:
tmp = current.next
current.next = current.next.next
if current.next:
current.next.prev = current # 삭제된 노드의 다음 노드의 prev를 현재 노드로 설정
tmp = None
return
else:
current = current.next
next
와 prev
포인터를 적절히 업데이트하여 노드를 리스트에서 제거합니다.insert_before
메서드특정 노드 앞에 새로운 노드를 삽입합니다.
def insert_before(self, target_data, data):
if self.head is None:
print('Linked List is empty')
return
if self.head.data == target_data:
new_node = Node(data)
new_node.next = self.head
self.head.prev = new_node
self.head = new_node
return
current = self.head
while current.next and current.next.data != target_data:
current = current.next
if current.next is None:
print(f"Node with data {target_data} not found.")
else:
new_node = Node(data)
new_node.next = current.next
new_node.prev = current
current.next.prev = new_node
current.next = new_node
target_data
를 가진 노드를 찾아 그 앞에 새로운 노드를 삽입합니다. next
와 prev
포인터를 적절히 설정하여 새로운 노드를 연결합니다.insert_after
메서드특정 노드 뒤에 새로운 노드를 삽입합니다.
def insert_after(self, target_data, data):
if self.head is None:
print('Linked List is empty')
return
current = self.head
while current is not None:
if current.data == target_data:
new_node = Node(data)
new_node.next = current.next
new_node.prev = current
if current.next:
current.next.prev = new_node
current.next = new_node
return
current = current.next
print(f'Node with data {target_data} not found.')
target_data
를 가진 노드를 찾아 그 뒤에 새로운 노드를 삽입합니다. 마찬가지로 next
와 prev
포인터를 설정하여 연결합니다.마지막으로, 구현한 이중 연결 리스트를 사용하는 예제를 살펴보겠습니다.
# 이중 연결 리스트 사용 예제
if __name__ == '__main__':
dll = DoublyLinkedList(0)
# 1부터 10까지 데이터를 삽입
for i in range(1, 11):
dll.append(i)
# 리스트 출력
dll.print_linked_list
() # 0 <-> 1 <-> 2 <-> 3 <-> 4 <-> 5 <-> 6 <-> 7 <-> 8 <-> 9 <-> 10 <-> None
# 특정 노드 삭제
dll.delete(5)
dll.print_linked_list() # 0 <-> 1 <-> 2 <-> 3 <-> 4 <-> 6 <-> 7 <-> 8 <-> 9 <-> 10 <-> None
# 특정 노드 앞에 데이터 삽입
dll.insert_before(4, 99)
dll.print_linked_list() # 0 <-> 1 <-> 2 <-> 3 <-> 99 <-> 4 <-> 6 <-> 7 <-> 8 <-> 9 <-> 10 <-> None
# 특정 노드 뒤에 데이터 삽입
dll.insert_after(7, 77)
dll.print_linked_list() # 0 <-> 1 <-> 2 <-> 3 <-> 99 <-> 4 <-> 6 <-> 7 <-> 77 <-> 8 <-> 9 <-> 10 <-> None
이중 연결 리스트는 단일 연결 리스트보다 더 유연한 데이터 구조를 제공합니다. 양방향으로 리스트를 순회할 수 있기 때문에, 리스트의 양 끝에서 삽입과 삭제가 더 효율적으로 이루어질 수 있습니다. 이중 연결 리스트를 구현하면서 포인터(next
, prev
)를 올바르게 관리하는 것이 중요합니다.
이중 연결 리스트와 단일 연결 리스트의 차이점을 이해하고, 각각의 장단점을 고려하여 상황에 맞는 데이터 구조를 선택하는 것이 중요합니다.
이번 글에서는 단일 연결 리스트와 이중 연결 리스트의 개념과 파이썬 구현 방법을 자세히 살펴보았습니다. 링크드 리스트는 데이터 구조의 기본적인 개념 중 하나로, 메모리 효율성과 삽입/삭제의 유연성에서 큰 장점을 가지고 있습니다. 단일 연결 리스트는 구조가 간단하여 이해하기 쉽고, 이중 연결 리스트는 양방향으로 리스트를 순회할 수 있어 더욱 강력한 기능을 제공합니다.
링크드 리스트의 장점과 단점을 이해하고, 상황에 맞게 적절한 자료 구조를 선택하는 것은 매우 중요합니다. 예를 들어, 빠른 삽입과 삭제가 중요한 경우에는 링크드 리스트가 좋은 선택이 될 수 있습니다. 반면, 인덱스를 통한 빠른 접근이 필요하다면 배열과 같은 다른 자료 구조가 더 적합할 수 있습니다.
링크드 리스트의 개념을 확실히 이해하고 직접 구현해보면서, 자료 구조의 기초를 탄탄히 다지는 계기가 되었기를 바랍니다. 앞으로 더 복잡한 자료 구조를 배울 때도 링크드 리스트의 기본 개념이 중요한 역할을 할 것입니다.
하지만! 코딩테스트에서 링크드 리스트 자체를 직접 구현하거나 사용하는 경우는 드뭅니다.
그래도, 링크드 리스트의 개념을 이해하는 것은 여전히 중요합니다. 그 이유는 덱(Deque)과 같은 고수준 자료 구조가 링크드 리스트의 이점을 활용하고 있기 때문입니다.
파이썬에서 collections
모듈의 deque
는 덱(Deque, Double-ended Queue)을 제공하는데, 이 자료 구조는 링크드 리스트의 장점을 일부 가지고 있습니다. 덱은 양쪽 끝에서 삽입과 삭제가 O(1) 시간 복잡도로 이루어질 수 있기 때문에, 효율적인 연산이 필요한 상황에서 매우 유용합니다.
덱은 다음과 같은 경우에 유용합니다:
코딩테스트에서는 덱이 링크드 리스트의 역할을 대신할 수 있는 자료 구조로 자주 사용됩니다. 예를 들어:
코딩테스트에서 링크드 리스트는 직접적으로 사용되지 않지만, 덱과 같은 고수준 자료 구조가 링크드 리스트의 장점을 활용하는 경우가 많습니다. 따라서 링크드 리스트의 개념을 이해하고 덱을 사용하는 방법을 익히는 것은 매우 유용합니다. 덱을 잘 활용하면 코딩테스트에서 다양한 문제를 효율적으로 해결할 수 있습니다.
전체 코드는 아래 링크에서 확인하실 수 있습니다:
Python/자료구조/구현/Linked_List at main · jw9603/Python
다음 시간은 해쉬 테이블에 대해 알아보도록 하겠습니다.
많은 기대 부탁드립니다. 🙏
긴 글 읽어주셔서 감사합니다!