ELECTRA model을 이용한 이진 class 분류 portfolio
구름 교육에서 ELECTRA model을 이용한 긍/부정 class 분류 project를 시행했다.
BERT 파생 model을 이용해본 첫 project라 code도 깔끔하지 않고 생각대로 잘 안 되었지만, 그래도 많은 것을 얻어갔고 발전해보는 정말 의미 있는 시간이었다.
각 component에 따른 결과 값을 정리해보았고, 자세한 code는 깃허브 참조 바람.
프로젝트 개요
프로젝트 개요
- Hugging face 의 ELECTRA model 을 기반으로 Learning rate, Scheduler, Batch size 등의 다양한 Hyperparameter 를 적용하여 최적의 성능 도출 (Base Hyperparameter: lr =5e-5)
팀 구성 및 역할
프로젝트 진행 프로세스
7/13 - BERT base 를 활용한 전체적인 model process 파악
7/14~7/15 – ELECTRA 를 활용하여 Learning rate, Batch size 등을 수정하며 model 성능 개선
7/16~7/17 – Scheduler, Weight decay 등을 활용하여 overfitting 을 극복
Batch size – Base batch size 인 32 를 시작으로 64, 128, 256 의 batch size 적용
Learning rate – Base learning rate 인 5e-5 를 시작으로 1e-7, 1e-6, 1e-5, 1e-4, 1e-3 의 learning rate 적용
Scheduler – Linear scheduler 와 Cosine Annealing scheduler 적용
Weight decay – 0 (No weight decay), 1e-4, 1e-5 의 weight decay 적용
Batch size
Train Batch size | Val Batch size | Time | GPU Resource |
32 | 64 | 57:46 | 20% |
64 | 128 | 46:13 | 40% |
128 | 256 | 43:33 | 60% |
256 | 512 | 40:37 | 90~100% |
512 | 102 | Out-Of-Memory |
원래는 Batch size가 크면 train 성능이 떨어지지만, AdamW optimizer 사용으로 극복
따라서, Train Batch size=256, Val Batch size=512로 GPU 허용량 내 최대 Batch size 사용
Batch size 가 클수록 loss 가 더 낮아지는 것을 확인 . But, Overfitting 문제 존재
Scheduler
· Cosine Annealing LR scheduler
Train 속도 향상과 성능 개선을 위해 Linear scheduler 사용
Why use Linear scheduler?
→ model 의 saddle point 를 빠르게 벗어날 수 있다는 장점
→ But, Base model 이 원래 안정적인 loss 감소 형태를 보였기 때문에 이러한 Cosine scheduler 의 이점을 살리지 못하고 있다고 판단
∴ Linear scheduler 를 사용하여 안정적인 Learning rate 의 scheduler 과정 수행하기로 결론
Learning rate
(Baseline Learning rate)
cs231n 강의의 Learning rate 판단 요소에 근거하여 최적의 Learning rate 도출하기로 판단
LR=1e-7 일시 , 전형적인 low learning rate 형태
5e-5 와 1e-4 가 가장 좋은 성능을 보였고 , 그 중 5e-5(Baseline) 가 더 안정적인 loss 변화를 보여줌
1e-4 vs. 5e-5 (Baseline Learning rate)
Lr=5e-5 일 때 , 더 안정적인 learning rate 변화를 보여주고 , 1e-4 일 때보다 overshotting 될 확률이 적다고 판단
하지만 , 여전히 약간의 Overfitting 중이기 때문에 Weight decay 를 적용 시켜야겠다는 결론 도출
Weight decay
· Weight decay = 1e-5
· Weight decay = 1e-4
· Weight decay = 0 (No weight decay)
Weight decay 를 적용시켰기 때문에 Train loss 는 약간 늘었지만 , Val loss 가 줄어들면서 어느 정도 overfitting 해소
Weight decay 를 계속 늘려도 overfitting 이 진행되어 최종적으로 weight decay=1.5 로 선정
Final model select
Final model
(Batch size=256, Learning rate=5e-5, Weight_decay =1.5, Linear scheduler)
Conclusion
Next step
• Label smoothing: 0 과 1 로 이루어진 hard label 을 0.25, 0.75 등의 형태로 smoothin 한 soft label 로 변경하여 model 의 overfitting 을 방지하는 기법
• Data analysis: model 성능 개선에서만 초점을 맞춰 data 의 duplicate 제거 외에는 큰 component 를 주지 않았는데 , data 자체에 대한 변형으로 model 성능 방법 연구
• Pre-trained model 을 사용한 project 는 처음 해보는 것이었는데 , 처음에 방향성을 못 잡아서 시간을 많이 소비해서 더 다양한 component 를 적용해보지 못한 것이 아쉽다 .
이진 거래의 장점
[스카웃 알고리즘 강좌] 5-1 이진 트리
안녕하세요. Programog를 운영하고 있는 스카웃입니다.
여기까지 알아본 배열이나 연결 리스트, 스택, 큐, 덱 등은 모두
1차원의 선형적인 구조를 가지는데 비해 오늘 우리가 알아볼
트리는 2차원적인 구조를 가지는 뭔가 새롭고 산뜻한 느낌의 자료구조입니다.
지금부터 이 말도 많은 트리를 집중적으로 파고 들어가볼텐데
준비되셨다면 본론으로 들어가서 한번 알아보도록 하겠습니다.
[ 저번시간을 통하여 덱에 대해 확실히 다들알고계시겠으리라 믿고 수업을 진행하도록하겠습니다 ]
1. 트리의 개념
앞서 말했듯 지금부터 알아볼 '트리'는 이 때까지 봐왔던 자료구조와는 조금 틀립니다.
2차원이기때문에 구조가 입체적이고 다소 복잡해서. 배열이나 연결 리스트보다는 다루기가 어렵고 까다롭습니다.
또한 스택이나 연결 리스트처럼 구현 방법이 딱딱 정해진 것이 아니라 계속 바뀌며
트리에 적용할 수 있는 알고리즘에 따라 구조를 바꿔야하는 경우도 많아 생각을 좀 해야하는 녀석입니다.
그러나, 고생 끝에 낙이온다고. 입체적인 구조로 인해 자료의 삽입, 삭제 속도 가 빠를 뿐만 아니라 (→ 연결 리스트의 장점)
참조와 검색 속 도까지 만족할만하며 용량이 커지더라도 속도의 감소가 미약 하게 줄어들기 때문에 (배열의 장점 + 트리만의 장점)
오히려 대규모의. 대용량의 자료를 다룰 때 훨씬 더 효율적 입니다.
그래서 응용 분야가 굉장히 다양한데. 예를들어 디렉토리 구조, 상용 DB 엔진, DOM같은 위엄 쩔고 웅장한 것들에 사용될 뿐만 아니라
실생활에서도 회사의 기구도, 한 가문의 족보라던지. 축구 시합 대진표라던지 등등 아주 많이 쓰입니다.
이 때문에 트리는 고급 자료구조로 분류됩니다. (비선형 자료구조이기도 하죠. )
지금까지 트리에 대해 간략하게나마 소개를 하였는데요. 좀 파악이 되실련지요.
트리는 이 때까지 배웠던 자료구조들을 응용해서 만들어나가는 구조라 지금까지 제대로 공부를 해오시지 않으셨다면 좀 힘든 감이 있을 거라 판단되므로
찔리신다면 다시 복습해주고 와주시기 바라며.
본격적으로 트리가 어떤 녀석인지에 대해 분석들어가도록 하겠습니다.
우선 트리를 프로그래밍하는 방법을 배우기 전에 트리에서 사용하는 용어부터 정리해보도록 하겠습니다.
다음 장으로 넘어가보겠습니다.
2. 트리 관련 용어
트리 는 말 그대로 나무 입니다. 나무와 비스무리하게 생겼다고 해서 붙여진 이름이죠.
트리에서 사용하는 용어들은 나무 구조 자체가 워낙 실생활과 밀접한 것이기 때문에 죄다 우리가 실생활에서 사용하고 있는 용어들을 따온 것들입니다.
그런데 이 용어들이 동의어가 좀 짜증날 정도로(?) 많이 있어서 혼란스럽기는 한데 금방 적응하고 이해하실 수 있습니다. ^^
먼저 이진 거래의 장점 트리가 어떤식으로 생겼는지 그림을 통해 볼까요?
" 엥? 뭐야 장난치냐? 나무가 무슨 위에서 아래로 쭉쭉 뻗어나가냐? 나무는 밑에서부터 위로 우뚝서있는 거잖아 ㅡㅡ "
공부하는 스누피
이진수를 정규화된 형태로 표현하기 위해서는 기수(base)가 필요한데, 이를 이진 소수점(binary point)이라고 한다.
이런 수를 지원하는 컴퓨터 연산이 부동소수점(floating point) 연산이다. 부동(floating)은 소수점이 고정되어 있지 않다는 의미다. C언어에서는 이러한 수를 나타내기 위해 float라는 변수명을 사용한다.
이진 소수점으로 나타낸 실수는 다음과 같다.
컴퓨터에서는 y에 해당하는 지수도 이진법을 사용하여 나타낸다.
부동소수점의 장점
- 부동소수점 숫자를 포함한 자료의 교환을 간단하게 한다.
- 숫자가 항상 정규화된 형태로 표현되어 산술 연산이 간단해진다.
- 불필요하게 선행되는 0을 저장하지 않아도 되기 때문에 한 워드 내에 저장할 수 있는 수의 정밀도를 증가시킨다.
(ex. 0.000101보다 1.01 * 2^3이 더 효율적이다)
부동소수점의 표현
고정된 워드 크기를 사용하므로 소수부분(fraction, mentissa)의 크기와 지수(exponent)의 크기 사이에서 타협점을 찾아야 한다. 소수부분의 크기를 증가시키면 수의 정밀도가 높아지고, 지수의 크기를 증가시키면 수의 표현 범위가 늘어난다.
계산된 지수가 너무 커서 지수 필드에 표현될 수 없을 만큼 클 때 오버플로(overflow)가 발생한다.
음의 값을 갖는 지수의 절댓값이 너무 커서 지수 부분에 표현될 수 없을 경우 언더플로(underflow)가 발생한다.
언더플로와 오버플로의 발생을 줄이는 방법으로 지수 부분을 크게 하는 2배 정밀도(double precision) 부동소수점 연산이 있다. C언어에서는 이를 double이라고 한다. 기존 32bit로 표현한 방식은 단일 정밀도(single precision) 부동소수점이라고 한다.
오버플로와 언더플로가 발생한 것을 사용자에게 알리기 위해 예외(or 인터럽트)를 발생시킨다. 예외와 인터럽트는 예정되지 않은 프로시저 호출이다. 오버플로/언더플로가 발생된 명령어의 주소는 레지스터에 저장되고, 예외에 적합한 루틴으로 점프한다. 예외 루틴이 끝난 뒤 다시 돌아와 프로그램을 정상적으로 동작하게 한다.
바이어스된 표현법(biased notation)
IEEE 754 부동소수점 표준은 정규화된 이진수의 가장 앞쪽 1비트를 생략하고 표현하지 않는다. 정렬을 쉽게 하기 위해 부호 비트를 최상위 비트에 위치시켰다. 하지만 음수 지수는 숫자 정렬을 어렵게 만들어 지수를 바이어스된 표현법으로 표기하는 것이 이상적이다.
가장 음수인 지수를 00. 00으로, 가장 양수인 지수를 11. 11로 표현하는 방법을 바이어스된 표현법이라고 한다. 바이어스는 실제값을 구하기 위해 부호없이 표현된 수에서 빼야 하는 상수를 말한다.
IEEE 754는 단일 정밀도 펴현방식에서는 바이어스값 127을 사용한다. 따라서 -1은 -1 + 127 또는 126 = 01111110으로 표현된다. 2배 정밀도의 바이어스는 1023이다.
(예제) 십진수를 이진수로
-0.75
= -3/4
= -11/100 (이진수로)
= -0.11
= -0.11 * 2^0 (과학적 표기법)
= -1.1 * 2^-1 (정규화)
= (-1)^1 * 0.1 * 2^(126 - 127) (단일 정밀도 표현)
=> (-1)^1 * (1 + .1000 0000 0000 0000 0000 0000) * 2^(126-127)
= (-1)^1 * 0.1 * 2^(1022 - 1023) (2배 정밀도 표현)
(예제) 이진 부동소수점 수를 십진 부동소수점 수로 변환
1100000010100000000000000000000
=>
significant bit: 1
지수 부분: 10000001
소수부분 0100000000000000000000
=>
부호 비트 1, 지수 부분 129, 소수부분 1/4 = 0.25
(-1)^1 * (1+0.25) *2^(129-127) = -1 * 1.25 * 4 = -5.0
부동소수점 덧셈
1) 두 수의 지수를 비교한다. 작은 쪽의 지수가 큰 쪽의 지수와 같아질때까지 작은 수를 오른쪽으로 시프트한다.
3) 정규화된 과학적 표기법으로 정돈한다. (오버플로와 언더플로를 검사한다)
4) 부호를 제외한 유효자리의 길이에 맞게 자리맞춤(round)을 해준다. (반올림해준다)
(예제)
0.5 + -0.4375
0) 두 수를 정규화된 이진 표기법으로 정돈한다.
0.5 = 1.000 * 2^-1
-0.4375 = -7/16 = -111/10000
= -0.0111 * 2^0 = -1.110 * 2^-2
1) -1.110*2^-2의 지수가 작으니 오른쪽으로 시프트해준다.
-1.110*2^-2 = -0.111 * 2^-1
2) 유효자리를 더한다.
1.000 * 2^-1 + -0.111 * 2^-1 = (1.000 - 0.111) * 2^-1
= 0.001 * 2^-1
3) 정규화된 과학적 표기법으로 정돈한다.
0.001 * 2^-1 = 1.000 * 2^-4
4) 결과를 자리맞춤한다.이진 거래의 장점
이미 4bit 정밀도와 일치하므로 round할 필요가 없다.
1.000 * 2^-4 = 0.0001000 = 0.0001 = 1/2^4 = 1/16
부동소수점 곱셈
1) 두 수의 바이어스된 지수를 더한 값에 바이어스를 빼서 곱의 지수를 구한다. (=바이어스 없이 지수를 더한다)
3) 정규화된 형태로 정돈한다. (오버플로와 언더플로를 검사한다)
4) 부호를 제외한 유효자리의 길이에 맞게 자리맞춤을 해준다.
5) 부호가 같으면 결과의 부호는 양수이고 그렇지 않으면 음수이다.
(예제)
0.5 * -0.4375
0) 두 수를 이진 표기법으로 만든다.
0.5 = 1.000 이진 거래의 장점 * 2^-1
-0.4375 = -7/16 = -111/10000
= -0.0111 * 2^0 = -1.110 * 2^-2
1) 바이어스 없이 지수를 더한다.
-1 + (-2) = -3
2) 유효자리를 곱한다.
1.000
*1.110
-------
0000
1000
1000
1000
1110000
= 1.110 * 2^-3
3) 정규화한다.
이미 정규화되어 있고, 지수가 [-126, 127]에 포함된다.
4) 자리맞춤한다.
이미 자리맞춤 되어있다.
5) 이진 거래의 장점 피연산자들의 부호가 다르므로 곱의 부호를 음수로 한다.
-1.110 * 2^-3
= -0.001110 = -0.00111 = -7/32
+ 소수 십진수를 이진수로 표현하는 방법
소수부분을 2로 계속 곱해준다.
처음 이진 거래의 장점 곱했을 때 나왔던 소수부분이 다시 나오면 종료한다. (무한으로 반복되는것을 의미)
0.25 => 0.25*2 = 0.5, 0.5*2 = 1.0 => 0.01
0.3 => 0.3*2 = 0.6, 0.6*2 = 1.2, 0.2*2 = 0.4, 0.4*2 = 0.8, 0.8*2 = 1.6 => 0.6이 처음 나왔던 소수 부분이니까 종료.
+ 소수 이진수를 십진수로 표현하는 방법
소수 십진수를 이진수로 표현하는 방법을 거꾸로 사용한다.
소수부분의 끝부터 하나씩 읽는다. 처음 소수부분이 1일 경우 x * 2= 1을 만족하는 x가 1/2이다.
그 다음 소수부분도 1일 경우 x * 2 = 1+1/2를 만족하는 x를 구한다. 이때 x는 3/4가 된다.
다음 소수부분이 0일 경우 x * 2 = 3/4를 만족하는 x를 구한다. 이때는 식의 결과부분에 1을 더해주지 않는다.
1) 소수부분의 끝부터 하나씩 읽는다. 소수부분의 끝은 항상 1이므로 0.5부터 시작한다.
2) 다음 소수부분이 1일 경우 이전 결과값에서 1을 더한값과 동일한 x*2 식의 x를 구한다.
3) 다음 소수부분이 0일 경우 이전 결과값과 같은 x*2식의 x를 구한다.
1 => x*2 = 3/2 => x = 3/4
1 => x*2 = 7/4 => x = 7/8
0 => x*2 = 7/8 => x = 7/16
0 => x*2 = 7/16 => x = 7/32
David A. Patterson, John L. Hennessy (2018). Computer Organization and Design (ARM Edition)
자료구조 - 이진 트리 구현 배열 vs 리스트
관련 포스트를 보시면 아시겠지만 부모 노드의 배열 인덱스가 k이면 왼쪽 자식 노드의 인덱스는 2k+1, 오른쪽 자식 노드의 인덱스는 2k+2입니다.
이 말은 곧, 왼쪽 자식 노드 값을 알기 위해서는 arr[2k+1], 오른쪽 자식 노드 값을 알기 위해서는 arr[2k+2]에 바로 직접 접근하면 된다는 뜻입니다.
또한, 왼쪽 자식 노드의 왼쪽 자식 노드를 알고 싶다면 그냥 arr[2*(2k+1)]에 접근하면 됩니다.
리스트의 경우에는 지속적으로 포인터 연산을 해야 되기 때문에 속도면에서는 당연히 배열 인덱스에 별다른 연산 없이(단순히 사칙 연산만 함) 직접 접근해버리는 배열을 이용한 구현이 더 앞설 것입니다.
힙(완전 이진 트리)을 구현하는데 더욱 효율적이다.
힙을 구현할 때는 보통 완전 이진 트리를 이용해서 구현합니다.
완전 이진 트리에는 중간 중간 빈 노드들이 없기 때문에 배열을 이용해서 구현하면 0번부터 마지막 잎 노드가 들어가 있는 인덱스까지 중간에 텅 비어있는 인덱스가 없습니다.
그리고, 완전 이진 트리에서 노드를 추가하는 것은 어차피 제일 오른쪽에 있는 잎 노드 옆에 새로운 노드를 붙이는 것이기 때문에 결국 arr[제일 오른쪽에 있는 잎 노드의 인덱스 + 1]에 새로운 데이터를 할당하는 것입니다.
만약, 완전 이진 트리를 구현하는데 리스트를 썼다면 왼쪽 오른쪽 자식 노드에 대한 포인터까지 들고 있어야 하므로 메모리적으로는 당연히 배열이 유리할 것입니다.
리스트를 이용했을 때의 장점
완전 이진 트리가 아닌 경우, 메모리적으로 더 효율적이다.
완전 이진 트리가 아닌데 만약 배열을 이용해서 구현한다고 가정해봅시다.
임의의 위치에 노드들을 삽입할 텐데 이렇게 되면 중간 중간 배열 인덱스가 텅 비어있게 됩니다.
예를 들어 보면 아래와 같습니다.
이 트리에서 C에 오른쪽 자식 노드 D를 추가한다 하면 어떻게 될까요?
C의 인덱스는 2였으므로 일반식에 의해 D는 당연히 2*2+2 = 6, 배열의 6번 인덱스에 할당받을 것입니다.
이러면 결국, 배열의 3번, 4번, 5번 인덱스는 텅 비어있게 되고 메모리 낭비로 이어지게 됩니다.
만약 여기서 또 D에 오른쪽 자식 노드 E를 추가한다면 E는 6*2+2 = 12, 배열의 12번 인덱스에 할당받을 것이고, 배열의 7~11번의 인덱스는 또 메모리 낭비로 이어질 것입니다.
따라서, 완전 이진 트리가 아닌 경우에는 왼쪽과 오른쪽 자식 노드에 대한 포인터만 가지고 있으면 되는 리스트를 이용한 구현이 메모리적으로 효율적일 것입니다.
waca's field
우선 이진트리의 노드에는 많은 NULL 링크들이 존재한다. 구체적으로 이 사실을 살펴보도록 하자.
/* 일반식으로서의 의미를 지니기 위해 n이라고 쓰지만, 실제로 논리를 따라 갈 때는 그림과 같이 n = 7 이라 생각하자. */
트리의 노드 개수가 n개 라고 가정해보자. 각 노드당 2개의 링크가 존재하므로 (NULL도 포함) 총 링크의 개수는 2n개다. 이들 링크 중에서, n-1개의 링크들이 루트 노드를 이진 거래의 장점 이진 거래의 장점 제외한 n-1개의 다른 노드들을 가리키고 있다. n-1개의 링크만이 링크로서의 의미를 갖고 나머지는 NULL이란 뜻이다. 실제로 그림을 보면, 노드 개수는 7개인데 선은 6개가 존재한다. 6개의 선 만이 의미있는 링크를 가지고 나머지 8개 선은 NULL인 것이다.
따라서 총 링크개수 2n개 중에 의미 있는 n-1개의 링크를 빼주면, n+1개의 링크가 NULL이라는 걸 알 수 있다. 이 포인트에서 n+1개의 남아도는 링크 자원을 효율적으로 활용할 수는 없을까? 라는 고민이 생겼다. 그 결과 스레드 이진 트리(threaded binary tree)라는 개념이 등장했다.
아이디어를 요약하자면 n+1개의 NULL 링크에 중위 순회 시에 선행 노드인 중위선행자(inorder predecessor)나 중위 순회 시에 후속 노드인 중위 후속자(inorder successor)를 저장시켜 놓은 트리가 스레드 이진 트리(threaded binary tree)이다. 실을 이용하여 노드들을 순회 순서대로 연결 시킨다고 하여 붙여진 이름이다. 문제를 간단히 하기 위해 일단 중위 후속자만 저장되어 있다고 가정하자.
중위 후속자라는 단어는 어떤 걸 의미할까? 중위 순회 순서에서 그 다음에 이어질 차례인 노드를 말한다. 아래의 그림을 살펴보자.
/* L : 왼쪽 서브트리 R: 오른쪽 서브트리 V: 루트 노드 */
중위 순회의 경우 순서가 LVR 순으로 가기 때문에 제일 처음 방문하는 노드는 C가 된다. C에서 다음으로 방문할 노드는 B노드이므로, C의 링크를 B에 연결시킨다. 중위 후속자의 의미를 이젠 알 수 있다.
그러나 만약 이런 식으로 NULL 링크에 스레드가 저장된다면, 링크에 자식을 가리키는 포인터가 저장되어 있는지 아니면 중위후속자를 가리키는 스레드가 저장되어 있는지 구분이 안된다. 따라서 이를 구별해주는 태그가 필요하다. 이를 반영하여 트리노드의 구조가 다음과 같이 변경 되어야 한다.
만약 is_thread가 TRUE(1) 이면 right는 중위 후속자이고 is_thread가 FALSE(0)이면 오른쪽 자식을 가리키는 포인터가 된다. right만 따지는 이유는 어차피 두 개의 포인터 중 다음 중위후속자를 가리키는 건 하나만 필요하기 때문이다. 게다가 중위 순회의 순서는 언제나 왼쪽 서브트리를 먼저 방문하는 방식으로 이루어지고, 오른쪽 서브트리는 가장 나중에 방문한다. 논리적 구조상으로도 right만 스레드인지 아닌지를 따지는 것이 옳다.
그렇다면 이제 스레드 이진트리가 구성되었다고 생각해보자. 중위 순회 연산은 어떻게 변경되어야 할까? 먼저 노드 n의 중위 후속자를 반환하는 함수 find_successor를 제작해보자. find_successor는 노드 n의 is_thread가 TRUE로 되어 있으면 이진 거래의 장점 바로 오른쪽 자식이 중위 후속자가 되므로 오른쪽 자식을 반환한다. 만약 오른쪽 자식이 NULL이면 더 이상 후속자가 없다는 것이므로 NULL을 반환한다.
만약 is_thread가 아닌 경우는 서브 트리의 가장 왼쪽 노드로 가야 한다. is_thread가 FALSE인 경우는 내 자신이 루트노드 또는 중위후속자일 때 혹은 오른쪽 서브트리의 마지막 오른쪽 노드다. 중위 순회의 순서는 L V R 이고 현재 내 자신이 루트(중위후속자, V)이므로, 왼쪽 서브트리로 내려가는 것이 맞다. 따라서 왼쪽 자식이 NULL이 될 때까지 왼쪽 링크를 타고 이동한다.
이번에는 스레드 이진 트리에서 중위 순회를 하는 함수 thread_inorder를 제작해보도록 하자. 중위 순회는 가장 왼쪽 노드부터 방문하는 순서로 되야하므로, 왼쪽 자식이 NULL이 될 때까지 왼쪽 링크를 타고 계속 이동해야 한다. 가장 왼쪽 끝에 도달한 후 데이터를 출력하면, 중위 후속자를 찾는 함수를 호출해서 후속자가 NULL이 아니면 계속 루프를 반복한다.
지금까지 다룬 내용들을 바탕으로 스레드 이진트리 순회 프로그램을 아래 작성했다. 스레드 트리는 순회를 빠르게 하는 장점이 있지만 스레드를 설정하기 위해 삽입 및 삭제 등 함수가 좀 더 많은 일을 하게끔 설계해야하는 단점이 있다.
0 개 댓글