👨‍💻 TIL

TIL3 - 2023.09.22

zunwon 2023. 9. 23. 12:55

자료구조 & 알고리즘

자료구조 + 알고리즘 = 프로그램

 

자료구조

  • 메모리를 효율적으로 사용하며 데이터를 빠르고 안정적으로 처리하는 것이 궁극적인 목표로 상황에 따라 유용하게 사용될 수 있도록 특정 구조를 이루고 있다
  • 상황에 따라 올바른 자료구조를 선택하는 능력이 필요하다
  • 일차원인 컴퓨터 메모리를 현실에 대응되도록 구조를 만든 것이라 할 수 있음

 

알고리즘

  • 특정 문제를 메모리 효율적이고 빠른 성능으로 해결하는 것이 궁극적인 목표로 정해진 일련의 절차나 방법을 공식화한 형태로 표현한 것을 말한다.

자료구조와 알고리즘이 중요한 이유

  • 기초 코딩 능력을 기르기 위해 => 자료구조와 알고리즘을 공부하면 능력 UP

 

문제 해결 능력

  • 논리적 사고 : 어떠한 현상이 존재할 때 추론하고 구조화하여 해답을 내는 것
  • 전산화 능력 : 현실에 있는것을 컴퓨터세계 즉 소프트웨어로 구현하는 것, 논리를 조합하여 컴퓨터 사고가 가능한지
  • 엣지 케이스 탐색 : 예외 상황을 얼마나 잘 찾는지

자료구조의 종류 

단순 구조

  • 정수, 실수, 문자열, 논리

선형 구조

  • 한 원소 뒤에 하나의 원소 만이 존재하는 형태로 자료들이 선형으로 나열되어 있는 구조
  • 배열, 연결 리스트, 스택, 큐

비선형 구조

  • 원소 간 다대다 관계를 가지는 구조로 계층적인 구조나 망형 구조를 표현하기에 적절
  • 컴퓨터의 폴더 구조, 인간 관계를 나타내기 적합함
  • 트리, 그래프

 

완벽한 자료구조는 없다 !!! 우리는 상황에 맞게 적절한 자료구조를 선택하자


시간 복잡도

시간 복잡도

  • 프로그램의 성능을 알기 위해 고려 할것
  • 입력 크기, 하드웨어 성능, 운영체제 성능, 컴파일러 최적화, 비동기 로직
  • 따라서 프로그램의 성능을 정확히 파악하는 것을 불가능에 가깝다

 

빅오 표기법(Big-O notation)

  • 빅오 표기법은 접근적 표기법을 따른다
  • 접근적 표기법 : 함수의 증감 추세를 비교하는 방법

 

빅오 표기법의 4가지 법칙

  • 계수 법칙 : n이 무한에 가까울 수록 k의 크기는 의미가 없기 때문에 빅오 표기법에서는 생략하여 표기한다
// 두 루프는 같은 O(n)으로 표기된다.
for(let i = 0; i < n; i += 1){
    // ....
}

for(let i = 0; i < n * 5; i += 1){
    // ....
}
  • 합의 법칙 : 빅오끼리는 더해질 수 있다
// 두 루프를 합쳐 O(n + m)으로 표기할 수 있다.
// 계수 법칙에 의해 5는 사라진다
for(let i = 0; i < n; i += 1){
    // ....
}

for(let i = 0; i < m * 5; i += 1){
    // ....
}
  • 곱의 법칙 : 빅오끼리는 곱해질 수 있다
// 두 루프를 합쳐 O(n*2)으로 표기할 수 있다.
// 계수 법칙에 의해 5는 사라진다
for(let i = 0; i < n; i += 1){
    for(let j = 0; j < n * 5; j += 1){
        // ....
    }
}
  • 다항 법칙 : 다항식일 때 표기하는 법
// 다음 루프는 O(n^3)으로 표기할 수 있다.
for(let i = 0; i < n * n * n; i += 1){
    // ....
}

2가지만 기억하기

상수항은 무시

가장 큰 항 외엔 무시

 

성능 측정 방법

  • Date 객체를 이용
const start = new Date().getTime();
// ...
const end = new Date().getTime();
console.log(end - start);

배열, 순차 리스트(Array)

배열

  • 연관된 데이터를 연속적인 형태로 구성된 구조
  • 배열에 포함된 원소는 순서대로 번호(index)가 붙는다

 

배열의 특징

  • 고정된 크기를 가지며 일반적으로 동적으로 크기를 늘릴 수 없지만 자바스크립트처럼 대부분의 스크립트 언어는 동적으로 크기가 증감된다
  • 원하는 원소의 index를 알고 있다면 O(1)로 원소를 찾을 수 있다
  • 원소를 삭제하면 해당 index는 당겨지지 않고 빈자리가 된다

배열 요소 삭제 / 추가

삭제 후 순서를 맞추려면 O(n)이 소요된다

중간에 요소를 추가하고 싶다면 O(n)이 소요된다

 

따라서 추가삭제가 반복되는 로직이라면 배열 사용을 권장하지 않는다!


연결리스트(Linked List)

추가와 삭제가 반복되는 로직이라면 어떻게 해야할까??

 

연결 리스트

  • 각 요소를 포인터로 연결하여 관리하는 선형 자료구조
  • 각 요소는 노드라고 부르며 데이터 영역과 포인터 영역으로 구성된다

 

연결 리스트의 특징

  • 메모리가 허용하는 한 요소를 제한없이 동적으로 추가할 수 있다
  • 탐색에 O(n)이 소요
  • 요소를 추가하거나 제거할 때는 O(1)이 소요
  • Singly Linked List, Doubly Linked List, Circular Linked List가 존재

배열과 연결 리스트의 차이

  • 배열 : 순차적인 데이터가 들어가서 메모리의 영역을 연속적으로 사용
  • 연결리스트 : 순차적이지 않기 때문에 각 데이터가 퍼져 있어 포인터를 사용하여 각 영역을 참조

 


Singly Linked List (단일 연결 리스트)

  • Head → Tail까지 단방향으로 이어지는 연결 리스트
  • remove시 자동으로 가비지컬렉터에 의해서 사라진다

데이터 찾기

  • Head 포인터를 참고하여 Head를 찾고 맞는지 확인 후 다르면 다음 요소로 넘어감
  • O(n) 선형 시간이 소요된다.

데이터 추가

  • 6과 2 사이에 21을 추가했을 때, 아래와 같이 진행된다
  • 6의 포인터 영역은 새롭게 만든 21을 가리키고, 21은 2를 가리키게 된다
  • O(1) 상수 시간만 소요된다

데이터 삭제

  • 2를 삭제했을 때, 아래 사진과 같이 진행된다
  • 21의 포인터 영역을 2의 포인터 영역인 8로 이어준 후, 2를 삭제한다
  • O(1) 상수 시간만 소요된다

Singly Linked List 코드 (예외처리 + 'size' 메소드 추가)

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

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

  find(value) {
    let currentNode = this.head;
    
    while (currentNode !== null && currentNode.value != value) {
      currentNode = currentNode.next;
    }
    
    if (currentNode === null) {
      console.log(`FindError...Can't find the value : ${value}`);
    }
    return currentNode;
  }
  
  append(value) {
    const newNode = new Node(value);
    
    if (this.head === null) {
      this.head = newNode;
      this.tail = newNode;
    } else {
      this.tail.next = newNode;
      this.tail = newNode;
    }
    this.size++;
  }

  insert(node, value) {
    if (node === null) {
      return;
    }
    
    const newNode = new Node(value);
    newNode.next = node.next;
    node.next = newNode;
    this.size++;
  }

  remove(value) {
    let prevNode = this.head;
    
    while (prevNode.next.value !== value) {
      prevNode = prevNode.next;
    }
    
    if (prevNode.next !== null) {
      prevNode.next = prevNode.next.next;
      this.size--;
    } else {
      console.log(`RemoveError...Can't find the value : ${value}`);
    }
  }

  display() {
    let currentNode = this.head;
    let displayString = "[";

    while (currentNode !== null) {
      displayString += `${currentNode.value}, `;
      currentNode = currentNode.next;
    }

    displayString = displayString.substr(0, displayString.length - 2);
    displayString += "]";
    console.log(displayString);
  }

  getSize() {
    console.log(this.size);
  }
}

const linkedList = new SinglyLinkedList();
linkedList.append(1);
linkedList.append(2);
linkedList.append(4);
linkedList.append(7);
linkedList.display();
linkedList.insert(linkedList.find(4), 21);
linkedList.display();
linkedList.remove(2);
linkedList.display();

Doubly Linked List (이중 연결 리스트)

  • 양방향으로 이어지는 연결 리스트
  • Singly Linked List보다 자료구조의 크기가 조금 더 크다 (이전 노드를 카르키는 메모리가 있어야 하기 때문)

데이터 추가

31을 6과 2 사이에 추가해보기

  1. 우선 새롭게 만든 31의 다음 노드를 6의 다음 노드인 2로 연결한다
  2. 2의 이전 노드는 31을 가리키도록 한다
  3. 6의 다음 노드를 31로 연결한다
  4. 31의 이전 노드를 6으로 연결한다

데이터 삭제

 

6을 지웠을 경우다

  1. 31의 이전 노드를 6의 이전 노드인 1을 가리키도록 한다
  2. 1의 다음 노드를 31로 바꾼다
  3. 6을 삭제한다

Doubly Linked List 코드

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

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

  find(value) {
    let currentNode = this.head;

    while (currentNode !== null && currentNode.value != value) {
      currentNode = currentNode.next;
    }

    if (currentNode === null) {
      console.log(`FindError...Can't find the value : ${value}`);
    }

    return currentNode;
  }

  append(value) {
    const newNode = new Node(value);

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

  insert(node, value) {
    if (node === null) {
      return;
    }

    const newNode = new Node(value);

    node.next.prev = newNode;
    newNode.prev = node;
    newNode.next = node.next;
    node.next = newNode;
    this.size++;
  }

  remove(value) {
    let prevNode = this.head;

    while (prevNode.next !== null && prevNode.next.value !== value) {
      prevNode = prevNode.next;
    }

    if (prevNode.next !== null) {
      prevNode.next = prevNode.next.next;
      this.size--;
    } else {
      console.log(`RmoveError...Can't find the value : ${value}`);
    }
  }

  display() {
    let currentNode = this.head;
    let displayString = "[";

    while (currentNode !== null) {
      displayString += `${currentNode.value}, `;
      currentNode = currentNode.next;
    }

    displayString = displayString.substr(0, displayString.length - 2);
    displayString += "]";
    console.log(displayString);
  }

  getSize() {
    console.log(this.size);
  }
}

const dll = new DoublyLinkedList();
dll.append(1);
dll.append(6);
dll.append(2);
dll.append(8);
dll.display();
dll.insert(dll.find(6), 31);
dll.display();
dll.remove(6);
dll.display();

Circular Linked List (환형 연결 리스트)

  • Singly 혹은 Doubly Linked List에서 Tail이 Head로 연결되는 연결 리스트
  • 메모리를 아껴쓸 수 있다
  • 원형 큐 등을 만들 때도 사용된다

Circular Linked List 코드


스택

  • Last In First Out이라는 개념을 가진 선형 자료구조
  • 바닥이 막힌 상자를 생각하면 편함