본문 바로가기
코딩테스트 및 알고리즘

재귀함수

by 영카이브 2024. 2. 18.

재귀함수란?

  • 자신이 자신을 호출하는 함수
  • 재귀함수는 stack구조를 사용
  • 재귀함수에는 함수가 더 이상 반복되지않는 경우인 기저 조건이 적어도 하나는 있어야 함

 

재귀함수와 스택프레임

  • 재귀호출을 할 때마다 새로운 호출 스택이 메모리 스택에 생성
  • 각 호출 스택에는 해당 호출의 인수와 지역변수 등이 저장
  • 가장 최근에 호출된 함수가 스택의 맨 위에 위치
  • 재귀 호출에서 함수가 실행을 마치고 반환되면 해당 함수에 대한 호출 스택이 메모리에서 제거 ( 호출 스택 해제 )
  • 재귀 호출이 많이 일어나면 호출 스택이 많이 쌓이고 메모리 사용량을 늘리게 되어 메모리 낭비라는 부작용이 있다. 

 

재귀함수 예시

1. 출력 : 3 2 1

public class Main {	
	public void DFS(int n) {
		if(n==0)return; // return : 함수 종료 
		else {
			System.out.println(n+ " ");
			// 무한반복
			DFS(n-1); 
		}
	}

	public static void main(String[] args) {
		Main T = new Main();
		T.DFS(3); 
	}
}

 

2. 출력 : 1 2 3

public class Main {	
	public void DFS(int n) {
		if(n==0)return; // return : 함수 종료 
		else {
			// 무한반복
			DFS(n-1); 
			System.out.println(n+ " ");
		}
		
	}

	public static void main(String[] args) {
		Main T = new Main();
		T.DFS(3); 
	}
}

 

 

1번과 2번의 차이

 

1번은 재귀 호출이 기저 조건에 도달할 때까지 계속 호출되고, 함수가 종료될 때마다 해당 호출의 매개변수 n 값을 출력한다. 따라서 3  2  1 로 출력된다.

 

2번은 재귀 호출이 기저 조건에 도달한 후 반환되면 호출 스택이 메모리에서 제거되면서 저장되어있던 정보를 출력한다. 따라서 1 2 3 으로 출력된다.

 


 

 

이진수 재귀함수

public class Main {	
	public void DFS(int n) {
		if(n==0) return; 
		else {
			DFS(n/2); 
			System.out.print(n%2+ " ");
		}
	}

	public static void main(String[] args) {
		Main T = new Main();
		T.DFS(11);
	}
}

 

  1. DFS(11)에서 n은 11이므로, DFS(11/2)인 DFS(5)를 호출
  2. DFS(5)에서 n은 5이므로, DFS(5/2)인 DFS(2)를 호출
  3. DFS(2)에서 n은 2이므로, DFS(2/2)인 DFS(1)을 호출
  4. DFS(1)에서 n은 1이므로, DFS(1/2)인 DFS(0)을 호출
  5. DFS(0)에서는 n이 0이므로 재귀 호출이 종료되고 함수가 리턴
  6. 역순으로 각 호출의 결과를 출력 / 1011

 

 


팩토리얼 재귀함수

public class Main {	
	public int DFS(int n) {
		if(n==1) return 1; 
		else return n*DFS(n-1); 
	}

	public static void main(String[] args) {
		Main T = new Main();
		System.out.println(T.DFS(5));
	}
}

 

DFS(5) / 5 * DFS(4)

DFS(4) / 4 * DFS(3)

DFS(3) / 3 * DFS(2)

DFS(2) / 2 * DFS(1)

DFS(1) / 1

 

로 진행후 return 값을 받게 된다. 

 

1

2 * 1 = 2

3 * 2 = 6

4 * 6 = 24

5 * 24 = 120

 

즉 DFS(n-1)에 축적을 받는 셈이다. 

1  >>  1 * 2  >> 1 * 2 * 3  >>  1 * 2 * 3 * 4  >>  1 * 2 * 3 * 4 * 5

 

답 : 120

 

 

피보나치 재귀함수

  • DFS(n-2) 호출을 먼저 수행하고, 이후에 DFS(n-1) 호출을 수행

ex ) n = 5일 때 

1. 단순한 재귀 함수 사용

  • 같은 값에 대해 중복된 계산을 수행하기 때문에 성능이 좋지 않음
  • 메모이제이션을 사용하지 않으므로 중복 계산을 효율적으로 제거하지 않음
public class Main {	
	public int DFS(int n) {
		if(n==1) return 1; 
		else if(n==2) return 1; 
		else {
			return DFS(n-2)+DFS(n-1); 
		}
	}

	public static void main(String[] args) {
		Main T = new Main();
		Scanner sc = new Scanner(System.in); 
		int n = sc.nextInt();
		for( int i=1; i<=n; i++ ) {
			System.out.print(T.DFS(i) + " ");
		}
	}
}

 

 

1. DFS(1) 

  • if(n==1) return 1; 

2. DFS(2)

  • else if(n==2) return 1; 

3. DFS(3)

  • DFS(n-2)를 먼저 실행, DFS(1)이므로 1리턴
  • DFS(n-1)를 실행, DFS(2)이므로 return 1 
  • 1+ 1 =2 , 2 리턴

4. DFS(4)

  • DFS(n-2)를 먼저 실행, DFS(2)이므로 1리턴
  • DFS(n-1)를 실행, DFS(3)이므로 
    • DFS(1)은 1리턴 
    • DFS(2)는 1리턴
    • 둘을 합쳐서 2리턴
  • DFS(4)는 1 + 2 =3 리턴

5. DFS(5) 

  • DFS(n-2)를 먼저 실행, DFS(3)이므로 
    • DFS(1)은 1리턴
    • DFS(2)는 1리턴
    • 둘을 합쳐서 2리턴
  • DFS(n-1)를 실행, DFS(4)이므로 
    • DFS(2)는 1리턴
    • DFS(3)은 
      • DFS(1)은 1리턴
      • DFS(2)는 1리턴
      • 둘을 합쳐서 2리턴
    • DFS(2)+DFS(3) = 3리턴
  • DFS(5) 는 2 + 3 = 5 리턴 1 1 2 3 5 출력

6. 1 1 2 3 5 출력

 

중복 계산이 발생!

 

2. 메모이제이션을 사용하여 중복된 계산을 효율적으로 제거

  • 각 계산된 값을 배열에 저장하고 필요할 때마다 배열에서 값을 가져와 사용
  • 조건문을 추가하여, 이미 계산된 값을 배열에 저장한 후 해당 값이 있을 경우에는 바로 반환
  • 중복된 계산이 한 번만 수행되므로 성능 개선
public class Main {	
	static int[] fibo; 
	public int DFS(int n) {
		// 이미구한 값
		if(fibo[n]>0) return fibo[n]; 
		if(n==1) return fibo[n]=1; 
		else if(n==2) return fibo[n]=1; 
		else {
			return fibo[n]=DFS(n-2)+DFS(n-1); 
		}
	}

	public static void main(String[] args) {
		Main T = new Main();
		Scanner sc = new Scanner(System.in); 
		int n = sc.nextInt();
		fibo = new int[n+1]; 
		for( int i=1; i<=n; i++ ) {
			System.out.print(T.DFS(i) + " ");
		}
	}
}

 

1. DFS(1) 

  • if(n==1) return fibo[1]=1; 

2. DFS(2)

  • else if(n==2) return fibo[2]=1; 

3. DFS(3)

  • return fibo[3] = DFS(1) + DFS(2)
  • DFS(n-1)를 실행, DFS(2)이므로 return 1 
  • 1+ 1 =2 이고 이 값이 fibo[3]에 저장된 후 리턴

4. DFS(4)

  • DFS(n-2)를 먼저 실행, DFS(2)이므로 1리턴
  • DFS(n-1)를 실행, DFS(3)이므로 2리턴 (이미 fibo[3]에 DFS(3)값이 저장되어있기 때문)
  • 1 + 2 =3 이고 이 값이 fibo[4]에 저장된 후 리턴

5. DFS(5) 

  • DFS(n-2)를 먼저 실행, DFS(3)이므로 2리턴 (이미 fibo[3]에 DFS(3)값이 저장되어있기 때문)
  • DFS(n-1)를 실행, DFS(4)이므로 3리턴 (이미 fibo[4]에 DFS(4)값이 저장되어있기 때문)
  • DFS(5) 는 2 + 3 = 5 리턴 

6. 1 1 2 3 5 출력

 

 

참고) int[] fibo; 가 아닌 static int[] fibo; 인 이유

  • main 메서드는 정적 메서드이며, 정적 메서드에서 인스턴스 변수에 직접적으로 접근 가능
  • 따라서 main 메서드에서 인스턴스 변수인 fibo에 접근하려고 하면 컴파일 오류가 발생
  • fibo 배열을 static으로 선언하여 클래스 수준에서의 변수로 만들면 모든 객체가 이 변수를 공유 가능

 

피보나치 수열을 for문으로 구하기 

public class Main {	
	public int[] solution(int n) {
		int[] answer = new int[n]; 
		answer[0]=1; 
		answer[1]=1;
		for( int i=2; i<n; i++ ) {
			answer[i]=answer[i-2]+answer[i-1]; 
		}
		return answer; 
	}

	public static void main(String[] args) {
		Main T = new Main();
		Scanner sc = new Scanner(System.in); 
		int n = sc.nextInt();
		for(int x : T.solution(n)) {
			System.out.print(x+ " ");
		}
	}
}

 

 

피보나치 수열 구현 시 재귀함수보다 for문을 사용하는 것이 더 성능이 좋은 이유

  1. 스택 프레임이 생성되지 않으므로 스택 오버플로우 문제가 발생하지 않는다.
  2. 반복문을 사용하면 메모리 사용량이 증가하지 않으며, 각 항을 한 번만 계산하므로 중복 계산 문제도 발생하지 않는다.따라서 성능 면에서도 반복문을 사용하는 것이 더 효율적이다.
  3. 일반적으로 더 직관적이고 이해하기 쉽다. 

 

 

 

 

 

 

 

참고 자료 출처 : https://www.inflearn.com/course/lecture?courseSlug=%EC%9E%90%EB%B0%94-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EB%AC%B8%EC%A0%9C%ED%92%80%EC%9D%B4-%EC%BD%94%ED%85%8C%EB%8C%80%EB%B9%84&unitId=72765&category=questionDetail

https://www.inflearn.com/course/lecture?courseSlug=%EC%9E%90%EB%B0%94-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EB%AC%B8%EC%A0%9C%ED%92%80%EC%9D%B4-%EC%BD%94%ED%85%8C%EB%8C%80%EB%B9%84&unitId=72770&tab=curriculum

'코딩테스트 및 알고리즘' 카테고리의 다른 글

DFS 부분 집합 구하기  (0) 2024.02.21
DFS  (0) 2024.02.20
결정 알고리즘 2  (0) 2024.02.18
결정 알고리즘  (0) 2024.02.16
이분 검색  (0) 2024.02.15