Language/Java

[Java] LinkedList 파헤쳐 보기

Deveun 2021. 10. 6. 04:59

Java의 ArrayList의 실제 구현코드를 살펴보았던 이전 게시글에 이어서 LinkedList도 소스코드를 파헤쳐보자.

(참고: 2021.10.06 - [Language/Java] - [Java] ArrayList 파헤쳐 보기)

 

Doubly-linked list implementation of the List and Deque interfaces.

 

LinkedList는 이전/이후의 리스트와 연결되어 있는 구조이다. 이는 다음과 같은 특징을 가진다.

 o element 갯수에 대해 정해진 limit 없음

 o Reference Type만 가질 수 있음

 o Thread Safe 하지 않음

 

 

ArrayList가 내부적으로 Object[]으로 이루어졌던거에 비해 LinkedList는 Node로 이루어져있다.

코드를 통해서 살펴보자.

 

o 인스턴스 변수

: LinkedList는 Deque 인터페이스를 구현하고 있는 양 방향 접근이 가능한 자료이다. ("double ended queue")

내부적으로 첫 노드를 가리키는 first Node와 마지막 노드를 가리키는 last Node를 가진다. 그리고 ArrayList와 동일하게 element 갯수를 의미하는 size 변수를 가진다.

transient int size = 0;
transient Node<E> first; // Pointer to first node.
transient Node<E> last; // Pointer to last node.

 

* Node class

: 내부에서 계속해서 쓰이는 Node는 다음과 같이 이전/이후 Node의 pointer와 item값을 가진다.

    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

 

o 데이터 삽입

: List 인터페이스를 구현한 add()메서드는 데이터를 (1) 맨 뒤에 추가하거나, (2) index에 삽입하는 두가지 경우로 나뉜다.

중간에 삽입되는 (2)번의 경우를 살펴보면 내부적으로 linkBefore() 메서드가 호출된다.

 

newNode를 생성하여 삽입 전에 index 위치에 존재하던 node를 succ로, 해당 node의 prev를 pred로 값을 넣어준다.  --> new Node<>(pred, e, succ)

pred의 next값을 newNode로 업데이트해준다.

LinkedList에 데이터가 하나 추가되었으므로 size++; 를 해준다.

    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        final Node<E> pred = succ.prev;
        final Node<E> newNode = new Node<>(pred, e, succ);
        succ.prev = newNode;
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
    }

 

코드보다도 그림으로 보면 쉽게 이해가 될 것이다.

 

[출처] https://eskeptor.tistory.com/89

 

o 데이터 삭제

: List 인터페이스의 remove(index) 메소드를 구현한 부분에서는 unlink(node) 를 호출한다.

index에 해당하는 node를 찾아 다음과 같이 연결을 해제한다.

 

[출처] https://eskeptor.tistory.com/89

E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;

        if (prev == null) {
            first = next;
        } else {
            prev.next = next; // 삭제하고자 하는 x 이전 노드의 next와 x의 next를 연결
            x.prev = null; // 연결해제된 x의 prev는 null
        }

        if (next == null) {
            last = prev;
        } else {
            next.prev = prev; // 삭제하고자 하는 x 다음 노드의 prev와 x의 prev를 연결
            x.next = null; // 연결해제된 x의 next는 null
        }

        x.item = null;
        size--; // 사이즈 감소
        modCount++;
        return element;
    }

 

모든 데이터를 삭제하는 clear() 메소드의 경우에는 모든 Node들을 순회하며 item, next, prev를 모두 null로 바꾼다.

 

o 데이터 탐색

: List 인터페이스 메소드를 구현한 get()은 ArrayList에서 데이터를 가져오는 것과는 큰 차이가 있다.

ArrayList의 경우 RandomAccess가 가능한 데이터 타입인데, LinkedList는 그렇지 않다. index를 통해 값을 바로 찾는 것이 불가능 하며 index 값을 앞 or 뒤에서 부터 이동시켜가며 해당 위치의 데이터를 찾아야만 한다. 

    Node<E> node(int index) {
        // assert isElementIndex(index);

        if (index < (size >> 1)) { // index가 전체 사이즈/2보다 작으면 앞부터 탐색
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else { // index가 전체 사이즈/2보다 크거나 같으면 뒤부터 탐색
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

즉, 총 n개의 데이터가 저장되어있다고 가정할 때 LinkedList에서는 최대 n/2번의 탐색이 발생하므로, ArrayList보다 비효율적임을 알 수 있다.

 

이를 통해 두 자료형을 서로 비교해서 생각해보자. LinkedList는 데이터의 삽입/삭제가 빈번하고 탐색이 적은 케이스에 ArrayList보다 더 유용하다.