WY J
학습 공간
WY J
  • 분류 전체보기 (95)
    • Java (38)
      • 알고리즘 (5)
      • 자료구조 (4)
      • 기초 (9)
      • OOP (10)
      • Collection (3)
      • Effective (5)
      • reator (2)
    • HTML&CSS (5)
    • macOS (3)
    • Git (5)
    • Network (5)
    • MySQL (2)
    • Spring Boot (31)
      • Core (5)
      • MVC (15)
      • Security (10)
    • 알고리즘 (1)
    • Cloud (3)
      • AWS (3)
    • Docker (1)
    • Project (0)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

인기 글

최근 글

hELLO · Designed By 정상우.
WY J

학습 공간

[Java Algorithm] 재귀(Recursion)
Java/알고리즘

[Java Algorithm] 재귀(Recursion)

2022. 7. 22. 10:27

재귀 (Recursion) 함수란?

  • 특정 함수 내에서 자기 자신을 다시 호출하여 문제를 해결해나가는 함수
  • 문제를 해결하기 위해 원래 범위의 문제에서 더 작은 범위의 하위 문제를 먼저 해결함으로써 원래 문제를 해결해 나가는 방식
  • 일반 반복문 <-> 재귀 함수 쌍방으로 구현 가능함

 

장점

  • 여러개의 반복문을 사용하지 않기 때문에, 코드가 간결해지고, 수정이 용이
  • 변수를 여러개 사용할 필요가 없다.

단점

  • 반복문과 달리, 코드의 흐름을 직관적으로 파악하기 어렵다.
  • 반복하여 매서드를 호출하며 지역변수, 매개변수, 반환값을 모두 process stack에 저장하게 되어 반복문에 비해 메모리 사용이 많아진다.

사용 조건

  • 문제의 크기를 점점 작은 단위로 쪼갤수 있어야 한다.
  • 재귀 호출이 종료되는 시점(base case)이 존재해야 한다.
  • 모든 입력은 base case로 수렴해야 한다.

사용 상황

  • 주어진 문제를 비슷한 구조의 더 작은 문제로 나눌 수 있는 경우
  • 중첩된 반복문이 많거나 반복문의 중첩 횟수를 예측하기 어려운 경우
  • 변수 사용을 줄여 mutable state (변경 가능한 상태) 를 제거하여 프로그램 오류가 발생할 수 있는 가능성을 줄이는 경우

 

예시) 자연수로 이루어진 리스트(배열)를 입력받고, 리스트의 합을 리턴하는 메서드 `arrSum` 을 작성하세요.

//일반 for문 사용
public int arrSum(int[] arr) {
  int sum = 0;
  for(int i = 0; i < arr.length; i++) {
    sum += arr[i];
  }
  return sum;
}

// 1. 문제를 작게 쪼개기

arrSum([1, 2, 3, 4, 5]) == 1 + arrSum([2, 3, 4, 5])
arrSum([2, 3, 4, 5]) == 2 + arrSum([3, 4, 5])
arrSum([3, 4, 5]) == 3 + arrSum([4, 5])
arrSum([4, 5]) == 4 + arrSum([5])
arrSum([5]) == 5 + arrSum([])

// 2. 문제 해결하기

arrSum([]) == 0; // <-- 문제가 더는 작아지지 않는 순간
// 가장 작은 경우의 해결책을 적용합니다.
arrSum([5]) == 5 + arrSum([]) == 5 + 0 == 5;
arrSum([4, 5]) == 4 + arrSum([5]) == 4 + 5 == 9;
arrSum([3, 4, 5]) == 3 + arrSum([4, 5]) == 3 + 9 == 12;
arrSum([2, 3, 4, 5]) == 2 + arrSum([3, 4, 5]) == 2 + 12 == 14;
arrSum([1, 2, 3, 4, 5]) == 1 + arrSum([2, 3, 4, 5]) == 1 + 14 == 15;

// 재귀함수 사용
public int arrSum (int[] arr) {
  // 빈 배열을 받았을 때 0을 리턴하는 조건문
  //   --> 가장 작은 문제를 해결하는 코드 & 재귀를 멈추는 코드
  if (arr.length == 0) {
    return 0;
  }
  // 배열의 첫 요소 + 나머지 요소가 담긴 배열을 받는 arrSum 함수
  //   --> 재귀(자기 자신을 호출)를 통해 문제를 작게 쪼개나가는 코드
	return arr[0] + arrSum(arr);
}

 

재귀를 활용할 상황을 판별하는 방법 (위 예제를 예시로)

 

1. 재귀 함수의 입력값과 출력값 정의

  • 문제를 가장 추상적, 단순하게 정의한다.
  • arrsum: [int] -> int 이며 해설하면 int 타입을 요소로 갖는 배열을 입력으로 받고, int 타입을 리턴한다.

2. 문제를 쪼개고 경우의 수를 나누기

  • 중요한 관점은 입력값이나 문제의 순서와 크기이다.
  • arrSum(new int[]{1, 2, 3, 4}) 를 구하는 방법과 arrSum(new int[]{2, 3, 4]} 을 구하는 방법은 동일하다.

3. 단순한 문제 해결하기

  • 가장 해결하기 쉬운 문제부터 해결한다.
  • 이를 재귀의 기초(base case)라고 부르고, 재귀의 탈출 조건을 구성한다.
  • arrsum을 더 이상 쪼갤 수 없는 경우는 입력값이 빈 배열인 경우이다. 이때 arrSum(new int[]{}) 의 리턴값은 0

4. 복잡한 문제 해결하기

  • 배열 맨 앞의 요소(head)에 대한 결과를 따로 구하고, 나머지 요소(tail)를 새로운 입력값을 갖는 문제로 구분하고 이를 해결하여 얻은 결과를 head에 더한다.

 

예제

1. sumTo

수(num)를 입력받아 1부터 num까지의 합을 리턴

public class Solution { 
  public int sumTo(int num){

    if (num <= 0) {
      return 0;
    }
    else{
      return num + sumTo(num - 1);
    }
  }
}

 

2. isOdd

수를 입력받아 홀수인지 여부를 리턴

public class Solution { 
	public boolean isOdd(int num) {

    if (num == 0) return false; // 2를 num만큼 뺀 후 남은 수가 0이므로 짝수
    if (num == 1) return true;  // 2를 num만큼 뺀 후 남은 수가 1이므로 홀수

    if (num < 0) return isOdd(-num); // 음수를 입력받은 경우 -1을 곱해줌

    return isOdd(num - 2);
      // isOdd(5 - 2)
        // isOdd(3 - 2)
          // isOdd(1) -> base case로 return true;
  }
}

 

3. factorial

수를 입력받아 n-factorial(n!; 엔-팩토리얼) 값을 리턴

public class Solution { 
	public int factorial(int num){

    if (num == 0) {
      return 1;
    }
    return num * factorial(num - 1);
      // 4 * factorial(4 - 1)
        // 3 * factorial(3 - 1)
          // 2 * factorial(2 - 1)
            // 1 * factorial(1 - 1) 
          // 1 * factorial(0)         -> base case 이므로 return 1;
        // 2 * factorial(1 * 1)
      // 3 * factorial(2 * 1)
    // 4 * factorial(3 * 2)
    
    // factorial(4) = 24
  } 
}

 

4. fibonacci

수(num)를 입력받아 피보나치 수열의 num번째 요소를 리턴

public class Solution {  
	public int fibonacci(int num){

    if (num <= 1){
    return num;
    }
    return fibonacci(num - 1) + fibonacci (num - 2);
	}
}

 

 

 

5. arrSum

배열을 입력받아 모든 요소의 합을 리턴

public class Solution { 
	public int arrSum(int[] arr){

    if(arr.length == 0) return 0;

    int head = arr[0];
    int[] tail = Arrays.copyOfRange(arr, 1, arr.length);

    return head + arrSum(tail);
    // input [1, 2, 3, 4]
      // head[1] + [2, 3, 4]
        // head[1+2] + [3, 4]
          // head[1+2+3] + [4]
            // head[1+2+3+4] + [ ] -> return = 0;
  }
}

 

7. drop

수(num)와 배열을 입력받아 차례대로 num개의 요소가 제거된 새로운 배열을 리턴

public class Solution { 
	public int[] drop(int num, int[] arr){
if (arr.length == 0) { // 배열에 더 이상 제거할게 없을 경우 그대로 리턴
  return arr;
}
if (num == 0) { // 0개를 제거해야 하는 경우
  return arr;
}
// 작은 단위로 쪼갤 수 있는 경우
int head = num - 1;
int[] tail = Arrays.copyOfRange(arr, 1, arr.length);

return drop(head, tail);
  // input(2, [1, -2, 1, 3])
   // head(num - 1), tail[]
    // (2 - 1), [-2, 1, 3]
      // (1 - 1), [1, 3]
        // (0), [1, 3] -> return arr;
	} 
}

 

8. take

수(num)와 배열을 입력받아 차례대로 num개의 요소만 포함된 새로운 배열을 리턴

public class Solution { 
	public int[] take(int num, int[] arr){
    if(arr.length == 0 || num == 0) { //입력된 배열이 빈 배열인 경우, 입력된 num이 0인 경우
      return new int[]{}; //빈 배열을 반환합니다.
    }
    //배열의 가장 첫번째 요소만을 가지고 있는 head 배열을 선언, 할당합니다.
    int[] head = Arrays.copyOfRange(arr, 0, 1);

    //남은 요소를 가지고 있는 tail 배열을 선언, 할당하고, 해당 배열의 요소가 모두 제거될 때까지 재귀함수를 호출합니다.
    //한번 호출될 때마다, num을 하나씩 줄여줍니다. head 배열에 현재 선택된 요소가 있기 때문에, 앞으로 선택할 요소를 나타내는 num을 1씩 줄여서 재귀함수가 실행되어야 합니다.
    int[] tail = take(num - 1, Arrays.copyOfRange(arr, 1, arr.length));

    //재귀함수가 모두 호출된 이후에, 할당된 head배열과 tail 배열을 합친 새로운 배열을 선언, 할당합니다.
    //새로운 배열을 선언합니다. 배열의 크기는 head.length와 tail.length를 합친 크기로 선언합니다.
    int[] dest = new int[head.length + tail.length];
    
    //System.arraycopy메서드를 사용하여, head, tail 두 배열의 요소를 모두 dest 배열에 복사합니다.
    //System.arraycopy(src, srcPos, dest, destPos, length);
    System.arraycopy(head, 0, dest, 0, head.length);
    System.arraycopy(tail, 0, dest, head.length, tail.length);

    return dest;
    //input 2, [1, 2, 3, 4]
    // [head], (num - 1) [tail]
      // [1], (2 - 1) [2, 3, 4]
        // [1, 2], (1 - 1) [3, 4] -> num = 0, return new int[]{};
	} 
}

 

10. and

배열을 입력받아 모든 요소의 논리곱(and)을 리턴해야 합니다.

public class Solution {  
	public boolean and(boolean[] arr){

    if (arr.length == 0){
      return true;
    }
    boolean head = arr[0];
    boolean[] tail = Arrays.copyOfRange(arr, 1, arr.length);

    return (head && and(tail));
	} 
}

 

12. reverseArr

배열을 입력받아 순서가 뒤집힌 배열을 리턴해야 합니다.

public class Solution { 
	public int[] reverseArr(int[] arr){
      if(arr.length == 0) {
        return new int[]{};
      }
      //배열의 가장 마지막 요소만을 가지고 있는 head 배열을 선언, 할당합니다.
      int[] head = Arrays.copyOfRange(arr, arr.length - 1, arr.length);

      //남은 요소를 가지고 있는 tail 배열을 선언, 할당하고, 해당 배열의 요소가 모두 제거될 때까지 재귀함수를 호출합니다.
      int[] tail = reverseArr(Arrays.copyOfRange(arr, 0, arr.length - 1));

      //재귀함수가 모두 호출된 이후에, 할당된 head배열과 tail 배열을 합친 새로운 배열을 선언, 할당합니다.
      //새로운 배열을 선언합니다. 배열의 크기는 head.length와 tail.length를 합친 크기로 선언합니다.
      int[] dest = new int[head.length + tail.length];
      
      //System.arraycopy메서드를 사용하여, head, tail 두 배열의 요소를 모두 dest 배열에 복사합니다.
      System.arraycopy(head, 0, dest, 0, head.length);
      System.arraycopy(tail, 0, dest, head.length, tail.length);

      return dest;
	} 
}

 

'Java > 알고리즘' 카테고리의 다른 글

[Java Algorithm] LRU 알고리즘이란?  (0) 2023.02.01
[Java Algorithm] Algorithm with Math (순열 / 조합)  (0) 2022.08.01
[Java Algorithm] 탐욕 알고리즘(Greedy)  (0) 2022.07.28
[Java Algorithm] 시간복잡도(Time Complexity), Big-O 표기법  (0) 2022.07.28
    'Java/알고리즘' 카테고리의 다른 글
    • [Java Algorithm] LRU 알고리즘이란?
    • [Java Algorithm] Algorithm with Math (순열 / 조합)
    • [Java Algorithm] 탐욕 알고리즘(Greedy)
    • [Java Algorithm] 시간복잡도(Time Complexity), Big-O 표기법
    WY J
    WY J

    티스토리툴바