Linked List - Java

Linked List - Java

개요

  • Singly Linked List 장점과 단점
  • Java built-in LinkedList class
  • Doubly Linked List 에 대한 간단한 설명
  • Linked List 를 사용하여 구현할 수 있는 또 다른 데이터 구조
  • 구현예제보기


Singly Linked List

linked-list
Singly Linked List

얼핏 보면 array 와 비슷하게 생긴 자료구조가 linked list 이다. 박스 같이 생긴 하나하나를 node 라고 부르고, 각 node 는 다음 node 의 참조값을 가지고 있다. 아래는 array 와 비교했을 때 linked list 가 가지는 장단점이다.

Linked List 의 장점
  • 동적 size 조절 가능
  • 데이터 삽입/삭제가 편함

동적 size 조절의 경우, java 의 ArrayList 와 같은 클래스를 사용하면 할 수 있는 것 처럼 보이지만 ArrayList 는 사실상 copy 방식이므로 (여기참고) 동적 size 조절이 되는 것 처럼 보이게 한 것이지 실제로는 linked list 의 방식에 비해 비용이 많이 드는 방식이다. 반면, linked list 는 head, tail 포인터만 있다면 list 의 앞 뒤 어디든 삽입 연산의 경우 O(1) 의 시간복잡도를 가진다.

Linked List 의 단점
  • Random access 불가능. 첫 번째 요소 부터 탐색해야 한다. Binary search 같은 작업은 linked list 만으로는 어렵다.
  • 각 노드의 reference 를 저장할 공간이 추가적으로 필요하다.


Singly Linked List 의 구현 - Java

구현 메소드 3가지

  • 기존 리스트의 맨 뒤에 node 를 추가하는 append()
  • head 앞에 node 를 추가하는 prepend()
  • 지정한 값을 가지고 있는 node 를 삭제 하는 deleteWithValue()

Sample Code

class 정의

먼저 LinkedList 라는 이름으로 class 를 하나 만든다. 이 linked list 는 head 라는 Node 타입의 reference 변수를 가지고 있는데 linked list 의 진입점이라고 할 수 있다. 그러면 Node class 에 대한 정의도 필요하겠다. Node 클래스는 LinkedList 객체 내부적으로만 조작할 수 있도록 하기 위해 private 제어자를 둔다. Node 클래스는 다음 값을 가리킬 레퍼런스 변수 next 와 자신의 값 data 를 가진다.

class LinkedList {
    Node head;
    
    private class Node {
        Node next;
        int data;
        public Node(int data){
            this.data = data;
            this.next = null;
        }
    }
}
append()
  • 현재 head 가 null 이면 (아무것도 없으면) head 에 새롭게 append 할 node 를 붙여준다.
  • 그렇지 않은 경우 현재 head 를 current 라는 변수로 받고 리스트의 가장 끝으로 간다.
  • current 를 마지막 node 로 옮겨간다 - while 문
  • current.next == null 이면 current 는 마지막 node 에 도착한 것이다.
  • current.next = “추가할 노드” 지정해주면 끝이다.

구현은 아래와 같다.

public void append(int data) {
    Node appendNode = new Node(data);

    // 현재 head 가 null 이면 (아무것도 없으면) head 에 새롭게 append 할 node 를 붙여준다.
    if (head == null) {
        head = appendNode;
        return;
    }

    // 그렇지 않은 경우 현재 head 를 current 라는 변수로 받고 리스트의 가장 끝으로 간다.
    Node current = head;
    while (current.next != null) {  // current.next == null 이면 current 는 마지막 node 에 도착한 것이다
        current = current.next;     // current 를 마지막 node 로 옮겨간다. 
    }
    current.next = appendNode;      // current.next = "추가할 노드" 지정해주면 끝이다
}
prepend()

prepend 메소드 구현은 단순하다. 구현은 아래와 같다.

public void prepend(int data) {
    Node newHead = new Node(data);
    newHead.next = head;
    head = newHead;
}
deleteWithValue()

그나마 조금 까다로워 보일 수 있는 특정 값을 지우는 메소드인데 비슷하게 하면 된다.

  • head 부터 시작한다
  • 리스트가 비어있는 경우 리턴
  • 찾는 값이 head 에 있나? 그러면 head 삭제
  • head 를 제외한 나머지. 그림 참고.
linked-list-detele-1
그림1

그림1 지우려는 노드가 value 로 표시된 노드라면, node 하나씩 탐색 중 current.next.data 가 현재 지우려는 값과 일치 할 것이다.

linked-list-detele-2
그림2

그림2 current.next 는 지우려는 node 를 가리키고 있었을 것이고, 지우려는 node 의 다음 node 는 current.next.next 로 표현할 수 있다. 그러므로 current.next = current.next.next 를 해주면 지우려는 node 이전의 node (current) 가 지우려는 node 다음 node (current.next.next) 와 연결되어 지우려는 노드는 연결에서 disconnect 된다.

public boolean deleteWithValue(int data) {
    Node current = head; // head 부터 시작한다

    if (head == null) return false; // 리스트가 비어있는 경우 리턴

    // 찾는 값이 head 에 있나? 그러면 head 삭제
    if (current.data == data) {
        head = head.next;
        return true;
    }

    // head 를 제외한 나머지. 그림 참고.
    while (current.next != null) {
        if (current.next.data == data) {
            current.next = current.next.next;
            return true;
        }
        current = current.next;
    }

    return false;
}

혹시 다른 메소드 만들어 보고 싶은 게 있다면 1->2->3->4 이런식으로 출력되는 print() 메소드 만들어 보는 것도 재밌다. Sample Code


Linked List 의 사용 예

  • Stack
  • Queue
  • Graph
  • Tree

등 다양한 곳에 다양한 목적으로 사용될 수 있는 기본적인 data structure 가 linked list 이다.


Java built-in LinkedList class

사실 java 에도 기본적으로 지원되는 LinkedList class 가 있다. AbstractSequentialList 를 상속받고 List 인터페이스를 구현한 클래스이다. 아래와 같은 방법으로 Queue 로 사용할 수 있다.

built-in LinkedList class 를 사용한 Queue
import java.util.LinkedList;
import java.util.Queue;

public class BuiltinLinkedListQueueExample {
    public static void main(String[] args){
        Queue<Integer> queue = new LinkedList<>();

        queue.add(1);
        queue.add(2);
        queue.add(3);
        queue.add(4);
        queue.add(5);
        queue.add(6);
        queue.add(7);
        System.out.println(queue.remove());
        System.out.println(queue.toString());
    }
}
built-in LinkedList class 소스 까보기

Built-in LinkedList 소스를 까보면 linkLast(), linkBefore() 메소드에서 재밌는 것을 발견 할 수 있다. 글 시작부의 타이틀에 Singly Linked List 라는 표현을 썼다. 가장 기초가 되는 linked list 구현이었고, 한 방향으로만 연결되는 linked list 이기 때문이다. 참조값으로도 head 하나 밖에 쓰지 않았다. 하지만 built-in LinkedList 에는 first, last, prev, next 네 가지의 참조변수가 있다.

/**
 * Links e as last element.
 */
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

/**
 * Inserts element e before non-null Node succ.
 */
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++;
}

이러한 방식을 Doubly Linked List 라고 하고 아래와 같이 각 node 들은 자신의 이전 node 에 대한 레퍼 값도 가지고 있다.

Java doc 의 LinkedList class 페이지로 가보면 아래와 같은 설명을 볼 수 있다.

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

doubly-linked-list
Doubly Linked List

위 구현 그림을 보면 head(first), last(tail) 포인터도 존재하는데 Queue 를 구현할 때에는 보통 head, tail 두 종류의 포인터를 함께 쓴다. append 는 tail 에 하고, remove 는 head 에서 하여 FIFO 를 구현하기 때문이다. (Stack 의 경우에는 head 하나만 있어도 된다. LIFO 이기 때문에.) Linked List 를 이용한 Stack, Queue 를 직접 구현하는 것은 다음 포스팅에서 다뤄 봐야겠다.


참고한 자료

Comments

Yaboong's Picture

Yaboong

오스카 쉰들러는 흔해빠진 기회주의자요 부패한 사업가였다. 그러나 거대한 악이 세상을 점령하는 것처럼 보일 때 그 악에 대항해서 사람의 생명을 구한 것은 귀족도 지식인도 종교인도 아닌 부패한 기회주의자 오스카 쉰들러였다.

Seoul, South Korea https://github.com/yaboong