[JAVA] LinkedList
[JAVA] LinkedList
노드(Node)
배열은 크기를 미리 지정해야 하며, 중간에 데이터를 삽입/삭제할 때 성능에 제약이 있음.
이를 해결하기 위해 동적으로 메모리를 확보하고, 각 데이터를 노드(Node)로 만들어 연결하는 구조가 연결 리스트(Linked List)다.
- 노드는 내부에 데이터와 다음 노드에 대한 참조를 가지고 있다.
- 각각의 노드가 참조를 통해 연결(Link) 되어 있다.
- 배열처럼 미리 메모리를 할당하지 않아 낭비가 없다는 장점이 있지만, 각 노드가 다음 노드를 가리키기 위한 포인터를 가지고 있어 추가적인 메모리 오버헤드는 존재
- 각각의 노드를 연결(링크)해서 사용하는 자료 구조로 리스트를 만들 수 있는데, 이것을 연결 리스트라 한다.
연결 리스트(LinkedList)
직접 구현
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
public class MyNode {
//단방향 연결 리스트(Singly Linked List)
Object data; //노드가 가지는 실제 값
MyNode next; //다음 노드를 가리키는 참조(포인터)
//앞 노드도 알고 있는건 양방향 연결 리스트(Doubly Linked List)
//MyNode prev;
public MyNode(Object data) {
this.data = data;
}
//[A->B->C]
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
MyNode x = this;
sb.append("[");
while (x != null) {
sb.append(x.data);
if (x.next != null) {
sb.append("->");
}
x = x.next;
}
sb.append("]");
return sb.toString();
}
}
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
public class MyLinkedList {
private MyNode head; // 연결 리스트의 첫 번째 노드를 가리키는 참조 (시작점)
private int size;
//입력받은 순서의 노드를 가져옴
public Object get(int index) {
MyNode x = getNode(index);
return x.data;
}
//입력받은 순서와 데이터를 삽입
public Object set(int index, Object e) {
MyNode x = getNode(index);
Object oldValue = x.data;
x.data = e;
return oldValue;
}
//index번째 노드 삭제
public Object remove(int index) {
MyNode x = getNode(index); //삭제할 노드 가져옴
Object oldValue = x.data; //그 노드의 data를 oldValue에 저장
if(index == 0) { //삭제노드가 첫번째면
head = head.next; //head를 다음 노드로 이동시킴
} else {
MyNode prev = getNode(index - 1); //이전 노드를 가져와
prev.next = x.next; //이전 노드가 삭제 대상의 다음 노드를 가리키게 만듦
}
x.next = null; //삭제 노드의 next 참조를 끊어 메모리 누수 방지
size--; //리스트 크기 감소
return oldValue; //삭제된 노드의 데이터 반환
}
//맨 끝에 새로운 노드를 추가
public void add(Object e) {
MyNode newNode = new MyNode(e); //1. 입력받은 e로 새 노드 생성 (item = e, next = null)
if(head == null) { //2. 리스트가 비어있으면
head = newNode; //3. 새 노드가 첫 노드가 됨
} else {
MyNode lastNode = getLastNode(); //4. 마지막 노드를 lastNode에 저장
lastNode.next = newNode; // 5. 그 뒤에 새 노드를 연결
}
size++;
}
//index 위치에 새로운 노드 추가
public void add(Object e, int index) {
MyNode newNode = new MyNode(e); //새 노드 생성
if(index == 0) {
newNode.next = head;
head = newNode; //새 노드가 첫 노드가 됨
} else {
MyNode prev = getNode(index -1); //새 노드를 넣을 직전 노드를 찾음
newNode.next = prev.next; //새 노드의 next는 기존 prev 뒤의 노드로 연결
prev.next = newNode;
}
size++;
}
//마지막 노드를 찾기
public MyNode getLastNode() {
MyNode x = head; //리스트의 첫 번째 노드인 head를 x에 저장
while(x.next != null) { //x가 마지막이 아닐때까지 반복
x = x.next;
}
return x; //x는 마지막 노드 반환
}
//index 번째 있는 노드 찾기
private MyNode getNode(int index) {
MyNode x = head;
for(int i= 0; i < index; i++) {
x = x.next;
}
return x;
}
public int size() {
return size;
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[");
MyNode x = head;
while(x != null) {
sb.append(x.data);
if(x.next != null) {
sb.append(", ");
}
x = x.next;
}
sb.append("]");
sb.append(" size = " + size);
return sb.toString();
}
}
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
32
33
34
35
36
37
38
39
40
41
42
public static void main(String[] args) {
MyLinkedList list = new MyLinkedList();
//노드 추가
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
System.out.println(list);
//노드 덮어쓰기
list.set(1, "z");
System.out.println(list);
//1번째 노드 찾기
System.out.println(list.get(0));
//1번째 노드 삭제
list.remove(0);
System.out.println(list);
//마지막 노드 찾기
MyNode lastNode = list.getLastNode();
System.out.println(lastNode);
//사이즈 확인
int size = list.size();
System.out.println(size);
//2번째에 노드 넣기
list.add("f", 2);
System.out.println(list);
}
//출력
[a, b, c, d, e] size = 5
[a, z, c, d, e] size = 5
a
[z, c, d, e] size = 4
[e]
4
[z, c, f, d, e] size = 5
- 연결 리스트는 데이터를 추가할 때 마다 동적으로 노드가 늘어나기 때문에 범위를 초과하는 문제가 발생하지 않는다.
배열 위치별 데이터 추가/삭제
첫 번째 위치에 데이터 추가
- 배열의 경우 첫 번째 항목에 데이터가 추가되면 모든 데이터를 오른쪽으로 하나씩 밀어야 하지만 연결 리스트는 새로 생성한 노드의 참조만 변경하면 된다. 나머지는 어떤 일이 일어난지도 모른다.
- 연결 리스트의 첫 번째 항목에 값을 추가하는 것은 빠르다.
O(1)
첫 번째 위치의 데이터 삭제
- 노드 삭제 후 오른쪽 노드의
index가 하나씩 당겨진다. - 배열의 경우 첫 번째 항목이 삭제되면 모든 데이터를 왼쪽으로 밀어야 하지만 연결리스트는 일부 참조만 변경하면 된다.
- 연결 리스트의 첫 번쨰 항목에 값을 삭제하는 것은 빠르다.
O(1)
중간 위치에 데이터 추가
- 추가한 노드 오른쪽에 있는 노드들의
index가 하나씩 뒤로 밀린다. - 배열의 경우 데이터가 추가되면 인덱스 위치 부터 모든 데이터를 오른쪽으로 하나씩 밀어야 하지만 연결 리스트는 새로 생성한 노드의 참조만 변경하면 된다.
- 위치를 찾는데
O(n), 위치를 찾아 노드를 추가하는데O(1)따라서 둘을 합하면O(n)
중간 위치에 데이터 삭제
- 오른쪽 노드의
index가 하나씩 당겨진다. - 연결리스트에서 인덱스로 삭제할 항목을 찾는데
O(n) - 항목을 삭제하는데
O(1)둘을 합하면O(n)
연결 리스트와 배열 리스트 둘다 중간에 있는 항목을 추가하거나 삭제하는 경우 둘다 같은 O(n)
| 기능 | 배열 리스트 (ArrayList) | 연결 리스트 (LinkedList) |
|---|---|---|
| 인덱스 조회 | O(1) | O(n) |
| 검색 | O(n) | O(n) |
| 앞에 추가/삭제 | O(n) | O(1) |
| 뒤에 추가/삭제 | O(1) | O(n) |
| 평균 추가/삭제 | O(n) | O(n) |
배열 리스트(ArrayList)와 연결 리스트(LinkedList)
데이터를 자주 조회하거나, 뒤쪽에 추가할 일이 많다면 ArrayList가 더 효율적이다.
반면, 앞쪽에서 삽입이나 삭제가 자주 발생한다면 LinkedList가 더 나은 선택이다.
이중 연결 리스트(Doubly Linked List)
이중 연결 리스트(Doubly Linked List)는 각 노드가 prev와 next 모두를 가지기 때문에 양방향 탐색이 가능하다. 삽입/삭제 시, 이전 노드를 따로 찾을 필요가 없어 성능상 이점이 있다. 단, 포인터가 2개이므로 메모리 사용량은 더 많다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyNode {
//단방향 연결 리스트(Singly Linked List)
Object data; //노드가 가지는 실제 값
MyNode next; //다음 노드를 가리키는 참조(포인터)
//앞 노드도 알고 있는건 양방향 연결 리스트(Doubly Linked List)
MyNode prev;
}
//마지막 노두를 참조하는 연결 리스트
public class LinkedList {
private Node first;
private Node last; //마지막 노드 참조
private int size = 0;
}
- 노드를 앞 뒤로 모두 연결하는 이중 연결 리스트를 사용해 성능을 좀 더 개선할 수 있다.
- 자바가 제공하는 연결 리스트는
prev/next를 모두 가지는 이중 연결 리스트로 구현되어 있으며first/last/포인터까지 유지하기 때문에 맨 앞/뒤 삽입/삭제는O(1)의 성능을 보장한다.
연결 리스트는 삽입/삭제가 자주 발생하는 구조에 적합한 자료구조다. 반면, 조회와 마지막 위치 추가가 많다면 배열 리스트가 더 효율적이다. 실무에서는 두 구조의 장단점을 파악해 상황에 맞는 선택이 중요하다.
제네릭 도입
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
public class MyLinkedListV2<E> {
private MyNode<E> head; // 연결 리스트의 첫 번째 노드를 가리키는 참조 (시작점)
private int size;
//입력받은 순서의 노드를 가져옴
public E get(int index) {
MyNode<E> x = getNode(index);
return x.data;
}
//입력받은 순서와 데이터를 삽입
public E set(int index, E e) {
MyNode<E> x = getNode(index);
E oldValue = x.data;
x.data = e;
return oldValue;
}
//index번째 노드 삭제
public E remove(int index) {
MyNode<E> x = getNode(index); //삭제할 노드 가져옴
E oldValue = x.data; //그 노드의 data를 oldValue에 저장
if(index == 0) { //삭제노드가 첫번째면
head = head.next; //head를 다음 노드로 이동시킴
} else {
MyNode<E> prev = getNode(index - 1); //이전 노드를 가져와
prev.next = x.next; //이전 노드가 삭제 대상의 다음 노드를 가리키게 만듦
}
x.next = null; //삭제 노드의 next 참조를 끊어 메모리 누수 방지
size--; //리스트 크기 감소
return oldValue; //삭제된 노드의 데이터 반환
}
//맨 끝에 새로운 노드를 추가
public void add(E e) {
MyNode<E> newNode = new MyNode<>(e); //1. 입력받은 e로 새 노드 생성 (item = e, next = null)
if(head == null) { //2. 리스트가 비어있으면
head = newNode; //3. 새 노드가 첫 노드가 됨
} else {
MyNode<E> lastNode = getLastNode(); //4. 마지막 노드를 lastNode에 저장
lastNode.next = newNode; // 5. 그 뒤에 새 노드를 연결
}
size++;
}
//index 위치에 새로운 노드 추가
public void add(E e, int index) {
MyNode<E> newNode = new MyNode<>(e); //새 노드 생성
if(index == 0) {
newNode.next = head;
head = newNode; //새 노드가 첫 노드가 됨
} else {
MyNode<E> prev = getNode(index -1); //새 노드를 넣을 직전 노드를 찾음
newNode.next = prev.next; //새 노드의 next는 기존 prev 뒤의 노드로 연결
prev.next = newNode;
}
size++;
}
//마지막 노드를 찾기
public MyNode<E> getLastNode() {
MyNode<E> x = head; //리스트의 첫 번째 노드인 head를 x에 저장
while(x.next != null) { //x가 마지막이 아닐때까지 반복
x = x.next;
}
return x; //x는 마지막 노드 반환
}
//index 번째 있는 노드 찾기
private MyNode<E> getNode(int index) {
MyNode<E> x = head;
for(int i= 0; i < index; i++) {
x = x.next;
}
return x;
}
public int size() {
return size;
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[");
MyNode x = head;
while(x != null) {
sb.append(x.data);
if(x.next != null) {
sb.append(", ");
}
x = x.next;
}
sb.append("]");
sb.append(" size = " + size);
return sb.toString();
}
private static class MyNode<E> {
E data;
MyNode<E> next;
public MyNode(E data) {
this.data = data;
}
@Override
public String toString() {
return String.valueOf(data);
}
}
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
32
33
34
35
36
37
38
39
40
41
42
43
44
public static void main(String[] args) {
MyLinkedListV2<String> stringList = new MyLinkedListV2<>();
stringList.add("a");
stringList.add("b");
stringList.add("c");
System.out.println( stringList);
stringList.remove(0);
System.out.println( stringList);
stringList.add("a",0);
System.out.println( stringList);
stringList.set(2, "z");
System.out.println( stringList);
System.out.println("----------------");
MyLinkedListV2<Integer> intList = new MyLinkedListV2<>();
intList.add(1);
intList.add(2);
intList.add(3);
System.out.println( intList);
intList.remove(0);
System.out.println( intList);
intList.add(1,0);
System.out.println( intList);
intList.set(2, 99);
System.out.println( intList);
}
//실행결과
[a, b, c] size = 3
[b, c] size = 2
[a, b, c] size = 3
[a, b, z] size = 3
----------------
[1, 2, 3] size = 3
[2, 3] size = 2
[1, 2, 3] size = 3
[1, 2, 99] size = 3
- 기존
MyLinkedList는Object타입으로 모든 데이터를 처리함 → 타입 안정성이 없음 Object로 받아서 사용하면 형변환(casting)이 필요하고, 런타임 오류 발생 가능성 있음- 제네릭을 도입하면 컴파일 타임에 타입을 강제할 수 있어서, 타입 안전성을 확보하고 불필요한 캐스팅도 제거 가능
String전용 리스트,Integer전용 리스트처럼 각각 타입을 제한할 수 있음
제네릭은 타입 안정성과 재사용성을 모두 향상시키는 훌륭한 도구이며, 실무에서 컬렉션, 유틸리티 클래스 등을 만들 때 거의 필수적으로 사용됨.
This post is licensed under
CC BY 4.0
by the author.