재귀 카테고리 : 반복실행
//단순히 함수를 반복적으로 호출하는 재귀 함수
function countdown(number) {
console.log(number);
if (number === 0) {
return;
} else {
countdown(number - 1);
}
}
//index를 추가 인자로 전달하면서 배열 내의 숫자를 두배로 바꾸는 재귀 함수
function doubleArray(array, index=0) {
if (index >= array.length){
return
}
array[index] *= 2;
doubleArray(array, index + 1)
}
재귀 카테고리 : 계산
//팩토리얼을 구하는 재귀 함수
function factorial(num) {
if (num === 1 || num === 0) return 1
return num * factorial(num-1)
}
factorial(6) = 6 * factorial(5)
인것 처럼 호출되는 재귀 함수를 하위 문제라고 부르며 하위 문제를 호출하면 계산하는 방식을 하향식이라고 한다.
//상향식 호출로 팩토리얼을 구하는 재귀 함수
function factorial(num, i=1, product=1) {
if (i > num) return product
return factorial(num, i+1, product*i)
}
상향식으로도 팩토리얼을 계산할 수 있지만 i, product 처럼 추가적인 변수가 필요하며 루프와 같은 전략으로 계산한다.
function sum(array) {
if (array.length === 1) {
return array[0]
}
return array[0] + sum(array.slice(1))
}
function reverse(string) {
if (string.length === 1) {
return string[0]
}
return reverse(string.slice(1)) + string[0]
}
function countX(string) {
if (string.length === 0) return 0
if (string[0] === "x") {
return 1 + countX(string.slice(1, string.length))
} else {
return countX(string.slice(1, string.length))
}
}
책의 ruby 코드를 javaScript 코드로 변경//한번에 계단을 한개 또는 두개 또는 세개씩 올라갈 수 있을때 계단 수에 따라 오르는 방법의 경우의 수를 리턴하는 재귀 함수
function numberOfPaths(n) {
if(n < 0) return 0
if(n === 1 || n === 0) return 1
return numberOfPaths(n-1) + numberOfPaths(n-2) + numberOfPaths(n-3)
}
def anagrams_of(string)
return [string[0]] if string.length == 1
collection = []
substring_anagrams = anagrams_of(string[1, string.length - 1])
substring_anagrams.each do |substring_anagram|
(0..substring_anagram.length).each do |index|
copy = String.new(substring_anagram)
collection << copy.insert(index, string[0])
end
end
return collection
end
//위 루비로 작성된 애너그램 함수와 동일한 순열을 리턴하는 함수
function findPermutations(string) {
if (string.length === 1){
return string
}
let permutationsArray = []
for (let i = 0; i < string.length; i++){
let char = string[i]
if (string.indexOf(char) != i)
continue
let remainingChars = string.slice(0, i) + string.slice(i + 1, string.length)
for (let permutation of findPermutations(remainingChars)){
permutationsArray.push(char + permutation) }
}
return permutationsArray
}
문자 4개: 애너그램 4 3 2 1개불필요한 재귀 호출
function max(array) {
if (array.length == 1) return array[0]
if (array[0] > max(array.slice(1))){
return array[0]
} else {
return max(array.slice(1))
}
}
빅 오를 위한 작은 개선
function max(array) {
if (array.length == 1) return array[0]
const maxOfRemainder = max(array.slice(1))
if (array[0] > maxOfRemainder){
return array[0]
} else {
return maxOfRemainder
}
}
max(array.slice(1))
를 변수에 저장하여 불필요한 재귀를 개선하였다.하위 문제 중첩
function fibonacci(n) {
if (n === 0 || n === 1) return n
return fibonacci(n - 2) + fibonacci(n - 1)
}
메모이제이션을 통한 동적 프로그래밍
function fibonacci(n) {
let fibonacciArr = [0, 1];
function addFibonacci(n) {
if (fibonacciArr[n] !== undefined) return fibonacciArr[n];
fibonacciArr[n] = addFibonacci(n - 1) + addFibonacci(n - 2);
return fibonacciArr[n];
}
return addFibonacci(n)
}
상향식을 통한 동적 프로그래밍
function fibonacci(n) {
if (n === 0) return
let a = 0;
let b = 1;
for (let i=1; i<=n; i++) {
let temp = a
a = b
b = temp + b
}
return b
}
1. 두 포인터를 사용해 하나는 배열 가장 왼쪽에 다른 하나는 피벗을 제외한 배열 가장 오른쪽에 있는 값에 할당한다.
2. 왼쪽 포인터를 한 셀씩 계속 오른쪽으로 옮기면서 피벗보다 크거나 같은 값에 도달하면 멈춘다.
3. 오른쪽 포인터를 한 셀 씩 계속 왼쪽으로 옮기면서 피벗보다 작거나 같은 값에 도달하면 멈춘다. 또는 배열 맨 앞에 도달해도 멈춘다.
4. 왼쪽 포인터가 오른쪽 포인터에 도달했으면 (또는 넘어섰으면) 다음 단계로 넘어가고 그렇지 않으면 2, 3, 4단계를 반복한다.
5. 왼쪽 포인터가 현재 가리키도 있는 값과 피벗을 교환한다.
- 위 과정을 마치면 피벗 왼쪽에는 피벗보다 작은 값, 오른쪽에는 피벗보다 큰 값이 위치하게 되어, 피벗이 올바른 위치에 있게 된다.
1. 피벗의 왼쪽과 오른쪽에 있는 하위 배열을 각각 또 다른 배열로 보고 분할 과정을 거치고 그 이후의 하위 배열에도 분할과정을 재귀적으로 반복한다.
2. 하위 배열이 원소를 0개 또는 1개를 포함하면 정렬을 마친다.
//부트캠프에서 구현했던 퀵정렬 함수
//왼쪽 포인터와 오른쪽 포인터를 비교해서 위치를 바꾸는 부분이 함수와 차이가 있다.
const quickSort = function (arr) {
if (arr.length <= 1) return arr;
let pivot = arr[0];
let left = [];
let right = [];
for (let i = 1; i < arr.length; i++) {
if (arr[i] < pivot) left.push(arr[i]);
else right.push(arr[i]);
}
let leftArr = quickSort(left);
let rightArr = quickSort(right);
return [...leftArr, pivot, ...rightArr];
}
퀵 정렬의 효율성
퀵정렬의 최악의 시나리오
퀵 정렬 대 삽입 정렬
최선의 경우 | 평균적인 경우 | 최악의 경우 | |
---|---|---|---|
삽입 정렬 | O(N) | O(N2) | O(N2) |
퀵 정렬 | O(NlogN) | O(NlogN) | O(N2) |
최악의 경우에는 삽입 정렬과 퀵 정렬의 시간복잡도가 동일하고 최선의 경우에는 삽입 정렬이 더 빠르지만 평균적인 경우에는 퀵 정렬이 훨씬 빠르기 때문에 내부적으로 퀵 정렬을 사용해 내장 정렬 함수를 구현하는 언어가 많다.
퀵 셀렉트
//퀵 셀렉트 구현 코드
function partitionStart(arr, left, right) {
var pivotIdx = Math.floor(Math.random() * (right - left + 1)) + left;
var storeIdx = left, pivotVal = arr[pivotIdx]
for (var i = left; i <= right; i++) {
if (arr[i] < pivotVal) {
swap(arr, storeIdx, i)
storeIdx++
}
}
return storeIdx;
}
function quickSelectLoop(arr, k) {
var pivotDist;
var left = 0, right = arr.length - 1;
while(right !== left) {
pivotDist = partitionStart(arr, left, right)
if (k < pivotDist) {
right = pivotDist - 1;
} else {
left = pivotDist;
}
}
return arr[k]
}
다른 알고리즘의 핵심 역할을 하는 정렬
function hasDuplicateValue(array) {
array.sort((a, b) => (a < b) ? -1 : 1);
for(let i = 0; i < array.length - 1; i++) {
if (array[i] === array[i + 1]) {
return true;
}
}
return false;
}
이전에는 알고리즘을 이해해서 코드로 구현하고 응용을 통해 문제를 푸는데 집중했엇는데, 책을 통해 다시 공부하면서 효율성을 생각하고 최선, 평균의 경우를 고려하여 개선하는 방향을 생각한다는 것이 큰 차이점이였다. 코딩 테스트는 자료구조와 알고리즘을 이해하는데 도움이 되겠지만 사실 취업이나 대회에 초점을 맞춘 공부라고 생각하기 때문에 실무에서 시간복잡도를 계산하고 나은 방향을 이끌어내는데는 지금의 공부가 맞다고 느꼈다.