- JavaScript Algorithms and Data Structures Masterclass(Udemy Course)
기본적으로 이중 연결 리스트는 지난 포스팅에서 다룬 단일 연결 리스트와 구조가 동일하다.
다만, 각 노드가 다음 노드에 대한 포인터 뿐만 아니라 이전 노드에 대한 포인터도 갖고 있다는 점이 다르다.
즉, 단일 연결 리스트에 비해 메모리를 두 배로 잡아 먹는다
(공간 복잡도가 두배이다)
공간 복잡도를 희생한 만큼 유연해지는 상충 관계가 발생한다
const Node = {
init: function (val) {
this.val = val;
this.prev = null;
this.next = null;
},
};
// Node는 Object.create으로 생성한 위임객체로부터 동작을 위임받을 수임객체
const head = Object.create(Node);
// head는 init의 작동을 Node에 위임한 위임객체
head.init("시작 노드");
const DoublyLinkedList = {
init: function () {
this.head = null;
this.tail = null;
this.length = 0;
},
};
const list = Object.create(DoublyLinkedList);
list.init();
const DoublyLinkedList = {
// 앞 생략
push: function (val) {
const newNode = Object.create(Node);
newNode.init(val);
if (!this.head) {
// head가 없다는 것은 연결 리스트의 head와 tail을 초기화해주어야 한다는 것
this.head = newNode;
this.tail = newNode;
} else {
this.tail.next = newNode;
newNode.prev = this.tail;
this.tail = newNode;
}
this.length++;
return this;
},
};
const list = Object.create(DoublyLinkedList);
list.init();
const DoublyLinkedList = {
// 앞 생략
pop: function () {
if (!this.head) {
return;
}
const oldTail = this.tail;
if (this.length == 1) {
this.head = null;
this.tail = null;
} else {
const newTail = oldTail.prev;
newTail.next = null;
this.tail = newTail;
oldTail.prev = null;
}
this.length--;
return oldTail;
},
};
const list = Object.create(DoublyLinkedList);
list.init();
list.push("우분투");
list.push("리눅스");
단일 연결 리스트가 tail바로 이전 노드를 잡기 위해 전체 순회를 돌아야 했던 반면,
이중 연결 리스트는 바로 tail에 접근해서 prev포인터로 이전 노드를 잡을 수 있기 때문에 시간 복잡도가 1이다.
const DoublyLinkedList = {
// 앞 생략
shift: function () {
if (!this.head) {
return;
}
const oldHead = this.head;
if (this.length == 1) {
this.head = null;
this.tail = null;
} else {
const newHead = oldHead.next;
newHead.prev = null;
this.head = newHead;
oldHead.next = null;
}
this.length--;
return oldHead;
},
};
const list = Object.create(DoublyLinkedList);
list.init();
list.push("우분투");
list.push("리눅스");
const DoublyLinkedList = {
// 앞 생략
unshift: function (val) {
const newNode = Object.create(Node);
newNode.init(val);
if (!this.head) {
this.head = newNode;
this.tail = newNode;
} else {
newNode.next = this.head;
this.head.prev = newNode;
this.head = newNode;
}
this.length++;
return this;
},
};
const list = Object.create(DoublyLinkedList);
list.init();
const DoublyLinkedList = {
// 앞 생략
get: function (idx) {
if (idx < 0 || idx >= this.length) {
return;
}
let currentNode, limit, command;
const middleIndex = Math.floor((this.length - 1) / 2);
if (idx <= middleIndex) {
currentNode = this.head;
limit = idx;
command = "next";
console.log("head부터");
} else {
currentNode = this.tail;
limit = this.length - 1 - idx;
command = "prev";
console.log("tail부터");
}
for (let i = 0; i < limit; i++) {
currentNode = currentNode[command];
}
return currentNode;
},
};
const list = Object.create(DoublyLinkedList);
list.init();
list.push(0);
list.push(1);
list.push(2);
list.push(3);
list.push(4);
list.push(5);
const DoublyLinkedList = {
// 앞 생략
set: function (idx, val) {
const targetNode = this.get(idx);
if (!targetNode) {
return;
}
targetNode.val = val;
return targetNode;
},
};
const list = Object.create(DoublyLinkedList);
list.init();
list.push(0);
list.push(1);
list.push(2);
const DoublyLinkedList = {
// 앞 생략
insert: function (idx, val) {
if (idx < 0 || idx > this.length) {
return;
}
if (idx == 0) {
return this.unshift(val);
}
if (idx == this.length) {
return this.push(val);
}
const newNode = Object.create(Node);
newNode.init(val);
const previousNode = this.get(idx - 1);
// 앞선 예외처리의 결과로 여기에서 idx - 1의 범위는 다음과 같다.
// 1 <= idx <= this.length - 1
// 0 <= (idx - 1) <= this.length - 2
// 따라서 previousNode는 반드시 존재할 수 밖에 없음.
const nextNode = previousNode.next;
// previousNode의 인덱스 범위에 1을 더한 값을 범위로 가진다.
// 즉, 1 <= indexOfNextNode <= this.length - 1
// 역시, nextNode도 반드시 존재한다.
previousNode.next = newNode;
newNode.prev = previousNode;
newNode.next = nextNode;
nextNode.prev = newNode;
this.length++;
return this;
},
};
const list = Object.create(DoublyLinkedList);
list.init();
list.push(0);
list.push(1);
list.push(2);
인덱스가 head와 tail중 어디에 가깝느냐에 따라 시작점을 달리한다는 것 이외에는 단일 연결 리스트와 동일하다
get을 재사용했기에 시간 복잡도는 n으로 간주한다.
const DoublyLinkedList = {
// 앞 생략
remove: function (idx) {
if (idx < 0 || idx >= this.length) {
return;
}
if (idx == 0) {
return this.shift();
}
if (idx == this.length - 1) {
return this.pop();
}
const targetNode = this.get(idx);
const previousNode = targetNode.prev;
const nextNode = targetNode.next;
previousNode.next = nextNode;
nextNode.prev = previousNode;
targetNode.prev = null;
targetNode.next = null;
this.length--;
return targetNode;
},
};
const list = Object.create(DoublyLinkedList);
list.init();
list.push(0);
list.push(1);
list.push(2);
list.push(3);
list.push(4);
const DoublyLinkedList = {
// 앞 생략
find: function (val) {
if (!this.head) {
return;
}
let currentNode = this.head;
for (let i = 0; i < this.length; i++) {
if (currentNode.val == val) {
return i.toString();
}
currentNode = currentNode.next;
}
return;
},
};
const list = Object.create(DoublyLinkedList);
list.init();
list.push("우분투");
list.push("레드햇");
list.push("CentOS");
const DoublyLinkedList = {
// 앞 생략
removeByValue: function (val) {
const indexOfTargetNode = this.find(val);
if (isNaN(parseInt(indexOfTargetNode))) {
return;
}
return this.remove(indexOfTargetNode);
},
};
const list = Object.create(DoublyLinkedList);
list.init();
list.push("우분투");
list.push("레드햇");
list.push("CentOS");
list.push("리눅스");
list.push("자바스크립트");
const DoublyLinkedList = {
// 앞 생략
removeDuplicate: function () {
if (!this.head) {
return this;
}
let currentNode = this.head;
const hashTable = {};
let count = 0;
while (true) {
let nextNode = currentNode.next;
// 밑에서 remove할 경우 currentNode의 next참조가 사라지기 때문에
// 해당 참조를 보존할 목적
if (!hashTable.hasOwnProperty(currentNode.val)) {
hashTable[currentNode.val] = 1;
} else {
this.remove(count);
count--;
}
if (nextNode) {
currentNode = nextNode;
count++;
} else {
return this;
}
}
},
};
const list = Object.create(DoublyLinkedList);
list.init();
list.push("우분투");
list.push("리눅스");
list.push("우분투");
list.push("CentOS");
list.push("리눅스");
list.push("우분투");
const Node = {
init: function (val) {
this.val = val;
this.prev = null;
this.next = null;
},
};
const DoublyLinkedList = {
init: function () {
this.head = null;
this.tail = null;
this.length = 0;
},
push: function (val) {
const newNode = Object.create(Node);
newNode.init(val);
if (!this.head) {
// head가 없다는 것은 연결 리스트의 head와 tail을 초기화해주어야 한다는 것
this.head = newNode;
this.tail = newNode;
} else {
this.tail.next = newNode;
newNode.prev = this.tail;
this.tail = newNode;
}
this.length++;
return this;
},
pop: function () {
if (!this.head) {
return;
}
const oldTail = this.tail;
if (this.length == 1) {
this.head = null;
this.tail = null;
} else {
const newTail = oldTail.prev;
newTail.next = null;
this.tail = newTail;
oldTail.prev = null;
}
this.length--;
return oldTail;
},
shift: function () {
if (!this.head) {
return;
}
const oldHead = this.head;
if (this.length == 1) {
this.head = null;
this.tail = null;
} else {
const newHead = oldHead.next;
newHead.prev = null;
this.head = newHead;
oldHead.next = null;
}
this.length--;
return oldHead;
},
unshift: function (val) {
const newNode = Object.create(Node);
newNode.init(val);
if (!this.head) {
this.head = newNode;
this.tail = newNode;
} else {
newNode.next = this.head;
this.head.prev = newNode;
this.head = newNode;
}
this.length++;
return this;
},
get: function (idx) {
if (idx < 0 || idx >= this.length) {
return;
}
let currentNode, limit, command;
const middleIndex = Math.floor((this.length - 1) / 2);
if (idx <= middleIndex) {
currentNode = this.head;
limit = idx;
command = "next";
console.log("head부터");
} else {
currentNode = this.tail;
limit = this.length - 1 - idx;
command = "prev";
console.log("tail부터");
}
for (let i = 0; i < limit; i++) {
currentNode = currentNode[command];
}
return currentNode;
},
set: function (idx, val) {
const targetNode = this.get(idx);
if (!targetNode) {
return;
}
targetNode.val = val;
return targetNode;
},
insert: function (idx, val) {
if (idx < 0 || idx > this.length) {
return;
}
if (idx == 0) {
return this.unshift(val);
}
if (idx == this.length) {
return this.push(val);
}
const newNode = Object.create(Node);
newNode.init(val);
const previousNode = this.get(idx - 1);
// 앞선 예외처리의 결과로 여기에서 idx - 1의 범위는 다음과 같다.
// 1 <= idx <= this.length - 1
// 0 <= (idx - 1) <= this.length - 2
// 따라서 previousNode는 반드시 존재할 수 밖에 없음.
const nextNode = previousNode.next;
// previousNode의 인덱스 범위에 1을 더한 값을 범위로 가진다.
// 즉, 1 <= indexOfNextNode <= this.length - 1
// 역시, nextNode도 반드시 존재한다.
previousNode.next = newNode;
newNode.prev = previousNode;
newNode.next = nextNode;
nextNode.prev = newNode;
this.length++;
return this;
},
remove: function (idx) {
if (idx < 0 || idx >= this.length) {
return;
}
if (idx == 0) {
return this.shift();
}
if (idx == this.length - 1) {
return this.pop();
}
const targetNode = this.get(idx);
const previousNode = targetNode.prev;
const nextNode = targetNode.next;
previousNode.next = nextNode;
nextNode.prev = previousNode;
targetNode.prev = null;
targetNode.next = null;
this.length--;
return targetNode;
},
find: function (val) {
if (!this.head) {
return;
}
let currentNode = this.head;
for (let i = 0; i < this.length; i++) {
if (currentNode.val == val) {
return i.toString();
}
currentNode = currentNode.next;
}
return;
},
removeByValue: function (val) {
const indexOfTargetNode = this.find(val);
if (isNaN(parseInt(indexOfTargetNode))) {
return;
}
return this.remove(indexOfTargetNode);
},
removeDuplicate: function () {
if (!this.head) {
return this;
}
let currentNode = this.head;
const hashTable = {};
let count = 0;
while (true) {
let nextNode = currentNode.next;
// 밑에서 remove할 경우 currentNode의 next참조가 사라지기 때문에
// 해당 참조를 보존할 목적
if (!hashTable.hasOwnProperty(currentNode.val)) {
hashTable[currentNode.val] = 1;
} else {
this.remove(count);
count--;
}
if (nextNode) {
currentNode = nextNode;
count++;
} else {
return this;
}
}
},
};
const list = Object.create(DoublyLinkedList);
list.init();