jeul2   6년 전

트리를 처음 접해서 짜본 문제입니다.

연결리스트로 해보려다 트리를 입력받는게 생각보다 어려워 먼저 배열로 짜게 되었는데요.

이진트리의 최악의 경우는 일자형태인 변질트리(Degenerate Tree)라고 들었습니다. 

노드개수3

A . B

B . C

C . .

노드의 개수를 받는다면 일자형태가 된다하면 레벨이 노드개수만큼 깊에 들어가게되고 이에따라 배열크기가 늘어날거라 생각해서

N<=26이므로 배열을 0부터 알차게 사용한다면 (2^26 - 1)크기의 배열이 필요할 것 같았는데 막상 해보니깐 런타임에러가 떠서

좋지못한 방법이지만 어찌어찌 정답은 맞추게 되었습니다..

아마 테스트케이스가 없는 이유는 문제와 맞지 않아서?

일자가 되는 경우는 이 문제에서 원하는 것도 아니고, 트리를 쓰는 이유도 사라지지만 제가 생각한 배열크기가 혹시 잘못되었나 해서 궁금해서 질문남깁니다.

추가로 if문 4개로 리커시브를 만들었는데 비효율적인것같다는 생각이 듭니다. 비효율적인가요?

아래 소스코드 남기고 간단히 주석달아놓았습니다.

dps2   6년 전

음....안녕하세요!

처음으로 질문 글에 답해보네요 헤헤

우선 고생하셨어요~ 한달전 질문이라 다른분들 코드보면서 아셨을 수도 있는데 일단 제가 아는한 답해드릴게요

만약 left child와 right child의 위치를 고정해둔다면 (*2 +1 ,*2+2 등) 말씀하신대로 어마어마한 크기의 배열이 필요해요. 그래서 다른 방법을 쓰셔야해요

두 가지 방법을 고안했어요

참 그리고 들어오는 입력 중에 이런게 있는지 모르겠네요

아직 부모 노드의 위치를 모르는데 자식 노드의 위치를 주어지는 경우

예)

6

A B C

D E F

B D .

C . .

E . .

F . .

저는 둘 다 이런 케이스도 풀 수 있도록 짰어요

첫번째 방법은 이게 대문자 알파벳으로만 들어온다는 점을 중요하게 생각한 방법이고요(소스코드 올려두었어요)

26개의 배열을 선언하여 순서대로 A ... Z까지 의미하고 걔네들의 자식노드의 위치를 저장해요

그리고 주어진 노드가 맞는지(isused) 체크해서 주어진 노드가 아니면 무시하고요...

이 방법의 장점은 위치를 찾을 필요가 없어요

앞서 말한 아직 부모 노드의 위치를 모르는데 자식 노드의 위치를 주어지는 경우를 대비하기 위해

입력을 전부 받아두고 자식 노드의 위치를 찾아야하지만 이 방법은 그럴 필요가 없어요

입력 받는 순서대로 저장하면 되요

그런데 이런 방법은 알파벳이나 숫자(0~9)같은 몇몇 케이스에만 적용할 수 있어요. 그래서 다른 방법을 소개시켜드릴게요

배열을 선언해서 순서대로 넣는거에요

방법은 간단하지만 이 방법은 자식 노드의 위치를 찾아서 저장하는 걸 한번 하기 때문에 그렇게 효율적이진 않아요 사실 이게 26개까지만 들어와서 괜찮지 더 많이 들어오면 O(n^2)애여서 좀 힘들어요

시간 복잡도를 줄이려면 arr을 sort해서 binary search하시면 O(nlogn)까지 줄일 순 있긴해요


그리고 개행문자나 space 문자 안받으시려면 scanf(" %c",&c);처럼 %c앞을 한칸 띄우시면 공백문자는 무시하고 받으라는 뜻이되요

또 for문안에 i가 0일때 따로 처리하셨는데 이러면 for문 돌면서 계속 if확인하니까 0인경우를 for문 앞에 써주시고 for문을 1부터 시작하시면 더 좋아요

마지막으로 재귀함수를 좀 복잡하게짜셨는데....함수가 종료되면 함수가 불려졌던 곳에서 부터 다시 실행되요 그런데 지금 이 코드는 index를 계속 바꾸면서 호출하게해서 호출자체가 많은데 지금은 상관없는데 다른 문제같은경우는 이렇게 짜시면 스택이 터질수도 있을거같아요....

jeul2   6년 전

일단 제 소스를 읽어주셔서 너무 감사드립니다.

이름짓는거부터 시작해서, 순서가 뒤섞인 것도 같고..

제가 지금 봐도 제가 짠 소스가 부끄럽네요ㅎㅎ...

초보자의 입장에서 잘 설명해주셔서 감사합니다!! 저도 나중에 실력이 올라가면 꼭 이렇게 해주고싶네요

----------

작성해주신 글 정독하고 제 나름대로 다시 짜봤는데요. 이 과정에서 답변해주신 것에 대해 궁금한게 또 생겼어요 허허

제가 알고있는 부분에서 다르게 생각하신 점이나 확실하게 틀렸다고 아시는 것을 짚어주시면 감사하겟습니다

----------

Q1.첫번째는  마지막에 설명해주신 부분인데요.

///" 마지막으로 재귀함수를 좀 복잡하게짜셨는데....함수가 종료되면 함수가 불려졌던 곳에서 부터 다시 실행되요 그런데 지금 이 코드는 index를 계속 바꾸면서 호출하게해서 호출자체가 많은데 지금은 상관없는데 다른 문제같은경우는 이렇게 짜시면 스택이 터질수도 있을거같아요.... "///


먼저, 함수를 그 안에서 자기를 다시 호출하는 것이 재귀함수라고 알고있었습니다.

C언어 기준, 반환형이 void인 함수를 호출하면, 함수인자값들을 값을 복사(??)하여

함수가 정의된 {}함수몸체부분에 쓸수있게 해주고

몸체 안에 적혀있는 순서대로 ;단위로 순차적으로 일을 수행하고 }를 만나거나 return;을 만나면 함수를 종료한다고 알고있었습니다.


그래서 return을 안쓰고 반환을 하는 경우는 함수가 아직 종료되지 않았기에 호출스택이 쌓이게 되고 이 스택이 오버플로우되면 터진다고 알고있어서

함수안에서 함수를 부를때 남은 일이 없게하기 위해 저렇게 짯었거든요... 

예를들어 함수의 정의가 아래와 같다면

void PreOrder(int nData)

{

    if (조건1)

        preOrder(nData);

    if (조건2)

        preOrder(nData);

....

{

조건1이 참이어서 preOrder를 호출하면 새롭게 호출했던 녀석이 끝나야 조건2로 시작되는 남은 구문을 읽을 수 있다고 이해했었거든요.

index를 바꾸면서 호출해도 call by value로 복사가 일어나는 것은 매한가지 아닌가요? 호출자체가 많다는 것을 어떻게 이해해야할지 모르겠습니다 ㅠㅠ

정리 : 재귀함수에서 return 함수호출과 그냥 함수호출의 차이점? 정도로 요약할 수 있겠네요...

--------------------

Q2. 두번째는 그냥 궁금한 거입니다. 첫번째, 두번째 방식 코드를 곰곰히 살펴보았는데, 첫번째는 배열의 장점을 이용하신거고, 두번째는 제가 연결리스트방식을 알려주시려고 배열로 연결리스트방식을 구현하신건가요?

다시한번 친절한 설명 감사드립니다~ 밑에는 그냥 다시한번짜본거 올릴게요!!

dps2   6년 전

안녕하세요~ㅎㅎ

순서대로 말씀드리면...

Q1


재귀함수가 return을 만나면 종료되는 것은 맞습니다. 그러나 return 부에 함수가 적혀있으면 그 함수를 계산하고 종료합니다. 

 재귀함수에서 주의해야할 점은 역시 스택의 크기입니다. 함수가 호출될 때마다 함수 안에 선언된 변수의 크기+알파 만큼 스택을 쌓습니다. 물론 기존 코드를 보시면 변수가 파라메터로 받는 int밖에 없고 최대 깊이가 26이니 터질 일은 없습니다.

재귀함수에서 중요시하는 점은 재귀함수 호출 횟수가 아니라 깊이입니다. (물론 acm문제에서는 시간제한이 있기 때문에 횟수도 중요하겠죠?) 예를 들어

   A

B  C

이런 트리 구조가 있다면 질문자 분께서 기존에 짜셨던 inorder코드라면

main 에서 index_A(f1){함수 호출 순서대로 몇번째 호출된 함수인지를 f{n}으로 표시했습니다}를 호출 후 index_B(f2)를 호출 후 index_A(f3)를 재호출후(60번줄) index_C(f4)를 호출 후 index_A(f5)를 호출 합니다. 이때 호출되어 있는 함수의 수는 5개입니다. 즉 스택에 5개가 쌓여있는 상태입니다.

그리고 두번째 짜신코드 혹은 제 코드(첫번째 두번째 모두) main에서 index_A(f1)을 호출후 index_B(f2) 호출후 f2가 종료하고 f1이 index_C(f3)를 호출합니다. 이때 최대 중첩갯수는 2개입니다.( f1,f2) 혹은 (f1,f3)입니다.

물론 지금은 터지지 않지만 균형잡힌 트리의 경우 뒤의 방법은 O(logN)만큼 최대 중첩되지만 기존의 방법은 O(N)만큼 중첩됩니다.


Q2

둘 다 배열을 사용한 것입니다. 다만 첫번째의 경우 input이 대문자로 제한된다는 점을 이용하여 최적화한 것입니다. 이 문제의 경우 대문자가 들어온다는 점이 보장되지만 다른 문제를 풀때 쓸 수 있는 좀더 일반적인 방법을 보여드리고자 2번째 방법을 이용했습니다.

그리고 연결리스트는 쓰지 않았습니다. 이 문제는 링크드리스트를 사용하면 더 어려울 것 같고 동적할당을 이용하려면 트리를 사용해야 합니다.

그리고 올리신 소스보면서 몇가지 더 참조하실만한 점을 써보자면....

0으로 초기화는 ={0,}; 으로 씁니다(0뒤에 ,까지 쓰여야해요) 원래 이게 standard입니다. 그러나 전역변수의 경우 초기화를 안해주셔도 자동으로 0으로 초기화됩니다. (static 변수포함)

C는 char와 int의 형변환이 상당히 느슨합니다. 그래서 fnCharToInteger와 fnintegertoChar와 같은 함수를 선언안하시고 바로 쓰셔도 됩니다.

그리고 변수의 값을 바꾸실때 파라메터로 넘기시지 않고 return값을 받아서 대입하셔도 됩니다. 이게 코딩하기 더 편하실 거에요. (물론 return 값으로 함수의 종료를 확인하고 파라메터로 받아서 포인터로 접근해서 값바꾸는 방법이 나중엔 더 좋지만 acm에서 코딩하실때는 코드의 량이 길어지는 만큼 귀찮으실거같아요ㅎㅎ)

그리고 혹시 다른 언어를 쓰시다 C를 쓰시는 건가요? 네이밍이나 const쓰시는 걸 보니 평범하신분은 아닌것같아서요....

jeul2   6년 전

아 드디어 얼핏 이해한 느낌입니다 너무 감사합니다!!

void형은 return이 임시공간을 만들지 않는다는 거에만 사로잡혀서 요상하게 생각을 했네요 ㅎㅎ;;

   A

B  C

이런 트리 구조가 있다면 질문자 분께서 기존에 짜셨던 inorder코드라면

main 에서 index_A(f1){함수 호출 순서대로 몇번째 호출된 함수인지를 f{n}으로 표시했습니다}를 호출 후 index_B(f2)를 호출 후 index_A(f3)를 재호출후(60번줄) index_C(f4)를 호출 후 index_A(f5)를 호출 합니다. 이때 호출되어 있는 함수의 수는 5개입니다. 즉 스택에 5개가 쌓여있는 상태입니다.

그리고 두번째 짜신코드 혹은 제 코드(첫번째 두번째 모두) main에서 index_A(f1)을 호출후 index_B(f2) 호출후 f2가 종료하고 f1이 index_C(f3)를 호출합니다. 이때 최대 중첩갯수는 2개입니다.( f1,f2) 혹은 (f1,f3)입니다.

물론 지금은 터지지 않지만 균형잡힌 트리의 경우 뒤의 방법은 O(logN)만큼 최대 중첩되지만 기존의 방법은 O(N)만큼 중첩됩니다.

------------------------
제가 구현했던게 이거라면( A->B->A->C->A )
A호출
B호출
  A호출
   C호출
    A호출
    A종료
   C종료
  A종료
B종료
A종료

답변자분이 구현하신게 (A->B->C)
A호출
B호출
B종료
 C호출
C종료
A종료

이렇게 되는 거였군요! 좋은 내용 알아갑니다~ 아 제 목적이 acm준비가 아니라서요.. 코딩습관 같은거를 익히기위해 변수명만큼은 최대한 알아보기쉽게?

하려고 하고 잇습니다.. 이제 제대로 배운지 3~4달정도라 많이 부족합니다... 타이핑이 귀찮은건 말이안되지요 하하ㅠㅠ

댓글을 작성하려면 로그인해야 합니다.