Docsity
Docsity

Prepare for your exams
Prepare for your exams

Study with the several resources on Docsity


Earn points to download
Earn points to download

Earn points by helping other students or get them with a premium plan


Guidelines and tips
Guidelines and tips

Recursion in Programming: Concepts and Examples, Lecture notes of Algorithms and Programming

The concept of recursion in programming, providing definitions, examples, and a comparison between recursion and iteration. It covers direct and indirect recursion, base and general cases, and the implementation of recursive algorithms in sorting, searching, and factorial calculations. It also explores the use of recursion in solving problems such as the tower of hanoi and combinations.

Typology: Lecture notes

2017/2018

Uploaded on 10/23/2018

keehwan
keehwan 🇨🇦

20 documents

1 / 12

Toggle sidebar

This page cannot be seen from the preview

Don't miss anything!

bg1
- 115 -
제9장 재귀 프로그래밍
장에서는 재귀 프로그래밍에 대해 살펴본다.
9.1. 교육목표
장의 교육목표는 다음과 같다.
교육목표
재귀
재귀 프로그래밍
프로그래밍: 문제를 작은 문제로 분해하여 해결하는 매우
강력하고 유용한 문제 해결 방법이다. 이때작은문는원문
동일한 종류이다.
반복(iteration) 에사할수있는방
factorial, 조합, 하노이 타워
배열로 구현된 정렬 리스트에서 이진 검색
정렬 연결 리스트에서 역순 출력
정렬 연결 리스트에서 삽입
9.2. 재귀 기초
재귀(recursion)
재귀
재귀 호출
호출: 호출하는 메소드와 호출받는 메소드가 같은 메소드 호출
직접
직접 재귀
재귀(direct recursion): 자기 자신을 바로 호출하는 메소드
간접
간접 재귀
재귀(indirect recursion): 이상의 메소드 호출이 최초의
호출 메소드으로 되돌아 오는 경우
재귀의 : factorial
8.1)
8.1) 4!=4±3±2±1=24
factorial 정의를 다시 표현하면
이와 같이 자신의 작은 버전으로 자신을 정의하는 정의를
재귀
재귀 정의
정의(recursive definition)라한.
1, if 0
!(1) 1if 0
n
nnn n
=
=×−×× >
"
1, if 0
!(1)!if 0
n
nnn n
=
=×− >
base case: 재귀가 아닌 경우
general case
재귀 호출(recursive call)이란 호출하는 메소드와 호출 받는 메소드가 같은 메소드 호출을
말한다. 재귀 호출은 크게 직접 재귀와 간접 재귀로 나뉠 있다. 직접 재귀는 자기 자신
바로 호출하는 메소드를 말하며, 간접 재귀는 이상의 메소드 호출이 최초의 호출
소드로 되돌아오는 경우를 말한다. 재귀 프로그래밍을 설명할 가장 많이 사용하는 예제
pf3
pf4
pf5
pf8
pf9
pfa

Partial preview of the text

Download Recursion in Programming: Concepts and Examples and more Lecture notes Algorithms and Programming in PDF only on Docsity!

재귀재귀 프로그래밍프로그래밍 : 문제를 작은 문제로 분해하여 해결하는 매우 강력하고 유용한 문제 해결 방법이다. 이 때 작은 문제는 원 문제와 동일한 종류이다. 반복 (iteration) 대신에 사용할 수 있는 방법 factorial, 조합 , 하노이 타워 배열로 구현된 정렬 리스트에서 이진 검색 정렬 연결 리스트에서 역순 출력 정렬 연결 리스트에서 삽입

재귀(recursion)

재귀재귀 호출호출 : 호출하는 메소드와 호출받는 메소드가 같은 메소드 호출 직접직접 재귀재귀 (direct recursion):^ 자기 자신을 바로 호출하는 메소드 간접간접 재귀재귀 (indirect recursion): 둘 이상의 메소드 호출이 최초의 호출 메소드으로 되돌아 오는 경우 재귀의 예 : factorial

예예 8.1)8.1) 4!=4 ±^3 ±^2 ± 1=factorial 의 정의를 다시 표현하면

이와 같이 자신의 작은 버전으로 자신을 정의하는 정의를 재귀재귀 정의정의 (recursive definition) 라 한다.

! 1,^ if^0 ( 1) 1 if 0 n n n n n = ⎨⎧ = ⎩ ×^ −^ ×^ "×^ >

! 1,^ if^0 ( 1)! if 0 n n n n n = ⎨⎧ = ⎩×^ −^ >

base case: 재귀가 아닌 경우 general case

재귀 호출(recursive call)이란 호출하는 메소드와 호출 받는 메소드가 같은 메소드 호출을 말한다. 재귀 호출은 크게 직접 재귀와 간접 재귀로 나뉠 수 있다. 직접 재귀는 자기 자신 을 바로 호출하는 메소드를 말하며, 간접 재귀는 둘 이상의 메소드 호출이 최초의 호출 메 소드로 되돌아오는 경우를 말한다. 재귀 프로그래밍을 설명할 때 가장 많이 사용하는 예제

n=4 n=3 n=2 n=1 n= factorial(4) 24

4

6

3

2

2

1

1

1

0

non-recursive 보통 반복문 (for, while) 방법 으로 구현

recursive 보통 선택문 방법 (if, switch) 으로 구현 함수 호출이 많이 이루어진다.

public static int factorial(int n){if(n==0) return 1; } else return (n * factorial(n-1));

public static int factorial(int n){int val = 1; for(int i=2; i<=n; i++) val = val * i;return val; }

결하는 경우가 많다. 이런 형태의 문제 해결법을 분할정복법(divide-and-conquer)이라 한다. 문제를 분할하였을 때 작은 문제가 원 문제와 동일한 형태이면 재귀 방법을 사용하여 해결 할 수 있다. 재귀 프로그래밍은 보통 선택문을 사용하여 구현되며, 조건에 따라 재귀 호출 을 하거나 기저 경우를 실행하게 된다. 보통 재귀 방식으로 프로그래밍되어 있는 것은 반복 문을 사용하여 해결할 수도 있다. 두 가지 방법을 비교하였을 때 반복문을 사용하는 방식이 효율성 측면에서는 보통 우수하다. 이것은 재귀 방식에서는 함수 호출이 반본적으로 많이 일어나기 때문이다. 하지만 반복문을 사용하여 쉽게 해결하지 못하는 문제를 재귀 방법을 사용하면 매우 편리하게 해결할 수도 있다. 보통 재귀 프로그래밍에서는 기저 경우에 도달 할 때까지 계속 함수 호출만 일어나며, 기저 경우에서 최초 호출까지 되돌아오는 과정에서 실제 필요한 계산이 수행된다.

search의 재귀 버전

UnsortedListsearch base case (1) element[startPosition] 에 찾고자 하는 것이 있으면 true 를 반환한다. (2) startPosition==size-1 이고 element[startPosition] 에 찾고자 하는 것이 없으면 false 를 반환한다. general case: 남은 부분 (startPosition 에서 끝까지 ) 을 검색한다. … … … 이미 검색한 부분 검색해야 할 부분 startPosition

search의 재귀 버전 – 계속

public boolean search(Object item) throws ListUnderflowException { if(isEmpty()) return new ListUnderflowException(“…”); } return search(item, 0); private boolean search(Object item, int startPosition){ if(item.equals(element[startPosition])) return true;else if(startPosition==(size-1)) return false; else return search(item, startPosition+1); }

사용자는 기존처럼 메소드를 호출한다 (^) .search(Object item)

public boolean search(Object item) throws ListUnderflowException {if(isEmpty()) return new ListUnderflowException(“…”); for(int i=0; i<size; i++) if(item.equals(element[i])) return true; } return false; … … … 1 2 3

배열을 이용한 비정렬 리스트 구현에서 검색 연산을 재귀적으로 작성할 수 있다. 하지만 이 경우는 각 재귀 호출에서 문제의 크기가 하나만 축소되므로 찾고자 하는 것이 배열에 없으 면 총 n번 재귀호출이 일어난다. 따라서 재귀적으로 구현할 수 있지만 이 예는 재귀보다는 반복문으로 해결하는 것이 보다 효과적이다.

조합(Combination)

순열순열 (permutation): n 개 중 r 개를 비복원추출하여 어떤 순서로 나열하는 경우의 수 조합조합 (combination):^ 순서와 상관없이^ n 개의 요소에서^ r 개를 비복원추 출하는 경우의 수 조합의 재귀적 정의

! !!! n C^ r^ n^ Pr^ n = (^) r = (^) r nr

n Pr^ =^ n^ ×^^ (^ n^ −^ 1)^ ×^ "×^ (^ n^ −^ r + 1)

1 1 1

if 1 1 if if 1

n r n r n r

n r C n rC^ − − C^ n^ r

⎧⎪ = = (^) ⎨ = ⎪⎩ (^) + > >

( ) ( ) ( ) ( ) ( )

(^1 1 1) ( (1)!^ 1)!^!! (^ 1)!1! ( 1)! 1 1 ( 1)!! ( 1)! 1! ( 1)! 1! ( )!!

n Cr^ n Cr^ n C^ r r n^ n r r nn r n n n n r n r r n r r n r r n r r n r

= (^) − − + (^) − = (^) − −^ − + −− − = −^ ⎛⎜^ + ⎞⎟ = − ⎛⎜^ ⎞⎟= − − − (^) ⎝ − (^) ⎠ − − − (^) ⎝ − (^) ⎠ −

C(3,2)

combination(4,3)

3

1 C(3,3)

1 C(2,2)

2 C(2,1)

public static int combination(int n, int r){ if(r==1) return n;else if(n==r) return 1; else return (combination(n-1,r-1)+combination(n-1,r)); }

하노이 타워 알고리즘 단계단계 1.1. n-1 개의 링을 peg 1( 시작 peg) 에서 peg 2( 보조 peg) 로 옮겨라. 단계단계 2.2. n 번째 링을 peg 3( 목적 peg) 으로 옮겨라. 단계단계^ 3.3. peg 2( 보조^ peg) 에 있는^ n-1 개의 링을^ peg 3( 목적^ peg) 으로 옮겨라. public static void doTowers(int numRings, int begPeg, int auxPeg, int endPeg){ if(numRings>0){doTower(numRings-1, begPeg, endPeg, auxPeg); system.out.println(“Move ring from peg ”+ begPeg + “ to peg ” + endPeg); } doTower(numRings-1, auxPeg, begPeg, endPeg); }

위 프로그램을 수행하면 n개의 링으로 구성된 하노이 타워 문제를 해결하는 방법을 출력하 여 준다.

Base case (1) first>last Æ false 를 반환 (2) item.compareTo(element[mid])==0 Æ true 를 반환 General case (1) item.compareTo(element[mid])<0 Æ search(first,mid-1) (2) item.compareTo(element[mid])>0 Æ search(mid+1,last)

public boolean search(Object item){ if(isEmpty()) return new ListUnderflowException(“…”);Comparable x = (Comparable)item; return search(x, 0, size-1); } private boolean search(Comparable item, int first, int last){if(first>last) return false; else{ int mid = (first+last)/2;int comp = item.compareTo(element[mid]); if(comp==0) return true; else if(comp<0) return search(item, first, mid-1); } else return search(item, mid+1,last); }

정확하게 표현하면 최악의 경우 n번의 재귀 호출이 필요하였다. 이진 검색을 재귀 방법을 구현하면 최악의 경우 log 2 n번의 재귀 호출이 필요하므로 이와 같은 경우에는 재귀 호출에 따른 오버헤드(함수 호출 비용)가 문제가 되지 않는다.

정렬 연결 리스트에서 역순으로 출력

A B C D E

list

(^2) 이것을 출력 : E,D,C,B, A (^1) 이 부분을 역순으로 출력 : E,D,C,B

public void PrintReverse() { revPrint(list); } private void revPrint(ListNode node){if(node != null){ revPrint(node.next); } System.out.println(“ ” + node.info); }

은 LIFO 구조이므로 이 특성을 사용하면 쉽게 역순으로 출력하는 메소드를 작성할 수 있다.

먼저 리스트의 각 노드를 방문하면서 방문하는 순서대로 스택에 요소들을 push한다. 그 다 음에 리스트의 모든 노드를 방문하였으면 스택에서 차례대로 pop하여 출력하면 역순으로 출력된다. 재귀적으로 이 문제를 고려하면 문제를 다음과 같이 다시 서술할 수 있다. 첫 노드를 제외한 나머지 노드들을 역순으로 출력한다. 첫 노드를 출력한다. 첫 번째 부분은 원 문제의 작은 버전이므로 위 서술을 이용하여 쉽게 역순으로 출력하는 재 귀 방식의 프로그램을 작성할 수 있다. 이처럼 재귀적으로 구현하기는 쉬우나 반복 방식으 로 구현하는 것이 어려운 문제도 있다.

private ListNode insert(ListNode subList, Comparable item){ if(subList==null || item.compareTo(subList.info)<0){ListNode newNode = new ListNode(); newNode.info = item; newNode.next = subList;return NewNode; } else{ subList.next = insert(subList.next, item);return subList; } }public void insert(Object item){ if(item==null) Comparable x = (Comparable)item;return new NullPointerException(“…”); list = insert(list, x); }

정렬 리스트에서 삽입 Base Case: (1) 부분 리스트가 비어있으면빈 리스트에 삽입 (2) 삽입하고자 하는 요소가 부분 리스트의 첫 요소보다작으면 리스트 맨 앞에 삽입 General Case:insert(sublist.next, item)

존 작업 공간은 스택에 push되고 또 다른 작업 공간이 할당되어 사용된다. 이렇게 하여 기 저 경우에 도달되어 현재 메소드가 종료되면 스택에서 작업 공간을 차례대로 하나씩 가지고 오면서 처리된다.

재귀 방식으로 구현된 메소드를 재귀 호출을 사용하지 않도록 바꾸는 방법 방법방법 1.1. 반복문 사용 모든 경우에 반복문을 바꿀 수 있는 것은 아니다. 꼬리꼬리 재귀재귀 (tail recursion) 이면 쉽게 바꿀 수 있다. 방법방법 2.2. 스택 사용 리스트의 역순 출력 : 리스트의 각 노드를 차례대로 방문하면서 노드의 요소 값을 스택에 push 한 다음에 스택에서 하나씩 pop 하여 출력하면 역순으로 출력할 수 있다.

재귀 Æ 반복

꼬리 재귀 : 메소드 내에 모든 재귀 호출이 메소드의 마지막 문장인 경우 base case 가 되면 반복이 종료되도록 반복문을 구성하여 쉽게 재귀를 제거할 수 있음 이 때 루프 변수는 재귀 호출에서 값이 변하는 인자 꼬리 재귀가 아니면 쉽게 바꿀 수 없다. boolean search(Comparable item, int start){if(item.compareTo(element[start])==0) else if(start==size-1)return true; else return search(item, start+1);return false; }

boolean search(Comparable item){int loc = 0; boolean found = false;while(loc<size && !found){ if(item.compareTo(element[loc])==0)found = true; } else loc++; } return found;

(tail recursion)라 한다. 메소드의 마지막 문장이라는 것은 물리적으로 그 메소드의 마지막 문장이라는 것이 아니라, 그 문장 이후에 더 이상 추가로 수행해야 하는 문장이 없는 경우 를 말한다. 꼬리 재귀인 경우에는 기저 경우에 반복이 종료되도록 반복문을 구성하여 쉽게 반복문을 사용하는 버전으로 바꿀 수 있다. 이 때 재귀 호출에서 값이 변하는 인자를 반복 문의 제어 변수로 사용한다.

두 가지 측면 : 명확성 , 효율성 명확성 : 재귀가 보통 알고리즘을 이해하기가 더 쉽다. 예예 8.2)8.2) 역순으로 정렬 연결 리스트 출력 효율성 : 재귀가 당연히 공간과 시간 측면에서 모두 나쁘다. 절대적인 것은 아님 : 문제 , 컴퓨터 , 컴파일러에 따라 다를 수 있다. 예예 8.3)8.3) factorial(n): n+1 개의^ stack frame 이 필요^ Æ^ O(n) 문제가 본질적으로 재귀에 맞지 않을 수 있다. Æ 조합 : 같은 계산이 무수히 반복 Æ O(2N) C(5,3) C(4,2) C(4,3) C(3,1) C(3,2) C(3,2) C(3,3) C(2,1) C(2,2) C(2,1) C(2,2)