삽입 정렬(insertion sort), 자바스크립트로 작성하기

앞에서부터 순서대로 정렬하여 자신의 위치에 삽입하는 방법

삽입 정렬은 배열의 앞에서부터 시작하여 자신의 앞에 있는 모든 요소와 크기 비교를 통해 자신에게 맞는 위치에 요소를 삽입한다.

다음 그림과 같은 방식이다.

3을 [6]의 적절한 위치에 삽입하고 다음으로는 5를 [3, 6]의 적절한 위치에 삽입한다. 다음으로는 1을 [3, 5, 6]의 적절한 위치에 삽입하고 이 방식으로 삽입 작업이 반복된다.

코드는 다음과 같다.

const insertionSort = (arr) => {

  for(let i=1;i<arr.length;i++){
    const checkValue = arr[i];
    let left = i-1;

    while(left>=0&&arr[left]>checkValue){
      arr[left+1] = arr[left];
      left--;
    }
    arr[left+1] = checkValue;
  }
  return arr;
}

console.log(insertionSort([6,3,5,1,9,4,8,2,7]));

다른 메모리 공간이 필요하지 않고 안정 정렬(stable sort)이며 시간 복잡도는 O(N)이다. 단점으로 최악의 시간 복잡도는 O(N^2)가 될 수 있으며 배열이 커질수록 비효율적이다.

퀵 정렬(quick sort), 자바스크립트로 구현하기

문제를 세분화하는 분할 정복(divide and conquer)과 퀵 정렬

퀵 정렬은 하나의 중심(pivot) 값을 기준으로 큰 값과 작은 값을 분류하고 이 작업을 반복하여 정렬하는 방법이다.

그림으로 보면 다음과 같다.

첫 요소인 6을 기준으로 잡아서 이보다 작은 수를 left, 큰 수를 right로 분류하고 이 작업을 세분화해서 반복 진행하면 최종적으로 정렬된 배열을 반환할 수 있다.

const quickSort = (arr) => {
  if(arr.length<=1){
    return arr;
  }

  const pivot = arr[0];
  const left = [];
  const right = [];

  for(let i=1; i<arr.length; i++){
    if(arr[i]<pivot){
      left.push(arr[i]);
    }else{
      right.push(arr[i]);
    }
  }
  return [...quick(left), pivot, ...quick(right)]
}

console.log(quickSort([6,3,5,1,9,4,8,2,7]));

퀵 정렬은 최악의 경우를 제외하면 O(NlogN)의 시간 복잡도를 갖는 속도가 장점이지만 단점은 최악의 경우(이미 정렬된 배열) 시간 복잡도가 O(N^2)로 대폭 증가하고 불안정 정렬(unstable sort, 동일한 값에 대해서는 순서가 바뀔 수 있음)이라는 점이다.

이진 탐색(binary search), 자바스크립트로 구현하기

이진 탐색과 구현방법

이진 탐색(binary search)은 데이터 집합에서 원하는 데이터를 찾을 때까지 집합을 이분(二分)하여 탐색하는 방법입니다.

데이터 집합을 둘로 나누고 찾는 데이터가 있는 집합을 선택하여 다시 반으로 나누고 다시 데이터가 있는 집합에 같은 과정을 계속 반복합니다.

따라서 아무리 큰 데이터라도 몇 번의 연산만으로 원하는 데이터를 찾을 수 있습니다.

하지만 이진 탐색은 조건이 있는데요.

데이터가 반드시 순서대로 정렬된 상태여야 합니다.

그림을 통해 탐색의 과정을 확인해 보겠습니다.

위와 같은 데이터 집합에서 2를 찾으려면 먼저 집합을 반으로 나누고 찾는 데이터가 속한 집합을 선택합니다.

그럼 첫 번째로 선택한 집합은 다음과 같습니다.

또 반을 나누고 2가 속한 집합만을 선택합니다.

여기서 반을 나누고 2가 속한 집합을 선택하면 다음과 같습니다.

이제 둘 중 하나를 확인하여 원하는 데이터를 선택하면 됩니다.

이진 탐색 코드는 다음과 같습니다.

const binarySearch=(target, data)=>{

  let low = 0;
  let high = data.length-1;
  
  while(low<=high){
    
    let mid = Math.floor((low+high)/2);    
  
    if(target===data[mid]){
       return mid;
    }else if(target>data[mid]){
      low = mid+1 
    }else if(target<data[mid]){
      high = mid-1
    }
  }

  return undefined;
}

이 탐색 방법은 데이터의 양이 많아지면 엄청난 효율을 자랑합니다.

데이터가 10000까지 있을 때 8000을 찾기 위해서 무차별 대입은 7999번의 연산을 진행해야 하지만 이진 탐색 방법은 13번의 연산이면 원하는 데이터를 찾을 수 있습니다.

자바스크립트를 이용한 BFS, DFS 구현하기(javascript)

데이터는 선형 구조(배열, 연결리스트, 스택, 큐) 또는 비선형 구조(트리, 그래프)로 이루어져 있으며, 순차적으로 나열된 선형 구조에 비해 비선형 구조의 데이터는 탐색이 어렵습니다.

하지만 비선형 구조의 대표적인 탐색 방법인 BFS, DFS를 사용하면 깔끔하게 탐색이 가능합니다.

두 방법 모두 무차별 탐색(Brute Force Search, 모든 데이터를 하나씩 탐색) 방법을 사용합니다.


DFS는 깊이 우선 탐색 방법으로 트리 구조의 데이터에서 노드마다 가장 깊이까지 탐색한 뒤 다음 노드로 이동하는 방법입니다.

위와 같은 트리 구조의 데이터가 있을 때, DFS는 한번 선택한 길은 끝까지 가본 뒤 다음 길을 탐색하는 방식과 같습니다.

그림으로 나타내면 다음과 같습니다.

검색 속도는 BFS에 비해서 느리지만 조금 더 간단합니다.

경로의 특징이 필요한 문제를 풀 때 DFS를 사용합니다.

const graph = {
  A: ["B", "C"],
  B: ["A", "D"],
  C: ["A", "E"],
  D: ["B", "F"],
  E: ["C","G"],
  F: ["D","H","I"],
  G: ["E","J","K"],
  H: ["F","L"],
  I: ["F", "M"],
  J: ["G","N"],
  K: ["G","O"],
  L: ["H"],
  M: ["I","P"],
  N: ["J"],
  O: ["K"],
  P: ["M"]
};

const bfs = (graph, start) => {

    const checked = [];    // 탐색 완료 데이터
    const willCheck = [];  // 탐색 예정 데이터
    
    willCheck.push(start);
    
    while(willCheck.length!==0){
      const node = willCheck.pop();  // 스택(Last In First Out)
      if(!checked.includes(node)){
       	 checked.push(node);
         //reverse() 제거 시 그림의 4,3,2,1 순서로 탐색     
      	 willCheck.push(...graph[node].reverse());  
        
      }
   }
	return checked;
}

console.log(bfs(graph, "A"));
// ['A', 'B', 'D', 'F', 'H', 'L', 'I', 'M', 'P', 'C', 'E', 'G', 'J', 'N', 'K', 'O']

BFS는 너비 우선 탐색 방법으로 트리 구조 데이터에서 노드의 인접 데이터를 모두 탐색한 뒤 다음 데이터로 이동하는 방법입니다.

그림으로 나타내면 다음과 같습니다.

탐색 속도는 DFS보다 빠르며 최단 거리를 구하는 문제에서 사용할 수 있습니다.

const graph = {
  A: ["B", "C"],
  B: ["A", "D"],
  C: ["A", "E"],
  D: ["B", "F"],
  E: ["C","G"],
  F: ["D","H","I"],
  G: ["E","J","K"],
  H: ["F","L"],
  I: ["F", "M"],
  J: ["G","N"],
  K: ["G","O"],
  L: ["H"],
  M: ["I","P"],
  N: ["J"],
  O: ["K"],
  P: ["M"]
};

const bfs = (graph, start) => {

    const checked = [];
    const willCheck = [];
    
    willCheck.push(start);
    
    while(willCheck.length!==0){
      const node = willCheck.shift(); // 큐(First In First Out)
      if(!checked.includes(node)){
       	 checked.push(node);
      	 willCheck.push(...graph[node]);       
      }
   }
	return checked;
}

console.log(bfs(graph, "A"));
// ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P']

빅오 표기법(Big O notation)을 알아보자

알고리즘의 효율성을 나타내는 빅오 표기법

빅오(Big O) 표기법은 점근적 표기법(Asymptotic Notation)의 하나로 Landau symbol이라고 부르기도 하며, 알고리즘의 효율성(시간 복잡도)를 나타낼 때 사용합니다.

그렇다면 점근적 표기법이란 무엇일까?

점근적 표기법은 중요하지 않은 상수와 계수를 제거하여 알고리즘의 복잡도를 단순화하여 나타낼 때 사용하는 표기법입니다.

점근적 표기법은 빅 세타(Big-θ), 빅 오(Big-O), 빅 오메가(Big-Ω)가 있으며 대략적인 의미는 다음과 같습니다.

빅 오(Big-O)점근적 상한에 대한 표기ex) O(n²)
빅 세타(Big-θ)상한과 하한의 평균에 대한 표기ex) θ(n²)
빅 오메가(Big-Ω)점근적 하한에 대한 표기ex) Ω(n²)

빅 세타와 빅 오메가는 원하는 효율성을 정확하게 나타내지 못하는 관계로 빅 오 표기법이 주로 사용됩니다.

알고리즘의 대략적인 실행 시간만 알면 되므로 빅 오 표기법은 차수가 가장 높은 항만 사용해 수행 시간의 상한(최악의 경우)을 나타냅니다.

예를 들어 f(n)=3n²+2n+1일 때, 빅 오 표기법을 사용하면 O(n²)로 나타낼 수 있습니다.

입력값이 작을 때는 문제가 없지만 입력값이 무한대에 가까워지는 경우에는 기하 급수적으로 복잡도가 증가하며 주요 증가율은 다음과 같습니다.

시간 복잡도n이 2배가 될 경우
O(1)변함 X
O(log n)약간 증가
O(n)2배 증가
O(n log n)2배보다 약간 증가
O(n2)4배 증가
O(n3)8배 증가
O(nk)2k배 증가
O(kn)kn배 증가
O(n!)nn배 증가

증가율을 그래프로 보면 다음과 같습니다.

그래프 기울기가 완만할수록 효율이 좋은 것을 나타내며 기울기가 가파를수록 효율성이 나쁜 것을 의미합니다.

출처 : https://en.wikipedia.org/wiki/Big_O_notation

위 그래프를 통해 상수->로그->선형->다항->지수->팩토리얼의 순으로 효율이 떨어지고 있는 것을 알 수 있습니다.

피보나치 수열, 자바스크립트 재귀로 함수 만들기(feat.메모이제이션)

메모이제이션을 사용한 피보나치 수열의 재귀 함수
피보나치(Fibonacci) 수는 이탈리아 수학자인 레오나르도 보나치(Leonardo Bonacci)에 의해 연구되어 알려진 것으로, 토끼가 새끼를 낳아 매달 증가하는 토끼의 수에 관해 계산하는 것으로 유명합니다.

0부터 시작하는 수를 순서대로 나열하면,

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181…….와 같이 수가 증가하는데요.

바로 앞(n-1)의 수와 그 바로 앞(n-2)의 수를 더하면 현재의 수(n)가 됩니다.

다음 그림은 네모칸 채우기로 한 면의 길이가 피보나치 수와 같이 증가를 보이는 것을 알 수 있습니다.

출처:https://en.wikipedia.org/wiki/Fibonacci_number

사각형이 1/4씩 회전하며 그리는 나선형은 황금 비율(1.618)에 수렴하여 과학적이며 예술적인 비율을 만들어 낸다고 합니다.

https://en.wikipedia.org/wiki/Fibonacci_number

그럼 이 피보나치 수열을 재귀를 사용해 자바스크립트 함수로 만들어 보겠습니다.

const fibonacci = (n) => {

  if ( n <= 1 ) {
    return n;
  }
  
  return fibonacci(n-1) + fibonacci(n-2);
}

위 함수는 피보나치 수열을 재귀적으로 수행하지만 계산 양이 늘어날 때마다 시간 복잡도는 가파르게 증가합니다.

이는 계산의 중복 문제 때문인데요.

예를 들어 fibonacci(10)을 실행하면 fibonacci(9), fibonacci(8), fibonacci(7)……. 순서로 실행을 하게 되는데요.

fibonacci(9)는 다시 fibonacci(8), fibonacci(7), fibonacci(6)…. 을 실행하게 됩니다.

결국 이미 한 계산을 하고 또 하고 또 하고… 합니다.

하지만 이미 수행한 계산 결과를 배열로 저장해두고 거기서 값을 꺼내쓰면 계산의 중복은 발생하지 않으므로 효율적이고 아름다운 함수가 됩니다.

이를 메모이제이션(Memoization)이라고 하는데요.

메모이제이션을 사용하는 자바스크립트 코드는 다음과 같습니다.

const fibo = (num, memo) => {

      memo = memo || {}

      if(memo[num]){
          return memo[num]
      }

      if(num<=1){
          return num;
      }

      return fibo(num-1, memo) + fibo(num-2, memo);

}

간단하면서도 효율적으로 피보나치를 계산하는 자바스크립트 함수를 사용할 수 있습니다.