intro
big o 표기법은 같은 문제를 해결하는 여러 방법, 즉 여러 알고리즘 중 어떤 것이 더 나은지 판단할 수 있는 기준이 된다. 어떤 알고리즘이 더 낫다고 이야기하는 것은 상황에 따라 다르지만, 빠른 속도와 적은 메모리 사용량이 보편적인 기준이다.
+) 속도나 메모리 사용량 뿐만 아니라 코드의 가독성, 간편한 유지보수 가능성 등이 더 나은 코드를 판별하는 기준이 될 수 있다.
time complexiry | how long to execute the function?
perfomance.now 이용하기
예를 들어 문자열을 input으로 받고, 그 문자열을 거꾸로 뒤집은 값을 반환하는 함수를 만든다고 해 보자. 이 함수를 구현해내는 방법은 아래의 두가지 외에도 많을 것이다. 둘 중 어떤 함수가 더 나은 함수일까? 우선 함수를 실행하는 데 걸리는 시간을 기준으로 답해보자.
function reverse(input) {
const lastIndex = input.length - 1
let reveresedString = ''
for (let index = lastIndex; index >= 0; index--) {
reveresedString = reveresedString + input[index]
}
return reveresedString
}
function reverseWithArray(input) {
const arrayOfInput = input.split('')
return arrayOfInput.reverse().join('')
}
const time1 = performance.now()
reverseWithArray('hello hello hello hello hello')
const time2 = performance.now()
const duration = (time2 - time1) / 1000
위와 같이 코드를 작성하면 reverseWithArray 함수를 실행하는 데 걸린 시간을 확인할 수 있다. 마찬가지 방법으로 reverse 함수를 실행할 때의 걸리는 시간도 비교해 볼 수 있는데, reverseWithArray 함수를 실행할 때 더 짧은 시간이 걸리는 것을 확인할 수 있다. 그러나 함수를 실행할 때 마다 걸리는 시간을 수동적으로 재는 것은 번거로울 뿐만 아니라, 여러 가지 한계를 가지고 있다.
- 어떤 기기에서 함수를 실행하느냐에 따라 그 값이 달라질 수 있다.
- 같은 기기에서 여러번 performance를 측정할 때에도, 매번 그 값이 다르게 측정된다.
- 시간 차이가 매우 작을 때에는 그 차이를 정확하게 측정하지 못할 수 있다.
- 함수를 실행하는 데 얼마나 걸리는지 확인하기 위해서, 그 함수를 실행해야 한다. => 결과를 반환하는 데 1시간이 걸리는 함수가 있다고 생각해 보자.
처리해야 할 연산의 개수 이용하기
환경에 따라 값이 달라지는 시간과는 달리, 컴퓨터가 처리해야 하는 연산의 갯수는 변하지 않는다. 따라서 각 함수가 처리해야 하는 연산의 갯수를 이용하면, 보다 객관적으로 성능을 비교할 수 있다. 아래 예제 addUpTo(n)은 1~n까지의 자연수의 덧셈을 반환한다. 예를 들어 addUpTo(3)은1 + 2 + 3의 결과를 반환한다.
// 1. 3 개의 연산
function addUpTo(n) {
return (n * (1 + n)) / 2 // 3 operaions(*, + , /)
}
// 2. 5n + 2 개의 연산
function addUpTo(n) {
let total = 0 // 1 assignment
for (let index = 1; index <= n; index++) {
// 1 assignment, n comparisons, n additions & assignments
total += total + index // n additions & assignments
}
return total
}
같은 결과를 반환하는 함수라고 해도, 첫 번째 경우는 n에 값에 관계 없이 항상 3개의 연산만 처리하는 반면, 두 번째 경우는 n의 값이 커짐에 따라 처리해야 하는 연산의 개수도 비례해서 커지는 것을 확인할 수 있다. 따라서 전자의 방법이 더 뛰어난 성능을 가지고 있다고 판단할 수 있다.
what is BigO notation?
Big O notation is a mathematical notation that describes the ??? limiting behavior of a function when the argument tends towards a particular value or infinity.
BigO 표기법은 앞서 설명한 연산의 갯수를 대략적으로(fuzzy counting) 나타내는 방법이다. 더 정교하게 이야기하자면, 함수의 input 값이 커질 때 알고리즘의 실행 시간은 어떻게 변화하는지를 다룰 수 있는 방법이다.
- linear(선형) | f(n) = n => 알고리즘 f에서 인풋과 실행시간은 선형으로 비례한다. => O(n)이라고 표기한다.
- quadratic(2차) | f(n) = n^2 => 중첩 반복문, O(n^2)이라고 표기한다.
- constant(상수) | f(n) = 1, 2, ... => 알고리즘 f는 인풋 감에 관계없이 항상 동일한 실행 시간을 갖는다. => O(1)이라고 표기한다.
- 기타 등등
BigO 표기법은 함수의 인풋과 실행 시간 사이의 관계를 전반적인 추세로 나타내는 것이 목적이므로, n, 2n, 5n+2... 등등 상수에 관계 없이 모두 O(n)으로 표기한다. 같은 이유로 O(1), O(n^2)으로 표기한다. 다음은 몇몇 연산에 관한 시간 복잡도를 나타낸 것이다.
- 수학 연산(+, -, *, / ...)은 O(1)이다. 예를 들어 2 + 1이나, 2000 + 10293이나 값을 계산하는 데 걸리는 시간은 비슷하다.
- 변수 할당은 O(1)이다.
- index나 key를 이용해서 배열, 객체의 요소에 접근하는 것은 O(1)이다.
- 반복문은 O(n), 중첩 반복문은 O(n^2)이다.
space complexiry | how much memory to allocate?
지금까지는 함수를 실행하는데 얼마나 많은 시간이 걸리는 지(time complexity)에 관해 살펴보았다. space complexity는 함수를 실행하는 데 얼마나 많은 메모리 공간을 필요로 하는지를 설명한다. 이 때 함수의 input이 차지하는 메모리 공간은 고려하지 않으며, 함수(알고리즘)를 실행할 때 필요로 하는 메모리 공간 만을 복잡도 계산에 포함시킨다. 정확한 용어로는 axiliary space complexity라고 하는데, space complexity라고 하면 보통 이를 가리킨다. 공간 복잡도를 표현하는 데에도 BigO 표기법을 사용할 수 있다.
- 대부분의 원시 데이터(primitive type data)인 boolean, numbers, undefined, null)는 O(1)의 메모리 공간을 차지한다. => true나 false가 차지하는 메모리 크기, 1과 2000이 차지하는 메모리 크기는 같다.
- 반면 원시 데이터 중 문자열 데이터는 문자열의 length에 따라 O(n)의 공간을 차지한다. => 길이가 50인 문자열은 1인 문자열보다 50 배 많은 메모리 공간을 차지한다.
- 참조 타입 데이터(reference data)는 데이터의 length에 따라 O(n)의 공간을 차지한다.
function add(array) {
let total = 0
for (let i = 0; i < array.length; i++) {
total += total + array[i]
}
return total
}
function double(array) {
let newArray = []
for (let i = 0; i < array.length; i++) {
newArray.push(2 * array[i])
}
return newArray
}
위의 add 함수는 total이라는 변수에 숫자 데이터를 재할당 하는 것이 전부이기 때문에, space complexity는 O(1)이 된다. 반면 double 함수는 newArray라는 변수에 배열을 저장하고, 그 배열의 아이템을 input의 길이만큼 추가하고 있기 때문에 space complexity는 O(n)이 된다.
logarithm
A logarithm is the power to which a number must be raised in order to get some other number. For example, the base ten logarithm of 100 is 2.
=> base number인 b를 다른 숫자(b^x)로 만들기 위해 b의 지수가 되어야 하는 숫자(x)를 logarithm이라고 한다.
=> 곱하기와 나누기의 관계 처럼, 로그함수는 지수함수의 역함수(reverse)이다. 로그는 지수 값을 나타내므로, base 숫자에 지수를 올리면(b^c) 원하는 숫자(a)를 만들어 낸다.
로그는 추후에 다룰 sorting 알고리즘, recursion 등의 시간/공간 복잡도와 관련있다.
'Study > Algorithm & Data structure' 카테고리의 다른 글
sort algorithm(1) (0) | 2022.10.17 |
---|---|
recursion (0) | 2022.10.11 |
problem solving patterns(2) (1) | 2022.10.08 |
problem solving patterns (0) | 2022.10.03 |
performance of array & object (0) | 2022.10.03 |