연결리스트(Linked List)란 무엇인가

2020.03.09

메인

연결리스트(Linked List)란?

데이터를 순서대로 나열한 자료구조로 순서가 있는 데이터를 관리할 때 사용한다.

배열(배열 문서 참고)과 유사하지만, 배열과는 달리 데이터가 메모리상에 연속적으로 위치하지는 않는다.
각 데이터는 포인터로 연결되어 있어 데이터끼리 물리적으로 떨어져 있어도 된다.

HEAD포인터, 리스트요소들로 구성되어있다.

HEAD포인터는 첫번째 리스트요소의 위치정보를 저장한다.
그래서 리스트의 데이터 유무는 HEAD포인터만 확인하는것으로 쉽게 알 수 있다.

각 리스트 요소는 NEXT포인터를 가지고 있다.

각 리스트의 요소는 다음 요소의 위치정보를 가지고 있는 NEXT포인터를 가지고있다.
마지막 요소는 NEXT포인터가 없는 것으로 구별한다.

연결리스트의 장점

데이터의 삽입/삭제 성능이 빠르다.

데이터의 삽입/삭제 후 다른 요소들의 위치를 변경시킬 필요 없이 데이터의 포인터들만 변경시켜주면 된다. - O(1) 실행시간

연결리스트의 단점

순차 접근(sequential access)만 가능하므로 검색이 비효율적이다.

연결리스트에서 조회를 하려면 리스트의 첫 요소(head)부터 순차적으로 조회를 해야하므로 검색 성능이 좋지 않다.- O(n) 실행시간

모든 요소의 값을 다 읽어야 한다면 괜찮지만, 특정한 요소를 조회할때는 그 전의 모든 요소들을 조회해야하므로 비효율적이다.
따라서 조회 연산이 많은 상황이라면 연결리스트보다는 배열이 더 적절한 자료구조이다.

인터페이스

  • head - 리스트의 첫 요소
  • length - 리스트의 길이
  • append(element) - 리스트의 끝에 요소를 추가한다
  • insert(position, element) - 특정 위치에 요소를 추가한다.
  • remove(element) - 특정 요소를 삭제한다.
  • removeAt(position) - 특정 위치의 요소를 삭제한다.
  • indexOf(element) - 요소의 인덱스를 반환한다.
  • isEmpty() - 리스트가 비었는지 확인한다.
  • size() - 요소의 갯수를 반환한다.
  • toString() - 전체 리스트요소를 문자열로 반환한다.

구현 - JavaScript에서의 연결리스트

class LinkedListNode {
  constructor(element) {
    this._element = element;
    this._next = null;
  }

  get element() {
    return this._element;
  }

  get next() {
    return this._next;
  }

  set next(node) {
    this._next = node;
  }

  toString() {
    return `element: ${this.element} / next: ${this.next && this.next.element}`;
  }
}

class LinkedList {
  constructor() {
    this._head = null;
    this._length = 0;
  }

  get head() {
    return this._head;
  }

  set head(node) {
    this._head = node;
  }

  get length() {
    return this._length;
  }

  set length(val) {
    this._length = val;
  }

  append(element) {
    const node = new LinkedListNode(element);

    if (!this.head) {
      this.head = node;
    } else {
      let current = this.head;
      while (current.next) {
        current = current.next;
      }

      let last = current;
      last.next = node;
    }
    this.length++;
  }

  insert(position, element) {
    if (
      typeof position !== "number" ||
      position < 0 ||
      position > this.length - 1
    ) {
      return false;
    }

    const node = new LinkedListNode(element);

    if (position === 0) {
      node.next = this.head;
      this.head = node;
    } else {
      const previous = this._findPreviousByPosition(position);

      node.next = previous.next;
      previous.next = node;
    }
    this.length++;
  }

  remove(element) {
    if (!this.head) {
      return null;
    }

    if (this.head.element === element) {
      this.head = this.head.next;
    } else {
      let current = this.head;
      let previous;
      while (current && !previous) {
        if (current.next && current.next.element === element) {
          previous = current;
        }

        current = current.next;
      }

      const target = previous.next;
      previous.next = target.next;
    }

    this.length--;
  }

  removeAt(position) {
    if (
      typeof position !== "number" ||
      position < 0 ||
      position > this.length - 1
    ) {
      return false;
    }

    if (position === 0) {
      this.head = this.head.next;
    } else {
      const previous = this._findPreviousByPosition(position);
      const target = previous.next;
      previous.next = target.next;
    }

    this.length--;
  }

  indexOf(element) {
    let current = this.head;
    let index = 0;

    while (current) {
      if (current.element === element) {
        return index;
      }
      current = current.next;
      index++;
    }

    return -1;
  }

  isEmpty() {
    return this.length === 0;
  }

  size() {
    return this.length;
  }

  toString() {
    let result = "### Linked List ###\n\n";
    result += `[head] ${this.head.toString()}\n\n`;
    let current = this.head;

    while (current) {
      result += `[node] ${current.toString()}\n`;
      current = current.next;
    }

    result += `\nlength: ${this.length}`;

    return result;
  }

  _findPreviousByPosition(position) {
    let previous = this.head;
    let loopTime = 0;
    while (loopTime++ < position - 1) {
      previous = previous.next;
    }

    return previous;
  }
}

// Test
it("LinkedList 클래스는 잘 동작한다.", () => {
  const linkedList = new LinkedList();
  linkedList.append("쿠스");
  linkedList.append("톰");
  linkedList.append("마이클");
  linkedList.append("미셀");

  expect(linkedList.head.element).toEqual("쿠스");
  expect(linkedList.head.next.element).toEqual("톰");
  expect(linkedList.head.next.next.element).toEqual("마이클");
  expect(linkedList.head.next.next.next.element).toEqual("미셀");

  const result1 = linkedList.insert(9, "에이미");
  expect(result1).toEqual(false);

  linkedList.insert(0, "에이미");
  expect(linkedList.head.element).toEqual("에이미");

  linkedList.insert(3, "룰라");
  expect(linkedList.head.next.next.next.element).toEqual("룰라");

  linkedList.remove("룰라");
  expect(linkedList.head.next.next.next.element).toEqual("마이클");

  linkedList.remove("에이미");
  expect(linkedList.head.element).toEqual("쿠스");

  linkedList.removeAt(2);
  expect(linkedList.head.next.next.element).toEqual("미셀");

  expect(linkedList.length).toEqual(3);

  expect(linkedList.indexOf("미셀")).toEqual(2);
  expect(linkedList.indexOf("티라노")).toEqual(-1);

  expect(linkedList.isEmpty()).toEqual(false);

  expect(linkedList.size()).toEqual(3);
});

활용 사례

큐는 항상 첫번째 요소만 읽고 삽입, 삭제 연산이 많으므로 큐를 구현하는데 유리하다.

확장 - 이중 연결 리스트 Doubly Linked List

양방향 리스트라고도 한다.
단방향 리스트와 달리 양방향으로 리스트 순회가 가능해진다.

HEAD포인터, TAIL포인터, 리스트요소들로 구성되어있다.

단방향 리스트에 TAIL포인터가 추가된다.

각 리스트 요소는 NEXT포인터, PREV포인터를 가지고 있다.

단방향 연결 리스트의 리스트 요소에 이전 요소의 위치정보를 가지고 있는 'PREV포인터'가 추가된다.

구현 - JavaScript에서의 이중 연결 리스트 Doubly Linked List

위에서 구현한 LinkedListNode 클래스와 LinkedList 클래스를 상속받아 필요한 부분만 추가 & 오버라이드해서 구현했다.

class DoublyLinkedListNode extends LinkedListNode {
  constructor(element) {
    super(element);
    this._prev = null;
  }

  get prev() {
    return this._prev;
  }

  set prev(node) {
    this._prev = node;
  }

  toString() {
    return `element: ${this.element} / prev: ${this.prev &&
      this.prev.element} / next: ${this.next && this.next.element}`;
  }
}

class DoublyLinkedList extends LinkedList {
  constructor() {
    super();
    this._tail = null;
  }

  get tail() {
    return this._tail;
  }

  set tail(node) {
    this._tail = node;
  }

  append(element) {
    const node = new DoublyLinkedListNode(element);

    if (!this.head) {
      this.head = node;
      this.tail = node;
    } else {
      let current = this.head;
      while (current.next) {
        current = current.next;
      }

      let last = current;
      last.next = node;
      node.prev = last;
      this.tail = node;
    }
    this.length++;
  }

  insert(position, element) {
    if (
      typeof position !== "number" ||
      position < 0 ||
      position > this.length - 1
    ) {
      return false;
    }

    const node = new DoublyLinkedListNode(element);

    if (position === 0) {
      node.next = this.head;
      this.head = node;
    } else {
      const previous = this._findPreviousByPosition(position);

      node.next = previous.next;
      node.prev = previous;
      if (previous.next) {
        previous.next.prev = node;
      }
      previous.next = node;
    }
    this.length++;
  }

  remove(element) {
    if (!this.head) {
      return null;
    }

    if (this.head.element === element) {
      if (this.head.next) {
        this.head.next.prev = null;
      }
      this.head = this.head.next;
      if (this.head === this.tail) {
        this.tail = this.head.next;
      }
    } else if (this.tail.element === element) {
      this.tail = this.head.perv;
      if (this.head === this.tail) {
        this.head = this.tail.prev;
      }
    } else {
      let current = this.head;
      let previous;
      while (current && !previous) {
        if (current.next && current.next.element === element) {
          previous = current;
        }
        current = current.next;
      }

      const target = previous.next;
      if (target.next) {
        target.next.prev = previous;
      }
      previous.next = target.next;
    }
    this.length--;
  }

  removeAt(position) {
    if (
      typeof position !== "number" ||
      position < 0 ||
      position > this.length - 1
    ) {
      return false;
    }

    if (position === 0) {
      this.head = this.head.next;
      this.head.prev = null;
    } else if (position === this.length - 1) {
      this.tail = this.tail.prev;
      this.tail.next = null;
    } else {
      const previous = this._findPreviousByPosition(position);

      const target = previous.next;
      if (target.next) {
        target.next.prev = previous;
      }
      previous.next = target.next;
    }

    this.length--;
  }

  toString() {
    let result = "### Doubly Linked List ###\n\n";
    result += `[head] ${this.head.toString()}\n\n`;

    let current = this.head;

    while (current) {
      result += `[node] ${current.toString()}\n`;
      current = current.next;
    }
    result += `\n[tail] ${this.tail.toString()}\n`;

    result += `\nlength: ${this.length}`;

    return result;
  }
}

// Test
it("DoublyLinkedList 클래스는 잘 동작한다.", () => {
  const doublyLinkedList = new DoublyLinkedList();

  doublyLinkedList.append("쿠스");
  doublyLinkedList.append("쉘든");
  doublyLinkedList.insert(1, "바우");
  doublyLinkedList.append("나비");

  expect(doublyLinkedList.head.element).toEqual("쿠스");
  expect(doublyLinkedList.head.next.element).toEqual("바우");
  expect(doublyLinkedList.head.next.next.element).toEqual("쉘든");
  expect(doublyLinkedList.head.next.next.next.element).toEqual("나비");

  expect(doublyLinkedList.tail.element).toEqual("나비");

  doublyLinkedList.remove("쿠스");
  doublyLinkedList.remove("바우");

  expect(doublyLinkedList.head.element).toEqual("쉘든");
  expect(doublyLinkedList.head.next.element).toEqual("나비");
  doublyLinkedList.append("복실");
  doublyLinkedList.append("만수");
  doublyLinkedList.append("앨리스");

  doublyLinkedList.removeAt(2);
  doublyLinkedList.removeAt(0);
  doublyLinkedList.removeAt(2);
  expect(doublyLinkedList.head.element).toEqual("나비");
  expect(doublyLinkedList.head.next.element).toEqual("만수");

  expect(doublyLinkedList.length).toEqual(2);
});

확장 - 환형 연결 리스트 Circular Linked List

마지막 리스트요소의 NEXT 포인터가 첫번째 요소를 가리킨다.

구현 - JavaScript에서의 환형 연결 리스트 Circular Linked List

위에서 구현한 LinkedList 클래스를 상속받아
요소 추가시 마지막 요소의 next포인터가 head를 향하도록 오버라이드했다.

class CircularLinkedList extends LinkedList {
  append(element) {
    const node = new LinkedListNode(element);

    if (!this.head) {
      this.head = node;
      this.head.next = node;
    } else {
      let current = this.head;
      let last;

      while (current) {
        if (current.next === this.head) {
          last = current;
          break;
        }

        current = current.next;
      }

      last.next = node;
      node.next = this.head;
    }
    this.length++;
  }

  toString() {
    let result = "### Linked List ###\n\n";
    result += `[head] ${this.head.toString()}\n\n`;
    let current = this.head;

    while (current) {
      result += `[node] ${current.toString()}\n`;
      current = current.next === this.head ? null : current.next; // infinite loop 방지
    }

    result += `\nlength: ${this.length}`;

    return result;
  }
}

// Test
it("CircularLinkedList 클래스는 잘 동작한다.", () => {
  const circularLinkedList = new CircularLinkedList();
  circularLinkedList.append("쿠스");
  circularLinkedList.append("빵꾸");
  circularLinkedList.append("울랄라");

  expect(circularLinkedList.head.element).toEqual("쿠스");
  expect(circularLinkedList.head.next.element).toEqual("빵꾸");
  expect(circularLinkedList.head.next.next.element).toEqual("울랄라");
  expect(circularLinkedList.head.next.next.next.element).toEqual("쿠스");
});

활용 사례

스트림버퍼 구현에 많이 사용된다.

  • 아디트야 바르가바, 『Hello Coding 그림으로 개념을 이해하는 알고리즘』, 김도형, 한빛미디어(2017)
  • 스기우라 켄, 『그림으로 배우는 알고리즘』, 서재원, 영진(2016)
  • 이소 히로시, 『모던 자바스크립트 입문』, 서재원, 길벗(2018)
  • Martin Erwig, 『그들은 알고리즘을 알았을까?』, 송원형, 영진(2017)
  • Loiane Groner, 『자바스크립트 자료 구조와 알고리즘』, 이일웅, PACKT-에이콘(2015)
글쓴이쿠스, Qus
직업은 소프트웨어 개발자.
기술로 사회문제를 해결하는 시빅해킹(Civic Hacking)에 관심이 많고,
이것저것 생각나는 것들을 글로 정리하는 것을 좋아합니다.