자료구조

[JS] Singly Linked List 구현 (단일 연결 리스트)

okonomiyakki 2023. 9. 16. 02:07
728x90

Singly Linked List는 head, tail 참조를 갖는 구조로 만든다.

 

마지막 요소 (tail)를 참조하는 포인터를 가지고 있어야 마지막 요소에 값을 추가하는 시간복잡도가 O(n)이 아니라 O(1)로 만들 수 있다.

구현할 메서드

  • addFirst(e) - 연결 리스트 맨 앞에 요소 추가
  • addLast(e) - 연결 리스트 맨 마지막에 요소 추가
  • add(index, e) - 특정 위치에 요소를 추가
  • get(index) - 특정 위치에 요소를 반환
  • set(index, e) - 특정 위치에 요소를 parameter로 전달된 요소로 변환
  • remove(index) - 특정 위치에 요소를 삭제

노드 생성자

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

연결리스트는 노드로 구성되어 있기 때문에 가장 먼저 노드 객체를 생성하는 생성자를 만들어주어야 한다.

 

해당 노드 생성자는 노드가 가지는 데이터를 초기화하고, 다음 노드를 가리키는 값을 null 로 초기 설정하는 역할을 한다.

add()

현재 포스팅 목적에 맞는 add() 메서드를 구현하기 위해서는 해당 메서드가 두 개로 나뉘어야 한다.

 

하나는 맨 앞에 추가되는 메서드와 다른 하나는 맨 끝에 추가되는 메서드이다.

 

우선은 가장 기본적으로 tail 포인터가 없는 가장 기본적으로 동작하는 add() 메서드를 구현해 보았다.

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

    add(data) {
        const newNode = new Node(data);

        if (!this.head) {
            this.head = newNode; // 연결 리스트가 비어있을 경우 head가 새로 추가된 노드를 가리키도록 설정
        } else {
            let current = this.head; // 현재 head인 노드

            while (current.next) {
                // next값이 null인 가장 끝(오른쪽) 노드가 될 때 까지 loop을 돌며 현재 노드를 마지막 노드로 초기화
                current = current.next;
            }

            current.next = newNode; // 마지막 노드의 next가 새로 추가한 노드를 가리키도록 설정
        }
    }
}
  1. 연결리스트 생성자를 만들어 준다.
  2. head 를 null로 초기화 한다.
  3. add() 메서드 안에서 인자값을 통해 새로운 노드 객체를 생성한다.
  4. 첫 노드를 추가하는 작업인 경우에는 head 가 해당 노드를 가리키도록 한다.
  5. 그 이후 부터는 현재 head 가 가리키는 노드가 가리키는 다음 노드가 null 이 될 때 까지, while 문을 돌며 노드의 next 값을 초기화 한다.
  6. 마지막으로 새로운 노드를 현재 노드의 다음을 가리키도록 설정한다.

연결 리스트 생성자

class LinkedList {
    constructor() {
        this.head = null;
        this.tail = null;
        this.length = 0;
    }
}

addFirst()

    addFirst(data) {
        const newNode = new Node(data);

        if (!this.head) {
            // 연결 리스트가 비어있을 경우 head, tail 각각 새로 추가된 노드를 가리키도록 설정
            this.head = newNode;
            this.tail = newNode;
        } else {
            newNode.next = this.head; // 현재 head가 가리키는 노드를 새로 추가된 노드의 next로 초기화
            this.head = newNode; // 새로 추가된 노드로 head를 초기화
        }
        this.length++; // 전체 길이 증가
    }

여기서는 새로운 노드가 맨 앞에 추가되어야 하기 때문에

 

새로 추가된 노드의 next 값을 기존의 head 값으로 초기화 해주고,

 

기존의 head 값을 새로운 노드로 초기화 해주어야 한다.

addLast()

    addLast(data) {
        const newNode = new Node(data);

        if (!this.head) {
            this.head = newNode;
            this.tail = newNode;
        } else {
            this.tail.next = newNode; // 현재 tail이 가리키는 노드의 next 값을 새로 추가된 노드로 초기화
            this.tail = newNode; // 새로 추가된 노드로 tail을 초기화
        }
        this.length++;
    }

새로운 노드가 맨 끝에 추가되려면,

 

tail과 tail 이 가리키는 next 값을 새로 추가된 노드를 가리키도록 만들면 된다.

add()

    add(index, data) {
        if (index === 0) {
            this.addFirst(data);
        } else if (index === this.length) {
            this.addLast(data);
        } else {
            const newNode = new Node(data);

            let current = this.head;
            for (let i = 0; i < index - 1; i++) {
                current = current.next; // current를 index - 1 위치에 존재하는 노드로 설정 (우리가 넣고자 하는 인덱스의 직전 노드)
            }

            newNode.next = current.next; // 새로 추가된 노드가 현재 노드의 다음 노드를 가리키도록 초기화
            current.next = newNode; // 현재 노드가 새로 추가된 노드를 가리키도록 초기화

            this.length++;
        }
    }

원하는 인덱스에 data 를 삽입하는 경우에는 현재 노드를 삽입 위치 직전의 노드로 설정하고,

 

현재 노드의 next 값을 새로 추가된 노드로,

 

새로 추가된 노드의 next 값을 현재 노드의 next 로 초기화 시켜주어야 한다.

get()

    get(index) {
        let current = this.head;
        for (let i = 0; i < index; i++) {
            current = current.next; // current를 index 위치에 존재하는 노드로 설정 (우리가 찾고자 하는 인덱스의 노드)
        }

        return current.data;
    }

원하는 값을 찾을 때는 위의 add() 메서드에서 for 문만 살짝 수정해주면 쉽게 구현이 가능하다.

set()

  set(index, data) {
    let current = this.head;
    for (let i = 0; i < index; i++) {
      current = current.next;
    }

    current.data = data;
  }

마찬가지로 원하는 인덱스에 위치한 data를 바꾸는 작업는 get() 메서드와 유사하다.

 

get() 이 찾기 였다면,

 

set() 은 찾고, 초기화다.

remove()

    remove(index) {
        if (index === 0) {
            this.head = this.head.next;

            if (!this.head) {
                this.tail = null;
            }
        } else {
            let current = this.head;
            for (let i = 0; i < index - 1; i++) {
                current = current.next; // current를 index - 1 위치에 존재하는 노드로 설정 (우리가 삭제하고자 하는 인덱스의 직전 노드)
            }

            current.next = current.next.next; // 삭제할 노드의 전 노드가 삭제할 노드의 다음 노드를 가리키도록 초기화

            if (!current.next) {
                this.tail = current; // 해당 값이 null일때, tail이 해당 노드를 가리키도록 초기화
            }
        }
        this.length--;
    }

add() 메서드와 동일한 방식으로 원하는 인덱스 직전 노드를 찾은 다음,

 

삭제할 노드의 직전 노드가 삭제할 노드를 건너 띄고 그 다음 노드를 가리키도록 초기화해준다.

 

이때, 그 다음 노드가 존재하지 않아 null이 할당 될 경우 해당 노드가 마지막 노드가 되므로 tail이 해당 노드를 가리키도록 초기화 시켜준다.

전체 코드

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

class LinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.length = 0;
  }

  addFirst(data) {
    const newNode = new Node(data);

    if (!this.head) {
      this.head = newNode;
      this.tail = newNode;
    } else {
      newNode.next = this.head;
      this.head = newNode;
    }
    this.length++;
  }

  addLast(data) {
    const newNode = new Node(data);

    if (!this.head) {
      this.head = newNode;
      this.tail = newNode;
    } else {
      this.tail.next = newNode;
      this.tail = newNode;
    }
    this.length++;
  }

  add(index, data) {
    if (index === 0) {
      this.addFirst(data);
    } else if (index === this.length) {
      this.addLast(data);
    } else {
      const newNode = new Node(data);

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

      newNode.next = current.next;
      current.next = newNode;

      this.length++;
    }
  }

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

    return current.data;
  }

  set(index, data) {
    let current = this.head;
    for (let i = 0; i < index; i++) {
      current = current.next;
    }

    current.data = data;
  }

  remove(index) {
    if (index === 0) {
      this.head = this.head.next;

      if (!this.head) {
        this.tail = null;
      }
    } else {
      let current = this.head;
      for (let i = 0; i < index - 1; i++) {
        current = current.next;
      }

      current.next = current.next.next;

      if (!current.next) {
        this.tail = current;
      }
    }
    this.length--;
  }
}

마무리

연결 리스트는 삽입 및 삭제 연산에서 배열 보다 훨신 빠른 시간 복잡도로 연산을 수행할 수 있다.

 

가장 기본이라고 생각되는 자료구조이기 때문에 꼭 알고 넘어가야한다고 생각한다.

 

추가적으로 이중 연결 리스트, 원형 연결 리스트가 있다.

 

이중 연결 리스트는 주로 역순 및 양방향 탐색이 필요한 경우나 데이터의 삽입 및 삭제가 빈번한 경우에 사용하고,

 

원형 연결 리스트는 순환적인 작업이 필요한 상황에서 사용한다고 한다.

728x90