<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>r-jelly 님의 블로그</title>
    <link>https://r-jelly.tistory.com/</link>
    <description>r-jelly 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Mon, 15 Jun 2026 02:14:16 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>r-jelly</managingEditor>
    <item>
      <title>소수 판별 알고리즘</title>
      <link>https://r-jelly.tistory.com/16</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;소수 (Prime Number)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소수란? &lt;u&gt;1&lt;b&gt;보다 큰 자연수 중에서, 1과 자기 자신으로만 나누어 떨어지는 숫자&lt;/b&gt;&lt;/u&gt;를 말한다. 즉, 약수가 2개인 자연수이다. 소수는 2, 3, 5, 7, 11, &amp;hellip; 등으로 무한히 많으며, 규칙성이 아직 증명되지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;소수 판별법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, 특정 숫자 N이 소수인지 아닌지를 확인하는 문제를 해결한다고 하자. 소수의 정의에 의해서 소수는 1과 자기 자신으로만 나누어 떨어지므로, 반대로 2부터 자기 자신보다 1 작은 값까지는 나누어 떨어지지 않는다는 것을 알 수 있다. 따라서, N이 소수인지 여부를 판단하려면, 2부터 N-1까지 모든 숫자에 대해서 나누어 떨어지는지를 확인하는 방법을 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;def is_prime(N: int) -&amp;gt; bool:
    for i in range(2, N):
        if N % i == 0:
            return False
    return True&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;합성수 N이 있다고 한다면, 1을 제외한 가장 작은 약수는 $\sqrt N$ 이하일 수 밖에 없다. 만약 $x$가 1을 제외한 $N$의 가장 작은 약수라면, $\frac{N}{x}$ 또한 $N$의 약수이고, 아래와 같이 $x \le \sqrt 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;$$ \begin{align*} x \le \frac{N}{x} \\ x^2 \le N \\ x \le \sqrt N \\ \end{align*} $$&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;따라서, N이 소수인지 여부를 판정하기 위해서는 2부터 $\sqrt N$까지의 수로 나누어 떨어지는지를 확인하면 된다. 따라서 해당 방법은 시간복잡도가 $O(\sqrt N)$이 된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;def is_prime(N: int) -&amp;gt; bool:
    for i in range(2, int(N**0.5)):
        if N % i == 0:
            return False
    return True&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;에라토스테네스의 체&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N까지의 모든 자연수에 대해서 각 숫자들이 소수인지 아닌지를 판별해야 하는 경우도 존재한다. 만약, 위의 소수 판정법을 사용하면 전체 숫자들이 소수인지 아닌지를 판정하기 위해서는 $O(N\sqrt 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;이때, 에라토스테네스의 체를 이용하면 N까지의 모든 소수를 빠르게 찾을 수 있다. 에라토스테네스의 체는 크게 아래와 같이 동작한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;2부터 N까지의 모든 숫자를 우선 소수라고 가정하자. (1은 소수가 아니므로 제외한다.)&lt;/li&gt;
&lt;li&gt;이때, 2의 모든 배수는 소수가 아닌 합성수이므로 소수가 아님을 알 수 있다.&lt;/li&gt;
&lt;li&gt;소수로 가정된 다음 숫자인 3 또한 마찬가지로 모든 배수가 소수가 아님을 알 수 있다.&lt;/li&gt;
&lt;li&gt;4는 이미 합성수로 판정되었기 때문에, 가정된 다음 숫자인 5에 대해서 같은 방식으로 동작한다.&lt;/li&gt;
&lt;li&gt;모든 N에 대해서 계속 진행한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;def prime_list(N: int) -&amp;gt; List:
    check_prime = [True] * (N+1)
    for num in range(2, N+1):
        if check_prime[num]:
            for multiple in range(num*2, N+1, num):
                check_prime[multiple] = False
    return [p for p in range(2, N+1) if check_prime[p]]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 에라토스테네스의 체를 이용해서 N까지의 소수를 알고 싶을 때, &lt;b&gt;N까지 모든 수의 배수를 전부 확인해 볼 필요는 없다.&lt;/b&gt; 만약, N보다 작은 합성수 M이 $M = a\times b$라면, $N \gt M$이므로 두 숫자 $a, b$ 중 적어도 하나는 $\sqrt N$보다 작은 수 이다. 따라서, N보다 작은 모든 합성수 M에 대해서 해당 합성수의 약수 중 하나는 적어도 $\sqrt N$보다 작다는 의미이므로 소수 판별법에서와 마찬가지로 &lt;u&gt;&lt;b&gt;2부터 $\sqrt N$까지에 대해서만 배수를 확인&lt;/b&gt;&lt;/u&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;u&gt;&lt;b&gt;$x$가 소수일 때, $x \times 2$부터 시작하는 것이 아니라, $x \times x$부터 값을 제거&lt;/b&gt;&lt;/u&gt;해 나가도 된다. 만약, $x \times x$보다 작은 수가 $x$의 배수라고 한다면, $2 \times x$의 경우와 같이 이미 이전 숫자를 제거할 때 제거되었을 것이다. 즉, &lt;b&gt;$i \times x$에서 $i &amp;lt; x$인 경우까지는 이미 검사&lt;/b&gt;되었으므로, $x^2$인 경우부터 제거하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 개선점들을 반영한 에라토스테네스의 체는 아래와 같이 구현될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;def prime_list(N: int) -&amp;gt; List:
    check_prime = [True] * (N+1)
    for num in range(2, int(N**0.5)):
        if check_prime[num]:
            for multiple in range(num**2, N+1, num):
                check_prime[multiple] = False
    return [p for p in range(2, N+1) if check_prime[p]]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 알고리즘의 시간 복잡도는 $O(N\log\log N)$으로, 거의 $O(N)$과 같다고 볼 수 있다. N이 5천만인 경우에 0.5초 정도밖에 걸리지 않는 매우 효율적인 알고리즘으로 볼 수 있다.&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;소인수분해 (Prime Factorization)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소인수분해는 특정한 자연수를 소수의 곱으로만 나타낸 것이다. 이러한 표현법은 특정 숫자에 대해서 단 하나만 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 숫자 N을 소인수분해하는 방법은 아래와 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;2부터 시작해서, 숫자 $i$에 대해서 $N$을 $i$로 나누었을 때, 나누어 떨어지는지를 확인한다.&lt;/li&gt;
&lt;li&gt;나누어 떨어진다면, $N \leftarrow N / i$으로 대체하여 나누어 떨어지지 않을때 까지 반복한다.&lt;/li&gt;
&lt;li&gt;나누어 떨어지지 않았을 때, 다음 숫자 $i+1$에 대해서 다시 반복한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소인수분해 또한 마찬가지로, $N &amp;lt; i \times i$가 될 때까지만 진행하면 된다. 따라서, 소인수분해 알고리즘도 $O(\sqrt N)$의 시간복잡도를 가질 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;def prime_factorization(N: int):
    for i in range(2, int(N**0.5)):
        while N % i == 0:
            print(i)
            N /= i
    if N &amp;gt; 1:
        print(int(N))&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;다른 방법은 위의 에라토스테네스의 체를 이용하는 것이다, 소인수분해를 하고자 하는 수 N보다 작은 모든 소수는 위의 에라토스테네스의 체를 통해서 구해놓았다고 가정하자. 그렇다면 아래와 같이 구해진 소수에 대해서만 계산할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;def prime_factorization(N: int):
    prime_nums = prime_list(N)
    for prime_num in prime_nums:
        while N % prime_num == 0:
            print(prime_num)
            N /= prime_num&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;References&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://loosie.tistory.com/267&quot;&gt;https://loosie.tistory.com/267&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.algodale.com/algorithms/sieve-of-eratosthenes/&quot;&gt;https://www.algodale.com/algorithms/sieve-of-eratosthenes/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cherryrain.tistory.com/102&quot;&gt;https://cherryrain.tistory.com/102&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Algorithm/개념 정리</category>
      <category>소수</category>
      <category>알고리즘</category>
      <category>에라토스테네스의 체</category>
      <author>r-jelly</author>
      <guid isPermaLink="true">https://r-jelly.tistory.com/16</guid>
      <comments>https://r-jelly.tistory.com/16#entry16comment</comments>
      <pubDate>Fri, 1 May 2026 13:42:37 +0900</pubDate>
    </item>
    <item>
      <title>우선순위 큐 (Priority Queue) &amp;amp; 힙 (Heap)</title>
      <link>https://r-jelly.tistory.com/15</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;우선순위 큐&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 큐(Queue)는 First-In-First-Out(FIFO) 구조로, 먼저 들어온 데이터가 먼저 나가게 된다. 우선순위 큐는 일반적인 큐와는 다르게, 각 데이터에 우선순위가 존재하여 Pop 연산을 수행할 때 &lt;u&gt;&lt;b&gt;우선순위가 가장 높은 원소가 (or 우선순위가 가장 낮은 원소가) 나오는 큐&lt;/b&gt;&lt;/u&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;우선순위 큐와 Linked List&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DJ83P/dJMcagehj9v/jGKZkAqsOW43oZXGx2kwDk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DJ83P/dJMcagehj9v/jGKZkAqsOW43oZXGx2kwDk/img.webp&quot; data-alt=&quot;Queue의 구현&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DJ83P/dJMcagehj9v/jGKZkAqsOW43oZXGx2kwDk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDJ83P%2FdJMcagehj9v%2FjGKZkAqsOW43oZXGx2kwDk%2Fimg.webp&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;600&quot; height=&quot;300&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Queue의 구현&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선순위 큐는 일반적인 큐와 같이 Linked List를 이용해서 구현할 수 있다. 이렇게 구현하게 된다면 데이터 추가에 드는 시간복잡도는, Linked List의 Rear에 데이터를 추가하는 시간이므로 $O(1)$이 된다. 그러나, 우선순위 큐에서 값을 제거하는 연산은, 최대값 or 최소값을 찾아야하므로 Linked List 내부의 모든 데이터를 확인해야만 가능하다. 즉, $O(N)$의 시간복잡도를 가지게 된다. 마찬가지로 우선순위가 제일 높거나 낮은 원소를 확인하는 데에도 똑같이 $O(N)$의 시간복잡도를 갖게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;힙 (Heap)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힙은 완전이진트리(Complete Binary Tree)의 일종으로 아래와 같은 특징을 가지고 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 노드의 자식은 최대 2개이며, 트리의 마지막 레벨에는 왼쪽부터 차례대로 노드가 채워져 있다. (=완전이진트리)&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;최대 힙 (Max Heap): 부모 노드의 값이 자식 노드의 값보다 항상 크거나 같은 힙&lt;/li&gt;
&lt;li&gt;최소 힙 (Min Heap): 부모 노드의 값이 자식 노드의 값보다 항상 작거나 같은 힙&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 성질에 의해서, 최대 힙의 Root에는 해당 힙 내부의 최댓값이, 최소 힙의 Root에는 해당 힙 내부의 최소값이 존재하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 우선순위 큐를 Linked List로 구현했을 때의 문제점을 해결하기 위해서 힙을 이용해서 우선순위 큐를 구현할 수 있다.&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;구현&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;861&quot; data-origin-height=&quot;411&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cb13m4/dJMcaib6OOa/wxeYGgNjQ7PZoh6v0v8sx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cb13m4/dJMcaib6OOa/wxeYGgNjQ7PZoh6v0v8sx0/img.png&quot; data-alt=&quot;Heap의 구현&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cb13m4/dJMcaib6OOa/wxeYGgNjQ7PZoh6v0v8sx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcb13m4%2FdJMcaib6OOa%2FwxeYGgNjQ7PZoh6v0v8sx0%2Fimg.png&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;600&quot; height=&quot;286&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;861&quot; data-origin-height=&quot;411&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Heap의 구현&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힙은 일반적으로 배열을 이용해서 구현할 수 있다. 힙은 완전이진트리이므로, Root 노드의 인덱스틑 1이라고 했을 때, 특정 위치 $x$의 왼쪽 자식 노드는 $2x$, 오른쪽 자식 노드는 $2x+1$의 인덱스를 갖게 된다. 또한, 부모 노드는 $\lfloor \frac{x}{2} \rfloor$가 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이진 탐색 트리를 배열로 구현하지 못하는 이유?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힙은 완전이진트리이므로, 값이 배열의 순서대로 채워진다. 그러나, 이진 탐색 트리는 불균형이 발생할 수 있기 때문에, 배열 중간에 계속해서 빈 값이 발생하게 된다. 따라서 최악의 경우 공간이 $O(2^N)$만큼 필요하므로 문제가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 이진 탐색 트리의 균형을 맞추는 과정에서 Rotation이 발생하게 되는데, 이때 값들의 인덱스를 모두 변경해야 하는 문제가 발생할 수 있다. 따라서 전체 서브트리 데이터의 값을 변경하는 시간이 소요되므로 배열로 구현하는 것이 손해이다.&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;&lt;b&gt;확인 (Find)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최대 힙 및 최소 힙에서 다음에 출력될 값은 단순히 Root 노드의 값을 확인하면 된다. 따라서 시간복잡도는 $O(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;&lt;b&gt;삽입 (Insert)&lt;/b&gt;&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;힙의 제일 마지막 노드 다음으로 새로운 데이터를 값으로 하는 노드를 추가한다.&lt;/li&gt;
&lt;li&gt;추가한 노드의 부모 노드와 대소 비교를 하여, 부모 노드가 더 작다면 값을 교환한다.&lt;/li&gt;
&lt;li&gt;부모 노드의 값이 작지 않거나, 더 이상 교환할 수 없을 때까지 2번을 반복한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최소 힙은 대소 관계를 바꾸면 똑같이 작동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 경우에서는, 최대로 일어날 수 있는 교환의 횟수가 힙의 높이만큼이 된다. 이때, 힙은 완전이진트리라서 불균형하지 않기 때문에, 높이는 $\log N$이 되므로 시간복잡도 또한 $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;&lt;b&gt;삭제 (Remove)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최대 힙, 최소 힙 모두 Root 노드가 다음에 삭제될 값이므로 해당 노드를 삭제해야 한다. 그러나 삭제된 이후에도 힙의 성질이 유지되어야 하므로, 다음과 같은 방법으로 힙을 복구한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;제거할 Root 노드와 힙의 제일 마지막 노드를 교환한다.&lt;/li&gt;
&lt;li&gt;이동된 Root 노드를 제거한다.&lt;/li&gt;
&lt;li&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;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우도 마찬가지로, 최대 교환 횟수가 힙의 높이만큼이다. 따라서, 삭제 연산도 시간복잡도는 $O(\log N)$이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, Linked List와 힙으로 구현한 우선순위 큐의 시간복잡도를 비교해보면 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 72px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 18px;&quot;&gt;Linked List&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 18px;&quot;&gt;Heap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 18px;&quot;&gt;Find&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 18px;&quot;&gt;$O(N)$&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 18px;&quot;&gt;$O(1)$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 18px;&quot;&gt;Insert&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 18px;&quot;&gt;$O(1)$&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 18px;&quot;&gt;$O(\log N)$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 18px;&quot;&gt;Remove&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 18px;&quot;&gt;$O(N)$&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 18px;&quot;&gt;$O(\log N)$&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;h3 data-ke-size=&quot;size23&quot;&gt;Python에서의 힙&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python에서는 &lt;code&gt;heapq&lt;/code&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;code&gt;heapq.heappush(heap, item)&lt;/code&gt;&amp;rarr;&lt;code&gt;heap&lt;/code&gt; 에 &lt;code&gt;item&lt;/code&gt; 을 삽입한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;heapq.heappop(heap)&lt;/code&gt;&amp;rarr;&lt;code&gt;heap&lt;/code&gt;에서 값을 삭제한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;heapq.heapify(x)&lt;/code&gt;&amp;rarr;리스트 &lt;code&gt;x&lt;/code&gt;를 힙으로 변환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, &lt;code&gt;heapq&lt;/code&gt; 패키지는 &lt;u&gt;&lt;b&gt;최소 힙만을 지원&lt;/b&gt;&lt;/u&gt;한다. 따라서, 최대 힙으로 이용하기 위해서는 값에 &amp;ldquo;-&amp;rdquo;를 붙여 힙에 넣는 방식을 사용할 수 있다. ($y=-x$에서 $x$가 커질수록 $y$는 작아지는 것을 이용할 수 있다!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히, 위와 같이 구성된 &lt;code&gt;heap&lt;/code&gt; 객체의 맨 처음 인덱스에는 항상 우선순위 최소인 값이 존재하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;References&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.encrypted.gg/1015&quot;&gt;바킹독의 실전 알고리즘 0x17강: 우선순위 큐&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/ko/3/library/heapq.html&quot;&gt;https://docs.python.org/ko/3/library/heapq.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84_%ED%81%90&quot;&gt;https://ko.wikipedia.org/wiki/우선순위_큐&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://suyeon96.tistory.com/31&quot;&gt;https://suyeon96.tistory.com/31&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Algorithm/개념 정리</category>
      <category>알고리즘</category>
      <category>우선순위 큐</category>
      <category>힙</category>
      <author>r-jelly</author>
      <guid isPermaLink="true">https://r-jelly.tistory.com/15</guid>
      <comments>https://r-jelly.tistory.com/15#entry15comment</comments>
      <pubDate>Wed, 29 Apr 2026 17:33:40 +0900</pubDate>
    </item>
    <item>
      <title>이진 탐색 트리 (Binary Search Tree)</title>
      <link>https://r-jelly.tistory.com/14</link>
      <description>&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1067&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sDApT/dJMcajhHRGI/8SuVvEYk3np6iKdWj6bbm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sDApT/dJMcajhHRGI/8SuVvEYk3np6iKdWj6bbm1/img.png&quot; data-alt=&quot;이진 탐색 트리 예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sDApT/dJMcajhHRGI/8SuVvEYk3np6iKdWj6bbm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsDApT%2FdJMcajhHRGI%2F8SuVvEYk3np6iKdWj6bbm1%2Fimg.png&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;400&quot; height=&quot;333&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1067&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이진 탐색 트리 예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 노드의 자식 노드가 최대 2개인 이진 트리&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;이진 탐색 트리는 최적의 경우, 노드의 개수가 $N$일때 삽입, 삭제, 탐색 모두 $O(\log N)$의 시간복잡도를 갖게 된다. 일반적으로는 트리의 높이를 $h$라고 했을 때, 세 작업 모두 $O(h)$의 시간복잡도를 갖게 된다.&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;이진 탐색 트리의 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트리의 기본적인 뼈대를 Python으로 구성하면 아래와 같다. 이때 탐색, 삽입, 제거의 동작을 구현해보자.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;class Node:
    def __init__(self, val):
        self.val = val
        self.left_child = None
        self.right_child = None

class BinarySearchTree:
    def __init__(self, root):
        self.root = Node(root)&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;탐색 (Search)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이진 탐색 트리에서의 탐색은 이진 탐색에서의 탐색과 동일하다. 즉, Root 노드부터 시작해서 탐색하고자 하는 값이 해당 노드보다 작으면 왼쪽 자식 노드로, 크다면 오른쪽 자식 노드로 이동하여 구하고자 하는 값이 나올 때까지 반복하면 된다. 이때 더 이상 이동할 수 없다면, 탐색하려는 값이 트리에 존재하지 않는 것으로 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;class BinarySearchTree:
    ...
    def search(self, value):
        cur_node = self.root
        while cur_node != None:
            if cur_node.val == value:
                return cur_node
            elif cur_node.val &amp;gt; value:
                cur_node = cur_node.left_child
            else:
                cur_node = cur_node.right_child
        return None&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;삽입 (Insert)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Root 노드부터 현재 노드와 입력하고자 하는 값의 대소비교를 통해서 제일 아래쪽 자식 노드(Leaf Node)까지 내려간 후, 해당 Leaf 노드의 값보다 입력할 값이 작으면 왼쪽 자식 노드로, 크면 아래쪽 자식 노드로 삽입하면 된다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;class BinarySearchTree:
    ...
    def insert(self, value):
        cur_node = self.root
        while cur_node.left_child or cur_node.right_child:
            if cur_node.val &amp;gt; value:
                cur_node = cur_node.left_child
            else:
                cur_node = cur_node.right_child

        if cur_node.val &amp;gt; value:
            cur_node.left_child = Node(value)
        else:
            cur_node.right_child = Node(value)&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;삭제 (Delete)&lt;/b&gt;&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;삭제하려는 노드의 자식 노드가 0개인 경우, 단순히 해당 노드를 제거하는 것으로 삭제 작업이 끝나게 된다.&lt;/li&gt;
&lt;li&gt;삭제하려는 노드의 자식 노드가 1개인 경우, 해당 노드를 지우고, 해당 노드의 자식 노드를 해당 노드의 부모 노드와 연결하면 끝난다.&lt;/li&gt;
&lt;li&gt;삭제하려는 노드의 자식 노드가 2개인 경우, 지우려는 노드의 값보다 큰 숫자 중에 제일 작은 숫자를 가진 노드(=Successor)를 해당 위치로 옮기고, 원래 successor에 해당하는 위치의 노드를 삭제한다.&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;&lt;u&gt;&lt;b&gt;삭제의 3번 경우는 어떻게 가능한 것일까?&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제하려는 노드의 Successor는 항상 자식이 없거나, 오른쪽 자식 노드만 존재(=자식 노드가 1개)하므로 위와 같은 동작이 성립할 수 있다. 만약, Successor의 자식 노드가 2개라고 한다면, 이는 Successor 노드보다 작은 값을 갖는 노드가 존재한다는 것으로 이는 Successor 노드의 정의에 모순하게 된다. 즉, &lt;b&gt;Successor 노드는 특정 노드의 오른쪽 서브트리의 제일 왼쪽에 위치한 노드&lt;/b&gt;로 볼 수 있으므로 해당 노드의 삭제는 1번과 2번 경우로 모두 해결 할 수 있게 된다.&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;한계점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이진 탐색 트리는 최적의 경우, 노드의 수가 $N$일 때 모든 동작이 $O(\log N)$의 시간복잡도로 해결될 수 있다. 그러나 최악의 경우에는 이진 탐색 트리의 Root 노드 값이 트리의 최대값 또는 최소값이 될 수 있다. 이런 경우 트리의 높이 $h = N$이 되어 모든 동작이 $O(N)$이 된다. 즉, 최악의 경우에는 연결 리스트와 동일한 구조라고 볼 수 있으며 모든 연산이 비효율적으로 동작하게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;789&quot; data-origin-height=&quot;391&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eisc6m/dJMb997gM16/WXdqDarB1fA3jLKkOgQ2q1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eisc6m/dJMb997gM16/WXdqDarB1fA3jLKkOgQ2q1/img.png&quot; data-alt=&quot;이진 탐색 트리의 시간복잡도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eisc6m/dJMb997gM16/WXdqDarB1fA3jLKkOgQ2q1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Feisc6m%2FdJMb997gM16%2FWXdqDarB1fA3jLKkOgQ2q1%2Fimg.png&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;789&quot; height=&quot;391&quot; data-origin-width=&quot;789&quot; data-origin-height=&quot;391&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이진 탐색 트리의 시간복잡도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자가 균형 이진 탐색 트리 (Self-Balancing BST)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 일반 이진 탐색 트리의 한계점을 해결하기 위해, 트리 높이의 불균형이 발생할 때 마다 트리의 구조를 균형으로 바꿔 항상 시간복잡도를 $O(\log N)$으로 유지하도록 하는 트리이다. 대표적인 종류로는 Red-Black Tree, AVL Tree 등이 있다. 아래와 같은 Rotation 동작을 통해서 트리의 높이를 모든 경로에서 항상 균일하게 유지하도록 할 수 있다. 이러한 자가 균형 이진 탐색 트리를 사용할 때 비로소 이진 탐색 트리를 효율적으로 사용할 수 있게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oEFAc/dJMcadaHhKv/ggaOaYyGBk2SmkXnNrs6R0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oEFAc/dJMcadaHhKv/ggaOaYyGBk2SmkXnNrs6R0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oEFAc/dJMcadaHhKv/ggaOaYyGBk2SmkXnNrs6R0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoEFAc%2FdJMcadaHhKv%2FggaOaYyGBk2SmkXnNrs6R0%2Fimg.png&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;400&quot; height=&quot;222&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;532&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;References&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EC%9D%B4%EC%A7%84_%ED%83%90%EC%83%89_%ED%8A%B8%EB%A6%AC&quot;&gt;https://ko.wikipedia.org/wiki/이진_탐색_트리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ratsgo.github.io/data%20structure&amp;amp;algorithm/2017/10/22/bst/&quot;&gt;https://ratsgo.github.io/data%20structure&amp;amp;algorithm/2017/10/22/bst/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://engineerinsight.tistory.com/321#%F0%9F%92%8B%C2%A0%EB%85%B8%EB%93%9C%EC%9D%98%20successor%20(%ED%9B%84%EC%9E%84%EC%9E%90)-1&quot;&gt;https://engineerinsight.tistory.com/321#  노드의 successor (후임자)-1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Algorithm/개념 정리</category>
      <category>알고리즘</category>
      <category>이진탐색트리</category>
      <category>자가균형트리</category>
      <author>r-jelly</author>
      <guid isPermaLink="true">https://r-jelly.tistory.com/14</guid>
      <comments>https://r-jelly.tistory.com/14#entry14comment</comments>
      <pubDate>Tue, 28 Apr 2026 21:31:49 +0900</pubDate>
    </item>
    <item>
      <title>투 포인터 (Two Pointers)</title>
      <link>https://r-jelly.tistory.com/13</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;투 포인터&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;투 포인터 알고리즘은, 배열이나 문자열에서 두 개의 포인터(=인덱스)를 사용해서 탐색 범위를 줄이는 알고리즘. 특히, 다중 반복문을 사용해서 모든 쌍을 확인해야 하는 경우에 문제를 더 효율적으로 풀 수 있도록 해준다.&lt;br&gt;&amp;nbsp;&lt;br&gt;만약, 배열에서 모든 쌍을 확인해야 한다면 이중 for문으로 $O(N^2)$의 시간복잡도를 통해 확인할 수 있지만, 조건이 존재한다면 투 포인터를 이용해 포인터를 조건에 따라 움직이며 $O(N)$의 시간복잡도만으로 해결할 수 있다. 즉, 모든 경우를 확인하는 대신에 조건에 맞는 경우를 찾아서 순회하는 알고리즘이라고 할 수 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이러한 투 포인터로 풀 수 있는 문제 중 다수는 이분 탐색으로도 풀 수 있고, 이분 탐색으로 풀 수 있는 문제도 마찬가지로 투 포인터로 풀 수 있는 문제가 많다.&lt;br&gt;&amp;nbsp;&lt;br&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;code&gt;start, end&lt;/code&gt;가 모두 첫 번째 원소부터 시작해서 이동하는 경우&lt;/li&gt; 
 &lt;li&gt;&lt;code&gt;start&lt;/code&gt; 는 배열의 처음, &lt;code&gt;end&lt;/code&gt;는 배열의 끝을 나타내어 범위를 좁혀나가는 경우&lt;/li&gt; 
 &lt;li&gt;첫 번째 탐색에서 조건을 만족하는 &lt;code&gt;start&lt;/code&gt; 의 위치를 찾고, 해당 위치로부터 &lt;code&gt;end&lt;/code&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;즉, 각 &lt;code&gt;start&lt;/code&gt; 위치의 값에 대해서 모든 가능한 &lt;code&gt;end&lt;/code&gt; 위치를 보는 것이 아니라, 이전 시점의 &lt;code&gt;end&lt;/code&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;특정 부분 합 이상을 만족하는 최소 길이의 수열 찾기&lt;/h3&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt; 
 &lt;p data-ke-size=&quot;size16&quot;&gt;길이 N짜리 자연수 수열이 주어졌을 때, 연속된 수들의 부분합 중에 그 합이 S 이상인 것 중 가장 짧은 수열의 길이를 구하시오.&lt;/p&gt; 
&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;위 문제를 보고 제일 먼저 떠올릴 수 있는 알고리즘은 DP에서 다뤘던 Prefix Sum이다. 그러나, 모든 위치에서의 Prefix Sum을 구해도, 수열의 처음 위치와 끝 위치를 탐색하기 위해서는 이중 for문을 이용해 모든 경우의 수에서 부분합의 값을 구해 비교해야 한다. 즉, $O(N^2)$의 시간복잡도로 해결할 수 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그러나 투 포인터를 사용한다면 $O(N)$으로 구현할 수 있다.&lt;/p&gt;&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt; 
 &lt;li&gt;&lt;code&gt;start, end&lt;/code&gt; 포인터를 배열의 맨 처음 인덱스로 설정하고, 현재의 부분합 &lt;code&gt;cur_S = arr[start]&lt;/code&gt;로 설정한다.&lt;/li&gt; 
 &lt;li&gt;&lt;code&gt;cur_S &amp;lt; S&lt;/code&gt;인 경우, &lt;code&gt;cur_S&lt;/code&gt;에다 &lt;code&gt;end&lt;/code&gt;에 위치한 값을 더하고 &lt;code&gt;end&lt;/code&gt;를 오른쪽으로 한 칸 옮긴다.&lt;/li&gt; 
 &lt;li&gt;&lt;code&gt;cur_S &amp;gt;= S&lt;/code&gt;인 경우, 현재 수열의 길이 &lt;code&gt;end - start + 1&lt;/code&gt;을 기존 수열의 최소 길이와 비교해서 더 짧은 값을 최솟값으로 둔다. 그리고 &lt;code&gt;cur_S&lt;/code&gt;에다 &lt;code&gt;start&lt;/code&gt; 에 위치한 값을 빼고 &lt;code&gt;start&lt;/code&gt; 를 오른쪽으로 한 칸 옮긴다.&lt;/li&gt; 
 &lt;li&gt;2~3을 반복하다가, &lt;code&gt;end&lt;/code&gt;가 배열의 인덱스를 벗어나게 되면 탐색을 종료한다.&lt;/li&gt; 
&lt;/ol&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&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;code&gt;arr[start] + ... + arr[end] &amp;lt; S&lt;/code&gt; 인 경우, &lt;code&gt;end&lt;/code&gt; 를 오른쪽으로 이동시키면 새로운 값이 더해지므로 &lt;code&gt;S&lt;/code&gt; 에 가까워진다.&lt;/li&gt; 
 &lt;li&gt;&lt;code&gt;arr[start] + ... + arr[end] &amp;gt;= S&lt;/code&gt; 인 경우, &lt;code&gt;start&lt;/code&gt; 를 오른쪽으로 이동시켰을 때, 더 작은 길이의 수열이 되므로 항상 옮기지 않았을 때보다 좋은 선택이다.&lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;Python으로 구현하면 아래와 같다.&lt;/p&gt;&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;def min_sequence_len(num_list: List[int], S: int) -&amp;gt; int:
    &quot;&quot;&quot;
    Args:
        num_list: 자연수로 구성된 수열
        S: 만족해야하는 최소 부분합
    Return:
        int: S 이상의 부분합을 갖는 수열 중 최소 길이
    &quot;&quot;&quot;
    start = 0
    end = 0
    cur_S = num_list[0]
    min_len = float('inf')

    while True:
        if cur_S &amp;lt; S:
            end += 1
            if end &amp;gt;= len(num_list):
                break
            cur_S += num_list[end]
        else:
            min_len = min(min_len, end - start + 1)
            cur_S -= num_list[start]
            start += 1

    return min_len if min_len != float('inf') else 0&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;References&lt;/h2&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;&lt;a href=&quot;https://blog.encrypted.gg/1004&quot; target=&quot;_self&quot;&gt;&lt;span&gt;바킹독의 실전 알고리즘 0x14: 투 포인터&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://sehun5515.tistory.com/62&quot; target=&quot;_self&quot;&gt;&lt;span&gt;https://sehun5515.tistory.com/62&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://bytebytego.com/courses/coding-patterns/two-pointers/introduction-to-two-pointers?fpr=javarevisited&quot; target=&quot;_self&quot;&gt;&lt;span&gt;https://bytebytego.com/courses/coding-patterns/two-pointers/introduction-to-two-pointers?fpr=javarevisited&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;</description>
      <category>Algorithm/개념 정리</category>
      <category>알고리즘</category>
      <category>투 포인터</category>
      <author>r-jelly</author>
      <guid isPermaLink="true">https://r-jelly.tistory.com/13</guid>
      <comments>https://r-jelly.tistory.com/13#entry13comment</comments>
      <pubDate>Wed, 22 Apr 2026 19:09:33 +0900</pubDate>
    </item>
    <item>
      <title>이진 탐색 (Binary Search)</title>
      <link>https://r-jelly.tistory.com/12</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;이진 탐색&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이미 정렬되어 있는 데이터&lt;/b&gt;에서 특정 데이터를 찾기 위해, &lt;b&gt;탐색 범위를 계속 절반으로 나눠&lt;/b&gt; 탐색하는 알고리즘&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이진 탐색은 배열에서 특정 데이터를 탐색하기 위한 알고리즘이다. 배열의 모든 원소를 탐색하면서 $O(N)$의 시간복잡도로 원하는 데이터를 찾는 선형 탐색과는 다르게, 한 번 탐색할 때마다 범위를 절반으로 줄여가면서 탐색하므로 $O(\log N)$의 시간복잡도를 갖게 된다. 그러나, &lt;u&gt;&lt;b&gt;이진 탐색은 반드시 정렬된 배열에서만 사용&lt;/b&gt;&lt;/u&gt;할 수 있으므로, &lt;b&gt;정렬되지 않은 데이터에 대해서는 정렬을 하기 위한 시간복잡도가 추가로 필요&lt;/b&gt;하게 된다. &lt;a href=&quot;https://www.cs.usfca.edu/~galles/visualization/Search.html&quot;&gt;해당 링크&lt;/a&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;이진 탐색은 변수 3개를 이용해서 수행한다. 탐색 범위의 시작을 나타내는 &lt;code&gt;start&lt;/code&gt;, 탐색 범위의 끝을 나타내는 &lt;code&gt;end&lt;/code&gt;, 그리고 탐색 범위의 중간점을 나타내는 &lt;code&gt;mid&lt;/code&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;code&gt;start&lt;/code&gt;, 끝 점을 &lt;code&gt;end&lt;/code&gt; 로 두고, &lt;code&gt;mid = (start + end) / 2&lt;/code&gt; 로 정의한다.&lt;/li&gt;
&lt;li&gt;찾으려고 하는 값을 &lt;code&gt;target&lt;/code&gt;이라고 했을 때, &lt;code&gt;target&lt;/code&gt;과 &lt;code&gt;mid&lt;/code&gt; 위치에 존재하는 값의 대소를 비교한다.&lt;/li&gt;
&lt;li&gt;만약, &lt;code&gt;target &amp;lt; arr[mid]&lt;/code&gt; 라면, &lt;code&gt;mid&lt;/code&gt; 왼쪽에 &lt;code&gt;target&lt;/code&gt; 데이터가 존재하는 것으로 생각할 수 있다. 따라서, &lt;code&gt;end = mid - 1&lt;/code&gt;로 설정한다.&lt;/li&gt;
&lt;li&gt;만약, &lt;code&gt;target &amp;gt; arr[mid]&lt;/code&gt;라면, &lt;code&gt;mid&lt;/code&gt; 오른쪽에 &lt;code&gt;target&lt;/code&gt; 데이터가 존재하는 것으로 생각할 수 있다. 따라서, &lt;code&gt;start = mid + 1&lt;/code&gt; 로 설정한다.&lt;/li&gt;
&lt;li&gt;2~4의 과정을 반복하다가, &lt;code&gt;target == arr[mid]&lt;/code&gt;가 되는 순간 탐색을 종료한다.&lt;/li&gt;
&lt;li&gt;만약 &lt;code&gt;end &amp;lt; start&lt;/code&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;Python으로 구현한 이진 탐색의 코드는 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def binary_search(num_list: List[int], target: int) -&amp;gt; int:
    &quot;&quot;&quot;
    Args:
        num_list: 오름차순으로 정렬된 숫자의 배열
        target: 찾고자 하는 데이터
    Return:
        int: target이 위치하는 인덱스, 존재하지 않는다면 -1
    &quot;&quot;&quot;
    start = 0
    end = len(num_list)-1

    while start &amp;lt;= end:
        mid = (start + end) // 2

        if target &amp;lt; num_list[mid]:
            end = mid - 1
        elif target &amp;gt; num_list[mid]:
            start = mid + 1
        else:
            return mid
    return -1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이진 탐색 주의사항&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;반드시 배열이 정렬되어 있어야 한다: 알고리즘이 &lt;code&gt;target&lt;/code&gt;의 위치에 기반해 동작하므로, 오름차순 or 내림차순으로 반드시 배열이 정렬되어야 한다. 만약, 내림차순 정렬이라면 위의 3번과 4번 과정을 신경써주면 된다.&lt;/li&gt;
&lt;li&gt;무한루프에 빠지지 않게 &lt;code&gt;mid&lt;/code&gt; 값을 정해야 한다: 만약 구간이 정확히 절반으로 나누어지지 않는 경우, 문제에 따라 무한루프에 빠질 수 있다. Lower Bound나 Upper Bound 탐색 문제에서 이 문제를 더욱 신경써야 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Lower Bound &amp;amp; Upper Bound&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 이진 탐색은 찾고자 하는 값이 없다면, 그대로 탐색에 실패하게 된다. 그러나, Lower Bound나 Upper Bound와 같은 문제는 이와 상관없이 찾고자 하는 값 이상/초과의 위치를 탐색하게 된다. 이는 탐색하고자 하는 값이 없는 경우나, 동일한 값이 여러 개 들어있는 경우 모두에서 사용가능하다.&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;Lower Bound&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;찾고자 하는 값 이상이 처음 나타나는 위치를 탐색&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 위에서는 &lt;code&gt;target == arr[mid]&lt;/code&gt; 인 &lt;code&gt;mid&lt;/code&gt; 값을 찾았다면, Lower Bound 문제는 &lt;code&gt;target &amp;gt; arr[mid - 1]&lt;/code&gt;이면서 &lt;code&gt;target &amp;lt;= arr[mid]&lt;/code&gt; 를 만족하는 &lt;code&gt;mid&lt;/code&gt; 값을 찾는 것이 목표이다.&lt;br /&gt;e.g.) &lt;code&gt;10, 13, 15, 15, 17, 17, 19&lt;/code&gt; 에서 &lt;code&gt;15&lt;/code&gt;의 Lower Bound는 15가 처음 등장하는 위치&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;code&gt;target&lt;/code&gt; 보다 작다면, &amp;ldquo;배열의 마지막 인덱스 + 1&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;Python으로 구현하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def lower_bound(num_list: List[int], target: int) -&amp;gt; int:
    &quot;&quot;&quot;
    Args:
        num_list: 오름차순으로 정렬된 숫자의 배열
        target: 찾고자 하는 데이터
    Return:
        int: target 이상의 값이 처음으로 등장하는 인덱스
    &quot;&quot;&quot;
    start = 0
    end = len(num_list)

    while start &amp;lt; end:
        mid = (start + end) // 2

        if target &amp;lt;= num_list[mid]:
            end = mid
        elif target &amp;gt; num_list[mid]:
            start = mid + 1

    return end&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Upper Bound&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;찾고자 하는 값보다 큰 값이 처음 나타나는 위치를 탐색&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lower Bound는 찾고자 하는 값인 &lt;code&gt;target&lt;/code&gt;을 탐색 범위에 포함했다면, Upper Bound는 찾고자 하는 값보다 큰 값이 처음 등장하는 위치를 탐색하는 것이다. 즉, &lt;code&gt;target &amp;gt;= arr[mid - 1]&lt;/code&gt;이면서 &lt;code&gt;target &amp;lt; arr[mid]&lt;/code&gt; 를 만족하는 &lt;code&gt;mid&lt;/code&gt; 값을 찾는 것이 목표이다.&lt;br /&gt;e.g.) &lt;code&gt;10, 13, 15, 15, 17, 17, 19&lt;/code&gt; 에서 &lt;code&gt;15&lt;/code&gt;의 Upper Bound는 15가 마지막으로 등장하는 위치 + 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;Upper Bound도 마찬가지로, 배열에 들어있는 모든 값들이 &lt;code&gt;target&lt;/code&gt; 보다 작다면, &amp;ldquo;배열의 마지막 인덱스 + 1&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;Python으로 구현하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def upper_bound(num_list: List[int], target: int) -&amp;gt; int:
    &quot;&quot;&quot;
    Args:
        num_list: 오름차순으로 정렬된 숫자의 배열
        target: 찾고자 하는 데이터
    Return:
        int: target보다 큰 값이 처음으로 등장하는 인덱스
    &quot;&quot;&quot;
    start = 0
    end = len(num_list)

    while start &amp;lt; end:
        mid = (start + end) // 2

        if target &amp;lt; num_list[mid]:
            end = mid
        elif target &amp;gt;= num_list[mid]:
            start = mid + 1

    return end&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동일한 값의 개수 찾기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 Lower Bound &amp;amp; Upper Bound를 이용해서, 정렬된 배열 내에서 동일한 값이 몇 개 존재하는지를 빠르게 계산할 수 있다.&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;Upper Bound는 찾는 값보다 큰 값이 처음 등장한 위치를 탐색하고, Lower Bound는 찾는 값 이상의 값이 처음 등장한 위치를 탐색하므로, &quot;Upper Bound 값 - Lower Bound 값&quot;이 찾고자 하는 값의 등장 횟수가 된다. 만약 이 값이 0이면, 해당 배열에 찾는 값이 존재하지 않는 경우로 생각할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;References&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.encrypted.gg/985&quot;&gt;바킹독의 실전 알고리즘 0x13: 이분탐색&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.geeksforgeeks.org/dsa/binary-search/&quot;&gt;https://www.geeksforgeeks.org/dsa/binary-search/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@kimdukbae/%EC%9D%B4%EB%B6%84-%ED%83%90%EC%83%89-%EC%9D%B4%EC%A7%84-%ED%83%90%EC%83%89-Binary-Search&quot;&gt;https://velog.io/@kimdukbae/%EC%9D%B4%EB%B6%84-%ED%83%90%EC%83%89-%EC%9D%B4%EC%A7%84-%ED%83%90%EC%83%89-Binary-Search&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://12bme.tistory.com/120&quot;&gt;https://12bme.tistory.com/120&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Algorithm/개념 정리</category>
      <category>알고리즘</category>
      <category>이분탐색</category>
      <category>이진탐색</category>
      <author>r-jelly</author>
      <guid isPermaLink="true">https://r-jelly.tistory.com/12</guid>
      <comments>https://r-jelly.tistory.com/12#entry12comment</comments>
      <pubDate>Wed, 22 Apr 2026 17:56:20 +0900</pubDate>
    </item>
    <item>
      <title>그리디 알고리즘 (Greedy Algorithm)</title>
      <link>https://r-jelly.tistory.com/11</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;그리디 알고리즘&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매 선택 시, 현재 시점에서 가장 최적인 값만을 선택하여 결과를 도출하는 알고리즘&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말 그대로, &lt;u&gt;&lt;b&gt;현재 나한테 제일 이익이 되는 것은 무엇일지 보고 해당 값을 선택&lt;/b&gt;&lt;/u&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;다이나믹 프로그래밍(DP)와 같이, 매 순간의 최적값이 전체 문제의 최적값이라는 점은 동일하지만, DP와 다르게 꼭 세부 문제들이 동일한 구조는 아니어도 된다. 또한, 현재 시점에서의 최적 해결 방법만을 찾으면 되는 알고리즘이므로, 백트래킹과 같이 모든 경로를 탐색할 필요는 없기 때문에 연산 속도 측면에서 이득을 볼 수 있다. 그러나 반대로 생각하면 현재 시점에서의 최적 방법의 합이 전체적으로는 최적 해결 방법이 아닐 수도 있다.&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;&lt;b&gt;Optimal Substructure(최적 부분 구조)&lt;/b&gt;: 전체 문제의 최적 해결 방법은 부분 문제에 대한 최적 해결 방법으로 구성되어야 한다. &amp;rarr; DP와 동일!&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Greedy Choice Property(탐욕 선택 속성)&lt;/b&gt;: 이전에 결정한 최적의 선택이, 이후의 최적 선택에 영향을 주지 않아야 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 그리디 알고리즘은 특정한 방법으로 풀 수 있는 것이 아니라, 문제의 상황에 따라 아이디어를 파악하고 알고리즘을 설계해야 한다. 그리디 알고리즘 문제에 접근하는 방식은 1) 관찰을 통해서 해결할 탐색 범위를 줄이는 방법을 고안하고, 2) 줄어든 탐색 범위에서 최적해를 선택했을 때, 이것이 전체 범위에서도 최적해인지를 증명하는 것과 같이 두 단계로 이루어진다. 즉, 가설 설정 &amp;rarr; 증명의 단계로 접근하는 방식을 가져야 한다.&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;거스름돈 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거스름돈 문제는 그리디 알고리즘을 사용할 수 있는 대표적인 문제이다. (&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/12907&quot;&gt;프로그래머스 예시&lt;/a&gt;)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Q. 거스름돈 N원을 500원, 100원, 50원, 10원 동전 만으로 거슬러줄 때, 제일 동전을 적게 사용하는 경우는 몇 개의 동전을 사용해야 하는가?&lt;/p&gt;
&lt;/blockquote&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;동전 개수를 최소화 하려면, 500원 &amp;rarr; 100원 &amp;rarr; 50원 &amp;rarr; 10원 순으로 동전을 사용해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 명제로부터 &amp;ldquo;동전을 최소로 소모하려면, 100원은 4개 이하, 50원은 1개 이하, 10원은 4개 이하로 사용해야한다.&amp;rdquo;라는 다른 정리를 도출할 수 있다. 이를 100원에 대해서 우선 증명하면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약, 100원을 5개 이상 사용했을 때, 동전을 최소로 사용할 수 있다고 하자.&lt;/li&gt;
&lt;li&gt;100원을 5개 이상 사용하는 순간, 이는 500원 동전으로 대체될 수 있다.&lt;/li&gt;
&lt;li&gt;이때 100원을 5개 사용하는 것보다, 500원을 1개 사용하는 것이 항상 더 적은 동전을 사용하게 된다. 따라서 위 가정은 모순이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;50원, 10원의 경우도 위와 같이 증명할 수 있다. 따라서, 100원, 50원, 10원 동전을 사용하는 최대 개수는 정해지게 되고, 이 최대 개수를 모두 사용했을 때가 490원이므로, 해당 금액 이상의 거스름돈에 대해서는 500원 동전을 우선적으로 사용하지 않는다면 아래 금액의 동전을 추가로 소모해야 한다. 따라서, 500원 동전의 경우를 제일 먼저 생각해야 한다. 차례로, 100원, 50원, 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;Python으로 위 문제를 해결하는 코드를 작성하면 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;def solution(N: int):
    &quot;&quot;&quot;N: 거스름돈의 크기&quot;&quot;&quot;
    coin_list = [500, 100, 50, 10]
    count = 0

    for coin in coin_list:
        while N &amp;gt;= coin:
            N -= coin
            count += 1

    return count&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;u&gt;&lt;b&gt;그렇다면 모든 거스름돈 문제에서 그리디 알고리즘을 사용하면 될까?&lt;/b&gt;&lt;/u&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;아쉽게도, 사용할 수 있는 동전이 배수관계일 때만 그리디 알고리즘이 성립하게 된다. 만약, 1/9/10원 동전이 있고, 거스름돈이 18원이라고 하자. 큰 동전부터 선택하는 방법을 이용한다면 (10, 1, 1, 1, 1, 1, 1, 1, 1) 총 9개의 동전을 사용해야 하지만, 실제로는 (9, 9) 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;&amp;rArr; 즉, 지금 당장은 손해여도 나중에 이득이 되는 경우가 존재한다면 이는 그리디 알고리즘으로 해결할 수 없다!&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;그리디 알고리즘으로 풀 수 없는 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리디 알고리즘으로 풀 수 있는 문제는 (시간복잡도는 느리지만) DP나 백트래킹과 같은 방법으로 해결할 수는 있다. 그렇지만, 반대로 그리디 알고리즘으로 풀 수 있을 것 같으나 풀 수 없는 문제가 존재한다.&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;0-1 Kanpsack 알고리즘, Parametric Search와 같이 그리디로 풀 수 없는데 그리디 알고리즘으로 해결할 수 있는 것처럼 보이는 문제들을 빠르게 파악할 수 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;References&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.encrypted.gg/975&quot;&gt;바킹독의 실전 알고리즘 0x11강: 그리디&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://memodayoungee.tistory.com/103&quot;&gt;https://memodayoungee.tistory.com/103&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://6mini.github.io/computer%20science/2021/12/04/dp-greedy/&quot;&gt;https://6mini.github.io/computer science/2021/12/04/dp-greedy/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Algorithm/개념 정리</category>
      <category>그리디</category>
      <category>알고리즘</category>
      <author>r-jelly</author>
      <guid isPermaLink="true">https://r-jelly.tistory.com/11</guid>
      <comments>https://r-jelly.tistory.com/11#entry11comment</comments>
      <pubDate>Wed, 22 Apr 2026 16:29:56 +0900</pubDate>
    </item>
    <item>
      <title>다이나믹 프로그래밍 (Dynamic Programming)</title>
      <link>https://r-jelly.tistory.com/10</link>
      <description>&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;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이름의 유래?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dynamic에는 별 뜻이 없고, 리처드 벨만이 &amp;ldquo;Dynamic&amp;rdquo; 이라는 단어가 멋있어 보여서 붙였다고 한다. Programming은 최적화 연구에서 최적의 프로그램을 찾을 때 쓰는 단어라고 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 다이나믹 프로그래밍(Dynamic Programming, DP)은 아래 두 가지 조건을 만족하는 문제에서 사용할 수 있다.&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;Optimal Substructure(최적 부분 구조)&lt;/b&gt;: 전체 문제의 최적 해결 방법은 부분 문제에 대한 최적 해결 방법으로 구성되어야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Overlapping Subproblems(부분 문제 반복)&lt;/b&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;다이나믹 프로그래밍으로 문제를 푸는 과정은 다음과 같다.&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;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;u&gt;&lt;b&gt;부분 문제를 계산해서 따로 저장하고, 반복적으로 해당 부분 문제의 결과를 필요로 할 때 해당 값을 사용&lt;/b&gt;&lt;/u&gt;하는 방식으로 풀 수 있다. 이러한 기법을 &lt;b&gt;Memoization&lt;/b&gt; 또는 &lt;b&gt;Caching&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;DP를 구현하는 방식에는 Top-Down, Bottom-Up 두 가지 방식이 있다.&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;Top-Down&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재귀함수를 통해서 큰 문제를 계속해서 쪼갠 후, 결과 값을 저장해서 이를 재활용하는 방식&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;피보나치 수열에서 $F(N)=F(N-1)+F(N-2)$을 해결할 때와 같이 특정 N에서의 피보나치 값을 반복해서 구해야 하는 경우, 제일 먼저 계산한 값을 테이블에 저장해서 나중에 이 값이 필요할 때 꺼내서 사용할 수 있다.&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;아래는 피보나치 수열의 N번째 숫자를 찾는 문제를 Top-Down 방식으로 해결한 코드이다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;dp = [0] * 10000

def fibo(N: int):
    if N &amp;lt;= 1:
        return N

    if dp[N] != 0:
        return dp[N]

    dp[N] = fibo(N-1) + fibo(N-2)
    return dp[N]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Bottom-Up&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복문을 이용해서 작은 문제에서부터 계산을 수행하고 이를 누적해 큰 문제를 해결하는 방식&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DP[0]&lt;/code&gt;이 초기값일 때, 점화식을 통해서 &lt;code&gt;DP[1], DP[2], ..., DP[N]&lt;/code&gt;까지의 값을 차례대로 구해나가는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Top-Down 방식과는 반대로 모든 부분을 다 구해서 이용해야 할 때는 Bottom-Up 방식이 유리하며, Top-Down 방식은 재귀적으로 계산하기 때문에 불필요한 스택 메모리 공간을 낭비할 수 있게 된다.&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;아래는 피보나치 수열의 N번째 숫자를 찾는 문제를 Bottom-Up 방식으로 해결한 코드이다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def fibo(N: int):
    dp = [0] * 10000
    dp[1] = 1
    dp[2] = 1

    for i in range(3, N):
        dp[i] = dp[i-1] + dp[i-2]

    return dp[N-1] + dp[N-2]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Prefix Sum&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prefix Sum은 특정 구간의 합을 반복적으로 계산해야 할 때, 미리 처음부터 특정 위치까지의 합을 계산한 Table을 만들어서 이를 반복적으로 이용하는 것이다. 작은 부분 문제를 계산하여 이후에 재사용하기 때문에, Dynamic Programming의 일종이라고 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;614&quot; data-origin-height=&quot;564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPkPGM/dJMcagkPR7G/HZlTCKyEBLVS05qto8G0F0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPkPGM/dJMcagkPR7G/HZlTCKyEBLVS05qto8G0F0/img.png&quot; data-alt=&quot;출처: https://venkys.io/articles/details/prefix-sum&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPkPGM/dJMcagkPR7G/HZlTCKyEBLVS05qto8G0F0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPkPGM%2FdJMcagkPR7G%2FHZlTCKyEBLVS05qto8G0F0%2Fimg.png&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;600&quot; height=&quot;551&quot; data-origin-width=&quot;614&quot; data-origin-height=&quot;564&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://venkys.io/articles/details/prefix-sum&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python으로 Prefix Sum을 구현한 코드는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def prefix_sum(num_list: List[int]):
    sum_list = [0] * len(num_list)
    sum_list[0] = num_list[0]

    for i in range(1, len(num_list)):
        sum_list[i] = sum_list[i-1] + num_list[i]

    return sum_list&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 구현했다면, &lt;code&gt;sum_list&lt;/code&gt; 의 &lt;code&gt;i&lt;/code&gt;번째 위치에는 &lt;code&gt;num_list&lt;/code&gt;의 맨 처음 값부터 &lt;code&gt;i&lt;/code&gt;번째 값까지 모두 더한 값이 저장된다. 이때, &lt;code&gt;num_list&lt;/code&gt;의 &lt;code&gt;a&lt;/code&gt;번째 위치부터 &lt;code&gt;b&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;우리는 &lt;code&gt;num_list[a] + num_list[a+1] + ... + num_list[b-1] + num_list[b]&lt;/code&gt; 의 값을 구해야 한다. 이때 &lt;code&gt;sum_list[i]&lt;/code&gt;가 &lt;code&gt;sum_list[i] = num_list[0] + num_list[1] + ... + num_list[i]&lt;/code&gt;와 같이 정의되므로, 우리가 구하고자 하는 값은 다음과 같이 구할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;sum_list[b] - sum_list[a-1] 
= (num_list[0] + ... + num_list[b]) - (num_list[0] + ... + num_list[a-1])
= num_list[a] + ... + num_list[b]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;code&gt;num_list&lt;/code&gt; 내 모든 구간의 합을 $O(1)$의 단순 뺄셈으로 구할 수 있다.&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;왜 필요할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 N개의 원소를 가지고 있는 리스트의 특정 구간의 합을 M번 구해야 한다고 하자. 그렇다면 반복문으로 구간의 합을 M번 구했을 때는 해당 코드의 시간복잡도가 $O(NM)$이 될 것이다.&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;이때 위와 같이 Prefix Sum 알고리즘을 이용한다면 해당 테이블을 만들때의 시간복잡도는 $O(N)$이다. 또한, 특정 구간 &lt;code&gt;[a, b]&lt;/code&gt; 의 합은 &lt;code&gt;prefix_sum[b]-prefix_sum[a-1]&lt;/code&gt;로 계산될 수 있으므로 시간복잡도는 $O(1)$이 된다. 따라서 전체 시간복잡도는 $O(N)$이 된다.&lt;/p&gt;</description>
      <category>Algorithm/개념 정리</category>
      <category>dynamic programming</category>
      <category>다이나믹 프로그래밍</category>
      <category>알고리즘</category>
      <author>r-jelly</author>
      <guid isPermaLink="true">https://r-jelly.tistory.com/10</guid>
      <comments>https://r-jelly.tistory.com/10#entry10comment</comments>
      <pubDate>Tue, 14 Apr 2026 18:52:50 +0900</pubDate>
    </item>
    <item>
      <title>백트래킹 (Backtracking)</title>
      <link>https://r-jelly.tistory.com/9</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;백트래킹이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백트래킹(Backtracking)이란, 현재 상태에서 가능한 모든 후보를 탐색하여 정답을 찾도록 하는 알고리즘을 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 일반적인 브루트포스(Brute Force) 알고리즘과 백트래킹의 차이점은, &lt;b&gt;&amp;ldquo;정답을 찾는 도중 정답이 될 가능성이 없는 경우, 그 즉시 되돌아가 다른 경로를 탐색하여 답을 찾는다&amp;rdquo;&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;백트래킹이 작동되는 방식은 크게 다음과 같다:&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;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;/ol&gt;
&lt;/li&gt;
&lt;li&gt;탐색이 끝나면, 상태를 원래대로 되돌린 후, 다른 후보를 골라 반복한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;409&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9LdtF/dJMcaakDddT/RDXtvZAe2kNKXFGIAv4K90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9LdtF/dJMcaakDddT/RDXtvZAe2kNKXFGIAv4K90/img.png&quot; data-alt=&quot;출처: https://www.geeksforgeeks.org/dsa/n-queen-problem-backtracking-3&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9LdtF/dJMcaakDddT/RDXtvZAe2kNKXFGIAv4K90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9LdtF%2FdJMcaakDddT%2FRDXtvZAe2kNKXFGIAv4K90%2Fimg.png&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;600&quot; height=&quot;307&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;409&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://www.geeksforgeeks.org/dsa/n-queen-problem-backtracking-3&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 풀기 위해서 위와 같은 상태 공간 트리(State Space Tree)를 사용할 수 있다. 위 그림처럼, 매 상태에서 조건에 맞는지 여부를 파악하고, 조건을 만족하지 않는다면 이후 경로를 더 이상 탐색하지 않고, 이전 상태로 돌아간 뒤 다음 후보를 찾아 탐색을 진행한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상태 공간 트리&lt;/b&gt;&lt;br /&gt;문제 해결과정에서 각각의 중간 상태 하나를 한 개의 Node로 표현하여 나타낸 트리&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 가능성이 없는 경로를 배제하는 방법을 가지치기(Pruning)이라고 하며, 가지치기가 일어나는 시점 아래의 모든 Subtree에 대한 탐색을 건너뛰고 다음 경로를 탐색할 수 있도록 한다. 이를 통해 브루트포스보다 더 효율적으로 정답을 찾아갈 수 있다.&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;일반적으로 백트래킹은 재귀함수를 이용한 DFS 방식으로 구현하게 된다. BFS를 잘 안쓰는 이유는, 모든 경우의 수를 고려하기 위해서는 Queue의 크기가 매우 커질 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;N-Queen&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백트래킹 문제 중, 제일 유명한 문제인 N-Queen 문제를 풀어보자. 유명한 만큼, 여러 코딩테스트 사이트에서 쉽게 찾아볼 수 있는 문제이다. (&lt;a href=&quot;https://www.acmicpc.net/problem/9663&quot;&gt;백준&lt;/a&gt;, &lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/12952&quot;&gt;프로그래머스&lt;/a&gt;, &lt;a href=&quot;https://leetcode.com/problems/n-queens/description/&quot;&gt;LeetCode&lt;/a&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;우리는 가로와 세로 길이가 모두 N인 체스판에서 N개의 Queen이 서로를 공격하지 못하도록 배치할 수 있는 경우의 수를 찾아보도록 하자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1218&quot; data-origin-height=&quot;785&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8yoOw/dJMcaaybm4m/SZFtOt1bK0Uj9CTn7fShKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8yoOw/dJMcaaybm4m/SZFtOt1bK0Uj9CTn7fShKk/img.png&quot; data-alt=&quot;N-Queen 문제의 상태 공간 트리&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8yoOw/dJMcaaybm4m/SZFtOt1bK0Uj9CTn7fShKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8yoOw%2FdJMcaaybm4m%2FSZFtOt1bK0Uj9CTn7fShKk%2Fimg.png&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;600&quot; height=&quot;387&quot; data-origin-width=&quot;1218&quot; data-origin-height=&quot;785&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;N-Queen 문제의 상태 공간 트리&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;2개의 Queen은 서로 같은 행에 존재할 수 없으므로, 모든 N개의 행 각각에는 Queen이 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;구체적으로 문제 해설을 하지는 않지만, 백트래킹을 이용해서 Python으로 구현한 정답은 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;cols = [False] * N
left_diag = [False] * (N*2) # 오른쪽 아래에서 왼쪽 위로, row+col 값 동일
right_diag = [False] * (N*2) # 왼쪽 아래에서 오른쪽 위로, row-col 값 동일

def dfs(N, cur_row):
    if N==cur_row:
        return 1

    cnt = 0
    for col in range(N):
        if not cols[col] and not left_diag[cur_row+col] and not right_diag[cur_row-col]:
            cols[col] = True
            left_diag[cur_row+col] = True
            right_diag[cur_row-col] = True

            cnt += dfs(N, cur_row+1)

            cols[col] = False
            left_diag[cur_row+col] = False
            right_diag[cur_row-col] = False

    return cnt&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;References&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.encrypted.gg/945&quot;&gt;바킹독의 실전 알고리즘 0x0C강: 백트래킹&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@gayeong39/%EB%B0%B1%ED%8A%B8%EB%9E%98%ED%82%B9-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-BackTracking&quot;&gt;https://velog.io/@gayeong39/백트래킹-알고리즘-BackTracking&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Backtracking&quot;&gt;https://en.wikipedia.org/wiki/Backtracking&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Algorithm/개념 정리</category>
      <category>backtracking</category>
      <category>dfs</category>
      <category>알고리즘</category>
      <author>r-jelly</author>
      <guid isPermaLink="true">https://r-jelly.tistory.com/9</guid>
      <comments>https://r-jelly.tistory.com/9#entry9comment</comments>
      <pubDate>Tue, 14 Apr 2026 16:59:08 +0900</pubDate>
    </item>
    <item>
      <title>재귀함수 (Recursive Function)</title>
      <link>https://r-jelly.tistory.com/8</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;재귀함수란?&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHR0bu/dJMcajhqaYT/FeH3nrBbfNLHKpZXcLGqh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHR0bu/dJMcajhqaYT/FeH3nrBbfNLHKpZXcLGqh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHR0bu/dJMcajhqaYT/FeH3nrBbfNLHKpZXcLGqh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHR0bu%2FdJMcajhqaYT%2FFeH3nrBbfNLHKpZXcLGqh0%2Fimg.png&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;600&quot; height=&quot;480&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;수학적 귀납법이란?&lt;/b&gt;&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;해당 명제가 n=1일때 성립함을 보인다.&lt;/li&gt;
&lt;li&gt;해당 명제가 n=k일때 성립한다고 &lt;b&gt;가정&lt;/b&gt;한 후, n=k+1일때도 성립하는지 증명한다.&lt;/li&gt;
&lt;/ol&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;특정한 입력에 대해서는 자기자신을 다시 호출하지 않고 함수가 종료되어야 한다. (= Base Condition)&lt;/li&gt;
&lt;li&gt;모든 입력은 Base Condition으로 수렴해야 한다.&lt;/li&gt;
&lt;li&gt;위 1, 2를 만족하지 않는 경우에는 함수가 무한하게 돌도록 한다.&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;간단하게, 숫자 10부터 차례대로 10&amp;rarr;9&amp;rarr;8&amp;rarr;&amp;hellip;&amp;rarr;1을 출력하게 하는 코드를 재귀적으로 작성한다면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def foo(N: int):
    if N &amp;lt; 1:
        return

    print(N)
    foo(N-1)

foo(10)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 보면, 함수의 입력 값이 10일 때 입력 값을 출력하고 해당 값에서 -1을 한 값을 함수 자기자신의 입력 값으로 하여 다시 호출하고 있다. 결과적으로 N은 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;이번에는 유명한 문제 중의 하나인 피보나치 수열에서 특정 위치의 숫자를 구하는 문제를 재귀로 풀어보자. 피보나치 수열은 다음과 같은 점화식으로 정의되는 수열이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$&lt;br /&gt;F(0)=0, \; F(1)=1, \; F(N) = F(N-1)+F(N-2)&lt;br /&gt;$$&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def fibo(N: int):
    if N &amp;lt;= 1:
        return N
    return fibo(N-1) + fibo(N-2)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 구현한다면, 이론적으로 모든 피보나치 수열의 값을 계산할 수 있다. 그러나, 실제 코드를 실행해보면 N이 커짐에 따라 실행시간이 기하급수적으로 커지는 것을 확인할 수 있다. 계산해보면 해당 코드의 시간복잡도는 $O(2^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;code&gt;fibo(5)&lt;/code&gt;를 계산하기 위해 &lt;code&gt;fibo(4), fibo(3)&lt;/code&gt; 의 값을 구해야 하고, &lt;code&gt;fibo(4)&lt;/code&gt; 의 값을 구하기 위해서는 &lt;code&gt;fibo(3), fibo(2)&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;해당 문제를 해결하기 위해서는, 계산한 값을 따로 저장해서 확인하는 Memoization과 같은 방식을 사용할 수 있는데, 이는 Dynamic Programming에서 더 자세하게 설명하고자 한다.&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;재귀(Recursion) vs 반복(Iteration)&lt;/h3&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;그러나, 재귀함수는 반복문에 비해 메모리 및 시간 복잡도적인 측면에서 손해를 보게 되며, 자기자신을 계속 호출하는 과정에서 메모리의 Stack 영역을 채우게 되므로 Stack Overflow를 발생할 수 있는 문제점이 존재한다.&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;</description>
      <category>Algorithm/개념 정리</category>
      <category>알고리즘</category>
      <category>재귀</category>
      <category>재귀함수</category>
      <author>r-jelly</author>
      <guid isPermaLink="true">https://r-jelly.tistory.com/8</guid>
      <comments>https://r-jelly.tistory.com/8#entry8comment</comments>
      <pubDate>Sun, 5 Apr 2026 16:29:00 +0900</pubDate>
    </item>
    <item>
      <title>너비 우선 탐색 (BFS) vs 깊이 우선 탐색 (DFS)</title>
      <link>https://r-jelly.tistory.com/7</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;너비 우선 탐색 (BFS)&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BFS(Breadth-First Search)란, 한 지점에서 다른 지점들을 방문할 때, &lt;u&gt;&lt;b&gt;너비를 우선으로&lt;/b&gt;&lt;/u&gt; 모든 지점을 방문하는 것&lt;/p&gt;
&lt;/blockquote&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;단점: DFS에 비해 더 많은 메모리를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;bfs.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;683&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GcCJD/dJMcaaEPB6U/l7CCzy6MnkkuUcO2Cwczqk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GcCJD/dJMcaaEPB6U/l7CCzy6MnkkuUcO2Cwczqk/img.gif&quot; data-alt=&quot;출처: https://skilled.dev/course/breadth-first-search&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GcCJD/dJMcaaEPB6U/l7CCzy6MnkkuUcO2Cwczqk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/GcCJD/dJMcaaEPB6U/l7CCzy6MnkkuUcO2Cwczqk/img.gif&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;600&quot; height=&quot;512&quot; data-filename=&quot;bfs.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;683&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://skilled.dev/course/breadth-first-search&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 노드를 기준으로 &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;주로 다차원 배열에서 특정 지점까지 걸리는 최소 시간을 구하는 문제에 BFS를 주로 사용할 수 있다. 코드로 구현하기 위해서는 Queue를 사용한다. BFS를 이용해 모든 지점을 방문하는 방법은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;비어있는 Queue에 시작 지점의 좌표를 입력한다.&lt;/li&gt;
&lt;li&gt;시작 지점을 방문하고, 이를 기록한다.&lt;/li&gt;
&lt;li&gt;Queue의 Front에서 좌표를 꺼낸다.&lt;/li&gt;
&lt;li&gt;해당 지점에 인접한 다른 지점 모두를 Queue의 Rear에 넣는다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;이때, 방문 여부를 판단하여 방문했던 지점은 Queue에 넣지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Queue가 빌 때까지 2-4를 반복한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 동작을 하는 Python 코드를 간단하게 작성하면 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;from collections import deque

def bfs(start_node):
    queue = deque([start_node])
    visited[start_node] = True

      while queue:
        cur_node = queue.popleft()
        for next_node in linked[cur_node]:
              if visited[next_node]:
                  continue
              visited[next_node] = True
              queue.append(next_node)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방문했던 지점을 다시 Queue에 넣지 않는 로직이 존재하므로, 칸의 수가 N개 일때 시간복잡도는 O(N)이 된다. 또한, Queue를 사용하므로, 반드시 시작 지점과의 거리순으로 방문하게 된다. &lt;code&gt;visited&lt;/code&gt; 배열을 단순 True/False가 아니라 숫자를 이용해 기록한다면, 반드시 특정 위치에 도달한 순간이 최소 시간임이 보장되므로, 시작 지점으로부터의 거리를 관리할 수 있게 된다.&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;u&gt;&lt;b&gt;만약, 방문 표시를 Queue에 넣기 전에 남기지 않고, Queue에서 뺄 때 남긴다면 어떻게 될까?&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;from collections import deque

def bfs(start_node):
    queue = deque([start_node])

      while queue:
        cur_node = queue.popleft()
        visited[cur_node] = True

        for next_node in linked[cur_node]:
              if visited[next_node]:
                  continue
              queue.append(next_node)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 구현을 한다면, &lt;b&gt;같은 지점이 여러 번 Queue에 들어가는 것&lt;/b&gt;을 방지 할 수 없다! 따라서, 시간 초과 or 메모리 초과가 발생할 가능성이 높아지게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;&amp;rArr; 즉, BFS에서는 Queue에 넣기 전에 방문 표시를 남기도록 코드를 작성해야 한다&lt;/b&gt;&lt;/u&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;깊이 우선 탐색 (DFS)&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DFS(Depth-First Search)란, 한 지점에서 다른 지점들을 방문할 때, &lt;u&gt;&lt;b&gt;깊이를 우선&lt;/b&gt;&lt;/u&gt;으로 모든 지점을 방문하는 것&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점: BFS에 비해 목표 지점에 더 빠르게 도달할 수 있다.&lt;/li&gt;
&lt;li&gt;단점: 최단 경로를 보장하지 않으며, 정답이 없는 경로에 깊이 빠질 위험이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;dfs.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;683&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/M6cas/dJMcaaEPB6G/qHeBYj2G6CXMWqtsRc7Wn1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/M6cas/dJMcaaEPB6G/qHeBYj2G6CXMWqtsRc7Wn1/img.gif&quot; data-alt=&quot;출처: https://skilled.dev/course/depth-first-search&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/M6cas/dJMcaaEPB6G/qHeBYj2G6CXMWqtsRc7Wn1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/M6cas/dJMcaaEPB6G/qHeBYj2G6CXMWqtsRc7Wn1/img.gif&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;600&quot; height=&quot;512&quot; data-filename=&quot;dfs.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;683&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://skilled.dev/course/depth-first-search&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 노드부터 &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;대부분 BFS로 풀 수 있는 문제는 DFS로 해결할 수 있다. 그러나, 최단 거리 측정을 할 수 없다는 단점이 있어, 탐색 문제에서는 BFS가 조금 더 광범위하게 사용된다. DFS를 코드로 구현하기 위해서는 Stack을 사용하거나, 재귀적인 방식으로 구현할 수 있다. Stack을 이용한 DFS로 모든 지점을 방문하는 방법은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;비어있는 Stack에 시작 지점의 좌표를 입력한다.&lt;/li&gt;
&lt;li&gt;시작 지점을 방문하고, 이를 기록한다.&lt;/li&gt;
&lt;li&gt;Stack의 Top에서 좌표를 꺼낸다.&lt;/li&gt;
&lt;li&gt;해당 지점에 인접한 다른 지점 모두를 Stack의 Top에 넣는다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;이때, 방문 여부를 판단하여 방문했던 지점은 Stack에 넣지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Stack이 빌 때까지 2-4를 반복한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 동작을 하는 Python 코드를 간단하게 작성하면 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;from collections import deque

def dfs(start_node):
    stack = deque([start_node])
    visited[start_node] = True

      while queue:
        cur_node = stack.pop()
        for next_node in linked[cur_node]:
              if visited[next_node]:
                  continue
              visited[next_node] = True
              stack.append(next_node)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BFS의 구현 방식에서 Queue가 Stack으로 대체된 형식이다. 따라서 시간복잡도는 BFS와 동일하게 O(N)이다. DFS는 BFS와 달리, 특정 시점에 처음으로 방문한 것이 최소 시간임을 보장하지 못하므로 최소 시간을 구하는 문제에 이용하기는 어렵다.&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;재귀를 이용해서도 DFS를 구현할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;from collections import deque

def dfs(start_node):
    visited[start_node] = True

    for next_node in linked[start_node]:
        if visited[next_node]:
            continue
        dfs(next_node)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 구현 방식은 백트래킹과 같이 재귀적으로 경우의 수를 탐색할 때 많이 사용될 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;References&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.encrypted.gg/941&quot;&gt;바킹독의 실전 알고리즘 0x09강: BFS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.encrypted.gg/942&quot;&gt;바킹독의 실전 알고리즘 0x0A강: DFS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gmlwjd9405.github.io/2018/08/15/algorithm-bfs.html&quot;&gt;https://gmlwjd9405.github.io/2018/08/15/algorithm-bfs.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gmlwjd9405.github.io/2018/08/14/algorithm-dfs.html&quot;&gt;https://gmlwjd9405.github.io/2018/08/14/algorithm-dfs.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Algorithm/개념 정리</category>
      <category>BFS</category>
      <category>dfs</category>
      <category>알고리즘</category>
      <author>r-jelly</author>
      <guid isPermaLink="true">https://r-jelly.tistory.com/7</guid>
      <comments>https://r-jelly.tistory.com/7#entry7comment</comments>
      <pubDate>Fri, 3 Apr 2026 15:02:12 +0900</pubDate>
    </item>
  </channel>
</rss>