안녕하세요! 이번에는 자바 컬렉션 프레임워크의 또 다른 핵심 자료구조인 LinkedList에 대해 알아보겠습니다. ArrayList에 이어 LinkedList의 개념과 구현 방법, 그리고 두 자료구조의 차이점과 각각 어떤 상황에서 사용하면 좋은지 초보자도 쉽게 이해할 수 있도록 설명해드리겠습니다.
LinkedList란 무엇인가?
LinkedList는 각 노드(Node)가 데이터와 포인터를 가지고 한 줄로 연결되어 있는 방식으로 데이터를 저장하는 자료구조입니다. 각 노드는 데이터와 다음 노드를 가리키는 참조(포인터)로 구성되어 있습니다.
ArrayList와 LinkedList의 핵심 차이점
메모리 확보 방식
- ArrayList: 연속된 메모리 공간에 데이터를 저장하며, 공간이 부족하면 더 큰 배열을 생성하고 복사
- LinkedList: 필요할 때마다 개별 노드를 생성하여 연결하므로 메모리를 효율적으로 사용
데이터 접근 방식
- ArrayList: 인덱스를 통한 직접 접근이 가능하여 조회 속도가 빠름 (O(1))
- LinkedList: 첫 노드부터 순차적으로 탐색해야 하므로 조회 속도가 상대적으로 느림 (O(n))
데이터 변경 특성
- ArrayList: 중간에 데이터 삽입/삭제 시 요소들을 이동시켜야 하므로 성능이 저하됨
- LinkedList: 노드 간의 참조만 변경하면 되므로 중간 삽입/삭제가 빠름
간단히 정리하면, 검색이 많은 작업에는 ArrayList가, 삽입/삭제가 많은 작업에는 LinkedList가 더 적합합니다.
LinkedList의 기본 구조
LinkedList의 가장 기본적인 구성 요소는 **노드(Node)**입니다. 각 노드는 다음과 같은 구조를 가집니다:
public class Node {
Object item; // 데이터를 저장
Node next; // 다음 노드의 참조(주소)
public Node(Object item) {
this.item = item;
}
}
이 노드들이 서로 연결되어 LinkedList를 형성합니다:
[Node A] -> [Node B] -> [Node C] -> null
LinkedList 직접 구현하기
1. 기본 구조 구현
가장 먼저 LinkedList의 기본 구조를 구현해봅시다:
public class MyLinkedListV1 {
private Node first; // 첫 번째 노드
private int size = 0; // 요소 개수
// 기타 메서드들...
}
2. 데이터 추가 (마지막에 추가)
public void add(Object e) {
Node newNode = new Node(e);
if (first == null) {
first = newNode; // 리스트가 비어있으면 첫 노드로 설정
} else {
Node lastNode = getlastNode(); // 마지막 노드 찾기
lastNode.next = newNode; // 마지막 노드의 다음으로 새 노드 연결
}
size++;
}
private Node getlastNode() {
Node x = first;
while (x.next != null) { // 다음 노드가 없을 때까지 진행
x = x.next;
}
return x;
}
3. 특정 위치에 데이터 추가
public void add(int idx, Object e) {
Node newNode = new Node(e);
if (idx == 0) {
// 첫 번째 위치에 추가할 경우
newNode.next = first;
first = newNode;
} else {
// 중간이나 끝에 추가할 경우
Node prevNode = getNode(idx - 1); // 이전 노드 찾기
newNode.next = prevNode.next; // 새 노드의 다음을 이전 노드의 다음으로 설정
prevNode.next = newNode; // 이전 노드의 다음을 새 노드로 설정
}
size++;
}
4. 데이터 조회
public Object get(int idx) {
Node x = getNode(idx);
return x.item;
}
private Node getNode(int idx) {
Node x = first;
for (int i = 0; i < idx; i++) {
x = x.next;
}
return x;
}
5. 데이터 삭제
public Object remove(int idx) {
Node removeNode = getNode(idx);
Object removeItem = removeNode.item;
if (idx == 0) {
// 첫 번째 노드 삭제
first = removeNode.next;
} else {
// 중간이나 마지막 노드 삭제
Node prevNode = getNode(idx - 1);
prevNode.next = removeNode.next;
}
// 삭제된 노드의 참조 정리
removeNode.next = null;
removeNode.item = null;
size--;
return removeItem;
}
제네릭을 적용한 LinkedList 구현
타입 안전성을 강화하기 위해 제네릭을 적용한 LinkedList를 구현해봅시다:
public class MyLinkedListV3<E> {
private Node<E> first;
private int size = 0;
// Node 클래스도 제네릭으로 구현
private static class Node<E> {
E item;
Node<E> next;
public Node(E item) {
this.item = item;
}
}
// 다른 메서드들도 제네릭 타입으로 변환
public void add(E e) { /* ... */ }
public E get(int idx) { /* ... */ }
public E remove(int idx) { /* ... */ }
// ...
}
제네릭을 사용하면 컴파일 시점에 타입 안전성을 보장받을 수 있습니다:
// 제네릭 사용 예시
MyLinkedListV3<String> strList = new MyLinkedListV3<>();
strList.add("Hello");
String str = strList.get(0); // 형변환 불필요
MyLinkedListV3<Integer> intList = new MyLinkedListV3<>();
intList.add(1);
Integer num = intList.get(0); // 형변환 불필요
LinkedList 연산의 시간 복잡도
LinkedList의 주요 연산별 시간 복잡도는 다음과 같습니다:
- get(index): O(n) - 처음부터 순차적으로 탐색
- add(element) (끝에 추가): O(n) - 마지막 노드를 찾아야 함
- add(0, element) (맨 앞에 추가): O(1) - 첫 노드 참조만 변경
- add(index, element) (중간에 삽입): O(n) - 해당 위치까지 탐색 필요
- remove(0) (맨 앞 삭제): O(1) - 첫 노드 참조만 변경
- remove(index) (중간/끝 삭제): O(n) - 해당 위치까지 탐색 필요
ArrayList vs LinkedList: 언제 무엇을 사용할까?
ArrayList가 유리한 경우
- 빈번한 조회 작업: 인덱스로 직접 접근할 수 있어 조회가 O(1)로 매우 빠름
- 순차적 접근: 메모리 지역성(locality) 원리로 인해 캐시 효율이 좋음
- 크기 변화가 적은 경우: 크기 변경이 적다면 재할당 비용이 발생하지 않음
LinkedList가 유리한 경우
- 빈번한 삽입/삭제 작업: 특히 리스트의 시작이나 중간에 자주 삽입/삭제하는 경우
- 크기 예측이 어려운 경우: 동적으로 노드만 추가하므로 메모리 효율적
- 큐나 스택처럼 양 끝에서의 작업이 많은 경우: 양방향 LinkedList는 이런 작업에 O(1) 성능
실전 활용 예시
LinkedList를 실제로 활용하는 예시를 살펴보겠습니다:
import java.util.LinkedList;
public class LinkedListExample {
public static void main(String[] args) {
// 문자열을 저장하는 LinkedList 생성
LinkedList<String> names = new LinkedList<>();
// 요소 추가
names.add("Alice");
names.add("Bob");
names.add("Charlie");
// 맨 앞과 맨 뒤에 요소 추가 (LinkedList 전용 메서드)
names.addFirst("David");
names.addLast("Emily");
System.out.println("LinkedList: " + names);
// 첫 번째와 마지막 요소 조회
System.out.println("First: " + names.getFirst());
System.out.println("Last: " + names.getLast());
// 요소 삭제
names.removeFirst();
names.removeLast();
// 반복문을 통한 모든 요소 출력
System.out.println("이름 목록:");
for (String name : names) {
System.out.println("- " + name);
}
// LinkedList를 스택처럼 사용
LinkedList<Integer> stack = new LinkedList<>();
stack.push(1); // addFirst()와 동일
stack.push(2);
stack.push(3);
System.out.println("스택에서 꺼낸 항목: " + stack.pop()); // 3
// LinkedList를 큐처럼 사용
LinkedList<Integer> queue = new LinkedList<>();
queue.offer(1); // add()와 동일
queue.offer(2);
queue.offer(3);
System.out.println("큐에서 꺼낸 항목: " + queue.poll()); // 1
}
}
요약 및 결론
LinkedList는 노드들이 서로 연결된 형태로 데이터를 저장하는 자료구조입니다. ArrayList와 달리 메모리 상에 연속적으로 저장되지 않고, 필요할 때마다 노드를 생성하여 연결합니다.
LinkedList의 주요 특징:
- 각 노드는 데이터와 다음 노드를 가리키는 참조로 구성됨
- 중간 삽입/삭제가 빠름 (O(1)) - 참조만 변경하면 됨
- 조회가 상대적으로 느림 (O(n)) - 처음부터 순차적으로 탐색해야 함
- 메모리 사용이 효율적 - 필요할 때만 노드를 생성
LinkedList는 삽입/삭제가 빈번한 작업, 크기를 예측하기 어려운 경우, 또는 큐나 스택과 같은 자료구조를 구현할 때 적합합니다. 반면, 조회 작업이 많거나 인덱스 기반 접근이 필요한 경우에는 ArrayList가 더 적합합니다.
자료구조의 선택은 프로그램의 요구사항과 데이터 사용 패턴에 따라 달라질 수 있으므로, 각 자료구조의 장단점을 이해하고 적절한 상황에 맞는 자료구조를 선택하는 것이 중요합니다.
'📚 학습 기록 > Java 기초 & 중급' 카테고리의 다른 글
| 자바 컬렉션 프레임워크 완전 정복: ArrayList vs LinkedList 실전 비교 (1) | 2025.04.15 |
|---|---|
| ArrayList vs LinkedList: 성능 비교와 의존관계 이해하기 (1) | 2025.04.14 |
| ArrayList 완전 정복: 동적 배열 구현과 활용 기법 (1) | 2025.04.09 |
| 자바 컬렉션 프레임워크: ArrayList와 동적 배열 구현 이해하기 (1) | 2025.04.07 |
| 자바 제네릭 완전정복: 와일드카드의 모든 것 (김영한의 자바 중급) (0) | 2025.04.03 |