Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
Tags
- TDD
- MVC요청플로우
- 클린아키텍처
- 로그백
- 커뮤니티서버
- 설정세팅
- 사면초가
- 이벤트핸들러등록
- DDD
- 디스크i/o
- 전전긍긍
- OOP
- AOP
- 이벤트핸들러this
- 공통기술
- SpringLegacy
- MVC
- index
- 문제선택근거
- JDBC
- string
- JPA
- springsecurity
- 냄새라도
- 단위테스트
- model
- 공통규약
- SEQUENCE
- Transaction
- Ajax
Archives
- Today
- Total
개발이군고구마
[자료구조] LIST 원리 본문
728x90
1. List 종류
- List는 ‘순서가 있고, 중복을 허용하는’ 컬렉션 인터페이스
- 대표 구현체
- ArrayList (동적 배열 기반)
- LinkedList (이중 연결 리스트 기반)
2. Array(=ArrayList)와 LinkedList의 차이점
▶ ArrayList 원리
- 데이터가 메모리 상에 연속적으로 나열됨.
- Index(주소)를 통해 데이터에 즉시 접근 가능 (Random Access).
- 공간이 꽉 차면 더 큰 새 배열을 만들고(=복사) 데이터를 전부 이사(Copy)시켜야 함.
- 중간에 데이터를 삭제하면, 뒤에 있는 데이터들을 한 칸씩 앞으로 당겨야 함(Shift).
public class MyArrayList {
private Object[] elements; // 내부는 실제 배열(Array)로 되어 있음
private int size = 0;
// 1. 조회 (Get)
// 원리: 배열 인덱스로 바로 접근함. 속도 매우 빠름 O(1).
public Object get(int index) {
return elements[index];
}
// 2. 추가 (Add)
// 원리: 공간이 남으면 그냥 넣음.
// 꽉 차면 2배 크기 배열을 만들고 이사(Copy) 시킴. 속도 느려질 수 있음.
public void add(Object value) {
if (size == elements.length) {
growArray(); // 배열 크기 늘리기 (비용이 큼)
}
elements[size] = value;
size++;
}
// 3. 삭제 (Remove)
// 원리: 중간 데이터를 빼면 구멍이 생기므로, 뒤의 요소들을 전부 앞으로 당겨야 함. O(n)
public void remove(int index) {
for (int i = index; i < size - 1; i++) {
elements[i] = elements[i + 1]; // 한 칸씩 앞으로 당김 (Shift)
}
size--;
}
}
- 내부에 Object[] 배열을 들고 있음.
- add(e) 하면 배열이 꽉 찼는지 확인함.
- 꽉 찼으면 더 큰 배열을 새로 만들고 기존 요소를 복사함(리사이즈).
- get(i)는 배열 인덱싱으로 바로 접근함.
▶ LinkedList 원리
ㄴ 보물찾기 쪽지 첫 번째 쪽지를 찾으면 "두 번째 쪽지는 나무 아래에 있어"라고 적혀있어! 라고 말하는 것
- 데이터가 메모리 상에 흩어져 저장될 수 있음.
- 각 데이터(Node)가 다음 데이터의 **주소(Reference)**를 들고 있음.
- 특정 순서(예: 5번째)를 찾으려면 첫 번째부터 타고 들어가야 함 (Sequential Access).
- 중간 삽입/삭제 시, 데이터를 옮길 필요 없이 **주소 연결(Link)**만 바꿔주면 됨.
// 데이터를 담는 그릇 (Node)
class Node {
Object data;
Node next; // 다음 노드의 주소를 가리킴 (핵심)
}
public class MyLinkedList {
private Node head; // 첫 번째 노드만 알고 있음
// 1. 조회 (Get)
// 원리: 인덱스 개념이 없음. ✨ head부터 next를 타고 원하는 횟수만큼 이동해야 함. O(n)
public Object get(int index) {
Node current = head;
for (int i = 0; i < index; i++) {
current = current.next; // 보물찾기처럼 다음 위치로 이동
}
return current.data;
}
// 2. 중간 삽입/삭제
// 원리: 앞뒤 노드의 주소(next)만 바꿔주면 됨. 데이터 이동(Shift) 없음. O(1) (단, 위치 탐색 시간 제외)
public void insertAfter(Node prevNode, Object value) {
Node newNode = new Node();
newNode.data = value;
newNode.next = prevNode.next; // 새 노드가 기존의 다음 노드를 가리킴
prevNode.next = newNode; // 앞 노드가 새 노드를 가리킴
// 끝. 배열처럼 뒤의 데이터를 밀어낼 필요 없음.
}
}
- 노드 기반임.
- 인덱스 접근 느림(O(n)) 됨.
- 삽입/삭제는 링크 변경으로 처리됨(단, 위치 탐색 비용은 별개임).
3. 자료구조 사용 예시
ㄴ 기본은 ArrayList를 우선 선택하고, “정말 LinkedList가 유리한 상황인지”를 따져보는 게 안전
▶ ArrayList가 좋은 경우
- 조회가 많음 (get(i), 반복 접근, 인덱스 기반 처리)
- 끝에 추가가 많음 (add(e)가 대체로 빠름: 리사이즈가 가끔 발생하더라도 평균적으로 효율적)
- 메모리 효율이 중요함(연속 배열이라 오버헤드가 상대적으로 적음)
▶ LinkedList가 좋은 경우
- “앞/뒤”에서 삽입/삭제가 매우 빈번함 (큐/덱처럼)
- 이미 ListIterator 같은 걸로 “현재 노드 위치”를 잡고 그 주변에 삽입/삭제를 계속할 때
- 단순히 add(0, x)를 많이 한다고 무조건 유리한 게 아님(매번 위치 찾으면 결국 느릴 수 있음)
❌ 이건 해당 안 됨
list.add(500_000, x);
list.remove(500_000);
list.add(500_000, y);
- 매번 index로 접근함
- 내부적으로 처음/끝부터 500,000번째까지 탐색함
- LinkedList라도 매번 O(n) 발생함
- 👉 이 경우 ArrayList보다 나을 게 없음
✅ 좋은 예: 위치를 한 번 잡고 반복 변경
LinkedList<Integer> list = new LinkedList<>();
for (int i = 0; i < 1_000_000; i++) list.add(i);
// 🔥 한 번만 위치를 찾음
ListIterator<Integer> it = list.listIterator(500_000);
// 🔥 그 위치를 기준으로 반복 수정
for (int i = 0; i < 1000; i++) {
it.add(-1); // 현재 위치에 삽입 (탐색 없음)
it.previous();
it.remove(); // 바로 앞 노드 제거 (탐색 없음)
}
▶ 원리(중요 포인트)
- ArrayList 중간 삽입/삭제: shift 발생함.
- LinkedList 중간 삽입/삭제: 링크만 변경함.
- LinkedList는 “찾는 비용”이 먼저 듦.
- 그래서 “중간 삽입/삭제가 많다 = 무조건 LinkedList”가 아님.
- “중간 위치를 이미 잡고 반복적으로 바꾸는가”가 핵심임.
| 상황 | 추천 자료구조 | 이유 (원리 기반) |
| 데이터 조회가 많을 때 | ArrayList | 인덱스($O(1)$)로 바로 접근하므로 속도가 매우 빠름. |
| 데이터 끝에만 추가할 때 | ArrayList | 배열 크기만 충분하다면 맨 뒤에 넣는 것은 빠름. |
| 데이터 중간 삽입/삭제가 잦을 때 | LinkedList | 데이터를 밀거나 당기는(Shift) 비용 없이, 주소 연결만 바꾸면 되므로 효율적임. |
| 데이터 개수를 예측할 수 없을 때 | LinkedList | ArrayList는 크기를 늘릴 때마다 복사 비용(Copy)이 발생하지만, LinkedList는 노드만 추가하면 됨. |
4.다른 자료구조와의 차이점
(1) List (순서가 있는 저장소)
- 특징: 줄을 서는 것과 같음. 들어온 **순서(Order)**가 중요하고 유지됨.
- 중복: 똑같은 데이터가 여러 번 들어갈 수 있음.
- 접근: "3번째 사람 나와" (인덱스 사용).
(2) Map (키-값 쌍 저장소)
ㄴ 사물함 사물함 번호(Key)를 알면 바로 물건(Value)을 꺼낼 수 있음. 사물함 번호는 겹칠 수 없음
- 특징: 순서보다 **검색(Lookup)**이 중요함. Key와 Value가 짝을 이룸.
- 중복: Key는 중복 불가능(유일해야 함), Value는 중복 가능.
- 원리: 내부적으로 Hash Function을 사용해 Key가 저장될 위치를 즉시 계산함. 그래서 데이터가 아무리 많아도 검색 속도가 $O(1)$에 가까움.
(3) Set (집합)
ㄴ 초대장 명단 이미 명단에 있는 사람은 또 적지 않음. 순서는 중요하지 않고 "명단에 있는가?"가 중요.
- 특징: 순서 없음, 중복 불가능.
- 목적: 데이터의 유일성을 보장하고 싶을 때 사용. (예: 로또 번호 추첨)
5. 코드로 보는 차이
(A) ArrayList 중간 삽입이 왜 느린가: shift 확인
import java.util.*;
public class Demo {
public static void main(String[] args) {
List<Integer> a = new ArrayList<>();
for (int i = 0; i < 5; i++) a.add(i); // [0,1,2,3,4]
a.add(2, 99); // index 2에 삽입 -> 2부터 끝까지 한 칸씩 밀림
System.out.println(a); // [0,1,99,2,3,4]
}
}
(B) LinkedList가 “항상” 중간 삽입이 빠른 게 아닌 이유: 탐색 비용
import java.util.*;
public class Demo2 {
public static void main(String[] args) {
LinkedList<Integer> l = new LinkedList<>();
for (int i = 0; i < 1_000_000; i++) l.add(i);
// "중간에 넣기" 자체는 링크만 바꾸면 되지만,
// index 500_000까지 가는 데 이미 시간이 듦.
l.add(500_000, 123);
}
}
(C) Map이 “키로 빠르게 찾는 느낌”이 나는 이유 (HashMap)
import java.util.*;
public class Demo3 {
public static void main(String[] args) {
Map<String, Integer> m = new HashMap<>();
m.put("apple", 10);
m.put("banana", 20);
}
}'SERVER > Java' 카테고리의 다른 글
| 자바 I/O (2) - (문자) 파싱 구현체 만들기 (0) | 2026.01.07 |
|---|---|
| 자바 I/O (1) - 원리 (0) | 2025.12.29 |
| [자료구조] MAP 원리 (0) | 2025.12.19 |
| [JVM] 자바에서 생성한 코드는 어떻게 실행되는가 (0) | 2025.05.20 |
| [JVM] Static (자바 메모리 구조) (0) | 2025.02.10 |