[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.