<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>미완성의 기록</title>
    <link>https://beta-log.tistory.com/</link>
    <description>느려도 천천히 쌓아가는 중 </description>
    <language>ko</language>
    <pubDate>Sat, 18 Jul 2026 01:07:49 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>와일</managingEditor>
    <image>
      <title>미완성의 기록</title>
      <url>https://tistory1.daumcdn.net/tistory/8562782/attach/f67e7be2c7be430c8ff4753366a94383</url>
      <link>https://beta-log.tistory.com</link>
    </image>
    <item>
      <title>파인튜닝은 모델보다 데이터에서 시작된다 - 한 권으로 끝내는 실전 LLM 파인튜닝, 강다솔</title>
      <link>https://beta-log.tistory.com/54</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;615&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c3BGhS/dJMcagsIuMY/pxISkOAXJpwWO7p3XfWFIk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c3BGhS/dJMcagsIuMY/pxISkOAXJpwWO7p3XfWFIk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c3BGhS/dJMcagsIuMY/pxISkOAXJpwWO7p3XfWFIk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc3BGhS%2FdJMcagsIuMY%2FpxISkOAXJpwWO7p3XfWFIk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;458&quot; height=&quot;615&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;615&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;LLM을 활용한 프로젝트를 몇 번 진행했지만, 언어 모델이 어떤 원리로 학습되고 파인튜닝되는지는 제대로 이해하지 못하고 있었다. 원하는 결과가 나오지 않으면 프롬프트를 수정하거나 다른 모델을 사용해 보는 것이 가장 먼저 떠올리는 해결책이었다. 데이터셋은 모델에 입력하기 위해 준비해야 하는 재료 정도로만 생각했던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;『한 권으로 끝내는 실전 LLM 파인튜닝』은 제목 그대로 언어 모델을 파인튜닝하는 실전 과정을 다루는 책이다. 하지만 책을 읽고 가장 크게 남은 것은 구체적인 학습 코드나 파인튜닝 기법보다도, &lt;b&gt;좋은 모델을 만들기 위해서는 데이터와 평가 기준을 먼저 설계해야 한다는 사실&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;책을 읽으며 내가 그동안 제대로 알지 못한 채 사용하고 있던 개념도 많다는 것을 알게 되었다. 임베딩, 어텐션, 옵티마이저, LoRA, 양자화처럼 익숙하게 들어 본 용어도 막상 원리를 설명하려고 하니 쉽게 말할 수 없었다. 이번 독서는 파인튜닝 방법을 한 번에 익히는 과정이라기보다, 앞으로 더 공부해야 할 질문들을 발견하는 과정에 가까웠다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인공지능은 어떻게 지금의 언어 모델이 되었을까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;책의 앞부분에서는 인공지능과 자연어 처리 기술의 발전 과정을 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;1950년 앨런 튜링은 「Computing Machinery and Intelligence」에서 &amp;ldquo;기계는 생각할 수 있는가?&amp;rdquo;라는 질문을 던졌다. 튜링은 사고나 지능을 완벽하게 정의하려 하기보다, 기계가 인간과 구별하기 어려운 행동을 보일 수 있는지를 평가하는 실용적인 접근을 택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이후 인간의 신경세포를 수학적으로 모방한 초기 신경망 모델이 등장했고, 프랭크 로젠블랫은 퍼셉트론을 제안했다. 퍼셉트론은 입력값에 가중치를 곱하고 그 결과를 바탕으로 출력을 결정하는 단순한 구조였다. 오늘날의 거대한 신경망과 비교하면 매우 제한적인 모델이지만, 기계가 데이터로부터 가중치를 조정하며 학습할 수 있다는 가능성을 보여주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;1980년대에는 오차를 출력층에서 입력층 방향으로 전달하며 가중치를 수정하는 역전파 알고리즘이 주목받았다. 2010년대에는 Word2Vec과 같은 임베딩 기술, 어텐션 메커니즘, 트랜스포머가 차례로 등장했다. 이후 BERT와 GPT 같은 사전 학습 언어 모델이 발전하면서, 매번 모델을 처음부터 학습하지 않고 거대한 모델이 이미 학습한 언어 능력을 특정 목적에 맞게 활용할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 따라가며 인공지능의 발전은 완전히 새로운 기술이 갑자기 나타난 결과라기보다, 오랫동안 제기된 질문과 한계를 하나씩 해결해 온 결과라는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;동시에 새로운 질문도 생겼다. 튜링은 인간과 비슷하게 행동하는지를 기준으로 기계의 지능을 평가하고자 했다. 그러나 인간과 구별하기 어려운 문장을 생성하는 것만으로 기계가 사고한다고 말할 수 있을까? 인공지능이 AGI로 발전하기 위해서는 언어적 행동뿐 아니라 현실에 대한 이해, 지속적인 기억, 목표 설정, 상황에 따른 판단과 같은 능력도 함께 고려해야 하지 않을까?&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기계가 의미를 이해하는 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;언어 모델이 문장을 처리하려면 먼저 문자를 컴퓨터가 계산할 수 있는 숫자로 변환해야 한다. 이 과정에서 사용되는 핵심 개념 중 하나가 임베딩이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;예를 들어 어휘의 크기가 1,000이고 임베딩 차원을 300으로 설정한 &lt;code&gt;nn.Embedding&lt;/code&gt; 레이어가 있다고 하자. 처음에는 이것이 정확히 어떤 의미인지 잘 이해하지 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 설정은 1,000개의 단어마다 각각 하나의 300차원 벡터를 부여한다는 뜻이다. 단어 하나가 300개의 숫자로 표현되는 것이다. 따라서 임베딩 레이어는 전체적으로 &lt;code&gt;1000 &amp;times; 300&lt;/code&gt; 크기의 학습 가능한 행렬을 갖는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;중요한 점은 300개의 숫자가 사람이 이해할 수 있는 300개의 명확한 속성과 일대일로 대응하는 것은 아니라는 것이다. 첫 번째 숫자는 성별, 두 번째 숫자는 감정, 세 번째 숫자는 생물 여부를 나타내는 식으로 해석할 수 있는 것이 아니다. 모델은 학습 과정에서 단어가 사용되는 맥락을 바탕으로 벡터의 값을 조정하고, 여러 차원의 조합을 통해 단어 간의 관계를 표현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;비슷한 문맥에서 사용되는 단어는 임베딩 공간에서 가까운 위치에 놓일 가능성이 커진다. 이를 통해 모델은 단어가 완전히 같지 않더라도 의미적 유사성을 계산할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Word2Vec이 혁신적으로 평가된 이유도 여기에 있다. 이전의 단어 표현 방식은 각각의 단어를 서로 완전히 독립적인 기호처럼 다루는 경우가 많았다. 반면 Word2Vec은 대규모 텍스트에서 단어가 사용되는 문맥을 학습해, 단어 사이의 의미적 관계를 벡터 공간에 표현할 수 있음을 보여주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;다만 임베딩 차원이 높다고 항상 좋은 것은 아니다. 차원이 너무 작으면 단어의 복잡한 관계를 충분히 표현하기 어렵지만, 지나치게 크면 더 많은 메모리와 연산량이 필요하고 학습 데이터에 과도하게 맞춰질 가능성도 있다. 결국 적절한 임베딩 차원은 모델의 크기, 데이터의 양, 수행하려는 작업을 함께 고려해 결정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 부분을 읽으며 내가 프로젝트에서 경험했던 현상도 떠올랐다. 한국어 텍스트를 생성하도록 했는데 문장 중간에 한자나 러시아어처럼 보이는 문자가 섞여 나온 적이 있었다. 당시에는 프롬프트가 잘못되었거나 모델이 지시를 제대로 따르지 않는다고 생각해 프롬프트를 계속 수정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;책에서는 양자화 과정에서 발생한 작은 오차로 인해 임베딩 공간상 가까운 토큰이 잘못 선택될 수 있다고 설명한다. 모든 문자 혼합 현상을 하나의 원인으로 설명할 수는 없겠지만, 출력 오류가 반드시 프롬프트의 문제만은 아닐 수 있다는 점이 인상적이었다. 모델의 표현 방식과 정밀도 역시 생성 결과에 영향을 줄 수 있었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;트랜스포머는 문장 속 관계를 어떻게 찾을까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;트랜스포머는 RNN이나 LSTM처럼 문장을 처음부터 끝까지 순서대로 처리하는 대신, 어텐션 메커니즘을 통해 문장 안에 있는 단어들의 관계를 계산한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;책의 설명을 읽을 때 가장 이해하기 어려웠던 부분은 쿼리, 키, 밸류였다. 책을 읽을 때 왜 굳이 세 가지가 필요한지 감이 오지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 내가 이해한 방식은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿼리는 현재 단어가 다른 단어에서 어떤 정보를 찾고 있는지를 나타낸다.&lt;/li&gt;
&lt;li&gt;키는 각 단어가 어떤 정보를 가지고 있는지를 비교하기 위한 기준이 된다.&lt;/li&gt;
&lt;li&gt;밸류는 관계가 중요하다고 판단되었을 때 실제로 가져올 정보다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;도서관에서 책을 검색하는 과정과 비슷하게 생각할 수도 있다. 검색어는 쿼리, 각 책을 찾기 위한 제목이나 분류 정보는 키, 실제 책의 내용은 밸류에 해당한다. 쿼리와 키를 비교해 관련성을 계산한 뒤, 관련성이 높은 단어의 밸류를 더 많이 반영하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셀프 어텐션은 이 과정을 문장 안의 모든 단어에 대해 수행한다. 덕분에 문장에서 멀리 떨어져 있는 단어끼리도 직접 관계를 계산할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;멀티헤드 어텐션은 이러한 관계 계산을 여러 관점에서 동시에 수행한다. 하나의 어텐션만으로는 문법적 관계, 의미적 유사성, 지시 대상, 문장의 분위기처럼 다양한 관계를 모두 포착하기 어려울 수 있다. 여러 개의 헤드는 서로 다른 표현 공간에서 관계를 계산하고, 그 결과를 합쳐 더 다양한 정보를 반영한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그렇다면 어텐션 뒤에 피드포워드 네트워크가 필요한 이유는 무엇일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;책에서는 피드포워드 네트워크가 어텐션을 통해 얻은 표현을 더 풍부하게 만든다고 설명한다. 처음에는 &amp;lsquo;풍부하게 만든다&amp;rsquo;는 말이 구체적으로 무엇을 뜻하는지 이해하기 어려웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 어텐션과 피드포워드 네트워크의 역할을 다음과 같이 구분해서 이해하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; letter-spacing: 0px;&quot;&gt;&amp;nbsp;어텐션이 다른 단어에서 어떤 정보를 가져올지를 결정한다면, 피드포워드 네트워크는 가져온 정보를 각 단어의 위치에서 다시 가공하고 변환한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;피드포워드 네트워크에는 비선형 활성화 함수가 포함된다. 단순한 선형 연산만 반복하면 여러 층을 쌓더라도 복잡한 관계를 표현하는 데 한계가 있다. 비선형 변환을 거치면서 모델은 입력 특징을 조합하고 변형해 더 복잡한 패턴을 학습할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;아직 쿼리&amp;middot;키&amp;middot;밸류 행렬이 실제 학습 과정에서 어떻게 달라지는지, 각 어텐션 헤드가 어떤 정보를 담당하게 되는지까지 완전히 이해한 것은 아니다. 트랜스포머를 단순히 구조도로 외우기보다, 문장 하나가 각 층을 통과하며 어떻게 변하는지 별도로 공부해 보고 싶다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 모델을 처음부터 학습하지 않고 파인튜닝할까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정한 목적의 모델이 필요하다면 처음부터 원하는 데이터만 학습시키면 되지 않을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론적으로는 가능하지만, 현실적으로는 여러 문제가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;우선 자연스러운 언어 능력은 매우 다양하고 방대한 텍스트를 통해 형성된다. 제한된 데이터만으로 모델을 처음부터 학습하면 특정 정보나 정해진 답변 형식은 익힐 수 있어도, 문맥에 맞는 자연스러운 언어를 생성하기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;데이터의 규모가 작을수록 과적합의 위험도 커진다. 학습 데이터에 포함된 문장은 잘 처리하지만, 표현이나 상황이 조금만 달라져도 제대로 대응하지 못할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;사전 학습된 언어 모델은 이미 방대한 데이터에서 언어 구조와 다양한 지식을 학습했다. 파인튜닝은 이 기반 능력을 유지하면서 특정 작업이나 도메인에 맞게 모델의 행동을 조정하는 과정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;파인튜닝에는 모델의 전체 가중치를 수정하는 전체 파인튜닝과, 일부 파라미터만 효율적으로 학습하는 PEFT 방식이 있다. 대표적인 PEFT 기법에는 어댑터 튜닝, 프롬프트 튜닝, LoRA가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;LoRA는 기존 모델의 거대한 가중치 행렬 전체를 직접 수정하지 않고, 작은 저차원 행렬 두 개를 추가로 학습하는 방식이다. 두 행렬을 곱해 만든 변화량을 기존 가중치에 더함으로써, 훨씬 적은 수의 파라미터만 학습하면서 모델의 행동을 조정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;QLoRA는 여기에 양자화를 결합한다. 모델의 숫자를 더 적은 비트로 표현해 메모리 사용량을 줄이고, LoRA 파라미터를 학습함으로써 제한된 자원에서도 비교적 큰 모델을 파인튜닝할 수 있게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;전체 가중치를 수정하는 방법보다 효율적이라는 점은 이해했지만, LoRA의 저차원 행렬이 구체적으로 어떤 변화를 학습하는지는 아직 명확하게 설명하기 어렵다. 단순히 &amp;ldquo;작은 행렬만 학습한다&amp;rdquo;는 설명을 넘어, 왜 거대한 가중치 변화가 저차원 구조로도 근사될 수 있는지 더 찾아보고 싶다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파인튜닝에서 정말 중요한 것은 데이터&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 책을 읽고 가장 크게 남은 내용은 모델 구조보다 데이터에 관한 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;파인튜닝을 시작하면 어떤 모델을 사용할지, 전체 파인튜닝을 할지 LoRA를 사용할지, 학습률이나 배치 크기를 어떻게 설정할지부터 고민하기 쉽다. 나 역시 모델과 학습 방법이 성능을 결정하는 핵심이라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;하지만 어떤 모델을 선택하더라도 데이터의 품질이 낮으면 좋은 결과를 기대하기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;파인튜닝 데이터는 단순히 많은 양을 모으는 것으로 끝나지 않는다. 모델이 어떤 작업을 수행해야 하는지에 맞춰 데이터를 구성해야 하며, 잘못된 정보, 중복된 문장, 불필요한 기호와 형식 오류를 정리해야 한다. 데이터에 편향이 포함되어 있다면 모델의 출력에도 그대로 반영될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;학습용 데이터와 테스트용 데이터를 분리하는 것도 중요하다. 학습에 사용한 데이터로 모델을 평가하면, 모델이 새로운 상황에서도 잘 작동하는지 확인할 수 없다. 이미 본 문제의 답을 기억하고 있는지를 평가하는 것에 가까워지기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 데이터는 다음과 같이 나눌 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;학습 데이터:&lt;/b&gt; 모델의 가중치를 조정하는 데 사용한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;검증 데이터:&lt;/b&gt; 학습 중 모델의 성능을 확인하고 하이퍼파라미터를 조정하는 데 사용한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 데이터:&lt;/b&gt; 모든 학습과 설정이 끝난 뒤 최종적인 일반화 성능을 평가하는 데 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 구분의 목적은 모델이 학습 데이터만 잘 외운 것인지, 처음 보는 데이터에도 학습한 패턴을 적용할 수 있는지를 확인하는 데 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;돌이켜 보면 나도 프로젝트를 진행하면서 데이터가 중요하다는 말을 알고는 있었다. 하지만 실제 과정에서는 데이터 준비를 정식 설계 단계라기보다 모델을 실행하기 전에 거쳐야 하는 절차처럼 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;결과가 좋지 않으면 데이터셋의 구성과 분포를 먼저 살펴보기보다 프롬프트를 고치고 생성 설정을 바꾸었다. 문제를 명확히 정의하지 않은 채 수정부터 반복하면서, 한 번이면 끝났을 일을 여러 번 하기도 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;책을 읽으며 데이터 준비는 모델 학습 이전의 부가 작업이 아니라, 모델이 무엇을 학습할지를 결정하는 핵심 설계 과정이라는 점을 다시 생각하게 되었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;좋은 모델은 뭘 보고 정할까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;좋은 데이터를 준비하고 모델을 학습했다고 해서 과정이 끝나는 것은 아니다. 모델이 실제로 좋아졌는지를 판단하려면 평가 기준이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;기계 번역에서는 BLEU와 같은 자동 평가 지표를 사용할 수 있고, 분류 문제에서는 정확도, 정밀도, 재현율 등을 사용할 수 있다. 그러나 대화형 언어 모델의 답변은 하나의 정답만 존재하지 않는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;같은 질문에도 서로 다른 표현의 답변이 모두 적절할 수 있다. 문법적으로 자연스러워도 사실과 다른 답변일 수 있고, 사실은 맞지만 사용자의 요청 형식을 지키지 않을 수도 있다. 이 때문에 대화 모델의 성능을 모든 상황에서 설명할 수 있는 하나의 평가 지표는 존재하기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;따라서 모델을 평가할 때는 실제 사용 목적에 따라 기준을 구체적으로 정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 논문 추천 모델이라면 다음과 같은 기준을 생각할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추천된 논문이 사용자의 관심 분야와 관련 있는가?&lt;/li&gt;
&lt;li&gt;이미 읽거나 제외한 논문을 다시 추천하지 않는가?&lt;/li&gt;
&lt;li&gt;추천 이유가 논문의 실제 내용과 일치하는가?&lt;/li&gt;
&lt;li&gt;최신성만 강조해 핵심 논문을 놓치고 있지는 않은가?&lt;/li&gt;
&lt;li&gt;정해진 출력 형식을 안정적으로 지키는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;고객 상담 모델이라면 정확성뿐 아니라 친절성, 안전성, 정책 준수, 개인정보 노출 가능성 등을 평가해야 할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;책에서는 OpenAI 모델을 평가자로 사용하는 방법과 Structured Outputs를 활용해 평가 결과를 정해진 형식으로 받는 방법도 소개한다. LLM을 사용해 대규모 결과를 평가하면 시간과 비용을 절약할 수 있지만, 평가 모델 역시 편향되거나 일관되지 않은 판단을 내릴 수 있다. 따라서 자동 평가와 사람의 검토를 적절히 결합하고, 평가 기준을 가능한 한 구체적으로 작성해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;결국 모델 평가는 &amp;ldquo;이 모델이 좋은가?&amp;rdquo;라는 막연한 질문보다, &lt;b&gt;&amp;ldquo;이 모델이 내가 사용하려는 상황에서 필요한 조건을 충족하는가?&amp;rdquo;&lt;/b&gt;를 확인하는 과정에 가깝다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파인튜닝 방법보다 먼저 배워야 할 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책을 읽기 전에는 파인튜닝을 특정 라이브러리와 코드를 사용해 모델을 학습시키는 기술로 생각했다. 책을 다 읽고 난 뒤에는 파인튜닝을 다음과 같은 전체 과정으로 보게 되었다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;모델을 어디에 사용할 것인지 정의한다.&lt;/li&gt;
&lt;li&gt;모델이 수행해야 하는 작업과 출력 형식을 정한다.&lt;/li&gt;
&lt;li&gt;목적에 맞는 데이터를 수집하고 정제한다.&lt;/li&gt;
&lt;li&gt;학습&amp;middot;검증&amp;middot;테스트 데이터를 분리한다.&lt;/li&gt;
&lt;li&gt;전체 파인튜닝, LoRA, 프롬프트 설계 등 적절한 방법을 선택한다.&lt;/li&gt;
&lt;li&gt;실제 사용 환경을 반영한 평가 기준을 만든다.&lt;/li&gt;
&lt;li&gt;오류를 분석하고 데이터와 학습 방식을 수정한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;앞으로는 파인튜닝 코드를 하나하나 직접 작성하는 일보다 인공지능의 도움을 받아 학습 환경을 구성하는 경우가 많을 것 같다. 하지만 인공지능이 코드를 대신 작성해 준다고 해서 사용자가 알아야 할 것이 줄어드는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;오히려 어떤 데이터를 사용해야 하는지, 데이터에 문제가 없는지, 모델의 결과를 무엇으로 평가할지, 과적합이나 편향이 발생하지 않았는지를 판단하는 능력이 더 중요해질 수 있다. 이러한 기준을 모르면 인공지능에게 무엇을 지시해야 할지조차 결정하기 어렵기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;도구가 코드를 대신 작성해 줄수록 사람은 모델 학습의 목적과 조건을 더 정확하게 정의해야 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 프로젝트에서 바뀔 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이번 독서를 통해 다음 프로젝트에서는 모델을 실행하기 전의 설계 과정에 더 많은 시간을 사용해야겠다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;첫째, 모델이나 API를 선택하기 전에 해결하려는 문제부터 명확히 정의할 것이다. 무엇을 생성해야 하는지뿐 아니라, 좋은 결과와 나쁜 결과를 구분하는 기준도 먼저 정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;둘째, 데이터셋을 단순한 입력 파일로 다루지 않을 것이다. 데이터의 출처, 품질, 중복, 분포, 라벨의 일관성을 확인하고 왜 이 데이터가 필요한지를 기록해 두고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;셋째, 결과가 좋지 않을 때 프롬프트부터 반복해서 수정하지 않을 것이다. 데이터 문제인지, 모델 문제인지, 생성 설정 문제인지, 평가 방식의 문제인지 나누어 살펴봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;넷째, 학습하기 전에 평가 방법을 정할 것이다. 모델을 만든 뒤 결과를 보고 유리한 평가 기준을 고르는 것이 아니라, 목적에 맞는 성공 조건을 먼저 정의해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;다섯째, 오류를 유형별로 기록할 것이다. 단순히 결과가 좋지 않다고 판단하는 데서 끝내지 않고 사실 오류, 형식 오류, 관련성 부족, 반복 생성처럼 문제를 구분하면 다음 수정 방향도 더 명확해질 것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한 권을 읽고 더 많아진 질문들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 책은 파인튜닝의 전체 과정을 폭넓게 보여주었지만, 한 번 읽는 것만으로 모든 개념을 이해하기는 어려웠다. 오히려 익숙한 용어를 정확히 알고 있지 않았다는 사실을 확인했고, 앞으로 공부할 질문이 더 많아졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 남아 있는 질문은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단어 하나를 수백 차원의 벡터로 표현한다는 것은 정확히 어떤 의미일까?&lt;/li&gt;
&lt;li&gt;임베딩 차원이 많아질수록 모델의 표현력은 항상 좋아질까?&lt;/li&gt;
&lt;li&gt;쿼리, 키, 밸류 행렬은 학습을 거치며 각각 무엇을 나타내게 될까?&lt;/li&gt;
&lt;li&gt;멀티헤드 어텐션의 각 헤드는 실제로 서로 다른 역할을 담당할까?&lt;/li&gt;
&lt;li&gt;피드포워드 네트워크의 비선형 변환은 표현을 어떻게 변화시킬까?&lt;/li&gt;
&lt;li&gt;역전파는 비선형 신경망의 가중치를 어떻게 조정할까?&lt;/li&gt;
&lt;li&gt;LoRA는 왜 작은 저차원 행렬만으로 모델의 행동을 바꿀 수 있을까?&lt;/li&gt;
&lt;li&gt;양자화로 줄어든 정밀도는 어떤 상황에서 실제 출력 오류로 나타날까?&lt;/li&gt;
&lt;li&gt;temperature가 높아져 생성 결과가 다양해지는 현상을 인공지능의 창의성이라고 볼 수 있을까?&lt;/li&gt;
&lt;li&gt;인간과 비슷한 언어를 생성하는 것을 넘어 AGI에 필요한 능력은 무엇일까?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 질문들은 이후 임베딩, 셀프 어텐션, LoRA와 QLoRA, 데이터셋 설계와 모델 평가에 관한 별도의 글로 정리해 보려고 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;『한 권으로 끝내는 실전 LLM 파인튜닝』은 모델을 직접 학습하는 방법뿐 아니라, 현대의 언어 모델이 어떤 기술적 흐름을 거쳐 등장했는지를 함께 보여준다. 다루는 범위가 넓어 모든 개념을 깊이 이해하기에는 추가적인 공부가 필요했지만, 파인튜닝의 전체 과정을 한 번 훑어보는 데 도움이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;무엇보다 이 책을 통해 파인튜닝은 단순히 모델의 가중치를 업데이트하는 작업이 아니라는 점을 알게 되었다. 문제를 정의하고, 데이터를 구성하고, 적절한 학습 방법을 선택하고, 결과를 평가하는 과정 전체가 파인튜닝에 포함된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;좋은 모델을 고르는 것만큼이나 모델이 무엇을 학습할지를 결정하는 일이 중요하다. 그리고 그것을 결정하는 것은 결국 데이터와 평가 기준이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책을 읽고 내가 얻은 가장 큰 결론은 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;파인튜닝은 모델을 학습시키는 코드에서 시작되는 것이 아니라, 모델에게 무엇을 학습시킬지 결정하는 데이터에서 시작된다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>독서 기록</category>
      <category>LLM</category>
      <category>강다솔</category>
      <category>독서기록</category>
      <category>파인튜닝</category>
      <category>한 권으로 끝내는 실전 LLM 파인튜닝</category>
      <author>와일</author>
      <guid isPermaLink="true">https://beta-log.tistory.com/54</guid>
      <comments>https://beta-log.tistory.com/54#entry54comment</comments>
      <pubDate>Thu, 16 Jul 2026 14:56:45 +0900</pubDate>
    </item>
    <item>
      <title>사랑이라기에는 너무 무거운 마음 - 용의자 X의 헌신, 히가시노 게이고</title>
      <link>https://beta-log.tistory.com/53</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 글에는 『용의자 X의 헌신』의 주요 내용과 결말에 대한 언급이 포함되어 있습니다.&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;380&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4YgY6/dJMcaa66L3m/LhHNku1aP3BeZQnFLzqRSk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4YgY6/dJMcaa66L3m/LhHNku1aP3BeZQnFLzqRSk/img.jpg&quot; data-alt=&quot;용의자 X의 헌신 표지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4YgY6/dJMcaa66L3m/LhHNku1aP3BeZQnFLzqRSk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4YgY6%2FdJMcaa66L3m%2FLhHNku1aP3BeZQnFLzqRSk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;380&quot; height=&quot;540&quot; data-origin-width=&quot;380&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;용의자 X의 헌신 표지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다들 재밌다고 해서 기대했던 소설이다. 만약 아직 읽지 않았다면 책을 읽고 난 후 이 글을 읽는 것을 추천드린다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;처음에는 이시가미의 선택이 이해되지 않았다. 그는 논리를 중시하는 사람이었고, 감정보다 이성에 가까운 인물처럼 보였다. 그런데 자신과 깊은 관계를 맺은 것도 아닌 여성을 위해 범죄를 설계하고, 아무 죄 없는 사람까지 죽였다는 사실은 쉽게 납득하기 어려웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;야스코를 향한 감정도 처음에는 일반적인 호감처럼 보였다. 매일 도시락 가게를 찾아가고, 얼굴을 바라보는 것만으로 만족하는 사랑이었다. 그런 감정만으로 자신의 인생을 모두 내놓을 수 있을까. 그렇게 논리적인 사람이 왜 이토록 비논리적인 선택을 했을까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이시가미의 시점이 드러난 뒤에야 그의 행동을 조금은 이해할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그가 죽으려던 날, 옆집으로 이사 온 야스코와 미사토가 인사를 하러 왔다. 두 사람의 맑고 깨끗한 눈을 본 순간, 이시가미는 다시 살아갈 의지를 얻는다. 야스코 모녀는 그에게 특별한 말을 건네거나 의도적으로 그를 구한 것이 아니었다. 그저 존재하는 것만으로 이시가미를 죽음에서 삶으로 돌려놓았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 사실을 알고 나니 그의 감정은 단순한 남녀 간의 사랑과는 다르게 느껴졌다.&amp;nbsp;이시가미에게 야스코 모녀는 좋아하는 사람을 넘어, 자신의 삶이 다시 시작된 이유였다. 그들에게 사랑받거나 관계를 맺고 싶다는 욕망보다, 그들로 인해 다시 얻게 된 삶을 그들을 위해 사용하고 싶다는 마음에 가까웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;어차피 그들 덕분에 다시 살게 된 삶이니, 그 삶을 다시 그들에게 돌려주겠다는 선택. 나는 그 선택에 공감하지는 못했지만, 그가 왜 그런 선택을 했는지는 이해할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그렇다고 이시가미의 행동을 완전히 이타적인 사랑이라고 보기는 어려웠다.&amp;nbsp;그가 지키고 싶었던 것은 야스코 모녀이기도 했지만, 동시에 그들로 인해 다시 살아가게 된 자신의 삶의 의미이기도 했다. 그들을 위해 희생함으로써 자신이 다시 살아난 이유를 완성하려 했던 것은 아닐까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;수학은 분명 이시가미에게 중요한 세계였다. 그는 수학을 사랑했고, 뛰어난 재능도 가지고 있었다. 그러나 현실적인 환경 때문에 원하는 방식으로 수학을 계속할 수 없었다. 수학은 그의 삶을 지탱하는 중요한 부분이었지만, 끝내 그를 죽음에서 되돌린 것은 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그를 다시 살게 만든 것은 야스코 모녀였다.&amp;nbsp;그래서 이시가미의 희생은 사랑이면서도 자기완성처럼 느껴졌다. 그는 모녀 덕분에 다시 시작된 자신의 삶을 그들을 지키는 데 사용함으로써, 자신의 인생에 하나의 완결된 구조를 만들려고 했던 것 같다.&amp;nbsp;이시가미가 수학 문제를 풀듯 범죄를 설계한 것도 이와 연결되어 보였다. 그는 모든 변수를 계산하고, 다른 사람들의 행동까지 예측하며, 자신의 계획을 완벽하게 완성하려 했다. 자신의 삶마저 하나의 문제처럼 설계하고 끝까지 풀이하려 했던 셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;하지만 그 감정을 순수한 헌신이라고만 보기도 어려웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이시가미는 자신의 감정을 직접 드러내거나 야스코에게 사랑을 요구하지 않았다. 겉으로는 대가를 바라지 않는 사람처럼 보인다. 그러나 표현하지 않았다고 해서 그 감정이 집착이 아니었던 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그는 야스코를 자신의 삶의 의미로 삼았다. 상대가 원했는지와 관계없이 한 사람을 자신의 존재 이유로 만들었고, 그 의미를 지키기 위해 주변의 모든 상황을 통제하려 했다. 이시가미가 스토커처럼 보이기 위해 남겼던 말과 행동들도 전부 연기였을까 하는 생각이 들었다. 범죄 계획을 위한 장치였던 것은 분명하지만, 그 안에 그의 진심이 전혀 섞여 있지 않았다고 단정하기는 어렵다.&amp;nbsp;야스코를 감시하고, 그녀의 인간관계와 행동을 파악하고, 자신이 만든 계획 안에서 움직이게 하려는 모습은 사랑이라기보다 집착에 가까워 보이기도 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;무엇보다 이시가미는 야스코가 어떤 선택을 원하는지 묻지 않았다. 그는 야스코를 지키기 위해 모든 것을 계획했지만, 그 결과 야스코에게 평생 감당해야 할 죄책감을 남겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그의 행동에는 분명 희생이 있었지만, 그 희생은 상대가 요청한 것이 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 나는 이시가미를 불쌍하게 생각하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그가 외로운 삶을 살았고, 원하는 방식으로 수학을 계속하지 못했으며, 야스코 모녀를 통해 삶의 의미를 되찾았다는 사실은 이해한다. 그러나 그것이 무고한 사람을 죽인 행동까지 정당화할 수는 없다. 그의 내면을 알고 나서도 그를 동정하게 되지는 않았다. 다만 처음에는 이해할 수 없었던 행동이 어떤 논리에서 시작되었는지를 알게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공감하지는 않지만 이해할 수 있는 사람.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;나에게 이시가미는 그런 인물로 남았다. 그의 마음을 단순히 사랑이라고 부르기에는 너무 많은 것이 섞여 있었다. 감사와 구원, 집착과 희생, 그리고 자신의 삶을 완성하고 싶다는 욕망까지. 그래서 이시가미의 마음은 사랑이라기에는 너무 무거웠다.&lt;/p&gt;</description>
      <category>독서 기록</category>
      <category>독서기록</category>
      <category>사랑</category>
      <category>용의자X의헌신</category>
      <category>추리소설</category>
      <category>히가시노게이고</category>
      <author>와일</author>
      <guid isPermaLink="true">https://beta-log.tistory.com/53</guid>
      <comments>https://beta-log.tistory.com/53#entry53comment</comments>
      <pubDate>Sun, 12 Jul 2026 08:10:34 +0900</pubDate>
    </item>
    <item>
      <title>나는 무엇에 설득당하며 살고 있을까 - 다크 넛지, 로라 도즈워스&amp;middot;패트릭 페이건</title>
      <link>https://beta-log.tistory.com/52</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;365&quot; data-origin-height=&quot;547&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cYj1xB/dJMcacKBFnZ/amu6JLMKWE0GYcREoKKEjK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cYj1xB/dJMcacKBFnZ/amu6JLMKWE0GYcREoKKEjK/img.jpg&quot; data-alt=&quot;로라 도즈워스&amp;amp;middot;패트릭 페이건, 『다크 넛지: 치밀하고 은밀한 알고리즘의 심리 조작』&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cYj1xB/dJMcacKBFnZ/amu6JLMKWE0GYcREoKKEjK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcYj1xB%2FdJMcacKBFnZ%2Famu6JLMKWE0GYcREoKKEjK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;365&quot; height=&quot;547&quot; data-origin-width=&quot;365&quot; data-origin-height=&quot;547&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;로라 도즈워스&amp;middot;패트릭 페이건, 『다크 넛지: 치밀하고 은밀한 알고리즘의 심리 조작』&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 책을 처음 읽기 전에는 제목 그대로 알고리즘이나 소셜 미디어가 사람을 어떻게 조종하는지에 관한 책이라고 생각했다. 그런데 읽다 보니 이 책은 단순히 알고리즘의 문제만을 다루는 책은 아니었다. 우리가 매일 접하는 정보, 이미지, 언어, 공간, 집단 분위기, 심지어 배고픔이나 피곤함 같은 몸의 상태까지도 우리의 선택에 영향을 준다는 이야기였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책을 읽으면서 가장 많이 들었던 생각은 이것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp;나는 완전히 자유롭게 선택한다고 생각했지만, 사실 내 선택은 수많은 환경과 정보와 감정의 영향을 받고 있었다. 그래서 이제는 아무 영향도 받지 않는 사람이 되기보다, 내가 어떤 영향 속에 머물 것인지를 더 신중하게 고르고 싶다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;우리는 생각보다 쉽게 조작된다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;책의 초반부에서는 모든 형태의 의사소통이 어떤 식으로든 우리를 설득하기 위해 고안되었다고 말한다. 이 문장이 인상 깊었다. 보통 조작이나 선전이라고 하면 아주 노골적이고 악의적인 무언가를 떠올리게 된다. 하지만 이 책은 설득이 훨씬 일상적인 곳에 숨어 있다고 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;광고, 뉴스, 검색 결과, 추천 알고리즘, 누군가의 말투, 집단의 분위기까지 모두 우리의 생각과 행동에 영향을 줄 수 있다. 특히 무서운 것은 그 영향이 한 번에 크게 밀려오는 것이 아니라, 아주 작은 변화로 조금씩 다가올 때 알아차리기 어렵다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;책에서는 조작 가능성이 큰 상황에서 평소보다 더 냉소적이고 회의적인 태도를 취하라고 말한다. 처음에는 이 표현이 조금 강하게 느껴졌다. 냉소적이고 부정적인 태도가 항상 좋은 것은 아니라고 생각했기 때문이다. 하지만 책을 읽다 보니 모든 상황에서 부정적이 되라는 의미라기보다, 무언가가 나를 너무 쉽게 설득하려고 할 때 한 번 멈춰 서야 한다는 의미로 받아들이게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;특히 중요한 결정을 내릴 때 내 상태를 먼저 점검해야 한다는 부분이 기억에 남았다. 책에서는 배고픔(hungry), 분노나 불안(angry), 외로움(lonely), 피곤함(tired)을 뜻하는 HALT를 언급한다. 이런 상태에 있을 때 사람은 유혹과 암시에 더 약해질 수 있다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;생각해보면 중요한 결정을 할 때 나는 상황을 고려하지 않고 늘 &amp;ldquo;무엇을 선택할까&amp;rdquo;에 집중했다. 그런데 이 책을 읽고 나서는 그보다 먼저 &amp;ldquo;지금 내가 선택해도 되는 상태인가&amp;rdquo;를 자문해야겠다는 생각이 들었다. 배고프거나, 불안하거나, 피곤하거나, 외로운 상태에서 내린 결정은 내가 진짜 원해서 한 선택이 아닐 수도 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;선택지는 정말 두 개뿐일까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;책에서 또 인상 깊었던 부분은 선택권에 대한 이야기였다. 조작자는 우리가 스스로 선택하고 있다고 느끼게 만들면서도, 실제로는 우리에게 불리한 방향으로 상황을 설계할 수 있다고 한다. &amp;ldquo;선택은 여러분이 하는 겁니다&amp;rdquo;라는 말조차 순응을 유도하는 기술이 될 수 있다는 내용이 흥미로웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;나는 선택지가 주어지면 그중에서 무엇이 나은지 고르는 데 익숙하다. 하지만 책을 읽고 나니, 애초에 그 선택지가 누가 만든 것인지도 생각해야겠다는 마음이 들었다. 눈앞에 놓인 두 가지 선택지가 전부인 것처럼 보여도, 실제로는 그 밖에 다른 선택지가 있을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 부분은 일상에서도 자주 떠올릴 수 있을 것 같다. 어떤 문제를 마주했을 때 &amp;ldquo;A를 할까, B를 할까&amp;rdquo;만 고민하기보다, &amp;ldquo;정말 선택지가 A와 B뿐인가?&amp;rdquo;를 물어보는 것. 이것만으로도 꽤 많은 조작과 압박에서 벗어날 수 있을 것 같다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;환경은 나를 조종하지만, 나도 환경을 설계할 수 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;5장에서 감각과 환경에 대해 다룬 부분도 인상 깊었다. 게임 아케이드나 카지노는 사람들이 그곳에 오래 머물도록 감각을 설계한 공간이라고 한다. 빛, 소리, 촉감, 냄새, 공간의 구조 같은 것들이 모두 사람의 행동에 영향을 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 부분을 읽으면서 무섭다는 생각도 들었지만, 동시에 반대 방향의 가능성도 떠올랐다. 누군가가 환경을 설계해서 나를 소비하게 만들고, 충동적으로 행동하게 만들 수 있다면, 나 역시 내 환경을 설계해서 나를 더 나은 방향으로 유도할 수 있지 않을까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;책에는 시각적으로 어수선한 환경이 의사결정의 오류를 일으킬 수 있다는 내용도 나온다. 이 문장을 읽고 바로 방 정리와 책상 정리가 떠올랐다. 방이 어수선하면 마음도 같이 어수선해지는 느낌이 있는데, 그것이 단순한 기분 탓만은 아닐 수 있겠다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;결국 좋은 루틴은 대단한 의지력에서만 나오는 것이 아니라, 나를 덜 흔들리게 하는 환경에서 시작될 수도 있다. 침대를 정리하고, 책상 위를 비우고, 스마트폰을 손이 닿지 않는 곳에 두고, 읽고 싶은 책을 잘 보이는 곳에 두는 것. 이런 사소한 배치가 나에게 거는 좋은 넛지가 될 수 있겠다고 느꼈다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생각을 알고리즘에 맡기지 않기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;소셜 미디어와 알고리즘에 대한 장은 가장 현실적으로 다가왔다. 책에서는 우리가 &amp;ldquo;주머니 속의 뇌&amp;rdquo;에 생각을 아웃소싱하고 있다고 말한다. 스스로 생각하지 않으면 알고리즘이 우리 대신 생각하게 된다는 문장이 특히 기억에 남았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;요즘은 모르는 것이 있으면 바로 검색하고, 알고리즘이 추천해주는 콘텐츠를 보고, 인공지능 챗봇에게 질문한다. 이것들은 분명 편리하다. 나도 자주 도움을 받는다. 하지만 편리함에 익숙해질수록 내가 직접 생각하는 과정이 줄어들 수 있다는 점은 경계해야겠다고 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;AI를 활용하는 것과 AI에게 생각을 맡기는 것은 다르다. 내가 생각할 수 있는 능력을 기르면서 더 빠르고 정확하게 판단하기 위해 AI를 사용하는 것은 도움이 된다. 하지만 &amp;ldquo;그냥 해줘&amp;rdquo;에 익숙해져서 문제를 해석하고, 질문을 만들고, 내 의견을 세우는 과정까지 전부 넘겨버린다면 그것은 위험할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;블로그에 대해서도 잠깐 생각하게 되었다. 블로그는 나를 정리하는 기록이지만, 동시에 온라인에 남는 디지털 발자국이기도 하다. 기록하는 일은 분명 의미 있지만, 무엇을 쓰는지뿐 아니라 어떤 거리감을 두고 쓰는지도 중요하겠다고 느꼈다. 모든 것을 즉각적으로 공유하기보다, 충분히 생각하고 정리한 뒤 남기는 기록이 더 오래 갈 수 있을 것 같다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데이터와 AI를 믿는 사람에게 필요한 회의감&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 책에서 &amp;ldquo;데이터교&amp;rdquo;라는 표현도 기억에 남았다. 데이터는 사실의 축적처럼 보인다. 그래서 나 역시 데이터를 감정보다 더 객관적이고 신뢰할 만한 것으로 받아들이는 편이었다. 하지만 책을 읽고 나니 데이터 역시 누가, 어떤 목적으로, 어떤 방식으로 수집하고 해석하느냐에 따라 사람을 설득하거나 조종하는 도구가 될 수 있다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;특히 적은 양의 데이터만으로도 사람의 특성을 꽤 정확하게 예측할 수 있다는 내용은 흥미로우면서도 무서웠다. 데이터의 활용 가능성이 무궁무진하다는 뜻이기도 하지만, 동시에 개인이 얼마나 쉽게 분석되고 분류될 수 있는지를 보여주는 말이기도 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;나는 앞으로도 데이터와 AI에 관심을 가지고 공부하고 싶다. 그렇기 때문에 오히려 더 회의적인 태도가 필요하다는 생각이 들었다. 기술을 잘 활용하는 것만큼이나, 그 기술이 사람의 선택과 자유에 어떤 영향을 미치는지 생각하는 것도 중요하다. 개발이나 연구를 할 때 편리함과 효율성만 볼 것이 아니라, 그것이 사람을 어떤 방향으로 유도하는지도 함께 고민해야 한다. 윤리적인 생각은 항상 기저에 있어야 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결국 내가 선택해야 하는 환상&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;책의 후반부에서는 우리가 모두 어떤 식으로든 세뇌되어 있을 가능성이 크다고 말한다. 처음에는 꽤 불편한 문장이었다. 하지만 생각해보면 우리의 생각과 습관은 대부분 주변 환경, 사람, 콘텐츠, 언어, 사회 분위기의 영향을 받아 만들어진다. 완전히 독립적이고 순수한 나만의 생각이라는 것이 과연 얼마나 있을까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그렇다고 해서 모든 것이 무의미하다는 뜻은 아니다. 책에서는 우리가 자신이 주의를 기울인 대상이 된다고 말한다. 이 문장이 오래 남았다. 내가 자주 보는 것, 오래 듣는 것, 반복해서 생각하는 것이 결국 나를 만든다면, 나는 내가 어떤 것에 주의를 기울일지 선택해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;결국 이 책을 읽고 얻은 결론은 &amp;ldquo;아무 영향도 받지 않는 사람이 되자&amp;rdquo;가 아니었다. 그것은 거의 불가능에 가깝다. 우리는 모두 무언가의 영향을 받으며 살아간다. 중요한 것은 내가 어떤 영향 속에 머물고 있는지 알아차리는 일이다. 그리고 가능하다면 나를 더 건강하고, 단단하고, 자유롭게 만드는 영향 쪽으로 조금씩 이동하는 일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;『다크 넛지』는 조금 경계심을 갖게 만드는 책이었다. 세상을 전부 의심하자는 의미는 아니지만, 내가 당연하게 받아들이던 것들을 다시 보게 만들었다. 내가 보고 있는 정보, 머무는 공간, 사용하는 플랫폼, 자주 듣는 말, 반복해서 하는 생각이 나를 만들고 있다면, 앞으로는 그것들을 조금 더 신중하게 고르고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완전히 자유로운 선택은 어려울지 모른다. 그래도 내가 무엇에 설득당하며 살고 있는지 알아차리려는 노력은 할 수 있다. 그 알아차림이야말로 다크 넛지 속에서 내 생각을 지키는 첫걸음일지도 모른다.&lt;/p&gt;</description>
      <category>독서 기록</category>
      <category>AI윤리</category>
      <category>다크넛지</category>
      <category>독서기록</category>
      <category>심리조작</category>
      <category>알고리즘</category>
      <author>와일</author>
      <guid isPermaLink="true">https://beta-log.tistory.com/52</guid>
      <comments>https://beta-log.tistory.com/52#entry52comment</comments>
      <pubDate>Fri, 10 Jul 2026 08:08:47 +0900</pubDate>
    </item>
    <item>
      <title>자료구조를 마치며: 모든 자료구조는 무언가를 포기한다</title>
      <link>https://beta-log.tistory.com/51</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;135ms와 3일. 처음 자료구조를 시작하면서 봤던 숫자다. 같은 결과를 내는 프로그램이 어떤 자료구조를 쓰느냐에 따라 그만큼 차이가 난다는 것. 그때는 그 차이가 단순히 &quot;좋은 자료구조를 쓰느냐 마느냐&quot;의 문제인 줄 알았다. 빠른 걸 고르면 되는 거 아닌가.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배워보니 그렇게 단순하지 않았다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;빠른 게 전부가 아니었다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;처음엔 단순하게 생각했다. 빠른 자료구조를 쓰면 되는 거 아닌가.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 배열부터 막혔다. 정렬된 배열은 이진 탐색으로 O(log n)에 원소를 찾을 수 있다. 빠르다. 근데 삽입은? 자리를 만들기 위해 원소들을 한 칸씩 밀어야 하니까 O(n)이다. 반대로 정렬을 포기하면 삽입은 O(1)인데, 이번엔 탐색이 O(n)이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Linked List도 비슷했다. 삽입과 삭제는 포인터만 바꾸면 되니까 빠른데, 인덱스로 바로 접근하는 건 불가능하다. 배열은 반대고.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;빠르다&quot;는 말이 생각보다 복잡한 말이었다. 어떤 연산에 대해 빠른지를 따져야 한다. 그리고 한쪽이 빠르면 다른 쪽이 느려지는 경우가 대부분이었다. 자료구조를 선택한다는 건, 어떤 연산을 포기할지 결정하는 일이었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구조가 달라지면 가능한 것도 달라진다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;스택과 큐를 배울 때까지만 해도 자료구조가 선형이라는 걸 당연하게 생각했다. 앞이 있고 뒤가 있고, 그 사이 어딘가에 데이터가 있는 구조.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;트리를 보는 순간 그 생각이 깨졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;트리는 계층적이다. 부모가 있고, 자식이 있고, 그 자식에 또 자식이 있다. 이 구조 덕분에 &quot;다음으로 중요한 게 뭔지&quot;를 선형 탐색 없이 알아낼 수 있었다. Priority Queue에서 heap을 쓰면 최솟값을 O(1)에 꺼낼 수 있다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;선형 구조에선 O(n)이었던 게&lt;/span&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;O(log n)이 가능한 이유도 트리였다. 탐색할 때마다 절반씩 후보를 줄여나가는 것 - 이건 데이터를 선형으로 쌓는 구조에선 그냥 안 된다. 어떻게 데이터를 &quot;생각하느냐&quot;가 성능을 결정한다는 걸 처음 체감한 파트였다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;O(1)의 조건&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Hash Table을 처음 봤을 때 솔직히 믿기지 않았다. 삽입, 탐색, 삭제가 전부 O(1)?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 조건이 있었다. hash function이 좋아야 하고, load factor를 적절히 유지해야 하고, 충돌을 처리해야 한다. 충돌을 separate chaining으로 처리하면 최악의 경우 O(n)까지 떨어진다. 그리고 hash table은 O(n)의 공간을 항상 들고 다닌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;O(1)은 공짜가 아니었다. 공간을 미리 쓰는 것, 충돌 가능성을 감수하는 것, hash function 설계에 신경을 써야 하는 것 &amp;mdash; 이 조건들을 맞춰줘야 O(1)이 가능하다. 그리고 그 조건들이 깨지는 순간, 보장은 사라진다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;균형이라는 강박&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Search Tree 파트가 이 과목에서 가장 힘들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;BST는 아이디어 자체는 단순하다. 왼쪽엔 작은 것, 오른쪽엔 큰 것. 탐색, 삽입, 삭제가 전부 O(h), 여기서 h는 트리의 높이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 h가 보장이 안 된다는 거였다. 이미 정렬된 데이터를 순서대로 삽입하면 BST는 그냥 한 줄로 늘어진 체인이 된다. h = n이고, O(n)이다. 최악의 경우 선형 탐색이랑 다를 게 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그래서 AVL 트리가 나왔다. 삽입하거나 삭제할 때마다 balance factor를 확인하고, 균형이 깨지면 rotation으로 맞춘다. h가 항상 O(log n) 안에 있도록 강제하는 것이다. 근데 AVL은 삭제 후 restructuring이 여러 번 일어날 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그래서 (2,4) 트리가 나왔고, Red-Black 트리가 나왔다. 각각의 단점을 보완하면서, 또 새로운 복잡도를 들고 왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;BST에서 AVL, 거기서 Red-Black까지 오는 과정이 이 과목 전체의 흐름을 압축해놓은 것 같았다. 더 나은 보장을 얻으려면 더 복잡한 구조를 감수해야 한다. 그 과정이 한 번에 끝나지 않는다는 것도.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;알고리즘이 자료구조를 만난다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Graph와 Sorting은 &quot;자료구조 위에서 알고리즘이 돌아간다&quot;는 느낌이 처음으로 선명하게 왔던 파트였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그래프는 지금까지 배운 거의 모든 것의 일반화였다. 트리도 그래프고, 연결 리스트도 그래프다. 거기에 탐색 알고리즘(DFS, BFS), 최단 경로(Dijkstra), 최소 비용 연결(MST)이 올라갔다. Dijkstra는 Prim's와 구조가 거의 같은데 재는 값만 달랐다. Union-Find는 Kruskal's가 &quot;같은 그룹인지 확인하는&quot; 연산을 O(log n)에 해결하기 위해 만들어진 자료구조였다. 문제가 자료구조를 요구하고, 자료구조가 알고리즘을 가능하게 한다는 게 여기서 가장 잘 보였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Sorting은 조금 달랐다. Heap-Sort, Merge-Sort, Quick-Sort &amp;mdash; 전부 O(n log n)인데, 어디서 비용을 지불하는지가 달랐다. Heap-Sort는 추가 공간이 필요 없지만 stable하지 않다. Merge-Sort는 항상 O(n log n)이 보장되지만 O(n)의 공간을 쓴다. Quick-Sort는 평균적으로 빠르지만 최악의 경우 O(n^2)로 떨어진다. 같은 시간복잡도 안에서도 선택지가 있었고, 상황에 따라 뭘 포기할지가 달랐다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;한 학기 동안 배운 자료구조들을 돌아보면 공통된 패턴이 보인다. 어떤 자료구조도 모든 연산에서 최선일 수 없다. 빠른 탐색을 원하면 공간을 더 쓰거나 삽입이 느려지고, 공간을 아끼려면 탐색이 느려지거나 보장이 사라진다. 균형을 맞추려면 구조가 복잡해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 그게 한계처럼 보였는데, 지금은 조금 다르게 느껴진다. 어떤 연산이 중요한지를 알면, 그에 맞는 자료구조를 고를 수 있다. 그 선택을 제대로 하려면 각 자료구조가 무엇을 포기했는지를 알아야 한다.&lt;/p&gt;</description>
      <category>공부 기록/자료구조</category>
      <category>마무리</category>
      <category>인공지능</category>
      <category>자료구조</category>
      <category>종강</category>
      <category>컴퓨터</category>
      <author>와일</author>
      <guid isPermaLink="true">https://beta-log.tistory.com/51</guid>
      <comments>https://beta-log.tistory.com/51#entry51comment</comments>
      <pubDate>Mon, 29 Jun 2026 08:32:11 +0900</pubDate>
    </item>
    <item>
      <title>[HRD 쪽글 모음 #5] 나는 어떻게 살아야 하는가 - 인적자원개발론 한 학기를 마치며</title>
      <link>https://beta-log.tistory.com/50</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;한 학기 내내 나는 수업을 들으면서 교수님께 질문을 던졌는데, 돌이켜보면 그 질문들은 전부 같은 뿌리에서 나왔다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;나는 어떻게 살아야 하지?&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;인적자원개발론이라는 과목을 선택한 건 솔직히 말하면 가벼운 마음이었다. 컴퓨터를 전공하는 내가 교육학 과목을 듣는 건 약간 이색적인 조합이었고, 처음엔 그냥 새로운 분야를 맛보는 정도로 생각했다. 그런데 수업이 쌓일수록 이 과목이 나한테 묻고 있다는 걸 느꼈다. 조직 안에서 사람을 어떻게 개발할 것인가라는 질문이, 어느 순간 나 자신을 어떻게 개발할 것인가라는 질문으로 바뀌어 있었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기능주의자로 자란 사람&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;수업 첫 주에 교수님이 세 가지 관점을 소개했다. 기능주의, 해석주의, 비판주의. 기능주의는 쉽게 말하면 기계론적 세계관이다. 전체는 부분으로 나뉘고, 부분을 잘 조합하면 전체가 된다는 생각. 효율, 성과, 측정 가능한 결과물. 교수님은 이렇게 말씀하셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&quot;컴퓨터공학도 어떻게 보면 기능론적 관점을 가지고 있는 학문 분야다.&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그전에는 학문 분야에서의 관점을 생각해 본 적이 없었다. 그냥 문제가 있으면 그 문제를 해결하고, 효율적인 방법은 무엇일까 고민하고 이런 게 당연했다. 듣고 되돌아보니 맞는 말이었다. 이공계의 핵심은 통제라고 했는데, 나는 자연스럽게 그 사고방식 안에서 자라왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그런데 해석주의는 달랐다. 지식은 사람 머릿속에 있는 게 아니라 관계 사이에 있다는 관점. 똑같은 현상도 사람마다 다르게 구성한다는 생각. 교수님은 자신이 해석주의자라고 했다. 현실과 동떨어져 있다는 비판을 많이 받지만, 그게 인간을 인간으로 바라보는 가장 본질적인 관점이라고.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;나는 이 수업을 들으면서 나도 모르게 내가 얼마나 세상을 기능주의적으로 봐왔는지 깨달았다. 스펙을 쌓는 것, 역량을 측정하는 것, 효율적으로 성장하는 것. 나는 나 자신을 자원으로 개발하고 있었다. 그게 나쁜 건 아닌데, 그것만이 전부는 아니었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&quot;개발자가 되면 어떻게 되는 거지?&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;학년이 올라가면서 이 질문이 머릿속에서 떠나질 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;컴퓨터를 전공한다고 하면 다들 자연스럽게 개발자 프레임을 씌운다. 나도 어느 순간 그 프레임을 받아들이고 있었는데, 문제는 AI가 너무 빠르게 발전하고 있다는 거였다. Claude한테 시키면 뚝딱 나오는 코드들, 내가 몇 시간씩 고민하던 것들을 몇 초 만에 해결하는 모델들. 이런 걸 보면서 자꾸 이런 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;내 전공의 장점이 뭐지? 인공지능이 다 해줄 수 있는데? 내 강점은 어디서 나오는 거지?&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;수업에서 교수님이 이런 말씀을 하셨다. AI 시대에 중요한 건 인공지능 자체를 배우는 게 아니라, 그 해당 도메인의 전문성이라고. 인공지능 기술은 평준화되지만 도메인 전문성은 대체되지 않는다고. 그리고 아마존의 채용 AI가 편향된 데이터를 학습해서 특정 인종과 성별을 걸러냈던 사례를 말씀하시면서 이렇게 덧붙이셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp;&quot;그 문제가 생긴 이유는 HRD 전문가가 아니라 IT 전문가가 그걸 만들었기 때문이에요. HRD 전문가가 같이 얘기하면서 만들었으면 그 문제 발생하지 않아.&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 말이 오래 남았다. 기술을 만드는 사람과 그 기술이 작동하는 맥락을 아는 사람. 둘 다 필요하다는 얘기였다. 그러면서 교수님이 나한테 직접 말씀해주셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;컴퓨터공학 전공은 세상에 무언가를 내놓기에 장벽이 낮은 게 큰 장점이라고. 아이디어가 생겼을 때 그걸 실제로 만들어서 세상에 내보일 수 있는 능력. 그게 다른 전공에 비해 훨씬 빠르다고. 잘한 선택이라고.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그 말을 듣고 나서 조금 숨이 트였다. 나는 계속 내가 대체될 것을 걱정했는데, 사실 내가 가진 도구가 더 강력해진 것일 수도 있다는 생각이 들었다. 도메인 지식을 쌓으면서, 그걸 실제로 구현할 수 있는 능력을 함께 가진 사람. 그게 내가 될 수 있는 방향이었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;경력 개발이라는 말의 무게&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 수업에서 개인 개발, 경력 개발, 조직 개발을 구분해서 배웠다. 그중에서 경력 개발 파트가 유독 마음에 걸렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교수님이 우리한테 물었다. 고등학교 때 지금 이 자리에 오게 된 데 영향을 미친 게 뭐였냐고. 학원 선생님, 부모님, 주변 친구들, 담임 선생님. 각자 다른 대답이 나왔는데, 교수님이 정리하셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&quot;지금까지의 내가 이뤄온 경력이 단순히 내 노력인가라고 한번 곰곰이 생각해봅시다.&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;인간의 선택은 생각보다 훨씬 많이 외부 요소에 의존한다는 얘기였다. 주변 환경, 만나는 사람들, 어떤 순간에 어떤 말을 들었는가. 경력 개발에서 멘토링이 핵심적인 이유가 이거였다. 내가 뭔가를 잘한다고 기회가 오는 게 아니라, 나를 알아보는 사람이 기회를 열어주는 구조.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;비판적 관점에서 보면 이게 불공정하다. 맞다. 하지만 그렇다고 그 구조를 외면하면 아무것도 달라지지 않는다. 오히려 그 구조를 알고 있는 사람이 더 잘 헤쳐나간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그래서 교수님이 강조했던 게 다양한 사람을 만나고, 다양한 환경에 자신을 노출시키라는 거였다. 내가 되고 싶은 모습이 있다면 그 환경 안에 자신을 갖다 놓으라고. 맹자 어머니가 세 번 이사한 게 그냥 이야기가 아니라고.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;링 위에 올라가서 얻어터져봐라&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;마지막 수업 즈음, 교수님이 이 말씀을 하셨다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;모든 건 써봐야 알아. 링에 가서 터져봐야 해.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;드라마 대사를 언급하면서였는데, 내용인즉슨 이랬다. 완벽하게 준비하고 나서 세상에 내보이려 하지 말라는 것. 일단 부딪히고, 피드백을 받고, 수정하고, 다시 내보내는 순환. 성공한 기업가들의 공통점이 처음에는 다 망했다는 거라고. 그런데 그냥 망하고 끝난 게 아니라, 망함으로써 피드백을 받고, 그걸 다시 교정해서 확산시켰다고.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이게 머리로는 알고 있는 말이었다. 근데 수업에서 계속 이 얘기가 나오니까, 내가 얼마나 링 바깥에서 관망하고 있었는지 보였다. 준비가 안되었으니까, 아직 이르니까, 이것만 보완하고 이런저런 핑계를 대며 미루고 있는 것들. 사실 얻어터지기 두려워서 미룬 것일 수도.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;교수님이 AI 리터러시에 대해 말씀하시면서도 같은 결론이 나왔다. 인공지능 강의를 듣는다고 역량이 생기는 게 아니라, 실제로 써봐야 하고, 목표를 잡아서 만들어봐야 하고, 그게 쌓여서 자산이 된다고. 그러려면 루틴이 필요하고, 루틴 안에서 반복이 필요하다고.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장인이 왜 자기 기술을 남한테 가르쳐주냐는 질문에 교수님이 하신 대답도 기억에 남는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&quot;손에 쥐고 있는 걸 놓고 다른 걸 집으러 가는 거야. 그러니까 자기도 성장하는 거죠.&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;기술을 공유하는 게 손해가 아니라 성장의 방식이라는 것. 내가 가진 걸 세상에 내놓을 때, 나는 그다음 것을 잡을 수 있다는 것.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;일의 의미라는 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 수업에서 가장 여러 번 강조된 주제는 결국 이거였다. 일에서 의미를 찾는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;교수님이 말씀하셨다. 인간의 전체 인생에서 절반은 일을 하면서 산다. 그 절반이 그냥 생존을 위한 소비라면, 나머지 절반만 행복을 추구하는 삶이 된다. 그러면 그 사람의 삶은 절반만 행복한 셈이라고. 그래서 일에서 의미를 찾는 게 단순한 이상주의가 아니라, 삶 전체의 행복과 직결되는 문제라고.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;돈이 많은 사람들이 삶의 끝에서 꼭 행복하지는 않다는 말도 하셨다. 200억을 갑자기 주고 일 관두고 놀라고 하면, 아이러니하게도 대부분은 일을 만들어낸다고. 인간은 본능적으로 일을 한다고. 그러니까 어떤 일을 하느냐보다 그 일에서 어떤 의미를 만드느냐가 더 중요하다고.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;교수님이 재미와 보람 얘기를 하셨는데, 술 먹고 필름 끊기는 건 재밌지만 아침에 일어나면 다음엔 이러지 말아야지 한다는 비유가 웃기면서 맞았다. 재미있다고 다 가치 있는 건 아니고, 보람이 함께 있어야 한다고. 열정을 가지고 뭔가를 해서 성취감을 느끼고, 돌아봤을 때 가치로웠다고 느끼는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그게 내가 만들어가야 하는 삶의 방향 같았다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한 학기가 지나고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;수업이 끝나는 날, 교수님께서 물었다. 이 수업을 듣고 뭘 가지고 돌아가느냐고.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 이 과목에서 HRD 이론을 배웠지만, 사실 그것보다 더 많은 걸 가지고 간다. 기능주의자로 자란 내가 해석주의적 관점을 잠깐이라도 몸에 얹어본 경험. 개발자 프레임에 갇혀 있다가 장벽이 낮다는 게 강점이 될 수 있다는 걸 들은 기억. 링 위에 올라가서 얻어터져봐야 한다는 말.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 어떻게 살아야 하지?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;아직도 잘 모른다. 그런데 이 질문을 계속 갖고 다니는 게 중요하다는 건 알겠다. 너무 거창하게 미래를 설계하려 하지 말고, 지금 할 수 있는 것을 루틴으로 만들고, 세상에 내놓고, 피드백 받고, 다시 만들고. 그 과정에서 조금씩 발견해가는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그리고 그 일이 나한테 의미 있으려면, 내가 거기에 시간과 노력을 충분히 들여야 한다는 것도. 의미는 처음부터 주어지는 게 아니라 만들어진다는 것도.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;한 학기 동안 좋은 질문들을 던져주시고 세상을 바라보는 지평을 넓혀주신 교수님께 진심으로 감사하다. 그리고 아직 링 바깥에 서 있는 나한테, 조금씩 올라가보자고 말하고 싶다.&lt;/p&gt;</description>
      <category>생각 정리</category>
      <category>HRD</category>
      <category>대학생</category>
      <category>인적자원개발</category>
      <category>종강</category>
      <category>진로고민</category>
      <author>와일</author>
      <guid isPermaLink="true">https://beta-log.tistory.com/50</guid>
      <comments>https://beta-log.tistory.com/50#entry50comment</comments>
      <pubDate>Sun, 28 Jun 2026 10:13:23 +0900</pubDate>
    </item>
    <item>
      <title>Sorting (3)</title>
      <link>https://beta-log.tistory.com/49</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;(1)에서 Selection/Insertion/Heap-Sort, (2)에서 Merge-Sort를 다뤘다. 이번 (3)은 Sorting 단원의 마지막으로, Quick-Sort와 Stable Sorting, 그리고 지금까지 다룬 5개 정렬 알고리즘의 전체 비교/정리를 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글의 흐름:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Quick-Sort: Merge-Sort와 정반대 구조(divide가 어렵고 conquer가 trivial)&lt;/li&gt;
&lt;li&gt;pivot 선택이 시간복잡도에 미치는 영향 - 최악의 경우 O(n^2)&lt;/li&gt;
&lt;li&gt;in-place partitioning&lt;/li&gt;
&lt;li&gt;Stable Sort의 정의와, 각 알고리즘의 stability&lt;/li&gt;
&lt;li&gt;5개 알고리즘 전체 비교표와 trade-off 정리&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Quick-Sort&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 Merge-Sort와 정반대 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Merge-Sort는 &quot;&lt;b&gt;Divide는 trivial(그냥 반으로), Conquer(merge)가 어려움&lt;/b&gt;&quot;이었다. Quick-Sort는 정반대다: &quot;&lt;b&gt;Divide(partition)가 어려운 일을 다 하고, Conquer는 trivial&lt;/b&gt;&quot;하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 알고리즘&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Pivot&lt;/b&gt; 하나를 고른다 (보통 마지막 원소)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Partition&lt;/b&gt;: 배열을 pivot보다 작은 그룹 / pivot / pivot보다 큰 그룹 세 부분으로 재배열한다. 이 과정에서 pivot은 자기 최종 위치에 자리잡는다&lt;/li&gt;
&lt;li&gt;작은 그룹과 큰 그룹을 각각 재귀적으로 Quick-Sort&lt;/li&gt;
&lt;li&gt;&lt;b&gt;합치는 과정이 따로 필요 없다&lt;/b&gt; - partition이 끝나면 &quot;작은 그룹 | pivot | 큰 그룹&quot; 순서가 이미 보장되므로, 양쪽이 각자 정렬되면 전체가 정렬된 것&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.3 예제: [5,2,8,1,9,3] (pivot = 마지막 원소)&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[5,2,8,1,9,3] pivot=3
  partition -&amp;gt; [2,1 | 3 | 5,8,9]
   /                          \
[2,1] pivot=1             [5,8,9] pivot=9
partition-&amp;gt;[1|2]          partition-&amp;gt;[5,8 | 9]
 /     \                       pivot=8
[1]    [2]                  partition-&amp;gt;[5|8]
                              /      \
                            [5]      [8]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 단계에서 partition은 그 레벨의 배열 크기만큼 한 번 훑으면서, pivot보다 작은/큰 값으로 나눈다. 최종 결과: [1,2,3,5,8,9]&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.4 Pivot 선택이 왜 중요한가 - 시간복잡도&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Merge-sort tree는 &lt;b&gt;항상 정확히 반씩 나뉘어서 height=O(log n)이 보장&lt;/b&gt;됐다. 그런데 Quick-sort tree의 모양은 &lt;b&gt;partition이 얼마나 균등하게 나누는지&lt;/b&gt;, 즉 pivot을 잘 고르느냐에 달려있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;균등하게 나뉘는 경우(평균/최선)&lt;/b&gt;: 매번 절반씩 나뉘면 height=O(log n) -&amp;gt; 전체 O(n log n)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최악의 경우&lt;/b&gt;: pivot이 매번 &quot;가장 작은 값&quot; 또는 &quot;가장 큰 값&quot;이라면, partition 결과가 &quot;한쪽은 0개, 다른 쪽은 나머지 전부&quot;가 된다&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;최악의 예: 이미 정렬된 배열 [1,2,3,4,5], pivot=마지막 원소&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[1,2,3,4,5] pivot=5 -&amp;gt; partition -&amp;gt; [1,2,3,4 | 5 | ]
  -&amp;gt; [1,2,3,4] pivot=4 -&amp;gt; [1,2,3 | 4 | ]
    -&amp;gt; [1,2,3] pivot=3 -&amp;gt; [1,2 | 3 | ]
      -&amp;gt; [1,2] pivot=2 -&amp;gt; [1 | 2 | ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;매 단계마다 &quot;큰 쪽 그룹이 0개&quot;라서, 트리가 한 줄로 길게 늘어선 체인(height=n-1)이 된다. 각 레벨에서 partition은 그 시점에 남은 크기만큼 비교를 하므로:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;n + (n-1) + (n-2) + ... + 1 = O(n^2)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 패턴은 &lt;b&gt;1부의 Selection-Sort와 정확히 같은 구조&lt;/b&gt;다 - &quot;매번 전체를 훑으면서 하나씩만 확정해나가는&quot; 것과 동일하다. pivot을 잘못 고르면 Quick-Sort가 Selection-Sort처럼 퇴화한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;pivot을 첫 번째 원소로 바꿔도 마찬가지&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;같은 배열 [1,2,3,4,5]에서 pivot을 마지막 원소가 아니라 &lt;b&gt;첫 번째 원소&lt;/b&gt;로 바꿔보자. 첫 번째 원소(1)는 이 배열의 최솟값이므로, partition하면 &quot;작은 그룹은 0개, 큰 그룹은 나머지 4개&quot;가 된다 ([1 | 2,3,4,5]). 그 다음도 마찬가지로 pivot(2)이 다시 남은 배열의 최솟값이라 [2 | 3,4,5]... 똑같이 한 줄 체인이 되어 O(n^2)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;일반화&lt;/b&gt;: last-element pivot이든 first-element pivot이든, &quot;이미 정렬된(혹은 역순) 배열&quot;에서는 pivot이 매번 그 구간의 극값(최소 또는 최대)이 되어버린다. 그러면 partition 결과가 항상 &quot;한쪽은 0개&quot;가 되고 O(n^2)이 된다. 즉 어느 쪽 끝을 pivot으로 고르든, &quot;이미 정렬된 배열&quot;이라는 흔한 입력 패턴에 대해 똑같이 최악의 경우를 만난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(참고: 이 때문에 실전에서는 &quot;랜덤하게 pivot 고르기&quot;나 &quot;median-of-three&quot;(첫/중간/끝 중 중간값) 같은 방법을 써서, 이미 정렬된 배열처럼 자주 등장하는 입력에 대해 우연히 최악의 경우에 빠지는 걸 피한다.)&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.5 In-place Quick-Sort&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Merge-Sort는 merge할 때 O(n)의 새 배열이 필요했다. Quick-Sort의 partition은 &lt;b&gt;같은 배열 안에서 swap만으로&lt;/b&gt; 수행할 수 있다 (Lomuto partition 방식):&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;partition(A, lo, hi):
    pivot = A[hi]
    i = lo - 1
    for j = lo to hi-1:
        if A[j] &amp;lt;= pivot:
            i = i + 1
            swap(A[i], A[j])
    swap(A[i+1], A[hi])   # pivot을 최종 위치로
    return i+1            # pivot의 최종 인덱스&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;i는 &quot;지금까지 확인한 것 중 pivot보다 작거나 같은 원소들의 마지막 위치&quot;를 추적한다. j가 그런 원소를 만날 때마다 i를 한 칸 늘리고 그 자리와 swap - 결국 pivot보다 작은 원소들이 앞쪽에 모인다. 마지막에 pivot을 i+1 자리로 옮기면 partition 완성.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;swap만 쓰므로 &lt;b&gt;추가 공간 O(1)&lt;/b&gt; (재귀 호출 스택까지 고려하면 O(log n)~O(n)) - Merge-Sort가 O(n) 추가 공간이 필요했던 것과 대조적이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Stable Sorting&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 Stable Sort란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 key(값)를 가진 원소가 여러 개 있을 때, &lt;b&gt;정렬 후에도 그 원소들의 &quot;원래 순서&quot;가 그대로 유지&lt;/b&gt;되면 그 정렬을 stable하다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 중요할까? 예를 들어 학생 명단을 먼저 이름순으로 정렬해놓고, 그 다음 &quot;학년&quot;으로 stable sort를 하면 - 같은 학년 안에서는 이름순이 그대로 유지된다. 여러 기준으로 순차 정렬할 때 stability가 이런 보장을 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 예제: Selection-Sort는 stable이 아니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열 [(3,a),(3,b),(1,c)] - 값이 같은 a,b의 원래 순서(a가 먼저)가 유지되는지 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Selection-Sort&lt;/b&gt;:&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;[(3,a),(3,b),(1,c)]
최솟값 (1,c)(idx2) 찾아서 idx0과 swap -&amp;gt; [(1,c),(3,b),(3,a)]
나머지 [(3,b),(3,a)]에서 최솟값은 (3,b)(idx1, 이미 자리) -&amp;gt; 그대로
결과: [(1,c),(3,b),(3,a)]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;a와 b의 순서가 b,a로 뒤바뀌었다! (1,c)를 idx0으로 끌어오는 swap 때문에, 원래 맨 뒤에 있던 a가 b보다 앞으로 튕겨나갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Insertion-Sort&lt;/b&gt;:&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;[(3,a) | (3,b),(1,c)]
(3,b) 삽입: (3,a)와 비교, 3&amp;gt;3은 거짓(같으면 안 밀음) -&amp;gt; 그대로 뒤에 -&amp;gt; [(3,a),(3,b)]
(1,c) 삽입: (3,b),(3,a) 모두 밀고 맨앞 -&amp;gt; [(1,c),(3,a),(3,b)]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;a,b 순서(a가 먼저) 그대로 유지된다 - Insertion-Sort는 &quot;같으면 옮기지 않는다&quot;(&amp;gt; 비교, &amp;gt;= 아님)는 규칙 덕분에 stable하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 각 알고리즘의 stability&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Selection-Sort&lt;/b&gt;: 아니오 - 위 예제처럼, &quot;최솟값을 멀리서 끌어오는 swap&quot;이 다른 원소들의 순서를 망가뜨릴 수 있다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Insertion-Sort&lt;/b&gt;: 예 - 비교에서 &quot;크면&quot; 옮기고 &quot;같으면&quot; 안 옮기면 순서 보존&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Merge-Sort&lt;/b&gt;: 예 - merge에서 두 값이 같을 때 &lt;b&gt;왼쪽 배열의 원소를 먼저&lt;/b&gt; 가져오면(왼쪽 배열은 원래 더 앞쪽에 있던 원소들이므로) 순서 보존&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Heap-Sort&lt;/b&gt;: 아니오 - heap 연산(down-heap 등)은 원래 위치 정보를 추적하지 않음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Quick-Sort&lt;/b&gt;: 아니오 (일반적인 in-place 구현) - partition 중의 swap이 같은 값들의 상대 순서를 바꿀 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 전체 비교표&lt;/h2&gt;
&lt;table style=&quot;height: 231px;&quot; width=&quot;773&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style5&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 23px;&quot;&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;알고리즘&lt;/th&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;최선&lt;/th&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;평균&lt;/th&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;최악&lt;/th&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;추가공간&lt;/th&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;stable&lt;/th&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;in-place&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Selection-Sort&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O(n^2)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O(n^2)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O(n^2)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O(1)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;아니오&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;예&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Insertion-Sort&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O(n)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O(n^2)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O(n^2)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O(1)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;예&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;예&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Heap-Sort&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O(n log n)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O(n log n)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O(n log n)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O(1)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;아니오&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;예&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Merge-Sort&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O(n log n)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O(n log n)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O(n log n)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O(n)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;예&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;아니오&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Quick-Sort&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O(n log n)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O(n log n)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O(n^2)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O(log n)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;아니오&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;예(대체로)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;적용 예시: &quot;메모리가 매우 제한적이고, 같은 점수를 가진 학생들의 원래 등록 순서를 반드시 유지해야 한다&quot;면?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표에서 O(1) 공간과 stable을 &lt;b&gt;동시에&lt;/b&gt; 만족하는 건 &lt;b&gt;Insertion-Sort뿐&lt;/b&gt;이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Selection-Sort: O(1)이지만 not stable - 탈락&lt;/li&gt;
&lt;li&gt;Heap-Sort: O(1)이지만 not stable - 탈락&lt;/li&gt;
&lt;li&gt;Merge-Sort: stable이지만 O(n) 공간 - 탈락&lt;/li&gt;
&lt;li&gt;Quick-Sort: O(log n)이고 not stable - 탈락&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Insertion-Sort&lt;/b&gt;: O(1) + stable 둘 다 만족 (대신 O(n^2) worst case를 감수)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 어떤 알고리즘도 &quot;시간 빠름 + 공간 적음 + stable + worst case 보장&quot;을 전부 만족하진 못한다 - 상황에 맞게 어떤 조건을 희생할지 선택하는 게 알고리즘 선택의 본질이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리 - Sorting 단원 전체 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 편에 걸쳐 본 5개 알고리즘을 한 줄로 정리하면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Selection-Sort&lt;/b&gt;: 항상 O(n^2). &quot;최솟값 찾기&quot;에 early exit이 없어서 입력 상태와 무관&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Insertion-Sort&lt;/b&gt;: O(n)~O(n^2), adaptive, stable. &quot;맞는 자리를 찾으면 즉시 멈춤&quot;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Heap-Sort&lt;/b&gt;: O(n log n), in-place, not stable. down-heap으로 &quot;다음 최댓값은 항상 array[0]&quot;을 O(log n)에 유지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Merge-Sort&lt;/b&gt;: 항상 O(n log n), O(n) 공간, stable. &quot;divide는 trivial, merge가 핵심&quot;이고 레벨당 O(n) x 레벨수 O(log n)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Quick-Sort&lt;/b&gt;: 평균 O(n log n), 최악 O(n^2), in-place, not stable. &quot;partition이 핵심&quot;이고 pivot 선택이 균형을 좌우 - 잘못 고르면 Selection-Sort처럼 퇴화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;큰 그림으로 보면: O(n^2) 알고리즘들은 &quot;한 번에 하나씩 확정&quot;하는 방식이고, O(n log n) 알고리즘들은 모두 어떤 형태로든 &quot;문제를 절반 크기로 줄여나가는&quot; 구조(heap의 트리 구조, merge/quick의 divide-and-conquer)를 갖고 있다 - 이게 O(n^2)에서 O(n log n)으로의 도약이 일어나는 공통된 이유다.&lt;/p&gt;</description>
      <category>공부 기록/자료구조</category>
      <category>quick-sort</category>
      <category>STABLE</category>
      <category>인공지능</category>
      <category>자료구조</category>
      <category>컴퓨터</category>
      <author>와일</author>
      <guid isPermaLink="true">https://beta-log.tistory.com/49</guid>
      <comments>https://beta-log.tistory.com/49#entry49comment</comments>
      <pubDate>Sat, 27 Jun 2026 08:22:55 +0900</pubDate>
    </item>
    <item>
      <title>Sorting (2)</title>
      <link>https://beta-log.tistory.com/48</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글의 흐름:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Divide-and-Conquer 패러다임과 Merge-Sort의 재귀 구조&lt;/li&gt;
&lt;li&gt;Merge-sort tree로 재귀 구조를 시각화하고, 트리의 height를 정확히 계산하는 법&lt;/li&gt;
&lt;li&gt;실제 merge 연산(두 포인터 방식)&lt;/li&gt;
&lt;li&gt;이 모든 걸 합쳐서 O(n log n) 시간복잡도가 나오는 이유&lt;/li&gt;
&lt;li&gt;Heap-Sort와 달리 O(n) 추가 공간이 필요한 이유 (in-place가 아님)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Divide-and-Conquer란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 알고리즘이 따르는 3단계 패턴이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Divide&lt;/b&gt;: 문제를 더 작은 부분 문제(subproblem)들로 나눈다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Recur&lt;/b&gt;: 각 부분 문제를 (재귀적으로) 푼다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Conquer&lt;/b&gt;: 부분 문제들의 답을 합쳐서 원래 문제의 답을 만든다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Merge-Sort는 이 패턴을 정렬에 그대로 적용한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Divide&lt;/b&gt;: 배열을 반으로 나눈다 (왼쪽 절반, 오른쪽 절반)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Recur&lt;/b&gt;: 각 절반을 재귀적으로 Merge-Sort한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Conquer(merge)&lt;/b&gt;: 정렬된 두 절반을 합쳐서 하나의 정렬된 배열로 만든다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;base case&lt;/b&gt;: 원소가 1개(또는 0개)인 배열은 이미 정렬된 것으로 본다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Merge-Sort Tree로 보는 재귀 구조&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 예제: [7,2,9,4,3,8,6,1]&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Divide 단계 (위 -&amp;gt; 아래로 반씩 나눔)&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;                          [7,2,9,4,3,8,6,1]
                          /                \
                  [7,2,9,4]                [3,8,6,1]
                  /       \                /       \
              [7,2]      [9,4]          [3,8]      [6,1]
              /   \       /   \          /   \       /   \
            [7]   [2]   [9]   [4]      [3]   [8]   [6]   [1]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8개짜리 배열이 4개 -&amp;gt; 2개 -&amp;gt; 1개로, 매 레벨마다 정확히 절반씩 줄어든다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Conquer(merge) 단계 (아래 -&amp;gt; 위로 합치기)&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;            [7]   [2]   [9]   [4]      [3]   [8]   [6]   [1]
              \   /       \   /          \   /       \   /
              [2,7]      [4,9]          [3,8]      [1,6]
                  \       /                  \       /
                  [2,4,7,9]                  [1,3,6,8]
                          \                  /
                          [1,2,3,4,6,7,8,9]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 leaf(원소 1개)는 &quot;이미 정렬됨&quot;으로 시작하고, 한 레벨씩 올라가면서 두 개의 정렬된 배열을 하나의 정렬된 배열로 합치는(merge) 작업이 일어난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 트리를 두 방향으로 훑는다고 생각하면 된다 - &lt;b&gt;Divide는 위에서 아래로&lt;/b&gt;(큰 문제를 계속 쪼갬), &lt;b&gt;Merge는 아래에서 위로&lt;/b&gt;(작은 답들을 합쳐서 큰 답을 만듦).&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 트리의 height - 헷갈리기 쉬운 부분&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n=8 트리를 보면 레벨이 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;레벨0: 크기8  (1개)   [7,2,9,4,3,8,6,1]
레벨1: 크기4  (2개)
레벨2: 크기2  (4개)
레벨3: 크기1  (8개, leaf)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;b&gt;레벨(층)은 0~3까지 총 4개&lt;/b&gt;가 있다. 그런데 &lt;b&gt;height(높이)는 &quot;root에서 leaf까지 거치는 edge(간선)의 개수&quot;&lt;/b&gt;이므로 0-&amp;gt;1-&amp;gt;2-&amp;gt;3, 즉 &lt;b&gt;3&lt;/b&gt;이다. log2(8)=3과 일치한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &quot;레벨의 개수 = height + 1&quot;이지, &quot;height = log2(n) + 1&quot;이 아니다. n=16이라면:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;레벨0: 크기16 (1개)
레벨1: 크기8  (2개)
레벨2: 크기4  (4개)
레벨3: 크기2  (8개)
레벨4: 크기1  (16개, leaf)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;레벨은 0~4까지 5개이지만, &lt;b&gt;height는 4&lt;/b&gt;(=log2(16), root-&amp;gt;leaf까지 edge 4개)다. 처음엔 &quot;log2(16)=4번 쪼개야 한다&quot;는 계산에 추가로 1을 더해 height=5라고 잘못 생각하기 쉬운데, 그 &quot;+1&quot;은 height가 아니라 &lt;b&gt;레벨의 개수&lt;/b&gt;에 적용되는 보정이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 일반화&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트리의 height = log2(n) = O(log n)&lt;/li&gt;
&lt;li&gt;레벨의 개수 = height + 1 = O(log n)&lt;/li&gt;
&lt;li&gt;leaf의 개수 = n&lt;/li&gt;
&lt;li&gt;&lt;b&gt;각 레벨에서 처리할 원소의 총합은 항상 n개&lt;/b&gt;다 (레벨이 내려갈수록 배열 개수는 2배씩 늘지만 각 배열 크기는 절반씩 줄어들기 때문).&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Merge 연산 - 두 포인터(two-pointer) 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;merge(A, B)는 이미 정렬된 두 배열 A, B를 합쳐서 하나의 정렬된 배열을 만드는 연산이다. 핵심 아이디어: A와 B의 맨 앞 원소끼리만 비교하면 된다 - 둘 다 정렬돼있으므로, 두 맨 앞 원소 중 더 작은 게 전체에서도 가장 작다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;A, B 각각 포인터(i, j)를 맨 앞에 둔다&lt;/li&gt;
&lt;li&gt;A[i]와 B[j]를 비교, 더 작은 쪽을 결과에 넣고 그 포인터를 한 칸 전진&lt;/li&gt;
&lt;li&gt;한쪽이 끝나면, 남은 쪽을 그대로 결과 뒤에 이어붙인다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 예제: merge([2,4,7,9], [1,3,6,8])&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;A=[2,4,7,9] (i=0)   B=[1,3,6,8] (j=0)   결과=[]
2 vs 1 -&amp;gt; 1 작음 -&amp;gt; [1]              (j=1)
2 vs 3 -&amp;gt; 2 작음 -&amp;gt; [1,2]            (i=1)
4 vs 3 -&amp;gt; 3 작음 -&amp;gt; [1,2,3]          (j=2)
4 vs 6 -&amp;gt; 4 작음 -&amp;gt; [1,2,3,4]        (i=2)
7 vs 6 -&amp;gt; 6 작음 -&amp;gt; [1,2,3,4,6]      (j=3)
7 vs 8 -&amp;gt; 7 작음 -&amp;gt; [1,2,3,4,6,7]    (i=3)
9 vs 8 -&amp;gt; 8 작음 -&amp;gt; [1,2,3,4,6,7,8]  (j=4, B 끝)
B 끝났으니 A의 남은 원소(9)를 그대로 추가 -&amp;gt; [1,2,3,4,6,7,8,9]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 예제: merge([3,8], [1,6])&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;A=[3,8] (i=0)   B=[1,6] (j=0)   결과=[]
3 vs 1 -&amp;gt; 1 작음 -&amp;gt; [1]       (j=1)
3 vs 6 -&amp;gt; 3 작음 -&amp;gt; [1,3]     (i=1)
8 vs 6 -&amp;gt; 6 작음 -&amp;gt; [1,3,6]   (j=2, B 끝)
B 끝났으니 A의 남은 원소(8)를 그대로 추가 -&amp;gt; [1,3,6,8]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;두 예제 모두 &lt;b&gt;A와 B의 크기 합이 k라면 merge는 O(k)&lt;/b&gt;임을 보여준다 - 모든 원소를 정확히 한 번씩만 들여다보고 결과에 배치하기 때문이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 시간복잡도 - 왜 O(n log n)인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.3에서 본 &quot;각 레벨에서 처리할 원소의 총합은 항상 n개&quot;가 핵심이다. n=8 예제로 merge 단계(아래-&amp;gt;위)의 작업량을 보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;레벨3(크기1) -&amp;gt; 레벨2: merge 4번, 각각 크기1+1=2 -&amp;gt; 4 x O(2) = O(8) = O(n)
레벨2(크기2) -&amp;gt; 레벨1: merge 2번, 각각 크기2+2=4 -&amp;gt; 2 x O(4) = O(8) = O(n)
레벨1(크기4) -&amp;gt; 레벨0: merge 1번, 크기4+4=8     -&amp;gt; 1 x O(8) = O(8) = O(n)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 레벨에서 일어나는 merge들의 작업량을 다 더하면 항상 O(n)이다 - merge의 개수는 절반씩 줄어들지만, 각 merge가 처리하는 크기는 그만큼 2배씩 커지므로 서로 상쇄된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 레벨의 개수는 2.3에서 본 대로 O(log n)이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;전체 시간 = (레벨당 작업량 O(n)) x (레벨 개수 O(log n)) = O(n log n)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 공간복잡도 - Heap-Sort와의 차이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;(1)의 Heap-Sort는 swap만으로 동작해서 in-place(추가 공간 O(1))였다. 반면 Merge-Sort의 merge 연산은 &lt;b&gt;결과를 담을 새 배열이 필요&lt;/b&gt;하다 - 두 포인터로 비교하면서 &quot;새로운 자리&quot;에 채워나가야 하므로 원본 자리에 그대로 덮어쓸 수 없다. 그래서 Merge-Sort는 &lt;b&gt;O(n)의 추가 공간&lt;/b&gt;이 필요하고, in-place가 아니다.&lt;/p&gt;
&lt;table style=&quot;height: 104px;&quot; width=&quot;594&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style5&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 23px;&quot;&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;&amp;nbsp;&lt;/th&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;시간복잡도&lt;/th&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;공간복잡도&lt;/th&gt;
&lt;th style=&quot;height: 23px;&quot;&gt;in-place&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Heap-Sort&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O(n log n)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O(1)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;예&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Merge-Sort&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O(n log n)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;O(n)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;아니오&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;같은 O(n log n)이라도 &quot;어디서 비용을 지불하는가&quot;가 다르다 - Heap-Sort는 시간 대신 down-heap이라는 추가 연산으로 메모리를 아끼고, Merge-Sort는 merge를 위한 임시 공간을 대가로 더 단순한 구조를 가진다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글의 흐름: Divide-and-Conquer 패러다임 -&amp;gt; Merge-Sort tree로 재귀 구조 시각화(divide는 위-&amp;gt;아래, merge는 아래-&amp;gt;위) -&amp;gt; 두 포인터 merge 연산 -&amp;gt; &quot;레벨당 O(n)&quot; 논리로 O(n log n) 도출 -&amp;gt; O(n) 추가 공간이 필요한 이유.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 정리:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;height = log2(n), 레벨 개수 = height+1. n=16이면 height=4, 레벨 5개, leaf 16개 - height에 함부로 +1 하지 않기&lt;/li&gt;
&lt;li&gt;merge(A,B)는 두 포인터로 맨 앞 원소끼리만 비교, O(|A|+|B|)&lt;/li&gt;
&lt;li&gt;각 레벨의 merge 작업량 총합은 항상 O(n) -&amp;gt; 레벨 O(log n)개 -&amp;gt; 전체 O(n log n)&lt;/li&gt;
&lt;li&gt;Merge-Sort는 O(n) 추가 공간 필요 (Heap-Sort는 O(1))&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>공부 기록/자료구조</category>
      <category>divide-and-conquer</category>
      <category>Merge-sort</category>
      <category>인공지능</category>
      <category>자료구조</category>
      <category>컴퓨터</category>
      <author>와일</author>
      <guid isPermaLink="true">https://beta-log.tistory.com/48</guid>
      <comments>https://beta-log.tistory.com/48#entry48comment</comments>
      <pubDate>Fri, 26 Jun 2026 09:11:42 +0900</pubDate>
    </item>
    <item>
      <title>Sorting (1)</title>
      <link>https://beta-log.tistory.com/47</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Graph 단원이 끝나고 이번엔 Sorting이다. 이번 (1)에서는 &quot;비교 기반 정렬의 기초&quot;에 해당하는 Selection-Sort, Insertion-Sort, Heap-Sort를 다루고, Merge-Sort는 (2)에서 단독으로, Quick-Sort와 Stable Sorting은 (3)에서 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글의 흐름:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Selection-Sort: &quot;최솟값을 찾아서 옮긴다&quot;는 가장 단순한 방법. 항상 O(n^2)&lt;/li&gt;
&lt;li&gt;Insertion-Sort: &quot;정렬된 부분에 끼워넣는다&quot;는 방법. 입력에 따라 O(n) ~ O(n^2)&lt;/li&gt;
&lt;li&gt;둘의 핵심 차이: early exit이 가능한가&lt;/li&gt;
&lt;li&gt;Heap-Sort: heap 자료구조를 이용해 &quot;최댓값 찾기&quot;를 O(n)에서 O(log n)으로 줄여서 O(n log n) 달성&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Selection-Sort&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 아이디어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매 단계마다, &quot;아직 정렬되지 않은 부분&quot;에서 가장 작은 값을 찾아 정렬된 부분의 바로 다음 자리로 옮긴다(swap). 정렬된 부분은 배열의 앞쪽부터 한 칸씩 늘어난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열 [7,2,9,4,1]을 정렬해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[7,2,9,4,1]   -&amp;gt; 전체에서 최솟값 1(idx4) 찾아서 idx0과 swap -&amp;gt; [1,2,9,4,7]
[1|2,9,4,7]   -&amp;gt; 나머지에서 최솟값 2(idx1, 이미 자리) -&amp;gt; [1,2,9,4,7]
[1,2|9,4,7]   -&amp;gt; 나머지에서 최솟값 4(idx3) 찾아서 idx2와 swap -&amp;gt; [1,2,4,9,7]
[1,2,4|9,7]   -&amp;gt; 나머지에서 최솟값 7(idx4) 찾아서 idx3과 swap -&amp;gt; [1,2,4,7,9]
[1,2,4,7|9]   -&amp;gt; 마지막 원소는 자동으로 자리 -&amp;gt; 완료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;|&lt;/code&gt;는 &quot;정렬된 부분 | 정렬 안 된 부분&quot;의 경계를 나타낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.3 시간복잡도&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 단계에서 &quot;최솟값 찾기&quot;에 필요한 비교 횟수는 (남은 원소 개수 - 1)이다. 배열 크기가 n이면:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;(n-1) + (n-2) + ... + 1 + 0 = n(n-1)/2 = O(n^2)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;위 예제(n=5)에서는 4+3+2+1 = 10번의 비교가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;가장 중요한 포인트&lt;/b&gt;: 이 비교 횟수는 &lt;b&gt;입력이 어떤 순서든(이미 정렬돼있어도) 항상 동일&lt;/b&gt;하다. &quot;최솟값 찾기&quot;는 남은 부분을 전부 훑어봐야만 &quot;이게 최솟값이다&quot;라고 확신할 수 있기 때문에, 입력 상태와 무관하게 항상 O(n^2)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 swap(자리 교환)은 패스당 최대 1번이므로, 전체적으로 최대 n-1번뿐이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Insertion-Sort&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 아이디어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열의 왼쪽부터 &quot;정렬된 부분&quot;을 한 칸씩 늘려나간다. 새로 추가되는 원소를 정렬된 부분 안의 올바른 위치에 &quot;끼워넣을&quot; 때, 자기보다 큰 원소들을 한 칸씩 뒤로 밀면서(shift) 자리를 찾는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 배열 [7,2,9,4,1]을 Insertion-Sort로 정렬해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[7 | 2,9,4,1]
2 삽입: 7&amp;gt;2 -&amp;gt; 7을 한 칸 밀고 2를 맨앞에 -&amp;gt; [2,7 | 9,4,1]
9 삽입: 7&amp;lt;9 -&amp;gt; 비교 1번으로 끝, 9는 그대로 맨뒤 -&amp;gt; [2,7,9 | 4,1]
4 삽입: 9&amp;gt;4, 7&amp;gt;4 두 개를 밀고, 2&amp;lt;4에서 멈춤 -&amp;gt; [2,4,7,9 | 1]
1 삽입: 9,7,4,2 전부 밀고 맨앞에 -&amp;gt; [1,2,4,7,9]
완료&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 Best case와 Worst case&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새 원소를 끼워넣을 때마다 비교+이동 횟수가 원소마다 다르다. 위 예제에서 9를 넣을 때는 비교 1번, 1을 넣을 때는 비교+이동 4번이 필요했다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이미 정렬된 배열&lt;/b&gt;이라면: 매 원소가 &quot;바로 왼쪽 원소보다 크다&quot;는 것만 확인하고 즉시 끝난다 -&amp;gt; 비교 n-1번, &lt;b&gt;O(n)&lt;/b&gt; (best case)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;역순(최악)&lt;/b&gt; 배열이라면: 매 원소가 맨 앞까지 밀려간다 -&amp;gt; &lt;b&gt;O(n^2)&lt;/b&gt; (worst case)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 입력 상태에 따라 성능이 달라지는 성질을 &lt;b&gt;adaptive&lt;/b&gt;하다고 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Selection-Sort vs Insertion-Sort - Early Exit의 차이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;두 알고리즘 모두 worst case는 O(n^2)이지만, 이미 정렬된 입력을 줬을 때 Insertion-Sort는 O(n)으로 빨라지는 반면 Selection-Sort는 여전히 O(n^2)이다. 왜 이런 차이가 생길까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;b&gt;early exit(조기 종료)이 가능한가&lt;/b&gt;이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Insertion-Sort&lt;/b&gt;: 새 원소를 왼쪽 원소와 비교해서 &quot;더 크네, 여기가 맞는 자리&quot;라고 판단되면 &lt;b&gt;그 즉시 멈출 수 있다&lt;/b&gt;. 더 왼쪽까지 볼 필요가 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Selection-Sort&lt;/b&gt;: &quot;이 값이 남은 부분 중 최솟값이다&quot;라는 걸 확인하려면, &lt;b&gt;남은 원소를 전부 비교해봐야&lt;/b&gt; 한다. 미리 &quot;이게 최솟값이다&quot;라고 알 수 있는 방법이 없으므로 early exit이 불가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;height: 106px;&quot; width=&quot;604&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style5&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;비교 횟수&lt;/th&gt;
&lt;th&gt;swap/shift&lt;/th&gt;
&lt;th&gt;adaptive&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Selection-Sort&lt;/td&gt;
&lt;td&gt;항상 O(n^2)&lt;/td&gt;
&lt;td&gt;최대 n-1번&lt;/td&gt;
&lt;td&gt;아니오&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Insertion-Sort&lt;/td&gt;
&lt;td&gt;최선 O(n) ~ 최악 O(n^2)&lt;/td&gt;
&lt;td&gt;입력에 따라 다름&lt;/td&gt;
&lt;td&gt;예&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Heap-Sort&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 Array 기반 Heap 복습&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Heap-Sort는 배열을 max-heap으로 다루면서 정렬한다. Array 기반 heap의 핵심 규칙:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;index 0 = 항상 root(최댓값)&lt;/b&gt; - 이건 array 기반 heap의 정의 자체이다. 찾는 게 아니라 항상 그 자리에 있어야 한다.&lt;/li&gt;
&lt;li&gt;index i인 노드의 자식은 index 2i+1, 2i+2에 위치한다.&lt;/li&gt;
&lt;li&gt;max-heap property: 모든 노드는 자기 자식보다 크거나 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 array = [9,5,7,1]은 다음 트리에 대응된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;       9        (index0, root)
      / \
     5   7      (index1, index2)
    /
   1            (index3, 5의 자식)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9&amp;gt;=5, 9&amp;gt;=7, 5&amp;gt;=1 이므로 유효한 max-heap이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(이 점이 헷갈릴 수 있는데, 만약 array의 index0에 있는 값이 자식보다 작다면 그건 처음부터 max-heap이 아닌 것이다. &quot;root가 무엇인지&quot;를 보고 array를 만드는 게 아니라, &lt;b&gt;array의 index0이 곧 root&lt;/b&gt;라는 점을 기억해야 한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 알고리즘 - 두 단계&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Build-heap&lt;/b&gt;: 배열 전체를 max-heap으로 변환한다. (bottom-up 방식으로 O(n)에 가능)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;반복&lt;/b&gt; (heap 크기가 1이 될 때까지):
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;array[0](root, 현재 heap에서 최댓값)과 array[heap크기-1](현재 heap의 마지막 원소)을 swap&lt;/li&gt;
&lt;li&gt;heap 크기를 1 줄인다&lt;/li&gt;
&lt;li&gt;array[0]에 대해 &lt;b&gt;down-heap&lt;/b&gt;(sift-down) 수행: 새 root가 max-heap property를 만족하지 않으면, 더 큰 자식과 swap을 반복하며 내려간다. heap의 높이가 O(log n)이므로 down-heap은 O(log n)&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 예제 - 전체 trace&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;array = [9,5,7,1] (위에서 확인했듯 유효한 max-heap)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Step 1&lt;/b&gt;: heap크기=4&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;swap(array[0]=9, array[3]=1) -&amp;gt; [1,5,7,9]&lt;/li&gt;
&lt;li&gt;heap크기 -&amp;gt; 3 (9는 이제 자기 자리, 즉 정렬 완료 영역으로 확정)&lt;/li&gt;
&lt;li&gt;down-heap(array[0]=1), heap크기3 -&amp;gt; [1,5,7] 범위. 1의 자식은 idx1=5, idx2=7. 더 큰 자식 7과 swap -&amp;gt; [7,5,1,9]. 1은 이제 idx2인데, heap크기3에서 idx2의 자식(idx5,6)은 범위 밖 -&amp;gt; 종료&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과: [7,5,1,9], heap크기=3, 정렬완료=[9]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Step 2&lt;/b&gt;: heap크기=3&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;swap(array[0]=7, array[2]=1) -&amp;gt; [1,5,7,9]&lt;/li&gt;
&lt;li&gt;heap크기 -&amp;gt; 2 (7은 자기 자리로 확정)&lt;/li&gt;
&lt;li&gt;down-heap(array[0]=1), heap크기2 -&amp;gt; [1,5] 범위. 1의 자식은 idx1=5뿐. 1&amp;lt;5 -&amp;gt; swap -&amp;gt; [5,1,7,9]. idx1의 자식(idx3,4)은 범위 밖 -&amp;gt; 종료&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과: [5,1,7,9], heap크기=2, 정렬완료=[7,9]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Step 3&lt;/b&gt;: heap크기=2&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;swap(array[0]=5, array[1]=1) -&amp;gt; [1,5,7,9]&lt;/li&gt;
&lt;li&gt;heap크기 -&amp;gt; 1 (5는 자기 자리로 확정)&lt;/li&gt;
&lt;li&gt;down-heap(array[0]=1), heap크기1 -&amp;gt; 자식 없음 -&amp;gt; 종료&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과: [1,5,7,9], heap크기=1, 정렬완료=[5,7,9]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Step 4&lt;/b&gt;: heap크기=1 -&amp;gt; 더 진행할 것 없음, 종료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 결과: [1,5,7,9] - 정렬 완료!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.4 down-heap이 왜 중요한가 - Selection-Sort와의 연결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;여기서 자연스럽게 떠오르는 질문: &quot;어차피 매 단계마다 root를 끝으로 보내는 건데, 굳이 down-heap으로 다음 최댓값을 매번 array[0]으로 갖다 놓을 필요가 있나?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답은: &lt;b&gt;그래, 그게 바로 Heap-Sort가 빠른 이유의 핵심이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 down-heap을 하지 않는다면, 다음 차례의 최댓값(예: Step2 시작 시점의 [1,5,7] 중 7)이 어디 있는지 알아내기 위해 &lt;b&gt;heap 영역 전체를 훑어봐야&lt;/b&gt; 한다 - 이건 Selection-Sort가 &quot;최솟값 찾기&quot;를 위해 매번 전체를 훑는 것과 똑같은 비용(O(n))이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;반면 down-heap을 매번 수행해두면, &quot;&lt;b&gt;다음 최댓값은 항상 array[0]에 있다&lt;/b&gt;&quot;는 사실이 항상 보장된다. 그러면 다음 최댓값을 찾는 데는 O(1)이면 되고, 그 보장을 유지하는 데(= down-heap)는 O(log n)만 든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;즉, down-heap은 &quot;&lt;b&gt;다음 최댓값이 어디 있는지 매번 새로 찾는 비용 O(n)&lt;/b&gt;&quot;을 &quot;&lt;b&gt;heap 모양을 살짝 복구하는 비용 O(log n)&lt;/b&gt;&quot;으로 바꿔주는 장치다. 이것이 Selection-Sort(O(n^2))와 Heap-Sort(O(n log n))의 본질적인 차이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.5 시간복잡도와 공간&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Build-heap: O(n)&lt;/li&gt;
&lt;li&gt;반복문에서 down-heap을 최대 n번 수행, 각 O(log n) -&amp;gt; O(n log n)&lt;/li&gt;
&lt;li&gt;합쳐서 전체 &lt;b&gt;O(n log n)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;swap만으로 동작하므로 추가 공간 O(1) - &lt;b&gt;in-place&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글의 흐름: Selection-Sort(항상 O(n^2), early exit 불가) -&amp;gt; Insertion-Sort(O(n)~O(n^2), early exit 가능, adaptive) -&amp;gt; 이 둘의 차이(early exit) -&amp;gt; Heap-Sort(heap 구조로 &quot;최댓값 찾기&quot;를 O(1)+O(log n)으로 만들어 O(n log n) 달성).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 정리:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Selection-Sort: 비교 n(n-1)/2 = O(n^2), 항상 동일. swap은 최대 n-1번&lt;/li&gt;
&lt;li&gt;Insertion-Sort: adaptive. 정렬된 입력에서 O(n), 역순 입력에서 O(n^2). &quot;더 큰 원소를 만나면 즉시 멈춤&quot;이 핵심&lt;/li&gt;
&lt;li&gt;early exit 가능 여부가 두 알고리즘의 차이를 만든다 - &quot;최솟값 찾기&quot;는 끝까지 봐야 하고, &quot;삽입 위치 찾기&quot;는 도중에 멈출 수 있다&lt;/li&gt;
&lt;li&gt;Heap-Sort: array 기반 heap에서 index0=root, 자식은 2i+1,2i+2. swap(root,마지막) -&amp;gt; heap크기 감소 -&amp;gt; down-heap을 반복. down-heap이 &quot;다음 최댓값은 항상 array[0]&quot;이라는 보장을 O(log n)에 유지해주는 게 핵심. 전체 O(n log n), in-place&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>공부 기록/자료구조</category>
      <category>heap-sort</category>
      <category>max-heap</category>
      <category>인공지능</category>
      <category>자료구조</category>
      <category>컴퓨터</category>
      <author>와일</author>
      <guid isPermaLink="true">https://beta-log.tistory.com/47</guid>
      <comments>https://beta-log.tistory.com/47#entry47comment</comments>
      <pubDate>Thu, 25 Jun 2026 08:00:07 +0900</pubDate>
    </item>
    <item>
      <title>[HRD 쪽글 모음 #4] 경력과 조직, 그리고 나</title>
      <link>https://beta-log.tistory.com/37</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;경력을 어떻게 볼 것인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;경력개발이란 개인의 경력을 장기적으로 성장&amp;middot;발전시키는 활동이다. 그런데 현대의 경력개발은 고용 불안정성의 증가와 함께 개인 주도적&amp;middot;유동적 경력관으로 전환되고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;만화경 경력모형이라는 개념이 있다. 주관적 경력 성공과 내적 동기를 강조하며, 단선적&amp;middot;정태적 경력관에서 벗어나 다양하고 창의적으로 경력을 구성해 나가는 역량이 중요하다는 관점이다. 아직 뚜렷한 방향을 정하지 못한 채 탐색 중인 입장에서, 이 관점이 지금의 나에게 더 적합한 틀로 느껴졌다. 경력을 하나의 고정된 목표를 향해 직선적으로 나아가는 것이 아니라, 경험을 쌓으며 유동적으로 구성해 나가는 과정으로 바라보는 것. 당장의 방향이 없다는 게 문제가 아닐 수 있다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Super의 생애단계이론처럼 개인의 특성과 발달 단계에 맞는 경력 개발을 설명하는 이론들도 있지만, 사회문화적 맥락이나 성별&amp;middot;인종에 따른 일반화의 한계가 있다는 비판도 함께 다뤄졌다. 조직 차원에서는 토너먼트식 경력이동모형처럼 초기 선발에서 탈락한 사람은 이후 승진 기회가 줄어든다는 냉정한 현실도 있다. 개인 차원의 경력 이론이 자기 인식을 도와도, 그것이 실제 배치와 업무로 연결되는 조직 차원의 메커니즘이 없으면 이론에 그칠 수밖에 없다. 그 간극을 어떻게 메울 수 있는지는 쉽게 답이 나오지 않는 질문이었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;조직은 어떻게 변하는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;조직개발이란 조직 전체의 지속적&amp;middot;장기적 변화를 지향하며, 구성원의 협력과 참여, 최고 경영진의 지원을 전제로 하는 체계적 접근이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;조직변화모형 중 긍정적 탐구모형과 실행연구모형의 대비가 인상적이었다. 긍정적 탐구모형은 문제보다 강점에 집중하여 동기부여와 변화를 이끌어내는 접근이고, 실행연구모형은 문제 확인에서 출발해 순환적 과정을 통해 변화를 실행한다. 강점에 집중하는 접근이 구성원의 동기부여 측면에서 의미 있다는 건 이해하지만, 현실에서는 문제 진단에서 출발하는 방식이 더 효율적으로 개선을 이끌어낼 수 있을 것 같다는 생각도 들었다. 어느 접근이 더 낫다고 단언하기보다는 조직의 상황과 맥락에 따라 달라질 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;AI가 조직 내 변화의 신호를 조기에 감지하고 적절한 시점에 개입을 제안하는 역할을 할 수 있을 것이라는 기대도 생겼다. 다만 그때 구성원이 변화의 주체로 남으려면 AI의 개입 범위를 어디까지로 설정해야 하는지는 여전히 풀리지 않는 질문이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사람을 사람으로 바라보는 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;HRD 담당자의 역할은 시대에 따라 변화해 왔다. 초기에는 운영자&amp;middot;학습 전문가 역할이 중심이었고, 이후 성과 측면에서 재규정되었으며, 지금은 기술 발전과 산업 변화에 대응하는 전략가로서의 역할이 부각되고 있다. 요구되는 역량도 전략적 사고, 대인관계와 감성 지능, 데이터 분석, 변화 관리까지 단순히 교육 전문가의 그것을 넘어선다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그런데 수업의 마지막에 등장한 말이 그 모든 역량보다 더 오래 남았다. 전문성을 넘어 장인성으로. 4장에서 처음 접했던 장인성 개념이 12장 마지막에 다시 등장하면서, 단순한 지식이나 기술의 축적을 넘어 인간에 대한 사랑과 성장 가능성에 대한 믿음이 HRD의 근본이라는 점을 전하고 있었다. 수업의 처음부터 마지막까지 일관되게 흐른 메시지가 여기서 하나로 묶이는 느낌이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;HRD 담당자의 역량으로 제시된 내용들이 단지 이 분야의 직무 역량에 그치지 않고, 한 사람이 삶을 살아가면서 갖추어야 할 태도와 자질로도 읽혔다. 언젠가 나 자신도 누군가의 성장을 돕는 역할을 수행할 수 있을까 하는 상상도 해보게 됐다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;성과 대신 가치를&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;마지막 장에서 다룬 가치 패러다임은 이 수업 전체를 정리하는 언어처럼 느껴졌다. 훈련 대 학습(내적 과정), 성과 대 가치(외적 결과)의 틀로 인적자원개발의 지향점을 재정의한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;가치 창출의 핵심 동인은 사람이고, 개인 차원에서 일의 의미와 삶의 보람을 강조하며, 조직과 구성원이 장기적으로 상생하는 인본주의적 관점을 취한다. 1장에서 인적자원개발의 목적을 처음 접했을 때부터 마지막 장에 이르기까지, 인간을 도구가 아니라 가치를 실현하는 주체로 보는 시선이 일관되게 흐르고 있었다. 수업 내내 그 흐름이 있었는데, 마지막 장에서야 명확하게 보였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;새로운 화두로 제시된 것들도 인상적이었다. 디지털 전환, 다문화 일터, 창의적 인재 육성, 워라밸과 일의 의미. 특히 일은 삶에서 중심성을 갖지만 삶의 전부가 아니라는 워라밸의 관점은, 앞으로의 진로를 고민하는 시점에서 마음에 새겨둘 만한 말이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그런데 현실의 조직과 사회에서는 여전히 반대 방향의 압력이 강하다고 느껴진다. 근시안적인 성과보다 가치를, 훈련보다 학습을, 전문성보다 장인성을 강조하는 흐름이 수업 내내 일관되었지만, 그 간극 속에서 어떻게 방향을 잃지 않을 수 있는지는 아직 답을 모르겠다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기술을 만드는 사람으로서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;14장에서 디지털 매체의 차가운 연결망을 어떻게 인간화할 것인가라는 질문이 나왔다. 기술이 차갑다고 느껴본 적이 없었는데, 편리함과 효율성 이면에 인간적 온기가 희석될 수 있다는 문제의식은 기술을 다루는 입장에서 특히 무게 있게 다가왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;효율과 기능만큼이나 인간적 연결의 온기를 함께 담아내는 것. 기술을 만드는 사람으로서 그것이 인간의 연결을 어떻게 매개하는지에 대한 책임감을 가져야겠다는 생각이 들었다. 이 수업이 HRD를 가르쳐준 동시에, 내가 앞으로 만들어갈 것들에 대해 다르게 생각하게 해 준 것 같다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;한 학기 동안 인적자원개발론을 들으면서, HRD 지식보다 더 많이 남은 건 질문들이었다. 경력을 어떻게 볼 것인가, 조직 안에서 어떻게 성장할 것인가, 기술을 만드는 사람으로서 무엇을 책임져야 하는가. 답은 아직 없지만, 질문이 생겼다는 것만으로도 충분한 것 같다. 앞으로도 꾸준히 나를 성찰하고 고민하는 삶을 살아야지.&lt;/p&gt;</description>
      <category>생각 정리</category>
      <category>HRD</category>
      <category>가치패러다임</category>
      <category>경력</category>
      <category>인적자원개발</category>
      <category>조직</category>
      <author>와일</author>
      <guid isPermaLink="true">https://beta-log.tistory.com/37</guid>
      <comments>https://beta-log.tistory.com/37#entry37comment</comments>
      <pubDate>Wed, 24 Jun 2026 09:23:06 +0900</pubDate>
    </item>
    <item>
      <title>Graphs (2)</title>
      <link>https://beta-log.tistory.com/46</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Graphs(1)에서는 그래프가 뭔지(vertex, edge, path, cycle, connectedness, subgraph, tree)와 그래프를 어떻게 저장하는지(Edge List / Adjacency List / Adjacency Matrix)를 다뤘다. 이번 (2)에서는 그 그래프 위에서 실제로 무엇을 할 수 있는지를 본다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그래프 전체를 빠짐없이 둘러보는 탐색 방법 - DFS와 BFS&lt;/li&gt;
&lt;li&gt;가중 그래프에서 한 점으로부터 다른 모든 점까지의 최단 경로를 구하는 Dijkstra's Algorithm&lt;/li&gt;
&lt;li&gt;모든 vertex를 가장 싸게 연결하는 트리를 찾는 Minimum Spanning Tree(MST) - Prim's, Kruskal's, 그리고 이 둘을 가능하게 하는 Union-Find 자료구조&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 그래프 탐색 - DFS와 BFS&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 DFS - 실과 페인트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DFS(Depth First Search)의 직관은 &quot;실과 페인트&quot;다. 시작 vertex에 실을 묶어두고 페인트 통을 들고 있다고 생각하자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 vertex에서 아직 안 가본 edge를 하나 골라, 실을 풀면서 따라간다&lt;/li&gt;
&lt;li&gt;도착한 vertex를 페인트로 칠한다(visited 표시)&lt;/li&gt;
&lt;li&gt;더 갈 곳이 없으면(막다른 길) 실을 되감으며 이전 vertex로 돌아간다(backtrack)&lt;/li&gt;
&lt;li&gt;돌아간 vertex에 아직 안 가본 edge가 있으면 다시 그 길로 출발&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Depth First&quot;는 매번 &quot;안 가본 edge를 골라 끝까지 가본 뒤에야 다른 선택지를 본다&quot;는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고리즘(재귀):&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;DFS(G, u):
    u는 이미 visited 상태
    for u의 각 outgoing edge e=(u,v):
        if v가 아직 미방문이면:
            e를 discovery edge로 표시
            v를 visited로 표시
            DFS(G, v)
        else:
            e를 back edge(또는 cross edge 등)로 표시&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 새로 등장하는 용어 둘:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;discovery edge: 처음으로 새 vertex를 방문하게 만든 edge. 이 edge들만 모으면 트리(DFS tree)가 된다&lt;/li&gt;
&lt;li&gt;back edge: 이미 visited인 vertex로 이어지는 edge (discovery edge는 아님)&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 DFS 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vertex A,B,C,D,E, edge는 A-B, A-C, A-D, A-E, B-C, C-D, C-E (총 7개). A와 C는 나머지 모든 vertex와 연결되어 있다. A에서 시작, 인접 vertex는 알파벳 순서로 본다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;A 방문. 인접 B,C,D,E 중 첫 미방문 B &amp;rarr; discovery A-B&lt;/li&gt;
&lt;li&gt;B. 인접 A(부모, 건너뜀), C(미방문) &amp;rarr; discovery B-C&lt;/li&gt;
&lt;li&gt;C. 인접 A(visited, 부모 아님 &amp;rarr; back C--&amp;gt;A), B(부모), D(미방문) &amp;rarr; discovery C-D&lt;/li&gt;
&lt;li&gt;D. 인접 A(visited, 부모 아님 &amp;rarr; back D--&amp;gt;A), C(부모). 더 없음 &amp;rarr; backtrack&lt;/li&gt;
&lt;li&gt;C로 복귀. E(미방문) &amp;rarr; discovery C-E&lt;/li&gt;
&lt;li&gt;E. 인접 A(visited, 부모 아님 &amp;rarr; back E--&amp;gt;A), C(부모). 더 없음 &amp;rarr; backtrack&lt;/li&gt;
&lt;li&gt;C, B, A 순서로 backtrack. 종료&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 DFS tree:&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;            A
            |
            B
            |
            C
           / \
          D   E

back edge(점선): C--&amp;gt;A, D--&amp;gt;A, E--&amp;gt;A&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;DFS tree는 &quot;한 줄기로 깊게 들어갔다가, 막히면 가장 가까운 분기점으로 돌아와 다른 가지를 타는&quot; 모양이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.3 DFS의 성질&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;discovery edge들 사이엔 cycle이 없다 (= DFS tree)&lt;/li&gt;
&lt;li&gt;undirected graph: DFS(G,u)는 u의 connected component 전체를 방문하고, discovery edge들이 그 component의 spanning tree(DFS tree)를 만든다&lt;/li&gt;
&lt;li&gt;directed graph: DFS(G,u)는 u로부터 reachable한 모든 vertex를 방문한다&lt;/li&gt;
&lt;li&gt;시간복잡도: adjacency list 기준 O(n+m)&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.4 Cycle Detection - back edge와 cross edge&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 규칙: DFS에서 back edge가 존재한다 == cycle이 존재한다. (back edge의 정의: 자신의 ancestor(조상)로 이어지는 edge)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;undirected graph: 간단하다. 이미 explored된 vertex를 다시 만나면, 그건 항상 자신의 ancestor이므로 무조건 back edge = cycle&lt;/li&gt;
&lt;li&gt;directed graph: 덜 간단하다. 이미 explored된 vertex를 다시 만났어도, 그게 ancestor가 아닐 수 있다. 이런 edge를 cross edge라고 부르고, cycle을 의미하지 않는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시: vertex U,V,W,X,Y,Z의 directed graph에서 DFS(G,U)가 다음과 같이 진행됐다고 하자.&lt;/p&gt;
&lt;pre class=&quot;tp&quot;&gt;&lt;code&gt;discovery edges: U-&amp;gt;V, V-&amp;gt;X, X-&amp;gt;Z, X-&amp;gt;Y, Y-&amp;gt;W
back edges:      W--&amp;gt;V, W--&amp;gt;X
cross edge:      U--&amp;gt;W&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;W에서 V, X로 가는 edge는 back edge다 (V, X는 W의 ancestor, 즉 U-&amp;gt;V-&amp;gt;X-&amp;gt;Y-&amp;gt;W 경로 위에 있다). 이 back edge들로 X,Y,W와 V,X,Y,W가 각각 cycle을 이룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;(U,W)는 cross edge다. DFS가 U-&amp;gt;V-&amp;gt;X-&amp;gt;Y-&amp;gt;W까지 다 탐색하고 backtrack해서 U로 돌아온 뒤, U의 마지막 edge인 (U,W)를 확인하는 시점에는 W가 이미 &quot;탐색이 끝난(finished)&quot; 상태다. 이때 W는 U의 ancestor가 아니므로 back edge가 아니라 cross edge다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;(U,W)가 cross edge라는 게 왜 cycle을 의미하지 않을까? cycle이 되려면 (U,W) 외에도 W에서 다시 U로 돌아가는 길이 있어야 한다. 그런데 W가 가진 outgoing edge는 W-&amp;gt;V, W-&amp;gt;X뿐이고 둘 다 V의 서브트리(V,X,Y,Z,W) 안쪽만 가리킨다. U는 그 서브트리 밖에 있으므로, W에서 U로 돌아갈 길이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리: back edge는 &quot;지금 밟고 있는 경로 위의 조상으로 되돌아가는 edge&quot;라서 항상 cycle을 만든다. cross edge는 &quot;이미 끝난 다른 가지로 건너가는 edge&quot;인데, 그 가지에서 나에게로 돌아오는 길이 없으므로 cycle을 만들지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.5 Path Finding with DFS&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DFS(G,u)의 discovery edge들을 얻었다면, u에서 v까지의 path 찾기:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;v에서 시작해서, discovery edge를 따라 거꾸로(부모 방향으로) 올라간다&lt;/li&gt;
&lt;li&gt;u에 도달하면, 지금까지의 경로를 역순으로 뒤집은 게 u에서 v까지의 path&lt;/li&gt;
&lt;li&gt;u에 도달하지 못하면 path 없음&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.2의 DFS tree에서 A에서 D로 가는 path: D -&amp;gt; C -&amp;gt; B -&amp;gt; A로 거꾸로 올라간 뒤 뒤집으면 A -&amp;gt; B -&amp;gt; C -&amp;gt; D.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.6 BFS - 물결처럼 퍼지기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BFS(Breadth First Search)는 DFS와 정반대다. 시작 vertex에서 물결이 동심원처럼 퍼져나가듯, 가까운 vertex부터 레벨 단위로 넓게 탐색한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Level 0: 시작 vertex&lt;/li&gt;
&lt;li&gt;Level 1: 시작 vertex와 직접 연결된(1-hop) vertex들&lt;/li&gt;
&lt;li&gt;Level 2: Level 1의 vertex들과 연결된, 아직 안 본 vertex들 (시작점에서 2-hop)&lt;/li&gt;
&lt;li&gt;큐(queue)를 이용해 레벨을 하나씩 늘려가며 처리&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.7 BFS 예제 - 같은 그래프로 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.2와 같은 그래프(A,B,C,D,E, edges A-B,A-C,A-D,A-E,B-C,C-D,C-E)에 BFS를 적용해보자. A에서 시작, 인접 vertex는 알파벳 순서.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Level 0: A&lt;/li&gt;
&lt;li&gt;A의 인접 B,C,D,E 전부 미방문 &amp;rarr; 전부 Level 1, discovery edge A-B, A-C, A-D, A-E&lt;/li&gt;
&lt;li&gt;B 처리: A(부모), C(이미 Level1) &amp;rarr; cross edge B-C&lt;/li&gt;
&lt;li&gt;C 처리: A(부모), B,D,E(이미 Level1) &amp;rarr; cross edge C-D, C-E&lt;/li&gt;
&lt;li&gt;D, E: 더 볼 것 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;            A                (Level 0)
        /  |  |  \
       B   C  D   E          (Level 1)
        \ / \  \ /
        B-C C-D C-E  : cross edge&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DFS는 A-B-C-D-E가 한 줄로 이어진 트리였는데, BFS는 A를 중심으로 B,C,D,E가 모두 Level 1에 모이는 별(star) 모양이 된다. 같은 그래프인데 탐색 순서만 바뀌었을 뿐인데 트리 모양이 완전히 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BFS에서의 edge 분류:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;undirected graph: BFS에서는 back edge가 절대 생기지 않는다. 이미 visited인 vertex로 가는 edge는 항상 cross edge다&lt;/li&gt;
&lt;li&gt;directed graph: BFS에서는 back edge와 cross edge가 둘 다 생길 수 있다&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.8 BFS의 성질 - 최단 경로&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;undirected graph: BFS(G,u)도 u의 connected component 전체를 방문하고, discovery edge들이 spanning tree(BFS tree)를 만든다&lt;/li&gt;
&lt;li&gt;시간복잡도: O(n+m)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 핵심적인 성질: Level i에 있는 vertex v에 대해, BFS tree에서 시작점 s부터 v까지의 경로는 정확히 i개의 edge를 거친다. 그리고 G 안의 어떤 path로도 s에서 v까지 가려면 최소 i개의 edge가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;즉, BFS tree에서 v까지의 거리(edge 개수)가 실제 그래프에서의 최단 거리와 정확히 같다. BFS는 (가중치 없는 그래프에서) 최단 경로를 찾는 방법이기도 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.9 DFS와 BFS 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 vertex F를 추가하고 D-F edge 하나만 연결했다고 하자. 각 vertex의 인접 목록:&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;A: B, C, D, E
B: A, C
C: A, B, D, E
D: A, C, F
E: A, C
F: D&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DFS(A) tree:&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;A
|
B
|
C
|\
D E
|
F&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A-B-C-D-F가 한 줄로 길게 이어지고, C에서 E로 가지 하나가 뻗어나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BFS(A) tree:&lt;/p&gt;
&lt;pre class=&quot;gherkin&quot;&gt;&lt;code&gt;        A
    /   |  |  \
   B    C  D   E
              |
              F&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;F는 Level 2가 되고, A에서 F까지는 정확히 2개의 edge(A-D-F)를 거친다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DFS: 길고 깊은 트리. 거리 정보는 보장 안 되지만(DFS tree에서 A-D 거리는 3인데 실제 최단은 1), 연결 여부 확인, cycle 탐지, 경로 존재 여부 확인 등에 적합&lt;/li&gt;
&lt;li&gt;BFS: 넓고 얕은 트리. Level = 시작점부터의 최소 거리(edge 개수)이므로, 가중치 없는 그래프에서 최단 경로/최단 거리를 구할 때 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 가중 그래프와 최단 경로 - Dijkstra's Algorithm&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 Weighted Graph와 Shortest Path&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;weighted graph는 각 edge e=(u,v)에 숫자 weight w(e)=w(u,v)가 붙은 그래프(거리, 비용 등). path의 length는 그 path를 이루는 edge들의 weight 합이고, shortest path는 두 vertex 사이에서 length가 최소인 path다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Dijkstra's Algorithm은 weight가 음수가 아니라는 가정에 크게 의존한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 Dijkstra - cloud와 edge relaxation&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Dijkstra는 greedy algorithm이다: 매 순간 &quot;지금 보기에 가장 좋은 선택&quot;을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;cloud란 &quot;이미 시작점으로부터의 최단거리가 확정되어서, 앞으로 절대 안 바뀔 vertex들의 집합&quot;이다. 다이어그램에서 이 영역을 구름 모양으로 감싸서 그리기 때문에 그렇게 부른다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;처음엔 cloud = {시작 vertex s}뿐이다. D(s)=0은 더 줄어들 수 없으므로 확정&lt;/li&gt;
&lt;li&gt;매 단계: cloud 밖에서 D값이 가장 작은 vertex를 골라 cloud에 추가한다 - &quot;이 vertex의 D값은 이제부터 영원히 안 바뀐다&quot;는 선언&lt;/li&gt;
&lt;li&gt;방금 들어온 vertex를 거쳐서, 아직 cloud 밖인 인접 vertex들의 D값이 더 줄어들 수 있는지 확인한다 (edge relaxation)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;edge relaxation 공식: D(v) = min(D(v), D(u) + w(u,v)) (u는 방금 cloud에 들어온 vertex)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 공식이 의미하는 건 &quot;v까지 가는 두 가지 방법 중 더 짧은 걸 택하자&quot;다 - 지금까지 알던 최선의 값 D(v)와, 방금 확정된 u를 거쳐서 가는 값 D(u)+w(u,v) 중 더 작은 쪽.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vertex A,B,C,D,E,F. edges = A-B(8), A-C(2), A-D(4), B-C(7), B-E(2), C-D(1), C-E(3), C-F(9), D-F(5). A에서 시작.&lt;/p&gt;
&lt;table style=&quot;height: 249px;&quot; width=&quot;573&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style5&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;cloud에 새로 추가&lt;/th&gt;
&lt;th&gt;D(A)&lt;/th&gt;
&lt;th&gt;D(B)&lt;/th&gt;
&lt;th&gt;D(C)&lt;/th&gt;
&lt;th&gt;D(D)&lt;/th&gt;
&lt;th&gt;D(E)&lt;/th&gt;
&lt;th&gt;D(F)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;초기&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;무한&lt;/td&gt;
&lt;td&gt;무한&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;C (D=2)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;D (D=3)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;E (D=5)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;B (D=7)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;F (D=8)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 단계 설명:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1단계: cloud={A}에서 가장 작은 C(=2)를 추가. C를 거쳐가는 게 더 짧은지 확인 &amp;rarr; D(D)=min(4, 2+1)=3, D(E)=min(무한,2+3)=5, D(F)=min(무한,2+9)=11&lt;/li&gt;
&lt;li&gt;2단계: D(=3)를 추가. D를 거치면 D(F)=min(11,3+5)=8로 줄어듦&lt;/li&gt;
&lt;li&gt;3단계: E(=5) 추가. E를 거치면 D(B)=min(8,5+2)=7로 줄어듦&lt;/li&gt;
&lt;li&gt;4단계: B(=7) 추가. B의 인접(A,C,E) 모두 이미 cloud &amp;rarr; 변화 없음&lt;/li&gt;
&lt;li&gt;5단계: F(=8) 추가. 끝&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;edge relaxation을 직접 적용해보는 예: 2단계에서 D가 cloud에 들어온 직후, D-F(weight5) edge로 relaxation을 적용하면 D(F) = min(D(F), D(D)+w(D,F)) = min(11, 3+5) = min(11,8) = 8. &quot;원래 11이라고 알던 D(F)가, D를 통해 가는 길(3+5=8)이 더 짧다는 걸 발견해서 8로 줄어든다&quot;는 게 이 단계의 의미다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 결과: A에서 각 vertex까지의 최단거리는 A=0, B=7, C=2, D=3, E=5, F=8.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.4 왜 동작할까&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;핵심은 weight가 음수가 아니라는 가정이다. cloud 안의 vertex들은 이미 확정된 최단거리인데, cloud 밖으로 나가는 어떤 경로든 cloud 경계를 넘는 순간 거리가 늘어날 수밖에 없다(weight가 0 이상이므로). 그래서 &quot;지금 cloud 밖에서 가장 가까운 vertex&quot;를 골라도, 나중에 다른 경로를 거쳐서 그보다 더 짧아질 일이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;만약 weight가 음수라면, 나중에 &quot;지름길&quot;이 생겨서 이미 확정한 D값이 사실은 최단이 아니었던 게 될 수 있다. 즉 &quot;cloud에 넣어서 확정한다&quot;는 행위 자체가 의미를 잃어버려서 알고리즘이 깨진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.5 시간복잡도&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n개 vertex, m개 edge, adjacency list + priority queue(연산당 O(log n)) 기준:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 vertex는 PQ에 한 번 삽입, 한 번 제거 &amp;rarr; O(n log n)&lt;/li&gt;
&lt;li&gt;각 edge는 relaxation으로 인한 D값 업데이트가 최대 한 번씩 일어날 수 있음 &amp;rarr; O(m log n)&lt;/li&gt;
&lt;li&gt;합쳐서 O((n+m) log n)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Minimum Spanning Tree - Prim's, Kruskal's, Union-Find&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 MST란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;spanning tree는 그래프의 모든 vertex를 포함하면서 cycle 없이 전부 연결한 부분그래프였다. 한 그래프에는 spanning tree가 여러 개 있을 수 있다. 각 edge에 weight(비용)가 있다면, 각 spanning tree마다 &quot;포함된 edge들의 weight 합&quot;(총 비용)이 정해진다. 이 총 비용이 가장 작은 spanning tree를 Minimum Spanning Tree(MST)라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비유: 여러 도시 사이에 도로를 놓을 때, 모든 도시가 연결되어야 하고 굳이 cycle은 필요 없다. &quot;도로 건설 총 비용을 최소화하는 도로망&quot;이 MST다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 Cut(Partition) Property&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Prim's와 Kruskal's 둘 다 다음 사실에 기댄다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;전체 vertex 집합을 두 그룹 V1, V2로 나눈다고 하자(어떻게 나누든 둘 다 비어있지 않으면 됨). V1과 V2 사이를 잇는 edge들 중 weight가 가장 작은 edge를 e라고 하면, e를 포함하는 MST가 반드시 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;왜 그럴까? spanning tree는 모든 vertex를 연결해야 하므로 V1과 V2를 잇는 &quot;다리&quot; 역할의 edge가 최소 하나는 있어야 한다. 그 다리들 중 가장 싼 걸 안 쓰고 더 비싼 다리를 썼다면, 그 비싼 다리를 떼고 가장 싼 다리로 바꿔도 여전히 모든 vertex가 연결되고 cycle도 안 생기며 총 비용은 같거나 줄어든다. 그러니 가장 싼 다리를 쓰는 게 항상 이득이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 사실을 반복 적용하는 게 두 알고리즘의 본질이고, &quot;V1, V2를 어떻게 나누느냐&quot;의 방식이 둘 사이에서 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 Prim's Algorithm&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prim's는 Dijkstra와 구조가 거의 같다. cloud를 하나씩 키워나가는 그 패턴 그대로인데, 딱 한 가지가 다르다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dijkstra의 D(v) = &quot;시작점 s부터 v까지 가는 전체 경로의 길이&quot;(여러 edge weight의 합)&lt;/li&gt;
&lt;li&gt;Prim's의 D(v) = &quot;v를 지금의 cloud에 직접 연결하는 edge 하나의 weight&quot;(cloud에 연결되는 가장 싼 edge가 뭔지)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고리즘:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;아무 vertex나 하나 골라 cloud에 넣는다(D=0)&lt;/li&gt;
&lt;li&gt;나머지 vertex들의 D는 무한대로 시작&lt;/li&gt;
&lt;li&gt;반복: cloud 밖에서 D가 가장 작은 vertex를 cloud에 추가한다 - 이때 그 vertex를 cloud에 연결시킨 edge가 MST edge가 된다&lt;/li&gt;
&lt;li&gt;방금 들어온 vertex와 연결된, 아직 cloud 밖인 vertex들에 대해: 그 edge의 weight가 지금의 D보다 작으면 D를 그 edge weight로 갱신&lt;/li&gt;
&lt;li&gt;모든 vertex가 cloud에 들어가면 종료&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제: vertex A,B,C,D,E,F, edges = A-B(2), A-C(8), A-E(7), B-C(5), B-D(7), C-D(9), C-E(8), D-F(4), E-F(3). A에서 시작.&lt;/p&gt;
&lt;table style=&quot;height: 285px;&quot; width=&quot;749&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style5&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;cloud에 추가&lt;/th&gt;
&lt;th&gt;사용된 edge&lt;/th&gt;
&lt;th&gt;D(A)&lt;/th&gt;
&lt;th&gt;D(B)&lt;/th&gt;
&lt;th&gt;D(C)&lt;/th&gt;
&lt;th&gt;D(D)&lt;/th&gt;
&lt;th&gt;D(E)&lt;/th&gt;
&lt;th&gt;D(F)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;초기&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;무한&lt;/td&gt;
&lt;td&gt;무한&lt;/td&gt;
&lt;td&gt;무한&lt;/td&gt;
&lt;td&gt;무한&lt;/td&gt;
&lt;td&gt;무한&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;(시작점)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;무한&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;무한&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;B (D=2)&lt;/td&gt;
&lt;td&gt;A-B&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;무한&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;C (D=5)&lt;/td&gt;
&lt;td&gt;B-C&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;무한&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;D (D=7)&lt;/td&gt;
&lt;td&gt;B-D&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;F (D=4)&lt;/td&gt;
&lt;td&gt;D-F&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;E (D=3)&lt;/td&gt;
&lt;td&gt;F-E&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4단계에서 D(D)=7과 D(E)=7이 tie인데, D를 먼저 추가한다고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 MST edges: A-B(2), B-C(5), B-D(7), D-F(4), F-E(3). 총 weight = 2+5+7+4+3 = 21&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.4 Kruskal's Algorithm&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kruskal's는 Prim's와 완전히 다른 접근이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 각 vertex를 자기 혼자만의 작은 그룹(cluster)으로 본다. 전체 edge들을 weight가 작은 순서대로 하나씩 살펴보면서, 그 edge가 서로 다른 두 그룹을 연결해주면 추가하고 두 그룹을 합친다(merge). 같은 그룹끼리 잇는 edge라면(이미 연결돼있으니 cycle만 생기므로) 버린다. 결국 그룹이 하나만 남으면 끝.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고리즘:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;각 vertex를 자기만의 클러스터로 만든다(n개의 클러스터)&lt;/li&gt;
&lt;li&gt;모든 edge를 weight가 작은 순서로 정렬(또는 priority queue)&lt;/li&gt;
&lt;li&gt;반복: 가장 작은 weight의 edge (u,v)를 꺼낸다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;u와 v가 다른 클러스터에 속하면 &amp;rarr; MST에 추가, 두 클러스터를 합친다&lt;/li&gt;
&lt;li&gt;같은 클러스터에 속하면 &amp;rarr; 버린다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;MST가 n-1개의 edge를 가지면(클러스터가 1개로 합쳐짐) 종료&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 그래프로 추적: weight 순서 정렬 - A-B(2), E-F(3), D-F(4), B-C(5), A-E(7), B-D(7), A-C(8), C-E(8), C-D(9)&lt;/p&gt;
&lt;table style=&quot;height: 190px;&quot; width=&quot;901&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style5&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;순서&lt;/th&gt;
&lt;th&gt;edge(weight)&lt;/th&gt;
&lt;th&gt;같은 클러스터?&lt;/th&gt;
&lt;th&gt;결과&lt;/th&gt;
&lt;th&gt;클러스터 상태&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;A-B(2)&lt;/td&gt;
&lt;td&gt;다름(각자 혼자)&lt;/td&gt;
&lt;td&gt;추가&lt;/td&gt;
&lt;td&gt;{A,B}, {C}, {D}, {E}, {F}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;E-F(3)&lt;/td&gt;
&lt;td&gt;다름&lt;/td&gt;
&lt;td&gt;추가&lt;/td&gt;
&lt;td&gt;{A,B}, {C}, {D}, {E,F}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;D-F(4)&lt;/td&gt;
&lt;td&gt;다름(D 혼자, F는 {E,F})&lt;/td&gt;
&lt;td&gt;추가&lt;/td&gt;
&lt;td&gt;{A,B}, {C}, {D,E,F}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;B-C(5)&lt;/td&gt;
&lt;td&gt;다름(B는 {A,B}, C 혼자)&lt;/td&gt;
&lt;td&gt;추가&lt;/td&gt;
&lt;td&gt;{A,B,C}, {D,E,F}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;A-E(7)&lt;/td&gt;
&lt;td&gt;다름(A는 {A,B,C}, E는 {D,E,F})&lt;/td&gt;
&lt;td&gt;추가, n-1=5개 됨, 종료&lt;/td&gt;
&lt;td&gt;{A,B,C,D,E,F}&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kruskal's가 찾은 MST: A-B(2), E-F(3), D-F(4), B-C(5), A-E(7). 총 weight = 2+3+4+5+7 = 21&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.5 MST may not be unique&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Prim's에서는 4단계에서 D(D)=7과 D(E)=7이 tie였고 B-D(weight7)를 선택했다. Kruskal's는 weight7짜리 edge 중 A-E를 먼저 선택했다(B-D는 정렬 순서상 A-E 다음인데, A-E를 추가한 순간 이미 클러스터가 1개로 합쳐져서 B-D는 볼 필요도 없이 끝났다).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 알고리즘이 고른 MST의 구체적인 edge 집합은 다르지만(B-D vs A-E), 총 weight는 똑같이 21이다. weight가 같은 edge가 여러 개 있을 때 어떤 걸 고르느냐에 따라 트리의 모양은 달라질 수 있지만, 최소 총 비용 자체는 항상 같다 - 이게 &quot;MST may not be unique&quot;의 실제 의미다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.6 Union-Find(Partition) 자료구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kruskal's에서 매 edge (u,v)마다 빠르게 해야 하는 두 가지:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&quot;u와 v가 같은 클러스터에 있는가?&quot;(확인)&lt;/li&gt;
&lt;li&gt;다른 클러스터라면 두 클러스터를 합쳐라(병합)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 효율적으로 해주는 자료구조가 Union-Find다. 제공하는 연산:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;make_group(e): 새 원소 e 하나로 이루어진 클러스터를 만든다(처음에 각 vertex마다 한 번씩 호출)&lt;/li&gt;
&lt;li&gt;find(p): p가 속한 클러스터의 대표(leader/root)가 누구인지 알려준다. find(p) == find(q)이면 p와 q는 같은 클러스터&lt;/li&gt;
&lt;li&gt;union(p,q): p가 속한 클러스터와 q가 속한 클러스터를 하나로 합친다&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Tree 기반 구현&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 원소는 자기 &quot;부모&quot;를 가리키는 포인터 하나만 들고 있다. 자기 자신을 가리키는 원소가 그 클러스터의 root(대표)다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;find(p): p에서 시작해서 부모, 부모의 부모, ...를 따라가다가 자기 자신을 가리키는 원소(root)를 찾으면 그게 답&lt;/li&gt;
&lt;li&gt;union(p,q): 한 트리의 root를 다른 트리의 root의 자식으로 만든다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.4의 Kruskal's 진행을 Union-Find 관점에서 보면:&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;초기: A B C D E F     (전부 자기 자신을 가리킴, 전부 root)

union(A,B): B의 root를 A로
union(E,F): F의 root를 E로
union(D,F): D(크기1)가 더 작으므로, D의 root를 E로 (F의 root는 이미 E)
union(B,C): C(크기1)가 더 작으므로, C의 root를 A로 (B의 root는 이미 A)
union(A,E): 둘 다 크기3으로 같음 -&amp;gt; E의 root를 A로

최종 구조:
        A
      / | \
     B  C  E
          / \
         D   F&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Union-by-size&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;union할 때 항상 더 작은(원소 개수가 적은) 트리의 root를 더 큰 트리의 root 밑에 붙인다. 이렇게 하면 트리의 높이가 잘 안 늘어나서 find()가 부모를 따라 올라가는 횟수가 대략 O(log n)을 넘지 않는다. 크기가 같은 경우에는 루트로 아무거나 골라도 상관 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직관: 어떤 원소의 &quot;부모를 한 번 따라간다&quot;는 건, 그 부모가 한때 어떤 트리의 root였다는 뜻이고, 그 트리는 합쳐질 때마다 최소 2배씩 커졌어야 한다. n개 원소로는 이런 일이 최대 log n번만 반복될 수 있다. 결과적으로 n번의 union-find 연산을 합쳐서 O(n log n) 시간이 걸린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Kruskal's 전체 시간복잡도&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;edge들을 weight 순으로 정렬/PQ: O(m log m)&lt;/li&gt;
&lt;li&gt;union-find 연산들(클러스터 확인+병합): O(n log n)&lt;/li&gt;
&lt;li&gt;합쳐서 대략 O(m log n) (단순 그래프에서는 m이 O(n^2)이지만 log m과 log n은 같은 차수)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글의 흐름: 그래프 탐색(DFS의 깊은 트리, BFS의 넓은 트리와 최단거리) -&amp;gt; 가중 그래프에서의 최단경로(Dijkstra, cloud와 edge relaxation) -&amp;gt; 모든 vertex를 가장 싸게 연결하는 트리(MST, Prim's와 Kruskal's, 그리고 Kruskal's를 가능하게 하는 Union-Find).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 정리:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DFS: back edge=cycle, cross edge != cycle (directed graph에서만 구분 필요). Path finding은 discovery edge를 거꾸로 따라가서 뒤집기&lt;/li&gt;
&lt;li&gt;BFS: Level = 시작점부터의 최소 거리. 같은 그래프라도 DFS는 길고 좁은 트리, BFS는 넓고 얕은 트리가 된다&lt;/li&gt;
&lt;li&gt;Dijkstra: cloud = 확정된 최단거리 집합. edge relaxation D(v)=min(D(v), D(u)+w(u,v)). nonnegative weight 가정이 핵심&lt;/li&gt;
&lt;li&gt;Prim's: Dijkstra와 같은 구조지만, D(v)는 &quot;cloud에 연결되는 가장 싼 edge의 weight&quot;&lt;/li&gt;
&lt;li&gt;Kruskal's: 작은 클러스터들을 weight가 작은 edge부터 합쳐나간다. MST may not be unique(총 weight는 같지만 구체적 edge 구성은 다를 수 있음)&lt;/li&gt;
&lt;li&gt;Union-Find: find(p)==find(q)로 같은 클러스터 확인, union-by-size로 트리 높이를 O(log n) 수준으로 유지&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>공부 기록/자료구조</category>
      <category>Dijkstra&amp;rsquo;s Algorithm</category>
      <category>Kruskal&amp;rsquo;s algorithm</category>
      <category>인공지능</category>
      <category>자료구조</category>
      <category>컴퓨터</category>
      <author>와일</author>
      <guid isPermaLink="true">https://beta-log.tistory.com/46</guid>
      <comments>https://beta-log.tistory.com/46#entry46comment</comments>
      <pubDate>Tue, 23 Jun 2026 08:36:24 +0900</pubDate>
    </item>
  </channel>
</rss>