코드 반복 작성만이 익숙해지는 길인가, 좀처럼 익숙해지지 않고 있는 애증의 재귀 함수.
오늘은 Recursion 재귀에 대해 정리해 본다.
Recursion 이란?
함수는 자신도 호출할 수 있는데, 함수 안에서 자기 자신을 다시 호출하는 프로그래밍 패턴을 말하며 '재귀 함수'라고 한다.
- 재귀 함수를 사용할 때에는 반드시 재귀 함수를 끝내는 종료 조건(초기화)이 필요하다.
- 종료 조건을 설정하지 않으면 시스템이 무한 루프의 재귀함수를 돌아서 메모리 오류가 발생한다.
- 재귀는 같은 종류의 작업을 반복적으로 실행할 때 유용하다.
재귀함수를 쓰는 이유는?
재귀적 알고리즘은 비 재귀적인 알고리즘으로 변환할 수 있다. 재귀는 반복문과 유사하다. 둘 다 동일한 코드를 여러 번 실행하고 종료 조건이 필요하다. 일부 함수는 반복문으로도 구현이 가능하지만 그렇게 변환하기 어려운 경우도 있다. 예를 들어 tree구조에서 모든 노드의 값을 확인하는 경우는 재귀를 사용하는 게 훨씬 쉽고 코드도 직관적이다.
- 알고리즘을 기술한 그대로 코드로 표현할 수 있다. 즉 코드 가독성이 높다.
- 변수 사용을 줄일 수 있다.
재귀함수의 단점
함수가 호출될 때 함수의 입력값, 리턴 값, 돌아갈 위치 값 등이 스택에 저장된다. 이때 함수가 끝나지 않은 채 재귀적인 함수 호출이 계속 이루어지게 되면 스택에도 메모리가 계속 쌓이게 되고 overflow가 발생할 수 있다.
- 재귀적으로 함수를 호출할 때마다 스택을 소비하기 때문에 반복문에 비해 시간이 많이 걸린다.
먼저 비교적 간단한 계산식 알고리즘을 예시로 보자.
팩토리얼은 그 수보다 작거나 같은 모든 양의 정수의 곱을 말한다. 팩토리얼을 재귀 함수로 작성하면 아래와 같다.
function factorial(n) {
if(n > 1) {
return n * factorial(n - 1);
}
return n;
}
factorial(1); //1
factorial(2); //2
factorial(3); //6
factorial(4); //24
factorial(5); //120
같은 알고리즘을 반복문으로도 작성할 수 있다.
function factorial(n) {
let total = 1;
for(let i = n; n >= 1; n--) {
total = total * n;
}
return total;
}
이번에는 두 수의 최대공약수를 구하는 알고리즘이다. 아래 코드를 이해하기 위해서는 유클리드 호제법 을 알아야 한다.
* 유클리드 호제법 : 2개의 자연수(또는 정식) a, b에 대해서 a를 b로 나눈 나머지를 r이라 하면(단, a>b), a와 b의 최대공약수는 b와 r의 최대공약수와 같다.
ex.
- 1071은 1029로 나누어떨어지지 않기 때문에, 1071을 1029로 나눈 나머지를 구한다. ≫ 42
- 1029는 42로 나누어떨어지지 않기 때문에, 1029를 42로 나눈 나머지를 구한다. ≫ 21
- 42는 21로 나누어떨어진다. ≫ 최대공약수는 21
function gcd(m, n) {
if (!n) {
return m;
}
return gcd(n, m % n);
}
gcd(1071, 1029); //21
다른 예제로 중첩된 배열의 뎁스를 제거하는 알고리즘도 재귀함수로 작성할 수 있다.
예를 들어 아래와 같이 중첩된 배열의 결괏값으로 뎁스를 제거한 배열을 얻고 싶다면 reduce()를 활용해볼 수 있다. 값을 리턴할 때 삼항 연산자를 이용해 해당 val의 값이 배열인지를 확인하고 배열이면 재귀 호출, 아니면 기존 배열에 합치도록 한다.
이런 예제는 주어진 배열의 뎁스가 랜덤 한 경우, 반복문으로 작성하기는 어렵다.
function flat(array) {
return array.reduce(function(acc, val) {
return acc.concat(Array.isArray(val) ? flat(val) : val);
}, []);
}
flat([[1, 2], 3, [4, [5, 6, 7]]]); //[ 1, 2, 3, 4, 5, 6, 7 ]
반복적인 작업을 하는 알고리즘으로 객체 안에 겹의 객체를 가지고 있는 오브젝트에서 특정한 값이 있는지를 찾는 코드도 예시로 종종 볼 수 있다. 아래 코드는 nestedObject에서 특정 값이 존재하는지의 여부를 true, false로 리턴하는 알고리즘이다.
var nestedObject = {
data: {
info: {
stuff: {
thing: {
moreStuff: {
magicNumber: 44,
something: 'foo2'
}
}
}
}
}
}
function contains(obj, value){
for(let key in obj){
if(typeof obj[key] === 'object'){
return contains(obj[key], value); //재귀 호출
}
if (obj[key] === value){
return true;
}
}
return false;
}
contains(nestedObject, 44); // true
contains(nestedObject, 'foo'); // false
위와 비슷한 예제로 트리 구조를 재귀 함수로 탐색하는 코드도 생각해볼 수 있다.
let check = function(tree) {
let result = [];
if(tree.children) {
for (let i = 0; i < tree.children.length; i++) {
result.push(tree.children[i].value);
check(tree.children[i])
}
}
return result;
}
'Coding > TIL (Today I Learned)' 카테고리의 다른 글
[Javascript] 명령어 return의 쓰임 (2) | 2019.12.18 |
---|---|
[Javascript] 논리연산자를 이용한 변수 초기화 (0) | 2019.12.17 |
Data Structure 03. Tree, Binary Search Tree (0) | 2019.12.13 |
Data Structure 02: (0) | 2019.12.09 |
OOP 02: JavaScript의 Prototype (0) | 2019.12.08 |