단일 연결 리스트(Singly linked list)
- 단일 연결 리스트는
head
, tail
, length
프로퍼티들을 포함한 자료구조다.
- 단일 연결 리스트는
node
들로 구성되어 있으며, 각각의 node
는 value
와 다른 노드나 null을 향한 pointer
를 갖고 있다.
- 마치 다음 열차와 연결되어 있는 기차와 같다.
- 단일 연결 리스트에는 인덱스가 없다. 예를 들어, 마지막 노드를 찾으려면 첫 노드에서부터 포인터를 통해 다른 노드들을 모두 순차적으로 지니야 한다.
- 연결 리스트의 특징
- 연결 리스트는 인덱스가 없다.
- 연결 리스트는 무작위 접근이 불가능하다. 즉, 10번째 요소에 접근하려면 처음부터 10번째 요소까지 차례대로 순회하여야 한다.
- (반면, 배열은 원하는 인덱스의 요소에 무작위 접근이 빠르게 가능하다)
- 연결 리스트는 노드와 포인터로 구성되어 있다. 따라서 노드의 삽입이나 삭제는 그 노드의 앞 뒤 노드에만 영향을 미친다.
- (반면, 배열은 요소의 삽입이나 삭제 시, 기존의 모든 요소들이 인덱스를 다시 받아야 하므로 비용이 더 든다고 볼 수 있다.)
- 이러한 이유 때문에 아주 긴 데이터에서 무작위 접근은 하지 않고 저장이나 삭제를 주로 한다면, 배열 대신 연결 리스트를 사용하는 것이 효율적일 수 있다.
기본 구조
- Node 클래스의 constructor
- 연결 리스트를 구성하는 기본 자료구조인 Node 클래스를 정의하고, 필요한 곳에서 인스턴스화하여 사용한다.
- value는 this.val에 할당하고, 다음으로 연결된 노드에 대한 pointer는 this.next에 할당한다.
- SinglyLinkedList의 construcotr
class Node {
constructor(val) {
this.val = val;
this.next = null;
}
}
class SinglyLinkedList {
constructor() {
this.head = null;
this.tail = null;
this.length = 0;
}
}
push 메서드
- push 메서드는 연결 리스트의 맨 뒤에 노드를 추가한다.
- 단일 연결 리스트 push 메서드 자바스크립트 코드
push(val) {
const newNode = new Node(val);
if (!this.head) {
this.head = newNode;
this.tail = this.head;
} else {
this.tail.next = newNode;
this.tail = newNode;
}
this.length++;
return this;
}
}
- 이 클래스를 이용하여 값들을 push하고 해당 연결리스트를 출력하면 아래의 주석과 같은 형태로 나타난다.
const list = new SinglyLinkedList();
list.push('HELLO');
list.push('MARCO');
list.push('GOODBYE');
console.log(list);
pop 메서드
- pop 메서드는 연결 리스트의 맨 뒤에서 노드를 제거하고, 제거한 노드를 반환한다.
pop() {
if (!this.head) return undefined;
let current = this.head;
let newTail = current;
while (current.next) {
newTail = current;
current = current.next;
}
this.tail = newTail;
this.tail.next = null;
this.length--;
if (this.length === 0) {
this.head = null;
this.tail = null;
}
return current;
}
Shift 메서드
- Shift 메서드는 연결 리스트에서 첫 번재 노드를 제거한다. (= head를 삭제한다. = 두 번째 노드를 head로 지정한다)
shift() {
if (!this.head) return undefined;
const currentHead = this.head;
this.head = currentHead.next;
this.length--;
if (this.length === 0) {
this.tail = null;
}
return currentHead;
}
Unshift 메서드
- 연결 리스트의 첫 번째에 새 노드를 추가한다.(연결 리스트의 구조 덕분에 간단하다)
unshift(val) {
const newNode = new Node(val);
if (!this.head) {
this.head = newNode;
this.tail = this.head;
}
newNode.next = this.head;
this.head = newNode;
this.length++;
return this;
}
Get 메서드
- 연결 리스트에서 위치를 통해 노드를 반환받는다.
- 연결리스트의 특성상 개별 요소에 상응하는 인덱스는 존재하지 않는다. 따라서 그 위치까지 순회(루프)하여 노드를 반환받는다.
get(index) {
if (index < 0 || index >= this.length) return null;
let counter = 0;
let current = this.head;
while (counter !== index) {
current = current.next;
counter++;
}
return current;
}
Set 메서드
- 연결 리스트에서 특정 위치의 노드의 값를 변경한다.
set(index, val) {
const foundNode = this.get(index);
if (foundNode) {
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 prev = this.get(index - 1);
const temp = prev.next;
prev.next = newNode;
newNode.next = temp;
this.length++;
return true;
}
Remove 메서드
- 연결 리스트에서 특정 위치에 있는 노드를 삭제한다.
- A→B→C 노드로 된 연결리스트가 있다고 했을 때, B 노드를 삭제하려고 할 경우
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 previousNode = this.get(index - 1);
const removed = previousNode.next;
previousNode.next = removed.next;
this.length--;
return removed;
}
Reverse 메서드
- 연결 리스트의 노드 순서를 뒤집기
- 순회를 하면서 하나씩 뒤집어 놓는 방법을 사용한다.
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;
}
return this;
}
단일 연결 리스트의 성능
시간복잡도
- 삽입(제일 앞이나 뒤에) : O(1)
- 단일 연결리스트는 제일 앞이나 뒤에 노드 추가를 할 때, O(1)의 시간 복잡도를 갖는다.
- [비교] 배열의 경우, 제일 앞에 노드 추가는 O(N)의 시간 복잡도를 갖는다. 따라서 삽입의 경우 단일 연결 리스트가 유리하다.
- 제거(제일 앞이나 뒤에) : 첫 번째 노드 제거는 O(1), 마지막 노드 제거는 O(N)
- 단일 연결리스트는 제일 앞에서 노드를 제거하면 O(1)의 시간 복잡도를 갖고, 제일 뒤에서 노드를 제거하면 O(N)의 시간복잡도를 갖는다(제일 뒤에서 두 번째에 있는 노드를 처음부터 순회해야 하므로) .
- 탐색 : O(N)
- 접근 : O(N)
- 배열에서는 인덱스로 접근을 하므로 동일한 시간인 O(1)만 소요된다.
결론
- 단일 연결 리스트는 삽입과 제거라는 분야에서 배열보다 성능이 앞선다. 따라서 맨 앞 부분에서 삽입과 제거를 자주 해서 이와 관련한 효율이 필요하고, 무작위 접근은 필요하지 않아서 요소에 접근 시 순서대로 접근해도 괜찮은 상황이라면, 단일 연결 리스트는 좋은 대체품이다.
- 배열에는 내장 인덱스가 있지만, 단일 연결 리스트에는 내장 인덱스가 없다. 단일 연결 리스트는 서로가
.next
라는 포인터로 연결되어 있는 노드들의 연결체다.