Skip to content

JavaScript로 이중 연결 리스트 구현하기

CS / Data Structure
  • data-structure
  • linked-list
  • doubly-linked-list
  • cs
  • javascript

이번 노트에서는 양방향으로 이동 가능한 이중 연결 리스트를 구현해보겠습니다.

노드와 리스트 만들기

Node 클래스에 prev를 추가하고 DoublyLinkedList는 이전과 같이 head, tail, size로 구성합니다.

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
    this.prev = null;
  }
}

class DoublyLinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.size = 0;
  }
}

맨 앞에 추가하기

새 노드가 기존 head를 가리키도록 하고, 기존 head의 prev도 새 노드로 연결합니다.

prepend(value) {
  const newNode = new Node(value);
  newNode.next = this.head;

  if (this.head === null) {
    this.tail = newNode;
  } else {
    this.head.prev = newNode;
  }

  this.head = newNode;
  this.size += 1;
}

빈 리스트에서는 새 노드가 첫 번째이면서 마지막 노드이므로 tail도 함께 지정합니다.

맨 뒤에 추가하기

새 노드의 prev에 기존 tail을 저장하고, 기존 tail의 next를 새 노드로 바꿉니다.

append(value) {
  const newNode = new Node(value);
  newNode.prev = this.tail;

  if (this.tail === null) {
    this.head = newNode;
  } else {
    this.tail.next = newNode;
  }

  this.tail = newNode;
  this.size += 1;
}

빈 리스트에서는 head도 새 노드를 가리키게 합니다. 새 노드의 next는 초기값인 null을 유지합니다.

순회하며 값 확인하기

값을 배열로 반환하는 toArray는 이전 리스트 구현과 동일합니다. head에서 시작해 next를 따라갑니다.

toArray() {
  const values = [];
  let current = this.head;

  while (current !== null) {
    values.push(current.value);
    current = current.next;
  }

  return values;
}

원하는 위치의 값 읽기

조회, 삽입, 삭제에 필요한 노드를 찾는 과정은 #getNode로 분리합니다. 앞뒤로 이동할 수 있으므로 인덱스가 앞쪽에 있으면 head에서, 뒤쪽에 있으면 tail에서 탐색을 시작합니다.

#getNode(index) {
  if (index < this.size / 2) {
    let current = this.head;

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

    return current;
  }

  let current = this.tail;

  for (let i = this.size - 1; i > index; i -= 1) {
    current = current.prev;
  }

  return current;
}

get(index) {
  if (!Number.isInteger(index) || index < 0 || index >= this.size) {
    throw new RangeError("Index out of range");
  }

  return this.#getNode(index).value;
}

원하는 위치에 삽입하기

맨 앞과 맨 뒤 삽입은 prepend, append로 처리합니다. 중간에 삽입할 때는 해당 위치의 노드를 찾고, prev로 바로 앞 노드에 접근합니다.

insert(index, value) {
  if (!Number.isInteger(index) || index < 0 || index > this.size) {
    throw new RangeError("Index out of range");
  }

  if (index === 0) {
    this.prepend(value);
    return;
  }

  if (index === this.size) {
    this.append(value);
    return;
  }

  const nextNode = this.#getNode(index);
  const previousNode = nextNode.prev;
  const newNode = new Node(value);

  newNode.prev = previousNode;
  newNode.next = nextNode;
  previousNode.next = newNode;
  nextNode.prev = newNode;
  this.size += 1;
}

새 노드의 prev와 next를 지정한 뒤 앞 노드의 next와 뒤 노드의 prev도 새 노드를 가리키도록 바꿉니다.

맨 앞에서 삭제하기

removeFirst는 head를 다음 노드로 옮기고 새 head의 prev를 null로 바꿉니다.

removeFirst() {
  if (this.size === 0) {
    throw new RangeError("List is empty");
  }

  const removedNode = this.head;
  this.head = removedNode.next;

  if (this.head === null) {
    this.tail = null;
  } else {
    this.head.prev = null;
  }

  this.size -= 1;
  return removedNode.value;
}

노드가 하나뿐이었다면 head가 null이 됩니다. 이때 tail도 null로 바꾸면 size가 0인 빈 리스트가 됩니다.

맨 뒤에서 삭제하기

removeLast는 tail을 이전 노드로 옮기고 새 tail의 next를 null로 바꿉니다.

removeLast() {
  if (this.size === 0) {
    throw new RangeError("List is empty");
  }

  const removedNode = this.tail;
  this.tail = removedNode.prev;

  if (this.tail === null) {
    this.head = null;
  } else {
    this.tail.next = null;
  }

  this.size -= 1;
  return removedNode.value;
}

마지막 남은 노드를 삭제하면 head도 null로 바꿉니다.

원하는 위치에서 삭제하기

첫 번째와 마지막 노드는 removeFirst, removeLast로 삭제합니다. 중간 노드는 삭제 대상을 찾은 뒤 양쪽 연결을 바꿉니다.

remove(index) {
  if (!Number.isInteger(index) || index < 0 || index >= this.size) {
    throw new RangeError("Index out of range");
  }

  if (index === 0) {
    return this.removeFirst();
  }

  if (index === this.size - 1) {
    return this.removeLast();
  }

  const removedNode = this.#getNode(index);

  removedNode.prev.next = removedNode.next;
  removedNode.next.prev = removedNode.prev;
  this.size -= 1;

  return removedNode.value;
}

삭제 대상의 앞 노드와 뒤 노드가 서로를 가리키게 하면 대상이 리스트에서 빠집니다.

전체 코드

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
    this.prev = null;
  }
}

class DoublyLinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.size = 0;
  }

  prepend(value) {
    const newNode = new Node(value);
    newNode.next = this.head;

    if (this.head === null) {
      this.tail = newNode;
    } else {
      this.head.prev = newNode;
    }

    this.head = newNode;
    this.size += 1;
  }

  append(value) {
    const newNode = new Node(value);
    newNode.prev = this.tail;

    if (this.tail === null) {
      this.head = newNode;
    } else {
      this.tail.next = newNode;
    }

    this.tail = newNode;
    this.size += 1;
  }

  removeFirst() {
    if (this.size === 0) {
      throw new RangeError("List is empty");
    }

    const removedNode = this.head;
    this.head = removedNode.next;

    if (this.head === null) {
      this.tail = null;
    } else {
      this.head.prev = null;
    }

    this.size -= 1;
    return removedNode.value;
  }

  removeLast() {
    if (this.size === 0) {
      throw new RangeError("List is empty");
    }

    const removedNode = this.tail;
    this.tail = removedNode.prev;

    if (this.tail === null) {
      this.head = null;
    } else {
      this.tail.next = null;
    }

    this.size -= 1;
    return removedNode.value;
  }

  #getNode(index) {
    if (index < this.size / 2) {
      let current = this.head;

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

      return current;
    }

    let current = this.tail;

    for (let i = this.size - 1; i > index; i -= 1) {
      current = current.prev;
    }

    return current;
  }

  get(index) {
    if (!Number.isInteger(index) || index < 0 || index >= this.size) {
      throw new RangeError("Index out of range");
    }

    return this.#getNode(index).value;
  }

  insert(index, value) {
    if (!Number.isInteger(index) || index < 0 || index > this.size) {
      throw new RangeError("Index out of range");
    }

    if (index === 0) {
      this.prepend(value);
      return;
    }

    if (index === this.size) {
      this.append(value);
      return;
    }

    const nextNode = this.#getNode(index);
    const previousNode = nextNode.prev;
    const newNode = new Node(value);

    newNode.prev = previousNode;
    newNode.next = nextNode;
    previousNode.next = newNode;
    nextNode.prev = newNode;
    this.size += 1;
  }

  remove(index) {
    if (!Number.isInteger(index) || index < 0 || index >= this.size) {
      throw new RangeError("Index out of range");
    }

    if (index === 0) {
      return this.removeFirst();
    }

    if (index === this.size - 1) {
      return this.removeLast();
    }

    const removedNode = this.#getNode(index);

    removedNode.prev.next = removedNode.next;
    removedNode.next.prev = removedNode.prev;
    this.size -= 1;

    return removedNode.value;
  }

  toArray() {
    const values = [];
    let current = this.head;

    while (current !== null) {
      values.push(current.value);
      current = current.next;
    }

    return values;
  }
}