이중 연결 리스트는 단순 연결 리스트를 기반으로 약간의 변형을 가한 것이기 때문에 단순 연결 리스트의 소스 코드를 수정하는 방법으로 접근하면 됩니다. 여기서는 차이점을 중심으로 알아보도록 하겠습니다.
클래스
DoublyLinkedList.java
package list.doublylinkedlist.implementation; public class DoublyLinkedList { // 첫번째 노드를 가리키는 필드 private Node head; private Node tail; private int size = 0; }
노드
노드에는 이전 노드에 대한 부분이 추가 됩니다.
private class Node { // 데이터가 저장될 필드 private Object data; // 다음 노드를 가리키는 필드 private Node next; private Node prev; public Node(Object input) { this.data = input; this.next = null; this.prev = null; } // 노드의 내용을 쉽게 출력해서 확인해볼 수 있는 기능 public String toString() { return String.valueOf(this.data); } }
아래는 linked list 대비 doubly linked list에서 변경된 부분을 보여주고 있습니다. 붉은색으로 표시된 부분이 추가된 부분입니다.
DoublyLinkedList.java VS LinkedList.java
추가
시작 위치에 추가
public void addFirst(Object input) { // 노드를 생성합니다. Node newNode = new Node(input); // 새로운 노드의 다음 노드로 헤드를 지정합니다. newNode.next = head; // 기존에 노드가 있었다면 현재 헤드의 이전 노드로 새로운 노드를 지정합니다. if (head != null) head.prev = newNode; // 헤드로 새로운 노드를 지정합니다. head = newNode; size++; if (head.next == null) { tail = head; } }
head 앞에 새로운 노드가 위치하게 되기 때문에 head의 이전 값으로 새로운 노드를 지정하는 부분이 추가 되었습니다.
끝에 추가
public void addLast(Object input) { // 노드를 생성합니다. Node newNode = new Node(input); // 리스트의 노드가 없다면 첫번째 노드를 추가하는 메소드를 사용합니다. if (size == 0) { addFirst(input); } else { // tail의 다음 노드로 생성한 노드를 지정합니다. tail.next = newNode; // 새로운 노드의 이전 노드로 tail을 지정합니다. newNode.prev = tail; // 마지막 노드를 갱신합니다. tail = newNode; // 엘리먼트의 개수를 1 증가 시킵니다. size++; } }
특정 위치에 추가
public void add(int k, Object input) { // 만약 k가 0이라면 첫번째 노드에 추가하는 것이기 때문에 addFirst를 사용합니다. if (k == 0) { addFirst(input); } else { Node temp1 = node(k - 1); // k 번째 노드를 temp2로 지정합니다. Node temp2 = temp1.next; // 새로운 노드를 생성합니다. Node newNode = new Node(input); // temp1의 다음 노드로 새로운 노드를 지정합니다. temp1.next = newNode; // 새로운 노드의 다음 노드로 temp2를 지정합니다. newNode.next = temp2; // temp2의 이전 노드로 새로운 노드를 지정합니다. if (temp2 != null) temp2.prev = newNode; // 새로운 노드의 이전 노드로 temp1을 지정합니다. newNode.prev = temp1; size++; // 새로운 노드의 다음 노드가 없다면 새로운 노드가 마지막 노드이기 때문에 tail로 지정합니다. if (newNode.next == null) { tail = newNode; } } }
위의 코드를 보면 temp1을 지정하는 로직이 node(k-1)로 변경된 것을 볼 수 있습니다. 노드 탐색 작업이 빈번한 작업이기 때문에 별도의 메소드로 분리시켰습니다. 이 부분은 곧 살펴보겠습니다. 그 외의 코드 상의 변화는 앞뒤 노드를 연결해주는 것과 관련된 부분입니다. 그림을 그려서 관계를 따라그려보면 어렵지 않게 파악할 수 있는 내용입니다.
노드 찾기
아래는 노드를 찾는 작업을 메소드화시킨 것입니다.
Node node(int index) { // 노드의 인덱스가 전체 노드 수의 반보다 큰지 작은지 계산 if (index < size / 2) { // head부터 next를 이용해서 인덱스에 해당하는 노드를 찾습니다. Node x = head; for (int i = 0; i < index; i++) x = x.next; return x; } else { // tail부터 prev를 이용해서 인덱스에 해당하는 노드를 찾습니다. Node x = tail; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }
위의 코드는 중요한 내용을 담고 있습니다. 이중 연결 리스트의 장점이 담겨있기 때문입니다.
위의 그림과 같이 리스트의 노드가 6개라고 했을 때 get의 인자로 전달된 인덱스가 2(30의 인덱스)이하라면 head부터 next를 이용해서 탐색을 합니다. 인덱스가 3이상이라면 tail부터 prev를 이용해서 탐색을 할 수 있습니다. 즉 인덱스의 위치에 따라서 탐색 방향을 달리 할 수 있습니다. 덕분에 탐색 시간을 두배로 향상시킬 수 있습니다. 이것이 양방향으로 연결을 했을 때의 효용입니다.
삭제
처음 노드 삭제
public Object removeFirst() { // 첫번째 노드를 temp로 지정하고 head의 값을 두번째 노드로 변경합니다. Node temp = head; head = temp.next; // 데이터를 삭제하기 전에 리턴할 값을 임시 변수에 담습니다. Object returnData = temp.data; temp = null; // 리스트 내에 노드가 있다면 head의 이전 노드를 null로 지정합니다. if (head != null) head.prev = null; size--; return returnData; }
특정 위치 노드 삭제
public Object remove(int k) { if (k == 0) return removeFirst(); // k-1번째 노드를 temp로 지정합니다. Node temp = node(k - 1); // temp.next를 삭제하기 전에 todoDeleted 변수에 보관합니다. Node todoDeleted = temp.next; // 삭제 대상 노드를 연결에서 분리합니다. temp.next = temp.next.next; if (temp.next != null) { // 삭제할 노드의 전후 노드를 연결합니다. temp.next.prev = temp; } // 삭제된 노드의 데이터를 리턴하기 위해서 returnData에 데이터를 저장합니다. Object returnData = todoDeleted.data; // 삭제된 노드가 tail이었다면 tail을 이전 노드를 tail로 지정합니다. if (todoDeleted == tail) { tail = temp; } // cur.next를 삭제 합니다. todoDeleted = null; size--; return returnData; }
마지막 노드 삭제
public Object removeLast() { return remove(size - 1); }
인덱스로 엘리먼트 가져오기
public Object get(int k) { Node temp = node(k); return temp.data; }
탐색
다음 코드는 특정 데이터가 저장된 인덱스를 알아내는 방법입니다.
public int indexOf(Object data) { // 탐색 대상이 되는 노드를 temp로 지정합니다. Node temp = head; // 탐색 대상이 몇번째 엘리먼트에 있는지를 의미하는 변수로 index를 사용합니다. int index = 0; // 탐색 값과 탐색 대상의 값을 비교합니다. while (temp.data != data) { temp = temp.next; index++; // temp의 값이 null이라는 것은 더 이상 탐색 대상이 없다는 것을 의미합니다.이 때 -1을 리턴합니다. if (temp == null) return -1; } // 탐색 대상을 찾았다면 대상의 인덱스 값을 리턴합니다. return index; }
반복
반복자(iterator)를 이용해서 순차적으로 노드를 탐색하는 방법은 Linked list에서 이미 알아봤습니다. 그런데 doubly linked list는 반복과 관련해서 중요한 차이가 있습니다. 바로 탐색 방향을 자유롭게 변경할 수 있다는 것입니다. 기존의 iterator 대비 3개의 메소드를 더 추가하겠습니다.
이전 노드 탐색
이중 연결 리스트의 기본적인 클래스 골격은 아래와 같습니다. next는 다음번 리턴 할 노드를 가르키고, lastReturned는 마지막으로 리턴된 노드를 가르킵니다. lastReturned는 next를 한칸 뒤에서 따라다닙니다. nextIndex는 next를 호출했을 때 다음 번 리턴될 노드의 인덱스 값을 의미합니다.
public class ListIterator { private Node lastReturned; private Node next; private int nextIndex; ListIterator() { next = head; nextIndex = 0; } }
이중 연결 리스트는 next와 prev를 가지고 있기 때문에 탐색의 방향을 원하는데로 지정할 수 있습니다.
// 본 메소드를 호출하면 cursor의 참조값이 기존 cursor.next로 변경됩니다. public Object next() { lastReturned = next; next = next.next; nextIndex++; return lastReturned.data; } // cursor의 값이 없다면 다시 말해서 더 이상 next를 통해서 가져올 노드가 없다면 false를 리턴합니다. // 이를 통해서 next를 호출해도 되는지를 사전에 판단할 수 있습니다. public boolean hasNext() { return nextIndex < size(); } public boolean hasPrevious() { return nextIndex > 0; } public Object previous() { if (next == null) { lastReturned = next = tail; } else { lastReturned = next = next.prev; } nextIndex--; return lastReturned.data; }
다음은 탐색 과정에서 노드의 삭제, 추가와 관련된 작업을 구현한 코드입니다.
public void add(Object input) { Node newNode = new Node(input); if (lastReturned == null) { head = newNode; newNode.next = next; } else { lastReturned.next = newNode; newNode.prev = lastReturned; if (next != null) { newNode.next = next; next.prev = newNode; } else { tail = newNode; } } lastReturned = newNode; nextIndex++; size++; }
public void remove() { if (nextIndex == 0) { throw new IllegalStateException(); } Node n = lastReturned.next; Node p = lastReturned.prev; if (p == null) { head = n; head.prev = null; lastReturned = null; } else { p.next = next; lastReturned.prev = null; } if (n == null) { tail = p; tail.next = null; } else { n.prev = p; } if (next == null) { lastReturned = tail; } else { lastReturned = next.prev; } size--; nextIndex--; }
전체코드
아래는 전체 코드입니다.
package list.doublylinkedlist.implementation; public class DoublyLinkedList { // 첫번째 노드를 가리키는 필드 private Node head; private Node tail; private int size = 0; private class Node { // 데이터가 저장될 필드 private Object data; // 다음 노드를 가리키는 필드 private Node next; private Node prev; public Node(Object input) { this.data = input; this.next = null; this.prev = null; } // 노드의 내용을 쉽게 출력해서 확인해볼 수 있는 기능 public String toString() { return String.valueOf(this.data); } } public void addFirst(Object input) { // 노드를 생성합니다. Node newNode = new Node(input); // 새로운 노드의 다음 노드로 헤드를 지정합니다. newNode.next = head; // 기존에 노드가 있었다면 현재 헤드의 이전 노드로 새로운 노드를 지정합니다. if (head != null) head.prev = newNode; // 헤드로 새로운 노드를 지정합니다. head = newNode; size++; if (head.next == null) { tail = head; } } public void addLast(Object input) { // 노드를 생성합니다. Node newNode = new Node(input); // 리스트의 노드가 없다면 첫번째 노드를 추가하는 메소드를 사용합니다. if (size == 0) { addFirst(input); } else { // tail의 다음 노드로 생성한 노드를 지정합니다. tail.next = newNode; // 새로운 노드의 이전 노드로 tail을 지정합니다. newNode.prev = tail; // 마지막 노드를 갱신합니다. tail = newNode; // 엘리먼트의 개수를 1 증가 시킵니다. size++; } } Node node(int index) { // 노드의 인덱스가 전체 노드 수의 반보다 큰지 작은지 계산 if (index < size / 2) { // head부터 next를 이용해서 인덱스에 해당하는 노드를 찾습니다. Node x = head; for (int i = 0; i < index; i++) x = x.next; return x; } else { // tail부터 prev를 이용해서 인덱스에 해당하는 노드를 찾습니다. Node x = tail; for (int i = size - 1; i > index; i--) x = x.prev; return x; } } public void add(int k, Object input) { // 만약 k가 0이라면 첫번째 노드에 추가하는 것이기 때문에 addFirst를 사용합니다. if (k == 0) { addFirst(input); } else { Node temp1 = node(k - 1); // k 번째 노드를 temp2로 지정합니다. Node temp2 = temp1.next; // 새로운 노드를 생성합니다. Node newNode = new Node(input); // temp1의 다음 노드로 새로운 노드를 지정합니다. temp1.next = newNode; // 새로운 노드의 다음 노드로 temp2를 지정합니다. newNode.next = temp2; // temp2의 이전 노드로 새로운 노드를 지정합니다. if (temp2 != null) temp2.prev = newNode; // 새로운 노드의 이전 노드로 temp1을 지정합니다. newNode.prev = temp1; size++; // 새로운 노드의 다음 노드가 없다면 새로운 노드가 마지막 노드이기 때문에 tail로 지정합니다. if (newNode.next == null) { tail = newNode; } } } public String toString() { // 노드가 없다면 []를 리턴합니다. if (head == null) { return "[]"; } // 탐색을 시작합니다. Node temp = head; String str = "["; // 다음 노드가 없을 때까지 반복문을 실행합니다. // 마지막 노드는 다음 노드가 없기 때문에 아래의 구문은 마지막 노드는 제외됩니다. while (temp.next != null) { str += temp.data + ","; temp = temp.next; } // 마지막 노드를 출력결과에 포함시킵니다. str += temp.data; return str + "]"; } public Object removeFirst() { // 첫번째 노드를 temp로 지정하고 head의 값을 두번째 노드로 변경합니다. Node temp = head; head = temp.next; // 데이터를 삭제하기 전에 리턴할 값을 임시 변수에 담습니다. Object returnData = temp.data; temp = null; // 리스트 내에 노드가 있다면 head의 이전 노드를 null로 지정합니다. if (head != null) head.prev = null; size--; return returnData; } public Object remove(int k) { if (k == 0) return removeFirst(); // k-1번째 노드를 temp로 지정합니다. Node temp = node(k - 1); // temp.next를 삭제하기 전에 todoDeleted 변수에 보관합니다. Node todoDeleted = temp.next; // 삭제 대상 노드를 연결에서 분리합니다. temp.next = temp.next.next; if (temp.next != null) { // 삭제할 노드의 전후 노드를 연결합니다. temp.next.prev = temp; } // 삭제된 노드의 데이터를 리턴하기 위해서 returnData에 데이터를 저장합니다. Object returnData = todoDeleted.data; // 삭제된 노드가 tail이었다면 tail을 이전 노드를 tail로 지정합니다. if (todoDeleted == tail) { tail = temp; } // cur.next를 삭제 합니다. todoDeleted = null; size--; return returnData; } public Object removeLast() { return remove(size - 1); } public int size() { return size; } public Object get(int k) { Node temp = node(k); return temp.data; } public int indexOf(Object data) { // 탐색 대상이 되는 노드를 temp로 지정합니다. Node temp = head; // 탐색 대상이 몇번째 엘리먼트에 있는지를 의미하는 변수로 index를 사용합니다. int index = 0; // 탐색 값과 탐색 대상의 값을 비교합니다. while (temp.data != data) { temp = temp.next; index++; // temp의 값이 null이라는 것은 더 이상 탐색 대상이 없다는 것을 의미합니다.이 때 -1을 리턴합니다. if (temp == null) return -1; } // 탐색 대상을 찾았다면 대상의 인덱스 값을 리턴합니다. return index; } // 반복자를 생성해서 리턴해줍니다. public ListIterator listIterator() { return new ListIterator(); } public class ListIterator { private Node lastReturned; private Node next; private int nextIndex; ListIterator() { next = head; nextIndex = 0; } // 본 메소드를 호출하면 cursor의 참조값이 기존 cursor.next로 변경됩니다. public Object next() { lastReturned = next; next = next.next; nextIndex++; return lastReturned.data; } // cursor의 값이 없다면 다시 말해서 더 이상 next를 통해서 가져올 노드가 없다면 false를 리턴합니다. // 이를 통해서 next를 호출해도 되는지를 사전에 판단할 수 있습니다. public boolean hasNext() { return nextIndex < size(); } public boolean hasPrevious() { return nextIndex > 0; } public Object previous() { if (next == null) { lastReturned = next = tail; } else { lastReturned = next = next.prev; } nextIndex--; return lastReturned.data; } public void add(Object input) { Node newNode = new Node(input); if (lastReturned == null) { head = newNode; newNode.next = next; } else { lastReturned.next = newNode; newNode.prev = lastReturned; if (next != null) { newNode.next = next; next.prev = newNode; } else { tail = newNode; } } lastReturned = newNode; nextIndex++; size++; } public void remove() { if (nextIndex == 0) { throw new IllegalStateException(); } Node n = lastReturned.next; Node p = lastReturned.prev; if (p == null) { head = n; head.prev = null; lastReturned = null; } else { p.next = next; lastReturned.prev = null; } if (n == null) { tail = p; tail.next = null; } else { n.prev = p; } if (next == null) { lastReturned = tail; } else { lastReturned = next.prev; } size--; nextIndex--; } } }