-
CS50 2019 (10)recursion, 쌓이는 것CS50 2022. 7. 16. 16:31
===
모두를 위한 컴퓨터 과학 (CS50 2019)
부스트코스 무료 강의
www.boostcourse.org
1.컴퓨팅사고
2.C언어
3.배열
4.알고리즘
5.메모리
6.자료구조
=====================
7)재귀
h가 3, 2, 1일때 draw(2), draw(1), draw(0)으로 실행된다. draw(0)이 실행되고 바로 return될 뿐 최다득표자를 선거의 승자로 하거나 결선 투표로 하거나 여러명을 고르게 하거나 등의 알고리즘이 있다 생활에서도 다양하게 쓰이고 있다는 말
수많은 정렬 알고리즘이 존재한다 중요한 몇가지만 볼 것, O(n^2)보다 더 나은
이제부터 컴퓨터 공학을 배우면서 여러분의 생각을 흔들어 버릴 수많은 개념 중 하나를 소개할 것
목표는 경험해보는 것 다양한 개념을 적용하다보면 점차 익숙해질 것
===
재귀의 핵심 개념을 위해 예제를 하나 볼 것.
첫주에 했던 것, 그땐 반복문을 강조했었다.
이 알고리즘을 좀 다르게, 더 좋게 만들 수 있지 않을까? 물론 trade off가 있다
여기서 노란색의 반복문을 지워보고 생각해보자
남은것중에 Open to moddle of left(or right) ~ 의 이유는 남은 절반에서 찾기 위해서였음. 중요한 사실은 전화번호부의 크기를 절반으로 줄인다는 것. 알고리즘은 사실 똑같으니 의사코드를 저렇게 이것저것 다시하라고 하지 말고
저 화면의 코드는 Search라는 함수다.
재귀 함수는 내부에서 자기 자신을 호출합니다
뭔말인지 모를 수도 있지만 곧 자세히 배울 것임
저 함수 이름이 Search이고, Search left(right) half of book이라는 줄에서 Search함수를 반복적으로 호출하기 때문에 재귀
특정 줄로 가라고 할 필요 없이 무엇을 할지만 알려주기. 왼편 탐색해 오른편 탐색해라고 해보자.
순환논리처럼 보일 수도 있다. Mike Smith를 어떻게 찾아? Mike Smith를 찾아 하는것과 같은데
중요한 점은 단순히 같은 것을 반복시키는 게 아니라(반복문 말하는거겠지)
Mike Smith를 이렇게 큰 전화번호부에서 찾는 데 없다면 더 작은 크기에서 다시 찾아보라고 하며
알고리즘 다음 단계에서의 문제의 크기를 계속 절반으로 줄여나가는 겁니다. --이거를 설명하려고 저 내용으로 수업하심
이것이 재귀라는 프로그래밍 기법의 예시이다.
여러분의 프로그램이나 알고리듬이 스스로를 계속 호출하는 것
===
이걸 만드려면?
iteration.c --반복 이라는 뜻
*do-while은 제대로 입력하는지 확인할 때, 오늘은 안쓸게요 올바른 입력이 주어진다 가정하고
draw함수를 만들 것
값을 반환 안하고 출력만 하므로 void draw --구분하라고 직관에
입력으로 숫자 n을 받음 void draw(int h)
의사코드와 비슷하게 하기 위해 i를 1로 초기화하고(0부터 시작하는 것과 똑같지만 때로는 지금처럼 1부터 시작하는 것도 좋습니다)
i가 높이보다 작거나 같으면 계속하기 --1로 하면 작거나 '같다'를 해줘야 h만큼
만약 지금처럼 2차원에서 무언가를 하려 한다면 저처럼 중첩 반복문을 쓸 것입니다
h를 받으면
h번 반복할건데(세로)
반복마다 i가 1부터 증가하고, 그 i만큼 가로로 찍어내는 --▽보통 그 상황에서 변수들의 숫자가 뭐냐를 활용해야, 첫번째 줄일 째 한번 반복 두번째 줄일 때 두번 반복이니까 이걸 활용해야겠지
잘 된다!중첩 반복문으로 마리오 피라미드를 성공적으로 구현헀다
지금까지 본 문제들처럼 마리오 피라미드에는 흥미로운 점이 있어
이게 흔한 피라미드인가? 높이 4의 피라미드를 어떻게 정의하나? 분명 높이 3의 피라미드에 한 줄을 추가한것임
높이 3은 높이 2에서
높이2는 높이 1에서 한줄을 추가한 것
이것을 재귀적 정의라고 한다!!!
눈에 보이는 혹은 가상의 물체의 구조를 그 물체의 자체를 이용해서 설명하는 것을 말한다
특수한 경우 높이 1은 높이 0의 뭐야 아무것도 아니야
-> 반환, 종료 혹은 정지 등 알고리듬에 맞게 처리를 해야함
시작점에 대해서는 여러분들이 직접 이건 특별한 경우이므로 아무것도 하지 말고 재귀적으로 자기 자신을 호출하지 않도록 알려줘야 합니다
: ▽시작점은 특별하게 대해야 한다는 이런 것도 다 알려주네
===
recursion.c
이번엔 중첩 반복문을 사용하지 않고 작은 피라미드로 큰 피라미드를 정의해볼 것
(프로토타입 이따가 마지막에 적음)
4짜리 그리려면 제일 먼저 뭘 해야 할까? 3짜리를 그리면 된다, 어떻게 코드로 적나??
그리고 이러면 무한 반복호출되고 있어 3, 2, 1, 0, -1, -2.. 처럼 빼기만 하면 안끝나
어떤 조건에서 그리지 말아야 할지(=시작점)의 조건을 줘야 한다
하드 코딩된 조건문으로 의도치 않은 반복을 막는 것
그리고
높이4의 피라미드를 그릴 땐
(1)높이 3의 피라미드를 그리고
(2)한 줄 추가한다
draw(h-1)가 피라미드를 그려주는 애라고 생각하면
이제 여기에 한 줄을 추가로 그려주는 부분을 구현해야 한다
h가 4 일 때 4개의 #을 그리는 반복문
반복문이 필요하지만, 한 번이면 충분합니다
(이번 강의에서 이런거 엄청 많이 알려주네 이렇게 하는거 맞아? 응 맞아 식의 중의중에 정해주는 설명)
여기까지의 코드가 놀랍게도 다야.
명시적으로 출력한건 한줄에 해당하는 벽돌뿐인데!!
▽
draw(h-1)이 draw(h-2)랑 한줄 출력으로 변한다
draw가 전부 바뀌면
h=0일때 return되고
h=1일때의 출력
h=2일때의 출력
h=3일때의 출력
h=4일때의 출력
이 됨
(내 생각)머릿속에 프랙탈이 되고 있는 코드 내용을 상상하자 그 안에서 계속 코드가 있는 것임 인자만 -1되면서
재귀적으로 스스로를 호출하면서
이진 탐색이나 분할 정복법에서처럼
기존 문제보다 더 작은 크기의 문제를 풀어갑니다
3개가 필요한거군
시작점 조건
재귀함수
한줄추가
문제를 조금씩 먹어가는것
몇 주 후 스택을 배우면서 이 마법이 어떻게 가능한지 볼 것임
우선은 함수가 초기에 주어진 입력보다 더 작은 입력을 갖고 계속해서 자신을 호출한다는 것만 이해합시다
===
네이버 :
재귀
함수를 사용할 때 어디에서 호출했나요? main 안에서 프로그램을 작성하면서 필요한 순간에 호출하여 사용합니다.
그런데 main 역시 함수라는걸 기억하시나요? main이라는 함수 안에서 또 다른 함수를 사용한 것입니다.
이 사실을 알게 되었을 때, 우리는 함수가 본인 스스로를 호출해서 사용할 수 있는지에 대해 의문을 가질 수 있습니다.
이에 대한 대답은 할 수 있다 이며, 이러한 것을 재귀(Recursion)라고 부릅니다.
아래와 같이 피라미드 모양을 출력하기 위해 다음과 같은 코드를 작성할 수 있습니다
#
##
###
####
#include <cs50.h> #include <stdio.h> void draw(int h); int main(void) { // 사용자로부터 피라미드의 높이를 입력 받아 저장 int height = get_int("Height: "); // 피라미드 그리기 draw(height); } void draw(int h) { // 높이가 h인 피라미드 그리기 for (int i = 1; i <= h; i++) { for (int j = 1; j <= i; j++) { printf("#"); } printf("\n"); } }
높이를 입력 받아 중첩 루프를 통해 피라미드를 출력해주는 draw 함수를 정의한 것이죠.
여기서 꼭 중첩 루프를 써야만 할까요? 사실 바깥 쪽 루프는 안 쪽 루프에서 수행하는 내용을 반복하도록 하는 것일 뿐입니다.
따라서 바깥 쪽 루프를 없앤 draw함수를 만들고, 이를 ‘재귀적으로’ 호출하도록 해서 똑같은 작업을 수행할 수 있습니다.
즉, draw 함수 안에서 draw 함수를 호출 하는 것이죠. 아래 코드와 같이 수정할 수 있습니다.
#include <cs50.h> #include <stdio.h> void draw(int h); int main(void) { int height = get_int("Height: "); draw(height); } void draw(int h) { // 높이가 0이라면 (그릴 필요가 없다면) if (h == 0) { return; } // 높이가 h-1인 피라미드 그리기 draw(h - 1); // 피라미드에서 폭이 h인 한 층 그리기 for (int i = 0; i < h; i++) { printf("#"); } printf("\n"); }
draw 함수 안에서 draw 함수를 다시 호출 하는 부분을 유의하시기 바랍니다.
h라는 높이를 받았을 때, h-1 높이로 draw 함수를 먼저 호출하고, 그 후에 h 만큼의 #을 출력합니다.(▽재귀를 할 때마다 함수의 인자가 바뀌고 h의 조건이 바뀐다) 여기서 내부적으로 호출된 draw 함수를 따라가다 보면 h = 0인 상황이 오게 됩니다. 따라서 그 때는 아무것도 출력을 하지 않도록 하는 조건문을 추가해줘야 합니다.
이렇게 재귀를 사용하면 중첩 루프를 사용하지 않고도 하나의 함수로 동일한 작업을 수행할 수 있습니다.
---
내 직관
어떤 조건 전 과 후를 나누고 후를 계속 쌓아놓을 수가 있어 조건이 터지면 그때부터 콸콸콸 재귀는 이 문제에서 볼 수 있듯이 뒤에 쌓아놓을 수가 있다
쌓아놓는다는게 뭐냐면
재귀가
h=5
1
재귀(h-1)
2
면
5-1, 4-1, 3-1, 2-1, 1-1 1-2, 2-2, 3-2, 4-2, 5-2 이런식으로 동작하겠네?
h=1
1
재귀(h+1)
2
면
1-1, 2-1, 3-1, 4-1, 5-1, 5-2, 4-2, 3-2, 2-2, 1-2 이렇고
재귀함수 앞뒤 코드로 나눠서
앞에것만 일단 h조건 다르게 실행하고
뒤에것을 거꾸로 h조건 다르게 실행가능
'CS50' 카테고리의 다른 글
CS50 2019 (12)메모리, 16진수, 진법 직관, 포인터, 문자열 (0) 2023.03.11 CS50 2019 (11)merge sort (0) 2022.07.16 CS50 2019 (9)bubble sort, selection sort (0) 2022.07.16 CS50 2019 (8)검색, Big-O, Omega, 선형 검색, 구조체 (0) 2022.07.14 CS50 2019 (7)문자열과 배열, 명령행 인자 (0) 2022.07.12