이중 연결 리스트(Doubly linked list)
- 이중 연결 리스트는 앞에서 살펴본 단일 연결 리스트에서
이전의 노드를 가리키는 포인터
를 하나 더하는 것 뿐이다. 그러니까 각각의 노드에 포인터
가 두 개씩 있게 된다.
- 이중 연결 리스트는 이처럼 반대 방향 포인터도 갖게 되어 성능상 유연함을 갖게 됐지만, 더 많은 메모리가 필요하게 됐다.
기본 구조
- 이중 연결 리스트의 constructor는 단일 연결 리스트의 것과 같다.
class Node {
constructor(val) {
this.val = val;
this.next = null;
this.prev = null;
}
}
class DoublyLinkedList {
constructor() {
this.head = null;
this.tail = null;
this.length = 0;
}
}
Push 메서드
push(val) {
const newNode = new Node(val);
if (this.length === 0) {
this.head = newNode;
this.tail = newNode;
} else {
this.tail.next = newNode;
newNode.prev = this.tail;
this.tail = newNode;
}
this.length++;
return this;
}
Pop 메서드
pop() {
if (!this.head) return undefined;
const poppedNode = this.tail;
if (this.length === 1) {
this.head = null;
this.tail = null;
} else {
this.tail = poppedNode.prev;
this.tail.next = null;
poppedNode.prev = null;
}
this.length--;
return poppedNode;
}
Shift 메서드
shift() {
if (this.length === 0) return undefined;
const oldHead = this.head;
if (this.length === 1) {
this.head = null;
this.tail = null;
} else {
this.head = oldHead.next;
this.head.prev = null;
oldHead.next = null;
}
this.length--;
return oldHead;
}
Unshift 메서드
unshift(val) {
const newNode = new Node(val);
if (this.length === 0) {
this.head = newNode;
this.tail = newNode;
} else {
this.head.prev = newNode;
newNode.next = this.head;
this.head = newNode;
}
this.length++;
return this;
}
Get 메서드
- 연결 리스트에서 위치를 통해 노드를 반환받는다.
- 효율적으로 루프롤 돌기 위해, 찾는 index가 해당 리스트의 중앙을 기준으로 어디에 있는지 확인하고, 그 결과에 따라 앞에서부터 루프를 시작할지, 뒤에서부터 루프를 시작할지 결정한다.
get(index) {
if (index < 0 || index >= this.length) return null;
let count;
let current;
if (index <= this.length / 2) {
count = 0;
current = this.head;
while (count !== index) {
current = current.next;
count++;
}
} else {
count = this.length - 1;
current = this.tail;
while (count !== index) {
current = current.prev;
count--;
}
}
return current;
}
Set 메서드
- 연결 리스트에서 특정 위치의 노드의 값를 변경한다.
- 위에서 만든 get 메서드를 사용한다.
- 단일 연결 리스트에서 해당 메서드 로직과 동일하다.
set(index, val) {
const foundNode = this.get(index);
if (foundNode != null) {
foundNode.val = val;
return true;
}
return false;
}
Insert 메서드
insert(index, val) {
if (index < 0 || index > this.length) return false;
if (index === this.length) return !!this.push(val);
if (index === 0) return !!this.unshift(val);
const newNode = new Node(val);
const beforeNode = this.get(index - 1);
const afterNode = beforeNode.next;
(beforeNode.next = newNode), (newNode.prev = beforeNode);
(newNode.next = afterNode), (afterNode.prev = newNode);
this.length++;
return true;
}
Remove 메서드
- 연결 리스트에서 특정 위치에 있는 노드를 삭제한다.
remove(index) {
if (index < 0 || index > this.length) return undefined;
if (index === 0) return this.shift();
if (index === this.length - 1) return this.pop();
const removedNode = this.get(index);
removedNode.prev.next = removedNode.next;
removedNode.next.prev = removedNode.prev;
removedNode.next = null;
removedNode.prev = null;
this.length--;
return removedNode;
}
}
- 위 코드에서 헷갈리는 부분은 아마 이 부분일 텐데, 변수로 할당해서 단순화할 수 있다.
removedNode.prev.next = removedNode.next;
const beforeNode = removedNode.prev;
beforeNode.next = afterNode;
--------------------------------------------------------------------
removedNode.next.prev = removedNode.prev;
const afterNode = removedNode.next;
afterNode.prev = beforeNode;
Reverse 메서드
- 이중 연결 리스트의 reverse 메서드는 head와 tail을 바꾸고, 모든 노드의 next 포인터와 prev 포인터 방향을 뒤집음로써, 리스트 자체를 뒤집는다.
reverse() {
let node = this.head;
this.head = this.tail;
this.tail = node;
let next;
let prev = null;
for (let i = 0; i < this.length; i++) {
next = node.next;
node.next = prev;
prev = node;
node = next;
}
next = null;
node = this.head;
for (let i = 0; i < this.length; i++) {
prev = node.prev;
node.prev = next;
next = node;
node = prev;
}
}
- reverse 메서드 실행 결과 확인
- 이 메서드는 강의에서 알려주지 않아서, 그냥 혼자 만들어 본 것이므로 정확하지 않을 수도 있는 점을 참고해주세요.
const list = new DoublyLinkedList();
list.push(5).push(10).push(15).push(20);
console.log(list);
list.reverse();
console.log(list);
이중 연결 리스트의 성능
시간복잡도
- 삽입 : O(1)
- 제거 : O(1)
- 탐색 : O(N)
- 정확하게는 탐색의 시간복잡도는 O(N/2)이나, Big O에 의하면 여전히 O(N)이다.
- 접근 : O(N)
결론
- 이중 연결 리스트는 이전 노드를 향하는 포인터가 추가됐다는 점만 빼면 단일 연겨 리스트와 거의 비슷하다.
- 노드를 찾을 때는 이중 연결 리스트가 단일 연결리스트보다 시간을 절반으로 줄일 수 있으므로 더 낫다. 이 외의 이중 연결 리스트의 시간복잡도는 단일 연결 리스트의 성능과 같다.
- 하지만, 이중 연결 리스트는 추가 포인터 때문에 더 많은 메모리가 필요하다는 점이 단점이다.