에르노트
이중 연결 리스트 with Javascript 본문
연결 리스트(Linkd List)를 확장하여 양방향으로 링크가 구성되는 이중 연결 리스트를 만들 수 있다. 단일 연결 리스트의 노드가 next 포인터만을 가졌다면 이중 연결 리스트의 노드는 prev와 next 두 개의 포인터를 갖는다.
class LinkedListNode{ constructor(data){ this.data = data this.prev = null this.next = null } }
그리고 DoublyLinkedList 클래스 자체도 head와 더불어 tail을 추가 멤버로 가진다.
class DoublyLinkedList{ constructor(){ this.head = null this.tail = null this.size = 0 } isEmpty(){ return this.size == 0 } }
삽입
addFront(value){ //if list is empty if(this.isEmpty()){ this.head = new LinkedListNode(value) this.tail = this.head } else{ let tmp = new LinkedListNode(value) tmp.next = this.head this.head.prev = tmp this.head = tmp } this.size++ } addBack(value){ //if list is empty if(this.isEmpty()){ this.tail = new LinkedListNode(value) this.head = this.tail } else{ let tmp = new LinkedListNode(value) tmp.prev = this.tail this.tail.next = tmp this.tail = tmp } this.size++ }
이중 연결 리스트에서 요소의 삽입은 양방향으로 가능하다. head를 갱신하는 addFront() 방식과 tail을 갱신하는 addBack() 모두 사용할 수 있으므로 적재적소에 활용할 수 있다는 장점이 있다. 빈 리스트에 최초 삽입할 경우에는 head와 tail 모두 새로운 노드로 갱신해준다. head를 갱신할 때는 기존 헤드를 새 노드의 next로 삼고, 기존 헤드의 prev 포인터를 새 노드로 삼는다. 반대로 테일을 갱신하다면 기존 테일을 새 노드의 prev로 삼고, 기존 테일의 next를 새 노드로 삼아준다.
삭제
removeHead(){ if(this.isEmpty()) return null let data = this.head.data //if head(=tail) is target if(this.head == this.tail){ this.head = null this.tail = null } else{ this.head = this.head.next this.head.prev = null } this.size-- return data } removeTail(){ if(this.isEmpty()) return null let data = this.tail.data //if head(=tail) is target if(this.head == this.tail){ this.head = null this.tail = null } else{ this.tail = this.tail.prev this.tail.next = null } this.size-- return data } removeByValue(value){ let current = this.head //Using tail instead of head also works. //if head is target if(current.data == value){ this.head = current.next this.size-- } else{ let prev = current while(current.next){ if(current.data == value){ prev.next = current.next current = current.next break } prev = current current = current.next } if(current.data == value) prev.next = null this.size-- } }
삭제 역시 head를 없애거나, tail을 없애거나 양방향 접근이 가능하다. 단일 연결 리스트에서처럼 순차 탐색에 의해 원하는 값을 제거할 수도 있는데, 이 역시 head로부터 next 방향으로 탐색하든 tail로부터 prev 방향으로 탐색하든 무방하다. removeByValue() 메소드의 동작원리는 연결 리스트(Linkd List)를 참고하자.
검색
findStartingHead(value){ let current = this.head while(current.next){ if(current.data == value) return true current = current.next } return false } findStartingTail(value){ let current = this.tail while(current.prev){ if(current.data == value) return true current = current.prev } return false }
마찬가지로 양방향이다! 로직 자체는 단일 연결리스트에서의 검색과 완전히 동일하다. 다만 양방향 검색은 생각보다 더 큰 의미를 갖는데, 이중 연결 리스트에서는 단 하나의 노드로부터 전체 리스트에 대한 완전 탐색이 진행될 수 있다는 점이다. 단일 연결 리스트에서는 단방향으로만(next 방향으로만) 탐색이 가능하므로 주어진 노드 앞쪽(prev 방향) 정보는 알 길이 없었다. 즉 head에 전적으로 의존할 수 밖에 없는 구조였다. 반면 이중 연결 리스트는 그 어떤 노드가 주어지더라도 양방향 탐색을 통해 완전 탐색을 해낼 수 있다.
전체 소스(테스트 코드)
class LinkedListNode{ constructor(data){ this.data = data this.prev = null this.next = null } } class DoublyLinkedList{ constructor(){ this.head = null this.tail = null this.size = 0 } isEmpty(){ return this.size == 0 } addFront(value){ //if list is empty if(this.isEmpty()){ this.head = new LinkedListNode(value) this.tail = this.head } else{ let tmp = new LinkedListNode(value) tmp.next = this.head this.head.prev = tmp this.head = tmp } this.size++ } addBack(value){ //if list is empty if(this.isEmpty()){ this.tail = new LinkedListNode(value) this.head = this.tail } else{ let tmp = new LinkedListNode(value) tmp.prev = this.tail this.tail.next = tmp this.tail = tmp } this.size++ } removeHead(){ if(this.isEmpty()) return null let data = this.head.data //if head(=tail) is target if(this.head == this.tail){ this.head = null this.tail = null } else{ this.head = this.head.next this.head.prev = null } this.size-- return data } removeTail(){ if(this.isEmpty()) return null let data = this.tail.data //if head(=tail) is target if(this.head == this.tail){ this.head = null this.tail = null } else{ this.tail = this.tail.prev this.tail.next = null } this.size-- return data } removeByValue(value){ let current = this.head //Using tail instead of head also works. //if head is target if(current.data == value){ this.head = current.next this.size-- } else{ let prev = current while(current.next){ if(current.data == value){ prev.next = current.next current = current.next break } prev = current current = current.next } if(current.data == value) prev.next = null this.size-- } } findStartingHead(value){ let current = this.head while(current.next){ if(current.data == value) return true current = current.next } return false } findStartingTail(value){ let current = this.tail while(current.prev){ if(current.data == value) return true current = current.prev } return false } print(){ let result = "" let current = this.head while(current.next){ result += `${current.data} -> ` current = current.next } result += current.data console.log(result) } } (function test(){ let ll = new DoublyLinkedList() for(let i =1; i<=5; i++) ll.addFront(i) ll.print() //5->4->3->2->1 ll.removeHead() ll.print() //4->3->2->1 ll.removeTail() ll.print() //4->3->2 ll.removeByValue(3) ll.print() //4->2 ll.addBack(1) ll.print() //4->2->1 })()
정리
기본적으로 이중 연결리스트는 단일 연결리스트에 비해 포인터 역할의 변수 하나를 더 사용할 뿐이다. 따라서 공간복잡도 면에서 다소 손해를 보겠지만 시간복잡도는 단일 리스트와 다를 바가 없다. 메모리를 더 많이 사용한다는 오버헤드가 있지만 양방향 탐색이 가져다주는 이점이 더 크다고 볼 수 있다. 따라서 대부분의 Linked List는 Doubly Linked List로 구현된다고 한다.
시간복잡도 | 삽입 | 값에 의한 삭제 | 헤드/테일 삭제 | 검색 |
Doubly Linked List | O(1) | O(n) | O(1) | O(n) |
'CS > Algorithm' 카테고리의 다른 글
연결 리스트 with Javascript (0) | 2020.09.24 |
---|---|
스택 & 큐 with Javascript (1) | 2020.09.12 |