Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1주차 과제 풀이 제출 #9

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open

Conversation

JohnPrk
Copy link

@JohnPrk JohnPrk commented Oct 15, 2023

소감

  • 자바 개발자라서 윤석님께 허락을 구하고 자바로 풀이를 작성했습니다.
  • 이번 주에 회사에 이슈가 많아서 시간을 많이 할애하지 못한게 조금 아쉽습니다.
  • 먼저는 구현할 수 있는데까지 구현 하는 것을 목표로 했기에, 가장 쉬운 방법으로 구현하고 재귀 -> 꼬리 재귀 -> 꼬리 재귀 최적화 순으로 할 수 있는만큼 코드를 작성했습니다.
  • 예시 케이스가 많지는 않지만, 이를 테스트하기 위해서 내장 JUnit5를 사용했습니다.
  • 추후에 회고를 작성할 예정이고, 제가 풀었던 부분과 풀이 영상 및 코드 리뷰를 보고 느꼈던 것이나 수정했던 부분을 포함한 내용을 회고 내용을 다룰 것입니다.

풀이 설명 및 궁금증

1번 문제

  • 재귀를 사용하기 좋은 조건이 '알고리즘이 얼마나 깊이 들어가야 하는지 모르는 상황에서 문제를 여러 단계로 파고들어야 하는 경우'와 '하위 문제의 계산 결과에 기반해 계산할 수 있는 경우'라고 할 수 있는데, '1주차 첫 번째 문제에서 배열의 총합을 구하는 경우는 2번 조건에 맞는가?'라는 고민이 들었습니다.
  • 그래서 가장 쉬운 방법은 for문과 전역 변수를 통해서 합연산을 진행했고, 2번 조건을 만족하는 재귀를 구현하기 위해서 재귀를 사용하는 분할정복(solution2_1 메서드)을 사용 했습니다. 하지만 '분할정복은 꼬리재귀를 만들 수 없겠다'라는 생각을 했고, 맞는지는 모르겠지만 일반적인 재귀(solution2_2)를 사용해서 구현하고 꼬리 재귀와 꼬리 재귀 최적화를 구현 했습니다.
  • 하지만, 과제를 진행 하면서 '1번 문제가 과연 재귀로 풀 수 있는 좋은 문제인가?'라는 궁금증이 들었습니다.
  • 그리고 재귀를 사용하는 것이 콜스택으로 인해서 메모리적으로나 연산적인 측면에서 좋지 않다는 생각이 들었는데, 사실 컴퓨터는 매우 빠르고 캐시(공간 지역성) 때문에 정확히 이들 간에 얼마나 차이가 나는지 알 수 없다고 느꼈습니다. 혹시 이를 측정할 수 있거나 어느정도의 차이가 있는지 알 수 있는 방법이 있을까 궁금 했습니다.

2번 문제

  • 피보나치 문제는 재귀로 풀기에 좋은 문제라고 생각 했습니다.
  • 다만, 중복된 연산을 없애기 위해 상향식 다이나믹 프로그래밍을 사용해서 구현을 했습니다.

3, 4번 문제

  • 3번과 4번 문제는 2로 나눈 나머지를 통해서 구현하는 것은 가능 했으나, 이를 재귀로 어떻게 풀어야할지 답을 찾지 못했습니다.
  • 재귀로 구현하기 위해서는 하나의 작업이 하위의 여러 개의 작업의 연산으로 이루어진 것으로 이해를 했는데, 그러면 '2로 나눈 나머지가 0보다 작아질 때까지 찾는 작업을 재귀로 바꿔야하나?'라는 생각이 들었습니다.
  • 하지만, 이 것은 상위 함수를 하위 함수를 호출하는 반복적인 작업이 아니라는 느낌이 아니라 상위 함수에 대한 연산 작업이라고 느꼈습니다.
  • 다만, 이 내용을 적다보니 상위 함수에서 연산한 값을 하위 함수로 넘겨서 처리할 수 있다는 생각은 들긴 하는데, 그러면 연산한 값과 몫은 어떻게 함께 하위 함수로 전달하고 보관할까?라는 고민이 들었습니다.

5번

  • 유클리드 호제법 역시 3, 4번 문제와 마찬가지로 구현하는 것은 설명 그대로 구현을 했으나 이를 재귀로 어떻게 풀어야할지 답을 찾지 못했습니다.

6번

  • 다이나믹 프로그래밍의 경우, 직접 케이스들을 해보면서 규칙성을 찾는 것이 중요하다고 생각 했는데, 시간이 부족해서 규칙성을 찾지도 못했고, 이를 재귀로는 어떻게 풀어야할지 감조차 못 잡은 것 같습니다.

@hannut91
Copy link
Contributor

'1번 문제가 과연 재귀로 풀 수 있는 좋은 문제인가?'

재귀적인 사고를 연습하는 단계라서 우리가 흔하게 반복문으로 푸는 문제를 재귀적인 방법으로 바라보는 연습을 한 것으로 생각하시면 될 것 같습니다.

상위 함수에서 연산한 값을 하위 함수로 넘겨서 처리할 수 있다는 생각은 들긴 하는데, 그러면 연산한 값과 몫은 어떻게 함께 하위 함수로 전달하고 보관할까?

일반적인 재귀 함수에서는 함수 호출 스택에 값을 저장하고 있고, 꼬리 재귀 같은 경우에는 파라미터로 값을 계속해서 전달하여 값과 목을 하위 함수로 전달합니다.

@hannut91
Copy link
Contributor

hannut91 commented Oct 16, 2023

그리고 재귀를 사용하는 것이 콜스택으로 인해서 메모리적으로나 연산적인 측면에서 좋지 않다는 생각이 들었는데, 사실 컴퓨터는 매우 빠르고 캐시(공간 지역성) 때문에 정확히 이들 간에 얼마나 차이가 나는지 알 수 없다고 느꼈습니다. 혹시 이를 측정할 수 있거나 어느정도의 차이가 있는지 알 수 있는 방법이 있을까 궁금 했습니다.

재귀를 사용하는 것이 더 느릴 수 밖에 없습니다. 추가적인 메모리를 할당하고 해제하기 때문에요. 그래서 우리가 재귀적인 사고로 문제를 해결 -> 꼬리 재귀로 변경 -> 꼬리 재귀 최적화로 재귀를 제거 하는 순서로 문제를 풀어야 합니다.

Comment on lines +18 to +27
public int solution2_1(int[] input) {
if (input.length == 0) {
return 0;
}
if (input.length == 1) {
return input[0];
}
int mid = input.length / 2;
return solution2_1(Arrays.copyOfRange(input, 0, mid)) + solution2_1(Arrays.copyOfRange(input, mid, input.length));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2개씩 나누면서 1개가 될 때까지 나누어서 더하는 방식이군요 ㅎㅎ 저는 생각 못 했던 방식이네요!

Comment on lines +37 to +42
private int plus_1(int input, int output) {
if (input == 0 || input == 1) {
return input;
}
return output + plus_1(input - 1, output) + plus_1(input - 2, output);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

output이 처음에 0으로 주어지고, 재귀적인 호출에도 같은 output을 계속 넘겨주기 때문에 0에서 변하지 않네요. 그래서 위에서 구현했던 solution2랑 같은 결과가 나오게 되겠어요. 따라서 output은 없어도 되겠네요

Comment on lines +44 to +51
private int plus_2(int input, int output) {
while(true) {
if (input == 0 || input == 1) {
return input;
}
return output + plus_2(input - 1, output) + plus_2(input - 2, output);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아마 꼬리재귀 최적화를 시도하셨던 것 같네요. 꼬리 재귀 최적화가 된 결과는 solution1과 동일하긴 해요. 그전에 꼬리재귀 최적화가 된 모양은 다음과 같습니다.

  public int _solution(int input, int count, int a, int b) {
    if (input == 0 || input == 1) {
      return input;
    }

    if (input == count - 1) {
      return b;
    }

    return solution(input, count + 1, b, a + b);
  }

  public int solution(int input) {
    return _solution(input, 0, 0, 1);
  }

Copy link

@kyupid kyupid Jan 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hannut91
안녕하세요. 코드숨 공개 레포로 개인공부하고 있는 개발자입니다~ 먼저 좋은 학습자료 올려주셔서 감사하다는 인사의 말씀 드립니다.

저는 자바로 공부하고 있는데 꼬리 재귀 최적화는 현재 자바에서 지원되지 않습니다.

https://blogs.oracle.com/javamagazine/post/curly-braces-java-recursion-tail-call-optimization

I spoke to Brian Goetz, the Java language architect at Oracle, about TCO back in 2014. At the time, he told me that it’s on a list of things to be added to the JVM. I caught up with him recently (이게 블로그 글로 미루어보아 2022년인거 같습니다) to discuss the topic, and he said that while progress has been made, and more may occur in the future, TCO hasn’t been fully implemented due to some security concerns.

아래 링크를 확인해보시면 workaround가 있는 것 같긴합니다.
https://stackoverflow.com/a/53787951
https://github.com/jonckvanderkogel/java-tail-recursion

Copy link
Contributor

@hannut91 hannut91 Jan 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kyupid 맞습니다. 꼬리 재귀 최적화가 된 결과가 아니라 꼬리 재귀로 작성된 이라고 정정해야겠네요! 제보 감사합니다 ㅎㅎ
자바에서 꼬리 재귀 최적화를 지원하지 않으므로, 직접 꼬리 재귀 최적화를 해야 합니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants