<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>mingmingeee.log</title>
        <link>https://velog.io/</link>
        <description>백엔드 코린이😁</description>
        <lastBuildDate>Wed, 18 Dec 2024 12:44:20 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>mingmingeee.log</title>
            <url>https://velog.velcdn.com/images/mingming_eee/profile/e7db069f-75ca-4b10-b45a-18893b6c1b01/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. mingmingeee.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/mingming_eee" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Chapter 14. 그래프(Graph)]]></title>
            <link>https://velog.io/@mingming_eee/datastructure-14</link>
            <guid>https://velog.io/@mingming_eee/datastructure-14</guid>
            <pubDate>Wed, 18 Dec 2024 12:44:20 GMT</pubDate>
            <description><![CDATA[<p>드디어 마지막 Chapter다.
마지막까지 힘내서 완독해보자~!</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/209750e8-e0c9-4033-a527-e3df6db8f6d2/image.png" alt=""></p>
<h2 id="14-1-그래프의-이해와-종류">14-1. 그래프의 이해와 종류</h2>
<h3 id="그래프의-역사"><em>그래프의 역사</em></h3>
<p>버스와 지하철 노선도와 같이 출발지와 목적지를 정해 최적의 경로를 알 수 있는 것이 있다.
이러한 프로그램의 구현에 사용되는 것이 바로 그래프 알고리즘이다.
그래프 알고리즘은 수학자 오일러(Euler)에 의해 고안되었다.
오일러는 1736년도 &#39;쾨니히스베르크의 다리 문제&#39;를 풀기 위해 그래프 이론을 사용하였다.</p>
<p>쾨니히스베르크의 다리 문제는 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/3417da52-1de8-42fd-b3b9-1c6ee4e3619a/image.png" alt=""></p>
<p>위 그림을 보고 &quot;모든 다리를 한 번씩만 건너서 처음 출발했던 장소로 돌아올 수 있는가?&quot;라는 질문에 답을 하는 것이 쾨니히스베르크의 다리 문제다.
이것이 가능하기 위한 필요충분 조건이 정점 별로 연결된 간선의 수가 모두 짝수여야 간선을 한 번씩만 지나서 처음 출발했던 정점으로 돌아올 수 있는데 이를 만족시키지 못하기 때문에 불가능하다.</p>
<p>여기서 처음 언급된 용어 2가지(정점, 간선)에 대해 설명하면 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/fc85c68c-b0b9-4d74-b10b-0e4e5b5666e7/image.png" alt=""></p>
<ul>
<li>간선(edge): 다리</li>
<li>정점(vertex): 다리가 연결하는, 강으로 구분되는 땅</li>
</ul>
<p>그럼 다시 위 그림을 살펴보면 정점에 연결된 간선의 수가 짝수가 아니라 문제를 해결할 수 없다는 것이 이해가 될 것이다.</p>
<p>이제 위와 같은 그래프를 구현까지 해보자.</p>
<h3 id="그래프의-이해와-종류"><em>그래프의 이해와 종류</em></h3>
<p>그래프의 이해를 위해 예시로 5학년 3반의 비상연락망 구조를 표현한 그래프를 살펴보자.
학생의 이름이 정점이고, 연결하는 선이 간선이다.
정점은 연결의 대상이 되는 개체 또는 위치를 의미하고
간선은 이들 사이의 연결을 의미한다.</p>
<p>Type|
--|--
<img src="https://velog.velcdn.com/images/mingming_eee/post/88b6be8c-378e-4e3a-b544-e0ec3b27b507/image.png" alt=""><br>&lt;무방향 그래프(undirected graph)&gt;|- 방향성 X
<img src="https://velog.velcdn.com/images/mingming_eee/post/0896774a-a74d-4f72-a477-8b3c046b025a/image.png" alt=""><br>&lt;방향 그래프(directed graph, digraph)&gt;|- 방향 정보 포함<br>- 다이그래프
<img src="https://velog.velcdn.com/images/mingming_eee/post/c6c1fcfd-ba79-4d35-aaa7-1e63244b8ee0/image.png" alt=""><br>&lt;무방향 완전 그래프(complete undirected graph)&gt;|- 각각의 정점에서 다른 모든 정점을 연결한 그래프
<img src="https://velog.velcdn.com/images/mingming_eee/post/8add45b9-2797-4481-8a55-2a2c57468313/image.png" alt=""><br>&lt;방향 완전 그래프(complete directed graph)&gt;|- 각각의 정점에서 다른 모든 정점을 방향 정보 포함하여 연결한 그래프<br>- 방향 그래프의 간선의 수 = 무방향 그래프의 간선의 수 X 2</p>
<h3 id="가중치-그래프weight-graph와-부분-그래프sub-graph"><em>가중치 그래프(Weight Graph)와 부분 그래프(Sub Graph)</em></h3>
<p>간선에 가중치 정보를 두어 그래프를 구성할 수도 있다.
그리고 이러한 유형의 그래프를 가리켜 &#39;가중치 그래프(Weight Graph)&#39;라 한다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/mingming_eee/post/b917b187-09e4-474e-b700-24da26c70097/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/mingming_eee/post/19f94c7c-192b-49a3-b3d5-7f783f4d5c8b/image.png" alt=""></th>
</tr>
</thead>
<tbody><tr>
<td>&lt;무방향 가중치 그래프&gt;</td>
<td>&lt;방향 가중치 그래프&gt;</td>
</tr>
</tbody></table>
<p>가중치는 두 정점 사이의 거리라던지 두 정점을 이동하는데 걸리는 시간과 같은 정보가 될 수 있다.
예를 들어 위 그래프에서 가중치가 시간을 의미하고 A에서 C로 이동하는 가장 빠른 길을 찾는다면 <code>정점A → 정점B → 정점C</code>가 될 것이다.</p>
<p>그리고 부분 집합과 유사한 개념으로 &#39;부분 그래프(Sub Graph)&#39;가 있다.
부분 집합이 원 집합의 일부 원소로 이뤄진 집합인 것처럼,
부분 그래프는 원 그래프의 일부 정점 및 간선으로 이뤄진 그래프를 뜻한다.</p>
<p>위 무방향 가중치 그래프의 부분 그래프는 다음과 같다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/mingming_eee/post/9f98390f-928a-4752-913d-5bb38a38c812/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/mingming_eee/post/481106d1-a435-4ea1-b9a9-bfae7083d4ce/image.png" alt=""></th>
</tr>
</thead>
</table>
<h3 id="그래프의-집합-표현"><em>그래프의 집합 표현</em></h3>
<p>그래프는 정점과 간선의 집합이다.
따라서 집합의 표기법을 이용해서 표현할 수 있다.
그래프는 정점과 간선으로 이뤄지므로, 정점의 집합과 간선의 집합을 다음과 같이 나눠서 표현한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/f4f7adbc-bbb8-4269-b3c2-c471b52047cf/image.png" alt=""></p>
<p>아래 그림의 무방향 그래프를 집합의 표기법으로 표현하면 다음과 같다.</p>
<table>
<thead>
<tr>
<th>무방향 그래프</th>
<th>집합의 표기법</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/3a10dbcd-b6ac-46ae-b06d-c9e92a148103/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/f8e325ee-632c-4446-9b68-5645ad941921/image.png" alt=""></td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/f51e785b-6058-4fc8-b847-b9ca8e8ef423/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/069d060a-2cb7-40ef-a254-cdd9ec530daf/image.png" alt=""></td>
</tr>
</tbody></table>
<p>정점A와 정점B를 연결하는 간선을 <code>(A, B)</code>로 표현했다.
무방향 그래프의 간선에는 방향성이 없으므로 <code>(A, B)</code>와 <code>(B, A)</code> 는 같은 간선을 나타낸다.</p>
<p>방향 그래프의 집합 표현법은 다음과 같다.</p>
<table>
<thead>
<tr>
<th>방향 그래프</th>
<th>집합의 표기법</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/2d3f2986-5253-47f1-9769-e9eecb44090c/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/67cdd134-e4f8-4a23-8851-977c8fa63fb0/image.png" alt=""></td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/992b7180-e6d3-43f3-85ca-207c98660694/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/44e1a470-54b2-4c2e-ac3c-4a45b1dd5819/image.png" alt=""></td>
</tr>
</tbody></table>
<p>무방향 그래프의 집합 표현과의 유일한 차이점은 방향성이 있는 간선의 표시법에 있다.
두 그래프 모두 정점A가 정점C를 가리키는데, 이 간선을 <code>&lt;A, C&gt;</code>와 같이 표현한다.</p>
<p>이로써 그래프의 종류, 용어 및 표기법에 대해 알아봤다.
이제 그래프를 구현하기 위한 ADT에 대해 알아보자.</p>
<h3 id="그래프의-adt"><em>그래프의 ADT</em></h3>
<p>그래프의 ADT를 정의하기에 앞서 다음과 같이 모든 유형의 그래프를 생성할 수 있고, 또 그 구조에 유연성을 부여할 수 있는 형태의 ADT를 기대할 수 있다.</p>
<p>&quot;그래프를 생성 및 초기화 할 때 간선의 방향성 여부를 선택할 수 있고, 가중치의 부여 여부도 선택할 수 있다.
뿐만 아니라, 이후에는 얼마든지 그리고 언제든지 정점과 간섭을 삽입하고 삭제할 수 있다.&quot;</p>
<p>그래프의 구성이 실질적인 목표가 아니기 때문에 위와 같은 그래프 보다는 다음 ADT와 같이 필요한만큼 제한적으로 정의한 그래프를 구현할 예정이다.</p>
<p>👉🏻그래프 자료구조의 ADT
✅Operations:</p>
<ul>
<li><p>void GraphInit(UALGraph * pg, int nv);</p>
<ul>
<li>그래프의 초기화 진행</li>
<li>두 번째 인자로 정점의 수 전달</li>
</ul>
</li>
<li><p>void GraphDestory(UALGraph * pg);</p>
<ul>
<li>그래프 초기화 과정에서 할당한 리소스 반환</li>
</ul>
</li>
<li><p>void AddEdge(UALGraph * pg, int fromV, int toV);</p>
<ul>
<li>매개변수 fromV와 toV로 전달된 정점을 연결하는 간선을 그래프에 추가</li>
</ul>
</li>
<li><p>void ShowGraphEdgeInfo(UALGraph * pg);</p>
<ul>
<li>그래프의 간선정보를 출력</li>
</ul>
</li>
</ul>
<p>위 ADT에서 보이듯이 그래프의 초기화 과정에서 정점의 수를 결정하도록 정의했다.
뿐만 아니라 간선을 추가하되 삭제는 불가능하게 정의했다.
이 정도로도 그래프의 구성 이후의 주제를 논의하기에 충분하다.
실제로 응용 프로그램을 개발하는 경우에도 이 정도 수준의 그래프를 구성하는 경우가 흔하다.</p>
<p>정점에 이름을 어떻게 부여할까?
AddEdge 함수의 두 번째, 세 번째 인자로 무엇을 전달해야 할까?
그래프의 헤더파일에 <code>enum {A, B, C, D, E, F, G, H, I, J};</code>와 같이 정점의 이름을 열거형 상수로 선언할 것이고 이는 정점의 이름을 상수화한 것이다.
따라서 GraphInit 함수의 두 번째 인자로 5가 전달되면 정점A, B, C, D, E로 이뤄진 그래프가 형성된다.
그리고 이 상수들이 AddEdge 함수의 인자로 전달될 것이다.
실제 프로그램 개발에 활용한다면, <code>enum {SEOUL, INCHEON, DAEGU, BUSAN, KWANJU};</code>와 같이 의미있는 이름을 부여해야 한다.</p>
<p>그래프의 헤더파일을 정의하는데 앞서 그래프의 구현방법에 대해 결정해야 한다.
구현방법에 따라서 헤더파일의 내용도 달라지기 때문이다.</p>
<h3 id="그래프-구현-방법"><em>그래프 구현 방법</em></h3>
<p>그래프를 구현하는 방법도 배열을 이용하는 방법과 연결 리스트를 이용하는 방법으로 나뉜다.
하지만, 그래프에서는 이들을 각각 다음과 같이 표현한다.</p>
<ul>
<li>인접 행렬(adjacent matrix) 기반 그래프 : 정방 행렬 활용</li>
<li>인접 리스트(adjacent list) 기반 그래프 : 연결 리스트 활용</li>
</ul>
<p>정방 행렬은 가로세로의 길이가 같은 행렬을 의미한다.
이러한 행렬은 2차원 배열로 표현한다.
인접 행렬을 기반으로 무방향 그래프와 방향 그래프를 표현하는 방법은 다음과 같다.</p>
<p>예시|
--|--
&lt;무방향 그래프의 인접 행렬 표현&gt;|
<img src="https://velog.velcdn.com/images/mingming_eee/post/a32c60fd-84fc-4e95-bb18-77c35ff4fe5e/image.png" alt="">|
&lt;방향 그래프의 인접 행렬 표현&gt;|
<img src="https://velog.velcdn.com/images/mingming_eee/post/87a9b84f-c4e1-4d69-8121-054a6059d614/image.png" alt="">|</p>
<p>정점이 4개면 가로세로 길이가 4인 2차 배열을 선언한다.
두 정점이 연결되어 있으면 1로, 연결되어 있지 않으면 0으로 표시한다.
단, 무방향 그래프의 경우 대각선을 기준으로 대칭을 이루고,
방향 그래프의 경우 대칭을 이루지 않는다.</p>
<p>인접 리스트 기반의 그래프 표현방법에 대해 살펴보자.</p>
<p>예시|
--|--
&lt;무방향 그래프의 인접 리스트 표현&gt;|
<img src="https://velog.velcdn.com/images/mingming_eee/post/3ad3b891-6dbe-49d6-b863-8bb160a8d28d/image.png" alt="">|
&lt;방향 그래프의 인접 리스트 표현&gt;|
<img src="https://velog.velcdn.com/images/mingming_eee/post/7ca0c63d-2fb6-46c2-8d12-6a461c203b86/image.png" alt="">|</p>
<p>각각의 정점은 자신과 연결된 정점의 정보를 담기 위해서 하나의 연결 리스트를 갖는다.
그리고 각각의 정점에 연결된 간선의 정보는 각각의 연결리스트에 담아야 한다.
방향 그래프에서는 각 정점 별로 가리키는 정점의 정보만을 연결 리스트에 담는다.
따라서 무방향 그래프에 비해서 추가되는 노드의 수가 반으로 준다.</p>
<hr>
<h2 id="14-2-인접-리스트-기반의-그래프-구현">14-2. 인접 리스트 기반의 그래프 구현</h2>
<p>그래프를 인접 행렬로 구현하는 방법과 인접 리스트로 구현하는 방법 중 인접 리스트 기반으로 구현하는 방법에 대해 배울 것이다.</p>
<p>그래프의 구현 관점에서 무방향 그래프와 방향 그래프의 유일한 차이점은 연결 리스트에 추가하는 노드의 수에 있기 때문에, 이 둘의 구현방법에는 차이가 없다고 볼 수 있다.
그러나 굳이 따지자면 무방향 그래프의 구현이 조금 더 복잡하다 할 수 있다.
연결 리스트에 추가해야 하는 노드의 수가 방향 그래프에 비해 두 배 더 많기 때문이다.</p>
<p>따라서 조금 더 복잡한 무방향 그래프의 구현에 대해 알아보자.</p>
<h3 id="1-헤더파일-정의---algraphh"><em>1) 헤더파일 정의 - ALGraph.h</em></h3>
<p>인접 리스트 기반의 구현이므로 핵심은 연결 리스트에 있다.
그래서 이전에 구현한 연결 리스트를 사용할 것이고, <a href="https://velog.io/@mingming_eee/datastructure-04#%EC%A0%95%EB%A0%AC-%EA%B8%B0%EB%8A%A5%EC%9D%B4-%EC%B6%94%EA%B0%80%EB%90%9C-%EC%97%B0%EA%B2%B0-%EB%A6%AC%EC%8A%A4%ED%8A%B8-2-%EA%B5%AC%EC%A1%B0%EC%B2%B4%EC%99%80-%ED%97%A4%EB%8D%94%ED%8C%8C%EC%9D%BC%EC%9D%98-%EC%A0%95%EC%9D%98">DLinkedList.h</a>과 <a href="https://velog.io/@mingming_eee/datastructure-04#%EC%A0%95%EB%A0%AC-%EA%B8%B0%EB%8A%A5%EC%9D%B4-%EC%B6%94%EA%B0%80%EB%90%9C-%EC%97%B0%EA%B2%B0-%EB%A6%AC%EC%8A%A4%ED%8A%B8-3-%EB%8D%94%EB%AF%B8-%EB%85%B8%EB%93%9Cdummy-node-%EA%B8%B0%EB%B0%98%EC%9D%98-%EB%8B%A8%EC%88%9C-%EC%97%B0%EA%B2%B0-%EB%A6%AC%EC%8A%A4%ED%8A%B8-%EA%B5%AC%ED%98%84">DLinkedList.c</a>가 필요하다.</p>
<p>헤더파일은 다음과 같다.</p>
<pre><code class="language-c">#ifndef __AL_GRAPH__
#define __AL_GRAPH__

#include &quot;DLinkedList.h&quot;

// 정점의 이름을 상수화
enum {A, B, C, D, E, F, G, H, I, J};

typedef struct _ual
{
    int numV;       // 정점의 수
    int numE;       // 간선의 수
    List * adjList; // 간선의 정보
} ALGraph;

// 그래프의 초기화
void GraphInit(ALGraph * pg, int nv);

// 그래프의 리소스 해제
void GraphDestroy(ALGraph * pg);

// 간선의 추가
void AddEdge(ALGraph * pg, int fromV, int toV);

// 간선의 정보 출력
void ShowGraphEdgeInfo(ALGraph * pg);

#endif</code></pre>
<p>필요한 정점의 수가 10개를 넘으면 이름을 더 추가하면 되고, 또한 프로그램의 성격에 따라서 이름을 바꿀 수 있다.</p>
<h3 id="2-소스파일-정의---algraphc"><em>2) 소스파일 정의 - ALGraph.c</em></h3>
<p>먼저, GraphInit 함수를 보자.
이 함수를 보면 구현의 방식이 전체적으로 머릿속에 그려진다.</p>
<pre><code class="language-c">void GraphInit(ALGraph * pg, int nv)
{
    int i;    

    pg-&gt;adjList = (List*)malloc(sizeof(List)*nv);    // 간선정보를 저장할 리스트 생성
    pg-&gt;numV = nv;        // 정점의 수는 nv에 저장된 값으로 결정
    pg-&gt;numE = 0;        // 초기의 간선 수는 0개

    // 정점의 수만큼 생성된 리스트들 초기화
    for(i=0; i&lt;nv; i++)
    {
        ListInit(&amp;(pg-&gt;adjList[i]));
        SetSortRule(&amp;(pg-&gt;adjList[i]), WhoIsPrecede); 
    }
}</code></pre>
<p>여기서 <code>SetSortRule(&amp;(pg-&gt;adjList[i]), WhoIsPrecede);</code> 를 통해 생성된 연결 리스트에 정렬기준을 설정하고 있는데,
사실 이는 그래프의 표현에 있어서 불필요한 것이다.
다만, 출력의 형태를 보기 좋게 하기 위해 알파벳 순으로 출력을 유도하기 위해서 정렬기준을 설정했다.</p>
<p>다음은 GraphDestroy 함수다.</p>
<pre><code class="language-c">void GraphDestroy(ALGraph * pg)        // 그래프의 리소스 해제
{
    if(pg-&gt;adjList != NULL)
        free(pg-&gt;adjList);            // 동적 할당된 연결 리스트 소멸
}</code></pre>
<p>보다 간단하게 함수를 정의할 수 있다.</p>
<p>다음은 간선의 추가를 담당하는 AddEdge 함수다.</p>
<pre><code class="language-c">void AddEdge(ALGraph * pg, int fromV, int toV)    // fromV, toV 연결하는 간선 추가
{
    // 정점 fromV의 연결 리스트에 정점 toV의 정보 추가
    LInsert(&amp;(pg-&gt;adjList[fromV]), toV);

    // 정점 toV의 연결 리스트에 정점 fromV의 정보 추가
    LInsert(&amp;(pg-&gt;adjList[toV]), fromV);
    pg-&gt;numE += 1;
}</code></pre>
<p>무방향 그래프의 간선을 추가하는 것이므로 위의 함수에서는 LInsert 함수를 두 번 호출했다.
만약에 구현하는 것이 방향 그래프였다면 LInsert 함수는 한번만 호출해도 충분하다.
그리고 연결 리스트를 지정하는 인덱스 값으로 fromV와 toV가 사용되었다.
fromV와 toV가 각각 A, B에 전달되면, LInsert 함수의 호출형태는 다음과 같아진다.
<code>LInsert(&amp;(pg-&gt;addjList[A]), B); LInsert(&amp;(pg-&gt;adjList[B]), A);</code>
이렇듯 정점의 이름이 바로 사용될 수 있는 이유는, 정점의 이름이 의미하는 바가 상수고, 그 값이 0부터 시작해서 1씩 증가하기 때문이다.</p>
<p>마지막으로 ShowGraphEdgeInfo 함수에서 문자의 출력을 위해 65를 더한 것은 아스키 코드로 변환하기 위함이다.</p>
<p>소스파일을 하나로 정리하면 다음과 같다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;ALGraph.h&quot;
#include &quot;DLinkedList.h&quot;

int WhoIsPrecede(int data1, int data2);

// 그래프의 초기화
void GraphInit(ALGraph * pg, int nv)
{
    int i;    

    pg-&gt;adjList = (List*)malloc(sizeof(List)*nv);
    pg-&gt;numV = nv;
    pg-&gt;numE = 0;     // 초기의 간선 수는 0개

    for(i=0; i&lt;nv; i++)
    {
        ListInit(&amp;(pg-&gt;adjList[i]));
        SetSortRule(&amp;(pg-&gt;adjList[i]), WhoIsPrecede); 
    }
}

// 그래프 리소스의 해제
void GraphDestroy(ALGraph * pg)
{
    if(pg-&gt;adjList != NULL)
        free(pg-&gt;adjList);
}

// 간선의 추가
void AddEdge(ALGraph * pg, int fromV, int toV)
{
    LInsert(&amp;(pg-&gt;adjList[fromV]), toV);
    LInsert(&amp;(pg-&gt;adjList[toV]), fromV);
    pg-&gt;numE += 1;
}

// 유틸리티 함수 : 간선의 정보 출력
void ShowGraphEdgeInfo(ALGraph * pg)
{
    int i;
    int vx;

    for(i=0; i&lt;pg-&gt;numV; i++)
    {
        printf(&quot;%c와 연결된 정점: &quot;, i + 65);

        if(LFirst(&amp;(pg-&gt;adjList[i]), &amp;vx))
        {
            printf(&quot;%c &quot;, vx + 65);

            while(LNext(&amp;(pg-&gt;adjList[i]), &amp;vx))
                printf(&quot;%c &quot;, vx + 65);
        }
        printf(&quot;\n&quot;);
    }
}

int WhoIsPrecede(int data1, int data2)
{
    if(data1 &lt; data2)
        return 0;
    else
        return 1;
}</code></pre>
<h3 id="3-실행파일-정의---algraphmainc"><em>3) 실행파일 정의 - ALGraphMain.c</em></h3>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &quot;AlGraph.h&quot;

int main()
{
    ALGraph graph;          // 그래프 생성
    GraphInit(&amp;graph, 5);   // 그래프의 초기화

    AddEdge(&amp;graph, A, B);      // 정점 A와 B를 연결
    AddEdge(&amp;graph, A, D);      // 정점 A와 D를 연결
    AddEdge(&amp;graph, B, C);      // 정점 B와 C를 연결
    AddEdge(&amp;graph, C, D);      // 정점 C와 D를 연결
    AddEdge(&amp;graph, D, E);      // 정점 D와 E를 연결
    AddEdge(&amp;graph, E, A);      // 정점 E와 A를 연결

    ShowGraphEdgeInfo(&amp;graph);  // 그래프 간선정보 출력
    GraphDestroy(&amp;graph);       // 그래프의 리소스 소멸

    return 0;
}

&gt; gcc .\ALGraph.c .\ALGraphMain.c .\DLinkedList.c
&gt; .\a.exe
&gt; 출력
A와 연결된 정점: B D E 
B와 연결된 정점: A C   
C와 연결된 정점: B D
D와 연결된 정점: A C E
E와 연결된 정점: A D</code></pre>
<p>그래프의 생성 및 초기화는 <code>ALGraph graph; GraphInit(&amp;graph, 5);</code> 이 두 문장에 의해 이뤄진다.
특히 GraphInit 함수의 호출문에서 두 번째 인자로 5를 전달하였는데 이는 정점의 수를 의미한다.
즉, 저 두 문장으로 5개의 정점으로 이뤄진 그래프가 형성되는 것이다.
그리고 이때 생성되는 정점의 이름은 A, B, C, D, E가 된다.</p>
<p>이후에 AddEdge 함수의 호출을 통해서 총 6개의 간선을 추가하는데 정점의 이름을 인자로 전달하고 있다.
그리하여 형성된 그래프의 모양은 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/903eb8c1-eaaa-406a-a0b5-9dff55392714/image.png" alt=""></p>
<p>위 그림대로 그래프가 형성되었는지 ShowGraphEdgeInfo를 통해 각각의 정점에 연결된 정점들의 정보를 출력한다.
따라서 출력 결과를 통해 그래프의 구조를 유추할 수 있다.
마지막으로 GraphDestroy 함수를 호출하고 있고 이 함수가 호출되면서 할당된 별도의 메모를 소멸한다.</p>
<hr>
<h2 id="14-3-그래프의-탐색">14-3. 그래프의 탐색</h2>
<p>연결 리스트는 노드의 연결 방향이 명확하기 때문에 탐색하는 것이 어렵지 않다.
반면 트리는 노드의 연결 방향이 일정하지 않아서 탐색을 진행하는 것이 복잡한 편이다.
하지만 이진 탐색 트리의 경우 노드의 연결에 규칙이 있기 때문에 비교적 간단한 편이었다.</p>
<p>그래프의 탐색은 어떨까?
그래프의 모든 정점을 돌아다니려면(탐색하려면) 어떤 방법이 있을까?</p>
<p>그래프의 탐색은 어떤 자료구조보다도 탐색이 복잡한 편이다.
그래프느 정점의 구성뿐만 아니라, 간선의 연결에도 규칙이 존재하지 않기 때문이다.
그래서 그래프의 탐색을 위한(그래프의 모든 정점을 돌아다니기 위한) 별도의 알고리즘 두 가지에 대해 알아보자.</p>
<ul>
<li>깊이 우선 탐색 (Depth First Search: DFS)</li>
<li>너비 우선 탐색 (Breadth First Search: BFS)</li>
</ul>
<h3 id="1-깊이-우선-탐색depth-first-search-dfs"><em>1) 깊이 우선 탐색(Depth First Search: DFS)</em></h3>
<p>깊이 우선 탐색의 설명을 위해 5학년 3반 어린이들의 비상연락망을 예시로 살펴보자.</p>
<p>&lt;DFS의 과정&gt;</p>
<table>
<thead>
<tr>
<th>Pic.</th>
<th>Expl.</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/b05c150b-6fdf-40b1-9532-c38833413dd6/image.png" alt=""></td>
<td>동수에게 비상 메시지 전달(시작)<br>한 사람에게만 전달한다고 가정하고 지민에게만 연락</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/4e0b709a-bed9-4968-b207-8f3d014d208a/image.png" alt=""></td>
<td>지민부터 지민-민석-정희-수정 순으로 수정에게 전달</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/9a75934f-9f3c-45be-aaae-86c34e29bcd7/image.png" alt=""></td>
<td>수정이 연결된 애들 중에서 연락을 받지 못한 사람이 있는지<br>역으로 되돌아 가면서 연락 취할 곳을 찾기</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/d9ef94fd-554e-4be7-b88a-d40b66ac1b9a/image.png" alt=""></td>
<td>모든 사람이 연락을 받음<br>연락을 처음 취한 &#39;동수&#39;에게 다시 연락이 가야 종료.</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/9019ec31-7597-4faf-866f-bb4a864e5059/image.png" alt=""></td>
<td>&#39;동수&#39;에서 시작해 &#39;동수&#39;로 끝나는 메시지 전달과정 최종</td>
</tr>
</tbody></table>
<p>여기서 DFS의 핵심 3가지는 다음과 같다.</p>
<ul>
<li>한 사람에게만 연락한다.</li>
<li>연락할 사람이 없으면, 자신에게 연락한 사람에게 이를 알린다.</li>
<li>처음 연락을 시작한 사람의 위치에서 연락이 끝난다.</li>
</ul>
<h3 id="2-너비-우선-탐색breadth-first-search-bfs"><em>2) 너비 우선 탐색(Breadth First Search: BFS)</em></h3>
<p>DFS가 한 사람에게 연락하는 방식이라면 BFS는 자신에게 연결된 모든 사람에게 연락하는 방식이다.
BFS의 B는 Breadth의 약자로 폭, 너비를 뜻한다.
BFS에서 폭의 의미는 한 사람을 기준으로 메시지를 전달하는 사람의 수(폭)을 나타낸다.
그리고 BFS는 이러한 폭을 우선시로 넓히는 방식이다.</p>
<p>&lt;BFS의 과정&gt;</p>
<table>
<thead>
<tr>
<th>Pic.</th>
<th>Expl.</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/8520bf5d-2572-440b-8afc-5717ad13c92e/image.png" alt=""></td>
<td>지율을 기준으로 시작.</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/e6a6b5e0-0ca4-4f1e-95ce-91b84ad93781/image.png" alt=""></td>
<td>지율과 연결된 동수와 민석 두 사람 모두에게 연락.<br>동수와 민석도 자신에게 연결된 모든 사람에게 연락.<br>누가 먼저 연락을 취하느냐 문제되지 않음.</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/d70c925b-6b68-48d4-8c78-c7be66df9fcc/image.png" alt=""></td>
<td>동수가 먼저 주변인에게 연락을 취한다고 가정.<br>이어서 민석이 주변인에게 연락 취함.</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/5f09dbab-b77b-48a8-9d38-e08f328f4a64/image.png" alt=""></td>
<td>동수에 이어 민석이 연락이 취한뒤 상황.<br>지민, 수정은 주변인이 모두 연락을 받음.</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/2e0f16ae-0a87-4ffb-875b-c82f7e3d2ae7/image.png" alt=""></td>
<td>정희만 명석에게 연락 취하기.<br>명석이 연락을 취할 기회를 가지며 BFS 종료.</td>
</tr>
</tbody></table>
<p>BFS는 약간 소문이 퍼저가는 상황과 비슷하다고 할 수 있다.</p>
<h3 id="깊이-우선-탐색dfs-구현"><em>깊이 우선 탐색(DFS) 구현</em></h3>
<p>위에서 배운 DFS를 구현해보자.</p>
<h4 id="1-모델-및-이해">1) 모델 및 이해</h4>
<p>DFS 구현을 위해 필요한 2가지는 다음과 같다.</p>
<ul>
<li>스택: 경로 정보의 추적을 목적으로</li>
<li>배열: 방문 정보의 기록을 목적으로</li>
</ul>
<p>DFS에서는 갔던 길을 되돌아 오는 상황이 존재한다. (전달할 사람이 없을 경우)
그리고 각 정점별 방문의 상태를 표시하기 위해 배열이 필요하다.</p>
<table>
<thead>
<tr>
<th>Pic.</th>
<th>Expl.</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/57a0026c-8af4-4552-b421-2c376e224921/image.png" alt=""></td>
<td>동수는 시작과 동시에 방문한 상태로 표시.</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/d1dcaa72-4d47-4825-a3ad-2c6b07dc8ea4/image.png" alt=""></td>
<td>동수를 떠나 지민에게 방문할 때, 떠나는 동수의 이름(정보)을 스택으로 옮기기.<br>방문한 정점을 떠날 때에는 떠나는 정점의 정보를 스택에 쌓기.</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/9781a7b1-5aac-41e6-bf8b-a20af40672c6/image.png" alt=""></td>
<td>동수를 시작으로 수정까지 방문한 결과.<br>수정과 연락된 사람들 모두 연락을 받았으니 자신에게 연락한 사람에게 기회 넘기기.<br>자신에게 연락한 사람의 정보는 스택에서 확인 가능.</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/c4297be7-c78a-4fb5-97f9-0ea24fcd0dac/image.png" alt=""></td>
<td>스택의 가장 맨 위에 있는 정희로 되돌아오기.<br>정희 역시 연결된 모든 이에게 연락을 취했으니 스택에서 다음 이름을 꺼내 그 사람에게 기회 넘기기.</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/3563648f-8a3e-471b-9504-3a76834d0975/image.png" alt=""></td>
<td>민석에게로 연락의 기회 되돌아오기.<br>민석이 연락할 수 있는 대상인 지율에게 연락.</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/fdea18ba-ccf5-462f-9806-128a12404a1f/image.png" alt=""></td>
<td>지율에게 연락하면서 민석의 이름이 다시 스택으로 옮겨짐.<br>시작점으로 되돌아가는 일만 남음.</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/352f8507-4593-4f04-bd13-98ae5513730a/image.png" alt=""></td>
<td>스택에 쌓여 있는 정점의 정보를 꺼내 이동.</td>
</tr>
</tbody></table>
<p>DFS 알고리즘의 요구대로 정점을 이동하는데 있어 스택은 매우 중요한 역할을 한다.</p>
<h4 id="2-실제-구현">2) 실제 구현</h4>
<p>DFS 알고리즘 구현을 위해 필요한 파일은 다음과 같다.</p>
<ul>
<li>ALGraphDFS.h, ALGraphDFS.c : DFS 알고리즘 관련 그래프 함수의 선언과 정의 관련</li>
<li><a href="https://velog.io/@mingming_eee/datastructure-06#%EB%B0%B0%EC%97%B4-%EA%B8%B0%EB%B0%98-%EC%8A%A4%ED%83%9D-%EA%B5%AC%ED%98%84-1-%ED%97%A4%EB%8D%94%ED%8C%8C%EC%9D%BC%EC%9D%98-%EC%A0%95%EC%9D%98">ArrayBaseStack.h</a>, <a href="https://velog.io/@mingming_eee/datastructure-06#%EB%B0%B0%EC%97%B4-%EA%B8%B0%EB%B0%98-%EC%8A%A4%ED%83%9D-%EA%B5%AC%ED%98%84-2-%EC%86%8C%EC%8A%A4%ED%8C%8C%EC%9D%BC%EC%9D%98-%EC%A0%95%EC%9D%98">ArrayBaseStack.c</a> : 스택 관련 (Chapter 06에서 구현)</li>
<li><a href="https://velog.io/@mingming_eee/datastructure-04#%EC%A0%95%EB%A0%AC-%EA%B8%B0%EB%8A%A5%EC%9D%B4-%EC%B6%94%EA%B0%80%EB%90%9C-%EC%97%B0%EA%B2%B0-%EB%A6%AC%EC%8A%A4%ED%8A%B8-2-%EA%B5%AC%EC%A1%B0%EC%B2%B4%EC%99%80-%ED%97%A4%EB%8D%94%ED%8C%8C%EC%9D%BC%EC%9D%98-%EC%A0%95%EC%9D%98">DLinkedList.h</a>, <a href="https://velog.io/@mingming_eee/datastructure-04#%EC%A0%95%EB%A0%AC%EC%9D%98-%EA%B8%B0%EC%A4%80%EC%9D%84-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0-%EC%9C%84%ED%95%9C-%ED%95%A8%EC%88%98%EC%9D%98-%EC%A0%95%EC%9D%98">DLinkedList.c</a> : 연결 리스트 관련 (Chapter 04에서 구현)</li>
<li>DFSMain.c: main 함수 관련</li>
</ul>
<p><strong>-헤더파일 (ALGraphDFS.h)</strong></p>
<p>DFS 알고리즘을 근거로 그래프의 모든 정점 정보를 출력하는 함수는 <code>void DFSowGraphVertex(ALGraph * pg, int startV);</code>이다.
이 함수의 선언이 추가된 헤더파일 ALGraphDFS.h 파일을 먼저 살펴보자.</p>
<pre><code class="language-c">#ifndef __AL_GRAPH_DFS__
#define __AL_GRAPH_DFS__

#include &quot;DLinkedList.h&quot;    // 연결 리스트 사용용

// 정점의 이름들을 상수화화
enum {A, B, C, D, E, F, G, H, I, J};

typedef struct _ual
{
    int numV;           // 정점의 수
    int numE;           // 간선의 수
    List * adjList;     // 간선의 정보보
    int * visitInfo;
} ALGraph;

// 그래프의 초기화화
void GraphInit(ALGraph * pg, int nv);

// 그래프의 리소스 해제제
void GraphDestroy(ALGraph * pg);

// 간선의 추가가
void AddEdge(ALGraph * pg, int fromV, int toV);

// 간선의 정보 출력력
void ShowGraphEdgeInfo(ALGraph * pg);

// 정점의 정보 출력: Depth First Search_DFS 기반
void DFShowGraphVertex(ALGraph * pg, int startV);

#endif</code></pre>
<p>ALGraph.h에서 정의된 그래프를 표현한 구조체는 다음과 같다.</p>
<pre><code class="language-c">typedef struct _ual
{
    int numV;
    int numE;
    List * adjList;
} ALGraph;</code></pre>
<p>하지만 위 헤더파일에서는 구조체에 <code>int * visitInfo;</code> 라는 멤버가 하나 더 추가되었다.
이 멤버를 추가한 이유는 DFS 기분의 탐색과정에서 탐색이 진행된 정점의 정보를 담기 위함이다.</p>
<p><strong>-소스파일 (ALGraphDFS.c)</strong></p>
<p>위 헤더파일과 새로운 구조체 멤버를 바탕으로 ALGraphDFS.c에 정의된 다음 두 함수에는 새로운 구조체 멤버와 관련된 문장이 추가된다.</p>
<ul>
<li><code>GraphInit</code> : visitInfo 관련 초기화를 함께 진행</li>
<li><code>GraphDestroy</code> : visitInfo 관련 리소스의 해제도 함께 진행</li>
</ul>
<p>함수에 추가된 문장들을 구분하여 보며 이를 통해 visitInfo가 가리키는 대상이 무엇인지 확인해보자.</p>
<pre><code class="language-c">// 그래프의 초기화
void GraphInit(ALGraph * pg, int nv)
{
    ....
    // 정점의 수를 길이로 하여 배열을 할당
    pg-&gt;visitInfo = (int *)malloc(sizeof(int) * pg-&gt;numV);

    // 배열의 모든 요소를 0으로 초기화
    memset(pg-&gt;visitInfo, 0, sizeof(int) * pg-&gt;numV);
}

// 그래프의 리소스 해제
void GraphDestroy(ALGraph * pg)
{
    ....
    // 할당된 배열의 소멸
    if(pg-&gt;visitInfo != NULL)
        free(pg-&gt;visitInfo);
}</code></pre>
<p>정리하면 visitInfo가 가리키는 것은 들렀던 정점의 정보를 위한 배열이고,
이를 동적으로 할당했다가 소멸하는 문장이 추가된 것이다.</p>
<p>그리고 새로 추가된 핵심이라 할 수 있는 두 함수를 살펴보자.</p>
<ul>
<li><code>void DFShowGraphVertex(ALGraph * pg, int startV);</code> : 그래프의 정정 정보 출력</li>
<li><code>int VisitVertex(ALGraph * pg, int visitV);</code> : 정점의 방문을 진행</li>
</ul>
<p>이 중에서 VisitVertex 함수는 DFShowGraphVertex 함수 내에서 호출되는 함수다.</p>
<pre><code class="language-c">// 두 번째 매개변수로 전달된 이름의 정점에 방문
int VisitVertex(ALGraph * pg, int visitV)
{
    if(pg-&gt;visitInfo[visitV] == 0)    // visitV에 처음 방문일 때 &#39;참&#39;인 if문
    {
        pg-&gt;visitInfo[visitV] = 1;    // visitV에 방문한 것으로 기록
        printf(&quot;%c &quot;, visitV + 65);    // 방문한 정점의 이름을 출력
        return TRUE;         // 방문 성공
    }
    return FALSE;            // 방문 실패
}</code></pre>
<p>위 함수에서 볼 수 있듯, 방문이 이뤄지면 해당 정점의 이름을 인덱스 값으로 하는 배열의 요소에 1을 저장하여 방문이 이뤄졌음을 기록하고, 또 방문한 정점의 이름을 출력한다.
이제 DFShowGraphVertex 함수를 살펴보자.</p>
<pre><code class="language-c">// DFS 기반으로 정의된 함수: 정점의 정보 출력
void DFShowGraphVertex(ALGraph * pg, int startV)
{
    Stack stack;
    int visitV = startV;
    int nextV;

    // DFS를 위한 스택의 초기화
    StackInit(&amp;stack);
    // 시작 정점을 방문
    VisitVertex(pg, visitV);
    // 시작 정점의 정보를 스택으로
    SPush(&amp;stack, visitV);

    // visitV에 담긴 정점과 연결된 정점의 방문을 시도하는 while문
    while(LFirst(&amp;(pg-&gt;adjList[visitV]), &amp;nextV) == TRUE)
    {
        // visitV와 연결된 정점의 정보가 nextV에 담긴 상태에서 이하를 진행
        int visitFlag = FALSE;

        if(VisitVertex(pg, nextV) == TRUE)    // 방문에 성공했다면
        {
            SPush(&amp;stack, visitV);    // visitV에 담긴 정점의 정보를 PUSH
            visitV = nextV;
            visitFlag = TRUE;
        }
        else    // 방문에 실패했다면, 연결된 다른 정점을 찾기
        {
            while(LNext(&amp;(pg-&gt;adjList[visitV]), &amp;nextV) == TRUE)
            {
                if(VisitVertex(pg, nextV) == TRUE)
                {
                    SPush(&amp;stack, visitV);
                    visitV = nextV;
                    visitFlag = TRUE;
                    break;
                }
            }
        }

        if(visitFlag == FALSE)    // 추가로 방문한 정점이 없었다면
        {
            // 스택이 비면 탐색의 시작점으로 되돌아 온 것
            if(SIsEmpty(&amp;stack) == TRUE)    // 시작점으로 되돌아 옴
                break;
            else
                visitV = SPop(&amp;stack);        // 길 되돌아 가기
        }
    }

    // 이후의 탐색을 위해서 탐색 정보 초기화
    memset(pg-&gt;visitInfo, 0, sizeof(int) * pg-&gt;numV);
}</code></pre>
<p>위 함수의 정의가 이해되지 않는다면 반복문과 기타 제어문을 중심으로 함수를 설명하는 아래 내용을 다시 한번 살펴보자.</p>
<pre><code class="language-c">void DFShowGraphVertex(ALGraph * pg, int startV)
{
    // - 함수의 앞 부분에서 시작 정점의 방문이 이뤄짐.

    // - 아래 while문에서 모든 정점의 방문이 이뤄짐.
    while(LFirst(&amp;(pg-&gt;adjList[visitV]), &amp;nextV) == TRUE)
    {
        // - 위의 LFirst 함수호출을 통해서 visitV에 연결된 정점 하나를 얻기.
        // - 이렇게 해서 얻은 정점의 정보는 nextV에 저장.
        // - nextV에 담긴 정점의 정보를 가지고 방문 시도.
        if(VisitVertex(pg, nextV) == TRUE)    // 방문에 성공했다면
        {
            // - nextV의 방문에 성공, visitV의 정보는 스택에 PUSH
            // - nextV에 담긴 정보를 visitV에 담고서 while문 다시 시작
            // - while문을 다시 시작하는 것은 또 다른 정점의 방문을 시도하는 것
        }
        // - LFirst 함수호출을 통해서 얻은 정점의 방문에 실패한 경우 else 구문 실행
        else
        {
            // - 아래의 while문은 visitV에 연결된 정점을 찾을 때까지 반복
            while(LNext(&amp;(pg-&gt;adjList[visitV]), &amp;nextV) == TRUE)
            {
                // - 위의 LNext 함수호출을 통해서 visitV에 연결된 정점 하나 얻기.
                // - 이렇게 해서 얻은 정점의 정보는 nextV에 저장.

                // - nextV에 담긴 정점의 정보를 가지고 방문 시도
                if(VisitVertex(pg, nextV) == TRUE)
                {
                // - nextV의 방문에 성공, visitV의 정보는 스택에 PUSH
                // - nextV에 담긴 정보를 visitV에 담고서 Break
                }
            }

            // - 정점 방문에 실패했다면 그에 따른 처리 진행
            if(visitFlag == FALSE)
            {
            // - 길을 되돌아 가거나 시작 위치로 되돌아와서 프로그램 종료
            }
        }
    }
}</code></pre>
<p>위에서 설명한 함수들을 모두 정리한 소스파일(ALGraphDFS.c)은 다음과 같다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &quot;ALGraphDFS.h&quot;
#include &quot;DLinkedList.h&quot;
#include &quot;ArrayBaseStack.h&quot;

int WhoIsPrecede(int data1, int data2);

// 그래프의 초기화
void GraphInit(ALGraph * pg, int nv)
{
    int i;    

    pg-&gt;adjList = (List*)malloc(sizeof(List)*nv);
    pg-&gt;numV = nv;
    pg-&gt;numE = 0;     // 초기의 간선 수는 0개

    for(i=0; i&lt;nv; i++)
    {
        ListInit(&amp;(pg-&gt;adjList[i]));
        SetSortRule(&amp;(pg-&gt;adjList[i]), WhoIsPrecede); 
    }

    // visitInfo 멤버 관련 추가 문장
    pg-&gt;visitInfo= (int *)malloc(sizeof(int) * pg-&gt;numV);
    memset(pg-&gt;visitInfo, 0, sizeof(int) * pg-&gt;numV);
}

// 그래프 리소스의 해제
void GraphDestroy(ALGraph * pg)
{
    if(pg-&gt;adjList != NULL)
        free(pg-&gt;adjList);

    // visitInfo 멤버 관련 추가 문장
    if(pg-&gt;visitInfo != NULL)
        free(pg-&gt;visitInfo);
}

// 간선의 추가
void AddEdge(ALGraph * pg, int fromV, int toV)
{
    LInsert(&amp;(pg-&gt;adjList[fromV]), toV);
    LInsert(&amp;(pg-&gt;adjList[toV]), fromV);
    pg-&gt;numE += 1;
}

// 유틸리티 함수 : 간선의 정보 출력
void ShowGraphEdgeInfo(ALGraph * pg)
{
    int i;
    int vx;

    for(i=0; i&lt;pg-&gt;numV; i++)
    {
        printf(&quot;%c와 연결된 정점: &quot;, i + 65);

        if(LFirst(&amp;(pg-&gt;adjList[i]), &amp;vx))
        {
            printf(&quot;%c &quot;, vx + 65);

            while(LNext(&amp;(pg-&gt;adjList[i]), &amp;vx))
                printf(&quot;%c &quot;, vx + 65);
        }
        printf(&quot;\n&quot;);
    }
}

int WhoIsPrecede(int data1, int data2)
{
    if(data1 &lt; data2)
        return 0;
    else
        return 1;
}

// 두 번째 매개변수로 전달된 이름의 정점에 방문
int VisitVertex(ALGraph * pg, int visitV)
{
    if(pg-&gt;visitInfo[visitV] == 0)
    {
        pg-&gt;visitInfo[visitV] = 1;
        printf(&quot;%c &quot;, visitV + 65);
        return TRUE;
    }

    return FALSE;
}

// 정점의 정보 출력: Depth First Search(DFS) 기반
void DFShowGraphVertex(ALGraph * pg, int startV)
{
    Stack stack;
    int visitV = startV;
    int nextV;

    // DFS를 위한 스택의 초기화
    StackInit(&amp;stack);
    VisitVertex(pg, visitV);    // 시작 정점을 방문
    SPush(&amp;stack, visitV);      // 시작 정점의 정보를 스택으로

    // visitV에 담긴 정점과 연결된 정점의 방문을 시도하는 while문
    while(LFirst(&amp;(pg-&gt;adjList[visitV]), &amp;nextV) == TRUE)
    {
        // visitV와 연결된 정점의 정보가 nextV에 담긴 상태에서 이하를 진행
        int visitFlag = FALSE;

        if(VisitVertex(pg, nextV) == TRUE)    // 방문에 성공했다면
        {
            SPush(&amp;stack, visitV);    // visitV에 담긴 정점의 정보를 PUSH
            visitV = nextV;
            visitFlag = TRUE;
        }
        else    // 방문에 실패했다면, 연결된 다른 정점을 찾기
        {
            while(LNext(&amp;(pg-&gt;adjList[visitV]), &amp;nextV) == TRUE)
            {
                if(VisitVertex(pg, nextV) == TRUE)
                {
                    SPush(&amp;stack, visitV);
                    visitV = nextV;
                    visitFlag = TRUE;
                    break;
                }
            }
        }

        if(visitFlag == FALSE)    // 추가로 방문한 정점이 없었다면
        {
            // 스택이 비면 탐색의 시작점으로 되돌아 온 것
            if(SIsEmpty(&amp;stack) == TRUE)    // 시작점으로 되돌아 옴
                break;
            else
                visitV = SPop(&amp;stack);        // 길 되돌아 가기
        }
    }

    // 이후의 탐색을 위해서 탐색 정보 초기화
    memset(pg-&gt;visitInfo, 0, sizeof(int) * pg-&gt;numV);
}</code></pre>
<p><strong>-실행파일 (DFSMain.c)</strong></p>
<p>위에서 구현한 함수들을 실제로 잘 작동하는지 확인할 수 있는 실행파일(Main 함수)는 다음과 같다.</p>
<pre><code class="language-c">include &lt;stdio.h&gt;
#include &quot;ALGraphDFS.h&quot;

int main(void)
{
    ALGraph graph;
    GraphInit(&amp;graph, 7);      // A, B, C, D, E, F, G 정점 생성성

    AddEdge(&amp;graph, A, B);
    AddEdge(&amp;graph, A, D);
    AddEdge(&amp;graph, B, C);
    AddEdge(&amp;graph, D, C);
    AddEdge(&amp;graph, D, E);
    AddEdge(&amp;graph, E, F);
    AddEdge(&amp;graph, E, G);

    ShowGraphEdgeInfo(&amp;graph);

    DFShowGraphVertex(&amp;graph, A); printf(&quot;\n&quot;);
    DFShowGraphVertex(&amp;graph, C); printf(&quot;\n&quot;);
    DFShowGraphVertex(&amp;graph, E); printf(&quot;\n&quot;);
    DFShowGraphVertex(&amp;graph, G); printf(&quot;\n&quot;);

    GraphDestroy(&amp;graph);
    return 0;
}

&gt; gcc .\ALGraphDFS.c .\ArrayBaseStack.c .\DFSMain.c .\DLinkedList.c
&gt; .\a.exe
&gt; 출력
A와 연결된 정점: B D 
B와 연결된 정점: A C 
C와 연결된 정점: B D 
D와 연결된 정점: A C E 
E와 연결된 정점: D F G 
F와 연결된 정점: E 
G와 연결된 정점: E 
A B C D E F G 
C B A D E F G 
E D A B C F G 
G E D A B C F </code></pre>
<p>실행 결과 어디서 시작을 하든지 모든 정점에 방문함을 볼 수 있다.
실행결과를 참조해서 main 함수에서 형성한 그래프의 모습을 그려볼 수도 있어 DFS를 기반으로 한 정점의 방문에 문제가 없었는지 대략적 검토가 가능하다.</p>
<h3 id="너비-우선-탐색bfs-구현"><em>너비 우선 탐색(BFS) 구현</em></h3>
<h4 id="1-모델-및-이해-1">1) 모델 및 이해</h4>
<p>BFS의 구현을 위해서 필요한 2가지는 다음과 같다.</p>
<ul>
<li>큐: 방문 차례의 기록을 목적으로</li>
<li>배열: 방문 정보의 기록을 목적으로</li>
</ul>
<p>BFS의 구현을 그림을 통해 먼저 이해해보자.</p>
<table>
<thead>
<tr>
<th>Pic.</th>
<th>Expl.</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/48ff66b5-633d-4976-a9b4-2769c3bee752/image.png" alt=""></td>
<td>큐에는 다음 단계에서 연락을 받은 두 사람의 이름(정보)이 순서대로 들어감.</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/9e434a62-4484-49ce-bfd8-9bbc3ca33a8c/image.png" alt=""></td>
<td>동수는 연락을 받기만 했을 뿐 아직 연락을 취하지 않는 대상.<br>큐에서 이름을 하나 꺼내 그 이름의 정점에 연결된 모든 사람에게 연락.</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/b742a513-d3f1-404c-83c3-a5d96f19abf4/image.png" alt=""></td>
<td>동수를 꺼내 동수를 기준으로 연락.<br>이때 연락을 받은 지민이 큐에 들어감.<br>BFS에 있어서 큐는 연락을 취할 정점의 순서를 기록하기 위한 것.</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/6fb4d4e9-4174-4cfa-856c-0a8a74d88050/image.png" alt=""></td>
<td>큐에서 꺼낸 이름 민석을 중심으로 연결된 수정과 정희가 큐에 들어감.<br>다음 차례인 지민과 수정의 이름을 큐에서 순서대로 꺼내는데<br>이 둘은 연락을 취할 대상이 없음.</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/f5416f72-f294-4eea-be94-f789e1bcf63f/image.png" alt=""></td>
<td>마지막으로 정희의 이름까지 꺼내 명석에게 연락.<br>이때 명석이 큐에 들어갔다가 연락할 대상이 없어 큐에서 빠져나와 종료.</td>
</tr>
</tbody></table>
<h4 id="2-실제-구현-1">2) 실제 구현</h4>
<p>BFS 구현을 위해 필요한 파일은 다음과 같다.</p>
<ul>
<li>ALGraphBFS.h, ALGraphBFS.c : BFS 알고리즘 관련 그래프 함수의 선언과 정의 관련</li>
<li><a href="https://velog.io/@mingming_eee/datastructure-07#%EC%9B%90%ED%98%95-%ED%81%90%EC%9D%98-%EA%B5%AC%ED%98%84">CircularQueue.h, CircularQueue.c</a> : 큐 관련 (Chapter 07에서 구현)</li>
<li>DLinkedList.h, DLinkedList.c : 연결 리스트 관련 (Chapter 04에서 구현)</li>
<li>BFSMain.c : main 함수 관련</li>
</ul>
<p><strong>-헤더파일 (ALGraphBFS.h)</strong></p>
<p>DFS와 마찬가지로 BFS에서도 구현결과를 <code>void BFShowGraphVertex(ALGraph * pg, int startV);</code> 에 담을 것이다.
이 함수의 선언이 추가된 헤더파일 ALGraphBFS.h을 먼저 살펴보자.</p>
<pre><code class="language-c">#ifndef __AL_GRAPH_BFS__
#define __AL_GRAPH_BFS__

#include &quot;DLinkedList.h&quot;

// 정점의 이름들을 상수화화
enum {A, B, C, D, E, F, G, H, I, J};

typedef struct _ual
{
    int numV;           // 정점의 수
    int numE;           // 간선의 수
    List * adjList;     // 간선의 정보
    int * visitInfo;
} ALGraph;

// 그래프의 초기화
void GraphInit(ALGraph * pg, int nv);

// 그래프의 리소스 해제
void GraphDestroy(ALGraph * pg);

// 간선의 추가
void AddEdge(ALGraph * pg, int fromV, int toV);

// 그래프의 간선 정보 출력
void ShowGraphEdgeInfo(ALGraph * pg);

// 정점의 정보 출력: Breadth First Search(BFS) 기반
void BFShowGraphVertex(ALGraph * pg, int startV);

#endif</code></pre>
<p><strong>-소스파일 (ALGraphBFS.c)</strong></p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &quot;ALGraphBFS.h&quot;
#include &quot;DLinkedList.h&quot;
#include &quot;CircularQueue.h&quot;

int WhoIsPrecede(int data1, int data2);

// 그래프의 초기화
void GraphInit(ALGraph * pg, int nv)
{
    int i;

    pg-&gt;adjList = (List*)malloc(sizeof(List)*nv);
    pg-&gt;numV = nv;
    pg-&gt;numE = 0;     // 초기의 간선 수는 0개

    for(i=0; i&lt;nv; i++)
    {
        ListInit(&amp;(pg-&gt;adjList[i]));
        SetSortRule(&amp;(pg-&gt;adjList[i]), WhoIsPrecede); 
    }

    // visitInfo 멤버 관련 추가 문장
    pg-&gt;visitInfo= (int *)malloc(sizeof(int) * pg-&gt;numV);
    memset(pg-&gt;visitInfo, 0, sizeof(int) * pg-&gt;numV);
}

// 그래프 리소스의 해제
void GraphDestroy(ALGraph * pg)
{
    if(pg-&gt;adjList != NULL)
        free(pg-&gt;adjList);

    if(pg-&gt;visitInfo != NULL)
        free(pg-&gt;visitInfo);
}

// 간선의 추가
void AddEdge(ALGraph * pg, int fromV, int toV)
{
    LInsert(&amp;(pg-&gt;adjList[fromV]), toV);
    LInsert(&amp;(pg-&gt;adjList[toV]), fromV);
    pg-&gt;numE += 1;
}

// 유틸리티 함수 : 간선의 정보 출력
void ShowGraphEdgeInfo(ALGraph * pg)
{
    int i;
    int vx;

    for(i=0; i&lt;pg-&gt;numV; i++)
    {
        printf(&quot;%c와 연결된 정점: &quot;, i + 65);

        if(LFirst(&amp;(pg-&gt;adjList[i]), &amp;vx))
        {
            printf(&quot;%c &quot;, vx + 65);

            while(LNext(&amp;(pg-&gt;adjList[i]), &amp;vx))
                printf(&quot;%c &quot;, vx + 65);
        }
        printf(&quot;\n&quot;);
    }
}

int WhoIsPrecede(int data1, int data2)
{
    if(data1 &lt; data2)
        return 0;
    else
        return 1;
}

// 두 번째 매개변수로 전달된 이름의 정점에 방문
int VisitVertex(ALGraph * pg, int visitV)
{
    if(pg-&gt;visitInfo[visitV] == 0)
    {
        pg-&gt;visitInfo[visitV] = 1;
        printf(&quot;%c &quot;, visitV + 65);    // �湮 ���� ���
        return TRUE;
    }

    return FALSE;
}

// 정점의 정보 출력: Breadth First Search(BFS) 기반
void BFShowGraphVertex(ALGraph * pg, int startV)
{
    Queue queue;
    int visitV = startV;
    int nextV;

    QueueInit(&amp;queue);

    // 시작 정점 탐색색
    VisitVertex(pg, visitV);

    // while문에서 visitV와 연결된 모든 정점에 방문문
    while(LFirst(&amp;(pg-&gt;adjList[visitV]), &amp;nextV) == TRUE)
    {
        if(VisitVertex(pg, nextV) == TRUE)
            Enqueue(&amp;queue, nextV);     // nextV에 방문했으니 큐에 저장

        while(LNext(&amp;(pg-&gt;adjList[visitV]), &amp;nextV) == TRUE)
        {
            if(VisitVertex(pg, nextV) == TRUE)
                Enqueue(&amp;queue, nextV); // nextV에 방문했으니 큐에 저장
        }

        if(QIsEmpty(&amp;queue) == TRUE)    // 큐가 비면 BFS 종료료
            break;
        else
            visitV = Dequeue(&amp;queue);    // 큐에서 하나 꺼내어 while문 다시 시작작
    }

    // 탐색 정보 초기화
    memset(pg-&gt;visitInfo, 0, sizeof(int) * pg-&gt;numV);
}</code></pre>
<p>내용은 다르지만 코드의 성경 상당부분이 DFShowGraphVertex 함수와 유사하기 때문에 주석으로 충분이 이해할 수 있는 코드일 것이다.</p>
<p><strong>-실행파일 (BFSMain.c)</strong></p>
<p>위에서 구현한 함수들이 실제로 잘 작동하는지 확인할 수 있는 실행파일(Main 함수)는 다음과 같다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &quot;ALGraphBFS.h&quot;

int main(void)
{
    ALGraph graph;
    GraphInit(&amp;graph, 7);

    AddEdge(&amp;graph, A, B);
    AddEdge(&amp;graph, A, D);
    AddEdge(&amp;graph, B, C);
    AddEdge(&amp;graph, D, C);
    AddEdge(&amp;graph, D, E);
    AddEdge(&amp;graph, E, F);
    AddEdge(&amp;graph, E, G);

    ShowGraphEdgeInfo(&amp;graph);

    BFShowGraphVertex(&amp;graph, A); printf(&quot;\n&quot;);
    BFShowGraphVertex(&amp;graph, C); printf(&quot;\n&quot;);
    BFShowGraphVertex(&amp;graph, E); printf(&quot;\n&quot;);
    BFShowGraphVertex(&amp;graph, G); printf(&quot;\n&quot;);

    GraphDestroy(&amp;graph);
    return 0;
}

&gt; gcc .\ALGraphBFS.c .\BFSMain.c .\CircularQueue.c .\DLinkedList.c  
&gt; .\a.exe
A와 연결된 정점: B D 
B와 연결된 정점: A C
C와 연결된 정점: B D
D와 연결된 정점: A C E
E와 연결된 정점: D F G
F와 연결된 정점: E
G와 연결된 정점: E
A B D C E F G
C B D A E F G
E D F G A C B
G E D F A C B</code></pre>
<hr>
<h2 id="14-4-최소-비용-신장-트리-minimum-cost-spanning-tree">14-4. 최소 비용 신장 트리 (Minimum Cost Spanning Tree)</h2>
<p>사실 트리는 그래프의 한 유형이다...!</p>
<h3 id="사이클cycle을-형성하지-않는-그래프"><em>사이클(Cycle)을 형성하지 않는 그래프</em></h3>
<p>다음 그래프를 보고 정점B에서 정점D에 이르는 모든 경로(path)를 찾아서 열거해보자.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/a763ad7c-62f0-49e8-bb82-01f86118fb48/image.png" alt=""></p>
<p>경로는 총 4개며 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/70d1a54b-cef7-4929-a330-82b272972191/image.png" alt=""></p>
<p>이렇든 두 개의 정점을 잇는 간선을 순서대로 나열한 것을 경로라 한다.
즉, 위의 네 경로는 정점B에서 정점D에 이르는 경로가 된다.
그리고 위 네 경로와 같이 동일한 간선을 중복하여 포함하지 않는 경로를 가리켜 &#39;단순 경로(simple path)&#39;라 한다.
단순 경로가 아닌 예시는 <code>B–A–C–B–A–D</code> (B와 A를 잇는 간선이 두 번 포함됨)와 같은 것이 있다.</p>
<p><code>A-B-C-A</code>와 같은 경로도 단순 경로로, 중복된 간선이 포함되어 있지 않고, 시작과 끝이 같은 경로로 &#39;사이클(cycle)&#39;이라 한다.</p>
<p>이번에 구성할 그래프는 다음 그림의 예시들과 같이 사이클을 형성하지 않는 그래프이다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/920adb5b-015d-42ff-82be-8c55bc9919c6/image.png" alt=""></p>
<p>이러한 종류의 그래프를 가리켜 &#39;신장 트리(spanning tree)&#39;라고 한다.
위 그래프를 회전시켜 살펴보면 트리라고 하는 이유를 더 잘 알 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/64852a85-af34-4526-b239-2674ff5abe46/image.png" alt=""></p>
<h3 id="최소-비용-신장-트리의-이해와-적용"><em>최소 비용 신장 트리의 이해와 적용</em></h3>
<p>신장 트리의 특징 2가지는 다음과 같다.</p>
<ul>
<li>그래프의 모든 정점이 간선에 의해서 하나로 연결되어 있다.</li>
<li>그래프 내에서 사이클을 형성하지 않는다.</li>
</ul>
<p>물론 다음과 같이 가중치 그래프를 대상으로도, 그리고 그림으로는 보이지 않지만 간선에 방향성이 부여된 방향 그래프를 대상으로도 신장 트리를 구성할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/4d4578e6-9f86-451d-bb23-1e918a0475c9/image.png" alt=""></p>
<p>그리고 신장 트리의 모든 간선의 가중치 합이 최소인 그래프를 가리켜 &#39;최소 비용 신장 트리(minimum cost spanning tree)&#39; 또는 &#39;최소 신장 트리(minimum spanning tree)&#39;라 한다.
즉, 위 그래프의 최소 비용 신장 트리는 다음의 형태가 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/659ef1b0-ef15-4af9-ba71-679d7b725871/image.png" alt=""></p>
<p>이렇듯 줄여서 MST라 표현되는 &#39;최소 비용 신장 트리&#39;는 개념적으로 간단하다.</p>
<p>MST를 활용한 예시로, 강원, 경기, 경북, 울산, 전북을 직선으로 연결하는 물류에 특화된 도로를 건설한다고 가정해보자.
그렇다면 다음과 같은, 모든 지역이 직선으로 연결된 이상적인 환경을 기대할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/15a52a3a-5fcb-442b-80fd-29f2eeff25d4/image.png" alt=""></p>
<p>하지만 직선으로 연결하는 도로 건설에는 비용이 많이 드니 다섯 개의 지역이 모두 연결되게 하되, 그 거리를 최소화하는 형태로 도로를 건설한다고 생각해보자.
그리고 이를 목적으로 지역간 직선거리를 조사하여 다음과 같이 그림이 완성되었다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/04342569-0e0e-4aab-93ab-b53fd42de8aa/image.png" alt=""></p>
<p>이렇게 보니 무방향 가중치 그래프와 모습이 동일하다.
위 그래프를 대상으로 최소 비용 신장 트리를 구하면 된다.
그리고 그 모습은 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/eb2dd646-4603-4636-94ea-cbd001a5b1f9/image.png" alt=""></p>
<h3 id="최소-비용-신장-트리의-구성을-위한-알고리즘---크루스칼kruskal-알고리즘"><em>최소 비용 신장 트리의 구성을 위한 알고리즘 - 크루스칼(Kruskal) 알고리즘</em></h3>
<p>최소 비용 신장 트리의 구성에 사용되는 대표적인 알고리즘 2가지는 다음과 같다.</p>
<ul>
<li><p>크루스칼(Kruskal) 알고리즘
: 가중치를 기준으로 간선을 정렬한 후에 MST가 될 때까지 간선을 하나씩 선택 또는 삭제해 나가는 방식</p>
</li>
<li><p>프림(Prim) 알고리즘
: 하나의 정점을 시작으로 MST가 될 때까지 트리를 확장해 나가는 방식</p>
</li>
</ul>
<p>물론 이 2가지 알고리즘 말고도 다른 알고리즘이 더 있다.
하지만 하나의 알고리즘을 정확하게 이해하고 이를 구현하는 것이 더 의미있고 이 경험이 중요하기 대문에 이 2가지 중 MST를 보다 더 대표하는 크루스칼 알고리즘에 대해 더 알아보고 이를 이용해 구현하려한다.</p>
<h4 id="1-이해---모델-1">1) 이해 - 모델 1</h4>
<p>크루스칼 알고리즘의 핵심은 가중치를 기준으로 간선을 정렬한다는 것에 있다.
예시 그래프 하나를 크루스칼 알고리즘을 활용해 MST가 되게 하려고 한다.</p>
<table>
<thead>
<tr>
<th>Pic.</th>
<th>Expl.</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/87b85558-fcd6-4a3b-8a6b-724f3e8b78c4/image.png" alt=""></td>
<td>가중치를 기준으로 간선을 오름차순으로 정렬.<br>간선의 가중치가 중복되지 않기 때문에 가중치 정보만을 오름차순으로 정렬.</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/2ad1bae0-1608-469f-ae04-e23660aa6a92/image.png" alt=""></td>
<td>가중치가 가장 낮은 가중치 2 간선(B-C)를 추가.</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/f5c244f4-9411-4e92-bb1b-c1860ec75948/image.png" alt=""></td>
<td>그 다음으로 가중치가 낮은 간선 추가.<br>(D-E),(D-F),(C-D)</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/78036c1d-1700-416c-9b17-67f3a98b8ecb/image.png" alt=""></td>
<td>그 다음 가중치가 낮은 가중치 7 간선 추가하려 했으나,<br>그래프 내에서 사이클이 발생.<br>해당 간선 건너뛰고 그 다음 간선 추가.</td>
</tr>
</tbody></table>
<p>이로써 추가된 간선의 수가 정점의 수보다 하나 작은 5개가 되었고 알고리즘이 종료된다.
MST는 $$간선의 수 + 1 = 정점의 수$$라는 식을 만족하기 때문이다.</p>
<p>따라서 위 그래프의 MST는 다음과 같이 형성된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/597158d6-c544-4227-b820-c6662a0f717d/image.png" alt=""></p>
<p>지금까지 배운 크루스칼 알고리즘의 흐름에 있어 핵심되는 사항은 다음과 같다.</p>
<ul>
<li>가중치를 기준으로 간선을 오름차순으로 정렬</li>
<li>낮은 가중치의 간선부터 시작해서 하나씩 그래프에 추가</li>
<li>사이클을 형성하는 간선은 추가하지 않기</li>
<li>간선의 수가 정점의 수보다 하나 적을 때 MST 완성</li>
</ul>
<p>앞서 언급했듯이 크루스칼 알고리즘의 핵심은 가중치를 기준으로 간선을 정렬한다는데 있다.
하지만 반드시 오름차순으로 정렬해야 하는 것은 아니다.
크루스칼 알고리즘에는 내림차순으로 정렬된 상황에서 적용할 수 있는 모델도 있기 때문이다.</p>
<h4 id="2-이해---모델-2">2) 이해 - 모델 2</h4>
<p>간선을 내림차순으로 정렬하면 낮은 가중치의 간선을 하나씩 추가하는 방식이 아니라 높은 가중치의 간선을 하나씩 빼는 방식으로 알고리즘이 전개된다.
그래서 모델 1과 2로 구분하기도 한다.
그럼 다음 예시로 크루스칼 알고리즘의 다른 적용 모델에 대해 알아보자.</p>
<table>
<thead>
<tr>
<th>Pic.</th>
<th>Expl.</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/dcd36ae7-6097-4be3-87ea-b8bc97dfc555/image.png" alt=""></td>
<td>내림차순으로 가중치를 정렬.</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/dee829f2-5f62-4150-866a-4b17494cc0be/image.png" alt=""></td>
<td>가중치가 가장 높은 가중치 13 간선(E-F) 삭제.</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/1406c023-0bba-4786-a1d7-83b2be44b28c/image.png" alt=""></td>
<td>그 다음으로 가중치가 높은 간선 삭제.<br>(A-C), (A-F), (A-B)<br>그 다음으로 가중치가 높은 간선인 가중치 8 간선 삭제.<br>이 간선을 삭제시 문제 발생.</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/0123ad78-3c3f-4c8b-a959-f21180cc44e1/image.png" alt=""></td>
<td>가중치 8 간선을 삭제시 정점A와 정점D를 잇는 경로가 없음.<br>(단, 직접적인 경로가 아니더라도 다른 경로를 통해 연결되어 있는 경우에는 삭제 가능.)<br>그 다음으로 가중치가 큰 가중치 7 간선(C-E) 삭제.</td>
</tr>
</tbody></table>
<p>이로써 MST가 완성되었다. (정점의 수가 간선의 수보다 하나 더 많은 상황이기 때문)
두 번째 크루스칼 알고리즘의 핵심되는 사항은 다음과 같다.</p>
<ul>
<li>가중치를 기준으로 간선을 내림차순으로 정렬</li>
<li>높은 가중치의 간선부터 시작해서 하나씩 그래프에서 제거</li>
<li>두 정점을 연결하는 다른 경로가 없을 경우 해당 간선은 제거하지 않기</li>
<li>간선의 수가 정점의 수보다 하나 적을 때 MST 완성</li>
</ul>
<h4 id="3-구현을-위한-계획">3) 구현을 위한 계획</h4>
<p>이전에 구현한 것들을 최대한 활용해서 크루스칼 알고리즘을 구현하려고 한다.
구현할 알고리즘은 가중치를 기준으로 간선을 내림차순으로 정렬한 다음 높은 가중치의 간선부터 시작해서 하나씩 그래프에서 제거하는 방식을 따라갈 것이다.</p>
<p>이를 위해서 DFS의 구현결과인 다음 파일들을 활용할 것이다.</p>
<ul>
<li>DLinkedList.h, DLinkedList.c : 연결 리스트</li>
<li>ArrayBaseStack.h, ArrayBaseStack.c : 배열 기반 스택</li>
<li>ALGraphDFS.h, ALGraphDFS.c : 깊이 우선 탐색을 포함하는 그래프</li>
</ul>
<p>이 중에서 ALGraphDFS.h와 ALGraphDFS.c에 담겨있는 대표적인 기능은 &#39;그래프의 구성&#39;, &#39;DFS 기반의 정점 정보 출력&#39;이다.
크루스칼 알고리즘의 구현을 위해서는 먼저 그래프를 구성해야 하기 때문에 이는 유용하게 활용된다.
하지만 가중치 그래프를 구성해야 하기 때문에 ALGraphDFS.h와 ALGraphDFS.c은 활용은 하되 많은 부분이 수정되어야 한다. 그래서 이 두 파일을 수정하고 확장하여 <code>ALGraphKruskal.h</code>과 <code>ALGraphKruskal.c</code>로 파일명을 변경해 사용할 것이다.</p>
<p>크루스칼 알고리즘을 구현하기 위해서는 간선을 삭제해도 이 간선에 의해 연결된 두 정점을 연결하는 경로가 있는지 확인을 해야하는데 이는 DFS 알고리즘을 활용해 확인할 것이다.</p>
<p>가중치를 기준으로 간선을 정렬할 수 있어야하는데, 이 부분을 위해서는 우선순위 큐를 활용할 것이다.</p>
<ul>
<li>PriorityQueue.h, PriorityQueue.c : 우선순위 큐</li>
<li>UsefulHeap.h, UsefulHeap.c : 우선순위 큐의 기반이 되는 힙</li>
</ul>
<p>그리고 가중치 그래프의 표현을 위해서는 가중치가 포함된 간선의 정보를 담을 수 있어야하기 때문에 다음과 같은 헤더파일이 추갈 필요하다.</p>
<ul>
<li>ALEdge.h : 가중치가 포함된 간선의 표현을 위한 구조체 정의</li>
</ul>
<p>정리하자면 다음과 같은 파일들이 크루스칼 알고리즘 구현에 필요하고 하나로 묶여야 한다. 
(여태까지 배운 것의 총집합이라 할 수 있다.)</p>
<ul>
<li>DLinkedList.h, DLinkedList.c : 연결 리스트</li>
<li>ArrayBaseStack.h, ArrayBaseStack.c : 배열 기반 스택</li>
<li>ALGraphDFS.h, ALGraphDFS.c : 깊이 우선 탐색을 포함하는 그래프</li>
<li>PriorityQueue.h, PriorityQueue.c : 우선순위 큐</li>
<li>UsefulHeap.h, UsefulHeap.c : 우선순위 큐의 기반이 되는 힙</li>
<li>ALEdge.h : 가중치가 포함된 간선의 표현을 위한 구조체 정의</li>
</ul>
<h4 id="4-구현">4) 구현</h4>
<p><strong>-헤더파일 (ALEdge.h, ALGraphKruskal.h)</strong></p>
<p>우선 추가되는 헤더파일인 ALEdge.h 다음과 같다.</p>
<pre><code class="language-c">#ifndef __AL_EDGE__
#define __AL_EDGE__

typedef struct _edge
{
    int v1;            // 간선이 연결하는 첫 번째 정점
    int v2;            // 간선이 연결하는 두두 번째 정점
    int weight;        // 간선의 가중치
} Edge;

#endif</code></pre>
<p>이 구조체의 목적은 간선 정보의 저장이 아니다.
이 구조체의 실질적인 목적은 간선의 가중치 정보를 저장한다는 것에 잇다.
간선의 가중치 정보를 별도로 저장하는 이유는 이 정보를 대상으로 크루스칼 알고리즘의 핵심인 가중치 기반의 정렬을 진행하기 위함이다.</p>
<p>다음으로 ALGraphDFS.h을 약간 변형한 ALGraphKruskal.h을 살펴보자.</p>
<pre><code class="language-c">#ifndef __AL_GRAPH_KRUSKAL__
#define __AL_GRAPH_KRUSKAL__

#include &quot;DLinkedList.h&quot;
#include &quot;PriorityQueue.h&quot;

#include &quot;ALEdge.h&quot;

enum {A, B, C, D, E, F, G, H, I, J};

typedef struct _ual
{
    int numV;
    int numE;
    List * adjList;
    int * visitInfo;
    PQueue pqueue;        // 간선의 가중치 정보 저장
} ALGraph;

// 이전 함수의 정의와 차이가 있음
void GraphInit(ALGraph * pg, int nv);

// 이전 함수와 동일
void GraphDestroy(ALGraph * pg);

// 이전 함수의 정의와 차이가 있음
void AddEdge(ALGraph * pg, int fromV, int toV, int weight);

// 이전 함수와 동일
void ShowGraphEdgeInfo(ALGraph * pg);

// 이전 함수와 동일
void DFShowGraphVertex(ALGraph * pg, int startV);

// 새로 추가된 함수
// 최소 비용 신장 트리의 구성성
void ConKruskalMST(ALGraph * pg);

// 가중치 정보 출력력
void ShowGraphEdgeWeightInfo(ALGraph * pg);

#endif</code></pre>
<p>구조체 ALGraph에서 <code>PQueue pqueue;</code>라는 멤버가 하나 추가되었다.</p>
<p><strong>-주요 함수 정의</strong></p>
<p>구조체에서 우선순위 큐가 멤버로 추가되었고 이는 간선의 가중치 정보를 나타내는 구조체인 Edge의 변수를 저장하기 위함이다.
이로 인해서 GraphInit 함수에는 우선순위 큐의 초기화를 위한 문장이 추가되어야 한다.</p>
<pre><code class="language-c">void GraphInit(ALGraph * pg, int nv)
{
    ....
    // 우선순위 큐의 초기화
    PQueueInit(&amp;(pg-&gt;pqueue), PQWeightComp);
}</code></pre>
<p>그리고 우선순위 큐의 우선순위 비교기준인, PQueueInit 함수의 인자로 전달된 PQWeightComp 함수도 정의되어야 한다.</p>
<pre><code class="language-c">int PQWeightComp(Edge d1, Edge d2)
{
    return d1.weight - d2.weight;
}</code></pre>
<p>첫 번째 인자로 전달된 간선의 가중치가 클 때 양수가 반환되도록 정의되었다.
따라서 가중치를 기준으로 내림차순으로 간선의 정보를 꺼낼 수 있게 되었다.
(이는 크루스칼 알고리즘의 구현을 위한 것이다.)</p>
<p>이어서 간선을 추가할 때 호출하는 AddEdge 함수를 보자.
간선에 가중치 정보가 포함되었기 때문에 다음과 같이 변경되어야 한다.</p>
<pre><code class="language-c">void AddEdge(ALGraph * pg, int fromV, int toV, int weight)
{
    Edge edge = {fromV, toV, weight};     // 간선의 가중치 정보를 담음

    LInsert(&amp;(pg-&gt;adjList[fromV]), toV);
    LInsert(&amp;(pg-&gt;adjList[toV]), fromV);
    pg-&gt;numE += 1;

    // 간선의 가중치 정보를 우선순위 큐에 저장장
    PEnqueue(&amp;(pg-&gt;pqueue), edge);
}</code></pre>
<p>이렇듯 AddEdge 함수에 간선의 가중치 정보를 우선순위 큐에 담는 문장이 추가되었고 가중치 정보가 더불어 전달되도록 매개변수의 수도 하나 늘었다.
물론 우선순위 큐의 저장 대상이 구조체 Edge의 변수이므로 UsefulHeap.h에 다음 #include문을 포함시키고, typedef 선언도 변경해야 한다.</p>
<pre><code class="language-c">#include &quot;ALEdge.h&quot;

typedef Edge HData;</code></pre>
<p>마지막으로 크루스칼 알고리즘의 실질적인 구현결과를 나타내줄 함수는 <code>void ConKruskalMST(ALGraph * pg);</code>이다.
이 함수를 호출하면서 그래프의 주소 값을 전달하면, 전달된 주소 값의 그래프는 최소 비용 신장 트리가 된다.</p>
<pre><code class="language-c">// 크루스칼 알고리즘 기반 MST의 구성
void ConKruskalMST(ALGraph * pg)
{
    Edge recvEdge[20];    // 복원할 간선의 정보 저장
    Edge edge;
    int eidx = 0;
    int i;

    // MST를 형성할 때까지 아래의 while문 반복복
    while(pg-&gt;numE+1 &gt; pg-&gt;numV)        // MST 간선의 수 + 1 == 정점의 수
    {
        // 우선순위 큐에서 가중치가 제일 높은 간선의 정보를 꺼냄
        edge = PDequeue(&amp;(pg-&gt;pqueue));

        // 우선순위 큐에서 꺼낸 간선을 실제로 그래프에서 삭제
        RemoveEdge(pg, edge.v1, edge.v2);

        // 간선을 삭제하고 나서도 두 정점을 연결하는 경로가 있는지 확인
        if(!IsConnVertex(pg, edge.v1, edge.v2))
        {
            // 경로가 없다면 삭제한 간선을 복원
            RecoverEdge(pg, edge.v1, edge.v2, edge.weight);

            // 복원한 간선의 정보를 별도로 저장
            recvEdge[eidx++] = edge;
        }
    }

    // 우선순위 큐에서 삭제된 간선의 정보를 회복
    for(i=0; i&lt;eidx; i++)
        PEnqueue(&amp;(pg-&gt;pqueue), recvEdge[i]);

}</code></pre>
<p>위 함수에서는 아직 소개하지 않은 함수를 호출하고 있는데, 그 함수들의 기능을 정리하면 다음과 같다.</p>
<ul>
<li>RemoveEdge : 그래프에서 간선을 삭제</li>
<li>IsConnVertex : 두 정점이 연결되어 있는지 확인</li>
<li>RecoverEdge : 삭제된 간선을 다시 삽입</li>
</ul>
<p>그리고 ConKruskalMST 함수에서 복원한 간선의 정보를 우선순위 큐에 넣지 않고 별도로 저장하는 이유는 우선순위 큐에 다시 넣으면 PDequeue 함수 호출 시 다시 꺼내게 되기 때문이다.
크루스칼 알고리즘에서는 한번 검토가 이뤄진 간선은 재검토하지 않는다.
따라서 복원된 간선을 우선순위 큐에 다시 넣으면 안된다.
그리고 이렇게 별도로 저장한 간선의 정보는 반복문을 빠져나간 후에 다음과 같이 for문으로 우선순위 큐에 다시 넣는다.
때문에 위의 반복문까지 실행되면 우선순위 큐에는 MST를 이루는 간선의 정보만 남게 된다.
그리고 이렇게 우선순위 큐에 남은 간선의 가중치 정보를 출력하기 위해 <code>void ShowGraphEdgeWeightInfo(ALGraph * pg);</code>라는 함수를 선언 및 정의했다.</p>
<pre><code class="language-c">void ShowGraphEdgeWeightInfo(ALGraph * pg)
{
    PQueue copyPQ = pg-&gt;pqueue;
    Edge edge;

    while(!PQIsEmpty(&amp;copyPQ))
    {
        edge = PDequeue(&amp;copyPQ);
        printf(&quot;(%c-%c), w:%d \n&quot;, edge.v1+65, edge.v2+65, edge.weight);
    }
}</code></pre>
<p>이제 남은 함수는 위에서 정리한 <code>RemoveEdge</code>, <code>IsConnVertex</code>, <code>RecoverEdge</code> 함수의 정의를 살펴보자.</p>
<pre><code class="language-c">// 간선의 소멸
void RemoveEdge(ALGraph * pg, int fromV, int toV)
{
    RemoveWayEdge(pg, fromV, toV);
    RemoveWayEdge(pg, toV, fromV);
    (pg-&gt;numE)--;
}

// 한쪽 방향의 간선 소멸
void RemoveWayEdge(ALGraph * pg, int fromV, int toV)
{
    int edge;

    if(LFirst(&amp;(pg-&gt;adjList[fromV]), &amp;edge))
    {
        if(edge == toV)
        {
            LRemove(&amp;(pg-&gt;adjList[fromV]));
            return;
        }

        while(LNext(&amp;(pg-&gt;adjList[fromV]), &amp;edge))
        {
            if(edge == toV)
            {
                LRemove(&amp;(pg-&gt;adjList[fromV]));
                return;
            }
        }
    }
}</code></pre>
<p>구현하는 그래프가 무방향 그래프이다 보니 소멸시킬 간선의 정보가 두개다.
그래서 각각의 소멸을 위한 RemoveWayEdge 함수를 정의하고 RemoveEdge 함수가 이를 두 번 호출하는 형태로 정의되었다.</p>
<p>다음은 RecoverEdge 함수다.</p>
<pre><code class="language-c">// 삭제된 간선을 다시 삽입
void RecoverEdge(ALGraph * pg, int fromV, int toV)
{
    LInsert(&amp;(pg-&gt;adjList[fromV]), toV);
    LInsert(&amp;(pg-&gt;adjList[toV]), fromV);
    (pg-&gt;numE)++;
}</code></pre>
<p>이는 분명 AddEdge 함수와 다르다.
위 함수들은 어디까지나 ConKruskalMST 함수를 위해서 정의한, ConKruskalMST 함수를 돕는 함수일 뿐이다.
간선의 삽입과 삭제의 과정에서 해당 간선의 가중치 정보를 고려하지 않은 점에서 이를 알 수 있다.</p>
<p>마지막으로 두 정점의 연결을 확인하는 IsConnVertex 함수를 살펴보자.
이는 앞에서 얘기한 DFShowGraphVertex 함수를 수정한 것이다.</p>
<pre><code class="language-c">//  인자로 전달된 두 정점이 연결되어 있다면 TRUE, 그렇지 않다면 FALSE 반환
int IsConnVertex(ALGraph * pg, int v1, int v2)
{
    Stack stack;
    int visitV = v1;
    int nextV;

    StackInit(&amp;stack);
    VisitVertex(pg, visitV);
    SPush(&amp;stack, visitV);

    while(LFirst(&amp;(pg-&gt;adjList[visitV]), &amp;nextV) == TRUE)
    {
        int visitFlag = FALSE;

        // 정점을 돌아다니는 도중에 목표를 찾는다면 TURE를 반환
        if(nextV == v2)
        {
            // 함수가 반환하기 전에 초기화 진행
            memset(pg-&gt;visitInfo, 0, sizeof(int) * pg-&gt;numV);
            return TRUE;    // 목표를 찾았으니 TRUE 반환
        }

        if(VisitVertex(pg, nextV) == TRUE)
        {
            SPush(&amp;stack, visitV);
            visitV = nextV;
            visitFlag = TRUE;
        }
        else
        {
            while(LNext(&amp;(pg-&gt;adjList[visitV]), &amp;nextV) == TRUE)
            {
                // 정점을 돌아다니는 도중에 목표를 찾는다면 TRUE를 반환
                if(nextV == v2)
                {
                    // 함수가 반환하기 전에 초기화를 진행
                    memset(pg-&gt;visitInfo, 0, sizeof(int) * pg-&gt;numV);
                    return TRUE;
                }

                if(VisitVertex(pg, nextV) == TRUE)
                {
                    SPush(&amp;stack, visitV);
                    visitV = nextV;
                    visitFlag = TRUE;
                    break;
                }
            }
        }

        if(visitFlag == FALSE)
        {
            if(SIsEmpty(&amp;stack) == TRUE)
                break;
            else
                visitV = SPop(&amp;stack);    
        }
    }

    memset(pg-&gt;visitInfo, 0, sizeof(int) * pg-&gt;numV);
    return FALSE;    // 목표 찾지 못함.
}</code></pre>
<p>위에서 설명한 함수들을 정리한 소스파일은 다음과 같다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &quot;ALGraphKruskal.h&quot;
#include &quot;DLinkedList.h&quot;
#include &quot;ArrayBaseStack.h&quot;

int WhoIsPrecede(int data1, int data2);
int PQWeightComp(Edge d1, Edge d2);

void GraphInit(ALGraph * pg, int nv)
{
    int i;    

    pg-&gt;adjList = (List*)malloc(sizeof(List)*nv);
    pg-&gt;numV = nv;
    pg-&gt;numE = 0;

    for(i=0; i&lt;nv; i++)
    {
        ListInit(&amp;(pg-&gt;adjList[i]));
        SetSortRule(&amp;(pg-&gt;adjList[i]), WhoIsPrecede); 
    }

    pg-&gt;visitInfo= (int *)malloc(sizeof(int) * pg-&gt;numV);
    memset(pg-&gt;visitInfo, 0, sizeof(int) * pg-&gt;numV);

    // 우선순위 큐의 초기화
    PQueueInit(&amp;(pg-&gt;pqueue), PQWeightComp);
}

void GraphDestroy(ALGraph * pg)
{
    if(pg-&gt;adjList != NULL)
        free(pg-&gt;adjList);

    if(pg-&gt;visitInfo != NULL)
        free(pg-&gt;visitInfo);
}

void AddEdge(ALGraph * pg, int fromV, int toV, int weight)
{
    Edge edge = {fromV, toV, weight};     // 간선의 가중치 정보를 담음

    LInsert(&amp;(pg-&gt;adjList[fromV]), toV);
    LInsert(&amp;(pg-&gt;adjList[toV]), fromV);
    pg-&gt;numE += 1;

    // 간선의 가중치 정보를 우선순위 큐에 저장
    PEnqueue(&amp;(pg-&gt;pqueue), edge);
}

// 삭제된 간선을 다시 삽입 : ConKruskalMST Helper function
void RecoverEdge(ALGraph * pg, int fromV, int toV)
{
    LInsert(&amp;(pg-&gt;adjList[fromV]), toV);
    LInsert(&amp;(pg-&gt;adjList[toV]), fromV);
    (pg-&gt;numE)++;
}

// 한쪽 방향의 간선 소멸 : ConKruskalMST Helper function
void RemoveWayEdge(ALGraph * pg, int fromV, int toV)
{
    int edge;

    if(LFirst(&amp;(pg-&gt;adjList[fromV]), &amp;edge))
    {
        if(edge == toV)
        {
            LRemove(&amp;(pg-&gt;adjList[fromV]));
            return;
        }

        while(LNext(&amp;(pg-&gt;adjList[fromV]), &amp;edge))
        {
            if(edge == toV)
            {
                LRemove(&amp;(pg-&gt;adjList[fromV]));
                return;
            }
        }
    }
}

// 그래프에서 간선을 삭제 : ConKruskalMST Helper function
void RemoveEdge(ALGraph * pg, int fromV, int toV)
{
    RemoveWayEdge(pg, fromV, toV);
    RemoveWayEdge(pg, toV, fromV);
    (pg-&gt;numE)--;
}

void ShowGraphEdgeInfo(ALGraph * pg)
{
    int i;
    int vx;

    for(i=0; i&lt;pg-&gt;numV; i++)
    {
        printf(&quot;%c와 연결된 정점: &quot;, i + 65);

        if(LFirst(&amp;(pg-&gt;adjList[i]), &amp;vx))
        {
            printf(&quot;%c &quot;, vx + 65);

            while(LNext(&amp;(pg-&gt;adjList[i]), &amp;vx))
                printf(&quot;%c &quot;, vx + 65);
        }
        printf(&quot;\n&quot;);
    }
}

// 간선의 가중치 정보 출력
void ShowGraphEdgeWeightInfo(ALGraph * pg)
{
    PQueue copyPQ = pg-&gt;pqueue;
    Edge edge;

    while(!PQIsEmpty(&amp;copyPQ))
    {
        edge = PDequeue(&amp;copyPQ);
        printf(&quot;(%c-%c), w:%d \n&quot;, edge.v1+65, edge.v2+65, edge.weight);
    }
}

int WhoIsPrecede(int data1, int data2)
{
    if(data1 &lt; data2)
        return 0;
    else
        return 1;
}

int PQWeightComp(Edge d1, Edge d2)
{
    return d1.weight - d2.weight;
}

int VisitVertex(ALGraph * pg, int visitV)
{
    if(pg-&gt;visitInfo[visitV] == 0)
    {
        pg-&gt;visitInfo[visitV] = 1;
    //    printf(&quot;%c &quot;, visitV + 65);
        return TRUE;
    }

    return FALSE;
}


void DFShowGraphVertex(ALGraph * pg, int startV)
{
    Stack stack;
    int visitV = startV;
    int nextV;

    StackInit(&amp;stack);
    VisitVertex(pg, visitV);
    SPush(&amp;stack, visitV);

    while(LFirst(&amp;(pg-&gt;adjList[visitV]), &amp;nextV) == TRUE)
    {
        int visitFlag = FALSE;

        if(VisitVertex(pg, nextV) == TRUE)
        {
            SPush(&amp;stack, visitV);
            visitV = nextV;
            visitFlag = TRUE;
        }
        else
        {
            while(LNext(&amp;(pg-&gt;adjList[visitV]), &amp;nextV) == TRUE)
            {
                if(VisitVertex(pg, nextV) == TRUE)
                {
                    SPush(&amp;stack, visitV);
                    visitV = nextV;
                    visitFlag = TRUE;
                    break;
                }
            }
        }

        if(visitFlag == FALSE)
        {
            if(SIsEmpty(&amp;stack) == TRUE)
                break;
            else
                visitV = SPop(&amp;stack);    
        }
    }

    memset(pg-&gt;visitInfo, 0, sizeof(int) * pg-&gt;numV);
}

// 두 정점이 연결되어 있는지 확인 : ConKruskalMST Helper function
//  인자로 전달된 두 정점이 연결되어 있다면 TRUE, 그렇지 않다면 FALSE 반환
int IsConnVertex(ALGraph * pg, int v1, int v2)
{
    Stack stack;
    int visitV = v1;
    int nextV;

    StackInit(&amp;stack);
    VisitVertex(pg, visitV);
    SPush(&amp;stack, visitV);

    while(LFirst(&amp;(pg-&gt;adjList[visitV]), &amp;nextV) == TRUE)
    {
        int visitFlag = FALSE;

        if(nextV == v2)
        {
            memset(pg-&gt;visitInfo, 0, sizeof(int) * pg-&gt;numV);
            return TRUE;
        }

        if(VisitVertex(pg, nextV) == TRUE)
        {
            SPush(&amp;stack, visitV);
            visitV = nextV;
            visitFlag = TRUE;
        }
        else
        {
            while(LNext(&amp;(pg-&gt;adjList[visitV]), &amp;nextV) == TRUE)
            {

                if(nextV == v2)
                {
                    memset(pg-&gt;visitInfo, 0, sizeof(int) * pg-&gt;numV);
                    return TRUE;
                }

                if(VisitVertex(pg, nextV) == TRUE)
                {
                    SPush(&amp;stack, visitV);
                    visitV = nextV;
                    visitFlag = TRUE;
                    break;
                }
            }
        }

        if(visitFlag == FALSE)
        {
            if(SIsEmpty(&amp;stack) == TRUE)
                break;
            else
                visitV = SPop(&amp;stack);    
        }
    }

    memset(pg-&gt;visitInfo, 0, sizeof(int) * pg-&gt;numV);
    return FALSE;
}


// 크루스칼 알고리즘 기반 MST의 구성
void ConKruskalMST(ALGraph * pg)
{
    Edge recvEdge[20];    // 복원할 간선의 정보 저장
    Edge edge;
    int eidx = 0;
    int i;

    // MST를 형성할 때까지 아래의 while문 반복복
    while(pg-&gt;numE+1 &gt; pg-&gt;numV)        // MST 간선의 수 + 1 == 정점의 수
    {
        edge = PDequeue(&amp;(pg-&gt;pqueue));
        RemoveEdge(pg, edge.v1, edge.v2);

        if(!IsConnVertex(pg, edge.v1, edge.v2))
        {
            RecoverEdge(pg, edge.v1, edge.v2, edge.weight);
            recvEdge[eidx++] = edge;
        }
    }

    // 우선순위 큐에서 삭제된 간선의 정보를 회복
    for(i=0; i&lt;eidx; i++)
        PEnqueue(&amp;(pg-&gt;pqueue), recvEdge[i]);

}</code></pre>
<p><strong>-실행파일 (KruskalMain.c)</strong></p>
<p>구현 결과의 확인을 위한 main 함수는 다음과 같다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &quot;ALGraphKruskal.h&quot;

int main(void)
{
    ALGraph graph;
    GraphInit(&amp;graph, 6);

    AddEdge(&amp;graph, A, B, 9);
    AddEdge(&amp;graph, B, C, 2);
    AddEdge(&amp;graph, A, C, 12);
    AddEdge(&amp;graph, A, D, 8);
    AddEdge(&amp;graph, D, C, 6);
    AddEdge(&amp;graph, A, F, 11);
    AddEdge(&amp;graph, F, D, 4);
    AddEdge(&amp;graph, D, E, 3);
    AddEdge(&amp;graph, E, C, 7);
    AddEdge(&amp;graph, F, E, 13);

    ConKruskalMST(&amp;graph);
    ShowGraphEdgeInfo(&amp;graph);
    ShowGraphEdgeWeightInfo(&amp;graph);

    GraphDestroy(&amp;graph);
    return 0;
}

&gt; gcc .\ALGraphKruskal.c .\ArrayBaseStack.c .\DLinkedList.c .\KruskalMain.c .\PriorityQueue.c .\UsefulHeap.c
&gt; .\a.exe
&gt; 출력
A와 연결된 정점: D
B와 연결된 정점: C
C와 연결된 정점: B D
D와 연결된 정점: A C E F
E와 연결된 정점: D
F와 연결된 정점: D
(A-D), w:8
(D-C), w:6
(F-D), w:4
(D-E), w:3
(B-C), w:2</code></pre>
<p>출력결과를 담당하고 있는 VisitVertex 함수를 살펴보면 <code>printf(&quot;%c &quot;, visitV + 65);</code> 문장이 주석처리 되어있다.
이는 방문한 정점의 이름을 출력하기 위한 문장으로 다 보여주면 너무 많아서 주석처리 했다.</p>
<hr>
<h2 id="review"><code>&lt;Review&gt;</code></h2>
<p>이로써 c언어로도 자료구조 배우기를 끝냈다...!</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/55eddc44-257a-41c4-8815-681d0ba2669f/image.png" alt=""></p>
<p>사실 뒤로 갈 수록 지치기도 하고 (다른 일이 많기도 하고)
왜 C언어를 안쓰는지 알게 된기도 한거 같다.(?)</p>
<p>마지막 크루스칼 알고리즘은 여태 배운 것을 총망라 한 느낌을 받아서 더 머리에 안들어온거 같다...
(앞에서 한 내용이 기억에 잘 안났달까...)</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/dcc3741a-41f7-4a14-b717-c178bf975fe0/image.png" alt=""></p>
<p>아무튼 여기까지 달려온 나에게 고생했다고 얘기하고 싶다.
같이 여기까지 함께해준 스터디원들에게도 감사하다고 말하고 싶다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/3e7b3e33-a851-4009-97ec-f3b74bd2b493/image.png" alt=""></p>
<p>9월부터 12월까지 약 4개월을 함께 달려왔고 앞으로 코딩쪽으로 더 멋진 성장과 활동 할 수 있길 응원한다..!</p>
<p>다음에는 다른 학습 또는 도전으로 velog를 찾아올 예정이다.
다음에 봐용~!</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/0da1690d-f78f-494c-9a77-35764706e810/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chapter 13. 테이블(Table)과 해쉬(Hash)]]></title>
            <link>https://velog.io/@mingming_eee/datastructure-13</link>
            <guid>https://velog.io/@mingming_eee/datastructure-13</guid>
            <pubDate>Wed, 11 Dec 2024 04:15:19 GMT</pubDate>
            <description><![CDATA[<h2 id="13-1-빠른-탐색을-보이는-해쉬-테이블">13-1. 빠른 탐색을 보이는 해쉬 테이블</h2>
<p>AVL 트리는 탐색 키의 비교 과정을 거치면서 찾는 대상에 가까워지는 방식이기 때문에 원하는 바를 &#39;단번에&#39; 찾아내는 방식이라고 말하기 어렵다.
이러한 상황에서 더 높은 성능을 낼 수 있는 자료구조가 바로 &#39;테이블 (Table)&#39;이다.
AVL 트리의 탐색 연산이 $$O(log_2n)$$의 시간 복잡도를 보이는 반면에, 
테이블 자료구조의 탐색 연산은 $$O(1)$$의 시간 복잡도를 보이니, 
적용할 수 있는 상황에서는 테이블의 탐색 성능이 훨씬 좋다고 할 수 있다.</p>
<h3 id="테이블table-자료구조"><em>테이블(Table) 자료구조</em></h3>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/87469c53-6d3b-41dc-9fa6-b8d3d0920bbb/image.png" alt=""></p>
<p>위 그림에서 보이는 것은 문서 편집에서 한번은 봤을 만한 &#39;표&#39;다.
그리고 이것이 바로 테이블이다.
하지만 자료구조의 관전에서 모든 표를 가리켜 테이블이라 하지 않는다.
표에 저장된 데이터의 형태가 <strong>&quot;키(key)와 값(value)이 하나의 쌍을 이룰 때&quot;</strong> 테이블로 구분 짓는다.
이렇듯 테이블에 저장되는 모든 데이터들은 이를 구분하는 &#39;키&#39;가 있어야 하고,
이 키는 데이터를 구분하는 기준이 되기 때문에 중복이 허용되지 않는다.
그리고 테이블에서는 키가 존재하지 않는 값은 저장할 수 없다.</p>
<p>이렇듯 테이블의 핵심은 키와 값이 하나의 쌍을 이뤄 저장되는 데이터 유형에 있다.
자료구조의 &#39;테이블&#39;은 &#39;사전 구조&#39;라고도 불리고 더불어 &#39;맵(map)&#39;이라고도 불린다.
사전구조라고 하는 이유는 테이블의 대표적인 예시가 사전이기 때문이다.
사전인 이유는 단어가 키가 되고 그 단어에 대한 설명 또는 내용이 값이 되기 때문이다.</p>
<h3 id="1-배열을-기반으로-하는-테이블"><em>1) 배열을 기반으로 하는 테이블</em></h3>
<p>배열을 기반으로 누구나 쉽게 이해할 수 있는 간단한 예제를 살펴보자.
테이블 자료구조를 코드상으로 볼 수 있게 되어있다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

typedef struct _empInfo
{
    int empNum;     // 직원의 고유 번호
    int age;        // 직원의 나이
} EmpInfo;

int main()
{
    EmpInfo empInfoArr[100];
    EmpInfo ei;
    int eNum;

    printf(&quot;사번과 나이 입력: &quot;);
    scanf(&quot;%d %d&quot;, &amp;(ei.empNum), &amp;(ei.age));
    empInfoArr[ei.empNum] = ei; // 바로 저장

    printf(&quot;확인하고 싶은 직원의 사번 입력: &quot;);
    scanf(&quot;%d&quot;, %eNum);

    ei = empInfoArr[eNum];  // 바로 탐색
    printf(&quot;사번 %d, 나이 %d \n&quot;, ei.empNum, ei.age);
    return 0;
}

&gt; 출력
사번과 나이 입력: 129 29
확인하고 싶은 직원의 사번 입력: 129
사번 129, 나이 29 </code></pre>
<p>저장과 탐색의 원리만 확인할 수 있도록 main 함수를 간단하게 작성하였다.
선언된 구조체를 살펴보면 키와 값을 하나의 쌍으로 묶기 위해 정의되었다.
이 구조체를 기반으로 하는 배열 선언으로 <code>EmpInfo empInfoArr[1000];</code>로 했지만 이 배열을 가리켜 &#39;테이블&#39;이라 하기엔 많은 것이 부족해 보인다.
이것이 테이블이라 할 수 있으려면 키를 결정하였을 때 이를 기반으로 데이터를 단번에 찾아야되기 때문이다.
즉, 테이블에서 의미하는 키는 데이터를 (단번에) 찾는 도구가 되어야 한다.
그래서 저장으로 <code>empInfoArr[ei.empNum] = ei;</code>문장과 
탐색으로 <code>ei = empInfoArr[eNum];</code>이 사용되었다.
이 두 개의 문장을 통해 알 수 있는 점은 직원의 고유번호를 인덱스 값으로 하여, 그 위치에 데이터를 저장한 것이다.
이렇듯 &#39;키의 값은 저장위치&#39;라는 관계를 형성해 단번에 데이터를 저장하고 단번에 데이터를 탐색할 수 있게 했다.
하지만 위 테이블은 더 많은 양의 데이터를 담기엔 배열이 작아보인다.
이러한 문제점은 테이블의 핵심인 해쉬와 관련된 내용이 빠져서 그렇다.</p>
<h3 id="테이블에-의미를-부여하는-해쉬-함수와-충돌-문제"><em>테이블에 의미를 부여하는 해쉬 함수와 충돌 문제</em></h3>
<p>앞 예제에서 보인 테이블과 관련해서 지적된 문제점 두 가지는 다음과 같다.</p>
<ul>
<li>직원 고유번호의 범위가 배열의 인덳 값으로 사용하기에 적당하지 않다.</li>
<li>직원 고유번호의 범위를 수용할 수 있는 매우 큰 배열이 필요하다.</li>
</ul>
<p>이 두 가지 문제를 동시에 해결해주는 것이 바로 &#39;해쉬 함수&#39;이다.
해쉬 함수의 소개를 위해 앞 예제에서 &quot;직원의 고유번호는 입사 년도 네 자리와 입사순서 네 자리로 구성된다.&quot;라는 가정을 추가해 다시 구현해보자.
예를 들어 2012년도에 세 번째로 입사한 직원의 고유번호는 &quot;20120003&quot;이 되고,
이어서 같은 해에 입사한 직원의 고유번호는 &quot;20120004&quot;가 된다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

typedef struct _empInfo
{
    int empNum;     // 직원의 고유 번호
    int age;        // 직원의 나이
} EmpInfo;

int GetHashValue(int empNum)
{
    return empNum % 100;
}

int main()
{
    EmpInfo empInfoArr[100];

    EmpInfo emp1={20120003, 24};
    EmpInfo emp2={20130012, 33};
    EmpInfo emp3={20170049, 27};

    EmpInfo r1, r2, r3;

    // 키를 인덱스 값으로 이용해서 저장
    empInfoArr[GetHashValues(emp1, empNum)] = emp1;
    empInfoArr[GetHashValues(emp2, empNum)] = emp2;
    empInfoArr[GetHashValues(emp3, empNum)] = emp3;

    // 키를 인덱스 값으로 이용해서 탐색
    r1 = empInfoArr[GetHashValues(20120003)];
    r2 = empInfoArr[GetHashValues(20130012)];
    r3 = empInfoArr[GetHashValues(20170049)];

    // 탐색 결과 확인
    printf(&quot;사번 %d, 나이 %d \n&quot;, r1.empNum, r1.age);
    printf(&quot;사번 %d, 나이 %d \n&quot;, r2.empNum, r2.age);
    printf(&quot;사번 %d, 나이 %d \n&quot;, r3.empNum, r3.age);

    return 0;
}

&gt; 출력
사번 20120003, 나이 24 
사번 20130012, 나이 33 
사번 20170049, 나이 27</code></pre>
<p>위 예제에서는 길이가 100인 배열을 선언했다.
직원의 수가 100명을 넘길 경우를 고려하지 않은 것이고,
데이터의 저장위치를 결정하는데 있어서 직원의 고유번호를 활용하되, <code>GetHashValue</code>라는 함수를 이용해 가공의 과정을 거쳤다.
위 함수에서 100으로 % 연산을 한 것은 여덟 자리의 수로 이뤄진 직원의 고유번호를 두 자리의 수로 변경한다는 의미를 가지고 있다.
실제로는 앞의 숫자 6개를 잘라낸 것이지만 이것도 변경의 일종이다.
그럼 100으로 나눠서 그 나머지를 취하는 연산을 함수 $$f(x)$$라 하자.
그럼 이 함수의 기능은 아래의 그림과 같이 표현할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/07ce8522-e372-4265-9970-d13b8434c689/image.png" alt=""></p>
<p>이 함수 $$f(x)$$를 가리켜 &#39;해쉬 함수(hash function)&#39;이라 한다.
그리고 이러한 해쉬 함수는 넓은 번위의 키를 좁은 범위의 키로 변경하는 역할을 한다.
실제로 위의 예제에서는 해쉬 함수와 관련해서 흔히 거론되는 % 연산자를 이용해서 여덟 자리의 키를 두 자리의 키로 바꿨다.</p>
<p>여기서 직원의 수가 100명을 넘어 직원 번호가 20210103이 형성되면 다음과 같은 문제가 발생한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/7727de54-7ec8-42d3-adf3-5ada6792defc/image.png" alt=""></p>
<p>서로 다른 두 개의 키가 해쉬 함수를 통과했는데 그 결과가 03으로 모두 동일해 &#39;충돌(collision)&#39;이 발생한 것이다.
이러한 충돌은 배열의 길이를 늘리는 등의 방법으로 피해야 할 상황이 아니다.
이러한 충돌은 &#39;해결해야 하는 상황&#39;이고 충돌의 해결방법에 따라서 테이블의 구조가 달라지는 경우가 있을 정도로 충돌의 해결 방법은 테이블에 있어서 큰 의미를 갖는다.</p>
<h3 id="2-어느정도-갖춰진-테이블과-해쉬의-구현-예시"><em>2) 어느정도 갖춰진 테이블과 해쉬의 구현 예시</em></h3>
<p>앞 예제들을 통해 테이블과 해쉬 함수의 구현에 대해 간단히 살펴보았다.
두 예제 모두 테이블의 구현 사례로는 약간의 부족한 점들이 있었다.
따라서 이번에는 어느 정도 갖춰진 테이블과 해쉬를 구현해 보려고 한다.
우선 테이블에 저장할 대상에 대한 헤더파일과 소스파일을 정의해보자.</p>
<ul>
<li><p>Person.h</p>
<pre><code class="language-c">#ifndef __PERSON_H__
#define __PERSON_H__

#define STR_LEN 50

typedef struct _person
{
    int ssn;                // 주민등록번호
    char name[STR_LEN];     // 이름
    char addr[STR_LEN];     // 주소
} Person;

int GetSSN(Person *p);
void ShowPerInfo(Person *p);
Person * MakePersonData(int ssn, char * name, char * addr);

#endif</code></pre>
</li>
<li><p>Person.c</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &quot;PErson.h&quot;

int GetSSN(Person *p)
{
    return p-&gt;ssn;
}

void ShowPerInfo(Person * p)
{
    printf(&quot;SSN: %d \n&quot;, p-&gt;ssn);
    printf(&quot;Name: %s \n&quot;, p-&gt;name);
    printf(&quot;Address: %s \n&quot;, p-&gt;addr);
}

Person * MakePersonData(int ssn, char * naem, char * addr)
{
    Person * newP = (Person *)malloc(sizeof(Person));
    newP-&gt;ssn = ssn;
    strcpy(newP-&gt;name, name);
    strcpy(newP-&gt;addr, addr);
    return newP;
}</code></pre>
</li>
</ul>
<p>위의 헤더파일에 정의된 Person 구조체의 변수가 (정확히는 구조체 변수의 주소 값이) 테이블에 저장될 값이다.
그리고 그 중에서 구조체의 멤버 ssn(주민등록번호)를 키로 결정하였다.
키를 별도로 추출하는데 사용하기 위해서 GetSSN 함수를 정의했다.
그리고 Person 구조체 변수의 생성 및 초기화의 편의를 위해서 MakePersonData 함수도 정의했다.
이어서 테이블의 구현과 관련이 있는 파일들을 살펴보자.
첫 번째로 볼 헤더파일은 테이블의 슬롯을 정의한 헤더파일이다.
슬롯(Slot)이 무엇인지는 코드를 먼저 살펴보고 설명을 할 예정이다.</p>
<ul>
<li><p>Slot.h</p>
<pre><code class="language-c">#ifndef __SLOT_H__
#define __SLOT_H__

#include &quot;Person.h&quot;

typedef int Key;    // 주민등록번호
typedef Person * Value;

enum SlotStatus {EMPTY, DELETED, INUSE};

typedef struct _slot
{
    Key key;
    Value val;
    enum SlotStatus status;
} Slot;

#endif</code></pre>
<p>슬롯(slot)이란 테이블을 이루는, 테이블을 저장할 수 있는 각각의 공간을 의미한다.
그리고 위의 typedef 선언에서도 보이듯 키와 값은 다음과 같이 결정했다.</p>
<ul>
<li>키 : 주민등록번호</li>
<li>값 : Person 구조체 변수의 주소 값</li>
</ul>
<p>그리고 enum 선언을 통해 슬롯의 상태를 나타내는 상수 EMPTY, DELETED, INUSE 가 정의되었고, 이를 기반으로 다음과 같이 Slot 구조체의 멤버를 선언했다.
<code>enum SlotStatus status;</code></p>
<p>슬롯의 상태를 나타내는 상수 각각이 의미하는 바는 다음과 같다.</p>
<ul>
<li>EMPTY: 이 슬롯에는 데이터가 저장된 바 없다.</li>
<li>DELETED: 이 슬롯에는 데이터가 저장된 바 있으나 현재는 비워진 상태다.</li>
<li>INUSE: 이 슬롯에는 현재 유효한 데이터가 저장되어 있다.</li>
</ul>
<p>이 중에서 EMPTY와 INUSE의 필요성은 이해가 되지만, 사실 DELETED의 필요성에 대해서는 의문이 든다.
DELETED의 필요성은 충돌의 해결책에 대해 배우면서 함께 배울 예정이다.</p>
</li>
</ul>
<p>이제 테이블의 실질적인 구현에 해당하는 헤더파일과 소스파일에 대해 알아보자.</p>
<ul>
<li><p>Table.h</p>
<pre><code class="language-c">#ifndef __TABLE_H__
#define __TABLE_H__

#include &quot;Slot.h&quot;

#define MAX_TBL 100

typedef int HashFunc(Key k);

typedef struct _table
{
    Slot tbl[MAX_TBL];
    HashFunc * hf;
} Table;

// 테이블의 초기화
void TBLInit(Table * pt, HashFunc * f);

// 테이블에 키와 값을 저장
void TBLInsert(Table * pt, Key k, Value v);

// 키를 근거로 테이블에서 데이터 삭제
Value TBLDelete(Table * pt, Key k);

// 키를 근거로 테이블에서 데이터 탐색
Value TBLSearch(Table * pt, Key k);

#endif</code></pre>
</li>
<li><p>Table.c</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;Table.h&quot;

void TBLInit(Table * pt, HashFunc * f)
{
    int i;

    // 모든 슬롯 초기화
    for(i=0; i&lt;MAX_TBL; i++)
        (pt-&gt;tbl[i]).status = EMPTY;

    pt-&gt;hf = f; // 해쉬 함수 등록    
}

void TBLInsert(Table * pt, Key k, Value v)
{
    int hv = pt-&gt;hf(k);
    pt-&gt;tbl[hv].val = v;
    pt-&gt;tbl[hv].key = k;
    pt-&gt;tbl[hv].status = INUSE;
}

Value TBLDelete(Table * pt, Key k)
{
    int hv = pt-&gt;hf(k);

    if((pt-&gt;tbl[hv]).status != INUSE)
        return NULL;
    else
    {
        (pt-&gt;tbl[hv]).status = DELETED;
        return (pt-&gt;tbl[hv]).val;   // 소멸 대상의 값 반환
    }
}

Value TBLSearch(Table * pt, Key k)
{
    int hv = pt-&gt;hf(k);

    if((pt-&gt;tbl[hv]).status != INUSE)
        return NULL;
    else
        return (pt-&gt;tbl[hv]).val;   // 탐색 대상의 값 반환
}</code></pre>
</li>
</ul>
<p>직접 정의한 해쉬 함수를 등록하도록 디자인되었다는 사실과 Slot 배열로 테이블을 구성했다는 사실에 주목하면 코드를 쉽게 이해할 수 있다.
위 테이블을 대상으로 하는 main 함수(실행파일)은 다음과 같다.</p>
<ul>
<li><p>SimpleHashMain.c</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;Person.h&quot;
#include &quot;Table.h&quot;

int MyHashFunc(int k)
{
    return k % 100;
}

int main()
{
    Table myTbl;
    Person * np;
    Person * sp;
    Person * rp;

    TBLInit(&amp;myTbl, MyHashFunc);

    // 데이터 입력
    np = MakePersonData(20120003, &quot;Lee&quot;, &quot;Seoul&quot;);
    TBLInsert(&amp;myTbl, GetSSN(np), np);

    np = MakePersonData(20130012, &quot;Kim&quot;, &quot;Jeju&quot;);
    TBLInsert(&amp;myTbl, GetSSN(np), np);

    np = MakePersonData(20170049, &quot;Han&quot;, &quot;Kangwon&quot;);
    TBLInsert(&amp;myTbl, GetSSN(np), np);

    // 데이터 탐색
    sp = TBLSearch(&amp;myTbl, 20120003);
    if(sp!= NULL)
        ShowPerInfo(sp);

    sp = TBLSearch(&amp;myTbl, 20130012);
    if(sp!= NULL)
        ShowPerInfo(sp);

    sp = TBLSearch(&amp;myTbl, 20170049);
    if(sp!= NULL)
        ShowPerInfo(sp);

    // 데이터 삭제
    rp = TBLDelete(&amp;myTbl, 20120003);
    if(rp!= NULL)
        free(rp);

    rp = TBLDelete(&amp;myTbl, 20130012);
    if(rp!= NULL)
        free(rp);

    rp = TBLDelete(&amp;myTbl, 20170049);
    if(rp!= NULL)
        free(rp);

    return 0;
}

&gt; gcc .\Person.c .\SimpleHashMain.c .\Table.c
&gt; .\a.exe
&gt; 출력
SSN: 20120003 
Name: Lee 
Address: Seoul

SSN: 20130012
Name: Kim
Address: Jeju

SSN: 20170049
Name: Han
Address: Kangwon</code></pre>
</li>
</ul>
<h3 id="좋은-해쉬-함수의-조건"><em>좋은 해쉬 함수의 조건</em></h3>
<p>좋은 해쉬 함수의 조건을 언급하기에 앞서 좋은 해쉬 함수를 사용한 결과와 좋지 않은 해쉬 함수를 사용한 결과를 비교해보자.
먼저 좋은 해쉬 함수를 사용한 결과는 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/8e2ebce1-54dd-4b00-9adc-aa1f274a775c/image.png" alt=""></p>
<p>위 그림은 테이블의 메모리 상황을 표현한 것인데 검은 영역은 데이터가 채워진 슬롯을 의미한고, 흰 영역은 빈 슬롯을 의미한다.
데이터가 테이블의 전체 영역에 고루 분포되어 있는 것을 알 수 있는데,
이렇듯 고루 분포된다는 것은 그만큼 충돌이 발생할 확률이 낮다는 것을 의미한다.
충돌의 해결책이 마련되어 있다 하더라도 충돌이 덜 발생해야 데이터의 저장, 삭제 및 탐색의 효율을 높일 수 있다.
때문에 좋은 해쉬 함수는 &#39;충돌을 덜 일으키는 해쉬 함수&#39;라고도 할 수 있다.</p>
<p>반면에 좋지 못한 해쉬 함수는 다음과 같은 사용결과를 보여준다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/b177a815-3e1b-40be-bbee-60fddf223c19/image.png" alt=""></p>
<p>테이블의 특정 영역에 데이터가 몰려 있고 이는 해쉬 함수가 특정 영역에 데이터가 몰리도록 &#39;해쉬 값&#39;을 생성한 결과다. (해쉬 값이란 해쉬 함수가 만들어 낸 값이다.)
때문에 충돌이 발생할 확률이 그만큼 높은 상황이다.
좋은 해쉬 함수를 디자인하는 방법은 키의 특성에 따라 달라지기 때문에 &quot;키의 일부분을 참조하여 해쉬 값을 만들지 않고, 키의 전체를 참조하여 해쉬 값을 만들어야&quot; 일반적으로 좋은 해쉬 함수를 정의할 수 있다고 한다.
아무래도 적은 수의 데이터를 조합하여 해쉬 값을 생성하는 것보다 많은 수의 데이터를 조합하여 해쉬 값을 생성했을 때 보다 다양한 값의 생성을 기대할 수 있기 때문이다.</p>
<h3 id="자릿수-선택digit-selection-방법과-자릿수-폴딩digit-folding-방법"><em>자릿수 선택(Digit Selection) 방법과 자릿수 폴딩(Digit Folding) 방법</em></h3>
<p>좋은 해쉬 함수의 디자인 방법은 위에서도 언급했지만 키의 특성에 따라 달라진다.
때문에 해쉬 함수의 디자인에 있어서 절대적인 방법은 존재하지 않는다.
다만 위의 조언, 키 전체를 참조하는 방법과 관련해서는 다양한 방법이 소개되고 있는데, </p>
<p>그 중 하나는 &quot;자릿수 선택 방법으로&quot; 여덟 자리의 수로 이뤄진 키에서 다양한 해쉬 값 생성에 도움을 주는 네 자리의 수를 뽑아서 해쉬 값을 생성한다는 것이다.
키의 특정 위치에서 중복의 비율이 높고나, 아예 공통으로 들어가는 값이 있다면, 이를 제외한 나머지를 가지고 해쉬 값을 생성하는 방법이다.</p>
<p>그리고 이와 유사한 방법으로 &quot;비트 추출 방법&quot;이 있다.
탐색 키의 비트 열에서 일부를 추출 및 조합하는 방법이다.</p>
<p>그 다음 방법으로는 &quot;자릿수 폴딩&quot;이다.
종이를 접듯이 숫자를 겹치게 하여 더한 결과를 해쉬 값으로 결정하는 방법이다.
예를 들어 아래 그림과 같이 숫자가 적힌 종이를 삼등분으로 접으면</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/b8ed5d9b-f0fd-4623-8eb7-e0b641ff2370/image.png" alt=""></p>
<p>27, 34, 19로 나뉘고 이 두 자릿수 세 개를 모두 더하면 80이 된다.
이를 해쉬 값이라 하면 여섯 자리의 숫자를 모두 반영하여 얻은 결과가 된다.</p>
<p>이 외에도 키를 제곱하여 그 중 일부를 추출하는 방법, 폴딩의 과정에서 덧셈 대신 XOR 연산을 하는 방법, 그리고 둘 이상의 방법을 조합하는 방법 등 통계적으로 넓은 분포를 보이는 다양한 방법들이 있다.</p>
<hr>
<h2 id="13-2-충돌collision-문제의-해결책">13-2. 충돌(Collision) 문제의 해결책</h2>
<p>테이블의 핵심 주제라 할 수 있는 <code>충돌(Collision)</code>에 대해서 고민해볼 차례다.
충돌의 해결책은 대단한 것은 아니고, 충돌이 발생한 자리를 대신해서 빈 자리를 찾아주는 것이다.
이 빈 자리를 찾는 방법에 따라서 해결책이 구분된다.</p>
<h3 id="선형-조사법linear-probing과-이차-조사법quadratic-probing"><em>선형 조사법(Linear Probing)과 이차 조사법(Quadratic Probing)</em></h3>
<p>충돌이 발생했을 때 그 옆자리가 비었는지 살펴보고, 비었을 경우 그 자리에 대신 저장하는 것이 바로 <code>선형 조사법(Linear Probing)</code> 이다.
예를 들어 해쉬 함수 $$f(x) = key$$ % $$7$$가 있고 테이블의 내부 저장소가 배열이라고 생각해보자.
키가 9인 데이터가 저장될 때 해쉬 값이 2가 되므로 다음 그림과 같이 인덱스가 2인 위치에 저장이 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/be57ed84-5d7d-4e3a-be54-f2c3dda03c2e/image.png" alt=""></p>
<p>이어서 키가 2인 데이터가 등장했다고 생각해보자. 이 경우 해쉬 값이 2이기 때문에 앞서 저장한 키가 9인 데이터와 충돌이 발생한다.
이렇게 충돌이 발생했을 때 인덱스 값이 3인 바로 옆자리를 살피는 것이 선형 조사법이다.
따라서 키가 2인 두 번째 데이터의 저장결과는 다음 그림과 같이 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/22118ac9-ec8d-4742-972a-38d6858b5979/image.png" alt=""></p>
<p>물론 옆자리가 비어있지 않을 경우 한 칸 더 이동해서 자리를 살피게 된다.
정리하면 $$k$$의 키에서 충돌 발생시 선현 조사법의 조사 순서(빈자리를 찾는 순서)는 다음과 같이 전개된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/973c671c-9b38-49c6-bbad-e3776accc4e7/image.png" alt=""></p>
<p>그런데 이러한 선형 조사법은 충돌 횟수가 증가함에 따라서 &#39;클러스터(cluster)현상&#39;
즉, 특정 영역에 데이터가 집중적으로 몰리는 현상이 발생한다는 단점이 있다.
그리고 이러한 클러스터 현상은 충돌의 확률을 높이는 직접적인 원인이 된다.
그렇다면 선형 조사법의 이러한 단점을 극복하려면 어떤 방법을 사용하면 좋을까?</p>
<p>바로 옆 빈자리를 찾는 것이 아닌 좀 먼 곳에서 빈자리를 찾으면 된다!
이러한 생각을 근거로 탄생한 것이 <code>이차 조사법(Quadratic Probing)</code> 이다.
충돌 발생시 이차 조사법의 조사 순서는 다음과 같이 전개된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/2aeeb637-aa78-4240-96c4-7942a3251683/image.png" alt=""></p>
<p>선형 조사법은 충돌 발생시 $$n$$칸 옆의 슬롯을 검사한다면,
이차 조사법은 충돌 발생시 $$n^2$$칸 옆의 슬롯을 검사한다.
이렇듯 좀 멀리서 빈 공간을 찾으려는 노력이 이차 조사법에 담겨있다.
물론 이차 조사법에도 나름의 문제가 있는데 이는 잠시 후 &#39;이중 해쉬&#39;를 통해 배울 것이다.</p>
<p>이번에는 슬롯의 상태 정보를 별도로 관리(EMPTY, DELETED, INUSE)해야 하는 이유에 대해서 알아보자.
앞에서 본 배열 형태를 가진 해쉬 함수에서 두 번째로 해쉬 값이 2인 데이터를 저장할 경우 3에 저장된 경우를 생각해보자. (아래 그림)</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/f40d40d4-fd93-475e-a97d-02cbf06c3f71/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/a19b5e1e-3f5b-447e-a6bc-450fe06d6719/image.png" alt=""></p>
<p>위 그림은 값이 9라 키 값이 2인 데이터가 삭제된 이후 상황이다.
여기서 인덱스 값이 2인 부분은 DELETED가 되고 3인 부분이 INUSE, 나머지 부분들은 EMPTY가 된다.
여기서 각 상태가 의미하는 바는 앞에서 한번 배웠지만 아래와 같이 정리할 수 있다.</p>
<ul>
<li>EMPTY: 이 슬롯에는 데이터가 저장된 바 없다.</li>
<li>DELETED: 이 슬롯에는 데이터가 저장된 바 있으나 현재는 비워진 상태다.</li>
<li>INUSE: 이 슬롯에는 현재 유효한 데이터가 저장되어 있다.</li>
</ul>
<p>여기서 중점을 두어야할 부분은 DELETED 상태 정보가 있어 EMPTY 상태와 구분되어 있다는 점이다.
이 이유는 키가 2인 데이터의 탐색 과정을 살펴보면 알 수 있다.
키가 2인 데이터의 탐색을 진행하기 위해서는 %7의 해쉬 함수를 거친다.
그리고 그 결과로 얻은 2를 인덱스 값으로 하여 탐색을 진행하게 된다.
만약에 그 위치의 슬롯 상태가 EMPTY라면 데이터가 존재하지 않는다고 판단하여 탐색을 종료하게 된다.
반면 DELETED 상태일 경우 충돌이 발생했을 경우를 의심해서 선형 조사법에 근거한 탐색의 과정을 진행해야 한다.
따라서 선형, 이차 조사법과 같은 충돌의 해결책을 적용하기 위해서는 슬롯의 상태가 DELETED인 경우를 포함시켜야 하는 것이다.
선형, 이차 조사법을 적용하였다면 탐색의 과정에서도 이를 근거로 충돌을 의심하는 탐색의 과정을 포함시켜야 한다.
이렇듯 슬롯의 DELETED 상태는 충돌의 해결책과 관련이 있다.</p>
<h3 id="이중-해쉬double-hash"><em>이중 해쉬(Double Hash)</em></h3>
<p>앞서 충돌을 해결하기 위해 제시된 방법은 선형 조사법과 이차 조사법이었다.
이차 조사법의 문제점으로는 해쉬 값이 같으면 충돌 발생시 빈 슬롯을 찾기 위해서 접근하는 위치가 늘 동일하다 점이 있다.
예를 들어서 해쉬 값을 기준으로 $$f(k)$$에서 충돌이 발생한다면 (해쉬 값 $$f(k)$$가 동일하다면) $$k$$가 다르더라도 다음의 순서대로 일정하게 빈 슬롯을 찾게 된다.</p>
<ul>
<li>첫 번째 관찰 위치 : $$f(k)+1^2$$ (1칸 옆의 슬롯)</li>
<li>두 번째 관찰 위치 : $$f(k)+2^2$$ (4칸 옆의 슬롯)</li>
<li>세 번째 관찰 위치 : $$f(k)+3^2$$ (9칸 옆의 슬롯)</li>
<li>네 번째 관찰 위치 : $$f(k)+4^2$$ (16칸 옆의 슬롯)</li>
</ul>
<p>이렇듯 해쉬 값이 같을 경우 빈 슬롯을 찾아서 접근하는 위치가 동일하기 때문에 선형 조사법보다는 낮지만, 접근이 진행되는 슬롯을 중심으로 클러스터 현상이 발생할 확률은 여전히 높을 수 밖에 없다.
이 단점은 이중 조사법에서 멀리서 빈 공간을 찾는 대신 규칙적인 방식으로 빈 공간을 선택하는 것이 아닌 불규칙하게 구성하면 해결할 수 있다.
이러한 것을 반영한 것이 &#39;이중 해쉬&#39;방법이다.</p>
<p>이중 해쉬 방법에서는 두 개의 해쉬 함수를 마련한다.
하나는 앞에서 본 것과 마찬가지로 키를 근거로 저장위치를 결정하는 것이고,
다른 하나는 충돌이 발생했을 때 몇 칸 뒤에 위치한 슬롯을 살펴볼지 그 거리를 결정하기 위한 것이다.</p>
<ul>
<li>1차 해쉬 함수: 키를 근거로 저장위치를 결정하기 위한 것</li>
<li>2차 해쉬 함수: 충돌 발생시 몇 칸 뒤를 살필지 결정하기 위한 것</li>
</ul>
<p>이해를 돕기 위해 이중 해쉬의 두 해쉬 함수를 예시로 들어보자.
먼저 배열을 저장소로 하는 테이블이 있다고 가정한다.
그리고 그 테이블의 해쉬 함수는 다음과 같이 정의되어 있다.</p>
<ul>
<li>1차 해쉬 함수: $$h1(k) = k$$ % $$15$$</li>
<li>2차 해쉬 함수: $$h2(k) = 1 + (k$$ % $$c )$$</li>
</ul>
<p>2차 해쉬 함수 식은 절대적인 것은 아니지만 일반적인 형태로 많이 사용한다.
2차 해쉬 함수의 상수 $$c$$는 1차 해쉬 함수를 % 15로 결정한 것을 보아 배열의 길이가 15라고 예상할 수 있고, 그럼 15보다 작으면서도 소수(prime number) 중 하나로 결정할 수 있다.</p>
<p>따라서 다음과 같이 해쉬 함수를 정리할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/e8241e18-aba5-4d06-9ba0-f7becc9f50c6/image.png" alt=""></p>
<p>상수 $$c$$는 7말고 다른 소수로 정해도 상관 없다.</p>
<p>왜 2차 해쉬 함수식은 일반적으로 $$h2(k) = 1 + (k$$ % $$c )$$를 사용할까?
먼저 1을 더하는 이유는 2차 해쉬 값이 0이 되는 것을 막기 위해서다.</p>
<p>그 다음 상수 $$c$$를 15보다 작은 소수로 결정하는 이유는 뭘까?
그 이유는 가급적 2차 해쉬 값이 1차 해쉬 값을 넘어서지 않게 하기 위함이다.
예를 들어서 1차 해쉬의 최대 값이 14인데 2차 해쉬 값이 최대 32라면 빈 자리를 찾아서 몇번 반복해서 돌아야할지 모르기 때문이다.</p>
<p>그렇다면 소수로 결정하는 이유는 뭘까?
이는 소수를 선택했을 때 클러스터 현상이 발생할 확률을 현저히 낮춘다는 통계를 근거로 정한 것이다.</p>
<p>마지막으로 2차 해쉬 함수의 활용에 대한 예를 하나 들겠다.
앞서 정의한 1차 해쉬 함수에 3개의 키 3, 18, 33을 적용해서 해쉬 값을 구하면 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/4ecb5e24-c90a-41c6-bc84-4dd3a96aaf2a/image.png" alt=""></p>
<p>때문에 키가 3, 18, 33인 데이터를 순서대로 저장하면, 
키가 18인 데이터를 저장할 때와 키가 33인 데이터를 저장할 때 충돌이 발생한다.
따라서 이 두 개의 키를 대상으로 2차 해쉬 값을 계산한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/a48a7159-f613-4aa3-a539-42ce0f6b5d01/image.png" alt=""></p>
<p>2차 해쉬 값은 달라지게 되고 이 2차 해쉬 값을 근거로 빈 슬롯을 찾는 과정은 각각 다음과 같이 전개된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/f04a7d76-4bfe-4cff-948d-7968e4b71fec/image.png" alt=""></p>
<p>이렇듯 2차 해쉬 값의 크기만큼 건너 뛰면서 빈 슬롯을 찾게 되므로, 키가 다르면 건너 뛰는 길이도 달라진다.
따라서 클러스터 현상의 발생 확률을 낮출 수 있다.</p>
<h3 id="체이닝chaining"><em>체이닝(Chaining)</em></h3>
<p>앞에서 본 유형의 충돌 해결 방법들을 가리켜 &#39;열린 어드레싱 방법(open addressing method)&#39;라 한다.
이는 충돌이 발생하면 다른 자리에 대신 저장한다는 의미가 담겨져 있다.
반면, 이번에 배울 유형의 방법을 가리켜 &#39;닫힌 어드레싱 방법(closed addressing method)&#39;라 한다.
이는 무슨 일이 있어도 자신의 자리에 저장한다는 의미이다.
충돌이 발생해도 자신의 자리에 들어가기 위해서는 여러 개의 자리를 마련하는 수 밖에 없다.
여러 개의 자리를 마련하는 방법으로는 배열을 이용하는 방법과 연결 리스트를 이용하는 방법이 있다.</p>
<p>배열을 이용해 자리를 여러 개 마련하는 방법은 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/51d5a675-9073-4fcf-a43a-83d24727ffa9/image.png" alt=""></p>
<p>위 그림과 같이 2차원 배열을 구성해서 해쉬 값 별로 다수의 슬롯을 마련할 수 있다.
하지만 이는 닫힌 어드레싱 방법 중에 흔히 언급되는 방법이 아니다.
충돌이 발생하지 않을 경우 메모리 낭비가 심하고, 충돌의 최대 횟수를 정해야 하는 부담이 있기 때문이다.
따라서 &#39;체이닝(Chaining)&#39;이라는 연결 리스트를 이용해 슬롯을 연결하는 방법이 닫힌 어드레싱 방법을 대표한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/6baa2a91-4ca1-4307-8123-fe634b969fa7/image.png" alt=""></p>
<p>위 그림처럼 슬롯을 생성해서 연결 리스트의 모델로 연결하는 방식으로 충돌 문제를 해결하는 것이 체이닝 방법이다.
체이닝 방법을 적용하면 하나의 해쉬 값에 다수의 슬롯을 둘 수 있다.
따라서 탐색을 위해서는 동일한 해쉬 값으로 묶여있는 연결된 슬롯을 모두 조사해야 한다는 불편함이 있다.
하지만 해쉬 함수를 잘 정의한다면 (충돌의 확률이 높지 않다면) 연결된 슬롯의 길이는 부담스러운 정도가 아니게 될 수 있다.</p>
<h3 id="충돌-문제-해결을-위한-체이닝의-구현"><em>충돌 문제 해결을 위한 체이닝의 구현</em></h3>
<p>앞서 구현한 테이블을 변경, 확장하는 형태로 체이닝 방법을 구현해보자.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/65a873c1-bac6-441d-868c-926ce0fec796/image.png" alt=""></p>
<p>위 파일들을 사용하고 추가로 동일한 해쉬 값의 슬롯을 연결 리스트로 연결하기 위해서 Chapter 04에서 구현한 연결 리스트 (<a href="https://velog.io/@mingming_eee/datastructure-04#%EC%A0%95%EB%A0%AC-%EA%B8%B0%EB%8A%A5%EC%9D%B4-%EC%B6%94%EA%B0%80%EB%90%9C-%EC%97%B0%EA%B2%B0-%EB%A6%AC%EC%8A%A4%ED%8A%B8-2-%EA%B5%AC%EC%A1%B0%EC%B2%B4%EC%99%80-%ED%97%A4%EB%8D%94%ED%8C%8C%EC%9D%BC%EC%9D%98-%EC%A0%95%EC%9D%98">DLinkedList.h</a> / <a href="https://velog.io/@mingming_eee/datastructure-04#%EC%A0%95%EB%A0%AC-%EA%B8%B0%EB%8A%A5%EC%9D%B4-%EC%B6%94%EA%B0%80%EB%90%9C-%EC%97%B0%EA%B2%B0-%EB%A6%AC%EC%8A%A4%ED%8A%B8-3-%EB%8D%94%EB%AF%B8-%EB%85%B8%EB%93%9Cdummy-node-%EA%B8%B0%EB%B0%98%EC%9D%98-%EB%8B%A8%EC%88%9C-%EC%97%B0%EA%B2%B0-%EB%A6%AC%EC%8A%A4%ED%8A%B8-%EA%B5%AC%ED%98%84">DLinkedList.c)</a>) 를 활용할 것이다.</p>
<p>구현한 테이블에서 Person.h파일과 Person.c 파일에는 변함이 없다.</p>
<ul>
<li><p>Slot2.h</p>
<pre><code class="language-c">#ifndef __SLOT2_H__
#define __SLOT2_H__

#include &quot;Person.h&quot;

typedef int Key;
typedef Person * Value;

typedef struct _slot
{
    Key key;
    Value val;
} Slot;

#endif</code></pre>
<p>Slot.h 파일을 수정하였고 파일명을 변경해준다.
슬롯의 상태 정보를 표시하기 위한 enum 선언과 관련 구조체의 멤버가 삭제되었다.
열린 어드레싱 방법에서는 슬롯의 상태 정보를 표시해야 했지만, 닫힌 어드레싱 방법에서는 슬롯의 상태 정보를 표시할 필요가 없기 때문이다.
따라서 그에 대한 선언들이 생략되었다.</p>
</li>
<li><p>Table2.h</p>
<pre><code class="language-c">#ifndef __TABLE2_H__
#define __TABLE2_H__

#include &quot;Slot2.h&quot;
#include &quot;DLinkedList.h&quot;

#define MAX_TBL 100

typedef int HashFunc(Key k);

typedef struct _table
{
    List tbl[MAX_TBL];
    HashFunc * hf;
} Table;

// 테이블의 초기화
void TBLInit(Table * pt, HashFunc * f);

// 테이블에 키와 값을 저장
void TBLInsert(Table * pt, Key k, Value v);

// 키를 근거로 테이블에서 데이터 삭제
Value TBLDelete(Table * pt, Key k);

// 키를 근거로 테이블에서 데이터 탐색
Value TBLSearch(Table * pt, Key k);

#endif</code></pre>
<p>Table.h 파일을 수정하였고 파일명을 변경해준다.
Table.h 파일과 가장 큰 차이점은 테이블의 저장소였던 Slot형 배열을 List형 배열로 바꿨다는 점이다.
이를 위해서 헤더 파일 DLinkedList.h를 포함하는 선언문도 추가되었다.
배열의 각 요소가 연결 리스트로 이뤄진 셈이 되었다.</p>
</li>
</ul>
<p>여기까지는 기본적인 수준의 변경이다.
하지만 이제부터는 연결 리스트와 구조체 Slot의 관계에 대해 생각해봐야 한다.
두 가지의 경우에 대해 생각해 볼 수 있는데,
첫 번째, 구조체 Slot을 연결 리스트의 노드로 활용하는 방법이다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/99db48e2-ed09-4f06-9d58-87d8ffd2c246/image.png" alt=""></p>
<p>이 방법은 기존에 구현해 놓은 리스트 자료구조를 활용하지 않고 리스트 자료구조와 관련된 코드를 직접 새로 작성하는 경우에 생각해 볼 수 있는 방법이다.
이 방법을 택하려면 구조체 Slot을 다음과 같이 정의해야 한다.</p>
<pre><code class="language-c">typedef struct _slot        // 구조체 Slot이 연결 리스트의 노드 역할을 겸하는 구조
{
    Key key;
    Value val;
    struct _slot * next;    // 다음 노드를 가리키는 포인터 변수
} Slot;</code></pre>
<p>다른 방법으로 슬롯과 노드를 엄연히 구분하는 보다 좋은 구조가 있다.
이는 노드에 슬롯의 주소 값을 저장하는 형태로 노드에 저장할 데이터의 형을 결정하는 typedef 선언문이 다음과 같이 추가될 필요가 있다.</p>
<pre><code class="language-c">typedef Slot * Data;        // 노드에 저장할 데이터는 Slot형 변수의 주소 값

typedef struct _node
{
    Data data;
    struct _node * next;
} Node;</code></pre>
<p>위와 유사한 방법으로 다음 그림과 같이 슬롯이 노드의 멤버가 되게 하는 방법이 있다.
이 방법도 슬롯과 노드를 구분하는 방법과 비슷하다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/2c338bca-7e9d-4d0e-866b-fbb4b5db2417/image.png" alt=""></p>
<p>그림만 봐서는 슬롯과 노드가 구분되지 않는다고 볼 수 있다.
하지만 구분된 것으로 보는 것이 맞고, 코드에서 이를 확인할 수 있다.
위의 방법을 구현하기 위해서 노드가 저장할 데이터의 형을 결정하는 typedef 선언문을 다음과 같이 작성해야 한다.</p>
<pre><code class="language-c">typedef Slot Data;        // 노드에 저장할 데이터는 Slot형 변수다.

typedef struct _node
{
    Data data;
    struct _node * next;
} Node;</code></pre>
<p>이렇게 해서 체이닝의 구현을 위해 다음 두 가지 방법이 있다는 것을 배웠다.</p>
<ul>
<li>슬롯이 연결 리스트의 노드 역할을 하게 하는 방법</li>
<li>연결 리스트의 노드와 슬롯을 구분하는 방법</li>
</ul>
<p>이 중에서 노드와 슬롯을 구분하는 방법을 선택하면 연결 리스트 간련 코드와 테이블 관련 코드의 구분이 용이하기 때문에 이 방법으로 배울 예정이다.
(이 방법을 선택해야 앞에서 구현한 연결 리스트를 활용할 수 있기도 하다.)
따라서 연결 리스트의 노드와 슬롯을 구분하는 방법으로 체이닝을 구현해보자.</p>
<ul>
<li><p>DLinkedList.h
연결 리스트를 구현한 헤더파일로 다음과 같이 typedef 선언을 변경해야 한다.</p>
<pre><code class="language-c">#ifndef __D_LINKED_LIST_H__
#define __D_LINKED_LIST_H__

#include &quot;Slot2.h&quot;  // 헤더파일 선언문 추가

#define TRUE        1
#define FALSE       0

typedef Slot LData; // 변경된 typedef 선언문

typedef struct _node
{
    LData data;
    struct _node* next;
} Node;

typedef struct _linkedList
{
    Node * head;                        
    Node * cur;                            
    Node * before;                        
    int numOfData;                        
    int (*comp)(LData d1, LData d2);
} LinkedList;

typedef LinkedList List;

// .... 생략

#endif</code></pre>
</li>
<li><p>Table2.c
: 데이터 저장의 형태가 배열이 아닌 연결 리스트로 변경되었고 Table2.h에 선언된 함수를 재정의하기 위해서 Table.c 파일을 수정하고 파일명을 바꾼다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;Table2.h&quot;         // 수정
#include &quot;DLinkedList.h&quot;    // 추가

void TBLInit(Table * pt, HashFunc * f)
{
    int i;

    for(i=0; i&lt;MAX_TBL; i++)
        ListInit(&amp;(pt-&gt;tbl[i]));    // 수정

    pt-&gt;hf = f;
}

void TBLInsert(Table * pt, Key k, Value v)
{
    int hv = pt-&gt;hf(k);
    Slot ns = {k, v};

    if(TBLSearch(pt,k) != NULL) // 키가 중복이라면
    {
        printf(&quot;키 중복 오류 발생 \n&quot;);
        return;
    }
    else
    {
        LInsert(&amp;(pt-&gt;tbl[hv]), ns);
    }
}

Value TBLDelete(Table * pt, Key k)
{
    int hv = pt-&gt;hf(k);
    Slot cSlot;

    if(LFirst(&amp;(pt-&gt;tbl[hv]), &amp;cSlot))
    {
        if(cSlot.key == k)
        {
            LRemove(&amp;(pt-&gt;tbl[hv]));
            return cSlot.val;
        }
        else
        {
            while(LNext(&amp;(pt-&gt;tbl[hv]), &amp;cSlot))
            {
                if(cSlot.key == k)
                {
                    LRemove(&amp;(pt-&gt;tbl[hv]));
                    return cSlot.val;
                }
            }
        }
    }

    return NULL;
}

Value TBLSearch(Table * pt, Key k)
{
    int hv = pt-&gt;hf(k);
    Slot cSlot;

    if(LFirst(&amp;(pt-&gt;tbl[hv]), &amp;cSlot))
    {
        if(cSlot.key == k)
        {
            return cSlot.val;
        }
        else
        {
            while(LNext(&amp;(pt-&gt;tbl[hv]), &amp;cSlot))
            {
                if(cSlot.key == k)
                {
                    return cSlot.val;
                }
            }
        }
    }

    return NULL;
}</code></pre>
<p>연결 리스트의 탐색과정에서 처음에는 LFirst, 그 다음부터는 LNext 함수를 호출한다는 것과 LRemove 함수가 호출되었을 때 앞서 LFirst 또는 LNext 호출 시 반환되는 값이 삭제된다는 것만 알아도 위 코드들을 이해하기 어렵지 않을 것이다. <del>(아뇨 전 어렵습니다...)</del></p>
</li>
<li><p>ChainedTableMain.c
: 마지막으로 실행 파일인 Main 함수를 소개로 정리해보겠다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;Person.h&quot;
#include &quot;Table2.h&quot;

int MyHashFunc(int k)
{
    return k % 100;
}

int main(void)
{
    Table myTbl;
    Person * np;
    Person * sp;
    Person * rp;

    TBLInit(&amp;myTbl, MyHashFunc);

    // 데이터 입력
    np = MakePersonData(900254, &quot;Lee&quot;, &quot;Seoul&quot;);
    TBLInsert(&amp;myTbl, GetSSN(np), np);

    np = MakePersonData(900139, &quot;KIM&quot;, &quot;Jeju&quot;);
    TBLInsert(&amp;myTbl, GetSSN(np), np);

    np = MakePersonData(900827, &quot;HAN&quot;, &quot;Kangwon&quot;);
    TBLInsert(&amp;myTbl, GetSSN(np), np);

    // 데이터 탐색
    sp = TBLSearch(&amp;myTbl, 900254);
    if(sp != NULL)
        ShowPerInfo(sp);

    sp = TBLSearch(&amp;myTbl, 900139);
    if(sp != NULL)
        ShowPerInfo(sp);

    sp = TBLSearch(&amp;myTbl, 900827);
    if(sp != NULL)
        ShowPerInfo(sp);

    // 데이터 삭제
    rp = TBLDelete(&amp;myTbl, 900254);
    if(rp != NULL)
        free(rp);

    rp = TBLDelete(&amp;myTbl, 900139);
    if(rp != NULL)
        free(rp);

    rp = TBLDelete(&amp;myTbl, 900827);
    if(rp != NULL)
        free(rp);

    return 0;
}

&gt; gcc .\ChainedTableMain.c .\DLinkedList.c .\Person.c .\Table2.c
&gt; .\a.exe
&gt; 출력
SSN: 900254 
Name: Lee      
Address: Seoul 

SSN: 900139    
Name: KIM      
Address: Jeju  

SSN: 900827
Name: HAN
Address: Kangwon</code></pre>
</li>
</ul>
<h3 id="구현한-테이블-회고"><em>구현한 테이블 회고</em></h3>
<p>구현한 테이블에 대해서 반성할 점이 있어 이것에 대해 알아보자.
(사소한 것이라고 생각할 수 있다.)</p>
<p>테이블 관련 삭제와 탐색 관련 함수를 다음과 같이 정의하였다.</p>
<pre><code class="language-c">Value TBLDelete(Table * pt, Key k)
{
    ....
    return NULL;    // 삭제할 대상이 존재하지 않는 경우
}

Value TBLSearch(Table * pt, Key k)
{
    ....
    return NULL;    // 찾는 대상이 존재하지 않는 경우
}</code></pre>
<p>위에서 보이듯 삭제 또는 탐색의 결과로 값을 반환하도록 함수를 정의했다.
그리고 삭제 또는 탐색의 대상을 찾지 못하면 NULL을 반환하도록 정의했다.
그런데 이 NULL 반환이 문제가 되지 않기 위해서는 &quot;반환되는 값, 테이블에 저장되는 값은 메모리의 주소 값이라고 가정하고 Value는 포인터 형으로 선언된다고 가정한다&quot;라는 가정이 필요하다.</p>
<p>NULL은 일반적으로 &quot;의미 없는 주소 값&quot;을 뜻할 때 사용된다.
그런데 NULL은 그 자체로 정수 0이기 때문에 다른 용도로 사용할 경우 0이라는 의미를 지니는 데이터로 오해할 수 있다.
실제로 위의 두 함수가 반환하는 값이 int형이라면 같은 의미지만, Value를 포인터 형으로 선언하였기 때문에 같은 의미가 아니다.</p>
<p>따라서 구현한 테이블의 이러한 특성을 알고 있어 Value가 포인터 형으로 선언되어야 한다는 제약을 없애기 위해서는 어떻게 하면 좋을까?
그러기 위해서는 다음 두 가지를 고민해봐야 한다.</p>
<ul>
<li>함수호출을 통해서 얻고자 하는 데이터를 어떻게 전달할 것인가?</li>
<li>함수호출의 성공여부를 어떻게 전달할 것인가?</li>
</ul>
<p>함수의 반환 값 하나에 담으려 한 경우 괜찮은 경우도 있지만 그렇지 않은 경우가 있다.
예를 들어서 Value가 포인터 형이어야 한다는 제약사항을 없애기 위해서는 위의 두 가지 답변을 듣는 경로를 다음과 같이 나눠야 한다.</p>
<pre><code class="language-c">Value TBLDelete(Table * pt, Key k, Value * pv)
{
    ....
    return FALSE;    // 삭제할 대상이 존재하지 않는 경우
}

Value TBLSearch(Table * pt, Key k, Value * pv)
{
    ....
    return FALSE;    // 찾는 대상이 존재하지 않는 경우
}</code></pre>
<p>이는 Value가 포인터 형이어야 한다는 제약사항을 없앨 목적으로 다시 정의한 함수다.
반환 값을 통해서 함수호출의 성공여부를, 매개변수를 통해서 삭제 또는 탐색 대상의 값을 얻도록 정의했다.
이를 통해서 Value가 주소 값이어야 한다는 제약사항도 사라지게 되었다.</p>
<hr>
<h2 id="review"><code>&lt;Review&gt;</code></h2>
<p>이번 테이블은 해쉬 함수까지는 괜찮았지만 연결 리스트 (앞에서 배운 내용)과 연결지어 나올 땐 약간 이해하기 어려웠다.
코드를 따라 치면서도 어떤 걸 가리키고 있는지 머리로 잘 그려지지 않았다.
이 부분에 대해서는 손으로 직접 그려가며, 예시를 생각하면서 다시 한번 이해해봐야 하는 부분인거 같다.</p>
<p>드디어 마지막 Chapter 하나를 남겨두고 있다.
코딩 공부을 하면 할수록 내가 알고 있는 것도 잘 알고 있는지 의문이 들고 있는데,,,
코딩을 정말 좋아하고 열정적으로 공부하는 사람들의 마음가짐,
내가 초반에 코딩 공부를 시작하게 된 이유와 마음가짐을 다시 한번 돌아봐야겠다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/ba418d56-d16b-463e-bd5f-ce6341709e73/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chapter 12. 탐색(Search) 2]]></title>
            <link>https://velog.io/@mingming_eee/datastructure-12</link>
            <guid>https://velog.io/@mingming_eee/datastructure-12</guid>
            <pubDate>Wed, 04 Dec 2024 02:39:32 GMT</pubDate>
            <description><![CDATA[<h2 id="12-1-균형-잡힌-이진-탐색-트리-avl-트리의-이해">12-1. 균형 잡힌 이진 탐색 트리: AVL 트리의 이해</h2>
<p>이번 Chapter에서는 Chapter 11에서 배운 이진 탐색 트리의 단점을 개선한 또 다른 이진 탐색 트리에 대해 배우려한다.</p>
<h3 id="이진-탐색-트리의-단점과-avl-트리"><em>이진 탐색 트리의 단점과 AVL 트리</em></h3>
<p>이진 탐색 트리의 탐색 연산은 $$O(log_2n)$$의 시간 복잡도를 가진다.
트리의 높이를 하나씩 더해갈수록 추가할 수 있는 노드의 수가 두 배씩 증가하므로, 이진 탐색 트리의 빅-오는 별도의 계산을 거치지 않고서 쉽게 판단이 가능하다.
그런데 이러한 이진 탐색 트리는 균형이 맞지 않을수록 $$O(n)$$에 가까운 시간 복잡도를 보인다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/44d4dfc1-46ab-4b9e-af7c-a77acde7c1a6/image.png" alt=""></p>
<p>위 그림은 1부터 5까지의 정수가 순서대로 저장되었을 때 이진 탐색 트리를 보여준다.
이 트리는 이진 탐색 트리의 조건을 만족하는데 그럼에도 불구하고 노드의 수에 가까운 높이를 형성한다.
반면 1부터 5까지의 정수를 순서대로 저장하되 3을 제일 먼저 저장하면 결과는 다음과 같아진다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/e8d598e5-a69d-4f0a-ad69-a2e00b899c92/image.png" alt=""></p>
<p>저장 순서만 조금 바꿨더니 루트 노드를 기준으로 어느 정도 균형이 잡혔고 트리의 높이도 반으로 줄었다.
이렇듯 앞서 구현한 이진 탐색 트리는 저장 순서에 따라 탐색의 성능에 큰 차이를 보인다.
이것이 바로 이진 탐색 트리의 단점이다.</p>
<p>이러한 이진 탐색 트리의 단점을 해결한 트리를 가리켜 &#39;균형 잡힌 이진 트리&#39;라 하고,
그 종류는 대략 다음과 같다.</p>
<ul>
<li>AVL 트리</li>
<li>2-3 트리</li>
<li>2-3-4 트리</li>
<li>Red-Black 트리</li>
<li>B 트리</li>
</ul>
<p>이 중에서 하나를 선택하여 우리가 구현한 &#39;이진 탐색 트리&#39;가 자동으로 균형 잡을 수 있도록 개선하고자 한다.
그럼 이제부터 AVL 트리를 구현해보자~!</p>
<h3 id="avl-트리와-균형-인수-balance-factor"><em>AVL 트리와 균형 인수 (Balance Factor)</em></h3>
<p>AVL 트리는 G.M.Adelson-Velskii와 E.M.Landis에 의해 1960년대에 고안되었다.
그래서 트리의 이름도 이들의 이름을 따서 정해졌다.
AVL 트리는 노드가 추가될 때, 그리고 삭제될 때 트리의 균형상태를 파악해서 스스로 그 구조를 변경해 균형을 잡는 트리다.
AVL 트리에서의 균형의 정도를 표현하기 위해 &#39;균형 인수(Balance Factor)&#39;라는 것을 사용하는데 이는 <code>균형 인수 = 왼쪽 서브 트리의 높이 - 오른쪽 서브 트리의 높이</code>로 계산된다.
다음 두 개의 예시 이진 트리의 각 노드 별 균형 인수를 확인해보자.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/1073df15-c955-4569-88a0-856ed304b7a8/image.png" alt=""></p>
<p>노드 위에 적힌 숫자가 &#39;균형 인수&#39;다.
트리의 균형을 잡기 위해 구조를 재조정하는 것을 가리켜 리밸런싱(rebalancing)이라고 하는데, AVL 트리의 리밸런싱 시기를 짐작해보자.
균형 인수의 절댓값이 크면 클수록 그만큼 트리의 균형이 무너진 상태다.
따라서 AVL 트리는 균형 인수의 절댓값이 2 이상인 경우에 균형을 잡기 위한 트리의 재조정을 진행한다.</p>
<h3 id="리밸런싱이-필요"><em>리밸런싱이 필요</em></h3>
<p>AVL 트리의 균형이 무너지는 상태는 4가지로 정리가 된다.
그리고 각 상태 별 리밸런싱 방법에도 차이가 있다.</p>
<h4 id="1-첫-번째-상태와-ll회전">1) 첫 번째 상태와 LL회전</h4>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/3df6ab01-fbb2-4ce3-b820-d6b253337fb4/image.png" alt=""></p>
<p>리밸런싱이 필요한 첫 번째 상태로 위 그림의 왼편을 보면 루트 노드의 균형 인수가 +2다.
이렇듯 균형 인수 +2가 연출된 상황을 &quot;5가 저장된 노드의 <U>왼쪽</U>에 3이 저장된 자식 노드가 하나 존재하고, 그 자식 노드의 <U>왼쪽</U>에 1이 저장된 자식 노드가 또 하나 존재한다.&quot;라고 표현할 수 있다.
이 표현을 다시 잘 살펴보면 말의 핵심은 자식 노드 두 개가 왼쪽으로 연이어 연결되어 균형 인수 +2가 연출되었다는 것이다.
따라서 균형 인수 +2가 연출된 이 상태를 가리켜 &quot;Left Left 상태&quot;. 즉, &quot;LL상태&quot;라 한다.
그리고 이러한 &quot;LL상태&quot;에서 발생한 불균형의 해소를 위해 등장한 리밸런싱 방법을 가리켜 &#39;LL회전&#39;이라 한다.
&#39;LL회전&#39;이란 &#39;LL상태&#39;에서 균형을 잡기 위해 필요한 회전을 의미한다.</p>
<p>따라서 위 그림에서 LL회전의 방법과 그 결과를 오른쪽에서 보여준다.
LL회전의 핵심은 균형 인수가 +2인 노드를 균형 인수가 +1인 노드의 오른쪽 자식 노드가 되게 하는데 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/37f94bd5-592e-480d-85cb-0e31555d6725/image.png" alt=""></p>
<p>따라서 위 그림처럼 pNode와 cNode가 각각 균형 인수가 +2인 노드와 그 자식 노드를 가리킨다고 가정하면, <a href="https://velog.io/@mingming_eee/datastructure-11#%EC%9D%B4%EC%A7%84-%ED%83%90%EC%83%89-%ED%8A%B8%EB%A6%AC%EC%9D%98-%EA%B5%AC%ED%98%84">Chapter 11에서 완성한 BinaryTree3.c</a>에 정의된 함수를 도구로 하여 <code>ChangeRightSubTree(cNode, pNode);</code> 문장으로 LL회전을 완성할 수 있다.</p>
<p>LL회전을 일반화한 그림은 다음과 같다.
그리고 사실 저 한문장으로 LL회전을 일반화하여 구현할 수는 없다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/d2e3e06c-b615-42d0-808b-7e72f6e828c3/image.png" alt=""></p>
<p>T1, T2, T3, T4를 높이가 동일한 서브 트리라고 생각해보자.
이들은 5가 저장된 노드와 3이 저장된 노드의 균형 인수에 영향을 미치지 않는다.
때문에 이 그림의 구조 역시 LL상태에 해당한다.
그리고 T1, T2, T3, T4를 NULL로 치환하면 앞서 보인 LL상태가 된다.</p>
<p>이 그림에서 LL회전을 위해 추가로 고민해야 할 것은 무엇일까? T3이다.
T3의 부모 노드는 루트 노드가 될 것이기 때문에 T3의 자리를 다른 노드에게 양보해야한다.
예시 그림으로 치면 5가 저장된 노드에게 양보해야하고 이 후 T3는 어디로 가야할까?
5가 저장된 노드의 왼쪽 자식 노드의 위치이다.</p>
<p>따라서 위 그림의 오른쪽과 같이 cNode가 가리키는 노드의 오른쪽 서브 트리를 pNode가 가리키는 노드의 왼쪽 서브 트리로 옮기기 위해 <code>ChangeLeftSubTree(pNode, GetRightSubTree(cNode));</code> 문장을 실행해야 한다.
따라서 일반화한 LL회전을 구현하려면 다음 문장을 순서대로 실행해야 한다.</p>
<pre><code class="language-c">ChangeLeftSubTree(pNode, GetRightSubTree(cNode));
ChangeRightSubTree(cNode, pNode);</code></pre>
<h4 id="2-두-번째-상태와-rr회전">2) 두 번째 상태와 RR회전</h4>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/2309e87a-3410-41c0-bae2-8ac8dfcf2fe9/image.png" alt=""></p>
<p>이번에는 리밸런싱이 필요한 두 번째 상태로 RR상태이자 RR회전이다.
RR상태와 LL상태의 차이점은 RR회전과 LL회전으로 방향이 유일한 차이이다.
따라서 위 그림과 같이 회전하면 된다.
5가 저장된 노드가 7이 저장된 노드의 왼쪽 자식 노드가 되고, T3은 5가 저장된 노드의 오른쪽 서브 트리가 된다.
코드로 나타내면 다음과 같다.</p>
<pre><code class="language-c">ChangeRightSubTree(pNode, GetLeftSubTree(cNode));
ChangeLeftSubTree(cNode, pNode);</code></pre>
<p>LL회전과 RR회전에서 서브트리를 먼저 옮겨주는 것은 먼저 옮기지 않으면 이 서브 트리의 주소 값을 잃기 때문이다.</p>
<h4 id="3-세-번째-상태와-lr회전">3) 세 번째 상태와 LR회전</h4>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/d2d25cab-714a-4246-968f-959ade4320f2/image.png" alt=""></p>
<p>리밸런싱이 필요한 세번째 상태로는 LR상태이자 LR회전이다.
LR회전은 LL상태나 RR상태보다 균형을 잡기 복잡하다.
한번의 회전으로 균형을 잡을 수 없기 때문이다.
따라서 LR상태를 한 번의 회전으로 균형이 잡히는 LL상태나 RR상태로 바꾼 다음 LL회전이나 RR회전으로 리밸런싱을 진행한다.
LR상태는 RR회전을 통해 LL상태가 될 수 있게 된다.
이는 RR회전의 부수적인 효과를 이용한 것으로 볼 수 있는데, 자세히 이 과정을 살펴보자.</p>
<table>
<thead>
<tr>
<th>No.</th>
<th>Pic.</th>
<th>Expl.</th>
</tr>
</thead>
<tbody><tr>
<td>0</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/5016bb77-6782-4ffe-9957-9a7ab02ec0c0/image.png" alt=""></td>
<td>이것이 일반적인 RR회전이다.</td>
</tr>
<tr>
<td>1</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/5b812f43-20ee-487e-b32e-d9b21118378a/image.png" alt=""></td>
<td>여기서 9가 저장된 노드를 NULL로 치환한다.</td>
</tr>
<tr>
<td>2</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/9e3101c5-b250-4203-8647-cefd1c2a4fff/image.png" alt=""></td>
<td>그리고 RR회전을 진행하면 부모자식이 바뀌는 효과를 얻게 된다.</td>
</tr>
</tbody></table>
<p>이 부수적인 효과를 이용해서 LR상태를 RR회전을 통해 LL상태로 만들 것이다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/fdc518f2-62df-4a54-9897-fa095f7d700a/image.png" alt=""></p>
<p>위 그림과 같이 LR상태를 LL상태로 바꾸고</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/ce58fc0c-4f1c-48d3-ae44-1298f513cd4c/image.png" alt=""></p>
<p>바뀐 부분을 LL회전을 통해 리밸런싱해 정리한다.</p>
<h4 id="4-네-번째-상태와-rl회전">4) 네 번째 상태와 RL회전</h4>
<p>마지막으로 리밸런싱이 필요한 상태로는 RL상태이자 RL회전이다.
위에서 이미 조금 복잡한 과정이었던 LR상태를 배웠다.
동일하게 RL상태를 LL회전을 통해 RR상태로 바꿔서 RR회전을 통해 리밸런싱을 한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/f842c754-8918-4bc8-afb3-eb44c5d50618/image.png" alt=""></p>
<p>RL상태에서 LL회전을 통해 RR상태로 만들고</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/d63ff812-ffd1-4c29-901e-b911cd95bb40/image.png" alt=""></p>
<p>마지막으로 RR회전을 통해 리밸런싱을 마무리한다.</p>
<hr>
<h2 id="12-2-균형-잡힌-이진-탐색-트리-avl-트리의-구현">12-2. 균형 잡힌 이진 탐색 트리: AVL 트리의 구현</h2>
<p>AVL 트리의 &#39;이론적 설명&#39;과 &#39;구현&#39;을 구분한 이유는 AVL 트리의 이론적인 이해만으로도 의미가 있기 때문이다.</p>
<h3 id="avl-트리-구현-방법"><em>AVL 트리 구현 방법</em></h3>
<p>AVL 트리도 이진 탐색 트리이므로, 이진 탐색 트리의 구현결과인 다음 파일들을 확장하여 AVL 트리를 구현하고자 한다.</p>
<ul>
<li>BinaryTree3.h : 이진 트리의 헤더파일</li>
<li>BinaryTree3.c : 이진 트리를 구성하는데 필요한 도구들의 모임 (소스파일)</li>
<li>BinarySearchTree2.h : 이진 탐색 트리의 헤더파일</li>
<li>BinarySearchTree2.c : 이진 탐색 트리의 구현 (소스파일)</li>
</ul>
<p>위 파일들은 Chapter 11에서 공부했던 내용으로 <a href="https://velog.io/@mingming_eee/datastructure-11#%EC%9D%B4%EC%A7%84-%ED%83%90%EC%83%89-%ED%8A%B8%EB%A6%AC%EC%9D%98-%EA%B5%AC%ED%98%84">지난 게시물</a>에서도 파일 내용을 확인할 수 있다.
위 파일들 중 BinarySearchTree2.c는 이진 탐색 트리의 구현 결과에 해당하는데 이 파일에서도 이진 트리의 구성을 위해서 BinaryTree3.c에 정의된 함수들을 호출한다.</p>
<p>그럼 AVL 트리의 구현을 위해서 BinaryTree3.c와 BinarySearchTree2.c를 변경해야할까?
BinaryTree3.c에 담긴 것은 이진 트리를 구성하는 도구들이기 때문에 이미 그 기능이 충분해서 변경하지 않고 BinarySearchTree2.c에 담겨있는 이진 탐색 트리에 리밸런싱 기능을 추가하면 AVL 트리가 된다.
따라서 BinarySearchTree2.c에 리밸런싱 기능을 추가하여 파일 이름을 BinarySearchTree3.c로 파일명을 변경할 것이다.
리밸런싱 기능은 리밸런싱에 필요한 함수를 추가로 정의하는 것이 아닌 노드의 추가 및 삭제 시 자동으로 리밸런싱이 진행되도록 그 기능을 확장할 것이다.</p>
<p>마지막으로 다음 두 파일을 추가로 생성하여 리밸런싱을 진행하는데 필요한 도구들을 선언하고 정의할 것이다.</p>
<ul>
<li>AVLRebalance.h : 리밸런싱 관련 함수들의 선언</li>
<li>AVLRebalance.c : 리밸런싱 관련 함수들의 정의</li>
</ul>
<p>위 파일들에서 정의된 리밸런싱 도구를 이용해 BinarySearchTree3.c에 담긴 AVL 트리 리밸런싱이 진행된다.</p>
<h3 id="binarysearchtree2c의-확장-포인트binarysearchtree3c"><em>BinarySearchTree2.c의 확장 포인트(BinarySearchTree3.c)</em></h3>
<p>루트 노드를 기준으로 왼쪽과 오른쪽의 균형이 잘 잡혀있는 이진 탐색 트리가 있다.
이 트리의 균형이 깨지는 (루트 노드의 균형 인수의 절댓값이 1을 넘어가는) 상황은 언제 발생할까?
노드의 삽입과 삭제의 과정에서 발생한다.
따라서 BinarySearchTree2.c의 이진 탐색 트리를 AVL 트리가 되게 하기 위해서 확장해야 하는 함수는 다음 두 가지다.</p>
<ul>
<li>BSTInsert 함수 : 트리에 노드를 추가</li>
<li>BSTRemove 함수 : 트리에서 노드를 제거</li>
</ul>
<p>따라서 트리의 균형을 재조정하는 함수의 이름을 Rebalance라 했을 때, 위에서 언급한 두 함수는 대략 다음과 같은 방식으로 확장될 것이다.</p>
<pre><code class="language-c">void BSTInsert(BTreeNode ** pRoot, BSTData data)
{
    ....
    Rebalance(pRoot);    // 노드 추가 후 리밸런싱
}

BTreeNode * BSTRemove(BTreeNode ** pRoot, BSTData target)
{
    ....
    Rebalance(pRoot);    // 노드 제거 후 리밸런싱
    return dNode;
}</code></pre>
<p>Rebalance 함수 호출문을 보면 인자로 루트 노드의 정보를 전달하고 있다.
이는 트리의 불균형 여부를 루트 노드를 기준으로 확인해야 하기 때문이다.</p>
<h3 id="리밸런싱에-필요한-도구들의-정의"><em>리밸런싱에 필요한 도구들의 정의</em></h3>
<p>리밸런싱에 필요한 도구들을 살펴보기 위해서는 다음 두 질문에 답을 해야한다.</p>
<ol>
<li>이 트리는 리밸런싱이 필요한 불균형 상태인가?</li>
<li>왼쪽 서브 트리의 높이는 어떻게 되며, 오른쪽 서브 트리의 높이는 어떻게 되는가?</li>
</ol>
<p>불균형의 여부는 루트 노드 기준으로 판단하고, 루트 노드의 왼쪽 서브 트리의 높이와 오른쪽 서브 트리의 높이를 확인해서 그 차를 계산해 각 서브 트리의 높이와 불균형 여부를 확인할 수 있다.</p>
<h4 id="1-균형을-이루고-있는가">1) 균형을 이루고 있는가?</h4>
<p>리밸런싱에 필요한 첫 번째 도구로 다음 함수를 정의한다.</p>
<pre><code class="language-c">// 트리의 높이를 계산하여 반환
int GetHeight(BTreeNode * bst)
{
    int leftH;        // left height
    int rightH;        // right height

    if(bst == NULL)
        return 0;

    leftH = GetHeight(GetLeftSubTree(bst));        // 왼쪽 서브 트리의 높이 계산
    rightH = GetHeight(GetRightSubTree(bst));    // 오른쪽 서브 트리의 높이 계산

    // 큰 값의 높이를 반환
    if(leftH &gt; rightH)
        return leftH + 1;
    else
        return rightH + 1;
}</code></pre>
<p>트리는 단말 노드의 수만큼 경로가 나뉘기 때문에, 그리고 트리의 높이는 그 중에서 가장 깊이 뻗은 경로를 기준으로 결정되기 때문에 위와 같이 재귀적인 형태로 정의해야 한다.
위 함수는 GetHeight 함수가 호출될 때마다 높이를 1씩 더해가는 구조로 정의되어 있다.
그리고 동일한 레벨에서의 왼쪽 서브 트리와 오른쪽 서브 트리의 높이를 비교하여 큰 값이 반환되도록 정의되어 있다.
따라서 트리의 모든 경로 중에서 가장 깊이 뻗은 경로의 높이를 반환하게 된다.</p>
<p>이 트리 높이를 계산하는 도구를 이용해서 균형 인수를 계산해주는 함수를 만든다.</p>
<pre><code class="language-c">// 두 서브 트리의 &#39;높이의 차(균형 인수)&#39;를 반환
int GetHeightDiff(BTreeNode * bst)
{
    int lsh;    // left sub tree height
    int rsh;    // right sub tree height

    if(bst == NULL)
        return 0;

    lsh = GetHeight(GetLeftSubTree(bst));    // 왼쪽 서브 트리의 높이
    rsh = GetHeight(GetRightSubTree(bst));    // 오른쪽 서브 트리의 높이
    return lsh - rsh;    // 균형 인수 계산 결과 반환
}</code></pre>
<p>왼쪽 서브 트리의 높이와 오른쪽 서브 트리의 높이를 구하고 그 차를 반환하는 매우 간단한 함수다.
그럼에도 불구하고 매우 유용하게 사용할 수 있다.
이로써 불균형의 여부를 판단하는데 필요한 도구를 모두 마련했다.</p>
<h4 id="2-ll회전-rr회전">2) LL회전, RR회전</h4>
<p>리밸런싱에 필요한 도구로 회전관련 함수를 추가해보자.
앞서 배운 이론적 설명을 근거로 구현하면 된다.</p>
<p>먼저, LL회전을 담당하는 함수부터 살펴보자.</p>
<pre><code class="language-c">// LL회전을 담당하는 함수
BTreeNode * RotateLL(BTreeNode * bst)
{
    BTreeNode * pNode;        // parent node
    BTreeNode * cNode;        // child node

    // pNode와 cNode가 LL회전을 위해 적절한 위치 가리키기
    pNode = bst;
    cNode = GetLeftSubTree(pNode);

    // 실제 LL회전을 담당하는 부분
    ChangeLeftSubTree(pNode, GetRightSubTree(cNode));
    ChangeRightSubTree(cNode, pNode);

    // LL회전으로 인해 변경된 루트 노드의 주소 값 반환
    return cNode;
}</code></pre>
<p>위 함수는 다음 그림과 같은 LL상태에서 호출된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/af8bc7f6-e0c2-48d5-a94a-bc004c92134e/image.png" alt=""></p>
<p>호출될 때 5가 저장된 노드의 주소 값이 인자로 전달된다.
LL회전이 완료되고 나면 (함수의 마지막 부분에서) 루트 노드가 바뀌게 된다.
따라서 변경된 루트 노드의 정보를 반환해야 한다.
그래서 cNode에 저장된 값을 반환하는 것이다.</p>
<p>다음으로, RR회전을 담당하는 함수를 살펴보자.</p>
<pre><code class="language-c">// RR회전을 담당하는 함수
BTreeNode * RotateRR(BTreeNode * bst)
{
    BTreeNode * pNode;        // parent node
    BTreeNode * cNode;        // child node

    // pNode와 cNode가 RR회전을 위해 적절한 위치를 가리키기
    pNode = bst;
    cNode = GetRightSubTree(pNode);

    // 실제 RR회전을 담당하는 부분
    ChangeRightSubTree(pNode, GetLeftSubTree(cNode));
    ChangeLeftSubTree(cNode, pNode);

    // RR회전으로 인해 변경된 루트 노드의 주소 값 반환
    return cNode;
}</code></pre>
<p>구현된 코드를 살펴보면 RotateLL함수와 방향 말고는 차이가 없다.</p>
<h4 id="2-lr회전-rl회전">2) LR회전, RL회전</h4>
<p>앞서 LR회전과 RL회전에 대해 이론적 설명을 통해 이해했다.
코드로 어떻게 표현되어야 하는지 다시 한번 정리하면 다음과 같다.</p>
<ul>
<li>LR회전 : 부분적 RR회전에 이어서 LL회전 진행</li>
<li>RL회전 : 부분적 LL회전에 이어서 RR회전 진행</li>
</ul>
<p>그럼 먼저 LR회전을 담당하는 함수의 구현을 살펴보자.</p>
<pre><code class="language-c">// LR회전을 담당하는 함수
BTreeNode * RotateLR(BTreeNode * bst)
{
    BTreeNode * pNode;        // parent node
    BTreeNode * cNode;        // child node

    // pNode와 cNode가 LR회전을 위해 적잘한 위치를 가리키기
    pNode = bst;
    cNode = GetLeftSubTree(pNode);

    // 실제 LR회전을 담당하는 부분
    ChangeLeftSubTree(pNode, RotateRR(cNode));    // 부분적 RR회전
    return RotateLL(pNode);                        // LL회전
}</code></pre>
<p>실제 LR회전을 담당하는 부분 코드에서 <code>ChangeLeftSubTree(pNode, RotateRR(cNode));</code>를 살펴보자.
우선 RotateRR 함수를 호출하면서 cNode를 전달한다.
cNode가 가리키는 위치는 다음 그림과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/8f7743f1-b3cd-40a5-86a7-1bde5320e0f2/image.png" alt=""></p>
<p>RotateRR 함수 호출을 통해서 1이 저장된 노드 즉, 5가 저장된 루트 노드의 왼쪽 자식 노드를 중심으로 RR회전을 진행한다.
이렇듯 일부를 떼어서 회전을 진행한다는 것은 루트 노드가 아닌 <strong>1이 저장된 노드의 주소 값을 인자로</strong> RotateRR 함수를 호출함을 의미한다.</p>
<p>RotateRR 함수 호출이 완료되면 아래 그림에서 3이 저장된 노드의 주소 값이 반환된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/d989a96d-5db2-45cf-9258-cc58dc08ea08/image.png" alt=""></p>
<p>그리고 이 주소 값을 두 번째 인자로 하여 ChangeLeftSubTree 함수를 호출하여 LL 상태가 되게 한다.
위 그림에서 &#39;다시 붙인다&#39;는 말은 ChangeLeftSubTree 함수를 호출하면서 RotateRR 함수의 반환 값을 두 번째 인자로 전달한다는 것을 의마한다.</p>
<p>LR회전의 첫 번째 단계인 부분적 RR회전이 완료된 후 그 다음 문장인 <code>return RotateLL(pNode);</code> 부분이 실행되고 이것이 LL회전이다.
RotateLR 함수의 반환 값으로 RotateLL 함수의 반환 값ㅇ르 반환하는 이유는 LR회전의 결과로 바뀌게 된 루트 노드의 주소 값을 반환하기 위함이다.</p>
<p>마지막으로 RL회전을 담당하는 함수의 구현을 보자.</p>
<pre><code class="language-c">// RL회전을 담당하는 함수
BTreeNode * RotateRL(BTreeNode * bst)
{
    BTreeNode * pNode;        // parent node
    BTreeNode * cNode;        // child node

    // pNode와 cNode가 RL회전을 위해 적절한 위치를 가리키기
    pNode = bst;
    cNode = GetRightSubTree(pNode);

    // 실제 RL회전을 담당하는 부분
    ChangeRightSubTree(pNode, RotateLL(cNode));    // 부분적 LL회전
    return RotateRR(pNode);                        // RR회전
}</code></pre>
<p>RotateLR 함수와 방향 및 회전의 순서에만 차이를 보인다.</p>
<h4 id="3-rebalance-함수">3) Rebalance 함수</h4>
<p>불균형 여부를 판단하는 도구와 상태에 따른 회전 도구를 만듦으로써 기본적인 도구는 모두 마련이 되었다.
마지막으로 이 도구들을 사용하기 편하도록 이 도구들의 사용순서 및 사용시기를 모두 담은 도구를 하나 만든다.</p>
<pre><code class="language-c">BTreeNode * Rebalance(BTreeNode ** pRoot)
{
    int hDiff = GetHeightDiff(*pRoot);        // 균형 인수 계산

    // 균형 인수가 +2 이상이면 LL상태 또는 LR상태
    if(hDiff &gt; 1)    // 왼쪽 서브 트리 방향으로 높이가 2이상 크다면,
    {
        if(GetHeightDiff(GetLeftSubTree(*pRoot)) &gt; 0)
            *pRoot = RotateLL(*pRoot);
        else
            *pRoot = RotateLR(*pRoot);
    }

    // 균형 인수가 -2 이하면 RR상태 또는 RL상태
    if(hDiff &lt; -1)    // 오른쪽 서브 트리 방향으로 높이가 2 이상 크다면,
    {
        if(GetHeightDiff(GetRightSubTree(*pRoot)) &lt; 0)
            *pRoot = RotateRR(*pRoot);
        else
            *pRoot = RotateRL(*pRoot);
    }

    return *pRoot;
}</code></pre>
<p>루트 노드의 균형 인수가 +2 이상이면 LL상태 또는 LR상태이고,
-2 이하면 RR상태 또는 RL상태란 것은 알고 있을 것이다.
그럼 LL상태와 LR상태를 구분하는 방법에 대해 자세히 이해해보자.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/59a645ce-4df0-49ca-8546-5e99eab7b90d/image.png" alt=""></p>
<p><code>GetHeightDiff(GetLeftSubTree(*pRoot))</code>부분의 반환값을 살펴보면
LL상태라면 +1이므로 0보다 크게 되고, 따라서 RotateLL 함수가 호출된다.
LR상태라면 -1이므로 0보다 작게 되고, 따라서 RotateLR 함수가 호출된다.</p>
<p>RR상태와 RL상태를 구분하는 방법도 비슷하게 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/5af232a6-9d85-47c2-a30e-5a29c4cd6f60/image.png" alt=""></p>
<p>RR상태라면 -1이므로 0보다 작게 되고, 따라서 RotateRR 함수가 호출된다.
RL상태라면 +1이므로 0보다 크게 되고, 따라서 RotateRL 함수가 호출된다.</p>
<p>참고로 XX방향 회전관련 함수의 호출은 <code>*pRoot = RotateXX(*pRoot);</code> 와 같은 형태를 보이게 되고,
Rebalance 함수의 마지막 문장이 <code>return *pRoot;</code>로 루트 노드의 주소 값 정보를 반환한다.
반환하는 이유는 회전의 과정에서 루트 노드가 변경될 수 있기 때문이다.
따라서 Rebalance 함수를 호출할 때는 루트 노드의 변경을 대비해서 이 함수가 반환하는 값을 루트 노드를 가리키는 포인터 변수에 저장해야 한다.</p>
<h4 id="4-리밸런싱의-도구">4) 리밸런싱의 도구</h4>
<p>리밸런싱 도구를 담고 있는 헤더파일과 소스파일을 정리하려한다.
설명은 앞에서 다 했으므로 자세한 설명은 생략하고 코드만 보여주려 한다.</p>
<p><strong>- AVLRebalance.h</strong></p>
<pre><code class="language-c">#ifndef __AVL_REBALANCE_H__
#define __AVL_REBALANCE_H__

#include &quot;BinaryTree3.h&quot;

// 트리의 균형 잡기 (Rebalnceing)
BTreeNode * Rebalance(BTreeNode ** pRoot);

#endif</code></pre>
<p><strong>- AVLRebalance.c</strong></p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &quot;BinaryTree3.h&quot;

// LL회전
BTreeNode * RotateLL(BTreeNode * bst)
{
    BTreeNode * pNode;        // parent node
    BTreeNode * cNode;        // child node

    // pNode와 cNode가 LL회전을 위해 적절한 위치 가리키기
    pNode = bst;
    cNode = GetLeftSubTree(pNode);

    // 실제 LL회전을 담당하는 부분
    ChangeLeftSubTree(pNode, GetRightSubTree(cNode));
    ChangeRightSubTree(cNode, pNode);

    // LL회전으로 인해 변경된 루트 노드의 주소 값 반환
    return cNode;
}

// RR회전
BTreeNode * RotateRR(BTreeNode * bst)
{
    BTreeNode * pNode;        // parent node
    BTreeNode * cNode;        // child node

    // pNode와 cNode가 RR회전을 위해 적절한 위치를 가리키기
    pNode = bst;
    cNode = GetRightSubTree(pNode);

    // 실제 RR회전을 담당하는 부분
    ChangeRightSubTree(pNode, GetLeftSubTree(cNode));
    ChangeLeftSubTree(cNode, pNode);

    // RR회전으로 인해 변경된 루트 노드의 주소 값 반환
    return cNode;
}

// LR회전
BTreeNode * RotateLR(BTreeNode * bst)
{
    BTreeNode * pNode;        // parent node
    BTreeNode * cNode;        // child node

    // pNode와 cNode가 LR회전을 위해 적잘한 위치를 가리키기
    pNode = bst;
    cNode = GetLeftSubTree(pNode);

    // 실제 LR회전을 담당하는 부분
    ChangeLeftSubTree(pNode, RotateRR(cNode));    // 부분적 RR회전
    return RotateLL(pNode);                        // LL회전
}

// RL회전
BTreeNode * RotateRL(BTreeNode * bst)
{
    BTreeNode * pNode;        // parent node
    BTreeNode * cNode;        // child node

    // pNode와 cNode가 RL회전을 위해 적절한 위치를 가리키기
    pNode = bst;
    cNode = GetRightSubTree(pNode);

    // 실제 RL회전을 담당하는 부분
    ChangeRightSubTree(pNode, RotateLL(cNode));    // 부분적 LL회전
    return RotateRR(pNode);                        // RR회전
}

// 트리의 높이를 계산하여 반환
int GetHeight(BTreeNode * bst)
{
    int leftH;        // left height
    int rightH;        // right height

    if(bst == NULL)
        return 0;

    leftH = GetHeight(GetLeftSubTree(bst));        // 왼쪽 서브 트리의 높이 계산
    rightH = GetHeight(GetRightSubTree(Bst));    // 오른쪽 서브 트리의 높이 계산

    // 큰 값의 높이를 반환
    if(leftH &gt; rightH)
        return leftH + 1;
    else
        return rightH + 1;
}

// 두 서브 트리의 &#39;높이의 차(균형 인수)&#39;를 반환
int GetHeightDiff(BTreeNode * bst)
{
    int lsh;    // left sub tree height
    int rsh;    // right sub tree height

    if(bst == NULL)
        return 0;

    lsh = GetHeight(GetLeftSubTree(bst));    // 왼쪽 서브 트리의 높이
    rsh = GetHeight(GetRightSubTree(bst));    // 오른쪽 서브 트리의 높이
    return lsh - rsh;    // 균형 인수 계산 결과 반환
}

// Rebalance 함수
BTreeNode * Rebalance(BTreeNode ** pRoot)
{
    int hDiff = GetHeightDiff(*pRoot);        // 균형 인수 계산

    // 균형 인수가 +2 이상이면 LL상태 또는 LR상태
    if(hDiff &gt; 1)    // 왼쪽 서브 트리 방향으로 높이가 2이상 크다면,
    {
        if(GetHeightDiff(GetLeftSubTree(*pRoot)) &gt; 0)
            *pRoot = RotateLL(*pRoot);
        else
            *pRoot = RotateLR(*pRoot);
    }

    // 균형 인수가 -2 이하면 RR상태 또는 RL상태
    if(hDiff &lt; -1)    // 오른쪽 서브 트리 방향으로 높이가 2 이상 크다면,
    {
        if(GetHeightDiff(GetRightSubTree(*pRoot)) &lt; 0)
            *pRoot = RotateRR(*pRoot);
        else
            *pRoot = RotateRL(*pRoot);
    }

    return *pRoot;
}</code></pre>
<h3 id="avl-트리를-담고-있는-파일">AVL 트리를 담고 있는 파일</h3>
<p>앞에서 BSTInsert 함수와 BSTRemove 함수를 소개하면서 Rebalance 함수의 호출시기를 언급했었는데 이 부분에 대해서 루트 노드가 변경되는 것을 대비해서 함수의 호출 문장을 수정해야 한다.</p>
<pre><code class="language-c">void BSTInsert(BTreeNode ** pRoot, BSTData data)
{
    ....
    *pRoot = Rebalance(pRoot);    // 노드 추가 후 리밸런싱
}

BTreeNode * BSTRemove(BTreeNode ** pRoot, BSTData target)
{
    ....
    *pRoot = Rebalance(pRoot);    // 노드 제거 후 리밸런싱
    return dNode;
}</code></pre>
<p>여기서 주의해야할 점은 노드를 추가할 때 삽입 과정에서 불균형 여부ㄹ의 확인 대상을 루트 노드만으로 하는 경우 모든 불균형 상황을 감지하지 못하기 때문에 경우를 나누어 리밸런싱을 진행해야 한다.</p>
<p>그 과정은 다음과 같다.</p>
<ol>
<li>루트 노드를 대상으로 데이터의 저장을 시도 (함수 호출 시작)</li>
<li>루트 노드에 저장된 데이터와 새 데이터 비교
3-1. 비교하여 새 데이터의 값이 작으면 왼쪽 자식 노드를 루트 노드로 하여 데이터의 저장을 시도
3-2. 비교하여 새 데이터의 값이 크면 오른쪽 자식 노드를 루트 노드로 하여 데이터의 저장을 시도</li>
<li>저장이 완료되면 해당 루트 노드를 기준으로 리밸런싱 진행</li>
</ol>
<p>그림으로 이 과정에 대한 이해를 더 잘해보자.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/94bdaf0a-dde7-4657-9ea1-62b24f1bd936/image.png" alt=""></p>
<p>이와 같은 이진 트리가 있고 이는 균형 잡혀 있는 상태다.
여기서 6이 저장된 노드를 추가하려 한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/babbd627-b77b-47aa-8faa-89c04500daa6/image.png" alt=""></p>
<p>그럼 위와 같이 균형이 잡히지 않는 상태가 된다.
이러한 형태로 균형이 무너지게 되면 루트 노드를 기준으로 어떤 회전을 하더라도 균형이 잡히지 않는다.
사실 4가 저장된 노드에서부터 균형이 무너진 상태가 되는데 따라서 4를 중심으로 리밸런싱을 진행해야 한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/6125cf96-51c7-418a-a7b3-ecab90ccb625/image.png" alt=""></p>
<p>4가 저장된 노드를 중심으로 리밸런싱을 진행하면 위 그림과 같이 균형이 맞게 된다.</p>
<p>이와 같이 새로 저장된 노드의 부모 노드들을 모두 살펴야 한다.
위 그림에서는 3, 4, 5가 저장된 노드를 기준으로 불균형 여부를 검사해야 한다.
6이 저장된 이 후에도 불균형 여부 검사를 해야한다.</p>
<p>따라서 BSTInsert 함수는 다음과 같이 수정되어야 한다.</p>
<pre><code class="language-c">void * BSTInsert(BTreeNode ** pRoot, BSTData data)
{
    if(*pRoot == NULL)
    {
        *pRoot = MakeBTreeNode();
        SetData(*pRoot, data);
    }
    else if(data &lt; GetData(*pRoot))
    {
        BSTInsert(&amp;((*pRoot)-&gt;left),data);
        *pRoot = Rebalance(pRoot);
    }
    else if(data &gt; GetData(*pRoot))
    {
        BSTInsert(&amp;((*pRoot)-&gt;right),data);
        *pRoot = Rebalance(pRoot);
    }
    else
        return NULL;    // 키의 중복을 허용하지 않음

    return *pRoot;
}</code></pre>
<p>헤더파일을 포함한 #include문의 변화를 제외하고는 Rebalance 함수의 호출문 추가가 BinarySearchTree3.c에서의 유일한 변화다.
BinarySearchTree3.h에서의 변화는 없다.
각 파일의 내용은 아래에 정리해두었다.</p>
<h4 id="1-binarysearchtree3h">1) BinarySearchTree3.h</h4>
<pre><code class="language-c">#ifndef __BINARY_SEARCH_TREE3_H__
#define __BINARY_SEARCH_TREE3_H__

#include &quot;BinaryTree3.h&quot;

typedef BTData BSTData;

// BST의 생성 및 초기화
void BSTMakeAndInit(BTreeNode ** pRoot);

// 노드에 저장된 데이터 반환
BSTData BSTGetNodeData(BTreeNode * bst);

// BST를 대상으로 데이터 저장(노드의 생성과정 포함)
void BSTInsert(BTreeNode ** pRoot, BSTData data);

// BST를 대상으로 데이터 탐색
BTreeNode * BSTSearch(BTreeNode * bst, BSTData target);

// 트리에서 노드를 제거하고 제거된 노드의 주소 값 반환
BTreeNode * BSTRemove(BTreeNode ** pRoot, BSTData target);

// 이진 탐색 트리에 저장된 모든 노드의 데이터 출력
void BSTShowAll(BTreeNode * bst);

#endif</code></pre>
<h4 id="2-binarysearchtree3c">2) BinarySearchTree3.c</h4>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;BinaryTree3.h&quot;
#include &quot;BinarySearchTree3.h&quot;
#include &quot;AVLRebalance.h&quot;

void BSTMakeAndInit(BTreeNode ** pRoot)
{
    *pRoot = NULL;
}

BSTData BSTGetNodeData(BTreeNode * bst)
{
    return GetData(bst);
}

void * BSTInsert(BTreeNode ** pRoot, BSTData data)
{
    if(*pRoot == NULL)
    {
        *pRoot = MakeBTreeNode();
        SetData(*pRoot, data);
    }
    else if(data &lt; GetData(*pRoot))
    {
        BSTInsert(&amp;((*pRoot)-&gt;left),data);
        *pRoot = Rebalance(pRoot);
    }
    else if(data &gt; GetData(*pRoot))
    {
        BSTInsert(&amp;((*pRoot)-&gt;right),data);
        *pRoot = Rebalance(pRoot);
    }
    else
        return NULL;    // 키의 중복을 허용하지 않음

    return *pRoot;
}

BTreeNode * BSTSearch(BTreeNode * bst, BSTData target)
{
    BTreeNode * cNode = bst;        // current node
    BSTData cd;                     // current data

    while(cNode != NULL)
    {
        cd = GetData(cNode);

        if(target == cd)
            return cNode;
        else if(target &lt; cd)
            cNode = GetLeftSubTree(cNode);
        else
            cNode = GetRightSubTree(cNode);
    }

    return NULL;    // 탐색대상이 저장되어 있지 않음.
}

BTreeNode * BSTRemove(BTreeNode ** pRoot, BSTData target)
{
    // 삭제 대상이 루트 노드인 경우 별도로 고려
    BTreeNode * pVRoot = MakeBTreeNode();   // 가상의 루트 노드
    BTreeNode * pNode = pVRoot;             // parent node
    BTreeNode * cNode = *pRoot;             // current node
    BTreeNode * dNode;                      // delete node

    // 루트 노드를 pVRoot가 가리키는 노드의 오른쪽 자식 노드가 되게
    ChangeRightSubTree(pVRoot, *pRoot);

    // 삭제 대상인 노드를 탐색
    while(cNode != NULL &amp;&amp; GetData(cNode) != target)
    {
        pNode = cNode;

        if(target &lt; GetData(cNode))
            cNode = GetLeftSubTree(cNode);
        else
            cNode = GetRightSubTree(cNode);
    }
    // 삭제 대상이 존재하지 않다면
    if(cNode == NULL)
        return NULL;

    // 삭제 대상 dNode 가리키게
    dNode = cNode;

    // 경우 1) 삭제 대상이 단말 노드
    if(GetLeftSubTree(dNode) == NULL &amp;&amp; GetRightSubTree(dNode) == NULL)
    {
        if(GetLeftSubTree(pNode) == dNode)
            RemoveLeftSubTree(pNode);
        else
            RemoveRightSubTree(pNode);
    }

    // 경우 2) 삭제 대상이 하나의 자식 노드
    else if(GetLeftSubTree(dNode) == NULL || GetRightSubTree(dNode) == NULL)
    {
        // 삭제 대상의 자식 노드
        BTreeNode * dcNode;

        if(GetLeftSubTree(dNode) != NULL)
            dcNode = GetLeftSubTree(dNode);
        else
            dcNode = GetRightSubTree(dNode);

        if(GetLeftSubTree(pNode) == dNode)
            ChangeLeftSubTree(pNode, dcNode);
        else
            ChangeRightSubTree(pNode, dcNode);
    }

    // 경우 3) 삭제 대상이 두 개의 자식 노드
    else
    {
        // 대체 노드 가리킴
        BTreeNode * mNode = GetRightSubTree(dNode);
        // 대체 노드의 부모 노드
        BTreeNode * mpNode = dNode;
        int delData;

        // 삭제 대상의 대체 노드 찾기
        while(GetLeftSubTree(mNode) != NULL)
        {
            mpNode = mNode;
            mNode = GetLeftSubTree(mNode);
        }

        // 대체 노드에 저장된 값을 삭제 노드에 대입
        delData = GetData(dNode);
        SetData(dNode, GetData(mNode));

        // 대체 노드의 부모 노드와 자식 노드 연결
        if(GetLeftSubTree(mpNode) == mNode)
            ChangeLeftSubTree(mpNode, GetRightSubTree(mNode));
        else
            ChangeRightSubTree(mpNode, GetRightSubTree(mNode));

        dNode = mNode;
        SetData(dNode, delData);
    }

    // 삭제된 노드가 루트 노드인 경우
    if(GetRightSubTree(pVRoot) != *pRoot)
        *pRoot = GetRightSubTree(pVRoot);   // 루트 노드의 변경 반영

    free(pVRoot);   // 가상 루트 노드 소멸

    // Rebalancing 추가
    *pRoot = Rebalance(pRoot);
    return dNode;   // 삭제된 노드 반환
}

void ShowIntData(int data)
{
    printf(&quot;%d &quot;, data);
}

void BSTShowAll(BTreeNode * bst)
{   
    // 중위 순회
    InorderTraverse(bst, ShowIntData);
    printf(&quot;\n&quot;);
}</code></pre>
<h4 id="3-avltreemainc">3) AVLTreeMain.c</h4>
<p>이제 마지막으로 AVL 트리가 리밸런싱을 제대로 하는지 확인해보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &quot;BinaryTree3.h&quot;
#include &quot;BinarySearchTree3.h&quot;

int main()
{
    BTreeNode * avlRoot;
    BTreeNode * clNode;         // current left node
    BTreeNode * crNode;         // current right node
    BSTMakeAndInit(&amp;avlRoot);

    BSTInsert(&amp;avlRoot, 1);
    BSTInsert(&amp;avlRoot, 2);
    BSTInsert(&amp;avlRoot, 3);
    BSTInsert(&amp;avlRoot, 4);
    BSTInsert(&amp;avlRoot, 5);
    BSTInsert(&amp;avlRoot, 6);
    BSTInsert(&amp;avlRoot, 7);
    BSTInsert(&amp;avlRoot, 8);
    BSTInsert(&amp;avlRoot, 9);

    printf(&quot;Root Node: %d \n&quot;, GetData(avlRoot));

    clNode = GetLeftSubTree(avlRoot);
    crNode = GetRightSubTree(avlRoot);
    printf(&quot;Left 1: %d, Right 1: %d \n&quot;, GetData(clNode), GetData(crNode));

    clNode = GetLeftSubTree(clNode);
    crNode = GetRightSubTree(crNode);
    printf(&quot;Left 2: %d, Right 2: %d \n&quot;, GetData(clNode), GetData(crNode));

    clNode = GetLeftSubTree(clNode);
    crNode = GetRightSubTree(crNode);
    printf(&quot;Left 3: %d, Right 3: %d \n&quot;, GetData(clNode), GetData(crNode));

    clNode = GetLeftSubTree(clNode);
    crNode = GetRightSubTree(crNode);
    printf(&quot;Left 4: %d, Right 4: %d \n&quot;, GetData(clNode), GetData(crNode));

    return 0;
}

&gt; gcc .\AVLRebalance.c .\AVLTreeMain.c .\BinarySearchTree3.c .\BinaryTree3.c
&gt; .\a.exe
&gt; 출력
Root Node: 4 
Left 1: 2, Right 1: 6 
Left 2: 1, Right 2: 8
Left 3: 6422400, Right 3: 1528349827
Left 4: 6422400, Right 4: 1528349827

&gt; 올바른 출력
Root Node: 5
Left 1: 4, Right 1: 6
Left 2: 3, Right 2: 7
Left 3: 2, Right 3: 8
Left 4: 1, Right 4: 9</code></pre>
<p>왜 안될까,,,,</p>
<h3 id="수정"><em>수정</em></h3>
<p>방법을 알아냈다.</p>
<p>마지막에 넣는 자식 노드의 이름들을 잘못 넣었다.
예를 들면 지금 노드 변수가 3쌍(clNode-crNode, clNode2-crNode2, clNode3-crNode3)이 있는데
루트 노드용(avlRoot), 루트 노드의 첫 번째 자식들 노드용(clNode-crNode), 루트 노드의 두 번째 자식들 노드용(clNode2-crNode2), 루트 노드의 세 번째 자식들 노드용(clNode3-crNode3)이다.</p>
<p>이 이름들을 잘 기억하고 있어야한다...
이를 아래와 같이 작성해서 실행해야 한다.</p>
<pre><code class="language-c">    // 변수 추가
    BTreeNode * clNode2;
    BTreeNode * crNode2;

    BTreeNode * clNode3;
    BTreeNode * crNode3;

    // 첫 번째 printf 이후 수정
    clNode = GetLeftSubTree(avlRoot);
    crNode = GetRightSubTree(avlRoot);
    printf(&quot;Left 1: %d, Right 1: %d \n&quot;, GetData(clNode), GetData(crNode));

    clNode2 = GetLeftSubTree(clNode);    // l 2개 주의
    crNode2 = GetRightSubTree(clNode);    // l 2개 주의
    printf(&quot;Left 2: %d, Right 2: %d \n&quot;, GetData(clNode2), GetData(crNode2));

    clNode2 = GetLeftSubTree(crNode);    // r 2개 주의
    crNode2 = GetRightSubTree(crNode);    // r 2개 주의
    printf(&quot;Left 3: %d, Right 3: %d \n&quot;, GetData(clNode2), GetData(crNode2));

    clNode3 = GetLeftSubTree(crNode2);    // r 2개 주의
    crNode3 = GetRightSubTree(crNode2);    // r 2개 주의
    printf(&quot;Left 4: %d, Right 4: %d \n&quot;, GetData(clNode3), GetData(crNode3));</code></pre>
<p>파일들을 다 정리하면 아래와 같다.</p>
<p><strong>1) BinarySearchTree3.h</strong></p>
<pre><code class="language-c">#ifndef __BINARY_SEARCH_TREE3_H__
#define __BINARY_SEARCH_TREE3_H__

#include &quot;BinaryTree3.h&quot;

typedef BTData BSTData;

// BST의 생성 및 초기화
void BSTMakeAndInit(BTreeNode ** pRoot);

// 노드에 저장된 데이터 반환
BSTData BSTGetNodeData(BTreeNode * bst);

// BST를 대상으로 데이터 저장(노드의 생성과정 포함)
BTreeNode * BSTInsert(BTreeNode ** pRoot, BSTData data);

// BST를 대상으로 데이터 탐색
BTreeNode * BSTSearch(BTreeNode * bst, BSTData target);

// 트리에서 노드를 제거하고 제거된 노드의 주소 값 반환
BTreeNode * BSTRemove(BTreeNode ** pRoot, BSTData target);

// 이진 탐색 트리에 저장된 모든 노드의 데이터 출력
void BSTShowAll(BTreeNode * bst);

#endif</code></pre>
<p><strong>2) BinarySearchTree3.c</strong></p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;BinaryTree3.h&quot;
#include &quot;BinarySearchTree3.h&quot;
#include &quot;AVLRebalance.h&quot;

void BSTMakeAndInit(BTreeNode ** pRoot)
{
    *pRoot = NULL;
}

BSTData BSTGetNodeData(BTreeNode * bst)
{
    return GetData(bst);
}

BTreeNode * BSTInsert(BTreeNode ** pRoot, BSTData data)
{
    if(*pRoot == NULL)
    {
        *pRoot = MakeBTreeNode();
        SetData(*pRoot, data);
    }
    else if(data &lt; GetData(*pRoot))
    {
        BSTInsert(&amp;((*pRoot)-&gt;left),data);
        *pRoot = Rebalance(pRoot);
    }
    else if(data &gt; GetData(*pRoot))
    {
        BSTInsert(&amp;((*pRoot)-&gt;right),data);
        *pRoot = Rebalance(pRoot);
    }
    else
    {
        return NULL;    // 키의 중복을 허용하지 않음
    }

    return *pRoot;
}

BTreeNode * BSTSearch(BTreeNode * bst, BSTData target)
{
    BTreeNode * cNode = bst;        // current node
    BSTData cd;                     // current data

    while(cNode != NULL)
    {
        cd = GetData(cNode);

        if(target == cd)
            return cNode;
        else if(target &lt; cd)
            cNode = GetLeftSubTree(cNode);
        else
            cNode = GetRightSubTree(cNode);
    }

    return NULL;    // 탐색대상이 저장되어 있지 않음.
}

BTreeNode * BSTRemove(BTreeNode ** pRoot, BSTData target)
{
    // 삭제 대상이 루트 노드인 경우 별도로 고려
    BTreeNode * pVRoot = MakeBTreeNode();   // 가상의 루트 노드
    BTreeNode * pNode = pVRoot;             // parent node
    BTreeNode * cNode = *pRoot;             // current node
    BTreeNode * dNode;                      // delete node

    // 루트 노드를 pVRoot가 가리키는 노드의 오른쪽 자식 노드가 되게
    ChangeRightSubTree(pVRoot, *pRoot);

    // 삭제 대상인 노드를 탐색
    while(cNode != NULL &amp;&amp; GetData(cNode) != target)
    {
        pNode = cNode;

        if(target &lt; GetData(cNode))
            cNode = GetLeftSubTree(cNode);
        else
            cNode = GetRightSubTree(cNode);
    }
    // 삭제 대상이 존재하지 않다면
    if(cNode == NULL)
        return NULL;

    // 삭제 대상 dNode 가리키게
    dNode = cNode;

    // 경우 1) 삭제 대상이 단말 노드
    if(GetLeftSubTree(dNode) == NULL &amp;&amp; GetRightSubTree(dNode) == NULL)
    {
        if(GetLeftSubTree(pNode) == dNode)
            RemoveLeftSubTree(pNode);
        else
            RemoveRightSubTree(pNode);
    }

    // 경우 2) 삭제 대상이 하나의 자식 노드
    else if(GetLeftSubTree(dNode) == NULL || GetRightSubTree(dNode) == NULL)
    {
        // 삭제 대상의 자식 노드
        BTreeNode * dcNode;

        if(GetLeftSubTree(dNode) != NULL)
            dcNode = GetLeftSubTree(dNode);
        else
            dcNode = GetRightSubTree(dNode);

        if(GetLeftSubTree(pNode) == dNode)
            ChangeLeftSubTree(pNode, dcNode);
        else
            ChangeRightSubTree(pNode, dcNode);
    }

    // 경우 3) 삭제 대상이 두 개의 자식 노드
    else
    {
        // 대체 노드 가리킴
        BTreeNode * mNode = GetRightSubTree(dNode);
        // 대체 노드의 부모 노드
        BTreeNode * mpNode = dNode;
        int delData;

        // 삭제 대상의 대체 노드 찾기
        while(GetLeftSubTree(mNode) != NULL)
        {
            mpNode = mNode;
            mNode = GetLeftSubTree(mNode);
        }

        // 대체 노드에 저장된 값을 삭제 노드에 대입
        delData = GetData(dNode);
        SetData(dNode, GetData(mNode));

        // 대체 노드의 부모 노드와 자식 노드 연결
        if(GetLeftSubTree(mpNode) == mNode)
            ChangeLeftSubTree(mpNode, GetRightSubTree(mNode));
        else
            ChangeRightSubTree(mpNode, GetRightSubTree(mNode));

        dNode = mNode;
        SetData(dNode, delData);
    }

    // 삭제된 노드가 루트 노드인 경우
    if(GetRightSubTree(pVRoot) != *pRoot)
        *pRoot = GetRightSubTree(pVRoot);   // 루트 노드의 변경 반영

    free(pVRoot);   // 가상 루트 노드 소멸

    // Rebalancing 추가
    *pRoot = Rebalance(pRoot);
    return dNode;   // 삭제된 노드 반환
}

void ShowIntData(int data)
{
    printf(&quot;%d &quot;, data);
}

void BSTShowAll(BTreeNode * bst)
{   
    // 중위 순회
    InorderTraverse(bst, ShowIntData);
    printf(&quot;\n&quot;);
}</code></pre>
<p><strong>3) AVLTreeMain.c</strong></p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &quot;BinaryTree3.h&quot;
#include &quot;BinarySearchTree3.h&quot;

int main()
{
    BTreeNode * avlRoot;
    BTreeNode * clNode;         // current left node
    BTreeNode * crNode;         // current right node

    BTreeNode * clNode2;
    BTreeNode * crNode2;

    BTreeNode * clNode3;
    BTreeNode * crNode3;

    BSTMakeAndInit(&amp;avlRoot);

    BSTInsert(&amp;avlRoot, 1);
    BSTInsert(&amp;avlRoot, 2);
    BSTInsert(&amp;avlRoot, 3);
    BSTInsert(&amp;avlRoot, 4);
    BSTInsert(&amp;avlRoot, 5);
    BSTInsert(&amp;avlRoot, 6);
    BSTInsert(&amp;avlRoot, 7);
    BSTInsert(&amp;avlRoot, 8);
    BSTInsert(&amp;avlRoot, 9);

    printf(&quot;Root Node: %d \n&quot;, GetData(avlRoot));

    clNode = GetLeftSubTree(avlRoot);
    crNode = GetRightSubTree(avlRoot);
    printf(&quot;Left 1: %d, Right 1: %d \n&quot;, GetData(clNode), GetData(crNode));

    clNode2 = GetLeftSubTree(clNode);
    crNode2 = GetRightSubTree(clNode);
    printf(&quot;Left 2: %d, Right 2: %d \n&quot;, GetData(clNode2), GetData(crNode2));

    clNode2 = GetLeftSubTree(crNode);
    crNode2 = GetRightSubTree(crNode);
    printf(&quot;Left 3: %d, Right 3: %d \n&quot;, GetData(clNode2), GetData(crNode2));

    clNode3 = GetLeftSubTree(crNode2);
    crNode3 = GetRightSubTree(crNode2);
    printf(&quot;Left 4: %d, Right 4: %d \n&quot;, GetData(clNode3), GetData(crNode3));

    return 0;
}

&gt; gcc .\AVLRebalance.c .\AVLTreeMain.c .\BinarySearchTree3.c .\BinaryTree3.c
&gt; .\a.exe
&gt; 출력
Root Node: 4
Left 1: 2, Right 1: 6
Left 2: 1, Right 2: 3
Left 3: 5, Right 3: 8
Left 4: 7, Right 4: 9</code></pre>
<p>따라서 위 코드대로 작성하면 루트 노드에 5가 저장되는 것이 아닌 4로 저장되어 잘 출력되는 것을 확인할 수 있다.
그림으로 그려보면 아래와 같이 이진 트리가 생성된 것을 알 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/2bbd88c5-6d75-4d5e-83ec-1d2d13c5b609/image.png" alt=""></p>
<p>(Draw by @hyunminmax)
(그림 그려주셔서 감사합니다. 현민님 ^^)</p>
<h3 id="다른-수정_"><em>다른 수정_</em></h3>
<p>5가 루트 노드로 정렬하고 싶다면 아래와 같이 코드를 작성하면 된다.</p>
<p><strong>1) BinarySearchTree3.h</strong></p>
<pre><code class="language-c">#ifndef __BINARY_SEARCH_TREE3_H__
#define __BINARY_SEARCH_TREE3_H__

#include &quot;BinaryTree3.h&quot;

typedef BTData    BSTData;

void BSTMakeAndInit(BTreeNode ** pRoot);
BSTData BSTGetNodeData(BTreeNode * bst);
void BSTInsert(BTreeNode ** pRoot, BSTData data);
BTreeNode * BSTSearch(BTreeNode * bst, BSTData target); 
BTreeNode * BSTRemove(BTreeNode ** pRoot, BSTData target);
void BSTShowAll(BTreeNode * bst);

#endif</code></pre>
<p><strong>2) BinarySearchTree3.c</strong></p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;BinaryTree3.h&quot;
#include &quot;BinarySearchTree3.h&quot;
#include &quot;AVLRebalance.h&quot;

// 동일

void BSTInsert(BTreeNode ** pRoot, BSTData data)
{
    BTreeNode * pNode = NULL;    // parent node
    BTreeNode * cNode = *pRoot;    // current node
    BTreeNode * nNode = NULL;    // new node

    while(cNode != NULL)
    {
        if(data == GetData(cNode))
            return;

        pNode = cNode;

        if(GetData(cNode) &gt; data)
            cNode = GetLeftSubTree(cNode);
        else
            cNode = GetRightSubTree(cNode);
    }

    nNode = MakeBTreeNode();
    SetData(nNode, data);

    if(pNode != NULL)
    {
        if(data &lt; GetData(pNode))
            MakeLeftSubTree(pNode, nNode);
        else
            MakeRightSubTree(pNode, nNode);
    }
    else
    {
        *pRoot = nNode;
    }

    *pRoot = Rebalance(pRoot);
}
// 이하 동일</code></pre>
<p><strong>3) AVLTreeMain.c</strong></p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &quot;BinaryTree3.h&quot;
#include &quot;BinarySearchTree3.h&quot;

int main(void)
{
    BTreeNode * avlRoot;
    BTreeNode * clNode;        // current left node
    BTreeNode * crNode;        // current right node
    BSTMakeAndInit(&amp;avlRoot);

    BSTInsert(&amp;avlRoot, 1);
    BSTInsert(&amp;avlRoot, 2);
    BSTInsert(&amp;avlRoot, 3);
    BSTInsert(&amp;avlRoot, 4);
    BSTInsert(&amp;avlRoot, 5);
    BSTInsert(&amp;avlRoot, 6);
    BSTInsert(&amp;avlRoot, 7);
    BSTInsert(&amp;avlRoot, 8);
    BSTInsert(&amp;avlRoot, 9);

    printf(&quot;Root Node: %d \n&quot;, GetData(avlRoot));

    clNode = GetLeftSubTree(avlRoot);
    crNode = GetRightSubTree(avlRoot);
    printf(&quot;Left 1: %d, Right 1: %d \n&quot;, GetData(clNode), GetData(crNode));

    clNode = GetLeftSubTree(clNode);
    crNode = GetRightSubTree(crNode);
    printf(&quot;Left 2: %d, Right 2: %d \n&quot;, GetData(clNode), GetData(crNode));

    clNode = GetLeftSubTree(clNode);
    crNode = GetRightSubTree(crNode);
    printf(&quot;Left 3: %d, Right 3: %d \n&quot;, GetData(clNode), GetData(crNode));

    clNode = GetLeftSubTree(clNode);
    crNode = GetRightSubTree(crNode);
    printf(&quot;Left 4: %d, Right 4: %d \n&quot;, GetData(clNode), GetData(crNode));
    return 0;
}</code></pre>
<hr>
<h2 id="review"><code>&lt;Review&gt;</code></h2>
<p>마지막에 구현이 잘 되었는지 확인하는 실행파일에서 오류가 계속 나서 애를 먹었다.</p>
<pre><code>&gt; gcc .\AVLRebalance.c .\AVLRebalanceMain.c .\BinarySearchTree3.c .\BinaryTree3.c
.\BinarySearchTree3.c: In function &#39;BSTInsert&#39;:
.\BinarySearchTree3.c:36:13: warning: &#39;return&#39; with a value, in function returning void
             ^~~~
.\BinarySearchTree3.c:17:6: note: declared here
 void BSTInsert(BTreeNode ** pRoot, BSTData data)
      ^~~~~~~~~
.\BinarySearchTree3.c:39:12: warning: &#39;return&#39; with a value, in function returning void
     return *pRoot;
            ^~~~~~
.\BinarySearchTree3.c:17:6: note: declared here
 void BSTInsert(BTreeNode ** pRoot, BSTData data)
      ^~~~~~~~~</code></pre><p>void로 반환 되는 형을 정해놨기 때문에 *pRoot가 안된다는 것이다.</p>
<p>위에 정리처럼 스터디를 하면서 4명의 지성을 모아 문제를 해결했다...</p>
<p>다행히 내 오타로 끝까지 작동이 안한거였지만 답답했다...ㅎㅎ
GPT한테도 물어봤을 때 고치지 못했던 부분이었기 때문이다...
아무튼! 잘 해결되서 다행이다...!</p>
<p>(이 날_12/3 밤 11시에 윤대통령님께서 비상 계엄을 선포해서 경과를 지켜보느라 잠을 4시간 밖에 못잤다 아주 화가 난다.)</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/e48ceea2-30d2-4890-9561-b3a8d726c8e3/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chapter 11. 탐색(Search) 1]]></title>
            <link>https://velog.io/@mingming_eee/datastructure-11</link>
            <guid>https://velog.io/@mingming_eee/datastructure-11</guid>
            <pubDate>Tue, 26 Nov 2024 08:29:26 GMT</pubDate>
            <description><![CDATA[<h2 id="11-1-탐색의-이해와-보간-탐색">11-1. 탐색의 이해와 보간 탐색</h2>
<h3 id="탐색의-이해"><em>탐색의 이해</em></h3>
<p>탐색(Search)이란, 데이터를 찾는 방법이다.
앞에서 배운 &#39;순차 탐색(linear search)&#39;이나 &#39;이진 탐색(binary search)&#39;와 같은 탐색 알고리즘을 주로 배우는 것이 아닌 이번 Chapter에서는 Chapter 08에서 배운 트리의 연장선이라 볼 수 있다.
따라서 굳이 따지자면 탐색은 알고리즘이라기 보단 자료구조에 더 가까운 주제다.
효율적인 탐색을 위해서는 &#39;어떻게 찾을까&#39;만 생각하는 것이 아닌 &#39;효율적인 탐색을 위한 저장방법이 무엇일까&#39;에 대해 우선 고민해야한다.
그리고 이 효율적인 탐색이 가능한 대표적인 저장 방법은 &#39;트리&#39;다.</p>
<p>이 다음 Chapter에서는 &#39;테이블과 해쉬&#39;에 관해 배우게 되는데 이도 탐색과 관련이 있는 내용이다.
앞에서 배운 정렬도 탐색을 목적으로 하는 경우가 대부분인 만큼 탐색은 자료구조에서 (더 넓게는 컴퓨터 공학에서) 매우 중요한 부분을 차지하고 있다.</p>
<h3 id="보간-탐색-interpolation-search"><em>보간 탐색 (Interpolation Search)</em></h3>
<p>&#39;보간 탐색(interpolation search)&#39;은 앞에서 배운 순차 탐색(정렬되지 않은 대상을 기반으로 하는 탐색)과 이진 탐색(정렬된 대상을 기반으로 하는 탐색) 중에서 이진 탐색의 비효율성을 개선시킨 알고리즘이다.
이진 탐색은 중앙에 위치한 데이터를 탐색한 후 이를 기준으로 탐색 탐색을 진행하는 알고리즘이다.
찾는 대상이 중앙에 위치하든 맨 앞에 위치하던 상관하지 않고 일관되게 반씩 줄여가면서 탐색을 진행해 나간다.
때문에 찾는 대상의 위치에 따라서 탐색의 효율에 차이가 발생했다.</p>
<p>보간 탐색은 이진 탐색처럼 그냥 중앙에서 탐색을 시작하는 것이 아닌 탐색 대상이 앞쪽에 위치해 있으면 앞쪽에서 탐색을 시작하는 것이다.
예를 들어 오른차순으로 정렬된 배열을 대상으로 앞쪽에 위치한 데이터를 찾고자 할 때 이진 탐색과 보간 탐색의 첫 번째 탐색 위치는 다음과 같이 차이가 난다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/bf6fe5e7-7fb0-4f86-851e-90b4c9e27184/image.png" alt=""></p>
<p>위 그림은 정수 12를 찾을 때의 첫 번째 탐색위치를 보여준다.
이진 탐색은 값에 상관없이 탐색위치를 결정하지만 보간 탐색은 그 값이 상대적으로 앞에 위치한다고 판단하면 앞쪽에서 탐색을 진행한다.
따라서 &#39;데이터&#39;와 데이터가 저장된 위치의 &#39;인덱스 값&#39;이 직선의 형태로 비례하면(선형의 형태로 비례하면), 보간 탐색의 경우 단번에 데이터를 찾기도 한다.
단번에 찾지 못하더라도 탐색의 위치가 찾는 데이터와 가깝기 때문에 탐색 대상을 줄이는 속도가 이진 탐색보다 뛰어나다.</p>
<p>이제 고민해야하는 것은 보간 탐색의 탐색위치를 결정하는 방법이다.
이는 수학적으로 접근할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/11f03cfd-d80a-4822-b79b-bedd2a2fdf02/image.png" alt=""></p>
<p>위 그림에서 $$low$$와 $$high$$는 탐색대상의 시작과 끝에 해당하는 인덱스 값이고, $$s$$는 찾는 데이터가 저장된 위치의 인덱스 값이다.
그런데 보간 탐색은 데이터의 값과 그 데이터가 저장된 위치의 인덱스 값이 비례한다고 가정하기 때문에 위 그림을 근거로 다음의 비례식을 구성한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/73a42c11-878e-42b8-80e5-3ec7d79fc318/image.png" alt=""></p>
<p>이 식에서 $$s$$는 찾고자 하는 데이터의 인덱스 값이므로 위의 비례식을 $$s$$에 대한 식으로 다음과 같이 정리할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/c09cc2f0-13a2-4faf-b98c-cee45fe40f58/image.png" alt=""></p>
<p>그리고 찾는 데이터의 값 $$arr[s]$$를 $$x$$라 하면 위의 식은 최종적으로 다음과 같이 정리가 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/6ecdaf50-d851-4814-8b86-c88a6fdd9409/image.png" alt=""></p>
<p>이로써 찾는 값을 $$x$$에 삽입하여 탐색위치 $$s$$를 구하는 식을 얻을 수 있다.
이 식은 단순하지만 나눗셈 연산이 들어가고 오차율을 최소화하기 위해서 정수형 나눗셈이 아닌 실수형 나눗셈을 한다는 것에 주목해야 한다.
이는 보간 탐색의 단점이기 때문이다.</p>
<h3 id="탐색-키-search-key와-탐색-데이터-search-data"><em>탐색 키 (Search Key)와 탐색 데이터 (Search Data)</em></h3>
<p>위에서 설명한 보간탐색의 원리를 구현할 차례다.
보간 탐색의 구현에 앞서 구조체 하나를 살펴보자.</p>
<pre><code class="language-c">typedef int key;        // 탐색 티에 대한 typedef 선언
typedef double Data;    // 탐색 데이터에 대한 typedef 선언

typedef struct item
{
    key searchKey;        // 탐색 키 (search key)
    Data searchData;     // 탐색 데이터 (search data)
} Item;</code></pre>
<p>여기서 정의된 구조체 Item의 멤버는 &#39;탐색 키&#39;와 &#39;탐색 데이터&#39;로 이루어져 있다.
예를 들어 &#39;사번이 7인 직원의 정보를 찾는다&#39;고 했을 때 사번이 &#39;탐색 키 (search key)&#39;가 디고 직원의 정보가 &#39;탐색 데이터 (search data)&#39;가 된다.
때문에 일반적인 상황에서는 위의 구조체 정의에서 보이듯이 탐색 키와 탐색 데이터를 묶는 형태의 구조체를 정의하게 되고, 정렬이나 탐색이나 그 탐색의 대상을 탐색 키에 맞추게 된다.
실제 프로그램 개발에 있어서 탐색의 대상은 데이터가 아닌 키이고 학습의 편의를 위해 여태까지는 데이터를 찾는 형태로 배웠을 뿐이다.</p>
<p>그리고 중요하게 기억해야 하는 부분은 &quot;탐색 키는 그 값이 고유해야 한다&quot;는 것이다.
키에는 그 값이 유일하다는 의미가 담겨 있고 NULL과 같은 값이 채워질 수 없다는 의미도 담겨있다.</p>
<h3 id="보간-탐색의-구현"><em>보간 탐색의 구현</em></h3>
<p>이론적으로 보간 탐색과 이진 탐색의 유일한 차이점은 탐색의 대상을 선정하는 방법에 있다.
따라서 보간 탐색 구현을 위해 이진 탐색을 다시 한번 살펴보자.
(이진 탐색이 잘 기억 나지 않는다면 <a href="https://velog.io/@mingming_eee/datastructure-02#%EC%9D%B4%EC%A7%84-%ED%83%90%EC%83%89-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EC%9D%98-%EC%9E%AC%EA%B7%80%EC%A0%81-%ED%91%9C%ED%98%84">Chapter 02. 재귀(Recursion)</a>글을 다시 한번 읽어 보자.😉)</p>
<pre><code class="language-c">// RecursiveBinarySearch.c
#include &lt;stdio.h&gt;

int BSearchRecur(int ar[], int first, int last, int target)
{
    int mid;

    if(first &gt; last)
        return -1;            // -1의 반환 = 탐색의 실패

    mid = (first+last)/2;    // 탐색 대상의 중앙 찾기.
    if(ar[mid]==target)
        return mid;            // 탐색된 타겟의 인덱스 값 반환.
    else if(target &lt; ar[mid])
        return BSearchRecur(ar, first, mid-1, target);
    else
        return BSearchRecur(ar, mid+1, last, target);
}

int main()
{
    ....
    return 0;
}</code></pre>
<p>BSearchRecur 함수는 <code>mid = (first + last) / 2;</code> 문장을 통해서 탐색의 위치를 계산한다.
그런데 이진 탐색과 보간 탐색의 유일한 차이점은 탐색의 대상을 선택하는 방법에 있으니 이를 아까 구한 찾고자 하는 데이터의 인덱스 값 구하는 정리로 바꾸면 보간 탐색의 구현이 완료가 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/30b99cda-8389-480d-8e29-03ad30cf4a4c/image.png" alt=""></p>
<p>위의 문장에서는 나눗셈의 피연산자를 double 형으로 형 변환하여 모든 연산들이 실수형으로 진행되어 오차를 최소화하도록 했다.</p>
<p>그럼 완성된 보간 탐색 구현 코드를 살펴보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int ISearch(int ar[],  int first, int last, int target)
{
    int mid;

    if(first &gt; last)
        return -1;            // -1의 반환 = 탐색의 실패

    // 보간 탐색의 탐색 대상 선택 방법
    mid = ((double)(target-ar[first]) / (ar[last]-ar[first]) * (last-first)) + first;

    if(ar[mid]==target)
        return mid;            // 탐색된 타겟의 인덱스 값 반환.
    else if(target &lt; ar[mid])
        return ISearch(ar, first, mid-1, target);
    else
        return ISearch(ar, mid+1, last, target);
}

int main()
{
    int arr[] = {1, 3, 5, 7, 9};
    int idx;

    idx = ISearch(arr, 0, sizeof(arr)/sizeof(int)-1, 7);
    if(idx==-1)
        printf(&quot;search fail. \n&quot;);
    else
        printf(&quot;target index: %d \n&quot;, idx);

    idx = ISearch(arr, 0, sizeof(arr)/sizeof(int)-1, 10);
    if(idx==-1)
        printf(&quot;search fail. \n&quot;);
    else
        printf(&quot;target index: %d \n&quot;, idx);

    return 0;
}

&gt; 출력
target index: 3 
search fail.</code></pre>
<p>여기서 만약에 배열에 저장되어 있지 않은 2를 찾으려고 하면 main 함수를 대상으로 ISearch 함수가 제대로 동작하지 않는다.
문제는 ISearch 함수의 <code>if(first &gt; last) return -1;</code>이라는 탈출조건을 만족시키지 못해서 발생한 오류다.</p>
<p>따라서 오류를 해결하기 위해서 함수의 탈출조건을 <code>if(ar[first]&gt;target || ar[last]&lt;target) return -1;</code>로 변경해야 한다.
탐색대상이 존재하지 않는 경우 ISearch 함수가 재귀적으로 호출됨에 따라 target에 저장된 값은 first와 last가 가리키는 값의 범위를 넘어서게 된다.
정렬된 탐색대상의 범위를 좁혀가면서 정렬을 진행하기 때문에 당연히 발생하는 현상이다.
따라서 이러한 특성을 기반으로 변경된 탈출조건과 같이 구성해야 한다.</p>
<h3 id="탈출-조건을-만족하지-않는-이유"><em>탈출 조건을 만족하지 않는 이유</em></h3>
<p>우선, 탈출 조건이 만족되지 않는 상황에 대해서 알아보자.
다음과 같이 ISearch 함수가 호출되었다고 가정해보자.</p>
<pre><code class="language-c">int main()
{
    int arr[] = {1, 3, 5, 7, 9};
    ....
    ISearch(arr, 1, 4, 2);    // 배열 arr의 인덱스 1~4 범위 내에서 2 탐색
    ....
}</code></pre>
<p>위와 같이 ISearch 함수가 호출되는 상황을 인위적으로 만들면 탐색위치를 계산하는 다음 문장에 값을 대입해 봤을 때 mid에 0이 저장됨을 알 수 있다.
인덱스 1~4의 범위 내에서 최댓값과 최솟값은 각각 3과 9다.
그런데 타겟은 2다.
즉, 그 값이 탐색 범위 내에 위치하지 못할 만큼 작다.
그러니 가장 왼쪽에 있는 값과 비교하라는 의미로 0을 반환하는 것이 맞지 않을까?
하지만 이 결과가 다음에 오는 코드들과 결합되면서 문제가 된다.</p>
<pre><code class="language-c">if(ar[mid]==target)
       return mid;            // 탐색된 타겟의 인덱스 값 반환.
else if(target &lt; ar[mid])
       return ISearch(ar, first, mid-1, target);
else
       return ISearch(ar, mid+1, last, target);    // 이 문장이 실행됨.</code></pre>
<p>조건에 의해서 마지막 else 구문의 ISearch 함수 호출문이 실행이 되면서 무한루프에 빠지게 된다.
그래서 아까 변경한 조건과 같이 &#39;탐색 대상이 존재하지 않을 경우, 탐색 대상의 값은 탐색 범위의 값을 넘어선다&#39;라고 수정해준 것이다.</p>
<hr>
<h2 id="11-2-이진-탐색-트리">11-2. 이진 탐색 트리</h2>
<p>이번에 배울 이진 탐색 트리는 앞에서 배운 이진 트리 바탕의 탐색에 효율적인 자료구조다.</p>
<h3 id="이진-탐색-트리의-이해"><em>이진 탐색 트리의 이해</em></h3>
<p>이진 트리의 구조를 보면 저장된 데이터의 수가 10억 개가 된다고 해도 트리의 높이는 30을 넘지 않는다는 것을 보아 탐색에 있어 효율적이라는 것을 금방 알 수 있다.
즉, 데이터에 이르는 길을 알고 있다면 루트 노드에서부터 단말 노드에 이르기까지 총 30개 노드를 지나는 과정에서 원하는 데이터를 찾을 수 있다는 것이다.
따라서 이진 트리는 단말 노드에 이르는 길의 갈래가 매우 많기 때문에 찾는 데이터가 존재하는 제대로 된 길을 선택할 수 있어야 한다.</p>
<p>이진 탐색 트리에는 데이터를 저장하는 규칙이 있어 그 규칙은 특정 데이터의 위치를 찾는데 사용할 수 있다.
쉽게 말해서 이진 트리에 데이터 저장 규칙을 더해놓은 것이 이진 탐색 트리다.
이진 탐색 트리에는 몇 가지 조건이 있는데 참고로 &#39;탐색 키&#39;는 정수라 가정하였고 이러한 탐색 키를 간단하게 키라고 언급했다.</p>
<ul>
<li>이진 탐색 트리의 노드에 저장된 키는 유일하다.</li>
<li>루트 노드의 키가 왼쪽 서브 트리를 구성하는 어떠한 노드의 키보다 크다.</li>
<li>루트 노드의 키가 오른쪽 서브 트리를 구성하는 어떠한 노드의 키보다 작다.</li>
<li>왼쪽과 오른쪽 서브 트리도 이진 탐색 트리다.</li>
</ul>
<p>이러한 조건들을 만족하는 트리의 예시를 하나 보자.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/702aa57f-80db-461f-aa8e-2448481507c1/image.png" alt=""></p>
<p>위 그림에서 볼 수 있듯 루트 노드의 왼쪽 서브 트리에 저장된 값들은 12보다 작고,
오른쪽 서브 트리에 저장된 값들은 12보다 크다. <code>(왼쪽 자식 노드의 키 &lt; 부모 노드의 키 &lt; 오른쪽 자식 노드의 키)</code>
그리고 이 수식이 이진 탐색 트리의 구현에 더 직접적으로 도움이 되는 결론이다.
예를 들어 위 이진 탐색 트리를 대상으로 숫자 10을 저장한다고 가정하자.
그러면 루트 노드를 시작으로 다음의 과정을 거쳐 저장될 위치를 결정하게 된다.</p>
<ol>
<li>10 &lt; 12 : 왼쪽 자식노드로 이동</li>
<li>10 &gt; 8  : 오른쪽 자식 노드로 이동</li>
<li>10 &gt; 9  : 오른쪽 자식 노드로 이동</li>
<li>아무것도 없으니 그 위치에 저장.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/29e8d628-8df8-42da-95e1-7b2e2cb05a2e/image.png" alt=""></p>
<p>따라서 위 그림과 같이 10이 위치하게 된다.
이진 탐색 트리는 작으면 왼쪽, 크면 오른쪽으로 라는 원칙을 기준으로 데이터를 삽입하고 탐색한다.</p>
<h3 id="이진-탐색-트리의-구현"><em>이진 탐색 트리의 구현</em></h3>
<p>이진 탐색 트리를 구현할 수 있는 방법에는 2가지가 있다.</p>
<ol>
<li>이전에 구현한 이진 트리를 참조하여 처음부터 완전히 다시 구현.</li>
<li>이전에 구현한 이진 트리를 활용하여 구현.</li>
</ol>
<p>우선 첫 번째 방법을 이용하면, 이전에 공부한 이진 트리의 이해 없이 코드를 바로 분석할 수 있다. 그리고 이전에 경험했던 이진 트리를 코드 레벨에서 다시 한번 복습할 수 있다.
(이것들이 과연 장점일까...?😂)</p>
<p>두 번째 방법을 이용하면, 이진 탐색 트리가 이진 트리의 확장이라는 사실을 코드 레벨에서 확인할 수 있다.
앞서 구현한 이진 탐색 트리를 점검하는 기회가 되며, 이진 트리의 ADT를 통해서 정의한 함수들이 기능적으로 부족한 부분에 있어 코드 리펙토링할 수 있다.
이진 트리를 활용한 이진 탐색 트리의 구현을 경험하는 것은 좋은 프로그래밍 모델을 경험하는 기회도 된다.</p>
<p>사실 이전에도 이진 트리를 활용하여 이진 트리의 일종인 &#39;수식 트리&#39;를 구현한 경험이 있다.
그때 활용한 방법을 이번에도 적용하여 이진 탐색 트리를 구현해볼 것이다.</p>
<p>그러기 위해서는 이전에 작성한 파일 2개가 필요하다.</p>
<ul>
<li><a href="https://velog.io/@mingming_eee/datastructure-08#%EB%85%B8%EB%93%9C%EC%9D%98-%EB%B0%A9%EB%AC%B8-%EC%9D%B4%EC%9C%A0">BinaryTree2.h</a>    : 이진 트리 정의한 헤더파일</li>
<li><a href="https://velog.io/@mingming_eee/datastructure-08#%EB%85%B8%EB%93%9C%EC%9D%98-%EB%B0%A9%EB%AC%B8-%EC%9D%B4%EC%9C%A0">BinaryTree2.c</a>    : 이진 트리 구현한 소스파일</li>
</ul>
<p>헤더파일(BinaryTree2.h) 선언된 함수들에 대해서도 다시 한번 간단하게 리마인드 해보자.</p>
<ul>
<li><code>BTreeNode * MakeBTreeNode(void);</code>
: 노드를 동적으로 할당해서 그 노드의 주소 값을 반환</li>
<li><code>BTData GetData(BTreeNode * bt);</code>
: 노드에 저장된 데이터를 반환</li>
<li><code>void SetData(BTreeNode * bt, BTData data);</code>
: 인자로 전달된 데이터를 노드에 저장</li>
<li><code>BTreeNode * GetLeftSubTree(BTreeNode * bt);</code>
: 인자로 전달된 노드의 왼쪽 자식 노드의 주소 값을 반환</li>
<li><code>BTreeNode * GetRightSubTree(BTreeNode * bt);</code>
: 인자로 전달된 노드의 오른쪽 자식 노드의 주소 값을 반환</li>
<li><code>void MakeLeftSubTree(BTreeNode * main, BTreeNode * sub);</code>
: 인자로 전달된 노드의 왼쪽 자식 노드를 교체</li>
<li><code>void MakeRightSubTree(BTreeNode * main, BTreeNode * sub);</code>
: 인자로 전달된 노드의 오른쪽 자식 노드를 교체</li>
</ul>
<p>이제 이 함수들을 이용해서 이진 트리의 일종인 이진 탐색 트리를 구현해보자.</p>
<h4 id="1-헤더파일-binarysearchtreeh">1) 헤더파일 (BinarySearchTree.h)</h4>
<pre><code class="language-c">#ifndef __BINARY_SEARCH_TREE_H__
#define __BINARY_SEARCH_TREE_H__

#include &quot;BinaryTree2.h&quot;

typedef BTData BSTData;

// BST의 생성 및 초기화
void BSTMakeAndInit(BTreeNode ** pRoot);

// 노드에 저장된 데이터 반환
BSTData BSTGetNodeData(BTreeNode * bst);

// BST를 대상으로 데이터 저장(노드의 생성과정 포함)
void BSTInsert(BTreeNode ** pRoot, BSTData data);

// BST를 대상으로 데이터 탐색
BTreeNode * BSTSearch(BTreeNode * bst, BSTData target);

#endif</code></pre>
<p>이진 탐색 트리의 핵심 연산 세 가지는 다른 자료구조들과 마찬가지로 삽입, 삭제, 그리고 탐색이다.
그런데 삭제는 별도로 고민해야 할 문제이기 때문에 우선 삽입과 탐색에 대한 함수를 각각 정의했다.</p>
<ul>
<li>삽입 : <code>void BSTInsert(BTreeNode ** pRoot, BSTData data) {...}</code></li>
<li>탐색 : <code>BTreeNode * BSTSearch(BTreeNode * bst, BSTData target) {...}</code></li>
</ul>
<p>간단한 main 함수를 통해서 위 두 함수와 헤더파일에 선언된 또 다른 두 함수의 사용방법을 살펴보자. (실제로 실행파일을 만들진 않을 것이다. 실제 실행파일은 소스파일 작성 이후 만들 예정.)</p>
<pre><code class="language-c">int main(void)
{
    BTreeNode * bstRoot;        // bstRoot는 BST의 루트 노드를 가리킨다.
    BTreeNode * sNode;

    BSTMakeAndInit(&amp;bstRoot);    // Binary Search Tree의 생성 및 초기화

    BSTInsert(&amp;bstRoot, 1);        // bstRoot에 1을 저장
    BSTInsert(&amp;bstRoot, 2);        // bstRoot에 2을 저장
    BSTInsert(&amp;bstRoot, 3);        // bstRoot에 3을 저장

    // 1 탐색
    sNode = BSTSearch(bstRoot, 1);
    if(sNode == NULL)
        printf(&quot;search fail. \n&quot;);
    else
        printf(&quot;key value: %d \n&quot;, BSTGetNodeData(sNode));

    return 0;
}</code></pre>
<p>위 main 함수에서 보이듯이 다음과 같이 BSTMakeAndInit 함수가 호출되고 나면, 이진 탐색 트리의 생성 및 초기화가 완료된 것으로 간주한다.
그때부터 bstRoot는 생성된 이진 탐색 트리를 지칭하는 이름이 된다.
그래서 트리에 데이터를 추가할 때는 다음과 같이 <code>BSTInsert(&amp;bstRoot, 1);</code> 함수를 호출한다.
이진 탐색 트리에 데이터를 저장하기 위해서 직접 노드를 생성할 필요가 없다.
노드의 생성과 위치 선정 및 저장은 BSTInsert 함수 내에서 이뤄지기 때문이다.
탐색도 유사한 방법으로 진행된다.
<code>sNode = BSTSearch(bstRoot, 1);</code> 문장이 실행되고 탐색의 결과, 목표한 대상을 찾았다면 이를 저장하고 있는 노드의 주소 값이 반환된다.
그리고 main 함수에서 보였듯이 BSTGetNodeData 함수를 통해서 노드에 저장된 값을 얻을 수도 있다.</p>
<h4 id="2-소스파일">2) 소스파일</h4>
<p><strong>[1] 삽입과 탐색 (BinarySearchTree.c)</strong></p>
<p>이진 탐색 트리에서 삽입과 탐색은 어렵지 않다.
그래도 몇몇 사례를 통해서 그 과정을 살펴보자.
먼저 세 개의 노드로 구성된 트리에 숫자 11과 10의 추가과정을 순서대로 보자.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/ea51e21d-8a2a-4315-89d4-1217f919646b/image.png" alt=""></p>
<p>비교대상보다 값이 작으면 왼쪽 자식 노드로, 값이 크면 오른쪽 자식 노드로 이동하는데 여기서 추가로 알아야 할 점이 &quot;비교대상이 없을 때까지 내려가고, 비교대상이 없는 그 위치가 새 데이터가 저장될 위치다.&quot;라는 것이다.</p>
<p>위 그림에 이어서 29와 22를 저장하는 예를 살펴보자.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/3769702d-bd79-45ac-bad3-41388612a2d4/image.png" alt=""></p>
<p>여기서도 동일한 규칙이 적용되어 데이터들이 저장되는 것을 확인할 수 있다.</p>
<p>이제 새 데이터의 저장을 담당하는 BSTInsert 함수에 대해 알아보자.</p>
<pre><code class="language-c">void BSTInsert(BTreeNode ** pRoot, BSTData data)
{
    BTreeNode * pNode = NULL;       // parent node
    BTreeNode * cNode = *pRoot;     // current node
    BTreeNode * dNode = NULL;       // new node

    // 새로운 노드(새 데이터 담긴 노드)가 추가될 위치 찾기
    while(cNode != NULL)
    {
        if(data == GetData(cNode))
            return;     // already inserted

        pNode = cNode;

        if(GetData(cNode) &gt; data)
            cNode = GetLeftSubTree(cNode);
        else
            cNode = GetRightSubTree(cNode);
    }

    // pNode의 자식 노드로 추가할 새 노드의 생성
    nNode = MakeBTreeNode();        // 새 노드 생성
    SetData(nNode, data);           // 새 노드에 데이터 저장

    // pNode의 자식 노드로 새 노드를 추가
    if(pNode != NULL)               // 새 노드가 루트 노드가 아니라면
    {
        if(data &lt; GetData(nNode))
            MakeLeftSubTree(pNode, nNode);
        else
            MakeRightSubTree(pNode, nNode);
    }
    else                            // 새 노드가 루트 노드라면
    {
        *pRoot = nNode;
    }
}</code></pre>
<p>여기서 while문의 역할은 저장할 값의 크기에 따라 왼쪽 또는 오른쪽 자식 노드로 이동하면서 새 노드의 저장위치를 찾는 것이다.</p>
<p>따라서 이 while문을 빠져나오면 cNode에는 새 노드가 저장될 위치정보가 담긴다.
그런데 이 위치에 노드를 저장하기 위해 필요한 것은 이 위치를 자식으로 하는 부모 노드의 주소 값이다. (부모 노드에 자식 노드의 주소 값이 저장되기 때문이다.)
실제로 이어지는 if~else 구문에서 부모 노드의 주소 값이 담긴 pNode를 기반으로 자식 노드가 추가되고 있다.</p>
<p>이어서 탐색을 담당하는 BSTSearch 함수를 보자.
탐색의 과정은 삽입의 과정을 근거로 하기에 별도의 설명이 필요 없을 것이다.</p>
<pre><code class="language-c">BTreeNode * BSTSearch(BTreeNode * bst, BSTData target)
{
    BTreeNode * cNode = bst;        // current node
    BSTData cd;                     // current data

    while(cNode != NULL)
    {
        cd = GetData(cNode);

        if(target == cd)
            return cNode;
        else if(target &lt; cd)
            cNode = GetLeftSubTree(cNode);
        else
            cNode = GetRightSubTree(cNode);
    }

    return NULL;    // 탐색대상이 저장되어 있지 않음.
}</code></pre>
<p>위 함수에 삽입된 while문에서도 비교대상의 노드보다 값이 작으면 왼쪽 자식 노드로, 값이 크면 오른쪽 자식 노드로 이동하고 있다.
그럼에도 불구하고 찾는 데이터가 없으면 NULL을 반환하도록 했다.</p>
<p>위 내용과 BST를 초기화 하는 함수, 데이터를 가져오는 함수 등을 추가로 소스파일을 정리하면 아래와 같다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &quot;BinaryTree2.h&quot;
#include &quot;BinarySearchTree.h&quot;

void BSTMakeAndInit(BTreeNode ** pRoot)
{
    *pRoot = NULL;
}

BSTData BSTGetNodeData(BTreeNode * bst)
{
    return GetData(bst);
}

void BSTInsert(BTreeNode ** pRoot, BSTData data)
{
    BTreeNode * pNode = NULL;       // parent node
    BTreeNode * cNode = *pRoot;     // current node
    BTreeNode * nNode = NULL;       // new node

    // 새로운 노드(새 데이터 담긴 노드)가 추가될 위치 찾기
    while(cNode != NULL)
    {
        if(data == GetData(cNode))
            return;     // already inserted

        pNode = cNode;

        if(GetData(cNode) &gt; data)
            cNode = GetLeftSubTree(cNode);
        else
            cNode = GetRightSubTree(cNode);
    }

    // pNode의 자식 노드로 추가할 새 노드의 생성
    nNode = MakeBTreeNode();        // 새 노드 생성
    SetData(nNode, data);           // 새 노드에 데이터 저장

    // pNode의 자식 노드로 새 노드를 추가
    if(pNode != NULL)               // 새 노드가 루트 노드가 아니라면
    {
        if(data &lt; GetData(pNode))
            MakeLeftSubTree(pNode, nNode);
        else
            MakeRightSubTree(pNode, nNode);
    }
    else                            // 새 노드가 루트 노드라면
    {
        *pRoot = nNode;
    }
}

BTreeNode * BSTSearch(BTreeNode * bst, BSTData target)
{
    BTreeNode * cNode = bst;        // current node
    BSTData cd;                     // current data

    while(cNode != NULL)
    {
        cd = GetData(cNode);

        if(target == cd)
            return cNode;
        else if(target &lt; cd)
            cNode = GetLeftSubTree(cNode);
        else
            cNode = GetRightSubTree(cNode);
    }

    return NULL;    // 탐색대상이 저장되어 있지 않음.
}</code></pre>
<p><strong>- 실행파일 (BinarySearchTreeMain.c)</strong></p>
<p>위 함수들이 제대로 잘 작동하는지 확인하기 위한 실행파일은 다음과 같다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &quot;BinarySearchTree.h&quot;

int main()
{
    BTreeNode * bstRoot;
    BTreeNode * sNode;

    BSTMakeAndInit(&amp;bstRoot);

    BSTInsert(&amp;bstRoot, 9);
    BSTInsert(&amp;bstRoot, 1);
    BSTInsert(&amp;bstRoot, 6);
    BSTInsert(&amp;bstRoot, 2);
    BSTInsert(&amp;bstRoot, 8);
    BSTInsert(&amp;bstRoot, 3);
    BSTInsert(&amp;bstRoot, 5);

    sNode = BSTSearch(bstRoot, 1);
    if(sNode == NULL)
        printf(&quot;search failed.\n&quot;);
    else
        printf(&quot;key data : %d\n&quot;, BSTGetNodeData(sNode));

    sNode = BSTSearch(bstRoot, 4);
    if(sNode == NULL)
        printf(&quot;search failed.\n&quot;);
    else
        printf(&quot;key data : %d\n&quot;, BSTGetNodeData(sNode));

    sNode = BSTSearch(bstRoot, 6);
    if(sNode == NULL)
        printf(&quot;search failed.\n&quot;);
    else
        printf(&quot;key data : %d\n&quot;, BSTGetNodeData(sNode));

    sNode = BSTSearch(bstRoot, 7);
    if(sNode == NULL)
        printf(&quot;search failed.\n&quot;);
    else
        printf(&quot;key data : %d\n&quot;, BSTGetNodeData(sNode));

    return 0;
}

&gt; gcc .\BinarySearchTree.c .\BinarySearchTreeMain.c .\BinaryTree2.c
&gt; .\a.exe
&gt; 출력
key data : 1
search failed.
key data : 6
search failed.</code></pre>
<p>위 main 함수에서는 이진 탐색 트리에 다수의 값을 저장한 다음에 그 값의 저장유무를 확인하는 정도에서 마무리했다.
다음에 배울 내용은 트리의 순회를 이용해서 트리에 저장된 값을 전반적으로 확인해 볼 것이다.</p>
<p><strong>[2] 삭제 1 : 이론적 설명과 일부 구현</strong></p>
<p>이진 탐색 트리의 삭제는 단순하지 않은데 그 과정이 복잡한 이유 대해서 알아보자.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/7afc00e4-eb97-41a5-b98e-f5d856b9b8d1/image.png" alt=""></p>
<p>위 이진 탐색 트리에서 8이 담긴 노드를 삭제한다고 가정해보자.
이 상황에서 그냥 이 노드만 삭제하면 그만일까?
그 자리를 누군가 대신 채워줘야만 이진 탐색 트리가 유지된다.
즉, 이진 탐색 트리에서 임의의 노드를 삭제하는 경우, 삭제 후에도 이진 탐색 트리가 유지되도록 빈자리를 채워야되기 때문에 어려운 것이다.</p>
<p>물론 모든 삭제의 경우에 있어서 이 문제를 고민해야 하는 것은 아니다.
삭제 대상이 단말 노드라면 간단하지만, 단말노드가 아니라면 무엇으로 대신할지 고민해야 한다.
이진 탐색 트리의 삭제에 대한 경우의 수는 다음과 같다.</p>
<ul>
<li>상황 1) 삭제할 노드가 단말 노드인 경우</li>
<li>상황 2) 삭제할 노드가 하나의 자식 노드(하나의 서브 트리)를 갖는 경우</li>
<li>상황 3) 삭제할 노드가 두 개의 자식 노드(두 개의 서브 트리)를 갖는 경우</li>
</ul>
<p>이렇게 삭제에 대한 경우의 수는 세 가지다.
하지만 구현방법에 따라서 삭제 대상이 루트 노드인 경우와 그렇지 않은 경우를 나눠야 하기 때문에 삭제에 대한 경우의 수는 최대 여섯 가지로 구분할 수도 있다.</p>
<p>이제 상황별 삭제 방법을 그림을 통해 이해해보자.</p>
<p><strong>- 상황 1) 삭제할 노드가 단말 노드인 경우</strong></p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/401be6d8-df40-407b-a5ff-dd74c424f196/image.png" alt=""></p>
<p>삭제 대상인 단말 노드를 삭제하는 것으로 삭제 과정이 완료되기 때문에 비교적 간단하며 코드로 구현하면 다음과 같다.</p>
<pre><code class="language-c">// dNode와 pNode는 각각 삭제할 노드와 이의 부모 노드를 가리키는 포인터 변수
if(삭제할 노드가 단말 노드)
{
    if(GetLeftSubTree(pNode) == dNode)    // 삭제할 노드가 왼쪽 자식 노드라면
        RemoveLeftSubTree(pNode);        // 왼쪽 자식 노드 트리에서 제거
    else
        RemoveRightSubTree(pNode);        // 오른쪽 자식 노드 트리에서 제거
}</code></pre>
<p>위에서 호출한 Remove~ 로 시작하는 두 함수는 아직 정의한 바 없는 함수들인데,
이들에 대해서는 잠시 후 선언 및 정의할 예정이고 각각의 함수호출이 의미하는 바는 다음과 같다.</p>
<ul>
<li>RemoveLeftSubTree(pNode) : pNode가 가리키는 노드의 왼쪽 자식 노드 트리에서 제거</li>
<li>RemoveRightSubTree(pNode)    : pNode가 가리키는 노드의 오른쪽 자식 노드 트리에서 제거</li>
</ul>
<p><strong>- 상황 2) 삭제할 노드가 하나의 자식 노드(하나의 서브 트리)를 갖는 경우</strong></p>
<p>이 상황 역시 비교적 쉽다.
상황 1에서 부모 노드와 자식 노드를 연결하는 작업만 추가하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/72c354bc-95c2-4156-bbc3-ab05099cc167/image.png" alt=""></p>
<p>하지만 여기에도 한 가지 주의해야할 사항이 있다.
위 그림의 왼쪽 트리에서 10이 저장된 노드가 왼쪽 자식 노드이건 오른쪽 자식 노드이건 이에 상관 없이 10이 저장된 노드는 8이 저장된 노드의 오른쪽 자식 노드가 되어야 한다.
따라서 이를 코드로 구현하면 다음과 같다.</p>
<pre><code class="language-c">// dNode와 pNode는 각각 삭제할 노드와 이의 부모 노드를 가리키는 포인터 변수
if(삭제할 노드가 하나의 자식 노드 갖고 있음)
{
    BTreeNode * dcNode;        // 삭제 대상의 자식 노드를 가리키는 포인터 변수

    // 삭제 대상의 자식 노드 찾기
    if(GetLeftSubTree(dNode) != NULL)    // 자식 노드가 왼쪽에 있다면
        dcNode = GetLeftSubTree(dNode);
    else
        dcNode = GetRightSubTree(dNode);

    // 삭제 대상의 부모 노드와 자식 노드를 연결
    if(GetLeftSubTree(pNode) == dNode)    // 삭제 대상이 왼쪽 자식 노드라면
        ChangeLeftSubTree(pNode, dcNode);    // 왼쪽으로 연결
    else                                // 삭제 대상이 오른쪽 자식 노드라면
        ChangeRightSubTree(pNode, dcNode);    // 오른쪽으로 연결
}</code></pre>
<p>위 코드에서도 Change~로 시작하는 두 함수는 아직 정의한 바 없는 함수들인데,
이들에 대해서도 잠시 후 선언 및 정의할 예정이고 각각의 함수호출이 의미하는 바는 다음과 같다.</p>
<ul>
<li>ChangeLeftSubTree : MakeLeftSubTree 함수와 유사한 함수</li>
<li>ChangeRightSubTree : MakeRightSubTree 함수와 유사한 함수</li>
</ul>
<p>위의 두 함수와 BinaryTree2.h에 선언된 Make~로 시작하는 두 함수와의 유일한 차이점은,
기존 자식 노드의 메모리 소멸 과정을 동반하느냐 안하느냐에 있다.</p>
<p><strong>- 상황 3) 삭제할 노드가 두 개의 자식 노드(두 개의 서브 트리)를 갖는 경우</strong></p>
<p>마지막 상황을 살펴보기 위해 다음 그림을 보면서 삭제로 인해 비는 위치를 무엇으로 채울지 한번 생각해보자.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/bb207a7f-994a-4abc-90e6-fccb067830d8/image.png" alt=""></p>
<p>8을 삭제하는 경우 이를 대체할 후보로 다음 두 개의 노드를 꼽을 수 있다.</p>
<ul>
<li>8이 저장된 노드의 왼쪽 서브 트리에서 가장 큰 값인 7을 저장한 노드</li>
<li>8이 저장된 노드의 오른쪽 서브 트리에서 가장 작은 값인 9를 저장한 노드</li>
</ul>
<p>삭제 대상을 7 또는 9를 저장한 노드로 각각 대체했을 때의 결과는 다음과 같다.</p>
<table>
<thead>
<tr>
<th>7이 저장된 노드로 대체한 경우</th>
<th>9가 저장된 노드로 대체한 경우</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/61e72c28-2515-4a45-9e54-2416214c5728/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/39647a41-1daa-45e8-a17f-77b82cd33f04/image.png" alt=""></td>
</tr>
</tbody></table>
<p>이렇듯 삭제할 노드의 왼쪽 서브 트리에서 가장 큰 값이나, 삭제할 노드의 오른쪽 서브 트리에서 가장 작은 값을 저장한 노드로 대체하면 된다.
서브 트리에서 가장 큰 값이나 가장 작은 값을 저장한 노드를 찾는 것은 어렵지 않다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/c0eda827-76de-47a3-baa9-e365463958c6/image.png" alt=""></p>
<p>가장 큰 값을 찾을 때는 NULL을 만날 때까지 계속해서 오른쪽 자식 노드로 이동하면 되고,
가장 작은 값을 찾을 때는 NULL을 만날 때까지 계속해서 왼쪽 자식 노드로 이동하면 된다.</p>
<p>위 두 가지 방법 중 &quot;삭제할 노드의 오른쪽 서브 트리에서 가장 작은 값을 지니는 노드를 찾아서 이것으로 삭제할 노드를 대체하는 것&quot;을 선택할 것이다.
이 방법으로 이진 탐색 트리의 삭제 전과 후의 모습을 보면 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/4fe2a159-8c2b-47ae-a773-726cb7f6f5d9/image.png" alt=""></p>
<p>삭제의 결과를 이루기 위해 해야 할 일은 다음과 같다.
삭제가 되는 8이 저장된 노드를 9가 저장된 노드로 대체한다.
그리고 이로 인해서 생기는 빈자리는 9가 저장된 노드의 자식 노드로 대체한다.
위 과정을 위해 8이 저장된 노드의 위치에 9가 저장된 노드를 가져다 놓지 않고,
값의 대입을 통해 노드의 교체를 대신하는 방법을 적용할 것이다.
이 방법을 사용하게 되면, 삭제 대상인 8이 저장된 노드의 부모 노드와 자식 노드의 연결을 위해서 별도의 코드를 삽입할 필요가 없어지기 때문에 여러모로 편리하다.</p>
<p>위 과정을 다시 한번 정리하면 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/68152e66-c30e-4079-bbae-4dd5ee4a9741/image.png" alt=""></p>
<ul>
<li>단계 1) 삭제할 노드를 대체할 노드 찾기</li>
<li>단계 2) 대체할 노드에 저장된 값을 삭제할 노드에 대입</li>
<li>단계 3) 대체할 노드의 부모 노드와 자식 노드 연결</li>
</ul>
<p>이 단계들을 코드로 구현하면 다음과 같다.</p>
<pre><code class="language-c">// dNode와 pNode는 각각 삭제할 노드와 이의 부모 노드를 가리키는 포인터 변수
if(삭제할 노드가 두 개의 자식 노드 갖고 있음)
{
    BTreeNode * mNode = GetRightSubTree(dNode);        // mNode는 대체 노드
    BTreeNode * mpNode = dNode;                        // mpNode는 대체 노드의 부모 노드

    // 단계 1
    while(GetLeftSubTree(mNode) != NULL)
    {
        mpNode = mNode;
        mNode = GetLeftSubTree(mNode);
    }

    // 단계 2
    SetData(dNode, GetData(mNode));

    // 단계 3
    if(GetLeftSubTree(mpNode) == mNode)        // 대체할 노드가 왼쪽 자식 노드라면
    {
        // 대체할 노드의 자식 노드를 부모 노드의 왼쪽에 연결
        ChangeLeftSubTree(mpNode, GetRightSubTree(mNode));
    }
    else                                    // 대체할 노드가 오른쪽 자식 노드라면
        // 대체할 노드의 자식 노드를 부모 노드의 오른쪽에 연결
        ChangeRightSubTree(mpNode, GetRightSubTree(mNode));
}</code></pre>
<p>단계 3에서 GetRightSubTree 함수가 두 번 호출된 것이 어색하다고 생각이 들 수 있다.
이 중 하나는 GetLeftSubTree 함수가 호출되어야 뭔가 짝이 맞을거 같다는 생각이다.
하지만 대체할 노드의 자식 노드는 항상 오른쪽에 존재하기 때문에 이 코드가 맞다.
삭제할 노드의 오른쪽 서브 트리에서 가장 작은 값을 지니는 노드를 찾아서 이것으로 삭제할 노드를 대체하기 때문에 가장 값을 지니는 노드를 찾으려면 NULL을 만날 때까지 왼쪽 자식 노드로 계속해서 이동해야 한다.
그리고 그 노드이에 자식 노드가 있다면 그것은 오른쪽 노드일 것이기 때문이다!</p>
<p><strong>[3] 삭제 2 : 이진 트리 확장</strong></p>
<p>삭제를 포함한 이진 탐색 트리의 완성을 위해서 우선 BinaryTree2.h와 BinaryTree2.c에 다음 네 개의 함수를 추가로 선언 및 정의하려 한다.</p>
<ul>
<li><code>BTreeNode * RemoveLeftSubTree(BTreeNode * bt);</code>
: 왼쪽 자식 노드를 트리에서 제거, 제거된 노드의 주소 값 반환</li>
<li><code>BTreeNode * RemoveRightSubTree(BTreeNode * bt);</code>
: 오른쪽 자식 노드를 트리에서 제거, 제거된 노드의 주소 값 반환</li>
<li><code>void ChangeLeftSubTree(BTreeNode * main, BTreeNode * sub);</code>
: 메모리 소멸을 수반하지 않고 main의 왼쪽 자식 노드를 변경</li>
<li><code>void ChangeRightSubTree(BTreeNode * main, BTreeNode * sub);</code>
: 메모리 소멸을 수반하지 않고 main의 오른쪽 자식 노드를 변경</li>
</ul>
<p>이전에 구현한 이진 탐색 트리는 이미 구현한 이진 트리의 함수만으로는 그 도구가 충분하지 않았다.
노드의 제거에 대한 기능이 정의되지 않았고, MakeLeftSubTree와 MakeRightSubTree 함수는 교체되는 노드의 소멸까지 진행했기 때문이다.
그래서 단순히 자식 노드의 교체를 목적으로 함수 두 가지를 추가로 정의했다.
그리고 당시에는 이진 트리의 구성에 초점을 맞췄기 때문에 삭제에 대한 기능을 정의하지 않았다.
삭제에 관련된 함수 두 가지를 추가로 정의했다.
이 네 가지 함수를 구현하면 다음과 같다.</p>
<pre><code class="language-c">// 왼쪽 자식 노드 제거, 제거된 노드의 주소 값 반환
BTreeNode * RemoveLeftSubTree(BTreeNode * bt)
{
    BTreeNode * delNode;

    if(bt != NULL)
    {
        delNode = bt-&gt;left;
        bt-&gt;left = NULL;
    }
    return delNode;
}

// 오른쪽 자식 노드를 트리에서 제거, 제거된 노드의 주소 값 반환
BTreeNode * RemoveRightSubTree(BTreeNode * bt)
{
    BTreeNode * delNode;

    if(bt != NULL)
    {
        delNode = bt-&gt;right;
        bt-&gt;right = NULL;
    }
    return delNode;
}

// 메모리 소멸을 수반하지 않고 main의 왼쪽 자식 노드를 변경
void ChangeLeftSubTree(BTreeNode * main, BTreeNode * sub)
{
    main-&gt;left = sub;
}

// 메모리 소멸을 수반하지 않고 main의 오른쪽 자식 노드를 변경
void ChangeRightSubTree(BTreeNode * main, BTreeNode * sub)
{
    main-&gt;right = sub;
}</code></pre>
<p>보통 삭제 또는 제거라고 하면 반드시 메모리의 해제까지 담당해야 한다고 생각하는 경우가 많다.
하지만 삭제와 메모리의 해제는 별개의 것이며 삭제 과정에서 메모리의 해제를 포함하는 경우도 있지만 반드시 필요한 것은 아니며 포함해서는 안되는 경우도 존재한다.</p>
<p>위에 새로 정의한 함수 네 가지는 BinaryTree3.h와 BinaryTree3.c로 기존 함수에 더해서 저장해보려고 한다.</p>
<p><strong>- 헤더 파일 (BinaryTree3.h)</strong></p>
<pre><code class="language-c">#ifndef __BINARY_TREE3_H__
#define __BINARY_TREE3_H__

typedef int BTData;

typedef struct _bTreeNode
{
    BTData data;
    struct _bTreeNode * left;
    struct _bTreeNode * right;
} BTreeNode;

BTreeNode * MakeBTreeNode(void);
BTData GetData(BTreeNode * bt);
void SetData(BTreeNode * bt, BTData data);

BTreeNode * GetLeftSubTree(BTreeNode * bt);
BTreeNode * GetRightSubTree(BTreeNode * bt);

void MakeLeftSubTree(BTreeNode * main, BTreeNode * sub);
void MakeRightSubTree(BTreeNode * main, BTreeNode * sub);

// 순회 방법 함수
typedef void (*VisitFuncPtr)(BTData data);

void InorderTraverse(BTreeNode * bt, VisitFuncPtr action);
void PostorderTraverse(BTreeNode * bt, VisitFuncPtr action);
void PreorderTraverse(BTreeNode * bt, VisitFuncPtr action);

// 노드 삭제 관련 함수 추가
BTreeNode * RemoveLeftSubTree(BTreeNode * bt);
BTreeNode * RemoveRightSubTree(BTreeNode * bt);
void ChangeLeftSubTree(BTreeNode * main, BTreeNode * sub);
void ChangeRightSubTree(BTreeNode * main, BTreeNode * sub);

#endif</code></pre>
<p><strong>- 소스 파일 (BinaryTree3.c)</strong></p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;BinaryTree3.h&quot;

BTreeNode * MakeBTreeNode(void)
{
    BTreeNode * nd = (BTreeNode *)malloc(sizeof(BTreeNode));
    nd-&gt;left = NULL;
    nd-&gt;right = NULL;
    return nd;
}

BTData GetData(BTreeNode * bt)
{
    return bt-&gt;data;
}

void SetData(BTreeNode * bt, BTData data)
{
    bt-&gt;data = data;
}

BTreeNode * GetLeftSubTree(BTreeNode * bt)
{
    return bt-&gt;left;
}

BTreeNode * GetRightSubTree(BTreeNode * bt)
{
    return bt-&gt;right;
}

void MakeLeftSubTree(BTreeNode * main, BTreeNode * sub)
{
    if(main-&gt;left != NULL)
        free(main-&gt;left);

    main-&gt;left = sub;
}

void MakeRightSubTree(BTreeNode * main, BTreeNode * sub)
{
    if(main-&gt;right != NULL)
        free(main-&gt;right);

    main-&gt;right = sub;
}

// 중위 순회
void InorderTraverse(BTreeNode * bt, VisitFuncPtr action)
{
    if(bt == NULL)
        return;

    InorderTraverse(bt-&gt;left, action);
    action(bt-&gt;data);
    InorderTraverse(bt-&gt;right, action);
}

// 후위 순회
void PostorderTraverse(BTreeNode * bt, VisitFuncPtr action)
{
    if(bt == NULL)
        return;

    PostorderTraverse(bt-&gt;left, action);
    PostorderTraverse(bt-&gt;right, action);
    action(bt-&gt;data);
}

// 전위 순회
void PreorderTraverse(BTreeNode * bt, VisitFuncPtr action)
{
    if(bt == NULL)
        return;

    action(bt-&gt;data);
    PreorderTraverse(bt-&gt;left, action);
    PreorderTraverse(bt-&gt;right, action);
}

BTreeNode * RemoveLeftSubTree(BTreeNode * bt)
{
    BTreeNode * delNode;

    if(bt != NULL)
    {
        delNode = bt-&gt;left;
        bt-&gt;left = NULL;
    }
    return delNode;
}

BTreeNode * RemoveRightSubTree(BTreeNode * bt)
{
    BTreeNode * delNode;

    if(bt != NULL)
    {
        delNode = bt-&gt;right;
        bt-&gt;right = NULL;
    }
    return delNode;
}

void ChangeLeftSubTree(BTreeNode * main, BTreeNode * sub)
{
    main-&gt;left = sub;
}

void ChangeRightSubTree(BTreeNode * main, BTreeNode * sub)
{
    main-&gt;right = sub;
}</code></pre>
<p><strong>[4] 삭제 3 : 완전한 구현</strong></p>
<p>삭제 기능을 추가한 이진 탐색 트리의 완전한 구현결과를 보기 위해서 다음 두 개의 파일을 작성하자.</p>
<ul>
<li>BinarySearchTree2.h : 이진 탐색 트리의 헤더파일</li>
<li>BinarySearchTree2.c : 이진 탐색 트리의 소스파일</li>
</ul>
<p>이전에 구현한 이진 탐색 트리(BinarySearchTree.h, BinarySearchTree.c)의 확장판이다.</p>
<p><strong>- 헤더 파일 (BinarySearchTree2.h)</strong></p>
<pre><code class="language-c">#ifndef __BINARY_SEARCH_TREE2_H__
#define __BINARY_SEARCH_TREE2_H__

#include &quot;BinaryTree3.h&quot;

typedef BTData BSTData;

// BST의 생성 및 초기화
void BSTMakeAndInit(BTreeNode ** pRoot);

// 노드에 저장된 데이터 반환
BSTData BSTGetNodeData(BTreeNode * bst);

// BST를 대상으로 데이터 저장(노드의 생성과정 포함)
void BSTInsert(BTreeNode ** pRoot, BSTData data);

// BST를 대상으로 데이터 탐색
BTreeNode * BSTSearch(BTreeNode * bst, BSTData target);

// ㅌ리에서 노드를 제거하고 제거된 노드의 주소 값 반환
BTreeNode * BSTRemove(BTreeNode ** pRoot, BSTData target);

// 이진 탐색 트리에 저장된 모든 노드의 데이터 출력
void BSTShowAll(BTreeNode * bst);

#endif</code></pre>
<p><strong>- 소스 파일 (BinarySearchTree2.c)</strong></p>
<p>거의 BinarySearchTree.c 함수와 동일하고 마지막 함수 3개만 추가되었다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;BinaryTree3.h&quot;
#include &quot;BinarySearchTree2.h&quot;

void BSTMakeAndInit(BTreeNode ** pRoot)
{
    *pRoot = NULL;
}

BSTData BSTGetNodeData(BTreeNode * bst)
{
    return GetData(bst);
}

void BSTInsert(BTreeNode ** pRoot, BSTData data)
{
    BTreeNode * pNode = NULL;       // parent node
    BTreeNode * cNode = *pRoot;     // current node
    BTreeNode * nNode = NULL;       // new node

    // 새로운 노드(새 데이터 담긴 노드)가 추가될 위치 찾기
    while(cNode != NULL)
    {
        if(data == GetData(cNode))
            return;     // already inserted

        pNode = cNode;

        if(GetData(cNode) &gt; data)
            cNode = GetLeftSubTree(cNode);
        else
            cNode = GetRightSubTree(cNode);
    }

    // pNode의 자식 노드로 추가할 새 노드의 생성
    nNode = MakeBTreeNode();        // 새 노드 생성
    SetData(nNode, data);           // 새 노드에 데이터 저장

    // pNode의 자식 노드로 새 노드를 추가
    if(pNode != NULL)               // 새 노드가 루트 노드가 아니라면
    {
        if(data &lt; GetData(pNode))
            MakeLeftSubTree(pNode, nNode);
        else
            MakeRightSubTree(pNode, nNode);
    }
    else                            // 새 노드가 루트 노드라면
    {
        *pRoot = nNode;
    }
}

BTreeNode * BSTSearch(BTreeNode * bst, BSTData target)
{
    BTreeNode * cNode = bst;        // current node
    BSTData cd;                     // current data

    while(cNode != NULL)
    {
        cd = GetData(cNode);

        if(target == cd)
            return cNode;
        else if(target &lt; cd)
            cNode = GetLeftSubTree(cNode);
        else
            cNode = GetRightSubTree(cNode);
    }

    return NULL;    // 탐색대상이 저장되어 있지 않음.
}

BTreeNode * BSTRemove(BTreeNode ** pRoot, BSTData target)
{
    // 삭제 대상이 루트 노드인 경우 별도로 고려
    BTreeNode * pVRoot = MakeBTreeNode();   // 가상의 루트 노드
    BTreeNode * pNode = pVRoot;             // parent node
    BTreeNode * cNode = *pRoot;             // current node
    BTreeNode * dNode;                      // delete node

    // 루트 노드를 pVRoot가 가리키는 노드의 오른쪽 자식 노드가 되게
    ChangeRightSubTree(pVRoot, *pRoot);

    // 삭제 대상인 노드를 탐색
    while(cNode != NULL &amp;&amp; GetData(cNode) != target)
    {
        pNode = cNode;

        if(target &lt; GetData(cNode))
            cNode = GetLeftSubTree(cNode);
        else
            cNode = GetRightSubTree(cNode);
    }
    // 삭제 대상이 존재하지 않다면
    if(cNode == NULL)
        return NULL;

    // 삭제 대상 dNode 가리키게
    dNode = cNode;

    // 경우 1) 삭제 대상이 단말 노드
    if(GetLeftSubTree(dNode) == NULL &amp;&amp; GetRightSubTree(dNode) == NULL)
    {
        if(GetLeftSubTree(pNode) == dNode)
            RemoveLeftSubTree(pNode);
        else
            RemoveRightSubTree(pNode);
    }

    // 경우 2) 삭제 대상이 하나의 자식 노드
    else if(GetLeftSubTree(dNode) == NULL || GetRightSubTree(dNode) == NULL)
    {
        // 삭제 대상의 자식 노드
        BTreeNode * dcNode;

        if(GetLeftSubTree(dNode) != NULL)
            dcNode = GetLeftSubTree(dNode);
        else
            dcNode = GetRightSubTree(dNode);

        if(GetLeftSubTree(pNode) == dNode)
            ChangeLeftSubTree(pNode, dcNode);
        else
            ChangeRightSubTree(pNode, dcNode);
    }

    // 경우 3) 삭제 대상이 두 개의 자식 노드
    else
    {
        // 대체 노드 가리킴
        BTreeNode * mNode = GetRightSubTree(dNode);
        // 대체 노드의 부모 노드
        BTreeNode * mpNode = dNode;
        int delData;

        // 삭제 대상의 대체 노드 찾기
        while(GetLeftSubTree(mNode) != NULL)
        {
            mpNode = mNode;
            mNode = GetLeftSubTree(mNode);
        }

        // 대체 노드에 저장된 값을 삭제 노드에 대입
        delData = GetData(dNode);
        SetData(dNode, GetData(mNode));

        // 대체 노드의 부모 노드와 자식 노드 연결
        if(GetLeftSubTree(mpNode) == mNode)
            ChangeLeftSubTree(mpNode, GetRightSubTree(mNode));
        else
            ChangeRightSubTree(mpNode, GetRightSubTree(mNode));

        dNode = mNode;
        SetData(dNode, delData);
    }

    // 삭제된 노드가 루트 노드인 경우
    if(GetRightSubTree(pVRoot) != *pRoot)
        *pRoot = GetRightSubTree(pVRoot);   // 루트 노드의 변경 반영

    free(pVRoot);   // 가상 루트 노드 소멸
    return dNode;   // 삭제된 노드 반환
}

void ShowIntData(int data)
{
    printf(&quot;%d &quot;, data);
}

void BSTShowAll(BTreeNode * bst)
{   
    // 중위 순회
    InorderTraverse(bst, ShowIntData);
    printf(&quot;\n&quot;);
}</code></pre>
<p>위 함수 중 BSTRemove 함수에 대한 이해를 돕기 위해 그림을 하나 살펴보자.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/5a185fdd-449b-450e-8129-e9063673a892/image.png" alt=""></p>
<p>V라는 노드를 하나 생성해서 실제 루트 노드인 R이 V의 오른쪽 자식 노드가 되게 한다.
이는 삭제할 노드가 루트 노드인 경우의 예외적인 삭제흐름을 일반화 하기 위함이다.
루트 노드를 삭제할 경우 코드에서는 cNode와 pNode가 연결되어 있어 삭제할 때 부모와 자식을 연결시키는 가정이 있기 때문에 cNode의 부모 노드를 언제든 가리키고 있어야 한다.
따라서 V라는 가상의 노드를 만들어 부모 노드와 자식 노드의 연결을 만들어주는 것이다.
(과정을 일반화 하기 위함)</p>
<p>그리고 삭제 대상을 탐색하기 위해서 while문을 사용했는데 BSTSearch 함수를 사용하지 않은 이유는 반복문을 실행하게 되면 cNode는 삭제할 노드를 가리키게 되는데, BSTSearch 함수를 사용하면 cNode의 부모 노드를 pNode가 가리킬 수 없게 되기 때문이다.
따라서 pNode가 가리키는 대상의 갱신을 위해서 별도의 반복문을 구성한 것이다.</p>
<p>마지막으로, 이진 탐색 트리를 대상으로 중위 순회를 할 경우 정렬된 순서대로 데이터를 참조 및 출력할 수 있다.</p>
<p><strong>- 실행 파일 (BinarySearchTreeDelMain.c)</strong></p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;BinarySearchTree2.h&quot;

int main()
{
    BTreeNode * bstRoot;
    BTreeNode * sNode;

    BSTMakeAndInit(&amp;bstRoot);

    BSTInsert(&amp;bstRoot, 5);
    BSTInsert(&amp;bstRoot, 8);
    BSTInsert(&amp;bstRoot, 1);
    BSTInsert(&amp;bstRoot, 6);
    BSTInsert(&amp;bstRoot, 4);
    BSTInsert(&amp;bstRoot, 9);
    BSTInsert(&amp;bstRoot, 3);
    BSTInsert(&amp;bstRoot, 2);
    BSTInsert(&amp;bstRoot, 7);

    BSTShowAll(bstRoot);
    sNode = BSTRemove(&amp;bstRoot, 3);
    free(sNode);

    BSTShowAll(bstRoot);
    sNode = BSTRemove(&amp;bstRoot, 8);
    free(sNode);

    BSTShowAll(bstRoot);
    sNode = BSTRemove(&amp;bstRoot, 1);
    free(sNode);

    BSTShowAll(bstRoot);
    sNode = BSTRemove(&amp;bstRoot, 6);
    free(sNode);

    BSTShowAll(bstRoot);
    return 0;
}

&gt; gcc .\BinarySearchTree2.c .\BinarySerachTreeDelMain.c .\BinaryTree3.c     &gt; .\a.exe
&gt; 출력
1 2 3 4 5 6 7 8 9
1 2 4 5 6 7 8 9
1 2 4 5 6 7 9
2 4 5 6 7 9
2 4 5 7 9</code></pre>
<hr>
<h2 id="review"><code>&lt;Review&gt;</code></h2>
<p>탐색에 대한 부분이 절반 정도 끝이 났다.</p>
<p>사실 마지막 이진 탐색 트리의 삭제에 대한 부분은 아직도 이해가 잘 안가서 그림을 그리면서 다시 복습해봐야겠다.
(어차피 삭제되도 오른쪽 자식을 가져온다는 부분이 이해가 안된달까....)</p>
<p>그래도 여기까지 온 내 자신이 대견하다...!
앞으로 chapter 3개가 남았는데 그 때까지 화이팅!!</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/9b604c46-e2c9-484b-8d0e-b1d3677e22cf/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chapter 10. 정렬(Sorting)]]></title>
            <link>https://velog.io/@mingming_eee/datastructure-10</link>
            <guid>https://velog.io/@mingming_eee/datastructure-10</guid>
            <pubDate>Thu, 21 Nov 2024 08:01:40 GMT</pubDate>
            <description><![CDATA[<h2 id="10-1-단순한-정렬-알고리즘">10-1. 단순한 정렬 알고리즘</h2>
<p>이번 Chapter에서는 각종 정렬 알고리즘에 대해 알아보자.
각각의 알고리즘이 갖는 특징에 중점을 두고 알아보자.</p>
<h3 id="1-버블-정렬-bubble-sort"><em>(1) 버블 정렬 (Bubble Sort)</em></h3>
<h4 id="1-이해와-구현">1) 이해와 구현</h4>
<p>버블 정렬은 정렬의 대명사로 알려져 있는, 이미 알고 있을 수 있는 정렬 방법이다.
그만큼 이해하기도, 구현하기도 쉽다. 이해와 구현이 쉬운 만큼 성능에 아쉬움이 있다.
버블 정렬의 이해를 위해 예시로 3, 2, 4, 1이 순서대로 저장된 다음 배열을 &#39;오름차순&#39;으로 정렬하는 과정을 살펴보자.</p>
<p>버블 정렬은 인접한 두 개의 데이터를 비교해가며 정렬을 진행하는 방식이다.
두 데이터를 비교하여 정렬순서상 위치가 바뀌어야 하는 경우 두 데이터의 위치를 바꿔나간다.</p>
<table>
<thead>
<tr>
<th>No.</th>
<th>Pic.</th>
<th>Expl.</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/418ac6d8-e62d-4654-95ac-6be7825cc5d7/image.png" alt=""></td>
<td>정렬의 우선순위가 가장 낮은, 제일 큰 값(4)을 맨 뒤로 보내기.</td>
</tr>
<tr>
<td>2</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/16f388e1-071c-47dd-884b-904fb443dd64/image.png" alt=""></td>
<td>두 번째로 큰 값(3)을 맨 뒤에서 한 칸 앞으로 보내기.<br>정렬이 완료된(배열의 끝에 위치한) 데이터를 제외하고 나머지를 대상으로 비교와 교환 진행.</td>
</tr>
<tr>
<td>3</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/81bb6bd1-1de5-4015-b4cd-9474c56549df/image.png" alt=""></td>
<td>남은 두 데이터를 비교하고 교환하여 정렬이 완료.</td>
</tr>
</tbody></table>
<p>버블 정렬이란 이름이 붙게 된 이유는 앞에서부터 순서대로 비교하고 교환하는 일련의 과정이 거품이 일어나는 모습에 비유되어 붙여진 이름이다.</p>
<p>다음은 버블 정렬의 구현 예시다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

void BubbleSort(int arr[], int n)
{
    int i, j;
    int temp;

    for(i=0; i&lt;n-1; i++)
    {
        for(j=0; j&lt;(n-1); j++)
        if(arr[j] &gt; arr[j+1])
        {
            // 데이터 교환
            temp = arr[j];
            arr[j] = arr[j+1];
            arr[j+1] = temp;
        }
    }
}

int main()
{
    int arr[4] = {3, 2, 4, 1};
    int i;

    BubbleSort(arr, sizeof(arr)/sizeof(int));

    for(i=0; i&lt;4; i++)
        printf(&quot;%d &quot;, arr[i]);
    printf(&quot;\n&quot;);

    return 0;
}

&gt; 출력
1 2 3 4 </code></pre>
<p>위 코드에서는 버블 정렬을 구성하는 두 for문의 반복조건이 핵심이다.
따라서 바깥쪽 for문의 반복 조건과 안쪽 for문의 반복조건에 대한 정확한 이해가 필요하다.</p>
<h4 id="2-성능-평가">2) 성능 평가</h4>
<p>정렬 알고리즘의 성능은 다음 두 가지를 근거로 판단하는 것이 일반적이다.</p>
<ul>
<li>비교의 횟수: 두 데이터간의 비교연산의 횟수</li>
<li>이동의 횟수: 위치의 변경을 위한 데이터의 이동횟수</li>
</ul>
<p>&#39;비교연산&#39;과 데이터 이동을 위한 &#39;대입연산&#39;이 정렬과정의 핵심연산이기 때문이다.</p>
<p>실제도로 시간복잡도에 대한 빅-오를 결정하는 기준은 &#39;비교의 횟수&#39;다.
하지만 &#39;이동의 횟수&#39;까지 살펴보면 동일한 빅-오의 복잡도를 갖는 알고리즘 간의 세밀한 비교가 가능하다.</p>
<p>버블 정렬의 비교횟수는 다음 반복문 안에 위치한 if문의 실행 횟수를 기준으로 계산할 수 있다.
버블 정렬에서 데이터의 수가 $$n$$개일 때 진행이 되는 비교의 횟수는 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/e28eb1ab-1297-494f-881d-3f83dfc50e27/image.png" alt=""></p>
<p>그리고 이는 등차수열의 합이므로 다음과 같이 정리된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/3b00276e-a0bf-4ff8-ae92-2d22456303bf/image.png" alt=""></p>
<p>따라서 버블 정렬의 비교연산에 대한 빅-오는 최악의 경우와 최선의 경우 구분 없이 최고 차항을 따라 $$O(n^2)$$가 된다.</p>
<p>단순히 반복문이 중첩되어 있을 뿐인데 실제 활용하기 부담스러운 정도의 성능을 보인다.
그렇다면 데이터의 이동횟수(교환횟수)는 어떨까?
이는 최선의 경우와 최악의 경우가 구분된다.
데이터가 이미 정렬되어 있는 상태라면 데이터의 이동(교환)이 한 번도 일어나지 않지만,
반대로 정렬기준의 역순으로 저장된 상태라면 비교의 횟수와 이동의 횟수(교환의 횟수)가 일치하기 때문이다.
따라서 데이터 이동연산에 대한 빅-오는 최악의 경우 $$O(n^2)$$가 된다.
실제로 최악의 경우 버블 정렬의 데이터 이동횟수는 비교횟수보다 3배 많아지게 된다. 값의 교환 과정에서 대입 연산이 3회 진행되기 때문이다.
하지만 빅-오를 판단하는 과정에서 계수가 생략되기 때문에 이를 무시한다.</p>
<h3 id="2-선택-정렬-selection-sort"><em>(2) 선택 정렬 (Selection Sort)</em></h3>
<p>이번에 배울 선택 정렬은 버블 정렬보다도 쉽고 간단한 알고리즘이다.</p>
<h4 id="1-이해와-구현-1">1) 이해와 구현</h4>
<p>다음 그림과 같이 1, 2, 4, 3이 나란히 저장된 배열의 오름차순 정렬과정을 살펴보자.
간단하다는 것을 바로 알 수 있게 된다.</p>
<table>
<thead>
<tr>
<th>.</th>
<th>Pic.</th>
<th>Expl.</th>
</tr>
</thead>
<tbody><tr>
<td>초기</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/4f280091-2e09-4466-8887-827d75315d9f/image.png" alt=""></td>
<td>선택 정렬은 정렬순서에 맞게 하나씩 선택해서 옮기고, 옮기면서 정렬이 되게 하는 알고리즘.<br>이 방법대로라면 선택 정렬은 정렬결과를 담을 별도의 메모리 공간이 필요.<br>하지만 데이터를 하나 옮길 때마다 공간이 하나씩 비게 된다는 사실을 기반으로 개선하게 되면 별도의 공간을 마련할 필요 없음.</td>
</tr>
<tr>
<td>개선</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/87f1f0e8-1fc6-4c7d-a53f-690f492b5444/image.png" alt=""></td>
<td>어떻게 보면 교환해서 정렬을 시킨다고 생각할 수 있지만<br>&quot;정렬순서상 가장 앞서는 것을 선택해서 가장 왼쪽으로 이동시키고, 원래 그 자리에 있던 데이터는 빈 자리에 가져다 놓는다.&quot;<br>라고 이해. (이게 교환 아닌가...? ㅎ)<br>빈자리를 활용하는 과정에서 비롯한 교환이라는 점을 이해할 필요 있다.</td>
</tr>
</tbody></table>
<p>위에서 설명한 개선된 선택 정렬을 구현해보면 아래와 같다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

void SelSort(int arr[], int n)
{
    int i, j;
    int maxIdx;
    int temp;

    for(i=0; i&lt;n-1; i++)
    {
        maxIdx = i;

        for(j=i+1; j&lt;n; j++)    // 최솟값 탐색
            if(arr[j] &lt; arr[maxIdx])
                maxIdx = j;

        // 교환
        temp = arr[i];
        arr[i] = arr[maxIdx];
        arr[maxIdx] = temp;
    }
}

int main()
{
    int arr[4] = {3, 4, 2, 1};
    int i;

    SelSort(arr, sizeof(arr)/sizeof(int));

    for(i=0; i&lt;4; i++)
        printf(&quot;%d &quot;, arr[i]);

    printf(&quot;\n&quot;);

    return 0;
}

&gt; 출력
1 2 3 4</code></pre>
<h4 id="2-성능-평가-1">2) 성능 평가</h4>
<p>선택 정렬의 코드만 봤을 때는 for문이 이중으로 되어 있어 버블 정렬과 성능상 큰 차이가 없음을 알 수 있다.
그럼 비교횟수의 확인을 위해서 반복문을 좀 더 자세히 살펴보자.</p>
<p>바깥쪽 for문의 $$i$$가 $$0$$일 때 안쪽 for문의 $$j$$는 $$1$$부터 $$n-1$$까지 증가하여 성태 ㄱ정렬의 비교 연산은 $$n-1$$회 진행된다.
그리고 바깥쪽 for문의 $$i$$가 $$1$$일 때는 안쪽 for문의 $$j$$가 $$2$$부터 $$n-1$$까지 증가하여 선택 정렬의 비교연산은 $$n-2$$회 진행된다.
이는 버블 정렬의 경우와 똑같기 때문에 선택 정렬의 빅-오 역시 최악의 경우와 최선의 경우 구분 없이 최고 차항을 따라 $$O(n^2)$$가 된다.</p>
<p>언뜻보면 버블 정렬보다 나은 성능을 보장할 것처럼 보였지만 비교횟수를 기준으로 보면 차이가 없음을 알 수 있다.
그렇다면 데이터의 이동횟수도 버블 정렬과 차이가 없을까?
여기서는 제법 차이가 있다.</p>
<p>버블정렬이나 선택 정렬이나 데이터의 교환을 위한 세 번의 대입연산이 <code>temp = A; A = B; B = temp;</code>형식으로 진행된다.
하지만 이 교환이 위치하는 곳이 다르다.
버블 정렬의 경우에는 안쪽 for문에 위치해 있고,
선택 정렬의 경우 바깥쪽 for문에 위치해 있다.
때문에 선택 정렬의 경우 $$n-1$$회의 교환이 이뤄지므로 데이터의 이동횟수는 이의 세 배인 $$3(n-1)$$이 된다.
따라서 선택 정렬의 데이터 이동연산에 대한(대입연산에 대한) 빅-오는 최악의 경우와 최선의 경우 상관없이 $$O(n)$$이 된다.</p>
<p>최악의 경우 버블 정렬보다 선택 정렬에 좋은 성능을 기대할 수 있지만,
버블 정렬은 최선의 경우 단 한번의 데이터 이동도 발생하지 않는 점,
실제로 데이터들이 늘 최악의 상황으로 배치되지 않는다는 점을 감안하면
이 둘의 성능을 비교하는 것은 약간 무의미하다고 할 수 있다.</p>
<h3 id="3-삽입-정렬-insertion-sort"><em>(3) 삽입 정렬 (Insertion Sort)</em></h3>
<p>이번에 배울 삽입 정렬은 보는 관점에 따라서 별도의 메모리를 필요로 하지 않는 &#39;개선된 선택 정렬&#39;과 유사하다고 생각할 수 있다.
하지만 전혀 다른 방법으로 정렬을 이뤄나간다.</p>
<h4 id="1-이해와-구현-2">1) 이해와 구현</h4>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/e1054c7a-7dba-4333-99e5-4c988a1196d1/image.png" alt=""></p>
<p>위 그림과 같은 배열이 있다고 예시를 들어보자.
배열은 정령이 완료된 부분과 완료되지 않는 부분으로 나눠있다.
삽입 정렬은 정렬 대상을 이렇게 두 부분으로 나눠서 정렬이 안 된 부분에 있는 데이터를 정렬 된 부분의 특정 위치에 삽입해 가면서 정렬을 진행하는 알고리즘이다.
그럼 다음 그림을 통해서 5, 3, 2, 4, 1의 오름차순 정렬과정을 살펴보자.</p>
<table>
<thead>
<tr>
<th>.</th>
<th>Pic.</th>
<th>Expl.</th>
</tr>
</thead>
<tbody><tr>
<td>이해</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/19e9a390-3db8-479b-9597-816c2c2cf42a/image.png" alt=""></td>
<td>첫 번째 데이터와 두 번째 데이터를 비교하여 정렬된 상태가 되도록 두 번째 데이터를 옮기면서 정렬 시작.<br>첫 번째 데이터와 두 번째 데이터가 정렬이 완료된 영역 형성.<br>세 번째 데이터와 정렬된 부분을 비교하여 세 번째 데이터 이동.<br>세 번째 데이터와 네 번째 데이터가 정렬이 완료된 영역으로 삽입.<br>이렇게 정렬이 진행되기 위해서는 정렬된 상태로 삽입하기 위해 특정위치를 비워야하고 비우기 위해서 데이터들을 한 칸씩 뒤로 미는 연산을 수행할 필요 있음.<br>정렬이 완료된 영역의 다음에 위치한 데이터가 그 다음 정렬대상이 됨.<br>삽입할 위치를 발견하고 데이터를 한 칸 씩 밀수도 있지만, 데이터를 한 칸씩 뒤로 밀면서 삽입할 위치를 찾을 수도 있음.</td>
</tr>
<tr>
<td>구현</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/73e4edc1-a55e-4c93-865f-8b00fdd26a2b/image.png" alt=""></td>
<td>위에서 얘기한 &#39;데이터를 한 칸씩 뒤로 밀면서 삽입할 위치를 찾는 것&#39;을 의미하는 바를 왼쪽에서 그림으로 나타냄.<br>숫자 3을 정렬이 완료된 영역으로 옮기는 과정.<br>삽입위치를 찾는 과정과 삽입을 위한 공간마련의 과정을 구분할 필요 없음.<br>정렬된 영역에서 삽입의 위치를 찾는 것이니 삽입위치를 찾으면서 삽입을 위한 공간의 마련을 병행.</td>
</tr>
</tbody></table>
<p>위에서 설명한 구현 부분을 코드로 나타내면 다음과 같다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

void InserSort(int arr[], int n)
{
    int i, j;
    int insData;

    for(i=1; i&lt;n; i++)
    {
        insData = arr[i];

        for(j=i-1; j&gt;=0; j--)
            if(arr[j] &gt; insData)
                arr[j+1] = arr[j];  // 비교대상 한 칸 뒤로 밀기
            else
                break;  // 삽입위치 찾았으니 탈출

        arr[j+1] = insData;  // 삽입위치에 정렬대상 삽입
    }
}

int main()
{
    int arr[] = {5, 3, 2, 4, 1};
    int i;

    InserSort(arr, sizeof(arr)/sizeof(int));

    for(i=0; i&lt;5; i++)
        printf(&quot;%d &quot;, arr[i]);
    printf(&quot;\n&quot;);

    return 0;
}

&gt; 출력
1 2 3 4 5</code></pre>
<h4 id="2-성능-평가-2">2) 성능 평가</h4>
<p>삽입 정렬은 정렬대상의 대부분이 이미 정렬되어 있는 경우 매우 빠르게 동작한다.
삽입 정렬의 성능평가를 위해 반복문을 살펴보자.
정렬대상이 완전히 정렬된 상태라면, 안쪽 for문의 if...else문의 조건은 항상 거짓이 되어 break문을 실행해 빠져나오게 된다.
따라서 데이터의 이동도 발생하지 않고 if...else문의 조건비교도 바깥쪽 for문의 반복횟수 이상 진행되지 않는다.</p>
<p>하지만 최악의 경우를 생각하면, 앞에서 배운 버블 정렬 &amp; 선택 정렬과 동일하다.
최악의 경우 안쪽 for문의 if...else문의 조건은 항상 참이 디어 break문을 한번도 실행하지 않게된다.
따라서 바깥쪽 for문의 반복횟수와 안쪽 for문의 반복횟수를 곱한 수만큼 비교연산도, 이동연산도 진행되므로
최악의 경우 비교연산과 이동연산에 대한 빅-오는 $$O(n^2)$$가 된다.</p>
<hr>
<h2 id="10-2-복잡하지만-효율적인-정렬-알고리즘">10-2. 복잡하지만 효율적인 정렬 알고리즘</h2>
<p>앞서 배운 단순한 정렬 알고리즘들은 정렬대상의 수가 적은 경우 효율적으로 사용할 수 있어서 나름의 의미를 지닌다.
하지만 정렬대상의 수가 적지 않은 경우에는 보다 효율적인 알고리즘이 필요하다.
이번에 배울 알고리즘들이 그렇다.
소제목에서 &#39;복잡하다&#39;라는 단어 때문에 어려운 알고리즘일거라고 생각할 수 있지만,
앞에서 배운 알고리즘들보다 상대적으로 복잡하기 때문이지 어려운 것은 아닐 수 있다.
(오히려 기발한 정렬방식에 흥미를 느낄 수도...?😉)</p>
<h3 id="1-힙-정렬-heap-sort"><em>(1) 힙 정렬 (Heap Sort)</em></h3>
<p>힙 정렬(heap sort)은 힙을 이용한 정렬방식으로 &quot;힙의 루트 노드에 저장된 값이 가장 커야한다.&quot;는 힙의 특성을 활용해서 정렬하는 알고리즘이다.
물론 이는 &#39;최대 힙(max heap)&#39;의 특징이다.
하지만, &quot;힙의 루트 노드에 저장된 값이 정렬순서상 가장 앞선다&quot;는 특징을 갖도록 힙을 구성할 수도 있다.</p>
<h4 id="1-이해와-구현-3">1) 이해와 구현</h4>
<p>예제를 하나 볼 건데 이 예제만 이해하더라도 힙 정렬을 쉽게 이해할 수 있다.
이 예제를 실행하기 위해서는 앞서 공부한 Chapter 09에서 사용한 <a href="https://velog.io/@mingming_eee/datastructure-09#2-%EC%82%AC%EC%9A%A9-%EA%B0%80%EB%8A%A5-%EC%88%98%EC%A4%80%EC%9D%98-%ED%9E%99-%EA%B5%AC%ED%98%84">UsefulHeap.h 파일과 UsefulHeap.c 파일</a>이 필요하다.</p>
<ul>
<li>UsefulHeap.h : 사용 가능 수준의 힙이라 이름 붙였던 힙의 헤더파일</li>
<li>UsefulHeap.c : 사용 가능 수준의 힙이라 이름 붙였던 힙의 소스파일</li>
</ul>
<p>그리고 컴파일 하기 전 UsefulHeap.h에서 typedef 선언을 <code>typedef char HData; → typedef int HData;</code>로 변경해야 한다.
(우리는 정수 데이터만 다룰 것이기 때문에)</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &quot;UsefulHeap.h&quot;

int PriComp(int n1, int n2)
{
    return n2-n1;   // 오름차순 정렬을 위한 문장
//  return n1-n2;   // 내림차순 정렬을 위한 문장
}

void HeapSort(int arr[], int n, PriorityComp pc)
{
    Heap heap;
    int i;

    HeapInit(&amp;heap, pc);

    // 정렬대상을 가지고 힙을 구성
    for(i=0; i&lt;n; i++)
        HInsert(&amp;heap, arr[i]);

    // 순서대로 하나씩 꺼내어 정렬을 완성
    for(i=0; i&lt;n; i++)
        arr[i] = HDelete(&amp;heap);
}

int main()
{
    int arr[4] = {3, 4, 2, 1};
    int i;

    HeapSort(arr, sizeof(arr)/sizeof(int), PriComp);

    for(i=0; i&lt;4; i++)
        printf(&quot;%d &quot;, arr[i]);
    printf(&quot;\n&quot;);

    return 0;
}

&gt; gcc .\HeapSort.c .\UsefulHeap.c
&gt; .\a.exe
&gt; 출력
1 2 3 4 </code></pre>
<p>힙 정렬의 원리를 설명하기 위해 힙 정렬을 구현한 함수의 일부를 보자.</p>
<pre><code class="language-c">void HeapSort(int arr[], int n, PriorityComp pc)
{
    ....
    // 힙 정렬 단계 1: 데이터를 모두 힙에 넣기.
    for(i=0; i&lt;n; i++)
        HInsert(&amp;heap, arr[i]);

    // 힙 정렬 단계 2: 힙에서 다시 데이터를 꺼내기.
    for(i=0; i&lt;n; i++)
        arr[i] = HDelete(&amp;heap);
}</code></pre>
<p>정렬의 대상인 데이터들을 힙에 넣었다가 꺼내는 것이 전부다.
그럼에도 정렬이 완료되는 이유는 꺼낼 때 힙의 루트 노드에 저장된 데이터가 반환되기 때문이다.
위 예제는 오름차순으로 정렬하기 위해 PriComp 조건이 <code>n2-n1</code>으로 되어있고
이를 내림차순으로 정렬하기 위해서는 조건을 <code>n1-n2</code>로 변경해주면 된다.</p>
<h4 id="2-성능-평가-3">2) 성능 평가</h4>
<p>언뜻 보면 저장된 데이터를 한번에 힙에 넣었다가 다시 꺼내기 때문에 성능상 이점이 별로 없어 보이지만, 힙 정렬은 지금까지 소개한 정렬들 중 가장 좋은 성능을 보이는 알고리즘이다.
Chapter 09에서 비교연산의 횟수를 근거로 하여 힙의 데이터 저장 및 삭제의 시간 복잡도를 다음과 같이 정리했었다.</p>
<ul>
<li>힙의 데이터 저장 시간 복잡도 : $$O(log_2n)$$</li>
<li>힙의 데이터 삭제 시간 복잡도 : $$O(log_2n)$$</li>
</ul>
<p>따라서 삽입과 삭제를 하나의 연산으로 묶는다면 이 연산에 대한 시간 복잡도는 $$O(2log_2n)$$가 되고 2는 빅-오에서 무시할만한 크기기 때문에 $$O(log_2n)$$가 된다.
정렬과정에 대한 시간 복잡도는 정렬대상의 수가 $$n$$개라면 총 $$n$$개의 데이터를 삽입 및 삭제해야하므로 위 빅-오에 $$n$$을 곱한 $$O(nlog_2n)$$이 된다.</p>
<p>딱 보기엔 $$O(nlog_2n)$$이나 $$O(n^2)$$의 차이를 모를 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/1cd12c07-b75a-413b-a930-b8b339044c07/image.png" alt=""></p>
<p>위 표를 보면 $$O(nlog_2n)$$과 $$O(n^2)$$의 차이가 크다는 것을 알고 있다.
$$O(n^2)$$성능을 보이는 알고리즘을 $$O(nlog_2n)$$의 성능을 보일 수 있도록 개선한다면 이는 낮은 성능으로 인해 현실적으로 활용하기 어려운 알고리즘에서 활용이 가능한 수준으로 평가 받을 수 있다.</p>
<h3 id="2-병합-정렬-merge-sort"><em>(2) 병합 정렬 (Merge Sort)</em></h3>
<p>병합 정렬(merge sort)은 &#39;분할 정복(divide and conquer)&#39;이라는 알고리즘 디자인 기법에 근거하여 만들어진 정렬 방법이다.</p>
<p>분할 정복이란, 말 그래도 복잡한 문제를 복잡하지 않은 문제로 &#39;분할(divide)&#39;하여 &#39;정복(conquer)&#39;하는 방법이다.
단 분할해서 정복했으니 정복한 후에는 &#39;결합(combine)&#39;의 과정을 거쳐야 한다.
즉, 다음 3단계를 거치도록 알고리즘을 디자인 하는 것이 분할 정복법이다.</p>
<ul>
<li>1단계 [분할(Divide)]: 해결이 용이한 단계까지 문제를 분할해 나간다.</li>
<li>2단계 [정복(Conquer)]: 해결이 용이한 수준까지 분할된 문제를 해결한다.</li>
<li>3단계 [결합(Combine)]: 분할해서 해결한 결과를 결합하여 마무리한다.</li>
</ul>
<h4 id="1-이해와-구현-4">1) 이해와 구현</h4>
<p>위 분할 정복 방법을 근거로 병합 정렬 알고리즘의 기본 원리는 다음과 같다.
8개의 데이터를 동시에 정렬하는 것보다, 이를 둘로 나눠서 4개의 데이터를 정렬하는 것이 쉽고, 또 이들 각각을 다시 한번 둘로 나눠서 2개의 데이터를 정렬하는 것이 더 쉬운 것이다.</p>
<p>따라서 병합 정렬의 방식은 다음과 같이 설명할 수 있다.</p>
<table>
<thead>
<tr>
<th>.</th>
<th>Pic.</th>
<th>Expl.</th>
</tr>
</thead>
<tbody><tr>
<td>기본 원리</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/a4d6ed49-0737-4342-842b-09b3995d8e18/image.png" alt=""></td>
<td>오른차순 정렬을 기준으로 병합 정렬의 기본 원리.<br>8개의 데이터를 둘씩 나눈 것이 전부지만 실제로는 훨씬 더 작게 분할.<br>2개의 데이터만 남을 때 까지 분할 하면, if문 하나로 간단히 정렬할 수 있기 때문.<br>하지만 여기서 한번 더 나눠 병합 정렬 데이터가 1개만 남을 때 까지 분할한다면 if문으로 정렬할 필요도 없기 때문에 1개만 남을 때까지 분할.<br><br>언뜻보면 병합 정렬은 나누는 것이 핵심같아 보이지만, 나눈 것을 병합하는 과정에서 그 진가가 발휘.</td>
</tr>
<tr>
<td>병합 정렬 예시</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/e9ef1280-c817-435e-9011-967a3fc0bdc3/image.png" alt=""></td>
<td>1. 분할 과정.<br>전체 데이터를 둘로 나누는 과정을 데이터가 하나씩 구분 될 때까지 진행. (8개의 데이터이므로 총 3회 진행)<br>분할 시 정렬을 고려하지 않고 그저 분할만 진행.<br>2. 분할이 완료되면 병합 진행.<br>정렬순서를 고려해서 둘을 하나로 병합.<br>정렬순서를 고려해서 묶기. (분할과 동일하게 총 3회 진행)<br>분할의 과정에서 하나씩 구분될 때까지 둘로 나누는 과정을 한 이유는 재귀적 구현을 위한 것.</td>
</tr>
<tr>
<td>병합 정렬 구현</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/c2858b55-8fdf-4979-8772-c3258c64a1c0/image.png" alt=""></td>
<td>&lt;병합 정렬 진행하는 함수 - MergeSort&gt;<br>첫 번째 인자로 정렬대상이 담긴 배열의 주소 값 전달. 두 번째 인자와 세 번째 인자로 정렬대상의 범위정보를 인덱스 값의 형태로 전달.<br>ex.정렬대상이 배열 전체라면 배열의 첫 번째 요소와 마지막 요소의 인덱스 값을, 두 번째 인자와 세 번째 인자로 각각 전달.<br>병합 정렬의 경우 정렬될 데이터 갯수 정보를 전달했던 이전의 정렬 함수들과 달리 정렬의 범위정보를 전달.<br>마지막 문장 &#39;MergeTwoArea&#39;함수는 정렬된 두 영역을 하나로 묶기 위한 함수.<br>배열 arr의 left ~ mid까지, 그리고 mid+1 ~ right까지 각각 정렬되어 있으니 이를 하나의 정렬된 상태로 묶어서 배열 arr에 저장.</td>
</tr>
</tbody></table>
<p>위에서 이해한 원리, 정렬을 실제 코드로 구현하면 다음과 같다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;

void MergeTwoArea(int arr[], int left, int mid, int right)
{
    int fIdx = left;
    int rIdx = mid + 1;
    int i;

    // 병합 한 결과를 담을 배열 sortArr 동적 할당
    int * sortArr = (int*)malloc(sizeof(int)*(right+1));
    int sIdx = left;

    while(fIdx&lt;=mid &amp;&amp; rIdx&lt;=right) // 분할한 영역일 경우
    {
        // 병합 할 두 영역의 데이터들을 비교하여 정렬 순서대로 sortArr에 하나씩 옮기기
        if(arr[fIdx] &lt;= arr[rIdx])  
            sortArr[sIdx] = arr[fIdx++];
        else
            sortArr[sIdx] = arr[rIdx++];

        sIdx++;
    }

    if(fIdx &gt; mid)  // 배열의 앞부분이 모두 sortArr에 옮겨졌다면
        // 배열의 뒷부분에 남은 데이터들을 sortArr에 그대로 옮기기
        for(i=rIdx; i &lt;= right; i++, sIdx++)
            sortArr[sIdx] = arr[i];
    else    // 배열의 뒷부분이 모두 sortArr에 옮겨졌다면
        // 배열 앞부분에 남은 데이터들 sortArr에 그대로 옮기기
        for(i=fIdx; i &lt;= mid; i++, sIdx++)
            sortArr[sIdx] = arr[i];

    for(i=left; i &lt;= right; i++)    // 마지막으로 정리
        arr[i] = sortArr[i];

    free(sortArr);
}

void MergeSort(int arr[], int left, int right)
{
    int mid;

    if(left &lt; right)
    {
        // 중간 지점 계산
        mid = (left + right) / 2;

        // 둘로 나눠서 각각 정렬
        MergeSort(arr, left, mid);
        MergeSort(arr, mid+1, right);

        // 정렬된 두 배열 병합
        MergeTwoArea(arr, left, mid, right);
    }
}

int main()
{
    int arr[7] = {3, 2, 4, 1, 7, 6, 5};
    int i;

    // 배열 arr의 전체 영역 정렬
    MergeSort(arr, 0, sizeof(arr)/sizeof(int)-1);

    for(i=0; i&lt;7; i++)
        printf(&quot;%d &quot;, arr[i]);
    printf(&quot;\n&quot;);

    return 0;
}

&gt; 출력
1 2 3 4 5 6 7 </code></pre>
<p>여기서 MergeTwoArea 함수 안에서 while문 안에 <code>sIdx++;</code>를 빼먹지 않도록 주의한다.⭐
MergeTwoArea 함수의 구현이 쉽지가 않을 것이다.
그래서 이 함수의 정의에 대해서 더 자세히 이해해보자.</p>
<p>먼저, 위 함수에 삽입된 while문의 반복조건을 이해해야한다.
fIdx와 rIdx에는 각각 병합할 두 영역의 첫 번째 위치정보가 담긴다. (물론 위치정보는 인덱스 값이다.)
그리고 이 두 변수의 값을 증가시켜 가면서 두 영역의 데이터들을 비교해 나가게 된다.
병합할 두 영역은 하나의 배열 안에 함께 존재한다.
따라서 fIdx는 배열의 앞쪽 영역을 가리키게 되고, rIdx는 배열의 뒤쪽 영역을 가리키게 되는데 배열의 앞과 뒤를 구분하는 기준은 변수 mid에 저장되어 있다.
mid+1의 위치서부터 뒤쪽 영역이 시작되기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/0f9c260d-e1cd-474e-9357-55225b8608be/image.png" alt=""></p>
<p>위 그림은 두 영역을 합치기 직전의 상태를 보여준다.
fIdx와 rIdx가 가리키는 위치에 저장된 값은 하나씩 증가하면서 다음과 같이 비교를 진행하게 된다.</p>
<ul>
<li>2와 1 비교: 비교연산 후, 1을 sortArr로 이동, 그리고 rIdx값 1 증가</li>
<li>2와 4 비교: 비교연산 후, 2를 sortArr로 이동, 그리고 fIdx값 1 증가</li>
<li>3과 4 비교: 비교연산 후, 3을 sortArr로 이동, 그리고 fIdx값 1 증가</li>
<li>7과 4 비교: 비교연산 후, 4를 sortArr로 이동, 그리고 fIdx값 1 증가</li>
<li>7과 5 비교: 비교연산 후, 5를 sortArr로 이동, 그리고 fIdx값 1 증가</li>
<li>7과 6 비교: 비교연산 후, 6를 sortArr로 이동, 그리고 fIdx값 1 증가</li>
</ul>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/47e40dbe-76b0-45f7-87db-7555f63bf3f0/image.png" alt=""></p>
<p>이렇게 마지막 비교가 끝나게 되면 rIdx는 right를 넘어서는 위치를 가리키게 되어 더 이상 비교가 무의미해진다.
두 영역을 비교하다 보면, 한 영역의 데이터들이 모두 옮겨져서 더 이상 비교가 불가느한 상황에 도달하는데 이때 while문의 반복조건(<code>fIdx&lt;=mid &amp;&amp; rIdx&lt;=right</code>)에서 벗어나게 되어 while문을 빠져나올 수 있게 되는 것이다.</p>
<p>while문을 빠져나온 뒤 어느 영역의 데이터가 남아 있는지 확인해서 그 남은 데이터를 나머지 배열에 옮기는 것으로 마무리된다. (<code>if~else문</code>)</p>
<h4 id="2-성능-평가-4">2) 성능 평가</h4>
<p>병합 정렬의 성능평가를 위해서 비교연산의 횟수와 이동연산(대입연산)의 횟수를 계산해보자.
그런데 병합 정렬의 성능은 MergeSort 함수가 아닌 MergeTwoArea 함수를 기준으로 계산해야한다.</p>
<p>비교연산의 횟수를 위해 while문을 살펴보자.
정렬의 우선순위를 비교하는 비교연산이 중심이므로 <code>if(arr[fIdx] &lt;= arr[rIdx])</code>문장의 비교연산을 중심으로 비교연산의 횟수를 계산하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/cc972fab-f5a0-42f4-80fe-6007c38c9cc4/image.png" alt=""></p>
<p>위 그림에서 하나와 하나가 모여서 둘이 될 때 비교연산은 최대 2회 진행이 된다.
8과 2를 비교해서 하나로 뭉치게 하려면 8과 2를 비교해야하는데 이는 비교연산 횟수가 1회다.
하지만 while문 이후에 등장하는 if~else문에서의 비교연산 횟수를 포함해서 2회가 된다.
(사실 2든 1이든 빅-오를 구하는데 있어서 그 결과에 큰 영향을 끼치지 않는다.)</p>
<p>그렇다면 둘과 둘이 모여서 넷이 될 때, 최대 비교연산의 횟수는 어떻게 될까?
이 상황에서 최대(최악의 경우) 비교연산의 횟수는 4다.
오른쪽 분리된 배열을 예시로 들면 다음과 같이 비교가 진행된다.</p>
<ul>
<li>1과 4 대상으로 비교연산 후 1을 sortArr로 이동</li>
<li>5와 4 대상으로 비교연산 후 4를 sortArr로 이동</li>
<li>5와 6 대상으로 비교연산 후 5를 sortArr로 이동</li>
<li>마지막 남은 6을 sortArr로 이동하기 위한 if~else문에서 비교연산</li>
</ul>
<p>따라서 병합 2단계에서 8개의 데이터가 있는 배열의 비교연산 횟수는 8회가 될 것이다.
이는 정렬의 대상인 데이터의 수가 $$n$$개 일 때, 각 병합의 단계마다 최대 $$n$$번의 비교연산이 진행되고 $$nlog_2n$$으로 나타낼 수 있다. 
따라서 병합 정렬의 비교연산에 대한 빅-오는 $$O(nlog_2n)$$가 된다.</p>
<p>이번에는 이동연산 횟수를 계산해보자.
이동연산 관점에서 MergeTwoArea 함수를 정리하면</p>
<ol>
<li>정리된 배열을 위한 임시 배열 생성. 이 영역에 병합 결과를 정렬하여 저장 (malloc 부분)</li>
<li>배열 sortArr에 데이터를 정렬하며 이동 (while문)</li>
<li>매열 sortArr에 나머지 데이터 이동 (if~else문)</li>
<li>임시 배열에 저장된 데이터 전부 이동 (for문)</li>
</ol>
<p>따라서 데이터의 이동이 발생하는 이유는 1. 임시 배열에 데이터를 병합하는 과정에서 한번 
2. 임시 배열에 저장된 데이터 전부를 원위치로 옮기는 과정에서 한번
이렇게 두 가지로 볼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/99b58691-e135-4973-82e6-2c33b696c0b4/image.png" alt=""></p>
<p>따라서 각각의 상황에서 비교연산 횟수의 2배에 해당하는 이동연산이 이뤄진다 할 수 있다.
따라서 병합 정렬의 이동연산 횟수는 최악, 평균, 최선의 경우 상관없이 $$2nlog_2n$$가 된다.
빅-오 표기에서 2는 무시할 수 있는 숫자이므로 이동연산의 빅-오는 $$O(nlog_2n)$$가 된다.</p>
<p>참고로 병합 정렬에는 임시 메모리가 필요하다는 단점이 있다. (sortArr를 위한)
하지만 이는 정렬의 대상이 배열이 아닌 연결 리스트의 경우 단점이 되지 않기 때문에 연결 리스트의 경우에는 병합 정렬의 더 좋은 성능을 기대할 수 있다.</p>
<h3 id="3-퀵-정렬-quick-sort"><em>(3) 퀵 정렬 (Quick Sort)</em></h3>
<p>퀵 정렬 (quick sort)도 병합 정렬과 마찬가지로 &#39;분할 정복&#39;에 근거하여 만들어진 정렬 방법이다.
실제로 퀵 정렬 역시 정렬대상을 반씩 줄여가는 과정을 포함한다.
퀵 정렬은 이름이 의미하듯이 평균적으로 매우 빠른 정렬의 속도를 보이는 알고리즘이다.</p>
<h4 id="1-이해와-구현-5">1) 이해와 구현</h4>
<p>퀵 정렬의 기본 원리를 오름차순 정렬을 기준으로 이해해보자.</p>
<table>
<thead>
<tr>
<th>No.</th>
<th>Pic.</th>
<th>Expl.</th>
</tr>
</thead>
<tbody><tr>
<td>1. 초기화</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/74dcfbba-4e60-4878-bcac-c38beaaa8e01/image.png" alt=""></td>
<td>퀵 정렬의 대상이 되는 배열.<br>퀵 정렬을 위해서는 총 5개의 변수 left, right, pivot, low, high를 선언해야 함.<br>- left : 정렬대상의 가장 왼쪽 지점을 가리키는 이름<br>- right : 정렬대상의 가장 오른쪽 지점을 가리키는 이름<br>- pivot (피벗) : 중심점, 중심축 (기준)<br>현재는 가장 왼쪽에 위치한 데이터(5)를 피벗으로 결정<br>- low : 피벗을 제외한 가장 왼쪽에 위치한 지점을 가리키는 이름<br>- high : 피벗을 제외한 가장 오른쪽에 위치한 지점을 가리키는 이름</td>
</tr>
<tr>
<td>2. low와 high의 이동</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/69d0ce45-db41-49f1-ad8d-e17acc9a29b0/image.png" alt=""></td>
<td>(오름차순 기준)<br>- low의 오른쪽 방향 이동 : 피벗보다 큰 값을 만날 때까지<br>- high의 왼쪽 방향 이동 : 피벗보다 작은 값을 만날 때까지<br>(일반화)<br>- low의 오른쪽 방향 이동 : 피벗보다 정렬의 우선순위가 낮은 데이터를 만날 때까지<br>- high의 왼쪽 방향 이동 : 피벗보다 정렬의 우선수위가 높은 데이터를 만날 때까지<br>low와 high의 이동은 별개로 같이 한 칸씩 이동할 필요 없음.</td>
</tr>
<tr>
<td>3. low와 high의 교환</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/642d68dc-dbf1-41a3-a237-9b90ac168e7e/image.png" alt=""><br><img src="https://velog.velcdn.com/images/mingming_eee/post/1c0d96c7-4319-4306-9bf4-46c8ffa5912e/image.png" alt=""><br><img src="https://velog.velcdn.com/images/mingming_eee/post/e7bfa46a-980b-4d06-b90d-333600a75714/image.png" alt=""></td>
<td>이전 단계에서 피벗을 기준으로 low는 피벗보다 큰 값을 만날 때까지, high는 피벗보다 작은 값을 만날 때까지 이동하면<br>low는 7에서 멈추게 되고, high는 4에서 멈추게 됨.<br>low와 high의 데이터 교환.<br>교환이 된 이후 low와 high 이동 계속 진행.<br>low와 high가 가리키는 위치가 교차될 때까지 진행.</td>
</tr>
<tr>
<td>4. 피벗의 이동</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/c0e25e1e-d235-4faa-abc2-34d76c2cf9c8/image.png" alt=""><br><img src="https://velog.velcdn.com/images/mingming_eee/post/971f16e6-2c3f-4a88-98e3-1790e1569d6f/image.png" alt=""></td>
<td>피벗과 high가 가리키는 데이터를 서로 교환.<br>여기까지가 1회전.<br>피벗이었던 5의 정렬 완료.<br>5를 기준으로 왼쪽 영역과 오른쪽 영역으로 나뉘게 됨.<br>왼쪽 영역은 피벗인 2를 기준으로 퀵 정렬 진행.<br>오른쪽 영역은 피벗인 9를 기준으로 퀵 정렬 진행.<br>left와 right가 각각 정렬대상의 시작과 끝을 의미하기 때문에<br>이 과정은 left &gt; right (left와 right가 교차되는 상황)이 될 때까지 진행.</td>
</tr>
</tbody></table>
<p>위에서 설명한 과정을 함수로 구현해보자.
병합 정렬에서 두 배열을 합치는 MergeTwoArea 함수에 비하면 간단하다.</p>
<pre><code class="language-c">void Swap(int arr[], int idx1, int idx2)
{
    int temp = arr[idx1];
    arr[idx1] = arr[idx2];
    arr[idx2] = temp;
}

int Partition(int arr[], int left, int right)
{
    int pivot = arr[left];  // 비벗 위치는 가장 왼쪽
    int low = left + 1;
    int high = right;

    while(low &lt;= high)      // 교차되지 않을 때까지 반복
    {
        // 피벗보다 큰 값 찾기
        while(pivot &gt; arr[low])
            low++;          // low를 오른쪽으로 이동

        // 피벗보다 작은 값 찾기
        while(pivot &lt; arr[high])
            high--;         // high를 왼쪽으로 이동

        // low와 high가 교차되지 않은 상태, Swap
        if(low &lt;= high)
            Swap(arr, low, high);
    }

    Swap(arr, left, high);  // 피벗과 high가 가리키는 대상 교환
    return high;            // 옮겨진 피벗의 위치정보 반환
}</code></pre>
<p>여기서 Partition이란 함수는 이 함수가 반환하는 값은 제 자리를 찾은 피벗의 인덱스 값이기 때문에 이 값을 기준으로 정렬의 대상은 왼쪽 영역과 오른쪽 영역으로 나뉘게 된다는 것을 알 수 있다.
다음은 퀵 정렬을 구현한 함수다.</p>
<pre><code class="language-c">void QuickSort(int arr[], int left, int right)
{
    if(left &lt;= right)
    {
        int pivot = Partition(arr, left, right);    // 피벗 기준 영역 나누기
        QuickSort(arr, left, pivot-1);              // 왼쪽 영역 정렬
        QuickSort(arr, pivot+1, right);             // 오른쪽 영역 정렬
    }
}</code></pre>
<p>반으로 나눈 각각의 영역에서 재귀적인 형태의 함수호출을 통해 각각 정렬하는 것을 알 수 있다.
여기서 if문의 조건은 left &gt; right (left와 right가 교차되는 상황)이 되면 정렬을 종료한 다는 것을 의미하고 있다.</p>
<p>이제 구현한 함수들을 실행할 수 있는 실행파일 전체를 작성하면 다음과 같다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

void Swap(int arr[], int idx1, int idx2)
{
    int temp = arr[idx1];
    arr[idx1] = arr[idx2];
    arr[idx2] = temp;
}

int Partition(int arr[], int left, int right)
{
    int pivot = arr[left];  // 비벗 위치는 가장 왼쪽
    int low = left + 1;
    int high = right;

    while(low &lt;= high)      // 교차되지 않을 때까지 반복
    {
        // 피벗보다 큰 값 찾기
        while(pivot &gt; arr[low])
            low++;          // low를 오른쪽으로 이동

        // 피벗보다 작은 값 찾기
        while(pivot &lt; arr[high])
            high--;         // high를 왼쪽으로 이동

        // low와 high가 교차되지 않은 상태, Swap
        if(low &lt;= high)
            Swap(arr, low, high);
    }

    Swap(arr, left, high);  // 피벗과 high가 가리키는 대상 교환
    return high;            // 옮겨진 피벗의 위치정보 반환
}

void QuickSort(int arr[], int left, int right)
{
    if(left &lt;= right)
    {
        int pivot = Partition(arr, left, right);    // 피벗 기준 영역 나누기
        QuickSort(arr, left, pivot-1);              // 왼쪽 영역 정렬
        QuickSort(arr, pivot+1, right);             // 오른쪽 영역 정렬
    }
}

int main()
{
    int arr[7] = {3, 2, 4, 1, 7, 6, 5};
//  int arr[3] = {3, 3, 3};

    int len = sizeof(arr) / sizeof(int);
    int i;

    QuickSort(arr, 0, len-1);

    for(i=0; i&lt;len; i++)
        printf(&quot;%d &quot;, arr[i]);
    printf(&quot;\n&quot;);

    return 0;
}

&gt; 출력
1 2 3 4 5 6 7</code></pre>
<p>여기서 주석으로 해놓은 <code>int arr[3] = {3, 3, 3};</code> 상황으로 변경해서 적용하면 코드의 버그로 인해서 Partition 함수를 빠져나오지 못해 무한루프가 생성된다.
왜냐하면 <code>pivot == arr[low] == arr[high]</code>상태가 디기 때문에 while 조건이 항상 거짓이 되고 low는 증가의 기회를, high는 감소의 기회를 얻지 못한다.
따라서 바깥족의 while문을 빠져나가지 못해 무한 루프에 갇힌 것이다.</p>
<p>이 부분은 아래와 같이 변경하게 되면 저장된 세 개의 데이터가 같은 상황에서도 진행이 될 수 있다.</p>
<pre><code class="language-c">int Partition(int arr[], int left, int right)
{
    ....
    while(low &lt;= high)
    {
        while(pivot &gt;= arr[low] &amp;&amp; low &lt;= right)
            low++;

        while(pivot &lt;= arr[high] &amp;&amp; high &gt;= (left+1))
            high--;
        ....
    }
    ....
}</code></pre>
<p>이렇게 바꾸게 되면 low와 high는 각각 증가와 감소의 기회를 얻고 각 방향으로 이동할 수 있게 된다.
그리고 추가된 조건이 하나 더 있는데 이는 경계검사를 위한 연산이고 left+1은 피벗을 제외하기 위함이다.</p>
<p>마지막으로 피벗의 선택에 대해서 더 생각해보자.
가장 왼쪽에 위치한 데이터를 피벗으로 결정하였지만 실은 전체 데이터를 기준으로 중간에 해당하는 값을 피벗으로 결정할 때 더 좋은 성능을 보인다.
왜냐면 피벗이 중간에 해당하는 값일 경우 정렬대상이 균등하게 나뉘기 때문이다.
정렬의 과정에서 선택되는 피벗의 수는 앞서 정의한 Partition 함수의 호출횟수를 의마한다.
그리고 Partition 함수의 호출횟수가 많다는 것은 그 만큼 데이터의 비교 및 횟수가 증가함을 의미한다.
즉, 좋은 성능을 보이려면 최대한 중간 값에 가까운 피벗이 지속적으로 선택되어야 한다.
이를 위해서 정렬대상에서 세 개의 데이터를 추출한 후 그 중에서 중간 값에 해당하는 것을 피벗으로 선택하는 방법을 사용한다.
이 방법을 사용하면 중간에 가까운 값을 선택할 확률이 높아지고 이에 따른 추가 연산이 필요하지만 특정 위치의 값을 무조건 피벗으로 결정하는 것보다 좋은 성능을 가질 수 있다.</p>
<h4 id="2-성능-평가-5">2) 성능 평가</h4>
<p>먼저 퀵 정렬의 비교연산 횟수를 알아보자.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/901fed94-6f8d-448e-9c4d-163aa8094266/image.png" alt=""></p>
<p>피벗이 결정되면 low는 오른쪽으로, high는 왼쪽으로 이동하기 시작한다.
그리고 이동은 low와 high가 역전될 때까지 진행된다.
이동의 과정에서 피벗과의 비교를 매번 수반하므로 하나의 피벗이 제 자리를 찾아가는 과정에서 발생하는 비교연산의 횟수는 데이터의 수에 해당하는 $$n$$라고 할 수 있다.
물론 피벗보다 하나 적은 $$n-1$$라고 해도 되지만 1은 무시해도 되는 작은 수기 때문에 $$n$$라고 한다.
이러한 비교연산의 횟수는 다음과 같이 정렬의 범위가 분할된 상태에서도 마찬가지다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/c856b166-4602-4955-bfc7-afe66836cd59/image.png" alt=""></p>
<p>반으로 나뉜 상태에서 각각의 low와 high가 각각의 방향으로 이동하는데 이때도 비교연산이 진행되니 비교연산의 횟수는 데이터의 수에 해당하는 $$n$$라고 할 수 있다.</p>
<p>이제 분할이 몇 단계에 걸쳐서 이뤄지는가를 생각해보자.
예시로 31개의 데이터를 대상으로 퀵 정렬을 진행한다고 생각해보자.
피벗이 항상 중간 값으로 결정되는 이상적인 경우라고 생각한다면 처음에 15개씩 두 개로 나뉠 것이고, 이어서 이들은 각각 다시 7개씩 두 개로 나뉠 것이다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/96459fcf-25f6-4aff-8125-2508b7c14e80/image.png" alt=""></p>
<p>이 횟수는 $$k = log_2n$$이 될 것이고
따라서 비교연산의 빅-오는 $$O(nlog_2n)$$이 된다.</p>
<p>하지만 이 빅-오는 이상적인 상황에서의 빅-오기 때문에 최악의 상황에서의 빅-오를 알아봐야하지 않을까?
최악의 경우 비교 연산의 횟수는 데이터 수와 동일하게 $$n$$가 된다.
이는 단순한 정렬 알고리즘에서 많이 봐 온 빅-오 복잡도이다.
하지만 퀵 정렬은 실제로 $$O(nlog_2n)$$의 복잡도를 갖는 다른 정렬 알고리즘과 비교했을 때에도 평균적으로 가장 빠른 것으로 알려져 있다.
그리고 그 이유를 퀵 정렬의 데이터 이동횟수가 상대적으로 적고, 병합 정렬과 같이 별도의 메모리 공간을 요구하지 않는다는 사실에서 알 수 있다.
따라서 다른 정렬 알고리즘 중에서 평균적으로 가장 빠른 정렬속도를 보이는 알고리즘이다.</p>
<h3 id="4-기수-정렬radix-sort"><em>(4) 기수 정렬(Radix Sort)</em></h3>
<p>기수 정렬 (radix sort)에서 기수(radix)란 주어진 데이터를 구성하는 기본 요소를 의미한다.
예를 들어서 2진수는 0과 1의 조합으로 데이터를 표현하니 2진수의 기수는 0과 1이다.
유사하게 10진수는 0과 9까지의 숫자가 10진수의 기수가 된다.
따라서, 기수 정렬은 데이터를 구성하는 기본 요소, 즉 기수를 이용해서 정렬을 진행하는 방식이다.
그리고 기수 정렬은 정렬순서상 앞서고 뒤섬의 판단을 위한 비교연산을 하지 않는다.</p>
<p>이게 어떤 의미일까?
비교연산은 정렬 알고리즘의 핵심이라 할 수 있다.
두 데이터 간의 정렬순서상 우선순위를 판단하기 위한 비교연산은 핵심중의 핵심이다.
따라서 앞에 배운 모든 정렬 알고리즘들은 이 연산을 포함하고 있고 알고리즘의 복잡도도 이 연산을 근거로 판단해왔다.</p>
<p>그리고 기수 정렬은 정렬 알고리즘의 이론상 성능의 한계인 $$O(nlog_2n)$$라는 빅-오 복잡도를 넘어설 수 있는 유일한 알고리즘이다.</p>
<p>단, 적용할 수 있는 범위가 제한적이란 단점이 있다.
예를 들어 배열에 저장된 1, 7, 9, 5, 2, 6을 오름차순으로 정렬하는 경우, 영단어 red, whe, zoo, box를 사전편찬 순서대로 정렬하는 경우 기수 정렬로 해결할 수 있다.</p>
<p>반면 배열에 저장된 21, -9, 125, 8, -136, 45를 오름차순으로 정렬하는 경우, 영단어 professionalism, few, hydroxproline, simple을 사전편찬 순서대로 정렬하는 경우 기수 정렬로 해결할 수 없다.</p>
<p>데이터의 길이가 같은 대상으로는 정렬이 가능하지만, 길이가 같지 않은 데이터들을 대상으로 기수 정렬의 적용이 어렵다.</p>
<p>물론 방법은 있겠지만 그렇게 하기 위해서는 데이터 가공을 위한 별도의 알고리즘이 필요하고 이 알고리즘의 적용으로 인한 효율 문제도 고려해야한다.
이렇게 될 경우 기수 정렬대신 다른 정렬을 적용하는 것이 훨씬 더 효율 적일 수도 있다.</p>
<h4 id="1-이해">1) 이해</h4>
<p>기수 정렬 과정에 대해 이해해보자.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/4ddf7b7e-29d4-4019-b6a8-2b78f3992976/image.png" alt=""></p>
<p>위 그림은 한자리 정수(10진수)에 대해 기수 정렬을 바탕으로 정렬되는 과정을 보여준다.
10진수 정수의 정렬을 위해서는 총 10개의 버킷(양동이)가 필요하고 버킷은 0부터 9까지 순서대로 이름이 매겨져 있다. 
정렬대상은 값에 해당하는 버킷으로 이동한다.
버킷으로의 이동이 끝났다면 버킷 0에 저장된 것부터 시작해서 버킷 9에 저장된 것까지 순서대로 꺼내서 차례로 나열만 하면 된다.
이것이 기수 정렬의 기본 원리다.</p>
<p>기수 정렬로 세 자릿수 10진수 정수들을 정렬할 때에도, 다섯 자리 정수들이라고 해도 버킷은 기수의 개수인 10개가 필요하다.
영단어를 정렬하려면 아스키 코드에서 영단어에 해당하는 만큼 버킷이 필요하다.
따라서 이 부분도 기수 정렬의 단점이 될 수 있다.</p>
<h4 id="2-lsd-vs-msd">2) LSD vs. MSD</h4>
<p>기수 정렬의 구현을 목적으로 다음 세 자리수 정수들을 대상으로 기수 정렬을 진행해보자.
오름차순으로 정렬하며 이 세자리 정수들은 10진수가 아닌 5진수로 표현되어 있다.
따라서 5개의 버킷만을 가지고 기수 정렬을 진행한다.</p>
<p>134, 224, 232, 122</p>
<p>지금부터 사용하는 방법을 가리켜 &#39;LSD 기수 정렬&#39;이라 한다.
&#39;LSD&#39;란 Least Significant Digit의 약자로 덜 중요한 자리수에서부터 정렬을 진행해 나간다는 의미다.
쉽게 말해 첫 번째 자리수부터 시작해서 정렬을 진행해 나간다.</p>
<table>
<thead>
<tr>
<th>No.</th>
<th>Pic.</th>
<th>Expl.</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/6763014f-e12a-4e2f-86ff-522be2254da0/image.png" alt=""></td>
<td>첫 번째 자리수를 기준으로 하여 134와 224를 순서대로 버킷 4에 넣고 이어서 232와 122를 버킷 2에 넣기.<br>버킷 0에서부터 시작해 데이터를 꺼낸다. 하나의 버킷에 둘 이상의 데이터가 존재하는 경우 들어간 순서대로 꺼내기.<br>232, 122, 134, 224순으로 데이터 정렬.</td>
</tr>
<tr>
<td>2</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/b2747d06-b37c-45b6-9fed-7679fefa3f9c/image.png" alt=""></td>
<td>두 번째 자리수를 기준으로 하여 정렬 진행.<br>122, 224, 232, 134순으로 데이터 정렬.</td>
</tr>
<tr>
<td>3</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/af00f9aa-3459-4a6b-9dbf-b45a96fd4fb0/image.png" alt=""></td>
<td>마지막으로 세 번째 자리수를 기준으로 하여 정렬 진행.<br>122, 134, 224, 232로 정렬 완료.</td>
</tr>
</tbody></table>
<p>이 방법의 단점은 작은 자릿수에서 시작해서 가장 큰 자릿수까지 모두 비교를 해야 값의 대소를 판단할 수 있다.
비교 중간에 대소를 판단하는 것은 불가능하다.
가장 영향력이 큰 자릿수를 마지막에 비교하니 마지막까지 결과를 알 수 없는 것이 이 방법의 단점이다.</p>
<p>이번에는 LSD와 반대 방향으로 정렬을 진행하는 MSD 기수 정렬에 대해 알아보자.
&#39;MSD&#39;란 Most Significant Digit의 약자로써 가장 중요한 자릿수, 즉 가장 큰 자릿수에서부터 정렬을 진행하는 것이다.
그렇다면 LSD와 방향만 다르고 정렬의 과정이 같을까?
아니다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/7f958179-337d-4f4b-ad08-c9bdfeaee13d/image.png" alt=""></p>
<p>LSD와 같은 방식으로 정렬을 진행하면 위와 같이 정렬이 완료되지 않는다는 것을 알 수 있다.
MSD방식은 LSD 방식과 달리 첫 번째 과정만 거쳐도 대략적인 정렬의 결과가 눈에 보인다.
즉, LSD는 마지막에 가서 정렬순서를 판단하는 방식이고, MSD는 점진적으로 정렬을 완성해가는 방식이다.
MSD 방식의 가장 큰 장점은 반드시 끝까지 가지 않아도 되고 중간에 정렬이 완료될 수도 있다는 것이다.
하지만, 모든 데이터에 일괄적인 과정을 거치게 (재귀적 호출) 할 수 없다는 단점이 있다.
따라서 구현의 난이도가 LSD에 비해 상대적으로 높고 중간에 데이터가 잘 정렬되고 있는지 점검해야 하므로 성능의 이점도 반감될 수 있다.</p>
<h4 id="3-lsd-기준-구현">3) LSD 기준 구현</h4>
<p>일반적으로 기수 정렬이라 하면 LSD 방식을 기준으로 얘기한다.
구현의 편의성이 가장 큰 이유이기도 하면서 MSD의 경우 정렬의 과정에서 모든 데이터에 일괄적인 과정을 거치게 할 수 없기 때문에 추가적인 연산과 별도의 메모리가 요구된다는 점이 있기 때문에 LSD방식으로 기수 정렬을 구현한다.
(참고로 LSD와 MSD의 빅-오는 같다.)</p>
<p>양의 정수라면 그 길이에 상관없이 정렬의 대상에 포함시킬 수 있는 기수 정렬을 구현해보자.
예를 들어 42, 715와 같은 두 정수를 정렬한다고 가정한다면
처음에는 2와 5를 가지고, 두번째에는 4와 1을 가지고 정렬을 진행한다.
그리고 마지막으로 0과 7을 추출해서 정렬을 진행한다.
각 수의 각 자리수를 추출하는 것이 포인트다. 따라서 각 자리수의 추출방법은 다음과 같다.</p>
<p>∙ NUM으로부터 첫 번째 자리 숫자 추출 NUM / 1 % 10
∙ NUM으로부터 두 번째 자리 숫자 추출 NUM / 10 % 10
∙ NUM으로부터 세 번째 자리 숫자 추출 NUM / 100 % 10</p>
<p>위 수식을 바탕으로 한 기수 정렬 구현은 다음과 같다.
참고로 버킷은 그 구조가 큐에 해당하기 때문에 앞에서 배운 &#39;연결 리스트 기반의 큐&#39;를 활용했다.
(<a href="https://velog.io/@mingming_eee/datastructure-07#1-%ED%97%A4%EB%8D%94%ED%8C%8C%EC%9D%BC-%EC%A0%95%EC%9D%98listbasequeueh">ListBaseQueue.h</a>, <a href="https://velog.io/@mingming_eee/datastructure-07#2-%EC%86%8C%EC%8A%A4%ED%8C%8C%EC%9D%BC-%EC%A0%95%EC%9D%98listbasequeuec">ListBaseQueue.c</a>)</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &quot;ListBaseQueue.h&quot;

#define BUCKET_NUM 10

void RadixSort(int arr[], int num, int maxLen)
{
    // 매개변수 maxLen에는 정렬대상 중 가장 긴 데이터의 길이 정보가 전달
    Queue buckets[BUCKET_NUM];
    int bi;
    int pos;
    int di;
    int divfac = 1;
    int radix;

    // 총 10개의 버킷 초기화
    for(bi=0; bi&lt;BUCKET_NUM; bi++)
        QueueInit(&amp;buckets[bi]);

    // 가장 긴 데이터의 길이만큼 반복
    for(pos=0; pos&lt;maxLen; pos++)
    {
        // 정렬대상의 수만큼 반복
        for(di=0; di&lt;num; di++)
        {
            // N번째 자리의 숫자 추출
            radix = (arr[di]/divfac) % 10;

            // 추출한 숫자를 근거로 버킷에 데이터 저장
            Enqueue(&amp;buckets[radix], arr[di]);
        }

        // 버킷수만큼 반복
        for(bi=0, di=0; bi&lt;BUCKET_NUM; bi++)
        {
            // 버킷에 저장된 것 순서대로 다 꺼내서 다시 arr에 저장
            while(!QIsEmpty(&amp;buckets[bi]))
                arr[di++] = Dequeue(&amp;buckets[bi]);
        }

        // N번째 자리의 숫자 추출을 위한 피제수의 증가
        divfac *= 10;
    }
}

int main()
{
    int arr[7] = {13, 212, 14, 7141, 10987, 6, 15};

    int len = sizeof(arr)/sizeof(int);
    int i;

    RadixSort(arr, len, 5);

    for(i=0; i&lt;len; i++)
        printf(&quot;%d &quot;, arr[i]);
    printf(&quot;\n&quot;);

    return 0;
}

&gt; gcc .\ListBaseQueue.c .\RadixSort.c    
&gt; .\a.exe
&gt; 출력
6 13 14 15 212 7141 10987</code></pre>
<p>위 함수 RadixSort는 길이가 가장 긴 데이터의 길이 정보를 전달받도록 정의했다.
함수 내에서 길이 정보를 직접 계산해서 구현할 수도 있지만 불필요한 연산을 수반할 수 있기 때문에 직접 입력으로 진행했다.</p>
<h4 id="4-성능-평가">4) 성능 평가</h4>
<p>기수 정렬은 비교연산이 핵심이 아니다.
오히려 버킷으로의 데이터 삽입과 추출이 핵심이다.
따라서 이 정렬의 시간 복잡도는 삽입과 추출의 빈도수를 대상으로 결정해야 한다.
버킷을 대상으로 하는 데이터의 삽입과 추출을 한 쌍의 연산으로 묶으면 이 한 쌍의 연산이 수행되는 횟수는 $$maxLen × num$$이 된다.
정렬대상의 수가 $$n$$이고, 모든 정렬대상의 길이가 $$l$$이라 할 때, 시간 복답도에 대한 기수 정렬의 빅-오는 $$O(nl)$$이 된다.
이는 상수 $$l$$을 무시하고 $$O(n)$$로 봐도 되고 퀵 정렬의 복잡도인 $$O(nlog_2n)$$보다 뛰어난 성능임을 알 수 있다. (물론 적용 가능한 대상이 제한적이라는 단점이 있지만!)</p>
<hr>
<h2 id="reveiw"><code>&lt;Reveiw&gt;</code></h2>
<p>이렇게 다양한 정렬 알고리즘에 대해서도 알아봤다.
더 다양한 정렬 방법이 있지만 내가 아는 c언어 수준에서는 이정도까지 적당한 것 같다 ㅎㅎ
파이썬으로 구현한 다양한 정렬도 궁금하다면 <a href="https://velog.io/@mingming_eee/data-structure-with-python-6">이 글📚</a>을 읽어보는 것도 추천한다.</p>
<p>이제 본격적으로 탐색(Search)를 시작해보자!</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/103b2636-760c-4e9f-bbb6-d6525ec8da29/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chapter 09. 우선순위 큐(Priority Queue)와 힙(Heap)]]></title>
            <link>https://velog.io/@mingming_eee/datastructure-09</link>
            <guid>https://velog.io/@mingming_eee/datastructure-09</guid>
            <pubDate>Tue, 12 Nov 2024 06:44:53 GMT</pubDate>
            <description><![CDATA[<h2 id="09-1-우선순위-큐의-이해">09-1. 우선순위 큐의 이해</h2>
<p>이름만 보았을 때 우선순위 큐는 큐에서 확장하는 수준이 될 것 같지만 실제로 &#39;큐&#39;의 구현과 &#39;우선순위 큐&#39;의 구현에는 많은 차이가 있다.</p>
<h3 id="우선순위-큐와-우선순위"><em>우선순위 큐와 우선순위</em></h3>
<p>&#39;큐&#39;의 핵심 연산은 큐에 데이터를 삽입하는 <code>enqueue</code>와 큐에서 데이터를 꺼내는 <code>dequeue</code>가 있다.
&#39;우선순위 큐&#39;에서도 핵심 연산이 동일하게 <code>enqueue</code>와 <code>dequeue</code>다.</p>
<p>하지만, 연산의 결과에 차이가 있다.
&#39;큐&#39;의 연산의 결과로는 먼저 들어간 데이터가 먼저 나오지만, &#39;우선순위 큐
의 경우 <strong>들어간 순서에 상관없이 우선순위가 높은 데이터가 먼저</strong> 나오게된다.</p>
<p>우선순위 큐에 저장되는 데이터들은 모두 우선순위를 &#39;지니는&#39;것이 아닌 데이터를 근거로 우선순위를 판단할 수 있어야한다.
이 우선순위는 프로그래머가 결정할 수 있으며 목적에 맞게 우선순위를 결정하면 된다.
우선순위는 정수로 표현되기도 하며 정수값이 클수록 우선순위가 높은건지 낮을수록 우선순위가 높은건지도 결정하기 나름이다.
우선순위가 같은 데이터가 존재할 수도 있고 우선순위가 서로 다른 데이터들만 저장된다면 자료구조로 활용할 수 있는 범위가 제한적이게 된다.</p>
<h3 id="우선순위-큐의-구현-방법"><em>우선순위 큐의 구현 방법</em></h3>
<p>우선순위 큐를 구현하는 방법은 다음 세 가지가 있다.</p>
<ul>
<li>배열을 기반으로 구현하는 방법</li>
<li>연결 리스트를 기반으로 구현하는 방법</li>
<li>힙(heap)을 이용하는 방법</li>
</ul>
<p>배열이나 연결 리스트를 이용하면 우선순위 큐를 매우 간단히 구현할 수 있다.
다음 그림에서 저장된 숫자는 데이터인 동시에 우선순위 정보라고 가정한다.
숫자 1이 가장 높은 우선순위를 뜻하며 이보다 값이 커질수록 우선순위는 낮아진다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/47b1c323-aec9-4b00-8a39-2aae843f0cbe/image.png" alt=""></p>
<p>배열의 경우 데이터의 우선순위가 높을수록 배열의 앞쪽에 데이터를 위치시킨다. 이렇게 하면 우선수위가 높은 데이터를 반환 및 소멸하는 것이 보다 쉽다.
하지만 이렇게 배치할 경우 데이터를 삽입 및 삭제하는 과정에서 데이터를 한 칸씩 뒤로 밀거나 한 칸씩 앞으로 당기는 연산을 수반해야 한다.
그리고 우선순위가 가장 낮은 데이터를 저장하는 경우 삽입의 위치를 찾기 위해서 배열에 저장된 모든 데이터와 우선수위의 비교를 진행해야 할 수도 있는 최악의 상황이 벌어지기도 한다.</p>
<p>연결 리스트의 경우 배열의 첫 번째 단점은 갖지 않는다.
두 번째 단점은 연결 리스트에도 존재한다.
삽입의 위치를 찾기 위해서 첫 번째 노드에서부터 시작해서 마지막 노드에 저장된 데이터와 우선순위의 비교를 진행해야 할 수도 있다.
이 단점은 데이터의 수가 적은 경우 크게 문제되지 않을 수 있다.
하지만 데이터의 수가 많아지면, 연결된 노드의 수가 많아지면 노드의 수에 비례해서 성능을 저하시키는 주원인이 된다.
그래서 우선순위 큐는 단순 배열도, 연결 리스트도 아닌 &#39;힙&#39;이라는 자료구조를 이용해서 구현하는 것이 일반적이다.</p>
<h3 id="힙heap"><em>힙(Heap)</em></h3>
<p>우리는 이전에도 연결 리스트를 기반으로 스택을 구현한 적이 있다.
우선순위 큐도 &#39;힙&#39;이라는 자료구조를 이용해 구현하고자 한다.</p>
<p>그렇다면 <code>힙(Heap)</code>이란 무엇일까?
힙은 &#39;이진 트리&#39;이되 &#39;완전 이진 트리&#39;다.
그리고 모든 노드에 저장된 값은 자식 노드에 저장된 값보다 크거나 같아야 한다.
즉, 루트 노드에 저장된 값이 가장 커야 한다.
여기서 말하는 값이란 말 그래도 &#39;값&#39;일 수도, &#39;우선순위&#39;가 될 수도 있다.
하지만 힙을 기반으로 우선순위 큐를 구현할 때는 &#39;우선순위&#39;가 값이 된다.
힙을 보여주는 간단한 예시 트리가 다음과 같이 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/1fa7749b-a732-44c7-a544-606dc27bc877/image.png" alt=""></p>
<p>위와 같이 루트 노드로 올라갈수록 저장된 값이 커지는 완전 이진 트리를 가리켜 <code>최대 힙(max heap)</code>이라 한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/cf60eed8-846c-4e08-9d72-e0c65b339aba/image.png" alt=""></p>
<p>그리고 위 그림과 같이 루트 노드로 올라갈수록 저장된 값이 작아지는 완전 이진 트리를 가리켜 <code>최소 힙(min heap)</code>이라 한다.</p>
<p>이렇듯 힙은 루트 노드에 우선순위가 가장 높은 데이터를 위치시킬 수 있는 자료구조기 때문에 이를 기반으로 하면 우선순위 큐를 간다히 구현할 수 있다.
(힙 자체의 의미는 &#39;차곡차곡 무엇인가를 쌓아 올린 더미&#39;로 이 트리 모양과 흡사해 지어진 이름이다.)</p>
<hr>
<h2 id="09-2-힙의-구현과-우선순위-큐의-완성">09-2. 힙의 구현과 우선순위 큐의 완성</h2>
<p>힙의 구현은 곧 우선순위 큐의 완성이다.
따라서 힙과 우선순위 큐를 동일하게 인식할 수 있다.
하지만, 동일하지 않으며 우선순위 큐와 힙을 어느정도는 구분할줄 알아야 한다.
힙은 우선순위 큐의 구현에 어울리는 완전 이진 트리의 일종이라는 것을 알아야한다.⭐</p>
<h3 id="0-힙의-구현"><em>0. 힙의 구현</em></h3>
<p>힙을 구현한고 이를 기반으로 우선순위 큐를 구현하고자 한다.
힙의 구현을 위해서는 데이터의 저장과 삭제 방법을 먼저 알아야 한다.</p>
<h4 id="1-힙에서의-데이터-저장과정">1) 힙에서의 데이터 저장과정</h4>
<p>데이터의 저장과정을 &#39;최소 힙&#39;을 기준으로 배워보자.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/30e8a554-9fd7-4593-9193-c99c453c835c/image.png" alt=""></p>
<p>위 그림에 쓰여있는 숫자를 데이터 겸 우선순위라 하자.
그리고 숫자가 작을수록 우선순위가 높다고 가정한다.
그렇다면 위 트리는 우선순위 관점에서 힙이 맞고 완전 이진 트리면서 어느 위치든 <code>자식 노드 데이터의 우선순위 ≤ 부모 노드 데이터의 우선순위</code>가 성립한다.</p>
<p>이 상황에서 3을 저장한다고 생각해보자.
저장한 후에도 트리의 우선순위 관계를 유지하는 것이 관건이다.
문제 해결을 위한 알고리즘이 딱 떠올리기가 어렵다.
이렇게 생각해보자.
&quot;새로운 데이터는 우선순위가 제일 낮다는 가정하에서 마지막 위치에 저장한다. 그리고 부모 노드와 우선순위를 비교해서 위치가 바껴야 한다면 바꿔준다. 바뀐 다음에도 올바른 위치에 도달할 때 까지 계속해서 부모 노드와 비교한다.&quot;
여기에서 말하는 &#39;마지막 위치&#39;는 노드를 추가한 이후에도 완전 이진 트리가 유지되는, 마지막 레벨의 가장 오른쪽 위치를 뜻한다.
위에서 언급한 방법을 그림과 함께 살펴보자.</p>
<table>
<thead>
<tr>
<th>No.</th>
<th>Pic.</th>
<th>Expl.</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/24d25ce5-9959-4a74-b190-777685e339c1/image.png" alt=""></td>
<td>마지막 노드에 새 노드를 추가하고 부모 노드와 우선순위 비교.<br>두 노드의 위치 바뀔 필요 있음.</td>
</tr>
<tr>
<td>2</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/00f06a4f-11ff-4f95-a989-3f8041bf5f46/image.png" alt=""></td>
<td>두 노드의 위치가 바뀐 이후 이어서 다시 부모 노드와 비교.<br>부모 노드보다 우선순위 높으므로 바뀔 필요 있음.</td>
</tr>
<tr>
<td>3</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/d9d553f6-8171-460c-9745-1c533c4891e9/image.png" alt=""></td>
<td>두 노드의 위치가 바뀐 이후 부모 노드와 비교.<br>부모 노드의 우선순위가 더 높으므로 비교 종료.</td>
</tr>
</tbody></table>
<p>최종적으로 힙의 조건을 만족하여 새로운 데이터를 넣는데 성공했다.
이렇듯 데이터의 추가과정은 마지막 위치에 데이터를 두고서 부모 노드와의 비교를 통해 자신의 위치를 찾아가는 매우 단순한 방식이다.</p>
<h4 id="2-힙에서의-데이터-삭제과정">2) 힙에서의 데이터 삭제과정</h4>
<p>삭제과정은 저장과정보다 까다로울 것이 예상된다.
만약 가장 높은 우선순위의 데이터를 삭제한다고 생각해보자.
힙에서 루트 노드를 삭제하는 것이므로 루트 노드 자체를 삭제하는 것은 어렵지 않지만 삭제 이후에도 힙의 구조를 유지하는 것을 어떻게 할지 고민해야한다.
루트 노드를 삭제한 이후 어떻게 이 부분을 채울 것인가...?
다음 그림과 순서를 보면서 삭제과정에 대해 이해해보자.</p>
<table>
<thead>
<tr>
<th>No.</th>
<th>Pic.</th>
<th>Expl.</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/5dad5545-0f24-4616-a30a-16fc3f2168c0/image.png" alt=""></td>
<td>우선순위가 가장 높은 데이터 1이 담긴 노드를 삭제.</td>
</tr>
<tr>
<td>2</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/43992c02-9a00-469d-8d15-cdc8e7d9b74b/image.png" alt=""></td>
<td>빈 루트 노드에 마지막 노드를 옮긴 다음 자식 노드와 비교.⭐<br>비교를 통한 제자리 찾는 과정 반복.</td>
</tr>
<tr>
<td>3</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/28d173ec-e09d-43f0-9541-8834fe98720a/image.png" alt=""></td>
<td>두 개의 자식 중 우선순위가 높은 3이 저장된 왼쪽 자식 노드와 8이 저장된 노드 비교 후 교환.<br>오른쪽 자식 노드의 우선순위가 높다면 오른쪽 자식 노드와 교환.<br>우선순위가 낮은 자식 노드와 교환하게 될 경우 힙의 기본 조건이 무너짐.⭐</td>
</tr>
<tr>
<td>4</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/7a1708c8-b296-4891-8198-78f02b960e3c/image.png" alt=""></td>
<td>숫자 8과 자식 노드를 비교.<br>4와 9 중에서 우선순위가 높은 4와 비교를 진행.<br>4가 8보다 우선순위 높으므로 교환 진행.<br>8과 자식 노드인 15 비교.<br>8의 우선순위가 더 높으므로 삭제과정 마무리.</td>
</tr>
</tbody></table>
<h4 id="3-삽입과-삭제의-과정에서-보인-성능-평가">3) 삽입과 삭제의 과정에서 보인 성능 평가</h4>
<p>우선순위 큐의 구현에 있어서 단순 배열이나 연결 리스트보다 힙이 더 적합한 이유가 뭘까?
시간 복잡도로 이를 비교해보자.
(최악의 경우를 생각해야 하므로 우선순위가 낮은 데이터를 저장하는 경우를 생각한다.)</p>
<ul>
<li><p>배열 기반의 우선순위 큐</p>
<ul>
<li>데이터 저장의 시간 복잡도: $$O(n)$$
배열에 저장된 모든 데이터와의 우선순위 비교과정을 거쳐야한다.</li>
<li>데이터 삭제의 시간 복잡도: $$O(1)$$
맨 앞에 저장된 데이터를 삭제하면 된다.</li>
</ul>
</li>
<li><p>연결 리스트 기반의 우선순위 큐</p>
<ul>
<li>데이터 저장의 시간 복잡도: $$O(n)$$
저장된 모든 데이터와의 우선순위 비교과정을 거쳐야한다.</li>
<li>데이터 삭제의 시간 복잡도: $$O(1)$$
맨 앞에 저장된 데이터를 삭제하면 된다.</li>
</ul>
</li>
<li><p>힙 기반의 우선순위 큐</p>
<ul>
<li>데이터 저장의 시간 복잡도: $$O(log_2n)$$</li>
<li>데이터 삭제의 시간 복잡도: $$O(log_2n)$$</li>
</ul>
</li>
</ul>
<p>힙 기반의 우선순위 큐의 경우 삽입이나 삭제의 경우 동반되는 비교연산은 주로 부모 노드와 자식 노드 사이에 일어나기 때문에 트리의 높이에 해당하는 수만큼만 비교연산을 진행하면 된다.
힙은 완전 이진 트리이므로, 힙에 저장할 수 있는 데이터의 수는 트리의 높이가 하나 늘 때마다 두 배씩 증가한다.
때문에 데이터의 수가 두 배 늘 때마다, 비교연산의 횟수는 1회 증가한다.
시간 복잡도 $$O(n)$$와 $$O(log_2n)$$의 차이는 데이터의 수가 많아질수록 그 성능 차이가 크기 때문에 힙 기반의 우선순위 큐가 더 효율적이다라고 할 수 있다.</p>
<h4 id="4-힙-구현에-어울리는-자료구조">4) 힙 구현에 어울리는 자료구조</h4>
<p>우선순위 큐의 구현에는 힙이 어울리는 것으로 결론이 났다.
힙의 구현방법은 어떨까?
트리를 구현하는 방법에 배열 또는 연결 리스트로 구현했기 때문에 둘 중 하나를 선택하면 된다.
앞서 배울 땐 연결 리스트를 기반으로 트리를 구현했으니 힙도 연결 리스트 기반으로 구현될 것이라 생각할 수 있다.
하지만 <strong>완전 이진 트리 구조를 갖고 있고 그 구조를 유지해야하기 때문에 &#39;힙&#39;은 &#39;배열&#39;기반으로 구현해야 한다.</strong>
연결 리스트를 기반으로 힙을 구현하면 새로운 노드를 힙의 &#39;마지막 위치&#39;에 추가하는 것이 쉽지 않기 때문이다.</p>
<h4 id="5-배열-기반-힙-구현에-필요한-지식들">5) 배열 기반 힙 구현에 필요한 지식들</h4>
<p>Chapter 08에서 배열을 기반으로 트리를 구성하는 방법에 대해 살짝 언급한 적이 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/047860d3-ace8-43aa-8250-f26c8ced27c8/image.png" alt=""></p>
<p>위 그림과 같이 노드에 고유 번호를 부여하여 그 번호가 각 노드의 데이터가 저장 될 배열의 인덱스 값이 되는 것이다.
구현의 편의를 위해서 인덱스가 0인 위치의 배열 요소는 사용하지 않았다.
여기서 배열을 기반으로 힙을 구현하기 위해 더 알아야할 것은 뭘까?
왼쪽 그리고 오른쪽 자식 노드의 인덱스 값을 얻는 방법, 그리고 부모 노드의 인덱스 값을 얻는 방법이다.
자식 노드의 인덱스 값을 얻는 방법은 데이터의 삭제를 위해서,
부모 노드의 인덱스 값을 얻는 방법은 데이터의 추가(저장)을 위해서 필요하다.
다음 식으로 인덱스 값을 얻을 수 있다.</p>
<ul>
<li>왼쪽 자식 노드의 인덱스 값: (부모 노드의 인덱스 값)$$× 2$$</li>
<li>오른쪽 자식 노드의 인덱스 값: (부모 노드의 인덱스 값)$$× 2 + 1$$</li>
<li>부모 노드의 인덱스 값: (자식 노드의 인덱스 값)$$÷ 2$$</li>
</ul>
<p>이진 트리는 레벨이 증가함에 따라서 추가할 수 있는 자식 노드의 수가 2배씩 증가하므로 2를 나누고 곱하는 방식으로 부모 노드와 자식 노드의 인덱스 값을 구할 수 있다.
여기서 주의해야할 점은 부모 노드의 인덱스 값을 구하기 위한 나눗셈 연산은 몫 구하기(정수형 나눗셈)이다.</p>
<h3 id="1-원리-이해-중심의-힙-구현"><em>1. 원리 이해 중심의 힙 구현</em></h3>
<p>위에서 이해한 원리를 중심으로 힙을 먼저 알아보자.
그리고 보다 사용하기 좋은, 합리적인 형태로 변경할 예정이다.</p>
<h4 id="1-헤더파일-정의-simpleheaph">1) 헤더파일 정의 (SimpleHeap.h)</h4>
<pre><code class="language-c">#ifndef __SIMPLE_HEAP_H__
#define __SIMPLE_HEAP_H__

#define TRUE    1
#define FALSE   0

#define HEAP_LEN    100

typedef char HData;
typedef int Priority;

typedef struct _heapElem
{
    Priority pr;    // 값이 작을수록 높은 우선순위
    HData data;
} HeapElem;

typedef struct _heap
{
    int numOfData;
    HeapElem heapArr[HEAP_LEN];
} Heap;

void HeapInit(Heap * ph);
int HIsEmpty(Heap * ph);

void HInsert(Heap * ph, HData data, Priority pr);
HData HDelete(Heap * ph);

#endif</code></pre>
<p>위의 헤더파일은 순수한 힙의 구현을 위한 헤더파일이 아닌 우선순위 큐의 구현을 염두에 두고 정의한 헤더파일이다.
이는 <code>HeapElem</code> 구조체의 정의에서 알 수 있다.
힙에 저장될 데이터의 모델을 정의한 구조체로 우선수위 정보를 별도로 담을 수 있도록 정의되어 있다.
이는 우선순위 큐의 구현을 고려했다는 것이다.
따라서 위의 헤더파일에 선언된 <code>HDelete</code>함수는 우선순위 큐와 마찬가지로 데이터의 삽입 순서에 상관없이 우선순위에 근거하여 삭제가 이뤄질 수 있도록 정의할 것이다.</p>
<h4 id="2-소스파일-정의-simpleheapc">2) 소스파일 정의 (SimpleHeap.c)</h4>
<p>헤더파일에 선언된 함수들을 정의해보자.
이에 앞서 힙에 대한 몇 가지 사실들에 대해 정리하고 가자.</p>
<ul>
<li>힙은 완전 이진 트리.</li>
<li>힙의 구현은 배열 기반으로 하며 인덱스가 0인 요소 비워두기.</li>
<li>힙에 저장된 노드의 개수와 마지막 노드의 고유번호는 일치.</li>
<li>노드의 고유번호가 노드가 저장되는 배열의 인덱스 값.</li>
<li>우선순위를 나타내는 정수 값이 작을수록 높은 우선순위. (가정)</li>
</ul>
<p>그리고 소스파일은 다음과 같이 정의된다.</p>
<pre><code class="language-c">#include &quot;SimpleHeap.h&quot;

// 힙의 초기화
void HeapInit(Heap * ph)
{
    ph-&gt;numOfData = 0;
}

// 힙이 비었는지 확인
int HIsEmpty(Heap * ph)
{
    if(ph-&gt;numOfData == 0)
        return TRUE;
    else
        return FALSE;
}

// 부모 노드의 인덱스 값 반환
int GetParentIDX(int idx)
{
    return idx/2;
}

// 왼쪽 자식 노드의 인덱스 값 반환
int GetLChildIDX(int idx)
{
    return idx*2;
}

// 오른쪽 자식 노드의 인덱스 값 반환
int GetRChildIDX(int idx)
{
    return GetLChildIDX(idx)+1;
}

// 두 개의 자식 노드 중 높은 우선순위의 자식 노드 인덱스 값 반환
int GetHiPriChildIDX(Heap * ph, int idx)
{
    if(GetLChildIDX(idx) &gt; ph-&gt;numOfData)
        return 0;
    else if(GetLChildIDX(idx) == ph-&gt;numOfData)
        return GetLChildIDX(idx);
    else
    {
        if(ph-&gt;heapArr[GetLChildIDX(idx)].pr &gt; ph-&gt;heapArr[GetRChildIDX(idx)].pr)
            return GetRChildIDX(idx);
        else
            return GetLChildIDX(idx);
    }
}

// 힙에 데이터 저장
void HInsert(Heap * ph, HData data, Priority pr)
{
    int idx = ph-&gt;numOfData+1;
    HeapElem nelem = {pr, data};

    while(idx != 1)
    {
        if(pr &lt; (ph-&gt;heapArr[GetParentIDX(idx)].pr))
        {
            ph-&gt;heapArr[idx] = ph-&gt;heapArr[GetParentIDX(idx)];
            idx = GetParentIDX(idx);
        }
        else
            break;
    }

    ph-&gt;heapArr[idx] = nelem;
    ph-&gt;numOfData += 1;
}

// 힙에서 데이터 삭제
HData HDelete(Heap * ph)
{
    HData retData = (ph-&gt;heapArr[1].data);
    HeapElem lastElem = ph-&gt;heapArr[ph-&gt;numOfData];

    int parentIdx = 1;
    int childIdx;

    while(childIdx = GetHiPriChildIDX(ph, parentIdx))
    {
        if(lastElem.pr &lt;= ph-&gt;heapArr[childIdx].pr)
            break;
        ph-&gt;heapArr[parentIdx] = ph-&gt;heapArr[childIdx];
        parentIdx = childIdx;
    }

    ph-&gt;heapArr[parentIdx] = lastElem;
    ph-&gt;numOfData -= 1;
    return retData;
}</code></pre>
<p>힙에서 데이터를 저장하고 삭제하는 함수를 설명하기 이전에 <code>GetHiPriChildIDX</code> 함수 구현에 대해 먼저 자세히 살펴보자.</p>
<pre><code class="language-c">int GetHiPriChildIDX(Heap * ph, int idx)
{
    if(GetLChildIDX(idx) &gt; ph-&gt;numOfData)
        return 0;
    else if(GetLChildIDX(idx) == ph-&gt;numOfData)
        return GetLChildIDX(idx);
    else
    {
        if(ph-&gt;heapArr[GetLChildIDX(idx)].pr &gt; ph-&gt;heapArr[GetRChildIDX(idx)].pr)
            return GetRChildIDX(idx);
        else
            return GetLChildIDX(idx);
    }
}</code></pre>
<p>이 함수에 노드의 인덱스 값을 전달하면, 이 노드의 두 자식 노드 중에서 우선순위가 높은 것의 인덱스 값을 반환한다. (else문에 해당하는 부분)</p>
<p>이 함수는 인자로 전달된 노드에 자식 노드가 없으면 0을 반환하고 자식 노드가 하나인 경우에는 그 노드의 인덱스 값을 반환한다. (if와 else if문에 해당하는 부분)</p>
<p>위의 함수에서는 자식 노드가 하나도 존재하지 않는 상황을 <code>ph-&gt;numOfData</code>와 비교한 연산문을 통해 확인하고 있는데 이 부분이 이해가 잘 안 갈 수 있다.
힙은 완전 이진 트리이므로 오른쪽 자식 노드만 있는 상황이 발생하지 않는다.
따라서 왼쪽 자식 노드가 없다면 자식 노드가 존재하지 않는 것으로 판단하는 것이다.
자식 노드가 하나도 없는 노드는 단말 노드가 되고 단말 노드의 왼쪽 자식 노드의 인덱스 값은 힙에 저장된 노드의 수를 넘어선다.
따라서 <code>ph-&gt;numOfData</code>가 더 크다면 단말 노드가 되는 것이다.
그 다음 else if 문의 연산문에서 자식 노드가 하나일 경우 힙은 완전 이진 트리이므로 그 자식 노드는 왼쪽 자식 노드가 되고 힙의 마지막 노드가 된다.</p>
<p>그 다음 데이터 저장보다 데이터 삭제 함수(<code>HDelete</code>)함수에 대해 먼저 알아보자.
데이터 삭제과정은 &quot;힙의 마지막 노드를 루트 노드의 위치에 올린 다음 자식 노드와의 비교과정을 거쳐 자신의 위치를 찾을 때까지 내린다.&quot;이다.
여기서 루트 노드로 올려진 마지막 노드는 자신의 위치를 찾을 때까지 아래로 이동하면서 자신의 위치를 찾아가는데 이러한 빈번한 이동을 코드에 그대로 담을 필요가 없다.
최종 목적지가 결정되면 그 곳으로 한번에 옮기면 되는 것이다.
따라서 위에서 그림과 함께 설명했던 것과 코드 구현에서 살짝 다른 점이 있지만 개념은 동일하다.
코드에 주석을 추가하여 다시 한번 자세히 살펴보자.</p>
<pre><code class="language-c">HData HDelete(Heap * ph)
{
    HData retData = (ph-&gt;heapArr[1].data);    // 반환을 위한 삭제할 데이터 저장
    HeapElem lastElem = ph-&gt;heapArr[ph-&gt;numOfData];    // 힙의 마지막 노드 저장

    // 아래의 변수 parentIdx에는 마지막 노드가 저장될 위치 정보가 저장됨.
    int parentIdx = 1;    // 루트 노드가 위치해야 할 인덱스 값 저장 (시작)
    int childIdx;

    // 루트 노드의 우선순위가 높은 자식 노드를 시작으로 반복문 시작
    while(childIdx = GetHiPriChildIDX(ph, parentIdx))
    {
        if(lastElem.pr &lt;= ph-&gt;heapArr[childIdx].pr)    // 마지막 노드와 우선순위 비교
            break;    // 마지막 노드의 우선순위가 높은 경우 반복문 탈출

        // 마지막 노드보다 우선수위 높으니 비교대상 노드의 위치를 한 레벨 올리기
        ph-&gt;heapArr[parentIdx] = ph-&gt;heapArr[childIdx];
        // 마지막 노드가 저장될 위치 정보는 한 레벨 내리기
        parentIdx = childIdx;
    }    // 반복문을 탈출하면 parentIdx에는 마지막 노드의 위치 정보가 저장됨

    ph-&gt;heapArr[parentIdx] = lastElem;    // 마지막 노드 최종 저장
    ph-&gt;numOfData -= 1;    // 노드 갯수 하나 삭제
    return retData;    // 삭제된 노드 반환
}</code></pre>
<p>따라서 함수 <code>HDelete</code>에서는 마지막 노드가 있어야 할 위치를 parentIdx에 저장된 인덱스 값을 갱신해가며 찾아가고 있다.
<code>HDelete</code>함수가 호출되면서 변수 parentIdx는 1로 초기화된다.
1은 루트 노드의 인덱스 값이므로 변수 parentIdxfmf 1로 초기화한 것은 마지막 노드를 루트 노드로 옮긴 상황으로 이해할 수 있다.
그리고 while문의 if문 다음에 위치한 두 문장의 실행은, 루트 노드로 옮겨진 마지막 노드와 우선순위가 높은 자식 노드와의 교환이 이뤄지는 상황으로 이해할 수 있다.</p>
<p>마지막으로 데이터 저장과정을 다룬 함수 <code>HInsert</code>에 대해 알아보자.
힙에서의 데이터 저장은 새로운 데이터는 우선순위가 가장 낮다는 가정하에 마지막 위치에 저장하고 부모 노드와의 우선순위 비교를 통해 자신의 위치를 찾을 때까지 위로 올린다.
<code>HDelete</code>함수 구현과 마찬가지로 <code>HInsert</code>함수 구현에서는 새로운 데이터가 담긴 노드를 매과정 옮길 필요가 없다.
코드에 주석을 추가하여 다시 한번 자세히 살펴보자.</p>
<pre><code class="language-c">void HInsert(Heap * ph, HData data, Priority pr)
{
    int idx = ph-&gt;numOfData+1;        // 새 노드가 저장될 인덱스 값을 idx에 저장
    HeapElem nelem = {pr, data};    // 새 노드의 생성 및 초기화

    // 새 노드가 저장될 위치가 루트 노드의 위치가 아니라면 while문 반복
    while(idx != 1)
    {    
        // 새 노드와 부모 노드의 우선순위 비교
        if(pr &lt; (ph-&gt;heapArr[GetParentIDX(idx)].pr))    // 새 노드의 우선순위가 높다면
        {
            // 부모 노드를 한 레벨 내리기
            ph-&gt;heapArr[idx] = ph-&gt;heapArr[GetParentIDX(idx)];

            // 새 노드를 한 레벨 올리기
            idx = GetParentIDX(idx);
        }
        else    // 새 노드의 우선순위가 높지 않다면
            break;
    }

    ph-&gt;heapArr[idx] = nelem;    // 새 노드를 배열에 저장
    ph-&gt;numOfData += 1;
}</code></pre>
<p>위 함수에서도 새로운 노드가 저장되어야 할 위치 정보를 변수 idx를 통해서 계속 갱신해 나가고 있다.
따라서 앞서 그림을 통해서 설명한 노드의 삽입과정을 그대로 코드로 옮긴 것으로 볼 수 있다.</p>
<h4 id="3-실행파일-정의-simpleheapmainc--리뷰">3) 실행파일 정의 (SimpleHeapMain.c) + 리뷰</h4>
<p>위에서 구현한 힙의 테스트를 위해 실행파일을 정의해보자.
그리고 개선해야 할 부분에 대해 알아보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &quot;SimpleHeap.h&quot;

int main()
{
    Heap heap;
    HeapInit(&amp;heap);            // 힙의 초기화

    HInsert(&amp;heap, &#39;A&#39;, 1);     // 문자 &#39;A&#39;를 우선순위 1로 저장
    HInsert(&amp;heap, &#39;B&#39;, 2);     // 문자 &#39;B&#39;를 우선순위 2로 저장
    HInsert(&amp;heap, &#39;C&#39;, 3);     // 문자 &#39;C&#39;를 우선순위 3로 저장
    printf(&quot;%c \n&quot;, HDelete(&amp;heap));

    HInsert(&amp;heap, &#39;A&#39;, 1);     // 문자 &#39;A&#39; 한번 더 저장
    HInsert(&amp;heap, &#39;B&#39;, 2);     // 문자 &#39;B&#39; 한번 더 저장
    HInsert(&amp;heap, &#39;C&#39;, 3);     // 문자 &#39;C&#39; 한번 더 저장
    printf(&quot;%c \n&quot;, HDelete(&amp;heap));

    while(!HIsEmpty(&amp;heap))
        printf(&quot;%c \n&quot;, HDelete(&amp;heap));

    return 0;
}

&gt; gcc .\SimpleHeap.c .\SimpleHeapMain.c
&gt; .\a.exe
&gt; 출력
A 
A 
B
B
C
C</code></pre>
<p>실행 결과, 우선순위가 높은 문자들이 먼저 꺼내져서 구현한 힙이 잘 작동되고 있다고 생각할 수 있다.
다만, 약간의 부족한 점이 있다.</p>
<pre><code class="language-c">typedef struct _heapElem
{
    Priority pr;    // typedef int Priority
    HData data;     // typedef char HData
}</code></pre>
<p>위 구조체는 힙을 이루는 노드에 관한 것인데 구조체의 멤버로 우선순위 정보를 담는 변수가 선언되어 있다. 이것이 문제가 될 수 있다.
예를 들어 데이터 저장을 위한 함수 <code>HInsert</code>에서 <code>void HInsert(Heap * ph, HData data, Priority pr);</code>라는 함수 선언을 보면 데이터의 우선순위 정보도 넘겨줌으로써 작동되는 거을 볼 수 있다.
데이터를 입력하기 전에 프로그래머가 알아서 우선순위를 결정하고 그 값을 전달해줘야 한다는 것이다.
우선순위라는 것은 데이터를 기준으로 결정되는 것이 대부분인데 우선순위의 결정 기준을 세우는 것이 아니고 일일이 프로그래머가 우선순위를 매기는 것은 데이터 양이 많아질수록 효율이 떨어지고 불편할 것이다.
이 부분을 develop할 필요가 있다.</p>
<h3 id="2-사용-가능-수준의-힙-구현"><em>2. 사용 가능 수준의 힙 구현</em></h3>
<p>앞서 구현한 힙의 부족한 점을 해결해보자.
프로그래머가 우선순위의 판단 기준을 힙에 설정해야한다.
이는 힙의 적용 범위와 활용 방법이 넓어짐을 의미한다.
앞서 힙의 구현을 위한 구조체는 다음과 같았다.</p>
<pre><code class="language-c">typedef struct _heapElem
{
    Priority pr;
    HData data;
} HeapElem;

typedef struct _heap
{
    int numOfData;
    HeapElem heapArr[HEAP_LEN];
} Heap;</code></pre>
<p>우선순위의 판단 기준을 힙에 설정할 수 있어야한다는 요구사항을 만족하기 위해서는
다음과 같이 하나의 구조체로 정의하면 된다.</p>
<pre><code class="language-c">typedef struct _heap
{
    PriorityComp * comp;        // typedef int (*ProirityComp)(HData dq, HData d2);
    int numOfData;
    HData heapArr[HEAP_LEN];    // typedef char HData;
} Heap;</code></pre>
<p><code>HeapElem</code>이라는 구조체가 사라지고 우선순위의 높고 낮음을 판단할 수 있는 함수 포인터 변수를 <code>Heap</code>구조체 안에 넣었다.
두 개의 데이터를 대상으로 우선순위의 높고 낮음을 판단할 수 있는 함수를 등록할 예정이다.
그리고 여기에 등록할 함수는 프로그래머가 직접 정의해야 ㅎ한다.
예시로 아래 가이드라인대로 함수를 작성할 것이다.</p>
<ul>
<li>PriorityComp의 typedef 선언<ul>
<li><code>typedef int (PriorityComp)(HData d1, HData d2);</code></li>
<li>첫 번째 인자의 우선순위가 높다면 0보다 큰 값 반환</li>
<li>두 번째 인자의 우선순위가 높다면 0보다 작은 값 반환</li>
<li>첫 번째, 두 번째 인자의 우선순위가 동일하다면 0이 반환</li>
</ul>
</li>
</ul>
<p>위 가이드라인을 근거로 데이터간 우선순위의 비교에 사용될 함수를 정의해서 힙에 등록해야 한다.
따라서 힙의 초기화 함수는 다음과 같이 수정된다.</p>
<pre><code class="language-c">void HeapInit(Heap * ph, PriorityComp pc)
{
    ph-&gt;numOfData = 0;
    ph-&gt;comp = pc;        // 우선순위 비교에 사용되는 함수 등록
}</code></pre>
<p>더불어 <code>HInsert</code>함수를 호출하면서 우선순위 정보를 직접 전달하지 않기 때문에 이 함수도 다음과 같이 수정되어야 한다.
<code>void HInsert(Heap * ph, HData data);</code>
이렇게 변경되면 프로그래머는 우선순위 값을 직접 계산할 필요가 없고 우선순위를 비교할 수 있는 기준이 되는 함수만 정의해서 등록하면 된다.</p>
<h4 id="1-헤더파일-정의-usefulheaph">1) 헤더파일 정의 (UsefulHeap.h)</h4>
<p>위에서 수정한 내용을 바탕으로 나머지 부분을 마저 수정해보자.
수정해야 하는 함수는 다음과 같다.</p>
<ul>
<li><code>int GetHiPriChildIDX(Heap * hp, int idx);</code></li>
<li><code>void HInsert(Heap * ph, HData data);</code></li>
<li><code>HData HDelete(Heap * ph);</code></li>
</ul>
<p>변경 포인트는 우선순위의 비교를 위해 사용된 대소 비교 연산자가 존재하는 문장들이다.
수정된 헤더파일은 다음과 같다.</p>
<pre><code class="language-c">#ifndef __USEFUL_HEAP_H__
#define __USEFUL_HEAP_H__

#define TRUE    1
#define FALSE   0

#define HEAP_LEN    100

typedef char HData;
typedef int (*PriorityComp)(HData d1, HData d2);

typedef struct _heap
{
    PriorityComp comp;    // 포인터 변수로 정의 X
    int numOfData;
    HData heapArr[HEAP_LEN];
} Heap;

void HeapInit(Heap * ph, PriorityComp pc);
int HIsEmpty(Heap * ph);

void HInsert(Heap * ph, HData data);
HData HDelete(Heap * ph);

#endif</code></pre>
<p>여기서 <code>typedef int (PrioirityComp)(HData d1, HData d2);</code>그리고 구조체 안에서 <code>PriorityComp * comp;</code> 로 포인터의 위치를 바꿔서 선언해도 제대로 동작한다.
두 코드 모두 동일한 것을 의미한다.</p>
<h4 id="2-소스파일-정의-usefulheapc">2) 소스파일 정의 (UsefulHeap.c)</h4>
<p>헤더파일 수정 및 구조체의 변경에 따른 수정된 소스파일은 다음과 같다.</p>
<pre><code class="language-c">#include &quot;UsefulHeap.h&quot;

// 힙의 초기화 (수정)
void HeapInit(Heap * ph, PriorityComp pc)
{
    ph-&gt;numOfData = 0;
    ph-&gt;comp = pc;
}

// 힙이 비었는지 확인
int HIsEmpty(Heap * ph)
{
    if(ph-&gt;numOfData == 0)
        return TRUE;
    else
        return FALSE;
}

// 부모 노드의 인덱스 값 반환
int GetParentIDX(int idx)
{
    return idx/2;
}

// 왼쪽 자식 노드의 인덱스 값 반환
int GetLChildIDX(int idx)
{
    return idx*2;
}

// 오른쪽 자식 노드의 인덱스 값 반환
int GetRChildIDX(int idx)
{
    return GetLChildIDX(idx)+1;
}

// 두 개의 자식 노드 중 높은 우선순위의 자식 노드 인덱스 값 반환 (수정)
int GetHiPriChildIDX(Heap * ph, int idx)
{
    if(GetLChildIDX(idx) &gt; ph-&gt;numOfData)
        return 0;

    else if(GetLChildIDX(idx) == ph-&gt;numOfData)
        return GetLChildIDX(idx);

    else
    {
        // 수정
        if(ph-&gt;comp(ph-&gt;heapArr[GetLChildIDX(idx)], ph-&gt;heapArr[GetRChildIDX(idx)]) &lt; 0)
            return GetRChildIDX(idx);

        else
            return GetLChildIDX(idx);
    }
}

// 힙에 데이터 저장 (수정)
void HInsert(Heap * ph, HData data)
{
    // 수정
    int idx = ph-&gt;numOfData+1;

    while(idx != 1)
    {    
        // 수정
        if(ph-&gt;comp(data, ph-&gt;heapArr[GetParentIDX(idx)]) &gt; 0)
        {
            ph-&gt;heapArr[idx] = ph-&gt;heapArr[GetParentIDX(idx)];
            idx = GetParentIDX(idx);
        }
        else
            break;
    }

    ph-&gt;heapArr[idx] = data;
    ph-&gt;numOfData += 1;
}

// 힙에서 데이터 삭제 (수정)
HData HDelete(Heap * ph)
{   
    // 수정
    HData retData = ph-&gt;heapArr[1];
    HData lastElem = ph-&gt;heapArr[ph-&gt;numOfData];

    int parentIdx = 1;
    int childIdx;

    // 수정
    while(childIdx = GetHiPriChildIDX(ph, parentIdx))
    {
        if(ph-&gt;comp(lastElem, ph-&gt;heapArr[childIdx]) &gt;=0)
            break;

        ph-&gt;heapArr[parentIdx] = ph-&gt;heapArr[childIdx];
        parentIdx = childIdx;
    }    

    ph-&gt;heapArr[parentIdx] = lastElem;
    ph-&gt;numOfData -= 1;
    return retData;
}</code></pre>
<h4 id="3-실행파일-정의-usefulheapmainc">3) 실행파일 정의 (UsefulHeapMain.c)</h4>
<p>막으로 위에서 수정한 헤더파일과 소스파일을 바탕으로 잘 작동하는지 확인해볼 수 있는 실행파일 정의다.
여기서 주목해야할 부분은 우선순위 비교에 사용되는 함수를 직접 정의하고 이를 힙에 등록하는 부분이다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &quot;UsefulHeap.h&quot;

int DataPriorityComp(char ch1, char ch2)    // 우선순위 비교함수
{
    return ch2-ch1;
    // return ch1-ch2;
}

int main()
{
    Heap heap;
    HeapInit(&amp;heap, DataPriorityComp);  // 우선순위 비교함수 등록

    HInsert(&amp;heap, &#39;A&#39;);
    HInsert(&amp;heap, &#39;B&#39;);
    HInsert(&amp;heap, &#39;C&#39;);
    printf(&quot;%c \n&quot;, HDelete(&amp;heap));

    HInsert(&amp;heap, &#39;A&#39;);
    HInsert(&amp;heap, &#39;B&#39;);
    HInsert(&amp;heap, &#39;C&#39;);
    printf(&quot;%c \n&quot;, HDelete(&amp;heap));

    while(!HIsEmpty(&amp;heap))
        printf(&quot;%c \n&quot;, HDelete(&amp;heap));

    return 0;
}

&gt; gcc .\UsefulHeap.c .\UsefulHeapMain.c
&gt; .\a.exe
&gt; 출력
A 
A 
B
B
C
C</code></pre>
<p>실행파일에서 우선순위 비교함수로 정의되어 있는 <code>DataPriorityComp</code>함수를 살펴보자.</p>
<pre><code class="language-c">int DataPriorityComp(char ch1, char ch2)    // 우선순위 비교함수
{
    return ch2-ch1;
    // return ch1-ch2;
}</code></pre>
<p>이 함수는 첫 번째 인자로 전달된 문자의 아스키 코드 값이 작을 때 0보다 큰 값을 반환하도록 정의되었다.
그런데 이 함수를 정의하는 기준을 앞서 다음과 같이 결정하였다.</p>
<ul>
<li>첫 번째 인자의 우선순위가 높다면 0보다 큰 값을 반환</li>
<li>두 번째 인자의 우선순위가 높다면 0보다 작은 값을 반환</li>
<li>두 인자의 우선순위가 같다면 0이 반환</li>
</ul>
<p>따라서 위 함수의 우선순위 판단 기준은 아스키 코드 값이 작은 문자의 우선순위가 더 높다는 것을 알 수 있다.</p>
<h3 id="3-우선순위-큐-구현---사용-가능-수준의-힙-이용"><em>3. 우선순위 큐 구현 - 사용 가능 수준의 힙 이용</em></h3>
<p>힙을 완성했으니 이번 Chapter의 목표인 우선순위 큐를 구현할 차례다.
우선순위 큐를 고려해서 힙을 구현했기 때문에 사실상 우선순위 큐를 거의 다 구현한 것이나 마찬가지다.
실제로 힙의 <code>HInsert</code>와 <code>HDelete</code> 함수의 호출 결과는 우선순위 큐의 <code>enqueue</code>와 <code>dequeue</code> 연산결과와 일치한다.
그래도 우선순위 큐의 구현까지 해서 끝까지 마무리 지어보자~!
우선 우선순위 큐의 ADT를 정의하고 앞서 구현한 힙을 활용하여 우선순위 큐를 완성해보자.</p>
<h4 id="1-adt-정의">1) ADT 정의</h4>
<p>✅Operations:</p>
<ul>
<li><p>void PQueueInit(PQueue * ppq, PriorityComp pc);</p>
<ul>
<li>우선순위 큐의 초기화를 진행</li>
<li>우선순위 큐 생성 후 제일 먼저 호출되는 함수</li>
</ul>
</li>
<li><p>int PQIsEmpty(PQueue * ppq);</p>
<ul>
<li>우선순위 큐가 빈 경우 TRUE(1), 그렇지 않은 경우 FALSE(0) 반환</li>
</ul>
</li>
<li><p>void PEnqueue(PQueue * ppq, PQData data);</p>
<ul>
<li>우선순위 큐에 데이터 저장. 매개변수 data로 전달된 값 저장</li>
</ul>
</li>
<li><p>PQData PDequeue(PQueue * ppq);</p>
<ul>
<li>우선순위가 가장 높은 데이터 삭제</li>
<li>삭제된 데이터 반환</li>
<li>본 함수의 호출을 위해서는 데이터가 하나 이상 존재함이 보장 필요</li>
</ul>
</li>
</ul>
<p>※ 참고로 힙 기반의 우선순위 큐이기 때문에 아래 파일들과 위에서 구현한 UsefulHeap 헤더파일과 소스파일이 같은 폴더내에 있어야하며 함께 컴파일 해야한다.</p>
<h4 id="2-헤더파일-정의-priorityqueueh">2) 헤더파일 정의 (PriorityQueue.h)</h4>
<pre><code class="language-c">#ifndef __PRIORITY_QUEUE_H__
#define __PRIORITY_QUEUE_H

#include &quot;UsefulHeap.h&quot;

typedef Heap PQueue;
typedef HData PQData;

void PQueueInit(PQueue * ppq, PriorityComp pc);
int PQIsEmpty(PQueue * ppq);

void PEnqueue(PQueue * ppq, PQData data);
PQData PDequeue(PQueue * ppq);

#endif</code></pre>
<h4 id="3-소스파일-정의-priorityqueuec">3) 소스파일 정의 (PriorityQueue.c)</h4>
<pre><code class="language-c">#include &quot;PriorityQueue.h&quot;
#include &quot;UsefulHeap.h&quot;

void PQueueInit(PQueue * ppq, PriorityComp pc)
{
    HeapInit(ppq, pc);
}

int PQIsEmpty(PQueue * ppq)
{
    return HIsEmpty(ppq);
}

void PEnqueue(PQueue * ppq, PQData data)
{
    HInsert(ppq, data);
}

PQData PDequeue(PQueue * ppq)
{
    return HDelete(ppq);
}</code></pre>
<p>사실상 다 구현해 놓은 힙을 가져다가 사용하는 것이기 때문에 간단하다.</p>
<h4 id="4-실행파일-정의-priorityqueuemainc">4) 실행파일 정의 (PriorityQueueMain.c)</h4>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &quot;PriorityQueue.h&quot;

int DataPriorityComp(char ch1, char ch2)
{
    return ch2-ch1;
}

int main()
{
    PQueue pq;
    PQueueInit(&amp;pq, DataPriorityComp);

    PEnqueue(&amp;pq, &#39;A&#39;);
    PEnqueue(&amp;pq, &#39;B&#39;);
    PEnqueue(&amp;pq, &#39;C&#39;);
    printf(&quot;%c \n&quot;, PDequeue(&amp;pq));

    PEnqueue(&amp;pq, &#39;A&#39;);
    PEnqueue(&amp;pq, &#39;B&#39;);
    PEnqueue(&amp;pq, &#39;C&#39;);
    printf(&quot;%c \n&quot;, PDequeue(&amp;pq));

    while(!PQIsEmpty(&amp;pq))
        printf(&quot;%c \n&quot;, PDequeue(&amp;pq));

    return 0;
}

&gt; gcc .\PriorityQueue.c .\PriorityQueueMain.c .\UsefulHeap.c
&gt; .\a.exe
&gt; 출력
A 
A 
B
B
C
C</code></pre>
<h4 id="-우선순위-큐의-활용">+) 우선순위 큐의 활용</h4>
<p>우선순위 큐를 이용해서 다수의 문자열을 저장하고, 저장된 문자열을 꺼내어 출력하는 프로그램을 작성해보자.
단, 힙에 저장되는 문자열은 길이가 짧을수록 우선순위가 높다고 가정하자.</p>
<p>이 문제를 해결하기 위해서는 우선순위 비교함수를 수정하면 된다.
미리 구현해놓은 <code>UsefulHeap.h, UsefulHeap.c, PriorityQueue.h, PriorityQueue.c</code> 파일을 사용하자.
추가로 우선순위 큐의 저장 대상이 문자열인 관계로 헤더파일 <code>UsefulHeap.h</code>의 typedef 선언을 <code>typedef char HData; → typedef char *HData;</code>로 변경해야 한다.</p>
<p>따라서 변경된 실행파일은 다음과 같다.</p>
<pre><code class="language-c">// q1.c
#include &lt;stdio.h&gt;
#include &lt;string.h&gt;
#include &quot;PriorityQueue.h&quot;

int DataPriorityComp(char * str1, char * str2)
{
    return strlen(str2) - strlen(str1);
}

int main()
{
    PQueue pq;
    PQueueInit(&amp;pq, DataPriorityComp);

    PEnqueue(&amp;pq, &quot;Apple&quot;);
    PEnqueue(&amp;pq, &quot;Banana&quot;);
    PEnqueue(&amp;pq, &quot;Cherry&quot;);
    PEnqueue(&amp;pq, &quot;Grape&quot;);
    PEnqueue(&amp;pq, &quot;Orange&quot;);
    PEnqueue(&amp;pq, &quot;Kiwi&quot;);
    PEnqueue(&amp;pq, &quot;Mango&quot;);
    PEnqueue(&amp;pq, &quot;Pineapple&quot;);
    PEnqueue(&amp;pq, &quot;Strawberry&quot;);
    PEnqueue(&amp;pq, &quot;Watermelon&quot;);

    while(!PQIsEmpty(&amp;pq))
        printf(&quot;%s \n&quot;, PDequeue(&amp;pq));

    return 0;
}

&gt; gcc .\PriorityQueue.c .\q1.c .\UsefulHeap.c
&gt; .\a.exe
&gt; 출력
Kiwi 
Grape 
Apple
Mango
Banana
Orange
Cherry
Pineapple
Strawberry
Watermelon </code></pre>
<p>여기서 더 develop 시키고 싶다면 데이터를 입력하는 것이 아닌 입력 받아 보는 방법도 있다.</p>
<hr>
<h2 id="review"><code>&lt;Review&gt;</code></h2>
<p>드디어 나의 100번째 벨로그다~!
우선순위 큐는 힙을 구현하면 거의 다 구현한 것이기 때문에 힙에 대한 이해가 중요했다.
힙은 트리란 자료구조를 이해해야했고, 트리는 기존에 연결 리스트 기반으로 구현한 방법 이 외에 배열로 구현한 방법에 대한 이해가 필요했다.
앞에 배운 내용을 총망라하여 다뤘다는 느낌을 받았다.
그리고 자료구조를 서로 이용하는데 각 자료구조의 특징을 이용해서 다른 자료구조를 구현한다는 점이 매력적으로 다가왔다.</p>
<p>앞으로 정렬, 탐색, 테이블과 해쉬, 그래프 등이 남았는데 포기하지 말고 앞에 내용을 잘 되새기며 파이썬으로 학습했을 때보다 보다 더 자료구조에 대해, 알고리즘에 대해 이해할 수 있었음 좋겠다.</p>
<p>화이팅~!</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/7d0250e8-100e-4c7d-ba52-44edb23e8903/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chapter 08. 트리(Tree)]]></title>
            <link>https://velog.io/@mingming_eee/datastructure-08</link>
            <guid>https://velog.io/@mingming_eee/datastructure-08</guid>
            <pubDate>Mon, 04 Nov 2024 14:48:49 GMT</pubDate>
            <description><![CDATA[<h2 id="08-1-트리의-개요">08-1. 트리의 개요</h2>
<p>이번에 배우게 될 <code>트리(Tree)</code>는 고급 자료구조로 구분된다.
이전에 배운 선형 자료구조들과 달리 트리는 비선형 자료구조다.
집중해서 알아보자~!</p>
<h3 id="트리tree의-접근"><em>트리(Tree)의 접근</em></h3>
<p>트리(Tree)는 계층적 관계(Hierarchical Relationship)를 표현하는 자료구조다.
이번에 배울 트리는 &#39;계층적 관계&#39;라는 점도 중요하지만 &#39;표현&#39;하는 자료구조라는 점도 매우 중요하다.
데이터의 저장과 삭제가 아닌 &#39;표현&#39;에 초점이 맞춰서 트리에 대해 배울 것이기 때문에 이 점을 꼭 기억하면서 학습하면 좋다.
따라서 트리의 ADT를 정의하거나 배울 때 &quot;트리의 구조로 이뤄진 무엇인가를 표현하기에 적절히 정의되어 있나?&quot;를 생각하며 트리를 표현하는 것이 좋다.</p>
<h3 id="트리가-표현할-수-있는-것들"><em>트리가 표현할 수 있는 것들</em></h3>
<p>그렇다면 트리는 무엇을 표현하기 위한 자료구조일까?
사실 트리도 큐와 비슷하게 주변에서 쉽게 찾아 볼 수 있는 자료구조다.
컴퓨터의 디렉터리 구조, 집안의 족보, 기업 및 정부의 조직도도 트리의 예시다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/16acb8f6-8a39-4097-b1b3-3d82c489a4f3/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/d8081b0b-7198-4863-a842-a6d508da2f21/image.png" alt=""></p>
<p>따라서 트리 구조라는 것은 나무처럼 가지를 늘려가며 뻗어나가는 것이다.
트리는 어떠한 데이터를 저장하고 꺼내서 사용하는 것이 아닌 무엇인가를 대표하는 도구라 생각하고 학습하면 편하다~!</p>
<h3 id="트리-관련-용어"><em>트리 관련 용어</em></h3>
<p>트리와 관련해서 많은 용어가 정의되어 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/81a3afcb-01bf-4ce9-b144-6725b660eec6/image.png" alt=""></p>
<ul>
<li><p>노드(node)
: 트리의 구성요소에 해당하는 A, B, C, D, E, F와 같은 요소</p>
</li>
<li><p>간선(edge)
: 노드와 노드를 연결하는 연결선</p>
</li>
<li><p>루트 노드(root node)
: 트리 구조에서 최상위에 존재하는 A와 같은 노드</p>
</li>
<li><p>단말 노드(terminal node)_입사귀 노드(leaf node)
: 아래로 또 다른 노드가 연결되어 있지 않은 E, F, C, D와 같은 요소</p>
</li>
<li><p>내부 노드(internal node)_비단말 노드(nonterminal node)
: 단말 노드를 제외한 모든 노드로 A, B와 같은 노드</p>
</li>
</ul>
<p>트리 내 노드간에는 부모(parent), 자식(child), 형제(sibling)의 관계가 성립되어 다음과 같은 표현도 할 수 있다.
(참고로 트리의 구조상 위에 있을수록 촌수가 높다.)</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/89692c19-5adc-431d-af06-35d9d3b3a31c/image.png" alt=""></p>
<ul>
<li>노드 A는 노드 B, C, D의 부모 노드(parent node)다.</li>
<li>노드 B, C, D는 노드 A의 자식 노드(child node)다.</li>
<li>노드 B, C, D는 부모 노드가 같으므로 서로가 서로에게 형제 노드(sibling node)다.</li>
</ul>
<p>부모와 자식의 관계는 상대적이므로 노드 B는 노드 A에게 자식 노드이면서 노드 E와 F의 부모 노드도 된다.</p>
<p>조금 더 확장해서 조상(Ancestor)과 후손(Descendant)의 관계도 있다. 
특정 노드의 위에 위치한 모든 노드를 가리며 조상 노드라 하고,
특정 노드의 아래에 위치한 모든 노드를 가리켜 후손 노드라 한다.
즉, 노드 A와 B는 노드 E의 조상 노드고 B, C, D, E, F는 모두 노드 A의 후손 노드다.</p>
<p>트리에서는 각 층별로 숫자를 매겨서 이를 트리의 &#39;레벨(level)&#39;이라 하고 트리의 최고 레벨을 가리켜 &#39;높이(height)&#39;라 한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/a825fa39-460f-4586-b155-fd4fc7793ba5/image.png" alt=""></p>
<p>그림과 같이 트리는 레벨 0부터 시작해 최종 레벨이 3이고 이것이 트리의 높이가 된다.</p>
<h3 id="트리의-종류"><em>트리의 종류</em></h3>
<h4 id="1-서브-트리-sub-tree">1) 서브 트리 (Sub Tree)</h4>
<p>큰 트리는 작은 트리들로 구성되어 있다.
큰 트리에 속하는 작은 트리를 가리켜 &#39;서브 트리(sub tree)&#39;라 한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/c95cad13-ddc4-41c6-9e59-d71dd2e7d762/image.png" alt=""></p>
<p>큰 트리 안에 노드 B를 루트 노드로 하는 서브 트리가 있다.
이 트리 안에도 노드 D를 루트 노드로 하는 서브 트리와 노드 E를 루트 노드로 하는 서브 트리가 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/4f83f62d-d4b5-4ae2-9b0a-9f4e95e7aacd/image.png" alt=""></p>
<h4 id="2-이진-트리-binary-tree">2) 이진 트리 (Binary Tree)</h4>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/a05bb667-a89a-4b9d-bf70-052c137c0a3c/image.png" alt=""></p>
<p>트리 중에서도 이진 트리(Binary Tree)라는 것이 있는데 이진 트리에 해당하기 위한 조건은 다음 두 가지와 같다.</p>
<ol>
<li>루트 노드를 중심으로 두 개의 서브 트리로 나눠진다.</li>
<li>나눠진 두 서브 트리도 모두 이진 트리여야 한다.</li>
</ol>
<p>정의 안에 이진 트리라는 단어가 들어가 있으므로 재귀적인 느낌을 받을 수 있다.
만약에 아래와 같은 트리가 있다면 이것은 이진 트리가 아닐까?</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/67b592f1-8df2-4d6c-b5c6-c5ea9ea0089d/image.png" alt=""></p>
<p>이것도 이진 트리가 맞다.
이진 트리를 정의할 땐 노드가 위치할 수 있는 곳에 노드가 존재하지 않을 경우 공집합(empty set) 노드가 존재하는 것으로 간주하기 때문이다.</p>
<p>따라서 이진 트리에서 말하는 공집합 노드를 추가해주면 아래와 같이 표현할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/64f955d9-d6c5-4fa2-861b-b3a5aa9456b6/image.png" alt=""></p>
<p>이러한 공집합 노드 덕분에 서브트리가 하나인 노드 B와 C도 그리고 단말 노드인 D와 E도 모두 이진 트리가 될 수 있다.</p>
<p>한줄로 되어있는 트리도 공집합을 넣어주면 이진 트리가 될 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/2e48b1b0-77d8-417f-b72a-28672d2c9171/image.png" alt=""></p>
<h4 id="3-포화-이진-트리-full-binary-tree">3) 포화 이진 트리 (Full Binary Tree)</h4>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/6352c67c-3383-4f2e-827b-4356d6412aab/image.png" alt=""></p>
<p>위 그림과 같이 모든 레벨이 꽉 찬 이진 트리를 가리켜 &#39;포화 이진 트리(Full Binary Tree)&#39;라 한다.</p>
<h4 id="4-완전-이진-트리-complete-binary-tree">4) 완전 이진 트리 (Complete Binary Tree)</h4>
<p>위에서 예시를 든 포화 이진 트리에서 레벨을 하나 더 늘려서 노드 H와 I를 추가한 트리가 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/cccc9a03-0284-458a-b97b-63fb34db58b8/image.png" alt=""></p>
<p>위 트리를 가리켜 &#39;완전 이진 트리(Complete Binary Tree)&#39;라 한다.
포화 이진 트리처럼 모든 레벨이 꽉 찬 상태는 아니지만 차곡차곡 빈 틈 없이 노드가 채워진 이진 트리를 말한다.
여기서 &#39;차곡차곡 빈 틈 없이 채워졌다&#39;라는 것은 &quot;노드가 위에서 아래로, 그리고 왼쪽에서 오른쪽의 순서대로 채워졌다.&quot;라는 의미다.</p>
<p>완전 이진 트리와 달리 그냥 이진 트리는 다음과 같은 구조를 말한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/09417384-b09a-4705-bb74-2fbc5cdb0834/image.png" alt=""></p>
<hr>
<h2 id="08-2-이진-트리의-구현">08-2. 이진 트리의 구현</h2>
<p>트리에 대한 기본적인 것을 배웠으니 이제 트리 구현에 대해 배워보자!
이진 트리는 재귀적인 특성을 가지고 있다.
이러한 특성 때문에 이진 트리와 관련된 일부 연산은 재귀호출의 형태를 띈다.</p>
<h3 id="배열-기반-구현-or-연결-리스트-기반-구현"><em>배열 기반 구현 or 연결 리스트 기반 구현</em></h3>
<p>이진 트리 역시 배열 기반으로도, 연결 리스트 기반으로도 구현이 가능하다.
하지만 트리를 표현하기에는 연결 리스트가 더 유연하기 때문에 연결 리스트 기반으로 구현하는 방법으로 배울 것이다.
단, 트리가 완성 된 이후부터 트리를 대상으로 매우 빈번하게 탐색이 이뤄지는 완전 이진 트리의 경우 배열 기반의 구현이 좀 더 용이하고 빠르다.
그 이유는 배열이 연결 리스트에 비해 탐색에 있어 매우 용이하고 빠르기 때문이다.
따라서 배열 기반으로 이진 트리를 구성하는 방법에 대해 간단하게 알아보고,
대부분의 학습을 연결 기반의 이진 트리 구현으로 할 것이다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/bba19aea-1127-4f5b-ab0d-13725f33036b/image.png" alt=""></p>
<p>다음과 같이 이진 트리가 있다고 가정해보자.
이 이진 트리를 배열 기반으로 구현하려면 각 노드에 번호를 부여해야한다.
이 번호는 각 노드의 ㄷ이터가 저장되어야할 배열의 인덱스 값을 의미하게 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/94885cc2-9445-4f6a-aed7-6c207b39f362/image.png" alt=""></p>
<p>길이가 8인 배열을 선언하여 노드 번호 1부터 5까지의 노드에 저장된 데이터를 배열에 저장했다.
데이터가 저장되는 배열의 위치는 노드 번호 기준으로 결정되었다.</p>
<p>그렇다면 연결 리스트 기반으로 트리를 어떻게 구현할 수 있을까?</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/6703a8ea-4bb8-4856-8263-8eb8c3f274be/image.png" alt=""></p>
<p>연결 리스트 기반으로 트리를 구현한다면 연결 리스트이 구성 형태가 실제 트리와 일치하기 때문에 용이하다~!</p>
<p>그렇다면 배열 기반 리스트는 사용하지 않을까?
아니다. 완전 이진 트리의 구조를 갖는 &#39;힙(heap)&#39;이라는 자료구조는 배열을 기반으로 구현하기 때문에 배열 기반의 트리와 연결 리스트 기반의 트리 모두 중요하다.
(힙은 다음 chapter에서 배울 예정이다.)</p>
<h3 id="1-헤더파일-정의"><em>1) 헤더파일 정의</em></h3>
<p>원래는 ADT를 정의한 다음에 헤더 파일을 정의한 다음 ADT를 정의하려한다.
ADT를 먼저 정의할 경우 이해하기 어려운 부분이 있기 때문이다.
연결리스트 기반의 트리 구현에서 헤더 파일은 다음과 같다.</p>
<pre><code class="language-c">// BinaryTree.h
#ifndef __BINARY_TREE_H__
#define __BINARY_TREE_H__

typedef int BTData;

typedef struct _bTreeNode
{
    BTData data;
    struct _bTreeNode * left;
    struct _bTreeNode * right;
} BTreeNode;

BTreeNode * MakeBTreeNode(void);
BTData GetData(BTreeNode * bt);
void SetData(BTreeNode * bt, BTData data);

BTreeNode * GetLeftSubTree(BTreeNode * bt);
BTreeNode * GetRightSubTree(BTreeNode * bt);

void MakeLeftSubTree(BTreeNode * main, BTreeNode * sub);
void MakeRightSubTree(BTreeNode * main, BTreeNode * sub);

#endif</code></pre>
<p>헤더파일에 정의되어 있는 구조체는 다음과 같다.</p>
<pre><code class="language-c">typedef struct _bTreeNode    // 이진 트리의 노드를 표현한 구조체
{
    BTData data;
    struct _bTreeNode * left;
    struct _bTreeNode * right;
} BTreeNode;</code></pre>
<p>이진 트리를 표현하기 위함인데 노드의 구조체만 정의되어 있고 이진 트리를 표현한 구조체는 없다.
이는 노드가 위치할 수 있는 곳에 노드가 존재하지 않는다면 공집합(empty set) 노드가 존재하는 것으로 간주하고 공집합 노드도 이진 트리를 판단하는데 있어서 노드로 인정하기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/2fbe027e-49b6-418c-a58f-ee25bdd8e109/image.png" alt=""></p>
<p>위 그림과 같이 하나의 노드는 두 개의 공집합 노드를 자식으로 두고 있는 그 자체로도 이진 트리이다.
그러므로 구조체 BTreeNode는 노드를 표현함과 동시에 이진 트리를 표현한 결과가 된다.
구조체 포인터 변수의 이름은 <code>BTreeNode * pnode;</code>로 선언될 수 있지만, 어떻게 보면 <code>BTreeNode * ptree;</code>로 선언되는 것과 같은 것이다.
(이 문장의 의미는 BTreeNode는 노드의 표현 결과뿐만 아니라 이진 트리의 표현 결과도 된다는 것이다.)</p>
<h3 id="2-이진-트리-adt-정의"><em>2) 이진 트리 ADT 정의</em></h3>
<p>위에서 정의한 헤더파일을 바탕으로 이진 트리의 구현 및 연산에 대해 생각해보자.
중요한 점은 이진 트리의 재귀적인 성향을 이해하는 것이다.</p>
<p>헤더파일에서 이진 트리를 만드는 도구가 되는 함수 3가지가 있다.</p>
<ul>
<li><code>BTreeNode * MakeBTreeNode(void);</code> : 노드의 생성</li>
<li><code>BTData GetData(BTreeNode * bt);</code> : 노드에 저장된 데이터 반환</li>
<li><code>void SetData(BTreeNode * bt, BTData data);</code> : 노드에 데이터 저장</li>
</ul>
<p>여기서 노드의 생성을 담당하는 <code>MakeBTreeNode</code>함수는 호출되면 다음 그림과 같은 형태의 노드를 동적 할당 및 초기화하여 그 주소값을 반환한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/9b49549a-6272-4b3c-8b2f-ad579ea88bae/image.png" alt=""></p>
<p>위 물음표는 데이터가 저장되는 부분이며 첫 초기화에서는 데이터가 저장되는 멤버 data를 대상으로 별도의 초기화를 진행하지 않는다.
그러나 왼쪽 서브 트리와 오른쪽 서브 트리를 가리키기 위한 멤버 left와 right은 NULL로 초괴화한다.</p>
<p>그 다음 왼쪽과 오른쪽 서브 트리의 주소 값을 반환하는 함수 2가지가 있다.</p>
<ul>
<li><code>BTreeNode * GetLeftSubTree(BTreeNode * bt);</code> : 왼쪽 서브 트리 주소 값 반환</li>
<li><code>BTreeNode * GetRightSubTree(BTreeNode * bt);</code> : 오른쪽 서브 트리 주소 값 반환</li>
</ul>
<p>루트 노드를 포함하여 어떠한 노드의 주소 값도 위 함수의 인자로 전달될 수 있다.
두 함수는 각각 인자로 전달된 이진 트리의 왼쪽 서브 트리, 오른쪽 서브 트리의 루트 노드의 주소 값(또는 그냥 노드의 주소 값)을 반환하는 함수다.</p>
<p>그리고 마지막으로 함수 2가지가 더 정의되어 있다.</p>
<ul>
<li><code>void MakeLeftSubTree(BTreeNode * main, BTreeNode * sub);</code></li>
<li><code>void MakeRightSubTree(BTreeNode * main, BTreeNode * sub);</code></li>
</ul>
<p>위 두 함수는 서브 트리의 연결을 담당한다.
첫 번째 함수는 매개 변수 sub으로 전달된 트리 또는 노드를 매개변수 main으로 전달된 노드의 왼쪽 서브 트리로 연결한다.
두 번째 함수는 마찬가지로 전달된 노드를 오른쪽 서브 트리로 연결한다.
이 두 함수는 연결을 담당할 뿐, 노드나 트리의 생성을 담당하지 않는다.</p>
<p>위 함수들의 역할을 이해하기 위해 예시를 보자.
&quot;만약 노드 A, B, C를 생성해서 A를 루트로 하고 B와 C를 각각 A의 왼쪽과 오른쪽 자식 노드가 되도록 하려면 함수의 호출 흐름을 어떻게 하면 좋을까?&quot;
순서는 다음가 같이 구성할 수 있다.</p>
<pre><code class="language-c">int main()
{
    BTreeNode * ndA = MakeBTreeNode();    // 노드 A 생성
    BTreeNode * ndB = MakeBTreeNode();    // 노드 B 생성
    BTreeNode * ndC = MakeBTreeNode();    // 노드 C 생성
    ....
    MakeLeftSubTree(ndA, ndB);    // 노드 A의 왼쪽 자식 노드로 노드 B 연결
    MakeRightSubTree(ndA, ndC);    // 노드 A의 오른쪽 자식 노드로 노드 C 연결
    ....
}</code></pre>
<p>하나의 노드는 곧 하나의 이진 트리와 같다고 앞서 배웠기 때문에 노드를 루트노드에 연결할 때도 <code>MakeLeft(Right)SubTree</code>함수가 사용된다.</p>
<p>위에서 설명한 내용을 ADT 정의로 정리해보자.</p>
<p>✅Operations:</p>
<ul>
<li><p>BTreeNode * MakeBRTreeNode(void);</p>
<ul>
<li>이진 트리 노드를 생성하여 그 주소 값 반환</li>
</ul>
</li>
<li><p>BTData GetData(BTreeNode * bt);</p>
<ul>
<li>노드에 저장된 데이터를 반환</li>
</ul>
</li>
<li><p>void SetData(BTreeNode * bt, BTData data);</p>
<ul>
<li>노드에 데이터 저장. data로 전달된 값 저장</li>
</ul>
</li>
<li><p>BTreeNode * GetLeftSubTree(BTreeNode * bt);</p>
<ul>
<li>왼쪽 서브 트리의 주소 값을 반환</li>
</ul>
</li>
<li><p>BTreeNode * GetRightSubTree(BTreeNode * bt);</p>
<ul>
<li>오른쪽 서브 트리의 주소 값을 반환</li>
</ul>
</li>
<li><p>void MakeLeftSubTree(BTreeNode * main, BTreeNode * sub);</p>
<ul>
<li>왼쪽 서브 트리를 연결</li>
</ul>
</li>
<li><p>void MakeRightSubTree(BTreeNode * main, BTreeNode * sub);</p>
<ul>
<li>오른쪽 서브 트리를 연결</li>
</ul>
</li>
</ul>
<h3 id="3-소스파일-정의"><em>3) 소스파일 정의</em></h3>
<p>위에서 정의한 ADT와 헤더파일을 바탕으로 소스파일을 정의해보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;BinaryTree.h&quot;

BTreeNode * MakeBTreeNode(void)
{
    BTreeNode * nd = (BTreeNode *)malloc(sizeof(BTreeNode));
    nd-&gt;left = NULL;
    nd-&gt;right = NULL;
    return nd;
}

BTData GetData(BTreeNode * bt)
{
    return bt-&gt;data;
}

void SetData(BTreeNode * bt, BTData data)
{
    bt-&gt;data = data;
}

BTreeNode * GetLeftSubTree(BTreeNode * bt)
{
    return bt-&gt;left;
}

BTreeNode * GetRightSubTree(BTreeNode * bt)
{
    return bt-&gt;right;
}

void MakeLeftSubTree(BTreeNode * main, BTreeNode * sub)
{
    if(main-&gt;left != NULL)
        free(main-&gt;left);

    main-&gt;left = sub;
}

void MakeRightSubTree(BTreeNode * main, BTreeNode * sub)
{
    if(main-&gt;right != NULL)
        free(main-&gt;right);

    main-&gt;right = sub;
}</code></pre>
<p>여기서 <code>MakeLeftSubTree</code>와 <code>MakeRightSubTree</code>를 좀 더 살펴보자.
왼쪽 또는 오른쪽 서브 트리가 존재한다면, 해당 트리를 삭제하고 새로운 왼쪽 또는 오른쪽 서브 트리를 연결한다.
이는 함수 구현에 있어서 나름 의미를 부여할 수 있는 선택이다.
하지만 free 함수 호출이 한번만 이뤄지기 때문에 삭제할 서브 트리가 하나의 노드로 이뤄져 있다면 문제되지 않지만 그렇지 않다면 메모리 누수가 발생하게 된다.
따라서 둘 이상의 노드로 이뤄져 있는 서브 트리를 완전히 삭제하려면 서브 트리를 구성하는 모든 노드를 대상으로 free 함수를 호출해야한다. 
모든 노드의 방문이 필요한 것이다.
이렇게 모든 노드를 방문하는 것을 &#39;순회&#39;라 하고 이진 트리의 순회는 연결 리스트의 순회와 달리 별도의 방법이 필요하다.
우선 이진 트리 구성의 예를 보이는 실행파일을 정의하고
이후에 순회에 대한 여러 방법에 대해 배워보자.</p>
<h3 id="4-실행파일-정의"><em>4) 실행파일 정의</em></h3>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &quot;BinaryTree.h&quot;

int main()
{
    BTreeNode * bt1 = MakeBTreeNode();     // 노드 bt1 생성
    BTreeNode * bt2 = MakeBTreeNode();     // 노드 bt2 생성
    BTreeNode * bt3 = MakeBTreeNode();     // 노드 bt3 생성
    BTreeNode * bt4 = MakeBTreeNode();     // 노드 bt4 생성

    SetData(bt1, 1);    // bt1에 1 저장
    SetData(bt2, 2);    // bt2에 2 저장
    SetData(bt3, 3);    // bt3에 3 저장
    SetData(bt4, 4);    // bt4에 4 저장

    MakeLeftSubTree(bt1, bt2);    // bt1의 왼쪽 자식 노드 bt2로 설정
    MakeRightSubTree(bt1, bt3);   // bt1의 오른쪽 자식 노드 bt3으로 설정
    MakeLeftSubTree(bt2, bt4);    // bt2의 왼쪽 자식 노드 bt4로 설정

    printf(&quot;bt1의 왼쪽 자식 노드의 데이터 출력\n&quot;);
    printf(&quot;%d \n&quot;, GetData(GetLeftSubTree(bt1)));

    printf(&quot;bt1의 왼쪽 자식 노드의 왼쪽 자식 노드의 데이터 출력\n&quot;);
    printf(&quot;%d \n&quot;, GetData(GetLeftSubTree(GetLeftSubTree(bt1))));

    return 0;
}

&gt; gcc .\BinaryTree.c .\BinaryTreeMain.c
&gt; .\a.exe   
&gt; 출력
bt1의 왼쪽 자식 노드의 데이터 출력
2
bt1의 왼쪽 자식 노드의 왼쪽 자식 노드의 데이터 출력
4</code></pre>
<p>위 실행파일을 통해 형성되는 이진 트리의 구조는 다음 그림과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/433e383a-b3fd-483a-bf93-444024464ff0/image.png" alt=""></p>
<hr>
<h2 id="08-3-이진-트리의-순회traversal">08-3. 이진 트리의 순회(Traversal)</h2>
<p>이진 트리의 순회가 필요한 부분은 앞에서 언급했다.
트리의 순회 방법은 총 3가지 있고 순회 방법 또한 재귀적이다.</p>
<h3 id="순회-방법"><em>순회 방법</em></h3>
<ol>
<li>중위 순회(Inorder Traversal) : 루트 노드를 중간에</li>
<li>후위 순회(Postorder Traversal) : 루트 노드를 마지막에</li>
<li>전위 순회(Preorder Traversal) : 루트 노드 먼저</li>
</ol>
<p>루트 노드를 언제 방문하느냐에 따라 순회 방법은 3가지로 나뉜다.</p>
<p>다음 그림과 같은 순서 및 방향으로 순회할 경우 루트 노드는 중간에 방문하기 때문에 이러한 방식의 순회를 가리켜 &#39;중위 순회&#39;라 한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/933b920e-b62a-4ef9-9d34-942b384649eb/image.png" alt=""></p>
<p>다음 그림과 같은 순서 및 방향으로 순회할 경우 루트 노드를 마지막에 방문하므로 이러한 방식의 순회를 가리켜 &#39;후위 순회&#39;라 한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/7eabca21-ca56-44d4-8332-798fbd48464d/image.png" alt=""></p>
<p>다음 그림과 같은 순서 및 방향으로 순회할 경우 루트 노드를 가장 먼저 방문하기 때문에 이러한 방식의 순회를 가리켜 &#39;전위 순회&#39;라 한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/bd78ebd2-2ad6-45fe-ac3e-2c9a4d1d0bf7/image.png" alt=""></p>
<p>순회 방법은 루트 노드와 이를 부모로 하는 두 자식 노드를 놓고 한쪽 방향으로 순서대로 방문하면 된다.</p>
<p>그럼 높이가 2 이상인 이진 트리는 어떻게 순회할까?
재귀적인 형태로 순회의 과정을 구성하면 높이에 상관없이 순회가 가능하다.</p>
<h3 id="순회의-재귀적-표현"><em>순회의 재귀적 표현</em></h3>
<p>그럼 가장 먼저 중위 순회에 대한 함수를 정의해보자.
예시 이진 트리는 다음 그림과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/3cb4143c-f970-4309-aa91-ba6b35f863fa/image.png" alt=""></p>
<p>이 이진 트리를 대상으로 중위 순회를 할 경우 순회의 순서는 다음과 같다.</p>
<ol>
<li>왼쪽 서브 트리의 순회</li>
<li>루트 노드의 방문</li>
<li>오른쪽 서브 트리의 순회</li>
</ol>
<p>여기서 주목해야하는 순서는 1번, 3번이다.
각 서브 트리도 정한 순회 방법(중위 순회)으로 순회를 진행하면 된다.
따라서 이진 트리 전체를 중위 순회 하는 함수는 개략적으로 다음과 같이 정의할 수 있다.</p>
<pre><code class="language-c">void InorderTraverse(BTreeNode * bt)
{
    InorderTraverse(bt-&gt;left);        // 1. 왼쪽 서브 트리의 순회
    printf(&quot;%d \n&quot;, bt-&gt;data);        // 2. 루트 노드 방문
    InorderTraverse(bt-&gt;right);        // 3. 오른쪽 서브 트리의 순회
}</code></pre>
<p>위 함수에 대해 다음과 같은 의문이 들 수 있다.
&quot;재귀의 탈출 조건은 무엇인가?&quot;, &quot;노드의 방문이 그저 데이터 출력인가?&quot;
노드에 저장된 데이터의 출력으로 노드의 방문이 이뤄진 것으로 가정한 이유는
순회 방법에 초점을 두기 위함이다.
재귀의 탈출 조건에 관해서 다음 이진 트리를 예시로 이해해보자.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/72590d43-ecd9-4a48-8125-6ce9a013c5cd/image.png" alt=""></p>
<p>루트 노드가 L인 왼쪽 서브 트리에서는 다음의 과정을 거쳐서 순회를 진행하게 된다.</p>
<ol>
<li>왼쪽 서브 트리의 순회 (노드 N 대상)</li>
<li>루트 노드의 방문 (노드 L 대상)</li>
<li>오른쪽 서브 트리의 순회 (공집합 노드 대상)</li>
</ol>
<p>여기 3번 순서에서 노드 L의 오른쪽 서브 트리가 NULL이므로 함수에 NULL이 전달되기 때문에 재귀의 탈출 조건이 성립된다.
따라서 함수는 다음과 같이 정의되어야 한다.</p>
<pre><code class="language-c">void InorderTraverse(BTreeNode * bt)
{
    if(bt == NULL)    // bt가 NULL이면 재귀 탈출
        return;

    InorderTraverse(bt-&gt;left);
    printf(&quot;%d \n&quot;, bt-&gt;data);
    InorderTraverse(bt-&gt;right);
}</code></pre>
<p>위에서 배운 중위순회를 실행파일로 만들어서 확인해보자.
(이전에 정의한 헤더파일과 소스파일을 사용한다.)</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &quot;BinaryTree.h&quot;

void InorderTraverse(BTreeNode * bt)
{
    if(bt == NULL)  // bt가 NULL이면 재귀 탈출
        return;

    InorderTraverse(bt-&gt;left);
    printf(&quot;%d \n&quot;, bt-&gt;data);
    InorderTraverse(bt-&gt;right);
}

int main()
{
    BTreeNode * bt1 = MakeBTreeNode();
    BTreeNode * bt2 = MakeBTreeNode();
    BTreeNode * bt3 = MakeBTreeNode();
    BTreeNode * bt4 = MakeBTreeNode();

    SetData(bt1, 1);
    SetData(bt2, 2);
    SetData(bt3, 3);
    SetData(bt4, 4);

    MakeLeftSubTree(bt1, bt2);
    MakeRightSubTree(bt1, bt3);
    MakeLeftSubTree(bt2, bt4);

    printf(&quot;&lt;Inorder Traversal Check&gt;\n&quot;);
    InorderTraverse(bt1);

    return 0;
}

&gt; gcc .\BinaryTree.c .\BinaryTreeTraverseMain.c
&gt; .\a.exe
&gt; 출력
&lt;Inorder Traversal Check&gt;
4 
2 
1
3</code></pre>
<p>중위 순회와 마찬가지로 후위 순회와 전위 순회도 비슷한 방식으로 함수를 정의할 수 있다.
함수는 다음과 같다.</p>
<pre><code class="language-c">// 후위 순회 (Postorder Traversal)
void PostorderTraverse(BTreeNode * bt)
{
    if(bt == NULL)
        return;

    PostorderTraverse(bt-&gt;left);
    PostorderTraverse(bt-&gt;right);
    printf(&quot;%d \n&quot;, bt-&gt;data);  // 후위 순회로 루트 노드 마지막 방문
}

// 전위 순회 (Preorder Traversal)
void PreorderTraverse(BTreeNode * bt)
{
    if(bt == NULL)
        return;

    printf(&quot;%d \n&quot;, bt-&gt;data);  // 전위 순회로 루트 노드 먼저 방문
    PreorderTraverse(bt-&gt;left);
    PreorderTraverse(bt-&gt;right);
}</code></pre>
<h3 id="노드의-방문-이유"><em>노드의 방문 이유</em></h3>
<p>노드의 방문목적은 데이터의 출력이 전부가 아니다.
방문 목적은 상황에 따라 달라진다.
방문했을 때 할 일을 결정할 수 있도록 앞에서 정의한 순회 함수 세 가지를 약간 변경해보자.
함수 포인터를 사용할 예정이다.</p>
<pre><code class="language-c">// 중위 순회 함수 변경
typedef void (*VisitFuncPtr)(BTData data);

void InorderTraverse(BTreeNode * bt, VisitFuncPtr action)
{
    if(bt == NULL)
        return;

    InorderTraverse(bt-&gt;left, action);
    action(bt-&gt;data);    // 노드의 방문
    InorderTraverse(bt-&gt;right, action);
}</code></pre>
<p>함수의 주소 값을 매개변수 action을 통해서 전달받도록 변경했다.
따라서 매개변수 action에 전달되는 함수의 내용에 따라서 노드의 방문 결과가 결정된다.
이와 관련된 예제는 다음과 같다.</p>
<ol>
<li><p>헤더파일 정의 (BinaryTree2.h)
: 앞서 정의한 헤더파일 BinaryTree.h에 중위, 후위, 전위 순회 관련 함수 선언을 추가한 것이다.</p>
<pre><code class="language-c">#ifndef __BINARY_TREE2_H__
#define __BINARY_TREE2_H__

typedef int BTData;

typedef struct _bTreeNode
{
   BTData data;
   struct _bTreeNode * left;
   struct _bTreeNode * right;
} BTreeNode;

BTreeNode * MakeBTreeNode(void);
BTData GetData(BTreeNode * bt);
void SetData(BTreeNode * bt, BTData data);

BTreeNode * GetLeftSubTree(BTreeNode * bt);
BTreeNode * GetRightSubTree(BTreeNode * bt);

void MakeLeftSubTree(BTreeNode * main, BTreeNode * sub);
void MakeRightSubTree(BTreeNode * main, BTreeNode * sub);

// 순회 방법 함수 추가
typedef void (*VisitFuncPtr)(BTData data);

void InorderTraverse(BTreeNode * bt, VisitFuncPtr action);
void PostorderTraverse(BTreeNode * bt, VisitFuncPtr action);
void PreorderTraverse(BTreeNode * bt, VisitFuncPtr action);

#endif</code></pre>
</li>
<li><p>소스파일 정의 (BinaryTree2.c)
: 헤더파일과 동일하게 앞서 정의한 BinaryTree.c에서 순회 관련 함수의 정의를 추가한 것이다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;BinaryTree2.h&quot;

BTreeNode * MakeBTreeNode(void)
{
   BTreeNode * nd = (BTreeNode *)malloc(sizeof(BTreeNode));
   nd-&gt;left = NULL;
   nd-&gt;right = NULL;
   return nd;
}

BTData GetData(BTreeNode * bt)
{
   return bt-&gt;data;
}

void SetData(BTreeNode * bt, BTData data)
{
   bt-&gt;data = data;
}

BTreeNode * GetLeftSubTree(BTreeNode * bt)
{
   return bt-&gt;left;
}

BTreeNode * GetRightSubTree(BTreeNode * bt)
{
   return bt-&gt;right;
}

void MakeLeftSubTree(BTreeNode * main, BTreeNode * sub)
{
   if(main-&gt;left != NULL)
       free(main-&gt;left);

   main-&gt;left = sub;
}

void MakeRightSubTree(BTreeNode * main, BTreeNode * sub)
{
   if(main-&gt;right != NULL)
       free(main-&gt;right);

   main-&gt;right = sub;
}

// 중위 순회
void InorderTraverse(BTreeNode * bt, VisitFuncPtr action)
{
   if(bt == NULL)
       return;

   InorderTraverse(bt-&gt;left, action);
   action(bt-&gt;data);
   InorderTraverse(bt-&gt;right, action);
}

// 후위 순회
void PostorderTraverse(BTreeNode * bt, VisitFuncPtr action)
{
   if(bt == NULL)
       return;

   PostorderTraverse(bt-&gt;left, action);
   PostorderTraverse(bt-&gt;right, action);
   action(bt-&gt;data);
}

// 전위 순회
void PreorderTraverse(BTreeNode * bt, VisitFuncPtr action)
{
   if(bt == NULL)
       return;

   action(bt-&gt;data);
   PreorderTraverse(bt-&gt;left, action);
   PreorderTraverse(bt-&gt;right, action);
}</code></pre>
</li>
<li><p>실행파일 정의 (BinaryTree2Main.c)
: 순회 관련 함수의 두 번째 인자로 전달되어 노드 방문의 결과를 결정하는 함수는 트리를 활용해서 프로그램을 구현하는 프로그램의 몫이 되어야 한다.
그래서 그러한 의미를 반영하기 위해 main 함수가 위치한 다음 소스파일에 두 번째 인자로 전달되는 함수를 정의했다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &quot;BinaryTree2.h&quot;
</code></pre>
</li>
</ol>
<p>// 노드 방문 결과 결정하는 함수
void ShowIntData(int data);</p>
<p>int main()
{
    BTreeNode * bt1 = MakeBTreeNode();
    BTreeNode * bt2 = MakeBTreeNode();
    BTreeNode * bt3 = MakeBTreeNode();
    BTreeNode * bt4 = MakeBTreeNode();
    BTreeNode * bt5 = MakeBTreeNode();
    BTreeNode * bt6 = MakeBTreeNode();</p>
<pre><code>SetData(bt1, 1);
SetData(bt2, 2);
SetData(bt3, 3);
SetData(bt4, 4);
SetData(bt5, 5);
SetData(bt6, 6);

MakeLeftSubTree(bt1, bt2);
MakeRightSubTree(bt1, bt3);
MakeLeftSubTree(bt2, bt4);
MakeRightSubTree(bt2, bt5);
MakeRightSubTree(bt3, bt6);

printf(&quot;&lt;Inorder Traversal&gt;\n&quot;);
InorderTraverse(bt1, ShowIntData);
printf(&quot;\n&quot;);
printf(&quot;&lt;Postorder Traversal&gt;\n&quot;);
PostorderTraverse(bt1, ShowIntData);
printf(&quot;\n&quot;);
printf(&quot;&lt;Preorder Traversal&gt;\n&quot;);
PreorderTraverse(bt1, ShowIntData);
printf(&quot;\n&quot;);

return 0;</code></pre><p>}</p>
<p>void ShowIntData(int data)
{
    printf(&quot;%d &quot;, data);
}</p>
<blockquote>
<p>gcc .\BinaryTree2.c .\BinaryTree2Main.c
.\a.exe
출력
<Inorder Traversal>
4 2 5 1 3 6 
<Postorder Traversal>
4 5 2 6 3 1
<Preorder Traversal>
1 2 4 5 3 6</p>
</blockquote>
<pre><code>
### *+) 이진 트리의 소멸*

지금까지 구현한 이진 트리에서 소멸 관련 함수가 정의되어 있지 않다.
따라서 이진 트리를 완전히 소멸시키는 함수를 다음과 같이 선언하고 정의했다.
```c
// 함수 선언 및 정의
void DeleteTree(BTreeNode * bt);

// 함수 호출
int main()
{
    BTreeNode * bt1 = MakeBTreeNode();
    ....
    DeleteTree(bt1);
    ....
}</code></pre><p>위에서 작성한 BinaryTree2.h와 BinaryTree2.c에 함수를 선언하고 정의해보자.</p>
<pre><code class="language-c">// BinaryTreeDelete.h - BinaryTree2.h에 아래 코드 추가
void DeleteTree(BTreeNode * bt);

// BinaryTreeDelete.c - BinaryTree2.c에 아래 코드 추가
void DeleteTree(BTreeNode * bt)
{
    if(bt == NULL)
        return;

    DeleteTree(bt-&gt;left);
    DeleteTree(bt-&gt;right);

    printf(&quot;Delete tree data: %d \n&quot;, bt-&gt;data);
    free(bt);
}

// BinaryTreeDeleteMain.c
#include &lt;stdio.h&gt;
#include &quot;BinaryTreeDelete.h&quot;

// 노드 방문 결과 결정하는 함수
void ShowIntData(int data);

int main()
{
    BTreeNode * bt1 = MakeBTreeNode();
    BTreeNode * bt2 = MakeBTreeNode();
    BTreeNode * bt3 = MakeBTreeNode();
    BTreeNode * bt4 = MakeBTreeNode();
    BTreeNode * bt5 = MakeBTreeNode();
    BTreeNode * bt6 = MakeBTreeNode();

    SetData(bt1, 1);
    SetData(bt2, 2);
    SetData(bt3, 3);
    SetData(bt4, 4);
    SetData(bt5, 5);
    SetData(bt6, 6);

    MakeLeftSubTree(bt1, bt2);
    MakeRightSubTree(bt1, bt3);
    MakeLeftSubTree(bt2, bt4);
    MakeRightSubTree(bt2, bt5);
    MakeRightSubTree(bt3, bt6);

    // 이진 트리 소멸
    printf(&quot;Delete Binary Tree...\n&quot;);
    DeleteTree(bt1);

    return 0;
}

void ShowIntData(int data)
{
    printf(&quot;%d &quot;, data);
}

&gt; gcc .\BinaryTreeDelete.c .\BinaryTreeDeleteMain.c
&gt; .\a.exe        
&gt; 출력
Delete Binary Tree...
Delete tree data: 4 
Delete tree data: 5
Delete tree data: 2
Delete tree data: 6
Delete tree data: 3
Delete tree data: 1</code></pre>
<p>트리 전체 삭제를 위한 함수에서의 핵심은 루트 노드가 마지막에 소멸되어야하기 때문에 후위 순회의 과정을 통해 데이터를 소멸해야한다는 것이다.
그리고 bt1을 소멸하므로써 루트 노드를 삭제했으므로 전체 이진 트리가 삭제된 것을 알 수 있다.</p>
<hr>
<h2 id="08-4-수식-트리expression-tree의-구현">08-4. 수식 트리(Expression Tree)의 구현</h2>
<p>이번에는 배운 이진 트리를 가지고 도구로 활용하여 이진 트리의 일종인 &#39;수식 트리&#39;에 대해 알아보자.</p>
<h3 id="수식-트리의-이해"><em>수식 트리의 이해</em></h3>
<p>이진 트리를 이용해서 수식을 표현해 놓은 것을 가리켜 &#39;수식 트리&#39;라 한다.
(수식 트리는 이진 트리와 구분되는 별개의 것이 아니다.)</p>
<p><code>7 + 4 * 2 - 1</code>이라는 수식이 있다.
이러한 수식을 컴퓨터는 스스로 알아서 인식할 수 없다.</p>
<pre><code class="language-c">int main()
{
    int result = 0;
    result = 7 + 4 * 2 - 1;
}</code></pre>
<p>위와 같은 수식을 컴파일러는 바로 우리가 아는 수식으로 인식하여 계산하지 않는다.
우리가 인식하는 방법을 결정하여 컴파일러에게 알려줘야 코드가 실행되고 결과를 도출하게 된다.
컴퓨터의 유연한 판단을 유도하는 것은 쉽지 않다.
따라서 가급적 정해진 일련의 과정을 거쳐서 수식을 인식할 수 있도록 도와야 한다.
이를 위해 수식 트리라는 것을 활용하여 컴파일러의 수식해석을 좋아지게 할 수 있다.
컴파일러의 수식 이해를 위해 수식 트리를 사용한 예시는 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/9684d498-3f71-44d7-b976-2caf46aa3f6d/image.png" alt=""></p>
<p>이 수식 트리를 보고 어떤 표기법을 사용하는지 궁금할 수 있다.
그런데 수식 트리는 그냥 수식 트리다.
중위 표기법이 수식 표현의 한 가지 방법이라면, 수식 트리도 수식을 표현하는 또 다른 방법일 뿐이다.
그럼 수식 트리의 계산과정은 어떻게 될까?
수식 트리를 구성하는 모든 서브 트리는 기본적으로 다음 방식으로 연산이 진행된다.
&quot;루트 노드에 저장된 연산자의 연산을 하되, 두 개의 자식 노드에 저장된 두 피연산자를 대상으로 연산한다.&quot;</p>
<p>즉, 위의 수식 트리에서는 다음 그림에서 보이듯 곱셈이 먼저 진행되어 그 결과가 + 연산자의 오른쪽 자식 노드를 대체하게 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/34669163-58a4-4ee4-b81c-43289766055a/image.png" alt=""></p>
<p>그리고 이어서 + 연산이 진행되고 그 결과가 - 연산자의 왼쪽 자식 노드를 대신하게 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/af3bb600-f886-4005-9c8a-6f14bf18b583/image.png" alt=""></p>
<p>마지막으로 15와 1을 대상으로 - 연산이 진행되어 최종 연산 결과인 14를 얻게 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/a69689a8-9b97-46e4-b00b-063fde8804c3/image.png" alt=""></p>
<p>이렇듯 수식 트리로 표현된 것을 코드로 구성하여 수식 트리를 만드는 프로그램을 만들어보자.</p>
<p>중위 표기법의 수식을 곧장 수식 트리로 표현하는 것은 복잡하고 힘들다.
하지만 후위 표기법 수식을 수식 트리로 표현하는 것은 비교적 간단하다.
따라서 <code>중위 표기법의 수식 → 후위 표기법의 수식 → 수식 트리</code>라는 과정을 거쳐서 수식 트리를 만들 것이다.
Chapter 06에서 중위 표기법의 수식을 후위 표기법의 수식으로 바꾸는 함수를 정의했었다.
따라서 이번 Chapter에서는 후위 표기법의 수식을 수식 트리로 바꾸는 것이 주가 된다.</p>
<h3 id="1-헤더파일-정의-expressiontreeh"><em>1) 헤더파일 정의 (ExpressionTree.h)</em></h3>
<p>수식 트리는 이진 트리의 한 종류기 때문에 위에서 만들어 놓은 이진 트리를 활용해서 수식 트리로 바꾸는 코드를 작성할 것이다.
수식 트리를 만드는 과정에는 스택을 필요로 한다.
(이것도 이전 Chapter에서 만들어 놓은 것을 활용하면 된다.)
따라서 프로그램 구현에 필요한 파일들은 아래와 같다.</p>
<ul>
<li>수식 트리 구현에 필요한 이진 트리 : <a href="https://velog.io/@mingming_eee/datastructure-08#1-%ED%97%A4%EB%8D%94%ED%8C%8C%EC%9D%BC-%EC%A0%95%EC%9D%98">BinaryTree2.h</a>, <a href="https://velog.io/@mingming_eee/datastructure-08#3-%EC%86%8C%EC%8A%A4%ED%8C%8C%EC%9D%BC-%EC%A0%95%EC%9D%98">BinaryTree2.c</a></li>
<li>수식 트리 구현에 필요한 스택 : <a href="https://velog.io/@mingming_eee/datastructure-06#%EC%97%B0%EA%B2%B0-%EB%A6%AC%EC%8A%A4%ED%8A%B8-%EA%B8%B0%EB%B0%98-%EC%8A%A4%ED%83%9D-%EA%B5%AC%ED%98%84-1-%ED%97%A4%EB%8D%94%ED%8C%8C%EC%9D%BC%EC%9D%98-%EC%A0%95%EC%9D%98">ListBaseStack.h</a>, <a href="https://velog.io/@mingming_eee/datastructure-06#%EC%97%B0%EA%B2%B0-%EB%A6%AC%EC%8A%A4%ED%8A%B8-%EA%B8%B0%EB%B0%98-%EC%8A%A4%ED%83%9D-%EA%B5%AC%ED%98%84-2-%EC%86%8C%EC%8A%A4%ED%8C%8C%EC%9D%BC%EC%9D%98-%EC%A0%95%EC%9D%98">ListBaseStack.c</a></li>
</ul>
<p>수식 트리의 표현을 위한 헤더파일은 다음과 같다.</p>
<pre><code class="language-c">#ifndef __EXPRESSION_TREE_H__
#define __EXPRESSION_TREE_H__

#include &quot;BinaryTree2.h&quot;

BTreeNode * MakeExpTree(char exp[]);        // 수식 트리 구성
int EvaluateExpTree(BTreeNode * bt);        // 수식 트리 계산

void ShowPrefixTypeExp(BTreeNode * bt);     // 전위 표기법 기반 출력
void ShowInfixTypeExp(BTreeNode * bt);      // 중위 표기법 기반 출력
void ShowPostfixTypeExp(BTreeNode * bt);    // 후위 표기법 기반 출력

#endif</code></pre>
<p>위의 헤더파일에 선언된 함수 중에서 핵심이라 할 수 있는 첫 번째 함수는 다음과 같다.
(이 함수가 바로 수식 트리를 구성하는 함수다.)</p>
<p><code>BTreeNode * MakeExpTree(char exp[]);</code>
이 함수는 후위 표기법의 수식을 문자열의 형태로 입력 받으면 이를 기반으로 수식 트리를 구성하고 수식 트리의 주소 값(수식 트리의 루트 노드의 주소 값)을 반환한다.</p>
<p>이어서 핵심이 되는 두 번째 함수는 인자로 전달된 수식 트리의 수식을 계산해서 그 결과를 반환하는 함수다.
<code>int EvaluateExpTree(BTreeNode * bt);</code></p>
<p>마지막으로 수식 트리의 구성을 검증하기 위해서 각각 수식 트리의 수식을 전위, 중위, 후위 표기법으로 출력하는 기능의 함수다.
이 세 함수는 트리의 순회와 관련이 있어 그 구현이 어렵지 않다.
수식 트리를 후위 순회하면서 ㄴ드에 저장된 데이터를 출력하면 그 결과가 바로 후위 표기법의 수식이 된다.</p>
<h3 id="2-소스파일-정의---후위-표기법-기반-expressiontreec"><em>2) 소스파일 정의 - 후위 표기법 기반 (ExpressionTree.c)</em></h3>
<p>함수 <code>MakeExpTree</code>의 구현을 위해 후위 표기법의 수식을 수식 트리로 표현하는 방법을 알아야 한다.
<code>1 2 + 7 *</code>라는 후위 표기법의 수식을 예를 들어보자.
이를 수식 트리로 표현하면 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/8297e348-9c6f-4bd2-816c-3cfe9903c1b8/image.png" alt=""></p>
<p>수식 트리의 구성을 위해서 후위 표기법의 다음 두 가지 특징을 알고 있어야 한다.</p>
<ol>
<li>연산 순서대로 왼쪽에서 오른쪽으로 연산자가 나열된다.</li>
<li>해당 연산자의 두 피연산자는 연산자 앞에 나열된다.</li>
</ol>
<p>그런데 수식 트리에서는 트리의 아래쪽에 위치한 연산자들의 연산이 먼저 진행된다.
때문에 후위 표기법의 수식에서 먼저 등장하는 피연산자와 연산자를 이용해서 트리의 하단을 만들고 이를 바탕으로 점진적으로 트리의 윗부분을 구성해 나가야 한다.</p>
<p>그렇다면 수식 트리의 구성과정에 대해 알아보자.</p>
<table>
<thead>
<tr>
<th>No.</th>
<th>Image</th>
<th>Explanation</th>
</tr>
</thead>
<tbody><tr>
<td>1.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/9cae43c7-20eb-4946-96ab-8676257aa950/image.png" alt=""></td>
<td>수식 트리를 만드는데 사용할 수식,<br> 스택을 의미하는 쟁반 필요.</td>
</tr>
<tr>
<td>2.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/5941f055-82e3-4141-b1d5-33c98aee6295/image.png" alt=""></td>
<td>수식을 이루는 문자를 하나씩 처리<br>문자가 피연산자일 경우 스택(쟁반)으로 옮기기.<br></td>
</tr>
<tr>
<td>3.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/e4a166db-8d7f-43ef-8e7e-bdd496b40090/image.png" alt=""></td>
<td>두 번째 문자도 피연산자로 스택(쟁반)으로 옮기기.<br></td>
</tr>
<tr>
<td>4.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/fb49dda4-e3f0-472f-bd5d-38f83de4634e/image.png" alt=""></td>
<td>연산자가 등장하면 스택에 쌓여있는 두 개의 피연산자를 꺼내어 연산자의 자식 노드로 연결.<br>먼저 꺼낸 피연산자가 오른쪽 자식 노드가 되고,<br>그 다음에 꺼낸 피연산자가 왼쪽 자식 노드가 됨.</td>
</tr>
<tr>
<td>5.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/907da785-7cd4-408b-a63d-4abec164dada/image.png" alt=""></td>
<td>만든 수식 트리 자체를 스택(쟁반)으로 옮기기.☆<br>트리 전체가 다른 연산자의 자식 노드가 되기 때문.<br>그림으로는 트리 자체가 스택으로 옮겨지는 듯 하나<br>실제로는 + 연산자가 저장된 노드의 주소값만 스택으로 옮겨진다.</td>
</tr>
<tr>
<td>6.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/e99f09fe-beec-48ef-8566-cf26e74c6e5d/image.png" alt=""></td>
<td>피연산자를 스택(쟁반)으로 옮기기.</td>
</tr>
<tr>
<td>7.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/38c999f3-5629-4cb9-bfb7-147b46cbe306/image.png" alt=""></td>
<td>최종 과정으로 연산자가 등장하면서 스택에서 두 개의 <br>노드를 꺼내어 각각 오른쪽과 왼쪽의 자식 노드로 연결.</td>
</tr>
</tbody></table>
<p>위 구성 과정을 정리한 수식 트리를 구성하는 방법은 다음과 같다.</p>
<ol>
<li>피연산자를 만나면 무조건 스택으로 옮기기.</li>
<li>연산자를 만나면 스택에서 두 개의 피연산자를 꺼내어 자식 노드로 연결.</li>
<li>자식 노드를 연결해서 만들어지 늩리는 다시 스택으로 옮기기.</li>
</ol>
<p>이 과정을 코드로 구현하면 다음과 같다.</p>
<pre><code class="language-c">#include &quot;ListBaseStack.h&quot;
#include &quot;BinaryTree2.h&quot;
.... 몇몇의 표준 헤더파일의 선언....

BTreeNode * MakeExpTree(char exp[])
{
    Stack stack;
    BTreeNode * pnode;

    int expLen = strlen(exp);
    int i;
    StackInit(&amp;stack);

    for(i=0, i&lt;expLen; i++)
    {
        pnode = MakeBTreeNode();

        if(isdigit(exp[i]))        // 피연산자일 경우
        {
            SetData(pnode, exp[i]-&#39;0&#39;);        // 문자를 정수로 바꿔서 저장
        }
        else    // 연산자일 경우
        {
            MakeRightSubTree(pnode, SPop(&amp;stack));    // 스택에서 꺼내어 오른쪽 자식 노드로 연결
            MakeLeftSubTree(pnode, SPop(&amp;stack));    // 스택에서 꺼내어 왼쪽 자식 노드로 연결
            SetData(pnode, exp[i]);
        }

        SPush(&amp;stack, pnode);
    }

    return SPop(&amp;stack);
}</code></pre>
<p>이렇게 수식 트리로 작성하는 함수가 제대로 동작하는지 확인해야한다.</p>
<h3 id="2-소스파일-정의---수식-트리의-순회-expressiontreec"><em>2) 소스파일 정의 - 수식 트리의 순회 (ExpressionTree.c)</em></h3>
<p>수식 트리가 잘 구현되었는지 확인하기 위해서 아까 우리는 세 가지 방식인 전위, 중위, 후위 순회를 하면서 노드에 저장된 데이터를 출력하는 함수를 사용하려고 했다.
이 세 함수에 대한 정의를 구현해보자.</p>
<ul>
<li>전위 순회하여 데이터를 출력한 결과 : 전위 표기법의 수식</li>
<li>중위 순회하여 데이터를 출력한 결과 : 중위 표기법의 수식</li>
<li>후위 순회하여 데이터를 출력한 결과 : 후위 표기법의 수식</li>
</ul>
<p>이 함수들을 헤더파일에는 다음과 같이 선언했었다.</p>
<pre><code class="language-c">void ShowPrefixTypeExp(BTreeNode * bt);     // 전위 표기법 기반 출력
void ShowInfixTypeExp(BTreeNode * bt);      // 중위 표기법 기반 출력
void ShowPostfixTypeExp(BTreeNode * bt);    // 후위 표기법 기반 출력</code></pre>
<p>이것을 그대로 활용하는 것이 아닌 이진 트리 구현에서 전위, 중위, 후위 순회한 것을 이용해서 함수를 정의해야한다.</p>
<pre><code class="language-c">// BinaryTree2.h에서 선언한 순회 함수
typedef void (*VisitFuncPtr)(BTData data);
void PreorderTraverse(BTreeNode * bt, VisitFuncPtr action);
void InorderTraverse(BTreeNode * bt, VisitFuncPtr action);
void PostorderTraverse(BTreeNode * bt, VisitFuncPtr action);</code></pre>
<p>때문에 노드에 저장된 데이터를 출력하는 함수는 다음과 같이 정의할 수 있다.</p>
<pre><code class="language-c">void ShowNodeData(int data)
{
    if(0&lt;=data &amp;&amp; data &lt;=9)
        printf(&quot;%d &quot;, data);    // 피연산자 출력
    else
        printf(&quot;%c &quot;, data);    // 연산자 출력
}</code></pre>
<p>그리고 이를 기반으로 헤더 파일에 선언된 함수들은 다음과 같이 채우면 된다.</p>
<pre><code class="language-c">void ShowPrefixTypeExp(BTreeNode * bt)    // 전위 표기법으로 수식 출력
{
    PreorderTraverse(bt, ShowNodeData);
}

void ShowInfixTypeExp(BTreeNode * bt)    // 중위 표기법으로 수식 출력
{
    InorderTraverse(bt, ShowNodeData);
}

void ShowPostfixTypeExp(BTreeNode * bt)    // 후위 표기법으로 수식 출력
{
    PostorderTraverse(bt, ShowNodeData);
}</code></pre>
<p>위에서 정의한 함수들을 정리하면 아래와 같다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &lt;ctype.h&gt;
#include &quot;ListBaseStack.h&quot;
#include &quot;BinaryTree2.h&quot;

BTreeNode * MakeExpTree(char exp[])
{
    Stack stack;
    BTreeNode * pnode;

    int expLen = strlen(exp);
    int i;

    StackInit(&amp;stack);

    for(i=0; i&lt;expLen; i++)
    {
        pnode = MakeBTreeNode();

        if(isdigit(exp[i]))        // 피연산자일 경우
        {
            SetData(pnode, exp[i]-&#39;0&#39;);        // 문자를 정수로 바꿔서 저장
        }
        else    // 연산자일 경우
        {
            MakeRightSubTree(pnode, SPop(&amp;stack));    // 스택에서 꺼내어 오른쪽 자식 노드로 연결
            MakeLeftSubTree(pnode, SPop(&amp;stack));    // 스택에서 꺼내어 왼쪽 자식 노드로 연결
            SetData(pnode, exp[i]);
        }

        SPush(&amp;stack, pnode);
    }

    return SPop(&amp;stack);
}

int EvaluateExpTree(BTreeNode * bt)
{
    // 잠시 후 구현할 함수
}

void ShowNodeData(int data)
{
    if(0&lt;=data &amp;&amp; data &lt;=9)
        printf(&quot;%d &quot;, data);    // 피연산자 출력
    else
        printf(&quot;%c &quot;, data);    // 연산자 출력
}

void ShowPrefixTypeExp(BTreeNode * bt)    // 전위 표기법으로 수식 출력
{
    PreorderTraverse(bt, ShowNodeData);
}

void ShowInfixTypeExp(BTreeNode * bt)    // 중위 표기법으로 수식 출력
{
    InorderTraverse(bt, ShowNodeData);
}

void ShowPostfixTypeExp(BTreeNode * bt)    // 후위 표기법으로 수식 출력
{
    PostorderTraverse(bt, ShowNodeData);
}</code></pre>
<h3 id="3-실행파일-정의-expressiontreemainc"><em>3) 실행파일 정의 (ExpressionTreeMain.c)</em></h3>
<p>지금까지 구현한 내용을 바탕으로 수식 트리의 헤더파일과 소스파일을 제시하고 이를 테스트하기 위한 main 함수(실행파일)을 정의하겠다.
그리고 실행을 위해 필요한 파일들을 정리하면 다음과 같다.</p>
<ul>
<li><p>이진 트리 관련
: BinaryTree2.h, BinaryTree2.c</p>
</li>
<li><p>스택 관련
: ListBaseStack.h, ListBaseStack.c</p>
</li>
<li><p>수식 트리 관련
: ExpressionTree.h, ExpressionTree.c</p>
</li>
<li><p>main 함수 관련
: ExpressionMain.c</p>
</li>
</ul>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &quot;ExpressionTree.h&quot;

int main()
{
    char exp[] = &quot;12+7*&quot;;
    BTreeNode * eTree = MakeExpTree(exp);

    printf(&quot;Prefix Type Expression: &quot;);
    ShowPrefixTypeExp(eTree);
    printf(&quot;\n&quot;);

    printf(&quot;Infix Type Expression: &quot;);
    ShowInfixTypeExp(eTree);
    printf(&quot;\n&quot;);

    printf(&quot;Postfix Type Expression: &quot;);
    ShowPostfixTypeExp(eTree);
    printf(&quot;\n&quot;);

    printf(&quot;Result of Calculation: %d \n&quot;, EvaluateExpTree(eTree));

    return 0;
}

&gt; gcc .\BinaryTree2.c .\ExpressionTree.c .\ExpressionTreeMain.c .\ListBaseStack.c
&gt; .\a.exe
&gt; 출력
Prefix Type Expression: * + 1 2 7
Infix Type Expression: 1 + 2 * 7
Postfix Type Expression: 1 2 + 7 *
Result of Calculation: 12720832</code></pre>
<p>이제 여기서 아직 구현하지 않은 함수 EvaluateExpTree 정의하기와 중위 표기법에서 소괄호 출력 하기에 대해 추가로 더 알아보자.</p>
<h3 id="4-수식-트리-계산-함수-정의하기"><em>4) 수식 트리 계산 함수 정의하기</em></h3>
<p>수식 트리에 담겨있는 수식을 계산하는 함수를 정의해보자.
보통 트리이기 때문에 순회와 동일하게 단말 노드가 붙어 있는 서브 트리에서부터 계산해야 한다고 생각한다.
하지만 트리는 재귀적인 구조를 띄기 때문에 접근방법을 달리해야 한다.
먼저 함수의 정의는 다음과 같다.</p>
<pre><code class="language-c">int EvaluateExpTree(BTreeNode * bt)
{
    int op1, op2;

    op1 = GetData(GetLeftSubTree(bt));    // 첫 번째 피연산자
    op2 = GetData(GetRightSubTree(bt));    // 두 번째 피연산자

    switch(GetData(bt))        // 연산자 확인하여 연산 진행
    {
    case &#39;+&#39;:
        return op1+op2;
    case &#39;-&#39;:
        return op1-op2;
    case &#39;*&#39;:    
        return op1*op2;
    case &#39;/&#39;:
        return op1/op2;
    }
    return 0;
}</code></pre>
<p>위 함수는 두 개의 자식 노드에 담겨있는 두 피연산자를 확인하고, 부모 노드에 저장된 연산자를 확인하여 연산을 진행한다.</p>
<p>근데 문제는 자식 노드에 피연산자가 아닌 서브 트리가 달려있는 경우 발생한다.
위 함수를 어떻게 develop 시켜야할까?
따라서 다음과 같이 수정한다면 서브 트리의 수식도 계산할 수 있을 것이다.</p>
<pre><code class="language-c">int EvaluateExpTree(BTreeNode * bt)
{
    int op1, op2;

    op1 = EvaluateExpTree(GetLeftSubTree(bt));        // 왼쪽 서브 트리 계산
    op2 = EvaluateExpTree(GetRightSubTree(bt));        // 오른쪽 서브 트리 계산

    switch(GetData(bt))        // 연산자 확인하여 연산 진행
    {
    case &#39;+&#39;:
        return op1+op2;
    case &#39;-&#39;:
        return op1-op2;
    case &#39;*&#39;:    
        return op1*op2;
    case &#39;/&#39;:
        return op1/op2;
    }
    return 0;
}</code></pre>
<p>하지만 이렇게 함수를 정의한다면 자식 노드가 단말 노드일 경우 문제가 발생하고,
재귀함수의 탈출조건도 존재하지 않는다.</p>
<p>피연산자를 받는 문장(왼쪽, 오른쪽 서브 트리 계산 문장)을 보면 서브 트리를 대상으로 서브 트리의 주소 값을 전달하면 EvaluateExpTree 함수를 호출하고 있다.
따라서 함수의 탈출 조건은 <strong>&quot;던잘된 것이 서브 트리가 추가로 달려있지 않은 단말 노드의 주소 값이라면 단말 노드에 저장된 피연산자를 반환&quot;</strong>으로 하면 된다.</p>
<p>따라서 완성된 EvaluateExpTree 함수는 다음과 같다.</p>
<pre><code class="language-c">int EvaluateExpTree(BTreeNode * bt)
{
    int op1, op2;

    if(GetLeftSubTree(bt)==NULL &amp;&amp; GetRightSubTree(bt)==NULL)    // 단말 노드의 경우
        return GetData(bt);

    op1 = EvaluateExpTree(GetLeftSubTree(bt));
    op2 = EvaluateExpTree(GetRightSubTree(bt));

    switch(GetData(bt))
    {
    case &#39;+&#39;:
        return op1+op2;
    case &#39;-&#39;:
        return op1-op2;
    case &#39;*&#39;:
        return op1*op2;
    case &#39;/&#39;:
        return op1/op2;
    }

    return 0;
}</code></pre>
<p>위 함수 정의를 ExpressionTree.c 파일에 추가하고 다시 main 함수를 실행하면 다음과 같은 출력 결과를 얻을 수 있다.</p>
<pre><code class="language-c">Prefix Type Expression: * + 1 2 7 
Infix Type Expression: 1 + 2 * 7
Postfix Type Expression: 1 2 + 7 *
Result of Calculation: 21</code></pre>
<h3 id="-중위-표기법의-소괄호-출력"><em>+) 중위 표기법의 소괄호 출력</em></h3>
<p>소괄호를 출력할 때 소괄호를 어디까지 출력해야할지 정해야한다.
그 이유는</p>
<pre><code class="language-c">3 + 2 * 7
3 + ( 2 * 7 )
( 3 + ( 2 * 7 ) )</code></pre>
<p>위 세 수식이 모두 같은 수식이기 때문이다.
따라서 혼동이 없도록 연산자의 수와 소괄호 한쌍의 수를 일치시켜 (마지막 수식 방식) 중위 표기법을 출력할 것이다.
ShowInfixTypeExp 함수를 다음과 같이 수정하면 된다.</p>
<pre><code class="language-c">void ShowInfixTypeExp(BTreeNode * bt)
{
    if(bt == NULL)
        return;

    if(bt-&gt;left != NULL || bt-&gt;right != NULL)
        printf(&quot; ( &quot;);

    ShowInfixTypeExp(bt-&gt;left);        // 첫 번째 피연산자 출력
    ShowNodeData(bt-&gt;data);            // 연산자 출력
    ShowInfixTypeExp(bt-&gt;right);    // 두 번째 피연산자 출력

    if(bt-&gt;left != NULL || bt-&gt;right != NULL)
        printf(&quot; ) &quot;);
}</code></pre>
<p>수정 전 함수는 InorderTraverse 함수를 호출하는 형태였지만, 순회의 과정에서 소괄호를 출력해야 하기 때문에 직접 순회하는 코드를 작성했다.</p>
<p>따라서 수정된 코드를 실행하면 다음과 같은 결과를 얻을 수 있다.</p>
<pre><code class="language-c">Prefix Type Expression: * + 1 2 7 
Infix Type Expression:  (  ( 1 + 2  ) * 7  )
Postfix Type Expression: 1 2 + 7 *
Result of Calculation: 21</code></pre>
<hr>
<h2 id="review"><code>&lt;Review&gt;</code></h2>
<p>오늘도 즐겁게 자료구조 공부를 했다.
요즘 계속해서 새로운 도전을 하고 있는데
트리를 보다보니 내가 하는 새로운 도전들이 자식 노드 하나하나가 되어 나중엔 최종 루트 노드에 도달할 수 있지 않을까 라는 생각을 하면서 공부했다.</p>
<p>트리를 표현해서 각 노드를 순회하는 것으로 노드의 값에 접근하고 원하는 노드 값을 가져와 연산할 수 있다는 것이 트리의 간편한 부분인 것 같다.
트리의 순회도 3가지나 있어 더 매력적으로 다가왔다.</p>
<p>앞으로 우선순위 큐와 힙에 대해 배울텐데 더 어려울 것이 예상되지만 기대가 된다...!
오늘도 고생했다~!</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/c5e1de72-1301-4882-8d6a-285c35deefbc/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chapter 07. 큐(Queue)]]></title>
            <link>https://velog.io/@mingming_eee/datastructure-07</link>
            <guid>https://velog.io/@mingming_eee/datastructure-07</guid>
            <pubDate>Wed, 30 Oct 2024 02:15:22 GMT</pubDate>
            <description><![CDATA[<h2 id="07-1-큐의-이해와-adt-정의">07-1. 큐의 이해와 ADT 정의</h2>
<p>큐는 스택과 함께 언급되고 비교되는 자료구조다.
스택은 먼저 들어간 데이터가 나중에 나오는데에 반해
큐는 먼저 들어간 데이터가 먼저 나오기 때문이다.
이것이 큐와 스택의 유일한 차이점이다...!
그렇다면 연결 리스트 기반으로 큐를 구현할 때 tail이 아닌 head에 새 데이터를 추가하면 될까...?
배우면서 맞는지 살펴보자!</p>
<h3 id="큐queue의-이해"><em>큐(Queue)의 이해</em></h3>
<p>큐(Queue)는 선입선출(先入先出)구조의 자료구조다. FIFO(First-In, First-Out) 구조의 자료구조라고도 한다.
일상생활 속에서 줄서기와 터널, 호스 등 다양하게 큐란 자료구조의 모습을 볼 수 있다.</p>
<h3 id="큐queue-adt-정의"><em>큐(Queue) ADT 정의</em></h3>
<p>스택과 마찬가지로 큐의 ADT도 정형화된 편이다.
그 중 핵심이 되는 두 가지 연산은 다음과 같다.</p>
<ul>
<li>enqueue : 큐에 데이터를 넣는 연산 (스택의 Push와 비슷)</li>
<li>dequeue : 큐에 데이터를 꺼내는 연산 (스택의 Pop과 비슷)</li>
</ul>
<p>따라서 이 연산에 대한 ADT는 다음과 같다.</p>
<p>✅Operations:</p>
<ul>
<li><p>void QueueInit(Queue * pq);</p>
<ul>
<li>큐의 초기화를 진행</li>
<li>큐 생성 후 제일 먼저 호출되는 함수</li>
</ul>
</li>
<li><p>int QIsEmpty(Queue * pq);</p>
<ul>
<li>큐가 빈 경우 TRUE(1), 그렇지 않은 경우 FALSE(0)을 반환</li>
</ul>
</li>
<li><p>void Enqueue(Queue * pq, Data data);</p>
<ul>
<li>큐에 데이터를 저장. 매개변수 data로 전달된 값 저장</li>
</ul>
</li>
<li><p>Data Dequeue(Queue * pq);</p>
<ul>
<li>저장순서가 가장 앞선 데이터 삭제</li>
<li>삭제된 데이터는 반환</li>
<li>본 함수의 호출을 위해서는 데이터가 하나 이상 존재 필수. (QIsEmpty 함수 활용)</li>
</ul>
</li>
<li><p>Data QPeek(Queue * pq);</p>
<ul>
<li>저장순서가 가장 앞선 데이터를 반환하되 삭제하지 않음.</li>
<li>본 함수의 호출을 위해서는 데이터가 하나 이상 존재 필수. (QIsEmpty 함수 활용)</li>
</ul>
</li>
</ul>
<p>ADT를 정의했으니 이제 큐를 구현해보려 하는데 큐도 스택과 동일하게 배열 기반, 연결 리스트 기반으로 구현할 수 있다.</p>
<hr>
<h2 id="07-2-큐의-배열-기반-구현">07-2. 큐의 배열 기반 구현</h2>
<h3 id="큐의-구현에-대한-논리"><em>큐의 구현에 대한 논리</em></h3>
<p>큐와 스택에 대한 차이가 데이터가 앞에서 꺼내지는지 뒤에서 꺼내지는지 밖에 없으니 구현해 놓은 스택을 대상으로 꺼내는 방법만 조금만 바꾸면 큐를 구현할 수 있지 않을까? 생각할 수도 있다.
하지만 큐의 구현 모델을 생각해보면 생각보다 차이가 크다고 느껴질 것이다.
예를 들어 enqueue 연산을 생각해보면 아래 그림과 같이 F는 Front의 약자로 큐의 머리를, R은 Rear의 약자로 큐의 꼬리를 가리킨다고 해보자.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/862e2215-3ec5-4edf-97db-ee2fabc9e099/image.png" alt=""></p>
<p>enqueue 연산시 R이 다음칸으로 가고 그 자리에 새로운 데이터가 저장된다.
하지만 dequeue 연산을 생각해보면 어떨까?</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/58a47097-d70f-40b4-b08c-a2818ee3687d/image.png" alt=""></p>
<p>F가 가리키는 데이터를 대상으로 데이터가 반환되고 사라진다. 
즉, F를 참조하여 dequeue 연산을 하고, R을 참조하여 enqueue 연산을 한다.
(뒤로 데이터를 넣고 앞으로 데이터를 빼는 구조)</p>
<p>만약 dequeue 연산을 할 때 단순히 데이터를 반환하는데 그치지 않고 배열에 데이터들을 앞쪽으로 계속해서 땡겨 채우게 되면 어떻게 될까?
이 방법을 적용하면 dequeue 연산의 대상이 맨 앞부분에 위치하므로 F가 필요없어진다.
단, 이 방식은 dequeue 연산 시마다 저장된 데이터를 한 칸씩 이동시켜야 하는 단점이 있다.
배열 기반의 큐에서는 위의 방식으로 dequeue 연산을 진행하지 않는다.</p>
<p>그래서 이전에 보여준 그림과 같은 방식을 사용하지만 이때도 약간의 문제가 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/e06f93fc-c54f-498f-a57e-5107f0fc8bc1/image.png" alt=""></p>
<p>배열의 맨 끝까지 데이터가 저장될 경우 R을 더이상 오른쪽으로 이동시킬 수 없다.
이럴 경우 enqueue 연산을 어떻게 할까?
R을 다시 배열의 시작으로 옮기는 것이다. 쉽게 생각해서 R을 회전시키는 것이다.
이런 방식으로 동작하는 배열 기반의 큐를 가리켜 &#39;원형 큐(Circular queue)&#39;라 한다.</p>
<h3 id="원형-큐circular-queue"><em>원형 큐(Circular Queue)</em></h3>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/04fd2210-44f8-40a2-83cf-d904a05c075f/image.png" alt=""></p>
<p>위에서 설명한 배열 기반의 큐를 원형큐로 나타내면 바로 위 그림과 같다.
위 그림과 같은 다음 그림과 같은 과정을 거치면서 완성된 모습니다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/71790e05-81be-4aae-a1a9-3b8f6ad98733/image.png" alt=""></p>
<p>그리고 위 그림에서 dequeue 연산 2회를 진행하면 다음과 같아진다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/8f193942-d902-4d79-9997-105e6f9acd2c/image.png" alt=""></p>
<p>이제 이 모습들을 구현하면 되는데...
그 전에 먼저 그림 07-6 상황에서 enqueue 연산을 진행해 데이터가 큐에 다 채워진 모습을,</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/a5a787ee-ffe5-49ff-9def-f390c19532d1/image.png" alt=""></p>
<p>그림 07-7 상황에서 dequeue 연산을 진행해 데이터가 큐에 하나도 없는 모습을 생각해보자.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/beb3a070-0944-4d29-9dba-47f21d204f48/image.png" alt=""></p>
<p>위 두 상황 모두 F가 R보다 한칸 앞 서 있다는 것을 알 수 있다.
이걸로 우리는 큐가 꽉 찬 상황과 큐가 텅 빈 상황을 구분할 수 없다는 것을 알 수 있다.
이를 해결하기 위해서 배열의 길이가 N이라면 N-1개 채워졌을 때 이를 꽉찬 것으로 간주한다.</p>
<p>그럼 텅 빈 상황에서 큐의 상황은 아래와 같고,</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/fa8bb21e-17c7-47cf-8821-9a0df56c1867/image.png" alt=""></p>
<p>일련의 과정을 거쳐서 데이터가 다 찬 상황에서 큐의 상황은 아래와 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/1653bbe3-21f5-4c3b-82bb-cc17b00f016c/image.png" alt=""></p>
<p>enqueue 연산시 R이 가리키는 위치를 한 칸 이동시킨 다음에 R이 가리키는 위치에 데이터를 저장한다.
dequeue 연산시에는 F가 가리키는 위치를 한 칸 이동시키고 F가 가리키는 위치에 저장된 데이터를 반환 및 소멸한다.</p>
<p>그리고 원형 큐가 텅 빈 상태에는 F와 R이 동일한 위치를 가리키고,
원형 큐가 꽉 찬 상태에는 R이 가리키는 앞을 F가 가리키고 있다.</p>
<p>우리는 이러한 특성들을 코드 구현할 때 명심하고 옮겨야한다.</p>
<h3 id="원형-큐의-구현"><em>원형 큐의 구현</em></h3>
<p>배열 기반의 큐라 하면 대부분의 경우 원형 큐를 의미한다고 생각해도 된다.
따라서 배열 기반의 큐를 대표하는 원형 큐를 구현해보도록 하자.</p>
<h4 id="1-헤더파일circularqueueh">1) 헤더파일(CircularQueue.h)</h4>
<p>우선 정의한 큐의 ADT를 바탕으로 헤더파일을 정의하자.
큐를 나타내는 구조체를 CQueue라고 하며 그 안에는 front와 rear라는 변수가 있다.</p>
<pre><code class="language-c">#ifndef __C_QUEUE_H__
#define __C_QUEUE_H__

#define TRUE    1
#define FALSE   0

#define QUE_LEN     100

typedef int Data;

typedef struct _cQueue
{
    int front;
    int rear;
    Data queArr[QUE_LEN];
} CQueue;

typedef CQueue Queue;

void QueueInit(Queue * pq);
int QIsEmpty(Queue * pq);

void Enqueue(Queue * pq, Data data);
int Dequeue(Queue * pq);
int QPeek(Queue * pq);

#endif</code></pre>
<h4 id="2-소스파일circularqueuec">2) 소스파일(CircularQueue.c)</h4>
<p>앞서 헤더파일에서 선언된 함수들을 정의할 차례다.
그 전에 원형 큐의 핵심이 되는 함수를 하나 먼저 알고가자.</p>
<pre><code class="language-c">int NextPosIdx(int pos)    // 큐의 다음 위치에 해당하는 인덱스 값 반환 함수
{
    if(pos == QUE_LEN-1)
        return 0;
    else
        return pos+1;
}</code></pre>
<p>이 함수는 1을 전달하면 2를 반환하고, pos를 전달하면 pos+1을 반환한다.
큐의 길이보다 하나 작은 값이 인자로 전달되면 0을 반환해 F와 R의 회전을 할 수 있게 도와준다.</p>
<p>이제 함수들을 정의해보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;CircularQueue.h&quot;

void QueueInit(Queue * pq)
{   
    // 큐의 첫 시작은 front와 rear 모두 동일한 위치(0)를 가리킴
    pq-&gt;front = 0;
    pq-&gt;rear = 0;
}

int QIsEmpty(Queue * pq)
{
    if(pq-&gt;front == pq-&gt;rear)   // 큐가 텅 빈 경우
        return TRUE;
    else
        return FALSE;
}

// 큐의 다음 위치에 해당하는 인덱스 값 반환 함수
int NextPosIdx(int pos)    
{
    if(pos == QUE_LEN-1)    // 배열의 마지막 요소의 인덱스 값인 경우
        return 0;
    else
        return pos+1;
}

void Enqueue(Queue * pq, int data)
{
    if(NextPosIdx(pq-&gt;rear) == pq-&gt;front)   // 큐가 다 찬 경우
    {
        printf(&quot;Queue Memory Error!&quot;);
        exit(-1);
    }

    pq-&gt;rear = NextPosIdx(pq-&gt;rear);    // rear을 한 칸 이동
    pq-&gt;queArr[pq-&gt;rear] = data;        // rear이 가리키는 곳에 데이터 저장
}

Data Dequeue(Queue * pq)
{
    if(QIsEmpty(pq))
    {
        printf(&quot;Queue Memory Error!&quot;);
        exit(-1);
    }

    pq-&gt;front = NextPosIdx(pq-&gt;front);    // front를 한 칸 이동
    return pq-&gt;queArr[pq-&gt;front];        // front가 가리키는 데이터 반환
}

Data QPeek(Queue * pq)
{
    if(QIsEmpty(pq))
    {
        printf(&quot;Queue Memory Error!&quot;);
        exit(-1);
    }

    return pq-&gt;queArr[NextPosIdx(pq-&gt;front)];
}</code></pre>
<h4 id="3-실행파일circularqueuemainc">3) 실행파일(CircularQueueMain.c)</h4>
<p>구현한 원형 큐의 enqueue 연산과 dequeue 연산을 실행파일을 통해 살펴보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &quot;CircularQueue.h&quot;

int main()
{
    // Queue의 생성 및 초기화
    Queue q;
    QueueInit(&amp;q);

    // 데이터 넣기
    Enqueue(&amp;q, 1);
    Enqueue(&amp;q, 2);
    Enqueue(&amp;q, 3);
    Enqueue(&amp;q, 4);
    Enqueue(&amp;q, 5);

    // 데이터 꺼내기
    while (!QIsEmpty(&amp;q))
        printf(&quot;%d &quot;, Dequeue(&amp;q));

    return 0;
}

&gt; gcc .\CircularQueue.c .\CircularQueueMain.c
&gt; .\a.exe
&gt; 출력
1 2 3 4 5 </code></pre>
<hr>
<h2 id="07-3-큐의-연결-리스트-기반-구현">07-3. 큐의 연결 리스트 기반 구현</h2>
<p>연결 리스트로 큐를 구현할 경우 배열 기반으로 원형 큐를 구현했던 것에 비해 신경쓸 부분이 좀 줄어든다.</p>
<h3 id="1-헤더파일-정의listbasequeueh"><em>1) 헤더파일 정의(ListBaseQueue.h)</em></h3>
<p>스택과 큐의 유일한 차이점은 데이터를 앞에서 꺼내느냐 뒤에서 꺼내느냐의 차이기 때문에 구현해 놓은 스택을 대상으로 꺼내는 방법만 조금만 변경하면 큐가 될 것 같다!라는 생각이
연결 리스트 기반으로 큐를 구현할 때는 어느정도 맞다!
단, 구현할 때 스택은 push와 pop이 이뤄지는 위치가 동일했지만, 큐는 enqueue와 dequeue가 이뤄지는 위치가 다르다는 점에 유의하면 된다.</p>
<p>따라서 헤더파일을 작성하면 다음과 같다.</p>
<pre><code class="language-c">#ifndef __LB_QUEUE_H__
#define __LB_QUEUE_H__

#define TRUE    1
#define FALSE   0

typedef int Data;

typedef struct _node
{
    Data data;
    struct  _node * next;
} Node;

typedef struct _lQueue
{
    Node * front;
    Node * rear;
} LQueue;

typedef LQueue Queue;

void QueueInit(Queue * pq);
int QIsEmpty(Queue * pq);

void Enqueue(Queue * pq, Data data);
Data Dequeue(Queue * pq);
Data QPeek(Queue * pq);

#endif</code></pre>
<h3 id="2-소스파일-정의listbasequeuec"><em>2) 소스파일 정의(ListBaseQueue.c)</em></h3>
<p>우선 연결 리스트 기반의 큐에 대해 이해해보자.
큐가 생성되고 처음 초기화된 모습은 아래와 같이 F(front)와 R(rear)이 가리킬 대상이 없어 NULL을 가리킨다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/6982a8ae-74b3-44e4-81b5-b2f4f3f36eae/image.png" alt=""></p>
<p>따라서 함수 QueueInit은 다음과 같이 정의된다.</p>
<pre><code class="language-c">void QueueInit(Queue * pq)
{
    pq-&gt;front = NULL;
    pq-&gt;rear = NULL;
}</code></pre>
<p>그리고 큐가 비었는지 확인하는 함수인 QIsEmpty는 dequeue 연산이 F를 참조하여 이뤄지기 때문에 F가 NULL인지 확인하면 된다.
따라서 QIsEmpty 함수는 다음과 같다.</p>
<pre><code class="language-c">int QIsEmpty(Queue * pq)
{
    if(pq-&gt;front == NULL)
        return TRUE;
    else
        return FALSE;
}</code></pre>
<p>Enqueue 함수는 노드를 추가하는 과정으로 노드의 추가 유형이 두개로 나뉜다.
첫 번째는, 첫 노드가 추가 될 때로 F와 R 모두 첫 노드를 가리키도록 설정해야한다.
두 번째 이후 노드는 R만 새 노드를 가리키게 하고 노드간의 연결을 위해 가장 끝에 있는 노드가 새 노드를 가리키게 해야한다. </p>
<table>
<thead>
<tr>
<th>첫 번째 노드 추가</th>
<th>두 번째 이후 노드 추가</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/800d1223-cdd5-42d0-a663-2c20339a80a0/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/76acaf64-1254-461a-b516-e87a931fe5bd/image.png" alt=""></td>
</tr>
<tr>
<td>F와 R 모두 new node 가리킴</td>
<td>R만 새 노드 가리키게</td>
</tr>
</tbody></table>
<p>따라서 함수 Enqueue를 구현하면 다음과 같다.</p>
<pre><code class="language-c">void Enqueue(Queue * pq, Data data)
{
    Node * newNode = (Node *)malloc(sizeof(Node));
    newNode-&gt;next = NULL;
    newNode-&gt;data = data;

    if(QIsEmpty(pq))    // 첫 번째 노드 추가
    {
        pq-&gt;front = newNode;
        pq-&gt;rear = newNode;
    }
    else                // 두 번째 이후 노드 추가
    {
        pq-&gt;rear-&gt;next = newNode;    // 마지막 노드가 새 노드 가리킬 수 있도록
        pq-&gt;rear = newNode;            // rear도 새 노드 가리킬 수 있도록
    }
}</code></pre>
<p>Dequeue 함수는 Enqueue 함수에 비해 고려할 것이 작다. F만 고려하면 되기 때문이다!</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/a15bc3e1-8da8-45d0-b1ab-5bc119ac5d8e/image.png" alt=""></p>
<p>위 그림에서 볼 수 있듯이 1) F가 다음 노드를 가리키게 하고 2) F가 이전에 가리키던 노드를 삭제하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/f63dc762-7911-40ea-8b07-4be6267beab6/image.png" alt=""></p>
<p>노드 하나만 남았을 경우에도 동일하게 1) F가 NULL을 가리키고 2) F가 이전에 가리키던 노드를 삭제하면 된다.
그럼 R은 노드가 삭제되었기 때문에 무엇을 가리키게 될까? 이는 상관이 없다.
큐가 비었는지 확인할 때 큐의 F가 NULL을 가리키는지 고려해서 TRUE, FALSE를 결정했기 때문이다.
따라서 함수 Dequeue를 정의하면 다음과 같다.</p>
<pre><code class="language-c">Data Dequeue(Queue * pq)
{
    Node * delNode;
    Data retData;

    if(QIsEmpty(pq))
    {
        printf(&quot;Queue Memory Error!&quot;)
        exit(-1);
    }

    delNode = pq-&gt;front;            // 삭제할 노드의 주소 값 저장
    retdata = delNode-&gt;data;        // 삭제할 노드가 지닌 값 저장
    pq-&gt;front = pq-&gt;front-&gt;next;    // 삭제할 노드의 다음 노드를 front가 가리킴

    free(delNode);
    return retData;
}</code></pre>
<p>QPeek 함수는 Dequeue 함수에서 삭제하는 부분을 빼고 정의하면 된다.</p>
<p>위에서 설명한 모든 함수를 정리한 소스파일은 다음과 같다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;ListBaseQueue.h&quot;

void QueueInit(Queue * pq)
{
    pq-&gt;front = NULL;
    pq-&gt;rear = NULL;
}

int QIsEmpty(Queue * pq)
{
    if(pq-&gt;front == NULL)
        return TRUE;
    else
        return FALSE;  
}

void Enqueue(Queue * pq, Data data)
{
    Node * newNode = (Node *)malloc(sizeof(Node));
    newNode-&gt;next = NULL;
    newNode-&gt;data = data;

    if(QIsEmpty(pq))
    {
        pq-&gt;front = newNode;
        pq-&gt;rear = newNode;
    }
    else
    {
        pq-&gt;rear-&gt;next = newNode;
        pq-&gt;rear = newNode;
    }
}

Data Dequeue(Queue * pq)
{
    Node * delNode;
    Data retData;

    if(QIsEmpty(pq))
    {
        printf(&quot;Queue Memory Error!&quot;);
        exit(-1);
    }

    delNode = pq-&gt;front;
    retData = delNode-&gt;data;
    pq-&gt;front = pq-&gt;front-&gt;next;

    free(delNode);
    return retData;
}

Data QPeek(Queue * pq)
{
    if(QIsEmpty(pq))
    {
        printf(&quot;Queue Memory Error!&quot;);
        exit(-1);
    }

    return pq-&gt;front-&gt;data;
}</code></pre>
<h3 id="3-실행파일-정의listbasequeuemainc"><em>3) 실행파일 정의(ListBaseQueueMain.c)</em></h3>
<p>구현한 큐를 테스트하기 위한 실행파일내 main 함수를 작성해보자.
원형 큐를 테스트할 때 정의한 main 함수와 동일하고
#include 문을 통해 포함하는 헤더파일 이름만 변경되었다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &quot;ListBaseQueue.h&quot;

int main()
{
    // Queue의 생성 및 초기화
    Queue q;
    QueueInit(&amp;q);

    // 데이터 넣기
    Enqueue(&amp;q, 1);
    Enqueue(&amp;q, 2);
    Enqueue(&amp;q, 3);
    Enqueue(&amp;q, 4);
    Enqueue(&amp;q, 5);

    // 데이터 꺼내기
    while (!QIsEmpty(&amp;q))
        printf(&quot;%d &quot;, Dequeue(&amp;q));

    return 0;
}

&gt; gcc .\ListBaseQueue.c .\ListBaseQueueMain.c
&gt; .\a.exe
&gt; 출력
1 2 3 4 5</code></pre>
<hr>
<h2 id="07-4-큐의-활용">07-4. 큐의 활용</h2>
<p>큐는 운영체제 및 네트워크와 관련된 소프트웨어의 구현에 있어서 중요한 역할을 담당하는 자료구조다.
&#39;큐잉 이론(queuing theory)&#39;이라는 학문에서 수학적으로 모델링 된 결과의 확인을 위해서 특정 현상을 &#39;시뮬레이션(simulation)&#39;하게 되는데 이때에도 큐는 중요한 역할을 담당한다.
따라서 시뮬레이션이라는 주제를 통해 큐가 활용되는 형태에 대해 배워보자.</p>
<h3 id="시뮬레이션simulation"><em>시뮬레이션(Simulation)</em></h3>
<p>시뮬레이션이란, 특정 상황에 놓인 복잡한 문제의 해결을 위해서 실제와 비슷한 상황을 연출하는 것을 말한다.</p>
<p>예를 들어 햄버거 가게가 있는데 점심시간 1시간 동안에 고객이 15초당 1명씩 주문을 한다고 가정해보자.
햄버거 종류는 3가지로 치즈버거, 불고기버거, 더블버거가 있다.
각 햄버거별 만드는데 걸리는 시간은 치즈버거 12초, 불고기버거 15초, 더블버거 24초다.
이때 주문한 음식이 포장되어 나오기를 기다리는 고객들을 위한 대기실을 만들 때 필요한 수용 인원 사이즈를 결정하기 위해 시뮬레이션을 해보는 것이다.
그래서 대략 다음과 같은 결과를 얻을 수 있다.</p>
<ul>
<li>수용인원이 30명인 공간 : 안정적으로 고객 수용할 확률 50%</li>
<li>수용인원이 50명인 공간 : 안정적으로 고객 수용할 확률 70%</li>
<li>수용인원이 100명인 공간 : 안정적으로 고객 수용할 확률 90%</li>
<li>수용인원이 200명인 공간 : 안정적으로 고객 수용할 확률 100%</li>
</ul>
<p>여기서 확률은 10번의 시뮬레이션을 진행하면 5번의 시뮬레이션에서만 고객을 전부 수용할 수 있었다는 의미다.</p>
<h3 id="시뮬레이션-예제의-작성"><em>시뮬레이션 예제의 작성</em></h3>
<p>위와 같이 다양한 상황에서 시뮬레이션을 이용할 수 있는데
이러한 시뮬레이션에 큐가 도구가 될 수 있다는 점을 이해하기 위해서 위 시뮬레이션에 몇 가지 조건으로 상황을 정리해보자.</p>
<ol>
<li>점심시간 1시간, 고객은 15초에 1명씩 주문</li>
<li>한 명의 고객은 하나의 버거만 주문</li>
<li>주문하는 메뉴의 가중치는 X, 모든 고객은 무작위로 메뉴 선정</li>
<li>햄버거 만드는 사람은 1명, 한번에 하나의 버거만을 만듦</li>
<li>주문한 메뉴를 받을 다음 고객은 대기실에서 나와서 대기</li>
</ol>
<p>3번 조건을 위해 <code>int rand(void)</code>라는 함수를 사용해 무작위로 메뉴를 선정할 수 있게 할 것이다.
원형 큐 구현한 헤더파일과 소스파일을 사용해서 큐의 이용을 볼 것이다.
이 시뮬레이션을 코드로 구현하면 다음과 같다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;time.h&gt;
#include &quot;CircularQueue.h&quot;

#define CUS_COME_TERM   15  // 고객의 주문 간격(초)

#define CHE_BUR     0   // 치즈버거 상수
#define BUL_BUR     1   // 불고기버거 상수
#define DUB_BUR     2   // 더블버거 상수

#define CHE_TERM    12  // 치즈버거 제작 시간(초)
#define BUL_TERM    15  // 불고기버거 제작 시간(초)
#define DUB_TERM    24  // 더블버거 제작 시간(초)

int main()
{
    int makeProc = 0;   // 햄버거 제작 진행상황
    int cheOrder = 0, bulOrder = 0, dubOrder = 0; // 각 버거 주문량
    int sec;

    // 큐 생성 및 초기화
    Queue que;
    QueueInit(&amp;que);
    srand(time(NULL));

    // for문의 1회 = 1초의 시간
    for(sec=0; sec&lt;3600; sec++)
    {
        if(sec % CUS_COME_TERM == 0)
        {
            switch(rand() % 3)
            {
            case CHE_BUR:
                Enqueue(&amp;que, CHE_TERM);
                cheOrder++;
                break;

            case BUL_BUR:
                Enqueue(&amp;que, BUL_TERM);
                bulOrder++;
                break;

            case DUB_BUR:
                Enqueue(&amp;que, DUB_TERM);
                dubOrder++;
                break;
            }
        }

        if(makeProc&lt;=0 &amp;&amp; !QIsEmpty(&amp;que))
            makeProc = Dequeue(&amp;que);

        makeProc--;
    }

    printf(&quot;Simulation Report! \n&quot;);
    printf(&quot; - Cheese burger: %d \n&quot;, cheOrder);
    printf(&quot; - Bulgogi burger: %d \n&quot;, bulOrder);
    printf(&quot; - Double burger: %d \n&quot;, dubOrder);
    printf(&quot;# Waiting room size: %d \n&quot;, QUE_LEN);
    return 0;
}

&gt; gcc .\CircularQueue.c .\HamburgerSim.c
&gt; .\a.exe
&gt; 출력 1 (수용인원 100명)
Simulation Report!
 - Cheese burger: 75
 - Bulgogi burger: 93
 - Double burger: 72
??Waiting room size: 100

&gt; 출력 2 (수용인원 200명)
Simulation Report! 
 - Cheese burger: 89 
 - Bulgogi burger: 64
 - Double burger: 87
# Waiting room size: 200

&gt; 출력 3 (수용인원 30명)
Queue Memory Error!</code></pre>
<p>주석을 보고도 충분히 이해할 수 있겠지만 한번 더 간략하게 정리하면 다음과 같다.</p>
<ul>
<li>헤더파일 <code>&quot;CircularQueue.h&quot;</code>를 포함한 것을 통해 원형 큐를 이용해 시뮬레이션을 진행한 것을 알 수 있다.</li>
<li>헤더파일 내 <code>#define QUE_LEN 50</code>는 수용인원을 의미한다.</li>
<li>for문의 조건을 보면 1시간(3600초)를 기준으로 1초에 1회 반복되는 것을 알 수 있고 <code>if(sec % CUS_COME_TERM == 0)</code> 문을 통해 15초에 한번씩 주문이 들어오는 것을 알 수 있다.</li>
<li><code>if(makeProc&lt;=0 &amp;&amp; !QIsEmpty(&amp;que))</code>을 통해 주문을 받은 고객은 대기실 밖으로 나갈 수 있게 해준다. </li>
</ul>
<hr>
<h2 id="07-5-덱deque의-이해와-구현">07-5. 덱(Deque)의 이해와 구현</h2>
<h3 id="덱의-이해와-adt-정의"><em>덱의 이해와 ADT 정의</em></h3>
<p>덱(Deque)이란 앞으로도 뒤로도 데이터를 넣을 수 있고, 앞으로도 뒤로도 데이터를 뺄 수 있는 자료구조다.
deque가 double-ended queue를 줄여서 표현한 것으로 양방향으로 넣고 뺄 수 있다는 것을 의미한다.
스택과 큐를 조합한 형태로도 이해할 수 있다.
따라서 덱의 ADT를 구성하는 핵심 함수 네 가지의 기능은 다음과 같다.</p>
<ol>
<li>앞으로 넣기(AddFirst)</li>
<li>뒤로 넣기(AddLast)</li>
<li>앞에서 빼기(RemoveFirst)</li>
<li>뒤에서 빼기(RemoveLast)</li>
</ol>
<p>이를 정리하면 다음과 같다.</p>
<p>✅Operations:</p>
<ul>
<li><p>void DequeInit(Deque * pdeq);</p>
<ul>
<li>덱의 초기화를 진행</li>
<li>덱 생성 후 제일 먼저 호출되어야 하는 함수</li>
</ul>
</li>
<li><p>int DQIsEmpty(Deque * pdeq);</p>
<ul>
<li>덱이 빈 경우 TRUE(1)를, 그렇지 않은 경우 FALSE(0)을 반환</li>
</ul>
</li>
<li><p>void DQAddFirst(Deque * pdeq, Data data);</p>
<ul>
<li>덱의 머리에 데이터를 저장. data로 전달된 값을 저장.</li>
</ul>
</li>
<li><p>void DQAddLast(Deque * pdeq, Data data);</p>
<ul>
<li>덱의 꼬리에 데이터를 저장. data로 전달된 값을 저장.</li>
</ul>
</li>
<li><p>Data DQRemoveFirst(Deque * pdeq);</p>
<ul>
<li>덱의 머리에 위치한 데이터를 반환 및 소멸.</li>
</ul>
</li>
<li><p>Data DQRemoveLast(Deque * pdeq);</p>
<ul>
<li>덱의 꼬리에 위치한 데이터를 반환 및 소멸.</li>
</ul>
</li>
<li><p>Data DQGetFirst(Deque * pdeq);</p>
<ul>
<li>덱의 머리에 위치한 데이터를 소멸하지 않고 반환.</li>
</ul>
</li>
<li><p>Data DQGetLast(Deque * pdeq);</p>
<ul>
<li>덱의 꼬리에 위치한 데이터를 소멸하지 않고 반환.</li>
</ul>
</li>
</ul>
<h3 id="덱의-구현"><em>덱의 구현</em></h3>
<p>덱도 스택이나 큐와 같이 배열 기반으로, 연결 리스트 기반으로 구현할 수 있다.
덱은 양방향 연결 리스트와 비슷하므로 양방향 연결 리스트 기반으로 구현할 것이다.
예를 들어 덱의 꼬리에 위치한 데이터를 삭제하는 함수 DQRemoveLast의 경우 양방향 리스트로 구현하지 않은 경우 굉장히 까다롭다.</p>
<p>따라서 양방향 연결 리스트 기반의 덱 구현을 위해 첫 번째로 헤더파일을 정의해보자.</p>
<pre><code class="language-c">// Deque.h
#ifndef __DEQUE_H__
#define __DEQUE_H__

#define TRUE 1
#define FALSE 0

typedef int Data;

typedef struct _node
{
    Data data;
    struct _node * next;
    struct _node * prev;
} Node;

typedef struct _dlDeque
{
    Node * head;
    Node * tail;
} DLDeque;

typedef DLDeque Deque;

void DequeInit(Deque * pdeq);
int DQIsEmpty(Deque * pdeq);

void DQAddFirst(Deque * pdeq, Data data);
void DQAddLast(Deque * pdeq, Data data);

Data DQRemoveFirst(Deque * pdeq);
Data DQRemoveLast(Deque * pdeq);

Data DQGetFirst(Deque * pdeq);
Data DQGetLast(Deque * pdeq);

#endif</code></pre>
<p>양방향 연결 리스트 기반으로 덱을 구현하긴 하지만 완전히 동일한 구조를 갖지 않는다.
이전에 양방향 연결 리스트를 구현할 때 tail이 없는 구조로 구현했었다. (<a href="https://velog.io/@mingming_eee/datastructure-05#05-2-%EC%96%91%EB%B0%A9%ED%96%A5-%EC%97%B0%EA%B2%B0-%EB%A6%AC%EC%8A%A4%ED%8A%B8">양방향 연결 리스트 구현 📚</a>)</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/861e1cbc-85a5-458b-9fbc-9add2cbd704b/image.png" alt=""></p>
<p>이번에는 tail을 넣어 덱을 구현할 것이다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/f74eb7f0-46a4-4656-bbe5-5a1d8eca2bd2/image.png" alt=""></p>
<p>이를 바탕으로 소스파일과 실행파일도 정의해보자.</p>
<pre><code class="language-c">// Deque.c
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;Deque.h&quot;

// 덱의 초기화
void DequeInit(Deque * pdeq)
{
    pdeq-&gt;head = NULL;
    pdeq-&gt;tail = NULL;
}

// 덱의 차고 빔 확인
int DQIsEmpty(Deque * pdeq)
{
    if(pdeq-&gt;head == NULL)
        return TRUE;
    else
        return FALSE;
}

// 덱에 데이터 추가
void DQAddFirst(Deque * pdeq, Data data)    // 머리
{
    Node * newNode = (Node *)malloc(sizeof(Node));
    newNode-&gt;data = data;
    newNode-&gt;next = pdeq-&gt;head;

    if(DQIsEmpty(pdeq))
        pdeq-&gt;tail = newNode;
    else
        pdeq-&gt;head-&gt;prev = newNode;

    newNode-&gt;prev = NULL;
    pdeq-&gt;head = newNode;
}
void DQAddLast(Deque * pdeq, Data data)     // 꼬리
{
    Node * newNode = (Node *)malloc(sizeof(Node));
    newNode-&gt;data = data;
    newNode-&gt;prev = pdeq-&gt;tail;

    if(DQIsEmpty(pdeq))
        pdeq-&gt;head = newNode;
    else
        pdeq-&gt;tail-&gt;next = newNode;

    newNode-&gt;next = NULL;
    pdeq-&gt;tail = newNode;
}

// 덱에 데이터 제거 및 반환
Data DQRemoveFirst(Deque * pdeq)    // 머리
{
    Node * rnode = pdeq-&gt;head;
    Data rdata;
    if(DQIsEmpty(pdeq))
    {
        printf(&quot;Deque Memory Error!\n&quot;);
        exit(-1);
    }
    rdata = pdeq-&gt;head-&gt;data;

    pdeq-&gt;head = pdeq-&gt;head-&gt;next;
    free(rnode);

    if(pdeq-&gt;head == NULL)
        pdeq-&gt;tail = NULL;
    else  
        pdeq-&gt;head-&gt;prev = NULL;

    return rdata;
}
Data DQRemoveLast(Deque * pdeq)     // 꼬리
{
    Node * rnode = pdeq-&gt;tail;
    Data rdata;
    if(DQIsEmpty(pdeq))
    {
        printf(&quot;Deque Memory Error!\n&quot;);
        exit(-1);
    }
    rdata = pdeq-&gt;tail-&gt;data;

    pdeq-&gt;tail = pdeq-&gt;tail-&gt;prev;
    free(rnode);

    if(pdeq-&gt;tail == NULL)
        pdeq-&gt;head = NULL;
    else  
        pdeq-&gt;tail-&gt;next = NULL;

    return rdata;
}

// 덱에 데이터 반환
Data DQGetFirst(Deque * pdeq)   // 머리
{
    if(DQIsEmpty(pdeq))
    {
        printf(&quot;Deque Memory Error!\n&quot;);
        exit(-1);
    }
    return pdeq-&gt;head-&gt;data;
}
Data DQGetLast(Deque * pdeq)    // 꼬리
{
    if(DQIsEmpty(pdeq))
    {
        printf(&quot;Deque Memory Error!\n&quot;);
        exit(-1);
    }
    return pdeq-&gt;tail-&gt;data;
}</code></pre>
<pre><code class="language-c">//DequeMain.c
#include &lt;stdio.h&gt;
#include &quot;Deque.h&quot;

int main()
{
    // Deque 생성 및 초기화
    Deque deq;
    DequeInit(&amp;deq);

    // 데이터 넣기 1차
    DQAddFirst(&amp;deq, 3);
    DQAddFirst(&amp;deq, 2);
    DQAddFirst(&amp;deq, 1);

    DQAddLast(&amp;deq, 4);
    DQAddLast(&amp;deq, 5);
    DQAddLast(&amp;deq, 6);

    printf(&quot;&lt;First Insert &amp; delete in deque&gt;\n&quot;);
    // 데이터 꺼내기 1차
    while(!DQIsEmpty(&amp;deq))
        printf(&quot;%d &quot;, DQRemoveFirst(&amp;deq));

    printf(&quot;\n&quot;);

    // 데이터 넣기 2차
    DQAddFirst(&amp;deq, 3);
    DQAddFirst(&amp;deq, 2);
    DQAddFirst(&amp;deq, 1);

    DQAddLast(&amp;deq, 4);
    DQAddLast(&amp;deq, 5);
    DQAddLast(&amp;deq, 6);

    // 데이터 꺼내기 2차
    while(!DQIsEmpty(&amp;deq))
        printf(&quot;%d &quot;, DQRemoveLast(&amp;deq));

    return 0;
}

&gt; gcc .\Deque.c .\DequeMain.c        
&gt; .\a.exe
&gt; 출력
&lt;First Insert &amp; delete in deque&gt;
1 2 3 4 5 6 
6 5 4 3 2 1</code></pre>
<h2 id="추가-덱을-기반으로-큐-구현하기">[추가] 덱을 기반으로 큐 구현하기</h2>
<h3 id="1-adt">1. ADT</h3>
<p>✅Operations:</p>
<ul>
<li><p>void QueueInit(Queue * pq);</p>
<ul>
<li>큐의 초기화</li>
</ul>
</li>
<li><p>int QIsEmpty(Queue * pq);</p>
<ul>
<li>큐가 비었는지 확인</li>
</ul>
</li>
<li><p>void Enqueue(Queue * pq, Data data);</p>
<ul>
<li>enqueue 연산</li>
</ul>
</li>
<li><p>Data Dequeue(Queue * pq);</p>
<ul>
<li>dequeue 연산</li>
</ul>
</li>
<li><p>Data QPeek(Queue * pq);</p>
<ul>
<li>peek 연산</li>
</ul>
</li>
</ul>
<h3 id="2-헤더파일-dequebasequeueh">2. 헤더파일 (DequeBaseQueue.h)</h3>
<pre><code class="language-c">#ifndef __DEQUE_BASE_QUEUE_H__
#define __DEQUE_BASE_QUEUE_H__

#include &quot;Deque.h&quot;

typedef Deque Queue;

void QueueInit(Queue * pq);
int QIsEmpty(Queue * pq);

void Enqueue(Queue * pq, Data data);
Data Dequeue(Queue * pq);
Data QPeek(Queue * pq);

#endif</code></pre>
<h3 id="3-소스파일-dequebasequeuec">3. 소스파일 (DequeBaseQueue.c)</h3>
<pre><code class="language-c">#include &quot;DequeBaseQueue.h&quot;

void QueueInit(Queue * pq)
{
    DequeInit(pq);
}

int QIsEmpty(Queue * pq)
{
    return DQIsEmpty(pq);
}

void Enqueue(Queue * pq, Data data)
{
    DQAddLast(pq, data);
}

Data Dequeue(Queue * pq)
{
    return DQRemoveFirst(pq);
}

Data QPeek(Queue * pq)
{
    return DQGetFirst(pq);
}</code></pre>
<p>이미 덱의 소스파일에서 구현을 다 해놨기 때문에 간편하게 함수만 가져오는 것으로 구현할 수 있다. 단 우리는 여기서 꼬리를 가리키는 포인터 변수가 움직여야 하는지, 머리를 가리키는 포인터 변수가 움직여야 하는지 확인해서 사용하면 된다.</p>
<h3 id="4-실행파일-dequebasequeuemainc">4. 실행파일 (DequeBaseQueueMain.c)</h3>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &quot;DequeBaseQueue.h&quot;

int main()
{
    Queue q;
    QueueInit(&amp;q);

    Enqueue(&amp;q, 1);
    Enqueue(&amp;q, 2);
    Enqueue(&amp;q, 3);
    Enqueue(&amp;q, 4);
    Enqueue(&amp;q, 5);

    while(!QIsEmpty(&amp;q))
        printf(&quot;%d &quot;, Dequeue(&amp;q));

    return 0;
}

&gt; gcc .\Deque.c .\DequeBaseQueue.c .\DequeBaseQueueMain.c
&gt; .\a.exe
&gt; 출력
1 2 3 4 5 </code></pre>
<hr>
<h2 id="review"><code>&lt;Review&gt;</code></h2>
<p>드디어 큐까지 끝냈다.
다음은 어려운 트리가 진행될 예정...!
큐랑 덱은 앞서 배열, 리스트, 스택을 이용해서 구현하다 보니 이해하는 속도가 빨라졌다는 것을 느끼며 배울 수 있었다...!
오늘도 고생했다~</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/2f12f392-9834-498b-b043-0be8dd30ff92/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chapter 06. 스택(Stack)]]></title>
            <link>https://velog.io/@mingming_eee/datastructure-06</link>
            <guid>https://velog.io/@mingming_eee/datastructure-06</guid>
            <pubDate>Tue, 29 Oct 2024 06:16:47 GMT</pubDate>
            <description><![CDATA[<p>리스트는 대표적인 선형 자료구조이다.
연결 리스트의 지옥에서 벗어나 이번에 배울 스택도 선형 자료구조의 일종이다.
챕터가 하나로만 이루어져 있으니 조금은 더 쉬울 것이라는 긱대를 하면서어...!</p>
<p>그럼 레츠고우~!</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/16d46596-4f07-4130-8996-aed0e3f36541/image.png" alt=""></p>
<h2 id="06-1-스택의-이해와-adt-정의">06-1. 스택의 이해와 ADT 정의</h2>
<h3 id="스택stack의-이해"><em>스택(Stack)의 이해</em></h3>
<p><code>스택(Stack)</code>은 후입선출(後入先出)방식의 자료구조로, &#39;LIFO(First-In, First-Out)&#39;구조의 자료구조라고도 불린다.
한쪽은 막히고 한쪽은 뚤려 있어 먼저 들어간 것이 나중에 나온다고 생각하면 된다.</p>
<p>그럼 리스트보다 간단한가?
맞다고만 할 순 없다...
스택의 활용 혹은 스택 기반의 알고리즘들이 의외로 간단하지 않아 스택을 공부하는 것 보단 스택을 경험하는데 더 노력이 필요하다.</p>
<h3 id="스택-adt-정의"><em>스택 ADT 정의</em></h3>
<p>스택을 가지고 사용할 수 있는 기능에는 어떤 것들이 있을까?</p>
<ol>
<li>데이터 저장 (push)</li>
<li>데이터 내보내기 (pop)</li>
<li>데이터 들여다보기 (peek)</li>
</ol>
<p>이렇게 크게 세 가지가 있고 하나 더 추가한다면 <code>데이터 유무 확인</code>일 것이다. 위 세 가지 연산은 스택의 기본 연산이다.
이러한 기능들을 정의하면 다음과 같다.</p>
<p>✅Operations:</p>
<ul>
<li><p>void StackInit(Stack * pstack);</p>
<ul>
<li>스택의 초기화를 진행</li>
<li>스택 생성 후 제일 먼저 호출되는 함수</li>
</ul>
</li>
<li><p>int SIsEmpty(Stack * pstack);</p>
<ul>
<li>스택이 빈 경우 TRUE(1)을, 그렇지 않은 경우 FALSE(0) 반환</li>
</ul>
</li>
<li><p>void SPush(Stack * pstack, Data data);</p>
<ul>
<li>스택에 데이터를 저장. 매개변수 data로 전달된 값을 저장</li>
</ul>
</li>
<li><p>Data SPop(Stack * pstack);</p>
<ul>
<li>마지막에 저장된 요소 삭제</li>
<li>삭제된 데이터는 반환</li>
<li>본 함수의 호출을 위해서는 데이터가 하나 이상 존재 필수. (SIsEmpty 함수 활용)</li>
</ul>
</li>
<li><p>Data SPeek(Stack * pstack);</p>
<ul>
<li>마지막에 저장된 요소를 반환하되 삭제하지 않음</li>
<li>본 함수의 호출을 위해서는 데이터가 하나 이상 존재 필수. (SIsEmpty 함수 활용)</li>
</ul>
</li>
</ul>
<p>스택의 ADT를 정의했으니 스택 구현을 위한 구조체를 정의하고 헤더파일을 디자인할 차례다.
그 전에 결정해야할 사항이 하나 있다.</p>
<ol>
<li>배열 기반의 스택 구현</li>
<li>연결 리스트 기반의 스택 구현</li>
</ol>
<p>스택은 배열로도, 연결 리스트로도 구현이 가능하다.
차근 차근히, 하나 하나 배워보도록 하자.</p>
<hr>
<h2 id="06-2-스택의-배열-기반-구현">06-2. 스택의 배열 기반 구현</h2>
<h3 id="배열-기반-스택-구현의-논리"><em>배열 기반 스택 구현의 논리</em></h3>
<p>스택은 리스트에 비해 상황이 다양하지 않다.
단순히 데이터를 추가하고 꺼내는 상황을 생각하면 된다.
먼저 데이터를 추가하는 상황을 그림으로 표현한 것이다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/54b6d695-b2d5-4325-853a-a44b9cfe7103/image.png" alt=""></p>
<p>이 그림에서 주목할 것은 두 가지 있다.</p>
<ol>
<li>인덱스 0의 배열 요소가 &#39;스택의 바닥&#39;으로 정의되었다.</li>
<li>마지막에 저장된 데이터의 위치를 기억해야 한다.</li>
</ol>
<p>우선 인덱스 0의 배열 요소를 스택의 바닥으로 둔 이유는 &quot;인덱스 0의 요소를 스택의 바닥으로 정의하면, 배열의 길이에 상관 없이 언제나 인덱스 0의 요소가 스택의 바닥이 되기 때문&quot;이다.</p>
<p>그리고 위 그림에서는 가장 최근에 저장된 (가장 위에 저장된) 데이터를 Top으로 가리키고 있으며 이 Top이 가리키는 위치의 인덱스 값을 변수 topIndex에 저장하고 있다.
(실제로 Top은 변수 topIndex를 의미한다.)
이렇듯 마지막에 저장된 데이터의 위치를 별도로 기억해 둬야 다음과 같이 push연산과 pop 연산을 쉽게 완성할 수 있다.</p>
<ul>
<li>push : Top을 위로 한 칸 올리고, Top이 가리키는 위치에 데이터 저장.</li>
<li>pop : Top이 가리키는 데이터를 반환하고 Top을 아래로 한 칸 내림.</li>
</ul>
<h3 id="배열-기반-스택-구현-1-헤더파일의-정의"><em>배열 기반 스택 구현 1: 헤더파일의 정의</em></h3>
<p>배열 기반 스택은 그 구조가 단순하다.
push와 pop에 대해 이해하였고 ADT도 개략 정의했으니 이를 기반으로 스택의 헤더파일을 정의해보자.</p>
<pre><code class="language-c">#ifndef __AB_STACK_H__
#define __AB_STACK_H__

#define TRUE    1
#define FALSE   0
#define STACK_LEN   100

typedef int Data;

typedef struct _arrayStack
{
    Data stackArr[STACK_LEN];
    int topIndex;
} ArrayStack;

typedef ArrayStack Stack;

void StackInit(Stack * pstack);         // 스택의 초기화
int SIsEmpty(Stack * pstack);           // 스택이 비엇는지 확인

void SPush(Stack * pstack, Data data);  // 스택의 push 연산
Data SPop(Stack * pstack);              // 스택의 pop 연산
Data SPeek(Stack * pstack);             // 스택의 peek 연산

#endif</code></pre>
<h3 id="배열-기반-스택-구현-2-소스파일의-정의"><em>배열 기반 스택 구현 2: 소스파일의 정의</em></h3>
<p>스택을 표현한 다음 구조체의 정의를 보면 StackInit 함수를 무엇으로 채워야할지 알 수 있다.</p>
<pre><code class="language-c">typedef struct _arrayStack
{
    Data stackArr[STACK_LEN];    // typedef int Data;
    int topIndex;
} ArrayStack;</code></pre>
<p>이 중에서 초기화할 멤버는 가장 마지막에 저장된 데이터의 위치를 가리키는 topIndex 하나다. 따라서 StackInit 함수는 다음과 같이 정의된다.</p>
<pre><code class="language-c">void StackInit(Stack * pstack)
{
    pstack-&gt;topIndex = -1;    // topIndex의 -1은 빈 상태를 의미
}</code></pre>
<p>topIndex에 0이 저장되면 이는 인덱스 0의 위치에 마지막 데이터가 저장되었음을 뜻한다.
따라서 topIndex를 0이 아닌 -1로 초기화해야한다.</p>
<p>이어서 SIsEmpty 함수는 스택이 비어있는지 확인할 때 호출하는 함수다.
스택이 비어있는 경우 topIndex의 값이 -1인지로 알 수 있고 다음과 같이 구현 가능하다.</p>
<pre><code class="language-c">int SIsEmpty(Stack * pstack)
{
    if(pstack-&gt;topIndex == -1)    // 스택이 비어있는 상태일 경우
        return TRUE;
    else
        return FALSE;
}</code></pre>
<p>이제 스택의 핵심인 SPush 함수와 SPop함수를 구현해보자.</p>
<pre><code class="language-c">void SPush(Stack * pstack, Data data)    // push 연산 함수
{
    pstack-&gt;topIndex += 1;    // 데이터 추가를 위한 인덱스 값 증가
    pstack-&gt;stackArr[pstack-&gt;topIndex] = data;    // 데이터 저장
}

Data SPop(Stack * pstack)    // pop 연산 함수
{
    int rIdx;

    if(SIsEmpty(pstack))
    {
        printf(&quot;Stack Memory Error!&quot;);
        exit(-1);
    }

    rIdx = pstack-&gt;topIndex;    // 삭제할 데이터가 저장된 인덱스 값 저장
    pstack-&gt;topIndex -= 1;        // pop 연산의 결과로 topIndex 값 하나 감소

    return pstack-&gt;stackArr[rIdx];    // 삭제되는 데이터 반환
}</code></pre>
<p>SPop 함수에서 유의해야할 점은 데이터를 꺼낸다는 것이 삭제와 반환의 의미를 모두 담고 있다는 것이다.
꺼냈으니 삭제된 것이고, 반환도 이뤄진 것이다.
topIndex의 값을 근거로 데이터를 저장하니 이 변수에 저장된 값을 하나 감소시키는 것만으로도 데이터의 소멸은 완성된다.</p>
<p>이제 마지막으로 SPeek 함수를 정의하자.
SPop 함수와 달리 반환한 데이터를 삭제시키진 않는다.</p>
<pre><code class="language-c">Data SPeek(Stack * pstack)    // peek 연산 함수
{
    if(SIsEmpty(pstack))
    {
        printf(&quot;Stack Memory Error!&quot;);
        exit(-1);
    }

    return pstack-&gt;stackArr[pstack-&gt;topIndex];    // 맨 위에 저장된 데이터 반환
}</code></pre>
<p>프로그램을 구현하다보면 스택이 반환할 다음 데이터를 확인할 필요가 간혹있다. 데이터 확인을 하되 소멸시키지 않는 함수가 필요한데 그 때 peek 연산을 담당한 함수를 사용하면 된다.</p>
<p>소스파일을 하나로 정리하면 다음과 같다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;ArrayBaseStack.h&quot;

void StackInit(Stack * pstack)
{
    pstack-&gt;topIndex = -1;
}

int SIsEmpty(Stack * pstack)
{
    if(pstack-&gt;topIndex == -1)
        return TRUE;
    else
        return FALSE;
}

void SPush(Stack * pstack, Data data)
{
    pstack-&gt;topIndex += 1;
    pstack-&gt;stackArr[pstack-&gt;topIndex] = data;
}

Data SPop(Stack * pstack)
{
    int rIdx;

    if(SIsEmpty(pstack))
    {
        printf(&quot;Stack Memory Error!&quot;);
        exit(-1);
    }

    rIdx = pstack-&gt;topIndex;
    pstack-&gt;topIndex -= 1;

    return pstack-&gt;stackArr[rIdx];
}

Data SPeek(Stack * pstack)
{
    if(SIsEmpty(pstack))
    {
        printf(&quot;Stack Memory Error!&quot;);
        exit(-1);
    }

    return pstack-&gt;stackArr[pstack-&gt;topIndex];
}</code></pre>
<h3 id="배열-기반-스택-구현-3-실행파일의-정의"><em>배열 기반 스택 구현 3: 실행파일의 정의</em></h3>
<p>배열 기반의 스택을 잘 구현했는지 확인하기 위한 실행 파일(main 함수)는 다음과 같다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &quot;ArrayBaseStack.h&quot;

int main()
{
    // Stack의 생성 및 초기화
    Stack stack;
    StackInit(&amp;stack);

    // 데이터 넣기
    SPush(&amp;stack,1);
    SPush(&amp;stack,2);
    SPush(&amp;stack,3);
    SPush(&amp;stack,4);
    SPush(&amp;stack,5);

    // 데이터 꺼내기
    while(!SIsEmpty(&amp;stack))
        printf(&quot;%d &quot;, SPop(&amp;stack));

    return 0;
}

&gt; gcc .\ArrayBaseStack.c .\ArrayBaseStackMain.c
&gt; .\a.exe
&gt; 출력
5 4 3 2 1 </code></pre>
<p>실행결과에서 입력된 데이터가 역순으로 출력되는 것을 알 수 있다. 이것이 스택의 가장 중요한 특성이다.</p>
<hr>
<h2 id="06-3-스택의-연결-리스트-기반-구현">06-3. 스택의 연결 리스트 기반 구현</h2>
<p>기능적인 부분만 고려한다면 배열은 대부분 연결 리스트로 교체가 가능하다.
배열도 연결 리스트도 기본적인 선형 자료구조이기 때문이다.</p>
<h3 id="연결-리스트-기반-스택-구현-1-헤더파일의-정의"><em>연결 리스트 기반 스택 구현 1: 헤더파일의 정의</em></h3>
<p>앞서 구현한 스택에서 배열을 연결 리스트로 변경할 경우, 연결 리스트가 갖는 특징이 그대로 스택에 반영된다.
스택을 연결 리스트로 구현할 때 생각하면 될 부분을 저장된 순서의 역순으로 조회(삭제)가 가능한 연결리스트라는 것이다.
앞서 배운 연결 리스트 종류 중에서 새로운 노드를 꼬리가 아닌 머리에 추가한 형태의 연결 리스트가 스택과 유사하다!</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/bda4da03-3870-4146-9018-930b773f1ef3/image.png" alt=""></p>
<p>따라서 스택을 구현하기 위해서 필요한 것은 포인터 변수 head 하나다.
연결 리스트 기반의 스택을 위한 헤더파일은 다음과 같이 정의할 수 있다.</p>
<pre><code class="language-c">// ListBaseStack.h
#ifndef __LB_STACK_H__
#define __LB_STACK_H__

#define TRUE    1
#define FALSE   0

typedef int Data;

typedef struct _node   // 연결 리스트의 노드를 표현한 구조체
{
    Data data;
    struct _node * next;
} Node;

typedef struct _listStack   // 연결 리스트 기반 스택을 표현한 구조체
{
    Node * head;
} ListStack;

typedef ListStack Stack;

void StackInit(ListStack * pstack);     // 스택 초기화
int SIsEmpty(Stack * pstack);            // 스택이 비어 있는지 확인

void SPush(Stack * pstack, Data data);    // 스택 push 연산
Data SPop(Stack * pstack);                // 스택 pop 연산
Data SPeek(Stack * pstack);               // 스택 top (peek) 연산

#endif</code></pre>
<h3 id="연결-리스트-기반-스택-구현-2-소스파일의-정의"><em>연결 리스트 기반 스택 구현 2: 소스파일의 정의</em></h3>
<p>가장 먼저 정의할 함수는 스택을 초기화하는 StackInit 함수와 스택이 비어있는지 확인하는 SIsEmpty 함수다.</p>
<pre><code class="language-c">void StackInit(Stack * pstack)
{
    pstack-&gt;head = NULL;    // 포인터 변수 head를 NULL로 초기화
}

int SIsEmpty(Stack * pstack)
{
    if(pstack-&gt;head == NULL)    // 스택이 비어 있는 경우 head에 NULL이 저장.
        return TRUE;
    else
        return FALSE;
}</code></pre>
<p>그 다음엔 SPush 함수를 정의해보려 한다.
리스트의 머리에 새 노드를 추가해야하는 함수로 다음과 같이 구현할 수 있다.</p>
<pre><code class="language-c">void SPush(Stack * pstack, Data data)
{
    Node * newNode = (Node*)malloc(sizeof(Node));    // 새 노드 생성

    newNode-&gt;data = data;            // 새 노드 데이터 저장
    newNode-&gt;next = pstack-&gt;head;    // 새 노드가 최근에 추가된 노드를 가리킴

    pstack-&gt;head = newNode;            // 포인터 변수 head가 새 노드를 가리킴
}</code></pre>
<p>반대로 SPop 함수는 포인터 변수 head가 가리키는 노드를 소멸시키고, 소멸된 노드의 데이터를 반환해야하므로 다음과 같이 정의해야한다.</p>
<pre><code class="language-c">Data SPop(Stack * pstack)
{
    Data rdata;
    Node * rnode;

    if(SIsEmpty(pstack))
    {
        printf(&quot;Stack Memory Error!&quot;);
        exit(-1);
    }

    rdata = pstack-&gt;head-&gt;data;    // 삭제할 노드의 데이터를 임시로 저장
    rnode = pstack-&gt;head;        // 삭제할 노드의 주소 값을 임시로 저장

    pstack-&gt;head = pstack-&gt;head-&gt;next;    // 삭제할 노드의 다음 노드를 head가 가리킴
    free(rnode);    // 노드 삭제
    return rdata;    // 삭제된 노드의 데이터 반환
}</code></pre>
<p>마지막으로 SPeek 함수의 정의다.</p>
<pre><code class="language-c">Data SPeek(Stack * pstack)
{
    if(SIsEmpty(pstack))
    {
        printf(&quot;Stack Memory Error!&quot;);
        exit(-1);
    }

    return pstack-&gt;head-&gt;data;    // head가 가리키는 노드에 저장된 데이터 반환
}</code></pre>
<p>위에서 정의한 함수들을 정리한 소스파일은 다음과 같다.</p>
<pre><code class="language-c">// ListBaseStack.c
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;ListBaseStack.h&quot;

void StackInit(Stack * pstack)
{
    pstack-&gt;head = NULL;
}

int SIsEmpty(Stack * pstack)
{
    if(pstack-&gt;head == NULL)
        return TRUE;
    else
        return FALSE;
}

void SPush(Stack * pstack, Data data)
{
    Node * newNode = (Node*)malloc(sizeof(Node));

    newNode-&gt;data = data;
    newNode-&gt;next = pstack-&gt;head;

    pstack-&gt;head = newNode;
}

Data SPop(Stack * pstack)
{
    Data rdata;
    Node * rnode;

    if(SIsEmpty(pstack))
    {
        printf(&quot;Stack Memory Error!&quot;);
        exit(-1);
    }

    rdata = pstack-&gt;head-&gt;data;
    rnode = pstack-&gt;head;

    pstack-&gt;head = pstack-&gt;head-&gt;next;
    free(rnode);
    return rdata;
}

Data SPeek(Stack * pstack)
{
    if(SIsEmpty(pstack))
    {
        printf(&quot;Stack Memory Error!&quot;);
        exit(-1);
    }

    return pstack-&gt;head-&gt;data;
}</code></pre>
<h3 id="연결-리스트-기반-스택-구현-3-실행파일의-정의"><em>연결 리스트 기반 스택 구현 3: 실행파일의 정의</em></h3>
<p>지금까지 구현한 연결 리스트 기반 스택이 잘 작동하는지 확인하기 위한 main 함수는 다음과 같다.</p>
<pre><code class="language-c">// ListBaseStackMain.c
#include &lt;stdio.h&gt;
#include &quot;ListBaseStack.h&quot;

int main()
{
    // Stack의 생성 및 초기화
    Stack stack;
    StackInit(&amp;stack);

    // 데이터 넣기
    SPush(&amp;stack, 1);
    SPush(&amp;stack, 2);
    SPush(&amp;stack, 3);
    SPush(&amp;stack, 4);
    SPush(&amp;stack, 5);

    // 데이터 꺼내기
    while(!SIsEmpty(&amp;stack))
        printf(&quot;%d &quot;, SPop(&amp;stack));

    return 0;
}

&gt;gcc .\ListBaseStack.c .\ListBaseStackMain.c
&gt; .\a.exe
&gt; 출력
5 4 3 2 1 </code></pre>
<hr>
<h2 id="06-4-계산기-프로그램-구현">06-4. 계산기 프로그램 구현</h2>
<h3 id="구현할-계산기-프로그램-특징"><em>구현할 계산기 프로그램 특징</em></h3>
<p>구현할 계산기의 특징은 다음과 같다.</p>
<ol>
<li>소괄호를 파악하여 그 부분을 먼저 연산한다.</li>
<li>연산자의 우선수위를 근거로 연산의 순위를 결정한다.</li>
</ol>
<p>스택만 알아서는 계산기를 구현하기 어렵다.
계산기 구현에 필요한 별도의 알고리즘에 대해 알아보자.</p>
<h3 id="수식의-표기법---중위-전위-후위-표기법"><em>수식의 표기법 - 중위, 전위, 후위 표기법</em></h3>
<p>계산기 구현에 필요한 부가 알고리즘에 대해 알아보기 전에 계산기의 기능을 어느정도 제한하고자한다.
수식을 이루는 피연산자는 한자리 숫자로만 이뤄진다는 점이다.
따라서 1의 자리 숫자들로만 계산을 진행할 예정이다.</p>
<p>계산기 구현에 필요한 알고리즘의 첫 번째 단계로 수식의 표기법에 대해 알아보자.
수식을 표기하는 방법에는 세 가지 방법이 있다.</p>
<ol>
<li>중위 표기법 (Infix Notation)
ex) 5 + 2 / 7</li>
<li>전위 표기법 (Prefix Notation)
ex) + 5 / 2 7</li>
<li>후위 표기법 (Postfix Notation)
ex) 5 2 7 / +</li>
</ol>
<p>우리가 평소에 사용하고 많이 봐온 표기법이 중위 표기법이다.
이 표기법에는 수식의 연산 순서에 대한 정보가 담겨 있지 않다.
우리는 어렸을 때부터 +(덧셈)와 -(뺄셈)보다 X(곱셈)과 /(나눗셈)이 먼저 계산되어야 한다는 것을 배웠기 때문에 알고 있는 것이지 단순히 연산만 처음 본 것이면 알 수 없다.</p>
<p>전위 표기법이나 후위 표기법에는 이러한 연산들의 순서에 대한 정보가 담겨져 있다.
예를 들어 <code>5 + 2 / 7</code>이 수식을 연산자를 OP1, OP2로 보고 후위 표기법으로 표현하고 나면 <code>5 2 7 OP1 OP2</code>와 같은 방식으로 표현할 수 있다.
그러면 단순히 OP1과 OP2가 뭔진 몰라도 OP1 연산자를 먼저 실행하고 OP2 연산자를 실행해야하겠구나를 알 수 있다.
그리고 연산자의 배치 순서를 바꿈으로써 연산의 순서를 바꿀 수 있기 때문에 소괄호가 필요하지도 않다.</p>
<p>즉, 전위 표기법의 수식이나 후위 표기법의 수식은 연자나의 배치 순서에 따라서 연산순서가 결정되기 때문에 이 두 표기법의 수식을 계산하기 위해서 연산자의 우선순위를 알 필요가 없고, 소괄호도 삽입되지 않으니 소괄호에 대한 처리도 불필요하다.</p>
<blockquote>
<p>&lt;중위 표기법&gt; ( 1 + 2 ) * 7
&lt;전위 표기법&gt; * + 1 2 7
&lt;후위 표기법&gt; 1 2 + 7 *</p>
</blockquote>
<p>따라서 우리는 계산기 사용자는 중위 표기법으로 수식을 입력하면
우리는 이것을 전위 표기법 또는 후위 표기법으로 바꿀 수 있도록 할 것이다. (후위 표기법으로 구현할 예정.)</p>
<p>그렇다면 실제로 구현을 위해 중위 표기법을 후위 표기법으로 변환하는 과정에 대해 알아보자.</p>
<h3 id="중위-표기법-→-후위-표기법-방법-1-소괄호-고려-x"><em>중위 표기법 → 후위 표기법 방법 1: 소괄호 고려 X</em></h3>
<p>이번에 구현할 계산기는 다음과 같은 과정을 거쳐서 연산을 하도록 할 것이다.</p>
<ol>
<li>중위 표기법의 수식을 후위 표기법의 수식으로 바꾼다.</li>
<li>후위 표기법으로 바뀐 수식을 계산하여 그 결과를 얻는다.</li>
</ol>
<p>각각의 과정 모두 별도의 알고리즘이 필요하다.
두 과정은 별개의 과정으로 진행되며 우리는 그 중 첫 번째 과정에 대해 이해해보려 한다.
이해를 돕기 위해서 예시로 <code>5 + 2 / 7</code>수식을 구성하는 연산자와 피연산자가 블록들로 있고 쟁반이 하나 있다고 가정한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/ab95caae-352e-4b94-b22d-0c4dcd1ba34c/image.png" alt=""></p>
<p>우리는 5블록부터 7블록까지 후위 표기법이라는 일관된 방식으로 처리할 것이다.
우선 피연산자는 바로 통과가 되어 변환된 수식 자리로 이동한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/2575971f-3080-47fd-972e-1a7dab35a8fb/image.png" alt=""></p>
<p>이 후 연산자 블록을 만나게 되면 쟁반에 담아둔다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/69650d9d-1ccd-490f-88b2-2cbc7e5597a8/image.png" alt=""></p>
<p>이 후 만난 피연산자 블록을 변환된 수식 자리로 이동시킨다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/d624513f-f1f5-4ab4-be66-5e0db220d56a/image.png" alt=""></p>
<p>이번에는 /라는 연산자를 만나게 되었는데 여기서 두 연산자의 우선순위를 비교해야한다.</p>
<ol>
<li><p>쟁반에 위치한 연산자의 우선순위가 높다면
: 쟁반에 위치한 연산자를 꺼내어 변환된 수식 자리로 이동시키고 새 연산자를 쟁반에 놓는다.</p>
</li>
<li><p>쟁반에 위치한 연산자의 우선순위가 낮다면
: 쟁반에 위치한 연산자 위에 새 연산자를 놓는다. (스택)</p>
</li>
</ol>
<p>/(나눗셈)은 +(덧셈)보다 우선순위가 높기 때문에 2번 방법을 통해 다음과 같이 이동한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/fa0eeadb-e883-4a9d-b1d3-3230ff63e500/image.png" alt=""></p>
<p>우선순위가 높은 연산자를 우선순위가 낮은 연산자 위에 올려서 우선순위가 낮은 연산자가 변환된 수식 자리로 이동하지 못하게 한다.</p>
<p>이 후 피연산자인 7을 변환된 수식 자리로 이동시킨다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/17d90270-7451-47c7-9632-b1550d37c7ab/image.png" alt=""></p>
<p>이제 쟁반 위에 있는 연산자 블록들을 처리해야하는데 이때 스택의 원리를 이용해서 위에 있는 블록부터 차례대로 변환된 수식 자리로 이동시킨다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/1c04119e-a9c2-453a-b218-4729deaed46b/image.png" alt=""></p>
<p>따라서 위 과정을 정리하면 다음과 같다.</p>
<blockquote>
<p>&lt;중위 표기법 → 후위 표기법 (소괄호 X)&gt;</p>
</blockquote>
<ol>
<li>피연산자는 그냥 옮긴다.</li>
<li>연산자는 쟁반으로 옮긴다.</li>
<li>연산자가 쟁반에 있다면 우선순위를 비교하여 처리 방법을 결정한다.</li>
<li>마지막으로 쟁반에 남아있는 연산자들을 하나씩 옮긴다.</li>
</ol>
<p>만약 우선순위가 동일한 연산자 둘이 만나면 어떻게 될까?</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/7c602be1-d8a0-40f4-b965-624900fc9b06/image.png" alt=""></p>
<p>위 그림과 같이 +블록과 -블록이 만나면 +블록을 변환된 수식으로 옮긴 뒤 빈 쟁반에 -블록을 올려놓는다.</p>
<p>만약에 우선순위가 높은 연산자를 만나 쟁반에 블록 2개가 있는데 이보다 우선 순위가 낮은 경우엔 어떻게 될까?</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/20088456-ab14-4977-a407-1d7debd8cc93/image.png" alt=""></p>
<p>위 그림과 같이 +블록과 /블록이 쌓여 있는 상태에서 -블록을 만나게 되면 쟁반 위에 있는 블록들을 하나씩 변환된 수식으로 옮긴 후 빈 쟁반에 -블록을 올려놓으면 된다.</p>
<p>따라서 쟁반에는 자신보다 나중에 연산이 이뤄져야 하는 연산자의 위에 올라서 있어야한다.</p>
<h3 id="중위-표기법-→-후위-표기법-방법-2-소괄호-고려-o"><em>중위 표기법 → 후위 표기법 방법 2: 소괄호 고려 O</em></h3>
<p>이번에는 소괄호도 포함되어 있는 중위 표기법을 후위 표기법으로 바꾸는 방법에 대해 생각해보자.
후위 표기법의 수식에서는 먼저 연산이 이뤄져야 하는 연산자가 뒤에 연산이 이뤄지는 연산자보다 앞에 위치해야한다는 점을 생각하며 이해해보자.
예시로 <code>( 1 + 2 * 3 ) / 4</code>라는 수식에 대해 블록과 쟁반으로 그 과정을 이해해보자.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/72ce2379-161e-403b-8e19-f0e8f995616a/image.png" alt=""></p>
<p><code>(</code>연산자의 우선순위는 그 어떤 사칙 연산자들보다 낮다고 간주하자.
따라서 <code>)</code> 연산자가 등장할 때까지 쟁반에 남아 소괄호의 경계 역할을 해야한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/c18645c9-41b5-4481-a62a-1ec0a871d6d4/image.png" alt=""></p>
<p>이후에 <code>)</code>연산자가 나오기 전까지는 소괄호가 없을 때의 경우와 동일하게 진행한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/eda0beb9-9659-4cdf-98fe-f0ddc525fca6/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/31fe6eef-0991-4945-8475-e922a1c8e8be/image.png" alt=""></p>
<p><code>)</code>연산자를 만나고 쟁반 위에 있던 연산자들을 모두 변환된 수식으로 하나씩 옮기고 괄호들은 옮길 필요가 없다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/e7c95efb-9f1c-429b-92b9-6f619321feb7/image.png" alt=""></p>
<p>마지막으로 남은 블록들도 정리하면 아래와 같이 정리된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/fe78195f-aa1b-4f4d-bccb-f6c18fb7fa7e/image.png" alt=""></p>
<p>따라서 괄호가 있을 때 유의할 점은 <strong><code>)</code>연산자를 만나면 <code>(</code>연산자를 만날 때까지 연산자를 변환된 수식 자리로 이동시키고 괄호 블록들은 삭제된다</strong>는 것이다. </p>
<h3 id="중위-표기법을-후위-표기법으로-바꾸는-방법-프로그램의-구현-소스파일"><em>중위 표기법을 후위 표기법으로 바꾸는 방법: 프로그램의 구현 (소스파일)</em></h3>
<p>이제 위에서 설명한 것들을 코드로 구현해보자.
후위 표기법으로 변환하는 함수를 다음과 같은 형태로 정의하자.</p>
<pre><code class="language-c">void ConvToRPNExp(char exp[])    // 후위 표기법의 수식으로 변환
{
    ....
}
// RPN : Reverse Polish Notation</code></pre>
<p>함수의 매개변수 형과 반환형을 위와 같이 정의한 의도는 중위 표기법의 수식을 담고 있는 배열의 주소 값을 인자로 전달하면서 ConvToRPNExp 함수를 호출하면 인자로 전달된 주소 값의 배열에는 후위 표기법으로 바뀐 수식이 저장된다는 것이다.</p>
<p>따라서 실행 파일에서는 다음과 같이 함수가 호출되어야 한다.</p>
<pre><code class="language-c">int main()
{
    char exp[] = &quot;3-2+4&quot;;    // 중위 표기법 수식
    ConvToRPNExp(exp);    // 후위 표기법으로 수식 변환 요청
}</code></pre>
<p>ConvToRONExp 함수를 도와주는 함수도 몇가지 있다.</p>
<p>그 중 첫 번째는 인자로 전달된 연산자의 우선순위 정보를 정수의 형태로 반환하는 함수다.</p>
<pre><code class="language-c">int GetOpPrec(char op)    // 연산자의 연산 우선순위 정보 반환
{
    switch(op)
    {
    case &#39;*&#39;:
    case &#39;/&#39;:
        return 5;    // 가장 높은 연산의 우선순위
    case &#39;+&#39;:
    case &#39;-&#39;:
        return 3;    // 중간정도의 연산 우선순위
    case&#39;(&#39;:
        return 1;    // 가장 낮은 연산의 우선순위
    }

    return -1;    // 등록되지 않은 연산자 알림
}</code></pre>
<p>반환 값이 클수록 우선순위가 높음을 의미한다.
<code>(</code>연산자의 우선순위가 가장 낮은 이유는 <code>)</code>연산자가 등장하기 전까지 쟁반 위에 남아있어야하기 때문에 다른 사칙 연산들보다 연산의 우선순위가 낮아야한다. 
그리고 <code>)</code>연산자는 소괄호의 끝에 관한 메시지를 전달할 뿐이므로 메시지만 취하고 사라지기 때문에 연산의 우선순위가 부여되지 않았다.</p>
<p>연산의 우선순위를 매겼다면 GetOpPrec 함수의 호출결과를 바탕으로 두 연산자의 우선순위를 비교하여 그 결과를 반환하는 함수가 필요하다.
쟁반 위의 연산자를 어떻게 처리할지 결정하는 함수라고 할 수 있다.
그 함수의 정의는 다음과 같다.</p>
<pre><code class="language-c">int WhoPrecOp(char op1, char op2)
{
    int op1Prec = GetOpPrec(op1);
    int op2Prec = GetOpPrec(op2);

    if(op1Prec &gt; op2Prec)        // op1의 연산 우선순위가 더 높을 경우
        return 1;
    else if(op1Prec &lt; op2Prec)    // op2의 연산 우선순위가 더 높을 경우
        return -1;
    else                        // op1과 op2의 연산 우선순위가 같은 경우
        return 0;
}</code></pre>
<p>이제 마지막으로 후위 표기법으로 변환하는 함수인 ConvToRPNExp 함수를 정리해보자.</p>
<pre><code class="language-c">void ConvToRPNExp(char exp[])
{
    Stack stack;
    int expLen = strlen(exp);
    char * convExp = (char*)malloc(expLen+1);    // 변환된 수식을 담는 공간 마련

    int i, idx = 0;
    char tok, popOp;

    memset(convExp, 0, sizeof(char)*expLen+1);    // 할당된 배열을 0으로 초기화
    StackInit(&amp;stack);

    for(i=0; i&lt;expLen; i++)
    {
        tok = exp[i];        // exp로 전달된 수식을 한 문자씩 tok에 저장
        if(isdigit(tok))    // tok에 저장된 문자가 숫자인지 확인
            convExp[idx++] = tok;    // 숫자일 경우 배열 convExp에 바로 저장
        else    // 숫자가 아닐 경우 (연산자일 경우)
        {
            switch(tok)
            {
            case &#39;(&#39;:                // 여는 소괄호일 경우
                SPush(&amp;stack, tok);    // 스택(쟁반)에 놓기
                break;
            case &#39;)&#39;:                // 닫는 소괄호일 경우
                while(1)            // 반복해서 연산자 우선순위 판단
                {
                    popOp = SPop(&amp;stack);    // 스택에서 연산자를 꺼내기
                    if(popOp == &#39;(&#39;)        // 연산자 &#39;(&#39;를 만나면 반복 종료
                        break;
                    convExp[idx++] = popOp;    // 배열 convExp에 연산자 저장
                }
                break;
            case &#39;+&#39;:
            case &#39;-&#39;:
            case &#39;*&#39;:
            case &#39;/&#39;:
                while(!SIsEmpty(&amp;stack) &amp;&amp; WhoPrecOp(SPeek(&amp;stack), tok) &gt;= 0)    // 스택이 비지 않았고 우선순위 확인 함수 확인시 0이상일 경우
                    convExp[idx++] = SPop(&amp;stack);
                SPush(&amp;stack, tok);
                break;
            }
        }
    }

    while(!SIsEmpty(&amp;stack))            // 스택에 남아있는 모든 연산자들
        convExp[idx++] = SPop(&amp;stack);    // 배열 convExp에 저장

    strcpy(exp, convExp);    // 변환된 수식 exp에 복사
    free(convExp);            // convExp 배열 소멸
}</code></pre>
<p>위에서 우선순위 확인 함수에서 반환하는 값이 0이상인 경우는 쟁반 위에 있는 연산자가 뒤이어 오는 연산자보다 우선순위가 높거나 같을 때다.</p>
<p>그리고 추가로 호출된 표준 함수가 두 가지 있었는데 이것의 의미는 다음과 같다.</p>
<ul>
<li><code>void * memset(void * ptr, int val, size_t len);</code> <strong>&lt;헤더파일 ctype.h&gt;</strong>
: ptr로 전달된 주소의 메모리서부터 len 바이트를 val의 값으로 채운다.
그래서 위 코드에서 <code>memset(convExp, 0, sizeof(char)*expLen+1);</code>로 사용되었으며,
변환된 수식들을 저장하는 convExp란 배열을 0으로 초기화하여 하나 생성한 것으로 이해하면 된다.</li>
<li><code>int isdigit(int ch);</code> <strong>&lt;헤더파일 string.h&gt;</strong>
: ch로 전달된 문자의 내용이 10진수라면 1을 반환한다.
그래서 위 코드에서 if문 안에 <code>isdigit(tok)</code>이란 조건으로 사용되었는데,
처음 주어진 배열에서 숫자와 연산자를 구별하는 조건으로 10진수 숫자일 경우 1을 반환하여 바로 convExp라는 변환된 수식 배열에 저장할 수 있게 한다.</li>
</ul>
<h3 id="중위-표기법을-후위-표기법으로-바꾸는-방법-프로그램의-실행-헤더파일-소스파일-실행파일"><em>중위 표기법을 후위 표기법으로 바꾸는 방법: 프로그램의 실행 (헤더파일, 소스파일, 실행파일)</em></h3>
<p>이제 이 ConvToRPNExp 함수의 동작결과를 확인해볼 차례다.
함수의 선언은 <code>InFixToPostfix.h</code> 이란 헤더파일에, 함수의 정의는 <code>InFixToPostfix.c</code>란 소스파일에 저장한다.
그리고 실행파일인 <code>InFixToPostfixMain.c</code>에는 중위 표기법의 수식을 후위 표기법의 수식으로 변환하여 그 결과를 보여주는 main함수를 저장한다.</p>
<pre><code class="language-c">// InfixToPostfix.h
#ifndef __INFIX_TO_POSTFIX_H__
#define __INFIX_TO_POSTFIX_H__

// 중위 표기법 -&gt; 후위 표기법 변환 함수 선언
void ConvToRPNExp(char exp[]);

#endif</code></pre>
<pre><code class="language-c">// InfixToPostfix.c
#include &lt;string.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;ctype.h&gt;
#include &quot;ListBaseStack.h&quot;

// 연산자의 연산 우선순위 정보 반환
int GetOpPrec(char op)
{
    switch(op)
    {
    case &#39;*&#39;:
    case &#39;/&#39;:
        return 5;    // 가장 높은 연산의 우선순위
    case &#39;+&#39;:
    case &#39;-&#39;:
        return 3;    // 중간정도의 연산 우선순위
    case&#39;(&#39;:
        return 1;    // 가장 낮은 연산의 우선순위
    }

    return -1;    // 등록되지 않은 연산자 알림
}

// 연산자의 연산 우선순위 비교 및 처리 함수
int WhoPrecOp(char op1, char op2)
{
    int op1Prec = GetOpPrec(op1);
    int op2Prec = GetOpPrec(op2);

    if(op1Prec &gt; op2Prec)        // op1의 연산 우선순위가 더 높을 경우
        return 1;
    else if(op1Prec &lt; op2Prec)    // op2의 연산 우선순위가 더 높을 경우
        return -1;
    else                        // op1과 op2의 연산 우선순위가 같은 경우
        return 0;
}

// 중위 표기법을 후위 표기법으로 변환
void ConvToRPNExp(char exp[])
{
    Stack stack;
    int expLen = strlen(exp);
    char * convExp = (char*)malloc(expLen+1);    // 변환된 수식을 담는 공간 마련

    int i, idx = 0;
    char tok, popOp;

    memset(convExp, 0, sizeof(char)*expLen+1);    // 할당된 배열을 0으로 초기화
    StackInit(&amp;stack);

    for(i=0; i&lt;expLen; i++)
    {
        tok = exp[i];        // exp로 전달된 수식을 한 문자씩 tok에 저장
        if(isdigit(tok))    // tok에 저장된 문자가 숫자인지 확인
            convExp[idx++] = tok;    // 숫자일 경우 배열 convExp에 바로 저장
        else    // 숫자가 아닐 경우 (연산자일 경우)
        {
            switch(tok)
            {
            case &#39;(&#39;:                // 여는 소괄호일 경우
                SPush(&amp;stack, tok);    // 스택(쟁반)에 놓기
                break;
            case &#39;)&#39;:                // 닫는 소괄호일 경우
                while(1)            // 반복해서 연산자 우선순위 판단
                {
                    popOp = SPop(&amp;stack);    // 스택에서 연산자를 꺼내기
                    if(popOp == &#39;(&#39;)        // 연산자 &#39;(&#39;를 만나면 반복 종료
                        break;
                    convExp[idx++] = popOp;    // 배열 convExp에 연산자 저장
                }
                break;
            case &#39;+&#39;:
            case &#39;-&#39;:
            case &#39;*&#39;:
            case &#39;/&#39;:
                while(!SIsEmpty(&amp;stack) &amp;&amp; WhoPrecOp(SPeek(&amp;stack), tok) &gt;= 0)
                // 스택이 비지 않았고 우선순위 확인 함수 확인시 0이상일 경우
                    convExp[idx++] = SPop(&amp;stack);
                SPush(&amp;stack, tok);
                break;
            }
        }
    }

    while(!SIsEmpty(&amp;stack))            // 스택에 남아있는 모든 연산자들
        convExp[idx++] = SPop(&amp;stack);    // 배열 convExp에 저장

    strcpy(exp, convExp);    // 변환된 수식 exp에 복사
    free(convExp);            // convExp 배열 소멸
}</code></pre>
<pre><code class="language-c">// InfixToPostfixMain.c
#include &lt;stdio.h&gt;
#include &quot;InfixToPostfix.h&quot;

int main()
{
    char exp1[] = &quot;1+2*3&quot;;
    char exp2[] = &quot;(1+2)*3&quot;;
    char exp3[] = &quot;((1-2)+3)*(5-2)&quot;;

    printf(&quot;&lt;Infix Notation(Origin)&gt;\n&quot;);
    printf(&quot;%s&quot;, exp1);
    printf(&quot;%s&quot;, exp2);
    printf(&quot;%s&quot;, exp3);

    ConvToRPNExp(exp1);
    ConvToRPNExp(exp2);
    ConvToRPNExp(exp3);

    printf(&quot;&lt;Postfix Notation(Fix)&gt;\n&quot;);
    printf(&quot;%s \n&quot;, exp1);
    printf(&quot;%s \n&quot;, exp2);
    printf(&quot;%s \n&quot;, exp3);

    return 0;
}

&gt; gcc .\InfixToPostfix.c .\L istBaseStack.c .\InfixToPostfixMain.c
&gt; .\a.exe
&gt; 출력
&lt;Infix Notation(Origin)&gt;
1+2*3 
(1+2)*3
((1-2)+3)*(5-2)
&lt;Postfix Notation(Fix)&gt;
123*+
12+3*
12-3+52-*</code></pre>
<p>참고로 쟁반을 연결리스트 기반의 스택을 사용할 것이기 때문에 이 세 파일과 이전에 작성한 <code>ListBaseStack.h</code>과 <code>ListBaseStack.c</code> 파일을 함께 둬야한다.</p>
<h3 id="후위-표기법으로-표현된-수식의-계산-방법"><em>후위 표기법으로 표현된 수식의 계산 방법</em></h3>
<p>이제 계산기 구현에서 마지막 단계인 변환된 후위 표기법의 수식을 계산하여 그 결과를 얻는 것이다.
이 과정을 구현하려면 먼저 후위 표기법의 수식을 계산하는 방법에 대해 알아야한다.</p>
<p><code>3 + 2 * 4</code>라는 중위 표기법 수식을 후위 표기법 수식으로 변경하면 <code>3 2 4 * +</code>와 같다.
후위 표기법에서는 먼저 연산되어야 하는 연산자가 수식의 앞쪽에 배치된다. 
따라서 곱셈(*)이 우선 진행되고 이후 덧셈(+)이 진행된다.
그렇다면 곱셈의 피연산자는 무엇일까?
후위 표기법의 수식에서는 연산자의 앞에 등장하는 두 개의 숫자가 피연산자다.
따라서 2와 4가 곱셈의 피연산자가 된다.</p>
<p>곱셈 이후의 표기법을 <code>3 8 +</code>이 되고 덧셈의 피연산자는 덧셈 연산자 앞 두개가 된다.
따라서 결과는 11이 된다.</p>
<p>다른 예를 하나 더 생각해보자.
<code>( 1 * 2 + 3 ) / 4</code>라는 중위 표기법 수식이 있고 이를 후위 표기법으로 바꾸면 <code>1 2 * 3 + 4 /</code>가 된다.
곱셈 연산자가 가장 앞에 있고 그 앞에는 피연산자 1과 2가 있기 때문에 첫 연산자의 결과는 <code>2 3 + 4 /</code>가 된다.
그 다음 연산자는 덧셈이고 피연산자 2와 3이 앞에 있으므로 두 번째 연산자의 결과는 <code>5 4 /</code>가 된다.
마지막으로 5를 4로 나누는 연산이 진행되고 마무리 된다.</p>
<h3 id="후위-표기법-수식-계산-구현-1-프로그램의-구현"><em>후위 표기법 수식 계산 구현 1: 프로그램의 구현</em></h3>
<p>프로그램으로 구현하기 위한 기본 원칙이 세 가지 있다.</p>
<ol>
<li>피연산자는 무조건 스택으로 옮긴다.</li>
<li>연산자를 만나면 스택에서 두 개의 피연산자를 꺼내서 계산을 한다.</li>
<li>계산 결과는 다시 스택에 넣는다.</li>
</ol>
<p>이 기본 원칙을 적용해서 <code>3 2 4 * +</code>라는 수식 계산을 예시로 이해해보자.
3, 2, 4 모두 피연산자이므로 하나씩 스택으로 쌓는다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/fbe7cb15-486b-423e-9039-332670b2f5c9/image.png" alt=""></p>
<p>이어서 <code>*</code>연산자를 처리해야한다.
따라서 스택에서 두 개의 피연산자를 꺼내 연산을 진행한다.
여기서 주목할 점은 진행되는 연산이 <code>4 * 2</code>가 아닌 <code>2 * 4</code>로 되어야한다는 점이다.
곱셈이나 덧셈은 피연산자의 위치가 바껴도 상관 없지만 뺄셈과 나눗셈은 값이 달라지기 때문에 연산의 순서는 중요하다.
스택에서 먼저 꺼낸 피연산자가 연산자의 오른쪽(두번째)로 오고 나중에 꺼낸 피연산자가 연산자의 왼쪽(첫번째)로 와야한다.
따라서 그 연산의 결과가 다시 스택으로 들어가게 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/7bdcedaf-adc1-4ff8-b512-29b720eb7e77/image.png" alt=""></p>
<p>이제 마지막 남은 덧셈 연산의 진행을 위해 스택에 남은 두 개의 피연산자 모두 꺼내어 덧셈을 진행하고 그 결과로 11을 얻게 된다.</p>
<p>이 내용을 바탕으로 함수를 정의해보자.</p>
<pre><code class="language-c">int EvalPRNExp(char exp[])    // 후위 표기법의 수식을 계산하여 그 결과를 반환
{
    ....
}</code></pre>
<p>이 함수는 후위 표기법의 수식을 담고 있는 문자열의 주소 값을 인자로 전달받는다.
그리고 그 수식의 계산결과를 반환한다.
이제 함수의 정의 내용을 채워보자...!</p>
<pre><code class="language-c">int EvalRPNExp(char exp[])
{
    Stack stack;
    int expLen = strlen(exp);
    int i;
    char tok, op1, op2;

    StackInit(&amp;stack);

    for(i=0; i&lt;expLen; i++)    // 수식을 구성하는 문자 각각을 대상으로 반복
    {
        tok = exp[i];        // 한 문자씩 tok에 저장해서
        if(isdigit(tok))    // 문자의 내용이 연산자인지 피연산자인지 확인
            SPush(&amp;stack, tok - &#39;0&#39;);    // 피연산자(정수)일 경우 숫자로 변환 후 스택에 Push
        else                    // 연산자일 경우
           {
            op2 = SPop(&amp;stack);    // 스택에서 두 번째 피연산자 꺼내기.
            op1 = SPop(&amp;stack);    // 스택에서 첫 번째 피연산자 꺼내기.

            switch(tok)        // 연산하고 그 결과를 다시 스택에 Push
            {
            case &#39;+&#39;:
                SPush(&amp;stack, op1+op2);
                break;
            case &#39;-&#39;:
                SPush(&amp;stack, op1-op2);
                break;
            case &#39;*&#39;:
                SPush(&amp;stack, op1*op2);
                break;
            case &#39;/&#39;:
                SPush(&amp;stack, op1/op2);
                break;
            }
        }
    }
    return SPop(&amp;stack);    // 마지막 연산결과 스택에서 꺼내어 반환
}</code></pre>
<h3 id="후위-표기법-수식-계산-구현-2-프로그램의-실행"><em>후위 표기법 수식 계산 구현 2: 프로그램의 실행</em></h3>
<p>위에서 정의한 EvalRPNExp 함수의 실행 결과를 확인해볼 차례다.
헤더파일(<code>PostCalculator.h</code>)과 소스파일(<code>PostCalculator.c</code>), 실행파일(<code>PostCalculatorMain.c</code>)을 정리하면 아래와 같다.
이번에도 스택을 사용하기 때문에 스택과 관련된 <code>ListBaseStack.h</code>과 <code>ListBaseStack.c</code> 파일을 함께 둬야한다.</p>
<pre><code class="language-c">// PostCalculator.h
#ifndef __POST_CALCULATOR_H__
#define __POST_CALCULATOR_H__

int EvalRPNExp(char exp[]);

#endif</code></pre>
<pre><code class="language-c">// PostCalculator.c
#include &lt;string.h&gt;
#include &lt;ctype.h&gt;
#include &quot;ListBaseStack.h&quot;

int EvalRPNExp(char exp[])
{
    Stack stack;
    int expLen = strlen(exp);
    int i;
    char tok, op1, op2;

    StackInit(&amp;stack);

    for(i=0; i&lt;expLen; i++)    // 수식을 구성하는 문자 각각을 대상으로 반복
    {
        tok = exp[i];        // 한 문자씩 tok에 저장해서
        if(isdigit(tok))                // 문자의 내용이 연산자인지 피연산자인지 확인
            SPush(&amp;stack, tok - &#39;0&#39;);    // 피연산자(정수)일 경우 숫자로 변환 후 스택에 Push
        else                    // 연산자일 경우
           {
            op2 = SPop(&amp;stack);    // 스택에서 두 번째 피연산자 꺼내기.
            op1 = SPop(&amp;stack);    // 스택에서 첫 번째 피연산자 꺼내기.

            switch(tok)         // 연산하고 그 결과를 다시 스택에 Push
            {
            case &#39;+&#39;:
                SPush(&amp;stack, op1+op2);
                break;
            case &#39;-&#39;:
                SPush(&amp;stack, op1-op2);
                break;
            case &#39;*&#39;:
                SPush(&amp;stack, op1*op2);
                break;
            case &#39;/&#39;:
                SPush(&amp;stack, op1/op2);
                break;
            }
        }
    }
    return SPop(&amp;stack);    // 마지막 연산결과 스택에서 꺼내어 반환
}</code></pre>
<pre><code class="language-c">// PostCalculatorMain.c
#include &lt;stdio.h&gt;
#include &quot;PostCalculator.h&quot;

int main()
{
    char postExp1[] = &quot;42*8+&quot;;
    char postExp2[] = &quot;123+*4/&quot;;

    printf(&quot;&lt;Postfix Calculation&gt;\n&quot;);
    printf(&quot;%s = %d \n&quot;, postExp1, EvalRPNExp(postExp1));
    printf(&quot;%s = %d \n&quot;, postExp2, EvalRPNExp(postExp2));

    return 0;
}

&gt; gcc .\ListBaseStack.c .\PostCalculator.c .\PostCalculatorMain.c
&gt; .\a.exe
&gt; 출력
&lt;Postfix Calculation&gt;
42*8+ = 16 
123+*4/ = 1</code></pre>
<p>여기서는 후위 표기법의 수식을 입력 받아서 이를 계산하여 결과를 출력하는 과정만 보여주고 있다.
우리가 생각하는 계산기를 만들려면 중위 표기법으로 수식을 입력해서 이것을 후위 표기법으로 저장하고 계산된 값이 도출되어야하기 때문에 이 모든 함수를 정리할 필요가 있다.</p>
<h3 id="계산기-프로그램-최종-정리하기"><em>계산기 프로그램 최종 정리하기</em></h3>
<p>중위 표기법의 수식을 후위 표기법의 수식으로 변환하는 함수도 정의하였고,
후위 표기법의 수식을 계산하는 함수도 정의했다.
프로그램 사용자로부터 소괄호를 포함하는 중위 표기법의 수식을 입력 받아서 그 결과를 출력하는 프로그램, 즉 계산기를 최종 정리해서 구현해보자.
그 과정을 간략하게 표현하면 <code>중위 표기법 수식 → ConvToRPNExp → EvalRPNExp → 연산결과</code>가 출력하게 된다.</p>
<p>이 과정을 <code>EvalInfixExp</code>라는 함수로 정리할 것이고 이 함수의 선언, 정의, 실행 파일은 다음과 같이 정리할 수 있다.</p>
<pre><code class="language-c">// InfixCalculator.h
#ifndef __INFIX_CALCULATOR_H__
#define __INFIX_CALCULATOR_H__

int EvalInfixExp(char exp[]);

#endif</code></pre>
<pre><code class="language-c">// InfixCalculator.c
#include &lt;string.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;InfixToPostfix.h&quot; // ConvToRPNExp 함수
#include &quot;PostCalculator.h&quot; // EvalRPNExp 함수

int EvalInfixExp(char exp[])
{
    int len = strlen(exp);
    int ret;
    char * expcpy = (char *)malloc(len + 1);    // 문자열 저장공간 마련
    strcpy(expcpy, exp);                        // exp를 expcpy에 복사

    ConvToRPNExp(expcpy);       // 후위 표기법의 수식으로 변환
    ret = EvalRPNExp(expcpy);   // 변환된 수식 계산

    free(expcpy);   // 문자열 저장공간 해제
    return ret;     // 계산 결과 반환
}</code></pre>
<pre><code class="language-c">// InfixCalculatorMain.c
#include &lt;stdio.h&gt;
#include &quot;InfixCalculator.h&quot;

int main()
{
    char exp1[] = &quot;1+2*3&quot;;
    char exp2[] = &quot;(1+2)*3&quot;;
    char exp3[] = &quot;((1-2)+3)*(5-2)&quot;;

    printf(&quot;&lt;Calculator&gt;\n&quot;);
    printf(&quot;1: %s = %d\n&quot;, exp1, EvalInfixExp(exp1));
    printf(&quot;2: %s = %d\n&quot;, exp2, EvalInfixExp(exp2));
    printf(&quot;3: %s = %d\n&quot;, exp3, EvalInfixExp(exp3));
    return 0;
}

&gt; gcc .\InfixCalculator.c .\InfixToPostfix.c .\ListBaseStack.c .\PostCalculator.c .\InfixCalculatorMain.c
&gt; .\a.exe
&gt; 출력
&lt;Calculator&gt;
1: 1+2*3 = 7
2: (1+2)*3 = 9
3: ((1-2)+3)*(5-2) = 6</code></pre>
<p>마지막으로 계산기 프로그램 실행에 필요한 헤더파일, 소스파일들을 정리하면 다음과 같다.</p>
<ul>
<li>스택의 활용: ListBaseStack.h, ListBaseStack.c</li>
<li>후위 표기법의 수식으로 변환: InfixToPostfix.h, InfixToPostfix.c</li>
<li>후위 표기법의 수식을 계산: PostCalculator.h, PostCalculator.c</li>
<li>중위 표기법의 수식을 계산: InfixCalculator.h, InfixCalculator.c</li>
<li>main 함수: InfixCalculatorMain.c</li>
</ul>
<hr>
<h2 id="review"><code>&lt;Review&gt;</code></h2>
<p>길고 길었던 스택 공부가 끝이 났다.
연결리스트에 비해 이해하는데 쉬웠지만
계산기 프로그램 구현에서 헤더파일과 소스파일이 많아지니 헷갈렸다...
궁금한 점은 왜 InfixCalculator에서는 기존에 사용한 ListBaseStack 헤더파일을 불러오지 않아도 작동하는지 (이 전 PostCalculator나 InfixToPost 헤더파일에서 불러져서 그런건지)
이런 점에 대해서 설명이 더 있었으면 좋았겠다란 생각이 있었다. (있었는데 내가 못 본걸수도!)
아무튼 오늘도 공부 완!</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/9a996232-4964-4ff8-9c2f-fc1e939635c9/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chapter 05. 연결 리스트(Linked List) 3]]></title>
            <link>https://velog.io/@mingming_eee/datastructure-05</link>
            <guid>https://velog.io/@mingming_eee/datastructure-05</guid>
            <pubDate>Tue, 22 Oct 2024 16:49:46 GMT</pubDate>
            <description><![CDATA[<h2 id="05-1-원형-연결-리스트circular-linked-list">05-1. 원형 연결 리스트(Circular Linked List)</h2>
<p>이번에 배울 &#39;원형 연결 리스트&#39;는 Chapter 04에서 배운 단순 연결 리스트에서 약간의 변경만 하면 쉽게 만들 수 있다.</p>
<h3 id="원형-연결-리스트의-이해"><em>원형 연결 리스트의 이해</em></h3>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/90908756-f199-4613-82c8-1fec8eb2f9ca/image.png" alt=""></p>
<p>단순 연결 리스트에서 마지막 노드는 NULL을 가리켰다.
이 마지막 노드가 첫 번째 노드를 가리키게 하면 이것이 바로 &#39;원형 연결 리스트&#39;가 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/6cb860c6-c8ae-48c3-989c-f6cc012e8840/image.png" alt=""></p>
<p>이 원형 리스트에서 머리 부분에 데이터 1이 저장된 노드를 추가하면 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/a43986d5-c7c3-4fde-9c6d-01f2c6156be4/image.png" alt=""></p>
<p>꼬리 부분에 데이터 1이 저장된 노드를 추가하면 어떻게 될까?</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/8d0e3f67-9869-4530-9f36-ab58bcb00c3f/image.png" alt=""></p>
<p>두 연결 리스트 모두 8이 저장된 노드는 1이 저장된 새 노드를 가리키고, 1이 저장된 새 노드는 2가 저장된 노드를 가리킨다.
이러한 특성 때문에 원형 연결 리스트에서는 머리와 꼬리의 구분이 없다고도 얘기한다.
두 연결 리스트의 유일한 차이점은 포인터 변수 head가 무엇을 가리키고 있는지 뿐이다.</p>
<p>그렇다면 포인터 변수 tail은 없어도 되는 걸까?
단순 연결 리스트에서는 tail이 없다면 매우 비효율적이기 때문에 tail이 있어야 했다.
하지만, 원형 연결 리스트에서는 하나의 포인터 변수만 있어도 머리 또는 꼬리에 노드를 추가할 수 있다는 점이 장점이다. </p>
<h3 id="변형된-원형-연결-리스트"><em>변형된 원형 연결 리스트</em></h3>
<p>우리는 이제 포인터 변수 head만 있는 원형 연결 리스트를 생각하는 것이 아닌,
포인터 변수 tail만 있는 변형된 원형 연결 리스트를 주로 다룰 것이다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/0ded1e5e-271a-4dcd-bb6b-185e65104065/image.png" alt=""></p>
<p>한 방향으로 연결되어 있는 리스트기 때문에 머리와 꼬리를 가리키는 포인터 변수를 두지 않고 하나의 포인터 변수를 둔다면 tail 포인터 변수를 사용하는 것이 보다 일반적이라고 인식되고 있다.
새로운 노드를 리스트의 끝에 추가할 때는 tail이 가리키는 곳 뒤에 넣으면 되고 <code>tail-&gt;next</code>가 가리키는 것이 첫 번째 노드니 이를 이용하면 리스트의 처음에도 노드를 추가하기 쉽다.
즉, 하나의 포인터 변수로 첫 번째 노드와 마지막 노드를 가리키는 포인터 변수가 각각 존재하게 되는 상황이니 어렵지 않게 머리와 꼬리에 노드를 추가할 수 있는 것이다.</p>
<h3 id="변형된-원형-연결-리스트-구현-1-헤더파일"><em>변형된 원형 연결 리스트 구현 1: 헤더파일</em></h3>
<p>이번 원형 연결 리스트를 구현하는데 있어서 LFirst, LNext, LRemove 함수의 역할이 중요하다.
단, 주의할 점은 원형 연결 리스트에서 LNext함수는 무한 반복 호출이 가능해 리스트의 끝에 도달할 경우 첫 번째 노드부터 다시 조회가 시작된다는 점이다.
정렬과 관련이 있는 기능은 제외시키고 데이터를 저장하는 함수는 두 개를 정의할 것이다.
(리스트 머리에 노드 추가, 꼬리에 노드 추가)
원래는 ADT를 먼저 정의하고 헤더파일을 정의해야하지만 이전에 배운 단순 연결 리스트와 큰 차이가 없기 때문에 바로 헤더파일을 정의할 것이다.</p>
<pre><code class="language-c">// CLinkedList.h
#ifndef __C_LINKED_LIST_H__
#define __C_LINKED_LIST_H__

#define TRUE        1
#define FALSE       0

typedef int Data;

typedef struct _node
{
    Data data;
    struct _node * next;
} Node;

typedef struct _CLL
{
    Node * tail;
    Node * cur;
    Node * before;
    int numOfData;
} CList;

typedef CList List;

void ListInit(List * plist);
void LInsert(List * plist, Data data);      // 꼬리에 노드 추가
void LInsertFront(List * plist, Data data); // 머리에 노드 추가

int LFirst(List * plist, Data * pdata);
int LNext(List * plist, Data * pdata);
Data LRemove(List * plist);
int LCount(List * plist);

#endif</code></pre>
<p>아까도 설명했듯 변경된 원형 연결 리스트에서는 노드 추가 부분에서 <code>LInsert</code> 함수와 <code>LInsertFront</code> 함수 두 가지로 나누어 정의할 것이다.</p>
<h3 id="변형된-원형-연결-리스트-구현-1-함수-정의"><em>변형된 원형 연결 리스트 구현 1: 함수 정의</em></h3>
<h4 id="1-리스트의-초기화">1) 리스트의 초기화</h4>
<p>원형 연결 리스트의 초기화는 단순 연결 리스트의 초기화만큼 간단하다.
아래 코드와 같이 리스트의 멤버를 NULL또는 0으로 초기화하는 것이 전부다.</p>
<pre><code class="language-c">void ListInit(List * plist)
{
    plist-&gt;tail = NULL;
    plist-&gt;cur = NULL;
    plist-&gt;before = NULL;
    plist-&gt;numOfData = 0;
}</code></pre>
<p>이 함수의 호출을 통해 초기화가 완료된 이후 첫 번째 노드가 추가되는 상황을 그림으로 나타내면 다음과 같다.</p>
<img src="https://velog.velcdn.com/images/mingming_eee/post/e8f04dbb-e152-4942-86b6-d424173db5e2/image.png" width=30%>

<p>새 노드가 추가되었지만 아직 리스트에 추가되지 않았다.
tail이 NULL을 가리킨다는 것은 노드가 하나도 추가되지 않았다는 것을 의미한다.</p>
<h4 id="2-노드의-삽입">2) 노드의 삽입</h4>
<p>초기화 이후 첫 번째 노드를 추가한 뒤 리스트에 노드를 추가하기 위해서는 tail이 새 노드를 가리켜야 하고, 새 노드도 자기 자신을 가리켜야 한다.
왜냐하면 처음 추가된 노드는 그 자체로 머리이자 꼬리이기 때문이다.
아래는 첫 번째 노드가 리스트에 추가가 완료 되었을 때 그림이다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/3847d723-26cd-4dea-8fc4-0ff0b5c16ed4/image.png" alt=""></p>
<p>첫 번째 노드 추가 부분은 <code>LInsert</code> 함수와 <code>LInsertFront</code> 함수 모두 동일한 기본 구성을 갖게 된다.</p>
<pre><code class="language-c">void LInsert~(List * plist, Data data)    // LInsert &amp; LInsertFront 공통 부분
{
    Node * newNode = (Node *)malloc(sizeof(Node));    // 새 노드 생성
    newNode-&gt;data = data;                            // 새 노드에 데이터 저장

    if(plist-&gt;tail == NULL)            // 첫 번째 노드
    {
        plist-&gt;tail = newNode;        // tail이 새 노드를 가리키게 함
        newNode-&gt;next = newNode;    // 새 노드 자신도 가리키게 함
    }
       else    // 두 번째 이후 노드
    {
        ....
    }

    (plist-&gt;numOfData)++;
}</code></pre>
<p>두 번째 이후 노드 추가되는 것에 대해 생각해보자.
아래 그림은 2와 4가 저장된 연결 리스트에 7이 저장된 노드를 리스트의 머리와 꼬리에 각각 추가했을 때 상황이다.</p>
<table>
<thead>
<tr>
<th>Case</th>
<th>Image</th>
</tr>
</thead>
<tbody><tr>
<td>두 번째 이후의 노드 추가</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/5e703804-8cf5-4fb0-8f1e-6df70299fabd/image.png" width=60%></td>
</tr>
<tr>
<td>머리에 노드 추가</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/cc38c842-b523-4e52-9829-094408d7b741/image.png" alt=""></td>
</tr>
<tr>
<td>꼬리에 노드 추가</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/952b9bfb-175c-411b-bd8c-c4d9a208a497/image.png" alt=""></td>
</tr>
</tbody></table>
<p>위 그림의 결과를 얻기 위한 코드를 else문에 작성하면 다음과 같다.</p>
<pre><code class="language-c">void LInsertFront(List * plist, Data data)
{
    Node * newNode = (Node *)malloc(sizeof(Node));
    newNode-&gt;data = data;

    if(plist-&gt;tail == NULL)
    {
        plist-&gt;tail = newNode;
        newNode-&gt;next = newNode;
    }
       else    // 두 번째 이후 노드
    {
        newNode-&gt;next = plist-&gt;tail-&gt;next;    // 새 노드와 4가 저장된 노드 연결
        plist-&gt;tail-&gt;next = newNode;        // 2가 저장된 노드와 새 노드 연결
    }

    (plist-&gt;numOfData)++;
}

void LInsert(List * plist, Data data)
{
    Node * newNode = (Node *)malloc(sizeof(Node));
    newNode-&gt;data = data;

    if(plist-&gt;tail == NULL)
    {
        plist-&gt;tail = newNode;
        newNode-&gt;next = newNode;
    }
       else    // 두 번째 이후 노드
    {
        newNode-&gt;next = plist-&gt;tail-&gt;next;
        plist-&gt;tail-&gt;next = newNode;
        plist-&gt;tail = newNode;    // LInsertFront 함수와 유일한 차이점
    }

    (plist-&lt;numOfData)++;
}</code></pre>
<p>LInsert 함수를 보면 LInsertFront 함수와 달리 코드 한 줄이 더 추가되어있따.
이는 노드를 꼬리에 추가했을 때와 머리에 추가했을 때 tail이 가리키는 노드가 다르다는 점이다.
아래 그림에서 볼 수 있듯 새 노드를 머리에 추가한 상태에서 연결 방향에 따라 tail만 한 번 이동시키면, 그 결과가 새 노드를 꼬리에 추가한 결과가 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/b01df097-e8cf-4e2d-abfd-332489195dc4/image.png" alt=""></p>
<p>이를 통해 원형 연결 리스트는 머리와 꼬리의 구분이 의미가 없다는 것을 알 수 있다.</p>
<h4 id="3-데이터-조회">3) 데이터 조회</h4>
<p>데이터의 조회를 담당하는 LFirst 함수와 LNext 함수를 구현해보자.
이를 위해 구조체 정의한 것을 다시 한번 살펴보자.</p>
<pre><code class="language-c">typedef struct _CLL
{
    Node * tail;
    Node * cur;
    Node * before;
    int numOfData;
} CList;</code></pre>
<p>위 구조체의 멤버 cur과 before의 역할은 단순 연결 리스트의 경우와 동일하다.
즉 before는 cur보다 하나 앞선 노드를 가리켜야 하기 때문에 LFirst 함수가 호출되면 다음 그림과 같이 초기화 되어야 한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/1303b78e-5c95-4a1b-ae6a-53952eefa828/image.png" alt=""></p>
<p>cur이 가리키는 노드가 곧 머리가 되기 때문에
cur과 before를 초기화하고 LFirst 함수는 cur이 가리키는 노드의 데이터를 반환하면 된다.</p>
<pre><code class="language-c">int LFirst(List * plist, Data * pdata)
{
    if(plist-&gt;tail == NULL)            // 저장된 노드가 없다면
        return FALSE;

    plist-&gt;before = plist-&gt;tail;    // before가 꼬리를 가리키게
    plist-&gt;cur = plist-&gt;tail-&gt;next;    // cur이 머리를 가리키게

    *pdata = plist-&gt;cur-&gt;data;        // cur이 가리키는 노드의 데이터 반환
    return TRUE;
}</code></pre>
<p>LFirst 함수가 호출되면서 cur과 before의 초기화가 이뤄졌기 때문에 LNext 함수가 호출되면 cur과 before가 가리키는 노드를 한 칸씩 다음 노드로 이동시키면 된다.</p>
<pre><code class="language-c">int LNext(List * plist, Data * pdata)
{
    if(plist-&gt;tail == NULL)            // 저장된 노드가 없다면
        return FALSE;

    plist-&gt;before = plist-&gt;cur;        // before가 다음 노드 가리키게
    plist-&gt;cur = plist-&gt;cur-&gt;next;    // cur가 다음 노드 가리키게

    *pdata = plist-&gt;cur-&gt;data;        // cur이 가리키는 노드의 데이터 반환
    return TRUE;
}</code></pre>
<p>이 코드를 그림으로 나타내면 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/f2d76ac2-fbe0-4830-9f13-e4b03138eeb4/image.png" alt=""></p>
<p>참고로 LNext 함수에는 리스트의 끝을 검사하는 코드가 없다.
무한으로 반복해서 호출이 가능하며 대상이 되는 원형 연결 리스트는 머리와 꼬리가 연결된 관계로 리스트의 마지막까지 조회가 이뤄졌다면 다시 첫 번째 노드에서부터 조회가 시작된다.</p>
<h4 id="4-노드의-삭제">4) 노드의 삭제</h4>
<p>머리와 꼬리가 연결되어 있다는 점을 제외하고는 원형 연결 리스트와 단순 연결 리스트는 그 구조가 동일하기 때문에 삭제 방법이 유사하다.
따라서, 원형 연결 리스트의 삭제를 구현하기 위해 단순 연결 리스트의 삭제과정을 다시 한번 살펴보자.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/7589ae32-61e2-47e4-b719-e288f4f83a41/image.png" alt=""></p>
<ul>
<li>핵심 연산 1) 삭제할 노드의 이전 노드가 삭제할 노드의 다음 노드를 가리키게 한다.</li>
<li>핵심 연산 2) 포인터 변수 cur을 한 칸 뒤로 이동시킨다.</li>
</ul>
<p>원형 연결 리스트의 경우 아래와 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/a7ded0a2-c474-4097-b735-d05a3e7bc16c/image.png" alt=""></p>
<p>단순 연결 리스트에서는 더미 노드가 있고 원형 연결 리스트에서는 더미 노드가 없기 때문에 삭제의 과정이 상황에 따라 달라질 수 있다.
따라서 다음 두 가지 예외적인 상황을 구분해야 한다.</p>
<ul>
<li>예외적인 상황 1) 삭제할 노드를 tail이 가리키는 경우</li>
<li>예외적인 상황 2) 삭제할 노드가 리스트에 홀로 남은 경우</li>
</ul>
<p>예외적인 상황 1의 경우 tail이 가리키는 노드가 삭제되므로 tail이 다른 노드를 가리키게 해야 한다. 따라서 삭제될 노드의 이전 노드가 tail이 가리키게 만든 다음 노드를 삭제해야한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/5ee5b5bc-68c4-437a-b3f2-c679f605e429/image.png" alt=""></p>
<p>예외적인 상황 2의 경우 마지막 노드까지 삭제를 한 뒤 포인터 변수 tail은 더 이상 가리킬 노드가 존재하지 않기 때문에 NULL을 가리키게 해야 한다.</p>
<p>위 두 경우를 모두 생각해서 코드를 작성하면 다음과 같다.</p>
<pre><code class="language-c">Data LRemove(List * plist)
{
    Node * rpos = plist-&gt;cur;
    Data rdata = rpos-&gt;data;

    if(rpos == plist-&gt;tail)                        // 예외적인 상황 1) 삭제할 노드를 tail이 가리키는 경우
    {
        if(plist-&gt;tail == plist-&gt;tail-&gt;next)    // 예외적인 상황 2) 삭제할 노드가 리스트에 홀로 남은 경우
            plist-&gt;tail == NULL;
        else
            plist-&gt;tail = plist-&gt;before;
    }

    plist-&gt;before-&gt;next = plist-&gt;cur-&gt;next;
    plist-&gt;cur = plist-&gt;before;

    free(rpos);
    (plist-&gt;numOfData)--;
    return rdata;
}</code></pre>
<p>만약 원형 연결 리스트에서 단순 연결 리스트와 같이 더미 노드를 붙여주면 예외적인 상황 2가지를 고려하지 않고 간단해지지 않을까?
LRemove 함수 뿐만 아니라 노드의 추가 관련 두 함수의 구현도 간단해질 수 있다.
다만 데이터를 순환 참조하는 LNext 함수 구현에 있어서 더미 노드의 처리를 위한 코드를 추가로 삽입해야 한다는 단점이 생긴다.</p>
<h4 id="5-정리하기">5) 정리하기</h4>
<p>위에 각 기능별 함수 구현이 끝났다.
이 모든 것을 헤더파일, 소스파일로 다시 한번 정리해보자.</p>
<pre><code class="language-c">// CLinkedList.h
#ifndef __C_LINKED_LIST_H__
#define __C_LINKED_LIST_H__

#define TRUE        1
#define FALSE       0

typedef int Data;

typedef struct _node
{
    Data data;
    struct _node * next;
} Node;

typedef struct _CLL
{
    Node * tail;
    Node * cur;
    Node * before;
    int numOfData;
} CList;

typedef CList List;

void ListInit(List * plist);
void LInsert(List * plist, Data data);      // 꼬리에 노드 추가
void LInsertFront(List * plist, Data data); // 머리에 노드 추가

int LFirst(List * plist, Data * pdata);
int LNext(List * plist, Data * pdata);
Data LRemove(List * plist);
int LCount(List * plist);

#endif</code></pre>
<pre><code class="language-c">// CLinkedList.c
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;CLinkedList.h&quot;

// 리스트의 초기화
void ListInit(List * plist)
{
    plist-&gt;tail = NULL;
    plist-&gt;cur = NULL;
    plist-&gt;before = NULL;
    plist-&gt;numOfData = 0;
}

// 노드의 삽입
// 머리에 삽입
void LInsertFront(List * plist, Data data)
{
    Node * newNode = (Node *)malloc(sizeof(Node));
    newNode-&gt;data = data;

    if(plist-&gt;tail == NULL)
    {
        plist-&gt;tail = newNode;
        newNode-&gt;next = newNode;
    }
       else
    {
        newNode-&gt;next = plist-&gt;tail-&gt;next;
        plist-&gt;tail-&gt;next = newNode;
    }

    (plist-&gt;numOfData)++;
}

// 꼬리에 삽입
void LInsert(List * plist, Data data)
{
    Node * newNode = (Node *)malloc(sizeof(Node));
    newNode-&gt;data = data;

    if(plist-&gt;tail == NULL)
    {
        plist-&gt;tail = newNode;
        newNode-&gt;next = newNode;
    }
       else
    {
        newNode-&gt;next = plist-&gt;tail-&gt;next;
        plist-&gt;tail-&gt;next = newNode;
        plist-&gt;tail = newNode;
    }

    (plist-&gt;numOfData)++;
}

// 데이터 조회
// 첫 번째 노드
int LFirst(List * plist, Data * pdata)
{
    if(plist-&gt;tail == NULL)
        return FALSE;

    plist-&gt;before = plist-&gt;tail;
    plist-&gt;cur = plist-&gt;tail-&gt;next;

    *pdata = plist-&gt;cur-&gt;data;
    return TRUE;
}

// 두 번째 이후 노드
int LNext(List * plist, Data * pdata)
{
    if(plist-&gt;tail == NULL)
        return FALSE;

    plist-&gt;before = plist-&gt;cur;
    plist-&gt;cur = plist-&gt;cur-&gt;next;

    *pdata = plist-&gt;cur-&gt;data;
    return TRUE;
}

// 노드의 삭제
Data LRemove(List * plist)
{
    Node * rpos = plist-&gt;cur;
    Data rdata = rpos-&gt;data;

    if(rpos == plist-&gt;tail)
    {
        if(plist-&gt;tail == plist-&gt;tail-&gt;next)
            plist-&gt;tail == NULL;
        else
            plist-&gt;tail = plist-&gt;before;
    }

    plist-&gt;before-&gt;next = plist-&gt;cur-&gt;next;
    plist-&gt;cur = plist-&gt;before;

    free(rpos);
    (plist-&gt;numOfData)--;
    return rdata;
}

int LCount(List * plist)
{
    return plist-&gt;numOfData;
}</code></pre>
<h3 id="변형된-원형-연결-리스트-구현-3-main-함수-정의"><em>변형된 원형 연결 리스트 구현 3: main 함수 정의</em></h3>
<p>위의 원형 연결 리스트를 테스트하기 위한 main 함수를 작성해보자.
그리고 그 실행 결과도 확인해보자!</p>
<pre><code class="language-c">// CLinkedListMain.c
#include &lt;stdio.h&gt;
#include &quot;CLinkedList.h&quot;

int main()
{
    // 원형 연결 리스트의 생성 및 초기화
    List list;
    int data, i , nodeNum;
    ListInit(&amp;list);

    // 리스트에 5개의 데이터 저장
    LInsert(&amp;list, 3);
    LInsert(&amp;list, 4);
    LInsert(&amp;list, 5);
    LInsertFront(&amp;list, 2);
    LInsertFront(&amp;list, 1);

    // 리스트에 저장된 데이터 연속 3회 출력
    if(LFirst(&amp;list, &amp;data))
    {
        printf(&quot;%d &quot;, data);

        for(i=0; i&lt;LCount(&amp;list)*3-1; i++)
        {
            if(LNext(&amp;list, &amp;data))
                printf(&quot;%d &quot;, data);
        }
    }
    printf(&quot;\n&quot;);

    // 2의 배수를 찾아서 모두 삭제
    nodeNum = LCount(&amp;list);

    if(nodeNum != 0)
    {
        LFirst(&amp;list, &amp;data);
        if(data%2 == 0)
            LRemove(&amp;list);

        for(i=0; i&lt;nodeNum-1; i++)
        {
            LNext(&amp;list, &amp;data);
            if(data%2 == 0)
                LRemove(&amp;list);
        }
    }

    // 전체 데이터 1회 출력
    if(LFirst(&amp;list, &amp;data))
    {
        printf(&quot;%d &quot;, data);

        for(i=0; i&lt;LCount(&amp;list)-1; i++)
        {
            if(LNext(&amp;list, &amp;data))
                printf(&quot;%d &quot;, data);
        }
    }
    return 0;
}

&gt; gcc .\CLinkedList.c .\CLinkedListMain.c
&gt; .\a.exe
&gt; 출력
1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 
1 3 5</code></pre>
<hr>
<h2 id="05-2-양방향-연결-리스트">05-2. 양방향 연결 리스트</h2>
<p><code>양방향 연결 리스트(Doubly Linked List)</code> 또는 <code>이중 연결 리스트</code>라고도 불리는 이 자료구조는 그 이름이 의미하듯이 노드가 양쪽 방향으로 연결된 구조의 리스트다.
즉, 왼쪽 노드가 오른쪽 노드를 가리킴과 동시에 오른쪽 노드도 왼쪽 노드를 가리키는 구조다.</p>
<h3 id="양방향-연결-리스트의-이해"><em>양방향 연결 리스트의 이해</em></h3>
<p>양방향 연결 리스트의 유형 몇 가지를 그림을 통해서 소개하고자 한다.
가장 기본이 되는 모델을 다음 그림과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/228af7f1-a868-47aa-a8e7-a88e53095051/image.png" alt=""></p>
<p>하나의 노드가 자신의 왼쪽과 오른쪽 노드를 동시에 가리키는 구조가 양방향 연결 리스트다.
때문에 양방향 연결 리스트의 노드를 표현하는 구조체는 다음과 같이 정의된다.</p>
<pre><code class="language-c">typedef struct _node
{
    Data data;                // typedef int Data
    struct _node * next;    // 오른쪽 노드를 가리키는 포인터 변수
    struct _node * prev;    // 왼쪽 노드를 가리키는 포인터 변수
} Node;</code></pre>
<p>그리고 다음 그림과 같이 더미 노드가 추가된 양방향 연결 리스트도 존재하는데, 더미 노드의 이점은 단순 연결 리스트에서 보인 바와 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/65967f03-30e0-4f16-8086-914826f68a3a/image.png" alt=""></p>
<p>그리고 다음 그림과 같이 양방향 연결 리스트면서 원형 연결 리스트의 구조를 동시에 지니는 리스트도 존재한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/2d845caf-8628-4cca-8912-19ef4f5f6bfc/image.png" alt=""></p>
<p>하지만 지금 보인 이 세 가지 모델이 양방향 연결 리스트의 전부는 아니다.
이들 모두 꼬리를 가리키는 포인터 변수 tail이 없었으나 필요하면 추가할 수 있고, 또 두 번째로 소개한 양방향 연결 리스트의 경우, 더미 노드가 앞에만 존재하는 형태이지만 필요에 따라서 뒤에도 더미 노드를 둘 수 있다.</p>
<p>그렇다면 양방향 연결 리스트는 다른 연결 리스트에 비해 구현하기 어려울까?
생각보다 많이 복잡하지 않다는 것을 이제부터 배울 예정이다!
양쪽 방향으로 이동할 수 있기 때문에 단방향 연결 리스트에서는 어렵게 구현되던 거싱 오히려 단순하게 구현되는 부분도 있다.
그 예시로 LNext 함수를 살펴보자.</p>
<pre><code class="language-c">// CLinkedList.c (원형 연결 리스트의 LNext 함수)
int LNext(List * plist, Data * pdata)
{
    if(plist-&gt;tail == NULL)
        return FALSE;

    plist-&gt;before = plist-&gt;cur;
    plist-&gt;cur = plist-&gt;cur-&gt;next;

    *pdata = plist-&gt;cur-&gt;data;
    return TRUE;
}</code></pre>
<pre><code class="language-c">// DBLinkedList.c 예정 (양방향 연결 리스트의 LNext 함수)
int LNext(List * plist, Data * pdata)
{
    if(plist-&gt;cur-&gt;next == NULL)
        return FALSE;

    plist-&gt;cur = plist-&gt;cur-&gt;next;
    *pdata = plist-&gt;cur-&gt;data;

    return TRUE;
}</code></pre>
<p>위의 두 LNext 함수는 매우 유사하다.
LNext 함수는 두 번째 노드 이후의 데이터를 조회하는데 사용되는 함수이기 때문에 리스트의 구조에 따라 크게 달라지지 않는다.
양방향 연결 리스트가 더 복잡해보였지만 오히려 원형 연결 리스트에서 한 문장이 더 추가되어 있다.
포인터 변수 before에 대한 코드다.
이 포인터 변수는 리스트가 한 방향으로만 조회가 가능하기 때문에 사용된 (삭제 과정에서 필요했다.) 멤버이다.
하지만 양방향 연결 리스트에서는 양방향으로 얼마든지 조회가 가능하기 때문에 포인터 변수 before가 불필요하고, 이 포인터 변수를 유지하기 위한 다른 곳곳에 쓰인 문장들도 불필요해진다.
이렇듯 양방향으로 노드를 연결하는 것에는 큰 이점이 있다.
(물론 노드를 양방향으로 연결하기 위해 몇몇 추가되는 문장이 있지만 마냥 더 복잡해지거나 구현하기 어렵다는 고정 관념을 버리자!)</p>
<h3 id="양방향-연결-리스트-구현-1-헤더파일의-정의"><em>양방향 연결 리스트 구현 1: 헤더파일의 정의</em></h3>
<p>총 두 가지 형태로 양방향 연결 리스트를 구현해 볼 것이다.</p>
<p>1) 양방향 연결 리스트
2) 더미 기반 양방향 연결 리스트 (연습 문제 2번)</p>
<p>그 중 배워 볼 첫 번째 양방향 연결 리스트는 아래 그림과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/775acccb-d77b-4d33-ad21-4403e75987fb/image.png" alt=""></p>
<p>그리고 이러한 양방향 연결 리스트의 ADT를 구현한 헤더파일은 다음과 같다.</p>
<pre><code class="language-c">// DBLinkedList.h
#ifndef __DB_LINKED_LIST_H__
#define __DB_LINKED_LIST_H__

#define TRUE 1
#define FALSE 0

typedef int Data;

typedef struct _node
{
    Data data;
    struct _node * next;
    struct _node * prev;
} Node;

typedef struct _DLinkedList
{
    Node * head;
    Node * cur;
    int numOfData;
} DBLinkedList;

typedef DBLinkedList List;

void ListInit(List * plist);
void ListInsert(List * plist, Data data);

int LFirst(List * plist, Data * data);
int LNext(List * plist, Data * data);
int LPrevious(List * plist, Data * data);
int LCount(List * plist);

#endif</code></pre>
<p>여기서 처음보는 함수는 <code>LPrevious</code> 함수일 것이다.
이 함수에 대한 설명은 소스파일의 구현 파트에서 알아보자.</p>
<h3 id="양방향-연결-리스트-구현-2-소스파일의-정의"><em>양방향 연결 리스트 구현 2: 소스파일의 정의</em></h3>
<h4 id="1-리스트의-초기화-1">1) 리스트의 초기화</h4>
<p>양방향 연결 리스트의 초기화를 담당하는 ListInit 함수의 정의를 위해 다음 구조체를 보자.</p>
<pre><code class="language-c">typedef struct _dbLinkedList
{
    Node * head;
    Node * cur;
    int numOfData;
} DBLinkedList;</code></pre>
<p>위 구조체에서 주목해야 할 것은, 데이터의 조회의 목적으로 선언된 멤버 cur이 하나라는 것이다.
단순 연결 리스트에 있던 before가 없다.
따라서 ListInit 함수는 다음과 같이 간단히 정의된다.</p>
<pre><code class="language-c">void ListInit(List * plist)
{
    plist-&gt;head = NULL;
    plist-&gt;numOfData = 0;
}</code></pre>
<p>조회에 사용되는 멤버 cur은 LFirst 함수가 호출됨에 동시에 초기화되기 때문에 리스트를 초기화 하는 단계에서 굳이 하지 않아도 된다.</p>
<h4 id="2-노드의-삽입-1">2) 노드의 삽입</h4>
<p>양방향 연결 리스트의 LInsert 함수는 리스트의 머리에 새 노드를 추가하는 방식으로 구현할 것이다.
따라서 <code>head → 8 → 7 → 6 → 5 → 4 → 3 → 2 → 1</code>의 형태로 저장된다.
머리에 추가되는 새로운 노드 및 데이터의 저장과 관련해서 다음 두 가지 상황을 고려하면 된다.</p>
<ol>
<li><p>첫 번째 노드 추가.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/333573d9-b160-4846-a76b-1c11d55dd01a/image.png" alt=""></p>
<p>연결 리스트가 텅 빈 상황에서 연결 리스트의 포인터 변수 head에는 NULL이 저장되어 있다.
새 노드의 next와 prev를 NULL로 초기화 하고, 새 노드를 head가 가리키게 한다.
코드로 구현하면 아래와 같다.</p>
<pre><code class="language-c">void LInsert(List * plist, Data data)
{
 Node * newNode = (Node *)malloc(sizeof(Node));
 newNode-&gt;data = data;

 // 아래 문장에서 plist-&gt;head == NULL
 newNode-&gt;next = plist-&gt;head;
 newNode-&gt;prev = NULL;
 plist-&gt;head = newNode;    // 포인터 변수 head가 새 노드를 가리키기

 (plist-&gt;numOfData)++;
}</code></pre>
</li>
<li><p>두 번째 이후 노드 추가.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/153ac0e2-5fa5-4b37-a96d-def381c54d89/image.png" alt=""></p>
<p>첫 번째 노드가 추가된 상황에서 두 번째 노드를 추가하기 위해 가장 먼저 할 일은 &quot;새 노드를 생성하고, 이 새 노드와 head가 가리키는 노드가 서로를 가리키게 한다&quot;는 것이다.</p>
<p>이를 그림으로 나타내면 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/a4e83f7a-b5cf-4cbc-89eb-f21b3f96e795/image.png" alt=""></p>
<p>이 과정은 <code>newNode-&gt;next = plist-&gt;head;</code>와 <code>plist-&gt;head-&gt;prev = newNode;</code> 문장으로 구현할 수 있다.
이제 head가 새 노드를 가리키게 하고 새 노드의 prev에 NULL을 채워서 다음 그림과 같이 만들면 두 번째 이후 노드 추가도 끝이 난다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/6545b1a1-25c7-4bb0-a1fd-914dea68bf2a/image.png" alt=""></p>
<p>이 과정은 <code>newNode-&gt;prev = NULL;</code>과 <code>plist-&gt;head = newnode;</code> 문장으로 구현할 수 있다.</p>
<p>위 1, 2번 케이스 모든 내용을 정리하면 아래와 같다.</p>
<pre><code class="language-c">void LInsert(List * plist, Data data)
{
   Node * newNode = (Node *)malloc(sizeof(Node));
   newNode-&gt;data = data;

   // 첫 번째 노드일 경우 plist-&gt;head == NULL
   newNode-&gt;next = plist-&gt;head;
     if(plist-&gt;head != NULL)    // 두 번째 이후 노드 추가
         plist-&gt;head-&gt;prev = newNode;

   newNode-&gt;prev = NULL;
   plist-&gt;head = newNode;

   (plist-&gt;numOfData)++;
}</code></pre>
</li>
</ol>
<h4 id="3-데이터-조회-1">3) 데이터 조회</h4>
<p>양방향 연결 리스트에서 데이터 조회는 세 가지 함수로 이루어져 있다.
LFirst와 LNext 함수는 단방향 연결 리스트와 거의 차이가 없다. (오히려 간단해졌다.)
before라는 구조체 멤버가 없어졌기 때문에 코드를 정리하면 아래와 같다.</p>
<pre><code class="language-c">int LFirst(List * plist, Data * pdata)
{
    if(plist-&gt;head == NULL)
        return FALSE;

    plist-&gt;cur = plist-&gt;head;    // cur이 첫 번째 노드를 가리키게
    *pdata = plist-&gt;cur-&gt;data;    // cur이 가리키는 노드의 데이터 반환
    return TRUE;
}

int LNext(List * plist, Data * pdata)
{
    if(plist-&gt;cur-&gt;next == NULL)
        return FALSE;

    plist-&gt;cur = plist-&gt;cur-&gt;next;    // cur을 오른쪽으로 이동
    *pdata = plist-&gt;cur-&gt;data;    // cur이 가리키는 노드의 데이터 반환
    return TRUE;
}

int LPrevious(List * plist, Data * pdata)
{
    if(plist-&gt;cur-&gt;prev == NULL)
        return FALSE;

    plist-&gt;cur = plist-&gt;cur-&gt;prev;    // cur을 왼쪽으로 이동
    *pdata = plist-&gt;cur-&gt;data;    // cur이 가리키는 노드의 데이터 반환
    return TRUE;
}</code></pre>
<p>LPrevious 함수는 LNext 함수의 반대방향으로 데이터를 조회하기 때문에 구조체 Node의 멤버 next가 아닌 prev를 사용해서 cur을 이동시켰다. </p>
<h4 id="-데이터-삭제">+) 데이터 삭제</h4>
<p>이 형태의 양방향 연결 리스트의 LRemove 함수를 정의하려면,</p>
<p>1) 첫 번째 노드를 삭제하는 경우
2) 마지막 노드를 삭제하는 경우
3) 그 이외의 노드를 삭제하는 경우
를 각각 별도로 구분해야하기 때문에 따로 구현하지 않을 예정이다.
대신 양방향 연결 리스트가 아니라면 구현하기 힘든 함수인 <code>int LPrevious(List * plist, Data * pdata);</code> 함수를 위에서 정의했다.
이 함수는 LFirst 또는 LNext 함수가 호출된 이후에 어디서든 호출이 가능하며,
LNext 함수가 오른쪽 노드로 이동해서 그 노드의 데이터를 참조하는 함수라면,
이 함수는 그와 반대인 왼쪽 노드로 이동해서 그 노드의 데이터를 참조한다.</p>
<h4 id="4-정리하기">4) 정리하기</h4>
<p>위에서 구현한 헤더파일, 소스파일을 정리해보자.</p>
<pre><code class="language-c">// DBLinkedList.h
#ifndef __DB_LINKED_LIST_H__
#define __DB_LINKED_LIST_H__

#define TRUE 1
#define FALSE 0

typedef int Data;

typedef struct _node
{
    Data data;
    struct _node * next;
    struct _node * prev;
} Node;

typedef struct _DLinkedList
{
    Node * head;
    Node * cur;
    int numOfData;
} DBLinkedList;

typedef DBLinkedList List;

void ListInit(List * plist);
void ListInsert(List * plist, Data data);

int LFirst(List * plist, Data * data);
int LNext(List * plist, Data * data);
int LPrevious(List * plist, Data * data);
int LCount(List * plist);

#endif</code></pre>
<pre><code class="language-c">// DBLinkedList.c
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;DBLinkedList.h&quot;

void ListInit(List * plist)
{
    plist-&gt;head = NULL;
    plist-&gt;numOfData = 0;
}

void LInsert(List * plist, Data data)
{
    Node * newNode = (Node *)malloc(sizeof(Node));
    newNode-&gt;data = data;

    // 첫 번째 노드일 경우 plist-&gt;head == NULL
    newNode-&gt;next = plist-&gt;head;
      if(plist-&gt;head != NULL)    // 두 번째 이후 노드 추가
          plist-&gt;head-&gt;prev = newNode;

    newNode-&gt;prev = NULL;
    plist-&gt;head = newNode;

    (plist-&gt;numOfData)++;
}

int LFirst(List * plist, Data * pdata)
{
    if(plist-&gt;head == NULL)
        return FALSE;

    plist-&gt;cur = plist-&gt;head;    // cur이 첫 번째 노드를 가리키게
    *pdata = plist-&gt;cur-&gt;data;    // cur이 가리키는 노드의 데이터 반환
    return TRUE;
}

int LNext(List * plist, Data * pdata)
{
    if(plist-&gt;cur-&gt;next == NULL)
        return FALSE;

    plist-&gt;cur = plist-&gt;cur-&gt;next;    // cur을 오른쪽으로 이동
    *pdata = plist-&gt;cur-&gt;data;    // cur이 가리키는 노드의 데이터 반환
    return TRUE;
}

int LPrevious(List * plist, Data * pdata)
{
    if(plist-&gt;cur-&gt;prev == NULL)
        return FALSE;

    plist-&gt;cur = plist-&gt;cur-&gt;prev;    // cur을 왼쪽으로 이동
    *pdata = plist-&gt;cur-&gt;data;    // cur이 가리키는 노드의 데이터 반환
    return TRUE;
}

int LCount(List * plist)
{
    return plist-&gt;numOfData;
}</code></pre>
<h3 id="양방향-연결-리스트-구현-3-실행-파일의-정의"><em>양방향 연결 리스트 구현 3: 실행 파일의 정의</em></h3>
<p>구현한 양방향 연결 리스트의 작동과 LPrevious 함수의 기능을 확인할 수 있는 실행 파일은 다음과 같다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &quot;DBLinkedList.h&quot;

int main()
{
    // 양방향 연결 리스트의 생성 및 초기화
    List list;
    int data;
    ListInit(&amp;list);

    // 8개의 데이터 저장
    LInsert(&amp;list, 1);
    LInsert(&amp;list, 2);
    LInsert(&amp;list, 3);
    LInsert(&amp;list, 4);
    LInsert(&amp;list, 5);
    LInsert(&amp;list, 6);
    LInsert(&amp;list, 7);
    LInsert(&amp;list, 8);

    // 저장된 데이터 조회
    if(LFirst(&amp;list, &amp;data))
    {
        printf(&quot;%d \n&quot;, data);

        // 오른쪽 노드로 이동하며 데이터 조회
        while(LNext(&amp;list, &amp;data))
            printf(&quot;%d \n&quot;, data);

        // 왼쪽 노드로 이동하며 데이터 조회
        while(LPrevious(&amp;list, &amp;data))
            printf(&quot;%d \n&quot;, data);

        printf(&quot;\n\n&quot;);
    }

    return 0;
}

&gt; gcc .\DBLinkedList.c .\DBLinkedListMain.c
&gt; .\a.exe
&gt; 출력
8 7 6 5 4 3 2 1 2 3 4 5 6 7 8 </code></pre>
<p>LPrevious 함수는 LNext 함수와 반대 방향의 노드로 이동하면서 데이터를 반환한다.
따라서 LNext 함수와 마찬가지로 더 이상 참조할 노드가 없을 땐 0을 반환하도록 구현되어 있기 때문에 while 문이 사용되었다.</p>
<h2 id="추가-더미-기반-양방향-연결-리스트">[추가] 더미 기반 양방향 연결 리스트</h2>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/9e0792bb-5a6d-42a1-987d-5582528ccabc/image.png" alt=""></p>
<p>이번에는 더미 기반의 양방향 연결 리스트를 구현할 차례다.
이 연결 리스트의 특징은 다음과 같다.</p>
<ul>
<li>양방향 연결 리스트다.</li>
<li>더미 노드가 리스트의 앞과 뒤에 각각 존재한다.</li>
<li>포인터 변수 head와 tail이 있어서 리스트의 앞과 뒤를 각각 가리킨다.</li>
</ul>
<p>이번에는 포인터 변수가 head 뿐만 아니라 tail도 있다는 점에 유의해야한다.</p>
<h3 id="더미-기반-양방향-연결-리스트-구현-1-헤더-파일의-정의"><em>더미 기반 양방향 연결 리스트 구현 1: 헤더 파일의 정의</em></h3>
<p>첫번째로 구조체와 더미 기반 양방향 연결 리스트를 구현하기 위해 필요한 함수에 대해 정리 되어있는 헤더파일을 작성해보자.</p>
<pre><code class="language-c">// DBDLinkedList.h
#ifndef __DBD_LINKED_LIST_H__
#define __DBD_LINKED_LIST_H__

#define TRUE    1
#define FALSE    0

typedef int Data;

typedef struct _node
{
    Data data;
    struct _node * next;
    struct _node * prev;
} Node;

typedef struct _dbDLinkedList
{
    Node * head;
    Node * tail;
    Node * cur;
    int numOfData;
} DBDLinkedList;

typedef DBDLinkedList List;

void ListInit(List * plist);
void LInsert(List * plist, Data data);    // 꼬리에 노드 추가

int LFirst(List * plist, Data * pdata);
int LNext(List * plist, Data * pdata);

Data LRemove(List * plist);    // 참조가 이뤄진 노드 삭제
int LCount(List * plist);

#endif</code></pre>
<h3 id="더미-기반-양방향-연결-리스트-구현-2-소스-파일의-정의"><em>더미 기반 양방향 연결 리스트 구현 2: 소스 파일의 정의</em></h3>
<h4 id="1-리스트의-초기화-2">1) 리스트의 초기화</h4>
<p>우선 그림으로 이해하고 코드르 작성해보자.
아래 그림은 리스트가 생성되고 초기화가 완료된 직후의 모습이다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/f38a4559-0d4b-471c-8dff-a5340ab18a29/image.png" alt=""></p>
<pre><code class="language-c">void ListInit(List * plist)
{
    plist-&gt;head = (Node*)malloc(sizeof(Node));
    plist-&gt;tail = (Node*)malloc(sizeof(Node));

    plist-&gt;head-&gt;prev = NULL;
    plist-&gt;head-&gt;next = plist-&gt;tail;

    plist-&gt;tail-&gt;next = NULL;
    plist-&gt;tail-&gt;prev = plist-&gt;head;

    plist-&gt;numOfData = 0;
}</code></pre>
<h4 id="2-노드의-삽입-2">2) 노드의 삽입</h4>
<p>더미 기반 양방향 연결 리스트는 리스트의 머리와 꼬리에 각각 더미 노드가 존재하기 때문에 추가의 방법에 있어서 경우의 수가 나뉘지 않는다.
그래서 데이터 2가 저장되어 있는 노드가 있는 상태에서 새 노드에 데이터 4를 추가한다고 생각했을 때를 고려하면 일련의 과정이 필요하다는 것을 알 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/052e06ad-b170-4820-9670-c2b9ad391b22/image.png" alt=""></p>
<ol>
<li>새 노드를 생성하고 데이터를 저장한다.</li>
<li>새 노드와 새 노드의 왼쪽에 위치할 노드가 서로를 가리키게 한다.</li>
<li>새 노드와 새 노드의 오른쪽에 위치할 노드가 서로를 가리키게 한다.</li>
</ol>
<p>이를 코드로 구현하면 다음과 같다.</p>
<pre><code class="language-c">void LInsert(List * plist, Data data) 
{
    // 1 단계
    Node * newNode = (Node*)malloc(sizeof(Node));
    newNode-&gt;data = data;

    // 2 단계
    newNode-&gt;prev = plist-&gt;tail-&gt;prev;
    plist-&gt;tail-&gt;prev-&gt;next = newNode;

    // 3 단계
    newNode-&gt;next = plist-&gt;tail;
    plist-&gt;tail-&gt;prev = newNode;

    (plist-&gt;numOfData)++;
}</code></pre>
<h4 id="3-데이터의-조회">3) 데이터의 조회</h4>
<p>더미 기반 양방향 연결 리스트 데이터 조회 함수는 LFirst와 LNext만으로 이루어져 있다.
조회는 비교적 단순하기 때문에 코드를 바로 보자.</p>
<pre><code class="language-c">int LFirst(List * plist, Data * pdata)
{
    if(plist-&gt;head-&gt;next == plist-&gt;tail)
        return FALSE;

    plist-&gt;cur = plist-&gt;head-&gt;next;
    *pdata = plist-&gt;cur-&gt;data;
    return TRUE;
}

int LNext(List * plist, Data * pdata)
{
    if(plist-&gt;cur-&gt;next == plist-&gt;tail)
        return FALSE;

    plist-&gt;cur = plist-&gt;cur-&gt;next;
    *pdata = plist-&gt;cur-&gt;data;
    return TRUE;
}</code></pre>
<h4 id="4-데이터의-삭제">4) 데이터의 삭제</h4>
<p>삭제를 구현하기 위해서는 아래 그림을 예시로 2가 저장된 노드를 삭제한다고 가정하고 살펴보자.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/63012835-3bc9-495d-a0f0-06f588be20ba/image.png" alt=""></p>
<p>①화살표와 ④화살표는 삭제를 위해서 다른 위치를 가리켜야 하는 화살표다.
②화살표와 ③화살표는 삭제할 노드에서 나오는 포인터 변수이기 때문에 신경쓰지 않아도 된다.
①화살표를 삭제될 노드의 다음 노드에, ④화살표를 삭제될 노드의 이전 노드에 연결하기만 하면 된다.
삭제된 이후의 모습은 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/2dbac75e-7cef-4ed4-a36f-189c72d10cb2/image.png" alt=""></p>
<p>이를 코드로 구현하면 다음과 같다.</p>
<pre><code class="language-c">Data LRemove(List * plist)
{
    Node * rpos = plist-&gt;cur;
    Data remv = rpos-&gt;data;

    plist-&gt;cur-&gt;prev-&gt;next = plist-&gt;cur-&gt;next;    // 1번 화살표 가리키는 곳 변경
    plist-&gt;cur-&gt;next-&gt;prev = plist-&gt;cur-&gt;prev;    // 4번 화살표 가리키는 곳 변경

    plist-&gt;cur = plist-&gt;cur-&gt;prev;    // cur 위치를 재조정

    free(rpos);
    (plist-&gt;numOfData)--;
    return remv;
}</code></pre>
<h4 id="5-정리하기-1">5) 정리하기</h4>
<p>위에서 설명한 소스코드를 하나로 정리하면 다음과 같다.</p>
<pre><code class="language-c">// DBDLinkedList.c
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;DBDLinkedList.h&quot;

void ListInit(List * plist)
{
    plist-&gt;head = (Node*)malloc(sizeof(Node));
    plist-&gt;tail = (Node*)malloc(sizeof(Node));

    plist-&gt;head-&gt;prev = NULL;
    plist-&gt;head-&gt;next = plist-&gt;tail;

    plist-&gt;tail-&gt;next = NULL;
    plist-&gt;tail-&gt;prev = plist-&gt;head;

    plist-&gt;numOfData = 0;
}

void LInsert(List * plist, Data data) 
{
    Node * newNode = (Node*)malloc(sizeof(Node));
    newNode-&gt;data = data;

    newNode-&gt;prev = plist-&gt;tail-&gt;prev;
    plist-&gt;tail-&gt;prev-&gt;next = newNode;

    newNode-&gt;next = plist-&gt;tail;
    plist-&gt;tail-&gt;prev = newNode;

    (plist-&gt;numOfData)++;
}

int LFirst(List * plist, Data * pdata)
{
    if(plist-&gt;head-&gt;next == plist-&gt;tail)
        return FALSE;

    plist-&gt;cur = plist-&gt;head-&gt;next;
    *pdata = plist-&gt;cur-&gt;data;
    return TRUE;
}

int LNext(List * plist, Data * pdata)
{
    if(plist-&gt;cur-&gt;next == plist-&gt;tail)
        return FALSE;

    plist-&gt;cur = plist-&gt;cur-&gt;next;
    *pdata = plist-&gt;cur-&gt;data;
    return TRUE;
}

Data LRemove(List * plist)
{
    Node * rpos = plist-&gt;cur;
    Data remv = rpos-&gt;data;

    plist-&gt;cur-&gt;prev-&gt;next = plist-&gt;cur-&gt;next;
    plist-&gt;cur-&gt;next-&gt;prev = plist-&gt;cur-&gt;prev;

    plist-&gt;cur = plist-&gt;cur-&gt;prev;

    free(rpos);
    (plist-&gt;numOfData)--;
    return remv;
}

int LCount(List * plist)
{
    return plist-&gt;numOfData;
}</code></pre>
<h3 id="더미-기반-양방향-연결-리스트-구현-3-실행-파일의-정의"><em>더미 기반 양방향 연결 리스트 구현 3: 실행 파일의 정의</em></h3>
<p>위에서 구현한 헤더 파일과 소스 파일을 확인할 수 있는 실행 파일은 다음과 같다.</p>
<pre><code class="language-c">// DBDLinkedListMain.c
#include &lt;stdio.h&gt;
#include &quot;DBDLinkedList.h&quot;

int main(void)
{
    List list;
    int data;
    ListInit(&amp;list);

    // 8개의 데이터 저장
    LInsert(&amp;list, 1);
    LInsert(&amp;list, 2);
    LInsert(&amp;list, 3);
    LInsert(&amp;list, 4);
    LInsert(&amp;list, 5);
    LInsert(&amp;list, 6);
    LInsert(&amp;list, 7);
    LInsert(&amp;list, 8);

    // 저장된 데이터의 조회회
    if(LFirst(&amp;list, &amp;data))
    {
        printf(&quot;%d &quot;, data);

        while(LNext(&amp;list, &amp;data)) 
            printf(&quot;%d &quot;, data);

        printf(&quot;\n&quot;);
    }

    // 2의 배수 전부 삭제
    if(LFirst(&amp;list, &amp;data))
    {
        if(data%2 == 0)
            LRemove(&amp;list);

        while(LNext(&amp;list, &amp;data)) 
        {        
            if(data%2 == 0)
                LRemove(&amp;list);
        }
    }

    // 저장된 데이터 재조회
    if(LFirst(&amp;list, &amp;data))
    {
        printf(&quot;%d &quot;, data);

        while(LNext(&amp;list, &amp;data)) 
            printf(&quot;%d &quot;, data);

        printf(&quot;\n\n&quot;);
    }

    return 0;
}

&gt; gcc .\DBDLinkedList.c .\DBDLinkedListMain.c
&gt; .\a.exe
&gt; 출력
1 2 3 4 5 6 7 8 
1 3 5 7</code></pre>
<p>꼬리에 노드를 추가하기 때문에 1부터 8까지 데이터다 오름차순으로 잘 연결된 것을 확인할 수 있고
2의 배수들만 삭제된 것을 알 수 있다.</p>
<hr>
<h2 id="review"><code>&lt;Review&gt;</code></h2>
<p>우선 이번 Chapter에 대한 리뷰를 먼저 하자면
python으로 구현할 때 보다 한번 더 해서 그런지 머리속으로 생각하긴 어렵지 않았다.
(앞으로도 c언어로 공부하는 자료구조의 리뷰 서론은 항상 이 말일 것이다.)</p>
<p>다만 구현된 예제들을 바탕으로 새로운 유형의 연결 리스트를 구현해보라 했을 때 선뜻 코드로 작성하는 것이 익숙하지 않는다...
코드를 계속 따라 쳐도 이를 아직 익숙하지 않는 것은 더 공부하고 많이 코드를 쳐보라는 것인가?!
앞으로도 더 정진해야겠다~!</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/c3db5832-6fe8-47d4-b22e-5eebee97bb2a/image.png" alt=""></p>
<p>추가로 사담...
이번주엔 일이 정말 많았다...
아버지 사무실 일도 도와드리면서 SSAFY도 준비하면서
친구 결혼식 축가도 준비하면서
2onC라는 활동하고 있는 댄스팀의 새로운 비디오 촬영도 준비하면서 ㅋㅋㅋㅋㅋ
당장 19시간 뒤에 C언어 스터디가 있는데 아직 Chapter 하나를 더 해야한다 후...
그래도 해야지!!!</p>
<p>이렇게나마 덕분에 코딩을 손에 놓지 않고 할 수 있게 된거 같다.
잘했다 과거의 나자신!</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/bfb9a771-9c38-4be5-9461-06d22fa63a11/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chapter 04. 연결 리스트(Linked List) 2]]></title>
            <link>https://velog.io/@mingming_eee/datastructure-04</link>
            <guid>https://velog.io/@mingming_eee/datastructure-04</guid>
            <pubDate>Thu, 17 Oct 2024 06:44:41 GMT</pubDate>
            <description><![CDATA[<p>Chapter 03에서 배운 것을 다음과 같이 세 가지로 정리할 수 있다.</p>
<ol>
<li>추상 자료형에 대한 이해</li>
<li>리스트 자료구조의 특성과 활용</li>
<li>리스트 자료구조의 배열 기반 구현</li>
</ol>
<p>Chapter 04에서는 &#39;연결&#39;을 기반으로 하는 다른 르시트의 구현방법에 대해 배울 것이다.</p>
<h2 id="04-1-연결-리스트의-개념적인-이해">04-1. 연결 리스트의 개념적인 이해</h2>
<h3 id="linked-list"><em>Linked List</em></h3>
<p>연결 기반의 리스트를 간단하게 <code>연결 리스트</code>라 한다.
연결 리스트의 구현을 이해하기 위해서는 <code>malloc 함수와 free 함수를 기반으로 하는 메모리의 동적 할당</code>에 대해 이해를 하고 있어야한다.
다음 예제를 통해 연결 리스트에서의 <code>연결</code>이 의미하는 것을 알아보자.</p>
<pre><code class="language-c">// ArrayRead.c
#include &lt;stdio.h&gt;

int main()
{
    int arr[10];
    int readCount = 0;
    int readData;
    int i;

    while(1)
    {
        printf(&quot;자연수 입력: &quot;);
        scanf(&quot;%d&quot;, &amp;readData);
        if(readData &lt; 1)
            break;

        arr[readCount++] = readData;
    }

    for(i=0; i&lt;readCount; i++)
        printf(&quot;%d &quot;, arr[i]);

    return 0;
}

&gt; 출력
자연수 입력: 1
자연수 입력: 2
자연수 입력: 3
자연수 입력: 4
자연수 입력: 5
자연수 입력: 0
1 2 3 4 5 </code></pre>
<p>위 예제는 0 이하의 값을 입력하기 전까지 입력이 계속되는 간단한 예제이다.
여기서 우린 배열의 단점을 볼 수 있다.
<strong>배열은 메모리의 특성이 정적이어서 메모리의 길이를 변경하는 것이 불가능하다.</strong>
따라서 배열 길이 10을 넘어서도 0보다 큰 자연수 값을 입력하게 되면 문제가 발생하게 된다.
이렇든 특성이 정적인 배열은 필요로 하는 메모리의 크기에 유연하게 대처하지 못한다.
그래서 등장한 것이 <strong>&#39;동적인 메모리의 구성`</strong>이다.
동적인 메모리의 구성에 대해서 다음 예제를 통해 이해해보자.
(분석이 쉽지는 않을 것이다...!)</p>
<pre><code class="language-c">// LinkedRead.c
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;

typedef struct _node
{
    int data;
    struct _node* next;
} Node;

int main()
{
    Node * head = NULL;
    Node * tail = NULL;
    Node * cur = NULL;

    Node * newNode = NULL;
    int readData;

    // 데이터를 입력 받는 과정
    while(1)
    {
        printf(&quot;자연수 입력: &quot;);
        scanf(&quot;%d&quot;, &amp;readData);
        if(readData&lt;1)
            break;

        // 노드 추가 과정
        newNode = (Node *)malloc(sizeof(Node));
        newNode-&gt;data = readData;
        newNode-&gt;next = NULL;

        if(head == NULL)
            head = newNode;
        else
            tail-&gt;next = newNode;

        tail = newNode;
    }
    printf(&quot;\n&quot;);

    // 입력 받은 데이터의 출력 과정
    printf(&quot;입력 받은 데이터의 전체 출력! \n&quot;);
    if(head == NULL)
        printf(&quot;저장된 자연수가 없습니다.&quot;);
    else
    {
        cur = head;
        printf(&quot;%d &quot;, cur-&gt;data);

        while(cur-&gt;next != NULL)
        {
            cur = cur-&gt;next;
            printf(&quot;%d &quot;, cur-&gt;data);
        }
    }
    printf(&quot;\n\n&quot;);

    // 메모리 해체 과정
    if(head == NULL)
        return 0;       // 해제할 노드가 없음.
    else
    {
        Node * delNode = head;
        Node * delNextNode = head-&gt;next;

        printf(&quot;%d을(를) 삭제합니다. \n&quot;, head-&gt;data);
        free(delNode);  // 첫 번째 노드 삭제.

        while(delNextNode != NULL)  // 두 번째 이후 노드 삭제.
        {
            delNode = delNextNode;
            delNextNode = delNextNode-&gt;next;

            printf(&quot;%d을(를) 삭제합니다. \n&quot;, delNode-&gt;data);
            free(delNode);
        }
    }
    return 0;
}

&gt; 출력
자연수 입력: 2
자연수 입력: 4
자연수 입력: 6
자연수 입력: 8
자연수 입력: 1
자연수 입력: 3
자연수 입력: 7
자연수 입력: 0

입력 받은 데이터의 전체 출력!
2 4 6 8 1 3 7

2을(를) 삭제합니다.
4을(를) 삭제합니다.
6을(를) 삭제합니다.
8을(를) 삭제합니다.
1을(를) 삭제합니다.
3을(를) 삭제합니다.
7을(를) 삭제합니다.</code></pre>
<p>위 예제를 이해하기 위해 먼저 정의된 구조체를 살펴보자.</p>
<pre><code class="language-c">typedef struct _node
{
    int data;                // 데이터를 담을 공간
    struct _node* next;        // 연결 도구
} Node;</code></pre>
<p>위 구조체의 멤버 next는 Node형 구조체 변수의 주소 값을 저장할 수 있는 포인터 변수이다.
위 구조체 변수를 바구니에 비교하자면 구조체의 첫 번째 멤버 data에 값을 저장할 수 있는 것이고 바구니와 바구니를 연결하는 것은 next인 것이다.
이 next 멤버 덕분에 모든 Node형 구조체 변수는 다른 Node형 구조체 변수를 가리킬 수 있게 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/1ba41c92-43df-4898-a334-7b6e9bef936e/image.png" alt=""></p>
<p>따라서 Node형 구조체를 대상으로 필요할 때마다 구조체 변수를 마련해서 그곳에 데이터를 저장하고 이들을 배열처럼 서로 연결한다는 것이다!
<strong>즉, 필요할 때마다 바구니의 역할을 하는 구조체 변수를 하나씩 동적 할당(malloc함수)해서 이들을 연결한다는 것이다.</strong>
이것이 연결 리스트의 기본 원리다.
데이터를 저장하는 곳과 다른 변수를 가리키기 위한 곳을 구분한 것을 아래 그림과 같이 나타낼 수 있따.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/f3cbd3f0-e25b-418b-89a5-2b0d8a5381c5/image.png" alt=""></p>
<h3 id="연결-리스트에서의-데이터-삽입"><em>연결 리스트에서의 데이터 삽입</em></h3>
<p>위 예제 LinkedRead.c는 연결 리스트의 구현 결과를 나타낸 것이고 이것에 대해 더 자세히 알아보자.</p>
<p>main함수에서 등장하는 포인터 변수의 선언에 대해 알아보자.</p>
<pre><code class="language-c">Node * head = NULL;        // 리스트의 머리를 가리키는 포인터 변수
Node * tail = NULL;        // 리스트의 꼬리를 가리키는 포인터 변수
Node * cur = NULL;        // 저장된 데이터의 조회에 사용되는 포인터 변수</code></pre>
<p>이 변수 세개는 연결 리스트에서 주요한 역할을 하는 포인터 변수들이다.
head와 tail은 연결을 추가 및 유지하기 위한 것이고, cur은 참조 및 조회를 위한 것이다.
다음 그림들을 통해 이 변수들이 어떻게 작동되는지 자세히 알아보자.</p>
<table>
<thead>
<tr>
<th>NO.</th>
<th>Status</th>
<th>Content</th>
</tr>
</thead>
<tbody><tr>
<td>0</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/6746bba2-5908-4a03-a797-f46c120d8251/image.png" alt=""></td>
<td>연결 리스트의 초기상태.<br>구조체 Node의 포인터 변수 head와 tail이 NULL 가리키고 있는 상황.<br><br>[코드]<br><code>Node * head = NULL;</code><br><code>Node * tail = NULL;</code></td>
</tr>
<tr>
<td>1</td>
<td>-</td>
<td>&lt;첫 번째 노드 추가하는 과정.&gt;</td>
</tr>
<tr>
<td>1-1</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/91d31122-0f77-415f-a650-4d24d1abf797/image.png" alt=""></td>
<td>while 반복문이 처음 실행된 상황이며<br>newNode(새 노드, 바구니 생성)에 입력값 2가 저장되고<br>newNode의 data부분에 2가 저장.<br>newNode의 next부분은 Null로 저장.<br><br>[코드]<br><code>newNode = (Node *)malloc(sizeof(Node));</code><br><code>newNode-&gt;data = readData;</code><br><code>newNode-&gt;next = NULL;</code></td>
</tr>
<tr>
<td>1-2</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/0b917ba5-d3a0-436c-af28-1fdb8158c7ba/image.png" alt=""></td>
<td>첫 번째 노드이기 때문에 head가 newNode를 가리키게 함.<br><br>[코드]<br><code>if(head == NULL){head = newNode;}</code></td>
</tr>
<tr>
<td>1-3</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/7b43eed8-a837-414e-bdb7-a928f9ff4588/image.png" alt=""></td>
<td>첫 번째 노드이기 때문에 tail도 newNode를 가리키게 됨.<br><br>[코드]<br><code>tail = newNode;</code><br><br>+) NULL을 사선으로 표현</td>
</tr>
<tr>
<td>2</td>
<td>-</td>
<td>&lt;두 번째 이후의 노드를 추가하는 과정.&gt;</td>
</tr>
<tr>
<td>2-1</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/932e7e81-fcd5-4cd6-965a-5b774a2e0955/image.png" alt=""></td>
<td>tail이 가리키는 노드의 뒤에 연결해야 함.<br>새 노드의 생성 및 초기화 이후 else 구문에 담긴 문장 실행.<br>tail의 next에 새로운 Node인 newNode 연결.<br><br>[코드]<br><code>tail-&gt;next = newNode;</code></td>
</tr>
<tr>
<td>2-2</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/51d44ae3-4b62-42b2-b251-d2c8d038c20e/image.png" alt=""></td>
<td>이 후에도 동일한 방법으로 newNode를 새롭게 할당하고<br>tail의 next 부분을 새 노드인 newNode에 연결.</td>
</tr>
</tbody></table>
<h3 id="연결-리스트에서의-데이터-조회"><em>연결 리스트에서의 데이터 조회</em></h3>
<p>삽입만큼이나 이해하기 쉬운 것이 조회다.
조회는 cur라는 포인터 변수를 사용할 것이다.</p>
<table>
<thead>
<tr>
<th>No.</th>
<th>Status</th>
<th>Content</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/e57a6a82-d152-4ea3-a427-3c5c03894a0f/image.png" alt=""></td>
<td>조회부분 코드에서 else 부분 중 <code>cur = head;</code> 코드 실행되면 연결 리스트 중 첫 번째 노드 가리킴.<br>따라서, cur를 이용한 첫 번째 데이터 출력 가능.</td>
</tr>
<tr>
<td>2</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/3929a105-f540-4975-9187-106f378d99ee/image.png" alt=""></td>
<td>else 구문 안에 있는 while문 작동하면서 <code>cur = cur-&gt;next;</code> 코드를 통해 그림과 같이 모든 노드를 가리키며 이동 가능.</td>
</tr>
</tbody></table>
<h3 id="연결-리스트에서의-데이터-삭제"><em>연결 리스트에서의 데이터 삭제</em></h3>
<p>삭제 코드는 삽입이나 조회보다는 살짝 어려울 순 있다. 하지만 찬찬히 살펴보도록 하자.</p>
<table>
<thead>
<tr>
<th>No.</th>
<th>Status</th>
<th>Content</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/6125435e-ce34-4f02-a838-c4db4ce6a85a/image.png" alt=""></td>
<td>head가 가리키는 노드의 삭제를 위해<br><code>Node * delNode = head;</code><br><code>Node * delNextNode = head-&gt;next;</code><br>위와 같이 포인터 변수 두 개를 추가로 선언했다.<br>delNode(dN)는 삭제할 노드를 가리키고, delNextNode(dNN)은 삭제될 노드가 가리키는 다음 노드의 주소값을 저장.<br>둘을 구분지어 놓는 이유는 삭제될 노드가 가리키는 다음 노드의 주소 값을 별도로 저장되지 않을 경우<br>다음 노드와 삭제된 노드 앞 노드와의 연결이 끊어지기 때문.</td>
</tr>
<tr>
<td>2</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/e99af690-fb49-4eb2-b6fd-4acb2be9a124/image.png" alt=""></td>
<td><code>free(delNode);</code> 코드를 통해 첫 번째 노드를 삭제.<br>(head가 다음 노드를 가리키는 것이 원칙이나 삭제 과정만을 보여주기 위해 생략됨.)<br><code>delNode = delNextNode;</code><br><code>delNextNode = delNextNode-&gt;next;</code><br>위 코드를 통해 오른쪽으로 노드 가리킴 이동.</td>
</tr>
<tr>
<td>3</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/092587e2-c126-4ba2-8520-45afadf269a0/image.png" alt=""></td>
<td>이동한 delNode과 free 함수의 호출을 통해 마지막 노드를 소멸할 때까지 계속 진행.</td>
</tr>
</tbody></table>
<h3 id="정리하기"><em>정리하기</em></h3>
<p>LinkedRead.c를 통해 연결리스트를 100% 구현하고 이해했다고 말할 수는 없다.
그 이유는 대표적으로 두 가지가 있다.</p>
<ol>
<li>연결 리스트의 ADT를 정의하지 않았다.</li>
<li>삽입, 삭제, 조회의 기능이 별도의 함수로 구분되어 있지 않았다.</li>
</ol>
<p>연결리스트와 관련된 코드를 모조리 main 함수에 넣은거기 때문에 필요할 때마다 가져다 쓸 수 없는 것이다.
자료구조는 다음 세 가지 순서를 지키는 것이 자료구조를 이해하고 구현하는데 있어 중요하다.</p>
<ol>
<li>자료구조의 ADT 정의</li>
<li>정의한 ADT 구현</li>
<li>구현이 완료된 자료구조의 활용</li>
</ol>
<hr>
<h2 id="04-2-단순-연결-리스트의-adt와-구현">04-2. 단순 연결 리스트의 ADT와 구현</h2>
<p>처음으로 배울 연결 리스트는 연결의 형태가 한쪽 방향으로 전개되고 시작과 끝이 분명히 존재하는 <code>단순 연결 리스트</code>다.</p>
<h3 id="정렬-기능이-추가된-연결-리스트-1-adt-정의"><em>정렬 기능이 추가된 연결 리스트 1: ADT 정의</em></h3>
<p>기능적으로 무엇인가를 변경하거나 추가하지 않는다면 ADT를 변경할 이유는 없다.
그래서 Chapter 03에서 정의한 리스트 ADT를 그대로 적용해도 되지만 연결 리스트 정렬 관련 기능을 추가하기 위해서 해당 부분 ADT 정의를 추가해보려한다.</p>
<p>✅정렬 기능이 추가된 리스트 자료구조의 ADT</p>
<ul>
<li><p>void ListInit(List * plist);</p>
<ul>
<li>초기화할 리스트의 주소 값을 인자로 전달.</li>
<li>리스트 생성 후 제일 먼저 호출되어야 하는 함수.</li>
</ul>
</li>
<li><p>void LInsert(List * plist, LData data);</p>
<ul>
<li>리스트에 데이터를 저장. 매개변수 data에 전달된 값을 저장.</li>
</ul>
</li>
<li><p>int LFirst(List * plist, LData * pdata);</p>
<ul>
<li>첫 번째 데이터가 pdata가 가리키는 메모리에 저장.</li>
<li>데이터의 참조를 위한 초기화 진행.</li>
<li>참조 성공 시 TRUE(1), 실패 시 FALSE(0) 반환.</li>
</ul>
</li>
<li><p>int LNext(List * plist, LData * pdata);</p>
<ul>
<li>참조된 데이터의 다음 데이터가 pdata가 가리키는 메모리에 저장.</li>
<li>순차적인 참조를 위해 반복 호출 가능.</li>
<li>참조를 새로 시작하려면 먼저 LFirst 함수 호출 필요.</li>
<li>참조 성공 시 TRUE(1), 실패 시 FALSE(0) 반환.</li>
</ul>
</li>
<li><p>LData LRemove(List * plist);</p>
<ul>
<li>LFirst 또는 LNext 함수의 마지막 반환 데이터를 삭제.</li>
<li>삭제된 데이터 반환.</li>
<li>마지막 반환 데이터를 삭제하므로 연이은 반복 호출 허용 안함.</li>
</ul>
</li>
<li><p>int LCount(List * plist);</p>
<ul>
<li>리스트에 저장되어 있는 데이터의 수 반환.</li>
</ul>
</li>
<li><p>void SetSortRule(List * plist, int (<em>comp)(LData d1, LData d2));  **[*신규</em> ✨]**</p>
<ul>
<li>리스트에 정렬의 기준이 되는 함수를 등록.</li>
</ul>
</li>
</ul>
<p>여기서 우리는 새 노드를 추가할 때 리스트의 머리와 꼬리 중 어디에 저장할 것인지 정해야한다.
두 가지 방법 모두 장단점이 있다.
머리에 추가할 경우의 장단점은 다음과 같다.</p>
<ul>
<li>장점 : 포인터 변수 tail이 불필요.</li>
<li>단점 : 저장된 순서를 유지하지 않음.</li>
</ul>
<p>꼬리에 추가할 경우의 장단점은 다음과 같다.</p>
<ul>
<li>장점 : 저장된 순서가 유지.</li>
<li>단점 : 포인터 변수 tail이 필요.</li>
</ul>
<p>단순 연결 리스트의 구현을 배울 땐 머리에 추가하는 경우가 선호된다.
그 이유는 포인터 변수 tail을 유지하기 위해서 넣어야할 부가적인 코드가 번거롭게 느껴질 수 있고, 리스트 자료구조는 저장된 순서를 유지해야하는 자료구조가 아니기 때문이다.</p>
<p>다시 돌아와서 연결 리스트이 정렬기준을 지정하기 위해 추가된 ADT는 SetSortRule함수이다.
SetSortRule 함수의 선언 및 정의를 이해하기 위해서는 &#39;함수 포인터&#39;에 대한 이해가 필요하다.
정렬의 기준이란 것이 정수를 대상으로 보면 수의 크기가 전부이지만, 경우에 따라서는 알파벳의 순서나 이름과 같은 문자열 길이의 길고 짧음이 대상이 될 수도 있다.
함수의 두 번째 매개변수 선언을 보면 반환형이 int고 LData형 인자 두 개 전달받는 함수의 주소 값을 두 번째 인자로 전달받는다.
따라서 다음과 같이 정의된 함수의 주소 값이 SetSortRule 함수의 두 번째 인자로 전달 가능한 경우이다.</p>
<pre><code class="language-c">int WhoIsPrecede(LData d1, LData d2)    // typedef int LData;
{
    if(d1 &lt; d2)
        return 0;    // d1이 정렬 순서상 앞선다.
    else
        return 1;    // d2가 정렬 순서상 앞서거나 같다.
}</code></pre>
<p>그리고 SetSortRule의 두 번째 인자로 전달되는 함수는 위 함수의 정의에서 보이듯이 
매개변수 d1에 전달되는 인자가 정렬 순서상 앞서서 head에 더 가까워야하는 경우 0을 반환하고,
매개변수 d2에 전달되는 인자가 정렬 순서상 앞서거나 같은 경우에는 1을 반환한다.
이렇든 반환 값이 어떻게 되고 그것이 어떤 의미를 갖는지는 연결 리스트를 구현하는 사람이 결정하는 부분이다. 
이렇게 반환되는 값을 가지고 SetSortRule함수가 어떻게 활용되고 연결 리스트 내부적으로 어떤 의미를 지니는지 천천히 알아가보자.</p>
<h3 id="-더미-노드dummy-node-기반의-단순-연결-리스트"><em>+) 더미 노드(Dummy Node) 기반의 단순 연결 리스트</em></h3>
<p>LinkedRead.c 예제에서는 첫 노드의 추가, 삭제 및 조회하는 방법이 두 번째 노드 이후의 추가, 삭제 및 조회하는 방법에 차이가 있다.
이러한 차이없이 일관된 형태로 구성하기 위해 더미 노드(Dummy Node)를 사용하게 된다.
우선 포인터 변수 tail이 사라지고 더미 노드가 추가되어 왼쪽 그림에서 오른쪽 그림과 같이 연결 리스트 형태를 생각해 볼 수 있다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/mingming_eee/post/51d44ae3-4b62-42b2-b251-d2c8d038c20e/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/mingming_eee/post/0de35b4c-b3ed-421b-9e69-beabe0bb059b/image.png" alt=""></th>
</tr>
</thead>
</table>
<p>노드를 머리에서부터 채우기로 했기 때문에 tail 포인터 변수가 필요 없는 것이고, 더미 노드라는 빈 노드를 미리 넣어두어 처음 추가되는 노드가 구조상 두 번째 노드가 되어 노드 추가, 삭제 및 조회의 과정을 일관된 형태로 만들어 준다.</p>
<h3 id="정렬-기능이-추가된-연결-리스트-2-구조체와-헤더파일의-정의"><em>정렬 기능이 추가된 연결 리스트 2: 구조체와 헤더파일의 정의</em></h3>
<p>정렬 기능이 추가된 연결 리스트의 ADT를 정의했다면 연결 리스트의 구조체와 헤더파일을 정의해보자.
노드는 초반에 설정했던 Node 구조체와 동일하다.</p>
<pre><code class="language-c">typedef struct _node        // typedef int LData
{
    LData data;
    struct _node * next;
} Node;</code></pre>
<p>그 다음 연결 리스트의 구현에 필요한 다음 두 포인터 변수들을 별도의 구조체로 묶지 않고 그냥 main 함수의 지역변수로 선언하거나 나쁜 방법으로는 전역변수로 선언하기도 한다.
전역변수로 선언하는 것은 나쁜 습관이다. 
프로그램을 구현하는데 있어서 리스트 자료구조가 하나만 사용되지 않고 배열도 하나가 아닌 다수의 배열이 필요하다. 
따라서 다수의 리스트가 필요한 상황에서 head와 cur과 같은 포인터 변수를 전역 변수로 선언하게 된다면 아래와 같이 리스트의 세트를 필요할 때마다 만들어서 사용해야하기 때문에 불편하다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
Node * headOne, * curOne;        // 리스트 한 세트
....
Node * headTwo, * curTwo;        // 리스트 두 세트
....
Node * headThree, * curThree;    // 리스트 세 세트
....

int main()
{
    ....
    return 0;
}</code></pre>
<p>따라서 head와 cur과 같은 포인터 변수를 묶어서 다음과 같이 연결 리스트를 의미하는 구조체를 별도로 정의해야 한다.</p>
<pre><code class="language-c">typedef struct _linkedList
{
    Node * head;                        // 더미 노드를 가리키는 멤버
    Node * cur;                            // 참조 및 삭제를 돕는 멤버
    Node * before;                        // 삭제를 돕는 멤버
    int numOfData;                        // 저장된 데이터의 수를 기록하기 위한 멤버
    int (*comp)(LData d1, LData d2);    // 정렬의 기준을 등록하기 위한 멤버
} LinkedList;</code></pre>
<p>위 구조체는 Chapter 03에서 정의한 ArrayList와 그 성격이 동일하다.
ArrayList는 배열 기반 리스트라면, LinkedList는 연결 기반 리스트다.
준비는 다 끝났다!
이제 더미 노드 기반의 정렬 삽입도 되고, 정렬 삽입의 기준도 바꿀 수 있는 연결 리스트를 위한 헤더파일을 구현해보자.</p>
<pre><code class="language-c">// DLinkedList.h
#ifndef __D_LINKED_LIST_H__
#define __D_LINKED_LIST_H__

#define TRUE        1
#define FALSE       0

typedef int LData;

typedef struct _node
{
    LData data;
    struct _node* next;
} Node;

typedef struct _linkedList
{
    Node * head;                        
    Node * cur;                            
    Node * before;                        
    int numOfData;                        
    int (*comp)(LData d1, LData d2);
} LinkedList;

typedef LinkedList List;

void ListInit(List * plist);
void LInsert(List * plist, LData data);

int LFirst(List * plist, LData * pdata);
int LNext(List * plist, LData * pdata);

LData LRemove(List * plist);
int LCount(List * plist);

void SetSortRule(List * plist, int (*comp)(LData d1, LData d2));

#endif</code></pre>
<p>위 헤더파일에 선언된 함수는 Chapter 03의 배열 기반 리스트의 헤더파일과 LinkedList 구조체 정의, SetSortRule 함수의 선언이 추가되었다는 것 말고는 거의 비슷하다.</p>
<h3 id="정렬-기능이-추가된-연결-리스트-3-더미-노드dummy-node-기반의-단순-연결-리스트-구현"><em>정렬 기능이 추가된 연결 리스트 3: 더미 노드(Dummy Node) 기반의 단순 연결 리스트 구현</em></h3>
<p>다음은 구현한 헤더파일을 바탕으로 더미 노드 기반의 단순 연결 리스트 함수들을 구현할 것이다.</p>
<h4 id="1-리스트-초기화">1) 리스트 초기화</h4>
<p>우리는 위에서 LinkedList라는 구조체를 선언했다.</p>
<pre><code class="language-c">typedef struct _linkedList
{
    Node * head;                        
    Node * cur;                            
    Node * before;                        
    int numOfData;                        
    int (*comp)(LData d1, LData d2);
} LinkedList;</code></pre>
<p>위 구조체의 변수가 선언되면 이를 대상으로 초기화를 진행해야 하는데, 이때 호출되는 함수는 다음과 같다.</p>
<pre><code class="language-c">void ListInit(List * plist)
{
    plist-&gt;head = (Node *)malloc(sizeof(Node));        // 더미 노드 생성
    plist-&gt;head-&gt;next = NULL;
    plist-&gt;comp = NULL;
    plist-&gt;numOfData = 0;
}</code></pre>
<p>이 코드를 그림으로 나타내면 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/3eb372ca-262c-4f75-9da4-b0b93d6b2a7f/image.png" alt=""></p>
<p>여기서 중요한 점은 더미 노드가 존재한다는 것이다.
그리고 그림에는 표현되지 않았지만 리스트의 멤버 comp이 NULL로, 멤버 numOfData가 0으로 초기화된 점도 기억하자.</p>
<h4 id="2-노드-삽입">2) 노드 삽입</h4>
<p>리스트 초기화 이후 노드를 추가하기 위해 호출되는 함수는 다음과 같다.</p>
<pre><code class="language-c">void LInsert(List * plist, LData data)
{
    if(plist-&gt;comp == NULL)        // 정렬 기준이 마련되지 않았다면
        FInsert(plist, data);    // 머리에 노드 추가
    else                        // 정렬 기준이 마련됐다면
        SInsert(plist, data);    // 정렬기준에 근거하여 노드 추가
}</code></pre>
<p>위 함수에서 볼 수 있듯 노드의 추가는 리스트의 멤버 comp에 어떤 것이 저장되어 있느냐에 따라 FInsert 또는 SInsert 함수를 통해 진행된다.
이 두 함수들도 마저 정의해보자.</p>
<p>우선, comp가 NULL일 때 호출되는 FInsert 함수이다.</p>
<pre><code class="language-c">void FInsert(List * plist, LData data)
{
    Node * newNode = (Node *)malloc(sizeof(Node));    // 새 노드 생성
    newNode-&gt;data = data;                            // 새 노드에 데이터 저장

    newNode-&gt;next = plist-&gt;head-&gt;next;                // 새 노드가 다른 노드를 가리키게 함
    plist-&gt;head-&gt;next = newNode;                    // 더미 노드가 새 노드를 가리키게 함

    (plist-&gt;numOfData)++;                            // 저장된 노드의 수 +1
}</code></pre>
<p>위 함수는 포인터 변수 head가 가리키고 있는 더미 노드에서 새 노드로 재지정하는 과정을 담고 있다.
if...else 구문이 없어 모든 노드의 추가과정이 일관된다는 것을 보여주고 이것이 바로 더미 노드가 주는 이점이다.
이해를 돕기 위한 예시로 리스트에 이미 4와 6이 들어있다.
그리고 새 노드의 값으로 2를 삽입하려고 한다.
첫 두줄의 코드가 실행되며 새 노드가 추가되어 아래 그림과 같은 형태가 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/30325311-a6f6-49a9-84b8-449ec27d2ce4/image.png" alt=""></p>
<p>그 다음 두줄의 코드가 실행되어 아래 그림과 같이 기존 리스트에 새 노드를 넣게 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/6b52f199-f737-4772-879d-ce37cd761232/image.png" alt=""></p>
<p>그 다음으로 comp가 NULL이 아닐 때 호출되는 SInsert 함수에 대해 알아볼 차례다.
이 함수는 SetSortRule 함수까지 학습하고 난 다음 설명을 이해가겠다.
(<a href="https://velog.io/@mingming_eee/datastructure-04#%EC%97%B0%EA%B2%B0-%EB%A6%AC%EC%8A%A4%ED%8A%B8%EC%97%90%EC%84%9C%EC%9D%98-%EC%A0%95%EB%A0%AC%EA%B8%B0%EC%A4%80-%EC%84%A4%EC%A0%95%EA%B3%BC-%EA%B4%80%EB%A0%A8%EB%90%9C-%EB%B6%80%EB%B6%84">그래도 궁금한 사람은 여기로 👉</a>)</p>
<h4 id="3-데이터-조회">3) 데이터 조회</h4>
<p>조회 관련한 함수는 LFirst 함수와 LNext 함수이다.
이 두 함수의 구조는 ArrayList의 LFisrt 함수와 LNext 함수와 거의 동일하다.</p>
<pre><code class="language-c">int LFirst(List * plist, LData * pdata)
{
    if(plist-&gt;head-&gt;next == NULL)    // 더미 노드가 NULL을 가리킨다면
        return FALSE;                // 반환할 데이터가 없음

    plist-&gt;before = plist-&gt;head;    // before은 더미 노드를 가리키게 함
    plist-&gt;cur = plist-&gt;head-&gt;next;    // cur은 첫 번째 노드를 가리키게 함

    *pdata = plist-&gt;cur-&gt;data;        // 첫 번째 노드의 데이터 전달
    return TRUE;
}</code></pre>
<p>위 함수의 핵심이 되는 문장은 <code>plist-&gt;before = plist-&gt;head;</code>와 <code>plist-&gt;cur = plist-&gt;head-&gt;next;</code>이다.
2, 4, 6, 8이 저장된 리스트에서 LFirst 함수가 호출되고, 이 두 문장이 실행되면 아래 그림과 같이 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/089124ec-47a0-4759-8608-84b1ee801516/image.png" alt=""></p>
<p>그리고 그 다음 문장을 통해서 첫 번째 데이터가 전달(반환)된다.
왜 before이란 구조체 멤버를 둬서 cur보다 하나 앞선 노드를 가리키게 할까?
이는 뒤에서 배울 &#39;노드의 삭제&#39;와 관련이 있다.</p>
<p>그 다음은 LNext 함수이다.</p>
<pre><code class="language-c">int LNext(List * plist, LData * pdata)
{
    if(plist-&gt;cur-&gt;next == NULL)    // cur이 NULL을 가리킨다면,
        return FALSE;                // 반환할 데이터 없음

    plist-&gt;before = plist-&gt;cur;        // cur이 가리키던 것을 before가 가리킴
    plist-&gt;cur = plist-&gt;cur-&gt;next;    // cur은 그 다음 노드 가리킴

    *pdata = plist-&gt;cur-&gt;data;        // cur이 가리키는 노드의 데이터 전달
    return TRUE;
}</code></pre>
<p>위 함수의 핵심이 되는 문장은 LFirst 함수와 비슷하게 <code>plist-&gt;before = plist-&gt;cur;</code>과 <code>plist-&gt;cur = plist-&gt;cur-&gt;next;</code>이다.
아래 그림에서 보듯 cur과 before가 가리키는 대상이 하나씩 오른쪽으로 이동하게 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/1ad19b4f-6d46-444a-bdda-50912a4cc9c6/image.png" alt=""></p>
<p>그리고 그 다음 문장을 통해 다음 노드의 데이터가 전달(반환)된다.</p>
<h4 id="4-노드의-삭제">4) 노드의 삭제</h4>
<p>연결 리스트에서 데이터의 추가만큼이나 주의해야하는 것이 삭제다.
LRemove함수의 기능은 바로 이전에 호출된 LFirst 혹은 LNext 함수가 반환한 데이터를 삭제하는 점에 집중해야한다.
만약 아래 그림과 같은 상황에서 LRemove 함수가 호출되었다고 생각해보자.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/d0e6e5b9-c58d-4161-9ad8-0caa3d31b85e/image.png" alt=""></p>
<p>위와 같은 상황은 LNext 함수의 호출을 통해 4가 반환된 이후다. 따라서 LRemove 함수의 호출시 소멸시켜야 하는 노드는 현재 cur이 가리키는 4가 저장된 노드다.
따라서 삭제의 결과는 다음과 같아야한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/a1373317-fbdb-4eb6-ad56-19f6f201d4ba/image.png" alt=""></p>
<p>위 그림에서 주목할 점은 cur의 위치가 재조정되었다는 것이다.
4가 지워지면서 이전 노드인 2를 가리켜야한다(왼쪽으로 한칸 이동해야한다). 그리고 동시에 before 노드도 왼쪽으로 한칸 이동해야하는데 LFirst 또는 LNext 함수가 호출되면 before는 cur보다 하나 왼쪽의 노드를 가리키게 되므로 굳이 before 위치까지 재조정할 필요는 없다.</p>
<p>설명으로 토대로 LRemove 함수를 구현하면 다음과 같다.</p>
<pre><code class="language-c">LData LRemove(List * plist)
{
    Node * rpos = plist-&gt;cur;    // 소멸 대상의 주소 값을 rpos에 저장
    LData rdata = rpos-&gt;data;    // 소멸 대상의 데이터를 rdata에 저장

    plist-&gt;before-&gt;next = plist-&gt;cur-&gt;next;        // 소멸 대상을 리스트에서 제거. (before의 다음을 cur의 다음으로 가리키도록)
    plist-&gt;cur = plist-&gt;before;                    // cur이 가리키는 위치 앞으로 한칸 이동하기

    free(rpos);                // 리스트에 제거된 노드 소멸
    (plist-&gt;numOfData)--;    // 저장된 데이터의 수 하나 감소
    return rdata;            // 제거된 노드의 데이터 반환
}</code></pre>
<p>위 코드의 과정들을 그림으로 정리하면 아래와 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/cc1a09ca-3d76-4e41-997e-4f00e9a5b76b/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/c920a245-ea93-4466-aaf8-7b2478527b2a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/6744ee2e-e0e3-484e-9aba-4be695167b65/image.png" alt=""></p>
<p>이렇게 되면 리스트에서 노드 하나가 삭제된 것이다.
나머지 세 문장에 의해서 삭제된 노드를 반영하기 위한 데이터의 수 하나 감소, 제거된 노드에 저장된 값을 반환 작업이 이뤄진다.</p>
<h4 id="5-데이터-전체-수-조회">5) 데이터 전체 수 조회</h4>
<p>데이터 전체 수 조회는 Chapter 03 ArrayList에서 한 것과 동일하다.</p>
<pre><code class="language-c">int LCount(List * plist)
{
    return plist-&gt;numOfData;
}</code></pre>
<h4 id="6-정렬-기준-등록">6) 정렬 기준 등록</h4>
<p>이 부분을 노드의 삽입 함수 중 SInsert 함수와 함께 이따가 배워볼 예정이다.
(<a href="https://velog.io/@mingming_eee/datastructure-04#%EC%97%B0%EA%B2%B0-%EB%A6%AC%EC%8A%A4%ED%8A%B8%EC%97%90%EC%84%9C%EC%9D%98-%EC%A0%95%EB%A0%AC%EA%B8%B0%EC%A4%80-%EC%84%A4%EC%A0%95%EA%B3%BC-%EA%B4%80%EB%A0%A8%EB%90%9C-%EB%B6%80%EB%B6%84">그래도 궁금한 사람은 여기로 👉</a>)</p>
<h4 id="7-정리하기">7) 정리하기</h4>
<p>위에서 구현한 함수들을 하나로 정리해보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;DLinkedList.h&quot;

// 리스트 초기화
void ListInit(List * plist)
{
    plist-&gt;head = (Node *)malloc(sizeof(Node));
    plist-&gt;head-&gt;next = NULL;
    plist-&gt;comp = NULL;
    plist-&gt;numOfData = 0;
}

// 노드 삽입
void FInsert(List * plist, LData data)
{
    Node * newNode = (Node *)malloc(sizeof(Node));
    newNode-&gt;data = data;

    newNode-&gt;next = plist-&gt;head-&gt;next;
    plist-&gt;head-&gt;next = newNode;

    (plist-&gt;numOfData)++;
}

void SInsert(List *plist, LData data)
{
    // 뒤에 설명 예정
}

void LInsert(List * plist, LData data)
{
    if(plist-&gt;comp == NULL)
        FInsert(plist, data);
    else
        SInsert(plist, data);
}

// 데이터 조회
int LFirst(List * plist, LData * pdata)
{
    if(plist-&gt;head-&gt;next == NULL)
        return FALSE;

    plist-&gt;before = plist-&gt;head;
    plist-&gt;cur = plist-&gt;head-&gt;next;

    *pdata = plist-&gt;cur-&gt;data;
    return TRUE;
}

int LNext(List * plist, LData * pdata)
{
    if(plist-&gt;cur-&gt;next == NULL)
        return FALSE;

    plist-&gt;before = plist-&gt;cur;
    plist-&gt;cur = plist-&gt;cur-&gt;next;

    *pdata = plist-&gt;cur-&gt;data;
    return TRUE;
}

// 노드 삭제
LData LRemove(List * plist)
{
    Node * rpos = plist-&gt;cur;
    LData rdata = rpos-&gt;data;

    plist-&gt;before-&gt;next = plist-&gt;cur-&gt;next;
    plist-&gt;cur = plist-&gt;before;

    free(rpos);
    (plist-&gt;numOfData)--;
    return rdata;
}

// 데이터 전체 수 조회
int LCount(List * plist)
{
    return plist-&gt;numOfData;
}

// 정렬 기준 등록
void SetSortRule(List * plist, int (*comp)(LData d1, LData d2))
{
    // 뒤에 설명 예정
}</code></pre>
<p>위에서 구현한 연결 리스트를 기반으로 작성된 main 함수 및 실행 결과는 아래와 같다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &quot;DLinkedList.h&quot;

int main()
{
    // 리스트의 생성 및 초기화
    List list;
    int data;
    ListInit(&amp;list);

    // 5개의 데이터 저장
    LInsert(&amp;list, 11);
    LInsert(&amp;list, 11);
    LInsert(&amp;list, 22);
    LInsert(&amp;list, 22);
    LInsert(&amp;list, 33);

    // 저장된 데이터의 전체 출력
    printf(&quot;Current Number of data: %d\n&quot;, LCount(&amp;list));

    if(LFirst(&amp;list, &amp;data))
    {
        printf(&quot;%d &quot;, data);

        while(LNext(&amp;list, &amp;data))
            printf(&quot;%d &quot;, data);
    }
    printf(&quot;\n\n&quot;);

    // 숫자 22를 검색하여 모두 삭제
    if(LFirst(&amp;list, &amp;data))
    {
        if(data == 22)
            LRemove(&amp;list);

        while(LNext(&amp;list, &amp;data))
        {
            if(data == 22)
                LRemove(&amp;list);
        }
    }

    // 삭제 후 남은 데이터 전체 출력
    printf(&quot;Now Number of data: %d\n&quot;, LCount(&amp;list));

    if(LFirst(&amp;list, &amp;data))
    {
        printf(&quot;%d &quot;, data);

        while(LNext(&amp;list, &amp;data))
            printf(&quot;%d &quot;, data);
    }
    printf(&quot;\n\n&quot;);
    return 0;
}

&gt; gcc .\DLinkedList.c .\DLinkedListMain.c
&gt; .\a.exe
&gt; 출력
Current Number of data: 5
33 22 22 11 11 

Now Number of data: 3
33 11 11</code></pre>
<hr>
<h2 id="04-3-연결-리스트의-정렬-삽입의-구현">04-3. 연결 리스트의 정렬 삽입의 구현</h2>
<p>정렬에 관한 부분을 마저 구현해보자!</p>
<h3 id="연결-리스트에서의-정렬기준-설정과-관련된-부분"><em>연결 리스트에서의 정렬기준 설정과 관련된 부분</em></h3>
<p>연결 리스트에서 정렬기준의 설정과 관련 있는 부분으 다음 세 가지와 같다.</p>
<ol>
<li>연결 리스트의 정렬기준이 되는 함수를 등록하는 SetSortRule 함수</li>
<li>SetSortRule 함수를 통해서 전달된 함수정보를 저장하기 위한 LinkedList의 멤버 comp</li>
<li>comp에 등록된 정렬기준을 근거로 데이터를 저장하는 SInsert 함수</li>
</ol>
<p>위에서 언급한 세 가지를 하나의 문장으로 정리하면 다음과 같다.
<strong>&quot;SetSortRule 함수가 호출되면서 정렬의 기준이 리스트의 멤버 comp에 등록되면, SInsert 함수 내에서는 comp에 등록된 정렬의 기준을 근거로 데이터를 정렬하여 저장한다.&quot;</strong></p>
<p>따라서 SetSortRule 함수는 리스트의 멤버 comp를 초기화 하는 함수이므로 다음과 같이 간단하게 정의된다.</p>
<pre><code class="language-c">void SetSortRule(List * plist, int (*comp)(LData d1, LData d2))
{
    plist-&gt;comp = comp;
}</code></pre>
<p>이어서 SInsert 함수를 구현해보자.</p>
<pre><code class="language-c">void SInsert(List *plist, LData data)
{
    Node * newNode = (Node *)malloc(sizeof(Node));    // 새 노드 생성
    Node * pred = plist-&gt;head;                        // pred = 더미 노드
    newNode-&gt;data = data;                            // 새 노드에 데이터 저장

    // 새 노드가 들어갈 위치를 찾기 위한 반복문
    while(pred-&gt;next != NULL &amp;&amp; plist-&gt;comp(data, pred-&gt;next-&gt;data) != 0)
        pred = pred-&gt;next;                // 다음 노드로 이동

    newNode-&gt;next = pred-&gt;next;        // 새 노드의 오른쪽 연결
    pred-&gt;next = newNode;            // 새 노드의 왼쪽 연결

    (plist-&gt;numOfData)++;            // 저장된 데이터의 수 +1
}</code></pre>
<p>위 함수의 반복문에서 보듯 comp에 등록된 함수의 호출결과를 기반으로 새 노드가 추가될 위치를 찾는다.
이해를 돕기 위해 그림과 같이 살펴보자.
다음과 같이 2, 4, 6이 저장된 리스트가 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/c89fb2f2-44f9-42db-8cbe-4a3119eb957e/image.png" alt=""></p>
<p>오름차순으로 데이터가 저장된 것을 확인할 수 있는데 숫자 5를 삽입하려고 한다.
<code>SInsert(&amp;slist, 5);</code>문장이 실행되면서 SInsert 함수가 실행된다.
함수의 첫 세문장이 실행되고 새 노드에 5가 저장된다.
그리고 모든 노드를 차례대로 가리키기 위해 선언된 포인터 변수 pred는 더미 노드를 가리키게 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/dc9c0e11-da1b-4717-9929-b0998db00ee3/image.png" alt=""></p>
<p>포인터 변수 pred가 더미 노드를 가리키는 이유는 지금 우리가 배우고 있는 연결 리스트가 단순(한방향) 연결 리스트이기 때문에 더미 노드부터 차례대로 오른쪽으로 가야하기 때문이다.</p>
<p>다음 이번 정렬의 핵심인 while문이다.
SInsert 함수의 while문은 두 가지 반복 조건을 가지고 있다.</p>
<ul>
<li><p>반복 조건 1: pred-&gt;next != NULL
: pred가 리스트의 마지막 노드를 가리키는지 묻기 위함</p>
</li>
<li><p>반복 조건 2: plist-&gt;comp(data, pred-&gt;next-&gt;data) != 0
: 새 데이터와 pred의 다음 노드에 저장된 데이터이 우선순위 비교를 위한 함수 호출</p>
</li>
</ul>
<p>따라서 위 조건들을 한 문장으로 정리하자면 다음과 같다.
&quot;pred가 마지막 노드를 가리키는 것도 아니고, 새 데이터가 들어갈 자리도 아직 찾지 못했다면 pred를 다음 노드로 이동시킨다.&quot;</p>
<p>반복 조건2를 더 자세히 살펴보자면 comp에 등록된 함수가 반환하는 값의 종류에 따라 정렬이 달라진다.</p>
<ul>
<li><p>comp가 0을 반환
: 첫 번째 인자인 data가 정렬 순서상 앞서서 head에 더 가까워야 하는 경우</p>
</li>
<li><p>comp가 1을 반환
: 두 번째 인자인 pred-&gt;next-&gt;data가 정렬 순서상 앞서서 head에 더 가가워야 하는 경우</p>
</li>
</ul>
<p>따라서 우리는 5를 넣어야하기 때문에 4가 5보다 앞서야하므로 0이 반환되면서 그 위치가 정해진다.
아래 그림과 같이 pred가 4에서 멈추게 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/8ee1bc38-1b46-4107-a6e0-d46209a19b69/image.png" alt=""></p>
<p>반복문을 지나서 다음 세 문장을 통해 4와 6사이에 새 노드가 삽입되고 그 값을 반환한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/25b31add-56c2-4079-a449-b8fa82688c17/image.png" alt=""></p>
<h3 id="정렬의-기준을-설정하기-위한-함수의-정의"><em>정렬의 기준을 설정하기 위한 함수의 정의</em></h3>
<p>마지막으로 SetSortRule 함수의 인자가 될 수 있는 함수를 정의하는 일만 남았다.
이 함수를 정의하는데 있어 필요한 정보 두 가지는 다음과 같다.</p>
<ol>
<li>두 개의 인자를 전달받도록 함수를 정의한다.</li>
<li>첫 번째 인자의 정렬 우선순위가 높으면 0을, 그렇지 않으면 1을 반환한다.</li>
</ol>
<p>이 두 가지만 만족한다면 어떤 함수건 SetSortRule 함수의 인자가 될 수 있다.
위 두 조건을 만족하는 함수는 다음과 같다.</p>
<pre><code class="language-c">int WhoIsPrecede(int d1, int d2)    // typedef int LData;
{
    if(d1 &lt; d2)
        return 0;    // d1이 정렬 순서상 앞선다.
    else
        return 1;    // d2가 정렬 순서상 앞서거나 같다.
}</code></pre>
<p>이 함수는 오름차순을 위한 함수로 d1과 d2의 대소관계를 바꾸면 내림차순을 위한 함수로 바꿀 수 있다. 
그렇다면 정렬의 기준인 WhoISPrecede 함수는 어디에 위치해야 할까?
이 함수는 main함수가 있는 파일에 지정해야 한다. 
프로그래머가 연결 리스트의 정렬 기준을 결정할수 있도록 유연성을 부여하기 위함이다!</p>
<p>따라서 총 정리된 단순 연결 리스트의 함수 구현 코드와 main 함수 코드는 다음과 같다.</p>
<ul>
<li>DLinkedList.c<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;DLinkedList.h&quot;
</code></pre>
</li>
</ul>
<p>// 리스트 초기화
void ListInit(List * plist)
{
    plist-&gt;head = (Node *)malloc(sizeof(Node));
    plist-&gt;head-&gt;next = NULL;
    plist-&gt;comp = NULL;
    plist-&gt;numOfData = 0;
}</p>
<p>// 노드 삽입
void FInsert(List * plist, LData data)
{
    Node * newNode = (Node *)malloc(sizeof(Node));
    newNode-&gt;data = data;</p>
<pre><code>newNode-&gt;next = plist-&gt;head-&gt;next;
plist-&gt;head-&gt;next = newNode;

(plist-&gt;numOfData)++;</code></pre><p>}</p>
<p>// [[[추가]]]
void SInsert(List *plist, LData data)
{
    Node * newNode = (Node *)malloc(sizeof(Node));    // 새 노드 생성
    Node * pred = plist-&gt;head;                        // pred = 더미 노드
    newNode-&gt;data = data;                            // 새 노드에 데이터 저장</p>
<pre><code>// 새 노드가 들어갈 위치를 찾기 위한 반복문
while(pred-&gt;next != NULL &amp;&amp; plist-&gt;comp(data, pred-&gt;next-&gt;data) != 0)
    pred = pred-&gt;next;                // 다음 노드로 이동

newNode-&gt;next = pred-&gt;next;        // 새 노드의 오른쪽 연결
pred-&gt;next = newNode;            // 새 노드의 왼쪽 연결

(plist-&gt;numOfData)++;            // 저장된 데이터의 수 +1</code></pre><p>}</p>
<p>void LInsert(List * plist, LData data)
{
    if(plist-&gt;comp == NULL)
        FInsert(plist, data);
    else
        SInsert(plist, data);
}</p>
<p>// 데이터 조회
int LFirst(List * plist, LData * pdata)
{
    if(plist-&gt;head-&gt;next == NULL)
        return FALSE;</p>
<pre><code>plist-&gt;before = plist-&gt;head;
plist-&gt;cur = plist-&gt;head-&gt;next;

*pdata = plist-&gt;cur-&gt;data;
return TRUE;</code></pre><p>}</p>
<p>int LNext(List * plist, LData * pdata)
{
    if(plist-&gt;cur-&gt;next == NULL)
        return FALSE;</p>
<pre><code>plist-&gt;before = plist-&gt;cur;
plist-&gt;cur = plist-&gt;cur-&gt;next;

*pdata = plist-&gt;cur-&gt;data;
return TRUE;</code></pre><p>}</p>
<p>// 노드 삭제
LData LRemove(List * plist)
{
    Node * rpos = plist-&gt;cur;
    LData rdata = rpos-&gt;data;</p>
<pre><code>plist-&gt;before-&gt;next = plist-&gt;cur-&gt;next;
plist-&gt;cur = plist-&gt;before;

free(rpos);
(plist-&gt;numOfData)--;
return rdata;</code></pre><p>}</p>
<p>// 데이터 전체 수 조회
int LCount(List * plist)
{
    return plist-&gt;numOfData;
}</p>
<p>// 정렬 기준 등록 [[[추가]]]
void SetSortRule(List * plist, int (*comp)(LData d1, LData d2))
{
    plist-&gt;comp = comp;
}</p>
<pre><code>
- DLinkedListSortMain.c
```c
#include &lt;stdio.h&gt;
#include &quot;DLinkedList.h&quot;

int WhoIsPrecede(int d1, int d2)
{
    if(d1 &lt; d2)
        return 0;    // d1이 정렬 순서상 앞선다.
    else
        return 1;    // d2가 정렬 순서상 앞서거나 같다.
}

int main()
{
    // 리스트의 생성 및 초기화
    List list;
    int data;
    ListInit(&amp;list);

    // 정렬 기준 등록
    SetSortRule(&amp;list, WhoIsPrecede);

    // 5개의 데이터 저장
    LInsert(&amp;list, 11);
    LInsert(&amp;list, 11);
    LInsert(&amp;list, 22);
    LInsert(&amp;list, 22);
    LInsert(&amp;list, 33);

    // 저장된 데이터의 전체 출력
    printf(&quot;Current Number of data: %d\n&quot;, LCount(&amp;list));

    if(LFirst(&amp;list, &amp;data))
    {
        printf(&quot;%d &quot;, data);

        while(LNext(&amp;list, &amp;data))
            printf(&quot;%d &quot;, data);
    }
    printf(&quot;\n\n&quot;);

    // 숫자 22를 검색하여 모두 삭제
    if(LFirst(&amp;list, &amp;data))
    {
        if(data == 22)
            LRemove(&amp;list);

        while(LNext(&amp;list, &amp;data))
        {
            if(data == 22)
                LRemove(&amp;list);
        }
    }

    // 삭제 후 남은 데이터 전체 출력
    printf(&quot;Now Number of data: %d\n&quot;, LCount(&amp;list));

    if(LFirst(&amp;list, &amp;data))
    {
        printf(&quot;%d &quot;, data);

        while(LNext(&amp;list, &amp;data))
            printf(&quot;%d &quot;, data);
    }
    printf(&quot;\n\n&quot;);
    return 0;
}

&gt; gcc .\DLinkedList.c .\DLinkedListSortMain.c
&gt; .\a.exe
&gt; 출력
Current Number of data: 5
11 11 22 22 33 

Now Number of data: 3
11 11 33</code></pre><hr>
<h2 id="review"><code>&lt;Review&gt;</code></h2>
<p>와... Chapter 04-2에서 LFirst 함수 구현할 때 <code>if(plist-&gt;head-&gt;next == NULL)</code>에서 ==이 아니라 =으로 오타내서 오류 잡느라 30분은 날린거 같다 후^^</p>
<p>생각보다 파이썬으로 연결리스트를 구현했을 때에 비해 디테일하지만서도 심플하게 하지만 코드는 엄청 길게 구현이 된다는 것을 알게 되었다.</p>
<p>이렇게 정리하는게 단순히 책 내용 배껴서 정리하는 걸로 볼 수 있겠지만
나중에 기억이 안나거나 헷갈렸던 부분을 내가 언제든 편하게 볼 수 있도록 정리하는 것이라고 생각한다.</p>
<p>앞으로도 화이티잉<del>!
내년안에 취업 가보자구우</del>!</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/d42c2f85-32d9-40b9-9e7b-5b727605ea57/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chapter 03. 연결 리스트(Linked List) 1]]></title>
            <link>https://velog.io/@mingming_eee/datastructure-03</link>
            <guid>https://velog.io/@mingming_eee/datastructure-03</guid>
            <pubDate>Tue, 15 Oct 2024 08:07:51 GMT</pubDate>
            <description><![CDATA[<h2 id="03-1-추상-자료형-abstract-data-type">03-1. 추상 자료형: Abstract Data Type</h2>
<h3 id="추상-자료형abstract-data-type"><em>추상 자료형(Abstract Data Type)</em></h3>
<p>추상 자료형은 간단히 ADT라고도 한다. 
객체지향 언어(C++이나, JAVA, Python 등)에서 공부하면 더 깊이 이해할 수 있기 때문에 여기서는 자료구조의 관점에서 ADT를 배워볼 것이다.</p>
<p>추상 자료형(ADT)이란 구체적인 기능의 완성과정을 언급하지 않고, 순수하게 기능이 무엇인지 나열하는 것을 말한다.
예를 들어 아래와 같이 Wallet이라는 구조체를 기반으로 하는 자료형을 정의했다고 하자.</p>
<pre><code class="language-c">typedef struct _wallet
{
    int coin100Num;        // 100원짜리 동전의 수
    int bill500Num;        // 5000원짜리 지폐의 수
} Wallet;</code></pre>
<p>단순히 구조체 정의만으로 Wallet이라는 자료형의 정의가 완성되는 것이 아니라 Wallet을 기반으로 하는 연산의 종류를 결정하는 것도 자료형 정의의 일부로 봐야하고, 연산의 종류가 결정되었을 때 자료형의 정의는 완성된다.
예를 들면 돈을 꺼내는 연산과 돈을 넣는 연산에 대한 것을 아래와 같이 정의하면 Wallet에 대한 자료형의 정의는 완성된다. (연산이 이 두개가 다라면)</p>
<pre><code class="language-c">int TakeOutMoney(Wallet * pw, int coinNum, int billNum);    // 돈을 꺼내는 연산
void PutMoney(Wallet * pw, int coinNum, int bilNum);    // 돈을 넣는 연산</code></pre>
<h3 id="추상-자료형-정의"><em>추상 자료형 정의</em></h3>
<p>정의한 자료형 Wallet에 대한 추상 자료형을 정의해보자.
위에서 언급한 연산 2가지에 대해서 명시해야 할 정보인 기능을 묘사하면 된다.</p>
<p>✅Operations:</p>
<ul>
<li><p>int TakeOutMoney(Wallet * pw, int coinNum, int billNum)</p>
<ul>
<li>첫 번째 인자로 전달된 주소의 지갑에서 돈을 꺼낸다.</li>
<li>두 번째 인자로 꺼낼 동전의 수, 세 번째 인자로 꺼낼 지폐의 수를 전달한다.</li>
<li>꺼내고자 하는 돈의 총액이 반환된다. 그리고 그만큼 돈은 차감된다.</li>
</ul>
</li>
<li><p>void PutMoney(Wallet * pw, int coinNum, int billNum)</p>
<ul>
<li>첫 번째 인자로 전달된 주소의 지갑에 돈을 넣는다.</li>
<li>두 번째 인자로 넣을 동전의 수, 세 번째 인자로 넣을 지폐의 수를 전달한다.</li>
<li>넣은 만큼 동전과 지폐의 수가 증가한다.</li>
</ul>
</li>
</ul>
<p>구조체 Wallet의 정의는 ADT에 포함되어야 하는 것일까?
main 함수를 통해 필요한지 알아보자.</p>
<pre><code class="language-c">int main(void)
{
    Wallet myWallet;
    ....
    PutMoney(&amp;myWallet, 5, 10);
    ....
    ret = TakeOutMoney(&amp;myWallet, 2, 5);
}</code></pre>
<p>보다시피 Wallet의 정의는 돈을 넣고 꺼내는데 필요한 정보가 아니다.
따라서 Wallte의 정의를 ADT에 넣는 것은 바람직하지 못하다.
생각해보면 우리는 FILE 구조체의 내부를 잘 몰라도 잘 사용해왔다. 따라서 필요한 구조체를 우리가 만들었다고 해서 그 내부 멤버들까지 추상 자료형에 추가해 알 필요는 없는 것이다.</p>
<h4 id="리스트-자료구조-adt"><em>리스트 자료구조 ADT</em></h4>
<p>이제 리스트 자료구조라는 것을 배울 것인데 학습 순서는 다음과 같다.</p>
<ol>
<li>리스트 자료구조의 ADT를 정의.</li>
<li>ADT를 근거로 리스트 자료구조를 활용하는 main 함수를 정의.</li>
<li>ADT를 근거로 리스트 구현.</li>
</ol>
<p>2, 3번의 순서가 저렇게 된 것은 리스트 사용자에게 사용방법 이외 불필요한 부분까지 알려주지 않아도 되기 때문이다.
따라서 앞으로 구현할 자료구조는 그 내부 구현을 알지 못해도 활용할 수 있도록 ADT를 정의하여 구현할 것이다.</p>
<hr>
<h2 id="03-2-배열을-이용한-리스트의-구현">03-2. 배열을 이용한 리스트의 구현</h2>
<h3 id="리스트의-이해"><em>리스트의 이해</em></h3>
<p>리스트는 구현 방법에 따라 크게 두 가지로 나뉜다.
(리스트 = 연결 리스트란 오해에서 벗어나보자.)</p>
<ul>
<li>순차 리스트: 배열을 기반으로 구현된 리스트</li>
<li>연결 리스트: 메모리의 동적 할당을 기반으로 구현된 리스트</li>
</ul>
<p>그렇 순차 리스트와 연결 리스트 ADT는 무조건 다른가? 아니다. 구현방법의 차이에서 비롯된 것이기 때문에 이 둘의 ADT가 동일할 수도 있다.
리스트의 기본 특성을 가진 ADT는 표준을 가지고 있다. 리스트의 ADT 정의를 위해서 리스트 자료구조의 가장 기본적이고도 중요한 특성은 다음과 같다.</p>
<p><strong>&quot;리스트 자료구조는 데이터를 나란히 저장한다. 그리고 중복된 데이터의 저장을 허락한다.&quot;</strong></p>
<p>자료구조 중에서 중복된 데이터의 저장을 허용하지 않는 경우도 있다. 하지만 리스트는 이를 허용한다. (집합은 중복을 허락하지 않는다.)</p>
<h3 id="리스트의-adt"><em>리스트의 ADT</em></h3>
<p>리스트의 특성에 대해 알았으니 리스트의 특성을 기반으로 제공해야 할 기능들을 정의해보자.</p>
<p>✅리스트 자료구조의 ADT</p>
<ul>
<li><p>void ListInit(List * plist);</p>
<ul>
<li>초기화할 리스트의 주소 값을 인자로 전달.</li>
<li>리스트 생성 후 제일 먼저 호출되어야 하는 함수.</li>
</ul>
</li>
<li><p>void LInsert(List * plist, LData data);</p>
<ul>
<li>리스트에 데이터 저장. 매개변수 data에 전달된 값 저장.</li>
</ul>
</li>
<li><p>int LFirst(List * plist, LData * pdata);</p>
<ul>
<li>첫 번째 데이터가 pdata가 가리키는 메모리에 저장.</li>
<li>데이터의 참조를 위한 초기화가 진행.</li>
<li>참조 성공 시 TRUE(1), 실패 시 FALSE(0) 반환.</li>
</ul>
</li>
<li><p>int LNext(List * plist, LData * pdata);</p>
<ul>
<li>참조된 데이터의 다음 데이터가 pdata가 가리키는 메로리에 저장.</li>
<li>순차적인 참조를 ㅜ이해서 반복 호출 가능.</li>
<li>참조를 새로 시작하려면 먼저 LFirst 함수를 호출해야하만 함.</li>
<li>참조 성공 시 TRUE(1), 실패 시 FALSE(0) 반환.</li>
</ul>
</li>
<li><p>LData LRemove(List * plist);</p>
<ul>
<li>LFirst 또는 LNext 함수의 마지막 반환 데이터를 삭제.</li>
<li>삭제된 데이터는 반환.</li>
<li>마지막 반환 데이터를 삭제하므로 연이은 반복 호출 허용 안함.</li>
</ul>
</li>
<li><p>int LCount(List * plist);</p>
<ul>
<li>리스트에 저장되어 있는 데이터의 수 반환.</li>
</ul>
</li>
</ul>
<p>LData는 리스트에 저장할 데이터의 자료형에 제한을 두지 않기 위한 typedef 선언의 결과로 헤더파일로 따로 관리한다.</p>
<p>우리는 리스트 자료구조의 ADT를 정의했고 ADT를 근거로 리스트 자료구조를 활용하는 main 함수를 정의할 것이다.</p>
<h3 id="리스트의-adt를-기반으로-정의된-main-함수"><em>리스트의 ADT를 기반으로 정의된 main 함수</em></h3>
<p>아래 main함수를 기반으로 리스트 ADT에서 소개하는 함수들의 기능을 완전히 이해해보자.</p>
<pre><code class="language-c">// ListMain.c
#include &lt;stdio.h&gt;
#include &quot;ArrayList.h&quot;

int main()
{
    // ArrayList의 생성 및 초기화 ///////
    List list;
    int data;
    ListInit(&amp;list);

    // 5개의 데이터 저장 ///////
    LInsert(&amp;list, 11);
    LInsert(&amp;list, 11);
    LInsert(&amp;list, 22);
    LInsert(&amp;list, 22);
    LInsert(&amp;list, 33);

    // 저장된 데이터의 전체 출력 ///////
    printf(&quot;Number of Current Data: %d\n&quot;, LCount(&amp;list));

    if(LFirst(&amp;list, &amp;data))        // 첫 번째 데이터 조회
    {
        printf(&quot;%d &quot;, data);

        while(LNext(&amp;list, &amp;data))  // 두 번째 이후의 데이터 조회
            printf(&quot;%d &quot;, data);
    }
    printf(&quot;\n\n&quot;);

    // 숫자 22를 탐색하여 모두 삭제 ///////
    if(LFirst(&amp;list, &amp;data))
    {
        if(data == 22)
            LRemove(&amp;list);

        while(LNext(&amp;list, &amp;data))
        {
            if(data == 22)
                LRemove(&amp;list);
        }
    }

    // 삭제 후 남은 데이터 전체 출력 ///////
    printf(&quot;Now Number of Data: %d\n&quot;, LCount(&amp;list));

    if(LFirst(&amp;list, &amp;data))
    {
        printf(&quot;%d &quot;, data);

        while(LNext(&amp;list, &amp;data))
            printf(&quot;%d &quot;, data);
    }
    printf(&quot;\n\n&quot;);
    return 0;
}</code></pre>
<p>현재는 헤더파일 중 &quot;ArrayList.h&quot;가 정의되지 않았기 때문에 실행을 하면 오류가 발생할 수 밖에 없다. 
하지만 우리가 설정한 리스트이 ADT의 기능을 main 함수에 구현한 것이 중점이기 때문에 이를 찬찬히 먼저 살펴보자.</p>
<p>우선 위의 main 함수에서 제일 먼저 등장하는 리스트의 생성 및 초기화 관련된 문장은 아래와 같다.</p>
<pre><code class="language-c">int main()
{
    List list;            // 리스트의 생성
    ....
    ListInit(&amp;list);    // 리스트의 초기화
    ....
}</code></pre>
<p>List를 기반으로 변수 list를 선언하고 있고 이걸 리스트라고 부를 것이다.
모든 자료구조는 단순히 데이터만 담는 것이 아니라 데이터를 효율적으로 저장 및 참조하기 위한 정보들도 담아야한다. 따라서 이와 관련된 변수들의 초기화 해야하면 이를 담당하는 함수가 ListInit이다. </p>
<p>이 후 데이터를 5번 저장했는데 LInsert 함수를 호출하면서 리스트의 주소 값을 첫 번째 인자로, 리스트에 담을 데이터를 두 번째 인자로 전달하고 있다.</p>
<p>이 후 데이터를 저장된 순서대로 데이터를 참조하여 출력을 진행하고, 마지막 데이터까지 참조하여 출력을 진행한다.
순서대로 참조하려면 먼저 LFirst를 호출해서 첫 번째 데이터를 얻고 두 번째 이후의 데이터는 LNext를 호출해서 얻으면 된다.
그리고 LFirst 함수와 LNext 함수는 더이상 참조할 데이터가 없다면 FALSE를 반환한다.</p>
<p>마지막으로 데이터 삭제관련 코드다.
삭제를 위해서는 데이터 탐색이 먼저 되어야하기 때문에 코드를 LFirst로 탐색한 후 삭제하고 싶은 데이터 값(22)과 동일하다면 LRemove 함수가 실행된다. 이 코드 진행은 출력과 동일하게 LFirst 이후 LNext로 이루어지게 된다.</p>
<h3 id="배열기반-리스트-구현하기-1-헤더파일의-정의"><em>배열기반 리스트 구현하기 1: 헤더파일의 정의</em></h3>
<p>우리는 어떤 자료구조던 간에 &#39;자료구조의 구현&#39;과 &#39;구현된 자료구조의 활용&#39;이 완전히 구분되도록 ADT를 정의해야한다.
리스트 구현방법 중 하나인 배열을 이용해 구현하는 방법(순차 리스트)을 사용할 것이다.
그 첫 번째 순서로 배열 기반의 리스트 구현을 위해 정의된 헤더파일은 다음과 같다.</p>
<pre><code class="language-c">// ArrayList.h
#ifndef __ARRAY_LIST_H__
#define __ARRAY_LIST_H__

#define TRUE        1   // 참을 표현하기 위한 메크로 정의
#define FALSE       0   // 거짓을 표현하기 위한 메크로 정의

#define LIST_LEN    100
typedef int LData;      // LData에 대한 typedef 선언

typedef struct __ArrayList  // 배열 기반 리스트를 정의한 구조체
{
    LData arr[LIST_LEN];    // 리스트의 저장소인 배열
    int numOfData;          // 저장된 데이터의 개수
    int curPosition;        // 데이터 참조위치를 기록
} ArrayList;

typedef ArrayList List;

void ListInit(List * plist);                // 초기화
void LInsert(List * plist, LData data);     // 데이터 저장

int LFirst(List * plist, LData * pdata);    // 첫 데이터 참조
int LNext(List * plist, LData * pdata);     // 두 번째 이후 데이터 참조

LData LRemove(List * plist);                // 참조한 데이터 삭제
int LCount(List * plist);                    // 저장된 데이터의 개수 반환

#endif</code></pre>
<p>위에서 정의한 구조체 ArrayList에는 데이터의 저장공간이 배열로 선언되었고,
저장된 데이터의 수를 기록하기 위한 멤버(numOfData)와
LFirst, LNext, LRemove 함수에서 참조의 위치를 기록하기 위한 멤버(curPosition)멤버가 있다.
다양한 종류의 데이터를 저장할 수 있게 하기 위한 typedef 선언도 다음과 같이 존재한다.</p>
<pre><code class="language-c">typedef int LData;            // 리스트에 int형 데이터의 저장을 위한 선언
typedef ArrayList List;        // List는 배열 기반 리스트</code></pre>
<p>ArrayList라는 이름에도 typedef 선언을 하면 연결리스트로 리스트 종류를 바꿀 수 있다.
<code>typedef LinkedList List;</code></p>
<h3 id="배열기반-리스트-구현하기-2-함수-정의"><em>배열기반 리스트 구현하기 2: 함수 정의</em></h3>
<p>이제 헤더파일에 선언된 함수들을 정의해보자.</p>
<h4 id="1-초기화-함수---listinit">1. 초기화 함수 - ListInit</h4>
<p>이 함수를 정의하기 위해서는 먼저 앞서 정의한 구조체 ArrayList에서 초기화할 대상이 어떤 것인지 알아야한다.</p>
<pre><code class="language-c">void ListInit(List * plist)
{
    (plist-&gt;numOfData) = 0;        // 현재 리트스에 저장된 데이터 수 0
    (plist-&gt;curPosition) = -1;    // 현재 아무 위치도 가리키지 않음
}</code></pre>
<p>curPosition에는 배열의 인덱스 값이 저장된다.
LFirst 함수와 LNext 함수에서 참조해야할 배열의 위치를 이 변수에 저장된 값을 통해 알 수 있게 한다.</p>
<h4 id="2-삽입-함수---linsert">2. 삽입 함수 - LInsert</h4>
<p>이 함수는 단순하게 우선 데이터의 수가 배열의 길이를 초과했는지 검사하고 초과하지 않았다면 일반적인 데이터의 저장과정을 진행한다. 저장할 때는 배열의 앞부분부터 채워나간다.</p>
<pre><code class="language-c">void LInsert(List * plist, LData data)
{
    if(plist-&gt;numOfData &gt;= LIST_LEN)    // 데이터의 수 &gt; 배열의 길이
    {
        puts(&quot;저장이 불가능합니다.&quot;);
        return;
    }

    plist-&gt;arr[plist-&gt;numOfData] = data;    // 데이터 저장
    (plist-&gt;numOfData)++;    // 저장된 데이터의 수 증가
}</code></pre>
<p>다음은 LFirst와 LNext 함수를 정의해보자.</p>
<h4 id="3-첫-번째-데이터-조회---lfirst">3. 첫 번째 데이터 조회 - LFirst</h4>
<pre><code class="language-c">int LFirst(List * plist, LData * pdata)
{
    if(plist-&gt;numOfData == 0)    // 저장된 데이터가 하나도 없다면
        return FALSE;

    (plist-&gt;numOfData) = 0;        // 참조 위치 초기화. 첫 번째 데이터의 참조
    *pdata = plist-&gt;arr[0];        // pdata가 가리키는 공간에 데이터 저장
    return TRUE;
}</code></pre>
<h4 id="4-두-번째-이후의-데이터-조회---lnext">4. 두 번째 이후의 데이터 조회 - LNext</h4>
<pre><code class="language-c">int LNext(List * plist, LData * pdata)
{
    if(plist-&gt;curPosition &gt;= (plist-&gt;numOfData)-1)    // 더 이상 참조할 데이터가 없다면
        return FALSE;

    (plist-&gt;curPosition)++;
    *pdata = plist-&gt;arr[plist-&gt;curPosition];
    return TRUE;
}</code></pre>
<p>LFirst 함수와 LNext 함수의 차이점은 다음 문장에 있다.
<code>(plist-&gt;numOfData) = 0; (LFirst)</code>와 <code>(plist-&gt;numOfData)++; (LNext)</code>이다.
LFirst에서는 curPosition에 저장된 값을 0으로 재설정함으로써 데이터의 참조가 앞에서부터 다시 진행할 수 있도록 한다.
반면, LNext에서는 이 값을 증가시켜 순서대로 데이터를 참조할 수 있도록 한다.</p>
<h4 id="5-삭제---lremove">5. 삭제 - LRemove</h4>
<p>마지막으로 데이터의 삭제를 담당하는 함수를 완성해보자.
LFirst 함수나 LNext 함수의 호출을 통해서 바로 직전에 참조가 이뤄진 데이터를 삭제하는 것이 LRemove 함수다보니 LRemove 함수가 호출되면 리스트의 멤버 curPosition을 확인해서 조회가 이뤄진 데이터의 위치를 확인한 다음 그 데이터를 삭제한다.
앞에서부터 데이터를 채우는 것이 원칙이니 중간에 데이터가 삭제되면, 뒤에 저장된 데이터들을 한 칸씩 앞으로 이동시켜서 그 빈 공간을 메워야 한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/ac8fec62-3314-4d1f-9a7f-8bb0c0f821fd/image.png" alt=""></p>
<p>따라서 주의해야할 점은 삭제할 데이터의 위치를 참조하는 방식과 삭제를 위한 데이터의 이동과정이다.</p>
<pre><code class="language-c">LData LRemove(List * plist)
{
    int rpos = plist-&gt;curPosition;        // 삭제할 데이터의 인덱스 값 참조
    int num = plist-&gt;numOfData;
    int i;
    LData rdata = plist-&gt;arr[rpos];        // 삭제할 데이터를 임시로 저장

    // 삭제를 위한 데이터의 이동을 진행하는 반복문
    for(i=rpos; i&lt;num-1; i++)
        plist-&gt;arr[i] = plist-&gt;arr[i+1];

    (plist-&gt;numOfData)--;    // 데이터의 수 감소
    (plist-&gt;curPosition)--;    // 참조위치를 하나 되돌림
    return rdata;            // 삭제된 데이터의 반환
}</code></pre>
<p>참조위치를 하나 되돌리는 이유는 C가 삭제된 이후에 curPosition에 최근 참조가 이뤄진 데이터의 인덱스 정보를 담고 있어야하기 때문에 B를 가리켜야하는데 하나 되돌리지 않을 경우 D를 가리키고 있어 아직 참조가 이루어지지 않은 인덱스를 가리키기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/111f8a18-34fa-4223-8e88-423571ec572d/image.png" alt=""></p>
<p>따라서 삭제 이후 LNext 함수가 호출되면 아직 참조되지 않은 D를 가리킬 수 있게 된다.</p>
<h4 id="6-데이터-갯수-조회---lcount"><em>6. 데이터 갯수 조회 - LCount</em></h4>
<p>데이터 수를 조회하는 LCount 함수 정의는 간단하다.</p>
<pre><code class="language-c">int LCount(List * plist)
{
    return plist-&gt;numOfData;
}</code></pre>
<h3 id="배열기반-리스트-구현하기-3-정리"><em>배열기반 리스트 구현하기 3: 정리</em></h3>
<p>위에서 설명한 함수 5가지를 하나의 파일로 정리해보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &quot;ArrayList.h&quot;

void ListInit(List * plist)
{
    (plist-&gt;numOfData) = 0;        // 현재 리트스에 저장된 데이터 수 0
    (plist-&gt;curPosition) = -1;    // 현재 아무 위치도 가리키지 않음
}

void LInsert(List * plist, LData data)
{
    if(plist-&gt;numOfData &gt;= LIST_LEN)    // 데이터의 수 &gt; 배열의 길이
    {
        puts(&quot;저장이 불가능합니다.&quot;);
        return;
    }

    plist-&gt;arr[plist-&gt;numOfData] = data;    // 데이터 저장
    (plist-&gt;numOfData)++;    // 저장된 데이터의 수 증가
}

int LFirst(List * plist, LData * pdata)
{
    if(plist-&gt;numOfData == 0)    // 저장된 데이터가 하나도 없다면
        return FALSE;

    (plist-&gt;curPosition) = 0;        // 참조 위치 초기화. 첫 번째 데이터의 참조
    *pdata = plist-&gt;arr[0];        // pdata가 가리키는 공간에 데이터 저장
    return TRUE;
}

int LNext(List * plist, LData * pdata)
{
    if(plist-&gt;curPosition &gt;= (plist-&gt;numOfData)-1)    // 더 이상 참조할 데이터가 없다면
        return FALSE;

    (plist-&gt;curPosition)++;
    *pdata = plist-&gt;arr[plist-&gt;curPosition];
    return TRUE;
}

LData LRemove(List * plist)
{
    int rpos = plist-&gt;curPosition;        // 삭제할 데이터의 인덱스 값 참조
    int num = plist-&gt;numOfData;
    int i;
    LData rdata = plist-&gt;arr[rpos];        // 삭제할 데이터를 임시로 저장

    // 삭제를 위한 데이터의 이동을 진행하는 반복문
    for(i=rpos; i&lt;num-1; i++)
        plist-&gt;arr[i] = plist-&gt;arr[i+1];

    (plist-&gt;numOfData)--;    // 데이터의 수 감소
    (plist-&gt;curPosition)--;    // 참조위치를 하나 되돌림
    return rdata;            // 삭제된 데이터의 반환
}

int LCount(List * plist)
{
    return plist-&gt;numOfData;
}</code></pre>
<p>이제 다시 한번 ListMain.c 파일을 컴파일해서 실행해보자.</p>
<pre><code class="language-bash">&gt; gcc .\ListMain.c .\ArrayList.c
&gt; .\a.exe
&gt; 출력
Number of Current Data: 5
11 11 22 22 33 

Now Number of Data: 3
11 11 33</code></pre>
<h3 id="리스트에-구조체-변수-저장하기-1-구조체-point와-관련-함수들의-정의"><em>리스트에 구조체 변수 저장하기 1: 구조체 Point와 관련 함수들의 정의</em></h3>
<p>이전에 구현한 리스트에는 단순히 정수를 저장했다.
하지만 실제로 구조체 변수를 비롯해서 각종 데이터들이 저장된다.
따라서 우리가 정의한 리스트에 <code>구조체 변수의 주소값</code>을 저장해 보자.</p>
<p>이를 위해서 다음과 같은 구조체를 정의했다.</p>
<pre><code class="language-c">typedef struct _point
{
    int xpos;        // x좌표 정보
    int ypos;        // y좌표 정보
} Point;</code></pre>
<p>정수가 아닌 다른 데이터를 리스트에 저장한다는데 의미가 있으므로, 구조체는 가급적 간단히 정의했다.
위의 구조체와 관련 있는 다른 함수들도 함께 정의해준다.</p>
<ul>
<li><code>void SetPointPos(Point * ppos, int xpos, int ypos);</code> : 구조체 변수에 값을 저장</li>
<li><code>void ShowPointPos(Point * ppos);</code> : 저장된 정보 출력</li>
<li><code>int PointComp(Point * pos1, Point * pos2);</code> : 두 구조체 변수에 저장된 값 비교</li>
</ul>
<p>위 함수가 반환하는 값은 다음과 같다고 정의한다.</p>
<ul>
<li>두 Point 변수의 멤버 xpos만 같으면 1 반환</li>
<li>두 Point 변수의 멤버 ypos만 같으면 2 반환</li>
<li>두 Point 변수의 멤버가 모두 같으면 0 반환</li>
<li>두 Point 변수의 멤버가 모두 다르면 -1 반환</li>
</ul>
<p>이렇게 해서 구조체 Point와 구조체 Point 관련 함수들의 선언 및 정의를 다음 헤더파일과 소스파일에 나누어 저장했다.</p>
<pre><code class="language-c">// Point.h
</code></pre>
<h3 id="리스트에-구조체-변수-저장하기-2-구조체-point와-관련함수들의-정의"><em>리스트에 구조체 변수 저장하기 2: 구조체 Point와 관련함수들의 정의</em></h3>
<p>배열 기반 리스트 코드에서 Point 구조체 변수의 주소 값을 저장할 수 있도록 Point 구조체 관련하여 약간의 수정을 해줘야한다.</p>
<ol>
<li><code>#include &quot;Point.h&quot;</code> 추가</li>
<li><code>typedef int LData;</code> 👉 <code>typedef Point * LData;</code> 변경</li>
</ol>
<p>그리고 Point 구조체 변수에 대해 실행할 main 함수를 구현하면 아래와 같다.</p>
<pre><code class="language-c">//PointListMain.c
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &quot;ArrayList.h&quot;
#include &quot;Point.h&quot;

int main()
{
    List list;
    Point compPos;
    Point * ppos;

    ListInit(&amp;list);

    // 4개의 데이터 저장 ///////
    ppos = (Point*)malloc(sizeof(Point));
    SetPointPos(ppos, 2, 1);
    LInsert(&amp;list, ppos);

    ppos = (Point*)malloc(sizeof(Point));
    SetPointPos(ppos, 2, 2);
    LInsert(&amp;list, ppos);

    ppos = (Point*)malloc(sizeof(Point));
    SetPointPos(ppos, 3, 1);
    LInsert(&amp;list, ppos);

    ppos = (Point*)malloc(sizeof(Point));
    SetPointPos(ppos, 3, 2);
    LInsert(&amp;list, ppos);

    // 저장된 데이터의 출력 ///////
    printf(&quot;Now number of data: %d \n&quot;, LCount(&amp;list));

    if(LFirst(&amp;list, &amp;ppos))
    {
        ShowPointPos(ppos);

        while(LNext(&amp;list, &amp;ppos))
            ShowPointPos(ppos);
    }
    printf(&quot;\n\n&quot;);

    // xpos가 2인 모든 데이터 삭제 ///////
    compPos.xpos = 2;
    compPos.ypos = 0;

    if(LFirst(&amp;list, &amp;ppos))
    {
        if(PointComp(ppos, &amp;compPos)==1)
        {
            ppos=LRemove(&amp;list);
            free(ppos);
        }

        while(LNext(&amp;list, &amp;ppos))
        {
            if(PointComp(ppos, &amp;compPos)==1)
            {
                ppos=LRemove(&amp;list);
                free(ppos);
            }
        }
    }

    // 삭제 후 남은 데이터 전체 출력 ///////
    printf(&quot;Now number of data: %d \n&quot;, LCount(&amp;list));

    if(LFirst(&amp;list, &amp;ppos))
    {
        ShowPointPos(ppos);

        while(LNext(&amp;list, &amp;ppos))
            ShowPointPos(ppos);
    }
    printf(&quot;\n&quot;);
    return 0;
}

&gt; gcc .\PointListMain.c .\ArrayList.c .\Point.c
&gt; .\a.exe
&gt; 출력
Now number of data: 4 
[2, 1]
[2, 2]
[3, 1]
[3, 2]


Now number of data: 2 
[3, 1]
[3, 2]</code></pre>
<p>위 main 함수에서 LRemove 함수가 삭제된 데이터를 반환하도록 한 이유는 리스트에 저장한 데이터가 &#39;Point 구조체 변수의 주소 값&#39;이기 때문이다. 이 주소 값은 Point 구조체를 동적으로 할당한 결과이기 때문에, 반드시 free 함수를 통한 메로리의 해체과정을 거쳐야한다.
때문에 LRemove 함수처럼 데이터를 소멸하는 함수는 소멸된 데이터를 반환하도록 정의해야 한다.</p>
<h3 id="배열-기반-리스트이-장점과-단점"><em>배열 기반 리스트이 장점과 단점</em></h3>
<ul>
<li><p>장점 (배열의 일반적인 장점)</p>
<ul>
<li>데이터 참조가 쉽다. 인덱스 값 기준으로 어디든 한 번에 참조가 가능하다.</li>
</ul>
</li>
<li><p>단점 (배열의 일반적인 단점)</p>
<ul>
<li>배열의 길이가 초기에 결정되어야 한다. 변경이 불가능하다.</li>
<li>삭제의 과정에서 데이터의 이동(복사)가 매우 빈번히 일어난다.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="review"><code>&lt;Review&gt;</code></h2>
<p>드디어 처음으로 C언어로 자료구조를 구현해봤다.
생각보다 Django에서 기능하나를 만들 때 어떤 기능인지 먼저 설정하고 이 기능에 필요한 것들을 model, serializer 등 하나하나 세워서 만드는 느낌과 비슷하다 생각이 들었다.
파이썬에서 문장은 이해하는데 머리 속으로 한번에 들어오지 않았는데 이걸 배우고 다시 본다면 다시 이해하기 쉬울지도...!
연결 리스트는 아직 안배웠으니 쉬운 걸 수도 있다 하핫
트리랑 그래프까지 할 수 있도록 화이팅이다~!</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/39df7adf-37cf-4c98-83b0-03aa906d3105/image.png" alt=""></p>
<p>(Point 구조체 사용할 때 기존 ArrayList.h를 수정하는 과정에서 <code>typedef Point * LData</code>에서 <code>*</code>를 빼먹어서 오류를 계속 찾는데 시간을 쏟았다.
앞으로 코드를 작성할 때 더 신중하고 꼼꼼하게 봐야겠다.)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chapter 02. 재귀(Recursion)]]></title>
            <link>https://velog.io/@mingming_eee/datastructure-02</link>
            <guid>https://velog.io/@mingming_eee/datastructure-02</guid>
            <pubDate>Thu, 10 Oct 2024 07:01:12 GMT</pubDate>
            <description><![CDATA[<h2 id="02-1-함수의-재귀적-호출의-이해">02-1. 함수의 재귀적 호출의 이해</h2>
<p>재귀는 자료구조와 알고리즘에 있어서 매우 중요한 요소이고, C언어는 재귀를 지원하는 언어다.
열혈 c프로그래밍에서 재귀의 동작방식을 이해하는데 중점을 두었다면 이번에는 재귀의 적용을 중심으로 배울 것이다.</p>
<h3 id="재귀함수의-기본적인-이해"><em>재귀함수의 기본적인 이해</em></h3>
<p>C 프로그래밍에서 배운 재귀함수를 다시 한번 살펴보자.</p>
<ul>
<li><p>재귀함수의 호출 원리
<img src="https://velog.velcdn.com/images/mingming_eee/post/7e5f66ad-3095-4dbf-bf15-9b57cbdeed13/image.png" alt=""></p>
<p>재귀함수란? 함수 내에서 자기 자신을 다시 호출하는 함수를 의미한다.
위 그림에서 Recursive 함수가 호출되면, Recursive 함수의 복사본이 만들어져서 복사본이 실행되는 구조로 재귀함수의 호출을 설명하고 있다.
Recursive 함수를 실행하는 중간에 다시 Recursive 함수가 호출되면, Recursive 함수의 복사본을 하나 더 만들어서 복사본을 실행하게 된다.
실제로 함수를 구성하는 명령문은 CPU로 이동이 되어서(복사가 되어서) 실행이 된다. 그런데 이 명령문은 얼마든지 CPU로 이동(복사)이(가) 가능하다. 따라서 Recursive 함수의 중간쯤 위치한 명령문을 실행하다가 함수의 실행을 완료하지 않은 상태에서 다시 Recursive 함수의 앞 부분에 위치한 명령문을 CPU로 이동시키는 것은 문제가 되지 않는다.
다음 예제를 통해 재귀의 탈출조건을 추가해서 재귀에 대해 이해해보자.</p>
</li>
</ul>
<pre><code class="language-c">#include &lt;stdio.h&gt;

void Recursive(int num)
{
    if (num &lt;=0)
        return;
    printf(&quot;Recursive call! %d\n&quot;, num);
    Recursive(num - 1);
}

int main()
{
    Recursive(3);
    return 0;
}

&gt; 출력
Recursive call! 3
Recursive call! 2
Recursive call! 1</code></pre>
<p>위에서 재귀의 탈출조건으로 Recursive(0)이 되면서 함수가 반환하기 시작한다.</p>
<h3 id="재귀함수의-디자인-사례"><em>재귀함수의 디자인 사례</em></h3>
<p>재귀함수는 자료구조나 알고리즘의 어려운 문제를 단순화하는데 사용되는 중요한 도구이다.
무엇보다도 재귀함수가 있기에 재귀적인 수학적 수식을 그대로 코드로 옮길 수 있다.
이러한 재귀함수의 특징을 보여주는 사례가 팩토리얼(factorial)값을 반환하는 함수이다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/d356f16c-28c8-4f20-adbf-155ca95f9f93/image.png" alt=""></p>
<p>위 그림에서 보듯 정수 n 팩토리얼은 정수 n과 n-1 팩토리얼의 곱으로 표현할 수 있으며 n 팩토리얼 $$f(n)$$은 수식적으로 다음과 같이 표현할 수 있다.</p>
<img src="https://velog.velcdn.com/images/mingming_eee/post/c141967b-c2dd-47bf-9888-d668fb3e688c/image.png" width=40%>

<p>이를 코드로 옮기게 되면 아래와 같이 표현할 수 있다.</p>
<pre><code class="language-c">if(n==0)
    return 1;
else    // n&gt;=1의 경우
    return n*Factorial(n-1);</code></pre>
<p>예제로 정리해서 실제로 잘 작동하는지 확인해보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int Factorial(int n)
{
    if(n==0)
        return 1;
    else
        return n*Factorial(n-1);
}

int main()
{
    printf(&quot;1! = %d\n&quot;, Factorial(1));
    printf(&quot;2! = %d\n&quot;, Factorial(2));
    printf(&quot;3! = %d\n&quot;, Factorial(3));
    printf(&quot;4! = %d\n&quot;, Factorial(4));
    printf(&quot;9! = %d\n&quot;, Factorial(9));
    printf(&quot;0! = %d\n&quot;, Factorial(0));

    return 0;
}

&gt; 출력
1! = 1
2! = 2
3! = 6
4! = 24
9! = 362880
0! = 1</code></pre>
<hr>
<h2 id="02-2-재귀의-활용">02-2. 재귀의 활용</h2>
<p>재귀함수는 이해하는 것도 중요하지만 잘 정의하는 것도 중요하다.
재귀함수를 잘 정의하기 위해서는 사고의 전환이 필요하다.</p>
<h3 id="피보나치-수열-fibonacci-sequence"><em>피보나치 수열: Fibonacci Sequence</em></h3>
<p>피보나치 수열은 재귀적인 형태를 띠는 대표적인 수열로서 다음과 같이 전개된다.</p>
<p>0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ....</p>
<p><strong>&#39;앞에 값 두 개를 더해서 현재의 수를 만들어가는 수열&#39;</strong>이 피보나치 수열이다.
수열의 $$n$$번째 값 $$=$$ 수열의 $$n-1$$번째 값 $$+$$ 수열의 $$n-2$$번째 값</p>
<p>피보나치 수열의 $$n$$번째 위치의 값을 반환하는 함수의 수학적 표현은 다음과 같다.</p>
<img src="https://velog.velcdn.com/images/mingming_eee/post/1668200d-b940-4bac-adfd-31e6e3a841d2/image.png" width=60%>

<p>이를 코드로 옮겨보자.</p>
<pre><code class="language-c">int Fibo(int n)
{
    if(n==1)
        return 0;
    else if(n==2)
        return 1;
    else
        return Fibo(n-1) + Fibo(n-2);
}</code></pre>
<p>코드로 잘 옮겨졌는지 다음 예제를 통해 알아보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int Fibo(int n)
{
    if(n==1)
        return 0;
    else if(n==2)
        return 1;
    else
        return Fibo(n-1) + Fibo(n-2);
}

int main()
{
    int i;
    for(i=1; i&lt;15; i++)
        printf(&quot;%d &quot;, Fibo(i));

    return 0;
}

&gt; 출력
0 1 1 2 3 5 8 13 21 34 55 89 144 233 </code></pre>
<p>위 예제를 함수의 호출순서에 대해 이해하기 위해서 약간의 코드를 수정해보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int Fibo(int n)
{
    printf(&quot;func call param %d \n&quot;, n);

    if(n==1)
        return 0;
    else if(n==2)
        return 1;
    else
        return Fibo(n-1) + Fibo(n-2);
}

int main()
{
    Fibo(7);
    return 0;
}

&gt; 출력
func call param 7 
func call param 6
func call param 5
func call param 4
func call param 3
func call param 2
func call param 1 
func call param 2
func call param 3
func call param 2
func call param 1
func call param 4
func call param 3
func call param 2
func call param 1
func call param 2
-----------------
func call param 5
func call param 4
func call param 3
func call param 2
func call param 1
func call param 2 
func call param 3
func call param 2
func call param 1</code></pre>
<p>출력을 보면 알 수 있듯 재귀함수는 매우 많은 수의 함수호출을동반한다. 피보나치 수열의 7번째 값의 출력을 위해서도 25회의 함수호출이 동반되었다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/fe25278a-3b3c-492f-b71d-218a017d9afa/image.png" alt=""></p>
<p>위 그림이 출력에서 점선 위까지의 호출을 도식화 한 것이다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/4b157bcb-1c45-4f71-94b9-925b01fa8355/image.png" alt=""></p>
<p>이 그림이 점선 아래에 해당하는 부분이다.</p>
<p>이렇게 호출순서를 정리하는 것은 큰 의미가 없기 때문에 재귀함수 자체를 이해하는 것이 좋다.</p>
<h3 id="이진-탐색-알고리즘의-재귀적-표현"><em>이진 탐색 알고리즘의 재귀적 표현</em></h3>
<p>이번에는 Chapter 01에서 구현한 이진 탐색 알고리즘을 재귀함수 기반으로 재구현해보려 한다.
먼저, 이진 탐색 알고리즘의 반복 패턴을 정리해보자.</p>
<ol>
<li>탐색 범위의 중앙에 목표 값이 저장되었는지 확인</li>
<li>저장되지 않았다면 탐색 범위를 반으로 줄여서 다시 탐색 시작</li>
</ol>
<p>그리고 탐색의 실패가 결정되는 시점은 first(탐색 시작 위치)가 last(탐색 범위의 끝)보다 커지는 경우다.
탈출조건을 먼저 코드로 정리해보자.</p>
<pre><code class="language-c">int BSearchRecur(int ar[], int first, int last, int target)
{
    if(first &gt; last)
        return -1;        // -1의 반환 = 탐색의 실패
}</code></pre>
<p>그리고 탐색 알고리즘의 반복 패턴 1인 &quot;탐색 범위의 중앙에 목표 값이 저장되었는지 확인&quot;하는 코드를 넣어본다.</p>
<pre><code class="language-c">int BSearchRecur(int ar[], int first, int last, int target)
{
    int mid;

    if(first &gt; last)
        return -1;

    mid = (first+last)/2;    // 탐색 대상의 중앙 찾기.
    if(ar[mid]==target)
        return mid;            // 탐색된 타겟의 인덱스 값 반환.
}</code></pre>
<p>그리고 그 다음 반복 패턴인 &quot;저장되지 않았다면 탐색 범위를 반으로 줄여서 다시 탐색 시작&quot; 코드를 삽입해보자.</p>
<pre><code class="language-c">int BSearchRecur(int ar[], int first, int last, int target)
{
    int mid;

    if(first &gt; last)
        return -1;

    mid = (first+last)/2;    // 탐색 대상의 중앙 찾기.
    if(ar[mid]==target)
        return mid;            // 탐색된 타겟의 인덱스 값 반환.
    else if(target &lt; ar[mid])
        return BSearchRecur(ar, first, mid-1, target);
    else
        return BSearchRecur(ar, mid+1, last, target);
}</code></pre>
<p>여기서 추가된 부분 <code>return BSearchRecur(ar, first, mid-1, target);</code>와 <code>return BSearchRecur(ar, mid+1, last, target);</code>이 재귀의 핵심이다.</p>
<p>실제로 탐색이 잘 이루어지는지 다음 예제를 통해 알아보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int BSearchRecur(int ar[], int first, int last, int target)
{
    int mid;

    if(first &gt; last)
        return -1;            // -1의 반환 = 탐색의 실패

    mid = (first+last)/2;    // 탐색 대상의 중앙 찾기.
    if(ar[mid]==target)
        return mid;            // 탐색된 타겟의 인덱스 값 반환.
    else if(target &lt; ar[mid])
        return BSearchRecur(ar, first, mid-1, target);
    else
        return BSearchRecur(ar, mid+1, last, target);
}

int main()
{
    int arr[] = {1, 3, 5, 7, 9};
    int idx;

    idx = BSearchRecur(arr, 0, sizeof(arr)/sizeof(int)-1, 7);
    if(idx==-1)
        printf(&quot;탐색 실패 \n&quot;);
    else
        printf(&quot;타겟 저장 인덱스: %d \n&quot;, idx);

    idx = BSearchRecur(arr, 0, sizeof(arr)/sizeof(int)-1, 4);
    if(idx==-1)
        printf(&quot;탐색 실패 \n&quot;);
    else
        printf(&quot;타겟 저장 인덱스: %d \n&quot;, idx);

    return 0;
}

&gt; 출력
타겟 저장 인덱스: 3 
탐색 실패</code></pre>
<hr>
<h2 id="02-3-하노이-타워-the-tower-of-hanoi">02-3. 하노이 타워: The Tower of Hanoi</h2>
<p>하노이 타워 문제는 &#39;하나의 막대에 쌓여 있는 원반을 다른 하나의 원반에 그대로 옮기는 방법&#39;에 관한 것이다. 이때 조건이 있다. 원반은 한 번에 하나씩만 옮길 수 있고, 옮기는 과정에서 작은 원반 위에 큰 원반이 올려질 수 없다는 것이다.
그래서 막대는 3개로 이루어져 있고 간단하게 3개의 원반을 옮기는 과정은 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/5d49d34b-2188-48cf-b081-48a2f9fa8628/image.png" alt="">
<img src="https://velog.velcdn.com/images/mingming_eee/post/353691c0-7324-4497-9dba-788a043e8a77/image.png" alt="">
<img src="https://velog.velcdn.com/images/mingming_eee/post/a4075efa-212c-4df7-ba20-96abbfd85561/image.png" alt="">
<img src="https://velog.velcdn.com/images/mingming_eee/post/d494281e-f136-4e99-b219-7957f9603413/image.png" alt="">
<img src="https://velog.velcdn.com/images/mingming_eee/post/17d5ab43-43c1-4e76-a0ae-98caacfbec52/image.png" alt=""></p>
<p>3개의 원반은 이렇게 생각하기 쉽지만 이 갯수가 많아진다면 어려워진다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/da9b2486-1842-4a68-b901-d13f445c3d2f/image.png" alt=""></p>
<p>하지만, 문제의 해결을 위해 생각해보면 3번 원반을 c로만 옮기기 위해 1, 2번 원반을 B 막대로 옮길 수 만 있다면 문제는 해결이 된다.</p>
<h3 id="하노이-타워의-반복패턴-연구"><em>하노이 타워의 반복패턴 연구</em></h3>
<p>그럼 이번엔 원반 4개를 생각해보자.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/6e6c5c76-16ec-4333-bcfb-b1c63711c565/image.png" alt=""></p>
<p>A 막대에서 원반 4개를 C 막대로 옮기기 위해서는 1, 2, 3원반을 우선 B막대로 옮겨야한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/91c420bb-4900-4579-9f00-354508b1f264/image.png" alt=""></p>
<p>그 다음 1, 2, 3원반을 C막대로 옮기면 된다. 이를 일반화해서 막대 A에 꽂혀있는 원반 $$n$$개를 막대 C로 옮기는 과정을 정리해보자.</p>
<ol>
<li>작은 원반 n-1개를 A에서 B로 이동</li>
<li>큰 원반 1개를 A에서 C로 이동</li>
<li>작은 원반 n-1개를 B에서 C로 이동</li>
</ol>
<h3 id="하노이-타워-문제의-해결"><em>하노이 타워 문제의 해결</em></h3>
<p>위에서 정리한 과정을 코드로 나타내보자.
우선 num개의 원반을 by를 거쳐서(by를 이용해서) from에서 to로 이동한다.</p>
<pre><code class="language-c">// from에 꽂혀있는 num개의 원반을 by를 거쳐서 to로 이동
void HanoiTowerMove(int num, char from, char by, char to)
{
    ...
}</code></pre>
<p>탈출 조건으로는 원반이 1개일 때 from에서 to로 이동시키면 된다.</p>
<pre><code class="language-c">// from에 꽂혀있는 num개의 원반을 by를 거쳐서 to로 이동
void HanoiTowerMove(int num, char from, char by, char to)
{
    if(num==1)    // 이동할 원반의 수가 1개
    {
        printf(&quot;원반1을 %c에서 %c로 이동 \n&quot;, from , to);
    }
    else
    {
        ...
    }
}</code></pre>
<p>그 다음 작은 원반 $$n-1$$개를 A에서 B로 이동을 코드로 추가해보자.</p>
<pre><code class="language-c">// from에 꽂혀있는 num개의 원반을 by를 거쳐서 to로 이동
void HanoiTowerMove(int num, char from, char by, char to)
{
    if(num==1)    // 이동할 원반의 수가 1개
    {
        printf(&quot;원반1을 %c에서 %c로 이동 \n&quot;, from , to);
    }
    else
    {
        HanoiTowerMove(n-1, from, to, by);    // 3단계 중 1단계
    }
}</code></pre>
<p>위에서 to 인자가 by로 전달되고, by인자가 to로 전달되고 있다. 이는 $$n-1$$개의 원반을 A에서 B로 우선 이동시켜야하기 때문이다.
그 다음 큰 원반 1개를 A에서 C로 이동하는 코드, 작은 원반 $$n-1$$개를 B에서 C로 이동하는 코드를 추가해보자.</p>
<pre><code class="language-c">// from에 꽂혀있는 num개의 원반을 by를 거쳐서 to로 이동
void HanoiTowerMove(int num, char from, char by, char to)
{
    if(num==1)    // 이동할 원반의 수가 1개
    {
        printf(&quot;원반1을 %c에서 %c로 이동 \n&quot;, from , to);
    }
    else
    {
        HanoiTowerMove(num-1, from, to, by);    // 3단계 중 1단계
        printf(&quot;원반%d을(를) %c에서 %c로 이동 \n&quot;, num, from, to);    // 3단계 중 2단계
        HanoiTowerMove(num-1, by, from, to);    // 3단계 중 3단계
    }
}</code></pre>
<p>이로써 하노이 타워의 문제를 해결하는 재귀함수가 완성이 되었다.
다음 예제에서 잘 작동하는지 확인해보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

void HanoiTowerMove(int num, char from, char by, char to)
{
    if(num==1)    // 이동할 원반의 수가 1개
    {
        printf(&quot;원반1을 %c에서 %c로 이동 \n&quot;, from , to);
    }
    else
    {
        HanoiTowerMove(num-1, from, to, by);    // 3단계 중 1단계
        printf(&quot;원반%d을(를) %c에서 %c로 이동 \n&quot;, num, from, to);    // 3단계 중 2단계
        HanoiTowerMove(num-1, by, from, to);    // 3단계 중 3단계
    }
}

int main()
{
    // 막대 A의 원반 3개를 막대 B를 경유해서 막대 C로 옮기기
    HanoiTowerMove(3, &#39;A&#39;, &#39;B&#39;, &#39;C&#39;);
    return 0;
}

&gt; 출력
원반1을 A에서 C로 이동 
원반2을(를) A에서 B로 이동 
원반1을 C에서 B로 이동
원반3을(를) A에서 C로 이동
원반1을 B에서 A로 이동
원반2을(를) B에서 C로 이동
원반1을 A에서 C로 이동</code></pre>
<hr>
<h2 id="review"><code>&lt;Review&gt;</code></h2>
<p>이렇게 c언어로 재귀함수까지 알아봤다.
python으로 할 때보다 이해가 더 빨리 되는 것은 언어가 단순해서 그런가... 
아니면 두 번째로 배워서 그런가...
오늘도 알차고 재밌었다!</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/8b92a8d7-139b-4065-9f44-c5c20c32959d/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chapter 01. 자료구조와 알고리즘의 이해]]></title>
            <link>https://velog.io/@mingming_eee/datastructure-01</link>
            <guid>https://velog.io/@mingming_eee/datastructure-01</guid>
            <pubDate>Thu, 10 Oct 2024 04:53:12 GMT</pubDate>
            <description><![CDATA[<h2 id="intro"><code>&lt;Intro&gt;</code></h2>
<p>오랜만에 돌아온 기술 블로그<del>🖐🏻
인턴십 마무리와 댄스팀 촬영, 적절한 휴식기를 가지고 다시 공부 시작</del>
오늘 <a href="https://www.ssafy.com/ksp/servlet/swp.board.controller.SwpBoardServlet?p_process=select-board-view&amp;p_tabseq=226504&amp;p_seq=142">SSAFY 13기 모집 공고💻</a>가 떴다!
내년에 SSAFY에서 교육을 듣는다면 내후년에 취업은 문제 없지 않을까...?!</p>
<p>c언어-python-Java 가보자구~~✨</p>
<p>우선 오늘은 C언어를 사용하여 자료구조를 배워보자~!</p>
<hr>
<h3 id="01-1-자료구조data-structure에-대한-기본적인-이해">01-1 &quot;자료구조(Data Structure)에 대한 기본적인 이해&quot;</h3>
<h4 id="c언어의-문법과-관련해서-알고-있어야-하는-부분"><em>C언어의 문법과 관련해서 알고 있어야 하는 부분</em></h4>
<p>우선 C언어를 사용해서 자료구조를 배우기 위해서는 아래와 같은 부분은 알고 있다고 가정하고 진행하려한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/126b04bf-807b-4454-83e1-37d1ced93beb/image.png" alt=""></p>
<p>나와 같이 C언어를 학습했다면 위에 부분은 어느정도 기초 지식을 가지고 이해가 보다 쉬울 수 있다.
어려운 분들이 계신다면 <a href="https://velog.io/@mingming_eee/series/C%EC%96%B8%EC%96%B4">열혈 C언어📚</a>에서 문법을 확인하면서 같이 알아가는 것도 좋을 것 같다.</p>
<h4 id="자료구조란"><em>자료구조란?</em></h4>
<p>자료구조란? 프로그램이란 데이터를 표현하고, 그렇게 표현된 데이터를 처리하는 것이다.
위에서 말한 데이터를 표현은 데이터 저장을 포함하는 개념이고 데이터의 저장을 담당하는 것이 바로 자료구조이다.
넓은 의미에서 int형 변수도, 구조체의 정의도 자료구조에 속한다. (배열 또한 동일하다.)</p>
<p>이러한 자료구조는 기본적으로 다음과 같이 분류할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/4c268dca-cff4-4fff-849d-7ee82708dd11/image.png" alt=""></p>
<p>이 중에서 <code>선형구조</code>와 <code>비선형구조</code>에 대해 배울 것이다.
<code>선형 자료구조</code>는 데이터를 선의 형태로 나란히 혹은 일렬로 저장하는 방식이다.
<code>비선형 자료구조</code>는 데이터를 나란히 저장하지 않는 구조다.
따라서 선형 자료구조에 비해 비선형 자료구조는 상대적으로 어렵다.</p>
<h4 id="알고리즘이란"><em>알고리즘이란?</em></h4>
<p>자료구조가 데이터의 표현 및 저장방법을 뜻한다면, 알고리즘은 표현 및 저장된 데이터를 대상으로 하는 문제의 해결 방법을 뜻한다.
따라서 자료구조가 결정되어야 그에 따른 효율적인 알고리즘을 결정할 수 있고,
자료구조에 따라 알고리즘이 달라진다.</p>
<hr>
<h3 id="01-2-알고리즘의-성능분석-방법">01-2 &quot;알고리즘의 성능분석 방법&quot;</h3>
<p>알고리즘의 성능분석을 위해서는 지수식($$y=2^x$$)과 로그식($$=log_2x$$)을 알고 있어야한다.</p>
<h4 id="시간-복잡도time-complexity와-공간-복잡도space-complexity">*시간 복잡도(Time Complexity)와 공간 복잡도(Space Complexity)</h4>
<p>좋은 성능의 자료구조와 알고리즘을 분석하고 평가하기 위해서 시간 복잡도와 공간 복잡도를 활용한다.
<code>시간 복잡도(Time Complexity)</code>는 속도에 해당하는 알고리즘의 수행시간 분석결과를 가리키며,
<code>공간 복잡도(Space Complexity)</code>는 메모리 사용량에 대한 분석결과를 가리킨다.
일반적으로 알고리즘을 평가할 때는 메모리의 사용량보다 실행속도에 초점을 둔다.
(메모리는 이전에 그 크기가 작았기 때문에 메모리의 사용량에 대한 중요도가 높았다면 메모리 용량이 큰 요즘에는 그 중요도가 조금 낮아졌다 볼 수 있다.)</p>
<p>그렇다면 알고리즘의 수행속도를 어떻게 알 수 있을까?
우선 연산의 횟수를 세고, 처리해야할 데이터의 수 $$n$$에 대한 연산횟수의 $$T(n)$$을 구성한다.
이렇듯 연산 횟수에 대한 식을 구성하면 데이터 수의 증가에 따른 연산횟수의 변화 정도를 판단할 수 있다. 또한, 둘 이상의 알고리즘을 비교하기 용이하다.</p>
<h4 id="순차-탐색-알고리즘linear-search-algorithm과-시간-복잡도-분석의-핵심요소"><em>순차 탐색 알고리즘(Linear Search Algorithm)과 시간 복잡도 분석의 핵심요소</em></h4>
<p><code>순차 탐색 알고리즘(Linear Search Algorithm)</code>에 대한 간단한 탐색 알고리즘을 예제로 살펴보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int LSearch(int ar[], int len, int target)    // 순차 탐색 알고리즘 적용된 함수
{
    int i;
    for (i = 0; i &lt; len; i++)
        if (ar[i] == target)
            return i;    // 찾은 대상의 인덱스 값 반환
    return -1;    // 찾지 못했음을 의미하는 값 반환
}

int main()
{
    int arr[] = { 3, 5, 2, 4, 9 };
    int idx;

    idx = LSearch(arr, sizeof(arr) / sizeof(int), 4);
    if (idx == -1)
        printf(&quot;탐색 실패 \n&quot;);
    else
        printf(&quot;타겟 저장 인덱스: %d \n&quot;, idx);

    idx = LSearch(arr, sizeof(arr) / sizeof(int), 7);
    if (idx == -1)
        printf(&quot;탐색 실패 \n&quot;);
    else
        printf(&quot;타겟 저장 인덱스: %d \n&quot;, idx);

    return 0;
}

&gt; 출력
타겟 저장 인덱스: 3 
탐색 실패</code></pre>
<p>위 예제는 맨 앞에서부터 순서대로 탐색을 진행하는 알고리즘이기때문에 순차 탐색이라는 이름이 붙었다.
순차 탐색 알고리즘을 실제로 구현하는 코드만 별도로 본다면 아래와 같다.</p>
<pre><code class="language-c">for (i = 0; i &lt; len; i++)
    if (ar[i] == target)
        return i;    // 찾은 대상의 인덱스 값 반환
return -1;    // 찾지 못했음을 의미하는 값 반환</code></pre>
<p>위 코드를 토대로 시간 복잡도를 분석해서 데이터의 수 n에 대한 연산횟수의 함수 $$T(n)$$을 구하기 위해서는 값의 동등을 비교하는 <code>==</code>연산 횟수를 대상으로 분석하면 된다.
더불어 동등 비교 연산을 적게 수행하는 탐색 알고리즘이 좋은 탐색 알고리즘이다.</p>
<p>모든 알고리즘에는 가장 행복한 경우(운이 좋은 경우)와 가장 우울한 경우(운이 없는 경우)가 있는데 이를 <code>최선의 경우(best case)</code> 그리고 <code>최악의 경우(worst case)</code>라 한다.
알고리즘의 시간 복잡도를 분석할 때는 최악의 경우를 생각하는 것이 중요하다.
최선의 경우와 최악의 경우의 평균을 구하는 것도 좋은 방법일 수 있지만,
어떤 것이 평균적인 상황인지는 다양한 자료들이 수집되어야 하기 때문에 최악의 경우를 시간 복잡도로 선택하게 된다.</p>
<h4 id="순차-탐색-알고리즘-시간-복잡도-계산-1-최악의-경우worst-case"><em>순차 탐색 알고리즘 시간 복잡도 계산 1: 최악의 경우(worst case)</em></h4>
<p>위에서 소개한 예제를 기준으로 최악의 경우를 살펴보자면 찾으려는 값이 배열에서 마지막에 있을 경우로 &quot;데이터 수가 $$n$$개 일때 최악의 경우에 해당하는 연산횟수(비교연산 횟수는) $$n$$이다.&quot;</p>
<p>따라서 시간 복잡도는 $$T(n) = n$$이 된다.</p>
<h4 id="순차-탐색-알고리즘-시간-복잡도-계산-2-평균적인-경우average-case"><em>순차 탐색 알고리즘 시간 복잡도 계산 2: 평균적인 경우(average case)</em></h4>
<p>평균적인 경우를 대상으로 $$T(n)$$함수를 정의해볼 것이다.
평균적인 경우의 연산횟수 계산을 위해 다음 두 가지 가정을 할 것이다.</p>
<ul>
<li>가정 1. 탐색 대상이 배열에 존재하지 않을 확률을 50%라고 가정한다.</li>
<li>가정 2. 배열의 첫 요소부터 마지막 요소까지, 탐색 대상이 존재할 확률은 동일하다.</li>
</ul>
<p>그럼 배열에 탐색 대상이 존재하는 경우와 존재하지 않는 경우를 나눠서 연산횟수를 계산할 것이고 우선 배열에 탐색 대상이 존재하지 않는 경우를 생각해보자.
데이터의 수가 n개일 때, 총 n번의 비교연산을 진행한다.
따라서 탐색 대상이 존재하지 않는 경우의 연산 횟수는 $$n$$이다.</p>
<p>이번에는 탐색 대상이 존재하는 경우의 연산횟수를 계산해 보자.
이 경우에는 $$n \over 2$$가 된다.</p>
<p>탐색 대상이 존재하지 않을 확률과 존재할 경우 확률이 각각 50%이기 때문에
순차 탐색 알고리즘의 평균적인 경우의 시간 복잡도 함수는 다음과 같다.
$$T(n) = n×{1 \over 2}+{n \over 2}×{1 \over 2} = {3 \over 4}n$$</p>
<p>최악의 경우에 비해 평균적인 경우 시간 복잡도 계산이 더 오래 걸리고 신뢰도가 높지 않다. 앞서 정의한 두 개의 가정을 뒷받침할 근거가 부족하기 때문이다.
따라서, 시간 복잡도는 &#39;최악의 경우&#39;를 기준으로 계산된다.</p>
<h4 id="이진-탐색-알고리즘binary-search-algorithm">*이진 탐색 알고리즘(Binary Search Algorithm)</h4>
<p>이진 탐색 알고리즘은 순차 탐색보다 훨씬 좋은 성능을 보인다. 단, <strong>정렬된 데이터가 아니면 적용이 어렵다</strong>는 조건이 있다.</p>
<p>이진 탐색 알고리즘의 원리를 알기 위해서 우선 길이가 9인 배열을 다음과 같이 정렬된 상태로 데이터가 저장되어 있다 생각하자.
배열 이름은 arr이고, 이 배열을 대상으로 숫자 3이 저장되어 있는지 확인하기 위해서 이진 탐색 알고리즘을 사용할 것이다.</p>
<p>이진 탐색 알고리즘의 첫 번째 시도는 다음과 같다.</p>
<p>👉이진 탐색 알고리즘의 첫 번째 시도:</p>
<ol>
<li>배열 인덱스의 시작과 끝은 각각 0과 8이다.</li>
<li>0과 8을 합하여 그 결과를 2로 나눈다.</li>
<li>2로 나눠서 얻은 결과 4를 인덱스 값으로 하여 arr[4]에 저장된 값이 3인지 확인한다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/1b0994a4-53d2-4ebe-a79d-b5175dca8a39/image.png" alt=""></p>
<p>첫 번째 시도를 통해 arr[4]에 3이 저장되지 않았음을 확인했다.
두 번째 시도를 진행한다.</p>
<p>👉이진 탐색 알고리즘의 두 번째 시도:</p>
<ol>
<li>arr[4]에 저장된 값 9와 탐색 대상 3의 대소를 비교한다.</li>
<li>대소의 비교결과는 arr[4]&gt;3이므로 탐색의 범위를 인덱스 기준 0~3으로 제한한다.</li>
<li>0과 3을 더하여 그 결과를 2로 나눈다. 이때 나머지는 버린다.</li>
<li>2로 나눠서 얻은 결과가 1이니 arr[2]에 저장된 값이 3인지 확인한다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/546fd9af-1f3c-4186-9ddb-4d50e68f52f0/image.png" alt=""></p>
<p>이진 탐색 알고리즘의 핵심이 여기서 나온다. 탐색의 대상을 절반으로 줄였다는 것이다.</p>
<p>arr[1]에도 3이 저장되어 있지 않았으니 두 번째 시도와 비슷하게 세 번째 시도를 진행한다.</p>
<p>👉이진 탐색 알고리즘의 세 번째 시도:</p>
<ol>
<li>arr[1]에 저장된 값 2와 탐색 대상인 3의 대소를 비교한다.</li>
<li>대소의 비교결과는 arr[1]&lt;3이므로 탐색의 범위를 인덱스 기준 2~3으로 제한한다.</li>
<li>2와 3을 더하여 그 결과를 2로 나눈다. 이때 나머지는 버린다.</li>
<li>2로 나눠서 얻은 결과가 2이니 arr[2]에 저장된 값이 3인지 확인한다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/26858b50-4beb-4e7d-932a-63e749c7bc29/image.png" alt=""></p>
<p>세 번째 시도에서 탐색 대상인 3을 찾게 되어 탐색은 마무리 되었다.</p>
<p>이렇듯 이진 탐색 알고리즘은 탐색의 대상을 반복해서 반씩 제외하는 방법의 알고리즘이다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/6c0692bd-bbde-44da-b8a9-8477a242aab3/image.png" alt=""></p>
<h4 id="이진-탐색-알고리즘의-구현"><em>이진 탐색 알고리즘의 구현</em></h4>
<p>이제 이진 탐색 알고리즘을 구현해보려고 한다.
구현하기 전에 탐색의 범위가 줄어드는 형태를 그림을 통해 다시 한번 상기시켜 보자.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/bf81df62-b2e7-4a44-994b-be20cb00cb0e/image.png" alt=""></p>
<p>탐색의 시작위치에 해당하는 인덱스 값을 first, 탐색의 마지막 위치에 해당하는 인덱스 값을 last로 표시하고 있다.
그림과 같이 이진 탐색 알고리즘이 진행됨에 따라 first와 last는 가까워진다. 이진 탐색 알고리즘은 first와 last가 만날 때까지 진행되며 first가 last보다 커지게 되면 탐색의 대상이 존재하지 않음을 뜻하기 때문에 탐색을 종료하게 된다.
따라서 구현하는 부분에서 while문의 조건이 <code>first &lt;= last</code>인 것을 명심하자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int BSearch(int ar[], int len, int target)
{
    int first = 0;    // 탐색 대상의 시작 인덱스 값
    int last = len - 1;    // 탐색 대상의 마지막 인덱스 값
    int mid;

    while (first &lt;= last)
    {
        mid = (first + last) / 2;    // 탐색 대상의 중앙을 찾기
        if (target == ar[mid])    //  중앙에 저장된 것이 타겟이라면
            return mid;    // 탐색 완료.
        else  // 타겟 탐색 안됐다면 탐색 대상 범위 반으로 줄이기
        {
            if (target &lt;= ar[mid])
                last = mid - 1;    // 중간값 보다 작을 경우 탐색 대상 마지막 인덱스를 중간값보다 하나 작게하여 범위 축소
            else
                first = mid + 1;    // 중간값 보다 클 경우 탐색 대상 시작 인덱스를 중간값보다 하나 많게하여 범위 축소
        }
    }
    return -1;    // 찾지 못했을 때 -1 반환
}

int main()
{
    int arr[] = { 1, 3, 5, 7, 9 };
    int idx;

    idx = BSearch(arr, sizeof(arr) / sizeof(int), 7);
    if (idx == -1)
        printf(&quot;탐색 실패 \n&quot;);
    else
        printf(&quot;타겟 저장 인덱스: %d \n&quot;, idx);

    idx = BSearch(arr, sizeof(arr) / sizeof(int), 4);
    if (idx == -1)
        printf(&quot;탐색 실패 \n&quot;);
    else
        printf(&quot;타겟 저장 인덱스: %d \n&quot;, idx);

    return 0;
}

&gt; 출력
타겟 저장 인덱스: 3 
탐색 실패 </code></pre>
<p>여기서 <code>last = mid - 1</code>이고 <code>first = mid + 1</code>인 이유는 mid에 저장된 인덱스 값의 배열요소도 새로운 탐색의 범위에 포함이 되어야하기 때문이다. 
단순히 <code>last = mid</code>이고 <code>first = mid</code>로 코드를 작성한다면 기본적으로 세 변수에 저장된 인덱스 값이 <code>first ≤ mid ≤ last</code>이기 때문에 first에 저장된 값이 last보다 커질 수 없게 된다.</p>
<h4 id="이진-탐색-알고리즘의-시간-복잡도-계산하기-최악의-경우-기준"><em>이진 탐색 알고리즘의 시간 복잡도 계산하기: 최악의 경우 기준</em></h4>
<p>이진 탐색 알고리즘의 일부를 보면서 연산횟수를 대표하는 연산이 무엇인지 찾아보자.</p>
<pre><code class="language-c">while (first &lt;= last)
{
    mid = (first + last) / 2;
    if (target == ar[mid])
        return mid;
    else
    {
        if (target &lt;= ar[mid])
            last = mid - 1;
        else
            first = mid + 1;
    }
}</code></pre>
<p>데이터의 수가 n개 일 때 최악의 경우 비교 연산이 총 몇번 이루어질까?</p>
<ul>
<li>$$n$$이 1이 되기까지 2로 나눈 횟수 $$k$$회, 비교연산 $$k$$회 진행.</li>
<li>데이터가 1개 남았을 때 마지막으로 비교연산 1회 진행</li>
</ul>
<p>👉$$T(n) = k + 1$$</p>
<p>$$k$$는 어떻게 구할까?</p>
<p>$$n × ({1\over2})^k = 1$$이기 때문에 $$k = log_2n$$이 된다.</p>
<p>따라서 최악의 경우에 대한 시간 복잡도 함수 $$T(n)$$은 $$log_2n$$이 된다.
정확하게 얘기하면 $$T(n) = log_2n + 1$$이 아닌가?라고 할 수 있다. 
시간 복잡도 자체가 데이터 수의 증가에 따른 연산횟수의 변화 정도를 판단하는 것이기 때문에 1은 전체 연산횟수에 대해 미치는 영향이 미미해 생략이 가능하다.</p>
<p>이 상수항의 생략과 관련된 것이 빅-오 표기법이다.</p>
<h4 id="빅-오-표기법big-oh-notation"><em>빅-오 표기법(Big-Oh Notation)</em></h4>
<p>빅-오 표기법이란 함수 $$T(n)$$에서 가장 영향력이 큰 부분을 표기하는 방법이다.
만약 시간 복잡도 함수가 $$T(n) = n^2 + 2n +1$$이라면
이를 빅-오 표기법으로 나타냈을 때 $$O(n^2)$$가 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/ff2ce3a7-4c26-4122-8a24-4a7e570ff91d/image.png" alt=""></p>
<p>n의 크기가 증가함에 따라 시간 복잡도 함수에서 가장 영향력이 있는 항은 최고차항이게 된다.
정리하자면, 시간 복잡도 함수 $$T(n) = n^2 + 2n +1$$의 빅오는 $$O(n^2)$$이며 $$n$$의 증가 및 감소에 따른 $$T(n)$$의 변화 정도가 $$n^2$$의 형태를 띈다.</p>
<h4 id="대표적인-빅-오"><em>대표적인 빅-오</em></h4>
<p>데이터 수의 증가에 따른 연산횟수의 증가 형태를 표현한 것이 빅-오이다 보니 대표적인 빅-오 표기는 다음과 같다.</p>
<ul>
<li><p>$$O(1)$$
: 상수형 빅-오로 데이터 수에 상관없이 연산횟수가 고정인 유형의 알고리즘이다.</p>
</li>
<li><p>$$O(logn)$$
: 로그형 빅-오로 데이터 수의 증가율에 비해서 연산횟수의 증가율이 훨씬 낮은 알고리즘을 의미한다. </p>
</li>
<li><p>$$O(n)$$
:  선형 빅-오로 데이터의 수와 연산횟수가 비례하는 알고리즘을 의미한다.</p>
</li>
<li><p>$$O(nlogn)$$
: 선형로그형 빅-오로 데이터의 수가 두배로 늘 때, 연산횟수는 두 배를 조금 넘게 증가하는 알고리즘을 의미한다.</p>
</li>
<li><p>$$O(n^2)$$
: 데이터 수의 제곱에 해당하는 연산횟수를 요구하는 알고리즘을 의미한다. 중첩된 반복문의 사용으로 알고리즘 디자인에서 그리 바람직한 것은 아니다.</p>
</li>
<li><p>$$O(n^3)$$
: 데이터 수의 세 제곱에 해당하는 연산횟수를 요구하는 알고리즘을 의미한다. 삼중으로 중첩된 반복문 내에서 알고리즘에 관련된 연산이 진행되는 경우 발생한다.</p>
</li>
<li><p>$$O(2^n)$$
: 지수형 빅-오로 사용한다는 것 자체가 비현실적인 알고리즘이다. </p>
</li>
</ul>
<p>지금까지 소개된 빅-오 표기들의 성능(수행시간, 연산횟수)의 대소를 정리하면 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/7686f657-1f19-40dc-b2a3-2f45d77e19f7/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/7f12cae3-e40f-4617-80b1-00470b086955/image.png" alt=""></p>
<h4 id="순차-탐색-알고리즘과-이진-탐색-알고리즘-비교"><em>순차 탐색 알고리즘과 이진 탐색 알고리즘 비교</em></h4>
<p>두 탐색 알고리즘의 빅-오를 판단해 비교해보자.
두 탐색 알고리즘의 시간 복잡도 함수 $$T(n)$$(최악의 경우)를 우선 살펴보자.</p>
<ul>
<li>$$T(n) = n$$ : 순차 탐색 알고리즘 $$T(n)$$함수</li>
<li>$$T(n) = log_2n+1$$ : 이진 탐색 알고리즘 $$T(n)$$함수</li>
</ul>
<p>따라서 빅-오 표기는 다음과 같다.</p>
<ul>
<li>$$O(n)$$ : 순차 탐색 알고리즘 빅-오</li>
<li>$$O(logn)$$ : 이진 탐색 알고리즘 빅-오</li>
</ul>
<p>이 두 빅-오 간의 성능 차이를 보기 위해 두 알고리즘의 비교연산횟수를 수치적으로 비교해볼 것이다.</p>
<ul>
<li>최악의 경우를 대상으로 비교하는 것이 목적이니 탐색의 실패를 유도한다.</li>
<li>탐색의 실패가 결정되기까지 몇 번의 비교연산이 진행되는지 센다.</li>
<li>데이터의 수는 500, 5000, 50000일 때를 기준으로 각각 실험을 진행한다.</li>
</ul>
<p>$$O(n)$$의 경우 데이터 수와 동일하게 비교연산 횟수가 진행되므로,
$$O(logn)$$의 경우를 예재를 통해서 살펴보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int BSearch(int ar[], int len, int target)
{
    int first = 0;
    int last = len - 1;
    int mid;
    int opCount = 0;    // 비교연산의 횟수를 기록

    while (first &lt;= last)
    {
        mid = (first + last) / 2;
        if (target == ar[mid])
            return mid;
        else
        {
            if (target &lt; ar[mid])
                last = mid - 1;
            else
                first = mid + 1;
        }
        opCount += 1;    // 비교연산의 횟수 1 증가
    }
    printf(&quot;비교연산횟수: %d \n&quot;, opCount);
    return -1;
}

int main()
{
    int arr1[500] = { 0, };
    int arr2[5000] = { 0, };
    int arr3[50000] = { 0, };    // 모든 요소 0으로 초기화
    int idx;

    // 배열 arr1을 대상으로, 저장되지 않은 정수 1을 찾으라고 명령
    idx = BSearch(arr1, sizeof(arr1) / sizeof(int), 1);
    if (idx == -1)
        printf(&quot;탐색 실패 \n\n&quot;);
    else
        printf(&quot;타겟 저장 인덱스: %d \n&quot;, idx);

    // 배열 arr2을 대상으로, 저장되지 않은 정수 2을 찾으라고 명령
    idx = BSearch(arr2, sizeof(arr2) / sizeof(int), 2);
    if (idx == -1)
        printf(&quot;탐색 실패 \n\n&quot;);
    else
        printf(&quot;타겟 저장 인덱스: %d \n&quot;, idx);

    // 배열 arr3을 대상으로, 저장되지 않은 정수 3을 찾으라고 명령
    idx = BSearch(arr3, sizeof(arr3) / sizeof(int), 3);
    if (idx == -1)
        printf(&quot;탐색 실패 \n\n&quot;);
    else
        printf(&quot;타겟 저장 인덱스: %d \n&quot;, idx);

    return 0;
}

&gt; 출력
비교연산횟수: 9  
탐색 실패        

비교연산횟수: 13 
탐색 실패        

비교연산횟수: 16 
탐색 실패 </code></pre>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/1e7e3748-bf5f-4e76-928c-9b271d992764/image.png" alt=""></p>
<p>$$O(n)$$와 $$O(logn)$$의 차이를 확연하게 볼 수 있다.</p>
<hr>
<h2 id="review"><code>&lt;Review&gt;</code></h2>
<p>자료구조를 c언어로 학습하다보니 c언어 공부도 되고 무엇보다 한번 공부해본 자료구조를 알고리즘 문제에서 어떻게 활용하면 좋을지 생각이 들었다.
역시 모든 처음엔 이해가 잘 안가지만 두 번째하면 더 잘 이해되고 잘 보이는 법!
이번에도 책 완독까지 가보자~!</p>
<p>아 그리고 SSAFY 지원 기간이 떳다!
기다려라 SSAFY... 이 녀석도 붙어주마!</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/3f93949b-a1bc-4549-8a24-14ad1051d555/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[마이다스 아이티] 온라인 인턴십 후기✨]]></title>
            <link>https://velog.io/@mingming_eee/Midasitintern</link>
            <guid>https://velog.io/@mingming_eee/Midasitintern</guid>
            <pubDate>Mon, 30 Sep 2024 07:23:18 GMT</pubDate>
            <description><![CDATA[<h2 id="✨intro">✨Intro</h2>
<p>마이다스 아이티에서 2024년 채용 전환형 인턴십 채용공고를 냈다.
<a href="https://www.midasit.com/">마이다스아이티 주식회사</a>는 과학기술용 시뮬레이션 소프트웨어를 개발하고 보급하는 대한민국의 IT회사이다.
2000년 9월에 설립되어 현재 미국, 중국, 일본, 인도, 러시아 등 8개의 현지법인 및 지사와 전세계 28개국의 해외 네트워크를 통해 110여개 국에 공학기술용 소프트웨어를 제공하고 있다.
2000년 9월 포스코건설에서 분사한 회사다. 당시 한국 시장에서 건설 분야 소프트웨어는 100% 외산이었다. 시공과 설계는 우리 기술로 가능했지만, 핵심 소프트웨어는 외국산이었다. 한국 엔지니어들에게 외국의 소프트웨어는 사용하기 어려운 도구였다. 이를 국산화하기 위해 포스코건설이 팀을 만들었고, 그 팀의 리더가 이형우 현 마이다스아이티 대표였다.</p>
<p>주전공이 토목공학과이면서 이번에 국비교육을 통해 개발(python)을 배우게 되어 나와 잘 맞는 회사라 생각해서 지원하게 되었다.</p>
<h3 id="채용-공고🏢"><a href="https://www.jobkorea.co.kr/Recruit/GI_Read/45205221">채용 공고🏢</a></h3>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/c50485ca-69bf-44e9-8f65-e63cdfbe0e3a/image.png" alt=""></p>
<p>✅접수기간 : 2024.07.24 ~ 2024.08.08</p>
<p>✅지원자격 :</p>
<p>1) 성장하고 싶은 누구나 (학력 무관)
2) 해외 여행에 결격사유가 없는 자
3) 남성의 경우, 병역필 또는 면제자</p>
<p>✅<a href="https://midas.recruiter.co.kr/career/2024intern">채용직무</a> :</p>
<ul>
<li>국내 영업</li>
<li>국내 영업(남부지사)</li>
<li>SW 기획(기술기획)</li>
<li>웹 서비스 기획</li>
<li>기술 지원</li>
<li>콘텐츠 기획</li>
<li>마케팅</li>
<li>HR</li>
<li>HR 연구기획</li>
</ul>
<p>✅채용과정 :</p>
<ul>
<li>OT (8/29 하루로 변경)</li>
<li>온라인 인턴십 (9/2 ~ 9/13)</li>
<li>오프라인 인턴십 (9/23 ~ 9/27)</li>
<li>정규직 전환 (10/14 ~ ) ※3개월 수습기간. 급여 100% 제공.</li>
</ul>
<h3 id="지원-및-역검-후기🙊">지원 및 역검 후기🙊</h3>
<p>나는 <code>SW 기획(기술기획)</code>에 지원했다.
토목관련 기사 자격증 2개(토목 기사, 콘크리트 기사), 토목설계 및 시공회사에서 일한 경험(총 2년 4개월정도), 부트캠프 수료 및 프로젝트를 적었다.
신기하게도 자소서 항목이 없어 정말 이력서만 넣는 느낌이었다.
포트폴리오 항목이 있어서 넣는건 자유였지만, 따로 포트폴리오가 없던 나는 개발자들의 이력서 형식으로 만들어 놓은 것을 포트폴리오로 넣었다.
기본 지원서에 있는 내용에서 내가 더 강조하고 표현하고 싶은 부분에 대해서 글을 적었고,
부트캠프에서 만든 운세 어플 앱에 대해서도 언급했다.</p>
<p>접수 기간 동안에 지원서를 제출하고 역검(AI 역량검사) 응시까지 완료해야 지원이 모두 완료된것이다!
나는 <a href="https://www.jobda.im/acca/test">잡다(jobda)</a>에서 역량검사 연습을 한번 정도 하고 그냥 있는 그대로의 나를 보여주자! 싶어서 두번째만에 바로 실제 역검에 응시했다.</p>
<p>역검에 관해서는 JOBDA 홈페이지에 들어가면 더 자세한 설명이 나와있으니 생략하겠다.</p>
<p>그 결과 합격!✨ (2024.08.23)</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/3b57ea13-8a13-4ba0-a583-c7abb3ac351e/image.png" alt=""></p>
<p>사실 내가 지금 탈색한 노란머리여서 과연 통과할 수 있을까....? 싶었다.
하지만 설계 회사를 다닐 때 마이다스 아이티 제품인 Civil을 사용해본 경험이 있다는 점,
제품을 사용해 Pipe rack 기초를 직접 설계한 경험 (엑셀로 계산서를 작성한 적이 있다.),
C언어를 학습해 프로그램 설계 관련해서 약간의 이해가 있을 수 있다는 점,
앱을 배포해 상용화해본 경험이 있다는 점에서 마이다스 아이티 제품을 기획하는 직무에 잘 어울리고 잘 할 수 있을 것이라고 생각했다.</p>
<p>아래는 SW 기획 직무 우대사항이다. 정말 이 자격들을 다 갖췄다!
<img src="https://velog.velcdn.com/images/mingming_eee/post/8dbcbc69-0e89-45be-ba83-a295a7ac3eb1/image.png" alt=""></p>
<p>역검 관려해서 팁을 주자면, 다른 블로그에서도 나와있는 내용일 수도 있겠지만 처음 내 얼굴을 찍을 때 최대한 무표정을 하고 찍었다.
그리고 이후 뒤로 갈 수록 약간의 미소를 띄며 여유있게 역검에 응하는 태도를 보여줬다.
사실 역검에서 하는 게임은 재밌었다! 잘 못 맞춰서 &#39;엣쿵 실수했네! &gt;&lt;&#39; 느낌으로 가도 좋다.
성적이 좋다 나쁘다를 보는 것이 아니기 때문에...</p>
<h3 id="🙉ot-20240829">🙉OT (2024.08.29)</h3>
<p>나는 마이다스 아이티를 가본적이 3번정도 있다.
설계 회사 재직 중일 때 마이다스 아이티 제품 교육을 3회정도 들으러 갔기 때문이다 ㅎㅎ
그래서 오프라인과 온라인 OT를 선택할 수 있었는데, 마이다스 아이티 구내 식당이 판교에서 제일 맛있다는 말도 들었기 때문에 오프라인을 선택하게 되었다.</p>
<p>전에 교육을 들었던 강의실(?)과 동일한 층에서 좀 더 컨퍼런스룸에서 OT가 진행되었다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/075fed72-b30c-4dd7-b824-da24c39e102f/image.jpg" alt=""></p>
<p>자세히보면 위에 LED에 &#39;♥2024 마이다스 채용 전환형 인턴십에 오신 것을 환영합니다♥&#39;라고 적혀있는 것을 볼 수 있다.
저런 디테일에 감동하는 나... 진정한 F다..💕</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/a635d233-7fb4-40e1-babb-e07e4286a5d6/image.jpg" alt=""></p>
<p>여기서 먼저 내 이름을 확인하고 왼쪽 문으로 들어가면 되는데 커피랑 아이스티 중에서 선택해서 음료를 받을 수 있었다. (카페인 못 마신 분들까지 배려한 이 디테일들... 너무 좋다...)</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/930f3aac-a4ec-4b8c-be98-5c6693df1303/image.jpg" alt=""></p>
<p>들어가면 이렇게 스크린에 마이다스 광고(?) 영상을 계속 틀어준다.
대략 80명 넘게 있었던거 같다.
내가 분명히 늦게 온 것도 아니고 15분 정도 일찍 왔는데도 2/3이 차있었다.😲</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/56a6ce99-4b83-4bd4-8656-5d21ece1c28a/image.jpg" alt=""></p>
<p>의도하지 않았지만 맨 앞자리에 앉게 되었고, OT가 시작되는 동안 정자세로 앉아있었다.
그러다보니 진행하시는 직원 분께서 편하게 준비된 쿠키 먹으면서 옆사람이랑 얘기하면서 있어도 된다고 하셨다.😂
(여기서 하는 행동이 평가에 들어갈지도 모르는데 어떻게요...)
참고로 계신 직원분들 대부분 인사팀 직원분들인거 같았다.
직원분들이 꽤 계셨고 (약 15분,,,?) 흰티에 청바지 까만신발로 맞춰서 입고 계셨다.
그리고 여기서 나만 머리가 노랬다...(여기서부터 엄청 눈치 보였다.)</p>
<p>OT 시작하기 전에 직원분들께서 오셔서 아이스 브레이킹 질문 몇개도 하시고 옆에 있는 인턴 지원자분과도 얘기하면서 시간을 보냈다.</p>
<p>OT에 참석한 인원은 대략 150명가량 됐던거 같다. (오프라인 + 온라인)
채용 직무가 10개인데 온라인 인턴을 150명 뽑은 곳이 있다?!?!
<del>경쟁율이 얼마나 됐는진 안알려줬다...ㅎㅎ</del></p>
<p>오전 OT가 끝나고 기다리고 기다리던... 점심을 먹으러 갔다!</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/311ef9e6-0865-4e2a-a867-8a919d3caab9/image.jpg" alt=""></p>
<p>음식 놓여진 곳이 이렇게 되어있는데 사진을 밥 다 먹고 찍어서 그렇지
두 줄로 음식이 놓여져 있었고, 이런 테이블이 하나 더 있고, 식단하시는 분들을 위한 원형 테이블 하나 더 있었다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/6621bd22-1222-4d61-8280-710eed72b222/image.jpg" alt=""></p>
<p>메뉴는 이렇게!
사실 OT 안내 메일에 메뉴가 뭔지도 적혀 있었다. 이거 알려준 것도 귀여웠다. ㅋㅋㅋ</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/e5ce27c8-5df9-450f-b473-bb931d8210aa/image.png" alt=""></p>
<p>진짜 음식 너무 맛있었다... 점심 먹고 입사 욕구가 200% 올랐다 ㅋㅋㅋ 원래도 나와 맞는 회사라 생각해서 꼭 입사하고 싶었는데...😞</p>
<p>점심식사는 7명씩 그룹을 지어(가나다순) 마이다스 아이티 직원분 한분과 같이 총 8명이서 한테이블에서 먹었다.
사실 편하게 밥 먹는 자리는 아니었다...ㅎㅎ
세미 면접 느낌으로 먹었달까... 하하... 
맛있는 밥을 두고도 맘편히 먹지 못해 살짝은 아쉬웠다.</p>
<p>우리 조의 직원분께서 성격이 너어어어어무 좋으셔서 정말 개인적으로도 친해지고 싶었다...(E모드 발동😍)
그 분께서 1층 카페는 마이다스 직원 전용으로 커피 천원에 마실 수 있다고 해서 한잔씩 사주셨다.(천사 아니시냐구요...😍)
직원분의 추천으로 파인애플 콤부차를 마셨는데 진짜 맛있었다 ㅎㅎㅎ</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/504d144b-bf2a-4daa-91b7-9da694449ed4/image.jpg" alt=""></p>
<p>밥을 다 먹고 다시 컨퍼런스 홀로 가는데 날이 너어어어무 좋아서 사진 한장 찍었다. ㅎㅎㅎ
입사하면 이 모습을 계속 볼 수 있다니<del>~</del>😎</p>
<p>이 후 다른 사람들 오기 전까지 듣고 싶은 노래 신청 받아서 들으면서 우리 조 직원분가 블로그 서로 이웃 하고 ㅋㅋㅋ 그러면서 시간을 보냈다.</p>
<p>이 후 오후 시간에는 인턴십 기간 관련해서 고용보험? 계약서를 작성했다.
첫 주엔 1일 4시간 근무, 둘째 주엔 1일 시간 근무로 적혀있었다.
인턴 급여는 9/25에 일괄 지급된다고 했다. (수료증도 발급)</p>
<h3 id="🙈온라인-인턴십-20240902--20240913">🙈온라인 인턴십 (2024.09.02 ~ 2024.09.13)</h3>
<p>온라인 인턴십은 마이다스 아이티에서 만든 <code>뉴로우</code>라는 사이트에서 일일 업무 계획을 작성하고, 소통/전략 과정 훈련, 회고, 과제 등을 통해 평가되었다.</p>
<p>일일 업무 계획은 매일 작성하는 거라 동일하게 내용 작성해도 된다고 했다.
하지만 난 매일 할 수록 전에 했던 내용에 추가 또는 수정하는 방식으로 약간은 다르게 작성했다.</p>
<p>소통/전략 과정은 AI와 대화(소통)하면서 또는 업무시 발생할 수 있는 일에 대해 전략적으로 대응하면서 마이다스에서 중요시 생각하는 CSR, 느깨바 등을 학습할 수 있는 과정이었다.
AI가 내가 소통한 것, 전략적으로 문제를 해결하는 것에 대해 평가해서 별점을 매겨주는데
나는 소통과 전략 훈련 과정 모두 대부분 한번에 다 별점 5점이 나왔고 (5점 만점) 여러번 해도 된다고 했지만 5점이 나온 것에 대해서는 따로 다시 진행하진 않았다.</p>
<p>5점이 나올 수 있었던 이유를 꼽자면,
메모장을 열어서 해당 훈련마다 작성되어 있던 훈련 목표, 훈련 주제에 대해 적어놓고 머리 속으로 시뮬레이션을 돌려서 예상 질문이나 답변을 정리하면서 했기 때문이라 생각한다.
그리고 내가 작성한 답변 또는 AI가 얘기해준 피드백도 정리해놨다.</p>
<p>과제는 첫 째주엔 독후감을 작성했는데, 마이다스 아이티 회사에서 만든 책(태도가 답이다)을 읽고 정해진 분량에 대한 독후감을 작성했다.
첫 날 독후감 작성을 오후 10시까지 했다... 와... 지원서에 자소서 항목이 없는 이유를 독후감을 작성하면서 알았다. 
셋째 날 까지 독후감을 작성했는데 정해진 분량에 대해 요약하는 것 이 외의 질문이 자소서 항목 느낌이었다.😂</p>
<p>작성한 내용은 <a href="https://www.saramin.co.kr/zf_user/tools/character-counter">사람인 글자수세기/맞춤법</a> 에서 확인하고 제출하였다.
독후감 과제를 통해 마이다스에서 추구하는 인재상, 발전할 수 있는 사람이 되는 방법, 일 잘하는 사람이 되는 방법 등에 대해 알게 되었다.</p>
<p>회고는 당일 내가 한 업무(?) 활동에 대한 후기를 작성하는 것인데
여기서 &#39;내가 실수한 것 같다&#39;라는 생각을 오프라인 인턴십 과정으로 가는데 실패하고 나서 깨달았다.</p>
<h5 id="매일-해야하는-과제-단계에-대해서-아래와-같이-매일-메일로-보내주었다">매일 해야하는 과제, 단계에 대해서 아래와 같이 매일 메일로 보내주었다.</h5>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/17f427d6-4556-4148-b17e-1f3f7686caf1/image.png" alt=""></p>
<p>자세히 보면 &#39;멘토 그룹 OOO님이 회고에 댓글을 남겼어요.&#39; 란게 있는데 내가 매일 적은 회고에 대해 멘토님이 댓글도 남겨주신다...
나의 멘토님은 개발자 분이셨다!
(이 분의 의견이 나의 채용 과정에 영향을 미쳤을 지는 미지수...)</p>
<p>최종적으로 온라인 인턴십을 수료한 사람은 대략 110명정도 되는 것 같다.
(시작보다 인원이 줄은 이유는 이미 다른 회사에 재직중이라 인턴십 계약서를 작성하지 못하는 사람, 인턴십 인원이 많아 채용 전환 확률이 낮을 것 같다 판단하여 하차하는 사람 등 다양한 이유로 1/5 정도 나갔다.)</p>
<h2 id="🌇최종-후기">🌇최종 후기</h2>
<p>사실 온라인 인턴십 합격 후기를 넥스트러너스(오즈코딩스쿨) 조교를 하고 있는 중에 확인해서
당장 다음주에 OT를 가야하고, 그래서 입사한지 13일만에 퇴사하게 되었다...
지금와서 생각해보면 조교 입사를 한달 미룰 수 있었던거 같은데... 미룰껄 아직까지 약간의 후회가 있다.😢</p>
<p>결론부터 얘기하자면 오프라인 인턴십으로는 가지 못했다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/63f7a96e-1a6f-4db5-816f-1e463e7ed651/image.png" alt=""></p>
<p>온라인 인턴십에서 오프라인 인턴십으로 몇명이 가는 것인지 정확한 TO를 얘기해주지 않은 점, 
(열심히, 성실히 임해서 마이다스가 원하는 기준에 맞기만 하면 뽑아줄 거라고 얘기했다 ㅎ)
온라인 인턴십 과정을 직무별로 나눠서 하지 않고 전체 다같이 해서 오프라인 인턴십 합불을 정한 것
(연구 기획 직무의 경우 석사분들이셨다;;)
등 이해가 안되는 부분이 많았다.</p>
<p>독후감 작성하고 GPT에 돌려서 책에서 얘기하는 CSR과 다르게 적은 사람들도 있었는데,
그 사람들보다 내가 정직하게 채용 과정에 임하지 않았나? 생각하면 전혀 아니었다.</p>
<p>그리고 줌 미팅을 여는 날도 있었는데 그 때 지각을 했나? (시간 엄수해 달라는 얘기를 많이 들었다.)라고 한다면 전혀 아니었다.</p>
<p>2주간 온라인 인턴십에 집중하기 위해서 약속도 하나도 안 잡고,
혹시나 오프라인 인턴십에 가게 되면 외적인 것에서 눈치가 보일까봐 가발도 샀다.
(왜 머리 염색을 안했냐는 질문에 답을 하자면 나는 댄스팀 활동도 하고 있고, 여기 회사에 붙을 거라는 보장이 없기 때문에 어렵게 탈색한 머리를 검은 색으로 덥기 싫었다.
그리고 오프라인 인턴십 기간 이전에 댄스팀 뮤비 촬영이 있었기 때문에 다른 머리색을 염색할 계획도 있었다.)</p>
<p>결론적으로, AI 면접(역량검사)이 처음 개발되어 코로나 시기에 사용했을 때도 마이다스 아이티가 지원자들을 실험체, 데이터 모으기 위한 수단 정도로 생각하는거 같다는 의견이 있었는데
이번에 만든 뉴로우에 인턴 지원자들이 이용된 거 같다는 느낌을 지우기 어려웠다.</p>
<p>회고에서 내가 실수 했다고 생각한 부분은,
회고는 주관적인 관점, 감정적인 부분을 배제하고 당일 업무에 대해 생각하는 과정, 단계인데
9/12에 내 생일이었다는 점을 언급했던 것, 입사 또는 인턴십 과정 등에 대해 약간의 감정적으로언급한 것이 평가에 안 좋게 반영되었나 혼자 생각하고 있다...😥
주간 회고라고 해서 매일 작성한 회고를 정리하는 과정이 한번 있었는데 이때 내가 별 5개 중에 3개를 받았기 때문이다...
(나와 친한 인턴 동생은 별 5개를 받아서 부러우면서도 매우 불안했다.😫 물론 그 친구는 정말 성실하고 진심으로 인턴십 과정에 임했다.)</p>
<p>온라인 인턴십 과정 자체가 AI가 평가한다는 생각이 강해서 마이다스 아이티는 AI를 정말 좋아하는구나 생각이 들었다.</p>
<p>그래도 이 과정 자체가 무의미한 것은 아니었다.
&#39;태도가 답이다.&#39;라는 책 자체가 준 교훈, 발전하는 사람이 되는 방법, 일 잘하는 사람이 되는 방법, 세상을 살아가는 태도에 대해 알려줬기 때문에
감정적인 기복이 좀 있던 내가 모든 일에 무던하게 해쳐갈 수 있는 지혜를 배울 수 있게 되었다.
그리고 사람과 사람이 만나서 소통하면서 다양한 상황, 일들이 진행되는데 거기서 내가 어떻게 행동해야 내가 추구하는 방향, 삶의 목표대로 살아갈 수 있는지 깊게 생각해 볼 수 있는 시간이었다.</p>
<p>인턴십을 통해 친해진 동생이 생겨 인맥도 얻었고,
마이다스 아이티 밥도 먹어보고, 마이다스 아이티가 어떤 회사인지 알아 볼 수 있는 기회였다.</p>
<p>아, 그리고 내가 머리를 염색하지 않은 점에 대해 아직도 의문을 가진 분이 있을 수도 있는데,
이거 때문에 내가 가족들, 친구들 등등 다양한 사람에게 일하는데 외적인 것이 얼마나 영향을 미치는가에 대해 물어봤다.</p>
<p>대부분의 대답은 <strong>대한민국에서 회사를 다니려면 눈에 띄는 것을 하면 안된다.</strong>였다.
사실 내가 개발 직군을 새로운 직무 분야로 선택하게 된 이유에는 복장의 자유도 있었다.
토목 직군에서 일하면서 보수적으로 제한된 외적 꾸밈이 나에게는 정말 맞지 않은 부분이었기 때문에 마이다스 아이티는 이런 쪽으로는 열려있을 거라는 약간의 기대가 있었다.
(안 열려 있었다면 탈색모 지원자를 인턴십 과정에 합격 시켰을까,,,?)</p>
<p>아무튼...!
정말 가고 싶었던 회사였지만 이렇게 경험할 수 있는 기회를 줘서 감사했고,
내가 아홉수라 그런지 악운이 많이 들어오는 해인데... 내년에 정말 잘되려나보다!
앞으로도 개발 관련해서 더 공부하면서 토목을 버리고 개발만으로도 취업할 수 있도록 노력해야겠다.
물론 아직 춤에 대한 열정도 있기 때문에 춤도 열심히! ㅎㅎㅎ</p>
<p>매사 모든 것에 열심히! 하지만 지치지 않게 완급조절 잘하면서! 현명하고 지혜롭게!
다른 취업 준비하는 모든 분들에게도 좋은 일이 가득하길 바란다~</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/f8d71cf1-ef1e-4a88-ab37-91e9fb3494c7/image.png" alt=""></p>
<h5 id="마지막-사진은-제2의-푸바오-요즘-유행하는-무뎅-사진-🦛💗">마지막 사진은 제2의 푸바오 요즘 유행하는 무뎅 사진 🦛💗</h5>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자료구조와 알고리즘 with Python] Chapter 8 : Graph]]></title>
            <link>https://velog.io/@mingming_eee/data-structure-with-python-8</link>
            <guid>https://velog.io/@mingming_eee/data-structure-with-python-8</guid>
            <pubDate>Fri, 27 Sep 2024 07:46:30 GMT</pubDate>
            <description><![CDATA[<h2 id="chapter-그래프-graph">Chapter: 그래프 (Graph)</h2>
<p>그래프는 복잡하게 연결된 객체 사이의 관계를 표현할 수 있는 가장 자유로운 자료구조이다.
모든 선형 자료구조나 트리조차도 그래프로 나타낼 수 있어 그래프의 한 종류로 볼 수 있다.</p>
<hr>
<h3 id="01-그래프란">01 &quot;그래프란?&quot;</h3>
<p>그래프(graph)는 복잡하게 연결된 객체 사이의 관계를 효율적으로 표현할 수 있는 자료구조이다. 지하철 노선도, 전기 회로, 소셜 네트워킹 서비스(SNS)와 같은 다양한 분야에서 많은 객체가 서로 복잡하게 연결된 자료를 다루고 있다. 지금까지 공부한 선형 자료구조나 트리도 그래프로 표현할 수 있어 그래프는 가장 일반화된 자료구조이다.</p>
<h4 id="그래프의-유래"><em>그래프의 유래</em></h4>
<p>레오하르트 오일러(Leonhard Euler) 수학자가 처음으로 &#39;위치&#39;라는 객체를 <code>정점(vertex)</code>으로, 위치 간의 관계인 &#39;다리&#39;는 <code>간선(edge)</code>으로 표현하여 그래프 문제로 변환하는 것에서 시작됐다.
오일러는 그래프에 존재하는 모든 간선을 한 번만 통과하면서 처음 정점으로 되돌아오는 경로를 오일러 경로(Eulerian tour)라 정의하고, 그래프의 모든 정점에 연결된 간선의 개수가 짝수일 때만 오일러 경로가 존재한다는 오일러의 정리를 증명하였다.
정점들 자체로는 큰 의미가 없지만, 이들을 간선으로 연결하면 &#39;관계&#39;가 만들어지고 그래프가 형성된다.
이때 정점은 객체를 의미하고, 간선은 이러한 객체 간의 관계를 나타내어, 정점 A와 B를 연결하는 간선은 <code>(A, B)</code>와 같이 정점의 쌍으로 표현한다.</p>
<h4 id="그래프의-종류"><em>그래프의 종류</em></h4>
<p>그래프는 간선의 방향성과 정점 간의 연결 정도에 따라 여러 가지 그래프로 나뉜다.</p>
<ol>
<li><p>무방향 그래프(undirected graph)
두 정점을 연결하는 간선에 방향성이 없는 그래프로 간선이 양방향으로 갈 수 있는 길일 경우이다. 
따라서 두 정점 A와 B를 연결하는 간선 <code>(A, B)</code>는 <code>(B, A)</code>와 동일한 간선이다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/mingming_eee/post/c9fae6ca-5a13-4501-9762-185684cb5fff/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/mingming_eee/post/269797f5-6668-4cef-ba89-2702cdc49897/image.png" alt=""></th>
</tr>
</thead>
</table>
<pre><code class="language-py">// 무방향 그래프 예시
V(G1) = {A, B, C, D}
E(G1) = {(A,B), (A,C), (A,D), (B,C), (C,D)}

V(G2) = {A, B, C}
E(G2) = {(A,B), (A,C), (B,C)}</code></pre>
</li>
<li><p>방향 그래프(directed graph)
간선에 방향이 있는 그래프로 <code>다이그래프(digraph)</code>라고도 합니다. 간선이 한 방향으로만 갈 수 있는 길일 경우이다. 
A에서 B로 가는 길은 <code>&lt;A, B&gt;</code>로 표현하고, <code>&lt;A, B&gt;</code>와 <code>&lt;B, A&gt;</code>는 서로 다른 간선이다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/mingming_eee/post/015c7259-a63e-4127-b97f-c519a56f9183/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/mingming_eee/post/54936b98-0221-49d2-8f3f-6f4cb97d0b6b/image.png" alt=""></th>
</tr>
</thead>
</table>
<pre><code class="language-py">// 방향 그래프(다이그래프) 예시
V(G3) = {A, B, C, D}
E(G3) = {&lt;A,B&gt;, &lt;A,C&gt;, &lt;B,C&gt;, &lt;C,D&gt;, &lt;D,A&gt;}

V(G4) = {A, B, C}
E(G4) = {&lt;A,B&gt;, &lt;A,C&gt;, &lt;B,C&gt;}</code></pre>
</li>
<li><p>완전 그래프(complete graph)
그래프의 모든 정점 사이에 간선이 존재하는 그래프를 말한다.
정점이 n개인 무방향 완전 그래프는 $$n×(n-1)/2$$개의 간선을 갖고, 방향 그래프는 $$n×(n-1)$$개의 간선을 갖는다.
따라서 아래 G5는 무방향 완전 그래프이고, G6는 방향 완전 그래프다. (G5에서 하나의 간선이 G6에서 두 개의 간선에 해당.)</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/mingming_eee/post/f3d0f7d9-3025-4a60-a545-0a43fa4667fe/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/mingming_eee/post/793b0fef-45c6-46e6-ae18-dc55861e2f4f/image.png" alt=""></th>
</tr>
</thead>
</table>
</li>
<li><p>부분 그래프(subgraph)
원래의 그래프에서 정점이나 간선 일부만을 이용해 만든 그래프다.
아래 그림에서 G1, G2, G3은 그래프 G의 부분 그래프다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/mingming_eee/post/48bc9f89-7904-4767-afa7-6a879f715ef2/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/mingming_eee/post/41df919b-a5ff-4f38-85e9-e787d0b2549b/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/mingming_eee/post/39aa5357-48d4-470d-9f7e-b9d5fca32bc7/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/mingming_eee/post/9148f39f-e1fc-4024-a264-243d6fb98a0c/image.png" alt=""></th>
</tr>
</thead>
</table>
</li>
<li><p>가중치 그래프(weighted graph)
간선에 가중치가 할당된 그래프를 가중치 그래프 또는 네트워크(network)라고 한다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/mingming_eee/post/2edfe22a-89f7-4ff2-840c-f98c3f4e17da/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/mingming_eee/post/04bdfaf1-83e2-444a-acc0-f3e5460227a2/image.png" alt=""></th>
</tr>
</thead>
</table>
</li>
</ol>
<h4 id="그래프의-용어"><em>그래프의 용어</em></h4>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/mingming_eee/post/4ee0d76d-6a4e-4a92-a585-3e6a9ce1fe9f/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/mingming_eee/post/f3a0ac4a-8b9b-465b-8601-3b34998420d2/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/mingming_eee/post/d2f6f852-de0c-4563-a1c3-53748f98ed2a/image.png" alt=""></th>
</tr>
</thead>
</table>
<ul>
<li><p>인접(adjacent)
: 간선으로 연결된 우 정점을 인접해 있다고 한다.
G1에서 A에 인접한 정점은 B, C, D다.</p>
</li>
<li><p>정점의 차수(degree)
: 그 정점에 연결된 간선의 수를 말한다.
방향 그래프에서는 차수가 두 가지로 나뉘는데, 외부에서 오는 간선의 수를 <code>진입 차수(in-degree)</code>, 외부로 향하는 간선의 수를 <code>진출 차수(out-degree)</code>라 한다.
G4에서 B의 차수는 3이고 이중 진입 차수가 2, 진출 차수가 1이다.</p>
</li>
<li><p>경로(path)
: 간선을 따라갈 수 있는 길을 순서대로 나열한 것을 말한다.
그리고 경로를 구성하는 간선의 수를 <code>경로 길이(path length)</code>라 한다.
G1에서 A~C의 경로는 A-B-C, A-D-C, A-C 등이 있고, A-B-C는 경로의 길이가 2, A-C는 경로의 길이가 1이다.</p>
</li>
<li><p>단순 경로(simple path)
: 반복되는 간선이 없는 경로를 말한다.
G1에서 A-B-C는 단순 경로지만, B-A-C-A는 단순경로가 아니다.</p>
</li>
<li><p>사이클(cycle)
: 시작 정점과 종료 정점이 같은 단순 경로를 말한다.
G1에서 B-A-C-B는 사이클이다.</p>
</li>
<li><p>연결 그래프(connected graph)
: 모든 정점 사이에 경로가 존재하는 그래프를 말한다.
G1은 연결 그래프지만 G9는 연결 그래프가 아니다.</p>
</li>
<li><p>트리(tree)
: 사이클을 가지지 않는 연결 그래프를 말한다.</p>
</li>
</ul>
<hr>
<h3 id="02-그래프의-표현">02 &quot;그래프의 표현&quot;</h3>
<p>그래프는 정점과 간선의 집합으로 구성되는데,
정점 집합은 리스트를 이용하면 쉽게 표현할 수 있다. 
간선은 좀 복잡하다. 
간선(u, v)는 정점 u와 v가 인접해 있다는 것을 말하는데, 그래프에서 정점들의 인접 관계를 어떻게 효율적으로 표현할 수 있을까?
인접 행렬과 인접 리스트를 사용할 수 있는데, 그래프의 특성과 필요한 연산에 따라 적절한 방법을 선택해야 한다.</p>
<h4 id="-인접-행렬을-이용한-표현">* 인접 행렬을 이용한 표현*</h4>
<p>간선들의 집합을 표현하는 가장 간단한 방법은 2차원 배열을 이용하는 것이다.
이것을 <code>인접 행렬(adjacency matrix)</code>라 한다.
그래프의 정점 개수가 $$n$$이라면 인접 행렬(2차원 배열)의 크기는 $$n×n$$이고 행렬의 각 성분이 두 정점의 연결 관계를 나타낸다.
간선이 있으면 1, 없으면 0으로 나타냄으로써 아래 그림과 같이 무방향 그래프를 파이썬 2차원 배열로 나타낼 수 있다.
무방향 그래프에서는 인접 행렬이 항상 대칭행렬이다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/mingming_eee/post/0fddeb41-3689-4662-ab60-f1521da4acd8/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/mingming_eee/post/12e687ab-c241-4301-b75a-b6888c5c4bd6/image.png" alt=""></th>
</tr>
</thead>
<tbody><tr>
<td>무방향 그래프</td>
<td>파이썬 인접 행렬 표현</td>
</tr>
</tbody></table>
<p>무방향 그래프는 배열의 상위 삼각이나 하위 삼각만 저장하여 메모리를 절약할 수도 있다.
위 그림의 그래프는 5개의 간선이 있으므로 인접 행렬에서 10개(5*2)의 원소가 1을 갖는다.
행렬의 대각선 성분은 모두 0으로 표현되고 이는 자신에서 출발해 자신으로 오는 간선이 없기 때문이다.
그래프에서 정점들 사이에는 순서가 없기 때문에 현재 인접행렬에서는 U, V, W, X, Y순으로 한거고 변경되면 행렬도 변경될 것이다.</p>
<h4 id="인접리스트를-이용한-표현"><em>인접리스트를 이용한 표현</em></h4>
<p>각 정점이 인접한 정점 리스트를 갖도록 하여 간선들을 표현할 수 있는데 이러한 리스트를 <code>인접 리스트(adjacency list)</code>라고 한다.
예를 들어, 아래 그림에서 정점 V는 U, W, X와 인접해 있고 따라서 길이가 3인 인접 리스트(<code>[0,2,3]</code>)로 표현할 수 있다.
그래프에서 정점 옆 숫자는 정점에 번호를 부여한 것이다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/mingming_eee/post/00d3a53c-ae8b-4bb0-ac77-b6cb808e458d/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/mingming_eee/post/662e7130-69cf-4b95-966e-02b067d6ebbb/image.png" alt=""></th>
</tr>
</thead>
<tbody><tr>
<td>무방향 그래프</td>
<td>파이썬 인접 리스트 표현</td>
</tr>
</tbody></table>
<p>이때 리스트로 연결 리스트를 사용할 수도 있고, 파이썬의 리스트와 같은 배열 구조를 사용할 수도 있다.
그래프의 &#39;인접 리스트 표현&#39;은 사실 개념적으로는 &#39;인접 집합 표현&#39;이라고 하는 것이 더 정확하다. 왜냐면 한 정점에 인접한 정점들 사이에는 순서가 없기 때문이다.</p>
<h4 id="인접행렬-vs-인접리스트"><em>인접행렬 vs 인접리스트</em></h4>
<p>단지 그래프를 표현하기 위해서라면 인접 행렬과 인접 리스트 어떤 것을 사용하더라도 괜찮다.
다만, 표현된 그래프로 어떤 작업을 하기 위해서라면 다르다.</p>
<p>정점이 $$n$$개인 그래프를 표현하기 위한 메모리의 양은 인접 행렬의 경우 $$n^2$$이므로 인접 리스트보다 약간 불리하다.
그래프에 간선 (u,v)가 있는지 검사하려면 행렬은 해당 성분을 바로 검사하면 되지만, 인접 리스트에서는 정점 u의 인접 리스트 v가 있는 하나씩 검사해야 하기 때문에 인접리스트가 불리하다.
이 외 다른 연산도 약간씩의 차이가 있지만 인접행렬과 인접리스트를 사용했을 때 약간의 장단점이 존재한다.</p>
<p>메모리의 사용량이 중요하거나 정점에 비해 간선이 별로 없는 희소 그래프(sparse graph)에서는 인접 리스트가, 
정점끼리의 인접 여부를 빨리 알아내야 하거나 완전 그래프나 이와 유사한 조밀 그래프(dense graph)의 경우 인접 행렬이 더 좋은 선택일 것이다.</p>
<hr>
<h3 id="03-그래프-순회">03 &quot;그래프 순회&quot;</h3>
<p>그래프 순회는 하나의 정점에서 시작하여 그래프의 모든 정점을 한 번씩 방문하는 작업을 말한다.
실제로 많은 그래프 문제들이 단순히 정점들을 체계적으로 방문하는 것만으로 해결된다. ex) 전자회로 단자 간의 연결성 검사, 미로 탐색 문제
그래프의 정점들을 순회하는 체계적인 방법에는 깊이 우선 탐색과 너비 우선 탐색이 있다.
이 순회 방법은 이진 트리의 순회 방법과 비교해 볼 수 있다.</p>
<ol>
<li><p>깊이 우선 탐색(DFS:Depth First Search)
: 시작 정점에서 한 방향으로 계속 가다가 더 이상 갈 수 없으면 가장 가까운 갈림길로 다시 돌아와 다른 방향을 다시 탐색한다. 이진트리의 전위순회와 비슷하다.</p>
</li>
<li><p>너비 우선 탐색(BFS:Breadth First Search)
: 시작정점에서 가까운 정점을 먼저 방문하고 먼 정점을 나중에 방문한다. 이진 트리의 레벨순회와 비슷하다.</p>
</li>
</ol>
<p>기억이 잘 안나는 분들을 위한 좌표 👉<a href="https://velog.io/@mingming_eee/data-structure-with-python-4#03-%EC%9D%B4%EC%A7%84-%ED%8A%B8%EB%A6%AC%EC%9D%98-%EC%97%B0%EC%82%B0">이진트리의 연산 - 전위순회, 레벨순회</a></p>
<h4 id="깊이-우선-탐색dfsdepth-first-search"><em>깊이 우선 탐색(DFS:Depth First Search)</em></h4>
<p>시작 정점에서 한 방향으로 갈 수 있는 곳까지 깊이 탐색을 진행하다가 더 이상 갈 곳이 없으면 가장 최근에 만났던 갈림길 정점으로 되돌아온다.
갈림길로 돌아와서는 가 보지 않은 다른 방향의 간선으로 탐색을 진행하고, 이 과정을 반복해 결국 모든 정점을 방문한다.
탐색과정에서 여러 갈림길을 만나지만 그중에서 가장 최근에 만났던 갈림길로 되돌아와야하므로, 이들을 후입선출 구조의 스택에 저장한다.</p>
<table>
<thead>
<tr>
<th>-</th>
<th>단계</th>
<th>깊이 우선 탐색</th>
<th>스택</th>
</tr>
</thead>
<tbody><tr>
<td>0</td>
<td>U에서 탐색을 시작.<br>맨 처음 스택에는 U만 있음.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/2c43bc7c-e736-413b-8d8b-c79412b09ffd/image.png" width=60%></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/b0ac814b-6ebf-4c39-bdbb-6da09b46025a/image.png" width=40%></td>
</tr>
<tr>
<td>1</td>
<td>현재 정점은 스택 상단의 U.<br>U의 인접 정점 중 하나인 V를 선택해 탐색을 진행.<br>(우선 이 방향으로 가보기.)<br>V를 스택에 넣고 이 정점이 현재 정점.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/1aadffab-ab58-4021-93e5-a581cef85742/image.png" width=60%></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/8fef443b-af66-44a5-8c78-e9bc3187beee/image.png" width=40%></td>
</tr>
<tr>
<td>2</td>
<td>V의 이웃 정점 중에서 아직 방문하지 않은 정점은 W와 X.<br>이 중 W를 선택해 탐색을 진행.<br>W를 스택에 넣기.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/252dde24-e69b-4059-a23d-bba43a436670/image.png" width=60%></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/27f095b6-b57e-48d2-9837-803c790a9357/image.png" width=40%></td>
</tr>
<tr>
<td>3</td>
<td>W의 이웃 정점 중에서 아직 방문하지 않은 정점은 Y뿐.<br>Y를 탐색 &amp; Y를 스택에 넣기.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/8bc1e5c7-2343-407e-bca1-c7ca08db4dbf/image.png" width=60%></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/a2a1c214-7320-494f-8a74-435a0cb723aa/image.png" width=40%></td>
</tr>
<tr>
<td>4</td>
<td>Y의 이웃 정점 중에서는 방문하지 않은 정점이 없음.<br>이전으로 되돌아가기. 스택 상단의 Y를 삭제.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/0c1bc6d1-d6ce-43f7-a01d-11e8cece0c7a/image.png" width=60%></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/5ba5f6b4-1954-4554-a929-d79b73dbd610/image.png" width=40%></td>
</tr>
<tr>
<td>5</td>
<td>이제 스택 상단이 W. 가장 최근의 갈림길로 되돌아온 것.<br>W도 방문하지 않은 이웃 정점이 없으므로 이전으로 되돌아가기.<br>스택 상단에서 W를 삭제.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/23f62496-1420-4b6f-8340-174f464d85b4/image.png" width=60%></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/e4f9da8a-0c9c-48b5-9150-76dc716f1801/image.png" width=40%></td>
</tr>
<tr>
<td>6</td>
<td>현재 정점이 V.<br>V에서 남은 이웃 정점 X 탐색.<br>X를 스택에 넣기.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/b9ae86b5-7b00-4ea4-a13d-599c9ec7d1f3/image.png" width=60%></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/55dc9525-eec6-41bc-a813-9097c923c765/image.png" width=40%></td>
</tr>
<tr>
<td>7</td>
<td>X에 더 탐색할 이웃 정점이 없어 되돌아가기.<br>V로 되돌아 갔다가 시작정점 U로 되돌아가기.<br>U에서도 갈 수 있는 정점이 없으므로 탐색 종료.<br>정점의 방문 순서: U → V → W → Y → X</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/62569492-1808-420e-91f5-c2f4c3e0e283/image.png" width=60%></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/91a71a16-1ea3-44c4-9867-461937bed493/image.png" width=40%></td>
</tr>
</tbody></table>
<p>이제 이 깊이 우선 탐색을 구현해보자.
그래프는 인접 행렬로 표현한다.
정점 리스트 vtx와 인접 행렬 adj가 주어진다고 가정하고, 시작 정점은 s라고 한다.
각 정점의 방문 여부를 기록하기 visited 배열을 사용하는데, 맨 처음에는 모든 정점을 방문하지 않았으므로 False로 초기화 한다.
깊이 우선 탐색은 스택을 사용해 갈림길을 저장하는데, 시스템 스택을 이용하는 순환 호출을 이용하면 더 간결하게 구현할 수 있다.</p>
<pre><code class="language-py"># 코드 8.1: 깊이 우선 탐색(인접행렬 방식)
def DFS(vtx, adj, s, visited):
    print(vix[s], end=&#39; &#39;)
    visited[s] = True

    for v in range(len(vtx)):
        if adj[s][v] != 0:
            if visited[v]==False:
                DFS(vtx, adj, v, visited)

# 코드 8.2: 깊이 우선 탐색 테스트 프로그램
vtx = [&#39;U&#39;, &#39;V&#39;, &#39;W&#39;, &#39;X&#39;, &#39;Y&#39;]
edge = [[0, 1, 1, 0, 0],
        [1, 0, 1, 1, 0],
        [1, 1, 0, 0, 1],
        [0, 1, 0, 0, 0],
        [0, 0, 1, 0, 0]]

print(&#39;DFS(출발:U) : &#39;, end=&#39;&#39;)
DFS(vtx, edge, 0, [False]*len(vtx))
print()

&gt; 출력
DFS(출발:U) : U V W Y X </code></pre>
<h4 id="너비-우선-탐색bfsbreadth-first-search"><em>너비 우선 탐색(BFS:Breadth First Search)</em></h4>
<p>가까운 정점부터 꼼꼼하게 살피고 먼 정점을 찾아가는 전략이다.
즉, 거리가 0인 시작 정점으로부터 거리가 1인 모든 정점을 방문하고, 거리가 2인 정점들, 거리가 3인 정점들 순으로 방문을 진행한다.
이것은 이진트리의 레벨 순회와 동작이 비슷하다.
너비 우선 탐색에서는 방문한 정점들을 차례대로 저장하고, 들어간 순서대로 꺼낼수 있는 큐를 사용한다.
BFS는 큐에서 정점을 꺼낼 때마다 아직 방문하지 않은 모든 인접 정점들을 방문하고 큐에 삽입한다. 이러한 탐색 과정은 큐가 공백 상태가 될 때까지 계속한다.</p>
<table>
<thead>
<tr>
<th>-</th>
<th>단계</th>
<th>너비 우선 탐색</th>
<th>큐</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>맨 처음에는 시작정점 U를 방문하고 큐에 넣기.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/692e2559-a4b7-42e9-a93a-1d416d6bd194/image.png" width=80%></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/1b91cb75-9dac-42ed-8fc2-e5d874df6714/image.png" width=50%></td>
</tr>
<tr>
<td>2</td>
<td>큐에서 U가 나오고, U의 이웃인 V와 W를 방문하고 큐에 넣기.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/b27b6fdc-8888-4573-8c9e-0411e98d2221/image.png" width=80%></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/52196b98-6b64-4be6-90dc-c32a1128d2dc/image.png" width=70%></td>
</tr>
<tr>
<td>3</td>
<td>큐에서 V가 나오고 V의 이웃 중에서 아직 방문하지 않은 X를 큐에 넣기.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/39731b57-b76a-47a3-bab0-6286b604a728/image.png" width=80%></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/8527658e-d205-4786-8ce0-f827c2697a41/image.png" width=70%></td>
</tr>
<tr>
<td>4</td>
<td>큐에서 W가 나오고 W의 이웃 중에서 아직 방문하지 않은 Y를 큐에 넣기.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/71474d1f-93bb-4ffb-bbf9-5fa02cb227dd/image.png" width=80%></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/f8dab609-be79-457c-90cd-1f4146cbb2e5/image.png" width=70%></td>
</tr>
<tr>
<td>5</td>
<td>큐에서 X가 나오고 방문하지 않은 정점이 없음.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/bd8edfc7-bc16-44e8-8afe-fc6829fc5ecc/image.png" width=80%></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/add137fc-6772-41b3-b3df-d35b5583cdc2/image.png" width=70%></td>
</tr>
<tr>
<td>6</td>
<td>큐에서 Y가 나오고 방문하지 않은 정점이 없음.<br>큐가 공백상태가 되었기 때문에 탐색은 종료.<br>정점의 방문 순서: U → V → W → X → Y</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/d18049c7-e946-4fc9-a909-f06a42fe8519/image.png" width=80%></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/d966e599-9292-4dcb-a15a-2a3c74dc1f53/image.png" width=70%></td>
</tr>
</tbody></table>
<p>이제 BFS를 구현해보자.
그래프를 인접 리스트로 표현한다.
정점 리스트 vtx와 인접 리스트 aList가 주어진다고 가정하자.
시작 정점은 s고, 정점의 방문을 표시하는 배열 visited를 함수 내부에서 만들어 사용한다.
순환 호출을 이용한 DFS와 달리 BFS는 반복구조로 구현된다.
큐는 2장에서 구현한 원형큐 클래스나 파이썬의 queue나 collections 모듈을 사용할 수 있다.
이번에는 파이썬 queue모듈의 Queue 클래스를 사용한다.</p>
<pre><code class="language-py"># 코드 8.3: 너비 우선 탐색(인접리스트 방식)
from queue import Queue     #queue 모듈의 Queue 사용
def BFS_AL(vtx, aList, s):
    n = len(vtx)            # 그래프의 정점 수
    visited = [False]*n     # 방문 확인을 위한 리스트
    q = Queue()
    q.put(s)
    visited[s] = True
    while not q.empty():
        s = q.get()
        print(vtx[s], end=&#39; &#39;)
        for v in aList[s]:
            if not visited[v]:
                q.put(v)
                visited[v] = True

# 코드 8.4: 너비 우선 탐색 테스트 프로그램
vtx = [&#39;U&#39;, &#39;V&#39;, &#39;W&#39;, &#39;X&#39;, &#39;Y&#39;]
aList = [[1, 2],
         [0, 2, 3],
         [0, 1, 4],
         [1],
         [2]]
print(&#39;BFS(출발:U) : &#39;, end=&#39;&#39;)
BFS_AL(vtx, aList, 0)
print()

&gt; 출력
BFS(출발:U) : U V W X Y </code></pre>
<p>만약 깊이 우선 탐색(DFS)에서 시작 정점이 U가 아니라 W였다면 정점의 방문 순서는 어떻게 될까? 
W → U → V → X → Y가 될 것이다.
만약 너비 우선 탐색(BFS)에서 시작 정점이 U가 아니라 W였다면 정점의 방문 순서는 어떻게 될까? 
W → U → V → Y → X가 될 것이다.</p>
<hr>
<h3 id="04-신장-트리">04 &quot;신장 트리&quot;</h3>
<p>신장 트리(spanning tree)는 그래프 내 모든 정점을 포함하는 트리를 말한다.
즉, 그래프의 정점은 모두 포함하고, 간선은 일부만을 포함해 트리의 형태(사이클이 없어져야함)를 이루어야 한다. 하나의 그래프에는 여러 개의 신장 트리가 가능하다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/6a0d96b9-24df-4162-a4a2-d9683ee37e67/image.png" alt=""></p>
<p>신장 트리도 트리의 일종이므로 당연히 모든 정점이 연결되어 있어야 한다. 또한, 트리이므로 사이클은 없어야 한다.
(신장 트리 아닌 예시들에서 확인할 수 있다.)
그래프의 정점 수가 $$n$$이라면 신장 트리는 정확히 $$n-1$$개의 간선으로 모든 정점을 연결해야한다.
신장 트리는 어떻게 구할 수 있을까?
DFS와 BFS를 이용해 구할 수 있다.
즉, 깊이 우선이나 너비 우선 탐색 도중에 사용된 간선들만 모으면 신장 트리가 만들어진다.
다음 코드는 코드 8.1을 수정하여 만든 신장트리 알고리즘이다.</p>
<pre><code class="language-py"># 코드 8.5: DFS를 이용한 신장트리(인접행렬 방식)
def ST_DFS(vtx, adj, s, visited):
    visited[s] = True
    for v in range(len(vtx)):
        if adj[s][v] != 0:
            if visited[v]==False:
                print(&quot;(&quot;, vtx[s], vtx[v], &quot;)&quot;, end=&#39; &#39;)
                ST_DFS(vtx, adj, v, visited)    
                &quot;&quot;&quot;방문하지 않은 s의 이웃 정점 v가 있으면, 간선 (s,v)를 신장트리에 추가하고, 
                v를 시작으로 다시 깊이 우선 탐색 진행.&quot;&quot;&quot;

# DFS를 이용한 신장트리 테스트 프로그램
vtx = [&#39;U&#39;,&#39;V&#39;,&#39;W&#39;,&#39;X&#39;,&#39;Y&#39;]
edge= [[0,  1,  1,  0,  0],
       [1,  0,  1,  1,  0],
       [1,  1,  0,  0,  1],
       [0,  1,  0,  0,  0],
       [0,  0,  1,  0,  0]]

print(&#39;ST_DFS_AM: &#39;, end=&quot;&quot;)
ST_DFS(vtx, edge, 0, [False]*len(vtx))
print()

&gt; 출력
ST_DFS_AM: ( U V ) ( V W ) ( W Y ) ( V X ) </code></pre>
<p>너비 우선 탐색(시작 정점은 U)을 이용해 신장트리를 구해보자.</p>
<pre><code class="language-py"># BFS를 이용한 신장트리(인접리스트 방식)
from queue import Queue     #queue 모듈의 Queue 사용
def ST_BFS(vtx, aList, s):
    n = len(vtx)            # 그래프의 정점 수
    visited = [False]*n     # 방문 확인을 위한 리스트
    q = Queue()
    q.put(s)
    visited[s] = True
    while not q.empty():
        s = q.get()
        for v in aList[s]:
            if not visited[v]:
                print(&quot;(&quot;, vtx[s], vtx[v], &quot;)&quot;, end=&#39; &#39;)
                q.put(v)
                visited[v] = True

# BFS를 이용한 신장트리 테스트 프로그램
vtx = [&#39;U&#39;, &#39;V&#39;, &#39;W&#39;, &#39;X&#39;, &#39;Y&#39;]
aList = [[1, 2],
         [0, 2, 3],
         [0, 1, 4],
         [1],
         [2]]

print(&#39;ST_BFS_AM: &#39;, end=&quot;&quot;)
ST_BFS(vtx, aList, 0)
print()

&gt; 출력
ST_BFS_AM: ( U V ) ( U W ) ( V X ) ( W Y ) </code></pre>
<hr>
<h3 id="05-최소-비용-신장-트리">05 &quot;최소 비용 신장 트리&quot;</h3>
<p>신장 트리는 위에서 배웠듯 그래프 내의 모든 정점을 포함하는 트리다.
이번엔 좀 특별한 트리를 배우려고 한다.</p>
<ul>
<li>그래프의 모든 정점은 연결되어야 하고,</li>
<li>연결에 필요한 간선의 가중치 합(비용)이 최소가 되어야하는</li>
</ul>
<p>최소 비용 신장 트리는 무엇일까?</p>
<p>만약 연결 방법 중에 사이클이 있으면 어떻게 될까?
사이클은 두 사이트를 연결하는 두 가지 경로를 제공하므로 비용 측면에서 절대 손해다.
이 문제의 해답은 주어진 그래프의 신장 트리 중에서 하나가 된다.
<code>최소 신장 트리(MST:Mimimum Spanning Tree)</code> 또는 <code>최소 비용 신장 트리</code>는 이처럼 가중치 그래프의 여러 신장 트리 중에서 간선의 가중치 합이 최소인 것을 말한다.
MST의 응용분야는 다음과 같이 다양하다.</p>
<ul>
<li>통신망: 모든 사이트가 연결되도록 하면서 비용을 최소화하는 문제</li>
<li>도로망: 도시들을 모두 연결하면서 도로의 길이가 최소가 되도록 하는 문제</li>
<li>배관 작업: 파이프를 모두 연결하면서 파이프의 길이를 최소화하는 문제</li>
<li>전기 회로: 단자들을 모두 연결하면서 전선의 길이를 최소화하는 문제</li>
</ul>
<p>최소 신장 트리를 구하는 방법에는 <code>Kruskal</code>과 <code>Prim(프림)</code>의 알고리즘이 있다.
우리는 Prim(프림)의 알고리즘에 대해 공부해보자.</p>
<h4 id="프림-알고리즘"><em>프림 알고리즘</em></h4>
<p>프림(Prim)은 하나의 정점에서부터 시작하여 최소 신장 트리(MST)를 단계적으로 확장해나가는 방법을 사용한다.
처음에는 MST에 시작정점만 포함되고, 다음부터 현재까지 만들어지 MST에 인접한 정점 중에서 간선의 가중치가 가장 작은(최소 간선) 정점을 선택하여 MST를 확장한다.
그리고 이 과정은 MST에 모든 정점이 삽입될 때까지 계속된다.
자연어로 기술한 알고리즘은 다음과 같다.</p>
<pre><code class="language-txt"># 코드 8.6: 프림의 최소 신장 트리 알고리즘(자연어)
Prim()
그래프에서 시작정점을 선택하여 초기 트리(MST)를 만든다.
MST와 인접한 정점 중 간선의 가중치가 가장 작은 정점 v를 선택한다.
v와 이때의 간선을 MST에 추가한다.
아직 모든 정점이 삽입되지 않았으면 처음단계로 돌아가 순서를 반복한다.</code></pre>
<p>여기서 &#39;MST에 인접한 정점 중에서 간선의 가중치가 가장 작은 정점을 선택한다.&#39;라는 것이 뭔가 애매하다. 
정점들의 상태를 저장할 배열을 두 개 사용할 것이다. 배열의 크기는 정점 수와 같다.</p>
<ul>
<li><p>selected[]
: 정점이 MST에 포함되어있는지를 기록한다. 
selected[v]가 True면 v가 MST에 포함된 것이다.
맨 처음에는 MST가 공백 트리이므로 배열의 모든 요소가 False가 되고 단계마다 선택되는 정점이 True로 변경된다.</p>
</li>
<li><p>dist[]
: 현재까지 구성된 MST와 정점 사이의 최단 거리를 저장한다.
처음에는 시작정점의 값만 0이고 나머지는 모두 무한대(∞)다.
새로운 정점 u가 MST에 추가되면 u에 인접한 정점 v의 최단 거리 dist[v]가 더 짧아질 수 있다.
만약, 즉, 간선 (u,v)의 가중치가 기존의 dist[v]보다 적으면 dist[v]를 (u,v)의 가중치로 변경하는 것이다.</p>
</li>
</ul>
<p>이렇게 하면 &#39;MST에 인접한 정점 중에서 간선의 가중치가 가장 작은 정점을 선택한다.&#39;을 확실하게 처리할 수 있다.
아직 선택되지 않은(selected가 False인) 정점 중에서 dist가 최소인 것을 찾으면 된다.
아래 예시를 통해 알고리즘 동작 단계를 확인해보자.</p>
<table>
<thead>
<tr>
<th>-</th>
<th>단계</th>
<th>배열</th>
<th>그래프</th>
<th>최소 신장 트리(MST)</th>
</tr>
</thead>
<tbody><tr>
<td>0</td>
<td>맨 처음에 MST는 공백 트리.<br>(selected 모두 False)<br>dist[]는 시작 정점 A만 0이고<br>나머지 ∞로 초기화.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/e4754a4f-f53c-432b-a239-2bd0fb28e942/image.png"></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/dd53dbf4-d9e4-44b6-af63-f552935033ec/image.png" width=80%></td>
<td></td>
</tr>
<tr>
<td>1</td>
<td>dist가 최소인 A를 MST에 넣는다.(selected 갱신)<br>이제 A의 인접 정점들의 dist를 갱신해야한다.<br>만약, A와 인접 정점 사이의 간선의 가중치가 기존 dist보다 작다면,<br>가중치를 갱신해야 한다.<br>B와 D가 25, 12로 변경된다.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/8b533aa6-b7de-44a1-9c8c-0e955a59eb90/image.png"></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/2729f009-3054-40cf-b564-39aa4e14400d/image.png" width=80%></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/6c411b86-b289-487a-9b0f-96fd28afbea4/image.png" width=14%></td>
</tr>
<tr>
<td>2</td>
<td>dist가 최소인 D를 선택하고, 정점 D와 간선(A,D)를 MST에 넣는다.<br>E와 G가 아직 선택되지 않은 인접 정점이고, 해당 dist를 17과 37로 갱신한다.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/484ab518-0336-4e53-904a-62ccec3c805a/image.png"></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/72795b51-431d-4011-9515-fd35b026e1b9/image.png" width=80%></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/9b55268a-eb3c-4f25-b88c-00f96ec34029/image.png" width=14%></td>
</tr>
<tr>
<td>3</td>
<td>dist가 최소인 E가 선택되고, E와 간선 (D, E)를 MST에 넣는다.<br>B, F, G가 아직 선택되지 않은 인접 정점이라 기존 dist와 비교해 이들을 각각 15, 14, 19로 갱신해준다.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/3c4fd422-0c64-496f-839c-55cedf20d6d7/image.png"></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/e0d299c2-af75-4074-86c1-d9c0eeaa7b58/image.png" width=80%></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/5e423510-b72d-499c-a2b9-346bb0eeba30/image.png" width=47%></td>
</tr>
<tr>
<td>4</td>
<td>dist가 최소인 F가 선택되고, F와 간선 (E, F)를 MST에 넣는다.<br>C, G가 아직 선택되지 않은 인접 정점이라 기존 dist와 비교해 C를 16으로 갱신해준다.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/3219104f-70e9-4d34-9003-a9467eb45631/image.png"></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/84ac433a-ebe0-488b-b21b-b56d5362c300/image.png" width=80%></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/12cabfd3-6814-4f34-9a5f-a86b854ebb12/image.png" width=80%></td>
</tr>
<tr>
<td>5</td>
<td>dist가 최소인 B가 선택되고, B와 간선 (B,E)를 MST에 넣는다.<br>C가 인접 정점인데 기존 dist보다 (B, C)의 가중치가 작아 C를 10으로 갱신한다.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/95b5a021-c281-4f28-a554-9f29d0e36c13/image.png"></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/62a481d6-46b4-4454-8371-95aa03c227b4/image.png" width=80%></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/0113361d-04e4-44f7-b216-6458da9915cb/image.png" width=80%></td>
</tr>
<tr>
<td>6</td>
<td>dist가 최소인 C가 선택되고, C와 간선 (B, C)를 MST에 넣는다.<br>선택되지 않은 인접 정점이 없다.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/b25e5e6f-600d-473d-977c-a147e32d9682/image.png"></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/230726e1-a18b-48de-ad28-0945e05acf6e/image.png" width=80%></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/829b07da-a4d1-4028-9852-f564c8adaede/image.png" width=80%></td>
</tr>
<tr>
<td>7</td>
<td>마지막으로 선택되지 않은 G정점을 선택하고, G를 간선 (E, G)와 함께 MST에 넣는다.<br>MST가 완성되었다.</td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/b6c8bcac-5d9c-403f-bd62-9116714285bf/image.png"></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/16bcff9e-c084-4b6d-b094-5c08b3ca96ee/image.png" width=80%></td>
<td><img src="https://velog.velcdn.com/images/mingming_eee/post/9f70f18a-70ce-4940-bbb9-94199fea6ac6/image.png" width=80%></td>
</tr>
</tbody></table>
<h4 id="프림-알고리즘의-구현"><em>프림 알고리즘의 구현</em></h4>
<p>이제 프림 알고리즘의 단계를 이해했으니 파이썬으로 알고리즘을 구현할 수 있다.
먼저 MST에 포함되지 않은 정점 중에서 dist가 최소인 것을 탐욕적으로 찾아 인덱스를 반환하는 함수는 다음과 같다.
(탐욕적 기법은 아래 추가로 내용을 다룰 예정이다.)</p>
<pre><code class="language-py"># 코드 8.7: MST에 포함되지 않은 최소 dist의 정점 찾기
INF = 999
def getMinVertex(dist, selected):
    minv = 0
    mindist = INF
    for v in range(len(dist)):
        if selected[v]==False and dist[v] &lt; mindist:
            mindist = dist[v]
            minv = v
    return minv

# 코드 8.8:프림의 최소 신장 트리 알고리즘
def MSTPrim(vertex, adj):
    n = len(vertex)
    dist = [INF]*n
    selected = [False]*n
    dist[0] = 0

    for i in range(n):  # n개의 정점을 MST에 추가하면 종료.
        u = getMinVertex(dist, selected)
        selected[u] = True
        print(vertex[u], end=&#39; &#39;)
        for v in range(n):
            # 간선 (u, v)가 있고, v가 MST에 없다면
            if adj[u][v] != 0 and not selected[v]:
                if adj[u][v] &lt; dist[v]:             # (u, v)가 dist[v]보다 작으면
                    dist[v] = adj[u][v]             # dist[v] 갱신
        print(&#39;: &#39;, dist)   # 중간 결과 출력
    print()

# Prim의 MST 테스트 프로그램
vertex =   [&#39;A&#39;,    &#39;B&#39;,    &#39;C&#39;,    &#39;D&#39;,    &#39;E&#39;,    &#39;F&#39;,    &#39;G&#39;]
weight = [ [0,       25,        INF,    12,      INF,     INF,        INF],
           [25,        0,        10,        INF,    15,       INF,        INF],
           [INF,    10,        0,        INF,    INF,    16,        INF],
           [12,        INF,    INF,    0,      17,        INF,    37],
           [INF,    15,        INF,    17,        0,      14,        19],
           [INF,    INF,    16,        INF,    14,        0,        42],
           [INF,    INF,    INF,    37,        19,        42,        0]]    

print(&quot;MST By Prim&#39;s Algorithm&quot;)
MSTPrim(vertex, weight)

&gt; 출력
MST By Prim&#39;s Algorithm
A :  [0, 25, 999, 12, 999, 999, 999]
D :  [0, 25, 999, 12, 17, 999, 37]
E :  [0, 15, 999, 12, 17, 14, 19]
F :  [0, 15, 16, 12, 17, 14, 19]
B :  [0, 15, 10, 12, 17, 14, 19]
C :  [0, 15, 10, 12, 17, 14, 19]
G :  [0, 15, 10, 12, 17, 14, 19]</code></pre>
<p>여기서 무한대는 999로 표현되었다.</p>
<p>프림 알고리즘은 얼마나 빠를까?
첫 for문인 외부 루프에서 정점의 수 n만큼 반복된다.
내부에서는 getMinVertex() 함수에 반복문이 있는데, 역시 n번 반복한다.
또 for문이 하나 더 내부 루프로 있으므로 이때도 n번 반복한다.
따라서 이 알고리즘은 외부와 내부 루프의 반복 횟수의 곱에 비례하는 연산이 필요할 것이고, 시간 복잡도는 $$O(n^2)$$가 된다.</p>
<h3 id="-탐욕적-기법greedy-method⭐">+) 탐욕적 기법(greedy method)⭐</h3>
<p><code>탐욕적 기법(greedy method)</code>는 단순하고 직관적인 방법으로 모든 경우를 고려해 보고 가장 좋은 답을 찾는 것이 아니라 어떤 결정을 해야 할 때마다 &quot;그 순간에 최적&quot;이라고 생각되는 것을 선택하는 방법이다. &quot;근시안적&quot;인 알고리즘이라 할 수 있다.
순간에 최적이라고 판단했던 선택들을 모아 만든 최종적인 답이 &#39;궁극적으로 최적&#39;인 최적해가 될까? 항상 그렇진 않다.
따라서 탐욕적 기법이 사용될 수 있는 경우는 다음과 같이 두 가지로 제한된다.</p>
<ol>
<li>최소 비용 신장 트리를 위한 프림 알고리즘, 최단 경로 거리를 구하는 다익스트라 알고리즘 등</li>
<li>시간이나 공간적인 제약으로 최적해가 현실적으로 불가능한 경우.(분기 한정 기법에서 좋은 한계값을 구할 때)</li>
</ol>
<h4 id="거스름돈-동전-최소화"><em>거스름돈 동전 최소화</em></h4>
<p>액면가가 서로 다른 m 가지의 동전 $${C_1, C_2, ..., C_m}$$이 있다.
거스름돈으로 V원을 동전으로만 돌려주어야 한다면 최소 몇 개의 동전이 필요한지를 구하세요.
단, 모든 동전은 무한히 사용할 수 있고, 액수가 큰 것부터 내림차순으로 순서대로 정렬되어 있다.</p>
<p>우리나라에는 {500원, 100원, 50원, 10원, 5원, 1원}의 6가지 동전이 사용되고 있다.
이 동전들로 몇 가지 거스름돈을 최소 동전으로 만들어보자.</p>
<ul>
<li>거스름돈 620원: 500원 + 100원 + 10원×2 → 동전 4개</li>
<li>거스름돈 345원: 100원×3 + 10원×4 + 5원 → 동전 8개</li>
<li>거스름돈 572원: 500원 + 50원 + 10원×2 + 1원×2 → 동전 6개</li>
</ul>
<p>액면가가 가장 높은 동전부터 탐욕적으로 최대한 사용하면서 거스름돈을 맞추면 어렵지 않게 동전 개수를 최소로 만들 수 있다.
하지만 만약 60원이라는 동전이 새로 생기게 된다면 620원의 경우 500원 + 60원×2 로 동전 3개만으로도 최소 동전을 만들 수 있기 때문에 탐욕적 기법이 항상 최적해를 구하진 않는다.
다만, 다음과 같은 동전 체계를 갖는다면 탐욕적 기법이 항상 최적해를 구하게 된다.
<strong>동전의 액면가 중에서 어떤 두 개를 고르더라도 큰 액면가를 작은 액면가로 나누어 떨어지는 동전 체계를 갖는다면 최적해를 보장한다. 작은 액면가를 여러 개 모으면 반드시 큰 액변가를 만들 수 있기 때문이다.</strong>
우리나라 동전이나 지폐의 단위는 이런 방법을 유지하기 때문에 이런 쳬게에서는 탐욕적 알고리즘이 항상 최적해를 보장한다.</p>
<h4 id="분할-가능한-배낭-채우기"><em>분할 가능한 배낭 채우기</em></h4>
<p>이 문제에 대한 두 가지 &#39;탐욕&#39;을 생각해 보자.</p>
<ul>
<li>탐욕 1: 무게와 상관없이 가장 비싼 물건부터 넣는 방법.</li>
<li>탐욕 2: 단위 무게당 가격이 가장 높은 물건부터 넣는 방법.</li>
</ul>
<p><strong>1. 배낭 채우기 문제</strong>
배낭 채우기 문제는 위 두 가지 경우 모두 탐욕적 기법으로 최적해를 구하지 못한다.
최적해가 아닌 상황을 예시를 통해 알아보자.
세 개의 물건 A=(12kg, 120만원), B=(10kg, 80만원), C=(8kg, 60만원)이 있고 배낭의 용량이 18kg다. 
최적해는 B와 C를 넣는 경우고 최대 가치는 140만원이다.</p>
<ul>
<li>탐욕 1: 가장 비싼 물건은 A다. 따라서 A를 넣게 되면 B나 C를 넣을 용량이 안되기 때문에 배낭 가치는 120만원이고 최적해(140만원)보다 적다.</li>
<li>탐욕 2: 단위 무게당 가장 비싼 물건도 A다. 따라서 탐욕 1과 같이 최적해가 아니다.</li>
</ul>
<p>하지만 이 문제를 약간 변형하면 최적해를 구할 수 있다.</p>
<p><strong>2. 분할 가능한 배낭 채우기 문제</strong>
만약 물건들이 소분 가능해 일부분만 배낭에 넣을 수 있다면 어떨까? 이를 분할 가능한 배낭 채우기(Fractional Knapsack)문제라고 한다.</p>
<p>각각 무게가 $$wgt_i$$이고 가치가 $$val_i$$인 $$n$$개의 물건이 있고, 이것을 배낭에 넣으려고 한다.
배낭에는 용량(최대 무게) $$W$$까지만 넣을 수 있다.
물건들의 가치의 합이 최대가 되도록 배낭을 채우고, 이 때 배나의 최대 가치를 구해보자.
단, 물건들은 나누어 일부분만 넣을 수도 있다.</p>
<p>이 문제에서는 배낭 채우기 문제와 달리 항상 배낭을 최대 용량으로 채울 수 있다.
파이썬 알고리즘으로 이 문제를 해결해 보자.</p>
<pre><code class="language-py"># 분할 가능한 배낭 채우기(탐욕적 기법)
def KnapSackFrac(wgt, val, W):
    bestVal = 0                 # 최대가치
    for i in range(len(wgt)):   # 단가가 높은 물건부터 처리
        if W == 0:              # 용량이 다 찼으면 채우기 종료
            break
        if W &gt;= wgt[i]:
            W -= wgt[i]
            bestVal += val[i]
        else:
            fraction = W / wgt[i]
            bestVal += val[i] * fraction
            break
    return bestVal  # 최대 가치 반환

# 테스트 프로그램
weight = [12, 10, 8]    # (정렬됨)
value = [120, 80, 60]   # (정렬됨)
W = 18
print(&quot;Fractional Knapsack(18):&quot;, KnapSackFrac(weight, value, W))

&gt; 출력
Fractional Knapsack(18): 168.0</code></pre>
<p>이 알고리즘은 넣을 수 있는 모든 공간을 항상 무게당 가격이 가장 높은 것부터 채우기 때문에 최적해를 확실히 보장한다.
알고리즘의 복잡도는 $$O(n)$$이다. 반복문이 하나 밖에 없기 때문이다.</p>
<hr>
<h2 id="후기">후기</h2>
<p>드디어 파이썬으로 배우는 자료구조 책도 완료했다!
부트 캠프 마지막쯤 부터 시작해서 이렇게 오래 걸리게 될 줄 몰랐다.😢
특히 이번 chapter에서는 그래프가 많아서 일일이 그리느라 더 오래걸렸던거 같다.😂
9월 초반을 마이다스 아이티 온라인 인턴십에 쏟아붓느라, 추석 연휴를 즐기느라 3주 동안은 코딩을 못했었다. 이 인턴십 관련해서 후기도 조만간 올릴 예정이다.😉</p>
<p>&#39;열혈 C언어&#39; 완독과 &#39;자료구조와 알고리즘 with 파이썬&#39; 완독을 했으니,
이제 &#39;열혈 자료구조&#39;를 통해 C언어로 자료구조를 알아보려한다.
왜이렇게까지 하나 싶겠지만,,, 재밌으니깐😎 새로운 개념이나 배움은 언제든 재밌다🤗💗</p>
<p>그럼 모두 keep coding~!</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/ec665ab2-f433-46b8-b20c-1d6397a9aee3/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C언어] 도전! 프로그래밍 4]]></title>
            <link>https://velog.io/@mingming_eee/c-4-28</link>
            <guid>https://velog.io/@mingming_eee/c-4-28</guid>
            <pubDate>Wed, 25 Sep 2024 12:26:40 GMT</pubDate>
            <description><![CDATA[<h2 id="chapter-28-도전-프로그래밍-4">Chapter 28. 도전! 프로그래밍 4</h2>
<h3 id="도전1">도전1</h3>
<p>간단한 도서 관리용 프로그램을 작성해보자.
[제목, 저자명, 페이지수]에 대한 정보를 저장할 수 있는 구조체를 정의하고, 구조체 배열을 선언해서 도서에 대한 정보를 저장하는 구조로 작성해 보자.
main 함수에서는 사용자로부터 3권의 도서에 대한 정보를 입력 받고, 입력이 끝나면 도서에 대한 내용을 출력해 주도록 하자.</p>
<ul>
<li><p>실행 예시
도서 정보 입력
저자: Yoon
제목: C Programming
페이지 수 : 200
저자: Hong
제목: C++ Programming
페이지 수 : 250
저자: James
제목: OS for Programmer
페이지 수 : 300</p>
<p>도서 정보 출력
book 1
저자: Yoon
제목: C Programming
페이지 수 : 200
book 2
저자: Hong
제목: C++ Programming
페이지 수 : 250
book 3
저자: James
제목: OS for Programmer
페이지 수 : 300</p>
</li>
</ul>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;string.h&gt;

struct Book {
    char title[50];
    char author[50];
    int page;
} Book;

int main()
{
    struct Book books[3];
    int i;

    printf(&quot;&lt;도서 정보&gt; - 입력\n&quot;);
    for(i = 0; i &lt; 3; i++)
    {
        printf(&quot;저자: &quot;);
        fgets(books[i].author, sizeof(books[i].author), stdin);
        strtok(books[i].author, &quot;\n&quot;);  // 개행 문자 제거

        printf(&quot;제목: &quot;);
        fgets(books[i].title, sizeof(books[i].title), stdin);
        strtok(books[i].title, &quot;\n&quot;); // 개행 문자 제거

        printf(&quot;페이지: &quot;);
        scanf(&quot;%d&quot;, &amp;books[i].page);
        getchar(); // 개행 문자 제거
        printf(&quot;\n&quot;);
    }

    printf(&quot;&lt;도서 정보&gt; - 출력\n&quot;);
    for(i = 0; i &lt; 3; i++)
    {
        printf(&quot;도서 %d\n&quot;, i + 1);
        printf(&quot;제목: %s\n&quot;, books[i].title);
        printf(&quot;저자: %s\n&quot;, books[i].author);
        printf(&quot;페이지: %d\n&quot;, books[i].page);
        printf(&quot;\n&quot;);
    }

    return 0;
}

&gt;출력
&lt;도서 정보&gt; - 입력
저자: Yoon
제목: C Programing
페이지: 200

저자: Hong
제목: C++ Programing
페이지: 250

저자: James
제목: OS for Programmer
페이지: 300

&lt;도서 정보&gt; - 출력
도서 1
제목: C Programing
저자: Yoon
페이지: 200

도서 2
제목: C++ Programing
저자: Hong
페이지: 250

도서 3
제목: OS for Programmer
저자: James
페이지: 300</code></pre>
<p><strong>-풀이-</strong>
처음에는 바로 printf와 scanf를 사용해서 문제를 풀려고 했는데 그렇게 하다보니 제목에서 띄어쓰기가 들어가면 페이지를 입력할 수 없게 되었다.
따라서 fgets함수를 이용해서 사용자로부터 입력받게 수정했다.
<code>strtok</code>함수는 string을 tokenize로 문자열(string)을 토큰(token)처럼 조각조각 내는 함수다. 쉼표(,)와 띄어쓰기( )를 구분자로 넣어서 띄어쓰기를 제외한 단어들을 가져올 수 있다.</p>
<hr>
<h3 id="도전-2">도전 2</h3>
<p>도전 1에서 구현한 프로그램에 약간의 변경을 줘보자.
구조체 배열을 선언하는 것이 아니라, 구조체 포인터 배열을 선언하고 구조체 변수를 동적으로 할당하는 형태로 프로그램을 재 구현해 보자.
그리고 도전 1에서 구현한 방법보다 도전 2에서 구현한 방법이 지니는 장점이 무엇인지도 생각해보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;

struct Book {
    char title[50];
    char author[50];
    int page;
};

int main() {
    struct Book **books;  // 구조체 포인터 배열
    int i;
    int num_books = 3;    // 도서 개수

    // 구조체 포인터 배열 동적 할당
    books = (struct Book **)malloc(num_books * sizeof(struct Book *));
    for (i = 0; i &lt; num_books; i++) {
        books[i] = (struct Book *)malloc(sizeof(struct Book));
    }

    printf(&quot;&lt;도서 정보&gt; - 입력\n&quot;);
    for (i = 0; i &lt; num_books; i++) {
        printf(&quot;저자: &quot;);
        fgets(books[i]-&gt;author, sizeof(books[i]-&gt;author), stdin);
        strtok(books[i]-&gt;author, &quot;\n&quot;);  // 개행 문자 제거

        printf(&quot;제목: &quot;);
        fgets(books[i]-&gt;title, sizeof(books[i]-&gt;title), stdin);
        strtok(books[i]-&gt;title, &quot;\n&quot;); // 개행 문자 제거

        printf(&quot;페이지: &quot;);
        scanf(&quot;%d&quot;, &amp;books[i]-&gt;page);
        getchar(); // 개행 문자 제거
        printf(&quot;\n&quot;);
    }

    printf(&quot;&lt;도서 정보&gt; - 출력\n&quot;);
    for (i = 0; i &lt; num_books; i++) {
        printf(&quot;도서 %d\n&quot;, i + 1);
        printf(&quot;제목: %s\n&quot;, books[i]-&gt;title);
        printf(&quot;저자: %s\n&quot;, books[i]-&gt;author);
        printf(&quot;페이지: %d\n&quot;, books[i]-&gt;page);
        printf(&quot;\n&quot;);
    }

    // 동적으로 할당한 메모리 해제
    for (i = 0; i &lt; num_books; i++) {
        free(books[i]);
    }
    free(books);

    return 0;
}

&gt;출력
&lt;도서 정보&gt; - 입력
저자: Yoon
제목: C Programing
페이지: 200

저자: Min
제목: Choregraphy Basic
페이지: 150

저자: Kim
제목: What is Love
페이지: 100

&lt;도서 정보&gt; - 출력
도서 1
제목: C Programing
저자: Yoon
페이지: 200

도서 2
제목: Choregraphy Basic
저자: Min
페이지: 150

도서 3
제목: What is Love
저자: Kim
페이지: 100</code></pre>
<p><strong>-풀이-</strong>
&lt;도전 1과 도전 2 비교&gt;</p>
<ol>
<li><p>유연한 메모리 관리:
도전 1 - 고정된 크기의 배열을 사용하여 메모리를 할당하므로, 프로그램 시작 시 도서의 개수를 정해야 함.
도전 2 - 동적 메모리 할당을 사용하여 필요에 따라 도서의 개수를 조절할 수 있음. 이로 인해 메모리 사용이 더 효율적.</p>
</li>
<li><p>메모리 사용 최적화:
도전 1 - 배열의 크기를 미리 정해야 하므로, 필요하지 않은 메모리가 낭비될 수 있음.
도전 2 - 입력받는 도서의 수에 따라 메모리를 동적으로 조절할 수 있어, 필요한 만큼만 메모리를 사용할 수 있음.</p>
</li>
<li><p>재사용 가능성:
도전 1 - 배열의 크기를 변경하려면 프로그램을 수정해야 함.
도전 2 - 동적 할당을 사용하면, 프로그램 실행 중에 도서의 수를 쉽게 변경하거나 확장할 수 있음.</p>
</li>
</ol>
<p>[결론]
동적 메모리 할당을 사용하면 더 유연하고 효율적인 메모리 관리가 가능하여, 다양한 조건에서 프로그램을 보다 쉽게 수정하고 확장할 수 있다.</p>
<hr>
<h3 id="도전-3">도전 3</h3>
<p>복소수(Complex Number)를 나타내는 구조체를 정의하고, 복소수의 덧셈과 곱셈을 위한 함수를 각각 정의하자. 그리고 이를 기반으로 프로그램 사용자로부터 두 개의 복소수 정보를 입력 받아서 두 복소수의 덧셈과 곱셈의 결과를 출력하는 프로그램을 작성하자.</p>
<ul>
<li><p>실행 예시
복소수 입력1[실수 허수]: 1.2 2.4
복소수 입력2[실수 허수]: 1.1 2.2
합의 결과] 실수: 2.300000, 허수: 4.600000
곱의 결과] 실수: -3.960000, 허수: 5.280000</p>
</li>
<li><p>복소수 공식</p>
<ul>
<li>덧셈공식: (a+bi)+(c+di) = (a+c) + (b+d)i</li>
<li>곱셈공식: (a+bi)*(c+di) = ac - bd + bci +adi</li>
</ul>
</li>
</ul>
<pre><code class="language-c">#include &lt;stdio.h&gt;

typedef struct {
    double real;
    double imaginary;
} ComplexNumber;

ComplexNumber addComplexNumbers(ComplexNumber num1, ComplexNumber num2)
{
    ComplexNumber result;
    result.real = num1.real + num2.real;
    result.imaginary = num1.imaginary + num2.imaginary;
    return result;
}

ComplexNumber multiplyComplexNumbers(ComplexNumber num1, ComplexNumber num2)
{
    ComplexNumber result;
    result.real = num1.real * num2.real - num1.imaginary * num2.imaginary;
    result.imaginary = num1.real * num2.imaginary + num1.imaginary * num2.real;
    return result;
}

int main()
{
    ComplexNumber num1, num2, sum, mul;
    printf(&quot;복소수 입력1[실수 허수]: &quot;);
    scanf(&quot;%lf %lf&quot;, &amp;num1.real, &amp;num1.imaginary);
    printf(&quot;복소수 입력2[실수 허수]: &quot;);
    scanf(&quot;%lf %lf&quot;, &amp;num2.real, &amp;num2.imaginary);

    sum = addComplexNumbers(num1, num2);
    mul = multiplyComplexNumbers(num1, num2);

    printf(&quot;합의 결과] 실수: %.5lf, 허수: %.5lf\n&quot;, sum.real, sum.imaginary);
    printf(&quot;곱의 결과] 실수: %.5lf, 허수: %.5lf\n&quot;, mul.real, mul.imaginary);

    return 0;
}

&gt;출력
복소수 입력1[실수 허수]: 1.2 2.4
복소수 입력2[실수 허수]: 1.1 2.2
합의 결과] 실수: 2.30000, 허수: 4.60000
곱의 결과] 실수: -3.96000, 허수: 5.28000</code></pre>
<p><strong>-풀이-</strong>
복소수를 오랜만에 다시 보게 됐는데 단순하게 복소수의 덧셈 공식과 곱셈 공식을 함수로 만들어서 해결했다.</p>
<hr>
<h3 id="도전-4">도전 4</h3>
<p>문자열을 저장하고 있는 파일을 열어서 A와 P로 시작하는 단어의 수를 세어서 출력하는 프로그램을 작성해보자.
단, 모든 단어는 공백문자(space bar, \t, \n)에 의해서 구분된다고 가정한다.</p>
<ul>
<li>실행 예시
실행파일의 이름이 wordcnt.exe이고 대상파일의 이름이 text.txt인 경우의 실행의 예
명령어: .\wordcnt text.txt
A로 시작하는 단어의 수: 4
P로 시작하는 단어의 수: 3</li>
</ul>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;ctype.h&gt;

#define MAX_WORD_LENGTH 100

int main(int argc, char *argv[])    // argc: 명령행 인자의 수, argv: 명령행 인자를 문자열 배열로 저장.
{
    if (argc != 2)  // 파일 이름이 인자로 제공되지 않으면 오류 메세지 출력하고 종료.
    {
        printf(&quot;사용법: %s &lt;파일이름&gt;\n&quot;, argv[0]);
        return 1;
    }

    FILE *file = fopen(argv[1], &quot;r&quot;);   // 파일 열기. 실패시 perror 함수 사용하여 오류 메세지 출력하고 종료.
    if (file == NULL)
    {
        perror(&quot;파일을 열 수 없습니다&quot;);
        return 1;
    }

    char word[MAX_WORD_LENGTH]; // 각 단어 저장.
    int countA = 0, countP = 0;

    while (fscanf(file, &quot;%s&quot;, word) == 1)   // fscanf으로 파일에서 단어 하나씩 읽어오기. %s로 공백으로 구분된 단어 읽음.
    {
        // 첫 글자를 대문자로 변환하여 비교
        char firstChar = toupper(word[0]);  // 단어의 첫 글자 대문자로 변환.
        if (firstChar == &#39;A&#39;) {
            countA++;   // A로 시작하는 단어 카운트.
        } else if (firstChar == &#39;P&#39;) {
            countP++;   // P로 시작하는 단어 카운트.
        }
    }

    fclose(file);

    printf(&quot;A로 시작하는 단어의 수: %d\n&quot;, countA);
    printf(&quot;P로 시작하는 단어의 수: %d\n&quot;, countP);

    return 0;
}

&gt;명령어 &amp; 출력
&gt;gcc -o wordcnt ch4.c    
&gt;.\wordcnt text.txt
A로 시작하는 단어의 수: 2
P로 시작하는 단어의 수: 6</code></pre>
<p><strong>-풀이-</strong>
ctype.h 헤더 파일은 C 표준 라이브러리의 일부로, 문자 처리에 관련된 여러 함수를 제공한다. 이 파일에 포함된 함수들은 주로 문자 분류와 변환을 위한 기능을 제공하며, 다음과 같은 함수들이 포함되어 있다.</p>
<ul>
<li>toupper(int c): 주어진 문자를 대문자로 변환. 만약 문자가 대문자일 경우 그대로 반환.</li>
<li>tolower(int c): 주어진 문자를 소문자로 변환. 만약 문자가 소문자일 경우 그대로 반환.</li>
<li>isalnum(int c), isalpha(int c), isdigit(int c) 등: 문자가 알파벳, 숫자, 또는 기타 특정 범주에 속하는지를 확인.</li>
</ul>
<p>이 프로그램에서는 toupper() 함수를 사용하여 단어의 첫 글자를 대문자로 변환하고, A 또는 P와 비교하기 위해 사용했다.</p>
<p>참고로 나의 text.txt에는 아래와 같이 적었었다.</p>
<pre><code class="language-txt">apple
avocado
banana
blackberry
blueberry
cherry tomato
cherry
coconut
grape
kiwi
lemon
lime
mango
melon
orange
papaya
peach
pear
persimmon
pineapple
plum
strawberry
tangerine
tomato
watermelon</code></pre>
<hr>
<h3 id="도전-5">도전 5</h3>
<p>두 개의 텍스트 파일이 같은지 다른지를 확인하는 프로그램을 작성해 보자.
단순히 공백문자 하나가 차이를 보여도 두 텍스트 파일은 다른 것으로 판별이 나야 한다.</p>
<ul>
<li>실행 예시
다음은 실행파일의 이름이 comp.exe이고 비교의 대상이 되는 두 파일의 이름이 각각 d1.txt와 d2.txt인 경우의 실행의 예이다.</li>
</ul>
<p>명령어: .\comp d1.txt d2.txt
두 개의 파일은 완전히 일치 합니다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        printf(&quot;사용법: %s &lt;파일1&gt; &lt;파일2&gt;\n&quot;, argv[0]);
        return 1;
    }

    FILE *file1 = fopen(argv[1], &quot;r&quot;);
    FILE *file2 = fopen(argv[2], &quot;r&quot;);

    if (file1 == NULL || file2 == NULL)
    {
        perror(&quot;파일을 열 수 없습니다&quot;);
        return 1;
    }

    char ch1, ch2;
    int areEqual = 1;  // 파일이 같다고 가정

    while ((ch1 = fgetc(file1)) != EOF &amp;&amp; (ch2 = fgetc(file2)) != EOF)
        if (ch1 != ch2)
        {
            areEqual = 0;  // 파일이 다름
            break;
        }

    // 파일의 끝을 넘어선 경우: 하나의 파일이 더 길 경우
    if (areEqual &amp;&amp; (fgetc(file1) != EOF || fgetc(file2) != EOF))
        areEqual = 0;  // 파일 길이가 다름

    fclose(file1);
    fclose(file2);

    if (areEqual)
        printf(&quot;두 개의 파일은 완전히 일치합니다.\n&quot;);
    else
        printf(&quot;두 개의 파일은 다릅니다.\n&quot;);

    return 0;
}

&gt;명령어 &amp; 출력
&gt;gcc -o comp ch5.c
&gt;.\comp text.txt text1.txt    //아까 위에서 사용한 파일 또 사용.
두 개의 파일은 완전히 일치합니다.</code></pre>
<p><strong>-풀이-</strong>
두 파일의 길이 또는 내용을 확인하는 것이 관건이었다.</p>
<hr>
<h3 id="도전-6">도전 6</h3>
<p>전화번호 관리 프로그램을 작성해 보자.
이 프로그램이 기본적으로 지녀야 하는 기능은 다음과 같다.</p>
<ul>
<li>입력: 이름과 전화번호의 입력</li>
<li>삭제: 이름을 입력하여 해당 이름의 정보 삭제</li>
<li>검색: 이름을 입력하여 해당 이름의 정보 출력</li>
<li>전체 출력: 저장된 모든 이름과 전화번호 정보를 출력</li>
</ul>
<p>실행 예시와 비슷하게 동작하는 전화번호 관리 프로그램을 구현하기 바란다.</p>
<ul>
<li>실행 예시
```</li>
<li><strong><strong>MENU*</strong></strong></li>
</ul>
<ol>
<li>Insert</li>
<li>Delete</li>
<li>Search</li>
<li>Print All</li>
<li>Exit
Choose the item: 1
[INSERT]
Input Name: Yoon
Input Tel Number: 333-4444<pre><code>         Data Inserted</code></pre></li>
</ol>
<p><strong><strong><em>MENU</em></strong></strong></p>
<ol>
<li>Insert</li>
<li>Delete</li>
<li>Search</li>
<li>Print All</li>
<li>Exit
Choose the item: 2
[DELETE]
Input Name that You Want to Delete: Yoon<pre><code>         Data Deleted</code></pre></li>
</ol>
<p><strong><strong><em>MENU</em></strong></strong></p>
<ol>
<li>Insert</li>
<li>Delete</li>
<li>Search</li>
<li>Print All</li>
<li>Exit
Choose the item: 3
[SEARCH]
Input Name that You Want to Find: Yoon
[Data]
Name: Yoon
Tel: 333-4444<pre><code>         Data Searched</code></pre></li>
</ol>
<p><strong><strong><em>MENU</em></strong></strong></p>
<ol>
<li>Insert</li>
<li>Delete</li>
<li>Search</li>
<li>Print All</li>
<li>Exit
Choose the item: 4
[Print All Data]
Name: Yoon    Tel: 333-4444<pre><code>         Data Printed</code></pre><pre><code>##### 실제 문제의 실행 예시에서 insert와 print all data만 주어졌다.
</code></pre></li>
</ol>
<pre><code class="language-c">// ch6_func.h
#ifndef CH6_FUNC_H
#define CH6_FUNC_H

#define MAX_CONTACTS 100
#define NAME_LENGTH 50
#define TEL_LENGTH 15

// 연락처 구조체 정의
typedef struct {
    char name[NAME_LENGTH];
    char tel[TEL_LENGTH];
} Contact;

// 함수 선언
void insertContact();
void deleteContact();
void searchContact();
void printAllContacts();

#endif</code></pre>
<pre><code class="language-c">// ch6_func.c
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &quot;ch6_func.h&quot;   // 헤더 파일 포함

#define MAX_CONTACTS 100
#define NAME_LENGTH 50
#define TEL_LENGTH 15

// 전화번호 목록
Contact contacts[MAX_CONTACTS];
int contactCount = 0;

void InsertContact()
{
    if(contactCount &gt;= MAX_CONTACTS)
    {
        printf(&quot;전화번호 목록이 가득 찼다.(100/100)\n\n&quot;);
        return;
    }

    printf(&quot;[INSERT]\n&quot;);
    printf(&quot;Input Name: &quot;);
    fgets(contacts[contactCount].name, NAME_LENGTH, stdin);
    strtok(contacts[contactCount].name, &quot;\n&quot;);

    printf(&quot;Input Tel Number: &quot;);
    fgets(contacts[contactCount].tel, TEL_LENGTH, stdin);
    strtok(contacts[contactCount].tel, &quot;\n&quot;);

    contactCount++;
    printf(&quot;\t\t\tData Inserted\n\n&quot;);
}

void DeleteContact()
{
    char name[NAME_LENGTH];
    printf(&quot;[DELETE]\n&quot;);
    printf(&quot;Input Name that You Want to Delete: &quot;);
    fgets(name, NAME_LENGTH, stdin);
    strtok(name, &quot;\n&quot;);

    for(int i = 0; i &lt; contactCount; i++)
    {
        if(strcmp(contacts[i].name, name) == 0)
        {
            for(int j = i; j &lt; contactCount - 1; j++)
            {
                strcpy(contacts[j].name, contacts[j + 1].name);
                strcpy(contacts[j].tel, contacts[j + 1].tel);
            }
            contactCount--;
            printf(&quot;\t\t\tData Deleted\n\n&quot;);
            return;
        }
    }
    printf(&quot;No such contact found with the name &#39;%s&#39;\n\n&quot;, name);
}

void SearchContact()
{
    char name[NAME_LENGTH];
    printf(&quot;[SEARCH]\n&quot;);
    printf(&quot;Input Name that You Want to Find: &quot;);
    fgets(name, NAME_LENGTH, stdin);
    strtok(name, &quot;\n&quot;);

    for(int i = 0; i &lt; contactCount; i++)
    {
        if(strcmp(contacts[i].name, name) == 0)
        {
            printf(&quot;[Data]\n&quot;);
            printf(&quot;Name: %s\n&quot;, contacts[i].name);
            printf(&quot;Tel: %s\n&quot;, contacts[i].tel);
            printf(&quot;\t\t\tData Searched\n\n&quot;);
            return;
        }
    }
    printf(&quot;No such contact found with the name &#39;%s&#39;\n\n&quot;, name);
}

void PrintAllContacts()
{
    printf(&quot;[Print All Data]\n&quot;);
    for(int i = 0; i &lt; contactCount; i++)
    {
        printf(&quot;Name: %s\tTel: %s\n&quot;, contacts[i].name, contacts[i].tel);
    }
    printf(&quot;\t\t\tData Printed\n\n&quot;);
}</code></pre>
<pre><code class="language-c">// ch6.c
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;

// 함수 참조
void InsertContact();
void DeleteContact();
void SearchContact();
void PrintAllContacts();

int main()
{
    int choice;

    while(1)
    {
        printf(&quot;*****MENU*****\n&quot;);
        printf(&quot;1. Insert\n&quot;);
        printf(&quot;2. Delete\n&quot;);
        printf(&quot;3. Search\n&quot;);
        printf(&quot;4. Print All\n&quot;);
        printf(&quot;5. Exit\n&quot;);
        printf(&quot;Choose the item: &quot;);
        scanf(&quot;%d&quot;, &amp;choice);
        getchar();

        switch(choice)
        {
            case 1:
                InsertContact();
                break;
            case 2:
                DeleteContact();
                break;
            case 3:
                SearchContact();
                break;
            case 4:
                PrintAllContacts();
                break;
            case 5:
                printf(&quot;Exiting...\n&quot;);
                return 0;
            default:
                printf(&quot;Invalid choice! Please try again.\n\n&quot;);
        }
    }
}</code></pre>
<pre><code class="language-bash">&gt;명령어 &amp; 출력
&gt;gcc -o phonebook ch6.c ch6_func.c
&gt;.\phonebook.exe
*****MENU*****
1. Insert
2. Delete
3. Search
4. Print All
5. Exit
Choose the item: 1
[INSERT]
Input Name: Kim
Input Tel Number: 111-2222
                        Data Inserted

*****MENU*****
1. Insert
2. Delete
3. Search
4. Print All
5. Exit
Choose the item: 2
[DELETE]
Input Name that You Want to Delete: Min
No such contact found with the name &#39;Min&#39;

*****MENU*****
1. Insert
2. Delete
3. Search
4. Print All
5. Exit
Choose the item: 2
[DELETE]
Input Name that You Want to Delete: Kim
                        Data Deleted

*****MENU*****
1. Insert
2. Delete
3. Search
4. Print All
5. Exit
Choose the item: 1
[INSERT]
Input Name: Kim
Input Tel Number: 111-2222
                        Data Inserted

*****MENU*****
1. Insert
2. Delete
3. Search
4. Print All
5. Exit
Choose the item: 1
[INSERT]
Input Name: Min
Input Tel Number: 222-3333
                        Data Inserted

*****MENU*****
1. Insert
2. Delete
3. Search
4. Print All
5. Exit
Choose the item: 3
[SEARCH]
Input Name that You Want to Find: Jung
No such contact found with the name &#39;Jung&#39;

*****MENU*****
1. Insert
2. Delete
3. Search
4. Print All
5. Exit
Choose the item: 3  
[SEARCH]
Input Name that You Want to Find: Min
[Data]
Name: Min
Tel: 222-3333
                        Data Searched

*****MENU*****
1. Insert
2. Delete
3. Search
4. Print All
5. Exit
Choose the item: 4
[Print All Data]
Name: Kim       Tel: 111-2222
Name: Min       Tel: 222-3333
                        Data Printed

*****MENU*****
1. Insert
2. Delete
3. Search
4. Print All
5. Exit
Choose the item: 6
Invalid choice! Please try again.
*****MENU*****
1. Insert
2. Delete
3. Search
4. Print All
5. Exit
Choose the item: 5
Exiting...</code></pre>
<p><strong>-풀이-</strong>
나는 Part 4에서 배운 내용을 사용하기 위해서 헤더 파일과 다른 파일로 나누어 이번 문제를 해결해보려 했다.
그리고 delete와 search를 했을 때는 기존에 작성한 데이터와 맞는지 안맞는지 경우에 따라 다른 알림 문장을 보여주려 했다.</p>
<hr>
<h3 id="도전-7">도전 7</h3>
<p>도전 6에서 구현한 프로그램의 문제점은 프로그램이 종료되고 나면 기존에 저장된 데이터가 전부 사라진다는 것이다. 이 문제점을 해결하자.
프로그램이 종료되기 전에 파일을 하나 생성해서 기존에 입력받은 데이터를 저장하고, 프로그램을 다시 실행하면 파일에 저장된 데이터를 읽어 들이는 방식으로 프로그램을 변경해보자.</p>
<pre><code class="language-c">// ch7_func.h
#ifndef CH7_FUNC_H
#define CH7_FUNC_H

#define MAX_CONTACTS 100
#define NAME_LENGTH 50
#define TEL_LENGTH 15

typedef struct 
{
    char name[NAME_LENGTH];
    char tel[TEL_LENGTH];
} Contact;

void InsertContact();
void DeleteContact();
void SearchContact();
void PrintAllContacts();
void SaveContacts();    // 파일 저장 함수 추가
void LoadContacts();    // 파일 로드 함수 추가

#endif</code></pre>
<pre><code class="language-c">// ch7_func.c
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &quot;ch7_func.h&quot;

Contact contacts[MAX_CONTACTS];
int contactCount = 0;

void InsertContact() 
{
    if (contactCount &gt;= MAX_CONTACTS)
    {
        printf(&quot;전화번호 목록이 가득 찼다.(100/100)\n\n&quot;);
        return;
    }

    printf(&quot;[INSERT]\n&quot;);
    printf(&quot;Input Name: &quot;);
    fgets(contacts[contactCount].name, NAME_LENGTH, stdin);
    strtok(contacts[contactCount].name, &quot;\n&quot;); // 개행 문자 제거

    printf(&quot;Input Tel Number: &quot;);
    fgets(contacts[contactCount].tel, TEL_LENGTH, stdin);
    strtok(contacts[contactCount].tel, &quot;\n&quot;); // 개행 문자 제거

    contactCount++;
    printf(&quot;\t\t\tData Inserted\n\n&quot;);
}

void DeleteContact() 
{
    char name[NAME_LENGTH];
    printf(&quot;[DELETE]\n&quot;);
    printf(&quot;Input Name that You Want to Delete: &quot;);
    fgets(name, NAME_LENGTH, stdin);
    strtok(name, &quot;\n&quot;); // 개행 문자 제거

    for (int i = 0; i &lt; contactCount; i++) 
    {
        if (strcmp(contacts[i].name, name) == 0) 
        {
            for (int j = i; j &lt; contactCount - 1; j++) 
            {
                contacts[j] = contacts[j + 1]; // 데이터 이동
            }
            contactCount--;
            printf(&quot;\t\t\tData Deleted\n\n&quot;);
            return;
        }
    }
    printf(&quot;No such contact found with the name &#39;%s&#39;.\n\n&quot;, name);
}

void SearchContact() 
{
    char name[NAME_LENGTH];
    printf(&quot;[SEARCH]\n&quot;);
    printf(&quot;Input Name that You Want to Find: &quot;);
    fgets(name, NAME_LENGTH, stdin);
    strtok(name, &quot;\n&quot;); // 개행 문자 제거

    for (int i = 0; i &lt; contactCount; i++) 
    {
        if (strcmp(contacts[i].name, name) == 0) 
        {
            printf(&quot;[Data]\n&quot;);
            printf(&quot;Name: %s\n&quot;, contacts[i].name);
            printf(&quot;Tel: %s\n&quot;, contacts[i].tel);
            printf(&quot;\t\t\tData Searched\n\n&quot;);
            return;
        }
    }
    printf(&quot;No contact found with the name &#39;%s&#39;.\n\n&quot;, name);
}

void PrintAllContacts() 
{
    printf(&quot;[Print All Data]\n&quot;);
    for (int i = 0; i &lt; contactCount; i++) 
    {
        printf(&quot;Name: %s\tTel: %s\n&quot;, contacts[i].name, contacts[i].tel);
    }
    printf(&quot;\t\t\tData Printed\n\n&quot;);
}

void SaveContacts() 
{
    FILE *file = fopen(&quot;phonebook.txt&quot;, &quot;w&quot;);
    if (file == NULL) 
    {
        perror(&quot;Unable to open file for writing.\n&quot;);
        return;
    }
    for (int i = 0; i &lt; contactCount; i++) 
    {
        fprintf(file, &quot;%s\n%s\n&quot;, contacts[i].name, contacts[i].tel);
    }
    fclose(file);
    printf(&quot;Contacts saved to phonebook.txt\n\n&quot;);
}

void LoadContacts() 
{
    FILE *file = fopen(&quot;phonebook.txt&quot;, &quot;r&quot;);
    if (file == NULL) 
    {
        printf(&quot;No existing contact data found. Starting fresh.\n&quot;);
        return;
    }
    while (fscanf(file, &quot;%[^\n]\n%[^\n]\n&quot;, contacts[contactCount].name, contacts[contactCount].tel) == 2) 
    {
        contactCount++;
    }
    fclose(file);
    printf(&quot;Contacts loaded from phonebook.txt\n\n&quot;);
}</code></pre>
<pre><code class="language-c">// ch7.c
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &quot;ch7_func.h&quot;

int main() 
{
    int choice;

    // 데이터 로드
    LoadContacts();

    while (1) 
    {
        printf(&quot;*****MENU*****\n&quot;);
        printf(&quot;1. Insert\n&quot;);
        printf(&quot;2. Delete\n&quot;);
        printf(&quot;3. Search\n&quot;);
        printf(&quot;4. Print All\n&quot;);
        printf(&quot;5. Exit\n&quot;);
        printf(&quot;Choose the item: &quot;);
        scanf(&quot;%d&quot;, &amp;choice);
        getchar(); // 개행 문자 처리

        switch (choice) 
        {
            case 1:
                InsertContact();
                break;
            case 2:
                DeleteContact();
                break;
            case 3:
                SearchContact();
                break;
            case 4:
                PrintAllContacts();
                break;
            case 5:
                SaveContacts();  // 데이터 저장
                printf(&quot;Exiting...\n&quot;);
                return 0;
            default:
                printf(&quot;Invalid choice! Please try again.\n\n&quot;);
        }
    }
}</code></pre>
<pre><code class="language-bash">&gt;명령어 &amp; 출력
&gt;gcc -o phonebook_new ch7_func.c ch7.c
&gt;.\phonebook_new.exe
No existing contact data found. Starting fresh.
*****MENU*****
1. Insert
2. Delete
3. Search
4. Print All
5. Exit
Choose the item: 1
[INSERT]
Input Name: Kim
Input Tel Number: 111-2222
                        Data Inserted

*****MENU*****
1. Insert
2. Delete
3. Search
4. Print All
5. Exit
Choose the item: 5
Contacts saved to phonebook.txt

Exiting...
&gt;.\phonebook_new.exe
Contacts loaded from phonebook.txt

*****MENU*****
1. Insert
2. Delete
3. Search
4. Print All
5. Exit
Choose the item: 1
[INSERT]
Input Name: Min
Input Tel Number: 010-2222-3333
                        Data Inserted

*****MENU*****
1. Insert
2. Delete
3. Search
4. Print All
5. Exit
Choose the item: 3
[SEARCH]
Input Name that You Want to Find: Jung
No contact found with the name &#39;Jung&#39;.

*****MENU*****
1. Insert
2. Delete
3. Search
4. Print All
5. Exit
Choose the item: 3
[SEARCH]
Input Name that You Want to Find: Kim
[Data]
Name: Kim
Tel: 111-2222
                        Data Searched

*****MENU*****
1. Insert
2. Delete
3. Search
4. Print All
5. Exit
Choose the item: 4
[Print All Data]
Name: Kim       Tel: 111-2222
Name: Min       Tel: 010-2222-3333
                        Data Printed

*****MENU*****
1. Insert
2. Delete
3. Search
4. Print All
5. Exit
Choose the item: 1
[INSERT]
Input Name: Jung
Input Tel Number: 010-1234-5678
                        Data Inserted

*****MENU*****
1. Insert
2. Delete
3. Search
4. Print All
5. Exit
Choose the item: 5
Contacts saved to phonebook.txt

Exiting...</code></pre>
<p><strong>-풀이-</strong>
전화번호 데이터를 다른 파일에 저장하고 불러오기 위해서는 저장하는 함수와 로드하는 함수를 따로 만들어 줘야했다.</p>
<ul>
<li><p>저장하는 함수</p>
<pre><code class="language-c">void SaveContacts() 
{
    FILE *file = fopen(&quot;phonebook.txt&quot;, &quot;w&quot;);
    if (file == NULL) 
    {
        perror(&quot;Unable to open file for writing.\n&quot;);
        return;
    }
    for (int i = 0; i &lt; contactCount; i++) 
    {
        fprintf(file, &quot;%s\n%s\n&quot;, contacts[i].name, contacts[i].tel);
    }
    fclose(file);
    printf(&quot;Contacts saved to phonebook.txt\n\n&quot;);
}</code></pre>
</li>
<li><p>로드하는 함수</p>
<pre><code class="language-c">void LoadContacts() 
{
    FILE *file = fopen(&quot;phonebook.txt&quot;, &quot;r&quot;);
    if (file == NULL) 
    {
        printf(&quot;No existing contact data found. Starting fresh.\n&quot;);
        return;
    }
    while (fscanf(file, &quot;%[^\n]\n%[^\n]\n&quot;, contacts[contactCount].name, contacts[contactCount].tel) == 2) 
    {
        contactCount++;
    }
    fclose(file);
    printf(&quot;Contacts loaded from phonebook.txt\n\n&quot;);
}</code></pre>
</li>
</ul>
<p>이렇게 해서 만들어진 <code>phonebook.txt</code> 파일을 확인해보면</p>
<pre><code class="language-txt">Kim
111-2222
Min
010-2222-3333
Jung
010-1234-5678</code></pre>
<p>내용이 잘 들어가있는 것을 확인할 수 있다.</p>
<hr>
<h2 id="review"><code>&lt;Review&gt;</code></h2>
<p>Visual Studio Code에서 C언어를 하면서 컴파일할 때 한글이 제대로 컴파일 되지 않는 오류가 계속 발생해서 이에 대한 해결책을 찾아보았다. 해결 방안으로는 다음 4가지와 같다.</p>
<ol>
<li><p>소스 파일 인코딩 확인
소스 파일이 UTF-8로 인코딩되어 있는지 확인한다. Visual Studio Code에서 파일을 열고 오른쪽 아래의 인코딩 표시를 클릭하여 UTF-8로 변경한다.</p>
</li>
<li><p>GCC 컴파일 시 인코딩 설정
GCC에서 한글을 제대로 출력하도록 하기 위해, 컴파일할 때 다음과 같은 플래그를 추가할 수 있다.</p>
<pre><code class="language-shell">gcc -o example your_file.c -finput-charset=UTF-8 -fexec-charset=UTF-8</code></pre>
</li>
<li><p>콘솔 인코딩 설정 (Windows)
Windows 콘솔의 기본 인코딩이 UTF-8이 아닐 수 있다. 이를 해결하기 위해 아래 명령어를 사용하여 콘솔 인코딩을 UTF-8로 변경한다.</p>
<pre><code class="language-shell">chcp 65001</code></pre>
<p>이 명령어를 실행한 후 프로그램을 다시 실행해 보면 잘 되는 것을 확인할 수 있다.</p>
</li>
</ol>
<ul>
<li>stdio.h: 표준 입출력 함수(printf, fscanf, fopen, fclose 등)를 사용하기 위해 포함한다.</li>
<li>stdlib.h: 메모리 할당, 프로세스 제어, 변환 등의 함수(malloc, free, exit 등)를 사용하기 위해 포함한다.</li>
<li>string.h: 문자열 처리 함수(strlen, strcpy, strcat 등)를 사용하기 위해 포함한다.</li>
<li>ctype.h: 문자 변환 및 검사 함수를 사용하기 위해 포함한다.</li>
</ul>
<p>마지막 6, 7번 문제는 쉬운듯 어려운듯 여태까지 배운 것들을 모두 사용할 수 있어서 재밌는 문제였다.</p>
<h2 id="참고-자료">&lt;참고 자료&gt;</h2>
<ul>
<li><a href="https://blockdmask.tistory.com/382">개발자 지망생:티스토리</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C언어] 파일의 분할과 헤더파일의 디자인]]></title>
            <link>https://velog.io/@mingming_eee/c-4-27</link>
            <guid>https://velog.io/@mingming_eee/c-4-27</guid>
            <pubDate>Tue, 24 Sep 2024 07:23:16 GMT</pubDate>
            <description><![CDATA[<h2 id="chapter-27-파일의-분할과-헤더파일의-디자인">Chapter 27. 파일의 분할과 헤더파일의 디자인</h2>
<h3 id="27-1-파일의-분할">27-1 &quot;파일의 분할&quot;</h3>
<p>파일을 나눠서 각각의 파일에 용도 및 특성 별로 함수와 변수를 나눠서 저장하면 소스코드의 관리가 용이해진다.
다음 예제를 대상으로 파일을 나눠보도록 해보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
int num = 0;

void Increment(void)
{
    num++;
}

int GetNum(void)
{
    return num;
}

int main()
{
    printf(&quot;num: %d \n&quot;, GetNum());
    Increment();
    printf(&quot;num: %d \n&quot;, GetNum());
    Increment();
    printf(&quot;num: %d \n&quot;, GetNum());
    return 0;
}

&gt; 출력
num: 0 
num: 1
num: 2</code></pre>
<p>이 파일을 총 세 개의 파일로 나눠서 저장한다고 가정해 보자.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/034000f4-da37-47a9-b66a-b73d016e23ad/image.png" alt=""></p>
<p>위 그림과 같이 단순하게 파일 세 개로 나누면 잘 작동할까?
안타깝게도 컴파일러는 다른 파일의 정보를 참조하여 알아서 컴파일을 진행하지 않는다.
그래서 func.c를 컴파일하면 변수 num이 선언되지 않았기 때문에 컴파일 에러가 발생한다.
main.c를 컴파일해도 파일 내에서 Increment 함수가 정의된 적이 없기 때문에 에러가 발생한다.
따라서, 각 변수 또는 함수가 외부에 선언 및 정의되었다고 컴파일러에게 알려줘야한다.</p>
<p>여기서 사용되는 것이 <strong><code>extern</code></strong> 키워드다.
<code>extern</code>키워드는 변수나 함수가 외부에 선언되었음을 컴파일러에게 알릴 때 사용된다.
함수가 외부에 정의되어 있음을 알릴 때에는 extern 선언을 생략할 수 있다.</p>
<pre><code class="language-c">extern int num;        // int형 변수 num이 외부에 선언되어 있다.
extern void Increment(void);    // void Increment(void) 함수가 외부에 정의되어 있다.
void Increment(void);    // extern 선언 생략 가능.</code></pre>
<p>따라서, 아래와 같이 정정되면 컴파일이 가능하며 extern 선언을 통해 함수 또는 변수가 외부에 선언 및 정의되어 있다는 것을 알리기만 하면되고 구체적으로 어느 파일에 선언 및 정의되어있는지 까지는 알리지 않아도 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/495c8b08-785a-4ce6-8ce7-69dac5639c07/image.png" alt=""></p>
<h4 id="static-전역변수의-활용"><em>static 전역변수의 활용</em></h4>
<p>이전에 <code>static 지역변수</code>에 대해 배웠다.
<code>static 전역변수</code>는 외부 파일에서의 접근을 허용하지 않을 때 사용한다. 즉, 변수의 접근범위를 파일 내부로 제한하는 것이다.</p>
<hr>
<h3 id="27-2-둘-이상의-파일을-컴파일하는-방법과-static에-대한-고찰">27-2 &quot;둘 이상의 파일을 컴파일하는 방법과 static에 대한 고찰&quot;</h3>
<p>둘 이상의 파일을 컴파일하는 방법은 Visual Studio를 사용했을 때 사용하면 유용한 점에 대해 알아볼 것이다.</p>
<p><strong>&lt;첫 번째 방법&gt;</strong>
이미 만들어진 파일을 프로젝트에 추가하는 방법.
&#39;소스파일 → 추가 → 기존 항목&#39;을 선택하여 위에서 만든 <code>num.c</code>, <code>func.c</code>, <code>main.c</code> 파일들을 추가한다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/dff5e7aa-416e-4f5e-a4a5-abedf0c5464a/image.png" alt=""></p>
<p><strong>&lt;두 번째 방법&gt;</strong>
이 방법은 소스 파일이 아닌 내가 새롭게 작성해서 사용할 때 사용하는 방법으로
&#39;소스파일 → 추가 → 새 항목&#39;을 선택하여 새로운 파일을 작성하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/c6c5e88f-7e60-4d26-9994-6cd946ffe741/image.png" alt=""></p>
<h4 id="함수의-static-선언"><em>함수의 static 선언</em></h4>
<p>전역 변수와 마찬가지로 함수에서도 static 선언을 할 수 있다.
변수와 마찬가지로 파일 내에서만 접근 가능하도록 함수를 제한하는 것이다.
이는 코드에 안정성을 부여할 수 있다.</p>
<hr>
<h3 id="27-3-헤더파일의-디자인과-활용">27-3 &quot;헤더파일의 디자인과 활용&quot;</h3>
<h4 id="include-지시자의-의미"><em>#include 지시자의 의미</em></h4>
<p>이번 예제는 <code>#include</code>지시자의 의미를 이해하기 위함이다.
아래 예제는 동일한 디렉터리에 존재해야 컴파일이 된다는 것에 유의하며 진행해보자.</p>
<pre><code class="language-c">// header1.h
{
    puts(&quot;Hello world!&quot;);</code></pre>
<pre><code class="language-c">// header2.h
    return 0;
}</code></pre>
<pre><code class="language-c">// main.c
#include &lt;stdio.h&gt;

int main(void)
#include &quot;header1.h&quot;
#include &quot;header2.h&quot;</code></pre>
<p>언뜻보면 각 파일이 다 완성되지 않은 것 처럼 보인다.
여기서 main.c 파일을 보면 <code>#include &quot;header1.h&quot;</code>문장은 이 문장의 위치에 header1.h에 저장된 내용을 가져다 놓으라는 메시지를 선행처리기에 전달하는 것이다.
따라서, 아래 그림과 같이 작동하게 된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/3fe598e7-ee62-4527-9512-905eb27ead8c/image.png" alt=""></p>
<p><code>#include</code>지시자는 파일의 내용을 단순히 포함시키는 용도로 사용된다.</p>
<h4 id="헤더파일을-include-하는-방법"><em>헤더파일을 include 하는 방법</em></h4>
<p>헤더파일을 include 하는 방법에는 두 가지가 있다.</p>
<p><strong>&lt;첫 번째 방법&gt;</strong>
<code>#include &lt;헤더파일 이름&gt;</code>로 stdio.h, stdlib.h, string.h와 같은 표준 헤더파일을 포함시킬 경우 사용된다.</p>
<p><strong>&lt;두 번째 방법&gt;</strong>
<code>#include &quot;헤더파일 이름&quot;</code>로 프로그래머가 정의하는 헤더파일을 포함시킬 때 사용하는 방식이다. 헤더파일 이름 부분에는 파일명 말고 경로를 넣어도 되는데 경로에는 두 가지 형식이 있다.
드라이브 명과 디렉터리 경로를 포함하는 <code>절대 경로(완전 경로)</code>, 상위 디렉터리를 표현해주는 <code>상대 경로</code>다.
절대 경로로 헤더파일을 지정하게될 경우 다른 컴퓨터에서 컴파일 하는 경우 절대 경로가 완전히 동일해야 컴파일이 되기 때문에 꽤나 번거로워진다. 또한 운영체제가 달라지면 디렉터리의 구조가 달라지기 때문에 경로지정에 대한 부분을 전면 수정해야하는 번거로움이 있다.</p>
<pre><code class="language-c">#include &quot;C:\CPoser\MyProject\header.h&quot;    // Windows 상에서의 절대 경로 지정</code></pre>
<p>반면에 상대 경로로 헤더파일을 지정하게 될 경우, 상위 디렉터리가 달라도 명시해준 디렉터리만 같으면 컴파일이 가능하기 때문에 시제로는 상대경로를 기반으로 헤더파일이 선언된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/17d6d548-07ef-492c-991f-f170ec405f0b/image.png" alt=""></p>
<p>그렇다면 헤더파일에는 무엇을 담으면 좋을까?
외부에 선언된 변수에 접근하거나 외부에 정의된 함수를 호출하기 위한 선언들을 필요할 때마다 매번 삽입시키는 번거로움을 덜기 위해 이런 선언들을 헤더파일에 모아두고 필요할 때마다 헤더파일을 포함시킨다.
예제를 통해 이를 더 자세히 알아보자.</p>
<pre><code class="language-c">// basicArith.h
#define PI 3.1415
double Add(double num1, double num2);
double Min(double num1, double num2);
double Mul(double num1, double num2);
double Div(double num1, double num2);

// basicArith.c
double Add(double num1, double num2)
{
    return num1 + num2;
}

double Min(double num1, double num2)
{
    return num1 - num2;
}   

double Mul(double num1, double num2)
{
    return num1 * num2;
}    

double Div(double num1, double num2)
{
    return num1 / num2;
}

// areaArith.h
double TriangleArea(double base, double height);
double CircleArea(double rad);

// areaArith.c
#include &quot;basicArith.h&quot;

double TriangleArea(double base, double height)
{
    return Div(Mul(base, height), 2);
}

double CircleArea(double rad)
{
    return Mul(Mul(rad, rad), PI);
}

// roundArith.h
double RectangleRound(double base, double height);
double SquareRound(double side);

// roundArith.c
#include &quot;basicArith.h&quot;

double RectangleRound(double base, double height)
{
    return Mul(Add(base, height), 2);
}

double SquareRound(double side)
{
    return Mul(side, 4);
}

// main2.c
#include &lt;stdio.h&gt;
#include &quot;areaArith.h&quot;
#include &quot;roundArith.h&quot;

int main()
{
    printf(&quot;삼각형 넓이(밑변 4, 높이 2): %g \n&quot;, TriangleArea(4, 2));
    printf(&quot;원 넓이(반지름 3): %g \n&quot;, CircleArea(3));

    printf(&quot;직사각형 둘레(밑변 2.5, 높이 5.2): %g \n&quot;, RectangleRound(2.5, 5.2));
    printf(&quot;정사각형 둘레(변 3): %g \n&quot;, SquareRound(3));
    return 0;
}

&gt; 출력
gcc .\main2.c
C:\Users\com\AppData\Local\Temp\cc0YWRqr.o:main2.c:(.text+0x22): undefined reference to `TriangleArea&#39;
C:\Users\com\AppData\Local\Temp\cc0YWRqr.o:main2.c:(.text+0x68): undefined reference to `RectangleRound&#39;
C:\Users\com\AppData\Local\Temp\cc0YWRqr.o:main2.c:(.text+0x86): undefined reference to `SquareRound&#39;</code></pre>
<p>VSC로 gcc해서 컴파일하는데 다른 파일에 있는 함수를 불러오지 못해서 오류가 발생한다...</p>
<p>해결 방안은 조만간 알아낼 것이다!!!</p>
<ul>
<li>헤더파일과 소스파일의 포함 관계
<img src="https://velog.velcdn.com/images/mingming_eee/post/2804c969-65c8-4d55-9743-9d71aa65e158/image.png" alt=""></li>
</ul>
<h3 id="해결⭐">해결⭐</h3>
<ul>
<li><p>오류 설명</p>
<ol>
<li>undefined reference to &#39;WinMain@16&#39;:
이 오류는 Windows 환경에서 GUI 애플리케이션을 찾고 있는 경우 발생한다. main 함수가 제대로 정의되어 있어도, gcc가 -mwindows 옵션 없이 실행되면 이 오류가 발생할 수 있다.</li>
</ol>
<ul>
<li>해결 방법: -mconsole 옵션을 추가하여 콘솔 애플리케이션으로 컴파일한다.</li>
</ul>
<ol start="2">
<li>undefined reference to &#39;IntDiv&#39;:
이 오류는 IntDiv 함수가 정의된 intdiv.c 파일과 함께 컴파일하지 않았기 때문에 발생한다.</li>
</ol>
<ul>
<li>해결 방법: 두 소스 파일을 함께 컴파일하여 링킹해 줘야 한다.</li>
</ul>
</li>
<li><p>해결</p>
<pre><code class="language-shell">gcc basicArith.c areaArith.c roundArith.c main2.c -o 원하는 파일명</code></pre>
<p>로 파일을 한번에 컴파일 하여 하나의 &quot;원하는_파일명&quot; 프로그램 실행 파일로 만들 수 있다.</p>
<ul>
<li>참고자료 (<a href="https://sean-ma.tistory.com/10">VS code C 빌드 방법</a>)</li>
</ul>
</li>
<li><p>다른 해결 방법
파일 수가 많거나 일일이 다 치기 귀찮을 땐 다른 방법도 하나 있다.
<code>Makerfile</code>하나를 만들어서 한번에 컴파일 하는 것이다.</p>
<ul>
<li>참고자료 (<a href="https://docs.redhat.com/ko/documentation/red_hat_enterprise_linux/7/html/developer_guide/managing-more-code-make#managing-more-code-make_make-makefile-overview">Make를 사용하여 더 많으 코드 관리</a>)
(사실 이 방법에 대해서는 좀 더 알아봐야겠다 ㅎㅎ)</li>
</ul>
</li>
<li><p>출력</p>
<pre><code>Triangle Area(base 4, height 2): 4 
Circle Area(rad 3): 28.2735 
Rectangle Round(base 2.5, height 5.2): 15.4
Square Round(side 3): 12
// 한글로 적다보니 컴파일이 잘 안되서 영어로 바꿔서 출력했다.</code></pre></li>
</ul>
<h4 id="구조체-정의"><em>구조체 정의</em></h4>
<p>구조체의 선언(typedef 선언)및 정의는 어디에 두는 것이 효율적일까?
소스파일 또는 헤더파일?
다음 예제를 통해 알아보자.</p>
<pre><code class="language-c">// intdiv.c
typedef struct div
{
    int quotient;   // 몫
    int remainder;  // 나머지
} Div;

Div IntDiv(int num1, int num2)
{
    Div dval;
    dval.quotient = num1/num2;
    dval.remainder = num1%num2;
    return dval;
}

// main3.c
#include &lt;stdio.h&gt;

typedef struct div
{
    int quotient;   // 몫
    int remainder;  // 나머지
} Div;

extern Div IntDiv(int num1, int num2);

int main()
{
    Div val = IntDiv(5, 2);
    printf(&quot;quotient: %d \n&quot;, val.quotient);
    printf(&quot;remainder: %d \n&quot;, val.remainder);
    return 0;
}

&gt; 명령어 &amp; 출력
&gt; gcc .\intdiv.c .\main3.c -o example2
&gt; .\example2.exe
quotient: 2 
remainder: 1</code></pre>
<p>구조체는 다른 변수나 함수와 다르게 각 파일에서 선언 및 정의가 Div 구조체를 필요로 하는 모든 파일에 존재한다.
헤더파일을 만들어 이를 개선해보자.</p>
<pre><code class="language-c">//stdiv.h
typedef struct div
{
    int quotient;
    int remainder;
} Div;

// intdiv2.c
#include &quot;stdiv.h&quot;

Div IntDiv(int num1, int num2)
{
    Div dval;
    dval.quotient = num1 / num2;
    dval.remainder = num1 % num2;
    return dval;
}

// main4.c
#include &lt;stdio.h&gt;
#include &quot;stdiv.h&quot;

extern Div IntDiv(int num1, int num2);

int main()
{
    Div val = IntDiv(5, 2);
    printf(&quot;quotient: %d \n&quot;, val.quotient);
    printf(&quot;remainder: %d \n&quot;, val.remainder);
    return 0;
}

&gt; 명령어 &amp; 출력
&gt; gcc .\intdiv2.c .\main4.c -o example3
&gt; .\example3.exe
&gt; quotient: 2 
remainder: 1 </code></pre>
<p>구조체의 선언 및 정의는 헤더파일에 삽입하는 것이 좋다.</p>
<h4 id="헤더파일의-중복삽입-문제"><em>헤더파일의 중복삽입 문제</em></h4>
<pre><code class="language-c">// stdiv.h
typedef struct div
{
    int quotient;
    int remainder;
} Div;

// intdiv3.c
#include &quot;stdiv.h&quot;

Div IntDiv(int num1, int num2)
{
    Div dval;
    dval.quotient = num1 / num2;
    dval.remainder = num1 % num2;
    return dval;
}

// intdiv3.h
#include &quot;stdiv.h&quot;
Div IntDiv(int num1, int num2);

// main5.c
#include &lt;stdio.h&gt;
#include &quot;stdiv.h&quot;
#include &quot;intdiv3.h&quot;

int main()
{
    Div val = IntDiv(5, 2);
    printf(&quot;quotient: %d \n&quot;, val.quotient);
    printf(&quot;remainder: %d \n&quot;, val.remainder);
    return 0;
}

&gt; 명령어 &amp; 출력
&gt; gcc .\intdiv3.c .\main5.c -o example4
In file included from .\intdiv3.h:1:0,
                 from .\main5.c:3:
.\stdiv.h:1:16: error: redefinition of &#39;struct div&#39;
 typedef struct div
                ^~~
In file included from .\main5.c:2:0:
.\stdiv.h:1:16: note: originally defined here
 typedef struct div
                ^~~
In file included from .\intdiv3.h:1:0,
                 from .\main5.c:3:
.\stdiv.h:5:3: error: conflicting types for &#39;Div&#39;
 } Div;
   ^~~
In file included from .\main5.c:2:0:
.\stdiv.h:5:3: note: previous declaration of &#39;Div&#39; was here
 } Div;
   ^~~</code></pre>
<p>이 파일들의 헤더파일 포함관계를 살펴보면 아래 그림과 같다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/71fdd808-5262-417b-b602-84ae4234eb8c/image.png" alt=""></p>
<p>헤더파일 intdiv3.h가 stidiv.h를 포함하고 이씩 때문에 main.c에서 이를 한번 더 포함한다. 구조체 Div가 두 번 정의된 형태가 되어 컴파일 에러가 발생한다.
다음과 같은 유형의 선언은 여러번 삽입되어도 컴파일 오류가 발생하지 않는다.</p>
<pre><code class="language-c">extern int num;
void Increment(void);</code></pre>
<h4 id="조건부-컴파일을-활용한-중복삽입-문제의-해결"><em>조건부 컴파일을 활용한 중복삽입 문제의 해결</em></h4>
<p>헤더파일의 중복삽입에 대한 해결책은 Chapter 26-3에서 학습한 &#39;조건부 컴파일을 위한 매크로&#39;에서 배웠었다.
다음 예제는 총 네 개의 파일로 이뤄진 구조에서 헤더파일의 중복삽입에 대한 해결책을 알 수 있다.</p>
<pre><code class="language-c">// stdiv2.h
#ifndef __STDIV2_H__
#define __STDIV2_H__

typedef struct div
{
    int quotient;
    int remainder;
} Div;

#endif

// intdiv4.h
#ifndef __INTDIV4_H__
#define __INTDIV4_H__

#include &quot;stdiv2.h&quot;
Div IntDiv(int num1, int num2);

#endif

// intdiv4.c
#include &quot;stdiv2.h&quot;

Div IntDiv(int num1, int num2)
{
    Div dval;
    dval.quotient = num1 / num2;
    dval.remainder = num1 % num2;
    return dval;
}

// main6.c
#include &lt;stdio.h&gt;
#include &quot;stdiv2.h&quot;
#include &quot;intdiv4.h&quot;

int main()
{
    Div val = IntDiv(10, 3);
    printf(&quot;quotient: %d \n&quot;, val.quotient);
    printf(&quot;remainder: %d \n&quot;, val.remainder);
    return 0;
}</code></pre>
<p><code>#ifndef~#endif</code>에 의해서 중복삽입으로 인한 문제가 발생하지 않도록 한다.</p>
<hr>
<h2 id="review"><code>&lt;Review&gt;</code></h2>
<p>드디어 C언어를 끝까지 다 했다!
와 이 책을 다 하게 될 줄 몰랐는데
사실 공부하면서 굳이 C언어를 왜 하냐라는 질문을 많이 받았었는데
나도 친구가 추천해줘서 시작을 했었다.
하지만 지금와서 이 이유에 대해 생각해봤을 때 컴퓨터의 흐름을 이해하면서 코딩을 배우기 가장 적합하고,
물론 나도 완전히 이해한 것은 아니지만 이 책 다음으로 열혈 자료구조를 통해서 조금 더 이해해보려고 한다.
나중에는 Java까지 배워서 3대 언어를 다 배워보고 싶다!</p>
<p>마지막 도전!프로그래밍을 통해서 정리하고 마무리해보려 한다.⭐</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/15c634c9-fdeb-480e-a3bc-61ee59e4af72/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C언어] 매크로와 선행처리기(Preprocessor)]]></title>
            <link>https://velog.io/@mingming_eee/c-4-26</link>
            <guid>https://velog.io/@mingming_eee/c-4-26</guid>
            <pubDate>Wed, 11 Sep 2024 17:14:07 GMT</pubDate>
            <description><![CDATA[<h2 id="chapter-26-매크로와-선행처리기preprocessor">Chapter 26. 매크로와 선행처리기(Preprocessor)</h2>
<p>이번 chapter에서는 C언어의 문법과 직접적인 연관은 없지만 실행파일의 생성과 관련해서 중요한 컴파일 과정의 일부로 포함되어 있는 &#39;선행처리&#39;에 대해 배울 예정이다.</p>
<h3 id="26-1-선행처리기와-매크로">26-1 &quot;선행처리기와 매크로&quot;</h3>
<p>Chapter1에서는 실행파일이 컴파일과 링크의 과정을 거쳐서 만들어지는 것이라고 했다.
(기억을 되짚어보자...)
그러나 실제로는 컴파일 이전에 &#39;선행처리&#39;라는 과정을 거치게 된다.
다만 이를 컴파일 과정에 포함시켜서 이야기 하는 것이 보통이기 때문에 별도의 구분을 하지 않았었다.
이번 Chapter에서는 이 &#39;선행처리&#39;가 주를 이루기 때문에 별도로 구분해서 언급했다.</p>
<h4 id="선행처리란"><em>선행처리란?</em></h4>
<p><code>선행처리</code>란 컴파일 이전의 처리를 의미한다.
아래 그림과 같이 선행처리는 선행처리기에 의해, 컴파일은 컴파일러에 의해, 그리고 링크는 링커에 의해 진행된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/8c2e58dd-3fed-4ac9-8665-a6f91890d11b/image.png" alt=""></p>
<p>컴파일 과정을 거치게 되면 바이너리 데이터로 이루어진 오브젝트 파일이 생성된다.
컴파일 이전에 진행되는 선행처리의 과정을 거치게 되면 어떠한 데이터로 채워진 파일이 생성될까?
선행처리의 과정을 거쳐서 생성되는 파일도 근야 소스파일일 뿐이다. 소스파일의 형태가 그대로 유지되기 때문이다.
선행처리기가 하는 일은 단순하다.
사용자가 삽입해 놓은 선행처리 명령문대로 소스코드의 일부를 수정할 뿐인데, 여기서 말하는 수정이란? 단순 치환(substitution)의 형태를 띠는 경우가 대부분이다.
다음 예시로 간단한 선행처리 명령문을 보자.</p>
<pre><code class="language-c">#define PI 3.14</code></pre>
<p>우리가 계속 사용했던 <code>#include</code>처럼 선행처리 명령문은 <code>#</code>문자로 시작하며 컴파일러가 아닌 선행처리기에 의해 처리되기 때문에 세미콜론을 끝에 붙이지 않아도 된다.
위 문장이 선행처리 과정을 지나면 이후 코드에서 PI를 만날 때 무조건 3.14로 변환된다.</p>
<hr>
<h3 id="26-2-대표적인-선행처리-명령문">26-2 &quot;대표적인 선행처리 명령문&quot;</h3>
<h4 id="define-object-like-macro"><em>define: Object-like macro</em></h4>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/8ad8ac26-6860-4630-b2ab-985cfaf40ffd/image.png" alt=""></p>
<p>위 그림처럼 선행처리 명령문은 기본적으로 세 부분으로 나뉜다.
<code>지시자</code>인 <code>#define</code>파트는 선행처리기가 이 부분을 보고 프로그래머가 지시하는 바를 파악한다.
이는 &quot;이어서 등장하는 매크로를 마지막에 등장하는 매크로 몸체로 치환하라!&quot;라는 내용을 지시한다.</p>
<p><code>#define</code>뒤에 등장하는 것을 가리켜 <code>매크로</code>라고 한다.
그리고 그 뒤에 등장하는 것을 <code>매크로 몸체(또는 대체 리스트)</code>라 한다.
따라서 위의 선행처리 명령문은 &quot;매크로 PI를 매크로 몸체 3.1415로 전부 치환하라!&quot;라는 내용을 선행처리기에게 지시한다.</p>
<p>PI와 같은 매크로를 가리켜 <code>오브젝트와 유사한 매크로(object-like macro)</code> 또는 <code>매크로 상수</code>라 한다.</p>
<p>다음 예제를 통해 매크로 상수가 적용된 예와 그 결과를 확인해보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

#define NAME        &quot;홍길동&quot;
#define AGE         24
#define PRINT_ADDR  puts(&quot;주소: 경기도 용인시\n&quot;);

int main()
{
    printf(&quot;이름: %s\n&quot;, NAME);
    printf(&quot;나이: %d\n&quot;, AGE);
    PRINT_ADDR;
    return 0;
}

&gt; 출력
이름: 홍길동
나이: 24
주소: 경기도 용인시</code></pre>
<p>참고로 매크로의 이름은 대문자로 정의하는 것이 일반적이다.
대문자로 정의함으로써 이 식별자가 매크로라는 사실을 부각시킬 수 있다.</p>
<h4 id="define-function-like-macro"><em>#define: Function-like macro</em></h4>
<p>매크로는 매개변수가 존재하는 형태로도 정의할 수 있다.
이렇게 매개변수가 존재하는 매크로는 그 동작방식이 마치 함수와 유사해서 <code>함수와 유사한 매크로(function-like macro)</code>라 하는데 줄여서 <code>매크로 함수</code>라고도 한다.
다음은 매크로 함수의 예다.</p>
<pre><code class="language-c">#define SQUARE(X) X*X</code></pre>
<p>이 명령문은 아래 그림과 같이 해석된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/9659088a-b7bf-47f5-9ddb-7e0abf9da52c/image.png" alt=""></p>
<p>괄호 안 X는 정해지지 않은 임의의 값(또는 문장)을 의미한다.
이렇게 정의한 매크로를 접한 선행처리기는 SQUARE(X)와 동일한 패턴을 만나면 무조건 <code>X*X</code>로 치환해버린다.
이렇게 선행처리기에 의해서 변환되는 과정 자체를 <code>매크로 확장(macro expansion)</code>이라 한다.
다음 예제를 통해 매크로 확장의 결과를 보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#define SQUARE(X) X*X

int main()
{
    int num = 20;

    /* 정상적 결과 출력 */
    printf(&quot;Square of num: %d \n&quot;, SQUARE(num));
    printf(&quot;Square of -5: %d \n&quot;, SQUARE(-5));
    printf(&quot;Square of 2.5: %d \n&quot;, SQUARE(2.5));

    /* 비정상적 결과 출력 */
    printf(&quot;Square of 3+2: %d \n&quot;, SQUARE(3+2));
    return 0;
}

&gt; 출력
Square of num: 400 
Square of -5: 25
Square of 2.5: 0
Square of 3+2: 11</code></pre>
<h4 id="잘못된-매크로-정의"><em>잘못된 매크로 정의</em></h4>
<p>위 예제에서 정의된 매크로에 어떤 문제가 있는지 보자.</p>
<pre><code class="language-c">SQUARE(3+2)</code></pre>
<p>이를 함수의 관점에서 본다면 3과 2의 합인 5를 SQUARE함수의 인자로 전달하는 것으로 생각하는 것이 당연하다.
하지만 그렇게 동작하지 않았다.
먼저 연산을 하고, 그 연산결과를 가지고 함수를 호출하게끔 돕는 것은 컴파일러이고,
매크로는 선행처리기에 의해 처리되기 때문이다.
따라서 저 명령어는 <code>3+2*3+2</code>로 처리되어 11이 출력되는 것이다. </p>
<p>해결 방법은 연산을 괄호로 싸서 매개변수로 넣으면 된다.
또는 매크로 함수를 정의할 때 <code>(X)*(X)</code>라고 하면 된다.</p>
<p>하지만, 이렇게 되면 아래 상황일 때는 어떻게 될까?</p>
<pre><code class="language-c">int num = 120 / SQUARE(2);</code></pre>
<p><code>120/4</code>이기 때문에 30을 기대하지만 실제 값은 120으로 초기화 된다.
그 이유는 <code>120 / (2) * (2)</code>으로 치환되기 때문이다.</p>
<p>그렇다면 매크로 함수를 <code>((X)*(X))</code>로 정의하면 해결된다.</p>
<p>따라서 매크로 함수를 정의할 때에는 매크로의 몸체부분을 구성하는 X와 같은 전달인자 하나하나에 괄호를 해야 함은 물론이고, 반드시 전체를 괄호로 한번 더 묶어줘야 한다는 사실을 기억하자~!</p>
<h4 id="매크로를-두-줄에-걸쳐서-정의하는-방법"><em>매크로를 두 줄에 걸쳐서 정의하는 방법</em></h4>
<p>정의하는 매크로의 길이가 길어지는 경우에는 가독성을 위해서 두 줄에 걸쳐서 매크로를 정의하기도 한다.
매크로를 두 줄 이상에 걸쳐서 정의할 때에는 <code>\</code>문자를 활용해서 줄이 바뀌었음을 명시해준다.</p>
<pre><code class="language-c">#define SQUARE(X)    \
        ((X)*(X))</code></pre>
<h5 id="매크로-정의시-먼저-정의된-매크로도-사용-가능"><em>매크로 정의시, 먼저 정의된 매크로도 사용 가능</em></h5>
<p>먼저 정의된 매크로는 뒤에서 매크로를 정의할 때 사용할 수 있다. 예제를 통해 확인해보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#define PI 3.14
#define PRODUCT(X,Y) ((X)*(Y))
#define CIRCLE_AREA(R) (PRODUCT((R), (R))*PI)

int main()
{
    double rad = 2.1;
    printf(&quot;Radius: %f\narea: %g \n&quot;, rad, CIRCLE_AREA(rad));
    return 0;
}

&gt; 출력
Radius: 2.100000
area: 13.8474</code></pre>
<h4 id="매크로-함수의-장단점"><em>매크로 함수의 장단점</em></h4>
<p>매크로 함수를 정의하는 것은 일반 함수를 정의하는 것보다 복잡하다.
정의하고자 하는 함수의 크기가 크면, 매크로로 정의하는 것 자체가 불가능할 수도 있다.
그럼에도 불구하고 매크로 함수를 정의하는 이유가 뭘까?
매크로 함수의 장단점을 알아보자.</p>
<p>매크로 함수의 장점은 아래와 같다.</p>
<ul>
<li>매크로 함수는 일반 함수에 비해 실행속도가 빠르다.</li>
<li>자료형에 따라서 별도로 함수를 정의하지 않아도 된다.</li>
</ul>
<p>매크로 함수의 실행속도가 빠른 이유는 일반적인 함수가 호출되면 </p>
<p>1) 호출된 함수를 위한 스택 메모리 할당 
2) 실행위치의 이동과 매개변수로의 인자 전달 
3) return 문에 의한 값의 반환
이렇게 3가지 이유 때문에 함수의 빈번한 호출은 실행속도 저하로 이어지게 된다.</p>
<p>반면에, 매크로 함수는 선행처리기에 의해서 매크로 함수의 몸체부분이 매크로 함수의 호출 문장을 대신하기 때문에 위에 얘기한 사항들이 동반하지 않는다.</p>
<p>매크로 함수의 단점은 다음과 같다.</p>
<ul>
<li>정의하기가 정말로 까다롭다.</li>
<li>디버깅하기 쉽지 않다.</li>
</ul>
<p>만약에 두 값의 차를 계산하는 함수가 있는데 반환하는 값이 절댓값 형태여야 한다. 그럼 아래의 형태처럼 함수를 만들 수 있는데</p>
<pre><code class="language-c">int DrffABS(int a, int b)
{
    if(a&gt;b)
        return a-b;
    else
        return b-a;
}</code></pre>
<p>이것을 매크로 함수로 구현하게 된다면 정의하는 과정이 살짝 부담스럽다. (<code>#define DIFF_ABS(X, Y) ((X)&gt;(Y) ? (X)-(Y) : (Y)-(X))</code>으로 하면 되지만,,, 어쨋든 부담스럽다!)
두 번째 단점은 아래 예시를 통해 알아보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#define DIFF_ABS(X, Y) ((x)&gt;(y) ? (x)-(y) : (y)-(x))

....</code></pre>
<p>이렇게 하고 다음 코드에서 DIFF_ABS 매크로 함수를 사용하게 되면 선언된 적없는 x와 y를 사용한다고 에러 메시지가 출력된다.
대문자와 소문자를 구분하기 때문에 이처럼 매크로를 잘못 정의한 경우, 에러 메시지는 선행처리 이전의 소스파일을 기준으로 출력되지 않고 선행처리 이후의 소스파일 기준으로 출력이 된다.
이런 점은 일반적인 에러 메시지보다 이해하기 힘들다는 단점이 있다.</p>
<h4 id="매크로-함수-정의"><em>매크로 함수 정의</em></h4>
<p>따라서 다음 특성을 지니는 함수들은 매크로 형태로 정의하는 것이 옳다.</p>
<ul>
<li>작은 크기의 함수</li>
<li>호출의 빈도수가 높은 함수</li>
</ul>
<p>함수의 크기가 작아야 매크로의 형태로 정의하기 편하고 에러의 발생 확률도 낮아서 디버깅에 대한 염려를 덜 수 있다.
호출의 빈도수가 높아야 매크로 함수가 가져다 주는 성능 향상의 이점도 최대한 누릴 수 있다.</p>
<hr>
<h3 id="26-3-조건부-컴파일conditional-compilation을-위한-매크로">26-3 &quot;조건부 컴파일(Conditional Compilation)을 위한 매크로&quot;</h3>
<p>매크로 지시자 중에는 특정 조건에 따라 소스코드의 일부를 삽입하거나 삭제할 수 있도록 디자인 된 지시자가 있다.</p>
<h4 id="ifendif-참이라면"><em>#if...#endif: 참이라면</em></h4>
<p>if문이 조건부 실행을 위한 것이라면 <code>#if...#endif</code>는 조건부 코드 삽입을 위한 지시자이다.
이 지시자의 처리 방식은 다음 예제를 통해 확인해보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#define ADD 1
#define MIN 2

int main()
{
    int num1, num2;
    printf(&quot;두 개의 정수 입력: &quot;);
    scanf(&quot;%d %d&quot;, &amp;num1, &amp;num2);

#if ADD     // ADD가 참이라면
    printf(&quot;%d + %d = %d \n&quot;, num1, num2, num1 + num2);
#endif

#if MIN     // MIN이 참이라면
    printf(&quot;%d - %d = %d \n&quot;, num1, num2, num1-num2);
#endif

    return 0;
}

&gt; 출력
두 개의 정수 입력: 5 4
5 + 4 = 9
5 - 4 = 1</code></pre>
<p>2행과 3행에 정의되어 있는 매크로 ADD와 MIN이 각각 1과 0인 관계로 <code>#endif</code>에 해당되는 부분은 삭제되었다. 
<code>#if</code>문의 구성에는 연산자도 활용할 수 있다.</p>
<h4 id="ifdefendif-정의되었다면"><em>#ifdef...#endif: 정의되었다면</em></h4>
<p><code>#ifdef</code>는 매크로가 정의되었느냐, 정의되지 않았느냐를 기준으로 동작한다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
// #define ADD 1
#define MIN 2

int main()
{
    int num1, num2;
    printf(&quot;두 개의 정수 입력: &quot;);
    scanf(&quot;%d %d&quot;, &amp;num1, &amp;num2);

#ifdef ADD     // ADD가 참이라면
    printf(&quot;%d + %d = %d \n&quot;, num1, num2, num1 + num2);
#endif

#ifdef MIN     // MIN이 참이라면
    printf(&quot;%d - %d = %d \n&quot;, num1, num2, num1-num2);
#endif

    return 0;
}

&gt; 출력
두 개의 정수 입력: 7 2
7 - 2 = 5</code></pre>
<h4 id="ifndefendif-정의되지-않았다면"><em>#ifndef...#endif: 정의되지 않았다면</em></h4>
<p>이 지시자는 <code>#ifdef...#endif</code>의 반대이다.
위 예제를 반대로 바꿔서 진행해보면 알 수 있다.</p>
<h4 id="else의-삽입"><em>#else의 삽입</em></h4>
<p>if문에서 else를 추가할 수 있듯이 위에서 제시한 <code>#if</code>, <code>#ifdef</code>, <code>#ifndef</code>문 모두 <code>#else</code>문을 추가할 수 있다.
다음 예제를 통해서 삽입의 방식과 의미를 알아보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#define HIT_NUM 5

int main()
{
#if HIT_NUM ==5
    puts(&quot;매크로 상수 HIT_NUM은 현재 5입니다.&quot;);
#else
    puts(&quot;매크로 상수 HIT_NUM은 현재 5가 아닙니다.&quot;);
#endif
    return 0;
}

&gt; 출력
매크로 상수 HIT_NUM은 현재 5입니다.</code></pre>
<h4 id="elif의-삽입"><em>elif의 삽입</em></h4>
<p>if문에서 else if를 여러 번 추가할 수 있듯,
<code>#if</code>문에서만 <code>#elif</code>를 여러 번 추가할 수 있다.
그리고 이 형식의 끝을 <code>#else</code>로 마무리 할 수 있다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#define HIT_NUM 7

int main()
{
#if HIT_NUM==5
    puts(&quot;매크로 상수 HIT_NUM은 현재 5입니다.&quot;);
#elif HIT_NUM==6
    puts(&quot;매크로 상수 HIT_NUM은 현재 6입니다.&quot;);
#elif HIT_NUM==7
    puts(&quot;매크로 상수 HIT_NUM은 현재 7입니다.&quot;);
#else
    puts(&quot;매크로 상수 HIT_NUM이 5, 6, 7은 확실히 아닙니다.&quot;);
#endif
    return 0;
}

&gt; 출력
매크로 상수 HIT_NUM은 현재 7입니다.</code></pre>
<hr>
<h3 id="26-4-매개변수의-결합과-문자열화">26-4 &quot;매개변수의 결합과 문자열화&quot;</h3>
<p>이번에는 두 개의 매크로 연산자를 배울 것이다.
이 연산자를 배울 땐 각자 조건이 있다.</p>
<h4 id="조건-1-문자열-내에서는-매크로의-매개변수-치환이-발생하지-않습니다"><em>조건 1) 문자열 내에서는 매크로의 매개변수 치환이 발생하지 않습니다.</em></h4>
<p>문자열의 구성을 위한 매크로 함수를 다음의 형태로 정의하였다.</p>
<pre><code class="language-c">#define STRING_JOB(A, B) &quot;A의 직업은 B이다.&quot;</code></pre>
<p>그리고는 <code>STRING_JOB(이동춘, 나무꾼)</code>이라는 매크로 문장이 &quot;이동춘의 직업은 나무꾼이다.&quot;와 같은 문자열을 만들어 낼 것을 기대한다.
하지만 문자열 안에서는 매크로의 매개변수 치환이 발생하지 않기 때문에 위 문자열을 만들어내지 못한다.</p>
<p>이를 위해 필요한 것이 <code>#</code>연산자이다.</p>
<h4 id="-연산자"><em># 연산자</em></h4>
<p><code>#</code>연산자는 문자열 내에서 매크로의 매개변수 치환이 가능하도록 한다.</p>
<pre><code class="language-c">#define STR(ABC) #ABC</code></pre>
<p>위 문장은 매개변수 ABC에 전달되는 인자를 문자열 &quot;ABC&quot;로 치환하라는 의미를 담고 있다.
<code>#</code>연산자는 치환의 결과를 문자열로 구성하는 연산자이다.
문자열을 나란히 선언하면 하나의 문자열로 간주된다.
따라서 다음과 같이 문자열을 선언하는 것도 가능하다.</p>
<pre><code class="language-c">char * str = &quot;ABC&quot; &quot;DEF&quot;;
char * str = &quot;ABCDEF&quot;;</code></pre>
<p>위와 아래의 문자열 선언은 동일하다.
이 내용을 바탕으로 예제를 보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#define STRING_JOB(A, B) #A &quot;의 직업은 &quot; #B &quot;입니다.&quot;

int main()
{
    printf(&quot;%s \n&quot;, STRING_JOB(이동춘, 나무꾼));
    printf(&quot;%s \n&quot;, STRING_JOB(한상순, 사냥꾼));
    return 0;
}

&gt; 출력
이동춘의 직업은 나무꾼입니다. 
한상순의 직업은 사냥꾼입니다.</code></pre>
<h4 id="조건-2-특별한-매크로-연산자-없이-단순히-연결하는-것은-불가능하다"><em>조건 2) 특별한 매크로 연산자 없이 단순히 연결하는 것은 불가능하다.</em></h4>
<p>대학교의 학번은 아래 그림과 같이 조합되어 발급된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/de5d67dc-0506-4846-9c38-e2131c06da9b/image.png" alt=""></p>
<p>우리는 학번을 조합하는 매크로 함수를 정의하고자 한다.
이 함수는 <code>STNUM(10, 65, 175);</code>와 같은 형태로 호출되고 이 문장은 선행처리기에 의해서 <code>1065175</code>라고 치환되어야 한다.
구현할 수 있는 경우의 수를 아래 예제를 통해 살펴보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
//#define STNUM(Y, S, P) YSP
//#define STNUM(Y, S, P) Y S P
#define STNUM(Y, S, P) ((Y)*100000+(S)*1000+(P))

int main()
{
    printf(&quot;학번: %d \n&quot;, STNUM(10, 65, 175)); 
    printf(&quot;학번: %d \n&quot;, STNUM(10, 65, 075));
    return 0;
}

&gt; 출력
[2번째 줄 사용할 때]
    &gt; gcc .\UnivStdNum.c
.\UnivStdNum.c: In function &#39;main&#39;:
.\UnivStdNum.c:2:24: error: &#39;YSP&#39; undeclared (first use in this function)
 #define STNUM(Y, S, P) YSP
                        ^
.\UnivStdNum.c:8:29: note: in expansion of macro &#39;STNUM&#39;
     printf(&quot;í•™ë²ˆ: %d \n&quot;, STNUM(10, 65, 175));
                             ^~~~~
.\UnivStdNum.c:2:24: note: each undeclared identifier is reported only once for each function it appears in 
 #define STNUM(Y, S, P) YSP
                        ^
.\UnivStdNum.c:8:29: note: in expansion of macro &#39;STNUM&#39;
     printf(&quot;í•™ë²ˆ: %d \n&quot;, STNUM(10, 65, 175));
                             ^~~~~

[3번째 줄 사용할 때]
    &gt; gcc .\UnivStdNum.c
.\UnivStdNum.c: In function &#39;main&#39;:
.\UnivStdNum.c:8:39: error: expected &#39;)&#39; before numeric constant
     printf(&quot;í•™ë²ˆ: %d \n&quot;, STNUM(10, 65, 175));
                                       ^
.\UnivStdNum.c:3:26: note: in definition of macro &#39;STNUM&#39;
 #define STNUM(Y, S, P) Y S P
                          ^
.\UnivStdNum.c:9:39: error: expected &#39;)&#39; before numeric constant
     printf(&quot;í•™ë²ˆ: %d \n&quot;, STNUM(10, 65, 075));
                                       ^
.\UnivStdNum.c:3:26: note: in definition of macro &#39;STNUM&#39;
 #define STNUM(Y, S, P) Y S P
                          ^

[4번째 줄 사용할 때]
학번: 1065175 
학번: 1065061</code></pre>
<p>2번째 줄을 사용할 경우 <code>printf(&quot;학번: %d \n&quot;, YSP);</code>가 되어 에러가 발생하고
3번째 줄을 사용할 경우 <code>printf(&quot;학번: %d \n&quot;, 10 65 175);</code>로 치환되어 에러가 발생한다.
4번재 줄을 사용할 경우 에러가 발생하진 않지만 마지막 매개변수가 0으로 시작하게 되면 8진수로 이해하게 되어 원하는 형태의 답을 얻진 못한다.</p>
<h4 id="-연산자-1"><em>## 연산자</em></h4>
<p><code>##</code>연산자는 필요한 형태대로 단순하게 결합할 수 있도록 도와준다. 즉, 매크로 함수의 전달인자를 다른 대상(전달인자, 숫자, 문자, 문자열 등)과 이어줄 때 사용한다.</p>
<pre><code class="language-c">#define CON(UPP, LOW) UPP ## 00 ## LOW</code></pre>
<p>위 매크로 몸체에는 UPP와 00과 LOW가 순서대로 이어질 수 있도록 <code>##</code>연산자가 사용되었다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
//#define STNUM(Y, S, P) YSP
//#define STNUM(Y, S, P) Y S P
//#define STNUM(Y, S, P) ((Y)*100000+(S)*1000+(P))
#define STNUM(Y, S, P) Y ## S ## P

int main()
{
    printf(&quot;학번: %d \n&quot;, STNUM(10, 65, 175)); 
    printf(&quot;학번: %d \n&quot;, STNUM(10, 65, 075));
    return 0;
}

&gt; 출력
학번: 1065175 
학번: 1065075</code></pre>
<p>따라서 우리가 원하는 형태의 학번을 얻을 수 있다...!</p>
<hr>
<h2 id="review"><code>&lt;Review&gt;</code></h2>
<p>후.... 새벽 2시가 넘어서야 스터디할 분량을 다 마무리 했다!
매크로 함수 아주 매력적인 친구다.
그리고 그 친구와 함께 매크로 함수를 더욱 풍부하게 사용할 수 있게 해주는 연산자 <code>#</code>와 <code>##</code>도 기억해야겠다.</p>
<p>이만 Bonne Nuit~🌜</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/b3fe8b39-3319-43ac-9641-c0879f0dcd0b/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C언어] 메모리 관리와 메모리의 동적 할당]]></title>
            <link>https://velog.io/@mingming_eee/c-4-25</link>
            <guid>https://velog.io/@mingming_eee/c-4-25</guid>
            <pubDate>Wed, 11 Sep 2024 15:33:02 GMT</pubDate>
            <description><![CDATA[<h2 id="chapter-25-메모리-관리와-메모리의-동적-할당">Chapter 25. 메모리 관리와 메모리의 동적 할당</h2>
<p>이번 chapter에서는 C언어의 메모리 구조에 대해 알아보고자 한다.</p>
<h3 id="25-1-c언어의-메모리-구조">25-1 &quot;C언어의 메모리 구조&quot;</h3>
<p>프로그램을 실행하면 해당 프로그램의 실행을 위한 메모리 공간이 운영체제에 의해서 미리 마련이 된다.
그리고 바로 이 메모리 공간 내에서 변수가 선언되고, 문자열이 선언되는 것이다.</p>
<h4 id="메모리의-구성"><em>메모리의 구성</em></h4>
<p>프로그램 실행 시 운영체제에 의해서 마련되는 메모리의 구조는 다음과 같이 네 개의 영역으로 구분된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/1c30d439-7428-43da-ba68-b117566e8e85/image.png" alt=""></p>
<p>메모리 공간을 나눠놓은 이유는 유사한 성향의 데이터를 묶어서 저장하면 관리가 용이해지고 메모리의 접근속도가 향상되기 때문이다.</p>
<h4 id="메모리-영역별로-저장되는-데이터-유형"><em>메모리 영역별로 저장되는 데이터 유형</em></h4>
<p>이어서 각 영역별 특성에 대해서 구체적으로 살펴보자.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/0118e3a8-b334-4e10-8723-8da16c8f34c3/image.png" alt=""></p>
<ol>
<li><p>코드 영역 (Code Area)
코드 영역은 이름 그대로 실행할 프로그램의 코드가 저장되는 메모리 공간이다.
따라서 CPU는 코드 영역에 저장된 명령문들을 하나씩 가져가서 실행한다.</p>
</li>
<li><p>데이터 영역 (Data Area)
데이터 영역에는 전역변수와 static으로 선언되는 static 변수가 할당된다.
즉, 이 영역에 할당되는 변수들은 프로그램의 시작과 동시에 메모리 공간에 할당되어 <strong>프로그램 종료 시까지 남아있게 된다</strong>는 특징이 있다.</p>
</li>
<li><p>스택 영역 (Stack Area)
스택 영역에는 지역변수와 매개변수가 할당된다.
이 영역에 할당되는 변수들은 <strong>선언된 함수를 빠져나가면 소멸된다</strong>는 특징이 있다.</p>
</li>
<li><p>힙 영역 (Heap Area)
데이터 영역에 할당되는 변수와 스택 영역에 할당되는 변수들은 생성과 소멸의 시점이 이미 결정되어 있다.
그러나 프로그램을 구현하다 보면, 이 두 영역의 변수들과는 다른 성격의 변수가 필요하기도 하다.
원하는 시점에 변수를 할당하고 또 소멸하도록 지원하는 변수들이 할당되는 영역이 힙 영역이다.
이 힙 영역을 대상으로 하는 변수의 할당과 소멸에 대해서는 잠시 후 배울 예정이다.</p>
</li>
</ol>
<h4 id="프로그램의-실행에-따른-메모리의-상태-변화"><em>프로그램의 실행에 따른 메모리의 상태 변화</em></h4>
<p>프로그램의 실행과정에서 보이는 메모리 공간의 변화를 통해서 각 영역별 특징에 대해 다시 한번 정리하겠다.
(코드 영역은 변수가 할당되는 영역이 아니기 때문에 생략한다.)
아래 그림에서는 왼편의 코드가 실행된 직후(main 함수가 호출되기 직전)의 상황을 보이고 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/b454882e-85c9-43fd-85d8-0ed3ed872b64/image.png" alt=""></p>
<p>실제로 main함수가 호출되기 이전에 데이터 영역이 먼저 초기화된다.
전역변수와 static 변수가 먼저 데이터 영역에 할당이 되고 나서 main 함수가 호출된다. (이 그림에서는 static 변수가 없다.)</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/4a4435a2-a5ed-480c-b446-001ba58ade87/image.png" alt=""></p>
<p>이어서 main 함수가 호출되고 main 함수 내에 선언된 지역변수 num1이 스택에 할당된다.
다음으로 main 함수 내에서 fct 함수가 호출된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/c8daaa0f-e9bd-4b89-ba24-6eb8ce9dae1c/image.png" alt=""></p>
<p>fct 함수의 매개변수가 스택에 할당되고 fct 함수의 지역변수도 그 뒤에 할당된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/369fcbb4-f79b-44b7-a4f4-4dcfd8aa723d/image.png" alt=""></p>
<p>다음으로 fct 함수가 반환을 하면서 fct 함수호출 시 할당되었던 매개변수와 지역변수가 소멸된다.
위 그림은 fct 함수를 빠져 나온 이후에 main 함수 내에서 num1의 값이 증가한 상황까지의 결과를 보여주고 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/84518cbc-8977-4b22-b62c-ae7faa62d651/image.png" alt=""></p>
<p>이어서 다시 fct 함수의 호출이 진행되고, 더불어 매개변수와 지역변수가 다시 스택에 할당된다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/a4844a69-2713-4198-8216-c4387c476e38/image.png" alt=""></p>
<p>마지막으로 fct 함수가 반환되고, main 함수의 return문이 실행되면서 프로그램이 종료된다.
프로그램이 종료되면 운영체제에 의해서 할당된 메모리 공간 전체를 반환하게 되는데 그때 전역변수가 소멸된다.</p>
<p>지금까지 살펴 본 내용을 기준으로 스택 영역의 특징을 하나 더 배워보자.
다음 순서로 함수가 호출되었다고 가정해보자.</p>
<pre><code>main 함수의 호출 → fct1 함수의 호출 → fct2 함수의 호출</code></pre><p>이는 fct1 함수 내에서 fct2 함수가 호출되었다는 뜻이다.
(fct1 함수 호출되고 반환된 이후 fct2 함수 호출이 이루어진게 아니다.)</p>
<p>이 경우 지역(매개)변수의 소멸순서는 다음과 같다.</p>
<pre><code>fct2의 지역변수 소멸 → fct1의 지역변수 소멸 → main의 지역변수 소멸</code></pre><p>먼저 호출된 함수의 스택공간일수록 늦게 해제된다는 것을 알 수 있다.
그래서 메모리 영역의 이름이 스택이다. (자료구조에서 배웠던 거!)</p>
<hr>
<h3 id="25-2-메모리의-동적-할당">25-2 &quot;메모리의 동적 할당&quot;</h3>
<h4 id="전역변수와-지역변수로-해결이-되지-않는-상황"><em>전역변수와 지역변수로 해결이 되지 않는 상황</em></h4>
<p>다음 예제는 프로그램 사용자로부터 입력 받은 문자열의 정보를 반환하는 함수가 정의되어 있다.
이 함수의 문제점이 뭔지 찾아보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

char * ReadUserName(void)
{
    char name[30];
    printf(&quot;Enter your name: &quot;);
    gets(name);
    return name;
}

int main()
{
    char * name1;
    char * name2;
    name1 = ReadUserName();
    printf(&quot;name1: %s \n&quot;, name1);
    name2 = ReadUserName();
    printf(&quot;name2: %s \n&quot;, name2);
    return 0;
}

&gt; 출력
gcc .\ReadStringFault1.c
.\ReadStringFault1.c: In function &#39;ReadUserName&#39;:
.\ReadStringFault1.c:8:12: warning: function returns address of local variable [-Wreturn-local-addr]        
     return name;
            ^~~~</code></pre>
<p>위 예제에서 문제점은 함수 내에 지역적으로 선언된 배열(변수)의 주소 값을 반환하는데 있다.
함수 내에서 프로그램 사용자로부터 문자열을 입력 받아서 그 결과를 반환하고 싶어보이는데 그 문자열이 저장되어 있는 배열이 지역적으로 선언되었기 때문에 함수를 빠져나오면서 소멸된다.
다음 예제는 어떨까?</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
char name[30];

char * ReadUserName(void)
{
    printf(&quot;Enter your name: &quot;);
    gets(name);
    return name;
}

int main()
{
    char * name1;
    char * name2;
    name1 = ReadUserName();
    printf(&quot;name1: %s \n&quot;, name1);
    name2 = ReadUserName();
    printf(&quot;name2: %s \n&quot;, name2);

    printf(&quot;name1: %s \n&quot;, name1);
    printf(&quot;name2: %s \n&quot;, name2);
    return 0;
}

&gt; 출력
Enter your name: Yoon sung woo
name1: Yoon sung woo
Enter your name: Choi jun kyung
name2: Choi jun kyung
name1: Choi jun kyung
name2: Choi jun kyung</code></pre>
<p>하나의 전역변수(전역으로 선언된 배열)을 이용하면, 이 전역변수를 덮어쓰게 되기 때문에, 함수 호출을 통해서 얻게 된 이름정보가 유지되지 않는다.
따라서, 지역변수도 전역변수도 답이 아니다...
그렇다면 어떠한 성격의 변수가 필요할까?
&quot;함수가 매번 호출될 때마다 새롭게 할당되고 또 함수를 빠져나가도 유지가 되는 유형의 변수&quot;
다시 말해서, 지역변수와 같이 함수가 호출될 때마다 매번 할당이 이뤄지지만, 할당이 되면 전역변수와 마찬가지로 함수를 빠져나가도 소멸되지 않는 성격의 변수가 필요하다.
<strong>생성과 소멸의 시기가 지역변수나 전역변수와 다른 유형의 변수</strong>는 malloc과 free라는 이름의 함수를 통해서 힙 영역에 할당하고 소멸할 수 있다.</p>
<h4 id="힙-영역의-메모리-공간-할당과-해제-malloc과-free-함수"><em>힙 영역의 메모리 공간 할당과 해제: malloc과 free 함수</em></h4>
<p><code>malloc</code>함수를 이용해서 힙 영역 메모리 공간에 할당하고,
<code>free</code>함수를 이용해서 힙 영역에 할당된 메모리 공간에서 해제할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/f4cabe4f-e894-44ec-ab6d-84b6db79c335/image.png" alt=""></p>
<p>힙 영역을 흔히 &#39;프로그래머가 관리하는 메모리 공간&#39;이라 한다.
그 이유는 <code>malloc</code>함수 호출로 할당된 메모리 공간은 프로그래머가 직접 <code>free</code>함수의 호출을 통해 해제하지 않으면 계속 남아있기 때문이다.
따라서 위 두 함수는 쌍을 이뤄 호출하게 되고 호출의 형태는 다음과 같다.</p>
<pre><code class="language-c">int main()
{
    void * ptr1 = malloc(4);    // 4바이트가 힙 영역에 할당
    void * ptr2 = malloc(12);    // 12바이트가 힙 영역에 할당
    ...
    free(ptr1);        // ptr1이 가리키는 4바이트 메모리 공간 해제
    free(ptr2);        // ptr2가 가리키는 12바이트 메모리 공간 해제
    ....
}</code></pre>
<p><code>malloc</code>함수는 인자로 전달된 정수 값에 해당하는 바이트 크기의 메모리 공간을 힙 영역에 할당하고, 이 메모리 공간의 주소 값을 반환한다.
<code>free</code>함수는 호출될 때마다 ptr1과 ptr2가 가리키는 메모리 공간이 소멸된다.
따라서 <code>malloc</code>함수와 <code>free</code>함수의 호출위치 및 시점에는 제한이 없다. 원하는 시점에 할당하고 원하는 시점에 소멸이 가능하다.
힙에 할당된 메모리 공간은 포인터를 이용해서 접근하는 방법 밖에 없다.
<code>malloc</code>함수는 주소 값을 반환하는데 이때 반환형이 무엇일까?</p>
<h4 id="malloc-함수의-반환형이-void형-포인터인-이유--힙-영역으로의-접근"><em>malloc 함수의 반환형이 void형 포인터인 이유 &amp; 힙 영역으로의 접근</em></h4>
<p><code>malloc</code>함수의 반환형은 void형 포인터이다.
따라서 <code>malloc</code>함수의 반환 값에 아무런 가공도 가하지 않으면 이를 이용해서는 할당된 메모리 공간에 접근이 불가능하다.</p>
<pre><code class="language-c">void * ptr = malloc(sizeof(int));    // int형 변수 크기의 메모리 공간 할당
*ptr = 20;    // ptr이 void형 포인터이므로 컴파일 에러</code></pre>
<p>그럼에도 불구하고 <code>malloc</code>함수의 반환형이 void형 포인터인 이유가 무엇일까?
<code>malloc</code>함수는 전달받은 데이터의 형이 정해지지 않았기 때문에 이 데이터가 어떤 자료형을 변할지 몰라서 void형 포인터로 반환하는 것이다.</p>
<pre><code class="language-c">void * ptr1 = malloc(sizeof(int));
void * ptr2 = malloc(sizeof(double));
void * ptr3 = malloc(sizeof(int)*7);
void * ptr4 = malloc(sizeof(double)*9);</code></pre>
<p>이렇게 하면 <code>malloc</code>함수에 충분한 자료형 정보를 제공한 것일까?
sizeof 연산과 곱셈연산 이후 정작 malloc 함수에게 전달되는 인자는 다음과 같다.</p>
<pre><code class="language-c">void * ptr1 = malloc(4);
void * ptr1 = malloc(8);
void * ptr1 = malloc(28);
void * ptr1 = malloc(72);</code></pre>
<p>때문에 <code>malloc</code>함수는 원하는 크기만큼 메모리 공간을 할당하고, 그 메모리의 주소값을 반환한다. 이 후 포인터 형의 변환을 통해 직접 결정하는 것이다.
따라서 다음과 같이 void으로 반환되는 주소 값을 적절히 형 변환해서 할당된 메모리 공간에 접근해야 한다.</p>
<pre><code class="language-c">int * ptr1 = (int *)malloc(sizeof(int));
double * ptr2 = (double *)malloc(sizeof(double));
int * ptr3 = (int *)malloc(sizeof(int)*7);
double * ptr4 = (double *)malloc(sizeof(double)*9);</code></pre>
<p>지금까지의 내용을 바탕으로 아래 예제에서 힙 영역에 int형 변수와 int형 배열을 각각 선언하고 접근했다가 해제해보겠다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;

int main()
{
    int * ptr1 = (int *)malloc(sizeof(int));
    int * ptr2 = (int *)malloc(sizeof(int)*7);
    int i;

    *ptr1 = 20;
    for(i = 0; i &lt; 7; i++)
        ptr2[i] = i+1;

    printf(&quot;%d \n&quot;, *ptr1);
    for(i = 0; i &lt; 7; i++)
        printf(&quot;%d &quot;, ptr2[i]);

    free(ptr1);
    free(ptr2);

    return 0;
}

&gt; 출력
20 
1 2 3 4 5 6 7</code></pre>
<p>참고로 <code>malloc</code>함수는 메모리 공간의 할당에 실패할 경우 NULL을 반환한다.
따라서, 메모리 할당의 성공여부를 확인하고자 한다면 다음과 같이 코드를 작성하면 된다.</p>
<pre><code class="language-c">int * ptr = (int *)malloc(sizeof(int));
if(ptr==NULL)
{
    // 메모리 할당 실패에 따른 오류 처리
}</code></pre>
<p>그리고 <code>malloc</code>함수의 호출을 통한 메모리 공간의 할당을 가리켜 <code>동적 할당(dynamic allocation)</code>이라 한다.
이유는 할당되는 메모리의 크기를 컴파일러가 결정하지 않고, 프로그램의 실행 중간에 호출되는 <code>malloc</code>함수가 결정하기 때문이다.</p>
<h4 id="free-함수를-호출하지-않는다면"><em>free 함수를 호출하지 않는다면?</em></h4>
<p>만약 <code>free</code>함수를 호출하지 않고 예제를 계속해서 실행하면 메모리 공간의 부족으로 운영체제의 실행에 문제가 발생할 것같다.
하지만 프로그램 실행 시 할당된 모든 메모리 공간은 프로그램이 종료되면서 운영체제에 의해서 전부 해제가 되기 때문에 문제는 없다.
다만, 이 예제는 간단한 것이고 프로그램을 구현한다면 더 복잡한 상황이 되기 때문에 반드시 <code>free</code>함수를 호출해야 한다.</p>
<h4 id="문자열을-반환하는-함수를-정의하는-문제의-해결"><em>문자열을 반환하는 함수를 정의하는 문제의 해결</em></h4>
<p>이제 <code>malloc</code>함수와 <code>free</code>함수를 알게 되었으니 이 문제를 해결할 수 있다.
올바른 예제를 통해 알아보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;

char * ReadUserName()
{
    char * name = (char *)malloc(sizeof(char)*30);
    printf(&quot;Enter your name: &quot;);
    gets(name);
    return name;
}

int main()
{
    char * name1;
    char * name2;
    name1 = ReadUserName();
    printf(&quot;name1: %s \n&quot;, name1);
    name2 = ReadUserName();
    printf(&quot;name2: %s \n&quot;, name2);

    printf(&quot;name1: %s \n&quot;, name1);
    printf(&quot;name2: %s \n&quot;, name2);
    free(name1);
    free(name2);
    return 0;
}

&gt; 출력
Enter your name: Yoon Sung Woo
name1: Yoon Sung Woo 
Enter your name: Hong Sook Jin
name2: Hong Sook Jin 
name1: Yoon Sung Woo 
name2: Hong Sook Jin </code></pre>
<p>이렇든 <code>malloc</code>함수와 <code>free</code>함수를 이용하면 메모리 공간의 할당과 소멸의 시점을 프로그래머가 직접 결정할 수 있어 전역변수나 지역변수가 감당하지 못하는 일들을 감당할 수 있다.</p>
<h4 id="calloc-함수"><em>calloc 함수</em></h4>
<p>힙 영역에 메모리 공간을 할당하는 함수로는 <code>calloc</code>함수도 있다.
<code>malloc</code>함수와 유일한 차이점은 메모리 공간의 할당을 위한 인자의 전달방식에 있다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/5657d14b-7771-4a33-8a4f-e48cab77a38c/image.png" alt=""></p>
<p><code>malloc</code>함수와 달리 <code>calloc</code>함수는 두 개의 숫자를 인자로 받는다.
첫 번째 전달인자로는 할당할 블록의 갯수 정보가 전달되고, 두 번째 전달인자로는 블록 하나당 바이트 크기의 정보가 전달된다.
따라서 <code>calloc</code>함수의 호출방식은 &quot;(elt_size)크기의 블록을 (elt_count)갯수만큼 힙 영역에 할당해주세요&quot;다.
또 다른 차이점은 <code>malloc</code>함수는 할당된 메모리 공간을 별도의 값으로 초기화하지 않는다.
따라서 할당된 메모리 공간이 쓰레기 값으로 채워진다.
<code>calloc</code>함수는 할당된 메모리 공간의 모든 비트를 0으로 초기화시킨다.
<code>calloc</code>함수의 호출로 할당된 메모리 공간을 해제할 때에도 <code>malloc</code>함수와 동일하게 <code>free</code>함수를 사용하면 된다.</p>
<h4 id="realloc-함수"><em>realloc 함수</em></h4>
<p>한번 할당된 메모리 공간은 그 크기를 확장할 수 없다.
이는 모든 영역의 메모리 공간에 해당되는 말이다.
하지만 그 영역이 힙이고 realloc 함수를 사용하면 가능하다.</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/757f06c6-8b66-4662-9542-dc813b3c3d23/image.png" alt=""></p>
<p>이 함수의 첫 번째 전달인자는 확장하고자 하는 힙 메모리의 시작 주소 값을 전달한다.
두 번째 전달인자는 확장하고자 하는 메모리의 전체 크기를 전달한다.
→ &quot;ptr이 가리키는 메모리의 크기를 size의 크기로 조절해줘(늘려줘)&quot;</p>
<p>함수호출 성공 시에는 새로 할당된 메모리 주소값이 반환되고, 실패 시에는 NULL이 반환된다.
<code>realloc</code>함수의 호출 형태는 다음과 같다.</p>
<pre><code class="language-c">int main(void)
{
    int * arr = (int *)malloc(sizeof(int)*3);    // 길이가 3인 int형 배열 할당
    ....
    arr = (int *)realloc(arr, sizeof(int)*5);    // 길이가 5인 int형 배열로 확장
}</code></pre>
<p>위 코드의 실행결과는 반환 값을 기준으로 두 가지로 구분된다.</p>
<ol>
<li><code>malloc</code>함수가 반환한 주소 값과 <code>realloc</code>함수가 반환한 주소 값이 같은 경우
: 기존에 할당된 메모리 공간의 뒤를 이어서, 확장할 영역이 넉넉한 경우에 발생.</li>
<li><code>malloc</code>함수가 반환한 주소 값과 <code>realloc</code>함수가 반환한 주소 값이 같지 않은 경우
: 메모리 공간이 넉넉하지 않아 힙의 다른 위치에 새로이 요구하는 크기의 메모리 공간을 별도로 할당해서 이전 배열에 저장된 값을 복사.</li>
</ol>
<hr>
<h2 id="review"><code>&lt;Review&gt;</code></h2>
<p>드디어 동적 할당까지 왔다!
malloc과 free, calloc, realloc 모두 다 재밌는 녀석들이다~!</p>
<p>사실 이 게시글을 올리고 있는 지금 딱 생일이 되었다,,, 헿
이번 생일은 성인된 이후에 처음으로 솔로로 보내는데... ㅋㅋ 감회가 새롭다 ㅎㅎㅎ
인턴십이 있으니깐~ 외롭진 않다!</p>
<p>계속해서 가보자구~~</p>
<p><img src="https://velog.velcdn.com/images/mingming_eee/post/8f42d5d8-895c-4445-b09a-f18b544a3b3a/image.png" alt=""></p>
]]></description>
        </item>
    </channel>
</rss>