Published on

자료구조 linkedList 에 대해서

Authors
  • avatar
    Name
    길재훈
    Twitter

linkedList 를 구현해보자

LinkedList 는 일반적이 배열과는 다르게, Memory 주소 를 통해 서로가 서로를 참조하는 방식으로 이루어져있다

이러한 Meomory Address 를 가리키는 것을 Pointer 라고 하는데, LinkedList 는 값과 함께 다음에 참조할 Pointer 를 가진다.

이렇게 LinkedList 를 통해 참조가 이루어진다면, 배열처럼 메모리 할당시 Overhead 없이 사용가능하며, 무엇보다 앞에서 다룬 QueueStack 처럼 원소 추가 및 삭제시 원소를 이동할 필요없이, Pointer 로 다음 연결한 원소를 연결해주면 된다

이는 마치 사람이 손에 손잡은 형태로 이루어져 있으며, 중간에 빠지는 사람은 손을 놓고, 그다음 사람의 손을 다시 잡는 형태로 이루어진다.

이를 구현한 로직은 다음과 같다

interface INode {
  elm: any // <-- Node 의 값
  next: INode | null // <-- next 가 null 이면 리스트의 마지막 Node 이다
  //     그렇지 않다면 다음 Node 를 참조한다
}

interface ILinkedList {
  append(elm: any): INode // <-- Node 추가
  insert(elm: any, pos: number): INode // pos 에 해당하는 Node 추가
  remove(elm: any): INode | string // <-- 값에 해당하는 Node 삭제
  removeAt(pos: number): INode | string // <-- pos 에 해당하는 Node 삭제
  toString(): string // <-- Node 의 값들을 문자열로 나열
  indexOf(elm: INode): number // <-- Node 의 index 반환
  size(): number // <-- Node 의 갯수반환
  isEmpty(): boolean // <-- List 가 비어있으면 true, 아니면 false
  clear(): void // <-- List 를 초기화
}

class LinkedNode implements INode {
  public next = null // <-- 다음 원소의 Pointer 참조, 참조 하지 않는다면 null 이다
  constructor(public elm: any) {} // <-- LinkedNode 생성시 들어갈 원소값
}

class LinkedList implements ILinkedList {
  private head: INode | null = null // <-- LinkedList 의 첫번째 Node참조 변수
  private length: number = 0 // <-- LinkedList 의 길이

  append(elm: any) {
    let cur = null
    if (!this.head) {
      // this.head 가 없다면 아직 리스트 생성이 이루어지지 않았으므로
      // 첫번째 Node 생성
      this.head = new LinkedNode(elm)
      this.length += 1
      return this.head
    } else {
      // Node 가 있으므로. this.head 를 참조하여
      // next 값이 null 인 요소를 찾는다
      cur = this.head
      while (cur.next) {
        cur = cur.next
      }
      cur.next = new LinkedNode(elm) // next 에 새로운 LinkedNode 생성
      this.length += 1
      return cur.next
    }
  }

  insert(elm: any, pos: number) {
    let prev = null
    if (!this.head) {
      // <-- this.head 가 null 이면 this.head 에 새로운 Node 생성
      this.head = new LinkedNode(elm)
      this.length += 1
      return this.head
    }
    if (pos <= this.length) {
      // <-- pos 가 length 보다 작다면 요소를 찾는다
      let cur = null
      let idx = 1 // <-- 1 부터 시작

      cur = this.head
      while (idx < pos && cur.next) {
        prev = cur
        cur = cur.next
        idx += 1
      }
      // pos 값이 항상 idx 값보다 크지는 않다
      // 이럴경우 prev 가 존재하지 않을 수 있다.
      // 밑은 prev 값이 존재한다면 실행한다
      if (prev) {
        prev.next = new LinkedNode(elm)
        prev.next.next = cur
        this.length += 1
        return prev.next
      }
      // prev 값이 존재하지 않는다면, 첫번째 값이라고 가정한다.
      // perv 에 현재의 this.head 를 참조하고
      // this.head 에 새로운 Node 를 만든후 next 값을 prev 로 연결한다.
      prev = this.head
      this.head = new LinkedNode(elm)
      this.head.next = prev
      this.length += 1
      return this.head
    } // <--- pos 가 length 보다 크다면, linkedNode 끝에 추가
    prev = this.head
    while (prev.next) {
      prev = prev.next
    }
    prev.next = new LinkedNode(elm)
    this.length += 1

    return prev.next
  }

  // 이 다음 부터는 로직들이 거의 동일하므로 생략한다.

  remove(elm: any) {
    let cur = null
    let prev = null
    if (!this.head) return '삭제할 값이 없습니다'
    cur = this.head
    while (cur.elm !== elm && cur.next) {
      prev = cur
      cur = cur.next
    }

    if (cur.elm !== elm) {
      return '삭제할 값이 없습니다.'
    }

    if (prev) {
      prev.next = cur.next
      this.length -= 1
      return cur
    }

    this.length -= 1
    this.head = cur.next
    return cur
  }

  removeAt(pos: number) {
    if (!this.head) return '삭제할 값이 없습니다'
    if (pos <= this.length) {
      let cur: INode | null = null
      let prev: INode | null = null
      let idx = 1
      cur = this.head
      while (idx !== pos && cur.next) {
        prev = cur
        cur = cur.next
        idx += 1
      }
      if (prev) {
        prev.next = cur.next
        this.length -= 1
        return cur
      }
      this.head = this.head.next
      this.length -= 1
      return cur
    }
    return '삭제할 값이 없습니다'
  }

  indexOf(elm: any): number {
    let idx = 1 // <-- idx 값은 1부터 시작하는것으로 한다
    if (!this.head) return -1

    let cur = this.head
    while (cur.elm !== elm && cur.next) {
      cur = cur.next
      idx += 1
    }
    return idx
  }

  size(): number {
    return this.length
  }

  isEmpty(): boolean {
    if (!this.head) return true
    return false
  }

  clear(): void {
    this.head = null
    this.length = 0
  }

  toString(): string {
    if (!this.head) return '값이 없습니다.'
    let cur = this.head
    let str = ''
    while (cur.next) {
      str += `${cur.elm} `
      cur = cur.next
    }
    return str + cur.elm
  }
}

const lk = new LinkedList()

console.log(lk.append(1)) // LinkedNode { elm: 1, next: null }
console.log(lk.append(2)) // LinkedNode { elm: 2, next: null }
console.log(lk.toString()) // 1 2
console.log(lk.insert(3, 1))
/*
LinkedNode {
  elm: 3,
  next: LinkedNode { elm: 1, next: LinkedNode { elm: 2, next: null } }
}
*/
console.log(lk.toString()) // 3 1 2
console.log(lk.remove(3))
/*
LinkedNode {
  elm: 3,
  next: LinkedNode { elm: 1, next: LinkedNode { elm: 2, next: null } }
}
*/
console.log(lk.toString()) // 1 2
console.log(lk.remove(5)) // 삭제할 값이 없습니다
console.log(lk.toString()) // 1 2
console.log(lk.removeAt(1)) // LinkedNode { elm: 1, next: LinkedNode { elm: 2, next: null } }
console.log(lk.toString()) // 2
console.log(lk.insert(3, 1))
console.log(lk.size()) // 2
console.log(lk.toString()) // 3 2
console.log(lk.indexOf(3)) // 1 <-- index 는 0 부터가 아닌 1부터 시작

이렇게 LinkedList 를 구현해 보았다. 중요한 포인트는 next 를 어떻게 연결하고 어떻게 끊느냐이다.

next 를 통해 Node 를 연결하는데, 따로 삭제처리하는 로직은 없다. 단순히 Node 의 연결을 다른 Node 로 연결만한다

이렇게 해도 괜찮을까 싶지만, 이렇게 해도 Javascript Garbage Collector 가 사용하지 않는 객체는 Memory 참조 에서 자동적으로 제거하므로, 따로 메모리 해제 처리를 해지 않아도 된다

실제로, C 에서는 위 로직에서 매번 Memory 해제 처리를 해주어야 한다. 안그러면, 사용하지 않는 객체가 쌓이면서, Memory 를 점유하는 형태가 이루어진다.

Javascript 는 이러한 불편함없이, 알아서 사용하지 않는 객체의 Memory 를 해제해주므로, 위와 같은 처리가 가능하다

LinkedList 에 대해서 알아보았으며, 다음은 Doubly Linked List 에 대해서 구현해도록 한다