단일 연결리스트 (Singly Linked List)
각 노드가 다음 노드를 가리키는 포인터를 가진 자료구조
구조
+-------+ +-------+ +-------+ +-------+
| data | -> | data | -> | data | -> | data | -> null
| next | | next | | next | | next |
+-------+ +-------+ +-------+ +-------+
head tail
노드 구조
class Node {
int data; // 저장할 데이터
Node next; // 다음 노드를 가리키는 포인터
Node(int data) {
this.data = data;
this.next = null;
}
}핵심 연산
1. 삽입 (Insert)
맨 앞 삽입 - O(1)
Before: head -> [A] -> [B] -> [C] -> null
Insert X at head:
1. newNode.next = head
2. head = newNode
After: head -> [X] -> [A] -> [B] -> [C] -> null
void insertFirst(int data) {
Node newNode = new Node(data);
newNode.next = head;
head = newNode;
}맨 뒤 삽입 - O(n)
1. tail까지 순회
2. tail.next = newNode
// tail 포인터 유지 시 O(1) 가능
void insertLast(int data) {
Node newNode = new Node(data);
if (head == null) {
head = newNode;
return;
}
Node current = head;
while (current.next != null) {
current = current.next;
}
current.next = newNode;
}중간 삽입 - O(n)
Before: [A] -> [B] -> [C]
Insert X after B:
1. prev = B (탐색 필요)
2. newNode.next = prev.next
3. prev.next = newNode
After: [A] -> [B] -> [X] -> [C]
2. 삭제 (Delete)
맨 앞 삭제 - O(1)
void deleteFirst() {
if (head == null) return;
head = head.next;
}특정 값 삭제 - O(n)
void delete(int data) {
if (head == null) return;
// 첫 노드가 삭제 대상
if (head.data == data) {
head = head.next;
return;
}
// 이전 노드를 추적하며 탐색
Node prev = head;
while (prev.next != null && prev.next.data != data) {
prev = prev.next;
}
// 찾으면 연결 변경
if (prev.next != null) {
prev.next = prev.next.next;
}
}3. 탐색 (Search) - O(n)
Node search(int data) {
Node current = head;
while (current != null) {
if (current.data == data) {
return current;
}
current = current.next;
}
return null; // 못 찾음
}4. 순회 (Traverse) - O(n)
void printAll() {
Node current = head;
while (current != null) {
System.out.print(current.data + " -> ");
current = current.next;
}
System.out.println("null");
}전체 구현
public class SinglyLinkedList {
private Node head;
private int size;
public void addFirst(int data) {
Node newNode = new Node(data);
newNode.next = head;
head = newNode;
size++;
}
public void addLast(int data) {
Node newNode = new Node(data);
if (head == null) {
head = newNode;
} else {
Node current = head;
while (current.next != null) {
current = current.next;
}
current.next = newNode;
}
size++;
}
public void removeFirst() {
if (head != null) {
head = head.next;
size--;
}
}
public int get(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException();
}
Node current = head;
for (int i = 0; i < index; i++) {
current = current.next;
}
return current.data;
}
public int size() {
return size;
}
}장단점
장점
- 동적 크기 - 필요한 만큼만 메모리 사용
- 앞쪽 삽입/삭제 O(1)
- 메모리 재할당 불필요
단점
- 인덱스 접근 O(n) - 순차 탐색 필요
- 포인터 저장 공간 추가 필요
- 캐시 지역성 낮음 (노드가 흩어져 있음)
- 역방향 탐색 불가능
활용 예시
- 스택 구현 - 앞쪽에서만 삽입/삭제
- LIFO 큐 - 단방향 처리
- 다항식 표현 - 각 항을 노드로
- 희소 행렬 - 0이 아닌 값만 저장
면접 빈출 문제
1. 연결리스트 뒤집기
Node reverse(Node head) {
Node prev = null;
Node current = head;
while (current != null) {
Node next = current.next; // 다음 노드 저장
current.next = prev; // 방향 뒤집기
prev = current; // prev 전진
current = next; // current 전진
}
return prev; // 새로운 head
}2. 사이클 탐지 (Floyd’s Algorithm)
boolean hasCycle(Node head) {
Node slow = head;
Node fast = head;
while (fast != null && fast.next != null) {
slow = slow.next; // 1칸 이동
fast = fast.next.next; // 2칸 이동
if (slow == fast) {
return true; // 사이클 존재
}
}
return false;
}3. 중간 노드 찾기
Node findMiddle(Node head) {
Node slow = head;
Node fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow; // slow가 중간에 위치
}면접 포인트
-
배열 vs 연결리스트 차이점은?
- 메모리 구조, 접근 방식, 삽입/삭제 효율성
-
연결리스트의 시간복잡도는?
- 접근: O(n), 삽입/삭제: O(1) (위치를 알 때)
-
언제 연결리스트를 사용하는가?
- 잦은 삽입/삭제, 크기 예측 불가, 앞쪽 작업 많을 때