기억의 실마리
2023. 6. 26. 20:48

1. 선택정렬(Selection Sort)

단계별로 정렬을 수행할때 가장 작은 원소를 선택해서 앞으로 보내는 정렬 방법이다.

앞으로 보내진 원소는 위치가 변경되지 않고 시간복잡도 O(N²)으로 비효율적인 알고리즘에 속한다.

 

동작방식

1. 단계별로 수행하면서 가장 작은 원소를 선택한다.

2. 처리되지 않은 원소중에 가장 앞의 원소와 교체한다.

 

  • 선택정렬 함수 예시
function selectionSort(arr) {
  for (let i = 0; i < arr.length; i++) {
    let minIndex = i;
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[minIndex] > arr[j]) {
        minIndex = j;
      }
    }
    // 스왑(swap)
    let temp = arr[i];
    arr[i] = arr[minIndex];
    arr[minIndex] = temp;
  }
}

 

 

2. 버블정렬(Bubble Sort)

인접한 두 원소를 체크하여 정렬이 안되어 있다면 위치를 서로 변경하는 정렬방식이다.

서로 인접한 두 원소를 비교하는 것이 마치 거품과 같다고 하여 붙여진 이름이다.

시간복잡도 O(N²)으로 비효율적인 정렬 알고리즘에 속한다.

 

동작방식

1. 단계별로 수행하면서 가장 큰 원소가 맨 뒤로 이동한다.

2. 다음단계에선 맨 뒤로 이동한 원소는 정렬에서 제외한다.

 

  • 버블정렬 함수 예시
function bubbleSort(arr) {
  for (let i = arr.length - 1; i > 0; i--) {
    for (let j = 0; j < i; j++) {
      if (arr[j] < arr[j + 1]) {
        let temp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = temp;
      }
    }
  }
}

 

 

3. 삽입정렬(Insertion Sort)

각 원소를 적절한 위치에 삽입하는 정렬이다.

 

동작방식

1. 단계별로 수행하면서 현재 원소가 삽입될 위치를 탐색한다.

2. 지정 위치에 도달할 때까지 반복적으로 왼쪽으로 이동한다.

 

  • 삽입정렬 함수 예시
function insertionSort(arr) {
  for (let i = 1; i < arr.length; i++) {
    for (let j = i; j > 0; j--) {
      // 시작인덱스부터 1까지 1씩 감소하며 반복
      if (arr[j] < arr[j - 1]) {
        let temp = arr[j];
        arr[j] = arr[j - 1]; // 한 칸씩 왼쪽으로 이동
        arr[j - 1] = temp; // 스왑(swap)
      } else {
        break; // 자기보다 작은 데이터를 만나면 그 위치에서 멈춤
      }
    }
  }
}

 

 

4. 병합정렬(Merge Sort)

병합정렬은 분할정복(divide and conquer) 알고리즘이다.

 

분할정복이란?

  • 분할(divide): 큰 문제를 작은 부분 문제(쉬운 문제)로 분할한다.
  • 정복(conquer): 작은 부분 문제를 각각해결하도록 한다.
  • 조합(combine): 해결한 작은 부분의 답을 이용하여 큰 문제를 해결해나간다.

일반적으로 재귀함수를 사용하여 구현한다.

큰 문제를 작은 문제로 분할하는 방식이 같은 경우가 많기 때문이다.

그렇기 때문에 쪼개질 수 없는 크기가 될 때까지 계속해서 분할한다.

 

장점: 최악의 경우에도 O(N log N)을 보장할 수 있다는 점에서 효율적이다.

단점: 전반적으로 정복과정에서 임시배열이 필요한 경우가 지배적이다.

 

동작방식

1. 단계별로 수행하면서 현재 원소가 삽입될 위치를 탐색한다.

2. 지정 위치에 도달할 때까지 반복적으로 왼쪽으로 이동한다.

 

  • 병합정렬 함수 예시
function merge(left, right) {
  let sorted = [];
  while (left.length && right.length) {
    if (left[0] <= right[0]) sorted.push(left.shift());
    else sorted.push(right.shift());
  }
  return [...sorted, ...left, ...right];
}

function mergeSort(arr) {
  if (arr.length === 1) return arr;
  let half = Math.ceil(arr.length / 2);
  let left = arr.slice(0, half);
  let right = arr.slice(half);
  return merge(mergeSort(left), mergeSort(right));
}

let arr = [7, 4, 3, 2, 1, 6, 5];
const sortedArray = mergeSort(arr);

console.log(arr); //[7, 4, 3, 2, 1, 6, 5]
console.log(sortedArray); //[1, 2, 3, 4, 5, 6, 7]

function merge: 이미정렬된 배열 left와 right를 하나의 함수로 합쳐주는 순수 함수

function mergeSort: 배열을 반으로 나누어 merge함수에게 left와 right로 전달한다.

 

 

마치며...

오늘 포스팅한 정렬알고리즘은 사실, 알고리즘을 따로 공부를하기전까진 단순하게 정렬하는거지! 라고 생각했지만 정말 다양하고 어떤방식으로 정렬을 하느냐에 따라 시간복잡도가 제곱이상의 차이가 나기도 한다는 것이 새롭게 다가왔던 것 같다. 앞으로도 코드의 퀄리티를 위해서 프로젝트에서 정렬할때 잘 생각해보고 적용시켜야겠다.

2023. 6. 26. 13:44

1. 배열(Array)

가장 기본적인 자료구조이며 여러개의 변수를 담는 공간으로 이해할 수 있다.

배열은 인덱스(Index)가 존재하고 인덱스는 0부터 시작한다.

특정한 인덱스에 직접적으로 접근이 가능하고 수행시간은 O(1)을 가진다.

 

장점: 캐시(cache) 히트 적중률이 높고 조회가 빠르다.

단점: 배열의 크기를 미리 지정해야 하는 것이 일반적이며 데이터의 추가 및 삭제에 한계가 있다.

 

캐시히트 적중률이란?

CPU(클라이언트)에서 요청한 데이터가 캐시에 존재하여 이를 이행할 수 있을때 캐시 히트(Hit)라고 한다. 

Memory address 0x0000 0x0004 0x0008
Index 0 1 2
Value 10 30 70

 

2. 연결리스트(Linked List)

연결 리스트는 메인 메모리상에서 주소가 연속적이지 않으며 배열과 다르게

크기가 정해져있지 않고 동적으로 변경이 가능한 것이 특징이다.

 

장점: 포인터가 다음 데이터의 위치를 가지고 있기 때문에 삽입과 삭제가 간편하다.

단점: 특정 인덱스의 원소를 검색할때 앞에서부터 원소를 찾기 때문에 검색속도가 느리다.

3. 스택(Stack)

먼저 들어온 데이터가 나중에 추출되는 자료구조이며 박스가 쌓인 형태를 스택이라고 볼 수 있다.

새로운 원소를 삽입할때는 마지막위치에 삽입하고 삭제할때 마찬가지로 마지막 위치에서 삭제된다.

 

 

4. 큐(Queue)

큐는 먼저 삽입된 데이터가 먼저 추출되는 자료구조이다.

예시로 게임 대기 큐의 경우 먼저 대기한 사람이 먼저 게임에 매칭되는 것과 동일하다.

Javascript Queue

Javascript에서는 큐 라이브러리를 기본적으로 제공하고있지 않기 때문에

간단하게 Queue 라이브러리를 직접 작성하여 사용할 수 있다.

 

필자 역시 Javascript를 주로 사용하기 때문에 Queue라이브러리를 만들어 두었다.

class Queue {
  constructor() {
    this.items = {};
    this.headId = null;
    this.tailId = null;
    this.length = 0;
  }
  size() { return this.length; }
  isEmpty() { return this.length === 0; }
  enq(item) {
    if (!this.headId) this.headId = 0;
    if (this.headId == 0) {
      this.items[this.length] = item;
      this.length++;
      this.tailId = this.length - 1;
    }
    else  {
      this.tailId++;
      this.items[this.tailId] = item;
      this.length++;
    }
  }
  deq() {
    if (this.isEmpty()) throw new Error('Queue is empty');
    if (this.length == 0) return
    this.length--;
    const item = this.items[this.headId];
    delete this.items[this.headId];
    this.headId++;
    if (this.length == 1) this.tailId = this.headId;
    if (this.length == 0) {
      this.headId = null;
      this.tailId = null;
    }
    return item;
  }
  head() {
    if (this.isEmpty()) throw new Error('Queue is empty');
    return this.items[this.headId];
  }
  tail() {
    if (this.isEmpty()) throw new Error('Queue is empty');
    return this.items[this.tailId - 1];
  }
}

혹시나 블로그를 방문하시는분들께서 javasciprt Queue가 필요하셨다면 사용하셔도 무방합니다.

 

5. 트리(Tree)

트리는 계층적인 구조를 표현할 때 사용할 수 있는 자료구조다.

나무를 뒤집은 것 같은 형태이다.

  • 루트 노드(root node): 부모가 없는 최상위 노드
  • 단말 노드(leaf node): 자식이 없는 노드

서로 같은 부모를 가지는 노드간의 관계를 형제관계라고 하며 루트노드에서의 길이를

깊이(depth)라고 한다. 이는 즉 출발노드에서 목적지 노드까지 거쳐야 하는 간선의 수를 의미한다.

트리의 높이(height)는 루트 노드에서 가장 깊은 노드까지의 길이를 의미한다.

 

트리의 종류

  • 이진 트리(Binary Tree): 최대 2개의 자식을 가질 수 있는 구조의 트리를 말한다.

 

  • 포화 이진 트리(Full Binary Tree): 리프노드를 제외한 모든 노드가 두 자식을 가지고 있는 트리다.

 

  • 완전 이진 트리(Complete Binary Tree): 모든 노드가 왼쪽 자식부터 차례로 채워진 트리다.

 

  • 높이 균형 트리(Height Balanced Tree): 왼쪽과 오른쪽 자식 트리의 높이가 1이상 차이가 나지 않는 트리다.

 

6. 힙(Heap)

힙은 원소중 최댓값 또는 최솟값을 빠르게 찾아내는 자료구조이다.

원소 삽입과 삭제할때 O(logN)의 수행시간을 요구하며 N개의 데이터를 힙에 넣었다

모두 꺼내는 작업은 정렬과 동일하여 수행시간 O(N log N)을 요구한다.

 

힙은 완전 이진트리 자료구조를 따르며 우선순위가 가장 높은 노드가 루트에 위치한다.

 

1. 최대 힙(max heap)

  • 부모 노드의 키 값이 자식 노드의 키 값보다 항상 크다.
  • 루트 노드가 가장 크며, 값이 큰 데이터가 우선순위를 가진다.

2. 최소 힙(min heap)

  • 부모 노드의 키 값이 자식 노드의 키 값보다 항상 작다.
  • 루트 노드가 가장 작으며, 값이 작은 데이터가 우선순위를 가진다.

 

7. 우선순위 큐(Priority Queue)

우선순위에 따라서 데이터를 추출할 수 있는 자료구조다.

우선순위 큐는 일반적으로 힙(heap)을 이용해 구현하며 배열 자료형으로도 구현가능하다.

우선선순위 큐 구현방식 삽입 시간 삭제 시간
배열 자료형 O(1) O(N)
힙(Heap) O(logN) O(logN)

일반적으로 우선순위 큐는 이진트리 구조를 사용한다.

 

javascript는 기본적으로 우선순위 큐 라이브러리를 제공하지 않기 때문에 필요한 경우

별도의 라이브러리를 사용해야 한다.

 

참고할 수 있는 깃허브: https://github.com/ndb796/priorityqueuejs

 

8. 그래프(Graph)

그래프는 특정 노드의 정점과 노드와 노드의 간선을 나타내기 위한 자료구조이다.

그래프는 두가지 방식으로 구현할 수 있다.

 

  • 인접행렬: 2차원 배열을 사용하는 방식

각 인덱스에 각 노드가 자리해 있으며 위 이미지의 표를 배열로 표현한다면 아래와 같다.

[0, 3, 10]

[3, 0, infinity]

[10, infinity, 0]

 

각 행은 각 노드의 인덱스번호부터 출발했을때 얻을 수 있는 비용이다.

즉 0, 1, 2 의 인덱스를 가지고 있기 때문에 행에 3의 길이여야 하고

각 인덱스에서 출발한다고 가정하면 1행에서 인덱스 0번자리는 자기 자신이기 때문에

비용이 발생하지 않아 0이다. 다음 인덱스 1번자리는  0번노드 -> 1번노드로 이동하는

비용이 3이다. 그리고 0번노드 -> 2번노드는 비용이 10이기 때문에 결과적으로

1번째 행은 [0. 3, 10]이 되는 것이다. 나머지 값들도 모두 동일하게 적용되며

간선이 없어 갈 수 없는 경우는 무한(infinity)으로 표기한다.

 

  • 인접리스트: 연결 리스트를 이용하는 방식

인접 리스트의 경우 노드의 각 인덱스를 각 열로 나열하고 연결가능한 노드와 간선비용을 저장한다.

0번노드의 경우 1번노드와 2번노드를 연결 할 수 있고 1번노드와 연결하면 비용은 3,

2번노드와 연결하면 비용은 10이기 때문에 최상단 행이(index 0번 노드) [1, 3], [2, 10]를 가지게 된다.

나머지도 마찬가지로 연결이 불가능한 경우는 모두 제외하고 연결이 되는 경우만

연결되는 노드번호와 간선의 비용만 저장한다.

 

그래프의 시간 복잡도

  • 인접행렬: 모든 정점들의 연결 여부를 저장해 O(V²)의 공간을 요구한다.
    공간효율성이 좋지 않지만 두 노드의 연결 여부를 O(1)의 시간만을 요구한다.

  • 인접리스트: 연결된 간선의 정보만을 저장하여 O(V + E)의 공간을 요구한다.
    공간효율성이 좋은편이지만 두 노드의 연결여부를 확인하는데 O(V)의 시간이 요구된다.

 

마치며...

지난 알고리즘 학습을 복습하면서 포스팅을 진행하고 있는데 초기에 학습했던 내용들이 분명 학습당시엔 명료했던 이해도가 시간이 지나니 흐리게 남아있는 경우가 많은 것 같다. 정말 기록하는 것은 중요하고 어떠한 키워드를 가지고 생각하며 기록해야하는지를 다시한번 하게 되었다.

2023. 6. 25. 16:46

알고리즘 이란?

어떠한 문제를 해결하기 위한 절차나 방법을 뜻한다.

공식화한 형태로 표현한 것을 의미하며 유한성을 가지고 언젠가는 끝나야 하는 속성을 가지고 있다.

 

알고리즘의 조건

알고리즘은 아래 5가지 조건에 만족해야 한다.

  • 입력: 외부에서 제공되는 자료가 0개 이상 존재해야 한다.
  • 출력: 적어도 2개 이상의 서로 다른 결과를 도출해내야 한다.
  • 명확성: 수행과정은 무엇을 하기위한 것인지 정의가 명확해야 한다.
  • 유한성: 알고리즘의 명령어대로 수행하여 처리가 되었을때 종료되어야 한다.
  • 효율성: 모든 과정이 명백히 실행가능해야 하며 시간적, 공간적 효율성을 가져야 한다.

 

1. 선형 자료구조

하나의 데이터 뒤에 다른 데이터가 하나 존재하는 자료구조이다.
데이터가 일렬로 연속적으로(순차적) 연결되어있다.
배열, 연결리스트, 스택, 큐, 덱 등이 있다.

 

2. 비선형 자료구조

하나의 데이터뒤에 다른 데이터가 여러개 올 수 있는 자료구조이다.
데이터가 일직선상으로 연결되어있지 않아도 되며 트리, 그래프 같은 것이 있다.

 

자료구조와 알고리즘의 관계

효율적인 자료구조 설계를 위해 알고리즘 지식이 요구된다.
효율적인 알고리즘을 작성하기 위해서는 문제상황에 맞는 적절한 자료구조가 사용되어야 하며
프로그램을 작성할 때 자료구조와 알고리즘 모두 고려해야 한다.

 

프로그램의 성능 측정 방법

  • 시간복잡도(time complexity): 알고리즘에 사용되는 연산 횟수를 측정한다.
  • 공간복잡도(space complexity): 알고리즘에 사용되는 메모리의 양을 측정한다.

주로 공간을 많이 사용하는 대신 시간을 단축하는 방법을 선택한다.

 

Big-O 표기법

복잡도를 표현할 때는 Big-O 표기법을 사용한다.

특정한 알고리즘이 얼마나 효율적인지 수치적으로 표현할 수 있으며 가장 빠르게 증가하는

항만을 고려하는 표기법이다.

 

  • 다음 알고리즘은 O(n)의 시간 복잡도를 가진다.
let n =10;
let sum = 0;
for (let i = 0; i < n; i++) {
  sum += i;
}
console.log(sum); // 45

 

  • 다음의 알고리즘은 O(n²)의 시간 복잡도를 가진다.
let n = 9;

for(let i = 1; i <= n; i++) {
  for(let j = 1; j <= n; j++) {
    console.log(`${i} X ${j} = ${i * j}`);
  }
}
/*
실행결과:
1 X 1 = 1
1 X 2 = 2
...
9X 8 = 72
9 X 9 = 81
*/

 

연산횟수에 대한 소요시간

일반적으로 연산횟수가 10억을 넘어가면 1초이상의 시간이 소요된다.
[예시] n = 1000

  • O(log n) : 약 10번 연산
  • O(n) : 약 1,000번 연산
  • O(n log n) : 약 10,000번 연산
  • O(n²) : 약 1,000,000번 연산
  • O(n³) : 약 1,000,000,000번 연산

Big-O표기법으로 시간 복잡도를 표기할 때는 가장 큰 항만을 표시한다.
가장 큰 항에 붙어 있는 계수는 제거한다.
O(3n² + n) = O(n²)

 

 

Big-O 표기법을 사용한 시간복잡도의 예시

  • O(1) : 스택에서 Push, Pop
  • O(log n) : 이진트리
  • O(n) : for 문
  • O(n log n) : 퀵 정렬, 병합정렬, 힙 정렬
  • O(n²): 이중 for 문, 삽입정렬, 거품정렬, 선택정렬
  • O(2ⁿ) : 피보나치 수열

 

마치며...

마무리 글에 갑자기 알고리즘에 대해서 공부를 하게 된 이유에 대해 적을까 한다. 메모서비스 개발을 진행하고 있다가 마무리되어 갈 즈음에 문득 스스로 문제해결능력에 대한 불만와 방식에 대해서 의구심을 품게 되었다. "과연 지금까지의 방식이 효율적이고 가장 빠른 길일까?" 그러다 알고리즘에 대해서 문득 떠올리게 되었고 하나씩 알아보게 되었는데 지금까지 나에게 느꼈던 부족함을 해결해 줄 수 있을 것이라 생각이 들었다. 그렇게 개발에 1개월의 공백을 두게 되었지만 다양한 알고리즘을 학습하고 코드를 직접 만들어보기도 하면서 지금까지 나에게 부족했던 부분들이 많이 채워지는 것을 느꼈다. 문제를 해결하기 위해 사고하는 방식이 달라졌고 시간복잡도에 대한 개념이 생기니 효율적으로 문제해결에 다가설 수 있게 된 것 같았다. 완벽하다고 할 수 없겠지만 나에게 있어 개발자로서 필요한 역량을 향상시키기 위한 없어선 안될 1개월이었던 것 같다.