Skip to content

연결 리스트 (Linked List) ​

연결 리스트는 여러 개의 데이터를 순서대로 저장하는 자료구조이다.

배열처럼 데이터를 순서대로 다룰 수 있지만,
메모리상에 데이터를 연속적으로 저장하지 않고 각 데이터를 서로 연결해서 관리한다.


연결 리스트란? ​

연결 리스트는 데이터와 다음 데이터의 위치 정보를 함께 저장하는 자료구조이다.

연결 리스트에서 각각의 데이터를 노드(Node)라고 한다.

txt
노드 = 데이터 값 + 다음 노드를 가리키는 참조
  • 하나의 노드는 데이터를 담는 변수와 다음 노드를 가리키는 변수를 가진다.

연결 리스트의 구조 ​

연결 리스트는 첫 번째 노드의 주소만 알고 있으면 다른 모든 노드에 접근할 수 있다.

첫 번째 노드를 보통 head라고 부른다.

txt
head
↓
[10 | next] → [20 | next] → [30 | null]

head에서 시작해 각 노드의 next를 따라가면 다음 노드로 이동할 수 있다.


txt
head → 10 → 20 → 30 → null

즉, 연결 리스트는 첫 번째 노드부터 차례대로 연결을 따라가며 데이터를 확인한다.


연결 리스트 순회하기 ​

연결 리스트는 배열처럼 인덱스로 바로 접근할 수 없다.
따라서 첫 번째 노드부터 next를 따라가며 순서대로 확인해야 한다.

js
let current = node1;

while (current !== null) {
  console.log(current.data);
  current = current.next;
}
// 10;
// 20;
// 30;

위 코드는 현재 노드의 값을 출력한 뒤,
current를 다음 노드로 이동시키며 모든 노드를 확인한다.

연결 리스트의 모든 노드를 확인해야 하므로 전체 순회의 시간 복잡도는 O(n)이다.


배열과 연결 리스트 비교 ​

배열과 연결 리스트는 모두 여러 데이터를 순서대로 저장할 수 있지만,
데이터를 저장하고 접근하는 방식이 다르다.

구분배열연결 리스트
크기고정동적
주소연속불연속
데이터 참조O(1)O(n)
삽입과 삭제O(n)위치를 알고 있으면 O(1),
위치를 찾아야 하면 O(n)

데이터 참조 ​

배열은 인덱스를 사용해 원하는 위치의 데이터에 바로 접근할 수 있다.

js
const numbers = [10, 20, 30];

console.log(numbers[2]); // 30

배열은 시작 위치와 인덱스를 이용해 값을 바로 찾을 수 있기 때문에 데이터 참조가 O(1)이다.


반면 연결 리스트는 특정 위치에 바로 접근할 수 없다.

txt
head → 10 → 20 → 30 → null

예를 들어 30을 찾으려면 head에서 시작해 10, 20, 30 순서로 이동해야 한다.
따라서 연결 리스트에서 특정 데이터를 참조하거나 찾는 작업은 O(n)이다.


삽입과 삭제 ​

배열은 중간에 데이터를 삽입하거나 삭제하면 뒤에 있는 요소들의 위치를 이동해야 한다.

txt
[10, 20, 30]
↓ 15 삽입
[10, 15, 20, 30]

그래서 배열의 앞이나 중간에서 삽입과 삭제가 일어나면 보통 O(n)의 시간이 걸린다.


반면 연결 리스트는 노드의 연결만 바꾸면 데이터를 삽입하거나 삭제할 수 있다.

txt
[10] → [20] → [30]

10과 20 사이에 15를 추가하려면 다음처럼 연결을 바꾸면 된다.

txt
[10] → [15] → [20] → [30]

이미 삽입할 위치를 알고 있다면 연결만 변경하면 되므로 O(1)에 처리할 수 있다.
하지만 삽입할 위치를 찾기 위해 처음부터 순회해야 한다면, 위치를 찾는 데 O(n)이 걸릴 수 있다.


연결 리스트가 적합한 경우 ​

연결 리스트는 데이터의 삽입과 삭제가 자주 일어나는 경우에 유리할 수 있다.

txt
삽입과 삭제가 자주 일어난다 → 연결 리스트

반대로 특정 위치의 데이터를 자주 참조해야 한다면 배열이 더 유리하다.

txt
참조가 자주 일어난다 → 배열

배열은 인덱스를 이용해 원하는 위치의 데이터에 O(1)로 접근할 수 있지만,
연결 리스트는 첫 번째 노드부터 순서대로 이동해야 하므로 참조에 O(n)이 걸린다.


연결 리스트의 추상 자료형 ​

추상 자료형 (Abstract Data Type, ADT)

추상 자료형은 데이터와 그 데이터에 대해 수행할 수 있는 연산을 정의한 것이다.

즉, 내부 구현 방식보다 “어떤 기능을 제공하는가”에 초점을 둔다.

연결 리스트는 다음과 같은 기능을 제공할 수 있다.

기능메서드설명
모든 데이터 출력printAll()연결 리스트의 모든 노드를 순서대로 출력한다.
모든 데이터 제거clear()연결 리스트를 비운다.
인덱스 삽입insertAt(index, data)원하는 위치에 새 데이터를 삽입한다.
마지막 삽입insertLast(data)연결 리스트의 마지막에 데이터를 삽입한다.
인덱스 삭제deleteAt(index)원하는 위치의 데이터를 삭제한다.
마지막 삭제deleteLast()연결 리스트의 마지막 데이터를 삭제한다.
인덱스 읽기getNodeAt(index)원하는 위치의 노드를 가져온다.

JavaScript로 연결 리스트 구현하기 ​

Node 클래스 ​

각 노드는 데이터와 다음 노드의 참조를 가진다.

js
class Node {
  constructor(data, next = null) {
    this.data = data;
    this.next = next;
  }
}
  • data: 노드가 저장하는 값
  • next: 다음 노드를 가리키는 참조

LinkedList 클래스 ​

js
class LinkedList {
  constructor() {
    this.head = null;
    this.count = 0;
  }

  printAll() {
    let currentNode = this.head;
    let text = "[";

    while (currentNode !== null) {
      text += currentNode.data;
      currentNode = currentNode.next;

      if (currentNode !== null) {
        text += ", ";
      }
    }
    text += "]";
    console.log(text);
  }

  clear() {
    this.head = null;
    this.count = 0;
  }

  insertAt(index, data) {
    if (index > this.count || index < 0) {
      throw new Error("범위를 넘어갔습니다.");
    }

    let newNode = new Node(data);

    if (index === 0) {
      newNode.next = this.head;
      this.head = newNode;
    } else {
      let currentNode = this.head;

      for (let i = 0; i < index - 1; i++) {
        currentNode = currentNode.next;
      }
      newNode.next = currentNode.next;
      currentNode.next = newNode;
    }
    this.count++;
  }

  insertLast(data) {
    this.insertAt(this.count, data);
  }

  deleteAt(index) {
    if (index >= this.count || index < 0) {
      throw new Error("제거할 수 없습니다.");
    }

    let currentNode = this.head;

    if (index === 0) {
      let deletedNode = this.head;
      this.head = deletedNode.next;
      this.count--;
      return deletedNode;
    } else {
      for (let i = 0; i < index - 1; i++) {
        currentNode = currentNode.next;
      }

      let deletedNode = currentNode.next;
      currentNode.next = currentNode.next.next;
      this.count--;
      return deletedNode;
    }
  }

  deleteLast() {
    return this.deleteAt(this.count - 1);
  }

  getNodeAt(index) {
    if (index >= this.count || index < 0) {
      throw new Error("범위를 넘어갔습니다.");
    }

    let currentNode = this.head;

    for (let i = 0; i < index; i++) {
      currentNode = currentNode.next;
    }

    return currentNode;
  }
}

기본 노드 연결 ​

js
let node1 = new Node(1);
let node2 = new Node(2);
let node3 = new Node(3);

node1.next = node2;
node2.next = node3;

console.log(node1.data); // 1
console.log(node1.next.data); // 2
console.log(node1.next.next.data); // 3

insertAt() - 특정 위치에 삽입 ​

js
let list = new LinkedList();

list.insertAt(0, 0);
list.insertAt(1, 1);
list.insertAt(2, 2);
list.insertAt(3, 3);
list.insertAt(4, 4);
list.printAll(); // [0, 1, 2, 3, 4]

clear() - 모든 데이터 제거 ​

js
list.clear();
list.printAll(); // []

insertLast() - 마지막에 삽입 ​

js
list.insertLast(0);
list.insertLast(1);
list.insertLast(2);
list.printAll(); // [0, 1, 2]

deleteAt() - 특정 위치에서 삭제 ​

js
list.deleteAt(0);
list.printAll(); // [1, 2]

list.deleteAt(1);
list.printAll(); // [1]

deleteLast() - 마지막 노드 삭제 ​

js
list.insertLast(5);
list.printAll(); // [1, 5]

list.deleteLast();
list.printAll(); // [1]

getNodeAt() - 특정 위치의 노드 조회 ​

js
list.insertLast(1);
list.insertLast(2);
list.insertLast(3);
list.insertLast(4);
list.insertLast(5);
list.printAll(); // [1, 1, 2, 3, 4, 5]

let secondNode = list.getNodeAt(2);
console.log(secondNode); // Node { data: 2, next: Node { ... } }