알고리즘과 데이터 구조의 효율성을 분석 및 평가에 사용되는 두가지 척도

  • 점진적 표기법의 구성
    • 최상의 경우 : Big-Ω Notation (오메가표기법)
    • 평균의 경우 : Big-⍬ Notation (세타표기법)
    • 최악의 경우 : Big-Notation(빅오표기법)
  • Big-O 표기법은 최악의 경우를 고려하여야 한다.
    • '’이 것 이상으로 X “ / “이 정도 까지 O” => 예측이 쉬워짐
  • Big-O는 복잡도 순서

    O(1) < O(log n) < O(n) < O(n log n) < O(n^2) < O(n^3, ) < O(2^n) < O(n!) 순서로 복잡도가 증가한다.

  • 시∙공 간 은 Big-O 표기법으로 표기할 수 있다.

시간 복잡도 (Time complexity)

알고리즘의 입력 크기의 함수로 실행하는 데 걸리는 시간을 뜻함


O(1) Constant complexity (일정한 복잡도)

  • 입력한 값이 증가하더라도 시간이 늘지 않는다.
  • 입력값이 아무리 크더라도 즉시 출력값을 얻어낼 수 있다.
    # 두개의 숫자 더하기 #
    function add(a,b) {
      return a + b
    }
    
    # 배열의 특정 요소 접근 #
    const array = [1, 2, 3, 4, 5]
    console.log (array[3]) // 4
    
    # 문자열 길이 측정 #
    const length = "Hello Time Complexity"
    console.log (length.length) // 21
    

O(log n) logarithmic complexity (로그 복잡도)

  • 입력 크기가 커짐에 따라 걸리는 시간이 천천히 증가
  • 그래프는 n이 증가함에 따라 낮게 시작하여 상승하고 평평해지는 곡선을 갖게 됨
  • 이는 이진 검색(binary Search) 알고리즘에서 일반적
    # 바이너리(이진)검색
    function binarySearch(array, target) { // 검색범위의 하한 및 상한을 각각 배열의 첫 번째 인덱스와 마지막 인덱스로 초기화
      let low = 0 // 하한
      let high = array.length -1 // 상한
    
      while (low <= high) {
          let mid = math.floor((low + high)/2) // 나누기를 통해 검색범위의 중간을 계산
          if (array[mid] === target) { // 타겟과 대상이 같은지 판단
              return mid // 같다면 mid를 반환
          } else if (array[mid] < target) { // 대상이 타겟보다 작다면
              low = mid + 1 // 하한에 1을 더하고 그 대상요소의 위쪽을 검색한다.
          } else if (array[mid] > target) { // 대상이 타겟보다 크다면
              high = mid - 1 // 상한에 1을 빼고 그 대상요소의 아랫쪽을 검색한다.
          }
      }
      return -1 // 대상 요소가 없다면 -1 을 반환
    }
    

O(n) Linear complexity (선형 복잡도)

  • 시간은 입력 크기에 비례하여 증가합니다.
  • 그래프는 입력 크기와 걸린 시간 사이의 1:1 관계를 나타내는 양의 기울기가 있는 직선
  • 선형 시간 복잡도 알고리즘의 예로는 단순 검색 및 순회 알고리즘
    function findMaxIndex(array){ // array를 인자로 받음
      let maxIndex = 0 // 최대값 index를 maxIndex라 하고 0 으로 초기화
      for (let i = 1; i < array.length; i++) { // for 반복문을 통해 배열의 길이만큰 탐색
          if (array[i] > array[maxIndex]) { // 탐색한 index가 maxIndex 보다 클 경우 
              maxIndex = i // 탐색 index로 업데이트
          }
      }
      return maxIndex // 탐색이 끝나면 maxIndex 반환
    }
    # Index의 크기가 증가  수록 탐색시간이 증가
    

O(n log n) Linearithmic complexity (선형 복잡도)

  • 선형과 2차 사이의 성장률을 나타냄
  • 그래프는 낮게 시작하여 n이 커짐에 따라 기울기가 점차 증가하는 곡선
  • 이는 병합 정렬 및 퀵 정렬과 같은 정렬 알고리즘에 일반적
병합정렬 알고리즘

function mergeSort(array) {
	if (array.length <= 1) { // 병합정렬 (mergeSort) 함수는 배열을 입력으로 사용하고 배열의 정렬된 버전을 반환 배열의 길이가 1 또는 0 이면, 이미 정렬된 것으로 함수는 변경되지 않고 반환 됨
		return array
	}
	const midIndex = Math.floor(array.length / 2) // 반환되지 않으면 배열을 나누어 중간 인덱스를 할당
	const leftIndex = array.slice(0,midIndex) // 왼쪽 정렬
	const rightIndex = array.slice(midIndex) // 오른쪽 정렬

	const sortedLeftArray = mergeSort(leftArray) // 왼쪽 정렬 mergeSort 이용 재귀적 정렬
	const sortedRightArray = mergeSort(rightIndex) // 오른쪽 정렬 mergeSort 이용 재귀적 정렬

	return mergre(sortedLeftArray, sortedRightArray) //merge로 병합하여 정렬된 배열이 반환 됨
}

병합 알고리즘
function merge(leftArray, rightArray) { // 3가지 leftIndex, rightIndex, mergeArray 초기화
	let leftIndex = 0
	let rightIndex = 0
	let mergeArray = []

	while (leftIndex < leftArray.length && rightIndex < rightArray.length) {
	// 이 함수는 왼쪽 및 오른쪽 배열을 비교하고 각 단계에서 병합된 배열에 가장 작은 값을 추가하는 while 루프를 사용하여 배열을 반복
		if (leftArray[leftIndex] < rightArray[rightIndex]) {
			mergeArray.push(leftArray[leftIndex])
			leftIndex++
		} else {
			mergeArray.push(rightArray[rightIndex])
			rightIndex++
		}
	}
  // 배열 중 하나가 소진되면 함수는 다른 배열의 나머지 요소를 병합된 배열에 추가
	while (leftIndex < leftArray.length) {
		mergeArray.push(leftArray[leftIndex])
		leftIndex++
	}
	while (rightIndex < rightArray.length) {
		mergeArray.push(rightArray[rightIndex])
		rightIndex++
	}
	// 병합된 배열이 반환됩니다.
	return mergedArray;
}

O(n^2) Quadratic complexity (2차 복잡도)

  • 소요 시간은 입력 크기의 제곱에 따라 증가
  • 그래프는 낮게 시작하여 n이 증가함에 따라 기울어지는 포물선 곡선
  • 버블 정렬 및 삽입 정렬과 같은 중첩 루프 알고리즘의 특징
    function sumOfPairs(array) { 
      let sum = 0; 
      for (let i = 0; i < array.length; i++) { 
          for (let j = i + 1; j < array.length; j++) { 
          sum += array[i] + array[j]; 	
          } 
      } 
      return sum; 
    } 
    const array = [1, 2, 3, 4]; 
    console.log(sumOfPairs(array)); // Output: 20
    

O(n^3) Cubic complexity (큐빅 복잡도)

  • 입력 크기의 세제곱에 비례하는 성장률을 나타냄
  • 그래프는 낮게 시작하여 급격히 가팔라지고 n이 증가함에 따라 계속 가파르게 되는 곡선을 갖게 됨
  • 3개의 중첩 루프가 있는 알고리즘에서 일반적
    function productOfTriplets(array) {
    let product = 1;
    
    for (let i = 0; i < array.length; i++) {
      for (let j = i + 1; j < array.length; j++) {
        for (let k = j + 1; k < array.length; k++) {
          product *= array[i] * array[j] * array[k];
        }
      }
    }
    return product;
    }
    const array = [1, 2, 3, 4];
    console.log(productOfTriplets(array)); // Output: 144
    

O(2^n) Exponential complexity (지수적 복잡도)

  • 기하급수적으로 복잡한 알고리즘의 경우 입력 크기가 증가할 때마다 걸리는 시간이 두 배가 됨
  • 그래프는 n이 커짐에 따라 거의 수직이 되는 가파른 곡선을 보여줌
  • 방문 판매원 문제에 대한 무차별 대입 솔루션과 같은 일부 재귀 알고리즘에서 일반적
    function generateSubsets(set) {
    const subsets = [];
    for (let i = 0; i < Math.pow(2, set.length); i++) {
      const subset = [];
      for (let j = 0; j < set.length; j++) {
        if (i & (1 << j)) {
          subset.push(set[j]);
        }
      }
      subsets.push(subset);
    }
    return subsets;
    }
    const set = [1, 2, 3];
    console.log(generateSubsets(set)); // Output: [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]
    

O(n!) Factorial complexity (계승 복잡도)

  • n이 커질수록 알고리즘을 완료하는 데 필요한 작업 수가 매우 빠르게 증가하므로 대부분의 실제 응용 프로그램에서는 비실용적
    function generatePermutations(set) {
    const permutations = [];
    
    function swap(array, i, j) {
      const temp = array[i];
      array[i] = array[j];
      array[j] = temp;
    }
    function permute(array, startIndex) {
      if (startIndex === array.length - 1) {
        permutations.push([...array]);
      } else {
        for (let i = startIndex; i < array.length; i++) {
          swap(array, startIndex, i);
          permute(array, startIndex + 1);
          swap(array, startIndex, i);
        }
      }
    }
    permute(set, 0);
    return permutations;
    }
    const set = [1, 2, 3];
    console.log(generatePermutations(set)); // Output: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,2,1],[3,1,2]]
    

공간 복잡도 (Space Complexity)

알고리즘이 입력 크기의 함수로 문제를 해결하기위해 사용하는 메모리양

  • 프로그램을 실행 및 완료하는데 필요한 저장공간의 양
  • 총 필요 저장 공간
    • 고정공간 (알고리즘과 무관한 공간) : 코드 저장공간, 단순 변수 및 상수
    • 가변공간 (알고리즘 실행과 관련있는 공간) : 실행 중 동적으로 필요한 공간
    • S(P) = c + Sp(n)
      • c : 고정공간
      • Sp(n) : 가변공간

고정공간은 상수 이므로, 공간 복잡도는 가변 (Sp(n))에 영향을 받음

O(1) Constant complexity (일정한 복잡도)

function exampleFunction() {
	const a = 1;
	const b = 2;
	const c = 3;
	return a + b + c;
}

console.log(`Memory usage: ${memoryUsage} bytes`); // output 1720 Bytes

O(log n) logarithmic complexity (로그 복잡도)

// Define an O(log n) space complexity function

function exampleFunction(n) {
	let result = 0;
	for (let i = 1; i <= n; i *= 2) {
		result += i;
	}
	return result;
}

const memoryUsageBefore = process.memoryUsage().heapUsed;
exampleFunction(n);
const memoryUsageAfter = process.memoryUsage().heapUsed;
const memoryUsage = memoryUsageAfter - memoryUsageBefore

console.log(`Memory usage: ${memoryUsage} bytes`); // exampleFunction(1000 ~ 1000000000) => output : 1720 Bytes
																									 // exampleFunction(10000000000) => output : 1832 Bytes

O(n) Linear complexity (선형 복잡도)

// Define an O(log n) space complexity function
function exampleFunction(n) {
	const array = [];
	for (let i = 0; i < n; i++) {
		array.push(i);
	}
	return array;
}
const memoryUsageBefore = process.memoryUsage().heapUsed;
exampleFunction(10);
const memoryUsageAfter = process.memoryUsage().heapUsed;
const memoryUsage = memoryUsageAfter - memoryUsageBefore;

console.log(`N(${n})일 때, Memory usage: ${memoryUsage} bytes`);
N(10) , Memory usage: 1904 bytes
N(100) , Memory usage: 4072 bytes
N(1000) , Memory usage: 30360 bytes
N(10000) , Memory usage: 247112 bytes

O(n log n) Linearithmic complexity (선형 복잡도)

// Define an O(n log n) space complexity function
function exampleFunction(n) {
	const array = [];
	for (let i = 0; i < n; i++) {
		array.push(Math.floor(Math.random() * n));
	}
	array.sort();
	return array;
}
const memoryUsageBefore = process.memoryUsage().heapUsed;
let n = 10;
exampleFunction(n);
const memoryUsageAfter = process.memoryUsage().heapUsed;
const memoryUsage = memoryUsageAfter - memoryUsageBefore;

console.log(`N(${n})일 때, Memory usage: ${memoryUsage} bytes`);
N(10) , Memory usage: 3152 bytes
N(100) , Memory usage: 9312 bytes
N(1000) , Memory usage: 85584 bytes
N(10000) , Memory usage: 731496 bytes

O(n^2) Quadratic complexity (2차 복잡도)

// Define an O(n^2) space complexity function
function exampleFunction(n) {
	const matrix = [];
	for (let i = 0; i < n; i++) {
		matrix[i] = [];
		for (let j = 0; j < n; j++) {
			matrix[i][j] = i * j;
		}
	}
	return matrix;
}
const memoryUsageBefore = process.memoryUsage().heapUsed;
let n = 10;
exampleFunction(n);
const memoryUsageAfter = process.memoryUsage().heapUsed;
const memoryUsage = memoryUsageAfter - memoryUsageBefore;

console.log(`N(${n})일 때, Memory usage: ${memoryUsage} bytes`);

N(10) , Memory usage: 3872 bytes
N(100) , Memory usage: 241680 bytes
N(1000) , Memory usage: 13584624 bytes
N(10000) , Memory usage: 807010848 bytes

O(n^3) Cubic complexity (큐빅 복잡도)

// Define an O(n^3) space complexity function
function exampleFunction(n) {
	const matrix = [];
	for (let i = 0; i < n; i++) {
		matrix[i] = [];
		for (let j = 0; j < n; j++) {
			matrix[i][j] = [];
			for (let k = 0; k < n; k++) {
				matrix[i][j][k] = i * j * k;
			}
		}
	}
	return matrix;
}

const memoryUsageBefore = process.memoryUsage().heapUsed;
let n = 10;
exampleFunction(n);
const memoryUsageAfter = process.memoryUsage().heapUsed;
const memoryUsage = memoryUsageAfter - memoryUsageBefore;

console.log(`N(${n})일 때, Memory usage: ${memoryUsage} bytes`);
N(10) , Memory usage: 23904 bytes
N(100) , Memory usage: 13620472 bytes
N(500) , Memory usage: 1124227816 bytes
N(1000) , heap Memory 초과 

O(2^n) Exponential complexity (지수적 복잡도)

// Define an O(2^n) space complexity function
function exampleFunction(n) {
	if (n === 0) {
		return [[]];
	} else {
		const prev = exampleFunction(n - 1);
		const result = [];
		for (let i = 0; i < prev.length; i++) {
			result.push([...prev[i], 0]);
			result.push([...prev[i], 1]);
		}
		return result;
	}
}

const memoryUsageBefore = process.memoryUsage().heapUsed;
let n = 10;
exampleFunction(n);
const memoryUsageAfter = process.memoryUsage().heapUsed;
const memoryUsage = memoryUsageAfter - memoryUsageBefore;

console.log(`N(${n})일 때, Memory usage: ${memoryUsage} bytes`);
N(10) , Memory usage: 796552 bytes
N(20) , Memory usage: 791310504 bytes
N(21) , Memory usage: 1401836816 bytes
N(22) , Memory usage: 2818895488 bytes
N(23) , heap Memory 초과

O(n!) Factorial complexity (계승 복잡도)

function exampleFunction(n) {
  if (n <= 1) {
    return [[n]];
  } else {
    const prev = eexampleFunction(n - 1);
    const result = [];
    for (let i = 0; i < prev.length; i++) {
      for (let j = 0; j < n; j++) {
        const copy = [...prev[i]];
        copy.splice(j, 0, n);
        result.push(copy);
      }
    }
    return result;
  }
}

const memoryUsageBefore = process.memoryUsage().heapUsed;
let n = 10;
exampleFunction(n);
const memoryUsageAfter = process.memoryUsage().heapUsed;
const memoryUsage = memoryUsageAfter - memoryUsageBefore;  

console.log(`N(${n})일 때, Memory usage: ${memoryUsage} bytes`);
N(10) , Memory usage: 1254587888 bytes
N(11) , heap memory 초과