본 시리즈는 프로그래밍 자료구조의 개념을 정리하고 실습을 진행해보는 시리즈입니다.
LinkedList
를 제공하지 않으므로, 직접 Node
와 LinkedList
클래스를 구현해야 합니다. java.util.LinkedList
클래스를 통해 이미 구현된 연결 리스트를 사용할 수 있으며, 이는 이중 연결 리스트로 구현되어 있습니다.연결 리스트는 크게 단일 연결 리스트(Singly Linked List), 이중 연결 리스트(Doubly Linked List), 원형 연결 리스트(Circular Linked List)로 분류됩니다.
LinkedList
를 기본적으로 제공하지 않기 때문에 직접 구현해야 합니다.java.util.LinkedList
클래스를 통해 이미 구현된 연결 리스트를 바로 사용할 수 있습니다.참고: 이번 포스팅에서는 단일 연결 리스트와 이중 연결 리스트의 구조와 구현 방법에 집중하며, 원형 연결 리스트는 자세히 다루지 않습니다.
단일 연결 리스트는 각 노드가 하나의 다음 노드만 가리키며 연결되는 단방향 자료구조입니다.
Java에서의 LinkedList 사용 예시
Java의 LinkedList
는 사실 이중 연결 리스트로 구현되어 있지만, 단일 연결 리스트처럼 사용할 수 있습니다.
LinkedList
를 이용한 단일 연결 리스트처럼 사용하는 예입니다.import java.util.LinkedList;
public class Main {
public static void main(String[] args) {
LinkedList<Integer> list = new LinkedList<>();
list.add(10); // 끝에 추가 (O(1))
list.add(20);
list.add(30);
list.add(1, 15); // 중간 삽입 (O(n))
System.out.println(list); // [10, 15, 20, 30]
list.remove(2); // 중간 삭제 (O(n))
System.out.println(list); // [10, 15, 30]
}
}
Python에서의 단일 연결 리스트 구현 예시
Python에서는 LinkedList가 내장되어 있지 않기 때문에, 직접 Node
와 SinglyLinkedList
클래스를 구현해야 합니다.
Node
클래스는 데이터와 다음 노드를 가리키는 참조를 저장하며, SinglyLinkedList
클래스는 노드를 관리하는 구조입니다.class Node:
def __init__(self, data):
self.data = data
self.next = None
class SinglyLinkedList:
def __init__(self):
self.head = None
def append(self, data):
# 끝에 노드를 추가하는 연산은 O(1)의 시간 복잡도를 가집니다.
new_node = Node(data)
if not self.head:
self.head = new_node
else:
current = self.head
while current.next:
current = current.next
current.next = new_node
def insert_at(self, index, data):
# 인덱스 위치에 노드를 삽입하는 연산은 O(n)의 시간 복잡도를 가집니다.
if index == 0:
new_node = Node(data)
new_node.next = self.head
self.head = new_node
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 = Node(data)
new_node.next = current.next
current.next = new_node
def display(self):
current = self.head
while current:
print(current.data, end=" -> ")
current = current.next
print("None")
# 실습
slist = SinglyLinkedList()
slist.append(10)
slist.append(20)
slist.append(30)
slist.display() # 10 -> 20 -> 30 -> None
이중 연결 리스트는 각 노드가 다음 노드와 이전 노드를 모두 가리킵니다.
Java에서의 이중 연결 리스트 사용 예시
LinkedList
는 기본적으로 이중 연결 리스트
로 구현되어 있습니다.LinkedList
를 사용한 이중 연결 리스트 예제입니다.LinkedList<Integer> list = new LinkedList<>();
list.add(10); // 끝에 추가 (O(1))
list.add(20);
list.add(30);
list.addFirst(5); // 맨 앞에 삽입 (O(1)) -> [5, 10, 20, 30]
list.removeLast(); // 끝에서 삭제 (O(1)) -> [5, 10, 20]
System.out.println(list); // [5, 10, 20]
addFirst()
는 첫 번째 위치에 노드를 삽입하고, removeLast()
는 마지막 노드를 삭제합니다. Python에서의 이중 연결 리스트 구현 예시
class Node:
def __init__(self, data):
self.data = data
self.prev = None
self.next = None
class DoublyLinkedList:
def __init__(self):
self.head = None
self.tail = None
def append(self, data):
# 끝에 노드를 추가하는 연산은 O(1)의 시간 복잡도를 가집니다.
new_node = Node(data)
if not self.head:
self.head = new_node
self.tail = new_node
else:
self.tail.next = new_node # 기존 tail의 다음 노드로 연결
new_node.prev = self.tail # 새 노드의 이전 노드를 기존 tail로 연결
self.tail = new_node # tail을 새로운 노드로 업데이트
def remove_last(self):
# 마지막 노드를 삭제하는 연산 (O(1))
if not self.tail:
raise IndexError("List is empty")
elif self.head == self.tail:
self.head = None
self.tail = None
else:
self.tail = self.tail.prev
self.tail.next = None
def display(self):
current = self.head
while current:
print(current.data, end=" <-> ")
current = current.next
print("None")
# 실습
dlist = DoublyLinkedList()
dlist.append(10)
dlist.append(20)
dlist.append(30)
dlist.display() # 10 <-> 20 <-> 30 <-> None
dlist.remove_last()
dlist.display() # 10 <-> 20 <-> None
자료구조 | 끝에서 삽입 | 중간 삽입 | 끝에서 삭제 | 중간 삭제 | 탐색 |
---|---|---|---|---|---|
단일 연결 리스트 | O(1) | O(n) | O(n) | O(n) | O(n) |
이중 연결 리스트 | O(1) | O(n) | O(1) | O(n) | O(n) |
LinkedList
를 사용해도 무방하지만, 이번엔 직접 구현해보며 원리를 파악해보도록 하겠습니다.Node
클래스를 정의하고, 각 노드는 data
, next
, prev
필드를 가집니다. DoublyLinkedList
클래스에서 head
와 tail
노드를 관리합니다.class Node {
int data;
Node prev;
Node next;
public Node(int data) {
this.data = data;
this.prev = null;
this.next = null;
}
}
class DoublyLinkedList {
Node head;
Node tail;
public DoublyLinkedList() {
this.head = null;
this.tail = null;
}
}
데이터를 앞/뒤 또는 중간에 삽입하는 메서드를 작성합니다.
addFirst()
메서드는 리스트의 앞에 노드를 삽입하고,addLast()
메서드는 리스트의 뒤에 노드를 삽입하며,insertAt()
메서드는 중간에 데이터를 삽입합니다.class DoublyLinkedList {
// 기존 생성자 생략...
// 리스트의 앞에 노드를 추가하는 메서드
public void addFirst(int data) {
Node newNode = new Node(data);
if (head == null) {
head = tail = newNode;
} else {
newNode.next = head;
head.prev = newNode;
head = newNode;
}
}
// 리스트의 뒤에 노드를 추가하는 메서드
public void addLast(int data) {
Node newNode = new Node(data);
if (tail == null) {
head = tail = newNode;
} else {
tail.next = newNode;
newNode.prev = tail;
tail = newNode;
}
}
// 특정 인덱스에 노드를 삽입하는 메서드
public void insertAt(int index, int data) {
if (index == 0) {
addFirst(data);
return;
}
Node newNode = new Node(data);
Node current = head;
for (int i = 0; i < index - 1; i++) {
if (current == null) {
throw new IndexOutOfBoundsException("Index out of bounds");
}
current = current.next;
}
newNode.next = current.next;
if (current.next != null) {
current.next.prev = newNode;
}
newNode.prev = current;
current.next = newNode;
if (newNode.next == null) {
tail = newNode;
}
}
}
삭제 연산은 앞/뒤 또는 특정 위치에서 노드를 삭제합니다.
removeFirst()
는 리스트의 첫 번째 노드를 삭제하고,removeLast()
는 리스트의 마지막 노드를 삭제하며,removeAt()
는 특정 위치의 노드를 삭제합니다.class DoublyLinkedList {
// 기존 생성 및 삽입 메서드 생략...
// 리스트의 첫 번째 노드를 삭제하는 메서드
public void removeFirst() {
if (head == null) {
throw new IndexOutOfBoundsException("List is empty");
}
if (head == tail) {
head = tail = null;
} else {
head = head.next;
head.prev = null;
}
}
// 리스트의 마지막 노드를 삭제하는 메서드
public void removeLast() {
if (tail == null) {
throw new IndexOutOfBoundsException("List is empty");
}
if (head == tail) {
head = tail = null;
} else {
tail = tail.prev;
tail.next = null;
}
}
// 특정 인덱스의 노드를 삭제하는 메서드
public void removeAt(int index) {
if (index == 0) {
removeFirst();
return;
}
Node current = head;
for (int i = 0; i < index; i++) {
if (current == null) {
throw new IndexOutOfBoundsException("Index out of bounds");
}
current = current.next;
}
if (current == tail) {
removeLast();
} else {
current.prev.next = current.next;
if (current.next != null) {
current.next.prev = current.prev;
}
}
}
}
removeAt
메서드는 중간에 노드를 삭제할 때 O(n)
의 시간 복잡도를 가집니다. 리스트를 순방향 또는 역방향으로 탐색하면서 데이터를 출력하는 메서드를 작성합니다.
class DoublyLinkedList {
// 기존 생성, 삽입, 삭제 메서드 생략...
// 리스트의 모든 노드를 순방향으로 출력하는 메서드
public void displayForward() {
Node current = head;
while (current != null) {
System.out.print(current.data + " <-> ");
current = current.next;
}
System.out.println("None");
}
// 리스트의 모든 노드를 역방향으로 출력하는 메서드
public void displayBackward() {
Node current = tail;
while (current != null) {
System.out.print(current.data + " <-> ");
current = current.prev;
}
System.out.println("None");
}
}
DoublyLinkedList
클래스의 모든 주요 연산이 구현되었습니다. public class Main {
public static void main(String[] args) {
DoublyLinkedList list = new DoublyLinkedList();
list.addLast(10);
list.addLast(20);
list.addLast(30);
list.displayForward(); // 10 <-> 20 <-> 30 <-> None
list.addFirst(5);
list.displayForward(); // 5 <-> 10 <-> 20 <-> 30 <-> None
list.removeAt(2);
list.displayForward(); // 5 <-> 10 <-> 30 <-> None
list.insertAt(2, 15);
list.displayForward(); // 5 <-> 10 <-> 15 <-> 30 <-> None
list.displayBackward(); // 30 <-> 15 <-> 10 <-> 5 <-> None
}
}
Node
와 DoublyLinkedList
클래스를 정의하여 각 노드의 data
, prev
, next
를 관리합니다.먼저 Node
클래스를 정의하고, DoublyLinkedList
클래스에서 head
와 tail
노드를 관리합니다.
class Node:
def __init__(self, data):
self.data = data
self.prev = None
self.next = None
class DoublyLinkedList:
def __init__(self):
self.head = None
self.tail = None
다음으로, 앞/뒤 또는 중간에 데이터를 삽입하는 메서드를 작성합니다.
add_first()
는 리스트의 앞에 노드를 삽입하고,add_last()
는 리스트의 뒤에 노드를 삽입하며,insert_at()
는 중간에 데이터를 삽입합니다.class DoublyLinkedList:
# 기존 생성자 생략...
# 리스트의 앞에 노드를 추가하는 메서드
def add_first(self, data):
new_node = Node(data)
if self.head is None:
self.head = self.tail = new_node
else:
new_node.next = self.head
self.head.prev = new_node
self.head = new_node
# 리스트의 뒤에 노드를 추가하는 메서드
def add_last(self, data):
new_node = Node(data)
if self.tail is None:
self.head = self.tail = new_node
else:
self.tail.next = new_node
new_node.prev = self.tail
self.tail = new_node
# 특정 인덱스에 노드를 삽입하는 메서드
def insert_at(self, index, data):
if index == 0:
self.add_first(data)
return
new_node = Node(data)
current = self.head
for _ in range(index - 1):
if current is None:
raise IndexError("Index out of bounds")
current = current.next
new_node.next = current.next
if current.next is not None:
current.next.prev = new_node
new_node.prev = current
current.next = new_node
if new_node.next is None:
self.tail = new_node
데이터 삭제 연산도 앞/뒤 또는 특정 위치에서 노드를 삭제하는 메서드를 작성합니다.
remove_first()
는 리스트의 첫 번째 노드를 삭제하고,remove_last()
는 리스트의 마지막 노드를 삭제하며,remove_at()
는 특정 위치의 노드를 삭제합니다.class DoublyLinkedList:
# 기존 생성자 및 삽입 메서드 생략...
# 리스트의 첫 번째 노드를 삭제하는 메서드
def remove_first(self):
if self.head is None:
raise IndexError("List is empty")
if self.head == self.tail:
self.head = self.tail = None
else:
self.head = self.head.next
self.head.prev = None
# 리스트의 마지막 노드를 삭제하는 메서드
def remove_last(self):
if self.tail is None:
raise IndexError("List is empty")
if self.head == self.tail:
self.head = self.tail = None
else:
self.tail = self.tail.prev
self.tail.next = None
# 특정 인덱스의 노드를 삭제하는 메서드
def remove_at(self, index):
if index == 0:
self.remove_first()
return
current = self.head
for _ in range(index):
if current is None:
raise IndexError("Index out of bounds")
current = current.next
if current == self.tail:
self.remove_last()
else:
current.prev.next = current.next
if current.next is not None:
current.next.prev = current.prev
class DoublyLinkedList:
# 기존 생성, 삽입, 삭제 메서드 생략...
# 리스트의 모든 노드를 순방향으로 출력하는 메서드
def display_forward(self):
current = self.head
while current:
print(current.data, end=" <-> ")
current = current.next
print("None")
# 리스트의 모든 노드를 역방향으로 출력하는 메서드
def display_backward(self):
current = self.tail
while current:
print(current.data, end=" <-> ")
current = current.prev
print("None")
DoublyLinkedList
클래스의 모든 주요 연산이 구현되었습니다. # 테스트
dlist = DoublyLinkedList()
dlist.add_last(10)
dlist.add_last(20)
dlist.add_last(30)
dlist.display_forward() # 10 <-> 20 <-> 30 <-> None
dlist.add_first(5)
dlist.display_forward() # 5 <-> 10 <-> 20 <-> 30 <-> None
dlist.remove_at(2)
dlist.display_forward() # 5 <-> 10 <-> 30 <-> None
dlist.display_backward() # 30 <-> 10 <-> 5 <-> None
원형 연결 리스트는 마지막 노드가 첫 번째 노드를 가리키는 구조로, 연결 리스트의 끝과 처음이 연결된 순환형 구조입니다.
원형 연결 리스트의 주요 특징:
next
가 첫 번째 노드를 가리킴.next
가 첫 번째 노드를, 첫 번째 노드의 prev
가 마지막 노드를 가리킴.원형 연결 리스트 활용 예시:
원형 연결 리스트 구현 예시: Java
class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
this.next = null;
}
}
class CircularLinkedList {
Node head = null;
Node tail = null;
public void addLast(int data) {
Node newNode = new Node(data);
if (head == null) {
head = newNode;
tail = newNode;
tail.next = head; // 원형 구조
} else {
tail.next = newNode;
tail = newNode;
tail.next = head;
}
}
public void display() {
Node current = head;
if (head != null) {
do {
System.out.print(current.data + " -> ");
current = current.next;
} while (current != head);
System.out.println("(head)");
}
}
}
원형 연결 리스트 구현 예시: Python
class Node:
def __init__(self, data):
self.data = data
self.next = None
class CircularLinkedList:
def __init__(self):
self.head = None
self.tail = None
def add_last(self, data):
new_node = Node(data)
if self.head is None:
self.head = self.tail = new_node
self.tail.next = self.head # 원형 구조
else:
self.tail.next = new_node
self.tail = new_node
self.tail.next = self.head
def display(self):
current = self.head
if self.head is not None:
while True:
print(current.data, end=" -> ")
current = current.next
if current == self.head:
break
print("(head)")
Queue와 Deque는 자료구조에서 자주 사용하는 개념입니다.
Java의 LinkedList
는 Queue
와 Deque
인터페이스를 구현하고 있으므로, 간단한 코드로 Queue와 Deque로 활용할 수 있습니다.
import java.util.LinkedList;
import java.util.Queue;
import java.util.Deque;
public class Main {
public static void main(String[] args) {
// Queue로 LinkedList 사용
Queue<Integer> queue = new LinkedList<>();
queue.add(10);
queue.add(20);
System.out.println(queue.poll()); // 10 출력
// Deque로 LinkedList 사용
Deque<Integer> deque = new LinkedList<>();
deque.addFirst(5);
deque.addLast(10);
System.out.println(deque.removeFirst()); // 5 출력
System.out.println(deque.removeLast()); // 10 출력
}
}
Python에서는 collections.deque
를 사용하여 큐와 덱의 기능을 사용할 수 있습니다.
collections.deque
는 양방향 큐로 동작하며, 양쪽 끝에서 삽입/삭제가 O(1)의 시간 복잡도를 가집니다. LinkedList
보다 효율적인 구현이기 때문에, Python에서는 대부분의 경우 deque
를 사용하는 것이 더 좋은 선택입니다. deque
는 리스트 연산보다 메모리 사용량도 적어 성능이 매우 뛰어납니다.from collections import deque
# Queue로 deque 사용
queue = deque()
queue.append(10)
queue.append(20)
print(queue.popleft()) # 10 출력
# Deque로 deque 사용
deque_obj = deque()
deque_obj.appendleft(5)
deque_obj.append(10)
print(deque_obj.popleft()) # 5 출력
print(deque_obj.pop()) # 10 출력
연결 리스트의 메모리 사용량 분석
next
, prev
)를 유지해야 하므로, 노드 수가 많아질수록 배열에 비해 메모리 사용량이 더 커집니다.LinkedList
는 100개의 데이터 필드뿐만 아니라 100개의 추가 포인터 필드를 유지해야 합니다. 반면, 배열은 하나의 연속된 메모리 블록에 모든 데이터를 저장하므로, 포인터에 대한 추가 비용이 들지 않습니다.Java에서 LinkedList
와 ArrayList
는 모두 동일한 List
인터페이스를 구현한 구현체이지만, 각기 다른 상황에서 적합한 성능을 보여줍니다.
자료구조 | 삽입 시간 복잡도 | 삭제 시간 복잡도 | 탐색 시간 복잡도 |
---|---|---|---|
ArrayList | O(n) (중간) | O(n) (중간) | O(1) (인덱스 접근) |
LinkedList | O(1) (앞/뒤) | O(1) (앞/뒤) | O(n) (순차 탐색) |
ArrayList
는 인덱스 접근이 빠르지만, 중간 삽입/삭제 시 모든 요소를 이동해야 하므로 성능이 떨어집니다. LinkedList
는 중간 삽입/삭제 시 포인터만 변경하면 되므로 유리하지만, 탐색 속도가 느리다는 단점이 있습니다.다만, 실무에서는 대부분의 경우 ArrayList
가 LinkedList
보다 더 나은 성능을 제공합니다.
ArrayList
가 내부적으로 고속 복사 메서드인 System.arraycopy
를 사용하기 때문입니다. ArrayList
는 데이터 블록을 통째로 복사하는 방식으로 상대적으로 빠르게 처리할 수 있습니다.따라서 대용량 데이터를 처리할 때도 LinkedList
보다 ArrayList
가 더 효율적인 경우가 많습니다.
ArrayList
는 읽기/탐색이 빈번한 경우 적합합니다. 특히 대규모 데이터를 순차적으로 접근할 때 유리합니다.LinkedList
는 대용량 데이터의 삽입/삭제가 빈번한 경우 적합하며, 특히 양방향 탐색이나 큐/덱 구조에서 유리합니다.참고 : 굳이 이렇게 분류했지만, 실무에서는 일반적으로 ArrayList
가 LinkedList
보다 훨씬 더 좋은 성능을 나타내기 때문에 List 자료형이 필요한 경우 ArrayList
를 쓰면 된다고 생각하시면 됩니다.
이번 포스팅에서는 LinkedList의 구조와 구현 방법에 대해 자세히 알아보았습니다.
Java의 경우 실제 개발에서는 대용량 데이터를 처리할 때는 ArrayList를 사용하는 것이 대부분 더 적합합니다.
다음 포스팅부터는 Stack/Queue/Deque 자료구조와 그 활용에 대해 다루어 보도록 하겠습니다.