참고할 블로그글
코딩테스트 준비
- 일반적으로 준비해야하는 단계 : 프로그래머스 기준 레벨 3, 해커링크 기준 medium 정도 난이도.
- 어렵게 내는 기업(카카오, 구글 (네이버는 약한 편)) : 4문제 중 1문제 꼴로 3.5~4단계, 해커랭크 hard 정도 문제 출제
알고리즘 문제 풀기 단기간에 준비 할 때는 주입식 교육을 통해 알고리즘을 학습하는 것이 효과적이다.
- 많은 문제 풀고 유형에 대비해야한다.
- 일부 코딩 테스트는 잘 알려진 유형들만 출제된다. 유형들을 접근하는 템플릿이 존재함.
코딩테스트 트랜드
- 자신이 생각하는 것을 코딩할 수 있는 것, 알고리즘 응용
대기업가려면...
알고리즘 공부
,컴퓨팅사고
,CS(컴퓨터사이언스)
만 1년 공부해서 가기도 한다.알고리즘 공부 추천 사이트
GeeksforGeeks.org
Lintcode - 대기업 코딩테스트 문제 사이트
알고리즘 이해 순서가 있다.
공부해야 할 알고리즘 : 재귀, Backtracking, Divide and Conquer, Graph Algorithm, Greedy, DP(냅색, 메모이제이션, 동전), Searching, Sorting(주어진 메소드 가장 빠르다(O(n) 안된다.) 쓰기 : arr.sort((a,b) => a-b)) (힙, 머지, 퀵소트 개념, 시간복잡도 등을 물어볼 수 있다.) Ramdomized Algorithm(매우 어려운것으로 코딩테스트에 절대 안나옴)...
기본적으로 알아야하는 유형
: 이해해서 외우고 코딩테스트 직전에 보고가야한다.
- GCD
- 순열/조합
- 정렬은 Array.prototype.sor 사용
- DFS(재귀), BFS(큐)
- 분할정복(재귀), DP => 감을 잡기 어렵기 때문에 케이스 스터디 필요 (Geeksforgeeks)
대부분의 코딩테스트는 실행시간 1초에 가깝게 디자인된다.
(보통 1억번의 연산 당 1초)시간복잡도 : 우선적 고려.
시간복잡도 1초가 걸리는 입력의 크기 O(N) 100,000,000 O(NlogN) 5,000,000 O(N^2) 10,000 O(N^3) 500 O(2^N) 26 O(N!) 11 공간복잡도 : JS에서 보통 변수 하나는 8Byte. 일반적으로 128/256/512MB 가 자주 등장. 공간복잡도는 메모리성능이 점점 좋아지므로 사실 시간복잡도에 비해 고려도가 낮음.
- 메모리 조건이 128MB일 때,
문제를 처음 봤을 때
- 입력과 공간 상한을 확인.
- 먼저, 완전탐색으로 풀어보기. (알고리즘을 알겠다면 바로 풀어도 됨.) 푸는 과정에서 문제 이해하기. 완전탐색 - 무차별 대입해서 문제 푸는 것.
- 문제 푸는 시간의 30~50%는 문제를 분석하는 데에만 사용.
알고리즘 문제는 문제 푸는게 중요. 숏코딩은 후차적 문제. 코드 정리, 주석달기는 소규모 코딩테스트에서 면접까지 대비할 경우.
시간이 부족해서 못푸는 것은 노력한 모습이라도 보여주면 좋다.
: 알고리즘은 문제를 해결하는 최선의 선택. 모든 경우의 수를 경험하고 최선의 수를 선택할 수 없기 때문에, 알고리즘을 이용해 문제를 해결해야 한다.
: 입력값의 변화에 따라 연산을 실행할 때, 연산 횟수에 비해 시간이 얼마만큼 걸리는가?
: 최악의 경우 시간 고려. 최악을 고려해야 그에 맞는 대응이 가능. 최악의 경우를 고려해야 문제가 발생했을 때 어디에서 문제가 생긴지 알수 있다.
1. O(1) = constant complexity
: 입력값이 증가하더라도 시간이 늘어나지 않음.
// O(1)의 시간 복잡도를 가지는 알고리즘 예시
function O_1_algorithm(arr, index) {
return arr[index];
}
// - 해당 index에 접근하는 것이므로 arr의 길이가 100만이 되어도
// 걸리는 시간은 같다.
let arr = [1, 2, 3, 4, 5];
let index = 1;
let result = O_1_algorithm(arr, index);
console.log(result); // 2
2. O(n) = linear complexity
: 입력값이 증가함에 따라 시간이 같은 비율로 증가하는 것.
// O(n)의 시간 복잡도를 가지는 알고리즘 예시
// do something for 1 second일 때..
function O_n_algorithm(n) {
for (let i = 0; i < n; i++) {
// 입력값(n)이 1 증가할 때마다 코드의 실행 시간이 1초씩 증가
}
}
function another_O_n_algorithm(n) {
for (let i = 0; i < 2n; i++) {
// 입력값이 2배로 늘어나므로, 입력값(n)이 1 증가할 때마다
// 코드의 실행 시간이 2초씩 증가.
}
}
상수
배수로 증가해도 O(n)으로 표기. 3. O(log n) = logarithmic complexity
: 한번 연산할 때 마다 n이 (1/2)줄어든다.
// O(log n)의 시간 복잡도를 가지는 알고리즘 예시
let i = n;
while (parseInt(i) > 0) {
i = i / 2;
}
// 수학적으론 k배수만큼 i가 커지며 n에 도달하고 있기 때문에 log(base k) n
// log base는 big O notation에서 중요하지 않기때문에
// O(log k n) -> O(log n)
for (let i = 0; i < n; i++) {
i *= k;
}
4. O(n^2) = quadratic complexity
: 입력값이 증가함에 따라 시간이 n의 제곱수의 비율로 증가하는 것.
// O(n^2)의 시간 복잡도를 가지는 알고리즘 예시
function O_quadratic_algorithm(n) {
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
// do something for 1 second
}
}
}
function another_O_quadratic_algorithm(n) {
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
for (let k = 0; k < n; k++) {
// do something for 1 second
}
}
}
}
5. O(2^n) = exponential complexity
: 입력값이 1 증가함에 따라 시간이 2배로 증가하는 것.
// O(2^n)의 시간 복잡도를 가지는 알고리즘 예시
// 재귀로 구현하는 피보나치 수열
function fibonacci(n) {
if (n <= 1) {
return 1;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
데이터 크기 제한 | 예상되는시간 복잡도 |
---|---|
n ≤ 1,000,000 | O(n) or O (logn) |
n ≤ 10,000 | O(n^2) |
n ≤ 500 | O(n^3) |
O(n)
, O(log n)
의 시간 복잡도를 만족하도록 풀기. 질문
- 가장 빠른/느린 시간 복잡도는 무엇일까요?
: 가장 빠른 O(1), 가장 느린 O(2^n)
: O(1) < O(logn) < O(n) < O(n^2) < O(2^n) < O(n!)- 효율적인 알고리즘을 구성했다 함은 무엇을 의미할까요?
: 시간 복잡도 적은 알고리즘 구성.- 시간 복잡도는 A와 B의 비례함이 어느 정도인지를 나타냅니다. A와 B는 무엇일까요?
: A(입력값의 크기(연산횟수)), B(실행 시간)
: 매 순간, 최적이라 생각되는 해답(locally optimal solution)을 찾으며, 이를 토대로 최종 문제의 해답(globally optimal solution)에 도달하는 문제 해결 방식.
탐욕 알고리즘
적용 가능한 문제 조건 2가지 : : 위의 경우를 제외하면 항상 최적의 결과를 보장하지는 못한다는 점을 명심. 탐욕 알고리즘은 항상 최적의 결과를 도출하는 것은 아니지만, 어느 정도 최적에 근사한 값을 빠르게 도출할 수 있는 장점이 있다. 이 장점으로 인해 탐욕 알고리즘은 근사 알고리즘으로 사용 가능.
: 방법은 굉장히 단순하고 무식하지만 "답이 무조건 있다"는 강력함. 단순히 모든 경우의 수를 탐색하는 모든 경우를 통칭.
: 시뮬레이션은 모든 과정과 조건이 제시되어, 그 과정을 거친 결과가 무엇인지 확인하는 유형.
: 주어진 문제를 여러 개의 하위 문제로 나누어 풀고, 하위 문제들의 해결 방법을 결합하여 최종 문제를 해결하는 문제 해결 방식. 하위 문제를 계산한 뒤 그 해결책을 저장하고, 나중에 동일한 하위 문제를 만날 경우 저장된 해결책을 적용해 계산 횟수를 줄인다. 즉, 하나의 문제는 단 한 번만 풀도록 하는 알고리즘.
탐욕 알고리즘과 함께 언급하는 알고리즘.
다이내믹 프로그래밍
적용 가능한 문제 조건 2가지 :
: 재귀를 이용한 DP(Memoization) 구현
function fibMemo(n, memo = []) {
// 이미 해결한 하위 문제인지 찾아본다
if(memo[n] !== undefined) return memo[n];
if(n <= 2) return 1;
// 없다면 재귀로 결괏값을 도출하여 res 에 할당
let res = fibMemo(n-1, memo) + fibMemo(n-2, memo);
// 추후 동일한 문제를 만났을 때 사용하기 위해 리턴 전에 memo 에 저장
memo[n] = res;
return res;
}
fibMemo
함수의 파라미터로 n 과 빈 배열을 전달. 이 빈 배열은 하위 문제의 결과값을 저장하는 데에 사용.memo
라는 빈 배열의 n번째 인덱스가 undefined 이 아니라면
(n 번째 인덱스에 어떤 값이 저장되어 있다면) 저장되어 있는 값을 그대로 사용.undefined
라면(처음 계산하는 수라면) fibMemo(n-1, memo) + fibMemo(n-2, memo)
를 이용하여 값을 계산하고, 그 결과값을 res
라는 변수에 할당.res
를 리턴하기 전, memo 의 n 번째 인덱스에 res 값을 저장. 이렇게 하면 (n+1)번째의 값을 구하고 싶을 때, n번째 값을 memo 에서 확인해 사용가능.: 반복문을 이용한 DP 구현
function fibTab(n) {
if(n <= 2) return 1;
let fibNum = [0, 1, 1];
// n 이 1 & 2일 때의 값을 미리 배열에 저장해 놓는다
for(let i = 3; i <= n; i++) {
fibNum[i] = fibNum[i-1] + fibNum[i-2];
// n >= 3 부터는 앞서 배열에 저장해 놓은 값들을 이용하여
// n번째 피보나치 수를 구한 뒤 배열에 저장 후 리턴한다
}
return fibNum[n];
}
질문
- Top-down과 Bottom-up의 소요 시간을 비교하였을 때 결과에 어떤 차이가 있고, 그 원인은 무엇이었을까요?
: Top-down 방식은 점화식을 이해하기 쉽다는 장점이 있고, Bottom-up 방식은 함수를 재귀 호출하지 않기 때문에 시간과 메모리 사용량을 줄일 수 있다는 장점.
두 방법 중 어느 것이 시간적으로 더 효율이 있는지 묻는 데 그 답은 '알수 없다'이다. 실제로 재귀는 내부 스택을 만들고 함수를 호출하는 과정이 더 있어보여서 반복이 더 빠를 것 같다고 느낄 수 있다. 하지만, Top-Down을 통해 문제를 풀어가는 경우에는 점화식에 따라 불 필요한 계산을 오히려 Bottom-Up보다 덜하는 경우가 있기 때문에 궁극적으로는 알 수 없다.
- Top-down은 가장 큰 문제를 방문 후 작은 문제를 호출 하여 답을 찾는 방식
- Bottom-up은 가장 작은 문제들 부터 답을 구해가며 전체 문제의 답을 찾는 방식
- 다이내믹 프로그래밍과 탐욕 알고리즘은 각각 어떤 경우에 사용하기 적합한 알고리즘일까요?
함수의 실행 시간을 측정하는 방법은 여러 가지가 있습니다. 그중에서 다음의 방법으로 간단하게 함수의 실행 시간을 확인할 수 있습니다. 실행 환경에 따라 결과가 다르므로 측정 결과는 학습 용도로만 사용하세요.
// < 함수의 실행 시간을 측정하는 방법 >
var t0 = performance.now();
fib(50); // 여기에서 함수 실행을 시켜주세요
var t1 = performance.now();
console.log("runtime: " + (t1 - t0) + 'ms')