Files
hello-algo/ko/search.json
krahets 2b601e7646 deploy
2026-10-31 03:36:47 +08:00

1 line
2.4 MiB
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"config":{"separator":"[\\s\\-_,:!=\\[\\]()\\\\\"`/]+|\\.(?!\\d)"},"items":[{"location":"chapter_appendix/","level":1,"title":"제 16 장   부록","text":"","path":["제 16 장   부록"],"tags":[]},{"location":"chapter_appendix/#_1","level":2,"title":"이 장의 내용","text":"<ul> <li>16.1   프로그래밍 환경 설치</li> <li>16.2   함께 만드는 책</li> <li>16.3   용어집</li> </ul>","path":["제 16 장   부록"],"tags":[]},{"location":"chapter_appendix/contribution/","level":1,"title":"16.2   함께 만드는 책","text":"<p>저자의 역량이 부족하여 책에 누락이나 오류가 있을 수 있다는 점을 양해 부탁드린다. 오탈자, 끊어진 링크, 빠진 내용, 모호한 표현, 불분명한 설명, 부자연스러운 글의 구성 등을 발견했다면 수정에 힘을 보태 주기 바란다. 독자에게 더 좋은 학습 자료를 제공하는 데 큰 도움이 된다.</p> <p>모든 집필 참여자의 GitHub ID는 저장소와 웹 버전의 첫 페이지에 표시된다. 이는 오픈 소스 커뮤니티에 아낌없이 기여한 분들에게 감사를 전하기 위한 것이다.</p> <p>오픈 소스의 매력</p> <p>종이책은 인쇄와 다음 인쇄 사이의 간격이 길어 내용을 갱신하기가 매우 불편하다.</p> <p>반면 이 오픈 소스 도서에서는 내용을 갱신하는 데 걸리는 시간이 며칠, 때로는 몇 시간으로 줄어든다.</p>","path":["제 16 장   부록","16.2   함께 만드는 책"],"tags":[]},{"location":"chapter_appendix/contribution/#1","level":3,"title":"1.   간단한 내용 수정","text":"<p>아래 그림(16-3)과 같이 각 페이지의 오른쪽 위에는 ‘편집 아이콘’이 있다. 다음 순서로 글이나 코드를 수정할 수 있다.</p> <ol> <li>‘편집 아이콘’을 누른다. 저장소를 포크해야 한다는 안내가 나오면 동의한다.</li> <li>Markdown 소스 파일을 수정하고 내용이 올바른지 확인한다. 서식은 가능한 한 일관되게 유지한다.</li> <li>페이지 아래쪽에 수정 내용을 설명하고 ‘Propose file change’ 버튼을 누른다. 이동한 페이지에서 ‘Create pull request’ 버튼을 누르면 풀 리퀘스트를 만들 수 있다.</li> </ol> <p></p> <p> 그림 16-3   페이지 편집 버튼 </p> <p>그림은 직접 수정할 수 없으므로 Issue를 만들거나 댓글로 문제를 설명해 주기 바란다. 확인 후 최대한 빨리 그림을 다시 제작하여 교체하겠다.</p>","path":["제 16 장   부록","16.2   함께 만드는 책"],"tags":[]},{"location":"chapter_appendix/contribution/#2","level":3,"title":"2.   내용 작성","text":"<p>코드를 다른 프로그래밍 언어로 옮기거나 글의 내용을 확장하는 등 이 오픈 소스 프로젝트에 참여하고 싶다면 다음 풀 리퀘스트 절차를 따르면 된다.</p> <ol> <li>GitHub에 로그인한 뒤 이 책의 코드 저장소를 개인 계정으로 포크한다.</li> <li>포크한 저장소의 웹페이지로 이동하여 <code>git clone</code> 명령으로 저장소를 로컬에 복제한다.</li> <li>로컬에서 내용을 작성하고 충분히 테스트하여 코드가 올바른지 검증한다.</li> <li>로컬 변경 사항을 커밋한 뒤 원격 저장소로 푸시한다.</li> <li>저장소 웹페이지를 새로 고침하고 ‘Create pull request’ 버튼을 눌러 풀 리퀘스트를 만든다.</li> </ol>","path":["제 16 장   부록","16.2   함께 만드는 책"],"tags":[]},{"location":"chapter_appendix/contribution/#3-docker","level":3,"title":"3.   Docker 배포","text":"<p><code>hello-algo</code> 루트 디렉터리에서 다음 Docker 명령을 실행하면 <code>http://localhost:8000/ko/</code>에서 한국어판을 열 수 있다.</p> <pre><code>docker-compose up -d\n</code></pre> <p>다음 명령으로 배포를 제거할 수 있다.</p> <pre><code>docker-compose down\n</code></pre>","path":["제 16 장   부록","16.2   함께 만드는 책"],"tags":[]},{"location":"chapter_appendix/installation/","level":1,"title":"16.1   프로그래밍 환경 설치","text":"","path":["제 16 장   부록","16.1   프로그래밍 환경 설치"],"tags":[]},{"location":"chapter_appendix/installation/#1611-ide","level":2,"title":"16.1.1   IDE 설치","text":"<p>로컬 통합 개발 환경(IDE)으로는 가벼운 오픈 소스 도구인 VS Code를 권장한다. VS Code 공식 웹사이트에서 운영체제에 맞는 버전을 내려받아 설치한다.</p> <p></p> <p> 그림 16-1   공식 웹사이트에서 VS Code 내려받기 </p> <p>VS Code는 풍부한 확장 생태계를 갖추고 있어 대부분의 프로그래밍 언어를 실행하고 디버깅할 수 있다. Python을 예로 들면, ‘Python Extension Pack’을 설치한 뒤 Python 코드를 디버깅할 수 있다. 설치 과정은 아래 그림(16-2)과 같다.</p> <p></p> <p> 그림 16-2   VS Code 확장 설치 </p>","path":["제 16 장   부록","16.1   프로그래밍 환경 설치"],"tags":[]},{"location":"chapter_appendix/installation/#1612","level":2,"title":"16.1.2   언어 실행 환경 설치","text":"","path":["제 16 장   부록","16.1   프로그래밍 환경 설치"],"tags":[]},{"location":"chapter_appendix/installation/#1-python","level":3,"title":"1.   Python 환경","text":"<ol> <li>Miniconda3를 내려받아 설치한다. Python 3.10 이상이 필요하다.</li> <li>VS Code 확장 마켓플레이스에서 <code>python</code>을 검색하여 Python Extension Pack을 설치한다.</li> <li>(선택 사항) 명령줄에 <code>pip install black</code>을 입력하여 코드 포매터를 설치한다.</li> </ol>","path":["제 16 장   부록","16.1   프로그래밍 환경 설치"],"tags":[]},{"location":"chapter_appendix/installation/#2-cc","level":3,"title":"2.   C/C++ 환경","text":"<ol> <li>Windows에서는 MinGW를 설치해야 한다(설정 안내). macOS에는 Clang이 기본으로 포함되어 있어 별도로 설치할 필요가 없다.</li> <li>VS Code 확장 마켓플레이스에서 <code>c++</code>를 검색하여 C/C++ Extension Pack을 설치한다.</li> <li>(선택 사항) Settings 페이지에서 코드 서식 옵션인 <code>Clang_format_fallback Style</code>을 검색하고 <code>{ BasedOnStyle: Microsoft, BreakBeforeBraces: Attach }</code>로 설정한다.</li> </ol>","path":["제 16 장   부록","16.1   프로그래밍 환경 설치"],"tags":[]},{"location":"chapter_appendix/installation/#3-java","level":3,"title":"3.   Java 환경","text":"<ol> <li>OpenJDK를 내려받아 설치한다(JDK 9보다 높은 버전이어야 한다).</li> <li>VS Code 확장 마켓플레이스에서 <code>java</code>를 검색하여 Extension Pack for Java를 설치한다.</li> </ol>","path":["제 16 장   부록","16.1   프로그래밍 환경 설치"],"tags":[]},{"location":"chapter_appendix/installation/#4-c","level":3,"title":"4.   C# 환경","text":"<ol> <li>.NET 8.0을 내려받아 설치한다.</li> <li>VS Code 확장 마켓플레이스에서 <code>C# Dev Kit</code>을 검색하여 C# Dev Kit을 설치한다(설정 안내).</li> <li>Visual Studio를 사용해도 된다(설치 안내).</li> </ol>","path":["제 16 장   부록","16.1   프로그래밍 환경 설치"],"tags":[]},{"location":"chapter_appendix/installation/#5-go","level":3,"title":"5.   Go 환경","text":"<ol> <li>go를 내려받아 설치한다.</li> <li>VS Code 확장 마켓플레이스에서 <code>go</code>를 검색하여 Go를 설치한다.</li> <li><code>Ctrl + Shift + P</code>를 눌러 명령 팔레트를 열고 go를 입력한다. <code>Go: Install/Update Tools</code>를 선택한 뒤 모든 항목을 체크하여 설치한다.</li> </ol>","path":["제 16 장   부록","16.1   프로그래밍 환경 설치"],"tags":[]},{"location":"chapter_appendix/installation/#6-swift","level":3,"title":"6.   Swift 환경","text":"<ol> <li>Swift를 내려받아 설치한다.</li> <li>VS Code 확장 마켓플레이스에서 <code>swift</code>를 검색하여 Swift for Visual Studio Code를 설치한다.</li> </ol>","path":["제 16 장   부록","16.1   프로그래밍 환경 설치"],"tags":[]},{"location":"chapter_appendix/installation/#7-javascript","level":3,"title":"7.   JavaScript 환경","text":"<ol> <li>Node.js를 내려받아 설치한다.</li> <li>(선택 사항) VS Code 확장 마켓플레이스에서 <code>Prettier</code>를 검색하여 코드 포매터를 설치한다.</li> </ol>","path":["제 16 장   부록","16.1   프로그래밍 환경 설치"],"tags":[]},{"location":"chapter_appendix/installation/#8-typescript","level":3,"title":"8.   TypeScript 환경","text":"<ol> <li>JavaScript 환경과 같은 순서로 설치한다.</li> <li>TypeScript Execute (tsx)를 설치한다.</li> <li>VS Code 확장 마켓플레이스에서 <code>typescript</code>를 검색하여 Pretty TypeScript Errors를 설치한다.</li> </ol>","path":["제 16 장   부록","16.1   프로그래밍 환경 설치"],"tags":[]},{"location":"chapter_appendix/installation/#9-dart","level":3,"title":"9.   Dart 환경","text":"<ol> <li>Dart를 내려받아 설치한다.</li> <li>VS Code 확장 마켓플레이스에서 <code>dart</code>를 검색하여 Dart를 설치한다.</li> </ol>","path":["제 16 장   부록","16.1   프로그래밍 환경 설치"],"tags":[]},{"location":"chapter_appendix/installation/#10-rust","level":3,"title":"10.   Rust 환경","text":"<ol> <li>Rust를 내려받아 설치한다.</li> <li>VS Code 확장 마켓플레이스에서 <code>rust</code>를 검색하여 rust-analyzer를 설치한다.</li> </ol>","path":["제 16 장   부록","16.1   프로그래밍 환경 설치"],"tags":[]},{"location":"chapter_appendix/terminology/","level":1,"title":"16.3   용어집","text":"<p>아래 표(16-1)에는 이 책에 나오는 주요 용어를 정리했다. 영어 문헌을 읽을 때 도움이 되므로 각 용어의 영어 명칭을 기억해 두기를 권장한다.</p> <p> 표 16-1   자료 구조와 알고리즘의 주요 용어 </p> English 한국어 algorithm 알고리즘 data structure 자료 구조 code 코드 file 파일 function 함수 method 메서드 variable 변수 asymptotic complexity analysis 점근적 복잡도 분석 time complexity 시간 복잡도 space complexity 공간 복잡도 loop 반복문 iteration 반복 recursion 재귀 tail recursion 꼬리 재귀 recursion tree 재귀 트리 big-\\(O\\) notation 빅 \\(O\\) 표기법 asymptotic upper bound 점근적 상한 sign-magnitude 부호와 크기 표현 1’s complement 1의 보수 2’s complement 2의 보수 array 배열 index 인덱스 linked list 연결 리스트 linked list node, list node 연결 리스트 노드 head node 머리 노드 tail node 꼬리 노드 list 리스트 dynamic array 동적 배열 hard disk 하드 디스크 random-access memory (RAM) 메모리 cache memory 캐시 메모리 cache miss 캐시 미스 cache hit rate 캐시 적중률 stack 스택 top of the stack 스택의 맨 위 bottom of the stack 스택의 맨 아래 queue 큐 double-ended queue 덱 front of the queue 큐의 앞 rear of the queue 큐의 뒤 hash table 해시 테이블 hash set 해시 집합 bucket 버킷 hash function 해시 함수 hash collision 해시 충돌 load factor 적재율 separate chaining 체이닝 open addressing 개방 주소법 linear probing 선형 탐사 lazy deletion 지연 삭제 binary tree 이진 트리 tree node 트리 노드 left-child node 왼쪽 자식 노드 right-child node 오른쪽 자식 노드 parent node 부모 노드 left subtree 왼쪽 서브트리 right subtree 오른쪽 서브트리 root node 루트 노드 leaf node 리프 노드 edge 간선 level 레벨 degree 차수 height 높이 depth 깊이 perfect binary tree 포화 이진 트리 complete binary tree 완전 이진 트리 full binary tree 정 이진 트리 balanced binary tree 균형 이진 트리 binary search tree 이진 탐색 트리 AVL tree AVL 트리 red-black tree 레드-블랙 트리 level-order traversal 레벨 순서 순회 breadth-first traversal 너비 우선 순회 depth-first traversal 깊이 우선 순회 pre-order traversal 전위 순회 in-order traversal 중위 순회 post-order traversal 후위 순회 balanced binary search tree 균형 이진 탐색 트리 balance factor 균형 인수 heap 힙 max heap 최대 힙 min heap 최소 힙 priority queue 우선순위 큐 heapify 힙 재정렬 top-\\(k\\) problem Top-\\(k\\) 문제 graph 그래프 vertex 정점 undirected graph 무방향 그래프 directed graph 방향 그래프 connected graph 연결 그래프 disconnected graph 비연결 그래프 weighted graph 가중 그래프 adjacency 인접 path 경로 in-degree 진입 차수 out-degree 진출 차수 adjacency matrix 인접 행렬 adjacency list 인접 리스트 breadth-first search 너비 우선 탐색 depth-first search 깊이 우선 탐색 binary search 이진 탐색 searching algorithm 탐색 알고리즘 sorting algorithm 정렬 알고리즘 selection sort 선택 정렬 bubble sort 버블 정렬 insertion sort 삽입 정렬 quick sort 퀵 정렬 merge sort 병합 정렬 heap sort 힙 정렬 bucket sort 버킷 정렬 counting sort 계수 정렬 radix sort 기수 정렬 divide and conquer 분할 정복 hanota problem 하노이의 탑 문제 backtracking algorithm 백트래킹 알고리즘 constraint 제약 조건 solution 해 state 상태 pruning 가지치기 permutations problem 모든 순열 구하기 subset-sum problem 부분 집합의 합 문제 \\(n\\)-queens problem \\(n\\)-퀸 문제 dynamic programming 동적 계획법 initial state 초기 상태 state-transition equation 상태 전이 식 knapsack problem 배낭 문제 edit distance problem 편집 거리 문제 greedy algorithm 탐욕 알고리즘","path":["제 16 장   부록","16.3   용어집"],"tags":[]},{"location":"chapter_array_and_linkedlist/","level":1,"title":"제 4 장   배열과 연결 리스트","text":"<p>이 장을 시작하며</p> <p>자료 구조의 세계는 두꺼운 벽돌벽과 같다.</p> <p>배열의 벽돌은 가지런히 빈틈없이 붙어 있다. 연결 리스트의 벽돌은 곳곳에 흩어져 있으며, 이들을 잇는 덩굴이 벽돌 틈 사이를 자유롭게 누빈다.</p>","path":["제 4 장   배열과 연결 리스트"],"tags":[]},{"location":"chapter_array_and_linkedlist/#_1","level":2,"title":"이 장의 내용","text":"<ul> <li>4.1   배열</li> <li>4.2   연결 리스트</li> <li>4.3   리스트</li> <li>4.4   메모리와 캐시 *</li> <li>4.5   요약</li> <li>4.6   연습 문제</li> </ul>","path":["제 4 장   배열과 연결 리스트"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/","level":1,"title":"4.1   배열","text":"<p>배열(array)은 같은 자료형의 원소를 연속된 메모리 공간에 저장하는 선형 자료 구조다. 배열에서 원소가 있는 위치를 해당 원소의 인덱스(index)라고 한다. 아래 그림(4-1)은 배열의 주요 개념과 저장 방식을 보여 준다.</p> <p></p> <p> 그림 4-1   배열의 정의와 저장 방식 </p>","path":["제 4 장   배열과 연결 리스트","4.1   배열"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#411","level":2,"title":"4.1.1   배열의 기본 연산","text":"","path":["제 4 장   배열과 연결 리스트","4.1   배열"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#1","level":3,"title":"1.   배열 초기화","text":"<p>필요에 따라 초깃값을 지정하지 않거나 지정하는 두 가지 초기화 방식을 사용할 수 있다. 초깃값을 지정하지 않으면 대부분의 프로그래밍 언어는 배열 원소를 \\(0\\)으로 초기화한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py<pre><code># 배열 초기화\narr: list[int] = [0] * 5 # [ 0, 0, 0, 0, 0 ]\nnums: list[int] = [1, 3, 2, 5, 4]\n</code></pre> array.cpp<pre><code>/* 배열 초기화 */\n// 스택에 저장\nint arr[5] = {};\nint nums[5] = { 1, 3, 2, 5, 4 };\n// 힙에 저장(공간을 직접 해제해야 함)\nint* arr1 = new int[5] {};\nint* nums1 = new int[5] { 1, 3, 2, 5, 4 };\n</code></pre> array.java<pre><code>/* 배열 초기화 */\nint[] arr = new int[5]; // { 0, 0, 0, 0, 0 }\nint[] nums = { 1, 3, 2, 5, 4 };\n</code></pre> array.cs<pre><code>/* 배열 초기화 */\nint[] arr = new int[5]; // [ 0, 0, 0, 0, 0 ]\nint[] nums = [1, 3, 2, 5, 4];\n</code></pre> array.go<pre><code>/* 배열 초기화 */\nvar arr [5]int\n// Go에서 길이를 지정하면([5]int) 배열이고, 지정하지 않으면([]int) 슬라이스다\n// Go 배열의 길이는 컴파일 시 결정되도록 설계되었으므로 상수로만 길이를 지정할 수 있다\n// 용량 확장 메서드 extend()를 쉽게 구현하기 위해 아래에서는 슬라이스(Slice)를 배열(Array)로 간주한다\nnums := []int{1, 3, 2, 5, 4}\n</code></pre> array.swift<pre><code>/* 배열 초기화 */\nlet arr = Array(repeating: 0, count: 5) // [0, 0, 0, 0, 0]\nlet nums = [1, 3, 2, 5, 4]\n</code></pre> array.js<pre><code>/* 배열 초기화 */\nvar arr = new Array(5).fill(0);\nvar nums = [1, 3, 2, 5, 4];\n</code></pre> array.ts<pre><code>/* 배열 초기화 */\nlet arr: number[] = new Array(5).fill(0);\nlet nums: number[] = [1, 3, 2, 5, 4];\n</code></pre> array.dart<pre><code>/* 배열 초기화 */\nList&lt;int&gt; arr = List.filled(5, 0); // [0, 0, 0, 0, 0]\nList&lt;int&gt; nums = [1, 3, 2, 5, 4];\n</code></pre> array.rs<pre><code>/* 배열 초기화 */\nlet arr: [i32; 5] = [0; 5]; // [0, 0, 0, 0, 0]\nlet slice: &amp;[i32] = &amp;[0; 5];\n// Rust에서 길이를 지정하면([i32; 5]) 배열이고, 지정하지 않으면(&amp;[i32]) 슬라이스다\n// Rust 배열의 길이는 컴파일 시 결정되도록 설계되었으므로 상수로만 길이를 지정할 수 있다\n// Vector는 Rust에서 일반적으로 동적 배열로 사용하는 자료형이다\n// 용량 확장 메서드 extend()를 쉽게 구현하기 위해 아래에서는 vector를 배열(array)로 간주한다\nlet nums: Vec&lt;i32&gt; = vec![1, 3, 2, 5, 4];\n</code></pre> array.c<pre><code>/* 배열 초기화 */\nint arr[5] = { 0 }; // { 0, 0, 0, 0, 0 }\nint nums[5] = { 1, 3, 2, 5, 4 };\n</code></pre> array.kt<pre><code>/* 배열 초기화 */\nvar arr = IntArray(5) // { 0, 0, 0, 0, 0 }\nvar nums = intArrayOf(1, 3, 2, 5, 4)\n</code></pre> array.rb<pre><code># 배열 초기화\narr = Array.new(5, 0)\nnums = [1, 3, 2, 5, 4]\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 4 장   배열과 연결 리스트","4.1   배열"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#2","level":3,"title":"2.   원소 접근","text":"<p>배열 원소는 연속된 메모리 공간에 저장되므로 메모리 주소를 매우 쉽게 계산할 수 있다. 배열의 메모리 주소(첫 원소의 메모리 주소)와 원소의 인덱스가 주어지면, 아래 그림(4-2)의 식으로 해당 원소의 메모리 주소를 구하여 직접 접근할 수 있다.</p> <p></p> <p> 그림 4-2   배열 원소의 메모리 주소 계산 </p> <p>위 그림(4-2)에서 배열의 첫 원소 인덱스는 \\(0\\)이다. \\(1\\)부터 세는 편이 더 자연스러워 보이므로 직관에 어긋날 수 있다. 그러나 주소 계산식의 관점에서 인덱스는 본질적으로 메모리 주소의 오프셋이다. 첫 원소의 주소 오프셋은 \\(0\\)이므로 인덱스가 \\(0\\)인 것은 합리적이다.</p> <p>배열의 원소 접근은 매우 효율적이며, 어떤 원소든 \\(O(1)\\) 시간에 임의 접근할 수 있다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py<pre><code>def random_access(nums: list[int]) -&gt; int:\n \"\"\"원소에 임의 접근\"\"\"\n # 구간 [0, len(nums)-1]에서 숫자 하나를 무작위로 뽑는다\n random_index = random.randint(0, len(nums) - 1)\n # 무작위 원소를 가져와 반환한다\n random_num = nums[random_index]\n return random_num\n</code></pre> array.cpp<pre><code>/* 원소에 임의 접근 */\nint randomAccess(int *nums, int size) {\n // 구간 [0, size)에서 무작위로 수 하나 선택\n int randomIndex = rand() % size;\n // 무작위 원소를 가져와 반환한다\n int randomNum = nums[randomIndex];\n return randomNum;\n}\n</code></pre> array.java<pre><code>/* 원소에 임의 접근 */\nint randomAccess(int[] nums) {\n // 구간 [0, nums.length)에서 숫자 하나를 무작위로 뽑는다\n int randomIndex = ThreadLocalRandom.current().nextInt(0, nums.length);\n // 무작위 원소를 가져와 반환한다\n int randomNum = nums[randomIndex];\n return randomNum;\n}\n</code></pre> array.cs<pre><code>/* 원소에 임의 접근 */\nint RandomAccess(int[] nums) {\n Random random = new();\n // 구간 [0, nums.Length)에서 무작위로 수 하나 선택\n int randomIndex = random.Next(nums.Length);\n // 무작위 원소를 가져와 반환한다\n int randomNum = nums[randomIndex];\n return randomNum;\n}\n</code></pre> array.go<pre><code>/* 원소에 임의 접근 */\nfunc randomAccess(nums []int) (randomNum int) {\n // 구간 [0, nums.length)에서 숫자 하나를 무작위로 뽑는다\n randomIndex := rand.Intn(len(nums))\n // 무작위 원소를 가져와 반환한다\n randomNum = nums[randomIndex]\n return\n}\n</code></pre> array.swift<pre><code>/* 원소에 임의 접근 */\nfunc randomAccess(nums: [Int]) -&gt; Int {\n // 구간 [0, nums.count)에서 숫자 하나를 무작위로 뽑는다\n let randomIndex = nums.indices.randomElement()!\n // 무작위 원소를 가져와 반환한다\n let randomNum = nums[randomIndex]\n return randomNum\n}\n</code></pre> array.js<pre><code>/* 원소에 임의 접근 */\nfunction randomAccess(nums) {\n // 구간 [0, nums.length)에서 숫자 하나를 무작위로 뽑는다\n const random_index = Math.floor(Math.random() * nums.length);\n // 무작위 원소를 가져와 반환한다\n const random_num = nums[random_index];\n return random_num;\n}\n</code></pre> array.ts<pre><code>/* 원소에 임의 접근 */\nfunction randomAccess(nums: number[]): number {\n // 구간 [0, nums.length)에서 숫자 하나를 무작위로 뽑는다\n const random_index = Math.floor(Math.random() * nums.length);\n // 무작위 원소를 가져와 반환한다\n const random_num = nums[random_index];\n return random_num;\n}\n</code></pre> array.dart<pre><code>/* 원소에 임의 접근 */\nint randomAccess(List&lt;int&gt; nums) {\n // 구간 [0, nums.length)에서 숫자 하나를 무작위로 뽑는다\n int randomIndex = Random().nextInt(nums.length);\n // 무작위 원소를 가져와 반환한다\n int randomNum = nums[randomIndex];\n return randomNum;\n}\n</code></pre> array.rs<pre><code>/* 원소에 임의 접근 */\nfn random_access(nums: &amp;[i32]) -&gt; i32 {\n // 구간 [0, nums.len())에서 숫자 하나를 무작위로 뽑는다\n let random_index = rand::thread_rng().gen_range(0..nums.len());\n // 무작위 원소를 가져와 반환한다\n let random_num = nums[random_index];\n random_num\n}\n</code></pre> array.c<pre><code>/* 원소에 임의 접근 */\nint randomAccess(int *nums, int size) {\n // 구간 [0, size)에서 무작위로 수 하나 선택\n int randomIndex = rand() % size;\n // 무작위 원소를 가져와 반환한다\n int randomNum = nums[randomIndex];\n return randomNum;\n}\n</code></pre> array.kt<pre><code>/* 원소에 임의 접근 */\nfun randomAccess(nums: IntArray): Int {\n // 구간 [0, nums.size)에서 숫자 하나를 무작위로 뽑는다\n val randomIndex = ThreadLocalRandom.current().nextInt(0, nums.size)\n // 무작위 원소를 가져와 반환한다\n val randomNum = nums[randomIndex]\n return randomNum\n}\n</code></pre> array.rb<pre><code>### 원소에 임의 접근 ###\ndef random_access(nums)\n # 구간 [0, nums.length)에서 숫자 하나를 무작위로 뽑는다\n random_index = Random.rand(0...nums.length)\n\n # 무작위 원소를 가져와 반환한다\n nums[random_index]\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 4 장   배열과 연결 리스트","4.1   배열"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#3","level":3,"title":"3.   원소 삽입","text":"<p>배열 원소는 메모리에서 ‘빈틈없이 붙어 있어’ 사이에 다른 데이터를 넣을 공간이 없다. 아래 그림(4-3)과 같이 배열 중간에 원소를 삽입하려면 해당 위치 이후의 모든 원소를 한 칸씩 뒤로 옮긴 다음, 그 인덱스에 새 원소를 대입해야 한다.</p> <p></p> <p> 그림 4-3   배열에 원소를 삽입하는 예 </p> <p>배열의 길이는 고정되어 있으므로 원소를 하나 삽입하면 반드시 마지막 원소가 ‘사라진다’는 점에 주의하자. 이 문제의 해결책은 ‘리스트’ 절에서 다룬다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py<pre><code>def insert(nums: list[int], num: int, index: int):\n \"\"\"배열의 인덱스 index에 원소 num 삽입\"\"\"\n # 인덱스 index와 그 뒤에 있는 모든 원소를 한 칸씩 뒤로 이동한다\n for i in range(len(nums) - 1, index, -1):\n nums[i] = nums[i - 1]\n # index 위치의 원소에 num을 대입한다\n nums[index] = num\n</code></pre> array.cpp<pre><code>/* 배열의 인덱스 index에 원소 num 삽입 */\nvoid insert(int *nums, int size, int num, int index) {\n // 인덱스 index와 그 뒤에 있는 모든 원소를 한 칸씩 뒤로 이동한다\n for (int i = size - 1; i &gt; index; i--) {\n nums[i] = nums[i - 1];\n }\n // index 위치의 원소에 num을 대입한다\n nums[index] = num;\n}\n</code></pre> array.java<pre><code>/* 배열의 인덱스 index에 원소 num 삽입 */\nvoid insert(int[] nums, int num, int index) {\n // 인덱스 index와 그 뒤에 있는 모든 원소를 한 칸씩 뒤로 이동한다\n for (int i = nums.length - 1; i &gt; index; i--) {\n nums[i] = nums[i - 1];\n }\n // index 위치의 원소에 num을 대입한다\n nums[index] = num;\n}\n</code></pre> array.cs<pre><code>/* 배열의 인덱스 index에 원소 num 삽입 */\nvoid Insert(int[] nums, int num, int index) {\n // 인덱스 index와 그 뒤에 있는 모든 원소를 한 칸씩 뒤로 이동한다\n for (int i = nums.Length - 1; i &gt; index; i--) {\n nums[i] = nums[i - 1];\n }\n // index 위치의 원소에 num을 대입한다\n nums[index] = num;\n}\n</code></pre> array.go<pre><code>/* 배열의 인덱스 index에 원소 num 삽입 */\nfunc insert(nums []int, num int, index int) {\n // 인덱스 index와 그 뒤에 있는 모든 원소를 한 칸씩 뒤로 이동한다\n for i := len(nums) - 1; i &gt; index; i-- {\n nums[i] = nums[i-1]\n }\n // index 위치의 원소에 num을 대입한다\n nums[index] = num\n}\n</code></pre> array.swift<pre><code>/* 배열의 인덱스 index에 원소 num 삽입 */\nfunc insert(nums: inout [Int], num: Int, index: Int) {\n // 인덱스 index와 그 뒤에 있는 모든 원소를 한 칸씩 뒤로 이동한다\n for i in stride(from: nums.count - 1, to: index, by: -1) {\n nums[i] = nums[i - 1]\n }\n // index 위치의 원소에 num을 대입한다\n nums[index] = num\n}\n</code></pre> array.js<pre><code>/* 배열의 인덱스 index에 원소 num 삽입 */\nfunction insert(nums, num, index) {\n // 인덱스 index와 그 뒤에 있는 모든 원소를 한 칸씩 뒤로 이동한다\n for (let i = nums.length - 1; i &gt; index; i--) {\n nums[i] = nums[i - 1];\n }\n // index 위치의 원소에 num을 대입한다\n nums[index] = num;\n}\n</code></pre> array.ts<pre><code>/* 배열의 인덱스 index에 원소 num 삽입 */\nfunction insert(nums: number[], num: number, index: number): void {\n // 인덱스 index와 그 뒤에 있는 모든 원소를 한 칸씩 뒤로 이동한다\n for (let i = nums.length - 1; i &gt; index; i--) {\n nums[i] = nums[i - 1];\n }\n // index 위치의 원소에 num을 대입한다\n nums[index] = num;\n}\n</code></pre> array.dart<pre><code>/* 배열의 인덱스 index에 원소 _num 삽입 */\nvoid insert(List&lt;int&gt; nums, int _num, int index) {\n // 인덱스 index와 그 뒤에 있는 모든 원소를 한 칸씩 뒤로 이동한다\n for (var i = nums.length - 1; i &gt; index; i--) {\n nums[i] = nums[i - 1];\n }\n // 인덱스 index의 원소에 _num을 대입한다\n nums[index] = _num;\n}\n</code></pre> array.rs<pre><code>/* 배열의 인덱스 index에 원소 num 삽입 */\nfn insert(nums: &amp;mut [i32], num: i32, index: usize) {\n // 인덱스 index와 그 뒤에 있는 모든 원소를 한 칸씩 뒤로 이동한다\n for i in (index + 1..nums.len()).rev() {\n nums[i] = nums[i - 1];\n }\n // index 위치의 원소에 num을 대입한다\n nums[index] = num;\n}\n</code></pre> array.c<pre><code>/* 배열의 인덱스 index에 원소 num 삽입 */\nvoid insert(int *nums, int size, int num, int index) {\n // 인덱스 index와 그 뒤에 있는 모든 원소를 한 칸씩 뒤로 이동한다\n for (int i = size - 1; i &gt; index; i--) {\n nums[i] = nums[i - 1];\n }\n // index 위치의 원소에 num을 대입한다\n nums[index] = num;\n}\n</code></pre> array.kt<pre><code>/* 배열의 인덱스 index에 원소 num 삽입 */\nfun insert(nums: IntArray, num: Int, index: Int) {\n // 인덱스 index와 그 뒤에 있는 모든 원소를 한 칸씩 뒤로 이동한다\n for (i in nums.size - 1 downTo index + 1) {\n nums[i] = nums[i - 1]\n }\n // index 위치의 원소에 num을 대입한다\n nums[index] = num\n}\n</code></pre> array.rb<pre><code>### 배열의 인덱스 index에 원소 num 삽입 ###\ndef insert(nums, num, index)\n # 인덱스 index와 그 뒤에 있는 모든 원소를 한 칸씩 뒤로 이동한다\n for i in (nums.length - 1).downto(index + 1)\n nums[i] = nums[i - 1]\n end\n\n # index 위치의 원소에 num을 대입한다\n nums[index] = num\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 4 장   배열과 연결 리스트","4.1   배열"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#4","level":3,"title":"4.   원소 삭제","text":"<p>마찬가지로 아래 그림(4-4)과 같이 인덱스 \\(i\\)의 원소를 삭제하려면 인덱스 \\(i\\) 뒤의 원소를 모두 한 칸씩 앞으로 옮겨야 한다.</p> <p></p> <p> 그림 4-4   배열에서 원소를 삭제하는 예 </p> <p>삭제가 끝난 뒤 원래의 마지막 원소는 ‘의미 없는’ 값이 되므로 따로 수정할 필요가 없다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py<pre><code>def remove(nums: list[int], index: int):\n \"\"\"인덱스 index에 있는 원소 삭제\"\"\"\n # 인덱스 index 뒤에 있는 모든 원소를 한 칸씩 앞으로 이동한다\n for i in range(index, len(nums) - 1):\n nums[i] = nums[i + 1]\n</code></pre> array.cpp<pre><code>/* 인덱스 index에 있는 원소 삭제 */\nvoid remove(int *nums, int size, int index) {\n // 인덱스 index 뒤에 있는 모든 원소를 한 칸씩 앞으로 이동한다\n for (int i = index; i &lt; size - 1; i++) {\n nums[i] = nums[i + 1];\n }\n}\n</code></pre> array.java<pre><code>/* 인덱스 index에 있는 원소 삭제 */\nvoid remove(int[] nums, int index) {\n // 인덱스 index 뒤에 있는 모든 원소를 한 칸씩 앞으로 이동한다\n for (int i = index; i &lt; nums.length - 1; i++) {\n nums[i] = nums[i + 1];\n }\n}\n</code></pre> array.cs<pre><code>/* 인덱스 index에 있는 원소 삭제 */\nvoid Remove(int[] nums, int index) {\n // 인덱스 index 뒤에 있는 모든 원소를 한 칸씩 앞으로 이동한다\n for (int i = index; i &lt; nums.Length - 1; i++) {\n nums[i] = nums[i + 1];\n }\n}\n</code></pre> array.go<pre><code>/* 인덱스 index에 있는 원소 삭제 */\nfunc remove(nums []int, index int) {\n // 인덱스 index 뒤에 있는 모든 원소를 한 칸씩 앞으로 이동한다\n for i := index; i &lt; len(nums)-1; i++ {\n nums[i] = nums[i+1]\n }\n}\n</code></pre> array.swift<pre><code>/* 인덱스 index에 있는 원소 삭제 */\nfunc remove(nums: inout [Int], index: Int) {\n // 인덱스 index 뒤에 있는 모든 원소를 한 칸씩 앞으로 이동한다\n for i in nums.indices.dropFirst(index).dropLast() {\n nums[i] = nums[i + 1]\n }\n}\n</code></pre> array.js<pre><code>/* 인덱스 index에 있는 원소 삭제 */\nfunction remove(nums, index) {\n // 인덱스 index 뒤에 있는 모든 원소를 한 칸씩 앞으로 이동한다\n for (let i = index; i &lt; nums.length - 1; i++) {\n nums[i] = nums[i + 1];\n }\n}\n</code></pre> array.ts<pre><code>/* 인덱스 index에 있는 원소 삭제 */\nfunction remove(nums: number[], index: number): void {\n // 인덱스 index 뒤에 있는 모든 원소를 한 칸씩 앞으로 이동한다\n for (let i = index; i &lt; nums.length - 1; i++) {\n nums[i] = nums[i + 1];\n }\n}\n</code></pre> array.dart<pre><code>/* 인덱스 index에 있는 원소 삭제 */\nvoid remove(List&lt;int&gt; nums, int index) {\n // 인덱스 index 뒤에 있는 모든 원소를 한 칸씩 앞으로 이동한다\n for (var i = index; i &lt; nums.length - 1; i++) {\n nums[i] = nums[i + 1];\n }\n}\n</code></pre> array.rs<pre><code>/* 인덱스 index에 있는 원소 삭제 */\nfn remove(nums: &amp;mut [i32], index: usize) {\n // 인덱스 index 뒤에 있는 모든 원소를 한 칸씩 앞으로 이동한다\n for i in index..nums.len() - 1 {\n nums[i] = nums[i + 1];\n }\n}\n</code></pre> array.c<pre><code>/* 인덱스 index에 있는 원소 삭제 */\n// 주의: stdio.h가 remove라는 이름을 사용한다\nvoid removeItem(int *nums, int size, int index) {\n // 인덱스 index 뒤에 있는 모든 원소를 한 칸씩 앞으로 이동한다\n for (int i = index; i &lt; size - 1; i++) {\n nums[i] = nums[i + 1];\n }\n}\n</code></pre> array.kt<pre><code>/* 인덱스 index에 있는 원소 삭제 */\nfun remove(nums: IntArray, index: Int) {\n // 인덱스 index 뒤에 있는 모든 원소를 한 칸씩 앞으로 이동한다\n for (i in index..&lt;nums.size - 1) {\n nums[i] = nums[i + 1]\n }\n}\n</code></pre> array.rb<pre><code>### 인덱스 index에 있는 원소 삭제 ###\ndef remove(nums, index)\n # 인덱스 index 뒤에 있는 모든 원소를 한 칸씩 앞으로 이동한다\n for i in index...(nums.length - 1)\n nums[i] = nums[i + 1]\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>배열의 삽입과 삭제 연산에는 다음과 같은 단점이 있다.</p> <ul> <li>높은 시간 복잡도: 삽입과 삭제의 평균 시간 복잡도는 모두 \\(O(n)\\)이며, \\(n\\)은 배열 길이다.</li> <li>원소 손실: 배열의 길이는 바꿀 수 없으므로 삽입 후 길이 범위를 벗어나는 원소가 사라진다.</li> <li>메모리 낭비: 긴 배열을 초기화하고 앞부분만 사용하면 삽입 시 사라지는 마지막 원소는 모두 ‘의미 없는’ 값이 된다. 하지만 이 방식은 메모리 공간 일부를 낭비한다.</li> </ul>","path":["제 4 장   배열과 연결 리스트","4.1   배열"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#5","level":3,"title":"5.   배열 순회","text":"<p>대부분의 프로그래밍 언어에서는 인덱스로 배열을 순회할 수도 있고, 배열의 각 원소를 직접 얻으며 순회할 수도 있다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py<pre><code>def traverse(nums: list[int]):\n \"\"\"배열 순회\"\"\"\n count = 0\n # 인덱스를 통해 배열을 순회한다\n for i in range(len(nums)):\n count += nums[i]\n # 배열의 원소를 직접 순회한다\n for num in nums:\n count += num\n # 데이터의 인덱스와 원소를 함께 순회한다\n for i, num in enumerate(nums):\n count += nums[i]\n count += num\n</code></pre> array.cpp<pre><code>/* 배열 순회 */\nvoid traverse(int *nums, int size) {\n int count = 0;\n // 인덱스를 통해 배열을 순회한다\n for (int i = 0; i &lt; size; i++) {\n count += nums[i];\n }\n}\n</code></pre> array.java<pre><code>/* 배열 순회 */\nvoid traverse(int[] nums) {\n int count = 0;\n // 인덱스를 통해 배열을 순회한다\n for (int i = 0; i &lt; nums.length; i++) {\n count += nums[i];\n }\n // 배열의 원소를 직접 순회한다\n for (int num : nums) {\n count += num;\n }\n}\n</code></pre> array.cs<pre><code>/* 배열 순회 */\nvoid Traverse(int[] nums) {\n int count = 0;\n // 인덱스를 통해 배열을 순회한다\n for (int i = 0; i &lt; nums.Length; i++) {\n count += nums[i];\n }\n // 배열의 원소를 직접 순회한다\n foreach (int num in nums) {\n count += num;\n }\n}\n</code></pre> array.go<pre><code>/* 배열 순회 */\nfunc traverse(nums []int) {\n count := 0\n // 인덱스를 통해 배열을 순회한다\n for i := 0; i &lt; len(nums); i++ {\n count += nums[i]\n }\n count = 0\n // 배열의 원소를 직접 순회한다\n for _, num := range nums {\n count += num\n }\n // 데이터의 인덱스와 원소를 함께 순회한다\n for i, num := range nums {\n count += nums[i]\n count += num\n }\n}\n</code></pre> array.swift<pre><code>/* 배열 순회 */\nfunc traverse(nums: [Int]) {\n var count = 0\n // 인덱스를 통해 배열을 순회한다\n for i in nums.indices {\n count += nums[i]\n }\n // 배열의 원소를 직접 순회한다\n for num in nums {\n count += num\n }\n // 데이터의 인덱스와 원소를 함께 순회한다\n for (i, num) in nums.enumerated() {\n count += nums[i]\n count += num\n }\n}\n</code></pre> array.js<pre><code>/* 배열 순회 */\nfunction traverse(nums) {\n let count = 0;\n // 인덱스를 통해 배열을 순회한다\n for (let i = 0; i &lt; nums.length; i++) {\n count += nums[i];\n }\n // 배열의 원소를 직접 순회한다\n for (const num of nums) {\n count += num;\n }\n}\n</code></pre> array.ts<pre><code>/* 배열 순회 */\nfunction traverse(nums: number[]): void {\n let count = 0;\n // 인덱스를 통해 배열을 순회한다\n for (let i = 0; i &lt; nums.length; i++) {\n count += nums[i];\n }\n // 배열의 원소를 직접 순회한다\n for (const num of nums) {\n count += num;\n }\n}\n</code></pre> array.dart<pre><code>/* 배열 원소 순회 */\nvoid traverse(List&lt;int&gt; nums) {\n int count = 0;\n // 인덱스를 통해 배열을 순회한다\n for (var i = 0; i &lt; nums.length; i++) {\n count += nums[i];\n }\n // 배열의 원소를 직접 순회한다\n for (int _num in nums) {\n count += _num;\n }\n // forEach 메서드로 배열을 순회한다\n nums.forEach((_num) {\n count += _num;\n });\n}\n</code></pre> array.rs<pre><code>/* 배열 순회 */\nfn traverse(nums: &amp;[i32]) {\n let mut _count = 0;\n // 인덱스를 통해 배열을 순회한다\n for i in 0..nums.len() {\n _count += nums[i];\n }\n // 배열의 원소를 직접 순회한다\n _count = 0;\n for &amp;num in nums {\n _count += num;\n }\n}\n</code></pre> array.c<pre><code>/* 배열 순회 */\nvoid traverse(int *nums, int size) {\n int count = 0;\n // 인덱스를 통해 배열을 순회한다\n for (int i = 0; i &lt; size; i++) {\n count += nums[i];\n }\n}\n</code></pre> array.kt<pre><code>/* 배열 순회 */\nfun traverse(nums: IntArray) {\n var count = 0\n // 인덱스를 통해 배열을 순회한다\n for (i in nums.indices) {\n count += nums[i]\n }\n // 배열의 원소를 직접 순회한다\n for (j in nums) {\n count += j\n }\n}\n</code></pre> array.rb<pre><code>### 배열 순회 ###\ndef traverse(nums)\n count = 0\n\n # 인덱스를 통해 배열을 순회한다\n for i in 0...nums.length\n count += nums[i]\n end\n\n # 배열의 원소를 직접 순회한다\n for num in nums\n count += num\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 4 장   배열과 연결 리스트","4.1   배열"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#6","level":3,"title":"6.   원소 탐색","text":"<p>배열에서 특정 원소를 찾으려면 배열을 순회하면서 매번 값이 일치하는지 확인하고, 일치하면 해당 인덱스를 출력한다.</p> <p>배열은 선형 자료 구조이므로 이 탐색 연산을 ‘선형 탐색’이라고 한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py<pre><code>def find(nums: list[int], target: int) -&gt; int:\n \"\"\"배열에서 지정한 원소 탐색\"\"\"\n for i in range(len(nums)):\n if nums[i] == target:\n return i\n return -1\n</code></pre> array.cpp<pre><code>/* 배열에서 지정한 원소 탐색 */\nint find(int *nums, int size, int target) {\n for (int i = 0; i &lt; size; i++) {\n if (nums[i] == target)\n return i;\n }\n return -1;\n}\n</code></pre> array.java<pre><code>/* 배열에서 지정한 원소 탐색 */\nint find(int[] nums, int target) {\n for (int i = 0; i &lt; nums.length; i++) {\n if (nums[i] == target)\n return i;\n }\n return -1;\n}\n</code></pre> array.cs<pre><code>/* 배열에서 지정한 원소 탐색 */\nint Find(int[] nums, int target) {\n for (int i = 0; i &lt; nums.Length; i++) {\n if (nums[i] == target)\n return i;\n }\n return -1;\n}\n</code></pre> array.go<pre><code>/* 배열에서 지정한 원소 탐색 */\nfunc find(nums []int, target int) (index int) {\n index = -1\n for i := 0; i &lt; len(nums); i++ {\n if nums[i] == target {\n index = i\n break\n }\n }\n return\n}\n</code></pre> array.swift<pre><code>/* 배열에서 지정한 원소 탐색 */\nfunc find(nums: [Int], target: Int) -&gt; Int {\n for i in nums.indices {\n if nums[i] == target {\n return i\n }\n }\n return -1\n}\n</code></pre> array.js<pre><code>/* 배열에서 지정한 원소 탐색 */\nfunction find(nums, target) {\n for (let i = 0; i &lt; nums.length; i++) {\n if (nums[i] === target) return i;\n }\n return -1;\n}\n</code></pre> array.ts<pre><code>/* 배열에서 지정한 원소 탐색 */\nfunction find(nums: number[], target: number): number {\n for (let i = 0; i &lt; nums.length; i++) {\n if (nums[i] === target) {\n return i;\n }\n }\n return -1;\n}\n</code></pre> array.dart<pre><code>/* 배열에서 지정한 원소 탐색 */\nint find(List&lt;int&gt; nums, int target) {\n for (var i = 0; i &lt; nums.length; i++) {\n if (nums[i] == target) return i;\n }\n return -1;\n}\n</code></pre> array.rs<pre><code>/* 배열에서 지정한 원소 탐색 */\nfn find(nums: &amp;[i32], target: i32) -&gt; Option&lt;usize&gt; {\n for i in 0..nums.len() {\n if nums[i] == target {\n return Some(i);\n }\n }\n None\n}\n</code></pre> array.c<pre><code>/* 배열에서 지정한 원소 탐색 */\nint find(int *nums, int size, int target) {\n for (int i = 0; i &lt; size; i++) {\n if (nums[i] == target)\n return i;\n }\n return -1;\n}\n</code></pre> array.kt<pre><code>/* 배열에서 지정한 원소 탐색 */\nfun find(nums: IntArray, target: Int): Int {\n for (i in nums.indices) {\n if (nums[i] == target)\n return i\n }\n return -1\n}\n</code></pre> array.rb<pre><code>### 배열에서 지정한 원소 탐색 ###\ndef find(nums, target)\n for i in 0...nums.length\n return i if nums[i] == target\n end\n\n -1\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 4 장   배열과 연결 리스트","4.1   배열"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#7","level":3,"title":"7.   배열 용량 확장","text":"<p>복잡한 시스템 환경에서는 배열 바로 뒤의 메모리 공간을 사용할 수 있다고 보장하기 어려우므로, 배열 용량을 안전하게 늘릴 수 없다. 따라서 대부분의 프로그래밍 언어에서 배열 길이는 바꿀 수 없다.</p> <p>배열 용량을 늘리려면 더 큰 배열을 새로 만들고 기존 원소를 차례로 복사해야 한다. 이는 \\(O(n)\\) 연산으로, 배열이 매우 크면 시간이 많이 든다. 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py<pre><code>def extend(nums: list[int], enlarge: int) -&gt; list[int]:\n \"\"\"배열 길이 확장\"\"\"\n # 길이를 확장한 배열을 초기화한다\n res = [0] * (len(nums) + enlarge)\n # 원래 배열의 모든 원소를 새 배열에 복사한다\n for i in range(len(nums)):\n res[i] = nums[i]\n # 확장한 새 배열을 반환한다\n return res\n</code></pre> array.cpp<pre><code>/* 배열 길이 확장 */\nint *extend(int *nums, int size, int enlarge) {\n // 길이를 확장한 배열을 초기화한다\n int *res = new int[size + enlarge];\n // 원래 배열의 모든 원소를 새 배열에 복사한다\n for (int i = 0; i &lt; size; i++) {\n res[i] = nums[i];\n }\n // 메모리 해제\n delete[] nums;\n // 확장한 새 배열을 반환한다\n return res;\n}\n</code></pre> array.java<pre><code>/* 배열 길이 확장 */\nint[] extend(int[] nums, int enlarge) {\n // 길이를 확장한 배열을 초기화한다\n int[] res = new int[nums.length + enlarge];\n // 원래 배열의 모든 원소를 새 배열에 복사한다\n for (int i = 0; i &lt; nums.length; i++) {\n res[i] = nums[i];\n }\n // 확장한 새 배열을 반환한다\n return res;\n}\n</code></pre> array.cs<pre><code>/* 배열 길이 확장 */\nint[] Extend(int[] nums, int enlarge) {\n // 길이를 확장한 배열을 초기화한다\n int[] res = new int[nums.Length + enlarge];\n // 원래 배열의 모든 원소를 새 배열에 복사한다\n for (int i = 0; i &lt; nums.Length; i++) {\n res[i] = nums[i];\n }\n // 확장한 새 배열을 반환한다\n return res;\n}\n</code></pre> array.go<pre><code>/* 배열 길이 확장 */\nfunc extend(nums []int, enlarge int) []int {\n // 길이를 확장한 배열을 초기화한다\n res := make([]int, len(nums)+enlarge)\n // 원래 배열의 모든 원소를 새 배열에 복사한다\n for i, num := range nums {\n res[i] = num\n }\n // 확장한 새 배열을 반환한다\n return res\n}\n</code></pre> array.swift<pre><code>/* 배열 길이 확장 */\nfunc extend(nums: [Int], enlarge: Int) -&gt; [Int] {\n // 길이를 확장한 배열을 초기화한다\n var res = Array(repeating: 0, count: nums.count + enlarge)\n // 원래 배열의 모든 원소를 새 배열에 복사한다\n for i in nums.indices {\n res[i] = nums[i]\n }\n // 확장한 새 배열을 반환한다\n return res\n}\n</code></pre> array.js<pre><code>/* 배열 길이 확장 */\n// JavaScript의 Array는 동적 배열이므로 직접 확장할 수 있다\n// 학습의 편의를 위해 이 함수에서는 Array를 길이가 고정된 배열로 취급한다\nfunction extend(nums, enlarge) {\n // 길이를 확장한 배열을 초기화한다\n const res = new Array(nums.length + enlarge).fill(0);\n // 원래 배열의 모든 원소를 새 배열에 복사한다\n for (let i = 0; i &lt; nums.length; i++) {\n res[i] = nums[i];\n }\n // 확장한 새 배열을 반환한다\n return res;\n}\n</code></pre> array.ts<pre><code>/* 배열 길이 확장 */\n// TypeScript의 Array는 동적 배열이므로 직접 확장할 수 있다\n// 학습의 편의를 위해 이 함수에서는 Array를 길이가 고정된 배열로 취급한다\nfunction extend(nums: number[], enlarge: number): number[] {\n // 길이를 확장한 배열을 초기화한다\n const res = new Array(nums.length + enlarge).fill(0);\n // 원래 배열의 모든 원소를 새 배열에 복사한다\n for (let i = 0; i &lt; nums.length; i++) {\n res[i] = nums[i];\n }\n // 확장한 새 배열을 반환한다\n return res;\n}\n</code></pre> array.dart<pre><code>/* 배열 길이 확장 */\nList&lt;int&gt; extend(List&lt;int&gt; nums, int enlarge) {\n // 길이를 확장한 배열을 초기화한다\n List&lt;int&gt; res = List.filled(nums.length + enlarge, 0);\n // 원래 배열의 모든 원소를 새 배열에 복사한다\n for (var i = 0; i &lt; nums.length; i++) {\n res[i] = nums[i];\n }\n // 확장한 새 배열을 반환한다\n return res;\n}\n</code></pre> array.rs<pre><code>/* 배열 길이 확장 */\nfn extend(nums: &amp;[i32], enlarge: usize) -&gt; Vec&lt;i32&gt; {\n // 길이를 확장한 배열을 초기화한다\n let mut res: Vec&lt;i32&gt; = vec![0; nums.len() + enlarge];\n // 원래 배열의 모든 원소를 새 배열로 복사한다\n res[0..nums.len()].copy_from_slice(nums);\n\n // 확장한 새 배열을 반환한다\n res\n}\n</code></pre> array.c<pre><code>/* 배열 길이 확장 */\nint *extend(int *nums, int size, int enlarge) {\n // 길이를 확장한 배열을 초기화한다\n int *res = (int *)malloc(sizeof(int) * (size + enlarge));\n // 원래 배열의 모든 원소를 새 배열에 복사한다\n for (int i = 0; i &lt; size; i++) {\n res[i] = nums[i];\n }\n // 확장한 공간 초기화\n for (int i = size; i &lt; size + enlarge; i++) {\n res[i] = 0;\n }\n // 확장한 새 배열을 반환한다\n return res;\n}\n</code></pre> array.kt<pre><code>/* 배열 길이 확장 */\nfun extend(nums: IntArray, enlarge: Int): IntArray {\n // 길이를 확장한 배열을 초기화한다\n val res = IntArray(nums.size + enlarge)\n // 원래 배열의 모든 원소를 새 배열에 복사한다\n for (i in nums.indices) {\n res[i] = nums[i]\n }\n // 확장한 새 배열을 반환한다\n return res\n}\n</code></pre> array.rb<pre><code>### 배열 길이 확장 ###\n# Ruby의 Array는 동적 배열이므로 직접 확장할 수 있다\n# 학습의 편의를 위해 이 함수에서는 Array를 길이가 고정된 배열로 취급한다\ndef extend(nums, enlarge)\n # 길이를 확장한 배열을 초기화한다\n res = Array.new(nums.length + enlarge, 0)\n\n # 원래 배열의 모든 원소를 새 배열에 복사한다\n for i in 0...nums.length\n res[i] = nums[i]\n end\n\n # 확장한 새 배열을 반환한다\n res\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 4 장   배열과 연결 리스트","4.1   배열"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#412","level":2,"title":"4.1.2   배열의 장점과 한계","text":"<p>배열은 연속된 메모리 공간에 저장되고 원소의 자료형이 모두 같다. 이러한 풍부한 사전 정보를 활용하면 시스템은 자료 구조의 연산 효율을 최적화할 수 있다.</p> <ul> <li>높은 공간 효율: 연속된 메모리 블록을 데이터에 할당하므로 추가적인 구조 비용이 없다.</li> <li>임의 접근 지원: 어떤 원소든 \\(O(1)\\) 시간에 접근할 수 있다.</li> <li>캐시 지역성: 배열 원소에 접근할 때 컴퓨터는 그 원소뿐 아니라 주변 데이터도 캐시에 저장한다. 이를 통해 이후 연산의 실행 속도를 높일 수 있다.</li> </ul> <p>연속 공간 저장은 양날의 검으로, 다음과 같은 한계가 있다.</p> <ul> <li>낮은 삽입 및 삭제 효율: 원소가 많으면 삽입이나 삭제 시 많은 원소를 이동해야 한다.</li> <li>고정된 길이: 초기화한 뒤에는 길이가 고정된다. 용량을 늘리려면 모든 데이터를 새 배열로 복사해야 하므로 비용이 크다.</li> <li>공간 낭비: 실제 필요한 크기보다 크게 할당하면 남는 공간이 낭비된다.</li> </ul>","path":["제 4 장   배열과 연결 리스트","4.1   배열"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#413","level":2,"title":"4.1.3   배열의 대표적인 활용","text":"<p>배열은 기본적이면서도 흔히 쓰이는 자료 구조다. 다양한 알고리즘에 자주 사용되며 복잡한 자료 구조를 구현하는 데도 쓰인다.</p> <ul> <li>임의 접근: 일부 표본을 무작위로 추출하려면 데이터를 배열에 저장하고 무작위 수열을 생성하여 인덱스에 따라 표본을 뽑을 수 있다.</li> <li>정렬과 탐색: 배열은 정렬 및 탐색 알고리즘에서 가장 많이 쓰이는 자료 구조다. 퀵 정렬, 병합 정렬, 이진 탐색 등은 주로 배열에서 수행된다.</li> <li>조회 테이블: 원소나 대응 관계를 빠르게 찾고 싶을 때 배열을 조회 테이블로 사용할 수 있다. 예를 들어 문자와 ASCII 코드의 대응을 구현하려면 문자의 ASCII 코드 값을 인덱스로 삼고, 해당 원소를 배열의 대응 위치에 저장하면 된다.</li> <li>머신 러닝: 신경망에서는 벡터, 행렬, 텐서 사이의 선형 대수 연산을 많이 사용하며, 이 데이터들은 모두 배열 형태로 구성된다. 배열은 신경망 프로그래밍에서 가장 많이 쓰는 자료 구조다.</li> <li>자료 구조 구현: 배열로 스택, 큐, 해시 테이블, 힙, 그래프 등을 구현할 수 있다. 예를 들어 그래프의 인접 행렬 표현은 실제로 이차원 배열이다.</li> </ul>","path":["제 4 장   배열과 연결 리스트","4.1   배열"],"tags":[]},{"location":"chapter_array_and_linkedlist/exercises/","level":1,"title":"4.6   연습 문제","text":"","path":["제 4 장   배열과 연결 리스트","4.6   연습 문제"],"tags":[]},{"location":"chapter_array_and_linkedlist/exercises/#461","level":2,"title":"4.6.1   개념 확인","text":"","path":["제 4 장   배열과 연결 리스트","4.6   연습 문제"],"tags":[]},{"location":"chapter_array_and_linkedlist/exercises/#1","level":3,"title":"1.   배열과 연결 리스트에서 원소 찾기","text":"<p>배열과 단일 연결 리스트에 모두 <code>[A, B, C, D, E]</code>가 순서대로 저장되어 있다. 네 번째 원소 <code>D</code>를 읽으려고 한다.</p> <ol> <li>배열에서는 어떤 인덱스를 바로 사용할 수 있는가?</li> <li>단일 연결 리스트에서는 머리 노드 <code>A</code>부터 <code>next</code>를 따라 어떤 노드를 차례대로 거쳐야 하는가?</li> <li>읽으려는 원소가 뒤쪽에 있을수록 두 구조에 필요한 단계 수는 어떻게 변하는가? 위치를 기준으로 반복해서 읽을 때는 어느 구조가 더 적합하며, 그 이유는 무엇인가?</li> </ol> 참고 답안 <ol> <li> <p>인덱스가 0부터 시작한다면 네 번째 원소의 인덱스는 3이므로, 배열에서는 <code>arr[3]</code>을 바로 읽을 수 있다.</p> </li> <li> <p>단일 연결 리스트는 맨 앞부터 시작해야 한다. 접근 경로는 <code>A → B → C → D</code>이며, <code>next</code>를 따라 3번 이동해야 한다.</p> </li> <li> <p>배열은 시작 주소와 인덱스로 원소의 위치를 바로 계산할 수 있으므로 위치를 기준으로 접근하는 시간 복잡도가 \\(O(1)\\)이다. 단일 연결 리스트에서 \\(k\\)번째 노드에 접근하려면 머리 노드부터 <code>next</code>를 따라 \\(k-1\\)번 이동해야 하므로, 최악의 경우 \\(O(n)\\) 시간이 필요하다.</p> <p>여기서는 위치를 기준으로 읽는 연산만 비교한 것이며, 연결 리스트가 모든 연산에서 더 느리다는 뜻은 아니다.</p> </li> </ol>","path":["제 4 장   배열과 연결 리스트","4.6   연습 문제"],"tags":[]},{"location":"chapter_array_and_linkedlist/exercises/#2","level":3,"title":"2.   배열과 연결 리스트에 원소 삽입하기","text":"<p>배열과 단일 연결 리스트에 모두 <code>A, B, C, D</code>가 저장되어 있다. <code>B</code> 뒤에 <code>X</code>를 삽입하려고 한다.</p> <ul> <li>배열의 용량은 5이고 현재 상태는 <code>[A, B, C, D, _]</code>다.</li> <li>연결 리스트는 <code>A → B → C → D</code>이며, 노드 <code>B</code>를 가리키는 참조를 이미 가지고 있다.</li> </ul> <ol> <li>배열에서는 어떤 원소를 이동해야 하는가? 삽입 후 배열을 적어 보자.</li> <li>연결 리스트에서는 <code>X.next</code>와 <code>B.next</code>를 어떤 순서로 수정해야 하는가? 삽입 후 연결 리스트를 적어 보자.</li> <li>삽입 효율성을 비교할 때 ‘노드 B의 참조를 이미 가지고 있다’는 조건을 강조하는 이유는 무엇인가?</li> </ol> 참고 답안 <ol> <li> <p>배열에서는 먼저 <code>D</code>를 오른쪽으로 한 칸 옮기고, 다음으로 <code>C</code>를 오른쪽으로 한 칸 옮긴 뒤, <code>X</code>를 인덱스 2에 넣는다. 결과는 <code>[A, B, X, C, D]</code>다.</p> </li> <li> <p><code>B.next</code>는 원래 <code>C</code>를 가리킨다. 먼저 <code>X.next = B.next</code>로 설정하여 <code>X</code>가 <code>C</code>를 가리키게 하고, 다음으로 <code>B.next = X</code>로 설정한다. 결과는 <code>A → B → X → C → D</code>다. 기존 연결을 저장하지 않은 채 <code>B.next</code>를 먼저 덮어쓰면 <code>C</code>를 찾지 못할 수 있다.</p> </li> <li> <p><code>B</code>의 위치를 알고 있다면 연결 리스트의 삽입은 연결 두 개만 수정하면 되므로 \\(O(1)\\) 시간에 완료할 수 있다. 맨 앞부터 <code>B</code>를 찾아야 한다면 탐색 자체에 \\(O(n)\\) 시간이 걸릴 수 있다.</p> </li> </ol>","path":["제 4 장   배열과 연결 리스트","4.6   연습 문제"],"tags":[]},{"location":"chapter_array_and_linkedlist/exercises/#3","level":3,"title":"3.   리스트의 용량은 어떻게 늘어나는가","text":"<p>배열로 구현한 리스트의 현재 내용은 <code>[A, B, C]</code>이고, 길이는 <code>size = 3</code>, 용량은 <code>capacity = 4</code>다. 용량이 부족해지면 새 배열의 용량을 기존의 2배로 늘리기로 한다.</p> <ol> <li><code>D</code>를 추가한 뒤 리스트의 길이와 용량은 각각 얼마인가? 용량을 늘려야 하는가?</li> <li>이어서 <code>E</code>를 추가하면 용량은 얼마가 되는가? 기존 원소를 몇 개 복사해야 하는가?</li> <li>내부 배열의 길이는 바꿀 수 없는데, 리스트의 용량은 어떻게 늘어나는 것처럼 보이는가?</li> </ol> 참고 답안 <ol> <li> <p><code>D</code>는 마지막 빈자리에 넣을 수 있다. 내용은 <code>[A, B, C, D]</code>가 되고, <code>size = 4</code>, <code>capacity = 4</code>이며 용량 확장은 필요 없다.</p> </li> <li> <p>이어서 <code>E</code>를 추가할 때는 빈자리가 없으므로 용량이 8인 새 배열을 만들어야 한다. 기존 원소 <code>A, B, C, D</code> 네 개를 복사한 뒤 <code>E</code>를 넣는다. 이때 <code>size = 5</code>, <code>capacity = 8</code>이다.</p> </li> <li> <p>원래 배열 자체의 길이가 늘어난 것은 아니다. 더 큰 새 배열을 만들고 기존 원소를 복사한 뒤, 새 배열을 내부 저장 공간으로 사용하기 때문에 사용자에게는 용량이 늘어난 것으로 보인다.</p> </li> </ol>","path":["제 4 장   배열과 연결 리스트","4.6   연습 문제"],"tags":[]},{"location":"chapter_array_and_linkedlist/exercises/#462","level":2,"title":"4.6.2   프로그래밍 연습","text":"","path":["제 4 장   배열과 연결 리스트","4.6   연습 문제"],"tags":[]},{"location":"chapter_array_and_linkedlist/exercises/#1-1","level":3,"title":"1.   배열로 표현한 큰 정수에 1 더하기","text":"<p>배열 <code>digits</code>에는 음이 아닌 정수의 각 자릿수가 왼쪽부터 저장되어 있다. 예를 들어 <code>[3, 0, 8]</code>은 308을 나타낸다. 숫자 0은 <code>[0]</code>으로 나타내며, 그 밖의 입력은 첫 번째 자릿수가 0이 아니다.</p> <p>십진수의 세로셈 덧셈 과정을 따라 이 정수에 1을 더한 뒤, 결과를 같은 배열 형식으로 반환하라. <code>digits</code>를 직접 수정해도 된다. 맨 앞에 새로운 자리 올림이 생기면 더 긴 배열을 반환할 수 있다.</p> 풀이 힌트 <ol> <li>세로셈 덧셈처럼 배열의 마지막 자릿수부터 시작한다.</li> <li>현재 자릿수가 9보다 작으면 1을 더한 뒤 바로 반환할 수 있다.</li> <li>현재 자릿수가 9라면 0으로 바꾼다. 모든 자릿수가 9였다면 맨 앞에 1을 추가해야 한다.</li> </ol> <p>LeetCode</p>","path":["제 4 장   배열과 연결 리스트","4.6   연습 문제"],"tags":[]},{"location":"chapter_array_and_linkedlist/exercises/#2_1","level":3,"title":"2.   단일 연결 리스트 뒤집기","text":"<p>단일 연결 리스트의 머리 노드 <code>head</code>가 주어진다. 각 노드에는 값과 다음 노드를 가리키는 <code>next</code>가 있다.</p> <p>반복 방식으로 모든 노드 사이의 연결 방향을 뒤집고, 뒤집힌 리스트의 머리 노드를 반환하라. 새로운 연결 리스트 노드를 만들어서는 안 된다.</p> 풀이 힌트 <ol> <li>먼저 종이에 연결된 노드 세 개와 prev, cur 두 포인터를 그려 보자.</li> <li>cur.next를 바꾸기 전에 nxt에 원래 다음 노드를 반드시 저장해야 한다.</li> <li>cur.next의 방향을 뒤집은 뒤 prev = cur, cur = nxt로 설정하고 원래 리스트의 다음 노드를 계속 처리한다.</li> </ol> <p>LeetCode</p>","path":["제 4 장   배열과 연결 리스트","4.6   연습 문제"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/","level":1,"title":"4.2   연결 리스트","text":"<p>메모리 공간은 모든 프로그램이 함께 사용하는 자원이다. 복잡한 시스템 실행 환경에서는 빈 메모리 공간이 곳곳에 흩어져 있을 수 있다. 배열을 저장할 메모리는 연속적이어야 하므로, 배열이 매우 크면 그만큼 큰 연속 공간을 확보하지 못할 수도 있다. 이때 연결 리스트의 유연성이 장점으로 드러난다.</p> <p>연결 리스트(linked list)는 각 원소가 노드 객체이며, 노드들이 ‘참조’로 연결된 선형 자료 구조다. 참조는 다음 노드의 메모리 주소를 기록하므로, 이를 통해 현재 노드에서 다음 노드에 접근할 수 있다.</p> <p>이러한 설계 덕분에 각 노드는 메모리 곳곳에 분산되어 저장될 수 있으며, 메모리 주소가 연속적일 필요가 없다.</p> <p></p> <p> 그림 4-5   연결 리스트의 정의와 저장 방식 </p> <p>위 그림(4-5)에서 연결 리스트를 구성하는 단위는 노드(node) 객체다. 각 노드는 노드의 ‘값’과 다음 노드를 가리키는 ‘참조’라는 두 데이터를 포함한다.</p> <ul> <li>연결 리스트의 첫 노드를 ‘머리 노드’, 마지막 노드를 ‘꼬리 노드’라고 한다.</li> <li>꼬리 노드는 ‘없음’을 가리키며, Java, C++, Python에서는 이를 각각 <code>null</code>, <code>nullptr</code>, <code>None</code>으로 표기한다.</li> <li>C, C++, Go, Rust처럼 포인터를 지원하는 언어에서는 위의 ‘참조’를 ‘포인터’로 바꾸어 이해하면 된다.</li> </ul> <p>다음 코드와 같이 연결 리스트 노드 <code>ListNode</code>는 값 외에도 참조(포인터)를 하나 더 저장해야 한다. 따라서 같은 양의 데이터를 저장할 때 연결 리스트는 배열보다 메모리 공간을 더 많이 차지한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby <pre><code>class ListNode:\n \"\"\"연결 리스트 노드 클래스\"\"\"\n def __init__(self, val: int):\n self.val: int = val # 노드 값\n self.next: ListNode | None = None # 다음 노드를 가리키는 참조\n</code></pre> <pre><code>/* 연결 리스트 노드 구조체 */\nstruct ListNode {\n int val; // 노드 값\n ListNode *next; // 다음 노드를 가리키는 포인터\n ListNode(int x) : val(x), next(nullptr) {} // 생성자\n};\n</code></pre> <pre><code>/* 연결 리스트 노드 클래스 */\nclass ListNode {\n int val; // 노드 값\n ListNode next; // 다음 노드를 가리키는 참조\n ListNode(int x) { val = x; } // 생성자\n}\n</code></pre> <pre><code>/* 연결 리스트 노드 클래스 */\nclass ListNode(int x) { //생성자\n int val = x; // 노드 값\n ListNode? next; // 다음 노드를 가리키는 참조\n}\n</code></pre> <pre><code>/* 연결 리스트 노드 구조체 */\ntype ListNode struct {\n Val int // 노드 값\n Next *ListNode // 다음 노드를 가리키는 포인터\n}\n\n// NewListNode 생성자: 새 연결 리스트 생성\nfunc NewListNode(val int) *ListNode {\n return &amp;ListNode{\n Val: val,\n Next: nil,\n }\n}\n</code></pre> <pre><code>/* 연결 리스트 노드 클래스 */\nclass ListNode {\n var val: Int // 노드 값\n var next: ListNode? // 다음 노드를 가리키는 참조\n\n init(x: Int) { // 생성자\n val = x\n }\n}\n</code></pre> <pre><code>/* 연결 리스트 노드 클래스 */\nclass ListNode {\n constructor(val, next) {\n this.val = (val === undefined ? 0 : val); // 노드 값\n this.next = (next === undefined ? null : next); // 다음 노드를 가리키는 참조\n }\n}\n</code></pre> <pre><code>/* 연결 리스트 노드 클래스 */\nclass ListNode {\n val: number;\n next: ListNode | null;\n constructor(val?: number, next?: ListNode | null) {\n this.val = val === undefined ? 0 : val; // 노드 값\n this.next = next === undefined ? null : next; // 다음 노드를 가리키는 참조\n }\n}\n</code></pre> <pre><code>/* 연결 리스트 노드 클래스 */\nclass ListNode {\n int val; // 노드 값\n ListNode? next; // 다음 노드를 가리키는 참조\n ListNode(this.val, [this.next]); // 생성자\n}\n</code></pre> <pre><code>use std::rc::Rc;\nuse std::cell::RefCell;\n/* 연결 리스트 노드 클래스 */\n#[derive(Debug)]\nstruct ListNode {\n val: i32, // 노드 값\n next: Option&lt;Rc&lt;RefCell&lt;ListNode&gt;&gt;&gt;, // 다음 노드를 가리키는 포인터\n}\n</code></pre> <pre><code>/* 연결 리스트 노드 구조체 */\ntypedef struct ListNode {\n int val; // 노드 값\n struct ListNode *next; // 다음 노드를 가리키는 포인터\n} ListNode;\n\n/* 생성자 */\nListNode *newListNode(int val) {\n ListNode *node;\n node = (ListNode *) malloc(sizeof(ListNode));\n node-&gt;val = val;\n node-&gt;next = NULL;\n return node;\n}\n</code></pre> <pre><code>/* 연결 리스트 노드 클래스 */\n// 생성자\nclass ListNode(x: Int) {\n val _val: Int = x // 노드 값\n var next: ListNode? = null // 다음 노드를 가리키는 참조\n}\n</code></pre> <pre><code># 연결 리스트 노드 클래스\nclass ListNode\n attr_accessor :val # 노드 값\n attr_accessor :next # 다음 노드를 가리키는 참조\n\n def initialize(val=0, next_node=nil)\n @val = val\n @next = next_node\n end\nend\n</code></pre>","path":["제 4 장   배열과 연결 리스트","4.2   연결 리스트"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#421","level":2,"title":"4.2.1   연결 리스트의 기본 연산","text":"","path":["제 4 장   배열과 연결 리스트","4.2   연결 리스트"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#1","level":3,"title":"1.   연결 리스트 초기화","text":"<p>연결 리스트를 만드는 과정은 두 단계다. 먼저 각 노드 객체를 초기화한 다음, 노드 사이의 참조 관계를 설정한다. 초기화가 끝나면 머리 노드부터 시작하여 <code>next</code> 참조를 따라 모든 노드에 차례로 접근할 수 있다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py<pre><code># 연결 리스트 1 -&gt; 3 -&gt; 2 -&gt; 5 -&gt; 4 초기화\n# 각 노드 초기화\nn0 = ListNode(1)\nn1 = ListNode(3)\nn2 = ListNode(2)\nn3 = ListNode(5)\nn4 = ListNode(4)\n# 노드 사이의 참조 설정\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n</code></pre> linked_list.cpp<pre><code>/* 연결 리스트 1 -&gt; 3 -&gt; 2 -&gt; 5 -&gt; 4 초기화 */\n// 각 노드 초기화\nListNode* n0 = new ListNode(1);\nListNode* n1 = new ListNode(3);\nListNode* n2 = new ListNode(2);\nListNode* n3 = new ListNode(5);\nListNode* n4 = new ListNode(4);\n// 노드 사이의 참조 설정\nn0-&gt;next = n1;\nn1-&gt;next = n2;\nn2-&gt;next = n3;\nn3-&gt;next = n4;\n</code></pre> linked_list.java<pre><code>/* 연결 리스트 1 -&gt; 3 -&gt; 2 -&gt; 5 -&gt; 4 초기화 */\n// 각 노드 초기화\nListNode n0 = new ListNode(1);\nListNode n1 = new ListNode(3);\nListNode n2 = new ListNode(2);\nListNode n3 = new ListNode(5);\nListNode n4 = new ListNode(4);\n// 노드 사이의 참조 설정\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n</code></pre> linked_list.cs<pre><code>/* 연결 리스트 1 -&gt; 3 -&gt; 2 -&gt; 5 -&gt; 4 초기화 */\n// 각 노드 초기화\nListNode n0 = new(1);\nListNode n1 = new(3);\nListNode n2 = new(2);\nListNode n3 = new(5);\nListNode n4 = new(4);\n// 노드 사이의 참조 설정\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n</code></pre> linked_list.go<pre><code>/* 연결 리스트 1 -&gt; 3 -&gt; 2 -&gt; 5 -&gt; 4 초기화 */\n// 각 노드 초기화\nn0 := NewListNode(1)\nn1 := NewListNode(3)\nn2 := NewListNode(2)\nn3 := NewListNode(5)\nn4 := NewListNode(4)\n// 노드 사이의 참조 설정\nn0.Next = n1\nn1.Next = n2\nn2.Next = n3\nn3.Next = n4\n</code></pre> linked_list.swift<pre><code>/* 연결 리스트 1 -&gt; 3 -&gt; 2 -&gt; 5 -&gt; 4 초기화 */\n// 각 노드 초기화\nlet n0 = ListNode(x: 1)\nlet n1 = ListNode(x: 3)\nlet n2 = ListNode(x: 2)\nlet n3 = ListNode(x: 5)\nlet n4 = ListNode(x: 4)\n// 노드 사이의 참조 설정\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n</code></pre> linked_list.js<pre><code>/* 연결 리스트 1 -&gt; 3 -&gt; 2 -&gt; 5 -&gt; 4 초기화 */\n// 각 노드 초기화\nconst n0 = new ListNode(1);\nconst n1 = new ListNode(3);\nconst n2 = new ListNode(2);\nconst n3 = new ListNode(5);\nconst n4 = new ListNode(4);\n// 노드 사이의 참조 설정\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n</code></pre> linked_list.ts<pre><code>/* 연결 리스트 1 -&gt; 3 -&gt; 2 -&gt; 5 -&gt; 4 초기화 */\n// 각 노드 초기화\nconst n0 = new ListNode(1);\nconst n1 = new ListNode(3);\nconst n2 = new ListNode(2);\nconst n3 = new ListNode(5);\nconst n4 = new ListNode(4);\n// 노드 사이의 참조 설정\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n</code></pre> linked_list.dart<pre><code>/* 연결 리스트 1 -&gt; 3 -&gt; 2 -&gt; 5 -&gt; 4 초기화 */\\\n// 각 노드 초기화\nListNode n0 = ListNode(1);\nListNode n1 = ListNode(3);\nListNode n2 = ListNode(2);\nListNode n3 = ListNode(5);\nListNode n4 = ListNode(4);\n// 노드 사이의 참조 설정\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n</code></pre> linked_list.rs<pre><code>/* 연결 리스트 1 -&gt; 3 -&gt; 2 -&gt; 5 -&gt; 4 초기화 */\n// 각 노드 초기화\nlet n0 = Rc::new(RefCell::new(ListNode { val: 1, next: None }));\nlet n1 = Rc::new(RefCell::new(ListNode { val: 3, next: None }));\nlet n2 = Rc::new(RefCell::new(ListNode { val: 2, next: None }));\nlet n3 = Rc::new(RefCell::new(ListNode { val: 5, next: None }));\nlet n4 = Rc::new(RefCell::new(ListNode { val: 4, next: None }));\n\n// 노드 사이의 참조 설정\nn0.borrow_mut().next = Some(n1.clone());\nn1.borrow_mut().next = Some(n2.clone());\nn2.borrow_mut().next = Some(n3.clone());\nn3.borrow_mut().next = Some(n4.clone());\n</code></pre> linked_list.c<pre><code>/* 연결 리스트 1 -&gt; 3 -&gt; 2 -&gt; 5 -&gt; 4 초기화 */\n// 각 노드 초기화\nListNode* n0 = newListNode(1);\nListNode* n1 = newListNode(3);\nListNode* n2 = newListNode(2);\nListNode* n3 = newListNode(5);\nListNode* n4 = newListNode(4);\n// 노드 사이의 참조 설정\nn0-&gt;next = n1;\nn1-&gt;next = n2;\nn2-&gt;next = n3;\nn3-&gt;next = n4;\n</code></pre> linked_list.kt<pre><code>/* 연결 리스트 1 -&gt; 3 -&gt; 2 -&gt; 5 -&gt; 4 초기화 */\n// 각 노드 초기화\nval n0 = ListNode(1)\nval n1 = ListNode(3)\nval n2 = ListNode(2)\nval n3 = ListNode(5)\nval n4 = ListNode(4)\n// 노드 사이의 참조 설정\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n</code></pre> linked_list.rb<pre><code># 연결 리스트 1 -&gt; 3 -&gt; 2 -&gt; 5 -&gt; 4 초기화\n# 각 노드 초기화\nn0 = ListNode.new(1)\nn1 = ListNode.new(3)\nn2 = ListNode.new(2)\nn3 = ListNode.new(5)\nn4 = ListNode.new(4)\n# 노드 사이의 참조 설정\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>배열은 전체가 하나의 변수다. 예를 들어 배열 <code>nums</code>에는 <code>nums[0]</code>, <code>nums[1]</code> 등의 원소가 들어 있다. 반면 연결 리스트는 독립된 여러 노드 객체로 이루어진다. 보통 머리 노드로 연결 리스트 전체를 지칭한다. 예를 들어 위 코드의 연결 리스트는 연결 리스트 <code>n0</code>라고 부를 수 있다.</p>","path":["제 4 장   배열과 연결 리스트","4.2   연결 리스트"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#2","level":3,"title":"2.   노드 삽입","text":"<p>연결 리스트에 노드를 삽입하기는 매우 쉽다. 아래 그림(4-6)과 같이 인접한 노드 <code>n0</code>와 <code>n1</code> 사이에 새 노드 <code>P</code>를 삽입하려면 노드 참조(포인터) 두 개만 바꾸면 된다. 시간 복잡도는 \\(O(1)\\)이다.</p> <p>반면 배열의 원소 삽입은 시간 복잡도가 \\(O(n)\\)이므로 데이터가 많을 때 효율이 낮다.</p> <p></p> <p> 그림 4-6   연결 리스트에 노드를 삽입하는 예 </p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py<pre><code>def insert(n0: ListNode, P: ListNode):\n \"\"\"연결 리스트의 노드 n0 뒤에 노드 P 삽입\"\"\"\n n1 = n0.next\n P.next = n1\n n0.next = P\n</code></pre> linked_list.cpp<pre><code>/* 연결 리스트의 노드 n0 뒤에 노드 P 삽입 */\nvoid insert(ListNode *n0, ListNode *P) {\n ListNode *n1 = n0-&gt;next;\n P-&gt;next = n1;\n n0-&gt;next = P;\n}\n</code></pre> linked_list.java<pre><code>/* 연결 리스트의 노드 n0 뒤에 노드 P 삽입 */\nvoid insert(ListNode n0, ListNode P) {\n ListNode n1 = n0.next;\n P.next = n1;\n n0.next = P;\n}\n</code></pre> linked_list.cs<pre><code>/* 연결 리스트의 노드 n0 뒤에 노드 P 삽입 */\nvoid Insert(ListNode n0, ListNode P) {\n ListNode? n1 = n0.next;\n P.next = n1;\n n0.next = P;\n}\n</code></pre> linked_list.go<pre><code>/* 연결 리스트의 노드 n0 뒤에 노드 P 삽입 */\nfunc insertNode(n0 *ListNode, P *ListNode) {\n n1 := n0.Next\n P.Next = n1\n n0.Next = P\n}\n</code></pre> linked_list.swift<pre><code>/* 연결 리스트의 노드 n0 뒤에 노드 P 삽입 */\nfunc insert(n0: ListNode, P: ListNode) {\n let n1 = n0.next\n P.next = n1\n n0.next = P\n}\n</code></pre> linked_list.js<pre><code>/* 연결 리스트의 노드 n0 뒤에 노드 P 삽입 */\nfunction insert(n0, P) {\n const n1 = n0.next;\n P.next = n1;\n n0.next = P;\n}\n</code></pre> linked_list.ts<pre><code>/* 연결 리스트의 노드 n0 뒤에 노드 P 삽입 */\nfunction insert(n0: ListNode, P: ListNode): void {\n const n1 = n0.next;\n P.next = n1;\n n0.next = P;\n}\n</code></pre> linked_list.dart<pre><code>/* 연결 리스트의 노드 n0 뒤에 노드 P 삽입 */\nvoid insert(ListNode n0, ListNode P) {\n ListNode? n1 = n0.next;\n P.next = n1;\n n0.next = P;\n}\n</code></pre> linked_list.rs<pre><code>/* 연결 리스트의 노드 n0 뒤에 노드 P 삽입 */\n#[allow(non_snake_case)]\npub fn insert&lt;T&gt;(n0: &amp;Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt;, P: Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt;) {\n let n1 = n0.borrow_mut().next.take();\n P.borrow_mut().next = n1;\n n0.borrow_mut().next = Some(P);\n}\n</code></pre> linked_list.c<pre><code>/* 연결 리스트의 노드 n0 뒤에 노드 P 삽입 */\nvoid insert(ListNode *n0, ListNode *P) {\n ListNode *n1 = n0-&gt;next;\n P-&gt;next = n1;\n n0-&gt;next = P;\n}\n</code></pre> linked_list.kt<pre><code>/* 연결 리스트의 노드 n0 뒤에 노드 P 삽입 */\nfun insert(n0: ListNode?, p: ListNode?) {\n val n1 = n0?.next\n p?.next = n1\n n0?.next = p\n}\n</code></pre> linked_list.rb<pre><code>### 연결 리스트의 노드 n0 뒤에 노드 _p 삽입 ###\n# Ruby에서 `p`는 내장 함수이고 `P`는 상수이므로, 대신 `_p`를 사용할 수 있다\ndef insert(n0, _p)\n n1 = n0.next\n _p.next = n1\n n0.next = _p\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 4 장   배열과 연결 리스트","4.2   연결 리스트"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#3","level":3,"title":"3.   노드 삭제","text":"<p>아래 그림(4-7)과 같이 연결 리스트의 노드 삭제도 매우 간단하다. 노드 하나의 참조(포인터)만 바꾸면 된다.</p> <p>삭제 후에도 노드 <code>P</code>는 여전히 <code>n1</code>을 가리키지만, 연결 리스트를 순회해서는 더 이상 <code>P</code>에 접근할 수 없다. 따라서 <code>P</code>는 이미 이 연결 리스트에 속하지 않는다.</p> <p></p> <p> 그림 4-7   연결 리스트의 노드 삭제 </p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py<pre><code>def remove(n0: ListNode):\n \"\"\"연결 리스트의 노드 n0 바로 뒤에 있는 노드 삭제\"\"\"\n if not n0.next:\n return\n # n0 -&gt; P -&gt; n1\n P = n0.next\n n1 = P.next\n n0.next = n1\n</code></pre> linked_list.cpp<pre><code>/* 연결 리스트의 노드 n0 바로 뒤에 있는 노드 삭제 */\nvoid remove(ListNode *n0) {\n if (n0-&gt;next == nullptr)\n return;\n // n0 -&gt; P -&gt; n1\n ListNode *P = n0-&gt;next;\n ListNode *n1 = P-&gt;next;\n n0-&gt;next = n1;\n // 메모리 해제\n delete P;\n}\n</code></pre> linked_list.java<pre><code>/* 연결 리스트의 노드 n0 바로 뒤에 있는 노드 삭제 */\nvoid remove(ListNode n0) {\n if (n0.next == null)\n return;\n // n0 -&gt; P -&gt; n1\n ListNode P = n0.next;\n ListNode n1 = P.next;\n n0.next = n1;\n}\n</code></pre> linked_list.cs<pre><code>/* 연결 리스트의 노드 n0 바로 뒤에 있는 노드 삭제 */\nvoid Remove(ListNode n0) {\n if (n0.next == null)\n return;\n // n0 -&gt; P -&gt; n1\n ListNode P = n0.next;\n ListNode? n1 = P.next;\n n0.next = n1;\n}\n</code></pre> linked_list.go<pre><code>/* 연결 리스트의 노드 n0 바로 뒤에 있는 노드 삭제 */\nfunc removeItem(n0 *ListNode) {\n if n0.Next == nil {\n return\n }\n // n0 -&gt; P -&gt; n1\n P := n0.Next\n n1 := P.Next\n n0.Next = n1\n}\n</code></pre> linked_list.swift<pre><code>/* 연결 리스트의 노드 n0 바로 뒤에 있는 노드 삭제 */\nfunc remove(n0: ListNode) {\n if n0.next == nil {\n return\n }\n // n0 -&gt; P -&gt; n1\n let P = n0.next\n let n1 = P?.next\n n0.next = n1\n}\n</code></pre> linked_list.js<pre><code>/* 연결 리스트의 노드 n0 바로 뒤에 있는 노드 삭제 */\nfunction remove(n0) {\n if (!n0.next) return;\n // n0 -&gt; P -&gt; n1\n const P = n0.next;\n const n1 = P.next;\n n0.next = n1;\n}\n</code></pre> linked_list.ts<pre><code>/* 연결 리스트의 노드 n0 바로 뒤에 있는 노드 삭제 */\nfunction remove(n0: ListNode): void {\n if (!n0.next) {\n return;\n }\n // n0 -&gt; P -&gt; n1\n const P = n0.next;\n const n1 = P.next;\n n0.next = n1;\n}\n</code></pre> linked_list.dart<pre><code>/* 연결 리스트의 노드 n0 바로 뒤에 있는 노드 삭제 */\nvoid remove(ListNode n0) {\n if (n0.next == null) return;\n // n0 -&gt; P -&gt; n1\n ListNode P = n0.next!;\n ListNode? n1 = P.next;\n n0.next = n1;\n}\n</code></pre> linked_list.rs<pre><code>/* 연결 리스트의 노드 n0 바로 뒤에 있는 노드 삭제 */\n#[allow(non_snake_case)]\npub fn remove&lt;T&gt;(n0: &amp;Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt;) {\n // n0 -&gt; P -&gt; n1\n let P = n0.borrow_mut().next.take();\n if let Some(node) = P {\n let n1 = node.borrow_mut().next.take();\n n0.borrow_mut().next = n1;\n }\n}\n</code></pre> linked_list.c<pre><code>/* 연결 리스트의 노드 n0 바로 뒤에 있는 노드 삭제 */\n// 주의: stdio.h가 remove라는 이름을 사용한다\nvoid removeItem(ListNode *n0) {\n if (!n0-&gt;next)\n return;\n // n0 -&gt; P -&gt; n1\n ListNode *P = n0-&gt;next;\n ListNode *n1 = P-&gt;next;\n n0-&gt;next = n1;\n // 메모리 해제\n free(P);\n}\n</code></pre> linked_list.kt<pre><code>/* 연결 리스트의 노드 n0 바로 뒤에 있는 노드 삭제 */\nfun remove(n0: ListNode?) {\n if (n0?.next == null)\n return\n // n0 -&gt; P -&gt; n1\n val p = n0.next\n val n1 = p?.next\n n0.next = n1\n}\n</code></pre> linked_list.rb<pre><code>### 연결 리스트의 노드 n0 바로 뒤에 있는 노드 삭제 ###\ndef remove(n0)\n return if n0.next.nil?\n\n # n0 -&gt; remove_node -&gt; n1\n remove_node = n0.next\n n1 = remove_node.next\n n0.next = n1\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 4 장   배열과 연결 리스트","4.2   연결 리스트"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#4","level":3,"title":"4.   노드 접근","text":"<p>연결 리스트의 노드 접근 효율은 낮다. 앞 절에서 설명했듯 배열은 어떤 원소든 \\(O(1)\\) 시간에 접근할 수 있다. 그러나 연결 리스트에서는 머리 노드부터 하나씩 뒤로 순회하여 목표 노드를 찾아야 한다. 즉, \\(i\\)번째 노드에 접근하려면 \\(i - 1\\)번 반복해야 하므로 시간 복잡도는 \\(O(n)\\)이다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py<pre><code>def access(head: ListNode, index: int) -&gt; ListNode | None:\n \"\"\"연결 리스트에서 인덱스가 index인 노드에 접근\"\"\"\n for _ in range(index):\n if not head:\n return None\n head = head.next\n return head\n</code></pre> linked_list.cpp<pre><code>/* 연결 리스트에서 인덱스가 index인 노드에 접근 */\nListNode *access(ListNode *head, int index) {\n for (int i = 0; i &lt; index; i++) {\n if (head == nullptr)\n return nullptr;\n head = head-&gt;next;\n }\n return head;\n}\n</code></pre> linked_list.java<pre><code>/* 연결 리스트에서 인덱스가 index인 노드에 접근 */\nListNode access(ListNode head, int index) {\n for (int i = 0; i &lt; index; i++) {\n if (head == null)\n return null;\n head = head.next;\n }\n return head;\n}\n</code></pre> linked_list.cs<pre><code>/* 연결 리스트에서 인덱스가 index인 노드에 접근 */\nListNode? Access(ListNode? head, int index) {\n for (int i = 0; i &lt; index; i++) {\n if (head == null)\n return null;\n head = head.next;\n }\n return head;\n}\n</code></pre> linked_list.go<pre><code>/* 연결 리스트에서 인덱스가 index인 노드에 접근 */\nfunc access(head *ListNode, index int) *ListNode {\n for i := 0; i &lt; index; i++ {\n if head == nil {\n return nil\n }\n head = head.Next\n }\n return head\n}\n</code></pre> linked_list.swift<pre><code>/* 연결 리스트에서 인덱스가 index인 노드에 접근 */\nfunc access(head: ListNode, index: Int) -&gt; ListNode? {\n var head: ListNode? = head\n for _ in 0 ..&lt; index {\n if head == nil {\n return nil\n }\n head = head?.next\n }\n return head\n}\n</code></pre> linked_list.js<pre><code>/* 연결 리스트에서 인덱스가 index인 노드에 접근 */\nfunction access(head, index) {\n for (let i = 0; i &lt; index; i++) {\n if (!head) {\n return null;\n }\n head = head.next;\n }\n return head;\n}\n</code></pre> linked_list.ts<pre><code>/* 연결 리스트에서 인덱스가 index인 노드에 접근 */\nfunction access(head: ListNode | null, index: number): ListNode | null {\n for (let i = 0; i &lt; index; i++) {\n if (!head) {\n return null;\n }\n head = head.next;\n }\n return head;\n}\n</code></pre> linked_list.dart<pre><code>/* 연결 리스트에서 인덱스가 index인 노드에 접근 */\nListNode? access(ListNode? head, int index) {\n for (var i = 0; i &lt; index; i++) {\n if (head == null) return null;\n head = head.next;\n }\n return head;\n}\n</code></pre> linked_list.rs<pre><code>/* 연결 리스트에서 인덱스가 index인 노드에 접근 */\npub fn access&lt;T&gt;(head: Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt;, index: i32) -&gt; Option&lt;Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt;&gt; {\n fn dfs&lt;T&gt;(\n head: Option&lt;&amp;Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt;&gt;,\n index: i32,\n ) -&gt; Option&lt;Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt;&gt; {\n if index &lt;= 0 {\n return head.cloned();\n }\n\n if let Some(node) = head {\n dfs(node.borrow().next.as_ref(), index - 1)\n } else {\n None\n }\n }\n\n dfs(Some(head).as_ref(), index)\n}\n</code></pre> linked_list.c<pre><code>/* 연결 리스트에서 인덱스가 index인 노드에 접근 */\nListNode *access(ListNode *head, int index) {\n for (int i = 0; i &lt; index; i++) {\n if (head == NULL)\n return NULL;\n head = head-&gt;next;\n }\n return head;\n}\n</code></pre> linked_list.kt<pre><code>/* 연결 리스트에서 인덱스가 index인 노드에 접근 */\nfun access(head: ListNode?, index: Int): ListNode? {\n var h = head\n for (i in 0..&lt;index) {\n if (h == null)\n return null\n h = h.next\n }\n return h\n}\n</code></pre> linked_list.rb<pre><code>### 연결 리스트에서 인덱스가 index인 노드에 접근 ###\ndef access(head, index)\n for i in 0...index\n return nil if head.nil?\n head = head.next\n end\n\n head\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 4 장   배열과 연결 리스트","4.2   연결 리스트"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#5","level":3,"title":"5.   노드 탐색","text":"<p>연결 리스트를 순회하여 값이 <code>target</code>인 노드를 찾고, 그 노드의 인덱스를 출력한다. 이 과정도 선형 탐색에 해당한다. 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py<pre><code>def find(head: ListNode, target: int) -&gt; int:\n \"\"\"연결 리스트에서 값이 target인 첫 번째 노드 탐색\"\"\"\n index = 0\n while head:\n if head.val == target:\n return index\n head = head.next\n index += 1\n return -1\n</code></pre> linked_list.cpp<pre><code>/* 연결 리스트에서 값이 target인 첫 번째 노드 탐색 */\nint find(ListNode *head, int target) {\n int index = 0;\n while (head != nullptr) {\n if (head-&gt;val == target)\n return index;\n head = head-&gt;next;\n index++;\n }\n return -1;\n}\n</code></pre> linked_list.java<pre><code>/* 연결 리스트에서 값이 target인 첫 번째 노드 탐색 */\nint find(ListNode head, int target) {\n int index = 0;\n while (head != null) {\n if (head.val == target)\n return index;\n head = head.next;\n index++;\n }\n return -1;\n}\n</code></pre> linked_list.cs<pre><code>/* 연결 리스트에서 값이 target인 첫 번째 노드 탐색 */\nint Find(ListNode? head, int target) {\n int index = 0;\n while (head != null) {\n if (head.val == target)\n return index;\n head = head.next;\n index++;\n }\n return -1;\n}\n</code></pre> linked_list.go<pre><code>/* 연결 리스트에서 값이 target인 첫 번째 노드 탐색 */\nfunc findNode(head *ListNode, target int) int {\n index := 0\n for head != nil {\n if head.Val == target {\n return index\n }\n head = head.Next\n index++\n }\n return -1\n}\n</code></pre> linked_list.swift<pre><code>/* 연결 리스트에서 값이 target인 첫 번째 노드 탐색 */\nfunc find(head: ListNode, target: Int) -&gt; Int {\n var head: ListNode? = head\n var index = 0\n while head != nil {\n if head?.val == target {\n return index\n }\n head = head?.next\n index += 1\n }\n return -1\n}\n</code></pre> linked_list.js<pre><code>/* 연결 리스트에서 값이 target인 첫 번째 노드 탐색 */\nfunction find(head, target) {\n let index = 0;\n while (head !== null) {\n if (head.val === target) {\n return index;\n }\n head = head.next;\n index += 1;\n }\n return -1;\n}\n</code></pre> linked_list.ts<pre><code>/* 연결 리스트에서 값이 target인 첫 번째 노드 탐색 */\nfunction find(head: ListNode | null, target: number): number {\n let index = 0;\n while (head !== null) {\n if (head.val === target) {\n return index;\n }\n head = head.next;\n index += 1;\n }\n return -1;\n}\n</code></pre> linked_list.dart<pre><code>/* 연결 리스트에서 값이 target인 첫 번째 노드 탐색 */\nint find(ListNode? head, int target) {\n int index = 0;\n while (head != null) {\n if (head.val == target) {\n return index;\n }\n head = head.next;\n index++;\n }\n return -1;\n}\n</code></pre> linked_list.rs<pre><code>/* 연결 리스트에서 값이 target인 첫 번째 노드 탐색 */\npub fn find&lt;T: PartialEq&gt;(head: Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt;, target: T) -&gt; i32 {\n fn find&lt;T: PartialEq&gt;(head: Option&lt;&amp;Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt;&gt;, target: T, idx: i32) -&gt; i32 {\n if let Some(node) = head {\n if node.borrow().val == target {\n return idx;\n }\n return find(node.borrow().next.as_ref(), target, idx + 1);\n } else {\n -1\n }\n }\n\n find(Some(head).as_ref(), target, 0)\n}\n</code></pre> linked_list.c<pre><code>/* 연결 리스트에서 값이 target인 첫 번째 노드 탐색 */\nint find(ListNode *head, int target) {\n int index = 0;\n while (head) {\n if (head-&gt;val == target)\n return index;\n head = head-&gt;next;\n index++;\n }\n return -1;\n}\n</code></pre> linked_list.kt<pre><code>/* 연결 리스트에서 값이 target인 첫 번째 노드 탐색 */\nfun find(head: ListNode?, target: Int): Int {\n var index = 0\n var h = head\n while (h != null) {\n if (h._val == target)\n return index\n h = h.next\n index++\n }\n return -1\n}\n</code></pre> linked_list.rb<pre><code>### 연결 리스트에서 값이 target인 첫 번째 노드 탐색 ###\ndef find(head, target)\n index = 0\n while head\n return index if head.val == target\n head = head.next\n index += 1\n end\n\n -1\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 4 장   배열과 연결 리스트","4.2   연결 리스트"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#422-vs","level":2,"title":"4.2.2   배열 vs. 연결 리스트","text":"<p>아래 표(4-1)는 배열과 연결 리스트의 특징과 연산 효율을 비교한다. 둘은 서로 반대되는 저장 전략을 사용하므로 성질과 연산 효율도 대조적이다.</p> <p> 표 4-1   배열과 연결 리스트의 효율 비교 </p> 배열 연결 리스트 저장 방식 연속된 메모리 공간 분산된 메모리 공간 용량 확장 길이를 바꿀 수 없음 유연하게 확장 가능 메모리 효율 원소의 메모리 사용량은 적지만 공간을 낭비할 수 있음 원소의 메모리 사용량이 많음 원소 접근 \\(O(1)\\) \\(O(n)\\) 원소 추가 \\(O(n)\\) \\(O(1)\\) 원소 삭제 \\(O(n)\\) \\(O(1)\\)","path":["제 4 장   배열과 연결 리스트","4.2   연결 리스트"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#423","level":2,"title":"4.2.3   대표적인 연결 리스트 종류","text":"<p>아래 그림(4-8)과 같이 대표적인 연결 리스트에는 세 종류가 있다.</p> <ul> <li>단일 연결 리스트: 앞에서 소개한 일반적인 연결 리스트다. 각 노드는 값과 다음 노드를 가리키는 참조라는 두 데이터를 포함한다. 첫 노드는 머리 노드, 마지막 노드는 꼬리 노드이며, 꼬리 노드는 <code>None</code>을 가리킨다.</li> <li>원형 연결 리스트: 단일 연결 리스트의 꼬리 노드가 머리 노드를 가리키도록 앞뒤를 이으면 원형 연결 리스트가 된다. 원형 연결 리스트에서는 어느 노드든 머리 노드로 볼 수 있다.</li> <li>이중 연결 리스트: 단일 연결 리스트와 달리 양방향 참조를 기록한다. 각 노드는 후속 노드(다음 노드)와 선행 노드(이전 노드)를 가리키는 참조(포인터)를 모두 포함한다. 양방향으로 순회할 수 있어 단일 연결 리스트보다 유연하지만, 그만큼 메모리 공간도 더 많이 차지한다.</li> </ul> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby <pre><code>class ListNode:\n \"\"\"이중 연결 리스트 노드 클래스\"\"\"\n def __init__(self, val: int):\n self.val: int = val # 노드 값\n self.next: ListNode | None = None # 후속 노드를 가리키는 참조\n self.prev: ListNode | None = None # 선행 노드를 가리키는 참조\n</code></pre> <pre><code>/* 이중 연결 리스트 노드 구조체 */\nstruct ListNode {\n int val; // 노드 값\n ListNode *next; // 후속 노드를 가리키는 포인터\n ListNode *prev; // 선행 노드를 가리키는 포인터\n ListNode(int x) : val(x), next(nullptr), prev(nullptr) {} // 생성자\n};\n</code></pre> <pre><code>/* 이중 연결 리스트 노드 클래스 */\nclass ListNode {\n int val; // 노드 값\n ListNode next; // 후속 노드를 가리키는 참조\n ListNode prev; // 선행 노드를 가리키는 참조\n ListNode(int x) { val = x; } // 생성자\n}\n</code></pre> <pre><code>/* 이중 연결 리스트 노드 클래스 */\nclass ListNode(int x) { // 생성자\n int val = x; // 노드 값\n ListNode next; // 후속 노드를 가리키는 참조\n ListNode prev; // 선행 노드를 가리키는 참조\n}\n</code></pre> <pre><code>/* 이중 연결 리스트 노드 구조체 */\ntype DoublyListNode struct {\n Val int // 노드 값\n Next *DoublyListNode // 후속 노드를 가리키는 포인터\n Prev *DoublyListNode // 선행 노드를 가리키는 포인터\n}\n\n// NewDoublyListNode 초기화\nfunc NewDoublyListNode(val int) *DoublyListNode {\n return &amp;DoublyListNode{\n Val: val,\n Next: nil,\n Prev: nil,\n }\n}\n</code></pre> <pre><code>/* 이중 연결 리스트 노드 클래스 */\nclass ListNode {\n var val: Int // 노드 값\n var next: ListNode? // 후속 노드를 가리키는 참조\n var prev: ListNode? // 선행 노드를 가리키는 참조\n\n init(x: Int) { // 생성자\n val = x\n }\n}\n</code></pre> <pre><code>/* 이중 연결 리스트 노드 클래스 */\nclass ListNode {\n constructor(val, next, prev) {\n this.val = val === undefined ? 0 : val; // 노드 값\n this.next = next === undefined ? null : next; // 후속 노드를 가리키는 참조\n this.prev = prev === undefined ? null : prev; // 선행 노드를 가리키는 참조\n }\n}\n</code></pre> <pre><code>/* 이중 연결 리스트 노드 클래스 */\nclass ListNode {\n val: number;\n next: ListNode | null;\n prev: ListNode | null;\n constructor(val?: number, next?: ListNode | null, prev?: ListNode | null) {\n this.val = val === undefined ? 0 : val; // 노드 값\n this.next = next === undefined ? null : next; // 후속 노드를 가리키는 참조\n this.prev = prev === undefined ? null : prev; // 선행 노드를 가리키는 참조\n }\n}\n</code></pre> <pre><code>/* 이중 연결 리스트 노드 클래스 */\nclass ListNode {\n int val; // 노드 값\n ListNode? next; // 후속 노드를 가리키는 참조\n ListNode? prev; // 선행 노드를 가리키는 참조\n ListNode(this.val, [this.next, this.prev]); // 생성자\n}\n</code></pre> <pre><code>use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* 이중 연결 리스트 노드 자료형 */\n#[derive(Debug)]\nstruct ListNode {\n val: i32, // 노드 값\n next: Option&lt;Rc&lt;RefCell&lt;ListNode&gt;&gt;&gt;, // 후속 노드를 가리키는 포인터\n prev: Option&lt;Rc&lt;RefCell&lt;ListNode&gt;&gt;&gt;, // 선행 노드를 가리키는 포인터\n}\n\n/* 생성자 */\nimpl ListNode {\n fn new(val: i32) -&gt; Self {\n ListNode {\n val,\n next: None,\n prev: None,\n }\n }\n}\n</code></pre> <pre><code>/* 이중 연결 리스트 노드 구조체 */\ntypedef struct ListNode {\n int val; // 노드 값\n struct ListNode *next; // 후속 노드를 가리키는 포인터\n struct ListNode *prev; // 선행 노드를 가리키는 포인터\n} ListNode;\n\n/* 생성자 */\nListNode *newListNode(int val) {\n ListNode *node;\n node = (ListNode *) malloc(sizeof(ListNode));\n node-&gt;val = val;\n node-&gt;next = NULL;\n node-&gt;prev = NULL;\n return node;\n}\n</code></pre> <pre><code>/* 이중 연결 리스트 노드 클래스 */\n// 생성자\nclass ListNode(x: Int) {\n val _val: Int = x // 노드 값\n var next: ListNode? = null // 후속 노드를 가리키는 참조\n var prev: ListNode? = null // 선행 노드를 가리키는 참조\n}\n</code></pre> <pre><code># 이중 연결 리스트 노드 클래스\nclass ListNode\n attr_accessor :val # 노드 값\n attr_accessor :next # 후속 노드를 가리키는 참조\n attr_accessor :prev # 선행 노드를 가리키는 참조\n\n def initialize(val=0, next_node=nil, prev_node=nil)\n @val = val\n @next = next_node\n @prev = prev_node\n end\nend\n</code></pre> <p></p> <p> 그림 4-8   대표적인 연결 리스트 종류 </p>","path":["제 4 장   배열과 연결 리스트","4.2   연결 리스트"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#424","level":2,"title":"4.2.4   연결 리스트의 대표적인 활용","text":"<p>단일 연결 리스트는 보통 스택, 큐, 해시 테이블, 그래프 등의 자료 구조를 구현하는 데 사용된다.</p> <ul> <li>스택과 큐: 삽입과 삭제를 모두 연결 리스트의 같은 끝에서 수행하면 후입선출 특성이 나타나 스택에 해당한다. 한쪽 끝에서 삽입하고 반대쪽 끝에서 삭제하면 선입선출 특성이 나타나 큐에 해당한다.</li> <li>해시 테이블: 체이닝은 해시 충돌을 해결하는 대표적인 방법 중 하나로, 충돌하는 모든 원소를 하나의 연결 리스트에 넣는다.</li> <li>그래프: 인접 리스트는 그래프를 표현하는 흔한 방법이다. 각 정점에 연결 리스트 하나를 연결하고, 그 리스트의 각 원소로 해당 정점과 연결된 다른 정점을 나타낸다.</li> </ul> <p>이중 연결 리스트는 이전 원소와 다음 원소를 빠르게 찾아야 하는 상황에 자주 사용된다.</p> <ul> <li>고급 자료 구조: 레드-블랙 트리나 B 트리에서는 노드의 부모 노드에 접근해야 한다. 이중 연결 리스트처럼 노드에 부모를 가리키는 참조를 저장하여 구현할 수 있다.</li> <li>브라우저 방문 기록: 사용자가 웹 브라우저의 앞으로 또는 뒤로 버튼을 누르면 이전이나 다음에 방문했던 페이지를 알아야 한다. 이중 연결 리스트를 사용하면 이러한 연산이 간단해진다.</li> <li>LRU 알고리즘: 캐시 교체 알고리즘인 LRU에서는 가장 오랫동안 사용하지 않은 데이터를 빠르게 찾고 노드도 빠르게 추가하고 삭제해야 한다. 이때 이중 연결 리스트가 매우 적합하다.</li> </ul> <p>원형 연결 리스트는 운영체제의 자원 스케줄링처럼 주기적인 연산이 필요한 상황에 자주 사용된다.</p> <ul> <li>라운드 로빈 스케줄링 알고리즘: 운영체제의 대표적인 CPU 스케줄링 알고리즘으로, 여러 프로세스를 순환한다. 각 프로세스에 타임 슬라이스를 할당하고, 시간이 다 되면 CPU가 다음 프로세스로 전환한다. 이러한 순환은 원형 연결 리스트로 구현할 수 있다.</li> <li>데이터 버퍼: 일부 데이터 버퍼도 원형 연결 리스트로 구현할 수 있다. 예를 들어 오디오나 동영상 플레이어에서 데이터 스트림을 여러 버퍼 블록으로 나누어 원형 연결 리스트에 넣으면 끊김 없이 재생할 수 있다.</li> </ul>","path":["제 4 장   배열과 연결 리스트","4.2   연결 리스트"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/","level":1,"title":"4.3   리스트","text":"<p>리스트(list)는 순서가 있는 원소의 모음을 나타내는 추상적인 자료 구조 개념이다. 원소 접근, 수정, 추가, 삭제, 순회 등의 연산을 지원하며, 사용자가 용량 제한을 신경 쓰지 않아도 된다. 리스트는 연결 리스트나 배열로 구현할 수 있다.</p> <ul> <li>연결 리스트는 원소의 삽입, 삭제, 탐색, 갱신을 지원하고 유연하게 용량을 늘릴 수 있으므로 자연스럽게 리스트로 볼 수 있다.</li> <li>배열도 원소의 삽입, 삭제, 탐색, 갱신을 지원하지만 길이를 바꿀 수 없으므로 길이가 제한된 리스트로 볼 수 있다.</li> </ul> <p>배열로 리스트를 구현하면 길이를 바꿀 수 없다는 특성 때문에 실용성이 낮아진다. 보통 저장할 데이터 양을 미리 알 수 없어 적절한 길이를 정하기 어렵기 때문이다. 길이가 너무 짧으면 필요를 충족하지 못하고, 너무 길면 메모리 공간을 낭비한다.</p> <p>이 문제는 동적 배열(dynamic array)로 리스트를 구현하여 해결할 수 있다. 동적 배열은 배열의 장점을 이어받으면서도 프로그램 실행 중에 용량을 동적으로 늘릴 수 있다.</p> <p>실제로 많은 프로그래밍 언어의 표준 라이브러리가 제공하는 리스트는 동적 배열로 구현된다. Python의 <code>list</code>, Java의 <code>ArrayList</code>, C++의 <code>vector</code>, C#의 <code>List</code> 등이 그 예다. 이후 설명에서는 ‘리스트’와 ‘동적 배열’을 같은 개념으로 다룬다.</p>","path":["제 4 장   배열과 연결 리스트","4.3   리스트"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#431","level":2,"title":"4.3.1   리스트의 기본 연산","text":"","path":["제 4 장   배열과 연결 리스트","4.3   리스트"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#1","level":3,"title":"1.   리스트 초기화","text":"<p>보통 ‘초깃값 없음’과 ‘초깃값 지정’의 두 가지 초기화 방식을 사용한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py<pre><code># 리스트 초기화\n# 초깃값 없음\nnums1: list[int] = []\n# 초깃값 지정\nnums: list[int] = [1, 3, 2, 5, 4]\n</code></pre> list.cpp<pre><code>/* 리스트 초기화 */\n// C++의 vector가 이 글에서 설명하는 nums에 해당한다\n// 초깃값 없음\nvector&lt;int&gt; nums1;\n// 초깃값 지정\nvector&lt;int&gt; nums = { 1, 3, 2, 5, 4 };\n</code></pre> list.java<pre><code>/* 리스트 초기화 */\n// 초깃값 없음\nList&lt;Integer&gt; nums1 = new ArrayList&lt;&gt;();\n// 초깃값 지정(배열은 int[]의 래퍼형인 Integer[]여야 함)\nInteger[] numbers = new Integer[] { 1, 3, 2, 5, 4 };\nList&lt;Integer&gt; nums = new ArrayList&lt;&gt;(Arrays.asList(numbers));\n</code></pre> list.cs<pre><code>/* 리스트 초기화 */\n// 초깃값 없음\nList&lt;int&gt; nums1 = [];\n// 초깃값 지정\nint[] numbers = [1, 3, 2, 5, 4];\nList&lt;int&gt; nums = [.. numbers];\n</code></pre> list_test.go<pre><code>/* 리스트 초기화 */\n// 초깃값 없음\nnums1 := []int{}\n// 초깃값 지정\nnums := []int{1, 3, 2, 5, 4}\n</code></pre> list.swift<pre><code>/* 리스트 초기화 */\n// 초깃값 없음\nlet nums1: [Int] = []\n// 초깃값 지정\nvar nums = [1, 3, 2, 5, 4]\n</code></pre> list.js<pre><code>/* 리스트 초기화 */\n// 초깃값 없음\nconst nums1 = [];\n// 초깃값 지정\nconst nums = [1, 3, 2, 5, 4];\n</code></pre> list.ts<pre><code>/* 리스트 초기화 */\n// 초깃값 없음\nconst nums1: number[] = [];\n// 초깃값 지정\nconst nums: number[] = [1, 3, 2, 5, 4];\n</code></pre> list.dart<pre><code>/* 리스트 초기화 */\n// 초깃값 없음\nList&lt;int&gt; nums1 = [];\n// 초깃값 지정\nList&lt;int&gt; nums = [1, 3, 2, 5, 4];\n</code></pre> list.rs<pre><code>/* 리스트 초기화 */\n// 초깃값 없음\nlet nums1: Vec&lt;i32&gt; = Vec::new();\n// 초깃값 지정\nlet nums: Vec&lt;i32&gt; = vec![1, 3, 2, 5, 4];\n</code></pre> list.c<pre><code>// C는 내장 동적 배열을 제공하지 않는다\n</code></pre> list.kt<pre><code>/* 리스트 초기화 */\n// 초깃값 없음\nvar nums1 = listOf&lt;Int&gt;()\n// 초깃값 지정\nvar numbers = arrayOf(1, 3, 2, 5, 4)\nvar nums = numbers.toMutableList()\n</code></pre> list.rb<pre><code># 리스트 초기화\n# 초깃값 없음\nnums1 = []\n# 초깃값 지정\nnums = [1, 3, 2, 5, 4]\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 4 장   배열과 연결 리스트","4.3   리스트"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#2","level":3,"title":"2.   원소 접근","text":"<p>리스트는 본질적으로 배열이므로 원소에 접근하고 갱신하는 데 \\(O(1)\\) 시간이 걸려 매우 효율적이다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py<pre><code># 원소 접근\nnum: int = nums[1] # 인덱스 1의 원소에 접근\n\n# 원소 갱신\nnums[1] = 0 # 인덱스 1의 원소를 0으로 갱신\n</code></pre> list.cpp<pre><code>/* 원소 접근 */\nint num = nums[1]; // 인덱스 1의 원소에 접근\n\n/* 원소 갱신 */\nnums[1] = 0; // 인덱스 1의 원소를 0으로 갱신\n</code></pre> list.java<pre><code>/* 원소 접근 */\nint num = nums.get(1); // 인덱스 1의 원소에 접근\n\n/* 원소 갱신 */\nnums.set(1, 0); // 인덱스 1의 원소를 0으로 갱신\n</code></pre> list.cs<pre><code>/* 원소 접근 */\nint num = nums[1]; // 인덱스 1의 원소에 접근\n\n/* 원소 갱신 */\nnums[1] = 0; // 인덱스 1의 원소를 0으로 갱신\n</code></pre> list_test.go<pre><code>/* 원소 접근 */\nnum := nums[1] // 인덱스 1의 원소에 접근\n\n/* 원소 갱신 */\nnums[1] = 0 // 인덱스 1의 원소를 0으로 갱신\n</code></pre> list.swift<pre><code>/* 원소 접근 */\nlet num = nums[1] // 인덱스 1의 원소에 접근\n\n/* 원소 갱신 */\nnums[1] = 0 // 인덱스 1의 원소를 0으로 갱신\n</code></pre> list.js<pre><code>/* 원소 접근 */\nconst num = nums[1]; // 인덱스 1의 원소에 접근\n\n/* 원소 갱신 */\nnums[1] = 0; // 인덱스 1의 원소를 0으로 갱신\n</code></pre> list.ts<pre><code>/* 원소 접근 */\nconst num: number = nums[1]; // 인덱스 1의 원소에 접근\n\n/* 원소 갱신 */\nnums[1] = 0; // 인덱스 1의 원소를 0으로 갱신\n</code></pre> list.dart<pre><code>/* 원소 접근 */\nint num = nums[1]; // 인덱스 1의 원소에 접근\n\n/* 원소 갱신 */\nnums[1] = 0; // 인덱스 1의 원소를 0으로 갱신\n</code></pre> list.rs<pre><code>/* 원소 접근 */\nlet num: i32 = nums[1]; // 인덱스 1의 원소에 접근\n/* 원소 갱신 */\nnums[1] = 0; // 인덱스 1의 원소를 0으로 갱신\n</code></pre> list.c<pre><code>// C는 내장 동적 배열을 제공하지 않는다\n</code></pre> list.kt<pre><code>/* 원소 접근 */\nval num = nums[1] // 인덱스 1의 원소에 접근\n/* 원소 갱신 */\nnums[1] = 0 // 인덱스 1의 원소를 0으로 갱신\n</code></pre> list.rb<pre><code># 원소 접근\nnum = nums[1] # 인덱스 1의 원소에 접근\n# 원소 갱신\nnums[1] = 0 # 인덱스 1의 원소를 0으로 갱신\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 4 장   배열과 연결 리스트","4.3   리스트"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#3","level":3,"title":"3.   원소 삽입과 삭제","text":"<p>배열과 달리 리스트는 원소를 자유롭게 추가하고 삭제할 수 있다. 끝에 원소를 추가하는 시간 복잡도는 \\(O(1)\\)이지만, 삽입과 삭제의 효율은 배열과 같아 시간 복잡도가 \\(O(n)\\)이다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py<pre><code># 리스트 비우기\nnums.clear()\n\n# 끝에 원소 추가\nnums.append(1)\nnums.append(3)\nnums.append(2)\nnums.append(5)\nnums.append(4)\n\n# 중간에 원소 삽입\nnums.insert(3, 6) # 인덱스 3에 숫자 6 삽입\n\n# 원소 삭제\nnums.pop(3) # 인덱스 3의 원소 삭제\n</code></pre> list.cpp<pre><code>/* 리스트 비우기 */\nnums.clear();\n\n/* 끝에 원소 추가 */\nnums.push_back(1);\nnums.push_back(3);\nnums.push_back(2);\nnums.push_back(5);\nnums.push_back(4);\n\n/* 중간에 원소 삽입 */\nnums.insert(nums.begin() + 3, 6); // 인덱스 3에 숫자 6 삽입\n\n/* 원소 삭제 */\nnums.erase(nums.begin() + 3); // 인덱스 3의 원소 삭제\n</code></pre> list.java<pre><code>/* 리스트 비우기 */\nnums.clear();\n\n/* 끝에 원소 추가 */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* 중간에 원소 삽입 */\nnums.add(3, 6); // 인덱스 3에 숫자 6 삽입\n\n/* 원소 삭제 */\nnums.remove(3); // 인덱스 3의 원소 삭제\n</code></pre> list.cs<pre><code>/* 리스트 비우기 */\nnums.Clear();\n\n/* 끝에 원소 추가 */\nnums.Add(1);\nnums.Add(3);\nnums.Add(2);\nnums.Add(5);\nnums.Add(4);\n\n/* 중간에 원소 삽입 */\nnums.Insert(3, 6); // 인덱스 3에 숫자 6 삽입\n\n/* 원소 삭제 */\nnums.RemoveAt(3); // 인덱스 3의 원소 삭제\n</code></pre> list_test.go<pre><code>/* 리스트 비우기 */\nnums = nil\n\n/* 끝에 원소 추가 */\nnums = append(nums, 1)\nnums = append(nums, 3)\nnums = append(nums, 2)\nnums = append(nums, 5)\nnums = append(nums, 4)\n\n/* 중간에 원소 삽입 */\nnums = append(nums[:3], append([]int{6}, nums[3:]...)...) // 인덱스 3에 숫자 6 삽입\n\n/* 원소 삭제 */\nnums = append(nums[:3], nums[4:]...) // 인덱스 3의 원소 삭제\n</code></pre> list.swift<pre><code>/* 리스트 비우기 */\nnums.removeAll()\n\n/* 끝에 원소 추가 */\nnums.append(1)\nnums.append(3)\nnums.append(2)\nnums.append(5)\nnums.append(4)\n\n/* 중간에 원소 삽입 */\nnums.insert(6, at: 3) // 인덱스 3에 숫자 6 삽입\n\n/* 원소 삭제 */\nnums.remove(at: 3) // 인덱스 3의 원소 삭제\n</code></pre> list.js<pre><code>/* 리스트 비우기 */\nnums.length = 0;\n\n/* 끝에 원소 추가 */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* 중간에 원소 삽입 */\nnums.splice(3, 0, 6); // 인덱스 3에 숫자 6 삽입\n\n/* 원소 삭제 */\nnums.splice(3, 1); // 인덱스 3의 원소 삭제\n</code></pre> list.ts<pre><code>/* 리스트 비우기 */\nnums.length = 0;\n\n/* 끝에 원소 추가 */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* 중간에 원소 삽입 */\nnums.splice(3, 0, 6); // 인덱스 3에 숫자 6 삽입\n\n/* 원소 삭제 */\nnums.splice(3, 1); // 인덱스 3의 원소 삭제\n</code></pre> list.dart<pre><code>/* 리스트 비우기 */\nnums.clear();\n\n/* 끝에 원소 추가 */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* 중간에 원소 삽입 */\nnums.insert(3, 6); // 인덱스 3에 숫자 6 삽입\n\n/* 원소 삭제 */\nnums.removeAt(3); // 인덱스 3의 원소 삭제\n</code></pre> list.rs<pre><code>/* 리스트 비우기 */\nnums.clear();\n\n/* 끝에 원소 추가 */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* 중간에 원소 삽입 */\nnums.insert(3, 6); // 인덱스 3에 숫자 6 삽입\n\n/* 원소 삭제 */\nnums.remove(3); // 인덱스 3의 원소 삭제\n</code></pre> list.c<pre><code>// C는 내장 동적 배열을 제공하지 않는다\n</code></pre> list.kt<pre><code>/* 리스트 비우기 */\nnums.clear();\n\n/* 끝에 원소 추가 */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* 중간에 원소 삽입 */\nnums.add(3, 6); // 인덱스 3에 숫자 6 삽입\n\n/* 원소 삭제 */\nnums.remove(3); // 인덱스 3의 원소 삭제\n</code></pre> list.rb<pre><code># 리스트 비우기\nnums.clear\n\n# 끝에 원소 추가\nnums &lt;&lt; 1\nnums &lt;&lt; 3\nnums &lt;&lt; 2\nnums &lt;&lt; 5\nnums &lt;&lt; 4\n\n# 중간에 원소 삽입\nnums.insert(3, 6) # 인덱스 3에 숫자 6 삽입\n\n# 원소 삭제\nnums.delete_at(3) # 인덱스 3의 원소 삭제\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 4 장   배열과 연결 리스트","4.3   리스트"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#4","level":3,"title":"4.   리스트 순회","text":"<p>배열처럼 리스트도 인덱스로 순회하거나 각 원소를 직접 순회할 수 있다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py<pre><code># 인덱스로 리스트 순회\ncount = 0\nfor i in range(len(nums)):\n count += nums[i]\n\n# 리스트 원소를 직접 순회\nfor num in nums:\n count += num\n</code></pre> list.cpp<pre><code>/* 인덱스로 리스트 순회 */\nint count = 0;\nfor (int i = 0; i &lt; nums.size(); i++) {\n count += nums[i];\n}\n\n/* 리스트 원소를 직접 순회 */\ncount = 0;\nfor (int num : nums) {\n count += num;\n}\n</code></pre> list.java<pre><code>/* 인덱스로 리스트 순회 */\nint count = 0;\nfor (int i = 0; i &lt; nums.size(); i++) {\n count += nums.get(i);\n}\n\n/* 리스트 원소를 직접 순회 */\nfor (int num : nums) {\n count += num;\n}\n</code></pre> list.cs<pre><code>/* 인덱스로 리스트 순회 */\nint count = 0;\nfor (int i = 0; i &lt; nums.Count; i++) {\n count += nums[i];\n}\n\n/* 리스트 원소를 직접 순회 */\ncount = 0;\nforeach (int num in nums) {\n count += num;\n}\n</code></pre> list_test.go<pre><code>/* 인덱스로 리스트 순회 */\ncount := 0\nfor i := 0; i &lt; len(nums); i++ {\n count += nums[i]\n}\n\n/* 리스트 원소를 직접 순회 */\ncount = 0\nfor _, num := range nums {\n count += num\n}\n</code></pre> list.swift<pre><code>/* 인덱스로 리스트 순회 */\nvar count = 0\nfor i in nums.indices {\n count += nums[i]\n}\n\n/* 리스트 원소를 직접 순회 */\ncount = 0\nfor num in nums {\n count += num\n}\n</code></pre> list.js<pre><code>/* 인덱스로 리스트 순회 */\nlet count = 0;\nfor (let i = 0; i &lt; nums.length; i++) {\n count += nums[i];\n}\n\n/* 리스트 원소를 직접 순회 */\ncount = 0;\nfor (const num of nums) {\n count += num;\n}\n</code></pre> list.ts<pre><code>/* 인덱스로 리스트 순회 */\nlet count = 0;\nfor (let i = 0; i &lt; nums.length; i++) {\n count += nums[i];\n}\n\n/* 리스트 원소를 직접 순회 */\ncount = 0;\nfor (const num of nums) {\n count += num;\n}\n</code></pre> list.dart<pre><code>/* 인덱스로 리스트 순회 */\nint count = 0;\nfor (var i = 0; i &lt; nums.length; i++) {\n count += nums[i];\n}\n\n/* 리스트 원소를 직접 순회 */\ncount = 0;\nfor (var num in nums) {\n count += num;\n}\n</code></pre> list.rs<pre><code>// 인덱스로 리스트 순회\nlet mut _count = 0;\nfor i in 0..nums.len() {\n _count += nums[i];\n}\n\n// 리스트 원소를 직접 순회\n_count = 0;\nfor num in &amp;nums {\n _count += num;\n}\n</code></pre> list.c<pre><code>// C는 내장 동적 배열을 제공하지 않는다\n</code></pre> list.kt<pre><code>/* 인덱스로 리스트 순회 */\nvar count = 0\nfor (i in nums.indices) {\n count += nums[i]\n}\n\n/* 리스트 원소를 직접 순회 */\nfor (num in nums) {\n count += num\n}\n</code></pre> list.rb<pre><code># 인덱스로 리스트 순회\ncount = 0\nfor i in 0...nums.length\n count += nums[i]\nend\n\n# 리스트 원소를 직접 순회\ncount = 0\nfor num in nums\n count += num\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 4 장   배열과 연결 리스트","4.3   리스트"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#5","level":3,"title":"5.   리스트 연결","text":"<p>새 리스트 <code>nums1</code>이 주어지면 기존 리스트의 끝에 이어 붙일 수 있다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py<pre><code># 두 리스트 연결\nnums1: list[int] = [6, 8, 7, 10, 9]\nnums += nums1 # 리스트 nums1을 nums 뒤에 연결\n</code></pre> list.cpp<pre><code>/* 두 리스트 연결 */\nvector&lt;int&gt; nums1 = { 6, 8, 7, 10, 9 };\n// 리스트 nums1을 nums 뒤에 연결\nnums.insert(nums.end(), nums1.begin(), nums1.end());\n</code></pre> list.java<pre><code>/* 두 리스트 연결 */\nList&lt;Integer&gt; nums1 = new ArrayList&lt;&gt;(Arrays.asList(new Integer[] { 6, 8, 7, 10, 9 }));\nnums.addAll(nums1); // 리스트 nums1을 nums 뒤에 연결\n</code></pre> list.cs<pre><code>/* 두 리스트 연결 */\nList&lt;int&gt; nums1 = [6, 8, 7, 10, 9];\nnums.AddRange(nums1); // 리스트 nums1을 nums 뒤에 연결\n</code></pre> list_test.go<pre><code>/* 두 리스트 연결 */\nnums1 := []int{6, 8, 7, 10, 9}\nnums = append(nums, nums1...) // 리스트 nums1을 nums 뒤에 연결\n</code></pre> list.swift<pre><code>/* 두 리스트 연결 */\nlet nums1 = [6, 8, 7, 10, 9]\nnums.append(contentsOf: nums1) // 리스트 nums1을 nums 뒤에 연결\n</code></pre> list.js<pre><code>/* 두 리스트 연결 */\nconst nums1 = [6, 8, 7, 10, 9];\nnums.push(...nums1); // 리스트 nums1을 nums 뒤에 연결\n</code></pre> list.ts<pre><code>/* 두 리스트 연결 */\nconst nums1: number[] = [6, 8, 7, 10, 9];\nnums.push(...nums1); // 리스트 nums1을 nums 뒤에 연결\n</code></pre> list.dart<pre><code>/* 두 리스트 연결 */\nList&lt;int&gt; nums1 = [6, 8, 7, 10, 9];\nnums.addAll(nums1); // 리스트 nums1을 nums 뒤에 연결\n</code></pre> list.rs<pre><code>/* 두 리스트 연결 */\nlet nums1: Vec&lt;i32&gt; = vec![6, 8, 7, 10, 9];\nnums.extend(nums1);\n</code></pre> list.c<pre><code>// C는 내장 동적 배열을 제공하지 않는다\n</code></pre> list.kt<pre><code>/* 두 리스트 연결 */\nval nums1 = intArrayOf(6, 8, 7, 10, 9).toMutableList()\nnums.addAll(nums1) // 리스트 nums1을 nums 뒤에 연결\n</code></pre> list.rb<pre><code># 두 리스트 연결\nnums1 = [6, 8, 7, 10, 9]\nnums += nums1\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 4 장   배열과 연결 리스트","4.3   리스트"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#6","level":3,"title":"6.   리스트 정렬","text":"<p>리스트를 정렬하면 배열 관련 알고리즘 문제에 자주 등장하는 ‘이진 탐색’과 ‘투 포인터’ 알고리즘을 사용할 수 있다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py<pre><code># 리스트 정렬\nnums.sort() # 정렬 후 리스트 원소가 오름차순으로 배치됨\n</code></pre> list.cpp<pre><code>/* 리스트 정렬 */\nsort(nums.begin(), nums.end()); // 정렬 후 리스트 원소가 오름차순으로 배치됨\n</code></pre> list.java<pre><code>/* 리스트 정렬 */\nCollections.sort(nums); // 정렬 후 리스트 원소가 오름차순으로 배치됨\n</code></pre> list.cs<pre><code>/* 리스트 정렬 */\nnums.Sort(); // 정렬 후 리스트 원소가 오름차순으로 배치됨\n</code></pre> list_test.go<pre><code>/* 리스트 정렬 */\nsort.Ints(nums) // 정렬 후 리스트 원소가 오름차순으로 배치됨\n</code></pre> list.swift<pre><code>/* 리스트 정렬 */\nnums.sort() // 정렬 후 리스트 원소가 오름차순으로 배치됨\n</code></pre> list.js<pre><code>/* 리스트 정렬 */\nnums.sort((a, b) =&gt; a - b); // 정렬 후 리스트 원소가 오름차순으로 배치됨\n</code></pre> list.ts<pre><code>/* 리스트 정렬 */\nnums.sort((a, b) =&gt; a - b); // 정렬 후 리스트 원소가 오름차순으로 배치됨\n</code></pre> list.dart<pre><code>/* 리스트 정렬 */\nnums.sort(); // 정렬 후 리스트 원소가 오름차순으로 배치됨\n</code></pre> list.rs<pre><code>/* 리스트 정렬 */\nnums.sort(); // 정렬 후 리스트 원소가 오름차순으로 배치됨\n</code></pre> list.c<pre><code>// C는 내장 동적 배열을 제공하지 않는다\n</code></pre> list.kt<pre><code>/* 리스트 정렬 */\nnums.sort() // 정렬 후 리스트 원소가 오름차순으로 배치됨\n</code></pre> list.rb<pre><code># 리스트 정렬\nnums = nums.sort { |a, b| a &lt;=&gt; b } # 정렬 후 리스트 원소가 오름차순으로 배치됨\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 4 장   배열과 연결 리스트","4.3   리스트"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#432","level":2,"title":"4.3.2   리스트 구현","text":"<p>Java, C++, Python 등 많은 프로그래밍 언어는 리스트를 내장하고 있다. 이들의 구현은 비교적 복잡하며 초기 용량, 확장 배수 등의 매개변수도 정교하게 설정되어 있다. 관심 있는 독자는 소스 코드를 살펴보며 학습할 수 있다.</p> <p>리스트의 작동 원리를 더 깊이 이해하기 위해 간단한 리스트를 구현해 보자. 핵심 설계는 다음 세 가지다.</p> <ul> <li>초기 용량: 배열의 초기 용량을 적절히 선택한다. 이 예에서는 10을 초기 용량으로 정한다.</li> <li>개수 기록: 현재 원소 수를 기록하는 변수 <code>size</code>를 선언하고, 삽입과 삭제에 따라 즉시 갱신한다. 이 변수를 통해 리스트의 끝을 찾고 용량 확장이 필요한지 판단할 수 있다.</li> <li>용량 확장 방식: 원소를 삽입할 때 리스트가 가득 찼으면 용량을 늘려야 한다. 확장 배수에 따라 더 큰 배열을 만든 뒤 기존 원소를 모두 차례로 옮긴다. 이 예에서는 매번 배열 용량을 이전의 2배로 늘린다.</li> </ul> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_list.py<pre><code>class MyList:\n \"\"\"리스트 클래스\"\"\"\n\n def __init__(self):\n \"\"\"생성자\"\"\"\n self._capacity: int = 10 # 리스트 용량\n self._arr: list[int] = [0] * self._capacity # 배열(리스트 원소 저장)\n self._size: int = 0 # 리스트 길이(현재 원소 개수)\n self._extend_ratio: int = 2 # 리스트의 용량을 확장할 때마다 적용하는 배수\n\n def size(self) -&gt; int:\n \"\"\"리스트 길이(현재 원소 개수) 반환\"\"\"\n return self._size\n\n def capacity(self) -&gt; int:\n \"\"\"리스트 용량 반환\"\"\"\n return self._capacity\n\n def get(self, index: int) -&gt; int:\n \"\"\"원소 접근\"\"\"\n # 인덱스가 범위를 벗어나면 예외를 발생시킨다. 아래에서도 동일하다\n if index &lt; 0 or index &gt;= self._size:\n raise IndexError(\"인덱스가 범위를 벗어났다\")\n return self._arr[index]\n\n def set(self, num: int, index: int):\n \"\"\"원소 갱신\"\"\"\n if index &lt; 0 or index &gt;= self._size:\n raise IndexError(\"인덱스가 범위를 벗어났다\")\n self._arr[index] = num\n\n def add(self, num: int):\n \"\"\"맨 뒤에 원소 추가\"\"\"\n # 원소 개수가 용량을 초과하면 용량 확장을 수행한다\n if self.size() == self.capacity():\n self.extend_capacity()\n self._arr[self._size] = num\n self._size += 1\n\n def insert(self, num: int, index: int):\n \"\"\"중간에 원소 삽입\"\"\"\n if index &lt; 0 or index &gt;= self._size:\n raise IndexError(\"인덱스가 범위를 벗어났다\")\n # 원소 개수가 용량을 초과하면 용량 확장을 수행한다\n if self._size == self.capacity():\n self.extend_capacity()\n # 인덱스 index와 그 뒤의 원소를 모두 한 칸씩 뒤로 이동한다\n for j in range(self._size - 1, index - 1, -1):\n self._arr[j + 1] = self._arr[j]\n self._arr[index] = num\n # 원소 개수를 갱신한다\n self._size += 1\n\n def remove(self, index: int) -&gt; int:\n \"\"\"원소 삭제\"\"\"\n if index &lt; 0 or index &gt;= self._size:\n raise IndexError(\"인덱스가 범위를 벗어났다\")\n num = self._arr[index]\n # 인덱스 index 뒤의 원소를 모두 한 칸씩 앞으로 이동한다\n for j in range(index, self._size - 1):\n self._arr[j] = self._arr[j + 1]\n # 원소 개수를 갱신한다\n self._size -= 1\n # 삭제한 원소를 반환한다\n return num\n\n def extend_capacity(self):\n \"\"\"리스트 용량 확장\"\"\"\n # 원래 배열 길이의 _extend_ratio 배인 새 배열을 만들고 원래 배열을 새 배열에 복사한다\n self._arr = self._arr + [0] * self.capacity() * (self._extend_ratio - 1)\n # 리스트 용량을 갱신한다\n self._capacity = len(self._arr)\n\n def to_array(self) -&gt; list[int]:\n \"\"\"유효한 길이만큼의 리스트 반환\"\"\"\n return self._arr[: self._size]\n</code></pre> my_list.cpp<pre><code>/* 리스트 클래스 */\nclass MyList {\n private:\n int *arr; // 배열(리스트 원소 저장)\n int arrCapacity = 10; // 리스트 용량\n int arrSize = 0; // 리스트 길이(현재 원소 개수)\n int extendRatio = 2; // 리스트의 용량을 확장할 때마다 적용하는 배수\n\n public:\n /* 생성자 */\n MyList() {\n arr = new int[arrCapacity];\n }\n\n /* 소멸자 */\n ~MyList() {\n delete[] arr;\n }\n\n /* 리스트 길이(현재 원소 개수) 반환*/\n int size() {\n return arrSize;\n }\n\n /* 리스트 용량 반환 */\n int capacity() {\n return arrCapacity;\n }\n\n /* 원소 접근 */\n int get(int index) {\n // 인덱스가 범위를 벗어나면 예외를 발생시킨다. 아래에서도 동일하다\n if (index &lt; 0 || index &gt;= size())\n throw out_of_range(\"인덱스가 범위를 벗어났다\");\n return arr[index];\n }\n\n /* 원소 갱신 */\n void set(int index, int num) {\n if (index &lt; 0 || index &gt;= size())\n throw out_of_range(\"인덱스가 범위를 벗어났다\");\n arr[index] = num;\n }\n\n /* 맨 뒤에 원소 추가 */\n void add(int num) {\n // 원소 개수가 용량을 초과하면 용량 확장을 수행한다\n if (size() == capacity())\n extendCapacity();\n arr[size()] = num;\n // 원소 개수를 갱신한다\n arrSize++;\n }\n\n /* 중간에 원소 삽입 */\n void insert(int index, int num) {\n if (index &lt; 0 || index &gt;= size())\n throw out_of_range(\"인덱스가 범위를 벗어났다\");\n // 원소 개수가 용량을 초과하면 용량 확장을 수행한다\n if (size() == capacity())\n extendCapacity();\n // 인덱스 index와 그 뒤의 원소를 모두 한 칸씩 뒤로 이동한다\n for (int j = size() - 1; j &gt;= index; j--) {\n arr[j + 1] = arr[j];\n }\n arr[index] = num;\n // 원소 개수를 갱신한다\n arrSize++;\n }\n\n /* 원소 삭제 */\n int remove(int index) {\n if (index &lt; 0 || index &gt;= size())\n throw out_of_range(\"인덱스가 범위를 벗어났다\");\n int num = arr[index];\n // 인덱스 index 뒤의 원소를 모두 한 칸씩 앞으로 이동한다\n for (int j = index; j &lt; size() - 1; j++) {\n arr[j] = arr[j + 1];\n }\n // 원소 개수를 갱신한다\n arrSize--;\n // 삭제한 원소를 반환한다\n return num;\n }\n\n /* 리스트 용량 확장 */\n void extendCapacity() {\n // 길이가 기존 배열의 extendRatio배인 새 배열 생성\n int newCapacity = capacity() * extendRatio;\n int *tmp = arr;\n arr = new int[newCapacity];\n // 원래 배열의 모든 원소를 새 배열에 복사한다\n for (int i = 0; i &lt; size(); i++) {\n arr[i] = tmp[i];\n }\n // 메모리 해제\n delete[] tmp;\n arrCapacity = newCapacity;\n }\n\n /* 출력을 위해 리스트를 Vector로 변환 */\n vector&lt;int&gt; toVector() {\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n vector&lt;int&gt; vec(size());\n for (int i = 0; i &lt; size(); i++) {\n vec[i] = arr[i];\n }\n return vec;\n }\n};\n</code></pre> my_list.java<pre><code>/* 리스트 클래스 */\nclass MyList {\n private int[] arr; // 배열(리스트 원소 저장)\n private int capacity = 10; // 리스트 용량\n private int size = 0; // 리스트 길이(현재 원소 개수)\n private int extendRatio = 2; // 리스트의 용량을 확장할 때마다 적용하는 배수\n\n /* 생성자 */\n public MyList() {\n arr = new int[capacity];\n }\n\n /* 리스트 길이(현재 원소 개수) 반환 */\n public int size() {\n return size;\n }\n\n /* 리스트 용량 반환 */\n public int capacity() {\n return capacity;\n }\n\n /* 원소 접근 */\n public int get(int index) {\n // 인덱스가 범위를 벗어나면 예외를 발생시킨다. 아래에서도 동일하다\n if (index &lt; 0 || index &gt;= size)\n throw new IndexOutOfBoundsException(\"인덱스가 범위를 벗어났다\");\n return arr[index];\n }\n\n /* 원소 갱신 */\n public void set(int index, int num) {\n if (index &lt; 0 || index &gt;= size)\n throw new IndexOutOfBoundsException(\"인덱스가 범위를 벗어났다\");\n arr[index] = num;\n }\n\n /* 맨 뒤에 원소 추가 */\n public void add(int num) {\n // 원소 개수가 용량을 초과하면 용량 확장을 수행한다\n if (size == capacity())\n extendCapacity();\n arr[size] = num;\n // 원소 개수를 갱신한다\n size++;\n }\n\n /* 중간에 원소 삽입 */\n public void insert(int index, int num) {\n if (index &lt; 0 || index &gt;= size)\n throw new IndexOutOfBoundsException(\"인덱스가 범위를 벗어났다\");\n // 원소 개수가 용량을 초과하면 용량 확장을 수행한다\n if (size == capacity())\n extendCapacity();\n // 인덱스 index와 그 뒤의 원소를 모두 한 칸씩 뒤로 이동한다\n for (int j = size - 1; j &gt;= index; j--) {\n arr[j + 1] = arr[j];\n }\n arr[index] = num;\n // 원소 개수를 갱신한다\n size++;\n }\n\n /* 원소 삭제 */\n public int remove(int index) {\n if (index &lt; 0 || index &gt;= size)\n throw new IndexOutOfBoundsException(\"인덱스가 범위를 벗어났다\");\n int num = arr[index];\n // 인덱스 index 뒤의 원소를 모두 한 칸씩 앞으로 이동한다\n for (int j = index; j &lt; size - 1; j++) {\n arr[j] = arr[j + 1];\n }\n // 원소 개수를 갱신한다\n size--;\n // 삭제한 원소를 반환한다\n return num;\n }\n\n /* 리스트 용량 확장 */\n public void extendCapacity() {\n // 길이가 원래 배열의 extendRatio배인 새 배열을 생성하고, 원래 배열을 새 배열에 복사한다\n arr = Arrays.copyOf(arr, capacity() * extendRatio);\n // 리스트 용량을 갱신한다\n capacity = arr.length;\n }\n\n /* 리스트를 배열로 변환 */\n public int[] toArray() {\n int size = size();\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n int[] arr = new int[size];\n for (int i = 0; i &lt; size; i++) {\n arr[i] = get(i);\n }\n return arr;\n }\n}\n</code></pre> my_list.cs<pre><code>/* 리스트 클래스 */\nclass MyList {\n private int[] arr; // 배열(리스트 원소 저장)\n private int arrCapacity = 10; // 리스트 용량\n private int arrSize = 0; // 리스트 길이(현재 원소 개수)\n private readonly int extendRatio = 2; // 리스트의 용량을 확장할 때마다 적용하는 배수\n\n /* 생성자 */\n public MyList() {\n arr = new int[arrCapacity];\n }\n\n /* 리스트 길이(현재 원소 개수) 반환*/\n public int Size() {\n return arrSize;\n }\n\n /* 리스트 용량 반환 */\n public int Capacity() {\n return arrCapacity;\n }\n\n /* 원소 접근 */\n public int Get(int index) {\n // 인덱스가 범위를 벗어나면 예외를 발생시킨다. 아래에서도 동일하다\n if (index &lt; 0 || index &gt;= arrSize)\n throw new IndexOutOfRangeException(\"인덱스가 범위를 벗어났다\");\n return arr[index];\n }\n\n /* 원소 갱신 */\n public void Set(int index, int num) {\n if (index &lt; 0 || index &gt;= arrSize)\n throw new IndexOutOfRangeException(\"인덱스가 범위를 벗어났다\");\n arr[index] = num;\n }\n\n /* 맨 뒤에 원소 추가 */\n public void Add(int num) {\n // 원소 개수가 용량을 초과하면 용량 확장을 수행한다\n if (arrSize == arrCapacity)\n ExtendCapacity();\n arr[arrSize] = num;\n // 원소 개수를 갱신한다\n arrSize++;\n }\n\n /* 중간에 원소 삽입 */\n public void Insert(int index, int num) {\n if (index &lt; 0 || index &gt;= arrSize)\n throw new IndexOutOfRangeException(\"인덱스가 범위를 벗어났다\");\n // 원소 개수가 용량을 초과하면 용량 확장을 수행한다\n if (arrSize == arrCapacity)\n ExtendCapacity();\n // 인덱스 index와 그 뒤의 원소를 모두 한 칸씩 뒤로 이동한다\n for (int j = arrSize - 1; j &gt;= index; j--) {\n arr[j + 1] = arr[j];\n }\n arr[index] = num;\n // 원소 개수를 갱신한다\n arrSize++;\n }\n\n /* 원소 삭제 */\n public int Remove(int index) {\n if (index &lt; 0 || index &gt;= arrSize)\n throw new IndexOutOfRangeException(\"인덱스가 범위를 벗어났다\");\n int num = arr[index];\n // 인덱스 index 뒤의 원소를 모두 한 칸씩 앞으로 이동한다\n for (int j = index; j &lt; arrSize - 1; j++) {\n arr[j] = arr[j + 1];\n }\n // 원소 개수를 갱신한다\n arrSize--;\n // 삭제한 원소를 반환한다\n return num;\n }\n\n /* 리스트 용량 확장 */\n public void ExtendCapacity() {\n // 길이가 arrCapacity * extendRatio인 배열을 만들고 기존 배열을 새 배열에 복사\n Array.Resize(ref arr, arrCapacity * extendRatio);\n // 리스트 용량을 갱신한다\n arrCapacity = arr.Length;\n }\n\n /* 리스트를 배열로 변환 */\n public int[] ToArray() {\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n int[] arr = new int[arrSize];\n for (int i = 0; i &lt; arrSize; i++) {\n arr[i] = Get(i);\n }\n return arr;\n }\n}\n</code></pre> my_list.go<pre><code>/* 리스트 클래스 */\ntype myList struct {\n arrCapacity int\n arr []int\n arrSize int\n extendRatio int\n}\n\n/* 생성자 */\nfunc newMyList() *myList {\n return &amp;myList{\n arrCapacity: 10, // 리스트 용량\n arr: make([]int, 10), // 배열(리스트 원소 저장)\n arrSize: 0, // 리스트 길이(현재 원소 개수)\n extendRatio: 2, // 리스트의 용량을 확장할 때마다 적용하는 배수\n }\n}\n\n/* 리스트 길이(현재 원소 개수) 반환 */\nfunc (l *myList) size() int {\n return l.arrSize\n}\n\n/* 리스트 용량 반환 */\nfunc (l *myList) capacity() int {\n return l.arrCapacity\n}\n\n/* 원소 접근 */\nfunc (l *myList) get(index int) int {\n // 인덱스가 범위를 벗어나면 예외를 발생시킨다. 아래에서도 동일하다\n if index &lt; 0 || index &gt;= l.arrSize {\n panic(\"인덱스가 범위를 벗어났다\")\n }\n return l.arr[index]\n}\n\n/* 원소 갱신 */\nfunc (l *myList) set(num, index int) {\n if index &lt; 0 || index &gt;= l.arrSize {\n panic(\"인덱스가 범위를 벗어났다\")\n }\n l.arr[index] = num\n}\n\n/* 맨 뒤에 원소 추가 */\nfunc (l *myList) add(num int) {\n // 원소 개수가 용량을 초과하면 용량 확장을 수행한다\n if l.arrSize == l.arrCapacity {\n l.extendCapacity()\n }\n l.arr[l.arrSize] = num\n // 원소 개수를 갱신한다\n l.arrSize++\n}\n\n/* 중간에 원소 삽입 */\nfunc (l *myList) insert(num, index int) {\n if index &lt; 0 || index &gt;= l.arrSize {\n panic(\"인덱스가 범위를 벗어났다\")\n }\n // 원소 개수가 용량을 초과하면 용량 확장을 수행한다\n if l.arrSize == l.arrCapacity {\n l.extendCapacity()\n }\n // 인덱스 index와 그 뒤의 원소를 모두 한 칸씩 뒤로 이동한다\n for j := l.arrSize - 1; j &gt;= index; j-- {\n l.arr[j+1] = l.arr[j]\n }\n l.arr[index] = num\n // 원소 개수를 갱신한다\n l.arrSize++\n}\n\n/* 원소 삭제 */\nfunc (l *myList) remove(index int) int {\n if index &lt; 0 || index &gt;= l.arrSize {\n panic(\"인덱스가 범위를 벗어났다\")\n }\n num := l.arr[index]\n // 인덱스 index 뒤의 원소를 모두 한 칸씩 앞으로 이동한다\n for j := index; j &lt; l.arrSize-1; j++ {\n l.arr[j] = l.arr[j+1]\n }\n // 원소 개수를 갱신한다\n l.arrSize--\n // 삭제한 원소를 반환한다\n return num\n}\n\n/* 리스트 용량 확장 */\nfunc (l *myList) extendCapacity() {\n // 길이가 원래 배열의 extendRatio배인 새 배열을 생성하고, 원래 배열을 새 배열에 복사한다\n l.arr = append(l.arr, make([]int, l.arrCapacity*(l.extendRatio-1))...)\n // 리스트 용량을 갱신한다\n l.arrCapacity = len(l.arr)\n}\n\n/* 유효한 길이만큼의 리스트 반환 */\nfunc (l *myList) toArray() []int {\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n return l.arr[:l.arrSize]\n}\n</code></pre> my_list.swift<pre><code>/* 리스트 클래스 */\nclass MyList {\n private var arr: [Int] // 배열(리스트 원소 저장)\n private var _capacity: Int // 리스트 용량\n private var _size: Int // 리스트 길이(현재 원소 개수)\n private let extendRatio: Int // 리스트의 용량을 확장할 때마다 적용하는 배수\n\n /* 생성자 */\n init() {\n _capacity = 10\n _size = 0\n extendRatio = 2\n arr = Array(repeating: 0, count: _capacity)\n }\n\n /* 리스트 길이(현재 원소 개수) 반환*/\n func size() -&gt; Int {\n _size\n }\n\n /* 리스트 용량 반환 */\n func capacity() -&gt; Int {\n _capacity\n }\n\n /* 원소 접근 */\n func get(index: Int) -&gt; Int {\n // 인덱스가 범위를 벗어나면 오류를 발생시킨다. 아래에서도 동일하다\n if index &lt; 0 || index &gt;= size() {\n fatalError(\"인덱스가 범위를 벗어났다\")\n }\n return arr[index]\n }\n\n /* 원소 갱신 */\n func set(index: Int, num: Int) {\n if index &lt; 0 || index &gt;= size() {\n fatalError(\"인덱스가 범위를 벗어났다\")\n }\n arr[index] = num\n }\n\n /* 맨 뒤에 원소 추가 */\n func add(num: Int) {\n // 원소 개수가 용량을 초과하면 용량 확장을 수행한다\n if size() == capacity() {\n extendCapacity()\n }\n arr[size()] = num\n // 원소 개수를 갱신한다\n _size += 1\n }\n\n /* 중간에 원소 삽입 */\n func insert(index: Int, num: Int) {\n if index &lt; 0 || index &gt;= size() {\n fatalError(\"인덱스가 범위를 벗어났다\")\n }\n // 원소 개수가 용량을 초과하면 용량 확장을 수행한다\n if size() == capacity() {\n extendCapacity()\n }\n // 인덱스 index와 그 뒤의 원소를 모두 한 칸씩 뒤로 이동한다\n for j in (index ..&lt; size()).reversed() {\n arr[j + 1] = arr[j]\n }\n arr[index] = num\n // 원소 개수를 갱신한다\n _size += 1\n }\n\n /* 원소 삭제 */\n @discardableResult\n func remove(index: Int) -&gt; Int {\n if index &lt; 0 || index &gt;= size() {\n fatalError(\"인덱스가 범위를 벗어났다\")\n }\n let num = arr[index]\n // 인덱스 index 뒤의 원소를 모두 한 칸씩 앞으로 이동한다\n for j in index ..&lt; (size() - 1) {\n arr[j] = arr[j + 1]\n }\n // 원소 개수를 갱신한다\n _size -= 1\n // 삭제한 원소를 반환한다\n return num\n }\n\n /* 리스트 용량 확장 */\n func extendCapacity() {\n // 길이가 원래 배열의 extendRatio배인 새 배열을 생성하고, 원래 배열을 새 배열에 복사한다\n arr = arr + Array(repeating: 0, count: capacity() * (extendRatio - 1))\n // 리스트 용량을 갱신한다\n _capacity = arr.count\n }\n\n /* 리스트를 배열로 변환 */\n func toArray() -&gt; [Int] {\n Array(arr.prefix(size()))\n }\n}\n</code></pre> my_list.js<pre><code>/* 리스트 클래스 */\nclass MyList {\n #arr = new Array(); // 배열(리스트 원소 저장)\n #capacity = 10; // 리스트 용량\n #size = 0; // 리스트 길이(현재 원소 개수)\n #extendRatio = 2; // 리스트의 용량을 확장할 때마다 적용하는 배수\n\n /* 생성자 */\n constructor() {\n this.#arr = new Array(this.#capacity);\n }\n\n /* 리스트 길이(현재 원소 개수) 반환*/\n size() {\n return this.#size;\n }\n\n /* 리스트 용량 반환 */\n capacity() {\n return this.#capacity;\n }\n\n /* 원소 접근 */\n get(index) {\n // 인덱스가 범위를 벗어나면 예외를 발생시킨다. 아래에서도 동일하다\n if (index &lt; 0 || index &gt;= this.#size) throw new Error('인덱스가 범위를 벗어났다');\n return this.#arr[index];\n }\n\n /* 원소 갱신 */\n set(index, num) {\n if (index &lt; 0 || index &gt;= this.#size) throw new Error('인덱스가 범위를 벗어났다');\n this.#arr[index] = num;\n }\n\n /* 맨 뒤에 원소 추가 */\n add(num) {\n // 길이가 용량과 같으면 용량을 확장해야 한다\n if (this.#size === this.#capacity) {\n this.extendCapacity();\n }\n // 새 원소를 리스트 맨 뒤에 추가한다\n this.#arr[this.#size] = num;\n this.#size++;\n }\n\n /* 중간에 원소 삽입 */\n insert(index, num) {\n if (index &lt; 0 || index &gt;= this.#size) throw new Error('인덱스가 범위를 벗어났다');\n // 원소 개수가 용량을 초과하면 용량 확장을 수행한다\n if (this.#size === this.#capacity) {\n this.extendCapacity();\n }\n // 인덱스 index와 그 뒤의 원소를 모두 한 칸씩 뒤로 이동한다\n for (let j = this.#size - 1; j &gt;= index; j--) {\n this.#arr[j + 1] = this.#arr[j];\n }\n // 원소 개수를 갱신한다\n this.#arr[index] = num;\n this.#size++;\n }\n\n /* 원소 삭제 */\n remove(index) {\n if (index &lt; 0 || index &gt;= this.#size) throw new Error('인덱스가 범위를 벗어났다');\n let num = this.#arr[index];\n // 인덱스 index 뒤의 원소를 모두 한 칸씩 앞으로 이동한다\n for (let j = index; j &lt; this.#size - 1; j++) {\n this.#arr[j] = this.#arr[j + 1];\n }\n // 원소 개수를 갱신한다\n this.#size--;\n // 삭제한 원소를 반환한다\n return num;\n }\n\n /* 리스트 용량 확장 */\n extendCapacity() {\n // 길이가 원래 배열의 extendRatio배인 새 배열을 생성하고, 원래 배열을 새 배열에 복사한다\n this.#arr = this.#arr.concat(\n new Array(this.capacity() * (this.#extendRatio - 1))\n );\n // 리스트 용량을 갱신한다\n this.#capacity = this.#arr.length;\n }\n\n /* 리스트를 배열로 변환 */\n toArray() {\n let size = this.size();\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n const arr = new Array(size);\n for (let i = 0; i &lt; size; i++) {\n arr[i] = this.get(i);\n }\n return arr;\n }\n}\n</code></pre> my_list.ts<pre><code>/* 리스트 클래스 */\nclass MyList {\n private arr: Array&lt;number&gt;; // 배열(리스트 원소 저장)\n private _capacity: number = 10; // 리스트 용량\n private _size: number = 0; // 리스트 길이(현재 원소 개수)\n private extendRatio: number = 2; // 리스트의 용량을 확장할 때마다 적용하는 배수\n\n /* 생성자 */\n constructor() {\n this.arr = new Array(this._capacity);\n }\n\n /* 리스트 길이(현재 원소 개수) 반환*/\n public size(): number {\n return this._size;\n }\n\n /* 리스트 용량 반환 */\n public capacity(): number {\n return this._capacity;\n }\n\n /* 원소 접근 */\n public get(index: number): number {\n // 인덱스가 범위를 벗어나면 예외를 발생시킨다. 아래에서도 동일하다\n if (index &lt; 0 || index &gt;= this._size) throw new Error('인덱스가 범위를 벗어났다');\n return this.arr[index];\n }\n\n /* 원소 갱신 */\n public set(index: number, num: number): void {\n if (index &lt; 0 || index &gt;= this._size) throw new Error('인덱스가 범위를 벗어났다');\n this.arr[index] = num;\n }\n\n /* 맨 뒤에 원소 추가 */\n public add(num: number): void {\n // 길이가 용량과 같으면 용량을 확장해야 한다\n if (this._size === this._capacity) this.extendCapacity();\n // 새 원소를 리스트 맨 뒤에 추가한다\n this.arr[this._size] = num;\n this._size++;\n }\n\n /* 중간에 원소 삽입 */\n public insert(index: number, num: number): void {\n if (index &lt; 0 || index &gt;= this._size) throw new Error('인덱스가 범위를 벗어났다');\n // 원소 개수가 용량을 초과하면 용량 확장을 수행한다\n if (this._size === this._capacity) {\n this.extendCapacity();\n }\n // 인덱스 index와 그 뒤의 원소를 모두 한 칸씩 뒤로 이동한다\n for (let j = this._size - 1; j &gt;= index; j--) {\n this.arr[j + 1] = this.arr[j];\n }\n // 원소 개수를 갱신한다\n this.arr[index] = num;\n this._size++;\n }\n\n /* 원소 삭제 */\n public remove(index: number): number {\n if (index &lt; 0 || index &gt;= this._size) throw new Error('인덱스가 범위를 벗어났다');\n let num = this.arr[index];\n // 인덱스 index 뒤의 원소를 모두 한 칸씩 앞으로 이동한다\n for (let j = index; j &lt; this._size - 1; j++) {\n this.arr[j] = this.arr[j + 1];\n }\n // 원소 개수를 갱신한다\n this._size--;\n // 삭제한 원소를 반환한다\n return num;\n }\n\n /* 리스트 용량 확장 */\n public extendCapacity(): void {\n // 길이가 size인 새 배열을 생성하고, 원래 배열을 새 배열에 복사한다\n this.arr = this.arr.concat(\n new Array(this.capacity() * (this.extendRatio - 1))\n );\n // 리스트 용량을 갱신한다\n this._capacity = this.arr.length;\n }\n\n /* 리스트를 배열로 변환 */\n public toArray(): number[] {\n let size = this.size();\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n const arr = new Array(size);\n for (let i = 0; i &lt; size; i++) {\n arr[i] = this.get(i);\n }\n return arr;\n }\n}\n</code></pre> my_list.dart<pre><code>/* 리스트 클래스 */\nclass MyList {\n late List&lt;int&gt; _arr; // 배열(리스트 원소 저장)\n int _capacity = 10; // 리스트 용량\n int _size = 0; // 리스트 길이(현재 원소 개수)\n int _extendRatio = 2; // 리스트의 용량을 확장할 때마다 적용하는 배수\n\n /* 생성자 */\n MyList() {\n _arr = List.filled(_capacity, 0);\n }\n\n /* 리스트 길이(현재 원소 개수) 반환*/\n int size() =&gt; _size;\n\n /* 리스트 용량 반환 */\n int capacity() =&gt; _capacity;\n\n /* 원소 접근 */\n int get(int index) {\n if (index &gt;= _size) throw RangeError('인덱스가 범위를 벗어났다');\n return _arr[index];\n }\n\n /* 원소 갱신 */\n void set(int index, int _num) {\n if (index &gt;= _size) throw RangeError('인덱스가 범위를 벗어났다');\n _arr[index] = _num;\n }\n\n /* 맨 뒤에 원소 추가 */\n void add(int _num) {\n // 원소 개수가 용량을 초과하면 용량 확장을 수행한다\n if (_size == _capacity) extendCapacity();\n _arr[_size] = _num;\n // 원소 개수를 갱신한다\n _size++;\n }\n\n /* 중간에 원소 삽입 */\n void insert(int index, int _num) {\n if (index &gt;= _size) throw RangeError('인덱스가 범위를 벗어났다');\n // 원소 개수가 용량을 초과하면 용량 확장을 수행한다\n if (_size == _capacity) extendCapacity();\n // 인덱스 index와 그 뒤의 원소를 모두 한 칸씩 뒤로 이동한다\n for (var j = _size - 1; j &gt;= index; j--) {\n _arr[j + 1] = _arr[j];\n }\n _arr[index] = _num;\n // 원소 개수를 갱신한다\n _size++;\n }\n\n /* 원소 삭제 */\n int remove(int index) {\n if (index &gt;= _size) throw RangeError('인덱스가 범위를 벗어났다');\n int _num = _arr[index];\n // 인덱스 index 뒤의 원소를 모두 한 칸씩 앞으로 이동한다\n for (var j = index; j &lt; _size - 1; j++) {\n _arr[j] = _arr[j + 1];\n }\n // 원소 개수를 갱신한다\n _size--;\n // 삭제한 원소를 반환한다\n return _num;\n }\n\n /* 리스트 용량 확장 */\n void extendCapacity() {\n // 길이가 원래 배열의 _extendRatio배인 새 배열을 생성한다\n final _newNums = List.filled(_capacity * _extendRatio, 0);\n // 원래 배열을 새 배열에 복사한다\n List.copyRange(_newNums, 0, _arr);\n // _arr의 참조를 갱신한다\n _arr = _newNums;\n // 리스트 용량을 갱신한다\n _capacity = _arr.length;\n }\n\n /* 리스트를 배열로 변환 */\n List&lt;int&gt; toArray() {\n List&lt;int&gt; arr = [];\n for (var i = 0; i &lt; _size; i++) {\n arr.add(get(i));\n }\n return arr;\n }\n}\n</code></pre> my_list.rs<pre><code>/* 리스트 클래스 */\n#[allow(dead_code)]\nstruct MyList {\n arr: Vec&lt;i32&gt;, // 배열(리스트 원소 저장)\n capacity: usize, // 리스트 용량\n size: usize, // 리스트 길이(현재 원소 개수)\n extend_ratio: usize, // 리스트의 용량을 확장할 때마다 적용하는 배수\n}\n\n#[allow(unused, unused_comparisons)]\nimpl MyList {\n /* 생성자 */\n pub fn new(capacity: usize) -&gt; Self {\n let mut vec = vec![0; capacity];\n Self {\n arr: vec,\n capacity,\n size: 0,\n extend_ratio: 2,\n }\n }\n\n /* 리스트 길이(현재 원소 개수) 반환*/\n pub fn size(&amp;self) -&gt; usize {\n return self.size;\n }\n\n /* 리스트 용량 반환 */\n pub fn capacity(&amp;self) -&gt; usize {\n return self.capacity;\n }\n\n /* 원소 접근 */\n pub fn get(&amp;self, index: usize) -&gt; i32 {\n // 인덱스가 범위를 벗어나면 예외를 발생시킨다. 아래에서도 동일하다\n if index &gt;= self.size {\n panic!(\"인덱스가 범위를 벗어났다\")\n };\n return self.arr[index];\n }\n\n /* 원소 갱신 */\n pub fn set(&amp;mut self, index: usize, num: i32) {\n if index &gt;= self.size {\n panic!(\"인덱스가 범위를 벗어났다\")\n };\n self.arr[index] = num;\n }\n\n /* 맨 뒤에 원소 추가 */\n pub fn add(&amp;mut self, num: i32) {\n // 원소 개수가 용량을 초과하면 용량 확장을 수행한다\n if self.size == self.capacity() {\n self.extend_capacity();\n }\n self.arr[self.size] = num;\n // 원소 개수를 갱신한다\n self.size += 1;\n }\n\n /* 중간에 원소 삽입 */\n pub fn insert(&amp;mut self, index: usize, num: i32) {\n if index &gt;= self.size() {\n panic!(\"인덱스가 범위를 벗어났다\")\n };\n // 원소 개수가 용량을 초과하면 용량 확장을 수행한다\n if self.size == self.capacity() {\n self.extend_capacity();\n }\n // 인덱스 index와 그 뒤의 원소를 모두 한 칸씩 뒤로 이동한다\n for j in (index..self.size).rev() {\n self.arr[j + 1] = self.arr[j];\n }\n self.arr[index] = num;\n // 원소 개수를 갱신한다\n self.size += 1;\n }\n\n /* 원소 삭제 */\n pub fn remove(&amp;mut self, index: usize) -&gt; i32 {\n if index &gt;= self.size() {\n panic!(\"인덱스가 범위를 벗어났다\")\n };\n let num = self.arr[index];\n // 인덱스 index 뒤의 원소를 모두 한 칸씩 앞으로 이동한다\n for j in index..self.size - 1 {\n self.arr[j] = self.arr[j + 1];\n }\n // 원소 개수를 갱신한다\n self.size -= 1;\n // 삭제한 원소를 반환한다\n return num;\n }\n\n /* 리스트 용량 확장 */\n pub fn extend_capacity(&amp;mut self) {\n // 길이가 원래 배열의 extend_ratio배인 새 배열을 생성하고, 원래 배열을 새 배열에 복사한다\n let new_capacity = self.capacity * self.extend_ratio;\n self.arr.resize(new_capacity, 0);\n // 리스트 용량을 갱신한다\n self.capacity = new_capacity;\n }\n\n /* 리스트를 배열로 변환 */\n pub fn to_array(&amp;self) -&gt; Vec&lt;i32&gt; {\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n let mut arr = Vec::new();\n for i in 0..self.size {\n arr.push(self.get(i));\n }\n arr\n }\n}\n</code></pre> my_list.c<pre><code>/* 리스트 클래스 */\ntypedef struct {\n int *arr; // 배열(리스트 원소 저장)\n int capacity; // 리스트 용량\n int size; // 리스트 크기\n int extendRatio; // 리스트 용량을 확장할 때마다 적용하는 배수\n} MyList;\n\n/* 생성자 */\nMyList *newMyList() {\n MyList *nums = malloc(sizeof(MyList));\n nums-&gt;capacity = 10;\n nums-&gt;arr = malloc(sizeof(int) * nums-&gt;capacity);\n nums-&gt;size = 0;\n nums-&gt;extendRatio = 2;\n return nums;\n}\n\n/* 소멸자 */\nvoid delMyList(MyList *nums) {\n free(nums-&gt;arr);\n free(nums);\n}\n\n/* 리스트 길이 구하기 */\nint size(MyList *nums) {\n return nums-&gt;size;\n}\n\n/* 리스트 용량 반환 */\nint capacity(MyList *nums) {\n return nums-&gt;capacity;\n}\n\n/* 원소 접근 */\nint get(MyList *nums, int index) {\n assert(index &gt;= 0 &amp;&amp; index &lt; nums-&gt;size);\n return nums-&gt;arr[index];\n}\n\n/* 원소 갱신 */\nvoid set(MyList *nums, int index, int num) {\n assert(index &gt;= 0 &amp;&amp; index &lt; nums-&gt;size);\n nums-&gt;arr[index] = num;\n}\n\n/* 맨 뒤에 원소 추가 */\nvoid add(MyList *nums, int num) {\n if (size(nums) == capacity(nums)) {\n extendCapacity(nums); // 용량 확장\n }\n nums-&gt;arr[size(nums)] = num;\n nums-&gt;size++;\n}\n\n/* 중간에 원소 삽입 */\nvoid insert(MyList *nums, int index, int num) {\n assert(index &gt;= 0 &amp;&amp; index &lt; size(nums));\n // 원소 개수가 용량을 초과하면 용량 확장을 수행한다\n if (size(nums) == capacity(nums)) {\n extendCapacity(nums); // 용량 확장\n }\n for (int i = size(nums); i &gt; index; --i) {\n nums-&gt;arr[i] = nums-&gt;arr[i - 1];\n }\n nums-&gt;arr[index] = num;\n nums-&gt;size++;\n}\n\n/* 원소 삭제 */\n// 주의: stdio.h가 remove라는 이름을 사용한다\nint removeItem(MyList *nums, int index) {\n assert(index &gt;= 0 &amp;&amp; index &lt; size(nums));\n int num = nums-&gt;arr[index];\n for (int i = index; i &lt; size(nums) - 1; i++) {\n nums-&gt;arr[i] = nums-&gt;arr[i + 1];\n }\n nums-&gt;size--;\n return num;\n}\n\n/* 리스트 용량 확장 */\nvoid extendCapacity(MyList *nums) {\n // 먼저 공간 할당\n int newCapacity = capacity(nums) * nums-&gt;extendRatio;\n int *extend = (int *)malloc(sizeof(int) * newCapacity);\n int *temp = nums-&gt;arr;\n\n // 기존 데이터를 새 공간에 복사\n for (int i = 0; i &lt; size(nums); i++)\n extend[i] = nums-&gt;arr[i];\n\n // 기존 데이터 공간 해제\n free(temp);\n\n // 새 데이터로 갱신\n nums-&gt;arr = extend;\n nums-&gt;capacity = newCapacity;\n}\n\n/* 출력을 위해 리스트를 Array로 변환 */\nint *toArray(MyList *nums) {\n return nums-&gt;arr;\n}\n</code></pre> my_list.kt<pre><code>/* 리스트 클래스 */\nclass MyList {\n private var arr: IntArray = intArrayOf() // 배열(리스트 원소 저장)\n private var capacity: Int = 10 // 리스트 용량\n private var size: Int = 0 // 리스트 길이(현재 원소 개수)\n private var extendRatio: Int = 2 // 리스트의 용량을 확장할 때마다 적용하는 배수\n\n /* 생성자 */\n init {\n arr = IntArray(capacity)\n }\n\n /* 리스트 길이(현재 원소 개수) 반환 */\n fun size(): Int {\n return size\n }\n\n /* 리스트 용량 반환 */\n fun capacity(): Int {\n return capacity\n }\n\n /* 원소 접근 */\n fun get(index: Int): Int {\n // 인덱스가 범위를 벗어나면 예외를 발생시킨다. 아래에서도 동일하다\n if (index &lt; 0 || index &gt;= size)\n throw IndexOutOfBoundsException(\"인덱스가 범위를 벗어났다\")\n return arr[index]\n }\n\n /* 원소 갱신 */\n fun set(index: Int, num: Int) {\n if (index &lt; 0 || index &gt;= size)\n throw IndexOutOfBoundsException(\"인덱스가 범위를 벗어났다\")\n arr[index] = num\n }\n\n /* 맨 뒤에 원소 추가 */\n fun add(num: Int) {\n // 원소 개수가 용량을 초과하면 용량 확장을 수행한다\n if (size == capacity())\n extendCapacity()\n arr[size] = num\n // 원소 개수를 갱신한다\n size++\n }\n\n /* 중간에 원소 삽입 */\n fun insert(index: Int, num: Int) {\n if (index &lt; 0 || index &gt;= size)\n throw IndexOutOfBoundsException(\"인덱스가 범위를 벗어났다\")\n // 원소 개수가 용량을 초과하면 용량 확장을 수행한다\n if (size == capacity())\n extendCapacity()\n // 인덱스 index와 그 뒤의 원소를 모두 한 칸씩 뒤로 이동한다\n for (j in size - 1 downTo index)\n arr[j + 1] = arr[j]\n arr[index] = num\n // 원소 개수를 갱신한다\n size++\n }\n\n /* 원소 삭제 */\n fun remove(index: Int): Int {\n if (index &lt; 0 || index &gt;= size)\n throw IndexOutOfBoundsException(\"인덱스가 범위를 벗어났다\")\n val num = arr[index]\n // 인덱스 index 뒤의 원소를 모두 한 칸씩 앞으로 이동한다\n for (j in index..&lt;size - 1)\n arr[j] = arr[j + 1]\n // 원소 개수를 갱신한다\n size--\n // 삭제한 원소를 반환한다\n return num\n }\n\n /* 리스트 용량 확장 */\n fun extendCapacity() {\n // 길이가 원래 배열의 extendRatio배인 새 배열을 생성하고, 원래 배열을 새 배열에 복사한다\n arr = arr.copyOf(capacity() * extendRatio)\n // 리스트 용량을 갱신한다\n capacity = arr.size\n }\n\n /* 리스트를 배열로 변환 */\n fun toArray(): IntArray {\n val size = size()\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n val arr = IntArray(size)\n for (i in 0..&lt;size) {\n arr[i] = get(i)\n }\n return arr\n }\n}\n</code></pre> my_list.rb<pre><code>### 리스트 클래스 ###\nclass MyList\n attr_reader :size # 리스트 길이(현재 원소 개수) 반환\n attr_reader :capacity # 리스트 용량 반환\n\n ### 생성자 ###\n def initialize\n @capacity = 10\n @size = 0\n @extend_ratio = 2\n @arr = Array.new(capacity)\n end\n\n ### 원소 접근 ###\n def get(index)\n # 인덱스가 범위를 벗어나면 예외를 발생시킨다. 아래에서도 동일하다\n raise IndexError, \"인덱스가 범위를 벗어났다\" if index &lt; 0 || index &gt;= size\n @arr[index]\n end\n\n ### 원소 접근 ###\n def set(index, num)\n raise IndexError, \"인덱스가 범위를 벗어났다\" if index &lt; 0 || index &gt;= size\n @arr[index] = num\n end\n\n ### 맨 뒤에 원소 추가 ###\n def add(num)\n # 원소 개수가 용량을 초과하면 용량 확장을 수행한다\n extend_capacity if size == capacity\n @arr[size] = num\n\n # 원소 개수를 갱신한다\n @size += 1\n end\n\n ### 중간에 원소 삽입 ###\n def insert(index, num)\n raise IndexError, \"인덱스가 범위를 벗어났다\" if index &lt; 0 || index &gt;= size\n\n # 원소 개수가 용량을 초과하면 용량 확장을 수행한다\n extend_capacity if size == capacity\n\n # 인덱스 index와 그 뒤의 원소를 모두 한 칸씩 뒤로 이동한다\n for j in (size - 1).downto(index)\n @arr[j + 1] = @arr[j]\n end\n @arr[index] = num\n\n # 원소 개수를 갱신한다\n @size += 1\n end\n\n ### 원소 삭제 ###\n def remove(index)\n raise IndexError, \"인덱스가 범위를 벗어났다\" if index &lt; 0 || index &gt;= size\n num = @arr[index]\n\n # 인덱스 index 뒤의 원소를 모두 한 칸씩 앞으로 이동한다\n for j in index...size\n @arr[j] = @arr[j + 1]\n end\n\n # 원소 개수를 갱신한다\n @size -= 1\n\n # 삭제한 원소를 반환한다\n num\n end\n\n ### 리스트 용량 확장 ###\n def extend_capacity\n # 길이가 원래 배열의 extend_ratio배인 새 배열을 생성하고, 원래 배열을 새 배열에 복사한다\n arr = @arr.dup + Array.new(capacity * (@extend_ratio - 1))\n # 리스트 용량을 갱신한다\n @capacity = arr.length\n end\n\n ### 리스트를 배열로 변환 ###\n def to_array\n sz = size\n # 유효한 길이 범위 안의 리스트 원소만 변환한다\n arr = Array.new(sz)\n for i in 0...sz\n arr[i] = get(i)\n end\n arr\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 4 장   배열과 연결 리스트","4.3   리스트"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/","level":1,"title":"4.4   메모리와 캐시 *","text":"<p>이 장의 앞선 두 절에서는 기본적이고 중요한 두 자료 구조인 배열과 연결 리스트를 살펴보았다. 이들은 각각 ‘연속 저장’과 ‘분산 저장’이라는 물리적 구조를 대표한다.</p> <p>실제로 물리적 구조는 프로그램의 메모리 및 캐시 사용 효율을 크게 좌우하며, 나아가 알고리즘 프로그램의 전체 성능에 영향을 준다.</p>","path":["제 4 장   배열과 연결 리스트","4.4   메모리와 캐시 *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#441","level":2,"title":"4.4.1   컴퓨터의 저장 장치","text":"<p>컴퓨터에는 하드 디스크(hard disk), 메모리(random-access memory, RAM), 캐시(cache memory)라는 세 종류의 저장 장치가 있다. 아래 표(4-2)는 컴퓨터 시스템에서 각 장치가 맡는 역할과 성능 특징을 보여 준다.</p> <p> 표 4-2   컴퓨터의 저장 장치 </p> 하드 디스크 메모리 캐시 용도 운영체제, 프로그램, 파일 등의 데이터를 장기간 저장 실행 중인 프로그램과 처리 중인 데이터를 임시 저장 자주 접근하는 데이터와 명령어를 저장하여 CPU의 메모리 접근 횟수를 줄임 휘발성 전원이 꺼져도 데이터가 사라지지 않음 전원이 꺼지면 데이터가 사라짐 전원이 꺼지면 데이터가 사라짐 용량 큼, TB 단위 작음, GB 단위 매우 작음, MB 단위 속도 느림, 수백~수천 MB/s 빠름, 수십 GB/s 매우 빠름, 수십~수백 GB/s 가격(중국 위안) 저렴함, GB당 수십 펀~수 위안 비쌈, GB당 수십~수백 위안 매우 비쌈, CPU 가격에 포함 <p>컴퓨터의 저장 시스템은 아래 그림(4-9)과 같은 피라미드 구조로 생각할 수 있다. 꼭대기에 가까울수록 속도는 빠르고, 용량은 작으며, 비용은 높다. 이러한 계층적 설계는 우연이 아니라 컴퓨터 과학자와 엔지니어들이 깊이 고민한 결과다.</p> <ul> <li>메모리로 하드 디스크를 대체하기는 어렵다. 우선 메모리의 데이터는 전원이 꺼지면 사라지므로 장기 저장에 적합하지 않다. 또한 메모리 가격은 하드 디스크의 수십 배여서 소비자 시장에 보편적으로 적용하기 어렵다.</li> <li>캐시에서 큰 용량과 빠른 속도를 동시에 얻기는 어렵다. L1, L2, L3 캐시의 용량이 커질수록 물리적 크기도 커지고 CPU 코어와의 거리도 멀어진다. 그 결과 데이터 전송 시간이 늘고 원소 접근 지연도 커진다. 현재 기술에서 다단계 캐시 구조는 용량, 속도, 비용 사이의 최적 절충점이다.</li> </ul> <p></p> <p> 그림 4-9   컴퓨터 저장 시스템 </p> <p>참고</p> <p>컴퓨터의 저장 계층 구조는 속도, 용량, 비용 사이의 정교한 균형을 보여 준다. 이러한 절충은 모든 산업 분야에 널리 존재하며, 서로 다른 장점과 제약 사이에서 최적의 균형을 찾아야 한다.</p> <p>전체적으로 하드 디스크는 많은 데이터를 장기간 저장하고, 메모리는 실행 중인 프로그램이 처리하는 데이터를 임시 저장하며, 캐시는 자주 접근하는 데이터와 명령어를 저장하여 실행 효율을 높인다. 세 장치는 서로 협력하여 컴퓨터 시스템이 효율적으로 작동하게 한다.</p> <p>아래 그림(4-10)과 같이 프로그램이 실행되면 하드 디스크의 데이터를 메모리로 읽어 와 CPU가 계산에 사용한다. 캐시는 CPU의 일부로 볼 수 있으며, 메모리에서 데이터를 지능적으로 불러와 CPU에 빠른 데이터 읽기를 제공한다. 이를 통해 실행 효율을 크게 높이고 더 느린 메모리에 대한 의존을 줄인다.</p> <p></p> <p> 그림 4-10   하드 디스크, 메모리, 캐시 사이의 데이터 흐름 </p>","path":["제 4 장   배열과 연결 리스트","4.4   메모리와 캐시 *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#442","level":2,"title":"4.4.2   자료 구조의 메모리 효율","text":"<p>메모리 공간 활용 측면에서 배열과 연결 리스트에는 각각 장점과 한계가 있다.</p> <p>한편으로 메모리는 한정되어 있으며 같은 메모리 영역을 여러 프로그램이 공유할 수 없으므로, 자료 구조가 공간을 최대한 효율적으로 사용하기를 바란다. 배열 원소는 빈틈없이 붙어 있어 연결 리스트 노드 사이의 참조(포인터)를 저장할 추가 공간이 필요 없으므로 공간 효율이 더 높다. 하지만 배열은 충분한 연속 메모리를 한꺼번에 할당해야 하므로 공간 낭비가 발생할 수 있으며, 용량 확장에도 시간과 공간이 추가로 든다. 반면 연결 리스트는 ‘노드’ 단위로 메모리를 동적으로 할당하고 회수하므로 더 유연하다.</p> <p>다른 한편으로 프로그램이 실행되면서 메모리 할당과 해제가 반복되면 빈 메모리의 단편화가 점점 심해져 메모리 활용 효율이 낮아진다. 배열은 연속 저장 방식을 사용하므로 상대적으로 단편화를 덜 일으킨다. 반면 연결 리스트의 원소는 분산되어 있으므로 삽입과 삭제가 잦으면 단편화가 더 쉽게 발생한다.</p>","path":["제 4 장   배열과 연결 리스트","4.4   메모리와 캐시 *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#443","level":2,"title":"4.4.3   자료 구조의 캐시 효율","text":"<p>캐시는 메모리보다 용량은 훨씬 작지만 속도는 훨씬 빨라 프로그램 실행 속도에 매우 중요한 역할을 한다. 용량이 제한되어 있어 자주 사용하는 데이터 일부만 저장할 수 있으므로, CPU가 접근하려는 데이터가 캐시에 없으면 캐시 미스(cache miss)가 발생한다. 이때 CPU는 더 느린 메모리에서 필요한 데이터를 불러와야 한다.</p> <p>따라서 ‘캐시 미스’가 적을수록 CPU의 데이터 읽기 및 쓰기 효율이 높아지고, 프로그램 성능도 좋아진다. CPU가 캐시에서 데이터를 성공적으로 가져오는 비율을 캐시 적중률(cache hit rate)이라고 하며, 캐시 효율을 측정하는 지표로 자주 사용한다.</p> <p>캐시는 효율을 최대한 높이기 위해 다음과 같은 데이터 로딩 방식을 사용한다.</p> <ul> <li>캐시 라인: 캐시는 데이터를 한 바이트씩 저장하고 불러오는 대신 캐시 라인 단위로 처리한다. 캐시 라인 단위 전송은 바이트 단위 전송보다 효율적이다.</li> <li>프리페치: 프로세서는 순차 접근이나 고정 간격 접근 같은 데이터 접근 패턴을 예측하고, 그 패턴에 따라 데이터를 미리 캐시에 불러와 적중률을 높인다.</li> <li>공간 지역성: 어떤 데이터에 접근하면 가까운 시일 안에 주변 데이터에도 접근할 가능성이 있다. 따라서 캐시는 데이터를 불러올 때 주변 데이터도 함께 불러와 적중률을 높인다.</li> <li>시간 지역성: 어떤 데이터에 접근하면 머지않아 다시 접근할 가능성이 높다. 캐시는 이 원리를 이용해 최근 접근한 데이터를 유지함으로써 적중률을 높인다.</li> </ul> <p>실제로 배열과 연결 리스트의 캐시 활용 효율은 서로 다르며, 주로 다음과 같은 차이가 있다.</p> <ul> <li>공간 사용량: 연결 리스트 원소는 배열 원소보다 더 많은 공간을 차지하므로 캐시에 담을 수 있는 유효 데이터의 양이 적다.</li> <li>캐시 라인: 연결 리스트의 데이터는 메모리 곳곳에 흩어져 있지만 캐시는 ‘라인 단위’로 불러오므로, 불필요한 데이터를 가져오는 비율이 높다.</li> <li>프리페치: 배열의 데이터 접근 패턴은 연결 리스트보다 ‘예측 가능성’이 높다. 즉, 시스템이 다음에 불러올 데이터를 더 쉽게 예상할 수 있다.</li> <li>공간 지역성: 배열은 한곳에 모인 메모리 공간에 저장되므로, 불러온 데이터 주변의 데이터에도 곧 접근할 가능성이 더 높다.</li> </ul> <p>대체로 배열은 캐시 적중률이 더 높아 연산 효율에서도 연결 리스트보다 유리한 경우가 많다. 이 때문에 알고리즘 문제를 풀 때는 배열 기반 자료 구조를 더 선호하는 경우가 많다.</p> <p>다만 캐시 효율이 높다고 해서 모든 상황에서 배열이 연결 리스트보다 좋은 것은 아니다. 실제로 어떤 자료 구조를 쓸지는 구체적인 요구에 따라 결정해야 한다. 예를 들어 배열과 연결 리스트 모두 ‘스택’을 구현할 수 있지만(다음 장에서 자세히 설명한다), 적합한 상황은 다르다.</p> <ul> <li>알고리즘 문제를 풀 때는 배열 기반 스택을 선호한다. 배열에 일정한 메모리 공간을 미리 할당하기만 하면, 높은 연산 효율과 임의 접근 기능을 얻을 수 있기 때문이다.</li> <li>데이터가 매우 많고 변화가 잦으며 스택의 예상 크기를 가늠하기 어렵다면 연결 리스트 기반 스택이 더 적합하다. 연결 리스트는 많은 데이터를 메모리 여러 곳에 분산 저장할 수 있으며, 배열 용량 확장에 따른 추가 비용도 피할 수 있다.</li> </ul>","path":["제 4 장   배열과 연결 리스트","4.4   메모리와 캐시 *"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/","level":1,"title":"4.5   요약","text":"","path":["제 4 장   배열과 연결 리스트","4.5   요약"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/#1","level":3,"title":"1.   핵심 정리","text":"<ul> <li>배열과 연결 리스트는 두 가지 기본 자료 구조로, 컴퓨터 메모리에 데이터를 저장하는 연속 공간 저장과 분산 공간 저장을 각각 대표한다. 둘의 특징은 서로 보완적이다.</li> <li>배열은 임의 접근을 지원하고 메모리 사용량이 적다. 그러나 원소 삽입과 삭제의 효율이 낮으며 초기화한 뒤 길이를 바꿀 수 없다.</li> <li>연결 리스트는 참조(포인터)를 바꾸어 노드를 효율적으로 삽입하고 삭제하며, 길이도 유연하게 조절할 수 있다. 하지만 노드 접근 효율이 낮고 메모리를 더 많이 사용한다. 대표적인 종류에는 단일 연결 리스트, 원형 연결 리스트, 이중 연결 리스트가 있다.</li> <li>리스트는 삽입, 삭제, 탐색, 갱신을 지원하는 순서 있는 원소의 모음으로, 보통 동적 배열로 구현한다. 배열의 장점을 유지하면서 길이를 유연하게 조절할 수 있다.</li> <li>리스트의 등장으로 배열의 실용성이 크게 높아졌지만, 메모리 공간 일부가 낭비될 수 있다.</li> <li>프로그램이 실행될 때 데이터는 주로 메모리에 저장된다. 배열은 메모리 공간 효율이 더 높고, 연결 리스트는 메모리를 더 유연하게 사용한다.</li> <li>캐시는 캐시 라인, 프리페치, 공간 지역성, 시간 지역성 등의 로딩 방식을 통해 CPU에 빠른 데이터 접근을 제공하여 프로그램 실행 효율을 크게 높인다.</li> <li>배열은 캐시 적중률이 더 높으므로 보통 연결 리스트보다 효율적이다. 자료 구조를 선택할 때는 구체적인 요구와 상황에 맞춰야 한다.</li> </ul>","path":["제 4 장   배열과 연결 리스트","4.5   요약"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/#2-q-a","level":3,"title":"2.   Q &amp; A","text":"<p>Q: 배열을 스택에 저장하는지 힙에 저장하는지에 따라 시간 효율과 공간 효율이 달라지는가?</p> <p>스택에 저장한 배열도 힙에 저장한 배열도 연속된 메모리 공간에 있으므로, 데이터 연산 효율은 대체로 같다. 다만 스택과 힙의 특성 때문에 다음과 같은 차이가 생긴다.</p> <ol> <li>할당 및 해제 효율: 스택은 비교적 작은 메모리 영역으로 컴파일러가 자동으로 할당한다. 힙은 더 크며 코드에서 동적으로 할당할 수 있고 단편화도 더 쉽게 발생한다. 따라서 힙의 할당 및 해제 연산은 보통 스택보다 느리다.</li> <li>크기 제한: 스택 메모리는 비교적 작고 힙의 크기는 대개 사용 가능한 메모리에 의해 제한된다. 따라서 큰 배열을 저장하기에는 힙이 더 적합하다.</li> <li>유연성: 스택에 저장하는 배열의 크기는 컴파일 시 정해야 하지만, 힙에 저장하는 배열의 크기는 실행 중에 동적으로 정할 수 있다.</li> </ol> <p>Q: 배열은 원소의 자료형이 같아야 한다고 하는데, 연결 리스트에서는 왜 이를 강조하지 않는가?</p> <p>연결 리스트는 참조(포인터)로 연결된 노드들로 이루어진다. 각 노드에는 <code>int</code>, <code>double</code>, <code>string</code>, <code>object</code> 등 서로 다른 자료형의 데이터를 저장할 수 있다.</p> <p>반면 배열 원소는 자료형이 같아야 오프셋을 계산하여 해당 원소의 위치를 구할 수 있다. 예를 들어 배열에 <code>int</code>와 <code>long</code>이 함께 있으면 원소 하나가 각각 4바이트와 8바이트를 차지한다. ‘원소 길이’가 두 가지이므로 다음 식으로 오프셋을 계산할 수 없다.</p> <pre><code># 원소 메모리 주소 = 배열 메모리 주소(첫 원소의 메모리 주소) + 원소 길이 * 원소 인덱스\n</code></pre> <p>Q: 노드 <code>P</code>를 삭제한 뒤 <code>P.next</code>를 <code>None</code>으로 설정해야 하는가?</p> <p><code>P.next</code>를 바꾸지 않아도 된다. 해당 연결 리스트에서는 머리 노드부터 꼬리 노드까지 순회해도 더 이상 <code>P</code>를 만나지 않는다. 즉, <code>P</code>는 이미 연결 리스트에서 삭제되었으므로 어디를 가리키든 이 연결 리스트에 영향을 주지 않는다.</p> <p>자료 구조와 알고리즘 문제를 푸는 관점에서는 프로그램 논리가 올바르기만 하면 연결을 끊지 않아도 된다. 표준 라이브러리 관점에서는 연결을 끊는 편이 더 안전하고 논리도 명확하다. 연결을 끊지 않은 상태에서 삭제한 노드가 정상적으로 회수되지 않으면, 뒤따르는 노드의 메모리 회수에도 영향을 줄 수 있다.</p> <p>Q: 연결 리스트의 삽입과 삭제는 시간 복잡도가 \\(O(1)\\)이라고 한다. 하지만 먼저 원소를 찾는 데 \\(O(n)\\)이 필요한데, 전체 시간 복잡도는 왜 \\(O(n)\\)이 아닌가?</p> <p>원소를 먼저 찾고 삭제한다면 시간 복잡도는 실제로 \\(O(n)\\)이다. 다만 연결 리스트의 \\(O(1)\\) 삽입 및 삭제는 다른 응용에서 장점을 발휘한다. 예를 들어 덱은 연결 리스트로 구현하기에 적합하다. 머리 노드와 꼬리 노드를 항상 가리키는 포인터 변수를 유지하면 매번 삽입과 삭제를 \\(O(1)\\)에 수행할 수 있다.</p> <p>Q: ‘연결 리스트의 정의와 저장 방식’ 그림에서 노드 포인터를 저장하는 연한 파란색 부분은 하나의 메모리 주소를 차지하는가? 아니면 노드 값과 반씩 차지하는가?</p> <p>그 그림은 정성적인 설명이다. 정확한 크기는 구체적인 상황에 따라 분석해야 한다.</p> <ul> <li>노드 값이 차지하는 공간은 자료형에 따라 다르다. 예를 들어 <code>int</code>, <code>long</code>, <code>double</code>, 인스턴스 객체 등이 있다.</li> <li>포인터 변수가 차지하는 메모리 크기는 운영체제와 컴파일 환경에 따라 정해지며, 대개 8바이트 또는 4바이트다.</li> </ul> <p>Q: 리스트 끝에 원소를 추가하는 연산은 항상 \\(O(1)\\)인가?</p> <p>원소를 추가할 때 리스트의 길이를 넘으면 먼저 용량을 늘려야 한다. 시스템이 새 메모리 영역을 요청하고 기존 원소 전체를 옮기므로, 이때의 시간 복잡도는 \\(O(n)\\)이다.</p> <p>Q: ‘리스트는 배열의 실용성을 크게 높였지만 메모리 공간 일부를 낭비할 수 있다’는 설명에서, 공간 낭비는 용량, 길이, 확장 배수 같은 추가 변수의 메모리를 뜻하는가?</p> <p>여기서 공간 낭비는 주로 두 가지를 뜻한다. 하나는 리스트에 초기 길이를 설정하지만 그만큼 필요하지 않을 수 있다는 것이다. 다른 하나는 잦은 확장을 피하려고 보통 \\(\\times 1.5\\)처럼 일정한 계수를 곱해 용량을 늘린다는 것이다. 이때도 빈자리가 많이 생기며, 보통 이를 전부 채우지는 못한다.</p> <p>Q: Python에서 <code>n = [1, 2, 3]</code>을 초기화하면 세 원소의 주소가 이어져 있다. 하지만 <code>m = [2, 1, 3]</code>을 초기화하면 원소들의 id가 연속적이지 않고 <code>n</code>의 같은 값과 각각 일치한다. 주소가 연속적이지 않아도 <code>m</code>은 배열인가?</p> <p>리스트 원소를 연결 리스트 노드로 바꾸어 <code>n = [n1, n2, n3, n4, n5]</code>라고 하면, 이 다섯 노드 객체도 보통 메모리 곳곳에 흩어져 있다. 그래도 리스트 인덱스가 주어지면 \\(O(1)\\) 시간에 노드의 메모리 주소를 얻어 해당 노드에 접근할 수 있다. 배열에 저장된 것은 노드 자체가 아니라 노드의 참조이기 때문이다.</p> <p>많은 언어와 달리 Python에서는 숫자도 객체로 감싸져 있으며, 리스트에는 숫자 자체가 아닌 숫자의 참조가 저장된다. 따라서 두 배열의 같은 숫자가 같은 id를 가지며, 숫자들의 메모리 주소는 연속적일 필요가 없다.</p> <p>Q: C++ STL의 <code>std::list</code>는 이중 연결 리스트를 구현하는데, 일부 알고리즘 책에서는 거의 직접 쓰지 않는 것 같다. 어떤 한계가 있기 때문인가?</p> <p>한편으로는 보통 배열로 알고리즘을 구현하는 것을 선호하며, 꼭 필요할 때만 연결 리스트를 사용한다. 주된 이유는 두 가지다.</p> <ul> <li>공간 비용: 원소마다 이전 원소와 다음 원소를 위한 포인터가 두 개 더 필요하므로 <code>std::list</code>는 보통 <code>std::vector</code>보다 공간을 많이 차지한다.</li> <li>캐시에 불리함: 데이터가 연속적으로 저장되지 않아 <code>std::list</code>의 캐시 활용률이 낮다. 일반적으로는 <code>std::vector</code>의 성능이 더 좋다.</li> </ul> <p>다른 한편으로 연결 리스트가 필요한 경우는 주로 이진 트리와 그래프다. 스택과 큐는 연결 리스트 대신 프로그래밍 언어가 제공하는 <code>stack</code>과 <code>queue</code>를 쓰는 경우가 많다.</p> <p>Q: <code>res = [[0]] * n</code>으로 만든 이차원 리스트에서 각 <code>[0]</code>은 서로 독립적인가?</p> <p>독립적이지 않다. 이 이차원 리스트의 모든 <code>[0]</code>은 실제로 같은 객체를 참조한다. 한 원소를 수정하면 대응하는 원소가 모두 함께 바뀐다.</p> <p>각 <code>[0]</code>을 독립적으로 만들려면 <code>res = [[0] for _ in range(n)]</code>을 사용하면 된다. 이 방식은 서로 독립적인 <code>[0]</code> 리스트 객체 \\(n\\)개를 초기화한다.</p> <p>Q: <code>res = [0] * n</code>으로 만든 리스트에서 각 정수 0은 서로 독립적인가?</p> <p>이 리스트의 모든 정수 0은 같은 객체를 참조한다. Python은 객체 재사용을 극대화하고 성능을 높이기 위해 작은 정수(보통 -5부터 256까지)를 캐시 풀로 관리하기 때문이다.</p> <p>모두 같은 객체를 가리켜도 리스트의 각 원소는 독립적으로 바꿀 수 있다. Python의 정수는 ‘불변 객체’이기 때문이다. 원소를 바꾸면 기존 객체 자체가 바뀌는 것이 아니라 다른 객체를 참조하게 된다.</p> <p>그러나 리스트 원소가 리스트, 딕셔너리, 클래스 인스턴스 같은 ‘가변 객체’라면, 원소의 내용을 수정할 때 객체 자체가 바뀌므로 이를 참조하는 모든 원소에서 같은 변화가 나타난다.</p>","path":["제 4 장   배열과 연결 리스트","4.5   요약"],"tags":[]},{"location":"chapter_backtracking/","level":1,"title":"제 13 장   백트래킹","text":"<p>이 장을 시작하며</p> <p>우리는 미로 속 탐험가처럼 앞으로 나아가는 길에서 어려움을 만날 수 있다.</p> <p>백트래킹의 힘은 다시 출발하고 끊임없이 시도하여, 마침내 빛으로 향하는 출구를 찾게 해 준다.</p>","path":["제 13 장   백트래킹"],"tags":[]},{"location":"chapter_backtracking/#_1","level":2,"title":"이 장의 내용","text":"<ul> <li>13.1   백트래킹 알고리즘</li> <li>13.2   모든 순열 문제</li> <li>13.3   부분 집합 합 문제</li> <li>13.4   n-퀸 문제</li> <li>13.5   요약</li> <li>13.6   연습 문제</li> </ul>","path":["제 13 장   백트래킹"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/","level":1,"title":"13.1   백트래킹 알고리즘","text":"<p>백트래킹 알고리즘(backtracking algorithm)은 모든 경우를 열거하여 문제를 푸는 방법이다. 초기 상태에서 출발하여 가능한 모든 해결 방법을 완전 탐색하고, 올바른 해를 만나면 기록한다. 해를 찾거나 가능한 선택을 모두 시도해도 찾을 수 없을 때까지 진행하는 것이 핵심이다.</p> <p>백트래킹은 보통 '깊이 우선 탐색'으로 해 공간을 순회한다. '이진 트리' 장에서 전위, 중위, 후위 순회가 모두 깊이 우선 탐색이라고 설명했다. 이제 전위 순회를 이용한 백트래킹 문제를 만들어 작동 원리를 차근차근 알아보자.</p> <p>예제 1</p> <p>이진 트리가 주어졌을 때 값이 \\(7\\)인 모든 노드를 찾아 기록하고, 노드 리스트를 반환하라.</p> <p>이 문제에서는 트리를 전위 순회하며 현재 노드의 값이 \\(7\\)인지 확인하고, 맞으면 해당 노드의 값을 결과 리스트 <code>res</code>에 추가한다. 과정은 아래 그림(13-1)과 코드에 나타나 있다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_i_compact.py<pre><code>def pre_order(root: TreeNode):\n \"\"\"전위 순회: 예제 1\"\"\"\n if root is None:\n return\n if root.val == 7:\n # 해를 기록한다\n res.append(root)\n pre_order(root.left)\n pre_order(root.right)\n</code></pre> preorder_traversal_i_compact.cpp<pre><code>/* 전위 순회: 예제 1 */\nvoid preOrder(TreeNode *root) {\n if (root == nullptr) {\n return;\n }\n if (root-&gt;val == 7) {\n // 해를 기록한다\n res.push_back(root);\n }\n preOrder(root-&gt;left);\n preOrder(root-&gt;right);\n}\n</code></pre> preorder_traversal_i_compact.java<pre><code>/* 전위 순회: 예제 1 */\nvoid preOrder(TreeNode root) {\n if (root == null) {\n return;\n }\n if (root.val == 7) {\n // 해를 기록한다\n res.add(root);\n }\n preOrder(root.left);\n preOrder(root.right);\n}\n</code></pre> preorder_traversal_i_compact.cs<pre><code>/* 전위 순회: 예제 1 */\nvoid PreOrder(TreeNode? root) {\n if (root == null) {\n return;\n }\n if (root.val == 7) {\n // 해를 기록한다\n res.Add(root);\n }\n PreOrder(root.left);\n PreOrder(root.right);\n}\n</code></pre> preorder_traversal_i_compact.go<pre><code>/* 전위 순회: 예제 1 */\nfunc preOrderI(root *TreeNode, res *[]*TreeNode) {\n if root == nil {\n return\n }\n if (root.Val).(int) == 7 {\n // 해를 기록한다\n *res = append(*res, root)\n }\n preOrderI(root.Left, res)\n preOrderI(root.Right, res)\n}\n</code></pre> preorder_traversal_i_compact.swift<pre><code>/* 전위 순회: 예제 1 */\nfunc preOrder(root: TreeNode?) {\n guard let root = root else {\n return\n }\n if root.val == 7 {\n // 해를 기록한다\n res.append(root)\n }\n preOrder(root: root.left)\n preOrder(root: root.right)\n}\n</code></pre> preorder_traversal_i_compact.js<pre><code>/* 전위 순회: 예제 1 */\nfunction preOrder(root, res) {\n if (root === null) {\n return;\n }\n if (root.val === 7) {\n // 해를 기록한다\n res.push(root);\n }\n preOrder(root.left, res);\n preOrder(root.right, res);\n}\n</code></pre> preorder_traversal_i_compact.ts<pre><code>/* 전위 순회: 예제 1 */\nfunction preOrder(root: TreeNode | null, res: TreeNode[]): void {\n if (root === null) {\n return;\n }\n if (root.val === 7) {\n // 해를 기록한다\n res.push(root);\n }\n preOrder(root.left, res);\n preOrder(root.right, res);\n}\n</code></pre> preorder_traversal_i_compact.dart<pre><code>/* 전위 순회: 예제 1 */\nvoid preOrder(TreeNode? root, List&lt;TreeNode&gt; res) {\n if (root == null) {\n return;\n }\n if (root.val == 7) {\n // 해를 기록한다\n res.add(root);\n }\n preOrder(root.left, res);\n preOrder(root.right, res);\n}\n</code></pre> preorder_traversal_i_compact.rs<pre><code>/* 전위 순회: 예제 1 */\nfn pre_order(res: &amp;mut Vec&lt;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;, root: Option&lt;&amp;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;) {\n if root.is_none() {\n return;\n }\n if let Some(node) = root {\n if node.borrow().val == 7 {\n // 해를 기록한다\n res.push(node.clone());\n }\n pre_order(res, node.borrow().left.as_ref());\n pre_order(res, node.borrow().right.as_ref());\n }\n}\n</code></pre> preorder_traversal_i_compact.c<pre><code>/* 전위 순회: 예제 1 */\nvoid preOrder(TreeNode *root) {\n if (root == NULL) {\n return;\n }\n if (root-&gt;val == 7) {\n // 해를 기록한다\n res[resSize++] = root;\n }\n preOrder(root-&gt;left);\n preOrder(root-&gt;right);\n}\n</code></pre> preorder_traversal_i_compact.kt<pre><code>/* 전위 순회: 예제 1 */\nfun preOrder(root: TreeNode?) {\n if (root == null) {\n return\n }\n if (root._val == 7) {\n // 해를 기록한다\n res!!.add(root)\n }\n preOrder(root.left)\n preOrder(root.right)\n}\n</code></pre> preorder_traversal_i_compact.rb<pre><code>### 전위 순회: 예제 1 ###\ndef pre_order(root)\n return unless root\n\n # 해를 기록한다\n $res &lt;&lt; root if root.val == 7\n\n pre_order(root.left)\n pre_order(root.right)\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p></p> <p> 그림 13-1   전위 순회로 노드 탐색 </p>","path":["제 13 장   백트래킹","13.1   백트래킹 알고리즘"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1311","level":2,"title":"13.1.1   시도와 되돌리기","text":"<p>백트래킹이라고 부르는 이유는 해 공간을 탐색할 때 '시도'와 '되돌리기' 전략을 사용하기 때문이다. 탐색 중 더 진행할 수 없거나 조건을 만족하는 해를 얻을 수 없는 상태를 만나면, 직전 선택을 취소하고 이전 상태로 돌아가 다른 선택을 시도한다.</p> <p>예제 1에서는 각 노드의 방문이 한 번의 '시도'이며, 리프 노드를 지나거나 부모 노드로 돌아가는 <code>return</code>이 '되돌리기'에 해당한다.</p> <p>되돌리기에는 함수 반환만 포함되는 것이 아니다. 이를 설명하기 위해 예제 1을 조금 확장해 보자.</p> <p>예제 2</p> <p>이진 트리에서 값이 \\(7\\)인 모든 노드를 찾아 루트 노드에서 각 노드까지의 경로를 반환하라.</p> <p>예제 1의 코드에 리스트 <code>path</code>를 추가하여 방문한 노드의 경로를 기록해야 한다. 값이 \\(7\\)인 노드를 방문하면 <code>path</code>를 복사하여 결과 리스트 <code>res</code>에 추가한다. 순회를 마치면 <code>res</code>에 모든 해가 저장된다. 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_ii_compact.py<pre><code>def pre_order(root: TreeNode):\n \"\"\"전위 순회: 예제 2\"\"\"\n if root is None:\n return\n # 시도\n path.append(root)\n if root.val == 7:\n # 해를 기록한다\n res.append(list(path))\n pre_order(root.left)\n pre_order(root.right)\n # 되돌리기\n path.pop()\n</code></pre> preorder_traversal_ii_compact.cpp<pre><code>/* 전위 순회: 예제 2 */\nvoid preOrder(TreeNode *root) {\n if (root == nullptr) {\n return;\n }\n // 시도\n path.push_back(root);\n if (root-&gt;val == 7) {\n // 해를 기록한다\n res.push_back(path);\n }\n preOrder(root-&gt;left);\n preOrder(root-&gt;right);\n // 되돌리기\n path.pop_back();\n}\n</code></pre> preorder_traversal_ii_compact.java<pre><code>/* 전위 순회: 예제 2 */\nvoid preOrder(TreeNode root) {\n if (root == null) {\n return;\n }\n // 시도\n path.add(root);\n if (root.val == 7) {\n // 해를 기록한다\n res.add(new ArrayList&lt;&gt;(path));\n }\n preOrder(root.left);\n preOrder(root.right);\n // 되돌리기\n path.remove(path.size() - 1);\n}\n</code></pre> preorder_traversal_ii_compact.cs<pre><code>/* 전위 순회: 예제 2 */\nvoid PreOrder(TreeNode? root) {\n if (root == null) {\n return;\n }\n // 시도\n path.Add(root);\n if (root.val == 7) {\n // 해를 기록한다\n res.Add(new List&lt;TreeNode&gt;(path));\n }\n PreOrder(root.left);\n PreOrder(root.right);\n // 되돌리기\n path.RemoveAt(path.Count - 1);\n}\n</code></pre> preorder_traversal_ii_compact.go<pre><code>/* 전위 순회: 예제 2 */\nfunc preOrderII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) {\n if root == nil {\n return\n }\n // 시도\n *path = append(*path, root)\n if root.Val.(int) == 7 {\n // 해를 기록한다\n *res = append(*res, append([]*TreeNode{}, *path...))\n }\n preOrderII(root.Left, res, path)\n preOrderII(root.Right, res, path)\n // 되돌리기\n *path = (*path)[:len(*path)-1]\n}\n</code></pre> preorder_traversal_ii_compact.swift<pre><code>/* 전위 순회: 예제 2 */\nfunc preOrder(root: TreeNode?) {\n guard let root = root else {\n return\n }\n // 시도\n path.append(root)\n if root.val == 7 {\n // 해를 기록한다\n res.append(path)\n }\n preOrder(root: root.left)\n preOrder(root: root.right)\n // 되돌리기\n path.removeLast()\n}\n</code></pre> preorder_traversal_ii_compact.js<pre><code>/* 전위 순회: 예제 2 */\nfunction preOrder(root, path, res) {\n if (root === null) {\n return;\n }\n // 시도\n path.push(root);\n if (root.val === 7) {\n // 해를 기록한다\n res.push([...path]);\n }\n preOrder(root.left, path, res);\n preOrder(root.right, path, res);\n // 되돌리기\n path.pop();\n}\n</code></pre> preorder_traversal_ii_compact.ts<pre><code>/* 전위 순회: 예제 2 */\nfunction preOrder(\n root: TreeNode | null,\n path: TreeNode[],\n res: TreeNode[][]\n): void {\n if (root === null) {\n return;\n }\n // 시도\n path.push(root);\n if (root.val === 7) {\n // 해를 기록한다\n res.push([...path]);\n }\n preOrder(root.left, path, res);\n preOrder(root.right, path, res);\n // 되돌리기\n path.pop();\n}\n</code></pre> preorder_traversal_ii_compact.dart<pre><code>/* 전위 순회: 예제 2 */\nvoid preOrder(\n TreeNode? root,\n List&lt;TreeNode&gt; path,\n List&lt;List&lt;TreeNode&gt;&gt; res,\n) {\n if (root == null) {\n return;\n }\n\n // 시도\n path.add(root);\n if (root.val == 7) {\n // 해를 기록한다\n res.add(List.from(path));\n }\n preOrder(root.left, path, res);\n preOrder(root.right, path, res);\n // 되돌리기\n path.removeLast();\n}\n</code></pre> preorder_traversal_ii_compact.rs<pre><code>/* 전위 순회: 예제 2 */\nfn pre_order(\n res: &amp;mut Vec&lt;Vec&lt;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;&gt;,\n path: &amp;mut Vec&lt;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;,\n root: Option&lt;&amp;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;,\n) {\n if root.is_none() {\n return;\n }\n if let Some(node) = root {\n // 시도\n path.push(node.clone());\n if node.borrow().val == 7 {\n // 해를 기록한다\n res.push(path.clone());\n }\n pre_order(res, path, node.borrow().left.as_ref());\n pre_order(res, path, node.borrow().right.as_ref());\n // 되돌리기\n path.pop();\n }\n}\n</code></pre> preorder_traversal_ii_compact.c<pre><code>/* 전위 순회: 예제 2 */\nvoid preOrder(TreeNode *root) {\n if (root == NULL) {\n return;\n }\n // 시도\n path[pathSize++] = root;\n if (root-&gt;val == 7) {\n // 해를 기록한다\n for (int i = 0; i &lt; pathSize; ++i) {\n res[resSize][i] = path[i];\n }\n resSize++;\n }\n preOrder(root-&gt;left);\n preOrder(root-&gt;right);\n // 되돌리기\n pathSize--;\n}\n</code></pre> preorder_traversal_ii_compact.kt<pre><code>/* 전위 순회: 예제 2 */\nfun preOrder(root: TreeNode?) {\n if (root == null) {\n return\n }\n // 시도\n path!!.add(root)\n if (root._val == 7) {\n // 해를 기록한다\n res!!.add(path!!.toMutableList())\n }\n preOrder(root.left)\n preOrder(root.right)\n // 되돌리기\n path!!.removeAt(path!!.size - 1)\n}\n</code></pre> preorder_traversal_ii_compact.rb<pre><code>### 전위 순회: 예제 2 ###\ndef pre_order(root)\n return unless root\n\n # 시도\n $path &lt;&lt; root\n\n # 해를 기록한다\n $res &lt;&lt; $path.dup if root.val == 7\n\n pre_order(root.left)\n pre_order(root.right)\n\n # 되돌리기\n $path.pop\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>매번 '시도'할 때는 현재 노드를 <code>path</code>에 추가하여 경로를 기록한다. '되돌리기' 전에는 이 노드를 <code>path</code>에서 꺼내어 이번 시도 이전의 상태를 복원해야 한다.</p> <p>아래 그림(13-2)의 과정을 보면 시도와 되돌리기를 각각 '전진'과 '취소'로 이해할 수 있으며, 두 연산은 서로 반대 방향이다.</p> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt;&lt;6&gt;&lt;7&gt;&lt;8&gt;&lt;9&gt;&lt;10&gt;&lt;11&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 13-2   시도와 되돌리기 </p>","path":["제 13 장   백트래킹","13.1   백트래킹 알고리즘"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1312","level":2,"title":"13.1.2   가지치기","text":"<p>복잡한 백트래킹 문제에는 보통 하나 이상의 제약 조건이 있으며, 이 제약 조건을 '가지치기'에 활용할 수 있다.</p> <p>예제 3</p> <p>이진 트리에서 값이 \\(7\\)인 모든 노드를 찾아 루트 노드에서 각 노드까지의 경로를 반환하라. 단, 경로에 값이 \\(3\\)인 노드가 포함되면 안 된다.</p> <p>이 제약을 만족하려면 가지치기 연산을 추가해야 한다. 탐색 중 값이 \\(3\\)인 노드를 만나면 더 탐색하지 않고 조기에 반환한다. 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_compact.py<pre><code>def pre_order(root: TreeNode):\n \"\"\"전위 순회: 예제 3\"\"\"\n # 가지치기\n if root is None or root.val == 3:\n return\n # 시도\n path.append(root)\n if root.val == 7:\n # 해를 기록한다\n res.append(list(path))\n pre_order(root.left)\n pre_order(root.right)\n # 되돌리기\n path.pop()\n</code></pre> preorder_traversal_iii_compact.cpp<pre><code>/* 전위 순회: 예제 3 */\nvoid preOrder(TreeNode *root) {\n // 가지치기\n if (root == nullptr || root-&gt;val == 3) {\n return;\n }\n // 시도\n path.push_back(root);\n if (root-&gt;val == 7) {\n // 해를 기록한다\n res.push_back(path);\n }\n preOrder(root-&gt;left);\n preOrder(root-&gt;right);\n // 되돌리기\n path.pop_back();\n}\n</code></pre> preorder_traversal_iii_compact.java<pre><code>/* 전위 순회: 예제 3 */\nvoid preOrder(TreeNode root) {\n // 가지치기\n if (root == null || root.val == 3) {\n return;\n }\n // 시도\n path.add(root);\n if (root.val == 7) {\n // 해를 기록한다\n res.add(new ArrayList&lt;&gt;(path));\n }\n preOrder(root.left);\n preOrder(root.right);\n // 되돌리기\n path.remove(path.size() - 1);\n}\n</code></pre> preorder_traversal_iii_compact.cs<pre><code>/* 전위 순회: 예제 3 */\nvoid PreOrder(TreeNode? root) {\n // 가지치기\n if (root == null || root.val == 3) {\n return;\n }\n // 시도\n path.Add(root);\n if (root.val == 7) {\n // 해를 기록한다\n res.Add(new List&lt;TreeNode&gt;(path));\n }\n PreOrder(root.left);\n PreOrder(root.right);\n // 되돌리기\n path.RemoveAt(path.Count - 1);\n}\n</code></pre> preorder_traversal_iii_compact.go<pre><code>/* 전위 순회: 예제 3 */\nfunc preOrderIII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) {\n // 가지치기\n if root == nil || root.Val == 3 {\n return\n }\n // 시도\n *path = append(*path, root)\n if root.Val.(int) == 7 {\n // 해를 기록한다\n *res = append(*res, append([]*TreeNode{}, *path...))\n }\n preOrderIII(root.Left, res, path)\n preOrderIII(root.Right, res, path)\n // 되돌리기\n *path = (*path)[:len(*path)-1]\n}\n</code></pre> preorder_traversal_iii_compact.swift<pre><code>/* 전위 순회: 예제 3 */\nfunc preOrder(root: TreeNode?) {\n // 가지치기\n guard let root = root, root.val != 3 else {\n return\n }\n // 시도\n path.append(root)\n if root.val == 7 {\n // 해를 기록한다\n res.append(path)\n }\n preOrder(root: root.left)\n preOrder(root: root.right)\n // 되돌리기\n path.removeLast()\n}\n</code></pre> preorder_traversal_iii_compact.js<pre><code>/* 전위 순회: 예제 3 */\nfunction preOrder(root, path, res) {\n // 가지치기\n if (root === null || root.val === 3) {\n return;\n }\n // 시도\n path.push(root);\n if (root.val === 7) {\n // 해를 기록한다\n res.push([...path]);\n }\n preOrder(root.left, path, res);\n preOrder(root.right, path, res);\n // 되돌리기\n path.pop();\n}\n</code></pre> preorder_traversal_iii_compact.ts<pre><code>/* 전위 순회: 예제 3 */\nfunction preOrder(\n root: TreeNode | null,\n path: TreeNode[],\n res: TreeNode[][]\n): void {\n // 가지치기\n if (root === null || root.val === 3) {\n return;\n }\n // 시도\n path.push(root);\n if (root.val === 7) {\n // 해를 기록한다\n res.push([...path]);\n }\n preOrder(root.left, path, res);\n preOrder(root.right, path, res);\n // 되돌리기\n path.pop();\n}\n</code></pre> preorder_traversal_iii_compact.dart<pre><code>/* 전위 순회: 예제 3 */\nvoid preOrder(\n TreeNode? root,\n List&lt;TreeNode&gt; path,\n List&lt;List&lt;TreeNode&gt;&gt; res,\n) {\n if (root == null || root.val == 3) {\n return;\n }\n\n // 시도\n path.add(root);\n if (root.val == 7) {\n // 해를 기록한다\n res.add(List.from(path));\n }\n preOrder(root.left, path, res);\n preOrder(root.right, path, res);\n // 되돌리기\n path.removeLast();\n}\n</code></pre> preorder_traversal_iii_compact.rs<pre><code>/* 전위 순회: 예제 3 */\nfn pre_order(\n res: &amp;mut Vec&lt;Vec&lt;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;&gt;,\n path: &amp;mut Vec&lt;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;,\n root: Option&lt;&amp;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;,\n) {\n // 가지치기\n if root.is_none() || root.as_ref().unwrap().borrow().val == 3 {\n return;\n }\n if let Some(node) = root {\n // 시도\n path.push(node.clone());\n if node.borrow().val == 7 {\n // 해를 기록한다\n res.push(path.clone());\n }\n pre_order(res, path, node.borrow().left.as_ref());\n pre_order(res, path, node.borrow().right.as_ref());\n // 되돌리기\n path.pop();\n }\n}\n</code></pre> preorder_traversal_iii_compact.c<pre><code>/* 전위 순회: 예제 3 */\nvoid preOrder(TreeNode *root) {\n // 가지치기\n if (root == NULL || root-&gt;val == 3) {\n return;\n }\n // 시도\n path[pathSize++] = root;\n if (root-&gt;val == 7) {\n // 해를 기록한다\n for (int i = 0; i &lt; pathSize; i++) {\n res[resSize][i] = path[i];\n }\n resSize++;\n }\n preOrder(root-&gt;left);\n preOrder(root-&gt;right);\n // 되돌리기\n pathSize--;\n}\n</code></pre> preorder_traversal_iii_compact.kt<pre><code>/* 전위 순회: 예제 3 */\nfun preOrder(root: TreeNode?) {\n // 가지치기\n if (root == null || root._val == 3) {\n return\n }\n // 시도\n path!!.add(root)\n if (root._val == 7) {\n // 해를 기록한다\n res!!.add(path!!.toMutableList())\n }\n preOrder(root.left)\n preOrder(root.right)\n // 되돌리기\n path!!.removeAt(path!!.size - 1)\n}\n</code></pre> preorder_traversal_iii_compact.rb<pre><code>### 전위 순회: 예제 3 ###\ndef pre_order(root)\n # 가지치기\n return if !root || root.val == 3\n\n # 시도\n $path.append(root)\n\n # 해를 기록한다\n $res &lt;&lt; $path.dup if root.val == 7\n\n pre_order(root.left)\n pre_order(root.right)\n\n # 되돌리기\n $path.pop\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>'가지치기'는 매우 직관적인 이름이다. 아래 그림(13-3)과 같이 탐색 중 제약 조건을 만족하지 않는 탐색 가지를 '잘라 내어' 의미 없는 시도를 많이 피하고 탐색 효율을 높인다.</p> <p></p> <p> 그림 13-3   제약 조건에 따른 가지치기 </p>","path":["제 13 장   백트래킹","13.1   백트래킹 알고리즘"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1313","level":2,"title":"13.1.3   기본 틀 코드","text":"<p>이제 백트래킹의 '시도, 되돌리기, 가지치기'라는 기본 틀을 추출하여 코드의 범용성을 높여 보자.</p> <p>아래 기본 틀 코드에서 <code>state</code>는 문제의 현재 상태를, <code>choices</code>는 그 상태에서 가능한 선택들을 나타낸다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby <pre><code>def backtrack(state: State, choices: list[choice], res: list[state]):\n \"\"\"백트래킹 알고리즘의 기본 틀\"\"\"\n # 해인지 확인\n if is_solution(state):\n # 해 기록\n record_solution(state, res)\n # 더 이상 탐색하지 않는다\n return\n # 모든 선택지 순회\n for choice in choices:\n # 가지치기: 선택이 유효한지 확인\n if is_valid(state, choice):\n # 시도: 선택하고 상태 갱신\n make_choice(state, choice)\n backtrack(state, choices, res)\n # 되돌리기: 선택을 취소하고 이전 상태 복원\n undo_choice(state, choice)\n</code></pre> <pre><code>/* 백트래킹 알고리즘의 기본 틀 */\nvoid backtrack(State *state, vector&lt;Choice *&gt; &amp;choices, vector&lt;State *&gt; &amp;res) {\n // 해인지 확인\n if (isSolution(state)) {\n // 해 기록\n recordSolution(state, res);\n // 더 이상 탐색하지 않는다\n return;\n }\n // 모든 선택지 순회\n for (Choice choice : choices) {\n // 가지치기: 선택이 유효한지 확인\n if (isValid(state, choice)) {\n // 시도: 선택하고 상태 갱신\n makeChoice(state, choice);\n backtrack(state, choices, res);\n // 되돌리기: 선택을 취소하고 이전 상태 복원\n undoChoice(state, choice);\n }\n }\n}\n</code></pre> <pre><code>/* 백트래킹 알고리즘의 기본 틀 */\nvoid backtrack(State state, List&lt;Choice&gt; choices, List&lt;State&gt; res) {\n // 해인지 확인\n if (isSolution(state)) {\n // 해 기록\n recordSolution(state, res);\n // 더 이상 탐색하지 않는다\n return;\n }\n // 모든 선택지 순회\n for (Choice choice : choices) {\n // 가지치기: 선택이 유효한지 확인\n if (isValid(state, choice)) {\n // 시도: 선택하고 상태 갱신\n makeChoice(state, choice);\n backtrack(state, choices, res);\n // 되돌리기: 선택을 취소하고 이전 상태 복원\n undoChoice(state, choice);\n }\n }\n}\n</code></pre> <pre><code>/* 백트래킹 알고리즘의 기본 틀 */\nvoid Backtrack(State state, List&lt;Choice&gt; choices, List&lt;State&gt; res) {\n // 해인지 확인\n if (IsSolution(state)) {\n // 해 기록\n RecordSolution(state, res);\n // 더 이상 탐색하지 않는다\n return;\n }\n // 모든 선택지 순회\n foreach (Choice choice in choices) {\n // 가지치기: 선택이 유효한지 확인\n if (IsValid(state, choice)) {\n // 시도: 선택하고 상태 갱신\n MakeChoice(state, choice);\n Backtrack(state, choices, res);\n // 되돌리기: 선택을 취소하고 이전 상태 복원\n UndoChoice(state, choice);\n }\n }\n}\n</code></pre> <pre><code>/* 백트래킹 알고리즘의 기본 틀 */\nfunc backtrack(state *State, choices []Choice, res *[]State) {\n // 해인지 확인\n if isSolution(state) {\n // 해 기록\n recordSolution(state, res)\n // 더 이상 탐색하지 않는다\n return\n }\n // 모든 선택지 순회\n for _, choice := range choices {\n // 가지치기: 선택이 유효한지 확인\n if isValid(state, choice) {\n // 시도: 선택하고 상태 갱신\n makeChoice(state, choice)\n backtrack(state, choices, res)\n // 되돌리기: 선택을 취소하고 이전 상태 복원\n undoChoice(state, choice)\n }\n }\n}\n</code></pre> <pre><code>/* 백트래킹 알고리즘의 기본 틀 */\nfunc backtrack(state: inout State, choices: [Choice], res: inout [State]) {\n // 해인지 확인\n if isSolution(state: state) {\n // 해 기록\n recordSolution(state: state, res: &amp;res)\n // 더 이상 탐색하지 않는다\n return\n }\n // 모든 선택지 순회\n for choice in choices {\n // 가지치기: 선택이 유효한지 확인\n if isValid(state: state, choice: choice) {\n // 시도: 선택하고 상태 갱신\n makeChoice(state: &amp;state, choice: choice)\n backtrack(state: &amp;state, choices: choices, res: &amp;res)\n // 되돌리기: 선택을 취소하고 이전 상태 복원\n undoChoice(state: &amp;state, choice: choice)\n }\n }\n}\n</code></pre> <pre><code>/* 백트래킹 알고리즘의 기본 틀 */\nfunction backtrack(state, choices, res) {\n // 해인지 확인\n if (isSolution(state)) {\n // 해 기록\n recordSolution(state, res);\n // 더 이상 탐색하지 않는다\n return;\n }\n // 모든 선택지 순회\n for (let choice of choices) {\n // 가지치기: 선택이 유효한지 확인\n if (isValid(state, choice)) {\n // 시도: 선택하고 상태 갱신\n makeChoice(state, choice);\n backtrack(state, choices, res);\n // 되돌리기: 선택을 취소하고 이전 상태 복원\n undoChoice(state, choice);\n }\n }\n}\n</code></pre> <pre><code>/* 백트래킹 알고리즘의 기본 틀 */\nfunction backtrack(state: State, choices: Choice[], res: State[]): void {\n // 해인지 확인\n if (isSolution(state)) {\n // 해 기록\n recordSolution(state, res);\n // 더 이상 탐색하지 않는다\n return;\n }\n // 모든 선택지 순회\n for (let choice of choices) {\n // 가지치기: 선택이 유효한지 확인\n if (isValid(state, choice)) {\n // 시도: 선택하고 상태 갱신\n makeChoice(state, choice);\n backtrack(state, choices, res);\n // 되돌리기: 선택을 취소하고 이전 상태 복원\n undoChoice(state, choice);\n }\n }\n}\n</code></pre> <pre><code>/* 백트래킹 알고리즘의 기본 틀 */\nvoid backtrack(State state, List&lt;Choice&gt;, List&lt;State&gt; res) {\n // 해인지 확인\n if (isSolution(state)) {\n // 해 기록\n recordSolution(state, res);\n // 더 이상 탐색하지 않는다\n return;\n }\n // 모든 선택지 순회\n for (Choice choice in choices) {\n // 가지치기: 선택이 유효한지 확인\n if (isValid(state, choice)) {\n // 시도: 선택하고 상태 갱신\n makeChoice(state, choice);\n backtrack(state, choices, res);\n // 되돌리기: 선택을 취소하고 이전 상태 복원\n undoChoice(state, choice);\n }\n }\n}\n</code></pre> <pre><code>/* 백트래킹 알고리즘의 기본 틀 */\nfn backtrack(state: &amp;mut State, choices: &amp;Vec&lt;Choice&gt;, res: &amp;mut Vec&lt;State&gt;) {\n // 해인지 확인\n if is_solution(state) {\n // 해 기록\n record_solution(state, res);\n // 더 이상 탐색하지 않는다\n return;\n }\n // 모든 선택지 순회\n for choice in choices {\n // 가지치기: 선택이 유효한지 확인\n if is_valid(state, choice) {\n // 시도: 선택하고 상태 갱신\n make_choice(state, choice);\n backtrack(state, choices, res);\n // 되돌리기: 선택을 취소하고 이전 상태 복원\n undo_choice(state, choice);\n }\n }\n}\n</code></pre> <pre><code>/* 백트래킹 알고리즘의 기본 틀 */\nvoid backtrack(State *state, Choice *choices, int numChoices, State *res, int numRes) {\n // 해인지 확인\n if (isSolution(state)) {\n // 해 기록\n recordSolution(state, res, numRes);\n // 더 이상 탐색하지 않는다\n return;\n }\n // 모든 선택지 순회\n for (int i = 0; i &lt; numChoices; i++) {\n // 가지치기: 선택이 유효한지 확인\n if (isValid(state, &amp;choices[i])) {\n // 시도: 선택하고 상태 갱신\n makeChoice(state, &amp;choices[i]);\n backtrack(state, choices, numChoices, res, numRes);\n // 되돌리기: 선택을 취소하고 이전 상태 복원\n undoChoice(state, &amp;choices[i]);\n }\n }\n}\n</code></pre> <pre><code>/* 백트래킹 알고리즘의 기본 틀 */\nfun backtrack(state: State?, choices: List&lt;Choice?&gt;, res: List&lt;State?&gt;?) {\n // 해인지 확인\n if (isSolution(state)) {\n // 해 기록\n recordSolution(state, res)\n // 더 이상 탐색하지 않는다\n return\n }\n // 모든 선택지 순회\n for (choice in choices) {\n // 가지치기: 선택이 유효한지 확인\n if (isValid(state, choice)) {\n // 시도: 선택하고 상태 갱신\n makeChoice(state, choice)\n backtrack(state, choices, res)\n // 되돌리기: 선택을 취소하고 이전 상태 복원\n undoChoice(state, choice)\n }\n }\n}\n</code></pre> <pre><code>### 백트래킹 알고리즘의 기본 틀 ###\ndef backtrack(state, choices, res)\n # 해인지 확인\n if is_solution?(state)\n # 해 기록\n record_solution(state, res)\n return\n end\n\n # 모든 선택지 순회\n for choice in choices\n # 가지치기: 선택이 유효한지 확인\n if is_valid?(state, choice)\n # 시도: 선택하고 상태 갱신\n make_choice(state, choice)\n backtrack(state, choices, res)\n # 되돌리기: 선택을 취소하고 이전 상태 복원\n undo_choice(state, choice)\n end\n end\nend\n</code></pre> <p>이제 기본 틀 코드로 예제 3을 해결해 보자. 상태 <code>state</code>는 노드 순회 경로, 선택지 <code>choices</code>는 현재 노드의 왼쪽과 오른쪽 자식 노드, 결과 <code>res</code>는 경로들의 리스트다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_template.py<pre><code>def is_solution(state: list[TreeNode]) -&gt; bool:\n \"\"\"현재 상태가 해인지 판단\"\"\"\n return state and state[-1].val == 7\n\ndef record_solution(state: list[TreeNode], res: list[list[TreeNode]]):\n \"\"\"해를 기록한다\"\"\"\n res.append(list(state))\n\ndef is_valid(state: list[TreeNode], choice: TreeNode) -&gt; bool:\n \"\"\"현재 상태에서 해당 선택이 유효한지 판단\"\"\"\n return choice is not None and choice.val != 3\n\ndef make_choice(state: list[TreeNode], choice: TreeNode):\n \"\"\"상태 갱신\"\"\"\n state.append(choice)\n\ndef undo_choice(state: list[TreeNode], choice: TreeNode):\n \"\"\"상태 복원\"\"\"\n state.pop()\n\ndef backtrack(\n state: list[TreeNode], choices: list[TreeNode], res: list[list[TreeNode]]\n):\n \"\"\"백트래킹 알고리즘: 예제 3\"\"\"\n # 해인지 확인한다\n if is_solution(state):\n # 해를 기록한다\n record_solution(state, res)\n # 모든 선택지를 순회한다\n for choice in choices:\n # 가지치기: 선택이 유효한지 확인한다\n if is_valid(state, choice):\n # 시도: 선택하고 상태를 갱신한다\n make_choice(state, choice)\n # 다음 선택을 진행한다\n backtrack(state, [choice.left, choice.right], res)\n # 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n undo_choice(state, choice)\n</code></pre> preorder_traversal_iii_template.cpp<pre><code>/* 현재 상태가 해인지 판단 */\nbool isSolution(vector&lt;TreeNode *&gt; &amp;state) {\n return !state.empty() &amp;&amp; state.back()-&gt;val == 7;\n}\n\n/* 해를 기록한다 */\nvoid recordSolution(vector&lt;TreeNode *&gt; &amp;state, vector&lt;vector&lt;TreeNode *&gt;&gt; &amp;res) {\n res.push_back(state);\n}\n\n/* 현재 상태에서 해당 선택이 유효한지 판단 */\nbool isValid(vector&lt;TreeNode *&gt; &amp;state, TreeNode *choice) {\n return choice != nullptr &amp;&amp; choice-&gt;val != 3;\n}\n\n/* 상태 갱신 */\nvoid makeChoice(vector&lt;TreeNode *&gt; &amp;state, TreeNode *choice) {\n state.push_back(choice);\n}\n\n/* 상태 복원 */\nvoid undoChoice(vector&lt;TreeNode *&gt; &amp;state, TreeNode *choice) {\n state.pop_back();\n}\n\n/* 백트래킹 알고리즘: 예제 3 */\nvoid backtrack(vector&lt;TreeNode *&gt; &amp;state, vector&lt;TreeNode *&gt; &amp;choices, vector&lt;vector&lt;TreeNode *&gt;&gt; &amp;res) {\n // 해인지 확인한다\n if (isSolution(state)) {\n // 해를 기록한다\n recordSolution(state, res);\n }\n // 모든 선택지를 순회한다\n for (TreeNode *choice : choices) {\n // 가지치기: 선택이 유효한지 확인한다\n if (isValid(state, choice)) {\n // 시도: 선택하고 상태를 갱신한다\n makeChoice(state, choice);\n // 다음 선택을 진행한다\n vector&lt;TreeNode *&gt; nextChoices{choice-&gt;left, choice-&gt;right};\n backtrack(state, nextChoices, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n undoChoice(state, choice);\n }\n }\n}\n</code></pre> preorder_traversal_iii_template.java<pre><code>/* 현재 상태가 해인지 판단 */\nboolean isSolution(List&lt;TreeNode&gt; state) {\n return !state.isEmpty() &amp;&amp; state.get(state.size() - 1).val == 7;\n}\n\n/* 해를 기록한다 */\nvoid recordSolution(List&lt;TreeNode&gt; state, List&lt;List&lt;TreeNode&gt;&gt; res) {\n res.add(new ArrayList&lt;&gt;(state));\n}\n\n/* 현재 상태에서 해당 선택이 유효한지 판단 */\nboolean isValid(List&lt;TreeNode&gt; state, TreeNode choice) {\n return choice != null &amp;&amp; choice.val != 3;\n}\n\n/* 상태 갱신 */\nvoid makeChoice(List&lt;TreeNode&gt; state, TreeNode choice) {\n state.add(choice);\n}\n\n/* 상태 복원 */\nvoid undoChoice(List&lt;TreeNode&gt; state, TreeNode choice) {\n state.remove(state.size() - 1);\n}\n\n/* 백트래킹 알고리즘: 예제 3 */\nvoid backtrack(List&lt;TreeNode&gt; state, List&lt;TreeNode&gt; choices, List&lt;List&lt;TreeNode&gt;&gt; res) {\n // 해인지 확인한다\n if (isSolution(state)) {\n // 해를 기록한다\n recordSolution(state, res);\n }\n // 모든 선택지를 순회한다\n for (TreeNode choice : choices) {\n // 가지치기: 선택이 유효한지 확인한다\n if (isValid(state, choice)) {\n // 시도: 선택하고 상태를 갱신한다\n makeChoice(state, choice);\n // 다음 선택을 진행한다\n backtrack(state, Arrays.asList(choice.left, choice.right), res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n undoChoice(state, choice);\n }\n }\n}\n</code></pre> preorder_traversal_iii_template.cs<pre><code>/* 현재 상태가 해인지 판단 */\nbool IsSolution(List&lt;TreeNode&gt; state) {\n return state.Count != 0 &amp;&amp; state[^1].val == 7;\n}\n\n/* 해를 기록한다 */\nvoid RecordSolution(List&lt;TreeNode&gt; state, List&lt;List&lt;TreeNode&gt;&gt; res) {\n res.Add(new List&lt;TreeNode&gt;(state));\n}\n\n/* 현재 상태에서 해당 선택이 유효한지 판단 */\nbool IsValid(List&lt;TreeNode&gt; state, TreeNode choice) {\n return choice != null &amp;&amp; choice.val != 3;\n}\n\n/* 상태 갱신 */\nvoid MakeChoice(List&lt;TreeNode&gt; state, TreeNode choice) {\n state.Add(choice);\n}\n\n/* 상태 복원 */\nvoid UndoChoice(List&lt;TreeNode&gt; state, TreeNode choice) {\n state.RemoveAt(state.Count - 1);\n}\n\n/* 백트래킹 알고리즘: 예제 3 */\nvoid Backtrack(List&lt;TreeNode&gt; state, List&lt;TreeNode&gt; choices, List&lt;List&lt;TreeNode&gt;&gt; res) {\n // 해인지 확인한다\n if (IsSolution(state)) {\n // 해를 기록한다\n RecordSolution(state, res);\n }\n // 모든 선택지를 순회한다\n foreach (TreeNode choice in choices) {\n // 가지치기: 선택이 유효한지 확인한다\n if (IsValid(state, choice)) {\n // 시도: 선택하고 상태를 갱신한다\n MakeChoice(state, choice);\n // 다음 선택을 진행한다\n Backtrack(state, [choice.left!, choice.right!], res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n UndoChoice(state, choice);\n }\n }\n}\n</code></pre> preorder_traversal_iii_template.go<pre><code>/* 현재 상태가 해인지 판단 */\nfunc isSolution(state *[]*TreeNode) bool {\n return len(*state) != 0 &amp;&amp; (*state)[len(*state)-1].Val == 7\n}\n\n/* 해를 기록한다 */\nfunc recordSolution(state *[]*TreeNode, res *[][]*TreeNode) {\n *res = append(*res, append([]*TreeNode{}, *state...))\n}\n\n/* 현재 상태에서 해당 선택이 유효한지 판단 */\nfunc isValid(state *[]*TreeNode, choice *TreeNode) bool {\n return choice != nil &amp;&amp; choice.Val != 3\n}\n\n/* 상태 갱신 */\nfunc makeChoice(state *[]*TreeNode, choice *TreeNode) {\n *state = append(*state, choice)\n}\n\n/* 상태 복원 */\nfunc undoChoice(state *[]*TreeNode, choice *TreeNode) {\n *state = (*state)[:len(*state)-1]\n}\n\n/* 백트래킹 알고리즘: 예제 3 */\nfunc backtrackIII(state *[]*TreeNode, choices *[]*TreeNode, res *[][]*TreeNode) {\n // 해인지 확인한다\n if isSolution(state) {\n // 해를 기록한다\n recordSolution(state, res)\n }\n // 모든 선택지를 순회한다\n for _, choice := range *choices {\n // 가지치기: 선택이 유효한지 확인한다\n if isValid(state, choice) {\n // 시도: 선택하고 상태를 갱신한다\n makeChoice(state, choice)\n // 다음 선택을 진행한다\n temp := make([]*TreeNode, 0)\n temp = append(temp, choice.Left, choice.Right)\n backtrackIII(state, &amp;temp, res)\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n undoChoice(state, choice)\n }\n }\n}\n</code></pre> preorder_traversal_iii_template.swift<pre><code>/* 현재 상태가 해인지 판단 */\nfunc isSolution(state: [TreeNode]) -&gt; Bool {\n !state.isEmpty &amp;&amp; state.last!.val == 7\n}\n\n/* 해를 기록한다 */\nfunc recordSolution(state: [TreeNode], res: inout [[TreeNode]]) {\n res.append(state)\n}\n\n/* 현재 상태에서 해당 선택이 유효한지 판단 */\nfunc isValid(state: [TreeNode], choice: TreeNode?) -&gt; Bool {\n choice != nil &amp;&amp; choice!.val != 3\n}\n\n/* 상태 갱신 */\nfunc makeChoice(state: inout [TreeNode], choice: TreeNode) {\n state.append(choice)\n}\n\n/* 상태 복원 */\nfunc undoChoice(state: inout [TreeNode], choice: TreeNode) {\n state.removeLast()\n}\n\n/* 백트래킹 알고리즘: 예제 3 */\nfunc backtrack(state: inout [TreeNode], choices: [TreeNode], res: inout [[TreeNode]]) {\n // 해인지 확인한다\n if isSolution(state: state) {\n recordSolution(state: state, res: &amp;res)\n }\n // 모든 선택지를 순회한다\n for choice in choices {\n // 가지치기: 선택이 유효한지 확인한다\n if isValid(state: state, choice: choice) {\n // 시도: 선택하고 상태를 갱신한다\n makeChoice(state: &amp;state, choice: choice)\n // 다음 선택을 진행한다\n backtrack(state: &amp;state, choices: [choice.left, choice.right].compactMap { $0 }, res: &amp;res)\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n undoChoice(state: &amp;state, choice: choice)\n }\n }\n}\n</code></pre> preorder_traversal_iii_template.js<pre><code>/* 현재 상태가 해인지 판단 */\nfunction isSolution(state) {\n return state &amp;&amp; state[state.length - 1]?.val === 7;\n}\n\n/* 해를 기록한다 */\nfunction recordSolution(state, res) {\n res.push([...state]);\n}\n\n/* 현재 상태에서 해당 선택이 유효한지 판단 */\nfunction isValid(state, choice) {\n return choice !== null &amp;&amp; choice.val !== 3;\n}\n\n/* 상태 갱신 */\nfunction makeChoice(state, choice) {\n state.push(choice);\n}\n\n/* 상태 복원 */\nfunction undoChoice(state) {\n state.pop();\n}\n\n/* 백트래킹 알고리즘: 예제 3 */\nfunction backtrack(state, choices, res) {\n // 해인지 확인한다\n if (isSolution(state)) {\n // 해를 기록한다\n recordSolution(state, res);\n }\n // 모든 선택지를 순회한다\n for (const choice of choices) {\n // 가지치기: 선택이 유효한지 확인한다\n if (isValid(state, choice)) {\n // 시도: 선택하고 상태를 갱신한다\n makeChoice(state, choice);\n // 다음 선택을 진행한다\n backtrack(state, [choice.left, choice.right], res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n undoChoice(state);\n }\n }\n}\n</code></pre> preorder_traversal_iii_template.ts<pre><code>/* 현재 상태가 해인지 판단 */\nfunction isSolution(state: TreeNode[]): boolean {\n return state &amp;&amp; state[state.length - 1]?.val === 7;\n}\n\n/* 해를 기록한다 */\nfunction recordSolution(state: TreeNode[], res: TreeNode[][]): void {\n res.push([...state]);\n}\n\n/* 현재 상태에서 해당 선택이 유효한지 판단 */\nfunction isValid(state: TreeNode[], choice: TreeNode): boolean {\n return choice !== null &amp;&amp; choice.val !== 3;\n}\n\n/* 상태 갱신 */\nfunction makeChoice(state: TreeNode[], choice: TreeNode): void {\n state.push(choice);\n}\n\n/* 상태 복원 */\nfunction undoChoice(state: TreeNode[]): void {\n state.pop();\n}\n\n/* 백트래킹 알고리즘: 예제 3 */\nfunction backtrack(\n state: TreeNode[],\n choices: TreeNode[],\n res: TreeNode[][]\n): void {\n // 해인지 확인한다\n if (isSolution(state)) {\n // 해를 기록한다\n recordSolution(state, res);\n }\n // 모든 선택지를 순회한다\n for (const choice of choices) {\n // 가지치기: 선택이 유효한지 확인한다\n if (isValid(state, choice)) {\n // 시도: 선택하고 상태를 갱신한다\n makeChoice(state, choice);\n // 다음 선택을 진행한다\n backtrack(state, [choice.left, choice.right], res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n undoChoice(state);\n }\n }\n}\n</code></pre> preorder_traversal_iii_template.dart<pre><code>/* 현재 상태가 해인지 판단 */\nbool isSolution(List&lt;TreeNode&gt; state) {\n return state.isNotEmpty &amp;&amp; state.last.val == 7;\n}\n\n/* 해를 기록한다 */\nvoid recordSolution(List&lt;TreeNode&gt; state, List&lt;List&lt;TreeNode&gt;&gt; res) {\n res.add(List.from(state));\n}\n\n/* 현재 상태에서 해당 선택이 유효한지 판단 */\nbool isValid(List&lt;TreeNode&gt; state, TreeNode? choice) {\n return choice != null &amp;&amp; choice.val != 3;\n}\n\n/* 상태 갱신 */\nvoid makeChoice(List&lt;TreeNode&gt; state, TreeNode? choice) {\n state.add(choice!);\n}\n\n/* 상태 복원 */\nvoid undoChoice(List&lt;TreeNode&gt; state, TreeNode? choice) {\n state.removeLast();\n}\n\n/* 백트래킹 알고리즘: 예제 3 */\nvoid backtrack(\n List&lt;TreeNode&gt; state,\n List&lt;TreeNode?&gt; choices,\n List&lt;List&lt;TreeNode&gt;&gt; res,\n) {\n // 해인지 확인한다\n if (isSolution(state)) {\n // 해를 기록한다\n recordSolution(state, res);\n }\n // 모든 선택지를 순회한다\n for (TreeNode? choice in choices) {\n // 가지치기: 선택이 유효한지 확인한다\n if (isValid(state, choice)) {\n // 시도: 선택하고 상태를 갱신한다\n makeChoice(state, choice);\n // 다음 선택을 진행한다\n backtrack(state, [choice!.left, choice.right], res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n undoChoice(state, choice);\n }\n }\n}\n</code></pre> preorder_traversal_iii_template.rs<pre><code>/* 현재 상태가 해인지 판단 */\nfn is_solution(state: &amp;mut Vec&lt;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;) -&gt; bool {\n return !state.is_empty() &amp;&amp; state.last().unwrap().borrow().val == 7;\n}\n\n/* 해를 기록한다 */\nfn record_solution(\n state: &amp;mut Vec&lt;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;,\n res: &amp;mut Vec&lt;Vec&lt;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;&gt;,\n) {\n res.push(state.clone());\n}\n\n/* 현재 상태에서 해당 선택이 유효한지 판단 */\nfn is_valid(_: &amp;mut Vec&lt;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;, choice: Option&lt;&amp;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;) -&gt; bool {\n return choice.is_some() &amp;&amp; choice.unwrap().borrow().val != 3;\n}\n\n/* 상태 갱신 */\nfn make_choice(state: &amp;mut Vec&lt;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;, choice: Rc&lt;RefCell&lt;TreeNode&gt;&gt;) {\n state.push(choice);\n}\n\n/* 상태 복원 */\nfn undo_choice(state: &amp;mut Vec&lt;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;, _: Rc&lt;RefCell&lt;TreeNode&gt;&gt;) {\n state.pop();\n}\n\n/* 백트래킹 알고리즘: 예제 3 */\nfn backtrack(\n state: &amp;mut Vec&lt;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;,\n choices: &amp;Vec&lt;Option&lt;&amp;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;&gt;,\n res: &amp;mut Vec&lt;Vec&lt;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;&gt;,\n) {\n // 해인지 확인한다\n if is_solution(state) {\n // 해를 기록한다\n record_solution(state, res);\n }\n // 모든 선택지를 순회한다\n for &amp;choice in choices.iter() {\n // 가지치기: 선택이 유효한지 확인한다\n if is_valid(state, choice) {\n // 시도: 선택하고 상태를 갱신한다\n make_choice(state, choice.unwrap().clone());\n // 다음 선택을 진행한다\n backtrack(\n state,\n &amp;vec![\n choice.unwrap().borrow().left.as_ref(),\n choice.unwrap().borrow().right.as_ref(),\n ],\n res,\n );\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n undo_choice(state, choice.unwrap().clone());\n }\n }\n}\n</code></pre> preorder_traversal_iii_template.c<pre><code>/* 현재 상태가 해인지 판단 */\nbool isSolution(void) {\n return pathSize &gt; 0 &amp;&amp; path[pathSize - 1]-&gt;val == 7;\n}\n\n/* 해를 기록한다 */\nvoid recordSolution(void) {\n for (int i = 0; i &lt; pathSize; i++) {\n res[resSize][i] = path[i];\n }\n resSize++;\n}\n\n/* 현재 상태에서 해당 선택이 유효한지 판단 */\nbool isValid(TreeNode *choice) {\n return choice != NULL &amp;&amp; choice-&gt;val != 3;\n}\n\n/* 상태 갱신 */\nvoid makeChoice(TreeNode *choice) {\n path[pathSize++] = choice;\n}\n\n/* 상태 복원 */\nvoid undoChoice(void) {\n pathSize--;\n}\n\n/* 백트래킹 알고리즘: 예제 3 */\nvoid backtrack(TreeNode *choices[2]) {\n // 해인지 확인한다\n if (isSolution()) {\n // 해를 기록한다\n recordSolution();\n }\n // 모든 선택지를 순회한다\n for (int i = 0; i &lt; 2; i++) {\n TreeNode *choice = choices[i];\n // 가지치기: 선택이 유효한지 확인한다\n if (isValid(choice)) {\n // 시도: 선택하고 상태를 갱신한다\n makeChoice(choice);\n // 다음 선택을 진행한다\n TreeNode *nextChoices[2] = {choice-&gt;left, choice-&gt;right};\n backtrack(nextChoices);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n undoChoice();\n }\n }\n}\n</code></pre> preorder_traversal_iii_template.kt<pre><code>/* 현재 상태가 해인지 판단 */\nfun isSolution(state: MutableList&lt;TreeNode?&gt;): Boolean {\n return state.isNotEmpty() &amp;&amp; state[state.size - 1]?._val == 7\n}\n\n/* 해를 기록한다 */\nfun recordSolution(state: MutableList&lt;TreeNode?&gt;?, res: MutableList&lt;MutableList&lt;TreeNode?&gt;?&gt;) {\n res.add(state!!.toMutableList())\n}\n\n/* 현재 상태에서 해당 선택이 유효한지 판단 */\nfun isValid(state: MutableList&lt;TreeNode?&gt;?, choice: TreeNode?): Boolean {\n return choice != null &amp;&amp; choice._val != 3\n}\n\n/* 상태 갱신 */\nfun makeChoice(state: MutableList&lt;TreeNode?&gt;, choice: TreeNode?) {\n state.add(choice)\n}\n\n/* 상태 복원 */\nfun undoChoice(state: MutableList&lt;TreeNode?&gt;, choice: TreeNode?) {\n state.removeLast()\n}\n\n/* 백트래킹 알고리즘: 예제 3 */\nfun backtrack(\n state: MutableList&lt;TreeNode?&gt;,\n choices: MutableList&lt;TreeNode?&gt;,\n res: MutableList&lt;MutableList&lt;TreeNode?&gt;?&gt;\n) {\n // 해인지 확인한다\n if (isSolution(state)) {\n // 해를 기록한다\n recordSolution(state, res)\n }\n // 모든 선택지를 순회한다\n for (choice in choices) {\n // 가지치기: 선택이 유효한지 확인한다\n if (isValid(state, choice)) {\n // 시도: 선택하고 상태를 갱신한다\n makeChoice(state, choice)\n // 다음 선택을 진행한다\n backtrack(state, mutableListOf(choice!!.left, choice.right), res)\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n undoChoice(state, choice)\n }\n }\n}\n</code></pre> preorder_traversal_iii_template.rb<pre><code>### 현재 상태가 해인지 판단 ###\ndef is_solution?(state)\n !state.empty? &amp;&amp; state.last.val == 7\nend\n\n### 해를 기록한다 ###\ndef record_solution(state, res)\n res &lt;&lt; state.dup\nend\n\n### 현재 상태에서 해당 선택이 유효한지 판단 ###\ndef is_valid?(state, choice)\n choice &amp;&amp; choice.val != 3\nend\n\n### 상태 갱신 ###\ndef make_choice(state, choice)\n state &lt;&lt; choice\nend\n\n### 상태 복원 ###\ndef undo_choice(state, choice)\n state.pop\nend\n\n### 백트래킹 알고리즘: 예제 3 ###\ndef backtrack(state, choices, res)\n # 해인지 확인한다\n record_solution(state, res) if is_solution?(state)\n\n # 모든 선택지를 순회한다\n for choice in choices\n # 가지치기: 선택이 유효한지 확인한다\n if is_valid?(state, choice)\n # 시도: 선택하고 상태를 갱신한다\n make_choice(state, choice)\n # 다음 선택을 진행한다\n backtrack(state, [choice.left, choice.right], res)\n # 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n undo_choice(state, choice)\n end\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>문제의 요구에 따르면 값이 \\(7\\)인 노드를 찾은 뒤에도 탐색을 계속해야 한다. 따라서 해를 기록한 뒤의 <code>return</code> 문을 삭제해야 한다. 아래 그림(13-4)은 <code>return</code> 문을 유지하거나 삭제했을 때의 탐색 과정을 비교한다.</p> <p></p> <p> 그림 13-4   return 유지 여부에 따른 탐색 과정 비교 </p> <p>전위 순회 기반 코드에 비해 백트래킹 기본 틀을 사용한 코드는 다소 장황하지만 범용성이 더 좋다. 실제로 많은 백트래킹 문제를 이 틀 안에서 해결할 수 있다. 구체적인 문제에 맞게 <code>state</code>와 <code>choices</code>를 정의하고, 틀 안의 각 메서드만 구현하면 된다.</p>","path":["제 13 장   백트래킹","13.1   백트래킹 알고리즘"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1314","level":2,"title":"13.1.4   주요 용어","text":"<p>알고리즘 문제를 더 명확히 분석하기 위해 백트래킹에서 자주 쓰는 용어를 정리하고, 예제 3에 대응하는 사례를 아래 표(13-1)에 나타냈다.</p> <p> 표 13-1   백트래킹 알고리즘의 주요 용어 </p> 용어 정의 예제 3 해(solution) 문제의 특정 조건을 만족하는 답으로, 하나 또는 여러 개일 수 있다 루트 노드에서 노드 \\(7\\)까지 제약 조건을 만족하는 모든 경로 제약 조건(constraint) 해의 유효성을 제한하는 조건으로, 보통 가지치기에 사용한다 경로에 노드 \\(3\\)이 포함되지 않는다 상태(state) 이미 내린 선택을 포함하여 특정 시점의 문제 상황을 나타낸다 현재까지 방문한 노드의 경로, 즉 노드 리스트 <code>path</code> 시도(attempt) 가능한 선택으로 해 공간을 탐색하는 과정으로, 선택, 상태 갱신, 해인지 확인하는 작업을 포함한다 왼쪽(오른쪽) 자식 노드를 재귀적으로 방문하고 <code>path</code>에 추가한 뒤 값이 \\(7\\)인지 확인한다 되돌리기(backtracking) 제약 조건을 만족하지 않는 상태를 만나면 이전 선택을 취소하고 직전 상태로 돌아간다 리프 노드를 지나거나 노드 방문을 마치거나 값이 \\(3\\)인 노드를 만나면 탐색을 멈추고 함수가 반환된다 가지치기(pruning) 문제의 특성과 제약 조건에 따라 의미 없는 탐색 경로를 피하여 효율을 높이는 방법이다 값이 \\(3\\)인 노드를 만나면 탐색을 더 진행하지 않는다 <p>참고</p> <p>문제, 해, 상태 등의 개념은 분할 정복, 백트래킹, 동적 계획법, 탐욕 알고리즘 등에서 공통으로 사용된다.</p>","path":["제 13 장   백트래킹","13.1   백트래킹 알고리즘"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1315","level":2,"title":"13.1.5   장점과 한계","text":"<p>백트래킹은 본질적으로 깊이 우선 탐색 알고리즘으로, 조건을 만족하는 해를 찾을 때까지 가능한 모든 해결 방법을 시도한다. 모든 가능한 해를 찾을 수 있으며, 적절히 가지치기하면 매우 효율적이라는 장점이 있다.</p> <p>하지만 규모가 크거나 복잡한 문제에서는 백트래킹의 실행 효율을 받아들이기 어려울 수 있다.</p> <ul> <li>시간: 상태 공간의 모든 가능성을 순회해야 하는 경우가 많아 시간 복잡도가 지수 또는 팩토리얼 수준에 이를 수 있다.</li> <li>공간: 재귀 호출 중 현재 상태(경로, 가지치기용 보조 변수 등)를 저장해야 하므로 깊이가 크면 공간 요구량도 매우 커질 수 있다.</li> </ul> <p>그렇더라도 일부 탐색 문제와 제약 만족 문제에서는 백트래킹이 여전히 최선의 해결 방법이다. 어떤 선택이 유효한 해를 만들지 예측할 수 없으므로 가능한 모든 선택을 순회해야 하기 때문이다. 이때는 효율을 어떻게 최적화하느냐가 핵심이며, 대표적인 방법은 두 가지다.</p> <ul> <li>가지치기: 해가 나올 수 없는 경로를 탐색하지 않아 시간과 공간을 절약한다.</li> <li>휴리스틱 탐색: 탐색에 전략이나 추정값을 도입하여 유효한 해가 나올 가능성이 높은 경로를 먼저 탐색한다.</li> </ul>","path":["제 13 장   백트래킹","13.1   백트래킹 알고리즘"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1316","level":2,"title":"13.1.6   대표적인 백트래킹 문제","text":"<p>백트래킹은 많은 탐색 문제, 제약 만족 문제, 조합 최적화 문제를 해결하는 데 사용할 수 있다.</p> <p>탐색 문제: 특정 조건을 만족하는 해결 방법을 찾는 것이 목표다.</p> <ul> <li>모든 순열 문제: 집합이 주어졌을 때 가능한 모든 배열 순서를 구한다.</li> <li>부분 집합 합 문제: 집합과 목표 합이 주어졌을 때 원소의 합이 목표 합인 모든 부분 집합을 찾는다.</li> <li>하노이의 탑 문제: 기둥 세 개와 크기가 다른 원판들이 주어졌을 때 모든 원판을 한 기둥에서 다른 기둥으로 옮긴다. 한 번에 하나만 옮길 수 있고 큰 원판을 작은 원판 위에 놓으면 안 된다.</li> </ul> <p>제약 만족 문제: 모든 제약 조건을 만족하는 해를 찾는 것이 목표다.</p> <ul> <li>\\(n\\)-퀸: \\(n \\times n\\) 체스판에 퀸 \\(n\\) 개를 서로 공격하지 않도록 놓는다.</li> <li>스도쿠: \\(9 \\times 9\\) 격자에 \\(1\\) ~ \\(9\\)를 넣되, 각 행과 열 및 각 \\(3 \\times 3\\) 부분 격자에서 숫자가 중복되지 않게 한다.</li> <li>그래프 색칠 문제: 무방향 그래프의 각 정점을 가능한 한 적은 색으로 칠하되, 인접한 정점은 서로 다른 색을 갖게 한다.</li> </ul> <p>조합 최적화 문제: 조합 공간에서 특정 조건을 만족하는 최적해를 찾는 것이 목표다.</p> <ul> <li>0-1 배낭 문제: 가치와 무게가 있는 물건들과 배낭이 주어졌을 때, 용량 제한 안에서 총가치가 최대가 되도록 물건을 고른다.</li> <li>외판원 문제: 그래프의 한 점에서 출발하여 다른 모든 점을 정확히 한 번 방문한 뒤 출발점으로 돌아오는 최단 경로를 구한다.</li> <li>최대 클리크 문제: 무방향 그래프에서 가장 큰 완전 부분 그래프, 즉 임의의 두 정점 사이에 간선이 있는 부분 그래프를 찾는다.</li> </ul> <p>많은 조합 최적화 문제에서 백트래킹이 최적의 해결 방법은 아니라는 점에 주의하자.</p> <ul> <li>0-1 배낭 문제는 더 높은 시간 효율을 위해 보통 동적 계획법으로 해결한다.</li> <li>외판원 문제는 유명한 NP-Hard 문제이며, 유전 알고리즘과 개미 군집 알고리즘 등이 자주 쓰인다.</li> <li>최대 클리크 문제는 그래프 이론의 대표적인 문제로, 탐욕 알고리즘 같은 휴리스틱 알고리즘을 사용할 수 있다.</li> </ul>","path":["제 13 장   백트래킹","13.1   백트래킹 알고리즘"],"tags":[]},{"location":"chapter_backtracking/exercises/","level":1,"title":"13.6   연습 문제","text":"","path":["제 13 장   백트래킹","13.6   연습 문제"],"tags":[]},{"location":"chapter_backtracking/exercises/#1361","level":2,"title":"13.6.1   개념 확인","text":"","path":["제 13 장   백트래킹","13.6   연습 문제"],"tags":[]},{"location":"chapter_backtracking/exercises/#1","level":3,"title":"1.   이 순열 알고리즘은 결과를 빠뜨리는가","text":"<p>한 백트래킹 알고리즘이 <code>1, 2, 3</code> 순서로 숫자를 시도하며 모든 순열을 생성한다. 숫자 <code>x</code>를 선택할 때마다 다음을 수행한다.</p> <ol> <li>현재 경로의 끝에 <code>x</code>를 추가한다.</li> <li><code>x</code>를 ‘사용함’으로 표시한다.</li> <li>재귀 호출로 다음 위치를 채운다.</li> </ol> <p>재귀 호출이 반환된 뒤, 학생은 경로 끝의 <code>x</code>만 삭제하고 다음 숫자를 계속 시도했다.</p> <ol> <li>알고리즘이 처음 얻는 순열은 무엇인가? 순열 6개를 모두 얻을 수 있는가?</li> <li>재귀 호출에서 이전 단계로 돌아가기 전에 경로 끝의 숫자를 삭제하는 것만으로 충분한가? 충분하지 않다면 무엇을 더 해야 하는지 이유와 함께 설명하라.</li> </ol> 참고 답안 <ol> <li> <p>처음 얻는 순열은 <code>[1, 2, 3]</code>이지만, 모든 순열을 얻을 수는 없다. 반환하면서 경로는 짧아져도 숫자 1, 2, 3은 여전히 모두 ‘사용함’으로 표시되어 있어 이후 분기에서 선택할 숫자가 없기 때문이다.</p> </li> <li> <p>충분하지 않다. 경로 끝의 <code>x</code>를 삭제한 뒤에는 <code>x</code>를 다시 ‘사용하지 않음’으로 표시해야 한다. 현재 경로와 사용 여부 표시가 함께 탐색 상태를 나타낸다. 선택할 때 두 곳을 수정했으므로 되돌아갈 때도 두 곳을 모두 복원해야 다른 분기에서 <code>x</code>를 다시 선택할 수 있다.</p> </li> </ol>","path":["제 13 장   백트래킹","13.6   연습 문제"],"tags":[]},{"location":"chapter_backtracking/exercises/#2","level":3,"title":"2.   숫자를 선택하는 순서가 중요한가","text":"<p>정렬된 배열 <code>[2, 3, 5]</code>와 목표 값 5가 주어지며, 각 숫자는 반복해서 선택할 수 있다. 알고리즘에서는 각 탐색 경로의 숫자가 작은 것부터 큰 것 순으로만 나타나도록 제한한다.</p> <ol> <li>서로 다른 어떤 조합을 얻을 수 있는가?</li> <li>같은 숫자들을 다른 순서로 반복해서 탐색할 필요가 없는 이유는 무엇인가? ‘작은 것부터 큰 것 순’이라는 제한은 어떤 역할을 하는가?</li> <li>현재 경로가 <code>[3]</code>이고 2가 더 필요한 상황에서 다음 후보 숫자가 3이다. 이때 이 단계의 나머지 후보를 모두 확인하지 않고 멈출 수 있는 이유는 무엇인가?</li> </ol> 참고 답안 <ol> <li> <p>서로 다른 조합은 <code>[2, 3]</code>과 <code>[5]</code>다.</p> </li> <li> <p>이 문제에서는 <code>[2, 3]</code>과 <code>[3, 2]</code>를 같은 조합으로 취급하므로 숫자를 선택하는 순서는 답에 반영하지 않는다. 경로의 숫자가 작은 것부터 큰 것 순으로 나타나도록 정하면 탐색 중에 <code>[3, 2]</code> 같은 중복 조합을 바로 건너뛸 수 있다.</p> </li> <li> <p>현재 2가 더 필요한데 후보 숫자 3은 이미 2보다 크다. 배열이 정렬되어 있으므로 3 뒤의 후보는 더 크기만 할 뿐이고, 모두 현재 조합에 추가할 수 없다. 따라서 이 단계의 확인을 바로 끝낼 수 있다.</p> </li> </ol>","path":["제 13 장   백트래킹","13.6   연습 문제"],"tags":[]},{"location":"chapter_backtracking/exercises/#3","level":3,"title":"3.   다음 퀸은 어디에 놓을 수 있는가","text":"<p><code>4 × 4</code> 체스판에 행별로 퀸을 놓으며, 행과 열의 인덱스는 모두 0부터 시작한다. 현재 <code>(0, 1)</code>과 <code>(1, 3)</code>에 퀸을 놓았고, 이제 인덱스 2인 행에 다음 퀸을 놓으려 한다.</p> <ol> <li>‘같은 열’이라는 이유로 제외되는 열은 어디인가?</li> <li>남은 열 가운데 ‘같은 대각선’이라는 이유로 제외되는 위치는 어디인가?</li> <li>인덱스 2인 행에서 시도해 볼 수 있는 위치는 어디인가?</li> </ol> 참고 답안 <ol> <li> <p>인덱스 1과 3인 열에 이미 퀸이 있으므로 <code>(2, 1)</code>과 <code>(2, 3)</code>은 제외된다.</p> </li> <li> <p>남은 위치 중 <code>(2, 2)</code>는 <code>(1, 3)</code>과 같은 대각선에 있으므로 역시 제외된다. <code>(2, 0)</code>은 이미 놓인 두 퀸 중 어느 것과도 같은 열이나 같은 대각선에 있지 않다.</p> </li> <li> <p>인덱스 2인 행에서 시도할 수 있는 위치는 <code>(2, 0)</code>뿐이다.</p> <p>이 단계에서는 현재 배치가 조건에 맞는다는 것만 확인한 셈이다. 이후 체스판을 완성할 수 없다면 여전히 되돌아가 더 앞 단계의 다른 선택을 시도해야 한다.</p> </li> </ol>","path":["제 13 장   백트래킹","13.6   연습 문제"],"tags":[]},{"location":"chapter_backtracking/exercises/#1362","level":2,"title":"13.6.2   프로그래밍 연습","text":"","path":["제 13 장   백트래킹","13.6   연습 문제"],"tags":[]},{"location":"chapter_backtracking/exercises/#1_1","level":3,"title":"1.   중복 원소가 없는 모든 순열","text":"<p>정수 배열 <code>nums</code>에는 원소가 적어도 하나 있으며, 모든 원소가 서로 다르다. 각 원소를 한 번씩 사용하여 만들 수 있는 모든 순서를 나열하고, 각 순서를 배열로 반환하라. 결과에서 순열들이 나오는 순서는 상관없다. 백트래킹을 사용하고, 각 위치의 원소가 현재 순열에 이미 선택되었는지 불리언 배열로 기록하라.</p> 풀이 힌트 <ol> <li>재귀 깊이는 순열의 몇 번째 위치를 채우는 중인지를 나타낸다.</li> <li>각 단계에서는 아직 사용하지 않은 원소만 시도한다.</li> <li>경로 길이가 <code>nums</code>의 길이에 도달하면 경로의 복사본을 답에 추가한다.</li> </ol> <p>LeetCode</p>","path":["제 13 장   백트래킹","13.6   연습 문제"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/","level":1,"title":"13.4   n-퀸 문제","text":"<p>문제</p> <p>체스 규칙에서 퀸은 같은 행, 열 또는 대각선에 있는 말을 공격할 수 있다. 퀸 \\(n\\) 개와 \\(n \\times n\\) 크기의 체스판이 주어졌을 때, 퀸들이 서로 공격할 수 없도록 배치하는 방법을 찾아라.</p> <p>아래 그림(13-15)과 같이 \\(n = 4\\)이면 해는 두 개다. 백트래킹 관점에서 \\(n \\times n\\) 체스판의 \\(n^2\\) 개 칸은 모든 선택지 <code>choices</code>를 제공한다. 퀸을 하나씩 놓는 동안 체스판은 계속 바뀌며, 각 시점의 체스판이 상태 <code>state</code>다.</p> <p></p> <p> 그림 13-15   4-퀸 문제의 해 </p> <p>아래 그림(13-16)은 세 가지 제약 조건을 보여 준다. 여러 퀸이 같은 행, 같은 열, 같은 대각선에 있으면 안 된다. 대각선은 주대각선 방향 <code>\\</code>과 부대각선 방향 <code>/</code>의 두 종류로 나뉜다.</p> <p></p> <p> 그림 13-16   n-퀸 문제의 제약 조건 </p>","path":["제 13 장   백트래킹","13.4   n-퀸 문제"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#1","level":3,"title":"1.   한 행씩 배치하는 전략","text":"<p>퀸 수와 체스판의 행 수가 모두 \\(n\\)이므로 각 행에는 퀸을 정확히 하나씩 놓아야 한다는 사실을 쉽게 도출할 수 있다.</p> <p>따라서 첫 행부터 마지막 행까지 매 행에 퀸 하나씩 놓는 전략을 사용할 수 있다.</p> <p>아래 그림(13-17)은 4-퀸 문제에서 한 행씩 배치하는 과정이다. 공간 제약 때문에 첫 행의 탐색 가지 중 하나만 펼쳤으며, 열과 대각선 제약을 만족하지 않는 배치는 모두 가지치기했다.</p> <p></p> <p> 그림 13-17   한 행씩 배치하는 전략 </p> <p>본질적으로 행별 배치 전략 자체가 가지치기 역할을 한다. 같은 행에 여러 퀸이 놓이는 모든 탐색 가지를 피하기 때문이다.</p>","path":["제 13 장   백트래킹","13.4   n-퀸 문제"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#2","level":3,"title":"2.   열과 대각선 가지치기","text":"<p>열 제약을 만족하기 위해 길이 \\(n\\)의 불리언 배열 <code>cols</code>에 각 열의 퀸 존재 여부를 기록할 수 있다. 배치를 결정하기 전에 <code>cols</code>로 이미 퀸이 있는 열을 가지치기하고, 백트래킹 중 <code>cols</code>의 상태를 동적으로 갱신한다.</p> <p>참고</p> <p>행렬의 시작점은 왼쪽 위이며, 행 인덱스는 아래로 갈수록, 열 인덱스는 오른쪽으로 갈수록 커진다.</p> <p>대각선 제약은 어떻게 처리할까? 한 칸의 행과 열 인덱스를 \\((row, col)\\)이라고 하자. 주대각선 방향의 대각선 하나를 고르면, 그 위의 모든 칸에서 행 인덱스와 열 인덱스의 차가 같다. 즉, 같은 주대각선 방향 대각선에서는 \\(row - col\\)이 일정하다.</p> <p>따라서 두 칸이 \\(row_1 - col_1 = row_2 - col_2\\)를 만족하면 반드시 같은 주대각선 방향 대각선에 있다. 이 규칙을 이용하여 아래 그림(13-18)의 배열 <code>diags1</code>에 각 대각선의 퀸 존재 여부를 기록할 수 있다.</p> <p>마찬가지로 같은 부대각선 방향 대각선의 모든 칸에서는 \\(row + col\\)이 일정하다. 배열 <code>diags2</code>를 이용하여 이 방향의 제약도 처리할 수 있다.</p> <p></p> <p> 그림 13-18   열 제약과 대각선 제약 처리 </p>","path":["제 13 장   백트래킹","13.4   n-퀸 문제"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#3","level":3,"title":"3.   코드 구현","text":"<p>\\(n\\) 차 정방행렬에서 \\(row - col\\)의 범위는 \\([-n + 1, n - 1]\\), \\(row + col\\)의 범위는 \\([0, 2n - 2]\\)이다. 따라서 두 방향의 대각선 수는 각각 \\(2n - 1\\) 개이며, 배열 <code>diags1</code>과 <code>diags2</code>의 길이도 모두 \\(2n - 1\\)이다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby n_queens.py<pre><code>def backtrack(\n row: int,\n n: int,\n state: list[list[str]],\n res: list[list[list[str]]],\n cols: list[bool],\n diags1: list[bool],\n diags2: list[bool],\n):\n \"\"\"백트래킹 알고리즘: n-퀸\"\"\"\n # 모든 행에 배치했으면 해를 기록한다\n if row == n:\n res.append([list(row) for row in state])\n return\n # 모든 열을 순회한다\n for col in range(n):\n # 해당 칸이 속한 주대각선과 부대각선을 계산한다\n diag1 = row - col + n - 1\n diag2 = row + col\n # 가지치기: 해당 칸이 속한 열, 주대각선, 부대각선에 퀸이 있으면 안 된다\n if not cols[col] and not diags1[diag1] and not diags2[diag2]:\n # 시도: 해당 칸에 퀸을 놓는다\n state[row][col] = \"Q\"\n cols[col] = diags1[diag1] = diags2[diag2] = True\n # 다음 행에 배치한다\n backtrack(row + 1, n, state, res, cols, diags1, diags2)\n # 되돌리기: 해당 칸을 빈칸으로 되돌린다\n state[row][col] = \"#\"\n cols[col] = diags1[diag1] = diags2[diag2] = False\n\ndef n_queens(n: int) -&gt; list[list[list[str]]]:\n \"\"\"n-퀸 문제 풀기\"\"\"\n # n*n 크기의 체스판을 초기화한다. 'Q'는 퀸, '#'은 빈칸을 나타낸다\n state = [[\"#\" for _ in range(n)] for _ in range(n)]\n cols = [False] * n # 열에 퀸이 있는지 기록한다\n diags1 = [False] * (2 * n - 1) # 주대각선에 퀸이 있는지 기록한다\n diags2 = [False] * (2 * n - 1) # 부대각선에 퀸이 있는지 기록한다\n res = []\n backtrack(0, n, state, res, cols, diags1, diags2)\n\n return res\n</code></pre> n_queens.cpp<pre><code>/* 백트래킹 알고리즘: n-퀸 */\nvoid backtrack(int row, int n, vector&lt;vector&lt;string&gt;&gt; &amp;state, vector&lt;vector&lt;vector&lt;string&gt;&gt;&gt; &amp;res, vector&lt;bool&gt; &amp;cols,\n vector&lt;bool&gt; &amp;diags1, vector&lt;bool&gt; &amp;diags2) {\n // 모든 행에 배치했으면 해를 기록한다\n if (row == n) {\n res.push_back(state);\n return;\n }\n // 모든 열을 순회한다\n for (int col = 0; col &lt; n; col++) {\n // 해당 칸이 속한 주대각선과 부대각선을 계산한다\n int diag1 = row - col + n - 1;\n int diag2 = row + col;\n // 가지치기: 해당 칸이 속한 열, 주대각선, 부대각선에 퀸이 있으면 안 된다\n if (!cols[col] &amp;&amp; !diags1[diag1] &amp;&amp; !diags2[diag2]) {\n // 시도: 해당 칸에 퀸을 놓는다\n state[row][col] = \"Q\";\n cols[col] = diags1[diag1] = diags2[diag2] = true;\n // 다음 행에 배치한다\n backtrack(row + 1, n, state, res, cols, diags1, diags2);\n // 되돌리기: 해당 칸을 빈칸으로 되돌린다\n state[row][col] = \"#\";\n cols[col] = diags1[diag1] = diags2[diag2] = false;\n }\n }\n}\n\n/* n-퀸 문제 풀기 */\nvector&lt;vector&lt;vector&lt;string&gt;&gt;&gt; nQueens(int n) {\n // n*n 크기의 체스판을 초기화한다. 'Q'는 퀸, '#'은 빈칸을 나타낸다\n vector&lt;vector&lt;string&gt;&gt; state(n, vector&lt;string&gt;(n, \"#\"));\n vector&lt;bool&gt; cols(n, false); // 열에 퀸이 있는지 기록한다\n vector&lt;bool&gt; diags1(2 * n - 1, false); // 주대각선에 퀸이 있는지 기록한다\n vector&lt;bool&gt; diags2(2 * n - 1, false); // 부대각선에 퀸이 있는지 기록한다\n vector&lt;vector&lt;vector&lt;string&gt;&gt;&gt; res;\n\n backtrack(0, n, state, res, cols, diags1, diags2);\n\n return res;\n}\n</code></pre> n_queens.java<pre><code>/* 백트래킹 알고리즘: n-퀸 */\nvoid backtrack(int row, int n, List&lt;List&lt;String&gt;&gt; state, List&lt;List&lt;List&lt;String&gt;&gt;&gt; res,\n boolean[] cols, boolean[] diags1, boolean[] diags2) {\n // 모든 행에 배치했으면 해를 기록한다\n if (row == n) {\n List&lt;List&lt;String&gt;&gt; copyState = new ArrayList&lt;&gt;();\n for (List&lt;String&gt; sRow : state) {\n copyState.add(new ArrayList&lt;&gt;(sRow));\n }\n res.add(copyState);\n return;\n }\n // 모든 열을 순회한다\n for (int col = 0; col &lt; n; col++) {\n // 해당 칸이 속한 주대각선과 부대각선을 계산한다\n int diag1 = row - col + n - 1;\n int diag2 = row + col;\n // 가지치기: 해당 칸이 속한 열, 주대각선, 부대각선에 퀸이 있으면 안 된다\n if (!cols[col] &amp;&amp; !diags1[diag1] &amp;&amp; !diags2[diag2]) {\n // 시도: 해당 칸에 퀸을 놓는다\n state.get(row).set(col, \"Q\");\n cols[col] = diags1[diag1] = diags2[diag2] = true;\n // 다음 행에 배치한다\n backtrack(row + 1, n, state, res, cols, diags1, diags2);\n // 되돌리기: 해당 칸을 빈칸으로 되돌린다\n state.get(row).set(col, \"#\");\n cols[col] = diags1[diag1] = diags2[diag2] = false;\n }\n }\n}\n\n/* n-퀸 문제 풀기 */\nList&lt;List&lt;List&lt;String&gt;&gt;&gt; nQueens(int n) {\n // n*n 크기의 체스판을 초기화한다. 'Q'는 퀸, '#'은 빈칸을 나타낸다\n List&lt;List&lt;String&gt;&gt; state = new ArrayList&lt;&gt;();\n for (int i = 0; i &lt; n; i++) {\n List&lt;String&gt; row = new ArrayList&lt;&gt;();\n for (int j = 0; j &lt; n; j++) {\n row.add(\"#\");\n }\n state.add(row);\n }\n boolean[] cols = new boolean[n]; // 열에 퀸이 있는지 기록한다\n boolean[] diags1 = new boolean[2 * n - 1]; // 주대각선에 퀸이 있는지 기록한다\n boolean[] diags2 = new boolean[2 * n - 1]; // 부대각선에 퀸이 있는지 기록한다\n List&lt;List&lt;List&lt;String&gt;&gt;&gt; res = new ArrayList&lt;&gt;();\n\n backtrack(0, n, state, res, cols, diags1, diags2);\n\n return res;\n}\n</code></pre> n_queens.cs<pre><code>/* 백트래킹 알고리즘: n-퀸 */\nvoid Backtrack(int row, int n, List&lt;List&lt;string&gt;&gt; state, List&lt;List&lt;List&lt;string&gt;&gt;&gt; res,\n bool[] cols, bool[] diags1, bool[] diags2) {\n // 모든 행에 배치했으면 해를 기록한다\n if (row == n) {\n List&lt;List&lt;string&gt;&gt; copyState = [];\n foreach (List&lt;string&gt; sRow in state) {\n copyState.Add(new List&lt;string&gt;(sRow));\n }\n res.Add(copyState);\n return;\n }\n // 모든 열을 순회한다\n for (int col = 0; col &lt; n; col++) {\n // 해당 칸이 속한 주대각선과 부대각선을 계산한다\n int diag1 = row - col + n - 1;\n int diag2 = row + col;\n // 가지치기: 해당 칸이 속한 열, 주대각선, 부대각선에 퀸이 있으면 안 된다\n if (!cols[col] &amp;&amp; !diags1[diag1] &amp;&amp; !diags2[diag2]) {\n // 시도: 해당 칸에 퀸을 놓는다\n state[row][col] = \"Q\";\n cols[col] = diags1[diag1] = diags2[diag2] = true;\n // 다음 행에 배치한다\n Backtrack(row + 1, n, state, res, cols, diags1, diags2);\n // 되돌리기: 해당 칸을 빈칸으로 되돌린다\n state[row][col] = \"#\";\n cols[col] = diags1[diag1] = diags2[diag2] = false;\n }\n }\n}\n\n/* n-퀸 문제 풀기 */\nList&lt;List&lt;List&lt;string&gt;&gt;&gt; NQueens(int n) {\n // n*n 크기의 체스판을 초기화한다. 'Q'는 퀸, '#'은 빈칸을 나타낸다\n List&lt;List&lt;string&gt;&gt; state = [];\n for (int i = 0; i &lt; n; i++) {\n List&lt;string&gt; row = [];\n for (int j = 0; j &lt; n; j++) {\n row.Add(\"#\");\n }\n state.Add(row);\n }\n bool[] cols = new bool[n]; // 열에 퀸이 있는지 기록한다\n bool[] diags1 = new bool[2 * n - 1]; // 주대각선에 퀸이 있는지 기록한다\n bool[] diags2 = new bool[2 * n - 1]; // 부대각선에 퀸이 있는지 기록한다\n List&lt;List&lt;List&lt;string&gt;&gt;&gt; res = [];\n\n Backtrack(0, n, state, res, cols, diags1, diags2);\n\n return res;\n}\n</code></pre> n_queens.go<pre><code>/* 백트래킹 알고리즘: n-퀸 */\nfunc backtrack(row, n int, state *[][]string, res *[][][]string, cols, diags1, diags2 *[]bool) {\n // 모든 행에 배치했으면 해를 기록한다\n if row == n {\n newState := make([][]string, len(*state))\n for i, _ := range newState {\n newState[i] = make([]string, len((*state)[0]))\n copy(newState[i], (*state)[i])\n\n }\n *res = append(*res, newState)\n return\n }\n // 모든 열을 순회한다\n for col := 0; col &lt; n; col++ {\n // 해당 칸이 속한 주대각선과 부대각선을 계산한다\n diag1 := row - col + n - 1\n diag2 := row + col\n // 가지치기: 해당 칸이 속한 열, 주대각선, 부대각선에 퀸이 있으면 안 된다\n if !(*cols)[col] &amp;&amp; !(*diags1)[diag1] &amp;&amp; !(*diags2)[diag2] {\n // 시도: 해당 칸에 퀸을 놓는다\n (*state)[row][col] = \"Q\"\n (*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = true, true, true\n // 다음 행에 배치한다\n backtrack(row+1, n, state, res, cols, diags1, diags2)\n // 되돌리기: 해당 칸을 빈칸으로 되돌린다\n (*state)[row][col] = \"#\"\n (*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = false, false, false\n }\n }\n}\n\n/* n-퀸 문제 풀기 */\nfunc nQueens(n int) [][][]string {\n // n*n 크기의 체스판을 초기화한다. 'Q'는 퀸, '#'은 빈칸을 나타낸다\n state := make([][]string, n)\n for i := 0; i &lt; n; i++ {\n row := make([]string, n)\n for i := 0; i &lt; n; i++ {\n row[i] = \"#\"\n }\n state[i] = row\n }\n // 열에 퀸이 있는지 기록한다\n cols := make([]bool, n)\n diags1 := make([]bool, 2*n-1)\n diags2 := make([]bool, 2*n-1)\n res := make([][][]string, 0)\n backtrack(0, n, &amp;state, &amp;res, &amp;cols, &amp;diags1, &amp;diags2)\n return res\n}\n</code></pre> n_queens.swift<pre><code>/* 백트래킹 알고리즘: n-퀸 */\nfunc backtrack(row: Int, n: Int, state: inout [[String]], res: inout [[[String]]], cols: inout [Bool], diags1: inout [Bool], diags2: inout [Bool]) {\n // 모든 행에 배치했으면 해를 기록한다\n if row == n {\n res.append(state)\n return\n }\n // 모든 열을 순회한다\n for col in 0 ..&lt; n {\n // 해당 칸이 속한 주대각선과 부대각선을 계산한다\n let diag1 = row - col + n - 1\n let diag2 = row + col\n // 가지치기: 해당 칸이 속한 열, 주대각선, 부대각선에 퀸이 있으면 안 된다\n if !cols[col] &amp;&amp; !diags1[diag1] &amp;&amp; !diags2[diag2] {\n // 시도: 해당 칸에 퀸을 놓는다\n state[row][col] = \"Q\"\n cols[col] = true\n diags1[diag1] = true\n diags2[diag2] = true\n // 다음 행에 배치한다\n backtrack(row: row + 1, n: n, state: &amp;state, res: &amp;res, cols: &amp;cols, diags1: &amp;diags1, diags2: &amp;diags2)\n // 되돌리기: 해당 칸을 빈칸으로 되돌린다\n state[row][col] = \"#\"\n cols[col] = false\n diags1[diag1] = false\n diags2[diag2] = false\n }\n }\n}\n\n/* n-퀸 문제 풀기 */\nfunc nQueens(n: Int) -&gt; [[[String]]] {\n // n*n 크기의 체스판을 초기화한다. 'Q'는 퀸, '#'은 빈칸을 나타낸다\n var state = Array(repeating: Array(repeating: \"#\", count: n), count: n)\n var cols = Array(repeating: false, count: n) // 열에 퀸이 있는지 기록한다\n var diags1 = Array(repeating: false, count: 2 * n - 1) // 주대각선에 퀸이 있는지 기록한다\n var diags2 = Array(repeating: false, count: 2 * n - 1) // 부대각선에 퀸이 있는지 기록한다\n var res: [[[String]]] = []\n\n backtrack(row: 0, n: n, state: &amp;state, res: &amp;res, cols: &amp;cols, diags1: &amp;diags1, diags2: &amp;diags2)\n\n return res\n}\n</code></pre> n_queens.js<pre><code>/* 백트래킹 알고리즘: n-퀸 */\nfunction backtrack(row, n, state, res, cols, diags1, diags2) {\n // 모든 행에 배치했으면 해를 기록한다\n if (row === n) {\n res.push(state.map((row) =&gt; row.slice()));\n return;\n }\n // 모든 열을 순회한다\n for (let col = 0; col &lt; n; col++) {\n // 해당 칸이 속한 주대각선과 부대각선을 계산한다\n const diag1 = row - col + n - 1;\n const diag2 = row + col;\n // 가지치기: 해당 칸이 속한 열, 주대각선, 부대각선에 퀸이 있으면 안 된다\n if (!cols[col] &amp;&amp; !diags1[diag1] &amp;&amp; !diags2[diag2]) {\n // 시도: 해당 칸에 퀸을 놓는다\n state[row][col] = 'Q';\n cols[col] = diags1[diag1] = diags2[diag2] = true;\n // 다음 행에 배치한다\n backtrack(row + 1, n, state, res, cols, diags1, diags2);\n // 되돌리기: 해당 칸을 빈칸으로 되돌린다\n state[row][col] = '#';\n cols[col] = diags1[diag1] = diags2[diag2] = false;\n }\n }\n}\n\n/* n-퀸 문제 풀기 */\nfunction nQueens(n) {\n // n*n 크기의 체스판을 초기화한다. 'Q'는 퀸, '#'은 빈칸을 나타낸다\n const state = Array.from({ length: n }, () =&gt; Array(n).fill('#'));\n const cols = Array(n).fill(false); // 열에 퀸이 있는지 기록한다\n const diags1 = Array(2 * n - 1).fill(false); // 주대각선에 퀸이 있는지 기록한다\n const diags2 = Array(2 * n - 1).fill(false); // 부대각선에 퀸이 있는지 기록한다\n const res = [];\n\n backtrack(0, n, state, res, cols, diags1, diags2);\n return res;\n}\n</code></pre> n_queens.ts<pre><code>/* 백트래킹 알고리즘: n-퀸 */\nfunction backtrack(\n row: number,\n n: number,\n state: string[][],\n res: string[][][],\n cols: boolean[],\n diags1: boolean[],\n diags2: boolean[]\n): void {\n // 모든 행에 배치했으면 해를 기록한다\n if (row === n) {\n res.push(state.map((row) =&gt; row.slice()));\n return;\n }\n // 모든 열을 순회한다\n for (let col = 0; col &lt; n; col++) {\n // 해당 칸이 속한 주대각선과 부대각선을 계산한다\n const diag1 = row - col + n - 1;\n const diag2 = row + col;\n // 가지치기: 해당 칸이 속한 열, 주대각선, 부대각선에 퀸이 있으면 안 된다\n if (!cols[col] &amp;&amp; !diags1[diag1] &amp;&amp; !diags2[diag2]) {\n // 시도: 해당 칸에 퀸을 놓는다\n state[row][col] = 'Q';\n cols[col] = diags1[diag1] = diags2[diag2] = true;\n // 다음 행에 배치한다\n backtrack(row + 1, n, state, res, cols, diags1, diags2);\n // 되돌리기: 해당 칸을 빈칸으로 되돌린다\n state[row][col] = '#';\n cols[col] = diags1[diag1] = diags2[diag2] = false;\n }\n }\n}\n\n/* n-퀸 문제 풀기 */\nfunction nQueens(n: number): string[][][] {\n // n*n 크기의 체스판을 초기화한다. 'Q'는 퀸, '#'은 빈칸을 나타낸다\n const state = Array.from({ length: n }, () =&gt; Array(n).fill('#'));\n const cols = Array(n).fill(false); // 열에 퀸이 있는지 기록한다\n const diags1 = Array(2 * n - 1).fill(false); // 주대각선에 퀸이 있는지 기록한다\n const diags2 = Array(2 * n - 1).fill(false); // 부대각선에 퀸이 있는지 기록한다\n const res: string[][][] = [];\n\n backtrack(0, n, state, res, cols, diags1, diags2);\n return res;\n}\n</code></pre> n_queens.dart<pre><code>/* 백트래킹 알고리즘: n-퀸 */\nvoid backtrack(\n int row,\n int n,\n List&lt;List&lt;String&gt;&gt; state,\n List&lt;List&lt;List&lt;String&gt;&gt;&gt; res,\n List&lt;bool&gt; cols,\n List&lt;bool&gt; diags1,\n List&lt;bool&gt; diags2,\n) {\n // 모든 행에 배치했으면 해를 기록한다\n if (row == n) {\n List&lt;List&lt;String&gt;&gt; copyState = [];\n for (List&lt;String&gt; sRow in state) {\n copyState.add(List.from(sRow));\n }\n res.add(copyState);\n return;\n }\n // 모든 열을 순회한다\n for (int col = 0; col &lt; n; col++) {\n // 해당 칸이 속한 주대각선과 부대각선을 계산한다\n int diag1 = row - col + n - 1;\n int diag2 = row + col;\n // 가지치기: 해당 칸이 속한 열, 주대각선, 부대각선에 퀸이 있으면 안 된다\n if (!cols[col] &amp;&amp; !diags1[diag1] &amp;&amp; !diags2[diag2]) {\n // 시도: 해당 칸에 퀸을 놓는다\n state[row][col] = \"Q\";\n cols[col] = true;\n diags1[diag1] = true;\n diags2[diag2] = true;\n // 다음 행에 배치한다\n backtrack(row + 1, n, state, res, cols, diags1, diags2);\n // 되돌리기: 해당 칸을 빈칸으로 되돌린다\n state[row][col] = \"#\";\n cols[col] = false;\n diags1[diag1] = false;\n diags2[diag2] = false;\n }\n }\n}\n\n/* n-퀸 문제 풀기 */\nList&lt;List&lt;List&lt;String&gt;&gt;&gt; nQueens(int n) {\n // n*n 크기의 체스판을 초기화한다. 'Q'는 퀸, '#'은 빈칸을 나타낸다\n List&lt;List&lt;String&gt;&gt; state = List.generate(n, (index) =&gt; List.filled(n, \"#\"));\n List&lt;bool&gt; cols = List.filled(n, false); // 열에 퀸이 있는지 기록한다\n List&lt;bool&gt; diags1 = List.filled(2 * n - 1, false); // 주대각선에 퀸이 있는지 기록한다\n List&lt;bool&gt; diags2 = List.filled(2 * n - 1, false); // 부대각선에 퀸이 있는지 기록한다\n List&lt;List&lt;List&lt;String&gt;&gt;&gt; res = [];\n\n backtrack(0, n, state, res, cols, diags1, diags2);\n\n return res;\n}\n</code></pre> n_queens.rs<pre><code>/* 백트래킹 알고리즘: n-퀸 */\nfn backtrack(\n row: usize,\n n: usize,\n state: &amp;mut Vec&lt;Vec&lt;String&gt;&gt;,\n res: &amp;mut Vec&lt;Vec&lt;Vec&lt;String&gt;&gt;&gt;,\n cols: &amp;mut [bool],\n diags1: &amp;mut [bool],\n diags2: &amp;mut [bool],\n) {\n // 모든 행에 배치했으면 해를 기록한다\n if row == n {\n res.push(state.clone());\n return;\n }\n // 모든 열을 순회한다\n for col in 0..n {\n // 해당 칸이 속한 주대각선과 부대각선을 계산한다\n let diag1 = row + n - 1 - col;\n let diag2 = row + col;\n // 가지치기: 해당 칸이 속한 열, 주대각선, 부대각선에 퀸이 있으면 안 된다\n if !cols[col] &amp;&amp; !diags1[diag1] &amp;&amp; !diags2[diag2] {\n // 시도: 해당 칸에 퀸을 놓는다\n state[row][col] = \"Q\".into();\n (cols[col], diags1[diag1], diags2[diag2]) = (true, true, true);\n // 다음 행에 배치한다\n backtrack(row + 1, n, state, res, cols, diags1, diags2);\n // 되돌리기: 해당 칸을 빈칸으로 되돌린다\n state[row][col] = \"#\".into();\n (cols[col], diags1[diag1], diags2[diag2]) = (false, false, false);\n }\n }\n}\n\n/* n-퀸 문제 풀기 */\nfn n_queens(n: usize) -&gt; Vec&lt;Vec&lt;Vec&lt;String&gt;&gt;&gt; {\n // n*n 크기의 체스판을 초기화한다. 'Q'는 퀸, '#'은 빈칸을 나타낸다\n let mut state: Vec&lt;Vec&lt;String&gt;&gt; = vec![vec![\"#\".to_string(); n]; n];\n let mut cols = vec![false; n]; // 열에 퀸이 있는지 기록한다\n let mut diags1 = vec![false; 2 * n - 1]; // 주대각선에 퀸이 있는지 기록한다\n let mut diags2 = vec![false; 2 * n - 1]; // 부대각선에 퀸이 있는지 기록한다\n let mut res: Vec&lt;Vec&lt;Vec&lt;String&gt;&gt;&gt; = Vec::new();\n\n backtrack(\n 0,\n n,\n &amp;mut state,\n &amp;mut res,\n &amp;mut cols,\n &amp;mut diags1,\n &amp;mut diags2,\n );\n\n res\n}\n</code></pre> n_queens.c<pre><code>/* 백트래킹 알고리즘: n-퀸 */\nvoid backtrack(int row, int n, char state[MAX_SIZE][MAX_SIZE], char ***res, int *resSize, bool cols[MAX_SIZE],\n bool diags1[2 * MAX_SIZE - 1], bool diags2[2 * MAX_SIZE - 1]) {\n // 모든 행에 배치했으면 해를 기록한다\n if (row == n) {\n res[*resSize] = (char **)malloc(sizeof(char *) * n);\n for (int i = 0; i &lt; n; ++i) {\n res[*resSize][i] = (char *)malloc(sizeof(char) * (n + 1));\n strcpy(res[*resSize][i], state[i]);\n }\n (*resSize)++;\n return;\n }\n // 모든 열을 순회한다\n for (int col = 0; col &lt; n; col++) {\n // 해당 칸이 속한 주대각선과 부대각선을 계산한다\n int diag1 = row - col + n - 1;\n int diag2 = row + col;\n // 가지치기: 해당 칸이 속한 열, 주대각선, 부대각선에 퀸이 있으면 안 된다\n if (!cols[col] &amp;&amp; !diags1[diag1] &amp;&amp; !diags2[diag2]) {\n // 시도: 해당 칸에 퀸을 놓는다\n state[row][col] = 'Q';\n cols[col] = diags1[diag1] = diags2[diag2] = true;\n // 다음 행에 배치한다\n backtrack(row + 1, n, state, res, resSize, cols, diags1, diags2);\n // 되돌리기: 해당 칸을 빈칸으로 되돌린다\n state[row][col] = '#';\n cols[col] = diags1[diag1] = diags2[diag2] = false;\n }\n }\n}\n\n/* n-퀸 문제 풀기 */\nchar ***nQueens(int n, int *returnSize) {\n char state[MAX_SIZE][MAX_SIZE];\n // n*n 크기의 체스판을 초기화한다. 'Q'는 퀸, '#'은 빈칸을 나타낸다\n for (int i = 0; i &lt; n; ++i) {\n for (int j = 0; j &lt; n; ++j) {\n state[i][j] = '#';\n }\n state[i][n] = '\\0';\n }\n bool cols[MAX_SIZE] = {false}; // 열에 퀸이 있는지 기록한다\n bool diags1[2 * MAX_SIZE - 1] = {false}; // 주대각선에 퀸이 있는지 기록한다\n bool diags2[2 * MAX_SIZE - 1] = {false}; // 부대각선에 퀸이 있는지 기록한다\n\n char ***res = (char ***)malloc(sizeof(char **) * MAX_SIZE);\n *returnSize = 0;\n backtrack(0, n, state, res, returnSize, cols, diags1, diags2);\n return res;\n}\n</code></pre> n_queens.kt<pre><code>/* 백트래킹 알고리즘: n-퀸 */\nfun backtrack(\n row: Int,\n n: Int,\n state: MutableList&lt;MutableList&lt;String&gt;&gt;,\n res: MutableList&lt;MutableList&lt;MutableList&lt;String&gt;&gt;?&gt;,\n cols: BooleanArray,\n diags1: BooleanArray,\n diags2: BooleanArray\n) {\n // 모든 행에 배치했으면 해를 기록한다\n if (row == n) {\n val copyState = mutableListOf&lt;MutableList&lt;String&gt;&gt;()\n for (sRow in state) {\n copyState.add(sRow.toMutableList())\n }\n res.add(copyState)\n return\n }\n // 모든 열을 순회한다\n for (col in 0..&lt;n) {\n // 해당 칸이 속한 주대각선과 부대각선을 계산한다\n val diag1 = row - col + n - 1\n val diag2 = row + col\n // 가지치기: 해당 칸이 속한 열, 주대각선, 부대각선에 퀸이 있으면 안 된다\n if (!cols[col] &amp;&amp; !diags1[diag1] &amp;&amp; !diags2[diag2]) {\n // 시도: 해당 칸에 퀸을 놓는다\n state[row][col] = \"Q\"\n diags2[diag2] = true\n diags1[diag1] = diags2[diag2]\n cols[col] = diags1[diag1]\n // 다음 행에 배치한다\n backtrack(row + 1, n, state, res, cols, diags1, diags2)\n // 되돌리기: 해당 칸을 빈칸으로 되돌린다\n state[row][col] = \"#\"\n diags2[diag2] = false\n diags1[diag1] = diags2[diag2]\n cols[col] = diags1[diag1]\n }\n }\n}\n\n/* n-퀸 문제 풀기 */\nfun nQueens(n: Int): MutableList&lt;MutableList&lt;MutableList&lt;String&gt;&gt;?&gt; {\n // n*n 크기의 체스판을 초기화한다. 'Q'는 퀸, '#'은 빈칸을 나타낸다\n val state = mutableListOf&lt;MutableList&lt;String&gt;&gt;()\n for (i in 0..&lt;n) {\n val row = mutableListOf&lt;String&gt;()\n for (j in 0..&lt;n) {\n row.add(\"#\")\n }\n state.add(row)\n }\n val cols = BooleanArray(n) // 열에 퀸이 있는지 기록한다\n val diags1 = BooleanArray(2 * n - 1) // 주대각선에 퀸이 있는지 기록한다\n val diags2 = BooleanArray(2 * n - 1) // 부대각선에 퀸이 있는지 기록한다\n val res = mutableListOf&lt;MutableList&lt;MutableList&lt;String&gt;&gt;?&gt;()\n\n backtrack(0, n, state, res, cols, diags1, diags2)\n\n return res\n}\n</code></pre> n_queens.rb<pre><code>### 백트래킹 알고리즘: n-퀸 ###\ndef backtrack(row, n, state, res, cols, diags1, diags2)\n # 모든 행에 배치했으면 해를 기록한다\n if row == n\n res &lt;&lt; state.map { |row| row.dup }\n return\n end\n\n # 모든 열을 순회한다\n for col in 0...n\n # 해당 칸이 속한 주대각선과 부대각선을 계산한다\n diag1 = row - col + n - 1\n diag2 = row + col\n # 가지치기: 해당 칸이 속한 열, 주대각선, 부대각선에 퀸이 있으면 안 된다\n if !cols[col] &amp;&amp; !diags1[diag1] &amp;&amp; !diags2[diag2]\n # 시도: 해당 칸에 퀸을 놓는다\n state[row][col] = \"Q\"\n cols[col] = diags1[diag1] = diags2[diag2] = true\n # 다음 행에 배치한다\n backtrack(row + 1, n, state, res, cols, diags1, diags2)\n # 되돌리기: 해당 칸을 빈칸으로 되돌린다\n state[row][col] = \"#\"\n cols[col] = diags1[diag1] = diags2[diag2] = false\n end\n end\nend\n\n### n-퀸 문제 풀기 ###\ndef n_queens(n)\n # n*n 크기의 체스판을 초기화한다. 'Q'는 퀸, '#'은 빈칸을 나타낸다\n state = Array.new(n) { Array.new(n, \"#\") }\n cols = Array.new(n, false) # 열에 퀸이 있는지 기록한다\n diags1 = Array.new(2 * n - 1, false) # 주대각선에 퀸이 있는지 기록한다\n diags2 = Array.new(2 * n - 1, false) # 부대각선에 퀸이 있는지 기록한다\n res = []\n backtrack(0, n, state, res, cols, diags1, diags2)\n\n res\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>행별로 \\(n\\) 번 배치할 때 열 제약을 고려하면 첫 행부터 마지막 행까지 선택지 수가 \\(n\\), \\(n-1\\), \\(\\dots\\), \\(2\\), \\(1\\)이므로 \\(O(n!)\\) 시간이 걸린다. 해를 기록할 때 행렬 <code>state</code>를 복사하여 <code>res</code>에 추가해야 하며, 복사에는 \\(O(n^2)\\) 시간이 든다. 따라서 전체 시간 복잡도는 \\(O(n! \\cdot n^2)\\)이다. 실제로는 대각선 제약에 따른 가지치기도 탐색 공간을 크게 줄이므로 위 복잡도보다 탐색 효율이 좋은 경우가 많다.</p> <p>배열 <code>state</code>는 \\(O(n^2)\\) 공간을, <code>cols</code>, <code>diags1</code>, <code>diags2</code>는 각각 \\(O(n)\\) 공간을 사용한다. 최대 재귀 깊이가 \\(n\\)이므로 스택 프레임 공간은 \\(O(n)\\)이다. 따라서 공간 복잡도는 \\(O(n^2)\\)이다.</p>","path":["제 13 장   백트래킹","13.4   n-퀸 문제"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/","level":1,"title":"13.2   모든 순열 문제","text":"<p>모든 순열 문제는 백트래킹 알고리즘의 대표적인 응용이다. 배열이나 문자열 같은 집합이 주어졌을 때 원소들의 가능한 모든 순열을 찾는 문제로 정의된다.</p> <p>아래 표(13-2)에는 입력 배열과 그에 대응하는 모든 순열의 예시가 나와 있다.</p> <p> 표 13-2   모든 순열의 예시 </p> 입력 배열 모든 순열 \\([1]\\) \\([1]\\) \\([1, 2]\\) \\([1, 2], [2, 1]\\) \\([1, 2, 3]\\) \\([1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]\\)","path":["제 13 장   백트래킹","13.2   모든 순열 문제"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1321","level":2,"title":"13.2.1   값이 같은 원소가 없는 경우","text":"<p>문제</p> <p>중복 원소가 없는 정수 배열을 입력받아 가능한 모든 순열을 반환하라.</p> <p>백트래킹 관점에서는 순열을 만드는 과정을 연속된 선택의 결과로 볼 수 있다. 입력 배열이 \\([1, 2, 3]\\)이고 \\(1\\), \\(3\\), \\(2\\) 순서로 선택하면 순열 \\([1, 3, 2]\\)를 얻는다. 되돌리기는 선택 하나를 취소한 뒤 다른 선택을 계속 시도한다는 뜻이다.</p> <p>백트래킹 코드에서 후보 집합 <code>choices</code>는 입력 배열의 모든 원소이고, 상태 <code>state</code>는 지금까지 선택한 원소들이다. 각 원소는 한 번만 선택할 수 있으므로 <code>state</code> 안의 원소는 모두 중복되지 않아야 한다.</p> <p>아래 그림(13-5)과 같이 탐색 과정을 재귀 트리로 펼칠 수 있다. 각 노드는 현재 상태 <code>state</code>를 나타낸다. 루트에서 시작해 세 차례 선택하면 리프에 도달하며, 각 리프 노드는 순열 하나에 대응한다.</p> <p></p> <p> 그림 13-5   모든 순열의 재귀 트리 </p>","path":["제 13 장   백트래킹","13.2   모든 순열 문제"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1","level":3,"title":"1.   중복 선택 가지치기","text":"<p>각 원소를 한 번만 선택하도록 불리언 배열 <code>selected</code>를 도입하자. <code>selected[i]</code>는 <code>choices[i]</code>를 이미 선택했는지 나타내며, 이를 바탕으로 다음과 같이 가지치기한다.</p> <ul> <li><code>choices[i]</code>를 선택하면 <code>selected[i]</code>를 \\(\\text{True}\\)로 설정하여 이미 선택했음을 표시한다.</li> <li>선택지 리스트 <code>choices</code>를 순회할 때 이미 선택한 모든 노드를 건너뛰어 가지치기한다.</li> </ul> <p>아래 그림(13-6)처럼 첫 번째에 1, 두 번째에 3, 세 번째에 2를 선택한다면, 두 번째에서는 원소 1의 가지를, 세 번째에서는 원소 1과 3의 가지를 잘라 내야 한다.</p> <p></p> <p> 그림 13-6   모든 순열의 가지치기 예시 </p> <p>위 그림(13-6)에서 이 가지치기가 탐색 공간의 크기를 \\(O(n^n)\\)에서 \\(O(n!)\\)로 줄이는 것을 볼 수 있다.</p>","path":["제 13 장   백트래킹","13.2   모든 순열 문제"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#2","level":3,"title":"2.   코드 구현","text":"<p>위 내용을 정리했으니 기본 틀 코드의 '빈칸을 채울' 수 있다. 전체 코드를 짧게 만들기 위해 각 함수를 따로 구현하지 않고 <code>backtrack()</code> 함수 안에 직접 작성한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby permutations_i.py<pre><code>def backtrack(\n state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]\n):\n \"\"\"백트래킹 알고리즘: 모든 순열 I\"\"\"\n # 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if len(state) == len(choices):\n res.append(list(state))\n return\n # 모든 선택지를 순회한다\n for i, choice in enumerate(choices):\n # 가지치기: 원소를 중복하여 선택할 수 없다\n if not selected[i]:\n # 시도: 선택하고 상태를 갱신한다\n selected[i] = True\n state.append(choice)\n # 다음 선택을 진행한다\n backtrack(state, choices, selected, res)\n # 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n selected[i] = False\n state.pop()\n\ndef permutations_i(nums: list[int]) -&gt; list[list[int]]:\n \"\"\"모든 순열 I\"\"\"\n res = []\n backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)\n return res\n</code></pre> permutations_i.cpp<pre><code>/* 백트래킹 알고리즘: 모든 순열 I */\nvoid backtrack(vector&lt;int&gt; &amp;state, const vector&lt;int&gt; &amp;choices, vector&lt;bool&gt; &amp;selected, vector&lt;vector&lt;int&gt;&gt; &amp;res) {\n // 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if (state.size() == choices.size()) {\n res.push_back(state);\n return;\n }\n // 모든 선택지를 순회한다\n for (int i = 0; i &lt; choices.size(); i++) {\n int choice = choices[i];\n // 가지치기: 원소를 중복하여 선택할 수 없다\n if (!selected[i]) {\n // 시도: 선택하고 상태를 갱신한다\n selected[i] = true;\n state.push_back(choice);\n // 다음 선택을 진행한다\n backtrack(state, choices, selected, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n selected[i] = false;\n state.pop_back();\n }\n }\n}\n\n/* 모든 순열 I */\nvector&lt;vector&lt;int&gt;&gt; permutationsI(vector&lt;int&gt; nums) {\n vector&lt;int&gt; state;\n vector&lt;bool&gt; selected(nums.size(), false);\n vector&lt;vector&lt;int&gt;&gt; res;\n backtrack(state, nums, selected, res);\n return res;\n}\n</code></pre> permutations_i.java<pre><code>/* 백트래킹 알고리즘: 모든 순열 I */\nvoid backtrack(List&lt;Integer&gt; state, int[] choices, boolean[] selected, List&lt;List&lt;Integer&gt;&gt; res) {\n // 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if (state.size() == choices.length) {\n res.add(new ArrayList&lt;Integer&gt;(state));\n return;\n }\n // 모든 선택지를 순회한다\n for (int i = 0; i &lt; choices.length; i++) {\n int choice = choices[i];\n // 가지치기: 원소를 중복하여 선택할 수 없다\n if (!selected[i]) {\n // 시도: 선택하고 상태를 갱신한다\n selected[i] = true;\n state.add(choice);\n // 다음 선택을 진행한다\n backtrack(state, choices, selected, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n selected[i] = false;\n state.remove(state.size() - 1);\n }\n }\n}\n\n/* 모든 순열 I */\nList&lt;List&lt;Integer&gt;&gt; permutationsI(int[] nums) {\n List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;List&lt;Integer&gt;&gt;();\n backtrack(new ArrayList&lt;Integer&gt;(), nums, new boolean[nums.length], res);\n return res;\n}\n</code></pre> permutations_i.cs<pre><code>/* 백트래킹 알고리즘: 모든 순열 I */\nvoid Backtrack(List&lt;int&gt; state, int[] choices, bool[] selected, List&lt;List&lt;int&gt;&gt; res) {\n // 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if (state.Count == choices.Length) {\n res.Add(new List&lt;int&gt;(state));\n return;\n }\n // 모든 선택지를 순회한다\n for (int i = 0; i &lt; choices.Length; i++) {\n int choice = choices[i];\n // 가지치기: 원소를 중복하여 선택할 수 없다\n if (!selected[i]) {\n // 시도: 선택하고 상태를 갱신한다\n selected[i] = true;\n state.Add(choice);\n // 다음 선택을 진행한다\n Backtrack(state, choices, selected, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n selected[i] = false;\n state.RemoveAt(state.Count - 1);\n }\n }\n}\n\n/* 모든 순열 I */\nList&lt;List&lt;int&gt;&gt; PermutationsI(int[] nums) {\n List&lt;List&lt;int&gt;&gt; res = [];\n Backtrack([], nums, new bool[nums.Length], res);\n return res;\n}\n</code></pre> permutations_i.go<pre><code>/* 백트래킹 알고리즘: 모든 순열 I */\nfunc backtrackI(state *[]int, choices *[]int, selected *[]bool, res *[][]int) {\n // 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if len(*state) == len(*choices) {\n newState := append([]int{}, *state...)\n *res = append(*res, newState)\n }\n // 모든 선택지를 순회한다\n for i := 0; i &lt; len(*choices); i++ {\n choice := (*choices)[i]\n // 가지치기: 원소를 중복하여 선택할 수 없다\n if !(*selected)[i] {\n // 시도: 선택하고 상태를 갱신한다\n (*selected)[i] = true\n *state = append(*state, choice)\n // 다음 선택을 진행한다\n backtrackI(state, choices, selected, res)\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n (*selected)[i] = false\n *state = (*state)[:len(*state)-1]\n }\n }\n}\n\n/* 모든 순열 I */\nfunc permutationsI(nums []int) [][]int {\n res := make([][]int, 0)\n state := make([]int, 0)\n selected := make([]bool, len(nums))\n backtrackI(&amp;state, &amp;nums, &amp;selected, &amp;res)\n return res\n}\n</code></pre> permutations_i.swift<pre><code>/* 백트래킹 알고리즘: 모든 순열 I */\nfunc backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) {\n // 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if state.count == choices.count {\n res.append(state)\n return\n }\n // 모든 선택지를 순회한다\n for (i, choice) in choices.enumerated() {\n // 가지치기: 원소를 중복하여 선택할 수 없다\n if !selected[i] {\n // 시도: 선택하고 상태를 갱신한다\n selected[i] = true\n state.append(choice)\n // 다음 선택을 진행한다\n backtrack(state: &amp;state, choices: choices, selected: &amp;selected, res: &amp;res)\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n selected[i] = false\n state.removeLast()\n }\n }\n}\n\n/* 모든 순열 I */\nfunc permutationsI(nums: [Int]) -&gt; [[Int]] {\n var state: [Int] = []\n var selected = Array(repeating: false, count: nums.count)\n var res: [[Int]] = []\n backtrack(state: &amp;state, choices: nums, selected: &amp;selected, res: &amp;res)\n return res\n}\n</code></pre> permutations_i.js<pre><code>/* 백트래킹 알고리즘: 모든 순열 I */\nfunction backtrack(state, choices, selected, res) {\n // 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if (state.length === choices.length) {\n res.push([...state]);\n return;\n }\n // 모든 선택지를 순회한다\n choices.forEach((choice, i) =&gt; {\n // 가지치기: 원소를 중복하여 선택할 수 없다\n if (!selected[i]) {\n // 시도: 선택하고 상태를 갱신한다\n selected[i] = true;\n state.push(choice);\n // 다음 선택을 진행한다\n backtrack(state, choices, selected, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n selected[i] = false;\n state.pop();\n }\n });\n}\n\n/* 모든 순열 I */\nfunction permutationsI(nums) {\n const res = [];\n backtrack([], nums, Array(nums.length).fill(false), res);\n return res;\n}\n</code></pre> permutations_i.ts<pre><code>/* 백트래킹 알고리즘: 모든 순열 I */\nfunction backtrack(\n state: number[],\n choices: number[],\n selected: boolean[],\n res: number[][]\n): void {\n // 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if (state.length === choices.length) {\n res.push([...state]);\n return;\n }\n // 모든 선택지를 순회한다\n choices.forEach((choice, i) =&gt; {\n // 가지치기: 원소를 중복하여 선택할 수 없다\n if (!selected[i]) {\n // 시도: 선택하고 상태를 갱신한다\n selected[i] = true;\n state.push(choice);\n // 다음 선택을 진행한다\n backtrack(state, choices, selected, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n selected[i] = false;\n state.pop();\n }\n });\n}\n\n/* 모든 순열 I */\nfunction permutationsI(nums: number[]): number[][] {\n const res: number[][] = [];\n backtrack([], nums, Array(nums.length).fill(false), res);\n return res;\n}\n</code></pre> permutations_i.dart<pre><code>/* 백트래킹 알고리즘: 모든 순열 I */\nvoid backtrack(\n List&lt;int&gt; state,\n List&lt;int&gt; choices,\n List&lt;bool&gt; selected,\n List&lt;List&lt;int&gt;&gt; res,\n) {\n // 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if (state.length == choices.length) {\n res.add(List.from(state));\n return;\n }\n // 모든 선택지를 순회한다\n for (int i = 0; i &lt; choices.length; i++) {\n int choice = choices[i];\n // 가지치기: 원소를 중복하여 선택할 수 없다\n if (!selected[i]) {\n // 시도: 선택하고 상태를 갱신한다\n selected[i] = true;\n state.add(choice);\n // 다음 선택을 진행한다\n backtrack(state, choices, selected, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n selected[i] = false;\n state.removeLast();\n }\n }\n}\n\n/* 모든 순열 I */\nList&lt;List&lt;int&gt;&gt; permutationsI(List&lt;int&gt; nums) {\n List&lt;List&lt;int&gt;&gt; res = [];\n backtrack([], nums, List.filled(nums.length, false), res);\n return res;\n}\n</code></pre> permutations_i.rs<pre><code>/* 백트래킹 알고리즘: 모든 순열 I */\nfn backtrack(mut state: Vec&lt;i32&gt;, choices: &amp;[i32], selected: &amp;mut [bool], res: &amp;mut Vec&lt;Vec&lt;i32&gt;&gt;) {\n // 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if state.len() == choices.len() {\n res.push(state);\n return;\n }\n // 모든 선택지를 순회한다\n for i in 0..choices.len() {\n let choice = choices[i];\n // 가지치기: 원소를 중복하여 선택할 수 없다\n if !selected[i] {\n // 시도: 선택하고 상태를 갱신한다\n selected[i] = true;\n state.push(choice);\n // 다음 선택을 진행한다\n backtrack(state.clone(), choices, selected, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n selected[i] = false;\n state.pop();\n }\n }\n}\n\n/* 모든 순열 I */\nfn permutations_i(nums: &amp;mut [i32]) -&gt; Vec&lt;Vec&lt;i32&gt;&gt; {\n let mut res = Vec::new(); // 상태(부분 집합)\n backtrack(Vec::new(), nums, &amp;mut vec![false; nums.len()], &amp;mut res);\n res\n}\n</code></pre> permutations_i.c<pre><code>/* 백트래킹 알고리즘: 모든 순열 I */\nvoid backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) {\n // 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if (stateSize == choicesSize) {\n res[*resSize] = (int *)malloc(choicesSize * sizeof(int));\n for (int i = 0; i &lt; choicesSize; i++) {\n res[*resSize][i] = state[i];\n }\n (*resSize)++;\n return;\n }\n // 모든 선택지를 순회한다\n for (int i = 0; i &lt; choicesSize; i++) {\n int choice = choices[i];\n // 가지치기: 원소를 중복하여 선택할 수 없다\n if (!selected[i]) {\n // 시도: 선택하고 상태를 갱신한다\n selected[i] = true;\n state[stateSize] = choice;\n // 다음 선택을 진행한다\n backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n selected[i] = false;\n }\n }\n}\n\n/* 모든 순열 I */\nint **permutationsI(int *nums, int numsSize, int *returnSize) {\n int *state = (int *)malloc(numsSize * sizeof(int));\n bool *selected = (bool *)malloc(numsSize * sizeof(bool));\n for (int i = 0; i &lt; numsSize; i++) {\n selected[i] = false;\n }\n int **res = (int **)malloc(MAX_SIZE * sizeof(int *));\n *returnSize = 0;\n\n backtrack(state, 0, nums, numsSize, selected, res, returnSize);\n\n free(state);\n free(selected);\n\n return res;\n}\n</code></pre> permutations_i.kt<pre><code>/* 백트래킹 알고리즘: 모든 순열 I */\nfun backtrack(\n state: MutableList&lt;Int&gt;,\n choices: IntArray,\n selected: BooleanArray,\n res: MutableList&lt;MutableList&lt;Int&gt;?&gt;\n) {\n // 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if (state.size == choices.size) {\n res.add(state.toMutableList())\n return\n }\n // 모든 선택지를 순회한다\n for (i in choices.indices) {\n val choice = choices[i]\n // 가지치기: 원소를 중복하여 선택할 수 없다\n if (!selected[i]) {\n // 시도: 선택하고 상태를 갱신한다\n selected[i] = true\n state.add(choice)\n // 다음 선택을 진행한다\n backtrack(state, choices, selected, res)\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n selected[i] = false\n state.removeAt(state.size - 1)\n }\n }\n}\n\n/* 모든 순열 I */\nfun permutationsI(nums: IntArray): MutableList&lt;MutableList&lt;Int&gt;?&gt; {\n val res = mutableListOf&lt;MutableList&lt;Int&gt;?&gt;()\n backtrack(mutableListOf(), nums, BooleanArray(nums.size), res)\n return res\n}\n</code></pre> permutations_i.rb<pre><code>### 백트래킹 알고리즘: 모든 순열 I ###\ndef backtrack(state, choices, selected, res)\n # 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if state.length == choices.length\n res &lt;&lt; state.dup\n return\n end\n\n # 모든 선택지를 순회한다\n choices.each_with_index do |choice, i|\n # 가지치기: 원소를 중복하여 선택할 수 없다\n unless selected[i]\n # 시도: 선택하고 상태를 갱신한다\n selected[i] = true\n state &lt;&lt; choice\n # 다음 선택을 진행한다\n backtrack(state, choices, selected, res)\n # 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n selected[i] = false\n state.pop\n end\n end\nend\n\n### 모든 순열 I ###\ndef permutations_i(nums)\n res = []\n backtrack([], nums, Array.new(nums.length, false), res)\n res\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 13 장   백트래킹","13.2   모든 순열 문제"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1322","level":2,"title":"13.2.2   값이 같은 원소가 있는 경우","text":"<p>문제</p> <p>중복 원소가 있을 수 있는 정수 배열을 입력받아 중복되지 않는 모든 순열을 반환하라.</p> <p>입력 배열이 \\([1, 1, 2]\\)라고 하자. 두 원소 \\(1\\)을 쉽게 구분하기 위해 두 번째 \\(1\\)을 \\(\\hat{1}\\)로 표시한다.</p> <p>아래 그림(13-7)과 같이 앞의 방법으로 만든 순열의 절반은 중복된다.</p> <p></p> <p> 그림 13-7   중복된 순열 </p> <p>중복된 순열은 어떻게 제거할까? 가장 직접적으로는 해시 집합을 이용해 결과에서 중복을 제거할 수 있다. 하지만 이 방법은 충분히 간결하지 않다. 중복된 순열을 만드는 탐색 가지는 불필요하므로 미리 알아내어 잘라 내야 하며, 그러면 알고리즘의 효율을 더 높일 수 있다.</p>","path":["제 13 장   백트래킹","13.2   모든 순열 문제"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1_1","level":3,"title":"1.   같은 값의 원소 가지치기","text":"<p>아래 그림(13-8)에서 첫 번째 선택으로 \\(1\\)을 고르든 \\(\\hat{1}\\)을 고르든 같다. 두 선택 아래에서 만들어지는 모든 순열이 중복되므로 \\(\\hat{1}\\)을 가지치기해야 한다.</p> <p>마찬가지로 첫 번째에 \\(2\\)를 선택한 뒤에도 두 번째의 \\(1\\)과 \\(\\hat{1}\\)이 중복 가지를 만들므로 두 번째의 \\(\\hat{1}\\)도 잘라 내야 한다.</p> <p>본질적으로 한 차례의 선택에서 값이 같은 여러 원소를 한 번만 선택하도록 보장하는 것이 목표다.</p> <p></p> <p> 그림 13-8   중복된 순열 가지치기 </p>","path":["제 13 장   백트래킹","13.2   모든 순열 문제"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#2_1","level":3,"title":"2.   코드 구현","text":"<p>앞 문제의 코드를 바탕으로 매 차례의 선택에서 해시 집합 <code>duplicated</code>를 만들어 이미 시도한 원소를 기록하고 중복 원소를 가지치기한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby permutations_ii.py<pre><code>def backtrack(\n state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]\n):\n \"\"\"백트래킹 알고리즘: 모든 순열 II\"\"\"\n # 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if len(state) == len(choices):\n res.append(list(state))\n return\n # 모든 선택지를 순회한다\n duplicated = set[int]()\n for i, choice in enumerate(choices):\n # 가지치기: 원소를 중복하여 선택할 수 없으며, 값이 같은 원소도 중복하여 선택할 수 없다\n if not selected[i] and choice not in duplicated:\n # 시도: 선택하고 상태를 갱신한다\n duplicated.add(choice) # 선택한 원소의 값을 기록한다\n selected[i] = True\n state.append(choice)\n # 다음 선택을 진행한다\n backtrack(state, choices, selected, res)\n # 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n selected[i] = False\n state.pop()\n\ndef permutations_ii(nums: list[int]) -&gt; list[list[int]]:\n \"\"\"모든 순열 II\"\"\"\n res = []\n backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)\n return res\n</code></pre> permutations_ii.cpp<pre><code>/* 백트래킹 알고리즘: 모든 순열 II */\nvoid backtrack(vector&lt;int&gt; &amp;state, const vector&lt;int&gt; &amp;choices, vector&lt;bool&gt; &amp;selected, vector&lt;vector&lt;int&gt;&gt; &amp;res) {\n // 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if (state.size() == choices.size()) {\n res.push_back(state);\n return;\n }\n // 모든 선택지를 순회한다\n unordered_set&lt;int&gt; duplicated;\n for (int i = 0; i &lt; choices.size(); i++) {\n int choice = choices[i];\n // 가지치기: 원소를 중복하여 선택할 수 없으며, 값이 같은 원소도 중복하여 선택할 수 없다\n if (!selected[i] &amp;&amp; duplicated.find(choice) == duplicated.end()) {\n // 시도: 선택하고 상태를 갱신한다\n duplicated.emplace(choice); // 선택한 원소의 값을 기록한다\n selected[i] = true;\n state.push_back(choice);\n // 다음 선택을 진행한다\n backtrack(state, choices, selected, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n selected[i] = false;\n state.pop_back();\n }\n }\n}\n\n/* 모든 순열 II */\nvector&lt;vector&lt;int&gt;&gt; permutationsII(vector&lt;int&gt; nums) {\n vector&lt;int&gt; state;\n vector&lt;bool&gt; selected(nums.size(), false);\n vector&lt;vector&lt;int&gt;&gt; res;\n backtrack(state, nums, selected, res);\n return res;\n}\n</code></pre> permutations_ii.java<pre><code>/* 백트래킹 알고리즘: 모든 순열 II */\nvoid backtrack(List&lt;Integer&gt; state, int[] choices, boolean[] selected, List&lt;List&lt;Integer&gt;&gt; res) {\n // 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if (state.size() == choices.length) {\n res.add(new ArrayList&lt;Integer&gt;(state));\n return;\n }\n // 모든 선택지를 순회한다\n Set&lt;Integer&gt; duplicated = new HashSet&lt;Integer&gt;();\n for (int i = 0; i &lt; choices.length; i++) {\n int choice = choices[i];\n // 가지치기: 원소를 중복하여 선택할 수 없으며, 값이 같은 원소도 중복하여 선택할 수 없다\n if (!selected[i] &amp;&amp; !duplicated.contains(choice)) {\n // 시도: 선택하고 상태를 갱신한다\n duplicated.add(choice); // 선택한 원소의 값을 기록한다\n selected[i] = true;\n state.add(choice);\n // 다음 선택을 진행한다\n backtrack(state, choices, selected, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n selected[i] = false;\n state.remove(state.size() - 1);\n }\n }\n}\n\n/* 모든 순열 II */\nList&lt;List&lt;Integer&gt;&gt; permutationsII(int[] nums) {\n List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;List&lt;Integer&gt;&gt;();\n backtrack(new ArrayList&lt;Integer&gt;(), nums, new boolean[nums.length], res);\n return res;\n}\n</code></pre> permutations_ii.cs<pre><code>/* 백트래킹 알고리즘: 모든 순열 II */\nvoid Backtrack(List&lt;int&gt; state, int[] choices, bool[] selected, List&lt;List&lt;int&gt;&gt; res) {\n // 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if (state.Count == choices.Length) {\n res.Add(new List&lt;int&gt;(state));\n return;\n }\n // 모든 선택지를 순회한다\n HashSet&lt;int&gt; duplicated = [];\n for (int i = 0; i &lt; choices.Length; i++) {\n int choice = choices[i];\n // 가지치기: 원소를 중복하여 선택할 수 없으며, 값이 같은 원소도 중복하여 선택할 수 없다\n if (!selected[i] &amp;&amp; !duplicated.Contains(choice)) {\n // 시도: 선택하고 상태를 갱신한다\n duplicated.Add(choice); // 선택한 원소의 값을 기록한다\n selected[i] = true;\n state.Add(choice);\n // 다음 선택을 진행한다\n Backtrack(state, choices, selected, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n selected[i] = false;\n state.RemoveAt(state.Count - 1);\n }\n }\n}\n\n/* 모든 순열 II */\nList&lt;List&lt;int&gt;&gt; PermutationsII(int[] nums) {\n List&lt;List&lt;int&gt;&gt; res = [];\n Backtrack([], nums, new bool[nums.Length], res);\n return res;\n}\n</code></pre> permutations_ii.go<pre><code>/* 백트래킹 알고리즘: 모든 순열 II */\nfunc backtrackII(state *[]int, choices *[]int, selected *[]bool, res *[][]int) {\n // 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if len(*state) == len(*choices) {\n newState := append([]int{}, *state...)\n *res = append(*res, newState)\n }\n // 모든 선택지를 순회한다\n duplicated := make(map[int]struct{}, 0)\n for i := 0; i &lt; len(*choices); i++ {\n choice := (*choices)[i]\n // 가지치기: 원소를 중복하여 선택할 수 없으며, 값이 같은 원소도 중복하여 선택할 수 없다\n if _, ok := duplicated[choice]; !ok &amp;&amp; !(*selected)[i] {\n // 시도: 선택하고 상태를 갱신한다\n // 선택한 원소의 값을 기록한다\n duplicated[choice] = struct{}{}\n (*selected)[i] = true\n *state = append(*state, choice)\n // 다음 선택을 진행한다\n backtrackII(state, choices, selected, res)\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n (*selected)[i] = false\n *state = (*state)[:len(*state)-1]\n }\n }\n}\n\n/* 모든 순열 II */\nfunc permutationsII(nums []int) [][]int {\n res := make([][]int, 0)\n state := make([]int, 0)\n selected := make([]bool, len(nums))\n backtrackII(&amp;state, &amp;nums, &amp;selected, &amp;res)\n return res\n}\n</code></pre> permutations_ii.swift<pre><code>/* 백트래킹 알고리즘: 모든 순열 II */\nfunc backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) {\n // 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if state.count == choices.count {\n res.append(state)\n return\n }\n // 모든 선택지를 순회한다\n var duplicated: Set&lt;Int&gt; = []\n for (i, choice) in choices.enumerated() {\n // 가지치기: 원소를 중복하여 선택할 수 없으며, 값이 같은 원소도 중복하여 선택할 수 없다\n if !selected[i], !duplicated.contains(choice) {\n // 시도: 선택하고 상태를 갱신한다\n duplicated.insert(choice) // 선택한 원소의 값을 기록한다\n selected[i] = true\n state.append(choice)\n // 다음 선택을 진행한다\n backtrack(state: &amp;state, choices: choices, selected: &amp;selected, res: &amp;res)\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n selected[i] = false\n state.removeLast()\n }\n }\n}\n\n/* 모든 순열 II */\nfunc permutationsII(nums: [Int]) -&gt; [[Int]] {\n var state: [Int] = []\n var selected = Array(repeating: false, count: nums.count)\n var res: [[Int]] = []\n backtrack(state: &amp;state, choices: nums, selected: &amp;selected, res: &amp;res)\n return res\n}\n</code></pre> permutations_ii.js<pre><code>/* 백트래킹 알고리즘: 모든 순열 II */\nfunction backtrack(state, choices, selected, res) {\n // 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if (state.length === choices.length) {\n res.push([...state]);\n return;\n }\n // 모든 선택지를 순회한다\n const duplicated = new Set();\n choices.forEach((choice, i) =&gt; {\n // 가지치기: 원소를 중복하여 선택할 수 없으며, 값이 같은 원소도 중복하여 선택할 수 없다\n if (!selected[i] &amp;&amp; !duplicated.has(choice)) {\n // 시도: 선택하고 상태를 갱신한다\n duplicated.add(choice); // 선택한 원소의 값을 기록한다\n selected[i] = true;\n state.push(choice);\n // 다음 선택을 진행한다\n backtrack(state, choices, selected, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n selected[i] = false;\n state.pop();\n }\n });\n}\n\n/* 모든 순열 II */\nfunction permutationsII(nums) {\n const res = [];\n backtrack([], nums, Array(nums.length).fill(false), res);\n return res;\n}\n</code></pre> permutations_ii.ts<pre><code>/* 백트래킹 알고리즘: 모든 순열 II */\nfunction backtrack(\n state: number[],\n choices: number[],\n selected: boolean[],\n res: number[][]\n): void {\n // 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if (state.length === choices.length) {\n res.push([...state]);\n return;\n }\n // 모든 선택지를 순회한다\n const duplicated = new Set();\n choices.forEach((choice, i) =&gt; {\n // 가지치기: 원소를 중복하여 선택할 수 없으며, 값이 같은 원소도 중복하여 선택할 수 없다\n if (!selected[i] &amp;&amp; !duplicated.has(choice)) {\n // 시도: 선택하고 상태를 갱신한다\n duplicated.add(choice); // 선택한 원소의 값을 기록한다\n selected[i] = true;\n state.push(choice);\n // 다음 선택을 진행한다\n backtrack(state, choices, selected, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n selected[i] = false;\n state.pop();\n }\n });\n}\n\n/* 모든 순열 II */\nfunction permutationsII(nums: number[]): number[][] {\n const res: number[][] = [];\n backtrack([], nums, Array(nums.length).fill(false), res);\n return res;\n}\n</code></pre> permutations_ii.dart<pre><code>/* 백트래킹 알고리즘: 모든 순열 II */\nvoid backtrack(\n List&lt;int&gt; state,\n List&lt;int&gt; choices,\n List&lt;bool&gt; selected,\n List&lt;List&lt;int&gt;&gt; res,\n) {\n // 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if (state.length == choices.length) {\n res.add(List.from(state));\n return;\n }\n // 모든 선택지를 순회한다\n Set&lt;int&gt; duplicated = {};\n for (int i = 0; i &lt; choices.length; i++) {\n int choice = choices[i];\n // 가지치기: 원소를 중복하여 선택할 수 없으며, 값이 같은 원소도 중복하여 선택할 수 없다\n if (!selected[i] &amp;&amp; !duplicated.contains(choice)) {\n // 시도: 선택하고 상태를 갱신한다\n duplicated.add(choice); // 선택한 원소의 값을 기록한다\n selected[i] = true;\n state.add(choice);\n // 다음 선택을 진행한다\n backtrack(state, choices, selected, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n selected[i] = false;\n state.removeLast();\n }\n }\n}\n\n/* 모든 순열 II */\nList&lt;List&lt;int&gt;&gt; permutationsII(List&lt;int&gt; nums) {\n List&lt;List&lt;int&gt;&gt; res = [];\n backtrack([], nums, List.filled(nums.length, false), res);\n return res;\n}\n</code></pre> permutations_ii.rs<pre><code>/* 백트래킹 알고리즘: 모든 순열 II */\nfn backtrack(mut state: Vec&lt;i32&gt;, choices: &amp;[i32], selected: &amp;mut [bool], res: &amp;mut Vec&lt;Vec&lt;i32&gt;&gt;) {\n // 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if state.len() == choices.len() {\n res.push(state);\n return;\n }\n // 모든 선택지를 순회한다\n let mut duplicated = HashSet::&lt;i32&gt;::new();\n for i in 0..choices.len() {\n let choice = choices[i];\n // 가지치기: 원소를 중복하여 선택할 수 없으며, 값이 같은 원소도 중복하여 선택할 수 없다\n if !selected[i] &amp;&amp; !duplicated.contains(&amp;choice) {\n // 시도: 선택하고 상태를 갱신한다\n duplicated.insert(choice); // 선택한 원소의 값을 기록한다\n selected[i] = true;\n state.push(choice);\n // 다음 선택을 진행한다\n backtrack(state.clone(), choices, selected, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n selected[i] = false;\n state.pop();\n }\n }\n}\n\n/* 모든 순열 II */\nfn permutations_ii(nums: &amp;mut [i32]) -&gt; Vec&lt;Vec&lt;i32&gt;&gt; {\n let mut res = Vec::new();\n backtrack(Vec::new(), nums, &amp;mut vec![false; nums.len()], &amp;mut res);\n res\n}\n</code></pre> permutations_ii.c<pre><code>/* 백트래킹 알고리즘: 모든 순열 II */\nvoid backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) {\n // 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if (stateSize == choicesSize) {\n res[*resSize] = (int *)malloc(choicesSize * sizeof(int));\n for (int i = 0; i &lt; choicesSize; i++) {\n res[*resSize][i] = state[i];\n }\n (*resSize)++;\n return;\n }\n // 모든 선택지를 순회한다\n bool duplicated[MAX_SIZE] = {false};\n for (int i = 0; i &lt; choicesSize; i++) {\n int choice = choices[i];\n // 가지치기: 원소를 중복하여 선택할 수 없으며, 값이 같은 원소도 중복하여 선택할 수 없다\n if (!selected[i] &amp;&amp; !duplicated[choice]) {\n // 시도: 선택하고 상태를 갱신한다\n duplicated[choice] = true; // 선택한 원소의 값을 기록한다\n selected[i] = true;\n state[stateSize] = choice;\n // 다음 선택을 진행한다\n backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n selected[i] = false;\n }\n }\n}\n\n/* 모든 순열 II */\nint **permutationsII(int *nums, int numsSize, int *returnSize) {\n int *state = (int *)malloc(numsSize * sizeof(int));\n bool *selected = (bool *)malloc(numsSize * sizeof(bool));\n for (int i = 0; i &lt; numsSize; i++) {\n selected[i] = false;\n }\n int **res = (int **)malloc(MAX_SIZE * sizeof(int *));\n *returnSize = 0;\n\n backtrack(state, 0, nums, numsSize, selected, res, returnSize);\n\n free(state);\n free(selected);\n\n return res;\n}\n</code></pre> permutations_ii.kt<pre><code>/* 백트래킹 알고리즘: 모든 순열 II */\nfun backtrack(\n state: MutableList&lt;Int&gt;,\n choices: IntArray,\n selected: BooleanArray,\n res: MutableList&lt;MutableList&lt;Int&gt;?&gt;\n) {\n // 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if (state.size == choices.size) {\n res.add(state.toMutableList())\n return\n }\n // 모든 선택지를 순회한다\n val duplicated = HashSet&lt;Int&gt;()\n for (i in choices.indices) {\n val choice = choices[i]\n // 가지치기: 원소를 중복하여 선택할 수 없으며, 값이 같은 원소도 중복하여 선택할 수 없다\n if (!selected[i] &amp;&amp; !duplicated.contains(choice)) {\n // 시도: 선택하고 상태를 갱신한다\n duplicated.add(choice) // 선택한 원소의 값을 기록한다\n selected[i] = true\n state.add(choice)\n // 다음 선택을 진행한다\n backtrack(state, choices, selected, res)\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n selected[i] = false\n state.removeAt(state.size - 1)\n }\n }\n}\n\n/* 모든 순열 II */\nfun permutationsII(nums: IntArray): MutableList&lt;MutableList&lt;Int&gt;?&gt; {\n val res = mutableListOf&lt;MutableList&lt;Int&gt;?&gt;()\n backtrack(mutableListOf(), nums, BooleanArray(nums.size), res)\n return res\n}\n</code></pre> permutations_ii.rb<pre><code>### 백트래킹 알고리즘: 모든 순열 II ###\ndef backtrack(state, choices, selected, res)\n # 상태의 길이가 원소 개수와 같으면 해를 기록한다\n if state.length == choices.length\n res &lt;&lt; state.dup\n return\n end\n\n # 모든 선택지를 순회한다\n duplicated = Set.new\n choices.each_with_index do |choice, i|\n # 가지치기: 원소를 중복하여 선택할 수 없으며, 값이 같은 원소도 중복하여 선택할 수 없다\n if !selected[i] &amp;&amp; !duplicated.include?(choice)\n # 시도: 선택하고 상태를 갱신한다\n duplicated.add(choice)\n selected[i] = true\n state &lt;&lt; choice\n # 다음 선택을 진행한다\n backtrack(state, choices, selected, res)\n # 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n selected[i] = false\n state.pop\n end\n end\nend\n\n### 모든 순열 II ###\ndef permutations_ii(nums)\n res = []\n backtrack([], nums, Array.new(nums.length, false), res)\n res\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>모든 원소가 서로 다르다면 \\(n\\) 개 원소의 순열은 \\(n!\\) 가지(팩토리얼)다. 결과를 기록할 때마다 길이 \\(n\\)의 리스트를 복사하는 데 \\(O(n)\\) 시간이 걸린다. 따라서 시간 복잡도는 \\(O(n!n)\\)이다.</p> <p>최대 재귀 깊이는 \\(n\\)이므로 \\(O(n)\\)의 스택 프레임 공간을 사용한다. <code>selected</code>는 \\(O(n)\\) 공간을 사용한다. 동시에 최대 \\(n\\) 개의 <code>duplicated</code>가 존재하여 \\(O(n^2)\\) 공간을 사용한다. 따라서 공간 복잡도는 \\(O(n^2)\\)이다.</p>","path":["제 13 장   백트래킹","13.2   모든 순열 문제"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#3","level":3,"title":"3.   두 가지 가지치기 비교","text":"<p><code>selected</code>와 <code>duplicated</code>는 모두 가지치기에 쓰이지만 목적이 다르다.</p> <ul> <li>중복 선택 가지치기: 전체 탐색 과정에 <code>selected</code>는 하나만 존재한다. 현재 상태에 어떤 원소가 포함되어 있는지를 기록하여 같은 원소가 <code>state</code>에 중복 등장하지 않게 한다.</li> <li>같은 값의 원소 가지치기: 매 차례의 선택(호출된 각 <code>backtrack</code> 함수)마다 <code>duplicated</code>가 하나씩 있다. 이번 순회(<code>for</code> 반복문)에서 이미 선택한 원소를 기록하여 같은 값의 원소를 한 번만 선택하게 한다.</li> </ul> <p>아래 그림(13-9)은 두 가지 가지치기 조건이 적용되는 범위를 보여 준다. 각 트리 노드는 선택 하나를 나타내며, 루트에서 리프까지의 경로에 있는 노드들이 하나의 순열을 이룬다.</p> <p></p> <p> 그림 13-9   두 가지 가지치기 조건의 적용 범위 </p>","path":["제 13 장   백트래킹","13.2   모든 순열 문제"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/","level":1,"title":"13.3   부분 집합 합 문제","text":"","path":["제 13 장   백트래킹","13.3   부분 집합 합 문제"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1331","level":2,"title":"13.3.1   중복 원소가 없는 경우","text":"<p>문제</p> <p>양의 정수 배열 <code>nums</code>와 목표 양의 정수 <code>target</code>이 주어진다. 원소의 합이 <code>target</code>인 모든 조합을 찾아라. 배열에는 중복 원소가 없고, 각 원소는 여러 번 선택할 수 있다. 조합들을 리스트로 반환하되 중복 조합이 없어야 한다.</p> <p>예를 들어 입력 집합이 \\(\\{3, 4, 5\\}\\)이고 목표 정수가 \\(9\\)이면 해는 \\(\\{3, 3, 3\\}, \\{4, 5\\}\\)다. 다음 두 가지에 주의하자.</p> <ul> <li>입력 집합의 원소는 횟수 제한 없이 반복 선택할 수 있다.</li> <li>부분 집합에서는 원소의 순서를 구분하지 않는다. 예를 들어 \\(\\{4, 5\\}\\)와 \\(\\{5, 4\\}\\)는 같은 부분 집합이다.</li> </ul>","path":["제 13 장   백트래킹","13.3   부분 집합 합 문제"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1","level":3,"title":"1.   모든 순열 풀이 참고","text":"<p>모든 순열 문제처럼 부분 집합을 만드는 과정을 연속된 선택의 결과로 볼 수 있다. 선택할 때마다 '원소의 합'을 갱신하고, 합이 <code>target</code>이 되면 해당 부분 집합을 결과 리스트에 기록한다.</p> <p>모든 순열 문제와 달리 이 문제에서는 집합의 원소를 횟수 제한 없이 선택할 수 있다. 따라서 선택 여부를 기록하는 불리언 리스트 <code>selected</code>가 필요하지 않다. 모든 순열 코드를 조금 수정하면 다음과 같은 첫 풀이를 얻는다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_i_naive.py<pre><code>def backtrack(\n state: list[int],\n target: int,\n total: int,\n choices: list[int],\n res: list[list[int]],\n):\n \"\"\"백트래킹 알고리즘: 부분 집합 합 I\"\"\"\n # 부분 집합의 합이 target과 같으면 해를 기록한다\n if total == target:\n res.append(list(state))\n return\n # 모든 선택지를 순회한다\n for i in range(len(choices)):\n # 가지치기: 부분 집합의 합이 target을 초과하면 해당 선택을 건너뛴다\n if total + choices[i] &gt; target:\n continue\n # 시도: 선택하고 원소의 합 total을 갱신한다\n state.append(choices[i])\n # 다음 선택을 진행한다\n backtrack(state, target, total + choices[i], choices, res)\n # 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.pop()\n\ndef subset_sum_i_naive(nums: list[int], target: int) -&gt; list[list[int]]:\n \"\"\"부분 집합 합 I 풀기(중복 부분 집합 포함)\"\"\"\n state = [] # 상태(부분 집합)\n total = 0 # 부분 집합의 합\n res = [] # 결과 리스트(부분 집합 리스트)\n backtrack(state, target, total, nums, res)\n return res\n</code></pre> subset_sum_i_naive.cpp<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 I */\nvoid backtrack(vector&lt;int&gt; &amp;state, int target, int total, vector&lt;int&gt; &amp;choices, vector&lt;vector&lt;int&gt;&gt; &amp;res) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if (total == target) {\n res.push_back(state);\n return;\n }\n // 모든 선택지를 순회한다\n for (size_t i = 0; i &lt; choices.size(); i++) {\n // 가지치기: 부분 집합의 합이 target을 초과하면 해당 선택을 건너뛴다\n if (total + choices[i] &gt; target) {\n continue;\n }\n // 시도: 선택하고 원소의 합 total을 갱신한다\n state.push_back(choices[i]);\n // 다음 선택을 진행한다\n backtrack(state, target, total + choices[i], choices, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.pop_back();\n }\n}\n\n/* 부분 집합 합 I 풀기(중복 부분 집합 포함) */\nvector&lt;vector&lt;int&gt;&gt; subsetSumINaive(vector&lt;int&gt; &amp;nums, int target) {\n vector&lt;int&gt; state; // 상태(부분 집합)\n int total = 0; // 부분 집합의 합\n vector&lt;vector&lt;int&gt;&gt; res; // 결과 리스트(부분 집합 리스트)\n backtrack(state, target, total, nums, res);\n return res;\n}\n</code></pre> subset_sum_i_naive.java<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 I */\nvoid backtrack(List&lt;Integer&gt; state, int target, int total, int[] choices, List&lt;List&lt;Integer&gt;&gt; res) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if (total == target) {\n res.add(new ArrayList&lt;&gt;(state));\n return;\n }\n // 모든 선택지를 순회한다\n for (int i = 0; i &lt; choices.length; i++) {\n // 가지치기: 부분 집합의 합이 target을 초과하면 해당 선택을 건너뛴다\n if (total + choices[i] &gt; target) {\n continue;\n }\n // 시도: 선택하고 원소의 합 total을 갱신한다\n state.add(choices[i]);\n // 다음 선택을 진행한다\n backtrack(state, target, total + choices[i], choices, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.remove(state.size() - 1);\n }\n}\n\n/* 부분 집합 합 I 풀기(중복 부분 집합 포함) */\nList&lt;List&lt;Integer&gt;&gt; subsetSumINaive(int[] nums, int target) {\n List&lt;Integer&gt; state = new ArrayList&lt;&gt;(); // 상태(부분 집합)\n int total = 0; // 부분 집합의 합\n List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;&gt;(); // 결과 리스트(부분 집합 리스트)\n backtrack(state, target, total, nums, res);\n return res;\n}\n</code></pre> subset_sum_i_naive.cs<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 I */\nvoid Backtrack(List&lt;int&gt; state, int target, int total, int[] choices, List&lt;List&lt;int&gt;&gt; res) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if (total == target) {\n res.Add(new List&lt;int&gt;(state));\n return;\n }\n // 모든 선택지를 순회한다\n for (int i = 0; i &lt; choices.Length; i++) {\n // 가지치기: 부분 집합의 합이 target을 초과하면 해당 선택을 건너뛴다\n if (total + choices[i] &gt; target) {\n continue;\n }\n // 시도: 선택하고 원소의 합 total을 갱신한다\n state.Add(choices[i]);\n // 다음 선택을 진행한다\n Backtrack(state, target, total + choices[i], choices, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.RemoveAt(state.Count - 1);\n }\n}\n\n/* 부분 집합 합 I 풀기(중복 부분 집합 포함) */\nList&lt;List&lt;int&gt;&gt; SubsetSumINaive(int[] nums, int target) {\n List&lt;int&gt; state = []; // 상태(부분 집합)\n int total = 0; // 부분 집합의 합\n List&lt;List&lt;int&gt;&gt; res = []; // 결과 리스트(부분 집합 리스트)\n Backtrack(state, target, total, nums, res);\n return res;\n}\n</code></pre> subset_sum_i_naive.go<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 I */\nfunc backtrackSubsetSumINaive(total, target int, state, choices *[]int, res *[][]int) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if target == total {\n newState := append([]int{}, *state...)\n *res = append(*res, newState)\n return\n }\n // 모든 선택지를 순회한다\n for i := 0; i &lt; len(*choices); i++ {\n // 가지치기: 부분 집합의 합이 target을 초과하면 해당 선택을 건너뛴다\n if total+(*choices)[i] &gt; target {\n continue\n }\n // 시도: 선택하고 원소의 합 total을 갱신한다\n *state = append(*state, (*choices)[i])\n // 다음 선택을 진행한다\n backtrackSubsetSumINaive(total+(*choices)[i], target, state, choices, res)\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n *state = (*state)[:len(*state)-1]\n }\n}\n\n/* 부분 집합 합 I 풀기(중복 부분 집합 포함) */\nfunc subsetSumINaive(nums []int, target int) [][]int {\n state := make([]int, 0) // 상태(부분 집합)\n total := 0 // 부분 집합의 합\n res := make([][]int, 0) // 결과 리스트(부분 집합 리스트)\n backtrackSubsetSumINaive(total, target, &amp;state, &amp;nums, &amp;res)\n return res\n}\n</code></pre> subset_sum_i_naive.swift<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 I */\nfunc backtrack(state: inout [Int], target: Int, total: Int, choices: [Int], res: inout [[Int]]) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if total == target {\n res.append(state)\n return\n }\n // 모든 선택지를 순회한다\n for i in choices.indices {\n // 가지치기: 부분 집합의 합이 target을 초과하면 해당 선택을 건너뛴다\n if total + choices[i] &gt; target {\n continue\n }\n // 시도: 선택하고 원소의 합 total을 갱신한다\n state.append(choices[i])\n // 다음 선택을 진행한다\n backtrack(state: &amp;state, target: target, total: total + choices[i], choices: choices, res: &amp;res)\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.removeLast()\n }\n}\n\n/* 부분 집합 합 I 풀기(중복 부분 집합 포함) */\nfunc subsetSumINaive(nums: [Int], target: Int) -&gt; [[Int]] {\n var state: [Int] = [] // 상태(부분 집합)\n let total = 0 // 부분 집합의 합\n var res: [[Int]] = [] // 결과 리스트(부분 집합 리스트)\n backtrack(state: &amp;state, target: target, total: total, choices: nums, res: &amp;res)\n return res\n}\n</code></pre> subset_sum_i_naive.js<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 I */\nfunction backtrack(state, target, total, choices, res) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if (total === target) {\n res.push([...state]);\n return;\n }\n // 모든 선택지를 순회한다\n for (let i = 0; i &lt; choices.length; i++) {\n // 가지치기: 부분 집합의 합이 target을 초과하면 해당 선택을 건너뛴다\n if (total + choices[i] &gt; target) {\n continue;\n }\n // 시도: 선택하고 원소의 합 total을 갱신한다\n state.push(choices[i]);\n // 다음 선택을 진행한다\n backtrack(state, target, total + choices[i], choices, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.pop();\n }\n}\n\n/* 부분 집합 합 I 풀기(중복 부분 집합 포함) */\nfunction subsetSumINaive(nums, target) {\n const state = []; // 상태(부분 집합)\n const total = 0; // 부분 집합의 합\n const res = []; // 결과 리스트(부분 집합 리스트)\n backtrack(state, target, total, nums, res);\n return res;\n}\n</code></pre> subset_sum_i_naive.ts<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 I */\nfunction backtrack(\n state: number[],\n target: number,\n total: number,\n choices: number[],\n res: number[][]\n): void {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if (total === target) {\n res.push([...state]);\n return;\n }\n // 모든 선택지를 순회한다\n for (let i = 0; i &lt; choices.length; i++) {\n // 가지치기: 부분 집합의 합이 target을 초과하면 해당 선택을 건너뛴다\n if (total + choices[i] &gt; target) {\n continue;\n }\n // 시도: 선택하고 원소의 합 total을 갱신한다\n state.push(choices[i]);\n // 다음 선택을 진행한다\n backtrack(state, target, total + choices[i], choices, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.pop();\n }\n}\n\n/* 부분 집합 합 I 풀기(중복 부분 집합 포함) */\nfunction subsetSumINaive(nums: number[], target: number): number[][] {\n const state = []; // 상태(부분 집합)\n const total = 0; // 부분 집합의 합\n const res = []; // 결과 리스트(부분 집합 리스트)\n backtrack(state, target, total, nums, res);\n return res;\n}\n</code></pre> subset_sum_i_naive.dart<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 I */\nvoid backtrack(\n List&lt;int&gt; state,\n int target,\n int total,\n List&lt;int&gt; choices,\n List&lt;List&lt;int&gt;&gt; res,\n) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if (total == target) {\n res.add(List.from(state));\n return;\n }\n // 모든 선택지를 순회한다\n for (int i = 0; i &lt; choices.length; i++) {\n // 가지치기: 부분 집합의 합이 target을 초과하면 해당 선택을 건너뛴다\n if (total + choices[i] &gt; target) {\n continue;\n }\n // 시도: 선택하고 원소의 합 total을 갱신한다\n state.add(choices[i]);\n // 다음 선택을 진행한다\n backtrack(state, target, total + choices[i], choices, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.removeLast();\n }\n}\n\n/* 부분 집합 합 I 풀기(중복 부분 집합 포함) */\nList&lt;List&lt;int&gt;&gt; subsetSumINaive(List&lt;int&gt; nums, int target) {\n List&lt;int&gt; state = []; // 상태(부분 집합)\n int total = 0; // 원소의 합\n List&lt;List&lt;int&gt;&gt; res = []; // 결과 리스트(부분 집합 리스트)\n backtrack(state, target, total, nums, res);\n return res;\n}\n</code></pre> subset_sum_i_naive.rs<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 I */\nfn backtrack(\n state: &amp;mut Vec&lt;i32&gt;,\n target: i32,\n total: i32,\n choices: &amp;[i32],\n res: &amp;mut Vec&lt;Vec&lt;i32&gt;&gt;,\n) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if total == target {\n res.push(state.clone());\n return;\n }\n // 모든 선택지를 순회한다\n for i in 0..choices.len() {\n // 가지치기: 부분 집합의 합이 target을 초과하면 해당 선택을 건너뛴다\n if total + choices[i] &gt; target {\n continue;\n }\n // 시도: 선택하고 원소의 합 total을 갱신한다\n state.push(choices[i]);\n // 다음 선택을 진행한다\n backtrack(state, target, total + choices[i], choices, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.pop();\n }\n}\n\n/* 부분 집합 합 I 풀기(중복 부분 집합 포함) */\nfn subset_sum_i_naive(nums: &amp;[i32], target: i32) -&gt; Vec&lt;Vec&lt;i32&gt;&gt; {\n let mut state = Vec::new(); // 상태(부분 집합)\n let total = 0; // 부분 집합의 합\n let mut res = Vec::new(); // 결과 리스트(부분 집합 리스트)\n backtrack(&amp;mut state, target, total, nums, &amp;mut res);\n res\n}\n</code></pre> subset_sum_i_naive.c<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 I */\nvoid backtrack(int target, int total, int *choices, int choicesSize) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if (total == target) {\n for (int i = 0; i &lt; stateSize; i++) {\n res[resSize][i] = state[i];\n }\n resColSizes[resSize++] = stateSize;\n return;\n }\n // 모든 선택지를 순회한다\n for (int i = 0; i &lt; choicesSize; i++) {\n // 가지치기: 부분 집합의 합이 target을 초과하면 해당 선택을 건너뛴다\n if (total + choices[i] &gt; target) {\n continue;\n }\n // 시도: 선택하고 원소의 합 total을 갱신한다\n state[stateSize++] = choices[i];\n // 다음 선택을 진행한다\n backtrack(target, total + choices[i], choices, choicesSize);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n stateSize--;\n }\n}\n\n/* 부분 집합 합 I 풀기(중복 부분 집합 포함) */\nvoid subsetSumINaive(int *nums, int numsSize, int target) {\n resSize = 0; // 해의 개수를 0으로 초기화\n backtrack(target, 0, nums, numsSize);\n}\n</code></pre> subset_sum_i_naive.kt<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 I */\nfun backtrack(\n state: MutableList&lt;Int&gt;,\n target: Int,\n total: Int,\n choices: IntArray,\n res: MutableList&lt;MutableList&lt;Int&gt;?&gt;\n) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if (total == target) {\n res.add(state.toMutableList())\n return\n }\n // 모든 선택지를 순회한다\n for (i in choices.indices) {\n // 가지치기: 부분 집합의 합이 target을 초과하면 해당 선택을 건너뛴다\n if (total + choices[i] &gt; target) {\n continue\n }\n // 시도: 선택하고 원소의 합 total을 갱신한다\n state.add(choices[i])\n // 다음 선택을 진행한다\n backtrack(state, target, total + choices[i], choices, res)\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.removeAt(state.size - 1)\n }\n}\n\n/* 부분 집합 합 I 풀기(중복 부분 집합 포함) */\nfun subsetSumINaive(nums: IntArray, target: Int): MutableList&lt;MutableList&lt;Int&gt;?&gt; {\n val state = mutableListOf&lt;Int&gt;() // 상태(부분 집합)\n val total = 0 // 부분 집합의 합\n val res = mutableListOf&lt;MutableList&lt;Int&gt;?&gt;() // 결과 리스트(부분 집합 리스트)\n backtrack(state, target, total, nums, res)\n return res\n}\n</code></pre> subset_sum_i_naive.rb<pre><code>### 백트래킹 알고리즘: 부분 집합 합 I ###\ndef backtrack(state, target, total, choices, res)\n # 부분 집합의 합이 target과 같으면 해를 기록한다\n if total == target\n res &lt;&lt; state.dup\n return\n end\n\n # 모든 선택지를 순회한다\n for i in 0...choices.length\n # 가지치기: 부분 집합의 합이 target을 초과하면 해당 선택을 건너뛴다\n next if total + choices[i] &gt; target\n # 시도: 선택하고 원소의 합 total을 갱신한다\n state &lt;&lt; choices[i]\n # 다음 선택을 진행한다\n backtrack(state, target, total + choices[i], choices, res)\n # 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.pop\n end\nend\n\n### 부분 집합 합 I 풀기(중복 부분 집합 포함)###\ndef subset_sum_i_naive(nums, target)\n state = [] # 상태(부분 집합)\n total = 0 # 부분 집합의 합\n res = [] # 결과 리스트(부분 집합 리스트)\n backtrack(state, target, total, nums, res)\n res\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>위 코드에 배열 \\([3, 4, 5]\\)와 목표 원소 \\(9\\)를 넣으면 \\([3, 3, 3], [4, 5], [5, 4]\\)가 나온다. 합이 \\(9\\)인 모든 부분 집합을 찾았지만 \\([4, 5]\\)와 \\([5, 4]\\)는 중복이다.</p> <p>탐색 과정에서는 선택 순서를 구분하지만 부분 집합에서는 구분하지 않기 때문이다. 아래 그림(13-10)에서 \\(4\\) 다음 \\(5\\)를 고르는 것과 \\(5\\) 다음 \\(4\\)를 고르는 것은 서로 다른 가지이지만 같은 부분 집합에 대응한다.</p> <p></p> <p> 그림 13-10   부분 집합 탐색과 범위 초과 가지치기 </p> <p>중복 부분 집합을 제거하는 직접적인 방법은 결과 리스트에서 중복을 제거하는 것이다. 하지만 다음 두 이유로 매우 비효율적이다.</p> <ul> <li>배열 원소가 많고 특히 <code>target</code>이 크면 탐색 과정에서 중복 부분 집합이 매우 많이 생긴다.</li> <li>부분 집합(배열)이 같은지 비교하는 데 시간이 많이 든다. 먼저 배열을 정렬한 뒤 원소를 하나씩 비교해야 한다.</li> </ul>","path":["제 13 장   백트래킹","13.3   부분 집합 합 문제"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#2","level":3,"title":"2.   중복 부분 집합 가지치기","text":"<p>탐색 중 가지치기로 중복을 제거해 보자. 아래 그림(13-11)에서 중복 부분 집합은 배열 원소를 서로 다른 순서로 선택할 때 생긴다. 예를 들면 다음과 같다.</p> <ol> <li>첫 번째와 두 번째에 각각 \\(3\\)과 \\(4\\)를 선택하면 이 두 원소를 포함하는 모든 부분 집합 \\([3, 4, \\dots]\\)가 만들어진다.</li> <li>이후 첫 번째에 \\(4\\)를 선택하면 두 번째에서는 \\(3\\)을 건너뛰어야 한다. 이 선택으로 만드는 \\([4, 3, \\dots]\\)는 <code>1.</code> 단계에서 만든 부분 집합과 완전히 중복되기 때문이다.</li> </ol> <p>탐색에서 각 레벨의 선택은 왼쪽부터 오른쪽 순서로 시도하므로, 오른쪽으로 갈수록 잘라 내는 가지가 많아진다.</p> <ol> <li>첫 두 차례에 \\(3\\)과 \\(5\\)를 선택하여 부분 집합 \\([3, 5, \\dots]\\)를 만든다.</li> <li>첫 두 차례에 \\(4\\)와 \\(5\\)를 선택하여 부분 집합 \\([4, 5, \\dots]\\)를 만든다.</li> <li>첫 번째에 \\(5\\)를 선택한다면 두 번째에서는 \\(3\\)과 \\(4\\)를 건너뛰어야 한다. \\([5, 3, \\dots]\\)와 \\([5, 4, \\dots]\\)는 <code>1.</code> 단계와 <code>2.</code> 단계의 부분 집합과 완전히 중복되기 때문이다.</li> </ol> <p></p> <p> 그림 13-11   선택 순서의 차이로 생기는 중복 부분 집합 </p> <p>정리하면 입력 배열이 \\([x_1, x_2, \\dots, x_n]\\)일 때 선택 순서 \\([x_{i_1}, x_{i_2}, \\dots, x_{i_m}]\\)는 \\(i_1 \\leq i_2 \\leq \\dots \\leq i_m\\)을 만족해야 한다. 이 조건을 만족하지 않는 선택 순서는 중복을 만들므로 가지치기해야 한다.</p>","path":["제 13 장   백트래킹","13.3   부분 집합 합 문제"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#3","level":3,"title":"3.   코드 구현","text":"<p>이 가지치기를 위해 순회 시작점을 나타내는 변수 <code>start</code>를 초기화한다. \\(x_{i}\\)를 선택한 뒤 다음 차례는 인덱스 \\(i\\)부터 순회하도록 설정한다. 그러면 선택 순서가 \\(i_1 \\leq i_2 \\leq \\dots \\leq i_m\\)을 만족하여 부분 집합의 유일성을 보장한다.</p> <p>이 밖에도 다음 두 가지를 최적화했다.</p> <ul> <li>탐색을 시작하기 전에 배열 <code>nums</code>를 정렬한다. 선택지를 순회하다가 부분 집합의 합이 <code>target</code>을 넘으면 반복문을 바로 종료한다. 뒤의 원소들은 더 크므로 합도 반드시 <code>target</code>을 넘기 때문이다.</li> <li>원소 합 변수 <code>total</code>을 없애고 <code>target</code>에서 값을 빼는 방식으로 합을 추적한다. <code>target</code>이 \\(0\\)이 되면 해를 기록한다.</li> </ul> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_i.py<pre><code>def backtrack(\n state: list[int], target: int, choices: list[int], start: int, res: list[list[int]]\n):\n \"\"\"백트래킹 알고리즘: 부분 집합 합 I\"\"\"\n # 부분 집합의 합이 target과 같으면 해를 기록한다\n if target == 0:\n res.append(list(state))\n return\n # 모든 선택지를 순회한다\n # 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n for i in range(start, len(choices)):\n # 가지치기 1: 부분 집합의 합이 target을 초과하면 즉시 반복문을 종료한다\n # 배열이 정렬되어 있어 뒤의 원소가 더 크므로, 부분 집합의 합이 반드시 target을 초과하기 때문이다\n if target - choices[i] &lt; 0:\n break\n # 시도: 선택하고 target, start를 갱신한다\n state.append(choices[i])\n # 다음 선택을 진행한다\n backtrack(state, target - choices[i], choices, i, res)\n # 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.pop()\n\ndef subset_sum_i(nums: list[int], target: int) -&gt; list[list[int]]:\n \"\"\"부분 집합 합 I 풀기\"\"\"\n state = [] # 상태(부분 집합)\n nums.sort() # nums를 정렬한다\n start = 0 # 순회 시작 위치\n res = [] # 결과 리스트(부분 집합 리스트)\n backtrack(state, target, nums, start, res)\n return res\n</code></pre> subset_sum_i.cpp<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 I */\nvoid backtrack(vector&lt;int&gt; &amp;state, int target, vector&lt;int&gt; &amp;choices, int start, vector&lt;vector&lt;int&gt;&gt; &amp;res) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if (target == 0) {\n res.push_back(state);\n return;\n }\n // 모든 선택지를 순회한다\n // 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n for (int i = start; i &lt; choices.size(); i++) {\n // 가지치기 1: 부분 집합의 합이 target을 초과하면 즉시 반복문을 종료한다\n // 배열이 정렬되어 있어 뒤의 원소가 더 크므로, 부분 집합의 합이 반드시 target을 초과하기 때문이다\n if (target - choices[i] &lt; 0) {\n break;\n }\n // 시도: 선택하고 target, start를 갱신한다\n state.push_back(choices[i]);\n // 다음 선택을 진행한다\n backtrack(state, target - choices[i], choices, i, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.pop_back();\n }\n}\n\n/* 부분 집합 합 I 풀기 */\nvector&lt;vector&lt;int&gt;&gt; subsetSumI(vector&lt;int&gt; &amp;nums, int target) {\n vector&lt;int&gt; state; // 상태(부분 집합)\n sort(nums.begin(), nums.end()); // nums를 정렬한다\n int start = 0; // 순회 시작 위치\n vector&lt;vector&lt;int&gt;&gt; res; // 결과 리스트(부분 집합 리스트)\n backtrack(state, target, nums, start, res);\n return res;\n}\n</code></pre> subset_sum_i.java<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 I */\nvoid backtrack(List&lt;Integer&gt; state, int target, int[] choices, int start, List&lt;List&lt;Integer&gt;&gt; res) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if (target == 0) {\n res.add(new ArrayList&lt;&gt;(state));\n return;\n }\n // 모든 선택지를 순회한다\n // 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n for (int i = start; i &lt; choices.length; i++) {\n // 가지치기 1: 부분 집합의 합이 target을 초과하면 즉시 반복문을 종료한다\n // 배열이 정렬되어 있어 뒤의 원소가 더 크므로, 부분 집합의 합이 반드시 target을 초과하기 때문이다\n if (target - choices[i] &lt; 0) {\n break;\n }\n // 시도: 선택하고 target, start를 갱신한다\n state.add(choices[i]);\n // 다음 선택을 진행한다\n backtrack(state, target - choices[i], choices, i, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.remove(state.size() - 1);\n }\n}\n\n/* 부분 집합 합 I 풀기 */\nList&lt;List&lt;Integer&gt;&gt; subsetSumI(int[] nums, int target) {\n List&lt;Integer&gt; state = new ArrayList&lt;&gt;(); // 상태(부분 집합)\n Arrays.sort(nums); // nums를 정렬한다\n int start = 0; // 순회 시작 위치\n List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;&gt;(); // 결과 리스트(부분 집합 리스트)\n backtrack(state, target, nums, start, res);\n return res;\n}\n</code></pre> subset_sum_i.cs<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 I */\nvoid Backtrack(List&lt;int&gt; state, int target, int[] choices, int start, List&lt;List&lt;int&gt;&gt; res) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if (target == 0) {\n res.Add(new List&lt;int&gt;(state));\n return;\n }\n // 모든 선택지를 순회한다\n // 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n for (int i = start; i &lt; choices.Length; i++) {\n // 가지치기 1: 부분 집합의 합이 target을 초과하면 즉시 반복문을 종료한다\n // 배열이 정렬되어 있어 뒤의 원소가 더 크므로, 부분 집합의 합이 반드시 target을 초과하기 때문이다\n if (target - choices[i] &lt; 0) {\n break;\n }\n // 시도: 선택하고 target, start를 갱신한다\n state.Add(choices[i]);\n // 다음 선택을 진행한다\n Backtrack(state, target - choices[i], choices, i, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.RemoveAt(state.Count - 1);\n }\n}\n\n/* 부분 집합 합 I 풀기 */\nList&lt;List&lt;int&gt;&gt; SubsetSumI(int[] nums, int target) {\n List&lt;int&gt; state = []; // 상태(부분 집합)\n Array.Sort(nums); // nums를 정렬한다\n int start = 0; // 순회 시작 위치\n List&lt;List&lt;int&gt;&gt; res = []; // 결과 리스트(부분 집합 리스트)\n Backtrack(state, target, nums, start, res);\n return res;\n}\n</code></pre> subset_sum_i.go<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 I */\nfunc backtrackSubsetSumI(start, target int, state, choices *[]int, res *[][]int) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if target == 0 {\n newState := append([]int{}, *state...)\n *res = append(*res, newState)\n return\n }\n // 모든 선택지를 순회한다\n // 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n for i := start; i &lt; len(*choices); i++ {\n // 가지치기 1: 부분 집합의 합이 target을 초과하면 즉시 반복문을 종료한다\n // 배열이 정렬되어 있어 뒤의 원소가 더 크므로, 부분 집합의 합이 반드시 target을 초과하기 때문이다\n if target-(*choices)[i] &lt; 0 {\n break\n }\n // 시도: 선택하고 target, start를 갱신한다\n *state = append(*state, (*choices)[i])\n // 다음 선택을 진행한다\n backtrackSubsetSumI(i, target-(*choices)[i], state, choices, res)\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n *state = (*state)[:len(*state)-1]\n }\n}\n\n/* 부분 집합 합 I 풀기 */\nfunc subsetSumI(nums []int, target int) [][]int {\n state := make([]int, 0) // 상태(부분 집합)\n sort.Ints(nums) // nums를 정렬한다\n start := 0 // 순회 시작 위치\n res := make([][]int, 0) // 결과 리스트(부분 집합 리스트)\n backtrackSubsetSumI(start, target, &amp;state, &amp;nums, &amp;res)\n return res\n}\n</code></pre> subset_sum_i.swift<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 I */\nfunc backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if target == 0 {\n res.append(state)\n return\n }\n // 모든 선택지를 순회한다\n // 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n for i in choices.indices.dropFirst(start) {\n // 가지치기 1: 부분 집합의 합이 target을 초과하면 즉시 반복문을 종료한다\n // 배열이 정렬되어 있어 뒤의 원소가 더 크므로, 부분 집합의 합이 반드시 target을 초과하기 때문이다\n if target - choices[i] &lt; 0 {\n break\n }\n // 시도: 선택하고 target, start를 갱신한다\n state.append(choices[i])\n // 다음 선택을 진행한다\n backtrack(state: &amp;state, target: target - choices[i], choices: choices, start: i, res: &amp;res)\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.removeLast()\n }\n}\n\n/* 부분 집합 합 I 풀기 */\nfunc subsetSumI(nums: [Int], target: Int) -&gt; [[Int]] {\n var state: [Int] = [] // 상태(부분 집합)\n let nums = nums.sorted() // nums를 정렬한다\n let start = 0 // 순회 시작 위치\n var res: [[Int]] = [] // 결과 리스트(부분 집합 리스트)\n backtrack(state: &amp;state, target: target, choices: nums, start: start, res: &amp;res)\n return res\n}\n</code></pre> subset_sum_i.js<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 I */\nfunction backtrack(state, target, choices, start, res) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if (target === 0) {\n res.push([...state]);\n return;\n }\n // 모든 선택지를 순회한다\n // 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n for (let i = start; i &lt; choices.length; i++) {\n // 가지치기 1: 부분 집합의 합이 target을 초과하면 즉시 반복문을 종료한다\n // 배열이 정렬되어 있어 뒤의 원소가 더 크므로, 부분 집합의 합이 반드시 target을 초과하기 때문이다\n if (target - choices[i] &lt; 0) {\n break;\n }\n // 시도: 선택하고 target, start를 갱신한다\n state.push(choices[i]);\n // 다음 선택을 진행한다\n backtrack(state, target - choices[i], choices, i, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.pop();\n }\n}\n\n/* 부분 집합 합 I 풀기 */\nfunction subsetSumI(nums, target) {\n const state = []; // 상태(부분 집합)\n nums.sort((a, b) =&gt; a - b); // nums를 정렬한다\n const start = 0; // 순회 시작 위치\n const res = []; // 결과 리스트(부분 집합 리스트)\n backtrack(state, target, nums, start, res);\n return res;\n}\n</code></pre> subset_sum_i.ts<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 I */\nfunction backtrack(\n state: number[],\n target: number,\n choices: number[],\n start: number,\n res: number[][]\n): void {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if (target === 0) {\n res.push([...state]);\n return;\n }\n // 모든 선택지를 순회한다\n // 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n for (let i = start; i &lt; choices.length; i++) {\n // 가지치기 1: 부분 집합의 합이 target을 초과하면 즉시 반복문을 종료한다\n // 배열이 정렬되어 있어 뒤의 원소가 더 크므로, 부분 집합의 합이 반드시 target을 초과하기 때문이다\n if (target - choices[i] &lt; 0) {\n break;\n }\n // 시도: 선택하고 target, start를 갱신한다\n state.push(choices[i]);\n // 다음 선택을 진행한다\n backtrack(state, target - choices[i], choices, i, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.pop();\n }\n}\n\n/* 부분 집합 합 I 풀기 */\nfunction subsetSumI(nums: number[], target: number): number[][] {\n const state = []; // 상태(부분 집합)\n nums.sort((a, b) =&gt; a - b); // nums를 정렬한다\n const start = 0; // 순회 시작 위치\n const res = []; // 결과 리스트(부분 집합 리스트)\n backtrack(state, target, nums, start, res);\n return res;\n}\n</code></pre> subset_sum_i.dart<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 I */\nvoid backtrack(\n List&lt;int&gt; state,\n int target,\n List&lt;int&gt; choices,\n int start,\n List&lt;List&lt;int&gt;&gt; res,\n) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if (target == 0) {\n res.add(List.from(state));\n return;\n }\n // 모든 선택지를 순회한다\n // 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n for (int i = start; i &lt; choices.length; i++) {\n // 가지치기 1: 부분 집합의 합이 target을 초과하면 즉시 반복문을 종료한다\n // 배열이 정렬되어 있어 뒤의 원소가 더 크므로, 부분 집합의 합이 반드시 target을 초과하기 때문이다\n if (target - choices[i] &lt; 0) {\n break;\n }\n // 시도: 선택하고 target, start를 갱신한다\n state.add(choices[i]);\n // 다음 선택을 진행한다\n backtrack(state, target - choices[i], choices, i, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.removeLast();\n }\n}\n\n/* 부분 집합 합 I 풀기 */\nList&lt;List&lt;int&gt;&gt; subsetSumI(List&lt;int&gt; nums, int target) {\n List&lt;int&gt; state = []; // 상태(부분 집합)\n nums.sort(); // nums를 정렬한다\n int start = 0; // 순회 시작 위치\n List&lt;List&lt;int&gt;&gt; res = []; // 결과 리스트(부분 집합 리스트)\n backtrack(state, target, nums, start, res);\n return res;\n}\n</code></pre> subset_sum_i.rs<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 I */\nfn backtrack(\n state: &amp;mut Vec&lt;i32&gt;,\n target: i32,\n choices: &amp;[i32],\n start: usize,\n res: &amp;mut Vec&lt;Vec&lt;i32&gt;&gt;,\n) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if target == 0 {\n res.push(state.clone());\n return;\n }\n // 모든 선택지를 순회한다\n // 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n for i in start..choices.len() {\n // 가지치기 1: 부분 집합의 합이 target을 초과하면 즉시 반복문을 종료한다\n // 배열이 정렬되어 있어 뒤의 원소가 더 크므로, 부분 집합의 합이 반드시 target을 초과하기 때문이다\n if target - choices[i] &lt; 0 {\n break;\n }\n // 시도: 선택하고 target, start를 갱신한다\n state.push(choices[i]);\n // 다음 선택을 진행한다\n backtrack(state, target - choices[i], choices, i, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.pop();\n }\n}\n\n/* 부분 집합 합 I 풀기 */\nfn subset_sum_i(nums: &amp;mut [i32], target: i32) -&gt; Vec&lt;Vec&lt;i32&gt;&gt; {\n let mut state = Vec::new(); // 상태(부분 집합)\n nums.sort(); // nums를 정렬한다\n let start = 0; // 순회 시작 위치\n let mut res = Vec::new(); // 결과 리스트(부분 집합 리스트)\n backtrack(&amp;mut state, target, nums, start, &amp;mut res);\n res\n}\n</code></pre> subset_sum_i.c<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 I */\nvoid backtrack(int target, int *choices, int choicesSize, int start) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if (target == 0) {\n for (int i = 0; i &lt; stateSize; ++i) {\n res[resSize][i] = state[i];\n }\n resColSizes[resSize++] = stateSize;\n return;\n }\n // 모든 선택지를 순회한다\n // 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n for (int i = start; i &lt; choicesSize; i++) {\n // 가지치기 1: 부분 집합의 합이 target을 초과하면 즉시 반복문을 종료한다\n // 배열이 정렬되어 있어 뒤의 원소가 더 크므로, 부분 집합의 합이 반드시 target을 초과하기 때문이다\n if (target - choices[i] &lt; 0) {\n break;\n }\n // 시도: 선택하고 target, start를 갱신한다\n state[stateSize] = choices[i];\n stateSize++;\n // 다음 선택을 진행한다\n backtrack(target - choices[i], choices, choicesSize, i);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n stateSize--;\n }\n}\n\n/* 부분 집합 합 I 풀기 */\nvoid subsetSumI(int *nums, int numsSize, int target) {\n qsort(nums, numsSize, sizeof(int), cmp); // nums를 정렬한다\n int start = 0; // 순회 시작 위치\n backtrack(target, nums, numsSize, start);\n}\n</code></pre> subset_sum_i.kt<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 I */\nfun backtrack(\n state: MutableList&lt;Int&gt;,\n target: Int,\n choices: IntArray,\n start: Int,\n res: MutableList&lt;MutableList&lt;Int&gt;?&gt;\n) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if (target == 0) {\n res.add(state.toMutableList())\n return\n }\n // 모든 선택지를 순회한다\n // 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n for (i in start..&lt;choices.size) {\n // 가지치기 1: 부분 집합의 합이 target을 초과하면 즉시 반복문을 종료한다\n // 배열이 정렬되어 있어 뒤의 원소가 더 크므로, 부분 집합의 합이 반드시 target을 초과하기 때문이다\n if (target - choices[i] &lt; 0) {\n break\n }\n // 시도: 선택하고 target, start를 갱신한다\n state.add(choices[i])\n // 다음 선택을 진행한다\n backtrack(state, target - choices[i], choices, i, res)\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.removeAt(state.size - 1)\n }\n}\n\n/* 부분 집합 합 I 풀기 */\nfun subsetSumI(nums: IntArray, target: Int): MutableList&lt;MutableList&lt;Int&gt;?&gt; {\n val state = mutableListOf&lt;Int&gt;() // 상태(부분 집합)\n nums.sort() // nums를 정렬한다\n val start = 0 // 순회 시작 위치\n val res = mutableListOf&lt;MutableList&lt;Int&gt;?&gt;() // 결과 리스트(부분 집합 리스트)\n backtrack(state, target, nums, start, res)\n return res\n}\n</code></pre> subset_sum_i.rb<pre><code>### 백트래킹 알고리즘: 부분 집합 합 I ###\ndef backtrack(state, target, choices, start, res)\n # 부분 집합의 합이 target과 같으면 해를 기록한다\n if target.zero?\n res &lt;&lt; state.dup\n return\n end\n # 모든 선택지를 순회한다\n # 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n for i in start...choices.length\n # 가지치기 1: 부분 집합의 합이 target을 초과하면 즉시 반복문을 종료한다\n # 배열이 정렬되어 있어 뒤의 원소가 더 크므로, 부분 집합의 합이 반드시 target을 초과하기 때문이다\n break if target - choices[i] &lt; 0\n # 시도: 선택하고 target, start를 갱신한다\n state &lt;&lt; choices[i]\n # 다음 선택을 진행한다\n backtrack(state, target - choices[i], choices, i, res)\n # 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.pop\n end\nend\n\n### 부분 집합 합 I 풀기 ###\ndef subset_sum_i(nums, target)\n state = [] # 상태(부분 집합)\n nums.sort! # nums를 정렬한다\n start = 0 # 순회 시작 위치\n res = [] # 결과 리스트(부분 집합 리스트)\n backtrack(state, target, nums, start, res)\n res\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>아래 그림(13-12)은 배열 \\([3, 4, 5]\\)와 목표 원소 \\(9\\)를 위 코드에 넣었을 때의 전체 백트래킹 과정이다.</p> <p></p> <p> 그림 13-12   부분 집합 합 I의 백트래킹 과정 </p>","path":["제 13 장   백트래킹","13.3   부분 집합 합 문제"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1332","level":2,"title":"13.3.2   중복 원소가 있는 경우","text":"<p>문제</p> <p>양의 정수 배열 <code>nums</code>와 목표 양의 정수 <code>target</code>이 주어진다. 원소의 합이 <code>target</code>인 모든 조합을 찾아라. 배열에는 중복 원소가 있을 수 있고, 각 원소는 한 번만 선택할 수 있다. 조합들을 리스트로 반환하되 중복 조합이 없어야 한다.</p> <p>앞 문제와 달리 입력 배열에 중복 원소가 있을 수 있어 새로운 문제가 생긴다. 예를 들어 배열 \\([4, \\hat{4}, 5]\\)와 목표 원소 \\(9\\)를 기존 코드에 넣으면 \\([4, 5], [\\hat{4}, 5]\\)가 나와 부분 집합이 중복된다.</p> <p>중복의 원인은 값이 같은 원소를 한 차례의 선택에서 여러 번 고르기 때문이다. 아래 그림(13-13)에서 첫 차례의 세 선택지 중 두 개가 \\(4\\)여서 중복된 두 탐색 가지가 만들어지고, 같은 부분 집합을 출력한다. 마찬가지로 두 번째 차례의 두 \\(4\\)도 중복을 만든다.</p> <p></p> <p> 그림 13-13   같은 값의 원소로 생기는 중복 부분 집합 </p>","path":["제 13 장   백트래킹","13.3   부분 집합 합 문제"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1_1","level":3,"title":"1.   같은 값의 원소 가지치기","text":"<p>이를 해결하려면 각 차례에서 같은 값의 원소를 한 번만 선택하도록 제한해야 한다. 구현 방법은 간단하면서도 기발하다. 배열이 정렬되어 있으므로 같은 원소끼리는 인접한다. 따라서 한 차례의 선택에서 현재 원소가 왼쪽 원소와 같다면 이미 선택한 값이라는 뜻이므로 바로 건너뛴다.</p> <p>또한 이 문제에서는 각 배열 원소를 한 번만 선택할 수 있다. 다행히 <code>start</code>로 이 제약도 만족시킬 수 있다. \\(x_{i}\\)를 선택한 뒤 다음 차례는 인덱스 \\(i + 1\\)부터 배열의 끝을 향해 순회하도록 설정한다. 그러면 중복 부분 집합과 같은 원소의 반복 선택을 모두 막을 수 있다.</p>","path":["제 13 장   백트래킹","13.3   부분 집합 합 문제"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#2_1","level":3,"title":"2.   코드 구현","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_ii.py<pre><code>def backtrack(\n state: list[int], target: int, choices: list[int], start: int, res: list[list[int]]\n):\n \"\"\"백트래킹 알고리즘: 부분 집합 합 II\"\"\"\n # 부분 집합의 합이 target과 같으면 해를 기록한다\n if target == 0:\n res.append(list(state))\n return\n # 모든 선택지를 순회한다\n # 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n # 가지치기 3: start부터 순회하여 같은 원소의 중복 선택을 방지한다\n for i in range(start, len(choices)):\n # 가지치기 1: 부분 집합의 합이 target을 초과하면 즉시 반복문을 종료한다\n # 배열이 정렬되어 있어 뒤의 원소가 더 크므로, 부분 집합의 합이 반드시 target을 초과하기 때문이다\n if target - choices[i] &lt; 0:\n break\n # 가지치기 4: 해당 원소가 왼쪽 원소와 같으면 탐색 분기가 중복되므로 바로 건너뛴다\n if i &gt; start and choices[i] == choices[i - 1]:\n continue\n # 시도: 선택하고 target, start를 갱신한다\n state.append(choices[i])\n # 다음 선택을 진행한다\n backtrack(state, target - choices[i], choices, i + 1, res)\n # 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.pop()\n\ndef subset_sum_ii(nums: list[int], target: int) -&gt; list[list[int]]:\n \"\"\"부분 집합 합 II 풀기\"\"\"\n state = [] # 상태(부분 집합)\n nums.sort() # nums를 정렬한다\n start = 0 # 순회 시작 위치\n res = [] # 결과 리스트(부분 집합 리스트)\n backtrack(state, target, nums, start, res)\n return res\n</code></pre> subset_sum_ii.cpp<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 II */\nvoid backtrack(vector&lt;int&gt; &amp;state, int target, vector&lt;int&gt; &amp;choices, int start, vector&lt;vector&lt;int&gt;&gt; &amp;res) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if (target == 0) {\n res.push_back(state);\n return;\n }\n // 모든 선택지를 순회한다\n // 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n // 가지치기 3: start부터 순회하여 같은 원소의 중복 선택을 방지한다\n for (int i = start; i &lt; choices.size(); i++) {\n // 가지치기 1: 부분 집합의 합이 target을 초과하면 즉시 반복문을 종료한다\n // 배열이 정렬되어 있어 뒤의 원소가 더 크므로, 부분 집합의 합이 반드시 target을 초과하기 때문이다\n if (target - choices[i] &lt; 0) {\n break;\n }\n // 가지치기 4: 해당 원소가 왼쪽 원소와 같으면 탐색 분기가 중복되므로 바로 건너뛴다\n if (i &gt; start &amp;&amp; choices[i] == choices[i - 1]) {\n continue;\n }\n // 시도: 선택하고 target, start를 갱신한다\n state.push_back(choices[i]);\n // 다음 선택을 진행한다\n backtrack(state, target - choices[i], choices, i + 1, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.pop_back();\n }\n}\n\n/* 부분 집합 합 II 풀기 */\nvector&lt;vector&lt;int&gt;&gt; subsetSumII(vector&lt;int&gt; &amp;nums, int target) {\n vector&lt;int&gt; state; // 상태(부분 집합)\n sort(nums.begin(), nums.end()); // nums를 정렬한다\n int start = 0; // 순회 시작 위치\n vector&lt;vector&lt;int&gt;&gt; res; // 결과 리스트(부분 집합 리스트)\n backtrack(state, target, nums, start, res);\n return res;\n}\n</code></pre> subset_sum_ii.java<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 II */\nvoid backtrack(List&lt;Integer&gt; state, int target, int[] choices, int start, List&lt;List&lt;Integer&gt;&gt; res) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if (target == 0) {\n res.add(new ArrayList&lt;&gt;(state));\n return;\n }\n // 모든 선택지를 순회한다\n // 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n // 가지치기 3: start부터 순회하여 같은 원소의 중복 선택을 방지한다\n for (int i = start; i &lt; choices.length; i++) {\n // 가지치기 1: 부분 집합의 합이 target을 초과하면 즉시 반복문을 종료한다\n // 배열이 정렬되어 있어 뒤의 원소가 더 크므로, 부분 집합의 합이 반드시 target을 초과하기 때문이다\n if (target - choices[i] &lt; 0) {\n break;\n }\n // 가지치기 4: 해당 원소가 왼쪽 원소와 같으면 탐색 분기가 중복되므로 바로 건너뛴다\n if (i &gt; start &amp;&amp; choices[i] == choices[i - 1]) {\n continue;\n }\n // 시도: 선택하고 target, start를 갱신한다\n state.add(choices[i]);\n // 다음 선택을 진행한다\n backtrack(state, target - choices[i], choices, i + 1, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.remove(state.size() - 1);\n }\n}\n\n/* 부분 집합 합 II 풀기 */\nList&lt;List&lt;Integer&gt;&gt; subsetSumII(int[] nums, int target) {\n List&lt;Integer&gt; state = new ArrayList&lt;&gt;(); // 상태(부분 집합)\n Arrays.sort(nums); // nums를 정렬한다\n int start = 0; // 순회 시작 위치\n List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;&gt;(); // 결과 리스트(부분 집합 리스트)\n backtrack(state, target, nums, start, res);\n return res;\n}\n</code></pre> subset_sum_ii.cs<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 II */\nvoid Backtrack(List&lt;int&gt; state, int target, int[] choices, int start, List&lt;List&lt;int&gt;&gt; res) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if (target == 0) {\n res.Add(new List&lt;int&gt;(state));\n return;\n }\n // 모든 선택지를 순회한다\n // 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n // 가지치기 3: start부터 순회하여 같은 원소의 중복 선택을 방지한다\n for (int i = start; i &lt; choices.Length; i++) {\n // 가지치기 1: 부분 집합의 합이 target을 초과하면 즉시 반복문을 종료한다\n // 배열이 정렬되어 있어 뒤의 원소가 더 크므로, 부분 집합의 합이 반드시 target을 초과하기 때문이다\n if (target - choices[i] &lt; 0) {\n break;\n }\n // 가지치기 4: 해당 원소가 왼쪽 원소와 같으면 탐색 분기가 중복되므로 바로 건너뛴다\n if (i &gt; start &amp;&amp; choices[i] == choices[i - 1]) {\n continue;\n }\n // 시도: 선택하고 target, start를 갱신한다\n state.Add(choices[i]);\n // 다음 선택을 진행한다\n Backtrack(state, target - choices[i], choices, i + 1, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.RemoveAt(state.Count - 1);\n }\n}\n\n/* 부분 집합 합 II 풀기 */\nList&lt;List&lt;int&gt;&gt; SubsetSumII(int[] nums, int target) {\n List&lt;int&gt; state = []; // 상태(부분 집합)\n Array.Sort(nums); // nums를 정렬한다\n int start = 0; // 순회 시작 위치\n List&lt;List&lt;int&gt;&gt; res = []; // 결과 리스트(부분 집합 리스트)\n Backtrack(state, target, nums, start, res);\n return res;\n}\n</code></pre> subset_sum_ii.go<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 II */\nfunc backtrackSubsetSumII(start, target int, state, choices *[]int, res *[][]int) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if target == 0 {\n newState := append([]int{}, *state...)\n *res = append(*res, newState)\n return\n }\n // 모든 선택지를 순회한다\n // 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n // 가지치기 3: start부터 순회하여 같은 원소의 중복 선택을 방지한다\n for i := start; i &lt; len(*choices); i++ {\n // 가지치기 1: 부분 집합의 합이 target을 초과하면 즉시 반복문을 종료한다\n // 배열이 정렬되어 있어 뒤의 원소가 더 크므로, 부분 집합의 합이 반드시 target을 초과하기 때문이다\n if target-(*choices)[i] &lt; 0 {\n break\n }\n // 가지치기 4: 해당 원소가 왼쪽 원소와 같으면 탐색 분기가 중복되므로 바로 건너뛴다\n if i &gt; start &amp;&amp; (*choices)[i] == (*choices)[i-1] {\n continue\n }\n // 시도: 선택하고 target, start를 갱신한다\n *state = append(*state, (*choices)[i])\n // 다음 선택을 진행한다\n backtrackSubsetSumII(i+1, target-(*choices)[i], state, choices, res)\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n *state = (*state)[:len(*state)-1]\n }\n}\n\n/* 부분 집합 합 II 풀기 */\nfunc subsetSumII(nums []int, target int) [][]int {\n state := make([]int, 0) // 상태(부분 집합)\n sort.Ints(nums) // nums를 정렬한다\n start := 0 // 순회 시작 위치\n res := make([][]int, 0) // 결과 리스트(부분 집합 리스트)\n backtrackSubsetSumII(start, target, &amp;state, &amp;nums, &amp;res)\n return res\n}\n</code></pre> subset_sum_ii.swift<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 II */\nfunc backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if target == 0 {\n res.append(state)\n return\n }\n // 모든 선택지를 순회한다\n // 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n // 가지치기 3: start부터 순회하여 같은 원소의 중복 선택을 방지한다\n for i in choices.indices.dropFirst(start) {\n // 가지치기 1: 부분 집합의 합이 target을 초과하면 즉시 반복문을 종료한다\n // 배열이 정렬되어 있어 뒤의 원소가 더 크므로, 부분 집합의 합이 반드시 target을 초과하기 때문이다\n if target - choices[i] &lt; 0 {\n break\n }\n // 가지치기 4: 해당 원소가 왼쪽 원소와 같으면 탐색 분기가 중복되므로 바로 건너뛴다\n if i &gt; start, choices[i] == choices[i - 1] {\n continue\n }\n // 시도: 선택하고 target, start를 갱신한다\n state.append(choices[i])\n // 다음 선택을 진행한다\n backtrack(state: &amp;state, target: target - choices[i], choices: choices, start: i + 1, res: &amp;res)\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.removeLast()\n }\n}\n\n/* 부분 집합 합 II 풀기 */\nfunc subsetSumII(nums: [Int], target: Int) -&gt; [[Int]] {\n var state: [Int] = [] // 상태(부분 집합)\n let nums = nums.sorted() // nums를 정렬한다\n let start = 0 // 순회 시작 위치\n var res: [[Int]] = [] // 결과 리스트(부분 집합 리스트)\n backtrack(state: &amp;state, target: target, choices: nums, start: start, res: &amp;res)\n return res\n}\n</code></pre> subset_sum_ii.js<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 II */\nfunction backtrack(state, target, choices, start, res) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if (target === 0) {\n res.push([...state]);\n return;\n }\n // 모든 선택지를 순회한다\n // 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n // 가지치기 3: start부터 순회하여 같은 원소의 중복 선택을 방지한다\n for (let i = start; i &lt; choices.length; i++) {\n // 가지치기 1: 부분 집합의 합이 target을 초과하면 즉시 반복문을 종료한다\n // 배열이 정렬되어 있어 뒤의 원소가 더 크므로, 부분 집합의 합이 반드시 target을 초과하기 때문이다\n if (target - choices[i] &lt; 0) {\n break;\n }\n // 가지치기 4: 해당 원소가 왼쪽 원소와 같으면 탐색 분기가 중복되므로 바로 건너뛴다\n if (i &gt; start &amp;&amp; choices[i] === choices[i - 1]) {\n continue;\n }\n // 시도: 선택하고 target, start를 갱신한다\n state.push(choices[i]);\n // 다음 선택을 진행한다\n backtrack(state, target - choices[i], choices, i + 1, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.pop();\n }\n}\n\n/* 부분 집합 합 II 풀기 */\nfunction subsetSumII(nums, target) {\n const state = []; // 상태(부분 집합)\n nums.sort((a, b) =&gt; a - b); // nums를 정렬한다\n const start = 0; // 순회 시작 위치\n const res = []; // 결과 리스트(부분 집합 리스트)\n backtrack(state, target, nums, start, res);\n return res;\n}\n</code></pre> subset_sum_ii.ts<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 II */\nfunction backtrack(\n state: number[],\n target: number,\n choices: number[],\n start: number,\n res: number[][]\n): void {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if (target === 0) {\n res.push([...state]);\n return;\n }\n // 모든 선택지를 순회한다\n // 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n // 가지치기 3: start부터 순회하여 같은 원소의 중복 선택을 방지한다\n for (let i = start; i &lt; choices.length; i++) {\n // 가지치기 1: 부분 집합의 합이 target을 초과하면 즉시 반복문을 종료한다\n // 배열이 정렬되어 있어 뒤의 원소가 더 크므로, 부분 집합의 합이 반드시 target을 초과하기 때문이다\n if (target - choices[i] &lt; 0) {\n break;\n }\n // 가지치기 4: 해당 원소가 왼쪽 원소와 같으면 탐색 분기가 중복되므로 바로 건너뛴다\n if (i &gt; start &amp;&amp; choices[i] === choices[i - 1]) {\n continue;\n }\n // 시도: 선택하고 target, start를 갱신한다\n state.push(choices[i]);\n // 다음 선택을 진행한다\n backtrack(state, target - choices[i], choices, i + 1, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.pop();\n }\n}\n\n/* 부분 집합 합 II 풀기 */\nfunction subsetSumII(nums: number[], target: number): number[][] {\n const state = []; // 상태(부분 집합)\n nums.sort((a, b) =&gt; a - b); // nums를 정렬한다\n const start = 0; // 순회 시작 위치\n const res = []; // 결과 리스트(부분 집합 리스트)\n backtrack(state, target, nums, start, res);\n return res;\n}\n</code></pre> subset_sum_ii.dart<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 II */\nvoid backtrack(\n List&lt;int&gt; state,\n int target,\n List&lt;int&gt; choices,\n int start,\n List&lt;List&lt;int&gt;&gt; res,\n) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if (target == 0) {\n res.add(List.from(state));\n return;\n }\n // 모든 선택지를 순회한다\n // 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n // 가지치기 3: start부터 순회하여 같은 원소의 중복 선택을 방지한다\n for (int i = start; i &lt; choices.length; i++) {\n // 가지치기 1: 부분 집합의 합이 target을 초과하면 즉시 반복문을 종료한다\n // 배열이 정렬되어 있어 뒤의 원소가 더 크므로, 부분 집합의 합이 반드시 target을 초과하기 때문이다\n if (target - choices[i] &lt; 0) {\n break;\n }\n // 가지치기 4: 해당 원소가 왼쪽 원소와 같으면 탐색 분기가 중복되므로 바로 건너뛴다\n if (i &gt; start &amp;&amp; choices[i] == choices[i - 1]) {\n continue;\n }\n // 시도: 선택하고 target, start를 갱신한다\n state.add(choices[i]);\n // 다음 선택을 진행한다\n backtrack(state, target - choices[i], choices, i + 1, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.removeLast();\n }\n}\n\n/* 부분 집합 합 II 풀기 */\nList&lt;List&lt;int&gt;&gt; subsetSumII(List&lt;int&gt; nums, int target) {\n List&lt;int&gt; state = []; // 상태(부분 집합)\n nums.sort(); // nums를 정렬한다\n int start = 0; // 순회 시작 위치\n List&lt;List&lt;int&gt;&gt; res = []; // 결과 리스트(부분 집합 리스트)\n backtrack(state, target, nums, start, res);\n return res;\n}\n</code></pre> subset_sum_ii.rs<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 II */\nfn backtrack(\n state: &amp;mut Vec&lt;i32&gt;,\n target: i32,\n choices: &amp;[i32],\n start: usize,\n res: &amp;mut Vec&lt;Vec&lt;i32&gt;&gt;,\n) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if target == 0 {\n res.push(state.clone());\n return;\n }\n // 모든 선택지를 순회한다\n // 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n // 가지치기 3: start부터 순회하여 같은 원소의 중복 선택을 방지한다\n for i in start..choices.len() {\n // 가지치기 1: 부분 집합의 합이 target을 초과하면 즉시 반복문을 종료한다\n // 배열이 정렬되어 있어 뒤의 원소가 더 크므로, 부분 집합의 합이 반드시 target을 초과하기 때문이다\n if target - choices[i] &lt; 0 {\n break;\n }\n // 가지치기 4: 해당 원소가 왼쪽 원소와 같으면 탐색 분기가 중복되므로 바로 건너뛴다\n if i &gt; start &amp;&amp; choices[i] == choices[i - 1] {\n continue;\n }\n // 시도: 선택하고 target, start를 갱신한다\n state.push(choices[i]);\n // 다음 선택을 진행한다\n backtrack(state, target - choices[i], choices, i + 1, res);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.pop();\n }\n}\n\n/* 부분 집합 합 II 풀기 */\nfn subset_sum_ii(nums: &amp;mut [i32], target: i32) -&gt; Vec&lt;Vec&lt;i32&gt;&gt; {\n let mut state = Vec::new(); // 상태(부분 집합)\n nums.sort(); // nums를 정렬한다\n let start = 0; // 순회 시작 위치\n let mut res = Vec::new(); // 결과 리스트(부분 집합 리스트)\n backtrack(&amp;mut state, target, nums, start, &amp;mut res);\n res\n}\n</code></pre> subset_sum_ii.c<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 II */\nvoid backtrack(int target, int *choices, int choicesSize, int start) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if (target == 0) {\n for (int i = 0; i &lt; stateSize; i++) {\n res[resSize][i] = state[i];\n }\n resColSizes[resSize++] = stateSize;\n return;\n }\n // 모든 선택지를 순회한다\n // 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n // 가지치기 3: start부터 순회하여 같은 원소의 중복 선택을 방지한다\n for (int i = start; i &lt; choicesSize; i++) {\n // 가지치기 1: 부분 집합의 합이 target을 넘으면 바로 건너뛴다\n if (target - choices[i] &lt; 0) {\n continue;\n }\n // 가지치기 4: 해당 원소가 왼쪽 원소와 같으면 탐색 분기가 중복되므로 바로 건너뛴다\n if (i &gt; start &amp;&amp; choices[i] == choices[i - 1]) {\n continue;\n }\n // 시도: 선택하고 target, start를 갱신한다\n state[stateSize] = choices[i];\n stateSize++;\n // 다음 선택을 진행한다\n backtrack(target - choices[i], choices, choicesSize, i + 1);\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n stateSize--;\n }\n}\n\n/* 부분 집합 합 II 풀기 */\nvoid subsetSumII(int *nums, int numsSize, int target) {\n // nums를 정렬한다\n qsort(nums, numsSize, sizeof(int), cmp);\n // 백트래킹 시작\n backtrack(target, nums, numsSize, 0);\n}\n</code></pre> subset_sum_ii.kt<pre><code>/* 백트래킹 알고리즘: 부분 집합 합 II */\nfun backtrack(\n state: MutableList&lt;Int&gt;,\n target: Int,\n choices: IntArray,\n start: Int,\n res: MutableList&lt;MutableList&lt;Int&gt;?&gt;\n) {\n // 부분 집합의 합이 target과 같으면 해를 기록한다\n if (target == 0) {\n res.add(state.toMutableList())\n return\n }\n // 모든 선택지를 순회한다\n // 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n // 가지치기 3: start부터 순회하여 같은 원소의 중복 선택을 방지한다\n for (i in start..&lt;choices.size) {\n // 가지치기 1: 부분 집합의 합이 target을 초과하면 즉시 반복문을 종료한다\n // 배열이 정렬되어 있어 뒤의 원소가 더 크므로, 부분 집합의 합이 반드시 target을 초과하기 때문이다\n if (target - choices[i] &lt; 0) {\n break\n }\n // 가지치기 4: 해당 원소가 왼쪽 원소와 같으면 탐색 분기가 중복되므로 바로 건너뛴다\n if (i &gt; start &amp;&amp; choices[i] == choices[i - 1]) {\n continue\n }\n // 시도: 선택하고 target, start를 갱신한다\n state.add(choices[i])\n // 다음 선택을 진행한다\n backtrack(state, target - choices[i], choices, i + 1, res)\n // 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.removeAt(state.size - 1)\n }\n}\n\n/* 부분 집합 합 II 풀기 */\nfun subsetSumII(nums: IntArray, target: Int): MutableList&lt;MutableList&lt;Int&gt;?&gt; {\n val state = mutableListOf&lt;Int&gt;() // 상태(부분 집합)\n nums.sort() // nums를 정렬한다\n val start = 0 // 순회 시작 위치\n val res = mutableListOf&lt;MutableList&lt;Int&gt;?&gt;() // 결과 리스트(부분 집합 리스트)\n backtrack(state, target, nums, start, res)\n return res\n}\n</code></pre> subset_sum_ii.rb<pre><code>### 백트래킹 알고리즘: 부분 집합 합 II ###\ndef backtrack(state, target, choices, start, res)\n # 부분 집합의 합이 target과 같으면 해를 기록한다\n if target.zero?\n res &lt;&lt; state.dup\n return\n end\n\n # 모든 선택지를 순회한다\n # 가지치기 2: start부터 순회하여 중복 부분 집합 생성을 방지한다\n # 가지치기 3: start부터 순회하여 같은 원소의 중복 선택을 방지한다\n for i in start...choices.length\n # 가지치기 1: 부분 집합의 합이 target을 초과하면 즉시 반복문을 종료한다\n # 배열이 정렬되어 있어 뒤의 원소가 더 크므로, 부분 집합의 합이 반드시 target을 초과하기 때문이다\n break if target - choices[i] &lt; 0\n # 가지치기 4: 해당 원소가 왼쪽 원소와 같으면 탐색 분기가 중복되므로 바로 건너뛴다\n next if i &gt; start &amp;&amp; choices[i] == choices[i - 1]\n # 시도: 선택하고 target, start를 갱신한다\n state &lt;&lt; choices[i]\n # 다음 선택을 진행한다\n backtrack(state, target - choices[i], choices, i + 1, res)\n # 되돌리기: 선택을 취소하고 이전 상태로 되돌린다\n state.pop\n end\nend\n\n### 부분 집합 합 II 풀기 ###\ndef subset_sum_ii(nums, target)\n state = [] # 상태(부분 집합)\n nums.sort! # nums를 정렬한다\n start = 0 # 순회 시작 위치\n res = [] # 결과 리스트(부분 집합 리스트)\n backtrack(state, target, nums, start, res)\n res\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>아래 그림(13-14)은 배열 \\([4, 4, 5]\\)와 목표 원소 \\(9\\)의 백트래킹 과정으로, 네 가지 가지치기를 포함한다. 그림과 코드 주석을 함께 보면서 전체 탐색 과정과 각 가지치기가 어떻게 작동하는지 이해해 보자.</p> <p></p> <p> 그림 13-14   부분 집합 합 II의 백트래킹 과정 </p>","path":["제 13 장   백트래킹","13.3   부분 집합 합 문제"],"tags":[]},{"location":"chapter_backtracking/summary/","level":1,"title":"13.5   요약","text":"","path":["제 13 장   백트래킹","13.5   요약"],"tags":[]},{"location":"chapter_backtracking/summary/#1","level":3,"title":"1.   핵심 정리","text":"<ul> <li>백트래킹은 본질적으로 모든 경우를 열거하는 방법이다. 해 공간을 깊이 우선으로 순회하여 조건에 맞는 해를 찾고 기록하며, 모든 해를 찾거나 순회가 끝나면 종료한다.</li> <li>백트래킹의 탐색 과정은 시도와 되돌리기로 이루어진다. 깊이 우선 탐색으로 여러 선택을 시도하고, 제약 조건을 만족하지 않으면 직전 선택을 취소하여 이전 상태로 돌아간 뒤 다른 선택을 계속 시도한다. 시도와 되돌리기는 서로 반대 방향의 연산이다.</li> <li>백트래킹 문제에는 보통 여러 제약 조건이 있고, 이를 가지치기에 사용할 수 있다. 가지치기는 불필요한 탐색 가지를 일찍 종료하여 효율을 크게 높인다.</li> <li>백트래킹은 주로 탐색 문제와 제약 만족 문제에 사용한다. 조합 최적화 문제도 해결할 수 있지만, 더 효율적이거나 더 좋은 결과를 내는 풀이가 있는 경우가 많다.</li> <li>모든 순열 문제는 주어진 집합 원소들의 가능한 모든 순열을 찾는 것이 목표다. 배열로 각 원소의 선택 여부를 기록하고 동일 원소를 중복 선택하는 가지를 잘라 내어 각 원소를 한 번만 선택하도록 한다.</li> <li>모든 순열 문제에서 집합에 중복 원소가 있으면 결과에 중복된 순열이 생긴다. 각 차례에서 같은 값의 원소를 한 번만 선택하도록 제한해야 하며, 보통 해시 집합으로 구현한다.</li> <li>부분 집합 합 문제는 주어진 집합에서 합이 목표 값인 모든 부분 집합을 찾는다. 집합은 원소 순서를 구분하지 않지만 탐색은 모든 순서의 결과를 출력하므로 중복이 생긴다. 백트래킹 전에 데이터를 정렬하고 각 차례의 순회 시작점을 변수로 지정하여 중복 부분 집합을 만드는 가지를 잘라 낸다.</li> <li>부분 집합 합 문제에서는 배열의 같은 값 원소가 중복 집합을 만들 수 있다. 배열이 정렬되어 있다는 전제를 이용해 인접 원소의 값이 같은지 확인하여 가지치기하고, 같은 값을 매 차례 한 번만 선택하도록 한다.</li> <li>\\(n\\)-퀸 문제는 \\(n \\times n\\) 체스판에 퀸 \\(n\\) 개를 서로 공격하지 않게 배치하는 문제다. 행, 열, 주대각선 방향, 부대각선 방향의 제약이 있다. 행 제약을 만족하려고 행별 배치 전략을 사용하여 각 행에 퀸 하나를 놓는다.</li> <li>열 제약과 대각선 제약은 비슷하게 처리한다. 열 제약은 배열에 각 열의 퀸 존재 여부를 기록하여 선택한 칸이 유효한지 판단한다. 대각선 제약은 두 배열로 각 방향 대각선의 퀸 존재 여부를 기록한다. 핵심은 같은 주대각선(부대각선) 방향 대각선의 칸들이 만족하는 행과 열 인덱스의 규칙을 찾는 것이다.</li> </ul>","path":["제 13 장   백트래킹","13.5   요약"],"tags":[]},{"location":"chapter_backtracking/summary/#2-q-a","level":3,"title":"2.   Q &amp; A","text":"<p>Q: 백트래킹과 재귀의 관계는 어떻게 이해하면 되는가?</p> <p>백트래킹은 '알고리즘 전략'이고, 재귀는 '도구'에 더 가깝다.</p> <ul> <li>백트래킹은 보통 재귀로 구현한다. 백트래킹은 재귀의 활용 사례 중 하나이며, 탐색 문제에 재귀를 적용한 것이다.</li> <li>재귀의 구조는 '부분 문제로 분해'하는 풀이 패턴을 보여 주며, 분할 정복, 백트래킹, 동적 계획법(메모이제이션을 이용한 재귀) 등의 문제에 자주 사용한다.</li> </ul>","path":["제 13 장   백트래킹","13.5   요약"],"tags":[]},{"location":"chapter_computational_complexity/","level":1,"title":"제 2 장   복잡도 분석","text":"<p>이 장을 시작하며</p> <p>복잡도 분석은 광활한 알고리즘 우주에서 시간과 공간을 안내하는 길잡이와 같다.</p> <p>시간과 공간이라는 두 차원을 깊이 탐구하며 더 우아한 해법을 찾도록 이끌어 준다.</p>","path":["제 2 장   복잡도 분석"],"tags":[]},{"location":"chapter_computational_complexity/#_1","level":2,"title":"이 장의 내용","text":"<ul> <li>2.1   알고리즘 효율 평가</li> <li>2.2   반복과 재귀</li> <li>2.3   시간 복잡도</li> <li>2.4   공간 복잡도</li> <li>2.5   요약</li> <li>2.6   연습 문제</li> </ul>","path":["제 2 장   복잡도 분석"],"tags":[]},{"location":"chapter_computational_complexity/exercises/","level":1,"title":"2.6   연습 문제","text":"","path":["제 2 장   복잡도 분석","2.6   연습 문제"],"tags":[]},{"location":"chapter_computational_complexity/exercises/#261","level":2,"title":"2.6.1   개념 확인","text":"","path":["제 2 장   복잡도 분석","2.6   연습 문제"],"tags":[]},{"location":"chapter_computational_complexity/exercises/#1","level":3,"title":"1.   반복과 재귀의 시간과 공간","text":"<p>다음 두 코드는 모두 \\(1 + 2 + \\dots + n\\)을 계산한다(\\(n \\ge 1\\)로 가정한다). <code>n</code>을 4로 설정하고, 프로그램이 실제로 실행되는 순서에 따라 질문에 답한 뒤 두 방식의 효율성을 비교해 보자.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby complexity_exercises.py<pre><code>def sum_iter(n: int) -&gt; int:\n \"\"\"반복으로 합 구하기\"\"\"\n res = 0\n for i in range(1, n + 1):\n res += i\n return res\n\ndef sum_recur(n: int) -&gt; int:\n \"\"\"재귀로 합 구하기\"\"\"\n if n == 1:\n return 1\n return n + sum_recur(n - 1)\n</code></pre> complexity_exercises.cpp<pre><code>/* 반복으로 합 구하기 */\nint sumIter(int n) {\n int res = 0;\n for (int i = 1; i &lt;= n; ++i) {\n res += i;\n }\n return res;\n}\n\n/* 재귀로 합 구하기 */\nint sumRecur(int n) {\n if (n == 1) {\n return 1;\n }\n return n + sumRecur(n - 1);\n}\n</code></pre> complexity_exercises.java<pre><code>/* 반복으로 합 구하기 */\nint sumIter(int n) {\n int res = 0;\n for (int i = 1; i &lt;= n; i++) {\n res += i;\n }\n return res;\n}\n\n/* 재귀로 합 구하기 */\nint sumRecur(int n) {\n if (n == 1) {\n return 1;\n }\n return n + sumRecur(n - 1);\n}\n</code></pre> complexity_exercises.cs<pre><code>/* 반복으로 합 구하기 */\nint SumIter(int n) {\n int res = 0;\n for (int i = 1; i &lt;= n; i++) {\n res += i;\n }\n return res;\n}\n\n/* 재귀로 합 구하기 */\nint SumRecur(int n) {\n if (n == 1) {\n return 1;\n }\n return n + SumRecur(n - 1);\n}\n</code></pre> complexity_exercises.go<pre><code>/* 반복으로 합 구하기 */\nfunc sumIter(n int) int {\n res := 0\n for i := 1; i &lt;= n; i++ {\n res += i\n }\n return res\n}\n\n/* 재귀로 합 구하기 */\nfunc sumRecur(n int) int {\n if n == 1 {\n return 1\n }\n return n + sumRecur(n-1)\n}\n</code></pre> complexity_exercises.swift<pre><code>/* 반복으로 합 구하기 */\nfunc sumIter(n: Int) -&gt; Int {\n var res = 0\n for i in 1 ... n {\n res += i\n }\n return res\n}\n\n/* 재귀로 합 구하기 */\nfunc sumRecur(n: Int) -&gt; Int {\n if n == 1 {\n return 1\n }\n return n + sumRecur(n: n - 1)\n}\n</code></pre> complexity_exercises.js<pre><code>/* 반복으로 합 구하기 */\nfunction sumIter(n) {\n let res = 0;\n for (let i = 1; i &lt;= n; i++) {\n res += i;\n }\n return res;\n}\n\n/* 재귀로 합 구하기 */\nfunction sumRecur(n) {\n if (n === 1) {\n return 1;\n }\n return n + sumRecur(n - 1);\n}\n</code></pre> complexity_exercises.ts<pre><code>/* 반복으로 합 구하기 */\nfunction sumIter(n: number): number {\n let res = 0;\n for (let i = 1; i &lt;= n; i++) {\n res += i;\n }\n return res;\n}\n\n/* 재귀로 합 구하기 */\nfunction sumRecur(n: number): number {\n if (n === 1) {\n return 1;\n }\n return n + sumRecur(n - 1);\n}\n</code></pre> complexity_exercises.dart<pre><code>/* 반복으로 합 구하기 */\nint sumIter(int n) {\n int res = 0;\n for (int i = 1; i &lt;= n; i++) {\n res += i;\n }\n return res;\n}\n\n/* 재귀로 합 구하기 */\nint sumRecur(int n) {\n if (n == 1) {\n return 1;\n }\n return n + sumRecur(n - 1);\n}\n</code></pre> complexity_exercises.rs<pre><code>/* 반복으로 합 구하기 */\nfn sum_iter(n: i32) -&gt; i32 {\n let mut res = 0;\n for i in 1..=n {\n res += i;\n }\n res\n}\n\n/* 재귀로 합 구하기 */\nfn sum_recur(n: i32) -&gt; i32 {\n if n == 1 {\n return 1;\n }\n n + sum_recur(n - 1)\n}\n</code></pre> complexity_exercises.c<pre><code>/* 반복으로 합 구하기 */\nint sumIter(int n) {\n int res = 0;\n for (int i = 1; i &lt;= n; i++) {\n res += i;\n }\n return res;\n}\n\n/* 재귀로 합 구하기 */\nint sumRecur(int n) {\n if (n == 1) {\n return 1;\n }\n return n + sumRecur(n - 1);\n}\n</code></pre> complexity_exercises.kt<pre><code>/* 반복으로 합 구하기 */\nfun sumIter(n: Int): Int {\n var res = 0\n for (i in 1..n) {\n res += i\n }\n return res\n}\n\n/* 재귀로 합 구하기 */\nfun sumRecur(n: Int): Int {\n if (n == 1) {\n return 1\n }\n return n + sumRecur(n - 1)\n}\n</code></pre> complexity_exercises.rb<pre><code>### 반복으로 합 구하기 ###\ndef sum_iter(n)\n res = 0\n for i in 1..n\n res += i\n end\n res\nend\n\n### 재귀로 합 구하기 ###\ndef sum_recur(n)\n return 1 if n == 1\n\n n + sum_recur(n - 1)\nend\n</code></pre> <ol> <li><code>n = 4</code>를 입력하여 반복 함수를 실행할 때, 각 반복이 끝난 뒤 누적 변수 <code>res</code>의 값은 각각 얼마인가?</li> <li><code>n = 4</code>를 입력하여 재귀 함수를 실행할 때, 매개변수 <code>n</code>은 어떤 값을 순서대로 갖는가? 가장 깊은 호출부터 반환할 때 결과는 어떻게 구해지는가?</li> <li>두 방식의 시간 복잡도와 공간 복잡도는 각각 얼마인가? 1번과 2번의 실행 과정을 바탕으로 이유를 설명하라.</li> </ol> 참고 답안 <ol> <li> <p>반복 변수 <code>i</code>는 차례대로 <code>1, 2, 3, 4</code>가 되고, 각 반복이 끝날 때 <code>res</code>는 <code>1, 3, 6, 10</code>으로 변한다. 따라서 반복 함수는 10을 반환한다.</p> </li> <li> <p>매개변수 <code>n</code>은 <code>4 → 3 → 2 → 1</code> 순서로 변한다. 가장 깊은 호출이 1을 반환한 뒤, 각 호출은 차례대로 <code>2 + 1 = 3</code>, <code>3 + 3 = 6</code>, <code>4 + 6 = 10</code>을 얻는다. 가장 깊은 지점에서는 4번의 함수 호출이 모두 아직 끝나지 않은 상태다.</p> </li> <li> <p>두 코드 모두 \\(n\\)에 비례하는 횟수만큼 반복하거나 호출하므로 시간 복잡도는 모두 \\(O(n)\\)이다. 공간 복잡도는 다르다. 반복 방식은 상수 개의 변수만 사용하므로 \\(O(1)\\)이다. 재귀 방식은 종료 조건에 도달할 때까지 앞선 호출들이 반환값을 기다려야 하므로 호출 스택에 최대 \\(n\\)번의 호출이 동시에 저장된다. 따라서 공간 복잡도는 \\(O(n)\\)이다.</p> <p>공간 복잡도를 분석할 때는 코드에 선언된 변수뿐 아니라 재귀 호출이 사용하는 공간도 고려해야 한다.</p> </li> </ol>","path":["제 2 장   복잡도 분석","2.6   연습 문제"],"tags":[]},{"location":"chapter_computational_complexity/exercises/#2","level":3,"title":"2.   세 코드의 시간 복잡도","text":"<p>다음 세 코드 조각은 모두 양의 정수 \\(n\\)을 입력받는다. 시간 복잡도가 낮은 순서대로 나열하고 각각의 복잡도를 적어 보자.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby complexity_exercises.py<pre><code>def linear_loop(n: int) -&gt; int:\n \"\"\"선형 차수 반복문\"\"\"\n res = 0\n for i in range(n):\n res += i\n return res\n\ndef quadratic_loop(n: int) -&gt; int:\n \"\"\"제곱 차수 반복문\"\"\"\n res = 0\n for i in range(n):\n for j in range(i, n):\n res += j\n return res\n\ndef logarithmic_loop(n: int) -&gt; int:\n \"\"\"로그 차수 반복문\"\"\"\n while n &gt; 1:\n n //= 2\n return n\n</code></pre> complexity_exercises.cpp<pre><code>/* 선형 차수 반복문 */\nint linearLoop(int n) {\n int res = 0;\n for (int i = 0; i &lt; n; ++i) {\n res += i;\n }\n return res;\n}\n\n/* 제곱 차수 반복문 */\nint quadraticLoop(int n) {\n int res = 0;\n for (int i = 0; i &lt; n; ++i) {\n for (int j = i; j &lt; n; ++j) {\n res += j;\n }\n }\n return res;\n}\n\n/* 로그 차수 반복문 */\nint logarithmicLoop(int n) {\n while (n &gt; 1) {\n n /= 2;\n }\n return n;\n}\n</code></pre> complexity_exercises.java<pre><code>/* 선형 차수 반복문 */\nint linearLoop(int n) {\n int res = 0;\n for (int i = 0; i &lt; n; i++) {\n res += i;\n }\n return res;\n}\n\n/* 제곱 차수 반복문 */\nint quadraticLoop(int n) {\n int res = 0;\n for (int i = 0; i &lt; n; i++) {\n for (int j = i; j &lt; n; j++) {\n res += j;\n }\n }\n return res;\n}\n\n/* 로그 차수 반복문 */\nint logarithmicLoop(int n) {\n while (n &gt; 1) {\n n /= 2;\n }\n return n;\n}\n</code></pre> complexity_exercises.cs<pre><code>/* 선형 차수 반복문 */\nint LinearLoop(int n) {\n int res = 0;\n for (int i = 0; i &lt; n; i++) {\n res += i;\n }\n return res;\n}\n\n/* 제곱 차수 반복문 */\nint QuadraticLoop(int n) {\n int res = 0;\n for (int i = 0; i &lt; n; i++) {\n for (int j = i; j &lt; n; j++) {\n res += j;\n }\n }\n return res;\n}\n\n/* 로그 차수 반복문 */\nint LogarithmicLoop(int n) {\n while (n &gt; 1) {\n n /= 2;\n }\n return n;\n}\n</code></pre> complexity_exercises.go<pre><code>/* 선형 차수 반복문 */\nfunc linearLoop(n int) int {\n res := 0\n for i := 0; i &lt; n; i++ {\n res += i\n }\n return res\n}\n\n/* 제곱 차수 반복문 */\nfunc quadraticLoop(n int) int {\n res := 0\n for i := 0; i &lt; n; i++ {\n for j := i; j &lt; n; j++ {\n res += j\n }\n }\n return res\n}\n\n/* 로그 차수 반복문 */\nfunc logarithmicLoop(n int) int {\n for n &gt; 1 {\n n /= 2\n }\n return n\n}\n</code></pre> complexity_exercises.swift<pre><code>/* 선형 차수 반복문 */\nfunc linearLoop(n: Int) -&gt; Int {\n var res = 0\n for i in 0 ..&lt; n {\n res += i\n }\n return res\n}\n\n/* 제곱 차수 반복문 */\nfunc quadraticLoop(n: Int) -&gt; Int {\n var res = 0\n for i in 0 ..&lt; n {\n for j in i ..&lt; n {\n res += j\n }\n }\n return res\n}\n\n/* 로그 차수 반복문 */\nfunc logarithmicLoop(n: Int) -&gt; Int {\n var n = n\n while n &gt; 1 {\n n /= 2\n }\n return n\n}\n</code></pre> complexity_exercises.js<pre><code>/* 선형 차수 반복문 */\nfunction linearLoop(n) {\n let res = 0;\n for (let i = 0; i &lt; n; i++) {\n res += i;\n }\n return res;\n}\n\n/* 제곱 차수 반복문 */\nfunction quadraticLoop(n) {\n let res = 0;\n for (let i = 0; i &lt; n; i++) {\n for (let j = i; j &lt; n; j++) {\n res += j;\n }\n }\n return res;\n}\n\n/* 로그 차수 반복문 */\nfunction logarithmicLoop(n) {\n while (n &gt; 1) {\n n = Math.floor(n / 2);\n }\n return n;\n}\n</code></pre> complexity_exercises.ts<pre><code>/* 선형 차수 반복문 */\nfunction linearLoop(n: number): number {\n let res = 0;\n for (let i = 0; i &lt; n; i++) {\n res += i;\n }\n return res;\n}\n\n/* 제곱 차수 반복문 */\nfunction quadraticLoop(n: number): number {\n let res = 0;\n for (let i = 0; i &lt; n; i++) {\n for (let j = i; j &lt; n; j++) {\n res += j;\n }\n }\n return res;\n}\n\n/* 로그 차수 반복문 */\nfunction logarithmicLoop(n: number): number {\n while (n &gt; 1) {\n n = Math.floor(n / 2);\n }\n return n;\n}\n</code></pre> complexity_exercises.dart<pre><code>/* 선형 차수 반복문 */\nint linearLoop(int n) {\n int res = 0;\n for (int i = 0; i &lt; n; i++) {\n res += i;\n }\n return res;\n}\n\n/* 제곱 차수 반복문 */\nint quadraticLoop(int n) {\n int res = 0;\n for (int i = 0; i &lt; n; i++) {\n for (int j = i; j &lt; n; j++) {\n res += j;\n }\n }\n return res;\n}\n\n/* 로그 차수 반복문 */\nint logarithmicLoop(int n) {\n while (n &gt; 1) {\n n ~/= 2;\n }\n return n;\n}\n</code></pre> complexity_exercises.rs<pre><code>/* 선형 차수 반복문 */\nfn linear_loop(n: i32) -&gt; i32 {\n let mut res = 0;\n for i in 0..n {\n res += i;\n }\n res\n}\n\n/* 제곱 차수 반복문 */\nfn quadratic_loop(n: i32) -&gt; i32 {\n let mut res = 0;\n for i in 0..n {\n for j in i..n {\n res += j;\n }\n }\n res\n}\n\n/* 로그 차수 반복문 */\nfn logarithmic_loop(mut n: i32) -&gt; i32 {\n while n &gt; 1 {\n n /= 2;\n }\n n\n}\n</code></pre> complexity_exercises.c<pre><code>/* 선형 차수 반복문 */\nint linearLoop(int n) {\n int res = 0;\n for (int i = 0; i &lt; n; i++) {\n res += i;\n }\n return res;\n}\n\n/* 제곱 차수 반복문 */\nint quadraticLoop(int n) {\n int res = 0;\n for (int i = 0; i &lt; n; i++) {\n for (int j = i; j &lt; n; j++) {\n res += j;\n }\n }\n return res;\n}\n\n/* 로그 차수 반복문 */\nint logarithmicLoop(int n) {\n while (n &gt; 1) {\n n /= 2;\n }\n return n;\n}\n</code></pre> complexity_exercises.kt<pre><code>/* 선형 차수 반복문 */\nfun linearLoop(n: Int): Int {\n var res = 0\n for (i in 0 until n) {\n res += i\n }\n return res\n}\n\n/* 제곱 차수 반복문 */\nfun quadraticLoop(n: Int): Int {\n var res = 0\n for (i in 0 until n) {\n for (j in i until n) {\n res += j\n }\n }\n return res\n}\n\n/* 로그 차수 반복문 */\nfun logarithmicLoop(n: Int): Int {\n var value = n\n while (value &gt; 1) {\n value /= 2\n }\n return value\n}\n</code></pre> complexity_exercises.rb<pre><code>### 선형 차수 반복문 ###\ndef linear_loop(n)\n res = 0\n for i in 0...n\n res += i\n end\n res\nend\n\n### 제곱 차수 반복문 ###\ndef quadratic_loop(n)\n res = 0\n for i in 0...n\n for j in i...n\n res += j\n end\n end\n res\nend\n\n### 로그 차수 반복문 ###\ndef logarithmic_loop(n)\n n /= 2 while n &gt; 1\n n\nend\n</code></pre> 참고 답안 <p>낮은 순서대로 코드 3은 \\(O(\\log n)\\), 코드 1은 \\(O(n)\\), 코드 2는 \\(O(n^2)\\)이다. 코드 3은 매번 \\(n\\)을 절반으로 줄이므로 반복 횟수는 약 \\(\\log_2 n\\)번이다. 코드 1의 반복문은 정확히 \\(n\\)번 실행된다. 코드 2의 안쪽 반복 횟수는 차례대로 \\(n,n-1,\\dots,1\\)이므로 총 \\(n(n+1)/2\\)번이며, 제곱 차수에 해당한다.</p>","path":["제 2 장   복잡도 분석","2.6   연습 문제"],"tags":[]},{"location":"chapter_computational_complexity/exercises/#3","level":3,"title":"3.   어느 뒤집기 방식이 공간을 더 절약하는가","text":"<p>배열 <code>nums</code>의 모든 원소를 역순으로 뒤집는 두 가지 방법이 있다.</p> <ol> <li>길이가 같은 새 배열 <code>res</code>를 만들고 원소를 역순으로 복사한 뒤 반환한다.</li> <li> <p>두 인덱스 <code>i</code>와 <code>j</code>를 각각 처음과 끝에서 가운데로 이동시키며 <code>nums[i]</code>와 <code>nums[j]</code>를 쌍으로 교환한다.</p> <p>두 방법의 공간 복잡도는 각각 얼마인가? 어느 방법이 ‘제자리’ 연산에 해당하는가?</p> </li> </ol> 참고 답안 <ol> <li> <p>입력과 길이가 같은 보조 배열이 필요하므로 공간 복잡도는 \\(O(n)\\)이다.</p> </li> <li> <p>인덱스 변수 두 개만 사용하므로 공간 복잡도는 \\(O(1)\\)이며, 제자리 연산이다.</p> <p>주의할 점은 제자리 뒤집기가 입력 배열을 변경한다는 것이다. 입력을 변경해도 되는 경우에만 우선적으로 선택해야 한다. 원본 배열을 보존해야 한다면 첫 번째 방법의 복사 비용은 피할 수 없다.</p> </li> </ol>","path":["제 2 장   복잡도 분석","2.6   연습 문제"],"tags":[]},{"location":"chapter_computational_complexity/exercises/#262","level":2,"title":"2.6.2   프로그래밍 연습","text":"","path":["제 2 장   복잡도 분석","2.6   연습 문제"],"tags":[]},{"location":"chapter_computational_complexity/exercises/#1_1","level":3,"title":"1.   피보나치 수","text":"<p>피보나치 수열은 \\(F(0)=0\\), \\(F(1)=1\\)이며, \\(n\\ge2\\)일 때 \\(F(n)=F(n-1)+F(n-2)\\)를 만족한다.</p> <p>음이 아닌 정수 <code>n</code>이 주어질 때, 재귀를 사용하지 않고 반복문으로 \\(F(n)\\)을 계산하여 반환하라.</p> 풀이 힌트 <ol> <li>n이 0 또는 1인 경우를 먼저 따로 처리한다.</li> <li>다음 항을 계산할 때는 앞선 두 항만 필요하므로 수열 전체를 저장할 필요가 없다.</li> <li>두 변수를 갱신할 때 아직 사용해야 하는 이전 값을 너무 일찍 덮어쓰지 않도록 주의한다.</li> </ol> <p>LeetCode</p>","path":["제 2 장   복잡도 분석","2.6   연습 문제"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/","level":1,"title":"2.2   반복과 재귀","text":"<p>알고리즘에서는 특정 작업을 반복해서 실행하는 일이 흔하며, 이는 복잡도 분석과 밀접하게 연결되어 있다. 따라서 시간 복잡도와 공간 복잡도를 소개하기 전에, 프로그램에서 작업을 반복 실행하는 두 가지 기본 제어 구조인 반복과 재귀부터 알아보자.</p>","path":["제 2 장   복잡도 분석","2.2   반복과 재귀"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#221","level":2,"title":"2.2.1   반복","text":"<p>반복(iteration)은 특정 작업을 거듭 실행하는 제어 구조다. 반복에서는 일정한 조건을 만족하는 동안 코드 일부를 계속 실행하다가, 조건을 더 이상 만족하지 않으면 멈춘다.</p>","path":["제 2 장   복잡도 분석","2.2   반복과 재귀"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#1-for","level":3,"title":"1.   for 반복문","text":"<p><code>for</code> 반복문은 가장 흔한 반복 형식 중 하나로, 반복 횟수를 미리 알고 있을 때 적합하다.</p> <p>다음 함수는 <code>for</code> 반복문으로 \\(1 + 2 + \\dots + n\\)을 계산하고, 그 합을 변수 <code>res</code>에 기록한다. Python의 <code>range(a, b)</code>는 왼쪽 끝을 포함하고 오른쪽 끝은 포함하지 않는 구간이므로, 순회 범위는 \\(a, a + 1, \\dots, b-1\\)이다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py<pre><code>def for_loop(n: int) -&gt; int:\n \"\"\"for 반복문\"\"\"\n res = 0\n # 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n for i in range(1, n + 1):\n res += i\n return res\n</code></pre> iteration.cpp<pre><code>/* for 반복문 */\nint forLoop(int n) {\n int res = 0;\n // 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n for (int i = 1; i &lt;= n; ++i) {\n res += i;\n }\n return res;\n}\n</code></pre> iteration.java<pre><code>/* for 반복문 */\nint forLoop(int n) {\n int res = 0;\n // 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n for (int i = 1; i &lt;= n; i++) {\n res += i;\n }\n return res;\n}\n</code></pre> iteration.cs<pre><code>/* for 반복문 */\nint ForLoop(int n) {\n int res = 0;\n // 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n for (int i = 1; i &lt;= n; i++) {\n res += i;\n }\n return res;\n}\n</code></pre> iteration.go<pre><code>/* for 반복문 */\nfunc forLoop(n int) int {\n res := 0\n // 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n for i := 1; i &lt;= n; i++ {\n res += i\n }\n return res\n}\n</code></pre> iteration.swift<pre><code>/* for 반복문 */\nfunc forLoop(n: Int) -&gt; Int {\n var res = 0\n // 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n for i in 1 ... n {\n res += i\n }\n return res\n}\n</code></pre> iteration.js<pre><code>/* for 반복문 */\nfunction forLoop(n) {\n let res = 0;\n // 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n for (let i = 1; i &lt;= n; i++) {\n res += i;\n }\n return res;\n}\n</code></pre> iteration.ts<pre><code>/* for 반복문 */\nfunction forLoop(n: number): number {\n let res = 0;\n // 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n for (let i = 1; i &lt;= n; i++) {\n res += i;\n }\n return res;\n}\n</code></pre> iteration.dart<pre><code>/* for 반복문 */\nint forLoop(int n) {\n int res = 0;\n // 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n for (int i = 1; i &lt;= n; i++) {\n res += i;\n }\n return res;\n}\n</code></pre> iteration.rs<pre><code>/* for 반복문 */\nfn for_loop(n: i32) -&gt; i32 {\n let mut res = 0;\n // 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n for i in 1..=n {\n res += i;\n }\n res\n}\n</code></pre> iteration.c<pre><code>/* for 반복문 */\nint forLoop(int n) {\n int res = 0;\n // 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n for (int i = 1; i &lt;= n; i++) {\n res += i;\n }\n return res;\n}\n</code></pre> iteration.kt<pre><code>/* for 반복문 */\nfun forLoop(n: Int): Int {\n var res = 0\n // 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n for (i in 1..n) {\n res += i\n }\n return res\n}\n</code></pre> iteration.rb<pre><code>### for 반복문 ###\ndef for_loop(n)\n res = 0\n\n # 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n for i in 1..n\n res += i\n end\n\n res\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>아래 그림(2-1)은 이 합계 함수의 순서도다.</p> <p></p> <p> 그림 2-1   합계 함수의 순서도 </p> <p>이 합계 함수의 연산 횟수는 입력 데이터 크기 \\(n\\)에 비례한다. 이를 ‘선형 관계’라고도 한다. 실제로 시간 복잡도가 설명하는 것이 바로 이 ‘선형 관계’다. 관련 내용은 다음 절에서 자세히 소개한다.</p>","path":["제 2 장   복잡도 분석","2.2   반복과 재귀"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#2-while","level":3,"title":"2.   while 반복문","text":"<p><code>for</code> 반복문과 마찬가지로 <code>while</code> 반복문도 반복을 구현하는 방법이다. <code>while</code> 반복문은 매번 먼저 조건을 검사하여 참이면 실행을 계속하고, 그렇지 않으면 반복을 끝낸다.</p> <p>다음은 <code>while</code> 반복문으로 \\(1 + 2 + \\dots + n\\)을 계산하는 코드다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py<pre><code>def while_loop(n: int) -&gt; int:\n \"\"\"while 반복문\"\"\"\n res = 0\n i = 1 # 조건 변수를 초기화한다\n # 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n while i &lt;= n:\n res += i\n i += 1 # 조건 변수를 갱신한다\n return res\n</code></pre> iteration.cpp<pre><code>/* while 반복문 */\nint whileLoop(int n) {\n int res = 0;\n int i = 1; // 조건 변수를 초기화한다\n // 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n while (i &lt;= n) {\n res += i;\n i++; // 조건 변수를 갱신한다\n }\n return res;\n}\n</code></pre> iteration.java<pre><code>/* while 반복문 */\nint whileLoop(int n) {\n int res = 0;\n int i = 1; // 조건 변수를 초기화한다\n // 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n while (i &lt;= n) {\n res += i;\n i++; // 조건 변수를 갱신한다\n }\n return res;\n}\n</code></pre> iteration.cs<pre><code>/* while 반복문 */\nint WhileLoop(int n) {\n int res = 0;\n int i = 1; // 조건 변수를 초기화한다\n // 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n while (i &lt;= n) {\n res += i;\n i += 1; // 조건 변수를 갱신한다\n }\n return res;\n}\n</code></pre> iteration.go<pre><code>/* while 반복문 */\nfunc whileLoop(n int) int {\n res := 0\n // 조건 변수를 초기화한다\n i := 1\n // 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n for i &lt;= n {\n res += i\n // 조건 변수를 갱신한다\n i++\n }\n return res\n}\n</code></pre> iteration.swift<pre><code>/* while 반복문 */\nfunc whileLoop(n: Int) -&gt; Int {\n var res = 0\n var i = 1 // 조건 변수를 초기화한다\n // 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n while i &lt;= n {\n res += i\n i += 1 // 조건 변수를 갱신한다\n }\n return res\n}\n</code></pre> iteration.js<pre><code>/* while 반복문 */\nfunction whileLoop(n) {\n let res = 0;\n let i = 1; // 조건 변수를 초기화한다\n // 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n while (i &lt;= n) {\n res += i;\n i++; // 조건 변수를 갱신한다\n }\n return res;\n}\n</code></pre> iteration.ts<pre><code>/* while 반복문 */\nfunction whileLoop(n: number): number {\n let res = 0;\n let i = 1; // 조건 변수를 초기화한다\n // 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n while (i &lt;= n) {\n res += i;\n i++; // 조건 변수를 갱신한다\n }\n return res;\n}\n</code></pre> iteration.dart<pre><code>/* while 반복문 */\nint whileLoop(int n) {\n int res = 0;\n int i = 1; // 조건 변수를 초기화한다\n // 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n while (i &lt;= n) {\n res += i;\n i++; // 조건 변수를 갱신한다\n }\n return res;\n}\n</code></pre> iteration.rs<pre><code>/* while 반복문 */\nfn while_loop(n: i32) -&gt; i32 {\n let mut res = 0;\n let mut i = 1; // 조건 변수를 초기화한다\n\n // 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n while i &lt;= n {\n res += i;\n i += 1; // 조건 변수를 갱신한다\n }\n res\n}\n</code></pre> iteration.c<pre><code>/* while 반복문 */\nint whileLoop(int n) {\n int res = 0;\n int i = 1; // 조건 변수를 초기화한다\n // 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n while (i &lt;= n) {\n res += i;\n i++; // 조건 변수를 갱신한다\n }\n return res;\n}\n</code></pre> iteration.kt<pre><code>/* while 반복문 */\nfun whileLoop(n: Int): Int {\n var res = 0\n var i = 1 // 조건 변수를 초기화한다\n // 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n while (i &lt;= n) {\n res += i\n i++ // 조건 변수를 갱신한다\n }\n return res\n}\n</code></pre> iteration.rb<pre><code>### while 반복문 ###\ndef while_loop(n)\n res = 0\n i = 1 # 조건 변수를 초기화한다\n\n # 반복문으로 1, 2, ..., n-1, n의 합을 구한다\n while i &lt;= n\n res += i\n i += 1 # 조건 변수를 갱신한다\n end\n\n res\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p><code>while</code> 반복문은 <code>for</code> 반복문보다 자유도가 높다. <code>while</code> 반복문에서는 조건 변수의 초기화와 갱신 단계를 자유롭게 설계할 수 있다.</p> <p>예를 들어 다음 코드에서는 조건 변수 \\(i\\)를 매번 두 차례 갱신한다. 이런 경우에는 <code>for</code> 반복문으로 구현하기가 다소 불편하다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py<pre><code>def while_loop_ii(n: int) -&gt; int:\n \"\"\"while 반복문(두 번 갱신)\"\"\"\n res = 0\n i = 1 # 조건 변수를 초기화한다\n # 반복문으로 1, 4, 10, ...의 합을 구한다\n while i &lt;= n:\n res += i\n # 조건 변수를 갱신한다\n i += 1\n i *= 2\n return res\n</code></pre> iteration.cpp<pre><code>/* while 반복문(두 번 갱신) */\nint whileLoopII(int n) {\n int res = 0;\n int i = 1; // 조건 변수를 초기화한다\n // 반복문으로 1, 4, 10, ...의 합을 구한다\n while (i &lt;= n) {\n res += i;\n // 조건 변수를 갱신한다\n i++;\n i *= 2;\n }\n return res;\n}\n</code></pre> iteration.java<pre><code>/* while 반복문(두 번 갱신) */\nint whileLoopII(int n) {\n int res = 0;\n int i = 1; // 조건 변수를 초기화한다\n // 반복문으로 1, 4, 10, ...의 합을 구한다\n while (i &lt;= n) {\n res += i;\n // 조건 변수를 갱신한다\n i++;\n i *= 2;\n }\n return res;\n}\n</code></pre> iteration.cs<pre><code>/* while 반복문(두 번 갱신) */\nint WhileLoopII(int n) {\n int res = 0;\n int i = 1; // 조건 변수를 초기화한다\n // 반복문으로 1, 4, 10, ...의 합을 구한다\n while (i &lt;= n) {\n res += i;\n // 조건 변수를 갱신한다\n i += 1; \n i *= 2;\n }\n return res;\n}\n</code></pre> iteration.go<pre><code>/* while 반복문(두 번 갱신) */\nfunc whileLoopII(n int) int {\n res := 0\n // 조건 변수를 초기화한다\n i := 1\n // 반복문으로 1, 4, 10, ...의 합을 구한다\n for i &lt;= n {\n res += i\n // 조건 변수를 갱신한다\n i++\n i *= 2\n }\n return res\n}\n</code></pre> iteration.swift<pre><code>/* while 반복문(두 번 갱신) */\nfunc whileLoopII(n: Int) -&gt; Int {\n var res = 0\n var i = 1 // 조건 변수를 초기화한다\n // 반복문으로 1, 4, 10, ...의 합을 구한다\n while i &lt;= n {\n res += i\n // 조건 변수를 갱신한다\n i += 1\n i *= 2\n }\n return res\n}\n</code></pre> iteration.js<pre><code>/* while 반복문(두 번 갱신) */\nfunction whileLoopII(n) {\n let res = 0;\n let i = 1; // 조건 변수를 초기화한다\n // 반복문으로 1, 4, 10, ...의 합을 구한다\n while (i &lt;= n) {\n res += i;\n // 조건 변수를 갱신한다\n i++;\n i *= 2;\n }\n return res;\n}\n</code></pre> iteration.ts<pre><code>/* while 반복문(두 번 갱신) */\nfunction whileLoopII(n: number): number {\n let res = 0;\n let i = 1; // 조건 변수를 초기화한다\n // 반복문으로 1, 4, 10, ...의 합을 구한다\n while (i &lt;= n) {\n res += i;\n // 조건 변수를 갱신한다\n i++;\n i *= 2;\n }\n return res;\n}\n</code></pre> iteration.dart<pre><code>/* while 반복문(두 번 갱신) */\nint whileLoopII(int n) {\n int res = 0;\n int i = 1; // 조건 변수를 초기화한다\n // 반복문으로 1, 4, 10, ...의 합을 구한다\n while (i &lt;= n) {\n res += i;\n // 조건 변수를 갱신한다\n i++;\n i *= 2;\n }\n return res;\n}\n</code></pre> iteration.rs<pre><code>/* while 반복문(두 번 갱신) */\nfn while_loop_ii(n: i32) -&gt; i32 {\n let mut res = 0;\n let mut i = 1; // 조건 변수를 초기화한다\n\n // 반복문으로 1, 4, 10, ...의 합을 구한다\n while i &lt;= n {\n res += i;\n // 조건 변수를 갱신한다\n i += 1;\n i *= 2;\n }\n res\n}\n</code></pre> iteration.c<pre><code>/* while 반복문(두 번 갱신) */\nint whileLoopII(int n) {\n int res = 0;\n int i = 1; // 조건 변수를 초기화한다\n // 반복문으로 1, 4, 10, ...의 합을 구한다\n while (i &lt;= n) {\n res += i;\n // 조건 변수를 갱신한다\n i++;\n i *= 2;\n }\n return res;\n}\n</code></pre> iteration.kt<pre><code>/* while 반복문(두 번 갱신) */\nfun whileLoopII(n: Int): Int {\n var res = 0\n var i = 1 // 조건 변수를 초기화한다\n // 반복문으로 1, 4, 10, ...의 합을 구한다\n while (i &lt;= n) {\n res += i\n // 조건 변수를 갱신한다\n i++\n i *= 2\n }\n return res\n}\n</code></pre> iteration.rb<pre><code>### while 반복문(두 번 갱신)###\ndef while_loop_ii(n)\n res = 0\n i = 1 # 조건 변수를 초기화한다\n\n # 반복문으로 1, 4, 10, ...의 합을 구한다\n while i &lt;= n\n res += i\n # 조건 변수를 갱신한다\n i += 1\n i *= 2\n end\n\n res\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>전반적으로 <code>for</code> 반복문은 코드가 더 간결하고, <code>while</code> 반복문은 더 유연하다. 둘 다 반복 구조를 구현할 수 있으며, 어떤 것을 쓸지는 문제의 구체적인 요구에 따라 결정해야 한다.</p>","path":["제 2 장   복잡도 분석","2.2   반복과 재귀"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#3","level":3,"title":"3.   중첩 반복문","text":"<p>하나의 반복 구조 안에 다른 반복 구조를 중첩할 수 있다. 다음은 <code>for</code> 반복문을 사용한 예다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py<pre><code>def nested_for_loop(n: int) -&gt; str:\n \"\"\"이중 for 반복문\"\"\"\n res = \"\"\n # i = 1, 2, ..., n-1, n을 반복한다\n for i in range(1, n + 1):\n # j = 1, 2, ..., n-1, n을 반복한다\n for j in range(1, n + 1):\n res += f\"({i}, {j}), \"\n return res\n</code></pre> iteration.cpp<pre><code>/* 이중 for 반복문 */\nstring nestedForLoop(int n) {\n ostringstream res;\n // i = 1, 2, ..., n-1, n을 반복한다\n for (int i = 1; i &lt;= n; ++i) {\n // j = 1, 2, ..., n-1, n을 반복한다\n for (int j = 1; j &lt;= n; ++j) {\n res &lt;&lt; \"(\" &lt;&lt; i &lt;&lt; \", \" &lt;&lt; j &lt;&lt; \"), \";\n }\n }\n return res.str();\n}\n</code></pre> iteration.java<pre><code>/* 이중 for 반복문 */\nString nestedForLoop(int n) {\n StringBuilder res = new StringBuilder();\n // i = 1, 2, ..., n-1, n을 반복한다\n for (int i = 1; i &lt;= n; i++) {\n // j = 1, 2, ..., n-1, n을 반복한다\n for (int j = 1; j &lt;= n; j++) {\n res.append(\"(\" + i + \", \" + j + \"), \");\n }\n }\n return res.toString();\n}\n</code></pre> iteration.cs<pre><code>/* 이중 for 반복문 */\nstring NestedForLoop(int n) {\n StringBuilder res = new();\n // i = 1, 2, ..., n-1, n을 반복한다\n for (int i = 1; i &lt;= n; i++) {\n // j = 1, 2, ..., n-1, n을 반복한다\n for (int j = 1; j &lt;= n; j++) {\n res.Append($\"({i}, {j}), \");\n }\n }\n return res.ToString();\n}\n</code></pre> iteration.go<pre><code>/* 이중 for 반복문 */\nfunc nestedForLoop(n int) string {\n res := \"\"\n // i = 1, 2, ..., n-1, n을 반복한다\n for i := 1; i &lt;= n; i++ {\n for j := 1; j &lt;= n; j++ {\n // j = 1, 2, ..., n-1, n을 반복한다\n res += fmt.Sprintf(\"(%d, %d), \", i, j)\n }\n }\n return res\n}\n</code></pre> iteration.swift<pre><code>/* 이중 for 반복문 */\nfunc nestedForLoop(n: Int) -&gt; String {\n var res = \"\"\n // i = 1, 2, ..., n-1, n을 반복한다\n for i in 1 ... n {\n // j = 1, 2, ..., n-1, n을 반복한다\n for j in 1 ... n {\n res.append(\"(\\(i), \\(j)), \")\n }\n }\n return res\n}\n</code></pre> iteration.js<pre><code>/* 이중 for 반복문 */\nfunction nestedForLoop(n) {\n let res = '';\n // i = 1, 2, ..., n-1, n을 반복한다\n for (let i = 1; i &lt;= n; i++) {\n // j = 1, 2, ..., n-1, n을 반복한다\n for (let j = 1; j &lt;= n; j++) {\n res += `(${i}, ${j}), `;\n }\n }\n return res;\n}\n</code></pre> iteration.ts<pre><code>/* 이중 for 반복문 */\nfunction nestedForLoop(n: number): string {\n let res = '';\n // i = 1, 2, ..., n-1, n을 반복한다\n for (let i = 1; i &lt;= n; i++) {\n // j = 1, 2, ..., n-1, n을 반복한다\n for (let j = 1; j &lt;= n; j++) {\n res += `(${i}, ${j}), `;\n }\n }\n return res;\n}\n</code></pre> iteration.dart<pre><code>/* 이중 for 반복문 */\nString nestedForLoop(int n) {\n String res = \"\";\n // i = 1, 2, ..., n-1, n을 반복한다\n for (int i = 1; i &lt;= n; i++) {\n // j = 1, 2, ..., n-1, n을 반복한다\n for (int j = 1; j &lt;= n; j++) {\n res += \"($i, $j), \";\n }\n }\n return res;\n}\n</code></pre> iteration.rs<pre><code>/* 이중 for 반복문 */\nfn nested_for_loop(n: i32) -&gt; String {\n let mut res = vec![];\n // i = 1, 2, ..., n-1, n을 반복한다\n for i in 1..=n {\n // j = 1, 2, ..., n-1, n을 반복한다\n for j in 1..=n {\n res.push(format!(\"({}, {}), \", i, j));\n }\n }\n res.join(\"\")\n}\n</code></pre> iteration.c<pre><code>/* 이중 for 반복문 */\nchar *nestedForLoop(int n) {\n // n * n은 해당 점의 개수이며, \"(i, j), \"의 최대 문자열 길이는 6+10*2이다. 마지막 널 문자 \\0을 위한 공간도 추가한다\n int size = n * n * 26 + 1;\n char *res = malloc(size * sizeof(char));\n // i = 1, 2, ..., n-1, n을 반복한다\n for (int i = 1; i &lt;= n; i++) {\n // j = 1, 2, ..., n-1, n을 반복한다\n for (int j = 1; j &lt;= n; j++) {\n char tmp[26];\n snprintf(tmp, sizeof(tmp), \"(%d, %d), \", i, j);\n strncat(res, tmp, size - strlen(res) - 1);\n }\n }\n return res;\n}\n</code></pre> iteration.kt<pre><code>/* 이중 for 반복문 */\nfun nestedForLoop(n: Int): String {\n val res = StringBuilder()\n // i = 1, 2, ..., n-1, n을 반복한다\n for (i in 1..n) {\n // j = 1, 2, ..., n-1, n을 반복한다\n for (j in 1..n) {\n res.append(\" ($i, $j), \")\n }\n }\n return res.toString()\n}\n</code></pre> iteration.rb<pre><code>### 이중 for 반복문 ###\ndef nested_for_loop(n)\n res = \"\"\n\n # i = 1, 2, ..., n-1, n을 반복한다\n for i in 1..n\n # j = 1, 2, ..., n-1, n을 반복한다\n for j in 1..n\n res += \"(#{i}, #{j}), \"\n end\n end\n\n res\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>아래 그림(2-2)은 이 중첩 반복문의 순서도다.</p> <p></p> <p> 그림 2-2   중첩 반복문의 순서도 </p> <p>이 경우 함수의 연산 횟수는 \\(n^2\\)에 비례한다. 즉, 알고리즘 실행 시간과 입력 데이터 크기 \\(n\\) 사이에는 ‘제곱 관계’가 있다.</p> <p>반복문을 계속 중첩할 수도 있다. 한 번 중첩할 때마다 ‘차원이 하나씩 늘어나’ 시간 복잡도가 ‘세제곱 관계’, ‘네제곱 관계’ 등으로 높아진다.</p>","path":["제 2 장   복잡도 분석","2.2   반복과 재귀"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#222","level":2,"title":"2.2.2   재귀","text":"<p>재귀(recursion)는 함수가 자기 자신을 호출하여 문제를 해결하는 알고리즘 전략이다. 크게 두 단계로 이루어진다.</p> <ol> <li>내려가기: 프로그램이 자기 자신을 계속 더 깊이 호출한다. 보통 더 작거나 단순한 인수를 전달하며, ‘종료 조건’에 도달할 때까지 이어진다.</li> <li>돌아오기: ‘종료 조건’에 도달하면 가장 깊은 재귀 함수부터 한 단계씩 반환하며, 각 단계의 결과를 모은다.</li> </ol> <p>구현 관점에서 보면 재귀 코드는 크게 세 요소로 이루어진다.</p> <ol> <li>종료 조건: ‘내려가기’에서 ‘돌아오기’로 전환할 시점을 결정한다.</li> <li>재귀 호출: ‘내려가기’에 해당한다. 함수가 자기 자신을 호출하며, 보통 더 작거나 단순한 인수를 입력한다.</li> <li>결과 반환: ‘돌아오기’에 해당한다. 현재 재귀 단계의 결과를 이전 단계로 반환한다.</li> </ol> <p>다음 코드를 보면, 함수 <code>recur(n)</code>을 호출하기만 하면 \\(1 + 2 + \\dots + n\\)을 계산할 수 있다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py<pre><code>def recur(n: int) -&gt; int:\n \"\"\"재귀\"\"\"\n # 종료 조건\n if n == 1:\n return 1\n # 내려가기: 재귀 호출\n res = recur(n - 1)\n # 돌아오기: 결과 반환\n return n + res\n</code></pre> recursion.cpp<pre><code>/* 재귀 */\nint recur(int n) {\n // 종료 조건\n if (n == 1)\n return 1;\n // 내려가기: 재귀 호출\n int res = recur(n - 1);\n // 돌아오기: 결과 반환\n return n + res;\n}\n</code></pre> recursion.java<pre><code>/* 재귀 */\nint recur(int n) {\n // 종료 조건\n if (n == 1)\n return 1;\n // 내려가기: 재귀 호출\n int res = recur(n - 1);\n // 돌아오기: 결과 반환\n return n + res;\n}\n</code></pre> recursion.cs<pre><code>/* 재귀 */\nint Recur(int n) {\n // 종료 조건\n if (n == 1)\n return 1;\n // 내려가기: 재귀 호출\n int res = Recur(n - 1);\n // 돌아오기: 결과 반환\n return n + res;\n}\n</code></pre> recursion.go<pre><code>/* 재귀 */\nfunc recur(n int) int {\n // 종료 조건\n if n == 1 {\n return 1\n }\n // 내려가기: 재귀 호출\n res := recur(n - 1)\n // 돌아오기: 결과 반환\n return n + res\n}\n</code></pre> recursion.swift<pre><code>/* 재귀 */\nfunc recur(n: Int) -&gt; Int {\n // 종료 조건\n if n == 1 {\n return 1\n }\n // 내려가기: 재귀 호출\n let res = recur(n: n - 1)\n // 돌아오기: 결과 반환\n return n + res\n}\n</code></pre> recursion.js<pre><code>/* 재귀 */\nfunction recur(n) {\n // 종료 조건\n if (n === 1) return 1;\n // 내려가기: 재귀 호출\n const res = recur(n - 1);\n // 돌아오기: 결과 반환\n return n + res;\n}\n</code></pre> recursion.ts<pre><code>/* 재귀 */\nfunction recur(n: number): number {\n // 종료 조건\n if (n === 1) return 1;\n // 내려가기: 재귀 호출\n const res = recur(n - 1);\n // 돌아오기: 결과 반환\n return n + res;\n}\n</code></pre> recursion.dart<pre><code>/* 재귀 */\nint recur(int n) {\n // 종료 조건\n if (n == 1) return 1;\n // 내려가기: 재귀 호출\n int res = recur(n - 1);\n // 돌아오기: 결과 반환\n return n + res;\n}\n</code></pre> recursion.rs<pre><code>/* 재귀 */\nfn recur(n: i32) -&gt; i32 {\n // 종료 조건\n if n == 1 {\n return 1;\n }\n // 내려가기: 재귀 호출\n let res = recur(n - 1);\n // 돌아오기: 결과 반환\n n + res\n}\n</code></pre> recursion.c<pre><code>/* 재귀 */\nint recur(int n) {\n // 종료 조건\n if (n == 1)\n return 1;\n // 내려가기: 재귀 호출\n int res = recur(n - 1);\n // 돌아오기: 결과 반환\n return n + res;\n}\n</code></pre> recursion.kt<pre><code>/* 재귀 */\nfun recur(n: Int): Int {\n // 종료 조건\n if (n == 1)\n return 1\n // 내려가기: 재귀 호출\n val res = recur(n - 1)\n // 돌아오기: 결과 반환\n return n + res\n}\n</code></pre> recursion.rb<pre><code>### 재귀 ###\ndef recur(n)\n # 종료 조건\n return 1 if n == 1\n # 내려가기: 재귀 호출\n res = recur(n - 1)\n # 돌아오기: 결과 반환\n n + res\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>아래 그림(2-3)은 이 함수의 재귀 과정을 보여 준다.</p> <p></p> <p> 그림 2-3   합계 함수의 재귀 과정 </p> <p>반복과 재귀는 계산 결과가 같을 수 있지만, 문제를 생각하고 해결하는 방식은 완전히 다르다.</p> <ul> <li>반복: ‘상향식’으로 문제를 해결한다. 가장 기본적인 단계에서 시작해 그 단계를 계속 반복하거나 누적하여 작업을 완료한다.</li> <li>재귀: ‘하향식’으로 문제를 해결한다. 원래 문제를 같은 형태의 더 작은 부분 문제로 나눈다. 그런 다음 부분 문제를 더 작은 부분 문제로 계속 나누다가 기저 사례에 도달하면 멈춘다(기저 사례의 해는 이미 알고 있다).</li> </ul> <p>앞의 합계 함수를 예로 들어, 문제를 \\(f(n) = 1 + 2 + \\dots + n\\)이라고 하자.</p> <ul> <li>반복: 반복문에서 합을 구하는 과정을 재현한다. \\(1\\)부터 \\(n\\)까지 순회하면서 매번 더하면 \\(f(n)\\)을 구할 수 있다.</li> <li>재귀: 문제를 부분 문제 \\(f(n) = n + f(n-1)\\)로 나누고, 계속 재귀적으로 나누다가 기저 사례 \\(f(1) = 1\\)에 도달하면 멈춘다.</li> </ul>","path":["제 2 장   복잡도 분석","2.2   반복과 재귀"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#1","level":3,"title":"1.   호출 스택","text":"<p>재귀 함수가 자기 자신을 호출할 때마다 시스템은 새로 실행되는 함수를 위해 메모리를 할당하고 지역 변수, 호출 주소 등의 정보를 저장한다. 이로 인해 두 가지 결과가 나타난다.</p> <ul> <li>함수의 문맥 데이터는 ‘스택 프레임 공간’이라는 메모리 영역에 저장되며, 함수가 반환된 뒤에야 해제된다. 따라서 재귀는 보통 반복보다 더 많은 메모리 공간을 사용한다.</li> <li>재귀 함수 호출에는 추가 비용이 발생한다. 따라서 재귀는 보통 반복문보다 시간 효율이 낮다.</li> </ul> <p>아래 그림(2-4)과 같이 종료 조건에 도달하기 전에는 아직 반환되지 않은 재귀 함수가 \\(n\\)개 동시에 존재하므로, 재귀 깊이는 \\(n\\)이다.</p> <p></p> <p> 그림 2-4   재귀 호출 깊이 </p> <p>실제로 프로그래밍 언어가 허용하는 재귀 깊이는 보통 제한되어 있으며, 재귀가 너무 깊어지면 스택 오버플로 오류가 발생할 수 있다.</p>","path":["제 2 장   복잡도 분석","2.2   반복과 재귀"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#2","level":3,"title":"2.   꼬리 재귀","text":"<p>흥미롭게도 함수가 반환하기 직전의 마지막 단계에서 재귀 호출을 수행하면, 컴파일러나 인터프리터가 이를 최적화하여 반복과 비슷한 공간 효율을 얻을 수 있다. 이런 경우를 꼬리 재귀(tail recursion)라고 한다.</p> <ul> <li>일반 재귀: 함수가 이전 단계의 함수로 반환된 뒤에도 실행할 코드가 남아 있으므로, 시스템은 이전 호출의 문맥을 저장해야 한다.</li> <li>꼬리 재귀: 재귀 호출이 함수 반환 전의 마지막 연산이다. 따라서 이전 단계로 반환된 뒤에 다른 연산을 수행할 필요가 없으므로, 시스템은 이전 함수의 문맥을 저장하지 않아도 된다.</li> </ul> <p>\\(1 + 2 + \\dots + n\\)을 계산하는 예에서 결과 변수 <code>res</code>를 함수 매개변수로 두면 꼬리 재귀를 구현할 수 있다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py<pre><code>def tail_recur(n, res):\n \"\"\"꼬리 재귀\"\"\"\n # 종료 조건\n if n == 0:\n return res\n # 꼬리 재귀 호출\n return tail_recur(n - 1, res + n)\n</code></pre> recursion.cpp<pre><code>/* 꼬리 재귀 */\nint tailRecur(int n, int res) {\n // 종료 조건\n if (n == 0)\n return res;\n // 꼬리 재귀 호출\n return tailRecur(n - 1, res + n);\n}\n</code></pre> recursion.java<pre><code>/* 꼬리 재귀 */\nint tailRecur(int n, int res) {\n // 종료 조건\n if (n == 0)\n return res;\n // 꼬리 재귀 호출\n return tailRecur(n - 1, res + n);\n}\n</code></pre> recursion.cs<pre><code>/* 꼬리 재귀 */\nint TailRecur(int n, int res) {\n // 종료 조건\n if (n == 0)\n return res;\n // 꼬리 재귀 호출\n return TailRecur(n - 1, res + n);\n}\n</code></pre> recursion.go<pre><code>/* 꼬리 재귀 */\nfunc tailRecur(n int, res int) int {\n // 종료 조건\n if n == 0 {\n return res\n }\n // 꼬리 재귀 호출\n return tailRecur(n-1, res+n)\n}\n</code></pre> recursion.swift<pre><code>/* 꼬리 재귀 */\nfunc tailRecur(n: Int, res: Int) -&gt; Int {\n // 종료 조건\n if n == 0 {\n return res\n }\n // 꼬리 재귀 호출\n return tailRecur(n: n - 1, res: res + n)\n}\n</code></pre> recursion.js<pre><code>/* 꼬리 재귀 */\nfunction tailRecur(n, res) {\n // 종료 조건\n if (n === 0) return res;\n // 꼬리 재귀 호출\n return tailRecur(n - 1, res + n);\n}\n</code></pre> recursion.ts<pre><code>/* 꼬리 재귀 */\nfunction tailRecur(n: number, res: number): number {\n // 종료 조건\n if (n === 0) return res;\n // 꼬리 재귀 호출\n return tailRecur(n - 1, res + n);\n}\n</code></pre> recursion.dart<pre><code>/* 꼬리 재귀 */\nint tailRecur(int n, int res) {\n // 종료 조건\n if (n == 0) return res;\n // 꼬리 재귀 호출\n return tailRecur(n - 1, res + n);\n}\n</code></pre> recursion.rs<pre><code>/* 꼬리 재귀 */\nfn tail_recur(n: i32, res: i32) -&gt; i32 {\n // 종료 조건\n if n == 0 {\n return res;\n }\n // 꼬리 재귀 호출\n tail_recur(n - 1, res + n)\n}\n</code></pre> recursion.c<pre><code>/* 꼬리 재귀 */\nint tailRecur(int n, int res) {\n // 종료 조건\n if (n == 0)\n return res;\n // 꼬리 재귀 호출\n return tailRecur(n - 1, res + n);\n}\n</code></pre> recursion.kt<pre><code>/* 꼬리 재귀 */\ntailrec fun tailRecur(n: Int, res: Int): Int {\n // tailrec 키워드를 추가하여 꼬리 재귀 최적화를 활성화한다\n // 종료 조건\n if (n == 0)\n return res\n // 꼬리 재귀 호출\n return tailRecur(n - 1, res + n)\n}\n</code></pre> recursion.rb<pre><code>### 꼬리 재귀 ###\ndef tail_recur(n, res)\n # 종료 조건\n return res if n == 0\n # 꼬리 재귀 호출\n tail_recur(n - 1, res + n)\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>꼬리 재귀의 실행 과정은 아래 그림(2-5)과 같다. 일반 재귀와 꼬리 재귀를 비교하면 합을 구하는 시점이 다르다.</p> <ul> <li>일반 재귀: ‘돌아오기’ 과정에서 합을 구한다. 각 단계가 반환된 뒤에 덧셈을 한 번씩 수행한다.</li> <li>꼬리 재귀: ‘내려가기’ 과정에서 합을 구한다. ‘돌아오기’ 과정에서는 단계별로 반환만 하면 된다.</li> </ul> <p></p> <p> 그림 2-5   꼬리 재귀 과정 </p> <p>참고</p> <p>많은 컴파일러와 인터프리터가 꼬리 재귀 최적화를 지원하지 않는다는 점에 주의하자. 예를 들어 Python은 기본적으로 꼬리 재귀 최적화를 지원하지 않으므로, 함수가 꼬리 재귀 형태여도 스택 오버플로 문제가 발생할 수 있다.</p>","path":["제 2 장   복잡도 분석","2.2   반복과 재귀"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#3_1","level":3,"title":"3.   재귀 트리","text":"<p>‘분할 정복’과 관련된 알고리즘 문제에서는 재귀가 반복보다 생각하기 쉽고 코드도 읽기 쉬운 경우가 많다. ‘피보나치 수열’을 예로 살펴보자.</p> <p>문제</p> <p>피보나치 수열 \\(0, 1, 1, 2, 3, 5, 8, 13, \\dots\\)가 주어졌을 때, 이 수열의 \\(n\\)번째 수를 구하라.</p> <p>피보나치 수열의 \\(n\\)번째 수를 \\(f(n)\\)이라고 하면 다음 두 가지 사실을 쉽게 알 수 있다.</p> <ul> <li>수열의 첫 두 수는 \\(f(1) = 0\\)과 \\(f(2) = 1\\)이다.</li> <li>수열의 각 수는 앞의 두 수의 합이다. 즉, \\(f(n) = f(n - 1) + f(n - 2)\\)이다.</li> </ul> <p>점화 관계에 따라 재귀 호출을 하고 첫 두 수를 종료 조건으로 삼으면 재귀 코드를 작성할 수 있다. <code>fib(n)</code>을 호출하면 피보나치 수열의 \\(n\\)번째 수를 얻는다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py<pre><code>def fib(n: int) -&gt; int:\n \"\"\"피보나치 수열: 재귀\"\"\"\n # 종료 조건 f(1) = 0, f(2) = 1\n if n == 1 or n == 2:\n return n - 1\n # 재귀 호출 f(n) = f(n-1) + f(n-2)\n res = fib(n - 1) + fib(n - 2)\n # 결과 f(n)을 반환한다\n return res\n</code></pre> recursion.cpp<pre><code>/* 피보나치 수열: 재귀 */\nint fib(int n) {\n // 종료 조건 f(1) = 0, f(2) = 1\n if (n == 1 || n == 2)\n return n - 1;\n // 재귀 호출 f(n) = f(n-1) + f(n-2)\n int res = fib(n - 1) + fib(n - 2);\n // 결과 f(n)을 반환한다\n return res;\n}\n</code></pre> recursion.java<pre><code>/* 피보나치 수열: 재귀 */\nint fib(int n) {\n // 종료 조건 f(1) = 0, f(2) = 1\n if (n == 1 || n == 2)\n return n - 1;\n // 재귀 호출 f(n) = f(n-1) + f(n-2)\n int res = fib(n - 1) + fib(n - 2);\n // 결과 f(n)을 반환한다\n return res;\n}\n</code></pre> recursion.cs<pre><code>/* 피보나치 수열: 재귀 */\nint Fib(int n) {\n // 종료 조건 f(1) = 0, f(2) = 1\n if (n == 1 || n == 2)\n return n - 1;\n // 재귀 호출 f(n) = f(n-1) + f(n-2)\n int res = Fib(n - 1) + Fib(n - 2);\n // 결과 f(n)을 반환한다\n return res;\n}\n</code></pre> recursion.go<pre><code>/* 피보나치 수열: 재귀 */\nfunc fib(n int) int {\n // 종료 조건 f(1) = 0, f(2) = 1\n if n == 1 || n == 2 {\n return n - 1\n }\n // 재귀 호출 f(n) = f(n-1) + f(n-2)\n res := fib(n-1) + fib(n-2)\n // 결과 f(n)을 반환한다\n return res\n}\n</code></pre> recursion.swift<pre><code>/* 피보나치 수열: 재귀 */\nfunc fib(n: Int) -&gt; Int {\n // 종료 조건 f(1) = 0, f(2) = 1\n if n == 1 || n == 2 {\n return n - 1\n }\n // 재귀 호출 f(n) = f(n-1) + f(n-2)\n let res = fib(n: n - 1) + fib(n: n - 2)\n // 결과 f(n)을 반환한다\n return res\n}\n</code></pre> recursion.js<pre><code>/* 피보나치 수열: 재귀 */\nfunction fib(n) {\n // 종료 조건 f(1) = 0, f(2) = 1\n if (n === 1 || n === 2) return n - 1;\n // 재귀 호출 f(n) = f(n-1) + f(n-2)\n const res = fib(n - 1) + fib(n - 2);\n // 결과 f(n)을 반환한다\n return res;\n}\n</code></pre> recursion.ts<pre><code>/* 피보나치 수열: 재귀 */\nfunction fib(n: number): number {\n // 종료 조건 f(1) = 0, f(2) = 1\n if (n === 1 || n === 2) return n - 1;\n // 재귀 호출 f(n) = f(n-1) + f(n-2)\n const res = fib(n - 1) + fib(n - 2);\n // 결과 f(n)을 반환한다\n return res;\n}\n</code></pre> recursion.dart<pre><code>/* 피보나치 수열: 재귀 */\nint fib(int n) {\n // 종료 조건 f(1) = 0, f(2) = 1\n if (n == 1 || n == 2) return n - 1;\n // 재귀 호출 f(n) = f(n-1) + f(n-2)\n int res = fib(n - 1) + fib(n - 2);\n // 결과 f(n)을 반환한다\n return res;\n}\n</code></pre> recursion.rs<pre><code>/* 피보나치 수열: 재귀 */\nfn fib(n: i32) -&gt; i32 {\n // 종료 조건 f(1) = 0, f(2) = 1\n if n == 1 || n == 2 {\n return n - 1;\n }\n // 재귀 호출 f(n) = f(n-1) + f(n-2)\n let res = fib(n - 1) + fib(n - 2);\n // 결과를 반환한다\n res\n}\n</code></pre> recursion.c<pre><code>/* 피보나치 수열: 재귀 */\nint fib(int n) {\n // 종료 조건 f(1) = 0, f(2) = 1\n if (n == 1 || n == 2)\n return n - 1;\n // 재귀 호출 f(n) = f(n-1) + f(n-2)\n int res = fib(n - 1) + fib(n - 2);\n // 결과 f(n)을 반환한다\n return res;\n}\n</code></pre> recursion.kt<pre><code>/* 피보나치 수열: 재귀 */\nfun fib(n: Int): Int {\n // 종료 조건 f(1) = 0, f(2) = 1\n if (n == 1 || n == 2)\n return n - 1\n // 재귀 호출 f(n) = f(n-1) + f(n-2)\n val res = fib(n - 1) + fib(n - 2)\n // 결과 f(n)을 반환한다\n return res\n}\n</code></pre> recursion.rb<pre><code>### 피보나치 수열: 재귀 ###\ndef fib(n)\n # 종료 조건 f(1) = 0, f(2) = 1\n return n - 1 if n == 1 || n == 2\n # 재귀 호출 f(n) = f(n-1) + f(n-2)\n res = fib(n - 1) + fib(n - 2)\n # 결과 f(n)을 반환한다\n res\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>위 코드는 함수 안에서 두 번의 재귀 호출을 수행한다. 이는 하나의 호출에서 두 개의 호출 분기가 생긴다는 뜻이다. 아래 그림(2-6)과 같이 재귀 호출을 계속하면 최종적으로 \\(n\\)개 층으로 이루어진 재귀 트리(recursion tree)가 만들어진다.</p> <p></p> <p> 그림 2-6   피보나치 수열의 재귀 트리 </p> <p>본질적으로 재귀는 ‘문제를 더 작은 부분 문제로 나누는’ 사고방식을 보여 준다. 이러한 분할 정복 전략은 매우 중요하다.</p> <ul> <li>알고리즘 관점에서 탐색, 정렬, 백트래킹, 분할 정복, 동적 계획법 등 중요한 알고리즘 전략 다수가 이 사고방식을 직간접적으로 활용한다.</li> <li>자료 구조 관점에서 재귀는 연결 리스트, 트리, 그래프와 관련된 문제를 다루는 데 자연스럽게 어울린다. 이들은 분할 정복 관점으로 분석하기에 매우 적합하기 때문이다.</li> </ul>","path":["제 2 장   복잡도 분석","2.2   반복과 재귀"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#223","level":2,"title":"2.2.3   두 방식의 비교","text":"<p>앞의 내용을 정리하면, 아래 표(2-1)와 같이 반복과 재귀는 구현, 성능, 적용 범위에서 차이가 있다.</p> <p> 표 2-1   반복과 재귀의 특징 비교 </p> 반복 재귀 구현 방식 반복 구조 함수가 자기 자신을 호출 시간 효율 보통 효율이 높고 함수 호출 비용이 없음 함수 호출마다 비용이 발생함 메모리 사용 보통 고정 크기의 메모리 공간을 사용함 함수 호출이 누적되면 스택 프레임 공간을 많이 사용할 수 있음 적합한 문제 간단한 반복 작업에 적합하며 코드가 직관적이고 읽기 쉬움 트리, 그래프, 분할 정복, 백트래킹 등 부분 문제로 나누는 문제에 적합하며 코드 구조가 간결하고 명확함 <p>참고</p> <p>다음 내용이 이해하기 어렵다면 ‘스택’ 장을 읽은 뒤에 다시 복습해도 좋다.</p> <p>그렇다면 반복과 재귀는 내부적으로 어떤 관계가 있을까? 앞의 재귀 함수를 예로 들면, 합을 구하는 연산은 재귀의 ‘돌아오기’ 단계에서 수행된다. 즉, 가장 먼저 호출된 함수가 실제로는 가장 나중에 자신의 덧셈을 마친다. 이 작동 방식은 스택의 ‘후입선출’ 원리와 통한다.</p> <p>실제로 ‘호출 스택’, ‘스택 프레임 공간’ 같은 재귀 관련 용어에도 재귀와 스택의 밀접한 관계가 드러난다.</p> <ol> <li>내려가기: 함수가 호출되면 시스템은 ‘호출 스택’에 새로운 스택 프레임을 할당하여 함수의 지역 변수, 매개변수, 반환 주소 등의 데이터를 저장한다.</li> <li>돌아오기: 함수가 실행을 마치고 반환되면 해당 스택 프레임이 ‘호출 스택’에서 제거되고, 이전 함수의 실행 환경이 복원된다.</li> </ol> <p>따라서 명시적인 스택으로 호출 스택의 동작을 재현하면, 재귀를 반복 형태로 바꿀 수 있다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py<pre><code>def for_loop_recur(n: int) -&gt; int:\n \"\"\"반복으로 재귀 구현\"\"\"\n # 명시적인 스택을 사용하여 시스템 호출 스택을 모방한다\n stack = []\n res = 0\n # 내려가기: 재귀 호출\n for i in range(n, 0, -1):\n # 스택에 삽입하여 내려가는 과정을 모방한다\n stack.append(i)\n # 돌아오기: 결과 반환\n while stack:\n # 스택에서 꺼내어 돌아오는 과정을 모방한다\n res += stack.pop()\n # res = 1+2+3+...+n\n return res\n</code></pre> recursion.cpp<pre><code>/* 반복으로 재귀 구현 */\nint forLoopRecur(int n) {\n // 명시적인 스택을 사용하여 시스템 호출 스택을 모방한다\n stack&lt;int&gt; stack;\n int res = 0;\n // 내려가기: 재귀 호출\n for (int i = n; i &gt; 0; i--) {\n // 스택에 삽입하여 내려가는 과정을 모방한다\n stack.push(i);\n }\n // 돌아오기: 결과 반환\n while (!stack.empty()) {\n // 스택에서 꺼내어 돌아오는 과정을 모방한다\n res += stack.top();\n stack.pop();\n }\n // res = 1+2+3+...+n\n return res;\n}\n</code></pre> recursion.java<pre><code>/* 반복으로 재귀 구현 */\nint forLoopRecur(int n) {\n // 명시적인 스택을 사용하여 시스템 호출 스택을 모방한다\n Stack&lt;Integer&gt; stack = new Stack&lt;&gt;();\n int res = 0;\n // 내려가기: 재귀 호출\n for (int i = n; i &gt; 0; i--) {\n // 스택에 삽입하여 내려가는 과정을 모방한다\n stack.push(i);\n }\n // 돌아오기: 결과 반환\n while (!stack.isEmpty()) {\n // 스택에서 꺼내어 돌아오는 과정을 모방한다\n res += stack.pop();\n }\n // res = 1+2+3+...+n\n return res;\n}\n</code></pre> recursion.cs<pre><code>/* 반복으로 재귀 구현 */\nint ForLoopRecur(int n) {\n // 명시적인 스택을 사용하여 시스템 호출 스택을 모방한다\n Stack&lt;int&gt; stack = new();\n int res = 0;\n // 내려가기: 재귀 호출\n for (int i = n; i &gt; 0; i--) {\n // 스택에 삽입하여 내려가는 과정을 모방한다\n stack.Push(i);\n }\n // 돌아오기: 결과 반환\n while (stack.Count &gt; 0) {\n // 스택에서 꺼내어 돌아오는 과정을 모방한다\n res += stack.Pop();\n }\n // res = 1+2+3+...+n\n return res;\n}\n</code></pre> recursion.go<pre><code>/* 반복으로 재귀 구현 */\nfunc forLoopRecur(n int) int {\n // 명시적인 스택을 사용하여 시스템 호출 스택을 모방한다\n stack := list.New()\n res := 0\n // 내려가기: 재귀 호출\n for i := n; i &gt; 0; i-- {\n // 스택에 삽입하여 내려가는 과정을 모방한다\n stack.PushBack(i)\n }\n // 돌아오기: 결과 반환\n for stack.Len() != 0 {\n // 스택에서 꺼내어 돌아오는 과정을 모방한다\n res += stack.Back().Value.(int)\n stack.Remove(stack.Back())\n }\n // res = 1+2+3+...+n\n return res\n}\n</code></pre> recursion.swift<pre><code>/* 반복으로 재귀 구현 */\nfunc forLoopRecur(n: Int) -&gt; Int {\n // 명시적인 스택을 사용하여 시스템 호출 스택을 모방한다\n var stack: [Int] = []\n var res = 0\n // 내려가기: 재귀 호출\n for i in (1 ... n).reversed() {\n // 스택에 삽입하여 내려가는 과정을 모방한다\n stack.append(i)\n }\n // 돌아오기: 결과 반환\n while !stack.isEmpty {\n // 스택에서 꺼내어 돌아오는 과정을 모방한다\n res += stack.removeLast()\n }\n // res = 1+2+3+...+n\n return res\n}\n</code></pre> recursion.js<pre><code>/* 반복으로 재귀 구현 */\nfunction forLoopRecur(n) {\n // 명시적인 스택을 사용하여 시스템 호출 스택을 모방한다\n const stack = [];\n let res = 0;\n // 내려가기: 재귀 호출\n for (let i = n; i &gt; 0; i--) {\n // 스택에 삽입하여 내려가는 과정을 모방한다\n stack.push(i);\n }\n // 돌아오기: 결과 반환\n while (stack.length) {\n // 스택에서 꺼내어 돌아오는 과정을 모방한다\n res += stack.pop();\n }\n // res = 1+2+3+...+n\n return res;\n}\n</code></pre> recursion.ts<pre><code>/* 반복으로 재귀 구현 */\nfunction forLoopRecur(n: number): number {\n // 명시적인 스택을 사용하여 시스템 호출 스택을 모방한다 \n const stack: number[] = [];\n let res: number = 0;\n // 내려가기: 재귀 호출\n for (let i = n; i &gt; 0; i--) {\n // 스택에 삽입하여 내려가는 과정을 모방한다\n stack.push(i);\n }\n // 돌아오기: 결과 반환\n while (stack.length) {\n // 스택에서 꺼내어 돌아오는 과정을 모방한다\n res += stack.pop();\n }\n // res = 1+2+3+...+n\n return res;\n}\n</code></pre> recursion.dart<pre><code>/* 반복으로 재귀 구현 */\nint forLoopRecur(int n) {\n // 명시적인 스택을 사용하여 시스템 호출 스택을 모방한다\n List&lt;int&gt; stack = [];\n int res = 0;\n // 내려가기: 재귀 호출\n for (int i = n; i &gt; 0; i--) {\n // 스택에 삽입하여 내려가는 과정을 모방한다\n stack.add(i);\n }\n // 돌아오기: 결과 반환\n while (!stack.isEmpty) {\n // 스택에서 꺼내어 돌아오는 과정을 모방한다\n res += stack.removeLast();\n }\n // res = 1+2+3+...+n\n return res;\n}\n</code></pre> recursion.rs<pre><code>/* 반복으로 재귀 구현 */\nfn for_loop_recur(n: i32) -&gt; i32 {\n // 명시적인 스택을 사용하여 시스템 호출 스택을 모방한다\n let mut stack = Vec::new();\n let mut res = 0;\n // 내려가기: 재귀 호출\n for i in (1..=n).rev() {\n // 스택에 삽입하여 내려가는 과정을 모방한다\n stack.push(i);\n }\n // 돌아오기: 결과 반환\n while !stack.is_empty() {\n // 스택에서 꺼내어 돌아오는 과정을 모방한다\n res += stack.pop().unwrap();\n }\n // res = 1+2+3+...+n\n res\n}\n</code></pre> recursion.c<pre><code>/* 반복으로 재귀 구현 */\nint forLoopRecur(int n) {\n int stack[1000]; // 큰 배열을 사용하여 스택을 구현한다\n int top = -1; // 스택의 맨 위 인덱스\n int res = 0;\n // 내려가기: 재귀 호출\n for (int i = n; i &gt; 0; i--) {\n // 스택에 삽입하여 내려가는 과정을 모방한다\n stack[1 + top++] = i;\n }\n // 돌아오기: 결과 반환\n while (top &gt;= 0) {\n // 스택에서 꺼내어 돌아오는 과정을 모방한다\n res += stack[top--];\n }\n // res = 1+2+3+...+n\n return res;\n}\n</code></pre> recursion.kt<pre><code>/* 반복으로 재귀 구현 */\nfun forLoopRecur(n: Int): Int {\n // 명시적인 스택을 사용하여 시스템 호출 스택을 모방한다\n val stack = Stack&lt;Int&gt;()\n var res = 0\n // 내려가기: 재귀 호출\n for (i in n downTo 0) {\n // 스택에 삽입하여 내려가는 과정을 모방한다\n stack.push(i)\n }\n // 돌아오기: 결과 반환\n while (stack.isNotEmpty()) {\n // 스택에서 꺼내어 돌아오는 과정을 모방한다\n res += stack.pop()\n }\n // res = 1+2+3+...+n\n return res\n}\n</code></pre> recursion.rb<pre><code>### 반복으로 재귀 구현 ###\ndef for_loop_recur(n)\n # 명시적인 스택을 사용하여 시스템 호출 스택을 모방한다\n stack = []\n res = 0\n\n # 내려가기: 재귀 호출\n for i in n.downto(0)\n # 스택에 삽입하여 내려가는 과정을 모방한다\n stack &lt;&lt; i\n end\n # 돌아오기: 결과 반환\n while !stack.empty?\n res += stack.pop\n end\n\n # res = 1+2+3+...+n\n res\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>위 코드를 보면 재귀를 반복으로 바꾸자 코드가 더 복잡해졌다. 반복과 재귀는 많은 경우 서로 변환할 수 있지만, 다음 두 가지 이유로 항상 그럴 가치가 있는 것은 아니다.</p> <ul> <li>변환한 코드가 더 이해하기 어렵고 가독성이 떨어질 수 있다.</li> <li>복잡한 문제에서는 시스템 호출 스택의 동작을 재현하기가 매우 어려울 수 있다.</li> </ul> <p>결국 반복과 재귀 중 어떤 것을 선택할지는 문제의 구체적인 성격에 달려 있다. 실제 프로그래밍에서는 둘의 장단점을 비교하고 상황에 맞는 방법을 선택하는 것이 중요하다.</p>","path":["제 2 장   복잡도 분석","2.2   반복과 재귀"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/","level":1,"title":"2.1   알고리즘 효율 평가","text":"<p>알고리즘을 설계할 때는 다음 두 단계의 목표를 차례로 추구한다.</p> <ol> <li>문제의 해법 찾기: 알고리즘은 정해진 입력 범위 안에서 문제의 올바른 해를 안정적으로 구해야 한다.</li> <li>최적의 해법 찾기: 같은 문제에도 여러 해법이 있을 수 있으므로, 가능한 한 효율적인 알고리즘을 찾고자 한다.</li> </ol> <p>즉, 문제를 해결할 수 있다는 전제하에 알고리즘 효율은 알고리즘의 우수성을 평가하는 주요 지표가 된다. 여기에는 다음 두 가지 측면이 있다.</p> <ul> <li>시간 효율: 알고리즘의 실행 시간이 얼마나 긴가.</li> <li>공간 효율: 알고리즘이 차지하는 메모리 공간이 얼마나 큰가.</li> </ul> <p>간단히 말해, 우리의 목표는 ‘빠르면서도 메모리를 적게 사용하는’ 자료 구조와 알고리즘을 설계하는 것이다. 알고리즘 효율을 효과적으로 평가하는 일도 매우 중요하다. 그래야 여러 알고리즘을 비교하고, 이를 바탕으로 알고리즘을 설계하고 최적화할 수 있기 때문이다.</p> <p>효율 평가 방법은 크게 실제 측정과 이론적 추정으로 나뉜다.</p>","path":["제 2 장   복잡도 분석","2.1   알고리즘 효율 평가"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/#211","level":2,"title":"2.1.1   실제 측정","text":"<p>같은 문제를 해결할 수 있는 알고리즘 <code>A</code>와 알고리즘 <code>B</code>가 있으며, 두 알고리즘의 효율을 비교해야 한다고 하자. 가장 직접적인 방법은 컴퓨터에서 두 알고리즘을 실행하고 실행 시간과 메모리 사용량을 관찰해 기록하는 것이다. 이러한 평가 방식은 실제 상황을 반영할 수 있지만 한계도 크다.</p> <p>한편으로는 측정 환경에 따른 영향을 배제하기 어렵다. 하드웨어 구성은 알고리즘 성능에 영향을 준다. 예를 들어 병렬성이 높은 알고리즘은 멀티코어 CPU에서 실행하기에 더 적합하고, 메모리 연산이 집중된 알고리즘은 고성능 메모리에서 더 좋은 성능을 보인다. 즉, 같은 알고리즘도 컴퓨터에 따라 측정 결과가 다를 수 있다. 따라서 다양한 컴퓨터에서 측정해 평균 효율을 구해야 하지만, 이는 현실적이지 않다.</p> <p>다른 한편으로는 충분한 측정을 수행하는 데 매우 많은 자원이 든다. 입력 데이터의 양이 달라지면 알고리즘의 효율도 달라진다. 예를 들어 입력 데이터가 적을 때는 알고리즘 <code>A</code>의 실행 시간이 알고리즘 <code>B</code>보다 짧지만, 입력 데이터가 많을 때는 결과가 정반대일 수 있다. 따라서 설득력 있는 결론을 얻으려면 다양한 크기의 입력 데이터를 측정해야 하며, 여기에는 많은 컴퓨팅 자원이 필요하다.</p>","path":["제 2 장   복잡도 분석","2.1   알고리즘 효율 평가"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/#212","level":2,"title":"2.1.2   이론적 추정","text":"<p>실제 측정에는 큰 한계가 있으므로, 계산만으로 알고리즘 효율을 평가하는 방법을 생각해 볼 수 있다. 이러한 추정 방법을 점근적 복잡도 분석(asymptotic complexity analysis), 줄여서 복잡도 분석이라고 한다.</p> <p>복잡도 분석은 알고리즘 실행에 필요한 시간 및 공간 자원과 입력 데이터 크기의 관계를 나타낸다. 입력 데이터 크기가 증가할 때 알고리즘 실행에 필요한 시간과 공간이 증가하는 추세를 설명한다. 이 정의는 다소 이해하기 어려우므로 세 가지 핵심으로 나누어 살펴보자.</p> <ul> <li>‘시간 및 공간 자원’은 각각 시간 복잡도(time complexity)와 공간 복잡도(space complexity)에 해당한다.</li> <li>‘입력 데이터 크기가 증가할 때’는 복잡도가 알고리즘의 실행 효율과 입력 데이터 크기 사이의 관계를 반영한다는 뜻이다.</li> <li>‘시간과 공간이 증가하는 추세’는 복잡도 분석이 실행 시간이나 사용 공간의 구체적인 값보다 시간 또는 공간이 얼마나 빠르게 증가하는지에 주목한다는 뜻이다.</li> </ul> <p>복잡도 분석은 실제 측정 방법의 단점을 극복한다. 구체적으로 다음과 같은 장점이 있다.</p> <ul> <li>실제로 코드를 실행하지 않아도 되므로 에너지를 절약하고 환경 부담을 줄인다.</li> <li>측정 환경에 의존하지 않아 분석 결과를 모든 실행 플랫폼에 적용할 수 있다.</li> <li>데이터 양에 따른 알고리즘 효율, 특히 데이터가 많을 때의 알고리즘 성능을 보여 준다.</li> </ul> <p>참고</p> <p>복잡도 개념이 아직 혼란스러워도 걱정할 필요 없다. 이후 절에서 자세히 소개한다.</p> <p>복잡도 분석은 알고리즘 효율을 평가하는 ‘자’와 같다. 이를 통해 알고리즘 실행에 필요한 시간과 공간 자원을 가늠하고 여러 알고리즘의 효율을 비교할 수 있다.</p> <p>복잡도는 수학적 개념이므로 초보자에게는 추상적으로 느껴지고 배우기도 다소 어려울 수 있다. 이런 관점에서는 복잡도 분석이 맨 처음 소개하기에 적절하지 않을 수도 있다. 그러나 어떤 자료 구조나 알고리즘의 특징을 논할 때는 실행 속도와 공간 사용량에 대한 분석을 피하기 어렵다.</p> <p>따라서 자료 구조와 알고리즘을 깊이 학습하기 전에, 복잡도 분석을 기초적으로 이해하여 간단한 알고리즘의 복잡도를 분석할 수 있도록 하는 것을 권한다.</p>","path":["제 2 장   복잡도 분석","2.1   알고리즘 효율 평가"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/","level":1,"title":"2.4   공간 복잡도","text":"<p>공간 복잡도(space complexity)는 데이터 양이 많아질 때 알고리즘이 차지하는 메모리 공간이 증가하는 추세를 측정한다. 시간 복잡도와 매우 비슷한 개념으로, ‘실행 시간’을 ‘차지하는 메모리 공간’으로 바꾸면 된다.</p>","path":["제 2 장   복잡도 분석","2.4   공간 복잡도"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#241","level":2,"title":"2.4.1   알고리즘과 관련된 공간","text":"<p>알고리즘이 실행 중 사용하는 메모리 공간에는 주로 다음과 같은 종류가 있다.</p> <ul> <li>입력 공간: 알고리즘의 입력 데이터를 저장한다.</li> <li>임시 공간: 실행 중의 변수, 객체, 함수 문맥 등의 데이터를 저장한다.</li> <li>출력 공간: 알고리즘의 출력 데이터를 저장한다.</li> </ul> <p>일반적으로 공간 복잡도에 포함하는 범위는 ‘임시 공간’과 ‘출력 공간’의 합이다.</p> <p>임시 공간은 다시 세 부분으로 나눌 수 있다.</p> <ul> <li>임시 데이터: 실행 중의 각종 상수, 변수, 객체 등을 저장한다.</li> <li>스택 프레임 공간: 호출한 함수의 문맥 데이터를 저장한다. 시스템은 함수가 호출될 때마다 스택 맨 위에 스택 프레임을 만들며, 함수가 반환되면 이 공간을 해제한다.</li> <li>명령어 공간: 컴파일된 프로그램 명령어를 저장하며, 실제 계산에서는 보통 무시한다.</li> </ul> <p>프로그램의 공간 복잡도를 분석할 때는 아래 그림(2-15)과 같이 보통 임시 데이터, 스택 프레임 공간, 출력 데이터의 세 부분을 계산한다.</p> <p></p> <p> 그림 2-15   알고리즘이 사용하는 공간 </p> <p>관련 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby <pre><code>class Node:\n \"\"\"클래스\"\"\"\n def __init__(self, x: int):\n self.val: int = x # 노드 값\n self.next: Node | None = None # 다음 노드를 가리키는 참조\n\ndef function() -&gt; int:\n \"\"\"함수\"\"\"\n # 몇 가지 연산 수행...\n return 0\n\ndef algorithm(n) -&gt; int: # 입력 데이터\n A = 0 # 임시 데이터(상수, 보통 대문자로 표기)\n b = 0 # 임시 데이터(변수)\n node = Node(0) # 임시 데이터(객체)\n c = function() # 스택 프레임 공간(함수 호출)\n return A + b + c # 출력 데이터\n</code></pre> <pre><code>/* 구조체 */\nstruct Node {\n int val;\n Node *next;\n Node(int x) : val(x), next(nullptr) {}\n};\n\n/* 함수 */\nint func() {\n // 몇 가지 연산 수행...\n return 0;\n}\n\nint algorithm(int n) { // 입력 데이터\n const int a = 0; // 임시 데이터(상수)\n int b = 0; // 임시 데이터(변수)\n Node* node = new Node(0); // 임시 데이터(객체)\n int c = func(); // 스택 프레임 공간(함수 호출)\n return a + b + c; // 출력 데이터\n}\n</code></pre> <pre><code>/* 클래스 */\nclass Node {\n int val;\n Node next;\n Node(int x) { val = x; }\n}\n\n/* 함수 */\nint function() {\n // 몇 가지 연산 수행...\n return 0;\n}\n\nint algorithm(int n) { // 입력 데이터\n final int a = 0; // 임시 데이터(상수)\n int b = 0; // 임시 데이터(변수)\n Node node = new Node(0); // 임시 데이터(객체)\n int c = function(); // 스택 프레임 공간(함수 호출)\n return a + b + c; // 출력 데이터\n}\n</code></pre> <pre><code>/* 클래스 */\nclass Node(int x) {\n int val = x;\n Node next;\n}\n\n/* 함수 */\nint Function() {\n // 몇 가지 연산 수행...\n return 0;\n}\n\nint Algorithm(int n) { // 입력 데이터\n const int a = 0; // 임시 데이터(상수)\n int b = 0; // 임시 데이터(변수)\n Node node = new(0); // 임시 데이터(객체)\n int c = Function(); // 스택 프레임 공간(함수 호출)\n return a + b + c; // 출력 데이터\n}\n</code></pre> <pre><code>/* 구조체 */\ntype node struct {\n val int\n next *node\n}\n\n/* node 구조체 생성 */\nfunc newNode(val int) *node {\n return &amp;node{val: val}\n}\n\n/* 함수 */\nfunc function() int {\n // 몇 가지 연산 수행...\n return 0\n}\n\nfunc algorithm(n int) int { // 입력 데이터\n const a = 0 // 임시 데이터(상수)\n b := 0 // 임시 데이터(변수)\n newNode(0) // 임시 데이터(객체)\n c := function() // 스택 프레임 공간(함수 호출)\n return a + b + c // 출력 데이터\n}\n</code></pre> <pre><code>/* 클래스 */\nclass Node {\n var val: Int\n var next: Node?\n\n init(x: Int) {\n val = x\n }\n}\n\n/* 함수 */\nfunc function() -&gt; Int {\n // 몇 가지 연산 수행...\n return 0\n}\n\nfunc algorithm(n: Int) -&gt; Int { // 입력 데이터\n let a = 0 // 임시 데이터(상수)\n var b = 0 // 임시 데이터(변수)\n let node = Node(x: 0) // 임시 데이터(객체)\n let c = function() // 스택 프레임 공간(함수 호출)\n return a + b + c // 출력 데이터\n}\n</code></pre> <pre><code>/* 클래스 */\nclass Node {\n val;\n next;\n constructor(val) {\n this.val = val === undefined ? 0 : val; // 노드 값\n this.next = null; // 다음 노드를 가리키는 참조\n }\n}\n\n/* 함수 */\nfunction constFunc() {\n // 몇 가지 연산 수행\n return 0;\n}\n\nfunction algorithm(n) { // 입력 데이터\n const a = 0; // 임시 데이터(상수)\n let b = 0; // 임시 데이터(변수)\n const node = new Node(0); // 임시 데이터(객체)\n const c = constFunc(); // 스택 프레임 공간(함수 호출)\n return a + b + c; // 출력 데이터\n}\n</code></pre> <pre><code>/* 클래스 */\nclass Node {\n val: number;\n next: Node | null;\n constructor(val?: number) {\n this.val = val === undefined ? 0 : val; // 노드 값\n this.next = null; // 다음 노드를 가리키는 참조\n }\n}\n\n/* 함수 */\nfunction constFunc(): number {\n // 몇 가지 연산 수행\n return 0;\n}\n\nfunction algorithm(n: number): number { // 입력 데이터\n const a = 0; // 임시 데이터(상수)\n let b = 0; // 임시 데이터(변수)\n const node = new Node(0); // 임시 데이터(객체)\n const c = constFunc(); // 스택 프레임 공간(함수 호출)\n return a + b + c; // 출력 데이터\n}\n</code></pre> <pre><code>/* 클래스 */\nclass Node {\n int val;\n Node next;\n Node(this.val, [this.next]);\n}\n\n/* 함수 */\nint function() {\n // 몇 가지 연산 수행...\n return 0;\n}\n\nint algorithm(int n) { // 입력 데이터\n const int a = 0; // 임시 데이터(상수)\n int b = 0; // 임시 데이터(변수)\n Node node = Node(0); // 임시 데이터(객체)\n int c = function(); // 스택 프레임 공간(함수 호출)\n return a + b + c; // 출력 데이터\n}\n</code></pre> <pre><code>use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* 구조체 */\nstruct Node {\n val: i32,\n next: Option&lt;Rc&lt;RefCell&lt;Node&gt;&gt;&gt;,\n}\n\n/* Node 구조체 생성 */\nimpl Node {\n fn new(val: i32) -&gt; Self {\n Self { val: val, next: None }\n }\n}\n\n/* 함수 */\nfn function() -&gt; i32 { \n // 몇 가지 연산 수행...\n return 0;\n}\n\nfn algorithm(n: i32) -&gt; i32 { // 입력 데이터\n const a: i32 = 0; // 임시 데이터(상수)\n let mut b = 0; // 임시 데이터(변수)\n let node = Node::new(0); // 임시 데이터(객체)\n let c = function(); // 스택 프레임 공간(함수 호출)\n return a + b + c; // 출력 데이터\n}\n</code></pre> <pre><code>/* 함수 */\nint func() {\n // 몇 가지 연산 수행...\n return 0;\n}\n\nint algorithm(int n) { // 입력 데이터\n const int a = 0; // 임시 데이터(상수)\n int b = 0; // 임시 데이터(변수)\n int c = func(); // 스택 프레임 공간(함수 호출)\n return a + b + c; // 출력 데이터\n}\n</code></pre> <pre><code>/* 클래스 */\nclass Node(var _val: Int) {\n var next: Node? = null\n}\n\n/* 함수 */\nfun function(): Int {\n // 몇 가지 연산 수행...\n return 0\n}\n\nfun algorithm(n: Int): Int { // 입력 데이터\n val a = 0 // 임시 데이터(상수)\n var b = 0 // 임시 데이터(변수)\n val node = Node(0) // 임시 데이터(객체)\n val c = function() // 스택 프레임 공간(함수 호출)\n return a + b + c // 출력 데이터\n}\n</code></pre> <pre><code>### 클래스 ###\nclass Node\n attr_accessor :val # 노드 값\n attr_accessor :next # 다음 노드를 가리키는 참조\n\n def initialize(x)\n @val = x\n end\nend\n\n### 함수 ###\ndef function\n # 몇 가지 연산 수행...\n 0\nend\n\n### 알고리즘 ###\ndef algorithm(n) # 입력 데이터\n a = 0 # 임시 데이터(상수)\n b = 0 # 임시 데이터(변수)\n node = Node.new(0) # 임시 데이터(객체)\n c = function # 스택 프레임 공간(함수 호출)\n a + b + c # 출력 데이터\nend\n</code></pre>","path":["제 2 장   복잡도 분석","2.4   공간 복잡도"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#242","level":2,"title":"2.4.2   계산 방법","text":"<p>공간 복잡도를 구하는 방법은 시간 복잡도와 대체로 같다. 계산 대상을 ‘연산 횟수’에서 ‘사용 공간의 크기’로 바꾸면 된다.</p> <p>시간 복잡도와 달리 보통 최악 공간 복잡도에만 관심을 둔다. 메모리 공간은 반드시 충족해야 하는 요구 사항이므로, 어떤 입력 데이터에서도 충분한 메모리 공간이 확보되어 있어야 하기 때문이다.</p> <p>다음 코드를 보면 최악 공간 복잡도에서 ‘최악’은 두 가지 의미를 지닌다.</p> <ol> <li>가장 불리한 입력 데이터를 기준으로 한다: \\(n &lt; 10\\)이면 공간 복잡도는 \\(O(1)\\)이다. 그러나 \\(n &gt; 10\\)이면 초기화한 배열 <code>nums</code>가 \\(O(n)\\) 공간을 차지하므로 최악 공간 복잡도는 \\(O(n)\\)이다.</li> <li>알고리즘 실행 중 최대 메모리 사용량을 기준으로 한다: 예를 들어 프로그램이 마지막 줄을 실행하기 전에는 \\(O(1)\\) 공간을 차지하지만, 배열 <code>nums</code>를 초기화하면 \\(O(n)\\) 공간을 차지한다. 따라서 최악 공간 복잡도는 \\(O(n)\\)이다.</li> </ol> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby <pre><code>def algorithm(n: int):\n a = 0 # O(1)\n b = [0] * 10000 # O(1)\n if n &gt; 10:\n nums = [0] * n # O(n)\n</code></pre> <pre><code>void algorithm(int n) {\n int a = 0; // O(1)\n vector&lt;int&gt; b(10000); // O(1)\n if (n &gt; 10)\n vector&lt;int&gt; nums(n); // O(n)\n}\n</code></pre> <pre><code>void algorithm(int n) {\n int a = 0; // O(1)\n int[] b = new int[10000]; // O(1)\n if (n &gt; 10)\n int[] nums = new int[n]; // O(n)\n}\n</code></pre> <pre><code>void Algorithm(int n) {\n int a = 0; // O(1)\n int[] b = new int[10000]; // O(1)\n if (n &gt; 10) {\n int[] nums = new int[n]; // O(n)\n }\n}\n</code></pre> <pre><code>func algorithm(n int) {\n a := 0 // O(1)\n b := make([]int, 10000) // O(1)\n var nums []int\n if n &gt; 10 {\n nums := make([]int, n) // O(n)\n }\n fmt.Println(a, b, nums)\n}\n</code></pre> <pre><code>func algorithm(n: Int) {\n let a = 0 // O(1)\n let b = Array(repeating: 0, count: 10000) // O(1)\n if n &gt; 10 {\n let nums = Array(repeating: 0, count: n) // O(n)\n }\n}\n</code></pre> <pre><code>function algorithm(n) {\n const a = 0; // O(1)\n const b = new Array(10000); // O(1)\n if (n &gt; 10) {\n const nums = new Array(n); // O(n)\n }\n}\n</code></pre> <pre><code>function algorithm(n: number): void {\n const a = 0; // O(1)\n const b = new Array(10000); // O(1)\n if (n &gt; 10) {\n const nums = new Array(n); // O(n)\n }\n}\n</code></pre> <pre><code>void algorithm(int n) {\n int a = 0; // O(1)\n List&lt;int&gt; b = List.filled(10000, 0); // O(1)\n if (n &gt; 10) {\n List&lt;int&gt; nums = List.filled(n, 0); // O(n)\n }\n}\n</code></pre> <pre><code>fn algorithm(n: i32) {\n let a = 0; // O(1)\n let b = [0; 10000]; // O(1)\n if n &gt; 10 {\n let nums = vec![0; n as usize]; // O(n)\n }\n}\n</code></pre> <pre><code>void algorithm(int n) {\n int a = 0; // O(1)\n int b[10000]; // O(1)\n if (n &gt; 10)\n int nums[n] = {0}; // O(n)\n}\n</code></pre> <pre><code>fun algorithm(n: Int) {\n val a = 0 // O(1)\n val b = IntArray(10000) // O(1)\n if (n &gt; 10) {\n val nums = IntArray(n) // O(n)\n }\n}\n</code></pre> <pre><code>def algorithm(n)\n a = 0 # O(1)\n b = Array.new(10000) # O(1)\n nums = Array.new(n) if n &gt; 10 # O(n)\nend\n</code></pre> <p>재귀 함수에서는 스택 프레임 공간을 계산하는 데 주의해야 한다. 다음 코드를 살펴보자.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby <pre><code>def function() -&gt; int:\n # 몇 가지 연산 수행\n return 0\n\ndef loop(n: int):\n \"\"\"반복문의 공간 복잡도는 O(1)\"\"\"\n for _ in range(n):\n function()\n\ndef recur(n: int):\n \"\"\"재귀의 공간 복잡도는 O(n)\"\"\"\n if n == 1:\n return\n return recur(n - 1)\n</code></pre> <pre><code>int func() {\n // 몇 가지 연산 수행\n return 0;\n}\n/* 반복문의 공간 복잡도는 O(1) */\nvoid loop(int n) {\n for (int i = 0; i &lt; n; i++) {\n func();\n }\n}\n/* 재귀의 공간 복잡도는 O(n) */\nvoid recur(int n) {\n if (n == 1) return;\n recur(n - 1);\n}\n</code></pre> <pre><code>int function() {\n // 몇 가지 연산 수행\n return 0;\n}\n/* 반복문의 공간 복잡도는 O(1) */\nvoid loop(int n) {\n for (int i = 0; i &lt; n; i++) {\n function();\n }\n}\n/* 재귀의 공간 복잡도는 O(n) */\nvoid recur(int n) {\n if (n == 1) return;\n recur(n - 1);\n}\n</code></pre> <pre><code>int Function() {\n // 몇 가지 연산 수행\n return 0;\n}\n/* 반복문의 공간 복잡도는 O(1) */\nvoid Loop(int n) {\n for (int i = 0; i &lt; n; i++) {\n Function();\n }\n}\n/* 재귀의 공간 복잡도는 O(n) */\nint Recur(int n) {\n if (n == 1) return 1;\n return Recur(n - 1);\n}\n</code></pre> <pre><code>func function() int {\n // 몇 가지 연산 수행\n return 0\n}\n\n/* 반복문의 공간 복잡도는 O(1) */\nfunc loop(n int) {\n for i := 0; i &lt; n; i++ {\n function()\n }\n}\n\n/* 재귀의 공간 복잡도는 O(n) */\nfunc recur(n int) {\n if n == 1 {\n return\n }\n recur(n - 1)\n}\n</code></pre> <pre><code>@discardableResult\nfunc function() -&gt; Int {\n // 몇 가지 연산 수행\n return 0\n}\n\n/* 반복문의 공간 복잡도는 O(1) */\nfunc loop(n: Int) {\n for _ in 0 ..&lt; n {\n function()\n }\n}\n\n/* 재귀의 공간 복잡도는 O(n) */\nfunc recur(n: Int) {\n if n == 1 {\n return\n }\n recur(n: n - 1)\n}\n</code></pre> <pre><code>function constFunc() {\n // 몇 가지 연산 수행\n return 0;\n}\n/* 반복문의 공간 복잡도는 O(1) */\nfunction loop(n) {\n for (let i = 0; i &lt; n; i++) {\n constFunc();\n }\n}\n/* 재귀의 공간 복잡도는 O(n) */\nfunction recur(n) {\n if (n === 1) return;\n return recur(n - 1);\n}\n</code></pre> <pre><code>function constFunc(): number {\n // 몇 가지 연산 수행\n return 0;\n}\n/* 반복문의 공간 복잡도는 O(1) */\nfunction loop(n: number): void {\n for (let i = 0; i &lt; n; i++) {\n constFunc();\n }\n}\n/* 재귀의 공간 복잡도는 O(n) */\nfunction recur(n: number): void {\n if (n === 1) return;\n return recur(n - 1);\n}\n</code></pre> <pre><code>int function() {\n // 몇 가지 연산 수행\n return 0;\n}\n/* 반복문의 공간 복잡도는 O(1) */\nvoid loop(int n) {\n for (int i = 0; i &lt; n; i++) {\n function();\n }\n}\n/* 재귀의 공간 복잡도는 O(n) */\nvoid recur(int n) {\n if (n == 1) return;\n recur(n - 1);\n}\n</code></pre> <pre><code>fn function() -&gt; i32 {\n // 몇 가지 연산 수행\n return 0;\n}\n/* 반복문의 공간 복잡도는 O(1) */\nfn loop(n: i32) {\n for i in 0..n {\n function();\n }\n}\n/* 재귀의 공간 복잡도는 O(n) */\nfn recur(n: i32) {\n if n == 1 {\n return;\n }\n recur(n - 1);\n}\n</code></pre> <pre><code>int func() {\n // 몇 가지 연산 수행\n return 0;\n}\n/* 반복문의 공간 복잡도는 O(1) */\nvoid loop(int n) {\n for (int i = 0; i &lt; n; i++) {\n func();\n }\n}\n/* 재귀의 공간 복잡도는 O(n) */\nvoid recur(int n) {\n if (n == 1) return;\n recur(n - 1);\n}\n</code></pre> <pre><code>fun function(): Int {\n // 몇 가지 연산 수행\n return 0\n}\n/* 반복문의 공간 복잡도는 O(1) */\nfun loop(n: Int) {\n for (i in 0..&lt;n) {\n function()\n }\n}\n/* 재귀의 공간 복잡도는 O(n) */\nfun recur(n: Int) {\n if (n == 1) return\n return recur(n - 1)\n}\n</code></pre> <pre><code>def function\n # 몇 가지 연산 수행\n 0\nend\n\n### 반복문의 공간 복잡도는 O(1) ###\ndef loop(n)\n (0...n).each { function }\nend\n\n### 재귀의 공간 복잡도는 O(n) ###\ndef recur(n)\n return if n == 1\n recur(n - 1)\nend\n</code></pre> <p>함수 <code>loop()</code>와 <code>recur()</code>의 시간 복잡도는 모두 \\(O(n)\\)이지만 공간 복잡도는 다르다.</p> <ul> <li>함수 <code>loop()</code>는 반복문에서 <code>function()</code>을 \\(n\\)번 호출한다. 매번 <code>function()</code>이 반환되어 스택 프레임 공간이 해제되므로, 공간 복잡도는 여전히 \\(O(1)\\)이다.</li> <li>재귀 함수 <code>recur()</code>의 실행 중에는 아직 반환되지 않은 <code>recur()</code>가 \\(n\\)개 동시에 존재하므로, \\(O(n)\\)의 스택 프레임 공간을 차지한다.</li> </ul>","path":["제 2 장   복잡도 분석","2.4   공간 복잡도"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#243","level":2,"title":"2.4.3   대표적인 유형","text":"<p>입력 데이터 크기를 \\(n\\)이라고 할 때, 아래 그림(2-16)은 대표적인 공간 복잡도 유형을 낮은 순서대로 보여 준다.</p> \\[ \\begin{aligned} &amp; O(1) &lt; O(\\log n) &lt; O(n) &lt; O(n^2) &lt; O(2^n) \\newline &amp; \\text{상수} &lt; \\text{로그} &lt; \\text{선형} &lt; \\text{제곱} &lt; \\text{지수} \\end{aligned} \\] <p></p> <p> 그림 2-16   대표적인 공간 복잡도 유형 </p>","path":["제 2 장   복잡도 분석","2.4   공간 복잡도"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#1-o1","level":3,"title":"1.   상수 공간 \\(O(1)\\)","text":"<p>상수 공간은 개수가 입력 데이터 크기 \\(n\\)과 무관한 상수, 변수, 객체에서 흔히 나타난다.</p> <p>반복문에서 변수를 초기화하거나 함수를 호출할 때 사용하는 메모리는 다음 반복에 들어가면 해제되므로 공간이 누적되지 않는다. 따라서 공간 복잡도는 여전히 \\(O(1)\\)이다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py<pre><code>def function() -&gt; int:\n \"\"\"함수\"\"\"\n # 몇 가지 연산을 수행한다\n return 0\n\ndef constant(n: int):\n \"\"\"상수 차수\"\"\"\n # 상수, 변수, 객체는 O(1) 공간을 차지한다\n a = 0\n nums = [0] * 10000\n node = ListNode(0)\n # 반복문 안의 변수는 O(1) 공간을 차지한다\n for _ in range(n):\n c = 0\n # 반복문 안의 함수는 O(1) 공간을 차지한다\n for _ in range(n):\n function()\n</code></pre> space_complexity.cpp<pre><code>/* 함수 */\nint func() {\n // 몇 가지 연산을 수행한다\n return 0;\n}\n\n/* 상수 차수 */\nvoid constant(int n) {\n // 상수, 변수, 객체는 O(1) 공간을 차지한다\n const int a = 0;\n int b = 0;\n vector&lt;int&gt; nums(10000);\n ListNode node(0);\n // 반복문 안의 변수는 O(1) 공간을 차지한다\n for (int i = 0; i &lt; n; i++) {\n int c = 0;\n }\n // 반복문 안의 함수는 O(1) 공간을 차지한다\n for (int i = 0; i &lt; n; i++) {\n func();\n }\n}\n</code></pre> space_complexity.java<pre><code>/* 함수 */\nint function() {\n // 몇 가지 연산을 수행한다\n return 0;\n}\n\n/* 상수 차수 */\nvoid constant(int n) {\n // 상수, 변수, 객체는 O(1) 공간을 차지한다\n final int a = 0;\n int b = 0;\n int[] nums = new int[10000];\n ListNode node = new ListNode(0);\n // 반복문 안의 변수는 O(1) 공간을 차지한다\n for (int i = 0; i &lt; n; i++) {\n int c = 0;\n }\n // 반복문 안의 함수는 O(1) 공간을 차지한다\n for (int i = 0; i &lt; n; i++) {\n function();\n }\n}\n</code></pre> space_complexity.cs<pre><code>/* 함수 */\nint Function() {\n // 몇 가지 연산을 수행한다\n return 0;\n}\n\n/* 상수 차수 */\nvoid Constant(int n) {\n // 상수, 변수, 객체는 O(1) 공간을 차지한다\n int a = 0;\n int b = 0;\n int[] nums = new int[10000];\n ListNode node = new(0);\n // 반복문 안의 변수는 O(1) 공간을 차지한다\n for (int i = 0; i &lt; n; i++) {\n int c = 0;\n }\n // 반복문 안의 함수는 O(1) 공간을 차지한다\n for (int i = 0; i &lt; n; i++) {\n Function();\n }\n}\n</code></pre> space_complexity.go<pre><code>/* 함수 */\nfunc function() int {\n // 몇 가지 연산을 수행한다...\n return 0\n}\n\n/* 상수 차수 */\nfunc spaceConstant(n int) {\n // 상수, 변수, 객체는 O(1) 공간을 차지한다\n const a = 0\n b := 0\n nums := make([]int, 10000)\n node := newNode(0)\n // 반복문 안의 변수는 O(1) 공간을 차지한다\n var c int\n for i := 0; i &lt; n; i++ {\n c = 0\n }\n // 반복문 안의 함수는 O(1) 공간을 차지한다\n for i := 0; i &lt; n; i++ {\n function()\n }\n b += 0\n c += 0\n nums[0] = 0\n node.val = 0\n}\n</code></pre> space_complexity.swift<pre><code>/* 함수 */\n@discardableResult\nfunc function() -&gt; Int {\n // 몇 가지 연산을 수행한다\n return 0\n}\n\n/* 상수 차수 */\nfunc constant(n: Int) {\n // 상수, 변수, 객체는 O(1) 공간을 차지한다\n let a = 0\n var b = 0\n let nums = Array(repeating: 0, count: 10000)\n let node = ListNode(x: 0)\n // 반복문 안의 변수는 O(1) 공간을 차지한다\n for _ in 0 ..&lt; n {\n let c = 0\n }\n // 반복문 안의 함수는 O(1) 공간을 차지한다\n for _ in 0 ..&lt; n {\n function()\n }\n}\n</code></pre> space_complexity.js<pre><code>/* 함수 */\nfunction constFunc() {\n // 몇 가지 연산을 수행한다\n return 0;\n}\n\n/* 상수 차수 */\nfunction constant(n) {\n // 상수, 변수, 객체는 O(1) 공간을 차지한다\n const a = 0;\n const b = 0;\n const nums = new Array(10000);\n const node = new ListNode(0);\n // 반복문 안의 변수는 O(1) 공간을 차지한다\n for (let i = 0; i &lt; n; i++) {\n const c = 0;\n }\n // 반복문 안의 함수는 O(1) 공간을 차지한다\n for (let i = 0; i &lt; n; i++) {\n constFunc();\n }\n}\n</code></pre> space_complexity.ts<pre><code>/* 함수 */\nfunction constFunc(): number {\n // 몇 가지 연산을 수행한다\n return 0;\n}\n\n/* 상수 차수 */\nfunction constant(n: number): void {\n // 상수, 변수, 객체는 O(1) 공간을 차지한다\n const a = 0;\n const b = 0;\n const nums = new Array(10000);\n const node = new ListNode(0);\n // 반복문 안의 변수는 O(1) 공간을 차지한다\n for (let i = 0; i &lt; n; i++) {\n const c = 0;\n }\n // 반복문 안의 함수는 O(1) 공간을 차지한다\n for (let i = 0; i &lt; n; i++) {\n constFunc();\n }\n}\n</code></pre> space_complexity.dart<pre><code>/* 함수 */\nint function() {\n // 몇 가지 연산을 수행한다\n return 0;\n}\n\n/* 상수 차수 */\nvoid constant(int n) {\n // 상수, 변수, 객체는 O(1) 공간을 차지한다\n final int a = 0;\n int b = 0;\n List&lt;int&gt; nums = List.filled(10000, 0);\n ListNode node = ListNode(0);\n // 반복문 안의 변수는 O(1) 공간을 차지한다\n for (var i = 0; i &lt; n; i++) {\n int c = 0;\n }\n // 반복문 안의 함수는 O(1) 공간을 차지한다\n for (var i = 0; i &lt; n; i++) {\n function();\n }\n}\n</code></pre> space_complexity.rs<pre><code>/* 함수 */\nfn function() -&gt; i32 {\n // 몇 가지 연산을 수행한다\n return 0;\n}\n\n/* 상수 차수 */\n#[allow(unused)]\nfn constant(n: i32) {\n // 상수, 변수, 객체는 O(1) 공간을 차지한다\n const A: i32 = 0;\n let b = 0;\n let nums = vec![0; 10000];\n let node = ListNode::new(0);\n // 반복문 안의 변수는 O(1) 공간을 차지한다\n for i in 0..n {\n let c = 0;\n }\n // 반복문 안의 함수는 O(1) 공간을 차지한다\n for i in 0..n {\n function();\n }\n}\n</code></pre> space_complexity.c<pre><code>/* 함수 */\nint func() {\n // 몇 가지 연산을 수행한다\n return 0;\n}\n\n/* 상수 차수 */\nvoid constant(int n) {\n // 상수, 변수, 객체는 O(1) 공간을 차지한다\n const int a = 0;\n int b = 0;\n int nums[1000];\n ListNode *node = newListNode(0);\n free(node);\n // 반복문 안의 변수는 O(1) 공간을 차지한다\n for (int i = 0; i &lt; n; i++) {\n int c = 0;\n }\n // 반복문 안의 함수는 O(1) 공간을 차지한다\n for (int i = 0; i &lt; n; i++) {\n func();\n }\n}\n</code></pre> space_complexity.kt<pre><code>/* 함수 */\nfun function(): Int {\n // 몇 가지 연산을 수행한다\n return 0\n}\n\n/* 상수 차수 */\nfun constant(n: Int) {\n // 상수, 변수, 객체는 O(1) 공간을 차지한다\n val a = 0\n var b = 0\n val nums = Array(10000) { 0 }\n val node = ListNode(0)\n // 반복문 안의 변수는 O(1) 공간을 차지한다\n for (i in 0..&lt;n) {\n val c = 0\n }\n // 반복문 안의 함수는 O(1) 공간을 차지한다\n for (i in 0..&lt;n) {\n function()\n }\n}\n</code></pre> space_complexity.rb<pre><code>### 함수 ###\ndef function\n # 몇 가지 연산을 수행한다\n 0\nend\n\n### 상수 차수 ###\ndef constant(n)\n # 상수, 변수, 객체는 O(1) 공간을 차지한다\n a = 0\n nums = [0] * 10000\n node = ListNode.new\n\n # 반복문 안의 변수는 O(1) 공간을 차지한다\n (0...n).each { c = 0 }\n # 반복문 안의 함수는 O(1) 공간을 차지한다\n (0...n).each { function }\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 2 장   복잡도 분석","2.4   공간 복잡도"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#2-on","level":3,"title":"2.   선형 공간 \\(O(n)\\)","text":"<p>선형 공간은 원소 수가 \\(n\\)에 비례하는 배열, 연결 리스트, 스택, 큐 등에 흔히 나타난다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py<pre><code>def linear(n: int):\n \"\"\"선형 차수\"\"\"\n # 길이가 n인 리스트는 O(n) 공간을 차지한다\n nums = [0] * n\n # 길이가 n인 해시 테이블은 O(n) 공간을 차지한다\n hmap = dict[int, str]()\n for i in range(n):\n hmap[i] = str(i)\n</code></pre> space_complexity.cpp<pre><code>/* 선형 차수 */\nvoid linear(int n) {\n // 길이가 n인 배열은 O(n) 공간을 차지한다\n vector&lt;int&gt; nums(n);\n // 길이가 n인 리스트는 O(n) 공간을 차지한다\n vector&lt;ListNode&gt; nodes;\n for (int i = 0; i &lt; n; i++) {\n nodes.push_back(ListNode(i));\n }\n // 길이가 n인 해시 테이블은 O(n) 공간을 차지한다\n unordered_map&lt;int, string&gt; map;\n for (int i = 0; i &lt; n; i++) {\n map[i] = to_string(i);\n }\n}\n</code></pre> space_complexity.java<pre><code>/* 선형 차수 */\nvoid linear(int n) {\n // 길이가 n인 배열은 O(n) 공간을 차지한다\n int[] nums = new int[n];\n // 길이가 n인 리스트는 O(n) 공간을 차지한다\n List&lt;ListNode&gt; nodes = new ArrayList&lt;&gt;();\n for (int i = 0; i &lt; n; i++) {\n nodes.add(new ListNode(i));\n }\n // 길이가 n인 해시 테이블은 O(n) 공간을 차지한다\n Map&lt;Integer, String&gt; map = new HashMap&lt;&gt;();\n for (int i = 0; i &lt; n; i++) {\n map.put(i, String.valueOf(i));\n }\n}\n</code></pre> space_complexity.cs<pre><code>/* 선형 차수 */\nvoid Linear(int n) {\n // 길이가 n인 배열은 O(n) 공간을 차지한다\n int[] nums = new int[n];\n // 길이가 n인 리스트는 O(n) 공간을 차지한다\n List&lt;ListNode&gt; nodes = [];\n for (int i = 0; i &lt; n; i++) {\n nodes.Add(new ListNode(i));\n }\n // 길이가 n인 해시 테이블은 O(n) 공간을 차지한다\n Dictionary&lt;int, string&gt; map = [];\n for (int i = 0; i &lt; n; i++) {\n map.Add(i, i.ToString());\n }\n}\n</code></pre> space_complexity.go<pre><code>/* 선형 차수 */\nfunc spaceLinear(n int) {\n // 길이가 n인 배열은 O(n) 공간을 차지한다\n _ = make([]int, n)\n // 길이가 n인 리스트는 O(n) 공간을 차지한다\n var nodes []*node\n for i := 0; i &lt; n; i++ {\n nodes = append(nodes, newNode(i))\n }\n // 길이가 n인 해시 테이블은 O(n) 공간을 차지한다\n m := make(map[int]string, n)\n for i := 0; i &lt; n; i++ {\n m[i] = strconv.Itoa(i)\n }\n}\n</code></pre> space_complexity.swift<pre><code>/* 선형 차수 */\nfunc linear(n: Int) {\n // 길이가 n인 배열은 O(n) 공간을 차지한다\n let nums = Array(repeating: 0, count: n)\n // 길이가 n인 리스트는 O(n) 공간을 차지한다\n let nodes = (0 ..&lt; n).map { ListNode(x: $0) }\n // 길이가 n인 해시 테이블은 O(n) 공간을 차지한다\n let map = Dictionary(uniqueKeysWithValues: (0 ..&lt; n).map { ($0, \"\\($0)\") })\n}\n</code></pre> space_complexity.js<pre><code>/* 선형 차수 */\nfunction linear(n) {\n // 길이가 n인 배열은 O(n) 공간을 차지한다\n const nums = new Array(n);\n // 길이가 n인 리스트는 O(n) 공간을 차지한다\n const nodes = [];\n for (let i = 0; i &lt; n; i++) {\n nodes.push(new ListNode(i));\n }\n // 길이가 n인 해시 테이블은 O(n) 공간을 차지한다\n const map = new Map();\n for (let i = 0; i &lt; n; i++) {\n map.set(i, i.toString());\n }\n}\n</code></pre> space_complexity.ts<pre><code>/* 선형 차수 */\nfunction linear(n: number): void {\n // 길이가 n인 배열은 O(n) 공간을 차지한다\n const nums = new Array(n);\n // 길이가 n인 리스트는 O(n) 공간을 차지한다\n const nodes: ListNode[] = [];\n for (let i = 0; i &lt; n; i++) {\n nodes.push(new ListNode(i));\n }\n // 길이가 n인 해시 테이블은 O(n) 공간을 차지한다\n const map = new Map();\n for (let i = 0; i &lt; n; i++) {\n map.set(i, i.toString());\n }\n}\n</code></pre> space_complexity.dart<pre><code>/* 선형 차수 */\nvoid linear(int n) {\n // 길이가 n인 배열은 O(n) 공간을 차지한다\n List&lt;int&gt; nums = List.filled(n, 0);\n // 길이가 n인 리스트는 O(n) 공간을 차지한다\n List&lt;ListNode&gt; nodes = [];\n for (var i = 0; i &lt; n; i++) {\n nodes.add(ListNode(i));\n }\n // 길이가 n인 해시 테이블은 O(n) 공간을 차지한다\n Map&lt;int, String&gt; map = HashMap();\n for (var i = 0; i &lt; n; i++) {\n map.putIfAbsent(i, () =&gt; i.toString());\n }\n}\n</code></pre> space_complexity.rs<pre><code>/* 선형 차수 */\n#[allow(unused)]\nfn linear(n: i32) {\n // 길이가 n인 배열은 O(n) 공간을 차지한다\n let mut nums = vec![0; n as usize];\n // 길이가 n인 리스트는 O(n) 공간을 차지한다\n let mut nodes = Vec::new();\n for i in 0..n {\n nodes.push(ListNode::new(i))\n }\n // 길이가 n인 해시 테이블은 O(n) 공간을 차지한다\n let mut map = HashMap::new();\n for i in 0..n {\n map.insert(i, i.to_string());\n }\n}\n</code></pre> space_complexity.c<pre><code>/* 해시 테이블 */\ntypedef struct {\n int key;\n int val;\n UT_hash_handle hh; // uthash.h를 기반으로 구현한다\n} HashTable;\n\n/* 선형 차수 */\nvoid linear(int n) {\n // 길이가 n인 배열은 O(n) 공간을 차지한다\n int *nums = malloc(sizeof(int) * n);\n free(nums);\n\n // 길이가 n인 리스트는 O(n) 공간을 차지한다\n ListNode **nodes = malloc(sizeof(ListNode *) * n);\n for (int i = 0; i &lt; n; i++) {\n nodes[i] = newListNode(i);\n }\n // 메모리 해제\n for (int i = 0; i &lt; n; i++) {\n free(nodes[i]);\n }\n free(nodes);\n\n // 길이가 n인 해시 테이블은 O(n) 공간을 차지한다\n HashTable *h = NULL;\n for (int i = 0; i &lt; n; i++) {\n HashTable *tmp = malloc(sizeof(HashTable));\n tmp-&gt;key = i;\n tmp-&gt;val = i;\n HASH_ADD_INT(h, key, tmp);\n }\n\n // 메모리 해제\n HashTable *curr, *tmp;\n HASH_ITER(hh, h, curr, tmp) {\n HASH_DEL(h, curr);\n free(curr);\n }\n}\n</code></pre> space_complexity.kt<pre><code>/* 선형 차수 */\nfun linear(n: Int) {\n // 길이가 n인 배열은 O(n) 공간을 차지한다\n val nums = Array(n) { 0 }\n // 길이가 n인 리스트는 O(n) 공간을 차지한다\n val nodes = mutableListOf&lt;ListNode&gt;()\n for (i in 0..&lt;n) {\n nodes.add(ListNode(i))\n }\n // 길이가 n인 해시 테이블은 O(n) 공간을 차지한다\n val map = mutableMapOf&lt;Int, String&gt;()\n for (i in 0..&lt;n) {\n map[i] = i.toString()\n }\n}\n</code></pre> space_complexity.rb<pre><code>### 선형 차수 ###\ndef linear(n)\n # 길이가 n인 리스트는 O(n) 공간을 차지한다\n nums = Array.new(n, 0)\n\n # 길이가 n인 해시 테이블은 O(n) 공간을 차지한다\n hmap = {}\n for i in 0...n\n hmap[i] = i.to_s\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>아래 그림(2-17)과 같이 이 함수의 재귀 깊이는 \\(n\\)이다. 즉, 아직 반환되지 않은 <code>linear_recur()</code> 함수가 \\(n\\)개 동시에 존재하며 \\(O(n)\\) 크기의 스택 프레임 공간을 사용한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py<pre><code>def linear_recur(n: int):\n \"\"\"선형 차수(재귀 구현)\"\"\"\n print(\"재귀 n =\", n)\n if n == 1:\n return\n linear_recur(n - 1)\n</code></pre> space_complexity.cpp<pre><code>/* 선형 차수(재귀 구현) */\nvoid linearRecur(int n) {\n cout &lt;&lt; \"재귀 n = \" &lt;&lt; n &lt;&lt; endl;\n if (n == 1)\n return;\n linearRecur(n - 1);\n}\n</code></pre> space_complexity.java<pre><code>/* 선형 차수(재귀 구현) */\nvoid linearRecur(int n) {\n System.out.println(\"재귀 n = \" + n);\n if (n == 1)\n return;\n linearRecur(n - 1);\n}\n</code></pre> space_complexity.cs<pre><code>/* 선형 차수(재귀 구현) */\nvoid LinearRecur(int n) {\n Console.WriteLine(\"재귀 n = \" + n);\n if (n == 1) return;\n LinearRecur(n - 1);\n}\n</code></pre> space_complexity.go<pre><code>/* 선형 차수(재귀 구현) */\nfunc spaceLinearRecur(n int) {\n fmt.Println(\"재귀 n =\", n)\n if n == 1 {\n return\n }\n spaceLinearRecur(n - 1)\n}\n</code></pre> space_complexity.swift<pre><code>/* 선형 차수(재귀 구현) */\nfunc linearRecur(n: Int) {\n print(\"재귀 n = \\(n)\")\n if n == 1 {\n return\n }\n linearRecur(n: n - 1)\n}\n</code></pre> space_complexity.js<pre><code>/* 선형 차수(재귀 구현) */\nfunction linearRecur(n) {\n console.log(`재귀 n = ${n}`);\n if (n === 1) return;\n linearRecur(n - 1);\n}\n</code></pre> space_complexity.ts<pre><code>/* 선형 차수(재귀 구현) */\nfunction linearRecur(n: number): void {\n console.log(`재귀 n = ${n}`);\n if (n === 1) return;\n linearRecur(n - 1);\n}\n</code></pre> space_complexity.dart<pre><code>/* 선형 차수(재귀 구현) */\nvoid linearRecur(int n) {\n print('재귀 n = $n');\n if (n == 1) return;\n linearRecur(n - 1);\n}\n</code></pre> space_complexity.rs<pre><code>/* 선형 차수(재귀 구현) */\nfn linear_recur(n: i32) {\n println!(\"재귀 n = {}\", n);\n if n == 1 {\n return;\n };\n linear_recur(n - 1);\n}\n</code></pre> space_complexity.c<pre><code>/* 선형 차수(재귀 구현) */\nvoid linearRecur(int n) {\n printf(\"재귀 n = %d\\r\\n\", n);\n if (n == 1)\n return;\n linearRecur(n - 1);\n}\n</code></pre> space_complexity.kt<pre><code>/* 선형 차수(재귀 구현) */\nfun linearRecur(n: Int) {\n println(\"재귀 n = $n\")\n if (n == 1)\n return\n linearRecur(n - 1)\n}\n</code></pre> space_complexity.rb<pre><code>### 선형 차수(재귀 구현)###\ndef linear_recur(n)\n puts \"재귀 n = #{n}\"\n return if n == 1\n linear_recur(n - 1)\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p></p> <p> 그림 2-17   재귀 함수에서 발생하는 선형 공간 복잡도 </p>","path":["제 2 장   복잡도 분석","2.4   공간 복잡도"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#3-on2","level":3,"title":"3.   제곱 공간 \\(O(n^2)\\)","text":"<p>제곱 공간은 원소 수가 \\(n\\)의 제곱에 비례하는 행렬과 그래프에 흔히 나타난다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py<pre><code>def quadratic(n: int):\n \"\"\"제곱 차수\"\"\"\n # 이차원 리스트는 O(n^2) 공간을 차지한다\n num_matrix = [[0] * n for _ in range(n)]\n</code></pre> space_complexity.cpp<pre><code>/* 제곱 차수 */\nvoid quadratic(int n) {\n // 이차원 리스트는 O(n^2) 공간을 차지한다\n vector&lt;vector&lt;int&gt;&gt; numMatrix;\n for (int i = 0; i &lt; n; i++) {\n vector&lt;int&gt; tmp;\n for (int j = 0; j &lt; n; j++) {\n tmp.push_back(0);\n }\n numMatrix.push_back(tmp);\n }\n}\n</code></pre> space_complexity.java<pre><code>/* 제곱 차수 */\nvoid quadratic(int n) {\n // 행렬은 O(n^2) 공간을 차지한다\n int[][] numMatrix = new int[n][n];\n // 이차원 리스트는 O(n^2) 공간을 차지한다\n List&lt;List&lt;Integer&gt;&gt; numList = new ArrayList&lt;&gt;();\n for (int i = 0; i &lt; n; i++) {\n List&lt;Integer&gt; tmp = new ArrayList&lt;&gt;();\n for (int j = 0; j &lt; n; j++) {\n tmp.add(0);\n }\n numList.add(tmp);\n }\n}\n</code></pre> space_complexity.cs<pre><code>/* 제곱 차수 */\nvoid Quadratic(int n) {\n // 행렬은 O(n^2) 공간을 차지한다\n int[,] numMatrix = new int[n, n];\n // 이차원 리스트는 O(n^2) 공간을 차지한다\n List&lt;List&lt;int&gt;&gt; numList = [];\n for (int i = 0; i &lt; n; i++) {\n List&lt;int&gt; tmp = [];\n for (int j = 0; j &lt; n; j++) {\n tmp.Add(0);\n }\n numList.Add(tmp);\n }\n}\n</code></pre> space_complexity.go<pre><code>/* 제곱 차수 */\nfunc spaceQuadratic(n int) {\n // 행렬은 O(n^2) 공간을 차지한다\n numMatrix := make([][]int, n)\n for i := 0; i &lt; n; i++ {\n numMatrix[i] = make([]int, n)\n }\n}\n</code></pre> space_complexity.swift<pre><code>/* 제곱 차수 */\nfunc quadratic(n: Int) {\n // 이차원 리스트는 O(n^2) 공간을 차지한다\n let numList = Array(repeating: Array(repeating: 0, count: n), count: n)\n}\n</code></pre> space_complexity.js<pre><code>/* 제곱 차수 */\nfunction quadratic(n) {\n // 행렬은 O(n^2) 공간을 차지한다\n const numMatrix = Array(n)\n .fill(null)\n .map(() =&gt; Array(n).fill(null));\n // 이차원 리스트는 O(n^2) 공간을 차지한다\n const numList = [];\n for (let i = 0; i &lt; n; i++) {\n const tmp = [];\n for (let j = 0; j &lt; n; j++) {\n tmp.push(0);\n }\n numList.push(tmp);\n }\n}\n</code></pre> space_complexity.ts<pre><code>/* 제곱 차수 */\nfunction quadratic(n: number): void {\n // 행렬은 O(n^2) 공간을 차지한다\n const numMatrix = Array(n)\n .fill(null)\n .map(() =&gt; Array(n).fill(null));\n // 이차원 리스트는 O(n^2) 공간을 차지한다\n const numList = [];\n for (let i = 0; i &lt; n; i++) {\n const tmp = [];\n for (let j = 0; j &lt; n; j++) {\n tmp.push(0);\n }\n numList.push(tmp);\n }\n}\n</code></pre> space_complexity.dart<pre><code>/* 제곱 차수 */\nvoid quadratic(int n) {\n // 행렬은 O(n^2) 공간을 차지한다\n List&lt;List&lt;int&gt;&gt; numMatrix = List.generate(n, (_) =&gt; List.filled(n, 0));\n // 이차원 리스트는 O(n^2) 공간을 차지한다\n List&lt;List&lt;int&gt;&gt; numList = [];\n for (var i = 0; i &lt; n; i++) {\n List&lt;int&gt; tmp = [];\n for (int j = 0; j &lt; n; j++) {\n tmp.add(0);\n }\n numList.add(tmp);\n }\n}\n</code></pre> space_complexity.rs<pre><code>/* 제곱 차수 */\n#[allow(unused)]\nfn quadratic(n: i32) {\n // 행렬은 O(n^2) 공간을 차지한다\n let num_matrix = vec![vec![0; n as usize]; n as usize];\n // 이차원 리스트는 O(n^2) 공간을 차지한다\n let mut num_list = Vec::new();\n for i in 0..n {\n let mut tmp = Vec::new();\n for j in 0..n {\n tmp.push(0);\n }\n num_list.push(tmp);\n }\n}\n</code></pre> space_complexity.c<pre><code>/* 제곱 차수 */\nvoid quadratic(int n) {\n // 이차원 리스트는 O(n^2) 공간을 차지한다\n int **numMatrix = malloc(sizeof(int *) * n);\n for (int i = 0; i &lt; n; i++) {\n int *tmp = malloc(sizeof(int) * n);\n for (int j = 0; j &lt; n; j++) {\n tmp[j] = 0;\n }\n numMatrix[i] = tmp;\n }\n\n // 메모리 해제\n for (int i = 0; i &lt; n; i++) {\n free(numMatrix[i]);\n }\n free(numMatrix);\n}\n</code></pre> space_complexity.kt<pre><code>/* 제곱 차수 */\nfun quadratic(n: Int) {\n // 행렬은 O(n^2) 공간을 차지한다\n val numMatrix = arrayOfNulls&lt;Array&lt;Int&gt;?&gt;(n)\n // 이차원 리스트는 O(n^2) 공간을 차지한다\n val numList = mutableListOf&lt;MutableList&lt;Int&gt;&gt;()\n for (i in 0..&lt;n) {\n val tmp = mutableListOf&lt;Int&gt;()\n for (j in 0..&lt;n) {\n tmp.add(0)\n }\n numList.add(tmp)\n }\n}\n</code></pre> space_complexity.rb<pre><code>### 제곱 차수 ###\ndef quadratic(n)\n # 이차원 리스트는 O(n^2) 공간을 차지한다\n Array.new(n) { Array.new(n, 0) }\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>아래 그림(2-18)과 같이 이 함수의 재귀 깊이는 \\(n\\)이고, 각 재귀 함수에서 길이가 각각 \\(n\\), \\(n-1\\), \\(\\dots\\), \\(2\\), \\(1\\)인 배열을 초기화한다. 평균 길이는 \\(n / 2\\)이므로 전체적으로 \\(O(n^2)\\) 공간을 차지한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py<pre><code>def quadratic_recur(n: int) -&gt; int:\n \"\"\"제곱 차수(재귀 구현)\"\"\"\n if n &lt;= 0:\n return 0\n # 배열 nums의 길이는 n, n-1, ..., 2, 1이다\n nums = [0] * n\n return quadratic_recur(n - 1)\n</code></pre> space_complexity.cpp<pre><code>/* 제곱 차수(재귀 구현) */\nint quadraticRecur(int n) {\n if (n &lt;= 0)\n return 0;\n vector&lt;int&gt; nums(n);\n cout &lt;&lt; \"재귀 n = \" &lt;&lt; n &lt;&lt; \" 일 때 nums의 길이 = \" &lt;&lt; nums.size() &lt;&lt; endl;\n return quadraticRecur(n - 1);\n}\n</code></pre> space_complexity.java<pre><code>/* 제곱 차수(재귀 구현) */\nint quadraticRecur(int n) {\n if (n &lt;= 0)\n return 0;\n // 배열 nums의 길이는 n, n-1, ..., 2, 1이다\n int[] nums = new int[n];\n System.out.println(\"재귀 n = \" + n + \" 일 때 nums의 길이 = \" + nums.length);\n return quadraticRecur(n - 1);\n}\n</code></pre> space_complexity.cs<pre><code>/* 제곱 차수(재귀 구현) */\nint QuadraticRecur(int n) {\n if (n &lt;= 0) return 0;\n int[] nums = new int[n];\n Console.WriteLine(\"재귀 n = \" + n + \" 일 때 nums의 길이 = \" + nums.Length);\n return QuadraticRecur(n - 1);\n}\n</code></pre> space_complexity.go<pre><code>/* 제곱 차수(재귀 구현) */\nfunc spaceQuadraticRecur(n int) int {\n if n &lt;= 0 {\n return 0\n }\n nums := make([]int, n)\n fmt.Printf(\"재귀 n = %d에서 nums의 길이 = %d \\n\", n, len(nums))\n return spaceQuadraticRecur(n - 1)\n}\n</code></pre> space_complexity.swift<pre><code>/* 제곱 차수(재귀 구현) */\n@discardableResult\nfunc quadraticRecur(n: Int) -&gt; Int {\n if n &lt;= 0 {\n return 0\n }\n // 배열 nums의 길이는 n, n-1, ..., 2, 1이다\n let nums = Array(repeating: 0, count: n)\n print(\"재귀(n = \\(n))의 nums 길이 = \\(nums.count)\")\n return quadraticRecur(n: n - 1)\n}\n</code></pre> space_complexity.js<pre><code>/* 제곱 차수(재귀 구현) */\nfunction quadraticRecur(n) {\n if (n &lt;= 0) return 0;\n const nums = new Array(n);\n console.log(`재귀 n = ${n}일 때 nums의 길이 = ${nums.length}`);\n return quadraticRecur(n - 1);\n}\n</code></pre> space_complexity.ts<pre><code>/* 제곱 차수(재귀 구현) */\nfunction quadraticRecur(n: number): number {\n if (n &lt;= 0) return 0;\n const nums = new Array(n);\n console.log(`재귀 n = ${n}일 때 nums의 길이 = ${nums.length}`);\n return quadraticRecur(n - 1);\n}\n</code></pre> space_complexity.dart<pre><code>/* 제곱 차수(재귀 구현) */\nint quadraticRecur(int n) {\n if (n &lt;= 0) return 0;\n List&lt;int&gt; nums = List.filled(n, 0);\n print('재귀(n = $n)의 nums 길이 = ${nums.length}');\n return quadraticRecur(n - 1);\n}\n</code></pre> space_complexity.rs<pre><code>/* 제곱 차수(재귀 구현) */\nfn quadratic_recur(n: i32) -&gt; i32 {\n if n &lt;= 0 {\n return 0;\n };\n // 배열 nums의 길이는 n, n-1, ..., 2, 1이다\n let nums = vec![0; n as usize];\n println!(\"재귀 n = {}에서 nums의 길이 = {}\", n, nums.len());\n return quadratic_recur(n - 1);\n}\n</code></pre> space_complexity.c<pre><code>/* 제곱 차수(재귀 구현) */\nint quadraticRecur(int n) {\n if (n &lt;= 0)\n return 0;\n int *nums = malloc(sizeof(int) * n);\n printf(\"재귀 n = %d에서 nums의 길이 = %d\\r\\n\", n, n);\n int res = quadraticRecur(n - 1);\n free(nums);\n return res;\n}\n</code></pre> space_complexity.kt<pre><code>/* 제곱 차수(재귀 구현) */\ntailrec fun quadraticRecur(n: Int): Int {\n if (n &lt;= 0)\n return 0\n // 배열 nums의 길이는 n, n-1, ..., 2, 1이다\n val nums = Array(n) { 0 }\n println(\"재귀(n = $n)의 nums 길이 = ${nums.size}\")\n return quadraticRecur(n - 1)\n}\n</code></pre> space_complexity.rb<pre><code>### 제곱 차수(재귀 구현)###\ndef quadratic_recur(n)\n return 0 unless n &gt; 0\n\n # 배열 nums의 길이는 n, n-1, ..., 2, 1이다\n nums = Array.new(n, 0)\n quadratic_recur(n - 1)\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p></p> <p> 그림 2-18   재귀 함수에서 발생하는 제곱 공간 복잡도 </p>","path":["제 2 장   복잡도 분석","2.4   공간 복잡도"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#4-o2n","level":3,"title":"4.   지수 공간 \\(O(2^n)\\)","text":"<p>지수 공간은 이진 트리에 흔히 나타난다. 아래 그림(2-19)에서 층수가 \\(n\\)인 ‘포화 이진 트리’의 노드 수는 \\(2^n - 1\\)이므로 \\(O(2^n)\\) 공간을 차지한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py<pre><code>def build_tree(n: int) -&gt; TreeNode | None:\n \"\"\"지수 차수(포화 이진 트리 구성)\"\"\"\n if n == 0:\n return None\n root = TreeNode(0)\n root.left = build_tree(n - 1)\n root.right = build_tree(n - 1)\n return root\n</code></pre> space_complexity.cpp<pre><code>/* 지수 차수(포화 이진 트리 구성) */\nTreeNode *buildTree(int n) {\n if (n == 0)\n return nullptr;\n TreeNode *root = new TreeNode(0);\n root-&gt;left = buildTree(n - 1);\n root-&gt;right = buildTree(n - 1);\n return root;\n}\n</code></pre> space_complexity.java<pre><code>/* 지수 차수(포화 이진 트리 구성) */\nTreeNode buildTree(int n) {\n if (n == 0)\n return null;\n TreeNode root = new TreeNode(0);\n root.left = buildTree(n - 1);\n root.right = buildTree(n - 1);\n return root;\n}\n</code></pre> space_complexity.cs<pre><code>/* 지수 차수(포화 이진 트리 구성) */\nTreeNode? BuildTree(int n) {\n if (n == 0) return null;\n TreeNode root = new(0) {\n left = BuildTree(n - 1),\n right = BuildTree(n - 1)\n };\n return root;\n}\n</code></pre> space_complexity.go<pre><code>/* 지수 차수(포화 이진 트리 구성) */\nfunc buildTree(n int) *TreeNode {\n if n == 0 {\n return nil\n }\n root := NewTreeNode(0)\n root.Left = buildTree(n - 1)\n root.Right = buildTree(n - 1)\n return root\n}\n</code></pre> space_complexity.swift<pre><code>/* 지수 차수(포화 이진 트리 구성) */\nfunc buildTree(n: Int) -&gt; TreeNode? {\n if n == 0 {\n return nil\n }\n let root = TreeNode(x: 0)\n root.left = buildTree(n: n - 1)\n root.right = buildTree(n: n - 1)\n return root\n}\n</code></pre> space_complexity.js<pre><code>/* 지수 차수(포화 이진 트리 구성) */\nfunction buildTree(n) {\n if (n === 0) return null;\n const root = new TreeNode(0);\n root.left = buildTree(n - 1);\n root.right = buildTree(n - 1);\n return root;\n}\n</code></pre> space_complexity.ts<pre><code>/* 지수 차수(포화 이진 트리 구성) */\nfunction buildTree(n: number): TreeNode | null {\n if (n === 0) return null;\n const root = new TreeNode(0);\n root.left = buildTree(n - 1);\n root.right = buildTree(n - 1);\n return root;\n}\n</code></pre> space_complexity.dart<pre><code>/* 지수 차수(포화 이진 트리 구성) */\nTreeNode? buildTree(int n) {\n if (n == 0) return null;\n TreeNode root = TreeNode(0);\n root.left = buildTree(n - 1);\n root.right = buildTree(n - 1);\n return root;\n}\n</code></pre> space_complexity.rs<pre><code>/* 지수 차수(포화 이진 트리 구성) */\nfn build_tree(n: i32) -&gt; Option&lt;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt; {\n if n == 0 {\n return None;\n };\n let root = TreeNode::new(0);\n root.borrow_mut().left = build_tree(n - 1);\n root.borrow_mut().right = build_tree(n - 1);\n return Some(root);\n}\n</code></pre> space_complexity.c<pre><code>/* 지수 차수(포화 이진 트리 구성) */\nTreeNode *buildTree(int n) {\n if (n == 0)\n return NULL;\n TreeNode *root = newTreeNode(0);\n root-&gt;left = buildTree(n - 1);\n root-&gt;right = buildTree(n - 1);\n return root;\n}\n</code></pre> space_complexity.kt<pre><code>/* 지수 차수(포화 이진 트리 구성) */\nfun buildTree(n: Int): TreeNode? {\n if (n == 0)\n return null\n val root = TreeNode(0)\n root.left = buildTree(n - 1)\n root.right = buildTree(n - 1)\n return root\n}\n</code></pre> space_complexity.rb<pre><code>### 지수 차수(포화 이진 트리 구성)###\ndef build_tree(n)\n return if n == 0\n\n TreeNode.new.tap do |root|\n root.left = build_tree(n - 1)\n root.right = build_tree(n - 1)\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p></p> <p> 그림 2-19   포화 이진 트리에서 발생하는 지수 공간 복잡도 </p>","path":["제 2 장   복잡도 분석","2.4   공간 복잡도"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#5-olog-n","level":3,"title":"5.   로그 공간 \\(O(\\log n)\\)","text":"<p>로그 공간은 분할 정복 알고리즘에 흔히 나타난다. 예를 들어 병합 정렬은 길이가 \\(n\\)인 배열을 입력받아 재귀 호출마다 중간 지점에서 반으로 나눈다. 높이가 \\(\\log n\\)인 재귀 트리가 만들어지므로 \\(O(\\log n)\\)의 스택 프레임 공간을 사용한다.</p> <p>또 다른 예로 수를 문자열로 변환하는 경우를 보자. 양의 정수 \\(n\\)을 입력받으면 자릿수는 \\(\\lfloor \\log_{10} n \\rfloor + 1\\)이며, 대응하는 문자열 길이도 \\(\\lfloor \\log_{10} n \\rfloor + 1\\)이다. 따라서 공간 복잡도는 \\(O(\\log_{10} n + 1) = O(\\log n)\\)이다.</p>","path":["제 2 장   복잡도 분석","2.4   공간 복잡도"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#244","level":2,"title":"2.4.4   시간과 공간의 절충","text":"<p>이상적으로는 알고리즘의 시간 복잡도와 공간 복잡도가 모두 최적이기를 바란다. 그러나 실제로 두 복잡도를 동시에 최적화하기는 대개 매우 어렵다.</p> <p>시간 복잡도를 낮추려면 보통 공간 복잡도가 높아지는 대가를 치러야 하며, 그 반대도 마찬가지다. 메모리 공간을 더 사용하여 실행 속도를 높이는 방식을 ‘공간으로 시간을 절약하기’, 반대의 방식을 ‘시간으로 공간을 절약하기’라고 한다.</p> <p>어떤 방식을 선택할지는 어느 쪽을 더 중시하는지에 달려 있다. 대부분의 경우 시간이 공간보다 귀하므로 ‘공간으로 시간을 절약하기’가 더 널리 쓰인다. 물론 데이터 양이 매우 많다면 공간 복잡도를 관리하는 것 역시 중요하다.</p>","path":["제 2 장   복잡도 분석","2.4   공간 복잡도"],"tags":[]},{"location":"chapter_computational_complexity/summary/","level":1,"title":"2.5   요약","text":"","path":["제 2 장   복잡도 분석","2.5   요약"],"tags":[]},{"location":"chapter_computational_complexity/summary/#1","level":3,"title":"1.   핵심 정리","text":"<p>알고리즘 효율 평가</p> <ul> <li>시간 효율과 공간 효율은 알고리즘의 우수성을 평가하는 두 가지 주요 지표다.</li> <li>실제 측정으로 알고리즘 효율을 평가할 수 있지만, 측정 환경의 영향을 없애기 어렵고 많은 컴퓨팅 자원이 든다.</li> <li>복잡도 분석은 실제 측정의 단점을 극복한다. 분석 결과는 모든 실행 플랫폼에 적용할 수 있으며, 데이터 크기에 따른 알고리즘 효율을 보여 준다.</li> </ul> <p>시간 복잡도</p> <ul> <li>시간 복잡도는 데이터 양이 증가할 때 알고리즘 실행 시간이 증가하는 추세를 측정하여 효율을 효과적으로 평가한다. 다만 입력 데이터가 적거나 시간 복잡도가 같은 경우처럼, 알고리즘 효율의 우열을 정확히 비교할 수 없는 상황도 있다.</li> <li>최악 시간 복잡도는 빅 \\(O\\) 표기법으로 나타내며 함수의 점근적 상한에 해당한다. \\(n\\)이 양의 무한대로 향할 때 연산 횟수 \\(T(n)\\)의 증가 차수를 반영한다.</li> <li>시간 복잡도를 구하는 과정은 두 단계다. 먼저 연산 횟수를 세고, 그다음 점근적 상한을 판단한다.</li> <li>대표적인 시간 복잡도를 낮은 순서대로 나열하면 \\(O(1)\\), \\(O(\\log n)\\), \\(O(n)\\), \\(O(n \\log n)\\), \\(O(n^2)\\), \\(O(2^n)\\), \\(O(n!)\\) 등이 있다.</li> <li>일부 알고리즘의 시간 복잡도는 고정되어 있지 않고 입력 데이터의 분포에 따라 달라진다. 시간 복잡도는 최악, 최선, 평균 시간 복잡도로 나뉜다. 최선의 경우에 도달하려면 대개 입력 데이터가 엄격한 조건을 만족해야 하므로 최선 시간 복잡도는 거의 쓰지 않는다.</li> <li>평균 시간 복잡도는 무작위 입력 데이터에서의 실행 효율을 반영하므로 실제 응용에서의 알고리즘 성능에 가장 가깝다. 이를 계산하려면 입력 데이터의 분포와 이를 종합한 기댓값을 구해야 한다.</li> </ul> <p>공간 복잡도</p> <ul> <li>공간 복잡도는 시간 복잡도와 비슷하게, 데이터 양이 증가할 때 알고리즘이 차지하는 메모리 공간이 증가하는 추세를 측정한다.</li> <li>알고리즘 실행에 쓰이는 메모리 공간은 입력 공간, 임시 공간, 출력 공간으로 나뉜다. 보통 입력 공간은 공간 복잡도 계산에 포함하지 않는다. 임시 공간은 임시 데이터, 스택 프레임 공간, 명령어 공간으로 나뉘며, 스택 프레임 공간은 보통 재귀 함수에서만 공간 복잡도에 영향을 준다.</li> <li>보통 최악 공간 복잡도에만 관심을 둔다. 즉, 가장 불리한 입력 데이터와 실행 시점을 기준으로 공간 복잡도를 계산한다.</li> <li>대표적인 공간 복잡도를 낮은 순서대로 나열하면 \\(O(1)\\), \\(O(\\log n)\\), \\(O(n)\\), \\(O(n^2)\\), \\(O(2^n)\\) 등이 있다.</li> </ul>","path":["제 2 장   복잡도 분석","2.5   요약"],"tags":[]},{"location":"chapter_computational_complexity/summary/#2-q-a","level":3,"title":"2.   Q &amp; A","text":"<p>Q: 꼬리 재귀의 공간 복잡도는 \\(O(1)\\)인가?</p> <p>이론적으로 꼬리 재귀 함수의 공간 복잡도는 \\(O(1)\\)로 최적화할 수 있다. 그러나 Java, Python, C++, Go, C# 등 대다수 프로그래밍 언어는 자동 꼬리 재귀 최적화를 지원하지 않으므로, 보통 공간 복잡도를 \\(O(n)\\)으로 본다.</p> <p>Q: 함수와 메서드라는 두 용어는 어떻게 다른가?</p> <p>함수(function)는 독립적으로 실행할 수 있으며, 모든 인수가 명시적으로 전달된다. 메서드(method)는 객체와 연결되어 있고, 자신을 호출하는 객체에 암묵적으로 전달되며, 클래스 인스턴스에 포함된 데이터를 조작할 수 있다.</p> <p>몇 가지 흔히 쓰는 프로그래밍 언어를 예로 살펴보자.</p> <ul> <li>C는 절차적 프로그래밍 언어로 객체 지향 개념이 없으므로 함수만 있다. 다만 구조체(struct)를 만들어 객체 지향 프로그래밍을 흉내 낼 수 있으며, 구조체에 연결된 함수는 다른 언어의 메서드에 해당한다.</li> <li>Java와 C#은 객체 지향 프로그래밍 언어로, 코드 블록(메서드)은 보통 클래스의 일부다. 정적 메서드는 클래스에 묶여 있고 특정 인스턴스 변수에 접근할 수 없으므로 함수와 비슷하게 동작한다.</li> <li>C++와 Python은 절차적 프로그래밍(함수)과 객체 지향 프로그래밍(메서드)을 모두 지원한다.</li> </ul> <p>Q: ‘대표적인 공간 복잡도 유형’ 그림은 사용 공간의 절대적인 크기를 나타내는가?</p> <p>아니다. 이 그림은 공간 복잡도를 보여 주므로, 사용 공간의 절대적인 크기가 아니라 증가 추세를 나타낸다.</p> <p>\\(n = 8\\)이라고 놓으면 각 곡선의 값이 해당 함수와 일치하지 않을 수 있다. 이는 각 곡선에 상수항을 넣어 값의 범위를 보기 편한 크기로 줄였기 때문이다.</p> <p>실제로는 각 방법의 ‘상수항’에 해당하는 비용을 보통 알 수 없으므로, \\(n = 8\\)일 때는 복잡도만으로 최적의 해법을 고르기 어렵다. 반면 \\(n = 8^5\\)일 때는 증가 추세가 지배적인 영향을 주므로 훨씬 쉽게 선택할 수 있다.</p> <p>Q 실제 사용 상황에 따라 시간이나 공간을 희생하도록 알고리즘을 설계하기도 하는가?</p> <p>실제 응용에서는 대부분 공간을 더 사용하여 시간을 줄이는 쪽을 선택한다. 예를 들어 데이터베이스 인덱스에서는 보통 B+ 트리나 해시 인덱스를 구축한다. 메모리 공간을 많이 사용하는 대신 \\(O(\\log n)\\), 나아가 \\(O(1)\\)의 효율적인 조회가 가능하다.</p> <p>공간 자원이 귀한 상황에서는 시간을 더 들여 공간을 절약하기도 한다. 예를 들어 임베디드 개발에서는 장치 메모리가 귀하므로, 해시 테이블 대신 배열에서 순차 탐색을 수행할 수 있다. 메모리 사용량을 줄이는 대가로 탐색 속도가 느려진다.</p>","path":["제 2 장   복잡도 분석","2.5   요약"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/","level":1,"title":"2.3   시간 복잡도","text":"<p>실행 시간은 알고리즘 효율을 직관적이고 정확하게 보여 준다. 코드의 실행 시간을 정확히 추정하려면 어떻게 해야 할까?</p> <ol> <li>실행 플랫폼을 정한다. 하드웨어 구성, 프로그래밍 언어, 시스템 환경 등이 포함되며, 모두 코드의 실행 효율에 영향을 준다.</li> <li>각 연산에 필요한 실행 시간을 평가한다. 예를 들어 덧셈 연산 <code>+</code>는 1 ns, 곱셈 연산 <code>*</code>는 10 ns, 출력 연산 <code>print()</code>는 5 ns가 걸린다고 하자.</li> <li>코드에 있는 모든 연산을 센다. 그런 다음 각 연산의 실행 시간을 모두 더해 전체 실행 시간을 구한다.</li> </ol> <p>예를 들어 다음 코드에서 입력 데이터 크기는 \\(n\\)이다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby <pre><code># 어떤 실행 플랫폼에서\ndef algorithm(n: int):\n a = 2 # 1 ns\n a = a + 1 # 1 ns\n a = a * 2 # 10 ns\n # n번 반복\n for _ in range(n): # 1 ns\n print(0) # 5 ns\n</code></pre> <pre><code>// 어떤 실행 플랫폼에서\nvoid algorithm(int n) {\n int a = 2; // 1 ns\n a = a + 1; // 1 ns\n a = a * 2; // 10 ns\n // n번 반복\n for (int i = 0; i &lt; n; i++) { // 1 ns\n cout &lt;&lt; 0 &lt;&lt; endl; // 5 ns\n }\n}\n</code></pre> <pre><code>// 어떤 실행 플랫폼에서\nvoid algorithm(int n) {\n int a = 2; // 1 ns\n a = a + 1; // 1 ns\n a = a * 2; // 10 ns\n // n번 반복\n for (int i = 0; i &lt; n; i++) { // 1 ns\n System.out.println(0); // 5 ns\n }\n}\n</code></pre> <pre><code>// 어떤 실행 플랫폼에서\nvoid Algorithm(int n) {\n int a = 2; // 1 ns\n a = a + 1; // 1 ns\n a = a * 2; // 10 ns\n // n번 반복\n for (int i = 0; i &lt; n; i++) { // 1 ns\n Console.WriteLine(0); // 5 ns\n }\n}\n</code></pre> <pre><code>// 어떤 실행 플랫폼에서\nfunc algorithm(n int) {\n a := 2 // 1 ns\n a = a + 1 // 1 ns\n a = a * 2 // 10 ns\n // n번 반복\n for i := 0; i &lt; n; i++ { // 1 ns\n fmt.Println(a) // 5 ns\n }\n}\n</code></pre> <pre><code>// 어떤 실행 플랫폼에서\nfunc algorithm(n: Int) {\n var a = 2 // 1 ns\n a = a + 1 // 1 ns\n a = a * 2 // 10 ns\n // n번 반복\n for _ in 0 ..&lt; n { // 1 ns\n print(0) // 5 ns\n }\n}\n</code></pre> <pre><code>// 어떤 실행 플랫폼에서\nfunction algorithm(n) {\n var a = 2; // 1 ns\n a = a + 1; // 1 ns\n a = a * 2; // 10 ns\n // n번 반복\n for(let i = 0; i &lt; n; i++) { // 1 ns\n console.log(0); // 5 ns\n }\n}\n</code></pre> <pre><code>// 어떤 실행 플랫폼에서\nfunction algorithm(n: number): void {\n var a: number = 2; // 1 ns\n a = a + 1; // 1 ns\n a = a * 2; // 10 ns\n // n번 반복\n for(let i = 0; i &lt; n; i++) { // 1 ns\n console.log(0); // 5 ns\n }\n}\n</code></pre> <pre><code>// 어떤 실행 플랫폼에서\nvoid algorithm(int n) {\n int a = 2; // 1 ns\n a = a + 1; // 1 ns\n a = a * 2; // 10 ns\n // n번 반복\n for (int i = 0; i &lt; n; i++) { // 1 ns\n print(0); // 5 ns\n }\n}\n</code></pre> <pre><code>// 어떤 실행 플랫폼에서\nfn algorithm(n: i32) {\n let mut a = 2; // 1 ns\n a = a + 1; // 1 ns\n a = a * 2; // 10 ns\n // n번 반복\n for _ in 0..n { // 1 ns\n println!(\"{}\", 0); // 5 ns\n }\n}\n</code></pre> <pre><code>// 어떤 실행 플랫폼에서\nvoid algorithm(int n) {\n int a = 2; // 1 ns\n a = a + 1; // 1 ns\n a = a * 2; // 10 ns\n // n번 반복\n for (int i = 0; i &lt; n; i++) { // 1 ns\n printf(\"%d\", 0); // 5 ns\n }\n}\n</code></pre> <pre><code>// 어떤 실행 플랫폼에서\nfun algorithm(n: Int) {\n var a = 2 // 1 ns\n a = a + 1 // 1 ns\n a = a * 2 // 10 ns\n // n번 반복\n for (i in 0..&lt;n) { // 1 ns\n println(0) // 5 ns\n }\n}\n</code></pre> <pre><code># 어떤 실행 플랫폼에서\ndef algorithm(n)\n a = 2 # 1 ns\n a = a + 1 # 1 ns\n a = a * 2 # 10 ns\n # n번 반복\n (0...n).each do # 1 ns\n puts 0 # 5 ns\n end\nend\n</code></pre> <p>위 방법으로 구한 알고리즘의 실행 시간은 \\((6n + 12)\\) ns다.</p> \\[ 1 + 1 + 10 + (1 + 5) \\times n = 6n + 12 \\] <p>그러나 실제로 알고리즘의 실행 시간을 이렇게 계산하는 것은 합리적이지도 현실적이지도 않다. 우선 알고리즘은 여러 플랫폼에서 실행되어야 하므로 추정 시간을 특정 실행 플랫폼에 묶고 싶지 않다. 또한 각 연산의 실행 시간을 알아내기 어려워 추정 과정이 매우 복잡해진다.</p>","path":["제 2 장   복잡도 분석","2.3   시간 복잡도"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#231","level":2,"title":"2.3.1   시간의 증가 추세 계산","text":"<p>시간 복잡도 분석이 계산하는 것은 알고리즘 실행 시간 자체가 아니라, 데이터 양이 많아질 때 알고리즘 실행 시간이 증가하는 추세다.</p> <p>‘시간의 증가 추세’는 추상적인 개념이므로 예를 통해 이해해 보자. 입력 데이터 크기가 \\(n\\)이고, 세 알고리즘 <code>A</code>, <code>B</code>, <code>C</code>가 주어졌다고 하자.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby <pre><code># 알고리즘 A의 시간 복잡도: 상수 시간\ndef algorithm_A(n: int):\n print(0)\n# 알고리즘 B의 시간 복잡도: 선형 시간\ndef algorithm_B(n: int):\n for _ in range(n):\n print(0)\n# 알고리즘 C의 시간 복잡도: 상수 시간\ndef algorithm_C(n: int):\n for _ in range(1000000):\n print(0)\n</code></pre> <pre><code>// 알고리즘 A의 시간 복잡도: 상수 시간\nvoid algorithm_A(int n) {\n cout &lt;&lt; 0 &lt;&lt; endl;\n}\n// 알고리즘 B의 시간 복잡도: 선형 시간\nvoid algorithm_B(int n) {\n for (int i = 0; i &lt; n; i++) {\n cout &lt;&lt; 0 &lt;&lt; endl;\n }\n}\n// 알고리즘 C의 시간 복잡도: 상수 시간\nvoid algorithm_C(int n) {\n for (int i = 0; i &lt; 1000000; i++) {\n cout &lt;&lt; 0 &lt;&lt; endl;\n }\n}\n</code></pre> <pre><code>// 알고리즘 A의 시간 복잡도: 상수 시간\nvoid algorithm_A(int n) {\n System.out.println(0);\n}\n// 알고리즘 B의 시간 복잡도: 선형 시간\nvoid algorithm_B(int n) {\n for (int i = 0; i &lt; n; i++) {\n System.out.println(0);\n }\n}\n// 알고리즘 C의 시간 복잡도: 상수 시간\nvoid algorithm_C(int n) {\n for (int i = 0; i &lt; 1000000; i++) {\n System.out.println(0);\n }\n}\n</code></pre> <pre><code>// 알고리즘 A의 시간 복잡도: 상수 시간\nvoid AlgorithmA(int n) {\n Console.WriteLine(0);\n}\n// 알고리즘 B의 시간 복잡도: 선형 시간\nvoid AlgorithmB(int n) {\n for (int i = 0; i &lt; n; i++) {\n Console.WriteLine(0);\n }\n}\n// 알고리즘 C의 시간 복잡도: 상수 시간\nvoid AlgorithmC(int n) {\n for (int i = 0; i &lt; 1000000; i++) {\n Console.WriteLine(0);\n }\n}\n</code></pre> <pre><code>// 알고리즘 A의 시간 복잡도: 상수 시간\nfunc algorithm_A(n int) {\n fmt.Println(0)\n}\n// 알고리즘 B의 시간 복잡도: 선형 시간\nfunc algorithm_B(n int) {\n for i := 0; i &lt; n; i++ {\n fmt.Println(0)\n }\n}\n// 알고리즘 C의 시간 복잡도: 상수 시간\nfunc algorithm_C(n int) {\n for i := 0; i &lt; 1000000; i++ {\n fmt.Println(0)\n }\n}\n</code></pre> <pre><code>// 알고리즘 A의 시간 복잡도: 상수 시간\nfunc algorithmA(n: Int) {\n print(0)\n}\n\n// 알고리즘 B의 시간 복잡도: 선형 시간\nfunc algorithmB(n: Int) {\n for _ in 0 ..&lt; n {\n print(0)\n }\n}\n\n// 알고리즘 C의 시간 복잡도: 상수 시간\nfunc algorithmC(n: Int) {\n for _ in 0 ..&lt; 1_000_000 {\n print(0)\n }\n}\n</code></pre> <pre><code>// 알고리즘 A의 시간 복잡도: 상수 시간\nfunction algorithm_A(n) {\n console.log(0);\n}\n// 알고리즘 B의 시간 복잡도: 선형 시간\nfunction algorithm_B(n) {\n for (let i = 0; i &lt; n; i++) {\n console.log(0);\n }\n}\n// 알고리즘 C의 시간 복잡도: 상수 시간\nfunction algorithm_C(n) {\n for (let i = 0; i &lt; 1000000; i++) {\n console.log(0);\n }\n}\n</code></pre> <pre><code>// 알고리즘 A의 시간 복잡도: 상수 시간\nfunction algorithm_A(n: number): void {\n console.log(0);\n}\n// 알고리즘 B의 시간 복잡도: 선형 시간\nfunction algorithm_B(n: number): void {\n for (let i = 0; i &lt; n; i++) {\n console.log(0);\n }\n}\n// 알고리즘 C의 시간 복잡도: 상수 시간\nfunction algorithm_C(n: number): void {\n for (let i = 0; i &lt; 1000000; i++) {\n console.log(0);\n }\n}\n</code></pre> <pre><code>// 알고리즘 A의 시간 복잡도: 상수 시간\nvoid algorithmA(int n) {\n print(0);\n}\n// 알고리즘 B의 시간 복잡도: 선형 시간\nvoid algorithmB(int n) {\n for (int i = 0; i &lt; n; i++) {\n print(0);\n }\n}\n// 알고리즘 C의 시간 복잡도: 상수 시간\nvoid algorithmC(int n) {\n for (int i = 0; i &lt; 1000000; i++) {\n print(0);\n }\n}\n</code></pre> <pre><code>// 알고리즘 A의 시간 복잡도: 상수 시간\nfn algorithm_A(n: i32) {\n println!(\"{}\", 0);\n}\n// 알고리즘 B의 시간 복잡도: 선형 시간\nfn algorithm_B(n: i32) {\n for _ in 0..n {\n println!(\"{}\", 0);\n }\n}\n// 알고리즘 C의 시간 복잡도: 상수 시간\nfn algorithm_C(n: i32) {\n for _ in 0..1000000 {\n println!(\"{}\", 0);\n }\n}\n</code></pre> <pre><code>// 알고리즘 A의 시간 복잡도: 상수 시간\nvoid algorithm_A(int n) {\n printf(\"%d\", 0);\n}\n// 알고리즘 B의 시간 복잡도: 선형 시간\nvoid algorithm_B(int n) {\n for (int i = 0; i &lt; n; i++) {\n printf(\"%d\", 0);\n }\n}\n// 알고리즘 C의 시간 복잡도: 상수 시간\nvoid algorithm_C(int n) {\n for (int i = 0; i &lt; 1000000; i++) {\n printf(\"%d\", 0);\n }\n}\n</code></pre> <pre><code>// 알고리즘 A의 시간 복잡도: 상수 시간\nfun algoritm_A(n: Int) {\n println(0)\n}\n// 알고리즘 B의 시간 복잡도: 선형 시간\nfun algorithm_B(n: Int) {\n for (i in 0..&lt;n){\n println(0)\n }\n}\n// 알고리즘 C의 시간 복잡도: 상수 시간\nfun algorithm_C(n: Int) {\n for (i in 0..&lt;1000000) {\n println(0)\n }\n}\n</code></pre> <pre><code># 알고리즘 A의 시간 복잡도: 상수 시간\ndef algorithm_A(n)\n puts 0\nend\n\n# 알고리즘 B의 시간 복잡도: 선형 시간\ndef algorithm_B(n)\n (0...n).each { puts 0 }\nend\n\n# 알고리즘 C의 시간 복잡도: 상수 시간\ndef algorithm_C(n)\n (0...1_000_000).each { puts 0 }\nend\n</code></pre> <p>아래 그림(2-7)은 이 세 알고리즘 함수의 시간 복잡도를 보여 준다.</p> <ul> <li>알고리즘 <code>A</code>에는 출력 연산이 \\(1\\)번뿐이므로, \\(n\\)이 커져도 실행 시간이 늘어나지 않는다. 이런 알고리즘의 시간 복잡도를 ‘상수 시간’이라고 한다.</li> <li>알고리즘 <code>B</code>의 출력 연산은 \\(n\\)번 반복되므로, \\(n\\)이 커짐에 따라 실행 시간이 선형으로 증가한다. 이런 알고리즘의 시간 복잡도를 ‘선형 시간’이라고 한다.</li> <li>알고리즘 <code>C</code>의 출력 연산은 \\(1000000\\)번 반복된다. 실행 시간은 길지만 입력 데이터 크기 \\(n\\)과는 무관하다. 따라서 <code>C</code>의 시간 복잡도는 <code>A</code>와 같은 ‘상수 시간’이다.</li> </ul> <p></p> <p> 그림 2-7   알고리즘 A, B, C의 시간 증가 추세 </p> <p>알고리즘의 실행 시간을 직접 계산하는 방식과 비교하면 시간 복잡도 분석에는 어떤 특징이 있을까?</p> <ul> <li>시간 복잡도는 알고리즘 효율을 효과적으로 평가할 수 있다. 예를 들어 알고리즘 <code>B</code>의 실행 시간은 선형으로 증가하므로 \\(n &gt; 1\\)이면 알고리즘 <code>A</code>보다 느리고, \\(n &gt; 1000000\\)이면 알고리즘 <code>C</code>보다 느리다. 실제로 입력 데이터 크기 \\(n\\)이 충분히 크면 ‘상수 시간’ 알고리즘은 반드시 ‘선형 시간’ 알고리즘보다 유리하다. 이것이 바로 시간의 증가 추세가 뜻하는 바다.</li> <li>시간 복잡도를 구하는 방법은 더 간단하다. 실행 플랫폼과 연산 종류는 알고리즘 실행 시간의 증가 추세와 무관하다. 따라서 시간 복잡도 분석에서는 모든 연산의 실행 시간을 같은 ‘단위 시간’으로 볼 수 있다. 그러면 ‘연산 실행 시간 계산’을 ‘연산 횟수 계산’으로 단순화할 수 있어 추정이 훨씬 쉬워진다.</li> <li>시간 복잡도에도 한계는 있다. 예를 들어 알고리즘 <code>A</code>와 <code>C</code>의 시간 복잡도는 같지만 실제 실행 시간에는 큰 차이가 있다. 마찬가지로 알고리즘 <code>B</code>의 시간 복잡도는 <code>C</code>보다 높지만, 입력 데이터 크기 \\(n\\)이 작으면 <code>B</code>가 <code>C</code>보다 확실히 유리하다. 이런 경우에는 시간 복잡도만으로 효율의 우열을 판단하기 어렵다. 물론 이러한 문제에도 불구하고 복잡도 분석은 여전히 알고리즘 효율을 평가하는 가장 효과적이고 널리 쓰이는 방법이다.</li> </ul>","path":["제 2 장   복잡도 분석","2.3   시간 복잡도"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#232","level":2,"title":"2.3.2   함수의 점근적 상한","text":"<p>입력 크기가 \\(n\\)인 다음 함수를 살펴보자.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby <pre><code>def algorithm(n: int):\n a = 1 # +1\n a = a + 1 # +1\n a = a * 2 # +1\n # n번 반복\n for i in range(n): # +1\n print(0) # +1\n</code></pre> <pre><code>void algorithm(int n) {\n int a = 1; // +1\n a = a + 1; // +1\n a = a * 2; // +1\n // n번 반복\n for (int i = 0; i &lt; n; i++) { // +1(매번 i ++ 실행)\n cout &lt;&lt; 0 &lt;&lt; endl; // +1\n }\n}\n</code></pre> <pre><code>void algorithm(int n) {\n int a = 1; // +1\n a = a + 1; // +1\n a = a * 2; // +1\n // n번 반복\n for (int i = 0; i &lt; n; i++) { // +1(매번 i ++ 실행)\n System.out.println(0); // +1\n }\n}\n</code></pre> <pre><code>void Algorithm(int n) {\n int a = 1; // +1\n a = a + 1; // +1\n a = a * 2; // +1\n // n번 반복\n for (int i = 0; i &lt; n; i++) { // +1(매번 i ++ 실행)\n Console.WriteLine(0); // +1\n }\n}\n</code></pre> <pre><code>func algorithm(n int) {\n a := 1 // +1\n a = a + 1 // +1\n a = a * 2 // +1\n // n번 반복\n for i := 0; i &lt; n; i++ { // +1\n fmt.Println(a) // +1\n }\n}\n</code></pre> <pre><code>func algorithm(n: Int) {\n var a = 1 // +1\n a = a + 1 // +1\n a = a * 2 // +1\n // n번 반복\n for _ in 0 ..&lt; n { // +1\n print(0) // +1\n }\n}\n</code></pre> <pre><code>function algorithm(n) {\n var a = 1; // +1\n a += 1; // +1\n a *= 2; // +1\n // n번 반복\n for(let i = 0; i &lt; n; i++){ // +1(매번 i ++ 실행)\n console.log(0); // +1\n }\n}\n</code></pre> <pre><code>function algorithm(n: number): void{\n var a: number = 1; // +1\n a += 1; // +1\n a *= 2; // +1\n // n번 반복\n for(let i = 0; i &lt; n; i++){ // +1(매번 i ++ 실행)\n console.log(0); // +1\n }\n}\n</code></pre> <pre><code>void algorithm(int n) {\n int a = 1; // +1\n a = a + 1; // +1\n a = a * 2; // +1\n // n번 반복\n for (int i = 0; i &lt; n; i++) { // +1(매번 i ++ 실행)\n print(0); // +1\n }\n}\n</code></pre> <pre><code>fn algorithm(n: i32) {\n let mut a = 1; // +1\n a = a + 1; // +1\n a = a * 2; // +1\n\n // n번 반복\n for _ in 0..n { // +1(매번 i ++ 실행)\n println!(\"{}\", 0); // +1\n }\n}\n</code></pre> <pre><code>void algorithm(int n) {\n int a = 1; // +1\n a = a + 1; // +1\n a = a * 2; // +1\n // n번 반복\n for (int i = 0; i &lt; n; i++) { // +1(매번 i ++ 실행)\n printf(\"%d\", 0); // +1\n }\n}\n</code></pre> <pre><code>fun algorithm(n: Int) {\n var a = 1 // +1\n a = a + 1 // +1\n a = a * 2 // +1\n // n번 반복\n for (i in 0..&lt;n) { // +1(매번 i ++ 실행)\n println(0) // +1\n }\n}\n</code></pre> <pre><code>def algorithm(n)\n a = 1 # +1\n a = a + 1 # +1\n a = a * 2 # +1\n # n번 반복\n (0...n).each do # +1\n puts 0 # +1\n end\nend\n</code></pre> <p>알고리즘의 연산 횟수를 입력 데이터 크기 \\(n\\)에 대한 함수 \\(T(n)\\)으로 나타내면, 위 함수의 연산 횟수는 다음과 같다.</p> \\[ T(n) = 3 + 2n \\] <p>\\(T(n)\\)은 일차 함수이므로 실행 시간이 선형으로 증가한다. 따라서 시간 복잡도는 선형 시간이다.</p> <p>선형 시간 복잡도를 \\(O(n)\\)으로 표기한다. 이 수학 기호를 빅 \\(O\\) 표기법(big-\\(O\\) notation)이라고 하며, 함수 \\(T(n)\\)의 점근적 상한(asymptotic upper bound)을 나타낸다.</p> <p>시간 복잡도 분석은 본질적으로 ‘연산 횟수 \\(T(n)\\)’의 점근적 상한을 계산하는 것으로, 명확한 수학적 정의가 있다.</p> <p>함수의 점근적 상한</p> <p>양의 실수 \\(c\\)와 실수 \\(n_0\\)가 존재하여 모든 \\(n &gt; n_0\\)에 대해 \\(T(n) \\leq c \\cdot f(n)\\)이면, \\(f(n)\\)은 \\(T(n)\\)의 점근적 상한을 제공한다고 하며 \\(T(n) = O(f(n))\\)으로 표기한다.</p> <p>아래 그림(2-8)과 같이 점근적 상한을 계산한다는 것은 함수 \\(f(n)\\)을 찾는 것이다. 이 함수는 \\(n\\)이 무한대로 향할 때 \\(T(n)\\)과 \\(f(n)\\)의 증가 차수가 같고 상수 계수 \\(c\\)만큼만 차이가 나도록 해야 한다.</p> <p></p> <p> 그림 2-8   함수의 점근적 상한 </p>","path":["제 2 장   복잡도 분석","2.3   시간 복잡도"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#233","level":2,"title":"2.3.3   계산 방법","text":"<p>점근적 상한은 수학적인 느낌이 강하므로, 완전히 이해되지 않아도 걱정할 필요 없다. 우선 계산 방법을 익힌 뒤 반복해서 연습하면 그 수학적 의미도 점차 이해할 수 있다.</p> <p>정의에 따르면 \\(f(n)\\)을 정하면 시간 복잡도 \\(O(f(n))\\)을 얻는다. 그렇다면 점근적 상한 \\(f(n)\\)은 어떻게 정할까? 크게 두 단계로 나뉜다. 먼저 연산 횟수를 세고, 그다음 점근적 상한을 판단한다.</p>","path":["제 2 장   복잡도 분석","2.3   시간 복잡도"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#1-1","level":3,"title":"1.   1단계: 연산 횟수 세기","text":"<p>코드를 위에서 아래로 한 줄씩 살펴보며 계산하면 된다. 다만 앞서 본 \\(c \\cdot f(n)\\)의 상수 계수 \\(c\\)는 어떤 크기든 가능하므로, 연산 횟수 \\(T(n)\\)의 각종 계수와 상수항은 무시해도 된다. 이 원리를 바탕으로 계산을 단순화하는 다음 요령을 얻을 수 있다.</p> <ol> <li>\\(T(n)\\)의 상수를 무시한다. 상수는 \\(n\\)과 무관하므로 시간 복잡도에 영향을 주지 않는다.</li> <li>모든 계수를 생략한다. 예를 들어 \\(2n\\)번, \\(5n + 1\\)번 등의 반복은 모두 \\(n\\)번으로 단순화할 수 있다. \\(n\\) 앞의 계수는 시간 복잡도에 영향을 주지 않기 때문이다.</li> <li>중첩 반복문에는 곱셈을 사용한다. 전체 연산 횟수는 바깥쪽 반복문과 안쪽 반복문의 연산 횟수의 곱이며, 각 반복문에는 여전히 <code>1.</code>과 <code>2.</code>의 요령을 따로 적용할 수 있다.</li> </ol> <p>다음 함수에 위 요령을 적용하여 연산 횟수를 세어 보자.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby <pre><code>def algorithm(n: int):\n a = 1 # +0(요령 1)\n a = a + n # +0(요령 1)\n # +n(요령 2)\n for i in range(5 * n + 1):\n print(0)\n # +n*n(요령 3)\n for i in range(2 * n):\n for j in range(n + 1):\n print(0)\n</code></pre> <pre><code>void algorithm(int n) {\n int a = 1; // +0(요령 1)\n a = a + n; // +0(요령 1)\n // +n(요령 2)\n for (int i = 0; i &lt; 5 * n + 1; i++) {\n cout &lt;&lt; 0 &lt;&lt; endl;\n }\n // +n*n(요령 3)\n for (int i = 0; i &lt; 2 * n; i++) {\n for (int j = 0; j &lt; n + 1; j++) {\n cout &lt;&lt; 0 &lt;&lt; endl;\n }\n }\n}\n</code></pre> <pre><code>void algorithm(int n) {\n int a = 1; // +0(요령 1)\n a = a + n; // +0(요령 1)\n // +n(요령 2)\n for (int i = 0; i &lt; 5 * n + 1; i++) {\n System.out.println(0);\n }\n // +n*n(요령 3)\n for (int i = 0; i &lt; 2 * n; i++) {\n for (int j = 0; j &lt; n + 1; j++) {\n System.out.println(0);\n }\n }\n}\n</code></pre> <pre><code>void Algorithm(int n) {\n int a = 1; // +0(요령 1)\n a = a + n; // +0(요령 1)\n // +n(요령 2)\n for (int i = 0; i &lt; 5 * n + 1; i++) {\n Console.WriteLine(0);\n }\n // +n*n(요령 3)\n for (int i = 0; i &lt; 2 * n; i++) {\n for (int j = 0; j &lt; n + 1; j++) {\n Console.WriteLine(0);\n }\n }\n}\n</code></pre> <pre><code>func algorithm(n int) {\n a := 1 // +0(요령 1)\n a = a + n // +0(요령 1)\n // +n(요령 2)\n for i := 0; i &lt; 5 * n + 1; i++ {\n fmt.Println(0)\n }\n // +n*n(요령 3)\n for i := 0; i &lt; 2 * n; i++ {\n for j := 0; j &lt; n + 1; j++ {\n fmt.Println(0)\n }\n }\n}\n</code></pre> <pre><code>func algorithm(n: Int) {\n var a = 1 // +0(요령 1)\n a = a + n // +0(요령 1)\n // +n(요령 2)\n for _ in 0 ..&lt; (5 * n + 1) {\n print(0)\n }\n // +n*n(요령 3)\n for _ in 0 ..&lt; (2 * n) {\n for _ in 0 ..&lt; (n + 1) {\n print(0)\n }\n }\n}\n</code></pre> <pre><code>function algorithm(n) {\n let a = 1; // +0(요령 1)\n a = a + n; // +0(요령 1)\n // +n(요령 2)\n for (let i = 0; i &lt; 5 * n + 1; i++) {\n console.log(0);\n }\n // +n*n(요령 3)\n for (let i = 0; i &lt; 2 * n; i++) {\n for (let j = 0; j &lt; n + 1; j++) {\n console.log(0);\n }\n }\n}\n</code></pre> <pre><code>function algorithm(n: number): void {\n let a = 1; // +0(요령 1)\n a = a + n; // +0(요령 1)\n // +n(요령 2)\n for (let i = 0; i &lt; 5 * n + 1; i++) {\n console.log(0);\n }\n // +n*n(요령 3)\n for (let i = 0; i &lt; 2 * n; i++) {\n for (let j = 0; j &lt; n + 1; j++) {\n console.log(0);\n }\n }\n}\n</code></pre> <pre><code>void algorithm(int n) {\n int a = 1; // +0(요령 1)\n a = a + n; // +0(요령 1)\n // +n(요령 2)\n for (int i = 0; i &lt; 5 * n + 1; i++) {\n print(0);\n }\n // +n*n(요령 3)\n for (int i = 0; i &lt; 2 * n; i++) {\n for (int j = 0; j &lt; n + 1; j++) {\n print(0);\n }\n }\n}\n</code></pre> <pre><code>fn algorithm(n: i32) {\n let mut a = 1; // +0(요령 1)\n a = a + n; // +0(요령 1)\n\n // +n(요령 2)\n for i in 0..(5 * n + 1) {\n println!(\"{}\", 0);\n }\n\n // +n*n(요령 3)\n for i in 0..(2 * n) {\n for j in 0..(n + 1) {\n println!(\"{}\", 0);\n }\n }\n}\n</code></pre> <pre><code>void algorithm(int n) {\n int a = 1; // +0(요령 1)\n a = a + n; // +0(요령 1)\n // +n(요령 2)\n for (int i = 0; i &lt; 5 * n + 1; i++) {\n printf(\"%d\", 0);\n }\n // +n*n(요령 3)\n for (int i = 0; i &lt; 2 * n; i++) {\n for (int j = 0; j &lt; n + 1; j++) {\n printf(\"%d\", 0);\n }\n }\n}\n</code></pre> <pre><code>fun algorithm(n: Int) {\n var a = 1 // +0(요령 1)\n a = a + n // +0(요령 1)\n // +n(요령 2)\n for (i in 0..&lt;5 * n + 1) {\n println(0)\n }\n // +n*n(요령 3)\n for (i in 0..&lt;2 * n) {\n for (j in 0..&lt;n + 1) {\n println(0)\n }\n }\n}\n</code></pre> <pre><code>def algorithm(n)\n a = 1 # +0(요령 1)\n a = a + n # +0(요령 1)\n # +n(요령 2)\n (0...(5 * n + 1)).each do { puts 0 }\n # +n*n(요령 3)\n (0...(2 * n)).each do\n (0...(n + 1)).each do { puts 0 }\n end\nend\n</code></pre> <p>아래 식은 위 요령을 적용하기 전과 후의 계산 결과를 보여 준다. 두 결과에서 구한 시간 복잡도는 모두 \\(O(n^2)\\)이다.</p> \\[ \\begin{aligned} T(n) &amp; = 2n(n + 1) + (5n + 1) + 2 &amp; \\text{빠짐없이 계산 (-.-|||)} \\newline &amp; = 2n^2 + 7n + 3 \\newline T(n) &amp; = n^2 + n &amp; \\text{간단히 계산 (o.O)} \\end{aligned} \\]","path":["제 2 장   복잡도 분석","2.3   시간 복잡도"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#2-2","level":3,"title":"2.   2단계: 점근적 상한 판단","text":"<p>시간 복잡도는 \\(T(n)\\)에서 가장 높은 차수의 항으로 결정된다. \\(n\\)이 무한대로 향하면 가장 높은 차수의 항이 지배적인 영향을 주므로 나머지 항의 영향은 무시할 수 있기 때문이다.</p> <p>아래 표(2-2)는 몇 가지 예를 보여 준다. 일부 값이 과장되어 있는 이유는 ‘계수로는 차수를 뒤집을 수 없다’는 결론을 강조하기 위해서다. \\(n\\)이 무한대로 향하면 이러한 상수는 중요하지 않게 된다.</p> <p> 표 2-2   연산 횟수에 따른 시간 복잡도 </p> 연산 횟수 \\(T(n)\\) 시간 복잡도 \\(O(f(n))\\) \\(100000\\) \\(O(1)\\) \\(3n + 2\\) \\(O(n)\\) \\(2n^2 + 3n + 2\\) \\(O(n^2)\\) \\(n^3 + 10000n^2\\) \\(O(n^3)\\) \\(2^n + 10000n^{10000}\\) \\(O(2^n)\\)","path":["제 2 장   복잡도 분석","2.3   시간 복잡도"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#234","level":2,"title":"2.3.4   대표적인 유형","text":"<p>입력 데이터 크기를 \\(n\\)이라고 할 때, 대표적인 시간 복잡도 유형은 아래 그림(2-9)과 같다(낮은 순서대로 나열).</p> \\[ \\begin{aligned} &amp; O(1) &lt; O(\\log n) &lt; O(n) &lt; O(n \\log n) &lt; O(n^2) &lt; O(2^n) &lt; O(n!) \\newline &amp; \\text{상수} &lt; \\text{로그} &lt; \\text{선형} &lt; \\text{선형 로그} &lt; \\text{제곱} &lt; \\text{지수} &lt; \\text{팩토리얼} \\end{aligned} \\] <p></p> <p> 그림 2-9   대표적인 시간 복잡도 유형 </p>","path":["제 2 장   복잡도 분석","2.3   시간 복잡도"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#1-o1","level":3,"title":"1.   상수 시간 \\(O(1)\\)","text":"<p>상수 시간의 연산 횟수는 입력 데이터 크기 \\(n\\)과 무관하며, \\(n\\)이 변해도 달라지지 않는다.</p> <p>다음 함수에서 연산 횟수 <code>size</code>는 매우 클 수 있지만 입력 데이터 크기 \\(n\\)과 무관하므로, 시간 복잡도는 여전히 \\(O(1)\\)이다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py<pre><code>def constant(n: int) -&gt; int:\n \"\"\"상수 차수\"\"\"\n count = 0\n size = 100000\n for _ in range(size):\n count += 1\n return count\n</code></pre> time_complexity.cpp<pre><code>/* 상수 차수 */\nint constant(int n) {\n int count = 0;\n int size = 100000;\n for (int i = 0; i &lt; size; i++)\n count++;\n return count;\n}\n</code></pre> time_complexity.java<pre><code>/* 상수 차수 */\nint constant(int n) {\n int count = 0;\n int size = 100000;\n for (int i = 0; i &lt; size; i++)\n count++;\n return count;\n}\n</code></pre> time_complexity.cs<pre><code>/* 상수 차수 */\nint Constant(int n) {\n int count = 0;\n int size = 100000;\n for (int i = 0; i &lt; size; i++)\n count++;\n return count;\n}\n</code></pre> time_complexity.go<pre><code>/* 상수 차수 */\nfunc constant(n int) int {\n count := 0\n size := 100000\n for i := 0; i &lt; size; i++ {\n count++\n }\n return count\n}\n</code></pre> time_complexity.swift<pre><code>/* 상수 차수 */\nfunc constant(n: Int) -&gt; Int {\n var count = 0\n let size = 100_000\n for _ in 0 ..&lt; size {\n count += 1\n }\n return count\n}\n</code></pre> time_complexity.js<pre><code>/* 상수 차수 */\nfunction constant(n) {\n let count = 0;\n const size = 100000;\n for (let i = 0; i &lt; size; i++) count++;\n return count;\n}\n</code></pre> time_complexity.ts<pre><code>/* 상수 차수 */\nfunction constant(n: number): number {\n let count = 0;\n const size = 100000;\n for (let i = 0; i &lt; size; i++) count++;\n return count;\n}\n</code></pre> time_complexity.dart<pre><code>/* 상수 차수 */\nint constant(int n) {\n int count = 0;\n int size = 100000;\n for (var i = 0; i &lt; size; i++) {\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.rs<pre><code>/* 상수 차수 */\nfn constant(n: i32) -&gt; i32 {\n _ = n;\n let mut count = 0;\n let size = 100_000;\n for _ in 0..size {\n count += 1;\n }\n count\n}\n</code></pre> time_complexity.c<pre><code>/* 상수 차수 */\nint constant(int n) {\n int count = 0;\n int size = 100000;\n for (int i = 0; i &lt; size; i++) {\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.kt<pre><code>/* 상수 차수 */\nfun constant(n: Int): Int {\n var count = 0\n val size = 100000\n for (i in 0..&lt;size)\n count++\n return count\n}\n</code></pre> time_complexity.rb<pre><code>### 상수 차수 ###\ndef constant(n)\n count = 0\n size = 100000\n\n (0...size).each { count += 1 }\n\n count\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 2 장   복잡도 분석","2.3   시간 복잡도"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#2-on","level":3,"title":"2.   선형 시간 \\(O(n)\\)","text":"<p>선형 시간의 연산 횟수는 입력 데이터 크기 \\(n\\)에 대해 선형으로 증가한다. 선형 시간은 보통 단일 반복문에서 나타난다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py<pre><code>def linear(n: int) -&gt; int:\n \"\"\"선형 차수\"\"\"\n count = 0\n for _ in range(n):\n count += 1\n return count\n</code></pre> time_complexity.cpp<pre><code>/* 선형 차수 */\nint linear(int n) {\n int count = 0;\n for (int i = 0; i &lt; n; i++)\n count++;\n return count;\n}\n</code></pre> time_complexity.java<pre><code>/* 선형 차수 */\nint linear(int n) {\n int count = 0;\n for (int i = 0; i &lt; n; i++)\n count++;\n return count;\n}\n</code></pre> time_complexity.cs<pre><code>/* 선형 차수 */\nint Linear(int n) {\n int count = 0;\n for (int i = 0; i &lt; n; i++)\n count++;\n return count;\n}\n</code></pre> time_complexity.go<pre><code>/* 선형 차수 */\nfunc linear(n int) int {\n count := 0\n for i := 0; i &lt; n; i++ {\n count++\n }\n return count\n}\n</code></pre> time_complexity.swift<pre><code>/* 선형 차수 */\nfunc linear(n: Int) -&gt; Int {\n var count = 0\n for _ in 0 ..&lt; n {\n count += 1\n }\n return count\n}\n</code></pre> time_complexity.js<pre><code>/* 선형 차수 */\nfunction linear(n) {\n let count = 0;\n for (let i = 0; i &lt; n; i++) count++;\n return count;\n}\n</code></pre> time_complexity.ts<pre><code>/* 선형 차수 */\nfunction linear(n: number): number {\n let count = 0;\n for (let i = 0; i &lt; n; i++) count++;\n return count;\n}\n</code></pre> time_complexity.dart<pre><code>/* 선형 차수 */\nint linear(int n) {\n int count = 0;\n for (var i = 0; i &lt; n; i++) {\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.rs<pre><code>/* 선형 차수 */\nfn linear(n: i32) -&gt; i32 {\n let mut count = 0;\n for _ in 0..n {\n count += 1;\n }\n count\n}\n</code></pre> time_complexity.c<pre><code>/* 선형 차수 */\nint linear(int n) {\n int count = 0;\n for (int i = 0; i &lt; n; i++) {\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.kt<pre><code>/* 선형 차수 */\nfun linear(n: Int): Int {\n var count = 0\n for (i in 0..&lt;n)\n count++\n return count\n}\n</code></pre> time_complexity.rb<pre><code>### 선형 차수 ###\ndef linear(n)\n count = 0\n (0...n).each { count += 1 }\n count\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>배열 순회와 연결 리스트 순회 등의 연산은 시간 복잡도가 모두 \\(O(n)\\)이다. 여기서 \\(n\\)은 배열이나 연결 리스트의 길이다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py<pre><code>def array_traversal(nums: list[int]) -&gt; int:\n \"\"\"선형 차수(배열 순회)\"\"\"\n count = 0\n # 반복 횟수는 배열 길이에 비례한다\n for num in nums:\n count += 1\n return count\n</code></pre> time_complexity.cpp<pre><code>/* 선형 차수(배열 순회) */\nint arrayTraversal(vector&lt;int&gt; &amp;nums) {\n int count = 0;\n // 반복 횟수는 배열 길이에 비례한다\n for (int num : nums) {\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.java<pre><code>/* 선형 차수(배열 순회) */\nint arrayTraversal(int[] nums) {\n int count = 0;\n // 반복 횟수는 배열 길이에 비례한다\n for (int num : nums) {\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.cs<pre><code>/* 선형 차수(배열 순회) */\nint ArrayTraversal(int[] nums) {\n int count = 0;\n // 반복 횟수는 배열 길이에 비례한다\n foreach (int num in nums) {\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.go<pre><code>/* 선형 차수(배열 순회) */\nfunc arrayTraversal(nums []int) int {\n count := 0\n // 반복 횟수는 배열 길이에 비례한다\n for range nums {\n count++\n }\n return count\n}\n</code></pre> time_complexity.swift<pre><code>/* 선형 차수(배열 순회) */\nfunc arrayTraversal(nums: [Int]) -&gt; Int {\n var count = 0\n // 반복 횟수는 배열 길이에 비례한다\n for _ in nums {\n count += 1\n }\n return count\n}\n</code></pre> time_complexity.js<pre><code>/* 선형 차수(배열 순회) */\nfunction arrayTraversal(nums) {\n let count = 0;\n // 반복 횟수는 배열 길이에 비례한다\n for (let i = 0; i &lt; nums.length; i++) {\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.ts<pre><code>/* 선형 차수(배열 순회) */\nfunction arrayTraversal(nums: number[]): number {\n let count = 0;\n // 반복 횟수는 배열 길이에 비례한다\n for (let i = 0; i &lt; nums.length; i++) {\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.dart<pre><code>/* 선형 차수(배열 순회) */\nint arrayTraversal(List&lt;int&gt; nums) {\n int count = 0;\n // 반복 횟수는 배열 길이에 비례한다\n for (var _num in nums) {\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.rs<pre><code>/* 선형 차수(배열 순회) */\nfn array_traversal(nums: &amp;[i32]) -&gt; i32 {\n let mut count = 0;\n // 반복 횟수는 배열 길이에 비례한다\n for _ in nums {\n count += 1;\n }\n count\n}\n</code></pre> time_complexity.c<pre><code>/* 선형 차수(배열 순회) */\nint arrayTraversal(int *nums, int n) {\n int count = 0;\n // 반복 횟수는 배열 길이에 비례한다\n for (int i = 0; i &lt; n; i++) {\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.kt<pre><code>/* 선형 차수(배열 순회) */\nfun arrayTraversal(nums: IntArray): Int {\n var count = 0\n // 반복 횟수는 배열 길이에 비례한다\n for (num in nums) {\n count++\n }\n return count\n}\n</code></pre> time_complexity.rb<pre><code>### 선형 차수(배열 순회)###\ndef array_traversal(nums)\n count = 0\n\n # 반복 횟수는 배열 길이에 비례한다\n for num in nums\n count += 1\n end\n\n count\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>입력 데이터 크기 \\(n\\)은 입력 데이터의 종류에 따라 구체적으로 정해야 한다는 점에 주의하자. 첫 번째 예에서는 변수 \\(n\\)이 입력 데이터 크기이고, 두 번째 예에서는 배열의 길이 \\(n\\)이 데이터 크기다.</p>","path":["제 2 장   복잡도 분석","2.3   시간 복잡도"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#3-on2","level":3,"title":"3.   제곱 시간 \\(O(n^2)\\)","text":"<p>제곱 시간의 연산 횟수는 입력 데이터 크기 \\(n\\)의 제곱에 비례하여 증가한다. 보통 중첩 반복문에서 나타나며, 바깥쪽과 안쪽 반복문의 시간 복잡도가 각각 \\(O(n)\\)이므로 전체 시간 복잡도는 \\(O(n^2)\\)이다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py<pre><code>def quadratic(n: int) -&gt; int:\n \"\"\"제곱 차수\"\"\"\n count = 0\n # 반복 횟수는 데이터 크기 n의 제곱에 비례한다\n for i in range(n):\n for j in range(n):\n count += 1\n return count\n</code></pre> time_complexity.cpp<pre><code>/* 제곱 차수 */\nint quadratic(int n) {\n int count = 0;\n // 반복 횟수는 데이터 크기 n의 제곱에 비례한다\n for (int i = 0; i &lt; n; i++) {\n for (int j = 0; j &lt; n; j++) {\n count++;\n }\n }\n return count;\n}\n</code></pre> time_complexity.java<pre><code>/* 제곱 차수 */\nint quadratic(int n) {\n int count = 0;\n // 반복 횟수는 데이터 크기 n의 제곱에 비례한다\n for (int i = 0; i &lt; n; i++) {\n for (int j = 0; j &lt; n; j++) {\n count++;\n }\n }\n return count;\n}\n</code></pre> time_complexity.cs<pre><code>/* 제곱 차수 */\nint Quadratic(int n) {\n int count = 0;\n // 반복 횟수는 데이터 크기 n의 제곱에 비례한다\n for (int i = 0; i &lt; n; i++) {\n for (int j = 0; j &lt; n; j++) {\n count++;\n }\n }\n return count;\n}\n</code></pre> time_complexity.go<pre><code>/* 제곱 차수 */\nfunc quadratic(n int) int {\n count := 0\n // 반복 횟수는 데이터 크기 n의 제곱에 비례한다\n for i := 0; i &lt; n; i++ {\n for j := 0; j &lt; n; j++ {\n count++\n }\n }\n return count\n}\n</code></pre> time_complexity.swift<pre><code>/* 제곱 차수 */\nfunc quadratic(n: Int) -&gt; Int {\n var count = 0\n // 반복 횟수는 데이터 크기 n의 제곱에 비례한다\n for _ in 0 ..&lt; n {\n for _ in 0 ..&lt; n {\n count += 1\n }\n }\n return count\n}\n</code></pre> time_complexity.js<pre><code>/* 제곱 차수 */\nfunction quadratic(n) {\n let count = 0;\n // 반복 횟수는 데이터 크기 n의 제곱에 비례한다\n for (let i = 0; i &lt; n; i++) {\n for (let j = 0; j &lt; n; j++) {\n count++;\n }\n }\n return count;\n}\n</code></pre> time_complexity.ts<pre><code>/* 제곱 차수 */\nfunction quadratic(n: number): number {\n let count = 0;\n // 반복 횟수는 데이터 크기 n의 제곱에 비례한다\n for (let i = 0; i &lt; n; i++) {\n for (let j = 0; j &lt; n; j++) {\n count++;\n }\n }\n return count;\n}\n</code></pre> time_complexity.dart<pre><code>/* 제곱 차수 */\nint quadratic(int n) {\n int count = 0;\n // 반복 횟수는 데이터 크기 n의 제곱에 비례한다\n for (int i = 0; i &lt; n; i++) {\n for (int j = 0; j &lt; n; j++) {\n count++;\n }\n }\n return count;\n}\n</code></pre> time_complexity.rs<pre><code>/* 제곱 차수 */\nfn quadratic(n: i32) -&gt; i32 {\n let mut count = 0;\n // 반복 횟수는 데이터 크기 n의 제곱에 비례한다\n for _ in 0..n {\n for _ in 0..n {\n count += 1;\n }\n }\n count\n}\n</code></pre> time_complexity.c<pre><code>/* 제곱 차수 */\nint quadratic(int n) {\n int count = 0;\n // 반복 횟수는 데이터 크기 n의 제곱에 비례한다\n for (int i = 0; i &lt; n; i++) {\n for (int j = 0; j &lt; n; j++) {\n count++;\n }\n }\n return count;\n}\n</code></pre> time_complexity.kt<pre><code>/* 제곱 차수 */\nfun quadratic(n: Int): Int {\n var count = 0\n // 반복 횟수는 데이터 크기 n의 제곱에 비례한다\n for (i in 0..&lt;n) {\n for (j in 0..&lt;n) {\n count++\n }\n }\n return count\n}\n</code></pre> time_complexity.rb<pre><code>### 제곱 차수 ###\ndef quadratic(n)\n count = 0\n\n # 반복 횟수는 데이터 크기 n의 제곱에 비례한다\n for i in 0...n\n for j in 0...n\n count += 1\n end\n end\n\n count\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>아래 그림(2-10)은 상수 시간, 선형 시간, 제곱 시간의 세 가지 시간 복잡도를 비교한다.</p> <p></p> <p> 그림 2-10   상수 시간, 선형 시간, 제곱 시간의 시간 복잡도 </p> <p>버블 정렬을 예로 들면, 바깥쪽 반복문은 \\(n - 1\\)번 실행되고 안쪽 반복문은 \\(n-1\\), \\(n-2\\), \\(\\dots\\), \\(2\\), \\(1\\)번, 평균 \\(n / 2\\)번 실행된다. 따라서 시간 복잡도는 \\(O((n - 1) n / 2) = O(n^2)\\)이다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py<pre><code>def bubble_sort(nums: list[int]) -&gt; int:\n \"\"\"제곱 차수(버블 정렬)\"\"\"\n count = 0 # 카운터\n # 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for i in range(len(nums) - 1, 0, -1):\n # 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for j in range(i):\n if nums[j] &gt; nums[j + 1]:\n # nums[j]와 nums[j + 1]을 교환한다\n tmp: int = nums[j]\n nums[j] = nums[j + 1]\n nums[j + 1] = tmp\n count += 3 # 원소 교환에는 단위 연산 3회가 포함된다\n return count\n</code></pre> time_complexity.cpp<pre><code>/* 제곱 차수(버블 정렬) */\nint bubbleSort(vector&lt;int&gt; &amp;nums) {\n int count = 0; // 카운터\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for (int i = nums.size() - 1; i &gt; 0; i--) {\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for (int j = 0; j &lt; i; j++) {\n if (nums[j] &gt; nums[j + 1]) {\n // nums[j]와 nums[j + 1]을 교환한다\n int tmp = nums[j];\n nums[j] = nums[j + 1];\n nums[j + 1] = tmp;\n count += 3; // 원소 교환에는 단위 연산 3회가 포함된다\n }\n }\n }\n return count;\n}\n</code></pre> time_complexity.java<pre><code>/* 제곱 차수(버블 정렬) */\nint bubbleSort(int[] nums) {\n int count = 0; // 카운터\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for (int i = nums.length - 1; i &gt; 0; i--) {\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for (int j = 0; j &lt; i; j++) {\n if (nums[j] &gt; nums[j + 1]) {\n // nums[j]와 nums[j + 1]을 교환한다\n int tmp = nums[j];\n nums[j] = nums[j + 1];\n nums[j + 1] = tmp;\n count += 3; // 원소 교환에는 단위 연산 3회가 포함된다\n }\n }\n }\n return count;\n}\n</code></pre> time_complexity.cs<pre><code>/* 제곱 차수(버블 정렬) */\nint BubbleSort(int[] nums) {\n int count = 0; // 카운터\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for (int i = nums.Length - 1; i &gt; 0; i--) {\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for (int j = 0; j &lt; i; j++) {\n if (nums[j] &gt; nums[j + 1]) {\n // nums[j]와 nums[j + 1]을 교환한다\n (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n count += 3; // 원소 교환에는 단위 연산 3회가 포함된다\n }\n }\n }\n return count;\n}\n</code></pre> time_complexity.go<pre><code>/* 제곱 차수(버블 정렬) */\nfunc bubbleSort(nums []int) int {\n count := 0 // 카운터\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for i := len(nums) - 1; i &gt; 0; i-- {\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for j := 0; j &lt; i; j++ {\n if nums[j] &gt; nums[j+1] {\n // nums[j]와 nums[j + 1]을 교환한다\n tmp := nums[j]\n nums[j] = nums[j+1]\n nums[j+1] = tmp\n count += 3 // 원소 교환에는 단위 연산 3회가 포함된다\n }\n }\n }\n return count\n}\n</code></pre> time_complexity.swift<pre><code>/* 제곱 차수(버블 정렬) */\nfunc bubbleSort(nums: inout [Int]) -&gt; Int {\n var count = 0 // 카운터\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for i in nums.indices.dropFirst().reversed() {\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for j in 0 ..&lt; i {\n if nums[j] &gt; nums[j + 1] {\n // nums[j]와 nums[j + 1]을 교환한다\n let tmp = nums[j]\n nums[j] = nums[j + 1]\n nums[j + 1] = tmp\n count += 3 // 원소 교환에는 단위 연산 3회가 포함된다\n }\n }\n }\n return count\n}\n</code></pre> time_complexity.js<pre><code>/* 제곱 차수(버블 정렬) */\nfunction bubbleSort(nums) {\n let count = 0; // 카운터\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for (let i = nums.length - 1; i &gt; 0; i--) {\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for (let j = 0; j &lt; i; j++) {\n if (nums[j] &gt; nums[j + 1]) {\n // nums[j]와 nums[j + 1]을 교환한다\n let tmp = nums[j];\n nums[j] = nums[j + 1];\n nums[j + 1] = tmp;\n count += 3; // 원소 교환에는 단위 연산 3회가 포함된다\n }\n }\n }\n return count;\n}\n</code></pre> time_complexity.ts<pre><code>/* 제곱 차수(버블 정렬) */\nfunction bubbleSort(nums: number[]): number {\n let count = 0; // 카운터\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for (let i = nums.length - 1; i &gt; 0; i--) {\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for (let j = 0; j &lt; i; j++) {\n if (nums[j] &gt; nums[j + 1]) {\n // nums[j]와 nums[j + 1]을 교환한다\n let tmp = nums[j];\n nums[j] = nums[j + 1];\n nums[j + 1] = tmp;\n count += 3; // 원소 교환에는 단위 연산 3회가 포함된다\n }\n }\n }\n return count;\n}\n</code></pre> time_complexity.dart<pre><code>/* 제곱 차수(버블 정렬) */\nint bubbleSort(List&lt;int&gt; nums) {\n int count = 0; // 카운터\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for (var i = nums.length - 1; i &gt; 0; i--) {\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for (var j = 0; j &lt; i; j++) {\n if (nums[j] &gt; nums[j + 1]) {\n // nums[j]와 nums[j + 1]을 교환한다\n int tmp = nums[j];\n nums[j] = nums[j + 1];\n nums[j + 1] = tmp;\n count += 3; // 원소 교환에는 단위 연산 3회가 포함된다\n }\n }\n }\n return count;\n}\n</code></pre> time_complexity.rs<pre><code>/* 제곱 차수(버블 정렬) */\nfn bubble_sort(nums: &amp;mut [i32]) -&gt; i32 {\n let mut count = 0; // 카운터\n\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for i in (1..nums.len()).rev() {\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for j in 0..i {\n if nums[j] &gt; nums[j + 1] {\n // nums[j]와 nums[j + 1]을 교환한다\n let tmp = nums[j];\n nums[j] = nums[j + 1];\n nums[j + 1] = tmp;\n count += 3; // 원소 교환에는 단위 연산 3회가 포함된다\n }\n }\n }\n count\n}\n</code></pre> time_complexity.c<pre><code>/* 제곱 차수(버블 정렬) */\nint bubbleSort(int *nums, int n) {\n int count = 0; // 카운터\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for (int i = n - 1; i &gt; 0; i--) {\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for (int j = 0; j &lt; i; j++) {\n if (nums[j] &gt; nums[j + 1]) {\n // nums[j]와 nums[j + 1]을 교환한다\n int tmp = nums[j];\n nums[j] = nums[j + 1];\n nums[j + 1] = tmp;\n count += 3; // 원소 교환에는 단위 연산 3회가 포함된다\n }\n }\n }\n return count;\n}\n</code></pre> time_complexity.kt<pre><code>/* 제곱 차수(버블 정렬) */\nfun bubbleSort(nums: IntArray): Int {\n var count = 0 // 카운터\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for (i in nums.size - 1 downTo 1) {\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for (j in 0..&lt;i) {\n if (nums[j] &gt; nums[j + 1]) {\n // nums[j]와 nums[j + 1]을 교환한다\n val temp = nums[j]\n nums[j] = nums[j + 1]\n nums[j + 1] = temp\n count += 3 // 원소 교환에는 단위 연산 3회가 포함된다\n }\n }\n }\n return count\n}\n</code></pre> time_complexity.rb<pre><code>### 제곱 차수(버블 정렬)###\ndef bubble_sort(nums)\n count = 0 # 카운터\n\n # 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for i in (nums.length - 1).downto(0)\n # 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for j in 0...i\n if nums[j] &gt; nums[j + 1]\n # nums[j]와 nums[j + 1]을 교환한다\n tmp = nums[j]\n nums[j] = nums[j + 1]\n nums[j + 1] = tmp\n count += 3 # 원소 교환에는 단위 연산 3회가 포함된다\n end\n end\n end\n\n count\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 2 장   복잡도 분석","2.3   시간 복잡도"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#4-o2n","level":3,"title":"4.   지수 시간 \\(O(2^n)\\)","text":"<p>생물학의 ‘세포 분열’은 지수적 증가의 대표적인 예다. 처음에는 세포가 \\(1\\)개이고, 한 차례 분열하면 \\(2\\)개, 두 차례 분열하면 \\(4\\)개가 된다. 이처럼 \\(n\\)차례 분열하면 세포는 \\(2^n\\)개가 된다.</p> <p>아래 그림(2-11)과 코드는 세포 분열 과정을 재현하며, 시간 복잡도는 \\(O(2^n)\\)이다. 입력 \\(n\\)은 분열이 진행되는 라운드 수이고, 반환값 <code>count</code>는 전체 분열 횟수라는 점에 주의하자.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py<pre><code>def exponential(n: int) -&gt; int:\n \"\"\"지수 차수(반복문 구현)\"\"\"\n count = 0\n base = 1\n # 세포가 매번 둘로 나뉘어 수열 1, 2, 4, 8, ..., 2^(n-1)을 이룬다\n for _ in range(n):\n for _ in range(base):\n count += 1\n base *= 2\n # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n return count\n</code></pre> time_complexity.cpp<pre><code>/* 지수 차수(반복문 구현) */\nint exponential(int n) {\n int count = 0, base = 1;\n // 세포가 매번 둘로 나뉘어 수열 1, 2, 4, 8, ..., 2^(n-1)을 이룬다\n for (int i = 0; i &lt; n; i++) {\n for (int j = 0; j &lt; base; j++) {\n count++;\n }\n base *= 2;\n }\n // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n return count;\n}\n</code></pre> time_complexity.java<pre><code>/* 지수 차수(반복문 구현) */\nint exponential(int n) {\n int count = 0, base = 1;\n // 세포가 매번 둘로 나뉘어 수열 1, 2, 4, 8, ..., 2^(n-1)을 이룬다\n for (int i = 0; i &lt; n; i++) {\n for (int j = 0; j &lt; base; j++) {\n count++;\n }\n base *= 2;\n }\n // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n return count;\n}\n</code></pre> time_complexity.cs<pre><code>/* 지수 차수(반복문 구현) */\nint Exponential(int n) {\n int count = 0, bas = 1;\n // 세포가 매번 둘로 나뉘어 수열 1, 2, 4, 8, ..., 2^(n-1)을 이룬다\n for (int i = 0; i &lt; n; i++) {\n for (int j = 0; j &lt; bas; j++) {\n count++;\n }\n bas *= 2;\n }\n // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n return count;\n}\n</code></pre> time_complexity.go<pre><code>/* 지수 차수(반복문 구현)*/\nfunc exponential(n int) int {\n count, base := 0, 1\n // 세포가 매번 둘로 나뉘어 수열 1, 2, 4, 8, ..., 2^(n-1)을 이룬다\n for i := 0; i &lt; n; i++ {\n for j := 0; j &lt; base; j++ {\n count++\n }\n base *= 2\n }\n // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n return count\n}\n</code></pre> time_complexity.swift<pre><code>/* 지수 차수(반복문 구현) */\nfunc exponential(n: Int) -&gt; Int {\n var count = 0\n var base = 1\n // 세포가 매번 둘로 나뉘어 수열 1, 2, 4, 8, ..., 2^(n-1)을 이룬다\n for _ in 0 ..&lt; n {\n for _ in 0 ..&lt; base {\n count += 1\n }\n base *= 2\n }\n // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n return count\n}\n</code></pre> time_complexity.js<pre><code>/* 지수 차수(반복문 구현) */\nfunction exponential(n) {\n let count = 0,\n base = 1;\n // 세포가 매번 둘로 나뉘어 수열 1, 2, 4, 8, ..., 2^(n-1)을 이룬다\n for (let i = 0; i &lt; n; i++) {\n for (let j = 0; j &lt; base; j++) {\n count++;\n }\n base *= 2;\n }\n // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n return count;\n}\n</code></pre> time_complexity.ts<pre><code>/* 지수 차수(반복문 구현) */\nfunction exponential(n: number): number {\n let count = 0,\n base = 1;\n // 세포가 매번 둘로 나뉘어 수열 1, 2, 4, 8, ..., 2^(n-1)을 이룬다\n for (let i = 0; i &lt; n; i++) {\n for (let j = 0; j &lt; base; j++) {\n count++;\n }\n base *= 2;\n }\n // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n return count;\n}\n</code></pre> time_complexity.dart<pre><code>/* 지수 차수(반복문 구현) */\nint exponential(int n) {\n int count = 0, base = 1;\n // 세포가 매번 둘로 나뉘어 수열 1, 2, 4, 8, ..., 2^(n-1)을 이룬다\n for (var i = 0; i &lt; n; i++) {\n for (var j = 0; j &lt; base; j++) {\n count++;\n }\n base *= 2;\n }\n // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n return count;\n}\n</code></pre> time_complexity.rs<pre><code>/* 지수 차수(반복문 구현) */\nfn exponential(n: i32) -&gt; i32 {\n let mut count = 0;\n let mut base = 1;\n // 세포가 매번 둘로 나뉘어 수열 1, 2, 4, 8, ..., 2^(n-1)을 이룬다\n for _ in 0..n {\n for _ in 0..base {\n count += 1\n }\n base *= 2;\n }\n // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n count\n}\n</code></pre> time_complexity.c<pre><code>/* 지수 차수(반복문 구현) */\nint exponential(int n) {\n int count = 0;\n int bas = 1;\n // 세포가 매번 둘로 나뉘어 수열 1, 2, 4, 8, ..., 2^(n-1)을 이룬다\n for (int i = 0; i &lt; n; i++) {\n for (int j = 0; j &lt; bas; j++) {\n count++;\n }\n bas *= 2;\n }\n // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n return count;\n}\n</code></pre> time_complexity.kt<pre><code>/* 지수 차수(반복문 구현) */\nfun exponential(n: Int): Int {\n var count = 0\n var base = 1\n // 세포가 매번 둘로 나뉘어 수열 1, 2, 4, 8, ..., 2^(n-1)을 이룬다\n for (i in 0..&lt;n) {\n for (j in 0..&lt;base) {\n count++\n }\n base *= 2\n }\n // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n return count\n}\n</code></pre> time_complexity.rb<pre><code>### 지수 차수(반복문 구현)###\ndef exponential(n)\n count, base = 0, 1\n\n # 세포가 매번 둘로 나뉘어 수열 1, 2, 4, 8, ..., 2^(n-1)을 이룬다\n (0...n).each do\n (0...base).each { count += 1 }\n base *= 2\n end\n\n # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n count\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p></p> <p> 그림 2-11   지수 시간 복잡도 </p> <p>실제 알고리즘에서 지수 시간은 재귀 함수에 자주 나타난다. 예를 들어 다음 코드는 재귀적으로 둘로 나뉘며, \\(n\\)번 나뉜 뒤 멈춘다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py<pre><code>def exp_recur(n: int) -&gt; int:\n \"\"\"지수 차수(재귀 구현)\"\"\"\n if n == 1:\n return 1\n return exp_recur(n - 1) + exp_recur(n - 1) + 1\n</code></pre> time_complexity.cpp<pre><code>/* 지수 차수(재귀 구현) */\nint expRecur(int n) {\n if (n == 1)\n return 1;\n return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n</code></pre> time_complexity.java<pre><code>/* 지수 차수(재귀 구현) */\nint expRecur(int n) {\n if (n == 1)\n return 1;\n return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n</code></pre> time_complexity.cs<pre><code>/* 지수 차수(재귀 구현) */\nint ExpRecur(int n) {\n if (n == 1) return 1;\n return ExpRecur(n - 1) + ExpRecur(n - 1) + 1;\n}\n</code></pre> time_complexity.go<pre><code>/* 지수 차수(재귀 구현)*/\nfunc expRecur(n int) int {\n if n == 1 {\n return 1\n }\n return expRecur(n-1) + expRecur(n-1) + 1\n}\n</code></pre> time_complexity.swift<pre><code>/* 지수 차수(재귀 구현) */\nfunc expRecur(n: Int) -&gt; Int {\n if n == 1 {\n return 1\n }\n return expRecur(n: n - 1) + expRecur(n: n - 1) + 1\n}\n</code></pre> time_complexity.js<pre><code>/* 지수 차수(재귀 구현) */\nfunction expRecur(n) {\n if (n === 1) return 1;\n return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n</code></pre> time_complexity.ts<pre><code>/* 지수 차수(재귀 구현) */\nfunction expRecur(n: number): number {\n if (n === 1) return 1;\n return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n</code></pre> time_complexity.dart<pre><code>/* 지수 차수(재귀 구현) */\nint expRecur(int n) {\n if (n == 1) return 1;\n return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n</code></pre> time_complexity.rs<pre><code>/* 지수 차수(재귀 구현) */\nfn exp_recur(n: i32) -&gt; i32 {\n if n == 1 {\n return 1;\n }\n exp_recur(n - 1) + exp_recur(n - 1) + 1\n}\n</code></pre> time_complexity.c<pre><code>/* 지수 차수(재귀 구현) */\nint expRecur(int n) {\n if (n == 1)\n return 1;\n return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n</code></pre> time_complexity.kt<pre><code>/* 지수 차수(재귀 구현) */\nfun expRecur(n: Int): Int {\n if (n == 1) {\n return 1\n }\n return expRecur(n - 1) + expRecur(n - 1) + 1\n}\n</code></pre> time_complexity.rb<pre><code>### 지수 차수(재귀 구현)###\ndef exp_recur(n)\n return 1 if n == 1\n exp_recur(n - 1) + exp_recur(n - 1) + 1\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>지수 시간은 매우 빠르게 증가하며 완전 열거(완전 탐색, 백트래킹 등)에 흔히 나타난다. 데이터가 많은 문제에서는 지수 시간을 감당하기 어려우므로, 보통 동적 계획법이나 탐욕 알고리즘 등으로 해결해야 한다.</p>","path":["제 2 장   복잡도 분석","2.3   시간 복잡도"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#5-olog-n","level":3,"title":"5.   로그 시간 \\(O(\\log n)\\)","text":"<p>지수 시간과 반대로 로그 시간은 ‘매번 절반으로 줄어드는’ 상황을 나타낸다. 입력 데이터 크기가 \\(n\\)이고 매번 절반으로 줄어들므로, 반복 횟수는 \\(2^n\\)의 역함수인 \\(\\log_2 n\\)이다.</p> <p>아래 그림(2-12)과 코드는 ‘매번 절반으로 줄어드는’ 과정을 재현한다. 시간 복잡도는 \\(O(\\log_2 n)\\)이며, 줄여서 \\(O(\\log n)\\)으로 쓴다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py<pre><code>def logarithmic(n: int) -&gt; int:\n \"\"\"로그 차수(반복문 구현)\"\"\"\n count = 0\n while n &gt; 1:\n n = n / 2\n count += 1\n return count\n</code></pre> time_complexity.cpp<pre><code>/* 로그 차수(반복문 구현) */\nint logarithmic(int n) {\n int count = 0;\n while (n &gt; 1) {\n n = n / 2;\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.java<pre><code>/* 로그 차수(반복문 구현) */\nint logarithmic(int n) {\n int count = 0;\n while (n &gt; 1) {\n n = n / 2;\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.cs<pre><code>/* 로그 차수(반복문 구현) */\nint Logarithmic(int n) {\n int count = 0;\n while (n &gt; 1) {\n n /= 2;\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.go<pre><code>/* 로그 차수(반복문 구현)*/\nfunc logarithmic(n int) int {\n count := 0\n for n &gt; 1 {\n n = n / 2\n count++\n }\n return count\n}\n</code></pre> time_complexity.swift<pre><code>/* 로그 차수(반복문 구현) */\nfunc logarithmic(n: Int) -&gt; Int {\n var count = 0\n var n = n\n while n &gt; 1 {\n n = n / 2\n count += 1\n }\n return count\n}\n</code></pre> time_complexity.js<pre><code>/* 로그 차수(반복문 구현) */\nfunction logarithmic(n) {\n let count = 0;\n while (n &gt; 1) {\n n = n / 2;\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.ts<pre><code>/* 로그 차수(반복문 구현) */\nfunction logarithmic(n: number): number {\n let count = 0;\n while (n &gt; 1) {\n n = n / 2;\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.dart<pre><code>/* 로그 차수(반복문 구현) */\nint logarithmic(int n) {\n int count = 0;\n while (n &gt; 1) {\n n = n ~/ 2;\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.rs<pre><code>/* 로그 차수(반복문 구현) */\nfn logarithmic(mut n: i32) -&gt; i32 {\n let mut count = 0;\n while n &gt; 1 {\n n = n / 2;\n count += 1;\n }\n count\n}\n</code></pre> time_complexity.c<pre><code>/* 로그 차수(반복문 구현) */\nint logarithmic(int n) {\n int count = 0;\n while (n &gt; 1) {\n n = n / 2;\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.kt<pre><code>/* 로그 차수(반복문 구현) */\nfun logarithmic(n: Int): Int {\n var n1 = n\n var count = 0\n while (n1 &gt; 1) {\n n1 /= 2\n count++\n }\n return count\n}\n</code></pre> time_complexity.rb<pre><code>### 로그 차수(반복문 구현)###\ndef logarithmic(n)\n count = 0\n\n while n &gt; 1\n n /= 2\n count += 1\n end\n\n count\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p></p> <p> 그림 2-12   로그 시간 복잡도 </p> <p>지수 시간처럼 로그 시간도 재귀 함수에 자주 나타난다. 다음 코드는 높이가 \\(\\log_2 n\\)인 재귀 트리를 만든다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py<pre><code>def log_recur(n: int) -&gt; int:\n \"\"\"로그 차수(재귀 구현)\"\"\"\n if n &lt;= 1:\n return 0\n return log_recur(n / 2) + 1\n</code></pre> time_complexity.cpp<pre><code>/* 로그 차수(재귀 구현) */\nint logRecur(int n) {\n if (n &lt;= 1)\n return 0;\n return logRecur(n / 2) + 1;\n}\n</code></pre> time_complexity.java<pre><code>/* 로그 차수(재귀 구현) */\nint logRecur(int n) {\n if (n &lt;= 1)\n return 0;\n return logRecur(n / 2) + 1;\n}\n</code></pre> time_complexity.cs<pre><code>/* 로그 차수(재귀 구현) */\nint LogRecur(int n) {\n if (n &lt;= 1) return 0;\n return LogRecur(n / 2) + 1;\n}\n</code></pre> time_complexity.go<pre><code>/* 로그 차수(재귀 구현)*/\nfunc logRecur(n int) int {\n if n &lt;= 1 {\n return 0\n }\n return logRecur(n/2) + 1\n}\n</code></pre> time_complexity.swift<pre><code>/* 로그 차수(재귀 구현) */\nfunc logRecur(n: Int) -&gt; Int {\n if n &lt;= 1 {\n return 0\n }\n return logRecur(n: n / 2) + 1\n}\n</code></pre> time_complexity.js<pre><code>/* 로그 차수(재귀 구현) */\nfunction logRecur(n) {\n if (n &lt;= 1) return 0;\n return logRecur(n / 2) + 1;\n}\n</code></pre> time_complexity.ts<pre><code>/* 로그 차수(재귀 구현) */\nfunction logRecur(n: number): number {\n if (n &lt;= 1) return 0;\n return logRecur(n / 2) + 1;\n}\n</code></pre> time_complexity.dart<pre><code>/* 로그 차수(재귀 구현) */\nint logRecur(int n) {\n if (n &lt;= 1) return 0;\n return logRecur(n ~/ 2) + 1;\n}\n</code></pre> time_complexity.rs<pre><code>/* 로그 차수(재귀 구현) */\nfn log_recur(n: i32) -&gt; i32 {\n if n &lt;= 1 {\n return 0;\n }\n log_recur(n / 2) + 1\n}\n</code></pre> time_complexity.c<pre><code>/* 로그 차수(재귀 구현) */\nint logRecur(int n) {\n if (n &lt;= 1)\n return 0;\n return logRecur(n / 2) + 1;\n}\n</code></pre> time_complexity.kt<pre><code>/* 로그 차수(재귀 구현) */\nfun logRecur(n: Int): Int {\n if (n &lt;= 1)\n return 0\n return logRecur(n / 2) + 1\n}\n</code></pre> time_complexity.rb<pre><code>### 로그 차수(재귀 구현)###\ndef log_recur(n)\n return 0 unless n &gt; 1\n log_recur(n / 2) + 1\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>로그 시간은 분할 정복 전략에 기반한 알고리즘에 자주 나타나며, ‘하나를 여러 개로 나누기’, ‘복잡한 것을 단순하게 만들기’라는 알고리즘 사고방식을 보여 준다. 증가 속도가 느려 상수 시간 다음으로 이상적인 시간 복잡도다.</p> <p>\\(O(\\log n)\\)의 밑은 무엇인가?</p> <p>정확히 말하면, ‘하나를 \\(m\\)개로 나누기’에 해당하는 시간 복잡도는 \\(O(\\log_m n)\\)이다. 로그의 밑 변환 공식을 적용하면 밑이 달라도 같은 시간 복잡도를 얻는다.</p> \\[ O(\\log_m n) = O(\\log_k n / \\log_k m) = O(\\log_k n) \\] <p>즉, 밑 \\(m\\)은 복잡도에 영향을 주지 않고 바꿀 수 있다. 따라서 보통 밑 \\(m\\)을 생략하고 로그 시간을 \\(O(\\log n)\\)으로 표기한다.</p>","path":["제 2 장   복잡도 분석","2.3   시간 복잡도"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#6-on-log-n","level":3,"title":"6.   선형 로그 시간 \\(O(n \\log n)\\)","text":"<p>선형 로그 시간은 중첩 반복문에 자주 나타나며, 두 반복문의 시간 복잡도는 각각 \\(O(\\log n)\\)과 \\(O(n)\\)이다. 관련 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py<pre><code>def linear_log_recur(n: int) -&gt; int:\n \"\"\"선형 로그 차수\"\"\"\n if n &lt;= 1:\n return 1\n # 둘로 나누어 부분 문제의 크기를 절반으로 줄인다\n count = linear_log_recur(n // 2) + linear_log_recur(n // 2)\n # 현재 부분 문제에는 n회의 연산이 포함된다\n for _ in range(n):\n count += 1\n return count\n</code></pre> time_complexity.cpp<pre><code>/* 선형 로그 차수 */\nint linearLogRecur(int n) {\n if (n &lt;= 1)\n return 1;\n int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n for (int i = 0; i &lt; n; i++) {\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.java<pre><code>/* 선형 로그 차수 */\nint linearLogRecur(int n) {\n if (n &lt;= 1)\n return 1;\n int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n for (int i = 0; i &lt; n; i++) {\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.cs<pre><code>/* 선형 로그 차수 */\nint LinearLogRecur(int n) {\n if (n &lt;= 1) return 1;\n int count = LinearLogRecur(n / 2) + LinearLogRecur(n / 2);\n for (int i = 0; i &lt; n; i++) {\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.go<pre><code>/* 선형 로그 차수 */\nfunc linearLogRecur(n int) int {\n if n &lt;= 1 {\n return 1\n }\n count := linearLogRecur(n/2) + linearLogRecur(n/2)\n for i := 0; i &lt; n; i++ {\n count++\n }\n return count\n}\n</code></pre> time_complexity.swift<pre><code>/* 선형 로그 차수 */\nfunc linearLogRecur(n: Int) -&gt; Int {\n if n &lt;= 1 {\n return 1\n }\n var count = linearLogRecur(n: n / 2) + linearLogRecur(n: n / 2)\n for _ in stride(from: 0, to: n, by: 1) {\n count += 1\n }\n return count\n}\n</code></pre> time_complexity.js<pre><code>/* 선형 로그 차수 */\nfunction linearLogRecur(n) {\n if (n &lt;= 1) return 1;\n let count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n for (let i = 0; i &lt; n; i++) {\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.ts<pre><code>/* 선형 로그 차수 */\nfunction linearLogRecur(n: number): number {\n if (n &lt;= 1) return 1;\n let count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n for (let i = 0; i &lt; n; i++) {\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.dart<pre><code>/* 선형 로그 차수 */\nint linearLogRecur(int n) {\n if (n &lt;= 1) return 1;\n int count = linearLogRecur(n ~/ 2) + linearLogRecur(n ~/ 2);\n for (var i = 0; i &lt; n; i++) {\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.rs<pre><code>/* 선형 로그 차수 */\nfn linear_log_recur(n: i32) -&gt; i32 {\n if n &lt;= 1 {\n return 1;\n }\n let mut count = linear_log_recur(n / 2) + linear_log_recur(n / 2);\n for _ in 0..n {\n count += 1;\n }\n return count;\n}\n</code></pre> time_complexity.c<pre><code>/* 선형 로그 차수 */\nint linearLogRecur(int n) {\n if (n &lt;= 1)\n return 1;\n int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n for (int i = 0; i &lt; n; i++) {\n count++;\n }\n return count;\n}\n</code></pre> time_complexity.kt<pre><code>/* 선형 로그 차수 */\nfun linearLogRecur(n: Int): Int {\n if (n &lt;= 1)\n return 1\n var count = linearLogRecur(n / 2) + linearLogRecur(n / 2)\n for (i in 0..&lt;n) {\n count++\n }\n return count\n}\n</code></pre> time_complexity.rb<pre><code>### 선형 로그 차수 ###\ndef linear_log_recur(n)\n return 1 unless n &gt; 1\n\n count = linear_log_recur(n / 2) + linear_log_recur(n / 2)\n (0...n).each { count += 1 }\n\n count\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>아래 그림(2-13)은 선형 로그 시간이 만들어지는 방식을 보여 준다. 이진 트리의 각 층에서 수행하는 전체 연산 횟수는 \\(n\\)이고, 층수는 \\(\\log_2 n + 1\\)이므로 시간 복잡도는 \\(O(n \\log n)\\)이다.</p> <p></p> <p> 그림 2-13   선형 로그 시간 복잡도 </p> <p>퀵 정렬, 병합 정렬, 힙 정렬 등 널리 쓰이는 정렬 알고리즘의 시간 복잡도는 보통 \\(O(n \\log n)\\)이다.</p>","path":["제 2 장   복잡도 분석","2.3   시간 복잡도"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#7-on","level":3,"title":"7.   팩토리얼 시간 \\(O(n!)\\)","text":"<p>팩토리얼 시간은 수학의 ‘모든 순열 구하기’ 문제에 해당한다. 서로 다른 원소 \\(n\\)개가 주어졌을 때 가능한 모든 순열을 구하면, 그 개수는 다음과 같다.</p> \\[ n! = n \\times (n - 1) \\times (n - 2) \\times \\dots \\times 2 \\times 1 \\] <p>팩토리얼은 보통 재귀로 구현한다. 아래 그림(2-14)과 코드에서 첫 번째 층은 \\(n\\)개로, 두 번째 층은 \\(n - 1\\)개로 분기하며, 이를 반복하다가 \\(n\\)번째 층에 도달하면 분기를 멈춘다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py<pre><code>def factorial_recur(n: int) -&gt; int:\n \"\"\"팩토리얼 차수(재귀 구현)\"\"\"\n if n == 0:\n return 1\n count = 0\n # 하나에서 n개로 분열한다\n for _ in range(n):\n count += factorial_recur(n - 1)\n return count\n</code></pre> time_complexity.cpp<pre><code>/* 팩토리얼 차수(재귀 구현) */\nint factorialRecur(int n) {\n if (n == 0)\n return 1;\n int count = 0;\n // 하나에서 n개로 분열한다\n for (int i = 0; i &lt; n; i++) {\n count += factorialRecur(n - 1);\n }\n return count;\n}\n</code></pre> time_complexity.java<pre><code>/* 팩토리얼 차수(재귀 구현) */\nint factorialRecur(int n) {\n if (n == 0)\n return 1;\n int count = 0;\n // 하나에서 n개로 분열한다\n for (int i = 0; i &lt; n; i++) {\n count += factorialRecur(n - 1);\n }\n return count;\n}\n</code></pre> time_complexity.cs<pre><code>/* 팩토리얼 차수(재귀 구현) */\nint FactorialRecur(int n) {\n if (n == 0) return 1;\n int count = 0;\n // 하나에서 n개로 분열한다\n for (int i = 0; i &lt; n; i++) {\n count += FactorialRecur(n - 1);\n }\n return count;\n}\n</code></pre> time_complexity.go<pre><code>/* 팩토리얼 차수(재귀 구현) */\nfunc factorialRecur(n int) int {\n if n == 0 {\n return 1\n }\n count := 0\n // 하나에서 n개로 분열한다\n for i := 0; i &lt; n; i++ {\n count += factorialRecur(n - 1)\n }\n return count\n}\n</code></pre> time_complexity.swift<pre><code>/* 팩토리얼 차수(재귀 구현) */\nfunc factorialRecur(n: Int) -&gt; Int {\n if n == 0 {\n return 1\n }\n var count = 0\n // 하나에서 n개로 분열한다\n for _ in 0 ..&lt; n {\n count += factorialRecur(n: n - 1)\n }\n return count\n}\n</code></pre> time_complexity.js<pre><code>/* 팩토리얼 차수(재귀 구현) */\nfunction factorialRecur(n) {\n if (n === 0) return 1;\n let count = 0;\n // 하나에서 n개로 분열한다\n for (let i = 0; i &lt; n; i++) {\n count += factorialRecur(n - 1);\n }\n return count;\n}\n</code></pre> time_complexity.ts<pre><code>/* 팩토리얼 차수(재귀 구현) */\nfunction factorialRecur(n: number): number {\n if (n === 0) return 1;\n let count = 0;\n // 하나에서 n개로 분열한다\n for (let i = 0; i &lt; n; i++) {\n count += factorialRecur(n - 1);\n }\n return count;\n}\n</code></pre> time_complexity.dart<pre><code>/* 팩토리얼 차수(재귀 구현) */\nint factorialRecur(int n) {\n if (n == 0) return 1;\n int count = 0;\n // 하나에서 n개로 분열한다\n for (var i = 0; i &lt; n; i++) {\n count += factorialRecur(n - 1);\n }\n return count;\n}\n</code></pre> time_complexity.rs<pre><code>/* 팩토리얼 차수(재귀 구현) */\nfn factorial_recur(n: i32) -&gt; i32 {\n if n == 0 {\n return 1;\n }\n let mut count = 0;\n // 하나에서 n개로 분열한다\n for _ in 0..n {\n count += factorial_recur(n - 1);\n }\n count\n}\n</code></pre> time_complexity.c<pre><code>/* 팩토리얼 차수(재귀 구현) */\nint factorialRecur(int n) {\n if (n == 0)\n return 1;\n int count = 0;\n for (int i = 0; i &lt; n; i++) {\n count += factorialRecur(n - 1);\n }\n return count;\n}\n</code></pre> time_complexity.kt<pre><code>/* 팩토리얼 차수(재귀 구현) */\nfun factorialRecur(n: Int): Int {\n if (n == 0)\n return 1\n var count = 0\n // 하나에서 n개로 분열한다\n for (i in 0..&lt;n) {\n count += factorialRecur(n - 1)\n }\n return count\n}\n</code></pre> time_complexity.rb<pre><code>### 팩토리얼 차수(재귀 구현)###\ndef factorial_recur(n)\n return 1 if n == 0\n\n count = 0\n # 하나에서 n개로 분열한다\n (0...n).each { count += factorial_recur(n - 1) }\n\n count\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p></p> <p> 그림 2-14   팩토리얼 시간 복잡도 </p> <p>\\(n \\geq 4\\)이면 항상 \\(n! &gt; 2^n\\)이므로 팩토리얼 시간은 지수 시간보다 더 빠르게 증가한다. 따라서 \\(n\\)이 클 때는 이 역시 감당하기 어렵다.</p>","path":["제 2 장   복잡도 분석","2.3   시간 복잡도"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#235","level":2,"title":"2.3.5   최악, 최선, 평균 시간 복잡도","text":"<p>알고리즘의 시간 효율은 고정되어 있지 않고 입력 데이터의 분포에 따라 달라지는 경우가 많다. 길이가 \\(n\\)인 배열 <code>nums</code>를 입력받는다고 하자. <code>nums</code>에는 \\(1\\)부터 \\(n\\)까지의 수가 한 번씩 들어 있으며, 원소 순서는 무작위로 섞여 있다. 목표는 원소 \\(1\\)의 인덱스를 반환하는 것이다. 여기서 다음과 같은 결론을 얻는다.</p> <ul> <li><code>nums = [?, ?, ..., 1]</code>, 즉 마지막 원소가 \\(1\\)이면 배열 전체를 순회해야 하므로 최악 시간 복잡도 \\(O(n)\\)에 도달한다.</li> <li><code>nums = [1, ?, ?, ...]</code>, 즉 첫 번째 원소가 \\(1\\)이면 배열 길이에 관계없이 더 순회할 필요가 없으므로 최선 시간 복잡도 \\(\\Omega(1)\\)에 도달한다.</li> </ul> <p>‘최악 시간 복잡도’는 함수의 점근적 상한에 해당하며 빅 \\(O\\) 표기법으로 나타낸다. 이에 대응하여 ‘최선 시간 복잡도’는 함수의 점근적 하한에 해당하며 \\(\\Omega\\) 표기법으로 나타낸다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby worst_best_time_complexity.py<pre><code>def random_numbers(n: int) -&gt; list[int]:\n \"\"\"원소가 1, 2, ..., n이며 순서가 섞인 배열 생성\"\"\"\n # 배열 nums =: 1, 2, 3, ..., n을 생성한다\n nums = [i for i in range(1, n + 1)]\n # 배열 원소를 무작위로 섞는다\n random.shuffle(nums)\n return nums\n\ndef find_one(nums: list[int]) -&gt; int:\n \"\"\"배열 nums에서 숫자 1이 있는 인덱스 탐색\"\"\"\n for i in range(len(nums)):\n # 원소 1이 배열 맨 앞에 있으면 최선 시간 복잡도 O(1)에 도달한다\n # 원소 1이 배열 맨 뒤에 있으면 최악 시간 복잡도 O(n)에 도달한다\n if nums[i] == 1:\n return i\n return -1\n</code></pre> worst_best_time_complexity.cpp<pre><code>/* 원소가 { 1, 2, ..., n }이며 순서가 섞인 배열 생성 */\nvector&lt;int&gt; randomNumbers(int n) {\n vector&lt;int&gt; nums(n);\n // 배열 nums = { 1, 2, 3, ..., n }을 생성한다\n for (int i = 0; i &lt; n; i++) {\n nums[i] = i + 1;\n }\n // 시스템 시간으로 난수 시드를 생성한다\n unsigned seed = chrono::system_clock::now().time_since_epoch().count();\n // 배열 원소를 무작위로 섞는다\n shuffle(nums.begin(), nums.end(), default_random_engine(seed));\n return nums;\n}\n\n/* 배열 nums에서 숫자 1이 있는 인덱스 탐색 */\nint findOne(vector&lt;int&gt; &amp;nums) {\n for (int i = 0; i &lt; nums.size(); i++) {\n // 원소 1이 배열 맨 앞에 있으면 최선 시간 복잡도 O(1)에 도달한다\n // 원소 1이 배열 맨 뒤에 있으면 최악 시간 복잡도 O(n)에 도달한다\n if (nums[i] == 1)\n return i;\n }\n return -1;\n}\n</code></pre> worst_best_time_complexity.java<pre><code>/* 원소가 { 1, 2, ..., n }이며 순서가 섞인 배열 생성 */\nint[] randomNumbers(int n) {\n Integer[] nums = new Integer[n];\n // 배열 nums = { 1, 2, 3, ..., n }을 생성한다\n for (int i = 0; i &lt; n; i++) {\n nums[i] = i + 1;\n }\n // 배열 원소를 무작위로 섞는다\n Collections.shuffle(Arrays.asList(nums));\n // Integer[] -&gt; int[]\n int[] res = new int[n];\n for (int i = 0; i &lt; n; i++) {\n res[i] = nums[i];\n }\n return res;\n}\n\n/* 배열 nums에서 숫자 1이 있는 인덱스 탐색 */\nint findOne(int[] nums) {\n for (int i = 0; i &lt; nums.length; i++) {\n // 원소 1이 배열 맨 앞에 있으면 최선 시간 복잡도 O(1)에 도달한다\n // 원소 1이 배열 맨 뒤에 있으면 최악 시간 복잡도 O(n)에 도달한다\n if (nums[i] == 1)\n return i;\n }\n return -1;\n}\n</code></pre> worst_best_time_complexity.cs<pre><code>/* 원소가 { 1, 2, ..., n }이며 순서가 섞인 배열 생성 */\nint[] RandomNumbers(int n) {\n int[] nums = new int[n];\n // 배열 nums = { 1, 2, 3, ..., n }을 생성한다\n for (int i = 0; i &lt; n; i++) {\n nums[i] = i + 1;\n }\n\n // 배열 원소를 무작위로 섞는다\n for (int i = 0; i &lt; nums.Length; i++) {\n int index = new Random().Next(i, nums.Length);\n (nums[i], nums[index]) = (nums[index], nums[i]);\n }\n return nums;\n}\n\n/* 배열 nums에서 숫자 1이 있는 인덱스 탐색 */\nint FindOne(int[] nums) {\n for (int i = 0; i &lt; nums.Length; i++) {\n // 원소 1이 배열 맨 앞에 있으면 최선 시간 복잡도 O(1)에 도달한다\n // 원소 1이 배열 맨 뒤에 있으면 최악 시간 복잡도 O(n)에 도달한다\n if (nums[i] == 1)\n return i;\n }\n return -1;\n}\n</code></pre> worst_best_time_complexity.go<pre><code>/* 원소가 { 1, 2, ..., n }이며 순서가 섞인 배열 생성 */\nfunc randomNumbers(n int) []int {\n nums := make([]int, n)\n // 배열 nums = { 1, 2, 3, ..., n }을 생성한다\n for i := 0; i &lt; n; i++ {\n nums[i] = i + 1\n }\n // 배열 원소를 무작위로 섞는다\n rand.Shuffle(len(nums), func(i, j int) {\n nums[i], nums[j] = nums[j], nums[i]\n })\n return nums\n}\n\n/* 배열 nums에서 숫자 1이 있는 인덱스 탐색 */\nfunc findOne(nums []int) int {\n for i := 0; i &lt; len(nums); i++ {\n // 원소 1이 배열 맨 앞에 있으면 최선 시간 복잡도 O(1)에 도달한다\n // 원소 1이 배열 맨 뒤에 있으면 최악 시간 복잡도 O(n)에 도달한다\n if nums[i] == 1 {\n return i\n }\n }\n return -1\n}\n</code></pre> worst_best_time_complexity.swift<pre><code>/* 원소가 { 1, 2, ..., n }이며 순서가 섞인 배열 생성 */\nfunc randomNumbers(n: Int) -&gt; [Int] {\n // 배열 nums = { 1, 2, 3, ..., n }을 생성한다\n var nums = Array(1 ... n)\n // 배열 원소를 무작위로 섞는다\n nums.shuffle()\n return nums\n}\n\n/* 배열 nums에서 숫자 1이 있는 인덱스 탐색 */\nfunc findOne(nums: [Int]) -&gt; Int {\n for i in nums.indices {\n // 원소 1이 배열 맨 앞에 있으면 최선 시간 복잡도 O(1)에 도달한다\n // 원소 1이 배열 맨 뒤에 있으면 최악 시간 복잡도 O(n)에 도달한다\n if nums[i] == 1 {\n return i\n }\n }\n return -1\n}\n</code></pre> worst_best_time_complexity.js<pre><code>/* 원소가 { 1, 2, ..., n }이며 순서가 섞인 배열 생성 */\nfunction randomNumbers(n) {\n const nums = Array(n);\n // 배열 nums = { 1, 2, 3, ..., n }을 생성한다\n for (let i = 0; i &lt; n; i++) {\n nums[i] = i + 1;\n }\n // 배열 원소를 무작위로 섞는다\n for (let i = 0; i &lt; n; i++) {\n const r = Math.floor(Math.random() * (i + 1));\n const temp = nums[i];\n nums[i] = nums[r];\n nums[r] = temp;\n }\n return nums;\n}\n\n/* 배열 nums에서 숫자 1이 있는 인덱스 탐색 */\nfunction findOne(nums) {\n for (let i = 0; i &lt; nums.length; i++) {\n // 원소 1이 배열 맨 앞에 있으면 최선 시간 복잡도 O(1)에 도달한다\n // 원소 1이 배열 맨 뒤에 있으면 최악 시간 복잡도 O(n)에 도달한다\n if (nums[i] === 1) {\n return i;\n }\n }\n return -1;\n}\n</code></pre> worst_best_time_complexity.ts<pre><code>/* 원소가 { 1, 2, ..., n }이며 순서가 섞인 배열 생성 */\nfunction randomNumbers(n: number): number[] {\n const nums = Array(n);\n // 배열 nums = { 1, 2, 3, ..., n }을 생성한다\n for (let i = 0; i &lt; n; i++) {\n nums[i] = i + 1;\n }\n // 배열 원소를 무작위로 섞는다\n for (let i = 0; i &lt; n; i++) {\n const r = Math.floor(Math.random() * (i + 1));\n const temp = nums[i];\n nums[i] = nums[r];\n nums[r] = temp;\n }\n return nums;\n}\n\n/* 배열 nums에서 숫자 1이 있는 인덱스 탐색 */\nfunction findOne(nums: number[]): number {\n for (let i = 0; i &lt; nums.length; i++) {\n // 원소 1이 배열 맨 앞에 있으면 최선 시간 복잡도 O(1)에 도달한다\n // 원소 1이 배열 맨 뒤에 있으면 최악 시간 복잡도 O(n)에 도달한다\n if (nums[i] === 1) {\n return i;\n }\n }\n return -1;\n}\n</code></pre> worst_best_time_complexity.dart<pre><code>/* 원소가 { 1, 2, ..., n }이며 순서가 섞인 배열 생성 */\nList&lt;int&gt; randomNumbers(int n) {\n final nums = List.filled(n, 0);\n // 배열 nums = { 1, 2, 3, ..., n }을 생성한다\n for (var i = 0; i &lt; n; i++) {\n nums[i] = i + 1;\n }\n // 배열 원소를 무작위로 섞는다\n nums.shuffle();\n\n return nums;\n}\n\n/* 배열 nums에서 숫자 1이 있는 인덱스 탐색 */\nint findOne(List&lt;int&gt; nums) {\n for (var i = 0; i &lt; nums.length; i++) {\n // 원소 1이 배열 맨 앞에 있으면 최선 시간 복잡도 O(1)에 도달한다\n // 원소 1이 배열 맨 뒤에 있으면 최악 시간 복잡도 O(n)에 도달한다\n if (nums[i] == 1) return i;\n }\n\n return -1;\n}\n</code></pre> worst_best_time_complexity.rs<pre><code>/* 원소가 { 1, 2, ..., n }이며 순서가 섞인 배열 생성 */\nfn random_numbers(n: i32) -&gt; Vec&lt;i32&gt; {\n // 배열 nums = { 1, 2, 3, ..., n }을 생성한다\n let mut nums = (1..=n).collect::&lt;Vec&lt;i32&gt;&gt;();\n // 배열 원소를 무작위로 섞는다\n nums.shuffle(&amp;mut thread_rng());\n nums\n}\n\n/* 배열 nums에서 숫자 1이 있는 인덱스 탐색 */\nfn find_one(nums: &amp;[i32]) -&gt; Option&lt;usize&gt; {\n for i in 0..nums.len() {\n // 원소 1이 배열 맨 앞에 있으면 최선 시간 복잡도 O(1)에 도달한다\n // 원소 1이 배열 맨 뒤에 있으면 최악 시간 복잡도 O(n)에 도달한다\n if nums[i] == 1 {\n return Some(i);\n }\n }\n None\n}\n</code></pre> worst_best_time_complexity.c<pre><code>/* 원소가 { 1, 2, ..., n }이며 순서가 섞인 배열 생성 */\nint *randomNumbers(int n) {\n // 힙 메모리를 할당한다(원소 개수가 n이고 원소 타입이 int인 1차원 가변 길이 배열을 생성한다)\n int *nums = (int *)malloc(n * sizeof(int));\n // 배열 nums = { 1, 2, 3, ..., n }을 생성한다\n for (int i = 0; i &lt; n; i++) {\n nums[i] = i + 1;\n }\n // 배열 원소를 무작위로 섞는다\n for (int i = n - 1; i &gt; 0; i--) {\n int j = rand() % (i + 1);\n int temp = nums[i];\n nums[i] = nums[j];\n nums[j] = temp;\n }\n return nums;\n}\n\n/* 배열 nums에서 숫자 1이 있는 인덱스 탐색 */\nint findOne(int *nums, int n) {\n for (int i = 0; i &lt; n; i++) {\n // 원소 1이 배열 맨 앞에 있으면 최선 시간 복잡도 O(1)에 도달한다\n // 원소 1이 배열 맨 뒤에 있으면 최악 시간 복잡도 O(n)에 도달한다\n if (nums[i] == 1)\n return i;\n }\n return -1;\n}\n</code></pre> worst_best_time_complexity.kt<pre><code>/* 원소가 { 1, 2, ..., n }이며 순서가 섞인 배열 생성 */\nfun randomNumbers(n: Int): Array&lt;Int?&gt; {\n val nums = IntArray(n)\n // 배열 nums = { 1, 2, 3, ..., n }을 생성한다\n for (i in 0..&lt;n) {\n nums[i] = i + 1\n }\n // 배열 원소를 무작위로 섞는다\n nums.shuffle()\n val res = arrayOfNulls&lt;Int&gt;(n)\n for (i in 0..&lt;n) {\n res[i] = nums[i]\n }\n return res\n}\n\n/* 배열 nums에서 숫자 1이 있는 인덱스 탐색 */\nfun findOne(nums: Array&lt;Int?&gt;): Int {\n for (i in nums.indices) {\n // 원소 1이 배열 맨 앞에 있으면 최선 시간 복잡도 O(1)에 도달한다\n // 원소 1이 배열 맨 뒤에 있으면 최악 시간 복잡도 O(n)에 도달한다\n if (nums[i] == 1)\n return i\n }\n return -1\n}\n</code></pre> worst_best_time_complexity.rb<pre><code>### 원소가 1, 2, ..., n이며 순서가 섞인 배열 생성 ###\ndef random_numbers(n)\n # 배열 nums =: 1, 2, 3, ..., n을 생성한다\n nums = Array.new(n) { |i| i + 1 }\n # 배열 원소를 무작위로 섞는다\n nums.shuffle!\nend\n\n### 배열 nums에서 숫자 1이 있는 인덱스 탐색 ###\ndef find_one(nums)\n for i in 0...nums.length\n # 원소 1이 배열 맨 앞에 있으면 최선 시간 복잡도 O(1)에 도달한다\n # 원소 1이 배열 맨 뒤에 있으면 최악 시간 복잡도 O(n)에 도달한다\n return i if nums[i] == 1\n end\n\n -1\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>실제로 최선 시간 복잡도는 거의 사용하지 않는다. 보통 최선의 경우가 발생할 확률이 매우 낮아 오해를 줄 수 있기 때문이다. 최악 시간 복잡도는 효율의 안전한 기준을 제시하므로 더 실용적이며, 이를 통해 안심하고 알고리즘을 사용할 수 있다.</p> <p>앞의 예에서 최악 시간 복잡도와 최선 시간 복잡도는 ‘특수한 데이터 분포’에서만 나타난다. 이러한 경우는 발생 확률이 낮을 수 있어 실제 실행 효율을 제대로 반영하지 못한다. 반면 평균 시간 복잡도는 무작위 입력 데이터에서의 알고리즘 실행 효율을 나타내며, \\(\\Theta\\) 표기법으로 표시한다.</p> <p>일부 알고리즘은 무작위 데이터 분포에서의 평균 상황을 간단히 계산할 수 있다. 앞의 예에서는 입력 배열이 섞여 있으므로 원소 \\(1\\)이 어느 인덱스에 있을 확률도 같다. 따라서 평균 반복 횟수는 배열 길이의 절반인 \\(n / 2\\)이며, 평균 시간 복잡도는 \\(\\Theta(n / 2) = \\Theta(n)\\)이다.</p> <p>하지만 더 복잡한 알고리즘에서는 데이터 분포에 따른 전체 기댓값을 분석하기 어려워 평균 시간 복잡도를 구하기 힘든 경우가 많다. 이럴 때는 보통 최악 시간 복잡도를 알고리즘 효율의 평가 기준으로 삼는다.</p> <p>\\(\\Theta\\) 기호는 왜 보기 드문가?</p> <p>\\(O\\) 기호가 워낙 익숙해서인지 평균 시간 복잡도에도 이를 자주 사용한다. 그러나 엄밀하게는 올바른 표기 방식이 아니다. 이 책이나 다른 자료에서 ‘평균 시간 복잡도 \\(O(n)\\)’ 같은 표현을 만나면 \\(\\Theta(n)\\)으로 이해하면 된다.</p>","path":["제 2 장   복잡도 분석","2.3   시간 복잡도"],"tags":[]},{"location":"chapter_data_structure/","level":1,"title":"제 3 장   자료 구조","text":"<p>이 장을 시작하며</p> <p>자료 구조는 튼튼하면서도 다양한 형태를 지닌 뼈대와 같다.</p> <p>데이터를 질서 있게 조직하는 설계도를 제공하며, 알고리즘은 그 토대 위에서 생동감을 얻는다.</p>","path":["제 3 장   자료 구조"],"tags":[]},{"location":"chapter_data_structure/#_1","level":2,"title":"이 장의 내용","text":"<ul> <li>3.1   자료 구조의 분류</li> <li>3.2   기본 자료형</li> <li>3.3   수의 인코딩 *</li> <li>3.4   문자 인코딩 *</li> <li>3.5   요약</li> <li>3.6   연습 문제</li> </ul>","path":["제 3 장   자료 구조"],"tags":[]},{"location":"chapter_data_structure/basic_data_types/","level":1,"title":"3.2   기본 자료형","text":"<p>컴퓨터의 데이터라고 하면 텍스트, 이미지, 동영상, 음성, 3D 모델 등 다양한 형태가 떠오른다. 이 데이터들은 조직된 형태는 서로 다르지만, 모두 여러 기본 자료형으로 이루어져 있다.</p> <p>기본 자료형은 CPU가 직접 연산할 수 있는 자료형으로, 알고리즘에서 직접 사용된다. 주로 다음과 같은 종류가 있다.</p> <ul> <li>정수형 <code>byte</code>, <code>short</code>, <code>int</code>, <code>long</code>.</li> <li>소수를 표현하는 부동 소수점형 <code>float</code>, <code>double</code>.</li> <li>여러 언어의 글자, 문장 부호, 나아가 이모지 등을 표현하는 문자형 <code>char</code>.</li> <li>‘예’와 ‘아니요’를 판단하는 불리언형 <code>bool</code>.</li> </ul> <p>기본 자료형은 컴퓨터에 이진수 형태로 저장된다. 이진수 한 자리가 \\(1\\)비트다. 현대 운영체제의 대다수에서 \\(1\\)바이트(byte)는 \\(8\\)비트(bit)로 이루어진다.</p> <p>기본 자료형의 값 범위는 차지하는 공간의 크기에 따라 결정된다. Java를 예로 살펴보자.</p> <ul> <li>정수형 <code>byte</code>는 \\(1\\)바이트 = \\(8\\)비트를 차지하며, \\(2^{8}\\)개의 수를 표현할 수 있다.</li> <li>정수형 <code>int</code>는 \\(4\\)바이트 = \\(32\\)비트를 차지하며, \\(2^{32}\\)개의 수를 표현할 수 있다.</li> </ul> <p>아래 표(3-1)는 Java의 여러 기본 자료형이 차지하는 공간, 값 범위, 기본값을 나열한다. 표를 외울 필요는 없으며, 대략 이해한 뒤 필요할 때 찾아보면 된다.</p> <p> 표 3-1   기본 자료형의 공간 사용량과 값 범위 </p> 자료형 기호 공간 사용량 최솟값 최댓값 기본값 정수 <code>byte</code> 1바이트 \\(-2^7\\) (\\(-128\\)) \\(2^7 - 1\\) (\\(127\\)) \\(0\\) <code>short</code> 2바이트 \\(-2^{15}\\) \\(2^{15} - 1\\) \\(0\\) <code>int</code> 4바이트 \\(-2^{31}\\) \\(2^{31} - 1\\) \\(0\\) <code>long</code> 8바이트 \\(-2^{63}\\) \\(2^{63} - 1\\) \\(0\\) 부동 소수점 <code>float</code> 4바이트 \\(1.175 \\times 10^{-38}\\) \\(3.403 \\times 10^{38}\\) \\(0.0\\text{f}\\) <code>double</code> 8바이트 \\(2.225 \\times 10^{-308}\\) \\(1.798 \\times 10^{308}\\) \\(0.0\\) 문자 <code>char</code> 2바이트 \\(0\\) \\(2^{16} - 1\\) \\(0\\) 불리언 <code>bool</code> 1바이트 \\(\\text{false}\\) \\(\\text{true}\\) \\(\\text{false}\\) <p>위 표(3-1)는 Java의 기본 자료형을 기준으로 한다. 프로그래밍 언어마다 자료형을 정의하는 방식이 있으므로, 공간 사용량과 값 범위, 기본값은 다를 수 있다.</p> <ul> <li>Python의 정수형 <code>int</code>는 사용 가능한 메모리만 충분하면 크기에 제한이 없다. 부동 소수점형 <code>float</code>는 64비트 배정밀도다. <code>char</code> 자료형은 없으며, 문자 하나는 실제로 길이가 1인 문자열 <code>str</code>이다.</li> <li>C와 C++는 기본 자료형의 크기를 명확히 규정하지 않으므로 구현과 플랫폼에 따라 다르다. 위 표(3-1)는 Linux, macOS 등의 64비트 Unix 운영체제에서 사용하는 LP64 데이터 모델을 따른다.</li> <li>문자형 <code>char</code>의 크기는 C와 C++에서 1바이트이며, 대부분의 프로그래밍 언어에서는 사용하는 문자 인코딩 방식에 따라 정해진다. 자세한 내용은 ‘문자 인코딩’ 절에서 설명한다.</li> <li>불리언 값을 표현하는 데는 1비트(\\(0\\) 또는 \\(1\\))면 충분하지만, 메모리에서는 보통 1바이트로 저장한다. 현대 컴퓨터 CPU가 대개 1바이트를 주소 지정이 가능한 최소 메모리 단위로 사용하기 때문이다.</li> </ul> <p>그렇다면 기본 자료형과 자료 구조는 어떤 관계가 있을까? 자료 구조란 컴퓨터에서 데이터를 조직하고 저장하는 방식이다. 이 문장의 핵심은 ‘데이터’보다 ‘구조’에 있다.</p> <p>‘한 줄로 늘어선 수’를 표현하려면 자연스럽게 배열을 떠올린다. 배열의 선형 구조가 수 사이의 인접 관계와 순서 관계를 표현할 수 있기 때문이다. 그러나 저장할 내용이 정수 <code>int</code>인지, 소수 <code>float</code>인지, 문자 <code>char</code>인지는 ‘자료 구조’와 무관하다.</p> <p>즉, 기본 자료형은 데이터의 ‘내용 유형’을 제공하고, 자료 구조는 데이터의 ‘조직 방식’을 제공한다. 다음 코드는 같은 자료 구조인 배열을 사용하여 <code>int</code>, <code>float</code>, <code>char</code>, <code>bool</code> 등 서로 다른 기본 자료형을 저장하고 표현한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby <pre><code># 여러 기본 자료형으로 배열 초기화\nnumbers: list[int] = [0] * 5\ndecimals: list[float] = [0.0] * 5\n# Python의 문자는 실제로 길이가 1인 문자열이다\ncharacters: list[str] = ['0'] * 5\nbools: list[bool] = [False] * 5\n# Python의 리스트는 여러 기본 자료형과 객체 참조를 자유롭게 저장할 수 있다\ndata = [0, 0.0, 'a', False, ListNode(0)]\n</code></pre> <pre><code>// 여러 기본 자료형으로 배열 초기화\nint numbers[5];\nfloat decimals[5];\nchar characters[5];\nbool bools[5];\n</code></pre> <pre><code>// 여러 기본 자료형으로 배열 초기화\nint[] numbers = new int[5];\nfloat[] decimals = new float[5];\nchar[] characters = new char[5];\nboolean[] bools = new boolean[5];\n</code></pre> <pre><code>// 여러 기본 자료형으로 배열 초기화\nint[] numbers = new int[5];\nfloat[] decimals = new float[5];\nchar[] characters = new char[5];\nbool[] bools = new bool[5];\n</code></pre> <pre><code>// 여러 기본 자료형으로 배열 초기화\nvar numbers = [5]int{}\nvar decimals = [5]float64{}\nvar characters = [5]byte{}\nvar bools = [5]bool{}\n</code></pre> <pre><code>// 여러 기본 자료형으로 배열 초기화\nlet numbers = Array(repeating: 0, count: 5)\nlet decimals = Array(repeating: 0.0, count: 5)\nlet characters: [Character] = Array(repeating: \"a\", count: 5)\nlet bools = Array(repeating: false, count: 5)\n</code></pre> <pre><code>// JavaScript의 배열은 여러 기본 자료형과 객체를 자유롭게 저장할 수 있다\nconst array = [0, 0.0, 'a', false];\n</code></pre> <pre><code>// 여러 기본 자료형으로 배열 초기화\nconst numbers: number[] = [];\nconst characters: string[] = [];\nconst bools: boolean[] = [];\n</code></pre> <pre><code>// 여러 기본 자료형으로 배열 초기화\nList&lt;int&gt; numbers = List.filled(5, 0);\nList&lt;double&gt; decimals = List.filled(5, 0.0);\nList&lt;String&gt; characters = List.filled(5, 'a');\nList&lt;bool&gt; bools = List.filled(5, false);\n</code></pre> <pre><code>// 여러 기본 자료형으로 배열 초기화\nlet numbers: Vec&lt;i32&gt; = vec![0; 5];\nlet decimals: Vec&lt;f32&gt; = vec![0.0; 5];\nlet characters: Vec&lt;char&gt; = vec!['0'; 5];\nlet bools: Vec&lt;bool&gt; = vec![false; 5];\n</code></pre> <pre><code>// 여러 기본 자료형으로 배열 초기화\nint numbers[10];\nfloat decimals[10];\nchar characters[10];\nbool bools[10];\n</code></pre> <pre><code>// 여러 기본 자료형으로 배열 초기화\nval numbers = IntArray(5)\nval decinals = FloatArray(5)\nval characters = CharArray(5)\nval bools = BooleanArray(5)\n</code></pre> <pre><code># Ruby의 리스트는 여러 기본 자료형과 객체 참조를 자유롭게 저장할 수 있다\ndata = [0, 0.0, 'a', false, ListNode(0)]\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 3 장   자료 구조","3.2   기본 자료형"],"tags":[]},{"location":"chapter_data_structure/character_encoding/","level":1,"title":"3.4   문자 인코딩 *","text":"<p>컴퓨터에서는 모든 데이터를 이진수 형태로 저장하며, 문자 <code>char</code>도 예외가 아니다. 문자를 표현하려면 각 문자와 이진수 사이의 일대일 대응 관계를 정한 ‘문자 집합’이 필요하다. 문자 집합이 있으면 컴퓨터는 대응표를 찾아 이진수를 문자로 바꿀 수 있다.</p>","path":["제 3 장   자료 구조","3.4   문자 인코딩 *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#341-ascii","level":2,"title":"3.4.1   ASCII 문자 집합","text":"<p>ASCII 코드는 가장 초기에 등장한 문자 집합으로, 정식 명칭은 American Standard Code for Information Interchange(미국 정보 교환 표준 부호)다. 7비트 이진수(한 바이트의 하위 7비트)로 문자 하나를 표현하여 최대 128개의 서로 다른 문자를 나타낸다. 아래 그림(3-6)과 같이 영문 대소문자, 숫자 0 ~ 9, 일부 문장 부호와 제어 문자(줄바꿈, 탭 등)를 포함한다.</p> <p></p> <p> 그림 3-6   ASCII 코드 </p> <p>그러나 ASCII 코드는 영어만 표현할 수 있다. 컴퓨터가 전 세계로 보급되면서 더 많은 언어를 표현할 수 있는 EASCII 문자 집합이 등장했다. ASCII의 7비트를 8비트로 확장하여 서로 다른 문자 256개를 표현한다.</p> <p>이후 세계 여러 지역에 맞는 EASCII 문자 집합이 잇따라 등장했다. 이 문자 집합들은 앞의 128개 문자는 모두 ASCII 코드로 통일하고, 뒤의 128개 문자는 각 언어의 필요에 맞게 서로 다르게 정의했다.</p>","path":["제 3 장   자료 구조","3.4   문자 인코딩 *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#342-gbk","level":2,"title":"3.4.2   GBK 문자 집합","text":"<p>하지만 EASCII 코드도 여러 언어에 필요한 문자 수를 충족하지 못했다. 예를 들어 한자는 거의 10만 자에 이르며, 일상에서 사용하는 것만 해도 수천 자다. 중국 국가표준총국은 1980년에 한자 6763자를 수록한 GB2312 문자 집합을 발표하여 컴퓨터의 한자 처리에 필요한 기본적인 요구를 충족했다.</p> <p>그러나 GB2312는 일부 드문 한자와 번체자를 처리할 수 없었다. GBK 문자 집합은 GB2312를 확장한 것으로 총 21886자의 한자를 수록한다. GBK 인코딩에서는 ASCII 문자를 1바이트로, 한자를 2바이트로 표현한다.</p>","path":["제 3 장   자료 구조","3.4   문자 인코딩 *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#343-unicode","level":2,"title":"3.4.3   Unicode 문자 집합","text":"<p>컴퓨터 기술이 빠르게 발전하면서 문자 집합과 인코딩 표준이 다양하게 등장했지만, 이는 여러 문제를 낳았다. 한편으로는 문자 집합이 대개 특정 언어의 문자만 정의하여 다국어 환경에서 제대로 작동하지 못했다. 다른 한편으로는 같은 언어에도 여러 문자 집합 표준이 있어, 두 컴퓨터의 인코딩 표준이 다르면 정보를 주고받을 때 글자가 깨졌다.</p> <p>당시 연구자들은 이렇게 생각했다. 세계의 모든 언어와 기호를 담을 만큼 완전한 문자 집합을 만들면 다국어 환경과 글자 깨짐 문제를 해결할 수 있지 않을까? 이러한 생각에서 크고 포괄적인 문자 집합 Unicode가 탄생했다.</p> <p>Unicode는 중국어로 ‘통일 부호’라는 뜻의 이름으로 불리며, 이론적으로 100만 개 이상의 문자를 수용할 수 있다. 전 세계 문자를 하나의 문자 집합에 통합하고, 여러 언어의 문자를 처리하고 표시하는 공통 문자 집합을 제공하여 인코딩 표준의 차이로 발생하는 글자 깨짐을 줄이는 것이 목표다. 1991년 발표된 뒤로 새로운 언어와 문자가 계속 추가되었다. 2022년 9월 기준으로 Unicode에는 여러 언어의 문자, 기호, 이모지 등을 포함하여 149186개의 문자가 들어 있다.</p> <p>Unicode는 범용 문자 집합으로서 각 문자에 고유한 ‘코드 포인트’(문자 번호)를 부여한다. 범위는 U+0000부터 U+10FFFF까지이며, 하나로 통일된 문자 번호 공간을 이룬다. 그러나 Unicode는 이러한 문자 코드 포인트를 컴퓨터에 어떻게 저장할지는 정하지 않는다. 길이가 다른 Unicode 코드 포인트들이 한 텍스트에 함께 나타나면 시스템은 문자를 어떻게 해석할까? 예를 들어 길이가 2바이트인 인코딩이 주어졌을 때, 2바이트 문자 하나인지 1바이트 문자 두 개인지 어떻게 판단할까?</p> <p>이 문제의 직접적인 해결책은 모든 문자를 같은 길이로 인코딩하여 저장하는 것이다. 아래 그림(3-7)에서 ‘Hello’의 각 문자는 1바이트, ‘算法’의 각 문자는 2바이트를 차지한다. 상위 비트에 0을 채우면 ‘Hello 算法’의 모든 문자를 2바이트 길이로 인코딩할 수 있다. 그러면 시스템이 2바이트마다 문자 하나를 해석하여 원래 문구를 복원할 수 있다.</p> <p></p> <p> 그림 3-7   Unicode 인코딩 예 </p> <p>하지만 ASCII 코드에서 보았듯이 영어 문자를 인코딩하는 데는 1바이트면 충분하다. 위 방식을 사용하면 영어 텍스트가 ASCII 인코딩의 두 배 공간을 차지하여 메모리를 크게 낭비한다. 따라서 더 효율적인 Unicode 인코딩 방법이 필요하다.</p>","path":["제 3 장   자료 구조","3.4   문자 인코딩 *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#344-utf-8","level":2,"title":"3.4.4   UTF-8 인코딩","text":"<p>현재 UTF-8은 세계에서 가장 널리 쓰이는 Unicode 인코딩 방식이다. 가변 길이 인코딩으로, 문자의 복잡성에 따라 1~4바이트로 문자 하나를 표현한다. ASCII 문자는 1바이트, 라틴 문자와 그리스 문자는 2바이트, 자주 쓰는 중국어 문자는 3바이트, 일부 드문 문자는 4바이트가 필요하다.</p> <p>UTF-8 인코딩 규칙은 복잡하지 않으며, 다음 두 경우로 나뉜다.</p> <ul> <li>길이가 1바이트인 문자는 최상위 비트를 \\(0\\)으로 두고 나머지 7비트에 Unicode 코드 포인트를 넣는다. ASCII 문자는 Unicode 문자 집합의 처음 128개 코드 포인트를 차지한다. 즉, UTF-8 인코딩은 ASCII 코드와 하위 호환된다. 따라서 오래된 ASCII 텍스트도 UTF-8로 해석할 수 있다.</li> <li>길이가 \\(n\\)바이트인 문자(\\(n &gt; 1\\))는 첫 바이트의 상위 \\(n\\)비트를 모두 \\(1\\)로, \\(n + 1\\)번째 비트를 \\(0\\)으로 둔다. 두 번째 바이트부터는 각 바이트의 상위 2비트를 \\(10\\)으로 두고, 나머지 모든 비트에 문자의 Unicode 코드 포인트를 채운다.</li> </ul> <p>아래 그림(3-8)은 ‘Hello算法’의 UTF-8 인코딩을 보여 준다. 상위 \\(n\\)비트가 모두 \\(1\\)이므로 시스템은 맨 앞에서 연속된 \\(1\\)의 개수를 읽어 문자 길이가 \\(n\\)임을 알 수 있다.</p> <p>그렇다면 나머지 바이트의 상위 2비트를 모두 \\(10\\)으로 두는 이유는 무엇일까? 이 \\(10\\)은 검사 표식 역할을 한다. 시스템이 잘못된 바이트에서 텍스트를 해석하기 시작했을 때, 바이트 앞부분의 \\(10\\)을 통해 이상을 빠르게 감지할 수 있다.</p> <p>\\(10\\)을 검사 표식으로 쓰는 이유는 UTF-8 규칙상 문자 인코딩의 맨 앞 두 비트가 \\(10\\)일 수 없기 때문이다. 이는 귀류법으로 증명할 수 있다. 문자 인코딩의 맨 앞 두 비트가 \\(10\\)이라고 가정하면, 길이가 \\(1\\)바이트인 ASCII 문자라는 뜻이다. 하지만 ASCII 코드의 최상위 비트는 \\(0\\)이어야 하므로 가정과 모순된다.</p> <p></p> <p> 그림 3-8   UTF-8 인코딩 예 </p> <p>UTF-8 외에도 다음 두 인코딩 방식이 널리 쓰인다.</p> <ul> <li>UTF-16 인코딩: 2바이트 또는 4바이트로 문자 하나를 표현한다. 모든 ASCII 문자와 자주 쓰는 비영어권 문자는 2바이트로 표현하고, 일부 문자는 4바이트가 필요하다. 2바이트 문자는 UTF-16 인코딩 값이 Unicode 코드 포인트와 같다.</li> <li>UTF-32 인코딩: 모든 문자에 4바이트를 사용한다. 따라서 UTF-8이나 UTF-16보다 공간을 더 많이 차지하며, 특히 ASCII 문자의 비중이 큰 텍스트에서 차이가 크다.</li> </ul> <p>저장 공간 관점에서 UTF-8은 영어 문자를 1바이트만으로 표현하므로 매우 효율적이다. 반면 중국어 같은 일부 비영어권 문자는 UTF-16으로 인코딩하는 것이 더 효율적이다. UTF-16은 2바이트면 되지만 UTF-8은 3바이트가 필요할 수 있기 때문이다.</p> <p>호환성 관점에서는 UTF-8의 범용성이 가장 뛰어나며, 많은 도구와 라이브러리가 UTF-8을 우선 지원한다.</p>","path":["제 3 장   자료 구조","3.4   문자 인코딩 *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#345","level":2,"title":"3.4.5   프로그래밍 언어의 문자 인코딩","text":"<p>과거의 대다수 프로그래밍 언어에서는 실행 중인 프로그램의 문자열에 UTF-16이나 UTF-32 같은 고정 길이 인코딩을 사용했다. 고정 길이 인코딩에서는 문자열을 배열처럼 다룰 수 있어 다음과 같은 장점이 있다.</p> <ul> <li>임의 접근: UTF-16 문자열은 임의 접근이 쉽다. UTF-8은 가변 길이 인코딩이므로 \\(i\\)번째 문자를 찾으려면 문자열의 처음부터 \\(i\\)번째 문자까지 순회해야 하며, \\(O(n)\\) 시간이 필요하다.</li> <li>문자 수 계산: 임의 접근과 마찬가지로 UTF-16 문자열의 길이 계산도 \\(O(1)\\) 연산이다. 반면 UTF-8 문자열의 길이를 계산하려면 문자열 전체를 순회해야 한다.</li> <li>문자열 연산: UTF-16 문자열에서는 분할, 연결, 삽입, 삭제 등 여러 문자열 연산을 더 쉽게 수행할 수 있다. UTF-8 문자열에서는 잘못된 UTF-8 인코딩이 생기지 않도록 보장하기 위해 보통 추가 계산이 필요하다.</li> </ul> <p>프로그래밍 언어의 문자 인코딩 설계는 여러 요소가 관련된 흥미로운 주제다.</p> <ul> <li>Java의 <code>String</code>은 UTF-16 인코딩을 사용하며 각 문자는 2바이트를 차지한다. Java를 처음 설계할 당시에는 16비트로 가능한 모든 문자를 표현할 수 있다고 생각했기 때문이다. 그러나 이는 잘못된 판단이었다. 이후 Unicode 규격이 16비트를 넘어 확장되면서, 현재 Java의 문자는 16비트 값 두 개로 이루어진 ‘서로게이트 쌍’으로 표현될 수 있다.</li> <li>JavaScript와 TypeScript의 문자열이 UTF-16을 사용하는 이유도 Java와 비슷하다. Netscape가 1995년에 JavaScript를 처음 발표했을 때 Unicode는 초기 발전 단계였으며, 당시에는 16비트 인코딩으로 모든 Unicode 문자를 표현할 수 있었다.</li> <li>C#이 UTF-16을 사용하는 주된 이유는 .NET 플랫폼을 Microsoft가 설계했으며, Windows 운영체제를 비롯한 Microsoft의 여러 기술에서 UTF-16을 널리 사용하기 때문이다.</li> </ul> <p>이 언어들은 문자 수를 과소평가했기 때문에 16비트를 넘는 Unicode 문자를 표현하려고 ‘서로게이트 쌍’을 도입할 수밖에 없었다. 이는 불가피한 선택이었다. 한편으로는 서로게이트 쌍이 포함된 문자열에서 문자 하나가 2바이트 또는 4바이트를 차지하여 고정 길이 인코딩의 장점을 잃는다. 다른 한편으로는 서로게이트 쌍을 처리하는 코드가 더 필요해 프로그래밍이 복잡해지고 디버깅도 어려워진다.</p> <p>이러한 이유로 일부 프로그래밍 언어는 다른 인코딩 방식을 채택했다.</p> <ul> <li>Python의 <code>str</code>은 Unicode 인코딩과 유연한 문자열 표현을 사용한다. 저장되는 문자 길이는 문자열에서 가장 큰 Unicode 코드 포인트에 따라 정해진다. 모든 문자가 ASCII이면 문자마다 1바이트를 사용한다. ASCII 범위를 넘는 문자가 있지만 모두 기본 다국어 평면(BMP)에 속하면 문자마다 2바이트를 사용한다. BMP를 벗어나는 문자가 있으면 문자마다 4바이트를 사용한다.</li> <li>Go의 <code>string</code>은 내부적으로 UTF-8 인코딩을 사용한다. Go는 Unicode 코드 포인트 하나를 나타내는 <code>rune</code> 자료형도 제공한다.</li> <li>Rust의 <code>str</code>과 <code>String</code>은 내부적으로 UTF-8 인코딩을 사용한다. Rust도 Unicode 코드 포인트 하나를 나타내는 <code>char</code> 자료형을 제공한다.</li> </ul> <p>위 내용은 프로그래밍 언어 안에서 문자열을 저장하는 방식에 관한 것이다. 이는 문자열을 파일에 저장하거나 네트워크로 전송하는 방식과는 별개의 문제다. 파일 저장이나 네트워크 전송에서는 보통 호환성과 공간 효율을 높이기 위해 문자열을 UTF-8 형식으로 인코딩한다.</p>","path":["제 3 장   자료 구조","3.4   문자 인코딩 *"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/","level":1,"title":"3.1   자료 구조의 분류","text":"<p>대표적인 자료 구조로는 배열, 연결 리스트, 스택, 큐, 해시 테이블, 트리, 힙, 그래프가 있다. 이들은 ‘논리적 구조’와 ‘물리적 구조’라는 두 관점에서 분류할 수 있다.</p>","path":["제 3 장   자료 구조","3.1   자료 구조의 분류"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#311","level":2,"title":"3.1.1   논리적 구조: 선형과 비선형","text":"<p>논리적 구조는 데이터 원소 사이의 논리적 관계를 드러낸다. 배열과 연결 리스트에서는 데이터가 일정한 순서로 배열되어 데이터 사이의 선형 관계를 나타낸다. 트리에서는 데이터가 위에서 아래로 계층을 이루어 ‘조상’과 ‘자손’ 사이의 파생 관계를 나타낸다. 그래프는 노드와 간선으로 이루어져 복잡한 네트워크 관계를 표현한다.</p> <p>아래 그림(3-1)과 같이 논리적 구조는 크게 ‘선형’과 ‘비선형’으로 나뉜다. 선형 구조는 데이터가 논리적으로 일렬로 배열되어 있어 비교적 직관적이다. 비선형 구조는 이와 달리 일렬로 배열되지 않는다.</p> <ul> <li>선형 자료 구조: 배열, 연결 리스트, 스택, 큐, 해시 테이블이 해당하며, 원소 사이에 일대일의 순서 관계가 있다.</li> <li>비선형 자료 구조: 트리, 힙, 그래프, 해시 테이블이 해당한다.</li> </ul> <p>비선형 자료 구조는 다시 트리 구조와 네트워크 구조로 나뉜다.</p> <ul> <li>트리 구조: 트리, 힙, 해시 테이블이 해당하며, 원소 사이에 일대다 관계가 있다.</li> <li>네트워크 구조: 그래프가 해당하며, 원소 사이에 다대다 관계가 있다.</li> </ul> <p></p> <p> 그림 3-1   선형 자료 구조와 비선형 자료 구조 </p>","path":["제 3 장   자료 구조","3.1   자료 구조의 분류"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#312","level":2,"title":"3.1.2   물리적 구조: 연속과 분산","text":"<p>알고리즘 프로그램이 실행될 때 처리 중인 데이터는 주로 메모리에 저장된다. 아래 그림(3-2)은 컴퓨터 메모리 모듈을 보여 주며, 검은색 칩마다 메모리 공간이 들어 있다. 메모리는 각 셀에 일정한 크기의 데이터를 저장할 수 있는 거대한 Excel 표라고 생각할 수 있다.</p> <p>시스템은 메모리 주소를 통해 원하는 위치의 데이터에 접근한다. 아래 그림(3-2)과 같이 컴퓨터는 특정 규칙에 따라 표의 각 셀에 번호를 부여하여 모든 메모리 공간이 고유한 메모리 주소를 갖게 한다. 프로그램은 이 주소를 이용해 메모리의 데이터에 접근한다.</p> <p></p> <p> 그림 3-2   메모리 모듈, 메모리 공간, 메모리 주소 </p> <p>참고</p> <p>메모리를 Excel 표에 빗댄 것은 단순화한 비유다. 실제 메모리의 작동 방식은 더 복잡하며 주소 공간, 메모리 관리, 캐시, 가상 메모리, 물리 메모리 등의 개념이 관련된다.</p> <p>메모리는 모든 프로그램이 공유하는 자원이다. 어떤 메모리 영역을 한 프로그램이 사용하면 보통 다른 프로그램은 동시에 사용할 수 없다. 따라서 자료 구조와 알고리즘을 설계할 때 메모리 자원은 중요한 고려 사항이다. 예를 들어 알고리즘의 최대 메모리 사용량은 시스템의 남은 메모리를 넘지 않아야 한다. 연속된 큰 메모리 공간이 부족하다면 분산된 메모리 공간에 저장할 수 있는 자료 구조를 선택해야 한다.</p> <p>아래 그림(3-3)과 같이 물리적 구조는 데이터가 컴퓨터 메모리에 저장되는 방식을 나타내며, 연속 공간 저장(배열)과 분산 공간 저장(연결 리스트)으로 나뉜다. 물리적 구조는 데이터 접근, 갱신, 삽입, 삭제 등의 연산 방식을 근본적으로 결정한다. 두 물리적 구조는 시간 효율과 공간 효율에서 서로 보완적인 특징을 지닌다.</p> <p></p> <p> 그림 3-3   연속 공간 저장과 분산 공간 저장 </p> <p>모든 자료 구조는 배열, 연결 리스트 또는 둘의 조합으로 구현된다는 점도 알아 두자. 예를 들어 스택과 큐는 배열이나 연결 리스트로 구현할 수 있으며, 해시 테이블 구현에는 배열과 연결 리스트가 함께 포함될 수 있다.</p> <ul> <li>배열로 구현할 수 있는 것: 스택, 큐, 해시 테이블, 트리, 힙, 그래프, 행렬, 텐서(차원이 \\(\\geq 3\\)인 배열) 등.</li> <li>연결 리스트로 구현할 수 있는 것: 스택, 큐, 해시 테이블, 트리, 힙, 그래프 등.</li> </ul> <p>연결 리스트는 초기화한 뒤에도 프로그램 실행 중에 길이를 조정할 수 있으므로 ‘동적 자료 구조’라고도 한다. 배열은 초기화한 뒤 길이를 바꿀 수 없으므로 ‘정적 자료 구조’라고도 한다. 다만 배열도 메모리를 다시 할당하면 길이를 바꿀 수 있어 어느 정도 ‘동적 특성’을 가질 수 있다.</p> <p>참고</p> <p>물리적 구조를 이해하기 어렵다면 다음 장을 먼저 읽은 뒤 이 절을 복습하는 것을 권한다.</p>","path":["제 3 장   자료 구조","3.1   자료 구조의 분류"],"tags":[]},{"location":"chapter_data_structure/exercises/","level":1,"title":"3.6   연습 문제","text":"","path":["제 3 장   자료 구조","3.6   연습 문제"],"tags":[]},{"location":"chapter_data_structure/exercises/#361","level":2,"title":"3.6.1   개념 확인","text":"","path":["제 3 장   자료 구조","3.6   연습 문제"],"tags":[]},{"location":"chapter_data_structure/exercises/#1","level":3,"title":"1.   일상 속 데이터의 관계","text":"<p>데이터 사이의 관계를 바탕으로 다음 세 상황에 대해 ‘선형 구조, 트리 구조, 네트워크 구조’ 중 하나를 고르고 이유를 설명하라.</p> <ol> <li>학생들이 한 줄로 서 있고, 각자 바로 앞과 뒤에 있는 사람만 신경 쓴다.</li> <li>학교를 ‘학교 → 학년 → 학급’의 계층으로 나누어 관리한다.</li> <li>도시의 도로가 여러 교차로를 연결한다. 한 교차로에서 여러 다른 교차로로 갈 수 있으며 순환하는 경로도 생길 수 있다.</li> </ol> 참고 답안 <ol> <li> <p>선형 구조다. 맨 앞과 맨 뒤에 선 사람을 제외하면 각 사람은 앞뒤로 한 사람씩만 이웃하며, 관계가 하나의 선을 따라 이어진다.</p> </li> <li> <p>트리 구조다. 각 학급은 한 학년에 속하고 각 학년은 학교에 속하므로, 관계가 위에서 아래로 계층적으로 펼쳐진다.</p> </li> <li> <p>네트워크 구조다. 한 교차로가 여러 교차로와 연결될 수 있고 경로들이 순환을 이룰 수도 있으므로, 하나의 순서나 엄격한 계층으로 나열할 수 없다.</p> <p>구조를 판단할 때는 메모리에서 차지하는 공간보다 원소 사이의 관계를 먼저 살펴보아야 한다.</p> </li> </ol>","path":["제 3 장   자료 구조","3.6   연습 문제"],"tags":[]},{"location":"chapter_data_structure/exercises/#2","level":3,"title":"2.   논리적 순서를 메모리에 저장하는 방법","text":"<p>논리적 순서 <code>A → B → C</code>를 저장하기 위해 다음과 같이 단순화한 두 메모리 배치 방식을 생각해 보자.</p> <ul> <li>방법 가: <code>A, B, C</code>를 각각 번호가 <code>20, 21, 22</code>인 메모리 칸에 저장한다.</li> <li>방법 나: <code>A, B, C</code>를 각각 번호가 <code>20, 7, 31</code>인 메모리 칸에 저장하고, <code>A</code>에 <code>B</code>의 위치를, <code>B</code>에 <code>C</code>의 위치를 기록한다.</li> </ul> <ol> <li>어느 방법이 연속 공간 저장이고, 어느 방법이 분산 공간 저장인가?</li> <li>두 방법은 각각 배열과 연결 리스트 중 어느 쪽에 더 가까운가?</li> <li>방법 나는 메모리 칸의 번호가 크기순으로 정렬되어 있지 않은데, 어떻게 <code>A → B → C</code>의 논리적 순서를 나타낼 수 있는가?</li> </ol> 참고 답안 <ol> <li> <p>방법 가는 연속된 메모리 칸을 사용하므로 연속 공간 저장이다. 방법 나는 노드가 여러 위치에 흩어져 있으므로 분산 공간 저장이다.</p> </li> <li> <p>방법 가는 배열에, 방법 나는 연결 리스트에 더 가깝다.</p> </li> <li> <p>논리적 순서는 메모리 칸 번호의 크기가 아니라 노드 사이에 기록된 연결 관계로 결정된다. <code>A</code>에 기록된 위치에서 <code>B</code>를 찾고, 다시 <code>B</code>에 기록된 위치에서 <code>C</code>를 찾을 수 있으므로 <code>A, B, C</code>에 차례대로 접근할 수 있다.</p> <p>이는 논리적 구조와 물리적 구조가 같은 데이터 집합을 바라보는 서로 다른 두 관점임을 보여 준다.</p> </li> </ol>","path":["제 3 장   자료 구조","3.6   연습 문제"],"tags":[]},{"location":"chapter_data_structure/exercises/#3","level":3,"title":"3.   과제 기록의 자료형과 구조","text":"<p>한 학습 모임에서 학생 4명의 과제 제출 여부를 좌석 순서대로 기록한 결과가 다음과 같다.</p> <p><code>[true, false, true, true]</code></p> <ol> <li>각 원소에는 어떤 기본 자료형이 적합한가?</li> <li>이 4개 원소를 좌석 순서대로 한 줄에 나열한 것은 어떤 논리적 구조인가?</li> <li>나중에 각 학생의 과제 점수 <code>[90, 0, 85, 100]</code>를 기록하도록 바꾼다면, 달라지는 것은 데이터의 ‘내용 유형’인가, ‘조직 방식’인가? 이유를 설명하라.</li> </ol> 참고 답안 <ol> <li> <p>각 원소는 ‘예’ 또는 ‘아니요’만 나타내므로 불리언 자료형 <code>bool</code>이 적합하다.</p> </li> <li> <p>원소들이 좌석 순서대로 나열되어 선형 구조를 이루며, 배열에 저장할 수 있다.</p> </li> <li> <p>달라지는 것은 내용 유형이다. 원소가 불리언 값에서 정수로 바뀐다. 조직 방식은 바뀌지 않는다. 데이터는 여전히 좌석 순서대로 한 줄에 나열되어 있으므로 배열이라는 선형 구조를 사용할 수 있다.</p> <p>기본 자료형은 ‘무엇을 저장하는가’를, 자료 구조는 ‘데이터를 어떻게 구성하는가’를 설명한다.</p> </li> </ol>","path":["제 3 장   자료 구조","3.6   연습 문제"],"tags":[]},{"location":"chapter_data_structure/exercises/#362","level":2,"title":"3.6.2   프로그래밍 연습","text":"","path":["제 3 장   자료 구조","3.6   연습 문제"],"tags":[]},{"location":"chapter_data_structure/exercises/#1-1","level":3,"title":"1.   이진수 표현에서 1의 개수 세기","text":"<p>음이 아닌 정수 <code>n</code>이 주어질 때, 이진수 표현에 포함된 1의 개수를 구하라.</p> <p>비트 연산을 사용해야 하며, 이진수 표현을 문자열로 변환하거나 1의 개수를 바로 세어 주는 내장 함수를 사용해서는 안 된다.</p> 풀이 힌트 <ol> <li>n &amp; 1로 n의 가장 오른쪽 비트를 얻어 그 비트가 1인지 확인할 수 있다.</li> <li>오른쪽으로 한 비트 이동하면 현재 가장 오른쪽 비트가 제거된다. 대부분의 언어에서는 &gt;&gt; 연산자를 사용한다.</li> <li>비트를 하나씩 확인하고 오른쪽으로 이동하는 방법을 완성한 뒤, n &amp; (n - 1)이 n의 가장 오른쪽 1을 0으로 바꾼다는 점도 살펴보자.</li> </ol> <p>LeetCode</p>","path":["제 3 장   자료 구조","3.6   연습 문제"],"tags":[]},{"location":"chapter_data_structure/number_encoding/","level":1,"title":"3.3   수의 인코딩 *","text":"<p>참고</p> <p>이 책에서 제목에 * 기호가 있는 절은 선택 학습 내용이다. 시간이 부족하거나 이해하기 어렵다면 먼저 건너뛰고, 필수 내용을 학습한 뒤 따로 도전해도 좋다.</p>","path":["제 3 장   자료 구조","3.3   수의 인코딩 *"],"tags":[]},{"location":"chapter_data_structure/number_encoding/#331-1-2","level":2,"title":"3.3.1   부호와 크기, 1의 보수, 2의 보수","text":"<p>앞 절의 표에서 모든 정수형은 표현할 수 있는 음수가 양수보다 하나 더 많았다. 예를 들어 <code>byte</code>의 값 범위는 \\([-128, 127]\\)이다. 직관과 어긋나는 이 현상의 이유를 이해하려면 부호와 크기, 1의 보수, 2의 보수에 대해 알아야 한다.</p> <p>먼저 수는 컴퓨터에 ‘2의 보수’ 형태로 저장된다는 점을 알아 두자. 그 이유를 분석하기 전에 세 가지 표현을 정의하겠다.</p> <ul> <li>부호와 크기: 이진수 표현의 최상위 비트를 부호 비트로 사용한다. \\(0\\)은 양수, \\(1\\)은 음수를 나타내며 나머지 비트는 수의 크기를 나타낸다.</li> <li>1의 보수: 양수의 1의 보수는 부호와 크기 표현과 같다. 음수의 1의 보수는 부호와 크기 표현에서 부호 비트를 제외한 모든 비트를 반전한 것이다.</li> <li>2의 보수: 양수의 2의 보수는 부호와 크기 표현과 같다. 음수의 2의 보수는 1의 보수에 \\(1\\)을 더한 것이다.</li> </ul> <p>아래 그림(3-4)은 부호와 크기, 1의 보수, 2의 보수를 서로 변환하는 방법을 보여 준다.</p> <p></p> <p> 그림 3-4   부호와 크기, 1의 보수, 2의 보수의 상호 변환 </p> <p>부호와 크기(sign-magnitude) 표현은 가장 직관적이지만 한계가 있다. 우선 음수의 부호와 크기 표현은 연산에 그대로 사용할 수 없다. 예를 들어 이 표현으로 \\(1 + (-2)\\)를 계산하면 결과가 \\(-3\\)이 되어 명백히 틀린다.</p> \\[ \\begin{aligned} &amp; 1 + (-2) \\newline &amp; \\rightarrow 0000 \\; 0001 + 1000 \\; 0010 \\newline &amp; = 1000 \\; 0011 \\newline &amp; \\rightarrow -3 \\end{aligned} \\] <p>이 문제를 해결하기 위해 1의 보수(1's complement)가 도입되었다. 부호와 크기 표현을 먼저 1의 보수로 바꾸고 \\(1 + (-2)\\)를 계산한 뒤, 결과를 다시 부호와 크기 표현으로 바꾸면 올바른 결과인 \\(-1\\)을 얻는다.</p> \\[ \\begin{aligned} &amp; 1 + (-2) \\newline &amp; \\rightarrow 0000 \\; 0001 \\; \\text{(부호와 크기)} + 1000 \\; 0010 \\; \\text{(부호와 크기)} \\newline &amp; = 0000 \\; 0001 \\; \\text{(1의 보수)} + 1111 \\; 1101 \\; \\text{(1의 보수)} \\newline &amp; = 1111 \\; 1110 \\; \\text{(1의 보수)} \\newline &amp; = 1000 \\; 0001 \\; \\text{(부호와 크기)} \\newline &amp; \\rightarrow -1 \\end{aligned} \\] <p>또한 부호와 크기 표현에서는 0을 \\(+0\\)과 \\(-0\\)의 두 가지 방식으로 나타낸다. 즉, 0에 서로 다른 두 이진 인코딩이 대응하므로 모호함이 생길 수 있다. 예를 들어 조건을 판단할 때 양의 0과 음의 0을 구분하지 않으면 결과가 틀릴 수 있다. 이러한 모호함을 처리하려면 추가 판단 연산이 필요하므로 컴퓨터의 연산 효율이 낮아질 수 있다.</p> \\[ \\begin{aligned} +0 &amp; \\rightarrow 0000 \\; 0000 \\newline -0 &amp; \\rightarrow 1000 \\; 0000 \\end{aligned} \\] <p>1의 보수에도 부호와 크기 표현처럼 양의 0과 음의 0의 모호함이 있다. 그래서 2의 보수(2's complement)가 도입되었다. 먼저 음의 0을 부호와 크기, 1의 보수, 2의 보수로 변환하는 과정을 살펴보자.</p> \\[ \\begin{aligned} -0 \\rightarrow \\; &amp; 1000 \\; 0000 \\; \\text{(부호와 크기)} \\newline = \\; &amp; 1111 \\; 1111 \\; \\text{(1의 보수)} \\newline = 1 \\; &amp; 0000 \\; 0000 \\; \\text{(2의 보수)} \\newline \\end{aligned} \\] <p>음의 0의 1의 보수에 \\(1\\)을 더하면 자리 올림이 발생한다. 그러나 <code>byte</code>는 길이가 8비트뿐이므로 9번째 비트로 넘친 \\(1\\)은 버려진다. 즉, 음의 0의 2의 보수는 \\(0000 \\; 0000\\)으로, 양의 0의 2의 보수와 같다. 따라서 2의 보수 표현에는 0이 하나만 존재하며, 양의 0과 음의 0의 모호함이 해결된다.</p> <p>마지막 의문이 남는다. <code>byte</code>의 값 범위는 \\([-128, 127]\\)인데, 하나 더 있는 음수 \\(-128\\)은 어떻게 얻을까? 구간 \\([-127, +127]\\)의 모든 정수에는 대응하는 부호와 크기, 1의 보수, 2의 보수 표현이 있으며, 부호와 크기 표현과 2의 보수는 서로 변환할 수 있다.</p> <p>그러나 2의 보수 \\(1000 \\; 0000\\)은 예외로, 대응하는 부호와 크기 표현이 없다. 변환 방법을 적용하면 부호와 크기 표현으로 \\(0000 \\; 0000\\)을 얻는다. 하지만 이 표현은 수 \\(0\\)을 나타내며 2의 보수도 자기 자신이어야 하므로 모순이다. 컴퓨터에서는 이 특수한 2의 보수 \\(1000 \\; 0000\\)이 \\(-128\\)을 나타내도록 정한다. 실제로 \\((-1) + (-127)\\)을 2의 보수로 계산한 결과는 \\(-128\\)이다.</p> \\[ \\begin{aligned} &amp; (-127) + (-1) \\newline &amp; \\rightarrow 1111 \\; 1111 \\; \\text{(부호와 크기)} + 1000 \\; 0001 \\; \\text{(부호와 크기)} \\newline &amp; = 1000 \\; 0000 \\; \\text{(1의 보수)} + 1111 \\; 1110 \\; \\text{(1의 보수)} \\newline &amp; = 1000 \\; 0001 \\; \\text{(2의 보수)} + 1111 \\; 1111 \\; \\text{(2의 보수)} \\newline &amp; = 1000 \\; 0000 \\; \\text{(2의 보수)} \\newline &amp; \\rightarrow -128 \\end{aligned} \\] <p>위의 계산이 모두 덧셈이라는 사실을 발견했을 것이다. 여기에는 중요한 사실이 담겨 있다. 컴퓨터 내부의 하드웨어 회로는 주로 덧셈 연산을 기반으로 설계된다. 덧셈은 곱셈, 나눗셈, 뺄셈 같은 다른 연산보다 하드웨어로 구현하기 쉽고, 병렬 처리도 쉬우며, 연산 속도도 빠르기 때문이다.</p> <p>그렇다고 컴퓨터가 덧셈만 할 수 있다는 뜻은 아니다. 덧셈과 몇 가지 기본 논리 연산을 결합하면 다양한 수학 연산을 구현할 수 있다. 예를 들어 뺄셈 \\(a - b\\)는 덧셈 \\(a + (-b)\\)로 바꿀 수 있고, 곱셈과 나눗셈도 여러 번의 덧셈이나 뺄셈으로 바꿀 수 있다.</p> <p>이제 컴퓨터가 2의 보수를 사용하는 이유를 정리할 수 있다. 2의 보수 표현을 쓰면 같은 회로와 연산으로 양수와 음수의 덧셈을 처리할 수 있다. 뺄셈을 위한 별도 하드웨어 회로를 설계할 필요가 없으며, 양의 0과 음의 0의 모호함도 따로 처리하지 않아도 된다. 이는 하드웨어 설계를 크게 단순화하고 연산 효율을 높인다.</p> <p>2의 보수는 매우 정교하게 설계된 방식이지만, 분량상 여기까지만 소개한다. 관심 있는 독자는 더 깊이 알아보기를 권한다.</p>","path":["제 3 장   자료 구조","3.3   수의 인코딩 *"],"tags":[]},{"location":"chapter_data_structure/number_encoding/#332","level":2,"title":"3.3.2   부동 소수점 인코딩","text":"<p>자세히 살펴보면 이런 의문이 생긴다. <code>int</code>와 <code>float</code>는 모두 4바이트로 길이가 같은데, 왜 <code>float</code>의 값 범위가 <code>int</code>보다 훨씬 넓을까? <code>float</code>는 소수도 표현해야 하므로 오히려 값 범위가 줄어들 것 같아 직관과 어긋난다.</p> <p>실제로 이는 부동 소수점형 <code>float</code>가 다른 표현 방식을 사용하기 때문이다. 길이가 32비트인 이진수를 다음과 같이 쓰자.</p> \\[ b_{31} b_{30} b_{29} \\ldots b_2 b_1 b_0 \\] <p>IEEE 754 표준에 따르면 32비트 <code>float</code>는 다음 세 부분으로 이루어진다.</p> <ul> <li>부호 비트 \\(\\mathrm{S}\\): 1비트를 차지하며 \\(b_{31}\\)에 해당한다.</li> <li>지수 비트 \\(\\mathrm{E}\\): 8비트를 차지하며 \\(b_{30} b_{29} \\ldots b_{23}\\)에 해당한다.</li> <li>가수의 소수부 비트 \\(\\mathrm{N}\\): 23비트를 차지하며 \\(b_{22} b_{21} \\ldots b_0\\)에 해당한다.</li> </ul> <p>이진수 <code>float</code>가 나타내는 값을 계산하는 방법은 다음과 같다.</p> \\[ \\text {val} = (-1)^{b_{31}} \\times 2^{\\left(b_{30} b_{29} \\ldots b_{23}\\right)_2-127} \\times\\left(1 . b_{22} b_{21} \\ldots b_0\\right)_2 \\] <p>이를 십진수로 바꾸면 다음 식을 얻는다.</p> \\[ \\text {val}=(-1)^{\\mathrm{S}} \\times 2^{\\mathrm{E} -127} \\times (1 + \\mathrm{N}) \\] <p>각 항의 값 범위는 다음과 같다.</p> \\[ \\begin{aligned} \\mathrm{S} \\in &amp; \\{ 0, 1\\}, \\quad \\mathrm{E} \\in \\{ 1, 2, \\dots, 254 \\} \\newline (1 + \\mathrm{N}) = &amp; (1 + \\sum_{i=1}^{23} b_{23-i} 2^{-i}) \\subset [1, 2 - 2^{-23}] \\end{aligned} \\] <p></p> <p> 그림 3-5   IEEE 754 표준에 따른 float 계산 예 </p> <p>위 그림(3-5)에서 예로 든 데이터가 \\(\\mathrm{S} = 0\\), \\(\\mathrm{E} = 124\\), \\(\\mathrm{N} = 2^{-2} + 2^{-3} = 0.375\\)이면 다음과 같다.</p> \\[ \\text { val } = (-1)^0 \\times 2^{124 - 127} \\times (1 + 0.375) = 0.171875 \\] <p>이제 처음의 질문에 답할 수 있다. <code>float</code>의 표현 방식에는 지수 비트가 있으므로 값 범위가 <code>int</code>보다 훨씬 넓다. 위 계산에 따르면 <code>float</code>로 표현할 수 있는 가장 큰 양수는 \\(2^{254 - 127} \\times (2 - 2^{-23}) \\approx 3.4 \\times 10^{38}\\)이며, 부호 비트를 바꾸면 가장 작은 음수를 얻는다.</p> <p>부동 소수점형 <code>float</code>는 값 범위를 넓히는 대신 정밀도를 희생한다. 정수형 <code>int</code>는 32비트 전체로 수를 표현하므로 수들이 균일하게 분포한다. 반면 부동 소수점형 <code>float</code>는 지수 비트 때문에 값이 커질수록 인접한 두 수 사이의 간격도 커지는 경향이 있다.</p> <p>아래 표(3-2)와 같이 지수 비트 \\(\\mathrm{E} = 0\\)과 \\(\\mathrm{E} = 255\\)는 특별한 의미를 가지며, 0, 무한대, \\(\\mathrm{NaN}\\) 등을 표현하는 데 사용된다.</p> <p> 표 3-2   지수 비트의 의미 </p> 지수 비트 E 소수부 \\(\\mathrm{N} = 0\\) 소수부 \\(\\mathrm{N} \\ne 0\\) 계산식 \\(0\\) \\(\\pm 0\\) 비정규화 수 \\((-1)^{\\mathrm{S}} \\times 2^{-126} \\times (0.\\mathrm{N})\\) \\(1, 2, \\dots, 254\\) 정규화 수 정규화 수 \\((-1)^{\\mathrm{S}} \\times 2^{(\\mathrm{E} -127)} \\times (1.\\mathrm{N})\\) \\(255\\) \\(\\pm \\infty\\) \\(\\mathrm{NaN}\\) <p>비정규화 수는 부동 소수점의 정밀도를 크게 높인다는 점도 알아 두자. 가장 작은 양의 정규화 수는 \\(2^{-126}\\)이고, 가장 작은 양의 비정규화 수는 \\(2^{-126} \\times 2^{-23}\\)이다.</p> <p>배정밀도 <code>double</code>도 <code>float</code>와 비슷한 표현 방식을 사용하므로 여기서는 자세한 설명을 생략한다.</p>","path":["제 3 장   자료 구조","3.3   수의 인코딩 *"],"tags":[]},{"location":"chapter_data_structure/summary/","level":1,"title":"3.5   요약","text":"","path":["제 3 장   자료 구조","3.5   요약"],"tags":[]},{"location":"chapter_data_structure/summary/#1","level":3,"title":"1.   핵심 정리","text":"<ul> <li>자료 구조는 논리적 구조와 물리적 구조라는 두 관점에서 분류할 수 있다. 논리적 구조는 데이터 원소 사이의 논리적 관계를 설명하고, 물리적 구조는 컴퓨터 메모리에 데이터를 저장하는 방식을 설명한다.</li> <li>대표적인 논리적 구조에는 선형, 트리형, 네트워크형 등이 있다. 보통 논리적 구조에 따라 자료 구조를 선형(배열, 연결 리스트, 스택, 큐)과 비선형(트리, 그래프, 힙)으로 나눈다. 해시 테이블 구현에는 선형과 비선형 자료 구조가 함께 포함될 수 있다.</li> <li>프로그램이 실행되면 데이터는 컴퓨터 메모리에 저장된다. 각 메모리 공간에는 대응하는 메모리 주소가 있으며, 프로그램은 이 주소로 데이터에 접근한다.</li> <li>물리적 구조는 주로 연속 공간 저장(배열)과 분산 공간 저장(연결 리스트)으로 나뉜다. 모든 자료 구조는 배열, 연결 리스트 또는 둘의 조합으로 구현된다.</li> <li>컴퓨터의 기본 자료형에는 정수형 <code>byte</code>, <code>short</code>, <code>int</code>, <code>long</code>, 부동 소수점형 <code>float</code>, <code>double</code>, 문자형 <code>char</code>, 불리언형 <code>bool</code>이 있다. 값 범위는 차지하는 공간의 크기와 표현 방식에 따라 결정된다.</li> <li>부호와 크기, 1의 보수, 2의 보수는 컴퓨터에서 수를 인코딩하는 세 가지 방식으로, 서로 변환할 수 있다. 정수의 부호와 크기 표현에서 최상위 비트는 부호 비트이고, 나머지 비트는 수의 크기를 나타낸다.</li> <li>정수는 컴퓨터에 2의 보수 형태로 저장된다. 이 표현에서는 양수와 음수의 덧셈을 똑같이 처리할 수 있어 뺄셈만을 위한 별도 하드웨어 회로가 필요 없으며, 양의 0과 음의 0의 모호함도 없다.</li> <li>부동 소수점 인코딩은 부호 1비트, 지수 8비트, 가수의 소수부 23비트로 이루어진다. 지수 비트 덕분에 정수보다 훨씬 넓은 값 범위를 갖는 대신 정밀도를 희생한다.</li> <li>ASCII 코드는 가장 초기에 등장한 영어 문자 집합으로, 길이가 1바이트이며 128개의 문자를 수록한다. GBK는 널리 쓰이는 중국어 문자 집합으로 2만 자 이상의 한자를 수록한다. Unicode는 전 세계 여러 언어의 문자를 담은 완전한 문자 집합 표준을 제공하여 인코딩 방식의 차이로 발생하는 글자 깨짐을 해결하고자 한다.</li> <li>UTF-8은 가장 널리 쓰이는 Unicode 인코딩 방식으로 범용성이 매우 좋다. 확장성이 뛰어난 가변 길이 인코딩이며 저장 공간을 효율적으로 활용한다. UTF-16과 UTF-32는 고정 길이 인코딩 방식이다. 중국어를 인코딩할 때 UTF-16은 UTF-8보다 공간을 적게 차지한다. Java, C# 등의 언어는 기본적으로 UTF-16을 사용한다.</li> </ul>","path":["제 3 장   자료 구조","3.5   요약"],"tags":[]},{"location":"chapter_data_structure/summary/#2-q-a","level":3,"title":"2.   Q &amp; A","text":"<p>Q: 해시 테이블에는 왜 선형 자료 구조와 비선형 자료 구조가 함께 포함되는가?</p> <p>해시 테이블의 기반은 배열이며, 해시 충돌을 해결하기 위해 ‘체이닝’을 사용할 수 있다(뒤의 ‘해시 충돌’ 절에서 다룬다). 배열의 각 버킷은 연결 리스트를 가리키며, 연결 리스트 길이가 일정 기준을 넘으면 트리(보통 레드-블랙 트리)로 바뀔 수 있다.</p> <p>저장 관점에서 해시 테이블의 기반은 배열이고, 각 버킷의 슬롯에는 값 하나, 연결 리스트 또는 트리가 들어갈 수 있다. 따라서 해시 테이블에는 선형 자료 구조(배열, 연결 리스트)와 비선형 자료 구조(트리)가 함께 포함될 수 있다.</p> <p>Q: <code>char</code> 자료형의 길이는 1바이트인가?</p> <p><code>char</code>의 길이는 프로그래밍 언어가 사용하는 인코딩 방식에 따라 정해진다. 예를 들어 Java, JavaScript, TypeScript, C#은 모두 UTF-16 인코딩으로 Unicode 코드 포인트를 저장하므로 <code>char</code>의 길이는 2바이트다.</p> <p>Q: 배열로 구현한 자료 구조를 ‘정적 자료 구조’라고 부르면 모호하지 않은가? 스택에서도 원소를 꺼내거나 넣을 수 있는데, 이런 연산은 모두 ‘동적’이다.</p> <p>스택은 동적으로 데이터를 조작할 수 있지만 자료 구조는 여전히 ‘정적’이다. 즉, 길이를 바꿀 수 없다. 배열 기반 자료 구조는 원소를 동적으로 추가하거나 삭제할 수 있지만 용량은 고정되어 있다. 데이터 양이 미리 할당한 크기를 넘으면 더 큰 배열을 새로 만들고 기존 내용을 복사해야 한다.</p> <p>Q: 스택이나 큐를 만들 때 크기를 지정하지 않았는데, 왜 ‘정적 자료 구조’인가?</p> <p>고급 프로그래밍 언어에서는 스택이나 큐의 초기 용량을 직접 지정하지 않아도 클래스 내부에서 자동으로 처리한다. 예를 들어 Java <code>ArrayList</code>의 초기 용량은 보통 10이다. 용량 확장도 자동으로 이루어진다. 자세한 내용은 뒤의 ‘리스트’ 절에서 설명한다.</p> <p>Q: 부호와 크기 표현을 2의 보수로 바꾸려면 ‘반전한 뒤 1을 더한다’. 그렇다면 반대 변환은 역연산인 ‘1을 뺀 뒤 반전하기’여야 할 텐데, ‘반전한 뒤 1을 더하기’로도 가능한 이유는 무엇인가?</p> <p>두 표현의 상호 변환은 실제로 ‘보수’를 구하는 과정이기 때문이다. 먼저 보수를 정의하자. \\(a + b = c\\)라면 \\(a\\)를 \\(c\\)에 대한 \\(b\\)의 보수라 하고, 반대로 \\(b\\)도 \\(c\\)에 대한 \\(a\\)의 보수라고 한다.</p> <p>\\(n = 4\\)비트 이진수 \\(0010\\)이 주어졌다고 하자. 이를 부호와 크기 표현으로 보면(부호 비트는 고려하지 않는다), 2의 보수는 ‘반전한 뒤 1을 더해’ 얻는다.</p> \\[ 0010 \\rightarrow 1101 \\rightarrow 1110 \\] <p>원래 표현과 2의 보수의 합은 \\(0010 + 1110 = 10000\\)이다. 즉, 2의 보수 \\(1110\\)은 \\(10000\\)에 대한 원래 수 \\(0010\\)의 ‘보수’다. 따라서 ‘반전한 뒤 1을 더하기’는 실제로 \\(10000\\)에 대한 보수를 계산하는 과정이다.</p> <p>그렇다면 \\(10000\\)에 대한 2의 보수 \\(1110\\)의 ‘보수’는 무엇일까? 똑같이 ‘반전한 뒤 1을 더해서’ 얻을 수 있다.</p> \\[ 1110 \\rightarrow 0001 \\rightarrow 0010 \\] <p>즉, 원래 표현과 2의 보수는 \\(10000\\)에 대해 서로의 ‘보수’다. 따라서 두 방향의 변환에 같은 연산인 ‘반전한 뒤 1을 더하기’를 사용할 수 있다.</p> <p>물론 역연산인 ‘1을 뺀 뒤 반전하기’로도 2의 보수 \\(1110\\)에서 원래 표현을 구할 수 있다.</p> \\[ 1110 \\rightarrow 1101 \\rightarrow 0010 \\] <p>정리하면 ‘반전한 뒤 1을 더하기’와 ‘1을 뺀 뒤 반전하기’는 모두 \\(10000\\)에 대한 보수를 구하는 연산이므로 서로 동등하다.</p> <p>본질적으로 ‘반전’은 \\(1111\\)에 대한 보수를 구하는 연산이다(항상 <code>부호와 크기 표현 + 1의 보수 = 1111</code>이 성립하기 때문이다). 여기에 1을 더해서 얻은 2의 보수는 \\(10000\\)에 대한 보수다.</p> <p>위 설명은 \\(n = 4\\)를 예로 들었지만, 자릿수가 임의의 값인 이진수에도 일반화할 수 있다.</p>","path":["제 3 장   자료 구조","3.5   요약"],"tags":[]},{"location":"chapter_divide_and_conquer/","level":1,"title":"제 12 장   분할 정복","text":"<p>이 장을 시작하며</p> <p>어려운 문제도 한 단계씩 나눌 때마다 더욱 단순해진다.</p> <p>분할 정복은 중요한 사실을 알려 준다. 쉬운 것부터 시작하면 모든 것이 더 이상 복잡하지 않다.</p>","path":["제 12 장   분할 정복"],"tags":[]},{"location":"chapter_divide_and_conquer/#_1","level":2,"title":"이 장의 내용","text":"<ul> <li>12.1   분할 정복 알고리즘</li> <li>12.2   분할 정복 탐색 전략</li> <li>12.3   이진 트리 구성 문제</li> <li>12.4   하노이의 탑 문제</li> <li>12.5   요약</li> <li>12.6   연습 문제</li> </ul>","path":["제 12 장   분할 정복"],"tags":[]},{"location":"chapter_divide_and_conquer/binary_search_recur/","level":1,"title":"12.2   분할 정복 탐색 전략","text":"<p>앞에서 배웠듯이 탐색 알고리즘은 크게 두 종류로 나뉜다.</p> <ul> <li>완전 탐색: 자료 구조를 순회하며, 시간 복잡도는 \\(O(n)\\)이다.</li> <li>적응형 탐색: 데이터 고유의 구성 방식이나 사전 정보를 이용하며, 시간 복잡도가 \\(O(\\log n)\\) 또는 \\(O(1)\\)에 이를 수 있다.</li> </ul> <p>실제로 시간 복잡도가 \\(O(\\log n)\\)인 탐색 알고리즘은 보통 분할 정복 전략을 기반으로 구현된다. 이진 탐색과 트리가 그 예다.</p> <ul> <li>이진 탐색은 각 단계에서 원래 문제(배열에서 목표 원소 찾기)를 더 작은 문제(배열의 절반에서 목표 원소 찾기)로 나눈다. 배열이 비거나 목표 원소를 찾을 때까지 계속한다.</li> <li>트리는 분할 정복 사고의 대표적인 예다. 이진 탐색 트리, AVL 트리, 힙 등의 자료 구조에서 여러 연산의 시간 복잡도는 \\(O(\\log n)\\)이다.</li> </ul> <p>이진 탐색의 분할 정복 전략은 다음과 같다.</p> <ul> <li>문제를 나눌 수 있다: 중간 원소와 목표 원소를 비교하여 원래 문제(배열에서 찾기)를 부분 문제(배열의 절반에서 찾기)로 재귀적으로 나눈다.</li> <li>부분 문제가 독립적이다: 이진 탐색은 매번 부분 문제 하나만 처리하며 다른 부분 문제의 영향을 받지 않는다.</li> <li>부분 문제의 해를 합칠 필요가 없다: 특정 원소 하나를 찾는 것이 목적이므로 부분 문제의 해를 합칠 필요가 없다. 부분 문제를 해결하면 원래 문제도 함께 해결된다.</li> </ul> <p>분할 정복이 탐색 효율을 높이는 근본적인 이유는 완전 탐색이 매번 선택지 하나만 제외하는 데 비해 분할 정복 탐색은 매번 선택지의 절반을 제외할 수 있기 때문이다.</p>","path":["제 12 장   분할 정복","12.2   분할 정복 탐색 전략"],"tags":[]},{"location":"chapter_divide_and_conquer/binary_search_recur/#1","level":3,"title":"1.   분할 정복으로 이진 탐색 구현","text":"<p>앞 장에서는 이진 탐색을 반복 방식으로 구현했다. 이제 분할 정복(재귀)으로 구현해 보자.</p> <p>문제</p> <p>길이가 \\(n\\)이고 모든 원소가 서로 다른 정렬된 배열 <code>nums</code>가 주어진다. 원소 <code>target</code>을 찾아라.</p> <p>분할 정복의 관점에서 탐색 구간 \\([i, j]\\)에 해당하는 부분 문제를 \\(f(i, j)\\)라고 하자.</p> <p>원래 문제 \\(f(0, n-1)\\)에서 시작하여 다음 단계로 이진 탐색을 수행한다.</p> <ol> <li>탐색 구간 \\([i, j]\\)의 중간 인덱스 \\(m\\)을 계산하고, 이를 기준으로 탐색 구간의 절반을 제외한다.</li> <li>크기가 절반으로 줄어든 부분 문제 \\(f(i, m-1)\\) 또는 \\(f(m+1, j)\\)를 재귀적으로 푼다.</li> <li><code>target</code>을 찾거나 구간이 빌 때까지 <code>1.</code> 단계와 <code>2.</code> 단계를 반복한 뒤 반환한다.</li> </ol> <p>아래 그림(12-4)은 배열에서 원소 \\(6\\)을 이진 탐색하는 분할 정복 과정을 보여 준다.</p> <p></p> <p> 그림 12-4   이진 탐색의 분할 정복 과정 </p> <p>구현 코드에서는 문제 \\(f(i, j)\\)를 푸는 재귀 함수 <code>dfs()</code>를 정의한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_recur.py<pre><code>def dfs(nums: list[int], target: int, i: int, j: int) -&gt; int:\n \"\"\"이진 탐색: 문제 f(i, j)\"\"\"\n # 구간이 비어 있으면 목표 원소가 없다는 뜻이므로 -1을 반환한다\n if i &gt; j:\n return -1\n # 중간 인덱스 m을 계산한다\n m = (i + j) // 2\n if nums[m] &lt; target:\n # 부분 문제 f(m+1, j)를 재귀적으로 푼다\n return dfs(nums, target, m + 1, j)\n elif nums[m] &gt; target:\n # 부분 문제 f(i, m-1)를 재귀적으로 푼다\n return dfs(nums, target, i, m - 1)\n else:\n # 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m\n\ndef binary_search(nums: list[int], target: int) -&gt; int:\n \"\"\"이진 탐색\"\"\"\n n = len(nums)\n # 문제 f(0, n-1)을 푼다\n return dfs(nums, target, 0, n - 1)\n</code></pre> binary_search_recur.cpp<pre><code>/* 이진 탐색: 문제 f(i, j) */\nint dfs(vector&lt;int&gt; &amp;nums, int target, int i, int j) {\n // 구간이 비어 있으면 목표 원소가 없다는 뜻이므로 -1을 반환한다\n if (i &gt; j) {\n return -1;\n }\n // 중간 인덱스 m을 계산한다\n int m = (i + j) / 2;\n if (nums[m] &lt; target) {\n // 부분 문제 f(m+1, j)를 재귀적으로 푼다\n return dfs(nums, target, m + 1, j);\n } else if (nums[m] &gt; target) {\n // 부분 문제 f(i, m-1)를 재귀적으로 푼다\n return dfs(nums, target, i, m - 1);\n } else {\n // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m;\n }\n}\n\n/* 이진 탐색 */\nint binarySearch(vector&lt;int&gt; &amp;nums, int target) {\n int n = nums.size();\n // 문제 f(0, n-1)을 푼다\n return dfs(nums, target, 0, n - 1);\n}\n</code></pre> binary_search_recur.java<pre><code>/* 이진 탐색: 문제 f(i, j) */\nint dfs(int[] nums, int target, int i, int j) {\n // 구간이 비어 있으면 목표 원소가 없다는 뜻이므로 -1을 반환한다\n if (i &gt; j) {\n return -1;\n }\n // 중간 인덱스 m을 계산한다\n int m = (i + j) / 2;\n if (nums[m] &lt; target) {\n // 부분 문제 f(m+1, j)를 재귀적으로 푼다\n return dfs(nums, target, m + 1, j);\n } else if (nums[m] &gt; target) {\n // 부분 문제 f(i, m-1)를 재귀적으로 푼다\n return dfs(nums, target, i, m - 1);\n } else {\n // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m;\n }\n}\n\n/* 이진 탐색 */\nint binarySearch(int[] nums, int target) {\n int n = nums.length;\n // 문제 f(0, n-1)을 푼다\n return dfs(nums, target, 0, n - 1);\n}\n</code></pre> binary_search_recur.cs<pre><code>/* 이진 탐색: 문제 f(i, j) */\nint DFS(int[] nums, int target, int i, int j) {\n // 구간이 비어 있으면 목표 원소가 없다는 뜻이므로 -1을 반환한다\n if (i &gt; j) {\n return -1;\n }\n // 중간 인덱스 m을 계산한다\n int m = (i + j) / 2;\n if (nums[m] &lt; target) {\n // 부분 문제 f(m+1, j)를 재귀적으로 푼다\n return DFS(nums, target, m + 1, j);\n } else if (nums[m] &gt; target) {\n // 부분 문제 f(i, m-1)를 재귀적으로 푼다\n return DFS(nums, target, i, m - 1);\n } else {\n // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m;\n }\n}\n\n/* 이진 탐색 */\nint BinarySearch(int[] nums, int target) {\n int n = nums.Length;\n // 문제 f(0, n-1)을 푼다\n return DFS(nums, target, 0, n - 1);\n}\n</code></pre> binary_search_recur.go<pre><code>/* 이진 탐색: 문제 f(i, j) */\nfunc dfs(nums []int, target, i, j int) int {\n // 구간이 비어 있으면 목표 원소가 없다는 뜻이므로 -1을 반환한다\n if i &gt; j {\n return -1\n }\n // 중간 인덱스를 계산한다\n m := i + ((j - i) &gt;&gt; 1)\n //중간 원소와 목표 원소의 크기를 비교한다\n if nums[m] &lt; target {\n // 중간 원소가 더 작으면 배열의 오른쪽 절반을 재귀적으로 탐색한다\n // 부분 문제 f(m+1, j)를 재귀적으로 푼다\n return dfs(nums, target, m+1, j)\n } else if nums[m] &gt; target {\n // 중간 원소가 더 크면 배열의 왼쪽 절반을 재귀적으로 탐색한다\n // 부분 문제 f(i, m-1)를 재귀적으로 푼다\n return dfs(nums, target, i, m-1)\n } else {\n // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m\n }\n}\n\n/* 이진 탐색 */\nfunc binarySearch(nums []int, target int) int {\n n := len(nums)\n return dfs(nums, target, 0, n-1)\n}\n</code></pre> binary_search_recur.swift<pre><code>/* 이진 탐색: 문제 f(i, j) */\nfunc dfs(nums: [Int], target: Int, i: Int, j: Int) -&gt; Int {\n // 구간이 비어 있으면 목표 원소가 없다는 뜻이므로 -1을 반환한다\n if i &gt; j {\n return -1\n }\n // 중간 인덱스 m을 계산한다\n let m = (i + j) / 2\n if nums[m] &lt; target {\n // 부분 문제 f(m+1, j)를 재귀적으로 푼다\n return dfs(nums: nums, target: target, i: m + 1, j: j)\n } else if nums[m] &gt; target {\n // 부분 문제 f(i, m-1)를 재귀적으로 푼다\n return dfs(nums: nums, target: target, i: i, j: m - 1)\n } else {\n // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m\n }\n}\n\n/* 이진 탐색 */\nfunc binarySearch(nums: [Int], target: Int) -&gt; Int {\n // 문제 f(0, n-1)을 푼다\n dfs(nums: nums, target: target, i: nums.startIndex, j: nums.endIndex - 1)\n}\n</code></pre> binary_search_recur.js<pre><code>/* 이진 탐색: 문제 f(i, j) */\nfunction dfs(nums, target, i, j) {\n // 구간이 비어 있으면 목표 원소가 없다는 뜻이므로 -1을 반환한다\n if (i &gt; j) {\n return -1;\n }\n // 중간 인덱스 m을 계산한다\n const m = i + ((j - i) &gt;&gt; 1);\n if (nums[m] &lt; target) {\n // 부분 문제 f(m+1, j)를 재귀적으로 푼다\n return dfs(nums, target, m + 1, j);\n } else if (nums[m] &gt; target) {\n // 부분 문제 f(i, m-1)를 재귀적으로 푼다\n return dfs(nums, target, i, m - 1);\n } else {\n // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m;\n }\n}\n\n/* 이진 탐색 */\nfunction binarySearch(nums, target) {\n const n = nums.length;\n // 문제 f(0, n-1)을 푼다\n return dfs(nums, target, 0, n - 1);\n}\n</code></pre> binary_search_recur.ts<pre><code>/* 이진 탐색: 문제 f(i, j) */\nfunction dfs(nums: number[], target: number, i: number, j: number): number {\n // 구간이 비어 있으면 목표 원소가 없다는 뜻이므로 -1을 반환한다\n if (i &gt; j) {\n return -1;\n }\n // 중간 인덱스 m을 계산한다\n const m = i + ((j - i) &gt;&gt; 1);\n if (nums[m] &lt; target) {\n // 부분 문제 f(m+1, j)를 재귀적으로 푼다\n return dfs(nums, target, m + 1, j);\n } else if (nums[m] &gt; target) {\n // 부분 문제 f(i, m-1)를 재귀적으로 푼다\n return dfs(nums, target, i, m - 1);\n } else {\n // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m;\n }\n}\n\n/* 이진 탐색 */\nfunction binarySearch(nums: number[], target: number): number {\n const n = nums.length;\n // 문제 f(0, n-1)을 푼다\n return dfs(nums, target, 0, n - 1);\n}\n</code></pre> binary_search_recur.dart<pre><code>/* 이진 탐색: 문제 f(i, j) */\nint dfs(List&lt;int&gt; nums, int target, int i, int j) {\n // 구간이 비어 있으면 목표 원소가 없다는 뜻이므로 -1을 반환한다\n if (i &gt; j) {\n return -1;\n }\n // 중간 인덱스 m을 계산한다\n int m = (i + j) ~/ 2;\n if (nums[m] &lt; target) {\n // 부분 문제 f(m+1, j)를 재귀적으로 푼다\n return dfs(nums, target, m + 1, j);\n } else if (nums[m] &gt; target) {\n // 부분 문제 f(i, m-1)를 재귀적으로 푼다\n return dfs(nums, target, i, m - 1);\n } else {\n // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m;\n }\n}\n\n/* 이진 탐색 */\nint binarySearch(List&lt;int&gt; nums, int target) {\n int n = nums.length;\n // 문제 f(0, n-1)을 푼다\n return dfs(nums, target, 0, n - 1);\n}\n</code></pre> binary_search_recur.rs<pre><code>/* 이진 탐색: 문제 f(i, j) */\nfn dfs(nums: &amp;[i32], target: i32, i: i32, j: i32) -&gt; i32 {\n // 구간이 비어 있으면 목표 원소가 없다는 뜻이므로 -1을 반환한다\n if i &gt; j {\n return -1;\n }\n let m: i32 = i + (j - i) / 2;\n if nums[m as usize] &lt; target {\n // 부분 문제 f(m+1, j)를 재귀적으로 푼다\n return dfs(nums, target, m + 1, j);\n } else if nums[m as usize] &gt; target {\n // 부분 문제 f(i, m-1)를 재귀적으로 푼다\n return dfs(nums, target, i, m - 1);\n } else {\n // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m;\n }\n}\n\n/* 이진 탐색 */\nfn binary_search(nums: &amp;[i32], target: i32) -&gt; i32 {\n let n = nums.len() as i32;\n // 문제 f(0, n-1)을 푼다\n dfs(nums, target, 0, n - 1)\n}\n</code></pre> binary_search_recur.c<pre><code>/* 이진 탐색: 문제 f(i, j) */\nint dfs(int nums[], int target, int i, int j) {\n // 구간이 비어 있으면 목표 원소가 없다는 뜻이므로 -1을 반환한다\n if (i &gt; j) {\n return -1;\n }\n // 중간 인덱스 m을 계산한다\n int m = (i + j) / 2;\n if (nums[m] &lt; target) {\n // 부분 문제 f(m+1, j)를 재귀적으로 푼다\n return dfs(nums, target, m + 1, j);\n } else if (nums[m] &gt; target) {\n // 부분 문제 f(i, m-1)를 재귀적으로 푼다\n return dfs(nums, target, i, m - 1);\n } else {\n // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m;\n }\n}\n\n/* 이진 탐색 */\nint binarySearch(int nums[], int target, int numsSize) {\n int n = numsSize;\n // 문제 f(0, n-1)을 푼다\n return dfs(nums, target, 0, n - 1);\n}\n</code></pre> binary_search_recur.kt<pre><code>/* 이진 탐색: 문제 f(i, j) */\nfun dfs(\n nums: IntArray,\n target: Int,\n i: Int,\n j: Int\n): Int {\n // 구간이 비어 있으면 목표 원소가 없다는 뜻이므로 -1을 반환한다\n if (i &gt; j) {\n return -1\n }\n // 중간 인덱스 m을 계산한다\n val m = (i + j) / 2\n return if (nums[m] &lt; target) {\n // 부분 문제 f(m+1, j)를 재귀적으로 푼다\n dfs(nums, target, m + 1, j)\n } else if (nums[m] &gt; target) {\n // 부분 문제 f(i, m-1)를 재귀적으로 푼다\n dfs(nums, target, i, m - 1)\n } else {\n // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n m\n }\n}\n\n/* 이진 탐색 */\nfun binarySearch(nums: IntArray, target: Int): Int {\n val n = nums.size\n // 문제 f(0, n-1)을 푼다\n return dfs(nums, target, 0, n - 1)\n}\n</code></pre> binary_search_recur.rb<pre><code>### 이진 탐색: 문제 f(i, j) ###\ndef dfs(nums, target, i, j)\n # 구간이 비어 있으면 목표 원소가 없다는 뜻이므로 -1을 반환한다\n return -1 if i &gt; j\n\n # 중간 인덱스 m을 계산한다\n m = (i + j) / 2\n\n if nums[m] &lt; target\n # 부분 문제 f(m+1, j)를 재귀적으로 푼다\n return dfs(nums, target, m + 1, j)\n elsif nums[m] &gt; target\n # 부분 문제 f(i, m-1)를 재귀적으로 푼다\n return dfs(nums, target, i, m - 1)\n else\n # 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m\n end\nend\n\n### 이진 탐색 ###\ndef binary_search(nums, target)\n n = nums.length\n # 문제 f(0, n-1)을 푼다\n dfs(nums, target, 0, n - 1)\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 12 장   분할 정복","12.2   분할 정복 탐색 전략"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/","level":1,"title":"12.3   이진 트리 구성 문제","text":"<p>문제</p> <p>이진 트리의 전위 순회 <code>preorder</code>와 중위 순회 <code>inorder</code>가 주어진다. 이를 이용해 이진 트리를 구성하고 루트 노드를 반환하라. 트리에는 값이 중복되는 노드가 없다고 가정한다(아래 그림(12-5) 참고).</p> <p></p> <p> 그림 12-5   이진 트리 구성 예시 데이터 </p>","path":["제 12 장   분할 정복","12.3   이진 트리 구성 문제"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#1","level":3,"title":"1.   분할 정복 문제인지 판단","text":"<p>원래 문제는 <code>preorder</code>와 <code>inorder</code>로 이진 트리를 구성하는 것으로, 전형적인 분할 정복 문제다.</p> <ul> <li>문제를 나눌 수 있다: 원래 문제를 왼쪽 서브트리 구성과 오른쪽 서브트리 구성이라는 두 부분 문제로 나누고, 루트 노드 초기화 연산을 하나 더할 수 있다. 각 서브트리(부분 문제)에도 같은 방법을 적용해 더 작은 서브트리(부분 문제)로 나눌 수 있으며, 가장 작은 부분 문제인 빈 서브트리에 도달하면 종료한다.</li> <li>부분 문제가 독립적이다: 왼쪽 서브트리와 오른쪽 서브트리는 겹치는 부분 없이 서로 독립적이다. 왼쪽 서브트리를 구성할 때는 중위 순회와 전위 순회에서 왼쪽 서브트리에 해당하는 부분만 보면 된다. 오른쪽도 마찬가지다.</li> <li>부분 문제의 해를 합칠 수 있다: 왼쪽 서브트리와 오른쪽 서브트리(부분 문제의 해)를 얻으면 루트 노드에 연결하여 원래 문제의 해를 만들 수 있다.</li> </ul>","path":["제 12 장   분할 정복","12.3   이진 트리 구성 문제"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#2","level":3,"title":"2.   서브트리를 나누는 방법","text":"<p>위 분석에 따르면 분할 정복으로 해결할 수 있다. 그런데 전위 순회 <code>preorder</code>와 중위 순회 <code>inorder</code>로 왼쪽과 오른쪽 서브트리를 어떻게 나눌까?</p> <p>정의에 따르면 <code>preorder</code>와 <code>inorder</code>는 모두 세 부분으로 나뉜다.</p> <ul> <li>전위 순회: <code>[ 루트 노드 | 왼쪽 서브트리 | 오른쪽 서브트리 ]</code>. 예를 들어 위 그림(12-5)의 트리는 <code>[ 3 | 9 | 2 1 7 ]</code>에 해당한다.</li> <li>중위 순회: <code>[ 왼쪽 서브트리 | 루트 노드 | 오른쪽 서브트리 ]</code>. 예를 들어 위 그림(12-5)의 트리는 <code>[ 9 | 3 | 1 2 7 ]</code>에 해당한다.</li> </ul> <p>위 그림(12-5)의 데이터를 예로 들면 다음 그림(12-6)의 단계로 나눌 수 있다.</p> <ol> <li>전위 순회의 첫 원소 3은 루트 노드의 값이다.</li> <li><code>inorder</code>에서 루트 노드 3의 인덱스를 찾아 이를 기준으로 <code>inorder</code>를 <code>[ 9 | 3 | 1 2 7 ]</code>로 나눈다.</li> <li><code>inorder</code>의 분할 결과로 왼쪽 서브트리의 노드 수는 1, 오른쪽은 3임을 알 수 있다. 따라서 <code>preorder</code>를 <code>[ 3 | 9 | 2 1 7 ]</code>로 나눌 수 있다.</li> </ol> <p></p> <p> 그림 12-6   전위 순회와 중위 순회에서 서브트리 나누기 </p>","path":["제 12 장   분할 정복","12.3   이진 트리 구성 문제"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#3","level":3,"title":"3.   변수로 서브트리 구간 표현","text":"<p>위 방법으로 <code>preorder</code>와 <code>inorder</code>에서 루트 노드, 왼쪽 서브트리, 오른쪽 서브트리의 인덱스 구간을 구했다. 이 구간들을 표현하려면 몇 개의 포인터 변수가 필요하다.</p> <ul> <li>현재 트리의 루트 노드가 <code>preorder</code>에서 갖는 인덱스를 \\(i\\)라고 한다.</li> <li>현재 트리의 루트 노드가 <code>inorder</code>에서 갖는 인덱스를 \\(m\\)이라고 한다.</li> <li>현재 트리가 <code>inorder</code>에서 차지하는 인덱스 구간을 \\([l, r]\\)이라고 한다.</li> </ul> <p>아래 표(12-1)와 같이 이 변수들로 <code>preorder</code>에서 루트 노드의 인덱스와 <code>inorder</code>에서 서브트리의 인덱스 구간을 나타낼 수 있다.</p> <p> 표 12-1   전위 순회와 중위 순회에서 루트 노드와 서브트리의 인덱스 </p> <code>preorder</code>에서 루트 노드의 인덱스 <code>inorder</code>에서 서브트리의 인덱스 구간 현재 트리 \\(i\\) \\([l, r]\\) 왼쪽 서브트리 \\(i + 1\\) \\([l, m-1]\\) 오른쪽 서브트리 \\(i + 1 + (m - l)\\) \\([m+1, r]\\) <p>오른쪽 서브트리의 루트 인덱스에 있는 \\((m-l)\\)은 '왼쪽 서브트리의 노드 수'를 뜻한다. 아래 그림(12-7)과 함께 이해해 보자.</p> <p></p> <p> 그림 12-7   루트 노드와 양쪽 서브트리의 인덱스 구간 표현 </p>","path":["제 12 장   분할 정복","12.3   이진 트리 구성 문제"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#4","level":3,"title":"4.   코드 구현","text":"<p>\\(m\\)을 더 효율적으로 조회하기 위해 해시 테이블 <code>hmap</code>에 배열 <code>inorder</code>의 원소와 인덱스 사이의 매핑을 저장한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby build_tree.py<pre><code>def dfs(\n preorder: list[int],\n inorder_map: dict[int, int],\n i: int,\n l: int,\n r: int,\n) -&gt; TreeNode | None:\n \"\"\"이진 트리 구성: 분할 정복\"\"\"\n # 서브트리 구간이 비어 있으면 종료한다\n if r - l &lt; 0:\n return None\n # 루트 노드를 초기화한다\n root = TreeNode(preorder[i])\n # m을 조회하여 왼쪽과 오른쪽 서브트리를 나눈다\n m = inorder_map[preorder[i]]\n # 부분 문제: 왼쪽 서브트리 구성\n root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)\n # 부분 문제: 오른쪽 서브트리 구성\n root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)\n # 루트 노드를 반환한다\n return root\n\ndef build_tree(preorder: list[int], inorder: list[int]) -&gt; TreeNode | None:\n \"\"\"이진 트리 구성\"\"\"\n # inorder 원소와 인덱스의 대응 관계를 저장할 해시 테이블을 초기화한다\n inorder_map = {val: i for i, val in enumerate(inorder)}\n root = dfs(preorder, inorder_map, 0, 0, len(inorder) - 1)\n return root\n</code></pre> build_tree.cpp<pre><code>/* 이진 트리 구성: 분할 정복 */\nTreeNode *dfs(vector&lt;int&gt; &amp;preorder, unordered_map&lt;int, int&gt; &amp;inorderMap, int i, int l, int r) {\n // 서브트리 구간이 비어 있으면 종료한다\n if (r - l &lt; 0)\n return NULL;\n // 루트 노드를 초기화한다\n TreeNode *root = new TreeNode(preorder[i]);\n // m을 조회하여 왼쪽과 오른쪽 서브트리를 나눈다\n int m = inorderMap[preorder[i]];\n // 부분 문제: 왼쪽 서브트리 구성\n root-&gt;left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n // 부분 문제: 오른쪽 서브트리 구성\n root-&gt;right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n // 루트 노드를 반환한다\n return root;\n}\n\n/* 이진 트리 구성 */\nTreeNode *buildTree(vector&lt;int&gt; &amp;preorder, vector&lt;int&gt; &amp;inorder) {\n // inorder 원소와 인덱스의 대응 관계를 저장할 해시 테이블을 초기화한다\n unordered_map&lt;int, int&gt; inorderMap;\n for (int i = 0; i &lt; inorder.size(); i++) {\n inorderMap[inorder[i]] = i;\n }\n TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorder.size() - 1);\n return root;\n}\n</code></pre> build_tree.java<pre><code>/* 이진 트리 구성: 분할 정복 */\nTreeNode dfs(int[] preorder, Map&lt;Integer, Integer&gt; inorderMap, int i, int l, int r) {\n // 서브트리 구간이 비어 있으면 종료한다\n if (r - l &lt; 0)\n return null;\n // 루트 노드를 초기화한다\n TreeNode root = new TreeNode(preorder[i]);\n // m을 조회하여 왼쪽과 오른쪽 서브트리를 나눈다\n int m = inorderMap.get(preorder[i]);\n // 부분 문제: 왼쪽 서브트리 구성\n root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n // 부분 문제: 오른쪽 서브트리 구성\n root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n // 루트 노드를 반환한다\n return root;\n}\n\n/* 이진 트리 구성 */\nTreeNode buildTree(int[] preorder, int[] inorder) {\n // inorder 원소와 인덱스의 대응 관계를 저장할 해시 테이블을 초기화한다\n Map&lt;Integer, Integer&gt; inorderMap = new HashMap&lt;&gt;();\n for (int i = 0; i &lt; inorder.length; i++) {\n inorderMap.put(inorder[i], i);\n }\n TreeNode root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n return root;\n}\n</code></pre> build_tree.cs<pre><code>/* 이진 트리 구성: 분할 정복 */\nTreeNode? DFS(int[] preorder, Dictionary&lt;int, int&gt; inorderMap, int i, int l, int r) {\n // 서브트리 구간이 비어 있으면 종료한다\n if (r - l &lt; 0)\n return null;\n // 루트 노드를 초기화한다\n TreeNode root = new(preorder[i]);\n // m을 조회하여 왼쪽과 오른쪽 서브트리를 나눈다\n int m = inorderMap[preorder[i]];\n // 부분 문제: 왼쪽 서브트리 구성\n root.left = DFS(preorder, inorderMap, i + 1, l, m - 1);\n // 부분 문제: 오른쪽 서브트리 구성\n root.right = DFS(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n // 루트 노드를 반환한다\n return root;\n}\n\n/* 이진 트리 구성 */\nTreeNode? BuildTree(int[] preorder, int[] inorder) {\n // inorder 원소와 인덱스의 대응 관계를 저장할 해시 테이블을 초기화한다\n Dictionary&lt;int, int&gt; inorderMap = [];\n for (int i = 0; i &lt; inorder.Length; i++) {\n inorderMap.TryAdd(inorder[i], i);\n }\n TreeNode? root = DFS(preorder, inorderMap, 0, 0, inorder.Length - 1);\n return root;\n}\n</code></pre> build_tree.go<pre><code>/* 이진 트리 구성: 분할 정복 */\nfunc dfsBuildTree(preorder []int, inorderMap map[int]int, i, l, r int) *TreeNode {\n // 서브트리 구간이 비어 있으면 종료한다\n if r-l &lt; 0 {\n return nil\n }\n // 루트 노드를 초기화한다\n root := NewTreeNode(preorder[i])\n // m을 조회하여 왼쪽과 오른쪽 서브트리를 나눈다\n m := inorderMap[preorder[i]]\n // 부분 문제: 왼쪽 서브트리 구성\n root.Left = dfsBuildTree(preorder, inorderMap, i+1, l, m-1)\n // 부분 문제: 오른쪽 서브트리 구성\n root.Right = dfsBuildTree(preorder, inorderMap, i+1+m-l, m+1, r)\n // 루트 노드를 반환한다\n return root\n}\n\n/* 이진 트리 구성 */\nfunc buildTree(preorder, inorder []int) *TreeNode {\n // inorder 원소와 인덱스의 대응 관계를 저장할 해시 테이블을 초기화한다\n inorderMap := make(map[int]int, len(inorder))\n for i := 0; i &lt; len(inorder); i++ {\n inorderMap[inorder[i]] = i\n }\n\n root := dfsBuildTree(preorder, inorderMap, 0, 0, len(inorder)-1)\n return root\n}\n</code></pre> build_tree.swift<pre><code>/* 이진 트리 구성: 분할 정복 */\nfunc dfs(preorder: [Int], inorderMap: [Int: Int], i: Int, l: Int, r: Int) -&gt; TreeNode? {\n // 서브트리 구간이 비어 있으면 종료한다\n if r - l &lt; 0 {\n return nil\n }\n // 루트 노드를 초기화한다\n let root = TreeNode(x: preorder[i])\n // m을 조회하여 왼쪽과 오른쪽 서브트리를 나눈다\n let m = inorderMap[preorder[i]]!\n // 부분 문제: 왼쪽 서브트리 구성\n root.left = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1, l: l, r: m - 1)\n // 부분 문제: 오른쪽 서브트리 구성\n root.right = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1 + m - l, l: m + 1, r: r)\n // 루트 노드를 반환한다\n return root\n}\n\n/* 이진 트리 구성 */\nfunc buildTree(preorder: [Int], inorder: [Int]) -&gt; TreeNode? {\n // inorder 원소와 인덱스의 대응 관계를 저장할 해시 테이블을 초기화한다\n let inorderMap = inorder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset }\n return dfs(preorder: preorder, inorderMap: inorderMap, i: inorder.startIndex, l: inorder.startIndex, r: inorder.endIndex - 1)\n}\n</code></pre> build_tree.js<pre><code>/* 이진 트리 구성: 분할 정복 */\nfunction dfs(preorder, inorderMap, i, l, r) {\n // 서브트리 구간이 비어 있으면 종료한다\n if (r - l &lt; 0) return null;\n // 루트 노드를 초기화한다\n const root = new TreeNode(preorder[i]);\n // m을 조회하여 왼쪽과 오른쪽 서브트리를 나눈다\n const m = inorderMap.get(preorder[i]);\n // 부분 문제: 왼쪽 서브트리 구성\n root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n // 부분 문제: 오른쪽 서브트리 구성\n root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n // 루트 노드를 반환한다\n return root;\n}\n\n/* 이진 트리 구성 */\nfunction buildTree(preorder, inorder) {\n // inorder 원소와 인덱스의 대응 관계를 저장할 해시 테이블을 초기화한다\n let inorderMap = new Map();\n for (let i = 0; i &lt; inorder.length; i++) {\n inorderMap.set(inorder[i], i);\n }\n const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n return root;\n}\n</code></pre> build_tree.ts<pre><code>/* 이진 트리 구성: 분할 정복 */\nfunction dfs(\n preorder: number[],\n inorderMap: Map&lt;number, number&gt;,\n i: number,\n l: number,\n r: number\n): TreeNode | null {\n // 서브트리 구간이 비어 있으면 종료한다\n if (r - l &lt; 0) return null;\n // 루트 노드를 초기화한다\n const root: TreeNode = new TreeNode(preorder[i]);\n // m을 조회하여 왼쪽과 오른쪽 서브트리를 나눈다\n const m = inorderMap.get(preorder[i]);\n // 부분 문제: 왼쪽 서브트리 구성\n root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n // 부분 문제: 오른쪽 서브트리 구성\n root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n // 루트 노드를 반환한다\n return root;\n}\n\n/* 이진 트리 구성 */\nfunction buildTree(preorder: number[], inorder: number[]): TreeNode | null {\n // inorder 원소와 인덱스의 대응 관계를 저장할 해시 테이블을 초기화한다\n let inorderMap = new Map&lt;number, number&gt;();\n for (let i = 0; i &lt; inorder.length; i++) {\n inorderMap.set(inorder[i], i);\n }\n const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n return root;\n}\n</code></pre> build_tree.dart<pre><code>/* 이진 트리 구성: 분할 정복 */\nTreeNode? dfs(\n List&lt;int&gt; preorder,\n Map&lt;int, int&gt; inorderMap,\n int i,\n int l,\n int r,\n) {\n // 서브트리 구간이 비어 있으면 종료한다\n if (r - l &lt; 0) {\n return null;\n }\n // 루트 노드를 초기화한다\n TreeNode? root = TreeNode(preorder[i]);\n // m을 조회하여 왼쪽과 오른쪽 서브트리를 나눈다\n int m = inorderMap[preorder[i]]!;\n // 부분 문제: 왼쪽 서브트리 구성\n root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n // 부분 문제: 오른쪽 서브트리 구성\n root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n // 루트 노드를 반환한다\n return root;\n}\n\n/* 이진 트리 구성 */\nTreeNode? buildTree(List&lt;int&gt; preorder, List&lt;int&gt; inorder) {\n // inorder 원소와 인덱스의 대응 관계를 저장할 해시 테이블을 초기화한다\n Map&lt;int, int&gt; inorderMap = {};\n for (int i = 0; i &lt; inorder.length; i++) {\n inorderMap[inorder[i]] = i;\n }\n TreeNode? root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n return root;\n}\n</code></pre> build_tree.rs<pre><code>/* 이진 트리 구성: 분할 정복 */\nfn dfs(\n preorder: &amp;[i32],\n inorder_map: &amp;HashMap&lt;i32, i32&gt;,\n i: i32,\n l: i32,\n r: i32,\n) -&gt; Option&lt;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt; {\n // 서브트리 구간이 비어 있으면 종료한다\n if r - l &lt; 0 {\n return None;\n }\n // 루트 노드를 초기화한다\n let root = TreeNode::new(preorder[i as usize]);\n // m을 조회하여 왼쪽과 오른쪽 서브트리를 나눈다\n let m = inorder_map.get(&amp;preorder[i as usize]).unwrap();\n // 부분 문제: 왼쪽 서브트리 구성\n root.borrow_mut().left = dfs(preorder, inorder_map, i + 1, l, m - 1);\n // 부분 문제: 오른쪽 서브트리 구성\n root.borrow_mut().right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r);\n // 루트 노드를 반환한다\n Some(root)\n}\n\n/* 이진 트리 구성 */\nfn build_tree(preorder: &amp;[i32], inorder: &amp;[i32]) -&gt; Option&lt;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt; {\n // inorder 원소와 인덱스의 대응 관계를 저장할 해시 테이블을 초기화한다\n let mut inorder_map: HashMap&lt;i32, i32&gt; = HashMap::new();\n for i in 0..inorder.len() {\n inorder_map.insert(inorder[i], i as i32);\n }\n let root = dfs(preorder, &amp;inorder_map, 0, 0, inorder.len() as i32 - 1);\n root\n}\n</code></pre> build_tree.c<pre><code>/* 이진 트리 구성: 분할 정복 */\nTreeNode *dfs(int *preorder, int *inorderMap, int i, int l, int r, int size) {\n // 서브트리 구간이 비어 있으면 종료한다\n if (r - l &lt; 0)\n return NULL;\n // 루트 노드를 초기화한다\n TreeNode *root = (TreeNode *)malloc(sizeof(TreeNode));\n root-&gt;val = preorder[i];\n root-&gt;left = NULL;\n root-&gt;right = NULL;\n // m을 조회하여 왼쪽과 오른쪽 서브트리를 나눈다\n int m = inorderMap[preorder[i]];\n // 부분 문제: 왼쪽 서브트리 구성\n root-&gt;left = dfs(preorder, inorderMap, i + 1, l, m - 1, size);\n // 부분 문제: 오른쪽 서브트리 구성\n root-&gt;right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r, size);\n // 루트 노드를 반환한다\n return root;\n}\n\n/* 이진 트리 구성 */\nTreeNode *buildTree(int *preorder, int preorderSize, int *inorder, int inorderSize) {\n // inorder 원소와 인덱스의 대응 관계를 저장할 해시 테이블을 초기화한다\n int *inorderMap = (int *)malloc(sizeof(int) * MAX_SIZE);\n for (int i = 0; i &lt; inorderSize; i++) {\n inorderMap[inorder[i]] = i;\n }\n TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorderSize - 1, inorderSize);\n free(inorderMap);\n return root;\n}\n</code></pre> build_tree.kt<pre><code>/* 이진 트리 구성: 분할 정복 */\nfun dfs(\n preorder: IntArray,\n inorderMap: Map&lt;Int?, Int?&gt;,\n i: Int,\n l: Int,\n r: Int\n): TreeNode? {\n // 서브트리 구간이 비어 있으면 종료한다\n if (r - l &lt; 0) return null\n // 루트 노드를 초기화한다\n val root = TreeNode(preorder[i])\n // m을 조회하여 왼쪽과 오른쪽 서브트리를 나눈다\n val m = inorderMap[preorder[i]]!!\n // 부분 문제: 왼쪽 서브트리 구성\n root.left = dfs(preorder, inorderMap, i + 1, l, m - 1)\n // 부분 문제: 오른쪽 서브트리 구성\n root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r)\n // 루트 노드를 반환한다\n return root\n}\n\n/* 이진 트리 구성 */\nfun buildTree(preorder: IntArray, inorder: IntArray): TreeNode? {\n // inorder 원소와 인덱스의 대응 관계를 저장할 해시 테이블을 초기화한다\n val inorderMap = HashMap&lt;Int?, Int?&gt;()\n for (i in inorder.indices) {\n inorderMap[inorder[i]] = i\n }\n val root = dfs(preorder, inorderMap, 0, 0, inorder.size - 1)\n return root\n}\n</code></pre> build_tree.rb<pre><code>### 이진 트리 구성: 분할 정복 ###\ndef dfs(preorder, inorder_map, i, l, r)\n # 서브트리 구간이 비어 있으면 종료한다\n return if r - l &lt; 0\n\n # 루트 노드를 초기화한다\n root = TreeNode.new(preorder[i])\n # m을 조회하여 왼쪽과 오른쪽 서브트리를 나눈다\n m = inorder_map[preorder[i]]\n # 부분 문제: 왼쪽 서브트리 구성\n root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)\n # 부분 문제: 오른쪽 서브트리 구성\n root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)\n\n # 루트 노드를 반환한다\n root\nend\n\n### 이진 트리 구성 ###\ndef build_tree(preorder, inorder)\n # inorder 원소와 인덱스의 대응 관계를 저장할 해시 테이블을 초기화한다\n inorder_map = {}\n inorder.each_with_index { |val, i| inorder_map[val] = i }\n dfs(preorder, inorder_map, 0, 0, inorder.length - 1)\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>아래 그림(12-8)은 이진 트리 구성의 재귀 과정을 보여 준다. 각 노드는 아래로 '호출'하는 과정에서 만들어지고, 간선(참조)은 위로 '반환'하는 과정에서 만들어진다.</p> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt;&lt;6&gt;&lt;7&gt;&lt;8&gt;&lt;9&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 12-8   이진 트리 구성의 재귀 과정 </p> <p>각 재귀 함수에서 전위 순회 <code>preorder</code>와 중위 순회 <code>inorder</code>를 나눈 결과는 아래 그림(12-9)과 같다.</p> <p></p> <p> 그림 12-9   각 재귀 함수의 분할 결과 </p> <p>트리의 노드 수가 \\(n\\)일 때 노드 하나를 초기화하는 데(재귀 함수 <code>dfs()</code> 한 번 실행) \\(O(1)\\) 시간이 걸린다. 따라서 전체 시간 복잡도는 \\(O(n)\\)이다.</p> <p>해시 테이블은 <code>inorder</code> 원소와 인덱스의 매핑을 저장하므로 공간 복잡도가 \\(O(n)\\)이다. 최악의 경우인 이진 트리가 연결 리스트로 퇴화한 경우에는 재귀 깊이가 \\(n\\)에 이르러 \\(O(n)\\)의 스택 프레임 공간을 사용한다. 따라서 전체 공간 복잡도는 \\(O(n)\\)이다.</p>","path":["제 12 장   분할 정복","12.3   이진 트리 구성 문제"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/","level":1,"title":"12.1   분할 정복 알고리즘","text":"<p>분할 정복(divide and conquer)은 '나누어 해결한다'는 뜻으로, 매우 중요하고 흔히 쓰이는 알고리즘 전략이다. 보통 재귀로 구현하며 '분할'과 '정복'의 두 단계로 이루어진다.</p> <ol> <li>분할(나누는 단계): 원래 문제를 둘 이상의 부분 문제로 재귀적으로 나누고, 가장 작은 부분 문제에 도달하면 멈춘다.</li> <li>정복(합치는 단계): 해를 이미 아는 가장 작은 부분 문제부터 시작하여 아래에서 위로 부분 문제의 해를 합침으로써 원래 문제의 해를 구성한다.</li> </ol> <p>아래 그림(12-1)의 '병합 정렬'은 분할 정복 전략의 대표적인 응용이다.</p> <ol> <li>분할: 원래 배열(원래 문제)을 두 부분 배열(부분 문제)로 재귀적으로 나누고, 부분 배열에 원소가 하나만 남으면(가장 작은 부분 문제) 멈춘다.</li> <li>정복: 정렬된 부분 배열(부분 문제의 해)을 아래에서 위로 합쳐 정렬된 원래 배열(원래 문제의 해)을 얻는다.</li> </ol> <p></p> <p> 그림 12-1   병합 정렬의 분할 정복 전략 </p>","path":["제 12 장   분할 정복","12.1   분할 정복 알고리즘"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1211","level":2,"title":"12.1.1   분할 정복 문제를 판단하는 방법","text":"<p>문제가 분할 정복에 적합한지는 보통 다음 기준으로 판단할 수 있다.</p> <ol> <li>문제를 나눌 수 있다: 원래 문제를 크기가 더 작고 형태가 비슷한 부분 문제로 나눌 수 있으며, 같은 방식으로 재귀적으로 나눌 수 있다.</li> <li>부분 문제가 독립적이다: 부분 문제들이 겹치지 않고 서로 의존하지 않으며, 독립적으로 해결할 수 있다.</li> <li>부분 문제의 해를 합칠 수 있다: 부분 문제의 해를 합쳐 원래 문제의 해를 얻는다.</li> </ol> <p>병합 정렬은 위 세 기준을 모두 만족한다.</p> <ol> <li>문제를 나눌 수 있다: 배열(원래 문제)을 두 부분 배열(부분 문제)로 재귀적으로 나눈다.</li> <li>부분 문제가 독립적이다: 각 부분 배열을 독립적으로 정렬할 수 있다(부분 문제를 독립적으로 해결할 수 있다).</li> <li>부분 문제의 해를 합칠 수 있다: 정렬된 두 부분 배열(부분 문제의 해)을 하나의 정렬된 배열(원래 문제의 해)로 합칠 수 있다.</li> </ol>","path":["제 12 장   분할 정복","12.1   분할 정복 알고리즘"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1212","level":2,"title":"12.1.2   분할 정복으로 효율 높이기","text":"<p>분할 정복은 알고리즘 문제를 효과적으로 해결할 뿐 아니라 효율을 높이는 경우도 많다. 퀵 정렬, 병합 정렬, 힙 정렬이 선택 정렬, 버블 정렬, 삽입 정렬보다 빠른 이유도 분할 정복 전략을 사용하기 때문이다.</p> <p>여기서 궁금해진다. 분할 정복이 알고리즘의 효율을 높이는 근본적인 원리는 무엇일까? 큰 문제를 여러 부분 문제로 나누고, 부분 문제를 해결하고, 그 해를 합쳐 원래 문제의 해를 구하는 과정이 왜 원래 문제를 직접 푸는 것보다 효율적일까? 연산 수와 병렬 계산이라는 두 관점에서 살펴볼 수 있다.</p>","path":["제 12 장   분할 정복","12.1   분할 정복 알고리즘"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1","level":3,"title":"1.   연산 수 최적화","text":"<p>'버블 정렬'은 길이 \\(n\\)의 배열을 처리하는 데 \\(O(n^2)\\) 시간이 걸린다. 아래 그림(12-2)처럼 중간에서 두 부분 배열로 나눈다고 하자. 분할에는 \\(O(n)\\) 시간, 각 부분 배열의 정렬에는 \\(O((n / 2)^2)\\) 시간, 두 부분 배열의 병합에는 \\(O(n)\\) 시간이 걸리므로 전체 시간 복잡도는 다음과 같다.</p> \\[ O(n + (\\frac{n}{2})^2 \\times 2 + n) = O(\\frac{n^2}{2} + 2n) \\] <p></p> <p> 그림 12-2   배열 분할 전후의 버블 정렬 </p> <p>이어서 왼쪽은 분할 전, 오른쪽은 분할 후의 총연산 수를 나타내는 다음 부등식을 계산해 보자.</p> \\[ \\begin{aligned} n^2 &amp; &gt; \\frac{n^2}{2} + 2n \\newline n^2 - \\frac{n^2}{2} - 2n &amp; &gt; 0 \\newline n(n - 4) &amp; &gt; 0 \\end{aligned} \\] <p>이는 \\(n &gt; 4\\)일 때 분할 후의 연산 수가 더 적어 정렬 효율이 높아질 것임을 뜻한다. 분할 후에도 시간 복잡도는 여전히 이차인 \\(O(n^2)\\)이며, 상수항만 작아졌다는 점에 주의하자.</p> <p>여기서 더 나아가 부분 배열을 계속 중간에서 두 개로 나누고, 원소가 하나만 남을 때까지 반복하면 어떨까? 이 발상은 바로 '병합 정렬'이며 시간 복잡도는 \\(O(n \\log n)\\)이다.</p> <p>다시 생각해 보자. 분할 지점을 더 많이 두어 원래 배열을 \\(k\\) 개의 부분 배열로 고르게 나누면 어떨까? 이는 '버킷 정렬'과 매우 비슷하다. 대규모 데이터 정렬에 적합하며 이론적으로 시간 복잡도가 \\(O(n + k)\\)에 이를 수 있다.</p>","path":["제 12 장   분할 정복","12.1   분할 정복 알고리즘"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#2","level":3,"title":"2.   병렬 계산 최적화","text":"<p>분할 정복으로 만들어진 부분 문제는 서로 독립적이므로 보통 병렬로 해결할 수 있다. 즉, 분할 정복은 알고리즘의 시간 복잡도를 줄일 뿐 아니라 운영체제의 병렬 최적화에도 유리하다.</p> <p>병렬 최적화는 멀티코어나 다중 프로세서 환경에서 특히 효과적이다. 여러 부분 문제를 동시에 처리하여 계산 자원을 더 충분히 활용하고 전체 실행 시간을 크게 줄일 수 있기 때문이다.</p> <p>예를 들어 아래 그림(12-3)의 '버킷 정렬'에서 대규모 데이터를 각 버킷에 고르게 분배하면, 버킷별 정렬 작업을 여러 계산 장치에 나누어 맡긴 뒤 결과를 합칠 수 있다.</p> <p></p> <p> 그림 12-3   버킷 정렬의 병렬 계산 </p>","path":["제 12 장   분할 정복","12.1   분할 정복 알고리즘"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1213","level":2,"title":"12.1.3   분할 정복의 주요 응용","text":"<p>분할 정복은 여러 대표적인 알고리즘 문제를 해결할 수 있다.</p> <ul> <li>가장 가까운 점 쌍 찾기: 먼저 점 집합을 두 부분으로 나누고 각 부분에서 가장 가까운 점 쌍을 찾은 뒤, 마지막으로 두 부분에 걸쳐 있는 가장 가까운 점 쌍을 찾는다.</li> <li>큰 정수의 곱셈: Karatsuba 알고리즘은 큰 정수의 곱셈을 더 작은 정수들의 곱셈과 덧셈으로 나눈다.</li> <li>행렬 곱셈: Strassen 알고리즘은 큰 행렬의 곱셈을 작은 행렬들의 곱셈과 덧셈으로 나눈다.</li> <li>하노이의 탑 문제: 재귀로 해결할 수 있는 하노이의 탑은 분할 정복 전략의 대표적인 응용이다.</li> <li>역전 쌍 구하기: 수열에서 앞의 수가 뒤의 수보다 크면 두 수가 역전 쌍을 이룬다. 역전 쌍 문제는 분할 정복의 아이디어로 병합 정렬을 이용하여 해결할 수 있다.</li> </ul> <p>또한 분할 정복은 알고리즘과 자료 구조의 설계에도 매우 널리 쓰인다.</p> <ul> <li>이진 탐색: 정렬된 배열을 중간 인덱스에서 두 부분으로 나누고, 목표 값과 중간 원소를 비교한 결과에 따라 제외할 절반을 정한다. 남은 구간에서 같은 이진 탐색 연산을 수행한다.</li> <li>병합 정렬: 이 절의 앞부분에서 소개했으므로 다시 설명하지 않는다.</li> <li>퀵 정렬: 피벗을 하나 고르고 피벗보다 작은 원소와 큰 원소를 각각 두 부분 배열로 나눈다. 부분 배열에 원소가 하나만 남을 때까지 같은 분할을 반복한다.</li> <li>버킷 정렬: 데이터를 여러 버킷에 분산하고 각 버킷의 원소를 정렬한 뒤, 버킷별 원소를 차례로 꺼내 정렬된 배열을 얻는다.</li> <li>트리: 이진 탐색 트리, AVL 트리, 레드-블랙 트리, B 트리, B+ 트리 등의 탐색, 삽입, 삭제 연산은 모두 분할 정복 전략의 응용으로 볼 수 있다.</li> <li>힙: 특수한 완전 이진 트리인 힙의 삽입, 삭제, 재정렬 등에도 분할 정복의 아이디어가 숨어 있다.</li> <li>해시 테이블: 분할 정복을 직접 적용하지는 않지만 일부 해시 충돌 해결 방법에는 간접적으로 사용된다. 예를 들어 체이닝의 긴 연결 리스트를 레드-블랙 트리로 바꾸어 조회 효율을 높인다.</li> </ul> <p>이처럼 분할 정복은 조용히 스며들어 작용하는 알고리즘 사고방식이며, 다양한 알고리즘과 자료 구조 속에 숨어 있다.</p>","path":["제 12 장   분할 정복","12.1   분할 정복 알고리즘"],"tags":[]},{"location":"chapter_divide_and_conquer/exercises/","level":1,"title":"12.6   연습 문제","text":"","path":["제 12 장   분할 정복","12.6   연습 문제"],"tags":[]},{"location":"chapter_divide_and_conquer/exercises/#1261","level":2,"title":"12.6.1   개념 확인","text":"","path":["제 12 장   분할 정복","12.6   연습 문제"],"tags":[]},{"location":"chapter_divide_and_conquer/exercises/#1","level":3,"title":"1.   어떤 작업에 분할 정복이 적합한가","text":"<p>한 학생이 ‘먼저 둘로 나누고, 각각 해결한 뒤, 결과를 합치는’ 방식으로 다음 작업을 수행하려 한다. 각 작업이 ‘분할 정복에 적합함’, ‘분할 정복이 가능하지만 전체 작업량은 줄어들지 않음’, ‘두 부분을 독립적으로 해결할 수 없음’ 중 어디에 해당하는지 판단하고 이유를 설명하라.</p> <ol> <li>정렬되지 않은 배열을 정렬한다.</li> <li>배열의 최댓값을 구한다.</li> <li>일련의 스택 연산 <code>push(x)</code>, <code>pop()</code>을 순서대로 실행하고 각 <code>pop()</code>에서 얻은 원소를 출력한다.</li> </ol> 참고 답안 <ol> <li>적합함: 반으로 나눈 뒤 두 부분을 독립적으로 정렬하고, \\(O(n)\\) 시간에 병합한다. 이것이 바로 병합 정렬이다.</li> <li>분할 정복이 가능하지만 전체 작업량은 줄어들지 않음: 왼쪽과 오른쪽을 합치면 여전히 \\(n\\)개 원소를 모두 확인해야 하므로, 직접 순회하는 것과 마찬가지로 \\(O(n)\\) 시간이 든다.</li> <li>두 부분을 독립적으로 해결할 수 없음: 후반부를 시작할 때의 스택 내용은 전반부의 실행 결과에 따라 달라진다. 따라서 서로의 결과를 모르는 상태에서 두 부분을 독립적으로 완료할 수 없다.</li> </ol>","path":["제 12 장   분할 정복","12.6   연습 문제"],"tags":[]},{"location":"chapter_divide_and_conquer/exercises/#2","level":3,"title":"2.   빠른 거듭제곱은 계산을 어떻게 줄이는가","text":"<p>다음 재귀 함수는 분할 정복으로 \\(x^n\\)을 계산한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby fast_power.py<pre><code>def fast_pow(x: int, n: int) -&gt; int:\n \"\"\"빠른 거듭제곱\"\"\"\n if n == 0:\n return 1\n half = fast_pow(x, n // 2)\n if n % 2 == 0:\n return half * half\n return half * half * x\n</code></pre> fast_power.cpp<pre><code>/* 빠른 거듭제곱 */\nint fastPow(int x, int n) {\n if (n == 0) {\n return 1;\n }\n int half = fastPow(x, n / 2);\n if (n % 2 == 0) {\n return half * half;\n }\n return half * half * x;\n}\n</code></pre> fast_power.java<pre><code>/* 빠른 거듭제곱 */\nint fastPow(int x, int n) {\n if (n == 0) {\n return 1;\n }\n int half = fastPow(x, n / 2);\n if (n % 2 == 0) {\n return half * half;\n }\n return half * half * x;\n}\n</code></pre> fast_power.cs<pre><code>/* 빠른 거듭제곱 */\nint FastPow(int x, int n) {\n if (n == 0) {\n return 1;\n }\n int half = FastPow(x, n / 2);\n if (n % 2 == 0) {\n return half * half;\n }\n return half * half * x;\n}\n</code></pre> fast_power.go<pre><code>/* 빠른 거듭제곱 */\nfunc fastPow(x, n int) int {\n if n == 0 {\n return 1\n }\n half := fastPow(x, n/2)\n if n%2 == 0 {\n return half * half\n }\n return half * half * x\n}\n</code></pre> fast_power.swift<pre><code>/* 빠른 거듭제곱 */\nfunc fastPow(x: Int, n: Int) -&gt; Int {\n if n == 0 {\n return 1\n }\n let half = fastPow(x: x, n: n / 2)\n if n % 2 == 0 {\n return half * half\n }\n return half * half * x\n}\n</code></pre> fast_power.js<pre><code>/* 빠른 거듭제곱 */\nfunction fastPow(x, n) {\n if (n === 0) {\n return 1;\n }\n const half = fastPow(x, Math.floor(n / 2));\n if (n % 2 === 0) {\n return half * half;\n }\n return half * half * x;\n}\n</code></pre> fast_power.ts<pre><code>/* 빠른 거듭제곱 */\nfunction fastPow(x: number, n: number): number {\n if (n === 0) {\n return 1;\n }\n const half = fastPow(x, Math.floor(n / 2));\n if (n % 2 === 0) {\n return half * half;\n }\n return half * half * x;\n}\n</code></pre> fast_power.dart<pre><code>/* 빠른 거듭제곱 */\nint fastPow(int x, int n) {\n if (n == 0) {\n return 1;\n }\n int half = fastPow(x, n ~/ 2);\n if (n % 2 == 0) {\n return half * half;\n }\n return half * half * x;\n}\n</code></pre> fast_power.rs<pre><code>/* 빠른 거듭제곱 */\nfn fast_pow(x: i32, n: i32) -&gt; i32 {\n if n == 0 {\n return 1;\n }\n let half = fast_pow(x, n / 2);\n if n % 2 == 0 {\n return half * half;\n }\n half * half * x\n}\n</code></pre> fast_power.c<pre><code>/* 빠른 거듭제곱 */\nint fastPow(int x, int n) {\n if (n == 0) {\n return 1;\n }\n int half = fastPow(x, n / 2);\n if (n % 2 == 0) {\n return half * half;\n }\n return half * half * x;\n}\n</code></pre> fast_power.kt<pre><code>/* 빠른 거듭제곱 */\nfun fastPow(x: Int, n: Int): Int {\n if (n == 0) {\n return 1\n }\n val half = fastPow(x, n / 2)\n if (n % 2 == 0) {\n return half * half\n }\n return half * half * x\n}\n</code></pre> fast_power.rb<pre><code>### 빠른 거듭제곱 ###\ndef fast_pow(x, n)\n return 1 if n == 0\n\n half = fast_pow(x, n / 2)\n return half * half if n.even?\n\n half * half * x\nend\n</code></pre> <p><code>x = 3</code>, <code>n = 5</code>로 설정하고 이 함수를 실행한다.</p> <ol> <li>재귀 호출에서 매개변수 <code>n</code>은 차례대로 어떤 값이 되는가?</li> <li>가장 깊은 호출부터 반환할 때 각 호출은 차례대로 어떤 값을 반환하는가?</li> <li>곱셈 양쪽에서 같은 부분 문제를 한 번씩 호출하지 않고, 먼저 재귀 호출의 결과를 <code>half</code>에 저장하는 이유는 무엇인가?</li> </ol> 참고 답안 <ol> <li> <p>매개변수는 <code>5 → 2 → 1 → 0</code> 순으로 바뀐다. 종료 조건에 도달할 때까지 매번 지수를 절반으로 줄인다.</p> </li> <li> <p><code>n = 0</code>이면 1을 반환한다. <code>n = 1</code>이면 \\(1×1×3=3\\)을 반환한다. <code>n = 2</code>이면 \\(3×3=9\\)를, <code>n = 5</code>이면 \\(9×9×3=243\\)을 반환한다.</p> </li> <li> <p>곱셈 양쪽에서 같은 부분 문제를 한 번씩 호출하면 두 재귀 호출은 완전히 같은 계산을 수행한다. 먼저 결과를 <code>half</code>에 저장하면 각 단계에서 재귀 호출을 한 번만 하면 되고, 재귀 깊이는 대략 \\(\\log n\\)이다. 두 번 호출하면 같은 계산이 대량으로 반복된다.</p> </li> </ol>","path":["제 12 장   분할 정복","12.6   연습 문제"],"tags":[]},{"location":"chapter_divide_and_conquer/exercises/#3","level":3,"title":"3.   순회 결과에서 왼쪽 서브트리와 오른쪽 서브트리 나누기","text":"<p>중복된 노드가 없는 이진 트리의 전위 순회와 중위 순회 결과는 다음과 같다.</p> <ul> <li>전위 순회: <code>[A, B, D, E, C]</code></li> <li>중위 순회: <code>[D, B, E, A, C]</code></li> </ul> <p>루트 노드 수준에서만 나누고, 더 깊이 재귀적으로 나누거나 트리 전체를 그릴 필요는 없다.</p> <ol> <li>루트 노드는 무엇인가?</li> <li>중위 순회에서 왼쪽 서브트리와 오른쪽 서브트리에 해당하는 구간은 각각 어디인가?</li> <li>전위 순회에서 왼쪽 서브트리와 오른쪽 서브트리에 해당하는 구간은 각각 어디인가? 루트 노드의 직접적인 자식은 누구인가?</li> </ol> 참고 답안 <ol> <li> <p>전위 순회의 첫 번째 노드가 루트 노드이므로 루트는 <code>A</code>다.</p> </li> <li> <p><code>A</code>를 기준으로 중위 순회가 두 부분으로 나뉜다. 왼쪽 서브트리는 <code>[D, B, E]</code>, 오른쪽 서브트리는 <code>[C]</code>다.</p> </li> <li> <p>왼쪽 서브트리에 노드가 3개 있으므로 전위 순회에서 루트 노드 <code>A</code> 다음의 원소 3개가 왼쪽 서브트리에 속한다. 즉 <code>[B, D, E]</code>가 왼쪽 서브트리이고, 나머지 <code>[C]</code>가 오른쪽 서브트리다. 따라서 루트 노드의 왼쪽 자식은 <code>B</code>, 오른쪽 자식은 <code>C</code>다.</p> </li> </ol>","path":["제 12 장   분할 정복","12.6   연습 문제"],"tags":[]},{"location":"chapter_divide_and_conquer/exercises/#1262","level":2,"title":"12.6.2   프로그래밍 연습","text":"","path":["제 12 장   분할 정복","12.6   연습 문제"],"tags":[]},{"location":"chapter_divide_and_conquer/exercises/#1_1","level":3,"title":"1.   빠른 거듭제곱","text":"<p>실수 <code>x</code>와 정수 <code>n</code>이 주어진다. 언어에서 제공하는 거듭제곱 함수를 호출하지 않고 \\(x^n\\)을 계산하라. 재귀적인 분할 정복을 사용하여 매번 지수를 절반으로 줄이고, 이미 계산한 부분 문제의 결과를 재사용해야 한다. 이 문제에서는 <code>x = 0</code>인 경우를 포함하여 \\(x^0=1\\)로 정한다. <code>n &lt; 0</code>일 때는 <code>x != 0</code>임이 보장되며, 답을 \\((1/x)^{-n}\\)으로 바꾸어 구할 수 있다.</p> 풀이 힌트 <ol> <li>n이 0이면 답은 1이다.</li> <li>x의 n // 2제곱을 계산하여 half에 저장한다. 같은 재귀 호출을 두 번 하지 마라.</li> <li>n &lt; 0이면 먼저 x를 1 / x로 바꾸고 n을 -n으로 바꾼다. C++이나 Java에서는 n을 먼저 64비트 정수로 변환하면 가장 작은 32비트 정수의 부호를 반대로 바꿀 때 발생하는 오버플로를 피할 수 있다.</li> </ol> <p>LeetCode</p>","path":["제 12 장   분할 정복","12.6   연습 문제"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/","level":1,"title":"12.4   하노이의 탑 문제","text":"<p>병합 정렬과 이진 트리 구성에서는 원래 문제를 크기가 절반인 두 부분 문제로 나누었다. 하지만 하노이의 탑 문제에서는 다른 분할 전략을 사용한다.</p> <p>문제</p> <p>기둥 세 개를 <code>A</code>, <code>B</code>, <code>C</code>라고 하자. 처음에는 기둥 <code>A</code>에 원판 \\(n\\) 개가 위에서 아래로 작은 것부터 큰 것 순서로 꽂혀 있다. 목표는 원래 순서를 유지하면서 이 원판 \\(n\\) 개를 기둥 <code>C</code>로 옮기는 것이다(아래 그림(12-10) 참고). 원판을 옮길 때는 다음 규칙을 지켜야 한다.</p> <ol> <li>원판은 한 기둥의 맨 위에서만 꺼내어 다른 기둥의 맨 위에 놓을 수 있다.</li> <li>한 번에 원판 하나만 옮길 수 있다.</li> <li>작은 원판은 항상 큰 원판 위에 있어야 한다.</li> </ol> <p></p> <p> 그림 12-10   하노이의 탑 문제 예시 </p> <p>크기가 \\(i\\)인 하노이의 탑 문제를 \\(f(i)\\)라고 하자. 예를 들어 \\(f(3)\\)은 원판 \\(3\\) 개를 <code>A</code>에서 <code>C</code>로 옮기는 문제다.</p>","path":["제 12 장   분할 정복","12.4   하노이의 탑 문제"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#1","level":3,"title":"1.   기본 경우 살펴보기","text":"<p>아래 그림(12-11)과 같이 \\(f(1)\\), 즉 원판이 하나뿐이면 <code>A</code>에서 <code>C</code>로 바로 옮기면 된다.</p> &lt;1&gt;&lt;2&gt; <p></p> <p></p> <p> 그림 12-11   크기 1인 문제의 해 </p> <p>아래 그림(12-12)과 같이 \\(f(2)\\), 즉 원판이 두 개이면 작은 원판이 항상 큰 원판 위에 있어야 하므로 <code>B</code>를 이용해야 한다.</p> <ol> <li>위의 작은 원판을 <code>A</code>에서 <code>B</code>로 옮긴다.</li> <li>큰 원판을 <code>A</code>에서 <code>C</code>로 옮긴다.</li> <li>작은 원판을 <code>B</code>에서 <code>C</code>로 옮긴다.</li> </ol> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt; <p></p> <p></p> <p></p> <p></p> <p> 그림 12-12   크기 2인 문제의 해 </p> <p>\\(f(2)\\)의 해결 과정은 <code>B</code>를 이용하여 원판 두 개를 <code>A</code>에서 <code>C</code>로 옮기는 것으로 요약할 수 있다. 여기서 <code>C</code>를 목표 기둥, <code>B</code>를 보조 기둥이라고 한다.</p>","path":["제 12 장   분할 정복","12.4   하노이의 탑 문제"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#2","level":3,"title":"2.   부분 문제로 나누기","text":"<p>\\(f(3)\\), 즉 원판이 세 개이면 상황이 조금 더 복잡해진다.</p> <p>\\(f(1)\\)과 \\(f(2)\\)의 해를 이미 알고 있으므로 분할 정복의 관점에서 생각해 보자. <code>A</code> 위쪽의 원판 두 개를 하나의 묶음으로 보고 아래 그림(12-13)의 단계를 수행하면 세 원판을 <code>A</code>에서 <code>C</code>로 옮길 수 있다.</p> <ol> <li><code>B</code>를 목표 기둥, <code>C</code>를 보조 기둥으로 삼아 원판 두 개를 <code>A</code>에서 <code>B</code>로 옮긴다.</li> <li><code>A</code>에 남은 원판 하나를 <code>C</code>로 바로 옮긴다.</li> <li><code>C</code>를 목표 기둥, <code>A</code>를 보조 기둥으로 삼아 원판 두 개를 <code>B</code>에서 <code>C</code>로 옮긴다.</li> </ol> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt; <p></p> <p></p> <p></p> <p></p> <p> 그림 12-13   크기 3인 문제의 해 </p> <p>본질적으로 문제 \\(f(3)\\)을 부분 문제 \\(f(2)\\) 두 개와 \\(f(1)\\) 하나로 나눈 것이다. 세 부분 문제를 순서대로 해결하면 원래 문제도 해결된다. 이는 부분 문제들이 독립적이고 해를 합칠 수 있음을 보여 준다.</p> <p>이제 아래 그림(12-14)과 같이 하노이의 탑을 푸는 분할 정복 전략을 정리할 수 있다. 원래 문제 \\(f(n)\\)을 부분 문제 \\(f(n-1)\\) 두 개와 \\(f(1)\\) 하나로 나누고, 다음 순서로 해결한다.</p> <ol> <li><code>C</code>를 이용하여 원판 \\(n-1\\) 개를 <code>A</code>에서 <code>B</code>로 옮긴다.</li> <li>남은 원판 \\(1\\) 개를 <code>A</code>에서 <code>C</code>로 바로 옮긴다.</li> <li><code>A</code>를 이용하여 원판 \\(n-1\\) 개를 <code>B</code>에서 <code>C</code>로 옮긴다.</li> </ol> <p>두 부분 문제 \\(f(n-1)\\)도 같은 방식으로 재귀적으로 나눌 수 있으며, 가장 작은 부분 문제 \\(f(1)\\)에 도달하면 멈춘다. \\(f(1)\\)의 해는 이미 알고 있고, 한 번만 옮기면 된다.</p> <p></p> <p> 그림 12-14   하노이의 탑을 푸는 분할 정복 전략 </p>","path":["제 12 장   분할 정복","12.4   하노이의 탑 문제"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#3","level":3,"title":"3.   코드 구현","text":"<p>코드에서는 재귀 함수 <code>dfs(i, src, buf, tar)</code>를 정의한다. 이 함수는 기둥 <code>src</code> 위쪽의 원판 \\(i\\) 개를 보조 기둥 <code>buf</code>를 이용하여 목표 기둥 <code>tar</code>로 옮긴다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hanota.py<pre><code>def move(src: list[int], tar: list[int]):\n \"\"\"원판 하나 이동\"\"\"\n # src의 맨 위에서 원판 하나를 꺼낸다\n pan = src.pop()\n # 원판을 tar의 맨 위에 놓는다\n tar.append(pan)\n\ndef dfs(i: int, src: list[int], buf: list[int], tar: list[int]):\n \"\"\"하노이의 탑 문제 f(i) 풀기\"\"\"\n # src에 원판이 하나만 남아 있으면 바로 tar로 옮긴다\n if i == 1:\n move(src, tar)\n return\n # 부분 문제 f(i-1): tar를 이용하여 src의 맨 위 원판 i-1개를 buf로 옮긴다\n dfs(i - 1, src, tar, buf)\n # 부분 문제 f(1): src에 남은 원판 하나를 tar로 옮긴다\n move(src, tar)\n # 부분 문제 f(i-1): src를 이용하여 buf의 맨 위 원판 i-1개를 tar로 옮긴다\n dfs(i - 1, buf, src, tar)\n\ndef solve_hanota(A: list[int], B: list[int], C: list[int]):\n \"\"\"하노이의 탑 문제 풀기\"\"\"\n n = len(A)\n # B를 이용하여 A의 맨 위 원판 n개를 C로 옮긴다\n dfs(n, A, B, C)\n</code></pre> hanota.cpp<pre><code>/* 원판 하나 이동 */\nvoid move(vector&lt;int&gt; &amp;src, vector&lt;int&gt; &amp;tar) {\n // src의 맨 위에서 원판 하나를 꺼낸다\n int pan = src.back();\n src.pop_back();\n // 원판을 tar의 맨 위에 놓는다\n tar.push_back(pan);\n}\n\n/* 하노이의 탑 문제 f(i) 풀기 */\nvoid dfs(int i, vector&lt;int&gt; &amp;src, vector&lt;int&gt; &amp;buf, vector&lt;int&gt; &amp;tar) {\n // src에 원판이 하나만 남아 있으면 바로 tar로 옮긴다\n if (i == 1) {\n move(src, tar);\n return;\n }\n // 부분 문제 f(i-1): tar를 이용하여 src의 맨 위 원판 i-1개를 buf로 옮긴다\n dfs(i - 1, src, tar, buf);\n // 부분 문제 f(1): src에 남은 원판 하나를 tar로 옮긴다\n move(src, tar);\n // 부분 문제 f(i-1): src를 이용하여 buf의 맨 위 원판 i-1개를 tar로 옮긴다\n dfs(i - 1, buf, src, tar);\n}\n\n/* 하노이의 탑 문제 풀기 */\nvoid solveHanota(vector&lt;int&gt; &amp;A, vector&lt;int&gt; &amp;B, vector&lt;int&gt; &amp;C) {\n int n = A.size();\n // B를 이용하여 A의 맨 위 원판 n개를 C로 옮긴다\n dfs(n, A, B, C);\n}\n</code></pre> hanota.java<pre><code>/* 원판 하나 이동 */\nvoid move(List&lt;Integer&gt; src, List&lt;Integer&gt; tar) {\n // src의 맨 위에서 원판 하나를 꺼낸다\n Integer pan = src.remove(src.size() - 1);\n // 원판을 tar의 맨 위에 놓는다\n tar.add(pan);\n}\n\n/* 하노이의 탑 문제 f(i) 풀기 */\nvoid dfs(int i, List&lt;Integer&gt; src, List&lt;Integer&gt; buf, List&lt;Integer&gt; tar) {\n // src에 원판이 하나만 남아 있으면 바로 tar로 옮긴다\n if (i == 1) {\n move(src, tar);\n return;\n }\n // 부분 문제 f(i-1): tar를 이용하여 src의 맨 위 원판 i-1개를 buf로 옮긴다\n dfs(i - 1, src, tar, buf);\n // 부분 문제 f(1): src에 남은 원판 하나를 tar로 옮긴다\n move(src, tar);\n // 부분 문제 f(i-1): src를 이용하여 buf의 맨 위 원판 i-1개를 tar로 옮긴다\n dfs(i - 1, buf, src, tar);\n}\n\n/* 하노이의 탑 문제 풀기 */\nvoid solveHanota(List&lt;Integer&gt; A, List&lt;Integer&gt; B, List&lt;Integer&gt; C) {\n int n = A.size();\n // B를 이용하여 A의 맨 위 원판 n개를 C로 옮긴다\n dfs(n, A, B, C);\n}\n</code></pre> hanota.cs<pre><code>/* 원판 하나 이동 */\nvoid Move(List&lt;int&gt; src, List&lt;int&gt; tar) {\n // src의 맨 위에서 원판 하나를 꺼낸다\n int pan = src[^1];\n src.RemoveAt(src.Count - 1);\n // 원판을 tar의 맨 위에 놓는다\n tar.Add(pan);\n}\n\n/* 하노이의 탑 문제 f(i) 풀기 */\nvoid DFS(int i, List&lt;int&gt; src, List&lt;int&gt; buf, List&lt;int&gt; tar) {\n // src에 원판이 하나만 남아 있으면 바로 tar로 옮긴다\n if (i == 1) {\n Move(src, tar);\n return;\n }\n // 부분 문제 f(i-1): tar를 이용하여 src의 맨 위 원판 i-1개를 buf로 옮긴다\n DFS(i - 1, src, tar, buf);\n // 부분 문제 f(1): src에 남은 원판 하나를 tar로 옮긴다\n Move(src, tar);\n // 부분 문제 f(i-1): src를 이용하여 buf의 맨 위 원판 i-1개를 tar로 옮긴다\n DFS(i - 1, buf, src, tar);\n}\n\n/* 하노이의 탑 문제 풀기 */\nvoid SolveHanota(List&lt;int&gt; A, List&lt;int&gt; B, List&lt;int&gt; C) {\n int n = A.Count;\n // B를 이용하여 A의 맨 위 원판 n개를 C로 옮긴다\n DFS(n, A, B, C);\n}\n</code></pre> hanota.go<pre><code>/* 원판 하나 이동 */\nfunc move(src, tar *list.List) {\n // src의 맨 위에서 원판 하나를 꺼낸다\n pan := src.Back()\n // 원판을 tar의 맨 위에 놓는다\n tar.PushBack(pan.Value)\n // src의 맨 위 원판을 제거한다\n src.Remove(pan)\n}\n\n/* 하노이의 탑 문제 f(i) 풀기 */\nfunc dfsHanota(i int, src, buf, tar *list.List) {\n // src에 원판이 하나만 남아 있으면 바로 tar로 옮긴다\n if i == 1 {\n move(src, tar)\n return\n }\n // 부분 문제 f(i-1): tar를 이용하여 src의 맨 위 원판 i-1개를 buf로 옮긴다\n dfsHanota(i-1, src, tar, buf)\n // 부분 문제 f(1): src에 남은 원판 하나를 tar로 옮긴다\n move(src, tar)\n // 부분 문제 f(i-1): src를 이용하여 buf의 맨 위 원판 i-1개를 tar로 옮긴다\n dfsHanota(i-1, buf, src, tar)\n}\n\n/* 하노이의 탑 문제 풀기 */\nfunc solveHanota(A, B, C *list.List) {\n n := A.Len()\n // B를 이용하여 A의 맨 위 원판 n개를 C로 옮긴다\n dfsHanota(n, A, B, C)\n}\n</code></pre> hanota.swift<pre><code>/* 원판 하나 이동 */\nfunc move(src: inout [Int], tar: inout [Int]) {\n // src의 맨 위에서 원판 하나를 꺼낸다\n let pan = src.popLast()!\n // 원판을 tar의 맨 위에 놓는다\n tar.append(pan)\n}\n\n/* 하노이의 탑 문제 f(i) 풀기 */\nfunc dfs(i: Int, src: inout [Int], buf: inout [Int], tar: inout [Int]) {\n // src에 원판이 하나만 남아 있으면 바로 tar로 옮긴다\n if i == 1 {\n move(src: &amp;src, tar: &amp;tar)\n return\n }\n // 부분 문제 f(i-1): tar를 이용하여 src의 맨 위 원판 i-1개를 buf로 옮긴다\n dfs(i: i - 1, src: &amp;src, buf: &amp;tar, tar: &amp;buf)\n // 부분 문제 f(1): src에 남은 원판 하나를 tar로 옮긴다\n move(src: &amp;src, tar: &amp;tar)\n // 부분 문제 f(i-1): src를 이용하여 buf의 맨 위 원판 i-1개를 tar로 옮긴다\n dfs(i: i - 1, src: &amp;buf, buf: &amp;src, tar: &amp;tar)\n}\n\n/* 하노이의 탑 문제 풀기 */\nfunc solveHanota(A: inout [Int], B: inout [Int], C: inout [Int]) {\n let n = A.count\n // 리스트의 맨 뒤가 기둥의 맨 위이다\n // B를 이용하여 src의 맨 위 원판 n개를 C로 옮긴다\n dfs(i: n, src: &amp;A, buf: &amp;B, tar: &amp;C)\n}\n</code></pre> hanota.js<pre><code>/* 원판 하나 이동 */\nfunction move(src, tar) {\n // src의 맨 위에서 원판 하나를 꺼낸다\n const pan = src.pop();\n // 원판을 tar의 맨 위에 놓는다\n tar.push(pan);\n}\n\n/* 하노이의 탑 문제 f(i) 풀기 */\nfunction dfs(i, src, buf, tar) {\n // src에 원판이 하나만 남아 있으면 바로 tar로 옮긴다\n if (i === 1) {\n move(src, tar);\n return;\n }\n // 부분 문제 f(i-1): tar를 이용하여 src의 맨 위 원판 i-1개를 buf로 옮긴다\n dfs(i - 1, src, tar, buf);\n // 부분 문제 f(1): src에 남은 원판 하나를 tar로 옮긴다\n move(src, tar);\n // 부분 문제 f(i-1): src를 이용하여 buf의 맨 위 원판 i-1개를 tar로 옮긴다\n dfs(i - 1, buf, src, tar);\n}\n\n/* 하노이의 탑 문제 풀기 */\nfunction solveHanota(A, B, C) {\n const n = A.length;\n // B를 이용하여 A의 맨 위 원판 n개를 C로 옮긴다\n dfs(n, A, B, C);\n}\n</code></pre> hanota.ts<pre><code>/* 원판 하나 이동 */\nfunction move(src: number[], tar: number[]): void {\n // src의 맨 위에서 원판 하나를 꺼낸다\n const pan = src.pop();\n // 원판을 tar의 맨 위에 놓는다\n tar.push(pan);\n}\n\n/* 하노이의 탑 문제 f(i) 풀기 */\nfunction dfs(i: number, src: number[], buf: number[], tar: number[]): void {\n // src에 원판이 하나만 남아 있으면 바로 tar로 옮긴다\n if (i === 1) {\n move(src, tar);\n return;\n }\n // 부분 문제 f(i-1): tar를 이용하여 src의 맨 위 원판 i-1개를 buf로 옮긴다\n dfs(i - 1, src, tar, buf);\n // 부분 문제 f(1): src에 남은 원판 하나를 tar로 옮긴다\n move(src, tar);\n // 부분 문제 f(i-1): src를 이용하여 buf의 맨 위 원판 i-1개를 tar로 옮긴다\n dfs(i - 1, buf, src, tar);\n}\n\n/* 하노이의 탑 문제 풀기 */\nfunction solveHanota(A: number[], B: number[], C: number[]): void {\n const n = A.length;\n // B를 이용하여 A의 맨 위 원판 n개를 C로 옮긴다\n dfs(n, A, B, C);\n}\n</code></pre> hanota.dart<pre><code>/* 원판 하나 이동 */\nvoid move(List&lt;int&gt; src, List&lt;int&gt; tar) {\n // src의 맨 위에서 원판 하나를 꺼낸다\n int pan = src.removeLast();\n // 원판을 tar의 맨 위에 놓는다\n tar.add(pan);\n}\n\n/* 하노이의 탑 문제 f(i) 풀기 */\nvoid dfs(int i, List&lt;int&gt; src, List&lt;int&gt; buf, List&lt;int&gt; tar) {\n // src에 원판이 하나만 남아 있으면 바로 tar로 옮긴다\n if (i == 1) {\n move(src, tar);\n return;\n }\n // 부분 문제 f(i-1): tar를 이용하여 src의 맨 위 원판 i-1개를 buf로 옮긴다\n dfs(i - 1, src, tar, buf);\n // 부분 문제 f(1): src에 남은 원판 하나를 tar로 옮긴다\n move(src, tar);\n // 부분 문제 f(i-1): src를 이용하여 buf의 맨 위 원판 i-1개를 tar로 옮긴다\n dfs(i - 1, buf, src, tar);\n}\n\n/* 하노이의 탑 문제 풀기 */\nvoid solveHanota(List&lt;int&gt; A, List&lt;int&gt; B, List&lt;int&gt; C) {\n int n = A.length;\n // B를 이용하여 A의 맨 위 원판 n개를 C로 옮긴다\n dfs(n, A, B, C);\n}\n</code></pre> hanota.rs<pre><code>/* 원판 하나 이동 */\nfn move_pan(src: &amp;mut Vec&lt;i32&gt;, tar: &amp;mut Vec&lt;i32&gt;) {\n // src의 맨 위에서 원판 하나를 꺼낸다\n let pan = src.pop().unwrap();\n // 원판을 tar의 맨 위에 놓는다\n tar.push(pan);\n}\n\n/* 하노이의 탑 문제 f(i) 풀기 */\nfn dfs(i: i32, src: &amp;mut Vec&lt;i32&gt;, buf: &amp;mut Vec&lt;i32&gt;, tar: &amp;mut Vec&lt;i32&gt;) {\n // src에 원판이 하나만 남아 있으면 바로 tar로 옮긴다\n if i == 1 {\n move_pan(src, tar);\n return;\n }\n // 부분 문제 f(i-1): tar를 이용하여 src의 맨 위 원판 i-1개를 buf로 옮긴다\n dfs(i - 1, src, tar, buf);\n // 부분 문제 f(1): src에 남은 원판 하나를 tar로 옮긴다\n move_pan(src, tar);\n // 부분 문제 f(i-1): src를 이용하여 buf의 맨 위 원판 i-1개를 tar로 옮긴다\n dfs(i - 1, buf, src, tar);\n}\n\n/* 하노이의 탑 문제 풀기 */\nfn solve_hanota(A: &amp;mut Vec&lt;i32&gt;, B: &amp;mut Vec&lt;i32&gt;, C: &amp;mut Vec&lt;i32&gt;) {\n let n = A.len() as i32;\n // B를 이용하여 A의 맨 위 원판 n개를 C로 옮긴다\n dfs(n, A, B, C);\n}\n</code></pre> hanota.c<pre><code>/* 원판 하나 이동 */\nvoid move(int *src, int *srcSize, int *tar, int *tarSize) {\n // src의 맨 위에서 원판 하나를 꺼낸다\n int pan = src[*srcSize - 1];\n src[*srcSize - 1] = 0;\n (*srcSize)--;\n // 원판을 tar의 맨 위에 놓는다\n tar[*tarSize] = pan;\n (*tarSize)++;\n}\n\n/* 하노이의 탑 문제 f(i) 풀기 */\nvoid dfs(int i, int *src, int *srcSize, int *buf, int *bufSize, int *tar, int *tarSize) {\n // src에 원판이 하나만 남아 있으면 바로 tar로 옮긴다\n if (i == 1) {\n move(src, srcSize, tar, tarSize);\n return;\n }\n // 부분 문제 f(i-1): tar를 이용하여 src의 맨 위 원판 i-1개를 buf로 옮긴다\n dfs(i - 1, src, srcSize, tar, tarSize, buf, bufSize);\n // 부분 문제 f(1): src에 남은 원판 하나를 tar로 옮긴다\n move(src, srcSize, tar, tarSize);\n // 부분 문제 f(i-1): src를 이용하여 buf의 맨 위 원판 i-1개를 tar로 옮긴다\n dfs(i - 1, buf, bufSize, src, srcSize, tar, tarSize);\n}\n\n/* 하노이의 탑 문제 풀기 */\nvoid solveHanota(int *A, int *ASize, int *B, int *BSize, int *C, int *CSize) {\n // B를 이용하여 A의 맨 위 원판 n개를 C로 옮긴다\n dfs(*ASize, A, ASize, B, BSize, C, CSize);\n}\n</code></pre> hanota.kt<pre><code>/* 원판 하나 이동 */\nfun move(src: MutableList&lt;Int&gt;, tar: MutableList&lt;Int&gt;) {\n // src의 맨 위에서 원판 하나를 꺼낸다\n val pan = src.removeAt(src.size - 1)\n // 원판을 tar의 맨 위에 놓는다\n tar.add(pan)\n}\n\n/* 하노이의 탑 문제 f(i) 풀기 */\nfun dfs(i: Int, src: MutableList&lt;Int&gt;, buf: MutableList&lt;Int&gt;, tar: MutableList&lt;Int&gt;) {\n // src에 원판이 하나만 남아 있으면 바로 tar로 옮긴다\n if (i == 1) {\n move(src, tar)\n return\n }\n // 부분 문제 f(i-1): tar를 이용하여 src의 맨 위 원판 i-1개를 buf로 옮긴다\n dfs(i - 1, src, tar, buf)\n // 부분 문제 f(1): src에 남은 원판 하나를 tar로 옮긴다\n move(src, tar)\n // 부분 문제 f(i-1): src를 이용하여 buf의 맨 위 원판 i-1개를 tar로 옮긴다\n dfs(i - 1, buf, src, tar)\n}\n\n/* 하노이의 탑 문제 풀기 */\nfun solveHanota(A: MutableList&lt;Int&gt;, B: MutableList&lt;Int&gt;, C: MutableList&lt;Int&gt;) {\n val n = A.size\n // B를 이용하여 A의 맨 위 원판 n개를 C로 옮긴다\n dfs(n, A, B, C)\n}\n</code></pre> hanota.rb<pre><code>### 원판 하나 이동 ###\ndef move(src, tar)\n # src의 맨 위에서 원판 하나를 꺼낸다\n pan = src.pop\n # 원판을 tar의 맨 위에 놓는다\n tar &lt;&lt; pan\nend\n\n### 하노이의 탑 문제 f(i) 풀기 ###\ndef dfs(i, src, buf, tar)\n # src에 원판이 하나만 남아 있으면 바로 tar로 옮긴다\n if i == 1\n move(src, tar)\n return\n end\n\n # 부분 문제 f(i-1): tar를 이용하여 src의 맨 위 원판 i-1개를 buf로 옮긴다\n dfs(i - 1, src, tar, buf)\n # 부분 문제 f(1): src에 남은 원판 하나를 tar로 옮긴다\n move(src, tar)\n # 부분 문제 f(i-1): src를 이용하여 buf의 맨 위 원판 i-1개를 tar로 옮긴다\n dfs(i - 1, buf, src, tar)\nend\n\n### 하노이의 탑 문제 풀기 ###\ndef solve_hanota(_A, _B, _C)\n n = _A.length\n # B를 이용하여 A의 맨 위 원판 n개를 C로 옮긴다\n dfs(n, _A, _B, _C)\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>아래 그림(12-15)과 같이 하노이의 탑 문제는 높이가 \\(n\\)인 재귀 트리를 이룬다. 각 노드는 부분 문제 하나를 나타내며 호출된 <code>dfs()</code> 함수 하나에 대응한다. 따라서 시간 복잡도는 \\(O(2^n)\\), 공간 복잡도는 \\(O(n)\\)이다.</p> <p></p> <p> 그림 12-15   하노이의 탑 문제의 재귀 트리 </p> <p>하노이의 탑 전설</p> <p>하노이의 탑 문제에는 오래된 전설이 전해진다. 고대 인도의 한 사원에서 승려들이 세 개의 높은 다이아몬드 기둥과 크기가 서로 다른 금 원판 \\(64\\) 개를 가지고 있었다. 승려들은 원판을 계속 옮기며 마지막 원판이 올바른 위치에 놓이는 순간 세상이 끝난다고 믿었다.</p> <p>하지만 매초 원판을 하나씩 옮겨도 총 \\(2^{64} \\approx 1.84×10^{19}\\) 초, 약 \\(5850\\) 억 년이 필요하다. 이는 현재 추정하는 우주의 나이보다 훨씬 길다. 따라서 전설이 사실이라고 해도 세상의 종말을 걱정할 필요는 없을 것이다.</p>","path":["제 12 장   분할 정복","12.4   하노이의 탑 문제"],"tags":[]},{"location":"chapter_divide_and_conquer/summary/","level":1,"title":"12.5   요약","text":"","path":["제 12 장   분할 정복","12.5   요약"],"tags":[]},{"location":"chapter_divide_and_conquer/summary/#1","level":3,"title":"1.   핵심 정리","text":"<ul> <li>분할 정복은 흔히 쓰이는 알고리즘 설계 전략이다. 분할(나누기)과 정복(합치기)의 두 단계로 이루어지며 보통 재귀로 구현한다.</li> <li>분할 정복 문제를 판단하는 기준은 문제를 나눌 수 있는지, 부분 문제들이 독립적인지, 부분 문제의 해를 합칠 수 있는지다.</li> <li>병합 정렬은 분할 정복 전략의 대표적인 응용이다. 배열을 같은 길이의 두 부분 배열로 재귀적으로 나누고, 원소가 하나만 남으면 단계적으로 병합하여 정렬한다.</li> <li>분할 정복 전략을 도입하면 알고리즘 효율이 높아지는 경우가 많다. 연산 수를 줄일 수 있고, 시스템의 병렬 최적화에도 유리하기 때문이다.</li> <li>분할 정복은 여러 알고리즘 문제를 해결할 뿐 아니라 자료 구조와 알고리즘의 설계에도 널리 사용되어 곳곳에서 찾아볼 수 있다.</li> <li>적응형 탐색은 완전 탐색보다 효율적이다. 시간 복잡도가 \\(O(\\log n)\\)인 탐색 알고리즘은 보통 분할 정복 전략을 기반으로 구현된다.</li> <li>이진 탐색은 분할 정복 전략의 또 다른 대표적인 응용으로, 부분 문제의 해를 합치는 단계가 없다. 재귀적 분할 정복으로 이진 탐색을 구현할 수 있다.</li> <li>이진 트리 구성 문제에서는 트리를 구성하는 원래 문제를 왼쪽 서브트리와 오른쪽 서브트리를 구성하는 부분 문제로 나눌 수 있다. 전위 순회와 중위 순회의 인덱스 구간을 나누어 구현한다.</li> <li>하노이의 탑에서는 크기 \\(n\\)인 문제를 크기 \\(n-1\\)인 부분 문제 두 개와 크기 \\(1\\)인 부분 문제 하나로 나눌 수 있다. 세 부분 문제를 순서대로 해결하면 원래 문제도 해결된다.</li> </ul>","path":["제 12 장   분할 정복","12.5   요약"],"tags":[]},{"location":"chapter_dynamic_programming/","level":1,"title":"제 14 장   동적 계획법","text":"<p>이 장을 시작하며</p> <p>시냇물은 강으로 모이고, 강물은 바다로 흘러든다.</p> <p>동적 계획법은 작은 문제의 해를 모아 큰 문제의 답을 만들며, 한 걸음씩 문제 해결의 건너편으로 우리를 이끈다.</p>","path":["제 14 장   동적 계획법"],"tags":[]},{"location":"chapter_dynamic_programming/#_1","level":2,"title":"이 장의 내용","text":"<ul> <li>14.1   동적 계획법 첫걸음</li> <li>14.2   동적 계획법 문제의 특성</li> <li>14.3   동적 계획법 문제 풀이 방법</li> <li>14.4   0-1 배낭 문제</li> <li>14.5   무한 배낭 문제</li> <li>14.6   편집 거리 문제</li> <li>14.7   요약</li> <li>14.8   연습 문제</li> </ul>","path":["제 14 장   동적 계획법"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/","level":1,"title":"14.2   동적 계획법 문제의 특성","text":"<p>앞 절에서는 부분 문제로 나누어 원래 문제를 푸는 동적 계획법을 배웠다. 부분 문제로 나누는 것은 일반적인 알고리즘 아이디어지만 분할 정복, 동적 계획법, 백트래킹에서는 강조점이 다르다.</p> <ul> <li>분할 정복은 원래 문제를 독립적인 여러 부분 문제로 재귀적으로 나누어 가장 작은 부분 문제까지 내려간다. 이후 되돌아오면서 해를 합쳐 원래 문제의 해를 얻는다.</li> <li>동적 계획법도 문제를 재귀적으로 나누지만, 분할 정복과 달리 부분 문제들이 서로 의존하며 분해 과정에서 중복 부분 문제가 많이 생긴다.</li> <li>백트래킹은 시도와 되돌리기로 가능한 모든 해를 열거하고 가지치기로 불필요한 탐색 가지를 피한다. 원래 문제의 해는 일련의 의사 결정 단계로 이루어지며, 각 결정 단계에 앞선 부분 수열을 부분 문제로 볼 수 있다.</li> </ul> <p>동적 계획법은 최적화 문제에 자주 쓰인다. 이러한 문제에는 중복 부분 문제뿐 아니라 최적 부분 구조와 무후효성이라는 두 가지 특성이 더 있다.</p>","path":["제 14 장   동적 계획법","14.2   동적 계획법 문제의 특성"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/#1421","level":2,"title":"14.2.1   최적 부분 구조","text":"<p>최적 부분 구조의 개념을 더 잘 보여 주도록 계단 오르기 문제를 조금 바꿔 보자.</p> <p>계단 오르기의 최소 비용</p> <p>계단을 한 번에 \\(1\\) 칸 또는 \\(2\\) 칸 오를 수 있다. 각 칸에는 그곳에서 지불할 비용인 음이 아닌 정수가 적혀 있다. 음이 아닌 정수 배열 \\(cost\\)가 주어지며, \\(cost[i]\\)는 \\(i\\) 번째 칸의 비용이고 \\(cost[0]\\)은 바닥(출발점)에 해당한다. 맨 위에 도달하는 데 필요한 최소 비용을 구하라.</p> <p>아래 그림(14-6)과 같이 \\(1\\), \\(2\\), \\(3\\) 번째 칸의 비용이 각각 \\(1\\), \\(10\\), \\(1\\)이면 바닥에서 \\(3\\) 번째 칸까지 오르는 최소 비용은 \\(2\\)다.</p> <p></p> <p> 그림 14-6   3번째 칸까지 오르는 최소 비용 </p> <p>\\(i\\) 번째 칸까지 오르는 데 지불한 누적 비용을 \\(dp[i]\\)라고 하자. \\(i\\) 번째 칸에는 \\(i - 1\\) 번째 또는 \\(i - 2\\) 번째 칸에서만 올 수 있으므로 \\(dp[i]\\)는 \\(dp[i - 1] + cost[i]\\) 또는 \\(dp[i - 2] + cost[i]\\)다. 비용을 최소화하려면 둘 중 작은 값을 선택해야 한다.</p> \\[ dp[i] = \\min(dp[i-1], dp[i-2]) + cost[i] \\] <p>여기서 최적 부분 구조의 의미를 알 수 있다. 원래 문제의 최적해는 부분 문제의 최적해로 구성된다.</p> <p>이 문제는 분명히 최적 부분 구조를 가진다. 두 부분 문제의 최적해 \\(dp[i-1]\\)과 \\(dp[i-2]\\) 중 더 좋은 것을 골라 원래 문제 \\(dp[i]\\)의 최적해를 구성하기 때문이다.</p> <p>그렇다면 앞 절의 계단 오르기 문제도 최적 부분 구조를 가질까? 방법 수를 구하는 것이 목표이므로 단순한 계수 문제처럼 보인다. 하지만 '최대 방법 수를 구하라'고 바꿔 보면 문제의 의미는 같은데 최적 부분 구조가 드러난다. \\(n\\) 번째 칸의 최대 방법 수는 \\(n-1\\) 번째와 \\(n-2\\) 번째 칸의 최대 방법 수를 더한 값이다. 이처럼 최적 부분 구조의 해석은 유연하며 문제마다 다른 의미를 가질 수 있다.</p> <p>상태 전이 식과 초기 상태 \\(dp[1] = cost[1]\\), \\(dp[2] = cost[2]\\)를 이용하면 다음 동적 계획법 코드를 얻는다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_cost_climbing_stairs_dp.py<pre><code>def min_cost_climbing_stairs_dp(cost: list[int]) -&gt; int:\n \"\"\"계단 오르기의 최소 비용: 동적 계획법\"\"\"\n n = len(cost) - 1\n if n == 1 or n == 2:\n return cost[n]\n # 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n dp = [0] * (n + 1)\n # 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1], dp[2] = cost[1], cost[2]\n # 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for i in range(3, n + 1):\n dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]\n return dp[n]\n</code></pre> min_cost_climbing_stairs_dp.cpp<pre><code>/* 계단 오르기의 최소 비용: 동적 계획법 */\nint minCostClimbingStairsDP(vector&lt;int&gt; &amp;cost) {\n int n = cost.size() - 1;\n if (n == 1 || n == 2)\n return cost[n];\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n vector&lt;int&gt; dp(n + 1);\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1] = cost[1];\n dp[2] = cost[2];\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for (int i = 3; i &lt;= n; i++) {\n dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];\n }\n return dp[n];\n}\n</code></pre> min_cost_climbing_stairs_dp.java<pre><code>/* 계단 오르기의 최소 비용: 동적 계획법 */\nint minCostClimbingStairsDP(int[] cost) {\n int n = cost.length - 1;\n if (n == 1 || n == 2)\n return cost[n];\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n int[] dp = new int[n + 1];\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1] = cost[1];\n dp[2] = cost[2];\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for (int i = 3; i &lt;= n; i++) {\n dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];\n }\n return dp[n];\n}\n</code></pre> min_cost_climbing_stairs_dp.cs<pre><code>/* 계단 오르기의 최소 비용: 동적 계획법 */\nint MinCostClimbingStairsDP(int[] cost) {\n int n = cost.Length - 1;\n if (n == 1 || n == 2)\n return cost[n];\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n int[] dp = new int[n + 1];\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1] = cost[1];\n dp[2] = cost[2];\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for (int i = 3; i &lt;= n; i++) {\n dp[i] = Math.Min(dp[i - 1], dp[i - 2]) + cost[i];\n }\n return dp[n];\n}\n</code></pre> min_cost_climbing_stairs_dp.go<pre><code>/* 계단 오르기의 최소 비용: 동적 계획법 */\nfunc minCostClimbingStairsDP(cost []int) int {\n n := len(cost) - 1\n if n == 1 || n == 2 {\n return cost[n]\n }\n min := func(a, b int) int {\n if a &lt; b {\n return a\n }\n return b\n }\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n dp := make([]int, n+1)\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1] = cost[1]\n dp[2] = cost[2]\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for i := 3; i &lt;= n; i++ {\n dp[i] = min(dp[i-1], dp[i-2]) + cost[i]\n }\n return dp[n]\n}\n</code></pre> min_cost_climbing_stairs_dp.swift<pre><code>/* 계단 오르기의 최소 비용: 동적 계획법 */\nfunc minCostClimbingStairsDP(cost: [Int]) -&gt; Int {\n let n = cost.count - 1\n if n == 1 || n == 2 {\n return cost[n]\n }\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n var dp = Array(repeating: 0, count: n + 1)\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1] = cost[1]\n dp[2] = cost[2]\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for i in 3 ... n {\n dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]\n }\n return dp[n]\n}\n</code></pre> min_cost_climbing_stairs_dp.js<pre><code>/* 계단 오르기의 최소 비용: 동적 계획법 */\nfunction minCostClimbingStairsDP(cost) {\n const n = cost.length - 1;\n if (n === 1 || n === 2) {\n return cost[n];\n }\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n const dp = new Array(n + 1);\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1] = cost[1];\n dp[2] = cost[2];\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for (let i = 3; i &lt;= n; i++) {\n dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];\n }\n return dp[n];\n}\n</code></pre> min_cost_climbing_stairs_dp.ts<pre><code>/* 계단 오르기의 최소 비용: 동적 계획법 */\nfunction minCostClimbingStairsDP(cost: Array&lt;number&gt;): number {\n const n = cost.length - 1;\n if (n === 1 || n === 2) {\n return cost[n];\n }\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n const dp = new Array(n + 1);\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1] = cost[1];\n dp[2] = cost[2];\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for (let i = 3; i &lt;= n; i++) {\n dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];\n }\n return dp[n];\n}\n</code></pre> min_cost_climbing_stairs_dp.dart<pre><code>/* 계단 오르기의 최소 비용: 동적 계획법 */\nint minCostClimbingStairsDP(List&lt;int&gt; cost) {\n int n = cost.length - 1;\n if (n == 1 || n == 2) return cost[n];\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n List&lt;int&gt; dp = List.filled(n + 1, 0);\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1] = cost[1];\n dp[2] = cost[2];\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for (int i = 3; i &lt;= n; i++) {\n dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];\n }\n return dp[n];\n}\n</code></pre> min_cost_climbing_stairs_dp.rs<pre><code>/* 계단 오르기의 최소 비용: 동적 계획법 */\nfn min_cost_climbing_stairs_dp(cost: &amp;[i32]) -&gt; i32 {\n let n = cost.len() - 1;\n if n == 1 || n == 2 {\n return cost[n];\n }\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n let mut dp = vec![-1; n + 1];\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1] = cost[1];\n dp[2] = cost[2];\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for i in 3..=n {\n dp[i] = cmp::min(dp[i - 1], dp[i - 2]) + cost[i];\n }\n dp[n]\n}\n</code></pre> min_cost_climbing_stairs_dp.c<pre><code>/* 계단 오르기의 최소 비용: 동적 계획법 */\nint minCostClimbingStairsDP(int cost[], int costSize) {\n int n = costSize - 1;\n if (n == 1 || n == 2)\n return cost[n];\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n int *dp = calloc(n + 1, sizeof(int));\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1] = cost[1];\n dp[2] = cost[2];\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for (int i = 3; i &lt;= n; i++) {\n dp[i] = myMin(dp[i - 1], dp[i - 2]) + cost[i];\n }\n int res = dp[n];\n // 메모리 해제\n free(dp);\n return res;\n}\n</code></pre> min_cost_climbing_stairs_dp.kt<pre><code>/* 계단 오르기의 최소 비용: 동적 계획법 */\nfun minCostClimbingStairsDP(cost: IntArray): Int {\n val n = cost.size - 1\n if (n == 1 || n == 2) return cost[n]\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n val dp = IntArray(n + 1)\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1] = cost[1]\n dp[2] = cost[2]\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for (i in 3..n) {\n dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]\n }\n return dp[n]\n}\n</code></pre> min_cost_climbing_stairs_dp.rb<pre><code>### 계단 오르기의 최소 비용: 동적 계획법 ###\ndef min_cost_climbing_stairs_dp(cost)\n n = cost.length - 1\n return cost[n] if n == 1 || n == 2\n # 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n dp = Array.new(n + 1, 0)\n # 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1], dp[2] = cost[1], cost[2]\n # 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n (3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }\n dp[n]\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>아래 그림(14-7)은 위 코드의 동적 계획법 과정이다.</p> <p></p> <p> 그림 14-7   계단 오르기 최소 비용의 동적 계획법 과정 </p> <p>이 문제도 공간을 최적화하여 1차원을 0차원으로 압축하고, 공간 복잡도를 \\(O(n)\\)에서 \\(O(1)\\)로 줄일 수 있다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_cost_climbing_stairs_dp.py<pre><code>def min_cost_climbing_stairs_dp_comp(cost: list[int]) -&gt; int:\n \"\"\"계단 오르기의 최소 비용: 공간을 최적화한 동적 계획법\"\"\"\n n = len(cost) - 1\n if n == 1 or n == 2:\n return cost[n]\n a, b = cost[1], cost[2]\n for i in range(3, n + 1):\n a, b = b, min(a, b) + cost[i]\n return b\n</code></pre> min_cost_climbing_stairs_dp.cpp<pre><code>/* 계단 오르기의 최소 비용: 공간을 최적화한 동적 계획법 */\nint minCostClimbingStairsDPComp(vector&lt;int&gt; &amp;cost) {\n int n = cost.size() - 1;\n if (n == 1 || n == 2)\n return cost[n];\n int a = cost[1], b = cost[2];\n for (int i = 3; i &lt;= n; i++) {\n int tmp = b;\n b = min(a, tmp) + cost[i];\n a = tmp;\n }\n return b;\n}\n</code></pre> min_cost_climbing_stairs_dp.java<pre><code>/* 계단 오르기의 최소 비용: 공간을 최적화한 동적 계획법 */\nint minCostClimbingStairsDPComp(int[] cost) {\n int n = cost.length - 1;\n if (n == 1 || n == 2)\n return cost[n];\n int a = cost[1], b = cost[2];\n for (int i = 3; i &lt;= n; i++) {\n int tmp = b;\n b = Math.min(a, tmp) + cost[i];\n a = tmp;\n }\n return b;\n}\n</code></pre> min_cost_climbing_stairs_dp.cs<pre><code>/* 계단 오르기의 최소 비용: 공간을 최적화한 동적 계획법 */\nint MinCostClimbingStairsDPComp(int[] cost) {\n int n = cost.Length - 1;\n if (n == 1 || n == 2)\n return cost[n];\n int a = cost[1], b = cost[2];\n for (int i = 3; i &lt;= n; i++) {\n int tmp = b;\n b = Math.Min(a, tmp) + cost[i];\n a = tmp;\n }\n return b;\n}\n</code></pre> min_cost_climbing_stairs_dp.go<pre><code>/* 계단 오르기의 최소 비용: 공간을 최적화한 동적 계획법 */\nfunc minCostClimbingStairsDPComp(cost []int) int {\n n := len(cost) - 1\n if n == 1 || n == 2 {\n return cost[n]\n }\n min := func(a, b int) int {\n if a &lt; b {\n return a\n }\n return b\n }\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n a, b := cost[1], cost[2]\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for i := 3; i &lt;= n; i++ {\n tmp := b\n b = min(a, tmp) + cost[i]\n a = tmp\n }\n return b\n}\n</code></pre> min_cost_climbing_stairs_dp.swift<pre><code>/* 계단 오르기의 최소 비용: 공간을 최적화한 동적 계획법 */\nfunc minCostClimbingStairsDPComp(cost: [Int]) -&gt; Int {\n let n = cost.count - 1\n if n == 1 || n == 2 {\n return cost[n]\n }\n var (a, b) = (cost[1], cost[2])\n for i in 3 ... n {\n (a, b) = (b, min(a, b) + cost[i])\n }\n return b\n}\n</code></pre> min_cost_climbing_stairs_dp.js<pre><code>/* 계단 오르기의 최소 비용: 공간을 최적화한 동적 계획법 */\nfunction minCostClimbingStairsDPComp(cost) {\n const n = cost.length - 1;\n if (n === 1 || n === 2) {\n return cost[n];\n }\n let a = cost[1],\n b = cost[2];\n for (let i = 3; i &lt;= n; i++) {\n const tmp = b;\n b = Math.min(a, tmp) + cost[i];\n a = tmp;\n }\n return b;\n}\n</code></pre> min_cost_climbing_stairs_dp.ts<pre><code>/* 계단 오르기의 최소 비용: 공간을 최적화한 동적 계획법 */\nfunction minCostClimbingStairsDPComp(cost: Array&lt;number&gt;): number {\n const n = cost.length - 1;\n if (n === 1 || n === 2) {\n return cost[n];\n }\n let a = cost[1],\n b = cost[2];\n for (let i = 3; i &lt;= n; i++) {\n const tmp = b;\n b = Math.min(a, tmp) + cost[i];\n a = tmp;\n }\n return b;\n}\n</code></pre> min_cost_climbing_stairs_dp.dart<pre><code>/* 계단 오르기의 최소 비용: 공간을 최적화한 동적 계획법 */\nint minCostClimbingStairsDPComp(List&lt;int&gt; cost) {\n int n = cost.length - 1;\n if (n == 1 || n == 2) return cost[n];\n int a = cost[1], b = cost[2];\n for (int i = 3; i &lt;= n; i++) {\n int tmp = b;\n b = min(a, tmp) + cost[i];\n a = tmp;\n }\n return b;\n}\n</code></pre> min_cost_climbing_stairs_dp.rs<pre><code>/* 계단 오르기의 최소 비용: 공간을 최적화한 동적 계획법 */\nfn min_cost_climbing_stairs_dp_comp(cost: &amp;[i32]) -&gt; i32 {\n let n = cost.len() - 1;\n if n == 1 || n == 2 {\n return cost[n];\n };\n let (mut a, mut b) = (cost[1], cost[2]);\n for i in 3..=n {\n let tmp = b;\n b = cmp::min(a, tmp) + cost[i];\n a = tmp;\n }\n b\n}\n</code></pre> min_cost_climbing_stairs_dp.c<pre><code>/* 계단 오르기의 최소 비용: 공간을 최적화한 동적 계획법 */\nint minCostClimbingStairsDPComp(int cost[], int costSize) {\n int n = costSize - 1;\n if (n == 1 || n == 2)\n return cost[n];\n int a = cost[1], b = cost[2];\n for (int i = 3; i &lt;= n; i++) {\n int tmp = b;\n b = myMin(a, tmp) + cost[i];\n a = tmp;\n }\n return b;\n}\n</code></pre> min_cost_climbing_stairs_dp.kt<pre><code>/* 계단 오르기의 최소 비용: 공간을 최적화한 동적 계획법 */\nfun minCostClimbingStairsDPComp(cost: IntArray): Int {\n val n = cost.size - 1\n if (n == 1 || n == 2) return cost[n]\n var a = cost[1]\n var b = cost[2]\n for (i in 3..n) {\n val tmp = b\n b = min(a, tmp) + cost[i]\n a = tmp\n }\n return b\n}\n</code></pre> min_cost_climbing_stairs_dp.rb<pre><code>### 계단 오르기의 최소 비용: 동적 계획법 ###\ndef min_cost_climbing_stairs_dp(cost)\n n = cost.length - 1\n return cost[n] if n == 1 || n == 2\n # 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n dp = Array.new(n + 1, 0)\n # 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1], dp[2] = cost[1], cost[2]\n # 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n (3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }\n dp[n]\nend\n\n# 계단 오르기의 최소 비용: 공간을 최적화한 동적 계획법\ndef min_cost_climbing_stairs_dp_comp(cost)\n n = cost.length - 1\n return cost[n] if n == 1 || n == 2\n a, b = cost[1], cost[2]\n (3...(n + 1)).each { |i| a, b = b, [a, b].min + cost[i] }\n b\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 14 장   동적 계획법","14.2   동적 계획법 문제의 특성"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/#1422","level":2,"title":"14.2.2   무후효성","text":"<p>무후효성은 동적 계획법이 문제를 효과적으로 해결할 수 있게 하는 중요한 특성이다. 어떤 상태가 주어졌을 때 이후의 전개가 현재 상태에만 의존하고, 과거에 거쳐 온 모든 상태와는 무관하다는 뜻이다.</p> <p>계단 오르기에서 상태 \\(i\\)는 \\(1\\) 칸 또는 \\(2\\) 칸 오르는 선택에 따라 상태 \\(i+1\\) 또는 \\(i+2\\)로 이어진다. 두 선택을 할 때 상태 \\(i\\) 이전의 상태를 고려할 필요는 없다. 과거 상태는 \\(i\\) 이후의 전개에 영향을 주지 않기 때문이다.</p> <p>하지만 계단 오르기에 제약 하나를 추가하면 상황이 달라진다.</p> <p>제약이 있는 계단 오르기</p> <p>총 \\(n\\) 칸의 계단을 한 번에 \\(1\\) 칸 또는 \\(2\\) 칸 오를 수 있다. 단, 연속 두 번 \\(1\\) 칸씩 오를 수는 없다. 맨 위까지 오르는 방법은 몇 가지인가?</p> <p>아래 그림(14-8)에서는 \\(3\\) 번째 칸까지 오르는 유효한 방법이 \\(2\\) 가지만 남는다. \\(1\\) 칸씩 세 번 연속 오르는 방법은 제약 조건을 만족하지 않아 제외된다.</p> <p></p> <p> 그림 14-8   제약이 있을 때 3번째 칸까지 오르는 방법 수 </p> <p>이 문제에서 직전에 \\(1\\) 칸 올라왔다면 다음에는 반드시 \\(2\\) 칸 올라야 한다. 즉, 다음 선택은 현재 상태(현재 칸 번호)만으로 정해지지 않고 이전 상태(직전에 있던 칸 번호)에도 의존한다.</p> <p>따라서 기존 상태 정의로는 무후효성을 만족하지 않으며, 상태 전이 식 \\(dp[i] = dp[i-1] + dp[i-2]\\)도 더 이상 유효하지 않다. \\(dp[i-1]\\)은 이번에 \\(1\\) 칸 오르는 경우인데, 그 안에는 '직전에도 \\(1\\) 칸 올라온' 방법이 많이 들어 있다. 제약을 만족하려면 \\(dp[i-1]\\)을 그대로 \\(dp[i]\\)에 더할 수 없다.</p> <p>이를 위해 상태 정의를 확장해야 한다. 상태 \\([i, j]\\)는 현재 \\(i\\) 번째 칸에 있고 직전에 \\(j\\) 칸 올라왔음을 나타내며, \\(j \\in \\{1, 2\\}\\)이다. 이렇게 정의하면 직전에 \\(1\\) 칸 왔는지 \\(2\\) 칸 왔는지를 구분하여 현재 상태의 이전 상태를 판단할 수 있다.</p> <ul> <li>직전에 \\(1\\) 칸 왔다면 그 앞에서는 \\(2\\) 칸만 올 수 있다. 즉, \\(dp[i, 1]\\)은 \\(dp[i-1, 2]\\)에서만 전이된다.</li> <li>직전에 \\(2\\) 칸 왔다면 그 앞에서는 \\(1\\) 칸이나 \\(2\\) 칸 모두 가능하다. 즉, \\(dp[i, 2]\\)는 \\(dp[i-2, 1]\\) 또는 \\(dp[i-2, 2]\\)에서 전이된다.</li> </ul> <p>아래 그림(14-9)과 같이 이 정의에서 \\(dp[i, j]\\)는 상태 \\([i, j]\\)의 방법 수를 나타낸다. 상태 전이 식은 다음과 같다.</p> \\[ \\begin{cases} dp[i, 1] = dp[i-1, 2] \\\\ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2] \\end{cases} \\] <p></p> <p> 그림 14-9   제약을 고려한 점화 관계 </p> <p>마지막으로 \\(dp[n, 1] + dp[n, 2]\\)를 반환하면 된다. 두 값의 합은 \\(n\\) 번째 칸까지 오르는 전체 방법 수다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_constraint_dp.py<pre><code>def climbing_stairs_constraint_dp(n: int) -&gt; int:\n \"\"\"제약 조건이 있는 계단 오르기: 동적 계획법\"\"\"\n if n == 1 or n == 2:\n return 1\n # 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n dp = [[0] * 3 for _ in range(n + 1)]\n # 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1][1], dp[1][2] = 1, 0\n dp[2][1], dp[2][2] = 0, 1\n # 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for i in range(3, n + 1):\n dp[i][1] = dp[i - 1][2]\n dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n return dp[n][1] + dp[n][2]\n</code></pre> climbing_stairs_constraint_dp.cpp<pre><code>/* 제약 조건이 있는 계단 오르기: 동적 계획법 */\nint climbingStairsConstraintDP(int n) {\n if (n == 1 || n == 2) {\n return 1;\n }\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n vector&lt;vector&lt;int&gt;&gt; dp(n + 1, vector&lt;int&gt;(3, 0));\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1][1] = 1;\n dp[1][2] = 0;\n dp[2][1] = 0;\n dp[2][2] = 1;\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for (int i = 3; i &lt;= n; i++) {\n dp[i][1] = dp[i - 1][2];\n dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n }\n return dp[n][1] + dp[n][2];\n}\n</code></pre> climbing_stairs_constraint_dp.java<pre><code>/* 제약 조건이 있는 계단 오르기: 동적 계획법 */\nint climbingStairsConstraintDP(int n) {\n if (n == 1 || n == 2) {\n return 1;\n }\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n int[][] dp = new int[n + 1][3];\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1][1] = 1;\n dp[1][2] = 0;\n dp[2][1] = 0;\n dp[2][2] = 1;\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for (int i = 3; i &lt;= n; i++) {\n dp[i][1] = dp[i - 1][2];\n dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n }\n return dp[n][1] + dp[n][2];\n}\n</code></pre> climbing_stairs_constraint_dp.cs<pre><code>/* 제약 조건이 있는 계단 오르기: 동적 계획법 */\nint ClimbingStairsConstraintDP(int n) {\n if (n == 1 || n == 2) {\n return 1;\n }\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n int[,] dp = new int[n + 1, 3];\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1, 1] = 1;\n dp[1, 2] = 0;\n dp[2, 1] = 0;\n dp[2, 2] = 1;\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for (int i = 3; i &lt;= n; i++) {\n dp[i, 1] = dp[i - 1, 2];\n dp[i, 2] = dp[i - 2, 1] + dp[i - 2, 2];\n }\n return dp[n, 1] + dp[n, 2];\n}\n</code></pre> climbing_stairs_constraint_dp.go<pre><code>/* 제약 조건이 있는 계단 오르기: 동적 계획법 */\nfunc climbingStairsConstraintDP(n int) int {\n if n == 1 || n == 2 {\n return 1\n }\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n dp := make([][3]int, n+1)\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1][1] = 1\n dp[1][2] = 0\n dp[2][1] = 0\n dp[2][2] = 1\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for i := 3; i &lt;= n; i++ {\n dp[i][1] = dp[i-1][2]\n dp[i][2] = dp[i-2][1] + dp[i-2][2]\n }\n return dp[n][1] + dp[n][2]\n}\n</code></pre> climbing_stairs_constraint_dp.swift<pre><code>/* 제약 조건이 있는 계단 오르기: 동적 계획법 */\nfunc climbingStairsConstraintDP(n: Int) -&gt; Int {\n if n == 1 || n == 2 {\n return 1\n }\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n var dp = Array(repeating: Array(repeating: 0, count: 3), count: n + 1)\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1][1] = 1\n dp[1][2] = 0\n dp[2][1] = 0\n dp[2][2] = 1\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for i in 3 ... n {\n dp[i][1] = dp[i - 1][2]\n dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n }\n return dp[n][1] + dp[n][2]\n}\n</code></pre> climbing_stairs_constraint_dp.js<pre><code>/* 제약 조건이 있는 계단 오르기: 동적 계획법 */\nfunction climbingStairsConstraintDP(n) {\n if (n === 1 || n === 2) {\n return 1;\n }\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n const dp = Array.from(new Array(n + 1), () =&gt; new Array(3));\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1][1] = 1;\n dp[1][2] = 0;\n dp[2][1] = 0;\n dp[2][2] = 1;\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for (let i = 3; i &lt;= n; i++) {\n dp[i][1] = dp[i - 1][2];\n dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n }\n return dp[n][1] + dp[n][2];\n}\n</code></pre> climbing_stairs_constraint_dp.ts<pre><code>/* 제약 조건이 있는 계단 오르기: 동적 계획법 */\nfunction climbingStairsConstraintDP(n: number): number {\n if (n === 1 || n === 2) {\n return 1;\n }\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n const dp = Array.from({ length: n + 1 }, () =&gt; new Array(3));\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1][1] = 1;\n dp[1][2] = 0;\n dp[2][1] = 0;\n dp[2][2] = 1;\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for (let i = 3; i &lt;= n; i++) {\n dp[i][1] = dp[i - 1][2];\n dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n }\n return dp[n][1] + dp[n][2];\n}\n</code></pre> climbing_stairs_constraint_dp.dart<pre><code>/* 제약 조건이 있는 계단 오르기: 동적 계획법 */\nint climbingStairsConstraintDP(int n) {\n if (n == 1 || n == 2) {\n return 1;\n }\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n List&lt;List&lt;int&gt;&gt; dp = List.generate(n + 1, (index) =&gt; List.filled(3, 0));\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1][1] = 1;\n dp[1][2] = 0;\n dp[2][1] = 0;\n dp[2][2] = 1;\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for (int i = 3; i &lt;= n; i++) {\n dp[i][1] = dp[i - 1][2];\n dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n }\n return dp[n][1] + dp[n][2];\n}\n</code></pre> climbing_stairs_constraint_dp.rs<pre><code>/* 제약 조건이 있는 계단 오르기: 동적 계획법 */\nfn climbing_stairs_constraint_dp(n: usize) -&gt; i32 {\n if n == 1 || n == 2 {\n return 1;\n };\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n let mut dp = vec![vec![-1; 3]; n + 1];\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1][1] = 1;\n dp[1][2] = 0;\n dp[2][1] = 0;\n dp[2][2] = 1;\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for i in 3..=n {\n dp[i][1] = dp[i - 1][2];\n dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n }\n dp[n][1] + dp[n][2]\n}\n</code></pre> climbing_stairs_constraint_dp.c<pre><code>/* 제약 조건이 있는 계단 오르기: 동적 계획법 */\nint climbingStairsConstraintDP(int n) {\n if (n == 1 || n == 2) {\n return 1;\n }\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n int **dp = malloc((n + 1) * sizeof(int *));\n for (int i = 0; i &lt;= n; i++) {\n dp[i] = calloc(3, sizeof(int));\n }\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1][1] = 1;\n dp[1][2] = 0;\n dp[2][1] = 0;\n dp[2][2] = 1;\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for (int i = 3; i &lt;= n; i++) {\n dp[i][1] = dp[i - 1][2];\n dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n }\n int res = dp[n][1] + dp[n][2];\n // 메모리 해제\n for (int i = 0; i &lt;= n; i++) {\n free(dp[i]);\n }\n free(dp);\n return res;\n}\n</code></pre> climbing_stairs_constraint_dp.kt<pre><code>/* 제약 조건이 있는 계단 오르기: 동적 계획법 */\nfun climbingStairsConstraintDP(n: Int): Int {\n if (n == 1 || n == 2) {\n return 1\n }\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n val dp = Array(n + 1) { IntArray(3) }\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1][1] = 1\n dp[1][2] = 0\n dp[2][1] = 0\n dp[2][2] = 1\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for (i in 3..n) {\n dp[i][1] = dp[i - 1][2]\n dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n }\n return dp[n][1] + dp[n][2]\n}\n</code></pre> climbing_stairs_constraint_dp.rb<pre><code>### 제약 조건이 있는 계단 오르기: 동적 계획법 ###\ndef climbing_stairs_constraint_dp(n)\n return 1 if n == 1 || n == 2\n\n # 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n dp = Array.new(n + 1) { Array.new(3, 0) }\n # 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1][1], dp[1][2] = 1, 0\n dp[2][1], dp[2][2] = 0, 1\n # 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for i in 3...(n + 1)\n dp[i][1] = dp[i - 1][2]\n dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n end\n\n dp[n][1] + dp[n][2]\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>위 예시는 이전 상태 하나만 추가로 고려하면 되므로 상태 정의를 확장하여 무후효성을 다시 만족시킬 수 있었다. 하지만 일부 문제는 '과거의 영향'이 매우 강하다.</p> <p>계단 오르기와 장애물 생성</p> <p>총 \\(n\\) 칸의 계단을 한 번에 \\(1\\) 칸 또는 \\(2\\) 칸 오를 수 있다. \\(i\\) 번째 칸에 도착하면 시스템이 자동으로 \\(2i\\) 번째 칸에 장애물을 놓으며, 이후의 모든 이동에서는 \\(2i\\) 번째 칸으로 갈 수 없다. 예를 들어 처음 두 번에 \\(2\\), \\(3\\) 번째 칸에 도착했다면 이후에는 \\(4\\), \\(6\\) 번째 칸에 갈 수 없다. 맨 위까지 오르는 방법은 몇 가지인가?</p> <p>이 문제에서 다음 이동은 과거의 모든 상태에 의존한다. 매번 이동할 때마다 더 높은 칸에 장애물이 생겨 이후 이동에 영향을 주기 때문이다. 이런 문제는 동적 계획법으로 해결하기 어려운 경우가 많다.</p> <p>외판원 문제 같은 복잡한 조합 최적화 문제 중에는 무후효성을 만족하지 않는 경우가 많다. 이런 문제에는 보통 휴리스틱 탐색, 유전 알고리즘, 강화 학습 등의 다른 방법을 사용하여 제한된 시간 안에 활용 가능한 국소 최적해를 구한다.</p>","path":["제 14 장   동적 계획법","14.2   동적 계획법 문제의 특성"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/","level":1,"title":"14.3   동적 계획법 문제 풀이 방법","text":"<p>앞의 두 절에서는 동적 계획법 문제의 주요 특성을 살펴보았다. 이제 더 실용적인 두 질문을 알아보자.</p> <ol> <li>어떤 문제가 동적 계획법 문제인지 어떻게 판단할까?</li> <li>동적 계획법 문제는 어디서 시작하며, 전체 풀이 단계는 어떻게 될까?</li> </ol>","path":["제 14 장   동적 계획법","14.3   동적 계획법 문제 풀이 방법"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1431","level":2,"title":"14.3.1   문제 판단","text":"<p>대체로 중복 부분 문제와 최적 부분 구조가 있고 무후효성을 만족하면 동적 계획법에 적합하다. 하지만 문제 설명에서 이러한 특성을 바로 끌어내기는 어렵다. 따라서 보통 조건을 완화하여 먼저 백트래킹(모든 경우 열거)으로 풀기에 적합한지 살펴본다.</p> <p>백트래킹에 적합한 문제는 보통 '의사 결정 트리 모형'을 따른다. 각 노드가 결정 하나를, 각 경로가 결정의 순서를 나타내는 트리 구조로 표현할 수 있는 문제다.</p> <p>즉, 명확한 결정 개념이 있고 연속된 결정을 통해 해가 만들어진다면 의사 결정 트리 모형을 만족하며, 보통 백트래킹으로 해결할 수 있다.</p> <p>여기에 더해 동적 계획법 문제로 판단할 때 '가산점'이 되는 요소도 있다.</p> <ul> <li>최댓값, 최솟값, 가장 많은 수, 가장 적은 수 등의 최적화 표현이 포함되어 있다.</li> <li>상태를 리스트, 다차원 행렬, 트리로 표현할 수 있고, 한 상태와 주변 상태들 사이에 점화 관계가 있다.</li> </ul> <p>반대로 '감점' 요소도 있다.</p> <ul> <li>최적해 하나가 아니라 가능한 모든 해결 방법을 찾는 것이 목표다.</li> <li>문제 설명에 순열이나 조합의 특성이 뚜렷하며, 구체적인 여러 방법을 반환해야 한다.</li> </ul> <p>의사 결정 트리 모형을 만족하면서 '가산점' 요소가 뚜렷하다면, 우선 동적 계획법 문제라고 가정하고 풀이 과정에서 확인해 볼 수 있다.</p>","path":["제 14 장   동적 계획법","14.3   동적 계획법 문제 풀이 방법"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1432","level":2,"title":"14.3.2   문제 해결 단계","text":"<p>동적 계획법의 풀이 흐름은 문제의 성격과 난이도에 따라 다르지만, 보통 결정 설명, 상태 정의, \\(dp\\) 테이블 구성, 상태 전이 식 도출, 경계 조건 결정 등의 단계를 따른다.</p> <p>단계를 더 구체적으로 보여 주기 위해 대표적인 '최소 경로 합' 문제를 예로 든다.</p> <p>문제</p> <p>\\(n \\times m\\)의 2차원 격자 <code>grid</code>가 주어진다. 각 칸에는 그 칸의 비용인 음이 아닌 정수가 들어 있다. 로봇은 왼쪽 위에서 출발하여 매번 아래 또는 오른쪽으로 한 칸씩만 이동하고, 오른쪽 아래에 도착한다. 왼쪽 위에서 오른쪽 아래까지의 최소 경로 합을 반환하라.</p> <p>아래 그림(14-10)의 격자에서는 최소 경로 합이 \\(13\\)이다.</p> <p></p> <p> 그림 14-10   최소 경로 합 예시 데이터 </p> <p>1단계: 매 차례의 결정을 생각하고 상태를 정의하여 \\(dp\\) 테이블을 얻는다</p> <p>이 문제에서 매 차례의 결정은 현재 칸에서 아래 또는 오른쪽으로 한 칸 이동하는 것이다. 현재 칸의 행과 열 인덱스가 \\([i, j]\\)라면 이동 후에는 \\([i+1, j]\\) 또는 \\([i, j+1]\\)이 된다. 따라서 상태는 행 인덱스와 열 인덱스라는 두 변수를 포함해야 하며 \\([i, j]\\)로 표시한다.</p> <p>상태 \\([i, j]\\)의 부분 문제는 출발점 \\([0, 0]\\)에서 \\([i, j]\\)까지의 최소 경로 합이고, 해를 \\(dp[i, j]\\)라고 한다.</p> <p>이로써 아래 그림(14-11)처럼 입력 격자 \\(grid\\)와 크기가 같은 2차원 \\(dp\\) 행렬을 얻는다.</p> <p></p> <p> 그림 14-11   상태 정의와 dp 테이블 </p> <p>참고</p> <p>동적 계획법과 백트래킹 과정은 결정의 순서로 표현할 수 있으며, 상태는 모든 결정 변수로 이루어진다. 상태에는 풀이의 진행 정도를 나타내는 모든 변수가 포함되어야 한다. 즉, 다음 상태를 도출하기에 충분한 정보가 있어야 한다.</p> <p>각 상태는 부분 문제 하나에 대응하며, 모든 부분 문제의 해를 저장하기 위해 \\(dp\\) 테이블을 정의한다. 상태의 독립 변수 각각이 \\(dp\\) 테이블의 한 차원이 된다. 본질적으로 \\(dp\\) 테이블은 상태와 부분 문제의 해 사이의 매핑이다.</p> <p>2단계: 최적 부분 구조를 찾아 상태 전이 식을 도출한다</p> <p>상태 \\([i, j]\\)는 위쪽 칸 \\([i-1, j]\\) 또는 왼쪽 칸 \\([i, j-1]\\)에서만 올 수 있다. 따라서 최적 부분 구조는 다음과 같다. \\([i, j]\\)까지의 최소 경로 합은 \\([i, j-1]\\)까지의 최소 경로 합과 \\([i-1, j]\\)까지의 최소 경로 합 중 작은 쪽에 의해 결정된다.</p> <p>위 분석으로 아래 그림(14-12)의 상태 전이 식을 얻는다.</p> \\[ dp[i, j] = \\min(dp[i-1, j], dp[i, j-1]) + grid[i, j] \\] <p></p> <p> 그림 14-12   최적 부분 구조와 상태 전이 식 </p> <p>참고</p> <p>정의한 \\(dp\\) 테이블을 바탕으로 원래 문제와 부분 문제의 관계를 생각하고, 부분 문제의 최적해로 원래 문제의 최적해를 만드는 방법, 즉 최적 부분 구조를 찾는다.</p> <p>최적 부분 구조를 찾으면 이를 이용해 상태 전이 식을 세울 수 있다.</p> <p>3단계: 경계 조건과 상태 전이 순서를 정한다</p> <p>이 문제에서 첫 행의 상태는 왼쪽 상태에서만, 첫 열의 상태는 위쪽 상태에서만 올 수 있다. 따라서 첫 행 \\(i = 0\\)과 첫 열 \\(j = 0\\)이 경계 조건이다.</p> <p>아래 그림(14-13)과 같이 각 칸은 왼쪽 칸과 위쪽 칸에서 전이되므로, 바깥 반복문은 행을, 안쪽 반복문은 열을 순회한다.</p> <p></p> <p> 그림 14-13   경계 조건과 상태 전이 순서 </p> <p>참고</p> <p>경계 조건은 동적 계획법에서 \\(dp\\) 테이블을 초기화하는 데, 탐색에서는 가지치기에 사용한다.</p> <p>상태 전이 순서의 핵심은 현재 문제의 해를 계산하기 전에 그 해가 의존하는 모든 작은 부분 문제의 해가 올바르게 계산되어 있도록 하는 것이다.</p> <p>이제 동적 계획법 코드를 바로 작성할 수 있다. 하지만 부분 문제로 나누는 사고는 위에서 아래로 진행하므로, '완전 탐색 \\(\\rightarrow\\) 메모이제이션을 이용한 탐색 \\(\\rightarrow\\) 동적 계획법' 순서로 구현하는 편이 사고 흐름에 더 자연스럽다.</p>","path":["제 14 장   동적 계획법","14.3   동적 계획법 문제 풀이 방법"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1-1","level":3,"title":"1.   방법 1: 완전 탐색","text":"<p>상태 \\([i, j]\\)에서 탐색을 시작하여 더 작은 상태 \\([i-1, j]\\)와 \\([i, j-1]\\)로 계속 나눈다. 재귀 함수에는 다음 요소가 포함된다.</p> <ul> <li>재귀 매개변수: 상태 \\([i, j]\\).</li> <li>반환값: \\([0, 0]\\)에서 \\([i, j]\\)까지의 최소 경로 합 \\(dp[i, j]\\).</li> <li>종료 조건: \\(i = 0\\)이고 \\(j = 0\\)이면 비용 \\(grid[0, 0]\\)을 반환한다.</li> <li>가지치기: \\(i &lt; 0\\) 또는 \\(j &lt; 0\\)이면 인덱스가 범위를 벗어나므로 불가능함을 나타내는 비용 \\(+\\infty\\)를 반환한다.</li> </ul> <p>구현 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py<pre><code>def min_path_sum_dfs(grid: list[list[int]], i: int, j: int) -&gt; int:\n \"\"\"최소 경로 합: 완전 탐색\"\"\"\n # 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n if i == 0 and j == 0:\n return grid[0][0]\n # 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n if i &lt; 0 or j &lt; 0:\n return inf\n # 왼쪽 위 모서리에서 (i-1, j)와 (i, j-1)까지의 최소 경로 비용을 계산한다\n up = min_path_sum_dfs(grid, i - 1, j)\n left = min_path_sum_dfs(grid, i, j - 1)\n # 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 반환한다\n return min(left, up) + grid[i][j]\n</code></pre> min_path_sum.cpp<pre><code>/* 최소 경로 합: 완전 탐색 */\nint minPathSumDFS(vector&lt;vector&lt;int&gt;&gt; &amp;grid, int i, int j) {\n // 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n if (i == 0 &amp;&amp; j == 0) {\n return grid[0][0];\n }\n // 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n if (i &lt; 0 || j &lt; 0) {\n return INT_MAX;\n }\n // 왼쪽 위 모서리에서 (i-1, j)와 (i, j-1)까지의 최소 경로 비용을 계산한다\n int up = minPathSumDFS(grid, i - 1, j);\n int left = minPathSumDFS(grid, i, j - 1);\n // 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 반환한다\n return min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX;\n}\n</code></pre> min_path_sum.java<pre><code>/* 최소 경로 합: 완전 탐색 */\nint minPathSumDFS(int[][] grid, int i, int j) {\n // 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n if (i == 0 &amp;&amp; j == 0) {\n return grid[0][0];\n }\n // 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n if (i &lt; 0 || j &lt; 0) {\n return Integer.MAX_VALUE;\n }\n // 왼쪽 위 모서리에서 (i-1, j)와 (i, j-1)까지의 최소 경로 비용을 계산한다\n int up = minPathSumDFS(grid, i - 1, j);\n int left = minPathSumDFS(grid, i, j - 1);\n // 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 반환한다\n return Math.min(left, up) + grid[i][j];\n}\n</code></pre> min_path_sum.cs<pre><code>/* 최소 경로 합: 완전 탐색 */\nint MinPathSumDFS(int[][] grid, int i, int j) {\n // 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n if (i == 0 &amp;&amp; j == 0) {\n return grid[0][0];\n }\n // 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n if (i &lt; 0 || j &lt; 0) {\n return int.MaxValue;\n }\n // 왼쪽 위 모서리에서 (i-1, j)와 (i, j-1)까지의 최소 경로 비용을 계산한다\n int up = MinPathSumDFS(grid, i - 1, j);\n int left = MinPathSumDFS(grid, i, j - 1);\n // 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 반환한다\n return Math.Min(left, up) + grid[i][j];\n}\n</code></pre> min_path_sum.go<pre><code>/* 최소 경로 합: 완전 탐색 */\nfunc minPathSumDFS(grid [][]int, i, j int) int {\n // 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n if i == 0 &amp;&amp; j == 0 {\n return grid[0][0]\n }\n // 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n if i &lt; 0 || j &lt; 0 {\n return math.MaxInt\n }\n // 왼쪽 위 모서리에서 (i-1, j)와 (i, j-1)까지의 최소 경로 비용을 계산한다\n up := minPathSumDFS(grid, i-1, j)\n left := minPathSumDFS(grid, i, j-1)\n // 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 반환한다\n return int(math.Min(float64(left), float64(up))) + grid[i][j]\n}\n</code></pre> min_path_sum.swift<pre><code>/* 최소 경로 합: 완전 탐색 */\nfunc minPathSumDFS(grid: [[Int]], i: Int, j: Int) -&gt; Int {\n // 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n if i == 0, j == 0 {\n return grid[0][0]\n }\n // 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n if i &lt; 0 || j &lt; 0 {\n return .max\n }\n // 왼쪽 위 모서리에서 (i-1, j)와 (i, j-1)까지의 최소 경로 비용을 계산한다\n let up = minPathSumDFS(grid: grid, i: i - 1, j: j)\n let left = minPathSumDFS(grid: grid, i: i, j: j - 1)\n // 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 반환한다\n return min(left, up) + grid[i][j]\n}\n</code></pre> min_path_sum.js<pre><code>/* 최소 경로 합: 완전 탐색 */\nfunction minPathSumDFS(grid, i, j) {\n // 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n if (i === 0 &amp;&amp; j === 0) {\n return grid[0][0];\n }\n // 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n if (i &lt; 0 || j &lt; 0) {\n return Infinity;\n }\n // 왼쪽 위 모서리에서 (i-1, j)와 (i, j-1)까지의 최소 경로 비용을 계산한다\n const up = minPathSumDFS(grid, i - 1, j);\n const left = minPathSumDFS(grid, i, j - 1);\n // 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 반환한다\n return Math.min(left, up) + grid[i][j];\n}\n</code></pre> min_path_sum.ts<pre><code>/* 최소 경로 합: 완전 탐색 */\nfunction minPathSumDFS(\n grid: Array&lt;Array&lt;number&gt;&gt;,\n i: number,\n j: number\n): number {\n // 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n if (i === 0 &amp;&amp; j == 0) {\n return grid[0][0];\n }\n // 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n if (i &lt; 0 || j &lt; 0) {\n return Infinity;\n }\n // 왼쪽 위 모서리에서 (i-1, j)와 (i, j-1)까지의 최소 경로 비용을 계산한다\n const up = minPathSumDFS(grid, i - 1, j);\n const left = minPathSumDFS(grid, i, j - 1);\n // 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 반환한다\n return Math.min(left, up) + grid[i][j];\n}\n</code></pre> min_path_sum.dart<pre><code>/* 최소 경로 합: 완전 탐색 */\nint minPathSumDFS(List&lt;List&lt;int&gt;&gt; grid, int i, int j) {\n // 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n if (i == 0 &amp;&amp; j == 0) {\n return grid[0][0];\n }\n // 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n if (i &lt; 0 || j &lt; 0) {\n // Dart의 int 타입은 범위가 고정된 정수이며, 무한대를 나타내는 값이 없다\n return BigInt.from(2).pow(31).toInt();\n }\n // 왼쪽 위 모서리에서 (i-1, j)와 (i, j-1)까지의 최소 경로 비용을 계산한다\n int up = minPathSumDFS(grid, i - 1, j);\n int left = minPathSumDFS(grid, i, j - 1);\n // 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 반환한다\n return min(left, up) + grid[i][j];\n}\n</code></pre> min_path_sum.rs<pre><code>/* 최소 경로 합: 완전 탐색 */\nfn min_path_sum_dfs(grid: &amp;Vec&lt;Vec&lt;i32&gt;&gt;, i: i32, j: i32) -&gt; i32 {\n // 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n if i == 0 &amp;&amp; j == 0 {\n return grid[0][0];\n }\n // 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n if i &lt; 0 || j &lt; 0 {\n return i32::MAX;\n }\n // 왼쪽 위 모서리에서 (i-1, j)와 (i, j-1)까지의 최소 경로 비용을 계산한다\n let up = min_path_sum_dfs(grid, i - 1, j);\n let left = min_path_sum_dfs(grid, i, j - 1);\n // 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 반환한다\n std::cmp::min(left, up) + grid[i as usize][j as usize]\n}\n</code></pre> min_path_sum.c<pre><code>/* 최소 경로 합: 완전 탐색 */\nint minPathSumDFS(int grid[MAX_SIZE][MAX_SIZE], int i, int j) {\n // 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n if (i == 0 &amp;&amp; j == 0) {\n return grid[0][0];\n }\n // 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n if (i &lt; 0 || j &lt; 0) {\n return INT_MAX;\n }\n // 왼쪽 위 모서리에서 (i-1, j)와 (i, j-1)까지의 최소 경로 비용을 계산한다\n int up = minPathSumDFS(grid, i - 1, j);\n int left = minPathSumDFS(grid, i, j - 1);\n // 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 반환한다\n return myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX;\n}\n</code></pre> min_path_sum.kt<pre><code>/* 최소 경로 합: 완전 탐색 */\nfun minPathSumDFS(grid: Array&lt;IntArray&gt;, i: Int, j: Int): Int {\n // 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n if (i == 0 &amp;&amp; j == 0) {\n return grid[0][0]\n }\n // 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n if (i &lt; 0 || j &lt; 0) {\n return Int.MAX_VALUE\n }\n // 왼쪽 위 모서리에서 (i-1, j)와 (i, j-1)까지의 최소 경로 비용을 계산한다\n val up = minPathSumDFS(grid, i - 1, j)\n val left = minPathSumDFS(grid, i, j - 1)\n // 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 반환한다\n return min(left, up) + grid[i][j]\n}\n</code></pre> min_path_sum.rb<pre><code>### 최소 경로 합: 완전 탐색 ###\ndef min_path_sum_dfs(grid, i, j)\n # 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n return grid[i][j] if i == 0 &amp;&amp; j == 0\n # 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n return Float::INFINITY if i &lt; 0 || j &lt; 0\n # 왼쪽 위 모서리에서 (i-1, j)와 (i, j-1)까지의 최소 경로 비용을 계산한다\n up = min_path_sum_dfs(grid, i - 1, j)\n left = min_path_sum_dfs(grid, i, j - 1)\n # 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 반환한다\n [left, up].min + grid[i][j]\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>아래 그림(14-14)은 \\(dp[2, 1]\\)을 루트로 하는 재귀 트리다. 중복 부분 문제가 포함되어 있으며, 그 수는 격자 <code>grid</code>의 크기가 커질수록 급격히 늘어난다.</p> <p>중복 부분 문제가 생기는 근본적인 이유는 왼쪽 위에서 어떤 칸에 도달하는 경로가 여러 개 있기 때문이다.</p> <p></p> <p> 그림 14-14   완전 탐색의 재귀 트리 </p> <p>각 상태에는 아래와 오른쪽이라는 두 선택이 있고, 왼쪽 위에서 오른쪽 아래까지는 총 \\(m + n - 2\\) 번 이동해야 한다. 따라서 최악의 시간 복잡도는 \\(O(2^{m + n})\\)이며, \\(n\\)과 \\(m\\)은 각각 격자의 행 수와 열 수다. 이 계산은 경계 근처의 상황을 고려하지 않았다. 격자 경계에서는 선택이 하나뿐이므로 실제 경로 수는 이보다 적다.</p>","path":["제 14 장   동적 계획법","14.3   동적 계획법 문제 풀이 방법"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#2-2","level":3,"title":"2.   방법 2: 메모이제이션을 이용한 탐색","text":"<p>격자 <code>grid</code>와 같은 크기의 메모이제이션 리스트 <code>mem</code>을 도입하여 각 부분 문제의 해를 기록하고 중복 부분 문제를 가지치기한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py<pre><code>def min_path_sum_dfs_mem(\n grid: list[list[int]], mem: list[list[int]], i: int, j: int\n) -&gt; int:\n \"\"\"최소 경로 합: 메모이제이션을 이용한 탐색\"\"\"\n # 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n if i == 0 and j == 0:\n return grid[0][0]\n # 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n if i &lt; 0 or j &lt; 0:\n return inf\n # 기록이 이미 있으면 바로 반환한다\n if mem[i][j] != -1:\n return mem[i][j]\n # 왼쪽 칸과 위쪽 칸까지의 최소 경로 비용\n up = min_path_sum_dfs_mem(grid, mem, i - 1, j)\n left = min_path_sum_dfs_mem(grid, mem, i, j - 1)\n # 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 기록하고 반환한다\n mem[i][j] = min(left, up) + grid[i][j]\n return mem[i][j]\n</code></pre> min_path_sum.cpp<pre><code>/* 최소 경로 합: 메모이제이션을 이용한 탐색 */\nint minPathSumDFSMem(vector&lt;vector&lt;int&gt;&gt; &amp;grid, vector&lt;vector&lt;int&gt;&gt; &amp;mem, int i, int j) {\n // 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n if (i == 0 &amp;&amp; j == 0) {\n return grid[0][0];\n }\n // 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n if (i &lt; 0 || j &lt; 0) {\n return INT_MAX;\n }\n // 기록이 이미 있으면 바로 반환한다\n if (mem[i][j] != -1) {\n return mem[i][j];\n }\n // 왼쪽 칸과 위쪽 칸까지의 최소 경로 비용\n int up = minPathSumDFSMem(grid, mem, i - 1, j);\n int left = minPathSumDFSMem(grid, mem, i, j - 1);\n // 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 기록하고 반환한다\n mem[i][j] = min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX;\n return mem[i][j];\n}\n</code></pre> min_path_sum.java<pre><code>/* 최소 경로 합: 메모이제이션을 이용한 탐색 */\nint minPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) {\n // 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n if (i == 0 &amp;&amp; j == 0) {\n return grid[0][0];\n }\n // 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n if (i &lt; 0 || j &lt; 0) {\n return Integer.MAX_VALUE;\n }\n // 기록이 이미 있으면 바로 반환한다\n if (mem[i][j] != -1) {\n return mem[i][j];\n }\n // 왼쪽 칸과 위쪽 칸까지의 최소 경로 비용\n int up = minPathSumDFSMem(grid, mem, i - 1, j);\n int left = minPathSumDFSMem(grid, mem, i, j - 1);\n // 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 기록하고 반환한다\n mem[i][j] = Math.min(left, up) + grid[i][j];\n return mem[i][j];\n}\n</code></pre> min_path_sum.cs<pre><code>/* 최소 경로 합: 메모이제이션을 이용한 탐색 */\nint MinPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) {\n // 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n if (i == 0 &amp;&amp; j == 0) {\n return grid[0][0];\n }\n // 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n if (i &lt; 0 || j &lt; 0) {\n return int.MaxValue;\n }\n // 기록이 이미 있으면 바로 반환한다\n if (mem[i][j] != -1) {\n return mem[i][j];\n }\n // 왼쪽 칸과 위쪽 칸까지의 최소 경로 비용\n int up = MinPathSumDFSMem(grid, mem, i - 1, j);\n int left = MinPathSumDFSMem(grid, mem, i, j - 1);\n // 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 기록하고 반환한다\n mem[i][j] = Math.Min(left, up) + grid[i][j];\n return mem[i][j];\n}\n</code></pre> min_path_sum.go<pre><code>/* 최소 경로 합: 메모이제이션을 이용한 탐색 */\nfunc minPathSumDFSMem(grid, mem [][]int, i, j int) int {\n // 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n if i == 0 &amp;&amp; j == 0 {\n return grid[0][0]\n }\n // 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n if i &lt; 0 || j &lt; 0 {\n return math.MaxInt\n }\n // 기록이 이미 있으면 바로 반환한다\n if mem[i][j] != -1 {\n return mem[i][j]\n }\n // 왼쪽 칸과 위쪽 칸까지의 최소 경로 비용\n up := minPathSumDFSMem(grid, mem, i-1, j)\n left := minPathSumDFSMem(grid, mem, i, j-1)\n // 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 기록하고 반환한다\n mem[i][j] = int(math.Min(float64(left), float64(up))) + grid[i][j]\n return mem[i][j]\n}\n</code></pre> min_path_sum.swift<pre><code>/* 최소 경로 합: 메모이제이션을 이용한 탐색 */\nfunc minPathSumDFSMem(grid: [[Int]], mem: inout [[Int]], i: Int, j: Int) -&gt; Int {\n // 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n if i == 0, j == 0 {\n return grid[0][0]\n }\n // 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n if i &lt; 0 || j &lt; 0 {\n return .max\n }\n // 기록이 이미 있으면 바로 반환한다\n if mem[i][j] != -1 {\n return mem[i][j]\n }\n // 왼쪽 칸과 위쪽 칸까지의 최소 경로 비용\n let up = minPathSumDFSMem(grid: grid, mem: &amp;mem, i: i - 1, j: j)\n let left = minPathSumDFSMem(grid: grid, mem: &amp;mem, i: i, j: j - 1)\n // 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 기록하고 반환한다\n mem[i][j] = min(left, up) + grid[i][j]\n return mem[i][j]\n}\n</code></pre> min_path_sum.js<pre><code>/* 최소 경로 합: 메모이제이션을 이용한 탐색 */\nfunction minPathSumDFSMem(grid, mem, i, j) {\n // 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n if (i === 0 &amp;&amp; j === 0) {\n return grid[0][0];\n }\n // 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n if (i &lt; 0 || j &lt; 0) {\n return Infinity;\n }\n // 기록이 이미 있으면 바로 반환한다\n if (mem[i][j] !== -1) {\n return mem[i][j];\n }\n // 왼쪽 칸과 위쪽 칸까지의 최소 경로 비용\n const up = minPathSumDFSMem(grid, mem, i - 1, j);\n const left = minPathSumDFSMem(grid, mem, i, j - 1);\n // 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 기록하고 반환한다\n mem[i][j] = Math.min(left, up) + grid[i][j];\n return mem[i][j];\n}\n</code></pre> min_path_sum.ts<pre><code>/* 최소 경로 합: 메모이제이션을 이용한 탐색 */\nfunction minPathSumDFSMem(\n grid: Array&lt;Array&lt;number&gt;&gt;,\n mem: Array&lt;Array&lt;number&gt;&gt;,\n i: number,\n j: number\n): number {\n // 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n if (i === 0 &amp;&amp; j === 0) {\n return grid[0][0];\n }\n // 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n if (i &lt; 0 || j &lt; 0) {\n return Infinity;\n }\n // 기록이 이미 있으면 바로 반환한다\n if (mem[i][j] != -1) {\n return mem[i][j];\n }\n // 왼쪽 칸과 위쪽 칸까지의 최소 경로 비용\n const up = minPathSumDFSMem(grid, mem, i - 1, j);\n const left = minPathSumDFSMem(grid, mem, i, j - 1);\n // 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 기록하고 반환한다\n mem[i][j] = Math.min(left, up) + grid[i][j];\n return mem[i][j];\n}\n</code></pre> min_path_sum.dart<pre><code>/* 최소 경로 합: 메모이제이션을 이용한 탐색 */\nint minPathSumDFSMem(List&lt;List&lt;int&gt;&gt; grid, List&lt;List&lt;int&gt;&gt; mem, int i, int j) {\n // 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n if (i == 0 &amp;&amp; j == 0) {\n return grid[0][0];\n }\n // 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n if (i &lt; 0 || j &lt; 0) {\n // Dart의 int 타입은 범위가 고정된 정수이며, 무한대를 나타내는 값이 없다\n return BigInt.from(2).pow(31).toInt();\n }\n // 기록이 이미 있으면 바로 반환한다\n if (mem[i][j] != -1) {\n return mem[i][j];\n }\n // 왼쪽 칸과 위쪽 칸까지의 최소 경로 비용\n int up = minPathSumDFSMem(grid, mem, i - 1, j);\n int left = minPathSumDFSMem(grid, mem, i, j - 1);\n // 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 기록하고 반환한다\n mem[i][j] = min(left, up) + grid[i][j];\n return mem[i][j];\n}\n</code></pre> min_path_sum.rs<pre><code>/* 최소 경로 합: 메모이제이션을 이용한 탐색 */\nfn min_path_sum_dfs_mem(grid: &amp;Vec&lt;Vec&lt;i32&gt;&gt;, mem: &amp;mut Vec&lt;Vec&lt;i32&gt;&gt;, i: i32, j: i32) -&gt; i32 {\n // 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n if i == 0 &amp;&amp; j == 0 {\n return grid[0][0];\n }\n // 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n if i &lt; 0 || j &lt; 0 {\n return i32::MAX;\n }\n // 기록이 이미 있으면 바로 반환한다\n if mem[i as usize][j as usize] != -1 {\n return mem[i as usize][j as usize];\n }\n // 왼쪽 칸과 위쪽 칸까지의 최소 경로 비용\n let up = min_path_sum_dfs_mem(grid, mem, i - 1, j);\n let left = min_path_sum_dfs_mem(grid, mem, i, j - 1);\n // 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 기록하고 반환한다\n mem[i as usize][j as usize] = std::cmp::min(left, up) + grid[i as usize][j as usize];\n mem[i as usize][j as usize]\n}\n</code></pre> min_path_sum.c<pre><code>/* 최소 경로 합: 메모이제이션을 이용한 탐색 */\nint minPathSumDFSMem(int grid[MAX_SIZE][MAX_SIZE], int mem[MAX_SIZE][MAX_SIZE], int i, int j) {\n // 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n if (i == 0 &amp;&amp; j == 0) {\n return grid[0][0];\n }\n // 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n if (i &lt; 0 || j &lt; 0) {\n return INT_MAX;\n }\n // 기록이 이미 있으면 바로 반환한다\n if (mem[i][j] != -1) {\n return mem[i][j];\n }\n // 왼쪽 칸과 위쪽 칸까지의 최소 경로 비용\n int up = minPathSumDFSMem(grid, mem, i - 1, j);\n int left = minPathSumDFSMem(grid, mem, i, j - 1);\n // 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 기록하고 반환한다\n mem[i][j] = myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX;\n return mem[i][j];\n}\n</code></pre> min_path_sum.kt<pre><code>/* 최소 경로 합: 메모이제이션을 이용한 탐색 */\nfun minPathSumDFSMem(\n grid: Array&lt;IntArray&gt;,\n mem: Array&lt;IntArray&gt;,\n i: Int,\n j: Int\n): Int {\n // 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n if (i == 0 &amp;&amp; j == 0) {\n return grid[0][0]\n }\n // 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n if (i &lt; 0 || j &lt; 0) {\n return Int.MAX_VALUE\n }\n // 기록이 이미 있으면 바로 반환한다\n if (mem[i][j] != -1) {\n return mem[i][j]\n }\n // 왼쪽 칸과 위쪽 칸까지의 최소 경로 비용\n val up = minPathSumDFSMem(grid, mem, i - 1, j)\n val left = minPathSumDFSMem(grid, mem, i, j - 1)\n // 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 기록하고 반환한다\n mem[i][j] = min(left, up) + grid[i][j]\n return mem[i][j]\n}\n</code></pre> min_path_sum.rb<pre><code>### 최소 경로 합: 메모이제이션을 이용한 탐색 ###\ndef min_path_sum_dfs_mem(grid, mem, i, j)\n # 왼쪽 위 모서리의 칸이면 탐색을 종료한다\n return grid[0][0] if i == 0 &amp;&amp; j == 0\n # 행 또는 열 인덱스가 범위를 벗어나면 비용 +∞를 반환한다\n return Float::INFINITY if i &lt; 0 || j &lt; 0\n # 기록이 이미 있으면 바로 반환한다\n return mem[i][j] if mem[i][j] != -1\n # 왼쪽 칸과 위쪽 칸까지의 최소 경로 비용\n up = min_path_sum_dfs_mem(grid, mem, i - 1, j)\n left = min_path_sum_dfs_mem(grid, mem, i, j - 1)\n # 왼쪽 위 모서리에서 (i, j)까지의 최소 경로 비용을 기록하고 반환한다\n mem[i][j] = [left, up].min + grid[i][j]\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>아래 그림(14-15)과 같이 메모이제이션을 도입하면 모든 부분 문제의 해를 한 번만 계산한다. 따라서 시간 복잡도는 전체 상태 수, 즉 격자 크기인 \\(O(nm)\\)에 의해 결정된다.</p> <p></p> <p> 그림 14-15   메모이제이션을 이용한 탐색의 재귀 트리 </p>","path":["제 14 장   동적 계획법","14.3   동적 계획법 문제 풀이 방법"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#3-3","level":3,"title":"3.   방법 3: 동적 계획법","text":"<p>반복을 이용한 동적 계획법 구현은 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py<pre><code>def min_path_sum_dp(grid: list[list[int]]) -&gt; int:\n \"\"\"최소 경로 합: 동적 계획법\"\"\"\n n, m = len(grid), len(grid[0])\n # dp 테이블을 초기화한다\n dp = [[0] * m for _ in range(n)]\n dp[0][0] = grid[0][0]\n # 상태 전이: 첫 번째 행\n for j in range(1, m):\n dp[0][j] = dp[0][j - 1] + grid[0][j]\n # 상태 전이: 첫 번째 열\n for i in range(1, n):\n dp[i][0] = dp[i - 1][0] + grid[i][0]\n # 상태 전이: 나머지 행과 열\n for i in range(1, n):\n for j in range(1, m):\n dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]\n return dp[n - 1][m - 1]\n</code></pre> min_path_sum.cpp<pre><code>/* 최소 경로 합: 동적 계획법 */\nint minPathSumDP(vector&lt;vector&lt;int&gt;&gt; &amp;grid) {\n int n = grid.size(), m = grid[0].size();\n // dp 테이블을 초기화한다\n vector&lt;vector&lt;int&gt;&gt; dp(n, vector&lt;int&gt;(m));\n dp[0][0] = grid[0][0];\n // 상태 전이: 첫 번째 행\n for (int j = 1; j &lt; m; j++) {\n dp[0][j] = dp[0][j - 1] + grid[0][j];\n }\n // 상태 전이: 첫 번째 열\n for (int i = 1; i &lt; n; i++) {\n dp[i][0] = dp[i - 1][0] + grid[i][0];\n }\n // 상태 전이: 나머지 행과 열\n for (int i = 1; i &lt; n; i++) {\n for (int j = 1; j &lt; m; j++) {\n dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n }\n }\n return dp[n - 1][m - 1];\n}\n</code></pre> min_path_sum.java<pre><code>/* 최소 경로 합: 동적 계획법 */\nint minPathSumDP(int[][] grid) {\n int n = grid.length, m = grid[0].length;\n // dp 테이블을 초기화한다\n int[][] dp = new int[n][m];\n dp[0][0] = grid[0][0];\n // 상태 전이: 첫 번째 행\n for (int j = 1; j &lt; m; j++) {\n dp[0][j] = dp[0][j - 1] + grid[0][j];\n }\n // 상태 전이: 첫 번째 열\n for (int i = 1; i &lt; n; i++) {\n dp[i][0] = dp[i - 1][0] + grid[i][0];\n }\n // 상태 전이: 나머지 행과 열\n for (int i = 1; i &lt; n; i++) {\n for (int j = 1; j &lt; m; j++) {\n dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n }\n }\n return dp[n - 1][m - 1];\n}\n</code></pre> min_path_sum.cs<pre><code>/* 최소 경로 합: 동적 계획법 */\nint MinPathSumDP(int[][] grid) {\n int n = grid.Length, m = grid[0].Length;\n // dp 테이블을 초기화한다\n int[,] dp = new int[n, m];\n dp[0, 0] = grid[0][0];\n // 상태 전이: 첫 번째 행\n for (int j = 1; j &lt; m; j++) {\n dp[0, j] = dp[0, j - 1] + grid[0][j];\n }\n // 상태 전이: 첫 번째 열\n for (int i = 1; i &lt; n; i++) {\n dp[i, 0] = dp[i - 1, 0] + grid[i][0];\n }\n // 상태 전이: 나머지 행과 열\n for (int i = 1; i &lt; n; i++) {\n for (int j = 1; j &lt; m; j++) {\n dp[i, j] = Math.Min(dp[i, j - 1], dp[i - 1, j]) + grid[i][j];\n }\n }\n return dp[n - 1, m - 1];\n}\n</code></pre> min_path_sum.go<pre><code>/* 최소 경로 합: 동적 계획법 */\nfunc minPathSumDP(grid [][]int) int {\n n, m := len(grid), len(grid[0])\n // dp 테이블을 초기화한다\n dp := make([][]int, n)\n for i := 0; i &lt; n; i++ {\n dp[i] = make([]int, m)\n }\n dp[0][0] = grid[0][0]\n // 상태 전이: 첫 번째 행\n for j := 1; j &lt; m; j++ {\n dp[0][j] = dp[0][j-1] + grid[0][j]\n }\n // 상태 전이: 첫 번째 열\n for i := 1; i &lt; n; i++ {\n dp[i][0] = dp[i-1][0] + grid[i][0]\n }\n // 상태 전이: 나머지 행과 열\n for i := 1; i &lt; n; i++ {\n for j := 1; j &lt; m; j++ {\n dp[i][j] = int(math.Min(float64(dp[i][j-1]), float64(dp[i-1][j]))) + grid[i][j]\n }\n }\n return dp[n-1][m-1]\n}\n</code></pre> min_path_sum.swift<pre><code>/* 최소 경로 합: 동적 계획법 */\nfunc minPathSumDP(grid: [[Int]]) -&gt; Int {\n let n = grid.count\n let m = grid[0].count\n // dp 테이블을 초기화한다\n var dp = Array(repeating: Array(repeating: 0, count: m), count: n)\n dp[0][0] = grid[0][0]\n // 상태 전이: 첫 번째 행\n for j in 1 ..&lt; m {\n dp[0][j] = dp[0][j - 1] + grid[0][j]\n }\n // 상태 전이: 첫 번째 열\n for i in 1 ..&lt; n {\n dp[i][0] = dp[i - 1][0] + grid[i][0]\n }\n // 상태 전이: 나머지 행과 열\n for i in 1 ..&lt; n {\n for j in 1 ..&lt; m {\n dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]\n }\n }\n return dp[n - 1][m - 1]\n}\n</code></pre> min_path_sum.js<pre><code>/* 최소 경로 합: 동적 계획법 */\nfunction minPathSumDP(grid) {\n const n = grid.length,\n m = grid[0].length;\n // dp 테이블을 초기화한다\n const dp = Array.from({ length: n }, () =&gt;\n Array.from({ length: m }, () =&gt; 0)\n );\n dp[0][0] = grid[0][0];\n // 상태 전이: 첫 번째 행\n for (let j = 1; j &lt; m; j++) {\n dp[0][j] = dp[0][j - 1] + grid[0][j];\n }\n // 상태 전이: 첫 번째 열\n for (let i = 1; i &lt; n; i++) {\n dp[i][0] = dp[i - 1][0] + grid[i][0];\n }\n // 상태 전이: 나머지 행과 열\n for (let i = 1; i &lt; n; i++) {\n for (let j = 1; j &lt; m; j++) {\n dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n }\n }\n return dp[n - 1][m - 1];\n}\n</code></pre> min_path_sum.ts<pre><code>/* 최소 경로 합: 동적 계획법 */\nfunction minPathSumDP(grid: Array&lt;Array&lt;number&gt;&gt;): number {\n const n = grid.length,\n m = grid[0].length;\n // dp 테이블을 초기화한다\n const dp = Array.from({ length: n }, () =&gt;\n Array.from({ length: m }, () =&gt; 0)\n );\n dp[0][0] = grid[0][0];\n // 상태 전이: 첫 번째 행\n for (let j = 1; j &lt; m; j++) {\n dp[0][j] = dp[0][j - 1] + grid[0][j];\n }\n // 상태 전이: 첫 번째 열\n for (let i = 1; i &lt; n; i++) {\n dp[i][0] = dp[i - 1][0] + grid[i][0];\n }\n // 상태 전이: 나머지 행과 열\n for (let i = 1; i &lt; n; i++) {\n for (let j: number = 1; j &lt; m; j++) {\n dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n }\n }\n return dp[n - 1][m - 1];\n}\n</code></pre> min_path_sum.dart<pre><code>/* 최소 경로 합: 동적 계획법 */\nint minPathSumDP(List&lt;List&lt;int&gt;&gt; grid) {\n int n = grid.length, m = grid[0].length;\n // dp 테이블을 초기화한다\n List&lt;List&lt;int&gt;&gt; dp = List.generate(n, (i) =&gt; List.filled(m, 0));\n dp[0][0] = grid[0][0];\n // 상태 전이: 첫 번째 행\n for (int j = 1; j &lt; m; j++) {\n dp[0][j] = dp[0][j - 1] + grid[0][j];\n }\n // 상태 전이: 첫 번째 열\n for (int i = 1; i &lt; n; i++) {\n dp[i][0] = dp[i - 1][0] + grid[i][0];\n }\n // 상태 전이: 나머지 행과 열\n for (int i = 1; i &lt; n; i++) {\n for (int j = 1; j &lt; m; j++) {\n dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n }\n }\n return dp[n - 1][m - 1];\n}\n</code></pre> min_path_sum.rs<pre><code>/* 최소 경로 합: 동적 계획법 */\nfn min_path_sum_dp(grid: &amp;Vec&lt;Vec&lt;i32&gt;&gt;) -&gt; i32 {\n let (n, m) = (grid.len(), grid[0].len());\n // dp 테이블을 초기화한다\n let mut dp = vec![vec![0; m]; n];\n dp[0][0] = grid[0][0];\n // 상태 전이: 첫 번째 행\n for j in 1..m {\n dp[0][j] = dp[0][j - 1] + grid[0][j];\n }\n // 상태 전이: 첫 번째 열\n for i in 1..n {\n dp[i][0] = dp[i - 1][0] + grid[i][0];\n }\n // 상태 전이: 나머지 행과 열\n for i in 1..n {\n for j in 1..m {\n dp[i][j] = std::cmp::min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n }\n }\n dp[n - 1][m - 1]\n}\n</code></pre> min_path_sum.c<pre><code>/* 최소 경로 합: 동적 계획법 */\nint minPathSumDP(int grid[MAX_SIZE][MAX_SIZE], int n, int m) {\n // dp 테이블을 초기화한다\n int **dp = malloc(n * sizeof(int *));\n for (int i = 0; i &lt; n; i++) {\n dp[i] = calloc(m, sizeof(int));\n }\n dp[0][0] = grid[0][0];\n // 상태 전이: 첫 번째 행\n for (int j = 1; j &lt; m; j++) {\n dp[0][j] = dp[0][j - 1] + grid[0][j];\n }\n // 상태 전이: 첫 번째 열\n for (int i = 1; i &lt; n; i++) {\n dp[i][0] = dp[i - 1][0] + grid[i][0];\n }\n // 상태 전이: 나머지 행과 열\n for (int i = 1; i &lt; n; i++) {\n for (int j = 1; j &lt; m; j++) {\n dp[i][j] = myMin(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n }\n }\n int res = dp[n - 1][m - 1];\n // 메모리 해제\n for (int i = 0; i &lt; n; i++) {\n free(dp[i]);\n }\n return res;\n}\n</code></pre> min_path_sum.kt<pre><code>/* 최소 경로 합: 동적 계획법 */\nfun minPathSumDP(grid: Array&lt;IntArray&gt;): Int {\n val n = grid.size\n val m = grid[0].size\n // dp 테이블을 초기화한다\n val dp = Array(n) { IntArray(m) }\n dp[0][0] = grid[0][0]\n // 상태 전이: 첫 번째 행\n for (j in 1..&lt;m) {\n dp[0][j] = dp[0][j - 1] + grid[0][j]\n }\n // 상태 전이: 첫 번째 열\n for (i in 1..&lt;n) {\n dp[i][0] = dp[i - 1][0] + grid[i][0]\n }\n // 상태 전이: 나머지 행과 열\n for (i in 1..&lt;n) {\n for (j in 1..&lt;m) {\n dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]\n }\n }\n return dp[n - 1][m - 1]\n}\n</code></pre> min_path_sum.rb<pre><code>### 최소 경로 합: 동적 계획법 ###\ndef min_path_sum_dp(grid)\n n, m = grid.length, grid.first.length\n # dp 테이블을 초기화한다\n dp = Array.new(n) { Array.new(m, 0) }\n dp[0][0] = grid[0][0]\n # 상태 전이: 첫 번째 행\n (1...m).each { |j| dp[0][j] = dp[0][j - 1] + grid[0][j] }\n # 상태 전이: 첫 번째 열\n (1...n).each { |i| dp[i][0] = dp[i - 1][0] + grid[i][0] }\n # 상태 전이: 나머지 행과 열\n for i in 1...n\n for j in 1...m\n dp[i][j] = [dp[i][j - 1], dp[i - 1][j]].min + grid[i][j]\n end\n end\n dp[n -1][m -1]\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>아래 그림(14-16)의 최소 경로 합 상태 전이 과정에서는 전체 격자를 순회하므로 시간 복잡도는 \\(O(nm)\\)이다.</p> <p>배열 <code>dp</code>의 크기는 \\(n \\times m\\)이므로 공간 복잡도는 \\(O(nm)\\)이다.</p> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt;&lt;6&gt;&lt;7&gt;&lt;8&gt;&lt;9&gt;&lt;10&gt;&lt;11&gt;&lt;12&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 14-16   최소 경로 합의 동적 계획법 과정 </p>","path":["제 14 장   동적 계획법","14.3   동적 계획법 문제 풀이 방법"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#4","level":3,"title":"4.   공간 최적화","text":"<p>각 칸은 왼쪽 칸과 위쪽 칸에만 의존하므로 한 행짜리 배열만으로 \\(dp\\) 테이블을 구현할 수 있다.</p> <p>배열 <code>dp</code>가 한 행의 상태만 나타내므로 첫 열의 상태를 미리 초기화할 수는 없다. 대신 각 행을 순회할 때 갱신해야 한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py<pre><code>def min_path_sum_dp_comp(grid: list[list[int]]) -&gt; int:\n \"\"\"최소 경로 합: 공간을 최적화한 동적 계획법\"\"\"\n n, m = len(grid), len(grid[0])\n # dp 테이블을 초기화한다\n dp = [0] * m\n # 상태 전이: 첫 번째 행\n dp[0] = grid[0][0]\n for j in range(1, m):\n dp[j] = dp[j - 1] + grid[0][j]\n # 상태 전이: 나머지 행\n for i in range(1, n):\n # 상태 전이: 첫 번째 열\n dp[0] = dp[0] + grid[i][0]\n # 상태 전이: 나머지 열\n for j in range(1, m):\n dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]\n return dp[m - 1]\n</code></pre> min_path_sum.cpp<pre><code>/* 최소 경로 합: 공간을 최적화한 동적 계획법 */\nint minPathSumDPComp(vector&lt;vector&lt;int&gt;&gt; &amp;grid) {\n int n = grid.size(), m = grid[0].size();\n // dp 테이블을 초기화한다\n vector&lt;int&gt; dp(m);\n // 상태 전이: 첫 번째 행\n dp[0] = grid[0][0];\n for (int j = 1; j &lt; m; j++) {\n dp[j] = dp[j - 1] + grid[0][j];\n }\n // 상태 전이: 나머지 행\n for (int i = 1; i &lt; n; i++) {\n // 상태 전이: 첫 번째 열\n dp[0] = dp[0] + grid[i][0];\n // 상태 전이: 나머지 열\n for (int j = 1; j &lt; m; j++) {\n dp[j] = min(dp[j - 1], dp[j]) + grid[i][j];\n }\n }\n return dp[m - 1];\n}\n</code></pre> min_path_sum.java<pre><code>/* 최소 경로 합: 공간을 최적화한 동적 계획법 */\nint minPathSumDPComp(int[][] grid) {\n int n = grid.length, m = grid[0].length;\n // dp 테이블을 초기화한다\n int[] dp = new int[m];\n // 상태 전이: 첫 번째 행\n dp[0] = grid[0][0];\n for (int j = 1; j &lt; m; j++) {\n dp[j] = dp[j - 1] + grid[0][j];\n }\n // 상태 전이: 나머지 행\n for (int i = 1; i &lt; n; i++) {\n // 상태 전이: 첫 번째 열\n dp[0] = dp[0] + grid[i][0];\n // 상태 전이: 나머지 열\n for (int j = 1; j &lt; m; j++) {\n dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];\n }\n }\n return dp[m - 1];\n}\n</code></pre> min_path_sum.cs<pre><code>/* 최소 경로 합: 공간을 최적화한 동적 계획법 */\nint MinPathSumDPComp(int[][] grid) {\n int n = grid.Length, m = grid[0].Length;\n // dp 테이블을 초기화한다\n int[] dp = new int[m];\n dp[0] = grid[0][0];\n // 상태 전이: 첫 번째 행\n for (int j = 1; j &lt; m; j++) {\n dp[j] = dp[j - 1] + grid[0][j];\n }\n // 상태 전이: 나머지 행\n for (int i = 1; i &lt; n; i++) {\n // 상태 전이: 첫 번째 열\n dp[0] = dp[0] + grid[i][0];\n // 상태 전이: 나머지 열\n for (int j = 1; j &lt; m; j++) {\n dp[j] = Math.Min(dp[j - 1], dp[j]) + grid[i][j];\n }\n }\n return dp[m - 1];\n}\n</code></pre> min_path_sum.go<pre><code>/* 최소 경로 합: 공간을 최적화한 동적 계획법 */\nfunc minPathSumDPComp(grid [][]int) int {\n n, m := len(grid), len(grid[0])\n // dp 테이블을 초기화한다\n dp := make([]int, m)\n // 상태 전이: 첫 번째 행\n dp[0] = grid[0][0]\n for j := 1; j &lt; m; j++ {\n dp[j] = dp[j-1] + grid[0][j]\n }\n // 상태 전이: 나머지 행과 열\n for i := 1; i &lt; n; i++ {\n // 상태 전이: 첫 번째 열\n dp[0] = dp[0] + grid[i][0]\n // 상태 전이: 나머지 열\n for j := 1; j &lt; m; j++ {\n dp[j] = int(math.Min(float64(dp[j-1]), float64(dp[j]))) + grid[i][j]\n }\n }\n return dp[m-1]\n}\n</code></pre> min_path_sum.swift<pre><code>/* 최소 경로 합: 공간을 최적화한 동적 계획법 */\nfunc minPathSumDPComp(grid: [[Int]]) -&gt; Int {\n let n = grid.count\n let m = grid[0].count\n // dp 테이블을 초기화한다\n var dp = Array(repeating: 0, count: m)\n // 상태 전이: 첫 번째 행\n dp[0] = grid[0][0]\n for j in 1 ..&lt; m {\n dp[j] = dp[j - 1] + grid[0][j]\n }\n // 상태 전이: 나머지 행\n for i in 1 ..&lt; n {\n // 상태 전이: 첫 번째 열\n dp[0] = dp[0] + grid[i][0]\n // 상태 전이: 나머지 열\n for j in 1 ..&lt; m {\n dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]\n }\n }\n return dp[m - 1]\n}\n</code></pre> min_path_sum.js<pre><code>/* 최소 경로 합: 공간을 최적화한 동적 계획법 */\nfunction minPathSumDPComp(grid) {\n const n = grid.length,\n m = grid[0].length;\n // dp 테이블을 초기화한다\n const dp = new Array(m);\n // 상태 전이: 첫 번째 행\n dp[0] = grid[0][0];\n for (let j = 1; j &lt; m; j++) {\n dp[j] = dp[j - 1] + grid[0][j];\n }\n // 상태 전이: 나머지 행\n for (let i = 1; i &lt; n; i++) {\n // 상태 전이: 첫 번째 열\n dp[0] = dp[0] + grid[i][0];\n // 상태 전이: 나머지 열\n for (let j = 1; j &lt; m; j++) {\n dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];\n }\n }\n return dp[m - 1];\n}\n</code></pre> min_path_sum.ts<pre><code>/* 최소 경로 합: 공간을 최적화한 동적 계획법 */\nfunction minPathSumDPComp(grid: Array&lt;Array&lt;number&gt;&gt;): number {\n const n = grid.length,\n m = grid[0].length;\n // dp 테이블을 초기화한다\n const dp = new Array(m);\n // 상태 전이: 첫 번째 행\n dp[0] = grid[0][0];\n for (let j = 1; j &lt; m; j++) {\n dp[j] = dp[j - 1] + grid[0][j];\n }\n // 상태 전이: 나머지 행\n for (let i = 1; i &lt; n; i++) {\n // 상태 전이: 첫 번째 열\n dp[0] = dp[0] + grid[i][0];\n // 상태 전이: 나머지 열\n for (let j = 1; j &lt; m; j++) {\n dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];\n }\n }\n return dp[m - 1];\n}\n</code></pre> min_path_sum.dart<pre><code>/* 최소 경로 합: 공간을 최적화한 동적 계획법 */\nint minPathSumDPComp(List&lt;List&lt;int&gt;&gt; grid) {\n int n = grid.length, m = grid[0].length;\n // dp 테이블을 초기화한다\n List&lt;int&gt; dp = List.filled(m, 0);\n dp[0] = grid[0][0];\n for (int j = 1; j &lt; m; j++) {\n dp[j] = dp[j - 1] + grid[0][j];\n }\n // 상태 전이: 나머지 행\n for (int i = 1; i &lt; n; i++) {\n // 상태 전이: 첫 번째 열\n dp[0] = dp[0] + grid[i][0];\n // 상태 전이: 나머지 열\n for (int j = 1; j &lt; m; j++) {\n dp[j] = min(dp[j - 1], dp[j]) + grid[i][j];\n }\n }\n return dp[m - 1];\n}\n</code></pre> min_path_sum.rs<pre><code>/* 최소 경로 합: 공간을 최적화한 동적 계획법 */\nfn min_path_sum_dp_comp(grid: &amp;Vec&lt;Vec&lt;i32&gt;&gt;) -&gt; i32 {\n let (n, m) = (grid.len(), grid[0].len());\n // dp 테이블을 초기화한다\n let mut dp = vec![0; m];\n // 상태 전이: 첫 번째 행\n dp[0] = grid[0][0];\n for j in 1..m {\n dp[j] = dp[j - 1] + grid[0][j];\n }\n // 상태 전이: 나머지 행\n for i in 1..n {\n // 상태 전이: 첫 번째 열\n dp[0] = dp[0] + grid[i][0];\n // 상태 전이: 나머지 열\n for j in 1..m {\n dp[j] = std::cmp::min(dp[j - 1], dp[j]) + grid[i][j];\n }\n }\n dp[m - 1]\n}\n</code></pre> min_path_sum.c<pre><code>/* 최소 경로 합: 공간을 최적화한 동적 계획법 */\nint minPathSumDPComp(int grid[MAX_SIZE][MAX_SIZE], int n, int m) {\n // dp 테이블을 초기화한다\n int *dp = calloc(m, sizeof(int));\n // 상태 전이: 첫 번째 행\n dp[0] = grid[0][0];\n for (int j = 1; j &lt; m; j++) {\n dp[j] = dp[j - 1] + grid[0][j];\n }\n // 상태 전이: 나머지 행\n for (int i = 1; i &lt; n; i++) {\n // 상태 전이: 첫 번째 열\n dp[0] = dp[0] + grid[i][0];\n // 상태 전이: 나머지 열\n for (int j = 1; j &lt; m; j++) {\n dp[j] = myMin(dp[j - 1], dp[j]) + grid[i][j];\n }\n }\n int res = dp[m - 1];\n // 메모리 해제\n free(dp);\n return res;\n}\n</code></pre> min_path_sum.kt<pre><code>/* 최소 경로 합: 공간을 최적화한 동적 계획법 */\nfun minPathSumDPComp(grid: Array&lt;IntArray&gt;): Int {\n val n = grid.size\n val m = grid[0].size\n // dp 테이블을 초기화한다\n val dp = IntArray(m)\n // 상태 전이: 첫 번째 행\n dp[0] = grid[0][0]\n for (j in 1..&lt;m) {\n dp[j] = dp[j - 1] + grid[0][j]\n }\n // 상태 전이: 나머지 행\n for (i in 1..&lt;n) {\n // 상태 전이: 첫 번째 열\n dp[0] = dp[0] + grid[i][0]\n // 상태 전이: 나머지 열\n for (j in 1..&lt;m) {\n dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]\n }\n }\n return dp[m - 1]\n}\n</code></pre> min_path_sum.rb<pre><code>### 최소 경로 합: 공간을 최적화한 동적 계획법 ###\ndef min_path_sum_dp_comp(grid)\n n, m = grid.length, grid.first.length\n # dp 테이블을 초기화한다\n dp = Array.new(m, 0)\n # 상태 전이: 첫 번째 행\n dp[0] = grid[0][0]\n (1...m).each { |j| dp[j] = dp[j - 1] + grid[0][j] }\n # 상태 전이: 나머지 행\n for i in 1...n\n # 상태 전이: 첫 번째 열\n dp[0] = dp[0] + grid[i][0]\n # 상태 전이: 나머지 열\n (1...m).each { |j| dp[j] = [dp[j - 1], dp[j]].min + grid[i][j] }\n end\n dp[m - 1]\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 14 장   동적 계획법","14.3   동적 계획법 문제 풀이 방법"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/","level":1,"title":"14.6   편집 거리 문제","text":"<p>편집 거리 또는 Levenshtein 거리는 두 문자열을 서로 바꾸는 데 필요한 최소 수정 횟수다. 정보 검색과 자연어 처리에서 두 시퀀스의 유사도를 측정할 때 자주 사용한다.</p> <p>문제</p> <p>두 문자열 \\(s\\)와 \\(t\\)를 입력받아 \\(s\\)를 \\(t\\)로 바꾸는 데 필요한 최소 편집 횟수를 반환하라.</p> <p>문자열에 문자를 하나 삽입하거나, 하나 삭제하거나, 한 문자를 임의의 다른 문자로 바꾸는 세 연산을 할 수 있다.</p> <p>아래 그림(14-27)과 같이 <code>kitten</code>을 <code>sitting</code>으로 바꾸려면 교체 2회와 추가 1회로 총 3회 편집해야 한다. <code>hello</code>를 <code>algo</code>로 바꾸려면 교체 2회와 삭제 1회로 총 3회가 필요하다.</p> <p></p> <p> 그림 14-27   편집 거리 예시 데이터 </p> <p>편집 거리 문제는 의사 결정 트리 모형으로 자연스럽게 설명할 수 있다. 문자열은 트리 노드에, 한 번의 결정(편집 연산)은 간선 하나에 대응한다.</p> <p>아래 그림(14-28)과 같이 연산을 제한하지 않으면 각 노드에서 다양한 연산에 대응하는 많은 간선이 뻗어 나온다. 즉, <code>hello</code>에서 <code>algo</code>로 가는 경로는 매우 많다.</p> <p>의사 결정 트리의 관점에서 이 문제의 목표는 노드 <code>hello</code>와 노드 <code>algo</code> 사이의 최단 경로를 구하는 것이다.</p> <p></p> <p> 그림 14-28   의사 결정 트리로 표현한 편집 거리 문제 </p>","path":["제 14 장   동적 계획법","14.6   편집 거리 문제"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#1","level":3,"title":"1.   동적 계획법 접근","text":"<p>1단계: 매 차례의 결정을 생각하고 상태를 정의하여 \\(dp\\) 테이블을 얻는다</p> <p>매 차례의 결정은 문자열 \\(s\\)에 편집 연산을 한 번 적용하는 것이다.</p> <p>부분 문제를 만들려면 편집하는 동안 문제의 크기가 점차 줄어들어야 한다. 문자열 \\(s\\)와 \\(t\\)의 길이를 각각 \\(n\\)과 \\(m\\)이라고 하고, 두 문자열의 마지막 문자 \\(s[n-1]\\)과 \\(t[m-1]\\)을 먼저 살펴보자.</p> <ul> <li>\\(s[n-1]\\)과 \\(t[m-1]\\)이 같으면 두 문자를 건너뛰고 \\(s[n-2]\\)와 \\(t[m-2]\\)를 바로 살펴볼 수 있다.</li> <li>\\(s[n-1]\\)과 \\(t[m-1]\\)이 다르면 \\(s\\)에 편집(삽입, 삭제, 교체)을 한 번 적용하여 끝 문자가 같아지게 하고, 이를 건너뛰어 더 작은 문제를 살펴본다.</li> </ul> <p>즉, 문자열 \\(s\\)에서 결정(편집 연산)을 할 때마다 \\(s\\)와 \\(t\\)에서 아직 맞추어야 하는 문자들이 달라진다. 따라서 현재 살펴보는 \\(s\\)의 \\(i\\) 번째 문자와 \\(t\\)의 \\(j\\) 번째 문자를 상태로 정의하고 \\([i, j]\\)라고 한다.</p> <p>상태 \\([i, j]\\)의 부분 문제는 \\(s\\)의 앞 \\(i\\) 개 문자를 \\(t\\)의 앞 \\(j\\) 개 문자로 바꾸는 최소 편집 횟수다.</p> <p>이로써 \\((n+1) \\times (m+1)\\) 크기의 2차원 \\(dp\\) 테이블을 얻는다.</p> <p>2단계: 최적 부분 구조를 찾아 상태 전이 식을 도출한다</p> <p>부분 문제 \\(dp[i, j]\\)에 해당하는 두 문자열의 끝 문자는 \\(s[i-1]\\)과 \\(t[j-1]\\)이다. 편집 연산에 따라 아래 그림(14-29)의 세 경우로 나뉜다.</p> <ol> <li>\\(s[i-1]\\) 뒤에 \\(t[j-1]\\)을 추가하면 부분 문제 \\(dp[i, j-1]\\)이 남는다.</li> <li>\\(s[i-1]\\)을 삭제하면 부분 문제 \\(dp[i-1, j]\\)가 남는다.</li> <li>\\(s[i-1]\\)을 \\(t[j-1]\\)로 바꾸면 부분 문제 \\(dp[i-1, j-1]\\)이 남는다.</li> </ol> <p></p> <p> 그림 14-29   편집 거리의 상태 전이 </p> <p>위 분석으로 최적 부분 구조를 얻는다. \\(dp[i, j]\\)의 최소 편집 횟수는 \\(dp[i, j-1]\\), \\(dp[i-1, j]\\), \\(dp[i-1, j-1]\\) 중 가장 작은 편집 횟수에 이번 편집 횟수 \\(1\\)을 더한 값이다. 상태 전이 식은 다음과 같다.</p> \\[ dp[i, j] = \\min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1 \\] <p>\\(s[i-1]\\)과 \\(t[j-1]\\)이 같으면 현재 문자를 편집할 필요가 없으며, 이때 상태 전이 식은 다음과 같다.</p> \\[ dp[i, j] = dp[i-1, j-1] \\] <p>3단계: 경계 조건과 상태 전이 순서를 정한다</p> <p>두 문자열이 모두 비어 있으면 편집 횟수는 \\(0\\)이므로 \\(dp[0, 0] = 0\\)이다. \\(s\\)만 비어 있고 \\(t\\)는 비어 있지 않으면 최소 편집 횟수는 \\(t\\)의 길이이므로 첫 행은 \\(dp[0, j] = j\\)다. 반대로 \\(s\\)는 비어 있지 않고 \\(t\\)가 비어 있으면 최소 편집 횟수는 \\(s\\)의 길이이므로 첫 열은 \\(dp[i, 0] = i\\)다.</p> <p>상태 전이 식에서 \\(dp[i, j]\\)는 왼쪽, 위쪽, 왼쪽 위의 해에 의존하므로 이중 반복문으로 전체 \\(dp\\) 테이블을 정방향으로 순회하면 된다.</p>","path":["제 14 장   동적 계획법","14.6   편집 거리 문제"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#2","level":3,"title":"2.   코드 구현","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby edit_distance.py<pre><code>def edit_distance_dp(s: str, t: str) -&gt; int:\n \"\"\"편집 거리: 동적 계획법\"\"\"\n n, m = len(s), len(t)\n dp = [[0] * (m + 1) for _ in range(n + 1)]\n # 상태 전이: 첫 번째 행과 첫 번째 열\n for i in range(1, n + 1):\n dp[i][0] = i\n for j in range(1, m + 1):\n dp[0][j] = j\n # 상태 전이: 나머지 행과 열\n for i in range(1, n + 1):\n for j in range(1, m + 1):\n if s[i - 1] == t[j - 1]:\n # 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[i][j] = dp[i - 1][j - 1]\n else:\n # 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[i][j] = min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1\n return dp[n][m]\n</code></pre> edit_distance.cpp<pre><code>/* 편집 거리: 동적 계획법 */\nint editDistanceDP(string s, string t) {\n int n = s.length(), m = t.length();\n vector&lt;vector&lt;int&gt;&gt; dp(n + 1, vector&lt;int&gt;(m + 1, 0));\n // 상태 전이: 첫 번째 행과 첫 번째 열\n for (int i = 1; i &lt;= n; i++) {\n dp[i][0] = i;\n }\n for (int j = 1; j &lt;= m; j++) {\n dp[0][j] = j;\n }\n // 상태 전이: 나머지 행과 열\n for (int i = 1; i &lt;= n; i++) {\n for (int j = 1; j &lt;= m; j++) {\n if (s[i - 1] == t[j - 1]) {\n // 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[i][j] = dp[i - 1][j - 1];\n } else {\n // 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n }\n }\n }\n return dp[n][m];\n}\n</code></pre> edit_distance.java<pre><code>/* 편집 거리: 동적 계획법 */\nint editDistanceDP(String s, String t) {\n int n = s.length(), m = t.length();\n int[][] dp = new int[n + 1][m + 1];\n // 상태 전이: 첫 번째 행과 첫 번째 열\n for (int i = 1; i &lt;= n; i++) {\n dp[i][0] = i;\n }\n for (int j = 1; j &lt;= m; j++) {\n dp[0][j] = j;\n }\n // 상태 전이: 나머지 행과 열\n for (int i = 1; i &lt;= n; i++) {\n for (int j = 1; j &lt;= m; j++) {\n if (s.charAt(i - 1) == t.charAt(j - 1)) {\n // 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[i][j] = dp[i - 1][j - 1];\n } else {\n // 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[i][j] = Math.min(Math.min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n }\n }\n }\n return dp[n][m];\n}\n</code></pre> edit_distance.cs<pre><code>/* 편집 거리: 동적 계획법 */\nint EditDistanceDP(string s, string t) {\n int n = s.Length, m = t.Length;\n int[,] dp = new int[n + 1, m + 1];\n // 상태 전이: 첫 번째 행과 첫 번째 열\n for (int i = 1; i &lt;= n; i++) {\n dp[i, 0] = i;\n }\n for (int j = 1; j &lt;= m; j++) {\n dp[0, j] = j;\n }\n // 상태 전이: 나머지 행과 열\n for (int i = 1; i &lt;= n; i++) {\n for (int j = 1; j &lt;= m; j++) {\n if (s[i - 1] == t[j - 1]) {\n // 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[i, j] = dp[i - 1, j - 1];\n } else {\n // 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[i, j] = Math.Min(Math.Min(dp[i, j - 1], dp[i - 1, j]), dp[i - 1, j - 1]) + 1;\n }\n }\n }\n return dp[n, m];\n}\n</code></pre> edit_distance.go<pre><code>/* 편집 거리: 동적 계획법 */\nfunc editDistanceDP(s string, t string) int {\n n := len(s)\n m := len(t)\n dp := make([][]int, n+1)\n for i := 0; i &lt;= n; i++ {\n dp[i] = make([]int, m+1)\n }\n // 상태 전이: 첫 번째 행과 첫 번째 열\n for i := 1; i &lt;= n; i++ {\n dp[i][0] = i\n }\n for j := 1; j &lt;= m; j++ {\n dp[0][j] = j\n }\n // 상태 전이: 나머지 행과 열\n for i := 1; i &lt;= n; i++ {\n for j := 1; j &lt;= m; j++ {\n if s[i-1] == t[j-1] {\n // 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[i][j] = dp[i-1][j-1]\n } else {\n // 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[i][j] = MinInt(MinInt(dp[i][j-1], dp[i-1][j]), dp[i-1][j-1]) + 1\n }\n }\n }\n return dp[n][m]\n}\n</code></pre> edit_distance.swift<pre><code>/* 편집 거리: 동적 계획법 */\nfunc editDistanceDP(s: String, t: String) -&gt; Int {\n let n = s.utf8CString.count\n let m = t.utf8CString.count\n var dp = Array(repeating: Array(repeating: 0, count: m + 1), count: n + 1)\n // 상태 전이: 첫 번째 행과 첫 번째 열\n for i in 1 ... n {\n dp[i][0] = i\n }\n for j in 1 ... m {\n dp[0][j] = j\n }\n // 상태 전이: 나머지 행과 열\n for i in 1 ... n {\n for j in 1 ... m {\n if s.utf8CString[i - 1] == t.utf8CString[j - 1] {\n // 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[i][j] = dp[i - 1][j - 1]\n } else {\n // 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1\n }\n }\n }\n return dp[n][m]\n}\n</code></pre> edit_distance.js<pre><code>/* 편집 거리: 동적 계획법 */\nfunction editDistanceDP(s, t) {\n const n = s.length,\n m = t.length;\n const dp = Array.from({ length: n + 1 }, () =&gt; new Array(m + 1).fill(0));\n // 상태 전이: 첫 번째 행과 첫 번째 열\n for (let i = 1; i &lt;= n; i++) {\n dp[i][0] = i;\n }\n for (let j = 1; j &lt;= m; j++) {\n dp[0][j] = j;\n }\n // 상태 전이: 나머지 행과 열\n for (let i = 1; i &lt;= n; i++) {\n for (let j = 1; j &lt;= m; j++) {\n if (s.charAt(i - 1) === t.charAt(j - 1)) {\n // 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[i][j] = dp[i - 1][j - 1];\n } else {\n // 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[i][j] =\n Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;\n }\n }\n }\n return dp[n][m];\n}\n</code></pre> edit_distance.ts<pre><code>/* 편집 거리: 동적 계획법 */\nfunction editDistanceDP(s: string, t: string): number {\n const n = s.length,\n m = t.length;\n const dp = Array.from({ length: n + 1 }, () =&gt;\n Array.from({ length: m + 1 }, () =&gt; 0)\n );\n // 상태 전이: 첫 번째 행과 첫 번째 열\n for (let i = 1; i &lt;= n; i++) {\n dp[i][0] = i;\n }\n for (let j = 1; j &lt;= m; j++) {\n dp[0][j] = j;\n }\n // 상태 전이: 나머지 행과 열\n for (let i = 1; i &lt;= n; i++) {\n for (let j = 1; j &lt;= m; j++) {\n if (s.charAt(i - 1) === t.charAt(j - 1)) {\n // 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[i][j] = dp[i - 1][j - 1];\n } else {\n // 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[i][j] =\n Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;\n }\n }\n }\n return dp[n][m];\n}\n</code></pre> edit_distance.dart<pre><code>/* 편집 거리: 동적 계획법 */\nint editDistanceDP(String s, String t) {\n int n = s.length, m = t.length;\n List&lt;List&lt;int&gt;&gt; dp = List.generate(n + 1, (_) =&gt; List.filled(m + 1, 0));\n // 상태 전이: 첫 번째 행과 첫 번째 열\n for (int i = 1; i &lt;= n; i++) {\n dp[i][0] = i;\n }\n for (int j = 1; j &lt;= m; j++) {\n dp[0][j] = j;\n }\n // 상태 전이: 나머지 행과 열\n for (int i = 1; i &lt;= n; i++) {\n for (int j = 1; j &lt;= m; j++) {\n if (s[i - 1] == t[j - 1]) {\n // 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[i][j] = dp[i - 1][j - 1];\n } else {\n // 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n }\n }\n }\n return dp[n][m];\n}\n</code></pre> edit_distance.rs<pre><code>/* 편집 거리: 동적 계획법 */\nfn edit_distance_dp(s: &amp;str, t: &amp;str) -&gt; i32 {\n let (n, m) = (s.len(), t.len());\n let mut dp = vec![vec![0; m + 1]; n + 1];\n // 상태 전이: 첫 번째 행과 첫 번째 열\n for i in 1..=n {\n dp[i][0] = i as i32;\n }\n for j in 1..m {\n dp[0][j] = j as i32;\n }\n // 상태 전이: 나머지 행과 열\n for i in 1..=n {\n for j in 1..=m {\n if s.chars().nth(i - 1) == t.chars().nth(j - 1) {\n // 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[i][j] = dp[i - 1][j - 1];\n } else {\n // 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[i][j] =\n std::cmp::min(std::cmp::min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n }\n }\n }\n dp[n][m]\n}\n</code></pre> edit_distance.c<pre><code>/* 편집 거리: 동적 계획법 */\nint editDistanceDP(char *s, char *t, int n, int m) {\n int **dp = malloc((n + 1) * sizeof(int *));\n for (int i = 0; i &lt;= n; i++) {\n dp[i] = calloc(m + 1, sizeof(int));\n }\n // 상태 전이: 첫 번째 행과 첫 번째 열\n for (int i = 1; i &lt;= n; i++) {\n dp[i][0] = i;\n }\n for (int j = 1; j &lt;= m; j++) {\n dp[0][j] = j;\n }\n // 상태 전이: 나머지 행과 열\n for (int i = 1; i &lt;= n; i++) {\n for (int j = 1; j &lt;= m; j++) {\n if (s[i - 1] == t[j - 1]) {\n // 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[i][j] = dp[i - 1][j - 1];\n } else {\n // 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[i][j] = myMin(myMin(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n }\n }\n }\n int res = dp[n][m];\n // 메모리 해제\n for (int i = 0; i &lt;= n; i++) {\n free(dp[i]);\n }\n return res;\n}\n</code></pre> edit_distance.kt<pre><code>/* 편집 거리: 동적 계획법 */\nfun editDistanceDP(s: String, t: String): Int {\n val n = s.length\n val m = t.length\n val dp = Array(n + 1) { IntArray(m + 1) }\n // 상태 전이: 첫 번째 행과 첫 번째 열\n for (i in 1..n) {\n dp[i][0] = i\n }\n for (j in 1..m) {\n dp[0][j] = j\n }\n // 상태 전이: 나머지 행과 열\n for (i in 1..n) {\n for (j in 1..m) {\n if (s[i - 1] == t[j - 1]) {\n // 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[i][j] = dp[i - 1][j - 1]\n } else {\n // 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1\n }\n }\n }\n return dp[n][m]\n}\n</code></pre> edit_distance.rb<pre><code>### 편집 거리: 동적 계획법 ###\ndef edit_distance_dp(s, t)\n n, m = s.length, t.length\n dp = Array.new(n + 1) { Array.new(m + 1, 0) }\n # 상태 전이: 첫 번째 행과 첫 번째 열\n (1...(n + 1)).each { |i| dp[i][0] = i }\n (1...(m + 1)).each { |j| dp[0][j] = j }\n # 상태 전이: 나머지 행과 열\n for i in 1...(n + 1)\n for j in 1...(m +1)\n if s[i - 1] == t[j - 1]\n # 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[i][j] = dp[i - 1][j - 1]\n else\n # 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[i][j] = [dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]].min + 1\n end\n end\n end\n dp[n][m]\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>아래 그림(14-30)과 같이 편집 거리의 상태 전이는 배낭 문제와 매우 비슷하며, 모두 2차원 격자를 채우는 과정으로 볼 수 있다.</p> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt;&lt;6&gt;&lt;7&gt;&lt;8&gt;&lt;9&gt;&lt;10&gt;&lt;11&gt;&lt;12&gt;&lt;13&gt;&lt;14&gt;&lt;15&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 14-30   편집 거리의 동적 계획법 과정 </p>","path":["제 14 장   동적 계획법","14.6   편집 거리 문제"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#3","level":3,"title":"3.   공간 최적화","text":"<p>\\(dp[i,j]\\)는 위쪽 \\(dp[i-1, j]\\), 왼쪽 \\(dp[i, j-1]\\), 왼쪽 위 \\(dp[i-1, j-1]\\)에서 전이된다. 그런데 정방향 순회는 왼쪽 위 값 \\(dp[i-1, j-1]\\)을 잃고, 역방향 순회는 \\(dp[i, j-1]\\)을 미리 계산하지 못하므로 두 순서 모두 그대로는 사용할 수 없다.</p> <p>이를 위해 변수 <code>leftup</code>에 왼쪽 위의 해 \\(dp[i-1, j-1]\\)을 잠시 저장하면 왼쪽과 위쪽의 해만 고려하면 된다. 이때는 무한 배낭 문제와 같은 상황이므로 정방향 순회를 사용할 수 있다. 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby edit_distance.py<pre><code>def edit_distance_dp_comp(s: str, t: str) -&gt; int:\n \"\"\"편집 거리: 공간을 최적화한 동적 계획법\"\"\"\n n, m = len(s), len(t)\n dp = [0] * (m + 1)\n # 상태 전이: 첫 번째 행\n for j in range(1, m + 1):\n dp[j] = j\n # 상태 전이: 나머지 행\n for i in range(1, n + 1):\n # 상태 전이: 첫 번째 열\n leftup = dp[0] # dp[i-1, j-1]을 임시로 저장한다\n dp[0] += 1\n # 상태 전이: 나머지 열\n for j in range(1, m + 1):\n temp = dp[j]\n if s[i - 1] == t[j - 1]:\n # 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[j] = leftup\n else:\n # 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[j] = min(dp[j - 1], dp[j], leftup) + 1\n leftup = temp # 다음 반복의 dp[i-1, j-1]로 갱신한다\n return dp[m]\n</code></pre> edit_distance.cpp<pre><code>/* 편집 거리: 공간을 최적화한 동적 계획법 */\nint editDistanceDPComp(string s, string t) {\n int n = s.length(), m = t.length();\n vector&lt;int&gt; dp(m + 1, 0);\n // 상태 전이: 첫 번째 행\n for (int j = 1; j &lt;= m; j++) {\n dp[j] = j;\n }\n // 상태 전이: 나머지 행\n for (int i = 1; i &lt;= n; i++) {\n // 상태 전이: 첫 번째 열\n int leftup = dp[0]; // dp[i-1, j-1]을 임시로 저장한다\n dp[0] = i;\n // 상태 전이: 나머지 열\n for (int j = 1; j &lt;= m; j++) {\n int temp = dp[j];\n if (s[i - 1] == t[j - 1]) {\n // 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[j] = leftup;\n } else {\n // 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;\n }\n leftup = temp; // 다음 반복의 dp[i-1, j-1]로 갱신한다\n }\n }\n return dp[m];\n}\n</code></pre> edit_distance.java<pre><code>/* 편집 거리: 공간을 최적화한 동적 계획법 */\nint editDistanceDPComp(String s, String t) {\n int n = s.length(), m = t.length();\n int[] dp = new int[m + 1];\n // 상태 전이: 첫 번째 행\n for (int j = 1; j &lt;= m; j++) {\n dp[j] = j;\n }\n // 상태 전이: 나머지 행\n for (int i = 1; i &lt;= n; i++) {\n // 상태 전이: 첫 번째 열\n int leftup = dp[0]; // dp[i-1, j-1]을 임시로 저장한다\n dp[0] = i;\n // 상태 전이: 나머지 열\n for (int j = 1; j &lt;= m; j++) {\n int temp = dp[j];\n if (s.charAt(i - 1) == t.charAt(j - 1)) {\n // 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[j] = leftup;\n } else {\n // 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), leftup) + 1;\n }\n leftup = temp; // 다음 반복의 dp[i-1, j-1]로 갱신한다\n }\n }\n return dp[m];\n}\n</code></pre> edit_distance.cs<pre><code>/* 편집 거리: 공간을 최적화한 동적 계획법 */\nint EditDistanceDPComp(string s, string t) {\n int n = s.Length, m = t.Length;\n int[] dp = new int[m + 1];\n // 상태 전이: 첫 번째 행\n for (int j = 1; j &lt;= m; j++) {\n dp[j] = j;\n }\n // 상태 전이: 나머지 행\n for (int i = 1; i &lt;= n; i++) {\n // 상태 전이: 첫 번째 열\n int leftup = dp[0]; // dp[i-1, j-1]을 임시로 저장한다\n dp[0] = i;\n // 상태 전이: 나머지 열\n for (int j = 1; j &lt;= m; j++) {\n int temp = dp[j];\n if (s[i - 1] == t[j - 1]) {\n // 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[j] = leftup;\n } else {\n // 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[j] = Math.Min(Math.Min(dp[j - 1], dp[j]), leftup) + 1;\n }\n leftup = temp; // 다음 반복의 dp[i-1, j-1]로 갱신한다\n }\n }\n return dp[m];\n}\n</code></pre> edit_distance.go<pre><code>/* 편집 거리: 공간을 최적화한 동적 계획법 */\nfunc editDistanceDPComp(s string, t string) int {\n n := len(s)\n m := len(t)\n dp := make([]int, m+1)\n // 상태 전이: 첫 번째 행\n for j := 1; j &lt;= m; j++ {\n dp[j] = j\n }\n // 상태 전이: 나머지 행\n for i := 1; i &lt;= n; i++ {\n // 상태 전이: 첫 번째 열\n leftUp := dp[0] // dp[i-1, j-1]을 임시로 저장한다\n dp[0] = i\n // 상태 전이: 나머지 열\n for j := 1; j &lt;= m; j++ {\n temp := dp[j]\n if s[i-1] == t[j-1] {\n // 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[j] = leftUp\n } else {\n // 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[j] = MinInt(MinInt(dp[j-1], dp[j]), leftUp) + 1\n }\n leftUp = temp // 다음 반복의 dp[i-1, j-1]로 갱신한다\n }\n }\n return dp[m]\n}\n</code></pre> edit_distance.swift<pre><code>/* 편집 거리: 공간을 최적화한 동적 계획법 */\nfunc editDistanceDPComp(s: String, t: String) -&gt; Int {\n let n = s.utf8CString.count\n let m = t.utf8CString.count\n var dp = Array(repeating: 0, count: m + 1)\n // 상태 전이: 첫 번째 행\n for j in 1 ... m {\n dp[j] = j\n }\n // 상태 전이: 나머지 행\n for i in 1 ... n {\n // 상태 전이: 첫 번째 열\n var leftup = dp[0] // dp[i-1, j-1]을 임시로 저장한다\n dp[0] = i\n // 상태 전이: 나머지 열\n for j in 1 ... m {\n let temp = dp[j]\n if s.utf8CString[i - 1] == t.utf8CString[j - 1] {\n // 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[j] = leftup\n } else {\n // 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1\n }\n leftup = temp // 다음 반복의 dp[i-1, j-1]로 갱신한다\n }\n }\n return dp[m]\n}\n</code></pre> edit_distance.js<pre><code>/* 편집 거리: 공간을 최적화한 동적 계획법 */\nfunction editDistanceDPComp(s, t) {\n const n = s.length,\n m = t.length;\n const dp = new Array(m + 1).fill(0);\n // 상태 전이: 첫 번째 행\n for (let j = 1; j &lt;= m; j++) {\n dp[j] = j;\n }\n // 상태 전이: 나머지 행\n for (let i = 1; i &lt;= n; i++) {\n // 상태 전이: 첫 번째 열\n let leftup = dp[0]; // dp[i-1, j-1]을 임시로 저장한다\n dp[0] = i;\n // 상태 전이: 나머지 열\n for (let j = 1; j &lt;= m; j++) {\n const temp = dp[j];\n if (s.charAt(i - 1) === t.charAt(j - 1)) {\n // 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[j] = leftup;\n } else {\n // 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;\n }\n leftup = temp; // 다음 반복의 dp[i-1, j-1]로 갱신한다\n }\n }\n return dp[m];\n}\n</code></pre> edit_distance.ts<pre><code>/* 편집 거리: 공간을 최적화한 동적 계획법 */\nfunction editDistanceDPComp(s: string, t: string): number {\n const n = s.length,\n m = t.length;\n const dp = new Array(m + 1).fill(0);\n // 상태 전이: 첫 번째 행\n for (let j = 1; j &lt;= m; j++) {\n dp[j] = j;\n }\n // 상태 전이: 나머지 행\n for (let i = 1; i &lt;= n; i++) {\n // 상태 전이: 첫 번째 열\n let leftup = dp[0]; // dp[i-1, j-1]을 임시로 저장한다\n dp[0] = i;\n // 상태 전이: 나머지 열\n for (let j = 1; j &lt;= m; j++) {\n const temp = dp[j];\n if (s.charAt(i - 1) === t.charAt(j - 1)) {\n // 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[j] = leftup;\n } else {\n // 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;\n }\n leftup = temp; // 다음 반복의 dp[i-1, j-1]로 갱신한다\n }\n }\n return dp[m];\n}\n</code></pre> edit_distance.dart<pre><code>/* 편집 거리: 공간을 최적화한 동적 계획법 */\nint editDistanceDPComp(String s, String t) {\n int n = s.length, m = t.length;\n List&lt;int&gt; dp = List.filled(m + 1, 0);\n // 상태 전이: 첫 번째 행\n for (int j = 1; j &lt;= m; j++) {\n dp[j] = j;\n }\n // 상태 전이: 나머지 행\n for (int i = 1; i &lt;= n; i++) {\n // 상태 전이: 첫 번째 열\n int leftup = dp[0]; // dp[i-1, j-1]을 임시로 저장한다\n dp[0] = i;\n // 상태 전이: 나머지 열\n for (int j = 1; j &lt;= m; j++) {\n int temp = dp[j];\n if (s[i - 1] == t[j - 1]) {\n // 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[j] = leftup;\n } else {\n // 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;\n }\n leftup = temp; // 다음 반복의 dp[i-1, j-1]로 갱신한다\n }\n }\n return dp[m];\n}\n</code></pre> edit_distance.rs<pre><code>/* 편집 거리: 공간을 최적화한 동적 계획법 */\nfn edit_distance_dp_comp(s: &amp;str, t: &amp;str) -&gt; i32 {\n let (n, m) = (s.len(), t.len());\n let mut dp = vec![0; m + 1];\n // 상태 전이: 첫 번째 행\n for j in 1..m {\n dp[j] = j as i32;\n }\n // 상태 전이: 나머지 행\n for i in 1..=n {\n // 상태 전이: 첫 번째 열\n let mut leftup = dp[0]; // dp[i-1, j-1]을 임시로 저장한다\n dp[0] = i as i32;\n // 상태 전이: 나머지 열\n for j in 1..=m {\n let temp = dp[j];\n if s.chars().nth(i - 1) == t.chars().nth(j - 1) {\n // 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[j] = leftup;\n } else {\n // 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[j] = std::cmp::min(std::cmp::min(dp[j - 1], dp[j]), leftup) + 1;\n }\n leftup = temp; // 다음 반복의 dp[i-1, j-1]로 갱신한다\n }\n }\n dp[m]\n}\n</code></pre> edit_distance.c<pre><code>/* 편집 거리: 공간을 최적화한 동적 계획법 */\nint editDistanceDPComp(char *s, char *t, int n, int m) {\n int *dp = calloc(m + 1, sizeof(int));\n // 상태 전이: 첫 번째 행\n for (int j = 1; j &lt;= m; j++) {\n dp[j] = j;\n }\n // 상태 전이: 나머지 행\n for (int i = 1; i &lt;= n; i++) {\n // 상태 전이: 첫 번째 열\n int leftup = dp[0]; // dp[i-1, j-1]을 임시로 저장한다\n dp[0] = i;\n // 상태 전이: 나머지 열\n for (int j = 1; j &lt;= m; j++) {\n int temp = dp[j];\n if (s[i - 1] == t[j - 1]) {\n // 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[j] = leftup;\n } else {\n // 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[j] = myMin(myMin(dp[j - 1], dp[j]), leftup) + 1;\n }\n leftup = temp; // 다음 반복의 dp[i-1, j-1]로 갱신한다\n }\n }\n int res = dp[m];\n // 메모리 해제\n free(dp);\n return res;\n}\n</code></pre> edit_distance.kt<pre><code>/* 편집 거리: 공간을 최적화한 동적 계획법 */\nfun editDistanceDPComp(s: String, t: String): Int {\n val n = s.length\n val m = t.length\n val dp = IntArray(m + 1)\n // 상태 전이: 첫 번째 행\n for (j in 1..m) {\n dp[j] = j\n }\n // 상태 전이: 나머지 행\n for (i in 1..n) {\n // 상태 전이: 첫 번째 열\n var leftup = dp[0] // dp[i-1, j-1]을 임시로 저장한다\n dp[0] = i\n // 상태 전이: 나머지 열\n for (j in 1..m) {\n val temp = dp[j]\n if (s[i - 1] == t[j - 1]) {\n // 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[j] = leftup\n } else {\n // 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1\n }\n leftup = temp // 다음 반복의 dp[i-1, j-1]로 갱신한다\n }\n }\n return dp[m]\n}\n</code></pre> edit_distance.rb<pre><code>### 편집 거리: 공간을 최적화한 동적 계획법 ###\ndef edit_distance_dp_comp(s, t)\n n, m = s.length, t.length\n dp = Array.new(m + 1, 0)\n # 상태 전이: 첫 번째 행\n (1...(m + 1)).each { |j| dp[j] = j }\n # 상태 전이: 나머지 행\n for i in 1...(n + 1)\n # 상태 전이: 첫 번째 열\n leftup = dp.first # dp[i-1, j-1]을 임시로 저장한다\n dp[0] += 1\n # 상태 전이: 나머지 열\n for j in 1...(m + 1)\n temp = dp[j]\n if s[i - 1] == t[j - 1]\n # 두 문자가 같으면 두 문자를 바로 건너뛴다\n dp[j] = leftup\n else\n # 최소 편집 횟수 = 삽입, 삭제, 교체 세 연산에 대한 최소 편집 횟수 + 1\n dp[j] = [dp[j - 1], dp[j], leftup].min + 1\n end\n leftup = temp # 다음 반복의 dp[i-1, j-1]로 갱신한다\n end\n end\n dp[m]\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 14 장   동적 계획법","14.6   편집 거리 문제"],"tags":[]},{"location":"chapter_dynamic_programming/exercises/","level":1,"title":"14.8   연습 문제","text":"","path":["제 14 장   동적 계획법","14.8   연습 문제"],"tags":[]},{"location":"chapter_dynamic_programming/exercises/#1481","level":2,"title":"14.8.1   개념 확인","text":"","path":["제 14 장   동적 계획법","14.8   연습 문제"],"tags":[]},{"location":"chapter_dynamic_programming/exercises/#1","level":3,"title":"1.   언제 동적 계획법을 사용하는 것이 적합한가","text":"<p>한 학생이 “점화식을 쓸 수만 있으면 동적 계획법을 사용해야 한다.”라고 말했다. 다음 세 작업에 동적 계획법, 백트래킹, 또는 <code>dp</code> 테이블을 만들지 않는 반복문이나 수학 공식 중 어느 방법이 더 적합한지 판단하고, 핵심 이유를 하나씩 적어 보자.</p> <ol> <li>액면가가 <code>[1, 3, 4]</code>인 동전으로 금액 6을 만들 때 필요한 동전의 최소 개수를 구한다. 각 동전은 반복해서 사용할 수 있다.</li> <li><code>[1, 2, 3]</code>의 모든 순열을 출력한다.</li> <li> <p>\\(1 + 2 + \\dots + n\\)을 계산한다.</p> <p>동적 계획법이 적합하다고 판단한 작업에서는 <code>dp[i]</code>가 무엇을 나타내는지도 설명하라.</p> </li> </ol> 참고 답안 <ol> <li> <p>동적 계획법이 적합하다. <code>dp[i]</code>를 금액 <code>i</code>를 만드는 데 필요한 동전의 최소 개수로 정의한다. <code>i</code> 이하인 각 동전 <code>c</code>에 대해 <code>dp[i-c] + 1</code>을 후보 답으로 삼고, 이 후보들 중 최솟값을 선택하면 된다. 서로 다른 선택에서 같은 금액을 반복해서 만나며, 더 큰 금액의 최적해도 더 작은 금액의 최적해로 구성할 수 있다. 금액 6의 답은 2이며, <code>3 + 3</code>으로 만들 수 있다.</p> </li> <li> <p>백트래킹을 사용해야 한다. 문제에서는 순열 6개를 모두 하나씩 생성해야 한다. 백트래킹은 하나를 선택하고 탐색을 이어 간 뒤, 선택을 취소하고 다른 분기를 시도하는 과정을 체계적으로 수행할 수 있다. 어떤 방법을 쓰더라도 이 순열들을 실제로 출력하려면 열거 과정을 생략할 수 없다.</p> </li> <li> <p>반복문이나 등차수열의 합 공식을 사용하면 된다. <code>S(i) = S(i-1) + i</code>라는 점화식을 쓸 수는 있지만, <code>S(i)</code>를 계산할 때는 더 작은 값 <code>S(i-1)</code> 하나에만 의존한다. 각 부분합을 한 번만 계산하면 되고 중복되는 부분 문제가 없으므로 <code>dp</code> 테이블을 만들 필요가 없다. ‘점화식을 쓸 수 있다’고 해서 ‘동적 계획법이 필요하다’는 뜻은 아니다.</p> </li> </ol>","path":["제 14 장   동적 계획법","14.8   연습 문제"],"tags":[]},{"location":"chapter_dynamic_programming/exercises/#2","level":3,"title":"2.   배낭 테이블의 한 칸은 어떻게 계산하는가","text":"<p>0-1 배낭 문제에서 물건의 무게는 <code>wgt = [1, 2, 3]</code>, 가치는 <code>val = [5, 11, 15]</code>이며, 배낭 용량은 4다. <code>dp[i][c]</code>는 앞의 물건 \\(i\\)개만 고려하고 배낭 용량의 상한이 \\(c\\)일 때 얻을 수 있는 최대 가치를 나타낸다. 배낭을 반드시 가득 채울 필요는 없다.</p> <p>지금은 상태 <code>dp[3][4]</code>만 계산한다. <code>dp[2][4] = 16</code>, <code>dp[2][1] = 5</code>임을 알고 있다.</p> <ol> <li>세 번째 물건을 선택하지 않을 때 후보 가치는 얼마인가?</li> <li>세 번째 물건을 선택하면 용량이 얼마나 남으며, 후보 가치는 얼마인가?</li> <li><code>dp[3][4]</code>는 얼마로 정해야 하는가? 이는 어떤 물건들을 선택한 결과인가?</li> </ol> 참고 답안 <ol> <li> <p>세 번째 물건을 선택하지 않으면 앞의 두 물건에 대한 결과를 그대로 사용하므로 후보 가치는 <code>dp[2][4] = 16</code>이다.</p> </li> <li> <p>세 번째 물건의 무게는 3이므로 넣은 뒤 남는 용량은 \\(4-3=1\\)이다. 후보 가치는 <code>dp[2][1] + 15 = 5 + 15 = 20</code>이다.</p> </li> <li> <p>16과 20을 비교하면 <code>dp[3][4] = 20</code>을 얻는다. 이는 첫 번째와 세 번째 물건을 선택한 결과로, 총무게는 \\(1+3=4\\), 총가치는 \\(5+15=20\\)이다.</p> <p>이 상태의 계산은 0-1 배낭 문제에서 한 번의 ‘선택하거나 선택하지 않거나’ 비교를 보여 준다.</p> </li> </ol>","path":["제 14 장   동적 계획법","14.8   연습 문제"],"tags":[]},{"location":"chapter_dynamic_programming/exercises/#3","level":3,"title":"3.   배낭 용량은 어떤 순서로 갱신해야 하는가","text":"<p>0-1 배낭 문제에 물건이 하나뿐이며, 무게는 2, 가치는 5다. 배낭 용량은 4다. 각 물건은 최대 한 번만 선택할 수 있고, 초기 일차원 배열은 <code>dp = [0, 0, 0, 0, 0]</code>이다.</p> <p>한 학생이 이 물건을 처리하면서 용량을 2부터 4까지 갱신했다.</p> <ul> <li><code>dp[2]</code>를 갱신한 뒤 5를 얻었다.</li> <li><code>dp[3]</code>을 갱신한 뒤에도 5를 얻었다.</li> <li><code>dp[4]</code>를 갱신할 때 방금 구한 <code>dp[2]</code>를 다시 사용하여 <code>dp[4] = 10</code>을 얻었다.</li> </ul> <ol> <li><code>dp[4] = 10</code>이라는 결과가 올바른가? 이유는 무엇인가?</li> <li>‘각 물건은 최대 한 번만 선택할 수 있다’는 조건에 따르면 <code>dp[4]</code>는 얼마여야 하는가?</li> <li>각 물건을 처리할 때 용량을 큰 것부터 작은 것 순으로 갱신해야 하는가, 그 반대인가? 그렇게 하면 어떤 문제를 피할 수 있는가?</li> </ol> 참고 답안 <ol> <li> <p>올바르지 않다. 가치 10은 가치가 5인 이 물건을 두 번 넣은 것과 같으므로, ‘각 물건은 최대 한 번만 선택할 수 있다’는 조건에 어긋난다.</p> </li> <li> <p>배낭에는 이 물건 하나만 넣을 수 있으므로 올바른 <code>dp[4]</code> 값은 5다.</p> </li> <li> <p>용량을 큰 것부터 작은 것 순으로, 즉 4, 3, 2 순으로 갱신해야 한다. 그러면 <code>dp[c]</code>를 계산할 때 읽는 <code>dp[c-2]</code>는 아직 현재 물건을 처리하기 전의 결과이므로, 같은 회전에서 현재 물건을 반복해서 사용하지 않게 된다.</p> </li> </ol>","path":["제 14 장   동적 계획법","14.8   연습 문제"],"tags":[]},{"location":"chapter_dynamic_programming/exercises/#1482","level":2,"title":"14.8.2   프로그래밍 연습","text":"","path":["제 14 장   동적 계획법","14.8   연습 문제"],"tags":[]},{"location":"chapter_dynamic_programming/exercises/#1_1","level":3,"title":"1.   계단을 오르는 방법의 수","text":"<p>계단은 총 <code>n</code>칸이다. 한 번에 1칸 또는 2칸만 올라갈 수 있으며, 정확히 <code>n</code>번째 칸에 도착해야 한다. 서로 다른 방법이 모두 몇 가지인지 계산하라. <code>n &gt;= 1</code>이며, 각 걸음에서 1칸을 오르는지 2칸을 오르는지에 따라서만 방법을 구분한다. 일차원 동적 계획법 배열을 사용하라. 상태 두 개만 유지하는 공간 최적화 방식은 아직 사용하지 마라.</p> 풀이 힌트 <ol> <li>i번째 칸에 도착하는 마지막 걸음은 1칸 또는 2칸을 오르는 것뿐이다.</li> <li>따라서 dp[i] = dp[i-1] + dp[i-2]다.</li> <li>n이 1과 2인 경우를 먼저 처리한 뒤, 세 번째 칸부터 테이블을 채운다.</li> </ol> <p>LeetCode</p>","path":["제 14 장   동적 계획법","14.8   연습 문제"],"tags":[]},{"location":"chapter_dynamic_programming/exercises/#2-0-1","level":3,"title":"2.   0-1 배낭 문제","text":"<p>길이가 같은 배열 <code>wgt</code>와 <code>val</code>이 주어진다. <code>i</code>번째 물건의 무게 <code>wgt[i]</code>는 양의 정수이고, 가치 <code>val[i]</code>는 음이 아닌 정수다. 배낭 용량 <code>cap</code>은 음이 아닌 정수다. 각 물건은 최대 한 번만 선택할 수 있다. 총무게가 <code>cap</code>을 넘지 않는 조건에서 배낭에 넣을 수 있는 최대 총가치를 구하라. 일차원 동적 계획법으로 구현하라.</p> 풀이 힌트 <ol> <li>길이가 cap + 1인 배열 dp를 초기화한다. dp[c]는 용량 상한이 c일 때의 최대 가치를 나타낸다.</li> <li>물건 i를 처리할 때 ‘선택하지 않는 경우’의 dp[c]와 ‘선택하는 경우’의 dp[c-wgt[i]] + val[i]를 비교한다.</li> <li>같은 회전에서 현재 물건을 반복해서 선택하지 않도록 용량을 반드시 큰 것부터 작은 것 순으로 갱신해야 한다.</li> </ol>","path":["제 14 장   동적 계획법","14.8   연습 문제"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/","level":1,"title":"14.1   동적 계획법 첫걸음","text":"<p>동적 계획법(dynamic programming)은 중요한 알고리즘 패러다임이다. 문제를 더 작은 부분 문제들로 나누고 그 해를 저장하여 중복 계산을 피함으로써 시간 효율을 크게 높인다.</p> <p>이 절에서는 대표적인 예제에서 출발하여 먼저 완전 탐색을 수행하는 백트래킹 풀이를 제시한다. 그 안의 중복 부분 문제를 살펴보고, 더 효율적인 동적 계획법 풀이를 단계적으로 도출한다.</p> <p>계단 오르기</p> <p>총 \\(n\\) 칸인 계단이 있고, 한 번에 \\(1\\) 칸 또는 \\(2\\) 칸을 오를 수 있다. 맨 위까지 오르는 방법은 몇 가지인가?</p> <p>아래 그림(14-1)과 같이 \\(3\\) 칸짜리 계단의 맨 위에 오르는 방법은 총 \\(3\\) 가지다.</p> <p></p> <p> 그림 14-1   3번째 칸까지 오르는 방법 수 </p> <p>이 문제는 방법의 수를 구하는 것이므로 백트래킹으로 모든 가능성을 열거할 수 있다. 계단 오르기를 여러 차례의 선택 과정으로 생각해 보자. 바닥에서 출발하여 매번 \\(1\\) 칸 또는 \\(2\\) 칸을 고른다. 맨 위에 도착할 때마다 방법 수를 \\(1\\) 늘리고, 맨 위를 넘어가면 가지치기한다. 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_backtrack.py<pre><code>def backtrack(choices: list[int], state: int, n: int, res: list[int]) -&gt; int:\n \"\"\"백트래킹\"\"\"\n # n번째 계단에 도달하면 방법의 수에 1을 더한다\n if state == n:\n res[0] += 1\n # 모든 선택지를 순회한다\n for choice in choices:\n # 가지치기: n번째 계단을 넘어갈 수 없다\n if state + choice &gt; n:\n continue\n # 시도: 선택하고 상태를 갱신한다\n backtrack(choices, state + choice, n, res)\n # 되돌리기\n\ndef climbing_stairs_backtrack(n: int) -&gt; int:\n \"\"\"계단 오르기: 백트래킹\"\"\"\n choices = [1, 2] # 한 계단 또는 두 계단 오르기를 선택할 수 있다\n state = 0 # 0번째 계단에서 오르기 시작한다\n res = [0] # res[0]에 방법의 수를 기록한다\n backtrack(choices, state, n, res)\n return res[0]\n</code></pre> climbing_stairs_backtrack.cpp<pre><code>/* 백트래킹 */\nvoid backtrack(vector&lt;int&gt; &amp;choices, int state, int n, vector&lt;int&gt; &amp;res) {\n // n번째 계단에 도달하면 방법의 수에 1을 더한다\n if (state == n)\n res[0]++;\n // 모든 선택지를 순회한다\n for (auto &amp;choice : choices) {\n // 가지치기: n번째 계단을 넘어갈 수 없다\n if (state + choice &gt; n)\n continue;\n // 시도: 선택하고 상태를 갱신한다\n backtrack(choices, state + choice, n, res);\n // 되돌리기\n }\n}\n\n/* 계단 오르기: 백트래킹 */\nint climbingStairsBacktrack(int n) {\n vector&lt;int&gt; choices = {1, 2}; // 한 계단 또는 두 계단 오르기를 선택할 수 있다\n int state = 0; // 0번째 계단에서 오르기 시작한다\n vector&lt;int&gt; res = {0}; // res[0]에 방법의 수를 기록한다\n backtrack(choices, state, n, res);\n return res[0];\n}\n</code></pre> climbing_stairs_backtrack.java<pre><code>/* 백트래킹 */\nvoid backtrack(List&lt;Integer&gt; choices, int state, int n, List&lt;Integer&gt; res) {\n // n번째 계단에 도달하면 방법의 수에 1을 더한다\n if (state == n)\n res.set(0, res.get(0) + 1);\n // 모든 선택지를 순회한다\n for (Integer choice : choices) {\n // 가지치기: n번째 계단을 넘어갈 수 없다\n if (state + choice &gt; n)\n continue;\n // 시도: 선택하고 상태를 갱신한다\n backtrack(choices, state + choice, n, res);\n // 되돌리기\n }\n}\n\n/* 계단 오르기: 백트래킹 */\nint climbingStairsBacktrack(int n) {\n List&lt;Integer&gt; choices = Arrays.asList(1, 2); // 한 계단 또는 두 계단 오르기를 선택할 수 있다\n int state = 0; // 0번째 계단에서 오르기 시작한다\n List&lt;Integer&gt; res = new ArrayList&lt;&gt;();\n res.add(0); // res[0]에 방법의 수를 기록한다\n backtrack(choices, state, n, res);\n return res.get(0);\n}\n</code></pre> climbing_stairs_backtrack.cs<pre><code>/* 백트래킹 */\nvoid Backtrack(List&lt;int&gt; choices, int state, int n, List&lt;int&gt; res) {\n // n번째 계단에 도달하면 방법의 수에 1을 더한다\n if (state == n)\n res[0]++;\n // 모든 선택지를 순회한다\n foreach (int choice in choices) {\n // 가지치기: n번째 계단을 넘어갈 수 없다\n if (state + choice &gt; n)\n continue;\n // 시도: 선택하고 상태를 갱신한다\n Backtrack(choices, state + choice, n, res);\n // 되돌리기\n }\n}\n\n/* 계단 오르기: 백트래킹 */\nint ClimbingStairsBacktrack(int n) {\n List&lt;int&gt; choices = [1, 2]; // 한 계단 또는 두 계단 오르기를 선택할 수 있다\n int state = 0; // 0번째 계단에서 오르기 시작한다\n List&lt;int&gt; res = [0]; // res[0]에 방법의 수를 기록한다\n Backtrack(choices, state, n, res);\n return res[0];\n}\n</code></pre> climbing_stairs_backtrack.go<pre><code>/* 백트래킹 */\nfunc backtrack(choices []int, state, n int, res []int) {\n // n번째 계단에 도달하면 방법의 수에 1을 더한다\n if state == n {\n res[0] = res[0] + 1\n }\n // 모든 선택지를 순회한다\n for _, choice := range choices {\n // 가지치기: n번째 계단을 넘어갈 수 없다\n if state+choice &gt; n {\n continue\n }\n // 시도: 선택하고 상태를 갱신한다\n backtrack(choices, state+choice, n, res)\n // 되돌리기\n }\n}\n\n/* 계단 오르기: 백트래킹 */\nfunc climbingStairsBacktrack(n int) int {\n // 한 계단 또는 두 계단 오르기를 선택할 수 있다\n choices := []int{1, 2}\n // 0번째 계단에서 오르기 시작한다\n state := 0\n res := make([]int, 1)\n // res[0]에 방법의 수를 기록한다\n res[0] = 0\n backtrack(choices, state, n, res)\n return res[0]\n}\n</code></pre> climbing_stairs_backtrack.swift<pre><code>/* 백트래킹 */\nfunc backtrack(choices: [Int], state: Int, n: Int, res: inout [Int]) {\n // n번째 계단에 도달하면 방법의 수에 1을 더한다\n if state == n {\n res[0] += 1\n }\n // 모든 선택지를 순회한다\n for choice in choices {\n // 가지치기: n번째 계단을 넘어갈 수 없다\n if state + choice &gt; n {\n continue\n }\n // 시도: 선택하고 상태를 갱신한다\n backtrack(choices: choices, state: state + choice, n: n, res: &amp;res)\n // 되돌리기\n }\n}\n\n/* 계단 오르기: 백트래킹 */\nfunc climbingStairsBacktrack(n: Int) -&gt; Int {\n let choices = [1, 2] // 한 계단 또는 두 계단 오르기를 선택할 수 있다\n let state = 0 // 0번째 계단에서 오르기 시작한다\n var res: [Int] = []\n res.append(0) // res[0]에 방법의 수를 기록한다\n backtrack(choices: choices, state: state, n: n, res: &amp;res)\n return res[0]\n}\n</code></pre> climbing_stairs_backtrack.js<pre><code>/* 백트래킹 */\nfunction backtrack(choices, state, n, res) {\n // n번째 계단에 도달하면 방법의 수에 1을 더한다\n if (state === n) res.set(0, res.get(0) + 1);\n // 모든 선택지를 순회한다\n for (const choice of choices) {\n // 가지치기: n번째 계단을 넘어갈 수 없다\n if (state + choice &gt; n) continue;\n // 시도: 선택하고 상태를 갱신한다\n backtrack(choices, state + choice, n, res);\n // 되돌리기\n }\n}\n\n/* 계단 오르기: 백트래킹 */\nfunction climbingStairsBacktrack(n) {\n const choices = [1, 2]; // 한 계단 또는 두 계단 오르기를 선택할 수 있다\n const state = 0; // 0번째 계단에서 오르기 시작한다\n const res = new Map();\n res.set(0, 0); // res[0]에 방법의 수를 기록한다\n backtrack(choices, state, n, res);\n return res.get(0);\n}\n</code></pre> climbing_stairs_backtrack.ts<pre><code>/* 백트래킹 */\nfunction backtrack(\n choices: number[],\n state: number,\n n: number,\n res: Map&lt;0, any&gt;\n): void {\n // n번째 계단에 도달하면 방법의 수에 1을 더한다\n if (state === n) res.set(0, res.get(0) + 1);\n // 모든 선택지를 순회한다\n for (const choice of choices) {\n // 가지치기: n번째 계단을 넘어갈 수 없다\n if (state + choice &gt; n) continue;\n // 시도: 선택하고 상태를 갱신한다\n backtrack(choices, state + choice, n, res);\n // 되돌리기\n }\n}\n\n/* 계단 오르기: 백트래킹 */\nfunction climbingStairsBacktrack(n: number): number {\n const choices = [1, 2]; // 한 계단 또는 두 계단 오르기를 선택할 수 있다\n const state = 0; // 0번째 계단에서 오르기 시작한다\n const res = new Map();\n res.set(0, 0); // res[0]에 방법의 수를 기록한다\n backtrack(choices, state, n, res);\n return res.get(0);\n}\n</code></pre> climbing_stairs_backtrack.dart<pre><code>/* 백트래킹 */\nvoid backtrack(List&lt;int&gt; choices, int state, int n, List&lt;int&gt; res) {\n // n번째 계단에 도달하면 방법의 수에 1을 더한다\n if (state == n) {\n res[0]++;\n }\n // 모든 선택지를 순회한다\n for (int choice in choices) {\n // 가지치기: n번째 계단을 넘어갈 수 없다\n if (state + choice &gt; n) continue;\n // 시도: 선택하고 상태를 갱신한다\n backtrack(choices, state + choice, n, res);\n // 되돌리기\n }\n}\n\n/* 계단 오르기: 백트래킹 */\nint climbingStairsBacktrack(int n) {\n List&lt;int&gt; choices = [1, 2]; // 한 계단 또는 두 계단 오르기를 선택할 수 있다\n int state = 0; // 0번째 계단에서 오르기 시작한다\n List&lt;int&gt; res = [];\n res.add(0); // res[0]에 방법의 수를 기록한다\n backtrack(choices, state, n, res);\n return res[0];\n}\n</code></pre> climbing_stairs_backtrack.rs<pre><code>/* 백트래킹 */\nfn backtrack(choices: &amp;[i32], state: i32, n: i32, res: &amp;mut [i32]) {\n // n번째 계단에 도달하면 방법의 수에 1을 더한다\n if state == n {\n res[0] = res[0] + 1;\n }\n // 모든 선택지를 순회한다\n for &amp;choice in choices {\n // 가지치기: n번째 계단을 넘어갈 수 없다\n if state + choice &gt; n {\n continue;\n }\n // 시도: 선택하고 상태를 갱신한다\n backtrack(choices, state + choice, n, res);\n // 되돌리기\n }\n}\n\n/* 계단 오르기: 백트래킹 */\nfn climbing_stairs_backtrack(n: usize) -&gt; i32 {\n let choices = vec![1, 2]; // 한 계단 또는 두 계단 오르기를 선택할 수 있다\n let state = 0; // 0번째 계단에서 오르기 시작한다\n let mut res = Vec::new();\n res.push(0); // res[0]에 방법의 수를 기록한다\n backtrack(&amp;choices, state, n as i32, &amp;mut res);\n res[0]\n}\n</code></pre> climbing_stairs_backtrack.c<pre><code>/* 백트래킹 */\nvoid backtrack(int *choices, int state, int n, int *res, int len) {\n // n번째 계단에 도달하면 방법의 수에 1을 더한다\n if (state == n)\n res[0]++;\n // 모든 선택지를 순회한다\n for (int i = 0; i &lt; len; i++) {\n int choice = choices[i];\n // 가지치기: n번째 계단을 넘어갈 수 없다\n if (state + choice &gt; n)\n continue;\n // 시도: 선택하고 상태를 갱신한다\n backtrack(choices, state + choice, n, res, len);\n // 되돌리기\n }\n}\n\n/* 계단 오르기: 백트래킹 */\nint climbingStairsBacktrack(int n) {\n int choices[2] = {1, 2}; // 한 계단 또는 두 계단 오르기를 선택할 수 있다\n int state = 0; // 0번째 계단에서 오르기 시작한다\n int *res = (int *)malloc(sizeof(int));\n *res = 0; // res[0]에 방법의 수를 기록한다\n int len = sizeof(choices) / sizeof(int);\n backtrack(choices, state, n, res, len);\n int result = *res;\n free(res);\n return result;\n}\n</code></pre> climbing_stairs_backtrack.kt<pre><code>/* 백트래킹 */\nfun backtrack(\n choices: MutableList&lt;Int&gt;,\n state: Int,\n n: Int,\n res: MutableList&lt;Int&gt;\n) {\n // n번째 계단에 도달하면 방법의 수에 1을 더한다\n if (state == n)\n res[0] = res[0] + 1\n // 모든 선택지를 순회한다\n for (choice in choices) {\n // 가지치기: n번째 계단을 넘어갈 수 없다\n if (state + choice &gt; n) continue\n // 시도: 선택하고 상태를 갱신한다\n backtrack(choices, state + choice, n, res)\n // 되돌리기\n }\n}\n\n/* 계단 오르기: 백트래킹 */\nfun climbingStairsBacktrack(n: Int): Int {\n val choices = mutableListOf(1, 2) // 한 계단 또는 두 계단 오르기를 선택할 수 있다\n val state = 0 // 0번째 계단에서 오르기 시작한다\n val res = mutableListOf&lt;Int&gt;()\n res.add(0) // res[0]에 방법의 수를 기록한다\n backtrack(choices, state, n, res)\n return res[0]\n}\n</code></pre> climbing_stairs_backtrack.rb<pre><code>### 백트래킹 ###\ndef backtrack(choices, state, n, res)\n # n번째 계단에 도달하면 방법의 수에 1을 더한다\n res[0] += 1 if state == n\n # 모든 선택지를 순회한다\n for choice in choices\n # 가지치기: n번째 계단을 넘어갈 수 없다\n next if state + choice &gt; n\n\n # 시도: 선택하고 상태를 갱신한다\n backtrack(choices, state + choice, n, res)\n end\n # 되돌리기\nend\n\n### 계단 오르기: 백트래킹 ###\ndef climbing_stairs_backtrack(n)\n choices = [1, 2] # 한 계단 또는 두 계단 오르기를 선택할 수 있다\n state = 0 # 0번째 계단에서 오르기 시작한다\n res = [0] # res[0]에 방법의 수를 기록한다\n backtrack(choices, state, n, res)\n res.first\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 14 장   동적 계획법","14.1   동적 계획법 첫걸음"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1411-1","level":2,"title":"14.1.1   방법 1: 완전 탐색","text":"<p>백트래킹은 보통 문제를 명시적으로 나누지 않고 일련의 의사 결정 단계로 바라보며, 시도와 가지치기를 통해 가능한 모든 해를 찾는다.</p> <p>이 문제를 부분 문제로 나누는 관점에서 분석해 보자. \\(i\\) 번째 칸에 오르는 방법 수를 \\(dp[i]\\)라고 하면 \\(dp[i]\\)가 원래 문제이고, 다음은 그 부분 문제들이다.</p> \\[ dp[i-1], dp[i-2], \\dots, dp[2], dp[1] \\] <p>한 번에 \\(1\\) 칸 또는 \\(2\\) 칸만 오를 수 있으므로, 현재 \\(i\\) 번째 칸에 있다면 직전에는 \\(i - 1\\) 번째 또는 \\(i - 2\\) 번째 칸에 있었어야 한다. 즉, \\(i -1\\) 번째 또는 \\(i - 2\\) 번째 칸에서만 \\(i\\) 번째 칸으로 올라올 수 있다.</p> <p>여기서 중요한 결론을 얻는다. \\(i - 1\\) 번째 칸에 오르는 방법 수와 \\(i - 2\\) 번째 칸에 오르는 방법 수를 더하면 \\(i\\) 번째 칸에 오르는 방법 수가 된다. 식으로 나타내면 다음과 같다.</p> \\[ dp[i] = dp[i-1] + dp[i-2] \\] <p>이는 계단 오르기 문제의 부분 문제들 사이에 점화 관계가 있으며, 부분 문제의 해로 원래 문제의 해를 구성할 수 있다는 뜻이다. 아래 그림(14-2)이 이 점화 관계를 보여 준다.</p> <p></p> <p> 그림 14-2   방법 수의 점화 관계 </p> <p>점화식으로 완전 탐색 풀이를 얻을 수 있다. \\(dp[n]\\)에서 출발해 큰 문제 하나를 더 작은 두 문제의 합으로 재귀적으로 나누고, 가장 작은 부분 문제 \\(dp[1]\\)과 \\(dp[2]\\)에 도달하면 반환한다. 이들의 해는 \\(dp[1] = 1\\), \\(dp[2] = 2\\)로 이미 알려져 있다. 즉, \\(1\\), \\(2\\) 번째 칸에 오르는 방법은 각각 \\(1\\), \\(2\\) 가지다.</p> <p>아래 코드는 일반적인 백트래킹 코드와 마찬가지로 깊이 우선 탐색이지만 더 간결하다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dfs.py<pre><code>def dfs(i: int) -&gt; int:\n \"\"\"탐색\"\"\"\n # dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n if i == 1 or i == 2:\n return i\n # dp[i] = dp[i-1] + dp[i-2]\n count = dfs(i - 1) + dfs(i - 2)\n return count\n\ndef climbing_stairs_dfs(n: int) -&gt; int:\n \"\"\"계단 오르기: 탐색\"\"\"\n return dfs(n)\n</code></pre> climbing_stairs_dfs.cpp<pre><code>/* 탐색 */\nint dfs(int i) {\n // dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n if (i == 1 || i == 2)\n return i;\n // dp[i] = dp[i-1] + dp[i-2]\n int count = dfs(i - 1) + dfs(i - 2);\n return count;\n}\n\n/* 계단 오르기: 탐색 */\nint climbingStairsDFS(int n) {\n return dfs(n);\n}\n</code></pre> climbing_stairs_dfs.java<pre><code>/* 탐색 */\nint dfs(int i) {\n // dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n if (i == 1 || i == 2)\n return i;\n // dp[i] = dp[i-1] + dp[i-2]\n int count = dfs(i - 1) + dfs(i - 2);\n return count;\n}\n\n/* 계단 오르기: 탐색 */\nint climbingStairsDFS(int n) {\n return dfs(n);\n}\n</code></pre> climbing_stairs_dfs.cs<pre><code>/* 탐색 */\nint DFS(int i) {\n // dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n if (i == 1 || i == 2)\n return i;\n // dp[i] = dp[i-1] + dp[i-2]\n int count = DFS(i - 1) + DFS(i - 2);\n return count;\n}\n\n/* 계단 오르기: 탐색 */\nint ClimbingStairsDFS(int n) {\n return DFS(n);\n}\n</code></pre> climbing_stairs_dfs.go<pre><code>/* 탐색 */\nfunc dfs(i int) int {\n // dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n if i == 1 || i == 2 {\n return i\n }\n // dp[i] = dp[i-1] + dp[i-2]\n count := dfs(i-1) + dfs(i-2)\n return count\n}\n\n/* 계단 오르기: 탐색 */\nfunc climbingStairsDFS(n int) int {\n return dfs(n)\n}\n</code></pre> climbing_stairs_dfs.swift<pre><code>/* 탐색 */\nfunc dfs(i: Int) -&gt; Int {\n // dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n if i == 1 || i == 2 {\n return i\n }\n // dp[i] = dp[i-1] + dp[i-2]\n let count = dfs(i: i - 1) + dfs(i: i - 2)\n return count\n}\n\n/* 계단 오르기: 탐색 */\nfunc climbingStairsDFS(n: Int) -&gt; Int {\n dfs(i: n)\n}\n</code></pre> climbing_stairs_dfs.js<pre><code>/* 탐색 */\nfunction dfs(i) {\n // dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n if (i === 1 || i === 2) return i;\n // dp[i] = dp[i-1] + dp[i-2]\n const count = dfs(i - 1) + dfs(i - 2);\n return count;\n}\n\n/* 계단 오르기: 탐색 */\nfunction climbingStairsDFS(n) {\n return dfs(n);\n}\n</code></pre> climbing_stairs_dfs.ts<pre><code>/* 탐색 */\nfunction dfs(i: number): number {\n // dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n if (i === 1 || i === 2) return i;\n // dp[i] = dp[i-1] + dp[i-2]\n const count = dfs(i - 1) + dfs(i - 2);\n return count;\n}\n\n/* 계단 오르기: 탐색 */\nfunction climbingStairsDFS(n: number): number {\n return dfs(n);\n}\n</code></pre> climbing_stairs_dfs.dart<pre><code>/* 탐색 */\nint dfs(int i) {\n // dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n if (i == 1 || i == 2) return i;\n // dp[i] = dp[i-1] + dp[i-2]\n int count = dfs(i - 1) + dfs(i - 2);\n return count;\n}\n\n/* 계단 오르기: 탐색 */\nint climbingStairsDFS(int n) {\n return dfs(n);\n}\n</code></pre> climbing_stairs_dfs.rs<pre><code>/* 탐색 */\nfn dfs(i: usize) -&gt; i32 {\n // dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n if i == 1 || i == 2 {\n return i as i32;\n }\n // dp[i] = dp[i-1] + dp[i-2]\n let count = dfs(i - 1) + dfs(i - 2);\n count\n}\n\n/* 계단 오르기: 탐색 */\nfn climbing_stairs_dfs(n: usize) -&gt; i32 {\n dfs(n)\n}\n</code></pre> climbing_stairs_dfs.c<pre><code>/* 탐색 */\nint dfs(int i) {\n // dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n if (i == 1 || i == 2)\n return i;\n // dp[i] = dp[i-1] + dp[i-2]\n int count = dfs(i - 1) + dfs(i - 2);\n return count;\n}\n\n/* 계단 오르기: 탐색 */\nint climbingStairsDFS(int n) {\n return dfs(n);\n}\n</code></pre> climbing_stairs_dfs.kt<pre><code>/* 탐색 */\nfun dfs(i: Int): Int {\n // dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n if (i == 1 || i == 2) return i\n // dp[i] = dp[i-1] + dp[i-2]\n val count = dfs(i - 1) + dfs(i - 2)\n return count\n}\n\n/* 계단 오르기: 탐색 */\nfun climbingStairsDFS(n: Int): Int {\n return dfs(n)\n}\n</code></pre> climbing_stairs_dfs.rb<pre><code>### 탐색 ###\ndef dfs(i)\n # dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n return i if i == 1 || i == 2\n # dp[i] = dp[i-1] + dp[i-2]\n dfs(i - 1) + dfs(i - 2)\nend\n\n### 계단 오르기: 탐색 ###\ndef climbing_stairs_dfs(n)\n dfs(n)\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>아래 그림(14-3)은 완전 탐색으로 만들어지는 재귀 트리다. 문제 \\(dp[n]\\)의 재귀 트리 깊이는 \\(n\\)이고 시간 복잡도는 \\(O(2^n)\\)이다. 지수적 증가는 폭발적이므로 비교적 큰 \\(n\\)을 입력하면 매우 오래 기다려야 한다.</p> <p></p> <p> 그림 14-3   계단 오르기의 재귀 트리 </p> <p>위 그림(14-3)에서 지수 시간 복잡도는 '중복 부분 문제' 때문에 생긴다. 예를 들어 \\(dp[9]\\)는 \\(dp[8]\\)과 \\(dp[7]\\)로, \\(dp[8]\\)은 \\(dp[7]\\)과 \\(dp[6]\\)으로 나뉘므로 두 경우 모두 부분 문제 \\(dp[7]\\)을 포함한다.</p> <p>그 아래의 부분 문제에도 더 작은 중복 부분 문제가 계속 생겨난다. 계산 자원의 대부분은 이러한 중복 부분 문제에 낭비된다.</p>","path":["제 14 장   동적 계획법","14.1   동적 계획법 첫걸음"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1412-2","level":2,"title":"14.1.2   방법 2: 메모이제이션을 이용한 탐색","text":"<p>효율을 높이려면 모든 중복 부분 문제를 한 번씩만 계산해야 한다. 이를 위해 각 부분 문제의 해를 기록하는 배열 <code>mem</code>을 만들고, 탐색 중 중복 부분 문제를 가지치기한다.</p> <ol> <li>\\(dp[i]\\)를 처음 계산하면 나중에 사용할 수 있도록 <code>mem[i]</code>에 기록한다.</li> <li>\\(dp[i]\\)가 다시 필요하면 <code>mem[i]</code>에서 결과를 바로 가져와 중복 계산을 피한다.</li> </ol> <p>코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dfs_mem.py<pre><code>def dfs(i: int, mem: list[int]) -&gt; int:\n \"\"\"메모이제이션을 이용한 탐색\"\"\"\n # dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n if i == 1 or i == 2:\n return i\n # dp[i]의 기록이 있으면 해당 값을 바로 반환한다\n if mem[i] != -1:\n return mem[i]\n # dp[i] = dp[i-1] + dp[i-2]\n count = dfs(i - 1, mem) + dfs(i - 2, mem)\n # dp[i]를 기록한다\n mem[i] = count\n return count\n\ndef climbing_stairs_dfs_mem(n: int) -&gt; int:\n \"\"\"계단 오르기: 메모이제이션을 이용한 탐색\"\"\"\n # mem[i]는 i번째 계단까지 오르는 전체 방법의 수를 기록하며, -1은 기록이 없음을 나타낸다\n mem = [-1] * (n + 1)\n return dfs(n, mem)\n</code></pre> climbing_stairs_dfs_mem.cpp<pre><code>/* 메모이제이션을 이용한 탐색 */\nint dfs(int i, vector&lt;int&gt; &amp;mem) {\n // dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n if (i == 1 || i == 2)\n return i;\n // dp[i]의 기록이 있으면 해당 값을 바로 반환한다\n if (mem[i] != -1)\n return mem[i];\n // dp[i] = dp[i-1] + dp[i-2]\n int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n // dp[i]를 기록한다\n mem[i] = count;\n return count;\n}\n\n/* 계단 오르기: 메모이제이션을 이용한 탐색 */\nint climbingStairsDFSMem(int n) {\n // mem[i]는 i번째 계단까지 오르는 전체 방법의 수를 기록하며, -1은 기록이 없음을 나타낸다\n vector&lt;int&gt; mem(n + 1, -1);\n return dfs(n, mem);\n}\n</code></pre> climbing_stairs_dfs_mem.java<pre><code>/* 메모이제이션을 이용한 탐색 */\nint dfs(int i, int[] mem) {\n // dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n if (i == 1 || i == 2)\n return i;\n // dp[i]의 기록이 있으면 해당 값을 바로 반환한다\n if (mem[i] != -1)\n return mem[i];\n // dp[i] = dp[i-1] + dp[i-2]\n int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n // dp[i]를 기록한다\n mem[i] = count;\n return count;\n}\n\n/* 계단 오르기: 메모이제이션을 이용한 탐색 */\nint climbingStairsDFSMem(int n) {\n // mem[i]는 i번째 계단까지 오르는 전체 방법의 수를 기록하며, -1은 기록이 없음을 나타낸다\n int[] mem = new int[n + 1];\n Arrays.fill(mem, -1);\n return dfs(n, mem);\n}\n</code></pre> climbing_stairs_dfs_mem.cs<pre><code>/* 메모이제이션을 이용한 탐색 */\nint DFS(int i, int[] mem) {\n // dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n if (i == 1 || i == 2)\n return i;\n // dp[i]의 기록이 있으면 해당 값을 바로 반환한다\n if (mem[i] != -1)\n return mem[i];\n // dp[i] = dp[i-1] + dp[i-2]\n int count = DFS(i - 1, mem) + DFS(i - 2, mem);\n // dp[i]를 기록한다\n mem[i] = count;\n return count;\n}\n\n/* 계단 오르기: 메모이제이션을 이용한 탐색 */\nint ClimbingStairsDFSMem(int n) {\n // mem[i]는 i번째 계단까지 오르는 전체 방법의 수를 기록하며, -1은 기록이 없음을 나타낸다\n int[] mem = new int[n + 1];\n Array.Fill(mem, -1);\n return DFS(n, mem);\n}\n</code></pre> climbing_stairs_dfs_mem.go<pre><code>/* 메모이제이션을 이용한 탐색 */\nfunc dfsMem(i int, mem []int) int {\n // dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n if i == 1 || i == 2 {\n return i\n }\n // dp[i]의 기록이 있으면 해당 값을 바로 반환한다\n if mem[i] != -1 {\n return mem[i]\n }\n // dp[i] = dp[i-1] + dp[i-2]\n count := dfsMem(i-1, mem) + dfsMem(i-2, mem)\n // dp[i]를 기록한다\n mem[i] = count\n return count\n}\n\n/* 계단 오르기: 메모이제이션을 이용한 탐색 */\nfunc climbingStairsDFSMem(n int) int {\n // mem[i]는 i번째 계단까지 오르는 전체 방법의 수를 기록하며, -1은 기록이 없음을 나타낸다\n mem := make([]int, n+1)\n for i := range mem {\n mem[i] = -1\n }\n return dfsMem(n, mem)\n}\n</code></pre> climbing_stairs_dfs_mem.swift<pre><code>/* 메모이제이션을 이용한 탐색 */\nfunc dfs(i: Int, mem: inout [Int]) -&gt; Int {\n // dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n if i == 1 || i == 2 {\n return i\n }\n // dp[i]의 기록이 있으면 해당 값을 바로 반환한다\n if mem[i] != -1 {\n return mem[i]\n }\n // dp[i] = dp[i-1] + dp[i-2]\n let count = dfs(i: i - 1, mem: &amp;mem) + dfs(i: i - 2, mem: &amp;mem)\n // dp[i]를 기록한다\n mem[i] = count\n return count\n}\n\n/* 계단 오르기: 메모이제이션을 이용한 탐색 */\nfunc climbingStairsDFSMem(n: Int) -&gt; Int {\n // mem[i]는 i번째 계단까지 오르는 전체 방법의 수를 기록하며, -1은 기록이 없음을 나타낸다\n var mem = Array(repeating: -1, count: n + 1)\n return dfs(i: n, mem: &amp;mem)\n}\n</code></pre> climbing_stairs_dfs_mem.js<pre><code>/* 메모이제이션을 이용한 탐색 */\nfunction dfs(i, mem) {\n // dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n if (i === 1 || i === 2) return i;\n // dp[i]의 기록이 있으면 해당 값을 바로 반환한다\n if (mem[i] != -1) return mem[i];\n // dp[i] = dp[i-1] + dp[i-2]\n const count = dfs(i - 1, mem) + dfs(i - 2, mem);\n // dp[i]를 기록한다\n mem[i] = count;\n return count;\n}\n\n/* 계단 오르기: 메모이제이션을 이용한 탐색 */\nfunction climbingStairsDFSMem(n) {\n // mem[i]는 i번째 계단까지 오르는 전체 방법의 수를 기록하며, -1은 기록이 없음을 나타낸다\n const mem = new Array(n + 1).fill(-1);\n return dfs(n, mem);\n}\n</code></pre> climbing_stairs_dfs_mem.ts<pre><code>/* 메모이제이션을 이용한 탐색 */\nfunction dfs(i: number, mem: number[]): number {\n // dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n if (i === 1 || i === 2) return i;\n // dp[i]의 기록이 있으면 해당 값을 바로 반환한다\n if (mem[i] != -1) return mem[i];\n // dp[i] = dp[i-1] + dp[i-2]\n const count = dfs(i - 1, mem) + dfs(i - 2, mem);\n // dp[i]를 기록한다\n mem[i] = count;\n return count;\n}\n\n/* 계단 오르기: 메모이제이션을 이용한 탐색 */\nfunction climbingStairsDFSMem(n: number): number {\n // mem[i]는 i번째 계단까지 오르는 전체 방법의 수를 기록하며, -1은 기록이 없음을 나타낸다\n const mem = new Array(n + 1).fill(-1);\n return dfs(n, mem);\n}\n</code></pre> climbing_stairs_dfs_mem.dart<pre><code>/* 메모이제이션을 이용한 탐색 */\nint dfs(int i, List&lt;int&gt; mem) {\n // dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n if (i == 1 || i == 2) return i;\n // dp[i]의 기록이 있으면 해당 값을 바로 반환한다\n if (mem[i] != -1) return mem[i];\n // dp[i] = dp[i-1] + dp[i-2]\n int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n // dp[i]를 기록한다\n mem[i] = count;\n return count;\n}\n\n/* 계단 오르기: 메모이제이션을 이용한 탐색 */\nint climbingStairsDFSMem(int n) {\n // mem[i]는 i번째 계단까지 오르는 전체 방법의 수를 기록하며, -1은 기록이 없음을 나타낸다\n List&lt;int&gt; mem = List.filled(n + 1, -1);\n return dfs(n, mem);\n}\n</code></pre> climbing_stairs_dfs_mem.rs<pre><code>/* 메모이제이션을 이용한 탐색 */\nfn dfs(i: usize, mem: &amp;mut [i32]) -&gt; i32 {\n // dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n if i == 1 || i == 2 {\n return i as i32;\n }\n // dp[i]의 기록이 있으면 해당 값을 바로 반환한다\n if mem[i] != -1 {\n return mem[i];\n }\n // dp[i] = dp[i-1] + dp[i-2]\n let count = dfs(i - 1, mem) + dfs(i - 2, mem);\n // dp[i]를 기록한다\n mem[i] = count;\n count\n}\n\n/* 계단 오르기: 메모이제이션을 이용한 탐색 */\nfn climbing_stairs_dfs_mem(n: usize) -&gt; i32 {\n // mem[i]는 i번째 계단까지 오르는 전체 방법의 수를 기록하며, -1은 기록이 없음을 나타낸다\n let mut mem = vec![-1; n + 1];\n dfs(n, &amp;mut mem)\n}\n</code></pre> climbing_stairs_dfs_mem.c<pre><code>/* 메모이제이션을 이용한 탐색 */\nint dfs(int i, int *mem) {\n // dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n if (i == 1 || i == 2)\n return i;\n // dp[i]의 기록이 있으면 해당 값을 바로 반환한다\n if (mem[i] != -1)\n return mem[i];\n // dp[i] = dp[i-1] + dp[i-2]\n int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n // dp[i]를 기록한다\n mem[i] = count;\n return count;\n}\n\n/* 계단 오르기: 메모이제이션을 이용한 탐색 */\nint climbingStairsDFSMem(int n) {\n // mem[i]는 i번째 계단까지 오르는 전체 방법의 수를 기록하며, -1은 기록이 없음을 나타낸다\n int *mem = (int *)malloc((n + 1) * sizeof(int));\n for (int i = 0; i &lt;= n; i++) {\n mem[i] = -1;\n }\n int result = dfs(n, mem);\n free(mem);\n return result;\n}\n</code></pre> climbing_stairs_dfs_mem.kt<pre><code>/* 메모이제이션을 이용한 탐색 */\nfun dfs(i: Int, mem: IntArray): Int {\n // dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n if (i == 1 || i == 2) return i\n // dp[i]의 기록이 있으면 해당 값을 바로 반환한다\n if (mem[i] != -1) return mem[i]\n // dp[i] = dp[i-1] + dp[i-2]\n val count = dfs(i - 1, mem) + dfs(i - 2, mem)\n // dp[i]를 기록한다\n mem[i] = count\n return count\n}\n\n/* 계단 오르기: 메모이제이션을 이용한 탐색 */\nfun climbingStairsDFSMem(n: Int): Int {\n // mem[i]는 i번째 계단까지 오르는 전체 방법의 수를 기록하며, -1은 기록이 없음을 나타낸다\n val mem = IntArray(n + 1)\n mem.fill(-1)\n return dfs(n, mem)\n}\n</code></pre> climbing_stairs_dfs_mem.rb<pre><code>### 메모이제이션을 이용한 탐색 ###\ndef dfs(i, mem)\n # dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n return i if i == 1 || i == 2\n # dp[i]의 기록이 있으면 해당 값을 바로 반환한다\n return mem[i] if mem[i] != -1\n\n # dp[i] = dp[i-1] + dp[i-2]\n count = dfs(i - 1, mem) + dfs(i - 2, mem)\n # dp[i]를 기록한다\n mem[i] = count\nend\n\n### 계단 오르기: 메모이제이션을 이용한 탐색 ###\ndef climbing_stairs_dfs_mem(n)\n # mem[i]는 i번째 계단까지 오르는 전체 방법의 수를 기록하며, -1은 기록이 없음을 나타낸다\n mem = Array.new(n + 1, -1)\n dfs(n, mem)\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>아래 그림(14-4)과 같이 메모이제이션을 적용하면 모든 중복 부분 문제를 한 번만 계산하므로 시간 복잡도가 \\(O(n)\\)으로 개선된다. 이는 매우 큰 발전이다.</p> <p></p> <p> 그림 14-4   메모이제이션을 이용한 탐색의 재귀 트리 </p>","path":["제 14 장   동적 계획법","14.1   동적 계획법 첫걸음"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1413-3","level":2,"title":"14.1.3   방법 3: 동적 계획법","text":"<p>메모이제이션을 이용한 탐색은 '위에서 아래로' 진행하는 방법이다. 원래 문제(루트 노드)에서 시작하여 큰 부분 문제를 작은 부분 문제로 재귀적으로 나누고, 해를 이미 아는 가장 작은 부분 문제(리프 노드)에 도달한다. 이후 되돌아오면서 각 단계의 해를 모아 원래 문제의 해를 구성한다.</p> <p>반대로 동적 계획법은 '아래에서 위로' 진행하는 방법이다. 가장 작은 부분 문제의 해에서 시작하여 반복적으로 더 큰 부분 문제의 해를 구성하고, 마침내 원래 문제의 해를 얻는다.</p> <p>동적 계획법에는 되돌아오는 과정이 없으므로 재귀 없이 반복문만으로 구현할 수 있다. 아래 코드에서는 부분 문제의 해를 저장하는 배열 <code>dp</code>를 초기화한다. 이는 메모이제이션을 이용한 탐색의 배열 <code>mem</code>과 같은 기록 역할을 한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dp.py<pre><code>def climbing_stairs_dp(n: int) -&gt; int:\n \"\"\"계단 오르기: 동적 계획법\"\"\"\n if n == 1 or n == 2:\n return n\n # 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n dp = [0] * (n + 1)\n # 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1], dp[2] = 1, 2\n # 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for i in range(3, n + 1):\n dp[i] = dp[i - 1] + dp[i - 2]\n return dp[n]\n</code></pre> climbing_stairs_dp.cpp<pre><code>/* 계단 오르기: 동적 계획법 */\nint climbingStairsDP(int n) {\n if (n == 1 || n == 2)\n return n;\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n vector&lt;int&gt; dp(n + 1);\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1] = 1;\n dp[2] = 2;\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for (int i = 3; i &lt;= n; i++) {\n dp[i] = dp[i - 1] + dp[i - 2];\n }\n return dp[n];\n}\n</code></pre> climbing_stairs_dp.java<pre><code>/* 계단 오르기: 동적 계획법 */\nint climbingStairsDP(int n) {\n if (n == 1 || n == 2)\n return n;\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n int[] dp = new int[n + 1];\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1] = 1;\n dp[2] = 2;\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for (int i = 3; i &lt;= n; i++) {\n dp[i] = dp[i - 1] + dp[i - 2];\n }\n return dp[n];\n}\n</code></pre> climbing_stairs_dp.cs<pre><code>/* 계단 오르기: 동적 계획법 */\nint ClimbingStairsDP(int n) {\n if (n == 1 || n == 2)\n return n;\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n int[] dp = new int[n + 1];\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1] = 1;\n dp[2] = 2;\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for (int i = 3; i &lt;= n; i++) {\n dp[i] = dp[i - 1] + dp[i - 2];\n }\n return dp[n];\n}\n</code></pre> climbing_stairs_dp.go<pre><code>/* 계단 오르기: 동적 계획법 */\nfunc climbingStairsDP(n int) int {\n if n == 1 || n == 2 {\n return n\n }\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n dp := make([]int, n+1)\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1] = 1\n dp[2] = 2\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for i := 3; i &lt;= n; i++ {\n dp[i] = dp[i-1] + dp[i-2]\n }\n return dp[n]\n}\n</code></pre> climbing_stairs_dp.swift<pre><code>/* 계단 오르기: 동적 계획법 */\nfunc climbingStairsDP(n: Int) -&gt; Int {\n if n == 1 || n == 2 {\n return n\n }\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n var dp = Array(repeating: 0, count: n + 1)\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1] = 1\n dp[2] = 2\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for i in 3 ... n {\n dp[i] = dp[i - 1] + dp[i - 2]\n }\n return dp[n]\n}\n</code></pre> climbing_stairs_dp.js<pre><code>/* 계단 오르기: 동적 계획법 */\nfunction climbingStairsDP(n) {\n if (n === 1 || n === 2) return n;\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n const dp = new Array(n + 1).fill(-1);\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1] = 1;\n dp[2] = 2;\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for (let i = 3; i &lt;= n; i++) {\n dp[i] = dp[i - 1] + dp[i - 2];\n }\n return dp[n];\n}\n</code></pre> climbing_stairs_dp.ts<pre><code>/* 계단 오르기: 동적 계획법 */\nfunction climbingStairsDP(n: number): number {\n if (n === 1 || n === 2) return n;\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n const dp = new Array(n + 1).fill(-1);\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1] = 1;\n dp[2] = 2;\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for (let i = 3; i &lt;= n; i++) {\n dp[i] = dp[i - 1] + dp[i - 2];\n }\n return dp[n];\n}\n</code></pre> climbing_stairs_dp.dart<pre><code>/* 계단 오르기: 동적 계획법 */\nint climbingStairsDP(int n) {\n if (n == 1 || n == 2) return n;\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n List&lt;int&gt; dp = List.filled(n + 1, 0);\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1] = 1;\n dp[2] = 2;\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for (int i = 3; i &lt;= n; i++) {\n dp[i] = dp[i - 1] + dp[i - 2];\n }\n return dp[n];\n}\n</code></pre> climbing_stairs_dp.rs<pre><code>/* 계단 오르기: 동적 계획법 */\nfn climbing_stairs_dp(n: usize) -&gt; i32 {\n // dp[1]과 dp[2]는 이미 알고 있으므로 해당 값을 반환한다\n if n == 1 || n == 2 {\n return n as i32;\n }\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n let mut dp = vec![-1; n + 1];\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1] = 1;\n dp[2] = 2;\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for i in 3..=n {\n dp[i] = dp[i - 1] + dp[i - 2];\n }\n dp[n]\n}\n</code></pre> climbing_stairs_dp.c<pre><code>/* 계단 오르기: 동적 계획법 */\nint climbingStairsDP(int n) {\n if (n == 1 || n == 2)\n return n;\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n int *dp = (int *)malloc((n + 1) * sizeof(int));\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1] = 1;\n dp[2] = 2;\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for (int i = 3; i &lt;= n; i++) {\n dp[i] = dp[i - 1] + dp[i - 2];\n }\n int result = dp[n];\n free(dp);\n return result;\n}\n</code></pre> climbing_stairs_dp.kt<pre><code>/* 계단 오르기: 동적 계획법 */\nfun climbingStairsDP(n: Int): Int {\n if (n == 1 || n == 2) return n\n // 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n val dp = IntArray(n + 1)\n // 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1] = 1\n dp[2] = 2\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for (i in 3..n) {\n dp[i] = dp[i - 1] + dp[i - 2]\n }\n return dp[n]\n}\n</code></pre> climbing_stairs_dp.rb<pre><code>### 계단 오르기: 동적 계획법 ###\ndef climbing_stairs_dp(n)\n return n if n == 1 || n == 2\n\n # 부분 문제의 해를 저장할 dp 테이블을 초기화한다\n dp = Array.new(n + 1, 0)\n # 초기 상태: 가장 작은 부분 문제의 해를 미리 설정한다\n dp[1], dp[2] = 1, 2\n # 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n (3...(n + 1)).each { |i| dp[i] = dp[i - 1] + dp[i - 2] }\n\n dp[n]\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>아래 그림(14-5)은 위 코드의 실행 과정을 보여 준다.</p> <p></p> <p> 그림 14-5   계단 오르기의 동적 계획법 과정 </p> <p>백트래킹과 마찬가지로 동적 계획법도 '상태'로 문제 해결의 특정 단계를 나타낸다. 각 상태는 부분 문제 하나와 그에 해당하는 국소 최적해에 대응한다. 예를 들어 계단 오르기 문제의 상태는 현재 위치한 계단의 칸 번호 \\(i\\)로 정의한다.</p> <p>위 내용을 바탕으로 동적 계획법의 주요 용어를 정리할 수 있다.</p> <ul> <li>배열 <code>dp</code>를 dp 테이블이라고 하며, \\(dp[i]\\)는 상태 \\(i\\)에 대응하는 부분 문제의 해다.</li> <li>가장 작은 부분 문제에 해당하는 상태(계단의 \\(1\\), \\(2\\) 번째 칸)를 초기 상태라고 한다.</li> <li>점화식 \\(dp[i] = dp[i-1] + dp[i-2]\\)를 상태 전이 식이라고 한다.</li> </ul>","path":["제 14 장   동적 계획법","14.1   동적 계획법 첫걸음"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1414","level":2,"title":"14.1.4   공간 최적화","text":"<p>주의 깊게 읽었다면 \\(dp[i]\\)는 \\(dp[i-1]\\)과 \\(dp[i-2]\\)에만 의존하므로 배열 <code>dp</code>에 모든 부분 문제의 해를 저장할 필요가 없음을 알아차렸을 것이다. 변수 두 개를 차례로 갱신하며 진행하면 된다. 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dp.py<pre><code>def climbing_stairs_dp_comp(n: int) -&gt; int:\n \"\"\"계단 오르기: 공간을 최적화한 동적 계획법\"\"\"\n if n == 1 or n == 2:\n return n\n a, b = 1, 2\n for _ in range(3, n + 1):\n a, b = b, a + b\n return b\n</code></pre> climbing_stairs_dp.cpp<pre><code>/* 계단 오르기: 공간을 최적화한 동적 계획법 */\nint climbingStairsDPComp(int n) {\n if (n == 1 || n == 2)\n return n;\n int a = 1, b = 2;\n for (int i = 3; i &lt;= n; i++) {\n int tmp = b;\n b = a + b;\n a = tmp;\n }\n return b;\n}\n</code></pre> climbing_stairs_dp.java<pre><code>/* 계단 오르기: 공간을 최적화한 동적 계획법 */\nint climbingStairsDPComp(int n) {\n if (n == 1 || n == 2)\n return n;\n int a = 1, b = 2;\n for (int i = 3; i &lt;= n; i++) {\n int tmp = b;\n b = a + b;\n a = tmp;\n }\n return b;\n}\n</code></pre> climbing_stairs_dp.cs<pre><code>/* 계단 오르기: 공간을 최적화한 동적 계획법 */\nint ClimbingStairsDPComp(int n) {\n if (n == 1 || n == 2)\n return n;\n int a = 1, b = 2;\n for (int i = 3; i &lt;= n; i++) {\n int tmp = b;\n b = a + b;\n a = tmp;\n }\n return b;\n}\n</code></pre> climbing_stairs_dp.go<pre><code>/* 계단 오르기: 공간을 최적화한 동적 계획법 */\nfunc climbingStairsDPComp(n int) int {\n if n == 1 || n == 2 {\n return n\n }\n a, b := 1, 2\n // 상태 전이: 작은 부분 문제부터 점차 큰 부분 문제를 푼다\n for i := 3; i &lt;= n; i++ {\n a, b = b, a+b\n }\n return b\n}\n</code></pre> climbing_stairs_dp.swift<pre><code>/* 계단 오르기: 공간을 최적화한 동적 계획법 */\nfunc climbingStairsDPComp(n: Int) -&gt; Int {\n if n == 1 || n == 2 {\n return n\n }\n var a = 1\n var b = 2\n for _ in 3 ... n {\n (a, b) = (b, a + b)\n }\n return b\n}\n</code></pre> climbing_stairs_dp.js<pre><code>/* 계단 오르기: 공간을 최적화한 동적 계획법 */\nfunction climbingStairsDPComp(n) {\n if (n === 1 || n === 2) return n;\n let a = 1,\n b = 2;\n for (let i = 3; i &lt;= n; i++) {\n const tmp = b;\n b = a + b;\n a = tmp;\n }\n return b;\n}\n</code></pre> climbing_stairs_dp.ts<pre><code>/* 계단 오르기: 공간을 최적화한 동적 계획법 */\nfunction climbingStairsDPComp(n: number): number {\n if (n === 1 || n === 2) return n;\n let a = 1,\n b = 2;\n for (let i = 3; i &lt;= n; i++) {\n const tmp = b;\n b = a + b;\n a = tmp;\n }\n return b;\n}\n</code></pre> climbing_stairs_dp.dart<pre><code>/* 계단 오르기: 공간을 최적화한 동적 계획법 */\nint climbingStairsDPComp(int n) {\n if (n == 1 || n == 2) return n;\n int a = 1, b = 2;\n for (int i = 3; i &lt;= n; i++) {\n int tmp = b;\n b = a + b;\n a = tmp;\n }\n return b;\n}\n</code></pre> climbing_stairs_dp.rs<pre><code>/* 계단 오르기: 공간을 최적화한 동적 계획법 */\nfn climbing_stairs_dp_comp(n: usize) -&gt; i32 {\n if n == 1 || n == 2 {\n return n as i32;\n }\n let (mut a, mut b) = (1, 2);\n for _ in 3..=n {\n let tmp = b;\n b = a + b;\n a = tmp;\n }\n b\n}\n</code></pre> climbing_stairs_dp.c<pre><code>/* 계단 오르기: 공간을 최적화한 동적 계획법 */\nint climbingStairsDPComp(int n) {\n if (n == 1 || n == 2)\n return n;\n int a = 1, b = 2;\n for (int i = 3; i &lt;= n; i++) {\n int tmp = b;\n b = a + b;\n a = tmp;\n }\n return b;\n}\n</code></pre> climbing_stairs_dp.kt<pre><code>/* 계단 오르기: 공간을 최적화한 동적 계획법 */\nfun climbingStairsDPComp(n: Int): Int {\n if (n == 1 || n == 2) return n\n var a = 1\n var b = 2\n for (i in 3..n) {\n val temp = b\n b += a\n a = temp\n }\n return b\n}\n</code></pre> climbing_stairs_dp.rb<pre><code>### 계단 오르기: 공간을 최적화한 동적 계획법 ###\ndef climbing_stairs_dp_comp(n)\n return n if n == 1 || n == 2\n\n a, b = 1, 2\n (3...(n + 1)).each { a, b = b, a + b }\n\n b\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>위 코드에서는 배열 <code>dp</code>의 공간을 없앴으므로 공간 복잡도가 \\(O(n)\\)에서 \\(O(1)\\)로 줄어든다.</p> <p>동적 계획법의 현재 상태는 앞선 몇 개의 상태에만 의존하는 경우가 많다. 이때 필요한 상태만 남겨 '차원을 줄이면' 메모리를 절약할 수 있다. 이 공간 최적화 기법을 '롤링 변수' 또는 '롤링 배열'이라고 한다.</p>","path":["제 14 장   동적 계획법","14.1   동적 계획법 첫걸음"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/","level":1,"title":"14.4   0-1 배낭 문제","text":"<p>배낭 문제는 동적 계획법 입문에 매우 좋은 예제이며 가장 흔한 문제 유형이다. 0-1 배낭, 무한 배낭, 개수 제한 배낭 등 여러 변형이 있다.</p> <p>이 절에서는 가장 흔한 0-1 배낭 문제부터 해결한다.</p> <p>문제</p> <p>물건 \\(n\\) 개와 용량이 \\(cap\\)인 배낭이 주어진다. \\(i\\) 번째 물건의 무게는 \\(wgt[i-1]\\), 가치는 \\(val[i-1]\\)이다. 각 물건을 한 번만 선택할 수 있을 때, 배낭 용량 제한 안에 담을 수 있는 물건의 최대 가치를 구하라.</p> <p>아래 그림(14-17)에서 물건 번호 \\(i\\)는 \\(1\\)부터, 배열 인덱스는 \\(0\\)부터 시작하므로 물건 \\(i\\)의 무게와 가치는 각각 \\(wgt[i-1]\\), \\(val[i-1]\\)이다.</p> <p></p> <p> 그림 14-17   0-1 배낭 문제 예시 데이터 </p> <p>0-1 배낭 문제는 \\(n\\) 차례의 결정으로 이루어진 과정으로 볼 수 있다. 각 물건에는 넣지 않기와 넣기라는 두 결정이 있으므로 의사 결정 트리 모형을 만족한다.</p> <p>목표가 '배낭 용량 제한 안에 담을 수 있는 최대 가치'이므로 동적 계획법 문제일 가능성이 높다.</p> <p>1단계: 매 차례의 결정을 생각하고 상태를 정의하여 \\(dp\\) 테이블을 얻는다</p> <p>물건을 넣지 않으면 배낭 용량은 그대로이고, 넣으면 용량이 줄어든다. 따라서 현재 물건 번호 \\(i\\)와 배낭 용량 \\(c\\)로 상태를 정의하고 \\([i, c]\\)라고 한다.</p> <p>상태 \\([i, c]\\)의 부분 문제는 앞의 \\(i\\) 개 물건을 용량 \\(c\\)인 배낭에 담았을 때의 최대 가치이며, 이를 \\(dp[i, c]\\)라고 한다.</p> <p>구할 값은 \\(dp[n, cap]\\)이므로 \\((n+1) \\times (cap+1)\\) 크기의 2차원 \\(dp\\) 테이블이 필요하다.</p> <p>2단계: 최적 부분 구조를 찾아 상태 전이 식을 도출한다</p> <p>물건 \\(i\\)에 대한 결정을 내리면 앞의 \\(i-1\\) 개 물건을 결정하는 부분 문제가 남는다. 다음 두 경우로 나뉜다.</p> <ul> <li>물건 \\(i\\)를 넣지 않음: 용량은 그대로이며 상태는 \\([i-1, c]\\)로 바뀐다.</li> <li>물건 \\(i\\)를 넣음: 용량이 \\(wgt[i-1]\\) 줄고 가치가 \\(val[i-1]\\) 늘며, 상태는 \\([i-1, c-wgt[i-1]]\\)로 바뀐다.</li> </ul> <p>이 분석에서 최적 부분 구조가 드러난다. 최대 가치 \\(dp[i, c]\\)는 물건 \\(i\\)를 넣지 않는 경우와 물건 \\(i\\)를 넣는 경우 중 가치가 더 큰 쪽이다. 상태 전이 식은 다음과 같다.</p> \\[ dp[i, c] = \\max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1]) \\] <p>현재 물건의 무게 \\(wgt[i - 1]\\)가 남은 용량 \\(c\\)를 넘으면 넣지 않는 선택만 가능하다.</p> <p>3단계: 경계 조건과 상태 전이 순서를 정한다</p> <p>물건이 없거나 배낭 용량이 \\(0\\)이면 최대 가치는 \\(0\\)이다. 즉, 첫 열 \\(dp[i, 0]\\)과 첫 행 \\(dp[0, c]\\)는 모두 \\(0\\)이다.</p> <p>현재 상태 \\([i, c]\\)는 바로 위의 \\([i-1, c]\\)와 왼쪽 위의 \\([i-1, c-wgt[i-1]]\\)에서 전이되므로, 이중 반복문으로 전체 \\(dp\\) 테이블을 정방향으로 순회하면 된다.</p> <p>이 분석을 바탕으로 완전 탐색, 메모이제이션을 이용한 탐색, 동적 계획법 순서로 구현한다.</p>","path":["제 14 장   동적 계획법","14.4   0-1 배낭 문제"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#1-1","level":3,"title":"1.   방법 1: 완전 탐색","text":"<p>탐색 코드에는 다음 요소가 포함된다.</p> <ul> <li>재귀 매개변수: 상태 \\([i, c]\\).</li> <li>반환값: 부분 문제의 해 \\(dp[i, c]\\).</li> <li>종료 조건: 물건 번호가 범위를 벗어나 \\(i = 0\\)이 되거나 남은 용량이 \\(0\\)이면 재귀를 종료하고 가치 \\(0\\)을 반환한다.</li> <li>가지치기: 현재 물건의 무게가 남은 배낭 용량보다 크면 넣지 않는 선택만 할 수 있다.</li> </ul> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py<pre><code>def knapsack_dfs(wgt: list[int], val: list[int], i: int, c: int) -&gt; int:\n \"\"\"0-1 배낭: 완전 탐색\"\"\"\n # 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n if i == 0 or c == 0:\n return 0\n # 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n if wgt[i - 1] &gt; c:\n return knapsack_dfs(wgt, val, i - 1, c)\n # 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n no = knapsack_dfs(wgt, val, i - 1, c)\n yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]\n # 두 경우 중 더 큰 가치를 반환한다\n return max(no, yes)\n</code></pre> knapsack.cpp<pre><code>/* 0-1 배낭: 완전 탐색 */\nint knapsackDFS(vector&lt;int&gt; &amp;wgt, vector&lt;int&gt; &amp;val, int i, int c) {\n // 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n if (i == 0 || c == 0) {\n return 0;\n }\n // 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n if (wgt[i - 1] &gt; c) {\n return knapsackDFS(wgt, val, i - 1, c);\n }\n // 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n int no = knapsackDFS(wgt, val, i - 1, c);\n int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n // 두 경우 중 더 큰 가치를 반환한다\n return max(no, yes);\n}\n</code></pre> knapsack.java<pre><code>/* 0-1 배낭: 완전 탐색 */\nint knapsackDFS(int[] wgt, int[] val, int i, int c) {\n // 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n if (i == 0 || c == 0) {\n return 0;\n }\n // 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n if (wgt[i - 1] &gt; c) {\n return knapsackDFS(wgt, val, i - 1, c);\n }\n // 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n int no = knapsackDFS(wgt, val, i - 1, c);\n int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n // 두 경우 중 더 큰 가치를 반환한다\n return Math.max(no, yes);\n}\n</code></pre> knapsack.cs<pre><code>/* 0-1 배낭: 완전 탐색 */\nint KnapsackDFS(int[] weight, int[] val, int i, int c) {\n // 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n if (i == 0 || c == 0) {\n return 0;\n }\n // 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n if (weight[i - 1] &gt; c) {\n return KnapsackDFS(weight, val, i - 1, c);\n }\n // 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n int no = KnapsackDFS(weight, val, i - 1, c);\n int yes = KnapsackDFS(weight, val, i - 1, c - weight[i - 1]) + val[i - 1];\n // 두 경우 중 더 큰 가치를 반환한다\n return Math.Max(no, yes);\n}\n</code></pre> knapsack.go<pre><code>/* 0-1 배낭: 완전 탐색 */\nfunc knapsackDFS(wgt, val []int, i, c int) int {\n // 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n if i == 0 || c == 0 {\n return 0\n }\n // 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n if wgt[i-1] &gt; c {\n return knapsackDFS(wgt, val, i-1, c)\n }\n // 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n no := knapsackDFS(wgt, val, i-1, c)\n yes := knapsackDFS(wgt, val, i-1, c-wgt[i-1]) + val[i-1]\n // 두 경우 중 더 큰 가치를 반환한다\n return int(math.Max(float64(no), float64(yes)))\n}\n</code></pre> knapsack.swift<pre><code>/* 0-1 배낭: 완전 탐색 */\nfunc knapsackDFS(wgt: [Int], val: [Int], i: Int, c: Int) -&gt; Int {\n // 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n if i == 0 || c == 0 {\n return 0\n }\n // 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n if wgt[i - 1] &gt; c {\n return knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c)\n }\n // 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n let no = knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c)\n let yes = knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c - wgt[i - 1]) + val[i - 1]\n // 두 경우 중 더 큰 가치를 반환한다\n return max(no, yes)\n}\n</code></pre> knapsack.js<pre><code>/* 0-1 배낭: 완전 탐색 */\nfunction knapsackDFS(wgt, val, i, c) {\n // 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n if (i === 0 || c === 0) {\n return 0;\n }\n // 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n if (wgt[i - 1] &gt; c) {\n return knapsackDFS(wgt, val, i - 1, c);\n }\n // 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n const no = knapsackDFS(wgt, val, i - 1, c);\n const yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n // 두 경우 중 더 큰 가치를 반환한다\n return Math.max(no, yes);\n}\n</code></pre> knapsack.ts<pre><code>/* 0-1 배낭: 완전 탐색 */\nfunction knapsackDFS(\n wgt: Array&lt;number&gt;,\n val: Array&lt;number&gt;,\n i: number,\n c: number\n): number {\n // 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n if (i === 0 || c === 0) {\n return 0;\n }\n // 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n if (wgt[i - 1] &gt; c) {\n return knapsackDFS(wgt, val, i - 1, c);\n }\n // 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n const no = knapsackDFS(wgt, val, i - 1, c);\n const yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n // 두 경우 중 더 큰 가치를 반환한다\n return Math.max(no, yes);\n}\n</code></pre> knapsack.dart<pre><code>/* 0-1 배낭: 완전 탐색 */\nint knapsackDFS(List&lt;int&gt; wgt, List&lt;int&gt; val, int i, int c) {\n // 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n if (i == 0 || c == 0) {\n return 0;\n }\n // 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n if (wgt[i - 1] &gt; c) {\n return knapsackDFS(wgt, val, i - 1, c);\n }\n // 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n int no = knapsackDFS(wgt, val, i - 1, c);\n int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n // 두 경우 중 더 큰 가치를 반환한다\n return max(no, yes);\n}\n</code></pre> knapsack.rs<pre><code>/* 0-1 배낭: 완전 탐색 */\nfn knapsack_dfs(wgt: &amp;[i32], val: &amp;[i32], i: usize, c: usize) -&gt; i32 {\n // 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n if i == 0 || c == 0 {\n return 0;\n }\n // 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n if wgt[i - 1] &gt; c as i32 {\n return knapsack_dfs(wgt, val, i - 1, c);\n }\n // 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n let no = knapsack_dfs(wgt, val, i - 1, c);\n let yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1] as usize) + val[i - 1];\n // 두 경우 중 더 큰 가치를 반환한다\n std::cmp::max(no, yes)\n}\n</code></pre> knapsack.c<pre><code>/* 0-1 배낭: 완전 탐색 */\nint knapsackDFS(int wgt[], int val[], int i, int c) {\n // 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n if (i == 0 || c == 0) {\n return 0;\n }\n // 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n if (wgt[i - 1] &gt; c) {\n return knapsackDFS(wgt, val, i - 1, c);\n }\n // 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n int no = knapsackDFS(wgt, val, i - 1, c);\n int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n // 두 경우 중 더 큰 가치를 반환한다\n return myMax(no, yes);\n}\n</code></pre> knapsack.kt<pre><code>/* 0-1 배낭: 완전 탐색 */\nfun knapsackDFS(\n wgt: IntArray,\n _val: IntArray,\n i: Int,\n c: Int\n): Int {\n // 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n if (i == 0 || c == 0) {\n return 0\n }\n // 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n if (wgt[i - 1] &gt; c) {\n return knapsackDFS(wgt, _val, i - 1, c)\n }\n // 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n val no = knapsackDFS(wgt, _val, i - 1, c)\n val yes = knapsackDFS(wgt, _val, i - 1, c - wgt[i - 1]) + _val[i - 1]\n // 두 경우 중 더 큰 가치를 반환한다\n return max(no, yes)\n}\n</code></pre> knapsack.rb<pre><code>### 0-1 배낭: 완전 탐색 ###\ndef knapsack_dfs(wgt, val, i, c)\n # 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n return 0 if i == 0 || c == 0\n # 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n return knapsack_dfs(wgt, val, i - 1, c) if wgt[i - 1] &gt; c\n # 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n no = knapsack_dfs(wgt, val, i - 1, c)\n yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]\n # 두 경우 중 더 큰 가치를 반환한다\n [no, yes].max\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>아래 그림(14-18)과 같이 물건마다 선택하지 않기와 선택하기라는 두 탐색 가지가 생기므로 시간 복잡도는 \\(O(2^n)\\)이다.</p> <p>재귀 트리에는 \\(dp[1, 10]\\) 같은 중복 부분 문제가 있음을 쉽게 알 수 있다. 물건이 많고 용량이 크며, 특히 무게가 같은 물건이 많을수록 중복 부분 문제의 수가 크게 늘어난다.</p> <p></p> <p> 그림 14-18   0-1 배낭 문제의 완전 탐색 재귀 트리 </p>","path":["제 14 장   동적 계획법","14.4   0-1 배낭 문제"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#2-2","level":3,"title":"2.   방법 2: 메모이제이션을 이용한 탐색","text":"<p>중복 부분 문제를 한 번만 계산하도록 메모이제이션 리스트 <code>mem</code>에 부분 문제의 해를 저장한다. <code>mem[i][c]</code>는 \\(dp[i, c]\\)에 대응한다.</p> <p>메모이제이션을 도입하면 시간 복잡도는 부분 문제의 수에 의해 결정되므로 \\(O(n \\times cap)\\)이다. 구현 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py<pre><code>def knapsack_dfs_mem(\n wgt: list[int], val: list[int], mem: list[list[int]], i: int, c: int\n) -&gt; int:\n \"\"\"0-1 배낭: 메모이제이션을 이용한 탐색\"\"\"\n # 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n if i == 0 or c == 0:\n return 0\n # 기록이 이미 있으면 바로 반환한다\n if mem[i][c] != -1:\n return mem[i][c]\n # 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n if wgt[i - 1] &gt; c:\n return knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n # 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n no = knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]\n # 두 경우 중 더 큰 가치를 기록하고 반환한다\n mem[i][c] = max(no, yes)\n return mem[i][c]\n</code></pre> knapsack.cpp<pre><code>/* 0-1 배낭: 메모이제이션을 이용한 탐색 */\nint knapsackDFSMem(vector&lt;int&gt; &amp;wgt, vector&lt;int&gt; &amp;val, vector&lt;vector&lt;int&gt;&gt; &amp;mem, int i, int c) {\n // 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n if (i == 0 || c == 0) {\n return 0;\n }\n // 기록이 이미 있으면 바로 반환한다\n if (mem[i][c] != -1) {\n return mem[i][c];\n }\n // 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n if (wgt[i - 1] &gt; c) {\n return knapsackDFSMem(wgt, val, mem, i - 1, c);\n }\n // 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n int no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n // 두 경우 중 더 큰 가치를 기록하고 반환한다\n mem[i][c] = max(no, yes);\n return mem[i][c];\n}\n</code></pre> knapsack.java<pre><code>/* 0-1 배낭: 메모이제이션을 이용한 탐색 */\nint knapsackDFSMem(int[] wgt, int[] val, int[][] mem, int i, int c) {\n // 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n if (i == 0 || c == 0) {\n return 0;\n }\n // 기록이 이미 있으면 바로 반환한다\n if (mem[i][c] != -1) {\n return mem[i][c];\n }\n // 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n if (wgt[i - 1] &gt; c) {\n return knapsackDFSMem(wgt, val, mem, i - 1, c);\n }\n // 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n int no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n // 두 경우 중 더 큰 가치를 기록하고 반환한다\n mem[i][c] = Math.max(no, yes);\n return mem[i][c];\n}\n</code></pre> knapsack.cs<pre><code>/* 0-1 배낭: 메모이제이션을 이용한 탐색 */\nint KnapsackDFSMem(int[] weight, int[] val, int[][] mem, int i, int c) {\n // 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n if (i == 0 || c == 0) {\n return 0;\n }\n // 기록이 이미 있으면 바로 반환한다\n if (mem[i][c] != -1) {\n return mem[i][c];\n }\n // 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n if (weight[i - 1] &gt; c) {\n return KnapsackDFSMem(weight, val, mem, i - 1, c);\n }\n // 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n int no = KnapsackDFSMem(weight, val, mem, i - 1, c);\n int yes = KnapsackDFSMem(weight, val, mem, i - 1, c - weight[i - 1]) + val[i - 1];\n // 두 경우 중 더 큰 가치를 기록하고 반환한다\n mem[i][c] = Math.Max(no, yes);\n return mem[i][c];\n}\n</code></pre> knapsack.go<pre><code>/* 0-1 배낭: 메모이제이션을 이용한 탐색 */\nfunc knapsackDFSMem(wgt, val []int, mem [][]int, i, c int) int {\n // 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n if i == 0 || c == 0 {\n return 0\n }\n // 기록이 이미 있으면 바로 반환한다\n if mem[i][c] != -1 {\n return mem[i][c]\n }\n // 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n if wgt[i-1] &gt; c {\n return knapsackDFSMem(wgt, val, mem, i-1, c)\n }\n // 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n no := knapsackDFSMem(wgt, val, mem, i-1, c)\n yes := knapsackDFSMem(wgt, val, mem, i-1, c-wgt[i-1]) + val[i-1]\n // 두 경우 중 더 큰 가치를 반환한다\n mem[i][c] = int(math.Max(float64(no), float64(yes)))\n return mem[i][c]\n}\n</code></pre> knapsack.swift<pre><code>/* 0-1 배낭: 메모이제이션을 이용한 탐색 */\nfunc knapsackDFSMem(wgt: [Int], val: [Int], mem: inout [[Int]], i: Int, c: Int) -&gt; Int {\n // 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n if i == 0 || c == 0 {\n return 0\n }\n // 기록이 이미 있으면 바로 반환한다\n if mem[i][c] != -1 {\n return mem[i][c]\n }\n // 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n if wgt[i - 1] &gt; c {\n return knapsackDFSMem(wgt: wgt, val: val, mem: &amp;mem, i: i - 1, c: c)\n }\n // 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n let no = knapsackDFSMem(wgt: wgt, val: val, mem: &amp;mem, i: i - 1, c: c)\n let yes = knapsackDFSMem(wgt: wgt, val: val, mem: &amp;mem, i: i - 1, c: c - wgt[i - 1]) + val[i - 1]\n // 두 경우 중 더 큰 가치를 기록하고 반환한다\n mem[i][c] = max(no, yes)\n return mem[i][c]\n}\n</code></pre> knapsack.js<pre><code>/* 0-1 배낭: 메모이제이션을 이용한 탐색 */\nfunction knapsackDFSMem(wgt, val, mem, i, c) {\n // 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n if (i === 0 || c === 0) {\n return 0;\n }\n // 기록이 이미 있으면 바로 반환한다\n if (mem[i][c] !== -1) {\n return mem[i][c];\n }\n // 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n if (wgt[i - 1] &gt; c) {\n return knapsackDFSMem(wgt, val, mem, i - 1, c);\n }\n // 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n const no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n const yes =\n knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n // 두 경우 중 더 큰 가치를 기록하고 반환한다\n mem[i][c] = Math.max(no, yes);\n return mem[i][c];\n}\n</code></pre> knapsack.ts<pre><code>/* 0-1 배낭: 메모이제이션을 이용한 탐색 */\nfunction knapsackDFSMem(\n wgt: Array&lt;number&gt;,\n val: Array&lt;number&gt;,\n mem: Array&lt;Array&lt;number&gt;&gt;,\n i: number,\n c: number\n): number {\n // 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n if (i === 0 || c === 0) {\n return 0;\n }\n // 기록이 이미 있으면 바로 반환한다\n if (mem[i][c] !== -1) {\n return mem[i][c];\n }\n // 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n if (wgt[i - 1] &gt; c) {\n return knapsackDFSMem(wgt, val, mem, i - 1, c);\n }\n // 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n const no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n const yes =\n knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n // 두 경우 중 더 큰 가치를 기록하고 반환한다\n mem[i][c] = Math.max(no, yes);\n return mem[i][c];\n}\n</code></pre> knapsack.dart<pre><code>/* 0-1 배낭: 메모이제이션을 이용한 탐색 */\nint knapsackDFSMem(\n List&lt;int&gt; wgt,\n List&lt;int&gt; val,\n List&lt;List&lt;int&gt;&gt; mem,\n int i,\n int c,\n) {\n // 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n if (i == 0 || c == 0) {\n return 0;\n }\n // 기록이 이미 있으면 바로 반환한다\n if (mem[i][c] != -1) {\n return mem[i][c];\n }\n // 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n if (wgt[i - 1] &gt; c) {\n return knapsackDFSMem(wgt, val, mem, i - 1, c);\n }\n // 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n int no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n // 두 경우 중 더 큰 가치를 기록하고 반환한다\n mem[i][c] = max(no, yes);\n return mem[i][c];\n}\n</code></pre> knapsack.rs<pre><code>/* 0-1 배낭: 메모이제이션을 이용한 탐색 */\nfn knapsack_dfs_mem(wgt: &amp;[i32], val: &amp;[i32], mem: &amp;mut Vec&lt;Vec&lt;i32&gt;&gt;, i: usize, c: usize) -&gt; i32 {\n // 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n if i == 0 || c == 0 {\n return 0;\n }\n // 기록이 이미 있으면 바로 반환한다\n if mem[i][c] != -1 {\n return mem[i][c];\n }\n // 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n if wgt[i - 1] &gt; c as i32 {\n return knapsack_dfs_mem(wgt, val, mem, i - 1, c);\n }\n // 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n let no = knapsack_dfs_mem(wgt, val, mem, i - 1, c);\n let yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1] as usize) + val[i - 1];\n // 두 경우 중 더 큰 가치를 기록하고 반환한다\n mem[i][c] = std::cmp::max(no, yes);\n mem[i][c]\n}\n</code></pre> knapsack.c<pre><code>/* 0-1 배낭: 메모이제이션을 이용한 탐색 */\nint knapsackDFSMem(int wgt[], int val[], int memCols, int **mem, int i, int c) {\n // 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n if (i == 0 || c == 0) {\n return 0;\n }\n // 기록이 이미 있으면 바로 반환한다\n if (mem[i][c] != -1) {\n return mem[i][c];\n }\n // 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n if (wgt[i - 1] &gt; c) {\n return knapsackDFSMem(wgt, val, memCols, mem, i - 1, c);\n }\n // 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n int no = knapsackDFSMem(wgt, val, memCols, mem, i - 1, c);\n int yes = knapsackDFSMem(wgt, val, memCols, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n // 두 경우 중 더 큰 가치를 기록하고 반환한다\n mem[i][c] = myMax(no, yes);\n return mem[i][c];\n}\n</code></pre> knapsack.kt<pre><code>/* 0-1 배낭: 메모이제이션을 이용한 탐색 */\nfun knapsackDFSMem(\n wgt: IntArray,\n _val: IntArray,\n mem: Array&lt;IntArray&gt;,\n i: Int,\n c: Int\n): Int {\n // 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n if (i == 0 || c == 0) {\n return 0\n }\n // 기록이 이미 있으면 바로 반환한다\n if (mem[i][c] != -1) {\n return mem[i][c]\n }\n // 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n if (wgt[i - 1] &gt; c) {\n return knapsackDFSMem(wgt, _val, mem, i - 1, c)\n }\n // 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n val no = knapsackDFSMem(wgt, _val, mem, i - 1, c)\n val yes = knapsackDFSMem(wgt, _val, mem, i - 1, c - wgt[i - 1]) + _val[i - 1]\n // 두 경우 중 더 큰 가치를 기록하고 반환한다\n mem[i][c] = max(no, yes)\n return mem[i][c]\n}\n</code></pre> knapsack.rb<pre><code>### 0-1 배낭: 메모이제이션을 이용한 탐색 ###\ndef knapsack_dfs_mem(wgt, val, mem, i, c)\n # 모든 물건의 선택을 마쳤거나 배낭에 남은 용량이 없으면 가치 0을 반환한다\n return 0 if i == 0 || c == 0\n # 기록이 이미 있으면 바로 반환한다\n return mem[i][c] if mem[i][c] != -1\n # 배낭 용량을 초과하면 배낭에 넣지 않는 선택만 가능하다\n return knapsack_dfs_mem(wgt, val, mem, i - 1, c) if wgt[i - 1] &gt; c\n # 물건 i를 넣지 않는 경우와 넣는 경우의 최대 가치를 계산한다\n no = knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]\n # 두 경우 중 더 큰 가치를 기록하고 반환한다\n mem[i][c] = [no, yes].max\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>아래 그림(14-19)은 메모이제이션을 이용한 탐색에서 잘라 낸 가지들을 보여 준다.</p> <p></p> <p> 그림 14-19   0-1 배낭 문제의 메모이제이션 탐색 재귀 트리 </p>","path":["제 14 장   동적 계획법","14.4   0-1 배낭 문제"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#3-3","level":3,"title":"3.   방법 3: 동적 계획법","text":"<p>동적 계획법은 본질적으로 상태 전이를 통해 \\(dp\\) 테이블을 채우는 과정이다. 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py<pre><code>def knapsack_dp(wgt: list[int], val: list[int], cap: int) -&gt; int:\n \"\"\"0-1 배낭: 동적 계획법\"\"\"\n n = len(wgt)\n # dp 테이블을 초기화한다\n dp = [[0] * (cap + 1) for _ in range(n + 1)]\n # 상태 전이\n for i in range(1, n + 1):\n for c in range(1, cap + 1):\n if wgt[i - 1] &gt; c:\n # 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i][c] = dp[i - 1][c]\n else:\n # 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1])\n return dp[n][cap]\n</code></pre> knapsack.cpp<pre><code>/* 0-1 배낭: 동적 계획법 */\nint knapsackDP(vector&lt;int&gt; &amp;wgt, vector&lt;int&gt; &amp;val, int cap) {\n int n = wgt.size();\n // dp 테이블을 초기화한다\n vector&lt;vector&lt;int&gt;&gt; dp(n + 1, vector&lt;int&gt;(cap + 1, 0));\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int c = 1; c &lt;= cap; c++) {\n if (wgt[i - 1] &gt; c) {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i][c] = dp[i - 1][c];\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[n][cap];\n}\n</code></pre> knapsack.java<pre><code>/* 0-1 배낭: 동적 계획법 */\nint knapsackDP(int[] wgt, int[] val, int cap) {\n int n = wgt.length;\n // dp 테이블을 초기화한다\n int[][] dp = new int[n + 1][cap + 1];\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int c = 1; c &lt;= cap; c++) {\n if (wgt[i - 1] &gt; c) {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i][c] = dp[i - 1][c];\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i][c] = Math.max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[n][cap];\n}\n</code></pre> knapsack.cs<pre><code>/* 0-1 배낭: 동적 계획법 */\nint KnapsackDP(int[] weight, int[] val, int cap) {\n int n = weight.Length;\n // dp 테이블을 초기화한다\n int[,] dp = new int[n + 1, cap + 1];\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int c = 1; c &lt;= cap; c++) {\n if (weight[i - 1] &gt; c) {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i, c] = dp[i - 1, c];\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i, c] = Math.Max(dp[i - 1, c - weight[i - 1]] + val[i - 1], dp[i - 1, c]);\n }\n }\n }\n return dp[n, cap];\n}\n</code></pre> knapsack.go<pre><code>/* 0-1 배낭: 동적 계획법 */\nfunc knapsackDP(wgt, val []int, cap int) int {\n n := len(wgt)\n // dp 테이블을 초기화한다\n dp := make([][]int, n+1)\n for i := 0; i &lt;= n; i++ {\n dp[i] = make([]int, cap+1)\n }\n // 상태 전이\n for i := 1; i &lt;= n; i++ {\n for c := 1; c &lt;= cap; c++ {\n if wgt[i-1] &gt; c {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i][c] = dp[i-1][c]\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i][c] = int(math.Max(float64(dp[i-1][c]), float64(dp[i-1][c-wgt[i-1]]+val[i-1])))\n }\n }\n }\n return dp[n][cap]\n}\n</code></pre> knapsack.swift<pre><code>/* 0-1 배낭: 동적 계획법 */\nfunc knapsackDP(wgt: [Int], val: [Int], cap: Int) -&gt; Int {\n let n = wgt.count\n // dp 테이블을 초기화한다\n var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1)\n // 상태 전이\n for i in 1 ... n {\n for c in 1 ... cap {\n if wgt[i - 1] &gt; c {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i][c] = dp[i - 1][c]\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1])\n }\n }\n }\n return dp[n][cap]\n}\n</code></pre> knapsack.js<pre><code>/* 0-1 배낭: 동적 계획법 */\nfunction knapsackDP(wgt, val, cap) {\n const n = wgt.length;\n // dp 테이블을 초기화한다\n const dp = Array(n + 1)\n .fill(0)\n .map(() =&gt; Array(cap + 1).fill(0));\n // 상태 전이\n for (let i = 1; i &lt;= n; i++) {\n for (let c = 1; c &lt;= cap; c++) {\n if (wgt[i - 1] &gt; c) {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i][c] = dp[i - 1][c];\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i][c] = Math.max(\n dp[i - 1][c],\n dp[i - 1][c - wgt[i - 1]] + val[i - 1]\n );\n }\n }\n }\n return dp[n][cap];\n}\n</code></pre> knapsack.ts<pre><code>/* 0-1 배낭: 동적 계획법 */\nfunction knapsackDP(\n wgt: Array&lt;number&gt;,\n val: Array&lt;number&gt;,\n cap: number\n): number {\n const n = wgt.length;\n // dp 테이블을 초기화한다\n const dp = Array.from({ length: n + 1 }, () =&gt;\n Array.from({ length: cap + 1 }, () =&gt; 0)\n );\n // 상태 전이\n for (let i = 1; i &lt;= n; i++) {\n for (let c = 1; c &lt;= cap; c++) {\n if (wgt[i - 1] &gt; c) {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i][c] = dp[i - 1][c];\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i][c] = Math.max(\n dp[i - 1][c],\n dp[i - 1][c - wgt[i - 1]] + val[i - 1]\n );\n }\n }\n }\n return dp[n][cap];\n}\n</code></pre> knapsack.dart<pre><code>/* 0-1 배낭: 동적 계획법 */\nint knapsackDP(List&lt;int&gt; wgt, List&lt;int&gt; val, int cap) {\n int n = wgt.length;\n // dp 테이블을 초기화한다\n List&lt;List&lt;int&gt;&gt; dp = List.generate(n + 1, (index) =&gt; List.filled(cap + 1, 0));\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int c = 1; c &lt;= cap; c++) {\n if (wgt[i - 1] &gt; c) {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i][c] = dp[i - 1][c];\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[n][cap];\n}\n</code></pre> knapsack.rs<pre><code>/* 0-1 배낭: 동적 계획법 */\nfn knapsack_dp(wgt: &amp;[i32], val: &amp;[i32], cap: usize) -&gt; i32 {\n let n = wgt.len();\n // dp 테이블을 초기화한다\n let mut dp = vec![vec![0; cap + 1]; n + 1];\n // 상태 전이\n for i in 1..=n {\n for c in 1..=cap {\n if wgt[i - 1] &gt; c as i32 {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i][c] = dp[i - 1][c];\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i][c] = std::cmp::max(\n dp[i - 1][c],\n dp[i - 1][c - wgt[i - 1] as usize] + val[i - 1],\n );\n }\n }\n }\n dp[n][cap]\n}\n</code></pre> knapsack.c<pre><code>/* 0-1 배낭: 동적 계획법 */\nint knapsackDP(int wgt[], int val[], int cap, int wgtSize) {\n int n = wgtSize;\n // dp 테이블을 초기화한다\n int **dp = malloc((n + 1) * sizeof(int *));\n for (int i = 0; i &lt;= n; i++) {\n dp[i] = calloc(cap + 1, sizeof(int));\n }\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int c = 1; c &lt;= cap; c++) {\n if (wgt[i - 1] &gt; c) {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i][c] = dp[i - 1][c];\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i][c] = myMax(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n int res = dp[n][cap];\n // 메모리 해제\n for (int i = 0; i &lt;= n; i++) {\n free(dp[i]);\n }\n return res;\n}\n</code></pre> knapsack.kt<pre><code>/* 0-1 배낭: 동적 계획법 */\nfun knapsackDP(wgt: IntArray, _val: IntArray, cap: Int): Int {\n val n = wgt.size\n // dp 테이블을 초기화한다\n val dp = Array(n + 1) { IntArray(cap + 1) }\n // 상태 전이\n for (i in 1..n) {\n for (c in 1..cap) {\n if (wgt[i - 1] &gt; c) {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i][c] = dp[i - 1][c]\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + _val[i - 1])\n }\n }\n }\n return dp[n][cap]\n}\n</code></pre> knapsack.rb<pre><code>### 0-1 배낭: 동적 계획법 ###\ndef knapsack_dp(wgt, val, cap)\n n = wgt.length\n # dp 테이블을 초기화한다\n dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n # 상태 전이\n for i in 1...(n + 1)\n for c in 1...(cap + 1)\n if wgt[i - 1] &gt; c\n # 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i][c] = dp[i - 1][c]\n else\n # 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i][c] = [dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]].max\n end\n end\n end\n dp[n][cap]\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>아래 그림(14-20)과 같이 시간 복잡도와 공간 복잡도는 모두 배열 <code>dp</code>의 크기인 \\(O(n \\times cap)\\)이다.</p> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt;&lt;6&gt;&lt;7&gt;&lt;8&gt;&lt;9&gt;&lt;10&gt;&lt;11&gt;&lt;12&gt;&lt;13&gt;&lt;14&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 14-20   0-1 배낭 문제의 동적 계획법 과정 </p>","path":["제 14 장   동적 계획법","14.4   0-1 배낭 문제"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#4","level":3,"title":"4.   공간 최적화","text":"<p>각 상태는 바로 윗행의 상태에만 의존하므로 두 배열을 번갈아 갱신하면 공간 복잡도를 \\(O(n \\times cap)\\)에서 \\(O(cap)\\)으로 줄일 수 있다.</p> <p>배열 하나만으로도 가능할까? 각 상태는 바로 위 또는 왼쪽 위 칸에서 전이된다. 배열 하나를 쓴다면 \\(i\\) 번째 행의 순회를 시작할 때 그 배열에는 아직 \\(i-1\\) 번째 행의 상태가 저장되어 있다.</p> <ul> <li>정방향으로 순회하면 \\(dp[i, j]\\)에 도달했을 때 왼쪽 위 \\(dp[i-1, 1]\\) ~ \\(dp[i-1, j-1]\\)의 값이 이미 덮어써졌을 수 있어 올바른 상태 전이를 할 수 없다.</li> <li>역방향으로 순회하면 값이 덮어써지는 문제가 없어 상태 전이를 올바르게 수행할 수 있다.</li> </ul> <p>아래 그림(14-21)은 배열 하나로 \\(i = 1\\) 행에서 \\(i = 2\\) 행으로 바꾸는 과정이다. 정방향과 역방향 순회의 차이를 생각해 보자.</p> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt;&lt;6&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 14-21   공간 최적화 후 0-1 배낭의 동적 계획법 과정 </p> <p>구현에서는 배열 <code>dp</code>의 첫 번째 차원 \\(i\\)를 없애고 안쪽 반복문을 역방향 순회로 바꾸기만 하면 된다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py<pre><code>def knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -&gt; int:\n \"\"\"0-1 배낭: 공간을 최적화한 동적 계획법\"\"\"\n n = len(wgt)\n # dp 테이블을 초기화한다\n dp = [0] * (cap + 1)\n # 상태 전이\n for i in range(1, n + 1):\n # 역방향 순회\n for c in range(cap, 0, -1):\n if wgt[i - 1] &gt; c:\n # 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[c] = dp[c]\n else:\n # 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n return dp[cap]\n</code></pre> knapsack.cpp<pre><code>/* 0-1 배낭: 공간을 최적화한 동적 계획법 */\nint knapsackDPComp(vector&lt;int&gt; &amp;wgt, vector&lt;int&gt; &amp;val, int cap) {\n int n = wgt.size();\n // dp 테이블을 초기화한다\n vector&lt;int&gt; dp(cap + 1, 0);\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n // 역방향 순회\n for (int c = cap; c &gt;= 1; c--) {\n if (wgt[i - 1] &lt;= c) {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[cap];\n}\n</code></pre> knapsack.java<pre><code>/* 0-1 배낭: 공간을 최적화한 동적 계획법 */\nint knapsackDPComp(int[] wgt, int[] val, int cap) {\n int n = wgt.length;\n // dp 테이블을 초기화한다\n int[] dp = new int[cap + 1];\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n // 역방향 순회\n for (int c = cap; c &gt;= 1; c--) {\n if (wgt[i - 1] &lt;= c) {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[cap];\n}\n</code></pre> knapsack.cs<pre><code>/* 0-1 배낭: 공간을 최적화한 동적 계획법 */\nint KnapsackDPComp(int[] weight, int[] val, int cap) {\n int n = weight.Length;\n // dp 테이블을 초기화한다\n int[] dp = new int[cap + 1];\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n // 역방향 순회\n for (int c = cap; c &gt; 0; c--) {\n if (weight[i - 1] &gt; c) {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[c] = dp[c];\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = Math.Max(dp[c], dp[c - weight[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[cap];\n}\n</code></pre> knapsack.go<pre><code>/* 0-1 배낭: 공간을 최적화한 동적 계획법 */\nfunc knapsackDPComp(wgt, val []int, cap int) int {\n n := len(wgt)\n // dp 테이블을 초기화한다\n dp := make([]int, cap+1)\n // 상태 전이\n for i := 1; i &lt;= n; i++ {\n // 역방향 순회\n for c := cap; c &gt;= 1; c-- {\n if wgt[i-1] &lt;= c {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = int(math.Max(float64(dp[c]), float64(dp[c-wgt[i-1]]+val[i-1])))\n }\n }\n }\n return dp[cap]\n}\n</code></pre> knapsack.swift<pre><code>/* 0-1 배낭: 공간을 최적화한 동적 계획법 */\nfunc knapsackDPComp(wgt: [Int], val: [Int], cap: Int) -&gt; Int {\n let n = wgt.count\n // dp 테이블을 초기화한다\n var dp = Array(repeating: 0, count: cap + 1)\n // 상태 전이\n for i in 1 ... n {\n // 역방향 순회\n for c in (1 ... cap).reversed() {\n if wgt[i - 1] &lt;= c {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n }\n }\n }\n return dp[cap]\n}\n</code></pre> knapsack.js<pre><code>/* 0-1 배낭: 공간을 최적화한 동적 계획법 */\nfunction knapsackDPComp(wgt, val, cap) {\n const n = wgt.length;\n // dp 테이블을 초기화한다\n const dp = Array(cap + 1).fill(0);\n // 상태 전이\n for (let i = 1; i &lt;= n; i++) {\n // 역방향 순회\n for (let c = cap; c &gt;= 1; c--) {\n if (wgt[i - 1] &lt;= c) {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[cap];\n}\n</code></pre> knapsack.ts<pre><code>/* 0-1 배낭: 공간을 최적화한 동적 계획법 */\nfunction knapsackDPComp(\n wgt: Array&lt;number&gt;,\n val: Array&lt;number&gt;,\n cap: number\n): number {\n const n = wgt.length;\n // dp 테이블을 초기화한다\n const dp = Array(cap + 1).fill(0);\n // 상태 전이\n for (let i = 1; i &lt;= n; i++) {\n // 역방향 순회\n for (let c = cap; c &gt;= 1; c--) {\n if (wgt[i - 1] &lt;= c) {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[cap];\n}\n</code></pre> knapsack.dart<pre><code>/* 0-1 배낭: 공간을 최적화한 동적 계획법 */\nint knapsackDPComp(List&lt;int&gt; wgt, List&lt;int&gt; val, int cap) {\n int n = wgt.length;\n // dp 테이블을 초기화한다\n List&lt;int&gt; dp = List.filled(cap + 1, 0);\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n // 역방향 순회\n for (int c = cap; c &gt;= 1; c--) {\n if (wgt[i - 1] &lt;= c) {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[cap];\n}\n</code></pre> knapsack.rs<pre><code>/* 0-1 배낭: 공간을 최적화한 동적 계획법 */\nfn knapsack_dp_comp(wgt: &amp;[i32], val: &amp;[i32], cap: usize) -&gt; i32 {\n let n = wgt.len();\n // dp 테이블을 초기화한다\n let mut dp = vec![0; cap + 1];\n // 상태 전이\n for i in 1..=n {\n // 역방향 순회\n for c in (1..=cap).rev() {\n if wgt[i - 1] &lt;= c as i32 {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = std::cmp::max(dp[c], dp[c - wgt[i - 1] as usize] + val[i - 1]);\n }\n }\n }\n dp[cap]\n}\n</code></pre> knapsack.c<pre><code>/* 0-1 배낭: 공간을 최적화한 동적 계획법 */\nint knapsackDPComp(int wgt[], int val[], int cap, int wgtSize) {\n int n = wgtSize;\n // dp 테이블을 초기화한다\n int *dp = calloc(cap + 1, sizeof(int));\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n // 역방향 순회\n for (int c = cap; c &gt;= 1; c--) {\n if (wgt[i - 1] &lt;= c) {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n int res = dp[cap];\n // 메모리 해제\n free(dp);\n return res;\n}\n</code></pre> knapsack.kt<pre><code>/* 0-1 배낭: 공간을 최적화한 동적 계획법 */\nfun knapsackDPComp(wgt: IntArray, _val: IntArray, cap: Int): Int {\n val n = wgt.size\n // dp 테이블을 초기화한다\n val dp = IntArray(cap + 1)\n // 상태 전이\n for (i in 1..n) {\n // 역방향 순회\n for (c in cap downTo 1) {\n if (wgt[i - 1] &lt;= c) {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = max(dp[c], dp[c - wgt[i - 1]] + _val[i - 1])\n }\n }\n }\n return dp[cap]\n}\n</code></pre> knapsack.rb<pre><code>### 0-1 배낭: 공간을 최적화한 동적 계획법 ###\ndef knapsack_dp_comp(wgt, val, cap)\n n = wgt.length\n # dp 테이블을 초기화한다\n dp = Array.new(cap + 1, 0)\n # 상태 전이\n for i in 1...(n + 1)\n # 역방향 순회\n for c in cap.downto(1)\n if wgt[i - 1] &gt; c\n # 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[c] = dp[c]\n else\n # 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = [dp[c], dp[c - wgt[i - 1]] + val[i - 1]].max\n end\n end\n end\n dp[cap]\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 14 장   동적 계획법","14.4   0-1 배낭 문제"],"tags":[]},{"location":"chapter_dynamic_programming/summary/","level":1,"title":"14.7   요약","text":"","path":["제 14 장   동적 계획법","14.7   요약"],"tags":[]},{"location":"chapter_dynamic_programming/summary/#1","level":3,"title":"1.   핵심 정리","text":"<ul> <li>동적 계획법은 문제를 나누고 부분 문제의 해를 저장하여 중복 계산을 피함으로써 계산 효율을 높인다.</li> <li>시간을 고려하지 않는다면 모든 동적 계획법 문제는 백트래킹(완전 탐색)으로 풀 수 있다. 하지만 재귀 트리에 중복 부분 문제가 매우 많아 비효율적이다. 메모이제이션 리스트에 계산한 해를 저장하면 중복 부분 문제를 한 번만 계산하도록 보장할 수 있다.</li> <li>메모이제이션을 이용한 탐색은 위에서 아래로 진행하는 재귀 풀이이며, 이에 대응하는 동적 계획법은 '표를 채우듯' 아래에서 위로 진행하는 반복 풀이이다. 현재 상태는 일부 주변 상태에만 의존하므로 \\(dp\\) 테이블의 한 차원을 없애 공간 복잡도를 줄일 수 있다.</li> <li>부분 문제로 나누는 것은 일반적인 알고리즘 아이디어이며, 분할 정복, 동적 계획법, 백트래킹에서 서로 다른 특성을 가진다.</li> <li>동적 계획법 문제의 세 가지 주요 특성은 중복 부분 문제, 최적 부분 구조, 무후효성이다.</li> <li>원래 문제의 최적해를 부분 문제의 최적해로 구성할 수 있다면 최적 부분 구조를 가진다.</li> <li>무후효성이란 한 상태의 이후 전개가 현재 상태에만 의존하고 과거의 모든 상태와는 무관하다는 뜻이다. 많은 조합 최적화 문제는 무후효성을 갖지 않아 동적 계획법으로 빠르게 풀 수 없다.</li> </ul> <p>배낭 문제</p> <ul> <li>배낭 문제는 가장 대표적인 동적 계획법 문제 중 하나로, 0-1 배낭, 무한 배낭, 개수 제한 배낭 등의 변형이 있다.</li> <li>0-1 배낭의 상태는 앞 \\(i\\) 개 물건을 용량 \\(c\\)인 배낭에 담을 때의 최대 가치로 정의한다. 넣지 않기와 넣기라는 두 결정으로 최적 부분 구조와 상태 전이 식을 얻는다. 공간 최적화에서는 바로 위와 왼쪽 위 상태에 의존하므로, 왼쪽 위 값이 덮어써지지 않도록 리스트를 역순으로 순회해야 한다.</li> <li>무한 배낭은 각 물건의 선택 횟수에 제한이 없어 물건을 넣는 상태 전이가 0-1 배낭과 다르다. 바로 위와 바로 왼쪽 상태에 의존하므로 공간 최적화에서는 정방향 순회를 해야 한다.</li> <li>동전 교환은 무한 배낭의 변형이다. '최대' 가치 대신 '최소' 동전 수를 구하므로 상태 전이 식의 \\(\\max()\\)를 \\(\\min()\\)으로 바꾼다. 용량을 '넘지 않는' 것이 아니라 목표 금액을 '정확히' 만들어야 하므로, 만들 수 없는 유효하지 않은 해를 \\(amt + 1\\)로 나타낸다.</li> <li>동전 교환 II는 '최소 동전 수' 대신 '동전 조합 수'를 구하므로 상태 전이 식의 \\(\\min()\\)을 합 연산으로 바꾼다.</li> </ul> <p>편집 거리 문제</p> <ul> <li>편집 거리(Levenshtein 거리)는 두 문자열의 유사도를 측정하며, 한 문자열을 다른 문자열로 바꾸는 최소 편집 횟수로 정의된다. 편집 연산에는 추가, 삭제, 교체가 있다.</li> <li>편집 거리의 상태는 \\(s\\)의 앞 \\(i\\) 개 문자를 \\(t\\)의 앞 \\(j\\) 개 문자로 바꾸는 최소 편집 횟수로 정의한다. \\(s[i-1] \\ne t[j-1]\\)이면 추가, 삭제, 교체의 세 결정이 있고 각각 남는 부분 문제가 있다. 이를 바탕으로 최적 부분 구조와 상태 전이 식을 구한다. \\(s[i-1] = t[j-1]\\)이면 현재 문자를 편집할 필요가 없다.</li> <li>편집 거리의 상태는 바로 위, 바로 왼쪽, 왼쪽 위 상태에 의존하므로 공간 최적화 후에는 정방향이나 역방향 순회만으로 올바른 전이를 할 수 없다. 변수를 하나 두어 왼쪽 위 상태를 임시 저장하면 무한 배낭과 같은 상황이 되어 정방향 순회가 가능하다.</li> </ul>","path":["제 14 장   동적 계획법","14.7   요약"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/","level":1,"title":"14.5   무한 배낭 문제","text":"<p>이 절에서는 또 다른 대표적인 배낭 문제인 무한 배낭을 먼저 풀고, 그 특수한 경우인 동전 교환을 알아본다.</p>","path":["제 14 장   동적 계획법","14.5   무한 배낭 문제"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1451","level":2,"title":"14.5.1   무한 배낭 문제","text":"<p>문제</p> <p>물건 \\(n\\) 개와 용량이 \\(cap\\)인 배낭이 주어진다. \\(i\\) 번째 물건의 무게는 \\(wgt[i-1]\\), 가치는 \\(val[i-1]\\)이다. 각 물건을 반복해서 선택할 수 있을 때, 용량 제한 안에 담을 수 있는 최대 가치를 구하라. 예시는 아래 그림(14-22)과 같다.</p> <p></p> <p> 그림 14-22   무한 배낭 문제 예시 데이터 </p>","path":["제 14 장   동적 계획법","14.5   무한 배낭 문제"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1","level":3,"title":"1.   동적 계획법 접근","text":"<p>무한 배낭 문제는 0-1 배낭 문제와 매우 비슷하며, 물건을 선택하는 횟수에 제한이 없다는 점만 다르다.</p> <ul> <li>0-1 배낭에서는 종류별 물건이 하나뿐이므로 물건 \\(i\\)를 넣은 뒤에는 앞의 \\(i-1\\) 개 물건에서만 선택할 수 있다.</li> <li>무한 배낭에서는 각 종류의 물건이 무한히 많으므로 물건 \\(i\\)를 넣은 뒤에도 앞의 \\(i\\) 개 물건에서 계속 선택할 수 있다.</li> </ul> <p>무한 배낭의 규칙에 따라 상태 \\([i, c]\\)의 변화는 두 경우로 나뉜다.</p> <ul> <li>물건 \\(i\\)를 넣지 않음: 0-1 배낭과 동일하게 \\([i-1, c]\\)로 전이한다.</li> <li>물건 \\(i\\)를 넣음: 0-1 배낭과 달리 \\([i, c-wgt[i-1]]\\)로 전이한다.</li> </ul> <p>따라서 상태 전이 식은 다음과 같이 바뀐다.</p> \\[ dp[i, c] = \\max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1]) \\]","path":["제 14 장   동적 계획법","14.5   무한 배낭 문제"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2","level":3,"title":"2.   코드 구현","text":"<p>두 문제의 코드를 비교하면 상태 전이의 한 곳이 \\(i-1\\)에서 \\(i\\)로 바뀌었을 뿐 나머지는 완전히 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby unbounded_knapsack.py<pre><code>def unbounded_knapsack_dp(wgt: list[int], val: list[int], cap: int) -&gt; int:\n \"\"\"무한 배낭: 동적 계획법\"\"\"\n n = len(wgt)\n # dp 테이블을 초기화한다\n dp = [[0] * (cap + 1) for _ in range(n + 1)]\n # 상태 전이\n for i in range(1, n + 1):\n for c in range(1, cap + 1):\n if wgt[i - 1] &gt; c:\n # 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i][c] = dp[i - 1][c]\n else:\n # 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1])\n return dp[n][cap]\n</code></pre> unbounded_knapsack.cpp<pre><code>/* 무한 배낭: 동적 계획법 */\nint unboundedKnapsackDP(vector&lt;int&gt; &amp;wgt, vector&lt;int&gt; &amp;val, int cap) {\n int n = wgt.size();\n // dp 테이블을 초기화한다\n vector&lt;vector&lt;int&gt;&gt; dp(n + 1, vector&lt;int&gt;(cap + 1, 0));\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int c = 1; c &lt;= cap; c++) {\n if (wgt[i - 1] &gt; c) {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i][c] = dp[i - 1][c];\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[n][cap];\n}\n</code></pre> unbounded_knapsack.java<pre><code>/* 무한 배낭: 동적 계획법 */\nint unboundedKnapsackDP(int[] wgt, int[] val, int cap) {\n int n = wgt.length;\n // dp 테이블을 초기화한다\n int[][] dp = new int[n + 1][cap + 1];\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int c = 1; c &lt;= cap; c++) {\n if (wgt[i - 1] &gt; c) {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i][c] = dp[i - 1][c];\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i][c] = Math.max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[n][cap];\n}\n</code></pre> unbounded_knapsack.cs<pre><code>/* 무한 배낭: 동적 계획법 */\nint UnboundedKnapsackDP(int[] wgt, int[] val, int cap) {\n int n = wgt.Length;\n // dp 테이블을 초기화한다\n int[,] dp = new int[n + 1, cap + 1];\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int c = 1; c &lt;= cap; c++) {\n if (wgt[i - 1] &gt; c) {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i, c] = dp[i - 1, c];\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i, c] = Math.Max(dp[i - 1, c], dp[i, c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[n, cap];\n}\n</code></pre> unbounded_knapsack.go<pre><code>/* 무한 배낭: 동적 계획법 */\nfunc unboundedKnapsackDP(wgt, val []int, cap int) int {\n n := len(wgt)\n // dp 테이블을 초기화한다\n dp := make([][]int, n+1)\n for i := 0; i &lt;= n; i++ {\n dp[i] = make([]int, cap+1)\n }\n // 상태 전이\n for i := 1; i &lt;= n; i++ {\n for c := 1; c &lt;= cap; c++ {\n if wgt[i-1] &gt; c {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i][c] = dp[i-1][c]\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i][c] = int(math.Max(float64(dp[i-1][c]), float64(dp[i][c-wgt[i-1]]+val[i-1])))\n }\n }\n }\n return dp[n][cap]\n}\n</code></pre> unbounded_knapsack.swift<pre><code>/* 무한 배낭: 동적 계획법 */\nfunc unboundedKnapsackDP(wgt: [Int], val: [Int], cap: Int) -&gt; Int {\n let n = wgt.count\n // dp 테이블을 초기화한다\n var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1)\n // 상태 전이\n for i in 1 ... n {\n for c in 1 ... cap {\n if wgt[i - 1] &gt; c {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i][c] = dp[i - 1][c]\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1])\n }\n }\n }\n return dp[n][cap]\n}\n</code></pre> unbounded_knapsack.js<pre><code>/* 무한 배낭: 동적 계획법 */\nfunction unboundedKnapsackDP(wgt, val, cap) {\n const n = wgt.length;\n // dp 테이블을 초기화한다\n const dp = Array.from({ length: n + 1 }, () =&gt;\n Array.from({ length: cap + 1 }, () =&gt; 0)\n );\n // 상태 전이\n for (let i = 1; i &lt;= n; i++) {\n for (let c = 1; c &lt;= cap; c++) {\n if (wgt[i - 1] &gt; c) {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i][c] = dp[i - 1][c];\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i][c] = Math.max(\n dp[i - 1][c],\n dp[i][c - wgt[i - 1]] + val[i - 1]\n );\n }\n }\n }\n return dp[n][cap];\n}\n</code></pre> unbounded_knapsack.ts<pre><code>/* 무한 배낭: 동적 계획법 */\nfunction unboundedKnapsackDP(\n wgt: Array&lt;number&gt;,\n val: Array&lt;number&gt;,\n cap: number\n): number {\n const n = wgt.length;\n // dp 테이블을 초기화한다\n const dp = Array.from({ length: n + 1 }, () =&gt;\n Array.from({ length: cap + 1 }, () =&gt; 0)\n );\n // 상태 전이\n for (let i = 1; i &lt;= n; i++) {\n for (let c = 1; c &lt;= cap; c++) {\n if (wgt[i - 1] &gt; c) {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i][c] = dp[i - 1][c];\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i][c] = Math.max(\n dp[i - 1][c],\n dp[i][c - wgt[i - 1]] + val[i - 1]\n );\n }\n }\n }\n return dp[n][cap];\n}\n</code></pre> unbounded_knapsack.dart<pre><code>/* 무한 배낭: 동적 계획법 */\nint unboundedKnapsackDP(List&lt;int&gt; wgt, List&lt;int&gt; val, int cap) {\n int n = wgt.length;\n // dp 테이블을 초기화한다\n List&lt;List&lt;int&gt;&gt; dp = List.generate(n + 1, (index) =&gt; List.filled(cap + 1, 0));\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int c = 1; c &lt;= cap; c++) {\n if (wgt[i - 1] &gt; c) {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i][c] = dp[i - 1][c];\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[n][cap];\n}\n</code></pre> unbounded_knapsack.rs<pre><code>/* 무한 배낭: 동적 계획법 */\nfn unbounded_knapsack_dp(wgt: &amp;[i32], val: &amp;[i32], cap: usize) -&gt; i32 {\n let n = wgt.len();\n // dp 테이블을 초기화한다\n let mut dp = vec![vec![0; cap + 1]; n + 1];\n // 상태 전이\n for i in 1..=n {\n for c in 1..=cap {\n if wgt[i - 1] &gt; c as i32 {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i][c] = dp[i - 1][c];\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i][c] = std::cmp::max(dp[i - 1][c], dp[i][c - wgt[i - 1] as usize] + val[i - 1]);\n }\n }\n }\n return dp[n][cap];\n}\n</code></pre> unbounded_knapsack.c<pre><code>/* 무한 배낭: 동적 계획법 */\nint unboundedKnapsackDP(int wgt[], int val[], int cap, int wgtSize) {\n int n = wgtSize;\n // dp 테이블을 초기화한다\n int **dp = malloc((n + 1) * sizeof(int *));\n for (int i = 0; i &lt;= n; i++) {\n dp[i] = calloc(cap + 1, sizeof(int));\n }\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int c = 1; c &lt;= cap; c++) {\n if (wgt[i - 1] &gt; c) {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i][c] = dp[i - 1][c];\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i][c] = myMax(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n int res = dp[n][cap];\n // 메모리 해제\n for (int i = 0; i &lt;= n; i++) {\n free(dp[i]);\n }\n return res;\n}\n</code></pre> unbounded_knapsack.kt<pre><code>/* 무한 배낭: 동적 계획법 */\nfun unboundedKnapsackDP(wgt: IntArray, _val: IntArray, cap: Int): Int {\n val n = wgt.size\n // dp 테이블을 초기화한다\n val dp = Array(n + 1) { IntArray(cap + 1) }\n // 상태 전이\n for (i in 1..n) {\n for (c in 1..cap) {\n if (wgt[i - 1] &gt; c) {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i][c] = dp[i - 1][c]\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + _val[i - 1])\n }\n }\n }\n return dp[n][cap]\n}\n</code></pre> unbounded_knapsack.rb<pre><code>### 무한 배낭: 동적 계획법 ###\ndef unbounded_knapsack_dp(wgt, val, cap)\n n = wgt.length\n # dp 테이블을 초기화한다\n dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n # 상태 전이\n for i in 1...(n + 1)\n for c in 1...(cap + 1)\n if wgt[i - 1] &gt; c\n # 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i][c] = dp[i - 1][c]\n else\n # 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i][c] = [dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]].max\n end\n end\n end\n dp[n][cap]\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 14 장   동적 계획법","14.5   무한 배낭 문제"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3","level":3,"title":"3.   공간 최적화","text":"<p>현재 상태는 왼쪽과 위쪽 상태에서 전이되므로 공간 최적화 후에는 \\(dp\\) 테이블의 각 행을 정방향으로 순회해야 한다.</p> <p>이는 0-1 배낭의 순회 순서와 정반대다. 아래 그림(14-23)으로 차이를 이해해 보자.</p> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt;&lt;6&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 14-23   공간 최적화 후 무한 배낭의 동적 계획법 과정 </p> <p>구현은 간단하다. 배열 <code>dp</code>의 첫 번째 차원을 없애면 된다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby unbounded_knapsack.py<pre><code>def unbounded_knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -&gt; int:\n \"\"\"무한 배낭: 공간을 최적화한 동적 계획법\"\"\"\n n = len(wgt)\n # dp 테이블을 초기화한다\n dp = [0] * (cap + 1)\n # 상태 전이\n for i in range(1, n + 1):\n # 정방향 순회\n for c in range(1, cap + 1):\n if wgt[i - 1] &gt; c:\n # 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[c] = dp[c]\n else:\n # 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n return dp[cap]\n</code></pre> unbounded_knapsack.cpp<pre><code>/* 무한 배낭: 공간을 최적화한 동적 계획법 */\nint unboundedKnapsackDPComp(vector&lt;int&gt; &amp;wgt, vector&lt;int&gt; &amp;val, int cap) {\n int n = wgt.size();\n // dp 테이블을 초기화한다\n vector&lt;int&gt; dp(cap + 1, 0);\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int c = 1; c &lt;= cap; c++) {\n if (wgt[i - 1] &gt; c) {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[c] = dp[c];\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[cap];\n}\n</code></pre> unbounded_knapsack.java<pre><code>/* 무한 배낭: 공간을 최적화한 동적 계획법 */\nint unboundedKnapsackDPComp(int[] wgt, int[] val, int cap) {\n int n = wgt.length;\n // dp 테이블을 초기화한다\n int[] dp = new int[cap + 1];\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int c = 1; c &lt;= cap; c++) {\n if (wgt[i - 1] &gt; c) {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[c] = dp[c];\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[cap];\n}\n</code></pre> unbounded_knapsack.cs<pre><code>/* 무한 배낭: 공간을 최적화한 동적 계획법 */\nint UnboundedKnapsackDPComp(int[] wgt, int[] val, int cap) {\n int n = wgt.Length;\n // dp 테이블을 초기화한다\n int[] dp = new int[cap + 1];\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int c = 1; c &lt;= cap; c++) {\n if (wgt[i - 1] &gt; c) {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[c] = dp[c];\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = Math.Max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[cap];\n}\n</code></pre> unbounded_knapsack.go<pre><code>/* 무한 배낭: 공간을 최적화한 동적 계획법 */\nfunc unboundedKnapsackDPComp(wgt, val []int, cap int) int {\n n := len(wgt)\n // dp 테이블을 초기화한다\n dp := make([]int, cap+1)\n // 상태 전이\n for i := 1; i &lt;= n; i++ {\n for c := 1; c &lt;= cap; c++ {\n if wgt[i-1] &gt; c {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[c] = dp[c]\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = int(math.Max(float64(dp[c]), float64(dp[c-wgt[i-1]]+val[i-1])))\n }\n }\n }\n return dp[cap]\n}\n</code></pre> unbounded_knapsack.swift<pre><code>/* 무한 배낭: 공간을 최적화한 동적 계획법 */\nfunc unboundedKnapsackDPComp(wgt: [Int], val: [Int], cap: Int) -&gt; Int {\n let n = wgt.count\n // dp 테이블을 초기화한다\n var dp = Array(repeating: 0, count: cap + 1)\n // 상태 전이\n for i in 1 ... n {\n for c in 1 ... cap {\n if wgt[i - 1] &gt; c {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[c] = dp[c]\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n }\n }\n }\n return dp[cap]\n}\n</code></pre> unbounded_knapsack.js<pre><code>/* 무한 배낭: 공간을 최적화한 동적 계획법 */\nfunction unboundedKnapsackDPComp(wgt, val, cap) {\n const n = wgt.length;\n // dp 테이블을 초기화한다\n const dp = Array.from({ length: cap + 1 }, () =&gt; 0);\n // 상태 전이\n for (let i = 1; i &lt;= n; i++) {\n for (let c = 1; c &lt;= cap; c++) {\n if (wgt[i - 1] &gt; c) {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[c] = dp[c];\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[cap];\n}\n</code></pre> unbounded_knapsack.ts<pre><code>/* 무한 배낭: 공간을 최적화한 동적 계획법 */\nfunction unboundedKnapsackDPComp(\n wgt: Array&lt;number&gt;,\n val: Array&lt;number&gt;,\n cap: number\n): number {\n const n = wgt.length;\n // dp 테이블을 초기화한다\n const dp = Array.from({ length: cap + 1 }, () =&gt; 0);\n // 상태 전이\n for (let i = 1; i &lt;= n; i++) {\n for (let c = 1; c &lt;= cap; c++) {\n if (wgt[i - 1] &gt; c) {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[c] = dp[c];\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[cap];\n}\n</code></pre> unbounded_knapsack.dart<pre><code>/* 무한 배낭: 공간을 최적화한 동적 계획법 */\nint unboundedKnapsackDPComp(List&lt;int&gt; wgt, List&lt;int&gt; val, int cap) {\n int n = wgt.length;\n // dp 테이블을 초기화한다\n List&lt;int&gt; dp = List.filled(cap + 1, 0);\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int c = 1; c &lt;= cap; c++) {\n if (wgt[i - 1] &gt; c) {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[c] = dp[c];\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n return dp[cap];\n}\n</code></pre> unbounded_knapsack.rs<pre><code>/* 무한 배낭: 공간을 최적화한 동적 계획법 */\nfn unbounded_knapsack_dp_comp(wgt: &amp;[i32], val: &amp;[i32], cap: usize) -&gt; i32 {\n let n = wgt.len();\n // dp 테이블을 초기화한다\n let mut dp = vec![0; cap + 1];\n // 상태 전이\n for i in 1..=n {\n for c in 1..=cap {\n if wgt[i - 1] &gt; c as i32 {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[c] = dp[c];\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = std::cmp::max(dp[c], dp[c - wgt[i - 1] as usize] + val[i - 1]);\n }\n }\n }\n dp[cap]\n}\n</code></pre> unbounded_knapsack.c<pre><code>/* 무한 배낭: 공간을 최적화한 동적 계획법 */\nint unboundedKnapsackDPComp(int wgt[], int val[], int cap, int wgtSize) {\n int n = wgtSize;\n // dp 테이블을 초기화한다\n int *dp = calloc(cap + 1, sizeof(int));\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int c = 1; c &lt;= cap; c++) {\n if (wgt[i - 1] &gt; c) {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[c] = dp[c];\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n }\n }\n }\n int res = dp[cap];\n // 메모리 해제\n free(dp);\n return res;\n}\n</code></pre> unbounded_knapsack.kt<pre><code>/* 무한 배낭: 공간을 최적화한 동적 계획법 */\nfun unboundedKnapsackDPComp(\n wgt: IntArray,\n _val: IntArray,\n cap: Int\n): Int {\n val n = wgt.size\n // dp 테이블을 초기화한다\n val dp = IntArray(cap + 1)\n // 상태 전이\n for (i in 1..n) {\n for (c in 1..cap) {\n if (wgt[i - 1] &gt; c) {\n // 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[c] = dp[c]\n } else {\n // 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = max(dp[c], dp[c - wgt[i - 1]] + _val[i - 1])\n }\n }\n }\n return dp[cap]\n}\n</code></pre> unbounded_knapsack.rb<pre><code>### 무한 배낭: 동적 계획법 ###\ndef unbounded_knapsack_dp(wgt, val, cap)\n n = wgt.length\n # dp 테이블을 초기화한다\n dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n # 상태 전이\n for i in 1...(n + 1)\n for c in 1...(cap + 1)\n if wgt[i - 1] &gt; c\n # 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[i][c] = dp[i - 1][c]\n else\n # 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[i][c] = [dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]].max\n end\n end\n end\n dp[n][cap]\nend\n\n### 무한 배낭: 공간을 최적화한 동적 계획법 ##3\ndef unbounded_knapsack_dp_comp(wgt, val, cap)\n n = wgt.length\n # dp 테이블을 초기화한다\n dp = Array.new(cap + 1, 0)\n # 상태 전이\n for i in 1...(n + 1)\n # 정방향 순회\n for c in 1...(cap + 1)\n if wgt[i -1] &gt; c\n # 배낭 용량을 초과하면 물건 i를 선택하지 않는다\n dp[c] = dp[c]\n else\n # 물건 i를 선택하지 않는 경우와 선택하는 경우 중 더 큰 값\n dp[c] = [dp[c], dp[c - wgt[i - 1]] + val[i - 1]].max\n end\n end\n end\n dp[cap]\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 14 장   동적 계획법","14.5   무한 배낭 문제"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1452","level":2,"title":"14.5.2   동전 교환 문제","text":"<p>배낭 문제는 동적 계획법의 큰 문제군을 대표하며, 동전 교환 같은 여러 변형이 있다.</p> <p>문제</p> <p>동전 \\(n\\) 종류가 주어지며 \\(i\\) 번째 동전의 액면가는 \\(coins[i - 1]\\)이고 목표 금액은 \\(amt\\)다. 각 종류의 동전을 반복해서 선택할 수 있을 때, 목표 금액을 만드는 최소 동전 수를 구하라. 목표 금액을 만들 수 없으면 \\(-1\\)을 반환하라. 예시는 아래 그림(14-24)과 같다.</p> <p></p> <p> 그림 14-24   동전 교환 문제 예시 데이터 </p>","path":["제 14 장   동적 계획법","14.5   무한 배낭 문제"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1_1","level":3,"title":"1.   동적 계획법 접근","text":"<p>동전 교환은 무한 배낭 문제의 특수한 경우로 볼 수 있으며, 다음과 같은 공통점과 차이점이 있다.</p> <ul> <li>두 문제는 서로 변환할 수 있다. '물건'은 '동전', '물건 무게'는 '동전 액면가', '배낭 용량'은 '목표 금액'에 대응한다.</li> <li>최적화 목표가 반대다. 무한 배낭은 물건 가치를 최대화하고, 동전 교환은 동전 수를 최소화한다.</li> <li>무한 배낭은 용량을 '넘지 않는' 해를 구하지만, 동전 교환은 목표 금액을 '정확히' 만드는 해를 구한다.</li> </ul> <p>1단계: 매 차례의 결정을 생각하고 상태를 정의하여 \\(dp\\) 테이블을 얻는다</p> <p>상태 \\([i, a]\\)의 부분 문제는 앞의 \\(i\\) 종류 동전으로 금액 \\(a\\)를 만드는 최소 동전 수이며, 이를 \\(dp[i, a]\\)라고 한다.</p> <p>2차원 \\(dp\\) 테이블의 크기는 \\((n+1) \\times (amt+1)\\)이다.</p> <p>2단계: 최적 부분 구조를 찾아 상태 전이 식을 도출한다</p> <p>이 문제의 상태 전이 식은 무한 배낭과 다음 두 가지가 다르다.</p> <ul> <li>최솟값을 구하므로 연산자 \\(\\max()\\)를 \\(\\min()\\)으로 바꾼다.</li> <li>최적화 대상은 상품 가치가 아니라 동전 수이므로, 동전을 선택하면 \\(+1\\)만 수행한다.</li> </ul> \\[ dp[i, a] = \\min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) \\] <p>3단계: 경계 조건과 상태 전이 순서를 정한다</p> <p>목표 금액이 \\(0\\)이면 최소 동전 수는 \\(0\\)이다. 즉, 첫 열의 모든 \\(dp[i, 0]\\)은 \\(0\\)이다.</p> <p>동전이 없으면 \\(&gt; 0\\)인 어떤 목표 금액도 만들 수 없으므로 유효하지 않은 해다. 상태 전이 식의 \\(\\min()\\)이 이를 알아보고 제외하도록 \\(+ \\infty\\)로 나타낸다. 즉, 첫 행의 모든 \\(dp[0, a]\\)를 \\(+ \\infty\\)로 둔다.</p>","path":["제 14 장   동적 계획법","14.5   무한 배낭 문제"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2_1","level":3,"title":"2.   코드 구현","text":"<p>대부분의 프로그래밍 언어는 \\(+ \\infty\\) 변수를 제공하지 않아 정수 타입 <code>int</code>의 최댓값으로 대신해야 한다. 하지만 그러면 상태 전이 식의 \\(+ 1\\) 연산에서 정수 오버플로가 발생할 수 있다.</p> <p>이를 피하기 위해 유효하지 않은 해를 \\(amt + 1\\)로 나타낸다. \\(amt\\)를 만드는 동전 수는 많아도 \\(amt\\) 개이기 때문이다. 마지막에 \\(dp[n, amt]\\)가 \\(amt + 1\\)인지 확인하고, 같으면 목표 금액을 만들 수 없다는 뜻으로 \\(-1\\)을 반환한다. 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change.py<pre><code>def coin_change_dp(coins: list[int], amt: int) -&gt; int:\n \"\"\"동전 교환: 동적 계획법\"\"\"\n n = len(coins)\n MAX = amt + 1\n # dp 테이블을 초기화한다\n dp = [[0] * (amt + 1) for _ in range(n + 1)]\n # 상태 전이: 첫 번째 행과 첫 번째 열\n for a in range(1, amt + 1):\n dp[0][a] = MAX\n # 상태 전이: 나머지 행과 열\n for i in range(1, n + 1):\n for a in range(1, amt + 1):\n if coins[i - 1] &gt; a:\n # 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i][a] = dp[i - 1][a]\n else:\n # 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)\n return dp[n][amt] if dp[n][amt] != MAX else -1\n</code></pre> coin_change.cpp<pre><code>/* 동전 교환: 동적 계획법 */\nint coinChangeDP(vector&lt;int&gt; &amp;coins, int amt) {\n int n = coins.size();\n int MAX = amt + 1;\n // dp 테이블을 초기화한다\n vector&lt;vector&lt;int&gt;&gt; dp(n + 1, vector&lt;int&gt;(amt + 1, 0));\n // 상태 전이: 첫 번째 행과 첫 번째 열\n for (int a = 1; a &lt;= amt; a++) {\n dp[0][a] = MAX;\n }\n // 상태 전이: 나머지 행과 열\n for (int i = 1; i &lt;= n; i++) {\n for (int a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i][a] = dp[i - 1][a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n }\n }\n }\n return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n</code></pre> coin_change.java<pre><code>/* 동전 교환: 동적 계획법 */\nint coinChangeDP(int[] coins, int amt) {\n int n = coins.length;\n int MAX = amt + 1;\n // dp 테이블을 초기화한다\n int[][] dp = new int[n + 1][amt + 1];\n // 상태 전이: 첫 번째 행과 첫 번째 열\n for (int a = 1; a &lt;= amt; a++) {\n dp[0][a] = MAX;\n }\n // 상태 전이: 나머지 행과 열\n for (int i = 1; i &lt;= n; i++) {\n for (int a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i][a] = dp[i - 1][a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n }\n }\n }\n return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n</code></pre> coin_change.cs<pre><code>/* 동전 교환: 동적 계획법 */\nint CoinChangeDP(int[] coins, int amt) {\n int n = coins.Length;\n int MAX = amt + 1;\n // dp 테이블을 초기화한다\n int[,] dp = new int[n + 1, amt + 1];\n // 상태 전이: 첫 번째 행과 첫 번째 열\n for (int a = 1; a &lt;= amt; a++) {\n dp[0, a] = MAX;\n }\n // 상태 전이: 나머지 행과 열\n for (int i = 1; i &lt;= n; i++) {\n for (int a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i, a] = dp[i - 1, a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[i, a] = Math.Min(dp[i - 1, a], dp[i, a - coins[i - 1]] + 1);\n }\n }\n }\n return dp[n, amt] != MAX ? dp[n, amt] : -1;\n}\n</code></pre> coin_change.go<pre><code>/* 동전 교환: 동적 계획법 */\nfunc coinChangeDP(coins []int, amt int) int {\n n := len(coins)\n max := amt + 1\n // dp 테이블을 초기화한다\n dp := make([][]int, n+1)\n for i := 0; i &lt;= n; i++ {\n dp[i] = make([]int, amt+1)\n }\n // 상태 전이: 첫 번째 행과 첫 번째 열\n for a := 1; a &lt;= amt; a++ {\n dp[0][a] = max\n }\n // 상태 전이: 나머지 행과 열\n for i := 1; i &lt;= n; i++ {\n for a := 1; a &lt;= amt; a++ {\n if coins[i-1] &gt; a {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i][a] = dp[i-1][a]\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[i][a] = int(math.Min(float64(dp[i-1][a]), float64(dp[i][a-coins[i-1]]+1)))\n }\n }\n }\n if dp[n][amt] != max {\n return dp[n][amt]\n }\n return -1\n}\n</code></pre> coin_change.swift<pre><code>/* 동전 교환: 동적 계획법 */\nfunc coinChangeDP(coins: [Int], amt: Int) -&gt; Int {\n let n = coins.count\n let MAX = amt + 1\n // dp 테이블을 초기화한다\n var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1)\n // 상태 전이: 첫 번째 행과 첫 번째 열\n for a in 1 ... amt {\n dp[0][a] = MAX\n }\n // 상태 전이: 나머지 행과 열\n for i in 1 ... n {\n for a in 1 ... amt {\n if coins[i - 1] &gt; a {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i][a] = dp[i - 1][a]\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)\n }\n }\n }\n return dp[n][amt] != MAX ? dp[n][amt] : -1\n}\n</code></pre> coin_change.js<pre><code>/* 동전 교환: 동적 계획법 */\nfunction coinChangeDP(coins, amt) {\n const n = coins.length;\n const MAX = amt + 1;\n // dp 테이블을 초기화한다\n const dp = Array.from({ length: n + 1 }, () =&gt;\n Array.from({ length: amt + 1 }, () =&gt; 0)\n );\n // 상태 전이: 첫 번째 행과 첫 번째 열\n for (let a = 1; a &lt;= amt; a++) {\n dp[0][a] = MAX;\n }\n // 상태 전이: 나머지 행과 열\n for (let i = 1; i &lt;= n; i++) {\n for (let a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i][a] = dp[i - 1][a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n }\n }\n }\n return dp[n][amt] !== MAX ? dp[n][amt] : -1;\n}\n</code></pre> coin_change.ts<pre><code>/* 동전 교환: 동적 계획법 */\nfunction coinChangeDP(coins: Array&lt;number&gt;, amt: number): number {\n const n = coins.length;\n const MAX = amt + 1;\n // dp 테이블을 초기화한다\n const dp = Array.from({ length: n + 1 }, () =&gt;\n Array.from({ length: amt + 1 }, () =&gt; 0)\n );\n // 상태 전이: 첫 번째 행과 첫 번째 열\n for (let a = 1; a &lt;= amt; a++) {\n dp[0][a] = MAX;\n }\n // 상태 전이: 나머지 행과 열\n for (let i = 1; i &lt;= n; i++) {\n for (let a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i][a] = dp[i - 1][a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n }\n }\n }\n return dp[n][amt] !== MAX ? dp[n][amt] : -1;\n}\n</code></pre> coin_change.dart<pre><code>/* 동전 교환: 동적 계획법 */\nint coinChangeDP(List&lt;int&gt; coins, int amt) {\n int n = coins.length;\n int MAX = amt + 1;\n // dp 테이블을 초기화한다\n List&lt;List&lt;int&gt;&gt; dp = List.generate(n + 1, (index) =&gt; List.filled(amt + 1, 0));\n // 상태 전이: 첫 번째 행과 첫 번째 열\n for (int a = 1; a &lt;= amt; a++) {\n dp[0][a] = MAX;\n }\n // 상태 전이: 나머지 행과 열\n for (int i = 1; i &lt;= n; i++) {\n for (int a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i][a] = dp[i - 1][a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n }\n }\n }\n return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n</code></pre> coin_change.rs<pre><code>/* 동전 교환: 동적 계획법 */\nfn coin_change_dp(coins: &amp;[i32], amt: usize) -&gt; i32 {\n let n = coins.len();\n let max = amt + 1;\n // dp 테이블을 초기화한다\n let mut dp = vec![vec![0; amt + 1]; n + 1];\n // 상태 전이: 첫 번째 행과 첫 번째 열\n for a in 1..=amt {\n dp[0][a] = max;\n }\n // 상태 전이: 나머지 행과 열\n for i in 1..=n {\n for a in 1..=amt {\n if coins[i - 1] &gt; a as i32 {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i][a] = dp[i - 1][a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[i][a] = std::cmp::min(dp[i - 1][a], dp[i][a - coins[i - 1] as usize] + 1);\n }\n }\n }\n if dp[n][amt] != max {\n return dp[n][amt] as i32;\n } else {\n -1\n }\n}\n</code></pre> coin_change.c<pre><code>/* 동전 교환: 동적 계획법 */\nint coinChangeDP(int coins[], int amt, int coinsSize) {\n int n = coinsSize;\n int MAX = amt + 1;\n // dp 테이블을 초기화한다\n int **dp = malloc((n + 1) * sizeof(int *));\n for (int i = 0; i &lt;= n; i++) {\n dp[i] = calloc(amt + 1, sizeof(int));\n }\n // 상태 전이: 첫 번째 행과 첫 번째 열\n for (int a = 1; a &lt;= amt; a++) {\n dp[0][a] = MAX;\n }\n // 상태 전이: 나머지 행과 열\n for (int i = 1; i &lt;= n; i++) {\n for (int a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i][a] = dp[i - 1][a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[i][a] = myMin(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n }\n }\n }\n int res = dp[n][amt] != MAX ? dp[n][amt] : -1;\n // 메모리 해제\n for (int i = 0; i &lt;= n; i++) {\n free(dp[i]);\n }\n free(dp);\n return res;\n}\n</code></pre> coin_change.kt<pre><code>/* 동전 교환: 동적 계획법 */\nfun coinChangeDP(coins: IntArray, amt: Int): Int {\n val n = coins.size\n val MAX = amt + 1\n // dp 테이블을 초기화한다\n val dp = Array(n + 1) { IntArray(amt + 1) }\n // 상태 전이: 첫 번째 행과 첫 번째 열\n for (a in 1..amt) {\n dp[0][a] = MAX\n }\n // 상태 전이: 나머지 행과 열\n for (i in 1..n) {\n for (a in 1..amt) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i][a] = dp[i - 1][a]\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)\n }\n }\n }\n return if (dp[n][amt] != MAX) dp[n][amt] else -1\n}\n</code></pre> coin_change.rb<pre><code>### 동전 교환: 동적 계획법 ###\ndef coin_change_dp(coins, amt)\n n = coins.length\n _MAX = amt + 1\n # dp 테이블을 초기화한다\n dp = Array.new(n + 1) { Array.new(amt + 1, 0) }\n # 상태 전이: 첫 번째 행과 첫 번째 열\n (1...(amt + 1)).each { |a| dp[0][a] = _MAX }\n # 상태 전이: 나머지 행과 열\n for i in 1...(n + 1)\n for a in 1...(amt + 1)\n if coins[i - 1] &gt; a\n # 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i][a] = dp[i - 1][a]\n else\n # 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[i][a] = [dp[i - 1][a], dp[i][a - coins[i - 1]] + 1].min\n end\n end\n end\n dp[n][amt] != _MAX ? dp[n][amt] : -1\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>아래 그림(14-25)의 동전 교환 동적 계획법 과정은 무한 배낭 문제와 매우 비슷하다.</p> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt;&lt;6&gt;&lt;7&gt;&lt;8&gt;&lt;9&gt;&lt;10&gt;&lt;11&gt;&lt;12&gt;&lt;13&gt;&lt;14&gt;&lt;15&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 14-25   동전 교환 문제의 동적 계획법 과정 </p>","path":["제 14 장   동적 계획법","14.5   무한 배낭 문제"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3_1","level":3,"title":"3.   공간 최적화","text":"<p>동전 교환의 공간 최적화는 무한 배낭과 같은 방식으로 처리한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change.py<pre><code>def coin_change_dp_comp(coins: list[int], amt: int) -&gt; int:\n \"\"\"동전 교환: 공간을 최적화한 동적 계획법\"\"\"\n n = len(coins)\n MAX = amt + 1\n # dp 테이블을 초기화한다\n dp = [MAX] * (amt + 1)\n dp[0] = 0\n # 상태 전이\n for i in range(1, n + 1):\n # 정방향 순회\n for a in range(1, amt + 1):\n if coins[i - 1] &gt; a:\n # 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a]\n else:\n # 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)\n return dp[amt] if dp[amt] != MAX else -1\n</code></pre> coin_change.cpp<pre><code>/* 동전 교환: 공간을 최적화한 동적 계획법 */\nint coinChangeDPComp(vector&lt;int&gt; &amp;coins, int amt) {\n int n = coins.size();\n int MAX = amt + 1;\n // dp 테이블을 초기화한다\n vector&lt;int&gt; dp(amt + 1, MAX);\n dp[0] = 0;\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1);\n }\n }\n }\n return dp[amt] != MAX ? dp[amt] : -1;\n}\n</code></pre> coin_change.java<pre><code>/* 동전 교환: 공간을 최적화한 동적 계획법 */\nint coinChangeDPComp(int[] coins, int amt) {\n int n = coins.length;\n int MAX = amt + 1;\n // dp 테이블을 초기화한다\n int[] dp = new int[amt + 1];\n Arrays.fill(dp, MAX);\n dp[0] = 0;\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);\n }\n }\n }\n return dp[amt] != MAX ? dp[amt] : -1;\n}\n</code></pre> coin_change.cs<pre><code>/* 동전 교환: 공간을 최적화한 동적 계획법 */\nint CoinChangeDPComp(int[] coins, int amt) {\n int n = coins.Length;\n int MAX = amt + 1;\n // dp 테이블을 초기화한다\n int[] dp = new int[amt + 1];\n Array.Fill(dp, MAX);\n dp[0] = 0;\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[a] = Math.Min(dp[a], dp[a - coins[i - 1]] + 1);\n }\n }\n }\n return dp[amt] != MAX ? dp[amt] : -1;\n}\n</code></pre> coin_change.go<pre><code>/* 동전 교환: 동적 계획법 */\nfunc coinChangeDPComp(coins []int, amt int) int {\n n := len(coins)\n max := amt + 1\n // dp 테이블을 초기화한다\n dp := make([]int, amt+1)\n for i := 1; i &lt;= amt; i++ {\n dp[i] = max\n }\n // 상태 전이\n for i := 1; i &lt;= n; i++ {\n // 정방향 순회\n for a := 1; a &lt;= amt; a++ {\n if coins[i-1] &gt; a {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a]\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[a] = int(math.Min(float64(dp[a]), float64(dp[a-coins[i-1]]+1)))\n }\n }\n }\n if dp[amt] != max {\n return dp[amt]\n }\n return -1\n}\n</code></pre> coin_change.swift<pre><code>/* 동전 교환: 공간을 최적화한 동적 계획법 */\nfunc coinChangeDPComp(coins: [Int], amt: Int) -&gt; Int {\n let n = coins.count\n let MAX = amt + 1\n // dp 테이블을 초기화한다\n var dp = Array(repeating: MAX, count: amt + 1)\n dp[0] = 0\n // 상태 전이\n for i in 1 ... n {\n for a in 1 ... amt {\n if coins[i - 1] &gt; a {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a]\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)\n }\n }\n }\n return dp[amt] != MAX ? dp[amt] : -1\n}\n</code></pre> coin_change.js<pre><code>/* 동전 교환: 공간을 최적화한 동적 계획법 */\nfunction coinChangeDPComp(coins, amt) {\n const n = coins.length;\n const MAX = amt + 1;\n // dp 테이블을 초기화한다\n const dp = Array.from({ length: amt + 1 }, () =&gt; MAX);\n dp[0] = 0;\n // 상태 전이\n for (let i = 1; i &lt;= n; i++) {\n for (let a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);\n }\n }\n }\n return dp[amt] !== MAX ? dp[amt] : -1;\n}\n</code></pre> coin_change.ts<pre><code>/* 동전 교환: 공간을 최적화한 동적 계획법 */\nfunction coinChangeDPComp(coins: Array&lt;number&gt;, amt: number): number {\n const n = coins.length;\n const MAX = amt + 1;\n // dp 테이블을 초기화한다\n const dp = Array.from({ length: amt + 1 }, () =&gt; MAX);\n dp[0] = 0;\n // 상태 전이\n for (let i = 1; i &lt;= n; i++) {\n for (let a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);\n }\n }\n }\n return dp[amt] !== MAX ? dp[amt] : -1;\n}\n</code></pre> coin_change.dart<pre><code>/* 동전 교환: 공간을 최적화한 동적 계획법 */\nint coinChangeDPComp(List&lt;int&gt; coins, int amt) {\n int n = coins.length;\n int MAX = amt + 1;\n // dp 테이블을 초기화한다\n List&lt;int&gt; dp = List.filled(amt + 1, MAX);\n dp[0] = 0;\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1);\n }\n }\n }\n return dp[amt] != MAX ? dp[amt] : -1;\n}\n</code></pre> coin_change.rs<pre><code>/* 동전 교환: 공간을 최적화한 동적 계획법 */\nfn coin_change_dp_comp(coins: &amp;[i32], amt: usize) -&gt; i32 {\n let n = coins.len();\n let max = amt + 1;\n // dp 테이블을 초기화한다\n let mut dp = vec![0; amt + 1];\n dp.fill(max);\n dp[0] = 0;\n // 상태 전이\n for i in 1..=n {\n for a in 1..=amt {\n if coins[i - 1] &gt; a as i32 {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[a] = std::cmp::min(dp[a], dp[a - coins[i - 1] as usize] + 1);\n }\n }\n }\n if dp[amt] != max {\n return dp[amt] as i32;\n } else {\n -1\n }\n}\n</code></pre> coin_change.c<pre><code>/* 동전 교환: 공간을 최적화한 동적 계획법 */\nint coinChangeDPComp(int coins[], int amt, int coinsSize) {\n int n = coinsSize;\n int MAX = amt + 1;\n // dp 테이블을 초기화한다\n int *dp = malloc((amt + 1) * sizeof(int));\n for (int j = 1; j &lt;= amt; j++) {\n dp[j] = MAX;\n } \n dp[0] = 0;\n\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[a] = myMin(dp[a], dp[a - coins[i - 1]] + 1);\n }\n }\n }\n int res = dp[amt] != MAX ? dp[amt] : -1;\n // 메모리 해제\n free(dp);\n return res;\n}\n</code></pre> coin_change.kt<pre><code>/* 동전 교환: 공간을 최적화한 동적 계획법 */\nfun coinChangeDPComp(coins: IntArray, amt: Int): Int {\n val n = coins.size\n val MAX = amt + 1\n // dp 테이블을 초기화한다\n val dp = IntArray(amt + 1)\n dp.fill(MAX)\n dp[0] = 0\n // 상태 전이\n for (i in 1..n) {\n for (a in 1..amt) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a]\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)\n }\n }\n }\n return if (dp[amt] != MAX) dp[amt] else -1\n}\n</code></pre> coin_change.rb<pre><code>### 동전 교환: 공간을 최적화한 동적 계획법 ###\ndef coin_change_dp_comp(coins, amt)\n n = coins.length\n _MAX = amt + 1\n # dp 테이블을 초기화한다\n dp = Array.new(amt + 1, _MAX)\n dp[0] = 0\n # 상태 전이\n for i in 1...(n + 1)\n # 정방향 순회\n for a in 1...(amt + 1)\n if coins[i - 1] &gt; a\n # 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a]\n else\n # 동전 i를 선택하지 않는 경우와 선택하는 경우 중 더 작은 값\n dp[a] = [dp[a], dp[a - coins[i - 1]] + 1].min\n end\n end\n end\n dp[amt] != _MAX ? dp[amt] : -1\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 14 장   동적 계획법","14.5   무한 배낭 문제"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1453-ii","level":2,"title":"14.5.3   동전 교환 문제 II","text":"<p>문제</p> <p>동전 \\(n\\) 종류가 주어지며 \\(i\\) 번째 동전의 액면가는 \\(coins[i - 1]\\)이고 목표 금액은 \\(amt\\)다. 각 종류의 동전을 반복해서 선택할 수 있을 때, 목표 금액을 만드는 동전 조합 수를 구하라. 예시는 아래 그림(14-26)과 같다.</p> <p></p> <p> 그림 14-26   동전 교환 문제 II 예시 데이터 </p>","path":["제 14 장   동적 계획법","14.5   무한 배낭 문제"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1_2","level":3,"title":"1.   동적 계획법 접근","text":"<p>앞 문제와 달리 이번에는 조합 수를 구하므로 부분 문제는 앞의 \\(i\\) 종류 동전으로 금액 \\(a\\)를 만드는 조합 수가 된다. \\(dp\\) 테이블은 여전히 \\((n+1) \\times (amt + 1)\\) 크기의 2차원 행렬이다.</p> <p>현재 상태의 조합 수는 현재 동전을 선택하지 않는 경우와 선택하는 경우의 조합 수를 더한 값이다. 상태 전이 식은 다음과 같다.</p> \\[ dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]] \\] <p>목표 금액이 \\(0\\)이면 동전을 하나도 선택하지 않고도 만들 수 있으므로 첫 열의 모든 \\(dp[i, 0]\\)을 \\(1\\)로 초기화한다. 동전이 없으면 \\(&gt;0\\)인 목표 금액을 만들 수 없으므로 첫 행의 모든 \\(dp[0, a]\\)는 \\(0\\)이다.</p>","path":["제 14 장   동적 계획법","14.5   무한 배낭 문제"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2_2","level":3,"title":"2.   코드 구현","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_ii.py<pre><code>def coin_change_ii_dp(coins: list[int], amt: int) -&gt; int:\n \"\"\"동전 교환 II: 동적 계획법\"\"\"\n n = len(coins)\n # dp 테이블을 초기화한다\n dp = [[0] * (amt + 1) for _ in range(n + 1)]\n # 첫 번째 열을 초기화한다\n for i in range(n + 1):\n dp[i][0] = 1\n # 상태 전이\n for i in range(1, n + 1):\n for a in range(1, amt + 1):\n if coins[i - 1] &gt; a:\n # 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i][a] = dp[i - 1][a]\n else:\n # 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n return dp[n][amt]\n</code></pre> coin_change_ii.cpp<pre><code>/* 동전 교환 II: 동적 계획법 */\nint coinChangeIIDP(vector&lt;int&gt; &amp;coins, int amt) {\n int n = coins.size();\n // dp 테이블을 초기화한다\n vector&lt;vector&lt;int&gt;&gt; dp(n + 1, vector&lt;int&gt;(amt + 1, 0));\n // 첫 번째 열을 초기화한다\n for (int i = 0; i &lt;= n; i++) {\n dp[i][0] = 1;\n }\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i][a] = dp[i - 1][a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n }\n }\n }\n return dp[n][amt];\n}\n</code></pre> coin_change_ii.java<pre><code>/* 동전 교환 II: 동적 계획법 */\nint coinChangeIIDP(int[] coins, int amt) {\n int n = coins.length;\n // dp 테이블을 초기화한다\n int[][] dp = new int[n + 1][amt + 1];\n // 첫 번째 열을 초기화한다\n for (int i = 0; i &lt;= n; i++) {\n dp[i][0] = 1;\n }\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i][a] = dp[i - 1][a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n }\n }\n }\n return dp[n][amt];\n}\n</code></pre> coin_change_ii.cs<pre><code>/* 동전 교환 II: 동적 계획법 */\nint CoinChangeIIDP(int[] coins, int amt) {\n int n = coins.Length;\n // dp 테이블을 초기화한다\n int[,] dp = new int[n + 1, amt + 1];\n // 첫 번째 열을 초기화한다\n for (int i = 0; i &lt;= n; i++) {\n dp[i, 0] = 1;\n }\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i, a] = dp[i - 1, a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[i, a] = dp[i - 1, a] + dp[i, a - coins[i - 1]];\n }\n }\n }\n return dp[n, amt];\n}\n</code></pre> coin_change_ii.go<pre><code>/* 동전 교환 II: 동적 계획법 */\nfunc coinChangeIIDP(coins []int, amt int) int {\n n := len(coins)\n // dp 테이블을 초기화한다\n dp := make([][]int, n+1)\n for i := 0; i &lt;= n; i++ {\n dp[i] = make([]int, amt+1)\n }\n // 첫 번째 열을 초기화한다\n for i := 0; i &lt;= n; i++ {\n dp[i][0] = 1\n }\n // 상태 전이: 나머지 행과 열\n for i := 1; i &lt;= n; i++ {\n for a := 1; a &lt;= amt; a++ {\n if coins[i-1] &gt; a {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i][a] = dp[i-1][a]\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[i][a] = dp[i-1][a] + dp[i][a-coins[i-1]]\n }\n }\n }\n return dp[n][amt]\n}\n</code></pre> coin_change_ii.swift<pre><code>/* 동전 교환 II: 동적 계획법 */\nfunc coinChangeIIDP(coins: [Int], amt: Int) -&gt; Int {\n let n = coins.count\n // dp 테이블을 초기화한다\n var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1)\n // 첫 번째 열을 초기화한다\n for i in 0 ... n {\n dp[i][0] = 1\n }\n // 상태 전이\n for i in 1 ... n {\n for a in 1 ... amt {\n if coins[i - 1] &gt; a {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i][a] = dp[i - 1][a]\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n }\n }\n }\n return dp[n][amt]\n}\n</code></pre> coin_change_ii.js<pre><code>/* 동전 교환 II: 동적 계획법 */\nfunction coinChangeIIDP(coins, amt) {\n const n = coins.length;\n // dp 테이블을 초기화한다\n const dp = Array.from({ length: n + 1 }, () =&gt;\n Array.from({ length: amt + 1 }, () =&gt; 0)\n );\n // 첫 번째 열을 초기화한다\n for (let i = 0; i &lt;= n; i++) {\n dp[i][0] = 1;\n }\n // 상태 전이\n for (let i = 1; i &lt;= n; i++) {\n for (let a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i][a] = dp[i - 1][a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n }\n }\n }\n return dp[n][amt];\n}\n</code></pre> coin_change_ii.ts<pre><code>/* 동전 교환 II: 동적 계획법 */\nfunction coinChangeIIDP(coins: Array&lt;number&gt;, amt: number): number {\n const n = coins.length;\n // dp 테이블을 초기화한다\n const dp = Array.from({ length: n + 1 }, () =&gt;\n Array.from({ length: amt + 1 }, () =&gt; 0)\n );\n // 첫 번째 열을 초기화한다\n for (let i = 0; i &lt;= n; i++) {\n dp[i][0] = 1;\n }\n // 상태 전이\n for (let i = 1; i &lt;= n; i++) {\n for (let a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i][a] = dp[i - 1][a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n }\n }\n }\n return dp[n][amt];\n}\n</code></pre> coin_change_ii.dart<pre><code>/* 동전 교환 II: 동적 계획법 */\nint coinChangeIIDP(List&lt;int&gt; coins, int amt) {\n int n = coins.length;\n // dp 테이블을 초기화한다\n List&lt;List&lt;int&gt;&gt; dp = List.generate(n + 1, (index) =&gt; List.filled(amt + 1, 0));\n // 첫 번째 열을 초기화한다\n for (int i = 0; i &lt;= n; i++) {\n dp[i][0] = 1;\n }\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i][a] = dp[i - 1][a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n }\n }\n }\n return dp[n][amt];\n}\n</code></pre> coin_change_ii.rs<pre><code>/* 동전 교환 II: 동적 계획법 */\nfn coin_change_ii_dp(coins: &amp;[i32], amt: usize) -&gt; i32 {\n let n = coins.len();\n // dp 테이블을 초기화한다\n let mut dp = vec![vec![0; amt + 1]; n + 1];\n // 첫 번째 열을 초기화한다\n for i in 0..=n {\n dp[i][0] = 1;\n }\n // 상태 전이\n for i in 1..=n {\n for a in 1..=amt {\n if coins[i - 1] &gt; a as i32 {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i][a] = dp[i - 1][a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1] as usize];\n }\n }\n }\n dp[n][amt]\n}\n</code></pre> coin_change_ii.c<pre><code>/* 동전 교환 II: 동적 계획법 */\nint coinChangeIIDP(int coins[], int amt, int coinsSize) {\n int n = coinsSize;\n // dp 테이블을 초기화한다\n int **dp = malloc((n + 1) * sizeof(int *));\n for (int i = 0; i &lt;= n; i++) {\n dp[i] = calloc(amt + 1, sizeof(int));\n }\n // 첫 번째 열을 초기화한다\n for (int i = 0; i &lt;= n; i++) {\n dp[i][0] = 1;\n }\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i][a] = dp[i - 1][a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n }\n }\n }\n int res = dp[n][amt];\n // 메모리 해제\n for (int i = 0; i &lt;= n; i++) {\n free(dp[i]);\n }\n free(dp);\n return res;\n}\n</code></pre> coin_change_ii.kt<pre><code>/* 동전 교환 II: 동적 계획법 */\nfun coinChangeIIDP(coins: IntArray, amt: Int): Int {\n val n = coins.size\n // dp 테이블을 초기화한다\n val dp = Array(n + 1) { IntArray(amt + 1) }\n // 첫 번째 열을 초기화한다\n for (i in 0..n) {\n dp[i][0] = 1\n }\n // 상태 전이\n for (i in 1..n) {\n for (a in 1..amt) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i][a] = dp[i - 1][a]\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n }\n }\n }\n return dp[n][amt]\n}\n</code></pre> coin_change_ii.rb<pre><code>### 동전 교환 II: 동적 계획법 ###\ndef coin_change_ii_dp(coins, amt)\n n = coins.length\n # dp 테이블을 초기화한다\n dp = Array.new(n + 1) { Array.new(amt + 1, 0) }\n # 첫 번째 열을 초기화한다\n (0...(n + 1)).each { |i| dp[i][0] = 1 }\n # 상태 전이\n for i in 1...(n + 1)\n for a in 1...(amt + 1)\n if coins[i - 1] &gt; a\n # 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[i][a] = dp[i - 1][a]\n else\n # 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n end\n end\n end\n dp[n][amt]\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 14 장   동적 계획법","14.5   무한 배낭 문제"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3_2","level":3,"title":"3.   공간 최적화","text":"<p>공간 최적화 방법도 같으며, 동전 차원을 없애면 된다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_ii.py<pre><code>def coin_change_ii_dp_comp(coins: list[int], amt: int) -&gt; int:\n \"\"\"동전 교환 II: 공간을 최적화한 동적 계획법\"\"\"\n n = len(coins)\n # dp 테이블을 초기화한다\n dp = [0] * (amt + 1)\n dp[0] = 1\n # 상태 전이\n for i in range(1, n + 1):\n # 정방향 순회\n for a in range(1, amt + 1):\n if coins[i - 1] &gt; a:\n # 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a]\n else:\n # 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[a] = dp[a] + dp[a - coins[i - 1]]\n return dp[amt]\n</code></pre> coin_change_ii.cpp<pre><code>/* 동전 교환 II: 공간을 최적화한 동적 계획법 */\nint coinChangeIIDPComp(vector&lt;int&gt; &amp;coins, int amt) {\n int n = coins.size();\n // dp 테이블을 초기화한다\n vector&lt;int&gt; dp(amt + 1, 0);\n dp[0] = 1;\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[a] = dp[a] + dp[a - coins[i - 1]];\n }\n }\n }\n return dp[amt];\n}\n</code></pre> coin_change_ii.java<pre><code>/* 동전 교환 II: 공간을 최적화한 동적 계획법 */\nint coinChangeIIDPComp(int[] coins, int amt) {\n int n = coins.length;\n // dp 테이블을 초기화한다\n int[] dp = new int[amt + 1];\n dp[0] = 1;\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[a] = dp[a] + dp[a - coins[i - 1]];\n }\n }\n }\n return dp[amt];\n}\n</code></pre> coin_change_ii.cs<pre><code>/* 동전 교환 II: 공간을 최적화한 동적 계획법 */\nint CoinChangeIIDPComp(int[] coins, int amt) {\n int n = coins.Length;\n // dp 테이블을 초기화한다\n int[] dp = new int[amt + 1];\n dp[0] = 1;\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[a] = dp[a] + dp[a - coins[i - 1]];\n }\n }\n }\n return dp[amt];\n}\n</code></pre> coin_change_ii.go<pre><code>/* 동전 교환 II: 공간을 최적화한 동적 계획법 */\nfunc coinChangeIIDPComp(coins []int, amt int) int {\n n := len(coins)\n // dp 테이블을 초기화한다\n dp := make([]int, amt+1)\n dp[0] = 1\n // 상태 전이\n for i := 1; i &lt;= n; i++ {\n // 정방향 순회\n for a := 1; a &lt;= amt; a++ {\n if coins[i-1] &gt; a {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a]\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[a] = dp[a] + dp[a-coins[i-1]]\n }\n }\n }\n return dp[amt]\n}\n</code></pre> coin_change_ii.swift<pre><code>/* 동전 교환 II: 공간을 최적화한 동적 계획법 */\nfunc coinChangeIIDPComp(coins: [Int], amt: Int) -&gt; Int {\n let n = coins.count\n // dp 테이블을 초기화한다\n var dp = Array(repeating: 0, count: amt + 1)\n dp[0] = 1\n // 상태 전이\n for i in 1 ... n {\n for a in 1 ... amt {\n if coins[i - 1] &gt; a {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a]\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[a] = dp[a] + dp[a - coins[i - 1]]\n }\n }\n }\n return dp[amt]\n}\n</code></pre> coin_change_ii.js<pre><code>/* 동전 교환 II: 공간을 최적화한 동적 계획법 */\nfunction coinChangeIIDPComp(coins, amt) {\n const n = coins.length;\n // dp 테이블을 초기화한다\n const dp = Array.from({ length: amt + 1 }, () =&gt; 0);\n dp[0] = 1;\n // 상태 전이\n for (let i = 1; i &lt;= n; i++) {\n for (let a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[a] = dp[a] + dp[a - coins[i - 1]];\n }\n }\n }\n return dp[amt];\n}\n</code></pre> coin_change_ii.ts<pre><code>/* 동전 교환 II: 공간을 최적화한 동적 계획법 */\nfunction coinChangeIIDPComp(coins: Array&lt;number&gt;, amt: number): number {\n const n = coins.length;\n // dp 테이블을 초기화한다\n const dp = Array.from({ length: amt + 1 }, () =&gt; 0);\n dp[0] = 1;\n // 상태 전이\n for (let i = 1; i &lt;= n; i++) {\n for (let a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[a] = dp[a] + dp[a - coins[i - 1]];\n }\n }\n }\n return dp[amt];\n}\n</code></pre> coin_change_ii.dart<pre><code>/* 동전 교환 II: 공간을 최적화한 동적 계획법 */\nint coinChangeIIDPComp(List&lt;int&gt; coins, int amt) {\n int n = coins.length;\n // dp 테이블을 초기화한다\n List&lt;int&gt; dp = List.filled(amt + 1, 0);\n dp[0] = 1;\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[a] = dp[a] + dp[a - coins[i - 1]];\n }\n }\n }\n return dp[amt];\n}\n</code></pre> coin_change_ii.rs<pre><code>/* 동전 교환 II: 공간을 최적화한 동적 계획법 */\nfn coin_change_ii_dp_comp(coins: &amp;[i32], amt: usize) -&gt; i32 {\n let n = coins.len();\n // dp 테이블을 초기화한다\n let mut dp = vec![0; amt + 1];\n dp[0] = 1;\n // 상태 전이\n for i in 1..=n {\n for a in 1..=amt {\n if coins[i - 1] &gt; a as i32 {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[a] = dp[a] + dp[a - coins[i - 1] as usize];\n }\n }\n }\n dp[amt]\n}\n</code></pre> coin_change_ii.c<pre><code>/* 동전 교환 II: 공간을 최적화한 동적 계획법 */\nint coinChangeIIDPComp(int coins[], int amt, int coinsSize) {\n int n = coinsSize;\n // dp 테이블을 초기화한다\n int *dp = calloc(amt + 1, sizeof(int));\n dp[0] = 1;\n // 상태 전이\n for (int i = 1; i &lt;= n; i++) {\n for (int a = 1; a &lt;= amt; a++) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a];\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[a] = dp[a] + dp[a - coins[i - 1]];\n }\n }\n }\n int res = dp[amt];\n // 메모리 해제\n free(dp);\n return res;\n}\n</code></pre> coin_change_ii.kt<pre><code>/* 동전 교환 II: 공간을 최적화한 동적 계획법 */\nfun coinChangeIIDPComp(coins: IntArray, amt: Int): Int {\n val n = coins.size\n // dp 테이블을 초기화한다\n val dp = IntArray(amt + 1)\n dp[0] = 1\n // 상태 전이\n for (i in 1..n) {\n for (a in 1..amt) {\n if (coins[i - 1] &gt; a) {\n // 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a]\n } else {\n // 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[a] = dp[a] + dp[a - coins[i - 1]]\n }\n }\n }\n return dp[amt]\n}\n</code></pre> coin_change_ii.rb<pre><code>### 동전 교환 II: 공간을 최적화한 동적 계획법 ###\ndef coin_change_ii_dp_comp(coins, amt)\n n = coins.length\n # dp 테이블을 초기화한다\n dp = Array.new(amt + 1, 0)\n dp[0] = 1\n # 상태 전이\n for i in 1...(n + 1)\n # 정방향 순회\n for a in 1...(amt + 1)\n if coins[i - 1] &gt; a\n # 목표 금액을 초과하면 동전 i를 선택하지 않는다\n dp[a] = dp[a]\n else\n # 동전 i를 선택하지 않는 경우와 선택하는 경우의 방법 수를 더한다\n dp[a] = dp[a] + dp[a - coins[i - 1]]\n end\n end\n end\n dp[amt]\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 14 장   동적 계획법","14.5   무한 배낭 문제"],"tags":[]},{"location":"chapter_graph/","level":1,"title":"제 9 장   그래프","text":"<p>이 장을 시작하며</p> <p>삶이라는 여정에서 우리는 하나하나의 노드처럼, 보이지 않는 수많은 간선으로 연결되어 있다.</p> <p>만남과 헤어짐은 매번 이 거대한 네트워크 그래프에 저마다의 흔적을 남긴다.</p>","path":["제 9 장   그래프"],"tags":[]},{"location":"chapter_graph/#_1","level":2,"title":"이 장의 내용","text":"<ul> <li>9.1   그래프</li> <li>9.2   그래프의 기본 연산</li> <li>9.3   그래프 순회</li> <li>9.4   요약</li> <li>9.5   연습 문제</li> </ul>","path":["제 9 장   그래프"],"tags":[]},{"location":"chapter_graph/exercises/","level":1,"title":"9.5   연습 문제","text":"","path":["제 9 장   그래프","9.5   연습 문제"],"tags":[]},{"location":"chapter_graph/exercises/#951","level":2,"title":"9.5.1   개념 확인","text":"","path":["제 9 장   그래프","9.5   연습 문제"],"tags":[]},{"location":"chapter_graph/exercises/#1","level":3,"title":"1.   같은 그래프를 두 가지 방식으로 표현하기","text":"<p>무방향 그래프의 정점은 <code>A, B, C, D</code> 4개이며, 간선은 <code>A-B, A-C, B-C, C-D</code>다.</p> <ol> <li>이 그래프의 인접 리스트를 적어 보자.</li> <li>0과 1만 들어가는 인접 행렬을 완성해 보자.</li> <li><code>A</code>와 <code>D</code>가 직접 연결되어 있는지 판단할 때, 저장된 항목 하나만 확인하면 되는 표현 방식은 무엇인가?</li> <li>정점이 많고 간선이 적다면 일반적으로 어느 표현 방식이 공간을 더 절약하는가?</li> </ol> 참고 답안 <ol> <li> <p>인접 리스트는 다음과 같다.</p> <pre><code>A: B, C\nB: A, C\nC: A, B, D\nD: C\n</code></pre> </li> <li> <p>인접 행렬은 다음과 같다.</p> A B C D A 0 1 1 0 B 1 0 1 0 C 1 1 0 1 D 0 0 1 0 </li> <li> <p>인접 행렬에서는 <code>A</code>행과 <code>D</code>열이 만나는 항목을 바로 확인할 수 있다. 따라서 임의의 두 정점이 직접 연결되어 있는지 판단하기에 적합하다.</p> </li> <li> <p>정점이 많고 간선이 적을 때 인접 리스트는 실제로 존재하는 간선만 기록한다. 따라서 모든 정점 쌍에 대해 자리를 마련하는 인접 행렬보다 일반적으로 공간을 더 절약한다.</p> </li> </ol>","path":["제 9 장   그래프","9.5   연습 문제"],"tags":[]},{"location":"chapter_graph/exercises/#2","level":3,"title":"2.   너비 우선 탐색과 깊이 우선 탐색의 방문 순서","text":"<p>무방향 그래프의 정점은 <code>A, B, C, D, E</code>이며, 간선은 <code>A-B, A-C, B-D, C-D, D-E</code>다.</p> <p>A에서 출발하고, 방문하지 않은 인접 정점이 여러 개면 알파벳순으로 선택한다고 하자.</p> <ol> <li>너비 우선 탐색(BFS)의 방문 순서를 적어 보자.</li> <li>재귀 깊이 우선 탐색(DFS)의 방문 순서를 적어 보자.</li> <li>두 탐색 모두 이미 방문한 정점을 기록해야 하는 이유는 무엇인가?</li> </ol> 참고 답안 <ol> <li> <p>BFS의 방문 순서는 <code>A, B, C, D, E</code>다. A에서 간선 하나로 도달하는 B와 C를 먼저 방문하고, 그다음 더 멀리 있는 D와 E를 방문한다.</p> </li> <li> <p>DFS의 방문 순서는 <code>A, B, D, C, E</code>다. 현재 정점에서 방문하지 않은 인접 정점으로 차례대로 들어가므로 먼저 <code>A → B → D → C</code>를 따라간다. C에서 새로 방문할 인접 정점이 없으면 D로 돌아와 E를 방문한다.</p> </li> <li> <p>그래프에는 <code>A-B-D-C-A</code>와 같은 사이클이 있다. 이미 방문한 정점을 기록하지 않으면 같은 정점들을 사이클을 따라 반복해서 방문하여 탐색이 정상적으로 끝나지 않을 수 있다.</p> </li> </ol>","path":["제 9 장   그래프","9.5   연습 문제"],"tags":[]},{"location":"chapter_graph/exercises/#3-bfs","level":3,"title":"3.   BFS 한 번으로 그래프 전체를 방문할 수 있는가","text":"<p>무방향 그래프의 정점은 <code>A, B, C, D, E, F</code>이며, 간선은 <code>A-B, B-C, D-E</code>뿐이다.</p> <ol> <li>A에서 BFS를 한 번 시작하면 어떤 정점을 방문할 수 있는가?</li> <li>1번의 결과를 바탕으로, 이 BFS가 그래프의 모든 정점을 방문했는지 판단하고 이유를 설명하라.</li> <li>모든 정점을 알파벳순으로 확인하면서 방문하지 않은 정점을 만날 때마다 BFS를 새로 시작한다면, 각 BFS의 시작 정점은 무엇인가? 이 그래프는 서로 연결되지 않은 부분인 연결 요소 몇 개로 이루어져 있는가?</li> </ol> 참고 답안 <ol> <li> <p>A에서 출발하면 <code>A, B, C</code>만 방문할 수 있다.</p> </li> <li> <p>모든 정점을 방문한 것은 아니다. <code>D, E</code>는 서로 연결된 별개의 부분을 이루고, F는 홀로 떨어진 정점이다. 이 정점들과 A 사이에는 경로가 없으므로 A에서 출발해서는 도달할 수 없다.</p> </li> <li> <p>세 번의 BFS는 차례대로 <code>A, D, F</code>에서 시작하여 각각 <code>{A, B, C}</code>, <code>{D, E}</code>, <code>{F}</code>를 방문한다. 따라서 이 그래프의 연결 요소는 3개다.</p> </li> </ol>","path":["제 9 장   그래프","9.5   연습 문제"],"tags":[]},{"location":"chapter_graph/exercises/#952","level":2,"title":"9.5.2   프로그래밍 연습","text":"","path":["제 9 장   그래프","9.5   연습 문제"],"tags":[]},{"location":"chapter_graph/exercises/#1_1","level":3,"title":"1.   무방향 그래프에 경로가 존재하는지 판단하기","text":"<p>정점이 \\(n\\)개인 무방향 그래프가 주어지며, 정점 번호는 \\(0\\)부터 \\(n-1\\)까지다. 배열 <code>edges</code>의 각 항목 <code>[u, v]</code>는 정점 <code>u</code>와 <code>v</code> 사이에 무방향 간선이 있음을 나타낸다.</p> <p>시작 정점 <code>source</code>와 도착 정점 <code>destination</code>도 주어진다. 먼저 <code>edges</code>로 인접 리스트를 만든 뒤 BFS 또는 DFS를 사용하여 <code>source</code>에서 <code>destination</code>까지의 경로가 있는지 판단하라. 있으면 <code>true</code>를, 없으면 <code>false</code>를 반환하라. 그래프에는 사이클이 있을 수 있으며, 모든 정점이 서로 연결되어 있지 않을 수도 있다.</p> 풀이 힌트 <ol> <li>무방향 간선은 양쪽 방향을 모두 추가해야 한다.</li> <li>그래프에는 사이클이 있을 수 있으므로 이미 방문한 노드를 반드시 기록해야 한다.</li> <li>source에서 출발하여 destination을 만나면 true를 반환한다. 탐색을 마칠 때까지 만나지 못하면 false를 반환한다.</li> </ol> <p>LeetCode</p>","path":["제 9 장   그래프","9.5   연습 문제"],"tags":[]},{"location":"chapter_graph/graph/","level":1,"title":"9.1   그래프","text":"<p>그래프(graph)는 정점(vertex)과 간선(edge)으로 이루어진 비선형 자료 구조다. 그래프 \\(G\\)는 정점 집합 \\(V\\)와 간선 집합 \\(E\\)로 추상적으로 표현할 수 있다. 다음 예시는 정점 5개와 간선 7개로 이루어진 그래프를 나타낸다.</p> \\[ \\begin{aligned} V &amp; = \\{ 1, 2, 3, 4, 5 \\} \\newline E &amp; = \\{ (1,2), (1,3), (1,5), (2,3), (2,4), (2,5), (4,5) \\} \\newline G &amp; = \\{ V, E \\} \\newline \\end{aligned} \\] <p>정점을 노드로, 간선을 노드들을 연결하는 참조(포인터)로 보면 그래프는 연결 리스트를 확장한 자료 구조로 생각할 수 있다. 아래 그림(9-1)과 같이 선형 관계(연결 리스트)와 분할 정복 관계(트리)에 비해 네트워크 관계(그래프)는 자유도가 높아 더 복잡하다.</p> <p></p> <p> 그림 9-1   연결 리스트, 트리, 그래프 사이의 관계 </p>","path":["제 9 장   그래프","9.1   그래프"],"tags":[]},{"location":"chapter_graph/graph/#911","level":2,"title":"9.1.1   그래프의 주요 종류와 용어","text":"<p>간선에 방향이 있는지에 따라 아래 그림(9-2)과 같이 무방향 그래프(undirected graph)와 방향 그래프(directed graph)로 나뉜다.</p> <ul> <li>무방향 그래프에서 간선은 두 정점 사이의 '양방향' 연결 관계를 나타낸다. 위챗이나 QQ의 '친구 관계'가 그 예다.</li> <li>방향 그래프에서 간선은 방향을 가진다. 즉, \\(A \\rightarrow B\\)와 \\(A \\leftarrow B\\) 방향의 간선은 서로 독립적이다. 웨이보나 더우인의 '팔로우'와 '팔로워' 관계가 그 예다.</li> </ul> <p></p> <p> 그림 9-2   방향 그래프와 무방향 그래프 </p> <p>모든 정점이 연결되어 있는지에 따라 아래 그림(9-3)과 같이 연결 그래프(connected graph)와 비연결 그래프(disconnected graph)로 나뉜다.</p> <ul> <li>연결 그래프에서는 한 정점에서 출발하여 다른 모든 정점에 도달할 수 있다.</li> <li>비연결 그래프에서는 한 정점에서 출발했을 때 도달할 수 없는 정점이 하나 이상 존재한다.</li> </ul> <p></p> <p> 그림 9-3   연결 그래프와 비연결 그래프 </p> <p>간선에 '가중치' 변수를 추가하면 아래 그림(9-4)과 같은 가중 그래프(weighted graph)를 얻는다. 예를 들어 『왕자영요』 같은 모바일 게임에서는 함께 게임한 시간을 바탕으로 플레이어 간의 '친밀도'를 계산한다. 이러한 친밀도 네트워크는 가중 그래프로 표현할 수 있다.</p> <p></p> <p> 그림 9-4   가중 그래프와 비가중 그래프 </p> <p>그래프 자료 구조에서 자주 사용하는 용어는 다음과 같다.</p> <ul> <li>인접(adjacency): 두 정점 사이에 간선이 있으면 두 정점이 '인접한다'고 한다. 위 그림(9-4)에서 정점 1에 인접한 정점은 2, 3, 5다.</li> <li>경로(path): 정점 A에서 정점 B로 갈 때 거치는 간선의 순서를 A에서 B로 가는 '경로'라고 한다. 위 그림(9-4)에서 간선 순서 1-5-2-4는 정점 1에서 정점 4로 가는 경로다.</li> <li>차수(degree): 한 정점에 연결된 간선의 수다. 방향 그래프에서는 진입 차수(in-degree)가 해당 정점으로 들어오는 간선 수를, 진출 차수(out-degree)가 해당 정점에서 나가는 간선 수를 나타낸다.</li> </ul>","path":["제 9 장   그래프","9.1   그래프"],"tags":[]},{"location":"chapter_graph/graph/#912","level":2,"title":"9.1.2   그래프의 표현","text":"<p>그래프를 표현하는 대표적인 방법은 '인접 행렬'과 '인접 리스트'다. 아래에서는 무방향 그래프를 예로 든다.</p>","path":["제 9 장   그래프","9.1   그래프"],"tags":[]},{"location":"chapter_graph/graph/#1","level":3,"title":"1.   인접 행렬","text":"<p>그래프의 정점 수를 \\(n\\)이라고 하자. 인접 행렬(adjacency matrix)은 \\(n \\times n\\) 크기의 행렬로 그래프를 표현한다. 각 행(열)은 정점 하나를, 행렬의 원소는 간선을 나타내며, \\(1\\) 또는 \\(0\\)으로 두 정점 사이에 간선이 있는지 표시한다.</p> <p>아래 그림(9-5)과 같이 인접 행렬을 \\(M\\), 정점 리스트를 \\(V\\)라고 하면, 행렬 원소 \\(M[i, j] = 1\\)은 정점 \\(V[i]\\)와 정점 \\(V[j]\\) 사이에 간선이 있음을 뜻한다. 반대로 \\(M[i, j] = 0\\)은 두 정점 사이에 간선이 없음을 뜻한다.</p> <p></p> <p> 그림 9-5   그래프의 인접 행렬 표현 </p> <p>인접 행렬에는 다음 성질이 있다.</p> <ul> <li>단순 그래프에서는 정점이 자기 자신과 연결될 수 없으므로, 인접 행렬의 주대각선 원소는 의미가 없다.</li> <li>무방향 그래프에서는 두 방향의 간선이 같으므로, 인접 행렬은 주대각선에 대해 대칭이다.</li> <li>인접 행렬의 원소 \\(1\\)과 \\(0\\)을 가중치로 바꾸면 가중 그래프를 표현할 수 있다.</li> </ul> <p>인접 행렬로 그래프를 표현하면 행렬 원소에 직접 접근하여 간선을 얻을 수 있다. 따라서 추가, 삭제, 조회, 수정 연산이 매우 효율적이며 시간 복잡도는 모두 \\(O(1)\\)이다. 하지만 행렬의 공간 복잡도는 \\(O(n^2)\\)이므로 메모리를 많이 차지한다.</p>","path":["제 9 장   그래프","9.1   그래프"],"tags":[]},{"location":"chapter_graph/graph/#2","level":3,"title":"2.   인접 리스트","text":"<p>인접 리스트(adjacency list)는 \\(n\\) 개의 연결 리스트로 그래프를 표현하며, 연결 리스트의 노드는 정점을 나타낸다. \\(i\\) 번째 연결 리스트는 정점 \\(i\\)에 대응하고, 해당 정점에 인접한 모든 정점(간선으로 연결된 정점)을 저장한다. 아래 그림(9-6)은 인접 리스트로 그래프를 저장한 예다.</p> <p></p> <p> 그림 9-6   그래프의 인접 리스트 표현 </p> <p>인접 리스트는 실제로 존재하는 간선만 저장한다. 전체 간선 수는 보통 \\(n^2\\)보다 훨씬 작으므로 공간을 더 절약할 수 있다. 하지만 간선을 찾으려면 연결 리스트를 순회해야 하므로 시간 효율은 인접 행렬보다 낮다.</p> <p>위 그림(9-6)을 보면 인접 리스트의 구조는 해시 테이블의 '체이닝'과 매우 비슷하므로, 비슷한 방법으로 효율을 높일 수 있다. 예를 들어 연결 리스트가 길면 AVL 트리나 레드-블랙 트리로 바꾸어 시간 복잡도를 \\(O(n)\\)에서 \\(O(\\log n)\\)으로 개선할 수 있다. 연결 리스트를 해시 테이블로 바꾸면 시간 복잡도를 \\(O(1)\\)로 줄일 수도 있다.</p>","path":["제 9 장   그래프","9.1   그래프"],"tags":[]},{"location":"chapter_graph/graph/#913","level":2,"title":"9.1.3   그래프의 주요 응용","text":"<p>아래 표(9-1)와 같이 현실의 많은 시스템을 그래프로 모델링할 수 있으며, 관련 문제를 그래프 계산 문제로 환원할 수 있다.</p> <p> 표 9-1   일상에서 볼 수 있는 그래프 </p> 정점 간선 그래프 계산 문제 소셜 네트워크 사용자 친구 관계 잠재적 친구 추천 지하철 노선 역 역 사이의 연결 관계 최단 경로 추천 태양계 천체 천체 사이의 만유인력 행성 궤도 계산","path":["제 9 장   그래프","9.1   그래프"],"tags":[]},{"location":"chapter_graph/graph_operations/","level":1,"title":"9.2   그래프의 기본 연산","text":"<p>그래프의 기본 연산은 '간선'에 대한 연산과 '정점'에 대한 연산으로 나뉜다. '인접 행렬'과 '인접 리스트' 중 어느 표현을 사용하는지에 따라 구현 방법이 달라진다.</p>","path":["제 9 장   그래프","9.2   그래프의 기본 연산"],"tags":[]},{"location":"chapter_graph/graph_operations/#921","level":2,"title":"9.2.1   인접 행렬 기반 구현","text":"<p>정점 수가 \\(n\\)인 무방향 그래프가 주어졌을 때 각 연산은 아래 그림(9-7)과 같이 구현한다.</p> <ul> <li>간선 추가 또는 삭제: 인접 행렬에서 해당 간선을 직접 수정하면 되며, \\(O(1)\\) 시간이 걸린다. 무방향 그래프이므로 두 방향의 간선을 함께 갱신해야 한다.</li> <li>정점 추가: 인접 행렬의 끝에 행과 열을 하나씩 추가하고 모두 \\(0\\)으로 채운다. \\(O(n)\\) 시간이 걸린다.</li> <li>정점 삭제: 인접 행렬에서 행과 열을 하나씩 삭제한다. 첫 번째 행과 열을 삭제할 때가 최악의 경우로, \\((n-1)^2\\) 개 원소를 '왼쪽 위로 이동'해야 하므로 \\(O(n^2)\\) 시간이 걸린다.</li> <li>초기화: 정점 \\(n\\) 개를 전달받아 길이가 \\(n\\)인 정점 리스트 <code>vertices</code>를 초기화하는 데 \\(O(n)\\) 시간이 걸린다. \\(n \\times n\\) 크기의 인접 행렬 <code>adjMat</code>을 초기화하는 데는 \\(O(n^2)\\) 시간이 걸린다.</li> </ul> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 9-7   인접 행렬의 초기화, 간선 추가와 삭제, 정점 추가와 삭제 </p> <p>다음은 인접 행렬로 그래프를 표현한 구현 코드다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_adjacency_matrix.py<pre><code>class GraphAdjMat:\n \"\"\"인접 행렬로 구현한 무방향 그래프 클래스\"\"\"\n\n def __init__(self, vertices: list[int], edges: list[list[int]]):\n \"\"\"생성자\"\"\"\n # 정점 리스트의 원소는 정점 값을, 인덱스는 정점 인덱스를 나타낸다\n self.vertices: list[int] = []\n # 인접 행렬의 행과 열 인덱스는 정점 인덱스에 해당한다\n self.adj_mat: list[list[int]] = []\n # 정점 추가\n for val in vertices:\n self.add_vertex(val)\n # 간선 추가\n # edges의 원소는 정점 인덱스, 즉 vertices 원소의 인덱스를 나타낸다\n for e in edges:\n self.add_edge(e[0], e[1])\n\n def size(self) -&gt; int:\n \"\"\"정점 개수 반환\"\"\"\n return len(self.vertices)\n\n def add_vertex(self, val: int):\n \"\"\"정점 추가\"\"\"\n n = self.size()\n # 정점 리스트에 새 정점의 값을 추가한다\n self.vertices.append(val)\n # 인접 행렬에 행 하나를 추가한다\n new_row = [0] * n\n self.adj_mat.append(new_row)\n # 인접 행렬에 열 하나를 추가한다\n for row in self.adj_mat:\n row.append(0)\n\n def remove_vertex(self, index: int):\n \"\"\"정점 삭제\"\"\"\n if index &gt;= self.size():\n raise IndexError()\n # 정점 리스트에서 인덱스 index의 정점을 제거한다\n self.vertices.pop(index)\n # 인접 행렬에서 인덱스 index의 행을 삭제한다\n self.adj_mat.pop(index)\n # 인접 행렬에서 인덱스 index의 열을 삭제한다\n for row in self.adj_mat:\n row.pop(index)\n\n def add_edge(self, i: int, j: int):\n \"\"\"간선 추가\"\"\"\n # 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\n # 인덱스가 범위를 벗어나거나 서로 같은 경우를 처리한다\n if i &lt; 0 or j &lt; 0 or i &gt;= self.size() or j &gt;= self.size() or i == j:\n raise IndexError()\n # 무방향 그래프의 인접 행렬은 주대각선에 대해 대칭이며, (i, j) == (j, i)를 만족한다\n self.adj_mat[i][j] = 1\n self.adj_mat[j][i] = 1\n\n def remove_edge(self, i: int, j: int):\n \"\"\"간선 삭제\"\"\"\n # 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\n # 인덱스가 범위를 벗어나거나 서로 같은 경우를 처리한다\n if i &lt; 0 or j &lt; 0 or i &gt;= self.size() or j &gt;= self.size() or i == j:\n raise IndexError()\n self.adj_mat[i][j] = 0\n self.adj_mat[j][i] = 0\n\n def print(self):\n \"\"\"인접 행렬 출력\"\"\"\n print(\"정점 리스트 =\", self.vertices)\n print(\"인접 행렬 =\")\n print_matrix(self.adj_mat)\n</code></pre> graph_adjacency_matrix.cpp<pre><code>/* 인접 행렬로 구현한 무방향 그래프 클래스 */\nclass GraphAdjMat {\n vector&lt;int&gt; vertices; // 정점 리스트의 원소는 정점 값을, 인덱스는 정점 인덱스를 나타낸다\n vector&lt;vector&lt;int&gt;&gt; adjMat; // 인접 행렬의 행과 열 인덱스는 정점 인덱스에 해당한다\n\n public:\n /* 생성자 */\n GraphAdjMat(const vector&lt;int&gt; &amp;vertices, const vector&lt;vector&lt;int&gt;&gt; &amp;edges) {\n // 정점 추가\n for (int val : vertices) {\n addVertex(val);\n }\n // 간선 추가\n // edges의 원소는 정점 인덱스, 즉 vertices 원소의 인덱스를 나타낸다\n for (const vector&lt;int&gt; &amp;edge : edges) {\n addEdge(edge[0], edge[1]);\n }\n }\n\n /* 정점 개수 반환 */\n int size() const {\n return vertices.size();\n }\n\n /* 정점 추가 */\n void addVertex(int val) {\n int n = size();\n // 정점 리스트에 새 정점의 값을 추가한다\n vertices.push_back(val);\n // 인접 행렬에 행 하나를 추가한다\n adjMat.emplace_back(vector&lt;int&gt;(n, 0));\n // 인접 행렬에 열 하나를 추가한다\n for (vector&lt;int&gt; &amp;row : adjMat) {\n row.push_back(0);\n }\n }\n\n /* 정점 삭제 */\n void removeVertex(int index) {\n if (index &gt;= size()) {\n throw out_of_range(\"정점이 존재하지 않는다\");\n }\n // 정점 리스트에서 인덱스 index의 정점을 제거한다\n vertices.erase(vertices.begin() + index);\n // 인접 행렬에서 인덱스 index의 행을 삭제한다\n adjMat.erase(adjMat.begin() + index);\n // 인접 행렬에서 인덱스 index의 열을 삭제한다\n for (vector&lt;int&gt; &amp;row : adjMat) {\n row.erase(row.begin() + index);\n }\n }\n\n /* 간선 추가 */\n // 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\n void addEdge(int i, int j) {\n // 인덱스가 범위를 벗어나거나 서로 같은 경우를 처리한다\n if (i &lt; 0 || j &lt; 0 || i &gt;= size() || j &gt;= size() || i == j) {\n throw out_of_range(\"정점이 존재하지 않는다\");\n }\n // 무방향 그래프의 인접 행렬은 주대각선에 대해 대칭이며, (i, j) == (j, i)를 만족한다\n adjMat[i][j] = 1;\n adjMat[j][i] = 1;\n }\n\n /* 간선 삭제 */\n // 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\n void removeEdge(int i, int j) {\n // 인덱스가 범위를 벗어나거나 서로 같은 경우를 처리한다\n if (i &lt; 0 || j &lt; 0 || i &gt;= size() || j &gt;= size() || i == j) {\n throw out_of_range(\"정점이 존재하지 않는다\");\n }\n adjMat[i][j] = 0;\n adjMat[j][i] = 0;\n }\n\n /* 인접 행렬 출력 */\n void print() {\n cout &lt;&lt; \"정점 리스트 = \";\n printVector(vertices);\n cout &lt;&lt; \"인접 행렬 =\" &lt;&lt; endl;\n printVectorMatrix(adjMat);\n }\n};\n</code></pre> graph_adjacency_matrix.java<pre><code>/* 인접 행렬로 구현한 무방향 그래프 클래스 */\nclass GraphAdjMat {\n List&lt;Integer&gt; vertices; // 정점 리스트의 원소는 정점 값을, 인덱스는 정점 인덱스를 나타낸다\n List&lt;List&lt;Integer&gt;&gt; adjMat; // 인접 행렬의 행과 열 인덱스는 정점 인덱스에 해당한다\n\n /* 생성자 */\n public GraphAdjMat(int[] vertices, int[][] edges) {\n this.vertices = new ArrayList&lt;&gt;();\n this.adjMat = new ArrayList&lt;&gt;();\n // 정점 추가\n for (int val : vertices) {\n addVertex(val);\n }\n // 간선 추가\n // edges의 원소는 정점 인덱스, 즉 vertices 원소의 인덱스를 나타낸다\n for (int[] e : edges) {\n addEdge(e[0], e[1]);\n }\n }\n\n /* 정점 개수 반환 */\n public int size() {\n return vertices.size();\n }\n\n /* 정점 추가 */\n public void addVertex(int val) {\n int n = size();\n // 정점 리스트에 새 정점의 값을 추가한다\n vertices.add(val);\n // 인접 행렬에 행 하나를 추가한다\n List&lt;Integer&gt; newRow = new ArrayList&lt;&gt;(n);\n for (int j = 0; j &lt; n; j++) {\n newRow.add(0);\n }\n adjMat.add(newRow);\n // 인접 행렬에 열 하나를 추가한다\n for (List&lt;Integer&gt; row : adjMat) {\n row.add(0);\n }\n }\n\n /* 정점 삭제 */\n public void removeVertex(int index) {\n if (index &gt;= size())\n throw new IndexOutOfBoundsException();\n // 정점 리스트에서 인덱스 index의 정점을 제거한다\n vertices.remove(index);\n // 인접 행렬에서 인덱스 index의 행을 삭제한다\n adjMat.remove(index);\n // 인접 행렬에서 인덱스 index의 열을 삭제한다\n for (List&lt;Integer&gt; row : adjMat) {\n row.remove(index);\n }\n }\n\n /* 간선 추가 */\n // 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\n public void addEdge(int i, int j) {\n // 인덱스가 범위를 벗어나거나 서로 같은 경우를 처리한다\n if (i &lt; 0 || j &lt; 0 || i &gt;= size() || j &gt;= size() || i == j)\n throw new IndexOutOfBoundsException();\n // 무방향 그래프의 인접 행렬은 주대각선에 대해 대칭이며, (i, j) == (j, i)를 만족한다\n adjMat.get(i).set(j, 1);\n adjMat.get(j).set(i, 1);\n }\n\n /* 간선 삭제 */\n // 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\n public void removeEdge(int i, int j) {\n // 인덱스가 범위를 벗어나거나 서로 같은 경우를 처리한다\n if (i &lt; 0 || j &lt; 0 || i &gt;= size() || j &gt;= size() || i == j)\n throw new IndexOutOfBoundsException();\n adjMat.get(i).set(j, 0);\n adjMat.get(j).set(i, 0);\n }\n\n /* 인접 행렬 출력 */\n public void print() {\n System.out.print(\"정점 리스트 = \");\n System.out.println(vertices);\n System.out.println(\"인접 행렬 =\");\n PrintUtil.printMatrix(adjMat);\n }\n}\n</code></pre> graph_adjacency_matrix.cs<pre><code>/* 인접 행렬로 구현한 무방향 그래프 클래스 */\nclass GraphAdjMat {\n List&lt;int&gt; vertices; // 정점 리스트의 원소는 정점 값을, 인덱스는 정점 인덱스를 나타낸다\n List&lt;List&lt;int&gt;&gt; adjMat; // 인접 행렬의 행과 열 인덱스는 정점 인덱스에 해당한다\n\n /* 생성자 */\n public GraphAdjMat(int[] vertices, int[][] edges) {\n this.vertices = [];\n this.adjMat = [];\n // 정점 추가\n foreach (int val in vertices) {\n AddVertex(val);\n }\n // 간선 추가\n // edges의 원소는 정점 인덱스, 즉 vertices 원소의 인덱스를 나타낸다\n foreach (int[] e in edges) {\n AddEdge(e[0], e[1]);\n }\n }\n\n /* 정점 개수 반환 */\n int Size() {\n return vertices.Count;\n }\n\n /* 정점 추가 */\n public void AddVertex(int val) {\n int n = Size();\n // 정점 리스트에 새 정점의 값을 추가한다\n vertices.Add(val);\n // 인접 행렬에 행 하나를 추가한다\n List&lt;int&gt; newRow = new(n);\n for (int j = 0; j &lt; n; j++) {\n newRow.Add(0);\n }\n adjMat.Add(newRow);\n // 인접 행렬에 열 하나를 추가한다\n foreach (List&lt;int&gt; row in adjMat) {\n row.Add(0);\n }\n }\n\n /* 정점 삭제 */\n public void RemoveVertex(int index) {\n if (index &gt;= Size())\n throw new IndexOutOfRangeException();\n // 정점 리스트에서 인덱스 index의 정점을 제거한다\n vertices.RemoveAt(index);\n // 인접 행렬에서 인덱스 index의 행을 삭제한다\n adjMat.RemoveAt(index);\n // 인접 행렬에서 인덱스 index의 열을 삭제한다\n foreach (List&lt;int&gt; row in adjMat) {\n row.RemoveAt(index);\n }\n }\n\n /* 간선 추가 */\n // 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\n public void AddEdge(int i, int j) {\n // 인덱스가 범위를 벗어나거나 서로 같은 경우를 처리한다\n if (i &lt; 0 || j &lt; 0 || i &gt;= Size() || j &gt;= Size() || i == j)\n throw new IndexOutOfRangeException();\n // 무방향 그래프의 인접 행렬은 주대각선에 대해 대칭이며, (i, j) == (j, i)를 만족한다\n adjMat[i][j] = 1;\n adjMat[j][i] = 1;\n }\n\n /* 간선 삭제 */\n // 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\n public void RemoveEdge(int i, int j) {\n // 인덱스가 범위를 벗어나거나 서로 같은 경우를 처리한다\n if (i &lt; 0 || j &lt; 0 || i &gt;= Size() || j &gt;= Size() || i == j)\n throw new IndexOutOfRangeException();\n adjMat[i][j] = 0;\n adjMat[j][i] = 0;\n }\n\n /* 인접 행렬 출력 */\n public void Print() {\n Console.Write(\"정점 리스트 = \");\n PrintUtil.PrintList(vertices);\n Console.WriteLine(\"인접 행렬 =\");\n PrintUtil.PrintMatrix(adjMat);\n }\n}\n</code></pre> graph_adjacency_matrix.go<pre><code>/* 인접 행렬로 구현한 무방향 그래프 클래스 */\ntype graphAdjMat struct {\n // 정점 리스트의 원소는 정점 값을, 인덱스는 정점 인덱스를 나타낸다\n vertices []int\n // 인접 행렬의 행과 열 인덱스는 정점 인덱스에 해당한다\n adjMat [][]int\n}\n\n/* 생성자 */\nfunc newGraphAdjMat(vertices []int, edges [][]int) *graphAdjMat {\n // 정점 추가\n n := len(vertices)\n adjMat := make([][]int, n)\n for i := range adjMat {\n adjMat[i] = make([]int, n)\n }\n // 그래프 초기화\n g := &amp;graphAdjMat{\n vertices: vertices,\n adjMat: adjMat,\n }\n // 간선 추가\n // edges의 원소는 정점 인덱스, 즉 vertices 원소의 인덱스를 나타낸다\n for i := range edges {\n g.addEdge(edges[i][0], edges[i][1])\n }\n return g\n}\n\n/* 정점 개수 반환 */\nfunc (g *graphAdjMat) size() int {\n return len(g.vertices)\n}\n\n/* 정점 추가 */\nfunc (g *graphAdjMat) addVertex(val int) {\n n := g.size()\n // 정점 리스트에 새 정점의 값을 추가한다\n g.vertices = append(g.vertices, val)\n // 인접 행렬에 행 하나를 추가한다\n newRow := make([]int, n)\n g.adjMat = append(g.adjMat, newRow)\n // 인접 행렬에 열 하나를 추가한다\n for i := range g.adjMat {\n g.adjMat[i] = append(g.adjMat[i], 0)\n }\n}\n\n/* 정점 삭제 */\nfunc (g *graphAdjMat) removeVertex(index int) {\n if index &gt;= g.size() {\n return\n }\n // 정점 리스트에서 인덱스 index의 정점을 제거한다\n g.vertices = append(g.vertices[:index], g.vertices[index+1:]...)\n // 인접 행렬에서 인덱스 index의 행을 삭제한다\n g.adjMat = append(g.adjMat[:index], g.adjMat[index+1:]...)\n // 인접 행렬에서 인덱스 index의 열을 삭제한다\n for i := range g.adjMat {\n g.adjMat[i] = append(g.adjMat[i][:index], g.adjMat[i][index+1:]...)\n }\n}\n\n/* 간선 추가 */\n// 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\nfunc (g *graphAdjMat) addEdge(i, j int) {\n // 인덱스가 범위를 벗어나거나 서로 같은 경우를 처리한다\n if i &lt; 0 || j &lt; 0 || i &gt;= g.size() || j &gt;= g.size() || i == j {\n fmt.Errorf(\"%s\", \"인덱스가 범위를 벗어났다\")\n }\n // 무방향 그래프의 인접 행렬은 주대각선에 대해 대칭이며, (i, j) == (j, i)를 만족한다\n g.adjMat[i][j] = 1\n g.adjMat[j][i] = 1\n}\n\n/* 간선 삭제 */\n// 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\nfunc (g *graphAdjMat) removeEdge(i, j int) {\n // 인덱스가 범위를 벗어나거나 서로 같은 경우를 처리한다\n if i &lt; 0 || j &lt; 0 || i &gt;= g.size() || j &gt;= g.size() || i == j {\n fmt.Errorf(\"%s\", \"인덱스가 범위를 벗어났다\")\n }\n g.adjMat[i][j] = 0\n g.adjMat[j][i] = 0\n}\n\n/* 인접 행렬 출력 */\nfunc (g *graphAdjMat) print() {\n fmt.Printf(\"\\t정점 리스트 = %v\\n\", g.vertices)\n fmt.Printf(\"\\t인접 행렬 = \\n\")\n for i := range g.adjMat {\n fmt.Printf(\"\\t\\t\\t%v\\n\", g.adjMat[i])\n }\n}\n</code></pre> graph_adjacency_matrix.swift<pre><code>/* 인접 행렬로 구현한 무방향 그래프 클래스 */\nclass GraphAdjMat {\n private var vertices: [Int] // 정점 리스트의 원소는 정점 값을, 인덱스는 정점 인덱스를 나타낸다\n private var adjMat: [[Int]] // 인접 행렬의 행과 열 인덱스는 정점 인덱스에 해당한다\n\n /* 생성자 */\n init(vertices: [Int], edges: [[Int]]) {\n self.vertices = []\n adjMat = []\n // 정점 추가\n for val in vertices {\n addVertex(val: val)\n }\n // 간선 추가\n // edges의 원소는 정점 인덱스, 즉 vertices 원소의 인덱스를 나타낸다\n for e in edges {\n addEdge(i: e[0], j: e[1])\n }\n }\n\n /* 정점 개수 반환 */\n func size() -&gt; Int {\n vertices.count\n }\n\n /* 정점 추가 */\n func addVertex(val: Int) {\n let n = size()\n // 정점 리스트에 새 정점의 값을 추가한다\n vertices.append(val)\n // 인접 행렬에 행 하나를 추가한다\n let newRow = Array(repeating: 0, count: n)\n adjMat.append(newRow)\n // 인접 행렬에 열 하나를 추가한다\n for i in adjMat.indices {\n adjMat[i].append(0)\n }\n }\n\n /* 정점 삭제 */\n func removeVertex(index: Int) {\n if index &gt;= size() {\n fatalError(\"범위를 벗어남\")\n }\n // 정점 리스트에서 인덱스 index의 정점을 제거한다\n vertices.remove(at: index)\n // 인접 행렬에서 인덱스 index의 행을 삭제한다\n adjMat.remove(at: index)\n // 인접 행렬에서 인덱스 index의 열을 삭제한다\n for i in adjMat.indices {\n adjMat[i].remove(at: index)\n }\n }\n\n /* 간선 추가 */\n // 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\n func addEdge(i: Int, j: Int) {\n // 인덱스가 범위를 벗어나거나 서로 같은 경우를 처리한다\n if i &lt; 0 || j &lt; 0 || i &gt;= size() || j &gt;= size() || i == j {\n fatalError(\"범위를 벗어남\")\n }\n // 무방향 그래프의 인접 행렬은 주대각선에 대해 대칭이며, (i, j) == (j, i)를 만족한다\n adjMat[i][j] = 1\n adjMat[j][i] = 1\n }\n\n /* 간선 삭제 */\n // 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\n func removeEdge(i: Int, j: Int) {\n // 인덱스가 범위를 벗어나거나 서로 같은 경우를 처리한다\n if i &lt; 0 || j &lt; 0 || i &gt;= size() || j &gt;= size() || i == j {\n fatalError(\"범위를 벗어남\")\n }\n adjMat[i][j] = 0\n adjMat[j][i] = 0\n }\n\n /* 인접 행렬 출력 */\n func print() {\n Swift.print(\"정점 리스트 = \", terminator: \"\")\n Swift.print(vertices)\n Swift.print(\"인접 행렬 =\")\n PrintUtil.printMatrix(matrix: adjMat)\n }\n}\n</code></pre> graph_adjacency_matrix.js<pre><code>/* 인접 행렬로 구현한 무방향 그래프 클래스 */\nclass GraphAdjMat {\n vertices; // 정점 리스트의 원소는 정점 값을, 인덱스는 정점 인덱스를 나타낸다\n adjMat; // 인접 행렬의 행과 열 인덱스는 정점 인덱스에 해당한다\n\n /* 생성자 */\n constructor(vertices, edges) {\n this.vertices = [];\n this.adjMat = [];\n // 정점 추가\n for (const val of vertices) {\n this.addVertex(val);\n }\n // 간선 추가\n // edges의 원소는 정점 인덱스, 즉 vertices 원소의 인덱스를 나타낸다\n for (const e of edges) {\n this.addEdge(e[0], e[1]);\n }\n }\n\n /* 정점 개수 반환 */\n size() {\n return this.vertices.length;\n }\n\n /* 정점 추가 */\n addVertex(val) {\n const n = this.size();\n // 정점 리스트에 새 정점의 값을 추가한다\n this.vertices.push(val);\n // 인접 행렬에 행 하나를 추가한다\n const newRow = [];\n for (let j = 0; j &lt; n; j++) {\n newRow.push(0);\n }\n this.adjMat.push(newRow);\n // 인접 행렬에 열 하나를 추가한다\n for (const row of this.adjMat) {\n row.push(0);\n }\n }\n\n /* 정점 삭제 */\n removeVertex(index) {\n if (index &gt;= this.size()) {\n throw new RangeError('인덱스가 범위를 벗어났다');\n }\n // 정점 리스트에서 인덱스 index의 정점을 제거한다\n this.vertices.splice(index, 1);\n\n // 인접 행렬에서 인덱스 index의 행을 삭제한다\n this.adjMat.splice(index, 1);\n // 인접 행렬에서 인덱스 index의 열을 삭제한다\n for (const row of this.adjMat) {\n row.splice(index, 1);\n }\n }\n\n /* 간선 추가 */\n // 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\n addEdge(i, j) {\n // 인덱스가 범위를 벗어나거나 서로 같은 경우를 처리한다\n if (i &lt; 0 || j &lt; 0 || i &gt;= this.size() || j &gt;= this.size() || i === j) {\n throw new RangeError('인덱스가 범위를 벗어났다');\n }\n // 무방향 그래프의 인접 행렬은 주대각선을 기준으로 대칭이므로 (i, j) === (j, i)를 만족한다\n this.adjMat[i][j] = 1;\n this.adjMat[j][i] = 1;\n }\n\n /* 간선 삭제 */\n // 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\n removeEdge(i, j) {\n // 인덱스가 범위를 벗어나거나 서로 같은 경우를 처리한다\n if (i &lt; 0 || j &lt; 0 || i &gt;= this.size() || j &gt;= this.size() || i === j) {\n throw new RangeError('인덱스가 범위를 벗어났다');\n }\n this.adjMat[i][j] = 0;\n this.adjMat[j][i] = 0;\n }\n\n /* 인접 행렬 출력 */\n print() {\n console.log('정점 리스트 = ', this.vertices);\n console.log('인접 행렬 =', this.adjMat);\n }\n}\n</code></pre> graph_adjacency_matrix.ts<pre><code>/* 인접 행렬로 구현한 무방향 그래프 클래스 */\nclass GraphAdjMat {\n vertices: number[]; // 정점 리스트의 원소는 정점 값을, 인덱스는 정점 인덱스를 나타낸다\n adjMat: number[][]; // 인접 행렬의 행과 열 인덱스는 정점 인덱스에 해당한다\n\n /* 생성자 */\n constructor(vertices: number[], edges: number[][]) {\n this.vertices = [];\n this.adjMat = [];\n // 정점 추가\n for (const val of vertices) {\n this.addVertex(val);\n }\n // 간선 추가\n // edges의 원소는 정점 인덱스, 즉 vertices 원소의 인덱스를 나타낸다\n for (const e of edges) {\n this.addEdge(e[0], e[1]);\n }\n }\n\n /* 정점 개수 반환 */\n size(): number {\n return this.vertices.length;\n }\n\n /* 정점 추가 */\n addVertex(val: number): void {\n const n: number = this.size();\n // 정점 리스트에 새 정점의 값을 추가한다\n this.vertices.push(val);\n // 인접 행렬에 행 하나를 추가한다\n const newRow: number[] = [];\n for (let j: number = 0; j &lt; n; j++) {\n newRow.push(0);\n }\n this.adjMat.push(newRow);\n // 인접 행렬에 열 하나를 추가한다\n for (const row of this.adjMat) {\n row.push(0);\n }\n }\n\n /* 정점 삭제 */\n removeVertex(index: number): void {\n if (index &gt;= this.size()) {\n throw new RangeError('인덱스가 범위를 벗어났다');\n }\n // 정점 리스트에서 인덱스 index의 정점을 제거한다\n this.vertices.splice(index, 1);\n\n // 인접 행렬에서 인덱스 index의 행을 삭제한다\n this.adjMat.splice(index, 1);\n // 인접 행렬에서 인덱스 index의 열을 삭제한다\n for (const row of this.adjMat) {\n row.splice(index, 1);\n }\n }\n\n /* 간선 추가 */\n // 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\n addEdge(i: number, j: number): void {\n // 인덱스가 범위를 벗어나거나 서로 같은 경우를 처리한다\n if (i &lt; 0 || j &lt; 0 || i &gt;= this.size() || j &gt;= this.size() || i === j) {\n throw new RangeError('인덱스가 범위를 벗어났다');\n }\n // 무방향 그래프의 인접 행렬은 주대각선을 기준으로 대칭이므로 (i, j) === (j, i)를 만족한다\n this.adjMat[i][j] = 1;\n this.adjMat[j][i] = 1;\n }\n\n /* 간선 삭제 */\n // 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\n removeEdge(i: number, j: number): void {\n // 인덱스가 범위를 벗어나거나 서로 같은 경우를 처리한다\n if (i &lt; 0 || j &lt; 0 || i &gt;= this.size() || j &gt;= this.size() || i === j) {\n throw new RangeError('인덱스가 범위를 벗어났다');\n }\n this.adjMat[i][j] = 0;\n this.adjMat[j][i] = 0;\n }\n\n /* 인접 행렬 출력 */\n print(): void {\n console.log('정점 리스트 = ', this.vertices);\n console.log('인접 행렬 =', this.adjMat);\n }\n}\n</code></pre> graph_adjacency_matrix.dart<pre><code>/* 인접 행렬로 구현한 무방향 그래프 클래스 */\nclass GraphAdjMat {\n List&lt;int&gt; vertices = []; // 정점 원소는 정점 값을, 인덱스는 정점 인덱스를 나타낸다\n List&lt;List&lt;int&gt;&gt; adjMat = []; //인접 행렬의 행과 열 인덱스는 정점 인덱스에 해당한다\n\n /* 생성자 */\n GraphAdjMat(List&lt;int&gt; vertices, List&lt;List&lt;int&gt;&gt; edges) {\n this.vertices = [];\n this.adjMat = [];\n // 정점 추가\n for (int val in vertices) {\n addVertex(val);\n }\n // 간선 추가\n // edges의 원소는 정점 인덱스, 즉 vertices 원소의 인덱스를 나타낸다\n for (List&lt;int&gt; e in edges) {\n addEdge(e[0], e[1]);\n }\n }\n\n /* 정점 개수 반환 */\n int size() {\n return vertices.length;\n }\n\n /* 정점 추가 */\n void addVertex(int val) {\n int n = size();\n // 정점 리스트에 새 정점의 값을 추가한다\n vertices.add(val);\n // 인접 행렬에 행 하나를 추가한다\n List&lt;int&gt; newRow = List.filled(n, 0, growable: true);\n adjMat.add(newRow);\n // 인접 행렬에 열 하나를 추가한다\n for (List&lt;int&gt; row in adjMat) {\n row.add(0);\n }\n }\n\n /* 정점 삭제 */\n void removeVertex(int index) {\n if (index &gt;= size()) {\n throw IndexError;\n }\n // 정점 리스트에서 인덱스 index의 정점을 제거한다\n vertices.removeAt(index);\n // 인접 행렬에서 인덱스 index의 행을 삭제한다\n adjMat.removeAt(index);\n // 인접 행렬에서 인덱스 index의 열을 삭제한다\n for (List&lt;int&gt; row in adjMat) {\n row.removeAt(index);\n }\n }\n\n /* 간선 추가 */\n // 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\n void addEdge(int i, int j) {\n // 인덱스가 범위를 벗어나거나 서로 같은 경우를 처리한다\n if (i &lt; 0 || j &lt; 0 || i &gt;= size() || j &gt;= size() || i == j) {\n throw IndexError;\n }\n // 무방향 그래프의 인접 행렬은 주대각선에 대해 대칭이며, (i, j) == (j, i)를 만족한다\n adjMat[i][j] = 1;\n adjMat[j][i] = 1;\n }\n\n /* 간선 삭제 */\n // 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\n void removeEdge(int i, int j) {\n // 인덱스가 범위를 벗어나거나 서로 같은 경우를 처리한다\n if (i &lt; 0 || j &lt; 0 || i &gt;= size() || j &gt;= size() || i == j) {\n throw IndexError;\n }\n adjMat[i][j] = 0;\n adjMat[j][i] = 0;\n }\n\n /* 인접 행렬 출력 */\n void printAdjMat() {\n print(\"정점 리스트 = $vertices\");\n print(\"인접 행렬 = \");\n printMatrix(adjMat);\n }\n}\n</code></pre> graph_adjacency_matrix.rs<pre><code>/* 인접 행렬로 구현한 무방향 그래프 자료형 */\npub struct GraphAdjMat {\n // 정점 리스트의 원소는 정점 값을, 인덱스는 정점 인덱스를 나타낸다\n pub vertices: Vec&lt;i32&gt;,\n // 인접 행렬의 행과 열 인덱스는 정점 인덱스에 해당한다\n pub adj_mat: Vec&lt;Vec&lt;i32&gt;&gt;,\n}\n\nimpl GraphAdjMat {\n /* 생성자 */\n pub fn new(vertices: Vec&lt;i32&gt;, edges: Vec&lt;[usize; 2]&gt;) -&gt; Self {\n let mut graph = GraphAdjMat {\n vertices: vec![],\n adj_mat: vec![],\n };\n // 정점 추가\n for val in vertices {\n graph.add_vertex(val);\n }\n // 간선 추가\n // edges의 원소는 정점 인덱스, 즉 vertices 원소의 인덱스를 나타낸다\n for edge in edges {\n graph.add_edge(edge[0], edge[1])\n }\n\n graph\n }\n\n /* 정점 개수 반환 */\n pub fn size(&amp;self) -&gt; usize {\n self.vertices.len()\n }\n\n /* 정점 추가 */\n pub fn add_vertex(&amp;mut self, val: i32) {\n let n = self.size();\n // 정점 리스트에 새 정점의 값을 추가한다\n self.vertices.push(val);\n // 인접 행렬에 행 하나를 추가한다\n self.adj_mat.push(vec![0; n]);\n // 인접 행렬에 열 하나를 추가한다\n for row in self.adj_mat.iter_mut() {\n row.push(0);\n }\n }\n\n /* 정점 삭제 */\n pub fn remove_vertex(&amp;mut self, index: usize) {\n if index &gt;= self.size() {\n panic!(\"인덱스 오류\")\n }\n // 정점 리스트에서 인덱스 index의 정점을 제거한다\n self.vertices.remove(index);\n // 인접 행렬에서 인덱스 index의 행을 삭제한다\n self.adj_mat.remove(index);\n // 인접 행렬에서 인덱스 index의 열을 삭제한다\n for row in self.adj_mat.iter_mut() {\n row.remove(index);\n }\n }\n\n /* 간선 추가 */\n pub fn add_edge(&amp;mut self, i: usize, j: usize) {\n // 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\n // 인덱스가 범위를 벗어나거나 서로 같은 경우를 처리한다\n if i &gt;= self.size() || j &gt;= self.size() || i == j {\n panic!(\"인덱스 오류\")\n }\n // 무방향 그래프의 인접 행렬은 주대각선에 대해 대칭이며, (i, j) == (j, i)를 만족한다\n self.adj_mat[i][j] = 1;\n self.adj_mat[j][i] = 1;\n }\n\n /* 간선 삭제 */\n // 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\n pub fn remove_edge(&amp;mut self, i: usize, j: usize) {\n // 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\n // 인덱스가 범위를 벗어나거나 서로 같은 경우를 처리한다\n if i &gt;= self.size() || j &gt;= self.size() || i == j {\n panic!(\"인덱스 오류\")\n }\n self.adj_mat[i][j] = 0;\n self.adj_mat[j][i] = 0;\n }\n\n /* 인접 행렬 출력 */\n pub fn print(&amp;self) {\n println!(\"정점 리스트 = {:?}\", self.vertices);\n println!(\"인접 행렬 =\");\n println!(\"[\");\n for row in &amp;self.adj_mat {\n println!(\" {:?},\", row);\n }\n println!(\"]\")\n }\n}\n</code></pre> graph_adjacency_matrix.c<pre><code>/* 인접 행렬로 구현한 무방향 그래프 구조체 */\ntypedef struct {\n int vertices[MAX_SIZE];\n int adjMat[MAX_SIZE][MAX_SIZE];\n int size;\n} GraphAdjMat;\n\n/* 생성자 */\nGraphAdjMat *newGraphAdjMat() {\n GraphAdjMat *graph = (GraphAdjMat *)malloc(sizeof(GraphAdjMat));\n graph-&gt;size = 0;\n for (int i = 0; i &lt; MAX_SIZE; i++) {\n for (int j = 0; j &lt; MAX_SIZE; j++) {\n graph-&gt;adjMat[i][j] = 0;\n }\n }\n return graph;\n}\n\n/* 소멸자 */\nvoid delGraphAdjMat(GraphAdjMat *graph) {\n free(graph);\n}\n\n/* 정점 추가 */\nvoid addVertex(GraphAdjMat *graph, int val) {\n if (graph-&gt;size == MAX_SIZE) {\n fprintf(stderr, \"그래프의 정점 개수가 최댓값에 도달했다\\n\");\n return;\n }\n // n번째 정점을 추가하고 n번째 행과 열을 0으로 설정한다\n int n = graph-&gt;size;\n graph-&gt;vertices[n] = val;\n for (int i = 0; i &lt;= n; i++) {\n graph-&gt;adjMat[n][i] = graph-&gt;adjMat[i][n] = 0;\n }\n graph-&gt;size++;\n}\n\n/* 정점 삭제 */\nvoid removeVertex(GraphAdjMat *graph, int index) {\n if (index &lt; 0 || index &gt;= graph-&gt;size) {\n fprintf(stderr, \"정점 인덱스가 범위를 벗어났다\\n\");\n return;\n }\n // 정점 리스트에서 인덱스 index의 정점을 제거한다\n for (int i = index; i &lt; graph-&gt;size - 1; i++) {\n graph-&gt;vertices[i] = graph-&gt;vertices[i + 1];\n }\n // 인접 행렬에서 인덱스 index의 행을 삭제한다\n for (int i = index; i &lt; graph-&gt;size - 1; i++) {\n for (int j = 0; j &lt; graph-&gt;size; j++) {\n graph-&gt;adjMat[i][j] = graph-&gt;adjMat[i + 1][j];\n }\n }\n // 인접 행렬에서 인덱스 index의 열을 삭제한다\n for (int i = 0; i &lt; graph-&gt;size; i++) {\n for (int j = index; j &lt; graph-&gt;size - 1; j++) {\n graph-&gt;adjMat[i][j] = graph-&gt;adjMat[i][j + 1];\n }\n }\n graph-&gt;size--;\n}\n\n/* 간선 추가 */\n// 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\nvoid addEdge(GraphAdjMat *graph, int i, int j) {\n if (i &lt; 0 || j &lt; 0 || i &gt;= graph-&gt;size || j &gt;= graph-&gt;size || i == j) {\n fprintf(stderr, \"간선 인덱스가 범위를 벗어나거나 서로 같다\\n\");\n return;\n }\n graph-&gt;adjMat[i][j] = 1;\n graph-&gt;adjMat[j][i] = 1;\n}\n\n/* 간선 삭제 */\n// 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\nvoid removeEdge(GraphAdjMat *graph, int i, int j) {\n if (i &lt; 0 || j &lt; 0 || i &gt;= graph-&gt;size || j &gt;= graph-&gt;size || i == j) {\n fprintf(stderr, \"간선 인덱스가 범위를 벗어나거나 서로 같다\\n\");\n return;\n }\n graph-&gt;adjMat[i][j] = 0;\n graph-&gt;adjMat[j][i] = 0;\n}\n\n/* 인접 행렬 출력 */\nvoid printGraphAdjMat(GraphAdjMat *graph) {\n printf(\"정점 리스트 = \");\n printArray(graph-&gt;vertices, graph-&gt;size);\n printf(\"인접 행렬 =\\n\");\n for (int i = 0; i &lt; graph-&gt;size; i++) {\n printArray(graph-&gt;adjMat[i], graph-&gt;size);\n }\n}\n</code></pre> graph_adjacency_matrix.kt<pre><code>/* 인접 행렬로 구현한 무방향 그래프 클래스 */\nclass GraphAdjMat(vertices: IntArray, edges: Array&lt;IntArray&gt;) {\n val vertices = mutableListOf&lt;Int&gt;() // 정점 리스트의 원소는 정점 값을, 인덱스는 정점 인덱스를 나타낸다\n val adjMat = mutableListOf&lt;MutableList&lt;Int&gt;&gt;() // 인접 행렬의 행과 열 인덱스는 정점 인덱스에 해당한다\n\n /* 생성자 */\n init {\n // 정점 추가\n for (vertex in vertices) {\n addVertex(vertex)\n }\n // 간선 추가\n // edges의 원소는 정점 인덱스, 즉 vertices 원소의 인덱스를 나타낸다\n for (edge in edges) {\n addEdge(edge[0], edge[1])\n }\n }\n\n /* 정점 개수 반환 */\n fun size(): Int {\n return vertices.size\n }\n\n /* 정점 추가 */\n fun addVertex(_val: Int) {\n val n = size()\n // 정점 리스트에 새 정점의 값을 추가한다\n vertices.add(_val)\n // 인접 행렬에 행 하나를 추가한다\n val newRow = mutableListOf&lt;Int&gt;()\n for (j in 0..&lt;n) {\n newRow.add(0)\n }\n adjMat.add(newRow)\n // 인접 행렬에 열 하나를 추가한다\n for (row in adjMat) {\n row.add(0)\n }\n }\n\n /* 정점 삭제 */\n fun removeVertex(index: Int) {\n if (index &gt;= size())\n throw IndexOutOfBoundsException()\n // 정점 리스트에서 인덱스 index의 정점을 제거한다\n vertices.removeAt(index)\n // 인접 행렬에서 인덱스 index의 행을 삭제한다\n adjMat.removeAt(index)\n // 인접 행렬에서 인덱스 index의 열을 삭제한다\n for (row in adjMat) {\n row.removeAt(index)\n }\n }\n\n /* 간선 추가 */\n // 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\n fun addEdge(i: Int, j: Int) {\n // 인덱스가 범위를 벗어나거나 서로 같은 경우를 처리한다\n if (i &lt; 0 || j &lt; 0 || i &gt;= size() || j &gt;= size() || i == j)\n throw IndexOutOfBoundsException()\n // 무방향 그래프의 인접 행렬은 주대각선에 대해 대칭이며, (i, j) == (j, i)를 만족한다\n adjMat[i][j] = 1\n adjMat[j][i] = 1\n }\n\n /* 간선 삭제 */\n // 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\n fun removeEdge(i: Int, j: Int) {\n // 인덱스가 범위를 벗어나거나 서로 같은 경우를 처리한다\n if (i &lt; 0 || j &lt; 0 || i &gt;= size() || j &gt;= size() || i == j)\n throw IndexOutOfBoundsException()\n adjMat[i][j] = 0\n adjMat[j][i] = 0\n }\n\n /* 인접 행렬 출력 */\n fun print() {\n print(\"정점 리스트 = \")\n println(vertices)\n println(\"인접 행렬 =\")\n printMatrix(adjMat)\n }\n}\n</code></pre> graph_adjacency_matrix.rb<pre><code>### 인접 행렬로 구현한 무방향 그래프 클래스 ###\nclass GraphAdjMat\n def initialize(vertices, edges)\n ### 생성자 ###\n # 정점 리스트의 원소는 정점 값을, 인덱스는 정점 인덱스를 나타낸다\n @vertices = []\n # 인접 행렬의 행과 열 인덱스는 정점 인덱스에 해당한다\n @adj_mat = []\n # 정점 추가\n vertices.each { |val| add_vertex(val) }\n # 간선 추가\n # edges의 원소는 정점 인덱스, 즉 vertices 원소의 인덱스를 나타낸다\n edges.each { |e| add_edge(e[0], e[1]) }\n end\n\n ### 정점 개수 반환 ###\n def size\n @vertices.length\n end\n\n ### 정점 추가 ###\n def add_vertex(val)\n n = size\n # 정점 리스트에 새 정점의 값을 추가한다\n @vertices &lt;&lt; val\n # 인접 행렬에 행 하나를 추가한다\n new_row = Array.new(n, 0)\n @adj_mat &lt;&lt; new_row\n # 인접 행렬에 열 하나를 추가한다\n @adj_mat.each { |row| row &lt;&lt; 0 }\n end\n\n ### 정점 삭제 ###\n def remove_vertex(index)\n raise IndexError if index &gt;= size\n\n # 정점 리스트에서 인덱스 index의 정점을 제거한다\n @vertices.delete_at(index)\n # 인접 행렬에서 인덱스 index의 행을 삭제한다\n @adj_mat.delete_at(index)\n # 인접 행렬에서 인덱스 index의 열을 삭제한다\n @adj_mat.each { |row| row.delete_at(index) }\n end\n\n ### 간선 추가 ###\n def add_edge(i, j)\n # 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\n # 인덱스가 범위를 벗어나거나 서로 같은 경우를 처리한다\n if i &lt; 0 || j &lt; 0 || i &gt;= size || j &gt;= size || i == j\n raise IndexError\n end\n # 무방향 그래프의 인접 행렬은 주대각선에 대해 대칭이며, (i, j) == (j, i)를 만족한다\n @adj_mat[i][j] = 1\n @adj_mat[j][i] = 1\n end\n\n ### 간선 삭제 ###\n def remove_edge(i, j)\n # 매개변수 i, j는 vertices 원소의 인덱스에 해당한다\n # 인덱스가 범위를 벗어나거나 서로 같은 경우를 처리한다\n if i &lt; 0 || j &lt; 0 || i &gt;= size || j &gt;= size || i == j\n raise IndexError\n end\n @adj_mat[i][j] = 0\n @adj_mat[j][i] = 0\n end\n\n ### 인접 행렬 출력 ###\n def __print__\n puts \"정점 리스트 = #{@vertices}\"\n puts '인접 행렬 ='\n print_matrix(@adj_mat)\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 9 장   그래프","9.2   그래프의 기본 연산"],"tags":[]},{"location":"chapter_graph/graph_operations/#922","level":2,"title":"9.2.2   인접 리스트 기반 구현","text":"<p>무방향 그래프의 전체 정점 수가 \\(n\\), 전체 간선 수가 \\(m\\)이라면 아래 그림(9-8)의 방법으로 각 연산을 구현할 수 있다.</p> <ul> <li>간선 추가: 해당 정점에 대응하는 연결 리스트 끝에 간선을 추가한다. \\(O(1)\\) 시간이 걸린다. 무방향 그래프이므로 두 방향의 간선을 함께 추가해야 한다.</li> <li>간선 삭제: 해당 정점에 대응하는 연결 리스트에서 지정한 간선을 찾아 삭제한다. \\(O(m)\\) 시간이 걸린다. 무방향 그래프에서는 두 방향의 간선을 함께 삭제해야 한다.</li> <li>정점 추가: 인접 리스트에 연결 리스트 하나를 추가하고 새 정점을 그 연결 리스트의 머리 노드로 둔다. \\(O(1)\\) 시간이 걸린다.</li> <li>정점 삭제: 인접 리스트 전체를 순회하며 지정한 정점이 포함된 모든 간선을 삭제한다. \\(O(n + m)\\) 시간이 걸린다.</li> <li>초기화: 인접 리스트에 정점 \\(n\\) 개와 간선 \\(2m\\) 개를 만든다. \\(O(n + m)\\) 시간이 걸린다.</li> </ul> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 9-8   인접 리스트의 초기화, 간선 추가와 삭제, 정점 추가와 삭제 </p> <p>다음은 인접 리스트의 구현 코드다. 위 그림(9-8)과 비교하면 실제 코드에는 다음 차이가 있다.</p> <ul> <li>정점을 편리하게 추가하고 삭제하며 코드를 간단하게 만들기 위해 연결 리스트 대신 리스트(동적 배열)를 사용한다.</li> <li>해시 테이블로 인접 리스트를 저장한다. <code>key</code>는 정점 인스턴스이고, <code>value</code>는 그 정점의 인접 정점 리스트(연결 리스트)다.</li> </ul> <p>또한 인접 리스트에서 정점을 나타내기 위해 <code>Vertex</code> 클래스를 사용한다. 인접 행렬처럼 리스트 인덱스로 정점을 구분한다면 인덱스가 \\(i\\)인 정점을 삭제할 때 인접 리스트 전체를 순회하면서 \\(i\\)보다 큰 모든 인덱스를 \\(1\\)씩 줄여야 하므로 매우 비효율적이기 때문이다. 반면 정점마다 고유한 <code>Vertex</code> 인스턴스를 사용하면 어느 정점을 삭제해도 다른 정점을 수정할 필요가 없다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_adjacency_list.py<pre><code>class GraphAdjList:\n \"\"\"인접 리스트로 구현한 무방향 그래프 클래스\"\"\"\n\n def __init__(self, edges: list[list[Vertex]]):\n \"\"\"생성자\"\"\"\n # 인접 리스트의 key: 정점, value: 해당 정점의 모든 인접 정점\n self.adj_list = dict[Vertex, list[Vertex]]()\n # 모든 정점과 간선을 추가한다\n for edge in edges:\n self.add_vertex(edge[0])\n self.add_vertex(edge[1])\n self.add_edge(edge[0], edge[1])\n\n def size(self) -&gt; int:\n \"\"\"정점 개수 반환\"\"\"\n return len(self.adj_list)\n\n def add_edge(self, vet1: Vertex, vet2: Vertex):\n \"\"\"간선 추가\"\"\"\n if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:\n raise ValueError()\n # 간선 vet1 - vet2를 추가한다\n self.adj_list[vet1].append(vet2)\n self.adj_list[vet2].append(vet1)\n\n def remove_edge(self, vet1: Vertex, vet2: Vertex):\n \"\"\"간선 삭제\"\"\"\n if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:\n raise ValueError()\n # 간선 vet1 - vet2를 삭제한다\n self.adj_list[vet1].remove(vet2)\n self.adj_list[vet2].remove(vet1)\n\n def add_vertex(self, vet: Vertex):\n \"\"\"정점 추가\"\"\"\n if vet in self.adj_list:\n return\n # 인접 리스트에 새 연결 리스트를 추가한다\n self.adj_list[vet] = []\n\n def remove_vertex(self, vet: Vertex):\n \"\"\"정점 삭제\"\"\"\n if vet not in self.adj_list:\n raise ValueError()\n # 인접 리스트에서 정점 vet에 해당하는 연결 리스트를 삭제한다\n self.adj_list.pop(vet)\n # 다른 정점의 연결 리스트를 순회하여 vet를 포함하는 모든 간선을 삭제한다\n for vertex in self.adj_list:\n if vet in self.adj_list[vertex]:\n self.adj_list[vertex].remove(vet)\n\n def print(self):\n \"\"\"인접 리스트 출력\"\"\"\n print(\"인접 리스트 =\")\n for vertex in self.adj_list:\n tmp = [v.val for v in self.adj_list[vertex]]\n print(f\"{vertex.val}: {tmp},\")\n</code></pre> graph_adjacency_list.cpp<pre><code>/* 인접 리스트로 구현한 무방향 그래프 클래스 */\nclass GraphAdjList {\n public:\n // 인접 리스트의 key: 정점, value: 해당 정점의 모든 인접 정점\n unordered_map&lt;Vertex *, vector&lt;Vertex *&gt;&gt; adjList;\n\n /* vector에서 지정한 노드 삭제 */\n void remove(vector&lt;Vertex *&gt; &amp;vec, Vertex *vet) {\n for (int i = 0; i &lt; vec.size(); i++) {\n if (vec[i] == vet) {\n vec.erase(vec.begin() + i);\n break;\n }\n }\n }\n\n /* 생성자 */\n GraphAdjList(const vector&lt;vector&lt;Vertex *&gt;&gt; &amp;edges) {\n // 모든 정점과 간선을 추가한다\n for (const vector&lt;Vertex *&gt; &amp;edge : edges) {\n addVertex(edge[0]);\n addVertex(edge[1]);\n addEdge(edge[0], edge[1]);\n }\n }\n\n /* 정점 개수 반환 */\n int size() {\n return adjList.size();\n }\n\n /* 간선 추가 */\n void addEdge(Vertex *vet1, Vertex *vet2) {\n if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)\n throw invalid_argument(\"정점이 존재하지 않는다\");\n // 간선 vet1 - vet2를 추가한다\n adjList[vet1].push_back(vet2);\n adjList[vet2].push_back(vet1);\n }\n\n /* 간선 삭제 */\n void removeEdge(Vertex *vet1, Vertex *vet2) {\n if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)\n throw invalid_argument(\"정점이 존재하지 않는다\");\n // 간선 vet1 - vet2를 삭제한다\n remove(adjList[vet1], vet2);\n remove(adjList[vet2], vet1);\n }\n\n /* 정점 추가 */\n void addVertex(Vertex *vet) {\n if (adjList.count(vet))\n return;\n // 인접 리스트에 새 연결 리스트를 추가한다\n adjList[vet] = vector&lt;Vertex *&gt;();\n }\n\n /* 정점 삭제 */\n void removeVertex(Vertex *vet) {\n if (!adjList.count(vet))\n throw invalid_argument(\"정점이 존재하지 않는다\");\n // 인접 리스트에서 정점 vet에 해당하는 연결 리스트를 삭제한다\n adjList.erase(vet);\n // 다른 정점의 연결 리스트를 순회하여 vet를 포함하는 모든 간선을 삭제한다\n for (auto &amp;adj : adjList) {\n remove(adj.second, vet);\n }\n }\n\n /* 인접 리스트 출력 */\n void print() {\n cout &lt;&lt; \"인접 리스트 =\" &lt;&lt; endl;\n for (auto &amp;adj : adjList) {\n const auto &amp;key = adj.first;\n const auto &amp;vec = adj.second;\n cout &lt;&lt; key-&gt;val &lt;&lt; \": \";\n printVector(vetsToVals(vec));\n }\n }\n};\n</code></pre> graph_adjacency_list.java<pre><code>/* 인접 리스트로 구현한 무방향 그래프 클래스 */\nclass GraphAdjList {\n // 인접 리스트의 key: 정점, value: 해당 정점의 모든 인접 정점\n Map&lt;Vertex, List&lt;Vertex&gt;&gt; adjList;\n\n /* 생성자 */\n public GraphAdjList(Vertex[][] edges) {\n this.adjList = new HashMap&lt;&gt;();\n // 모든 정점과 간선을 추가한다\n for (Vertex[] edge : edges) {\n addVertex(edge[0]);\n addVertex(edge[1]);\n addEdge(edge[0], edge[1]);\n }\n }\n\n /* 정점 개수 반환 */\n public int size() {\n return adjList.size();\n }\n\n /* 간선 추가 */\n public void addEdge(Vertex vet1, Vertex vet2) {\n if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n throw new IllegalArgumentException();\n // 간선 vet1 - vet2를 추가한다\n adjList.get(vet1).add(vet2);\n adjList.get(vet2).add(vet1);\n }\n\n /* 간선 삭제 */\n public void removeEdge(Vertex vet1, Vertex vet2) {\n if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n throw new IllegalArgumentException();\n // 간선 vet1 - vet2를 삭제한다\n adjList.get(vet1).remove(vet2);\n adjList.get(vet2).remove(vet1);\n }\n\n /* 정점 추가 */\n public void addVertex(Vertex vet) {\n if (adjList.containsKey(vet))\n return;\n // 인접 리스트에 새 연결 리스트를 추가한다\n adjList.put(vet, new ArrayList&lt;&gt;());\n }\n\n /* 정점 삭제 */\n public void removeVertex(Vertex vet) {\n if (!adjList.containsKey(vet))\n throw new IllegalArgumentException();\n // 인접 리스트에서 정점 vet에 해당하는 연결 리스트를 삭제한다\n adjList.remove(vet);\n // 다른 정점의 연결 리스트를 순회하여 vet를 포함하는 모든 간선을 삭제한다\n for (List&lt;Vertex&gt; list : adjList.values()) {\n list.remove(vet);\n }\n }\n\n /* 인접 리스트 출력 */\n public void print() {\n System.out.println(\"인접 리스트 =\");\n for (Map.Entry&lt;Vertex, List&lt;Vertex&gt;&gt; pair : adjList.entrySet()) {\n List&lt;Integer&gt; tmp = new ArrayList&lt;&gt;();\n for (Vertex vertex : pair.getValue())\n tmp.add(vertex.val);\n System.out.println(pair.getKey().val + \": \" + tmp + \",\");\n }\n }\n}\n</code></pre> graph_adjacency_list.cs<pre><code>/* 인접 리스트로 구현한 무방향 그래프 클래스 */\nclass GraphAdjList {\n // 인접 리스트의 key: 정점, value: 해당 정점의 모든 인접 정점\n public Dictionary&lt;Vertex, List&lt;Vertex&gt;&gt; adjList;\n\n /* 생성자 */\n public GraphAdjList(Vertex[][] edges) {\n adjList = [];\n // 모든 정점과 간선을 추가한다\n foreach (Vertex[] edge in edges) {\n AddVertex(edge[0]);\n AddVertex(edge[1]);\n AddEdge(edge[0], edge[1]);\n }\n }\n\n /* 정점 개수 반환 */\n int Size() {\n return adjList.Count;\n }\n\n /* 간선 추가 */\n public void AddEdge(Vertex vet1, Vertex vet2) {\n if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2)\n throw new InvalidOperationException();\n // 간선 vet1 - vet2를 추가한다\n adjList[vet1].Add(vet2);\n adjList[vet2].Add(vet1);\n }\n\n /* 간선 삭제 */\n public void RemoveEdge(Vertex vet1, Vertex vet2) {\n if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2)\n throw new InvalidOperationException();\n // 간선 vet1 - vet2를 삭제한다\n adjList[vet1].Remove(vet2);\n adjList[vet2].Remove(vet1);\n }\n\n /* 정점 추가 */\n public void AddVertex(Vertex vet) {\n if (adjList.ContainsKey(vet))\n return;\n // 인접 리스트에 새 연결 리스트를 추가한다\n adjList.Add(vet, []);\n }\n\n /* 정점 삭제 */\n public void RemoveVertex(Vertex vet) {\n if (!adjList.ContainsKey(vet))\n throw new InvalidOperationException();\n // 인접 리스트에서 정점 vet에 해당하는 연결 리스트를 삭제한다\n adjList.Remove(vet);\n // 다른 정점의 연결 리스트를 순회하여 vet를 포함하는 모든 간선을 삭제한다\n foreach (List&lt;Vertex&gt; list in adjList.Values) {\n list.Remove(vet);\n }\n }\n\n /* 인접 리스트 출력 */\n public void Print() {\n Console.WriteLine(\"인접 리스트 =\");\n foreach (KeyValuePair&lt;Vertex, List&lt;Vertex&gt;&gt; pair in adjList) {\n List&lt;int&gt; tmp = [];\n foreach (Vertex vertex in pair.Value)\n tmp.Add(vertex.val);\n Console.WriteLine(pair.Key.val + \": [\" + string.Join(\", \", tmp) + \"],\");\n }\n }\n}\n</code></pre> graph_adjacency_list.go<pre><code>/* 인접 리스트로 구현한 무방향 그래프 클래스 */\ntype graphAdjList struct {\n // 인접 리스트의 key: 정점, value: 해당 정점의 모든 인접 정점\n adjList map[Vertex][]Vertex\n}\n\n/* 생성자 */\nfunc newGraphAdjList(edges [][]Vertex) *graphAdjList {\n g := &amp;graphAdjList{\n adjList: make(map[Vertex][]Vertex),\n }\n // 모든 정점과 간선을 추가한다\n for _, edge := range edges {\n g.addVertex(edge[0])\n g.addVertex(edge[1])\n g.addEdge(edge[0], edge[1])\n }\n return g\n}\n\n/* 정점 개수 반환 */\nfunc (g *graphAdjList) size() int {\n return len(g.adjList)\n}\n\n/* 간선 추가 */\nfunc (g *graphAdjList) addEdge(vet1 Vertex, vet2 Vertex) {\n _, ok1 := g.adjList[vet1]\n _, ok2 := g.adjList[vet2]\n if !ok1 || !ok2 || vet1 == vet2 {\n panic(\"오류\")\n }\n // 간선 vet1 - vet2를 추가하고 익명 struct{}를 추가한다\n g.adjList[vet1] = append(g.adjList[vet1], vet2)\n g.adjList[vet2] = append(g.adjList[vet2], vet1)\n}\n\n/* 간선 삭제 */\nfunc (g *graphAdjList) removeEdge(vet1 Vertex, vet2 Vertex) {\n _, ok1 := g.adjList[vet1]\n _, ok2 := g.adjList[vet2]\n if !ok1 || !ok2 || vet1 == vet2 {\n panic(\"오류\")\n }\n // 간선 vet1 - vet2를 삭제한다\n g.adjList[vet1] = DeleteSliceElms(g.adjList[vet1], vet2)\n g.adjList[vet2] = DeleteSliceElms(g.adjList[vet2], vet1)\n}\n\n/* 정점 추가 */\nfunc (g *graphAdjList) addVertex(vet Vertex) {\n _, ok := g.adjList[vet]\n if ok {\n return\n }\n // 인접 리스트에 새 연결 리스트를 추가한다\n g.adjList[vet] = make([]Vertex, 0)\n}\n\n/* 정점 삭제 */\nfunc (g *graphAdjList) removeVertex(vet Vertex) {\n _, ok := g.adjList[vet]\n if !ok {\n panic(\"오류\")\n }\n // 인접 리스트에서 정점 vet에 해당하는 연결 리스트를 삭제한다\n delete(g.adjList, vet)\n // 다른 정점의 연결 리스트를 순회하여 vet를 포함하는 모든 간선을 삭제한다\n for v, list := range g.adjList {\n g.adjList[v] = DeleteSliceElms(list, vet)\n }\n}\n\n/* 인접 리스트 출력 */\nfunc (g *graphAdjList) print() {\n var builder strings.Builder\n fmt.Printf(\"인접 리스트 = \\n\")\n for k, v := range g.adjList {\n builder.WriteString(\"\\t\\t\" + strconv.Itoa(k.Val) + \": \")\n for _, vet := range v {\n builder.WriteString(strconv.Itoa(vet.Val) + \" \")\n }\n fmt.Println(builder.String())\n builder.Reset()\n }\n}\n</code></pre> graph_adjacency_list.swift<pre><code>/* 인접 리스트로 구현한 무방향 그래프 클래스 */\nclass GraphAdjList {\n // 인접 리스트의 key: 정점, value: 해당 정점의 모든 인접 정점\n public private(set) var adjList: [Vertex: [Vertex]]\n\n /* 생성자 */\n public init(edges: [[Vertex]]) {\n adjList = [:]\n // 모든 정점과 간선을 추가한다\n for edge in edges {\n addVertex(vet: edge[0])\n addVertex(vet: edge[1])\n addEdge(vet1: edge[0], vet2: edge[1])\n }\n }\n\n /* 정점 개수 반환 */\n public func size() -&gt; Int {\n adjList.count\n }\n\n /* 간선 추가 */\n public func addEdge(vet1: Vertex, vet2: Vertex) {\n if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 {\n fatalError(\"인수 오류\")\n }\n // 간선 vet1 - vet2를 추가한다\n adjList[vet1]?.append(vet2)\n adjList[vet2]?.append(vet1)\n }\n\n /* 간선 삭제 */\n public func removeEdge(vet1: Vertex, vet2: Vertex) {\n if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 {\n fatalError(\"인수 오류\")\n }\n // 간선 vet1 - vet2를 삭제한다\n adjList[vet1]?.removeAll { $0 == vet2 }\n adjList[vet2]?.removeAll { $0 == vet1 }\n }\n\n /* 정점 추가 */\n public func addVertex(vet: Vertex) {\n if adjList[vet] != nil {\n return\n }\n // 인접 리스트에 새 연결 리스트를 추가한다\n adjList[vet] = []\n }\n\n /* 정점 삭제 */\n public func removeVertex(vet: Vertex) {\n if adjList[vet] == nil {\n fatalError(\"인수 오류\")\n }\n // 인접 리스트에서 정점 vet에 해당하는 연결 리스트를 삭제한다\n adjList.removeValue(forKey: vet)\n // 다른 정점의 연결 리스트를 순회하여 vet를 포함하는 모든 간선을 삭제한다\n for key in adjList.keys {\n adjList[key]?.removeAll { $0 == vet }\n }\n }\n\n /* 인접 리스트 출력 */\n public func print() {\n Swift.print(\"인접 리스트 =\")\n for (vertex, list) in adjList {\n let list = list.map { $0.val }\n Swift.print(\"\\(vertex.val): \\(list),\")\n }\n }\n}\n</code></pre> graph_adjacency_list.js<pre><code>/* 인접 리스트로 구현한 무방향 그래프 클래스 */\nclass GraphAdjList {\n // 인접 리스트의 key: 정점, value: 해당 정점의 모든 인접 정점\n adjList;\n\n /* 생성자 */\n constructor(edges) {\n this.adjList = new Map();\n // 모든 정점과 간선을 추가한다\n for (const edge of edges) {\n this.addVertex(edge[0]);\n this.addVertex(edge[1]);\n this.addEdge(edge[0], edge[1]);\n }\n }\n\n /* 정점 개수 반환 */\n size() {\n return this.adjList.size;\n }\n\n /* 간선 추가 */\n addEdge(vet1, vet2) {\n if (\n !this.adjList.has(vet1) ||\n !this.adjList.has(vet2) ||\n vet1 === vet2\n ) {\n throw new Error('잘못된 인수');\n }\n // 간선 vet1 - vet2를 추가한다\n this.adjList.get(vet1).push(vet2);\n this.adjList.get(vet2).push(vet1);\n }\n\n /* 간선 삭제 */\n removeEdge(vet1, vet2) {\n if (\n !this.adjList.has(vet1) ||\n !this.adjList.has(vet2) ||\n vet1 === vet2 ||\n this.adjList.get(vet1).indexOf(vet2) === -1\n ) {\n throw new Error('잘못된 인수');\n }\n // 간선 vet1 - vet2를 삭제한다\n this.adjList.get(vet1).splice(this.adjList.get(vet1).indexOf(vet2), 1);\n this.adjList.get(vet2).splice(this.adjList.get(vet2).indexOf(vet1), 1);\n }\n\n /* 정점 추가 */\n addVertex(vet) {\n if (this.adjList.has(vet)) return;\n // 인접 리스트에 새 연결 리스트를 추가한다\n this.adjList.set(vet, []);\n }\n\n /* 정점 삭제 */\n removeVertex(vet) {\n if (!this.adjList.has(vet)) {\n throw new Error('잘못된 인수');\n }\n // 인접 리스트에서 정점 vet에 해당하는 연결 리스트를 삭제한다\n this.adjList.delete(vet);\n // 다른 정점의 연결 리스트를 순회하여 vet를 포함하는 모든 간선을 삭제한다\n for (const set of this.adjList.values()) {\n const index = set.indexOf(vet);\n if (index &gt; -1) {\n set.splice(index, 1);\n }\n }\n }\n\n /* 인접 리스트 출력 */\n print() {\n console.log('인접 리스트 =');\n for (const [key, value] of this.adjList) {\n const tmp = [];\n for (const vertex of value) {\n tmp.push(vertex.val);\n }\n console.log(key.val + ': ' + tmp.join());\n }\n }\n}\n</code></pre> graph_adjacency_list.ts<pre><code>/* 인접 리스트로 구현한 무방향 그래프 클래스 */\nclass GraphAdjList {\n // 인접 리스트의 key: 정점, value: 해당 정점의 모든 인접 정점\n adjList: Map&lt;Vertex, Vertex[]&gt;;\n\n /* 생성자 */\n constructor(edges: Vertex[][]) {\n this.adjList = new Map();\n // 모든 정점과 간선을 추가한다\n for (const edge of edges) {\n this.addVertex(edge[0]);\n this.addVertex(edge[1]);\n this.addEdge(edge[0], edge[1]);\n }\n }\n\n /* 정점 개수 반환 */\n size(): number {\n return this.adjList.size;\n }\n\n /* 간선 추가 */\n addEdge(vet1: Vertex, vet2: Vertex): void {\n if (\n !this.adjList.has(vet1) ||\n !this.adjList.has(vet2) ||\n vet1 === vet2\n ) {\n throw new Error('잘못된 인수');\n }\n // 간선 vet1 - vet2를 추가한다\n this.adjList.get(vet1).push(vet2);\n this.adjList.get(vet2).push(vet1);\n }\n\n /* 간선 삭제 */\n removeEdge(vet1: Vertex, vet2: Vertex): void {\n if (\n !this.adjList.has(vet1) ||\n !this.adjList.has(vet2) ||\n vet1 === vet2 ||\n this.adjList.get(vet1).indexOf(vet2) === -1\n ) {\n throw new Error('잘못된 인수');\n }\n // 간선 vet1 - vet2를 삭제한다\n this.adjList.get(vet1).splice(this.adjList.get(vet1).indexOf(vet2), 1);\n this.adjList.get(vet2).splice(this.adjList.get(vet2).indexOf(vet1), 1);\n }\n\n /* 정점 추가 */\n addVertex(vet: Vertex): void {\n if (this.adjList.has(vet)) return;\n // 인접 리스트에 새 연결 리스트를 추가한다\n this.adjList.set(vet, []);\n }\n\n /* 정점 삭제 */\n removeVertex(vet: Vertex): void {\n if (!this.adjList.has(vet)) {\n throw new Error('잘못된 인수');\n }\n // 인접 리스트에서 정점 vet에 해당하는 연결 리스트를 삭제한다\n this.adjList.delete(vet);\n // 다른 정점의 연결 리스트를 순회하여 vet를 포함하는 모든 간선을 삭제한다\n for (const set of this.adjList.values()) {\n const index: number = set.indexOf(vet);\n if (index &gt; -1) {\n set.splice(index, 1);\n }\n }\n }\n\n /* 인접 리스트 출력 */\n print(): void {\n console.log('인접 리스트 =');\n for (const [key, value] of this.adjList.entries()) {\n const tmp = [];\n for (const vertex of value) {\n tmp.push(vertex.val);\n }\n console.log(key.val + ': ' + tmp.join());\n }\n }\n}\n</code></pre> graph_adjacency_list.dart<pre><code>/* 인접 리스트로 구현한 무방향 그래프 클래스 */\nclass GraphAdjList {\n // 인접 리스트의 key: 정점, value: 해당 정점의 모든 인접 정점\n Map&lt;Vertex, List&lt;Vertex&gt;&gt; adjList = {};\n\n /* 생성자 */\n GraphAdjList(List&lt;List&lt;Vertex&gt;&gt; edges) {\n for (List&lt;Vertex&gt; edge in edges) {\n addVertex(edge[0]);\n addVertex(edge[1]);\n addEdge(edge[0], edge[1]);\n }\n }\n\n /* 정점 개수 반환 */\n int size() {\n return adjList.length;\n }\n\n /* 간선 추가 */\n void addEdge(Vertex vet1, Vertex vet2) {\n if (!adjList.containsKey(vet1) ||\n !adjList.containsKey(vet2) ||\n vet1 == vet2) {\n throw ArgumentError;\n }\n // 간선 vet1 - vet2를 추가한다\n adjList[vet1]!.add(vet2);\n adjList[vet2]!.add(vet1);\n }\n\n /* 간선 삭제 */\n void removeEdge(Vertex vet1, Vertex vet2) {\n if (!adjList.containsKey(vet1) ||\n !adjList.containsKey(vet2) ||\n vet1 == vet2) {\n throw ArgumentError;\n }\n // 간선 vet1 - vet2를 삭제한다\n adjList[vet1]!.remove(vet2);\n adjList[vet2]!.remove(vet1);\n }\n\n /* 정점 추가 */\n void addVertex(Vertex vet) {\n if (adjList.containsKey(vet)) return;\n // 인접 리스트에 새 연결 리스트를 추가한다\n adjList[vet] = [];\n }\n\n /* 정점 삭제 */\n void removeVertex(Vertex vet) {\n if (!adjList.containsKey(vet)) {\n throw ArgumentError;\n }\n // 인접 리스트에서 정점 vet에 해당하는 연결 리스트를 삭제한다\n adjList.remove(vet);\n // 다른 정점의 연결 리스트를 순회하여 vet를 포함하는 모든 간선을 삭제한다\n adjList.forEach((key, value) {\n value.remove(vet);\n });\n }\n\n /* 인접 리스트 출력 */\n void printAdjList() {\n print(\"인접 리스트 =\");\n adjList.forEach((key, value) {\n List&lt;int&gt; tmp = [];\n for (Vertex vertex in value) {\n tmp.add(vertex.val);\n }\n print(\"${key.val}: $tmp,\");\n });\n }\n}\n</code></pre> graph_adjacency_list.rs<pre><code>/* 인접 리스트로 구현한 무방향 그래프 자료형 */\npub struct GraphAdjList {\n // 인접 리스트의 key: 정점, value: 해당 정점의 모든 인접 정점\n pub adj_list: HashMap&lt;Vertex, Vec&lt;Vertex&gt;&gt;, // 값 부분에는 HashSet&lt;Vertex&gt;가 더 적합할까?\n}\n\nimpl GraphAdjList {\n /* 생성자 */\n pub fn new(edges: Vec&lt;[Vertex; 2]&gt;) -&gt; Self {\n let mut graph = GraphAdjList {\n adj_list: HashMap::new(),\n };\n // 모든 정점과 간선을 추가한다\n for edge in edges {\n graph.add_vertex(edge[0]);\n graph.add_vertex(edge[1]);\n graph.add_edge(edge[0], edge[1]);\n }\n\n graph\n }\n\n /* 정점 개수 반환 */\n #[allow(unused)]\n pub fn size(&amp;self) -&gt; usize {\n self.adj_list.len()\n }\n\n /* 간선 추가 */\n pub fn add_edge(&amp;mut self, vet1: Vertex, vet2: Vertex) {\n if vet1 == vet2 {\n panic!(\"값 오류\");\n }\n // 간선 vet1 - vet2를 추가한다\n self.adj_list.entry(vet1).or_default().push(vet2);\n self.adj_list.entry(vet2).or_default().push(vet1);\n }\n\n /* 간선 삭제 */\n #[allow(unused)]\n pub fn remove_edge(&amp;mut self, vet1: Vertex, vet2: Vertex) {\n if vet1 == vet2 {\n panic!(\"값 오류\");\n }\n // 간선 vet1 - vet2를 삭제한다\n self.adj_list\n .entry(vet1)\n .and_modify(|v| v.retain(|&amp;e| e != vet2));\n self.adj_list\n .entry(vet2)\n .and_modify(|v| v.retain(|&amp;e| e != vet1));\n }\n\n /* 정점 추가 */\n pub fn add_vertex(&amp;mut self, vet: Vertex) {\n if self.adj_list.contains_key(&amp;vet) {\n return;\n }\n // 인접 리스트에 새 연결 리스트를 추가한다\n self.adj_list.insert(vet, vec![]);\n }\n\n /* 정점 삭제 */\n #[allow(unused)]\n pub fn remove_vertex(&amp;mut self, vet: Vertex) {\n // 인접 리스트에서 정점 vet에 해당하는 연결 리스트를 삭제한다\n self.adj_list.remove(&amp;vet);\n // 다른 정점의 연결 리스트를 순회하여 vet를 포함하는 모든 간선을 삭제한다\n for list in self.adj_list.values_mut() {\n list.retain(|&amp;v| v != vet);\n }\n }\n\n /* 인접 리스트 출력 */\n pub fn print(&amp;self) {\n println!(\"인접 리스트 =\");\n for (vertex, list) in &amp;self.adj_list {\n let list = list.iter().map(|vertex| vertex.val).collect::&lt;Vec&lt;i32&gt;&gt;();\n println!(\"{}: {:?},\", vertex.val, list);\n }\n }\n}\n</code></pre> graph_adjacency_list.c<pre><code>/* 노드 구조체 */\ntypedef struct AdjListNode {\n Vertex *vertex; // 정점\n struct AdjListNode *next; // 다음 노드\n} AdjListNode;\n\n/* 정점에 해당하는 노드 찾기 */\nAdjListNode *findNode(GraphAdjList *graph, Vertex *vet) {\n for (int i = 0; i &lt; graph-&gt;size; i++) {\n if (graph-&gt;heads[i]-&gt;vertex == vet) {\n return graph-&gt;heads[i];\n }\n }\n return NULL;\n}\n\n/* 간선 추가 보조 함수 */\nvoid addEdgeHelper(AdjListNode *head, Vertex *vet) {\n AdjListNode *node = (AdjListNode *)malloc(sizeof(AdjListNode));\n node-&gt;vertex = vet;\n // 맨 앞에 삽입한다\n node-&gt;next = head-&gt;next;\n head-&gt;next = node;\n}\n\n/* 간선 삭제 보조 함수 */\nvoid removeEdgeHelper(AdjListNode *head, Vertex *vet) {\n AdjListNode *pre = head;\n AdjListNode *cur = head-&gt;next;\n // 연결 리스트에서 vet에 해당하는 노드를 찾는다\n while (cur != NULL &amp;&amp; cur-&gt;vertex != vet) {\n pre = cur;\n cur = cur-&gt;next;\n }\n if (cur == NULL)\n return;\n // 연결 리스트에서 vet에 해당하는 노드를 삭제한다\n pre-&gt;next = cur-&gt;next;\n // 메모리 해제\n free(cur);\n}\n\n/* 인접 리스트로 구현한 무방향 그래프 클래스 */\ntypedef struct {\n AdjListNode *heads[MAX_SIZE]; // 노드 배열\n int size; // 노드 개수\n} GraphAdjList;\n\n/* 생성자 */\nGraphAdjList *newGraphAdjList() {\n GraphAdjList *graph = (GraphAdjList *)malloc(sizeof(GraphAdjList));\n if (!graph) {\n return NULL;\n }\n graph-&gt;size = 0;\n for (int i = 0; i &lt; MAX_SIZE; i++) {\n graph-&gt;heads[i] = NULL;\n }\n return graph;\n}\n\n/* 소멸자 */\nvoid delGraphAdjList(GraphAdjList *graph) {\n for (int i = 0; i &lt; graph-&gt;size; i++) {\n AdjListNode *cur = graph-&gt;heads[i];\n while (cur != NULL) {\n AdjListNode *next = cur-&gt;next;\n if (cur != graph-&gt;heads[i]) {\n free(cur);\n }\n cur = next;\n }\n free(graph-&gt;heads[i]-&gt;vertex);\n free(graph-&gt;heads[i]);\n }\n free(graph);\n}\n\n/* 정점에 해당하는 노드 찾기 */\nAdjListNode *findNode(GraphAdjList *graph, Vertex *vet) {\n for (int i = 0; i &lt; graph-&gt;size; i++) {\n if (graph-&gt;heads[i]-&gt;vertex == vet) {\n return graph-&gt;heads[i];\n }\n }\n return NULL;\n}\n\n/* 간선 추가 */\nvoid addEdge(GraphAdjList *graph, Vertex *vet1, Vertex *vet2) {\n AdjListNode *head1 = findNode(graph, vet1);\n AdjListNode *head2 = findNode(graph, vet2);\n assert(head1 != NULL &amp;&amp; head2 != NULL &amp;&amp; head1 != head2);\n // 간선 vet1 - vet2를 추가한다\n addEdgeHelper(head1, vet2);\n addEdgeHelper(head2, vet1);\n}\n\n/* 간선 삭제 */\nvoid removeEdge(GraphAdjList *graph, Vertex *vet1, Vertex *vet2) {\n AdjListNode *head1 = findNode(graph, vet1);\n AdjListNode *head2 = findNode(graph, vet2);\n assert(head1 != NULL &amp;&amp; head2 != NULL);\n // 간선 vet1 - vet2를 삭제한다\n removeEdgeHelper(head1, head2-&gt;vertex);\n removeEdgeHelper(head2, head1-&gt;vertex);\n}\n\n/* 정점 추가 */\nvoid addVertex(GraphAdjList *graph, Vertex *vet) {\n assert(graph != NULL &amp;&amp; graph-&gt;size &lt; MAX_SIZE);\n AdjListNode *head = (AdjListNode *)malloc(sizeof(AdjListNode));\n head-&gt;vertex = vet;\n head-&gt;next = NULL;\n // 인접 리스트에 새 연결 리스트를 추가한다\n graph-&gt;heads[graph-&gt;size++] = head;\n}\n\n/* 정점 삭제 */\nvoid removeVertex(GraphAdjList *graph, Vertex *vet) {\n AdjListNode *node = findNode(graph, vet);\n assert(node != NULL);\n // 인접 리스트에서 정점 vet에 해당하는 연결 리스트를 삭제한다\n AdjListNode *cur = node, *pre = NULL;\n while (cur) {\n pre = cur;\n cur = cur-&gt;next;\n free(pre);\n }\n // 다른 정점의 연결 리스트를 순회하여 vet를 포함하는 모든 간선을 삭제한다\n for (int i = 0; i &lt; graph-&gt;size; i++) {\n cur = graph-&gt;heads[i];\n pre = NULL;\n while (cur) {\n pre = cur;\n cur = cur-&gt;next;\n if (cur &amp;&amp; cur-&gt;vertex == vet) {\n pre-&gt;next = cur-&gt;next;\n free(cur);\n break;\n }\n }\n }\n // 해당 정점 뒤의 정점을 앞으로 옮겨 빈자리를 채운다\n int i;\n for (i = 0; i &lt; graph-&gt;size; i++) {\n if (graph-&gt;heads[i] == node)\n break;\n }\n for (int j = i; j &lt; graph-&gt;size - 1; j++) {\n graph-&gt;heads[j] = graph-&gt;heads[j + 1];\n }\n graph-&gt;size--;\n free(vet);\n}\n</code></pre> graph_adjacency_list.kt<pre><code>/* 인접 리스트로 구현한 무방향 그래프 클래스 */\nclass GraphAdjList(edges: Array&lt;Array&lt;Vertex?&gt;&gt;) {\n // 인접 리스트의 key: 정점, value: 해당 정점의 모든 인접 정점\n val adjList = HashMap&lt;Vertex, MutableList&lt;Vertex&gt;&gt;()\n\n /* 생성자 */\n init {\n // 모든 정점과 간선을 추가한다\n for (edge in edges) {\n addVertex(edge[0]!!)\n addVertex(edge[1]!!)\n addEdge(edge[0]!!, edge[1]!!)\n }\n }\n\n /* 정점 개수 반환 */\n fun size(): Int {\n return adjList.size\n }\n\n /* 간선 추가 */\n fun addEdge(vet1: Vertex, vet2: Vertex) {\n if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n throw IllegalArgumentException()\n // 간선 vet1 - vet2를 추가한다\n adjList[vet1]?.add(vet2)\n adjList[vet2]?.add(vet1)\n }\n\n /* 간선 삭제 */\n fun removeEdge(vet1: Vertex, vet2: Vertex) {\n if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n throw IllegalArgumentException()\n // 간선 vet1 - vet2를 삭제한다\n adjList[vet1]?.remove(vet2)\n adjList[vet2]?.remove(vet1)\n }\n\n /* 정점 추가 */\n fun addVertex(vet: Vertex) {\n if (adjList.containsKey(vet))\n return\n // 인접 리스트에 새 연결 리스트를 추가한다\n adjList[vet] = mutableListOf()\n }\n\n /* 정점 삭제 */\n fun removeVertex(vet: Vertex) {\n if (!adjList.containsKey(vet))\n throw IllegalArgumentException()\n // 인접 리스트에서 정점 vet에 해당하는 연결 리스트를 삭제한다\n adjList.remove(vet)\n // 다른 정점의 연결 리스트를 순회하여 vet를 포함하는 모든 간선을 삭제한다\n for (list in adjList.values) {\n list.remove(vet)\n }\n }\n\n /* 인접 리스트 출력 */\n fun print() {\n println(\"인접 리스트 =\")\n for (pair in adjList.entries) {\n val tmp = mutableListOf&lt;Int&gt;()\n for (vertex in pair.value) {\n tmp.add(vertex._val)\n }\n println(\"${pair.key._val}: $tmp,\")\n }\n }\n}\n</code></pre> graph_adjacency_list.rb<pre><code>### 인접 리스트로 구현한 무방향 그래프 클래스 ###\nclass GraphAdjList\n attr_reader :adj_list\n\n ### 생성자 ###\n def initialize(edges)\n # 인접 리스트의 key: 정점, value: 해당 정점의 모든 인접 정점\n @adj_list = {}\n # 모든 정점과 간선을 추가한다\n for edge in edges\n add_vertex(edge[0])\n add_vertex(edge[1])\n add_edge(edge[0], edge[1])\n end\n end\n\n ### 정점 개수 반환 ###\n def size\n @adj_list.length\n end\n\n ### 간선 추가 ###\n def add_edge(vet1, vet2)\n raise ArgumentError if !@adj_list.include?(vet1) || !@adj_list.include?(vet2)\n\n @adj_list[vet1] &lt;&lt; vet2\n @adj_list[vet2] &lt;&lt; vet1\n end\n\n ### 간선 삭제 ###\n def remove_edge(vet1, vet2)\n raise ArgumentError if !@adj_list.include?(vet1) || !@adj_list.include?(vet2)\n\n # 간선 vet1 - vet2를 삭제한다\n @adj_list[vet1].delete(vet2)\n @adj_list[vet2].delete(vet1)\n end\n\n ### 정점 추가 ###\n def add_vertex(vet)\n return if @adj_list.include?(vet)\n\n # 인접 리스트에 새 연결 리스트를 추가한다\n @adj_list[vet] = []\n end\n\n ### 정점 삭제 ###\n def remove_vertex(vet)\n raise ArgumentError unless @adj_list.include?(vet)\n\n # 인접 리스트에서 정점 vet에 해당하는 연결 리스트를 삭제한다\n @adj_list.delete(vet)\n # 다른 정점의 연결 리스트를 순회하여 vet를 포함하는 모든 간선을 삭제한다\n for vertex in @adj_list\n @adj_list[vertex.first].delete(vet) if @adj_list[vertex.first].include?(vet)\n end\n end\n\n ### 인접 리스트 출력 ###\n def __print__\n puts '인접 리스트 ='\n for vertex in @adj_list\n tmp = @adj_list[vertex.first].map { |v| v.val }\n puts \"#{vertex.first.val}: #{tmp},\"\n end\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 9 장   그래프","9.2   그래프의 기본 연산"],"tags":[]},{"location":"chapter_graph/graph_operations/#923","level":2,"title":"9.2.3   효율 비교","text":"<p>그래프에 정점 \\(n\\) 개와 간선 \\(m\\) 개가 있다고 하자. 아래 표(9-2)는 인접 행렬과 인접 리스트의 시간 효율과 공간 효율을 비교한다. 여기서 인접 리스트(연결 리스트)는 이 절의 구현에 해당하며, 인접 리스트(해시 테이블)는 모든 연결 리스트를 해시 테이블로 바꾼 구현을 뜻한다.</p> <p> 표 9-2   인접 행렬과 인접 리스트 비교 </p> 인접 행렬 인접 리스트(연결 리스트) 인접 리스트(해시 테이블) 인접 여부 확인 \\(O(1)\\) \\(O(n)\\) \\(O(1)\\) 간선 추가 \\(O(1)\\) \\(O(1)\\) \\(O(1)\\) 간선 삭제 \\(O(1)\\) \\(O(n)\\) \\(O(1)\\) 정점 추가 \\(O(n)\\) \\(O(1)\\) \\(O(1)\\) 정점 삭제 \\(O(n^2)\\) \\(O(n + m)\\) \\(O(n)\\) 메모리 사용량 \\(O(n^2)\\) \\(O(n + m)\\) \\(O(n + m)\\) <p>위 표(9-2)만 보면 인접 리스트(해시 테이블)의 시간 효율과 공간 효율이 가장 좋아 보인다. 하지만 실제로는 배열 접근이나 대입 한 번이면 되는 인접 행렬의 간선 연산이 더 효율적이다. 종합하면 인접 행렬은 '공간을 더 써서 시간을 절약하는' 원리를, 인접 리스트는 '시간을 더 써서 공간을 절약하는' 원리를 보여 준다.</p>","path":["제 9 장   그래프","9.2   그래프의 기본 연산"],"tags":[]},{"location":"chapter_graph/graph_traversal/","level":1,"title":"9.3   그래프 순회","text":"<p>트리는 '일대다' 관계를 나타내지만, 그래프는 자유도가 더 높아 임의의 '다대다' 관계를 표현할 수 있다. 따라서 트리는 그래프의 특수한 경우로 볼 수 있다. 트리 순회 역시 그래프 순회의 특수한 경우다.</p> <p>그래프와 트리 모두 탐색 알고리즘을 이용하여 순회한다. 그래프 순회도 너비 우선 순회와 깊이 우선 순회의 두 가지로 나뉜다.</p>","path":["제 9 장   그래프","9.3   그래프 순회"],"tags":[]},{"location":"chapter_graph/graph_traversal/#931","level":2,"title":"9.3.1   너비 우선 순회","text":"<p>너비 우선 순회는 가까운 곳에서 먼 곳으로 진행하는 순회 방식이다. 한 노드에서 출발하여 항상 거리가 가장 가까운 정점을 먼저 방문하고, 한 층씩 바깥으로 확장한다. 아래 그림(9-9)에서는 왼쪽 위 정점에서 출발하여 해당 정점의 모든 인접 정점을 먼저 순회한 다음, 다음 정점의 모든 인접 정점을 순회한다. 이를 모든 정점을 방문할 때까지 반복한다.</p> <p></p> <p> 그림 9-9   그래프의 너비 우선 순회 </p>","path":["제 9 장   그래프","9.3   그래프 순회"],"tags":[]},{"location":"chapter_graph/graph_traversal/#1","level":3,"title":"1.   알고리즘 구현","text":"<p>BFS는 보통 큐로 구현하며, 코드는 다음과 같다. 큐의 '선입선출' 성질은 BFS의 '가까운 곳부터 먼 곳으로'라는 발상과 잘 맞는다.</p> <ol> <li>순회의 시작 정점 <code>startVet</code>를 큐에 넣고 반복문을 시작한다.</li> <li>매 반복에서 큐의 맨 앞 정점을 꺼내 방문을 기록한 다음, 해당 정점의 모든 인접 정점을 큐의 끝에 넣는다.</li> <li>모든 정점을 방문할 때까지 <code>2.</code> 단계를 반복한 뒤 종료한다.</li> </ol> <p>같은 정점을 여러 번 순회하지 않도록 해시 집합 <code>visited</code>를 사용하여 방문한 노드를 기록한다.</p> <p>참고</p> <p>해시 집합은 <code>value</code> 없이 <code>key</code>만 저장하는 해시 테이블로 볼 수 있다. <code>key</code>의 추가, 삭제, 조회, 수정 연산을 \\(O(1)\\) 시간에 수행한다. <code>key</code>는 중복될 수 없으므로, 해시 집합은 데이터 중복 제거 등에 자주 사용된다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_bfs.py<pre><code>def graph_bfs(graph: GraphAdjList, start_vet: Vertex) -&gt; list[Vertex]:\n \"\"\"너비 우선 순회\"\"\"\n # 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\n # 정점 순회 순서\n res = []\n # 방문한 정점을 기록하는 해시 집합\n visited = set[Vertex]([start_vet])\n # 큐를 사용하여 BFS를 구현한다\n que = deque[Vertex]([start_vet])\n # 정점 vet에서 시작하여 모든 정점을 방문할 때까지 반복한다\n while len(que) &gt; 0:\n vet = que.popleft() # 큐 맨 앞의 정점을 꺼낸다\n res.append(vet) # 방문한 정점을 기록한다\n # 해당 정점의 모든 인접 정점을 순회한다\n for adj_vet in graph.adj_list[vet]:\n if adj_vet in visited:\n continue # 이미 방문한 정점은 건너뛴다\n que.append(adj_vet) # 방문하지 않은 정점만 큐에 삽입한다\n visited.add(adj_vet) # 해당 정점을 방문했다고 표시한다\n # 정점 순회 순서를 반환한다\n return res\n</code></pre> graph_bfs.cpp<pre><code>/* 너비 우선 순회 */\n// 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\nvector&lt;Vertex *&gt; graphBFS(GraphAdjList &amp;graph, Vertex *startVet) {\n // 정점 순회 순서\n vector&lt;Vertex *&gt; res;\n // 방문한 정점을 기록하는 해시 집합\n unordered_set&lt;Vertex *&gt; visited = {startVet};\n // 큐를 사용하여 BFS를 구현한다\n queue&lt;Vertex *&gt; que;\n que.push(startVet);\n // 정점 vet에서 시작하여 모든 정점을 방문할 때까지 반복한다\n while (!que.empty()) {\n Vertex *vet = que.front();\n que.pop(); // 큐 맨 앞의 정점을 꺼낸다\n res.push_back(vet); // 방문한 정점을 기록한다\n // 해당 정점의 모든 인접 정점을 순회한다\n for (auto adjVet : graph.adjList[vet]) {\n if (visited.count(adjVet))\n continue; // 이미 방문한 정점은 건너뛴다\n que.push(adjVet); // 방문하지 않은 정점만 큐에 삽입한다\n visited.emplace(adjVet); // 해당 정점을 방문했다고 표시한다\n }\n }\n // 정점 순회 순서를 반환한다\n return res;\n}\n</code></pre> graph_bfs.java<pre><code>/* 너비 우선 순회 */\n// 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\nList&lt;Vertex&gt; graphBFS(GraphAdjList graph, Vertex startVet) {\n // 정점 순회 순서\n List&lt;Vertex&gt; res = new ArrayList&lt;&gt;();\n // 방문한 정점을 기록하는 해시 집합\n Set&lt;Vertex&gt; visited = new HashSet&lt;&gt;();\n visited.add(startVet);\n // 큐를 사용하여 BFS를 구현한다\n Queue&lt;Vertex&gt; que = new LinkedList&lt;&gt;();\n que.offer(startVet);\n // 정점 vet에서 시작하여 모든 정점을 방문할 때까지 반복한다\n while (!que.isEmpty()) {\n Vertex vet = que.poll(); // 큐 맨 앞의 정점을 꺼낸다\n res.add(vet); // 방문한 정점을 기록한다\n // 해당 정점의 모든 인접 정점을 순회한다\n for (Vertex adjVet : graph.adjList.get(vet)) {\n if (visited.contains(adjVet))\n continue; // 이미 방문한 정점은 건너뛴다\n que.offer(adjVet); // 방문하지 않은 정점만 큐에 삽입한다\n visited.add(adjVet); // 해당 정점을 방문했다고 표시한다\n }\n }\n // 정점 순회 순서를 반환한다\n return res;\n}\n</code></pre> graph_bfs.cs<pre><code>/* 너비 우선 순회 */\n// 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\nList&lt;Vertex&gt; GraphBFS(GraphAdjList graph, Vertex startVet) {\n // 정점 순회 순서\n List&lt;Vertex&gt; res = [];\n // 방문한 정점을 기록하는 해시 집합\n HashSet&lt;Vertex&gt; visited = [startVet];\n // 큐를 사용하여 BFS를 구현한다\n Queue&lt;Vertex&gt; que = new();\n que.Enqueue(startVet);\n // 정점 vet에서 시작하여 모든 정점을 방문할 때까지 반복한다\n while (que.Count &gt; 0) {\n Vertex vet = que.Dequeue(); // 큐 맨 앞의 정점을 꺼낸다\n res.Add(vet); // 방문한 정점을 기록한다\n foreach (Vertex adjVet in graph.adjList[vet]) {\n if (visited.Contains(adjVet)) {\n continue; // 이미 방문한 정점은 건너뛴다\n }\n que.Enqueue(adjVet); // 방문하지 않은 정점만 큐에 삽입한다\n visited.Add(adjVet); // 해당 정점을 방문했다고 표시한다\n }\n }\n\n // 정점 순회 순서를 반환한다\n return res;\n}\n</code></pre> graph_bfs.go<pre><code>/* 너비 우선 순회 */\n// 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\nfunc graphBFS(g *graphAdjList, startVet Vertex) []Vertex {\n // 정점 순회 순서\n res := make([]Vertex, 0)\n // 방문한 정점을 기록하는 해시 집합\n visited := make(map[Vertex]struct{})\n visited[startVet] = struct{}{}\n // 큐로 BFS를 구현하며, 슬라이스로 큐를 구현한다\n queue := make([]Vertex, 0)\n queue = append(queue, startVet)\n // 정점 vet에서 시작하여 모든 정점을 방문할 때까지 반복한다\n for len(queue) &gt; 0 {\n // 큐 맨 앞의 정점을 꺼낸다\n vet := queue[0]\n queue = queue[1:]\n // 방문한 정점을 기록한다\n res = append(res, vet)\n // 해당 정점의 모든 인접 정점을 순회한다\n for _, adjVet := range g.adjList[vet] {\n _, isExist := visited[adjVet]\n // 방문하지 않은 정점만 큐에 삽입한다\n if !isExist {\n queue = append(queue, adjVet)\n visited[adjVet] = struct{}{}\n }\n }\n }\n // 정점 순회 순서를 반환한다\n return res\n}\n</code></pre> graph_bfs.swift<pre><code>/* 너비 우선 순회 */\n// 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\nfunc graphBFS(graph: GraphAdjList, startVet: Vertex) -&gt; [Vertex] {\n // 정점 순회 순서\n var res: [Vertex] = []\n // 방문한 정점을 기록하는 해시 집합\n var visited: Set&lt;Vertex&gt; = [startVet]\n // 큐를 사용하여 BFS를 구현한다\n var que: [Vertex] = [startVet]\n // 정점 vet에서 시작하여 모든 정점을 방문할 때까지 반복한다\n while !que.isEmpty {\n let vet = que.removeFirst() // 큐 맨 앞의 정점을 꺼낸다\n res.append(vet) // 방문한 정점을 기록한다\n // 해당 정점의 모든 인접 정점을 순회한다\n for adjVet in graph.adjList[vet] ?? [] {\n if visited.contains(adjVet) {\n continue // 이미 방문한 정점은 건너뛴다\n }\n que.append(adjVet) // 방문하지 않은 정점만 큐에 삽입한다\n visited.insert(adjVet) // 해당 정점을 방문했다고 표시한다\n }\n }\n // 정점 순회 순서를 반환한다\n return res\n}\n</code></pre> graph_bfs.js<pre><code>/* 너비 우선 순회 */\n// 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\nfunction graphBFS(graph, startVet) {\n // 정점 순회 순서\n const res = [];\n // 방문한 정점을 기록하는 해시 집합\n const visited = new Set();\n visited.add(startVet);\n // 큐를 사용하여 BFS를 구현한다\n const que = [startVet];\n // 정점 vet에서 시작하여 모든 정점을 방문할 때까지 반복한다\n while (que.length) {\n const vet = que.shift(); // 큐 맨 앞의 정점을 꺼낸다\n res.push(vet); // 방문한 정점을 기록한다\n // 해당 정점의 모든 인접 정점을 순회한다\n for (const adjVet of graph.adjList.get(vet) ?? []) {\n if (visited.has(adjVet)) {\n continue; // 이미 방문한 정점은 건너뛴다\n }\n que.push(adjVet); // 방문하지 않은 정점만 큐에 삽입한다\n visited.add(adjVet); // 해당 정점을 방문했다고 표시한다\n }\n }\n // 정점 순회 순서를 반환한다\n return res;\n}\n</code></pre> graph_bfs.ts<pre><code>/* 너비 우선 순회 */\n// 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\nfunction graphBFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {\n // 정점 순회 순서\n const res: Vertex[] = [];\n // 방문한 정점을 기록하는 해시 집합\n const visited: Set&lt;Vertex&gt; = new Set();\n visited.add(startVet);\n // 큐를 사용하여 BFS를 구현한다\n const que = [startVet];\n // 정점 vet에서 시작하여 모든 정점을 방문할 때까지 반복한다\n while (que.length) {\n const vet = que.shift(); // 큐 맨 앞의 정점을 꺼낸다\n res.push(vet); // 방문한 정점을 기록한다\n // 해당 정점의 모든 인접 정점을 순회한다\n for (const adjVet of graph.adjList.get(vet) ?? []) {\n if (visited.has(adjVet)) {\n continue; // 이미 방문한 정점은 건너뛴다\n }\n que.push(adjVet); // 방문하지 않은 정점만 큐에 삽입한다\n visited.add(adjVet); // 해당 정점을 방문했다고 표시한다\n }\n }\n // 정점 순회 순서를 반환한다\n return res;\n}\n</code></pre> graph_bfs.dart<pre><code>/* 너비 우선 순회 */\nList&lt;Vertex&gt; graphBFS(GraphAdjList graph, Vertex startVet) {\n // 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\n // 정점 순회 순서\n List&lt;Vertex&gt; res = [];\n // 방문한 정점을 기록하는 해시 집합\n Set&lt;Vertex&gt; visited = {};\n visited.add(startVet);\n // 큐를 사용하여 BFS를 구현한다\n Queue&lt;Vertex&gt; que = Queue();\n que.add(startVet);\n // 정점 vet에서 시작하여 모든 정점을 방문할 때까지 반복한다\n while (que.isNotEmpty) {\n Vertex vet = que.removeFirst(); // 큐 맨 앞의 정점을 꺼낸다\n res.add(vet); // 방문한 정점을 기록한다\n // 해당 정점의 모든 인접 정점을 순회한다\n for (Vertex adjVet in graph.adjList[vet]!) {\n if (visited.contains(adjVet)) {\n continue; // 이미 방문한 정점은 건너뛴다\n }\n que.add(adjVet); // 방문하지 않은 정점만 큐에 삽입한다\n visited.add(adjVet); // 해당 정점을 방문했다고 표시한다\n }\n }\n // 정점 순회 순서를 반환한다\n return res;\n}\n</code></pre> graph_bfs.rs<pre><code>/* 너비 우선 순회 */\n// 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\nfn graph_bfs(graph: GraphAdjList, start_vet: Vertex) -&gt; Vec&lt;Vertex&gt; {\n // 정점 순회 순서\n let mut res = vec![];\n // 방문한 정점을 기록하는 해시 집합\n let mut visited = HashSet::new();\n visited.insert(start_vet);\n // 큐를 사용하여 BFS를 구현한다\n let mut que = VecDeque::new();\n que.push_back(start_vet);\n // 정점 vet에서 시작하여 모든 정점을 방문할 때까지 반복한다\n while let Some(vet) = que.pop_front() {\n res.push(vet); // 방문한 정점을 기록한다\n\n // 해당 정점의 모든 인접 정점을 순회한다\n if let Some(adj_vets) = graph.adj_list.get(&amp;vet) {\n for &amp;adj_vet in adj_vets {\n if visited.contains(&amp;adj_vet) {\n continue; // 이미 방문한 정점은 건너뛴다\n }\n que.push_back(adj_vet); // 방문하지 않은 정점만 큐에 삽입한다\n visited.insert(adj_vet); // 해당 정점을 방문했다고 표시한다\n }\n }\n }\n // 정점 순회 순서를 반환한다\n res\n}\n</code></pre> graph_bfs.c<pre><code>/* 노드 큐 구조체 */\ntypedef struct {\n Vertex *vertices[MAX_SIZE];\n int front, rear, size;\n} Queue;\n\n/* 생성자 */\nQueue *newQueue() {\n Queue *q = (Queue *)malloc(sizeof(Queue));\n q-&gt;front = q-&gt;rear = q-&gt;size = 0;\n return q;\n}\n\n/* 큐가 비어 있는지 판단 */\nint isEmpty(Queue *q) {\n return q-&gt;size == 0;\n}\n\n/* 큐 삽입 연산 */\nvoid enqueue(Queue *q, Vertex *vet) {\n q-&gt;vertices[q-&gt;rear] = vet;\n q-&gt;rear = (q-&gt;rear + 1) % MAX_SIZE;\n q-&gt;size++;\n}\n\n/* 큐에서 꺼내는 연산 */\nVertex *dequeue(Queue *q) {\n Vertex *vet = q-&gt;vertices[q-&gt;front];\n q-&gt;front = (q-&gt;front + 1) % MAX_SIZE;\n q-&gt;size--;\n return vet;\n}\n\n/* 정점을 이미 방문했는지 확인 */\nint isVisited(Vertex **visited, int size, Vertex *vet) {\n // O(n) 시간에 순회하여 노드를 찾는다\n for (int i = 0; i &lt; size; i++) {\n if (visited[i] == vet)\n return 1;\n }\n return 0;\n}\n\n/* 너비 우선 순회 */\n// 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\nvoid graphBFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize, Vertex **visited, int *visitedSize) {\n // 큐를 사용하여 BFS를 구현한다\n Queue *queue = newQueue();\n enqueue(queue, startVet);\n visited[(*visitedSize)++] = startVet;\n // 정점 vet에서 시작하여 모든 정점을 방문할 때까지 반복한다\n while (!isEmpty(queue)) {\n Vertex *vet = dequeue(queue); // 큐 맨 앞의 정점을 꺼낸다\n res[(*resSize)++] = vet; // 방문한 정점을 기록한다\n // 해당 정점의 모든 인접 정점을 순회한다\n AdjListNode *node = findNode(graph, vet);\n while (node != NULL) {\n // 이미 방문한 정점은 건너뛴다\n if (!isVisited(visited, *visitedSize, node-&gt;vertex)) {\n enqueue(queue, node-&gt;vertex); // 방문하지 않은 정점만 큐에 삽입한다\n visited[(*visitedSize)++] = node-&gt;vertex; // 해당 정점을 방문했다고 표시한다\n }\n node = node-&gt;next;\n }\n }\n // 메모리 해제\n free(queue);\n}\n</code></pre> graph_bfs.kt<pre><code>/* 너비 우선 순회 */\n// 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\nfun graphBFS(graph: GraphAdjList, startVet: Vertex): MutableList&lt;Vertex?&gt; {\n // 정점 순회 순서\n val res = mutableListOf&lt;Vertex?&gt;()\n // 방문한 정점을 기록하는 해시 집합\n val visited = HashSet&lt;Vertex&gt;()\n visited.add(startVet)\n // 큐를 사용하여 BFS를 구현한다\n val que = LinkedList&lt;Vertex&gt;()\n que.offer(startVet)\n // 정점 vet에서 시작하여 모든 정점을 방문할 때까지 반복한다\n while (!que.isEmpty()) {\n val vet = que.poll() // 큐 맨 앞의 정점을 꺼낸다\n res.add(vet) // 방문한 정점을 기록한다\n // 해당 정점의 모든 인접 정점을 순회한다\n for (adjVet in graph.adjList[vet]!!) {\n if (visited.contains(adjVet))\n continue // 이미 방문한 정점은 건너뛴다\n que.offer(adjVet) // 방문하지 않은 정점만 큐에 삽입한다\n visited.add(adjVet) // 해당 정점을 방문했다고 표시한다\n }\n }\n // 정점 순회 순서를 반환한다\n return res\n}\n</code></pre> graph_bfs.rb<pre><code>### 너비 우선 순회 ###\ndef graph_bfs(graph, start_vet)\n # 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\n # 정점 순회 순서\n res = []\n # 방문한 정점을 기록하는 해시 집합\n visited = Set.new([start_vet])\n # 큐를 사용하여 BFS를 구현한다\n que = [start_vet]\n # 정점 vet에서 시작하여 모든 정점을 방문할 때까지 반복한다\n while que.length &gt; 0\n vet = que.shift # 큐 맨 앞의 정점을 꺼낸다\n res &lt;&lt; vet # 방문한 정점을 기록한다\n # 해당 정점의 모든 인접 정점을 순회한다\n for adj_vet in graph.adj_list[vet]\n next if visited.include?(adj_vet) # 이미 방문한 정점은 건너뛴다\n que &lt;&lt; adj_vet # 방문하지 않은 정점만 큐에 삽입한다\n visited.add(adj_vet) # 해당 정점을 방문했다고 표시한다\n end\n end\n # 정점 순회 순서를 반환한다\n res\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>코드만으로는 추상적일 수 있으므로 아래 그림(9-10)과 함께 살펴보며 이해를 깊이 다져 보자.</p> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt;&lt;6&gt;&lt;7&gt;&lt;8&gt;&lt;9&gt;&lt;10&gt;&lt;11&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 9-10   그래프의 너비 우선 순회 단계 </p> <p>너비 우선 순회 순서는 유일한가?</p> <p>유일하지 않다. 너비 우선 순회는 '가까운 곳부터 먼 곳으로' 순회하기만 하면 되므로, 거리가 같은 여러 정점의 방문 순서는 자유롭게 바꿀 수 있다. 위 그림(9-10)에서는 정점 \\(1\\), \\(3\\)의 방문 순서를 바꿀 수 있고, 정점 \\(2\\), \\(4\\), \\(6\\)의 순서도 자유롭게 바꿀 수 있다.</p>","path":["제 9 장   그래프","9.3   그래프 순회"],"tags":[]},{"location":"chapter_graph/graph_traversal/#2","level":3,"title":"2.   복잡도 분석","text":"<p>시간 복잡도: 모든 정점은 큐에 한 번 들어갔다가 한 번 나오므로 \\(O(|V|)\\) 시간이 걸린다. 인접 정점을 순회할 때는 무방향 그래프이므로 모든 간선을 \\(2\\) 번씩 방문하여 \\(O(2|E|)\\) 시간이 걸린다. 전체 시간은 \\(O(|V| + |E|)\\)이다.</p> <p>공간 복잡도: 리스트 <code>res</code>, 해시 집합 <code>visited</code>, 큐 <code>que</code>에 저장되는 정점 수는 각각 최대 \\(|V|\\)이므로 \\(O(|V|)\\) 공간을 사용한다.</p>","path":["제 9 장   그래프","9.3   그래프 순회"],"tags":[]},{"location":"chapter_graph/graph_traversal/#932","level":2,"title":"9.3.2   깊이 우선 순회","text":"<p>깊이 우선 순회는 먼저 끝까지 나아가고, 더 갈 곳이 없으면 되돌아오는 순회 방식이다. 아래 그림(9-11)에서는 왼쪽 위 정점에서 출발하여 현재 정점의 인접 정점 하나를 방문한다. 끝에 도달하면 되돌아오고, 다시 끝까지 나아갔다가 돌아오는 과정을 모든 정점을 순회할 때까지 반복한다.</p> <p></p> <p> 그림 9-11   그래프의 깊이 우선 순회 </p>","path":["제 9 장   그래프","9.3   그래프 순회"],"tags":[]},{"location":"chapter_graph/graph_traversal/#1_1","level":3,"title":"1.   알고리즘 구현","text":"<p>'끝까지 갔다가 돌아오는' 알고리즘 패턴은 보통 재귀로 구현한다. 너비 우선 순회와 마찬가지로 깊이 우선 순회에서도 해시 집합 <code>visited</code>로 이미 방문한 정점을 기록하여 중복 방문을 방지한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_dfs.py<pre><code>def dfs(graph: GraphAdjList, visited: set[Vertex], res: list[Vertex], vet: Vertex):\n \"\"\"깊이 우선 순회 보조 함수\"\"\"\n res.append(vet) # 방문한 정점을 기록한다\n visited.add(vet) # 해당 정점을 방문했다고 표시한다\n # 해당 정점의 모든 인접 정점을 순회한다\n for adjVet in graph.adj_list[vet]:\n if adjVet in visited:\n continue # 이미 방문한 정점은 건너뛴다\n # 인접 정점을 재귀적으로 방문한다\n dfs(graph, visited, res, adjVet)\n\ndef graph_dfs(graph: GraphAdjList, start_vet: Vertex) -&gt; list[Vertex]:\n \"\"\"깊이 우선 순회\"\"\"\n # 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\n # 정점 순회 순서\n res = []\n # 방문한 정점을 기록하는 해시 집합\n visited = set[Vertex]()\n dfs(graph, visited, res, start_vet)\n return res\n</code></pre> graph_dfs.cpp<pre><code>/* 깊이 우선 순회 보조 함수 */\nvoid dfs(GraphAdjList &amp;graph, unordered_set&lt;Vertex *&gt; &amp;visited, vector&lt;Vertex *&gt; &amp;res, Vertex *vet) {\n res.push_back(vet); // 방문한 정점을 기록한다\n visited.emplace(vet); // 해당 정점을 방문했다고 표시한다\n // 해당 정점의 모든 인접 정점을 순회한다\n for (Vertex *adjVet : graph.adjList[vet]) {\n if (visited.count(adjVet))\n continue; // 이미 방문한 정점은 건너뛴다\n // 인접 정점을 재귀적으로 방문한다\n dfs(graph, visited, res, adjVet);\n }\n}\n\n/* 깊이 우선 순회 */\n// 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\nvector&lt;Vertex *&gt; graphDFS(GraphAdjList &amp;graph, Vertex *startVet) {\n // 정점 순회 순서\n vector&lt;Vertex *&gt; res;\n // 방문한 정점을 기록하는 해시 집합\n unordered_set&lt;Vertex *&gt; visited;\n dfs(graph, visited, res, startVet);\n return res;\n}\n</code></pre> graph_dfs.java<pre><code>/* 깊이 우선 순회 보조 함수 */\nvoid dfs(GraphAdjList graph, Set&lt;Vertex&gt; visited, List&lt;Vertex&gt; res, Vertex vet) {\n res.add(vet); // 방문한 정점을 기록한다\n visited.add(vet); // 해당 정점을 방문했다고 표시한다\n // 해당 정점의 모든 인접 정점을 순회한다\n for (Vertex adjVet : graph.adjList.get(vet)) {\n if (visited.contains(adjVet))\n continue; // 이미 방문한 정점은 건너뛴다\n // 인접 정점을 재귀적으로 방문한다\n dfs(graph, visited, res, adjVet);\n }\n}\n\n/* 깊이 우선 순회 */\n// 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\nList&lt;Vertex&gt; graphDFS(GraphAdjList graph, Vertex startVet) {\n // 정점 순회 순서\n List&lt;Vertex&gt; res = new ArrayList&lt;&gt;();\n // 방문한 정점을 기록하는 해시 집합\n Set&lt;Vertex&gt; visited = new HashSet&lt;&gt;();\n dfs(graph, visited, res, startVet);\n return res;\n}\n</code></pre> graph_dfs.cs<pre><code>/* 깊이 우선 순회 보조 함수 */\nvoid DFS(GraphAdjList graph, HashSet&lt;Vertex&gt; visited, List&lt;Vertex&gt; res, Vertex vet) {\n res.Add(vet); // 방문한 정점을 기록한다\n visited.Add(vet); // 해당 정점을 방문했다고 표시한다\n // 해당 정점의 모든 인접 정점을 순회한다\n foreach (Vertex adjVet in graph.adjList[vet]) {\n if (visited.Contains(adjVet)) {\n continue; // 이미 방문한 정점은 건너뛴다 \n }\n // 인접 정점을 재귀적으로 방문한다\n DFS(graph, visited, res, adjVet);\n }\n}\n\n/* 깊이 우선 순회 */\n// 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\nList&lt;Vertex&gt; GraphDFS(GraphAdjList graph, Vertex startVet) {\n // 정점 순회 순서\n List&lt;Vertex&gt; res = [];\n // 방문한 정점을 기록하는 해시 집합\n HashSet&lt;Vertex&gt; visited = [];\n DFS(graph, visited, res, startVet);\n return res;\n}\n</code></pre> graph_dfs.go<pre><code>/* 깊이 우선 순회 보조 함수 */\nfunc dfs(g *graphAdjList, visited map[Vertex]struct{}, res *[]Vertex, vet Vertex) {\n // append 연산은 새 참조를 반환하므로 원래 참조에 새 슬라이스의 참조를 다시 대입해야 한다\n *res = append(*res, vet)\n visited[vet] = struct{}{}\n // 해당 정점의 모든 인접 정점을 순회한다\n for _, adjVet := range g.adjList[vet] {\n _, isExist := visited[adjVet]\n // 인접 정점을 재귀적으로 방문한다\n if !isExist {\n dfs(g, visited, res, adjVet)\n }\n }\n}\n\n/* 깊이 우선 순회 */\n// 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\nfunc graphDFS(g *graphAdjList, startVet Vertex) []Vertex {\n // 정점 순회 순서\n res := make([]Vertex, 0)\n // 방문한 정점을 기록하는 해시 집합\n visited := make(map[Vertex]struct{})\n dfs(g, visited, &amp;res, startVet)\n // 정점 순회 순서를 반환한다\n return res\n}\n</code></pre> graph_dfs.swift<pre><code>/* 깊이 우선 순회 보조 함수 */\nfunc dfs(graph: GraphAdjList, visited: inout Set&lt;Vertex&gt;, res: inout [Vertex], vet: Vertex) {\n res.append(vet) // 방문한 정점을 기록한다\n visited.insert(vet) // 해당 정점을 방문했다고 표시한다\n // 해당 정점의 모든 인접 정점을 순회한다\n for adjVet in graph.adjList[vet] ?? [] {\n if visited.contains(adjVet) {\n continue // 이미 방문한 정점은 건너뛴다\n }\n // 인접 정점을 재귀적으로 방문한다\n dfs(graph: graph, visited: &amp;visited, res: &amp;res, vet: adjVet)\n }\n}\n\n/* 깊이 우선 순회 */\n// 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\nfunc graphDFS(graph: GraphAdjList, startVet: Vertex) -&gt; [Vertex] {\n // 정점 순회 순서\n var res: [Vertex] = []\n // 방문한 정점을 기록하는 해시 집합\n var visited: Set&lt;Vertex&gt; = []\n dfs(graph: graph, visited: &amp;visited, res: &amp;res, vet: startVet)\n return res\n}\n</code></pre> graph_dfs.js<pre><code>/* 깊이 우선 순회 */\n// 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\nfunction dfs(graph, visited, res, vet) {\n res.push(vet); // 방문한 정점을 기록한다\n visited.add(vet); // 해당 정점을 방문했다고 표시한다\n // 해당 정점의 모든 인접 정점을 순회한다\n for (const adjVet of graph.adjList.get(vet)) {\n if (visited.has(adjVet)) {\n continue; // 이미 방문한 정점은 건너뛴다\n }\n // 인접 정점을 재귀적으로 방문한다\n dfs(graph, visited, res, adjVet);\n }\n}\n\n/* 깊이 우선 순회 */\n// 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\nfunction graphDFS(graph, startVet) {\n // 정점 순회 순서\n const res = [];\n // 방문한 정점을 기록하는 해시 집합\n const visited = new Set();\n dfs(graph, visited, res, startVet);\n return res;\n}\n</code></pre> graph_dfs.ts<pre><code>/* 깊이 우선 순회 보조 함수 */\nfunction dfs(\n graph: GraphAdjList,\n visited: Set&lt;Vertex&gt;,\n res: Vertex[],\n vet: Vertex\n): void {\n res.push(vet); // 방문한 정점을 기록한다\n visited.add(vet); // 해당 정점을 방문했다고 표시한다\n // 해당 정점의 모든 인접 정점을 순회한다\n for (const adjVet of graph.adjList.get(vet)) {\n if (visited.has(adjVet)) {\n continue; // 이미 방문한 정점은 건너뛴다\n }\n // 인접 정점을 재귀적으로 방문한다\n dfs(graph, visited, res, adjVet);\n }\n}\n\n/* 깊이 우선 순회 */\n// 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\nfunction graphDFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {\n // 정점 순회 순서\n const res: Vertex[] = [];\n // 방문한 정점을 기록하는 해시 집합\n const visited: Set&lt;Vertex&gt; = new Set();\n dfs(graph, visited, res, startVet);\n return res;\n}\n</code></pre> graph_dfs.dart<pre><code>/* 깊이 우선 순회 보조 함수 */\nvoid dfs(\n GraphAdjList graph,\n Set&lt;Vertex&gt; visited,\n List&lt;Vertex&gt; res,\n Vertex vet,\n) {\n res.add(vet); // 방문한 정점을 기록한다\n visited.add(vet); // 해당 정점을 방문했다고 표시한다\n // 해당 정점의 모든 인접 정점을 순회한다\n for (Vertex adjVet in graph.adjList[vet]!) {\n if (visited.contains(adjVet)) {\n continue; // 이미 방문한 정점은 건너뛴다\n }\n // 인접 정점을 재귀적으로 방문한다\n dfs(graph, visited, res, adjVet);\n }\n}\n\n/* 깊이 우선 순회 */\nList&lt;Vertex&gt; graphDFS(GraphAdjList graph, Vertex startVet) {\n // 정점 순회 순서\n List&lt;Vertex&gt; res = [];\n // 방문한 정점을 기록하는 해시 집합\n Set&lt;Vertex&gt; visited = {};\n dfs(graph, visited, res, startVet);\n return res;\n}\n</code></pre> graph_dfs.rs<pre><code>/* 깊이 우선 순회 보조 함수 */\nfn dfs(graph: &amp;GraphAdjList, visited: &amp;mut HashSet&lt;Vertex&gt;, res: &amp;mut Vec&lt;Vertex&gt;, vet: Vertex) {\n res.push(vet); // 방문한 정점을 기록한다\n visited.insert(vet); // 해당 정점을 방문했다고 표시한다\n // 해당 정점의 모든 인접 정점을 순회한다\n if let Some(adj_vets) = graph.adj_list.get(&amp;vet) {\n for &amp;adj_vet in adj_vets {\n if visited.contains(&amp;adj_vet) {\n continue; // 이미 방문한 정점은 건너뛴다\n }\n // 인접 정점을 재귀적으로 방문한다\n dfs(graph, visited, res, adj_vet);\n }\n }\n}\n\n/* 깊이 우선 순회 */\n// 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\nfn graph_dfs(graph: GraphAdjList, start_vet: Vertex) -&gt; Vec&lt;Vertex&gt; {\n // 정점 순회 순서\n let mut res = vec![];\n // 방문한 정점을 기록하는 해시 집합\n let mut visited = HashSet::new();\n dfs(&amp;graph, &amp;mut visited, &amp;mut res, start_vet);\n\n res\n}\n</code></pre> graph_dfs.c<pre><code>/* 정점을 이미 방문했는지 확인 */\nint isVisited(Vertex **res, int size, Vertex *vet) {\n // O(n) 시간에 순회하여 노드를 찾는다\n for (int i = 0; i &lt; size; i++) {\n if (res[i] == vet) {\n return 1;\n }\n }\n return 0;\n}\n\n/* 깊이 우선 순회 보조 함수 */\nvoid dfs(GraphAdjList *graph, Vertex **res, int *resSize, Vertex *vet) {\n // 방문한 정점을 기록한다\n res[(*resSize)++] = vet;\n // 해당 정점의 모든 인접 정점을 순회한다\n AdjListNode *node = findNode(graph, vet);\n while (node != NULL) {\n // 이미 방문한 정점은 건너뛴다\n if (!isVisited(res, *resSize, node-&gt;vertex)) {\n // 인접 정점을 재귀적으로 방문한다\n dfs(graph, res, resSize, node-&gt;vertex);\n }\n node = node-&gt;next;\n }\n}\n\n/* 깊이 우선 순회 */\n// 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\nvoid graphDFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize) {\n dfs(graph, res, resSize, startVet);\n}\n</code></pre> graph_dfs.kt<pre><code>/* 깊이 우선 순회 보조 함수 */\nfun dfs(\n graph: GraphAdjList,\n visited: MutableSet&lt;Vertex?&gt;,\n res: MutableList&lt;Vertex?&gt;,\n vet: Vertex?\n) {\n res.add(vet) // 방문한 정점을 기록한다\n visited.add(vet) // 해당 정점을 방문했다고 표시한다\n // 해당 정점의 모든 인접 정점을 순회한다\n for (adjVet in graph.adjList[vet]!!) {\n if (visited.contains(adjVet))\n continue // 이미 방문한 정점은 건너뛴다\n // 인접 정점을 재귀적으로 방문한다\n dfs(graph, visited, res, adjVet)\n }\n}\n\n/* 깊이 우선 순회 */\n// 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\nfun graphDFS(graph: GraphAdjList, startVet: Vertex?): MutableList&lt;Vertex?&gt; {\n // 정점 순회 순서\n val res = mutableListOf&lt;Vertex?&gt;()\n // 방문한 정점을 기록하는 해시 집합\n val visited = HashSet&lt;Vertex?&gt;()\n dfs(graph, visited, res, startVet)\n return res\n}\n</code></pre> graph_dfs.rb<pre><code>### 깊이 우선 순회 보조 함수 ###\ndef dfs(graph, visited, res, vet)\n res &lt;&lt; vet # 방문한 정점을 기록한다\n visited.add(vet) # 해당 정점을 방문했다고 표시한다\n # 해당 정점의 모든 인접 정점을 순회한다\n for adj_vet in graph.adj_list[vet]\n next if visited.include?(adj_vet) # 이미 방문한 정점은 건너뛴다\n # 인접 정점을 재귀적으로 방문한다\n dfs(graph, visited, res, adj_vet)\n end\nend\n\n### 깊이 우선 순회 ###\ndef graph_dfs(graph, start_vet)\n # 지정한 정점의 모든 인접 정점을 얻기 쉽도록 그래프를 인접 리스트로 표현한다\n # 정점 순회 순서\n res = []\n # 방문한 정점을 기록하는 해시 집합\n visited = Set.new\n dfs(graph, visited, res, start_vet)\n res\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>깊이 우선 순회의 진행 과정은 아래 그림(9-12)과 같다.</p> <ul> <li>직선 점선은 아래로 내려가는 재귀 호출을 나타내며, 새 정점을 방문하기 위해 새로운 재귀 메서드를 호출했다는 뜻이다.</li> <li>곡선 점선은 위로 되돌아가는 백트래킹을 나타내며, 재귀 메서드가 반환되어 이를 호출한 위치로 돌아왔다는 뜻이다.</li> </ul> <p>이해를 깊이 다지기 위해 아래 그림(9-12)과 코드를 함께 보면서, 각 재귀 메서드가 언제 호출되고 언제 반환되는지까지 포함하여 전체 DFS 과정을 머릿속으로 따라가 보자(또는 직접 그려 보자).</p> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt;&lt;6&gt;&lt;7&gt;&lt;8&gt;&lt;9&gt;&lt;10&gt;&lt;11&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 9-12   그래프의 깊이 우선 순회 단계 </p> <p>깊이 우선 순회 순서는 유일한가?</p> <p>너비 우선 순회와 마찬가지로 깊이 우선 순회 순서도 유일하지 않다. 한 정점에서 어느 방향을 먼저 탐색해도 되므로, 인접 정점의 순서를 자유롭게 바꾸어도 모두 깊이 우선 순회에 해당한다.</p> <p>트리 순회를 예로 들면 '루트 \\(\\rightarrow\\) 왼쪽 \\(\\rightarrow\\) 오른쪽', '왼쪽 \\(\\rightarrow\\) 루트 \\(\\rightarrow\\) 오른쪽', '왼쪽 \\(\\rightarrow\\) 오른쪽 \\(\\rightarrow\\) 루트'는 각각 전위, 중위, 후위 순회에 해당한다. 세 방식은 방문 우선순위가 다르지만 모두 깊이 우선 순회다.</p>","path":["제 9 장   그래프","9.3   그래프 순회"],"tags":[]},{"location":"chapter_graph/graph_traversal/#2_1","level":3,"title":"2.   복잡도 분석","text":"<p>시간 복잡도: 모든 정점을 \\(1\\) 번씩 방문하는 데 \\(O(|V|)\\) 시간이 걸리고, 모든 간선을 \\(2\\) 번씩 방문하는 데 \\(O(2|E|)\\) 시간이 걸린다. 전체 시간은 \\(O(|V| + |E|)\\)이다.</p> <p>공간 복잡도: 리스트 <code>res</code>와 해시 집합 <code>visited</code>의 정점 수는 각각 최대 \\(|V|\\)이고, 재귀 깊이도 최대 \\(|V|\\)이므로 \\(O(|V|)\\) 공간을 사용한다.</p>","path":["제 9 장   그래프","9.3   그래프 순회"],"tags":[]},{"location":"chapter_graph/summary/","level":1,"title":"9.4   요약","text":"","path":["제 9 장   그래프","9.4   요약"],"tags":[]},{"location":"chapter_graph/summary/#1","level":3,"title":"1.   핵심 정리","text":"<ul> <li>그래프는 정점과 간선으로 이루어지며, 정점 집합과 간선 집합으로 표현할 수 있다.</li> <li>선형 관계(연결 리스트)와 분할 정복 관계(트리)에 비해 네트워크 관계(그래프)는 자유도가 높아 더 복잡하다.</li> <li>방향 그래프의 간선에는 방향이 있고, 연결 그래프에서는 모든 정점에 도달할 수 있으며, 가중 그래프의 각 간선에는 가중치 변수가 있다.</li> <li>인접 행렬은 행렬로 그래프를 표현한다. 각 행(열)은 정점 하나를, 행렬 원소는 간선을 나타내며, \\(1\\) 또는 \\(0\\)으로 두 정점 사이에 간선이 있는지 표시한다. 인접 행렬은 추가, 삭제, 조회, 수정 연산이 매우 효율적이지만 공간을 많이 차지한다.</li> <li>인접 리스트는 여러 연결 리스트로 그래프를 표현한다. \\(i\\) 번째 연결 리스트는 정점 \\(i\\)에 대응하고, 해당 정점의 모든 인접 정점을 저장한다. 인접 행렬보다 공간을 절약하지만, 간선을 찾으려면 연결 리스트를 순회해야 하므로 시간 효율이 낮다.</li> <li>인접 리스트 안의 연결 리스트가 너무 길면 레드-블랙 트리나 해시 테이블로 바꾸어 조회 효율을 높일 수 있다.</li> <li>알고리즘의 관점에서 보면 인접 행렬은 '공간을 더 써서 시간을 절약'하고, 인접 리스트는 '시간을 더 써서 공간을 절약'한다.</li> <li>그래프는 소셜 네트워크나 지하철 노선 등 다양한 현실 시스템을 모델링하는 데 사용할 수 있다.</li> <li>트리는 그래프의 특수한 경우이며, 트리 순회도 그래프 순회의 특수한 경우다.</li> <li>그래프의 너비 우선 순회는 가까운 곳에서 먼 곳으로 한 층씩 확장하는 탐색 방식이며, 보통 큐로 구현한다.</li> <li>그래프의 깊이 우선 순회는 먼저 끝까지 나아가고 더 갈 곳이 없으면 되돌아오는 탐색 방식이며, 보통 재귀로 구현한다.</li> </ul>","path":["제 9 장   그래프","9.4   요약"],"tags":[]},{"location":"chapter_graph/summary/#2-q-a","level":3,"title":"2.   Q &amp; A","text":"<p>Q: 경로는 정점의 순서로 정의하는가, 간선의 순서로 정의하는가?</p> <p>위키백과의 언어별 정의는 서로 다르다. 영어판에서는 '경로는 간선의 순서'라고 하고, 중국어판에서는 '경로는 정점의 순서'라고 한다. 영어판 원문을 옮기면 다음과 같다. '그래프 이론에서 그래프의 경로는 정점의 나열을 연결하는 간선의 유한하거나 무한한 나열이다.'</p> <p>이 절에서는 경로를 정점의 순서가 아니라 간선의 순서로 본다. 두 정점 사이에 여러 간선이 연결될 수 있으며, 이때 각 간선은 각각 하나의 경로에 해당하기 때문이다.</p> <p>Q: 비연결 그래프에는 순회할 수 없는 정점이 있는가?</p> <p>비연결 그래프에서는 한 정점에서 출발했을 때 도달할 수 없는 정점이 하나 이상 존재한다. 그래프의 모든 연결 요소를 순회하려면 시작점을 여러 개 설정해야 한다.</p> <p>Q: 인접 리스트에서 '해당 정점과 연결된 모든 정점'의 순서에 제약이 있는가?</p> <p>순서는 자유롭다. 하지만 실제 응용에서는 지정한 규칙에 따라 정렬해야 할 수도 있다. 예를 들어 정점이 추가된 순서나 정점 값의 크기순으로 정렬하면, '특정 기준에서 최댓값이나 최솟값을 가진' 정점을 빠르게 찾는 데 도움이 된다.</p>","path":["제 9 장   그래프","9.4   요약"],"tags":[]},{"location":"chapter_greedy/","level":1,"title":"제 15 장   탐욕","text":"<p>이 장을 시작하며</p> <p>해바라기는 태양을 따라 돌며 매 순간 자신의 성장 가능성을 최대한 추구한다.</p> <p>탐욕 전략은 단순한 선택을 거듭하면서 차츰 최선의 답으로 나아간다.</p>","path":["제 15 장   탐욕"],"tags":[]},{"location":"chapter_greedy/#_1","level":2,"title":"이 장의 내용","text":"<ul> <li>15.1   탐욕 알고리즘</li> <li>15.2   분할 가능한 배낭 문제</li> <li>15.3   최대 용량 문제</li> <li>15.4   정수 분할의 최대 곱 문제</li> <li>15.5   요약</li> <li>15.6   연습 문제</li> </ul>","path":["제 15 장   탐욕"],"tags":[]},{"location":"chapter_greedy/exercises/","level":1,"title":"15.6   연습 문제","text":"","path":["제 15 장   탐욕","15.6   연습 문제"],"tags":[]},{"location":"chapter_greedy/exercises/#1561","level":2,"title":"15.6.1   개념 확인","text":"","path":["제 15 장   탐욕","15.6   연습 문제"],"tags":[]},{"location":"chapter_greedy/exercises/#1","level":3,"title":"1.   매번 가장 큰 동전을 고르면 항상 최선인가","text":"<p>동전의 액면가는 <code>[1, 7, 10]</code>이며, 목표 금액은 14다.</p> <ol> <li>‘남은 금액을 넘지 않는 가장 큰 액면가의 동전을 매번 고른다’는 규칙을 실행하고 선택한 동전을 적어 보자.</li> <li>동전을 더 적게 사용하는 방법이 있는가? 있으면 하나를 적고, 없으면 이유를 설명하라.</li> <li>이 결과로 해당 탐욕 전략이 어떤 동전 액면가에서든 올바르다고 할 수 있는가?</li> </ol> 참고 답안 <ol> <li> <p>탐욕 방식은 <code>10 + 1 + 1 + 1 + 1</code>을 차례대로 선택하여 총 5개의 동전을 사용한다.</p> </li> <li> <p>더 적게 사용하는 방법이 있다. <code>7 + 7</code>이면 동전 2개만 필요하다.</p> </li> <li> <p>그럴 수 없다. 이 반례는 임의의 동전 액면가에서 현재 고를 수 있는 가장 큰 액면가를 매번 선택한다고 해서 반드시 최소 동전 개수를 얻는 것은 아님을 보여 준다. 당장 가장 큰 것을 선택하면 이후에 더 좋은 조합을 만들 기회를 잃을 수 있다.</p> </li> </ol>","path":["제 15 장   탐욕","15.6   연습 문제"],"tags":[]},{"location":"chapter_greedy/exercises/#2","level":3,"title":"2.   배낭에 어떤 물건을 먼저 넣어야 하는가","text":"<p>용량이 4킬로그램인 배낭에 다음 물건을 넣을 수 있다. 물건의 일부만 넣어도 되며, 얻는 가치는 넣은 무게에 비례한다.</p> <ul> <li>물건 A: 무게 4킬로그램, 가치 20</li> <li>물건 B: 무게 3킬로그램, 가치 18</li> </ul> <ol> <li>두 물건의 킬로그램당 가치는 각각 얼마인가? 어느 물건을 먼저 넣어야 하는가?</li> <li>분할 가능한 배낭 문제의 탐욕 전략으로 배낭을 가득 채우면 최종 가치는 얼마인가?</li> <li>물건을 나눌 수 있고 배낭이 총무게를 제한하는 상황에서는 물건을 고를 때 총가치와 킬로그램당 가치 중 무엇을 비교해야 하는가? 이유도 설명하라.</li> </ol> 참고 답안 <ol> <li> <p>A의 킬로그램당 가치는 <code>20 ÷ 4 = 5</code>, B의 킬로그램당 가치는 <code>18 ÷ 3 = 6</code>이다. 따라서 단위 무게당 가치가 더 높은 B를 먼저 넣어야 한다.</p> </li> <li> <p>먼저 B를 전부 넣으면 3킬로그램을 차지하고 가치 18을 얻는다. 남은 용량 1킬로그램에는 A를 1킬로그램 넣어 가치 5를 얻는다. 최종 가치는 <code>18 + 5 = 23</code>이다.</p> </li> <li> <p>배낭은 총무게를 제한하고 물건을 나눌 수 있으므로 단위 무게당 가치를 비교해야 한다. A는 총가치가 더 높지만 킬로그램당 가치는 B보다 낮다. A부터 넣어 배낭을 채우면 가치 20밖에 얻지 못한다.</p> </li> </ol>","path":["제 15 장   탐욕","15.6   연습 문제"],"tags":[]},{"location":"chapter_greedy/exercises/#3","level":3,"title":"3.   두 포인터를 다음에 어떻게 움직여야 하는가","text":"<p>칸막이 높이가 <code>[1, 8, 6, 2, 5]</code>일 때 양 끝에서 시작하는 두 포인터로 최대 용량을 찾는다. 용량은 ‘두 칸막이 중 더 낮은 높이 × 두 칸막이의 인덱스 차이’다.</p> <ol> <li>처음에는 왼쪽 포인터가 인덱스 0, 오른쪽 포인터가 인덱스 4에 있다. 현재 용량은 얼마인가? 다음에는 어느 포인터를 움직여야 하는가?</li> <li>1번에서 선택한 대로 한 번 움직이면 두 포인터는 각각 어느 인덱스에 있는가? 이때 용량은 얼마이며, 다음에는 어느 포인터를 움직여야 하는가?</li> <li>현재 두 칸막이에서 더 낮은 쪽의 포인터를 움직일 수도 있고, 더 높은 쪽의 포인터를 움직일 수도 있다. 어느 쪽을 움직여야 더 큰 용량을 얻을 가능성이 남는가? 이유는 무엇인가?</li> </ol> 참고 답안 <ol> <li> <p>현재 용량은 <code>min(1, 5) × (4 - 0) = 4</code>다. 왼쪽 칸막이가 더 낮으므로 왼쪽 포인터를 움직인다.</p> </li> <li> <p>왼쪽 포인터를 움직이면 두 포인터는 각각 인덱스 1과 4에 있다. 현재 용량은 <code>min(8, 5) × (4 - 1) = 15</code>다. 오른쪽 칸막이가 더 낮으므로 다음에는 오른쪽 포인터를 움직인다.</p> </li> <li> <p>더 낮은 칸막이 쪽의 포인터를 움직여야 더 큰 용량을 얻을 가능성이 남는다. 더 높은 칸막이를 움직이면 두 칸막이 사이의 거리는 반드시 줄어드는 반면, 용기의 높이는 여전히 움직이지 않은 낮은 칸막이에 의해 제한되므로 그대로이거나 더 낮아질 뿐이다. 따라서 용량은 움직이기 전보다 커질 수 없다. 낮은 칸막이를 움직여야만 더 높은 칸막이를 만날 기회가 생긴다.</p> </li> </ol>","path":["제 15 장   탐욕","15.6   연습 문제"],"tags":[]},{"location":"chapter_greedy/exercises/#1562","level":2,"title":"15.6.2   프로그래밍 연습","text":"","path":["제 15 장   탐욕","15.6   연습 문제"],"tags":[]},{"location":"chapter_greedy/exercises/#1_1","level":3,"title":"1.   분할 가능한 배낭 문제","text":"<p>길이가 같은 배열 <code>wgt</code>와 <code>val</code>이 주어진다. <code>wgt[i] &gt; 0</code>, <code>val[i] &gt;= 0</code>이며 배낭 용량은 <code>cap &gt;= 0</code>이다. 각 종류의 물건은 하나씩만 있지만, 그중 일부만 넣어도 된다. 얻는 가치는 넣은 무게가 해당 물건의 총무게에서 차지하는 비율로 계산한다. 탐욕 알고리즘을 사용하여 배낭에서 얻을 수 있는 최대 총가치를 실수로 반환하라.</p> 풀이 힌트 <ol> <li>먼저 각 물건의 단위 무게당 가치 val[i] / wgt[i]를 계산한다. 나눗셈 결과의 소수 부분을 유지해야 한다.</li> <li>단위 무게당 가치가 높은 물건부터 배낭에 넣는다.</li> <li>남은 용량이 현재 물건의 무게보다 작으면 배낭을 정확히 채우는 만큼만 넣고 끝낸다.</li> </ol>","path":["제 15 장   탐욕","15.6   연습 문제"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/","level":1,"title":"15.2   분할 가능한 배낭 문제","text":"<p>문제</p> <p>물건 \\(n\\) 개와 용량이 \\(cap\\)인 배낭이 주어진다. \\(i\\) 번째 물건의 무게는 \\(wgt[i-1]\\), 가치는 \\(val[i-1]\\)이다. 각 물건은 한 번만 선택할 수 있지만, 일부만 선택할 수 있으며 가치는 선택한 무게의 비율에 따라 계산한다. 용량 제한 안에서 배낭에 담을 수 있는 최대 가치를 구하라. 예시는 아래 그림(15-3)과 같다.</p> <p></p> <p> 그림 15-3   분할 가능한 배낭 문제 예시 데이터 </p> <p>분할 가능한 배낭 문제는 전체적으로 0-1 배낭 문제와 매우 비슷하다. 상태는 현재 물건 \\(i\\)와 용량 \\(c\\)를 포함하며, 목표는 용량 제한 안에서 최대 가치를 구하는 것이다.</p> <p>차이점은 물건의 일부만 선택할 수 있다는 점이다. 아래 그림(15-4)과 같이 물건을 원하는 대로 나누고 무게 비율에 따라 그 가치를 계산할 수 있다.</p> <ol> <li>물건 \\(i\\)의 단위 무게당 가치는 \\(val[i-1] / wgt[i-1]\\)이며, 줄여서 단위 가치라고 한다.</li> <li>물건 \\(i\\)의 일부를 무게 \\(w\\)만큼 넣으면 배낭의 가치는 \\(w \\times val[i-1] / wgt[i-1]\\)만큼 늘어난다.</li> </ol> <p></p> <p> 그림 15-4   물건의 단위 무게당 가치 </p>","path":["제 15 장   탐욕","15.2   분할 가능한 배낭 문제"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#1","level":3,"title":"1.   탐욕 전략 결정","text":"<p>배낭 안의 총가치를 최대화하는 것은 본질적으로 단위 무게당 물건 가치를 최대화하는 것이다. 여기서 아래 그림(15-5)의 탐욕 전략을 도출할 수 있다.</p> <ol> <li>물건을 단위 가치가 높은 순서로 정렬한다.</li> <li>모든 물건을 순회하며 매번 단위 가치가 가장 높은 물건을 탐욕적으로 선택한다.</li> <li>남은 용량이 부족하면 현재 물건의 일부로 배낭을 가득 채운다.</li> </ol> <p></p> <p> 그림 15-5   분할 가능한 배낭 문제의 탐욕 전략 </p>","path":["제 15 장   탐욕","15.2   분할 가능한 배낭 문제"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#2","level":3,"title":"2.   코드 구현","text":"<p>단위 가치순으로 정렬하기 쉽도록 물건 클래스 <code>Item</code>을 만든다. 탐욕적 선택을 반복하다가 배낭이 가득 차면 반복문을 빠져나와 해를 반환한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby fractional_knapsack.py<pre><code>class Item:\n \"\"\"물건\"\"\"\n\n def __init__(self, w: int, v: int):\n self.w = w # 물건의 무게\n self.v = v # 물건의 가치\n\ndef fractional_knapsack(wgt: list[int], val: list[int], cap: int) -&gt; int:\n \"\"\"분할 가능한 배낭: 탐욕\"\"\"\n # 무게와 가치 두 속성을 갖는 물건의 리스트를 생성한다\n items = [Item(w, v) for w, v in zip(wgt, val)]\n # 단위 가치 item.v / item.w를 기준으로 내림차순 정렬한다\n items.sort(key=lambda item: item.v / item.w, reverse=True)\n # 탐욕적 선택을 반복한다\n res = 0\n for item in items:\n if item.w &lt;= cap:\n # 남은 용량이 충분하면 현재 물건을 통째로 배낭에 넣는다\n res += item.v\n cap -= item.w\n else:\n # 남은 용량이 부족하면 현재 물건의 일부를 배낭에 넣는다\n res += (item.v / item.w) * cap\n # 남은 용량이 없으므로 반복문을 종료한다\n break\n return res\n</code></pre> fractional_knapsack.cpp<pre><code>/* 물건 */\nclass Item {\n public:\n int w; // 물건의 무게\n int v; // 물건의 가치\n\n Item(int w, int v) : w(w), v(v) {\n }\n};\n\n/* 분할 가능한 배낭: 탐욕 */\ndouble fractionalKnapsack(vector&lt;int&gt; &amp;wgt, vector&lt;int&gt; &amp;val, int cap) {\n // 무게와 가치 두 속성을 갖는 물건의 리스트를 생성한다\n vector&lt;Item&gt; items;\n for (int i = 0; i &lt; wgt.size(); i++) {\n items.push_back(Item(wgt[i], val[i]));\n }\n // 단위 가치 item.v / item.w를 기준으로 내림차순 정렬한다\n sort(items.begin(), items.end(), [](Item &amp;a, Item &amp;b) { return (double)a.v / a.w &gt; (double)b.v / b.w; });\n // 탐욕적 선택을 반복한다\n double res = 0;\n for (auto &amp;item : items) {\n if (item.w &lt;= cap) {\n // 남은 용량이 충분하면 현재 물건을 통째로 배낭에 넣는다\n res += item.v;\n cap -= item.w;\n } else {\n // 남은 용량이 부족하면 현재 물건의 일부를 배낭에 넣는다\n res += (double)item.v / item.w * cap;\n // 남은 용량이 없으므로 반복문을 종료한다\n break;\n }\n }\n return res;\n}\n</code></pre> fractional_knapsack.java<pre><code>/* 물건 */\nclass Item {\n int w; // 물건의 무게\n int v; // 물건의 가치\n\n public Item(int w, int v) {\n this.w = w;\n this.v = v;\n }\n}\n\n/* 분할 가능한 배낭: 탐욕 */\ndouble fractionalKnapsack(int[] wgt, int[] val, int cap) {\n // 무게와 가치 두 속성을 갖는 물건의 리스트를 생성한다\n Item[] items = new Item[wgt.length];\n for (int i = 0; i &lt; wgt.length; i++) {\n items[i] = new Item(wgt[i], val[i]);\n }\n // 단위 가치 item.v / item.w를 기준으로 내림차순 정렬한다\n Arrays.sort(items, Comparator.comparingDouble(item -&gt; -((double) item.v / item.w)));\n // 탐욕적 선택을 반복한다\n double res = 0;\n for (Item item : items) {\n if (item.w &lt;= cap) {\n // 남은 용량이 충분하면 현재 물건을 통째로 배낭에 넣는다\n res += item.v;\n cap -= item.w;\n } else {\n // 남은 용량이 부족하면 현재 물건의 일부를 배낭에 넣는다\n res += (double) item.v / item.w * cap;\n // 남은 용량이 없으므로 반복문을 종료한다\n break;\n }\n }\n return res;\n}\n</code></pre> fractional_knapsack.cs<pre><code>/* 물건 */\nclass Item(int w, int v) {\n public int w = w; // 물건의 무게\n public int v = v; // 물건의 가치\n}\n\n/* 분할 가능한 배낭: 탐욕 */\ndouble FractionalKnapsack(int[] wgt, int[] val, int cap) {\n // 무게와 가치 두 속성을 갖는 물건의 리스트를 생성한다\n Item[] items = new Item[wgt.Length];\n for (int i = 0; i &lt; wgt.Length; i++) {\n items[i] = new Item(wgt[i], val[i]);\n }\n // 단위 가치 item.v / item.w를 기준으로 내림차순 정렬한다\n Array.Sort(items, (x, y) =&gt; (y.v / y.w).CompareTo(x.v / x.w));\n // 탐욕적 선택을 반복한다\n double res = 0;\n foreach (Item item in items) {\n if (item.w &lt;= cap) {\n // 남은 용량이 충분하면 현재 물건을 통째로 배낭에 넣는다\n res += item.v;\n cap -= item.w;\n } else {\n // 남은 용량이 부족하면 현재 물건의 일부를 배낭에 넣는다\n res += (double)item.v / item.w * cap;\n // 남은 용량이 없으므로 반복문을 종료한다\n break;\n }\n }\n return res;\n}\n</code></pre> fractional_knapsack.go<pre><code>/* 물건 */\ntype Item struct {\n w int // 물건의 무게\n v int // 물건의 가치\n}\n\n/* 분할 가능한 배낭: 탐욕 */\nfunc fractionalKnapsack(wgt []int, val []int, cap int) float64 {\n // 무게와 가치 두 속성을 갖는 물건의 리스트를 생성한다\n items := make([]Item, len(wgt))\n for i := 0; i &lt; len(wgt); i++ {\n items[i] = Item{wgt[i], val[i]}\n }\n // 단위 가치 item.v / item.w를 기준으로 내림차순 정렬한다\n sort.Slice(items, func(i, j int) bool {\n return float64(items[i].v)/float64(items[i].w) &gt; float64(items[j].v)/float64(items[j].w)\n })\n // 탐욕적 선택을 반복한다\n res := 0.0\n for _, item := range items {\n if item.w &lt;= cap {\n // 남은 용량이 충분하면 현재 물건을 통째로 배낭에 넣는다\n res += float64(item.v)\n cap -= item.w\n } else {\n // 남은 용량이 부족하면 현재 물건의 일부를 배낭에 넣는다\n res += float64(item.v) / float64(item.w) * float64(cap)\n // 남은 용량이 없으므로 반복문을 종료한다\n break\n }\n }\n return res\n}\n</code></pre> fractional_knapsack.swift<pre><code>/* 물건 */\nclass Item {\n var w: Int // 물건의 무게\n var v: Int // 물건의 가치\n\n init(w: Int, v: Int) {\n self.w = w\n self.v = v\n }\n}\n\n/* 분할 가능한 배낭: 탐욕 */\nfunc fractionalKnapsack(wgt: [Int], val: [Int], cap: Int) -&gt; Double {\n // 무게와 가치 두 속성을 갖는 물건의 리스트를 생성한다\n var items = zip(wgt, val).map { Item(w: $0, v: $1) }\n // 단위 가치 item.v / item.w를 기준으로 내림차순 정렬한다\n items.sort { -(Double($0.v) / Double($0.w)) &lt; -(Double($1.v) / Double($1.w)) }\n // 탐욕적 선택을 반복한다\n var res = 0.0\n var cap = cap\n for item in items {\n if item.w &lt;= cap {\n // 남은 용량이 충분하면 현재 물건을 통째로 배낭에 넣는다\n res += Double(item.v)\n cap -= item.w\n } else {\n // 남은 용량이 부족하면 현재 물건의 일부를 배낭에 넣는다\n res += Double(item.v) / Double(item.w) * Double(cap)\n // 남은 용량이 없으므로 반복문을 종료한다\n break\n }\n }\n return res\n}\n</code></pre> fractional_knapsack.js<pre><code>/* 물건 */\nclass Item {\n constructor(w, v) {\n this.w = w; // 물건의 무게\n this.v = v; // 물건의 가치\n }\n}\n\n/* 분할 가능한 배낭: 탐욕 */\nfunction fractionalKnapsack(wgt, val, cap) {\n // 무게와 가치 두 속성을 갖는 물건의 리스트를 생성한다\n const items = wgt.map((w, i) =&gt; new Item(w, val[i]));\n // 단위 가치 item.v / item.w를 기준으로 내림차순 정렬한다\n items.sort((a, b) =&gt; b.v / b.w - a.v / a.w);\n // 탐욕적 선택을 반복한다\n let res = 0;\n for (const item of items) {\n if (item.w &lt;= cap) {\n // 남은 용량이 충분하면 현재 물건을 통째로 배낭에 넣는다\n res += item.v;\n cap -= item.w;\n } else {\n // 남은 용량이 부족하면 현재 물건의 일부를 배낭에 넣는다\n res += (item.v / item.w) * cap;\n // 남은 용량이 없으므로 반복문을 종료한다\n break;\n }\n }\n return res;\n}\n</code></pre> fractional_knapsack.ts<pre><code>/* 물건 */\nclass Item {\n w: number; // 물건의 무게\n v: number; // 물건의 가치\n\n constructor(w: number, v: number) {\n this.w = w;\n this.v = v;\n }\n}\n\n/* 분할 가능한 배낭: 탐욕 */\nfunction fractionalKnapsack(wgt: number[], val: number[], cap: number): number {\n // 무게와 가치 두 속성을 갖는 물건의 리스트를 생성한다\n const items: Item[] = wgt.map((w, i) =&gt; new Item(w, val[i]));\n // 단위 가치 item.v / item.w를 기준으로 내림차순 정렬한다\n items.sort((a, b) =&gt; b.v / b.w - a.v / a.w);\n // 탐욕적 선택을 반복한다\n let res = 0;\n for (const item of items) {\n if (item.w &lt;= cap) {\n // 남은 용량이 충분하면 현재 물건을 통째로 배낭에 넣는다\n res += item.v;\n cap -= item.w;\n } else {\n // 남은 용량이 부족하면 현재 물건의 일부를 배낭에 넣는다\n res += (item.v / item.w) * cap;\n // 남은 용량이 없으므로 반복문을 종료한다\n break;\n }\n }\n return res;\n}\n</code></pre> fractional_knapsack.dart<pre><code>/* 물건 */\nclass Item {\n int w; // 물건의 무게\n int v; // 물건의 가치\n\n Item(this.w, this.v);\n}\n\n/* 분할 가능한 배낭: 탐욕 */\ndouble fractionalKnapsack(List&lt;int&gt; wgt, List&lt;int&gt; val, int cap) {\n // 무게와 가치 두 속성을 갖는 물건의 리스트를 생성한다\n List&lt;Item&gt; items = List.generate(wgt.length, (i) =&gt; Item(wgt[i], val[i]));\n // 단위 가치 item.v / item.w를 기준으로 내림차순 정렬한다\n items.sort((a, b) =&gt; (b.v / b.w).compareTo(a.v / a.w));\n // 탐욕적 선택을 반복한다\n double res = 0;\n for (Item item in items) {\n if (item.w &lt;= cap) {\n // 남은 용량이 충분하면 현재 물건을 통째로 배낭에 넣는다\n res += item.v;\n cap -= item.w;\n } else {\n // 남은 용량이 부족하면 현재 물건의 일부를 배낭에 넣는다\n res += item.v / item.w * cap;\n // 남은 용량이 없으므로 반복문을 종료한다\n break;\n }\n }\n return res;\n}\n</code></pre> fractional_knapsack.rs<pre><code>/* 물건 */\nstruct Item {\n w: i32, // 물건의 무게\n v: i32, // 물건의 가치\n}\n\nimpl Item {\n fn new(w: i32, v: i32) -&gt; Self {\n Self { w, v }\n }\n}\n\n/* 분할 가능한 배낭: 탐욕 */\nfn fractional_knapsack(wgt: &amp;[i32], val: &amp;[i32], mut cap: i32) -&gt; f64 {\n // 무게와 가치 두 속성을 갖는 물건의 리스트를 생성한다\n let mut items = wgt\n .iter()\n .zip(val.iter())\n .map(|(&amp;w, &amp;v)| Item::new(w, v))\n .collect::&lt;Vec&lt;Item&gt;&gt;();\n // 단위 가치 item.v / item.w를 기준으로 내림차순 정렬한다\n items.sort_by(|a, b| {\n (b.v as f64 / b.w as f64)\n .partial_cmp(&amp;(a.v as f64 / a.w as f64))\n .unwrap()\n });\n // 탐욕적 선택을 반복한다\n let mut res = 0.0;\n for item in &amp;items {\n if item.w &lt;= cap {\n // 남은 용량이 충분하면 현재 물건을 통째로 배낭에 넣는다\n res += item.v as f64;\n cap -= item.w;\n } else {\n // 남은 용량이 부족하면 현재 물건의 일부를 배낭에 넣는다\n res += item.v as f64 / item.w as f64 * cap as f64;\n // 남은 용량이 없으므로 반복문을 종료한다\n break;\n }\n }\n res\n}\n</code></pre> fractional_knapsack.c<pre><code>/* 물건 */\ntypedef struct {\n int w; // 물건의 무게\n int v; // 물건의 가치\n} Item;\n\n/* 분할 가능한 배낭: 탐욕 */\nfloat fractionalKnapsack(int wgt[], int val[], int itemCount, int cap) {\n // 무게와 가치 두 속성을 갖는 물건의 리스트를 생성한다\n Item *items = malloc(sizeof(Item) * itemCount);\n for (int i = 0; i &lt; itemCount; i++) {\n items[i] = (Item){.w = wgt[i], .v = val[i]};\n }\n // 단위 가치 item.v / item.w를 기준으로 내림차순 정렬한다\n qsort(items, (size_t)itemCount, sizeof(Item), sortByValueDensity);\n // 탐욕적 선택을 반복한다\n float res = 0.0;\n for (int i = 0; i &lt; itemCount; i++) {\n if (items[i].w &lt;= cap) {\n // 남은 용량이 충분하면 현재 물건을 통째로 배낭에 넣는다\n res += items[i].v;\n cap -= items[i].w;\n } else {\n // 남은 용량이 부족하면 현재 물건의 일부를 배낭에 넣는다\n res += (float)cap / items[i].w * items[i].v;\n cap = 0;\n break;\n }\n }\n free(items);\n return res;\n}\n</code></pre> fractional_knapsack.kt<pre><code>/* 물건 */\nclass Item(\n val w: Int, // 물건\n val v: Int // 물건의 가치\n)\n\n/* 분할 가능한 배낭: 탐욕 */\nfun fractionalKnapsack(wgt: IntArray, _val: IntArray, c: Int): Double {\n // 무게와 가치 두 속성을 갖는 물건의 리스트를 생성한다\n var cap = c\n val items = arrayOfNulls&lt;Item&gt;(wgt.size)\n for (i in wgt.indices) {\n items[i] = Item(wgt[i], _val[i])\n }\n // 단위 가치 item.v / item.w를 기준으로 내림차순 정렬한다\n items.sortBy { item: Item? -&gt; -(item!!.v.toDouble() / item.w) }\n // 탐욕적 선택을 반복한다\n var res = 0.0\n for (item in items) {\n if (item!!.w &lt;= cap) {\n // 남은 용량이 충분하면 현재 물건을 통째로 배낭에 넣는다\n res += item.v\n cap -= item.w\n } else {\n // 남은 용량이 부족하면 현재 물건의 일부를 배낭에 넣는다\n res += item.v.toDouble() / item.w * cap\n // 남은 용량이 없으므로 반복문을 종료한다\n break\n }\n }\n return res\n}\n</code></pre> fractional_knapsack.rb<pre><code>### 물건 ###\nclass Item\n attr_accessor :w # 물건의 무게\n attr_accessor :v # 물건의 가치\n\n def initialize(w, v)\n @w = w\n @v = v\n end\nend\n\n### 분할 가능한 배낭: 탐욕 ###\ndef fractional_knapsack(wgt, val, cap)\n # 무게와 가치 두 속성을 갖는 물건의 리스트를 생성한다\n items = wgt.each_with_index.map { |w, i| Item.new(w, val[i]) }\n # 단위 가치 item.v / item.w를 기준으로 내림차순 정렬한다\n items.sort! { |a, b| (b.v.to_f / b.w) &lt;=&gt; (a.v.to_f / a.w) }\n # 탐욕적 선택을 반복한다\n res = 0\n for item in items\n if item.w &lt;= cap\n # 남은 용량이 충분하면 현재 물건을 통째로 배낭에 넣는다\n res += item.v\n cap -= item.w\n else\n # 남은 용량이 부족하면 현재 물건의 일부를 배낭에 넣는다\n res += (item.v.to_f / item.w) * cap\n # 남은 용량이 없으므로 반복문을 종료한다\n break\n end\n end\n res\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>내장 정렬 알고리즘의 시간 복잡도는 보통 \\(O(n \\log n)\\)이고, 공간 복잡도는 언어의 구체적인 구현에 따라 대체로 \\(O(\\log n)\\) 또는 \\(O(n)\\)이다.</p> <p>정렬을 제외하면 최악의 경우 물건 리스트 전체를 순회해야 하므로 시간 복잡도는 \\(O(n)\\)이다. 여기서 \\(n\\)은 물건 수다.</p> <p><code>Item</code> 객체 리스트를 초기화하므로 공간 복잡도는 \\(O(n)\\)이다.</p>","path":["제 15 장   탐욕","15.2   분할 가능한 배낭 문제"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#3","level":3,"title":"3.   정확성 증명","text":"<p>귀류법을 사용하자. 물건 \\(x\\)의 단위 가치가 가장 높고, 어떤 알고리즘으로 구한 최대 가치가 <code>res</code>인데 그 해에 \\(x\\)가 없다고 가정한다.</p> <p>배낭에서 임의의 물건을 단위 무게만큼 꺼내고 같은 무게의 물건 \\(x\\)로 바꾼다. \\(x\\)의 단위 가치가 가장 높으므로 바꾼 뒤의 총가치는 반드시 <code>res</code>보다 커진다. 이는 <code>res</code>가 최적해라는 가정에 모순이므로 최적해에는 물건 \\(x\\)가 포함되어야 한다.</p> <p>그 해의 다른 물건에도 같은 모순을 구성할 수 있다. 즉, 단위 가치가 더 높은 물건이 항상 더 좋은 선택이며, 탐욕 전략이 유효함을 알 수 있다.</p> <p>아래 그림(15-6)과 같이 물건 무게를 가로축, 단위 가치를 세로축으로 보면, 분할 가능한 배낭 문제는 '제한된 가로축 구간에서 최대 넓이를 구하는' 문제로 바뀐다. 이 비유는 탐욕 전략의 유효성을 기하학적으로 이해하는 데 도움이 된다.</p> <p></p> <p> 그림 15-6   분할 가능한 배낭 문제의 기하학적 표현 </p>","path":["제 15 장   탐욕","15.2   분할 가능한 배낭 문제"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/","level":1,"title":"15.1   탐욕 알고리즘","text":"<p>탐욕 알고리즘(greedy algorithm)은 최적화 문제를 푸는 대표적인 알고리즘이다. 매 결정 단계에서 현재 가장 좋아 보이는 선택, 즉 국소적으로 최적인 결정을 탐욕적으로 내려 전역 최적해를 얻으려는 것이 기본 아이디어다. 간결하고 효율적이어서 많은 실제 문제에 널리 쓰인다.</p> <p>탐욕 알고리즘과 동적 계획법은 모두 최적화 문제에 자주 사용한다. 최적 부분 구조에 의존한다는 공통점 등이 있지만 작동 원리는 다르다.</p> <ul> <li>동적 계획법은 이전 단계의 모든 결정을 바탕으로 현재 결정을 고려하고, 과거 부분 문제의 해로 현재 부분 문제의 해를 구성한다.</li> <li>탐욕 알고리즘은 과거 결정을 고려하지 않고 계속 앞으로 나아가며 탐욕적으로 선택한다. 문제의 범위를 점차 줄여 해결될 때까지 진행한다.</li> </ul> <p>먼저 '동전 교환' 예제로 탐욕 알고리즘의 원리를 알아보자. '무한 배낭 문제' 절에서 소개한 문제라 익숙할 것이다.</p> <p>문제</p> <p>동전 \\(n\\) 종류가 주어지며 \\(i\\) 번째 동전의 액면가는 \\(coins[i - 1]\\)이고 목표 금액은 \\(amt\\)다. 각 종류의 동전을 반복해서 선택할 수 있을 때 목표 금액을 만드는 최소 동전 수를 구하라. 만들 수 없다면 \\(-1\\)을 반환하라.</p> <p>이 문제에서 사용하는 탐욕 전략은 아래 그림(15-1)과 같다. 주어진 목표 금액에 대해 그 금액을 넘지 않으면서 가장 가까운 액면가의 동전을 탐욕적으로 고르고, 목표 금액을 만들 때까지 반복한다.</p> <p></p> <p> 그림 15-1   동전 교환의 탐욕 전략 </p> <p>구현 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_greedy.py<pre><code>def coin_change_greedy(coins: list[int], amt: int) -&gt; int:\n \"\"\"동전 교환: 탐욕\"\"\"\n # coins 리스트가 정렬되어 있다고 가정한다\n i = len(coins) - 1\n count = 0\n # 남은 금액이 없을 때까지 탐욕적 선택을 반복한다\n while amt &gt; 0:\n # 남은 금액보다 작으면서 가장 가까운 금액의 동전을 찾는다\n while i &gt; 0 and coins[i] &gt; amt:\n i -= 1\n # coins[i]를 선택한다\n amt -= coins[i]\n count += 1\n # 가능한 방법을 찾지 못하면 -1을 반환한다\n return count if amt == 0 else -1\n</code></pre> coin_change_greedy.cpp<pre><code>/* 동전 교환: 탐욕 */\nint coinChangeGreedy(vector&lt;int&gt; &amp;coins, int amt) {\n // coins 리스트가 정렬되어 있다고 가정한다\n int i = coins.size() - 1;\n int count = 0;\n // 남은 금액이 없을 때까지 탐욕적 선택을 반복한다\n while (amt &gt; 0) {\n // 남은 금액보다 작으면서 가장 가까운 금액의 동전을 찾는다\n while (i &gt; 0 &amp;&amp; coins[i] &gt; amt) {\n i--;\n }\n // coins[i]를 선택한다\n amt -= coins[i];\n count++;\n }\n // 가능한 방법을 찾지 못하면 -1을 반환한다\n return amt == 0 ? count : -1;\n}\n</code></pre> coin_change_greedy.java<pre><code>/* 동전 교환: 탐욕 */\nint coinChangeGreedy(int[] coins, int amt) {\n // coins 리스트가 정렬되어 있다고 가정한다\n int i = coins.length - 1;\n int count = 0;\n // 남은 금액이 없을 때까지 탐욕적 선택을 반복한다\n while (amt &gt; 0) {\n // 남은 금액보다 작으면서 가장 가까운 금액의 동전을 찾는다\n while (i &gt; 0 &amp;&amp; coins[i] &gt; amt) {\n i--;\n }\n // coins[i]를 선택한다\n amt -= coins[i];\n count++;\n }\n // 가능한 방법을 찾지 못하면 -1을 반환한다\n return amt == 0 ? count : -1;\n}\n</code></pre> coin_change_greedy.cs<pre><code>/* 동전 교환: 탐욕 */\nint CoinChangeGreedy(int[] coins, int amt) {\n // coins 리스트가 정렬되어 있다고 가정한다\n int i = coins.Length - 1;\n int count = 0;\n // 남은 금액이 없을 때까지 탐욕적 선택을 반복한다\n while (amt &gt; 0) {\n // 남은 금액보다 작으면서 가장 가까운 금액의 동전을 찾는다\n while (i &gt; 0 &amp;&amp; coins[i] &gt; amt) {\n i--;\n }\n // coins[i]를 선택한다\n amt -= coins[i];\n count++;\n }\n // 가능한 방법을 찾지 못하면 -1을 반환한다\n return amt == 0 ? count : -1;\n}\n</code></pre> coin_change_greedy.go<pre><code>/* 동전 교환: 탐욕 */\nfunc coinChangeGreedy(coins []int, amt int) int {\n // coins 리스트가 정렬되어 있다고 가정한다\n i := len(coins) - 1\n count := 0\n // 남은 금액이 없을 때까지 탐욕적 선택을 반복한다\n for amt &gt; 0 {\n // 남은 금액보다 작으면서 가장 가까운 금액의 동전을 찾는다\n for i &gt; 0 &amp;&amp; coins[i] &gt; amt {\n i--\n }\n // coins[i]를 선택한다\n amt -= coins[i]\n count++\n }\n // 가능한 방법을 찾지 못하면 -1을 반환한다\n if amt != 0 {\n return -1\n }\n return count\n}\n</code></pre> coin_change_greedy.swift<pre><code>/* 동전 교환: 탐욕 */\nfunc coinChangeGreedy(coins: [Int], amt: Int) -&gt; Int {\n // coins 리스트가 정렬되어 있다고 가정한다\n var i = coins.count - 1\n var count = 0\n var amt = amt\n // 남은 금액이 없을 때까지 탐욕적 선택을 반복한다\n while amt &gt; 0 {\n // 남은 금액보다 작으면서 가장 가까운 금액의 동전을 찾는다\n while i &gt; 0 &amp;&amp; coins[i] &gt; amt {\n i -= 1\n }\n // coins[i]를 선택한다\n amt -= coins[i]\n count += 1\n }\n // 가능한 방법을 찾지 못하면 -1을 반환한다\n return amt == 0 ? count : -1\n}\n</code></pre> coin_change_greedy.js<pre><code>/* 동전 교환: 탐욕 */\nfunction coinChangeGreedy(coins, amt) {\n // coins 배열이 정렬되어 있다고 가정한다\n let i = coins.length - 1;\n let count = 0;\n // 남은 금액이 없을 때까지 탐욕적 선택을 반복한다\n while (amt &gt; 0) {\n // 남은 금액보다 작으면서 가장 가까운 금액의 동전을 찾는다\n while (i &gt; 0 &amp;&amp; coins[i] &gt; amt) {\n i--;\n }\n // coins[i]를 선택한다\n amt -= coins[i];\n count++;\n }\n // 가능한 방법을 찾지 못하면 -1을 반환한다\n return amt === 0 ? count : -1;\n}\n</code></pre> coin_change_greedy.ts<pre><code>/* 동전 교환: 탐욕 */\nfunction coinChangeGreedy(coins: number[], amt: number): number {\n // coins 배열이 정렬되어 있다고 가정한다\n let i = coins.length - 1;\n let count = 0;\n // 남은 금액이 없을 때까지 탐욕적 선택을 반복한다\n while (amt &gt; 0) {\n // 남은 금액보다 작으면서 가장 가까운 금액의 동전을 찾는다\n while (i &gt; 0 &amp;&amp; coins[i] &gt; amt) {\n i--;\n }\n // coins[i]를 선택한다\n amt -= coins[i];\n count++;\n }\n // 가능한 방법을 찾지 못하면 -1을 반환한다\n return amt === 0 ? count : -1;\n}\n</code></pre> coin_change_greedy.dart<pre><code>/* 동전 교환: 탐욕 */\nint coinChangeGreedy(List&lt;int&gt; coins, int amt) {\n // coins 리스트가 정렬되어 있다고 가정한다\n int i = coins.length - 1;\n int count = 0;\n // 남은 금액이 없을 때까지 탐욕적 선택을 반복한다\n while (amt &gt; 0) {\n // 남은 금액보다 작으면서 가장 가까운 금액의 동전을 찾는다\n while (i &gt; 0 &amp;&amp; coins[i] &gt; amt) {\n i--;\n }\n // coins[i]를 선택한다\n amt -= coins[i];\n count++;\n }\n // 가능한 방법을 찾지 못하면 -1을 반환한다\n return amt == 0 ? count : -1;\n}\n</code></pre> coin_change_greedy.rs<pre><code>/* 동전 교환: 탐욕 */\nfn coin_change_greedy(coins: &amp;[i32], mut amt: i32) -&gt; i32 {\n // coins 리스트가 정렬되어 있다고 가정한다\n let mut i = coins.len() - 1;\n let mut count = 0;\n // 남은 금액이 없을 때까지 탐욕적 선택을 반복한다\n while amt &gt; 0 {\n // 남은 금액보다 작으면서 가장 가까운 금액의 동전을 찾는다\n while i &gt; 0 &amp;&amp; coins[i] &gt; amt {\n i -= 1;\n }\n // coins[i]를 선택한다\n amt -= coins[i];\n count += 1;\n }\n // 가능한 방법을 찾지 못하면 -1을 반환한다\n if amt == 0 {\n count\n } else {\n -1\n }\n}\n</code></pre> coin_change_greedy.c<pre><code>/* 동전 교환: 탐욕 */\nint coinChangeGreedy(int *coins, int size, int amt) {\n // coins 리스트가 정렬되어 있다고 가정한다\n int i = size - 1;\n int count = 0;\n // 남은 금액이 없을 때까지 탐욕적 선택을 반복한다\n while (amt &gt; 0) {\n // 남은 금액보다 작으면서 가장 가까운 금액의 동전을 찾는다\n while (i &gt; 0 &amp;&amp; coins[i] &gt; amt) {\n i--;\n }\n // coins[i]를 선택한다\n amt -= coins[i];\n count++;\n }\n // 가능한 방법을 찾지 못하면 -1을 반환한다\n return amt == 0 ? count : -1;\n}\n</code></pre> coin_change_greedy.kt<pre><code>/* 동전 교환: 탐욕 */\nfun coinChangeGreedy(coins: IntArray, amt: Int): Int {\n // coins 리스트가 정렬되어 있다고 가정한다\n var am = amt\n var i = coins.size - 1\n var count = 0\n // 남은 금액이 없을 때까지 탐욕적 선택을 반복한다\n while (am &gt; 0) {\n // 남은 금액보다 작으면서 가장 가까운 금액의 동전을 찾는다\n while (i &gt; 0 &amp;&amp; coins[i] &gt; am) {\n i--\n }\n // coins[i]를 선택한다\n am -= coins[i]\n count++\n }\n // 가능한 방법을 찾지 못하면 -1을 반환한다\n return if (am == 0) count else -1\n}\n</code></pre> coin_change_greedy.rb<pre><code>### 동전 교환: 탐욕 ###\ndef coin_change_greedy(coins, amt)\n # coins 리스트가 정렬되어 있다고 가정한다\n i = coins.length - 1\n count = 0\n # 남은 금액이 없을 때까지 탐욕적 선택을 반복한다\n while amt &gt; 0\n # 남은 금액보다 작으면서 가장 가까운 금액의 동전을 찾는다\n while i &gt; 0 &amp;&amp; coins[i] &gt; amt\n i -= 1\n end\n # coins[i]를 선택한다\n amt -= coins[i]\n count += 1\n end\n # 가능한 방법을 찾지 못하면 -1을 반환한다\n amt == 0 ? count : -1\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>'정말 간결하다!'라는 감탄이 나올 만하다. 탐욕 알고리즘은 약 열 줄의 코드만으로 동전 교환 문제를 해결했다.</p>","path":["제 15 장   탐욕","15.1   탐욕 알고리즘"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1511","level":2,"title":"15.1.1   탐욕 알고리즘의 장점과 한계","text":"<p>탐욕 알고리즘은 연산이 직접적이고 구현이 간단할 뿐 아니라 보통 효율도 매우 높다. 위 코드에서 최소 액면가를 \\(\\min(coins)\\)라고 하면 탐욕적 선택은 최대 \\(amt / \\min(coins)\\) 번 반복하므로 시간 복잡도는 \\(O(amt / \\min(coins))\\)이다. 동적 계획법의 \\(O(n \\times amt)\\)보다 한 차수 낮다.</p> <p>하지만 일부 동전 액면가 조합에서는 최적해를 찾지 못한다. 아래 그림(15-2)은 두 예를 보여 준다.</p> <ul> <li>성립 예 \\(coins = [1, 5, 10, 20, 50, 100]\\): 이 동전 조합에서는 어떤 \\(amt\\)에 대해서도 탐욕 알고리즘이 최적해를 찾는다.</li> <li>반례 \\(coins = [1, 20, 50]\\): \\(amt = 60\\)이면 탐욕 알고리즘은 \\(50 + 1 \\times 10\\)으로 동전 \\(11\\) 개를 고른다. 하지만 동적 계획법은 \\(20 + 20 + 20\\)이라는 최적해를 찾아 \\(3\\) 개만 사용한다.</li> <li>반례 \\(coins = [1, 49, 50]\\): \\(amt = 98\\)이면 탐욕 알고리즘은 \\(50 + 1 \\times 48\\)로 동전 \\(49\\) 개를 고른다. 하지만 동적 계획법은 \\(49 + 49\\)라는 최적해를 찾아 \\(2\\) 개만 사용한다.</li> </ul> <p></p> <p> 그림 15-2   탐욕 알고리즘이 최적해를 찾지 못하는 예 </p> <p>즉, 동전 교환에서 탐욕 알고리즘은 전역 최적해를 보장하지 못하고 매우 나쁜 해를 찾을 수도 있다. 이 문제는 동적 계획법으로 푸는 편이 더 적합하다.</p> <p>일반적으로 탐욕 알고리즘을 적용하는 상황은 다음 두 가지다.</p> <ol> <li>최적해를 보장할 수 있다: 보통 백트래킹이나 동적 계획법보다 효율적이므로 이런 경우에는 탐욕 알고리즘이 가장 좋은 선택인 경우가 많다.</li> <li>최적해에 가까운 해를 찾을 수 있다: 이 경우에도 탐욕 알고리즘을 쓸 수 있다. 복잡한 문제에서는 전역 최적해를 찾기 매우 어려우므로, 효율적으로 차선의 해를 찾는 것만으로도 유용하다.</li> </ol>","path":["제 15 장   탐욕","15.1   탐욕 알고리즘"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1512","level":2,"title":"15.1.2   탐욕 알고리즘의 특성","text":"<p>그렇다면 어떤 문제가 탐욕 알고리즘에 적합할까? 다시 말해 어떤 경우에 최적해를 보장할 수 있을까?</p> <p>탐욕 알고리즘은 동적 계획법보다 적용 조건이 엄격하며, 주로 다음 두 성질에 주목한다.</p> <ul> <li>탐욕적 선택 속성: 국소 최적 선택이 항상 전역 최적해로 이어져야만 탐욕 알고리즘이 최적해를 보장할 수 있다.</li> <li>최적 부분 구조: 원래 문제의 최적해가 부분 문제의 최적해를 포함한다.</li> </ul> <p>최적 부분 구조는 '동적 계획법' 장에서 설명했으므로 반복하지 않는다. 일부 문제는 최적 부분 구조가 뚜렷하지 않아도 탐욕 알고리즘으로 해결할 수 있다는 점을 알아 두자.</p> <p>여기서는 탐욕적 선택 속성을 판단하는 방법을 주로 살펴본다. 설명은 간단해 보이지만 많은 문제에서 이 속성을 증명하기는 쉽지 않다.</p> <p>예를 들어 동전 교환에서는 반례로 탐욕적 선택 속성을 부정하기는 쉽지만 증명하기는 어렵다. 어떤 조건을 만족하는 동전 조합을 탐욕 알고리즘으로 풀 수 있는가라는 질문에는 직관이나 사례를 바탕으로 애매한 답을 내놓기 쉽고, 엄밀한 수학적 증명을 제시하기는 어렵다.</p> <p>참고 문헌</p> <p>어떤 동전 조합에 대해 탐욕 알고리즘이 모든 금액의 최적해를 찾을 수 있는지 판단하는 \\(O(n^3)\\) 알고리즘을 제시한 논문이 있다.</p> <p>Pearson, D. A polynomial-time algorithm for the change-making problem[J]. Operations Research Letters, 2005, 33(3): 231-234.</p>","path":["제 15 장   탐욕","15.1   탐욕 알고리즘"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1513","level":2,"title":"15.1.3   탐욕 알고리즘의 문제 해결 단계","text":"<p>탐욕 문제의 풀이 과정은 대체로 다음 세 단계다.</p> <ol> <li>문제 분석: 상태 정의, 최적화 목표, 제약 조건 등 문제의 특성을 정리하고 이해한다. 백트래킹과 동적 계획법에서도 필요한 단계다.</li> <li>탐욕 전략 결정: 매 단계에서 어떻게 탐욕적으로 선택할지 정한다. 각 단계에서 문제 크기를 줄여 최종적으로 전체 문제를 해결할 수 있어야 한다.</li> <li>정확성 증명: 보통 탐욕적 선택 속성과 최적 부분 구조를 증명해야 한다. 귀납법이나 귀류법 같은 수학적 증명이 필요할 수 있다.</li> </ol> <p>탐욕 전략을 정하는 것이 핵심 단계지만, 다음 이유로 쉽지 않을 수 있다.</p> <ul> <li>문제마다 탐욕 전략이 크게 다르다. 많은 문제는 전략이 비교적 명확하여 대략적인 생각과 시도만으로 찾을 수 있다. 반면 복잡한 문제에서는 전략이 잘 드러나지 않아 문제 풀이 경험과 알고리즘 역량이 크게 요구된다.</li> <li>일부 탐욕 전략은 그럴듯해 보이지만 함정이 있다. 자신 있게 전략을 설계하고 코드를 작성해 제출해도 일부 테스트를 통과하지 못할 수 있다. 전략이 '부분적으로만 옳기' 때문이다. 앞에서 본 동전 교환이 대표적인 사례다.</li> </ul> <p>정확성을 보장하려면 탐욕 전략을 엄밀히 증명해야 하며, 보통 귀류법이나 수학적 귀납법을 사용한다.</p> <p>하지만 정확성 증명 자체도 쉽지 않을 수 있다. 실마리가 없으면 보통 테스트 사례로 코드를 디버깅하면서 탐욕 전략을 조금씩 수정하고 검증한다.</p>","path":["제 15 장   탐욕","15.1   탐욕 알고리즘"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1514","level":2,"title":"15.1.4   대표적인 탐욕 알고리즘 문제","text":"<p>탐욕 알고리즘은 탐욕적 선택 속성과 최적 부분 구조를 만족하는 최적화 문제에 자주 사용한다. 대표적인 예는 다음과 같다.</p> <ul> <li>동전 거스름돈 문제: 일부 동전 조합에서는 탐욕 알고리즘이 항상 최적해를 얻는다.</li> <li>구간 스케줄링 문제: 각 작업이 일정 시간 구간에 걸쳐 진행될 때 가능한 한 많은 작업을 완료하는 것이 목표다. 매번 가장 일찍 끝나는 작업을 고르면 최적해를 얻을 수 있다.</li> <li>분할 가능한 배낭 문제: 물건들과 적재 용량이 주어졌을 때 총무게가 용량을 넘지 않으면서 총가치가 최대가 되도록 물건을 고른다. 매번 무게 대비 가치의 비율(가치 / 무게)이 가장 높은 물건을 고르면 일부 경우에 최적해를 얻을 수 있다.</li> <li>주식 매매 문제: 주가의 과거 기록이 주어졌을 때 여러 번 매매하여 최대 이익을 구한다. 단, 이미 주식을 보유하고 있으면 팔기 전에는 다시 살 수 없다.</li> <li>허프먼 코딩: 무손실 데이터 압축에 쓰이는 탐욕 알고리즘이다. 매번 빈도가 가장 낮은 두 노드를 합쳐 허프먼 트리를 만들면, 최종 트리의 가중 경로 길이(부호 길이)가 최소가 된다.</li> <li>Dijkstra 알고리즘: 모든 간선 가중치가 음이 아닌 그래프에서 주어진 시작 정점부터 나머지 정점까지의 최단 경로를 구하는 탐욕 알고리즘이다.</li> </ul>","path":["제 15 장   탐욕","15.1   탐욕 알고리즘"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/","level":1,"title":"15.3   최대 용량 문제","text":"<p>문제</p> <p>배열 \\(ht\\)의 각 원소는 수직 칸막이의 높이를 나타낸다. 배열에서 임의의 두 칸막이와 그 사이의 공간으로 용기를 만들 수 있다.</p> <p>용량은 높이와 너비의 곱(넓이)이다. 높이는 더 낮은 칸막이로 결정되고, 너비는 두 칸막이의 배열 인덱스 차이다.</p> <p>용량이 최대가 되도록 두 칸막이를 골라 최대 용량을 반환하라. 예시는 아래 그림(15-7)과 같다.</p> <p></p> <p> 그림 15-7   최대 용량 문제 예시 데이터 </p> <p>임의의 두 칸막이로 용기를 만들므로 이 문제의 상태는 두 칸막이의 인덱스 \\([i, j]\\)다.</p> <p>문제에 따르면 용량은 높이와 너비의 곱이고, 높이는 낮은 칸막이로 결정되며 너비는 두 인덱스의 차다. 용량을 \\(cap[i, j]\\)라고 하면 다음 공식을 얻는다.</p> \\[ cap[i, j] = \\min(ht[i], ht[j]) \\times (j - i) \\] <p>배열 길이가 \\(n\\)이면 두 칸막이의 조합 수(전체 상태 수)는 \\(C_n^2 = \\frac{n(n - 1)}{2}\\)이다. 가장 직접적으로는 모든 상태를 열거하여 최대 용량을 구할 수 있으며, 시간 복잡도는 \\(O(n^2)\\)이다.</p>","path":["제 15 장   탐욕","15.3   최대 용량 문제"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#1","level":3,"title":"1.   탐욕 전략 결정","text":"<p>더 효율적인 풀이도 있다. 아래 그림(15-8)처럼 인덱스 \\(i &lt; j\\)이고 높이 \\(ht[i] &lt; ht[j]\\)인 상태 \\([i, j]\\)를 고르자. 즉, \\(i\\)는 낮은 칸막이, \\(j\\)는 높은 칸막이다.</p> <p></p> <p> 그림 15-8   초기 상태 </p> <p>아래 그림(15-9)과 같이 높은 칸막이 \\(j\\)를 낮은 칸막이 \\(i\\) 쪽으로 옮기면 용량은 반드시 작아진다.</p> <p>\\(j\\)를 옮기면 너비 \\(j-i\\)는 확실히 줄어든다. 높이는 낮은 칸막이로 결정되므로 그대로이거나(\\(i\\)가 계속 더 낮음), 더 낮아질 수만 있다(이동한 \\(j\\)가 더 낮아짐).</p> <p></p> <p> 그림 15-9   높은 칸막이를 안쪽으로 옮긴 상태 </p> <p>반대로 생각하면 낮은 칸막이 \\(i\\)를 안쪽으로 옮겨야만 용량이 커질 가능성이 있다. 너비는 반드시 줄지만 높이는 커질 수도 있기 때문이다(이동한 \\(i\\)가 더 높을 수 있다). 아래 그림(15-10)에서는 낮은 칸막이를 옮긴 뒤 넓이가 커진다.</p> <p></p> <p> 그림 15-10   낮은 칸막이를 안쪽으로 옮긴 상태 </p> <p>따라서 두 포인터를 용기의 양 끝에 놓고, 매번 낮은 칸막이 쪽 포인터를 안쪽으로 옮겨 두 포인터가 만날 때까지 진행하는 탐욕 전략을 도출할 수 있다.</p> <p>아래 그림(15-11)은 탐욕 전략의 실행 과정이다.</p> <ol> <li>처음에는 포인터 \\(i\\)와 \\(j\\)를 배열의 양 끝에 둔다.</li> <li>현재 상태의 용량 \\(cap[i, j]\\)를 계산하고 최대 용량을 갱신한다.</li> <li>칸막이 \\(i\\)와 \\(j\\)의 높이를 비교하여 낮은 쪽을 안쪽으로 한 칸 옮긴다.</li> <li>\\(i\\)와 \\(j\\)가 만날 때까지 <code>2.</code> 단계와 <code>3.</code> 단계를 반복한다.</li> </ol> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt;&lt;6&gt;&lt;7&gt;&lt;8&gt;&lt;9&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 15-11   최대 용량 문제의 탐욕 과정 </p>","path":["제 15 장   탐욕","15.3   최대 용량 문제"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#2","level":3,"title":"2.   코드 구현","text":"<p>반복문은 최대 \\(n\\) 회 실행되므로 시간 복잡도는 \\(O(n)\\)이다.</p> <p>변수 \\(i\\), \\(j\\), \\(res\\)는 상수 크기의 추가 공간을 사용하므로 공간 복잡도는 \\(O(1)\\)이다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby max_capacity.py<pre><code>def max_capacity(ht: list[int]) -&gt; int:\n \"\"\"최대 용량: 탐욕\"\"\"\n # i와 j가 각각 배열의 양 끝을 가리키도록 초기화한다\n i, j = 0, len(ht) - 1\n # 최대 용량의 초깃값은 0이다\n res = 0\n # 두 칸막이가 만날 때까지 탐욕적 선택을 반복한다\n while i &lt; j:\n # 최대 용량을 갱신한다\n cap = min(ht[i], ht[j]) * (j - i)\n res = max(res, cap)\n # 낮은 칸막이를 안쪽으로 이동한다\n if ht[i] &lt; ht[j]:\n i += 1\n else:\n j -= 1\n return res\n</code></pre> max_capacity.cpp<pre><code>/* 최대 용량: 탐욕 */\nint maxCapacity(vector&lt;int&gt; &amp;ht) {\n // i와 j가 각각 배열의 양 끝을 가리키도록 초기화한다\n int i = 0, j = ht.size() - 1;\n // 최대 용량의 초깃값은 0이다\n int res = 0;\n // 두 칸막이가 만날 때까지 탐욕적 선택을 반복한다\n while (i &lt; j) {\n // 최대 용량을 갱신한다\n int cap = min(ht[i], ht[j]) * (j - i);\n res = max(res, cap);\n // 낮은 칸막이를 안쪽으로 이동한다\n if (ht[i] &lt; ht[j]) {\n i++;\n } else {\n j--;\n }\n }\n return res;\n}\n</code></pre> max_capacity.java<pre><code>/* 최대 용량: 탐욕 */\nint maxCapacity(int[] ht) {\n // i와 j가 각각 배열의 양 끝을 가리키도록 초기화한다\n int i = 0, j = ht.length - 1;\n // 최대 용량의 초깃값은 0이다\n int res = 0;\n // 두 칸막이가 만날 때까지 탐욕적 선택을 반복한다\n while (i &lt; j) {\n // 최대 용량을 갱신한다\n int cap = Math.min(ht[i], ht[j]) * (j - i);\n res = Math.max(res, cap);\n // 낮은 칸막이를 안쪽으로 이동한다\n if (ht[i] &lt; ht[j]) {\n i++;\n } else {\n j--;\n }\n }\n return res;\n}\n</code></pre> max_capacity.cs<pre><code>/* 최대 용량: 탐욕 */\nint MaxCapacity(int[] ht) {\n // i와 j가 각각 배열의 양 끝을 가리키도록 초기화한다\n int i = 0, j = ht.Length - 1;\n // 최대 용량의 초깃값은 0이다\n int res = 0;\n // 두 칸막이가 만날 때까지 탐욕적 선택을 반복한다\n while (i &lt; j) {\n // 최대 용량을 갱신한다\n int cap = Math.Min(ht[i], ht[j]) * (j - i);\n res = Math.Max(res, cap);\n // 낮은 칸막이를 안쪽으로 이동한다\n if (ht[i] &lt; ht[j]) {\n i++;\n } else {\n j--;\n }\n }\n return res;\n}\n</code></pre> max_capacity.go<pre><code>/* 최대 용량: 탐욕 */\nfunc maxCapacity(ht []int) int {\n // i와 j가 각각 배열의 양 끝을 가리키도록 초기화한다\n i, j := 0, len(ht)-1\n // 최대 용량의 초깃값은 0이다\n res := 0\n // 두 칸막이가 만날 때까지 탐욕적 선택을 반복한다\n for i &lt; j {\n // 최대 용량을 갱신한다\n capacity := int(math.Min(float64(ht[i]), float64(ht[j]))) * (j - i)\n res = int(math.Max(float64(res), float64(capacity)))\n // 낮은 칸막이를 안쪽으로 이동한다\n if ht[i] &lt; ht[j] {\n i++\n } else {\n j--\n }\n }\n return res\n}\n</code></pre> max_capacity.swift<pre><code>/* 최대 용량: 탐욕 */\nfunc maxCapacity(ht: [Int]) -&gt; Int {\n // i와 j가 각각 배열의 양 끝을 가리키도록 초기화한다\n var i = ht.startIndex, j = ht.endIndex - 1\n // 최대 용량의 초깃값은 0이다\n var res = 0\n // 두 칸막이가 만날 때까지 탐욕적 선택을 반복한다\n while i &lt; j {\n // 최대 용량을 갱신한다\n let cap = min(ht[i], ht[j]) * (j - i)\n res = max(res, cap)\n // 낮은 칸막이를 안쪽으로 이동한다\n if ht[i] &lt; ht[j] {\n i += 1\n } else {\n j -= 1\n }\n }\n return res\n}\n</code></pre> max_capacity.js<pre><code>/* 최대 용량: 탐욕 */\nfunction maxCapacity(ht) {\n // i와 j가 각각 배열의 양 끝을 가리키도록 초기화한다\n let i = 0,\n j = ht.length - 1;\n // 최대 용량의 초깃값은 0이다\n let res = 0;\n // 두 칸막이가 만날 때까지 탐욕적 선택을 반복한다\n while (i &lt; j) {\n // 최대 용량을 갱신한다\n const cap = Math.min(ht[i], ht[j]) * (j - i);\n res = Math.max(res, cap);\n // 낮은 칸막이를 안쪽으로 이동한다\n if (ht[i] &lt; ht[j]) {\n i += 1;\n } else {\n j -= 1;\n }\n }\n return res;\n}\n</code></pre> max_capacity.ts<pre><code>/* 최대 용량: 탐욕 */\nfunction maxCapacity(ht: number[]): number {\n // i와 j가 각각 배열의 양 끝을 가리키도록 초기화한다\n let i = 0,\n j = ht.length - 1;\n // 최대 용량의 초깃값은 0이다\n let res = 0;\n // 두 칸막이가 만날 때까지 탐욕적 선택을 반복한다\n while (i &lt; j) {\n // 최대 용량을 갱신한다\n const cap: number = Math.min(ht[i], ht[j]) * (j - i);\n res = Math.max(res, cap);\n // 낮은 칸막이를 안쪽으로 이동한다\n if (ht[i] &lt; ht[j]) {\n i += 1;\n } else {\n j -= 1;\n }\n }\n return res;\n}\n</code></pre> max_capacity.dart<pre><code>/* 최대 용량: 탐욕 */\nint maxCapacity(List&lt;int&gt; ht) {\n // i와 j가 각각 배열의 양 끝을 가리키도록 초기화한다\n int i = 0, j = ht.length - 1;\n // 최대 용량의 초깃값은 0이다\n int res = 0;\n // 두 칸막이가 만날 때까지 탐욕적 선택을 반복한다\n while (i &lt; j) {\n // 최대 용량을 갱신한다\n int cap = min(ht[i], ht[j]) * (j - i);\n res = max(res, cap);\n // 낮은 칸막이를 안쪽으로 이동한다\n if (ht[i] &lt; ht[j]) {\n i++;\n } else {\n j--;\n }\n }\n return res;\n}\n</code></pre> max_capacity.rs<pre><code>/* 최대 용량: 탐욕 */\nfn max_capacity(ht: &amp;[i32]) -&gt; i32 {\n // i와 j가 각각 배열의 양 끝을 가리키도록 초기화한다\n let mut i = 0;\n let mut j = ht.len() - 1;\n // 최대 용량의 초깃값은 0이다\n let mut res = 0;\n // 두 칸막이가 만날 때까지 탐욕적 선택을 반복한다\n while i &lt; j {\n // 최대 용량을 갱신한다\n let cap = std::cmp::min(ht[i], ht[j]) * (j - i) as i32;\n res = std::cmp::max(res, cap);\n // 낮은 칸막이를 안쪽으로 이동한다\n if ht[i] &lt; ht[j] {\n i += 1;\n } else {\n j -= 1;\n }\n }\n res\n}\n</code></pre> max_capacity.c<pre><code>/* 최대 용량: 탐욕 */\nint maxCapacity(int ht[], int htLength) {\n // i와 j가 각각 배열의 양 끝을 가리키도록 초기화한다\n int i = 0;\n int j = htLength - 1;\n // 최대 용량의 초깃값은 0이다\n int res = 0;\n // 두 칸막이가 만날 때까지 탐욕적 선택을 반복한다\n while (i &lt; j) {\n // 최대 용량을 갱신한다\n int capacity = myMin(ht[i], ht[j]) * (j - i);\n res = myMax(res, capacity);\n // 낮은 칸막이를 안쪽으로 이동한다\n if (ht[i] &lt; ht[j]) {\n i++;\n } else {\n j--;\n }\n }\n return res;\n}\n</code></pre> max_capacity.kt<pre><code>/* 최대 용량: 탐욕 */\nfun maxCapacity(ht: IntArray): Int {\n // i와 j가 각각 배열의 양 끝을 가리키도록 초기화한다\n var i = 0\n var j = ht.size - 1\n // 최대 용량의 초깃값은 0이다\n var res = 0\n // 두 칸막이가 만날 때까지 탐욕적 선택을 반복한다\n while (i &lt; j) {\n // 최대 용량을 갱신한다\n val cap = min(ht[i], ht[j]) * (j - i)\n res = max(res, cap)\n // 낮은 칸막이를 안쪽으로 이동한다\n if (ht[i] &lt; ht[j]) {\n i++\n } else {\n j--\n }\n }\n return res\n}\n</code></pre> max_capacity.rb<pre><code>### 최대 용량: 탐욕 ###\ndef max_capacity(ht)\n # i와 j가 각각 배열의 양 끝을 가리키도록 초기화한다\n i, j = 0, ht.length - 1\n # 최대 용량의 초깃값은 0이다\n res = 0\n\n # 두 칸막이가 만날 때까지 탐욕적 선택을 반복한다\n while i &lt; j\n # 최대 용량을 갱신한다\n cap = [ht[i], ht[j]].min * (j - i)\n res = [res, cap].max\n # 낮은 칸막이를 안쪽으로 이동한다\n if ht[i] &lt; ht[j]\n i += 1\n else\n j -= 1\n end\n end\n\n res\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 15 장   탐욕","15.3   최대 용량 문제"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#3","level":3,"title":"3.   정확성 증명","text":"<p>탐욕 알고리즘이 모든 경우 열거보다 빠른 이유는 매번 일부 상태를 '건너뛰기' 때문이다.</p> <p>예를 들어 상태 \\(cap[i, j]\\)에서 \\(i\\)가 낮은 칸막이, \\(j\\)가 높은 칸막이라고 하자. 낮은 \\(i\\)를 탐욕적으로 한 칸 안쪽으로 옮기면 아래 그림(15-12)의 상태들을 건너뛴다. 이는 이후에 그 상태들의 용량을 확인할 수 없다는 뜻이다.</p> \\[ cap[i, i+1], cap[i, i+2], \\dots, cap[i, j-2], cap[i, j-1] \\] <p></p> <p> 그림 15-12   낮은 칸막이를 옮겨 건너뛴 상태들 </p> <p>건너뛴 상태들은 사실 높은 칸막이 \\(j\\)를 안쪽으로 옮긴 모든 상태다. 앞에서 높은 칸막이를 안쪽으로 옮기면 용량이 반드시 줄어듦을 보였다. 따라서 건너뛴 상태는 최적해가 될 수 없으며, 이를 건너뛰어도 최적해를 놓치지 않는다.</p> <p>이 분석은 낮은 칸막이를 옮기는 연산이 '안전'하며 탐욕 전략이 유효함을 보여 준다.</p>","path":["제 15 장   탐욕","15.3   최대 용량 문제"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/","level":1,"title":"15.4   정수 분할의 최대 곱 문제","text":"<p>문제</p> <p>양의 정수 \\(n\\)을 두 개 이상의 양의 정수의 합으로 나누었을 때, 나눈 정수들의 곱이 최대 얼마가 되는지 구하라. 아래 그림(15-13)을 참고하자.</p> <p></p> <p> 그림 15-13   정수 분할의 최대 곱 문제 정의 </p> <p>\\(n\\)을 \\(m\\) 개의 정수 인수로 나누고 \\(i\\) 번째 인수를 \\(n_i\\)라고 하면 다음과 같다.</p> \\[ n = \\sum_{i=1}^{m}n_i \\] <p>목표는 모든 정수 인수의 곱을 최대화하는 것이다.</p> \\[ \\max(\\prod_{i=1}^{m}n_i) \\] <p>생각해야 할 것은 나누는 개수 \\(m\\)과 각 \\(n_i\\)의 값이다.</p>","path":["제 15 장   탐욕","15.4   정수 분할의 최대 곱 문제"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#1","level":3,"title":"1.   탐욕 전략 결정","text":"<p>경험상 두 정수의 곱은 합보다 큰 경우가 많다. \\(n\\)에서 인수 \\(2\\)를 나누어 내면 곱은 \\(2(n-2)\\)다. 이를 \\(n\\)과 비교해 보자.</p> \\[ \\begin{aligned} 2(n-2) &amp; \\geq n \\newline 2n - n - 4 &amp; \\geq 0 \\newline n &amp; \\geq 4 \\end{aligned} \\] <p>아래 그림(15-14)과 같이 \\(n \\geq 4\\)이면 \\(2\\)를 나누어 낸 뒤 곱이 커진다. 이는 \\(4\\) 이상의 정수는 모두 나누어야 한다는 뜻이다.</p> <p>탐욕 전략 1: 분할에 \\(\\geq 4\\)인 인수가 있으면 계속 나누어야 한다. 최종 분할에는 \\(1\\), \\(2\\), \\(3\\)만 있어야 한다.</p> <p></p> <p> 그림 15-14   분할로 커지는 곱 </p> <p>이제 어느 인수가 가장 좋은지 생각해 보자. \\(1\\), \\(2\\), \\(3\\) 중 \\(1\\)이 가장 나쁘다는 것은 분명하다. \\(1 \\times (n-1) &lt; n\\)이 항상 성립하므로 \\(1\\)을 나누어 내면 오히려 곱이 줄어들기 때문이다.</p> <p>아래 그림(15-15)에서 \\(n = 6\\)일 때 \\(3 \\times 3 &gt; 2 \\times 2 \\times 2\\)다. 따라서 \\(2\\)보다 \\(3\\)으로 나누는 편이 더 좋다.</p> <p>탐욕 전략 2: 분할에는 \\(2\\)가 많아도 두 개만 있어야 한다. \\(2\\) 세 개는 언제나 \\(3\\) 두 개로 바꾸어 더 큰 곱을 얻을 수 있기 때문이다.</p> <p></p> <p> 그림 15-15   최적인 분할 인수 </p> <p>위 내용을 종합하면 다음 탐욕 전략을 얻는다.</p> <ol> <li>정수 \\(n\\)에서 인수 \\(3\\)을 계속 나누어 내고, 나머지가 \\(0\\), \\(1\\), \\(2\\)가 되면 멈춘다.</li> <li>나머지가 \\(0\\)이면 \\(n\\)이 \\(3\\)의 배수이므로 추가로 처리하지 않는다.</li> <li>나머지가 \\(2\\)이면 더 나누지 않고 그대로 둔다.</li> <li>나머지가 \\(1\\)이면 \\(2 \\times 2 &gt; 1 \\times 3\\)이므로 마지막 \\(3\\)과 나머지 \\(1\\)을 \\(2\\) 두 개로 바꾼다.</li> </ol>","path":["제 15 장   탐욕","15.4   정수 분할의 최대 곱 문제"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#2","level":3,"title":"2.   코드 구현","text":"<p>아래 그림(15-16)과 같이 반복문으로 정수를 나눌 필요 없이 내림 정수 나눗셈으로 \\(3\\)의 개수 \\(a\\)를, 나머지 연산으로 나머지 \\(b\\)를 구할 수 있다. 이때 다음이 성립한다.</p> \\[ n = 3 a + b \\] <p>경계 경우인 \\(n \\leq 3\\)에서는 반드시 \\(1\\)을 하나 나누어 내야 하므로 곱은 \\(1 \\times (n - 1)\\)이다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby max_product_cutting.py<pre><code>def max_product_cutting(n: int) -&gt; int:\n \"\"\"분할 후의 최대 곱: 탐욕\"\"\"\n # n &lt;= 3이면 반드시 1 하나가 나오도록 분할해야 한다\n if n &lt;= 3:\n return 1 * (n - 1)\n # 탐욕적으로 3을 떼어 낸다. a는 3의 개수이고 b는 나머지이다\n a, b = n // 3, n % 3\n if b == 1:\n # 나머지가 1이면 1 * 3 한 쌍을 2 * 2로 바꾼다\n return int(math.pow(3, a - 1)) * 2 * 2\n if b == 2:\n # 나머지가 2이면 별도로 처리하지 않는다\n return int(math.pow(3, a)) * 2\n # 나머지가 0이면 별도로 처리하지 않는다\n return int(math.pow(3, a))\n</code></pre> max_product_cutting.cpp<pre><code>/* 분할 후의 최대 곱: 탐욕 */\nint maxProductCutting(int n) {\n // n &lt;= 3이면 반드시 1 하나가 나오도록 분할해야 한다\n if (n &lt;= 3) {\n return 1 * (n - 1);\n }\n // 탐욕적으로 3을 떼어 낸다. a는 3의 개수이고 b는 나머지이다\n int a = n / 3;\n int b = n % 3;\n if (b == 1) {\n // 나머지가 1이면 1 * 3 한 쌍을 2 * 2로 바꾼다\n return (int)pow(3, a - 1) * 2 * 2;\n }\n if (b == 2) {\n // 나머지가 2이면 별도로 처리하지 않는다\n return (int)pow(3, a) * 2;\n }\n // 나머지가 0이면 별도로 처리하지 않는다\n return (int)pow(3, a);\n}\n</code></pre> max_product_cutting.java<pre><code>/* 분할 후의 최대 곱: 탐욕 */\nint maxProductCutting(int n) {\n // n &lt;= 3이면 반드시 1 하나가 나오도록 분할해야 한다\n if (n &lt;= 3) {\n return 1 * (n - 1);\n }\n // 탐욕적으로 3을 떼어 낸다. a는 3의 개수이고 b는 나머지이다\n int a = n / 3;\n int b = n % 3;\n if (b == 1) {\n // 나머지가 1이면 1 * 3 한 쌍을 2 * 2로 바꾼다\n return (int) Math.pow(3, a - 1) * 2 * 2;\n }\n if (b == 2) {\n // 나머지가 2이면 별도로 처리하지 않는다\n return (int) Math.pow(3, a) * 2;\n }\n // 나머지가 0이면 별도로 처리하지 않는다\n return (int) Math.pow(3, a);\n}\n</code></pre> max_product_cutting.cs<pre><code>/* 분할 후의 최대 곱: 탐욕 */\nint MaxProductCutting(int n) {\n // n &lt;= 3이면 반드시 1 하나가 나오도록 분할해야 한다\n if (n &lt;= 3) {\n return 1 * (n - 1);\n }\n // 탐욕적으로 3을 떼어 낸다. a는 3의 개수이고 b는 나머지이다\n int a = n / 3;\n int b = n % 3;\n if (b == 1) {\n // 나머지가 1이면 1 * 3 한 쌍을 2 * 2로 바꾼다\n return (int)Math.Pow(3, a - 1) * 2 * 2;\n }\n if (b == 2) {\n // 나머지가 2이면 별도로 처리하지 않는다\n return (int)Math.Pow(3, a) * 2;\n }\n // 나머지가 0이면 별도로 처리하지 않는다\n return (int)Math.Pow(3, a);\n}\n</code></pre> max_product_cutting.go<pre><code>/* 분할 후의 최대 곱: 탐욕 */\nfunc maxProductCutting(n int) int {\n // n &lt;= 3이면 반드시 1 하나가 나오도록 분할해야 한다\n if n &lt;= 3 {\n return 1 * (n - 1)\n }\n // 탐욕적으로 3을 떼어 낸다. a는 3의 개수이고 b는 나머지이다\n a := n / 3\n b := n % 3\n if b == 1 {\n // 나머지가 1이면 1 * 3 한 쌍을 2 * 2로 바꾼다\n return int(math.Pow(3, float64(a-1))) * 2 * 2\n }\n if b == 2 {\n // 나머지가 2이면 별도로 처리하지 않는다\n return int(math.Pow(3, float64(a))) * 2\n }\n // 나머지가 0이면 별도로 처리하지 않는다\n return int(math.Pow(3, float64(a)))\n}\n</code></pre> max_product_cutting.swift<pre><code>/* 분할 후의 최대 곱: 탐욕 */\nfunc maxProductCutting(n: Int) -&gt; Int {\n // n &lt;= 3이면 반드시 1 하나가 나오도록 분할해야 한다\n if n &lt;= 3 {\n return 1 * (n - 1)\n }\n // 탐욕적으로 3을 떼어 낸다. a는 3의 개수이고 b는 나머지이다\n let a = n / 3\n let b = n % 3\n if b == 1 {\n // 나머지가 1이면 1 * 3 한 쌍을 2 * 2로 바꾼다\n return pow(3, a - 1) * 2 * 2\n }\n if b == 2 {\n // 나머지가 2이면 별도로 처리하지 않는다\n return pow(3, a) * 2\n }\n // 나머지가 0이면 별도로 처리하지 않는다\n return pow(3, a)\n}\n</code></pre> max_product_cutting.js<pre><code>/* 분할 후의 최대 곱: 탐욕 */\nfunction maxProductCutting(n) {\n // n &lt;= 3이면 반드시 1 하나가 나오도록 분할해야 한다\n if (n &lt;= 3) {\n return 1 * (n - 1);\n }\n // 탐욕적으로 3을 떼어 낸다. a는 3의 개수이고 b는 나머지이다\n let a = Math.floor(n / 3);\n let b = n % 3;\n if (b === 1) {\n // 나머지가 1이면 1 * 3 한 쌍을 2 * 2로 바꾼다\n return Math.pow(3, a - 1) * 2 * 2;\n }\n if (b === 2) {\n // 나머지가 2이면 별도로 처리하지 않는다\n return Math.pow(3, a) * 2;\n }\n // 나머지가 0이면 별도로 처리하지 않는다\n return Math.pow(3, a);\n}\n</code></pre> max_product_cutting.ts<pre><code>/* 분할 후의 최대 곱: 탐욕 */\nfunction maxProductCutting(n: number): number {\n // n &lt;= 3이면 반드시 1 하나가 나오도록 분할해야 한다\n if (n &lt;= 3) {\n return 1 * (n - 1);\n }\n // 탐욕적으로 3을 떼어 낸다. a는 3의 개수이고 b는 나머지이다\n let a: number = Math.floor(n / 3);\n let b: number = n % 3;\n if (b === 1) {\n // 나머지가 1이면 1 * 3 한 쌍을 2 * 2로 바꾼다\n return Math.pow(3, a - 1) * 2 * 2;\n }\n if (b === 2) {\n // 나머지가 2이면 별도로 처리하지 않는다\n return Math.pow(3, a) * 2;\n }\n // 나머지가 0이면 별도로 처리하지 않는다\n return Math.pow(3, a);\n}\n</code></pre> max_product_cutting.dart<pre><code>/* 분할 후의 최대 곱: 탐욕 */\nint maxProductCutting(int n) {\n // n &lt;= 3이면 반드시 1 하나가 나오도록 분할해야 한다\n if (n &lt;= 3) {\n return 1 * (n - 1);\n }\n // 탐욕적으로 3을 떼어 낸다. a는 3의 개수이고 b는 나머지이다\n int a = n ~/ 3;\n int b = n % 3;\n if (b == 1) {\n // 나머지가 1이면 1 * 3 한 쌍을 2 * 2로 바꾼다\n return (pow(3, a - 1) * 2 * 2).toInt();\n }\n if (b == 2) {\n // 나머지가 2이면 별도로 처리하지 않는다\n return (pow(3, a) * 2).toInt();\n }\n // 나머지가 0이면 별도로 처리하지 않는다\n return pow(3, a).toInt();\n}\n</code></pre> max_product_cutting.rs<pre><code>/* 분할 후의 최대 곱: 탐욕 */\nfn max_product_cutting(n: i32) -&gt; i32 {\n // n &lt;= 3이면 반드시 1 하나가 나오도록 분할해야 한다\n if n &lt;= 3 {\n return 1 * (n - 1);\n }\n // 탐욕적으로 3을 떼어 낸다. a는 3의 개수이고 b는 나머지이다\n let a = n / 3;\n let b = n % 3;\n if b == 1 {\n // 나머지가 1이면 1 * 3 한 쌍을 2 * 2로 바꾼다\n 3_i32.pow(a as u32 - 1) * 2 * 2\n } else if b == 2 {\n // 나머지가 2이면 별도로 처리하지 않는다\n 3_i32.pow(a as u32) * 2\n } else {\n // 나머지가 0이면 별도로 처리하지 않는다\n 3_i32.pow(a as u32)\n }\n}\n</code></pre> max_product_cutting.c<pre><code>/* 분할 후의 최대 곱: 탐욕 */\nint maxProductCutting(int n) {\n // n &lt;= 3이면 반드시 1 하나가 나오도록 분할해야 한다\n if (n &lt;= 3) {\n return 1 * (n - 1);\n }\n // 탐욕적으로 3을 떼어 낸다. a는 3의 개수이고 b는 나머지이다\n int a = n / 3;\n int b = n % 3;\n if (b == 1) {\n // 나머지가 1이면 1 * 3 한 쌍을 2 * 2로 바꾼다\n return pow(3, a - 1) * 2 * 2;\n }\n if (b == 2) {\n // 나머지가 2이면 별도로 처리하지 않는다\n return pow(3, a) * 2;\n }\n // 나머지가 0이면 별도로 처리하지 않는다\n return pow(3, a);\n}\n</code></pre> max_product_cutting.kt<pre><code>/* 분할 후의 최대 곱: 탐욕 */\nfun maxProductCutting(n: Int): Int {\n // n &lt;= 3이면 반드시 1 하나가 나오도록 분할해야 한다\n if (n &lt;= 3) {\n return 1 * (n - 1)\n }\n // 탐욕적으로 3을 떼어 낸다. a는 3의 개수이고 b는 나머지이다\n val a = n / 3\n val b = n % 3\n if (b == 1) {\n // 나머지가 1이면 1 * 3 한 쌍을 2 * 2로 바꾼다\n return 3.0.pow((a - 1)).toInt() * 2 * 2\n }\n if (b == 2) {\n // 나머지가 2이면 별도로 처리하지 않는다\n return 3.0.pow(a).toInt() * 2 * 2\n }\n // 나머지가 0이면 별도로 처리하지 않는다\n return 3.0.pow(a).toInt()\n}\n</code></pre> max_product_cutting.rb<pre><code>### 분할 후의 최대 곱: 탐욕 ###\ndef max_product_cutting(n)\n # n &lt;= 3이면 반드시 1 하나가 나오도록 분할해야 한다\n return 1 * (n - 1) if n &lt;= 3\n # 탐욕적으로 3을 떼어 낸다. a는 3의 개수이고 b는 나머지이다\n a, b = n / 3, n % 3\n # 나머지가 1이면 1 * 3 한 쌍을 2 * 2로 바꾼다\n return (3.pow(a - 1) * 2 * 2).to_i if b == 1\n # 나머지가 2이면 별도로 처리하지 않는다\n return (3.pow(a) * 2).to_i if b == 2\n # 나머지가 0이면 별도로 처리하지 않는다\n 3.pow(a).to_i\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p></p> <p> 그림 15-16   정수 분할 최대 곱의 계산 방법 </p> <p>시간 복잡도는 프로그래밍 언어의 거듭제곱 구현 방식에 달려 있다. Python에서 자주 쓰는 거듭제곱 계산 방법은 세 가지다.</p> <ul> <li>연산자 <code>**</code>와 함수 <code>pow()</code>의 시간 복잡도는 모두 \\(O(\\log⁡ a)\\)이다.</li> <li>함수 <code>math.pow()</code>는 내부적으로 C 라이브러리의 <code>pow()</code>를 호출하여 부동 소수점 거듭제곱을 계산하며, 시간 복잡도는 \\(O(1)\\)이다.</li> </ul> <p>변수 \\(a\\)와 \\(b\\)는 상수 크기의 추가 공간을 사용하므로 공간 복잡도는 \\(O(1)\\)이다.</p>","path":["제 15 장   탐욕","15.4   정수 분할의 최대 곱 문제"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#3","level":3,"title":"3.   정확성 증명","text":"<p>귀류법을 사용하며 \\(n \\geq 4\\)인 경우만 분석한다.</p> <ol> <li>모든 인수는 \\(\\leq 3\\)이다: 최적 분할에 \\(\\geq 4\\)인 인수 \\(x\\)가 있다고 가정하자. 이를 \\(2(x-2)\\)로 더 나누면 곱이 커지거나 같아진다. 이는 가정과 모순이다.</li> <li>분할에 \\(1\\)이 없다: 최적 분할에 인수 \\(1\\)이 있다고 가정하자. 이를 다른 인수에 합치면 더 큰 곱을 얻으므로 가정과 모순이다.</li> <li>분할에 \\(2\\)는 최대 두 개다: 최적 분할에 \\(2\\)가 세 개 있다고 가정하자. \\(3\\) 두 개로 바꾸면 곱이 더 커지므로 가정과 모순이다.</li> </ol>","path":["제 15 장   탐욕","15.4   정수 분할의 최대 곱 문제"],"tags":[]},{"location":"chapter_greedy/summary/","level":1,"title":"15.5   요약","text":"","path":["제 15 장   탐욕","15.5   요약"],"tags":[]},{"location":"chapter_greedy/summary/#1","level":3,"title":"1.   핵심 정리","text":"<ul> <li>탐욕 알고리즘은 주로 최적화 문제에 사용하며, 매 결정 단계에서 국소 최적 선택을 내려 전역 최적해를 얻으려는 원리다.</li> <li>탐욕 알고리즘은 선택을 하나씩 반복하며 매번 문제를 더 작은 부분 문제로 바꾸고, 해결될 때까지 진행한다.</li> <li>탐욕 알고리즘은 구현이 간단하고 풀이 효율이 높다. 시간 복잡도는 보통 동적 계획법보다 낮다.</li> <li>동전 교환에서는 일부 동전 조합에 대해 최적해를 보장하지만, 다른 조합에서는 매우 나쁜 해를 찾을 수도 있다.</li> <li>탐욕 알고리즘에 적합한 문제는 탐욕적 선택 속성과 최적 부분 구조를 가진다. 탐욕적 선택 속성은 전략의 유효성을 나타낸다.</li> <li>복잡한 문제에서는 탐욕적 선택 속성의 증명이 쉽지 않다. 동전 교환처럼 반례로 부정하는 편이 상대적으로 쉬운 경우가 있다.</li> <li>탐욕 문제의 풀이는 문제 분석, 탐욕 전략 결정, 정확성 증명의 세 단계로 나뉜다. 전략 결정이 핵심이며, 정확성 증명이 어려운 경우가 많다.</li> <li>분할 가능한 배낭은 0-1 배낭에서 물건의 일부를 고를 수 있도록 바꾼 문제이므로 탐욕 알고리즘으로 풀 수 있다. 전략의 정확성은 귀류법으로 증명할 수 있다.</li> <li>최대 용량 문제는 모든 경우를 열거하면 시간 복잡도가 \\(O(n^2)\\)이다. 매번 낮은 칸막이를 안쪽으로 옮기는 탐욕 전략으로 \\(O(n)\\)까지 최적화할 수 있다.</li> <li>정수 분할의 최대 곱 문제에서는 \\(\\geq 4\\)인 정수를 계속 나누어야 하고 최적인 인수는 \\(3\\)이라는 두 탐욕 전략을 차례로 도출했다. 코드에는 거듭제곱이 포함되므로 시간 복잡도는 구현 방식에 따라 보통 \\(O(1)\\) 또는 \\(O(\\log n)\\)이다.</li> </ul>","path":["제 15 장   탐욕","15.5   요약"],"tags":[]},{"location":"chapter_hashing/","level":1,"title":"제 6 장   해시 테이블","text":"<p>이 장을 시작하며</p> <p>컴퓨터 세계에서 해시 테이블은 영리한 사서와 같다.</p> <p>청구 기호를 계산하는 방법을 알기에 원하는 책을 빠르게 찾아낸다.</p>","path":["제 6 장   해시 테이블"],"tags":[]},{"location":"chapter_hashing/#_1","level":2,"title":"이 장의 내용","text":"<ul> <li>6.1   해시 테이블</li> <li>6.2   해시 충돌</li> <li>6.3   해시 알고리즘</li> <li>6.4   요약</li> <li>6.5   연습 문제</li> </ul>","path":["제 6 장   해시 테이블"],"tags":[]},{"location":"chapter_hashing/exercises/","level":1,"title":"6.5   연습 문제","text":"","path":["제 6 장   해시 테이블","6.5   연습 문제"],"tags":[]},{"location":"chapter_hashing/exercises/#651","level":2,"title":"6.5.1   개념 확인","text":"","path":["제 6 장   해시 테이블","6.5   연습 문제"],"tags":[]},{"location":"chapter_hashing/exercises/#1","level":3,"title":"1.   해시 충돌이 일어난 뒤 탐색하는 방법","text":"<p>버킷이 5개인 해시 테이블의 해시 함수는 \\(h(x)=x \\bmod 5\\)다. 충돌이 나면 해당 버킷의 리스트에 원소를 순서대로 넣는다. <code>[1, 6, 11, 7]</code>을 차례대로 삽입한다.</p> <ol> <li>0~4번 버킷의 내용을 적어 보자.</li> <li>6을 찾을 때 어느 버킷으로 들어가고, 어떤 원소를 차례대로 확인하는가?</li> <li>1번에서 적은 버킷의 내용을 보면 나중에 삽입한 원소가 먼저 삽입한 원소를 덮어썼는가? 이 충돌 처리 방식과 연결하여 이유를 설명하라.</li> </ol> 참고 답안 <ol> <li> <p>\\(1\\bmod5=6\\bmod5=11\\bmod5=1\\)이고 \\(7\\bmod5=2\\)이므로 각 버킷의 내용은 다음과 같다.</p> <pre><code>0: []\n1: [1, 6, 11]\n2: [7]\n3: []\n4: []\n</code></pre> </li> <li> <p>6을 찾을 때는 먼저 1번 버킷으로 들어가 1, 6을 차례대로 비교한다. 두 번째 비교에서 목표를 찾는다.</p> </li> <li> <p>해시값이 같다는 것은 같은 버킷에 들어간다는 뜻일 뿐, 원소들이 같다는 뜻은 아니다. 체이닝은 충돌한 원소를 모두 버킷에 저장하고, 탐색할 때 하나씩 비교한다. 따라서 1, 6, 11은 서로를 덮어쓰지 않는다.</p> </li> </ol>","path":["제 6 장   해시 테이블","6.5   연습 문제"],"tags":[]},{"location":"chapter_hashing/exercises/#2","level":3,"title":"2.   해시 테이블의 용량을 늘리면 원소는 어디로 가는가","text":"<p>체이닝을 사용하는 해시 테이블에 원래 5개의 버킷이 있고, 해시 함수는 \\(h(x)=x\\bmod5\\)다. 키 <code>[1, 6, 11]</code>은 모두 1번 버킷에 있다.</p> <p>이제 버킷 수를 7개로 늘리고 해시 함수도 \\(h(x)=x\\bmod7\\)로 바꾼다.</p> <ol> <li>1, 6, 11의 새 버킷 번호를 각각 계산하라.</li> <li>용량을 늘린 뒤 어느 버킷에 원소가 들어 있는가?</li> <li>기존 1번 버킷의 리스트를 새 1번 버킷에 그대로 복사해도 되는가? 1번과 2번의 결과를 바탕으로 이유를 설명하라.</li> </ol> 참고 답안 <ol> <li> <p>새 버킷 번호는 각각 다음과 같다.</p> <ul> <li>\\(1\\bmod7=1\\)</li> <li>\\(6\\bmod7=6\\)</li> <li>\\(11\\bmod7=4\\)</li> </ul> </li> <li> <p>1번 버킷에 1, 4번 버킷에 11, 6번 버킷에 6이 저장된다. 세 키가 더 이상 하나의 버킷에 모여 있지 않다.</p> </li> <li> <p>그대로 복사해서는 안 된다. 버킷 번호는 키를 버킷 수로 나눈 나머지로 구한다. 버킷 수가 5에서 7로 바뀌면 같은 키의 버킷 번호도 달라질 수 있으므로, 모든 키의 위치를 다시 계산해야 한다. 이전 1번 버킷을 그대로 복사하면 새 식으로 6과 11을 찾을 때 각각 6번 버킷과 4번 버킷으로 가게 되어 찾지 못한다.</p> </li> </ol>","path":["제 6 장   해시 테이블","6.5   연습 문제"],"tags":[]},{"location":"chapter_hashing/exercises/#3-6-11","level":3,"title":"3.   6을 삭제한 뒤에도 11을 찾을 수 있는가","text":"<p>해시 테이블에 5개의 자리가 있고 인덱스는 <code>0~4</code>이며, 해시 함수는 \\(h(x)=x\\bmod5\\)다. 충돌이 나면 해시 함수로 계산한 인덱스부터 오른쪽으로 이동하며 처음 만나는 빈자리를 찾는다.</p> <p><code>[1, 6, 11]</code>을 차례대로 삽입한다.</p> <ol> <li>세 수는 최종적으로 각각 어느 인덱스에 저장되는가?</li> <li>11을 찾을 때 어떤 인덱스를 차례대로 확인하는가?</li> <li>6을 삭제하면서 그 자리를 ‘한 번도 사용하지 않은 빈자리’로 바꾸고, 탐색은 빈자리를 만나면 멈춘다고 하자. 다시 11을 찾으면 어떤 일이 일어나는가? 그 결과는 올바른가? 문제가 있다면 어떻게 피할 수 있는가?</li> </ol> 참고 답안 <ol> <li> <p>1은 인덱스 1에 저장된다. 6도 인덱스 1로 매핑되지만 충돌하므로 인덱스 2에 저장된다. 11 역시 인덱스 1부터 시작해 이미 사용 중인 인덱스 1과 2를 지나 인덱스 3에 저장된다.</p> </li> <li> <p>11을 찾을 때는 인덱스 <code>1, 2, 3</code>을 차례대로 확인하고 인덱스 3에서 찾는다.</p> </li> <li> <p>인덱스 2를 ‘한 번도 사용하지 않음’을 뜻하는 빈자리로 바꾸면, 11을 찾다가 인덱스 1을 확인한 뒤 인덱스 2에서 멈춘다. 따라서 11이 없다고 잘못 판단한다. 삭제할 때는 ‘삭제됨’ 표시를 남겨야 한다. 탐색 중 이 표시를 만나면 다음 인덱스를 계속 확인하고, 인덱스 4를 지나면 0으로 돌아간다. 이후 삽입할 때는 이 자리를 다시 사용할 수 있다.</p> </li> </ol>","path":["제 6 장   해시 테이블","6.5   연습 문제"],"tags":[]},{"location":"chapter_hashing/exercises/#652","level":2,"title":"6.5.2   프로그래밍 연습","text":"","path":["제 6 장   해시 테이블","6.5   연습 문제"],"tags":[]},{"location":"chapter_hashing/exercises/#1_1","level":3,"title":"1.   두 문자열의 문자 구성 비교하기","text":"<p>영어 소문자만 포함하는 두 문자열 <code>s</code>와 <code>t</code>가 주어진다. <code>s</code>의 문자 위치는 자유롭게 바꿀 수 있지만 문자를 추가하거나 삭제하거나 다른 문자로 바꿀 수는 없다.</p> <p>순서를 바꿔 <code>t</code>를 만들 수 있는지 판단하고, 가능하면 <code>true</code>, 그렇지 않으면 <code>false</code>를 반환하라. 문자열의 문자를 정렬하지 말고 해시 테이블로 각 문자의 등장 횟수를 기록하라.</p> 풀이 힌트 <ol> <li>두 문자열의 길이가 다르면 문자 구성도 반드시 다르다.</li> <li>해시 테이블에 각 문자의 개수를 기록한다. s를 순회할 때 해당 개수를 1씩 늘린다.</li> <li>t를 순회할 때 해당 개수를 1씩 줄인다. 마지막에 모든 개수가 0이어야 문자 구성이 같다.</li> </ol> <p>LeetCode</p>","path":["제 6 장   해시 테이블","6.5   연습 문제"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/","level":1,"title":"6.3   해시 알고리즘","text":"<p>앞의 두 절에서는 해시 테이블의 작동 원리와 해시 충돌 처리 방법을 알아보았다. 하지만 개방 주소법이든 체이닝이든 충돌이 발생해도 해시 테이블이 정상적으로 작동하게 할 뿐, 충돌 발생 자체를 줄이지는 못한다.</p> <p>충돌이 너무 자주 발생하면 성능이 급격히 나빠진다. 아래 그림(6-8)의 체이닝 해시 테이블에서 이상적인 경우는 키-값 쌍이 각 버킷에 고르게 분포하여 최고의 조회 효율을 얻는 것이다. 최악의 경우에는 모든 키-값 쌍이 같은 버킷에 들어가 시간 복잡도가 \\(O(n)\\)으로 나빠진다.</p> <p></p> <p> 그림 6-8   해시 충돌의 최선과 최악의 경우 </p> <p>키-값 쌍의 분포는 해시 함수가 결정한다. 해시 함수는 먼저 해시값을 계산하고 배열 길이로 나눈 나머지를 구한다.</p> <pre><code>index = hash(key) % capacity\n</code></pre> <p>위 식에서 해시 테이블 용량 <code>capacity</code>가 고정되면 해시 알고리즘 <code>hash()</code>가 출력값을 결정하며, 나아가 키-값 쌍의 분포를 결정한다.</p> <p>따라서 해시 충돌 확률을 낮추려면 해시 알고리즘 <code>hash()</code>의 설계에 집중해야 한다.</p>","path":["제 6 장   해시 테이블","6.3   해시 알고리즘"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#631","level":2,"title":"6.3.1   해시 알고리즘의 목표","text":"<p>‘빠르고 안정적인’ 해시 테이블을 구현하려면 해시 알고리즘이 다음 특징을 갖추어야 한다.</p> <ul> <li>결정성: 같은 입력에는 항상 같은 출력을 생성해야 한다. 그래야 해시 테이블을 신뢰할 수 있다.</li> <li>높은 효율: 해시값 계산이 충분히 빨라야 한다. 계산 비용이 작을수록 해시 테이블의 실용성이 높아진다.</li> <li>균등 분포: 키-값 쌍이 해시 테이블에 고르게 분포하도록 해야 한다. 분포가 고를수록 충돌 확률은 낮아진다.</li> </ul> <p>해시 알고리즘은 해시 테이블 외에도 여러 분야에서 널리 사용된다.</p> <ul> <li>비밀번호 저장: 사용자의 비밀번호를 보호하기 위해 시스템은 보통 평문 대신 해시값을 저장한다. 사용자가 비밀번호를 입력하면 해시값을 계산해 저장된 값과 비교하고, 두 값이 같으면 올바른 비밀번호로 판단한다.</li> <li>데이터 무결성 검사: 송신자는 데이터의 해시값을 계산해 함께 보낼 수 있다. 수신자는 받은 데이터의 해시값을 다시 계산하여 전달받은 해시값과 비교한다. 둘이 같으면 데이터가 온전하다고 판단한다.</li> </ul> <p>암호학 관련 응용에서는 해시값으로 원래 비밀번호를 추론하는 등의 역공학을 막기 위해 더 높은 수준의 보안 특성이 필요하다.</p> <ul> <li>일방향성: 해시값으로 입력 데이터에 관한 정보를 역으로 추론할 수 없어야 한다.</li> <li>충돌 저항성: 해시값이 같은 서로 다른 두 입력을 찾기가 극히 어려워야 한다.</li> <li>눈사태 효과: 입력의 작은 변화가 출력의 크고 예측할 수 없는 변화로 이어져야 한다.</li> </ul> <p>‘균등 분포’와 ‘충돌 저항성’은 서로 독립된 개념이다. 균등 분포를 만족한다고 충돌 저항성도 만족하는 것은 아니다. 예를 들어 <code>key</code>가 무작위로 입력되면 해시 함수 <code>key % 100</code>은 고르게 분포하는 출력을 만들 수 있다. 하지만 이 알고리즘은 너무 단순하여 마지막 두 자리가 같은 모든 <code>key</code>의 출력이 같다. 따라서 해시값에서 유효한 <code>key</code>를 쉽게 역추론하여 비밀번호를 깰 수 있다.</p>","path":["제 6 장   해시 테이블","6.3   해시 알고리즘"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#632","level":2,"title":"6.3.2   해시 알고리즘의 설계","text":"<p>해시 알고리즘 설계는 여러 요소를 고려해야 하는 복잡한 문제다. 다만 요구 수준이 높지 않은 상황에서는 간단한 알고리즘을 설계할 수 있다.</p> <ul> <li>덧셈 해싱: 입력의 각 문자에 해당하는 ASCII 코드를 모두 더한 값을 해시값으로 삼는다.</li> <li>곱셈 해싱: 곱셈의 비상관성을 이용하여 매번 상수를 곱하고, 각 문자의 ASCII 코드를 해시값에 누적한다.</li> <li>XOR 해싱: 입력 데이터의 각 원소를 XOR 연산으로 하나의 해시값에 누적한다.</li> <li>회전 해싱: 각 문자의 ASCII 코드를 해시값에 누적하되, 누적하기 전에 해시값에 회전 연산을 수행한다.</li> </ul> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby simple_hash.py<pre><code>def add_hash(key: str) -&gt; int:\n \"\"\"덧셈 해시\"\"\"\n hash = 0\n modulus = 1000000007\n for c in key:\n hash += ord(c)\n return hash % modulus\n\ndef mul_hash(key: str) -&gt; int:\n \"\"\"곱셈 해시\"\"\"\n hash = 0\n modulus = 1000000007\n for c in key:\n hash = 31 * hash + ord(c)\n return hash % modulus\n\ndef xor_hash(key: str) -&gt; int:\n \"\"\"XOR 해시\"\"\"\n hash = 0\n modulus = 1000000007\n for c in key:\n hash ^= ord(c)\n return hash % modulus\n\ndef rot_hash(key: str) -&gt; int:\n \"\"\"회전 해시\"\"\"\n hash = 0\n modulus = 1000000007\n for c in key:\n hash = (hash &lt;&lt; 4) ^ (hash &gt;&gt; 28) ^ ord(c)\n return hash % modulus\n</code></pre> simple_hash.cpp<pre><code>/* 덧셈 해시 */\nint addHash(string key) {\n long long hash = 0;\n const int MODULUS = 1000000007;\n for (unsigned char c : key) {\n hash = (hash + (int)c) % MODULUS;\n }\n return (int)hash;\n}\n\n/* 곱셈 해시 */\nint mulHash(string key) {\n long long hash = 0;\n const int MODULUS = 1000000007;\n for (unsigned char c : key) {\n hash = (31 * hash + (int)c) % MODULUS;\n }\n return (int)hash;\n}\n\n/* XOR 해시 */\nint xorHash(string key) {\n int hash = 0;\n const int MODULUS = 1000000007;\n for (unsigned char c : key) {\n hash ^= (int)c;\n }\n return hash &amp; MODULUS;\n}\n\n/* 회전 해시 */\nint rotHash(string key) {\n long long hash = 0;\n const int MODULUS = 1000000007;\n for (unsigned char c : key) {\n hash = ((hash &lt;&lt; 4) ^ (hash &gt;&gt; 28) ^ (int)c) % MODULUS;\n }\n return (int)hash;\n}\n</code></pre> simple_hash.java<pre><code>/* 덧셈 해시 */\nint addHash(String key) {\n long hash = 0;\n final int MODULUS = 1000000007;\n for (char c : key.toCharArray()) {\n hash = (hash + (int) c) % MODULUS;\n }\n return (int) hash;\n}\n\n/* 곱셈 해시 */\nint mulHash(String key) {\n long hash = 0;\n final int MODULUS = 1000000007;\n for (char c : key.toCharArray()) {\n hash = (31 * hash + (int) c) % MODULUS;\n }\n return (int) hash;\n}\n\n/* XOR 해시 */\nint xorHash(String key) {\n int hash = 0;\n final int MODULUS = 1000000007;\n for (char c : key.toCharArray()) {\n hash ^= (int) c;\n }\n return hash &amp; MODULUS;\n}\n\n/* 회전 해시 */\nint rotHash(String key) {\n long hash = 0;\n final int MODULUS = 1000000007;\n for (char c : key.toCharArray()) {\n hash = ((hash &lt;&lt; 4) ^ (hash &gt;&gt; 28) ^ (int) c) % MODULUS;\n }\n return (int) hash;\n}\n</code></pre> simple_hash.cs<pre><code>/* 덧셈 해시 */\nint AddHash(string key) {\n long hash = 0;\n const int MODULUS = 1000000007;\n foreach (char c in key) {\n hash = (hash + c) % MODULUS;\n }\n return (int)hash;\n}\n\n/* 곱셈 해시 */\nint MulHash(string key) {\n long hash = 0;\n const int MODULUS = 1000000007;\n foreach (char c in key) {\n hash = (31 * hash + c) % MODULUS;\n }\n return (int)hash;\n}\n\n/* XOR 해시 */\nint XorHash(string key) {\n int hash = 0;\n const int MODULUS = 1000000007;\n foreach (char c in key) {\n hash ^= c;\n }\n return hash &amp; MODULUS;\n}\n\n/* 회전 해시 */\nint RotHash(string key) {\n long hash = 0;\n const int MODULUS = 1000000007;\n foreach (char c in key) {\n hash = ((hash &lt;&lt; 4) ^ (hash &gt;&gt; 28) ^ c) % MODULUS;\n }\n return (int)hash;\n}\n</code></pre> simple_hash.go<pre><code>/* 덧셈 해시 */\nfunc addHash(key string) int {\n var hash int64\n var modulus int64\n\n modulus = 1000000007\n for _, b := range []byte(key) {\n hash = (hash + int64(b)) % modulus\n }\n return int(hash)\n}\n\n/* 곱셈 해시 */\nfunc mulHash(key string) int {\n var hash int64\n var modulus int64\n\n modulus = 1000000007\n for _, b := range []byte(key) {\n hash = (31*hash + int64(b)) % modulus\n }\n return int(hash)\n}\n\n/* XOR 해시 */\nfunc xorHash(key string) int {\n hash := 0\n modulus := 1000000007\n for _, b := range []byte(key) {\n fmt.Println(int(b))\n hash ^= int(b)\n hash = (31*hash + int(b)) % modulus\n }\n return hash &amp; modulus\n}\n\n/* 회전 해시 */\nfunc rotHash(key string) int {\n var hash int64\n var modulus int64\n\n modulus = 1000000007\n for _, b := range []byte(key) {\n hash = ((hash &lt;&lt; 4) ^ (hash &gt;&gt; 28) ^ int64(b)) % modulus\n }\n return int(hash)\n}\n</code></pre> simple_hash.swift<pre><code>/* 덧셈 해시 */\nfunc addHash(key: String) -&gt; Int {\n var hash = 0\n let MODULUS = 1_000_000_007\n for c in key {\n for scalar in c.unicodeScalars {\n hash = (hash + Int(scalar.value)) % MODULUS\n }\n }\n return hash\n}\n\n/* 곱셈 해시 */\nfunc mulHash(key: String) -&gt; Int {\n var hash = 0\n let MODULUS = 1_000_000_007\n for c in key {\n for scalar in c.unicodeScalars {\n hash = (31 * hash + Int(scalar.value)) % MODULUS\n }\n }\n return hash\n}\n\n/* XOR 해시 */\nfunc xorHash(key: String) -&gt; Int {\n var hash = 0\n let MODULUS = 1_000_000_007\n for c in key {\n for scalar in c.unicodeScalars {\n hash ^= Int(scalar.value)\n }\n }\n return hash &amp; MODULUS\n}\n\n/* 회전 해시 */\nfunc rotHash(key: String) -&gt; Int {\n var hash = 0\n let MODULUS = 1_000_000_007\n for c in key {\n for scalar in c.unicodeScalars {\n hash = ((hash &lt;&lt; 4) ^ (hash &gt;&gt; 28) ^ Int(scalar.value)) % MODULUS\n }\n }\n return hash\n}\n</code></pre> simple_hash.js<pre><code>/* 덧셈 해시 */\nfunction addHash(key) {\n let hash = 0;\n const MODULUS = 1000000007;\n for (const c of key) {\n hash = (hash + c.charCodeAt(0)) % MODULUS;\n }\n return hash;\n}\n\n/* 곱셈 해시 */\nfunction mulHash(key) {\n let hash = 0;\n const MODULUS = 1000000007;\n for (const c of key) {\n hash = (31 * hash + c.charCodeAt(0)) % MODULUS;\n }\n return hash;\n}\n\n/* XOR 해시 */\nfunction xorHash(key) {\n let hash = 0;\n const MODULUS = 1000000007;\n for (const c of key) {\n hash ^= c.charCodeAt(0);\n }\n return hash % MODULUS;\n}\n\n/* 회전 해시 */\nfunction rotHash(key) {\n let hash = 0;\n const MODULUS = 1000000007;\n for (const c of key) {\n hash = ((hash &lt;&lt; 4) ^ (hash &gt;&gt; 28) ^ c.charCodeAt(0)) % MODULUS;\n }\n return hash;\n}\n</code></pre> simple_hash.ts<pre><code>/* 덧셈 해시 */\nfunction addHash(key: string): number {\n let hash = 0;\n const MODULUS = 1000000007;\n for (const c of key) {\n hash = (hash + c.charCodeAt(0)) % MODULUS;\n }\n return hash;\n}\n\n/* 곱셈 해시 */\nfunction mulHash(key: string): number {\n let hash = 0;\n const MODULUS = 1000000007;\n for (const c of key) {\n hash = (31 * hash + c.charCodeAt(0)) % MODULUS;\n }\n return hash;\n}\n\n/* XOR 해시 */\nfunction xorHash(key: string): number {\n let hash = 0;\n const MODULUS = 1000000007;\n for (const c of key) {\n hash ^= c.charCodeAt(0);\n }\n return hash % MODULUS;\n}\n\n/* 회전 해시 */\nfunction rotHash(key: string): number {\n let hash = 0;\n const MODULUS = 1000000007;\n for (const c of key) {\n hash = ((hash &lt;&lt; 4) ^ (hash &gt;&gt; 28) ^ c.charCodeAt(0)) % MODULUS;\n }\n return hash;\n}\n</code></pre> simple_hash.dart<pre><code>/* 덧셈 해시 */\nint addHash(String key) {\n int hash = 0;\n final int MODULUS = 1000000007;\n for (int i = 0; i &lt; key.length; i++) {\n hash = (hash + key.codeUnitAt(i)) % MODULUS;\n }\n return hash;\n}\n\n/* 곱셈 해시 */\nint mulHash(String key) {\n int hash = 0;\n final int MODULUS = 1000000007;\n for (int i = 0; i &lt; key.length; i++) {\n hash = (31 * hash + key.codeUnitAt(i)) % MODULUS;\n }\n return hash;\n}\n\n/* XOR 해시 */\nint xorHash(String key) {\n int hash = 0;\n final int MODULUS = 1000000007;\n for (int i = 0; i &lt; key.length; i++) {\n hash ^= key.codeUnitAt(i);\n }\n return hash &amp; MODULUS;\n}\n\n/* 회전 해시 */\nint rotHash(String key) {\n int hash = 0;\n final int MODULUS = 1000000007;\n for (int i = 0; i &lt; key.length; i++) {\n hash = ((hash &lt;&lt; 4) ^ (hash &gt;&gt; 28) ^ key.codeUnitAt(i)) % MODULUS;\n }\n return hash;\n}\n</code></pre> simple_hash.rs<pre><code>/* 덧셈 해시 */\nfn add_hash(key: &amp;str) -&gt; i32 {\n let mut hash = 0_i64;\n const MODULUS: i64 = 1000000007;\n\n for c in key.chars() {\n hash = (hash + c as i64) % MODULUS;\n }\n\n hash as i32\n}\n\n/* 곱셈 해시 */\nfn mul_hash(key: &amp;str) -&gt; i32 {\n let mut hash = 0_i64;\n const MODULUS: i64 = 1000000007;\n\n for c in key.chars() {\n hash = (31 * hash + c as i64) % MODULUS;\n }\n\n hash as i32\n}\n\n/* XOR 해시 */\nfn xor_hash(key: &amp;str) -&gt; i32 {\n let mut hash = 0_i64;\n const MODULUS: i64 = 1000000007;\n\n for c in key.chars() {\n hash ^= c as i64;\n }\n\n (hash &amp; MODULUS) as i32\n}\n\n/* 회전 해시 */\nfn rot_hash(key: &amp;str) -&gt; i32 {\n let mut hash = 0_i64;\n const MODULUS: i64 = 1000000007;\n\n for c in key.chars() {\n hash = ((hash &lt;&lt; 4) ^ (hash &gt;&gt; 28) ^ c as i64) % MODULUS;\n }\n\n hash as i32\n}\n</code></pre> simple_hash.c<pre><code>/* 덧셈 해시 */\nint addHash(char *key) {\n long long hash = 0;\n const int MODULUS = 1000000007;\n for (int i = 0; i &lt; strlen(key); i++) {\n hash = (hash + (unsigned char)key[i]) % MODULUS;\n }\n return (int)hash;\n}\n\n/* 곱셈 해시 */\nint mulHash(char *key) {\n long long hash = 0;\n const int MODULUS = 1000000007;\n for (int i = 0; i &lt; strlen(key); i++) {\n hash = (31 * hash + (unsigned char)key[i]) % MODULUS;\n }\n return (int)hash;\n}\n\n/* XOR 해시 */\nint xorHash(char *key) {\n int hash = 0;\n const int MODULUS = 1000000007;\n\n for (int i = 0; i &lt; strlen(key); i++) {\n hash ^= (unsigned char)key[i];\n }\n return hash &amp; MODULUS;\n}\n\n/* 회전 해시 */\nint rotHash(char *key) {\n long long hash = 0;\n const int MODULUS = 1000000007;\n for (int i = 0; i &lt; strlen(key); i++) {\n hash = ((hash &lt;&lt; 4) ^ (hash &gt;&gt; 28) ^ (unsigned char)key[i]) % MODULUS;\n }\n\n return (int)hash;\n}\n</code></pre> simple_hash.kt<pre><code>/* 덧셈 해시 */\nfun addHash(key: String): Int {\n var hash = 0L\n val MODULUS = 1000000007\n for (c in key.toCharArray()) {\n hash = (hash + c.code) % MODULUS\n }\n return hash.toInt()\n}\n\n/* 곱셈 해시 */\nfun mulHash(key: String): Int {\n var hash = 0L\n val MODULUS = 1000000007\n for (c in key.toCharArray()) {\n hash = (31 * hash + c.code) % MODULUS\n }\n return hash.toInt()\n}\n\n/* XOR 해시 */\nfun xorHash(key: String): Int {\n var hash = 0\n val MODULUS = 1000000007\n for (c in key.toCharArray()) {\n hash = hash xor c.code\n }\n return hash and MODULUS\n}\n\n/* 회전 해시 */\nfun rotHash(key: String): Int {\n var hash = 0L\n val MODULUS = 1000000007\n for (c in key.toCharArray()) {\n hash = ((hash shl 4) xor (hash shr 28) xor c.code.toLong()) % MODULUS\n }\n return hash.toInt()\n}\n</code></pre> simple_hash.rb<pre><code>### 덧셈 해시 ###\ndef add_hash(key)\n hash = 0\n modulus = 1_000_000_007\n\n key.each_char { |c| hash += c.ord }\n\n hash % modulus\nend\n\n### 곱셈 해시 ###\ndef mul_hash(key)\n hash = 0\n modulus = 1_000_000_007\n\n key.each_char { |c| hash = 31 * hash + c.ord }\n\n hash % modulus\nend\n\n### XOR 해시 ###\ndef xor_hash(key)\n hash = 0\n modulus = 1_000_000_007\n\n key.each_char { |c| hash ^= c.ord }\n\n hash % modulus\nend\n\n### 회전 해시 ###\ndef rot_hash(key)\n hash = 0\n modulus = 1_000_000_007\n\n key.each_char { |c| hash = (hash &lt;&lt; 4) ^ (hash &gt;&gt; 28) ^ c.ord }\n\n hash % modulus\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>각 알고리즘의 마지막 단계에서는 큰 소수 \\(1000000007\\)로 나눈 나머지를 구하여 해시값이 적절한 범위에 있도록 한다. 왜 소수로 나누어야 할까? 합성수로 나누면 어떤 단점이 있을까? 생각해 볼 만한 흥미로운 문제다.</p> <p>결론부터 말하면 큰 소수를 모듈러 연산의 법으로 사용하면 해시값의 균등 분포를 최대한 보장할 수 있다. 소수는 다른 수와 공약수를 갖지 않으므로 나머지 연산에서 생기는 주기적 패턴을 줄여 충돌을 피할 수 있기 때문이다.</p> <p>예를 들어 합성수 \\(9\\)를 법으로 선택했다고 하자. 이 수는 \\(3\\)으로 나누어떨어지므로, \\(3\\)으로 나누어떨어지는 모든 <code>key</code>는 \\(0\\), \\(3\\), \\(6\\)의 세 해시값에 매핑된다.</p> \\[ \\begin{aligned} \\text{modulus} &amp; = 9 \\newline \\text{key} &amp; = \\{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \\dots \\} \\newline \\text{hash} &amp; = \\{ 0, 3, 6, 0, 3, 6, 0, 3, 6, 0, 3, 6,\\dots \\} \\end{aligned} \\] <p>입력 <code>key</code>가 마침 이런 등차수열 분포를 따르면 해시값에 군집이 생겨 충돌이 심해진다. 이제 <code>modulus</code>를 소수 \\(13\\)으로 바꾸면 <code>key</code>와 <code>modulus</code> 사이에 공약수가 없으므로 출력 해시값의 균등성이 뚜렷하게 좋아진다.</p> \\[ \\begin{aligned} \\text{modulus} &amp; = 13 \\newline \\text{key} &amp; = \\{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \\dots \\} \\newline \\text{hash} &amp; = \\{ 0, 3, 6, 9, 12, 2, 5, 8, 11, 1, 4, 7, \\dots \\} \\end{aligned} \\] <p><code>key</code>가 무작위로 균등하게 분포함을 보장할 수 있다면 소수와 합성수 중 어느 것을 법으로 써도 균등한 해시값을 얻을 수 있다. 반면 <code>key</code>의 분포에 주기성이 있으면 합성수로 나눌 때 군집이 생기기 쉽다.</p> <p>따라서 보통 소수를 법으로 선택하며, 가능한 한 큰 소수를 사용해 주기적 패턴을 줄이고 해시 알고리즘의 안정성을 높인다.</p>","path":["제 6 장   해시 테이블","6.3   해시 알고리즘"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#633","level":2,"title":"6.3.3   대표적인 해시 알고리즘","text":"<p>앞에서 소개한 간단한 해시 알고리즘은 비교적 ‘취약하여’ 설계 목표에 크게 못 미친다. 예를 들어 덧셈과 XOR는 교환법칙을 만족하므로, 이 두 해싱 방식은 내용은 같고 순서만 다른 문자열을 구분하지 못한다. 이는 해시 충돌을 늘리고 보안 문제를 일으킬 수 있다.</p> <p>실제로는 MD5, SHA-1, SHA-2, SHA-3 같은 표준 해시 알고리즘을 주로 사용한다. 이들은 길이가 임의인 입력 데이터를 고정 길이의 해시값으로 매핑한다.</p> <p>지난 한 세기 가까이 해시 알고리즘은 계속 개선되고 최적화되어 왔다. 일부 연구자는 성능을 높이는 데 힘쓰고, 다른 연구자와 해커는 보안 문제를 찾는 데 집중한다. 아래 표(6-2)는 실제 응용에서 흔히 쓰이는 해시 알고리즘을 보여 준다.</p> <ul> <li>MD5와 SHA-1은 여러 차례 공격에 성공한 사례가 있어 각종 보안 응용에서 사용이 중단되었다.</li> <li>SHA-2 계열의 SHA-256은 가장 안전한 해시 알고리즘 중 하나로, 아직 성공한 공격 사례가 없어 각종 보안 응용과 프로토콜에서 자주 사용된다.</li> <li>SHA-3는 SHA-2보다 구현 비용이 낮고 계산 효율이 높지만, 현재 보급 범위는 SHA-2 계열에 못 미친다.</li> </ul> <p> 표 6-2   대표적인 해시 알고리즘 </p> MD5 SHA-1 SHA-2 SHA-3 발표 시기 1992 1995 2002 2008 출력 길이 128 bit 160 bit 256/512 bit 224/256/384/512 bit 해시 충돌 비교적 많음 비교적 많음 매우 적음 매우 적음 보안 수준 낮음, 공격 성공 사례가 있음 낮음, 공격 성공 사례가 있음 높음 높음 활용 사용 중단, 데이터 무결성 검사에는 여전히 사용 사용 중단 암호화폐 거래 검증, 전자 서명 등 SHA-2를 대체할 수 있음","path":["제 6 장   해시 테이블","6.3   해시 알고리즘"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#634","level":2,"title":"6.3.4   자료 구조의 해시값","text":"<p>해시 테이블의 <code>key</code>는 정수, 소수, 문자열 등의 자료형일 수 있다. 프로그래밍 언어는 보통 이러한 자료형에 내장 해시 알고리즘을 제공하여 버킷 인덱스를 계산한다. 예를 들어 Python에서는 <code>hash()</code> 함수로 여러 자료형의 해시값을 구할 수 있다.</p> <ul> <li>정수와 불리언의 해시값은 그 값 자체다.</li> <li>부동 소수점 수와 문자열의 해시값 계산은 더 복잡하다. 관심 있는 독자는 따로 알아보자.</li> <li>튜플은 각 원소의 해시값을 계산한 뒤 이를 결합하여 하나의 해시값을 얻는다.</li> <li>객체의 해시값은 메모리 주소를 기반으로 생성된다. 객체의 해시 메서드를 재정의하면 내용을 기반으로 해시값을 만들 수도 있다.</li> </ul> <p>참고</p> <p>프로그래밍 언어마다 내장 해시값 계산 함수의 정의와 방식이 다르다는 점에 주의하자.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby built_in_hash.py<pre><code>num = 3\nhash_num = hash(num)\n# 정수 3의 해시값은 3\n\nbol = True\nhash_bol = hash(bol)\n# 불리언 True의 해시값은 1\n\ndec = 3.14159\nhash_dec = hash(dec)\n# 소수 3.14159의 해시값은 326484311674566659\n\nstr = \"Hello 算法\"\nhash_str = hash(str)\n# 문자열 “Hello 算法”의 해시값은 4617003410720528961\n\ntup = (12836, \"小哈\")\nhash_tup = hash(tup)\n# 튜플 (12836, '小哈')의 해시값은 1029005403108185979\n\nobj = ListNode(0)\nhash_obj = hash(obj)\n# 노드 객체 &lt;ListNode object at 0x1058fd810&gt;의 해시값은 274267521\n</code></pre> built_in_hash.cpp<pre><code>int num = 3;\nsize_t hashNum = hash&lt;int&gt;()(num);\n// 정수 3의 해시값은 3\n\nbool bol = true;\nsize_t hashBol = hash&lt;bool&gt;()(bol);\n// 불리언 1의 해시값은 1\n\ndouble dec = 3.14159;\nsize_t hashDec = hash&lt;double&gt;()(dec);\n// 소수 3.14159의 해시값은 4614256650576692846\n\nstring str = \"Hello 算法\";\nsize_t hashStr = hash&lt;string&gt;()(str);\n// 문자열 “Hello 算法”의 해시값은 15466937326284535026\n\n// C++의 내장 std::hash()는 기본 자료형의 해시값 계산만 제공한다\n// 배열과 객체의 해시값 계산은 직접 구현해야 한다\n</code></pre> built_in_hash.java<pre><code>int num = 3;\nint hashNum = Integer.hashCode(num);\n// 정수 3의 해시값은 3\n\nboolean bol = true;\nint hashBol = Boolean.hashCode(bol);\n// 불리언 true의 해시값은 1231\n\ndouble dec = 3.14159;\nint hashDec = Double.hashCode(dec);\n// 소수 3.14159의 해시값은 -1340954729\n\nString str = \"Hello 算法\";\nint hashStr = str.hashCode();\n// 문자열 “Hello 算法”의 해시값은 -727081396\n\nObject[] arr = { 12836, \"小哈\" };\nint hashTup = Arrays.hashCode(arr);\n// 배열 [12836, 小哈]의 해시값은 1151158\n\nListNode obj = new ListNode(0);\nint hashObj = obj.hashCode();\n// 노드 객체 utils.ListNode@7dc5e7b4의 해시값은 2110121908\n</code></pre> built_in_hash.cs<pre><code>int num = 3;\nint hashNum = num.GetHashCode();\n// 정수 3의 해시값은 3;\n\nbool bol = true;\nint hashBol = bol.GetHashCode();\n// 불리언 true의 해시값은 1;\n\ndouble dec = 3.14159;\nint hashDec = dec.GetHashCode();\n// 소수 3.14159의 해시값은 -1340954729;\n\nstring str = \"Hello 算法\";\nint hashStr = str.GetHashCode();\n// 문자열 “Hello 算法”의 해시값은 -586107568;\n\nobject[] arr = [12836, \"小哈\"];\nint hashTup = arr.GetHashCode();\n// 배열 [12836, 小哈]의 해시값은 42931033;\n\nListNode obj = new(0);\nint hashObj = obj.GetHashCode();\n// 노드 객체 0의 해시값은 39053774;\n</code></pre> built_in_hash.go<pre><code>// Go는 내장 hash code 함수를 제공하지 않는다\n</code></pre> built_in_hash.swift<pre><code>let num = 3\nlet hashNum = num.hashValue\n// 정수 3의 해시값은 9047044699613009734\n\nlet bol = true\nlet hashBol = bol.hashValue\n// 불리언 true의 해시값은 -4431640247352757451\n\nlet dec = 3.14159\nlet hashDec = dec.hashValue\n// 소수 3.14159의 해시값은 -2465384235396674631\n\nlet str = \"Hello 算法\"\nlet hashStr = str.hashValue\n// 문자열 “Hello 算法”의 해시값은 -7850626797806988787\n\nlet arr = [AnyHashable(12836), AnyHashable(\"小哈\")]\nlet hashTup = arr.hashValue\n// 배열 [AnyHashable(12836), AnyHashable(\"小哈\")]의 해시값은 -2308633508154532996\n\nlet obj = ListNode(x: 0)\nlet hashObj = obj.hashValue\n// 노드 객체 utils.ListNode의 해시값은 -2434780518035996159\n</code></pre> built_in_hash.js<pre><code>// JavaScript는 내장 hash code 함수를 제공하지 않는다\n</code></pre> built_in_hash.ts<pre><code>// TypeScript는 내장 hash code 함수를 제공하지 않는다\n</code></pre> built_in_hash.dart<pre><code>int num = 3;\nint hashNum = num.hashCode;\n// 정수 3의 해시값은 34803\n\nbool bol = true;\nint hashBol = bol.hashCode;\n// 불리언 true의 해시값은 1231\n\ndouble dec = 3.14159;\nint hashDec = dec.hashCode;\n// 소수 3.14159의 해시값은 2570631074981783\n\nString str = \"Hello 算法\";\nint hashStr = str.hashCode;\n// 문자열 “Hello 算法”의 해시값은 468167534\n\nList arr = [12836, \"小哈\"];\nint hashArr = arr.hashCode;\n// 배열 [12836, 小哈]의 해시값은 976512528\n\nListNode obj = new ListNode(0);\nint hashObj = obj.hashCode;\n// 노드 객체 Instance of 'ListNode'의 해시값은 1033450432\n</code></pre> built_in_hash.rs<pre><code>use std::collections::hash_map::DefaultHasher;\nuse std::hash::{Hash, Hasher};\n\nlet num = 3;\nlet mut num_hasher = DefaultHasher::new();\nnum.hash(&amp;mut num_hasher);\nlet hash_num = num_hasher.finish();\n// 정수 3의 해시값은 568126464209439262\n\nlet bol = true;\nlet mut bol_hasher = DefaultHasher::new();\nbol.hash(&amp;mut bol_hasher);\nlet hash_bol = bol_hasher.finish();\n// 불리언 true의 해시값은 4952851536318644461\n\nlet dec: f32 = 3.14159;\nlet mut dec_hasher = DefaultHasher::new();\ndec.to_bits().hash(&amp;mut dec_hasher);\nlet hash_dec = dec_hasher.finish();\n// 소수 3.14159의 해시값은 2566941990314602357\n\nlet str = \"Hello 算法\";\nlet mut str_hasher = DefaultHasher::new();\nstr.hash(&amp;mut str_hasher);\nlet hash_str = str_hasher.finish();\n// 문자열 “Hello 算法”의 해시값은 16092673739211250988\n\nlet arr = (&amp;12836, &amp;\"小哈\");\nlet mut tup_hasher = DefaultHasher::new();\narr.hash(&amp;mut tup_hasher);\nlet hash_tup = tup_hasher.finish();\n// 튜플 (12836, \"小哈\")의 해시값은 1885128010422702749\n\nlet node = ListNode::new(42);\nlet mut hasher = DefaultHasher::new();\nnode.borrow().val.hash(&amp;mut hasher);\nlet hash = hasher.finish();\n// 노드 객체 RefCell { value: ListNode { val: 42, next: None } }의 해시값은 15387811073369036852\n</code></pre> built_in_hash.c<pre><code>// C는 내장 hash code 함수를 제공하지 않는다\n</code></pre> built_in_hash.kt<pre><code>val num = 3\nval hashNum = num.hashCode()\n// 정수 3의 해시값은 3\n\nval bol = true\nval hashBol = bol.hashCode()\n// 불리언 true의 해시값은 1231\n\nval dec = 3.14159\nval hashDec = dec.hashCode()\n// 소수 3.14159의 해시값은 -1340954729\n\nval str = \"Hello 算法\"\nval hashStr = str.hashCode()\n// 문자열 “Hello 算法”의 해시값은 -727081396\n\nval arr = arrayOf&lt;Any&gt;(12836, \"小哈\")\nval hashTup = arr.hashCode()\n// 배열 [12836, 小哈]의 해시값은 189568618\n\nval obj = ListNode(0)\nval hashObj = obj.hashCode()\n// 노드 객체 utils.ListNode@1d81eb93의 해시값은 495053715\n</code></pre> built_in_hash.rb<pre><code>num = 3\nhash_num = num.hash\n# 정수 3의 해시값은 -4385856518450339636\n\nbol = true\nhash_bol = bol.hash\n# 불리언 true의 해시값은 -1617938112149317027\n\ndec = 3.14159\nhash_dec = dec.hash\n# 소수 3.14159의 해시값은 -1479186995943067893\n\nstr = \"Hello 算法\"\nhash_str = str.hash\n# 문자열 “Hello 算法”의 해시값은 -4075943250025831763\n\ntup = [12836, '小哈']\nhash_tup = tup.hash\n# 튜플 (12836, '小哈')의 해시값은 1999544809202288822\n\nobj = ListNode.new(0)\nhash_obj = obj.hash\n# 노드 객체 #&lt;ListNode:0x000078133140ab70&gt;의 해시값은 4302940560806366381\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>많은 프로그래밍 언어에서는 불변 객체만 해시 테이블의 <code>key</code>로 사용할 수 있다. 리스트(동적 배열)를 <code>key</code>로 사용하면 내용이 바뀔 때 해시값도 달라져, 기존의 <code>value</code>를 조회할 수 없게 된다.</p> <p>연결 리스트 노드 같은 사용자 정의 객체는 멤버 변수가 바뀔 수 있어도 해시 가능하다. 객체의 해시값은 보통 메모리 주소를 기반으로 생성되기 때문이다. 내용이 바뀌어도 메모리 주소가 그대로면 해시값도 바뀌지 않는다.</p> <p>서로 다른 콘솔에서 실행하면 출력되는 해시값이 다르다는 점을 발견했을 것이다. Python 인터프리터가 시작될 때마다 문자열 해시 함수에 무작위 솔트(salt) 값을 추가하기 때문이다. 이 방식은 HashDoS 공격을 효과적으로 방지하여 해시 알고리즘의 보안성을 높인다.</p>","path":["제 6 장   해시 테이블","6.3   해시 알고리즘"],"tags":[]},{"location":"chapter_hashing/hash_collision/","level":1,"title":"6.2   해시 충돌","text":"<p>앞 절에서 설명했듯이 해시 함수의 입력 공간은 보통 출력 공간보다 훨씬 크므로, 이론적으로 해시 충돌은 피할 수 없다. 예를 들어 입력 공간이 모든 정수이고 출력 공간이 배열 용량만큼이라면, 여러 정수가 같은 버킷 인덱스에 매핑될 수밖에 없다.</p> <p>해시 충돌은 잘못된 조회 결과를 일으켜 해시 테이블의 사용 가능성을 크게 해친다. 이를 해결하려면 충돌이 생길 때마다 해시 테이블을 확장하여 충돌이 없어질 때까지 반복할 수 있다. 단순하고 효과적인 방법이지만, 확장할 때 많은 데이터를 옮기고 해시값을 계산해야 하므로 효율은 매우 낮다. 효율을 높이려면 다음 전략을 사용할 수 있다.</p> <ol> <li>해시 테이블의 자료 구조를 개선하여 해시 충돌이 발생해도 정상적으로 작동하게 한다.</li> <li>필요한 경우, 즉 해시 충돌이 심할 때만 용량을 확장한다.</li> </ol> <p>해시 테이블의 구조를 개선하는 대표적인 방법은 ‘체이닝’과 ‘개방 주소법’이다.</p>","path":["제 6 장   해시 테이블","6.2   해시 충돌"],"tags":[]},{"location":"chapter_hashing/hash_collision/#621","level":2,"title":"6.2.1   체이닝","text":"<p>기본 해시 테이블에서는 버킷 하나에 키-값 쌍 하나만 저장할 수 있다. 체이닝(separate chaining)은 단일 원소를 연결 리스트로 바꾸고, 키-값 쌍을 노드로 삼아 충돌한 키-값 쌍을 모두 같은 연결 리스트에 저장한다. 아래 그림(6-5)은 체이닝 해시 테이블의 예다.</p> <p></p> <p> 그림 6-5   체이닝 해시 테이블 </p> <p>체이닝 해시 테이블에서는 연산 방식이 다음과 같이 바뀐다.</p> <ul> <li>원소 조회: <code>key</code>를 입력하여 해시 함수로 버킷 인덱스를 구한 뒤 연결 리스트의 머리 노드에 접근한다. 이후 리스트를 순회하며 <code>key</code>를 비교하여 목표 키-값 쌍을 찾는다.</li> <li>원소 추가: 해시 함수로 연결 리스트의 머리 노드에 접근한 뒤, 노드(키-값 쌍)를 리스트에 추가한다.</li> <li>원소 삭제: 해시 함수의 결과로 연결 리스트의 맨 앞에 접근한 뒤, 순회하여 목표 노드를 찾아 삭제한다.</li> </ul> <p>체이닝에는 다음과 같은 한계가 있다.</p> <ul> <li>공간 사용량 증가: 연결 리스트에는 노드 포인터가 있으므로 배열보다 메모리를 더 많이 사용한다.</li> <li>조회 효율 감소: 해당 원소를 찾으려면 연결 리스트를 선형으로 순회해야 한다.</li> </ul> <p>다음 코드는 간단한 체이닝 해시 테이블을 구현한다. 두 가지에 주의하자.</p> <ul> <li>코드를 단순화하기 위해 연결 리스트 대신 리스트(동적 배열)를 사용한다. 따라서 해시 테이블(배열)은 여러 버킷을 포함하며, 각 버킷은 리스트다.</li> <li>아래 구현에는 용량 확장 메서드가 포함된다. 적재율이 \\(\\frac{2}{3}\\)를 넘으면 해시 테이블을 기존 크기의 \\(2\\)배로 늘린다.</li> </ul> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map_chaining.py<pre><code>class HashMapChaining:\n \"\"\"체이닝 해시 테이블\"\"\"\n\n def __init__(self):\n \"\"\"생성자\"\"\"\n self.size = 0 # 키-값 쌍 개수\n self.capacity = 4 # 해시 테이블 용량\n self.load_thres = 2.0 / 3.0 # 용량 확장을 수행하는 적재율 임계값\n self.extend_ratio = 2 # 용량 확장 배수\n self.buckets = [[] for _ in range(self.capacity)] # 버킷 배열\n\n def hash_func(self, key: int) -&gt; int:\n \"\"\"해시 함수\"\"\"\n return key % self.capacity\n\n def load_factor(self) -&gt; float:\n \"\"\"적재율\"\"\"\n return self.size / self.capacity\n\n def get(self, key: int) -&gt; str | None:\n \"\"\"조회 연산\"\"\"\n index = self.hash_func(key)\n bucket = self.buckets[index]\n # 버킷을 순회하다 key를 찾으면 해당 val을 반환한다\n for pair in bucket:\n if pair.key == key:\n return pair.val\n # key를 찾지 못하면 None을 반환한다\n return None\n\n def put(self, key: int, val: str):\n \"\"\"추가 연산\"\"\"\n # 적재율이 임계값을 초과하면 용량을 확장한다\n if self.load_factor() &gt; self.load_thres:\n self.extend()\n index = self.hash_func(key)\n bucket = self.buckets[index]\n # 버킷을 순회하다 지정한 key를 만나면 해당 val을 갱신하고 반환한다\n for pair in bucket:\n if pair.key == key:\n pair.val = val\n return\n # 해당 key가 없으면 키-값 쌍을 맨 뒤에 추가한다\n pair = Pair(key, val)\n bucket.append(pair)\n self.size += 1\n\n def remove(self, key: int):\n \"\"\"삭제 연산\"\"\"\n index = self.hash_func(key)\n bucket = self.buckets[index]\n # 버킷을 순회하며 키-값 쌍을 삭제한다\n for pair in bucket:\n if pair.key == key:\n bucket.remove(pair)\n self.size -= 1\n break\n\n def extend(self):\n \"\"\"해시 테이블 용량 확장\"\"\"\n # 원래 해시 테이블을 임시로 저장한다\n buckets = self.buckets\n # 용량을 확장한 새 해시 테이블을 초기화한다\n self.capacity *= self.extend_ratio\n self.buckets = [[] for _ in range(self.capacity)]\n self.size = 0\n # 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n for bucket in buckets:\n for pair in bucket:\n self.put(pair.key, pair.val)\n\n def print(self):\n \"\"\"해시 테이블 출력\"\"\"\n for bucket in self.buckets:\n res = []\n for pair in bucket:\n res.append(str(pair.key) + \" -&gt; \" + pair.val)\n print(res)\n</code></pre> hash_map_chaining.cpp<pre><code>/* 체이닝 해시 테이블 */\nclass HashMapChaining {\n private:\n int size; // 키-값 쌍 개수\n int capacity; // 해시 테이블 용량\n double loadThres; // 용량 확장을 수행하는 적재율 임계값\n int extendRatio; // 용량 확장 배수\n vector&lt;vector&lt;Pair *&gt;&gt; buckets; // 버킷 배열\n\n public:\n /* 생성자 */\n HashMapChaining() : size(0), capacity(4), loadThres(2.0 / 3.0), extendRatio(2) {\n buckets.resize(capacity);\n }\n\n /* 소멸자 */\n ~HashMapChaining() {\n for (auto &amp;bucket : buckets) {\n for (Pair *pair : bucket) {\n // 메모리 해제\n delete pair;\n }\n }\n }\n\n /* 해시 함수 */\n int hashFunc(int key) {\n return key % capacity;\n }\n\n /* 적재율 */\n double loadFactor() {\n return (double)size / (double)capacity;\n }\n\n /* 조회 연산 */\n string get(int key) {\n int index = hashFunc(key);\n // 버킷을 순회하다 key를 찾으면 해당 val을 반환한다\n for (Pair *pair : buckets[index]) {\n if (pair-&gt;key == key) {\n return pair-&gt;val;\n }\n }\n // key를 찾지 못하면 빈 문자열을 반환한다\n return \"\";\n }\n\n /* 추가 연산 */\n void put(int key, string val) {\n // 적재율이 임계값을 초과하면 용량을 확장한다\n if (loadFactor() &gt; loadThres) {\n extend();\n }\n int index = hashFunc(key);\n // 버킷을 순회하다 지정한 key를 만나면 해당 val을 갱신하고 반환한다\n for (Pair *pair : buckets[index]) {\n if (pair-&gt;key == key) {\n pair-&gt;val = val;\n return;\n }\n }\n // 해당 key가 없으면 키-값 쌍을 맨 뒤에 추가한다\n buckets[index].push_back(new Pair(key, val));\n size++;\n }\n\n /* 삭제 연산 */\n void remove(int key) {\n int index = hashFunc(key);\n auto &amp;bucket = buckets[index];\n // 버킷을 순회하며 키-값 쌍을 삭제한다\n for (int i = 0; i &lt; bucket.size(); i++) {\n if (bucket[i]-&gt;key == key) {\n Pair *tmp = bucket[i];\n bucket.erase(bucket.begin() + i); // 해당 키-값 쌍을 삭제한다\n delete tmp; // 메모리 해제\n size--;\n return;\n }\n }\n }\n\n /* 해시 테이블 용량 확장 */\n void extend() {\n // 원래 해시 테이블을 임시로 저장한다\n vector&lt;vector&lt;Pair *&gt;&gt; bucketsTmp = buckets;\n // 용량을 확장한 새 해시 테이블을 초기화한다\n capacity *= extendRatio;\n buckets.clear();\n buckets.resize(capacity);\n size = 0;\n // 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n for (auto &amp;bucket : bucketsTmp) {\n for (Pair *pair : bucket) {\n put(pair-&gt;key, pair-&gt;val);\n // 메모리 해제\n delete pair;\n }\n }\n }\n\n /* 해시 테이블 출력 */\n void print() {\n for (auto &amp;bucket : buckets) {\n cout &lt;&lt; \"[\";\n for (Pair *pair : bucket) {\n cout &lt;&lt; pair-&gt;key &lt;&lt; \" -&gt; \" &lt;&lt; pair-&gt;val &lt;&lt; \", \";\n }\n cout &lt;&lt; \"]\\n\";\n }\n }\n};\n</code></pre> hash_map_chaining.java<pre><code>/* 체이닝 해시 테이블 */\nclass HashMapChaining {\n int size; // 키-값 쌍 개수\n int capacity; // 해시 테이블 용량\n double loadThres; // 용량 확장을 수행하는 적재율 임계값\n int extendRatio; // 용량 확장 배수\n List&lt;List&lt;Pair&gt;&gt; buckets; // 버킷 배열\n\n /* 생성자 */\n public HashMapChaining() {\n size = 0;\n capacity = 4;\n loadThres = 2.0 / 3.0;\n extendRatio = 2;\n buckets = new ArrayList&lt;&gt;(capacity);\n for (int i = 0; i &lt; capacity; i++) {\n buckets.add(new ArrayList&lt;&gt;());\n }\n }\n\n /* 해시 함수 */\n int hashFunc(int key) {\n return key % capacity;\n }\n\n /* 적재율 */\n double loadFactor() {\n return (double) size / capacity;\n }\n\n /* 조회 연산 */\n String get(int key) {\n int index = hashFunc(key);\n List&lt;Pair&gt; bucket = buckets.get(index);\n // 버킷을 순회하다 key를 찾으면 해당 val을 반환한다\n for (Pair pair : bucket) {\n if (pair.key == key) {\n return pair.val;\n }\n }\n // key를 찾지 못하면 null을 반환한다\n return null;\n }\n\n /* 추가 연산 */\n void put(int key, String val) {\n // 적재율이 임계값을 초과하면 용량을 확장한다\n if (loadFactor() &gt; loadThres) {\n extend();\n }\n int index = hashFunc(key);\n List&lt;Pair&gt; bucket = buckets.get(index);\n // 버킷을 순회하다 지정한 key를 만나면 해당 val을 갱신하고 반환한다\n for (Pair pair : bucket) {\n if (pair.key == key) {\n pair.val = val;\n return;\n }\n }\n // 해당 key가 없으면 키-값 쌍을 맨 뒤에 추가한다\n Pair pair = new Pair(key, val);\n bucket.add(pair);\n size++;\n }\n\n /* 삭제 연산 */\n void remove(int key) {\n int index = hashFunc(key);\n List&lt;Pair&gt; bucket = buckets.get(index);\n // 버킷을 순회하며 키-값 쌍을 삭제한다\n for (Pair pair : bucket) {\n if (pair.key == key) {\n bucket.remove(pair);\n size--;\n break;\n }\n }\n }\n\n /* 해시 테이블 용량 확장 */\n void extend() {\n // 원래 해시 테이블을 임시로 저장한다\n List&lt;List&lt;Pair&gt;&gt; bucketsTmp = buckets;\n // 용량을 확장한 새 해시 테이블을 초기화한다\n capacity *= extendRatio;\n buckets = new ArrayList&lt;&gt;(capacity);\n for (int i = 0; i &lt; capacity; i++) {\n buckets.add(new ArrayList&lt;&gt;());\n }\n size = 0;\n // 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n for (List&lt;Pair&gt; bucket : bucketsTmp) {\n for (Pair pair : bucket) {\n put(pair.key, pair.val);\n }\n }\n }\n\n /* 해시 테이블 출력 */\n void print() {\n for (List&lt;Pair&gt; bucket : buckets) {\n List&lt;String&gt; res = new ArrayList&lt;&gt;();\n for (Pair pair : bucket) {\n res.add(pair.key + \" -&gt; \" + pair.val);\n }\n System.out.println(res);\n }\n }\n}\n</code></pre> hash_map_chaining.cs<pre><code>/* 체이닝 해시 테이블 */\nclass HashMapChaining {\n int size; // 키-값 쌍 개수\n int capacity; // 해시 테이블 용량\n double loadThres; // 용량 확장을 수행하는 적재율 임계값\n int extendRatio; // 용량 확장 배수\n List&lt;List&lt;Pair&gt;&gt; buckets; // 버킷 배열\n\n /* 생성자 */\n public HashMapChaining() {\n size = 0;\n capacity = 4;\n loadThres = 2.0 / 3.0;\n extendRatio = 2;\n buckets = new List&lt;List&lt;Pair&gt;&gt;(capacity);\n for (int i = 0; i &lt; capacity; i++) {\n buckets.Add([]);\n }\n }\n\n /* 해시 함수 */\n int HashFunc(int key) {\n return key % capacity;\n }\n\n /* 적재율 */\n double LoadFactor() {\n return (double)size / capacity;\n }\n\n /* 조회 연산 */\n public string? Get(int key) {\n int index = HashFunc(key);\n // 버킷을 순회하다 key를 찾으면 해당 val을 반환한다\n foreach (Pair pair in buckets[index]) {\n if (pair.key == key) {\n return pair.val;\n }\n }\n // key를 찾지 못하면 null을 반환한다\n return null;\n }\n\n /* 추가 연산 */\n public void Put(int key, string val) {\n // 적재율이 임계값을 초과하면 용량을 확장한다\n if (LoadFactor() &gt; loadThres) {\n Extend();\n }\n int index = HashFunc(key);\n // 버킷을 순회하다 지정한 key를 만나면 해당 val을 갱신하고 반환한다\n foreach (Pair pair in buckets[index]) {\n if (pair.key == key) {\n pair.val = val;\n return;\n }\n }\n // 해당 key가 없으면 키-값 쌍을 맨 뒤에 추가한다\n buckets[index].Add(new Pair(key, val));\n size++;\n }\n\n /* 삭제 연산 */\n public void Remove(int key) {\n int index = HashFunc(key);\n // 버킷을 순회하며 키-값 쌍을 삭제한다\n foreach (Pair pair in buckets[index].ToList()) {\n if (pair.key == key) {\n buckets[index].Remove(pair);\n size--;\n break;\n }\n }\n }\n\n /* 해시 테이블 용량 확장 */\n void Extend() {\n // 원래 해시 테이블을 임시로 저장한다\n List&lt;List&lt;Pair&gt;&gt; bucketsTmp = buckets;\n // 용량을 확장한 새 해시 테이블을 초기화한다\n capacity *= extendRatio;\n buckets = new List&lt;List&lt;Pair&gt;&gt;(capacity);\n for (int i = 0; i &lt; capacity; i++) {\n buckets.Add([]);\n }\n size = 0;\n // 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n foreach (List&lt;Pair&gt; bucket in bucketsTmp) {\n foreach (Pair pair in bucket) {\n Put(pair.key, pair.val);\n }\n }\n }\n\n /* 해시 테이블 출력 */\n public void Print() {\n foreach (List&lt;Pair&gt; bucket in buckets) {\n List&lt;string&gt; res = [];\n foreach (Pair pair in bucket) {\n res.Add(pair.key + \" -&gt; \" + pair.val);\n }\n foreach (string kv in res) {\n Console.WriteLine(kv);\n }\n }\n }\n}\n</code></pre> hash_map_chaining.go<pre><code>/* 체이닝 해시 테이블 */\ntype hashMapChaining struct {\n size int // 키-값 쌍 개수\n capacity int // 해시 테이블 용량\n loadThres float64 // 용량 확장을 수행하는 적재율 임계값\n extendRatio int // 용량 확장 배수\n buckets [][]pair // 버킷 배열\n}\n\n/* 생성자 */\nfunc newHashMapChaining() *hashMapChaining {\n buckets := make([][]pair, 4)\n for i := 0; i &lt; 4; i++ {\n buckets[i] = make([]pair, 0)\n }\n return &amp;hashMapChaining{\n size: 0,\n capacity: 4,\n loadThres: 2.0 / 3.0,\n extendRatio: 2,\n buckets: buckets,\n }\n}\n\n/* 해시 함수 */\nfunc (m *hashMapChaining) hashFunc(key int) int {\n return key % m.capacity\n}\n\n/* 적재율 */\nfunc (m *hashMapChaining) loadFactor() float64 {\n return float64(m.size) / float64(m.capacity)\n}\n\n/* 조회 연산 */\nfunc (m *hashMapChaining) get(key int) string {\n idx := m.hashFunc(key)\n bucket := m.buckets[idx]\n // 버킷을 순회하다 key를 찾으면 해당 val을 반환한다\n for _, p := range bucket {\n if p.key == key {\n return p.val\n }\n }\n // key를 찾지 못하면 빈 문자열을 반환한다\n return \"\"\n}\n\n/* 추가 연산 */\nfunc (m *hashMapChaining) put(key int, val string) {\n // 적재율이 임계값을 초과하면 용량을 확장한다\n if m.loadFactor() &gt; m.loadThres {\n m.extend()\n }\n idx := m.hashFunc(key)\n // 버킷을 순회하다 지정한 key를 만나면 해당 val을 갱신하고 반환한다\n for i := range m.buckets[idx] {\n if m.buckets[idx][i].key == key {\n m.buckets[idx][i].val = val\n return\n }\n }\n // 해당 key가 없으면 키-값 쌍을 맨 뒤에 추가한다\n p := pair{\n key: key,\n val: val,\n }\n m.buckets[idx] = append(m.buckets[idx], p)\n m.size += 1\n}\n\n/* 삭제 연산 */\nfunc (m *hashMapChaining) remove(key int) {\n idx := m.hashFunc(key)\n // 버킷을 순회하며 키-값 쌍을 삭제한다\n for i, p := range m.buckets[idx] {\n if p.key == key {\n // 슬라이스에서 삭제한다\n m.buckets[idx] = append(m.buckets[idx][:i], m.buckets[idx][i+1:]...)\n m.size -= 1\n break\n }\n }\n}\n\n/* 해시 테이블 용량 확장 */\nfunc (m *hashMapChaining) extend() {\n // 원래 해시 테이블을 임시로 저장한다\n tmpBuckets := make([][]pair, len(m.buckets))\n for i := 0; i &lt; len(m.buckets); i++ {\n tmpBuckets[i] = make([]pair, len(m.buckets[i]))\n copy(tmpBuckets[i], m.buckets[i])\n }\n // 용량을 확장한 새 해시 테이블을 초기화한다\n m.capacity *= m.extendRatio\n m.buckets = make([][]pair, m.capacity)\n for i := 0; i &lt; m.capacity; i++ {\n m.buckets[i] = make([]pair, 0)\n }\n m.size = 0\n // 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n for _, bucket := range tmpBuckets {\n for _, p := range bucket {\n m.put(p.key, p.val)\n }\n }\n}\n\n/* 해시 테이블 출력 */\nfunc (m *hashMapChaining) print() {\n var builder strings.Builder\n\n for _, bucket := range m.buckets {\n builder.WriteString(\"[\")\n for _, p := range bucket {\n builder.WriteString(strconv.Itoa(p.key) + \" -&gt; \" + p.val + \" \")\n }\n builder.WriteString(\"]\")\n fmt.Println(builder.String())\n builder.Reset()\n }\n}\n</code></pre> hash_map_chaining.swift<pre><code>/* 체이닝 해시 테이블 */\nclass HashMapChaining {\n var size: Int // 키-값 쌍 개수\n var capacity: Int // 해시 테이블 용량\n var loadThres: Double // 용량 확장을 수행하는 적재율 임계값\n var extendRatio: Int // 용량 확장 배수\n var buckets: [[Pair]] // 버킷 배열\n\n /* 생성자 */\n init() {\n size = 0\n capacity = 4\n loadThres = 2.0 / 3.0\n extendRatio = 2\n buckets = Array(repeating: [], count: capacity)\n }\n\n /* 해시 함수 */\n func hashFunc(key: Int) -&gt; Int {\n key % capacity\n }\n\n /* 적재율 */\n func loadFactor() -&gt; Double {\n Double(size) / Double(capacity)\n }\n\n /* 조회 연산 */\n func get(key: Int) -&gt; String? {\n let index = hashFunc(key: key)\n let bucket = buckets[index]\n // 버킷을 순회하다 key를 찾으면 해당 val을 반환한다\n for pair in bucket {\n if pair.key == key {\n return pair.val\n }\n }\n // key를 찾지 못하면 nil을 반환한다\n return nil\n }\n\n /* 추가 연산 */\n func put(key: Int, val: String) {\n // 적재율이 임계값을 초과하면 용량을 확장한다\n if loadFactor() &gt; loadThres {\n extend()\n }\n let index = hashFunc(key: key)\n let bucket = buckets[index]\n // 버킷을 순회하다 지정한 key를 만나면 해당 val을 갱신하고 반환한다\n for pair in bucket {\n if pair.key == key {\n pair.val = val\n return\n }\n }\n // 해당 key가 없으면 키-값 쌍을 맨 뒤에 추가한다\n let pair = Pair(key: key, val: val)\n buckets[index].append(pair)\n size += 1\n }\n\n /* 삭제 연산 */\n func remove(key: Int) {\n let index = hashFunc(key: key)\n let bucket = buckets[index]\n // 버킷을 순회하며 키-값 쌍을 삭제한다\n for (pairIndex, pair) in bucket.enumerated() {\n if pair.key == key {\n buckets[index].remove(at: pairIndex)\n size -= 1\n break\n }\n }\n }\n\n /* 해시 테이블 용량 확장 */\n func extend() {\n // 원래 해시 테이블을 임시로 저장한다\n let bucketsTmp = buckets\n // 용량을 확장한 새 해시 테이블을 초기화한다\n capacity *= extendRatio\n buckets = Array(repeating: [], count: capacity)\n size = 0\n // 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n for bucket in bucketsTmp {\n for pair in bucket {\n put(key: pair.key, val: pair.val)\n }\n }\n }\n\n /* 해시 테이블 출력 */\n func print() {\n for bucket in buckets {\n let res = bucket.map { \"\\($0.key) -&gt; \\($0.val)\" }\n Swift.print(res)\n }\n }\n}\n</code></pre> hash_map_chaining.js<pre><code>/* 체이닝 해시 테이블 */\nclass HashMapChaining {\n #size; // 키-값 쌍 개수\n #capacity; // 해시 테이블 용량\n #loadThres; // 용량 확장을 수행하는 적재율 임계값\n #extendRatio; // 용량 확장 배수\n #buckets; // 버킷 배열\n\n /* 생성자 */\n constructor() {\n this.#size = 0;\n this.#capacity = 4;\n this.#loadThres = 2.0 / 3.0;\n this.#extendRatio = 2;\n this.#buckets = new Array(this.#capacity).fill(null).map((x) =&gt; []);\n }\n\n /* 해시 함수 */\n #hashFunc(key) {\n return key % this.#capacity;\n }\n\n /* 적재율 */\n #loadFactor() {\n return this.#size / this.#capacity;\n }\n\n /* 조회 연산 */\n get(key) {\n const index = this.#hashFunc(key);\n const bucket = this.#buckets[index];\n // 버킷을 순회하다 key를 찾으면 해당 val을 반환한다\n for (const pair of bucket) {\n if (pair.key === key) {\n return pair.val;\n }\n }\n // key를 찾지 못하면 null을 반환한다\n return null;\n }\n\n /* 추가 연산 */\n put(key, val) {\n // 적재율이 임계값을 초과하면 용량을 확장한다\n if (this.#loadFactor() &gt; this.#loadThres) {\n this.#extend();\n }\n const index = this.#hashFunc(key);\n const bucket = this.#buckets[index];\n // 버킷을 순회하다 지정한 key를 만나면 해당 val을 갱신하고 반환한다\n for (const pair of bucket) {\n if (pair.key === key) {\n pair.val = val;\n return;\n }\n }\n // 해당 key가 없으면 키-값 쌍을 맨 뒤에 추가한다\n const pair = new Pair(key, val);\n bucket.push(pair);\n this.#size++;\n }\n\n /* 삭제 연산 */\n remove(key) {\n const index = this.#hashFunc(key);\n let bucket = this.#buckets[index];\n // 버킷을 순회하며 키-값 쌍을 삭제한다\n for (let i = 0; i &lt; bucket.length; i++) {\n if (bucket[i].key === key) {\n bucket.splice(i, 1);\n this.#size--;\n break;\n }\n }\n }\n\n /* 해시 테이블 용량 확장 */\n #extend() {\n // 원래 해시 테이블을 임시로 저장한다\n const bucketsTmp = this.#buckets;\n // 용량을 확장한 새 해시 테이블을 초기화한다\n this.#capacity *= this.#extendRatio;\n this.#buckets = new Array(this.#capacity).fill(null).map((x) =&gt; []);\n this.#size = 0;\n // 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n for (const bucket of bucketsTmp) {\n for (const pair of bucket) {\n this.put(pair.key, pair.val);\n }\n }\n }\n\n /* 해시 테이블 출력 */\n print() {\n for (const bucket of this.#buckets) {\n let res = [];\n for (const pair of bucket) {\n res.push(pair.key + ' -&gt; ' + pair.val);\n }\n console.log(res);\n }\n }\n}\n</code></pre> hash_map_chaining.ts<pre><code>/* 체이닝 해시 테이블 */\nclass HashMapChaining {\n #size: number; // 키-값 쌍 개수\n #capacity: number; // 해시 테이블 용량\n #loadThres: number; // 용량 확장을 수행하는 적재율 임계값\n #extendRatio: number; // 용량 확장 배수\n #buckets: Pair[][]; // 버킷 배열\n\n /* 생성자 */\n constructor() {\n this.#size = 0;\n this.#capacity = 4;\n this.#loadThres = 2.0 / 3.0;\n this.#extendRatio = 2;\n this.#buckets = new Array(this.#capacity).fill(null).map((x) =&gt; []);\n }\n\n /* 해시 함수 */\n #hashFunc(key: number): number {\n return key % this.#capacity;\n }\n\n /* 적재율 */\n #loadFactor(): number {\n return this.#size / this.#capacity;\n }\n\n /* 조회 연산 */\n get(key: number): string | null {\n const index = this.#hashFunc(key);\n const bucket = this.#buckets[index];\n // 버킷을 순회하다 key를 찾으면 해당 val을 반환한다\n for (const pair of bucket) {\n if (pair.key === key) {\n return pair.val;\n }\n }\n // key를 찾지 못하면 null을 반환한다\n return null;\n }\n\n /* 추가 연산 */\n put(key: number, val: string): void {\n // 적재율이 임계값을 초과하면 용량을 확장한다\n if (this.#loadFactor() &gt; this.#loadThres) {\n this.#extend();\n }\n const index = this.#hashFunc(key);\n const bucket = this.#buckets[index];\n // 버킷을 순회하다 지정한 key를 만나면 해당 val을 갱신하고 반환한다\n for (const pair of bucket) {\n if (pair.key === key) {\n pair.val = val;\n return;\n }\n }\n // 해당 key가 없으면 키-값 쌍을 맨 뒤에 추가한다\n const pair = new Pair(key, val);\n bucket.push(pair);\n this.#size++;\n }\n\n /* 삭제 연산 */\n remove(key: number): void {\n const index = this.#hashFunc(key);\n let bucket = this.#buckets[index];\n // 버킷을 순회하며 키-값 쌍을 삭제한다\n for (let i = 0; i &lt; bucket.length; i++) {\n if (bucket[i].key === key) {\n bucket.splice(i, 1);\n this.#size--;\n break;\n }\n }\n }\n\n /* 해시 테이블 용량 확장 */\n #extend(): void {\n // 원래 해시 테이블을 임시로 저장한다\n const bucketsTmp = this.#buckets;\n // 용량을 확장한 새 해시 테이블을 초기화한다\n this.#capacity *= this.#extendRatio;\n this.#buckets = new Array(this.#capacity).fill(null).map((x) =&gt; []);\n this.#size = 0;\n // 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n for (const bucket of bucketsTmp) {\n for (const pair of bucket) {\n this.put(pair.key, pair.val);\n }\n }\n }\n\n /* 해시 테이블 출력 */\n print(): void {\n for (const bucket of this.#buckets) {\n let res = [];\n for (const pair of bucket) {\n res.push(pair.key + ' -&gt; ' + pair.val);\n }\n console.log(res);\n }\n }\n}\n</code></pre> hash_map_chaining.dart<pre><code>/* 체이닝 해시 테이블 */\nclass HashMapChaining {\n late int size; // 키-값 쌍 개수\n late int capacity; // 해시 테이블 용량\n late double loadThres; // 용량 확장을 수행하는 적재율 임계값\n late int extendRatio; // 용량 확장 배수\n late List&lt;List&lt;Pair&gt;&gt; buckets; // 버킷 배열\n\n /* 생성자 */\n HashMapChaining() {\n size = 0;\n capacity = 4;\n loadThres = 2.0 / 3.0;\n extendRatio = 2;\n buckets = List.generate(capacity, (_) =&gt; []);\n }\n\n /* 해시 함수 */\n int hashFunc(int key) {\n return key % capacity;\n }\n\n /* 적재율 */\n double loadFactor() {\n return size / capacity;\n }\n\n /* 조회 연산 */\n String? get(int key) {\n int index = hashFunc(key);\n List&lt;Pair&gt; bucket = buckets[index];\n // 버킷을 순회하다 key를 찾으면 해당 val을 반환한다\n for (Pair pair in bucket) {\n if (pair.key == key) {\n return pair.val;\n }\n }\n // key를 찾지 못하면 null을 반환한다\n return null;\n }\n\n /* 추가 연산 */\n void put(int key, String val) {\n // 적재율이 임계값을 초과하면 용량을 확장한다\n if (loadFactor() &gt; loadThres) {\n extend();\n }\n int index = hashFunc(key);\n List&lt;Pair&gt; bucket = buckets[index];\n // 버킷을 순회하다 지정한 key를 만나면 해당 val을 갱신하고 반환한다\n for (Pair pair in bucket) {\n if (pair.key == key) {\n pair.val = val;\n return;\n }\n }\n // 해당 key가 없으면 키-값 쌍을 맨 뒤에 추가한다\n Pair pair = Pair(key, val);\n bucket.add(pair);\n size++;\n }\n\n /* 삭제 연산 */\n void remove(int key) {\n int index = hashFunc(key);\n List&lt;Pair&gt; bucket = buckets[index];\n // 버킷을 순회하며 키-값 쌍을 삭제한다\n for (Pair pair in bucket) {\n if (pair.key == key) {\n bucket.remove(pair);\n size--;\n break;\n }\n }\n }\n\n /* 해시 테이블 용량 확장 */\n void extend() {\n // 원래 해시 테이블을 임시로 저장한다\n List&lt;List&lt;Pair&gt;&gt; bucketsTmp = buckets;\n // 용량을 확장한 새 해시 테이블을 초기화한다\n capacity *= extendRatio;\n buckets = List.generate(capacity, (_) =&gt; []);\n size = 0;\n // 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n for (List&lt;Pair&gt; bucket in bucketsTmp) {\n for (Pair pair in bucket) {\n put(pair.key, pair.val);\n }\n }\n }\n\n /* 해시 테이블 출력 */\n void printHashMap() {\n for (List&lt;Pair&gt; bucket in buckets) {\n List&lt;String&gt; res = [];\n for (Pair pair in bucket) {\n res.add(\"${pair.key} -&gt; ${pair.val}\");\n }\n print(res);\n }\n }\n}\n</code></pre> hash_map_chaining.rs<pre><code>/* 체이닝 해시 테이블 */\nstruct HashMapChaining {\n size: usize,\n capacity: usize,\n load_thres: f32,\n extend_ratio: usize,\n buckets: Vec&lt;Vec&lt;Pair&gt;&gt;,\n}\n\nimpl HashMapChaining {\n /* 생성자 */\n fn new() -&gt; Self {\n Self {\n size: 0,\n capacity: 4,\n load_thres: 2.0 / 3.0,\n extend_ratio: 2,\n buckets: vec![vec![]; 4],\n }\n }\n\n /* 해시 함수 */\n fn hash_func(&amp;self, key: i32) -&gt; usize {\n key as usize % self.capacity\n }\n\n /* 적재율 */\n fn load_factor(&amp;self) -&gt; f32 {\n self.size as f32 / self.capacity as f32\n }\n\n /* 삭제 연산 */\n fn remove(&amp;mut self, key: i32) -&gt; Option&lt;String&gt; {\n let index = self.hash_func(key);\n\n // 버킷을 순회하며 키-값 쌍을 삭제한다\n for (i, p) in self.buckets[index].iter_mut().enumerate() {\n if p.key == key {\n let pair = self.buckets[index].remove(i);\n self.size -= 1;\n return Some(pair.val);\n }\n }\n\n // key를 찾지 못하면 None을 반환한다\n None\n }\n\n /* 해시 테이블 용량 확장 */\n fn extend(&amp;mut self) {\n // 원래 해시 테이블을 임시로 저장한다\n let buckets_tmp = std::mem::take(&amp;mut self.buckets);\n\n // 용량을 확장한 새 해시 테이블을 초기화한다\n self.capacity *= self.extend_ratio;\n self.buckets = vec![Vec::new(); self.capacity as usize];\n self.size = 0;\n\n // 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n for bucket in buckets_tmp {\n for pair in bucket {\n self.put(pair.key, pair.val);\n }\n }\n }\n\n /* 해시 테이블 출력 */\n fn print(&amp;self) {\n for bucket in &amp;self.buckets {\n let mut res = Vec::new();\n for pair in bucket {\n res.push(format!(\"{} -&gt; {}\", pair.key, pair.val));\n }\n println!(\"{:?}\", res);\n }\n }\n\n /* 추가 연산 */\n fn put(&amp;mut self, key: i32, val: String) {\n // 적재율이 임계값을 초과하면 용량을 확장한다\n if self.load_factor() &gt; self.load_thres {\n self.extend();\n }\n\n let index = self.hash_func(key);\n\n // 버킷을 순회하다 지정한 key를 만나면 해당 val을 갱신하고 반환한다\n for pair in self.buckets[index].iter_mut() {\n if pair.key == key {\n pair.val = val;\n return;\n }\n }\n\n // 해당 key가 없으면 키-값 쌍을 맨 뒤에 추가한다\n let pair = Pair { key, val };\n self.buckets[index].push(pair);\n self.size += 1;\n }\n\n /* 조회 연산 */\n fn get(&amp;self, key: i32) -&gt; Option&lt;&amp;str&gt; {\n let index = self.hash_func(key);\n\n // 버킷을 순회하다 key를 찾으면 해당 val을 반환한다\n for pair in self.buckets[index].iter() {\n if pair.key == key {\n return Some(&amp;pair.val);\n }\n }\n\n // key를 찾지 못하면 None을 반환한다\n None\n }\n}\n</code></pre> hash_map_chaining.c<pre><code>/* 연결 리스트 노드 */\ntypedef struct Node {\n Pair *pair;\n struct Node *next;\n} Node;\n\n/* 체이닝 해시 테이블 */\ntypedef struct {\n int size; // 키-값 쌍 개수\n int capacity; // 해시 테이블 용량\n double loadThres; // 용량 확장을 수행하는 적재율 임계값\n int extendRatio; // 용량 확장 배수\n Node **buckets; // 버킷 배열\n} HashMapChaining;\n\n/* 생성자 */\nHashMapChaining *newHashMapChaining() {\n HashMapChaining *hashMap = (HashMapChaining *)malloc(sizeof(HashMapChaining));\n hashMap-&gt;size = 0;\n hashMap-&gt;capacity = 4;\n hashMap-&gt;loadThres = 2.0 / 3.0;\n hashMap-&gt;extendRatio = 2;\n hashMap-&gt;buckets = (Node **)malloc(hashMap-&gt;capacity * sizeof(Node *));\n for (int i = 0; i &lt; hashMap-&gt;capacity; i++) {\n hashMap-&gt;buckets[i] = NULL;\n }\n return hashMap;\n}\n\n/* 소멸자 */\nvoid delHashMapChaining(HashMapChaining *hashMap) {\n for (int i = 0; i &lt; hashMap-&gt;capacity; i++) {\n Node *cur = hashMap-&gt;buckets[i];\n while (cur) {\n Node *tmp = cur;\n cur = cur-&gt;next;\n free(tmp-&gt;pair);\n free(tmp);\n }\n }\n free(hashMap-&gt;buckets);\n free(hashMap);\n}\n\n/* 해시 함수 */\nint hashFunc(HashMapChaining *hashMap, int key) {\n return key % hashMap-&gt;capacity;\n}\n\n/* 적재율 */\ndouble loadFactor(HashMapChaining *hashMap) {\n return (double)hashMap-&gt;size / (double)hashMap-&gt;capacity;\n}\n\n/* 조회 연산 */\nchar *get(HashMapChaining *hashMap, int key) {\n int index = hashFunc(hashMap, key);\n // 버킷을 순회하다 key를 찾으면 해당 val을 반환한다\n Node *cur = hashMap-&gt;buckets[index];\n while (cur) {\n if (cur-&gt;pair-&gt;key == key) {\n return cur-&gt;pair-&gt;val;\n }\n cur = cur-&gt;next;\n }\n return \"\"; // key를 찾지 못하면 빈 문자열을 반환한다\n}\n\n/* 추가 연산 */\nvoid put(HashMapChaining *hashMap, int key, const char *val) {\n // 적재율이 임계값을 초과하면 용량을 확장한다\n if (loadFactor(hashMap) &gt; hashMap-&gt;loadThres) {\n extend(hashMap);\n }\n int index = hashFunc(hashMap, key);\n // 버킷을 순회하다 지정한 key를 만나면 해당 val을 갱신하고 반환한다\n Node *cur = hashMap-&gt;buckets[index];\n while (cur) {\n if (cur-&gt;pair-&gt;key == key) {\n strcpy(cur-&gt;pair-&gt;val, val); // 지정한 key를 만나면 해당 val을 갱신하고 반환한다\n return;\n }\n cur = cur-&gt;next;\n }\n // 해당 key가 없으면 키-값 쌍을 연결 리스트의 맨 앞에 추가한다\n Pair *newPair = (Pair *)malloc(sizeof(Pair));\n newPair-&gt;key = key;\n strcpy(newPair-&gt;val, val);\n Node *newNode = (Node *)malloc(sizeof(Node));\n newNode-&gt;pair = newPair;\n newNode-&gt;next = hashMap-&gt;buckets[index];\n hashMap-&gt;buckets[index] = newNode;\n hashMap-&gt;size++;\n}\n\n/* 해시 테이블 용량 확장 */\nvoid extend(HashMapChaining *hashMap) {\n // 원래 해시 테이블을 임시로 저장한다\n int oldCapacity = hashMap-&gt;capacity;\n Node **oldBuckets = hashMap-&gt;buckets;\n // 용량을 확장한 새 해시 테이블을 초기화한다\n hashMap-&gt;capacity *= hashMap-&gt;extendRatio;\n hashMap-&gt;buckets = (Node **)malloc(hashMap-&gt;capacity * sizeof(Node *));\n for (int i = 0; i &lt; hashMap-&gt;capacity; i++) {\n hashMap-&gt;buckets[i] = NULL;\n }\n hashMap-&gt;size = 0;\n // 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n for (int i = 0; i &lt; oldCapacity; i++) {\n Node *cur = oldBuckets[i];\n while (cur) {\n put(hashMap, cur-&gt;pair-&gt;key, cur-&gt;pair-&gt;val);\n Node *temp = cur;\n cur = cur-&gt;next;\n // 메모리 해제\n free(temp-&gt;pair);\n free(temp);\n }\n }\n\n free(oldBuckets);\n}\n\n/* 삭제 연산 */\nvoid removeItem(HashMapChaining *hashMap, int key) {\n int index = hashFunc(hashMap, key);\n Node *cur = hashMap-&gt;buckets[index];\n Node *pre = NULL;\n while (cur) {\n if (cur-&gt;pair-&gt;key == key) {\n // 해당 키-값 쌍을 삭제한다\n if (pre) {\n pre-&gt;next = cur-&gt;next;\n } else {\n hashMap-&gt;buckets[index] = cur-&gt;next;\n }\n // 메모리 해제\n free(cur-&gt;pair);\n free(cur);\n hashMap-&gt;size--;\n return;\n }\n pre = cur;\n cur = cur-&gt;next;\n }\n}\n\n/* 해시 테이블 출력 */\nvoid print(HashMapChaining *hashMap) {\n for (int i = 0; i &lt; hashMap-&gt;capacity; i++) {\n Node *cur = hashMap-&gt;buckets[i];\n printf(\"[\");\n while (cur) {\n printf(\"%d -&gt; %s, \", cur-&gt;pair-&gt;key, cur-&gt;pair-&gt;val);\n cur = cur-&gt;next;\n }\n printf(\"]\\n\");\n }\n}\n</code></pre> hash_map_chaining.kt<pre><code>/* 체이닝 해시 테이블 */\nclass HashMapChaining {\n var size: Int // 키-값 쌍 개수\n var capacity: Int // 해시 테이블 용량\n val loadThres: Double // 용량 확장을 수행하는 적재율 임계값\n val extendRatio: Int // 용량 확장 배수\n var buckets: MutableList&lt;MutableList&lt;Pair&gt;&gt; // 버킷 배열\n\n /* 생성자 */\n init {\n size = 0\n capacity = 4\n loadThres = 2.0 / 3.0\n extendRatio = 2\n buckets = mutableListOf()\n for (i in 0..&lt;capacity) {\n buckets.add(mutableListOf())\n }\n }\n\n /* 해시 함수 */\n fun hashFunc(key: Int): Int {\n return key % capacity\n }\n\n /* 적재율 */\n fun loadFactor(): Double {\n return (size / capacity).toDouble()\n }\n\n /* 조회 연산 */\n fun get(key: Int): String? {\n val index = hashFunc(key)\n val bucket = buckets[index]\n // 버킷을 순회하다 key를 찾으면 해당 val을 반환한다\n for (pair in bucket) {\n if (pair.key == key) return pair._val\n }\n // key를 찾지 못하면 null을 반환한다\n return null\n }\n\n /* 추가 연산 */\n fun put(key: Int, _val: String) {\n // 적재율이 임계값을 초과하면 용량을 확장한다\n if (loadFactor() &gt; loadThres) {\n extend()\n }\n val index = hashFunc(key)\n val bucket = buckets[index]\n // 버킷을 순회하다 지정한 key를 만나면 해당 val을 갱신하고 반환한다\n for (pair in bucket) {\n if (pair.key == key) {\n pair._val = _val\n return\n }\n }\n // 해당 key가 없으면 키-값 쌍을 맨 뒤에 추가한다\n val pair = Pair(key, _val)\n bucket.add(pair)\n size++\n }\n\n /* 삭제 연산 */\n fun remove(key: Int) {\n val index = hashFunc(key)\n val bucket = buckets[index]\n // 버킷을 순회하며 키-값 쌍을 삭제한다\n for (pair in bucket) {\n if (pair.key == key) {\n bucket.remove(pair)\n size--\n break\n }\n }\n }\n\n /* 해시 테이블 용량 확장 */\n fun extend() {\n // 원래 해시 테이블을 임시로 저장한다\n val bucketsTmp = buckets\n // 용량을 확장한 새 해시 테이블을 초기화한다\n capacity *= extendRatio\n // mutablelist는 크기가 고정되어 있지 않다\n buckets = mutableListOf()\n for (i in 0..&lt;capacity) {\n buckets.add(mutableListOf())\n }\n size = 0\n // 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n for (bucket in bucketsTmp) {\n for (pair in bucket) {\n put(pair.key, pair._val)\n }\n }\n }\n\n /* 해시 테이블 출력 */\n fun print() {\n for (bucket in buckets) {\n val res = mutableListOf&lt;String&gt;()\n for (pair in bucket) {\n val k = pair.key\n val v = pair._val\n res.add(\"$k -&gt; $v\")\n }\n println(res)\n }\n }\n}\n</code></pre> hash_map_chaining.rb<pre><code>### 체이닝 해시 테이블 ###\nclass HashMapChaining\n ### 생성자 ###\n def initialize\n @size = 0 # 키-값 쌍 개수\n @capacity = 4 # 해시 테이블 용량\n @load_thres = 2.0 / 3.0 # 용량 확장을 수행하는 적재율 임계값\n @extend_ratio = 2 # 용량 확장 배수\n @buckets = Array.new(@capacity) { [] } # 버킷 배열\n end\n\n ### 해시 함수 ###\n def hash_func(key)\n key % @capacity\n end\n\n ### 적재율 ###\n def load_factor\n @size / @capacity\n end\n\n ### 조회 연산 ###\n def get(key)\n index = hash_func(key)\n bucket = @buckets[index]\n # 버킷을 순회하다 key를 찾으면 해당 val을 반환한다\n for pair in bucket\n return pair.val if pair.key == key\n end\n # key를 찾지 못하면 nil을 반환한다\n nil\n end\n\n ### 추가 연산 ###\n def put(key, val)\n # 적재율이 임계값을 초과하면 용량을 확장한다\n extend if load_factor &gt; @load_thres\n index = hash_func(key)\n bucket = @buckets[index]\n # 버킷을 순회하다 지정한 key를 만나면 해당 val을 갱신하고 반환한다\n for pair in bucket\n if pair.key == key\n pair.val = val\n return\n end\n end\n # 해당 key가 없으면 키-값 쌍을 맨 뒤에 추가한다\n pair = Pair.new(key, val)\n bucket &lt;&lt; pair\n @size += 1\n end\n\n ### 삭제 연산 ###\n def remove(key)\n index = hash_func(key)\n bucket = @buckets[index]\n # 버킷을 순회하며 키-값 쌍을 삭제한다\n for pair in bucket\n if pair.key == key\n bucket.delete(pair)\n @size -= 1\n break\n end\n end\n end\n\n ### 해시 테이블 용량 확장 ###\n def extend\n # 원래 해시 테이블을 임시로 저장한다\n buckets = @buckets\n # 용량을 확장한 새 해시 테이블을 초기화한다\n @capacity *= @extend_ratio\n @buckets = Array.new(@capacity) { [] }\n @size = 0\n # 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n for bucket in buckets\n for pair in bucket\n put(pair.key, pair.val)\n end\n end\n end\n\n ### 해시 테이블 출력 ###\n def print\n for bucket in @buckets\n res = []\n for pair in bucket\n res &lt;&lt; \"#{pair.key} -&gt; #{pair.val}\"\n end\n pp res\n end\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>연결 리스트가 매우 길어지면 \\(O(n)\\)의 조회 효율은 나빠진다. 이때 연결 리스트를 ‘AVL 트리’나 ‘레드-블랙 트리’로 바꾸면 조회의 시간 복잡도를 \\(O(\\log n)\\)으로 개선할 수 있다.</p>","path":["제 6 장   해시 테이블","6.2   해시 충돌"],"tags":[]},{"location":"chapter_hashing/hash_collision/#622","level":2,"title":"6.2.2   개방 주소법","text":"<p>개방 주소법(open addressing)은 추가 자료 구조를 도입하지 않고 ‘여러 차례 탐사’하여 해시 충돌을 처리한다. 대표적인 탐사 방식에는 선형 탐사, 이차 탐사, 다중 해싱 등이 있다.</p> <p>선형 탐사를 예로 개방 주소법 해시 테이블의 작동 원리를 알아보자.</p>","path":["제 6 장   해시 테이블","6.2   해시 충돌"],"tags":[]},{"location":"chapter_hashing/hash_collision/#1","level":3,"title":"1.   선형 탐사","text":"<p>선형 탐사는 고정 간격의 선형 탐색으로 탐사하며, 일반 해시 테이블과 연산 방식이 다르다.</p> <ul> <li>원소 삽입: 해시 함수로 버킷 인덱스를 구한다. 이미 원소가 있으면 충돌 위치부터 뒤로 일정한 간격(보통 \\(1\\))으로 순회하여 빈 버킷을 찾고 원소를 넣는다.</li> <li>원소 탐색: 해시 충돌이 있으면 같은 간격으로 뒤를 순회하다가 해당 원소를 찾으면 <code>value</code>를 반환한다. 빈 버킷을 만나면 목표 원소가 없다는 뜻이므로 <code>None</code>을 반환한다.</li> </ul> <p>아래 그림(6-6)은 개방 주소법(선형 탐사) 해시 테이블의 키-값 쌍 분포를 보여 준다. 이 해시 함수에서는 마지막 두 자리가 같은 <code>key</code>가 모두 같은 버킷에 매핑된다. 선형 탐사를 통해 이들은 해당 버킷과 그 뒤의 버킷에 차례로 저장된다.</p> <p></p> <p> 그림 6-6   개방 주소법(선형 탐사) 해시 테이블의 키-값 쌍 분포 </p> <p>하지만 선형 탐사에서는 ‘군집 현상’이 쉽게 발생한다. 배열에서 연속으로 채워진 구간이 길수록 그 구간에서 충돌할 가능성도 커진다. 그 결과 군집이 더욱 커지는 악순환이 생기고, 결국 삽입, 삭제, 탐색, 갱신의 효율이 낮아진다.</p> <p>개방 주소법 해시 테이블에서는 원소를 곧바로 삭제하면 안 된다는 점에 주의하자. 삭제하면 배열에 빈 버킷 <code>None</code>이 생긴다. 조회 중 선형 탐사가 이 버킷에 도달하면 바로 반환하므로 그 뒤의 원소에 접근할 수 없게 된다. 아래 그림(6-7)과 같이 프로그램이 그 원소들을 존재하지 않는다고 잘못 판단할 수 있다.</p> <p></p> <p> 그림 6-7   개방 주소법에서 원소 삭제로 발생하는 조회 문제 </p> <p>이 문제는 지연 삭제(lazy deletion)로 해결할 수 있다. 원소를 해시 테이블에서 곧바로 제거하지 않고 상수 <code>TOMBSTONE</code>으로 해당 버킷을 표시한다. <code>None</code>과 <code>TOMBSTONE</code>은 모두 빈 버킷을 나타내며 새 키-값 쌍을 넣을 수 있다. 하지만 <code>TOMBSTONE</code>을 만나면 그 뒤에도 키-값 쌍이 있을 수 있으므로 탐사를 계속해야 한다.</p> <p>그러나 지연 삭제는 해시 테이블의 성능 저하를 가속할 수 있다. 삭제할 때마다 삭제 표시가 생기므로 <code>TOMBSTONE</code>이 늘어날수록 탐색 시간도 길어진다. 목표 원소를 찾기 위해 여러 <code>TOMBSTONE</code>을 건너뛰어야 할 수 있기 때문이다.</p> <p>이를 위해 선형 탐사 중 처음 만난 <code>TOMBSTONE</code>의 인덱스를 기록하고, 찾은 목표 원소와 그 <code>TOMBSTONE</code>의 위치를 교환하는 방법을 생각할 수 있다. 조회하거나 추가할 때마다 원소가 이상적인 위치(탐사 시작점)에 더 가까운 버킷으로 옮겨지므로 조회 효율이 좋아진다.</p> <p>다음 코드는 지연 삭제를 포함한 개방 주소법(선형 탐사) 해시 테이블을 구현한다. 공간을 더 충분히 활용하기 위해 해시 테이블을 ‘원형 배열’로 보고, 끝을 넘으면 처음으로 돌아와 순회를 계속한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map_open_addressing.py<pre><code>class HashMapOpenAddressing:\n \"\"\"개방 주소법 해시 테이블\"\"\"\n\n def __init__(self):\n \"\"\"생성자\"\"\"\n self.size = 0 # 키-값 쌍 개수\n self.capacity = 4 # 해시 테이블 용량\n self.load_thres = 2.0 / 3.0 # 용량 확장을 수행하는 적재율 임계값\n self.extend_ratio = 2 # 용량 확장 배수\n self.buckets: list[Pair | None] = [None] * self.capacity # 버킷 배열\n self.TOMBSTONE = Pair(-1, \"-1\") # 삭제 표시\n\n def hash_func(self, key: int) -&gt; int:\n \"\"\"해시 함수\"\"\"\n return key % self.capacity\n\n def load_factor(self) -&gt; float:\n \"\"\"적재율\"\"\"\n return self.size / self.capacity\n\n def find_bucket(self, key: int) -&gt; int:\n \"\"\"key에 해당하는 버킷 인덱스 탐색\"\"\"\n index = self.hash_func(key)\n first_tombstone = -1\n # 선형 탐사로 탐색하고, 빈 버킷을 만나면 반복문을 종료한다\n while self.buckets[index] is not None:\n # key를 만나면 해당 버킷 인덱스를 반환한다\n if self.buckets[index].key == key:\n # 이전에 삭제 표시를 만났으면 키-값 쌍을 그 인덱스로 옮긴다\n if first_tombstone != -1:\n self.buckets[first_tombstone] = self.buckets[index]\n self.buckets[index] = self.TOMBSTONE\n return first_tombstone # 옮긴 후의 버킷 인덱스를 반환한다\n return index # 버킷 인덱스를 반환한다\n # 처음 만난 삭제 표시를 기록한다\n if first_tombstone == -1 and self.buckets[index] is self.TOMBSTONE:\n first_tombstone = index\n # 버킷 인덱스를 계산하고, 끝을 넘어가면 처음으로 돌아온다\n index = (index + 1) % self.capacity\n # key가 없으면 추가할 위치의 인덱스를 반환한다\n return index if first_tombstone == -1 else first_tombstone\n\n def get(self, key: int) -&gt; str:\n \"\"\"조회 연산\"\"\"\n # key에 해당하는 버킷 인덱스 탐색\n index = self.find_bucket(key)\n # 키-값 쌍을 찾으면 해당 val을 반환한다\n if self.buckets[index] not in [None, self.TOMBSTONE]:\n return self.buckets[index].val\n # 키-값 쌍이 없으면 None을 반환한다\n return None\n\n def put(self, key: int, val: str):\n \"\"\"추가 연산\"\"\"\n # 적재율이 임계값을 초과하면 용량을 확장한다\n if self.load_factor() &gt; self.load_thres:\n self.extend()\n # key에 해당하는 버킷 인덱스 탐색\n index = self.find_bucket(key)\n # 키-값 쌍을 찾으면 val을 덮어쓰고 반환한다\n if self.buckets[index] not in [None, self.TOMBSTONE]:\n self.buckets[index].val = val\n return\n # 키-값 쌍이 없으면 해당 키-값 쌍을 추가한다\n self.buckets[index] = Pair(key, val)\n self.size += 1\n\n def remove(self, key: int):\n \"\"\"삭제 연산\"\"\"\n # key에 해당하는 버킷 인덱스 탐색\n index = self.find_bucket(key)\n # 키-값 쌍을 찾으면 삭제 표시로 덮어쓴다\n if self.buckets[index] not in [None, self.TOMBSTONE]:\n self.buckets[index] = self.TOMBSTONE\n self.size -= 1\n\n def extend(self):\n \"\"\"해시 테이블 용량 확장\"\"\"\n # 원래 해시 테이블을 임시로 저장한다\n buckets_tmp = self.buckets\n # 용량을 확장한 새 해시 테이블을 초기화한다\n self.capacity *= self.extend_ratio\n self.buckets = [None] * self.capacity\n self.size = 0\n # 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n for pair in buckets_tmp:\n if pair not in [None, self.TOMBSTONE]:\n self.put(pair.key, pair.val)\n\n def print(self):\n \"\"\"해시 테이블 출력\"\"\"\n for pair in self.buckets:\n if pair is None:\n print(\"None\")\n elif pair is self.TOMBSTONE:\n print(\"TOMBSTONE\")\n else:\n print(pair.key, \"-&gt;\", pair.val)\n</code></pre> hash_map_open_addressing.cpp<pre><code>/* 개방 주소법 해시 테이블 */\nclass HashMapOpenAddressing {\n private:\n int size; // 키-값 쌍 개수\n int capacity = 4; // 해시 테이블 용량\n const double loadThres = 2.0 / 3.0; // 용량 확장을 수행하는 적재율 임계값\n const int extendRatio = 2; // 용량 확장 배수\n vector&lt;Pair *&gt; buckets; // 버킷 배열\n Pair *TOMBSTONE = new Pair(-1, \"-1\"); // 삭제 표시\n\n public:\n /* 생성자 */\n HashMapOpenAddressing() : size(0), buckets(capacity, nullptr) {\n }\n\n /* 소멸자 */\n ~HashMapOpenAddressing() {\n for (Pair *pair : buckets) {\n if (pair != nullptr &amp;&amp; pair != TOMBSTONE) {\n delete pair;\n }\n }\n delete TOMBSTONE;\n }\n\n /* 해시 함수 */\n int hashFunc(int key) {\n return key % capacity;\n }\n\n /* 적재율 */\n double loadFactor() {\n return (double)size / capacity;\n }\n\n /* key에 해당하는 버킷 인덱스 탐색 */\n int findBucket(int key) {\n int index = hashFunc(key);\n int firstTombstone = -1;\n // 선형 탐사로 탐색하고, 빈 버킷을 만나면 반복문을 종료한다\n while (buckets[index] != nullptr) {\n // key를 만나면 해당 버킷 인덱스를 반환한다\n if (buckets[index]-&gt;key == key) {\n // 이전에 삭제 표시를 만났으면 키-값 쌍을 그 인덱스로 옮긴다\n if (firstTombstone != -1) {\n buckets[firstTombstone] = buckets[index];\n buckets[index] = TOMBSTONE;\n return firstTombstone; // 옮긴 후의 버킷 인덱스를 반환한다\n }\n return index; // 버킷 인덱스를 반환한다\n }\n // 처음 만난 삭제 표시를 기록한다\n if (firstTombstone == -1 &amp;&amp; buckets[index] == TOMBSTONE) {\n firstTombstone = index;\n }\n // 버킷 인덱스를 계산하고, 끝을 넘어가면 처음으로 돌아온다\n index = (index + 1) % capacity;\n }\n // key가 없으면 추가할 위치의 인덱스를 반환한다\n return firstTombstone == -1 ? index : firstTombstone;\n }\n\n /* 조회 연산 */\n string get(int key) {\n // key에 해당하는 버킷 인덱스 탐색\n int index = findBucket(key);\n // 키-값 쌍을 찾으면 해당 val을 반환한다\n if (buckets[index] != nullptr &amp;&amp; buckets[index] != TOMBSTONE) {\n return buckets[index]-&gt;val;\n }\n // 키-값 쌍이 없으면 빈 문자열을 반환한다\n return \"\";\n }\n\n /* 추가 연산 */\n void put(int key, string val) {\n // 적재율이 임계값을 초과하면 용량을 확장한다\n if (loadFactor() &gt; loadThres) {\n extend();\n }\n // key에 해당하는 버킷 인덱스 탐색\n int index = findBucket(key);\n // 키-값 쌍을 찾으면 val을 덮어쓰고 반환한다\n if (buckets[index] != nullptr &amp;&amp; buckets[index] != TOMBSTONE) {\n buckets[index]-&gt;val = val;\n return;\n }\n // 키-값 쌍이 없으면 해당 키-값 쌍을 추가한다\n buckets[index] = new Pair(key, val);\n size++;\n }\n\n /* 삭제 연산 */\n void remove(int key) {\n // key에 해당하는 버킷 인덱스 탐색\n int index = findBucket(key);\n // 키-값 쌍을 찾으면 삭제 표시로 덮어쓴다\n if (buckets[index] != nullptr &amp;&amp; buckets[index] != TOMBSTONE) {\n delete buckets[index];\n buckets[index] = TOMBSTONE;\n size--;\n }\n }\n\n /* 해시 테이블 용량 확장 */\n void extend() {\n // 원래 해시 테이블을 임시로 저장한다\n vector&lt;Pair *&gt; bucketsTmp = buckets;\n // 용량을 확장한 새 해시 테이블을 초기화한다\n capacity *= extendRatio;\n buckets = vector&lt;Pair *&gt;(capacity, nullptr);\n size = 0;\n // 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n for (Pair *pair : bucketsTmp) {\n if (pair != nullptr &amp;&amp; pair != TOMBSTONE) {\n put(pair-&gt;key, pair-&gt;val);\n delete pair;\n }\n }\n }\n\n /* 해시 테이블 출력 */\n void print() {\n for (Pair *pair : buckets) {\n if (pair == nullptr) {\n cout &lt;&lt; \"nullptr\" &lt;&lt; endl;\n } else if (pair == TOMBSTONE) {\n cout &lt;&lt; \"TOMBSTONE\" &lt;&lt; endl;\n } else {\n cout &lt;&lt; pair-&gt;key &lt;&lt; \" -&gt; \" &lt;&lt; pair-&gt;val &lt;&lt; endl;\n }\n }\n }\n};\n</code></pre> hash_map_open_addressing.java<pre><code>/* 개방 주소법 해시 테이블 */\nclass HashMapOpenAddressing {\n private int size; // 키-값 쌍 개수\n private int capacity = 4; // 해시 테이블 용량\n private final double loadThres = 2.0 / 3.0; // 용량 확장을 수행하는 적재율 임계값\n private final int extendRatio = 2; // 용량 확장 배수\n private Pair[] buckets; // 버킷 배열\n private final Pair TOMBSTONE = new Pair(-1, \"-1\"); // 삭제 표시\n\n /* 생성자 */\n public HashMapOpenAddressing() {\n size = 0;\n buckets = new Pair[capacity];\n }\n\n /* 해시 함수 */\n private int hashFunc(int key) {\n return key % capacity;\n }\n\n /* 적재율 */\n private double loadFactor() {\n return (double) size / capacity;\n }\n\n /* key에 해당하는 버킷 인덱스 탐색 */\n private int findBucket(int key) {\n int index = hashFunc(key);\n int firstTombstone = -1;\n // 선형 탐사로 탐색하고, 빈 버킷을 만나면 반복문을 종료한다\n while (buckets[index] != null) {\n // key를 만나면 해당 버킷 인덱스를 반환한다\n if (buckets[index].key == key) {\n // 이전에 삭제 표시를 만났으면 키-값 쌍을 그 인덱스로 옮긴다\n if (firstTombstone != -1) {\n buckets[firstTombstone] = buckets[index];\n buckets[index] = TOMBSTONE;\n return firstTombstone; // 옮긴 후의 버킷 인덱스를 반환한다\n }\n return index; // 버킷 인덱스를 반환한다\n }\n // 처음 만난 삭제 표시를 기록한다\n if (firstTombstone == -1 &amp;&amp; buckets[index] == TOMBSTONE) {\n firstTombstone = index;\n }\n // 버킷 인덱스를 계산하고, 끝을 넘어가면 처음으로 돌아온다\n index = (index + 1) % capacity;\n }\n // key가 없으면 추가할 위치의 인덱스를 반환한다\n return firstTombstone == -1 ? index : firstTombstone;\n }\n\n /* 조회 연산 */\n public String get(int key) {\n // key에 해당하는 버킷 인덱스 탐색\n int index = findBucket(key);\n // 키-값 쌍을 찾으면 해당 val을 반환한다\n if (buckets[index] != null &amp;&amp; buckets[index] != TOMBSTONE) {\n return buckets[index].val;\n }\n // 키-값 쌍이 없으면 null을 반환한다\n return null;\n }\n\n /* 추가 연산 */\n public void put(int key, String val) {\n // 적재율이 임계값을 초과하면 용량을 확장한다\n if (loadFactor() &gt; loadThres) {\n extend();\n }\n // key에 해당하는 버킷 인덱스 탐색\n int index = findBucket(key);\n // 키-값 쌍을 찾으면 val을 덮어쓰고 반환한다\n if (buckets[index] != null &amp;&amp; buckets[index] != TOMBSTONE) {\n buckets[index].val = val;\n return;\n }\n // 키-값 쌍이 없으면 해당 키-값 쌍을 추가한다\n buckets[index] = new Pair(key, val);\n size++;\n }\n\n /* 삭제 연산 */\n public void remove(int key) {\n // key에 해당하는 버킷 인덱스 탐색\n int index = findBucket(key);\n // 키-값 쌍을 찾으면 삭제 표시로 덮어쓴다\n if (buckets[index] != null &amp;&amp; buckets[index] != TOMBSTONE) {\n buckets[index] = TOMBSTONE;\n size--;\n }\n }\n\n /* 해시 테이블 용량 확장 */\n private void extend() {\n // 원래 해시 테이블을 임시로 저장한다\n Pair[] bucketsTmp = buckets;\n // 용량을 확장한 새 해시 테이블을 초기화한다\n capacity *= extendRatio;\n buckets = new Pair[capacity];\n size = 0;\n // 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n for (Pair pair : bucketsTmp) {\n if (pair != null &amp;&amp; pair != TOMBSTONE) {\n put(pair.key, pair.val);\n }\n }\n }\n\n /* 해시 테이블 출력 */\n public void print() {\n for (Pair pair : buckets) {\n if (pair == null) {\n System.out.println(\"null\");\n } else if (pair == TOMBSTONE) {\n System.out.println(\"TOMBSTONE\");\n } else {\n System.out.println(pair.key + \" -&gt; \" + pair.val);\n }\n }\n }\n}\n</code></pre> hash_map_open_addressing.cs<pre><code>/* 개방 주소법 해시 테이블 */\nclass HashMapOpenAddressing {\n int size; // 키-값 쌍 개수\n int capacity = 4; // 해시 테이블 용량\n double loadThres = 2.0 / 3.0; // 용량 확장을 수행하는 적재율 임계값\n int extendRatio = 2; // 용량 확장 배수\n Pair[] buckets; // 버킷 배열\n Pair TOMBSTONE = new(-1, \"-1\"); // 삭제 표시\n\n /* 생성자 */\n public HashMapOpenAddressing() {\n size = 0;\n buckets = new Pair[capacity];\n }\n\n /* 해시 함수 */\n int HashFunc(int key) {\n return key % capacity;\n }\n\n /* 적재율 */\n double LoadFactor() {\n return (double)size / capacity;\n }\n\n /* key에 해당하는 버킷 인덱스 탐색 */\n int FindBucket(int key) {\n int index = HashFunc(key);\n int firstTombstone = -1;\n // 선형 탐사로 탐색하고, 빈 버킷을 만나면 반복문을 종료한다\n while (buckets[index] != null) {\n // key를 만나면 해당 버킷 인덱스를 반환한다\n if (buckets[index].key == key) {\n // 이전에 삭제 표시를 만났으면 키-값 쌍을 그 인덱스로 옮긴다\n if (firstTombstone != -1) {\n buckets[firstTombstone] = buckets[index];\n buckets[index] = TOMBSTONE;\n return firstTombstone; // 옮긴 후의 버킷 인덱스를 반환한다\n }\n return index; // 버킷 인덱스를 반환한다\n }\n // 처음 만난 삭제 표시를 기록한다\n if (firstTombstone == -1 &amp;&amp; buckets[index] == TOMBSTONE) {\n firstTombstone = index;\n }\n // 버킷 인덱스를 계산하고, 끝을 넘어가면 처음으로 돌아온다\n index = (index + 1) % capacity;\n }\n // key가 없으면 추가할 위치의 인덱스를 반환한다\n return firstTombstone == -1 ? index : firstTombstone;\n }\n\n /* 조회 연산 */\n public string? Get(int key) {\n // key에 해당하는 버킷 인덱스 탐색\n int index = FindBucket(key);\n // 키-값 쌍을 찾으면 해당 val을 반환한다\n if (buckets[index] != null &amp;&amp; buckets[index] != TOMBSTONE) {\n return buckets[index].val;\n }\n // 키-값 쌍이 없으면 null을 반환한다\n return null;\n }\n\n /* 추가 연산 */\n public void Put(int key, string val) {\n // 적재율이 임계값을 초과하면 용량을 확장한다\n if (LoadFactor() &gt; loadThres) {\n Extend();\n }\n // key에 해당하는 버킷 인덱스 탐색\n int index = FindBucket(key);\n // 키-값 쌍을 찾으면 val을 덮어쓰고 반환한다\n if (buckets[index] != null &amp;&amp; buckets[index] != TOMBSTONE) {\n buckets[index].val = val;\n return;\n }\n // 키-값 쌍이 없으면 해당 키-값 쌍을 추가한다\n buckets[index] = new Pair(key, val);\n size++;\n }\n\n /* 삭제 연산 */\n public void Remove(int key) {\n // key에 해당하는 버킷 인덱스 탐색\n int index = FindBucket(key);\n // 키-값 쌍을 찾으면 삭제 표시로 덮어쓴다\n if (buckets[index] != null &amp;&amp; buckets[index] != TOMBSTONE) {\n buckets[index] = TOMBSTONE;\n size--;\n }\n }\n\n /* 해시 테이블 용량 확장 */\n void Extend() {\n // 원래 해시 테이블을 임시로 저장한다\n Pair[] bucketsTmp = buckets;\n // 용량을 확장한 새 해시 테이블을 초기화한다\n capacity *= extendRatio;\n buckets = new Pair[capacity];\n size = 0;\n // 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n foreach (Pair pair in bucketsTmp) {\n if (pair != null &amp;&amp; pair != TOMBSTONE) {\n Put(pair.key, pair.val);\n }\n }\n }\n\n /* 해시 테이블 출력 */\n public void Print() {\n foreach (Pair pair in buckets) {\n if (pair == null) {\n Console.WriteLine(\"null\");\n } else if (pair == TOMBSTONE) {\n Console.WriteLine(\"TOMBSTONE\");\n } else {\n Console.WriteLine(pair.key + \" -&gt; \" + pair.val);\n }\n }\n }\n}\n</code></pre> hash_map_open_addressing.go<pre><code>/* 개방 주소법 해시 테이블 */\ntype hashMapOpenAddressing struct {\n size int // 키-값 쌍 개수\n capacity int // 해시 테이블 용량\n loadThres float64 // 용량 확장을 수행하는 적재율 임계값\n extendRatio int // 용량 확장 배수\n buckets []*pair // 버킷 배열\n TOMBSTONE *pair // 삭제 표시\n}\n\n/* 생성자 */\nfunc newHashMapOpenAddressing() *hashMapOpenAddressing {\n return &amp;hashMapOpenAddressing{\n size: 0,\n capacity: 4,\n loadThres: 2.0 / 3.0,\n extendRatio: 2,\n buckets: make([]*pair, 4),\n TOMBSTONE: &amp;pair{-1, \"-1\"},\n }\n}\n\n/* 해시 함수 */\nfunc (h *hashMapOpenAddressing) hashFunc(key int) int {\n return key % h.capacity // 키를 바탕으로 해시값을 계산한다\n}\n\n/* 적재율 */\nfunc (h *hashMapOpenAddressing) loadFactor() float64 {\n return float64(h.size) / float64(h.capacity) // 현재 적재율을 계산한다\n}\n\n/* key에 해당하는 버킷 인덱스 탐색 */\nfunc (h *hashMapOpenAddressing) findBucket(key int) int {\n index := h.hashFunc(key) // 초기 인덱스를 구한다\n firstTombstone := -1 // 처음 만난 TOMBSTONE의 위치를 기록한다\n for h.buckets[index] != nil {\n if h.buckets[index].key == key {\n if firstTombstone != -1 {\n // 이전에 삭제 표시를 만났으면 키-값 쌍을 그 인덱스로 옮긴다\n h.buckets[firstTombstone] = h.buckets[index]\n h.buckets[index] = h.TOMBSTONE\n return firstTombstone // 옮긴 후의 버킷 인덱스를 반환한다\n }\n return index // 찾은 인덱스를 반환한다\n }\n if firstTombstone == -1 &amp;&amp; h.buckets[index] == h.TOMBSTONE {\n firstTombstone = index // 처음 만난 삭제 표시의 위치를 기록한다\n }\n index = (index + 1) % h.capacity // 선형 탐사를 수행하며, 끝을 넘어가면 처음으로 돌아온다\n }\n // key가 없으면 추가할 위치의 인덱스를 반환한다\n if firstTombstone != -1 {\n return firstTombstone\n }\n return index\n}\n\n/* 조회 연산 */\nfunc (h *hashMapOpenAddressing) get(key int) string {\n index := h.findBucket(key) // key에 해당하는 버킷 인덱스 탐색\n if h.buckets[index] != nil &amp;&amp; h.buckets[index] != h.TOMBSTONE {\n return h.buckets[index].val // 키-값 쌍을 찾으면 해당 val을 반환한다\n }\n return \"\" // 키-값 쌍이 없으면 \"\"를 반환한다\n}\n\n/* 추가 연산 */\nfunc (h *hashMapOpenAddressing) put(key int, val string) {\n if h.loadFactor() &gt; h.loadThres {\n h.extend() // 적재율이 임계값을 초과하면 용량을 확장한다\n }\n index := h.findBucket(key) // key에 해당하는 버킷 인덱스 탐색\n if h.buckets[index] == nil || h.buckets[index] == h.TOMBSTONE {\n h.buckets[index] = &amp;pair{key, val} // 키-값 쌍이 없으면 해당 키-값 쌍을 추가한다\n h.size++\n } else {\n h.buckets[index].val = val // 키-값 쌍을 찾으면 val을 덮어쓴다\n }\n}\n\n/* 삭제 연산 */\nfunc (h *hashMapOpenAddressing) remove(key int) {\n index := h.findBucket(key) // key에 해당하는 버킷 인덱스 탐색\n if h.buckets[index] != nil &amp;&amp; h.buckets[index] != h.TOMBSTONE {\n h.buckets[index] = h.TOMBSTONE // 키-값 쌍을 찾으면 삭제 표시로 덮어쓴다\n h.size--\n }\n}\n\n/* 해시 테이블 용량 확장 */\nfunc (h *hashMapOpenAddressing) extend() {\n oldBuckets := h.buckets // 원래 해시 테이블을 임시로 저장한다\n h.capacity *= h.extendRatio // 용량을 갱신한다\n h.buckets = make([]*pair, h.capacity) // 용량을 확장한 새 해시 테이블을 초기화한다\n h.size = 0 // 크기를 초기화한다\n // 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n for _, pair := range oldBuckets {\n if pair != nil &amp;&amp; pair != h.TOMBSTONE {\n h.put(pair.key, pair.val)\n }\n }\n}\n\n/* 해시 테이블 출력 */\nfunc (h *hashMapOpenAddressing) print() {\n for _, pair := range h.buckets {\n if pair == nil {\n fmt.Println(\"nil\")\n } else if pair == h.TOMBSTONE {\n fmt.Println(\"TOMBSTONE\")\n } else {\n fmt.Printf(\"%d -&gt; %s\\n\", pair.key, pair.val)\n }\n }\n}\n</code></pre> hash_map_open_addressing.swift<pre><code>/* 개방 주소법 해시 테이블 */\nclass HashMapOpenAddressing {\n var size: Int // 키-값 쌍 개수\n var capacity: Int // 해시 테이블 용량\n var loadThres: Double // 용량 확장을 수행하는 적재율 임계값\n var extendRatio: Int // 용량 확장 배수\n var buckets: [Pair?] // 버킷 배열\n var TOMBSTONE: Pair // 삭제 표시\n\n /* 생성자 */\n init() {\n size = 0\n capacity = 4\n loadThres = 2.0 / 3.0\n extendRatio = 2\n buckets = Array(repeating: nil, count: capacity)\n TOMBSTONE = Pair(key: -1, val: \"-1\")\n }\n\n /* 해시 함수 */\n func hashFunc(key: Int) -&gt; Int {\n key % capacity\n }\n\n /* 적재율 */\n func loadFactor() -&gt; Double {\n Double(size) / Double(capacity)\n }\n\n /* key에 해당하는 버킷 인덱스 탐색 */\n func findBucket(key: Int) -&gt; Int {\n var index = hashFunc(key: key)\n var firstTombstone = -1\n // 선형 탐사로 탐색하고, 빈 버킷을 만나면 반복문을 종료한다\n while buckets[index] != nil {\n // key를 만나면 해당 버킷 인덱스를 반환한다\n if buckets[index]!.key == key {\n // 이전에 삭제 표시를 만났으면 키-값 쌍을 그 인덱스로 옮긴다\n if firstTombstone != -1 {\n buckets[firstTombstone] = buckets[index]\n buckets[index] = TOMBSTONE\n return firstTombstone // 옮긴 후의 버킷 인덱스를 반환한다\n }\n return index // 버킷 인덱스를 반환한다\n }\n // 처음 만난 삭제 표시를 기록한다\n if firstTombstone == -1 &amp;&amp; buckets[index] == TOMBSTONE {\n firstTombstone = index\n }\n // 버킷 인덱스를 계산하고, 끝을 넘어가면 처음으로 돌아온다\n index = (index + 1) % capacity\n }\n // key가 없으면 추가할 위치의 인덱스를 반환한다\n return firstTombstone == -1 ? index : firstTombstone\n }\n\n /* 조회 연산 */\n func get(key: Int) -&gt; String? {\n // key에 해당하는 버킷 인덱스 탐색\n let index = findBucket(key: key)\n // 키-값 쌍을 찾으면 해당 val을 반환한다\n if buckets[index] != nil, buckets[index] != TOMBSTONE {\n return buckets[index]!.val\n }\n // 키-값 쌍이 없으면 null을 반환한다\n return nil\n }\n\n /* 추가 연산 */\n func put(key: Int, val: String) {\n // 적재율이 임계값을 초과하면 용량을 확장한다\n if loadFactor() &gt; loadThres {\n extend()\n }\n // key에 해당하는 버킷 인덱스 탐색\n let index = findBucket(key: key)\n // 키-값 쌍을 찾으면 val을 덮어쓰고 반환한다\n if buckets[index] != nil, buckets[index] != TOMBSTONE {\n buckets[index]!.val = val\n return\n }\n // 키-값 쌍이 없으면 해당 키-값 쌍을 추가한다\n buckets[index] = Pair(key: key, val: val)\n size += 1\n }\n\n /* 삭제 연산 */\n func remove(key: Int) {\n // key에 해당하는 버킷 인덱스 탐색\n let index = findBucket(key: key)\n // 키-값 쌍을 찾으면 삭제 표시로 덮어쓴다\n if buckets[index] != nil, buckets[index] != TOMBSTONE {\n buckets[index] = TOMBSTONE\n size -= 1\n }\n }\n\n /* 해시 테이블 용량 확장 */\n func extend() {\n // 원래 해시 테이블을 임시로 저장한다\n let bucketsTmp = buckets\n // 용량을 확장한 새 해시 테이블을 초기화한다\n capacity *= extendRatio\n buckets = Array(repeating: nil, count: capacity)\n size = 0\n // 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n for pair in bucketsTmp {\n if let pair, pair != TOMBSTONE {\n put(key: pair.key, val: pair.val)\n }\n }\n }\n\n /* 해시 테이블 출력 */\n func print() {\n for pair in buckets {\n if pair == nil {\n Swift.print(\"null\")\n } else if pair == TOMBSTONE {\n Swift.print(\"TOMBSTONE\")\n } else {\n Swift.print(\"\\(pair!.key) -&gt; \\(pair!.val)\")\n }\n }\n }\n}\n</code></pre> hash_map_open_addressing.js<pre><code>/* 개방 주소법 해시 테이블 */\nclass HashMapOpenAddressing {\n #size; // 키-값 쌍 개수\n #capacity; // 해시 테이블 용량\n #loadThres; // 용량 확장을 수행하는 적재율 임계값\n #extendRatio; // 용량 확장 배수\n #buckets; // 버킷 배열\n #TOMBSTONE; // 삭제 표시\n\n /* 생성자 */\n constructor() {\n this.#size = 0; // 키-값 쌍 개수\n this.#capacity = 4; // 해시 테이블 용량\n this.#loadThres = 2.0 / 3.0; // 용량 확장을 수행하는 적재율 임계값\n this.#extendRatio = 2; // 용량 확장 배수\n this.#buckets = Array(this.#capacity).fill(null); // 버킷 배열\n this.#TOMBSTONE = new Pair(-1, '-1'); // 삭제 표시\n }\n\n /* 해시 함수 */\n #hashFunc(key) {\n return key % this.#capacity;\n }\n\n /* 적재율 */\n #loadFactor() {\n return this.#size / this.#capacity;\n }\n\n /* key에 해당하는 버킷 인덱스 탐색 */\n #findBucket(key) {\n let index = this.#hashFunc(key);\n let firstTombstone = -1;\n // 선형 탐사로 탐색하고, 빈 버킷을 만나면 반복문을 종료한다\n while (this.#buckets[index] !== null) {\n // key를 만나면 해당 버킷 인덱스를 반환한다\n if (this.#buckets[index].key === key) {\n // 이전에 삭제 표시를 만났으면 키-값 쌍을 그 인덱스로 옮긴다\n if (firstTombstone !== -1) {\n this.#buckets[firstTombstone] = this.#buckets[index];\n this.#buckets[index] = this.#TOMBSTONE;\n return firstTombstone; // 옮긴 후의 버킷 인덱스를 반환한다\n }\n return index; // 버킷 인덱스를 반환한다\n }\n // 처음 만난 삭제 표시를 기록한다\n if (\n firstTombstone === -1 &amp;&amp;\n this.#buckets[index] === this.#TOMBSTONE\n ) {\n firstTombstone = index;\n }\n // 버킷 인덱스를 계산하고, 끝을 넘어가면 처음으로 돌아온다\n index = (index + 1) % this.#capacity;\n }\n // key가 없으면 추가할 위치의 인덱스를 반환한다\n return firstTombstone === -1 ? index : firstTombstone;\n }\n\n /* 조회 연산 */\n get(key) {\n // key에 해당하는 버킷 인덱스 탐색\n const index = this.#findBucket(key);\n // 키-값 쌍을 찾으면 해당 val을 반환한다\n if (\n this.#buckets[index] !== null &amp;&amp;\n this.#buckets[index] !== this.#TOMBSTONE\n ) {\n return this.#buckets[index].val;\n }\n // 키-값 쌍이 없으면 null을 반환한다\n return null;\n }\n\n /* 추가 연산 */\n put(key, val) {\n // 적재율이 임계값을 초과하면 용량을 확장한다\n if (this.#loadFactor() &gt; this.#loadThres) {\n this.#extend();\n }\n // key에 해당하는 버킷 인덱스 탐색\n const index = this.#findBucket(key);\n // 키-값 쌍을 찾으면 val을 덮어쓰고 반환한다\n if (\n this.#buckets[index] !== null &amp;&amp;\n this.#buckets[index] !== this.#TOMBSTONE\n ) {\n this.#buckets[index].val = val;\n return;\n }\n // 키-값 쌍이 없으면 해당 키-값 쌍을 추가한다\n this.#buckets[index] = new Pair(key, val);\n this.#size++;\n }\n\n /* 삭제 연산 */\n remove(key) {\n // key에 해당하는 버킷 인덱스 탐색\n const index = this.#findBucket(key);\n // 키-값 쌍을 찾으면 삭제 표시로 덮어쓴다\n if (\n this.#buckets[index] !== null &amp;&amp;\n this.#buckets[index] !== this.#TOMBSTONE\n ) {\n this.#buckets[index] = this.#TOMBSTONE;\n this.#size--;\n }\n }\n\n /* 해시 테이블 용량 확장 */\n #extend() {\n // 원래 해시 테이블을 임시로 저장한다\n const bucketsTmp = this.#buckets;\n // 용량을 확장한 새 해시 테이블을 초기화한다\n this.#capacity *= this.#extendRatio;\n this.#buckets = Array(this.#capacity).fill(null);\n this.#size = 0;\n // 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n for (const pair of bucketsTmp) {\n if (pair !== null &amp;&amp; pair !== this.#TOMBSTONE) {\n this.put(pair.key, pair.val);\n }\n }\n }\n\n /* 해시 테이블 출력 */\n print() {\n for (const pair of this.#buckets) {\n if (pair === null) {\n console.log('null');\n } else if (pair === this.#TOMBSTONE) {\n console.log('TOMBSTONE');\n } else {\n console.log(pair.key + ' -&gt; ' + pair.val);\n }\n }\n }\n}\n</code></pre> hash_map_open_addressing.ts<pre><code>/* 개방 주소법 해시 테이블 */\nclass HashMapOpenAddressing {\n private size: number; // 키-값 쌍 개수\n private capacity: number; // 해시 테이블 용량\n private loadThres: number; // 용량 확장을 수행하는 적재율 임계값\n private extendRatio: number; // 용량 확장 배수\n private buckets: Array&lt;Pair | null&gt;; // 버킷 배열\n private TOMBSTONE: Pair; // 삭제 표시\n\n /* 생성자 */\n constructor() {\n this.size = 0; // 키-값 쌍 개수\n this.capacity = 4; // 해시 테이블 용량\n this.loadThres = 2.0 / 3.0; // 용량 확장을 수행하는 적재율 임계값\n this.extendRatio = 2; // 용량 확장 배수\n this.buckets = Array(this.capacity).fill(null); // 버킷 배열\n this.TOMBSTONE = new Pair(-1, '-1'); // 삭제 표시\n }\n\n /* 해시 함수 */\n private hashFunc(key: number): number {\n return key % this.capacity;\n }\n\n /* 적재율 */\n private loadFactor(): number {\n return this.size / this.capacity;\n }\n\n /* key에 해당하는 버킷 인덱스 탐색 */\n private findBucket(key: number): number {\n let index = this.hashFunc(key);\n let firstTombstone = -1;\n // 선형 탐사로 탐색하고, 빈 버킷을 만나면 반복문을 종료한다\n while (this.buckets[index] !== null) {\n // key를 만나면 해당 버킷 인덱스를 반환한다\n if (this.buckets[index]!.key === key) {\n // 이전에 삭제 표시를 만났으면 키-값 쌍을 그 인덱스로 옮긴다\n if (firstTombstone !== -1) {\n this.buckets[firstTombstone] = this.buckets[index];\n this.buckets[index] = this.TOMBSTONE;\n return firstTombstone; // 옮긴 후의 버킷 인덱스를 반환한다\n }\n return index; // 버킷 인덱스를 반환한다\n }\n // 처음 만난 삭제 표시를 기록한다\n if (\n firstTombstone === -1 &amp;&amp;\n this.buckets[index] === this.TOMBSTONE\n ) {\n firstTombstone = index;\n }\n // 버킷 인덱스를 계산하고, 끝을 넘어가면 처음으로 돌아온다\n index = (index + 1) % this.capacity;\n }\n // key가 없으면 추가할 위치의 인덱스를 반환한다\n return firstTombstone === -1 ? index : firstTombstone;\n }\n\n /* 조회 연산 */\n get(key: number): string | null {\n // key에 해당하는 버킷 인덱스 탐색\n const index = this.findBucket(key);\n // 키-값 쌍을 찾으면 해당 val을 반환한다\n if (\n this.buckets[index] !== null &amp;&amp;\n this.buckets[index] !== this.TOMBSTONE\n ) {\n return this.buckets[index]!.val;\n }\n // 키-값 쌍이 없으면 null을 반환한다\n return null;\n }\n\n /* 추가 연산 */\n put(key: number, val: string): void {\n // 적재율이 임계값을 초과하면 용량을 확장한다\n if (this.loadFactor() &gt; this.loadThres) {\n this.extend();\n }\n // key에 해당하는 버킷 인덱스 탐색\n const index = this.findBucket(key);\n // 키-값 쌍을 찾으면 val을 덮어쓰고 반환한다\n if (\n this.buckets[index] !== null &amp;&amp;\n this.buckets[index] !== this.TOMBSTONE\n ) {\n this.buckets[index]!.val = val;\n return;\n }\n // 키-값 쌍이 없으면 해당 키-값 쌍을 추가한다\n this.buckets[index] = new Pair(key, val);\n this.size++;\n }\n\n /* 삭제 연산 */\n remove(key: number): void {\n // key에 해당하는 버킷 인덱스 탐색\n const index = this.findBucket(key);\n // 키-값 쌍을 찾으면 삭제 표시로 덮어쓴다\n if (\n this.buckets[index] !== null &amp;&amp;\n this.buckets[index] !== this.TOMBSTONE\n ) {\n this.buckets[index] = this.TOMBSTONE;\n this.size--;\n }\n }\n\n /* 해시 테이블 용량 확장 */\n private extend(): void {\n // 원래 해시 테이블을 임시로 저장한다\n const bucketsTmp = this.buckets;\n // 용량을 확장한 새 해시 테이블을 초기화한다\n this.capacity *= this.extendRatio;\n this.buckets = Array(this.capacity).fill(null);\n this.size = 0;\n // 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n for (const pair of bucketsTmp) {\n if (pair !== null &amp;&amp; pair !== this.TOMBSTONE) {\n this.put(pair.key, pair.val);\n }\n }\n }\n\n /* 해시 테이블 출력 */\n print(): void {\n for (const pair of this.buckets) {\n if (pair === null) {\n console.log('null');\n } else if (pair === this.TOMBSTONE) {\n console.log('TOMBSTONE');\n } else {\n console.log(pair.key + ' -&gt; ' + pair.val);\n }\n }\n }\n}\n</code></pre> hash_map_open_addressing.dart<pre><code>/* 개방 주소법 해시 테이블 */\nclass HashMapOpenAddressing {\n late int _size; // 키-값 쌍 개수\n int _capacity = 4; // 해시 테이블 용량\n double _loadThres = 2.0 / 3.0; // 용량 확장을 수행하는 적재율 임계값\n int _extendRatio = 2; // 용량 확장 배수\n late List&lt;Pair?&gt; _buckets; // 버킷 배열\n Pair _TOMBSTONE = Pair(-1, \"-1\"); // 삭제 표시\n\n /* 생성자 */\n HashMapOpenAddressing() {\n _size = 0;\n _buckets = List.generate(_capacity, (index) =&gt; null);\n }\n\n /* 해시 함수 */\n int hashFunc(int key) {\n return key % _capacity;\n }\n\n /* 적재율 */\n double loadFactor() {\n return _size / _capacity;\n }\n\n /* key에 해당하는 버킷 인덱스 탐색 */\n int findBucket(int key) {\n int index = hashFunc(key);\n int firstTombstone = -1;\n // 선형 탐사로 탐색하고, 빈 버킷을 만나면 반복문을 종료한다\n while (_buckets[index] != null) {\n // key를 만나면 해당 버킷 인덱스를 반환한다\n if (_buckets[index]!.key == key) {\n // 이전에 삭제 표시를 만났으면 키-값 쌍을 그 인덱스로 옮긴다\n if (firstTombstone != -1) {\n _buckets[firstTombstone] = _buckets[index];\n _buckets[index] = _TOMBSTONE;\n return firstTombstone; // 옮긴 후의 버킷 인덱스를 반환한다\n }\n return index; // 버킷 인덱스를 반환한다\n }\n // 처음 만난 삭제 표시를 기록한다\n if (firstTombstone == -1 &amp;&amp; _buckets[index] == _TOMBSTONE) {\n firstTombstone = index;\n }\n // 버킷 인덱스를 계산하고, 끝을 넘어가면 처음으로 돌아온다\n index = (index + 1) % _capacity;\n }\n // key가 없으면 추가할 위치의 인덱스를 반환한다\n return firstTombstone == -1 ? index : firstTombstone;\n }\n\n /* 조회 연산 */\n String? get(int key) {\n // key에 해당하는 버킷 인덱스 탐색\n int index = findBucket(key);\n // 키-값 쌍을 찾으면 해당 val을 반환한다\n if (_buckets[index] != null &amp;&amp; _buckets[index] != _TOMBSTONE) {\n return _buckets[index]!.val;\n }\n // 키-값 쌍이 없으면 null을 반환한다\n return null;\n }\n\n /* 추가 연산 */\n void put(int key, String val) {\n // 적재율이 임계값을 초과하면 용량을 확장한다\n if (loadFactor() &gt; _loadThres) {\n extend();\n }\n // key에 해당하는 버킷 인덱스 탐색\n int index = findBucket(key);\n // 키-값 쌍을 찾으면 val을 덮어쓰고 반환한다\n if (_buckets[index] != null &amp;&amp; _buckets[index] != _TOMBSTONE) {\n _buckets[index]!.val = val;\n return;\n }\n // 키-값 쌍이 없으면 해당 키-값 쌍을 추가한다\n _buckets[index] = new Pair(key, val);\n _size++;\n }\n\n /* 삭제 연산 */\n void remove(int key) {\n // key에 해당하는 버킷 인덱스 탐색\n int index = findBucket(key);\n // 키-값 쌍을 찾으면 삭제 표시로 덮어쓴다\n if (_buckets[index] != null &amp;&amp; _buckets[index] != _TOMBSTONE) {\n _buckets[index] = _TOMBSTONE;\n _size--;\n }\n }\n\n /* 해시 테이블 용량 확장 */\n void extend() {\n // 원래 해시 테이블을 임시로 저장한다\n List&lt;Pair?&gt; bucketsTmp = _buckets;\n // 용량을 확장한 새 해시 테이블을 초기화한다\n _capacity *= _extendRatio;\n _buckets = List.generate(_capacity, (index) =&gt; null);\n _size = 0;\n // 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n for (Pair? pair in bucketsTmp) {\n if (pair != null &amp;&amp; pair != _TOMBSTONE) {\n put(pair.key, pair.val);\n }\n }\n }\n\n /* 해시 테이블 출력 */\n void printHashMap() {\n for (Pair? pair in _buckets) {\n if (pair == null) {\n print(\"null\");\n } else if (pair == _TOMBSTONE) {\n print(\"TOMBSTONE\");\n } else {\n print(\"${pair.key} -&gt; ${pair.val}\");\n }\n }\n }\n}\n</code></pre> hash_map_open_addressing.rs<pre><code>/* 개방 주소법 해시 테이블 */\nstruct HashMapOpenAddressing {\n size: usize, // 키-값 쌍 개수\n capacity: usize, // 해시 테이블 용량\n load_thres: f64, // 용량 확장을 수행하는 적재율 임계값\n extend_ratio: usize, // 용량 확장 배수\n buckets: Vec&lt;Option&lt;Pair&gt;&gt;, // 버킷 배열\n TOMBSTONE: Option&lt;Pair&gt;, // 삭제 표시\n}\n\nimpl HashMapOpenAddressing {\n /* 생성자 */\n fn new() -&gt; Self {\n Self {\n size: 0,\n capacity: 4,\n load_thres: 2.0 / 3.0,\n extend_ratio: 2,\n buckets: vec![None; 4],\n TOMBSTONE: Some(Pair {\n key: -1,\n val: \"-1\".to_string(),\n }),\n }\n }\n\n /* 해시 함수 */\n fn hash_func(&amp;self, key: i32) -&gt; usize {\n (key % self.capacity as i32) as usize\n }\n\n /* 적재율 */\n fn load_factor(&amp;self) -&gt; f64 {\n self.size as f64 / self.capacity as f64\n }\n\n /* key에 해당하는 버킷 인덱스 탐색 */\n fn find_bucket(&amp;mut self, key: i32) -&gt; usize {\n let mut index = self.hash_func(key);\n let mut first_tombstone = -1;\n // 선형 탐사로 탐색하고, 빈 버킷을 만나면 반복문을 종료한다\n while self.buckets[index].is_some() {\n // key를 만나면 해당 버킷 인덱스를 반환한다\n if self.buckets[index].as_ref().unwrap().key == key {\n // 앞서 삭제 표시를 만났다면 키-값 쌍을 해당 인덱스로 옮긴다\n if first_tombstone != -1 {\n self.buckets[first_tombstone as usize] = self.buckets[index].take();\n self.buckets[index] = self.TOMBSTONE.clone();\n return first_tombstone as usize; // 옮긴 후의 버킷 인덱스를 반환한다\n }\n return index; // 버킷 인덱스를 반환한다\n }\n // 처음 만난 삭제 표시를 기록한다\n if first_tombstone == -1 &amp;&amp; self.buckets[index] == self.TOMBSTONE {\n first_tombstone = index as i32;\n }\n // 버킷 인덱스를 계산하고, 끝을 넘어가면 처음으로 돌아온다\n index = (index + 1) % self.capacity;\n }\n // key가 없으면 추가할 위치의 인덱스를 반환한다\n if first_tombstone == -1 {\n index\n } else {\n first_tombstone as usize\n }\n }\n\n /* 조회 연산 */\n fn get(&amp;mut self, key: i32) -&gt; Option&lt;&amp;str&gt; {\n // key에 해당하는 버킷 인덱스 탐색\n let index = self.find_bucket(key);\n // 키-값 쌍을 찾으면 해당 val을 반환한다\n if self.buckets[index].is_some() &amp;&amp; self.buckets[index] != self.TOMBSTONE {\n return self.buckets[index].as_ref().map(|pair| &amp;pair.val as &amp;str);\n }\n // 키-값 쌍이 없으면 null을 반환한다\n None\n }\n\n /* 추가 연산 */\n fn put(&amp;mut self, key: i32, val: String) {\n // 적재율이 임계값을 초과하면 용량을 확장한다\n if self.load_factor() &gt; self.load_thres {\n self.extend();\n }\n // key에 해당하는 버킷 인덱스 탐색\n let index = self.find_bucket(key);\n // 키-값 쌍을 찾으면 val을 덮어쓰고 반환한다\n if self.buckets[index].is_some() &amp;&amp; self.buckets[index] != self.TOMBSTONE {\n self.buckets[index].as_mut().unwrap().val = val;\n return;\n }\n // 키-값 쌍이 없으면 해당 키-값 쌍을 추가한다\n self.buckets[index] = Some(Pair { key, val });\n self.size += 1;\n }\n\n /* 삭제 연산 */\n fn remove(&amp;mut self, key: i32) {\n // key에 해당하는 버킷 인덱스 탐색\n let index = self.find_bucket(key);\n // 키-값 쌍을 찾으면 삭제 표시로 덮어쓴다\n if self.buckets[index].is_some() &amp;&amp; self.buckets[index] != self.TOMBSTONE {\n self.buckets[index] = self.TOMBSTONE.clone();\n self.size -= 1;\n }\n }\n\n /* 해시 테이블 용량 확장 */\n fn extend(&amp;mut self) {\n // 원래 해시 테이블을 임시로 저장한다\n let buckets_tmp = self.buckets.clone();\n // 용량을 확장한 새 해시 테이블을 초기화한다\n self.capacity *= self.extend_ratio;\n self.buckets = vec![None; self.capacity];\n self.size = 0;\n\n // 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n for pair in buckets_tmp {\n if pair.is_none() || pair == self.TOMBSTONE {\n continue;\n }\n let pair = pair.unwrap();\n\n self.put(pair.key, pair.val);\n }\n }\n /* 해시 테이블 출력 */\n fn print(&amp;self) {\n for pair in &amp;self.buckets {\n if pair.is_none() {\n println!(\"null\");\n } else if pair == &amp;self.TOMBSTONE {\n println!(\"TOMBSTONE\");\n } else {\n let pair = pair.as_ref().unwrap();\n println!(\"{} -&gt; {}\", pair.key, pair.val);\n }\n }\n }\n}\n</code></pre> hash_map_open_addressing.c<pre><code>/* 개방 주소법 해시 테이블 */\ntypedef struct {\n int size; // 키-값 쌍 개수\n int capacity; // 해시 테이블 용량\n double loadThres; // 용량 확장을 수행하는 적재율 임계값\n int extendRatio; // 용량 확장 배수\n Pair **buckets; // 버킷 배열\n Pair *TOMBSTONE; // 삭제 표시\n} HashMapOpenAddressing;\n\n/* 생성자 */\nHashMapOpenAddressing *newHashMapOpenAddressing() {\n HashMapOpenAddressing *hashMap = (HashMapOpenAddressing *)malloc(sizeof(HashMapOpenAddressing));\n hashMap-&gt;size = 0;\n hashMap-&gt;capacity = 4;\n hashMap-&gt;loadThres = 2.0 / 3.0;\n hashMap-&gt;extendRatio = 2;\n hashMap-&gt;buckets = (Pair **)calloc(hashMap-&gt;capacity, sizeof(Pair *));\n hashMap-&gt;TOMBSTONE = (Pair *)malloc(sizeof(Pair));\n hashMap-&gt;TOMBSTONE-&gt;key = -1;\n hashMap-&gt;TOMBSTONE-&gt;val = \"-1\";\n\n return hashMap;\n}\n\n/* 소멸자 */\nvoid delHashMapOpenAddressing(HashMapOpenAddressing *hashMap) {\n for (int i = 0; i &lt; hashMap-&gt;capacity; i++) {\n Pair *pair = hashMap-&gt;buckets[i];\n if (pair != NULL &amp;&amp; pair != hashMap-&gt;TOMBSTONE) {\n free(pair-&gt;val);\n free(pair);\n }\n }\n free(hashMap-&gt;buckets);\n free(hashMap-&gt;TOMBSTONE);\n free(hashMap);\n}\n\n/* 해시 함수 */\nint hashFunc(HashMapOpenAddressing *hashMap, int key) {\n return key % hashMap-&gt;capacity;\n}\n\n/* 적재율 */\ndouble loadFactor(HashMapOpenAddressing *hashMap) {\n return (double)hashMap-&gt;size / (double)hashMap-&gt;capacity;\n}\n\n/* key에 해당하는 버킷 인덱스 탐색 */\nint findBucket(HashMapOpenAddressing *hashMap, int key) {\n int index = hashFunc(hashMap, key);\n int firstTombstone = -1;\n // 선형 탐사로 탐색하고, 빈 버킷을 만나면 반복문을 종료한다\n while (hashMap-&gt;buckets[index] != NULL) {\n // key를 만나면 해당 버킷 인덱스를 반환한다\n if (hashMap-&gt;buckets[index]-&gt;key == key) {\n // 이전에 삭제 표시를 만났으면 키-값 쌍을 그 인덱스로 옮긴다\n if (firstTombstone != -1) {\n hashMap-&gt;buckets[firstTombstone] = hashMap-&gt;buckets[index];\n hashMap-&gt;buckets[index] = hashMap-&gt;TOMBSTONE;\n return firstTombstone; // 옮긴 후의 버킷 인덱스를 반환한다\n }\n return index; // 버킷 인덱스를 반환한다\n }\n // 처음 만난 삭제 표시를 기록한다\n if (firstTombstone == -1 &amp;&amp; hashMap-&gt;buckets[index] == hashMap-&gt;TOMBSTONE) {\n firstTombstone = index;\n }\n // 버킷 인덱스를 계산하고, 끝을 넘어가면 처음으로 돌아온다\n index = (index + 1) % hashMap-&gt;capacity;\n }\n // key가 없으면 추가할 위치의 인덱스를 반환한다\n return firstTombstone == -1 ? index : firstTombstone;\n}\n\n/* 조회 연산 */\nchar *get(HashMapOpenAddressing *hashMap, int key) {\n // key에 해당하는 버킷 인덱스 탐색\n int index = findBucket(hashMap, key);\n // 키-값 쌍을 찾으면 해당 val을 반환한다\n if (hashMap-&gt;buckets[index] != NULL &amp;&amp; hashMap-&gt;buckets[index] != hashMap-&gt;TOMBSTONE) {\n return hashMap-&gt;buckets[index]-&gt;val;\n }\n // 키-값 쌍이 없으면 빈 문자열을 반환한다\n return \"\";\n}\n\n/* 추가 연산 */\nvoid put(HashMapOpenAddressing *hashMap, int key, char *val) {\n // 적재율이 임계값을 초과하면 용량을 확장한다\n if (loadFactor(hashMap) &gt; hashMap-&gt;loadThres) {\n extend(hashMap);\n }\n // key에 해당하는 버킷 인덱스 탐색\n int index = findBucket(hashMap, key);\n // 키-값 쌍을 찾으면 val을 덮어쓰고 반환한다\n if (hashMap-&gt;buckets[index] != NULL &amp;&amp; hashMap-&gt;buckets[index] != hashMap-&gt;TOMBSTONE) {\n free(hashMap-&gt;buckets[index]-&gt;val);\n hashMap-&gt;buckets[index]-&gt;val = (char *)malloc(sizeof(strlen(val) + 1));\n strcpy(hashMap-&gt;buckets[index]-&gt;val, val);\n hashMap-&gt;buckets[index]-&gt;val[strlen(val)] = '\\0';\n return;\n }\n // 키-값 쌍이 없으면 해당 키-값 쌍을 추가한다\n Pair *pair = (Pair *)malloc(sizeof(Pair));\n pair-&gt;key = key;\n pair-&gt;val = (char *)malloc(sizeof(strlen(val) + 1));\n strcpy(pair-&gt;val, val);\n pair-&gt;val[strlen(val)] = '\\0';\n\n hashMap-&gt;buckets[index] = pair;\n hashMap-&gt;size++;\n}\n\n/* 삭제 연산 */\nvoid removeItem(HashMapOpenAddressing *hashMap, int key) {\n // key에 해당하는 버킷 인덱스 탐색\n int index = findBucket(hashMap, key);\n // 키-값 쌍을 찾으면 삭제 표시로 덮어쓴다\n if (hashMap-&gt;buckets[index] != NULL &amp;&amp; hashMap-&gt;buckets[index] != hashMap-&gt;TOMBSTONE) {\n Pair *pair = hashMap-&gt;buckets[index];\n free(pair-&gt;val);\n free(pair);\n hashMap-&gt;buckets[index] = hashMap-&gt;TOMBSTONE;\n hashMap-&gt;size--;\n }\n}\n\n/* 해시 테이블 용량 확장 */\nvoid extend(HashMapOpenAddressing *hashMap) {\n // 원래 해시 테이블을 임시로 저장한다\n Pair **bucketsTmp = hashMap-&gt;buckets;\n int oldCapacity = hashMap-&gt;capacity;\n // 용량을 확장한 새 해시 테이블을 초기화한다\n hashMap-&gt;capacity *= hashMap-&gt;extendRatio;\n hashMap-&gt;buckets = (Pair **)calloc(hashMap-&gt;capacity, sizeof(Pair *));\n hashMap-&gt;size = 0;\n // 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n for (int i = 0; i &lt; oldCapacity; i++) {\n Pair *pair = bucketsTmp[i];\n if (pair != NULL &amp;&amp; pair != hashMap-&gt;TOMBSTONE) {\n put(hashMap, pair-&gt;key, pair-&gt;val);\n free(pair-&gt;val);\n free(pair);\n }\n }\n free(bucketsTmp);\n}\n\n/* 해시 테이블 출력 */\nvoid print(HashMapOpenAddressing *hashMap) {\n for (int i = 0; i &lt; hashMap-&gt;capacity; i++) {\n Pair *pair = hashMap-&gt;buckets[i];\n if (pair == NULL) {\n printf(\"NULL\\n\");\n } else if (pair == hashMap-&gt;TOMBSTONE) {\n printf(\"TOMBSTONE\\n\");\n } else {\n printf(\"%d -&gt; %s\\n\", pair-&gt;key, pair-&gt;val);\n }\n }\n}\n</code></pre> hash_map_open_addressing.kt<pre><code>/* 개방 주소법 해시 테이블 */\nclass HashMapOpenAddressing {\n private var size: Int // 키-값 쌍 개수\n private var capacity: Int // 해시 테이블 용량\n private val loadThres: Double // 용량 확장을 수행하는 적재율 임계값\n private val extendRatio: Int // 용량 확장 배수\n private var buckets: Array&lt;Pair?&gt; // 버킷 배열\n private val TOMBSTONE: Pair // 삭제 표시\n\n /* 생성자 */\n init {\n size = 0\n capacity = 4\n loadThres = 2.0 / 3.0\n extendRatio = 2\n buckets = arrayOfNulls(capacity)\n TOMBSTONE = Pair(-1, \"-1\")\n }\n\n /* 해시 함수 */\n fun hashFunc(key: Int): Int {\n return key % capacity\n }\n\n /* 적재율 */\n fun loadFactor(): Double {\n return (size / capacity).toDouble()\n }\n\n /* key에 해당하는 버킷 인덱스 탐색 */\n fun findBucket(key: Int): Int {\n var index = hashFunc(key)\n var firstTombstone = -1\n // 선형 탐사로 탐색하고, 빈 버킷을 만나면 반복문을 종료한다\n while (buckets[index] != null) {\n // key를 만나면 해당 버킷 인덱스를 반환한다\n if (buckets[index]?.key == key) {\n // 이전에 삭제 표시를 만났으면 키-값 쌍을 그 인덱스로 옮긴다\n if (firstTombstone != -1) {\n buckets[firstTombstone] = buckets[index]\n buckets[index] = TOMBSTONE\n return firstTombstone // 옮긴 후의 버킷 인덱스를 반환한다\n }\n return index // 버킷 인덱스를 반환한다\n }\n // 처음 만난 삭제 표시를 기록한다\n if (firstTombstone == -1 &amp;&amp; buckets[index] == TOMBSTONE) {\n firstTombstone = index\n }\n // 버킷 인덱스를 계산하고, 끝을 넘어가면 처음으로 돌아온다\n index = (index + 1) % capacity\n }\n // key가 없으면 추가할 위치의 인덱스를 반환한다\n return if (firstTombstone == -1) index else firstTombstone\n }\n\n /* 조회 연산 */\n fun get(key: Int): String? {\n // key에 해당하는 버킷 인덱스 탐색\n val index = findBucket(key)\n // 키-값 쌍을 찾으면 해당 val을 반환한다\n if (buckets[index] != null &amp;&amp; buckets[index] != TOMBSTONE) {\n return buckets[index]?._val\n }\n // 키-값 쌍이 없으면 null을 반환한다\n return null\n }\n\n /* 추가 연산 */\n fun put(key: Int, _val: String) {\n // 적재율이 임계값을 초과하면 용량을 확장한다\n if (loadFactor() &gt; loadThres) {\n extend()\n }\n // key에 해당하는 버킷 인덱스 탐색\n val index = findBucket(key)\n // 키-값 쌍을 찾으면 val을 덮어쓰고 반환한다\n if (buckets[index] != null &amp;&amp; buckets[index] != TOMBSTONE) {\n buckets[index]!!._val = _val\n return\n }\n // 키-값 쌍이 없으면 해당 키-값 쌍을 추가한다\n buckets[index] = Pair(key, _val)\n size++\n }\n\n /* 삭제 연산 */\n fun remove(key: Int) {\n // key에 해당하는 버킷 인덱스 탐색\n val index = findBucket(key)\n // 키-값 쌍을 찾으면 삭제 표시로 덮어쓴다\n if (buckets[index] != null &amp;&amp; buckets[index] != TOMBSTONE) {\n buckets[index] = TOMBSTONE\n size--\n }\n }\n\n /* 해시 테이블 용량 확장 */\n fun extend() {\n // 원래 해시 테이블을 임시로 저장한다\n val bucketsTmp = buckets\n // 용량을 확장한 새 해시 테이블을 초기화한다\n capacity *= extendRatio\n buckets = arrayOfNulls(capacity)\n size = 0\n // 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n for (pair in bucketsTmp) {\n if (pair != null &amp;&amp; pair != TOMBSTONE) {\n put(pair.key, pair._val)\n }\n }\n }\n\n /* 해시 테이블 출력 */\n fun print() {\n for (pair in buckets) {\n if (pair == null) {\n println(\"null\")\n } else if (pair == TOMBSTONE) {\n println(\"TOMESTOME\")\n } else {\n println(\"${pair.key} -&gt; ${pair._val}\")\n }\n }\n }\n}\n</code></pre> hash_map_open_addressing.rb<pre><code>### 개방 주소법 해시 테이블 ###\nclass HashMapOpenAddressing\n TOMBSTONE = Pair.new(-1, '-1') # 삭제 표시\n\n ### 생성자 ###\n def initialize\n @size = 0 # 키-값 쌍 개수\n @capacity = 4 # 해시 테이블 용량\n @load_thres = 2.0 / 3.0 # 용량 확장을 수행하는 적재율 임계값\n @extend_ratio = 2 # 용량 확장 배수\n @buckets = Array.new(@capacity) # 버킷 배열\n end\n\n ### 해시 함수 ###\n def hash_func(key)\n key % @capacity\n end\n\n ### 적재율 ###\n def load_factor\n @size / @capacity\n end\n\n ### key에 해당하는 버킷 인덱스 탐색 ###\n def find_bucket(key)\n index = hash_func(key)\n first_tombstone = -1\n # 선형 탐사로 탐색하고, 빈 버킷을 만나면 반복문을 종료한다\n while !@buckets[index].nil?\n # key를 만나면 해당 버킷 인덱스를 반환한다\n if @buckets[index].key == key\n # 이전에 삭제 표시를 만났으면 키-값 쌍을 그 인덱스로 옮긴다\n if first_tombstone != -1\n @buckets[first_tombstone] = @buckets[index]\n @buckets[index] = TOMBSTONE\n return first_tombstone # 옮긴 후의 버킷 인덱스를 반환한다\n end\n return index # 버킷 인덱스를 반환한다\n end\n # 처음 만난 삭제 표시를 기록한다\n first_tombstone = index if first_tombstone == -1 &amp;&amp; @buckets[index] == TOMBSTONE\n # 버킷 인덱스를 계산하고, 끝을 넘어가면 처음으로 돌아온다\n index = (index + 1) % @capacity\n end\n # key가 없으면 추가할 위치의 인덱스를 반환한다\n first_tombstone == -1 ? index : first_tombstone\n end\n\n ### 조회 연산 ###\n def get(key)\n # key에 해당하는 버킷 인덱스 탐색\n index = find_bucket(key)\n # 키-값 쌍을 찾으면 해당 val을 반환한다\n return @buckets[index].val unless [nil, TOMBSTONE].include?(@buckets[index])\n # 키-값 쌍이 없으면 nil을 반환한다\n nil\n end\n\n ### 추가 연산 ###\n def put(key, val)\n # 적재율이 임계값을 초과하면 용량을 확장한다\n extend if load_factor &gt; @load_thres\n # key에 해당하는 버킷 인덱스 탐색\n index = find_bucket(key)\n # 키-값 쌍을 찾으면 val을 덮어쓰고 반환한다\n unless [nil, TOMBSTONE].include?(@buckets[index])\n @buckets[index].val = val\n return\n end\n # 키-값 쌍이 없으면 해당 키-값 쌍을 추가한다\n @buckets[index] = Pair.new(key, val)\n @size += 1\n end\n\n ### 삭제 연산 ###\n def remove(key)\n # key에 해당하는 버킷 인덱스 탐색\n index = find_bucket(key)\n # 키-값 쌍을 찾으면 삭제 표시로 덮어쓴다\n unless [nil, TOMBSTONE].include?(@buckets[index])\n @buckets[index] = TOMBSTONE\n @size -= 1\n end\n end\n\n ### 해시 테이블 용량 확장 ###\n def extend\n # 원래 해시 테이블을 임시로 저장한다\n buckets_tmp = @buckets\n # 용량을 확장한 새 해시 테이블을 초기화한다\n @capacity *= @extend_ratio\n @buckets = Array.new(@capacity)\n @size = 0\n # 원래 해시 테이블의 키-값 쌍을 새 해시 테이블로 옮긴다\n for pair in buckets_tmp\n put(pair.key, pair.val) unless [nil, TOMBSTONE].include?(pair)\n end\n end\n\n ### 해시 테이블 출력 ###\n def print\n for pair in @buckets\n if pair.nil?\n puts \"Nil\"\n elsif pair == TOMBSTONE\n puts \"TOMBSTONE\"\n else\n puts \"#{pair.key} -&gt; #{pair.val}\"\n end\n end\n end\nend\n</code></pre>","path":["제 6 장   해시 테이블","6.2   해시 충돌"],"tags":[]},{"location":"chapter_hashing/hash_collision/#2","level":3,"title":"2.   이차 탐사","text":"<p>이차 탐사는 선형 탐사처럼 개방 주소법의 대표적인 전략이다. 충돌이 발생하면 고정 간격으로 이동하는 대신 ‘탐사 횟수의 제곱’만큼, 즉 \\(1, 4, 9, \\dots\\)칸을 이동한다.</p> <p>이차 탐사의 주요 장점은 다음과 같다.</p> <ul> <li>탐사 횟수의 제곱만큼 떨어진 위치로 이동하여 선형 탐사의 군집 현상을 완화하려 한다.</li> <li>더 먼 거리로 건너뛰며 빈 위치를 찾으므로 데이터를 더 고르게 분포시키는 데 도움이 된다.</li> </ul> <p>하지만 이차 탐사도 완벽하지는 않다.</p> <ul> <li>일부 위치가 다른 위치보다 더 쉽게 채워지는 군집 현상이 여전히 존재한다.</li> <li>제곱의 증가 때문에 해시 테이블 전체를 탐사하지 못할 수 있다. 즉, 빈 버킷이 있어도 그곳에 도달하지 못할 수 있다.</li> </ul>","path":["제 6 장   해시 테이블","6.2   해시 충돌"],"tags":[]},{"location":"chapter_hashing/hash_collision/#3","level":3,"title":"3.   다중 해싱","text":"<p>이름 그대로 여러 해시 함수 \\(f_1(x)\\), \\(f_2(x)\\), \\(f_3(x)\\), \\(\\dots\\)를 사용하여 탐사하는 방법이다.</p> <ul> <li>원소 삽입: 해시 함수 \\(f_1(x)\\)에서 충돌하면 \\(f_2(x)\\)를 시도한다. 이런 식으로 빈 위치를 찾을 때까지 계속한 뒤 원소를 넣는다.</li> <li>원소 탐색: 같은 해시 함수 순서로 탐색하여 목표 원소를 찾으면 반환한다. 빈 위치를 만나거나 모든 함수를 시도했다면 원소가 없다는 뜻이므로 <code>None</code>을 반환한다.</li> </ul> <p>선형 탐사보다 군집 현상이 덜 발생하지만, 여러 해시 함수로 인해 추가 계산이 필요하다.</p> <p>참고</p> <p>개방 주소법의 선형 탐사, 이차 탐사, 다중 해싱 모두 ‘원소를 곧바로 삭제할 수 없다’는 문제가 있다는 점에 주의하자.</p>","path":["제 6 장   해시 테이블","6.2   해시 충돌"],"tags":[]},{"location":"chapter_hashing/hash_collision/#623","level":2,"title":"6.2.3   프로그래밍 언어의 선택","text":"<p>프로그래밍 언어마다 해시 테이블 구현 전략이 다르다. 몇 가지 예를 살펴보자.</p> <ul> <li>Python은 개방 주소법을 사용한다. 딕셔너리 <code>dict</code>는 의사 난수를 이용해 탐사한다.</li> <li>Java는 체이닝을 사용한다. JDK 1.8부터는 <code>HashMap</code> 내부 배열 길이가 64 이상이고 연결 리스트 길이가 8 이상이면, 탐색 성능을 높이기 위해 연결 리스트를 레드-블랙 트리로 바꾼다.</li> <li>Go 1.24부터 내장 <code>map</code>은 기본적으로 Swiss Table 기반의 개방 주소법을 사용한다. Go 1.23 이하에서는 체이닝을 사용하며, 버킷 하나에 키-값 쌍을 최대 8개 저장하고 용량을 넘으면 오버플로 버킷을 연결한다.</li> </ul>","path":["제 6 장   해시 테이블","6.2   해시 충돌"],"tags":[]},{"location":"chapter_hashing/hash_map/","level":1,"title":"6.1   해시 테이블","text":"<p>해시 테이블(hash table)은 해시 표라고도 하며, 키 <code>key</code>와 값 <code>value</code> 사이의 대응 관계를 만들어 원소를 효율적으로 조회한다. 구체적으로 키 <code>key</code>를 입력하면 해당 값 <code>value</code>를 \\(O(1)\\) 시간에 얻을 수 있다.</p> <p>아래 그림(6-1)과 같이 학생 \\(n\\)명에게 각각 ‘이름’과 ‘학번’이라는 두 데이터가 있다고 하자. ‘학번을 입력하면 해당 이름을 반환하는’ 조회 기능을 구현하려면 그림의 해시 테이블을 사용할 수 있다.</p> <p></p> <p> 그림 6-1   해시 테이블의 추상적 표현 </p> <p>해시 테이블 외에 배열과 연결 리스트로도 조회 기능을 구현할 수 있다. 효율 비교는 아래 표(6-1)와 같다.</p> <ul> <li>원소 추가: 배열(연결 리스트)의 끝에 원소를 추가하면 되므로 \\(O(1)\\) 시간이 걸린다.</li> <li>원소 조회: 배열(연결 리스트)이 정렬되어 있지 않으므로 모든 원소를 순회해야 하며 \\(O(n)\\) 시간이 걸린다.</li> <li>원소 삭제: 먼저 원소를 찾은 뒤 배열(연결 리스트)에서 삭제해야 하므로 \\(O(n)\\) 시간이 걸린다.</li> </ul> <p> 표 6-1   원소 조회 효율 비교 </p> 배열 연결 리스트 해시 테이블 원소 탐색 \\(O(n)\\) \\(O(n)\\) \\(O(1)\\) 원소 추가 \\(O(1)\\) \\(O(1)\\) \\(O(1)\\) 원소 삭제 \\(O(n)\\) \\(O(n)\\) \\(O(1)\\) <p>이처럼 해시 테이블에서는 삽입, 삭제, 탐색, 갱신의 시간 복잡도가 모두 \\(O(1)\\)로 매우 효율적이다.</p>","path":["제 6 장   해시 테이블","6.1   해시 테이블"],"tags":[]},{"location":"chapter_hashing/hash_map/#611","level":2,"title":"6.1.1   해시 테이블의 기본 연산","text":"<p>대표적인 연산에는 초기화, 조회, 키-값 쌍 추가와 삭제 등이 있다. 예제 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map.py<pre><code># 해시 테이블 초기화\nhmap: dict = {}\n\n# 추가 연산\n# 해시 테이블에 키-값 쌍 (key, value) 추가\nhmap[12836] = \"小哈\"\nhmap[15937] = \"小啰\"\nhmap[16750] = \"小算\"\nhmap[13276] = \"小法\"\nhmap[10583] = \"小鸭\"\n\n# 조회 연산\n# 해시 테이블에 키 key를 입력하여 값 value를 얻음\nname: str = hmap[15937]\n\n# 삭제 연산\n# 해시 테이블에서 키-값 쌍 (key, value) 삭제\nhmap.pop(10583)\n</code></pre> hash_map.cpp<pre><code>/* 해시 테이블 초기화 */\nunordered_map&lt;int, string&gt; map;\n\n/* 추가 연산 */\n// 해시 테이블에 키-값 쌍 (key, value) 추가\nmap[12836] = \"小哈\";\nmap[15937] = \"小啰\";\nmap[16750] = \"小算\";\nmap[13276] = \"小法\";\nmap[10583] = \"小鸭\";\n\n/* 조회 연산 */\n// 해시 테이블에 키 key를 입력하여 값 value를 얻음\nstring name = map[15937];\n\n/* 삭제 연산 */\n// 해시 테이블에서 키-값 쌍 (key, value) 삭제\nmap.erase(10583);\n</code></pre> hash_map.java<pre><code>/* 해시 테이블 초기화 */\nMap&lt;Integer, String&gt; map = new HashMap&lt;&gt;();\n\n/* 추가 연산 */\n// 해시 테이블에 키-값 쌍 (key, value) 추가\nmap.put(12836, \"小哈\");\nmap.put(15937, \"小啰\");\nmap.put(16750, \"小算\");\nmap.put(13276, \"小法\");\nmap.put(10583, \"小鸭\");\n\n/* 조회 연산 */\n// 해시 테이블에 키 key를 입력하여 값 value를 얻음\nString name = map.get(15937);\n\n/* 삭제 연산 */\n// 해시 테이블에서 키-값 쌍 (key, value) 삭제\nmap.remove(10583);\n</code></pre> hash_map.cs<pre><code>/* 해시 테이블 초기화 */\nDictionary&lt;int, string&gt; map = new() {\n /* 추가 연산 */\n // 해시 테이블에 키-값 쌍 (key, value) 추가\n { 12836, \"小哈\" },\n { 15937, \"小啰\" },\n { 16750, \"小算\" },\n { 13276, \"小法\" },\n { 10583, \"小鸭\" }\n};\n\n/* 조회 연산 */\n// 해시 테이블에 키 key를 입력하여 값 value를 얻음\nstring name = map[15937];\n\n/* 삭제 연산 */\n// 해시 테이블에서 키-값 쌍 (key, value) 삭제\nmap.Remove(10583);\n</code></pre> hash_map_test.go<pre><code>/* 해시 테이블 초기화 */\nhmap := make(map[int]string)\n\n/* 추가 연산 */\n// 해시 테이블에 키-값 쌍 (key, value) 추가\nhmap[12836] = \"小哈\"\nhmap[15937] = \"小啰\"\nhmap[16750] = \"小算\"\nhmap[13276] = \"小法\"\nhmap[10583] = \"小鸭\"\n\n/* 조회 연산 */\n// 해시 테이블에 키 key를 입력하여 값 value를 얻음\nname := hmap[15937]\n\n/* 삭제 연산 */\n// 해시 테이블에서 키-값 쌍 (key, value) 삭제\ndelete(hmap, 10583)\n</code></pre> hash_map.swift<pre><code>/* 해시 테이블 초기화 */\nvar map: [Int: String] = [:]\n\n/* 추가 연산 */\n// 해시 테이블에 키-값 쌍 (key, value) 추가\nmap[12836] = \"小哈\"\nmap[15937] = \"小啰\"\nmap[16750] = \"小算\"\nmap[13276] = \"小法\"\nmap[10583] = \"小鸭\"\n\n/* 조회 연산 */\n// 해시 테이블에 키 key를 입력하여 값 value를 얻음\nlet name = map[15937]!\n\n/* 삭제 연산 */\n// 해시 테이블에서 키-값 쌍 (key, value) 삭제\nmap.removeValue(forKey: 10583)\n</code></pre> hash_map.js<pre><code>/* 해시 테이블 초기화 */\nconst map = new Map();\n/* 추가 연산 */\n// 해시 테이블에 키-값 쌍 (key, value) 추가\nmap.set(12836, '小哈');\nmap.set(15937, '小啰');\nmap.set(16750, '小算');\nmap.set(13276, '小法');\nmap.set(10583, '小鸭');\n\n/* 조회 연산 */\n// 해시 테이블에 키 key를 입력하여 값 value를 얻음\nlet name = map.get(15937);\n\n/* 삭제 연산 */\n// 해시 테이블에서 키-값 쌍 (key, value) 삭제\nmap.delete(10583);\n</code></pre> hash_map.ts<pre><code>/* 해시 테이블 초기화 */\nconst map = new Map&lt;number, string&gt;();\n/* 추가 연산 */\n// 해시 테이블에 키-값 쌍 (key, value) 추가\nmap.set(12836, '小哈');\nmap.set(15937, '小啰');\nmap.set(16750, '小算');\nmap.set(13276, '小法');\nmap.set(10583, '小鸭');\nconsole.info('\\n추가한 뒤의 해시 테이블\\nKey -&gt; Value');\nconsole.info(map);\n\n/* 조회 연산 */\n// 해시 테이블에 키 key를 입력하여 값 value를 얻음\nlet name = map.get(15937);\nconsole.info('\\n학번 15937을 입력하여 찾은 이름 ' + name);\n\n/* 삭제 연산 */\n// 해시 테이블에서 키-값 쌍 (key, value) 삭제\nmap.delete(10583);\nconsole.info('\\n10583을 삭제한 뒤의 해시 테이블\\nKey -&gt; Value');\nconsole.info(map);\n</code></pre> hash_map.dart<pre><code>/* 해시 테이블 초기화 */\nMap&lt;int, String&gt; map = {};\n\n/* 추가 연산 */\n// 해시 테이블에 키-값 쌍 (key, value) 추가\nmap[12836] = \"小哈\";\nmap[15937] = \"小啰\";\nmap[16750] = \"小算\";\nmap[13276] = \"小法\";\nmap[10583] = \"小鸭\";\n\n/* 조회 연산 */\n// 해시 테이블에 키 key를 입력하여 값 value를 얻음\nString name = map[15937];\n\n/* 삭제 연산 */\n// 해시 테이블에서 키-값 쌍 (key, value) 삭제\nmap.remove(10583);\n</code></pre> hash_map.rs<pre><code>use std::collections::HashMap;\n\n/* 해시 테이블 초기화 */\nlet mut map: HashMap&lt;i32, String&gt; = HashMap::new();\n\n/* 추가 연산 */\n// 해시 테이블에 키-값 쌍 (key, value) 추가\nmap.insert(12836, \"小哈\".to_string());\nmap.insert(15937, \"小啰\".to_string());\nmap.insert(16750, \"小算\".to_string());\nmap.insert(13279, \"小法\".to_string());\nmap.insert(10583, \"小鸭\".to_string());\n\n/* 조회 연산 */\n// 해시 테이블에 키 key를 입력하여 값 value를 얻음\nlet _name: Option&lt;&amp;String&gt; = map.get(&amp;15937);\n\n/* 삭제 연산 */\n// 해시 테이블에서 키-값 쌍 (key, value) 삭제\nlet _removed_value: Option&lt;String&gt; = map.remove(&amp;10583);\n</code></pre> hash_map.c<pre><code>// C는 내장 해시 테이블을 제공하지 않는다\n</code></pre> hash_map.kt<pre><code>/* 해시 테이블 초기화 */\nval map = HashMap&lt;Int,String&gt;()\n\n/* 추가 연산 */\n// 해시 테이블에 키-값 쌍 (key, value) 추가\nmap[12836] = \"小哈\"\nmap[15937] = \"小啰\"\nmap[16750] = \"小算\"\nmap[13276] = \"小法\"\nmap[10583] = \"小鸭\"\n\n/* 조회 연산 */\n// 해시 테이블에 키 key를 입력하여 값 value를 얻음\nval name = map[15937]\n\n/* 삭제 연산 */\n// 해시 테이블에서 키-값 쌍 (key, value) 삭제\nmap.remove(10583)\n</code></pre> hash_map.rb<pre><code># 해시 테이블 초기화\nhmap = {}\n\n# 추가 연산\n# 해시 테이블에 키-값 쌍 (key, value) 추가\nhmap[12836] = \"小哈\"\nhmap[15937] = \"小啰\"\nhmap[16750] = \"小算\"\nhmap[13276] = \"小法\"\nhmap[10583] = \"小鸭\"\n\n# 조회 연산\n# 해시 테이블에 키 key를 입력하여 값 value를 얻음\nname = hmap[15937]\n\n# 삭제 연산\n# 해시 테이블에서 키-값 쌍 (key, value) 삭제\nhmap.delete(10583)\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>해시 테이블을 순회하는 대표적인 방식은 키-값 쌍 순회, 키 순회, 값 순회의 세 가지다. 예제 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map.py<pre><code># 해시 테이블 순회\n# 키-값 쌍 key-&gt;value 순회\nfor key, value in hmap.items():\n print(key, \"-&gt;\", value)\n# 키 key만 순회\nfor key in hmap.keys():\n print(key)\n# 값 value만 순회\nfor value in hmap.values():\n print(value)\n</code></pre> hash_map.cpp<pre><code>/* 해시 테이블 순회 */\n// 키-값 쌍 key-&gt;value 순회\nfor (auto kv: map) {\n cout &lt;&lt; kv.first &lt;&lt; \" -&gt; \" &lt;&lt; kv.second &lt;&lt; endl;\n}\n// 이터레이터로 key-&gt;value 순회\nfor (auto iter = map.begin(); iter != map.end(); iter++) {\n cout &lt;&lt; iter-&gt;first &lt;&lt; \"-&gt;\" &lt;&lt; iter-&gt;second &lt;&lt; endl;\n}\n</code></pre> hash_map.java<pre><code>/* 해시 테이블 순회 */\n// 키-값 쌍 key-&gt;value 순회\nfor (Map.Entry &lt;Integer, String&gt; kv: map.entrySet()) {\n System.out.println(kv.getKey() + \" -&gt; \" + kv.getValue());\n}\n// 키 key만 순회\nfor (int key: map.keySet()) {\n System.out.println(key);\n}\n// 값 value만 순회\nfor (String val: map.values()) {\n System.out.println(val);\n}\n</code></pre> hash_map.cs<pre><code>/* 해시 테이블 순회 */\n// 키-값 쌍 Key-&gt;Value 순회\nforeach (var kv in map) {\n Console.WriteLine(kv.Key + \" -&gt; \" + kv.Value);\n}\n// 키 key만 순회\nforeach (int key in map.Keys) {\n Console.WriteLine(key);\n}\n// 값 value만 순회\nforeach (string val in map.Values) {\n Console.WriteLine(val);\n}\n</code></pre> hash_map_test.go<pre><code>/* 해시 테이블 순회 */\n// 키-값 쌍 key-&gt;value 순회\nfor key, value := range hmap {\n fmt.Println(key, \"-&gt;\", value)\n}\n// 키 key만 순회\nfor key := range hmap {\n fmt.Println(key)\n}\n// 값 value만 순회\nfor _, value := range hmap {\n fmt.Println(value)\n}\n</code></pre> hash_map.swift<pre><code>/* 해시 테이블 순회 */\n// 키-값 쌍 Key-&gt;Value 순회\nfor (key, value) in map {\n print(\"\\(key) -&gt; \\(value)\")\n}\n// 키 Key만 순회\nfor key in map.keys {\n print(key)\n}\n// 값 Value만 순회\nfor value in map.values {\n print(value)\n}\n</code></pre> hash_map.js<pre><code>/* 해시 테이블 순회 */\nconsole.info('\\n키-값 쌍 Key-&gt;Value 순회');\nfor (const [k, v] of map.entries()) {\n console.info(k + ' -&gt; ' + v);\n}\nconsole.info('\\n키 Key만 순회');\nfor (const k of map.keys()) {\n console.info(k);\n}\nconsole.info('\\n값 Value만 순회');\nfor (const v of map.values()) {\n console.info(v);\n}\n</code></pre> hash_map.ts<pre><code>/* 해시 테이블 순회 */\nconsole.info('\\n키-값 쌍 Key-&gt;Value 순회');\nfor (const [k, v] of map.entries()) {\n console.info(k + ' -&gt; ' + v);\n}\nconsole.info('\\n키 Key만 순회');\nfor (const k of map.keys()) {\n console.info(k);\n}\nconsole.info('\\n값 Value만 순회');\nfor (const v of map.values()) {\n console.info(v);\n}\n</code></pre> hash_map.dart<pre><code>/* 해시 테이블 순회 */\n// 키-값 쌍 Key-&gt;Value 순회\nmap.forEach((key, value) {\n print('$key -&gt; $value');\n});\n\n// 키 Key만 순회\nmap.keys.forEach((key) {\n print(key);\n});\n\n// 값 Value만 순회\nmap.values.forEach((value) {\n print(value);\n});\n</code></pre> hash_map.rs<pre><code>/* 해시 테이블 순회 */\n// 키-값 쌍 Key-&gt;Value 순회\nfor (key, value) in &amp;map {\n println!(\"{key} -&gt; {value}\");\n}\n\n// 키 Key만 순회\nfor key in map.keys() {\n println!(\"{key}\");\n}\n\n// 값 Value만 순회\nfor value in map.values() {\n println!(\"{value}\");\n}\n</code></pre> hash_map.c<pre><code>// C는 내장 해시 테이블을 제공하지 않는다\n</code></pre> hash_map.kt<pre><code>/* 해시 테이블 순회 */\n// 키-값 쌍 key-&gt;value 순회\nfor ((key, value) in map) {\n println(\"$key -&gt; $value\")\n}\n// 키 key만 순회\nfor (key in map.keys) {\n println(key)\n}\n// 값 value만 순회\nfor (_val in map.values) {\n println(_val)\n}\n</code></pre> hash_map.rb<pre><code># 해시 테이블 순회\n# 키-값 쌍 key-&gt;value 순회\nhmap.entries.each { |key, value| puts \"#{key} -&gt; #{value}\" }\n\n# 키 key만 순회\nhmap.keys.each { |key| puts key }\n\n# 값 value만 순회\nhmap.values.each { |val| puts val }\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 6 장   해시 테이블","6.1   해시 테이블"],"tags":[]},{"location":"chapter_hashing/hash_map/#612","level":2,"title":"6.1.2   간단한 해시 테이블 구현","text":"<p>먼저 가장 간단한 경우로 배열 하나만 사용하여 해시 테이블을 구현해 보자. 해시 테이블에서는 배열의 각 자리를 버킷(bucket)이라고 하며, 버킷마다 키-값 쌍 하나를 저장할 수 있다. 따라서 조회 연산은 <code>key</code>에 해당하는 버킷을 찾고 그 안에서 <code>value</code>를 얻는 것이다.</p> <p>그렇다면 <code>key</code>로 해당 버킷을 어떻게 찾을까? 이를 위해 해시 함수(hash function)를 사용한다. 해시 함수는 큰 입력 공간을 작은 출력 공간에 매핑한다. 해시 테이블에서 입력 공간은 모든 <code>key</code>이고, 출력 공간은 모든 버킷(배열 인덱스)이다. 즉, <code>key</code>를 입력하면 해시 함수를 통해 그 키-값 쌍이 배열에 저장되는 위치를 얻을 수 있다.</p> <p><code>key</code>를 입력했을 때 해시 함수의 계산 과정은 다음 두 단계로 나뉜다.</p> <ol> <li>어떤 해시 알고리즘 <code>hash()</code>로 해시값을 계산한다.</li> <li>해시값을 버킷 수(배열 길이) <code>capacity</code>로 나눈 나머지를 구하여 <code>key</code>에 해당하는 버킷(배열 인덱스) <code>index</code>를 얻는다.</li> </ol> <pre><code>index = hash(key) % capacity\n</code></pre> <p>그런 다음 <code>index</code>로 해시 테이블의 해당 버킷에 접근하여 <code>value</code>를 얻는다.</p> <p>배열 길이가 <code>capacity = 100</code>이고 해시 알고리즘이 <code>hash(key) = key</code>이면, 해시 함수는 <code>key % 100</code>이다. 아래 그림(6-2)은 학번을 <code>key</code>, 이름을 <code>value</code>로 삼아 해시 함수의 작동 원리를 보여 준다.</p> <p></p> <p> 그림 6-2   해시 함수의 작동 원리 </p> <p>다음 코드는 간단한 해시 테이블을 구현한다. <code>key</code>와 <code>value</code>를 <code>Pair</code> 클래스로 묶어 키-값 쌍을 표현한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_hash_map.py<pre><code>class Pair:\n \"\"\"키-값 쌍\"\"\"\n\n def __init__(self, key: int, val: str):\n self.key = key\n self.val = val\n\nclass ArrayHashMap:\n \"\"\"배열로 구현한 해시 테이블\"\"\"\n\n def __init__(self):\n \"\"\"생성자\"\"\"\n # 버킷 100개를 포함하는 배열을 초기화한다\n self.buckets: list[Pair | None] = [None] * 100\n\n def hash_func(self, key: int) -&gt; int:\n \"\"\"해시 함수\"\"\"\n index = key % 100\n return index\n\n def get(self, key: int) -&gt; str | None:\n \"\"\"조회 연산\"\"\"\n index: int = self.hash_func(key)\n pair: Pair = self.buckets[index]\n if pair is None:\n return None\n return pair.val\n\n def put(self, key: int, val: str):\n \"\"\"추가 및 갱신 연산\"\"\"\n pair = Pair(key, val)\n index: int = self.hash_func(key)\n self.buckets[index] = pair\n\n def remove(self, key: int):\n \"\"\"삭제 연산\"\"\"\n index: int = self.hash_func(key)\n # None으로 설정하여 삭제를 나타낸다\n self.buckets[index] = None\n\n def entry_set(self) -&gt; list[Pair]:\n \"\"\"모든 키-값 쌍 반환\"\"\"\n result: list[Pair] = []\n for pair in self.buckets:\n if pair is not None:\n result.append(pair)\n return result\n\n def key_set(self) -&gt; list[int]:\n \"\"\"모든 키 반환\"\"\"\n result = []\n for pair in self.buckets:\n if pair is not None:\n result.append(pair.key)\n return result\n\n def value_set(self) -&gt; list[str]:\n \"\"\"모든 값 반환\"\"\"\n result = []\n for pair in self.buckets:\n if pair is not None:\n result.append(pair.val)\n return result\n\n def print(self):\n \"\"\"해시 테이블 출력\"\"\"\n for pair in self.buckets:\n if pair is not None:\n print(pair.key, \"-&gt;\", pair.val)\n</code></pre> array_hash_map.cpp<pre><code>/* 키-값 쌍 */\nstruct Pair {\n public:\n int key;\n string val;\n Pair(int key, string val) {\n this-&gt;key = key;\n this-&gt;val = val;\n }\n};\n\n/* 배열로 구현한 해시 테이블 */\nclass ArrayHashMap {\n private:\n vector&lt;Pair *&gt; buckets;\n\n public:\n ArrayHashMap() {\n // 버킷 100개를 포함하는 배열을 초기화한다\n buckets = vector&lt;Pair *&gt;(100);\n }\n\n ~ArrayHashMap() {\n // 메모리 해제\n for (const auto &amp;bucket : buckets) {\n delete bucket;\n }\n buckets.clear();\n }\n\n /* 해시 함수 */\n int hashFunc(int key) {\n int index = key % 100;\n return index;\n }\n\n /* 조회 연산 */\n string get(int key) {\n int index = hashFunc(key);\n Pair *pair = buckets[index];\n if (pair == nullptr)\n return \"\";\n return pair-&gt;val;\n }\n\n /* 추가 연산 */\n void put(int key, string val) {\n Pair *pair = new Pair(key, val);\n int index = hashFunc(key);\n buckets[index] = pair;\n }\n\n /* 삭제 연산 */\n void remove(int key) {\n int index = hashFunc(key);\n // 메모리를 해제하고 nullptr로 설정한다\n delete buckets[index];\n buckets[index] = nullptr;\n }\n\n /* 모든 키-값 쌍 반환 */\n vector&lt;Pair *&gt; pairSet() {\n vector&lt;Pair *&gt; pairSet;\n for (Pair *pair : buckets) {\n if (pair != nullptr) {\n pairSet.push_back(pair);\n }\n }\n return pairSet;\n }\n\n /* 모든 키 반환 */\n vector&lt;int&gt; keySet() {\n vector&lt;int&gt; keySet;\n for (Pair *pair : buckets) {\n if (pair != nullptr) {\n keySet.push_back(pair-&gt;key);\n }\n }\n return keySet;\n }\n\n /* 모든 값 반환 */\n vector&lt;string&gt; valueSet() {\n vector&lt;string&gt; valueSet;\n for (Pair *pair : buckets) {\n if (pair != nullptr) {\n valueSet.push_back(pair-&gt;val);\n }\n }\n return valueSet;\n }\n\n /* 해시 테이블 출력 */\n void print() {\n for (Pair *kv : pairSet()) {\n cout &lt;&lt; kv-&gt;key &lt;&lt; \" -&gt; \" &lt;&lt; kv-&gt;val &lt;&lt; endl;\n }\n }\n};\n</code></pre> array_hash_map.java<pre><code>/* 키-값 쌍 */\nclass Pair {\n public int key;\n public String val;\n\n public Pair(int key, String val) {\n this.key = key;\n this.val = val;\n }\n}\n\n/* 배열로 구현한 해시 테이블 */\nclass ArrayHashMap {\n private List&lt;Pair&gt; buckets;\n\n public ArrayHashMap() {\n // 버킷 100개를 포함하는 배열을 초기화한다\n buckets = new ArrayList&lt;&gt;();\n for (int i = 0; i &lt; 100; i++) {\n buckets.add(null);\n }\n }\n\n /* 해시 함수 */\n private int hashFunc(int key) {\n int index = key % 100;\n return index;\n }\n\n /* 조회 연산 */\n public String get(int key) {\n int index = hashFunc(key);\n Pair pair = buckets.get(index);\n if (pair == null)\n return null;\n return pair.val;\n }\n\n /* 추가 연산 */\n public void put(int key, String val) {\n Pair pair = new Pair(key, val);\n int index = hashFunc(key);\n buckets.set(index, pair);\n }\n\n /* 삭제 연산 */\n public void remove(int key) {\n int index = hashFunc(key);\n // null로 설정하여 삭제를 나타낸다\n buckets.set(index, null);\n }\n\n /* 모든 키-값 쌍 반환 */\n public List&lt;Pair&gt; pairSet() {\n List&lt;Pair&gt; pairSet = new ArrayList&lt;&gt;();\n for (Pair pair : buckets) {\n if (pair != null)\n pairSet.add(pair);\n }\n return pairSet;\n }\n\n /* 모든 키 반환 */\n public List&lt;Integer&gt; keySet() {\n List&lt;Integer&gt; keySet = new ArrayList&lt;&gt;();\n for (Pair pair : buckets) {\n if (pair != null)\n keySet.add(pair.key);\n }\n return keySet;\n }\n\n /* 모든 값 반환 */\n public List&lt;String&gt; valueSet() {\n List&lt;String&gt; valueSet = new ArrayList&lt;&gt;();\n for (Pair pair : buckets) {\n if (pair != null)\n valueSet.add(pair.val);\n }\n return valueSet;\n }\n\n /* 해시 테이블 출력 */\n public void print() {\n for (Pair kv : pairSet()) {\n System.out.println(kv.key + \" -&gt; \" + kv.val);\n }\n }\n}\n</code></pre> array_hash_map.cs<pre><code>/* 키-값 쌍 int-&gt;string */\nclass Pair(int key, string val) {\n public int key = key;\n public string val = val;\n}\n\n/* 배열로 구현한 해시 테이블 */\nclass ArrayHashMap {\n List&lt;Pair?&gt; buckets;\n public ArrayHashMap() {\n // 버킷 100개를 포함하는 배열을 초기화한다\n buckets = [];\n for (int i = 0; i &lt; 100; i++) {\n buckets.Add(null);\n }\n }\n\n /* 해시 함수 */\n int HashFunc(int key) {\n int index = key % 100;\n return index;\n }\n\n /* 조회 연산 */\n public string? Get(int key) {\n int index = HashFunc(key);\n Pair? pair = buckets[index];\n if (pair == null) return null;\n return pair.val;\n }\n\n /* 추가 연산 */\n public void Put(int key, string val) {\n Pair pair = new(key, val);\n int index = HashFunc(key);\n buckets[index] = pair;\n }\n\n /* 삭제 연산 */\n public void Remove(int key) {\n int index = HashFunc(key);\n // null로 설정하여 삭제를 나타낸다\n buckets[index] = null;\n }\n\n /* 모든 키-값 쌍 반환 */\n public List&lt;Pair&gt; PairSet() {\n List&lt;Pair&gt; pairSet = [];\n foreach (Pair? pair in buckets) {\n if (pair != null)\n pairSet.Add(pair);\n }\n return pairSet;\n }\n\n /* 모든 키 반환 */\n public List&lt;int&gt; KeySet() {\n List&lt;int&gt; keySet = [];\n foreach (Pair? pair in buckets) {\n if (pair != null)\n keySet.Add(pair.key);\n }\n return keySet;\n }\n\n /* 모든 값 반환 */\n public List&lt;string&gt; ValueSet() {\n List&lt;string&gt; valueSet = [];\n foreach (Pair? pair in buckets) {\n if (pair != null)\n valueSet.Add(pair.val);\n }\n return valueSet;\n }\n\n /* 해시 테이블 출력 */\n public void Print() {\n foreach (Pair kv in PairSet()) {\n Console.WriteLine(kv.key + \" -&gt; \" + kv.val);\n }\n }\n}\n</code></pre> array_hash_map.go<pre><code>/* 키-값 쌍 */\ntype pair struct {\n key int\n val string\n}\n\n/* 배열로 구현한 해시 테이블 */\ntype arrayHashMap struct {\n buckets []*pair\n}\n\n/* 해시 테이블 초기화 */\nfunc newArrayHashMap() *arrayHashMap {\n // 버킷 100개를 포함하는 배열을 초기화한다\n buckets := make([]*pair, 100)\n return &amp;arrayHashMap{buckets: buckets}\n}\n\n/* 해시 함수 */\nfunc (a *arrayHashMap) hashFunc(key int) int {\n index := key % 100\n return index\n}\n\n/* 조회 연산 */\nfunc (a *arrayHashMap) get(key int) string {\n index := a.hashFunc(key)\n pair := a.buckets[index]\n if pair == nil {\n return \"찾을 수 없음\"\n }\n return pair.val\n}\n\n/* 추가 연산 */\nfunc (a *arrayHashMap) put(key int, val string) {\n pair := &amp;pair{key: key, val: val}\n index := a.hashFunc(key)\n a.buckets[index] = pair\n}\n\n/* 삭제 연산 */\nfunc (a *arrayHashMap) remove(key int) {\n index := a.hashFunc(key)\n // nil로 설정하여 삭제를 나타낸다\n a.buckets[index] = nil\n}\n\n/* 모든 키-값 쌍 반환 */\nfunc (a *arrayHashMap) pairSet() []*pair {\n var pairs []*pair\n for _, pair := range a.buckets {\n if pair != nil {\n pairs = append(pairs, pair)\n }\n }\n return pairs\n}\n\n/* 모든 키 반환 */\nfunc (a *arrayHashMap) keySet() []int {\n var keys []int\n for _, pair := range a.buckets {\n if pair != nil {\n keys = append(keys, pair.key)\n }\n }\n return keys\n}\n\n/* 모든 값 반환 */\nfunc (a *arrayHashMap) valueSet() []string {\n var values []string\n for _, pair := range a.buckets {\n if pair != nil {\n values = append(values, pair.val)\n }\n }\n return values\n}\n\n/* 해시 테이블 출력 */\nfunc (a *arrayHashMap) print() {\n for _, pair := range a.buckets {\n if pair != nil {\n fmt.Println(pair.key, \"-&gt;\", pair.val)\n }\n }\n}\n</code></pre> array_hash_map.swift<pre><code>/* 키-값 쌍 */\nclass Pair: Equatable {\n public var key: Int\n public var val: String\n\n public init(key: Int, val: String) {\n self.key = key\n self.val = val\n }\n\n public static func == (lhs: Pair, rhs: Pair) -&gt; Bool {\n lhs.key == rhs.key &amp;&amp; lhs.val == rhs.val\n }\n}\n\n/* 배열로 구현한 해시 테이블 */\nclass ArrayHashMap {\n private var buckets: [Pair?]\n\n init() {\n // 버킷 100개를 포함하는 배열을 초기화한다\n buckets = Array(repeating: nil, count: 100)\n }\n\n /* 해시 함수 */\n private func hashFunc(key: Int) -&gt; Int {\n let index = key % 100\n return index\n }\n\n /* 조회 연산 */\n func get(key: Int) -&gt; String? {\n let index = hashFunc(key: key)\n let pair = buckets[index]\n return pair?.val\n }\n\n /* 추가 연산 */\n func put(key: Int, val: String) {\n let pair = Pair(key: key, val: val)\n let index = hashFunc(key: key)\n buckets[index] = pair\n }\n\n /* 삭제 연산 */\n func remove(key: Int) {\n let index = hashFunc(key: key)\n // nil로 설정하여 삭제를 나타낸다\n buckets[index] = nil\n }\n\n /* 모든 키-값 쌍 반환 */\n func pairSet() -&gt; [Pair] {\n buckets.compactMap { $0 }\n }\n\n /* 모든 키 반환 */\n func keySet() -&gt; [Int] {\n buckets.compactMap { $0?.key }\n }\n\n /* 모든 값 반환 */\n func valueSet() -&gt; [String] {\n buckets.compactMap { $0?.val }\n }\n\n /* 해시 테이블 출력 */\n func print() {\n for pair in pairSet() {\n Swift.print(\"\\(pair.key) -&gt; \\(pair.val)\")\n }\n }\n}\n</code></pre> array_hash_map.js<pre><code>/* 키-값 쌍 Number -&gt; String */\nclass Pair {\n constructor(key, val) {\n this.key = key;\n this.val = val;\n }\n}\n\n/* 배열로 구현한 해시 테이블 */\nclass ArrayHashMap {\n #buckets;\n constructor() {\n // 버킷 100개를 포함하는 배열을 초기화한다\n this.#buckets = new Array(100).fill(null);\n }\n\n /* 해시 함수 */\n #hashFunc(key) {\n return key % 100;\n }\n\n /* 조회 연산 */\n get(key) {\n let index = this.#hashFunc(key);\n let pair = this.#buckets[index];\n if (pair === null) return null;\n return pair.val;\n }\n\n /* 추가 연산 */\n set(key, val) {\n let index = this.#hashFunc(key);\n this.#buckets[index] = new Pair(key, val);\n }\n\n /* 삭제 연산 */\n delete(key) {\n let index = this.#hashFunc(key);\n // null로 설정하여 삭제를 나타낸다\n this.#buckets[index] = null;\n }\n\n /* 모든 키-값 쌍 반환 */\n entries() {\n let arr = [];\n for (let i = 0; i &lt; this.#buckets.length; i++) {\n if (this.#buckets[i]) {\n arr.push(this.#buckets[i]);\n }\n }\n return arr;\n }\n\n /* 모든 키 반환 */\n keys() {\n let arr = [];\n for (let i = 0; i &lt; this.#buckets.length; i++) {\n if (this.#buckets[i]) {\n arr.push(this.#buckets[i].key);\n }\n }\n return arr;\n }\n\n /* 모든 값 반환 */\n values() {\n let arr = [];\n for (let i = 0; i &lt; this.#buckets.length; i++) {\n if (this.#buckets[i]) {\n arr.push(this.#buckets[i].val);\n }\n }\n return arr;\n }\n\n /* 해시 테이블 출력 */\n print() {\n let pairSet = this.entries();\n for (const pair of pairSet) {\n console.info(`${pair.key} -&gt; ${pair.val}`);\n }\n }\n}\n</code></pre> array_hash_map.ts<pre><code>/* 키-값 쌍 Number -&gt; String */\nclass Pair {\n public key: number;\n public val: string;\n\n constructor(key: number, val: string) {\n this.key = key;\n this.val = val;\n }\n}\n\n/* 배열로 구현한 해시 테이블 */\nclass ArrayHashMap {\n private readonly buckets: (Pair | null)[];\n\n constructor() {\n // 버킷 100개를 포함하는 배열을 초기화한다\n this.buckets = new Array(100).fill(null);\n }\n\n /* 해시 함수 */\n private hashFunc(key: number): number {\n return key % 100;\n }\n\n /* 조회 연산 */\n public get(key: number): string | null {\n let index = this.hashFunc(key);\n let pair = this.buckets[index];\n if (pair === null) return null;\n return pair.val;\n }\n\n /* 추가 연산 */\n public set(key: number, val: string) {\n let index = this.hashFunc(key);\n this.buckets[index] = new Pair(key, val);\n }\n\n /* 삭제 연산 */\n public delete(key: number) {\n let index = this.hashFunc(key);\n // null로 설정하여 삭제를 나타낸다\n this.buckets[index] = null;\n }\n\n /* 모든 키-값 쌍 반환 */\n public entries(): (Pair | null)[] {\n let arr: (Pair | null)[] = [];\n for (let i = 0; i &lt; this.buckets.length; i++) {\n if (this.buckets[i]) {\n arr.push(this.buckets[i]);\n }\n }\n return arr;\n }\n\n /* 모든 키 반환 */\n public keys(): (number | undefined)[] {\n let arr: (number | undefined)[] = [];\n for (let i = 0; i &lt; this.buckets.length; i++) {\n if (this.buckets[i]) {\n arr.push(this.buckets[i].key);\n }\n }\n return arr;\n }\n\n /* 모든 값 반환 */\n public values(): (string | undefined)[] {\n let arr: (string | undefined)[] = [];\n for (let i = 0; i &lt; this.buckets.length; i++) {\n if (this.buckets[i]) {\n arr.push(this.buckets[i].val);\n }\n }\n return arr;\n }\n\n /* 해시 테이블 출력 */\n public print() {\n let pairSet = this.entries();\n for (const pair of pairSet) {\n console.info(`${pair.key} -&gt; ${pair.val}`);\n }\n }\n}\n</code></pre> array_hash_map.dart<pre><code>/* 키-값 쌍 */\nclass Pair {\n int key;\n String val;\n Pair(this.key, this.val);\n}\n\n/* 배열로 구현한 해시 테이블 */\nclass ArrayHashMap {\n late List&lt;Pair?&gt; _buckets;\n\n ArrayHashMap() {\n // 버킷 100개를 포함하는 배열을 초기화한다\n _buckets = List.filled(100, null);\n }\n\n /* 해시 함수 */\n int _hashFunc(int key) {\n final int index = key % 100;\n return index;\n }\n\n /* 조회 연산 */\n String? get(int key) {\n final int index = _hashFunc(key);\n final Pair? pair = _buckets[index];\n if (pair == null) {\n return null;\n }\n return pair.val;\n }\n\n /* 추가 연산 */\n void put(int key, String val) {\n final Pair pair = Pair(key, val);\n final int index = _hashFunc(key);\n _buckets[index] = pair;\n }\n\n /* 삭제 연산 */\n void remove(int key) {\n final int index = _hashFunc(key);\n _buckets[index] = null;\n }\n\n /* 모든 키-값 쌍 반환 */\n List&lt;Pair&gt; pairSet() {\n List&lt;Pair&gt; pairSet = [];\n for (final Pair? pair in _buckets) {\n if (pair != null) {\n pairSet.add(pair);\n }\n }\n return pairSet;\n }\n\n /* 모든 키 반환 */\n List&lt;int&gt; keySet() {\n List&lt;int&gt; keySet = [];\n for (final Pair? pair in _buckets) {\n if (pair != null) {\n keySet.add(pair.key);\n }\n }\n return keySet;\n }\n\n /* 모든 값 반환 */\n List&lt;String&gt; values() {\n List&lt;String&gt; valueSet = [];\n for (final Pair? pair in _buckets) {\n if (pair != null) {\n valueSet.add(pair.val);\n }\n }\n return valueSet;\n }\n\n /* 해시 테이블 출력 */\n void printHashMap() {\n for (final Pair kv in pairSet()) {\n print(\"${kv.key} -&gt; ${kv.val}\");\n }\n }\n}\n</code></pre> array_hash_map.rs<pre><code>/* 키-값 쌍 */\n#[derive(Debug, Clone, PartialEq)]\npub struct Pair {\n pub key: i32,\n pub val: String,\n}\n\n/* 배열로 구현한 해시 테이블 */\npub struct ArrayHashMap {\n buckets: Vec&lt;Option&lt;Pair&gt;&gt;,\n}\n\nimpl ArrayHashMap {\n pub fn new() -&gt; ArrayHashMap {\n // 버킷 100개를 포함하는 배열을 초기화한다\n Self {\n buckets: vec![None; 100],\n }\n }\n\n /* 해시 함수 */\n fn hash_func(&amp;self, key: i32) -&gt; usize {\n key as usize % 100\n }\n\n /* 조회 연산 */\n pub fn get(&amp;self, key: i32) -&gt; Option&lt;&amp;String&gt; {\n let index = self.hash_func(key);\n self.buckets[index].as_ref().map(|pair| &amp;pair.val)\n }\n\n /* 추가 연산 */\n pub fn put(&amp;mut self, key: i32, val: &amp;str) {\n let index = self.hash_func(key);\n self.buckets[index] = Some(Pair {\n key,\n val: val.to_string(),\n });\n }\n\n /* 삭제 연산 */\n pub fn remove(&amp;mut self, key: i32) {\n let index = self.hash_func(key);\n // None으로 설정하여 삭제를 나타낸다\n self.buckets[index] = None;\n }\n\n /* 모든 키-값 쌍 반환 */\n pub fn entry_set(&amp;self) -&gt; Vec&lt;&amp;Pair&gt; {\n self.buckets\n .iter()\n .filter_map(|pair| pair.as_ref())\n .collect()\n }\n\n /* 모든 키 반환 */\n pub fn key_set(&amp;self) -&gt; Vec&lt;&amp;i32&gt; {\n self.buckets\n .iter()\n .filter_map(|pair| pair.as_ref().map(|pair| &amp;pair.key))\n .collect()\n }\n\n /* 모든 값 반환 */\n pub fn value_set(&amp;self) -&gt; Vec&lt;&amp;String&gt; {\n self.buckets\n .iter()\n .filter_map(|pair| pair.as_ref().map(|pair| &amp;pair.val))\n .collect()\n }\n\n /* 해시 테이블 출력 */\n pub fn print(&amp;self) {\n for pair in self.entry_set() {\n println!(\"{} -&gt; {}\", pair.key, pair.val);\n }\n }\n}\n</code></pre> array_hash_map.c<pre><code>/* 키-값 쌍 int-&gt;string */\ntypedef struct {\n int key;\n char *val;\n} Pair;\n\n/* 배열로 구현한 해시 테이블 */\ntypedef struct {\n Pair *buckets[MAX_SIZE];\n} ArrayHashMap;\n\n/* 생성자 */\nArrayHashMap *newArrayHashMap() {\n ArrayHashMap *hmap = malloc(sizeof(ArrayHashMap));\n for (int i=0; i &lt; MAX_SIZE; i++) {\n hmap-&gt;buckets[i] = NULL;\n }\n return hmap;\n}\n\n/* 소멸자 */\nvoid delArrayHashMap(ArrayHashMap *hmap) {\n for (int i = 0; i &lt; MAX_SIZE; i++) {\n if (hmap-&gt;buckets[i] != NULL) {\n free(hmap-&gt;buckets[i]-&gt;val);\n free(hmap-&gt;buckets[i]);\n }\n }\n free(hmap);\n}\n\n/* 추가 연산 */\nvoid put(ArrayHashMap *hmap, const int key, const char *val) {\n Pair *Pair = malloc(sizeof(Pair));\n Pair-&gt;key = key;\n Pair-&gt;val = malloc(strlen(val) + 1);\n strcpy(Pair-&gt;val, val);\n\n int index = hashFunc(key);\n hmap-&gt;buckets[index] = Pair;\n}\n\n/* 삭제 연산 */\nvoid removeItem(ArrayHashMap *hmap, const int key) {\n int index = hashFunc(key);\n free(hmap-&gt;buckets[index]-&gt;val);\n free(hmap-&gt;buckets[index]);\n hmap-&gt;buckets[index] = NULL;\n}\n\n/* 모든 키-값 쌍 반환 */\nvoid pairSet(ArrayHashMap *hmap, MapSet *set) {\n Pair *entries;\n int i = 0, index = 0;\n int total = 0;\n /* 유효한 키-값 쌍의 개수를 센다 */\n for (i = 0; i &lt; MAX_SIZE; i++) {\n if (hmap-&gt;buckets[i] != NULL) {\n total++;\n }\n }\n entries = malloc(sizeof(Pair) * total);\n for (i = 0; i &lt; MAX_SIZE; i++) {\n if (hmap-&gt;buckets[i] != NULL) {\n entries[index].key = hmap-&gt;buckets[i]-&gt;key;\n entries[index].val = malloc(strlen(hmap-&gt;buckets[i]-&gt;val) + 1);\n strcpy(entries[index].val, hmap-&gt;buckets[i]-&gt;val);\n index++;\n }\n }\n set-&gt;set = entries;\n set-&gt;len = total;\n}\n\n/* 모든 키 반환 */\nvoid keySet(ArrayHashMap *hmap, MapSet *set) {\n int *keys;\n int i = 0, index = 0;\n int total = 0;\n /* 유효한 키-값 쌍의 개수를 센다 */\n for (i = 0; i &lt; MAX_SIZE; i++) {\n if (hmap-&gt;buckets[i] != NULL) {\n total++;\n }\n }\n keys = malloc(total * sizeof(int));\n for (i = 0; i &lt; MAX_SIZE; i++) {\n if (hmap-&gt;buckets[i] != NULL) {\n keys[index] = hmap-&gt;buckets[i]-&gt;key;\n index++;\n }\n }\n set-&gt;set = keys;\n set-&gt;len = total;\n}\n\n/* 모든 값 반환 */\nvoid valueSet(ArrayHashMap *hmap, MapSet *set) {\n char **vals;\n int i = 0, index = 0;\n int total = 0;\n /* 유효한 키-값 쌍의 개수를 센다 */\n for (i = 0; i &lt; MAX_SIZE; i++) {\n if (hmap-&gt;buckets[i] != NULL) {\n total++;\n }\n }\n vals = malloc(total * sizeof(char *));\n for (i = 0; i &lt; MAX_SIZE; i++) {\n if (hmap-&gt;buckets[i] != NULL) {\n vals[index] = hmap-&gt;buckets[i]-&gt;val;\n index++;\n }\n }\n set-&gt;set = vals;\n set-&gt;len = total;\n}\n\n/* 해시 테이블 출력 */\nvoid print(ArrayHashMap *hmap) {\n int i;\n MapSet set;\n pairSet(hmap, &amp;set);\n Pair *entries = (Pair *)set.set;\n for (i = 0; i &lt; set.len; i++) {\n printf(\"%d -&gt; %s\\n\", entries[i].key, entries[i].val);\n }\n free(set.set);\n}\n</code></pre> array_hash_map.kt<pre><code>/* 키-값 쌍 */\nclass Pair(\n var key: Int,\n var _val: String\n)\n\n/* 배열로 구현한 해시 테이블 */\nclass ArrayHashMap {\n // 버킷 100개를 포함하는 배열을 초기화한다\n private val buckets = arrayOfNulls&lt;Pair&gt;(100)\n\n /* 해시 함수 */\n fun hashFunc(key: Int): Int {\n val index = key % 100\n return index\n }\n\n /* 조회 연산 */\n fun get(key: Int): String? {\n val index = hashFunc(key)\n val pair = buckets[index] ?: return null\n return pair._val\n }\n\n /* 추가 연산 */\n fun put(key: Int, _val: String) {\n val pair = Pair(key, _val)\n val index = hashFunc(key)\n buckets[index] = pair\n }\n\n /* 삭제 연산 */\n fun remove(key: Int) {\n val index = hashFunc(key)\n // null로 설정하여 삭제를 나타낸다\n buckets[index] = null\n }\n\n /* 모든 키-값 쌍 반환 */\n fun pairSet(): MutableList&lt;Pair&gt; {\n val pairSet = mutableListOf&lt;Pair&gt;()\n for (pair in buckets) {\n if (pair != null)\n pairSet.add(pair)\n }\n return pairSet\n }\n\n /* 모든 키 반환 */\n fun keySet(): MutableList&lt;Int&gt; {\n val keySet = mutableListOf&lt;Int&gt;()\n for (pair in buckets) {\n if (pair != null)\n keySet.add(pair.key)\n }\n return keySet\n }\n\n /* 모든 값 반환 */\n fun valueSet(): MutableList&lt;String&gt; {\n val valueSet = mutableListOf&lt;String&gt;()\n for (pair in buckets) {\n if (pair != null)\n valueSet.add(pair._val)\n }\n return valueSet\n }\n\n /* 해시 테이블 출력 */\n fun print() {\n for (kv in pairSet()) {\n val key = kv.key\n val _val = kv._val\n println(\"$key -&gt; $_val\")\n }\n }\n}\n</code></pre> array_hash_map.rb<pre><code>### 키-값 쌍 ###\nclass Pair\n attr_accessor :key, :val\n\n def initialize(key, val)\n @key = key\n @val = val\n end\nend\n\n### 배열로 구현한 해시 테이블 ###\nclass ArrayHashMap\n ### 생성자 ###\n def initialize\n # 버킷 100개를 포함하는 배열을 초기화한다\n @buckets = Array.new(100)\n end\n\n ### 해시 함수 ###\n def hash_func(key)\n index = key % 100\n end\n\n ### 조회 연산 ###\n def get(key)\n index = hash_func(key)\n pair = @buckets[index]\n\n return if pair.nil?\n pair.val\n end\n\n ### 추가 연산 ###\n def put(key, val)\n pair = Pair.new(key, val)\n index = hash_func(key)\n @buckets[index] = pair\n end\n\n ### 삭제 연산 ###\n def remove(key)\n index = hash_func(key)\n # nil로 설정하여 삭제를 나타낸다\n @buckets[index] = nil\n end\n\n ### 모든 키-값 쌍 반환 ###\n def entry_set\n result = []\n @buckets.each { |pair| result &lt;&lt; pair unless pair.nil? }\n result\n end\n\n ### 모든 키 반환 ###\n def key_set\n result = []\n @buckets.each { |pair| result &lt;&lt; pair.key unless pair.nil? }\n result\n end\n\n ### 모든 값 반환 ###\n def value_set\n result = []\n @buckets.each { |pair| result &lt;&lt; pair.val unless pair.nil? }\n result\n end\n\n ### 해시 테이블 출력 ###\n def print\n @buckets.each { |pair| puts \"#{pair.key} -&gt; #{pair.val}\" unless pair.nil? }\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 6 장   해시 테이블","6.1   해시 테이블"],"tags":[]},{"location":"chapter_hashing/hash_map/#613","level":2,"title":"6.1.3   해시 충돌과 용량 확장","text":"<p>해시 함수는 본질적으로 모든 <code>key</code>로 이루어진 입력 공간을 배열의 모든 인덱스로 이루어진 출력 공간에 매핑한다. 입력 공간은 대개 출력 공간보다 훨씬 크므로 이론적으로 ‘여러 입력이 같은 출력에 대응하는’ 경우가 반드시 존재한다.</p> <p>앞의 해시 함수에서는 입력 <code>key</code>의 마지막 두 자리가 같으면 출력도 같다. 예를 들어 학번이 12836과 20336인 두 학생을 조회하면 다음과 같다.</p> <pre><code>12836 % 100 = 36\n20336 % 100 = 36\n</code></pre> <p>아래 그림(6-3)에서 두 학번이 같은 이름을 가리키는데, 이는 명백히 잘못되었다. 이처럼 여러 입력이 같은 출력에 대응하는 현상을 해시 충돌(hash collision)이라고 한다.</p> <p></p> <p> 그림 6-3   해시 충돌의 예 </p> <p>해시 테이블의 용량 \\(n\\)이 커질수록 여러 <code>key</code>가 같은 버킷에 들어갈 확률은 낮아지고 충돌도 줄어든다. 따라서 해시 테이블의 용량을 늘려 해시 충돌을 줄일 수 있다.</p> <p>아래 그림(6-4)에서 확장 전에는 키-값 쌍 <code>(136, A)</code>와 <code>(236, D)</code>가 충돌하지만 확장 후에는 충돌이 사라진다.</p> <p></p> <p> 그림 6-4   해시 테이블의 용량 확장 </p> <p>배열 확장과 마찬가지로 해시 테이블을 확장하려면 모든 키-값 쌍을 기존 테이블에서 새 테이블로 옮겨야 하므로 시간이 많이 든다. 또한 용량 <code>capacity</code>가 바뀌므로 해시 함수로 모든 키-값 쌍의 저장 위치를 다시 계산해야 하며, 이는 확장 비용을 더욱 늘린다. 따라서 프로그래밍 언어는 보통 충분히 큰 용량을 미리 확보하여 잦은 확장을 방지한다.</p> <p>적재율(load factor)은 해시 테이블의 원소 수를 버킷 수로 나눈 값으로, 해시 충돌의 심각도를 측정하는 중요한 개념이다. 용량 확장을 시작하는 조건으로도 자주 사용된다. 예를 들어 Java에서는 적재율이 \\(0.75\\)를 넘으면 해시 테이블의 용량을 기존의 \\(2\\)배로 늘린다.</p>","path":["제 6 장   해시 테이블","6.1   해시 테이블"],"tags":[]},{"location":"chapter_hashing/summary/","level":1,"title":"6.4   요약","text":"","path":["제 6 장   해시 테이블","6.4   요약"],"tags":[]},{"location":"chapter_hashing/summary/#1","level":3,"title":"1.   핵심 정리","text":"<ul> <li>해시 테이블은 <code>key</code>를 입력받아 \\(O(1)\\) 시간에 <code>value</code>를 조회하므로 매우 효율적이다.</li> <li>대표적인 연산에는 조회, 키-값 쌍 추가와 삭제, 해시 테이블 순회 등이 있다.</li> <li>해시 함수는 <code>key</code>를 배열 인덱스로 매핑하여 해당 버킷에 접근하고 <code>value</code>를 얻도록 한다.</li> <li>서로 다른 두 <code>key</code>가 해시 함수를 거쳐 같은 배열 인덱스를 얻으면 잘못된 조회 결과가 발생할 수 있다. 이를 해시 충돌이라고 한다.</li> <li>해시 테이블 용량이 클수록 충돌 확률이 낮으므로 용량 확장으로 충돌을 완화할 수 있다. 배열과 마찬가지로 해시 테이블의 용량 확장 비용도 크다.</li> <li>적재율은 원소 수를 버킷 수로 나눈 값이며, 해시 충돌의 심각도를 반영한다. 용량 확장의 시작 조건으로 자주 사용된다.</li> <li>체이닝은 단일 원소를 연결 리스트로 바꾸어 충돌하는 모든 원소를 같은 리스트에 저장한다. 리스트가 너무 길면 조회 효율이 낮아지므로, 레드-블랙 트리로 바꾸어 효율을 높일 수 있다.</li> <li>개방 주소법은 여러 차례 탐사하여 해시 충돌을 처리한다. 선형 탐사는 고정 간격을 사용하며, 원소를 삭제할 수 없고 군집이 생기기 쉽다는 단점이 있다. 다중 해싱은 여러 해시 함수로 탐사하므로 군집은 덜 생기지만 계산량이 늘어난다.</li> <li>프로그래밍 언어마다 해시 테이블 구현이 다르다. 예를 들어 Java의 <code>HashMap</code>은 체이닝을, Python의 <code>dict</code>는 개방 주소법을 사용한다.</li> <li>해시 테이블의 해시 알고리즘에는 결정성, 높은 효율, 균등 분포가 필요하다. 암호학에서는 충돌 저항성과 눈사태 효과도 갖추어야 한다.</li> <li>해시 알고리즘은 해시값의 균등 분포를 최대한 보장하고 충돌을 줄이기 위해 보통 큰 소수를 모듈러 연산의 법으로 사용한다.</li> <li>대표적인 해시 알고리즘으로 MD5, SHA-1, SHA-2, SHA-3 등이 있다. MD5는 파일 무결성 검사에, SHA-2는 보안 응용과 프로토콜에 자주 쓰인다.</li> <li>프로그래밍 언어는 보통 자료형별 내장 해시 알고리즘을 제공하여 버킷 인덱스를 계산한다. 일반적으로 불변 객체만 해시 가능하다.</li> </ul>","path":["제 6 장   해시 테이블","6.4   요약"],"tags":[]},{"location":"chapter_hashing/summary/#2-q-a","level":3,"title":"2.   Q &amp; A","text":"<p>Q: 해시 테이블의 시간 복잡도가 \\(O(n)\\)이 되는 경우는 언제인가?</p> <p>해시 충돌이 심하면 시간 복잡도가 \\(O(n)\\)으로 나빠진다. 해시 함수를 잘 설계하고 용량을 적절히 정하며 충돌이 비교적 고르게 분포하면 \\(O(1)\\)이다. 프로그래밍 언어의 내장 해시 테이블을 사용할 때는 보통 시간 복잡도를 \\(O(1)\\)로 본다.</p> <p>Q: 해시 함수 \\(f(x) = x\\)를 사용하면 충돌이 없을 텐데, 왜 그렇게 하지 않는가?</p> <p>\\(f(x) = x\\)에서는 각 원소에 고유한 버킷 인덱스가 대응하므로 배열과 같다. 그러나 입력 공간은 보통 출력 공간(배열 길이)보다 훨씬 크므로, 해시 함수의 마지막 단계에서 대개 배열 길이로 나눈 나머지를 구한다. 즉, 해시 테이블의 목적은 큰 상태 공간을 작은 공간에 매핑하면서 \\(O(1)\\)의 조회 효율을 제공하는 것이다.</p> <p>Q: 해시 테이블도 배열, 연결 리스트, 이진 트리로 구현되는데 어떻게 이들보다 효율이 높을 수 있는가?</p> <p>첫째, 시간 효율은 높아지지만 공간 효율은 낮아진다. 해시 테이블에는 사용하지 않는 메모리가 상당 부분 있다.</p> <p>둘째, 특정 활용 상황에서만 시간 효율이 높아진다. 같은 기능을 배열이나 연결 리스트로 같은 시간 복잡도에 구현할 수 있다면 보통 그쪽이 더 빠르다. 해시 함수 계산에 비용이 들어 시간 복잡도의 상수항이 더 크기 때문이다.</p> <p>마지막으로 해시 테이블의 시간 복잡도도 나빠질 수 있다. 예를 들어 체이닝에서는 연결 리스트나 레드-블랙 트리에서 탐색하므로, 여전히 \\(O(n)\\) 시간으로 나빠질 위험이 있다.</p> <p>Q: 다중 해싱도 원소를 곧바로 삭제할 수 없는가? 삭제 표시를 한 공간은 다시 사용할 수 있는가?</p> <p>다중 해싱은 개방 주소법의 한 종류다. 개방 주소법은 모두 원소를 곧바로 삭제할 수 없으므로 삭제 표시를 해야 한다. 표시된 공간은 다시 사용할 수 있다. 새 원소를 삽입할 때 해시 함수로 삭제 표시가 있는 위치를 찾으면 그곳에 새 원소를 넣을 수 있다. 이렇게 하면 탐사 순서를 유지하면서 공간 활용률도 보장한다.</p> <p>Q: 선형 탐사에서는 원소를 찾을 때도 왜 해시 충돌이 발생하는가?</p> <p>해시 함수로 해당 버킷과 키-값 쌍을 찾았는데 <code>key</code>가 일치하지 않으면 해시 충돌이 있다는 뜻이다. 따라서 미리 정한 간격으로 뒤를 차례로 탐색하다가 올바른 키-값 쌍을 찾거나, 찾을 수 없다고 판단하면 종료한다.</p> <p>Q: 해시 테이블의 용량 확장은 왜 해시 충돌을 완화하는가?</p> <p>해시 함수는 마지막에 대개 배열 길이 \\(n\\)으로 나눈 나머지를 구하여 출력이 인덱스 범위에 들어가도록 한다. 용량을 늘리면 \\(n\\)이 바뀌고 <code>key</code>에 대응하는 인덱스도 달라질 수 있다. 원래 같은 버킷에 있던 여러 <code>key</code>가 여러 버킷으로 분산될 수 있으므로 충돌이 완화된다.</p> <p>Q: 효율적으로 저장하고 접근하려는 목적이라면 배열을 바로 사용하면 되지 않는가?</p> <p><code>key</code>가 좁은 범위의 연속된 정수라면 배열을 바로 쓰는 것이 간단하고 효율적이다. 하지만 문자열처럼 다른 자료형이면 해시 함수로 <code>key</code>를 배열 인덱스에 매핑하고 버킷 배열에 원소를 저장해야 한다. 이것이 바로 해시 테이블이다.</p>","path":["제 6 장   해시 테이블","6.4   요약"],"tags":[]},{"location":"chapter_heap/","level":1,"title":"제 8 장   힙","text":"<p>이 장을 시작하며</p> <p>힙은 겹겹이 이어진 산봉우리처럼 높낮이도 모습도 제각각이다.</p> <p>봉우리마다 높이가 다르지만, 가장 높은 봉우리는 언제나 가장 먼저 눈에 들어온다.</p>","path":["제 8 장   힙"],"tags":[]},{"location":"chapter_heap/#_1","level":2,"title":"이 장의 내용","text":"<ul> <li>8.1   힙</li> <li>8.2   힙 구성 연산</li> <li>8.3   Top-k 문제</li> <li>8.4   요약</li> <li>8.5   연습 문제</li> </ul>","path":["제 8 장   힙"],"tags":[]},{"location":"chapter_heap/build_heap/","level":1,"title":"8.2   힙 구성 연산","text":"<p>어떤 상황에서는 리스트의 모든 원소를 사용해 힙을 구성하고 싶을 수 있다. 이 과정을 '힙 구성 연산'이라고 한다.</p>","path":["제 8 장   힙","8.2   힙 구성 연산"],"tags":[]},{"location":"chapter_heap/build_heap/#821","level":2,"title":"8.2.1   힙 삽입 연산을 이용한 구현","text":"<p>먼저 빈 힙을 만든 다음, 리스트를 순회하면서 각 원소에 대해 차례로 '힙 삽입 연산'을 수행한다. 즉, 원소를 힙의 끝에 추가한 뒤 해당 원소부터 '아래에서 위로' 힙을 재정렬한다.</p> <p>원소 하나를 힙에 삽입할 때마다 힙의 길이가 1씩 늘어난다. 이진 트리에 위에서 아래 순서로 노드를 추가하므로 힙은 '위에서 아래로' 구성된다.</p> <p>원소의 개수를 \\(n\\)이라고 하자. 각 원소의 힙 삽입 연산에는 \\(O(\\log{n})\\) 시간이 걸리므로, 이 힙 구성 방법의 시간 복잡도는 \\(O(n \\log n)\\)이다.</p>","path":["제 8 장   힙","8.2   힙 구성 연산"],"tags":[]},{"location":"chapter_heap/build_heap/#822","level":2,"title":"8.2.2   순회하며 힙을 재정렬하는 구현","text":"<p>실제로는 다음 두 단계로 더 효율적인 힙 구성 방법을 구현할 수 있다.</p> <ol> <li>리스트의 모든 원소를 그대로 힙에 추가한다. 이 시점에서는 아직 힙의 성질을 만족하지 않는다.</li> <li>힙을 역순으로 순회하면서(레벨 순서 순회의 역순), 각 내부 노드에 대해 차례로 '위에서 아래로 힙 재정렬'을 수행한다.</li> </ol> <p>노드 하나를 재정렬할 때마다 그 노드를 루트로 하는 서브트리가 올바른 부분 힙이 된다. 역순으로 순회하므로 힙은 '아래에서 위로' 구성된다.</p> <p>역순으로 순회하는 이유는 현재 노드 아래의 서브트리들이 이미 올바른 부분 힙임을 보장하기 위해서다. 그래야 현재 노드를 재정렬하는 작업이 유효해진다.</p> <p>리프 노드는 자식 노드가 없으므로 그 자체로 올바른 부분 힙이며, 재정렬할 필요가 없다. 아래 코드와 같이 마지막 내부 노드는 마지막 노드의 부모 노드다. 여기서부터 역순으로 순회하며 힙 재정렬을 수행한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py<pre><code>def __init__(self, nums: list[int]):\n \"\"\"생성자: 입력 리스트를 바탕으로 힙 구성\"\"\"\n # 리스트 원소를 그대로 힙에 추가한다\n self.max_heap = nums\n # 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n for i in range(self.parent(self.size() - 1), -1, -1):\n self.sift_down(i)\n</code></pre> my_heap.cpp<pre><code>/* 생성자: 입력 리스트를 바탕으로 힙 구성 */\nMaxHeap(vector&lt;int&gt; nums) {\n // 리스트 원소를 그대로 힙에 추가한다\n maxHeap = nums;\n // 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n for (int i = parent(size() - 1); i &gt;= 0; i--) {\n siftDown(i);\n }\n}\n</code></pre> my_heap.java<pre><code>/* 생성자: 입력 리스트를 바탕으로 힙 구성 */\nMaxHeap(List&lt;Integer&gt; nums) {\n // 리스트 원소를 그대로 힙에 추가한다\n maxHeap = new ArrayList&lt;&gt;(nums);\n // 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n for (int i = parent(size() - 1); i &gt;= 0; i--) {\n siftDown(i);\n }\n}\n</code></pre> my_heap.cs<pre><code>/* 생성자: 입력 리스트로 힙 구성 */\nMaxHeap(IEnumerable&lt;int&gt; nums) {\n // 리스트 원소를 그대로 힙에 추가한다\n maxHeap = new List&lt;int&gt;(nums);\n // 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n var size = Parent(this.Size() - 1);\n for (int i = size; i &gt;= 0; i--) {\n SiftDown(i);\n }\n}\n</code></pre> my_heap.go<pre><code>/* 생성자: 슬라이스를 바탕으로 힙 구성 */\nfunc newMaxHeap(nums []any) *maxHeap {\n // 리스트 원소를 그대로 힙에 추가한다\n h := &amp;maxHeap{data: nums}\n for i := h.parent(len(h.data) - 1); i &gt;= 0; i-- {\n // 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n h.siftDown(i)\n }\n return h\n}\n</code></pre> my_heap.swift<pre><code>/* 생성자: 입력 리스트를 바탕으로 힙 구성 */\ninit(nums: [Int]) {\n // 리스트 원소를 그대로 힙에 추가한다\n maxHeap = nums\n // 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n for i in (0 ... parent(i: size() - 1)).reversed() {\n siftDown(i: i)\n }\n}\n</code></pre> my_heap.js<pre><code>/* 생성자: 빈 힙을 만들거나 입력 리스트로 힙을 구성한다 */\nconstructor(nums) {\n // 리스트 원소를 그대로 힙에 추가한다\n this.#maxHeap = nums === undefined ? [] : [...nums];\n // 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n for (let i = this.#parent(this.size() - 1); i &gt;= 0; i--) {\n this.#siftDown(i);\n }\n}\n</code></pre> my_heap.ts<pre><code>/* 생성자: 빈 힙을 만들거나 입력 리스트로 힙을 구성한다 */\nconstructor(nums?: number[]) {\n // 리스트 원소를 그대로 힙에 추가한다\n this.maxHeap = nums === undefined ? [] : [...nums];\n // 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n for (let i = this.parent(this.size() - 1); i &gt;= 0; i--) {\n this.siftDown(i);\n }\n}\n</code></pre> my_heap.dart<pre><code>/* 생성자: 입력 리스트를 바탕으로 힙 구성 */\nMaxHeap(List&lt;int&gt; nums) {\n // 리스트 원소를 그대로 힙에 추가한다\n _maxHeap = nums;\n // 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n for (int i = _parent(size() - 1); i &gt;= 0; i--) {\n siftDown(i);\n }\n}\n</code></pre> my_heap.rs<pre><code>/* 생성자: 입력 리스트를 바탕으로 힙 구성 */\nfn new(nums: Vec&lt;i32&gt;) -&gt; Self {\n // 리스트 원소를 그대로 힙에 추가한다\n let mut heap = MaxHeap { max_heap: nums };\n // 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n for i in (0..=Self::parent(heap.size() - 1)).rev() {\n heap.sift_down(i);\n }\n heap\n}\n</code></pre> my_heap.c<pre><code>/* 생성자: 슬라이스를 바탕으로 힙 구성 */\nMaxHeap *newMaxHeap(int nums[], int size) {\n // 모든 원소를 힙에 삽입한다\n MaxHeap *maxHeap = (MaxHeap *)malloc(sizeof(MaxHeap));\n maxHeap-&gt;size = size;\n memcpy(maxHeap-&gt;data, nums, size * sizeof(int));\n for (int i = parent(maxHeap, size - 1); i &gt;= 0; i--) {\n // 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n siftDown(maxHeap, i);\n }\n return maxHeap;\n}\n</code></pre> my_heap.kt<pre><code>/* 최대 힙 */\nclass MaxHeap(nums: MutableList&lt;Int&gt;?) {\n // 배열 대신 리스트를 사용하므로 용량 확장을 고려할 필요가 없다\n private val maxHeap = mutableListOf&lt;Int&gt;()\n\n /* 생성자: 입력 리스트를 바탕으로 힙 구성 */\n init {\n // 리스트 원소를 그대로 힙에 추가한다\n maxHeap.addAll(nums!!)\n // 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n for (i in parent(size() - 1) downTo 0) {\n siftDown(i)\n }\n }\n\n /* 왼쪽 자식 노드의 인덱스 반환 */\n private fun left(i: Int): Int {\n return 2 * i + 1\n }\n\n /* 오른쪽 자식 노드의 인덱스 반환 */\n private fun right(i: Int): Int {\n return 2 * i + 2\n }\n\n /* 부모 노드의 인덱스 반환 */\n private fun parent(i: Int): Int {\n return (i - 1) / 2 // 나눗셈의 결과를 내림한다\n }\n\n /* 원소 교환 */\n private fun swap(i: Int, j: Int) {\n val temp = maxHeap[i]\n maxHeap[i] = maxHeap[j]\n maxHeap[j] = temp\n }\n\n /* 힙 크기 반환 */\n fun size(): Int {\n return maxHeap.size\n }\n\n /* 힙이 비어 있는지 판단 */\n fun isEmpty(): Boolean {\n /* 힙이 비어 있는지 판단 */\n return size() == 0\n }\n\n /* 힙의 루트 원소 접근 */\n fun peek(): Int {\n return maxHeap[0]\n }\n\n /* 힙에 원소 삽입 */\n fun push(_val: Int) {\n // 노드 추가\n maxHeap.add(_val)\n // 아래에서 위로 힙을 재정렬한다\n siftUp(size() - 1)\n }\n\n /* 노드 i에서 시작하여 아래에서 위로 힙 재정렬 */\n private fun siftUp(it: Int) {\n // Kotlin의 함수 매개변수는 변경할 수 없으므로 임시 변수를 생성한다\n var i = it\n while (true) {\n // 노드 i의 부모 노드를 구한다\n val p = parent(i)\n // 루트 노드를 넘어가거나 노드를 수정할 필요가 없으면 힙 재정렬을 마친다\n if (p &lt; 0 || maxHeap[i] &lt;= maxHeap[p]) break\n // 두 노드를 교환한다\n swap(i, p)\n // 위쪽으로 힙 재정렬을 반복한다\n i = p\n }\n }\n\n /* 힙에서 원소 꺼내기 */\n fun pop(): Int {\n // 비어 있는 경우를 처리한다\n if (isEmpty()) throw IndexOutOfBoundsException()\n // 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n swap(0, size() - 1)\n // 노드 삭제\n val _val = maxHeap.removeAt(size() - 1)\n // 위에서 아래로 힙을 재정렬한다\n siftDown(0)\n // 힙의 루트 원소를 반환한다\n return _val\n }\n\n /* 노드 i에서 시작하여 위에서 아래로 힙 재정렬 */\n private fun siftDown(it: Int) {\n // Kotlin의 함수 매개변수는 변경할 수 없으므로 임시 변수를 생성한다\n var i = it\n while (true) {\n // 노드 i, l, r 중 값이 가장 큰 노드를 찾아 ma로 표시한다\n val l = left(i)\n val r = right(i)\n var ma = i\n if (l &lt; size() &amp;&amp; maxHeap[l] &gt; maxHeap[ma]) ma = l\n if (r &lt; size() &amp;&amp; maxHeap[r] &gt; maxHeap[ma]) ma = r\n // 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n if (ma == i) break\n // 두 노드를 교환한다\n swap(i, ma)\n // 아래쪽으로 힙 재정렬을 반복한다\n i = ma\n }\n }\n\n /* 힙 출력(이진 트리) */\n fun print() {\n val queue = PriorityQueue { a: Int, b: Int -&gt; b - a }\n queue.addAll(maxHeap)\n printHeap(queue)\n }\n}\n</code></pre> my_heap.rb<pre><code>### 생성자: 입력 리스트를 바탕으로 힙 구성 ###\ndef initialize(nums)\n # 리스트 원소를 그대로 힙에 추가한다\n @max_heap = nums\n # 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n parent(size - 1).downto(0) do |i|\n sift_down(i)\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 8 장   힙","8.2   힙 구성 연산"],"tags":[]},{"location":"chapter_heap/build_heap/#823","level":2,"title":"8.2.3   복잡도 분석","text":"<p>이제 두 번째 힙 구성 방법의 시간 복잡도를 계산해 보자.</p> <ul> <li>완전 이진 트리의 노드 수가 \\(n\\)이라면, 리프 노드의 수는 \\((n + 1) / 2\\)이다. 여기서 \\(/\\)는 내림 정수 나눗셈이다. 따라서 재정렬해야 하는 노드의 수는 \\(n / 2\\)이다.</li> <li>위에서 아래로 힙을 재정렬할 때 각 노드는 최대 리프 노드까지 내려갈 수 있으므로, 최대 반복 횟수는 이진 트리의 높이인 \\(\\log n\\)이다.</li> </ul> <p>위의 두 값을 곱하면 힙 구성 과정의 시간 복잡도가 \\(O(n \\log n)\\)이 된다. 하지만 이 추정은 정확하지 않다. 이진 트리에서는 아래쪽 노드의 수가 위쪽 노드보다 훨씬 많다는 성질을 고려하지 않았기 때문이다.</p> <p>이제 더 정확하게 계산해 보자. 계산을 간단히 하기 위해 노드 수가 \\(n\\)이고 높이가 \\(h\\)인 '포화 이진 트리'가 주어졌다고 가정한다. 이 가정은 계산 결과의 정확성에 영향을 주지 않는다.</p> <p></p> <p> 그림 8-5   포화 이진 트리의 레벨별 노드 수 </p> <p>위 그림(8-5)에서 노드를 '위에서 아래로 재정렬'하는 최대 반복 횟수는 그 노드에서 리프 노드까지의 거리와 같으며, 이 거리는 바로 '노드의 높이'다. 따라서 각 레벨의 '노드 수 \\(\\times\\) 노드 높이'를 더하면 모든 노드에 대한 힙 재정렬의 총반복 횟수를 구할 수 있다.</p> \\[ T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \\dots + 2^{(h-1)}\\times1 \\] <p>위 식을 정리하려면 중등 수학의 수열 지식이 필요하다. 먼저 \\(T(h)\\)에 \\(2\\)를 곱하면 다음과 같다.</p> \\[ \\begin{aligned} T(h) &amp; = 2^0h + 2^1(h-1) + 2^2(h-2) + \\dots + 2^{h-1}\\times1 \\newline 2 T(h) &amp; = 2^1h + 2^2(h-1) + 2^3(h-2) + \\dots + 2^{h}\\times1 \\newline \\end{aligned} \\] <p>두 식의 항을 한 칸씩 어긋나게 놓고 아래 식 \\(2 T(h)\\)에서 위 식 \\(T(h)\\)를 빼면 다음을 얻는다.</p> \\[ 2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \\dots + 2^{h-1} + 2^h \\] <p>위 식에서 \\(T(h)\\)를 이루는 항들이 등비수열을 이루므로 합 공식을 바로 적용하여 시간 복잡도를 구할 수 있다.</p> \\[ \\begin{aligned} T(h) &amp; = 2 \\frac{1 - 2^h}{1 - 2} - h \\newline &amp; = 2^{h+1} - h - 2 \\newline &amp; = O(2^h) \\end{aligned} \\] <p>또한 높이가 \\(h\\)인 포화 이진 트리의 노드 수는 \\(n = 2^{h+1} - 1\\)이므로, 복잡도가 \\(O(2^h) = O(n)\\)임을 쉽게 알 수 있다. 위 계산은 리스트를 입력받아 힙을 구성하는 시간 복잡도가 \\(O(n)\\)으로 매우 효율적임을 보여 준다.</p>","path":["제 8 장   힙","8.2   힙 구성 연산"],"tags":[]},{"location":"chapter_heap/exercises/","level":1,"title":"8.5   연습 문제","text":"","path":["제 8 장   힙","8.5   연습 문제"],"tags":[]},{"location":"chapter_heap/exercises/#851","level":2,"title":"8.5.1   개념 확인","text":"","path":["제 8 장   힙","8.5   연습 문제"],"tags":[]},{"location":"chapter_heap/exercises/#1-10","level":3,"title":"1.   10을 힙에 넣은 뒤 어떻게 조정하는가","text":"<p>배열 <code>[9, 7, 8, 3, 5]</code>는 최대 힙을 나타낸다. 여기에 숫자 10을 삽입한다.</p> <ol> <li>먼저 10을 배열 끝에 추가한다. 부모 노드의 값은 얼마인가?</li> <li>새 노드부터 위로 힙을 재정렬하며, 교환할 때마다 배열을 적어 보자.</li> <li>최종 루트 원소는 무엇이며, 총 몇 번 교환했는가?</li> </ol> 참고 답안 <ol> <li> <p>추가한 10의 인덱스는 5이고, 부모 인덱스는 \\(\\lfloor(5-1)/2\\rfloor=2\\)이므로 부모 노드의 값은 8이다.</p> </li> <li> <p>10이 8보다 크므로 첫 번째 교환 후에는 <code>[9, 7, 10, 3, 5, 8]</code>이 된다. 10은 부모 9보다도 크므로 두 번째 교환 후에는 <code>[10, 7, 9, 3, 5, 8]</code>이 된다. 10이 루트에 도달했으므로 힙 재정렬을 마친다.</p> </li> <li> <p>최종 루트 값은 10이며, 총 2번 교환했다.</p> </li> </ol>","path":["제 8 장   힙","8.5   연습 문제"],"tags":[]},{"location":"chapter_heap/exercises/#2","level":3,"title":"2.   최소 힙의 부모와 자식 관계 확인하기","text":"<p>배열 <code>[1, 4, 3, 7, 6, 2]</code>는 완전 이진 트리를 나타낸다. 최소 힙에서는 모든 부모의 값이 자식의 값보다 작거나 같아야 한다. 인덱스 \\(i\\)의 왼쪽과 오른쪽 자식 인덱스는 각각 \\(2i+1\\), \\(2i+2\\)다.</p> <ol> <li>인덱스 2의 자식 인덱스와 값은 각각 무엇인가?</li> <li>인덱스 2의 값은 3이다. 이 노드와 자식의 관계가 최소 힙 규칙에 어긋나는가? 그렇다면 어떤 두 원소를 교환해야 하는가?</li> <li>2번의 판단에 따라, 규칙에 어긋나면 교환 후 배열을 적고 그렇지 않으면 교환할 필요가 없음을 설명하라. 마지막으로 나머지 부모와 자식 관계도 확인하라.</li> </ol> 참고 답안 <ol> <li> <p>인덱스 2의 왼쪽 자식 인덱스는 5이고 값은 2다. 오른쪽 자식 인덱스는 6이지만 배열 길이가 6이므로 오른쪽 자식은 없다.</p> </li> <li> <p>부모의 값 3이 자식의 값 2보다 크므로 최소 힙 규칙에 어긋난다. 인덱스 2와 5의 원소를 교환해야 한다.</p> </li> <li> <p>교환 후 배열은 <code>[1, 4, 2, 7, 6, 3]</code>이다. 관계를 하나씩 확인하면 <code>1 ≤ 4</code>, <code>1 ≤ 2</code>, <code>4 ≤ 7</code>, <code>4 ≤ 6</code>, <code>2 ≤ 3</code>이다. 모든 부모의 값이 자식의 값보다 작거나 같으므로 최소 힙 규칙을 만족한다.</p> </li> </ol>","path":["제 8 장   힙","8.5   연습 문제"],"tags":[]},{"location":"chapter_heap/exercises/#3","level":3,"title":"3.   최소 힙으로 가장 큰 수 세 개 유지하기","text":"<p>데이터 스트림 <code>[4, 1, 7, 3, 8]</code>에서 가장 큰 수 3개를 유지하려면 크기가 3을 넘지 않는 최소 힙을 사용할 수 있다.</p> <p>먼저 앞의 3개 수를 차례대로 최소 힙에 넣는다. 힙이 가득 찬 뒤에는 새 수를 읽을 때마다 루트보다 크면 루트를 삭제하고 새 수를 넣는다. 그렇지 않으면 힙을 그대로 둔다.</p> <p>각 수를 읽은 뒤 힙에 남아 있는 수와 루트 값을 적어 보자. 남아 있는 수는 집합으로만 적으면 되며, 힙 배열에서의 순서는 적지 않아도 된다.</p> 참고 답안 <p>각 수를 읽은 뒤의 결과는 다음과 같다.</p> 읽은 수 남아 있는 수 루트 4 <code>{4}</code> 4 1 <code>{1, 4}</code> 1 7 <code>{1, 4, 7}</code> 1 3 <code>{3, 4, 7}</code> 3 8 <code>{4, 7, 8}</code> 4 <p>힙이 가득 차면 루트는 현재 남아 있는 수 중 가장 작은 수다. 새 수가 루트보다 클 때만 새 수로 루트를 대체한다. 최종적으로 남은 <code>{4, 7, 8}</code>이 바로 가장 큰 수 3개다.</p>","path":["제 8 장   힙","8.5   연습 문제"],"tags":[]},{"location":"chapter_heap/exercises/#852","level":2,"title":"8.5.2   프로그래밍 연습","text":"","path":["제 8 장   힙","8.5   연습 문제"],"tags":[]},{"location":"chapter_heap/exercises/#1-k","level":3,"title":"1.   배열에서 k번째로 큰 원소 찾기","text":"<p>정수 배열 <code>nums</code>와 정수 \\(k\\)가 주어진다. 여기서 \\(1 \\le k \\le n\\)이며 \\(n\\)은 배열 길이다. 배열을 큰 것부터 나열했을 때 \\(k\\)번째 위치의 원소를 반환하라.</p> <p>중복 원소도 각각 센다. 예를 들어 <code>[5, 5, 2]</code>에서 두 번째로 큰 원소 역시 5다. 크기가 \\(k\\)를 넘지 않는 최소 힙을 사용하여 해결하라.</p> 풀이 힌트 <ol> <li>k번째로 큰 원소는 가장 큰 k개 수 중 가장 작은 수다.</li> <li>각 수를 최소 힙에 넣고 크기가 k를 넘으면 최솟값을 꺼낸다.</li> <li>순회가 끝나면 힙에 가장 큰 k개 수가 남으며, 루트가 답이다.</li> </ol> <p>LeetCode</p>","path":["제 8 장   힙","8.5   연습 문제"],"tags":[]},{"location":"chapter_heap/heap/","level":1,"title":"8.1   힙","text":"<p>힙(heap)은 특정 조건을 만족하는 완전 이진 트리로, 아래 그림(8-1)과 같이 크게 두 종류로 나뉜다.</p> <ul> <li>최소 힙(min heap): 모든 노드의 값이 자식 노드의 값보다 작거나 같다(\\(\\leq\\)).</li> <li>최대 힙(max heap): 모든 노드의 값이 자식 노드의 값보다 크거나 같다(\\(\\geq\\)).</li> </ul> <p></p> <p> 그림 8-1   최소 힙과 최대 힙 </p> <p>힙은 완전 이진 트리의 특수한 형태이므로 다음 성질을 가진다.</p> <ul> <li>가장 아래 레벨은 왼쪽부터 노드가 채워지며, 나머지 레벨은 모두 가득 차 있다.</li> <li>이진 트리의 루트 노드를 '힙의 루트', 가장 아래 레벨에서 맨 오른쪽에 있는 노드를 '힙의 끝'이라고 한다.</li> <li>최대 힙(최소 힙)에서는 힙의 루트 원소(루트 노드)의 값이 가장 크다(작다).</li> </ul>","path":["제 8 장   힙","8.1   힙"],"tags":[]},{"location":"chapter_heap/heap/#811","level":2,"title":"8.1.1   힙의 주요 연산","text":"<p>많은 프로그래밍 언어에서 제공하는 것은 우선순위 큐(priority queue)라는 점을 알아 두자. 우선순위 큐는 우선순위에 따라 정렬되는 큐로 정의되는 추상 자료 구조다.</p> <p>실제로 힙은 보통 우선순위 큐를 구현하는 데 사용되며, 최대 힙은 원소를 큰 값부터 작은 값 순서로 꺼내는 우선순위 큐에 해당한다. 사용하는 입장에서는 '우선순위 큐'와 '힙'을 동등한 자료 구조로 볼 수 있다. 따라서 이 책에서는 둘을 특별히 구분하지 않고 모두 '힙'이라고 한다.</p> <p>힙의 주요 연산은 아래 표(8-1)와 같다. 메서드 이름은 프로그래밍 언어에 따라 달라진다.</p> <p> 표 8-1   힙 연산의 효율 </p> 메서드 이름 설명 시간 복잡도 <code>push()</code> 힙에 원소 삽입 \\(O(\\log n)\\) <code>pop()</code> 힙의 루트 원소 꺼내기 \\(O(\\log n)\\) <code>peek()</code> 힙의 루트 원소에 접근(최대 / 최소 힙에서는 각각 최댓값 / 최솟값) \\(O(1)\\) <code>size()</code> 힙의 원소 수 구하기 \\(O(1)\\) <code>isEmpty()</code> 힙이 비어 있는지 확인 \\(O(1)\\) <p>실제 응용에서는 프로그래밍 언어가 제공하는 힙 클래스(또는 우선순위 큐 클래스)를 바로 사용할 수 있다.</p> <p>정렬 알고리즘에서 '오름차순 정렬'과 '내림차순 정렬'을 선택하듯이, <code>flag</code>를 설정하거나 <code>Comparator</code>를 수정하여 '최소 힙'과 '최대 힙'을 전환할 수 있다. 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby heap.py<pre><code># 최소 힙 초기화\nmin_heap, flag = [], 1\n# 최대 힙 초기화\nmax_heap, flag = [], -1\n\n# Python의 heapq 모듈은 기본적으로 최소 힙을 구현한다\n# 원소의 부호를 바꾼 뒤 삽입하면 대소 관계가 뒤집히므로 최대 힙을 구현할 수 있다\n# 이 예제에서 flag = 1이면 최소 힙, flag = -1이면 최대 힙에 해당한다\n\n# 힙에 원소 삽입\nheapq.heappush(max_heap, flag * 1)\nheapq.heappush(max_heap, flag * 3)\nheapq.heappush(max_heap, flag * 2)\nheapq.heappush(max_heap, flag * 5)\nheapq.heappush(max_heap, flag * 4)\n\n# 힙의 루트 원소 구하기\npeek: int = flag * max_heap[0] # 5\n\n# 힙의 루트 원소 꺼내기\n# 꺼낸 원소들은 큰 값부터 작은 값 순서로 나열된다\nval = flag * heapq.heappop(max_heap) # 5\nval = flag * heapq.heappop(max_heap) # 4\nval = flag * heapq.heappop(max_heap) # 3\nval = flag * heapq.heappop(max_heap) # 2\nval = flag * heapq.heappop(max_heap) # 1\n\n# 힙의 크기 구하기\nsize: int = len(max_heap)\n\n# 힙이 비어 있는지 확인\nis_empty: bool = not max_heap\n\n# 리스트를 입력받아 힙 구성\nmin_heap: list[int] = [1, 3, 2, 5, 4]\nheapq.heapify(min_heap)\n</code></pre> heap.cpp<pre><code>/* 힙 초기화 */\n// 최소 힙 초기화\npriority_queue&lt;int, vector&lt;int&gt;, greater&lt;int&gt;&gt; minHeap;\n// 최대 힙 초기화\npriority_queue&lt;int, vector&lt;int&gt;, less&lt;int&gt;&gt; maxHeap;\n\n/* 힙에 원소 삽입 */\nmaxHeap.push(1);\nmaxHeap.push(3);\nmaxHeap.push(2);\nmaxHeap.push(5);\nmaxHeap.push(4);\n\n/* 힙의 루트 원소 구하기 */\nint peek = maxHeap.top(); // 5\n\n/* 힙의 루트 원소 꺼내기 */\n// 꺼낸 원소들은 큰 값부터 작은 값 순서로 나열된다\nmaxHeap.pop(); // 5\nmaxHeap.pop(); // 4\nmaxHeap.pop(); // 3\nmaxHeap.pop(); // 2\nmaxHeap.pop(); // 1\n\n/* 힙의 크기 구하기 */\nint size = maxHeap.size();\n\n/* 힙이 비어 있는지 확인 */\nbool isEmpty = maxHeap.empty();\n\n/* 리스트를 입력받아 힙 구성 */\nvector&lt;int&gt; input{1, 3, 2, 5, 4};\npriority_queue&lt;int, vector&lt;int&gt;, greater&lt;int&gt;&gt; minHeap(input.begin(), input.end());\n</code></pre> heap.java<pre><code>/* 힙 초기화 */\n// 최소 힙 초기화\nQueue&lt;Integer&gt; minHeap = new PriorityQueue&lt;&gt;();\n// 최대 힙 초기화(lambda 식으로 Comparator를 수정하면 된다)\nQueue&lt;Integer&gt; maxHeap = new PriorityQueue&lt;&gt;((a, b) -&gt; b - a);\n\n/* 힙에 원소 삽입 */\nmaxHeap.offer(1);\nmaxHeap.offer(3);\nmaxHeap.offer(2);\nmaxHeap.offer(5);\nmaxHeap.offer(4);\n\n/* 힙의 루트 원소 구하기 */\nint peek = maxHeap.peek(); // 5\n\n/* 힙의 루트 원소 꺼내기 */\n// 꺼낸 원소들은 큰 값부터 작은 값 순서로 나열된다\npeek = maxHeap.poll(); // 5\npeek = maxHeap.poll(); // 4\npeek = maxHeap.poll(); // 3\npeek = maxHeap.poll(); // 2\npeek = maxHeap.poll(); // 1\n\n/* 힙의 크기 구하기 */\nint size = maxHeap.size();\n\n/* 힙이 비어 있는지 확인 */\nboolean isEmpty = maxHeap.isEmpty();\n\n/* 리스트를 입력받아 힙 구성 */\nminHeap = new PriorityQueue&lt;&gt;(Arrays.asList(1, 3, 2, 5, 4));\n</code></pre> heap.cs<pre><code>/* 힙 초기화 */\n// 최소 힙 초기화\nPriorityQueue&lt;int, int&gt; minHeap = new();\n// 최대 힙 초기화(lambda 식으로 Comparer를 수정하면 된다)\nPriorityQueue&lt;int, int&gt; maxHeap = new(Comparer&lt;int&gt;.Create((x, y) =&gt; y.CompareTo(x)));\n\n/* 힙에 원소 삽입 */\nmaxHeap.Enqueue(1, 1);\nmaxHeap.Enqueue(3, 3);\nmaxHeap.Enqueue(2, 2);\nmaxHeap.Enqueue(5, 5);\nmaxHeap.Enqueue(4, 4);\n\n/* 힙의 루트 원소 구하기 */\nint peek = maxHeap.Peek();//5\n\n/* 힙의 루트 원소 꺼내기 */\n// 꺼낸 원소들은 큰 값부터 작은 값 순서로 나열된다\npeek = maxHeap.Dequeue(); // 5\npeek = maxHeap.Dequeue(); // 4\npeek = maxHeap.Dequeue(); // 3\npeek = maxHeap.Dequeue(); // 2\npeek = maxHeap.Dequeue(); // 1\n\n/* 힙의 크기 구하기 */\nint size = maxHeap.Count;\n\n/* 힙이 비어 있는지 확인 */\nbool isEmpty = maxHeap.Count == 0;\n\n/* 리스트를 입력받아 힙 구성 */\nminHeap = new PriorityQueue&lt;int, int&gt;([(1, 1), (3, 3), (2, 2), (5, 5), (4, 4)]);\n</code></pre> heap.go<pre><code>// Go에서는 heap.Interface를 구현하여 정수 최대 힙을 만들 수 있다\n// heap.Interface를 구현하려면 sort.Interface도 구현해야 한다\ntype intHeap []any\n\n// Push는 heap.Interface의 메서드로, 힙에 원소를 삽입한다\nfunc (h *intHeap) Push(x any) {\n // Push와 Pop은 포인터 리시버(pointer receiver)를 매개변수로 사용한다\n // 슬라이스의 내용뿐 아니라 길이도 수정하기 때문이다.\n *h = append(*h, x.(int))\n}\n\n// Pop은 heap.Interface의 메서드로, 힙의 루트 원소를 꺼낸다\nfunc (h *intHeap) Pop() any {\n // 꺼낼 원소는 맨 끝에 저장되어 있다\n last := (*h)[len(*h)-1]\n *h = (*h)[:len(*h)-1]\n return last\n}\n\n// Len은 sort.Interface의 메서드다\nfunc (h *intHeap) Len() int {\n return len(*h)\n}\n\n// Less는 sort.Interface의 메서드다\nfunc (h *intHeap) Less(i, j int) bool {\n // 최소 힙을 구현하려면 부등호를 &lt;로 바꿔야 한다\n return (*h)[i].(int) &gt; (*h)[j].(int)\n}\n\n// Swap은 sort.Interface의 메서드다\nfunc (h *intHeap) Swap(i, j int) {\n (*h)[i], (*h)[j] = (*h)[j], (*h)[i]\n}\n\n// Top은 힙의 루트 원소를 구한다\nfunc (h *intHeap) Top() any {\n return (*h)[0]\n}\n\n/* 실행 코드 */\nfunc TestHeap(t *testing.T) {\n /* 힙 초기화 */\n // 최대 힙 초기화\n maxHeap := &amp;intHeap{}\n heap.Init(maxHeap)\n /* 힙에 원소 삽입 */\n // heap.Interface의 메서드를 호출하여 원소를 추가한다\n heap.Push(maxHeap, 1)\n heap.Push(maxHeap, 3)\n heap.Push(maxHeap, 2)\n heap.Push(maxHeap, 4)\n heap.Push(maxHeap, 5)\n\n /* 힙의 루트 원소 구하기 */\n top := maxHeap.Top()\n fmt.Printf(\"힙의 루트 원소: %d\\n\", top)\n\n /* 힙의 루트 원소 꺼내기 */\n // heap.Interface의 메서드를 호출하여 원소를 제거한다\n heap.Pop(maxHeap) // 5\n heap.Pop(maxHeap) // 4\n heap.Pop(maxHeap) // 3\n heap.Pop(maxHeap) // 2\n heap.Pop(maxHeap) // 1\n\n /* 힙의 크기 구하기 */\n size := len(*maxHeap)\n fmt.Printf(\"힙의 원소 수: %d\\n\", size)\n\n /* 힙이 비어 있는지 확인 */\n isEmpty := len(*maxHeap) == 0\n fmt.Printf(\"힙이 비어 있는가: %t\\n\", isEmpty)\n}\n</code></pre> heap.swift<pre><code>/* 힙 초기화 */\n// Swift의 Heap 타입은 최대 힙과 최소 힙을 모두 지원하며, swift-collections를 가져와야 한다\nvar heap = Heap&lt;Int&gt;()\n\n/* 힙에 원소 삽입 */\nheap.insert(1)\nheap.insert(3)\nheap.insert(2)\nheap.insert(5)\nheap.insert(4)\n\n/* 힙의 루트 원소 구하기 */\nvar peek = heap.max()!\n\n/* 힙의 루트 원소 꺼내기 */\npeek = heap.removeMax() // 5\npeek = heap.removeMax() // 4\npeek = heap.removeMax() // 3\npeek = heap.removeMax() // 2\npeek = heap.removeMax() // 1\n\n/* 힙의 크기 구하기 */\nlet size = heap.count\n\n/* 힙이 비어 있는지 확인 */\nlet isEmpty = heap.isEmpty\n\n/* 리스트를 입력받아 힙 구성 */\nlet heap2 = Heap([1, 3, 2, 5, 4])\n</code></pre> heap.js<pre><code>// JavaScript는 내장 Heap 클래스를 제공하지 않는다\n</code></pre> heap.ts<pre><code>// TypeScript는 내장 Heap 클래스를 제공하지 않는다\n</code></pre> heap.dart<pre><code>// Dart는 내장 Heap 클래스를 제공하지 않는다\n</code></pre> heap.rs<pre><code>use std::collections::BinaryHeap;\nuse std::cmp::Reverse;\n\n/* 힙 초기화 */\n// 최소 힙 초기화\nlet mut min_heap = BinaryHeap::&lt;Reverse&lt;i32&gt;&gt;::new();\n// 최대 힙 초기화\nlet mut max_heap = BinaryHeap::new();\n\n/* 힙에 원소 삽입 */\nmax_heap.push(1);\nmax_heap.push(3);\nmax_heap.push(2);\nmax_heap.push(5);\nmax_heap.push(4);\n\n/* 힙의 루트 원소 구하기 */\nlet peek = max_heap.peek().unwrap(); // 5\n\n/* 힙의 루트 원소 꺼내기 */\n// 꺼낸 원소들은 큰 값부터 작은 값 순서로 나열된다\nlet peek = max_heap.pop().unwrap(); // 5\nlet peek = max_heap.pop().unwrap(); // 4\nlet peek = max_heap.pop().unwrap(); // 3\nlet peek = max_heap.pop().unwrap(); // 2\nlet peek = max_heap.pop().unwrap(); // 1\n\n/* 힙의 크기 구하기 */\nlet size = max_heap.len();\n\n/* 힙이 비어 있는지 확인 */\nlet is_empty = max_heap.is_empty();\n\n/* 리스트를 입력받아 힙 구성 */\nlet min_heap = BinaryHeap::from(vec![Reverse(1), Reverse(3), Reverse(2), Reverse(5), Reverse(4)]);\n</code></pre> heap.c<pre><code>// C는 내장 Heap 클래스를 제공하지 않는다\n</code></pre> heap.kt<pre><code>/* 힙 초기화 */\n// 최소 힙 초기화\nvar minHeap = PriorityQueue&lt;Int&gt;()\n// 최대 힙 초기화(lambda 식으로 Comparator를 수정하면 된다)\nval maxHeap = PriorityQueue { a: Int, b: Int -&gt; b - a }\n\n/* 힙에 원소 삽입 */\nmaxHeap.offer(1)\nmaxHeap.offer(3)\nmaxHeap.offer(2)\nmaxHeap.offer(5)\nmaxHeap.offer(4)\n\n/* 힙의 루트 원소 구하기 */\nvar peek = maxHeap.peek() // 5\n\n/* 힙의 루트 원소 꺼내기 */\n// 꺼낸 원소들은 큰 값부터 작은 값 순서로 나열된다\npeek = maxHeap.poll() // 5\npeek = maxHeap.poll() // 4\npeek = maxHeap.poll() // 3\npeek = maxHeap.poll() // 2\npeek = maxHeap.poll() // 1\n\n/* 힙의 크기 구하기 */\nval size = maxHeap.size\n\n/* 힙이 비어 있는지 확인 */\nval isEmpty = maxHeap.isEmpty()\n\n/* 리스트를 입력받아 힙 구성 */\nminHeap = PriorityQueue(mutableListOf(1, 3, 2, 5, 4))\n</code></pre> heap.rb<pre><code># Ruby는 내장 Heap 클래스를 제공하지 않는다\n</code></pre> 실행 시각화 <p> 전체 화면 &gt;</p>","path":["제 8 장   힙","8.1   힙"],"tags":[]},{"location":"chapter_heap/heap/#812","level":2,"title":"8.1.2   힙의 구현","text":"<p>아래에서는 최대 힙을 구현한다. 이를 최소 힙으로 바꾸려면 모든 대소 비교 조건을 반대로 바꾸면 된다(예를 들어 \\(\\geq\\)를 \\(\\leq\\)로 바꾼다). 관심 있는 독자는 직접 구현해 보자.</p>","path":["제 8 장   힙","8.1   힙"],"tags":[]},{"location":"chapter_heap/heap/#1","level":3,"title":"1.   힙의 저장과 표현","text":"<p>'이진 트리' 장에서 살펴보았듯이 완전 이진 트리는 배열로 표현하기에 매우 적합하다. 힙은 완전 이진 트리이므로 배열을 사용하여 힙을 저장한다.</p> <p>배열로 이진 트리를 표현할 때 원소는 노드의 값을, 인덱스는 이진 트리에서 노드의 위치를 나타낸다. 노드 포인터는 인덱스 매핑 공식으로 구현한다.</p> <p>아래 그림(8-2)과 같이 인덱스 \\(i\\)가 주어지면 왼쪽 자식 노드의 인덱스는 \\(2i + 1\\), 오른쪽 자식 노드의 인덱스는 \\(2i + 2\\), 부모 노드의 인덱스는 \\((i - 1) / 2\\)(내림 정수 나눗셈)이다. 인덱스가 범위를 벗어나면 빈 노드이거나 노드가 존재하지 않는다는 뜻이다.</p> <p></p> <p> 그림 8-2   힙의 표현과 저장 </p> <p>나중에 편리하게 사용할 수 있도록 인덱스 매핑 공식을 함수로 만들 수 있다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py<pre><code>def left(self, i: int) -&gt; int:\n \"\"\"왼쪽 자식 노드의 인덱스 반환\"\"\"\n return 2 * i + 1\n\ndef right(self, i: int) -&gt; int:\n \"\"\"오른쪽 자식 노드의 인덱스 반환\"\"\"\n return 2 * i + 2\n\ndef parent(self, i: int) -&gt; int:\n \"\"\"부모 노드의 인덱스 반환\"\"\"\n return (i - 1) // 2 # 나눗셈의 결과를 내림한다\n</code></pre> my_heap.cpp<pre><code>/* 왼쪽 자식 노드의 인덱스 반환 */\nint left(int i) {\n return 2 * i + 1;\n}\n\n/* 오른쪽 자식 노드의 인덱스 반환 */\nint right(int i) {\n return 2 * i + 2;\n}\n\n/* 부모 노드의 인덱스 반환 */\nint parent(int i) {\n return (i - 1) / 2; // 나눗셈의 결과를 내림한다\n}\n</code></pre> my_heap.java<pre><code>/* 왼쪽 자식 노드의 인덱스 반환 */\nint left(int i) {\n return 2 * i + 1;\n}\n\n/* 오른쪽 자식 노드의 인덱스 반환 */\nint right(int i) {\n return 2 * i + 2;\n}\n\n/* 부모 노드의 인덱스 반환 */\nint parent(int i) {\n return (i - 1) / 2; // 나눗셈의 결과를 내림한다\n}\n</code></pre> my_heap.cs<pre><code>/* 왼쪽 자식 노드의 인덱스 반환 */\nint Left(int i) {\n return 2 * i + 1;\n}\n\n/* 오른쪽 자식 노드의 인덱스 반환 */\nint Right(int i) {\n return 2 * i + 2;\n}\n\n/* 부모 노드의 인덱스 반환 */\nint Parent(int i) {\n return (i - 1) / 2; // 나눗셈의 결과를 내림한다\n}\n</code></pre> my_heap.go<pre><code>/* 왼쪽 자식 노드의 인덱스 반환 */\nfunc (h *maxHeap) left(i int) int {\n return 2*i + 1\n}\n\n/* 오른쪽 자식 노드의 인덱스 반환 */\nfunc (h *maxHeap) right(i int) int {\n return 2*i + 2\n}\n\n/* 부모 노드의 인덱스 반환 */\nfunc (h *maxHeap) parent(i int) int {\n // 나눗셈의 결과를 내림한다\n return (i - 1) / 2\n}\n</code></pre> my_heap.swift<pre><code>/* 왼쪽 자식 노드의 인덱스 반환 */\nfunc left(i: Int) -&gt; Int {\n 2 * i + 1\n}\n\n/* 오른쪽 자식 노드의 인덱스 반환 */\nfunc right(i: Int) -&gt; Int {\n 2 * i + 2\n}\n\n/* 부모 노드의 인덱스 반환 */\nfunc parent(i: Int) -&gt; Int {\n (i - 1) / 2 // 나눗셈의 결과를 내림한다\n}\n</code></pre> my_heap.js<pre><code>/* 왼쪽 자식 노드의 인덱스 반환 */\n#left(i) {\n return 2 * i + 1;\n}\n\n/* 오른쪽 자식 노드의 인덱스 반환 */\n#right(i) {\n return 2 * i + 2;\n}\n\n/* 부모 노드의 인덱스 반환 */\n#parent(i) {\n return Math.floor((i - 1) / 2); // 나눗셈의 결과를 내림한다\n}\n</code></pre> my_heap.ts<pre><code>/* 왼쪽 자식 노드의 인덱스 반환 */\nleft(i: number): number {\n return 2 * i + 1;\n}\n\n/* 오른쪽 자식 노드의 인덱스 반환 */\nright(i: number): number {\n return 2 * i + 2;\n}\n\n/* 부모 노드의 인덱스 반환 */\nparent(i: number): number {\n return Math.floor((i - 1) / 2); // 나눗셈의 결과를 내림한다\n}\n</code></pre> my_heap.dart<pre><code>/* 왼쪽 자식 노드의 인덱스 반환 */\nint _left(int i) {\n return 2 * i + 1;\n}\n\n/* 오른쪽 자식 노드의 인덱스 반환 */\nint _right(int i) {\n return 2 * i + 2;\n}\n\n/* 부모 노드의 인덱스 반환 */\nint _parent(int i) {\n return (i - 1) ~/ 2; // 나눗셈의 결과를 내림한다\n}\n</code></pre> my_heap.rs<pre><code>/* 왼쪽 자식 노드의 인덱스 반환 */\nfn left(i: usize) -&gt; usize {\n 2 * i + 1\n}\n\n/* 오른쪽 자식 노드의 인덱스 반환 */\nfn right(i: usize) -&gt; usize {\n 2 * i + 2\n}\n\n/* 부모 노드의 인덱스 반환 */\nfn parent(i: usize) -&gt; usize {\n (i - 1) / 2 // 나눗셈의 결과를 내림한다\n}\n</code></pre> my_heap.c<pre><code>/* 왼쪽 자식 노드의 인덱스 반환 */\nint left(MaxHeap *maxHeap, int i) {\n return 2 * i + 1;\n}\n\n/* 오른쪽 자식 노드의 인덱스 반환 */\nint right(MaxHeap *maxHeap, int i) {\n return 2 * i + 2;\n}\n\n/* 부모 노드의 인덱스 반환 */\nint parent(MaxHeap *maxHeap, int i) {\n return (i - 1) / 2; // 내림\n}\n</code></pre> my_heap.kt<pre><code>/* 왼쪽 자식 노드의 인덱스 반환 */\nfun left(i: Int): Int {\n return 2 * i + 1\n}\n\n/* 오른쪽 자식 노드의 인덱스 반환 */\nfun right(i: Int): Int {\n return 2 * i + 2\n}\n\n/* 부모 노드의 인덱스 반환 */\nfun parent(i: Int): Int {\n return (i - 1) / 2 // 나눗셈의 결과를 내림한다\n}\n</code></pre> my_heap.rb<pre><code>### 왼쪽 자식 노드의 인덱스 반환 ###\ndef left(i)\n 2 * i + 1\nend\n\n### 오른쪽 자식 노드의 인덱스 반환 ###\ndef right(i)\n 2 * i + 2\nend\n\n### 부모 노드의 인덱스 반환 ###\ndef parent(i)\n (i - 1) / 2 # 나눗셈의 결과를 내림한다\nend\n</code></pre>","path":["제 8 장   힙","8.1   힙"],"tags":[]},{"location":"chapter_heap/heap/#2","level":3,"title":"2.   힙의 루트 원소에 접근","text":"<p>힙의 루트 원소는 이진 트리의 루트 노드, 즉 리스트의 첫 번째 원소다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py<pre><code>def peek(self) -&gt; int:\n \"\"\"힙의 루트 원소 접근\"\"\"\n return self.max_heap[0]\n</code></pre> my_heap.cpp<pre><code>/* 힙의 루트 원소 접근 */\nint peek() {\n return maxHeap[0];\n}\n</code></pre> my_heap.java<pre><code>/* 힙의 루트 원소 접근 */\nint peek() {\n return maxHeap.get(0);\n}\n</code></pre> my_heap.cs<pre><code>/* 힙의 루트 원소 접근 */\nint Peek() {\n return maxHeap[0];\n}\n</code></pre> my_heap.go<pre><code>/* 힙의 루트 원소 접근 */\nfunc (h *maxHeap) peek() any {\n return h.data[0]\n}\n</code></pre> my_heap.swift<pre><code>/* 힙의 루트 원소 접근 */\nfunc peek() -&gt; Int {\n maxHeap[0]\n}\n</code></pre> my_heap.js<pre><code>/* 힙의 루트 원소 접근 */\npeek() {\n return this.#maxHeap[0];\n}\n</code></pre> my_heap.ts<pre><code>/* 힙의 루트 원소 접근 */\npeek(): number {\n return this.maxHeap[0];\n}\n</code></pre> my_heap.dart<pre><code>/* 힙의 루트 원소 접근 */\nint peek() {\n return _maxHeap[0];\n}\n</code></pre> my_heap.rs<pre><code>/* 힙의 루트 원소 접근 */\nfn peek(&amp;self) -&gt; Option&lt;i32&gt; {\n self.max_heap.first().copied()\n}\n</code></pre> my_heap.c<pre><code>/* 힙의 루트 원소 접근 */\nint peek(MaxHeap *maxHeap) {\n return maxHeap-&gt;data[0];\n}\n</code></pre> my_heap.kt<pre><code>/* 힙의 루트 원소 접근 */\nfun peek(): Int {\n return maxHeap[0]\n}\n</code></pre> my_heap.rb<pre><code>### 힙의 루트 원소 접근 ###\ndef peek\n @max_heap[0]\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 8 장   힙","8.1   힙"],"tags":[]},{"location":"chapter_heap/heap/#3","level":3,"title":"3.   힙에 원소 삽입","text":"<p>원소 <code>val</code>이 주어지면 먼저 힙의 끝에 추가한다. 추가한 <code>val</code>이 힙의 다른 원소보다 클 수 있으므로, 힙의 성립 조건이 깨질 수 있다. 따라서 삽입한 노드에서 루트 노드까지의 경로에 있는 노드들을 조정해야 한다. 이 연산을 힙 재정렬(heapify)이라고 한다.</p> <p>삽입한 노드부터 아래에서 위로 힙을 재정렬해 보자. 아래 그림(8-3)과 같이 삽입한 노드의 값과 부모 노드의 값을 비교하여 삽입한 노드가 더 크면 서로 바꾼다. 이 작업을 반복하여 아래에서 위로 힙의 노드들을 조정하고, 루트 노드를 지나거나 바꿀 필요가 없는 노드를 만나면 종료한다.</p> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt;&lt;6&gt;&lt;7&gt;&lt;8&gt;&lt;9&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 8-3   힙에 원소를 삽입하는 단계 </p> <p>전체 노드 수가 \\(n\\)이면 트리의 높이는 \\(O(\\log n)\\)이다. 따라서 힙 재정렬의 반복 횟수는 최대 \\(O(\\log n)\\)이며, 힙에 원소를 삽입하는 연산의 시간 복잡도는 \\(O(\\log n)\\)이다. 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py<pre><code>def push(self, val: int):\n \"\"\"힙에 원소 삽입\"\"\"\n # 노드 추가\n self.max_heap.append(val)\n # 아래에서 위로 힙을 재정렬한다\n self.sift_up(self.size() - 1)\n\ndef sift_up(self, i: int):\n \"\"\"노드 i에서 시작하여 아래에서 위로 힙 재정렬\"\"\"\n while True:\n # 노드 i의 부모 노드를 구한다\n p = self.parent(i)\n # 루트 노드를 넘어가거나 노드를 수정할 필요가 없으면 힙 재정렬을 마친다\n if p &lt; 0 or self.max_heap[i] &lt;= self.max_heap[p]:\n break\n # 두 노드를 교환한다\n self.swap(i, p)\n # 위쪽으로 힙 재정렬을 반복한다\n i = p\n</code></pre> my_heap.cpp<pre><code>/* 힙에 원소 삽입 */\nvoid push(int val) {\n // 노드 추가\n maxHeap.push_back(val);\n // 아래에서 위로 힙을 재정렬한다\n siftUp(size() - 1);\n}\n\n/* 노드 i에서 시작하여 아래에서 위로 힙 재정렬 */\nvoid siftUp(int i) {\n while (true) {\n // 노드 i의 부모 노드를 구한다\n int p = parent(i);\n // 루트 노드를 넘어가거나 노드를 수정할 필요가 없으면 힙 재정렬을 마친다\n if (p &lt; 0 || maxHeap[i] &lt;= maxHeap[p])\n break;\n // 두 노드를 교환한다\n swap(maxHeap[i], maxHeap[p]);\n // 위쪽으로 힙 재정렬을 반복한다\n i = p;\n }\n}\n</code></pre> my_heap.java<pre><code>/* 힙에 원소 삽입 */\nvoid push(int val) {\n // 노드 추가\n maxHeap.add(val);\n // 아래에서 위로 힙을 재정렬한다\n siftUp(size() - 1);\n}\n\n/* 노드 i에서 시작하여 아래에서 위로 힙 재정렬 */\nvoid siftUp(int i) {\n while (true) {\n // 노드 i의 부모 노드를 구한다\n int p = parent(i);\n // 루트 노드를 넘어가거나 노드를 수정할 필요가 없으면 힙 재정렬을 마친다\n if (p &lt; 0 || maxHeap.get(i) &lt;= maxHeap.get(p))\n break;\n // 두 노드를 교환한다\n swap(i, p);\n // 위쪽으로 힙 재정렬을 반복한다\n i = p;\n }\n}\n</code></pre> my_heap.cs<pre><code>/* 힙에 원소 삽입 */\nvoid Push(int val) {\n // 노드 추가\n maxHeap.Add(val);\n // 아래에서 위로 힙을 재정렬한다\n SiftUp(Size() - 1);\n}\n\n/* 노드 i에서 시작하여 아래에서 위로 힙 재정렬 */\nvoid SiftUp(int i) {\n while (true) {\n // 노드 i의 부모 노드를 구한다\n int p = Parent(i);\n // 루트 노드를 넘어가거나 노드를 수정할 필요가 없으면 힙 재정렬을 종료한다\n if (p &lt; 0 || maxHeap[i] &lt;= maxHeap[p])\n break;\n // 두 노드를 교환한다\n Swap(i, p);\n // 위쪽으로 힙 재정렬을 반복한다\n i = p;\n }\n}\n</code></pre> my_heap.go<pre><code>/* 힙에 원소 삽입 */\nfunc (h *maxHeap) push(val any) {\n // 노드 추가\n h.data = append(h.data, val)\n // 아래에서 위로 힙을 재정렬한다\n h.siftUp(len(h.data) - 1)\n}\n\n/* 노드 i에서 시작하여 아래에서 위로 힙 재정렬 */\nfunc (h *maxHeap) siftUp(i int) {\n for true {\n // 노드 i의 부모 노드를 구한다\n p := h.parent(i)\n // 루트 노드를 넘어가거나 노드를 수정할 필요가 없으면 힙 재정렬을 마친다\n if p &lt; 0 || h.data[i].(int) &lt;= h.data[p].(int) {\n break\n }\n // 두 노드를 교환한다\n h.swap(i, p)\n // 위쪽으로 힙 재정렬을 반복한다\n i = p\n }\n}\n</code></pre> my_heap.swift<pre><code>/* 힙에 원소 삽입 */\nfunc push(val: Int) {\n // 노드 추가\n maxHeap.append(val)\n // 아래에서 위로 힙을 재정렬한다\n siftUp(i: size() - 1)\n}\n\n/* 노드 i에서 시작하여 아래에서 위로 힙 재정렬 */\nfunc siftUp(i: Int) {\n var i = i\n while true {\n // 노드 i의 부모 노드를 구한다\n let p = parent(i: i)\n // 루트 노드를 넘어가거나 노드를 수정할 필요가 없으면 힙 재정렬을 마친다\n if p &lt; 0 || maxHeap[i] &lt;= maxHeap[p] {\n break\n }\n // 두 노드를 교환한다\n swap(i: i, j: p)\n // 위쪽으로 힙 재정렬을 반복한다\n i = p\n }\n}\n</code></pre> my_heap.js<pre><code>/* 힙에 원소 삽입 */\npush(val) {\n // 노드 추가\n this.#maxHeap.push(val);\n // 아래에서 위로 힙을 재정렬한다\n this.#siftUp(this.size() - 1);\n}\n\n/* 노드 i에서 시작하여 아래에서 위로 힙 재정렬 */\n#siftUp(i) {\n while (true) {\n // 노드 i의 부모 노드를 구한다\n const p = this.#parent(i);\n // 루트 노드를 넘어가거나 노드를 수정할 필요가 없으면 힙 재정렬을 마친다\n if (p &lt; 0 || this.#maxHeap[i] &lt;= this.#maxHeap[p]) break;\n // 두 노드를 교환한다\n this.#swap(i, p);\n // 위쪽으로 힙 재정렬을 반복한다\n i = p;\n }\n}\n</code></pre> my_heap.ts<pre><code>/* 힙에 원소 삽입 */\npush(val: number): void {\n // 노드 추가\n this.maxHeap.push(val);\n // 아래에서 위로 힙을 재정렬한다\n this.siftUp(this.size() - 1);\n}\n\n/* 노드 i에서 시작하여 아래에서 위로 힙 재정렬 */\nsiftUp(i: number): void {\n while (true) {\n // 노드 i의 부모 노드를 구한다\n const p = this.parent(i);\n // 루트 노드를 넘어가거나 노드를 수정할 필요가 없으면 힙 재정렬을 마친다\n if (p &lt; 0 || this.maxHeap[i] &lt;= this.maxHeap[p]) break;\n // 두 노드를 교환한다\n this.swap(i, p);\n // 위쪽으로 힙 재정렬을 반복한다\n i = p;\n }\n}\n</code></pre> my_heap.dart<pre><code>/* 힙에 원소 삽입 */\nvoid push(int val) {\n // 노드 추가\n _maxHeap.add(val);\n // 아래에서 위로 힙을 재정렬한다\n siftUp(size() - 1);\n}\n\n/* 노드 i에서 시작하여 아래에서 위로 힙 재정렬 */\nvoid siftUp(int i) {\n while (true) {\n // 노드 i의 부모 노드를 구한다\n int p = _parent(i);\n // 루트 노드를 넘어가거나 노드를 수정할 필요가 없으면 힙 재정렬을 마친다\n if (p &lt; 0 || _maxHeap[i] &lt;= _maxHeap[p]) {\n break;\n }\n // 두 노드를 교환한다\n _swap(i, p);\n // 위쪽으로 힙 재정렬을 반복한다\n i = p;\n }\n}\n</code></pre> my_heap.rs<pre><code>/* 힙에 원소 삽입 */\nfn push(&amp;mut self, val: i32) {\n // 노드 추가\n self.max_heap.push(val);\n // 아래에서 위로 힙을 재정렬한다\n self.sift_up(self.size() - 1);\n}\n\n/* 노드 i에서 시작하여 아래에서 위로 힙 재정렬 */\nfn sift_up(&amp;mut self, mut i: usize) {\n loop {\n // 노드 i가 이미 힙의 루트이므로 힙 재정렬을 마친다\n if i == 0 {\n break;\n }\n // 노드 i의 부모 노드를 구한다\n let p = Self::parent(i);\n // 노드를 조정할 필요가 없으면 힙 재정렬을 마친다\n if self.max_heap[i] &lt;= self.max_heap[p] {\n break;\n }\n // 두 노드를 교환한다\n self.swap(i, p);\n // 위쪽으로 힙 재정렬을 반복한다\n i = p;\n }\n}\n</code></pre> my_heap.c<pre><code>/* 힙에 원소 삽입 */\nvoid push(MaxHeap *maxHeap, int val) {\n // 기본적으로 이렇게 많은 노드를 추가해서는 안 된다\n if (maxHeap-&gt;size == MAX_SIZE) {\n printf(\"힙이 가득 찼다!\");\n return;\n }\n // 노드 추가\n maxHeap-&gt;data[maxHeap-&gt;size] = val;\n maxHeap-&gt;size++;\n\n // 아래에서 위로 힙을 재정렬한다\n siftUp(maxHeap, maxHeap-&gt;size - 1);\n}\n\n/* 노드 i에서 시작하여 아래에서 위로 힙 재정렬 */\nvoid siftUp(MaxHeap *maxHeap, int i) {\n while (true) {\n // 노드 i의 부모 노드를 구한다\n int p = parent(maxHeap, i);\n // 루트 노드를 넘어가거나 노드를 수정할 필요가 없으면 힙 재정렬을 마친다\n if (p &lt; 0 || maxHeap-&gt;data[i] &lt;= maxHeap-&gt;data[p]) {\n break;\n }\n // 두 노드를 교환한다\n swap(maxHeap, i, p);\n // 위쪽으로 힙 재정렬을 반복한다\n i = p;\n }\n}\n</code></pre> my_heap.kt<pre><code>/* 힙에 원소 삽입 */\nfun push(_val: Int) {\n // 노드 추가\n maxHeap.add(_val)\n // 아래에서 위로 힙을 재정렬한다\n siftUp(size() - 1)\n}\n\n/* 노드 i에서 시작하여 아래에서 위로 힙 재정렬 */\nfun siftUp(it: Int) {\n // Kotlin의 함수 매개변수는 변경할 수 없으므로 임시 변수를 생성한다\n var i = it\n while (true) {\n // 노드 i의 부모 노드를 구한다\n val p = parent(i)\n // 루트 노드를 넘어가거나 노드를 수정할 필요가 없으면 힙 재정렬을 마친다\n if (p &lt; 0 || maxHeap[i] &lt;= maxHeap[p]) break\n // 두 노드를 교환한다\n swap(i, p)\n // 위쪽으로 힙 재정렬을 반복한다\n i = p\n }\n}\n</code></pre> my_heap.rb<pre><code>### 힙에 원소 삽입 ###\ndef push(val)\n # 노드 추가\n @max_heap &lt;&lt; val\n # 아래에서 위로 힙을 재정렬한다\n sift_up(size - 1)\nend\n\n### 노드 i에서 시작하여 아래에서 위로 힙 재정렬 ###\ndef sift_up(i)\n loop do\n # 노드 i의 부모 노드를 구한다\n p = parent(i)\n # 루트 노드를 넘어가거나 노드를 수정할 필요가 없으면 힙 재정렬을 마친다\n break if p &lt; 0 || @max_heap[i] &lt;= @max_heap[p]\n # 두 노드를 교환한다\n swap(i, p)\n # 위쪽으로 힙 재정렬을 반복한다\n i = p\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 8 장   힙","8.1   힙"],"tags":[]},{"location":"chapter_heap/heap/#4","level":3,"title":"4.   힙의 루트 원소 꺼내기","text":"<p>힙의 루트 원소는 이진 트리의 루트 노드, 즉 리스트의 첫 번째 원소다. 리스트에서 첫 번째 원소를 바로 삭제하면 이진 트리의 모든 노드 인덱스가 바뀌어, 이후 힙 재정렬로 복구하기가 어려워진다. 원소 인덱스의 변화를 최소화하기 위해 다음 절차를 사용한다.</p> <ol> <li>힙의 루트 원소와 끝 원소를 교환한다(루트 노드와 맨 오른쪽 리프 노드를 교환한다).</li> <li>교환 후 리스트에서 힙의 끝 원소를 삭제한다(이미 교환했으므로 실제로 삭제되는 것은 원래의 루트 원소다).</li> <li>루트 노드부터 위에서 아래로 힙을 재정렬한다.</li> </ol> <p>아래 그림(8-4)과 같이 '위에서 아래로 힙 재정렬'은 '아래에서 위로 힙 재정렬'과 진행 방향이 반대다. 루트 노드의 값을 두 자식 노드의 값과 비교하여, 더 큰 값을 가진 자식 노드와 루트 노드를 교환한다. 이 작업을 반복하며 리프 노드를 지나거나 교환할 필요가 없는 노드를 만나면 종료한다.</p> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt;&lt;6&gt;&lt;7&gt;&lt;8&gt;&lt;9&gt;&lt;10&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 8-4   힙의 루트 원소를 꺼내는 단계 </p> <p>원소 삽입과 마찬가지로 힙의 루트 원소를 꺼내는 연산의 시간 복잡도도 \\(O(\\log n)\\)이다. 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py<pre><code>def pop(self) -&gt; int:\n \"\"\"힙에서 원소 꺼내기\"\"\"\n # 비어 있는 경우를 처리한다\n if self.is_empty():\n raise IndexError(\"힙이 비어 있다\")\n # 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n self.swap(0, self.size() - 1)\n # 노드 삭제\n val = self.max_heap.pop()\n # 위에서 아래로 힙을 재정렬한다\n self.sift_down(0)\n # 힙의 루트 원소를 반환한다\n return val\n\ndef sift_down(self, i: int):\n \"\"\"노드 i에서 시작하여 위에서 아래로 힙 재정렬\"\"\"\n while True:\n # 노드 i, l, r 중 값이 가장 큰 노드를 찾아 ma로 표시한다\n l, r, ma = self.left(i), self.right(i), i\n if l &lt; self.size() and self.max_heap[l] &gt; self.max_heap[ma]:\n ma = l\n if r &lt; self.size() and self.max_heap[r] &gt; self.max_heap[ma]:\n ma = r\n # 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n if ma == i:\n break\n # 두 노드를 교환한다\n self.swap(i, ma)\n # 아래쪽으로 힙 재정렬을 반복한다\n i = ma\n</code></pre> my_heap.cpp<pre><code>/* 힙에서 원소 꺼내기 */\nvoid pop() {\n // 비어 있는 경우를 처리한다\n if (isEmpty()) {\n throw out_of_range(\"힙이 비어 있다\");\n }\n // 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n swap(maxHeap[0], maxHeap[size() - 1]);\n // 노드 삭제\n maxHeap.pop_back();\n // 위에서 아래로 힙을 재정렬한다\n siftDown(0);\n}\n\n/* 노드 i에서 시작하여 위에서 아래로 힙 재정렬 */\nvoid siftDown(int i) {\n while (true) {\n // 노드 i, l, r 중 값이 가장 큰 노드를 찾아 ma로 표시한다\n int l = left(i), r = right(i), ma = i;\n if (l &lt; size() &amp;&amp; maxHeap[l] &gt; maxHeap[ma])\n ma = l;\n if (r &lt; size() &amp;&amp; maxHeap[r] &gt; maxHeap[ma])\n ma = r;\n // 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n if (ma == i)\n break;\n swap(maxHeap[i], maxHeap[ma]);\n // 아래쪽으로 힙 재정렬을 반복한다\n i = ma;\n }\n}\n</code></pre> my_heap.java<pre><code>/* 힙에서 원소 꺼내기 */\nint pop() {\n // 비어 있는 경우를 처리한다\n if (isEmpty())\n throw new IndexOutOfBoundsException();\n // 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n swap(0, size() - 1);\n // 노드 삭제\n int val = maxHeap.remove(size() - 1);\n // 위에서 아래로 힙을 재정렬한다\n siftDown(0);\n // 힙의 루트 원소를 반환한다\n return val;\n}\n\n/* 노드 i에서 시작하여 위에서 아래로 힙 재정렬 */\nvoid siftDown(int i) {\n while (true) {\n // 노드 i, l, r 중 값이 가장 큰 노드를 찾아 ma로 표시한다\n int l = left(i), r = right(i), ma = i;\n if (l &lt; size() &amp;&amp; maxHeap.get(l) &gt; maxHeap.get(ma))\n ma = l;\n if (r &lt; size() &amp;&amp; maxHeap.get(r) &gt; maxHeap.get(ma))\n ma = r;\n // 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n if (ma == i)\n break;\n // 두 노드를 교환한다\n swap(i, ma);\n // 아래쪽으로 힙 재정렬을 반복한다\n i = ma;\n }\n}\n</code></pre> my_heap.cs<pre><code>/* 힙에서 원소 꺼내기 */\nint Pop() {\n // 비어 있는 경우를 처리한다\n if (IsEmpty())\n throw new IndexOutOfRangeException();\n // 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n Swap(0, Size() - 1);\n // 노드 삭제\n int val = maxHeap.Last();\n maxHeap.RemoveAt(Size() - 1);\n // 위에서 아래로 힙을 재정렬한다\n SiftDown(0);\n // 힙의 루트 원소를 반환한다\n return val;\n}\n\n/* 노드 i에서 시작하여 위에서 아래로 힙 재정렬 */\nvoid SiftDown(int i) {\n while (true) {\n // 노드 i, l, r 중 값이 가장 큰 노드를 찾아 ma로 표시한다\n int l = Left(i), r = Right(i), ma = i;\n if (l &lt; Size() &amp;&amp; maxHeap[l] &gt; maxHeap[ma])\n ma = l;\n if (r &lt; Size() &amp;&amp; maxHeap[r] &gt; maxHeap[ma])\n ma = r;\n // 노드 i의 값이 가장 크거나 리프 노드를 넘어가면 힙 재정렬을 종료한다\n if (ma == i) break;\n // 두 노드를 교환한다\n Swap(i, ma);\n // 아래쪽으로 힙 재정렬을 반복한다\n i = ma;\n }\n}\n</code></pre> my_heap.go<pre><code>/* 힙에서 원소 꺼내기 */\nfunc (h *maxHeap) pop() any {\n // 비어 있는 경우를 처리한다\n if h.isEmpty() {\n fmt.Println(\"오류\")\n return nil\n }\n // 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n h.swap(0, h.size()-1)\n // 노드 삭제\n val := h.data[len(h.data)-1]\n h.data = h.data[:len(h.data)-1]\n // 위에서 아래로 힙을 재정렬한다\n h.siftDown(0)\n\n // 힙의 루트 원소를 반환한다\n return val\n}\n\n/* 노드 i에서 시작하여 위에서 아래로 힙 재정렬 */\nfunc (h *maxHeap) siftDown(i int) {\n for true {\n // 노드 i, l, r 중 값이 가장 큰 노드를 찾아 max로 표시한다\n l, r, max := h.left(i), h.right(i), i\n if l &lt; h.size() &amp;&amp; h.data[l].(int) &gt; h.data[max].(int) {\n max = l\n }\n if r &lt; h.size() &amp;&amp; h.data[r].(int) &gt; h.data[max].(int) {\n max = r\n }\n // 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n if max == i {\n break\n }\n // 두 노드를 교환한다\n h.swap(i, max)\n // 아래쪽으로 힙 재정렬을 반복한다\n i = max\n }\n}\n</code></pre> my_heap.swift<pre><code>/* 힙에서 원소 꺼내기 */\nfunc pop() -&gt; Int {\n // 비어 있는 경우를 처리한다\n if isEmpty() {\n fatalError(\"힙이 비어 있다\")\n }\n // 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n swap(i: 0, j: size() - 1)\n // 노드 삭제\n let val = maxHeap.remove(at: size() - 1)\n // 위에서 아래로 힙을 재정렬한다\n siftDown(i: 0)\n // 힙의 루트 원소를 반환한다\n return val\n}\n\n/* 노드 i에서 시작하여 위에서 아래로 힙 재정렬 */\nfunc siftDown(i: Int) {\n var i = i\n while true {\n // 노드 i, l, r 중 값이 가장 큰 노드를 찾아 ma로 표시한다\n let l = left(i: i)\n let r = right(i: i)\n var ma = i\n if l &lt; size(), maxHeap[l] &gt; maxHeap[ma] {\n ma = l\n }\n if r &lt; size(), maxHeap[r] &gt; maxHeap[ma] {\n ma = r\n }\n // 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n if ma == i {\n break\n }\n // 두 노드를 교환한다\n swap(i: i, j: ma)\n // 아래쪽으로 힙 재정렬을 반복한다\n i = ma\n }\n}\n</code></pre> my_heap.js<pre><code>/* 힙에서 원소 꺼내기 */\npop() {\n // 비어 있는 경우를 처리한다\n if (this.isEmpty()) throw new Error('힙이 비어 있다');\n // 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n this.#swap(0, this.size() - 1);\n // 노드 삭제\n const val = this.#maxHeap.pop();\n // 위에서 아래로 힙을 재정렬한다\n this.#siftDown(0);\n // 힙의 루트 원소를 반환한다\n return val;\n}\n\n/* 노드 i에서 시작하여 위에서 아래로 힙 재정렬 */\n#siftDown(i) {\n while (true) {\n // 노드 i, l, r 중 값이 가장 큰 노드를 찾아 ma로 표시한다\n const l = this.#left(i),\n r = this.#right(i);\n let ma = i;\n if (l &lt; this.size() &amp;&amp; this.#maxHeap[l] &gt; this.#maxHeap[ma]) ma = l;\n if (r &lt; this.size() &amp;&amp; this.#maxHeap[r] &gt; this.#maxHeap[ma]) ma = r;\n // 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n if (ma === i) break;\n // 두 노드를 교환한다\n this.#swap(i, ma);\n // 아래쪽으로 힙 재정렬을 반복한다\n i = ma;\n }\n}\n</code></pre> my_heap.ts<pre><code>/* 힙에서 원소 꺼내기 */\npop(): number {\n // 비어 있는 경우를 처리한다\n if (this.isEmpty()) throw new RangeError('힙이 비어 있다.');\n // 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n this.swap(0, this.size() - 1);\n // 노드 삭제\n const val = this.maxHeap.pop();\n // 위에서 아래로 힙을 재정렬한다\n this.siftDown(0);\n // 힙의 루트 원소를 반환한다\n return val;\n}\n\n/* 노드 i에서 시작하여 위에서 아래로 힙 재정렬 */\nsiftDown(i: number): void {\n while (true) {\n // 노드 i, l, r 중 값이 가장 큰 노드를 찾아 ma로 표시한다\n const l = this.left(i),\n r = this.right(i);\n let ma = i;\n if (l &lt; this.size() &amp;&amp; this.maxHeap[l] &gt; this.maxHeap[ma]) ma = l;\n if (r &lt; this.size() &amp;&amp; this.maxHeap[r] &gt; this.maxHeap[ma]) ma = r;\n // 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n if (ma === i) break;\n // 두 노드를 교환한다\n this.swap(i, ma);\n // 아래쪽으로 힙 재정렬을 반복한다\n i = ma;\n }\n}\n</code></pre> my_heap.dart<pre><code>/* 힙에서 원소 꺼내기 */\nint pop() {\n // 비어 있는 경우를 처리한다\n if (isEmpty()) throw Exception('힙이 비어 있다');\n // 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n _swap(0, size() - 1);\n // 노드 삭제\n int val = _maxHeap.removeLast();\n // 위에서 아래로 힙을 재정렬한다\n siftDown(0);\n // 힙의 루트 원소를 반환한다\n return val;\n}\n\n/* 노드 i에서 시작하여 위에서 아래로 힙 재정렬 */\nvoid siftDown(int i) {\n while (true) {\n // 노드 i, l, r 중 값이 가장 큰 노드를 찾아 ma로 표시한다\n int l = _left(i);\n int r = _right(i);\n int ma = i;\n if (l &lt; size() &amp;&amp; _maxHeap[l] &gt; _maxHeap[ma]) ma = l;\n if (r &lt; size() &amp;&amp; _maxHeap[r] &gt; _maxHeap[ma]) ma = r;\n // 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n if (ma == i) break;\n // 두 노드를 교환한다\n _swap(i, ma);\n // 아래쪽으로 힙 재정렬을 반복한다\n i = ma;\n }\n}\n</code></pre> my_heap.rs<pre><code>/* 힙에서 원소 꺼내기 */\nfn pop(&amp;mut self) -&gt; i32 {\n // 비어 있는 경우를 처리한다\n if self.is_empty() {\n panic!(\"인덱스가 범위를 벗어났다\");\n }\n // 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n self.swap(0, self.size() - 1);\n // 노드 삭제\n let val = self.max_heap.pop().unwrap();\n // 위에서 아래로 힙을 재정렬한다\n self.sift_down(0);\n // 힙의 루트 원소를 반환한다\n val\n}\n\n/* 노드 i에서 시작하여 위에서 아래로 힙 재정렬 */\nfn sift_down(&amp;mut self, mut i: usize) {\n loop {\n // 노드 i, l, r 중 값이 가장 큰 노드를 찾아 ma로 표시한다\n let (l, r, mut ma) = (Self::left(i), Self::right(i), i);\n if l &lt; self.size() &amp;&amp; self.max_heap[l] &gt; self.max_heap[ma] {\n ma = l;\n }\n if r &lt; self.size() &amp;&amp; self.max_heap[r] &gt; self.max_heap[ma] {\n ma = r;\n }\n // 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n if ma == i {\n break;\n }\n // 두 노드를 교환한다\n self.swap(i, ma);\n // 아래쪽으로 힙 재정렬을 반복한다\n i = ma;\n }\n}\n</code></pre> my_heap.c<pre><code>/* 힙에서 원소 꺼내기 */\nint pop(MaxHeap *maxHeap) {\n // 비어 있는 경우를 처리한다\n if (isEmpty(maxHeap)) {\n printf(\"힙이 비어 있다!\");\n return INT_MAX;\n }\n // 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n swap(maxHeap, 0, size(maxHeap) - 1);\n // 노드 삭제\n int val = maxHeap-&gt;data[maxHeap-&gt;size - 1];\n maxHeap-&gt;size--;\n // 위에서 아래로 힙을 재정렬한다\n siftDown(maxHeap, 0);\n\n // 힙의 루트 원소를 반환한다\n return val;\n}\n\n/* 노드 i에서 시작하여 위에서 아래로 힙 재정렬 */\nvoid siftDown(MaxHeap *maxHeap, int i) {\n while (true) {\n // 노드 i, l, r 중 값이 가장 큰 노드를 찾아 max로 표시한다\n int l = left(maxHeap, i);\n int r = right(maxHeap, i);\n int max = i;\n if (l &lt; size(maxHeap) &amp;&amp; maxHeap-&gt;data[l] &gt; maxHeap-&gt;data[max]) {\n max = l;\n }\n if (r &lt; size(maxHeap) &amp;&amp; maxHeap-&gt;data[r] &gt; maxHeap-&gt;data[max]) {\n max = r;\n }\n // 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n if (max == i) {\n break;\n }\n // 두 노드를 교환한다\n swap(maxHeap, i, max);\n // 아래쪽으로 힙 재정렬을 반복한다\n i = max;\n }\n}\n</code></pre> my_heap.kt<pre><code>/* 힙에서 원소 꺼내기 */\nfun pop(): Int {\n // 비어 있는 경우를 처리한다\n if (isEmpty()) throw IndexOutOfBoundsException()\n // 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n swap(0, size() - 1)\n // 노드 삭제\n val _val = maxHeap.removeAt(size() - 1)\n // 위에서 아래로 힙을 재정렬한다\n siftDown(0)\n // 힙의 루트 원소를 반환한다\n return _val\n}\n\n/* 노드 i에서 시작하여 위에서 아래로 힙 재정렬 */\nfun siftDown(it: Int) {\n // Kotlin의 함수 매개변수는 변경할 수 없으므로 임시 변수를 생성한다\n var i = it\n while (true) {\n // 노드 i, l, r 중 값이 가장 큰 노드를 찾아 ma로 표시한다\n val l = left(i)\n val r = right(i)\n var ma = i\n if (l &lt; size() &amp;&amp; maxHeap[l] &gt; maxHeap[ma]) ma = l\n if (r &lt; size() &amp;&amp; maxHeap[r] &gt; maxHeap[ma]) ma = r\n // 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n if (ma == i) break\n // 두 노드를 교환한다\n swap(i, ma)\n // 아래쪽으로 힙 재정렬을 반복한다\n i = ma\n }\n}\n</code></pre> my_heap.rb<pre><code>### 힙에서 원소 꺼내기 ###\ndef pop\n # 비어 있는 경우를 처리한다\n raise IndexError, \"힙이 비어 있다\" if is_empty?\n # 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n swap(0, size - 1)\n # 노드 삭제\n val = @max_heap.pop\n # 위에서 아래로 힙을 재정렬한다\n sift_down(0)\n # 힙의 루트 원소를 반환한다\n val\nend\n\n### 노드 i에서 시작하여 위에서 아래로 힙 재정렬 ###\ndef sift_down(i)\n loop do\n # 노드 i, l, r 중 값이 가장 큰 노드를 찾아 ma로 표시한다\n l, r, ma = left(i), right(i), i\n ma = l if l &lt; size &amp;&amp; @max_heap[l] &gt; @max_heap[ma]\n ma = r if r &lt; size &amp;&amp; @max_heap[r] &gt; @max_heap[ma]\n\n # 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n break if ma == i\n\n # 두 노드를 교환한다\n swap(i, ma)\n # 아래쪽으로 힙 재정렬을 반복한다\n i = ma\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 8 장   힙","8.1   힙"],"tags":[]},{"location":"chapter_heap/heap/#813","level":2,"title":"8.1.3   힙의 주요 응용","text":"<ul> <li>우선순위 큐: 힙은 우선순위 큐를 구현할 때 가장 먼저 고려하는 자료 구조다. 삽입과 꺼내기 연산의 시간 복잡도는 모두 \\(O(\\log n)\\)이고, 힙 구성 연산은 \\(O(n)\\)으로 모두 매우 효율적이다.</li> <li>힙 정렬: 데이터가 주어지면 이를 이용해 힙을 만든 뒤 원소를 계속 꺼내어 정렬된 데이터를 얻을 수 있다. 다만 힙 정렬은 보통 더 간결한 방법으로 구현한다. 자세한 내용은 '힙 정렬' 절을 참고하자.</li> <li>가장 큰 \\(k\\) 개 원소 구하기: 대표적인 알고리즘 문제이자 흔한 응용 사례다. 예를 들어 인기 상위 10개 뉴스를 웨이보의 인기 검색어로 선정하거나, 판매량 상위 10개 상품을 고를 때 사용할 수 있다.</li> </ul>","path":["제 8 장   힙","8.1   힙"],"tags":[]},{"location":"chapter_heap/summary/","level":1,"title":"8.4   요약","text":"","path":["제 8 장   힙","8.4   요약"],"tags":[]},{"location":"chapter_heap/summary/#1","level":3,"title":"1.   핵심 정리","text":"<ul> <li>힙은 완전 이진 트리이며, 만족하는 조건에 따라 최대 힙과 최소 힙으로 나뉜다. 최대(최소) 힙의 루트 원소는 가장 큰(작은) 값이다.</li> <li>우선순위 큐는 원소를 꺼내는 우선순위가 있는 큐로 정의되며, 보통 힙으로 구현한다.</li> <li>힙의 주요 연산과 시간 복잡도는 원소 삽입 \\(O(\\log n)\\), 루트 원소 꺼내기 \\(O(\\log n)\\), 루트 원소 접근 \\(O(1)\\) 등이다.</li> <li>완전 이진 트리는 배열로 표현하기에 매우 적합하므로, 힙은 보통 배열에 저장한다.</li> <li>힙 재정렬 연산은 힙의 성질을 유지하는 데 사용하며, 삽입과 꺼내기 연산 모두에서 필요하다.</li> <li>\\(n\\) 개의 원소를 입력받아 힙을 구성하는 시간 복잡도는 \\(O(n)\\)까지 최적화할 수 있어 매우 효율적이다.</li> <li>Top-k는 대표적인 알고리즘 문제다. 힙 자료 구조를 사용하면 \\(O(n \\log k)\\)의 시간 복잡도로 효율적으로 해결할 수 있다.</li> </ul>","path":["제 8 장   힙","8.4   요약"],"tags":[]},{"location":"chapter_heap/summary/#2-q-a","level":3,"title":"2.   Q &amp; A","text":"<p>Q: 자료 구조의 '힙'과 메모리 관리의 '힙'은 같은 개념인가?</p> <p>두 개념은 서로 다르며, 우연히 이름만 '힙'으로 같을 뿐이다. 컴퓨터 시스템 메모리의 힙은 동적 메모리 할당에 사용되는 영역으로, 프로그램이 실행 중에 데이터를 저장하는 데 사용할 수 있다. 프로그램은 객체나 배열 같은 복잡한 구조를 저장하기 위해 일정량의 힙 메모리를 요청할 수 있다. 데이터가 더 이상 필요하지 않으면 메모리 누수를 막기 위해 해당 메모리를 해제해야 한다. 스택 메모리에 비해 힙 메모리는 관리와 사용에 더 주의해야 하며, 잘못 사용하면 메모리 누수나 유효하지 않은 포인터 등의 문제가 발생할 수 있다.</p>","path":["제 8 장   힙","8.4   요약"],"tags":[]},{"location":"chapter_heap/top_k/","level":1,"title":"8.3   Top-k 문제","text":"<p>문제</p> <p>길이가 \\(n\\)인 정렬되지 않은 배열 <code>nums</code>가 주어졌을 때, 배열에서 가장 큰 \\(k\\) 개의 원소를 반환하라.</p> <p>이 문제에 대해 먼저 발상이 비교적 직접적인 두 가지 풀이를 살펴보고, 이어서 더 효율적인 힙 기반 풀이를 소개한다.</p>","path":["제 8 장   힙","8.3   Top-k 문제"],"tags":[]},{"location":"chapter_heap/top_k/#831-1","level":2,"title":"8.3.1   방법 1: 순회하며 선택","text":"<p>아래 그림(8-6)과 같이 \\(k\\) 번 순회하면서 매 순회에서 각각 \\(1\\), \\(2\\), \\(\\dots\\), \\(k\\) 번째로 큰 원소를 뽑을 수 있다. 시간 복잡도는 \\(O(nk)\\)이다.</p> <p>이 방법은 \\(k \\ll n\\)인 경우에만 적합하다. \\(k\\)가 \\(n\\)에 가까워지면 시간 복잡도가 \\(O(n^2)\\)에 가까워져 매우 오래 걸리기 때문이다.</p> <p></p> <p> 그림 8-6   순회로 가장 큰 k개의 원소 찾기 </p> <p>참고</p> <p>\\(k = n\\)이면 전체가 정렬된 수열을 얻을 수 있으며, 이는 '선택 정렬' 알고리즘과 같다.</p>","path":["제 8 장   힙","8.3   Top-k 문제"],"tags":[]},{"location":"chapter_heap/top_k/#832-2","level":2,"title":"8.3.2   방법 2: 정렬","text":"<p>아래 그림(8-7)과 같이 배열 <code>nums</code>를 먼저 정렬한 뒤 맨 오른쪽의 \\(k\\) 개 원소를 반환할 수 있다. 시간 복잡도는 \\(O(n \\log n)\\)이다.</p> <p>이 방법은 필요한 것보다 더 많은 일을 한다. 가장 큰 \\(k\\) 개 원소만 찾으면 되며, 나머지 원소를 정렬할 필요는 없기 때문이다.</p> <p></p> <p> 그림 8-7   정렬로 가장 큰 k개의 원소 찾기 </p>","path":["제 8 장   힙","8.3   Top-k 문제"],"tags":[]},{"location":"chapter_heap/top_k/#833-3","level":2,"title":"8.3.3   방법 3: 힙","text":"<p>힙을 이용하면 Top-k 문제를 더 효율적으로 해결할 수 있다. 과정은 아래 그림(8-8)과 같다.</p> <ol> <li>루트 원소가 가장 작은 최소 힙을 초기화한다.</li> <li>먼저 배열의 앞 \\(k\\) 개 원소를 차례로 힙에 삽입한다.</li> <li>\\(k + 1\\) 번째 원소부터 시작하여, 현재 원소가 힙의 루트 원소보다 크면 루트 원소를 꺼내고 현재 원소를 삽입한다.</li> <li>순회를 마치면 힙에 남은 원소가 가장 큰 \\(k\\) 개 원소다.</li> </ol> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt;&lt;6&gt;&lt;7&gt;&lt;8&gt;&lt;9&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 8-8   힙으로 가장 큰 k개의 원소 찾기 </p> <p>예시 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby top_k.py<pre><code>def top_k_heap(nums: list[int], k: int) -&gt; list[int]:\n \"\"\"힙을 이용하여 배열에서 가장 큰 k개 원소 탐색\"\"\"\n # 최소 힙 초기화\n heap = []\n # 배열의 앞쪽 k개 원소를 힙에 삽입한다\n for i in range(k):\n heapq.heappush(heap, nums[i])\n # k+1번째 원소부터 힙의 길이를 k로 유지한다\n for i in range(k, len(nums)):\n # 현재 원소가 힙의 루트 원소보다 크면 루트 원소를 꺼내고 현재 원소를 힙에 삽입한다\n if nums[i] &gt; heap[0]:\n heapq.heappop(heap)\n heapq.heappush(heap, nums[i])\n return heap\n</code></pre> top_k.cpp<pre><code>/* 힙을 이용하여 배열에서 가장 큰 k개 원소 탐색 */\npriority_queue&lt;int, vector&lt;int&gt;, greater&lt;int&gt;&gt; topKHeap(vector&lt;int&gt; &amp;nums, int k) {\n // 최소 힙 초기화\n priority_queue&lt;int, vector&lt;int&gt;, greater&lt;int&gt;&gt; heap;\n // 배열의 앞쪽 k개 원소를 힙에 삽입한다\n for (int i = 0; i &lt; k; i++) {\n heap.push(nums[i]);\n }\n // k+1번째 원소부터 힙의 길이를 k로 유지한다\n for (int i = k; i &lt; nums.size(); i++) {\n // 현재 원소가 힙의 루트 원소보다 크면 루트 원소를 꺼내고 현재 원소를 힙에 삽입한다\n if (nums[i] &gt; heap.top()) {\n heap.pop();\n heap.push(nums[i]);\n }\n }\n return heap;\n}\n</code></pre> top_k.java<pre><code>/* 힙을 이용하여 배열에서 가장 큰 k개 원소 탐색 */\nQueue&lt;Integer&gt; topKHeap(int[] nums, int k) {\n // 최소 힙 초기화\n Queue&lt;Integer&gt; heap = new PriorityQueue&lt;Integer&gt;();\n // 배열의 앞쪽 k개 원소를 힙에 삽입한다\n for (int i = 0; i &lt; k; i++) {\n heap.offer(nums[i]);\n }\n // k+1번째 원소부터 힙의 길이를 k로 유지한다\n for (int i = k; i &lt; nums.length; i++) {\n // 현재 원소가 힙의 루트 원소보다 크면 루트 원소를 꺼내고 현재 원소를 힙에 삽입한다\n if (nums[i] &gt; heap.peek()) {\n heap.poll();\n heap.offer(nums[i]);\n }\n }\n return heap;\n}\n</code></pre> top_k.cs<pre><code>/* 힙을 이용하여 배열에서 가장 큰 k개 원소 탐색 */\nPriorityQueue&lt;int, int&gt; TopKHeap(int[] nums, int k) {\n // 최소 힙 초기화\n PriorityQueue&lt;int, int&gt; heap = new();\n // 배열의 앞쪽 k개 원소를 힙에 삽입한다\n for (int i = 0; i &lt; k; i++) {\n heap.Enqueue(nums[i], nums[i]);\n }\n // k+1번째 원소부터 힙의 길이를 k로 유지한다\n for (int i = k; i &lt; nums.Length; i++) {\n // 현재 원소가 힙의 루트 원소보다 크면 루트 원소를 꺼내고 현재 원소를 힙에 삽입한다\n if (nums[i] &gt; heap.Peek()) {\n heap.Dequeue();\n heap.Enqueue(nums[i], nums[i]);\n }\n }\n return heap;\n}\n</code></pre> top_k.go<pre><code>/* 힙을 이용하여 배열에서 가장 큰 k개 원소 탐색 */\nfunc topKHeap(nums []int, k int) *minHeap {\n // 최소 힙 초기화\n h := &amp;minHeap{}\n heap.Init(h)\n // 배열의 앞쪽 k개 원소를 힙에 삽입한다\n for i := 0; i &lt; k; i++ {\n heap.Push(h, nums[i])\n }\n // k+1번째 원소부터 힙의 길이를 k로 유지한다\n for i := k; i &lt; len(nums); i++ {\n // 현재 원소가 힙의 루트 원소보다 크면 루트 원소를 꺼내고 현재 원소를 힙에 삽입한다\n if nums[i] &gt; h.Top().(int) {\n heap.Pop(h)\n heap.Push(h, nums[i])\n }\n }\n return h\n}\n</code></pre> top_k.swift<pre><code>/* 힙을 이용하여 배열에서 가장 큰 k개 원소 탐색 */\nfunc topKHeap(nums: [Int], k: Int) -&gt; [Int] {\n // 최소 힙을 초기화하여 앞쪽 k개 원소로 힙을 구성한다\n var heap = Heap(nums.prefix(k))\n // k+1번째 원소부터 힙의 길이를 k로 유지한다\n for i in nums.indices.dropFirst(k) {\n // 현재 원소가 힙의 루트 원소보다 크면 루트 원소를 꺼내고 현재 원소를 힙에 삽입한다\n if nums[i] &gt; heap.min()! {\n _ = heap.removeMin()\n heap.insert(nums[i])\n }\n }\n return heap.unordered\n}\n</code></pre> top_k.js<pre><code>/* 힙에 원소 삽입 */\nfunction pushMinHeap(maxHeap, val) {\n // 원소의 부호를 반전한다\n maxHeap.push(-val);\n}\n\n/* 힙에서 원소 꺼내기 */\nfunction popMinHeap(maxHeap) {\n // 원소의 부호를 반전한다\n return -maxHeap.pop();\n}\n\n/* 힙의 루트 원소 접근 */\nfunction peekMinHeap(maxHeap) {\n // 원소의 부호를 반전한다\n return -maxHeap.peek();\n}\n\n/* 힙의 원소를 반환한다 */\nfunction getMinHeap(maxHeap) {\n // 원소의 부호를 반전한다\n return maxHeap.getMaxHeap().map((num) =&gt; -num);\n}\n\n/* 힙을 이용하여 배열에서 가장 큰 k개 원소 탐색 */\nfunction topKHeap(nums, k) {\n // 최소 힙 초기화\n // 힙의 모든 원소의 부호를 반전하여 최대 힙으로 최소 힙을 구현한다는 점에 유의한다\n const maxHeap = new MaxHeap([]);\n // 배열의 앞쪽 k개 원소를 힙에 삽입한다\n for (let i = 0; i &lt; k; i++) {\n pushMinHeap(maxHeap, nums[i]);\n }\n // k+1번째 원소부터 힙의 길이를 k로 유지한다\n for (let i = k; i &lt; nums.length; i++) {\n // 현재 원소가 힙의 루트 원소보다 크면 루트 원소를 꺼내고 현재 원소를 힙에 삽입한다\n if (nums[i] &gt; peekMinHeap(maxHeap)) {\n popMinHeap(maxHeap);\n pushMinHeap(maxHeap, nums[i]);\n }\n }\n // 힙의 원소를 반환한다\n return getMinHeap(maxHeap);\n}\n</code></pre> top_k.ts<pre><code>/* 힙에 원소 삽입 */\nfunction pushMinHeap(maxHeap: MaxHeap, val: number): void {\n // 원소의 부호를 반전한다\n maxHeap.push(-val);\n}\n\n/* 힙에서 원소 꺼내기 */\nfunction popMinHeap(maxHeap: MaxHeap): number {\n // 원소의 부호를 반전한다\n return -maxHeap.pop();\n}\n\n/* 힙의 루트 원소 접근 */\nfunction peekMinHeap(maxHeap: MaxHeap): number {\n // 원소의 부호를 반전한다\n return -maxHeap.peek();\n}\n\n/* 힙의 원소를 반환한다 */\nfunction getMinHeap(maxHeap: MaxHeap): number[] {\n // 원소의 부호를 반전한다\n return maxHeap.getMaxHeap().map((num: number) =&gt; -num);\n}\n\n/* 힙을 이용하여 배열에서 가장 큰 k개 원소 탐색 */\nfunction topKHeap(nums: number[], k: number): number[] {\n // 최소 힙 초기화\n // 힙의 모든 원소의 부호를 반전하여 최대 힙으로 최소 힙을 구현한다는 점에 유의한다\n const maxHeap = new MaxHeap([]);\n // 배열의 앞쪽 k개 원소를 힙에 삽입한다\n for (let i = 0; i &lt; k; i++) {\n pushMinHeap(maxHeap, nums[i]);\n }\n // k+1번째 원소부터 힙의 길이를 k로 유지한다\n for (let i = k; i &lt; nums.length; i++) {\n // 현재 원소가 힙의 루트 원소보다 크면 루트 원소를 꺼내고 현재 원소를 힙에 삽입한다\n if (nums[i] &gt; peekMinHeap(maxHeap)) {\n popMinHeap(maxHeap);\n pushMinHeap(maxHeap, nums[i]);\n }\n }\n // 힙의 원소를 반환한다\n return getMinHeap(maxHeap);\n}\n</code></pre> top_k.dart<pre><code>/* 힙을 이용하여 배열에서 가장 큰 k개 원소 탐색 */\nMinHeap topKHeap(List&lt;int&gt; nums, int k) {\n // 최소 힙을 초기화하고 배열의 앞쪽 k개 원소를 힙에 삽입한다\n MinHeap heap = MinHeap(nums.sublist(0, k));\n // k+1번째 원소부터 힙의 길이를 k로 유지한다\n for (int i = k; i &lt; nums.length; i++) {\n // 현재 원소가 힙의 루트 원소보다 크면 루트 원소를 꺼내고 현재 원소를 힙에 삽입한다\n if (nums[i] &gt; heap.peek()) {\n heap.pop();\n heap.push(nums[i]);\n }\n }\n return heap;\n}\n</code></pre> top_k.rs<pre><code>/* 힙을 이용하여 배열에서 가장 큰 k개 원소 탐색 */\nfn top_k_heap(nums: Vec&lt;i32&gt;, k: usize) -&gt; BinaryHeap&lt;Reverse&lt;i32&gt;&gt; {\n // BinaryHeap은 최대 힙이므로 Reverse로 원소의 순서를 뒤집어 최소 힙을 구현한다\n let mut heap = BinaryHeap::&lt;Reverse&lt;i32&gt;&gt;::new();\n // 배열의 앞쪽 k개 원소를 힙에 삽입한다\n for &amp;num in nums.iter().take(k) {\n heap.push(Reverse(num));\n }\n // k+1번째 원소부터 힙의 길이를 k로 유지한다\n for &amp;num in nums.iter().skip(k) {\n // 현재 원소가 힙의 루트 원소보다 크면 루트 원소를 꺼내고 현재 원소를 힙에 삽입한다\n if num &gt; heap.peek().unwrap().0 {\n heap.pop();\n heap.push(Reverse(num));\n }\n }\n heap\n}\n</code></pre> top_k.c<pre><code>/* 힙에 원소 삽입 */\nvoid pushMinHeap(MaxHeap *maxHeap, int val) {\n // 원소의 부호를 반전한다\n push(maxHeap, -val);\n}\n\n/* 힙에서 원소 꺼내기 */\nint popMinHeap(MaxHeap *maxHeap) {\n // 원소의 부호를 반전한다\n return -pop(maxHeap);\n}\n\n/* 힙의 루트 원소 접근 */\nint peekMinHeap(MaxHeap *maxHeap) {\n // 원소의 부호를 반전한다\n return -peek(maxHeap);\n}\n\n/* 힙의 원소를 반환한다 */\nint *getMinHeap(MaxHeap *maxHeap) {\n // 힙의 모든 원소의 부호를 반전하여 res 배열에 저장한다\n int *res = (int *)malloc(maxHeap-&gt;size * sizeof(int));\n for (int i = 0; i &lt; maxHeap-&gt;size; i++) {\n res[i] = -maxHeap-&gt;data[i];\n }\n return res;\n}\n\n/* 힙의 원소를 반환한다 */\nint *getMinHeap(MaxHeap *maxHeap) {\n // 힙의 모든 원소의 부호를 반전하여 res 배열에 저장한다\n int *res = (int *)malloc(maxHeap-&gt;size * sizeof(int));\n for (int i = 0; i &lt; maxHeap-&gt;size; i++) {\n res[i] = -maxHeap-&gt;data[i];\n }\n return res;\n}\n\n// 힙으로 배열에서 가장 큰 k개 원소를 찾는 함수\nint *topKHeap(int *nums, int sizeNums, int k) {\n // 최소 힙 초기화\n // 힙의 모든 원소의 부호를 반전하여 최대 힙으로 최소 힙을 구현한다는 점에 유의한다\n int *empty = (int *)malloc(0);\n MaxHeap *maxHeap = newMaxHeap(empty, 0);\n // 배열의 앞쪽 k개 원소를 힙에 삽입한다\n for (int i = 0; i &lt; k; i++) {\n pushMinHeap(maxHeap, nums[i]);\n }\n // k+1번째 원소부터 힙의 길이를 k로 유지한다\n for (int i = k; i &lt; sizeNums; i++) {\n // 현재 원소가 힙의 루트 원소보다 크면 루트 원소를 꺼내고 현재 원소를 힙에 삽입한다\n if (nums[i] &gt; peekMinHeap(maxHeap)) {\n popMinHeap(maxHeap);\n pushMinHeap(maxHeap, nums[i]);\n }\n }\n int *res = getMinHeap(maxHeap);\n // 메모리 해제\n delMaxHeap(maxHeap);\n return res;\n}\n</code></pre> top_k.kt<pre><code>/* 힙을 이용하여 배열에서 가장 큰 k개 원소 탐색 */\nfun topKHeap(nums: IntArray, k: Int): Queue&lt;Int&gt; {\n // 최소 힙 초기화\n val heap = PriorityQueue&lt;Int&gt;()\n // 배열의 앞쪽 k개 원소를 힙에 삽입한다\n for (i in 0..&lt;k) {\n heap.offer(nums[i])\n }\n // k+1번째 원소부터 힙의 길이를 k로 유지한다\n for (i in k..&lt;nums.size) {\n // 현재 원소가 힙의 루트 원소보다 크면 루트 원소를 꺼내고 현재 원소를 힙에 삽입한다\n if (nums[i] &gt; heap.peek()) {\n heap.poll()\n heap.offer(nums[i])\n }\n }\n return heap\n}\n</code></pre> top_k.rb<pre><code>### 힙을 이용하여 배열에서 가장 큰 k개 원소 탐색 ###\ndef top_k_heap(nums, k)\n # 최소 힙 초기화\n # 힙의 모든 원소의 부호를 반전하여 최대 힙으로 최소 힙을 구현한다는 점에 유의한다\n max_heap = MaxHeap.new([])\n\n # 배열의 앞쪽 k개 원소를 힙에 삽입한다\n for i in 0...k\n push_min_heap(max_heap, nums[i])\n end\n\n # k+1번째 원소부터 힙의 길이를 k로 유지한다\n for i in k...nums.length\n # 현재 원소가 힙의 루트 원소보다 크면 루트 원소를 꺼내고 현재 원소를 힙에 삽입한다\n if nums[i] &gt; peek_min_heap(max_heap)\n pop_min_heap(max_heap)\n push_min_heap(max_heap, nums[i])\n end\n end\n\n get_min_heap(max_heap)\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>삽입과 꺼내기를 총 \\(n\\) 차례 수행하고 힙의 최대 길이는 \\(k\\)이므로, 시간 복잡도는 \\(O(n \\log k)\\)이다. 이 방법은 매우 효율적이다. \\(k\\)가 작으면 시간 복잡도가 \\(O(n)\\)에 가까워지고, \\(k\\)가 커도 \\(O(n \\log n)\\)을 넘지 않는다.</p> <p>또한 이 방법은 동적으로 들어오는 데이터 스트림에도 적합하다. 데이터가 계속 추가될 때 힙 안의 원소를 지속적으로 관리하면 가장 큰 \\(k\\) 개 원소를 동적으로 갱신할 수 있다.</p>","path":["제 8 장   힙","8.3   Top-k 문제"],"tags":[]},{"location":"chapter_hello_algo/","level":1,"title":"머리말","text":"<p>몇 년 전, 나는 리트코드에 『剑指 Offer』(IT 대기업 코딩 면접 대비서)의 문제 풀이 시리즈를 공유했고 많은 독자의 격려와 지지를 받았다. 독자들과 이야기하면서 가장 자주 받은 질문은 “알고리즘 공부를 어떻게 시작하면 좋을까요?”였다. 점차 나는 이 질문에 깊은 관심을 갖게 되었다.</p> <p>아무것도 모르는 상태에서 무작정 문제를 푸는 방식이 가장 인기 있는 방법인 듯하다. 간단하고 직접적이며 효과도 있다. 하지만 문제 풀이는 ‘지뢰 찾기’ 게임과 비슷하다. 스스로 학습하는 능력이 뛰어난 사람은 지뢰를 하나씩 찾아 제거하며 앞으로 나아가지만, 기초가 부족한 사람은 지뢰를 거듭 밟으며 좌절하고, 결국 앞으로 나아가지 못한 채 주저앉기 쉽다. 알고리즘 교재를 처음부터 끝까지 읽는 것도 흔한 방법이다. 그러나 취업을 준비하는 사람에게는 졸업 논문 작성, 이력서 제출, 필기시험과 면접 준비만으로도 대부분의 에너지가 소모되므로, 두꺼운 책을 파고드는 일이 큰 도전이 되곤 한다.</p> <p>비슷한 어려움을 겪고 있다면, 다행히도 이 책이 여러분을 '찾아왔다'고 말할 수 있을 것이다. 이 책은 앞서 던진 질문에 대한 나의 답이다. 최선의 해답은 아닐지라도, 적어도 앞으로 나아가는 데 도움이 되는 시도라고 생각한다. 물론 이 책 한 권만으로 곧바로 취업에 성공할 수는 없을 것이다. 하지만 자료 구조와 알고리즘의 '지식 지도'를 탐험하도록 안내하고, 여러 ‘지뢰’의 모양과 크기, 분포를 이해하며 다양한 ‘지뢰 제거 방법’을 익힐 수 있도록 도울 것이다. 이를 바탕으로 알고리즘 문제를 풀거나 기술 문헌을 읽는 일이 한층 수월해지고, 차근차근 체계적인 지식을 쌓아 나갈 수 있으리라 믿는다.</p> <p>나는 파인만 교수의 말에 깊이 공감한다. “지식은 공짜가 아니다. 주의를 기울여야 한다.” 이런 의미에서 이 책은 완전히 ‘무료’인 것은 아니다. 여러분이 이 책에 쏟는 소중한 ‘주의와 관심’을 헛되게 하지 않도록, 나 역시 최대한의 ‘주의와 관심’을 기울여 집필에 최선을 다하고자 한다.</p> <p>내 학식과 재능이 부족함을 잘 알고 있다. 책의 내용을 한동안 다듬어 왔지만 여전히 많은 오류가 있을 것이다. 선생님과 학생 여러분의 비판과 지적을 부탁드린다.</p> <p></p> Hello, 알고리즘! <p>컴퓨터의 등장은 세상을 크게 바꾸었다. 빠른 계산 능력과 뛰어난 프로그래밍 가능성을 갖춘 컴퓨터는 알고리즘을 실행하고 데이터를 처리하는 이상적인 수단이 되었다. 비디오 게임의 사실적인 화면, 자율 주행의 지능적인 판단, AlphaGo의 멋진 대국, ChatGPT의 자연스러운 상호작용까지, 이 모든 응용은 컴퓨터 위에서 알고리즘이 정교하게 구현된 결과다.</p> <p>사실 컴퓨터가 등장하기 전부터 알고리즘과 자료 구조는 세상 곳곳에 존재했다. 초기의 알고리즘은 고대의 셈법이나 도구 제작 절차처럼 비교적 단순했다. 문명이 발전하면서 알고리즘도 점점 더 정교하고 복잡해졌다. 장인의 놀라운 솜씨에서 생산력을 높여 준 산업 제품에 이르기까지, 나아가 우주의 다양한 현상을 설명하는 과학 법칙에 이르기까지, 평범하거나 경이로운 거의 모든 것의 이면에는 정교한 알고리즘적 사고가 숨어 있다.</p> <p>자료 구조 역시 어디에나 있다. 크게는 사회 관계망부터 작게는 지하철 노선까지 많은 시스템을 ‘그래프’로 모델링할 수 있다. 국가부터 가정에 이르기까지 사회의 주요 조직 형태는 ‘트리’의 특성을 보인다. 겨울옷은 ‘스택’과 같아서 가장 먼저 입은 옷을 가장 나중에 벗는다. 셔틀콕 통은 한쪽으로 넣고 다른 쪽으로 꺼낸다는 점에서 ‘큐’와 닮았다. 사전은 원하는 표제어를 빠르게 찾을 수 있다는 점에서 ‘해시 테이블’과 비슷하다.</p> <p>이 책은 명확하고 이해하기 쉬운 애니메이션 그림과 실행 가능한 코드 예제를 통해 독자가 알고리즘과 자료 구조의 핵심 개념을 이해하고 직접 프로그래밍으로 구현할 수 있도록 돕는 것을 목표로 한다. 이를 바탕으로 복잡한 세상 속에서 알고리즘이 생생하게 드러나는 모습을 보여 주며 알고리즘의 아름다움을 전하고자 한다. 이 책이 여러분에게 도움이 되기를 바란다!</p>","path":["머리말"],"tags":[]},{"location":"chapter_introduction/","level":1,"title":"제 1 장   알고리즘과의 첫 만남","text":"<p>이 장을 시작하며</p> <p>한 소녀가 데이터와 어우러져 우아하게 춤을 춘다. 치맛자락에는 알고리즘의 선율이 나부낀다.</p> <p>소녀가 함께 춤추자고 손을 내민다. 그 발걸음을 따라 논리와 아름다움으로 가득한 알고리즘의 세계에 들어서 보자.</p>","path":["제 1 장   알고리즘과의 첫 만남"],"tags":[]},{"location":"chapter_introduction/#_1","level":2,"title":"이 장의 내용","text":"<ul> <li>1.1   어디에나 있는 알고리즘</li> <li>1.2   알고리즘이란 무엇인가</li> <li>1.3   요약</li> </ul>","path":["제 1 장   알고리즘과의 첫 만남"],"tags":[]},{"location":"chapter_introduction/algorithms_are_everywhere/","level":1,"title":"1.1   어디에나 있는 알고리즘","text":"<p>‘알고리즘’이라는 말을 들으면 자연스럽게 수학을 떠올리게 된다. 하지만 실제로 많은 알고리즘은 복잡한 수학보다는 기본적인 논리에 더 많이 의존하며, 이러한 논리는 일상생활 곳곳에서 찾아볼 수 있다.</p> <p>알고리즘을 본격적으로 살펴보기 전에 흥미로운 사실 하나를 소개하고자 한다. 여러분은 이미 자신도 모르게 많은 알고리즘을 익혔고, 일상생활에서 자연스럽게 사용하고 있다. 몇 가지 구체적인 예를 통해 이 사실을 확인해 보자.</p> <p>예 1: 사전에서 단어 찾기. 중국어 사전에서는 각 한자에 대응하는 병음이 있고, 표제어는 병음의 알파벳순으로 배열되어 있다. 병음의 첫 글자가 \\(r\\)인 한자를 찾는다고 가정하면, 보통 아래 그림(1-1)과 같은 방법을 사용한다.</p> <ol> <li>사전의 중간쯤을 펼쳐 그 페이지의 병음 첫 글자를 확인한다. 첫 글자가 \\(m\\)이라고 가정하자.</li> <li>병음의 알파벳순에서 \\(r\\)은 \\(m\\) 뒤에 있으므로 사전의 앞쪽 절반을 제외하고 탐색 범위를 뒤쪽 절반으로 좁힌다.</li> <li>병음의 첫 글자가 \\(r\\)인 페이지를 찾을 때까지 단계 <code>1.</code>과 단계 <code>2.</code>를 반복한다.</li> </ol> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 1-1   사전에서 단어를 찾는 단계 </p> <p>초등학생도 익히는 사전 찾기는 사실 유명한 ‘이진 탐색’ 알고리즘이다. 자료 구조의 관점에서 사전은 정렬된 ‘배열’로 볼 수 있고, 알고리즘의 관점에서 앞서 살펴본 사전 찾기의 일련의 동작은 ‘이진 탐색’으로 볼 수 있다.</p> <p>예 2: 카드 정리하기. 카드 게임을 할 때는 매번 손에 든 카드를 작은 수부터 큰 수 순서로 정리한다. 그 과정은 아래 그림(1-2)과 같다.</p> <ol> <li>카드를 ‘정렬된 부분’과 ‘정렬되지 않은 부분’으로 나누고, 처음에는 맨 왼쪽 카드 1장이 이미 정렬되어 있다고 가정한다.</li> <li>정렬되지 않은 부분에서 카드 한 장을 꺼내 정렬된 부분의 올바른 위치에 끼워 넣는다. 그러면 맨 왼쪽 카드 2장이 정렬된 상태가 된다.</li> <li>단계 <code>2.</code>를 반복하여 매번 정렬되지 않은 부분의 카드 한 장을 정렬된 부분에 끼워 넣는다. 모든 카드가 정렬될 때까지 계속한다.</li> </ol> <p></p> <p> 그림 1-2   카드를 정렬하는 단계 </p> <p>이러한 카드 정리 방법은 본질적으로 ‘삽입 정렬’ 알고리즘이며, 작은 데이터 집합을 처리할 때 매우 효율적이다. 많은 프로그래밍 언어의 정렬 라이브러리 함수에서도 삽입 정렬이 사용된다.</p> <p>예 3: 거스름돈 주기. 마트에서 \\(69\\)위안어치 물건을 사고 계산원에게 \\(100\\)위안을 냈다고 가정하자. 계산원은 \\(31\\)위안을 거슬러 주어야 하며, 자연스럽게 아래 그림(1-3)과 같은 과정을 떠올린다.</p> <ol> <li>선택할 수 있는 것은 액면가가 \\(31\\)위안보다 작은 화폐로, \\(1\\)위안, \\(5\\)위안, \\(10\\)위안, \\(20\\)위안이 있다.</li> <li>선택지 중 가장 큰 \\(20\\)위안을 꺼내면 \\(31 - 20 = 11\\)위안이 남는다.</li> <li>남은 선택지 중 가장 큰 \\(10\\)위안을 꺼내면 \\(11 - 10 = 1\\)위안이 남는다.</li> <li>남은 선택지 중 가장 큰 \\(1\\)위안을 꺼내면 \\(1 - 1 = 0\\)위안이 남는다.</li> <li>거스름돈을 모두 주었다. 그 구성은 \\(20 + 10 + 1 = 31\\)위안이다.</li> </ol> <p></p> <p> 그림 1-3   거스름돈을 주는 과정 </p> <p>위 과정에서는 각 단계마다 현재 가장 좋아 보이는 선택, 즉 가능한 한 액면가가 큰 화폐를 고른다. 그 결과 거스름돈을 줄 수 있는 방법을 얻는다. 자료 구조와 알고리즘의 관점에서 이 방법은 본질적으로 ‘탐욕’ 알고리즘이다.</p> <p>작게는 요리 한 가지를 만드는 일부터 크게는 성간 항해에 이르기까지, 거의 모든 문제의 해결에는 알고리즘이 필요하다. 컴퓨터의 등장으로 우리는 프로그래밍을 통해 자료 구조를 메모리에 저장하고, CPU와 GPU가 알고리즘을 실행하도록 코드를 작성할 수 있게 되었다. 이로써 현실의 문제를 컴퓨터로 옮겨 다양한 복잡한 문제를 더 효율적으로 해결할 수 있다.</p> <p>참고</p> <p>자료 구조, 알고리즘, 배열, 이진 탐색 등의 개념이 아직 막연하게 느껴져도 계속 읽어 나가면 된다. 이 책이 자료 구조와 알고리즘의 지식 세계로 안내할 것이다.</p>","path":["제 1 장   알고리즘과의 첫 만남","1.1   어디에나 있는 알고리즘"],"tags":[]},{"location":"chapter_introduction/summary/","level":1,"title":"1.3   요약","text":"","path":["제 1 장   알고리즘과의 첫 만남","1.3   요약"],"tags":[]},{"location":"chapter_introduction/summary/#1","level":3,"title":"1.   핵심 정리","text":"<ul> <li>알고리즘은 일상생활 어디에나 있으며, 손에 닿지 않는 어려운 지식이 아니다. 사실 우리는 이미 자신도 모르게 많은 알고리즘을 익혀 크고 작은 생활 속 문제를 해결하고 있다.</li> <li>사전에서 단어를 찾는 원리는 이진 탐색 알고리즘과 같다. 이진 탐색에는 분할 정복이라는 중요한 알고리즘적 사고가 담겨 있다.</li> <li>카드를 정리하는 과정은 삽입 정렬 알고리즘과 매우 비슷하다. 삽입 정렬은 작은 데이터 집합을 정렬하는 데 적합하다.</li> <li>거스름돈을 주는 과정은 본질적으로 탐욕 알고리즘이다. 각 단계에서 현재 가장 좋아 보이는 선택을 한다.</li> <li>알고리즘은 유한한 시간 안에 특정 문제를 해결하는 명령 또는 작업 단계의 집합이고, 자료 구조는 컴퓨터에서 데이터를 구성하고 저장하는 방식이다.</li> <li>자료 구조와 알고리즘은 긴밀하게 연결되어 있다. 자료 구조는 알고리즘의 토대이며, 알고리즘은 자료 구조에 생명력을 불어넣는다.</li> <li>자료 구조와 알고리즘은 블록 조립에 비유할 수 있다. 블록은 데이터를, 블록의 모양과 연결 방식 등은 자료 구조를, 블록을 조립하는 단계는 알고리즘을 나타낸다.</li> </ul>","path":["제 1 장   알고리즘과의 첫 만남","1.3   요약"],"tags":[]},{"location":"chapter_introduction/summary/#2-q-a","level":3,"title":"2.   Q &amp; A","text":"<p>Q: 프로그래머로 일하면서 직접 알고리즘으로 문제를 해결해 본 적이 없다. 자주 쓰는 알고리즘은 프로그래밍 언어에서 이미 제공하므로 그대로 사용하면 된다. 그렇다면 업무에서 다루는 문제가 아직 알고리즘이 필요할 정도는 아니라는 뜻인가?</p> <p>구체적인 업무 기술을 무술의 ‘동작’에 비유한다면, 기초 과목은 ‘내공’에 더 가깝다.</p> <p>알고리즘을 비롯한 기초 과목을 공부하는 의미는 업무에서 이를 처음부터 직접 구현하는 데 있지 않다고 생각한다. 배운 지식을 바탕으로 문제에 전문적으로 대응하고 판단하여 업무의 전반적인 품질을 높이는 데 의미가 있다. 간단한 예로, 프로그래밍 언어마다 내장 정렬 함수를 제공한다.</p> <ul> <li>자료 구조와 알고리즘을 배우지 않았다면 어떤 데이터든 이 정렬 함수에 넘길 수 있다. 실행도 원활하고 성능도 괜찮으니 별문제가 없어 보인다.</li> <li>하지만 알고리즘을 배웠다면 내장 정렬 함수의 시간 복잡도가 \\(O(n \\log n)\\)임을 안다. 주어진 데이터가 학번처럼 자릿수가 고정된 정수라면 더 효율적인 ‘기수 정렬’을 사용하여 시간 복잡도를 \\(O(nk)\\)로 낮출 수 있다. 여기서 \\(k\\)는 자릿수다. 데이터의 양이 많을 때는 이렇게 절약한 실행 시간이 비용 절감이나 사용자 경험 개선 등 큰 가치를 만들어 낼 수 있다.</li> </ul> <p>공학 분야의 많은 문제는 최적해를 얻기 어렵고, 그저 ‘어느 정도’ 해결된 상태로 남아 있다. 문제의 난이도는 문제 자체의 성질뿐 아니라 그 문제를 바라보는 사람의 지식에도 달려 있다. 지식이 체계적이고 경험이 많을수록 문제를 더 깊이 분석하고 더 우아하게 해결할 수 있다.</p> <p>Q: 이제는 거의 모든 코드를 AI 에이전트가 작성하는데, 그래도 알고리즘을 배워야 하는가?</p> <p>리무(李沐)의 말을 인용하자면, “에이전트는 능력 증폭기다. 능력에 10을 곱하는 것이지, 10을 더하는 덧셈기가 아니다.”</p> <p>AI가 능력 증폭기라면 증폭의 바탕이 되는 ‘능력’도 중요하다. 특히 두 가지 능력을 쌓을 가치가 있다고 생각한다. 첫째는 표현 능력, 특히 AI와 협업하는 능력이다. 목표를 명확히 설명하고 배경을 충분히 제공하며 잘못된 방향은 제때 바로잡아야 한다. AI에게 문제를 설명하는 일은 스스로 문제를 명확히 이해하는 데도 도움이 된다. 둘째는 기초 지식이다. 세부 사항은 AI가 보완할 수 있지만, 핵심을 짚어 질문하고 옳고 그름을 판단하며 AI의 출력을 유용한 결과물로 만들려면 기본 개념은 스스로 이해해야 한다.</p> <p>AI의 특별한 점은 과거의 도구처럼 기능의 경계가 뚜렷하지 않다는 것이다. AI는 물과 비슷해서 누구의 손에 들어가느냐에 따라 다른 모습을 띤다. 사람의 능력이 뛰어나고 안목이 좋을수록 AI의 증폭 효과도 더욱 뚜렷해진다.</p>","path":["제 1 장   알고리즘과의 첫 만남","1.3   요약"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/","level":1,"title":"1.2   알고리즘이란 무엇인가","text":"","path":["제 1 장   알고리즘과의 첫 만남","1.2   알고리즘이란 무엇인가"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#121","level":2,"title":"1.2.1   알고리즘의 정의","text":"<p>알고리즘(algorithm)은 유한한 시간 안에 특정 문제를 해결하는 명령 또는 작업 단계의 집합이며, 다음과 같은 특성을 갖는다.</p> <ul> <li>문제가 명확하고 입력과 출력이 분명하게 정의되어 있다.</li> <li>실행 가능하며, 유한한 단계와 시간, 메모리 공간 안에서 완료할 수 있다.</li> <li>각 단계의 의미가 명확하여 입력과 실행 조건이 같으면 항상 같은 출력을 얻는다.</li> </ul>","path":["제 1 장   알고리즘과의 첫 만남","1.2   알고리즘이란 무엇인가"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#122","level":2,"title":"1.2.2   자료 구조의 정의","text":"<p>자료 구조(data structure)는 데이터를 구성하고 저장하는 방식으로, 데이터의 내용, 데이터 사이의 관계, 데이터를 조작하는 방법을 포함한다. 자료 구조는 다음과 같은 목표를 고려하여 설계한다.</p> <ul> <li>컴퓨터 메모리를 절약하도록 공간 사용량을 최대한 줄인다.</li> <li>데이터 접근, 추가, 삭제, 갱신 등의 연산을 최대한 빠르게 수행한다.</li> <li>알고리즘이 효율적으로 실행되도록 간결한 데이터 표현과 논리적 정보를 제공한다.</li> </ul> <p>자료 구조를 설계하는 과정에는 수많은 절충이 따른다. 어느 한 측면을 개선하려면 다른 측면을 양보해야 할 때가 많다. 두 가지 예를 살펴보자.</p> <ul> <li>연결 리스트는 배열보다 데이터를 추가하거나 삭제하기가 편리하지만, 데이터에 접근하는 속도는 느리다.</li> <li>그래프는 연결 리스트보다 더 풍부한 논리적 정보를 제공하지만, 더 많은 메모리 공간이 필요하다.</li> </ul>","path":["제 1 장   알고리즘과의 첫 만남","1.2   알고리즘이란 무엇인가"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#123","level":2,"title":"1.2.3   자료 구조와 알고리즘의 관계","text":"<p>아래 그림(1-4)과 같이 자료 구조와 알고리즘은 밀접하게 연결되어 있다. 그 관계는 다음 세 가지 측면에서 살펴볼 수 있다.</p> <ul> <li>자료 구조는 알고리즘의 토대다. 자료 구조는 알고리즘에 체계적으로 저장된 데이터와 이를 조작하는 방법을 제공한다.</li> <li>알고리즘은 자료 구조에 생명력을 불어넣는다. 자료 구조 자체는 데이터 정보만 저장하며, 알고리즘과 결합해야 특정 문제를 해결할 수 있다.</li> <li>같은 알고리즘도 서로 다른 자료 구조를 바탕으로 구현할 수 있지만, 실행 효율은 크게 달라질 수 있다. 따라서 적절한 자료 구조를 선택하는 것이 중요하다.</li> </ul> <p></p> <p> 그림 1-4   자료 구조와 알고리즘의 관계 </p> <p>자료 구조와 알고리즘은 아래 그림(1-5)의 조립 블록에 비유할 수 있다. 블록 한 세트에는 여러 부품뿐 아니라 자세한 조립 설명서도 들어 있다. 설명서를 따라 차례대로 조립하면 멋진 블록 모형을 만들 수 있다.</p> <p></p> <p> 그림 1-5   블록 조립 </p> <p>구체적인 대응 관계는 아래 표(1-1)와 같다.</p> <p> 표 1-1   자료 구조와 알고리즘을 블록 조립에 비유하기 </p> 자료 구조와 알고리즘 블록 조립 입력 데이터 조립하기 전의 블록 자료 구조 모양, 크기, 연결 방식 등 블록의 구성 형태 알고리즘 블록을 원하는 형태로 조립하는 일련의 작업 단계 출력 데이터 완성된 블록 모형 <p>자료 구조와 알고리즘은 프로그래밍 언어에 종속되지 않는다. 이 때문에 이 책에서는 여러 프로그래밍 언어로 작성한 구현을 제공할 수 있다.</p> <p>관례적으로 사용하는 줄임말</p> <p>실제로 이야기할 때는 ‘자료 구조와 알고리즘’을 흔히 ‘알고리즘’이라고 줄여 부른다. 예를 들어 널리 알려진 리트코드의 알고리즘 문제는 실제로 자료 구조와 알고리즘 두 분야의 지식을 함께 평가한다.</p>","path":["제 1 장   알고리즘과의 첫 만남","1.2   알고리즘이란 무엇인가"],"tags":[]},{"location":"chapter_preface/","level":1,"title":"제 0 장   들어가며","text":"<p>이 장을 시작하며</p> <p>알고리즘은 아름다운 교향곡과 같다. 코드 한 줄 한 줄이 리듬처럼 흐른다.</p> <p>이 책이 여러분의 머릿속에 잔잔히 울려 퍼져, 특별하고 깊은 선율을 남기기를 바란다.</p>","path":["제 0 장   들어가며"],"tags":[]},{"location":"chapter_preface/#_1","level":2,"title":"이 장의 내용","text":"<ul> <li>0.1   이 책에 대하여</li> <li>0.2   이 책을 활용하는 방법</li> <li>0.3   요약</li> </ul>","path":["제 0 장   들어가며"],"tags":[]},{"location":"chapter_preface/about_the_book/","level":1,"title":"0.1   이 책에 대하여","text":"<p>이 프로젝트는 오픈 소스로 공개되는 무료 자료 구조와 알고리즘 입문서를 만드는 것을 목표로 한다. 처음 배우는 사람도 쉽게 접근할 수 있는 책을 지향한다.</p> <ul> <li>책 전체에 애니메이션 그림을 사용하여 내용을 명확하고 이해하기 쉽게 설명한다. 난이도를 완만하게 높여 가며 초보자가 자료 구조와 알고리즘의 지식 지도를 탐험하도록 안내한다.</li> <li>소스 코드를 클릭 한 번으로 실행할 수 있다. 독자가 실습을 통해 프로그래밍 능력을 키우고 알고리즘의 동작 원리와 자료 구조의 내부 구현을 이해하도록 돕는다.</li> <li>독자들이 서로 도우며 공부하기를 권장한다. 댓글에서 질문하고 의견을 나누며 함께 성장할 수 있다.</li> </ul>","path":["제 0 장   들어가며","0.1   이 책에 대하여"],"tags":[]},{"location":"chapter_preface/about_the_book/#011","level":2,"title":"0.1.1   대상 독자","text":"<p>알고리즘을 처음 접하는 입문자이거나, 문제를 풀어 본 경험은 조금 있지만 자료 구조와 알고리즘에 대한 이해가 막연해 알 것 같다가도 모르겠다는 생각이 든다면, 이 책은 바로 여러분을 위한 책이다!</p> <p>이미 상당한 수의 문제를 풀었고 대부분의 문제 유형에 익숙하다면, 이 책을 통해 알고리즘 지식을 되짚고 체계적으로 정리할 수 있다. 저장소의 소스 코드는 ‘문제 풀이 도구 모음’이나 ‘알고리즘 사전’처럼 활용할 수 있다.</p> <p>알고리즘의 ‘고수’라면 소중한 의견을 보내 주거나 집필에 함께 참여해 주기를 기대한다.</p> <p>선수 지식</p> <p>적어도 한 가지 언어의 프로그래밍 기초를 알고, 간단한 코드를 읽고 작성할 수 있어야 한다.</p>","path":["제 0 장   들어가며","0.1   이 책에 대하여"],"tags":[]},{"location":"chapter_preface/about_the_book/#012","level":2,"title":"0.1.2   내용 구성","text":"<p>이 책의 주요 내용은 아래 그림(0-1)과 같다.</p> <ul> <li>복잡도 분석: 자료 구조와 알고리즘을 평가하는 기준과 방법을 다룬다. 시간 복잡도와 공간 복잡도의 계산 방법, 대표적인 유형과 예제 등을 살펴본다.</li> <li>자료 구조: 기본 자료형과 자료 구조의 분류 방법을 다룬다. 배열, 연결 리스트, 스택, 큐, 해시 테이블, 트리, 힙, 그래프 등의 정의, 장단점, 자주 쓰는 연산, 대표적인 유형과 응용, 구현 방법 등을 살펴본다.</li> <li>알고리즘: 탐색, 정렬, 분할 정복, 백트래킹, 동적 계획법, 탐욕 등의 정의, 장단점, 효율성, 활용 상황, 문제 해결 단계와 예제 문제 등을 살펴본다.</li> </ul> <p></p> <p> 그림 0-1   이 책의 주요 내용 </p>","path":["제 0 장   들어가며","0.1   이 책에 대하여"],"tags":[]},{"location":"chapter_preface/about_the_book/#013","level":2,"title":"0.1.3   감사의 말","text":"<p>이 책은 오픈 소스 커뮤니티의 수많은 기여자가 함께 노력한 덕분에 계속 발전하고 있다. 시간과 노력을 들여 준 모든 집필 참여자에게 감사드린다. 참여자는 다음과 같다(GitHub가 자동으로 생성한 순서): krahets, coderonion, Gonglja, nuomi1, Reanon, justin-tse, hpstory, danielsss, curtishd, night-cruise, S-N-O-R-L-A-X, rongyi, msk397, gvenusleo, khoaxuantu, rivertwilight, K3v123, gyt95, zhuoqinyue, yuelinxin, Zuoxun, mingXta, Phoenix0415, FangYuan33, GN-Yu, longsizhuo, pengchzn, QiLOL, Cathay-Chen, guowei-gong, xBLACKICEx, IsChristina, JoseHung, qualifier1024, hello-ikun, magentaqin, Guanngxu, thomasq0, sunshinesDL, L-Super, Transmigration-zhou, WSL0809, Slone123c, lhxsm, yuan0221, what-is-me, theNefelibatas, Shyam-Chen, sangxiaai, longranger2, codeberg-user, xiongsp, JeffersonHuang, prinpal, seven1240, Wonderdch, malone6, xiaomiusa87, gaofer, bluebean-cloud, a16su, SamJin98, hongyun-robot, nanlei, XiaChuerwu, yd-j, iron-irax, mgisr, steventimes, junminhong, heshuyue, danny900714, Nigh, Dr-XYZ, MolDuM, XC-Zero, reeswell, PXG-XPG, NI-SW, Horbin-Magician, Enlightenus, YangXuanyi, xjr7670, beatrix-chan, DullSword, qq909244296, iStig, boloboloda, hts0000, gledfish, fbigm, echo1937, jiaxianhua, wenjianmin, keshida, kilikilikid, lclc6, lwbaptx, linyejoe2, liuxjerry, szu17dmy, dshlstarr, Yucao-cy, coderlef, czruby, bongbongbakudan, beintentional, ZongYangL, ZhongYuuu, ZhongGuanbin, hezhizhen, linzeyan, ZJKung, JTCPOWI, KawaiiAsh, luluxia, xb534, ztkuaikuai, yw-1021, ElaBosak233, baagod, zhouLion, yishangzhang, yi427, yanedie, yabo083, weibk, wangwang105, th1nk3r-ing, tao363, 4yDX3906, syd168, sslmj2020, smilelsb, siqyka, selear, sdshaoda, Xi-Row, popozhu, nuquist19, noobcodemaker, XiaoK29, chadyi, lyl625760, lucaswangdev, llql1211, 0130w, shanghai-Jerry, EJackYang, Javesun99, eltociear, lipusheng, KNChiu, BlindTerran, ShiMaRing, lovelock, FreddieLi, FloranceYeh, fanchenggang, gltianwen, goerll, nedchu, curly210102, CuB3y0nd, KraHsu, CarrotDLaw, youshaoXG, bubble9um, Asashishi, Asa0oo0o0o, fanenr, eagleanurag, akshiterate, 52coder, foursevenlove, KorsChen, hopkings2008, yang-le, realwujing, Evilrabbit520, Umer-Jahangir, Turing-1024-Lee, Suremotoo, paoxiaomooo, Chieko-Seren, Senrian, Allen-Scai, 19santosh99, ymmmas, Risuntsy, Richard-Zhang1019, RafaelCaso, qingpeng9802, primexiao, Urbaner3, codetypess, nidhoggfgg, MwumLi, CreatorMetaSky, martinx, ZnYang2018, hugtyftg, logan-qiu, psychelzh, Kunchen-Luo, Keynman 및 KeiichiKasai.</p> <p>이 책의 코드 검토는 coderonion, curtishd, Gonglja, gvenusleo, hpstory, justin-tse, khoaxuantu, krahets, night-cruise, nuomi1, Reanon 및 rongyi가 맡았다(알파벳순). 시간과 노력을 들여 준 이들에게 감사드린다. 이들 덕분에 각 언어의 코드가 일관된 규칙과 형식을 유지할 수 있었다.</p> <p>영문판은 yuelinxin, K3v123, magentaqin, QiLOL, Phoenix0415, SamJin98, yanedie, RafaelCaso, pengchzn 및 thomasq0가 검토했다. 일본어판은 eltociear가, 러시아어판은 И. А. Шевкун과 Yuyan Huang이, 번체 중국어판은 Shyam-Chen과 Dr-XYZ가 검토했다. 이들의 기여 덕분에 더 많은 독자에게 이 책을 전할 수 있게 되었다. 모두에게 감사드린다.</p> <p>이 책의 ePub 전자책 생성 도구는 zhongfq가 개발했다. 독자에게 더 유연한 독서 방식을 제공해 준 그의 기여에 감사드린다.</p> <p>이 책을 쓰는 동안 많은 분의 도움을 받았다.</p> <ul> <li>회사에서 나의 멘토였던 리시(李汐) 박사에게 감사드린다. 긴 대화 중 “어서 행동에 옮겨 보라”고 격려해 준 덕분에 이 책을 쓰겠다는 결심을 굳힐 수 있었다.</li> <li>이 책의 첫 독자가 되어 준 여자 친구 파오파오(泡泡)에게 감사드린다. 알고리즘 입문자의 시각에서 많은 소중한 의견을 주어 초보자가 읽기 더 좋은 책이 되었다.</li> <li>텅바오(腾宝), 치바오(琦宝), 페이바오(飞宝)에게 감사드린다. 이 책에 창의적인 이름을 지어 주어 모두가 첫 코드 “Hello World!”를 작성하던 아름다운 기억을 떠올리게 했다.</li> <li>지식재산권에 관한 전문적인 도움을 준 샤오취안(校铨)에게 감사드린다. 이 도움은 오픈 소스 도서인 이 책을 완성해 가는 데 중요한 역할을 했다.</li> <li>아름다운 표지와 로고를 디자인해 준 쑤퉁(苏潼)에게 감사드린다. 나의 지나친 꼼꼼함 때문에 여러 번 수정을 요청했는데도 인내심 있게 작업해 주었다.</li> <li>편집과 조판에 관한 조언을 해 주고 오픈 소스 문서 테마 Material-for-MkDocs를 개발한 @squidfunk에게 감사드린다.</li> </ul> <p>집필하면서 자료 구조와 알고리즘에 관한 많은 교재와 글을 읽었다. 이 작품들은 훌륭한 본보기가 되어 이 책의 정확성과 품질을 뒷받침해 주었다. 모든 선생님과 선배의 뛰어난 기여에 감사드린다!</p> <p>이 책은 손으로 실습하고 머리로 생각하며 배우는 방식을 권장한다. 이 점에서는 『실습으로 배우는 딥러닝(Dive into Deep Learning)』에서 깊은 영감을 받았다. 독자 여러분에게 이 훌륭한 책을 적극 추천한다.</p> <p>부모님께 진심으로 감사드린다. 늘 보내 주신 지지와 격려 덕분에 이처럼 흥미로운 일을 할 기회를 얻었다.</p>","path":["제 0 장   들어가며","0.1   이 책에 대하여"],"tags":[]},{"location":"chapter_preface/suggestions/","level":1,"title":"0.2   이 책을 활용하는 방법","text":"<p>참고</p> <p>가장 좋은 독서 경험을 위해 이 절을 끝까지 읽어 보기를 권장한다.</p>","path":["제 0 장   들어가며","0.2   이 책을 활용하는 방법"],"tags":[]},{"location":"chapter_preface/suggestions/#021","level":2,"title":"0.2.1   글의 표현과 서식에 관한 약속","text":"<ul> <li>제목 뒤에 <code>*</code>가 붙은 절은 선택해서 읽는 심화 내용이다. 시간이 부족하면 우선 건너뛰어도 된다.</li> <li>웹 버전에서는 전문 용어를 밑줄로 표시한다. 예를 들면 배열(array)과 같다. 문헌을 읽을 때 도움이 되므로 용어를 기억해 두기를 권장한다.</li> <li>핵심 내용과 요약 문장은 굵게 표시한다. 이러한 문장은 특히 주의 깊게 읽을 필요가 있다.</li> <li>특정한 의미로 사용하는 단어나 문구는 모호함을 피하기 위해 ‘따옴표’로 표시한다.</li> <li>프로그래밍 언어마다 명칭이 다를 때는 Python을 기준으로 삼는다. 예를 들어 ‘비어 있음’을 나타낼 때 <code>None</code>을 사용한다.</li> <li>이 책은 내용을 더 간결하게 배치하기 위해 프로그래밍 언어의 주석 작성 규칙을 일부 따르지 않는다. 주석은 주로 제목 주석, 설명 주석, 여러 줄 주석의 세 유형으로 나뉜다.</li> </ul> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby <pre><code>\"\"\"함수, 클래스, 테스트 사례 등을 표시하는 제목 주석\"\"\"\n\n# 코드를 자세히 설명하는 설명 주석\n\n\"\"\"\n여러 줄\n주석\n\"\"\"\n</code></pre> <pre><code>/* 함수, 클래스, 테스트 사례 등을 표시하는 제목 주석 */\n\n// 코드를 자세히 설명하는 설명 주석\n\n/**\n * 여러 줄\n * 주석\n */\n</code></pre> <pre><code>/* 함수, 클래스, 테스트 사례 등을 표시하는 제목 주석 */\n\n// 코드를 자세히 설명하는 설명 주석\n\n/**\n * 여러 줄\n * 주석\n */\n</code></pre> <pre><code>/* 함수, 클래스, 테스트 사례 등을 표시하는 제목 주석 */\n\n// 코드를 자세히 설명하는 설명 주석\n\n/**\n * 여러 줄\n * 주석\n */\n</code></pre> <pre><code>/* 함수, 클래스, 테스트 사례 등을 표시하는 제목 주석 */\n\n// 코드를 자세히 설명하는 설명 주석\n\n/**\n * 여러 줄\n * 주석\n */\n</code></pre> <pre><code>/* 함수, 클래스, 테스트 사례 등을 표시하는 제목 주석 */\n\n// 코드를 자세히 설명하는 설명 주석\n\n/**\n * 여러 줄\n * 주석\n */\n</code></pre> <pre><code>/* 함수, 클래스, 테스트 사례 등을 표시하는 제목 주석 */\n\n// 코드를 자세히 설명하는 설명 주석\n\n/**\n * 여러 줄\n * 주석\n */\n</code></pre> <pre><code>/* 함수, 클래스, 테스트 사례 등을 표시하는 제목 주석 */\n\n// 코드를 자세히 설명하는 설명 주석\n\n/**\n * 여러 줄\n * 주석\n */\n</code></pre> <pre><code>/* 함수, 클래스, 테스트 사례 등을 표시하는 제목 주석 */\n\n// 코드를 자세히 설명하는 설명 주석\n\n/**\n * 여러 줄\n * 주석\n */\n</code></pre> <pre><code>/* 함수, 클래스, 테스트 사례 등을 표시하는 제목 주석 */\n\n// 코드를 자세히 설명하는 설명 주석\n\n// 여러 줄\n// 주석\n</code></pre> <pre><code>/* 함수, 클래스, 테스트 사례 등을 표시하는 제목 주석 */\n\n// 코드를 자세히 설명하는 설명 주석\n\n/**\n * 여러 줄\n * 주석\n */\n</code></pre> <pre><code>/* 함수, 클래스, 테스트 사례 등을 표시하는 제목 주석 */\n\n// 코드를 자세히 설명하는 설명 주석\n\n/**\n * 여러 줄\n * 주석\n */\n</code></pre> <pre><code>### 함수, 클래스, 테스트 사례 등을 표시하는 제목 주석 ###\n\n# 코드를 자세히 설명하는 설명 주석\n\n# 여러 줄\n# 주석\n</code></pre>","path":["제 0 장   들어가며","0.2   이 책을 활용하는 방법"],"tags":[]},{"location":"chapter_preface/suggestions/#022","level":2,"title":"0.2.2   애니메이션 그림으로 효율적으로 배우기","text":"<p>영상과 그림은 글보다 정보 밀도가 높고 구조가 뚜렷하여 이해하기 쉽다. 이 책에서는 핵심 내용과 어려운 개념을 주로 애니메이션 그림으로 보여 주고, 글은 설명과 보충을 담당한다.</p> <p>책을 읽다가 아래 그림(0-2)과 같은 애니메이션 설명을 만나면 그림을 중심으로 보고 글을 보조 자료로 활용하여 두 가지를 함께 이해하기 바란다.</p> <p></p> <p> 그림 0-2   애니메이션 그림 예시 </p>","path":["제 0 장   들어가며","0.2   이 책을 활용하는 방법"],"tags":[]},{"location":"chapter_preface/suggestions/#023","level":2,"title":"0.2.3   코드 실습으로 이해를 깊게 하기","text":"<p>이 책의 예제 코드는 GitHub 저장소에 있다. 아래 그림(0-3)과 같이 소스 코드에는 테스트 사례가 포함되어 있어 클릭 한 번으로 실행할 수 있다.</p> <p>시간이 허락한다면 예제 코드를 참고하여 직접 한 번 작성해 보기를 권장한다. 공부할 시간이 부족하더라도 적어도 모든 코드를 읽고 실행해 보자.</p> <p>코드를 읽는 것보다 직접 작성하는 과정에서 더 많은 것을 얻는 경우가 많다. 직접 해 보며 배우는 것이 진짜 배움이다.</p> <p></p> <p> 그림 0-3   코드 실행 예시 </p> <p>코드를 실행하기 위한 준비는 크게 세 단계로 나뉜다.</p> <p>1단계: 로컬 프로그래밍 환경 설치. 부록의 안내에 따라 설치한다. 이미 설치했다면 이 단계는 건너뛰어도 된다.</p> <p>2단계: 코드 저장소 복제 또는 내려받기. GitHub 저장소로 이동한다. Git이 설치되어 있다면 다음 명령으로 저장소를 복제할 수 있다.</p> <pre><code>git clone https://github.com/krahets/hello-algo.git\n</code></pre> <p>물론 아래 그림(0-4)에 표시된 위치에서 ‘Download ZIP’ 버튼을 눌러 코드 압축 파일을 바로 내려받은 뒤 로컬에서 압축을 풀어도 된다.</p> <p></p> <p> 그림 0-4   저장소 복제와 코드 내려받기 </p> <p>3단계: 소스 코드 실행. 아래 그림(0-5)처럼 위쪽에 파일명이 표시된 코드 블록은 저장소의 <code>ko/codes</code> 폴더에서 해당 한국어판 소스 파일을 찾을 수 있다. 소스 파일은 클릭 한 번으로 실행할 수 있으므로 불필요한 디버깅 시간을 줄이고 학습 내용에 집중할 수 있다.</p> <p></p> <p> 그림 0-5   코드 블록과 이에 대응하는 소스 파일 </p> <p>로컬에서 실행하는 것 외에 웹 버전에서는 Python 코드의 실행 과정을 시각화할 수도 있다(pythontutor 기반). 아래 그림(0-6)처럼 코드 블록 아래의 ‘코드 시각화’를 누르면 화면이 펼쳐져 알고리즘 코드의 실행 과정을 관찰할 수 있다. ‘전체 화면’을 누르면 더 넓은 화면에서 볼 수 있다.</p> <p></p> <p> 그림 0-6   Python 코드 실행 시각화 </p>","path":["제 0 장   들어가며","0.2   이 책을 활용하는 방법"],"tags":[]},{"location":"chapter_preface/suggestions/#024","level":2,"title":"0.2.4   질문과 토론으로 함께 성장하기","text":"<p>책을 읽다가 이해하지 못한 내용을 만나도 쉽게 건너뛰지 않기를 바란다. 댓글란에 질문을 남기면 나와 동료들이 성심껏 답변하겠다. 보통 이틀 안에 답변을 받을 수 있다.</p> <p>아래 그림(0-7)처럼 웹 버전의 각 장 아래에는 댓글란이 있다. 댓글의 내용에도 관심을 가져 주기 바란다. 다른 사람들이 겪은 문제를 살펴보면서 놓친 내용을 보완하고 더 깊이 생각할 수 있다. 또한 다른 독자의 질문에 답하고 자신의 생각을 나누어 서로의 성장을 도울 수 있기를 기대한다.</p> <p></p> <p> 그림 0-7   댓글란 예시 </p>","path":["제 0 장   들어가며","0.2   이 책을 활용하는 방법"],"tags":[]},{"location":"chapter_preface/suggestions/#025","level":2,"title":"0.2.5   알고리즘 학습 경로","text":"<p>자료 구조와 알고리즘을 배우는 과정은 크게 세 단계로 나눌 수 있다.</p> <ol> <li>1단계: 알고리즘 입문. 다양한 자료 구조의 특성과 사용법에 익숙해지고, 각 알고리즘의 원리, 절차, 용도, 효율성 등을 배운다.</li> <li>2단계: 알고리즘 문제 풀기. 많이 다루는 문제부터 시작하여 최소 100문제 정도를 풀고 주요 알고리즘 문제에 익숙해지기를 권장한다. 처음 문제를 풀 때는 배운 내용을 잊는 일이 어려움으로 느껴질 수 있지만, 이는 매우 자연스러운 일이므로 걱정하지 않아도 된다. ‘에빙하우스의 망각 곡선’에 맞추어 문제를 복습하면 보통 3~5번 반복한 뒤에는 확실하게 기억할 수 있다. 추천 문제 목록과 학습 계획은 이 GitHub 저장소를 참고하기 바란다.</li> <li>3단계: 지식 체계 세우기. 학습 측면에서는 알고리즘 전문 글, 문제 해결 틀, 알고리즘 교재를 읽으며 지식 체계를 꾸준히 넓힐 수 있다. 문제 풀이 측면에서는 주제별 분류, 한 문제를 여러 방법으로 풀기, 한 해법을 여러 문제에 적용하기 등의 심화 전략을 시도할 수 있다. 관련 경험과 조언은 여러 커뮤니티에서 찾아볼 수 있다.</li> </ol> <p>아래 그림(0-8)처럼 이 책은 주로 ‘1단계’를 다루며, 이후 2단계와 3단계의 학습을 더 효율적으로 진행하도록 돕는 것을 목표로 한다.</p> <p></p> <p> 그림 0-8   알고리즘 학습 경로 </p>","path":["제 0 장   들어가며","0.2   이 책을 활용하는 방법"],"tags":[]},{"location":"chapter_preface/summary/","level":1,"title":"0.3   요약","text":"","path":["제 0 장   들어가며","0.3   요약"],"tags":[]},{"location":"chapter_preface/summary/#1","level":3,"title":"1.   핵심 정리","text":"<ul> <li>이 책은 주로 알고리즘 입문자를 대상으로 한다. 이미 기초를 갖추었다면 알고리즘 지식을 체계적으로 복습하는 데 도움이 되며, 소스 코드는 ‘문제 풀이 도구 모음’으로 활용할 수 있다.</li> <li>내용은 크게 복잡도 분석, 자료 구조, 알고리즘의 세 부분으로 구성되어 있으며, 이 분야의 주요 주제를 폭넓게 다룬다.</li> <li>알고리즘 초보자에게는 처음 공부할 때 입문서를 읽는 일이 매우 중요하다. 불필요하게 돌아가는 길을 많이 줄일 수 있다.</li> <li>애니메이션 그림은 주로 핵심 내용과 어려운 개념을 설명하는 데 쓰인다. 책을 읽을 때 이러한 내용을 특히 주의 깊게 살펴보자.</li> <li>실습은 프로그래밍을 배우는 가장 좋은 방법이다. 소스 코드를 실행하고 직접 코드를 작성해 볼 것을 적극 권장한다.</li> <li>웹 버전의 각 장에는 댓글란이 있다. 궁금한 점이나 생각을 언제든 나누기 바란다.</li> </ul>","path":["제 0 장   들어가며","0.3   요약"],"tags":[]},{"location":"chapter_reference/","level":1,"title":"참고 문헌","text":"<p>[1] Thomas H. Cormen, et al. Introduction to Algorithms (3<sup>rd</sup> Edition).</p> <p>[2] Aditya Bhargava. Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People (1<sup>st</sup> Edition).</p> <p>[3] Robert Sedgewick, et al. Algorithms (4<sup>th</sup> Edition).</p> <p>[4] 옌웨이민(严蔚敏). 『자료 구조(C 언어판)』(数据结构(C 语言版)).</p> <p>[5] 덩쥔후이(邓俊辉). 『자료 구조(C++ 언어판, 제3판)』(数据结构(C++ 语言版,第三版)).</p> <p>[6] Mark Allen Weiss 지음, 천웨(陈越) 옮김. 『자료 구조와 알고리즘 분석: Java 언어로 설명하기(제3판)』(数据结构与算法分析:Java语言描述(第三版)).</p> <p>[7] 청제(程杰). 『쉽게 풀어 쓴 자료 구조』(大话数据结构).</p> <p>[8] 왕정(王争). 『자료 구조와 알고리즘의 아름다움』(数据结构与算法之美).</p> <p>[9] Gayle Laakmann McDowell. Cracking the Coding Interview: 189 Programming Questions and Solutions (6<sup>th</sup> Edition).</p> <p>[10] Aston Zhang, et al. Dive into Deep Learning.</p>","path":["참고 문헌"],"tags":[]},{"location":"chapter_searching/","level":1,"title":"제 10 장   탐색","text":"<p>이 장을 시작하며</p> <p>탐색은 미지의 세계로 떠나는 모험이다. 신비로운 공간 구석구석을 모두 돌아봐야 할 수도, 목표를 빠르게 찾아낼 수도 있다.</p> <p>무언가를 찾아 나서는 이 여정에서는 매번 새로운 곳을 살필 때마다 뜻밖의 답을 만날 수 있다.</p>","path":["제 10 장   탐색"],"tags":[]},{"location":"chapter_searching/#_1","level":2,"title":"이 장의 내용","text":"<ul> <li>10.1   이진 탐색</li> <li>10.2   이진 탐색으로 삽입 위치 찾기</li> <li>10.3   이진 탐색으로 경계 찾기</li> <li>10.4   해시를 이용한 최적화 전략</li> <li>10.5   탐색 알고리즘 다시 보기</li> <li>10.6   요약</li> <li>10.7   연습 문제</li> </ul>","path":["제 10 장   탐색"],"tags":[]},{"location":"chapter_searching/binary_search/","level":1,"title":"10.1   이진 탐색","text":"<p>이진 탐색(binary search)은 분할 정복 전략에 기반한 효율적인 탐색 알고리즘이다. 데이터가 정렬되어 있다는 성질을 이용하여 매번 탐색 범위를 절반으로 줄이며, 목표 원소를 찾거나 탐색 구간이 빌 때까지 반복한다.</p> <p>문제</p> <p>길이가 \\(n\\)인 배열 <code>nums</code>가 주어진다. 원소는 오름차순으로 정렬되어 있고 중복되지 않는다. 이 배열에서 원소 <code>target</code>을 찾아 그 인덱스를 반환하라. 배열에 해당 원소가 없으면 \\(-1\\)을 반환하라. 예시는 아래 그림(10-1)과 같다.</p> <p></p> <p> 그림 10-1   이진 탐색 예시 데이터 </p> <p>아래 그림(10-2)과 같이 포인터 \\(i = 0\\)과 \\(j = n - 1\\)을 초기화하여 각각 배열의 첫 원소와 마지막 원소를 가리키게 한다. 이는 탐색 구간 \\([0, n - 1]\\)을 나타낸다. 대괄호는 경곗값 자체를 포함하는 닫힌 구간을 뜻한다.</p> <p>이어서 다음 두 단계를 반복한다.</p> <ol> <li>중간 인덱스 \\(m = \\lfloor {(i + j) / 2} \\rfloor\\)를 계산한다. 여기서 \\(\\lfloor \\: \\rfloor\\)는 내림 연산이다.</li> <li><code>nums[m]</code>과 <code>target</code>의 대소 관계를 비교하며, 다음 세 경우로 나뉜다.<ol> <li><code>nums[m] &lt; target</code>이면 <code>target</code>은 구간 \\([m + 1, j]\\)에 있으므로 \\(i = m + 1\\)을 수행한다.</li> <li><code>nums[m] &gt; target</code>이면 <code>target</code>은 구간 \\([i, m - 1]\\)에 있으므로 \\(j = m - 1\\)을 수행한다.</li> <li><code>nums[m] = target</code>이면 <code>target</code>을 찾았으므로 인덱스 \\(m\\)을 반환한다.</li> </ol> </li> </ol> <p>배열에 목표 원소가 없으면 탐색 구간은 결국 비게 된다. 이때 \\(-1\\)을 반환한다.</p> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt;&lt;6&gt;&lt;7&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 10-2   이진 탐색 과정 </p> <p>\\(i\\)와 \\(j\\)가 모두 <code>int</code> 타입이므로 \\(i + j\\)가 <code>int</code> 타입의 표현 범위를 벗어날 수 있다. 정수 오버플로를 피하기 위해 보통 \\(m = \\lfloor {i + (j - i) / 2} \\rfloor\\) 공식으로 중간 인덱스를 계산한다.</p> <p>코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search.py<pre><code>def binary_search(nums: list[int], target: int) -&gt; int:\n \"\"\"이진 탐색(양 끝을 포함하는 닫힌 구간)\"\"\"\n # 닫힌 구간 [0, n-1]을 초기화한다. i와 j는 각각 배열의 첫 원소와 마지막 원소를 가리킨다\n i, j = 0, len(nums) - 1\n # 반복하다 탐색 구간이 비면 종료한다(i &gt; j이면 빈 구간이다)\n while i &lt;= j:\n # 이론적으로 Python의 수는 메모리 크기에 따라 무한히 커질 수 있으므로 큰 수의 범위 초과를 고려할 필요가 없다\n m = (i + j) // 2 # 중간 인덱스 m을 계산한다\n if nums[m] &lt; target:\n i = m + 1 # 이 경우 target은 구간 [m+1, j]에 있다\n elif nums[m] &gt; target:\n j = m - 1 # 이 경우 target은 구간 [i, m-1]에 있다\n else:\n return m # 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return -1 # 목표 원소를 찾지 못했으므로 -1을 반환한다\n</code></pre> binary_search.cpp<pre><code>/* 이진 탐색(양 끝을 포함하는 닫힌 구간) */\nint binarySearch(vector&lt;int&gt; &amp;nums, int target) {\n // 닫힌 구간 [0, n-1]을 초기화한다. i와 j는 각각 배열의 첫 원소와 마지막 원소를 가리킨다\n int i = 0, j = nums.size() - 1;\n // 반복하다 탐색 구간이 비면 종료한다(i &gt; j이면 빈 구간이다)\n while (i &lt;= j) {\n int m = i + (j - i) / 2; // 중간 인덱스 m을 계산한다\n if (nums[m] &lt; target) // 이 경우 target은 구간 [m+1, j]에 있다\n i = m + 1;\n else if (nums[m] &gt; target) // 이 경우 target은 구간 [i, m-1]에 있다\n j = m - 1;\n else // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m;\n }\n // 목표 원소를 찾지 못했으므로 -1을 반환한다\n return -1;\n}\n</code></pre> binary_search.java<pre><code>/* 이진 탐색(양 끝을 포함하는 닫힌 구간) */\nint binarySearch(int[] nums, int target) {\n // 닫힌 구간 [0, n-1]을 초기화한다. i와 j는 각각 배열의 첫 원소와 마지막 원소를 가리킨다\n int i = 0, j = nums.length - 1;\n // 반복하다 탐색 구간이 비면 종료한다(i &gt; j이면 빈 구간이다)\n while (i &lt;= j) {\n int m = i + (j - i) / 2; // 중간 인덱스 m을 계산한다\n if (nums[m] &lt; target) // 이 경우 target은 구간 [m+1, j]에 있다\n i = m + 1;\n else if (nums[m] &gt; target) // 이 경우 target은 구간 [i, m-1]에 있다\n j = m - 1;\n else // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m;\n }\n // 목표 원소를 찾지 못했으므로 -1을 반환한다\n return -1;\n}\n</code></pre> binary_search.cs<pre><code>/* 이진 탐색(양 끝을 포함하는 닫힌 구간) */\nint BinarySearch(int[] nums, int target) {\n // 닫힌 구간 [0, n-1]을 초기화한다. i와 j는 각각 배열의 첫 원소와 마지막 원소를 가리킨다\n int i = 0, j = nums.Length - 1;\n // 반복하다 탐색 구간이 비면 종료한다(i &gt; j이면 빈 구간이다)\n while (i &lt;= j) {\n int m = i + (j - i) / 2; // 중간 인덱스 m을 계산한다\n if (nums[m] &lt; target) // 이 경우 target은 구간 [m+1, j]에 있다\n i = m + 1;\n else if (nums[m] &gt; target) // 이 경우 target은 구간 [i, m-1]에 있다\n j = m - 1;\n else // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m;\n }\n // 목표 원소를 찾지 못했으므로 -1을 반환한다\n return -1;\n}\n</code></pre> binary_search.go<pre><code>/* 이진 탐색(양 끝을 포함하는 닫힌 구간) */\nfunc binarySearch(nums []int, target int) int {\n // 닫힌 구간 [0, n-1]을 초기화한다. i와 j는 각각 배열의 첫 원소와 마지막 원소를 가리킨다\n i, j := 0, len(nums)-1\n // 반복하다 탐색 구간이 비면 종료한다(i &gt; j이면 빈 구간이다)\n for i &lt;= j {\n m := i + (j-i)/2 // 중간 인덱스 m을 계산한다\n if nums[m] &lt; target { // 이 경우 target은 구간 [m+1, j]에 있다\n i = m + 1\n } else if nums[m] &gt; target { // 이 경우 target은 구간 [i, m-1]에 있다\n j = m - 1\n } else { // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m\n }\n }\n // 목표 원소를 찾지 못했으므로 -1을 반환한다\n return -1\n}\n</code></pre> binary_search.swift<pre><code>/* 이진 탐색(양 끝을 포함하는 닫힌 구간) */\nfunc binarySearch(nums: [Int], target: Int) -&gt; Int {\n // 닫힌 구간 [0, n-1]을 초기화한다. i와 j는 각각 배열의 첫 원소와 마지막 원소를 가리킨다\n var i = nums.startIndex\n var j = nums.endIndex - 1\n // 반복하다 탐색 구간이 비면 종료한다(i &gt; j이면 빈 구간이다)\n while i &lt;= j {\n let m = i + (j - i) / 2 // 중간 인덱스 m을 계산한다\n if nums[m] &lt; target { // 이 경우 target은 구간 [m+1, j]에 있다\n i = m + 1\n } else if nums[m] &gt; target { // 이 경우 target은 구간 [i, m-1]에 있다\n j = m - 1\n } else { // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m\n }\n }\n // 목표 원소를 찾지 못했으므로 -1을 반환한다\n return -1\n}\n</code></pre> binary_search.js<pre><code>/* 이진 탐색(양 끝을 포함하는 닫힌 구간) */\nfunction binarySearch(nums, target) {\n // 닫힌 구간 [0, n-1]을 초기화한다. i와 j는 각각 배열의 첫 원소와 마지막 원소를 가리킨다\n let i = 0,\n j = nums.length - 1;\n // 반복하다 탐색 구간이 비면 종료한다(i &gt; j이면 빈 구간이다)\n while (i &lt;= j) {\n // 중간 인덱스 m을 계산하며, parseInt()로 소수 부분을 버린다\n const m = parseInt(i + (j - i) / 2);\n if (nums[m] &lt; target)\n // 이 경우 target은 구간 [m+1, j]에 있다\n i = m + 1;\n else if (nums[m] &gt; target)\n // 이 경우 target은 구간 [i, m-1]에 있다\n j = m - 1;\n else return m; // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n }\n // 목표 원소를 찾지 못했으므로 -1을 반환한다\n return -1;\n}\n</code></pre> binary_search.ts<pre><code>/* 이진 탐색(양 끝을 포함하는 닫힌 구간) */\nfunction binarySearch(nums: number[], target: number): number {\n // 닫힌 구간 [0, n-1]을 초기화한다. i와 j는 각각 배열의 첫 원소와 마지막 원소를 가리킨다\n let i = 0,\n j = nums.length - 1;\n // 반복하다 탐색 구간이 비면 종료한다(i &gt; j이면 빈 구간이다)\n while (i &lt;= j) {\n // 중간 인덱스 m을 계산한다\n const m = Math.floor(i + (j - i) / 2);\n if (nums[m] &lt; target) {\n // 이 경우 target은 구간 [m+1, j]에 있다\n i = m + 1;\n } else if (nums[m] &gt; target) {\n // 이 경우 target은 구간 [i, m-1]에 있다\n j = m - 1;\n } else {\n // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m;\n }\n }\n return -1; // 목표 원소를 찾지 못했으므로 -1을 반환한다\n}\n</code></pre> binary_search.dart<pre><code>/* 이진 탐색(양 끝을 포함하는 닫힌 구간) */\nint binarySearch(List&lt;int&gt; nums, int target) {\n // 닫힌 구간 [0, n-1]을 초기화한다. i와 j는 각각 배열의 첫 원소와 마지막 원소를 가리킨다\n int i = 0, j = nums.length - 1;\n // 반복하다 탐색 구간이 비면 종료한다(i &gt; j이면 빈 구간이다)\n while (i &lt;= j) {\n int m = i + (j - i) ~/ 2; // 중간 인덱스 m을 계산한다\n if (nums[m] &lt; target) {\n // 이 경우 target은 구간 [m+1, j]에 있다\n i = m + 1;\n } else if (nums[m] &gt; target) {\n // 이 경우 target은 구간 [i, m-1]에 있다\n j = m - 1;\n } else {\n // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m;\n }\n }\n // 목표 원소를 찾지 못했으므로 -1을 반환한다\n return -1;\n}\n</code></pre> binary_search.rs<pre><code>/* 이진 탐색(양 끝을 포함하는 닫힌 구간) */\nfn binary_search(nums: &amp;[i32], target: i32) -&gt; i32 {\n // 닫힌 구간 [0, n-1]을 초기화한다. i와 j는 각각 배열의 첫 원소와 마지막 원소를 가리킨다\n let mut i = 0;\n let mut j = nums.len() as i32 - 1;\n // 반복하다 탐색 구간이 비면 종료한다(i &gt; j이면 빈 구간이다)\n while i &lt;= j {\n let m = i + (j - i) / 2; // 중간 인덱스 m을 계산한다\n if nums[m as usize] &lt; target {\n // 이 경우 target은 구간 [m+1, j]에 있다\n i = m + 1;\n } else if nums[m as usize] &gt; target {\n // 이 경우 target은 구간 [i, m-1]에 있다\n j = m - 1;\n } else {\n // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m;\n }\n }\n // 목표 원소를 찾지 못했으므로 -1을 반환한다\n return -1;\n}\n</code></pre> binary_search.c<pre><code>/* 이진 탐색(양 끝을 포함하는 닫힌 구간) */\nint binarySearch(int *nums, int len, int target) {\n // 닫힌 구간 [0, n-1]을 초기화한다. i와 j는 각각 배열의 첫 원소와 마지막 원소를 가리킨다\n int i = 0, j = len - 1;\n // 반복하다 탐색 구간이 비면 종료한다(i &gt; j이면 빈 구간이다)\n while (i &lt;= j) {\n int m = i + (j - i) / 2; // 중간 인덱스 m을 계산한다\n if (nums[m] &lt; target) // 이 경우 target은 구간 [m+1, j]에 있다\n i = m + 1;\n else if (nums[m] &gt; target) // 이 경우 target은 구간 [i, m-1]에 있다\n j = m - 1;\n else // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m;\n }\n // 목표 원소를 찾지 못했으므로 -1을 반환한다\n return -1;\n}\n</code></pre> binary_search.kt<pre><code>/* 이진 탐색(양 끝을 포함하는 닫힌 구간) */\nfun binarySearch(nums: IntArray, target: Int): Int {\n // 닫힌 구간 [0, n-1]을 초기화한다. i와 j는 각각 배열의 첫 원소와 마지막 원소를 가리킨다\n var i = 0\n var j = nums.size - 1\n // 반복하다 탐색 구간이 비면 종료한다(i &gt; j이면 빈 구간이다)\n while (i &lt;= j) {\n val m = i + (j - i) / 2 // 중간 인덱스 m을 계산한다\n if (nums[m] &lt; target) // 이 경우 target은 구간 [m+1, j]에 있다\n i = m + 1\n else if (nums[m] &gt; target) // 이 경우 target은 구간 [i, m-1]에 있다\n j = m - 1\n else // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m\n }\n // 목표 원소를 찾지 못했으므로 -1을 반환한다\n return -1\n}\n</code></pre> binary_search.rb<pre><code>### 이진 탐색(양 끝을 포함하는 닫힌 구간) ###\ndef binary_search(nums, target)\n # 닫힌 구간 [0, n-1]을 초기화한다. i와 j는 각각 배열의 첫 원소와 마지막 원소를 가리킨다\n i, j = 0, nums.length - 1\n\n # 반복하다 탐색 구간이 비면 종료한다(i &gt; j이면 빈 구간이다)\n while i &lt;= j\n # Ruby의 정수는 이론상 메모리가 허용하는 만큼 커질 수 있으므로 큰 수의 오버플로를 고려할 필요가 없다\n m = (i + j) / 2 # 중간 인덱스 m을 계산한다\n\n if nums[m] &lt; target\n i = m + 1 # 이 경우 target은 구간 [m+1, j]에 있다\n elsif nums[m] &gt; target\n j = m - 1 # 이 경우 target은 구간 [i, m-1]에 있다\n else\n return m # 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n end\n end\n\n -1 # 목표 원소를 찾지 못했으므로 -1을 반환한다\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>시간 복잡도는 \\(O(\\log n)\\)이다. 이진 탐색의 반복문에서 구간이 매번 절반으로 줄어들므로 반복 횟수는 \\(\\log_2 n\\)이다.</p> <p>공간 복잡도는 \\(O(1)\\)이다. 포인터 \\(i\\)와 \\(j\\)는 상수 크기의 공간을 사용한다.</p>","path":["제 10 장   탐색","10.1   이진 탐색"],"tags":[]},{"location":"chapter_searching/binary_search/#1011","level":2,"title":"10.1.1   구간 표현 방법","text":"<p>위의 양끝이 닫힌 구간 외에 '왼쪽은 닫히고 오른쪽은 열린' 구간도 자주 사용한다. \\([0, n)\\)으로 정의하며 왼쪽 경곗값은 포함하고 오른쪽 경곗값은 포함하지 않는다. 이 표현에서 구간 \\([i, j)\\)는 \\(i = j\\)일 때 비어 있다.</p> <p>이 표현을 사용하여 같은 기능의 이진 탐색 알고리즘을 구현할 수 있다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search.py<pre><code>def binary_search_lcro(nums: list[int], target: int) -&gt; int:\n \"\"\"이진 탐색(왼쪽 끝은 포함하고 오른쪽 끝은 제외하는 구간)\"\"\"\n # 왼쪽 끝만 포함하는 구간 [0, n)을 초기화한다. i는 배열의 첫 원소를, j는 마지막 원소 바로 다음 위치를 가리킨다\n i, j = 0, len(nums)\n # 반복하다 탐색 구간이 비면 종료한다(i = j이면 빈 구간이다)\n while i &lt; j:\n m = (i + j) // 2 # 중간 인덱스 m을 계산한다\n if nums[m] &lt; target:\n i = m + 1 # 이 경우 target은 구간 [m+1, j)에 있다\n elif nums[m] &gt; target:\n j = m # 이 경우 target은 구간 [i, m)에 있다\n else:\n return m # 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return -1 # 목표 원소를 찾지 못했으므로 -1을 반환한다\n</code></pre> binary_search.cpp<pre><code>/* 이진 탐색(왼쪽 끝은 포함하고 오른쪽 끝은 제외하는 구간) */\nint binarySearchLCRO(vector&lt;int&gt; &amp;nums, int target) {\n // 왼쪽 끝만 포함하는 구간 [0, n)을 초기화한다. i는 배열의 첫 원소를, j는 마지막 원소 바로 다음 위치를 가리킨다\n int i = 0, j = nums.size();\n // 반복하다 탐색 구간이 비면 종료한다(i = j이면 빈 구간이다)\n while (i &lt; j) {\n int m = i + (j - i) / 2; // 중간 인덱스 m을 계산한다\n if (nums[m] &lt; target) // 이 경우 target은 구간 [m+1, j)에 있다\n i = m + 1;\n else if (nums[m] &gt; target) // 이 경우 target은 구간 [i, m)에 있다\n j = m;\n else // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m;\n }\n // 목표 원소를 찾지 못했으므로 -1을 반환한다\n return -1;\n}\n</code></pre> binary_search.java<pre><code>/* 이진 탐색(왼쪽 끝은 포함하고 오른쪽 끝은 제외하는 구간) */\nint binarySearchLCRO(int[] nums, int target) {\n // 왼쪽 끝만 포함하는 구간 [0, n)을 초기화한다. i는 배열의 첫 원소를, j는 마지막 원소 바로 다음 위치를 가리킨다\n int i = 0, j = nums.length;\n // 반복하다 탐색 구간이 비면 종료한다(i = j이면 빈 구간이다)\n while (i &lt; j) {\n int m = i + (j - i) / 2; // 중간 인덱스 m을 계산한다\n if (nums[m] &lt; target) // 이 경우 target은 구간 [m+1, j)에 있다\n i = m + 1;\n else if (nums[m] &gt; target) // 이 경우 target은 구간 [i, m)에 있다\n j = m;\n else // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m;\n }\n // 목표 원소를 찾지 못했으므로 -1을 반환한다\n return -1;\n}\n</code></pre> binary_search.cs<pre><code>/* 이진 탐색(왼쪽 끝은 포함하고 오른쪽 끝은 제외하는 구간) */\nint BinarySearchLCRO(int[] nums, int target) {\n // 왼쪽 끝만 포함하는 구간 [0, n)을 초기화한다. i는 배열의 첫 원소를, j는 마지막 원소 바로 다음 위치를 가리킨다\n int i = 0, j = nums.Length;\n // 반복하다 탐색 구간이 비면 종료한다(i = j이면 빈 구간이다)\n while (i &lt; j) {\n int m = i + (j - i) / 2; // 중간 인덱스 m을 계산한다\n if (nums[m] &lt; target) // 이 경우 target은 구간 [m+1, j)에 있다\n i = m + 1;\n else if (nums[m] &gt; target) // 이 경우 target은 구간 [i, m)에 있다\n j = m;\n else // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m;\n }\n // 목표 원소를 찾지 못했으므로 -1을 반환한다\n return -1;\n}\n</code></pre> binary_search.go<pre><code>/* 이진 탐색(왼쪽 끝은 포함하고 오른쪽 끝은 제외하는 구간) */\nfunc binarySearchLCRO(nums []int, target int) int {\n // 왼쪽 끝만 포함하는 구간 [0, n)을 초기화한다. i는 배열의 첫 원소를, j는 마지막 원소 바로 다음 위치를 가리킨다\n i, j := 0, len(nums)\n // 반복하다 탐색 구간이 비면 종료한다(i = j이면 빈 구간이다)\n for i &lt; j {\n m := i + (j-i)/2 // 중간 인덱스 m을 계산한다\n if nums[m] &lt; target { // 이 경우 target은 구간 [m+1, j)에 있다\n i = m + 1\n } else if nums[m] &gt; target { // 이 경우 target은 구간 [i, m)에 있다\n j = m\n } else { // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m\n }\n }\n // 목표 원소를 찾지 못했으므로 -1을 반환한다\n return -1\n}\n</code></pre> binary_search.swift<pre><code>/* 이진 탐색(왼쪽 끝은 포함하고 오른쪽 끝은 제외하는 구간) */\nfunc binarySearchLCRO(nums: [Int], target: Int) -&gt; Int {\n // 왼쪽 끝만 포함하는 구간 [0, n)을 초기화한다. i는 배열의 첫 원소를, j는 마지막 원소 바로 다음 위치를 가리킨다\n var i = nums.startIndex\n var j = nums.endIndex\n // 반복하다 탐색 구간이 비면 종료한다(i = j이면 빈 구간이다)\n while i &lt; j {\n let m = i + (j - i) / 2 // 중간 인덱스 m을 계산한다\n if nums[m] &lt; target { // 이 경우 target은 구간 [m+1, j)에 있다\n i = m + 1\n } else if nums[m] &gt; target { // 이 경우 target은 구간 [i, m)에 있다\n j = m\n } else { // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m\n }\n }\n // 목표 원소를 찾지 못했으므로 -1을 반환한다\n return -1\n}\n</code></pre> binary_search.js<pre><code>/* 이진 탐색(왼쪽 끝은 포함하고 오른쪽 끝은 제외하는 구간) */\nfunction binarySearchLCRO(nums, target) {\n // 왼쪽 끝만 포함하는 구간 [0, n)을 초기화한다. i는 배열의 첫 원소를, j는 마지막 원소 바로 다음 위치를 가리킨다\n let i = 0,\n j = nums.length;\n // 반복하다 탐색 구간이 비면 종료한다(i = j이면 빈 구간이다)\n while (i &lt; j) {\n // 중간 인덱스 m을 계산하며, parseInt()로 소수 부분을 버린다\n const m = parseInt(i + (j - i) / 2);\n if (nums[m] &lt; target)\n // 이 경우 target은 구간 [m+1, j)에 있다\n i = m + 1;\n else if (nums[m] &gt; target)\n // 이 경우 target은 구간 [i, m)에 있다\n j = m;\n // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n else return m;\n }\n // 목표 원소를 찾지 못했으므로 -1을 반환한다\n return -1;\n}\n</code></pre> binary_search.ts<pre><code>/* 이진 탐색(왼쪽 끝은 포함하고 오른쪽 끝은 제외하는 구간) */\nfunction binarySearchLCRO(nums: number[], target: number): number {\n // 왼쪽 끝만 포함하는 구간 [0, n)을 초기화한다. i는 배열의 첫 원소를, j는 마지막 원소 바로 다음 위치를 가리킨다\n let i = 0,\n j = nums.length;\n // 반복하다 탐색 구간이 비면 종료한다(i = j이면 빈 구간이다)\n while (i &lt; j) {\n // 중간 인덱스 m을 계산한다\n const m = Math.floor(i + (j - i) / 2);\n if (nums[m] &lt; target) {\n // 이 경우 target은 구간 [m+1, j)에 있다\n i = m + 1;\n } else if (nums[m] &gt; target) {\n // 이 경우 target은 구간 [i, m)에 있다\n j = m;\n } else {\n // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m;\n }\n }\n return -1; // 목표 원소를 찾지 못했으므로 -1을 반환한다\n}\n</code></pre> binary_search.dart<pre><code>/* 이진 탐색(왼쪽 끝은 포함하고 오른쪽 끝은 제외하는 구간) */\nint binarySearchLCRO(List&lt;int&gt; nums, int target) {\n // 왼쪽 끝만 포함하는 구간 [0, n)을 초기화한다. i는 배열의 첫 원소를, j는 마지막 원소 바로 다음 위치를 가리킨다\n int i = 0, j = nums.length;\n // 반복하다 탐색 구간이 비면 종료한다(i = j이면 빈 구간이다)\n while (i &lt; j) {\n int m = i + (j - i) ~/ 2; // 중간 인덱스 m을 계산한다\n if (nums[m] &lt; target) {\n // 이 경우 target은 구간 [m+1, j)에 있다\n i = m + 1;\n } else if (nums[m] &gt; target) {\n // 이 경우 target은 구간 [i, m)에 있다\n j = m;\n } else {\n // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m;\n }\n }\n // 목표 원소를 찾지 못했으므로 -1을 반환한다\n return -1;\n}\n</code></pre> binary_search.rs<pre><code>/* 이진 탐색(왼쪽 끝은 포함하고 오른쪽 끝은 제외하는 구간) */\nfn binary_search_lcro(nums: &amp;[i32], target: i32) -&gt; i32 {\n // 왼쪽 끝만 포함하는 구간 [0, n)을 초기화한다. i는 배열의 첫 원소를, j는 마지막 원소 바로 다음 위치를 가리킨다\n let mut i = 0;\n let mut j = nums.len() as i32;\n // 반복하다 탐색 구간이 비면 종료한다(i = j이면 빈 구간이다)\n while i &lt; j {\n let m = i + (j - i) / 2; // 중간 인덱스 m을 계산한다\n if nums[m as usize] &lt; target {\n // 이 경우 target은 구간 [m+1, j)에 있다\n i = m + 1;\n } else if nums[m as usize] &gt; target {\n // 이 경우 target은 구간 [i, m)에 있다\n j = m;\n } else {\n // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m;\n }\n }\n // 목표 원소를 찾지 못했으므로 -1을 반환한다\n return -1;\n}\n</code></pre> binary_search.c<pre><code>/* 이진 탐색(왼쪽 끝은 포함하고 오른쪽 끝은 제외하는 구간) */\nint binarySearchLCRO(int *nums, int len, int target) {\n // 왼쪽 끝만 포함하는 구간 [0, n)을 초기화한다. i는 배열의 첫 원소를, j는 마지막 원소 바로 다음 위치를 가리킨다\n int i = 0, j = len;\n // 반복하다 탐색 구간이 비면 종료한다(i = j이면 빈 구간이다)\n while (i &lt; j) {\n int m = i + (j - i) / 2; // 중간 인덱스 m을 계산한다\n if (nums[m] &lt; target) // 이 경우 target은 구간 [m+1, j)에 있다\n i = m + 1;\n else if (nums[m] &gt; target) // 이 경우 target은 구간 [i, m)에 있다\n j = m;\n else // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m;\n }\n // 목표 원소를 찾지 못했으므로 -1을 반환한다\n return -1;\n}\n</code></pre> binary_search.kt<pre><code>/* 이진 탐색(왼쪽 끝은 포함하고 오른쪽 끝은 제외하는 구간) */\nfun binarySearchLCRO(nums: IntArray, target: Int): Int {\n // 왼쪽 끝만 포함하는 구간 [0, n)을 초기화한다. i는 배열의 첫 원소를, j는 마지막 원소 바로 다음 위치를 가리킨다\n var i = 0\n var j = nums.size\n // 반복하다 탐색 구간이 비면 종료한다(i = j이면 빈 구간이다)\n while (i &lt; j) {\n val m = i + (j - i) / 2 // 중간 인덱스 m을 계산한다\n if (nums[m] &lt; target) // 이 경우 target은 구간 [m+1, j)에 있다\n i = m + 1\n else if (nums[m] &gt; target) // 이 경우 target은 구간 [i, m)에 있다\n j = m\n else // 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n return m\n }\n // 목표 원소를 찾지 못했으므로 -1을 반환한다\n return -1\n}\n</code></pre> binary_search.rb<pre><code>### 이진 탐색(왼쪽 끝은 포함하고 오른쪽 끝은 제외하는 구간) ###\ndef binary_search_lcro(nums, target)\n # 왼쪽 끝만 포함하는 구간 [0, n)을 초기화한다. i는 배열의 첫 원소를, j는 마지막 원소 바로 다음 위치를 가리킨다\n i, j = 0, nums.length\n\n # 반복하다 탐색 구간이 비면 종료한다(i = j이면 빈 구간이다)\n while i &lt; j\n # 중간 인덱스 m을 계산한다\n m = (i + j) / 2\n\n if nums[m] &lt; target\n i = m + 1 # 이 경우 target은 구간 [m+1, j)에 있다\n elsif nums[m] &gt; target\n j = m - 1 # 이 경우 target은 구간 [i, m)에 있다\n else\n return m # 목표 원소를 찾았으므로 해당 인덱스를 반환한다\n end\n end\n\n -1 # 목표 원소를 찾지 못했으므로 -1을 반환한다\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>아래 그림(10-3)과 같이 구간을 표현하는 방식에 따라 이진 탐색 알고리즘의 초기화, 반복 조건, 구간 축소 연산이 모두 달라진다.</p> <p>'양끝이 닫힌 구간'에서는 왼쪽과 오른쪽 경계를 모두 포함하므로, 포인터 \\(i\\)와 \\(j\\)로 구간을 줄이는 연산도 대칭적이다. 실수할 가능성이 더 작으므로 일반적으로 '양끝이 닫힌 구간'으로 작성하기를 권장한다.</p> <p></p> <p> 그림 10-3   두 가지 구간 정의 </p>","path":["제 10 장   탐색","10.1   이진 탐색"],"tags":[]},{"location":"chapter_searching/binary_search/#1012","level":2,"title":"10.1.2   장점과 한계","text":"<p>이진 탐색은 시간과 공간 양쪽에서 좋은 성능을 보인다.</p> <ul> <li>이진 탐색은 시간 효율이 높다. 데이터가 많을수록 로그 시간 복잡도의 이점이 뚜렷하다. 예를 들어 데이터 크기가 \\(n = 2^{20}\\)이면 선형 탐색은 \\(2^{20} = 1048576\\) 번 반복해야 하지만, 이진 탐색은 \\(\\log_2 2^{20} = 20\\) 번이면 된다.</li> <li>이진 탐색은 추가 공간이 필요하지 않다. 해시 탐색처럼 추가 공간을 사용하는 탐색 알고리즘보다 공간을 절약한다.</li> </ul> <p>하지만 이진 탐색이 모든 상황에 적합한 것은 아니다. 주요 이유는 다음과 같다.</p> <ul> <li>이진 탐색은 정렬된 데이터에만 적용할 수 있다. 입력이 정렬되지 않았다면 이진 탐색만을 위해 정렬하는 것은 오히려 손해다. 정렬 알고리즘의 시간 복잡도는 보통 \\(O(n \\log n)\\)으로 선형 탐색과 이진 탐색보다 높기 때문이다. 원소를 자주 삽입하는 상황에서도 정렬 상태를 유지하려면 특정 위치에 삽입해야 하므로, \\(O(n)\\)의 큰 시간 비용이 든다.</li> <li>이진 탐색은 배열에만 적합하다. 원소를 건너뛰며(연속적이지 않게) 접근해야 하는데, 연결 리스트에서는 이런 접근이 비효율적이다. 따라서 연결 리스트나 연결 리스트 기반 자료 구조에는 적합하지 않다.</li> <li>데이터가 적을 때는 선형 탐색의 성능이 더 좋다. 선형 탐색은 매번 비교 1회만 필요하지만, 이진 탐색은 덧셈 1회, 나눗셈 1회, 비교 1 ~ 3회, 덧셈(뺄셈) 1회로 총 4 ~ 6회의 기본 연산이 필요하다. 따라서 데이터 수 \\(n\\)이 작으면 선형 탐색이 오히려 더 빠르다.</li> </ul>","path":["제 10 장   탐색","10.1   이진 탐색"],"tags":[]},{"location":"chapter_searching/binary_search_edge/","level":1,"title":"10.3   이진 탐색으로 경계 찾기","text":"","path":["제 10 장   탐색","10.3   이진 탐색으로 경계 찾기"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1031","level":2,"title":"10.3.1   왼쪽 경계 찾기","text":"<p>문제</p> <p>중복 원소를 포함할 수 있는 길이 \\(n\\)의 정렬된 배열 <code>nums</code>가 주어진다. 배열에서 가장 왼쪽에 있는 원소 <code>target</code>의 인덱스를 반환하라. 해당 원소가 없으면 \\(-1\\)을 반환하라.</p> <p>이진 탐색으로 삽입 위치를 찾는 방법을 떠올려 보자. 탐색이 끝나면 \\(i\\)는 가장 왼쪽의 <code>target</code>을 가리킨다. 따라서 삽입 위치 찾기는 본질적으로 가장 왼쪽 <code>target</code>의 인덱스를 찾는 과정이다.</p> <p>삽입 위치를 찾는 함수를 이용하여 왼쪽 경계를 찾아 보자. 배열에 <code>target</code>이 없을 수도 있으며, 이때는 다음 두 결과 중 하나가 나올 수 있다.</p> <ul> <li>삽입 위치의 인덱스 \\(i\\)가 범위를 벗어난다.</li> <li>원소 <code>nums[i]</code>가 <code>target</code>과 다르다.</li> </ul> <p>위 두 경우에는 \\(-1\\)을 바로 반환하면 된다. 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_edge.py<pre><code>def binary_search_left_edge(nums: list[int], target: int) -&gt; int:\n \"\"\"가장 왼쪽에 있는 target을 이진 탐색\"\"\"\n # target의 삽입 위치를 찾는 것과 같다\n i = binary_search_insertion(nums, target)\n # target을 찾지 못했으므로 -1을 반환한다\n if i == len(nums) or nums[i] != target:\n return -1\n # target을 찾았으므로 인덱스 i를 반환한다\n return i\n</code></pre> binary_search_edge.cpp<pre><code>/* 가장 왼쪽에 있는 target을 이진 탐색 */\nint binarySearchLeftEdge(vector&lt;int&gt; &amp;nums, int target) {\n // target의 삽입 위치를 찾는 것과 같다\n int i = binarySearchInsertion(nums, target);\n // target을 찾지 못했으므로 -1을 반환한다\n if (i == nums.size() || nums[i] != target) {\n return -1;\n }\n // target을 찾았으므로 인덱스 i를 반환한다\n return i;\n}\n</code></pre> binary_search_edge.java<pre><code>/* 가장 왼쪽에 있는 target을 이진 탐색 */\nint binarySearchLeftEdge(int[] nums, int target) {\n // target의 삽입 위치를 찾는 것과 같다\n int i = binary_search_insertion.binarySearchInsertion(nums, target);\n // target을 찾지 못했으므로 -1을 반환한다\n if (i == nums.length || nums[i] != target) {\n return -1;\n }\n // target을 찾았으므로 인덱스 i를 반환한다\n return i;\n}\n</code></pre> binary_search_edge.cs<pre><code>/* 가장 왼쪽에 있는 target을 이진 탐색 */\nint BinarySearchLeftEdge(int[] nums, int target) {\n // target의 삽입 위치를 찾는 것과 같다\n int i = binary_search_insertion.BinarySearchInsertion(nums, target);\n // target을 찾지 못했으므로 -1을 반환한다\n if (i == nums.Length || nums[i] != target) {\n return -1;\n }\n // target을 찾았으므로 인덱스 i를 반환한다\n return i;\n}\n</code></pre> binary_search_edge.go<pre><code>/* 가장 왼쪽에 있는 target을 이진 탐색 */\nfunc binarySearchLeftEdge(nums []int, target int) int {\n // target의 삽입 위치를 찾는 것과 같다\n i := binarySearchInsertion(nums, target)\n // target을 찾지 못했으므로 -1을 반환한다\n if i == len(nums) || nums[i] != target {\n return -1\n }\n // target을 찾았으므로 인덱스 i를 반환한다\n return i\n}\n</code></pre> binary_search_edge.swift<pre><code>/* 가장 왼쪽에 있는 target을 이진 탐색 */\nfunc binarySearchLeftEdge(nums: [Int], target: Int) -&gt; Int {\n // target의 삽입 위치를 찾는 것과 같다\n let i = binarySearchInsertion(nums: nums, target: target)\n // target을 찾지 못했으므로 -1을 반환한다\n if i == nums.endIndex || nums[i] != target {\n return -1\n }\n // target을 찾았으므로 인덱스 i를 반환한다\n return i\n}\n</code></pre> binary_search_edge.js<pre><code>/* 가장 왼쪽에 있는 target을 이진 탐색 */\nfunction binarySearchLeftEdge(nums, target) {\n // target의 삽입 위치를 찾는 것과 같다\n const i = binarySearchInsertion(nums, target);\n // target을 찾지 못했으므로 -1을 반환한다\n if (i === nums.length || nums[i] !== target) {\n return -1;\n }\n // target을 찾았으므로 인덱스 i를 반환한다\n return i;\n}\n</code></pre> binary_search_edge.ts<pre><code>/* 가장 왼쪽에 있는 target을 이진 탐색 */\nfunction binarySearchLeftEdge(nums: Array&lt;number&gt;, target: number): number {\n // target의 삽입 위치를 찾는 것과 같다\n const i = binarySearchInsertion(nums, target);\n // target을 찾지 못했으므로 -1을 반환한다\n if (i === nums.length || nums[i] !== target) {\n return -1;\n }\n // target을 찾았으므로 인덱스 i를 반환한다\n return i;\n}\n</code></pre> binary_search_edge.dart<pre><code>/* 가장 왼쪽에 있는 target을 이진 탐색 */\nint binarySearchLeftEdge(List&lt;int&gt; nums, int target) {\n // target의 삽입 위치를 찾는 것과 같다\n int i = binarySearchInsertion(nums, target);\n // target을 찾지 못했으므로 -1을 반환한다\n if (i == nums.length || nums[i] != target) {\n return -1;\n }\n // target을 찾았으므로 인덱스 i를 반환한다\n return i;\n}\n</code></pre> binary_search_edge.rs<pre><code>/* 가장 왼쪽에 있는 target을 이진 탐색 */\nfn binary_search_left_edge(nums: &amp;[i32], target: i32) -&gt; i32 {\n // target의 삽입 위치를 찾는 것과 같다\n let i = binary_search_insertion(nums, target);\n // target을 찾지 못했으므로 -1을 반환한다\n if i == nums.len() as i32 || nums[i as usize] != target {\n return -1;\n }\n // target을 찾았으므로 인덱스 i를 반환한다\n i\n}\n</code></pre> binary_search_edge.c<pre><code>/* 가장 왼쪽에 있는 target을 이진 탐색 */\nint binarySearchLeftEdge(int *nums, int numSize, int target) {\n // target의 삽입 위치를 찾는 것과 같다\n int i = binarySearchInsertion(nums, numSize, target);\n // target을 찾지 못했으므로 -1을 반환한다\n if (i == numSize || nums[i] != target) {\n return -1;\n }\n // target을 찾았으므로 인덱스 i를 반환한다\n return i;\n}\n</code></pre> binary_search_edge.kt<pre><code>/* 가장 왼쪽에 있는 target을 이진 탐색 */\nfun binarySearchLeftEdge(nums: IntArray, target: Int): Int {\n // target의 삽입 위치를 찾는 것과 같다\n val i = binarySearchInsertion(nums, target)\n // target을 찾지 못했으므로 -1을 반환한다\n if (i == nums.size || nums[i] != target) {\n return -1\n }\n // target을 찾았으므로 인덱스 i를 반환한다\n return i\n}\n</code></pre> binary_search_edge.rb<pre><code>### 가장 왼쪽에 있는 target을 이진 탐색 ###\ndef binary_search_left_edge(nums, target)\n # target의 삽입 위치를 찾는 것과 같다\n i = binary_search_insertion(nums, target)\n\n # target을 찾지 못했으므로 -1을 반환한다\n return -1 if i == nums.length || nums[i] != target\n\n i # target을 찾았으므로 인덱스 i를 반환한다\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 10 장   탐색","10.3   이진 탐색으로 경계 찾기"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1032","level":2,"title":"10.3.2   오른쪽 경계 찾기","text":"<p>가장 오른쪽에 있는 <code>target</code>은 어떻게 찾을까? 가장 직접적인 방법은 코드를 수정하여 <code>nums[m] == target</code>일 때 포인터로 구간을 줄이는 방식을 바꾸는 것이다. 코드는 생략하니, 관심 있는 독자는 직접 구현해 보자.</p> <p>아래에서는 더 기발한 두 가지 방법을 소개한다.</p>","path":["제 10 장   탐색","10.3   이진 탐색으로 경계 찾기"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1","level":3,"title":"1.   왼쪽 경계 찾기 재활용","text":"<p>가장 왼쪽 원소를 찾는 함수로 가장 오른쪽 원소도 찾을 수 있다. 구체적으로는 가장 오른쪽 <code>target</code>을 찾는 문제를 가장 왼쪽 <code>target + 1</code>을 찾는 문제로 바꾼다.</p> <p>아래 그림(10-7)과 같이 탐색이 끝나면 포인터 \\(i\\)는 가장 왼쪽 <code>target + 1</code>(존재한다면)을 가리키고, \\(j\\)는 가장 오른쪽 <code>target</code>을 가리킨다. 따라서 \\(j\\)를 반환하면 된다.</p> <p></p> <p> 그림 10-7   오른쪽 경계 찾기를 왼쪽 경계 찾기로 변환 </p> <p>반환되는 삽입 위치는 \\(i\\)이므로, 여기서 \\(1\\)을 빼야 \\(j\\)를 얻을 수 있다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_edge.py<pre><code>def binary_search_right_edge(nums: list[int], target: int) -&gt; int:\n \"\"\"가장 오른쪽에 있는 target을 이진 탐색\"\"\"\n # 가장 왼쪽에 있는 target + 1을 찾는 문제로 바꾼다\n i = binary_search_insertion(nums, target + 1)\n # j는 가장 오른쪽의 target을 가리키고, i는 target보다 큰 첫 번째 원소를 가리킨다\n j = i - 1\n # target을 찾지 못했으므로 -1을 반환한다\n if j == -1 or nums[j] != target:\n return -1\n # target을 찾았으므로 인덱스 j를 반환한다\n return j\n</code></pre> binary_search_edge.cpp<pre><code>/* 가장 오른쪽에 있는 target을 이진 탐색 */\nint binarySearchRightEdge(vector&lt;int&gt; &amp;nums, int target) {\n // 가장 왼쪽에 있는 target + 1을 찾는 문제로 바꾼다\n int i = binarySearchInsertion(nums, target + 1);\n // j는 가장 오른쪽의 target을 가리키고, i는 target보다 큰 첫 번째 원소를 가리킨다\n int j = i - 1;\n // target을 찾지 못했으므로 -1을 반환한다\n if (j == -1 || nums[j] != target) {\n return -1;\n }\n // target을 찾았으므로 인덱스 j를 반환한다\n return j;\n}\n</code></pre> binary_search_edge.java<pre><code>/* 가장 오른쪽에 있는 target을 이진 탐색 */\nint binarySearchRightEdge(int[] nums, int target) {\n // 가장 왼쪽에 있는 target + 1을 찾는 문제로 바꾼다\n int i = binary_search_insertion.binarySearchInsertion(nums, target + 1);\n // j는 가장 오른쪽의 target을 가리키고, i는 target보다 큰 첫 번째 원소를 가리킨다\n int j = i - 1;\n // target을 찾지 못했으므로 -1을 반환한다\n if (j == -1 || nums[j] != target) {\n return -1;\n }\n // target을 찾았으므로 인덱스 j를 반환한다\n return j;\n}\n</code></pre> binary_search_edge.cs<pre><code>/* 가장 오른쪽에 있는 target을 이진 탐색 */\nint BinarySearchRightEdge(int[] nums, int target) {\n // 가장 왼쪽에 있는 target + 1을 찾는 문제로 바꾼다\n int i = binary_search_insertion.BinarySearchInsertion(nums, target + 1);\n // j는 가장 오른쪽의 target을 가리키고, i는 target보다 큰 첫 번째 원소를 가리킨다\n int j = i - 1;\n // target을 찾지 못했으므로 -1을 반환한다\n if (j == -1 || nums[j] != target) {\n return -1;\n }\n // target을 찾았으므로 인덱스 j를 반환한다\n return j;\n}\n</code></pre> binary_search_edge.go<pre><code>/* 가장 오른쪽에 있는 target을 이진 탐색 */\nfunc binarySearchRightEdge(nums []int, target int) int {\n // 가장 왼쪽에 있는 target + 1을 찾는 문제로 바꾼다\n i := binarySearchInsertion(nums, target+1)\n // j는 가장 오른쪽의 target을 가리키고, i는 target보다 큰 첫 번째 원소를 가리킨다\n j := i - 1\n // target을 찾지 못했으므로 -1을 반환한다\n if j == -1 || nums[j] != target {\n return -1\n }\n // target을 찾았으므로 인덱스 j를 반환한다\n return j\n}\n</code></pre> binary_search_edge.swift<pre><code>/* 가장 오른쪽에 있는 target을 이진 탐색 */\nfunc binarySearchRightEdge(nums: [Int], target: Int) -&gt; Int {\n // 가장 왼쪽에 있는 target + 1을 찾는 문제로 바꾼다\n let i = binarySearchInsertion(nums: nums, target: target + 1)\n // j는 가장 오른쪽의 target을 가리키고, i는 target보다 큰 첫 번째 원소를 가리킨다\n let j = i - 1\n // target을 찾지 못했으므로 -1을 반환한다\n if j == -1 || nums[j] != target {\n return -1\n }\n // target을 찾았으므로 인덱스 j를 반환한다\n return j\n}\n</code></pre> binary_search_edge.js<pre><code>/* 가장 오른쪽에 있는 target을 이진 탐색 */\nfunction binarySearchRightEdge(nums, target) {\n // 가장 왼쪽에 있는 target + 1을 찾는 문제로 바꾼다\n const i = binarySearchInsertion(nums, target + 1);\n // j는 가장 오른쪽의 target을 가리키고, i는 target보다 큰 첫 번째 원소를 가리킨다\n const j = i - 1;\n // target을 찾지 못했으므로 -1을 반환한다\n if (j === -1 || nums[j] !== target) {\n return -1;\n }\n // target을 찾았으므로 인덱스 j를 반환한다\n return j;\n}\n</code></pre> binary_search_edge.ts<pre><code>/* 가장 오른쪽에 있는 target을 이진 탐색 */\nfunction binarySearchRightEdge(nums: Array&lt;number&gt;, target: number): number {\n // 가장 왼쪽에 있는 target + 1을 찾는 문제로 바꾼다\n const i = binarySearchInsertion(nums, target + 1);\n // j는 가장 오른쪽의 target을 가리키고, i는 target보다 큰 첫 번째 원소를 가리킨다\n const j = i - 1;\n // target을 찾지 못했으므로 -1을 반환한다\n if (j === -1 || nums[j] !== target) {\n return -1;\n }\n // target을 찾았으므로 인덱스 j를 반환한다\n return j;\n}\n</code></pre> binary_search_edge.dart<pre><code>/* 가장 오른쪽에 있는 target을 이진 탐색 */\nint binarySearchRightEdge(List&lt;int&gt; nums, int target) {\n // 가장 왼쪽에 있는 target + 1을 찾는 문제로 바꾼다\n int i = binarySearchInsertion(nums, target + 1);\n // j는 가장 오른쪽의 target을 가리키고, i는 target보다 큰 첫 번째 원소를 가리킨다\n int j = i - 1;\n // target을 찾지 못했으므로 -1을 반환한다\n if (j == -1 || nums[j] != target) {\n return -1;\n }\n // target을 찾았으므로 인덱스 j를 반환한다\n return j;\n}\n</code></pre> binary_search_edge.rs<pre><code>/* 가장 오른쪽에 있는 target을 이진 탐색 */\nfn binary_search_right_edge(nums: &amp;[i32], target: i32) -&gt; i32 {\n // 가장 왼쪽에 있는 target + 1을 찾는 문제로 바꾼다\n let i = binary_search_insertion(nums, target + 1);\n // j는 가장 오른쪽의 target을 가리키고, i는 target보다 큰 첫 번째 원소를 가리킨다\n let j = i - 1;\n // target을 찾지 못했으므로 -1을 반환한다\n if j == -1 || nums[j as usize] != target {\n return -1;\n }\n // target을 찾았으므로 인덱스 j를 반환한다\n j\n}\n</code></pre> binary_search_edge.c<pre><code>/* 가장 오른쪽에 있는 target을 이진 탐색 */\nint binarySearchRightEdge(int *nums, int numSize, int target) {\n // 가장 왼쪽에 있는 target + 1을 찾는 문제로 바꾼다\n int i = binarySearchInsertion(nums, numSize, target + 1);\n // j는 가장 오른쪽의 target을 가리키고, i는 target보다 큰 첫 번째 원소를 가리킨다\n int j = i - 1;\n // target을 찾지 못했으므로 -1을 반환한다\n if (j == -1 || nums[j] != target) {\n return -1;\n }\n // target을 찾았으므로 인덱스 j를 반환한다\n return j;\n}\n</code></pre> binary_search_edge.kt<pre><code>/* 가장 오른쪽에 있는 target을 이진 탐색 */\nfun binarySearchRightEdge(nums: IntArray, target: Int): Int {\n // 가장 왼쪽에 있는 target + 1을 찾는 문제로 바꾼다\n val i = binarySearchInsertion(nums, target + 1)\n // j는 가장 오른쪽의 target을 가리키고, i는 target보다 큰 첫 번째 원소를 가리킨다\n val j = i - 1\n // target을 찾지 못했으므로 -1을 반환한다\n if (j == -1 || nums[j] != target) {\n return -1\n }\n // target을 찾았으므로 인덱스 j를 반환한다\n return j\n}\n</code></pre> binary_search_edge.rb<pre><code>### 가장 오른쪽에 있는 target을 이진 탐색 ###\ndef binary_search_right_edge(nums, target)\n # 가장 왼쪽에 있는 target + 1을 찾는 문제로 바꾼다\n i = binary_search_insertion(nums, target + 1)\n\n # j는 가장 오른쪽의 target을 가리키고, i는 target보다 큰 첫 번째 원소를 가리킨다\n j = i - 1\n\n # target을 찾지 못했으므로 -1을 반환한다\n return -1 if j == -1 || nums[j] != target\n\n j # target을 찾았으므로 인덱스 j를 반환한다\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 10 장   탐색","10.3   이진 탐색으로 경계 찾기"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#2","level":3,"title":"2.   원소 찾기로 변환","text":"<p>배열에 <code>target</code>이 없으면 최종적으로 \\(i\\)는 <code>target</code>보다 큰 첫 번째 원소를, \\(j\\)는 <code>target</code>보다 작은 가장 오른쪽 원소를 가리킨다.</p> <p>따라서 아래 그림(10-8)과 같이 배열에 없는 원소를 만들어 왼쪽 경계와 오른쪽 경계를 찾을 수 있다.</p> <ul> <li>가장 왼쪽 <code>target</code> 찾기: <code>target - 0.5</code>를 탐색하고 포인터 \\(i\\)를 반환하는 것으로 바꿀 수 있다.</li> <li>가장 오른쪽 <code>target</code> 찾기: <code>target + 0.5</code>를 탐색하고 포인터 \\(j\\)를 반환하는 것으로 바꿀 수 있다.</li> </ul> <p></p> <p> 그림 10-8   경계 찾기를 원소 찾기로 변환 </p> <p>코드는 생략하며, 다음 두 가지에 주의하자.</p> <ul> <li>주어진 배열에는 소수가 없으므로 값이 같은 경우의 처리는 고려하지 않아도 된다.</li> <li>이 방법은 소수를 사용하므로 함수의 변수 <code>target</code>을 부동 소수점 타입으로 바꿔야 한다(Python은 수정할 필요가 없다).</li> </ul>","path":["제 10 장   탐색","10.3   이진 탐색으로 경계 찾기"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/","level":1,"title":"10.2   이진 탐색으로 삽입 위치 찾기","text":"<p>이진 탐색은 목표 원소를 찾는 데만 쓰이는 것이 아니다. 목표 원소를 삽입할 위치를 찾는 등 여러 변형 문제에도 사용할 수 있다.</p>","path":["제 10 장   탐색","10.2   이진 탐색으로 삽입 위치 찾기"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/#1021","level":2,"title":"10.2.1   중복 원소가 없는 경우","text":"<p>문제</p> <p>중복 원소가 없는 길이 \\(n\\)의 정렬된 배열 <code>nums</code>와 원소 <code>target</code>이 주어진다. 정렬 상태를 유지하면서 <code>target</code>을 배열 <code>nums</code>에 삽입하려고 한다. 이미 <code>target</code>이 있으면 그 왼쪽에 삽입한다. 삽입 후 <code>target</code>의 배열 인덱스를 반환하라. 예시는 아래 그림(10-4)과 같다.</p> <p></p> <p> 그림 10-4   이진 탐색으로 삽입 위치를 찾는 예시 데이터 </p> <p>앞 절의 이진 탐색 코드를 재사용하려면 다음 두 질문에 답해야 한다.</p> <p>질문 1: 배열에 <code>target</code>이 있을 때 삽입 위치의 인덱스는 해당 원소의 인덱스인가?</p> <p>문제에서는 값이 같은 원소의 왼쪽에 <code>target</code>을 삽입하라고 한다. 따라서 새로 삽입하는 <code>target</code>이 기존 <code>target</code>의 자리를 차지한다. 즉, 배열에 <code>target</code>이 있으면 삽입 위치의 인덱스는 그 <code>target</code>의 인덱스다.</p> <p>질문 2: 배열에 <code>target</code>이 없을 때 삽입 위치는 어느 원소의 인덱스인가?</p> <p>이진 탐색 과정을 더 살펴보자. <code>nums[m] &lt; target</code>이면 \\(i\\)가 이동하므로 포인터 \\(i\\)는 <code>target</code> 이상인 원소에 가까워진다. 마찬가지로 포인터 \\(j\\)는 항상 <code>target</code> 이하인 원소에 가까워진다.</p> <p>따라서 이진 탐색이 끝나면 \\(i\\)는 <code>target</code>보다 큰 첫 번째 원소를, \\(j\\)는 <code>target</code>보다 작은 가장 오른쪽 원소를 가리킨다. 배열에 <code>target</code>이 없을 때 삽입 인덱스가 \\(i\\)임을 쉽게 알 수 있다. 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_insertion.py<pre><code>def binary_search_insertion_simple(nums: list[int], target: int) -&gt; int:\n \"\"\"삽입 위치의 이진 탐색(중복 원소 없음)\"\"\"\n i, j = 0, len(nums) - 1 # 닫힌 구간 [0, n-1]을 초기화한다\n while i &lt;= j:\n m = (i + j) // 2 # 중간 인덱스 m을 계산한다\n if nums[m] &lt; target:\n i = m + 1 # target은 구간 [m+1, j]에 있다\n elif nums[m] &gt; target:\n j = m - 1 # target은 구간 [i, m-1]에 있다\n else:\n return m # target을 찾았으므로 삽입 위치 m을 반환한다\n # target을 찾지 못했으므로 삽입 위치 i를 반환한다\n return i\n</code></pre> binary_search_insertion.cpp<pre><code>/* 삽입 위치의 이진 탐색(중복 원소 없음) */\nint binarySearchInsertionSimple(vector&lt;int&gt; &amp;nums, int target) {\n int i = 0, j = nums.size() - 1; // 닫힌 구간 [0, n-1]을 초기화한다\n while (i &lt;= j) {\n int m = i + (j - i) / 2; // 중간 인덱스 m을 계산한다\n if (nums[m] &lt; target) {\n i = m + 1; // target은 구간 [m+1, j]에 있다\n } else if (nums[m] &gt; target) {\n j = m - 1; // target은 구간 [i, m-1]에 있다\n } else {\n return m; // target을 찾았으므로 삽입 위치 m을 반환한다\n }\n }\n // target을 찾지 못했으므로 삽입 위치 i를 반환한다\n return i;\n}\n</code></pre> binary_search_insertion.java<pre><code>/* 삽입 위치의 이진 탐색(중복 원소 없음) */\nint binarySearchInsertionSimple(int[] nums, int target) {\n int i = 0, j = nums.length - 1; // 닫힌 구간 [0, n-1]을 초기화한다\n while (i &lt;= j) {\n int m = i + (j - i) / 2; // 중간 인덱스 m을 계산한다\n if (nums[m] &lt; target) {\n i = m + 1; // target은 구간 [m+1, j]에 있다\n } else if (nums[m] &gt; target) {\n j = m - 1; // target은 구간 [i, m-1]에 있다\n } else {\n return m; // target을 찾았으므로 삽입 위치 m을 반환한다\n }\n }\n // target을 찾지 못했으므로 삽입 위치 i를 반환한다\n return i;\n}\n</code></pre> binary_search_insertion.cs<pre><code>/* 삽입 위치의 이진 탐색(중복 원소 없음) */\nint BinarySearchInsertionSimple(int[] nums, int target) {\n int i = 0, j = nums.Length - 1; // 닫힌 구간 [0, n-1]을 초기화한다\n while (i &lt;= j) {\n int m = i + (j - i) / 2; // 중간 인덱스 m을 계산한다\n if (nums[m] &lt; target) {\n i = m + 1; // target은 구간 [m+1, j]에 있다\n } else if (nums[m] &gt; target) {\n j = m - 1; // target은 구간 [i, m-1]에 있다\n } else {\n return m; // target을 찾았으므로 삽입 위치 m을 반환한다\n }\n }\n // target을 찾지 못했으므로 삽입 위치 i를 반환한다\n return i;\n}\n</code></pre> binary_search_insertion.go<pre><code>/* 삽입 위치의 이진 탐색(중복 원소 없음) */\nfunc binarySearchInsertionSimple(nums []int, target int) int {\n // 닫힌 구간 [0, n-1]을 초기화한다\n i, j := 0, len(nums)-1\n for i &lt;= j {\n // 중간 인덱스 m을 계산한다\n m := i + (j-i)/2\n if nums[m] &lt; target {\n // target은 구간 [m+1, j]에 있다\n i = m + 1\n } else if nums[m] &gt; target {\n // target은 구간 [i, m-1]에 있다\n j = m - 1\n } else {\n // target을 찾았으므로 삽입 위치 m을 반환한다\n return m\n }\n }\n // target을 찾지 못했으므로 삽입 위치 i를 반환한다\n return i\n}\n</code></pre> binary_search_insertion.swift<pre><code>/* 삽입 위치의 이진 탐색(중복 원소 없음) */\nfunc binarySearchInsertionSimple(nums: [Int], target: Int) -&gt; Int {\n // 닫힌 구간 [0, n-1]을 초기화한다\n var i = nums.startIndex\n var j = nums.endIndex - 1\n while i &lt;= j {\n let m = i + (j - i) / 2 // 중간 인덱스 m을 계산한다\n if nums[m] &lt; target {\n i = m + 1 // target은 구간 [m+1, j]에 있다\n } else if nums[m] &gt; target {\n j = m - 1 // target은 구간 [i, m-1]에 있다\n } else {\n return m // target을 찾았으므로 삽입 위치 m을 반환한다\n }\n }\n // target을 찾지 못했으므로 삽입 위치 i를 반환한다\n return i\n}\n</code></pre> binary_search_insertion.js<pre><code>/* 삽입 위치의 이진 탐색(중복 원소 없음) */\nfunction binarySearchInsertionSimple(nums, target) {\n let i = 0,\n j = nums.length - 1; // 닫힌 구간 [0, n-1]을 초기화한다\n while (i &lt;= j) {\n const m = Math.floor(i + (j - i) / 2); // 중간 인덱스 m을 계산하며, Math.floor()로 내림한다\n if (nums[m] &lt; target) {\n i = m + 1; // target은 구간 [m+1, j]에 있다\n } else if (nums[m] &gt; target) {\n j = m - 1; // target은 구간 [i, m-1]에 있다\n } else {\n return m; // target을 찾았으므로 삽입 위치 m을 반환한다\n }\n }\n // target을 찾지 못했으므로 삽입 위치 i를 반환한다\n return i;\n}\n</code></pre> binary_search_insertion.ts<pre><code>/* 삽입 위치의 이진 탐색(중복 원소 없음) */\nfunction binarySearchInsertionSimple(\n nums: Array&lt;number&gt;,\n target: number\n): number {\n let i = 0,\n j = nums.length - 1; // 닫힌 구간 [0, n-1]을 초기화한다\n while (i &lt;= j) {\n const m = Math.floor(i + (j - i) / 2); // 중간 인덱스 m을 계산하며, Math.floor()로 내림한다\n if (nums[m] &lt; target) {\n i = m + 1; // target은 구간 [m+1, j]에 있다\n } else if (nums[m] &gt; target) {\n j = m - 1; // target은 구간 [i, m-1]에 있다\n } else {\n return m; // target을 찾았으므로 삽입 위치 m을 반환한다\n }\n }\n // target을 찾지 못했으므로 삽입 위치 i를 반환한다\n return i;\n}\n</code></pre> binary_search_insertion.dart<pre><code>/* 삽입 위치의 이진 탐색(중복 원소 없음) */\nint binarySearchInsertionSimple(List&lt;int&gt; nums, int target) {\n int i = 0, j = nums.length - 1; // 닫힌 구간 [0, n-1]을 초기화한다\n while (i &lt;= j) {\n int m = i + (j - i) ~/ 2; // 중간 인덱스 m을 계산한다\n if (nums[m] &lt; target) {\n i = m + 1; // target은 구간 [m+1, j]에 있다\n } else if (nums[m] &gt; target) {\n j = m - 1; // target은 구간 [i, m-1]에 있다\n } else {\n return m; // target을 찾았으므로 삽입 위치 m을 반환한다\n }\n }\n // target을 찾지 못했으므로 삽입 위치 i를 반환한다\n return i;\n}\n</code></pre> binary_search_insertion.rs<pre><code>/* 삽입 위치의 이진 탐색(중복 원소 없음) */\nfn binary_search_insertion_simple(nums: &amp;[i32], target: i32) -&gt; i32 {\n let (mut i, mut j) = (0, nums.len() as i32 - 1); // 닫힌 구간 [0, n-1]을 초기화한다\n while i &lt;= j {\n let m = i + (j - i) / 2; // 중간 인덱스 m을 계산한다\n if nums[m as usize] &lt; target {\n i = m + 1; // target은 구간 [m+1, j]에 있다\n } else if nums[m as usize] &gt; target {\n j = m - 1; // target은 구간 [i, m-1]에 있다\n } else {\n return m;\n }\n }\n // target을 찾지 못했으므로 삽입 위치 i를 반환한다\n i\n}\n</code></pre> binary_search_insertion.c<pre><code>/* 삽입 위치의 이진 탐색(중복 원소 없음) */\nint binarySearchInsertionSimple(int *nums, int numSize, int target) {\n int i = 0, j = numSize - 1; // 닫힌 구간 [0, n-1]을 초기화한다\n while (i &lt;= j) {\n int m = i + (j - i) / 2; // 중간 인덱스 m을 계산한다\n if (nums[m] &lt; target) {\n i = m + 1; // target은 구간 [m+1, j]에 있다\n } else if (nums[m] &gt; target) {\n j = m - 1; // target은 구간 [i, m-1]에 있다\n } else {\n return m; // target을 찾았으므로 삽입 위치 m을 반환한다\n }\n }\n // target을 찾지 못했으므로 삽입 위치 i를 반환한다\n return i;\n}\n</code></pre> binary_search_insertion.kt<pre><code>/* 삽입 위치의 이진 탐색(중복 원소 없음) */\nfun binarySearchInsertionSimple(nums: IntArray, target: Int): Int {\n var i = 0\n var j = nums.size - 1 // 닫힌 구간 [0, n-1]을 초기화한다\n while (i &lt;= j) {\n val m = i + (j - i) / 2 // 중간 인덱스 m을 계산한다\n if (nums[m] &lt; target) {\n i = m + 1 // target은 구간 [m+1, j]에 있다\n } else if (nums[m] &gt; target) {\n j = m - 1 // target은 구간 [i, m-1]에 있다\n } else {\n return m // target을 찾았으므로 삽입 위치 m을 반환한다\n }\n }\n // target을 찾지 못했으므로 삽입 위치 i를 반환한다\n return i\n}\n</code></pre> binary_search_insertion.rb<pre><code>### 삽입 위치의 이진 탐색(중복 원소 없음) ###\ndef binary_search_insertion_simple(nums, target)\n # 닫힌 구간 [0, n-1]을 초기화한다\n i, j = 0, nums.length - 1\n\n while i &lt;= j\n # 중간 인덱스 m을 계산한다\n m = (i + j) / 2\n\n if nums[m] &lt; target\n i = m + 1 # target은 구간 [m+1, j]에 있다\n elsif nums[m] &gt; target\n j = m - 1 # target은 구간 [i, m-1]에 있다\n else\n return m # target을 찾았으므로 삽입 위치 m을 반환한다\n end\n end\n\n i # target을 찾지 못했으므로 삽입 위치 i를 반환한다\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 10 장   탐색","10.2   이진 탐색으로 삽입 위치 찾기"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/#1022","level":2,"title":"10.2.2   중복 원소가 있는 경우","text":"<p>문제</p> <p>앞 문제에서 배열에 중복 원소가 있을 수 있다는 조건만 추가하고, 나머지 조건은 그대로 둔다.</p> <p>배열에 <code>target</code>이 여러 개 있다면 일반적인 이진 탐색으로는 그중 하나의 인덱스만 구할 수 있다. 그 원소의 왼쪽과 오른쪽에 <code>target</code>이 몇 개 더 있는지는 알 수 없다.</p> <p>문제에서는 목표 원소를 가장 왼쪽에 삽입하라고 한다. 따라서 배열에서 가장 왼쪽 <code>target</code>의 인덱스를 찾아야 한다. 먼저 아래 그림(10-5)과 같은 방법을 생각해 볼 수 있다.</p> <ol> <li>이진 탐색으로 임의의 <code>target</code>의 인덱스를 구하고 이를 \\(k\\)라고 한다.</li> <li>인덱스 \\(k\\)부터 왼쪽으로 선형 순회하여 가장 왼쪽 <code>target</code>을 찾으면 반환한다.</li> </ol> <p></p> <p> 그림 10-5   선형 탐색으로 중복 원소의 삽입 위치 찾기 </p> <p>이 방법도 가능하지만 선형 탐색이 포함되어 시간 복잡도가 \\(O(n)\\)이다. 배열에 중복된 <code>target</code>이 많으면 매우 비효율적이다.</p> <p>이제 이진 탐색 코드를 확장해 보자. 아래 그림(10-6)과 같이 전체 흐름은 그대로 유지한다. 매 반복에서 중간 인덱스 \\(m\\)을 계산하고, <code>target</code>과 <code>nums[m]</code>의 대소 관계에 따라 다음과 같이 처리한다.</p> <ul> <li><code>nums[m] &lt; target</code> 또는 <code>nums[m] &gt; target</code>이면 아직 <code>target</code>을 찾지 못했으므로, 일반적인 이진 탐색처럼 구간을 줄여 포인터 \\(i\\)와 \\(j\\)가 <code>target</code>에 가까워지게 한다.</li> <li><code>nums[m] == target</code>이면 <code>target</code>보다 작은 원소는 구간 \\([i, m - 1]\\)에 있으므로, \\(j = m - 1\\)로 구간을 줄여 포인터 \\(j\\)가 <code>target</code>보다 작은 원소에 가까워지게 한다.</li> </ul> <p>반복이 끝나면 \\(i\\)는 가장 왼쪽 <code>target</code>을, \\(j\\)는 <code>target</code>보다 작은 가장 오른쪽 원소를 가리킨다. 따라서 인덱스 \\(i\\)가 삽입 위치다.</p> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt;&lt;6&gt;&lt;7&gt;&lt;8&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 10-6   이진 탐색으로 중복 원소의 삽입 위치를 찾는 단계 </p> <p>아래 코드를 보면 <code>nums[m] &gt; target</code>과 <code>nums[m] == target</code> 분기에서 수행하는 연산이 같으므로 두 분기를 합칠 수 있다.</p> <p>그렇더라도 조건을 나누어 두면 논리가 더 명확하고 가독성이 좋으므로 그대로 둘 수 있다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_insertion.py<pre><code>def binary_search_insertion(nums: list[int], target: int) -&gt; int:\n \"\"\"삽입 위치의 이진 탐색(중복 원소 있음)\"\"\"\n i, j = 0, len(nums) - 1 # 닫힌 구간 [0, n-1]을 초기화한다\n while i &lt;= j:\n m = (i + j) // 2 # 중간 인덱스 m을 계산한다\n if nums[m] &lt; target:\n i = m + 1 # target은 구간 [m+1, j]에 있다\n elif nums[m] &gt; target:\n j = m - 1 # target은 구간 [i, m-1]에 있다\n else:\n j = m - 1 # target보다 작은 가장 오른쪽 원소는 구간 [i, m-1]에 있다\n # 삽입 위치 i를 반환한다\n return i\n</code></pre> binary_search_insertion.cpp<pre><code>/* 삽입 위치의 이진 탐색(중복 원소 있음) */\nint binarySearchInsertion(vector&lt;int&gt; &amp;nums, int target) {\n int i = 0, j = nums.size() - 1; // 닫힌 구간 [0, n-1]을 초기화한다\n while (i &lt;= j) {\n int m = i + (j - i) / 2; // 중간 인덱스 m을 계산한다\n if (nums[m] &lt; target) {\n i = m + 1; // target은 구간 [m+1, j]에 있다\n } else if (nums[m] &gt; target) {\n j = m - 1; // target은 구간 [i, m-1]에 있다\n } else {\n j = m - 1; // target보다 작은 가장 오른쪽 원소는 구간 [i, m-1]에 있다\n }\n }\n // 삽입 위치 i를 반환한다\n return i;\n}\n</code></pre> binary_search_insertion.java<pre><code>/* 삽입 위치의 이진 탐색(중복 원소 있음) */\nint binarySearchInsertion(int[] nums, int target) {\n int i = 0, j = nums.length - 1; // 닫힌 구간 [0, n-1]을 초기화한다\n while (i &lt;= j) {\n int m = i + (j - i) / 2; // 중간 인덱스 m을 계산한다\n if (nums[m] &lt; target) {\n i = m + 1; // target은 구간 [m+1, j]에 있다\n } else if (nums[m] &gt; target) {\n j = m - 1; // target은 구간 [i, m-1]에 있다\n } else {\n j = m - 1; // target보다 작은 가장 오른쪽 원소는 구간 [i, m-1]에 있다\n }\n }\n // 삽입 위치 i를 반환한다\n return i;\n}\n</code></pre> binary_search_insertion.cs<pre><code>/* 삽입 위치의 이진 탐색(중복 원소 있음) */\nint BinarySearchInsertion(int[] nums, int target) {\n int i = 0, j = nums.Length - 1; // 닫힌 구간 [0, n-1]을 초기화한다\n while (i &lt;= j) {\n int m = i + (j - i) / 2; // 중간 인덱스 m을 계산한다\n if (nums[m] &lt; target) {\n i = m + 1; // target은 구간 [m+1, j]에 있다\n } else if (nums[m] &gt; target) {\n j = m - 1; // target은 구간 [i, m-1]에 있다\n } else {\n j = m - 1; // target보다 작은 가장 오른쪽 원소는 구간 [i, m-1]에 있다\n }\n }\n // 삽입 위치 i를 반환한다\n return i;\n}\n</code></pre> binary_search_insertion.go<pre><code>/* 삽입 위치의 이진 탐색(중복 원소 있음) */\nfunc binarySearchInsertion(nums []int, target int) int {\n // 닫힌 구간 [0, n-1]을 초기화한다\n i, j := 0, len(nums)-1\n for i &lt;= j {\n // 중간 인덱스 m을 계산한다\n m := i + (j-i)/2\n if nums[m] &lt; target {\n // target은 구간 [m+1, j]에 있다\n i = m + 1\n } else if nums[m] &gt; target {\n // target은 구간 [i, m-1]에 있다\n j = m - 1\n } else {\n // target보다 작은 가장 오른쪽 원소는 구간 [i, m-1]에 있다\n j = m - 1\n }\n }\n // 삽입 위치 i를 반환한다\n return i\n}\n</code></pre> binary_search_insertion.swift<pre><code>/* 삽입 위치의 이진 탐색(중복 원소 있음) */\nfunc binarySearchInsertion(nums: [Int], target: Int) -&gt; Int {\n // 닫힌 구간 [0, n-1]을 초기화한다\n var i = nums.startIndex\n var j = nums.endIndex - 1\n while i &lt;= j {\n let m = i + (j - i) / 2 // 중간 인덱스 m을 계산한다\n if nums[m] &lt; target {\n i = m + 1 // target은 구간 [m+1, j]에 있다\n } else if nums[m] &gt; target {\n j = m - 1 // target은 구간 [i, m-1]에 있다\n } else {\n j = m - 1 // target보다 작은 가장 오른쪽 원소는 구간 [i, m-1]에 있다\n }\n }\n // 삽입 위치 i를 반환한다\n return i\n}\n</code></pre> binary_search_insertion.js<pre><code>/* 삽입 위치의 이진 탐색(중복 원소 있음) */\nfunction binarySearchInsertion(nums, target) {\n let i = 0,\n j = nums.length - 1; // 닫힌 구간 [0, n-1]을 초기화한다\n while (i &lt;= j) {\n const m = Math.floor(i + (j - i) / 2); // 중간 인덱스 m을 계산하며, Math.floor()로 내림한다\n if (nums[m] &lt; target) {\n i = m + 1; // target은 구간 [m+1, j]에 있다\n } else if (nums[m] &gt; target) {\n j = m - 1; // target은 구간 [i, m-1]에 있다\n } else {\n j = m - 1; // target보다 작은 가장 오른쪽 원소는 구간 [i, m-1]에 있다\n }\n }\n // 삽입 위치 i를 반환한다\n return i;\n}\n</code></pre> binary_search_insertion.ts<pre><code>/* 삽입 위치의 이진 탐색(중복 원소 있음) */\nfunction binarySearchInsertion(nums: Array&lt;number&gt;, target: number): number {\n let i = 0,\n j = nums.length - 1; // 닫힌 구간 [0, n-1]을 초기화한다\n while (i &lt;= j) {\n const m = Math.floor(i + (j - i) / 2); // 중간 인덱스 m을 계산하며, Math.floor()로 내림한다\n if (nums[m] &lt; target) {\n i = m + 1; // target은 구간 [m+1, j]에 있다\n } else if (nums[m] &gt; target) {\n j = m - 1; // target은 구간 [i, m-1]에 있다\n } else {\n j = m - 1; // target보다 작은 가장 오른쪽 원소는 구간 [i, m-1]에 있다\n }\n }\n // 삽입 위치 i를 반환한다\n return i;\n}\n</code></pre> binary_search_insertion.dart<pre><code>/* 삽입 위치의 이진 탐색(중복 원소 있음) */\nint binarySearchInsertion(List&lt;int&gt; nums, int target) {\n int i = 0, j = nums.length - 1; // 닫힌 구간 [0, n-1]을 초기화한다\n while (i &lt;= j) {\n int m = i + (j - i) ~/ 2; // 중간 인덱스 m을 계산한다\n if (nums[m] &lt; target) {\n i = m + 1; // target은 구간 [m+1, j]에 있다\n } else if (nums[m] &gt; target) {\n j = m - 1; // target은 구간 [i, m-1]에 있다\n } else {\n j = m - 1; // target보다 작은 가장 오른쪽 원소는 구간 [i, m-1]에 있다\n }\n }\n // 삽입 위치 i를 반환한다\n return i;\n}\n</code></pre> binary_search_insertion.rs<pre><code>/* 삽입 위치의 이진 탐색(중복 원소 있음) */\npub fn binary_search_insertion(nums: &amp;[i32], target: i32) -&gt; i32 {\n let (mut i, mut j) = (0, nums.len() as i32 - 1); // 닫힌 구간 [0, n-1]을 초기화한다\n while i &lt;= j {\n let m = i + (j - i) / 2; // 중간 인덱스 m을 계산한다\n if nums[m as usize] &lt; target {\n i = m + 1; // target은 구간 [m+1, j]에 있다\n } else if nums[m as usize] &gt; target {\n j = m - 1; // target은 구간 [i, m-1]에 있다\n } else {\n j = m - 1; // target보다 작은 가장 오른쪽 원소는 구간 [i, m-1]에 있다\n }\n }\n // 삽입 위치 i를 반환한다\n i\n}\n</code></pre> binary_search_insertion.c<pre><code>/* 삽입 위치의 이진 탐색(중복 원소 있음) */\nint binarySearchInsertion(int *nums, int numSize, int target) {\n int i = 0, j = numSize - 1; // 닫힌 구간 [0, n-1]을 초기화한다\n while (i &lt;= j) {\n int m = i + (j - i) / 2; // 중간 인덱스 m을 계산한다\n if (nums[m] &lt; target) {\n i = m + 1; // target은 구간 [m+1, j]에 있다\n } else if (nums[m] &gt; target) {\n j = m - 1; // target은 구간 [i, m-1]에 있다\n } else {\n j = m - 1; // target보다 작은 가장 오른쪽 원소는 구간 [i, m-1]에 있다\n }\n }\n // 삽입 위치 i를 반환한다\n return i;\n}\n</code></pre> binary_search_insertion.kt<pre><code>/* 삽입 위치의 이진 탐색(중복 원소 있음) */\nfun binarySearchInsertion(nums: IntArray, target: Int): Int {\n var i = 0\n var j = nums.size - 1 // 닫힌 구간 [0, n-1]을 초기화한다\n while (i &lt;= j) {\n val m = i + (j - i) / 2 // 중간 인덱스 m을 계산한다\n if (nums[m] &lt; target) {\n i = m + 1 // target은 구간 [m+1, j]에 있다\n } else if (nums[m] &gt; target) {\n j = m - 1 // target은 구간 [i, m-1]에 있다\n } else {\n j = m - 1 // target보다 작은 가장 오른쪽 원소는 구간 [i, m-1]에 있다\n }\n }\n // 삽입 위치 i를 반환한다\n return i\n}\n</code></pre> binary_search_insertion.rb<pre><code>### 삽입 위치의 이진 탐색(중복 원소 있음) ###\ndef binary_search_insertion(nums, target)\n # 닫힌 구간 [0, n-1]을 초기화한다\n i, j = 0, nums.length - 1\n\n while i &lt;= j\n # 중간 인덱스 m을 계산한다\n m = (i + j) / 2\n\n if nums[m] &lt; target\n i = m + 1 # target은 구간 [m+1, j]에 있다\n elsif nums[m] &gt; target\n j = m - 1 # target은 구간 [i, m-1]에 있다\n else\n j = m - 1 # target보다 작은 가장 오른쪽 원소는 구간 [i, m-1]에 있다\n end\n end\n\n i # 삽입 위치 i를 반환한다\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>참고</p> <p>이 절의 코드는 모두 '양끝이 닫힌 구간'을 사용한다. 관심 있는 독자는 '왼쪽은 닫히고 오른쪽은 열린 구간'으로 직접 구현해 보자.</p> <p>종합하면 이진 탐색은 포인터 \\(i\\)와 \\(j\\)에 각각 탐색 목표를 정해 주는 과정이다. 목표는 특정 원소(예: <code>target</code>)일 수도 있고, 원소의 범위(예: <code>target</code>보다 작은 원소)일 수도 있다.</p> <p>구간을 계속 반으로 나누는 동안 포인터 \\(i\\)와 \\(j\\)는 미리 정한 목표에 점차 가까워진다. 결국 답을 찾거나 경계를 넘어선 뒤 멈춘다.</p>","path":["제 10 장   탐색","10.2   이진 탐색으로 삽입 위치 찾기"],"tags":[]},{"location":"chapter_searching/exercises/","level":1,"title":"10.7   연습 문제","text":"","path":["제 10 장   탐색","10.7   연습 문제"],"tags":[]},{"location":"chapter_searching/exercises/#1071","level":2,"title":"10.7.1   개념 확인","text":"","path":["제 10 장   탐색","10.7   연습 문제"],"tags":[]},{"location":"chapter_searching/exercises/#1","level":3,"title":"1.   이진 탐색은 탐색 구간을 어떻게 줄이는가","text":"<p>정렬된 배열 <code>[2, 5, 8, 12, 16, 23, 38]</code>에서 16을 찾는다. 양 끝을 포함하는 닫힌 구간 <code>[i, j]</code>를 사용하며, 중간 인덱스는 \\(m=i+(j-i)/2\\)다. 나눗셈 결과의 소수 부분은 버린다.</p> <p>목표 값을 찾을 때까지 매 단계의 <code>(i, j, m)</code>과 중간 원소, 다음 단계에서 구간을 어떻게 줄이는지 적어 보자.</p> 참고 답안 <p>각 단계의 탐색 과정은 다음과 같다.</p> 단계 <code>(i, j, m)</code> 중간 원소 다음 동작 1 <code>(0, 6, 3)</code> 12 <code>12 &lt; 16</code>이므로 <code>i = 4</code>로 설정 2 <code>(4, 6, 5)</code> 23 <code>23 &gt; 16</code>이므로 <code>j = 4</code>로 설정 3 <code>(4, 4, 4)</code> 16 목표 값을 찾았으므로 인덱스 4를 반환 <p>배열이 정렬되어 있으므로 중간 값이 목표 값보다 작으면 중간 원소와 그 왼쪽을 제외할 수 있다. 중간 값이 목표 값보다 크면 중간 원소와 그 오른쪽을 제외할 수 있다.</p>","path":["제 10 장   탐색","10.7   연습 문제"],"tags":[]},{"location":"chapter_searching/exercises/#2","level":3,"title":"2.   중복 원소의 왼쪽 경계와 오른쪽 경계","text":"<p>배열 <code>[1, 2, 2, 2, 4, 6]</code>에서 숫자 2를 찾는다. 한 학생이 이진 탐색으로 인덱스 2에서 목표 값을 찾자마자 반환한 뒤, “인덱스 2가 숫자 2의 왼쪽 경계다.”라고 말했다.</p> <ol> <li>이 학생의 말이 맞는가? 숫자 2의 왼쪽 경계와 오른쪽 경계는 각각 어디인지 이유와 함께 설명하라.</li> <li>왼쪽 경계를 찾을 때 중간 원소가 목표 값과 같다면, 다음에는 어느 쪽을 계속 탐색해야 하는가?</li> <li>오른쪽 경계를 찾을 때는 어느 쪽을 계속 탐색해야 하는가? 탐색 과정 전체를 적지 말고 방향만 설명하라.</li> </ol> 참고 답안 <ol> <li> <p>이 학생의 말은 틀렸다. 2를 하나 찾자마자 반환하면 어떤 2를 찾았다는 것만 보장할 수 있고, 가장 왼쪽이나 가장 오른쪽의 2를 찾았다고 보장할 수는 없다. 이 문제에서 왼쪽 경계는 인덱스 1, 오른쪽 경계는 인덱스 3이다.</p> </li> <li> <p>왼쪽 경계를 찾을 때는 중간 원소가 2와 같더라도 왼쪽을 계속 탐색해야 한다. 예를 들어 양 끝을 포함하는 닫힌 구간을 사용하는 경우 <code>j = m - 1</code>로 설정한다.</p> </li> <li> <p>오른쪽 경계를 찾을 때는 중간 원소가 2와 같으면 오른쪽을 계속 탐색해야 한다. 예를 들어 <code>i = m + 1</code>로 설정한다.</p> </li> </ol>","path":["제 10 장   탐색","10.7   연습 문제"],"tags":[]},{"location":"chapter_searching/exercises/#3","level":3,"title":"3.   데이터에 따라 어떤 탐색 방법을 선택해야 하는가","text":"<p>다음 세 상황에 알맞은 방법을 ‘선형 탐색, 이진 탐색, 해시 테이블’ 중에서 고르고 이유를 설명하라.</p> <ol> <li>이미 정렬되어 있고 더 이상 변하지 않는 정수 \\(10^7\\)개에서 반복해서 값을 찾으며, 다른 자료 구조를 추가로 만들지 않는다.</li> <li>삽입과 삭제가 빈번한 데이터 집합에서 특정 키가 있는지 반복해서 확인한다. 정렬 상태를 유지할 필요가 없고 범위 탐색도 하지 않는다.</li> <li>정렬되지 않은 배열에서 특정 값을 한 번만 찾는다.</li> </ol> 참고 답안 <ol> <li>이진 탐색: 데이터가 정렬되어 있고 변하지 않으므로 \\(O(\\log n)\\) 시간에 탐색할 수 있으며, 추가 공간도 필요하지 않다.</li> <li>해시 테이블: 해시 함수가 키를 각 버킷에 비교적 고르게 분산한다면 삽입, 삭제, 키를 이용한 탐색의 평균 시간 복잡도가 모두 \\(O(1)\\)이다.</li> <li> <p>처음부터 끝까지 직접 순회: 한 번만 찾는 경우에는 정렬하거나 해시 테이블을 만드는 데에도 먼저 배열 전체를 처리해야 하므로, 이번 작업의 전체 작업량이 줄어들지 않는다.</p> <p>어떤 방법을 선택할지는 데이터의 정렬 여부, 추가 자료 구조의 허용 여부, 조회 횟수, 수행해야 하는 연산에 따라 달라진다.</p> </li> </ol>","path":["제 10 장   탐색","10.7   연습 문제"],"tags":[]},{"location":"chapter_searching/exercises/#1072","level":2,"title":"10.7.2   프로그래밍 연습","text":"","path":["제 10 장   탐색","10.7   연습 문제"],"tags":[]},{"location":"chapter_searching/exercises/#1_1","level":3,"title":"1.   정렬된 배열의 이진 탐색","text":"<p>엄격한 오름차순으로 정렬된 정수 배열 <code>nums</code>와 목표 값 <code>target</code>이 주어진다. 이진 탐색으로 <code>target</code>을 찾아라. 존재하면 배열 인덱스를 반환하고, 존재하지 않으면 -1을 반환하라.</p> 풀이 힌트 <ol> <li>초기 구간은 left = 0, right = n - 1이며, 구간이 비어 있지 않을 조건은 left &lt;= right다.</li> <li>mid = left + (right - left) // 2로 중간 인덱스를 계산한다.</li> <li><code>nums[mid]</code>가 <code>target</code>보다 작으면 왼쪽 경계를 <code>mid + 1</code>로 옮긴다. <code>nums[mid]</code>가 <code>target</code>보다 크면 오른쪽 경계를 <code>mid - 1</code>로 옮긴다. 같으면 즉시 반환한다.</li> </ol> <p>LeetCode</p>","path":["제 10 장   탐색","10.7   연습 문제"],"tags":[]},{"location":"chapter_searching/exercises/#2_1","level":3,"title":"2.   정렬된 배열의 삽입 위치","text":"<p>엄격한 오름차순으로 정렬된 정수 배열 <code>nums</code>와 목표 값 <code>target</code>이 주어진다.</p> <p><code>target</code>이 이미 배열에 있으면 그 인덱스를 반환하라. 없으면 <code>target</code>을 삽입해도 엄격한 오름차순을 유지할 수 있는 위치를 반환하라. 답은 0일 수도 있고 배열 길이와 같을 수도 있다. 이진 탐색을 사용하라.</p> 풀이 힌트 <ol> <li>답은 0일 수도 있고 배열 길이 n일 수도 있다.</li> <li>양 끝을 포함하는 닫힌 구간을 사용할 때 <code>nums[mid]</code>가 <code>target</code>보다 크거나 같으면 <code>right = mid - 1</code>로 설정하여 더 왼쪽의 위치를 계속 확인한다. 그렇지 않으면 <code>left = mid + 1</code>로 설정한다.</li> <li>반복문이 끝나면 left가 삽입 위치다.</li> </ol> <p>LeetCode</p>","path":["제 10 장   탐색","10.7   연습 문제"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/","level":1,"title":"10.4   해시를 이용한 최적화 전략","text":"<p>알고리즘 문제에서는 선형 탐색을 해시 탐색으로 바꾸어 시간 복잡도를 줄이는 경우가 많다. 한 가지 문제를 통해 더 자세히 알아보자.</p> <p>문제</p> <p>정수 배열 <code>nums</code>와 목표 원소 <code>target</code>이 주어진다. 배열에서 '합'이 <code>target</code>인 두 원소를 찾아 그 인덱스를 반환하라. 답이 여러 개라면 아무거나 하나를 반환해도 된다.</p>","path":["제 10 장   탐색","10.4   해시를 이용한 최적화 전략"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/#1041","level":2,"title":"10.4.1   선형 탐색: 시간을 더 써서 공간 절약","text":"<p>가능한 모든 조합을 직접 순회해 보자. 아래 그림(10-9)과 같이 이중 반복문을 사용하여 매번 두 정수의 합이 <code>target</code>인지 확인하고, 맞으면 두 인덱스를 반환한다.</p> <p></p> <p> 그림 10-9   선형 탐색으로 두 수의 합 구하기 </p> <p>코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby two_sum.py<pre><code>def two_sum_brute_force(nums: list[int], target: int) -&gt; list[int]:\n \"\"\"방법 1: 완전 열거\"\"\"\n # 이중 반복문이며 시간 복잡도는 O(n^2)이다\n for i in range(len(nums) - 1):\n for j in range(i + 1, len(nums)):\n if nums[i] + nums[j] == target:\n return [i, j]\n return []\n</code></pre> two_sum.cpp<pre><code>/* 방법 1: 완전 열거 */\nvector&lt;int&gt; twoSumBruteForce(vector&lt;int&gt; &amp;nums, int target) {\n int size = nums.size();\n // 이중 반복문이며 시간 복잡도는 O(n^2)이다\n for (int i = 0; i &lt; size - 1; i++) {\n for (int j = i + 1; j &lt; size; j++) {\n if (nums[i] + nums[j] == target)\n return {i, j};\n }\n }\n return {};\n}\n</code></pre> two_sum.java<pre><code>/* 방법 1: 완전 열거 */\nint[] twoSumBruteForce(int[] nums, int target) {\n int size = nums.length;\n // 이중 반복문이며 시간 복잡도는 O(n^2)이다\n for (int i = 0; i &lt; size - 1; i++) {\n for (int j = i + 1; j &lt; size; j++) {\n if (nums[i] + nums[j] == target)\n return new int[] { i, j };\n }\n }\n return new int[0];\n}\n</code></pre> two_sum.cs<pre><code>/* 방법 1: 완전 열거 */\nint[] TwoSumBruteForce(int[] nums, int target) {\n int size = nums.Length;\n // 이중 반복문이며 시간 복잡도는 O(n^2)이다\n for (int i = 0; i &lt; size - 1; i++) {\n for (int j = i + 1; j &lt; size; j++) {\n if (nums[i] + nums[j] == target)\n return [i, j];\n }\n }\n return [];\n}\n</code></pre> two_sum.go<pre><code>/* 방법 1: 완전 열거 */\nfunc twoSumBruteForce(nums []int, target int) []int {\n size := len(nums)\n // 이중 반복문이며 시간 복잡도는 O(n^2)이다\n for i := 0; i &lt; size-1; i++ {\n for j := i + 1; j &lt; size; j++ {\n if nums[i]+nums[j] == target {\n return []int{i, j}\n }\n }\n }\n return nil\n}\n</code></pre> two_sum.swift<pre><code>/* 방법 1: 완전 열거 */\nfunc twoSumBruteForce(nums: [Int], target: Int) -&gt; [Int] {\n // 이중 반복문이며 시간 복잡도는 O(n^2)이다\n for i in nums.indices.dropLast() {\n for j in nums.indices.dropFirst(i + 1) {\n if nums[i] + nums[j] == target {\n return [i, j]\n }\n }\n }\n return [0]\n}\n</code></pre> two_sum.js<pre><code>/* 방법 1: 완전 열거 */\nfunction twoSumBruteForce(nums, target) {\n const n = nums.length;\n // 이중 반복문이며 시간 복잡도는 O(n^2)이다\n for (let i = 0; i &lt; n; i++) {\n for (let j = i + 1; j &lt; n; j++) {\n if (nums[i] + nums[j] === target) {\n return [i, j];\n }\n }\n }\n return [];\n}\n</code></pre> two_sum.ts<pre><code>/* 방법 1: 완전 열거 */\nfunction twoSumBruteForce(nums: number[], target: number): number[] {\n const n = nums.length;\n // 이중 반복문이며 시간 복잡도는 O(n^2)이다\n for (let i = 0; i &lt; n; i++) {\n for (let j = i + 1; j &lt; n; j++) {\n if (nums[i] + nums[j] === target) {\n return [i, j];\n }\n }\n }\n return [];\n}\n</code></pre> two_sum.dart<pre><code>/* 방법 1: 완전 열거 */\nList&lt;int&gt; twoSumBruteForce(List&lt;int&gt; nums, int target) {\n int size = nums.length;\n // 이중 반복문이며 시간 복잡도는 O(n^2)이다\n for (var i = 0; i &lt; size - 1; i++) {\n for (var j = i + 1; j &lt; size; j++) {\n if (nums[i] + nums[j] == target) return [i, j];\n }\n }\n return [0];\n}\n</code></pre> two_sum.rs<pre><code>/* 방법 1: 완전 열거 */\npub fn two_sum_brute_force(nums: &amp;Vec&lt;i32&gt;, target: i32) -&gt; Option&lt;Vec&lt;i32&gt;&gt; {\n let size = nums.len();\n // 이중 반복문이며 시간 복잡도는 O(n^2)이다\n for i in 0..size - 1 {\n for j in i + 1..size {\n if nums[i] + nums[j] == target {\n return Some(vec![i as i32, j as i32]);\n }\n }\n }\n None\n}\n</code></pre> two_sum.c<pre><code>/* 방법 1: 완전 열거 */\nint *twoSumBruteForce(int *nums, int numsSize, int target, int *returnSize) {\n for (int i = 0; i &lt; numsSize; ++i) {\n for (int j = i + 1; j &lt; numsSize; ++j) {\n if (nums[i] + nums[j] == target) {\n int *res = malloc(sizeof(int) * 2);\n res[0] = i, res[1] = j;\n *returnSize = 2;\n return res;\n }\n }\n }\n *returnSize = 0;\n return NULL;\n}\n</code></pre> two_sum.kt<pre><code>/* 방법 1: 완전 열거 */\nfun twoSumBruteForce(nums: IntArray, target: Int): IntArray {\n val size = nums.size\n // 이중 반복문이며 시간 복잡도는 O(n^2)이다\n for (i in 0..&lt;size - 1) {\n for (j in i + 1..&lt;size) {\n if (nums[i] + nums[j] == target) return intArrayOf(i, j)\n }\n }\n return IntArray(0)\n}\n</code></pre> two_sum.rb<pre><code>### 방법 1: 완전 열거 ###\ndef two_sum_brute_force(nums, target)\n # 이중 반복문이며 시간 복잡도는 O(n^2)이다\n for i in 0...(nums.length - 1)\n for j in (i + 1)...nums.length\n return [i, j] if nums[i] + nums[j] == target\n end\n end\n\n []\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>이 방법의 시간 복잡도는 \\(O(n^2)\\), 공간 복잡도는 \\(O(1)\\)이다. 데이터가 많으면 시간이 매우 오래 걸린다.</p>","path":["제 10 장   탐색","10.4   해시를 이용한 최적화 전략"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/#1042","level":2,"title":"10.4.2   해시 탐색: 공간을 더 써서 시간 절약","text":"<p>키와 값으로 각각 배열 원소와 그 인덱스를 저장하는 해시 테이블을 사용해 보자. 배열을 순회하면서 매번 아래 그림(10-10)의 단계를 수행한다.</p> <ol> <li>수 <code>target - nums[i]</code>가 해시 테이블에 있는지 확인하고, 있으면 두 원소의 인덱스를 바로 반환한다.</li> <li><code>nums[i]</code>와 인덱스 <code>i</code>로 이루어진 키-값 쌍을 해시 테이블에 추가한다.</li> </ol> &lt;1&gt;&lt;2&gt;&lt;3&gt; <p></p> <p></p> <p></p> <p> 그림 10-10   보조 해시 테이블로 두 수의 합 구하기 </p> <p>구현 코드는 다음과 같으며, 단일 반복문만 있으면 된다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby two_sum.py<pre><code>def two_sum_hash_table(nums: list[int], target: int) -&gt; list[int]:\n \"\"\"방법 2: 보조 해시 테이블\"\"\"\n # 보조 해시 테이블의 공간 복잡도는 O(n)이다\n dic = {}\n # 단일 반복문이며 시간 복잡도는 O(n)이다\n for i in range(len(nums)):\n if target - nums[i] in dic:\n return [dic[target - nums[i]], i]\n dic[nums[i]] = i\n return []\n</code></pre> two_sum.cpp<pre><code>/* 방법 2: 보조 해시 테이블 */\nvector&lt;int&gt; twoSumHashTable(vector&lt;int&gt; &amp;nums, int target) {\n int size = nums.size();\n // 보조 해시 테이블의 공간 복잡도는 O(n)이다\n unordered_map&lt;int, int&gt; dic;\n // 단일 반복문이며 시간 복잡도는 O(n)이다\n for (int i = 0; i &lt; size; i++) {\n if (dic.find(target - nums[i]) != dic.end()) {\n return {dic[target - nums[i]], i};\n }\n dic.emplace(nums[i], i);\n }\n return {};\n}\n</code></pre> two_sum.java<pre><code>/* 방법 2: 보조 해시 테이블 */\nint[] twoSumHashTable(int[] nums, int target) {\n int size = nums.length;\n // 보조 해시 테이블의 공간 복잡도는 O(n)이다\n Map&lt;Integer, Integer&gt; dic = new HashMap&lt;&gt;();\n // 단일 반복문이며 시간 복잡도는 O(n)이다\n for (int i = 0; i &lt; size; i++) {\n if (dic.containsKey(target - nums[i])) {\n return new int[] { dic.get(target - nums[i]), i };\n }\n dic.put(nums[i], i);\n }\n return new int[0];\n}\n</code></pre> two_sum.cs<pre><code>/* 방법 2: 보조 해시 테이블 */\nint[] TwoSumHashTable(int[] nums, int target) {\n int size = nums.Length;\n // 보조 해시 테이블의 공간 복잡도는 O(n)이다\n Dictionary&lt;int, int&gt; dic = [];\n // 단일 반복문이며 시간 복잡도는 O(n)이다\n for (int i = 0; i &lt; size; i++) {\n if (dic.ContainsKey(target - nums[i])) {\n return [dic[target - nums[i]], i];\n }\n dic.Add(nums[i], i);\n }\n return [];\n}\n</code></pre> two_sum.go<pre><code>/* 방법 2: 보조 해시 테이블 */\nfunc twoSumHashTable(nums []int, target int) []int {\n // 보조 해시 테이블의 공간 복잡도는 O(n)이다\n hashTable := map[int]int{}\n // 단일 반복문이며 시간 복잡도는 O(n)이다\n for idx, val := range nums {\n if preIdx, ok := hashTable[target-val]; ok {\n return []int{preIdx, idx}\n }\n hashTable[val] = idx\n }\n return nil\n}\n</code></pre> two_sum.swift<pre><code>/* 방법 2: 보조 해시 테이블 */\nfunc twoSumHashTable(nums: [Int], target: Int) -&gt; [Int] {\n // 보조 해시 테이블의 공간 복잡도는 O(n)이다\n var dic: [Int: Int] = [:]\n // 단일 반복문이며 시간 복잡도는 O(n)이다\n for i in nums.indices {\n if let j = dic[target - nums[i]] {\n return [j, i]\n }\n dic[nums[i]] = i\n }\n return [0]\n}\n</code></pre> two_sum.js<pre><code>/* 방법 2: 보조 해시 테이블 */\nfunction twoSumHashTable(nums, target) {\n // 보조 해시 테이블의 공간 복잡도는 O(n)이다\n let m = {};\n // 단일 반복문이며 시간 복잡도는 O(n)이다\n for (let i = 0; i &lt; nums.length; i++) {\n if (m[target - nums[i]] !== undefined) {\n return [m[target - nums[i]], i];\n } else {\n m[nums[i]] = i;\n }\n }\n return [];\n}\n</code></pre> two_sum.ts<pre><code>/* 방법 2: 보조 해시 테이블 */\nfunction twoSumHashTable(nums: number[], target: number): number[] {\n // 보조 해시 테이블의 공간 복잡도는 O(n)이다\n let m: Map&lt;number, number&gt; = new Map();\n // 단일 반복문이며 시간 복잡도는 O(n)이다\n for (let i = 0; i &lt; nums.length; i++) {\n let index = m.get(target - nums[i]);\n if (index !== undefined) {\n return [index, i];\n } else {\n m.set(nums[i], i);\n }\n }\n return [];\n}\n</code></pre> two_sum.dart<pre><code>/* 방법 2: 보조 해시 테이블 */\nList&lt;int&gt; twoSumHashTable(List&lt;int&gt; nums, int target) {\n int size = nums.length;\n // 보조 해시 테이블의 공간 복잡도는 O(n)이다\n Map&lt;int, int&gt; dic = HashMap();\n // 단일 반복문이며 시간 복잡도는 O(n)이다\n for (var i = 0; i &lt; size; i++) {\n if (dic.containsKey(target - nums[i])) {\n return [dic[target - nums[i]]!, i];\n }\n dic.putIfAbsent(nums[i], () =&gt; i);\n }\n return [0];\n}\n</code></pre> two_sum.rs<pre><code>/* 방법 2: 보조 해시 테이블 */\npub fn two_sum_hash_table(nums: &amp;Vec&lt;i32&gt;, target: i32) -&gt; Option&lt;Vec&lt;i32&gt;&gt; {\n // 보조 해시 테이블의 공간 복잡도는 O(n)이다\n let mut dic = HashMap::new();\n // 단일 반복문이며 시간 복잡도는 O(n)이다\n for (i, num) in nums.iter().enumerate() {\n match dic.get(&amp;(target - num)) {\n Some(v) =&gt; return Some(vec![*v as i32, i as i32]),\n None =&gt; dic.insert(num, i as i32),\n };\n }\n None\n}\n</code></pre> two_sum.c<pre><code>/* 해시 테이블 */\ntypedef struct {\n int key;\n int val;\n UT_hash_handle hh; // uthash.h를 기반으로 구현한다\n} HashTable;\n\n/* 해시 테이블 조회 */\nHashTable *find(HashTable *h, int key) {\n HashTable *tmp;\n HASH_FIND_INT(h, &amp;key, tmp);\n return tmp;\n}\n\n/* 해시 테이블에 원소 삽입 */\nvoid insert(HashTable **h, int key, int val) {\n HashTable *t = find(*h, key);\n if (t == NULL) {\n HashTable *tmp = malloc(sizeof(HashTable));\n tmp-&gt;key = key, tmp-&gt;val = val;\n HASH_ADD_INT(*h, key, tmp);\n } else {\n t-&gt;val = val;\n }\n}\n\n/* 방법 2: 보조 해시 테이블 */\nint *twoSumHashTable(int *nums, int numsSize, int target, int *returnSize) {\n HashTable *hashtable = NULL;\n for (int i = 0; i &lt; numsSize; i++) {\n HashTable *t = find(hashtable, target - nums[i]);\n if (t != NULL) {\n int *res = malloc(sizeof(int) * 2);\n res[0] = t-&gt;val, res[1] = i;\n *returnSize = 2;\n return res;\n }\n insert(&amp;hashtable, nums[i], i);\n }\n *returnSize = 0;\n return NULL;\n}\n</code></pre> two_sum.kt<pre><code>/* 방법 2: 보조 해시 테이블 */\nfun twoSumHashTable(nums: IntArray, target: Int): IntArray {\n val size = nums.size\n // 보조 해시 테이블의 공간 복잡도는 O(n)이다\n val dic = HashMap&lt;Int, Int&gt;()\n // 단일 반복문이며 시간 복잡도는 O(n)이다\n for (i in 0..&lt;size) {\n if (dic.containsKey(target - nums[i])) {\n return intArrayOf(dic[target - nums[i]]!!, i)\n }\n dic[nums[i]] = i\n }\n return IntArray(0)\n}\n</code></pre> two_sum.rb<pre><code>### 방법 2: 보조 해시 테이블 ###\ndef two_sum_hash_table(nums, target)\n # 보조 해시 테이블의 공간 복잡도는 O(n)이다\n dic = {}\n # 단일 반복문이며 시간 복잡도는 O(n)이다\n for i in 0...nums.length\n return [dic[target - nums[i]], i] if dic.has_key?(target - nums[i])\n\n dic[nums[i]] = i\n end\n\n []\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>이 방법은 해시 탐색으로 시간 복잡도를 \\(O(n^2)\\)에서 \\(O(n)\\)으로 줄여 실행 효율을 크게 높인다.</p> <p>해시 테이블을 추가로 유지해야 하므로 공간 복잡도는 \\(O(n)\\)이다. 그렇더라도 시간과 공간 효율의 균형이 더 좋으므로 이 문제의 최적 풀이에 해당한다.</p>","path":["제 10 장   탐색","10.4   해시를 이용한 최적화 전략"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/","level":1,"title":"10.5   탐색 알고리즘 다시 보기","text":"<p>탐색 알고리즘(searching algorithm)은 배열, 연결 리스트, 트리, 그래프 등의 자료 구조에서 특정 조건을 만족하는 원소 하나 또는 여러 개를 찾는 데 사용한다.</p> <p>탐색 알고리즘은 구현 아이디어에 따라 다음 두 종류로 나뉜다.</p> <ul> <li>배열, 연결 리스트, 트리, 그래프의 순회처럼 자료 구조를 순회하여 목표 원소를 찾는다.</li> <li>이진 탐색, 해시 탐색, 이진 탐색 트리의 탐색처럼 데이터의 구성 방식이나 데이터에 대한 사전 정보를 이용하여 원소를 효율적으로 찾는다.</li> </ul> <p>이 내용은 모두 앞 장들에서 이미 소개했으므로 탐색 알고리즘은 낯설지 않을 것이다. 이 절에서는 더 체계적인 관점에서 탐색 알고리즘을 다시 살펴본다.</p>","path":["제 10 장   탐색","10.5   탐색 알고리즘 다시 보기"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1051","level":2,"title":"10.5.1   완전 탐색","text":"<p>완전 탐색은 자료 구조의 각 원소를 순회하여 목표 원소를 찾는다.</p> <ul> <li>'선형 탐색'은 배열이나 연결 리스트 같은 선형 자료 구조에 적합하다. 자료 구조의 한쪽 끝에서 원소를 하나씩 방문하며, 목표 원소를 찾거나 찾지 못한 채 반대쪽 끝에 도달하면 종료한다.</li> <li>'너비 우선 탐색'과 '깊이 우선 탐색'은 그래프와 트리의 두 가지 순회 전략이다. 너비 우선 탐색은 시작 노드에서 한 층씩 탐색하며 가까운 노드부터 먼 노드 순서로 방문한다. 깊이 우선 탐색은 시작 노드에서 한 경로를 따라 끝까지 간 뒤 되돌아와 다른 경로를 시도하며, 전체 자료 구조를 순회할 때까지 반복한다.</li> </ul> <p>완전 탐색은 간단하고 범용성이 좋으며, 데이터를 전처리하거나 추가 자료 구조를 사용할 필요가 없다.</p> <p>하지만 이러한 알고리즘의 시간 복잡도는 \\(O(n)\\)이다. 여기서 \\(n\\)은 원소 수이며, 데이터가 많으면 성능이 떨어진다.</p>","path":["제 10 장   탐색","10.5   탐색 알고리즘 다시 보기"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1052","level":2,"title":"10.5.2   적응형 탐색","text":"<p>적응형 탐색은 정렬 여부 같은 데이터 고유의 성질을 이용하여 탐색 과정을 최적화하고, 목표 원소를 더 효율적으로 찾는다.</p> <ul> <li>'이진 탐색'은 데이터의 정렬 상태를 이용하여 효율적으로 찾으며, 배열에만 적합하다.</li> <li>'해시 탐색'은 해시 테이블로 탐색 데이터와 목표 데이터 사이에 키-값 매핑을 만들어 조회한다.</li> <li>'트리 탐색'은 이진 탐색 트리 같은 특정 트리 구조에서 노드 값을 비교하여 탐색 대상에서 빠르게 제외하고 목표 원소를 찾는다.</li> </ul> <p>이러한 알고리즘은 효율이 높아 시간 복잡도가 \\(O(\\log n)\\), 나아가 \\(O(1)\\)에 이를 수 있다.</p> <p>하지만 이런 알고리즘을 사용하려면 데이터를 전처리해야 하는 경우가 많다. 예를 들어 이진 탐색은 배열을 미리 정렬해야 하며, 해시 탐색과 트리 탐색은 추가 자료 구조가 필요하다. 이러한 자료 구조를 유지하는 데도 시간과 공간이 추가로 든다.</p> <p>참고</p> <p>적응형 탐색 알고리즘은 조회 알고리즘이라고도 하며, 주로 특정 자료 구조에서 목표 원소를 빠르게 조회하는 데 사용한다.</p>","path":["제 10 장   탐색","10.5   탐색 알고리즘 다시 보기"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1053","level":2,"title":"10.5.3   탐색 방법 선택","text":"<p>크기가 \\(n\\)인 데이터 집합이 주어지면 선형 탐색, 이진 탐색, 트리 탐색, 해시 탐색 등 다양한 방법으로 목표 원소를 찾을 수 있다. 각 방법의 동작 원리는 아래 그림(10-11)과 같다.</p> <p></p> <p> 그림 10-11   다양한 탐색 전략 </p> <p>위 방법들의 연산 효율과 특성은 아래 표(10-1)와 같다.</p> <p> 표 10-1   탐색 알고리즘 효율 비교 </p> 선형 탐색 이진 탐색 트리 탐색 해시 탐색 원소 탐색 \\(O(n)\\) \\(O(\\log n)\\) \\(O(\\log n)\\) \\(O(1)\\) 원소 삽입 \\(O(1)\\) \\(O(n)\\) \\(O(\\log n)\\) \\(O(1)\\) 원소 삭제 \\(O(n)\\) \\(O(n)\\) \\(O(\\log n)\\) \\(O(1)\\) 추가 공간 \\(O(1)\\) \\(O(1)\\) \\(O(n)\\) \\(O(n)\\) 데이터 전처리 / 정렬 \\(O(n \\log n)\\) 트리 구성 \\(O(n \\log n)\\) 해시 테이블 구성 \\(O(n)\\) 데이터 정렬 여부 정렬되지 않음 정렬됨 정렬됨 정렬되지 않음 <p>탐색 알고리즘을 선택할 때는 데이터 규모, 요구되는 탐색 성능, 데이터 조회와 갱신 빈도 등도 고려해야 한다.</p> <p>선형 탐색</p> <ul> <li>범용성이 좋으며 데이터 전처리가 전혀 필요하지 않다. 데이터를 한 번만 조회한다면 나머지 세 방법은 전처리 시간만으로도 선형 탐색보다 오래 걸린다.</li> <li>데이터 규모가 작을 때 적합하다. 이때는 시간 복잡도가 효율에 미치는 영향이 작다.</li> <li>데이터를 추가로 관리할 필요가 없으므로 데이터 갱신이 잦은 상황에 적합하다.</li> </ul> <p>이진 탐색</p> <ul> <li>데이터가 많을 때 적합하며, 최악의 시간 복잡도가 \\(O(\\log n)\\)으로 성능이 안정적이다.</li> <li>배열을 저장하려면 연속된 메모리 공간이 필요하므로 데이터가 지나치게 많으면 안 된다.</li> <li>정렬된 배열을 유지하는 비용이 크므로 데이터 추가와 삭제가 잦은 상황에는 적합하지 않다.</li> </ul> <p>해시 탐색</p> <ul> <li>평균 시간 복잡도가 \\(O(1)\\)이므로 매우 높은 조회 성능이 필요한 상황에 적합하다.</li> <li>해시 테이블은 데이터의 정렬 상태를 유지할 수 없으므로 정렬된 데이터나 범위 탐색이 필요한 상황에는 적합하지 않다.</li> <li>해시 함수와 해시 충돌 처리 전략에 크게 의존하므로 성능이 저하될 위험이 비교적 크다.</li> <li>충돌을 최대한 줄여 좋은 조회 성능을 내려면 추가 공간이 필요하므로 데이터가 지나치게 많은 경우에는 적합하지 않다.</li> </ul> <p>트리 탐색</p> <ul> <li>트리 노드는 메모리에 분산 저장되므로 대규모 데이터에 적합하다.</li> <li>데이터의 정렬 상태를 유지하거나 범위 탐색을 해야 하는 상황에 적합하다.</li> <li>노드를 계속 추가하고 삭제하다 보면 이진 탐색 트리가 한쪽으로 치우쳐 시간 복잡도가 \\(O(n)\\)으로 저하될 수 있다.</li> <li>AVL 트리나 레드-블랙 트리를 사용하면 각 연산을 안정적으로 \\(O(\\log n)\\)에 수행할 수 있지만, 트리의 균형을 유지하는 데 추가 비용이 든다.</li> </ul>","path":["제 10 장   탐색","10.5   탐색 알고리즘 다시 보기"],"tags":[]},{"location":"chapter_searching/summary/","level":1,"title":"10.6   요약","text":"","path":["제 10 장   탐색","10.6   요약"],"tags":[]},{"location":"chapter_searching/summary/#1","level":3,"title":"1.   핵심 정리","text":"<ul> <li>이진 탐색은 데이터가 정렬되어 있다는 성질을 이용하여 반복할 때마다 탐색 구간을 절반씩 줄인다. 입력 데이터가 정렬되어 있어야 하며, 배열이나 배열 기반 자료 구조에만 적합하다.</li> <li>완전 탐색은 자료 구조를 순회하여 데이터를 찾는다. 선형 탐색은 배열과 연결 리스트에, 너비 우선 탐색과 깊이 우선 탐색은 그래프와 트리에 적합하다. 범용성이 좋고 전처리가 필요하지 않지만, 시간 복잡도가 \\(O(n)\\)으로 비교적 높다.</li> <li>해시 탐색, 트리 탐색, 이진 탐색은 특정 자료 구조에서 목표 원소를 빠르게 찾는 효율적인 탐색 방법이다. 시간 복잡도가 \\(O(\\log n)\\) 또는 \\(O(1)\\)에 이를 만큼 효율적이지만, 보통 추가 자료 구조가 필요하다.</li> <li>실제로는 데이터 규모, 요구되는 탐색 성능, 데이터 조회와 갱신 빈도 등을 구체적으로 분석하여 적합한 탐색 방법을 골라야 한다.</li> <li>선형 탐색은 작거나 자주 갱신되는 데이터에, 이진 탐색은 크고 정렬된 데이터에 적합하다. 해시 탐색은 높은 조회 효율이 필요하면서 범위 조회는 필요 없는 데이터에, 트리 탐색은 순서 유지와 범위 조회가 필요한 대규모 동적 데이터에 적합하다.</li> <li>선형 탐색을 해시 탐색으로 바꾸는 것은 실행 시간을 최적화하는 흔한 전략으로, 시간 복잡도를 \\(O(n)\\)에서 \\(O(1)\\)로 줄일 수 있다.</li> </ul>","path":["제 10 장   탐색","10.6   요약"],"tags":[]},{"location":"chapter_sorting/","level":1,"title":"제 11 장   정렬","text":"<p>이 장을 시작하며</p> <p>정렬은 혼란을 질서로 바꾸는 마법의 열쇠와 같아서, 데이터를 더 효율적으로 이해하고 처리하게 해 준다.</p> <p>단순한 오름차순이든 복잡한 분류와 배열이든, 정렬은 데이터가 지닌 조화로운 아름다움을 보여 준다.</p>","path":["제 11 장   정렬"],"tags":[]},{"location":"chapter_sorting/#_1","level":2,"title":"이 장의 내용","text":"<ul> <li>11.1   정렬 알고리즘</li> <li>11.2   선택 정렬</li> <li>11.3   버블 정렬</li> <li>11.4   삽입 정렬</li> <li>11.5   퀵 정렬</li> <li>11.6   병합 정렬</li> <li>11.7   힙 정렬</li> <li>11.8   버킷 정렬</li> <li>11.9   계수 정렬</li> <li>11.10   기수 정렬</li> <li>11.11   요약</li> <li>11.12   연습 문제</li> </ul>","path":["제 11 장   정렬"],"tags":[]},{"location":"chapter_sorting/bubble_sort/","level":1,"title":"11.3   버블 정렬","text":"<p>버블 정렬(bubble sort)은 인접한 원소를 계속 비교하고 교환하여 정렬한다. 이 과정이 거품이 아래에서 위로 올라오는 모습과 비슷하여 버블 정렬이라는 이름이 붙었다.</p> <p>아래 그림(11-4)과 같이 원소 교환으로 거품이 올라오는 과정을 흉내 낼 수 있다. 배열의 맨 왼쪽부터 오른쪽으로 순회하며 인접한 두 원소를 비교하고, '왼쪽 원소 &gt; 오른쪽 원소'이면 둘을 교환한다. 순회를 마치면 가장 큰 원소가 배열의 맨 오른쪽으로 이동한다.</p> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt;&lt;6&gt;&lt;7&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 11-4   원소 교환으로 거품의 이동 구현 </p>","path":["제 11 장   정렬","11.3   버블 정렬"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1131","level":2,"title":"11.3.1   알고리즘 과정","text":"<p>배열의 길이가 \\(n\\)일 때 버블 정렬의 단계는 아래 그림(11-5)과 같다.</p> <ol> <li>먼저 \\(n\\) 개 원소에 '버블 연산'을 수행하여 배열에서 가장 큰 원소를 올바른 위치로 옮긴다.</li> <li>이어서 남은 \\(n - 1\\) 개 원소에 '버블 연산'을 수행하여 두 번째로 큰 원소를 올바른 위치로 옮긴다.</li> <li>같은 과정을 반복하여 \\(n - 1\\) 회의 '버블 연산'을 수행하면 큰 순서로 \\(n - 1\\) 개 원소가 모두 올바른 위치에 놓인다.</li> <li>남은 원소 하나는 반드시 가장 작은 원소이므로 정렬할 필요가 없으며, 배열 정렬이 완료된다.</li> </ol> <p></p> <p> 그림 11-5   버블 정렬 과정 </p> <p>예시 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bubble_sort.py<pre><code>def bubble_sort(nums: list[int]):\n \"\"\"버블 정렬\"\"\"\n n = len(nums)\n # 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for i in range(n - 1, 0, -1):\n # 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for j in range(i):\n if nums[j] &gt; nums[j + 1]:\n # nums[j]와 nums[j + 1]을 교환한다\n nums[j], nums[j + 1] = nums[j + 1], nums[j]\n</code></pre> bubble_sort.cpp<pre><code>/* 버블 정렬 */\nvoid bubbleSort(vector&lt;int&gt; &amp;nums) {\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for (int i = nums.size() - 1; i &gt; 0; i--) {\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for (int j = 0; j &lt; i; j++) {\n if (nums[j] &gt; nums[j + 1]) {\n // nums[j]와 nums[j + 1]을 교환한다\n // 여기서는 std::swap() 함수를 사용한다\n swap(nums[j], nums[j + 1]);\n }\n }\n }\n}\n</code></pre> bubble_sort.java<pre><code>/* 버블 정렬 */\nvoid bubbleSort(int[] nums) {\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for (int i = nums.length - 1; i &gt; 0; i--) {\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for (int j = 0; j &lt; i; j++) {\n if (nums[j] &gt; nums[j + 1]) {\n // nums[j]와 nums[j + 1]을 교환한다\n int tmp = nums[j];\n nums[j] = nums[j + 1];\n nums[j + 1] = tmp;\n }\n }\n }\n}\n</code></pre> bubble_sort.cs<pre><code>/* 버블 정렬 */\nvoid BubbleSort(int[] nums) {\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for (int i = nums.Length - 1; i &gt; 0; i--) {\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for (int j = 0; j &lt; i; j++) {\n if (nums[j] &gt; nums[j + 1]) {\n // nums[j]와 nums[j + 1]을 교환한다\n (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n }\n }\n }\n}\n</code></pre> bubble_sort.go<pre><code>/* 버블 정렬 */\nfunc bubbleSort(nums []int) {\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for i := len(nums) - 1; i &gt; 0; i-- {\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for j := 0; j &lt; i; j++ {\n if nums[j] &gt; nums[j+1] {\n // nums[j]와 nums[j + 1]을 교환한다\n nums[j], nums[j+1] = nums[j+1], nums[j]\n }\n }\n }\n}\n</code></pre> bubble_sort.swift<pre><code>/* 버블 정렬 */\nfunc bubbleSort(nums: inout [Int]) {\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for i in nums.indices.dropFirst().reversed() {\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for j in 0 ..&lt; i {\n if nums[j] &gt; nums[j + 1] {\n // nums[j]와 nums[j + 1]을 교환한다\n nums.swapAt(j, j + 1)\n }\n }\n }\n}\n</code></pre> bubble_sort.js<pre><code>/* 버블 정렬 */\nfunction bubbleSort(nums) {\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for (let i = nums.length - 1; i &gt; 0; i--) {\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for (let j = 0; j &lt; i; j++) {\n if (nums[j] &gt; nums[j + 1]) {\n // nums[j]와 nums[j + 1]을 교환한다\n let tmp = nums[j];\n nums[j] = nums[j + 1];\n nums[j + 1] = tmp;\n }\n }\n }\n}\n</code></pre> bubble_sort.ts<pre><code>/* 버블 정렬 */\nfunction bubbleSort(nums: number[]): void {\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for (let i = nums.length - 1; i &gt; 0; i--) {\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for (let j = 0; j &lt; i; j++) {\n if (nums[j] &gt; nums[j + 1]) {\n // nums[j]와 nums[j + 1]을 교환한다\n let tmp = nums[j];\n nums[j] = nums[j + 1];\n nums[j + 1] = tmp;\n }\n }\n }\n}\n</code></pre> bubble_sort.dart<pre><code>/* 버블 정렬 */\nvoid bubbleSort(List&lt;int&gt; nums) {\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for (int i = nums.length - 1; i &gt; 0; i--) {\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for (int j = 0; j &lt; i; j++) {\n if (nums[j] &gt; nums[j + 1]) {\n // nums[j]와 nums[j + 1]을 교환한다\n int tmp = nums[j];\n nums[j] = nums[j + 1];\n nums[j + 1] = tmp;\n }\n }\n }\n}\n</code></pre> bubble_sort.rs<pre><code>/* 버블 정렬 */\nfn bubble_sort(nums: &amp;mut [i32]) {\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for i in (1..nums.len()).rev() {\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for j in 0..i {\n if nums[j] &gt; nums[j + 1] {\n // nums[j]와 nums[j + 1]을 교환한다\n nums.swap(j, j + 1);\n }\n }\n }\n}\n</code></pre> bubble_sort.c<pre><code>/* 버블 정렬 */\nvoid bubbleSort(int nums[], int size) {\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for (int i = size - 1; i &gt; 0; i--) {\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for (int j = 0; j &lt; i; j++) {\n if (nums[j] &gt; nums[j + 1]) {\n int temp = nums[j];\n nums[j] = nums[j + 1];\n nums[j + 1] = temp;\n }\n }\n }\n}\n</code></pre> bubble_sort.kt<pre><code>/* 버블 정렬 */\nfun bubbleSort(nums: IntArray) {\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for (i in nums.size - 1 downTo 1) {\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for (j in 0..&lt;i) {\n if (nums[j] &gt; nums[j + 1]) {\n // nums[j]와 nums[j + 1]을 교환한다\n val temp = nums[j]\n nums[j] = nums[j + 1]\n nums[j + 1] = temp\n }\n }\n }\n}\n</code></pre> bubble_sort.rb<pre><code>### 버블 정렬 ###\ndef bubble_sort(nums)\n n = nums.length\n # 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for i in (n - 1).downto(1)\n # 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for j in 0...i\n if nums[j] &gt; nums[j + 1]\n # nums[j]와 nums[j + 1]을 교환한다\n nums[j], nums[j + 1] = nums[j + 1], nums[j]\n end\n end\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 11 장   정렬","11.3   버블 정렬"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1132","level":2,"title":"11.3.2   효율 최적화","text":"<p>어느 회의 '버블 연산'에서 교환이 한 번도 일어나지 않았다면 배열은 이미 정렬되어 있으므로 결과를 바로 반환할 수 있다. 따라서 이 상황을 감지하는 플래그 <code>flag</code>를 추가하고, 발생하는 즉시 반환할 수 있다.</p> <p>최적화 후에도 버블 정렬의 최악 및 평균 시간 복잡도는 \\(O(n^2)\\)이다. 하지만 입력 배열이 완전히 정렬되어 있으면 최선 시간 복잡도인 \\(O(n)\\)에 도달한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bubble_sort.py<pre><code>def bubble_sort_with_flag(nums: list[int]):\n \"\"\"버블 정렬(플래그 최적화)\"\"\"\n n = len(nums)\n # 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for i in range(n - 1, 0, -1):\n flag = False # 플래그를 초기화한다\n # 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for j in range(i):\n if nums[j] &gt; nums[j + 1]:\n # nums[j]와 nums[j + 1]을 교환한다\n nums[j], nums[j + 1] = nums[j + 1], nums[j]\n flag = True # 원소가 교환되었음을 기록한다\n if not flag:\n break # 이번 버블 과정에서 원소 교환이 없었으므로 바로 종료한다\n</code></pre> bubble_sort.cpp<pre><code>/* 버블 정렬(플래그 최적화)*/\nvoid bubbleSortWithFlag(vector&lt;int&gt; &amp;nums) {\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for (int i = nums.size() - 1; i &gt; 0; i--) {\n bool flag = false; // 플래그를 초기화한다\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for (int j = 0; j &lt; i; j++) {\n if (nums[j] &gt; nums[j + 1]) {\n // nums[j]와 nums[j + 1]을 교환한다\n // 여기서는 std::swap() 함수를 사용한다\n swap(nums[j], nums[j + 1]);\n flag = true; // 원소가 교환되었음을 기록한다\n }\n }\n if (!flag)\n break; // 이번 버블 과정에서 원소 교환이 없었으므로 바로 종료한다\n }\n}\n</code></pre> bubble_sort.java<pre><code>/* 버블 정렬(플래그 최적화) */\nvoid bubbleSortWithFlag(int[] nums) {\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for (int i = nums.length - 1; i &gt; 0; i--) {\n boolean flag = false; // 플래그를 초기화한다\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for (int j = 0; j &lt; i; j++) {\n if (nums[j] &gt; nums[j + 1]) {\n // nums[j]와 nums[j + 1]을 교환한다\n int tmp = nums[j];\n nums[j] = nums[j + 1];\n nums[j + 1] = tmp;\n flag = true; // 원소가 교환되었음을 기록한다\n }\n }\n if (!flag)\n break; // 이번 버블 과정에서 원소 교환이 없었으므로 바로 종료한다\n }\n}\n</code></pre> bubble_sort.cs<pre><code>/* 버블 정렬(플래그 최적화)*/\nvoid BubbleSortWithFlag(int[] nums) {\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for (int i = nums.Length - 1; i &gt; 0; i--) {\n bool flag = false; // 플래그를 초기화한다\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for (int j = 0; j &lt; i; j++) {\n if (nums[j] &gt; nums[j + 1]) {\n // nums[j]와 nums[j + 1]을 교환한다\n (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n flag = true; // 원소가 교환되었음을 기록한다\n }\n }\n if (!flag) break; // 이번 버블 과정에서 원소 교환이 없었으므로 바로 종료한다\n }\n}\n</code></pre> bubble_sort.go<pre><code>/* 버블 정렬(플래그 최적화)*/\nfunc bubbleSortWithFlag(nums []int) {\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for i := len(nums) - 1; i &gt; 0; i-- {\n flag := false // 플래그를 초기화한다\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for j := 0; j &lt; i; j++ {\n if nums[j] &gt; nums[j+1] {\n // nums[j]와 nums[j + 1]을 교환한다\n nums[j], nums[j+1] = nums[j+1], nums[j]\n flag = true // 원소가 교환되었음을 기록한다\n }\n }\n if flag == false { // 이번 버블 과정에서 원소 교환이 없었으므로 바로 종료한다\n break\n }\n }\n}\n</code></pre> bubble_sort.swift<pre><code>/* 버블 정렬(플래그 최적화)*/\nfunc bubbleSortWithFlag(nums: inout [Int]) {\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for i in nums.indices.dropFirst().reversed() {\n var flag = false // 플래그를 초기화한다\n for j in 0 ..&lt; i {\n if nums[j] &gt; nums[j + 1] {\n // nums[j]와 nums[j + 1]을 교환한다\n nums.swapAt(j, j + 1)\n flag = true // 원소가 교환되었음을 기록한다\n }\n }\n if !flag { // 이번 버블 과정에서 원소 교환이 없었으므로 바로 종료한다\n break\n }\n }\n}\n</code></pre> bubble_sort.js<pre><code>/* 버블 정렬(플래그 최적화)*/\nfunction bubbleSortWithFlag(nums) {\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for (let i = nums.length - 1; i &gt; 0; i--) {\n let flag = false; // 플래그를 초기화한다\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for (let j = 0; j &lt; i; j++) {\n if (nums[j] &gt; nums[j + 1]) {\n // nums[j]와 nums[j + 1]을 교환한다\n let tmp = nums[j];\n nums[j] = nums[j + 1];\n nums[j + 1] = tmp;\n flag = true; // 원소가 교환되었음을 기록한다\n }\n }\n if (!flag) break; // 이번 버블 과정에서 원소 교환이 없었으므로 바로 종료한다\n }\n}\n</code></pre> bubble_sort.ts<pre><code>/* 버블 정렬(플래그 최적화)*/\nfunction bubbleSortWithFlag(nums: number[]): void {\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for (let i = nums.length - 1; i &gt; 0; i--) {\n let flag = false; // 플래그를 초기화한다\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for (let j = 0; j &lt; i; j++) {\n if (nums[j] &gt; nums[j + 1]) {\n // nums[j]와 nums[j + 1]을 교환한다\n let tmp = nums[j];\n nums[j] = nums[j + 1];\n nums[j + 1] = tmp;\n flag = true; // 원소가 교환되었음을 기록한다\n }\n }\n if (!flag) break; // 이번 버블 과정에서 원소 교환이 없었으므로 바로 종료한다\n }\n}\n</code></pre> bubble_sort.dart<pre><code>/* 버블 정렬(플래그 최적화)*/\nvoid bubbleSortWithFlag(List&lt;int&gt; nums) {\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for (int i = nums.length - 1; i &gt; 0; i--) {\n bool flag = false; // 플래그를 초기화한다\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for (int j = 0; j &lt; i; j++) {\n if (nums[j] &gt; nums[j + 1]) {\n // nums[j]와 nums[j + 1]을 교환한다\n int tmp = nums[j];\n nums[j] = nums[j + 1];\n nums[j + 1] = tmp;\n flag = true; // 원소가 교환되었음을 기록한다\n }\n }\n if (!flag) break; // 이번 버블 과정에서 원소 교환이 없었으므로 바로 종료한다\n }\n}\n</code></pre> bubble_sort.rs<pre><code>/* 버블 정렬(플래그 최적화) */\nfn bubble_sort_with_flag(nums: &amp;mut [i32]) {\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for i in (1..nums.len()).rev() {\n let mut flag = false; // 플래그를 초기화한다\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for j in 0..i {\n if nums[j] &gt; nums[j + 1] {\n // nums[j]와 nums[j + 1]을 교환한다\n nums.swap(j, j + 1);\n flag = true; // 원소가 교환되었음을 기록한다\n }\n }\n if !flag {\n break; // 이번 버블 과정에서 원소 교환이 없었으므로 바로 종료한다\n };\n }\n}\n</code></pre> bubble_sort.c<pre><code>/* 버블 정렬(플래그 최적화)*/\nvoid bubbleSortWithFlag(int nums[], int size) {\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for (int i = size - 1; i &gt; 0; i--) {\n bool flag = false;\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for (int j = 0; j &lt; i; j++) {\n if (nums[j] &gt; nums[j + 1]) {\n int temp = nums[j];\n nums[j] = nums[j + 1];\n nums[j + 1] = temp;\n flag = true;\n }\n }\n if (!flag)\n break;\n }\n}\n</code></pre> bubble_sort.kt<pre><code>/* 버블 정렬(플래그 최적화) */\nfun bubbleSortWithFlag(nums: IntArray) {\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for (i in nums.size - 1 downTo 1) {\n var flag = false // 플래그를 초기화한다\n // 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for (j in 0..&lt;i) {\n if (nums[j] &gt; nums[j + 1]) {\n // nums[j]와 nums[j + 1]을 교환한다\n val temp = nums[j]\n nums[j] = nums[j + 1]\n nums[j + 1] = temp\n flag = true // 원소가 교환되었음을 기록한다\n }\n }\n if (!flag) break // 이번 버블 과정에서 원소 교환이 없었으므로 바로 종료한다\n }\n}\n</code></pre> bubble_sort.rb<pre><code>### 버블 정렬(플래그 최적화)###\ndef bubble_sort_with_flag(nums)\n n = nums.length\n # 바깥쪽 반복문: 정렬되지 않은 구간은 [0, i]이다\n for i in (n - 1).downto(1)\n flag = false # 플래그를 초기화한다\n\n # 안쪽 반복문: 정렬되지 않은 구간 [0, i]의 최댓값을 교환하여 이 구간의 맨 오른쪽으로 옮긴다\n for j in 0...i\n if nums[j] &gt; nums[j + 1]\n # nums[j]와 nums[j + 1]을 교환한다\n nums[j], nums[j + 1] = nums[j + 1], nums[j]\n flag = true # 원소가 교환되었음을 기록한다\n end\n end\n\n break unless flag # 이번 버블 과정에서 원소 교환이 없었으므로 바로 종료한다\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 11 장   정렬","11.3   버블 정렬"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1133","level":2,"title":"11.3.3   알고리즘 특성","text":"<ul> <li>시간 복잡도 \\(O(n^2)\\), 적응형 정렬: 각 회의 '버블 연산'에서 순회하는 배열 길이는 차례로 \\(n - 1\\), \\(n - 2\\), \\(\\dots\\), \\(2\\), \\(1\\)이며, 합은 \\((n - 1) n / 2\\)이다. <code>flag</code>를 도입하여 최적화하면 최선 시간 복잡도가 \\(O(n)\\)이 된다.</li> <li>공간 복잡도 \\(O(1)\\), 제자리 정렬: 포인터 \\(i\\)와 \\(j\\)는 상수 크기의 추가 공간을 사용한다.</li> <li>안정 정렬: '버블 연산'에서 값이 같은 원소는 교환하지 않는다.</li> </ul>","path":["제 11 장   정렬","11.3   버블 정렬"],"tags":[]},{"location":"chapter_sorting/bucket_sort/","level":1,"title":"11.8   버킷 정렬","text":"<p>앞에서 살펴본 정렬 알고리즘은 모두 원소의 크기를 비교하여 정렬하는 '비교 기반 정렬 알고리즘'이다. 이 알고리즘들의 최악 시간 복잡도 하한은 \\(\\Omega(n \\log n)\\)이다. 이제 시간 복잡도가 선형에 이를 수 있는 몇 가지 '비교를 사용하지 않는 정렬 알고리즘'을 살펴보자.</p> <p>버킷 정렬(bucket sort)은 분할 정복 전략의 대표적인 응용이다. 크기순으로 배열된 여러 버킷을 만들고 각 버킷에 데이터 범위를 대응시킨 뒤, 데이터를 각 버킷에 고르게 분배한다. 이어서 버킷별로 내부 정렬을 수행하고, 마지막으로 버킷 순서대로 모든 데이터를 합친다.</p>","path":["제 11 장   정렬","11.8   버킷 정렬"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1181","level":2,"title":"11.8.1   알고리즘 과정","text":"<p>길이가 \\(n\\)이고 원소가 범위 \\([0, 1)\\)의 부동 소수점 수인 배열을 생각해 보자. 버킷 정렬의 과정은 아래 그림(11-13)과 같다.</p> <ol> <li>버킷 \\(k\\) 개를 초기화하고 원소 \\(n\\) 개를 이 \\(k\\) 개의 버킷에 분배한다.</li> <li>각 버킷을 따로 정렬한다(여기서는 프로그래밍 언어의 내장 정렬 함수를 사용한다).</li> <li>작은 버킷부터 큰 버킷 순서로 결과를 합친다.</li> </ol> <p></p> <p> 그림 11-13   버킷 정렬 알고리즘 과정 </p> <p>코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bucket_sort.py<pre><code>def bucket_sort(nums: list[float]):\n \"\"\"버킷 정렬\"\"\"\n # 버킷 k = n/2개를 초기화하며, 각 버킷에 원소 2개가 배정될 것으로 예상한다\n k = len(nums) // 2\n buckets = [[] for _ in range(k)]\n # 1. 배열 원소를 각 버킷에 배정한다\n for num in nums:\n # 입력 데이터의 범위는 [0, 1)이며, num * k를 사용하여 인덱스 범위 [0, k-1]에 대응시킨다\n i = int(num * k)\n # num을 버킷 i에 추가한다\n buckets[i].append(num)\n # 2. 각 버킷을 정렬한다\n for bucket in buckets:\n # 내장 정렬 함수를 사용하며, 다른 정렬 알고리즘으로 대체할 수도 있다\n bucket.sort()\n # 3. 버킷을 순회하여 결과를 합친다\n i = 0\n for bucket in buckets:\n for num in bucket:\n nums[i] = num\n i += 1\n</code></pre> bucket_sort.cpp<pre><code>/* 버킷 정렬 */\nvoid bucketSort(vector&lt;float&gt; &amp;nums) {\n // 버킷 k = n/2개를 초기화하며, 각 버킷에 원소 2개가 배정될 것으로 예상한다\n int k = nums.size() / 2;\n vector&lt;vector&lt;float&gt;&gt; buckets(k);\n // 1. 배열 원소를 각 버킷에 배정한다\n for (float num : nums) {\n // 입력 데이터의 범위는 [0, 1)이며, num * k를 사용하여 인덱스 범위 [0, k-1]에 대응시킨다\n int i = num * k;\n // num을 버킷 bucket_idx에 추가한다\n buckets[i].push_back(num);\n }\n // 2. 각 버킷을 정렬한다\n for (vector&lt;float&gt; &amp;bucket : buckets) {\n // 내장 정렬 함수를 사용하며, 다른 정렬 알고리즘으로 대체할 수도 있다\n sort(bucket.begin(), bucket.end());\n }\n // 3. 버킷을 순회하여 결과를 합친다\n int i = 0;\n for (vector&lt;float&gt; &amp;bucket : buckets) {\n for (float num : bucket) {\n nums[i++] = num;\n }\n }\n}\n</code></pre> bucket_sort.java<pre><code>/* 버킷 정렬 */\nvoid bucketSort(float[] nums) {\n // 버킷 k = n/2개를 초기화하며, 각 버킷에 원소 2개가 배정될 것으로 예상한다\n int k = nums.length / 2;\n List&lt;List&lt;Float&gt;&gt; buckets = new ArrayList&lt;&gt;();\n for (int i = 0; i &lt; k; i++) {\n buckets.add(new ArrayList&lt;&gt;());\n }\n // 1. 배열 원소를 각 버킷에 배정한다\n for (float num : nums) {\n // 입력 데이터의 범위는 [0, 1)이며, num * k를 사용하여 인덱스 범위 [0, k-1]에 대응시킨다\n int i = (int) (num * k);\n // num을 버킷 i에 추가한다\n buckets.get(i).add(num);\n }\n // 2. 각 버킷을 정렬한다\n for (List&lt;Float&gt; bucket : buckets) {\n // 내장 정렬 함수를 사용하며, 다른 정렬 알고리즘으로 대체할 수도 있다\n Collections.sort(bucket);\n }\n // 3. 버킷을 순회하여 결과를 합친다\n int i = 0;\n for (List&lt;Float&gt; bucket : buckets) {\n for (float num : bucket) {\n nums[i++] = num;\n }\n }\n}\n</code></pre> bucket_sort.cs<pre><code>/* 버킷 정렬 */\nvoid BucketSort(float[] nums) {\n // 버킷 k = n/2개를 초기화하며, 각 버킷에 원소 2개가 배정될 것으로 예상한다\n int k = nums.Length / 2;\n List&lt;List&lt;float&gt;&gt; buckets = [];\n for (int i = 0; i &lt; k; i++) {\n buckets.Add([]);\n }\n // 1. 배열 원소를 각 버킷에 배정한다\n foreach (float num in nums) {\n // 입력 데이터의 범위는 [0, 1)이며, num * k를 사용하여 인덱스 범위 [0, k-1]에 대응시킨다\n int i = (int)(num * k);\n // num을 버킷 i에 추가한다\n buckets[i].Add(num);\n }\n // 2. 각 버킷을 정렬한다\n foreach (List&lt;float&gt; bucket in buckets) {\n // 내장 정렬 함수를 사용하며, 다른 정렬 알고리즘으로 대체할 수도 있다\n bucket.Sort();\n }\n // 3. 버킷을 순회하여 결과를 합친다\n int j = 0;\n foreach (List&lt;float&gt; bucket in buckets) {\n foreach (float num in bucket) {\n nums[j++] = num;\n }\n }\n}\n</code></pre> bucket_sort.go<pre><code>/* 버킷 정렬 */\nfunc bucketSort(nums []float64) {\n // 버킷 k = n/2개를 초기화하며, 각 버킷에 원소 2개가 배정될 것으로 예상한다\n k := len(nums) / 2\n buckets := make([][]float64, k)\n for i := 0; i &lt; k; i++ {\n buckets[i] = make([]float64, 0)\n }\n // 1. 배열 원소를 각 버킷에 배정한다\n for _, num := range nums {\n // 입력 데이터의 범위는 [0, 1)이며, num * k를 사용하여 인덱스 범위 [0, k-1]에 대응시킨다\n i := int(num * float64(k))\n // num을 버킷 i에 추가한다\n buckets[i] = append(buckets[i], num)\n }\n // 2. 각 버킷을 정렬한다\n for i := 0; i &lt; k; i++ {\n // 내장 슬라이스 정렬 함수를 사용하며, 다른 정렬 알고리즘으로 대체할 수도 있다\n sort.Float64s(buckets[i])\n }\n // 3. 버킷을 순회하여 결과를 합친다\n i := 0\n for _, bucket := range buckets {\n for _, num := range bucket {\n nums[i] = num\n i++\n }\n }\n}\n</code></pre> bucket_sort.swift<pre><code>/* 버킷 정렬 */\nfunc bucketSort(nums: inout [Double]) {\n // 버킷 k = n/2개를 초기화하며, 각 버킷에 원소 2개가 배정될 것으로 예상한다\n let k = nums.count / 2\n var buckets = (0 ..&lt; k).map { _ in [Double]() }\n // 1. 배열 원소를 각 버킷에 배정한다\n for num in nums {\n // 입력 데이터의 범위는 [0, 1)이며, num * k를 사용하여 인덱스 범위 [0, k-1]에 대응시킨다\n let i = Int(num * Double(k))\n // num을 버킷 i에 추가한다\n buckets[i].append(num)\n }\n // 2. 각 버킷을 정렬한다\n for i in buckets.indices {\n // 내장 정렬 함수를 사용하며, 다른 정렬 알고리즘으로 대체할 수도 있다\n buckets[i].sort()\n }\n // 3. 버킷을 순회하여 결과를 합친다\n var i = nums.startIndex\n for bucket in buckets {\n for num in bucket {\n nums[i] = num\n i += 1\n }\n }\n}\n</code></pre> bucket_sort.js<pre><code>/* 버킷 정렬 */\nfunction bucketSort(nums) {\n // 버킷 k = n/2개를 초기화하며, 각 버킷에 원소 2개가 배정될 것으로 예상한다\n const k = nums.length / 2;\n const buckets = [];\n for (let i = 0; i &lt; k; i++) {\n buckets.push([]);\n }\n // 1. 배열 원소를 각 버킷에 배정한다\n for (const num of nums) {\n // 입력 데이터의 범위는 [0, 1)이며, num * k를 사용하여 인덱스 범위 [0, k-1]에 대응시킨다\n const i = Math.floor(num * k);\n // num을 버킷 i에 추가한다\n buckets[i].push(num);\n }\n // 2. 각 버킷을 정렬한다\n for (const bucket of buckets) {\n // 내장 정렬 함수를 사용하며, 다른 정렬 알고리즘으로 대체할 수도 있다\n bucket.sort((a, b) =&gt; a - b);\n }\n // 3. 버킷을 순회하여 결과를 합친다\n let i = 0;\n for (const bucket of buckets) {\n for (const num of bucket) {\n nums[i++] = num;\n }\n }\n}\n</code></pre> bucket_sort.ts<pre><code>/* 버킷 정렬 */\nfunction bucketSort(nums: number[]): void {\n // 버킷 k = n/2개를 초기화하며, 각 버킷에 원소 2개가 배정될 것으로 예상한다\n const k = nums.length / 2;\n const buckets: number[][] = [];\n for (let i = 0; i &lt; k; i++) {\n buckets.push([]);\n }\n // 1. 배열 원소를 각 버킷에 배정한다\n for (const num of nums) {\n // 입력 데이터의 범위는 [0, 1)이며, num * k를 사용하여 인덱스 범위 [0, k-1]에 대응시킨다\n const i = Math.floor(num * k);\n // num을 버킷 i에 추가한다\n buckets[i].push(num);\n }\n // 2. 각 버킷을 정렬한다\n for (const bucket of buckets) {\n // 내장 정렬 함수를 사용하며, 다른 정렬 알고리즘으로 대체할 수도 있다\n bucket.sort((a, b) =&gt; a - b);\n }\n // 3. 버킷을 순회하여 결과를 합친다\n let i = 0;\n for (const bucket of buckets) {\n for (const num of bucket) {\n nums[i++] = num;\n }\n }\n}\n</code></pre> bucket_sort.dart<pre><code>/* 버킷 정렬 */\nvoid bucketSort(List&lt;double&gt; nums) {\n // 버킷 k = n/2개를 초기화하며, 각 버킷에 원소 2개가 배정될 것으로 예상한다\n int k = nums.length ~/ 2;\n List&lt;List&lt;double&gt;&gt; buckets = List.generate(k, (index) =&gt; []);\n\n // 1. 배열 원소를 각 버킷에 배정한다\n for (double _num in nums) {\n // 입력 데이터의 범위는 [0, 1)이며, _num * k를 사용하여 인덱스 범위 [0, k-1]에 대응시킨다\n int i = (_num * k).toInt();\n // _num을 버킷 bucket_idx에 추가한다\n buckets[i].add(_num);\n }\n // 2. 각 버킷을 정렬한다\n for (List&lt;double&gt; bucket in buckets) {\n bucket.sort();\n }\n // 3. 버킷을 순회하여 결과를 합친다\n int i = 0;\n for (List&lt;double&gt; bucket in buckets) {\n for (double _num in bucket) {\n nums[i++] = _num;\n }\n }\n}\n</code></pre> bucket_sort.rs<pre><code>/* 버킷 정렬 */\nfn bucket_sort(nums: &amp;mut [f64]) {\n // 버킷 k = n/2개를 초기화하며, 각 버킷에 원소 2개가 배정될 것으로 예상한다\n let k = nums.len() / 2;\n let mut buckets = vec![vec![]; k];\n // 1. 배열 원소를 각 버킷에 배정한다\n for &amp;num in nums.iter() {\n // 입력 데이터의 범위는 [0, 1)이며, num * k를 사용하여 인덱스 범위 [0, k-1]에 대응시킨다\n let i = (num * k as f64) as usize;\n // num을 버킷 i에 추가한다\n buckets[i].push(num);\n }\n // 2. 각 버킷을 정렬한다\n for bucket in &amp;mut buckets {\n // 내장 정렬 함수를 사용하며, 다른 정렬 알고리즘으로 대체할 수도 있다\n bucket.sort_by(|a, b| a.partial_cmp(b).unwrap());\n }\n // 3. 버킷을 순회하여 결과를 합친다\n let mut i = 0;\n for bucket in buckets.iter() {\n for &amp;num in bucket.iter() {\n nums[i] = num;\n i += 1;\n }\n }\n}\n</code></pre> bucket_sort.c<pre><code>/* 버킷 정렬 */\nvoid bucketSort(float nums[], int n) {\n int k = n / 2; // k = n/2개의 버킷을 초기화한다\n int *sizes = malloc(k * sizeof(int)); // 각 버킷의 크기를 기록한다\n float **buckets = malloc(k * sizeof(float *)); // 동적 배열의 배열(버킷)\n // 각 버킷에 충분한 공간을 미리 할당한다\n for (int i = 0; i &lt; k; ++i) {\n buckets[i] = (float *)malloc(n * sizeof(float));\n sizes[i] = 0;\n }\n // 1. 배열 원소를 각 버킷에 배정한다\n for (int i = 0; i &lt; n; ++i) {\n int idx = (int)(nums[i] * k);\n buckets[idx][sizes[idx]++] = nums[i];\n }\n // 2. 각 버킷을 정렬한다\n for (int i = 0; i &lt; k; ++i) {\n qsort(buckets[i], sizes[i], sizeof(float), compare);\n }\n // 3. 정렬한 버킷을 병합한다\n int idx = 0;\n for (int i = 0; i &lt; k; ++i) {\n for (int j = 0; j &lt; sizes[i]; ++j) {\n nums[idx++] = buckets[i][j];\n }\n // 메모리 해제\n free(buckets[i]);\n }\n}\n</code></pre> bucket_sort.kt<pre><code>/* 버킷 정렬 */\nfun bucketSort(nums: FloatArray) {\n // 버킷 k = n/2개를 초기화하며, 각 버킷에 원소 2개가 배정될 것으로 예상한다\n val k = nums.size / 2\n val buckets = mutableListOf&lt;MutableList&lt;Float&gt;&gt;()\n for (i in 0..&lt;k) {\n buckets.add(mutableListOf())\n }\n // 1. 배열 원소를 각 버킷에 배정한다\n for (num in nums) {\n // 입력 데이터의 범위는 [0, 1)이며, num * k를 사용하여 인덱스 범위 [0, k-1]에 대응시킨다\n val i = (num * k).toInt()\n // num을 버킷 i에 추가한다\n buckets[i].add(num)\n }\n // 2. 각 버킷을 정렬한다\n for (bucket in buckets) {\n // 내장 정렬 함수를 사용하며, 다른 정렬 알고리즘으로 대체할 수도 있다\n bucket.sort()\n }\n // 3. 버킷을 순회하여 결과를 합친다\n var i = 0\n for (bucket in buckets) {\n for (num in bucket) {\n nums[i++] = num\n }\n }\n}\n</code></pre> bucket_sort.rb<pre><code>### 버킷 정렬 ###\ndef bucket_sort(nums)\n # 버킷 k = n/2개를 초기화하며, 각 버킷에 원소 2개가 배정될 것으로 예상한다\n k = nums.length / 2\n buckets = Array.new(k) { [] }\n\n # 1. 배열 원소를 각 버킷에 배정한다\n nums.each do |num|\n # 입력 데이터의 범위는 [0, 1)이며, num * k를 사용하여 인덱스 범위 [0, k-1]에 대응시킨다\n i = (num * k).to_i\n # num을 버킷 i에 추가한다\n buckets[i] &lt;&lt; num\n end\n\n # 2. 각 버킷을 정렬한다\n buckets.each do |bucket|\n # 내장 정렬 함수를 사용하며, 다른 정렬 알고리즘으로 대체할 수도 있다\n bucket.sort!\n end\n\n # 3. 버킷을 순회하여 결과를 합친다\n i = 0\n buckets.each do |bucket|\n bucket.each do |num|\n nums[i] = num\n i += 1\n end\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 11 장   정렬","11.8   버킷 정렬"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1182","level":2,"title":"11.8.2   알고리즘 특성","text":"<p>버킷 정렬은 매우 큰 데이터를 처리하는 데 적합하다. 예를 들어 입력에 원소가 100만 개 있고 공간 제약 때문에 시스템 메모리에 모든 데이터를 한 번에 올릴 수 없다고 하자. 이때 데이터를 1000개 버킷으로 나누어 각각 정렬한 다음 결과를 합칠 수 있다.</p> <ul> <li>시간 복잡도 \\(O(n + k)\\): 원소가 각 버킷에 고르게 분포한다면 버킷당 원소 수는 \\(\\frac{n}{k}\\)이다. 버킷 하나를 정렬하는 데 \\(O(\\frac{n}{k} \\log\\frac{n}{k})\\) 시간이 걸린다고 하면 모든 버킷의 정렬에는 \\(O(n \\log\\frac{n}{k})\\) 시간이 걸린다. 버킷 수 \\(k\\)가 충분히 크면 시간 복잡도는 \\(O(n)\\)에 가까워진다. 결과를 합칠 때는 모든 버킷과 원소를 순회하므로 \\(O(n + k)\\) 시간이 든다. 최악의 경우 모든 데이터가 하나의 버킷에 들어가며, 그 버킷을 정렬하는 데 \\(O(n^2)\\) 시간이 걸린다.</li> <li>공간 복잡도 \\(O(n + k)\\), 제자리 정렬이 아님: 버킷 \\(k\\) 개와 총 \\(n\\) 개의 원소를 위한 추가 공간이 필요하다.</li> <li>버킷 정렬의 안정성은 버킷 내부를 정렬하는 알고리즘의 안정성에 달려 있다.</li> </ul>","path":["제 11 장   정렬","11.8   버킷 정렬"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1183","level":2,"title":"11.8.3   고르게 분배하는 방법","text":"<p>버킷 정렬의 시간 복잡도는 이론적으로 \\(O(n)\\)에 이를 수 있다. 핵심은 원소를 각 버킷에 고르게 분배하는 것인데, 실제 데이터는 균일하게 분포하지 않는 경우가 많다. 예를 들어 타오바오의 모든 상품을 가격 범위에 따라 10개 버킷에 고르게 나누고 싶다고 하자. 상품 가격은 불균일하여 100위안 미만의 상품은 매우 많고 1000위안 초과 상품은 매우 적다. 가격 구간을 같은 폭으로 10등분하면 버킷별 상품 수에 큰 차이가 생긴다.</p> <p>고르게 분배하려면 먼저 대략적인 경계를 정해 데이터를 버킷 3개로 나눌 수 있다. 분배한 뒤 상품이 많은 버킷을 다시 3개로 나누고, 모든 버킷의 원소 수가 비슷해질 때까지 반복한다.</p> <p>아래 그림(11-14)과 같이 이 방법은 본질적으로 재귀 트리를 만드는 과정이며, 리프 노드의 값을 가능한 한 고르게 만드는 것이 목표다. 반드시 매번 버킷 3개로 나눌 필요는 없으며, 데이터 특성에 맞추어 분할 방식을 유연하게 선택할 수 있다.</p> <p></p> <p> 그림 11-14   버킷을 재귀적으로 분할 </p> <p>상품 가격의 확률 분포를 미리 알고 있다면 그 분포에 따라 각 버킷의 가격 경계를 설정할 수 있다. 데이터 분포를 반드시 따로 집계할 필요는 없으며, 데이터 특성에 따라 적절한 확률 모형으로 근사할 수도 있다.</p> <p>아래 그림(11-15)에서는 상품 가격이 정규 분포를 따른다고 가정한다. 이를 바탕으로 가격 구간을 적절히 정하면 상품을 각 버킷에 고르게 분배할 수 있다.</p> <p></p> <p> 그림 11-15   확률 분포에 따른 버킷 분할 </p>","path":["제 11 장   정렬","11.8   버킷 정렬"],"tags":[]},{"location":"chapter_sorting/counting_sort/","level":1,"title":"11.9   계수 정렬","text":"<p>계수 정렬(counting sort)은 원소의 개수를 세어 정렬하며, 주로 정수 배열에 사용한다.</p>","path":["제 11 장   정렬","11.9   계수 정렬"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1191","level":2,"title":"11.9.1   간단한 구현","text":"<p>먼저 간단한 예를 보자. 길이가 \\(n\\)인 배열 <code>nums</code>의 모든 원소가 '음이 아닌 정수'라고 하자. 계수 정렬의 전체 과정은 아래 그림(11-16)과 같다.</p> <ol> <li>배열을 순회하여 가장 큰 수 \\(m\\)을 찾고, 길이가 \\(m + 1\\)인 보조 배열 <code>counter</code>를 만든다.</li> <li><code>counter</code>를 이용하여 <code>nums</code>에서 각 수가 나타나는 횟수를 센다. <code>counter[num]</code>은 수 <code>num</code>의 등장 횟수다. 세는 방법은 간단하다. <code>nums</code>를 순회하면서 현재 수를 <code>num</code>이라고 할 때 <code>counter[num]</code>을 \\(1\\)씩 늘리면 된다.</li> <li><code>counter</code>의 인덱스는 본래 순서대로 배열되어 있으므로, 모든 수가 이미 정렬된 셈이다. 이어서 <code>counter</code>를 순회하며 각 수의 등장 횟수만큼 작은 수부터 큰 수 순서로 <code>nums</code>에 채우면 된다.</li> </ol> <p></p> <p> 그림 11-16   계수 정렬 과정 </p> <p>코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby counting_sort.py<pre><code>def counting_sort_naive(nums: list[int]):\n \"\"\"계수 정렬\"\"\"\n # 간단한 구현이며, 객체 정렬에는 사용할 수 없다\n # 1. 배열의 최댓값 m을 구한다\n m = max(nums)\n # 2. 각 숫자의 등장 횟수를 센다\n # counter[num]은 num의 등장 횟수를 나타낸다\n counter = [0] * (m + 1)\n for num in nums:\n counter[num] += 1\n # 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다\n i = 0\n for num in range(m + 1):\n for _ in range(counter[num]):\n nums[i] = num\n i += 1\n</code></pre> counting_sort.cpp<pre><code>/* 계수 정렬 */\n// 간단한 구현이며, 객체 정렬에는 사용할 수 없다\nvoid countingSortNaive(vector&lt;int&gt; &amp;nums) {\n // 1. 배열의 최댓값 m을 구한다\n int m = 0;\n for (int num : nums) {\n m = max(m, num);\n }\n // 2. 각 숫자의 등장 횟수를 센다\n // counter[num]은 num의 등장 횟수를 나타낸다\n vector&lt;int&gt; counter(m + 1, 0);\n for (int num : nums) {\n counter[num]++;\n }\n // 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다\n int i = 0;\n for (int num = 0; num &lt; m + 1; num++) {\n for (int j = 0; j &lt; counter[num]; j++, i++) {\n nums[i] = num;\n }\n }\n}\n</code></pre> counting_sort.java<pre><code>/* 계수 정렬 */\n// 간단한 구현이며, 객체 정렬에는 사용할 수 없다\nvoid countingSortNaive(int[] nums) {\n // 1. 배열의 최댓값 m을 구한다\n int m = 0;\n for (int num : nums) {\n m = Math.max(m, num);\n }\n // 2. 각 숫자의 등장 횟수를 센다\n // counter[num]은 num의 등장 횟수를 나타낸다\n int[] counter = new int[m + 1];\n for (int num : nums) {\n counter[num]++;\n }\n // 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다\n int i = 0;\n for (int num = 0; num &lt; m + 1; num++) {\n for (int j = 0; j &lt; counter[num]; j++, i++) {\n nums[i] = num;\n }\n }\n}\n</code></pre> counting_sort.cs<pre><code>/* 계수 정렬 */\n// 간단한 구현이며, 객체 정렬에는 사용할 수 없다\nvoid CountingSortNaive(int[] nums) {\n // 1. 배열의 최댓값 m을 구한다\n int m = 0;\n foreach (int num in nums) {\n m = Math.Max(m, num);\n }\n // 2. 각 숫자의 등장 횟수를 센다\n // counter[num]은 num의 등장 횟수를 나타낸다\n int[] counter = new int[m + 1];\n foreach (int num in nums) {\n counter[num]++;\n }\n // 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다\n int i = 0;\n for (int num = 0; num &lt; m + 1; num++) {\n for (int j = 0; j &lt; counter[num]; j++, i++) {\n nums[i] = num;\n }\n }\n}\n</code></pre> counting_sort.go<pre><code>/* 계수 정렬 */\n// 간단한 구현이며, 객체 정렬에는 사용할 수 없다\nfunc countingSortNaive(nums []int) {\n // 1. 배열의 최댓값 m을 구한다\n m := 0\n for _, num := range nums {\n if num &gt; m {\n m = num\n }\n }\n // 2. 각 숫자의 등장 횟수를 센다\n // counter[num]은 num의 등장 횟수를 나타낸다\n counter := make([]int, m+1)\n for _, num := range nums {\n counter[num]++\n }\n // 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다\n for i, num := 0, 0; num &lt; m+1; num++ {\n for j := 0; j &lt; counter[num]; j++ {\n nums[i] = num\n i++\n }\n }\n}\n</code></pre> counting_sort.swift<pre><code>/* 계수 정렬 */\n// 간단한 구현이며, 객체 정렬에는 사용할 수 없다\nfunc countingSortNaive(nums: inout [Int]) {\n // 1. 배열의 최댓값 m을 구한다\n let m = nums.max()!\n // 2. 각 숫자의 등장 횟수를 센다\n // counter[num]은 num의 등장 횟수를 나타낸다\n var counter = Array(repeating: 0, count: m + 1)\n for num in nums {\n counter[num] += 1\n }\n // 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다\n var i = 0\n for num in 0 ..&lt; m + 1 {\n for _ in 0 ..&lt; counter[num] {\n nums[i] = num\n i += 1\n }\n }\n}\n</code></pre> counting_sort.js<pre><code>/* 계수 정렬 */\n// 간단한 구현이며, 객체 정렬에는 사용할 수 없다\nfunction countingSortNaive(nums) {\n // 1. 배열의 최댓값 m을 구한다\n let m = Math.max(...nums);\n // 2. 각 숫자의 등장 횟수를 센다\n // counter[num]은 num의 등장 횟수를 나타낸다\n const counter = new Array(m + 1).fill(0);\n for (const num of nums) {\n counter[num]++;\n }\n // 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다\n let i = 0;\n for (let num = 0; num &lt; m + 1; num++) {\n for (let j = 0; j &lt; counter[num]; j++, i++) {\n nums[i] = num;\n }\n }\n}\n</code></pre> counting_sort.ts<pre><code>/* 계수 정렬 */\n// 간단한 구현이며, 객체 정렬에는 사용할 수 없다\nfunction countingSortNaive(nums: number[]): void {\n // 1. 배열의 최댓값 m을 구한다\n let m: number = Math.max(...nums);\n // 2. 각 숫자의 등장 횟수를 센다\n // counter[num]은 num의 등장 횟수를 나타낸다\n const counter: number[] = new Array&lt;number&gt;(m + 1).fill(0);\n for (const num of nums) {\n counter[num]++;\n }\n // 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다\n let i = 0;\n for (let num = 0; num &lt; m + 1; num++) {\n for (let j = 0; j &lt; counter[num]; j++, i++) {\n nums[i] = num;\n }\n }\n}\n</code></pre> counting_sort.dart<pre><code>/* 계수 정렬 */\n// 간단한 구현이며, 객체 정렬에는 사용할 수 없다\nvoid countingSortNaive(List&lt;int&gt; nums) {\n // 1. 배열의 최댓값 m을 구한다\n int m = 0;\n for (int _num in nums) {\n m = max(m, _num);\n }\n // 2. 각 숫자의 등장 횟수를 센다\n // counter[_num]은 _num의 등장 횟수를 나타낸다\n List&lt;int&gt; counter = List.filled(m + 1, 0);\n for (int _num in nums) {\n counter[_num]++;\n }\n // 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다\n int i = 0;\n for (int _num = 0; _num &lt; m + 1; _num++) {\n for (int j = 0; j &lt; counter[_num]; j++, i++) {\n nums[i] = _num;\n }\n }\n}\n</code></pre> counting_sort.rs<pre><code>/* 계수 정렬 */\n// 간단한 구현이며, 객체 정렬에는 사용할 수 없다\nfn counting_sort_naive(nums: &amp;mut [i32]) {\n // 1. 배열의 최댓값 m을 구한다\n let m = *nums.iter().max().unwrap();\n // 2. 각 숫자의 등장 횟수를 센다\n // counter[num]은 num의 등장 횟수를 나타낸다\n let mut counter = vec![0; m as usize + 1];\n for &amp;num in nums.iter() {\n counter[num as usize] += 1;\n }\n // 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다\n let mut i = 0;\n for num in 0..m + 1 {\n for _ in 0..counter[num as usize] {\n nums[i] = num;\n i += 1;\n }\n }\n}\n</code></pre> counting_sort.c<pre><code>/* 계수 정렬 */\n// 간단한 구현이며, 객체 정렬에는 사용할 수 없다\nvoid countingSortNaive(int nums[], int size) {\n // 1. 배열의 최댓값 m을 구한다\n int m = 0;\n for (int i = 0; i &lt; size; i++) {\n if (nums[i] &gt; m) {\n m = nums[i];\n }\n }\n // 2. 각 숫자의 등장 횟수를 센다\n // counter[num]은 num의 등장 횟수를 나타낸다\n int *counter = calloc(m + 1, sizeof(int));\n for (int i = 0; i &lt; size; i++) {\n counter[nums[i]]++;\n }\n // 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다\n int i = 0;\n for (int num = 0; num &lt; m + 1; num++) {\n for (int j = 0; j &lt; counter[num]; j++, i++) {\n nums[i] = num;\n }\n }\n // 4. 메모리를 해제한다\n free(counter);\n}\n</code></pre> counting_sort.kt<pre><code>/* 계수 정렬 */\n// 간단한 구현이며, 객체 정렬에는 사용할 수 없다\nfun countingSortNaive(nums: IntArray) {\n // 1. 배열의 최댓값 m을 구한다\n var m = 0\n for (num in nums) {\n m = max(m, num)\n }\n // 2. 각 숫자의 등장 횟수를 센다\n // counter[num]은 num의 등장 횟수를 나타낸다\n val counter = IntArray(m + 1)\n for (num in nums) {\n counter[num]++\n }\n // 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다\n var i = 0\n for (num in 0..&lt;m + 1) {\n var j = 0\n while (j &lt; counter[num]) {\n nums[i] = num\n j++\n i++\n }\n }\n}\n</code></pre> counting_sort.rb<pre><code>### 계수 정렬 ###\ndef counting_sort_naive(nums)\n # 간단한 구현이며, 객체 정렬에는 사용할 수 없다\n # 1. 배열의 최댓값 m을 구한다\n m = 0\n nums.each { |num| m = [m, num].max }\n # 2. 각 숫자의 등장 횟수를 센다\n # counter[num]은 num의 등장 횟수를 나타낸다\n counter = Array.new(m + 1, 0)\n nums.each { |num| counter[num] += 1 }\n # 3. counter를 순회하며 각 원소를 원래 배열 nums에 채운다\n i = 0\n for num in 0...(m + 1)\n (0...counter[num]).each do\n nums[i] = num\n i += 1\n end\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>계수 정렬과 버킷 정렬의 관계</p> <p>버킷 정렬의 관점에서는 계수 배열 <code>counter</code>의 각 인덱스를 하나의 버킷으로, 개수를 세는 과정을 원소를 해당 버킷에 분배하는 과정으로 볼 수 있다. 본질적으로 계수 정렬은 정수 데이터에 적용되는 버킷 정렬의 특수한 경우다.</p>","path":["제 11 장   정렬","11.9   계수 정렬"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1192","level":2,"title":"11.9.2   완전한 구현","text":"<p>주의 깊게 읽었다면 입력 데이터가 객체일 때 위의 <code>3.</code> 단계가 작동하지 않는다는 것을 알아차렸을 것이다. 입력이 상품 객체이고 상품 가격(클래스의 멤버 변수)에 따라 정렬하려 한다고 하자. 위 알고리즘으로는 가격의 정렬 결과만 얻을 수 있다.</p> <p>그렇다면 원래 데이터의 정렬 결과는 어떻게 얻을까? 먼저 <code>counter</code>의 '접두 합'을 계산한다. 이름 그대로 인덱스 <code>i</code>의 접두 합 <code>prefix[i]</code>는 배열의 앞 <code>i</code> 개 원소를 더한 값이다.</p> \\[ \\text{prefix}[i] = \\sum_{j=0}^i \\text{counter[j]} \\] <p>접두 합은 명확한 의미를 가진다. <code>prefix[num] - 1</code>은 결과 배열 <code>res</code>에서 원소 <code>num</code>이 마지막으로 나타나는 인덱스다. 이는 각 원소가 결과 배열의 어디에 놓여야 하는지 알려 주는 핵심 정보다. 이어서 원래 배열 <code>nums</code>의 각 원소 <code>num</code>을 역순으로 순회하며 매번 다음 두 단계를 수행한다.</p> <ol> <li>배열 <code>res</code>의 인덱스 <code>prefix[num] - 1</code>에 <code>num</code>을 넣는다.</li> <li>접두 합 <code>prefix[num]</code>을 \\(1\\) 줄여 다음에 <code>num</code>을 넣을 인덱스를 얻는다.</li> </ol> <p>순회를 마치면 배열 <code>res</code>에 정렬된 결과가 들어 있다. 마지막으로 <code>res</code>를 원래 배열 <code>nums</code>에 덮어쓰면 된다. 아래 그림(11-17)은 계수 정렬의 전체 과정을 보여 준다.</p> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt;&lt;6&gt;&lt;7&gt;&lt;8&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 11-17   계수 정렬 단계 </p> <p>계수 정렬의 구현 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby counting_sort.py<pre><code>def counting_sort(nums: list[int]):\n \"\"\"계수 정렬\"\"\"\n # 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다\n # 1. 배열의 최댓값 m을 구한다\n m = max(nums)\n # 2. 각 숫자의 등장 횟수를 센다\n # counter[num]은 num의 등장 횟수를 나타낸다\n counter = [0] * (m + 1)\n for num in nums:\n counter[num] += 1\n # 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다\n # 즉, counter[num]-1은 res에서 num이 마지막으로 등장하는 인덱스이다\n for i in range(m):\n counter[i + 1] += counter[i]\n # 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다\n # 결과를 기록할 배열 res를 초기화한다\n n = len(nums)\n res = [0] * n\n for i in range(n - 1, -1, -1):\n num = nums[i]\n res[counter[num] - 1] = num # num을 해당 인덱스에 배치한다\n counter[num] -= 1 # 접두 합을 1 감소시켜 다음에 num을 배치할 인덱스를 구한다\n # 결과 배열 res로 원래 배열 nums를 덮어쓴다\n for i in range(n):\n nums[i] = res[i]\n</code></pre> counting_sort.cpp<pre><code>/* 계수 정렬 */\n// 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다\nvoid countingSort(vector&lt;int&gt; &amp;nums) {\n // 1. 배열의 최댓값 m을 구한다\n int m = 0;\n for (int num : nums) {\n m = max(m, num);\n }\n // 2. 각 숫자의 등장 횟수를 센다\n // counter[num]은 num의 등장 횟수를 나타낸다\n vector&lt;int&gt; counter(m + 1, 0);\n for (int num : nums) {\n counter[num]++;\n }\n // 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다\n // 즉, counter[num]-1은 res에서 num이 마지막으로 등장하는 인덱스이다\n for (int i = 0; i &lt; m; i++) {\n counter[i + 1] += counter[i];\n }\n // 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다\n // 결과를 기록할 배열 res를 초기화한다\n int n = nums.size();\n vector&lt;int&gt; res(n);\n for (int i = n - 1; i &gt;= 0; i--) {\n int num = nums[i];\n res[counter[num] - 1] = num; // num을 해당 인덱스에 배치한다\n counter[num]--; // 접두 합을 1 감소시켜 다음에 num을 배치할 인덱스를 구한다\n }\n // 결과 배열 res로 원래 배열 nums를 덮어쓴다\n nums = res;\n}\n</code></pre> counting_sort.java<pre><code>/* 계수 정렬 */\n// 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다\nvoid countingSort(int[] nums) {\n // 1. 배열의 최댓값 m을 구한다\n int m = 0;\n for (int num : nums) {\n m = Math.max(m, num);\n }\n // 2. 각 숫자의 등장 횟수를 센다\n // counter[num]은 num의 등장 횟수를 나타낸다\n int[] counter = new int[m + 1];\n for (int num : nums) {\n counter[num]++;\n }\n // 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다\n // 즉, counter[num]-1은 res에서 num이 마지막으로 등장하는 인덱스이다\n for (int i = 0; i &lt; m; i++) {\n counter[i + 1] += counter[i];\n }\n // 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다\n // 결과를 기록할 배열 res를 초기화한다\n int n = nums.length;\n int[] res = new int[n];\n for (int i = n - 1; i &gt;= 0; i--) {\n int num = nums[i];\n res[counter[num] - 1] = num; // num을 해당 인덱스에 배치한다\n counter[num]--; // 접두 합을 1 감소시켜 다음에 num을 배치할 인덱스를 구한다\n }\n // 결과 배열 res로 원래 배열 nums를 덮어쓴다\n for (int i = 0; i &lt; n; i++) {\n nums[i] = res[i];\n }\n}\n</code></pre> counting_sort.cs<pre><code>/* 계수 정렬 */\n// 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다\nvoid CountingSort(int[] nums) {\n // 1. 배열의 최댓값 m을 구한다\n int m = 0;\n foreach (int num in nums) {\n m = Math.Max(m, num);\n }\n // 2. 각 숫자의 등장 횟수를 센다\n // counter[num]은 num의 등장 횟수를 나타낸다\n int[] counter = new int[m + 1];\n foreach (int num in nums) {\n counter[num]++;\n }\n // 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다\n // 즉, counter[num]-1은 res에서 num이 마지막으로 등장하는 인덱스이다\n for (int i = 0; i &lt; m; i++) {\n counter[i + 1] += counter[i];\n }\n // 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다\n // 결과를 기록할 배열 res를 초기화한다\n int n = nums.Length;\n int[] res = new int[n];\n for (int i = n - 1; i &gt;= 0; i--) {\n int num = nums[i];\n res[counter[num] - 1] = num; // num을 해당 인덱스에 배치한다\n counter[num]--; // 접두 합을 1 감소시켜 다음에 num을 배치할 인덱스를 구한다\n }\n // 결과 배열 res로 원래 배열 nums를 덮어쓴다\n for (int i = 0; i &lt; n; i++) {\n nums[i] = res[i];\n }\n}\n</code></pre> counting_sort.go<pre><code>/* 계수 정렬 */\n// 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다\nfunc countingSort(nums []int) {\n // 1. 배열의 최댓값 m을 구한다\n m := 0\n for _, num := range nums {\n if num &gt; m {\n m = num\n }\n }\n // 2. 각 숫자의 등장 횟수를 센다\n // counter[num]은 num의 등장 횟수를 나타낸다\n counter := make([]int, m+1)\n for _, num := range nums {\n counter[num]++\n }\n // 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다\n // 즉, counter[num]-1은 res에서 num이 마지막으로 등장하는 인덱스이다\n for i := 0; i &lt; m; i++ {\n counter[i+1] += counter[i]\n }\n // 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다\n // 결과를 기록할 배열 res를 초기화한다\n n := len(nums)\n res := make([]int, n)\n for i := n - 1; i &gt;= 0; i-- {\n num := nums[i]\n // num을 해당 인덱스에 배치한다\n res[counter[num]-1] = num\n // 접두 합을 1 감소시켜 다음에 num을 배치할 인덱스를 구한다\n counter[num]--\n }\n // 결과 배열 res로 원래 배열 nums를 덮어쓴다\n copy(nums, res)\n}\n</code></pre> counting_sort.swift<pre><code>/* 계수 정렬 */\n// 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다\nfunc countingSort(nums: inout [Int]) {\n // 1. 배열의 최댓값 m을 구한다\n let m = nums.max()!\n // 2. 각 숫자의 등장 횟수를 센다\n // counter[num]은 num의 등장 횟수를 나타낸다\n var counter = Array(repeating: 0, count: m + 1)\n for num in nums {\n counter[num] += 1\n }\n // 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다\n // 즉, counter[num]-1은 res에서 num이 마지막으로 등장하는 인덱스이다\n for i in 0 ..&lt; m {\n counter[i + 1] += counter[i]\n }\n // 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다\n // 결과를 기록할 배열 res를 초기화한다\n var res = Array(repeating: 0, count: nums.count)\n for i in nums.indices.reversed() {\n let num = nums[i]\n res[counter[num] - 1] = num // num을 해당 인덱스에 배치한다\n counter[num] -= 1 // 접두 합을 1 감소시켜 다음에 num을 배치할 인덱스를 구한다\n }\n // 결과 배열 res로 원래 배열 nums를 덮어쓴다\n for i in nums.indices {\n nums[i] = res[i]\n }\n}\n</code></pre> counting_sort.js<pre><code>/* 계수 정렬 */\n// 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다\nfunction countingSort(nums) {\n // 1. 배열의 최댓값 m을 구한다\n let m = Math.max(...nums);\n // 2. 각 숫자의 등장 횟수를 센다\n // counter[num]은 num의 등장 횟수를 나타낸다\n const counter = new Array(m + 1).fill(0);\n for (const num of nums) {\n counter[num]++;\n }\n // 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다\n // 즉, counter[num]-1은 res에서 num이 마지막으로 등장하는 인덱스이다\n for (let i = 0; i &lt; m; i++) {\n counter[i + 1] += counter[i];\n }\n // 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다\n // 결과를 기록할 배열 res를 초기화한다\n const n = nums.length;\n const res = new Array(n);\n for (let i = n - 1; i &gt;= 0; i--) {\n const num = nums[i];\n res[counter[num] - 1] = num; // num을 해당 인덱스에 배치한다\n counter[num]--; // 접두 합을 1 감소시켜 다음에 num을 배치할 인덱스를 구한다\n }\n // 결과 배열 res로 원래 배열 nums를 덮어쓴다\n for (let i = 0; i &lt; n; i++) {\n nums[i] = res[i];\n }\n}\n</code></pre> counting_sort.ts<pre><code>/* 계수 정렬 */\n// 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다\nfunction countingSort(nums: number[]): void {\n // 1. 배열의 최댓값 m을 구한다\n let m: number = Math.max(...nums);\n // 2. 각 숫자의 등장 횟수를 센다\n // counter[num]은 num의 등장 횟수를 나타낸다\n const counter: number[] = new Array&lt;number&gt;(m + 1).fill(0);\n for (const num of nums) {\n counter[num]++;\n }\n // 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다\n // 즉, counter[num]-1은 res에서 num이 마지막으로 등장하는 인덱스이다\n for (let i = 0; i &lt; m; i++) {\n counter[i + 1] += counter[i];\n }\n // 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다\n // 결과를 기록할 배열 res를 초기화한다\n const n = nums.length;\n const res: number[] = new Array&lt;number&gt;(n);\n for (let i = n - 1; i &gt;= 0; i--) {\n const num = nums[i];\n res[counter[num] - 1] = num; // num을 해당 인덱스에 배치한다\n counter[num]--; // 접두 합을 1 감소시켜 다음에 num을 배치할 인덱스를 구한다\n }\n // 결과 배열 res로 원래 배열 nums를 덮어쓴다\n for (let i = 0; i &lt; n; i++) {\n nums[i] = res[i];\n }\n}\n</code></pre> counting_sort.dart<pre><code>/* 계수 정렬 */\n// 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다\nvoid countingSort(List&lt;int&gt; nums) {\n // 1. 배열의 최댓값 m을 구한다\n int m = 0;\n for (int _num in nums) {\n m = max(m, _num);\n }\n // 2. 각 숫자의 등장 횟수를 센다\n // counter[_num]은 _num의 등장 횟수를 나타낸다\n List&lt;int&gt; counter = List.filled(m + 1, 0);\n for (int _num in nums) {\n counter[_num]++;\n }\n // 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다\n // 즉, counter[_num]-1은 res에서 _num이 마지막으로 등장하는 인덱스이다\n for (int i = 0; i &lt; m; i++) {\n counter[i + 1] += counter[i];\n }\n // 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다\n // 결과를 기록할 배열 res를 초기화한다\n int n = nums.length;\n List&lt;int&gt; res = List.filled(n, 0);\n for (int i = n - 1; i &gt;= 0; i--) {\n int _num = nums[i];\n res[counter[_num] - 1] = _num; // _num을 해당 인덱스에 배치한다\n counter[_num]--; // 접두 합을 1 감소시켜 다음에 _num을 배치할 인덱스를 구한다\n }\n // 결과 배열 res로 원래 배열 nums를 덮어쓴다\n nums.setAll(0, res);\n}\n</code></pre> counting_sort.rs<pre><code>/* 계수 정렬 */\n// 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다\nfn counting_sort(nums: &amp;mut [i32]) {\n // 1. 배열의 최댓값 m을 구한다\n let m = *nums.iter().max().unwrap() as usize;\n // 2. 각 숫자의 등장 횟수를 센다\n // counter[num]은 num의 등장 횟수를 나타낸다\n let mut counter = vec![0; m + 1];\n for &amp;num in nums.iter() {\n counter[num as usize] += 1;\n }\n // 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다\n // 즉, counter[num]-1은 res에서 num이 마지막으로 등장하는 인덱스이다\n for i in 0..m {\n counter[i + 1] += counter[i];\n }\n // 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다\n // 결과를 기록할 배열 res를 초기화한다\n let n = nums.len();\n let mut res = vec![0; n];\n for i in (0..n).rev() {\n let num = nums[i];\n res[counter[num as usize] - 1] = num; // num을 해당 인덱스에 배치한다\n counter[num as usize] -= 1; // 접두 합을 1 감소시켜 다음에 num을 배치할 인덱스를 구한다\n }\n // 결과 배열 res로 원래 배열 nums를 덮어쓴다\n nums.copy_from_slice(&amp;res)\n}\n</code></pre> counting_sort.c<pre><code>/* 계수 정렬 */\n// 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다\nvoid countingSort(int nums[], int size) {\n // 1. 배열의 최댓값 m을 구한다\n int m = 0;\n for (int i = 0; i &lt; size; i++) {\n if (nums[i] &gt; m) {\n m = nums[i];\n }\n }\n // 2. 각 숫자의 등장 횟수를 센다\n // counter[num]은 num의 등장 횟수를 나타낸다\n int *counter = calloc(m, sizeof(int));\n for (int i = 0; i &lt; size; i++) {\n counter[nums[i]]++;\n }\n // 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다\n // 즉, counter[num]-1은 res에서 num이 마지막으로 등장하는 인덱스이다\n for (int i = 0; i &lt; m; i++) {\n counter[i + 1] += counter[i];\n }\n // 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다\n // 결과를 기록할 배열 res를 초기화한다\n int *res = malloc(sizeof(int) * size);\n for (int i = size - 1; i &gt;= 0; i--) {\n int num = nums[i];\n res[counter[num] - 1] = num; // num을 해당 인덱스에 배치한다\n counter[num]--; // 접두 합을 1 감소시켜 다음에 num을 배치할 인덱스를 구한다\n }\n // 결과 배열 res로 원래 배열 nums를 덮어쓴다\n memcpy(nums, res, size * sizeof(int));\n // 5. 메모리를 해제한다\n free(res);\n free(counter);\n}\n</code></pre> counting_sort.kt<pre><code>/* 계수 정렬 */\n// 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다\nfun countingSort(nums: IntArray) {\n // 1. 배열의 최댓값 m을 구한다\n var m = 0\n for (num in nums) {\n m = max(m, num)\n }\n // 2. 각 숫자의 등장 횟수를 센다\n // counter[num]은 num의 등장 횟수를 나타낸다\n val counter = IntArray(m + 1)\n for (num in nums) {\n counter[num]++\n }\n // 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다\n // 즉, counter[num]-1은 res에서 num이 마지막으로 등장하는 인덱스이다\n for (i in 0..&lt;m) {\n counter[i + 1] += counter[i]\n }\n // 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다\n // 결과를 기록할 배열 res를 초기화한다\n val n = nums.size\n val res = IntArray(n)\n for (i in n - 1 downTo 0) {\n val num = nums[i]\n res[counter[num] - 1] = num // num을 해당 인덱스에 배치한다\n counter[num]-- // 접두 합을 1 감소시켜 다음에 num을 배치할 인덱스를 구한다\n }\n // 결과 배열 res로 원래 배열 nums를 덮어쓴다\n for (i in 0..&lt;n) {\n nums[i] = res[i]\n }\n}\n</code></pre> counting_sort.rb<pre><code>### 계수 정렬 ###\ndef counting_sort(nums)\n # 완전한 구현이며, 객체를 정렬할 수 있고 안정 정렬이다\n # 1. 배열의 최댓값 m을 구한다\n m = nums.max\n # 2. 각 숫자의 등장 횟수를 센다\n # counter[num]은 num의 등장 횟수를 나타낸다\n counter = Array.new(m + 1, 0)\n nums.each { |num| counter[num] += 1 }\n # 3. counter의 접두 합을 구하여 등장 횟수를 마지막 인덱스로 변환한다\n # 즉, counter[num]-1은 res에서 num이 마지막으로 등장하는 인덱스이다\n (0...m).each { |i| counter[i + 1] += counter[i] }\n # 4. nums를 역순으로 순회하며 각 원소를 결과 배열 res에 채운다\n # 결과를 기록할 배열 res를 초기화한다\n n = nums.length\n res = Array.new(n, 0)\n (n - 1).downto(0).each do |i|\n num = nums[i]\n res[counter[num] - 1] = num # num을 해당 인덱스에 배치한다\n counter[num] -= 1 # 접두 합을 1 감소시켜 다음에 num을 배치할 인덱스를 구한다\n end\n # 결과 배열 res로 원래 배열 nums를 덮어쓴다\n (0...n).each { |i| nums[i] = res[i] }\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 11 장   정렬","11.9   계수 정렬"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1193","level":2,"title":"11.9.3   알고리즘 특성","text":"<ul> <li>시간 복잡도 \\(O(n + m)\\), 비적응형 정렬: <code>nums</code>와 <code>counter</code>를 순회하며, 각각 선형 시간이 걸린다. 보통 \\(n \\gg m\\)이므로 시간 복잡도는 \\(O(n)\\)에 가까워진다.</li> <li>공간 복잡도 \\(O(n + m)\\), 제자리 정렬이 아님: 길이가 각각 \\(n\\)과 \\(m\\)인 배열 <code>res</code>와 <code>counter</code>를 사용한다.</li> <li>안정 정렬: <code>res</code>에는 '오른쪽에서 왼쪽으로' 원소를 채우므로, <code>nums</code>를 역순으로 순회하면 값이 같은 원소의 상대적 위치가 바뀌지 않아 안정 정렬이 된다. 정방향으로 순회해도 정렬 결과 자체는 올바르지만 안정적이지 않다.</li> </ul>","path":["제 11 장   정렬","11.9   계수 정렬"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1194","level":2,"title":"11.9.4   한계","text":"<p>여기까지 보면 계수 정렬이 매우 기발하다고 느낄 수 있다. 개수만 세어도 효율적으로 정렬할 수 있기 때문이다. 하지만 계수 정렬을 사용하기 위한 전제 조건은 비교적 엄격하다.</p> <p>계수 정렬은 음이 아닌 정수에만 적합하다. 다른 타입에 적용하려면 해당 데이터를 음이 아닌 정수로 바꿀 수 있어야 하며, 변환 과정에서 원소 간 대소 관계가 바뀌면 안 된다. 예를 들어 음수가 포함된 정수 배열은 모든 수에 일정한 상수를 더해 양수로 바꾼 뒤, 정렬 후 다시 원래 값으로 돌릴 수 있다.</p> <p>계수 정렬은 데이터 수는 많지만 값의 범위는 작은 경우에 적합하다. 위 예시에서 \\(m\\)이 너무 크면 공간을 지나치게 많이 차지한다. 또한 \\(n \\ll m\\)이면 계수 정렬에 \\(O(m)\\) 시간이 걸리므로 \\(O(n \\log n)\\) 정렬 알고리즘보다 느릴 수도 있다.</p>","path":["제 11 장   정렬","11.9   계수 정렬"],"tags":[]},{"location":"chapter_sorting/exercises/","level":1,"title":"11.12   연습 문제","text":"","path":["제 11 장   정렬","11.12   연습 문제"],"tags":[]},{"location":"chapter_sorting/exercises/#11121","level":2,"title":"11.12.1   개념 확인","text":"","path":["제 11 장   정렬","11.12   연습 문제"],"tags":[]},{"location":"chapter_sorting/exercises/#1","level":3,"title":"1.   선택 정렬과 버블 정렬의 처음 몇 회전","text":"<p>배열 <code>[4, 2, 5, 1, 3]</code>이 주어진다. 다음은 모두 오름차순으로 정렬한다.</p> <ol> <li>선택 정렬의 첫 두 회전을 따라가며 각 회전이 끝난 뒤의 배열을 적고, 어느 위치가 확정되었는지 설명하라.</li> <li>버블 정렬의 첫 회전을 따라가며 배열 상태와 교환 횟수를 적고, 어느 위치가 확정되었는지 설명하라.</li> </ol> 참고 답안 <ol> <li> <p>첫 두 회전은 다음과 같다.</p> 회전 배열 상태 설명 1 <code>[1, 2, 5, 4, 3]</code> 가장 작은 원소 1을 첫 번째 원소와 교환 2 <code>[1, 2, 5, 4, 3]</code> 값 2가 이미 인덱스 1에 있으므로 교환할 필요 없음 <p>이때 앞의 두 위치는 확정되었으므로, 이후에는 <code>[5, 4, 3]</code>에서 계속 최솟값을 선택하면 된다.</p> </li> <li> <p>이웃한 원소를 차례대로 비교한다. 4와 2는 교환하고, 4와 5는 교환하지 않으며, 5와 1을 교환한 뒤 5와 3을 교환한다. 결과는 <code>[2, 4, 1, 3, 5]</code>이고 총 3번 교환한다. 가장 큰 원소 5가 배열 끝으로 이동했으므로 마지막 위치가 확정되었다.</p> </li> </ol>","path":["제 11 장   정렬","11.12   연습 문제"],"tags":[]},{"location":"chapter_sorting/exercises/#2","level":3,"title":"2.   값이 같은 원소의 순서가 달라지는가","text":"<p>배열 \\([2_a, 2_b, 1]\\)에서 \\(2_a\\)와 \\(2_b\\)는 값이 같지만, 아래 첨자로 원래 순서를 표시했다.</p> <ol> <li>선택 정렬의 첫 회전이 끝난 뒤의 배열을 적어 보자. \\(2_a\\)와 \\(2_b\\)의 상대적인 순서가 바뀌었는가?</li> <li>버블 정렬의 첫 회전이 끝난 뒤의 배열을 적어 보자. \\(2_a\\)와 \\(2_b\\)의 상대적인 순서가 바뀌었는가?</li> <li>앞의 두 결과를 바탕으로, 값이 같은 원소의 원래 순서를 유지하는 측면에서 두 정렬이 어떻게 다른지 설명하라.</li> </ol> 참고 답안 <ol> <li> <p>선택 정렬의 첫 회전에서는 가장 작은 원소 1을 골라 첫 원소 \\(2_a\\)와 교환한다. 결과는 \\([1, 2_b, 2_a]\\)다. \\(2_a\\)가 \\(2_b\\) 뒤로 옮겨졌으므로 두 원소의 상대적인 순서가 바뀌었다.</p> </li> <li> <p>버블 정렬은 먼저 \\(2_a\\)와 \\(2_b\\)를 비교한다. 두 값이 같으므로 교환하지 않는다. 이어서 \\(2_b\\)와 1을 비교하여 교환한다. 첫 회전이 끝나면 \\([2_a, 1, 2_b]\\)가 된다. \\(2_a\\)가 여전히 \\(2_b\\) 앞에 있으므로 상대적인 순서는 바뀌지 않았다.</p> </li> <li> <p>이 예에서 선택 정렬은 값이 같은 원소의 원래 순서를 바꾼다. 버블 정렬은 왼쪽 원소가 오른쪽 원소보다 클 때만 이웃한 원소를 교환한다. 값이 같은 원소끼리는 교환하지 않으므로 원래의 앞뒤 순서를 유지할 수 있다.</p> </li> </ol>","path":["제 11 장   정렬","11.12   연습 문제"],"tags":[]},{"location":"chapter_sorting/exercises/#3","level":3,"title":"3.   계수 정렬과 기수 정렬 비교하기","text":"<p>학교에서 자릿수가 8자리로 고정된 학번을 대량으로 정렬하려 한다. 다음에 답하라.</p> <ol> <li>가장 낮은 자리부터 처리하는 기수 정렬은 총 몇 회전이 필요한가?</li> <li>학번을 그대로 정수로 취급하여 계수 정렬을 하면, 사용하지 않는 계수 배열의 자리를 많이 마련하게 되는 이유는 무엇인가?</li> <li>앞의 두 결과를 바탕으로, 8자리로 고정된 학번을 대량으로 처리하려면 계수 정렬과 기수 정렬 중 어느 것을 선택하겠는가? 이유도 설명하라.</li> </ol> 참고 답안 <ol> <li> <p>학번은 8자리이므로 가장 낮은 자리부터 가장 높은 자리까지 총 8회전이 필요하다. 매 회전에서는 0~9에 따라 그룹을 나누기만 하면 된다.</p> </li> <li> <p>학번 전체의 출현 횟수를 직접 세려면 가능한 모든 8자리 값에 대해 자리를 마련해야 한다. 하지만 실제 학생이 사용하는 학번은 그중 일부에 불과하므로, 계수 배열의 대부분은 계속 0으로 남는다.</p> </li> <li> <p>기수 정렬을 선택해야 한다. ‘자릿수가 고정되어 있고, 각 자리에 올 수 있는 값은 10가지뿐’이라는 특징을 이용하면 안정적인 그룹 나누기를 8회 반복하는 것으로 충분하다. 계수 정렬에서 8자리 학번 전체를 정수 인덱스로 사용하면 실제로는 나타나지 않을 수많은 값에 대해서도 계수 배열의 자리를 마련해야 한다.</p> </li> </ol>","path":["제 11 장   정렬","11.12   연습 문제"],"tags":[]},{"location":"chapter_sorting/exercises/#11122","level":2,"title":"11.12.2   프로그래밍 연습","text":"","path":["제 11 장   정렬","11.12   연습 문제"],"tags":[]},{"location":"chapter_sorting/exercises/#1_1","level":3,"title":"1.   병합 정렬로 배열 정렬하기","text":"<p>정수 배열 <code>nums</code>가 주어진다. 병합 정렬을 직접 구현하여 원소를 비내림차순으로 정렬한 뒤 반환하라. 언어에서 제공하는 정렬 함수를 호출하지 마라.</p> 풀이 힌트 <ol> <li>구간 길이가 1 이하면 이미 정렬되어 있다.</li> <li>중간 지점에서 구간을 둘로 나누고 각각 재귀적으로 정렬한다.</li> <li>두 포인터로 정렬된 두 구간을 병합한 뒤, 결과를 원래 배열에 다시 쓴다.</li> </ol> <p>LeetCode</p>","path":["제 11 장   정렬","11.12   연습 문제"],"tags":[]},{"location":"chapter_sorting/exercises/#2_1","level":3,"title":"2.   계수 정렬로 정수 배열 정렬하기","text":"<p>정수 배열 <code>nums</code>와 음이 아닌 정수 \\(K\\)가 주어진다. 배열의 모든 원소는 \\(0\\) 이상 \\(K\\) 이하이다.</p> <p>계수 정렬을 구현하여 비내림차순으로 정렬한 결과를 <code>nums</code>에 다시 쓰고 <code>nums</code>를 반환하라. 원소의 대소 비교로 순서를 결정하거나 언어에서 제공하는 정렬 함수를 호출하지 마라.</p> 풀이 힌트 <ol> <li>모든 원소가 0 이상 K 이하이므로 원소 값을 계수 배열의 인덱스로 바로 사용할 수 있다.</li> <li>먼저 nums를 한 번 순회하면서 해당 위치의 계수를 1씩 늘린다.</li> <li>이어서 계수 배열을 0부터 K까지 순회한다. 값 x가 나타난 횟수만큼 nums에 x를 연속해서 쓴다.</li> </ol>","path":["제 11 장   정렬","11.12   연습 문제"],"tags":[]},{"location":"chapter_sorting/heap_sort/","level":1,"title":"11.7   힙 정렬","text":"<p>참고</p> <p>이 절을 읽기 전에 '힙' 장을 먼저 학습하자.</p> <p>힙 정렬(heap sort)은 힙 자료 구조를 이용한 효율적인 정렬 알고리즘이다. 이미 배운 '힙 구성 연산'과 '힙에서 원소를 꺼내는 연산'으로 구현할 수 있다.</p> <ol> <li>배열을 입력받아 최소 힙을 구성한다. 이때 가장 작은 원소가 힙의 루트에 놓인다.</li> <li>힙에서 원소를 계속 꺼내며 순서대로 기록하면 오름차순으로 정렬된 수열을 얻는다.</li> </ol> <p>위 방법도 가능하지만 꺼낸 원소를 저장할 추가 배열이 필요하여 공간을 낭비한다. 실제로는 보통 더 간결한 구현 방식을 사용한다.</p>","path":["제 11 장   정렬","11.7   힙 정렬"],"tags":[]},{"location":"chapter_sorting/heap_sort/#1171","level":2,"title":"11.7.1   알고리즘 과정","text":"<p>배열의 길이가 \\(n\\)일 때 힙 정렬의 과정은 아래 그림(11-12)과 같다.</p> <ol> <li>배열을 입력받아 최대 힙을 구성한다. 완료하면 가장 큰 원소가 힙의 루트에 놓인다.</li> <li>힙의 루트 원소(첫 번째 원소)와 끝 원소(마지막 원소)를 교환한다. 교환 후 힙의 길이는 \\(1\\) 줄고, 정렬된 원소 수는 \\(1\\) 늘어난다.</li> <li>힙의 루트 원소부터 위에서 아래로 힙 재정렬(sift down)을 수행한다. 재정렬을 마치면 힙의 성질이 복구된다.</li> <li><code>2.</code> 단계와 <code>3.</code> 단계를 반복한다. \\(n - 1\\) 회 반복하면 배열 정렬이 완료된다.</li> </ol> <p>참고</p> <p>힙에서 원소를 꺼내는 연산에도 <code>2.</code> 단계와 <code>3.</code> 단계가 포함되며, 실제로 원소를 꺼내는 단계가 하나 더 있을 뿐이다.</p> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt;&lt;6&gt;&lt;7&gt;&lt;8&gt;&lt;9&gt;&lt;10&gt;&lt;11&gt;&lt;12&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 11-12   힙 정렬 단계 </p> <p>구현에서는 '힙' 장과 동일한 하향 힙 재정렬 함수 <code>sift_down()</code>을 사용한다. 가장 큰 원소를 꺼낼 때마다 힙의 길이가 줄어들므로, 현재 유효한 힙 길이를 지정하는 매개변수 \\(n\\)을 <code>sift_down()</code>에 추가해야 한다. 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby heap_sort.py<pre><code>def sift_down(nums: list[int], n: int, i: int):\n \"\"\"길이가 n인 힙에서 노드 i부터 위에서 아래로 힙 재정렬\"\"\"\n while True:\n # 노드 i, l, r 중 값이 가장 큰 노드를 찾아 ma로 표시한다\n l = 2 * i + 1\n r = 2 * i + 2\n ma = i\n if l &lt; n and nums[l] &gt; nums[ma]:\n ma = l\n if r &lt; n and nums[r] &gt; nums[ma]:\n ma = r\n # 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n if ma == i:\n break\n # 두 노드를 교환한다\n nums[i], nums[ma] = nums[ma], nums[i]\n # 아래쪽으로 힙 재정렬을 반복한다\n i = ma\n\ndef heap_sort(nums: list[int]):\n \"\"\"힙 정렬\"\"\"\n # 힙 구성 연산: 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n for i in range(len(nums) // 2 - 1, -1, -1):\n sift_down(nums, len(nums), i)\n # 힙에서 최댓값을 꺼내는 작업을 n-1회 반복한다\n for i in range(len(nums) - 1, 0, -1):\n # 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n nums[0], nums[i] = nums[i], nums[0]\n # 루트 노드에서 시작하여 위에서 아래로 힙을 재정렬한다\n sift_down(nums, i, 0)\n</code></pre> heap_sort.cpp<pre><code>/* 길이가 n인 힙에서 노드 i부터 위에서 아래로 힙 재정렬 */\nvoid siftDown(vector&lt;int&gt; &amp;nums, int n, int i) {\n while (true) {\n // 노드 i, l, r 중 값이 가장 큰 노드를 찾아 ma로 표시한다\n int l = 2 * i + 1;\n int r = 2 * i + 2;\n int ma = i;\n if (l &lt; n &amp;&amp; nums[l] &gt; nums[ma])\n ma = l;\n if (r &lt; n &amp;&amp; nums[r] &gt; nums[ma])\n ma = r;\n // 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n if (ma == i) {\n break;\n }\n // 두 노드를 교환한다\n swap(nums[i], nums[ma]);\n // 아래쪽으로 힙 재정렬을 반복한다\n i = ma;\n }\n}\n\n/* 힙 정렬 */\nvoid heapSort(vector&lt;int&gt; &amp;nums) {\n // 힙 구성 연산: 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n for (int i = nums.size() / 2 - 1; i &gt;= 0; --i) {\n siftDown(nums, nums.size(), i);\n }\n // 힙에서 최댓값을 꺼내는 작업을 n-1회 반복한다\n for (int i = nums.size() - 1; i &gt; 0; --i) {\n // 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n swap(nums[0], nums[i]);\n // 루트 노드에서 시작하여 위에서 아래로 힙을 재정렬한다\n siftDown(nums, i, 0);\n }\n}\n</code></pre> heap_sort.java<pre><code>/* 길이가 n인 힙에서 노드 i부터 위에서 아래로 힙 재정렬 */\nvoid siftDown(int[] nums, int n, int i) {\n while (true) {\n // 노드 i, l, r 중 값이 가장 큰 노드를 찾아 ma로 표시한다\n int l = 2 * i + 1;\n int r = 2 * i + 2;\n int ma = i;\n if (l &lt; n &amp;&amp; nums[l] &gt; nums[ma])\n ma = l;\n if (r &lt; n &amp;&amp; nums[r] &gt; nums[ma])\n ma = r;\n // 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n if (ma == i)\n break;\n // 두 노드를 교환한다\n int temp = nums[i];\n nums[i] = nums[ma];\n nums[ma] = temp;\n // 아래쪽으로 힙 재정렬을 반복한다\n i = ma;\n }\n}\n\n/* 힙 정렬 */\nvoid heapSort(int[] nums) {\n // 힙 구성 연산: 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n for (int i = nums.length / 2 - 1; i &gt;= 0; i--) {\n siftDown(nums, nums.length, i);\n }\n // 힙에서 최댓값을 꺼내는 작업을 n-1회 반복한다\n for (int i = nums.length - 1; i &gt; 0; i--) {\n // 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n int tmp = nums[0];\n nums[0] = nums[i];\n nums[i] = tmp;\n // 루트 노드에서 시작하여 위에서 아래로 힙을 재정렬한다\n siftDown(nums, i, 0);\n }\n}\n</code></pre> heap_sort.cs<pre><code>/* 길이가 n인 힙에서 노드 i부터 위에서 아래로 힙 재정렬 */\nvoid SiftDown(int[] nums, int n, int i) {\n while (true) {\n // 노드 i, l, r 중 값이 가장 큰 노드를 찾아 ma로 표시한다\n int l = 2 * i + 1;\n int r = 2 * i + 2;\n int ma = i;\n if (l &lt; n &amp;&amp; nums[l] &gt; nums[ma])\n ma = l;\n if (r &lt; n &amp;&amp; nums[r] &gt; nums[ma])\n ma = r;\n // 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n if (ma == i)\n break;\n // 두 노드를 교환한다\n (nums[ma], nums[i]) = (nums[i], nums[ma]);\n // 아래쪽으로 힙 재정렬을 반복한다\n i = ma;\n }\n}\n\n/* 힙 정렬 */\nvoid HeapSort(int[] nums) {\n // 힙 구성 연산: 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n for (int i = nums.Length / 2 - 1; i &gt;= 0; i--) {\n SiftDown(nums, nums.Length, i);\n }\n // 힙에서 최댓값을 꺼내는 작업을 n-1회 반복한다\n for (int i = nums.Length - 1; i &gt; 0; i--) {\n // 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n (nums[i], nums[0]) = (nums[0], nums[i]);\n // 루트 노드에서 시작하여 위에서 아래로 힙을 재정렬한다\n SiftDown(nums, i, 0);\n }\n}\n</code></pre> heap_sort.go<pre><code>/* 길이가 n인 힙에서 노드 i부터 위에서 아래로 힙 재정렬 */\nfunc siftDown(nums *[]int, n, i int) {\n for true {\n // 노드 i, l, r 중 값이 가장 큰 노드를 찾아 ma로 표시한다\n l := 2*i + 1\n r := 2*i + 2\n ma := i\n if l &lt; n &amp;&amp; (*nums)[l] &gt; (*nums)[ma] {\n ma = l\n }\n if r &lt; n &amp;&amp; (*nums)[r] &gt; (*nums)[ma] {\n ma = r\n }\n // 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n if ma == i {\n break\n }\n // 두 노드를 교환한다\n (*nums)[i], (*nums)[ma] = (*nums)[ma], (*nums)[i]\n // 아래쪽으로 힙 재정렬을 반복한다\n i = ma\n }\n}\n\n/* 힙 정렬 */\nfunc heapSort(nums *[]int) {\n // 힙 구성 연산: 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n for i := len(*nums)/2 - 1; i &gt;= 0; i-- {\n siftDown(nums, len(*nums), i)\n }\n // 힙에서 최댓값을 꺼내는 작업을 n-1회 반복한다\n for i := len(*nums) - 1; i &gt; 0; i-- {\n // 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n (*nums)[0], (*nums)[i] = (*nums)[i], (*nums)[0]\n // 루트 노드에서 시작하여 위에서 아래로 힙을 재정렬한다\n siftDown(nums, i, 0)\n }\n}\n</code></pre> heap_sort.swift<pre><code>/* 길이가 n인 힙에서 노드 i부터 위에서 아래로 힙 재정렬 */\nfunc siftDown(nums: inout [Int], n: Int, i: Int) {\n var i = i\n while true {\n // 노드 i, l, r 중 값이 가장 큰 노드를 찾아 ma로 표시한다\n let l = 2 * i + 1\n let r = 2 * i + 2\n var ma = i\n if l &lt; n, nums[l] &gt; nums[ma] {\n ma = l\n }\n if r &lt; n, nums[r] &gt; nums[ma] {\n ma = r\n }\n // 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n if ma == i {\n break\n }\n // 두 노드를 교환한다\n nums.swapAt(i, ma)\n // 아래쪽으로 힙 재정렬을 반복한다\n i = ma\n }\n}\n\n/* 힙 정렬 */\nfunc heapSort(nums: inout [Int]) {\n // 힙 구성 연산: 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n for i in stride(from: nums.count / 2 - 1, through: 0, by: -1) {\n siftDown(nums: &amp;nums, n: nums.count, i: i)\n }\n // 힙에서 최댓값을 꺼내는 작업을 n-1회 반복한다\n for i in nums.indices.dropFirst().reversed() {\n // 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n nums.swapAt(0, i)\n // 루트 노드에서 시작하여 위에서 아래로 힙을 재정렬한다\n siftDown(nums: &amp;nums, n: i, i: 0)\n }\n}\n</code></pre> heap_sort.js<pre><code>/* 길이가 n인 힙에서 노드 i부터 위에서 아래로 힙 재정렬 */\nfunction siftDown(nums, n, i) {\n while (true) {\n // 노드 i, l, r 중 값이 가장 큰 노드를 찾아 ma로 표시한다\n let l = 2 * i + 1;\n let r = 2 * i + 2;\n let ma = i;\n if (l &lt; n &amp;&amp; nums[l] &gt; nums[ma]) {\n ma = l;\n }\n if (r &lt; n &amp;&amp; nums[r] &gt; nums[ma]) {\n ma = r;\n }\n // 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n if (ma === i) {\n break;\n }\n // 두 노드를 교환한다\n [nums[i], nums[ma]] = [nums[ma], nums[i]];\n // 아래쪽으로 힙 재정렬을 반복한다\n i = ma;\n }\n}\n\n/* 힙 정렬 */\nfunction heapSort(nums) {\n // 힙 구성 연산: 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n for (let i = Math.floor(nums.length / 2) - 1; i &gt;= 0; i--) {\n siftDown(nums, nums.length, i);\n }\n // 힙에서 최댓값을 꺼내는 작업을 n-1회 반복한다\n for (let i = nums.length - 1; i &gt; 0; i--) {\n // 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n [nums[0], nums[i]] = [nums[i], nums[0]];\n // 루트 노드에서 시작하여 위에서 아래로 힙을 재정렬한다\n siftDown(nums, i, 0);\n }\n}\n</code></pre> heap_sort.ts<pre><code>/* 길이가 n인 힙에서 노드 i부터 위에서 아래로 힙 재정렬 */\nfunction siftDown(nums: number[], n: number, i: number): void {\n while (true) {\n // 노드 i, l, r 중 값이 가장 큰 노드를 찾아 ma로 표시한다\n let l = 2 * i + 1;\n let r = 2 * i + 2;\n let ma = i;\n if (l &lt; n &amp;&amp; nums[l] &gt; nums[ma]) {\n ma = l;\n }\n if (r &lt; n &amp;&amp; nums[r] &gt; nums[ma]) {\n ma = r;\n }\n // 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n if (ma === i) {\n break;\n }\n // 두 노드를 교환한다\n [nums[i], nums[ma]] = [nums[ma], nums[i]];\n // 아래쪽으로 힙 재정렬을 반복한다\n i = ma;\n }\n}\n\n/* 힙 정렬 */\nfunction heapSort(nums: number[]): void {\n // 힙 구성 연산: 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n for (let i = Math.floor(nums.length / 2) - 1; i &gt;= 0; i--) {\n siftDown(nums, nums.length, i);\n }\n // 힙에서 최댓값을 꺼내는 작업을 n-1회 반복한다\n for (let i = nums.length - 1; i &gt; 0; i--) {\n // 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n [nums[0], nums[i]] = [nums[i], nums[0]];\n // 루트 노드에서 시작하여 위에서 아래로 힙을 재정렬한다\n siftDown(nums, i, 0);\n }\n}\n</code></pre> heap_sort.dart<pre><code>/* 길이가 n인 힙에서 노드 i부터 위에서 아래로 힙 재정렬 */\nvoid siftDown(List&lt;int&gt; nums, int n, int i) {\n while (true) {\n // 노드 i, l, r 중 값이 가장 큰 노드를 찾아 ma로 표시한다\n int l = 2 * i + 1;\n int r = 2 * i + 2;\n int ma = i;\n if (l &lt; n &amp;&amp; nums[l] &gt; nums[ma]) ma = l;\n if (r &lt; n &amp;&amp; nums[r] &gt; nums[ma]) ma = r;\n // 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n if (ma == i) break;\n // 두 노드를 교환한다\n int temp = nums[i];\n nums[i] = nums[ma];\n nums[ma] = temp;\n // 아래쪽으로 힙 재정렬을 반복한다\n i = ma;\n }\n}\n\n/* 힙 정렬 */\nvoid heapSort(List&lt;int&gt; nums) {\n // 힙 구성 연산: 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n for (int i = nums.length ~/ 2 - 1; i &gt;= 0; i--) {\n siftDown(nums, nums.length, i);\n }\n // 힙에서 최댓값을 꺼내는 작업을 n-1회 반복한다\n for (int i = nums.length - 1; i &gt; 0; i--) {\n // 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n int tmp = nums[0];\n nums[0] = nums[i];\n nums[i] = tmp;\n // 루트 노드에서 시작하여 위에서 아래로 힙을 재정렬한다\n siftDown(nums, i, 0);\n }\n}\n</code></pre> heap_sort.rs<pre><code>/* 길이가 n인 힙에서 노드 i부터 위에서 아래로 힙 재정렬 */\nfn sift_down(nums: &amp;mut [i32], n: usize, mut i: usize) {\n loop {\n // 노드 i, l, r 중 값이 가장 큰 노드를 찾아 ma로 표시한다\n let l = 2 * i + 1;\n let r = 2 * i + 2;\n let mut ma = i;\n if l &lt; n &amp;&amp; nums[l] &gt; nums[ma] {\n ma = l;\n }\n if r &lt; n &amp;&amp; nums[r] &gt; nums[ma] {\n ma = r;\n }\n // 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n if ma == i {\n break;\n }\n // 두 노드를 교환한다\n nums.swap(i, ma);\n // 아래쪽으로 힙 재정렬을 반복한다\n i = ma;\n }\n}\n\n/* 힙 정렬 */\nfn heap_sort(nums: &amp;mut [i32]) {\n // 힙 구성 연산: 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n for i in (0..nums.len() / 2).rev() {\n sift_down(nums, nums.len(), i);\n }\n // 힙에서 최댓값을 꺼내는 작업을 n-1회 반복한다\n for i in (1..nums.len()).rev() {\n // 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n nums.swap(0, i);\n // 루트 노드에서 시작하여 위에서 아래로 힙을 재정렬한다\n sift_down(nums, i, 0);\n }\n}\n</code></pre> heap_sort.c<pre><code>/* 길이가 n인 힙에서 노드 i부터 위에서 아래로 힙 재정렬 */\nvoid siftDown(int nums[], int n, int i) {\n while (1) {\n // 노드 i, l, r 중 값이 가장 큰 노드를 찾아 ma로 표시한다\n int l = 2 * i + 1;\n int r = 2 * i + 2;\n int ma = i;\n if (l &lt; n &amp;&amp; nums[l] &gt; nums[ma])\n ma = l;\n if (r &lt; n &amp;&amp; nums[r] &gt; nums[ma])\n ma = r;\n // 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n if (ma == i) {\n break;\n }\n // 두 노드를 교환한다\n int temp = nums[i];\n nums[i] = nums[ma];\n nums[ma] = temp;\n // 아래쪽으로 힙 재정렬을 반복한다\n i = ma;\n }\n}\n\n/* 힙 정렬 */\nvoid heapSort(int nums[], int n) {\n // 힙 구성 연산: 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n for (int i = n / 2 - 1; i &gt;= 0; --i) {\n siftDown(nums, n, i);\n }\n // 힙에서 최댓값을 꺼내는 작업을 n-1회 반복한다\n for (int i = n - 1; i &gt; 0; --i) {\n // 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n int tmp = nums[0];\n nums[0] = nums[i];\n nums[i] = tmp;\n // 루트 노드에서 시작하여 위에서 아래로 힙을 재정렬한다\n siftDown(nums, i, 0);\n }\n}\n</code></pre> heap_sort.kt<pre><code>/* 길이가 n인 힙에서 노드 i부터 위에서 아래로 힙 재정렬 */\nfun siftDown(nums: IntArray, n: Int, li: Int) {\n var i = li\n while (true) {\n // 노드 i, l, r 중 값이 가장 큰 노드를 찾아 ma로 표시한다\n val l = 2 * i + 1\n val r = 2 * i + 2\n var ma = i\n if (l &lt; n &amp;&amp; nums[l] &gt; nums[ma]) \n ma = l\n if (r &lt; n &amp;&amp; nums[r] &gt; nums[ma]) \n ma = r\n // 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n if (ma == i) \n break\n // 두 노드를 교환한다\n val temp = nums[i]\n nums[i] = nums[ma]\n nums[ma] = temp\n // 아래쪽으로 힙 재정렬을 반복한다\n i = ma\n }\n}\n\n/* 힙 정렬 */\nfun heapSort(nums: IntArray) {\n // 힙 구성 연산: 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n for (i in nums.size / 2 - 1 downTo 0) {\n siftDown(nums, nums.size, i)\n }\n // 힙에서 최댓값을 꺼내는 작업을 n-1회 반복한다\n for (i in nums.size - 1 downTo 1) {\n // 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n val temp = nums[0]\n nums[0] = nums[i]\n nums[i] = temp\n // 루트 노드에서 시작하여 위에서 아래로 힙을 재정렬한다\n siftDown(nums, i, 0)\n }\n}\n</code></pre> heap_sort.rb<pre><code>### 길이가 n인 힙에서 노드 i부터 위에서 아래로 힙 재정렬 ###\ndef sift_down(nums, n, i)\n while true\n # 노드 i, l, r 중 값이 가장 큰 노드를 찾아 ma로 표시한다\n l = 2 * i + 1\n r = 2 * i + 2\n ma = i\n ma = l if l &lt; n &amp;&amp; nums[l] &gt; nums[ma]\n ma = r if r &lt; n &amp;&amp; nums[r] &gt; nums[ma]\n # 노드 i의 값이 가장 크거나 인덱스 l, r이 범위를 벗어나면 힙을 더 재정렬할 필요가 없으므로 종료한다\n break if ma == i\n # 두 노드를 교환한다\n nums[i], nums[ma] = nums[ma], nums[i]\n # 아래쪽으로 힙 재정렬을 반복한다\n i = ma\n end\nend\n\n### 힙 정렬 ###\ndef heap_sort(nums)\n # 힙 구성 연산: 리프 노드를 제외한 모든 노드에 힙 재정렬을 수행한다\n (nums.length / 2 - 1).downto(0) do |i|\n sift_down(nums, nums.length, i)\n end\n # 힙에서 최댓값을 꺼내는 작업을 n-1회 반복한다\n (nums.length - 1).downto(1) do |i|\n # 루트 노드와 가장 오른쪽 리프 노드를 교환한다(첫 원소와 마지막 원소 교환)\n nums[0], nums[i] = nums[i], nums[0]\n # 루트 노드에서 시작하여 위에서 아래로 힙을 재정렬한다\n sift_down(nums, i, 0)\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 11 장   정렬","11.7   힙 정렬"],"tags":[]},{"location":"chapter_sorting/heap_sort/#1172","level":2,"title":"11.7.2   알고리즘 특성","text":"<ul> <li>시간 복잡도 \\(O(n \\log n)\\), 비적응형 정렬: 힙 구성에는 \\(O(n)\\) 시간이 걸린다. 힙에서 최댓값을 꺼내는 시간 복잡도는 \\(O(\\log n)\\)이며, 총 \\(n - 1\\) 회 반복한다.</li> <li>공간 복잡도 \\(O(1)\\), 제자리 정렬: 몇 개의 포인터 변수가 \\(O(1)\\) 공간을 사용한다. 원소 교환과 힙 재정렬은 모두 원래 배열에서 수행한다.</li> <li>불안정 정렬: 힙의 루트 원소와 끝 원소를 교환할 때 값이 같은 원소들의 상대적 위치가 바뀔 수 있다.</li> </ul>","path":["제 11 장   정렬","11.7   힙 정렬"],"tags":[]},{"location":"chapter_sorting/insertion_sort/","level":1,"title":"11.4   삽입 정렬","text":"<p>삽입 정렬(insertion sort)은 간단한 정렬 알고리즘으로, 손으로 카드 한 벌을 정리하는 과정과 원리가 매우 비슷하다.</p> <p>구체적으로는 미정렬 구간에서 기준 원소 하나를 골라, 왼쪽의 정렬된 구간에 있는 원소들과 하나씩 크기를 비교한 뒤 올바른 위치에 삽입한다.</p> <p>아래 그림(11-6)은 배열에 원소를 삽입하는 과정을 보여 준다. 기준 원소를 <code>base</code>라고 하면 목표 인덱스부터 <code>base</code> 사이의 모든 원소를 오른쪽으로 한 칸씩 이동한 뒤, 목표 인덱스에 <code>base</code>를 대입한다.</p> <p></p> <p> 그림 11-6   한 번의 삽입 연산 </p>","path":["제 11 장   정렬","11.4   삽입 정렬"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1141","level":2,"title":"11.4.1   알고리즘 과정","text":"<p>삽입 정렬의 전체 과정은 아래 그림(11-7)과 같다.</p> <ol> <li>처음에는 배열의 첫 번째 원소가 정렬되어 있다.</li> <li>배열의 두 번째 원소를 <code>base</code>로 골라 올바른 위치에 삽입하면 배열의 앞 2개 원소가 정렬된다.</li> <li>세 번째 원소를 <code>base</code>로 골라 올바른 위치에 삽입하면 배열의 앞 3개 원소가 정렬된다.</li> <li>같은 과정을 반복한다. 마지막 회에서 마지막 원소를 <code>base</code>로 골라 올바른 위치에 삽입하면 모든 원소가 정렬된다.</li> </ol> <p></p> <p> 그림 11-7   삽입 정렬 과정 </p> <p>예시 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby insertion_sort.py<pre><code>def insertion_sort(nums: list[int]):\n \"\"\"삽입 정렬\"\"\"\n # 바깥쪽 반복문: 정렬된 구간은 [0, i-1]이다\n for i in range(1, len(nums)):\n base = nums[i]\n j = i - 1\n # 안쪽 반복문: base를 정렬된 구간 [0, i-1]의 올바른 위치에 삽입한다\n while j &gt;= 0 and nums[j] &gt; base:\n nums[j + 1] = nums[j] # nums[j]를 오른쪽으로 한 칸 이동한다\n j -= 1\n nums[j + 1] = base # 올바른 위치에 base를 대입한다\n</code></pre> insertion_sort.cpp<pre><code>/* 삽입 정렬 */\nvoid insertionSort(vector&lt;int&gt; &amp;nums) {\n // 바깥쪽 반복문: 정렬된 구간은 [0, i-1]이다\n for (int i = 1; i &lt; nums.size(); i++) {\n int base = nums[i], j = i - 1;\n // 안쪽 반복문: base를 정렬된 구간 [0, i-1]의 올바른 위치에 삽입한다\n while (j &gt;= 0 &amp;&amp; nums[j] &gt; base) {\n nums[j + 1] = nums[j]; // nums[j]를 오른쪽으로 한 칸 이동한다\n j--;\n }\n nums[j + 1] = base; // 올바른 위치에 base를 대입한다\n }\n}\n</code></pre> insertion_sort.java<pre><code>/* 삽입 정렬 */\nvoid insertionSort(int[] nums) {\n // 바깥쪽 반복문: 정렬된 구간은 [0, i-1]이다\n for (int i = 1; i &lt; nums.length; i++) {\n int base = nums[i], j = i - 1;\n // 안쪽 반복문: base를 정렬된 구간 [0, i-1]의 올바른 위치에 삽입한다\n while (j &gt;= 0 &amp;&amp; nums[j] &gt; base) {\n nums[j + 1] = nums[j]; // nums[j]를 오른쪽으로 한 칸 이동한다\n j--;\n }\n nums[j + 1] = base; // 올바른 위치에 base를 대입한다\n }\n}\n</code></pre> insertion_sort.cs<pre><code>/* 삽입 정렬 */\nvoid InsertionSort(int[] nums) {\n // 바깥쪽 반복문: 정렬된 구간은 [0, i-1]이다\n for (int i = 1; i &lt; nums.Length; i++) {\n int bas = nums[i], j = i - 1;\n // 안쪽 반복문: base를 정렬된 구간 [0, i-1]의 올바른 위치에 삽입한다\n while (j &gt;= 0 &amp;&amp; nums[j] &gt; bas) {\n nums[j + 1] = nums[j]; // nums[j]를 오른쪽으로 한 칸 이동한다\n j--;\n }\n nums[j + 1] = bas; // 올바른 위치에 base를 대입한다\n }\n}\n</code></pre> insertion_sort.go<pre><code>/* 삽입 정렬 */\nfunc insertionSort(nums []int) {\n // 바깥쪽 반복문: 정렬된 구간은 [0, i-1]이다\n for i := 1; i &lt; len(nums); i++ {\n base := nums[i]\n j := i - 1\n // 안쪽 반복문: base를 정렬된 구간 [0, i-1]의 올바른 위치에 삽입한다\n for j &gt;= 0 &amp;&amp; nums[j] &gt; base {\n nums[j+1] = nums[j] // nums[j]를 오른쪽으로 한 칸 이동한다\n j--\n }\n nums[j+1] = base // 올바른 위치에 base를 대입한다\n }\n}\n</code></pre> insertion_sort.swift<pre><code>/* 삽입 정렬 */\nfunc insertionSort(nums: inout [Int]) {\n // 바깥쪽 반복문: 정렬된 구간은 [0, i-1]이다\n for i in nums.indices.dropFirst() {\n let base = nums[i]\n var j = i - 1\n // 안쪽 반복문: base를 정렬된 구간 [0, i-1]의 올바른 위치에 삽입한다\n while j &gt;= 0, nums[j] &gt; base {\n nums[j + 1] = nums[j] // nums[j]를 오른쪽으로 한 칸 이동한다\n j -= 1\n }\n nums[j + 1] = base // 올바른 위치에 base를 대입한다\n }\n}\n</code></pre> insertion_sort.js<pre><code>/* 삽입 정렬 */\nfunction insertionSort(nums) {\n // 바깥쪽 반복문: 정렬된 구간은 [0, i-1]이다\n for (let i = 1; i &lt; nums.length; i++) {\n let base = nums[i],\n j = i - 1;\n // 안쪽 반복문: base를 정렬된 구간 [0, i-1]의 올바른 위치에 삽입한다\n while (j &gt;= 0 &amp;&amp; nums[j] &gt; base) {\n nums[j + 1] = nums[j]; // nums[j]를 오른쪽으로 한 칸 이동한다\n j--;\n }\n nums[j + 1] = base; // 올바른 위치에 base를 대입한다\n }\n}\n</code></pre> insertion_sort.ts<pre><code>/* 삽입 정렬 */\nfunction insertionSort(nums: number[]): void {\n // 바깥쪽 반복문: 정렬된 구간은 [0, i-1]이다\n for (let i = 1; i &lt; nums.length; i++) {\n const base = nums[i];\n let j = i - 1;\n // 안쪽 반복문: base를 정렬된 구간 [0, i-1]의 올바른 위치에 삽입한다\n while (j &gt;= 0 &amp;&amp; nums[j] &gt; base) {\n nums[j + 1] = nums[j]; // nums[j]를 오른쪽으로 한 칸 이동한다\n j--;\n }\n nums[j + 1] = base; // 올바른 위치에 base를 대입한다\n }\n}\n</code></pre> insertion_sort.dart<pre><code>/* 삽입 정렬 */\nvoid insertionSort(List&lt;int&gt; nums) {\n // 바깥쪽 반복문: 정렬된 구간은 [0, i-1]이다\n for (int i = 1; i &lt; nums.length; i++) {\n int base = nums[i], j = i - 1;\n // 안쪽 반복문: base를 정렬된 구간 [0, i-1]의 올바른 위치에 삽입한다\n while (j &gt;= 0 &amp;&amp; nums[j] &gt; base) {\n nums[j + 1] = nums[j]; // nums[j]를 오른쪽으로 한 칸 이동한다\n j--;\n }\n nums[j + 1] = base; // 올바른 위치에 base를 대입한다\n }\n}\n</code></pre> insertion_sort.rs<pre><code>/* 삽입 정렬 */\nfn insertion_sort(nums: &amp;mut [i32]) {\n // 바깥쪽 반복문: 정렬된 구간은 [0, i-1]이다\n for i in 1..nums.len() {\n let (base, mut j) = (nums[i], (i - 1) as i32);\n // 안쪽 반복문: base를 정렬된 구간 [0, i-1]의 올바른 위치에 삽입한다\n while j &gt;= 0 &amp;&amp; nums[j as usize] &gt; base {\n nums[(j + 1) as usize] = nums[j as usize]; // nums[j]를 오른쪽으로 한 칸 이동한다\n j -= 1;\n }\n nums[(j + 1) as usize] = base; // 올바른 위치에 base를 대입한다\n }\n}\n</code></pre> insertion_sort.c<pre><code>/* 삽입 정렬 */\nvoid insertionSort(int nums[], int size) {\n // 바깥쪽 반복문: 정렬된 구간은 [0, i-1]이다\n for (int i = 1; i &lt; size; i++) {\n int base = nums[i], j = i - 1;\n // 안쪽 반복문: base를 정렬된 구간 [0, i-1]의 올바른 위치에 삽입한다\n while (j &gt;= 0 &amp;&amp; nums[j] &gt; base) {\n // nums[j]를 오른쪽으로 한 칸 이동한다\n nums[j + 1] = nums[j];\n j--;\n }\n // 올바른 위치에 base를 대입한다\n nums[j + 1] = base;\n }\n}\n</code></pre> insertion_sort.kt<pre><code>/* 삽입 정렬 */\nfun insertionSort(nums: IntArray) {\n //바깥쪽 반복문: 정렬된 원소의 수는 1, 2, ..., n이 된다\n for (i in nums.indices) {\n val base = nums[i]\n var j = i - 1\n // 안쪽 반복문: base를 정렬된 구간 [0, i-1]의 올바른 위치에 삽입한다\n while (j &gt;= 0 &amp;&amp; nums[j] &gt; base) {\n nums[j + 1] = nums[j] // nums[j]를 오른쪽으로 한 칸 이동한다\n j--\n }\n nums[j + 1] = base // 올바른 위치에 base를 대입한다\n }\n}\n</code></pre> insertion_sort.rb<pre><code>### 삽입 정렬 ###\ndef insertion_sort(nums)\n n = nums.length\n # 바깥쪽 반복문: 정렬된 구간은 [0, i-1]이다\n for i in 1...n\n base = nums[i]\n j = i - 1\n # 안쪽 반복문: base를 정렬된 구간 [0, i-1]의 올바른 위치에 삽입한다\n while j &gt;= 0 &amp;&amp; nums[j] &gt; base\n nums[j + 1] = nums[j] # nums[j]를 오른쪽으로 한 칸 이동한다\n j -= 1\n end\n nums[j + 1] = base # 올바른 위치에 base를 대입한다\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 11 장   정렬","11.4   삽입 정렬"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1142","level":2,"title":"11.4.2   알고리즘 특성","text":"<ul> <li>시간 복잡도 \\(O(n^2)\\), 적응형 정렬: 최악의 경우 각 삽입 연산에서 \\(n - 1\\), \\(n-2\\), \\(\\dots\\), \\(2\\), \\(1\\) 번 반복해야 한다. 합은 \\((n - 1) n / 2\\)이므로 시간 복잡도는 \\(O(n^2)\\)이다. 정렬된 데이터를 만나면 삽입 연산이 일찍 끝난다. 입력 배열이 완전히 정렬되어 있으면 최선 시간 복잡도 \\(O(n)\\)에 도달한다.</li> <li>공간 복잡도 \\(O(1)\\), 제자리 정렬: 포인터 \\(i\\)와 \\(j\\)는 상수 크기의 추가 공간을 사용한다.</li> <li>안정 정렬: 삽입할 때 값이 같은 원소의 오른쪽에 삽입하므로 상대적 순서가 바뀌지 않는다.</li> </ul>","path":["제 11 장   정렬","11.4   삽입 정렬"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1143","level":2,"title":"11.4.3   삽입 정렬의 장점","text":"<p>삽입 정렬의 시간 복잡도는 \\(O(n^2)\\)이고, 곧 배울 퀵 정렬의 시간 복잡도는 \\(O(n \\log n)\\)이다. 삽입 정렬의 시간 복잡도가 더 높지만 데이터가 적으면 보통 삽입 정렬이 더 빠르다.</p> <p>이는 선형 탐색과 이진 탐색의 적합한 사용 상황에서 내린 결론과 비슷하다. 퀵 정렬 같은 \\(O(n \\log n)\\) 알고리즘은 분할 정복 전략에 기반한 정렬 알고리즘으로, 기본 연산이 더 많은 경우가 많다. 데이터가 적으면 \\(n^2\\)과 \\(n \\log n\\)의 값이 비슷하여 시간 복잡도가 결정적인 요인이 되지 못하고, 매 반복에서 수행하는 기본 연산 수가 결정적인 역할을 한다.</p> <p>실제로 Java 등 여러 프로그래밍 언어의 내장 정렬 함수는 삽입 정렬을 활용한다. 대체로 긴 배열에는 퀵 정렬 같은 분할 정복 기반 정렬을 사용하고, 짧은 배열에는 삽입 정렬을 바로 적용한다.</p> <p>버블 정렬, 선택 정렬, 삽입 정렬의 시간 복잡도는 모두 \\(O(n^2)\\)이지만, 실제로는 삽입 정렬이 버블 정렬이나 선택 정렬보다 훨씬 자주 사용된다. 주된 이유는 다음과 같다.</p> <ul> <li>버블 정렬은 원소 교환으로 구현하므로 임시 변수가 필요하고 총 3회의 기본 연산을 수행한다. 삽입 정렬은 원소 대입으로 구현하므로 기본 연산이 1회만 필요하다. 따라서 버블 정렬의 계산 비용은 보통 삽입 정렬보다 크다.</li> <li>선택 정렬의 시간 복잡도는 어떤 경우에도 \\(O(n^2)\\)이다. 데이터가 부분적으로 정렬되어 있다면 삽입 정렬이 보통 선택 정렬보다 효율적이다.</li> <li>선택 정렬은 불안정하므로 여러 기준으로 순차 정렬하는 데 사용할 수 없다.</li> </ul>","path":["제 11 장   정렬","11.4   삽입 정렬"],"tags":[]},{"location":"chapter_sorting/merge_sort/","level":1,"title":"11.6   병합 정렬","text":"<p>병합 정렬(merge sort)은 분할 정복 전략에 기반한 정렬 알고리즘으로, 아래 그림(11-10)의 '분할' 단계와 '병합' 단계로 이루어진다.</p> <ol> <li>분할 단계: 재귀적으로 배열을 계속 중간에서 나누어, 긴 배열을 정렬하는 문제를 짧은 배열을 정렬하는 문제로 바꾼다.</li> <li>병합 단계: 부분 배열의 길이가 1이 되면 분할을 멈추고 병합을 시작한다. 정렬된 짧은 왼쪽 배열과 오른쪽 배열을 하나의 더 긴 정렬된 배열로 계속 합치며, 끝날 때까지 반복한다.</li> </ol> <p></p> <p> 그림 11-10   병합 정렬의 분할 단계와 병합 단계 </p>","path":["제 11 장   정렬","11.6   병합 정렬"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1161","level":2,"title":"11.6.1   알고리즘 과정","text":"<p>아래 그림(11-11)과 같이 '분할 단계'에서는 위에서 아래로 재귀하며 배열을 중간에서 두 부분 배열로 나눈다.</p> <ol> <li>배열의 중간 인덱스 <code>mid</code>를 계산하고 왼쪽 부분 배열(구간 <code>[left, mid]</code>)과 오른쪽 부분 배열(구간 <code>[mid + 1, right]</code>)을 재귀적으로 나눈다.</li> <li>부분 배열 구간의 길이가 1이 될 때까지 <code>1.</code> 단계를 재귀적으로 반복한다.</li> </ol> <p>'병합 단계'에서는 아래에서 위로 왼쪽 부분 배열과 오른쪽 부분 배열을 하나의 정렬된 배열로 합친다. 길이가 1인 부분 배열부터 병합하므로, 병합 단계의 각 부분 배열은 이미 정렬되어 있다.</p> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt;&lt;6&gt;&lt;7&gt;&lt;8&gt;&lt;9&gt;&lt;10&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 11-11   병합 정렬 단계 </p> <p>병합 정렬의 재귀 순서는 이진 트리의 후위 순회와 같다는 것을 알 수 있다.</p> <ul> <li>후위 순회: 왼쪽 서브트리에 재귀한 다음 오른쪽 서브트리에 재귀하고, 마지막으로 루트 노드를 처리한다.</li> <li>병합 정렬: 왼쪽 부분 배열에 재귀한 다음 오른쪽 부분 배열에 재귀하고, 마지막으로 병합한다.</li> </ul> <p>병합 정렬의 구현은 다음 코드와 같다. <code>nums</code>에서 병합할 구간은 <code>[left, right]</code>이고, 이에 대응하는 <code>tmp</code>의 구간은 <code>[0, right - left]</code>라는 점에 주의하자.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby merge_sort.py<pre><code>def merge(nums: list[int], left: int, mid: int, right: int):\n \"\"\"왼쪽 부분 배열과 오른쪽 부분 배열 병합\"\"\"\n # 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다\n # 병합 결과를 저장할 임시 배열 tmp를 생성한다\n tmp = [0] * (right - left + 1)\n # 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다\n i, j, k = left, mid + 1, 0\n # 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다\n while i &lt;= mid and j &lt;= right:\n if nums[i] &lt;= nums[j]:\n tmp[k] = nums[i]\n i += 1\n else:\n tmp[k] = nums[j]\n j += 1\n k += 1\n # 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다\n while i &lt;= mid:\n tmp[k] = nums[i]\n i += 1\n k += 1\n while j &lt;= right:\n tmp[k] = nums[j]\n j += 1\n k += 1\n # 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다\n for k in range(0, len(tmp)):\n nums[left + k] = tmp[k]\n\ndef merge_sort(nums: list[int], left: int, right: int):\n \"\"\"병합 정렬\"\"\"\n # 종료 조건\n if left &gt;= right:\n return # 부분 배열의 길이가 1이면 재귀를 종료한다\n # 분할 단계\n mid = (left + right) // 2 # 중간 위치를 계산한다\n merge_sort(nums, left, mid) # 왼쪽 부분 배열에 재귀 호출한다\n merge_sort(nums, mid + 1, right) # 오른쪽 부분 배열에 재귀 호출한다\n # 병합 단계\n merge(nums, left, mid, right)\n</code></pre> merge_sort.cpp<pre><code>/* 왼쪽 부분 배열과 오른쪽 부분 배열 병합 */\nvoid merge(vector&lt;int&gt; &amp;nums, int left, int mid, int right) {\n // 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다\n // 병합 결과를 저장할 임시 배열 tmp를 생성한다\n vector&lt;int&gt; tmp(right - left + 1);\n // 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다\n int i = left, j = mid + 1, k = 0;\n // 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다\n while (i &lt;= mid &amp;&amp; j &lt;= right) {\n if (nums[i] &lt;= nums[j])\n tmp[k++] = nums[i++];\n else\n tmp[k++] = nums[j++];\n }\n // 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다\n while (i &lt;= mid) {\n tmp[k++] = nums[i++];\n }\n while (j &lt;= right) {\n tmp[k++] = nums[j++];\n }\n // 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다\n for (k = 0; k &lt; tmp.size(); k++) {\n nums[left + k] = tmp[k];\n }\n}\n\n/* 병합 정렬 */\nvoid mergeSort(vector&lt;int&gt; &amp;nums, int left, int right) {\n // 종료 조건\n if (left &gt;= right)\n return; // 부분 배열의 길이가 1이면 재귀를 종료한다\n // 분할 단계\n int mid = left + (right - left) / 2; // 중간 위치를 계산한다\n mergeSort(nums, left, mid); // 왼쪽 부분 배열에 재귀 호출한다\n mergeSort(nums, mid + 1, right); // 오른쪽 부분 배열에 재귀 호출한다\n // 병합 단계\n merge(nums, left, mid, right);\n}\n</code></pre> merge_sort.java<pre><code>/* 왼쪽 부분 배열과 오른쪽 부분 배열 병합 */\nvoid merge(int[] nums, int left, int mid, int right) {\n // 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다\n // 병합 결과를 저장할 임시 배열 tmp를 생성한다\n int[] tmp = new int[right - left + 1];\n // 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다\n int i = left, j = mid + 1, k = 0;\n // 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다\n while (i &lt;= mid &amp;&amp; j &lt;= right) {\n if (nums[i] &lt;= nums[j])\n tmp[k++] = nums[i++];\n else\n tmp[k++] = nums[j++];\n }\n // 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다\n while (i &lt;= mid) {\n tmp[k++] = nums[i++];\n }\n while (j &lt;= right) {\n tmp[k++] = nums[j++];\n }\n // 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다\n for (k = 0; k &lt; tmp.length; k++) {\n nums[left + k] = tmp[k];\n }\n}\n\n/* 병합 정렬 */\nvoid mergeSort(int[] nums, int left, int right) {\n // 종료 조건\n if (left &gt;= right)\n return; // 부분 배열의 길이가 1이면 재귀를 종료한다\n // 분할 단계\n int mid = left + (right - left) / 2; // 중간 위치를 계산한다\n mergeSort(nums, left, mid); // 왼쪽 부분 배열에 재귀 호출한다\n mergeSort(nums, mid + 1, right); // 오른쪽 부분 배열에 재귀 호출한다\n // 병합 단계\n merge(nums, left, mid, right);\n}\n</code></pre> merge_sort.cs<pre><code>/* 왼쪽 부분 배열과 오른쪽 부분 배열 병합 */\nvoid Merge(int[] nums, int left, int mid, int right) {\n // 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다\n // 병합 결과를 저장할 임시 배열 tmp를 생성한다\n int[] tmp = new int[right - left + 1];\n // 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다\n int i = left, j = mid + 1, k = 0;\n // 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다\n while (i &lt;= mid &amp;&amp; j &lt;= right) {\n if (nums[i] &lt;= nums[j])\n tmp[k++] = nums[i++];\n else\n tmp[k++] = nums[j++];\n }\n // 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다\n while (i &lt;= mid) {\n tmp[k++] = nums[i++];\n }\n while (j &lt;= right) {\n tmp[k++] = nums[j++];\n }\n // 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다\n for (k = 0; k &lt; tmp.Length; ++k) {\n nums[left + k] = tmp[k];\n }\n}\n\n/* 병합 정렬 */\nvoid MergeSort(int[] nums, int left, int right) {\n // 종료 조건\n if (left &gt;= right) return; // 부분 배열의 길이가 1이면 재귀를 종료한다\n // 분할 단계\n int mid = left + (right - left) / 2; // 중간 위치를 계산한다\n MergeSort(nums, left, mid); // 왼쪽 부분 배열에 재귀 호출한다\n MergeSort(nums, mid + 1, right); // 오른쪽 부분 배열에 재귀 호출한다\n // 병합 단계\n Merge(nums, left, mid, right);\n}\n</code></pre> merge_sort.go<pre><code>/* 왼쪽 부분 배열과 오른쪽 부분 배열 병합 */\nfunc merge(nums []int, left, mid, right int) {\n // 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다\n // 병합 결과를 저장할 임시 배열 tmp를 생성한다\n tmp := make([]int, right-left+1)\n // 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다\n i, j, k := left, mid+1, 0\n // 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다\n for i &lt;= mid &amp;&amp; j &lt;= right {\n if nums[i] &lt;= nums[j] {\n tmp[k] = nums[i]\n i++\n } else {\n tmp[k] = nums[j]\n j++\n }\n k++\n }\n // 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다\n for i &lt;= mid {\n tmp[k] = nums[i]\n i++\n k++\n }\n for j &lt;= right {\n tmp[k] = nums[j]\n j++\n k++\n }\n // 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다\n for k := 0; k &lt; len(tmp); k++ {\n nums[left+k] = tmp[k]\n }\n}\n\n/* 병합 정렬 */\nfunc mergeSort(nums []int, left, right int) {\n // 종료 조건\n if left &gt;= right {\n return\n }\n // 분할 단계\n mid := left + (right - left) / 2\n mergeSort(nums, left, mid)\n mergeSort(nums, mid+1, right)\n // 병합 단계\n merge(nums, left, mid, right)\n}\n</code></pre> merge_sort.swift<pre><code>/* 왼쪽 부분 배열과 오른쪽 부분 배열 병합 */\nfunc merge(nums: inout [Int], left: Int, mid: Int, right: Int) {\n // 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다\n // 병합 결과를 저장할 임시 배열 tmp를 생성한다\n var tmp = Array(repeating: 0, count: right - left + 1)\n // 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다\n var i = left, j = mid + 1, k = 0\n // 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다\n while i &lt;= mid, j &lt;= right {\n if nums[i] &lt;= nums[j] {\n tmp[k] = nums[i]\n i += 1\n } else {\n tmp[k] = nums[j]\n j += 1\n }\n k += 1\n }\n // 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다\n while i &lt;= mid {\n tmp[k] = nums[i]\n i += 1\n k += 1\n }\n while j &lt;= right {\n tmp[k] = nums[j]\n j += 1\n k += 1\n }\n // 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다\n for k in tmp.indices {\n nums[left + k] = tmp[k]\n }\n}\n\n/* 병합 정렬 */\nfunc mergeSort(nums: inout [Int], left: Int, right: Int) {\n // 종료 조건\n if left &gt;= right { // 부분 배열의 길이가 1이면 재귀를 종료한다\n return\n }\n // 분할 단계\n let mid = left + (right - left) / 2 // 중간 위치를 계산한다\n mergeSort(nums: &amp;nums, left: left, right: mid) // 왼쪽 부분 배열에 재귀 호출한다\n mergeSort(nums: &amp;nums, left: mid + 1, right: right) // 오른쪽 부분 배열에 재귀 호출한다\n // 병합 단계\n merge(nums: &amp;nums, left: left, mid: mid, right: right)\n}\n</code></pre> merge_sort.js<pre><code>/* 왼쪽 부분 배열과 오른쪽 부분 배열 병합 */\nfunction merge(nums, left, mid, right) {\n // 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다\n // 병합 결과를 저장할 임시 배열 tmp를 생성한다\n const tmp = new Array(right - left + 1);\n // 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다\n let i = left,\n j = mid + 1,\n k = 0;\n // 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다\n while (i &lt;= mid &amp;&amp; j &lt;= right) {\n if (nums[i] &lt;= nums[j]) {\n tmp[k++] = nums[i++];\n } else {\n tmp[k++] = nums[j++];\n }\n }\n // 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다\n while (i &lt;= mid) {\n tmp[k++] = nums[i++];\n }\n while (j &lt;= right) {\n tmp[k++] = nums[j++];\n }\n // 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다\n for (k = 0; k &lt; tmp.length; k++) {\n nums[left + k] = tmp[k];\n }\n}\n\n/* 병합 정렬 */\nfunction mergeSort(nums, left, right) {\n // 종료 조건\n if (left &gt;= right) return; // 부분 배열의 길이가 1이면 재귀를 종료한다\n // 분할 단계\n let mid = Math.floor(left + (right - left) / 2); // 중간 위치를 계산한다\n mergeSort(nums, left, mid); // 왼쪽 부분 배열에 재귀 호출한다\n mergeSort(nums, mid + 1, right); // 오른쪽 부분 배열에 재귀 호출한다\n // 병합 단계\n merge(nums, left, mid, right);\n}\n</code></pre> merge_sort.ts<pre><code>/* 왼쪽 부분 배열과 오른쪽 부분 배열 병합 */\nfunction merge(nums: number[], left: number, mid: number, right: number): void {\n // 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다\n // 병합 결과를 저장할 임시 배열 tmp를 생성한다\n const tmp = new Array(right - left + 1);\n // 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다\n let i = left,\n j = mid + 1,\n k = 0;\n // 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다\n while (i &lt;= mid &amp;&amp; j &lt;= right) {\n if (nums[i] &lt;= nums[j]) {\n tmp[k++] = nums[i++];\n } else {\n tmp[k++] = nums[j++];\n }\n }\n // 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다\n while (i &lt;= mid) {\n tmp[k++] = nums[i++];\n }\n while (j &lt;= right) {\n tmp[k++] = nums[j++];\n }\n // 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다\n for (k = 0; k &lt; tmp.length; k++) {\n nums[left + k] = tmp[k];\n }\n}\n\n/* 병합 정렬 */\nfunction mergeSort(nums: number[], left: number, right: number): void {\n // 종료 조건\n if (left &gt;= right) return; // 부분 배열의 길이가 1이면 재귀를 종료한다\n // 분할 단계\n let mid = Math.floor(left + (right - left) / 2); // 중간 위치를 계산한다\n mergeSort(nums, left, mid); // 왼쪽 부분 배열에 재귀 호출한다\n mergeSort(nums, mid + 1, right); // 오른쪽 부분 배열에 재귀 호출한다\n // 병합 단계\n merge(nums, left, mid, right);\n}\n</code></pre> merge_sort.dart<pre><code>/* 왼쪽 부분 배열과 오른쪽 부분 배열 병합 */\nvoid merge(List&lt;int&gt; nums, int left, int mid, int right) {\n // 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다\n // 병합 결과를 저장할 임시 배열 tmp를 생성한다\n List&lt;int&gt; tmp = List.filled(right - left + 1, 0);\n // 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다\n int i = left, j = mid + 1, k = 0;\n // 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다\n while (i &lt;= mid &amp;&amp; j &lt;= right) {\n if (nums[i] &lt;= nums[j])\n tmp[k++] = nums[i++];\n else\n tmp[k++] = nums[j++];\n }\n // 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다\n while (i &lt;= mid) {\n tmp[k++] = nums[i++];\n }\n while (j &lt;= right) {\n tmp[k++] = nums[j++];\n }\n // 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다\n for (k = 0; k &lt; tmp.length; k++) {\n nums[left + k] = tmp[k];\n }\n}\n\n/* 병합 정렬 */\nvoid mergeSort(List&lt;int&gt; nums, int left, int right) {\n // 종료 조건\n if (left &gt;= right) return; // 부분 배열의 길이가 1이면 재귀를 종료한다\n // 분할 단계\n int mid = left + (right - left) ~/ 2; // 중간 위치를 계산한다\n mergeSort(nums, left, mid); // 왼쪽 부분 배열에 재귀 호출한다\n mergeSort(nums, mid + 1, right); // 오른쪽 부분 배열에 재귀 호출한다\n // 병합 단계\n merge(nums, left, mid, right);\n}\n</code></pre> merge_sort.rs<pre><code>/* 왼쪽 부분 배열과 오른쪽 부분 배열 병합 */\nfn merge(nums: &amp;mut [i32], left: usize, mid: usize, right: usize) {\n // 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다\n // 병합 결과를 저장할 임시 배열 tmp를 생성한다\n let tmp_size = right - left + 1;\n let mut tmp = vec![0; tmp_size];\n // 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다\n let (mut i, mut j, mut k) = (left, mid + 1, 0);\n // 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다\n while i &lt;= mid &amp;&amp; j &lt;= right {\n if nums[i] &lt;= nums[j] {\n tmp[k] = nums[i];\n i += 1;\n } else {\n tmp[k] = nums[j];\n j += 1;\n }\n k += 1;\n }\n // 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다\n while i &lt;= mid {\n tmp[k] = nums[i];\n k += 1;\n i += 1;\n }\n while j &lt;= right {\n tmp[k] = nums[j];\n k += 1;\n j += 1;\n }\n // 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다\n for k in 0..tmp_size {\n nums[left + k] = tmp[k];\n }\n}\n\n/* 병합 정렬 */\nfn merge_sort(nums: &amp;mut [i32], left: usize, right: usize) {\n // 종료 조건\n if left &gt;= right {\n return; // 부분 배열의 길이가 1이면 재귀를 종료한다\n }\n\n // 분할 단계\n let mid = left + (right - left) / 2; // 중간 위치를 계산한다\n merge_sort(nums, left, mid); // 왼쪽 부분 배열에 재귀 호출한다\n merge_sort(nums, mid + 1, right); // 오른쪽 부분 배열에 재귀 호출한다\n\n // 병합 단계\n merge(nums, left, mid, right);\n}\n</code></pre> merge_sort.c<pre><code>/* 왼쪽 부분 배열과 오른쪽 부분 배열 병합 */\nvoid merge(int *nums, int left, int mid, int right) {\n // 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다\n // 병합 결과를 저장할 임시 배열 tmp를 생성한다\n int tmpSize = right - left + 1;\n int *tmp = (int *)malloc(tmpSize * sizeof(int));\n // 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다\n int i = left, j = mid + 1, k = 0;\n // 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다\n while (i &lt;= mid &amp;&amp; j &lt;= right) {\n if (nums[i] &lt;= nums[j]) {\n tmp[k++] = nums[i++];\n } else {\n tmp[k++] = nums[j++];\n }\n }\n // 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다\n while (i &lt;= mid) {\n tmp[k++] = nums[i++];\n }\n while (j &lt;= right) {\n tmp[k++] = nums[j++];\n }\n // 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다\n for (k = 0; k &lt; tmpSize; ++k) {\n nums[left + k] = tmp[k];\n }\n // 메모리 해제\n free(tmp);\n}\n\n/* 병합 정렬 */\nvoid mergeSort(int *nums, int left, int right) {\n // 종료 조건\n if (left &gt;= right)\n return; // 부분 배열의 길이가 1이면 재귀를 종료한다\n // 분할 단계\n int mid = left + (right - left) / 2; // 중간 위치를 계산한다\n mergeSort(nums, left, mid); // 왼쪽 부분 배열에 재귀 호출한다\n mergeSort(nums, mid + 1, right); // 오른쪽 부분 배열에 재귀 호출한다\n // 병합 단계\n merge(nums, left, mid, right);\n}\n</code></pre> merge_sort.kt<pre><code>/* 왼쪽 부분 배열과 오른쪽 부분 배열 병합 */\nfun merge(nums: IntArray, left: Int, mid: Int, right: Int) {\n // 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다\n // 병합 결과를 저장할 임시 배열 tmp를 생성한다\n val tmp = IntArray(right - left + 1)\n // 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다\n var i = left\n var j = mid + 1\n var k = 0\n // 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다\n while (i &lt;= mid &amp;&amp; j &lt;= right) {\n if (nums[i] &lt;= nums[j])\n tmp[k++] = nums[i++]\n else\n tmp[k++] = nums[j++]\n }\n // 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다\n while (i &lt;= mid) {\n tmp[k++] = nums[i++]\n }\n while (j &lt;= right) {\n tmp[k++] = nums[j++]\n }\n // 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다\n for (l in tmp.indices) {\n nums[left + l] = tmp[l]\n }\n}\n\n/* 병합 정렬 */\nfun mergeSort(nums: IntArray, left: Int, right: Int) {\n // 종료 조건\n if (left &gt;= right) return // 부분 배열의 길이가 1이면 재귀를 종료한다\n // 분할 단계\n val mid = left + (right - left) / 2 // 중간 위치를 계산한다\n mergeSort(nums, left, mid) // 왼쪽 부분 배열에 재귀 호출한다\n mergeSort(nums, mid + 1, right) // 오른쪽 부분 배열에 재귀 호출한다\n // 병합 단계\n merge(nums, left, mid, right)\n}\n</code></pre> merge_sort.rb<pre><code>### 왼쪽 부분 배열과 오른쪽 부분 배열 병합 ###\ndef merge(nums, left, mid, right)\n # 왼쪽 부분 배열의 구간은 [left, mid]이고, 오른쪽 부분 배열의 구간은 [mid+1, right]이다\n # 병합 결과를 저장할 임시 배열 tmp를 생성한다\n tmp = Array.new(right - left + 1, 0)\n # 왼쪽 부분 배열과 오른쪽 부분 배열의 시작 인덱스를 초기화한다\n i, j, k = left, mid + 1, 0\n # 양쪽 부분 배열 모두에 원소가 남아 있으면 비교하여 더 작은 원소를 임시 배열에 복사한다\n while i &lt;= mid &amp;&amp; j &lt;= right\n if nums[i] &lt;= nums[j]\n tmp[k] = nums[i]\n i += 1\n else\n tmp[k] = nums[j]\n j += 1\n end\n k += 1\n end\n # 왼쪽 부분 배열과 오른쪽 부분 배열의 남은 원소를 임시 배열에 복사한다\n while i &lt;= mid\n tmp[k] = nums[i]\n i += 1\n k += 1\n end\n while j &lt;= right\n tmp[k] = nums[j]\n j += 1\n k += 1\n end\n # 임시 배열 tmp의 원소를 원래 배열 nums의 해당 구간에 다시 복사한다\n (0...tmp.length).each do |k|\n nums[left + k] = tmp[k]\n end\nend\n\n### 병합 정렬 ###\ndef merge_sort(nums, left, right)\n # 종료 조건\n # 부분 배열의 길이가 1이면 재귀를 종료한다\n return if left &gt;= right\n # 분할 단계\n mid = left + (right - left) / 2 # 중간 위치를 계산한다\n merge_sort(nums, left, mid) # 왼쪽 부분 배열에 재귀 호출한다\n merge_sort(nums, mid + 1, right) # 오른쪽 부분 배열에 재귀 호출한다\n # 병합 단계\n merge(nums, left, mid, right)\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 11 장   정렬","11.6   병합 정렬"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1162","level":2,"title":"11.6.2   알고리즘 특성","text":"<ul> <li>시간 복잡도 \\(O(n \\log n)\\), 비적응형 정렬: 분할 과정에서 높이 \\(\\log n\\)의 재귀 트리가 만들어지고, 각 레벨에서 병합에 필요한 총연산 수는 \\(n\\)이므로 전체 시간 복잡도는 \\(O(n \\log n)\\)이다.</li> <li>공간 복잡도 \\(O(n)\\), 제자리 정렬이 아님: 재귀 깊이가 \\(\\log n\\)이므로 \\(O(\\log n)\\) 크기의 스택 프레임 공간을 사용한다. 병합에는 보조 배열이 필요하여 \\(O(n)\\)의 추가 공간을 사용한다.</li> <li>안정 정렬: 병합 과정에서 값이 같은 원소들의 순서가 유지된다.</li> </ul>","path":["제 11 장   정렬","11.6   병합 정렬"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1163","level":2,"title":"11.6.3   연결 리스트 정렬","text":"<p>연결 리스트에서는 병합 정렬이 다른 정렬 알고리즘보다 뚜렷한 장점을 가진다. 연결 리스트 정렬의 공간 복잡도를 \\(O(1)\\)까지 최적화할 수 있다.</p> <ul> <li>분할 단계: 연결 리스트를 나눌 때 '재귀' 대신 '반복'을 사용하면 재귀에 필요한 스택 프레임 공간을 없앨 수 있다.</li> <li>병합 단계: 연결 리스트에서는 참조(포인터)만 바꾸어 노드를 추가하거나 삭제할 수 있다. 따라서 정렬된 짧은 연결 리스트 두 개를 하나의 긴 연결 리스트로 합칠 때 추가 연결 리스트를 만들 필요가 없다.</li> </ul> <p>구체적인 구현은 다소 복잡하므로, 관심 있는 독자는 관련 자료를 찾아 학습해 보자.</p>","path":["제 11 장   정렬","11.6   병합 정렬"],"tags":[]},{"location":"chapter_sorting/quick_sort/","level":1,"title":"11.5   퀵 정렬","text":"<p>퀵 정렬(quick sort)은 분할 정복 전략에 기반한 정렬 알고리즘으로, 실행 효율이 높고 널리 사용된다.</p> <p>퀵 정렬의 핵심 연산은 '센티널 분할'이다. 배열의 한 원소를 '피벗'으로 고른 다음, 피벗보다 작은 원소는 모두 왼쪽으로, 큰 원소는 모두 오른쪽으로 옮기는 것이 목표다. 구체적인 과정은 아래 그림(11-8)과 같다.</p> <ol> <li>배열의 맨 왼쪽 원소를 피벗으로 고르고, 포인터 <code>i</code>와 <code>j</code>가 배열의 양 끝을 가리키도록 초기화한다.</li> <li>반복문을 실행하여 매번 <code>i</code>(<code>j</code>)로 피벗보다 큰(작은) 첫 번째 원소를 각각 찾은 뒤 두 원소를 교환한다.</li> <li><code>i</code>와 <code>j</code>가 만날 때까지 <code>2.</code> 단계를 반복한다. 마지막으로 피벗을 두 부분 배열의 경계로 옮긴다.</li> </ol> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt;&lt;6&gt;&lt;7&gt;&lt;8&gt;&lt;9&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 11-8   센티널 분할 단계 </p> <p>분할을 마치면 원래 배열은 왼쪽 부분 배열, 피벗, 오른쪽 부분 배열의 세 부분으로 나뉘며, '왼쪽 부분 배열의 모든 원소 \\(\\leq\\) 피벗 \\(\\leq\\) 오른쪽 부분 배열의 모든 원소'를 만족한다. 따라서 이후에는 두 부분 배열만 정렬하면 된다.</p> <p>퀵 정렬의 분할 정복 전략</p> <p>센티널 분할은 본질적으로 긴 배열 하나를 정렬하는 문제를 짧은 배열 두 개를 정렬하는 문제로 단순화한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py<pre><code>def partition(self, nums: list[int], left: int, right: int) -&gt; int:\n \"\"\"센티널 분할\"\"\"\n # nums[left]를 피벗으로 삼는다\n i, j = left, right\n while i &lt; j:\n while i &lt; j and nums[j] &gt;= nums[left]:\n j -= 1 # 오른쪽에서 왼쪽으로 피벗보다 작은 첫 번째 원소를 찾는다\n while i &lt; j and nums[i] &lt;= nums[left]:\n i += 1 # 왼쪽에서 오른쪽으로 피벗보다 큰 첫 번째 원소를 찾는다\n # 원소 교환\n nums[i], nums[j] = nums[j], nums[i]\n # 피벗을 두 부분 배열의 경계로 교환하여 옮긴다\n nums[i], nums[left] = nums[left], nums[i]\n return i # 피벗의 인덱스를 반환한다\n</code></pre> quick_sort.cpp<pre><code>/* 센티널 분할 */\nint partition(vector&lt;int&gt; &amp;nums, int left, int right) {\n // nums[left]를 피벗으로 삼는다\n int i = left, j = right;\n while (i &lt; j) {\n while (i &lt; j &amp;&amp; nums[j] &gt;= nums[left])\n j--; // 오른쪽에서 왼쪽으로 피벗보다 작은 첫 번째 원소를 찾는다\n while (i &lt; j &amp;&amp; nums[i] &lt;= nums[left])\n i++; // 왼쪽에서 오른쪽으로 피벗보다 큰 첫 번째 원소를 찾는다\n swap(nums[i], nums[j]); // 두 원소를 교환한다\n }\n swap(nums[i], nums[left]); // 피벗을 두 부분 배열의 경계로 교환하여 옮긴다\n return i; // 피벗의 인덱스를 반환한다\n}\n</code></pre> quick_sort.java<pre><code>/* 원소 교환 */\nvoid swap(int[] nums, int i, int j) {\n int tmp = nums[i];\n nums[i] = nums[j];\n nums[j] = tmp;\n}\n\n/* 센티널 분할 */\nint partition(int[] nums, int left, int right) {\n // nums[left]를 피벗으로 삼는다\n int i = left, j = right;\n while (i &lt; j) {\n while (i &lt; j &amp;&amp; nums[j] &gt;= nums[left])\n j--; // 오른쪽에서 왼쪽으로 피벗보다 작은 첫 번째 원소를 찾는다\n while (i &lt; j &amp;&amp; nums[i] &lt;= nums[left])\n i++; // 왼쪽에서 오른쪽으로 피벗보다 큰 첫 번째 원소를 찾는다\n swap(nums, i, j); // 두 원소를 교환한다\n }\n swap(nums, i, left); // 피벗을 두 부분 배열의 경계로 교환하여 옮긴다\n return i; // 피벗의 인덱스를 반환한다\n}\n</code></pre> quick_sort.cs<pre><code>/* 원소 교환 */\nvoid Swap(int[] nums, int i, int j) {\n (nums[j], nums[i]) = (nums[i], nums[j]);\n}\n\n/* 센티널 분할 */\nint Partition(int[] nums, int left, int right) {\n // nums[left]를 피벗으로 삼는다\n int i = left, j = right;\n while (i &lt; j) {\n while (i &lt; j &amp;&amp; nums[j] &gt;= nums[left])\n j--; // 오른쪽에서 왼쪽으로 피벗보다 작은 첫 번째 원소를 찾는다\n while (i &lt; j &amp;&amp; nums[i] &lt;= nums[left])\n i++; // 왼쪽에서 오른쪽으로 피벗보다 큰 첫 번째 원소를 찾는다\n Swap(nums, i, j); // 두 원소를 교환한다\n }\n Swap(nums, i, left); // 피벗을 두 부분 배열의 경계로 교환하여 옮긴다\n return i; // 피벗의 인덱스를 반환한다\n}\n</code></pre> quick_sort.go<pre><code>/* 센티널 분할 */\nfunc (q *quickSort) partition(nums []int, left, right int) int {\n // nums[left]를 피벗으로 삼는다\n i, j := left, right\n for i &lt; j {\n for i &lt; j &amp;&amp; nums[j] &gt;= nums[left] {\n j-- // 오른쪽에서 왼쪽으로 피벗보다 작은 첫 번째 원소를 찾는다\n }\n for i &lt; j &amp;&amp; nums[i] &lt;= nums[left] {\n i++ // 왼쪽에서 오른쪽으로 피벗보다 큰 첫 번째 원소를 찾는다\n }\n // 원소 교환\n nums[i], nums[j] = nums[j], nums[i]\n }\n // 피벗을 두 부분 배열의 경계로 교환하여 옮긴다\n nums[i], nums[left] = nums[left], nums[i]\n return i // 피벗의 인덱스를 반환한다\n}\n</code></pre> quick_sort.swift<pre><code>/* 센티널 분할 */\nfunc partition(nums: inout [Int], left: Int, right: Int) -&gt; Int {\n // nums[left]를 피벗으로 삼는다\n var i = left\n var j = right\n while i &lt; j {\n while i &lt; j, nums[j] &gt;= nums[left] {\n j -= 1 // 오른쪽에서 왼쪽으로 피벗보다 작은 첫 번째 원소를 찾는다\n }\n while i &lt; j, nums[i] &lt;= nums[left] {\n i += 1 // 왼쪽에서 오른쪽으로 피벗보다 큰 첫 번째 원소를 찾는다\n }\n nums.swapAt(i, j) // 두 원소를 교환한다\n }\n nums.swapAt(i, left) // 피벗을 두 부분 배열의 경계로 교환하여 옮긴다\n return i // 피벗의 인덱스를 반환한다\n}\n</code></pre> quick_sort.js<pre><code>/* 원소 교환 */\nswap(nums, i, j) {\n let tmp = nums[i];\n nums[i] = nums[j];\n nums[j] = tmp;\n}\n\n/* 센티널 분할 */\npartition(nums, left, right) {\n // nums[left]를 피벗으로 삼는다\n let i = left,\n j = right;\n while (i &lt; j) {\n while (i &lt; j &amp;&amp; nums[j] &gt;= nums[left]) {\n j -= 1; // 오른쪽에서 왼쪽으로 피벗보다 작은 첫 번째 원소를 찾는다\n }\n while (i &lt; j &amp;&amp; nums[i] &lt;= nums[left]) {\n i += 1; // 왼쪽에서 오른쪽으로 피벗보다 큰 첫 번째 원소를 찾는다\n }\n // 원소 교환\n this.swap(nums, i, j); // 두 원소를 교환한다\n }\n this.swap(nums, i, left); // 피벗을 두 부분 배열의 경계로 교환하여 옮긴다\n return i; // 피벗의 인덱스를 반환한다\n}\n</code></pre> quick_sort.ts<pre><code>/* 원소 교환 */\nswap(nums: number[], i: number, j: number): void {\n let tmp = nums[i];\n nums[i] = nums[j];\n nums[j] = tmp;\n}\n\n/* 센티널 분할 */\npartition(nums: number[], left: number, right: number): number {\n // nums[left]를 피벗으로 삼는다\n let i = left,\n j = right;\n while (i &lt; j) {\n while (i &lt; j &amp;&amp; nums[j] &gt;= nums[left]) {\n j -= 1; // 오른쪽에서 왼쪽으로 피벗보다 작은 첫 번째 원소를 찾는다\n }\n while (i &lt; j &amp;&amp; nums[i] &lt;= nums[left]) {\n i += 1; // 왼쪽에서 오른쪽으로 피벗보다 큰 첫 번째 원소를 찾는다\n }\n // 원소 교환\n this.swap(nums, i, j); // 두 원소를 교환한다\n }\n this.swap(nums, i, left); // 피벗을 두 부분 배열의 경계로 교환하여 옮긴다\n return i; // 피벗의 인덱스를 반환한다\n}\n</code></pre> quick_sort.dart<pre><code>/* 원소 교환 */\nvoid _swap(List&lt;int&gt; nums, int i, int j) {\n int tmp = nums[i];\n nums[i] = nums[j];\n nums[j] = tmp;\n}\n\n/* 센티널 분할 */\nint _partition(List&lt;int&gt; nums, int left, int right) {\n // nums[left]를 피벗으로 삼는다\n int i = left, j = right;\n while (i &lt; j) {\n while (i &lt; j &amp;&amp; nums[j] &gt;= nums[left]) j--; // 오른쪽에서 왼쪽으로 피벗보다 작은 첫 번째 원소를 찾는다\n while (i &lt; j &amp;&amp; nums[i] &lt;= nums[left]) i++; // 왼쪽에서 오른쪽으로 피벗보다 큰 첫 번째 원소를 찾는다\n _swap(nums, i, j); // 두 원소를 교환한다\n }\n _swap(nums, i, left); // 피벗을 두 부분 배열의 경계로 교환하여 옮긴다\n return i; // 피벗의 인덱스를 반환한다\n}\n</code></pre> quick_sort.rs<pre><code>/* 센티널 분할 */\nfn partition(nums: &amp;mut [i32], left: usize, right: usize) -&gt; usize {\n // nums[left]를 피벗으로 삼는다\n let (mut i, mut j) = (left, right);\n while i &lt; j {\n while i &lt; j &amp;&amp; nums[j] &gt;= nums[left] {\n j -= 1; // 오른쪽에서 왼쪽으로 피벗보다 작은 첫 번째 원소를 찾는다\n }\n while i &lt; j &amp;&amp; nums[i] &lt;= nums[left] {\n i += 1; // 왼쪽에서 오른쪽으로 피벗보다 큰 첫 번째 원소를 찾는다\n }\n nums.swap(i, j); // 두 원소를 교환한다\n }\n nums.swap(i, left); // 피벗을 두 부분 배열의 경계로 교환하여 옮긴다\n i // 피벗의 인덱스를 반환한다\n}\n</code></pre> quick_sort.c<pre><code>/* 원소 교환 */\nvoid swap(int nums[], int i, int j) {\n int tmp = nums[i];\n nums[i] = nums[j];\n nums[j] = tmp;\n}\n\n/* 센티널 분할 */\nint partition(int nums[], int left, int right) {\n // nums[left]를 피벗으로 삼는다\n int i = left, j = right;\n while (i &lt; j) {\n while (i &lt; j &amp;&amp; nums[j] &gt;= nums[left]) {\n j--; // 오른쪽에서 왼쪽으로 피벗보다 작은 첫 번째 원소를 찾는다\n }\n while (i &lt; j &amp;&amp; nums[i] &lt;= nums[left]) {\n i++; // 왼쪽에서 오른쪽으로 피벗보다 큰 첫 번째 원소를 찾는다\n }\n // 두 원소를 교환한다\n swap(nums, i, j);\n }\n // 피벗을 두 부분 배열의 경계로 교환하여 옮긴다\n swap(nums, i, left);\n // 피벗의 인덱스를 반환한다\n return i;\n}\n</code></pre> quick_sort.kt<pre><code>/* 원소 교환 */\nfun swap(nums: IntArray, i: Int, j: Int) {\n val temp = nums[i]\n nums[i] = nums[j]\n nums[j] = temp\n}\n\n/* 센티널 분할 */\nfun partition(nums: IntArray, left: Int, right: Int): Int {\n // nums[left]를 피벗으로 삼는다\n var i = left\n var j = right\n while (i &lt; j) {\n while (i &lt; j &amp;&amp; nums[j] &gt;= nums[left])\n j-- // 오른쪽에서 왼쪽으로 피벗보다 작은 첫 번째 원소를 찾는다\n while (i &lt; j &amp;&amp; nums[i] &lt;= nums[left])\n i++ // 왼쪽에서 오른쪽으로 피벗보다 큰 첫 번째 원소를 찾는다\n swap(nums, i, j) // 두 원소를 교환한다\n }\n swap(nums, i, left) // 피벗을 두 부분 배열의 경계로 교환하여 옮긴다\n return i // 피벗의 인덱스를 반환한다\n}\n</code></pre> quick_sort.rb<pre><code>### 센티널 분할 ###\ndef partition(nums, left, right)\n # nums[left]를 피벗으로 삼는다\n i, j = left, right\n while i &lt; j\n while i &lt; j &amp;&amp; nums[j] &gt;= nums[left]\n j -= 1 # 오른쪽에서 왼쪽으로 피벗보다 작은 첫 번째 원소를 찾는다\n end\n while i &lt; j &amp;&amp; nums[i] &lt;= nums[left]\n i += 1 # 왼쪽에서 오른쪽으로 피벗보다 큰 첫 번째 원소를 찾는다\n end\n # 원소 교환\n nums[i], nums[j] = nums[j], nums[i]\n end\n # 피벗을 두 부분 배열의 경계로 교환하여 옮긴다\n nums[i], nums[left] = nums[left], nums[i]\n i # 피벗의 인덱스를 반환한다\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 11 장   정렬","11.5   퀵 정렬"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1151","level":2,"title":"11.5.1   알고리즘 과정","text":"<p>퀵 정렬의 전체 과정은 아래 그림(11-9)과 같다.</p> <ol> <li>먼저 원래 배열에 '센티널 분할'을 한 번 수행하여 정렬되지 않은 왼쪽 부분 배열과 오른쪽 부분 배열을 얻는다.</li> <li>이어서 왼쪽 부분 배열과 오른쪽 부분 배열에 각각 '센티널 분할'을 재귀적으로 수행한다.</li> <li>부분 배열의 길이가 1이 될 때까지 재귀를 계속하면 배열 전체의 정렬이 완료된다.</li> </ol> <p></p> <p> 그림 11-9   퀵 정렬 과정 </p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py<pre><code>def quick_sort(self, nums: list[int], left: int, right: int):\n \"\"\"퀵 정렬\"\"\"\n # 부분 배열의 길이가 1이면 재귀를 종료한다\n if left &gt;= right:\n return\n # 센티널 분할\n pivot = self.partition(nums, left, right)\n # 왼쪽 부분 배열과 오른쪽 부분 배열에 재귀 호출한다\n self.quick_sort(nums, left, pivot - 1)\n self.quick_sort(nums, pivot + 1, right)\n</code></pre> quick_sort.cpp<pre><code>/* 퀵 정렬 */\nvoid quickSort(vector&lt;int&gt; &amp;nums, int left, int right) {\n // 부분 배열의 길이가 1이면 재귀를 종료한다\n if (left &gt;= right)\n return;\n // 센티널 분할\n int pivot = partition(nums, left, right);\n // 왼쪽 부분 배열과 오른쪽 부분 배열에 재귀 호출한다\n quickSort(nums, left, pivot - 1);\n quickSort(nums, pivot + 1, right);\n}\n</code></pre> quick_sort.java<pre><code>/* 퀵 정렬 */\nvoid quickSort(int[] nums, int left, int right) {\n // 부분 배열의 길이가 1이면 재귀를 종료한다\n if (left &gt;= right)\n return;\n // 센티널 분할\n int pivot = partition(nums, left, right);\n // 왼쪽 부분 배열과 오른쪽 부분 배열에 재귀 호출한다\n quickSort(nums, left, pivot - 1);\n quickSort(nums, pivot + 1, right);\n}\n</code></pre> quick_sort.cs<pre><code>/* 퀵 정렬 */\nvoid QuickSort(int[] nums, int left, int right) {\n // 부분 배열의 길이가 1이면 재귀를 종료한다\n if (left &gt;= right)\n return;\n // 센티널 분할\n int pivot = Partition(nums, left, right);\n // 왼쪽 부분 배열과 오른쪽 부분 배열에 재귀 호출한다\n QuickSort(nums, left, pivot - 1);\n QuickSort(nums, pivot + 1, right);\n}\n</code></pre> quick_sort.go<pre><code>/* 퀵 정렬 */\nfunc (q *quickSort) quickSort(nums []int, left, right int) {\n // 부분 배열의 길이가 1이면 재귀를 종료한다\n if left &gt;= right {\n return\n }\n // 센티널 분할\n pivot := q.partition(nums, left, right)\n // 왼쪽 부분 배열과 오른쪽 부분 배열에 재귀 호출한다\n q.quickSort(nums, left, pivot-1)\n q.quickSort(nums, pivot+1, right)\n}\n</code></pre> quick_sort.swift<pre><code>/* 퀵 정렬 */\nfunc quickSort(nums: inout [Int], left: Int, right: Int) {\n // 부분 배열의 길이가 1이면 재귀를 종료한다\n if left &gt;= right {\n return\n }\n // 센티널 분할\n let pivot = partition(nums: &amp;nums, left: left, right: right)\n // 왼쪽 부분 배열과 오른쪽 부분 배열에 재귀 호출한다\n quickSort(nums: &amp;nums, left: left, right: pivot - 1)\n quickSort(nums: &amp;nums, left: pivot + 1, right: right)\n}\n</code></pre> quick_sort.js<pre><code>/* 퀵 정렬 */\nquickSort(nums, left, right) {\n // 부분 배열의 길이가 1이면 재귀를 종료한다\n if (left &gt;= right) return;\n // 센티널 분할\n const pivot = this.partition(nums, left, right);\n // 왼쪽 부분 배열과 오른쪽 부분 배열에 재귀 호출한다\n this.quickSort(nums, left, pivot - 1);\n this.quickSort(nums, pivot + 1, right);\n}\n</code></pre> quick_sort.ts<pre><code>/* 퀵 정렬 */\nquickSort(nums: number[], left: number, right: number): void {\n // 부분 배열의 길이가 1이면 재귀를 종료한다\n if (left &gt;= right) {\n return;\n }\n // 센티널 분할\n const pivot = this.partition(nums, left, right);\n // 왼쪽 부분 배열과 오른쪽 부분 배열에 재귀 호출한다\n this.quickSort(nums, left, pivot - 1);\n this.quickSort(nums, pivot + 1, right);\n}\n</code></pre> quick_sort.dart<pre><code>/* 퀵 정렬 */\nvoid quickSort(List&lt;int&gt; nums, int left, int right) {\n // 부분 배열의 길이가 1이면 재귀를 종료한다\n if (left &gt;= right) return;\n // 센티널 분할\n int pivot = _partition(nums, left, right);\n // 왼쪽 부분 배열과 오른쪽 부분 배열에 재귀 호출한다\n quickSort(nums, left, pivot - 1);\n quickSort(nums, pivot + 1, right);\n}\n</code></pre> quick_sort.rs<pre><code>/* 퀵 정렬 */\npub fn quick_sort(left: i32, right: i32, nums: &amp;mut [i32]) {\n // 부분 배열의 길이가 1이면 재귀를 종료한다\n if left &gt;= right {\n return;\n }\n // 센티널 분할\n let pivot = Self::partition(nums, left as usize, right as usize) as i32;\n // 왼쪽 부분 배열과 오른쪽 부분 배열에 재귀 호출한다\n Self::quick_sort(left, pivot - 1, nums);\n Self::quick_sort(pivot + 1, right, nums);\n}\n</code></pre> quick_sort.c<pre><code>/* 퀵 정렬 */\nvoid quickSort(int nums[], int left, int right) {\n // 부분 배열의 길이가 1이면 재귀를 종료한다\n if (left &gt;= right) {\n return;\n }\n // 센티널 분할\n int pivot = partition(nums, left, right);\n // 왼쪽 부분 배열과 오른쪽 부분 배열에 재귀 호출한다\n quickSort(nums, left, pivot - 1);\n quickSort(nums, pivot + 1, right);\n}\n</code></pre> quick_sort.kt<pre><code>/* 퀵 정렬 */\nfun quickSort(nums: IntArray, left: Int, right: Int) {\n // 부분 배열의 길이가 1이면 재귀를 종료한다\n if (left &gt;= right) return\n // 센티널 분할\n val pivot = partition(nums, left, right)\n // 왼쪽 부분 배열과 오른쪽 부분 배열에 재귀 호출한다\n quickSort(nums, left, pivot - 1)\n quickSort(nums, pivot + 1, right)\n}\n</code></pre> quick_sort.rb<pre><code>### 퀵 정렬 클래스 ###\ndef quick_sort(nums, left, right)\n # 부분 배열의 길이가 1이 아니면 재귀 호출한다\n if left &lt; right\n # 센티널 분할\n pivot = partition(nums, left, right)\n # 왼쪽 부분 배열과 오른쪽 부분 배열에 재귀 호출한다\n quick_sort(nums, left, pivot - 1)\n quick_sort(nums, pivot + 1, right)\n end\n nums\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 11 장   정렬","11.5   퀵 정렬"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1152","level":2,"title":"11.5.2   알고리즘 특성","text":"<ul> <li>시간 복잡도 \\(O(n \\log n)\\), 비적응형 정렬: 평균적으로 분할의 재귀 레벨 수는 \\(\\log n\\)이고 각 레벨의 총반복 횟수는 \\(n\\)이므로 전체 시간은 \\(O(n \\log n)\\)이다. 최악의 경우 매 분할에서 길이 \\(n\\)의 배열이 길이 \\(0\\)과 \\(n - 1\\)인 두 부분 배열로 나뉜다. 이때 재귀 레벨 수는 \\(n\\)에 이르고 각 레벨의 반복 횟수는 \\(n\\)이므로 전체 시간은 \\(O(n^2)\\)이다.</li> <li>공간 복잡도 \\(O(n)\\), 제자리 정렬: 입력 배열이 완전히 역순이면 재귀 깊이가 최악의 값인 \\(n\\)에 이르므로 \\(O(n)\\)의 스택 프레임 공간을 사용한다. 정렬 자체는 추가 배열 없이 원래 배열에서 수행한다.</li> <li>불안정 정렬: 분할의 마지막 단계에서 피벗이 값이 같은 원소의 오른쪽으로 교환될 수 있다.</li> </ul>","path":["제 11 장   정렬","11.5   퀵 정렬"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1153","level":2,"title":"11.5.3   퀵 정렬이 빠른 이유","text":"<p>이름에서도 알 수 있듯이 퀵 정렬은 효율 측면에서 장점이 있다. 평균 시간 복잡도는 '병합 정렬', '힙 정렬'과 같지만 보통 퀵 정렬이 더 효율적이다. 주요 이유는 다음과 같다.</p> <ul> <li>최악의 경우가 발생할 확률이 낮다: 퀵 정렬의 최악 시간 복잡도는 \\(O(n^2)\\)으로 병합 정렬만큼 안정적이지 않지만, 대부분의 경우 \\(O(n \\log n)\\) 시간에 실행된다.</li> <li>캐시 활용 효율이 높다: 분할을 수행할 때 시스템이 부분 배열 전체를 캐시에 올릴 수 있어 원소 접근이 효율적이다. 반면 '힙 정렬' 같은 알고리즘은 원소를 건너뛰며 접근하므로 이런 특성이 없다.</li> <li>복잡도의 상수 계수가 작다: 위 세 알고리즘 중 퀵 정렬은 비교, 대입, 교환 등의 총연산 수가 가장 적다. '삽입 정렬'이 '버블 정렬'보다 빠른 이유와 비슷하다.</li> </ul>","path":["제 11 장   정렬","11.5   퀵 정렬"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1154","level":2,"title":"11.5.4   피벗 선택 최적화","text":"<p>일부 입력에서는 퀵 정렬의 시간 효율이 떨어질 수 있다. 극단적인 예로 입력 배열이 완전히 역순이라고 하자. 맨 왼쪽 원소를 피벗으로 고르면 분할 후 피벗이 맨 오른쪽으로 이동하여 왼쪽 부분 배열의 길이는 \\(n - 1\\), 오른쪽 부분 배열의 길이는 \\(0\\)이 된다. 이런 식으로 재귀하면 매번 한쪽 부분 배열의 길이가 \\(0\\)이 되어 분할 정복 전략이 제 기능을 하지 못하고, 퀵 정렬은 '버블 정렬'과 비슷한 형태로 퇴화한다.</p> <p>이 상황을 최대한 피하려면 분할에 사용할 피벗의 선택 전략을 최적화할 수 있다. 예를 들어 원소 하나를 무작위로 골라 피벗으로 삼을 수 있다. 하지만 운이 나빠 매번 좋지 않은 피벗을 고르면 여전히 만족스러운 효율을 얻지 못한다.</p> <p>프로그래밍 언어가 일반적으로 생성하는 것은 '의사 난수'라는 점에 주의하자. 의사 난수 수열에 맞추어 특정 테스트 사례를 만들면 퀵 정렬의 효율이 여전히 저하될 수 있다.</p> <p>더 개선하려면 배열에서 후보 원소 세 개(보통 첫 원소, 마지막 원소, 중간 원소)를 고르고, 이 세 후보의 중앙값을 피벗으로 사용한다. 그러면 피벗이 '너무 작지도 크지도 않을' 확률이 크게 높아진다. 후보를 더 많이 골라 알고리즘의 견고함을 높일 수도 있다. 이 방법을 사용하면 시간 복잡도가 \\(O(n^2)\\)으로 저하될 확률이 크게 줄어든다.</p> <p>예시 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py<pre><code>def median_three(self, nums: list[int], left: int, mid: int, right: int) -&gt; int:\n \"\"\"세 후보 원소의 중앙값 선택\"\"\"\n l, m, r = nums[left], nums[mid], nums[right]\n if (l &lt;= m &lt;= r) or (r &lt;= m &lt;= l):\n return mid # m은 l과 r 사이에 있다\n if (m &lt;= l &lt;= r) or (r &lt;= l &lt;= m):\n return left # l은 m과 r 사이에 있다\n return right\n\ndef partition(self, nums: list[int], left: int, right: int) -&gt; int:\n \"\"\"센티널 분할(세 수의 중앙값 선택)\"\"\"\n # nums[left]를 피벗으로 삼는다\n med = self.median_three(nums, left, (left + right) // 2, right)\n # 중앙값을 배열 맨 왼쪽으로 교환하여 옮긴다\n nums[left], nums[med] = nums[med], nums[left]\n # nums[left]를 피벗으로 삼는다\n i, j = left, right\n while i &lt; j:\n while i &lt; j and nums[j] &gt;= nums[left]:\n j -= 1 # 오른쪽에서 왼쪽으로 피벗보다 작은 첫 번째 원소를 찾는다\n while i &lt; j and nums[i] &lt;= nums[left]:\n i += 1 # 왼쪽에서 오른쪽으로 피벗보다 큰 첫 번째 원소를 찾는다\n # 원소 교환\n nums[i], nums[j] = nums[j], nums[i]\n # 피벗을 두 부분 배열의 경계로 교환하여 옮긴다\n nums[i], nums[left] = nums[left], nums[i]\n return i # 피벗의 인덱스를 반환한다\n</code></pre> quick_sort.cpp<pre><code>/* 세 후보 원소의 중앙값 선택 */\nint medianThree(vector&lt;int&gt; &amp;nums, int left, int mid, int right) {\n int l = nums[left], m = nums[mid], r = nums[right];\n if ((l &lt;= m &amp;&amp; m &lt;= r) || (r &lt;= m &amp;&amp; m &lt;= l))\n return mid; // m은 l과 r 사이에 있다\n if ((m &lt;= l &amp;&amp; l &lt;= r) || (r &lt;= l &amp;&amp; l &lt;= m))\n return left; // l은 m과 r 사이에 있다\n return right;\n}\n\n/* 센티널 분할(세 수의 중앙값 선택) */\nint partition(vector&lt;int&gt; &amp;nums, int left, int right) {\n // 세 후보 원소의 중앙값 선택\n int med = medianThree(nums, left, (left + right) / 2, right);\n // 중앙값을 배열 맨 왼쪽으로 교환하여 옮긴다\n swap(nums[left], nums[med]);\n // nums[left]를 피벗으로 삼는다\n int i = left, j = right;\n while (i &lt; j) {\n while (i &lt; j &amp;&amp; nums[j] &gt;= nums[left])\n j--; // 오른쪽에서 왼쪽으로 피벗보다 작은 첫 번째 원소를 찾는다\n while (i &lt; j &amp;&amp; nums[i] &lt;= nums[left])\n i++; // 왼쪽에서 오른쪽으로 피벗보다 큰 첫 번째 원소를 찾는다\n swap(nums[i], nums[j]); // 두 원소를 교환한다\n }\n swap(nums[i], nums[left]); // 피벗을 두 부분 배열의 경계로 교환하여 옮긴다\n return i; // 피벗의 인덱스를 반환한다\n}\n</code></pre> quick_sort.java<pre><code>/* 세 후보 원소의 중앙값 선택 */\nint medianThree(int[] nums, int left, int mid, int right) {\n int l = nums[left], m = nums[mid], r = nums[right];\n if ((l &lt;= m &amp;&amp; m &lt;= r) || (r &lt;= m &amp;&amp; m &lt;= l))\n return mid; // m은 l과 r 사이에 있다\n if ((m &lt;= l &amp;&amp; l &lt;= r) || (r &lt;= l &amp;&amp; l &lt;= m))\n return left; // l은 m과 r 사이에 있다\n return right;\n}\n\n/* 센티널 분할(세 수의 중앙값 선택) */\nint partition(int[] nums, int left, int right) {\n // 세 후보 원소의 중앙값 선택\n int med = medianThree(nums, left, (left + right) / 2, right);\n // 중앙값을 배열 맨 왼쪽으로 교환하여 옮긴다\n swap(nums, left, med);\n // nums[left]를 피벗으로 삼는다\n int i = left, j = right;\n while (i &lt; j) {\n while (i &lt; j &amp;&amp; nums[j] &gt;= nums[left])\n j--; // 오른쪽에서 왼쪽으로 피벗보다 작은 첫 번째 원소를 찾는다\n while (i &lt; j &amp;&amp; nums[i] &lt;= nums[left])\n i++; // 왼쪽에서 오른쪽으로 피벗보다 큰 첫 번째 원소를 찾는다\n swap(nums, i, j); // 두 원소를 교환한다\n }\n swap(nums, i, left); // 피벗을 두 부분 배열의 경계로 교환하여 옮긴다\n return i; // 피벗의 인덱스를 반환한다\n}\n</code></pre> quick_sort.cs<pre><code>/* 세 후보 원소의 중앙값 선택 */\nint MedianThree(int[] nums, int left, int mid, int right) {\n int l = nums[left], m = nums[mid], r = nums[right];\n if ((l &lt;= m &amp;&amp; m &lt;= r) || (r &lt;= m &amp;&amp; m &lt;= l))\n return mid; // m은 l과 r 사이에 있다\n if ((m &lt;= l &amp;&amp; l &lt;= r) || (r &lt;= l &amp;&amp; l &lt;= m))\n return left; // l은 m과 r 사이에 있다\n return right;\n}\n\n/* 센티널 분할(세 수의 중앙값 선택) */\nint Partition(int[] nums, int left, int right) {\n // 세 후보 원소의 중앙값 선택\n int med = MedianThree(nums, left, (left + right) / 2, right);\n // 중앙값을 배열 맨 왼쪽으로 교환하여 옮긴다\n Swap(nums, left, med);\n // nums[left]를 피벗으로 삼는다\n int i = left, j = right;\n while (i &lt; j) {\n while (i &lt; j &amp;&amp; nums[j] &gt;= nums[left])\n j--; // 오른쪽에서 왼쪽으로 피벗보다 작은 첫 번째 원소를 찾는다\n while (i &lt; j &amp;&amp; nums[i] &lt;= nums[left])\n i++; // 왼쪽에서 오른쪽으로 피벗보다 큰 첫 번째 원소를 찾는다\n Swap(nums, i, j); // 두 원소를 교환한다\n }\n Swap(nums, i, left); // 피벗을 두 부분 배열의 경계로 교환하여 옮긴다\n return i; // 피벗의 인덱스를 반환한다\n}\n</code></pre> quick_sort.go<pre><code>/* 세 후보 원소의 중앙값 선택 */\nfunc (q *quickSortMedian) medianThree(nums []int, left, mid, right int) int {\n l, m, r := nums[left], nums[mid], nums[right]\n if (l &lt;= m &amp;&amp; m &lt;= r) || (r &lt;= m &amp;&amp; m &lt;= l) {\n return mid // m은 l과 r 사이에 있다\n }\n if (m &lt;= l &amp;&amp; l &lt;= r) || (r &lt;= l &amp;&amp; l &lt;= m) {\n return left // l은 m과 r 사이에 있다\n }\n return right\n}\n\n/* 센티널 분할(세 수의 중앙값 선택)*/\nfunc (q *quickSortMedian) partition(nums []int, left, right int) int {\n // nums[left]를 피벗으로 삼는다\n med := q.medianThree(nums, left, (left+right)/2, right)\n // 중앙값을 배열 맨 왼쪽으로 교환하여 옮긴다\n nums[left], nums[med] = nums[med], nums[left]\n // nums[left]를 피벗으로 삼는다\n i, j := left, right\n for i &lt; j {\n for i &lt; j &amp;&amp; nums[j] &gt;= nums[left] {\n j-- //오른쪽에서 왼쪽으로 피벗보다 작은 첫 번째 원소를 찾는다\n }\n for i &lt; j &amp;&amp; nums[i] &lt;= nums[left] {\n i++ //왼쪽에서 오른쪽으로 피벗보다 큰 첫 번째 원소를 찾는다\n }\n //원소 교환\n nums[i], nums[j] = nums[j], nums[i]\n }\n //피벗을 두 부분 배열의 경계로 교환하여 옮긴다\n nums[i], nums[left] = nums[left], nums[i]\n return i //피벗의 인덱스를 반환한다\n}\n</code></pre> quick_sort.swift<pre><code>/* 세 후보 원소의 중앙값 선택 */\nfunc medianThree(nums: [Int], left: Int, mid: Int, right: Int) -&gt; Int {\n let l = nums[left]\n let m = nums[mid]\n let r = nums[right]\n if (l &lt;= m &amp;&amp; m &lt;= r) || (r &lt;= m &amp;&amp; m &lt;= l) {\n return mid // m은 l과 r 사이에 있다\n }\n if (m &lt;= l &amp;&amp; l &lt;= r) || (r &lt;= l &amp;&amp; l &lt;= m) {\n return left // l은 m과 r 사이에 있다\n }\n return right\n}\n\n/* 센티널 분할(세 수의 중앙값 선택) */\nfunc partitionMedian(nums: inout [Int], left: Int, right: Int) -&gt; Int {\n // 세 후보 원소의 중앙값 선택\n let med = medianThree(nums: nums, left: left, mid: left + (right - left) / 2, right: right)\n // 중앙값을 배열 맨 왼쪽으로 교환하여 옮긴다\n nums.swapAt(left, med)\n return partition(nums: &amp;nums, left: left, right: right)\n}\n</code></pre> quick_sort.js<pre><code>/* 세 후보 원소의 중앙값 선택 */\nmedianThree(nums, left, mid, right) {\n let l = nums[left],\n m = nums[mid],\n r = nums[right];\n // m은 l과 r 사이에 있다\n if ((l &lt;= m &amp;&amp; m &lt;= r) || (r &lt;= m &amp;&amp; m &lt;= l)) return mid;\n // l은 m과 r 사이에 있다\n if ((m &lt;= l &amp;&amp; l &lt;= r) || (r &lt;= l &amp;&amp; l &lt;= m)) return left;\n return right;\n}\n\n/* 센티널 분할(세 수의 중앙값 선택) */\npartition(nums, left, right) {\n // 세 후보 원소의 중앙값 선택\n let med = this.medianThree(\n nums,\n left,\n Math.floor((left + right) / 2),\n right\n );\n // 중앙값을 배열 맨 왼쪽으로 교환하여 옮긴다\n this.swap(nums, left, med);\n // nums[left]를 피벗으로 삼는다\n let i = left,\n j = right;\n while (i &lt; j) {\n while (i &lt; j &amp;&amp; nums[j] &gt;= nums[left]) j--; // 오른쪽에서 왼쪽으로 피벗보다 작은 첫 번째 원소를 찾는다\n while (i &lt; j &amp;&amp; nums[i] &lt;= nums[left]) i++; // 왼쪽에서 오른쪽으로 피벗보다 큰 첫 번째 원소를 찾는다\n this.swap(nums, i, j); // 두 원소를 교환한다\n }\n this.swap(nums, i, left); // 피벗을 두 부분 배열의 경계로 교환하여 옮긴다\n return i; // 피벗의 인덱스를 반환한다\n}\n</code></pre> quick_sort.ts<pre><code>/* 세 후보 원소의 중앙값 선택 */\nmedianThree(\n nums: number[],\n left: number,\n mid: number,\n right: number\n): number {\n let l = nums[left],\n m = nums[mid],\n r = nums[right];\n // m은 l과 r 사이에 있다\n if ((l &lt;= m &amp;&amp; m &lt;= r) || (r &lt;= m &amp;&amp; m &lt;= l)) return mid;\n // l은 m과 r 사이에 있다\n if ((m &lt;= l &amp;&amp; l &lt;= r) || (r &lt;= l &amp;&amp; l &lt;= m)) return left;\n return right;\n}\n\n/* 센티널 분할(세 수의 중앙값 선택) */\npartition(nums: number[], left: number, right: number): number {\n // 세 후보 원소의 중앙값 선택\n let med = this.medianThree(\n nums,\n left,\n Math.floor((left + right) / 2),\n right\n );\n // 중앙값을 배열 맨 왼쪽으로 교환하여 옮긴다\n this.swap(nums, left, med);\n // nums[left]를 피벗으로 삼는다\n let i = left,\n j = right;\n while (i &lt; j) {\n while (i &lt; j &amp;&amp; nums[j] &gt;= nums[left]) {\n j--; // 오른쪽에서 왼쪽으로 피벗보다 작은 첫 번째 원소를 찾는다\n }\n while (i &lt; j &amp;&amp; nums[i] &lt;= nums[left]) {\n i++; // 왼쪽에서 오른쪽으로 피벗보다 큰 첫 번째 원소를 찾는다\n }\n this.swap(nums, i, j); // 두 원소를 교환한다\n }\n this.swap(nums, i, left); // 피벗을 두 부분 배열의 경계로 교환하여 옮긴다\n return i; // 피벗의 인덱스를 반환한다\n}\n</code></pre> quick_sort.dart<pre><code>/* 세 후보 원소의 중앙값 선택 */\nint _medianThree(List&lt;int&gt; nums, int left, int mid, int right) {\n int l = nums[left], m = nums[mid], r = nums[right];\n if ((l &lt;= m &amp;&amp; m &lt;= r) || (r &lt;= m &amp;&amp; m &lt;= l))\n return mid; // m은 l과 r 사이에 있다\n if ((m &lt;= l &amp;&amp; l &lt;= r) || (r &lt;= l &amp;&amp; l &lt;= m))\n return left; // l은 m과 r 사이에 있다\n return right;\n}\n\n/* 센티널 분할(세 수의 중앙값 선택) */\nint _partition(List&lt;int&gt; nums, int left, int right) {\n // 세 후보 원소의 중앙값 선택\n int med = _medianThree(nums, left, (left + right) ~/ 2, right);\n // 중앙값을 배열 맨 왼쪽으로 교환하여 옮긴다\n _swap(nums, left, med);\n // nums[left]를 피벗으로 삼는다\n int i = left, j = right;\n while (i &lt; j) {\n while (i &lt; j &amp;&amp; nums[j] &gt;= nums[left]) j--; // 오른쪽에서 왼쪽으로 피벗보다 작은 첫 번째 원소를 찾는다\n while (i &lt; j &amp;&amp; nums[i] &lt;= nums[left]) i++; // 왼쪽에서 오른쪽으로 피벗보다 큰 첫 번째 원소를 찾는다\n _swap(nums, i, j); // 두 원소를 교환한다\n }\n _swap(nums, i, left); // 피벗을 두 부분 배열의 경계로 교환하여 옮긴다\n return i; // 피벗의 인덱스를 반환한다\n}\n</code></pre> quick_sort.rs<pre><code>/* 세 후보 원소의 중앙값 선택 */\nfn median_three(nums: &amp;mut [i32], left: usize, mid: usize, right: usize) -&gt; usize {\n let (l, m, r) = (nums[left], nums[mid], nums[right]);\n if (l &lt;= m &amp;&amp; m &lt;= r) || (r &lt;= m &amp;&amp; m &lt;= l) {\n return mid; // m은 l과 r 사이에 있다\n }\n if (m &lt;= l &amp;&amp; l &lt;= r) || (r &lt;= l &amp;&amp; l &lt;= m) {\n return left; // l은 m과 r 사이에 있다\n }\n right\n}\n\n/* 센티널 분할(세 수의 중앙값 선택) */\nfn partition(nums: &amp;mut [i32], left: usize, right: usize) -&gt; usize {\n // 세 후보 원소의 중앙값 선택\n let med = Self::median_three(nums, left, (left + right) / 2, right);\n // 중앙값을 배열 맨 왼쪽으로 교환하여 옮긴다\n nums.swap(left, med);\n // nums[left]를 피벗으로 삼는다\n let (mut i, mut j) = (left, right);\n while i &lt; j {\n while i &lt; j &amp;&amp; nums[j] &gt;= nums[left] {\n j -= 1; // 오른쪽에서 왼쪽으로 피벗보다 작은 첫 번째 원소를 찾는다\n }\n while i &lt; j &amp;&amp; nums[i] &lt;= nums[left] {\n i += 1; // 왼쪽에서 오른쪽으로 피벗보다 큰 첫 번째 원소를 찾는다\n }\n nums.swap(i, j); // 두 원소를 교환한다\n }\n nums.swap(i, left); // 피벗을 두 부분 배열의 경계로 교환하여 옮긴다\n i // 피벗의 인덱스를 반환한다\n}\n</code></pre> quick_sort.c<pre><code>/* 세 후보 원소의 중앙값 선택 */\nint medianThree(int nums[], int left, int mid, int right) {\n int l = nums[left], m = nums[mid], r = nums[right];\n if ((l &lt;= m &amp;&amp; m &lt;= r) || (r &lt;= m &amp;&amp; m &lt;= l))\n return mid; // m은 l과 r 사이에 있다\n if ((m &lt;= l &amp;&amp; l &lt;= r) || (r &lt;= l &amp;&amp; l &lt;= m))\n return left; // l은 m과 r 사이에 있다\n return right;\n}\n\n/* 센티널 분할(세 수의 중앙값 선택) */\nint partitionMedian(int nums[], int left, int right) {\n // 세 후보 원소의 중앙값 선택\n int med = medianThree(nums, left, (left + right) / 2, right);\n // 중앙값을 배열 맨 왼쪽으로 교환하여 옮긴다\n swap(nums, left, med);\n // nums[left]를 피벗으로 삼는다\n int i = left, j = right;\n while (i &lt; j) {\n while (i &lt; j &amp;&amp; nums[j] &gt;= nums[left])\n j--; // 오른쪽에서 왼쪽으로 피벗보다 작은 첫 번째 원소를 찾는다\n while (i &lt; j &amp;&amp; nums[i] &lt;= nums[left])\n i++; // 왼쪽에서 오른쪽으로 피벗보다 큰 첫 번째 원소를 찾는다\n swap(nums, i, j); // 두 원소를 교환한다\n }\n swap(nums, i, left); // 피벗을 두 부분 배열의 경계로 교환하여 옮긴다\n return i; // 피벗의 인덱스를 반환한다\n}\n</code></pre> quick_sort.kt<pre><code>/* 세 후보 원소의 중앙값 선택 */\nfun medianThree(nums: IntArray, left: Int, mid: Int, right: Int): Int {\n val l = nums[left]\n val m = nums[mid]\n val r = nums[right]\n if ((m in l..r) || (m in r..l))\n return mid // m은 l과 r 사이에 있다\n if ((l in m..r) || (l in r..m))\n return left // l은 m과 r 사이에 있다\n return right\n}\n\n/* 센티널 분할(세 수의 중앙값 선택) */\nfun partitionMedian(nums: IntArray, left: Int, right: Int): Int {\n // 세 후보 원소의 중앙값 선택\n val med = medianThree(nums, left, (left + right) / 2, right)\n // 중앙값을 배열 맨 왼쪽으로 교환하여 옮긴다\n swap(nums, left, med)\n // nums[left]를 피벗으로 삼는다\n var i = left\n var j = right\n while (i &lt; j) {\n while (i &lt; j &amp;&amp; nums[j] &gt;= nums[left])\n j-- // 오른쪽에서 왼쪽으로 피벗보다 작은 첫 번째 원소를 찾는다\n while (i &lt; j &amp;&amp; nums[i] &lt;= nums[left])\n i++ // 왼쪽에서 오른쪽으로 피벗보다 큰 첫 번째 원소를 찾는다\n swap(nums, i, j) // 두 원소를 교환한다\n }\n swap(nums, i, left) // 피벗을 두 부분 배열의 경계로 교환하여 옮긴다\n return i // 피벗의 인덱스를 반환한다\n}\n</code></pre> quick_sort.rb<pre><code>### 세 후보 원소의 중앙값 선택 ###\ndef median_three(nums, left, mid, right)\n # 세 후보 원소의 중앙값 선택\n _l, _m, _r = nums[left], nums[mid], nums[right]\n # m은 l과 r 사이에 있다\n return mid if (_l &lt;= _m &amp;&amp; _m &lt;= _r) || (_r &lt;= _m &amp;&amp; _m &lt;= _l)\n # l은 m과 r 사이에 있다\n return left if (_m &lt;= _l &amp;&amp; _l &lt;= _r) || (_r &lt;= _l &amp;&amp; _l &lt;= _m)\n return right\nend\n\n### 센티널 분할(세 수의 중앙값 선택)###\ndef partition(nums, left, right)\n ### nums[left]를 피벗으로 삼는다\n med = median_three(nums, left, (left + right) / 2, right)\n # 중앙값을 배열 맨 왼쪽으로 교환하여 옮긴다\n nums[left], nums[med] = nums[med], nums[left]\n i, j = left, right\n while i &lt; j\n while i &lt; j &amp;&amp; nums[j] &gt;= nums[left]\n j -= 1 # 오른쪽에서 왼쪽으로 피벗보다 작은 첫 번째 원소를 찾는다\n end\n while i &lt; j &amp;&amp; nums[i] &lt;= nums[left]\n i += 1 # 왼쪽에서 오른쪽으로 피벗보다 큰 첫 번째 원소를 찾는다\n end\n # 원소 교환\n nums[i], nums[j] = nums[j], nums[i]\n end\n # 피벗을 두 부분 배열의 경계로 교환하여 옮긴다\n nums[i], nums[left] = nums[left], nums[i]\n i # 피벗의 인덱스를 반환한다\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 11 장   정렬","11.5   퀵 정렬"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1155","level":2,"title":"11.5.5   재귀 깊이 최적화","text":"<p>일부 입력에서는 퀵 정렬이 많은 공간을 차지할 수 있다. 완전히 정렬된 입력 배열을 예로 들어 보자. 재귀 중인 부분 배열의 길이가 \\(m\\)이라면 매 분할에서 길이 \\(0\\)인 왼쪽 부분 배열과 길이 \\(m - 1\\)인 오른쪽 부분 배열이 생긴다. 이는 재귀 한 단계마다 줄어드는 문제 크기가 매우 작다는 뜻이다(원소 하나만 줄어든다). 재귀 트리의 높이는 \\(n - 1\\)에 이르며, \\(O(n)\\) 크기의 스택 프레임 공간이 필요하다.</p> <p>스택 프레임 공간이 쌓이지 않도록 매번 분할을 마친 뒤 두 부분 배열의 길이를 비교하여 더 짧은 부분 배열에만 재귀를 수행할 수 있다. 짧은 쪽의 길이는 \\(n / 2\\)를 넘지 않으므로 재귀 깊이를 \\(\\log n\\) 이하로 유지할 수 있다. 이로써 최악의 공간 복잡도를 \\(O(\\log n)\\)으로 개선한다. 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py<pre><code>def quick_sort(self, nums: list[int], left: int, right: int):\n \"\"\"퀵 정렬(재귀 깊이 최적화)\"\"\"\n # 부분 배열의 길이가 1이면 종료한다\n while left &lt; right:\n # 센티널 분할 연산\n pivot = self.partition(nums, left, right)\n # 두 부분 배열 중 더 짧은 쪽에 퀵 정렬을 수행한다\n if pivot - left &lt; right - pivot:\n self.quick_sort(nums, left, pivot - 1) # 왼쪽 부분 배열을 재귀적으로 정렬한다\n left = pivot + 1 # 남은 미정렬 구간은 [pivot + 1, right]이다\n else:\n self.quick_sort(nums, pivot + 1, right) # 오른쪽 부분 배열을 재귀적으로 정렬한다\n right = pivot - 1 # 남은 미정렬 구간은 [left, pivot - 1]이다\n</code></pre> quick_sort.cpp<pre><code>/* 퀵 정렬(재귀 깊이 최적화) */\nvoid quickSort(vector&lt;int&gt; &amp;nums, int left, int right) {\n // 부분 배열의 길이가 1이면 종료한다\n while (left &lt; right) {\n // 센티널 분할 연산\n int pivot = partition(nums, left, right);\n // 두 부분 배열 중 더 짧은 쪽에 퀵 정렬을 수행한다\n if (pivot - left &lt; right - pivot) {\n quickSort(nums, left, pivot - 1); // 왼쪽 부분 배열을 재귀적으로 정렬한다\n left = pivot + 1; // 남은 미정렬 구간은 [pivot + 1, right]이다\n } else {\n quickSort(nums, pivot + 1, right); // 오른쪽 부분 배열을 재귀적으로 정렬한다\n right = pivot - 1; // 남은 미정렬 구간은 [left, pivot - 1]이다\n }\n }\n}\n</code></pre> quick_sort.java<pre><code>/* 퀵 정렬(재귀 깊이 최적화) */\nvoid quickSort(int[] nums, int left, int right) {\n // 부분 배열의 길이가 1이면 종료한다\n while (left &lt; right) {\n // 센티널 분할 연산\n int pivot = partition(nums, left, right);\n // 두 부분 배열 중 더 짧은 쪽에 퀵 정렬을 수행한다\n if (pivot - left &lt; right - pivot) {\n quickSort(nums, left, pivot - 1); // 왼쪽 부분 배열을 재귀적으로 정렬한다\n left = pivot + 1; // 남은 미정렬 구간은 [pivot + 1, right]이다\n } else {\n quickSort(nums, pivot + 1, right); // 오른쪽 부분 배열을 재귀적으로 정렬한다\n right = pivot - 1; // 남은 미정렬 구간은 [left, pivot - 1]이다\n }\n }\n}\n</code></pre> quick_sort.cs<pre><code>/* 퀵 정렬(재귀 깊이 최적화) */\nvoid QuickSort(int[] nums, int left, int right) {\n // 부분 배열의 길이가 1이면 종료한다\n while (left &lt; right) {\n // 센티널 분할 연산\n int pivot = Partition(nums, left, right);\n // 두 부분 배열 중 더 짧은 쪽에 퀵 정렬을 수행한다\n if (pivot - left &lt; right - pivot) {\n QuickSort(nums, left, pivot - 1); // 왼쪽 부분 배열을 재귀적으로 정렬한다\n left = pivot + 1; // 남은 미정렬 구간은 [pivot + 1, right]이다\n } else {\n QuickSort(nums, pivot + 1, right); // 오른쪽 부분 배열을 재귀적으로 정렬한다\n right = pivot - 1; // 남은 미정렬 구간은 [left, pivot - 1]이다\n }\n }\n}\n</code></pre> quick_sort.go<pre><code>/* 퀵 정렬(재귀 깊이 최적화)*/\nfunc (q *quickSortTailCall) quickSort(nums []int, left, right int) {\n // 부분 배열의 길이가 1이면 종료한다\n for left &lt; right {\n // 센티널 분할 연산\n pivot := q.partition(nums, left, right)\n // 두 부분 배열 중 더 짧은 쪽에 퀵 정렬을 수행한다\n if pivot-left &lt; right-pivot {\n q.quickSort(nums, left, pivot-1) // 왼쪽 부분 배열을 재귀적으로 정렬한다\n left = pivot + 1 // 남은 미정렬 구간은 [pivot + 1, right]이다\n } else {\n q.quickSort(nums, pivot+1, right) // 오른쪽 부분 배열을 재귀적으로 정렬한다\n right = pivot - 1 // 남은 미정렬 구간은 [left, pivot - 1]이다\n }\n }\n}\n</code></pre> quick_sort.swift<pre><code>/* 퀵 정렬(재귀 깊이 최적화) */\nfunc quickSortTailCall(nums: inout [Int], left: Int, right: Int) {\n var left = left\n var right = right\n // 부분 배열의 길이가 1이면 종료한다\n while left &lt; right {\n // 센티널 분할 연산\n let pivot = partition(nums: &amp;nums, left: left, right: right)\n // 두 부분 배열 중 더 짧은 쪽에 퀵 정렬을 수행한다\n if (pivot - left) &lt; (right - pivot) {\n quickSortTailCall(nums: &amp;nums, left: left, right: pivot - 1) // 왼쪽 부분 배열을 재귀적으로 정렬한다\n left = pivot + 1 // 남은 미정렬 구간은 [pivot + 1, right]이다\n } else {\n quickSortTailCall(nums: &amp;nums, left: pivot + 1, right: right) // 오른쪽 부분 배열을 재귀적으로 정렬한다\n right = pivot - 1 // 남은 미정렬 구간은 [left, pivot - 1]이다\n }\n }\n}\n</code></pre> quick_sort.js<pre><code>/* 퀵 정렬(재귀 깊이 최적화) */\nquickSort(nums, left, right) {\n // 부분 배열의 길이가 1이면 종료한다\n while (left &lt; right) {\n // 센티널 분할 연산\n let pivot = this.partition(nums, left, right);\n // 두 부분 배열 중 더 짧은 쪽에 퀵 정렬을 수행한다\n if (pivot - left &lt; right - pivot) {\n this.quickSort(nums, left, pivot - 1); // 왼쪽 부분 배열을 재귀적으로 정렬한다\n left = pivot + 1; // 남은 미정렬 구간은 [pivot + 1, right]이다\n } else {\n this.quickSort(nums, pivot + 1, right); // 오른쪽 부분 배열을 재귀적으로 정렬한다\n right = pivot - 1; // 남은 미정렬 구간은 [left, pivot - 1]이다\n }\n }\n}\n</code></pre> quick_sort.ts<pre><code>/* 퀵 정렬(재귀 깊이 최적화) */\nquickSort(nums: number[], left: number, right: number): void {\n // 부분 배열의 길이가 1이면 종료한다\n while (left &lt; right) {\n // 센티널 분할 연산\n let pivot = this.partition(nums, left, right);\n // 두 부분 배열 중 더 짧은 쪽에 퀵 정렬을 수행한다\n if (pivot - left &lt; right - pivot) {\n this.quickSort(nums, left, pivot - 1); // 왼쪽 부분 배열을 재귀적으로 정렬한다\n left = pivot + 1; // 남은 미정렬 구간은 [pivot + 1, right]이다\n } else {\n this.quickSort(nums, pivot + 1, right); // 오른쪽 부분 배열을 재귀적으로 정렬한다\n right = pivot - 1; // 남은 미정렬 구간은 [left, pivot - 1]이다\n }\n }\n}\n</code></pre> quick_sort.dart<pre><code>/* 퀵 정렬(재귀 깊이 최적화) */\nvoid quickSort(List&lt;int&gt; nums, int left, int right) {\n // 부분 배열의 길이가 1이면 종료한다\n while (left &lt; right) {\n // 센티널 분할 연산\n int pivot = _partition(nums, left, right);\n // 두 부분 배열 중 더 짧은 쪽에 퀵 정렬을 수행한다\n if (pivot - left &lt; right - pivot) {\n quickSort(nums, left, pivot - 1); // 왼쪽 부분 배열을 재귀적으로 정렬한다\n left = pivot + 1; // 남은 미정렬 구간은 [pivot + 1, right]이다\n } else {\n quickSort(nums, pivot + 1, right); // 오른쪽 부분 배열을 재귀적으로 정렬한다\n right = pivot - 1; // 남은 미정렬 구간은 [left, pivot - 1]이다\n }\n }\n}\n</code></pre> quick_sort.rs<pre><code>/* 퀵 정렬(재귀 깊이 최적화) */\npub fn quick_sort(mut left: i32, mut right: i32, nums: &amp;mut [i32]) {\n // 부분 배열의 길이가 1이면 종료한다\n while left &lt; right {\n // 센티널 분할 연산\n let pivot = Self::partition(nums, left as usize, right as usize) as i32;\n // 두 부분 배열 중 더 짧은 쪽에 퀵 정렬을 수행한다\n if pivot - left &lt; right - pivot {\n Self::quick_sort(left, pivot - 1, nums); // 왼쪽 부분 배열을 재귀적으로 정렬한다\n left = pivot + 1; // 남은 미정렬 구간은 [pivot + 1, right]이다\n } else {\n Self::quick_sort(pivot + 1, right, nums); // 오른쪽 부분 배열을 재귀적으로 정렬한다\n right = pivot - 1; // 남은 미정렬 구간은 [left, pivot - 1]이다\n }\n }\n}\n</code></pre> quick_sort.c<pre><code>/* 퀵 정렬(재귀 깊이 최적화) */\nvoid quickSortTailCall(int nums[], int left, int right) {\n // 부분 배열의 길이가 1이면 종료한다\n while (left &lt; right) {\n // 센티널 분할 연산\n int pivot = partition(nums, left, right);\n // 두 부분 배열 중 더 짧은 쪽에 퀵 정렬을 수행한다\n if (pivot - left &lt; right - pivot) {\n // 왼쪽 부분 배열을 재귀적으로 정렬한다\n quickSortTailCall(nums, left, pivot - 1);\n // 남은 미정렬 구간은 [pivot + 1, right]이다\n left = pivot + 1;\n } else {\n // 오른쪽 부분 배열을 재귀적으로 정렬한다\n quickSortTailCall(nums, pivot + 1, right);\n // 남은 미정렬 구간은 [left, pivot - 1]이다\n right = pivot - 1;\n }\n }\n}\n</code></pre> quick_sort.kt<pre><code>/* 퀵 정렬(재귀 깊이 최적화) */\nfun quickSortTailCall(nums: IntArray, left: Int, right: Int) {\n // 부분 배열의 길이가 1이면 종료한다\n var l = left\n var r = right\n while (l &lt; r) {\n // 센티널 분할 연산\n val pivot = partition(nums, l, r)\n // 두 부분 배열 중 더 짧은 쪽에 퀵 정렬을 수행한다\n if (pivot - l &lt; r - pivot) {\n quickSort(nums, l, pivot - 1) // 왼쪽 부분 배열을 재귀적으로 정렬한다\n l = pivot + 1 // 남은 미정렬 구간은 [pivot + 1, right]이다\n } else {\n quickSort(nums, pivot + 1, r) // 오른쪽 부분 배열을 재귀적으로 정렬한다\n r = pivot - 1 // 남은 미정렬 구간은 [left, pivot - 1]이다\n }\n }\n}\n</code></pre> quick_sort.rb<pre><code>### 퀵 정렬(재귀 깊이 최적화)###\ndef quick_sort(nums, left, right)\n # 부분 배열의 길이가 1이 아니면 재귀 호출한다\n while left &lt; right\n # 센티널 분할\n pivot = partition(nums, left, right)\n # 두 부분 배열 중 더 짧은 쪽에 퀵 정렬을 수행한다\n if pivot - left &lt; right - pivot\n quick_sort(nums, left, pivot - 1)\n left = pivot + 1 # 남은 미정렬 구간은 [pivot + 1, right]이다\n else\n quick_sort(nums, pivot + 1, right)\n right = pivot - 1 # 남은 미정렬 구간은 [left, pivot - 1]이다\n end\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 11 장   정렬","11.5   퀵 정렬"],"tags":[]},{"location":"chapter_sorting/radix_sort/","level":1,"title":"11.10   기수 정렬","text":"<p>앞 절에서 배운 계수 정렬은 데이터 수 \\(n\\)은 많고 값의 범위 \\(m\\)은 작은 경우에 적합하다. 학번 \\(n = 10^6\\) 개를 정렬해야 하며, 학번이 \\(8\\) 자리 수라고 하자. 이때 값의 범위는 \\(m = 10^8\\)로 매우 커서 계수 정렬에는 많은 메모리가 필요하다. 기수 정렬을 사용하면 이 문제를 피할 수 있다.</p> <p>기수 정렬(radix sort)의 핵심 아이디어는 계수 정렬과 같으며, 개수를 세어 정렬한다. 여기에 자릿수 사이의 순서 관계를 이용하여 각 자릿수를 차례로 정렬함으로써 최종 정렬 결과를 얻는다.</p>","path":["제 11 장   정렬","11.10   기수 정렬"],"tags":[]},{"location":"chapter_sorting/radix_sort/#11101","level":2,"title":"11.10.1   알고리즘 과정","text":"<p>학번을 예로 들어 가장 낮은 자리를 \\(1\\) 번째, 가장 높은 자리를 \\(8\\) 번째라고 하자. 기수 정렬의 과정은 아래 그림(11-18)과 같다.</p> <ol> <li>자릿수 \\(k = 1\\)로 초기화한다.</li> <li>학번의 \\(k\\) 번째 자리를 기준으로 '계수 정렬'을 수행한다. 완료하면 데이터는 \\(k\\) 번째 자리의 값에 따라 오름차순으로 정렬된다.</li> <li>\\(k\\)를 \\(1\\) 늘리고 <code>2.</code> 단계로 돌아가 반복한다. 모든 자리를 정렬하면 종료한다.</li> </ol> <p></p> <p> 그림 11-18   기수 정렬 알고리즘 과정 </p> <p>이제 코드 구현을 살펴보자. \\(d\\) 진법으로 표현한 수 \\(x\\)의 \\(k\\) 번째 자리 \\(x_k\\)는 다음 공식으로 구할 수 있다.</p> \\[ x_k = \\lfloor\\frac{x}{d^{k-1}}\\rfloor \\bmod d \\] <p>여기서 \\(\\lfloor a \\rfloor\\)는 부동 소수점 수 \\(a\\)를 내림하는 연산이고, \\(\\bmod \\: d\\)는 \\(d\\)로 나눈 나머지를 구하는 연산이다. 학번 데이터에서는 \\(d = 10\\)이고 \\(k \\in [1, 8]\\)이다.</p> <p>또한 수의 \\(k\\) 번째 자리를 기준으로 정렬할 수 있도록 계수 정렬 코드를 조금 수정해야 한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby radix_sort.py<pre><code>def digit(num: int, exp: int) -&gt; int:\n \"\"\"원소 num의 k번째 자릿값 반환. 이때 exp = 10^(k-1)이다\"\"\"\n # k 대신 exp를 전달하면 여기서 비용이 큰 거듭제곱 계산을 반복하지 않아도 된다\n return (num // exp) % 10\n\ndef counting_sort_digit(nums: list[int], exp: int):\n \"\"\"계수 정렬(nums의 k번째 자릿값을 기준으로 정렬)\"\"\"\n # 십진수의 각 자리는 0~9 범위이므로 길이가 10인 버킷 배열이 필요하다\n counter = [0] * 10\n n = len(nums)\n # 0~9 각 숫자의 등장 횟수를 센다\n for i in range(n):\n d = digit(nums[i], exp) # nums[i]의 k번째 자릿값을 구하여 d로 표시한다\n counter[d] += 1 # 숫자 d의 등장 횟수를 센다\n # 접두 합을 구하여 등장 개수를 배열 인덱스로 변환한다\n for i in range(1, 10):\n counter[i] += counter[i - 1]\n # 역순으로 순회하며 버킷에 집계된 결과에 따라 각 원소를 res에 채운다\n res = [0] * n\n for i in range(n - 1, -1, -1):\n d = digit(nums[i], exp)\n j = counter[d] - 1 # 배열에서 d의 인덱스 j를 구한다\n res[j] = nums[i] # 현재 원소를 인덱스 j에 채운다\n counter[d] -= 1 # d의 개수를 1 줄인다\n # 결과로 원래 배열 nums를 덮어쓴다\n for i in range(n):\n nums[i] = res[i]\n\ndef radix_sort(nums: list[int]):\n \"\"\"기수 정렬\"\"\"\n # 배열의 최댓값을 구하여 최대 자릿수를 판단한다\n m = max(nums)\n # 낮은 자리에서 높은 자리 순서로 순회한다\n exp = 1\n while exp &lt;= m:\n # 배열 원소의 k번째 자릿값에 대해 계수 정렬을 수행한다\n # k = 1 -&gt; exp = 1\n # k = 2 -&gt; exp = 10\n # 즉, exp = 10^(k-1)이다\n counting_sort_digit(nums, exp)\n exp *= 10\n</code></pre> radix_sort.cpp<pre><code>/* 원소 num의 k번째 자릿값 반환. 이때 exp = 10^(k-1)이다 */\nint digit(int num, int exp) {\n // k 대신 exp를 전달하면 여기서 비용이 큰 거듭제곱 계산을 반복하지 않아도 된다\n return (num / exp) % 10;\n}\n\n/* 계수 정렬(nums의 k번째 자릿값을 기준으로 정렬) */\nvoid countingSortDigit(vector&lt;int&gt; &amp;nums, int exp) {\n // 십진수의 각 자리는 0~9 범위이므로 길이가 10인 버킷 배열이 필요하다\n vector&lt;int&gt; counter(10, 0);\n int n = nums.size();\n // 0~9 각 숫자의 등장 횟수를 센다\n for (int i = 0; i &lt; n; i++) {\n int d = digit(nums[i], exp); // nums[i]의 k번째 자릿값을 구하여 d로 표시한다\n counter[d]++; // 숫자 d의 등장 횟수를 센다\n }\n // 접두 합을 구하여 등장 개수를 배열 인덱스로 변환한다\n for (int i = 1; i &lt; 10; i++) {\n counter[i] += counter[i - 1];\n }\n // 역순으로 순회하며 버킷에 집계된 결과에 따라 각 원소를 res에 채운다\n vector&lt;int&gt; res(n, 0);\n for (int i = n - 1; i &gt;= 0; i--) {\n int d = digit(nums[i], exp);\n int j = counter[d] - 1; // 배열에서 d의 인덱스 j를 구한다\n res[j] = nums[i]; // 현재 원소를 인덱스 j에 채운다\n counter[d]--; // d의 개수를 1 줄인다\n }\n // 결과로 원래 배열 nums를 덮어쓴다\n for (int i = 0; i &lt; n; i++)\n nums[i] = res[i];\n}\n\n/* 기수 정렬 */\nvoid radixSort(vector&lt;int&gt; &amp;nums) {\n // 배열의 최댓값을 구하여 최대 자릿수를 판단한다\n int m = *max_element(nums.begin(), nums.end());\n // 낮은 자리에서 높은 자리 순서로 순회한다\n for (int exp = 1; exp &lt;= m; exp *= 10)\n // 배열 원소의 k번째 자릿값에 대해 계수 정렬을 수행한다\n // k = 1 -&gt; exp = 1\n // k = 2 -&gt; exp = 10\n // 즉, exp = 10^(k-1)이다\n countingSortDigit(nums, exp);\n}\n</code></pre> radix_sort.java<pre><code>/* 원소 num의 k번째 자릿값 반환. 이때 exp = 10^(k-1)이다 */\nint digit(int num, int exp) {\n // k 대신 exp를 전달하면 여기서 비용이 큰 거듭제곱 계산을 반복하지 않아도 된다\n return (num / exp) % 10;\n}\n\n/* 계수 정렬(nums의 k번째 자릿값을 기준으로 정렬) */\nvoid countingSortDigit(int[] nums, int exp) {\n // 십진수의 각 자리는 0~9 범위이므로 길이가 10인 버킷 배열이 필요하다\n int[] counter = new int[10];\n int n = nums.length;\n // 0~9 각 숫자의 등장 횟수를 센다\n for (int i = 0; i &lt; n; i++) {\n int d = digit(nums[i], exp); // nums[i]의 k번째 자릿값을 구하여 d로 표시한다\n counter[d]++; // 숫자 d의 등장 횟수를 센다\n }\n // 접두 합을 구하여 등장 개수를 배열 인덱스로 변환한다\n for (int i = 1; i &lt; 10; i++) {\n counter[i] += counter[i - 1];\n }\n // 역순으로 순회하며 버킷에 집계된 결과에 따라 각 원소를 res에 채운다\n int[] res = new int[n];\n for (int i = n - 1; i &gt;= 0; i--) {\n int d = digit(nums[i], exp);\n int j = counter[d] - 1; // 배열에서 d의 인덱스 j를 구한다\n res[j] = nums[i]; // 현재 원소를 인덱스 j에 채운다\n counter[d]--; // d의 개수를 1 줄인다\n }\n // 결과로 원래 배열 nums를 덮어쓴다\n for (int i = 0; i &lt; n; i++)\n nums[i] = res[i];\n}\n\n/* 기수 정렬 */\nvoid radixSort(int[] nums) {\n // 배열의 최댓값을 구하여 최대 자릿수를 판단한다\n int m = Integer.MIN_VALUE;\n for (int num : nums)\n if (num &gt; m)\n m = num;\n // 낮은 자리에서 높은 자리 순서로 순회한다\n for (int exp = 1; exp &lt;= m; exp *= 10) {\n // 배열 원소의 k번째 자릿값에 대해 계수 정렬을 수행한다\n // k = 1 -&gt; exp = 1\n // k = 2 -&gt; exp = 10\n // 즉, exp = 10^(k-1)이다\n countingSortDigit(nums, exp);\n }\n}\n</code></pre> radix_sort.cs<pre><code>/* 원소 num의 k번째 자릿값 반환. 이때 exp = 10^(k-1)이다 */\nint Digit(int num, int exp) {\n // k 대신 exp를 전달하면 여기서 비용이 큰 거듭제곱 계산을 반복하지 않아도 된다\n return (num / exp) % 10;\n}\n\n/* 계수 정렬(nums의 k번째 자릿값을 기준으로 정렬) */\nvoid CountingSortDigit(int[] nums, int exp) {\n // 십진수의 각 자리는 0~9 범위이므로 길이가 10인 버킷 배열이 필요하다\n int[] counter = new int[10];\n int n = nums.Length;\n // 0~9 각 숫자의 등장 횟수를 센다\n for (int i = 0; i &lt; n; i++) {\n int d = Digit(nums[i], exp); // nums[i]의 k번째 자릿값을 구하여 d로 표시한다\n counter[d]++; // 숫자 d의 등장 횟수를 센다\n }\n // 접두 합을 구하여 등장 개수를 배열 인덱스로 변환한다\n for (int i = 1; i &lt; 10; i++) {\n counter[i] += counter[i - 1];\n }\n // 역순으로 순회하며 버킷에 집계된 결과에 따라 각 원소를 res에 채운다\n int[] res = new int[n];\n for (int i = n - 1; i &gt;= 0; i--) {\n int d = Digit(nums[i], exp);\n int j = counter[d] - 1; // 배열에서 d의 인덱스 j를 구한다\n res[j] = nums[i]; // 현재 원소를 인덱스 j에 채운다\n counter[d]--; // d의 개수를 1 줄인다\n }\n // 결과로 원래 배열 nums를 덮어쓴다\n for (int i = 0; i &lt; n; i++) {\n nums[i] = res[i];\n }\n}\n\n/* 기수 정렬 */\nvoid RadixSort(int[] nums) {\n // 배열의 최댓값을 구하여 최대 자릿수를 판단한다\n int m = int.MinValue;\n foreach (int num in nums) {\n if (num &gt; m) m = num;\n }\n // 낮은 자리에서 높은 자리 순서로 순회한다\n for (int exp = 1; exp &lt;= m; exp *= 10) {\n // 배열 원소의 k번째 자릿값에 대해 계수 정렬을 수행한다\n // k = 1 -&gt; exp = 1\n // k = 2 -&gt; exp = 10\n // 즉, exp = 10^(k-1)이다\n CountingSortDigit(nums, exp);\n }\n}\n</code></pre> radix_sort.go<pre><code>/* 원소 num의 k번째 자릿값 반환. 이때 exp = 10^(k-1)이다 */\nfunc digit(num, exp int) int {\n // k 대신 exp를 전달하면 여기서 비용이 큰 거듭제곱 계산을 반복하지 않아도 된다\n return (num / exp) % 10\n}\n\n/* 계수 정렬(nums의 k번째 자릿값을 기준으로 정렬) */\nfunc countingSortDigit(nums []int, exp int) {\n // 십진수의 각 자리는 0~9 범위이므로 길이가 10인 버킷 배열이 필요하다\n counter := make([]int, 10)\n n := len(nums)\n // 0~9 각 숫자의 등장 횟수를 센다\n for i := 0; i &lt; n; i++ {\n d := digit(nums[i], exp) // nums[i]의 k번째 자릿값을 구하여 d로 표시한다\n counter[d]++ // 숫자 d의 등장 횟수를 센다\n }\n // 접두 합을 구하여 등장 개수를 배열 인덱스로 변환한다\n for i := 1; i &lt; 10; i++ {\n counter[i] += counter[i-1]\n }\n // 역순으로 순회하며 버킷에 집계된 결과에 따라 각 원소를 res에 채운다\n res := make([]int, n)\n for i := n - 1; i &gt;= 0; i-- {\n d := digit(nums[i], exp)\n j := counter[d] - 1 // 배열에서 d의 인덱스 j를 구한다\n res[j] = nums[i] // 현재 원소를 인덱스 j에 채운다\n counter[d]-- // d의 개수를 1 줄인다\n }\n // 결과로 원래 배열 nums를 덮어쓴다\n for i := 0; i &lt; n; i++ {\n nums[i] = res[i]\n }\n}\n\n/* 기수 정렬 */\nfunc radixSort(nums []int) {\n // 배열의 최댓값을 구하여 최대 자릿수를 판단한다\n max := math.MinInt\n for _, num := range nums {\n if num &gt; max {\n max = num\n }\n }\n // 낮은 자리에서 높은 자리 순서로 순회한다\n for exp := 1; max &gt;= exp; exp *= 10 {\n // 배열 원소의 k번째 자릿값에 대해 계수 정렬을 수행한다\n // k = 1 -&gt; exp = 1\n // k = 2 -&gt; exp = 10\n // 즉, exp = 10^(k-1)이다\n countingSortDigit(nums, exp)\n }\n}\n</code></pre> radix_sort.swift<pre><code>/* 원소 num의 k번째 자릿값 반환. 이때 exp = 10^(k-1)이다 */\nfunc digit(num: Int, exp: Int) -&gt; Int {\n // k 대신 exp를 전달하면 여기서 비용이 큰 거듭제곱 계산을 반복하지 않아도 된다\n (num / exp) % 10\n}\n\n/* 계수 정렬(nums의 k번째 자릿값을 기준으로 정렬) */\nfunc countingSortDigit(nums: inout [Int], exp: Int) {\n // 십진수의 각 자리는 0~9 범위이므로 길이가 10인 버킷 배열이 필요하다\n var counter = Array(repeating: 0, count: 10)\n // 0~9 각 숫자의 등장 횟수를 센다\n for i in nums.indices {\n let d = digit(num: nums[i], exp: exp) // nums[i]의 k번째 자릿값을 구하여 d로 표시한다\n counter[d] += 1 // 숫자 d의 등장 횟수를 센다\n }\n // 접두 합을 구하여 등장 개수를 배열 인덱스로 변환한다\n for i in 1 ..&lt; 10 {\n counter[i] += counter[i - 1]\n }\n // 역순으로 순회하며 버킷에 집계된 결과에 따라 각 원소를 res에 채운다\n var res = Array(repeating: 0, count: nums.count)\n for i in nums.indices.reversed() {\n let d = digit(num: nums[i], exp: exp)\n let j = counter[d] - 1 // 배열에서 d의 인덱스 j를 구한다\n res[j] = nums[i] // 현재 원소를 인덱스 j에 채운다\n counter[d] -= 1 // d의 개수를 1 줄인다\n }\n // 결과로 원래 배열 nums를 덮어쓴다\n for i in nums.indices {\n nums[i] = res[i]\n }\n}\n\n/* 기수 정렬 */\nfunc radixSort(nums: inout [Int]) {\n // 배열의 최댓값을 구하여 최대 자릿수를 판단한다\n var m = Int.min\n for num in nums {\n if num &gt; m {\n m = num\n }\n }\n // 낮은 자리에서 높은 자리 순서로 순회한다\n for exp in sequence(first: 1, next: { m &gt;= ($0 * 10) ? $0 * 10 : nil }) {\n // 배열 원소의 k번째 자릿값에 대해 계수 정렬을 수행한다\n // k = 1 -&gt; exp = 1\n // k = 2 -&gt; exp = 10\n // 즉, exp = 10^(k-1)이다\n countingSortDigit(nums: &amp;nums, exp: exp)\n }\n}\n</code></pre> radix_sort.js<pre><code>/* 원소 num의 k번째 자릿값 반환. 이때 exp = 10^(k-1)이다 */\nfunction digit(num, exp) {\n // k 대신 exp를 전달하면 여기서 비용이 큰 거듭제곱 계산을 반복하지 않아도 된다\n return Math.floor(num / exp) % 10;\n}\n\n/* 계수 정렬(nums의 k번째 자릿값을 기준으로 정렬) */\nfunction countingSortDigit(nums, exp) {\n // 십진수의 각 자리는 0~9 범위이므로 길이가 10인 버킷 배열이 필요하다\n const counter = new Array(10).fill(0);\n const n = nums.length;\n // 0~9 각 숫자의 등장 횟수를 센다\n for (let i = 0; i &lt; n; i++) {\n const d = digit(nums[i], exp); // nums[i]의 k번째 자릿값을 구하여 d로 표시한다\n counter[d]++; // 숫자 d의 등장 횟수를 센다\n }\n // 접두 합을 구하여 등장 개수를 배열 인덱스로 변환한다\n for (let i = 1; i &lt; 10; i++) {\n counter[i] += counter[i - 1];\n }\n // 역순으로 순회하며 버킷에 집계된 결과에 따라 각 원소를 res에 채운다\n const res = new Array(n).fill(0);\n for (let i = n - 1; i &gt;= 0; i--) {\n const d = digit(nums[i], exp);\n const j = counter[d] - 1; // 배열에서 d의 인덱스 j를 구한다\n res[j] = nums[i]; // 현재 원소를 인덱스 j에 채운다\n counter[d]--; // d의 개수를 1 줄인다\n }\n // 결과로 원래 배열 nums를 덮어쓴다\n for (let i = 0; i &lt; n; i++) {\n nums[i] = res[i];\n }\n}\n\n/* 기수 정렬 */\nfunction radixSort(nums) {\n // 배열의 최댓값을 구하여 최대 자릿수를 판단한다\n let m = Math.max(... nums);\n // 낮은 자리에서 높은 자리 순서로 순회한다\n for (let exp = 1; exp &lt;= m; exp *= 10) {\n // 배열 원소의 k번째 자릿값에 대해 계수 정렬을 수행한다\n // k = 1 -&gt; exp = 1\n // k = 2 -&gt; exp = 10\n // 즉, exp = 10^(k-1)이다\n countingSortDigit(nums, exp);\n }\n}\n</code></pre> radix_sort.ts<pre><code>/* 원소 num의 k번째 자릿값 반환. 이때 exp = 10^(k-1)이다 */\nfunction digit(num: number, exp: number): number {\n // k 대신 exp를 전달하면 여기서 비용이 큰 거듭제곱 계산을 반복하지 않아도 된다\n return Math.floor(num / exp) % 10;\n}\n\n/* 계수 정렬(nums의 k번째 자릿값을 기준으로 정렬) */\nfunction countingSortDigit(nums: number[], exp: number): void {\n // 십진수의 각 자리는 0~9 범위이므로 길이가 10인 버킷 배열이 필요하다\n const counter = new Array(10).fill(0);\n const n = nums.length;\n // 0~9 각 숫자의 등장 횟수를 센다\n for (let i = 0; i &lt; n; i++) {\n const d = digit(nums[i], exp); // nums[i]의 k번째 자릿값을 구하여 d로 표시한다\n counter[d]++; // 숫자 d의 등장 횟수를 센다\n }\n // 접두 합을 구하여 등장 개수를 배열 인덱스로 변환한다\n for (let i = 1; i &lt; 10; i++) {\n counter[i] += counter[i - 1];\n }\n // 역순으로 순회하며 버킷에 집계된 결과에 따라 각 원소를 res에 채운다\n const res = new Array(n).fill(0);\n for (let i = n - 1; i &gt;= 0; i--) {\n const d = digit(nums[i], exp);\n const j = counter[d] - 1; // 배열에서 d의 인덱스 j를 구한다\n res[j] = nums[i]; // 현재 원소를 인덱스 j에 채운다\n counter[d]--; // d의 개수를 1 줄인다\n }\n // 결과로 원래 배열 nums를 덮어쓴다\n for (let i = 0; i &lt; n; i++) {\n nums[i] = res[i];\n }\n}\n\n/* 기수 정렬 */\nfunction radixSort(nums: number[]): void {\n // 배열의 최댓값을 구하여 최대 자릿수를 판단한다\n let m: number = Math.max(... nums);\n // 낮은 자리에서 높은 자리 순서로 순회한다\n for (let exp = 1; exp &lt;= m; exp *= 10) {\n // 배열 원소의 k번째 자릿값에 대해 계수 정렬을 수행한다\n // k = 1 -&gt; exp = 1\n // k = 2 -&gt; exp = 10\n // 즉, exp = 10^(k-1)이다\n countingSortDigit(nums, exp);\n }\n}\n</code></pre> radix_sort.dart<pre><code>/* 원소 _num의 k번째 자릿값 반환. 이때 exp = 10^(k-1)이다 */\nint digit(int _num, int exp) {\n // k 대신 exp를 전달하면 여기서 비용이 큰 거듭제곱 계산을 반복하지 않아도 된다\n return (_num ~/ exp) % 10;\n}\n\n/* 계수 정렬(nums의 k번째 자릿값을 기준으로 정렬) */\nvoid countingSortDigit(List&lt;int&gt; nums, int exp) {\n // 십진수의 각 자리는 0~9 범위이므로 길이가 10인 버킷 배열이 필요하다\n List&lt;int&gt; counter = List&lt;int&gt;.filled(10, 0);\n int n = nums.length;\n // 0~9 각 숫자의 등장 횟수를 센다\n for (int i = 0; i &lt; n; i++) {\n int d = digit(nums[i], exp); // nums[i]의 k번째 자릿값을 구하여 d로 표시한다\n counter[d]++; // 숫자 d의 등장 횟수를 센다\n }\n // 접두 합을 구하여 등장 개수를 배열 인덱스로 변환한다\n for (int i = 1; i &lt; 10; i++) {\n counter[i] += counter[i - 1];\n }\n // 역순으로 순회하며 버킷에 집계된 결과에 따라 각 원소를 res에 채운다\n List&lt;int&gt; res = List&lt;int&gt;.filled(n, 0);\n for (int i = n - 1; i &gt;= 0; i--) {\n int d = digit(nums[i], exp);\n int j = counter[d] - 1; // 배열에서 d의 인덱스 j를 구한다\n res[j] = nums[i]; // 현재 원소를 인덱스 j에 채운다\n counter[d]--; // d의 개수를 1 줄인다\n }\n // 결과로 원래 배열 nums를 덮어쓴다\n for (int i = 0; i &lt; n; i++) nums[i] = res[i];\n}\n\n/* 기수 정렬 */\nvoid radixSort(List&lt;int&gt; nums) {\n // 배열의 최댓값을 구하여 최대 자릿수를 판단한다\n // Dart에서 int의 길이는 64비트이다\n int m = -1 &lt;&lt; 63;\n for (int _num in nums) if (_num &gt; m) m = _num;\n // 낮은 자리에서 높은 자리 순서로 순회한다\n for (int exp = 1; exp &lt;= m; exp *= 10)\n // 배열 원소의 k번째 자릿값에 대해 계수 정렬을 수행한다\n // k = 1 -&gt; exp = 1\n // k = 2 -&gt; exp = 10\n // 즉, exp = 10^(k-1)이다\n countingSortDigit(nums, exp);\n}\n</code></pre> radix_sort.rs<pre><code>/* 원소 num의 k번째 자릿값 반환. 이때 exp = 10^(k-1)이다 */\nfn digit(num: i32, exp: i32) -&gt; usize {\n // k 대신 exp를 전달하면 여기서 비용이 큰 거듭제곱 계산을 반복하지 않아도 된다\n return ((num / exp) % 10) as usize;\n}\n\n/* 계수 정렬(nums의 k번째 자릿값을 기준으로 정렬) */\nfn counting_sort_digit(nums: &amp;mut [i32], exp: i32) {\n // 십진수의 각 자리는 0~9 범위이므로 길이가 10인 버킷 배열이 필요하다\n let mut counter = [0; 10];\n let n = nums.len();\n // 0~9 각 숫자의 등장 횟수를 센다\n for i in 0..n {\n let d = digit(nums[i], exp); // nums[i]의 k번째 자릿값을 구하여 d로 표시한다\n counter[d] += 1; // 숫자 d의 등장 횟수를 센다\n }\n // 접두 합을 구하여 등장 개수를 배열 인덱스로 변환한다\n for i in 1..10 {\n counter[i] += counter[i - 1];\n }\n // 역순으로 순회하며 버킷에 집계된 결과에 따라 각 원소를 res에 채운다\n let mut res = vec![0; n];\n for i in (0..n).rev() {\n let d = digit(nums[i], exp);\n let j = counter[d] - 1; // 배열에서 d의 인덱스 j를 구한다\n res[j] = nums[i]; // 현재 원소를 인덱스 j에 채운다\n counter[d] -= 1; // d의 개수를 1 줄인다\n }\n // 결과로 원래 배열 nums를 덮어쓴다\n nums.copy_from_slice(&amp;res);\n}\n\n/* 기수 정렬 */\nfn radix_sort(nums: &amp;mut [i32]) {\n // 배열의 최댓값을 구하여 최대 자릿수를 판단한다\n let m = *nums.into_iter().max().unwrap();\n // 낮은 자리에서 높은 자리 순서로 순회한다\n let mut exp = 1;\n while exp &lt;= m {\n counting_sort_digit(nums, exp);\n exp *= 10;\n }\n}\n</code></pre> radix_sort.c<pre><code>/* 원소 num의 k번째 자릿값 반환. 이때 exp = 10^(k-1)이다 */\nint digit(int num, int exp) {\n // k 대신 exp를 전달하면 여기서 비용이 큰 거듭제곱 계산을 반복하지 않아도 된다\n return (num / exp) % 10;\n}\n\n/* 계수 정렬(nums의 k번째 자릿값을 기준으로 정렬) */\nvoid countingSortDigit(int nums[], int size, int exp) {\n // 십진수의 각 자리는 0~9 범위이므로 길이가 10인 버킷 배열이 필요하다\n int *counter = (int *)malloc((sizeof(int) * 10));\n memset(counter, 0, sizeof(int) * 10); // 이후 메모리 해제를 지원하도록 0으로 초기화한다\n // 0~9 각 숫자의 등장 횟수를 센다\n for (int i = 0; i &lt; size; i++) {\n // nums[i]의 k번째 자릿값을 구하여 d로 표시한다\n int d = digit(nums[i], exp);\n // 숫자 d의 등장 횟수를 센다\n counter[d]++;\n }\n // 접두 합을 구하여 등장 개수를 배열 인덱스로 변환한다\n for (int i = 1; i &lt; 10; i++) {\n counter[i] += counter[i - 1];\n }\n // 역순으로 순회하며 버킷에 집계된 결과에 따라 각 원소를 res에 채운다\n int *res = (int *)malloc(sizeof(int) * size);\n for (int i = size - 1; i &gt;= 0; i--) {\n int d = digit(nums[i], exp);\n int j = counter[d] - 1; // 배열에서 d의 인덱스 j를 구한다\n res[j] = nums[i]; // 현재 원소를 인덱스 j에 채운다\n counter[d]--; // d의 개수를 1 줄인다\n }\n // 결과로 원래 배열 nums를 덮어쓴다\n for (int i = 0; i &lt; size; i++) {\n nums[i] = res[i];\n }\n // 메모리 해제\n free(res);\n free(counter);\n}\n\n/* 기수 정렬 */\nvoid radixSort(int nums[], int size) {\n // 배열의 최댓값을 구하여 최대 자릿수를 판단한다\n int max = INT32_MIN;\n for (int i = 0; i &lt; size; i++) {\n if (nums[i] &gt; max) {\n max = nums[i];\n }\n }\n // 낮은 자리에서 높은 자리 순서로 순회한다\n for (int exp = 1; max &gt;= exp; exp *= 10)\n // 배열 원소의 k번째 자릿값에 대해 계수 정렬을 수행한다\n // k = 1 -&gt; exp = 1\n // k = 2 -&gt; exp = 10\n // 즉, exp = 10^(k-1)이다\n countingSortDigit(nums, size, exp);\n}\n</code></pre> radix_sort.kt<pre><code>/* 원소 num의 k번째 자릿값 반환. 이때 exp = 10^(k-1)이다 */\nfun digit(num: Int, exp: Int): Int {\n // k 대신 exp를 전달하면 여기서 비용이 큰 거듭제곱 계산을 반복하지 않아도 된다\n return (num / exp) % 10\n}\n\n/* 계수 정렬(nums의 k번째 자릿값을 기준으로 정렬) */\nfun countingSortDigit(nums: IntArray, exp: Int) {\n // 십진수의 각 자리는 0~9 범위이므로 길이가 10인 버킷 배열이 필요하다\n val counter = IntArray(10)\n val n = nums.size\n // 0~9 각 숫자의 등장 횟수를 센다\n for (i in 0..&lt;n) {\n val d = digit(nums[i], exp) // nums[i]의 k번째 자릿값을 구하여 d로 표시한다\n counter[d]++ // 숫자 d의 등장 횟수를 센다\n }\n // 접두 합을 구하여 등장 개수를 배열 인덱스로 변환한다\n for (i in 1..9) {\n counter[i] += counter[i - 1]\n }\n // 역순으로 순회하며 버킷에 집계된 결과에 따라 각 원소를 res에 채운다\n val res = IntArray(n)\n for (i in n - 1 downTo 0) {\n val d = digit(nums[i], exp)\n val j = counter[d] - 1 // 배열에서 d의 인덱스 j를 구한다\n res[j] = nums[i] // 현재 원소를 인덱스 j에 채운다\n counter[d]-- // d의 개수를 1 줄인다\n }\n // 결과로 원래 배열 nums를 덮어쓴다\n for (i in 0..&lt;n)\n nums[i] = res[i]\n}\n\n/* 기수 정렬 */\nfun radixSort(nums: IntArray) {\n // 배열의 최댓값을 구하여 최대 자릿수를 판단한다\n var m = Int.MIN_VALUE\n for (num in nums) if (num &gt; m) m = num\n var exp = 1\n // 낮은 자리에서 높은 자리 순서로 순회한다\n while (exp &lt;= m) {\n // 배열 원소의 k번째 자릿값에 대해 계수 정렬을 수행한다\n // k = 1 -&gt; exp = 1\n // k = 2 -&gt; exp = 10\n // 즉, exp = 10^(k-1)이다\n countingSortDigit(nums, exp)\n exp *= 10\n }\n}\n</code></pre> radix_sort.rb<pre><code>### 원소 num의 k번째 자릿값 반환. 이때 exp = 10^(k-1)이다 ###\ndef digit(num, exp)\n # k 대신 exp를 전달하면 여기서 비용이 큰 거듭제곱 계산을 반복하지 않아도 된다\n (num / exp) % 10\nend\n\n### 계수 정렬(nums의 k번째 자릿값을 기준으로 정렬)###\ndef counting_sort_digit(nums, exp)\n # 십진수의 각 자리는 0~9 범위이므로 길이가 10인 버킷 배열이 필요하다\n counter = Array.new(10, 0)\n n = nums.length\n # 0~9 각 숫자의 등장 횟수를 센다\n for i in 0...n\n d = digit(nums[i], exp) # nums[i]의 k번째 자릿값을 구하여 d로 표시한다\n counter[d] += 1 # 숫자 d의 등장 횟수를 센다\n end\n # 접두 합을 구하여 등장 개수를 배열 인덱스로 변환한다\n (1...10).each { |i| counter[i] += counter[i - 1] }\n # 역순으로 순회하며 버킷에 집계된 결과에 따라 각 원소를 res에 채운다\n res = Array.new(n, 0)\n for i in (n - 1).downto(0)\n d = digit(nums[i], exp)\n j = counter[d] - 1 # 배열에서 d의 인덱스 j를 구한다\n res[j] = nums[i] # 현재 원소를 인덱스 j에 채운다\n counter[d] -= 1 # d의 개수를 1 줄인다\n end\n # 결과로 원래 배열 nums를 덮어쓴다\n (0...n).each { |i| nums[i] = res[i] }\nend\n\n### 기수 정렬 ###\ndef radix_sort(nums)\n # 배열의 최댓값을 구하여 최대 자릿수를 판단한다\n m = nums.max\n # 낮은 자리에서 높은 자리 순서로 순회한다\n exp = 1\n while exp &lt;= m\n # 배열 원소의 k번째 자릿값에 대해 계수 정렬을 수행한다\n # k = 1 -&gt; exp = 1\n # k = 2 -&gt; exp = 10\n # 즉, exp = 10^(k-1)이다\n counting_sort_digit(nums, exp)\n exp *= 10\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>왜 가장 낮은 자리부터 정렬하는가?</p> <p>여러 차례 연속으로 정렬하면 뒤의 정렬 결과가 앞의 결과를 덮어쓴다. 예를 들어 첫 번째 정렬에서는 \\(a &lt; b\\)였지만 두 번째 정렬에서는 \\(a &gt; b\\)라면, 두 번째 결과가 첫 번째 결과를 대신한다. 높은 자릿수가 낮은 자릿수보다 우선하므로 낮은 자리부터 정렬한 뒤 높은 자리를 정렬해야 한다.</p>","path":["제 11 장   정렬","11.10   기수 정렬"],"tags":[]},{"location":"chapter_sorting/radix_sort/#11102","level":2,"title":"11.10.2   알고리즘 특성","text":"<p>기수 정렬은 계수 정렬보다 값의 범위가 큰 경우에 적합하다. 단, 데이터를 고정된 자릿수의 형식으로 표현할 수 있어야 하며 자릿수가 지나치게 크면 안 된다. 예를 들어 부동 소수점 수는 자릿수 \\(k\\)가 너무 커서 시간 복잡도가 \\(O(nk) \\gg O(n^2)\\)이 될 수 있으므로 기수 정렬에 적합하지 않다.</p> <ul> <li>시간 복잡도 \\(O(nk)\\), 비적응형 정렬: 데이터 수가 \\(n\\), 진법이 \\(d\\), 최대 자릿수가 \\(k\\)라면 한 자리를 계수 정렬하는 데 \\(O(n + d)\\) 시간이 걸리고, \\(k\\) 자리 전체를 정렬하는 데 \\(O((n + d)k)\\) 시간이 걸린다. 일반적으로 \\(d\\)와 \\(k\\)는 모두 비교적 작으므로 시간 복잡도는 \\(O(n)\\)에 가까워진다.</li> <li>공간 복잡도 \\(O(n + d)\\), 제자리 정렬이 아님: 계수 정렬과 마찬가지로 길이가 \\(n\\)과 \\(d\\)인 배열 <code>res</code>와 <code>counter</code>가 필요하다.</li> <li>안정 정렬: 계수 정렬이 안정적이면 기수 정렬도 안정적이다. 계수 정렬이 불안정하면 기수 정렬은 올바른 정렬 결과를 보장할 수 없다.</li> </ul>","path":["제 11 장   정렬","11.10   기수 정렬"],"tags":[]},{"location":"chapter_sorting/selection_sort/","level":1,"title":"11.2   선택 정렬","text":"<p>선택 정렬(selection sort)의 원리는 매우 간단하다. 반복문을 실행하여 매번 정렬되지 않은 구간에서 가장 작은 원소를 고른 뒤 정렬된 구간의 끝에 놓는다.</p> <p>배열의 길이가 \\(n\\)일 때 선택 정렬의 과정은 아래 그림(11-2)과 같다.</p> <ol> <li>처음에는 모든 원소가 정렬되지 않았으므로 미정렬 구간(인덱스)은 \\([0, n-1]\\)이다.</li> <li>구간 \\([0, n-1]\\)에서 가장 작은 원소를 골라 인덱스 \\(0\\)의 원소와 교환한다. 그러면 배열의 앞 1개 원소가 정렬된다.</li> <li>구간 \\([1, n-1]\\)에서 가장 작은 원소를 골라 인덱스 \\(1\\)의 원소와 교환한다. 그러면 배열의 앞 2개 원소가 정렬된다.</li> <li>같은 과정을 반복한다. \\(n - 1\\) 번 선택하고 교환하면 배열의 앞 \\(n - 1\\) 개 원소가 정렬된다.</li> <li>남은 원소 하나는 반드시 가장 큰 원소이므로 정렬할 필요가 없으며, 배열 정렬이 완료된다.</li> </ol> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt;&lt;6&gt;&lt;7&gt;&lt;8&gt;&lt;9&gt;&lt;10&gt;&lt;11&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 11-2   선택 정렬 단계 </p> <p>코드에서는 \\(k\\)로 미정렬 구간의 최소 원소를 기록한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby selection_sort.py<pre><code>def selection_sort(nums: list[int]):\n \"\"\"선택 정렬\"\"\"\n n = len(nums)\n # 바깥쪽 반복문: 정렬되지 않은 구간은 [i, n-1]이다\n for i in range(n - 1):\n # 안쪽 반복문: 정렬되지 않은 구간의 최솟값을 찾는다\n k = i\n for j in range(i + 1, n):\n if nums[j] &lt; nums[k]:\n k = j # 최솟값의 인덱스를 기록한다\n # 이 최솟값과 정렬되지 않은 구간의 첫 원소를 교환한다\n nums[i], nums[k] = nums[k], nums[i]\n</code></pre> selection_sort.cpp<pre><code>/* 선택 정렬 */\nvoid selectionSort(vector&lt;int&gt; &amp;nums) {\n int n = nums.size();\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [i, n-1]이다\n for (int i = 0; i &lt; n - 1; i++) {\n // 안쪽 반복문: 정렬되지 않은 구간의 최솟값을 찾는다\n int k = i;\n for (int j = i + 1; j &lt; n; j++) {\n if (nums[j] &lt; nums[k])\n k = j; // 최솟값의 인덱스를 기록한다\n }\n // 이 최솟값과 정렬되지 않은 구간의 첫 원소를 교환한다\n swap(nums[i], nums[k]);\n }\n}\n</code></pre> selection_sort.java<pre><code>/* 선택 정렬 */\nvoid selectionSort(int[] nums) {\n int n = nums.length;\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [i, n-1]이다\n for (int i = 0; i &lt; n - 1; i++) {\n // 안쪽 반복문: 정렬되지 않은 구간의 최솟값을 찾는다\n int k = i;\n for (int j = i + 1; j &lt; n; j++) {\n if (nums[j] &lt; nums[k])\n k = j; // 최솟값의 인덱스를 기록한다\n }\n // 이 최솟값과 정렬되지 않은 구간의 첫 원소를 교환한다\n int temp = nums[i];\n nums[i] = nums[k];\n nums[k] = temp;\n }\n}\n</code></pre> selection_sort.cs<pre><code>/* 선택 정렬 */\nvoid SelectionSort(int[] nums) {\n int n = nums.Length;\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [i, n-1]이다\n for (int i = 0; i &lt; n - 1; i++) {\n // 안쪽 반복문: 정렬되지 않은 구간의 최솟값을 찾는다\n int k = i;\n for (int j = i + 1; j &lt; n; j++) {\n if (nums[j] &lt; nums[k])\n k = j; // 최솟값의 인덱스를 기록한다\n }\n // 이 최솟값과 정렬되지 않은 구간의 첫 원소를 교환한다\n (nums[k], nums[i]) = (nums[i], nums[k]);\n }\n}\n</code></pre> selection_sort.go<pre><code>/* 선택 정렬 */\nfunc selectionSort(nums []int) {\n n := len(nums)\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [i, n-1]이다\n for i := 0; i &lt; n-1; i++ {\n // 안쪽 반복문: 정렬되지 않은 구간의 최솟값을 찾는다\n k := i\n for j := i + 1; j &lt; n; j++ {\n if nums[j] &lt; nums[k] {\n // 최솟값의 인덱스를 기록한다\n k = j\n }\n }\n // 이 최솟값과 정렬되지 않은 구간의 첫 원소를 교환한다\n nums[i], nums[k] = nums[k], nums[i]\n\n }\n}\n</code></pre> selection_sort.swift<pre><code>/* 선택 정렬 */\nfunc selectionSort(nums: inout [Int]) {\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [i, n-1]이다\n for i in nums.indices.dropLast() {\n // 안쪽 반복문: 정렬되지 않은 구간의 최솟값을 찾는다\n var k = i\n for j in nums.indices.dropFirst(i + 1) {\n if nums[j] &lt; nums[k] {\n k = j // 최솟값의 인덱스를 기록한다\n }\n }\n // 이 최솟값과 정렬되지 않은 구간의 첫 원소를 교환한다\n nums.swapAt(i, k)\n }\n}\n</code></pre> selection_sort.js<pre><code>/* 선택 정렬 */\nfunction selectionSort(nums) {\n let n = nums.length;\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [i, n-1]이다\n for (let i = 0; i &lt; n - 1; i++) {\n // 안쪽 반복문: 정렬되지 않은 구간의 최솟값을 찾는다\n let k = i;\n for (let j = i + 1; j &lt; n; j++) {\n if (nums[j] &lt; nums[k]) {\n k = j; // 최솟값의 인덱스를 기록한다\n }\n }\n // 이 최솟값과 정렬되지 않은 구간의 첫 원소를 교환한다\n [nums[i], nums[k]] = [nums[k], nums[i]];\n }\n}\n</code></pre> selection_sort.ts<pre><code>/* 선택 정렬 */\nfunction selectionSort(nums: number[]): void {\n let n = nums.length;\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [i, n-1]이다\n for (let i = 0; i &lt; n - 1; i++) {\n // 안쪽 반복문: 정렬되지 않은 구간의 최솟값을 찾는다\n let k = i;\n for (let j = i + 1; j &lt; n; j++) {\n if (nums[j] &lt; nums[k]) {\n k = j; // 최솟값의 인덱스를 기록한다\n }\n }\n // 이 최솟값과 정렬되지 않은 구간의 첫 원소를 교환한다\n [nums[i], nums[k]] = [nums[k], nums[i]];\n }\n}\n</code></pre> selection_sort.dart<pre><code>/* 선택 정렬 */\nvoid selectionSort(List&lt;int&gt; nums) {\n int n = nums.length;\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [i, n-1]이다\n for (int i = 0; i &lt; n - 1; i++) {\n // 안쪽 반복문: 정렬되지 않은 구간의 최솟값을 찾는다\n int k = i;\n for (int j = i + 1; j &lt; n; j++) {\n if (nums[j] &lt; nums[k]) k = j; // 최솟값의 인덱스를 기록한다\n }\n // 이 최솟값과 정렬되지 않은 구간의 첫 원소를 교환한다\n int temp = nums[i];\n nums[i] = nums[k];\n nums[k] = temp;\n }\n}\n</code></pre> selection_sort.rs<pre><code>/* 선택 정렬 */\nfn selection_sort(nums: &amp;mut [i32]) {\n if nums.is_empty() {\n return;\n }\n let n = nums.len();\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [i, n-1]이다\n for i in 0..n - 1 {\n // 안쪽 반복문: 정렬되지 않은 구간의 최솟값을 찾는다\n let mut k = i;\n for j in i + 1..n {\n if nums[j] &lt; nums[k] {\n k = j; // 최솟값의 인덱스를 기록한다\n }\n }\n // 이 최솟값과 정렬되지 않은 구간의 첫 원소를 교환한다\n nums.swap(i, k);\n }\n}\n</code></pre> selection_sort.c<pre><code>/* 선택 정렬 */\nvoid selectionSort(int nums[], int n) {\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [i, n-1]이다\n for (int i = 0; i &lt; n - 1; i++) {\n // 안쪽 반복문: 정렬되지 않은 구간의 최솟값을 찾는다\n int k = i;\n for (int j = i + 1; j &lt; n; j++) {\n if (nums[j] &lt; nums[k])\n k = j; // 최솟값의 인덱스를 기록한다\n }\n // 이 최솟값과 정렬되지 않은 구간의 첫 원소를 교환한다\n int temp = nums[i];\n nums[i] = nums[k];\n nums[k] = temp;\n }\n}\n</code></pre> selection_sort.kt<pre><code>/* 선택 정렬 */\nfun selectionSort(nums: IntArray) {\n val n = nums.size\n // 바깥쪽 반복문: 정렬되지 않은 구간은 [i, n-1]이다\n for (i in 0..&lt;n - 1) {\n var k = i\n // 안쪽 반복문: 정렬되지 않은 구간의 최솟값을 찾는다\n for (j in i + 1..&lt;n) {\n if (nums[j] &lt; nums[k])\n k = j // 최솟값의 인덱스를 기록한다\n }\n // 이 최솟값과 정렬되지 않은 구간의 첫 원소를 교환한다\n val temp = nums[i]\n nums[i] = nums[k]\n nums[k] = temp\n }\n}\n</code></pre> selection_sort.rb<pre><code>### 선택 정렬 ###\ndef selection_sort(nums)\n n = nums.length\n # 바깥쪽 반복문: 정렬되지 않은 구간은 [i, n-1]이다\n for i in 0...(n - 1)\n # 안쪽 반복문: 정렬되지 않은 구간의 최솟값을 찾는다\n k = i\n for j in (i + 1)...n\n if nums[j] &lt; nums[k]\n k = j # 최솟값의 인덱스를 기록한다\n end\n end\n # 이 최솟값과 정렬되지 않은 구간의 첫 원소를 교환한다\n nums[i], nums[k] = nums[k], nums[i]\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 11 장   정렬","11.2   선택 정렬"],"tags":[]},{"location":"chapter_sorting/selection_sort/#1121","level":2,"title":"11.2.1   알고리즘 특성","text":"<ul> <li>시간 복잡도 \\(O(n^2)\\), 비적응형 정렬: 바깥 반복문은 \\(n - 1\\) 번 실행된다. 안쪽 반복문은 첫 회에 \\(n - 1\\) 번, 마지막 회에 \\(1\\) 번 실행된다. 즉, 각 회의 실행 횟수는 \\(n - 1\\), \\(n - 2\\), \\(\\dots\\), \\(2\\), \\(1\\)이고 합은 \\(\\frac{n(n - 1)}{2}\\)이다.</li> <li>공간 복잡도 \\(O(1)\\), 제자리 정렬: 포인터 \\(i\\)와 \\(j\\)는 상수 크기의 추가 공간을 사용한다.</li> <li>불안정 정렬: 아래 그림(11-3)과 같이 원소 <code>nums[i]</code>가 자신과 값이 같은 원소의 오른쪽으로 교환되어 두 원소의 상대적 순서가 바뀔 수 있다.</li> </ul> <p> 그림 11-3   선택 정렬의 불안정성 예시 </p>","path":["제 11 장   정렬","11.2   선택 정렬"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/","level":1,"title":"11.1   정렬 알고리즘","text":"<p>정렬 알고리즘(sorting algorithm)은 데이터 집합을 특정 순서로 나열하는 데 사용한다. 정렬된 데이터는 대체로 더 효율적으로 탐색, 분석, 처리할 수 있으므로 정렬 알고리즘은 널리 활용된다.</p> <p>아래 그림(11-1)과 같이 정렬할 데이터의 타입은 정수, 부동 소수점 수, 문자, 문자열 등 다양하다. 정렬 기준도 수의 크기, 문자의 ASCII 코드 순서, 사용자 정의 규칙 등 필요에 따라 정할 수 있다.</p> <p></p> <p> 그림 11-1   데이터 타입과 정렬 기준의 예 </p>","path":["제 11 장   정렬","11.1   정렬 알고리즘"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/#1111","level":2,"title":"11.1.1   평가 기준","text":"<p>실행 효율: 정렬 알고리즘은 시간 복잡도가 가능한 한 낮고, 전체 연산 수도 적기를 기대한다(시간 복잡도의 상수항이 작아야 한다). 데이터가 많을 때는 실행 효율이 특히 중요하다.</p> <p>제자리 여부: 이름 그대로 제자리 정렬은 추가 보조 배열 없이 원래 배열을 직접 조작하여 정렬하므로 메모리를 절약한다. 보통 데이터 이동 연산이 적어 실행 속도도 더 빠르다.</p> <p>안정성: 안정 정렬은 정렬 후에도 값이 같은 원소들의 배열 내 상대적 순서가 바뀌지 않는다.</p> <p>안정 정렬은 여러 기준으로 순차 정렬할 때 필요한 조건이다. 학생 정보를 저장한 표의 첫 번째 열이 이름, 두 번째 열이 나이라고 하자. 이때 불안정 정렬을 사용하면 입력 데이터가 기존에 정렬되어 있던 순서가 깨질 수 있다.</p> <pre><code># 입력 데이터는 이름순으로 정렬되어 있다\n# (name, age)\n ('A', 19)\n ('B', 18)\n ('C', 21)\n ('D', 19)\n ('E', 23)\n\n# 불안정 정렬 알고리즘으로 리스트를 나이순으로 정렬하면,\n# 결과에서 ('D', 19)와 ('A', 19)의 상대적 위치가 바뀌어,\n# 입력 데이터의 이름순 정렬 상태가 사라질 수 있다\n ('B', 18)\n ('D', 19)\n ('A', 19)\n ('C', 21)\n ('E', 23)\n</code></pre> <p>적응성: 적응형 정렬은 입력 데이터에 이미 존재하는 순서 정보를 이용해 계산량을 줄이고 시간 효율을 높일 수 있다. 적응형 정렬 알고리즘의 최선 시간 복잡도는 보통 평균 시간 복잡도보다 낮다.</p> <p>비교 기반 여부: 비교 기반 정렬은 비교 연산자(\\(&lt;\\), \\(=\\), \\(&gt;\\))로 원소의 상대적 순서를 판단하여 배열 전체를 정렬한다. 최악의 시간 복잡도 하한은 \\(\\Omega(n \\log n)\\)이다. 반면 비교를 사용하지 않는 정렬은 비교 연산자를 사용하지 않으며 시간 복잡도가 \\(O(n)\\)에 이를 수 있지만, 범용성은 상대적으로 낮다.</p>","path":["제 11 장   정렬","11.1   정렬 알고리즘"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/#1112","level":2,"title":"11.1.2   이상적인 정렬 알고리즘","text":"<p>빠른 실행, 제자리 정렬, 안정성, 적응성, 높은 범용성을 모두 갖추면 이상적이다. 하지만 지금까지 이러한 특성을 모두 갖춘 정렬 알고리즘은 발견되지 않았다. 따라서 데이터의 구체적인 특성과 문제의 요구에 맞추어 정렬 알고리즘을 선택해야 한다.</p> <p>이제 다양한 정렬 알고리즘을 배우고, 위 평가 기준에 따라 각각의 장단점을 분석해 보자.</p>","path":["제 11 장   정렬","11.1   정렬 알고리즘"],"tags":[]},{"location":"chapter_sorting/summary/","level":1,"title":"11.11   요약","text":"","path":["제 11 장   정렬","11.11   요약"],"tags":[]},{"location":"chapter_sorting/summary/#1","level":3,"title":"1.   핵심 정리","text":"<ul> <li>버블 정렬은 인접한 원소를 교환하여 정렬한다. 플래그를 추가해 조기에 반환하도록 하면 최선 시간 복잡도를 \\(O(n)\\)으로 최적화할 수 있다.</li> <li>삽입 정렬은 매번 미정렬 구간의 원소를 정렬된 구간의 올바른 위치에 삽입한다. 시간 복잡도는 \\(O(n^2)\\)이지만 기본 연산 수가 비교적 적어, 데이터가 적은 정렬 작업에서 매우 많이 사용된다.</li> <li>퀵 정렬은 센티널 분할 연산으로 정렬한다. 분할할 때마다 최악의 피벗을 고르면 시간 복잡도가 \\(O(n^2)\\)으로 저하될 수 있다. 중앙값이나 무작위 원소를 피벗으로 사용하면 이런 확률을 줄일 수 있다. 더 짧은 부분 구간에 먼저 재귀하면 재귀 깊이를 효과적으로 줄여 공간 복잡도를 \\(O(\\log n)\\)으로 최적화할 수 있다.</li> <li>병합 정렬은 분할과 병합의 두 단계로 이루어지며 분할 정복 전략을 전형적으로 보여 준다. 배열을 정렬할 때는 보조 배열이 필요하여 공간 복잡도가 \\(O(n)\\)이지만, 연결 리스트 정렬은 \\(O(1)\\)까지 최적화할 수 있다.</li> <li>버킷 정렬은 데이터를 버킷에 분배하고, 각 버킷 안에서 정렬하고, 결과를 합치는 세 단계로 이루어진다. 역시 분할 정복 전략을 보여 주며 매우 큰 데이터에 적합하다. 핵심은 데이터를 고르게 분배하는 것이다.</li> <li>계수 정렬은 버킷 정렬의 특수한 경우로, 데이터의 등장 횟수를 세어 정렬한다. 데이터 수는 많지만 값의 범위가 제한된 경우에 적합하며, 데이터를 양의 정수로 변환할 수 있어야 한다.</li> <li>기수 정렬은 자릿수별로 정렬하여 전체 데이터를 정렬하며, 데이터를 고정된 자릿수의 수로 표현할 수 있어야 한다.</li> <li>우리는 효율, 안정성, 제자리 동작, 적응성 등의 장점을 갖춘 정렬 알고리즘을 원한다. 하지만 다른 자료 구조와 알고리즘처럼 이 모든 조건을 동시에 만족하는 정렬 알고리즘은 없다. 실제 응용에서는 데이터의 특성에 따라 적합한 알고리즘을 선택해야 한다.</li> <li>아래 그림(11-19)은 주요 정렬 알고리즘의 효율, 안정성, 제자리 여부, 적응성 등을 비교한다.</li> </ul> <p> 그림 11-19   정렬 알고리즘 비교 </p>","path":["제 11 장   정렬","11.11   요약"],"tags":[]},{"location":"chapter_sorting/summary/#2-q-a","level":3,"title":"2.   Q &amp; A","text":"<p>Q: 정렬 알고리즘의 안정성은 어떤 상황에서 필요한가?</p> <p>실제로 객체의 특정 속성을 기준으로 정렬하는 경우가 있다. 예를 들어 학생에게 이름과 키라는 두 속성이 있고, 여러 기준으로 순차 정렬하고 싶다고 하자. 먼저 이름순으로 정렬하여 <code>(A, 180) (B, 185) (C, 170) (D, 170)</code>을 얻은 뒤 키순으로 정렬한다. 정렬 알고리즘이 불안정하면 <code>(D, 170) (C, 170) (A, 180) (B, 185)</code>가 나올 수 있다.</p> <p>학생 D와 C의 위치가 바뀌어 이름순 정렬이 깨졌음을 알 수 있으며, 이는 원하는 결과가 아니다.</p> <p>Q: 센티널 분할에서 '오른쪽에서 왼쪽으로 찾기'와 '왼쪽에서 오른쪽으로 찾기'의 순서를 바꿔도 되는가?</p> <p>안 된다. 맨 왼쪽 원소를 피벗으로 선택하면 반드시 '오른쪽에서 왼쪽으로 찾기'를 먼저 수행하고, 그다음 '왼쪽에서 오른쪽으로 찾기'를 수행해야 한다. 직관과 다르게 느껴질 수 있으니 이유를 분석해 보자.</p> <p>분할 함수 <code>partition()</code>의 마지막 단계는 <code>nums[left]</code>와 <code>nums[i]</code>를 교환하는 것이다. 교환 후에는 피벗 왼쪽의 모든 원소에 대해 '원소 <code>&lt;=</code> 피벗'이 성립해야 하므로, 마지막 교환 전에 반드시 <code>nums[left] &gt;= nums[i]</code>가 성립해야 한다. 먼저 '왼쪽에서 오른쪽으로 찾기'를 수행했는데 피벗보다 큰 원소를 찾지 못한다면, <code>i == j</code>일 때 반복문을 빠져나오며 이때 <code>nums[j] == nums[i] &gt; nums[left]</code>일 수 있다. 즉, 마지막 교환이 피벗보다 큰 원소를 맨 왼쪽으로 옮겨 분할을 실패하게 할 수 있다.</p> <p>예를 들어 배열이 <code>[0, 0, 0, 0, 1]</code>일 때 '왼쪽에서 오른쪽으로 찾기'를 먼저 하면, 분할 결과가 <code>[1, 0, 0, 0, 0]</code>으로 잘못 나온다.</p> <p>더 생각해 보면 <code>nums[right]</code>를 피벗으로 선택했을 때는 반대로 '왼쪽에서 오른쪽으로 찾기'를 먼저 해야 한다.</p> <p>Q: 퀵 정렬의 재귀 깊이를 최적화할 때 짧은 배열을 선택하면 왜 깊이가 \\(\\log n\\)을 넘지 않는가?</p> <p>재귀 깊이는 현재 아직 반환되지 않은 재귀 메서드의 수다. 매 분할에서 원래 배열을 두 부분 배열로 나눈다. 재귀 깊이를 최적화하면 아래로 재귀하는 부분 배열의 길이는 최대 원래 길이의 절반이다. 최악의 경우 매번 정확히 절반이라고 해도 최종 재귀 깊이는 \\(\\log n\\)이다.</p> <p>원래 퀵 정렬에서는 더 긴 배열에 계속 재귀할 수 있다. 최악의 경우 길이가 \\(n\\), \\(n - 1\\), \\(\\dots\\), \\(2\\), \\(1\\)로 줄어 재귀 깊이가 \\(n\\)이 된다. 재귀 깊이 최적화는 이 상황을 방지한다.</p> <p>Q: 배열의 모든 원소가 같으면 퀵 정렬의 시간 복잡도는 \\(O(n^2)\\)인가? 이런 퇴화는 어떻게 처리하는가?</p> <p>그렇다. 이 경우 배열을 피벗보다 작은 부분, 같은 부분, 큰 부분의 세 부분으로 나누고 작은 부분과 큰 부분에만 재귀하는 방법을 고려할 수 있다. 이렇게 하면 모든 원소가 같은 입력 배열은 분할 한 번으로 정렬된다.</p> <p>Q: 버킷 정렬의 최악 시간 복잡도는 왜 \\(O(n^2)\\)인가?</p> <p>최악의 경우 모든 원소가 같은 버킷에 들어간다. 이 원소들을 \\(O(n^2)\\) 알고리즘으로 정렬한다면 시간 복잡도는 \\(O(n^2)\\)이다.</p>","path":["제 11 장   정렬","11.11   요약"],"tags":[]},{"location":"chapter_stack_and_queue/","level":1,"title":"제 5 장   스택과 큐","text":"<p>이 장을 시작하며</p> <p>스택은 고양이들이 포개져 있는 모습과 같고, 큐는 고양이들이 줄을 서 있는 모습과 같다.</p> <p>둘은 각각 후입선출과 선입선출의 논리적 관계를 나타낸다.</p>","path":["제 5 장   스택과 큐"],"tags":[]},{"location":"chapter_stack_and_queue/#_1","level":2,"title":"이 장의 내용","text":"<ul> <li>5.1   스택</li> <li>5.2   큐</li> <li>5.3   덱</li> <li>5.4   요약</li> <li>5.5   연습 문제</li> </ul>","path":["제 5 장   스택과 큐"],"tags":[]},{"location":"chapter_stack_and_queue/deque/","level":1,"title":"5.3   덱","text":"<p>큐에서는 맨 앞 원소를 삭제하거나 맨 뒤에 원소를 추가하는 것만 가능하다. 아래 그림(5-7)의 덱(double-ended queue)은 앞뒤 양쪽에서 원소를 추가하고 삭제할 수 있어 더 유연하다.</p> <p></p> <p> 그림 5-7   덱의 연산 </p>","path":["제 5 장   스택과 큐","5.3   덱"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#531","level":2,"title":"5.3.1   덱의 기본 연산","text":"<p>덱의 기본 연산은 아래 표(5-3)와 같다. 구체적인 메서드 이름은 사용하는 프로그래밍 언어에 따라 정해진다.</p> <p> 표 5-3   덱의 연산 효율 </p> 메서드 설명 시간 복잡도 <code>push_first()</code> 맨 앞에 원소 추가 \\(O(1)\\) <code>push_last()</code> 맨 뒤에 원소 추가 \\(O(1)\\) <code>pop_first()</code> 맨 앞 원소 삭제 \\(O(1)\\) <code>pop_last()</code> 맨 뒤 원소 삭제 \\(O(1)\\) <code>peek_first()</code> 맨 앞 원소에 접근 \\(O(1)\\) <code>peek_last()</code> 맨 뒤 원소에 접근 \\(O(1)\\) <p>마찬가지로 프로그래밍 언어가 제공하는 덱 클래스를 바로 사용할 수 있다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby deque.py<pre><code>from collections import deque\n\n# 덱 초기화\ndeq: deque[int] = deque()\n\n# 덱에 원소 삽입\ndeq.append(2) # 맨 뒤에 추가\ndeq.append(5)\ndeq.append(4)\ndeq.appendleft(3) # 맨 앞에 추가\ndeq.appendleft(1)\n\n# 원소 접근\nfront: int = deq[0] # 맨 앞 원소\nrear: int = deq[-1] # 맨 뒤 원소\n\n# 덱에서 원소 꺼내기\npop_front: int = deq.popleft() # 맨 앞 원소 꺼내기\npop_rear: int = deq.pop() # 맨 뒤 원소 꺼내기\n\n# 덱 길이 구하기\nsize: int = len(deq)\n\n# 덱이 비어 있는지 확인\nis_empty: bool = len(deq) == 0\n</code></pre> deque.cpp<pre><code>/* 덱 초기화 */\ndeque&lt;int&gt; deque;\n\n/* 덱에 원소 삽입 */\ndeque.push_back(2); // 맨 뒤에 추가\ndeque.push_back(5);\ndeque.push_back(4);\ndeque.push_front(3); // 맨 앞에 추가\ndeque.push_front(1);\n\n/* 원소 접근 */\nint front = deque.front(); // 맨 앞 원소\nint back = deque.back(); // 맨 뒤 원소\n\n/* 덱에서 원소 꺼내기 */\ndeque.pop_front(); // 맨 앞 원소 꺼내기\ndeque.pop_back(); // 맨 뒤 원소 꺼내기\n\n/* 덱 길이 구하기 */\nint size = deque.size();\n\n/* 덱이 비어 있는지 확인 */\nbool empty = deque.empty();\n</code></pre> deque.java<pre><code>/* 덱 초기화 */\nDeque&lt;Integer&gt; deque = new LinkedList&lt;&gt;();\n\n/* 덱에 원소 삽입 */\ndeque.offerLast(2); // 맨 뒤에 추가\ndeque.offerLast(5);\ndeque.offerLast(4);\ndeque.offerFirst(3); // 맨 앞에 추가\ndeque.offerFirst(1);\n\n/* 원소 접근 */\nint peekFirst = deque.peekFirst(); // 맨 앞 원소\nint peekLast = deque.peekLast(); // 맨 뒤 원소\n\n/* 덱에서 원소 꺼내기 */\nint popFirst = deque.pollFirst(); // 맨 앞 원소 꺼내기\nint popLast = deque.pollLast(); // 맨 뒤 원소 꺼내기\n\n/* 덱 길이 구하기 */\nint size = deque.size();\n\n/* 덱이 비어 있는지 확인 */\nboolean isEmpty = deque.isEmpty();\n</code></pre> deque.cs<pre><code>/* 덱 초기화 */\n// C#에서는 연결 리스트 LinkedList를 덱으로 사용한다\nLinkedList&lt;int&gt; deque = new();\n\n/* 덱에 원소 삽입 */\ndeque.AddLast(2); // 맨 뒤에 추가\ndeque.AddLast(5);\ndeque.AddLast(4);\ndeque.AddFirst(3); // 맨 앞에 추가\ndeque.AddFirst(1);\n\n/* 원소 접근 */\nint peekFirst = deque.First.Value; // 맨 앞 원소\nint peekLast = deque.Last.Value; // 맨 뒤 원소\n\n/* 덱에서 원소 꺼내기 */\ndeque.RemoveFirst(); // 맨 앞 원소 꺼내기\ndeque.RemoveLast(); // 맨 뒤 원소 꺼내기\n\n/* 덱 길이 구하기 */\nint size = deque.Count;\n\n/* 덱이 비어 있는지 확인 */\nbool isEmpty = deque.Count == 0;\n</code></pre> deque_test.go<pre><code>/* 덱 초기화 */\n// Go에서는 list를 덱으로 사용한다\ndeque := list.New()\n\n/* 덱에 원소 삽입 */\ndeque.PushBack(2) // 맨 뒤에 추가\ndeque.PushBack(5)\ndeque.PushBack(4)\ndeque.PushFront(3) // 맨 앞에 추가\ndeque.PushFront(1)\n\n/* 원소 접근 */\nfront := deque.Front() // 맨 앞 원소\nrear := deque.Back() // 맨 뒤 원소\n\n/* 덱에서 원소 꺼내기 */\ndeque.Remove(front) // 맨 앞 원소 꺼내기\ndeque.Remove(rear) // 맨 뒤 원소 꺼내기\n\n/* 덱 길이 구하기 */\nsize := deque.Len()\n\n/* 덱이 비어 있는지 확인 */\nisEmpty := deque.Len() == 0\n</code></pre> deque.swift<pre><code>/* 덱 초기화 */\n// Swift에는 내장 덱 클래스가 없으므로 Array를 덱으로 사용할 수 있다\nvar deque: [Int] = []\n\n/* 덱에 원소 삽입 */\ndeque.append(2) // 맨 뒤에 추가\ndeque.append(5)\ndeque.append(4)\ndeque.insert(3, at: 0) // 맨 앞에 추가\ndeque.insert(1, at: 0)\n\n/* 원소 접근 */\nlet peekFirst = deque.first! // 맨 앞 원소\nlet peekLast = deque.last! // 맨 뒤 원소\n\n/* 덱에서 원소 꺼내기 */\n// Array로 구현하면 popFirst의 복잡도는 O(n)이다\nlet popFirst = deque.removeFirst() // 맨 앞 원소 꺼내기\nlet popLast = deque.removeLast() // 맨 뒤 원소 꺼내기\n\n/* 덱 길이 구하기 */\nlet size = deque.count\n\n/* 덱이 비어 있는지 확인 */\nlet isEmpty = deque.isEmpty\n</code></pre> deque.js<pre><code>/* 덱 초기화 */\n// JavaScript에는 내장 덱이 없으므로 Array를 덱으로 사용해야 한다\nconst deque = [];\n\n/* 덱에 원소 삽입 */\ndeque.push(2);\ndeque.push(5);\ndeque.push(4);\n// 배열이므로 unshift() 메서드의 시간 복잡도는 O(n)임에 주의\ndeque.unshift(3);\ndeque.unshift(1);\n\n/* 원소 접근 */\nconst peekFirst = deque[0];\nconst peekLast = deque[deque.length - 1];\n\n/* 덱에서 원소 꺼내기 */\n// 배열이므로 shift() 메서드의 시간 복잡도는 O(n)임에 주의\nconst popFront = deque.shift();\nconst popBack = deque.pop();\n\n/* 덱 길이 구하기 */\nconst size = deque.length;\n\n/* 덱이 비어 있는지 확인 */\nconst isEmpty = size === 0;\n</code></pre> deque.ts<pre><code>/* 덱 초기화 */\n// TypeScript에는 내장 덱이 없으므로 Array를 덱으로 사용해야 한다\nconst deque: number[] = [];\n\n/* 덱에 원소 삽입 */\ndeque.push(2);\ndeque.push(5);\ndeque.push(4);\n// 배열이므로 unshift() 메서드의 시간 복잡도는 O(n)임에 주의\ndeque.unshift(3);\ndeque.unshift(1);\n\n/* 원소 접근 */\nconst peekFirst: number = deque[0];\nconst peekLast: number = deque[deque.length - 1];\n\n/* 덱에서 원소 꺼내기 */\n// 배열이므로 shift() 메서드의 시간 복잡도는 O(n)임에 주의\nconst popFront: number = deque.shift() as number;\nconst popBack: number = deque.pop() as number;\n\n/* 덱 길이 구하기 */\nconst size: number = deque.length;\n\n/* 덱이 비어 있는지 확인 */\nconst isEmpty: boolean = size === 0;\n</code></pre> deque.dart<pre><code>/* 덱 초기화 */\n// Dart의 Queue는 덱으로 정의되어 있다\nQueue&lt;int&gt; deque = Queue&lt;int&gt;();\n\n/* 덱에 원소 삽입 */\ndeque.addLast(2); // 맨 뒤에 추가\ndeque.addLast(5);\ndeque.addLast(4);\ndeque.addFirst(3); // 맨 앞에 추가\ndeque.addFirst(1);\n\n/* 원소 접근 */\nint peekFirst = deque.first; // 맨 앞 원소\nint peekLast = deque.last; // 맨 뒤 원소\n\n/* 덱에서 원소 꺼내기 */\nint popFirst = deque.removeFirst(); // 맨 앞 원소 꺼내기\nint popLast = deque.removeLast(); // 맨 뒤 원소 꺼내기\n\n/* 덱 길이 구하기 */\nint size = deque.length;\n\n/* 덱이 비어 있는지 확인 */\nbool isEmpty = deque.isEmpty;\n</code></pre> deque.rs<pre><code>/* 덱 초기화 */\nlet mut deque: VecDeque&lt;u32&gt; = VecDeque::new();\n\n/* 덱에 원소 삽입 */\ndeque.push_back(2); // 맨 뒤에 추가\ndeque.push_back(5);\ndeque.push_back(4);\ndeque.push_front(3); // 맨 앞에 추가\ndeque.push_front(1);\n\n/* 원소 접근 */\nif let Some(front) = deque.front() { // 맨 앞 원소\n}\nif let Some(rear) = deque.back() { // 맨 뒤 원소\n}\n\n/* 덱에서 원소 꺼내기 */\nif let Some(pop_front) = deque.pop_front() { // 맨 앞 원소 꺼내기\n}\nif let Some(pop_rear) = deque.pop_back() { // 맨 뒤 원소 꺼내기\n}\n\n/* 덱 길이 구하기 */\nlet size = deque.len();\n\n/* 덱이 비어 있는지 확인 */\nlet is_empty = deque.is_empty();\n</code></pre> deque.c<pre><code>// C는 내장 덱을 제공하지 않는다\n</code></pre> deque.kt<pre><code>/* 덱 초기화 */\nval deque = LinkedList&lt;Int&gt;()\n\n/* 덱에 원소 삽입 */\ndeque.offerLast(2) // 맨 뒤에 추가\ndeque.offerLast(5)\ndeque.offerLast(4)\ndeque.offerFirst(3) // 맨 앞에 추가\ndeque.offerFirst(1)\n\n/* 원소 접근 */\nval peekFirst = deque.peekFirst() // 맨 앞 원소\nval peekLast = deque.peekLast() // 맨 뒤 원소\n\n/* 덱에서 원소 꺼내기 */\nval popFirst = deque.pollFirst() // 맨 앞 원소 꺼내기\nval popLast = deque.pollLast() // 맨 뒤 원소 꺼내기\n\n/* 덱 길이 구하기 */\nval size = deque.size\n\n/* 덱이 비어 있는지 확인 */\nval isEmpty = deque.isEmpty()\n</code></pre> deque.rb<pre><code># 덱 초기화\n# Ruby에는 내장 덱이 없으므로 Array를 덱으로 사용해야 한다\ndeque = []\n\n# 덱에 원소 삽입\ndeque &lt;&lt; 2\ndeque &lt;&lt; 5\ndeque &lt;&lt; 4\n# 배열이므로 Array#unshift 메서드의 시간 복잡도는 O(n)임에 주의\ndeque.unshift(3)\ndeque.unshift(1)\n\n# 원소 접근\npeek_first = deque.first\npeek_last = deque.last\n\n# 덱에서 원소 꺼내기\n# 배열이므로 Array#shift 메서드의 시간 복잡도는 O(n)임에 주의\npop_front = deque.shift\npop_back = deque.pop\n\n# 덱 길이 구하기\nsize = deque.length\n\n# 덱이 비어 있는지 확인\nis_empty = size.zero?\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 5 장   스택과 큐","5.3   덱"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#532","level":2,"title":"5.3.2   덱 구현 *","text":"<p>덱의 구현은 큐와 비슷하며, 기반 자료 구조로 연결 리스트나 배열을 선택할 수 있다.</p>","path":["제 5 장   스택과 큐","5.3   덱"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#1","level":3,"title":"1.   이중 연결 리스트 기반 구현","text":"<p>앞 절에서는 일반적인 단일 연결 리스트로 큐를 구현했다. 머리 노드를 삭제하는 연산(큐에서 꺼내기)과 꼬리 노드 뒤에 새 노드를 추가하는 연산(큐에 삽입하기)을 쉽게 수행할 수 있기 때문이다.</p> <p>덱에서는 앞과 뒤 모두 삽입과 꺼내기를 수행할 수 있다. 즉, 큐와 대칭되는 반대 방향의 연산도 구현해야 한다. 이를 위해 ‘이중 연결 리스트’를 덱의 기반 자료 구조로 사용한다.</p> <p>아래 그림(5-8)과 같이 이중 연결 리스트의 머리 노드와 꼬리 노드를 덱의 맨 앞과 맨 뒤로 보고, 양 끝에서 노드를 추가하고 삭제하는 기능을 구현한다.</p> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 5-8   연결 리스트 기반 덱의 삽입과 꺼내기 </p> <p>구현 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_deque.py<pre><code>class ListNode:\n \"\"\"이중 연결 리스트 노드\"\"\"\n\n def __init__(self, val: int):\n \"\"\"생성자\"\"\"\n self.val: int = val\n self.next: ListNode | None = None # 다음 노드 참조\n self.prev: ListNode | None = None # 이전 노드 참조\n\nclass LinkedListDeque:\n \"\"\"이중 연결 리스트로 구현한 덱\"\"\"\n\n def __init__(self):\n \"\"\"생성자\"\"\"\n self._front: ListNode | None = None # 머리 노드 front\n self._rear: ListNode | None = None # 꼬리 노드 rear\n self._size: int = 0 # 덱의 길이\n\n def size(self) -&gt; int:\n \"\"\"덱의 길이 반환\"\"\"\n return self._size\n\n def is_empty(self) -&gt; bool:\n \"\"\"덱이 비어 있는지 판단\"\"\"\n return self._size == 0\n\n def push(self, num: int, is_front: bool):\n \"\"\"큐 삽입 연산\"\"\"\n node = ListNode(num)\n # 연결 리스트가 비어 있으면 front와 rear가 모두 node를 가리키게 한다\n if self.is_empty():\n self._front = self._rear = node\n # 맨 앞에 삽입하는 연산\n elif is_front:\n # node를 연결 리스트의 맨 앞에 추가한다\n self._front.prev = node\n node.next = self._front\n self._front = node # 머리 노드를 갱신한다\n # 맨 뒤에 삽입하는 연산\n else:\n # node를 연결 리스트의 맨 뒤에 추가한다\n self._rear.next = node\n node.prev = self._rear\n self._rear = node # 꼬리 노드를 갱신한다\n self._size += 1 # 큐 길이를 갱신한다\n\n def push_first(self, num: int):\n \"\"\"맨 앞에 삽입\"\"\"\n self.push(num, True)\n\n def push_last(self, num: int):\n \"\"\"맨 뒤에 삽입\"\"\"\n self.push(num, False)\n\n def pop(self, is_front: bool) -&gt; int:\n \"\"\"큐에서 꺼내는 연산\"\"\"\n if self.is_empty():\n raise IndexError(\"덱이 비어 있다\")\n # 큐의 길이가 1이면 머리와 꼬리가 같은 노드를 가리킨다\n if self._size == 1:\n val: int = self._front.val\n self._front = None\n self._rear = None\n # 맨 앞에서 꺼내는 연산\n elif is_front:\n val: int = self._front.val # 머리 노드의 값을 임시로 저장한다\n # 머리 노드를 삭제한다\n fnext: ListNode | None = self._front.next\n fnext.prev = None\n self._front.next = None\n self._front = fnext # 머리 노드를 갱신한다\n # 맨 뒤에서 꺼내는 연산\n else:\n val: int = self._rear.val # 꼬리 노드의 값을 임시로 저장한다\n # 꼬리 노드를 삭제한다\n rprev: ListNode | None = self._rear.prev\n rprev.next = None\n self._rear.prev = None\n self._rear = rprev # 꼬리 노드를 갱신한다\n self._size -= 1 # 큐 길이를 갱신한다\n return val\n\n def pop_first(self) -&gt; int:\n \"\"\"맨 앞에서 꺼내기\"\"\"\n return self.pop(True)\n\n def pop_last(self) -&gt; int:\n \"\"\"맨 뒤에서 꺼내기\"\"\"\n return self.pop(False)\n\n def peek_first(self) -&gt; int:\n \"\"\"맨 앞 원소 접근\"\"\"\n if self.is_empty():\n raise IndexError(\"덱이 비어 있다\")\n return self._front.val\n\n def peek_last(self) -&gt; int:\n \"\"\"맨 뒤 원소 접근\"\"\"\n if self.is_empty():\n raise IndexError(\"덱이 비어 있다\")\n return self._rear.val\n\n def to_array(self) -&gt; list[int]:\n \"\"\"출력에 사용할 배열 반환\"\"\"\n node = self._front\n res = [0] * self.size()\n for i in range(self.size()):\n res[i] = node.val\n node = node.next\n return res\n</code></pre> linkedlist_deque.cpp<pre><code>/* 이중 연결 리스트 노드 */\nstruct DoublyListNode {\n int val; // 노드 값\n DoublyListNode *next; // 다음 노드 포인터\n DoublyListNode *prev; // 이전 노드 포인터\n DoublyListNode(int val) : val(val), prev(nullptr), next(nullptr) {\n }\n};\n\n/* 이중 연결 리스트로 구현한 덱 */\nclass LinkedListDeque {\n private:\n DoublyListNode *front, *rear; // 머리 노드 front, 꼬리 노드 rear\n int queSize = 0; // 덱의 길이\n\n public:\n /* 생성자 */\n LinkedListDeque() : front(nullptr), rear(nullptr) {\n }\n\n /* 소멸자 */\n ~LinkedListDeque() {\n // 연결 리스트를 순회하며 노드를 삭제하여 메모리를 해제한다\n DoublyListNode *pre, *cur = front;\n while (cur != nullptr) {\n pre = cur;\n cur = cur-&gt;next;\n delete pre;\n }\n }\n\n /* 덱의 길이 반환 */\n int size() {\n return queSize;\n }\n\n /* 덱이 비어 있는지 판단 */\n bool isEmpty() {\n return size() == 0;\n }\n\n /* 큐 삽입 연산 */\n void push(int num, bool isFront) {\n DoublyListNode *node = new DoublyListNode(num);\n // 연결 리스트가 비어 있으면 front와 rear가 모두 node를 가리키게 한다\n if (isEmpty())\n front = rear = node;\n // 맨 앞에 삽입하는 연산\n else if (isFront) {\n // node를 연결 리스트의 맨 앞에 추가한다\n front-&gt;prev = node;\n node-&gt;next = front;\n front = node; // 머리 노드를 갱신한다\n // 맨 뒤에 삽입하는 연산\n } else {\n // node를 연결 리스트의 맨 뒤에 추가한다\n rear-&gt;next = node;\n node-&gt;prev = rear;\n rear = node; // 꼬리 노드를 갱신한다\n }\n queSize++; // 큐 길이를 갱신한다\n }\n\n /* 맨 앞에 삽입 */\n void pushFirst(int num) {\n push(num, true);\n }\n\n /* 맨 뒤에 삽입 */\n void pushLast(int num) {\n push(num, false);\n }\n\n /* 큐에서 꺼내는 연산 */\n int pop(bool isFront) {\n if (isEmpty())\n throw out_of_range(\"큐가 비어 있다\");\n int val;\n // 큐의 길이가 1이면 머리와 꼬리가 같은 노드를 가리킨다\n if (queSize == 1) {\n val = front-&gt;val;\n delete front; // 같은 노드를 가리키므로 하나만 해제하면 된다\n front = nullptr;\n rear = nullptr;\n }\n // 맨 앞에서 꺼내는 연산\n else if (isFront) {\n val = front-&gt;val; // 머리 노드의 값을 임시로 저장한다\n // 머리 노드를 삭제한다\n DoublyListNode *fNext = front-&gt;next;\n fNext-&gt;prev = nullptr;\n front-&gt;next = nullptr;\n delete front;\n front = fNext; // 머리 노드를 갱신한다\n // 맨 뒤에서 꺼내는 연산\n } else {\n val = rear-&gt;val; // 꼬리 노드의 값을 임시로 저장한다\n // 꼬리 노드를 삭제한다\n DoublyListNode *rPrev = rear-&gt;prev;\n rPrev-&gt;next = nullptr;\n rear-&gt;prev = nullptr;\n delete rear;\n rear = rPrev; // 꼬리 노드를 갱신한다\n }\n queSize--; // 큐 길이를 갱신한다\n return val;\n }\n\n /* 맨 앞에서 꺼내기 */\n int popFirst() {\n return pop(true);\n }\n\n /* 맨 뒤에서 꺼내기 */\n int popLast() {\n return pop(false);\n }\n\n /* 맨 앞 원소 접근 */\n int peekFirst() {\n if (isEmpty())\n throw out_of_range(\"덱이 비어 있다\");\n return front-&gt;val;\n }\n\n /* 맨 뒤 원소 접근 */\n int peekLast() {\n if (isEmpty())\n throw out_of_range(\"덱이 비어 있다\");\n return rear-&gt;val;\n }\n\n /* 출력에 사용할 배열 반환 */\n vector&lt;int&gt; toVector() {\n DoublyListNode *node = front;\n vector&lt;int&gt; res(size());\n for (int i = 0; i &lt; res.size(); i++) {\n res[i] = node-&gt;val;\n node = node-&gt;next;\n }\n return res;\n }\n};\n</code></pre> linkedlist_deque.java<pre><code>/* 이중 연결 리스트 노드 */\nclass ListNode {\n int val; // 노드 값\n ListNode next; // 다음 노드 참조\n ListNode prev; // 이전 노드 참조\n\n ListNode(int val) {\n this.val = val;\n prev = next = null;\n }\n}\n\n/* 이중 연결 리스트로 구현한 덱 */\nclass LinkedListDeque {\n private ListNode front, rear; // 머리 노드 front, 꼬리 노드 rear\n private int queSize = 0; // 덱의 길이\n\n public LinkedListDeque() {\n front = rear = null;\n }\n\n /* 덱의 길이 반환 */\n public int size() {\n return queSize;\n }\n\n /* 덱이 비어 있는지 판단 */\n public boolean isEmpty() {\n return size() == 0;\n }\n\n /* 큐 삽입 연산 */\n private void push(int num, boolean isFront) {\n ListNode node = new ListNode(num);\n // 연결 리스트가 비어 있으면 front와 rear가 모두 node를 가리키게 한다\n if (isEmpty())\n front = rear = node;\n // 맨 앞에 삽입하는 연산\n else if (isFront) {\n // node를 연결 리스트의 맨 앞에 추가한다\n front.prev = node;\n node.next = front;\n front = node; // 머리 노드를 갱신한다\n // 맨 뒤에 삽입하는 연산\n } else {\n // node를 연결 리스트의 맨 뒤에 추가한다\n rear.next = node;\n node.prev = rear;\n rear = node; // 꼬리 노드를 갱신한다\n }\n queSize++; // 큐 길이를 갱신한다\n }\n\n /* 맨 앞에 삽입 */\n public void pushFirst(int num) {\n push(num, true);\n }\n\n /* 맨 뒤에 삽입 */\n public void pushLast(int num) {\n push(num, false);\n }\n\n /* 큐에서 꺼내는 연산 */\n private int pop(boolean isFront) {\n if (isEmpty())\n throw new IndexOutOfBoundsException();\n int val;\n // 큐의 길이가 1이면 머리와 꼬리가 같은 노드를 가리킨다\n if (queSize == 1) {\n val = front.val;\n front = null;\n rear = null;\n }\n // 맨 앞에서 꺼내는 연산\n else if (isFront) {\n val = front.val; // 머리 노드의 값을 임시로 저장한다\n // 머리 노드를 삭제한다\n ListNode fNext = front.next;\n fNext.prev = null;\n front.next = null;\n front = fNext; // 머리 노드를 갱신한다\n // 맨 뒤에서 꺼내는 연산\n } else {\n val = rear.val; // 꼬리 노드의 값을 임시로 저장한다\n // 꼬리 노드를 삭제한다\n ListNode rPrev = rear.prev;\n rPrev.next = null;\n rear.prev = null;\n rear = rPrev; // 꼬리 노드를 갱신한다\n }\n queSize--; // 큐 길이를 갱신한다\n return val;\n }\n\n /* 맨 앞에서 꺼내기 */\n public int popFirst() {\n return pop(true);\n }\n\n /* 맨 뒤에서 꺼내기 */\n public int popLast() {\n return pop(false);\n }\n\n /* 맨 앞 원소 접근 */\n public int peekFirst() {\n if (isEmpty())\n throw new IndexOutOfBoundsException();\n return front.val;\n }\n\n /* 맨 뒤 원소 접근 */\n public int peekLast() {\n if (isEmpty())\n throw new IndexOutOfBoundsException();\n return rear.val;\n }\n\n /* 출력에 사용할 배열 반환 */\n public int[] toArray() {\n ListNode node = front;\n int[] res = new int[size()];\n for (int i = 0; i &lt; res.length; i++) {\n res[i] = node.val;\n node = node.next;\n }\n return res;\n }\n}\n</code></pre> linkedlist_deque.cs<pre><code>/* 이중 연결 리스트 노드 */\nclass ListNode(int val) {\n public int val = val; // 노드 값\n public ListNode? next = null; // 다음 노드 참조\n public ListNode? prev = null; // 이전 노드 참조\n}\n\n/* 이중 연결 리스트로 구현한 덱 */\nclass LinkedListDeque {\n ListNode? front, rear; // 머리 노드 front, 꼬리 노드 rear\n int queSize = 0; // 덱의 길이\n\n public LinkedListDeque() {\n front = null;\n rear = null;\n }\n\n /* 덱의 길이 반환 */\n public int Size() {\n return queSize;\n }\n\n /* 덱이 비어 있는지 판단 */\n public bool IsEmpty() {\n return Size() == 0;\n }\n\n /* 큐 삽입 연산 */\n void Push(int num, bool isFront) {\n ListNode node = new(num);\n // 연결 리스트가 비어 있으면 front와 rear가 모두 node를 가리키게 한다\n if (IsEmpty()) {\n front = node;\n rear = node;\n }\n // 맨 앞에 삽입하는 연산\n else if (isFront) {\n // node를 연결 리스트의 맨 앞에 추가한다\n front!.prev = node;\n node.next = front;\n front = node; // 머리 노드를 갱신한다 \n }\n // 맨 뒤에 삽입하는 연산\n else {\n // node를 연결 리스트의 맨 뒤에 추가한다\n rear!.next = node;\n node.prev = rear;\n rear = node; // 꼬리 노드를 갱신한다\n }\n\n queSize++; // 큐 길이를 갱신한다\n }\n\n /* 맨 앞에 삽입 */\n public void PushFirst(int num) {\n Push(num, true);\n }\n\n /* 맨 뒤에 삽입 */\n public void PushLast(int num) {\n Push(num, false);\n }\n\n /* 큐에서 꺼내는 연산 */\n int? Pop(bool isFront) {\n if (IsEmpty())\n throw new Exception();\n int? val;\n // 큐의 길이가 1이면 머리와 꼬리가 같은 노드를 가리킨다\n if (queSize == 1) {\n val = front!.val;\n front = null;\n rear = null;\n }\n // 맨 앞에서 꺼내는 연산\n else if (isFront) {\n val = front!.val; // 머리 노드의 값을 임시로 저장한다\n // 머리 노드를 삭제한다\n ListNode? fNext = front?.next;\n fNext!.prev = null;\n front.next = null;\n front = fNext; // 머리 노드를 갱신한다\n }\n // 맨 뒤에서 꺼내는 연산\n else {\n val = rear!.val; // 꼬리 노드의 값을 임시로 저장한다\n // 꼬리 노드를 삭제한다\n ListNode? rPrev = rear?.prev;\n rPrev!.next = null;\n rear.prev = null;\n rear = rPrev; // 꼬리 노드를 갱신한다\n }\n\n queSize--; // 큐 길이를 갱신한다\n return val;\n }\n\n /* 맨 앞에서 꺼내기 */\n public int? PopFirst() {\n return Pop(true);\n }\n\n /* 맨 뒤에서 꺼내기 */\n public int? PopLast() {\n return Pop(false);\n }\n\n /* 맨 앞 원소 접근 */\n public int? PeekFirst() {\n if (IsEmpty())\n throw new Exception();\n return front?.val;\n }\n\n /* 맨 뒤 원소 접근 */\n public int? PeekLast() {\n if (IsEmpty())\n throw new Exception();\n return rear?.val;\n }\n\n /* 출력에 사용할 배열 반환 */\n public int?[] ToArray() {\n ListNode? node = front;\n int?[] res = new int?[Size()];\n for (int i = 0; i &lt; res.Length; i++) {\n res[i] = node?.val;\n node = node?.next;\n }\n\n return res;\n }\n}\n</code></pre> linkedlist_deque.go<pre><code>/* 이중 연결 리스트로 구현한 덱 */\ntype linkedListDeque struct {\n // 내장 패키지 list를 사용한다\n data *list.List\n}\n\n/* 덱 초기화 */\nfunc newLinkedListDeque() *linkedListDeque {\n return &amp;linkedListDeque{\n data: list.New(),\n }\n}\n\n/* 맨 앞에 원소 삽입 */\nfunc (s *linkedListDeque) pushFirst(value any) {\n s.data.PushFront(value)\n}\n\n/* 맨 뒤에 원소 삽입 */\nfunc (s *linkedListDeque) pushLast(value any) {\n s.data.PushBack(value)\n}\n\n/* 맨 앞 원소를 꺼낸다 */\nfunc (s *linkedListDeque) popFirst() any {\n if s.isEmpty() {\n return nil\n }\n e := s.data.Front()\n s.data.Remove(e)\n return e.Value\n}\n\n/* 맨 뒤 원소를 꺼낸다 */\nfunc (s *linkedListDeque) popLast() any {\n if s.isEmpty() {\n return nil\n }\n e := s.data.Back()\n s.data.Remove(e)\n return e.Value\n}\n\n/* 맨 앞 원소 접근 */\nfunc (s *linkedListDeque) peekFirst() any {\n if s.isEmpty() {\n return nil\n }\n e := s.data.Front()\n return e.Value\n}\n\n/* 맨 뒤 원소 접근 */\nfunc (s *linkedListDeque) peekLast() any {\n if s.isEmpty() {\n return nil\n }\n e := s.data.Back()\n return e.Value\n}\n\n/* 큐의 길이 반환 */\nfunc (s *linkedListDeque) size() int {\n return s.data.Len()\n}\n\n/* 큐가 비어 있는지 판단 */\nfunc (s *linkedListDeque) isEmpty() bool {\n return s.data.Len() == 0\n}\n\n/* 출력에 사용할 List 반환 */\nfunc (s *linkedListDeque) toList() *list.List {\n return s.data\n}\n</code></pre> linkedlist_deque.swift<pre><code>/* 이중 연결 리스트 노드 */\nclass ListNode {\n var val: Int // 노드 값\n var next: ListNode? // 다음 노드 참조\n weak var prev: ListNode? // 이전 노드 참조\n\n init(val: Int) {\n self.val = val\n }\n}\n\n/* 이중 연결 리스트로 구현한 덱 */\nclass LinkedListDeque {\n private var front: ListNode? // 머리 노드 front\n private var rear: ListNode? // 꼬리 노드 rear\n private var _size: Int // 덱의 길이\n\n init() {\n _size = 0\n }\n\n /* 덱의 길이 반환 */\n func size() -&gt; Int {\n _size\n }\n\n /* 덱이 비어 있는지 판단 */\n func isEmpty() -&gt; Bool {\n size() == 0\n }\n\n /* 큐 삽입 연산 */\n private func push(num: Int, isFront: Bool) {\n let node = ListNode(val: num)\n // 연결 리스트가 비어 있으면 front와 rear가 모두 node를 가리키게 한다\n if isEmpty() {\n front = node\n rear = node\n }\n // 맨 앞에 삽입하는 연산\n else if isFront {\n // node를 연결 리스트의 맨 앞에 추가한다\n front?.prev = node\n node.next = front\n front = node // 머리 노드를 갱신한다\n }\n // 맨 뒤에 삽입하는 연산\n else {\n // node를 연결 리스트의 맨 뒤에 추가한다\n rear?.next = node\n node.prev = rear\n rear = node // 꼬리 노드를 갱신한다\n }\n _size += 1 // 큐 길이를 갱신한다\n }\n\n /* 맨 앞에 삽입 */\n func pushFirst(num: Int) {\n push(num: num, isFront: true)\n }\n\n /* 맨 뒤에 삽입 */\n func pushLast(num: Int) {\n push(num: num, isFront: false)\n }\n\n /* 큐에서 꺼내는 연산 */\n private func pop(isFront: Bool) -&gt; Int {\n if isEmpty() {\n fatalError(\"덱이 비어 있다\")\n }\n let val: Int\n // 큐의 길이가 1이면 머리와 꼬리가 같은 노드를 가리킨다\n if _size == 1 {\n val = front!.val\n front = nil\n rear = nil\n }\n // 맨 앞에서 꺼내는 연산\n else if isFront {\n val = front!.val // 머리 노드의 값을 임시로 저장한다\n // 머리 노드를 삭제한다\n let fNext = front?.next\n fNext?.prev = nil\n front?.next = nil\n front = fNext // 머리 노드를 갱신한다\n }\n // 맨 뒤에서 꺼내는 연산\n else {\n val = rear!.val // 꼬리 노드의 값을 임시로 저장한다\n // 꼬리 노드를 삭제한다\n let rPrev = rear?.prev\n rPrev?.next = nil\n rear?.prev = nil\n rear = rPrev // 꼬리 노드를 갱신한다\n }\n _size -= 1 // 큐 길이를 갱신한다\n return val\n }\n\n /* 맨 앞에서 꺼내기 */\n func popFirst() -&gt; Int {\n pop(isFront: true)\n }\n\n /* 맨 뒤에서 꺼내기 */\n func popLast() -&gt; Int {\n pop(isFront: false)\n }\n\n /* 맨 앞 원소 접근 */\n func peekFirst() -&gt; Int {\n if isEmpty() {\n fatalError(\"덱이 비어 있다\")\n }\n return front!.val\n }\n\n /* 맨 뒤 원소 접근 */\n func peekLast() -&gt; Int {\n if isEmpty() {\n fatalError(\"덱이 비어 있다\")\n }\n return rear!.val\n }\n\n /* 출력에 사용할 배열 반환 */\n func toArray() -&gt; [Int] {\n var node = front\n var res = Array(repeating: 0, count: size())\n for i in res.indices {\n res[i] = node!.val\n node = node?.next\n }\n return res\n }\n}\n</code></pre> linkedlist_deque.js<pre><code>/* 이중 연결 리스트 노드 */\nclass ListNode {\n prev; // 이전 노드 참조(포인터)\n next; // 다음 노드 참조(포인터)\n val; // 노드 값\n\n constructor(val) {\n this.val = val;\n this.next = null;\n this.prev = null;\n }\n}\n\n/* 이중 연결 리스트로 구현한 덱 */\nclass LinkedListDeque {\n #front; // 머리 노드 front\n #rear; // 꼬리 노드 rear\n #queSize; // 덱의 길이\n\n constructor() {\n this.#front = null;\n this.#rear = null;\n this.#queSize = 0;\n }\n\n /* 맨 뒤에 삽입하는 연산 */\n pushLast(val) {\n const node = new ListNode(val);\n // 연결 리스트가 비어 있으면 front와 rear가 모두 node를 가리키게 한다\n if (this.#queSize === 0) {\n this.#front = node;\n this.#rear = node;\n } else {\n // node를 연결 리스트의 맨 뒤에 추가한다\n this.#rear.next = node;\n node.prev = this.#rear;\n this.#rear = node; // 꼬리 노드를 갱신한다\n }\n this.#queSize++;\n }\n\n /* 맨 앞에 삽입하는 연산 */\n pushFirst(val) {\n const node = new ListNode(val);\n // 연결 리스트가 비어 있으면 front와 rear가 모두 node를 가리키게 한다\n if (this.#queSize === 0) {\n this.#front = node;\n this.#rear = node;\n } else {\n // node를 연결 리스트의 맨 앞에 추가한다\n this.#front.prev = node;\n node.next = this.#front;\n this.#front = node; // 머리 노드를 갱신한다\n }\n this.#queSize++;\n }\n\n /* 맨 뒤에서 꺼내는 연산 */\n popLast() {\n if (this.#queSize === 0) {\n return null;\n }\n const value = this.#rear.val; // 꼬리 노드의 값을 저장한다\n // 큐의 길이가 1이면 머리와 꼬리가 같은 노드를 가리킨다\n if (this.#queSize === 1) {\n this.#front = null;\n this.#rear = null;\n }\n // 꼬리 노드를 삭제한다\n else {\n let temp = this.#rear.prev;\n temp.next = null;\n this.#rear.prev = null;\n this.#rear = temp; // 꼬리 노드를 갱신한다\n }\n this.#queSize--;\n return value;\n }\n\n /* 맨 앞에서 꺼내는 연산 */\n popFirst() {\n if (this.#queSize === 0) {\n return null;\n }\n const value = this.#front.val; // 머리 노드의 값을 저장한다\n // 큐의 길이가 1이면 머리와 꼬리가 같은 노드를 가리킨다\n if (this.#queSize === 1) {\n this.#front = null;\n this.#rear = null;\n }\n // 머리 노드를 삭제한다\n else {\n let temp = this.#front.next;\n temp.prev = null;\n this.#front.next = null;\n this.#front = temp; // 머리 노드를 갱신한다\n }\n this.#queSize--;\n return value;\n }\n\n /* 맨 뒤 원소 접근 */\n peekLast() {\n return this.#queSize === 0 ? null : this.#rear.val;\n }\n\n /* 맨 앞 원소 접근 */\n peekFirst() {\n return this.#queSize === 0 ? null : this.#front.val;\n }\n\n /* 덱의 길이 반환 */\n size() {\n return this.#queSize;\n }\n\n /* 덱이 비어 있는지 판단 */\n isEmpty() {\n return this.#queSize === 0;\n }\n\n /* 덱을 출력한다 */\n print() {\n const arr = [];\n let temp = this.#front;\n while (temp !== null) {\n arr.push(temp.val);\n temp = temp.next;\n }\n console.log('[' + arr.join(', ') + ']');\n }\n}\n</code></pre> linkedlist_deque.ts<pre><code>/* 이중 연결 리스트 노드 */\nclass ListNode {\n prev: ListNode; // 이전 노드 참조(포인터)\n next: ListNode; // 다음 노드 참조(포인터)\n val: number; // 노드 값\n\n constructor(val: number) {\n this.val = val;\n this.next = null;\n this.prev = null;\n }\n}\n\n/* 이중 연결 리스트로 구현한 덱 */\nclass LinkedListDeque {\n private front: ListNode; // 머리 노드 front\n private rear: ListNode; // 꼬리 노드 rear\n private queSize: number; // 덱의 길이\n\n constructor() {\n this.front = null;\n this.rear = null;\n this.queSize = 0;\n }\n\n /* 맨 뒤에 삽입하는 연산 */\n pushLast(val: number): void {\n const node: ListNode = new ListNode(val);\n // 연결 리스트가 비어 있으면 front와 rear가 모두 node를 가리키게 한다\n if (this.queSize === 0) {\n this.front = node;\n this.rear = node;\n } else {\n // node를 연결 리스트의 맨 뒤에 추가한다\n this.rear.next = node;\n node.prev = this.rear;\n this.rear = node; // 꼬리 노드를 갱신한다\n }\n this.queSize++;\n }\n\n /* 맨 앞에 삽입하는 연산 */\n pushFirst(val: number): void {\n const node: ListNode = new ListNode(val);\n // 연결 리스트가 비어 있으면 front와 rear가 모두 node를 가리키게 한다\n if (this.queSize === 0) {\n this.front = node;\n this.rear = node;\n } else {\n // node를 연결 리스트의 맨 앞에 추가한다\n this.front.prev = node;\n node.next = this.front;\n this.front = node; // 머리 노드를 갱신한다\n }\n this.queSize++;\n }\n\n /* 맨 뒤에서 꺼내는 연산 */\n popLast(): number {\n if (this.queSize === 0) {\n return null;\n }\n const value: number = this.rear!.val; // 꼬리 노드의 값을 저장한다\n // 큐의 길이가 1이면 머리와 꼬리가 같은 노드를 가리킨다\n if (this.queSize === 1) {\n this.front = null;\n this.rear = null;\n }\n // 꼬리 노드를 삭제한다\n else {\n let temp: ListNode = this.rear!.prev;\n temp!.next = null;\n this.rear!.prev = null;\n this.rear = temp; // 꼬리 노드를 갱신한다\n }\n this.queSize--;\n return value;\n }\n\n /* 맨 앞에서 꺼내는 연산 */\n popFirst(): number {\n if (this.queSize === 0) {\n return null;\n }\n const value: number = this.front.val; // 머리 노드의 값을 저장한다\n // 큐의 길이가 1이면 머리와 꼬리가 같은 노드를 가리킨다\n if (this.queSize === 1) {\n this.front = null;\n this.rear = null;\n }\n // 머리 노드를 삭제한다\n else {\n let temp: ListNode = this.front!.next;\n temp!.prev = null;\n this.front!.next = null;\n this.front = temp; // 머리 노드를 갱신한다\n }\n this.queSize--;\n return value;\n }\n\n /* 맨 뒤 원소 접근 */\n peekLast(): number {\n return this.queSize === 0 ? null : this.rear.val;\n }\n\n /* 맨 앞 원소 접근 */\n peekFirst(): number {\n return this.queSize === 0 ? null : this.front.val;\n }\n\n /* 덱의 길이 반환 */\n size(): number {\n return this.queSize;\n }\n\n /* 덱이 비어 있는지 판단 */\n isEmpty(): boolean {\n return this.queSize === 0;\n }\n\n /* 덱을 출력한다 */\n print(): void {\n const arr: number[] = [];\n let temp: ListNode = this.front;\n while (temp !== null) {\n arr.push(temp.val);\n temp = temp.next;\n }\n console.log('[' + arr.join(', ') + ']');\n }\n}\n</code></pre> linkedlist_deque.dart<pre><code>/* 이중 연결 리스트 노드 */\nclass ListNode {\n int val; // 노드 값\n ListNode? next; // 다음 노드 참조\n ListNode? prev; // 이전 노드 참조\n\n ListNode(this.val, {this.next, this.prev});\n}\n\n/* 이중 연결 리스트로 구현한 덱 */\nclass LinkedListDeque {\n late ListNode? _front; // 머리 노드 _front\n late ListNode? _rear; // 꼬리 노드 _rear\n int _queSize = 0; // 덱의 길이\n\n LinkedListDeque() {\n this._front = null;\n this._rear = null;\n }\n\n /* 덱의 길이 반환 */\n int size() {\n return this._queSize;\n }\n\n /* 덱이 비어 있는지 판단 */\n bool isEmpty() {\n return size() == 0;\n }\n\n /* 큐 삽입 연산 */\n void push(int _num, bool isFront) {\n final ListNode node = ListNode(_num);\n if (isEmpty()) {\n // 연결 리스트가 비어 있으면 _front와 _rear가 모두 node를 가리키게 한다\n _front = _rear = node;\n } else if (isFront) {\n // 맨 앞에 삽입하는 연산\n // node를 연결 리스트의 맨 앞에 추가한다\n _front!.prev = node;\n node.next = _front;\n _front = node; // 머리 노드를 갱신한다\n } else {\n // 맨 뒤에 삽입하는 연산\n // node를 연결 리스트의 맨 뒤에 추가한다\n _rear!.next = node;\n node.prev = _rear;\n _rear = node; // 꼬리 노드를 갱신한다\n }\n _queSize++; // 큐 길이를 갱신한다\n }\n\n /* 맨 앞에 삽입 */\n void pushFirst(int _num) {\n push(_num, true);\n }\n\n /* 맨 뒤에 삽입 */\n void pushLast(int _num) {\n push(_num, false);\n }\n\n /* 큐에서 꺼내는 연산 */\n int? pop(bool isFront) {\n // 큐가 비어 있으면 바로 null을 반환한다\n if (isEmpty()) {\n return null;\n }\n final int val;\n // 큐의 길이가 1이면 머리와 꼬리가 같은 노드를 가리킨다\n if (_queSize == 1) {\n val = _front!.val;\n _front = null;\n _rear = null;\n }\n // 맨 앞에서 꺼내는 연산\n else if (isFront) {\n val = _front!.val; // 머리 노드의 값을 임시로 저장한다\n // 머리 노드를 삭제한다\n ListNode? fNext = _front!.next;\n fNext!.prev = null;\n _front!.next = null;\n _front = fNext; // 머리 노드를 갱신한다\n // 맨 뒤에서 꺼내는 연산\n } else {\n val = _rear!.val; // 꼬리 노드의 값을 임시로 저장한다\n // 꼬리 노드를 삭제한다\n ListNode? rPrev = _rear!.prev;\n rPrev!.next = null;\n _rear!.prev = null;\n _rear = rPrev; // 꼬리 노드를 갱신한다\n }\n _queSize--; // 큐 길이를 갱신한다\n return val;\n }\n\n /* 맨 앞에서 꺼내기 */\n int? popFirst() {\n return pop(true);\n }\n\n /* 맨 뒤에서 꺼내기 */\n int? popLast() {\n return pop(false);\n }\n\n /* 맨 앞 원소 접근 */\n int? peekFirst() {\n return _front?.val;\n }\n\n /* 맨 뒤 원소 접근 */\n int? peekLast() {\n return _rear?.val;\n }\n\n /* 출력에 사용할 배열 반환 */\n List&lt;int&gt; toArray() {\n ListNode? node = _front;\n final List&lt;int&gt; res = [];\n for (int i = 0; i &lt; _queSize; i++) {\n res.add(node!.val);\n node = node.next;\n }\n return res;\n }\n}\n</code></pre> linkedlist_deque.rs<pre><code>/* 이중 연결 리스트 노드 */\npub struct ListNode&lt;T&gt; {\n pub val: T, // 노드 값\n pub next: Option&lt;Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt;&gt;, // 다음 노드 포인터\n pub prev: Option&lt;Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt;&gt;, // 이전 노드 포인터\n}\n\nimpl&lt;T&gt; ListNode&lt;T&gt; {\n pub fn new(val: T) -&gt; Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt; {\n Rc::new(RefCell::new(ListNode {\n val,\n next: None,\n prev: None,\n }))\n }\n}\n\n/* 이중 연결 리스트로 구현한 덱 */\n#[allow(dead_code)]\npub struct LinkedListDeque&lt;T&gt; {\n front: Option&lt;Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt;&gt;, // 머리 노드 front\n rear: Option&lt;Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt;&gt;, // 꼬리 노드 rear\n que_size: usize, // 덱의 길이\n}\n\nimpl&lt;T: Copy&gt; LinkedListDeque&lt;T&gt; {\n pub fn new() -&gt; Self {\n Self {\n front: None,\n rear: None,\n que_size: 0,\n }\n }\n\n /* 덱의 길이 반환 */\n pub fn size(&amp;self) -&gt; usize {\n return self.que_size;\n }\n\n /* 덱이 비어 있는지 판단 */\n pub fn is_empty(&amp;self) -&gt; bool {\n return self.que_size == 0;\n }\n\n /* 큐 삽입 연산 */\n fn push(&amp;mut self, num: T, is_front: bool) {\n let node = ListNode::new(num);\n // 맨 앞에 삽입하는 연산\n if is_front {\n match self.front.take() {\n // 연결 리스트가 비어 있으면 front와 rear가 모두 node를 가리키게 한다\n None =&gt; {\n self.rear = Some(node.clone());\n self.front = Some(node);\n }\n // node를 연결 리스트의 맨 앞에 추가한다\n Some(old_front) =&gt; {\n old_front.borrow_mut().prev = Some(node.clone());\n node.borrow_mut().next = Some(old_front);\n self.front = Some(node); // 머리 노드를 갱신한다\n }\n }\n }\n // 맨 뒤에 삽입하는 연산\n else {\n match self.rear.take() {\n // 연결 리스트가 비어 있으면 front와 rear가 모두 node를 가리키게 한다\n None =&gt; {\n self.front = Some(node.clone());\n self.rear = Some(node);\n }\n // node를 연결 리스트의 맨 뒤에 추가한다\n Some(old_rear) =&gt; {\n old_rear.borrow_mut().next = Some(node.clone());\n node.borrow_mut().prev = Some(old_rear);\n self.rear = Some(node); // 꼬리 노드를 갱신한다\n }\n }\n }\n self.que_size += 1; // 큐 길이를 갱신한다\n }\n\n /* 맨 앞에 삽입 */\n pub fn push_first(&amp;mut self, num: T) {\n self.push(num, true);\n }\n\n /* 맨 뒤에 삽입 */\n pub fn push_last(&amp;mut self, num: T) {\n self.push(num, false);\n }\n\n /* 큐에서 꺼내는 연산 */\n fn pop(&amp;mut self, is_front: bool) -&gt; Option&lt;T&gt; {\n // 큐가 비어 있으면 바로 None을 반환한다\n if self.is_empty() {\n return None;\n };\n // 맨 앞에서 꺼내는 연산\n if is_front {\n self.front.take().map(|old_front| {\n match old_front.borrow_mut().next.take() {\n Some(new_front) =&gt; {\n new_front.borrow_mut().prev.take();\n self.front = Some(new_front); // 머리 노드를 갱신한다\n }\n None =&gt; {\n self.rear.take();\n }\n }\n self.que_size -= 1; // 큐 길이를 갱신한다\n old_front.borrow().val\n })\n }\n // 맨 뒤에서 꺼내는 연산\n else {\n self.rear.take().map(|old_rear| {\n match old_rear.borrow_mut().prev.take() {\n Some(new_rear) =&gt; {\n new_rear.borrow_mut().next.take();\n self.rear = Some(new_rear); // 꼬리 노드를 갱신한다\n }\n None =&gt; {\n self.front.take();\n }\n }\n self.que_size -= 1; // 큐 길이를 갱신한다\n old_rear.borrow().val\n })\n }\n }\n\n /* 맨 앞에서 꺼내기 */\n pub fn pop_first(&amp;mut self) -&gt; Option&lt;T&gt; {\n return self.pop(true);\n }\n\n /* 맨 뒤에서 꺼내기 */\n pub fn pop_last(&amp;mut self) -&gt; Option&lt;T&gt; {\n return self.pop(false);\n }\n\n /* 맨 앞 원소 접근 */\n pub fn peek_first(&amp;self) -&gt; Option&lt;&amp;Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt;&gt; {\n self.front.as_ref()\n }\n\n /* 맨 뒤 원소 접근 */\n pub fn peek_last(&amp;self) -&gt; Option&lt;&amp;Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt;&gt; {\n self.rear.as_ref()\n }\n\n /* 출력에 사용할 배열 반환 */\n pub fn to_array(&amp;self, head: Option&lt;&amp;Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt;&gt;) -&gt; Vec&lt;T&gt; {\n let mut res: Vec&lt;T&gt; = Vec::new();\n fn recur&lt;T: Copy&gt;(cur: Option&lt;&amp;Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt;&gt;, res: &amp;mut Vec&lt;T&gt;) {\n if let Some(cur) = cur {\n res.push(cur.borrow().val);\n recur(cur.borrow().next.as_ref(), res);\n }\n }\n\n recur(head, &amp;mut res);\n res\n }\n}\n</code></pre> linkedlist_deque.c<pre><code>/* 이중 연결 리스트 노드 */\ntypedef struct DoublyListNode {\n int val; // 노드 값\n struct DoublyListNode *next; // 다음 노드\n struct DoublyListNode *prev; // 이전 노드\n} DoublyListNode;\n\n/* 생성자 */\nDoublyListNode *newDoublyListNode(int num) {\n DoublyListNode *new = (DoublyListNode *)malloc(sizeof(DoublyListNode));\n new-&gt;val = num;\n new-&gt;next = NULL;\n new-&gt;prev = NULL;\n return new;\n}\n\n/* 소멸자 */\nvoid delDoublyListNode(DoublyListNode *node) {\n free(node);\n}\n\n/* 이중 연결 리스트로 구현한 덱 */\ntypedef struct {\n DoublyListNode *front, *rear; // 머리 노드 front, 꼬리 노드 rear\n int queSize; // 덱의 길이\n} LinkedListDeque;\n\n/* 생성자 */\nLinkedListDeque *newLinkedListDeque() {\n LinkedListDeque *deque = (LinkedListDeque *)malloc(sizeof(LinkedListDeque));\n deque-&gt;front = NULL;\n deque-&gt;rear = NULL;\n deque-&gt;queSize = 0;\n return deque;\n}\n\n/* 소멸자 */\nvoid delLinkedListdeque(LinkedListDeque *deque) {\n // 모든 노드를 해제한다\n for (int i = 0; i &lt; deque-&gt;queSize &amp;&amp; deque-&gt;front != NULL; i++) {\n DoublyListNode *tmp = deque-&gt;front;\n deque-&gt;front = deque-&gt;front-&gt;next;\n free(tmp);\n }\n // deque 구조체를 해제한다\n free(deque);\n}\n\n/* 큐의 길이 반환 */\nint size(LinkedListDeque *deque) {\n return deque-&gt;queSize;\n}\n\n/* 큐가 비어 있는지 판단 */\nbool empty(LinkedListDeque *deque) {\n return (size(deque) == 0);\n}\n\n/* 큐에 삽입 */\nvoid push(LinkedListDeque *deque, int num, bool isFront) {\n DoublyListNode *node = newDoublyListNode(num);\n // 연결 리스트가 비어 있으면 front와 rear가 모두 node를 가리키게 한다\n if (empty(deque)) {\n deque-&gt;front = deque-&gt;rear = node;\n }\n // 맨 앞에 삽입하는 연산\n else if (isFront) {\n // node를 연결 리스트의 맨 앞에 추가한다\n deque-&gt;front-&gt;prev = node;\n node-&gt;next = deque-&gt;front;\n deque-&gt;front = node; // 머리 노드를 갱신한다\n }\n // 맨 뒤에 삽입하는 연산\n else {\n // node를 연결 리스트의 맨 뒤에 추가한다\n deque-&gt;rear-&gt;next = node;\n node-&gt;prev = deque-&gt;rear;\n deque-&gt;rear = node;\n }\n deque-&gt;queSize++; // 큐 길이를 갱신한다\n}\n\n/* 맨 앞에 삽입 */\nvoid pushFirst(LinkedListDeque *deque, int num) {\n push(deque, num, true);\n}\n\n/* 맨 뒤에 삽입 */\nvoid pushLast(LinkedListDeque *deque, int num) {\n push(deque, num, false);\n}\n\n/* 맨 앞 원소 접근 */\nint peekFirst(LinkedListDeque *deque) {\n assert(size(deque) &amp;&amp; deque-&gt;front);\n return deque-&gt;front-&gt;val;\n}\n\n/* 맨 뒤 원소 접근 */\nint peekLast(LinkedListDeque *deque) {\n assert(size(deque) &amp;&amp; deque-&gt;rear);\n return deque-&gt;rear-&gt;val;\n}\n\n/* 큐에서 꺼내기 */\nint pop(LinkedListDeque *deque, bool isFront) {\n if (empty(deque))\n return -1;\n int val;\n // 큐의 길이가 1이면 머리와 꼬리가 같은 노드를 가리킨다\n if (deque-&gt;queSize == 1) {\n val = deque-&gt;front-&gt;val;\n delDoublyListNode(deque-&gt;front); // 같은 노드를 가리키므로 하나만 해제하면 된다\n deque-&gt;front = NULL;\n deque-&gt;rear = NULL;\n }\n // 맨 앞에서 꺼내는 연산\n else if (isFront) {\n val = peekFirst(deque); // 머리 노드의 값을 임시로 저장한다\n DoublyListNode *fNext = deque-&gt;front-&gt;next;\n fNext-&gt;prev = NULL;\n deque-&gt;front-&gt;next = NULL;\n delDoublyListNode(deque-&gt;front);\n deque-&gt;front = fNext; // 머리 노드를 갱신한다\n }\n // 맨 뒤에서 꺼내는 연산\n else {\n val = peekLast(deque); // 꼬리 노드의 값을 임시로 저장한다\n DoublyListNode *rPrev = deque-&gt;rear-&gt;prev;\n rPrev-&gt;next = NULL;\n deque-&gt;rear-&gt;prev = NULL;\n delDoublyListNode(deque-&gt;rear);\n deque-&gt;rear = rPrev; // 꼬리 노드를 갱신한다\n }\n deque-&gt;queSize--; // 큐 길이를 갱신한다\n return val;\n}\n\n/* 맨 앞에서 꺼내기 */\nint popFirst(LinkedListDeque *deque) {\n return pop(deque, true);\n}\n\n/* 맨 뒤에서 꺼내기 */\nint popLast(LinkedListDeque *deque) {\n return pop(deque, false);\n}\n\n/* 큐 출력 */\nvoid printLinkedListDeque(LinkedListDeque *deque) {\n int *arr = malloc(sizeof(int) * deque-&gt;queSize);\n // 연결 리스트의 데이터를 배열에 복사한다\n int i;\n DoublyListNode *node;\n for (i = 0, node = deque-&gt;front; i &lt; deque-&gt;queSize; i++) {\n arr[i] = node-&gt;val;\n node = node-&gt;next;\n }\n printArray(arr, deque-&gt;queSize);\n free(arr);\n}\n</code></pre> linkedlist_deque.kt<pre><code>/* 이중 연결 리스트 노드 */\nclass ListNode(var _val: Int) {\n // 노드 값\n var next: ListNode? = null // 다음 노드 참조\n var prev: ListNode? = null // 이전 노드 참조\n}\n\n/* 이중 연결 리스트로 구현한 덱 */\nclass LinkedListDeque {\n private var front: ListNode? = null // 머리 노드 front\n private var rear: ListNode? = null // 꼬리 노드 rear\n private var queSize: Int = 0 // 덱의 길이\n\n /* 덱의 길이 반환 */\n fun size(): Int {\n return queSize\n }\n\n /* 덱이 비어 있는지 판단 */\n fun isEmpty(): Boolean {\n return size() == 0\n }\n\n /* 큐 삽입 연산 */\n fun push(num: Int, isFront: Boolean) {\n val node = ListNode(num)\n // 연결 리스트가 비어 있으면 front와 rear가 모두 node를 가리키게 한다\n if (isEmpty()) {\n rear = node\n front = rear\n // 맨 앞에 삽입하는 연산\n } else if (isFront) {\n // node를 연결 리스트의 맨 앞에 추가한다\n front?.prev = node\n node.next = front\n front = node // 머리 노드를 갱신한다\n // 맨 뒤에 삽입하는 연산\n } else {\n // node를 연결 리스트의 맨 뒤에 추가한다\n rear?.next = node\n node.prev = rear\n rear = node // 꼬리 노드를 갱신한다\n }\n queSize++ // 큐 길이를 갱신한다\n }\n\n /* 맨 앞에 삽입 */\n fun pushFirst(num: Int) {\n push(num, true)\n }\n\n /* 맨 뒤에 삽입 */\n fun pushLast(num: Int) {\n push(num, false)\n }\n\n /* 큐에서 꺼내는 연산 */\n fun pop(isFront: Boolean): Int {\n if (isEmpty()) \n throw IndexOutOfBoundsException()\n val _val: Int\n // 큐의 길이가 1이면 머리와 꼬리가 같은 노드를 가리킨다\n if (queSize == 1) {\n _val = front!!._val\n front = null\n rear = null\n }\n // 맨 앞에서 꺼내는 연산\n else if (isFront) {\n _val = front!!._val // 머리 노드의 값을 임시로 저장한다\n // 머리 노드를 삭제한다\n val fNext = front!!.next\n fNext!!.prev = null\n front!!.next = null\n front = fNext // 머리 노드를 갱신한다\n // 맨 뒤에서 꺼내는 연산\n } else {\n _val = rear!!._val // 꼬리 노드의 값을 임시로 저장한다\n // 꼬리 노드를 삭제한다\n val rPrev = rear!!.prev\n rPrev!!.next = null\n rear!!.prev = null\n rear = rPrev // 꼬리 노드를 갱신한다\n }\n queSize-- // 큐 길이를 갱신한다\n return _val\n }\n\n /* 맨 앞에서 꺼내기 */\n fun popFirst(): Int {\n return pop(true)\n }\n\n /* 맨 뒤에서 꺼내기 */\n fun popLast(): Int {\n return pop(false)\n }\n\n /* 맨 앞 원소 접근 */\n fun peekFirst(): Int {\n if (isEmpty()) throw IndexOutOfBoundsException()\n return front!!._val\n }\n\n /* 맨 뒤 원소 접근 */\n fun peekLast(): Int {\n if (isEmpty()) throw IndexOutOfBoundsException()\n return rear!!._val\n }\n\n /* 출력에 사용할 배열 반환 */\n fun toArray(): IntArray {\n var node = front\n val res = IntArray(size())\n for (i in res.indices) {\n res[i] = node!!._val\n node = node.next\n }\n return res\n }\n}\n</code></pre> linkedlist_deque.rb<pre><code>=begin\nFile: linkedlist_deque.rb\nCreated Time: 2024-04-06\nAuthor: Xuan Khoa Tu Nguyen (ngxktuzkai2000@gmail.com)\n=end\n\n### 이중 연결 리스트 노드\nclass ListNode\n attr_accessor :val\n attr_accessor :next # 다음 노드 참조\n attr_accessor :prev # 이전 노드 참조\n\n ### 생성자 ###\n def initialize(val)\n @val = val\n end\nend\n\n### 이중 연결 리스트로 구현한 덱 ###\nclass LinkedListDeque\n ### 덱의 길이 반환 ###\n attr_reader :size\n\n ### 생성자 ###\n def initialize\n @front = nil # 머리 노드 front\n @rear = nil # 꼬리 노드 rear\n @size = 0 # 덱의 길이\n end\n\n ### 덱이 비어 있는지 판단 ###\n def is_empty?\n size.zero?\n end\n\n ### 큐 삽입 연산 ###\n def push(num, is_front)\n node = ListNode.new(num)\n # 연결 리스트가 비어 있으면 front와 rear가 모두 node를 가리키게 한다\n if is_empty?\n @front = @rear = node\n # 맨 앞에 삽입하는 연산\n elsif is_front\n # node를 연결 리스트의 맨 앞에 추가한다\n @front.prev = node\n node.next = @front\n @front = node # 머리 노드를 갱신한다\n # 맨 뒤에 삽입하는 연산\n else\n # node를 연결 리스트의 맨 뒤에 추가한다\n @rear.next = node\n node.prev = @rear\n @rear = node # 꼬리 노드를 갱신한다\n end\n @size += 1 # 큐 길이를 갱신한다\n end\n\n ### 맨 앞에 삽입 ###\n def push_first(num)\n push(num, true)\n end\n\n ### 맨 뒤에 삽입 ###\n def push_last(num)\n push(num, false)\n end\n\n ### 큐에서 꺼내는 연산 ###\n def pop(is_front)\n raise IndexError, '덱이 비어 있다' if is_empty?\n\n # 큐의 길이가 1이면 머리와 꼬리가 같은 노드를 가리킨다\n if @size == 1\n val = @front.val\n @front = nil\n @rear = nil\n # 맨 앞에서 꺼내는 연산\n elsif is_front\n val = @front.val # 머리 노드의 값을 임시로 저장한다\n # 머리 노드를 삭제한다\n fnext = @front.next\n fnext.prev = nil\n @front.next = nil\n @front = fnext # 머리 노드를 갱신한다\n # 맨 뒤에서 꺼내는 연산\n else\n val = @rear.val # 꼬리 노드의 값을 임시로 저장한다\n # 꼬리 노드를 삭제한다\n rprev = @rear.prev\n rprev.next = nil\n @rear.prev = nil\n @rear = rprev # 꼬리 노드를 갱신한다\n end\n @size -= 1 # 큐 길이를 갱신한다\n\n val\n end\n\n ### 맨 앞에서 꺼내기 ###\n def pop_first\n pop(true)\n end\n\n ### 맨 앞에서 꺼내기 ###\n def pop_last\n pop(false)\n end\n\n ### 맨 앞 원소 접근 ###\n def peek_first\n raise IndexError, '덱이 비어 있다' if is_empty?\n\n @front.val\n end\n\n ### 맨 뒤 원소 접근 ###\n def peek_last\n raise IndexError, '덱이 비어 있다' if is_empty?\n\n @rear.val\n end\n\n ### 출력에 사용할 배열 반환 ###\n def to_array\n node = @front\n res = Array.new(size, 0)\n for i in 0...size\n res[i] = node.val\n node = node.next\n end\n res\n end\nend\n</code></pre>","path":["제 5 장   스택과 큐","5.3   덱"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#2","level":3,"title":"2.   배열 기반 구현","text":"<p>아래 그림(5-9)과 같이 배열 기반 큐와 마찬가지로 원형 배열을 사용하여 덱을 구현할 수 있다.</p> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 5-9   배열 기반 덱의 삽입과 꺼내기 </p> <p>큐 구현에 ‘맨 앞에 삽입하기’와 ‘맨 뒤에서 꺼내기’ 메서드만 추가하면 된다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_deque.py<pre><code>class ArrayDeque:\n \"\"\"원형 배열로 구현한 덱\"\"\"\n\n def __init__(self, capacity: int):\n \"\"\"생성자\"\"\"\n self._nums: list[int] = [0] * capacity\n self._front: int = 0\n self._size: int = 0\n\n def capacity(self) -&gt; int:\n \"\"\"덱의 용량 반환\"\"\"\n return len(self._nums)\n\n def size(self) -&gt; int:\n \"\"\"덱의 길이 반환\"\"\"\n return self._size\n\n def is_empty(self) -&gt; bool:\n \"\"\"덱이 비어 있는지 판단\"\"\"\n return self._size == 0\n\n def index(self, i: int) -&gt; int:\n \"\"\"원형 배열 인덱스 계산\"\"\"\n # 나머지 연산을 통해 배열의 처음과 끝을 연결한다\n # i가 배열의 끝을 넘어가면 처음으로 돌아온다\n # i가 배열의 처음을 넘어가면 끝으로 돌아온다\n return (i + self.capacity()) % self.capacity()\n\n def push_first(self, num: int):\n \"\"\"맨 앞에 삽입\"\"\"\n if self._size == self.capacity():\n print(\"덱이 가득 찼다\")\n return\n # 맨 앞 포인터를 왼쪽으로 한 칸 이동한다\n # 나머지 연산을 통해 front가 배열의 처음을 넘어가면 끝으로 돌아오게 한다\n self._front = self.index(self._front - 1)\n # num을 맨 앞에 추가한다\n self._nums[self._front] = num\n self._size += 1\n\n def push_last(self, num: int):\n \"\"\"맨 뒤에 삽입\"\"\"\n if self._size == self.capacity():\n print(\"덱이 가득 찼다\")\n return\n # 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n rear = self.index(self._front + self._size)\n # num을 맨 뒤에 추가한다\n self._nums[rear] = num\n self._size += 1\n\n def pop_first(self) -&gt; int:\n \"\"\"맨 앞에서 꺼내기\"\"\"\n num = self.peek_first()\n # 맨 앞 포인터를 뒤로 한 칸 이동한다\n self._front = self.index(self._front + 1)\n self._size -= 1\n return num\n\n def pop_last(self) -&gt; int:\n \"\"\"맨 뒤에서 꺼내기\"\"\"\n num = self.peek_last()\n self._size -= 1\n return num\n\n def peek_first(self) -&gt; int:\n \"\"\"맨 앞 원소 접근\"\"\"\n if self.is_empty():\n raise IndexError(\"덱이 비어 있다\")\n return self._nums[self._front]\n\n def peek_last(self) -&gt; int:\n \"\"\"맨 뒤 원소 접근\"\"\"\n if self.is_empty():\n raise IndexError(\"덱이 비어 있다\")\n # 마지막 원소의 인덱스를 계산한다\n last = self.index(self._front + self._size - 1)\n return self._nums[last]\n\n def to_array(self) -&gt; list[int]:\n \"\"\"출력에 사용할 배열 반환\"\"\"\n # 유효한 길이 범위 안의 리스트 원소만 변환한다\n res = []\n for i in range(self._size):\n res.append(self._nums[self.index(self._front + i)])\n return res\n</code></pre> array_deque.cpp<pre><code>/* 원형 배열로 구현한 덱 */\nclass ArrayDeque {\n private:\n vector&lt;int&gt; nums; // 덱의 원소를 저장하는 배열\n int front; // 맨 앞 원소를 가리키는 맨 앞 포인터\n int queSize; // 덱 길이\n\n public:\n /* 생성자 */\n ArrayDeque(int capacity) {\n nums.resize(capacity);\n front = queSize = 0;\n }\n\n /* 덱의 용량 반환 */\n int capacity() {\n return nums.size();\n }\n\n /* 덱의 길이 반환 */\n int size() {\n return queSize;\n }\n\n /* 덱이 비어 있는지 판단 */\n bool isEmpty() {\n return queSize == 0;\n }\n\n /* 원형 배열 인덱스 계산 */\n int index(int i) {\n // 나머지 연산을 통해 배열의 처음과 끝을 연결한다\n // i가 배열의 끝을 넘어가면 처음으로 돌아온다\n // i가 배열의 처음을 넘어가면 끝으로 돌아온다\n return (i + capacity()) % capacity();\n }\n\n /* 맨 앞에 삽입 */\n void pushFirst(int num) {\n if (queSize == capacity()) {\n cout &lt;&lt; \"덱이 가득 찼다\" &lt;&lt; endl;\n return;\n }\n // 맨 앞 포인터를 왼쪽으로 한 칸 이동한다\n // 나머지 연산을 통해 front가 배열의 처음을 넘어가면 끝으로 돌아오게 한다\n front = index(front - 1);\n // num을 맨 앞에 추가한다\n nums[front] = num;\n queSize++;\n }\n\n /* 맨 뒤에 삽입 */\n void pushLast(int num) {\n if (queSize == capacity()) {\n cout &lt;&lt; \"덱이 가득 찼다\" &lt;&lt; endl;\n return;\n }\n // 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n int rear = index(front + queSize);\n // num을 맨 뒤에 추가한다\n nums[rear] = num;\n queSize++;\n }\n\n /* 맨 앞에서 꺼내기 */\n int popFirst() {\n int num = peekFirst();\n // 맨 앞 포인터를 뒤로 한 칸 이동한다\n front = index(front + 1);\n queSize--;\n return num;\n }\n\n /* 맨 뒤에서 꺼내기 */\n int popLast() {\n int num = peekLast();\n queSize--;\n return num;\n }\n\n /* 맨 앞 원소 접근 */\n int peekFirst() {\n if (isEmpty())\n throw out_of_range(\"덱이 비어 있다\");\n return nums[front];\n }\n\n /* 맨 뒤 원소 접근 */\n int peekLast() {\n if (isEmpty())\n throw out_of_range(\"덱이 비어 있다\");\n // 마지막 원소의 인덱스를 계산한다\n int last = index(front + queSize - 1);\n return nums[last];\n }\n\n /* 출력에 사용할 배열 반환 */\n vector&lt;int&gt; toVector() {\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n vector&lt;int&gt; res(queSize);\n for (int i = 0, j = front; i &lt; queSize; i++, j++) {\n res[i] = nums[index(j)];\n }\n return res;\n }\n};\n</code></pre> array_deque.java<pre><code>/* 원형 배열로 구현한 덱 */\nclass ArrayDeque {\n private int[] nums; // 덱의 원소를 저장하는 배열\n private int front; // 맨 앞 원소를 가리키는 맨 앞 포인터\n private int queSize; // 덱 길이\n\n /* 생성자 */\n public ArrayDeque(int capacity) {\n this.nums = new int[capacity];\n front = queSize = 0;\n }\n\n /* 덱의 용량 반환 */\n public int capacity() {\n return nums.length;\n }\n\n /* 덱의 길이 반환 */\n public int size() {\n return queSize;\n }\n\n /* 덱이 비어 있는지 판단 */\n public boolean isEmpty() {\n return queSize == 0;\n }\n\n /* 원형 배열 인덱스 계산 */\n private int index(int i) {\n // 나머지 연산을 통해 배열의 처음과 끝을 연결한다\n // i가 배열의 끝을 넘어가면 처음으로 돌아온다\n // i가 배열의 처음을 넘어가면 끝으로 돌아온다\n return (i + capacity()) % capacity();\n }\n\n /* 맨 앞에 삽입 */\n public void pushFirst(int num) {\n if (queSize == capacity()) {\n System.out.println(\"덱이 가득 찼다\");\n return;\n }\n // 맨 앞 포인터를 왼쪽으로 한 칸 이동한다\n // 나머지 연산을 통해 front가 배열의 처음을 넘어가면 끝으로 돌아오게 한다\n front = index(front - 1);\n // num을 맨 앞에 추가한다\n nums[front] = num;\n queSize++;\n }\n\n /* 맨 뒤에 삽입 */\n public void pushLast(int num) {\n if (queSize == capacity()) {\n System.out.println(\"덱이 가득 찼다\");\n return;\n }\n // 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n int rear = index(front + queSize);\n // num을 맨 뒤에 추가한다\n nums[rear] = num;\n queSize++;\n }\n\n /* 맨 앞에서 꺼내기 */\n public int popFirst() {\n int num = peekFirst();\n // 맨 앞 포인터를 뒤로 한 칸 이동한다\n front = index(front + 1);\n queSize--;\n return num;\n }\n\n /* 맨 뒤에서 꺼내기 */\n public int popLast() {\n int num = peekLast();\n queSize--;\n return num;\n }\n\n /* 맨 앞 원소 접근 */\n public int peekFirst() {\n if (isEmpty())\n throw new IndexOutOfBoundsException();\n return nums[front];\n }\n\n /* 맨 뒤 원소 접근 */\n public int peekLast() {\n if (isEmpty())\n throw new IndexOutOfBoundsException();\n // 마지막 원소의 인덱스를 계산한다\n int last = index(front + queSize - 1);\n return nums[last];\n }\n\n /* 출력에 사용할 배열 반환 */\n public int[] toArray() {\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n int[] res = new int[queSize];\n for (int i = 0, j = front; i &lt; queSize; i++, j++) {\n res[i] = nums[index(j)];\n }\n return res;\n }\n}\n</code></pre> array_deque.cs<pre><code>/* 원형 배열로 구현한 덱 */\nclass ArrayDeque {\n int[] nums; // 덱의 원소를 저장하는 배열\n int front; // 맨 앞 원소를 가리키는 맨 앞 포인터\n int queSize; // 덱 길이\n\n /* 생성자 */\n public ArrayDeque(int capacity) {\n nums = new int[capacity];\n front = queSize = 0;\n }\n\n /* 덱의 용량 반환 */\n int Capacity() {\n return nums.Length;\n }\n\n /* 덱의 길이 반환 */\n public int Size() {\n return queSize;\n }\n\n /* 덱이 비어 있는지 판단 */\n public bool IsEmpty() {\n return queSize == 0;\n }\n\n /* 원형 배열 인덱스 계산 */\n int Index(int i) {\n // 나머지 연산을 통해 배열의 처음과 끝을 연결한다\n // i가 배열의 끝을 넘어가면 처음으로 돌아온다\n // i가 배열의 처음을 넘어가면 끝으로 돌아온다\n return (i + Capacity()) % Capacity();\n }\n\n /* 맨 앞에 삽입 */\n public void PushFirst(int num) {\n if (queSize == Capacity()) {\n Console.WriteLine(\"덱이 가득 찼다\");\n return;\n }\n // 맨 앞 포인터를 왼쪽으로 한 칸 이동한다\n // 나머지 연산을 통해 front가 배열의 처음을 넘어가면 끝으로 돌아오게 한다\n front = Index(front - 1);\n // num을 맨 앞에 추가한다\n nums[front] = num;\n queSize++;\n }\n\n /* 맨 뒤에 삽입 */\n public void PushLast(int num) {\n if (queSize == Capacity()) {\n Console.WriteLine(\"덱이 가득 찼다\");\n return;\n }\n // 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n int rear = Index(front + queSize);\n // num을 맨 뒤에 추가한다\n nums[rear] = num;\n queSize++;\n }\n\n /* 맨 앞에서 꺼내기 */\n public int PopFirst() {\n int num = PeekFirst();\n // 맨 앞 포인터를 뒤로 한 칸 이동한다\n front = Index(front + 1);\n queSize--;\n return num;\n }\n\n /* 맨 뒤에서 꺼내기 */\n public int PopLast() {\n int num = PeekLast();\n queSize--;\n return num;\n }\n\n /* 맨 앞 원소 접근 */\n public int PeekFirst() {\n if (IsEmpty()) {\n throw new InvalidOperationException();\n }\n return nums[front];\n }\n\n /* 맨 뒤 원소 접근 */\n public int PeekLast() {\n if (IsEmpty()) {\n throw new InvalidOperationException();\n }\n // 마지막 원소의 인덱스를 계산한다\n int last = Index(front + queSize - 1);\n return nums[last];\n }\n\n /* 출력에 사용할 배열 반환 */\n public int[] ToArray() {\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n int[] res = new int[queSize];\n for (int i = 0, j = front; i &lt; queSize; i++, j++) {\n res[i] = nums[Index(j)];\n }\n return res;\n }\n}\n</code></pre> array_deque.go<pre><code>/* 원형 배열로 구현한 덱 */\ntype arrayDeque struct {\n nums []int // 덱의 원소를 저장하는 배열\n front int // 맨 앞 원소를 가리키는 맨 앞 포인터\n queSize int // 덱 길이\n queCapacity int // 큐 용량(저장할 수 있는 최대 원소 개수)\n}\n\n/* 큐 초기화 */\nfunc newArrayDeque(queCapacity int) *arrayDeque {\n return &amp;arrayDeque{\n nums: make([]int, queCapacity),\n queCapacity: queCapacity,\n front: 0,\n queSize: 0,\n }\n}\n\n/* 덱의 길이 반환 */\nfunc (q *arrayDeque) size() int {\n return q.queSize\n}\n\n/* 덱이 비어 있는지 판단 */\nfunc (q *arrayDeque) isEmpty() bool {\n return q.queSize == 0\n}\n\n/* 원형 배열 인덱스 계산 */\nfunc (q *arrayDeque) index(i int) int {\n // 나머지 연산을 통해 배열의 처음과 끝을 연결한다\n // i가 배열의 끝을 넘어가면 처음으로 돌아온다\n // i가 배열의 처음을 넘어가면 끝으로 돌아온다\n return (i + q.queCapacity) % q.queCapacity\n}\n\n/* 맨 앞에 삽입 */\nfunc (q *arrayDeque) pushFirst(num int) {\n if q.queSize == q.queCapacity {\n fmt.Println(\"덱이 가득 찼다\")\n return\n }\n // 맨 앞 포인터를 왼쪽으로 한 칸 이동한다\n // 나머지 연산을 통해 front가 배열의 처음을 넘어가면 끝으로 돌아오게 한다\n q.front = q.index(q.front - 1)\n // num을 맨 앞에 추가한다\n q.nums[q.front] = num\n q.queSize++\n}\n\n/* 맨 뒤에 삽입 */\nfunc (q *arrayDeque) pushLast(num int) {\n if q.queSize == q.queCapacity {\n fmt.Println(\"덱이 가득 찼다\")\n return\n }\n // 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n rear := q.index(q.front + q.queSize)\n // num을 맨 뒤에 추가한다\n q.nums[rear] = num\n q.queSize++\n}\n\n/* 맨 앞에서 꺼내기 */\nfunc (q *arrayDeque) popFirst() any {\n num := q.peekFirst()\n if num == nil {\n return nil\n }\n // 맨 앞 포인터를 뒤로 한 칸 이동한다\n q.front = q.index(q.front + 1)\n q.queSize--\n return num\n}\n\n/* 맨 뒤에서 꺼내기 */\nfunc (q *arrayDeque) popLast() any {\n num := q.peekLast()\n if num == nil {\n return nil\n }\n q.queSize--\n return num\n}\n\n/* 맨 앞 원소 접근 */\nfunc (q *arrayDeque) peekFirst() any {\n if q.isEmpty() {\n return nil\n }\n return q.nums[q.front]\n}\n\n/* 맨 뒤 원소 접근 */\nfunc (q *arrayDeque) peekLast() any {\n if q.isEmpty() {\n return nil\n }\n // 마지막 원소의 인덱스를 계산한다\n last := q.index(q.front + q.queSize - 1)\n return q.nums[last]\n}\n\n/* 출력에 사용할 Slice 반환 */\nfunc (q *arrayDeque) toSlice() []int {\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n res := make([]int, q.queSize)\n for i, j := 0, q.front; i &lt; q.queSize; i++ {\n res[i] = q.nums[q.index(j)]\n j++\n }\n return res\n}\n</code></pre> array_deque.swift<pre><code>/* 원형 배열로 구현한 덱 */\nclass ArrayDeque {\n private var nums: [Int] // 덱의 원소를 저장하는 배열\n private var front: Int // 맨 앞 원소를 가리키는 맨 앞 포인터\n private var _size: Int // 덱 길이\n\n /* 생성자 */\n init(capacity: Int) {\n nums = Array(repeating: 0, count: capacity)\n front = 0\n _size = 0\n }\n\n /* 덱의 용량 반환 */\n func capacity() -&gt; Int {\n nums.count\n }\n\n /* 덱의 길이 반환 */\n func size() -&gt; Int {\n _size\n }\n\n /* 덱이 비어 있는지 판단 */\n func isEmpty() -&gt; Bool {\n size() == 0\n }\n\n /* 원형 배열 인덱스 계산 */\n private func index(i: Int) -&gt; Int {\n // 나머지 연산을 통해 배열의 처음과 끝을 연결한다\n // i가 배열의 끝을 넘어가면 처음으로 돌아온다\n // i가 배열의 처음을 넘어가면 끝으로 돌아온다\n (i + capacity()) % capacity()\n }\n\n /* 맨 앞에 삽입 */\n func pushFirst(num: Int) {\n if size() == capacity() {\n print(\"덱이 가득 찼다\")\n return\n }\n // 맨 앞 포인터를 왼쪽으로 한 칸 이동한다\n // 나머지 연산을 통해 front가 배열의 처음을 넘어가면 끝으로 돌아오게 한다\n front = index(i: front - 1)\n // num을 맨 앞에 추가한다\n nums[front] = num\n _size += 1\n }\n\n /* 맨 뒤에 삽입 */\n func pushLast(num: Int) {\n if size() == capacity() {\n print(\"덱이 가득 찼다\")\n return\n }\n // 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n let rear = index(i: front + size())\n // num을 맨 뒤에 추가한다\n nums[rear] = num\n _size += 1\n }\n\n /* 맨 앞에서 꺼내기 */\n func popFirst() -&gt; Int {\n let num = peekFirst()\n // 맨 앞 포인터를 뒤로 한 칸 이동한다\n front = index(i: front + 1)\n _size -= 1\n return num\n }\n\n /* 맨 뒤에서 꺼내기 */\n func popLast() -&gt; Int {\n let num = peekLast()\n _size -= 1\n return num\n }\n\n /* 맨 앞 원소 접근 */\n func peekFirst() -&gt; Int {\n if isEmpty() {\n fatalError(\"덱이 비어 있다\")\n }\n return nums[front]\n }\n\n /* 맨 뒤 원소 접근 */\n func peekLast() -&gt; Int {\n if isEmpty() {\n fatalError(\"덱이 비어 있다\")\n }\n // 마지막 원소의 인덱스를 계산한다\n let last = index(i: front + size() - 1)\n return nums[last]\n }\n\n /* 출력에 사용할 배열 반환 */\n func toArray() -&gt; [Int] {\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n (front ..&lt; front + size()).map { nums[index(i: $0)] }\n }\n}\n</code></pre> array_deque.js<pre><code>/* 원형 배열로 구현한 덱 */\nclass ArrayDeque {\n #nums; // 덱의 원소를 저장하는 배열\n #front; // 맨 앞 원소를 가리키는 맨 앞 포인터\n #queSize; // 덱 길이\n\n /* 생성자 */\n constructor(capacity) {\n this.#nums = new Array(capacity);\n this.#front = 0;\n this.#queSize = 0;\n }\n\n /* 덱의 용량 반환 */\n capacity() {\n return this.#nums.length;\n }\n\n /* 덱의 길이 반환 */\n size() {\n return this.#queSize;\n }\n\n /* 덱이 비어 있는지 판단 */\n isEmpty() {\n return this.#queSize === 0;\n }\n\n /* 원형 배열 인덱스 계산 */\n index(i) {\n // 나머지 연산을 통해 배열의 처음과 끝을 연결한다\n // i가 배열의 끝을 넘어가면 처음으로 돌아온다\n // i가 배열의 처음을 넘어가면 끝으로 돌아온다\n return (i + this.capacity()) % this.capacity();\n }\n\n /* 맨 앞에 삽입 */\n pushFirst(num) {\n if (this.#queSize === this.capacity()) {\n console.log('덱이 가득 찼다');\n return;\n }\n // 맨 앞 포인터를 왼쪽으로 한 칸 이동한다\n // 나머지 연산을 통해 front가 배열의 처음을 넘어가면 끝으로 돌아오게 한다\n this.#front = this.index(this.#front - 1);\n // num을 맨 앞에 추가한다\n this.#nums[this.#front] = num;\n this.#queSize++;\n }\n\n /* 맨 뒤에 삽입 */\n pushLast(num) {\n if (this.#queSize === this.capacity()) {\n console.log('덱이 가득 찼다');\n return;\n }\n // 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n const rear = this.index(this.#front + this.#queSize);\n // num을 맨 뒤에 추가한다\n this.#nums[rear] = num;\n this.#queSize++;\n }\n\n /* 맨 앞에서 꺼내기 */\n popFirst() {\n const num = this.peekFirst();\n // 맨 앞 포인터를 뒤로 한 칸 이동한다\n this.#front = this.index(this.#front + 1);\n this.#queSize--;\n return num;\n }\n\n /* 맨 뒤에서 꺼내기 */\n popLast() {\n const num = this.peekLast();\n this.#queSize--;\n return num;\n }\n\n /* 맨 앞 원소 접근 */\n peekFirst() {\n if (this.isEmpty()) throw new Error('덱이 비어 있다.');\n return this.#nums[this.#front];\n }\n\n /* 맨 뒤 원소 접근 */\n peekLast() {\n if (this.isEmpty()) throw new Error('덱이 비어 있다.');\n // 마지막 원소의 인덱스를 계산한다\n const last = this.index(this.#front + this.#queSize - 1);\n return this.#nums[last];\n }\n\n /* 출력에 사용할 배열 반환 */\n toArray() {\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n const res = [];\n for (let i = 0, j = this.#front; i &lt; this.#queSize; i++, j++) {\n res[i] = this.#nums[this.index(j)];\n }\n return res;\n }\n}\n</code></pre> array_deque.ts<pre><code>/* 원형 배열로 구현한 덱 */\nclass ArrayDeque {\n private nums: number[]; // 덱의 원소를 저장하는 배열\n private front: number; // 맨 앞 원소를 가리키는 맨 앞 포인터\n private queSize: number; // 덱 길이\n\n /* 생성자 */\n constructor(capacity: number) {\n this.nums = new Array(capacity);\n this.front = 0;\n this.queSize = 0;\n }\n\n /* 덱의 용량 반환 */\n capacity(): number {\n return this.nums.length;\n }\n\n /* 덱의 길이 반환 */\n size(): number {\n return this.queSize;\n }\n\n /* 덱이 비어 있는지 판단 */\n isEmpty(): boolean {\n return this.queSize === 0;\n }\n\n /* 원형 배열 인덱스 계산 */\n index(i: number): number {\n // 나머지 연산을 통해 배열의 처음과 끝을 연결한다\n // i가 배열의 끝을 넘어가면 처음으로 돌아온다\n // i가 배열의 처음을 넘어가면 끝으로 돌아온다\n return (i + this.capacity()) % this.capacity();\n }\n\n /* 맨 앞에 삽입 */\n pushFirst(num: number): void {\n if (this.queSize === this.capacity()) {\n console.log('덱이 가득 찼다');\n return;\n }\n // 맨 앞 포인터를 왼쪽으로 한 칸 이동한다\n // 나머지 연산을 통해 front가 배열의 처음을 넘어가면 끝으로 돌아오게 한다\n this.front = this.index(this.front - 1);\n // num을 맨 앞에 추가한다\n this.nums[this.front] = num;\n this.queSize++;\n }\n\n /* 맨 뒤에 삽입 */\n pushLast(num: number): void {\n if (this.queSize === this.capacity()) {\n console.log('덱이 가득 찼다');\n return;\n }\n // 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n const rear: number = this.index(this.front + this.queSize);\n // num을 맨 뒤에 추가한다\n this.nums[rear] = num;\n this.queSize++;\n }\n\n /* 맨 앞에서 꺼내기 */\n popFirst(): number {\n const num: number = this.peekFirst();\n // 맨 앞 포인터를 뒤로 한 칸 이동한다\n this.front = this.index(this.front + 1);\n this.queSize--;\n return num;\n }\n\n /* 맨 뒤에서 꺼내기 */\n popLast(): number {\n const num: number = this.peekLast();\n this.queSize--;\n return num;\n }\n\n /* 맨 앞 원소 접근 */\n peekFirst(): number {\n if (this.isEmpty()) throw new Error('덱이 비어 있다.');\n return this.nums[this.front];\n }\n\n /* 맨 뒤 원소 접근 */\n peekLast(): number {\n if (this.isEmpty()) throw new Error('덱이 비어 있다.');\n // 마지막 원소의 인덱스를 계산한다\n const last = this.index(this.front + this.queSize - 1);\n return this.nums[last];\n }\n\n /* 출력에 사용할 배열 반환 */\n toArray(): number[] {\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n const res: number[] = [];\n for (let i = 0, j = this.front; i &lt; this.queSize; i++, j++) {\n res[i] = this.nums[this.index(j)];\n }\n return res;\n }\n}\n</code></pre> array_deque.dart<pre><code>/* 원형 배열로 구현한 덱 */\nclass ArrayDeque {\n late List&lt;int&gt; _nums; // 덱의 원소를 저장하는 배열\n late int _front; // 맨 앞 원소를 가리키는 맨 앞 포인터\n late int _queSize; // 덱 길이\n\n /* 생성자 */\n ArrayDeque(int capacity) {\n this._nums = List.filled(capacity, 0);\n this._front = this._queSize = 0;\n }\n\n /* 덱의 용량 반환 */\n int capacity() {\n return _nums.length;\n }\n\n /* 덱의 길이 반환 */\n int size() {\n return _queSize;\n }\n\n /* 덱이 비어 있는지 판단 */\n bool isEmpty() {\n return _queSize == 0;\n }\n\n /* 원형 배열 인덱스 계산 */\n int index(int i) {\n // 나머지 연산을 통해 배열의 처음과 끝을 연결한다\n // i가 배열의 끝을 넘어가면 처음으로 돌아온다\n // i가 배열의 처음을 넘어가면 끝으로 돌아온다\n return (i + capacity()) % capacity();\n }\n\n /* 맨 앞에 삽입 */\n void pushFirst(int _num) {\n if (_queSize == capacity()) {\n throw Exception(\"덱이 가득 찼다\");\n }\n // 맨 앞 포인터를 왼쪽으로 한 칸 이동한다\n // 나머지 연산을 사용하여 _front가 배열의 시작을 넘어가면 끝으로 돌아가게 한다\n _front = index(_front - 1);\n // _num을 맨 앞에 추가한다\n _nums[_front] = _num;\n _queSize++;\n }\n\n /* 맨 뒤에 삽입 */\n void pushLast(int _num) {\n if (_queSize == capacity()) {\n throw Exception(\"덱이 가득 찼다\");\n }\n // 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n int rear = index(_front + _queSize);\n // _num을 맨 뒤에 추가한다\n _nums[rear] = _num;\n _queSize++;\n }\n\n /* 맨 앞에서 꺼내기 */\n int popFirst() {\n int _num = peekFirst();\n // 맨 앞 포인터를 오른쪽으로 한 칸 이동한다\n _front = index(_front + 1);\n _queSize--;\n return _num;\n }\n\n /* 맨 뒤에서 꺼내기 */\n int popLast() {\n int _num = peekLast();\n _queSize--;\n return _num;\n }\n\n /* 맨 앞 원소 접근 */\n int peekFirst() {\n if (isEmpty()) {\n throw Exception(\"덱이 비어 있다\");\n }\n return _nums[_front];\n }\n\n /* 맨 뒤 원소 접근 */\n int peekLast() {\n if (isEmpty()) {\n throw Exception(\"덱이 비어 있다\");\n }\n // 마지막 원소의 인덱스를 계산한다\n int last = index(_front + _queSize - 1);\n return _nums[last];\n }\n\n /* 출력에 사용할 배열 반환 */\n List&lt;int&gt; toArray() {\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n List&lt;int&gt; res = List.filled(_queSize, 0);\n for (int i = 0, j = _front; i &lt; _queSize; i++, j++) {\n res[i] = _nums[index(j)];\n }\n return res;\n }\n}\n</code></pre> array_deque.rs<pre><code>/* 원형 배열로 구현한 덱 */\nstruct ArrayDeque&lt;T&gt; {\n nums: Vec&lt;T&gt;, // 덱의 원소를 저장하는 배열\n front: usize, // 맨 앞 원소를 가리키는 맨 앞 포인터\n que_size: usize, // 덱 길이\n}\n\nimpl&lt;T: Copy + Default&gt; ArrayDeque&lt;T&gt; {\n /* 생성자 */\n pub fn new(capacity: usize) -&gt; Self {\n Self {\n nums: vec![T::default(); capacity],\n front: 0,\n que_size: 0,\n }\n }\n\n /* 덱의 용량 반환 */\n pub fn capacity(&amp;self) -&gt; usize {\n self.nums.len()\n }\n\n /* 덱의 길이 반환 */\n pub fn size(&amp;self) -&gt; usize {\n self.que_size\n }\n\n /* 덱이 비어 있는지 판단 */\n pub fn is_empty(&amp;self) -&gt; bool {\n self.que_size == 0\n }\n\n /* 원형 배열 인덱스 계산 */\n fn index(&amp;self, i: i32) -&gt; usize {\n // 나머지 연산을 통해 배열의 처음과 끝을 연결한다\n // i가 배열의 끝을 넘어가면 처음으로 돌아온다\n // i가 배열의 처음을 넘어가면 끝으로 돌아온다\n ((i + self.capacity() as i32) % self.capacity() as i32) as usize\n }\n\n /* 맨 앞에 삽입 */\n pub fn push_first(&amp;mut self, num: T) {\n if self.que_size == self.capacity() {\n println!(\"덱이 가득 찼다\");\n return;\n }\n // 맨 앞 포인터를 왼쪽으로 한 칸 이동한다\n // 나머지 연산을 통해 front가 배열의 처음을 넘어가면 끝으로 돌아오게 한다\n self.front = self.index(self.front as i32 - 1);\n // num을 맨 앞에 추가한다\n self.nums[self.front] = num;\n self.que_size += 1;\n }\n\n /* 맨 뒤에 삽입 */\n pub fn push_last(&amp;mut self, num: T) {\n if self.que_size == self.capacity() {\n println!(\"덱이 가득 찼다\");\n return;\n }\n // 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n let rear = self.index(self.front as i32 + self.que_size as i32);\n // num을 맨 뒤에 추가한다\n self.nums[rear] = num;\n self.que_size += 1;\n }\n\n /* 맨 앞에서 꺼내기 */\n fn pop_first(&amp;mut self) -&gt; T {\n let num = self.peek_first();\n // 맨 앞 포인터를 뒤로 한 칸 이동한다\n self.front = self.index(self.front as i32 + 1);\n self.que_size -= 1;\n num\n }\n\n /* 맨 뒤에서 꺼내기 */\n fn pop_last(&amp;mut self) -&gt; T {\n let num = self.peek_last();\n self.que_size -= 1;\n num\n }\n\n /* 맨 앞 원소 접근 */\n fn peek_first(&amp;self) -&gt; T {\n if self.is_empty() {\n panic!(\"덱이 비어 있다\")\n };\n self.nums[self.front]\n }\n\n /* 맨 뒤 원소 접근 */\n fn peek_last(&amp;self) -&gt; T {\n if self.is_empty() {\n panic!(\"덱이 비어 있다\")\n };\n // 마지막 원소의 인덱스를 계산한다\n let last = self.index(self.front as i32 + self.que_size as i32 - 1);\n self.nums[last]\n }\n\n /* 출력에 사용할 배열 반환 */\n fn to_array(&amp;self) -&gt; Vec&lt;T&gt; {\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n let mut res = vec![T::default(); self.que_size];\n let mut j = self.front;\n for i in 0..self.que_size {\n res[i] = self.nums[self.index(j as i32)];\n j += 1;\n }\n res\n }\n}\n</code></pre> array_deque.c<pre><code>/* 원형 배열로 구현한 덱 */\ntypedef struct {\n int *nums; // 큐의 원소를 저장하는 배열\n int front; // 맨 앞 원소를 가리키는 맨 앞 포인터\n int queSize; // 맨 뒤 포인터, 큐의 맨 뒤 + 1을 가리킨다\n int queCapacity; // 큐 용량\n} ArrayDeque;\n\n/* 생성자 */\nArrayDeque *newArrayDeque(int capacity) {\n ArrayDeque *deque = (ArrayDeque *)malloc(sizeof(ArrayDeque));\n // 배열 초기화\n deque-&gt;queCapacity = capacity;\n deque-&gt;nums = (int *)malloc(sizeof(int) * deque-&gt;queCapacity);\n deque-&gt;front = deque-&gt;queSize = 0;\n return deque;\n}\n\n/* 소멸자 */\nvoid delArrayDeque(ArrayDeque *deque) {\n free(deque-&gt;nums);\n free(deque);\n}\n\n/* 덱의 용량 반환 */\nint capacity(ArrayDeque *deque) {\n return deque-&gt;queCapacity;\n}\n\n/* 덱의 길이 반환 */\nint size(ArrayDeque *deque) {\n return deque-&gt;queSize;\n}\n\n/* 덱이 비어 있는지 판단 */\nbool empty(ArrayDeque *deque) {\n return deque-&gt;queSize == 0;\n}\n\n/* 원형 배열 인덱스 계산 */\nint dequeIndex(ArrayDeque *deque, int i) {\n // 나머지 연산을 통해 배열의 처음과 끝을 연결한다\n // i가 배열의 끝을 넘어가면 처음으로 돌아온다\n // i가 배열의 처음을 넘어가면 끝으로 돌아온다\n return ((i + capacity(deque)) % capacity(deque));\n}\n\n/* 맨 앞에 삽입 */\nvoid pushFirst(ArrayDeque *deque, int num) {\n if (deque-&gt;queSize == capacity(deque)) {\n printf(\"덱이 가득 찼다\\r\\n\");\n return;\n }\n // 맨 앞 포인터를 왼쪽으로 한 칸 이동한다\n // 나머지 연산으로 front가 배열의 처음을 넘어가면 끝으로 돌아가게 한다\n deque-&gt;front = dequeIndex(deque, deque-&gt;front - 1);\n // num을 큐의 맨 앞에 추가한다\n deque-&gt;nums[deque-&gt;front] = num;\n deque-&gt;queSize++;\n}\n\n/* 맨 뒤에 삽입 */\nvoid pushLast(ArrayDeque *deque, int num) {\n if (deque-&gt;queSize == capacity(deque)) {\n printf(\"덱이 가득 찼다\\r\\n\");\n return;\n }\n // 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n int rear = dequeIndex(deque, deque-&gt;front + deque-&gt;queSize);\n // num을 맨 뒤에 추가한다\n deque-&gt;nums[rear] = num;\n deque-&gt;queSize++;\n}\n\n/* 맨 앞 원소 접근 */\nint peekFirst(ArrayDeque *deque) {\n // 접근 예외: 덱이 비어 있다\n assert(empty(deque) == 0);\n return deque-&gt;nums[deque-&gt;front];\n}\n\n/* 맨 뒤 원소 접근 */\nint peekLast(ArrayDeque *deque) {\n // 접근 예외: 덱이 비어 있다\n assert(empty(deque) == 0);\n int last = dequeIndex(deque, deque-&gt;front + deque-&gt;queSize - 1);\n return deque-&gt;nums[last];\n}\n\n/* 맨 앞에서 꺼내기 */\nint popFirst(ArrayDeque *deque) {\n int num = peekFirst(deque);\n // 맨 앞 포인터를 뒤로 한 칸 이동한다\n deque-&gt;front = dequeIndex(deque, deque-&gt;front + 1);\n deque-&gt;queSize--;\n return num;\n}\n\n/* 맨 뒤에서 꺼내기 */\nint popLast(ArrayDeque *deque) {\n int num = peekLast(deque);\n deque-&gt;queSize--;\n return num;\n}\n\n/* 출력에 사용할 배열 반환 */\nint *toArray(ArrayDeque *deque, int *queSize) {\n *queSize = deque-&gt;queSize;\n int *res = (int *)calloc(deque-&gt;queSize, sizeof(int));\n int j = deque-&gt;front;\n for (int i = 0; i &lt; deque-&gt;queSize; i++) {\n res[i] = deque-&gt;nums[j % deque-&gt;queCapacity];\n j++;\n }\n return res;\n}\n</code></pre> array_deque.kt<pre><code>/* 생성자 */\nclass ArrayDeque(capacity: Int) {\n private var nums: IntArray = IntArray(capacity) // 덱의 원소를 저장하는 배열\n private var front: Int = 0 // 맨 앞 원소를 가리키는 맨 앞 포인터\n private var queSize: Int = 0 // 덱 길이\n\n /* 덱의 용량 반환 */\n fun capacity(): Int {\n return nums.size\n }\n\n /* 덱의 길이 반환 */\n fun size(): Int {\n return queSize\n }\n\n /* 덱이 비어 있는지 판단 */\n fun isEmpty(): Boolean {\n return queSize == 0\n }\n\n /* 원형 배열 인덱스 계산 */\n private fun index(i: Int): Int {\n // 나머지 연산을 통해 배열의 처음과 끝을 연결한다\n // i가 배열의 끝을 넘어가면 처음으로 돌아온다\n // i가 배열의 처음을 넘어가면 끝으로 돌아온다\n return (i + capacity()) % capacity()\n }\n\n /* 맨 앞에 삽입 */\n fun pushFirst(num: Int) {\n if (queSize == capacity()) {\n println(\"덱이 가득 찼다\")\n return\n }\n // 맨 앞 포인터를 왼쪽으로 한 칸 이동한다\n // 나머지 연산을 통해 front가 배열의 처음을 넘어가면 끝으로 돌아오게 한다\n front = index(front - 1)\n // num을 맨 앞에 추가한다\n nums[front] = num\n queSize++\n }\n\n /* 맨 뒤에 삽입 */\n fun pushLast(num: Int) {\n if (queSize == capacity()) {\n println(\"덱이 가득 찼다\")\n return\n }\n // 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n val rear = index(front + queSize)\n // num을 맨 뒤에 추가한다\n nums[rear] = num\n queSize++\n }\n\n /* 맨 앞에서 꺼내기 */\n fun popFirst(): Int {\n val num = peekFirst()\n // 맨 앞 포인터를 뒤로 한 칸 이동한다\n front = index(front + 1)\n queSize--\n return num\n }\n\n /* 맨 뒤에서 꺼내기 */\n fun popLast(): Int {\n val num = peekLast()\n queSize--\n return num\n }\n\n /* 맨 앞 원소 접근 */\n fun peekFirst(): Int {\n if (isEmpty()) throw IndexOutOfBoundsException()\n return nums[front]\n }\n\n /* 맨 뒤 원소 접근 */\n fun peekLast(): Int {\n if (isEmpty()) throw IndexOutOfBoundsException()\n // 마지막 원소의 인덱스를 계산한다\n val last = index(front + queSize - 1)\n return nums[last]\n }\n\n /* 출력에 사용할 배열 반환 */\n fun toArray(): IntArray {\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n val res = IntArray(queSize)\n var i = 0\n var j = front\n while (i &lt; queSize) {\n res[i] = nums[index(j)]\n i++\n j++\n }\n return res\n }\n}\n</code></pre> array_deque.rb<pre><code>### 원형 배열로 구현한 덱 ###\nclass ArrayDeque\n ### 덱의 길이 반환 ###\n attr_reader :size\n\n ### 생성자 ###\n def initialize(capacity)\n @nums = Array.new(capacity, 0)\n @front = 0\n @size = 0\n end\n\n ### 덱의 용량 반환 ###\n def capacity\n @nums.length\n end\n\n ### 덱이 비어 있는지 판단 ###\n def is_empty?\n size.zero?\n end\n\n ### 맨 앞에 삽입 ###\n def push_first(num)\n if size == capacity\n puts '덱이 가득 찼다'\n return\n end\n\n # 맨 앞 포인터를 왼쪽으로 한 칸 이동한다\n # 나머지 연산을 통해 front가 배열의 처음을 넘어가면 끝으로 돌아오게 한다\n @front = index(@front - 1)\n # num을 맨 앞에 추가한다\n @nums[@front] = num\n @size += 1\n end\n\n ### 맨 뒤에 삽입 ###\n def push_last(num)\n if size == capacity\n puts '덱이 가득 찼다'\n return\n end\n\n # 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n rear = index(@front + size)\n # num을 맨 뒤에 추가한다\n @nums[rear] = num\n @size += 1\n end\n\n ### 맨 앞에서 꺼내기 ###\n def pop_first\n num = peek_first\n # 맨 앞 포인터를 뒤로 한 칸 이동한다\n @front = index(@front + 1)\n @size -= 1\n num\n end\n\n ### 맨 뒤에서 꺼내기 ###\n def pop_last\n num = peek_last\n @size -= 1\n num\n end\n\n ### 맨 앞 원소 접근 ###\n def peek_first\n raise IndexError, '덱이 비어 있다' if is_empty?\n\n @nums[@front]\n end\n\n ### 맨 뒤 원소 접근 ###\n def peek_last\n raise IndexError, '덱이 비어 있다' if is_empty?\n\n # 마지막 원소의 인덱스를 계산한다\n last = index(@front + size - 1)\n @nums[last]\n end\n\n ### 출력에 사용할 배열 반환 ###\n def to_array\n # 유효한 길이 범위 안의 리스트 원소만 변환한다\n res = []\n for i in 0...size\n res &lt;&lt; @nums[index(@front + i)]\n end\n res\n end\n\n private\n\n ### 원형 배열 인덱스 계산 ###\n def index(i)\n # 나머지 연산을 통해 배열의 처음과 끝을 연결한다\n # i가 배열의 끝을 넘어가면 처음으로 돌아온다\n # i가 배열의 처음을 넘어가면 끝으로 돌아온다\n (i + capacity) % capacity\n end\nend\n</code></pre>","path":["제 5 장   스택과 큐","5.3   덱"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#533","level":2,"title":"5.3.3   덱의 활용","text":"<p>덱은 스택과 큐의 논리를 모두 갖추므로 둘의 모든 활용 상황에 적용할 수 있으며, 더 높은 자유도도 제공한다.</p> <p>소프트웨어의 ‘실행 취소’는 보통 스택으로 구현한다. 시스템이 각 변경 작업을 스택에 <code>push</code>하고, <code>pop</code>으로 취소한다. 다만 자원 제약 때문에 보통 취소 가능한 단계 수를 제한한다(예를 들어 \\(50\\)단계만 저장). 스택 길이가 \\(50\\)을 넘으면 바닥(덱의 맨 앞)에서 삭제해야 한다. 스택은 이 기능을 지원하지 않으므로 덱으로 대체해야 한다. ‘실행 취소’의 핵심 논리는 여전히 스택의 후입선출 원리를 따르며, 덱은 여기에 필요한 추가 논리를 더 유연하게 구현하게 해 준다.</p>","path":["제 5 장   스택과 큐","5.3   덱"],"tags":[]},{"location":"chapter_stack_and_queue/exercises/","level":1,"title":"5.5   연습 문제","text":"","path":["제 5 장   스택과 큐","5.5   연습 문제"],"tags":[]},{"location":"chapter_stack_and_queue/exercises/#551","level":2,"title":"5.5.1   개념 확인","text":"","path":["제 5 장   스택과 큐","5.5   연습 문제"],"tags":[]},{"location":"chapter_stack_and_queue/exercises/#1","level":3,"title":"1.   스택과 큐에서 무엇이 먼저 나오는가","text":"<p>빈 스택 <code>S</code>와 빈 큐 <code>Q</code>를 준비하고, 각각에 같은 연산을 수행한다.</p> <p>1단계: <code>A</code>를 넣는다. 2단계: <code>B</code>를 넣는다. 3단계: 원소 하나를 꺼내 기록한다. 4단계: <code>C</code>를 넣는다. 5단계: 비어 있을 때까지 원소를 계속 꺼내 기록한다.</p> <p><code>S</code>와 <code>Q</code>에서 원소가 나오는 순서를 각각 적고, ‘후입선출’ 또는 ‘선입선출’로 차이를 설명하라.</p> 참고 답안 <p>스택 <code>S</code>에서 나오는 순서는 <code>B, C, A</code>다. <code>A, B</code>를 넣은 뒤 가장 최근에 넣은 <code>B</code>를 먼저 꺼내고 <code>C</code>를 넣는다. 이후 <code>C, A</code>를 차례대로 꺼내므로 ‘후입선출’의 특성을 보인다.</p> <p>큐 <code>Q</code>에서 나오는 순서는 <code>A, B, C</code>다. <code>A, B</code>를 넣은 뒤 가장 먼저 넣은 <code>A</code>를 꺼내고, <code>C</code>를 넣는다. 이후 <code>B, C</code>를 차례대로 꺼내므로 ‘선입선출’의 특성을 보인다.</p>","path":["제 5 장   스택과 큐","5.5   연습 문제"],"tags":[]},{"location":"chapter_stack_and_queue/exercises/#2","level":3,"title":"2.   큐의 뒤가 배열의 끝을 넘어가면 어떻게 되는가","text":"<p>길이가 5인 원형 배열로 큐를 구현하며, 배열의 인덱스는 <code>0~4</code>다. 현재 <code>front = 3</code>, <code>size = 2</code>이고 큐의 <code>A, B</code>는 각각 인덱스 3과 4에 있다.</p> <ol> <li>‘<code>C</code>를 큐에 넣기’를 수행하면 <code>C</code>를 어느 인덱스에 저장해야 하는가? 삽입 후 <code>size</code>는 얼마인가?</li> <li>이어서 큐에서 한 번 꺼내면 어떤 원소가 나오는가? 새로운 <code>front</code>와 <code>size</code>는 각각 얼마인가?</li> <li>이때 큐의 앞에서 뒤로 이어지는 논리적 순서는 무엇인가? 원소를 꺼낼 때 배열의 다른 원소도 이동해야 하는가? 이유를 설명하라.</li> </ol> 참고 답안 <ol> <li> <p>새 원소의 위치는 <code>(front + size) % 5 = (3 + 2) % 5 = 0</code>이므로, <code>C</code>는 인덱스 0에 저장된다. 삽입 후 <code>size = 3</code>이다.</p> </li> <li> <p>현재 큐의 맨 앞 원소인 <code>A</code>가 나온다. 새로운 맨 앞 인덱스는 <code>(3 + 1) % 5 = 4</code>이므로 <code>front = 4</code>, <code>size = 2</code>다.</p> </li> <li> <p>유효한 원소의 논리적 순서는 <code>B, C</code>이며, <code>B</code>는 인덱스 4에, <code>C</code>는 인덱스 0에 있다. 원소를 꺼낼 때는 <code>front</code>를 이동하고 <code>size</code>만 수정하면 된다. 원형 배열에서는 나머지 연산으로 인덱스가 처음으로 돌아가므로, 다른 원소를 모두 앞으로 옮길 필요가 없다.</p> </li> </ol>","path":["제 5 장   스택과 큐","5.5   연습 문제"],"tags":[]},{"location":"chapter_stack_and_queue/exercises/#3","level":3,"title":"3.   덱의 양 끝 연산","text":"<p>여기서는 <code>push_first</code>를 앞에 넣기, <code>push_last</code>를 뒤에 넣기, <code>pop_first</code>를 앞에서 꺼내기, <code>pop_last</code>를 뒤에서 꺼내기로 정의한다.</p> <p>빈 덱 <code>deq</code>에 다음 연산을 차례대로 수행한다.</p> <ol> <li><code>push_last(A)</code></li> <li><code>push_last(B)</code></li> <li><code>push_first(C)</code></li> <li><code>pop_last()</code></li> <li><code>push_last(D)</code></li> <li><code>pop_first()</code></li> </ol> <ol> <li>두 번 꺼낸 원소는 각각 무엇인가?</li> <li>모든 연산을 마친 뒤 앞에서 뒤까지 어떤 원소가 남는가?</li> <li>뒤에서만 넣고 앞에서만 삭제할 수 있는 큐로 이 6단계를 모두 수행할 수 있는가? 불가능하다면 수행할 수 없는 연산을 찾아라. 이어서 덱은 모두 수행할 수 있는지와 그 이유를 설명하라.</li> </ol> 참고 답안 <p>처음 세 단계를 마치면 덱의 앞에서 뒤까지는 <code>[C, A, B]</code>다.</p> <ol> <li> <p><code>pop_last()</code>로 <code>B</code>를 꺼낸다. <code>D</code>를 넣으면 <code>[C, A, D]</code>가 되고, <code>pop_first()</code>로 <code>C</code>를 꺼낸다.</p> </li> <li> <p>마지막에는 <code>[A, D]</code>가 남는다.</p> </li> <li> <p>뒤에서만 넣고 앞에서만 삭제하는 큐로는 모든 연산을 수행할 수 없다. 3단계의 <code>push_first(C)</code>는 앞에 넣어야 하고, 4단계의 <code>pop_last()</code>는 뒤에서 삭제해야 하므로 이 큐가 지원하는 연산 범위를 벗어난다. 덱은 양 끝에서 모두 삽입과 삭제가 가능하므로 6단계를 전부 수행할 수 있다.</p> </li> </ol>","path":["제 5 장   스택과 큐","5.5   연습 문제"],"tags":[]},{"location":"chapter_stack_and_queue/exercises/#552","level":2,"title":"5.5.2   프로그래밍 연습","text":"","path":["제 5 장   스택과 큐","5.5   연습 문제"],"tags":[]},{"location":"chapter_stack_and_queue/exercises/#1_1","level":3,"title":"1.   괄호 문자열 검사하기","text":"<p>세 종류의 괄호 <code>()</code>, <code>[]</code>, <code>{}</code>만 포함하는 문자열 <code>s</code>가 주어진다. 스택을 사용하여 올바른 괄호 문자열인지 판단하라.</p> <p>올바른 문자열은 다음 두 조건을 모두 만족해야 한다. 닫는 괄호는 가장 최근에 등장한, 아직 짝이 없는 여는 괄호와 종류가 같아야 하며, 순회를 마친 뒤 짝이 없는 여는 괄호가 남아 있어서는 안 된다. 판단 결과를 불리언 값으로 반환하라.</p> 풀이 힌트 <ol> <li>‘닫는 괄호에서 대응하는 여는 괄호로의 매핑’을 만들 수 있다.</li> <li>닫는 괄호를 만나면 먼저 스택이 비었는지 확인한 뒤 맨 위 괄호가 짝이 맞는지 확인한다.</li> <li>순회가 끝난 뒤에도 스택이 비어 있어야 한다.</li> </ol> <p>LeetCode</p>","path":["제 5 장   스택과 큐","5.5   연습 문제"],"tags":[]},{"location":"chapter_stack_and_queue/queue/","level":1,"title":"5.2   큐","text":"<p>큐(queue)는 선입선출 규칙을 따르는 선형 자료 구조다. 이름 그대로 줄 서는 현상을 재현한다. 새로 온 사람은 계속 줄의 뒤에 서고, 맨 앞에 있는 사람부터 하나씩 떠난다.</p> <p>아래 그림(5-4)과 같이 큐의 앞쪽을 ‘큐의 맨 앞’, 뒤쪽을 ‘큐의 맨 뒤’라고 한다. 맨 뒤에 원소를 추가하는 연산을 ‘큐에 삽입하기’, 맨 앞 원소를 삭제하는 연산을 ‘큐에서 꺼내기’라고 한다.</p> <p></p> <p> 그림 5-4   큐의 선입선출 규칙 </p>","path":["제 5 장   스택과 큐","5.2   큐"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#521","level":2,"title":"5.2.1   큐의 기본 연산","text":"<p>큐의 기본 연산은 아래 표(5-2)와 같다. 메서드 이름은 프로그래밍 언어에 따라 다를 수 있으며, 여기서는 스택과 같은 이름을 사용한다.</p> <p> 표 5-2   큐의 연산 효율 </p> 메서드 설명 시간 복잡도 <code>push()</code> 큐에 원소 삽입, 즉 맨 뒤에 추가 \\(O(1)\\) <code>pop()</code> 큐의 맨 앞 원소를 꺼냄 \\(O(1)\\) <code>peek()</code> 큐의 맨 앞 원소에 접근 \\(O(1)\\) <p>프로그래밍 언어가 제공하는 큐 클래스를 바로 사용할 수 있다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby queue.py<pre><code>from collections import deque\n\n# 큐 초기화\n# Python에서는 보통 덱 클래스 deque를 큐로 사용한다\n# queue.Queue()는 전용 큐 클래스이지만 사용이 불편하므로 권하지 않는다\nque: deque[int] = deque()\n\n# 큐에 원소 삽입\nque.append(1)\nque.append(3)\nque.append(2)\nque.append(5)\nque.append(4)\n\n# 큐의 맨 앞 원소에 접근\nfront: int = que[0]\n\n# 큐에서 원소 꺼내기\npop: int = que.popleft()\n\n# 큐 길이 구하기\nsize: int = len(que)\n\n# 큐가 비어 있는지 확인\nis_empty: bool = len(que) == 0\n</code></pre> queue.cpp<pre><code>/* 큐 초기화 */\nqueue&lt;int&gt; queue;\n\n/* 큐에 원소 삽입 */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* 큐의 맨 앞 원소에 접근 */\nint front = queue.front();\n\n/* 큐에서 원소 꺼내기 */\nqueue.pop();\n\n/* 큐 길이 구하기 */\nint size = queue.size();\n\n/* 큐가 비어 있는지 확인 */\nbool empty = queue.empty();\n</code></pre> queue.java<pre><code>/* 큐 초기화 */\nQueue&lt;Integer&gt; queue = new LinkedList&lt;&gt;();\n\n/* 큐에 원소 삽입 */\nqueue.offer(1);\nqueue.offer(3);\nqueue.offer(2);\nqueue.offer(5);\nqueue.offer(4);\n\n/* 큐의 맨 앞 원소에 접근 */\nint peek = queue.peek();\n\n/* 큐에서 원소 꺼내기 */\nint pop = queue.poll();\n\n/* 큐 길이 구하기 */\nint size = queue.size();\n\n/* 큐가 비어 있는지 확인 */\nboolean isEmpty = queue.isEmpty();\n</code></pre> queue.cs<pre><code>/* 큐 초기화 */\nQueue&lt;int&gt; queue = new();\n\n/* 큐에 원소 삽입 */\nqueue.Enqueue(1);\nqueue.Enqueue(3);\nqueue.Enqueue(2);\nqueue.Enqueue(5);\nqueue.Enqueue(4);\n\n/* 큐의 맨 앞 원소에 접근 */\nint peek = queue.Peek();\n\n/* 큐에서 원소 꺼내기 */\nint pop = queue.Dequeue();\n\n/* 큐 길이 구하기 */\nint size = queue.Count;\n\n/* 큐가 비어 있는지 확인 */\nbool isEmpty = queue.Count == 0;\n</code></pre> queue_test.go<pre><code>/* 큐 초기화 */\n// Go에서는 list를 큐로 사용한다\nqueue := list.New()\n\n/* 큐에 원소 삽입 */\nqueue.PushBack(1)\nqueue.PushBack(3)\nqueue.PushBack(2)\nqueue.PushBack(5)\nqueue.PushBack(4)\n\n/* 큐의 맨 앞 원소에 접근 */\npeek := queue.Front()\n\n/* 큐에서 원소 꺼내기 */\npop := queue.Front()\nqueue.Remove(pop)\n\n/* 큐 길이 구하기 */\nsize := queue.Len()\n\n/* 큐가 비어 있는지 확인 */\nisEmpty := queue.Len() == 0\n</code></pre> queue.swift<pre><code>/* 큐 초기화 */\n// Swift에는 내장 큐 클래스가 없으므로 Array를 큐로 사용할 수 있다\nvar queue: [Int] = []\n\n/* 큐에 원소 삽입 */\nqueue.append(1)\nqueue.append(3)\nqueue.append(2)\nqueue.append(5)\nqueue.append(4)\n\n/* 큐의 맨 앞 원소에 접근 */\nlet peek = queue.first!\n\n/* 큐에서 원소 꺼내기 */\n// 배열이므로 removeFirst의 복잡도는 O(n)이다\nlet pop = queue.removeFirst()\n\n/* 큐 길이 구하기 */\nlet size = queue.count\n\n/* 큐가 비어 있는지 확인 */\nlet isEmpty = queue.isEmpty\n</code></pre> queue.js<pre><code>/* 큐 초기화 */\n// JavaScript에는 내장 큐가 없으므로 Array를 큐로 사용할 수 있다\nconst queue = [];\n\n/* 큐에 원소 삽입 */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* 큐의 맨 앞 원소에 접근 */\nconst peek = queue[0];\n\n/* 큐에서 원소 꺼내기 */\n// 기반이 배열이므로 shift() 메서드의 시간 복잡도는 O(n)이다\nconst pop = queue.shift();\n\n/* 큐 길이 구하기 */\nconst size = queue.length;\n\n/* 큐가 비어 있는지 확인 */\nconst empty = queue.length === 0;\n</code></pre> queue.ts<pre><code>/* 큐 초기화 */\n// TypeScript에는 내장 큐가 없으므로 Array를 큐로 사용할 수 있다\nconst queue: number[] = [];\n\n/* 큐에 원소 삽입 */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* 큐의 맨 앞 원소에 접근 */\nconst peek = queue[0];\n\n/* 큐에서 원소 꺼내기 */\n// 기반이 배열이므로 shift() 메서드의 시간 복잡도는 O(n)이다\nconst pop = queue.shift();\n\n/* 큐 길이 구하기 */\nconst size = queue.length;\n\n/* 큐가 비어 있는지 확인 */\nconst empty = queue.length === 0;\n</code></pre> queue.dart<pre><code>/* 큐 초기화 */\n// Dart의 Queue 클래스는 덱이며 일반 큐로도 사용할 수 있다\nQueue&lt;int&gt; queue = Queue();\n\n/* 큐에 원소 삽입 */\nqueue.add(1);\nqueue.add(3);\nqueue.add(2);\nqueue.add(5);\nqueue.add(4);\n\n/* 큐의 맨 앞 원소에 접근 */\nint peek = queue.first;\n\n/* 큐에서 원소 꺼내기 */\nint pop = queue.removeFirst();\n\n/* 큐 길이 구하기 */\nint size = queue.length;\n\n/* 큐가 비어 있는지 확인 */\nbool isEmpty = queue.isEmpty;\n</code></pre> queue.rs<pre><code>/* 덱 초기화 */\n// Rust에서는 덱을 일반 큐로 사용한다\nlet mut deque: VecDeque&lt;u32&gt; = VecDeque::new();\n\n/* 큐에 원소 삽입 */\ndeque.push_back(1);\ndeque.push_back(3);\ndeque.push_back(2);\ndeque.push_back(5);\ndeque.push_back(4);\n\n/* 큐의 맨 앞 원소에 접근 */\nif let Some(front) = deque.front() {\n}\n\n/* 큐에서 원소 꺼내기 */\nif let Some(pop) = deque.pop_front() {\n}\n\n/* 큐 길이 구하기 */\nlet size = deque.len();\n\n/* 큐가 비어 있는지 확인 */\nlet is_empty = deque.is_empty();\n</code></pre> queue.c<pre><code>// C는 내장 큐를 제공하지 않는다\n</code></pre> queue.kt<pre><code>/* 큐 초기화 */\nval queue = LinkedList&lt;Int&gt;()\n\n/* 큐에 원소 삽입 */\nqueue.offer(1)\nqueue.offer(3)\nqueue.offer(2)\nqueue.offer(5)\nqueue.offer(4)\n\n/* 큐의 맨 앞 원소에 접근 */\nval peek = queue.peek()\n\n/* 큐에서 원소 꺼내기 */\nval pop = queue.poll()\n\n/* 큐 길이 구하기 */\nval size = queue.size\n\n/* 큐가 비어 있는지 확인 */\nval isEmpty = queue.isEmpty()\n</code></pre> queue.rb<pre><code># 큐 초기화\n# Ruby의 내장 큐(Thread::Queue)에는 peek와 순회 메서드가 없으므로 Array를 큐로 사용할 수 있다\nqueue = []\n\n# 큐에 원소 삽입\nqueue.push(1)\nqueue.push(3)\nqueue.push(2)\nqueue.push(5)\nqueue.push(4)\n\n# 큐 원소에 접근\npeek = queue.first\n\n# 큐에서 원소 꺼내기\n# 배열이므로 Array#shift 메서드의 시간 복잡도는 O(n)임에 주의\npop = queue.shift\n\n# 큐 길이 구하기\nsize = queue.length\n\n# 큐가 비어 있는지 확인\nis_empty = queue.empty?\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 5 장   스택과 큐","5.2   큐"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#522","level":2,"title":"5.2.2   큐 구현","text":"<p>큐를 구현하려면 한쪽 끝에서 원소를 추가하고 반대쪽 끝에서 삭제할 수 있는 자료 구조가 필요하다. 연결 리스트와 배열 모두 이 조건을 만족한다.</p>","path":["제 5 장   스택과 큐","5.2   큐"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#1","level":3,"title":"1.   연결 리스트 기반 구현","text":"<p>아래 그림(5-5)과 같이 연결 리스트의 ‘머리 노드’와 ‘꼬리 노드’를 각각 ‘큐의 맨 앞’과 ‘큐의 맨 뒤’로 볼 수 있다. 맨 뒤에서는 노드 추가만, 맨 앞에서는 노드 삭제만 허용한다.</p> &lt;1&gt;&lt;2&gt;&lt;3&gt; <p></p> <p></p> <p></p> <p> 그림 5-5   연결 리스트 기반 큐의 삽입과 꺼내기 </p> <p>연결 리스트로 큐를 구현한 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_queue.py<pre><code>class LinkedListQueue:\n \"\"\"연결 리스트로 구현한 큐\"\"\"\n\n def __init__(self):\n \"\"\"생성자\"\"\"\n self._front: ListNode | None = None # 머리 노드 front\n self._rear: ListNode | None = None # 꼬리 노드 rear\n self._size: int = 0\n\n def size(self) -&gt; int:\n \"\"\"큐의 길이 반환\"\"\"\n return self._size\n\n def is_empty(self) -&gt; bool:\n \"\"\"큐가 비어 있는지 판단\"\"\"\n return self._size == 0\n\n def push(self, num: int):\n \"\"\"큐에 삽입\"\"\"\n # 꼬리 노드 뒤에 num을 추가한다\n node = ListNode(num)\n # 큐가 비어 있으면 머리와 꼬리 노드가 모두 해당 노드를 가리키게 한다\n if self._front is None:\n self._front = node\n self._rear = node\n # 큐가 비어 있지 않으면 해당 노드를 꼬리 노드 뒤에 추가한다\n else:\n self._rear.next = node\n self._rear = node\n self._size += 1\n\n def pop(self) -&gt; int:\n \"\"\"큐에서 꺼내기\"\"\"\n num = self.peek()\n # 머리 노드를 삭제한다\n self._front = self._front.next\n self._size -= 1\n return num\n\n def peek(self) -&gt; int:\n \"\"\"맨 앞 원소 접근\"\"\"\n if self.is_empty():\n raise IndexError(\"큐가 비어 있다\")\n return self._front.val\n\n def to_list(self) -&gt; list[int]:\n \"\"\"출력에 사용할 리스트로 변환\"\"\"\n queue = []\n temp = self._front\n while temp:\n queue.append(temp.val)\n temp = temp.next\n return queue\n</code></pre> linkedlist_queue.cpp<pre><code>/* 연결 리스트로 구현한 큐 */\nclass LinkedListQueue {\n private:\n ListNode *front, *rear; // 머리 노드 front, 꼬리 노드 rear\n int queSize;\n\n public:\n LinkedListQueue() {\n front = nullptr;\n rear = nullptr;\n queSize = 0;\n }\n\n ~LinkedListQueue() {\n // 연결 리스트를 순회하며 노드를 삭제하여 메모리를 해제한다\n freeMemoryLinkedList(front);\n }\n\n /* 큐의 길이 반환 */\n int size() {\n return queSize;\n }\n\n /* 큐가 비어 있는지 판단 */\n bool isEmpty() {\n return queSize == 0;\n }\n\n /* 큐에 삽입 */\n void push(int num) {\n // 꼬리 노드 뒤에 num을 추가한다\n ListNode *node = new ListNode(num);\n // 큐가 비어 있으면 머리와 꼬리 노드가 모두 해당 노드를 가리키게 한다\n if (front == nullptr) {\n front = node;\n rear = node;\n }\n // 큐가 비어 있지 않으면 해당 노드를 꼬리 노드 뒤에 추가한다\n else {\n rear-&gt;next = node;\n rear = node;\n }\n queSize++;\n }\n\n /* 큐에서 꺼내기 */\n int pop() {\n int num = peek();\n // 머리 노드를 삭제한다\n ListNode *tmp = front;\n front = front-&gt;next;\n // 메모리 해제\n delete tmp;\n queSize--;\n return num;\n }\n\n /* 맨 앞 원소 접근 */\n int peek() {\n if (size() == 0)\n throw out_of_range(\"큐가 비어 있다\");\n return front-&gt;val;\n }\n\n /* 연결 리스트를 Vector로 변환하여 반환 */\n vector&lt;int&gt; toVector() {\n ListNode *node = front;\n vector&lt;int&gt; res(size());\n for (int i = 0; i &lt; res.size(); i++) {\n res[i] = node-&gt;val;\n node = node-&gt;next;\n }\n return res;\n }\n};\n</code></pre> linkedlist_queue.java<pre><code>/* 연결 리스트로 구현한 큐 */\nclass LinkedListQueue {\n private ListNode front, rear; // 머리 노드 front, 꼬리 노드 rear\n private int queSize = 0;\n\n public LinkedListQueue() {\n front = null;\n rear = null;\n }\n\n /* 큐의 길이 반환 */\n public int size() {\n return queSize;\n }\n\n /* 큐가 비어 있는지 판단 */\n public boolean isEmpty() {\n return size() == 0;\n }\n\n /* 큐에 삽입 */\n public void push(int num) {\n // 꼬리 노드 뒤에 num을 추가한다\n ListNode node = new ListNode(num);\n // 큐가 비어 있으면 머리와 꼬리 노드가 모두 해당 노드를 가리키게 한다\n if (front == null) {\n front = node;\n rear = node;\n // 큐가 비어 있지 않으면 해당 노드를 꼬리 노드 뒤에 추가한다\n } else {\n rear.next = node;\n rear = node;\n }\n queSize++;\n }\n\n /* 큐에서 꺼내기 */\n public int pop() {\n int num = peek();\n // 머리 노드를 삭제한다\n front = front.next;\n queSize--;\n return num;\n }\n\n /* 맨 앞 원소 접근 */\n public int peek() {\n if (isEmpty())\n throw new IndexOutOfBoundsException();\n return front.val;\n }\n\n /* 연결 리스트를 Array로 변환하여 반환 */\n public int[] toArray() {\n ListNode node = front;\n int[] res = new int[size()];\n for (int i = 0; i &lt; res.length; i++) {\n res[i] = node.val;\n node = node.next;\n }\n return res;\n }\n}\n</code></pre> linkedlist_queue.cs<pre><code>/* 연결 리스트로 구현한 큐 */\nclass LinkedListQueue {\n ListNode? front, rear; // 머리 노드 front, 꼬리 노드 rear \n int queSize = 0;\n\n public LinkedListQueue() {\n front = null;\n rear = null;\n }\n\n /* 큐의 길이 반환 */\n public int Size() {\n return queSize;\n }\n\n /* 큐가 비어 있는지 판단 */\n public bool IsEmpty() {\n return Size() == 0;\n }\n\n /* 큐에 삽입 */\n public void Push(int num) {\n // 꼬리 노드 뒤에 num을 추가한다\n ListNode node = new(num);\n // 큐가 비어 있으면 머리와 꼬리 노드가 모두 해당 노드를 가리키게 한다\n if (front == null) {\n front = node;\n rear = node;\n // 큐가 비어 있지 않으면 해당 노드를 꼬리 노드 뒤에 추가한다\n } else if (rear != null) {\n rear.next = node;\n rear = node;\n }\n queSize++;\n }\n\n /* 큐에서 꺼내기 */\n public int Pop() {\n int num = Peek();\n // 머리 노드를 삭제한다\n front = front?.next;\n queSize--;\n return num;\n }\n\n /* 맨 앞 원소 접근 */\n public int Peek() {\n if (IsEmpty())\n throw new Exception();\n return front!.val;\n }\n\n /* 연결 리스트를 Array로 변환하여 반환 */\n public int[] ToArray() {\n if (front == null)\n return [];\n\n ListNode? node = front;\n int[] res = new int[Size()];\n for (int i = 0; i &lt; res.Length; i++) {\n res[i] = node!.val;\n node = node.next;\n }\n return res;\n }\n}\n</code></pre> linkedlist_queue.go<pre><code>/* 연결 리스트로 구현한 큐 */\ntype linkedListQueue struct {\n // 내장 패키지 list로 큐를 구현한다\n data *list.List\n}\n\n/* 큐 초기화 */\nfunc newLinkedListQueue() *linkedListQueue {\n return &amp;linkedListQueue{\n data: list.New(),\n }\n}\n\n/* 큐에 삽입 */\nfunc (s *linkedListQueue) push(value any) {\n s.data.PushBack(value)\n}\n\n/* 큐에서 꺼내기 */\nfunc (s *linkedListQueue) pop() any {\n if s.isEmpty() {\n return nil\n }\n e := s.data.Front()\n s.data.Remove(e)\n return e.Value\n}\n\n/* 맨 앞 원소 접근 */\nfunc (s *linkedListQueue) peek() any {\n if s.isEmpty() {\n return nil\n }\n e := s.data.Front()\n return e.Value\n}\n\n/* 큐의 길이 반환 */\nfunc (s *linkedListQueue) size() int {\n return s.data.Len()\n}\n\n/* 큐가 비어 있는지 판단 */\nfunc (s *linkedListQueue) isEmpty() bool {\n return s.data.Len() == 0\n}\n\n/* 출력에 사용할 List 반환 */\nfunc (s *linkedListQueue) toList() *list.List {\n return s.data\n}\n</code></pre> linkedlist_queue.swift<pre><code>/* 연결 리스트로 구현한 큐 */\nclass LinkedListQueue {\n private var front: ListNode? // 머리 노드\n private var rear: ListNode? // 꼬리 노드\n private var _size: Int\n\n init() {\n _size = 0\n }\n\n /* 큐의 길이 반환 */\n func size() -&gt; Int {\n _size\n }\n\n /* 큐가 비어 있는지 판단 */\n func isEmpty() -&gt; Bool {\n size() == 0\n }\n\n /* 큐에 삽입 */\n func push(num: Int) {\n // 꼬리 노드 뒤에 num을 추가한다\n let node = ListNode(x: num)\n // 큐가 비어 있으면 머리와 꼬리 노드가 모두 해당 노드를 가리키게 한다\n if front == nil {\n front = node\n rear = node\n }\n // 큐가 비어 있지 않으면 해당 노드를 꼬리 노드 뒤에 추가한다\n else {\n rear?.next = node\n rear = node\n }\n _size += 1\n }\n\n /* 큐에서 꺼내기 */\n @discardableResult\n func pop() -&gt; Int {\n let num = peek()\n // 머리 노드를 삭제한다\n front = front?.next\n _size -= 1\n return num\n }\n\n /* 맨 앞 원소 접근 */\n func peek() -&gt; Int {\n if isEmpty() {\n fatalError(\"큐가 비어 있다\")\n }\n return front!.val\n }\n\n /* 연결 리스트를 Array로 변환하여 반환 */\n func toArray() -&gt; [Int] {\n var node = front\n var res = Array(repeating: 0, count: size())\n for i in res.indices {\n res[i] = node!.val\n node = node?.next\n }\n return res\n }\n}\n</code></pre> linkedlist_queue.js<pre><code>/* 연결 리스트로 구현한 큐 */\nclass LinkedListQueue {\n #front; // 머리 노드 #front\n #rear; // 꼬리 노드 #rear\n #queSize = 0;\n\n constructor() {\n this.#front = null;\n this.#rear = null;\n }\n\n /* 큐의 길이 반환 */\n get size() {\n return this.#queSize;\n }\n\n /* 큐가 비어 있는지 판단 */\n isEmpty() {\n return this.size === 0;\n }\n\n /* 큐에 삽입 */\n push(num) {\n // 꼬리 노드 뒤에 num을 추가한다\n const node = new ListNode(num);\n // 큐가 비어 있으면 머리와 꼬리 노드가 모두 해당 노드를 가리키게 한다\n if (!this.#front) {\n this.#front = node;\n this.#rear = node;\n // 큐가 비어 있지 않으면 해당 노드를 꼬리 노드 뒤에 추가한다\n } else {\n this.#rear.next = node;\n this.#rear = node;\n }\n this.#queSize++;\n }\n\n /* 큐에서 꺼내기 */\n pop() {\n const num = this.peek();\n // 머리 노드를 삭제한다\n this.#front = this.#front.next;\n this.#queSize--;\n return num;\n }\n\n /* 맨 앞 원소 접근 */\n peek() {\n if (this.size === 0) throw new Error('큐가 비어 있다');\n return this.#front.val;\n }\n\n /* 연결 리스트를 Array로 변환하여 반환 */\n toArray() {\n let node = this.#front;\n const res = new Array(this.size);\n for (let i = 0; i &lt; res.length; i++) {\n res[i] = node.val;\n node = node.next;\n }\n return res;\n }\n}\n</code></pre> linkedlist_queue.ts<pre><code>/* 연결 리스트로 구현한 큐 */\nclass LinkedListQueue {\n private front: ListNode | null; // 머리 노드 front\n private rear: ListNode | null; // 꼬리 노드 rear\n private queSize: number = 0;\n\n constructor() {\n this.front = null;\n this.rear = null;\n }\n\n /* 큐의 길이 반환 */\n get size(): number {\n return this.queSize;\n }\n\n /* 큐가 비어 있는지 판단 */\n isEmpty(): boolean {\n return this.size === 0;\n }\n\n /* 큐에 삽입 */\n push(num: number): void {\n // 꼬리 노드 뒤에 num을 추가한다\n const node = new ListNode(num);\n // 큐가 비어 있으면 머리와 꼬리 노드가 모두 해당 노드를 가리키게 한다\n if (!this.front) {\n this.front = node;\n this.rear = node;\n // 큐가 비어 있지 않으면 해당 노드를 꼬리 노드 뒤에 추가한다\n } else {\n this.rear!.next = node;\n this.rear = node;\n }\n this.queSize++;\n }\n\n /* 큐에서 꺼내기 */\n pop(): number {\n const num = this.peek();\n if (!this.front) throw new Error('큐가 비어 있다');\n // 머리 노드를 삭제한다\n this.front = this.front.next;\n this.queSize--;\n return num;\n }\n\n /* 맨 앞 원소 접근 */\n peek(): number {\n if (this.size === 0) throw new Error('큐가 비어 있다');\n return this.front!.val;\n }\n\n /* 연결 리스트를 Array로 변환하여 반환 */\n toArray(): number[] {\n let node = this.front;\n const res = new Array&lt;number&gt;(this.size);\n for (let i = 0; i &lt; res.length; i++) {\n res[i] = node!.val;\n node = node!.next;\n }\n return res;\n }\n}\n</code></pre> linkedlist_queue.dart<pre><code>/* 연결 리스트로 구현한 큐 */\nclass LinkedListQueue {\n ListNode? _front; // 머리 노드 _front\n ListNode? _rear; // 꼬리 노드 _rear\n int _queSize = 0; // 큐 길이\n\n LinkedListQueue() {\n _front = null;\n _rear = null;\n }\n\n /* 큐의 길이 반환 */\n int size() {\n return _queSize;\n }\n\n /* 큐가 비어 있는지 판단 */\n bool isEmpty() {\n return _queSize == 0;\n }\n\n /* 큐에 삽입 */\n void push(int _num) {\n // 꼬리 노드 뒤에 _num을 추가한다\n final node = ListNode(_num);\n // 큐가 비어 있으면 머리와 꼬리 노드가 모두 해당 노드를 가리키게 한다\n if (_front == null) {\n _front = node;\n _rear = node;\n } else {\n // 큐가 비어 있지 않으면 해당 노드를 꼬리 노드 뒤에 추가한다\n _rear!.next = node;\n _rear = node;\n }\n _queSize++;\n }\n\n /* 큐에서 꺼내기 */\n int pop() {\n final int _num = peek();\n // 머리 노드를 삭제한다\n _front = _front!.next;\n _queSize--;\n return _num;\n }\n\n /* 맨 앞 원소 접근 */\n int peek() {\n if (_queSize == 0) {\n throw Exception('큐가 비어 있다');\n }\n return _front!.val;\n }\n\n /* 연결 리스트를 Array로 변환하여 반환 */\n List&lt;int&gt; toArray() {\n ListNode? node = _front;\n final List&lt;int&gt; queue = [];\n while (node != null) {\n queue.add(node.val);\n node = node.next;\n }\n return queue;\n }\n}\n</code></pre> linkedlist_queue.rs<pre><code>/* 연결 리스트로 구현한 큐 */\n#[allow(dead_code)]\npub struct LinkedListQueue&lt;T&gt; {\n front: Option&lt;Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt;&gt;, // 머리 노드 front\n rear: Option&lt;Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt;&gt;, // 꼬리 노드 rear\n que_size: usize, // 큐의 길이\n}\n\nimpl&lt;T: Copy&gt; LinkedListQueue&lt;T&gt; {\n pub fn new() -&gt; Self {\n Self {\n front: None,\n rear: None,\n que_size: 0,\n }\n }\n\n /* 큐의 길이 반환 */\n pub fn size(&amp;self) -&gt; usize {\n return self.que_size;\n }\n\n /* 큐가 비어 있는지 판단 */\n pub fn is_empty(&amp;self) -&gt; bool {\n return self.que_size == 0;\n }\n\n /* 큐에 삽입 */\n pub fn push(&amp;mut self, num: T) {\n // 꼬리 노드 뒤에 num을 추가한다\n let new_rear = ListNode::new(num);\n match self.rear.take() {\n // 큐가 비어 있지 않으면 해당 노드를 꼬리 노드 뒤에 추가한다\n Some(old_rear) =&gt; {\n old_rear.borrow_mut().next = Some(new_rear.clone());\n self.rear = Some(new_rear);\n }\n // 큐가 비어 있으면 머리와 꼬리 노드가 모두 해당 노드를 가리키게 한다\n None =&gt; {\n self.front = Some(new_rear.clone());\n self.rear = Some(new_rear);\n }\n }\n self.que_size += 1;\n }\n\n /* 큐에서 꺼내기 */\n pub fn pop(&amp;mut self) -&gt; Option&lt;T&gt; {\n self.front.take().map(|old_front| {\n match old_front.borrow_mut().next.take() {\n Some(new_front) =&gt; {\n self.front = Some(new_front);\n }\n None =&gt; {\n self.rear.take();\n }\n }\n self.que_size -= 1;\n old_front.borrow().val\n })\n }\n\n /* 맨 앞 원소 접근 */\n pub fn peek(&amp;self) -&gt; Option&lt;&amp;Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt;&gt; {\n self.front.as_ref()\n }\n\n /* 연결 리스트를 Array로 변환하여 반환 */\n pub fn to_array(&amp;self, head: Option&lt;&amp;Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt;&gt;) -&gt; Vec&lt;T&gt; {\n let mut res: Vec&lt;T&gt; = Vec::new();\n\n fn recur&lt;T: Copy&gt;(cur: Option&lt;&amp;Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt;&gt;, res: &amp;mut Vec&lt;T&gt;) {\n if let Some(cur) = cur {\n res.push(cur.borrow().val);\n recur(cur.borrow().next.as_ref(), res);\n }\n }\n\n recur(head, &amp;mut res);\n\n res\n }\n}\n</code></pre> linkedlist_queue.c<pre><code>/* 연결 리스트로 구현한 큐 */\ntypedef struct {\n ListNode *front, *rear;\n int queSize;\n} LinkedListQueue;\n\n/* 생성자 */\nLinkedListQueue *newLinkedListQueue() {\n LinkedListQueue *queue = (LinkedListQueue *)malloc(sizeof(LinkedListQueue));\n queue-&gt;front = NULL;\n queue-&gt;rear = NULL;\n queue-&gt;queSize = 0;\n return queue;\n}\n\n/* 소멸자 */\nvoid delLinkedListQueue(LinkedListQueue *queue) {\n // 모든 노드를 해제한다\n while (queue-&gt;front != NULL) {\n ListNode *tmp = queue-&gt;front;\n queue-&gt;front = queue-&gt;front-&gt;next;\n free(tmp);\n }\n // queue 구조체를 해제한다\n free(queue);\n}\n\n/* 큐의 길이 반환 */\nint size(LinkedListQueue *queue) {\n return queue-&gt;queSize;\n}\n\n/* 큐가 비어 있는지 판단 */\nbool empty(LinkedListQueue *queue) {\n return (size(queue) == 0);\n}\n\n/* 큐에 삽입 */\nvoid push(LinkedListQueue *queue, int num) {\n // 꼬리 노드 위치에 node를 추가한다\n ListNode *node = newListNode(num);\n // 큐가 비어 있으면 머리와 꼬리 노드가 모두 해당 노드를 가리키게 한다\n if (queue-&gt;front == NULL) {\n queue-&gt;front = node;\n queue-&gt;rear = node;\n }\n // 큐가 비어 있지 않으면 해당 노드를 꼬리 노드 뒤에 추가한다\n else {\n queue-&gt;rear-&gt;next = node;\n queue-&gt;rear = node;\n }\n queue-&gt;queSize++;\n}\n\n/* 맨 앞 원소 접근 */\nint peek(LinkedListQueue *queue) {\n assert(size(queue) &amp;&amp; queue-&gt;front);\n return queue-&gt;front-&gt;val;\n}\n\n/* 큐에서 꺼내기 */\nint pop(LinkedListQueue *queue) {\n int num = peek(queue);\n ListNode *tmp = queue-&gt;front;\n queue-&gt;front = queue-&gt;front-&gt;next;\n free(tmp);\n queue-&gt;queSize--;\n return num;\n}\n\n/* 큐 출력 */\nvoid printLinkedListQueue(LinkedListQueue *queue) {\n int *arr = malloc(sizeof(int) * queue-&gt;queSize);\n // 연결 리스트의 데이터를 배열에 복사한다\n int i;\n ListNode *node;\n for (i = 0, node = queue-&gt;front; i &lt; queue-&gt;queSize; i++) {\n arr[i] = node-&gt;val;\n node = node-&gt;next;\n }\n printArray(arr, queue-&gt;queSize);\n free(arr);\n}\n</code></pre> linkedlist_queue.kt<pre><code>/* 연결 리스트로 구현한 큐 */\nclass LinkedListQueue(\n // 머리 노드 front, 꼬리 노드 rear\n private var front: ListNode? = null,\n private var rear: ListNode? = null,\n private var queSize: Int = 0\n) {\n\n /* 큐의 길이 반환 */\n fun size(): Int {\n return queSize\n }\n\n /* 큐가 비어 있는지 판단 */\n fun isEmpty(): Boolean {\n return size() == 0\n }\n\n /* 큐에 삽입 */\n fun push(num: Int) {\n // 꼬리 노드 뒤에 num을 추가한다\n val node = ListNode(num)\n // 큐가 비어 있으면 머리와 꼬리 노드가 모두 해당 노드를 가리키게 한다\n if (front == null) {\n front = node\n rear = node\n // 큐가 비어 있지 않으면 해당 노드를 꼬리 노드 뒤에 추가한다\n } else {\n rear?.next = node\n rear = node\n }\n queSize++\n }\n\n /* 큐에서 꺼내기 */\n fun pop(): Int {\n val num = peek()\n // 머리 노드를 삭제한다\n front = front?.next\n queSize--\n return num\n }\n\n /* 맨 앞 원소 접근 */\n fun peek(): Int {\n if (isEmpty()) throw IndexOutOfBoundsException()\n return front!!._val\n }\n\n /* 연결 리스트를 Array로 변환하여 반환 */\n fun toArray(): IntArray {\n var node = front\n val res = IntArray(size())\n for (i in res.indices) {\n res[i] = node!!._val\n node = node.next\n }\n return res\n }\n}\n</code></pre> linkedlist_queue.rb<pre><code>### 연결 리스트로 구현한 큐 ###\nclass LinkedListQueue\n ### 큐의 길이 반환 ###\n attr_reader :size\n\n ### 생성자 ###\n def initialize\n @front = nil # 머리 노드 front\n @rear = nil # 꼬리 노드 rear\n @size = 0\n end\n\n ### 큐가 비어 있는지 판단 ###\n def is_empty?\n @front.nil?\n end\n\n ### 큐에 삽입 ###\n def push(num)\n # 꼬리 노드 뒤에 num을 추가한다\n node = ListNode.new(num)\n\n # 큐가 비어 있으면 머리와 꼬리 노드가 모두 해당 노드를 가리키게 한다\n if @front.nil?\n @front = node\n @rear = node\n # 큐가 비어 있지 않으면 해당 노드를 꼬리 노드 뒤에 추가한다\n else\n @rear.next = node\n @rear = node\n end\n\n @size += 1\n end\n\n ### 큐에서 꺼내기 ###\n def pop\n num = peek\n # 머리 노드를 삭제한다\n @front = @front.next\n @size -= 1\n num\n end\n\n ### 맨 앞 원소 접근 ###\n def peek\n raise IndexError, '큐가 비어 있다' if is_empty?\n\n @front.val\n end\n\n ### 연결 리스트를 Array로 변환하여 반환 ###\n def to_array\n queue = []\n temp = @front\n while temp\n queue &lt;&lt; temp.val\n temp = temp.next\n end\n queue\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 5 장   스택과 큐","5.2   큐"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#2","level":3,"title":"2.   배열 기반 구현","text":"<p>배열에서 첫 원소를 삭제하는 시간 복잡도는 \\(O(n)\\)이므로, 큐에서 원소를 꺼내는 효율이 낮아진다. 그러나 다음과 같은 방법으로 이 문제를 피할 수 있다.</p> <p>변수 <code>front</code>로 큐의 맨 앞 원소 인덱스를 가리키고, 변수 <code>size</code>로 큐의 길이를 기록한다. <code>rear = front + size</code>로 정의하면 <code>rear</code>는 큐의 맨 뒤 원소 바로 다음 위치를 가리킨다.</p> <p>이 설계에서는 배열에서 원소가 들어 있는 유효 구간이 <code>[front, rear - 1]</code>이며, 각 연산의 구현 방법은 아래 그림(5-6)과 같다.</p> <ul> <li>삽입: 인덱스 <code>rear</code>에 입력 원소를 대입하고 <code>size</code>를 1 늘린다.</li> <li>꺼내기: <code>front</code>를 1 늘리고 <code>size</code>를 1 줄이면 된다.</li> </ul> <p>두 연산 모두 한 번의 처리만 필요하므로 시간 복잡도는 \\(O(1)\\)이다.</p> &lt;1&gt;&lt;2&gt;&lt;3&gt; <p></p> <p></p> <p></p> <p> 그림 5-6   배열 기반 큐의 삽입과 꺼내기 </p> <p>여기에는 한 가지 문제가 있다. 삽입과 꺼내기를 반복하면 <code>front</code>와 <code>rear</code>가 계속 오른쪽으로 이동하다가 배열의 끝에 도달하면 더 이상 움직일 수 없다. 이를 해결하려면 배열을 앞뒤가 이어진 ‘원형 배열’로 볼 수 있다.</p> <p>원형 배열에서는 <code>front</code>나 <code>rear</code>가 배열의 끝을 넘으면 곧바로 처음으로 돌아와 순회를 계속하게 해야 한다. 이 주기적 규칙은 ‘나머지 연산’으로 구현할 수 있다. 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_queue.py<pre><code>class ArrayQueue:\n \"\"\"원형 배열로 구현한 큐\"\"\"\n\n def __init__(self, size: int):\n \"\"\"생성자\"\"\"\n self._nums: list[int] = [0] * size # 큐의 원소를 저장하는 배열\n self._front: int = 0 # 맨 앞 원소를 가리키는 맨 앞 포인터\n self._size: int = 0 # 큐 길이\n\n def capacity(self) -&gt; int:\n \"\"\"큐의 용량 반환\"\"\"\n return len(self._nums)\n\n def size(self) -&gt; int:\n \"\"\"큐의 길이 반환\"\"\"\n return self._size\n\n def is_empty(self) -&gt; bool:\n \"\"\"큐가 비어 있는지 판단\"\"\"\n return self._size == 0\n\n def push(self, num: int):\n \"\"\"큐에 삽입\"\"\"\n if self._size == self.capacity():\n raise IndexError(\"큐가 가득 찼다\")\n # 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n # 나머지 연산을 통해 rear가 배열의 끝을 넘어가면 처음으로 돌아오게 한다\n rear: int = (self._front + self._size) % self.capacity()\n # num을 맨 뒤에 추가한다\n self._nums[rear] = num\n self._size += 1\n\n def pop(self) -&gt; int:\n \"\"\"큐에서 꺼내기\"\"\"\n num: int = self.peek()\n # 맨 앞 포인터를 뒤로 한 칸 이동하고, 끝을 넘어가면 배열의 처음으로 돌아온다\n self._front = (self._front + 1) % self.capacity()\n self._size -= 1\n return num\n\n def peek(self) -&gt; int:\n \"\"\"맨 앞 원소 접근\"\"\"\n if self.is_empty():\n raise IndexError(\"큐가 비어 있다\")\n return self._nums[self._front]\n\n def to_list(self) -&gt; list[int]:\n \"\"\"출력에 사용할 리스트 반환\"\"\"\n res = [0] * self.size()\n j: int = self._front\n for i in range(self.size()):\n res[i] = self._nums[(j % self.capacity())]\n j += 1\n return res\n</code></pre> array_queue.cpp<pre><code>/* 원형 배열로 구현한 큐 */\nclass ArrayQueue {\n private:\n int *nums; // 큐의 원소를 저장하는 배열\n int front; // 맨 앞 원소를 가리키는 맨 앞 포인터\n int queSize; // 큐 길이\n int queCapacity; // 큐 용량\n\n public:\n ArrayQueue(int capacity) {\n // 배열 초기화\n nums = new int[capacity];\n queCapacity = capacity;\n front = queSize = 0;\n }\n\n ~ArrayQueue() {\n delete[] nums;\n }\n\n /* 큐의 용량 반환 */\n int capacity() {\n return queCapacity;\n }\n\n /* 큐의 길이 반환 */\n int size() {\n return queSize;\n }\n\n /* 큐가 비어 있는지 판단 */\n bool isEmpty() {\n return size() == 0;\n }\n\n /* 큐에 삽입 */\n void push(int num) {\n if (queSize == queCapacity) {\n cout &lt;&lt; \"큐가 가득 찼다\" &lt;&lt; endl;\n return;\n }\n // 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n // 나머지 연산을 통해 rear가 배열의 끝을 넘어가면 처음으로 돌아오게 한다\n int rear = (front + queSize) % queCapacity;\n // num을 맨 뒤에 추가한다\n nums[rear] = num;\n queSize++;\n }\n\n /* 큐에서 꺼내기 */\n int pop() {\n int num = peek();\n // 맨 앞 포인터를 뒤로 한 칸 이동하고, 끝을 넘어가면 배열의 처음으로 돌아온다\n front = (front + 1) % queCapacity;\n queSize--;\n return num;\n }\n\n /* 맨 앞 원소 접근 */\n int peek() {\n if (isEmpty())\n throw out_of_range(\"큐가 비어 있다\");\n return nums[front];\n }\n\n /* 배열을 Vector로 변환하여 반환 */\n vector&lt;int&gt; toVector() {\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n vector&lt;int&gt; arr(queSize);\n for (int i = 0, j = front; i &lt; queSize; i++, j++) {\n arr[i] = nums[j % queCapacity];\n }\n return arr;\n }\n};\n</code></pre> array_queue.java<pre><code>/* 원형 배열로 구현한 큐 */\nclass ArrayQueue {\n private int[] nums; // 큐의 원소를 저장하는 배열\n private int front; // 맨 앞 원소를 가리키는 맨 앞 포인터\n private int queSize; // 큐 길이\n\n public ArrayQueue(int capacity) {\n nums = new int[capacity];\n front = queSize = 0;\n }\n\n /* 큐의 용량 반환 */\n public int capacity() {\n return nums.length;\n }\n\n /* 큐의 길이 반환 */\n public int size() {\n return queSize;\n }\n\n /* 큐가 비어 있는지 판단 */\n public boolean isEmpty() {\n return queSize == 0;\n }\n\n /* 큐에 삽입 */\n public void push(int num) {\n if (queSize == capacity()) {\n System.out.println(\"큐가 가득 찼다\");\n return;\n }\n // 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n // 나머지 연산을 통해 rear가 배열의 끝을 넘어가면 처음으로 돌아오게 한다\n int rear = (front + queSize) % capacity();\n // num을 맨 뒤에 추가한다\n nums[rear] = num;\n queSize++;\n }\n\n /* 큐에서 꺼내기 */\n public int pop() {\n int num = peek();\n // 맨 앞 포인터를 뒤로 한 칸 이동하고, 끝을 넘어가면 배열의 처음으로 돌아온다\n front = (front + 1) % capacity();\n queSize--;\n return num;\n }\n\n /* 맨 앞 원소 접근 */\n public int peek() {\n if (isEmpty())\n throw new IndexOutOfBoundsException();\n return nums[front];\n }\n\n /* 배열 반환 */\n public int[] toArray() {\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n int[] res = new int[queSize];\n for (int i = 0, j = front; i &lt; queSize; i++, j++) {\n res[i] = nums[j % capacity()];\n }\n return res;\n }\n}\n</code></pre> array_queue.cs<pre><code>/* 원형 배열로 구현한 큐 */\nclass ArrayQueue {\n int[] nums; // 큐의 원소를 저장하는 배열\n int front; // 맨 앞 원소를 가리키는 맨 앞 포인터\n int queSize; // 큐 길이\n\n public ArrayQueue(int capacity) {\n nums = new int[capacity];\n front = queSize = 0;\n }\n\n /* 큐의 용량 반환 */\n int Capacity() {\n return nums.Length;\n }\n\n /* 큐의 길이 반환 */\n public int Size() {\n return queSize;\n }\n\n /* 큐가 비어 있는지 판단 */\n public bool IsEmpty() {\n return queSize == 0;\n }\n\n /* 큐에 삽입 */\n public void Push(int num) {\n if (queSize == Capacity()) {\n Console.WriteLine(\"큐가 가득 찼다\");\n return;\n }\n // 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n // 나머지 연산을 통해 rear가 배열의 끝을 넘어가면 처음으로 돌아오게 한다\n int rear = (front + queSize) % Capacity();\n // num을 맨 뒤에 추가한다\n nums[rear] = num;\n queSize++;\n }\n\n /* 큐에서 꺼내기 */\n public int Pop() {\n int num = Peek();\n // 맨 앞 포인터를 뒤로 한 칸 이동하고, 끝을 넘어가면 배열의 처음으로 돌아온다\n front = (front + 1) % Capacity();\n queSize--;\n return num;\n }\n\n /* 맨 앞 원소 접근 */\n public int Peek() {\n if (IsEmpty())\n throw new Exception();\n return nums[front];\n }\n\n /* 배열 반환 */\n public int[] ToArray() {\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n int[] res = new int[queSize];\n for (int i = 0, j = front; i &lt; queSize; i++, j++) {\n res[i] = nums[j % this.Capacity()];\n }\n return res;\n }\n}\n</code></pre> array_queue.go<pre><code>/* 원형 배열로 구현한 큐 */\ntype arrayQueue struct {\n nums []int // 큐의 원소를 저장하는 배열\n front int // 맨 앞 원소를 가리키는 맨 앞 포인터\n queSize int // 큐 길이\n queCapacity int // 큐 용량(저장할 수 있는 최대 원소 개수)\n}\n\n/* 큐 초기화 */\nfunc newArrayQueue(queCapacity int) *arrayQueue {\n return &amp;arrayQueue{\n nums: make([]int, queCapacity),\n queCapacity: queCapacity,\n front: 0,\n queSize: 0,\n }\n}\n\n/* 큐의 길이 반환 */\nfunc (q *arrayQueue) size() int {\n return q.queSize\n}\n\n/* 큐가 비어 있는지 판단 */\nfunc (q *arrayQueue) isEmpty() bool {\n return q.queSize == 0\n}\n\n/* 큐에 삽입 */\nfunc (q *arrayQueue) push(num int) {\n // rear == queCapacity이면 큐가 가득 찼음을 나타낸다\n if q.queSize == q.queCapacity {\n return\n }\n // 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n // 나머지 연산을 통해 rear가 배열의 끝을 넘어가면 처음으로 돌아오게 한다\n rear := (q.front + q.queSize) % q.queCapacity\n // num을 맨 뒤에 추가한다\n q.nums[rear] = num\n q.queSize++\n}\n\n/* 큐에서 꺼내기 */\nfunc (q *arrayQueue) pop() any {\n num := q.peek()\n if num == nil {\n return nil\n }\n\n // 맨 앞 포인터를 뒤로 한 칸 이동하고, 끝을 넘어가면 배열의 처음으로 돌아온다\n q.front = (q.front + 1) % q.queCapacity\n q.queSize--\n return num\n}\n\n/* 맨 앞 원소 접근 */\nfunc (q *arrayQueue) peek() any {\n if q.isEmpty() {\n return nil\n }\n return q.nums[q.front]\n}\n\n/* 출력에 사용할 Slice 반환 */\nfunc (q *arrayQueue) toSlice() []int {\n rear := (q.front + q.queSize)\n if rear &gt;= q.queCapacity {\n rear %= q.queCapacity\n return append(q.nums[q.front:], q.nums[:rear]...)\n }\n return q.nums[q.front:rear]\n}\n</code></pre> array_queue.swift<pre><code>/* 원형 배열로 구현한 큐 */\nclass ArrayQueue {\n private var nums: [Int] // 큐의 원소를 저장하는 배열\n private var front: Int // 맨 앞 원소를 가리키는 맨 앞 포인터\n private var _size: Int // 큐 길이\n\n init(capacity: Int) {\n // 배열 초기화\n nums = Array(repeating: 0, count: capacity)\n front = 0\n _size = 0\n }\n\n /* 큐의 용량 반환 */\n func capacity() -&gt; Int {\n nums.count\n }\n\n /* 큐의 길이 반환 */\n func size() -&gt; Int {\n _size\n }\n\n /* 큐가 비어 있는지 판단 */\n func isEmpty() -&gt; Bool {\n size() == 0\n }\n\n /* 큐에 삽입 */\n func push(num: Int) {\n if size() == capacity() {\n print(\"큐가 가득 찼다\")\n return\n }\n // 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n // 나머지 연산을 통해 rear가 배열의 끝을 넘어가면 처음으로 돌아오게 한다\n let rear = (front + size()) % capacity()\n // num을 맨 뒤에 추가한다\n nums[rear] = num\n _size += 1\n }\n\n /* 큐에서 꺼내기 */\n @discardableResult\n func pop() -&gt; Int {\n let num = peek()\n // 맨 앞 포인터를 뒤로 한 칸 이동하고, 끝을 넘어가면 배열의 처음으로 돌아온다\n front = (front + 1) % capacity()\n _size -= 1\n return num\n }\n\n /* 맨 앞 원소 접근 */\n func peek() -&gt; Int {\n if isEmpty() {\n fatalError(\"큐가 비어 있다\")\n }\n return nums[front]\n }\n\n /* 배열 반환 */\n func toArray() -&gt; [Int] {\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n (front ..&lt; front + size()).map { nums[$0 % capacity()] }\n }\n}\n</code></pre> array_queue.js<pre><code>/* 원형 배열로 구현한 큐 */\nclass ArrayQueue {\n #nums; // 큐의 원소를 저장하는 배열\n #front = 0; // 맨 앞 원소를 가리키는 맨 앞 포인터\n #queSize = 0; // 큐 길이\n\n constructor(capacity) {\n this.#nums = new Array(capacity);\n }\n\n /* 큐의 용량 반환 */\n get capacity() {\n return this.#nums.length;\n }\n\n /* 큐의 길이 반환 */\n get size() {\n return this.#queSize;\n }\n\n /* 큐가 비어 있는지 판단 */\n isEmpty() {\n return this.#queSize === 0;\n }\n\n /* 큐에 삽입 */\n push(num) {\n if (this.size === this.capacity) {\n console.log('큐가 가득 찼다');\n return;\n }\n // 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n // 나머지 연산을 통해 rear가 배열의 끝을 넘어가면 처음으로 돌아오게 한다\n const rear = (this.#front + this.size) % this.capacity;\n // num을 맨 뒤에 추가한다\n this.#nums[rear] = num;\n this.#queSize++;\n }\n\n /* 큐에서 꺼내기 */\n pop() {\n const num = this.peek();\n // 맨 앞 포인터를 뒤로 한 칸 이동하고, 끝을 넘어가면 배열의 처음으로 돌아온다\n this.#front = (this.#front + 1) % this.capacity;\n this.#queSize--;\n return num;\n }\n\n /* 맨 앞 원소 접근 */\n peek() {\n if (this.isEmpty()) throw new Error('큐가 비어 있다');\n return this.#nums[this.#front];\n }\n\n /* Array를 반환한다 */\n toArray() {\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n const arr = new Array(this.size);\n for (let i = 0, j = this.#front; i &lt; this.size; i++, j++) {\n arr[i] = this.#nums[j % this.capacity];\n }\n return arr;\n }\n}\n</code></pre> array_queue.ts<pre><code>/* 원형 배열로 구현한 큐 */\nclass ArrayQueue {\n private nums: number[]; // 큐의 원소를 저장하는 배열\n private front: number; // 맨 앞 원소를 가리키는 맨 앞 포인터\n private queSize: number; // 큐 길이\n\n constructor(capacity: number) {\n this.nums = new Array(capacity);\n this.front = this.queSize = 0;\n }\n\n /* 큐의 용량 반환 */\n get capacity(): number {\n return this.nums.length;\n }\n\n /* 큐의 길이 반환 */\n get size(): number {\n return this.queSize;\n }\n\n /* 큐가 비어 있는지 판단 */\n isEmpty(): boolean {\n return this.queSize === 0;\n }\n\n /* 큐에 삽입 */\n push(num: number): void {\n if (this.size === this.capacity) {\n console.log('큐가 가득 찼다');\n return;\n }\n // 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n // 나머지 연산을 통해 rear가 배열의 끝을 넘어가면 처음으로 돌아오게 한다\n const rear = (this.front + this.queSize) % this.capacity;\n // num을 맨 뒤에 추가한다\n this.nums[rear] = num;\n this.queSize++;\n }\n\n /* 큐에서 꺼내기 */\n pop(): number {\n const num = this.peek();\n // 맨 앞 포인터를 뒤로 한 칸 이동하고, 끝을 넘어가면 배열의 처음으로 돌아온다\n this.front = (this.front + 1) % this.capacity;\n this.queSize--;\n return num;\n }\n\n /* 맨 앞 원소 접근 */\n peek(): number {\n if (this.isEmpty()) throw new Error('큐가 비어 있다');\n return this.nums[this.front];\n }\n\n /* Array를 반환한다 */\n toArray(): number[] {\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n const arr = new Array(this.size);\n for (let i = 0, j = this.front; i &lt; this.size; i++, j++) {\n arr[i] = this.nums[j % this.capacity];\n }\n return arr;\n }\n}\n</code></pre> array_queue.dart<pre><code>/* 원형 배열로 구현한 큐 */\nclass ArrayQueue {\n late List&lt;int&gt; _nums; // 큐의 원소를 저장하는 배열\n late int _front; // 맨 앞 원소를 가리키는 맨 앞 포인터\n late int _queSize; // 큐 길이\n\n ArrayQueue(int capacity) {\n _nums = List.filled(capacity, 0);\n _front = _queSize = 0;\n }\n\n /* 큐의 용량 반환 */\n int capaCity() {\n return _nums.length;\n }\n\n /* 큐의 길이 반환 */\n int size() {\n return _queSize;\n }\n\n /* 큐가 비어 있는지 판단 */\n bool isEmpty() {\n return _queSize == 0;\n }\n\n /* 큐에 삽입 */\n void push(int _num) {\n if (_queSize == capaCity()) {\n throw Exception(\"큐가 가득 찼다\");\n }\n // 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n // 나머지 연산을 통해 rear가 배열의 끝을 넘어가면 처음으로 돌아오게 한다\n int rear = (_front + _queSize) % capaCity();\n // _num을 맨 뒤에 추가한다\n _nums[rear] = _num;\n _queSize++;\n }\n\n /* 큐에서 꺼내기 */\n int pop() {\n int _num = peek();\n // 맨 앞 포인터를 뒤로 한 칸 이동하고, 끝을 넘어가면 배열의 처음으로 돌아온다\n _front = (_front + 1) % capaCity();\n _queSize--;\n return _num;\n }\n\n /* 맨 앞 원소 접근 */\n int peek() {\n if (isEmpty()) {\n throw Exception(\"큐가 비어 있다\");\n }\n return _nums[_front];\n }\n\n /* Array를 반환한다 */\n List&lt;int&gt; toArray() {\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n final List&lt;int&gt; res = List.filled(_queSize, 0);\n for (int i = 0, j = _front; i &lt; _queSize; i++, j++) {\n res[i] = _nums[j % capaCity()];\n }\n return res;\n }\n}\n</code></pre> array_queue.rs<pre><code>/* 원형 배열로 구현한 큐 */\nstruct ArrayQueue&lt;T&gt; {\n nums: Vec&lt;T&gt;, // 큐의 원소를 저장하는 배열\n front: i32, // 맨 앞 원소를 가리키는 맨 앞 포인터\n que_size: i32, // 큐 길이\n que_capacity: i32, // 큐 용량\n}\n\nimpl&lt;T: Copy + Default&gt; ArrayQueue&lt;T&gt; {\n /* 생성자 */\n fn new(capacity: i32) -&gt; ArrayQueue&lt;T&gt; {\n ArrayQueue {\n nums: vec![T::default(); capacity as usize],\n front: 0,\n que_size: 0,\n que_capacity: capacity,\n }\n }\n\n /* 큐의 용량 반환 */\n fn capacity(&amp;self) -&gt; i32 {\n self.que_capacity\n }\n\n /* 큐의 길이 반환 */\n fn size(&amp;self) -&gt; i32 {\n self.que_size\n }\n\n /* 큐가 비어 있는지 판단 */\n fn is_empty(&amp;self) -&gt; bool {\n self.que_size == 0\n }\n\n /* 큐에 삽입 */\n fn push(&amp;mut self, num: T) {\n if self.que_size == self.capacity() {\n println!(\"큐가 가득 찼다\");\n return;\n }\n // 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n // 나머지 연산을 통해 rear가 배열의 끝을 넘어가면 처음으로 돌아오게 한다\n let rear = (self.front + self.que_size) % self.que_capacity;\n // num을 맨 뒤에 추가한다\n self.nums[rear as usize] = num;\n self.que_size += 1;\n }\n\n /* 큐에서 꺼내기 */\n fn pop(&amp;mut self) -&gt; T {\n let num = self.peek();\n // 맨 앞 포인터를 뒤로 한 칸 이동하고, 끝을 넘어가면 배열의 처음으로 돌아온다\n self.front = (self.front + 1) % self.que_capacity;\n self.que_size -= 1;\n num\n }\n\n /* 맨 앞 원소 접근 */\n fn peek(&amp;self) -&gt; T {\n if self.is_empty() {\n panic!(\"인덱스가 범위를 벗어났다\");\n }\n self.nums[self.front as usize]\n }\n\n /* 배열 반환 */\n fn to_vector(&amp;self) -&gt; Vec&lt;T&gt; {\n let cap = self.que_capacity;\n let mut j = self.front;\n let mut arr = vec![T::default(); cap as usize];\n for i in 0..self.que_size {\n arr[i as usize] = self.nums[(j % cap) as usize];\n j += 1;\n }\n arr\n }\n}\n</code></pre> array_queue.c<pre><code>/* 원형 배열로 구현한 큐 */\ntypedef struct {\n int *nums; // 큐의 원소를 저장하는 배열\n int front; // 맨 앞 원소를 가리키는 맨 앞 포인터\n int queSize; // 현재 큐의 원소 개수\n int queCapacity; // 큐 용량\n} ArrayQueue;\n\n/* 생성자 */\nArrayQueue *newArrayQueue(int capacity) {\n ArrayQueue *queue = (ArrayQueue *)malloc(sizeof(ArrayQueue));\n // 배열 초기화\n queue-&gt;queCapacity = capacity;\n queue-&gt;nums = (int *)malloc(sizeof(int) * queue-&gt;queCapacity);\n queue-&gt;front = queue-&gt;queSize = 0;\n return queue;\n}\n\n/* 소멸자 */\nvoid delArrayQueue(ArrayQueue *queue) {\n free(queue-&gt;nums);\n free(queue);\n}\n\n/* 큐의 용량 반환 */\nint capacity(ArrayQueue *queue) {\n return queue-&gt;queCapacity;\n}\n\n/* 큐의 길이 반환 */\nint size(ArrayQueue *queue) {\n return queue-&gt;queSize;\n}\n\n/* 큐가 비어 있는지 판단 */\nbool empty(ArrayQueue *queue) {\n return queue-&gt;queSize == 0;\n}\n\n/* 맨 앞 원소 접근 */\nint peek(ArrayQueue *queue) {\n assert(size(queue) != 0);\n return queue-&gt;nums[queue-&gt;front];\n}\n\n/* 큐에 삽입 */\nvoid push(ArrayQueue *queue, int num) {\n if (size(queue) == capacity(queue)) {\n printf(\"큐가 가득 찼다\\r\\n\");\n return;\n }\n // 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n // 나머지 연산을 통해 rear가 배열의 끝을 넘어가면 처음으로 돌아오게 한다\n int rear = (queue-&gt;front + queue-&gt;queSize) % queue-&gt;queCapacity;\n // num을 맨 뒤에 추가한다\n queue-&gt;nums[rear] = num;\n queue-&gt;queSize++;\n}\n\n/* 큐에서 꺼내기 */\nint pop(ArrayQueue *queue) {\n int num = peek(queue);\n // 맨 앞 포인터를 뒤로 한 칸 이동하고, 끝을 넘어가면 배열의 처음으로 돌아온다\n queue-&gt;front = (queue-&gt;front + 1) % queue-&gt;queCapacity;\n queue-&gt;queSize--;\n return num;\n}\n\n/* 출력에 사용할 배열 반환 */\nint *toArray(ArrayQueue *queue, int *queSize) {\n *queSize = queue-&gt;queSize;\n int *res = (int *)calloc(queue-&gt;queSize, sizeof(int));\n int j = queue-&gt;front;\n for (int i = 0; i &lt; queue-&gt;queSize; i++) {\n res[i] = queue-&gt;nums[j % queue-&gt;queCapacity];\n j++;\n }\n return res;\n}\n</code></pre> array_queue.kt<pre><code>/* 원형 배열로 구현한 큐 */\nclass ArrayQueue(capacity: Int) {\n private val nums: IntArray = IntArray(capacity) // 큐의 원소를 저장하는 배열\n private var front: Int = 0 // 맨 앞 원소를 가리키는 맨 앞 포인터\n private var queSize: Int = 0 // 큐 길이\n\n /* 큐의 용량 반환 */\n fun capacity(): Int {\n return nums.size\n }\n\n /* 큐의 길이 반환 */\n fun size(): Int {\n return queSize\n }\n\n /* 큐가 비어 있는지 판단 */\n fun isEmpty(): Boolean {\n return queSize == 0\n }\n\n /* 큐에 삽입 */\n fun push(num: Int) {\n if (queSize == capacity()) {\n println(\"큐가 가득 찼다\")\n return\n }\n // 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n // 나머지 연산을 통해 rear가 배열의 끝을 넘어가면 처음으로 돌아오게 한다\n val rear = (front + queSize) % capacity()\n // num을 맨 뒤에 추가한다\n nums[rear] = num\n queSize++\n }\n\n /* 큐에서 꺼내기 */\n fun pop(): Int {\n val num = peek()\n // 맨 앞 포인터를 뒤로 한 칸 이동하고, 끝을 넘어가면 배열의 처음으로 돌아온다\n front = (front + 1) % capacity()\n queSize--\n return num\n }\n\n /* 맨 앞 원소 접근 */\n fun peek(): Int {\n if (isEmpty()) throw IndexOutOfBoundsException()\n return nums[front]\n }\n\n /* 배열 반환 */\n fun toArray(): IntArray {\n // 유효한 길이 범위 안의 리스트 원소만 변환한다\n val res = IntArray(queSize)\n var i = 0\n var j = front\n while (i &lt; queSize) {\n res[i] = nums[j % capacity()]\n i++\n j++\n }\n return res\n }\n}\n</code></pre> array_queue.rb<pre><code>### 원형 배열로 구현한 큐 ###\nclass ArrayQueue\n ### 큐의 길이 반환 ###\n attr_reader :size\n\n ### 생성자 ###\n def initialize(size)\n @nums = Array.new(size, 0) # 큐의 원소를 저장하는 배열\n @front = 0 # 맨 앞 원소를 가리키는 맨 앞 포인터\n @size = 0 # 큐 길이\n end\n\n ### 큐의 용량 반환 ###\n def capacity\n @nums.length\n end\n\n ### 큐가 비어 있는지 판단 ###\n def is_empty?\n size.zero?\n end\n\n ### 큐에 삽입 ###\n def push(num)\n raise IndexError, '큐가 가득 찼다' if size == capacity\n\n # 맨 뒤 인덱스 + 1을 가리키는 맨 뒤 포인터를 계산한다\n # 나머지 연산을 통해 rear가 배열의 끝을 넘어가면 처음으로 돌아오게 한다\n rear = (@front + size) % capacity\n # num을 맨 뒤에 추가한다\n @nums[rear] = num\n @size += 1\n end\n\n ### 큐에서 꺼내기 ###\n def pop\n num = peek\n # 맨 앞 포인터를 뒤로 한 칸 이동하고, 끝을 넘어가면 배열의 처음으로 돌아온다\n @front = (@front + 1) % capacity\n @size -= 1\n num\n end\n\n ### 맨 앞 원소 접근 ###\n def peek\n raise IndexError, '큐가 비어 있다' if is_empty?\n\n @nums[@front]\n end\n\n ### 출력에 사용할 리스트 반환 ###\n def to_array\n res = Array.new(size, 0)\n j = @front\n\n for i in 0...size\n res[i] = @nums[j % capacity]\n j += 1\n end\n\n res\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>위 큐 구현에는 여전히 길이를 바꿀 수 없다는 한계가 있다. 하지만 배열을 동적 배열로 바꾸어 용량 확장 방식을 도입하면 쉽게 해결할 수 있다. 관심 있는 독자는 직접 구현해 보자.</p> <p>두 구현의 비교 결과는 스택과 같으므로 여기서는 반복해서 설명하지 않는다.</p>","path":["제 5 장   스택과 큐","5.2   큐"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#523","level":2,"title":"5.2.3   큐의 대표적인 활용","text":"<ul> <li>타오바오 주문. 구매자가 주문하면 큐에 들어가고, 시스템은 순서대로 처리한다. 광군제 기간에는 짧은 시간에 엄청난 주문이 몰리므로 높은 동시성을 처리하는 일이 엔지니어들의 핵심 과제가 된다.</li> <li>여러 종류의 대기 작업. 프린터 작업 대기열, 식당 음식 제공 대기열 등 ‘먼저 온 순서’를 지켜야 하는 상황에서 큐는 처리 순서를 효과적으로 유지한다.</li> </ul>","path":["제 5 장   스택과 큐","5.2   큐"],"tags":[]},{"location":"chapter_stack_and_queue/stack/","level":1,"title":"5.1   스택","text":"<p>스택(stack)은 후입선출 원리를 따르는 선형 자료 구조다.</p> <p>스택은 탁자 위에 쌓인 접시에 비유할 수 있다. 한 번에 접시 하나만 옮길 수 있다면 맨 아래 접시를 꺼내려면 위의 접시들을 차례로 치워야 한다. 접시를 정수, 문자, 객체 등 여러 자료형의 원소로 바꾸면 스택이라는 자료 구조가 된다.</p> <p>아래 그림(5-1)과 같이 원소가 쌓인 맨 위를 ‘스택의 꼭대기’, 맨 아래를 ‘스택의 바닥’이라고 한다. 맨 위에 원소를 추가하는 연산을 ‘스택에 삽입하기’, 맨 위 원소를 삭제하는 연산을 ‘스택에서 꺼내기’라고 한다.</p> <p></p> <p> 그림 5-1   스택의 후입선출 규칙 </p>","path":["제 5 장   스택과 큐","5.1   스택"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#511","level":2,"title":"5.1.1   스택의 기본 연산","text":"<p>스택의 기본 연산은 아래 표(5-1)와 같다. 구체적인 메서드 이름은 프로그래밍 언어에 따라 다르며, 여기서는 흔히 쓰이는 <code>push()</code>, <code>pop()</code>, <code>peek()</code>를 예로 든다.</p> <p> 표 5-1   스택의 연산 효율 </p> 메서드 설명 시간 복잡도 <code>push()</code> 원소를 스택에 삽입(맨 위에 추가) \\(O(1)\\) <code>pop()</code> 스택 맨 위 원소를 꺼냄 \\(O(1)\\) <code>peek()</code> 스택 맨 위 원소에 접근 \\(O(1)\\) <p>보통 프로그래밍 언어에 내장된 스택 클래스를 바로 사용할 수 있다. 전용 스택 클래스가 없는 언어에서는 ‘배열’이나 ‘연결 리스트’를 스택으로 사용하면서, 프로그램 논리상 스택과 무관한 연산은 사용하지 않으면 된다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby stack.py<pre><code># 스택 초기화\n# Python에는 내장 스택 클래스가 없으므로 list를 스택으로 사용할 수 있다\nstack: list[int] = []\n\n# 스택에 원소 삽입\nstack.append(1)\nstack.append(3)\nstack.append(2)\nstack.append(5)\nstack.append(4)\n\n# 스택 맨 위 원소에 접근\npeek: int = stack[-1]\n\n# 스택에서 원소 꺼내기\npop: int = stack.pop()\n\n# 스택 길이 구하기\nsize: int = len(stack)\n\n# 비어 있는지 확인\nis_empty: bool = len(stack) == 0\n</code></pre> stack.cpp<pre><code>/* 스택 초기화 */\nstack&lt;int&gt; stack;\n\n/* 스택에 원소 삽입 */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* 스택 맨 위 원소에 접근 */\nint top = stack.top();\n\n/* 스택에서 원소 꺼내기 */\nstack.pop(); // 반환값 없음\n\n/* 스택 길이 구하기 */\nint size = stack.size();\n\n/* 비어 있는지 확인 */\nbool empty = stack.empty();\n</code></pre> stack.java<pre><code>/* 스택 초기화 */\nStack&lt;Integer&gt; stack = new Stack&lt;&gt;();\n\n/* 스택에 원소 삽입 */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* 스택 맨 위 원소에 접근 */\nint peek = stack.peek();\n\n/* 스택에서 원소 꺼내기 */\nint pop = stack.pop();\n\n/* 스택 길이 구하기 */\nint size = stack.size();\n\n/* 비어 있는지 확인 */\nboolean isEmpty = stack.isEmpty();\n</code></pre> stack.cs<pre><code>/* 스택 초기화 */\nStack&lt;int&gt; stack = new();\n\n/* 스택에 원소 삽입 */\nstack.Push(1);\nstack.Push(3);\nstack.Push(2);\nstack.Push(5);\nstack.Push(4);\n\n/* 스택 맨 위 원소에 접근 */\nint peek = stack.Peek();\n\n/* 스택에서 원소 꺼내기 */\nint pop = stack.Pop();\n\n/* 스택 길이 구하기 */\nint size = stack.Count;\n\n/* 비어 있는지 확인 */\nbool isEmpty = stack.Count == 0;\n</code></pre> stack_test.go<pre><code>/* 스택 초기화 */\n// Go에서는 Slice를 스택으로 사용하는 것을 권한다\nvar stack []int\n\n/* 스택에 원소 삽입 */\nstack = append(stack, 1)\nstack = append(stack, 3)\nstack = append(stack, 2)\nstack = append(stack, 5)\nstack = append(stack, 4)\n\n/* 스택 맨 위 원소에 접근 */\npeek := stack[len(stack)-1]\n\n/* 스택에서 원소 꺼내기 */\npop := stack[len(stack)-1]\nstack = stack[:len(stack)-1]\n\n/* 스택 길이 구하기 */\nsize := len(stack)\n\n/* 비어 있는지 확인 */\nisEmpty := len(stack) == 0\n</code></pre> stack.swift<pre><code>/* 스택 초기화 */\n// Swift에는 내장 스택 클래스가 없으므로 Array를 스택으로 사용할 수 있다\nvar stack: [Int] = []\n\n/* 스택에 원소 삽입 */\nstack.append(1)\nstack.append(3)\nstack.append(2)\nstack.append(5)\nstack.append(4)\n\n/* 스택 맨 위 원소에 접근 */\nlet peek = stack.last!\n\n/* 스택에서 원소 꺼내기 */\nlet pop = stack.removeLast()\n\n/* 스택 길이 구하기 */\nlet size = stack.count\n\n/* 비어 있는지 확인 */\nlet isEmpty = stack.isEmpty\n</code></pre> stack.js<pre><code>/* 스택 초기화 */\n// JavaScript에는 내장 스택 클래스가 없으므로 Array를 스택으로 사용할 수 있다\nconst stack = [];\n\n/* 스택에 원소 삽입 */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* 스택 맨 위 원소에 접근 */\nconst peek = stack[stack.length-1];\n\n/* 스택에서 원소 꺼내기 */\nconst pop = stack.pop();\n\n/* 스택 길이 구하기 */\nconst size = stack.length;\n\n/* 비어 있는지 확인 */\nconst is_empty = stack.length === 0;\n</code></pre> stack.ts<pre><code>/* 스택 초기화 */\n// TypeScript에는 내장 스택 클래스가 없으므로 Array를 스택으로 사용할 수 있다\nconst stack: number[] = [];\n\n/* 스택에 원소 삽입 */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* 스택 맨 위 원소에 접근 */\nconst peek = stack[stack.length - 1];\n\n/* 스택에서 원소 꺼내기 */\nconst pop = stack.pop();\n\n/* 스택 길이 구하기 */\nconst size = stack.length;\n\n/* 비어 있는지 확인 */\nconst is_empty = stack.length === 0;\n</code></pre> stack.dart<pre><code>/* 스택 초기화 */\n// Dart에는 내장 스택 클래스가 없으므로 List를 스택으로 사용할 수 있다\nList&lt;int&gt; stack = [];\n\n/* 스택에 원소 삽입 */\nstack.add(1);\nstack.add(3);\nstack.add(2);\nstack.add(5);\nstack.add(4);\n\n/* 스택 맨 위 원소에 접근 */\nint peek = stack.last;\n\n/* 스택에서 원소 꺼내기 */\nint pop = stack.removeLast();\n\n/* 스택 길이 구하기 */\nint size = stack.length;\n\n/* 비어 있는지 확인 */\nbool isEmpty = stack.isEmpty;\n</code></pre> stack.rs<pre><code>/* 스택 초기화 */\n// Vec를 스택으로 사용\nlet mut stack: Vec&lt;i32&gt; = Vec::new();\n\n/* 스택에 원소 삽입 */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* 스택 맨 위 원소에 접근 */\nlet top = stack.last().unwrap();\n\n/* 스택에서 원소 꺼내기 */\nlet pop = stack.pop().unwrap();\n\n/* 스택 길이 구하기 */\nlet size = stack.len();\n\n/* 비어 있는지 확인 */\nlet is_empty = stack.is_empty();\n</code></pre> stack.c<pre><code>// C는 내장 스택을 제공하지 않는다\n</code></pre> stack.kt<pre><code>/* 스택 초기화 */\nval stack = Stack&lt;Int&gt;()\n\n/* 스택에 원소 삽입 */\nstack.push(1)\nstack.push(3)\nstack.push(2)\nstack.push(5)\nstack.push(4)\n\n/* 스택 맨 위 원소에 접근 */\nval peek = stack.peek()\n\n/* 스택에서 원소 꺼내기 */\nval pop = stack.pop()\n\n/* 스택 길이 구하기 */\nval size = stack.size\n\n/* 비어 있는지 확인 */\nval isEmpty = stack.isEmpty()\n</code></pre> stack.rb<pre><code># 스택 초기화\n# Ruby에는 내장 스택 클래스가 없으므로 Array를 스택으로 사용할 수 있다\nstack = []\n\n# 스택에 원소 삽입\nstack &lt;&lt; 1\nstack &lt;&lt; 3\nstack &lt;&lt; 2\nstack &lt;&lt; 5\nstack &lt;&lt; 4\n\n# 스택 맨 위 원소에 접근\npeek = stack.last\n\n# 스택에서 원소 꺼내기\npop = stack.pop\n\n# 스택 길이 구하기\nsize = stack.length\n\n# 비어 있는지 확인\nis_empty = stack.empty?\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 5 장   스택과 큐","5.1   스택"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#512","level":2,"title":"5.1.2   스택 구현","text":"<p>스택의 작동 원리를 깊이 이해하기 위해 스택 클래스를 직접 구현해 보자.</p> <p>스택은 후입선출 원리를 따르므로 맨 위에서만 원소를 추가하거나 삭제할 수 있다. 반면 배열과 연결 리스트는 어느 위치에서든 추가와 삭제가 가능하므로, 스택은 제약을 가한 배열이나 연결 리스트로 볼 수 있다. 즉, 일부 무관한 연산을 ‘가려서’ 외부에 보이는 논리가 스택의 특성에 맞도록 만들 수 있다.</p>","path":["제 5 장   스택과 큐","5.1   스택"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#1","level":3,"title":"1.   연결 리스트 기반 구현","text":"<p>연결 리스트로 스택을 구현할 때는 머리 노드를 스택의 꼭대기로, 꼬리 노드를 바닥으로 볼 수 있다.</p> <p>아래 그림(5-2)과 같이 스택에 삽입할 때는 연결 리스트의 맨 앞에 원소를 넣으면 된다. 이 방식을 ‘머리 삽입’이라고 한다. 스택에서 꺼낼 때는 머리 노드를 삭제하면 된다.</p> &lt;1&gt;&lt;2&gt;&lt;3&gt; <p></p> <p></p> <p></p> <p> 그림 5-2   연결 리스트 기반 스택의 삽입과 꺼내기 </p> <p>연결 리스트로 스택을 구현한 예제 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_stack.py<pre><code>class LinkedListStack:\n \"\"\"연결 리스트로 구현한 스택\"\"\"\n\n def __init__(self):\n \"\"\"생성자\"\"\"\n self._peek: ListNode | None = None\n self._size: int = 0\n\n def size(self) -&gt; int:\n \"\"\"스택의 길이 반환\"\"\"\n return self._size\n\n def is_empty(self) -&gt; bool:\n \"\"\"스택이 비어 있는지 판단\"\"\"\n return self._size == 0\n\n def push(self, val: int):\n \"\"\"스택에 삽입\"\"\"\n node = ListNode(val)\n node.next = self._peek\n self._peek = node\n self._size += 1\n\n def pop(self) -&gt; int:\n \"\"\"스택에서 꺼내기\"\"\"\n num = self.peek()\n self._peek = self._peek.next\n self._size -= 1\n return num\n\n def peek(self) -&gt; int:\n \"\"\"스택 맨 위 원소 접근\"\"\"\n if self.is_empty():\n raise IndexError(\"스택이 비어 있다\")\n return self._peek.val\n\n def to_list(self) -&gt; list[int]:\n \"\"\"출력에 사용할 리스트로 변환\"\"\"\n arr = []\n node = self._peek\n while node:\n arr.append(node.val)\n node = node.next\n arr.reverse()\n return arr\n</code></pre> linkedlist_stack.cpp<pre><code>/* 연결 리스트로 구현한 스택 */\nclass LinkedListStack {\n private:\n ListNode *stackTop; // 머리 노드를 스택의 맨 위로 사용한다\n int stkSize; // 스택의 길이\n\n public:\n LinkedListStack() {\n stackTop = nullptr;\n stkSize = 0;\n }\n\n ~LinkedListStack() {\n // 연결 리스트를 순회하며 노드를 삭제하여 메모리를 해제한다\n freeMemoryLinkedList(stackTop);\n }\n\n /* 스택의 길이 반환 */\n int size() {\n return stkSize;\n }\n\n /* 스택이 비어 있는지 판단 */\n bool isEmpty() {\n return size() == 0;\n }\n\n /* 스택에 삽입 */\n void push(int num) {\n ListNode *node = new ListNode(num);\n node-&gt;next = stackTop;\n stackTop = node;\n stkSize++;\n }\n\n /* 스택에서 꺼내기 */\n int pop() {\n int num = top();\n ListNode *tmp = stackTop;\n stackTop = stackTop-&gt;next;\n // 메모리 해제\n delete tmp;\n stkSize--;\n return num;\n }\n\n /* 스택 맨 위 원소 접근 */\n int top() {\n if (isEmpty())\n throw out_of_range(\"스택이 비어 있다\");\n return stackTop-&gt;val;\n }\n\n /* List를 Array로 변환하여 반환 */\n vector&lt;int&gt; toVector() {\n ListNode *node = stackTop;\n vector&lt;int&gt; res(size());\n for (int i = res.size() - 1; i &gt;= 0; i--) {\n res[i] = node-&gt;val;\n node = node-&gt;next;\n }\n return res;\n }\n};\n</code></pre> linkedlist_stack.java<pre><code>/* 연결 리스트로 구현한 스택 */\nclass LinkedListStack {\n private ListNode stackPeek; // 머리 노드를 스택의 맨 위로 사용한다\n private int stkSize = 0; // 스택의 길이\n\n public LinkedListStack() {\n stackPeek = null;\n }\n\n /* 스택의 길이 반환 */\n public int size() {\n return stkSize;\n }\n\n /* 스택이 비어 있는지 판단 */\n public boolean isEmpty() {\n return size() == 0;\n }\n\n /* 스택에 삽입 */\n public void push(int num) {\n ListNode node = new ListNode(num);\n node.next = stackPeek;\n stackPeek = node;\n stkSize++;\n }\n\n /* 스택에서 꺼내기 */\n public int pop() {\n int num = peek();\n stackPeek = stackPeek.next;\n stkSize--;\n return num;\n }\n\n /* 스택 맨 위 원소 접근 */\n public int peek() {\n if (isEmpty())\n throw new IndexOutOfBoundsException();\n return stackPeek.val;\n }\n\n /* List를 Array로 변환하여 반환 */\n public int[] toArray() {\n ListNode node = stackPeek;\n int[] res = new int[size()];\n for (int i = res.length - 1; i &gt;= 0; i--) {\n res[i] = node.val;\n node = node.next;\n }\n return res;\n }\n}\n</code></pre> linkedlist_stack.cs<pre><code>/* 연결 리스트로 구현한 스택 */\nclass LinkedListStack {\n ListNode? stackPeek; // 머리 노드를 스택의 맨 위로 사용한다\n int stkSize = 0; // 스택의 길이\n\n public LinkedListStack() {\n stackPeek = null;\n }\n\n /* 스택의 길이 반환 */\n public int Size() {\n return stkSize;\n }\n\n /* 스택이 비어 있는지 판단 */\n public bool IsEmpty() {\n return Size() == 0;\n }\n\n /* 스택에 삽입 */\n public void Push(int num) {\n ListNode node = new(num) {\n next = stackPeek\n };\n stackPeek = node;\n stkSize++;\n }\n\n /* 스택에서 꺼내기 */\n public int Pop() {\n int num = Peek();\n stackPeek = stackPeek!.next;\n stkSize--;\n return num;\n }\n\n /* 스택 맨 위 원소 접근 */\n public int Peek() {\n if (IsEmpty())\n throw new Exception();\n return stackPeek!.val;\n }\n\n /* List를 Array로 변환하여 반환 */\n public int[] ToArray() {\n if (stackPeek == null)\n return [];\n\n ListNode? node = stackPeek;\n int[] res = new int[Size()];\n for (int i = res.Length - 1; i &gt;= 0; i--) {\n res[i] = node!.val;\n node = node.next;\n }\n return res;\n }\n}\n</code></pre> linkedlist_stack.go<pre><code>/* 연결 리스트로 구현한 스택 */\ntype linkedListStack struct {\n // 내장 패키지 list로 스택을 구현한다\n data *list.List\n}\n\n/* 스택 초기화 */\nfunc newLinkedListStack() *linkedListStack {\n return &amp;linkedListStack{\n data: list.New(),\n }\n}\n\n/* 스택에 삽입 */\nfunc (s *linkedListStack) push(value int) {\n s.data.PushBack(value)\n}\n\n/* 스택에서 꺼내기 */\nfunc (s *linkedListStack) pop() any {\n if s.isEmpty() {\n return nil\n }\n e := s.data.Back()\n s.data.Remove(e)\n return e.Value\n}\n\n/* 스택 맨 위 원소 접근 */\nfunc (s *linkedListStack) peek() any {\n if s.isEmpty() {\n return nil\n }\n e := s.data.Back()\n return e.Value\n}\n\n/* 스택의 길이 반환 */\nfunc (s *linkedListStack) size() int {\n return s.data.Len()\n}\n\n/* 스택이 비어 있는지 판단 */\nfunc (s *linkedListStack) isEmpty() bool {\n return s.data.Len() == 0\n}\n\n/* 출력에 사용할 List 반환 */\nfunc (s *linkedListStack) toList() *list.List {\n return s.data\n}\n</code></pre> linkedlist_stack.swift<pre><code>/* 연결 리스트로 구현한 스택 */\nclass LinkedListStack {\n private var _peek: ListNode? // 머리 노드를 스택의 맨 위로 사용한다\n private var _size: Int // 스택의 길이\n\n init() {\n _size = 0\n }\n\n /* 스택의 길이 반환 */\n func size() -&gt; Int {\n _size\n }\n\n /* 스택이 비어 있는지 판단 */\n func isEmpty() -&gt; Bool {\n size() == 0\n }\n\n /* 스택에 삽입 */\n func push(num: Int) {\n let node = ListNode(x: num)\n node.next = _peek\n _peek = node\n _size += 1\n }\n\n /* 스택에서 꺼내기 */\n @discardableResult\n func pop() -&gt; Int {\n let num = peek()\n _peek = _peek?.next\n _size -= 1\n return num\n }\n\n /* 스택 맨 위 원소 접근 */\n func peek() -&gt; Int {\n if isEmpty() {\n fatalError(\"스택이 비어 있다\")\n }\n return _peek!.val\n }\n\n /* List를 Array로 변환하여 반환 */\n func toArray() -&gt; [Int] {\n var node = _peek\n var res = Array(repeating: 0, count: size())\n for i in res.indices.reversed() {\n res[i] = node!.val\n node = node?.next\n }\n return res\n }\n}\n</code></pre> linkedlist_stack.js<pre><code>/* 연결 리스트로 구현한 스택 */\nclass LinkedListStack {\n #stackPeek; // 머리 노드를 스택의 맨 위로 사용한다\n #stkSize = 0; // 스택의 길이\n\n constructor() {\n this.#stackPeek = null;\n }\n\n /* 스택의 길이 반환 */\n get size() {\n return this.#stkSize;\n }\n\n /* 스택이 비어 있는지 판단 */\n isEmpty() {\n return this.size === 0;\n }\n\n /* 스택에 삽입 */\n push(num) {\n const node = new ListNode(num);\n node.next = this.#stackPeek;\n this.#stackPeek = node;\n this.#stkSize++;\n }\n\n /* 스택에서 꺼내기 */\n pop() {\n const num = this.peek();\n this.#stackPeek = this.#stackPeek.next;\n this.#stkSize--;\n return num;\n }\n\n /* 스택 맨 위 원소 접근 */\n peek() {\n if (!this.#stackPeek) throw new Error('스택이 비어 있다');\n return this.#stackPeek.val;\n }\n\n /* 연결 리스트를 Array로 변환하여 반환 */\n toArray() {\n let node = this.#stackPeek;\n const res = new Array(this.size);\n for (let i = res.length - 1; i &gt;= 0; i--) {\n res[i] = node.val;\n node = node.next;\n }\n return res;\n }\n}\n</code></pre> linkedlist_stack.ts<pre><code>/* 연결 리스트로 구현한 스택 */\nclass LinkedListStack {\n private stackPeek: ListNode | null; // 머리 노드를 스택의 맨 위로 사용한다\n private stkSize: number = 0; // 스택의 길이\n\n constructor() {\n this.stackPeek = null;\n }\n\n /* 스택의 길이 반환 */\n get size(): number {\n return this.stkSize;\n }\n\n /* 스택이 비어 있는지 판단 */\n isEmpty(): boolean {\n return this.size === 0;\n }\n\n /* 스택에 삽입 */\n push(num: number): void {\n const node = new ListNode(num);\n node.next = this.stackPeek;\n this.stackPeek = node;\n this.stkSize++;\n }\n\n /* 스택에서 꺼내기 */\n pop(): number {\n const num = this.peek();\n if (!this.stackPeek) throw new Error('스택이 비어 있다');\n this.stackPeek = this.stackPeek.next;\n this.stkSize--;\n return num;\n }\n\n /* 스택 맨 위 원소 접근 */\n peek(): number {\n if (!this.stackPeek) throw new Error('스택이 비어 있다');\n return this.stackPeek.val;\n }\n\n /* 연결 리스트를 Array로 변환하여 반환 */\n toArray(): number[] {\n let node = this.stackPeek;\n const res = new Array&lt;number&gt;(this.size);\n for (let i = res.length - 1; i &gt;= 0; i--) {\n res[i] = node!.val;\n node = node!.next;\n }\n return res;\n }\n}\n</code></pre> linkedlist_stack.dart<pre><code>/* 연결 리스트 클래스로 구현한 스택 */\nclass LinkedListStack {\n ListNode? _stackPeek; // 머리 노드를 스택의 맨 위로 사용한다\n int _stkSize = 0; // 스택의 길이\n\n LinkedListStack() {\n _stackPeek = null;\n }\n\n /* 스택의 길이 반환 */\n int size() {\n return _stkSize;\n }\n\n /* 스택이 비어 있는지 판단 */\n bool isEmpty() {\n return _stkSize == 0;\n }\n\n /* 스택에 삽입 */\n void push(int _num) {\n final ListNode node = ListNode(_num);\n node.next = _stackPeek;\n _stackPeek = node;\n _stkSize++;\n }\n\n /* 스택에서 꺼내기 */\n int pop() {\n final int _num = peek();\n _stackPeek = _stackPeek!.next;\n _stkSize--;\n return _num;\n }\n\n /* 스택 맨 위 원소 접근 */\n int peek() {\n if (_stackPeek == null) {\n throw Exception(\"스택이 비어 있다\");\n }\n return _stackPeek!.val;\n }\n\n /* 연결 리스트를 List로 변환하여 반환 */\n List&lt;int&gt; toList() {\n ListNode? node = _stackPeek;\n List&lt;int&gt; list = [];\n while (node != null) {\n list.add(node.val);\n node = node.next;\n }\n list = list.reversed.toList();\n return list;\n }\n}\n</code></pre> linkedlist_stack.rs<pre><code>/* 연결 리스트로 구현한 스택 */\n#[allow(dead_code)]\npub struct LinkedListStack&lt;T&gt; {\n stack_peek: Option&lt;Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt;&gt;, // 머리 노드를 스택의 맨 위로 사용한다\n stk_size: usize, // 스택의 길이\n}\n\nimpl&lt;T: Copy&gt; LinkedListStack&lt;T&gt; {\n pub fn new() -&gt; Self {\n Self {\n stack_peek: None,\n stk_size: 0,\n }\n }\n\n /* 스택의 길이 반환 */\n pub fn size(&amp;self) -&gt; usize {\n return self.stk_size;\n }\n\n /* 스택이 비어 있는지 판단 */\n pub fn is_empty(&amp;self) -&gt; bool {\n return self.size() == 0;\n }\n\n /* 스택에 삽입 */\n pub fn push(&amp;mut self, num: T) {\n let node = ListNode::new(num);\n node.borrow_mut().next = self.stack_peek.take();\n self.stack_peek = Some(node);\n self.stk_size += 1;\n }\n\n /* 스택에서 꺼내기 */\n pub fn pop(&amp;mut self) -&gt; Option&lt;T&gt; {\n self.stack_peek.take().map(|old_head| {\n self.stack_peek = old_head.borrow_mut().next.take();\n self.stk_size -= 1;\n\n old_head.borrow().val\n })\n }\n\n /* 스택 맨 위 원소 접근 */\n pub fn peek(&amp;self) -&gt; Option&lt;&amp;Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt;&gt; {\n self.stack_peek.as_ref()\n }\n\n /* List를 Array로 변환하여 반환 */\n pub fn to_array(&amp;self) -&gt; Vec&lt;T&gt; {\n fn _to_array&lt;T: Sized + Copy&gt;(head: Option&lt;&amp;Rc&lt;RefCell&lt;ListNode&lt;T&gt;&gt;&gt;&gt;) -&gt; Vec&lt;T&gt; {\n if let Some(node) = head {\n let mut nums = _to_array(node.borrow().next.as_ref());\n nums.push(node.borrow().val);\n return nums;\n }\n return Vec::new();\n }\n\n _to_array(self.peek())\n }\n}\n</code></pre> linkedlist_stack.c<pre><code>/* 연결 리스트로 구현한 스택 */\ntypedef struct {\n ListNode *top; // 머리 노드를 스택의 맨 위로 사용한다\n int size; // 스택의 길이\n} LinkedListStack;\n\n/* 생성자 */\nLinkedListStack *newLinkedListStack() {\n LinkedListStack *s = malloc(sizeof(LinkedListStack));\n s-&gt;top = NULL;\n s-&gt;size = 0;\n return s;\n}\n\n/* 소멸자 */\nvoid delLinkedListStack(LinkedListStack *s) {\n while (s-&gt;top) {\n ListNode *n = s-&gt;top-&gt;next;\n free(s-&gt;top);\n s-&gt;top = n;\n }\n free(s);\n}\n\n/* 스택의 길이 반환 */\nint size(LinkedListStack *s) {\n return s-&gt;size;\n}\n\n/* 스택이 비어 있는지 판단 */\nbool isEmpty(LinkedListStack *s) {\n return size(s) == 0;\n}\n\n/* 스택에 삽입 */\nvoid push(LinkedListStack *s, int num) {\n ListNode *node = (ListNode *)malloc(sizeof(ListNode));\n node-&gt;next = s-&gt;top; // 새로 추가한 노드의 포인터 필드를 갱신한다\n node-&gt;val = num; // 새로 추가한 노드의 데이터 필드를 갱신한다\n s-&gt;top = node; // 스택의 맨 위를 갱신한다\n s-&gt;size++; // 스택 크기를 갱신한다\n}\n\n/* 스택 맨 위 원소 접근 */\nint peek(LinkedListStack *s) {\n if (s-&gt;size == 0) {\n printf(\"스택이 비어 있다\\n\");\n return INT_MAX;\n }\n return s-&gt;top-&gt;val;\n}\n\n/* 스택에서 꺼내기 */\nint pop(LinkedListStack *s) {\n int val = peek(s);\n ListNode *tmp = s-&gt;top;\n s-&gt;top = s-&gt;top-&gt;next;\n // 메모리 해제\n free(tmp);\n s-&gt;size--;\n return val;\n}\n</code></pre> linkedlist_stack.kt<pre><code>/* 연결 리스트로 구현한 스택 */\nclass LinkedListStack(\n private var stackPeek: ListNode? = null, // 머리 노드를 스택의 맨 위로 사용한다\n private var stkSize: Int = 0 // 스택의 길이\n) {\n\n /* 스택의 길이 반환 */\n fun size(): Int {\n return stkSize\n }\n\n /* 스택이 비어 있는지 판단 */\n fun isEmpty(): Boolean {\n return size() == 0\n }\n\n /* 스택에 삽입 */\n fun push(num: Int) {\n val node = ListNode(num)\n node.next = stackPeek\n stackPeek = node\n stkSize++\n }\n\n /* 스택에서 꺼내기 */\n fun pop(): Int? {\n val num = peek()\n stackPeek = stackPeek?.next\n stkSize--\n return num\n }\n\n /* 스택 맨 위 원소 접근 */\n fun peek(): Int? {\n if (isEmpty()) throw IndexOutOfBoundsException()\n return stackPeek?._val\n }\n\n /* List를 Array로 변환하여 반환 */\n fun toArray(): IntArray {\n var node = stackPeek\n val res = IntArray(size())\n for (i in res.size - 1 downTo 0) {\n res[i] = node?._val!!\n node = node.next\n }\n return res\n }\n}\n</code></pre> linkedlist_stack.rb<pre><code>### 연결 리스트로 구현한 스택 ###\nclass LinkedListStack\n attr_reader :size\n\n ### 생성자 ###\n def initialize\n @size = 0\n end\n\n ### 스택이 비어 있는지 판단 ###\n def is_empty?\n @peek.nil?\n end\n\n ### 스택에 삽입 ###\n def push(val)\n node = ListNode.new(val)\n node.next = @peek\n @peek = node\n @size += 1\n end\n\n ### 스택에서 꺼내기 ###\n def pop\n num = peek\n @peek = @peek.next\n @size -= 1\n num\n end\n\n ### 스택 맨 위 원소 접근 ###\n def peek\n raise IndexError, '스택이 비어 있다' if is_empty?\n\n @peek.val\n end\n\n ### 연결 리스트를 Array로 변환하여 반환 ###\n def to_array\n arr = []\n node = @peek\n while node\n arr &lt;&lt; node.val\n node = node.next\n end\n arr.reverse\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 5 장   스택과 큐","5.1   스택"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#2","level":3,"title":"2.   배열 기반 구현","text":"<p>배열로 스택을 구현할 때는 배열의 끝을 스택의 꼭대기로 볼 수 있다. 아래 그림(5-3)과 같이 삽입과 꺼내기는 각각 배열 끝에 원소를 추가하고 삭제하는 연산에 해당하며, 시간 복잡도는 모두 \\(O(1)\\)이다.</p> &lt;1&gt;&lt;2&gt;&lt;3&gt; <p></p> <p></p> <p></p> <p> 그림 5-3   배열 기반 스택의 삽입과 꺼내기 </p> <p>스택에 삽입되는 원소가 계속 늘어날 수 있으므로 동적 배열을 사용하면 배열의 용량 확장을 직접 처리하지 않아도 된다. 예제 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_stack.py<pre><code>class ArrayStack:\n \"\"\"배열로 구현한 스택\"\"\"\n\n def __init__(self):\n \"\"\"생성자\"\"\"\n self._stack: list[int] = []\n\n def size(self) -&gt; int:\n \"\"\"스택의 길이 반환\"\"\"\n return len(self._stack)\n\n def is_empty(self) -&gt; bool:\n \"\"\"스택이 비어 있는지 판단\"\"\"\n return self.size() == 0\n\n def push(self, item: int):\n \"\"\"스택에 삽입\"\"\"\n self._stack.append(item)\n\n def pop(self) -&gt; int:\n \"\"\"스택에서 꺼내기\"\"\"\n if self.is_empty():\n raise IndexError(\"스택이 비어 있다\")\n return self._stack.pop()\n\n def peek(self) -&gt; int:\n \"\"\"스택 맨 위 원소 접근\"\"\"\n if self.is_empty():\n raise IndexError(\"스택이 비어 있다\")\n return self._stack[-1]\n\n def to_list(self) -&gt; list[int]:\n \"\"\"출력에 사용할 리스트 반환\"\"\"\n return self._stack\n</code></pre> array_stack.cpp<pre><code>/* 배열로 구현한 스택 */\nclass ArrayStack {\n private:\n vector&lt;int&gt; stack;\n\n public:\n /* 스택의 길이 반환 */\n int size() {\n return stack.size();\n }\n\n /* 스택이 비어 있는지 판단 */\n bool isEmpty() {\n return stack.size() == 0;\n }\n\n /* 스택에 삽입 */\n void push(int num) {\n stack.push_back(num);\n }\n\n /* 스택에서 꺼내기 */\n int pop() {\n int num = top();\n stack.pop_back();\n return num;\n }\n\n /* 스택 맨 위 원소 접근 */\n int top() {\n if (isEmpty())\n throw out_of_range(\"스택이 비어 있다\");\n return stack.back();\n }\n\n /* Vector 반환 */\n vector&lt;int&gt; toVector() {\n return stack;\n }\n};\n</code></pre> array_stack.java<pre><code>/* 배열로 구현한 스택 */\nclass ArrayStack {\n private ArrayList&lt;Integer&gt; stack;\n\n public ArrayStack() {\n // 리스트(동적 배열)를 초기화한다\n stack = new ArrayList&lt;&gt;();\n }\n\n /* 스택의 길이 반환 */\n public int size() {\n return stack.size();\n }\n\n /* 스택이 비어 있는지 판단 */\n public boolean isEmpty() {\n return size() == 0;\n }\n\n /* 스택에 삽입 */\n public void push(int num) {\n stack.add(num);\n }\n\n /* 스택에서 꺼내기 */\n public int pop() {\n if (isEmpty())\n throw new IndexOutOfBoundsException();\n return stack.remove(size() - 1);\n }\n\n /* 스택 맨 위 원소 접근 */\n public int peek() {\n if (isEmpty())\n throw new IndexOutOfBoundsException();\n return stack.get(size() - 1);\n }\n\n /* List를 Array로 변환하여 반환 */\n public Object[] toArray() {\n return stack.toArray();\n }\n}\n</code></pre> array_stack.cs<pre><code>/* 배열로 구현한 스택 */\nclass ArrayStack {\n List&lt;int&gt; stack;\n public ArrayStack() {\n // 리스트(동적 배열)를 초기화한다\n stack = [];\n }\n\n /* 스택의 길이 반환 */\n public int Size() {\n return stack.Count;\n }\n\n /* 스택이 비어 있는지 판단 */\n public bool IsEmpty() {\n return Size() == 0;\n }\n\n /* 스택에 삽입 */\n public void Push(int num) {\n stack.Add(num);\n }\n\n /* 스택에서 꺼내기 */\n public int Pop() {\n if (IsEmpty())\n throw new Exception();\n var val = Peek();\n stack.RemoveAt(Size() - 1);\n return val;\n }\n\n /* 스택 맨 위 원소 접근 */\n public int Peek() {\n if (IsEmpty())\n throw new Exception();\n return stack[Size() - 1];\n }\n\n /* List를 Array로 변환하여 반환 */\n public int[] ToArray() {\n return [.. stack];\n }\n}\n</code></pre> array_stack.go<pre><code>/* 배열로 구현한 스택 */\ntype arrayStack struct {\n data []int // 데이터\n}\n\n/* 스택 초기화 */\nfunc newArrayStack() *arrayStack {\n return &amp;arrayStack{\n // 스택의 길이를 0, 용량을 16으로 설정한다\n data: make([]int, 0, 16),\n }\n}\n\n/* 스택의 길이 */\nfunc (s *arrayStack) size() int {\n return len(s.data)\n}\n\n/* 스택이 비어 있는지 판단 */\nfunc (s *arrayStack) isEmpty() bool {\n return s.size() == 0\n}\n\n/* 스택에 삽입 */\nfunc (s *arrayStack) push(v int) {\n // 슬라이스는 용량이 자동으로 확장된다\n s.data = append(s.data, v)\n}\n\n/* 스택에서 꺼내기 */\nfunc (s *arrayStack) pop() any {\n val := s.peek()\n s.data = s.data[:len(s.data)-1]\n return val\n}\n\n/* 스택 맨 위 원소 반환 */\nfunc (s *arrayStack) peek() any {\n if s.isEmpty() {\n return nil\n }\n val := s.data[len(s.data)-1]\n return val\n}\n\n/* 출력에 사용할 Slice 반환 */\nfunc (s *arrayStack) toSlice() []int {\n return s.data\n}\n</code></pre> array_stack.swift<pre><code>/* 배열로 구현한 스택 */\nclass ArrayStack {\n private var stack: [Int]\n\n init() {\n // 리스트(동적 배열)를 초기화한다\n stack = []\n }\n\n /* 스택의 길이 반환 */\n func size() -&gt; Int {\n stack.count\n }\n\n /* 스택이 비어 있는지 판단 */\n func isEmpty() -&gt; Bool {\n stack.isEmpty\n }\n\n /* 스택에 삽입 */\n func push(num: Int) {\n stack.append(num)\n }\n\n /* 스택에서 꺼내기 */\n @discardableResult\n func pop() -&gt; Int {\n if isEmpty() {\n fatalError(\"스택이 비어 있다\")\n }\n return stack.removeLast()\n }\n\n /* 스택 맨 위 원소 접근 */\n func peek() -&gt; Int {\n if isEmpty() {\n fatalError(\"스택이 비어 있다\")\n }\n return stack.last!\n }\n\n /* List를 Array로 변환하여 반환 */\n func toArray() -&gt; [Int] {\n stack\n }\n}\n</code></pre> array_stack.js<pre><code>/* 배열로 구현한 스택 */\nclass ArrayStack {\n #stack;\n constructor() {\n this.#stack = [];\n }\n\n /* 스택의 길이 반환 */\n get size() {\n return this.#stack.length;\n }\n\n /* 스택이 비어 있는지 판단 */\n isEmpty() {\n return this.#stack.length === 0;\n }\n\n /* 스택에 삽입 */\n push(num) {\n this.#stack.push(num);\n }\n\n /* 스택에서 꺼내기 */\n pop() {\n if (this.isEmpty()) throw new Error('스택이 비어 있다');\n return this.#stack.pop();\n }\n\n /* 스택 맨 위 원소 접근 */\n top() {\n if (this.isEmpty()) throw new Error('스택이 비어 있다');\n return this.#stack[this.#stack.length - 1];\n }\n\n /* Array를 반환한다 */\n toArray() {\n return this.#stack;\n }\n}\n</code></pre> array_stack.ts<pre><code>/* 배열로 구현한 스택 */\nclass ArrayStack {\n private stack: number[];\n constructor() {\n this.stack = [];\n }\n\n /* 스택의 길이 반환 */\n get size(): number {\n return this.stack.length;\n }\n\n /* 스택이 비어 있는지 판단 */\n isEmpty(): boolean {\n return this.stack.length === 0;\n }\n\n /* 스택에 삽입 */\n push(num: number): void {\n this.stack.push(num);\n }\n\n /* 스택에서 꺼내기 */\n pop(): number | undefined {\n if (this.isEmpty()) throw new Error('스택이 비어 있다');\n return this.stack.pop();\n }\n\n /* 스택 맨 위 원소 접근 */\n top(): number | undefined {\n if (this.isEmpty()) throw new Error('스택이 비어 있다');\n return this.stack[this.stack.length - 1];\n }\n\n /* Array를 반환한다 */\n toArray() {\n return this.stack;\n }\n}\n</code></pre> array_stack.dart<pre><code>/* 배열로 구현한 스택 */\nclass ArrayStack {\n late List&lt;int&gt; _stack;\n ArrayStack() {\n _stack = [];\n }\n\n /* 스택의 길이 반환 */\n int size() {\n return _stack.length;\n }\n\n /* 스택이 비어 있는지 판단 */\n bool isEmpty() {\n return _stack.isEmpty;\n }\n\n /* 스택에 삽입 */\n void push(int _num) {\n _stack.add(_num);\n }\n\n /* 스택에서 꺼내기 */\n int pop() {\n if (isEmpty()) {\n throw Exception(\"스택이 비어 있다\");\n }\n return _stack.removeLast();\n }\n\n /* 스택 맨 위 원소 접근 */\n int peek() {\n if (isEmpty()) {\n throw Exception(\"스택이 비어 있다\");\n }\n return _stack.last;\n }\n\n /* 스택을 Array로 변환하여 반환 */\n List&lt;int&gt; toArray() =&gt; _stack;\n}\n</code></pre> array_stack.rs<pre><code>/* 배열로 구현한 스택 */\nstruct ArrayStack&lt;T&gt; {\n stack: Vec&lt;T&gt;,\n}\n\nimpl&lt;T&gt; ArrayStack&lt;T&gt; {\n /* 스택 초기화 */\n fn new() -&gt; ArrayStack&lt;T&gt; {\n ArrayStack::&lt;T&gt; {\n stack: Vec::&lt;T&gt;::new(),\n }\n }\n\n /* 스택의 길이 반환 */\n fn size(&amp;self) -&gt; usize {\n self.stack.len()\n }\n\n /* 스택이 비어 있는지 판단 */\n fn is_empty(&amp;self) -&gt; bool {\n self.size() == 0\n }\n\n /* 스택에 삽입 */\n fn push(&amp;mut self, num: T) {\n self.stack.push(num);\n }\n\n /* 스택에서 꺼내기 */\n fn pop(&amp;mut self) -&gt; Option&lt;T&gt; {\n self.stack.pop()\n }\n\n /* 스택 맨 위 원소 접근 */\n fn peek(&amp;self) -&gt; Option&lt;&amp;T&gt; {\n if self.is_empty() {\n panic!(\"스택이 비어 있다\")\n };\n self.stack.last()\n }\n\n /* &amp;Vec를 반환한다 */\n fn to_array(&amp;self) -&gt; &amp;Vec&lt;T&gt; {\n &amp;self.stack\n }\n}\n</code></pre> array_stack.c<pre><code>/* 배열로 구현한 스택 */\ntypedef struct {\n int *data;\n int size;\n} ArrayStack;\n\n/* 생성자 */\nArrayStack *newArrayStack() {\n ArrayStack *stack = malloc(sizeof(ArrayStack));\n // 용량 확장을 피하도록 큰 용량으로 초기화한다\n stack-&gt;data = malloc(sizeof(int) * MAX_SIZE);\n stack-&gt;size = 0;\n return stack;\n}\n\n/* 소멸자 */\nvoid delArrayStack(ArrayStack *stack) {\n free(stack-&gt;data);\n free(stack);\n}\n\n/* 스택의 길이 반환 */\nint size(ArrayStack *stack) {\n return stack-&gt;size;\n}\n\n/* 스택이 비어 있는지 판단 */\nbool isEmpty(ArrayStack *stack) {\n return stack-&gt;size == 0;\n}\n\n/* 스택에 삽입 */\nvoid push(ArrayStack *stack, int num) {\n if (stack-&gt;size == MAX_SIZE) {\n printf(\"스택이 가득 찼다\\n\");\n return;\n }\n stack-&gt;data[stack-&gt;size] = num;\n stack-&gt;size++;\n}\n\n/* 스택 맨 위 원소 접근 */\nint peek(ArrayStack *stack) {\n if (stack-&gt;size == 0) {\n printf(\"스택이 비어 있다\\n\");\n return INT_MAX;\n }\n return stack-&gt;data[stack-&gt;size - 1];\n}\n\n/* 스택에서 꺼내기 */\nint pop(ArrayStack *stack) {\n int val = peek(stack);\n stack-&gt;size--;\n return val;\n}\n</code></pre> array_stack.kt<pre><code>/* 배열로 구현한 스택 */\nclass ArrayStack {\n // 리스트(동적 배열)를 초기화한다\n private val stack = mutableListOf&lt;Int&gt;()\n\n /* 스택의 길이 반환 */\n fun size(): Int {\n return stack.size\n }\n\n /* 스택이 비어 있는지 판단 */\n fun isEmpty(): Boolean {\n return size() == 0\n }\n\n /* 스택에 삽입 */\n fun push(num: Int) {\n stack.add(num)\n }\n\n /* 스택에서 꺼내기 */\n fun pop(): Int {\n if (isEmpty()) throw IndexOutOfBoundsException()\n return stack.removeAt(size() - 1)\n }\n\n /* 스택 맨 위 원소 접근 */\n fun peek(): Int {\n if (isEmpty()) throw IndexOutOfBoundsException()\n return stack[size() - 1]\n }\n\n /* List를 Array로 변환하여 반환 */\n fun toArray(): Array&lt;Any&gt; {\n return stack.toTypedArray()\n }\n}\n</code></pre> array_stack.rb<pre><code>### 배열로 구현한 스택 ###\nclass ArrayStack\n ### 생성자 ###\n def initialize\n @stack = []\n end\n\n ### 스택의 길이 반환 ###\n def size\n @stack.length\n end\n\n ### 스택이 비어 있는지 판단 ###\n def is_empty?\n @stack.empty?\n end\n\n ### 스택에 삽입 ###\n def push(item)\n @stack &lt;&lt; item\n end\n\n ### 스택에서 꺼내기 ###\n def pop\n raise IndexError, '스택이 비어 있다' if is_empty?\n\n @stack.pop\n end\n\n ### 스택 맨 위 원소 접근 ###\n def peek\n raise IndexError, '스택이 비어 있다' if is_empty?\n\n @stack.last\n end\n\n ### 출력에 사용할 리스트 반환 ###\n def to_array\n @stack\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 5 장   스택과 큐","5.1   스택"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#513","level":2,"title":"5.1.3   두 구현의 비교","text":"<p>지원 연산</p> <p>두 구현 모두 스택 정의에 포함된 모든 연산을 지원한다. 배열 기반 구현은 임의 접근도 지원하지만, 이는 스택의 정의를 벗어나므로 일반적으로 사용하지 않는다.</p> <p>시간 효율</p> <p>배열 기반 구현에서는 삽입과 꺼내기를 미리 할당한 연속 메모리에서 수행한다. 캐시 지역성이 좋아 효율이 높다. 다만 삽입 시 배열 용량을 넘으면 확장이 발생하여 그 삽입 연산의 시간 복잡도는 \\(O(n)\\)이 된다.</p> <p>연결 리스트 기반 구현은 크기를 유연하게 늘릴 수 있어 배열 확장에 따른 효율 저하가 없다. 하지만 삽입 시 노드 객체를 초기화하고 포인터를 수정해야 하므로 상대적으로 효율이 낮다. 삽입할 원소 자체가 노드 객체라면 초기화 단계를 생략하여 효율을 높일 수 있다.</p> <p>따라서 삽입하고 꺼내는 원소가 <code>int</code>나 <code>double</code> 같은 기본 자료형이라면 다음과 같이 정리할 수 있다.</p> <ul> <li>배열 기반 스택은 용량 확장이 발생할 때 효율이 낮아지지만, 확장은 드물게 일어나므로 평균 효율은 더 높다.</li> <li>연결 리스트 기반 스택은 더 안정적인 효율을 제공한다.</li> </ul> <p>공간 효율</p> <p>리스트를 초기화할 때 시스템은 ‘초기 용량’을 할당하며, 이 용량은 실제 필요보다 클 수 있다. 또한 확장 시 보통 2배처럼 일정한 비율로 늘리므로 확장 후 용량도 실제 필요보다 클 수 있다. 따라서 배열 기반 스택은 어느 정도 공간을 낭비할 수 있다.</p> <p>반면 연결 리스트 노드는 포인터를 추가로 저장해야 하므로 노드 하나가 차지하는 공간이 상대적으로 크다.</p> <p>따라서 어느 구현이 메모리를 더 절약하는지는 단정할 수 없으며 구체적인 상황에 따라 분석해야 한다.</p>","path":["제 5 장   스택과 큐","5.1   스택"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#514","level":2,"title":"5.1.4   스택의 대표적인 활용","text":"<ul> <li>브라우저의 뒤로 및 앞으로, 소프트웨어의 실행 취소 및 다시 실행. 새 웹 페이지를 열 때마다 브라우저는 이전 페이지를 스택에 넣어 뒤로 가기로 돌아갈 수 있게 한다. 뒤로 가기는 실제로 스택에서 원소를 꺼내는 연산이다. 뒤로 가기와 앞으로 가기를 함께 지원하려면 스택 두 개를 사용해야 한다.</li> <li>프로그램 메모리 관리. 함수가 호출될 때마다 시스템은 스택 맨 위에 스택 프레임을 추가해 함수의 문맥 정보를 기록한다. 재귀 함수에서는 아래로 호출을 이어 가는 단계에서 스택에 계속 넣고, 위로 되돌아오는 단계에서 계속 꺼낸다.</li> </ul>","path":["제 5 장   스택과 큐","5.1   스택"],"tags":[]},{"location":"chapter_stack_and_queue/summary/","level":1,"title":"5.4   요약","text":"","path":["제 5 장   스택과 큐","5.4   요약"],"tags":[]},{"location":"chapter_stack_and_queue/summary/#1","level":3,"title":"1.   핵심 정리","text":"<ul> <li>스택은 후입선출 원리를 따르는 자료 구조로, 배열이나 연결 리스트로 구현할 수 있다.</li> <li>시간 효율 측면에서 배열 기반 스택의 평균 효율은 더 높지만, 용량 확장이 발생하면 한 번의 삽입 연산의 시간 복잡도가 \\(O(n)\\)으로 나빠진다. 반면 연결 리스트 기반 스택은 더 안정적인 효율을 보인다.</li> <li>공간 효율 측면에서 배열 기반 스택은 어느 정도 공간을 낭비할 수 있다. 다만 연결 리스트의 노드 하나는 배열 원소보다 더 많은 메모리를 차지한다.</li> <li>큐는 선입선출 원리를 따르는 자료 구조로, 역시 배열이나 연결 리스트로 구현할 수 있다. 시간 및 공간 효율의 비교 결과는 앞의 스택과 비슷하다.</li> <li>덱은 자유도가 더 높은 큐로, 양 끝에서 원소를 추가하고 삭제할 수 있다.</li> </ul>","path":["제 5 장   스택과 큐","5.4   요약"],"tags":[]},{"location":"chapter_stack_and_queue/summary/#2-q-a","level":3,"title":"2.   Q &amp; A","text":"<p>Q: 브라우저의 앞으로 가기와 뒤로 가기는 이중 연결 리스트로 구현되는가?</p> <p>브라우저의 앞으로 가기와 뒤로 가기는 본질적으로 ‘스택’을 보여 준다. 사용자가 새 페이지를 방문하면 맨 위에 추가되고, 뒤로 버튼을 누르면 그 페이지가 맨 위에서 꺼내진다. ‘덱’ 절에서 설명했듯 덱을 사용하면 일부 추가 연산을 편리하게 구현할 수 있다.</p> <p>Q: 스택에서 노드를 꺼낸 뒤 그 노드의 메모리를 해제해야 하는가?</p> <p>나중에 꺼낸 노드를 계속 사용해야 한다면 해제할 필요가 없다. 더 이상 사용하지 않는 경우, <code>Java</code>와 <code>Python</code> 같은 언어는 자동 가비지 컬렉션이 있으므로 직접 해제하지 않아도 된다. <code>C</code>와 <code>C++</code>에서는 직접 해제해야 한다.</p> <p>Q: 덱은 두 스택을 이어 붙인 것처럼 보이는데, 어디에 쓰이는가?</p> <p>덱은 스택과 큐를 합치거나 두 스택을 이어 붙인 것과 같다. 스택과 큐의 논리를 모두 지니므로 두 자료 구조의 모든 활용을 구현할 수 있고 더 유연하다.</p> <p>Q: 실행 취소(undo)와 다시 실행(redo)은 구체적으로 어떻게 구현하는가?</p> <p>스택 두 개를 사용한다. 스택 <code>A</code>는 실행 취소용, 스택 <code>B</code>는 다시 실행용이다.</p> <ol> <li>사용자가 작업을 수행할 때마다 그 작업을 스택 <code>A</code>에 넣고 스택 <code>B</code>를 비운다.</li> <li>‘실행 취소’를 수행하면 스택 <code>A</code>에서 가장 최근 작업을 꺼내 스택 <code>B</code>에 넣는다.</li> <li>‘다시 실행’을 수행하면 스택 <code>B</code>에서 가장 최근 작업을 꺼내 스택 <code>A</code>에 넣는다.</li> </ol>","path":["제 5 장   스택과 큐","5.4   요약"],"tags":[]},{"location":"chapter_tree/","level":1,"title":"제 7 장   트리","text":"<p>이 장을 시작하며</p> <p>하늘 높이 뻗은 나무는 깊은 뿌리와 무성한 잎, 넓게 펼쳐진 가지에 생명력이 가득하다.</p> <p>나무는 데이터를 나누어 다루는 모습을 생생하게 보여 준다.</p>","path":["제 7 장   트리"],"tags":[]},{"location":"chapter_tree/#_1","level":2,"title":"이 장의 내용","text":"<ul> <li>7.1   이진 트리</li> <li>7.2   이진 트리 순회</li> <li>7.3   이진 트리의 배열 표현</li> <li>7.4   이진 탐색 트리</li> <li>7.5   AVL 트리 *</li> <li>7.6   요약</li> <li>7.7   연습 문제</li> </ul>","path":["제 7 장   트리"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/","level":1,"title":"7.3   이진 트리의 배열 표현","text":"<p>연결 리스트로 표현할 때 이진 트리의 저장 단위는 노드 <code>TreeNode</code>이며, 노드들은 포인터로 연결된다. 앞 절에서는 이 표현을 사용하는 이진 트리의 기본 연산들을 살펴보았다.</p> <p>그렇다면 배열로도 이진 트리를 표현할 수 있을까? 가능하다.</p>","path":["제 7 장   트리","7.3   이진 트리의 배열 표현"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#731","level":2,"title":"7.3.1   포화 이진 트리 표현","text":"<p>먼저 간단한 경우를 살펴보자. 포화 이진 트리의 모든 노드를 레벨 순서 순회 순서대로 배열에 저장하면 각 노드는 고유한 배열 인덱스에 대응한다.</p> <p>레벨 순서 순회의 특성에서 부모와 자식 인덱스 사이의 '매핑 공식'을 도출할 수 있다. 노드의 인덱스가 \\(i\\)이면 왼쪽 자식의 인덱스는 \\(2i + 1\\), 오른쪽 자식의 인덱스는 \\(2i + 2\\)다. 아래 그림(7-12)은 노드 인덱스 사이의 매핑 관계를 보여 준다.</p> <p></p> <p> 그림 7-12   포화 이진 트리의 배열 표현 </p> <p>매핑 공식은 연결 리스트의 노드 참조(포인터)와 같은 역할을 한다. 배열의 임의의 노드가 주어지면 이 공식으로 왼쪽(오른쪽) 자식에 접근할 수 있다.</p>","path":["제 7 장   트리","7.3   이진 트리의 배열 표현"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#732","level":2,"title":"7.3.2   임의의 이진 트리 표현","text":"<p>포화 이진 트리는 특수한 경우이며, 일반적인 이진 트리의 중간 레벨에는 <code>None</code>이 많이 존재한다. 레벨 순서 순회 결과에는 이 <code>None</code>들이 포함되지 않으므로 결과만으로는 개수와 위치를 알 수 없다. 즉, 같은 레벨 순서 순회 결과에 여러 이진 트리 구조가 대응할 수 있다.</p> <p>아래 그림(7-13)처럼 포화 이진 트리가 아니면 앞의 배열 표현 방법은 더 이상 유효하지 않다.</p> <p></p> <p> 그림 7-13   하나의 레벨 순서 순회 결과에 대응하는 여러 이진 트리 </p> <p>이를 해결하려면 레벨 순서 순회 결과에 모든 <code>None</code>을 명시적으로 넣을 수 있다. 아래 그림(7-14)처럼 처리하면 순회 결과가 이진 트리를 유일하게 나타낸다. 예시 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby <pre><code># 이진 트리의 배열 표현\n# None으로 빈자리 표시\ntree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15]\n</code></pre> <pre><code>/* 이진 트리의 배열 표현 */\n// int의 최댓값 INT_MAX로 빈자리 표시\nvector&lt;int&gt; tree = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};\n</code></pre> <pre><code>/* 이진 트리의 배열 표현 */\n// int의 래퍼 클래스 Integer를 사용하면 null로 빈자리를 표시할 수 있다\nInteger[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 };\n</code></pre> <pre><code>/* 이진 트리의 배열 표현 */\n// 널 허용 타입 int?를 사용하면 null로 빈자리를 표시할 수 있다\nint?[] tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n</code></pre> <pre><code>/* 이진 트리의 배열 표현 */\n// any 타입의 슬라이스를 사용하면 nil로 빈자리를 표시할 수 있다\ntree := []any{1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15}\n</code></pre> <pre><code>/* 이진 트리의 배열 표현 */\n// 널 허용 타입 Int?를 사용하면 nil로 빈자리를 표시할 수 있다\nlet tree: [Int?] = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]\n</code></pre> <pre><code>/* 이진 트리의 배열 표현 */\n// null로 빈자리 표시\nlet tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n</code></pre> <pre><code>/* 이진 트리의 배열 표현 */\n// null로 빈자리 표시\nlet tree: (number | null)[] = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n</code></pre> <pre><code>/* 이진 트리의 배열 표현 */\n// 널 허용 타입 int?를 사용하면 null로 빈자리를 표시할 수 있다\nList&lt;int?&gt; tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n</code></pre> <pre><code>/* 이진 트리의 배열 표현 */\n// None으로 빈자리 표시\nlet tree = [Some(1), Some(2), Some(3), Some(4), None, Some(6), Some(7), Some(8), Some(9), None, None, Some(12), None, None, Some(15)];\n</code></pre> <pre><code>/* 이진 트리의 배열 표현 */\n// int의 최댓값으로 빈자리를 표시하므로 노드 값은 INT_MAX이면 안 된다\nint tree[] = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};\n</code></pre> <pre><code>/* 이진 트리의 배열 표현 */\n// null로 빈자리 표시\nval tree = arrayOf( 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 )\n</code></pre> <pre><code>### 이진 트리의 배열 표현 ###\n# nil로 빈자리 표시\ntree = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]\n</code></pre> <p></p> <p> 그림 7-14   임의의 이진 트리의 배열 표현 </p> <p>완전 이진 트리는 배열로 표현하기에 매우 적합하다. 정의를 떠올려 보면 <code>None</code>은 맨 아래 레벨의 오른쪽에만 있으므로 레벨 순서 순회 결과의 끝에만 나타난다.</p> <p>따라서 완전 이진 트리를 배열로 표현할 때는 모든 <code>None</code>의 저장을 생략할 수 있어 매우 편리하다. 아래 그림(7-15)은 그 예다.</p> <p></p> <p> 그림 7-15   완전 이진 트리의 배열 표현 </p> <p>다음 코드는 배열로 표현한 이진 트리를 구현하며 아래 연산들을 포함한다.</p> <ul> <li>주어진 노드의 값, 왼쪽(오른쪽) 자식, 부모 노드를 구한다.</li> <li>전위, 중위, 후위, 레벨 순서 순회 결과를 구한다.</li> </ul> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_binary_tree.py<pre><code>class ArrayBinaryTree:\n \"\"\"배열로 표현한 이진 트리 클래스\"\"\"\n\n def __init__(self, arr: list[int | None]):\n \"\"\"생성자\"\"\"\n self._tree = list(arr)\n\n def size(self):\n \"\"\"리스트 용량\"\"\"\n return len(self._tree)\n\n def val(self, i: int) -&gt; int | None:\n \"\"\"인덱스가 i인 노드의 값 반환\"\"\"\n # 인덱스가 범위를 벗어나면 빈칸을 나타내는 None을 반환한다\n if i &lt; 0 or i &gt;= self.size():\n return None\n return self._tree[i]\n\n def left(self, i: int) -&gt; int | None:\n \"\"\"인덱스가 i인 노드의 왼쪽 자식 노드 인덱스 반환\"\"\"\n return 2 * i + 1\n\n def right(self, i: int) -&gt; int | None:\n \"\"\"인덱스가 i인 노드의 오른쪽 자식 노드 인덱스 반환\"\"\"\n return 2 * i + 2\n\n def parent(self, i: int) -&gt; int | None:\n \"\"\"인덱스가 i인 노드의 부모 노드 인덱스 반환\"\"\"\n return (i - 1) // 2\n\n def level_order(self) -&gt; list[int]:\n \"\"\"레벨 순서 순회\"\"\"\n self.res = []\n # 배열을 직접 순회한다\n for i in range(self.size()):\n if self.val(i) is not None:\n self.res.append(self.val(i))\n return self.res\n\n def dfs(self, i: int, order: str):\n \"\"\"깊이 우선 순회\"\"\"\n if self.val(i) is None:\n return\n # 전위 순회\n if order == \"pre\":\n self.res.append(self.val(i))\n self.dfs(self.left(i), order)\n # 중위 순회\n if order == \"in\":\n self.res.append(self.val(i))\n self.dfs(self.right(i), order)\n # 후위 순회\n if order == \"post\":\n self.res.append(self.val(i))\n\n def pre_order(self) -&gt; list[int]:\n \"\"\"전위 순회\"\"\"\n self.res = []\n self.dfs(0, order=\"pre\")\n return self.res\n\n def in_order(self) -&gt; list[int]:\n \"\"\"중위 순회\"\"\"\n self.res = []\n self.dfs(0, order=\"in\")\n return self.res\n\n def post_order(self) -&gt; list[int]:\n \"\"\"후위 순회\"\"\"\n self.res = []\n self.dfs(0, order=\"post\")\n return self.res\n</code></pre> array_binary_tree.cpp<pre><code>/* 배열로 표현한 이진 트리 클래스 */\nclass ArrayBinaryTree {\n public:\n /* 생성자 */\n ArrayBinaryTree(vector&lt;int&gt; arr) {\n tree = arr;\n }\n\n /* 리스트 용량 */\n int size() {\n return tree.size();\n }\n\n /* 인덱스가 i인 노드의 값 반환 */\n int val(int i) {\n // 인덱스가 범위를 벗어나면 빈자리를 나타내는 INT_MAX를 반환한다\n if (i &lt; 0 || i &gt;= size())\n return INT_MAX;\n return tree[i];\n }\n\n /* 인덱스가 i인 노드의 왼쪽 자식 노드 인덱스 반환 */\n int left(int i) {\n return 2 * i + 1;\n }\n\n /* 인덱스가 i인 노드의 오른쪽 자식 노드 인덱스 반환 */\n int right(int i) {\n return 2 * i + 2;\n }\n\n /* 인덱스가 i인 노드의 부모 노드 인덱스 반환 */\n int parent(int i) {\n return (i - 1) / 2;\n }\n\n /* 레벨 순서 순회 */\n vector&lt;int&gt; levelOrder() {\n vector&lt;int&gt; res;\n // 배열을 직접 순회한다\n for (int i = 0; i &lt; size(); i++) {\n if (val(i) != INT_MAX)\n res.push_back(val(i));\n }\n return res;\n }\n\n /* 전위 순회 */\n vector&lt;int&gt; preOrder() {\n vector&lt;int&gt; res;\n dfs(0, \"pre\", res);\n return res;\n }\n\n /* 중위 순회 */\n vector&lt;int&gt; inOrder() {\n vector&lt;int&gt; res;\n dfs(0, \"in\", res);\n return res;\n }\n\n /* 후위 순회 */\n vector&lt;int&gt; postOrder() {\n vector&lt;int&gt; res;\n dfs(0, \"post\", res);\n return res;\n }\n\n private:\n vector&lt;int&gt; tree;\n\n /* 깊이 우선 순회 */\n void dfs(int i, string order, vector&lt;int&gt; &amp;res) {\n // 빈칸이면 반환한다\n if (val(i) == INT_MAX)\n return;\n // 전위 순회\n if (order == \"pre\")\n res.push_back(val(i));\n dfs(left(i), order, res);\n // 중위 순회\n if (order == \"in\")\n res.push_back(val(i));\n dfs(right(i), order, res);\n // 후위 순회\n if (order == \"post\")\n res.push_back(val(i));\n }\n};\n</code></pre> array_binary_tree.java<pre><code>/* 배열로 표현한 이진 트리 클래스 */\nclass ArrayBinaryTree {\n private List&lt;Integer&gt; tree;\n\n /* 생성자 */\n public ArrayBinaryTree(List&lt;Integer&gt; arr) {\n tree = new ArrayList&lt;&gt;(arr);\n }\n\n /* 리스트 용량 */\n public int size() {\n return tree.size();\n }\n\n /* 인덱스가 i인 노드의 값 반환 */\n public Integer val(int i) {\n // 인덱스가 범위를 벗어나면 빈칸을 나타내는 null을 반환한다\n if (i &lt; 0 || i &gt;= size())\n return null;\n return tree.get(i);\n }\n\n /* 인덱스가 i인 노드의 왼쪽 자식 노드 인덱스 반환 */\n public Integer left(int i) {\n return 2 * i + 1;\n }\n\n /* 인덱스가 i인 노드의 오른쪽 자식 노드 인덱스 반환 */\n public Integer right(int i) {\n return 2 * i + 2;\n }\n\n /* 인덱스가 i인 노드의 부모 노드 인덱스 반환 */\n public Integer parent(int i) {\n return (i - 1) / 2;\n }\n\n /* 레벨 순서 순회 */\n public List&lt;Integer&gt; levelOrder() {\n List&lt;Integer&gt; res = new ArrayList&lt;&gt;();\n // 배열을 직접 순회한다\n for (int i = 0; i &lt; size(); i++) {\n if (val(i) != null)\n res.add(val(i));\n }\n return res;\n }\n\n /* 깊이 우선 순회 */\n private void dfs(Integer i, String order, List&lt;Integer&gt; res) {\n // 빈칸이면 반환한다\n if (val(i) == null)\n return;\n // 전위 순회\n if (\"pre\".equals(order))\n res.add(val(i));\n dfs(left(i), order, res);\n // 중위 순회\n if (\"in\".equals(order))\n res.add(val(i));\n dfs(right(i), order, res);\n // 후위 순회\n if (\"post\".equals(order))\n res.add(val(i));\n }\n\n /* 전위 순회 */\n public List&lt;Integer&gt; preOrder() {\n List&lt;Integer&gt; res = new ArrayList&lt;&gt;();\n dfs(0, \"pre\", res);\n return res;\n }\n\n /* 중위 순회 */\n public List&lt;Integer&gt; inOrder() {\n List&lt;Integer&gt; res = new ArrayList&lt;&gt;();\n dfs(0, \"in\", res);\n return res;\n }\n\n /* 후위 순회 */\n public List&lt;Integer&gt; postOrder() {\n List&lt;Integer&gt; res = new ArrayList&lt;&gt;();\n dfs(0, \"post\", res);\n return res;\n }\n}\n</code></pre> array_binary_tree.cs<pre><code>/* 배열로 표현한 이진 트리 클래스 */\nclass ArrayBinaryTree(List&lt;int?&gt; arr) {\n List&lt;int?&gt; tree = new(arr);\n\n /* 리스트 용량 */\n public int Size() {\n return tree.Count;\n }\n\n /* 인덱스가 i인 노드의 값 반환 */\n public int? Val(int i) {\n // 인덱스가 범위를 벗어나면 빈칸을 나타내는 null을 반환한다\n if (i &lt; 0 || i &gt;= Size())\n return null;\n return tree[i];\n }\n\n /* 인덱스가 i인 노드의 왼쪽 자식 노드 인덱스 반환 */\n public int Left(int i) {\n return 2 * i + 1;\n }\n\n /* 인덱스가 i인 노드의 오른쪽 자식 노드 인덱스 반환 */\n public int Right(int i) {\n return 2 * i + 2;\n }\n\n /* 인덱스가 i인 노드의 부모 노드 인덱스 반환 */\n public int Parent(int i) {\n return (i - 1) / 2;\n }\n\n /* 레벨 순서 순회 */\n public List&lt;int&gt; LevelOrder() {\n List&lt;int&gt; res = [];\n // 배열을 직접 순회한다\n for (int i = 0; i &lt; Size(); i++) {\n if (Val(i).HasValue)\n res.Add(Val(i)!.Value);\n }\n return res;\n }\n\n /* 깊이 우선 순회 */\n void DFS(int i, string order, List&lt;int&gt; res) {\n // 빈칸이면 반환한다\n if (!Val(i).HasValue)\n return;\n // 전위 순회\n if (order == \"pre\")\n res.Add(Val(i)!.Value);\n DFS(Left(i), order, res);\n // 중위 순회\n if (order == \"in\")\n res.Add(Val(i)!.Value);\n DFS(Right(i), order, res);\n // 후위 순회\n if (order == \"post\")\n res.Add(Val(i)!.Value);\n }\n\n /* 전위 순회 */\n public List&lt;int&gt; PreOrder() {\n List&lt;int&gt; res = [];\n DFS(0, \"pre\", res);\n return res;\n }\n\n /* 중위 순회 */\n public List&lt;int&gt; InOrder() {\n List&lt;int&gt; res = [];\n DFS(0, \"in\", res);\n return res;\n }\n\n /* 후위 순회 */\n public List&lt;int&gt; PostOrder() {\n List&lt;int&gt; res = [];\n DFS(0, \"post\", res);\n return res;\n }\n}\n</code></pre> array_binary_tree.go<pre><code>/* 배열로 표현한 이진 트리 클래스 */\ntype arrayBinaryTree struct {\n tree []any\n}\n\n/* 생성자 */\nfunc newArrayBinaryTree(arr []any) *arrayBinaryTree {\n return &amp;arrayBinaryTree{\n tree: arr,\n }\n}\n\n/* 리스트 용량 */\nfunc (abt *arrayBinaryTree) size() int {\n return len(abt.tree)\n}\n\n/* 인덱스가 i인 노드의 값 반환 */\nfunc (abt *arrayBinaryTree) val(i int) any {\n // 인덱스가 범위를 벗어나면 빈칸을 나타내는 null을 반환한다\n if i &lt; 0 || i &gt;= abt.size() {\n return nil\n }\n return abt.tree[i]\n}\n\n/* 인덱스가 i인 노드의 왼쪽 자식 노드 인덱스 반환 */\nfunc (abt *arrayBinaryTree) left(i int) int {\n return 2*i + 1\n}\n\n/* 인덱스가 i인 노드의 오른쪽 자식 노드 인덱스 반환 */\nfunc (abt *arrayBinaryTree) right(i int) int {\n return 2*i + 2\n}\n\n/* 인덱스가 i인 노드의 부모 노드 인덱스 반환 */\nfunc (abt *arrayBinaryTree) parent(i int) int {\n return (i - 1) / 2\n}\n\n/* 레벨 순서 순회 */\nfunc (abt *arrayBinaryTree) levelOrder() []any {\n var res []any\n // 배열을 직접 순회한다\n for i := 0; i &lt; abt.size(); i++ {\n if abt.val(i) != nil {\n res = append(res, abt.val(i))\n }\n }\n return res\n}\n\n/* 깊이 우선 순회 */\nfunc (abt *arrayBinaryTree) dfs(i int, order string, res *[]any) {\n // 빈칸이면 반환한다\n if abt.val(i) == nil {\n return\n }\n // 전위 순회\n if order == \"pre\" {\n *res = append(*res, abt.val(i))\n }\n abt.dfs(abt.left(i), order, res)\n // 중위 순회\n if order == \"in\" {\n *res = append(*res, abt.val(i))\n }\n abt.dfs(abt.right(i), order, res)\n // 후위 순회\n if order == \"post\" {\n *res = append(*res, abt.val(i))\n }\n}\n\n/* 전위 순회 */\nfunc (abt *arrayBinaryTree) preOrder() []any {\n var res []any\n abt.dfs(0, \"pre\", &amp;res)\n return res\n}\n\n/* 중위 순회 */\nfunc (abt *arrayBinaryTree) inOrder() []any {\n var res []any\n abt.dfs(0, \"in\", &amp;res)\n return res\n}\n\n/* 후위 순회 */\nfunc (abt *arrayBinaryTree) postOrder() []any {\n var res []any\n abt.dfs(0, \"post\", &amp;res)\n return res\n}\n</code></pre> array_binary_tree.swift<pre><code>/* 배열로 표현한 이진 트리 클래스 */\nclass ArrayBinaryTree {\n private var tree: [Int?]\n\n /* 생성자 */\n init(arr: [Int?]) {\n tree = arr\n }\n\n /* 리스트 용량 */\n func size() -&gt; Int {\n tree.count\n }\n\n /* 인덱스가 i인 노드의 값 반환 */\n func val(i: Int) -&gt; Int? {\n // 인덱스가 범위를 벗어나면 빈칸을 나타내는 null을 반환한다\n if i &lt; 0 || i &gt;= size() {\n return nil\n }\n return tree[i]\n }\n\n /* 인덱스가 i인 노드의 왼쪽 자식 노드 인덱스 반환 */\n func left(i: Int) -&gt; Int {\n 2 * i + 1\n }\n\n /* 인덱스가 i인 노드의 오른쪽 자식 노드 인덱스 반환 */\n func right(i: Int) -&gt; Int {\n 2 * i + 2\n }\n\n /* 인덱스가 i인 노드의 부모 노드 인덱스 반환 */\n func parent(i: Int) -&gt; Int {\n (i - 1) / 2\n }\n\n /* 레벨 순서 순회 */\n func levelOrder() -&gt; [Int] {\n var res: [Int] = []\n // 배열을 직접 순회한다\n for i in 0 ..&lt; size() {\n if let val = val(i: i) {\n res.append(val)\n }\n }\n return res\n }\n\n /* 깊이 우선 순회 */\n private func dfs(i: Int, order: String, res: inout [Int]) {\n // 빈칸이면 반환한다\n guard let val = val(i: i) else {\n return\n }\n // 전위 순회\n if order == \"pre\" {\n res.append(val)\n }\n dfs(i: left(i: i), order: order, res: &amp;res)\n // 중위 순회\n if order == \"in\" {\n res.append(val)\n }\n dfs(i: right(i: i), order: order, res: &amp;res)\n // 후위 순회\n if order == \"post\" {\n res.append(val)\n }\n }\n\n /* 전위 순회 */\n func preOrder() -&gt; [Int] {\n var res: [Int] = []\n dfs(i: 0, order: \"pre\", res: &amp;res)\n return res\n }\n\n /* 중위 순회 */\n func inOrder() -&gt; [Int] {\n var res: [Int] = []\n dfs(i: 0, order: \"in\", res: &amp;res)\n return res\n }\n\n /* 후위 순회 */\n func postOrder() -&gt; [Int] {\n var res: [Int] = []\n dfs(i: 0, order: \"post\", res: &amp;res)\n return res\n }\n}\n</code></pre> array_binary_tree.js<pre><code>/* 배열로 표현한 이진 트리 클래스 */\nclass ArrayBinaryTree {\n #tree;\n\n /* 생성자 */\n constructor(arr) {\n this.#tree = arr;\n }\n\n /* 리스트 용량 */\n size() {\n return this.#tree.length;\n }\n\n /* 인덱스가 i인 노드의 값 반환 */\n val(i) {\n // 인덱스가 범위를 벗어나면 빈칸을 나타내는 null을 반환한다\n if (i &lt; 0 || i &gt;= this.size()) return null;\n return this.#tree[i];\n }\n\n /* 인덱스가 i인 노드의 왼쪽 자식 노드 인덱스 반환 */\n left(i) {\n return 2 * i + 1;\n }\n\n /* 인덱스가 i인 노드의 오른쪽 자식 노드 인덱스 반환 */\n right(i) {\n return 2 * i + 2;\n }\n\n /* 인덱스가 i인 노드의 부모 노드 인덱스 반환 */\n parent(i) {\n return Math.floor((i - 1) / 2); // 나눗셈의 결과를 내림한다\n }\n\n /* 레벨 순서 순회 */\n levelOrder() {\n let res = [];\n // 배열을 직접 순회한다\n for (let i = 0; i &lt; this.size(); i++) {\n if (this.val(i) !== null) res.push(this.val(i));\n }\n return res;\n }\n\n /* 깊이 우선 순회 */\n #dfs(i, order, res) {\n // 빈칸이면 반환한다\n if (this.val(i) === null) return;\n // 전위 순회\n if (order === 'pre') res.push(this.val(i));\n this.#dfs(this.left(i), order, res);\n // 중위 순회\n if (order === 'in') res.push(this.val(i));\n this.#dfs(this.right(i), order, res);\n // 후위 순회\n if (order === 'post') res.push(this.val(i));\n }\n\n /* 전위 순회 */\n preOrder() {\n const res = [];\n this.#dfs(0, 'pre', res);\n return res;\n }\n\n /* 중위 순회 */\n inOrder() {\n const res = [];\n this.#dfs(0, 'in', res);\n return res;\n }\n\n /* 후위 순회 */\n postOrder() {\n const res = [];\n this.#dfs(0, 'post', res);\n return res;\n }\n}\n</code></pre> array_binary_tree.ts<pre><code>/* 배열로 표현한 이진 트리 클래스 */\nclass ArrayBinaryTree {\n #tree: (number | null)[];\n\n /* 생성자 */\n constructor(arr: (number | null)[]) {\n this.#tree = arr;\n }\n\n /* 리스트 용량 */\n size(): number {\n return this.#tree.length;\n }\n\n /* 인덱스가 i인 노드의 값 반환 */\n val(i: number): number | null {\n // 인덱스가 범위를 벗어나면 빈칸을 나타내는 null을 반환한다\n if (i &lt; 0 || i &gt;= this.size()) return null;\n return this.#tree[i];\n }\n\n /* 인덱스가 i인 노드의 왼쪽 자식 노드 인덱스 반환 */\n left(i: number): number {\n return 2 * i + 1;\n }\n\n /* 인덱스가 i인 노드의 오른쪽 자식 노드 인덱스 반환 */\n right(i: number): number {\n return 2 * i + 2;\n }\n\n /* 인덱스가 i인 노드의 부모 노드 인덱스 반환 */\n parent(i: number): number {\n return Math.floor((i - 1) / 2); // 나눗셈의 결과를 내림한다\n }\n\n /* 레벨 순서 순회 */\n levelOrder(): number[] {\n let res = [];\n // 배열을 직접 순회한다\n for (let i = 0; i &lt; this.size(); i++) {\n if (this.val(i) !== null) res.push(this.val(i));\n }\n return res;\n }\n\n /* 깊이 우선 순회 */\n #dfs(i: number, order: Order, res: (number | null)[]): void {\n // 빈칸이면 반환한다\n if (this.val(i) === null) return;\n // 전위 순회\n if (order === 'pre') res.push(this.val(i));\n this.#dfs(this.left(i), order, res);\n // 중위 순회\n if (order === 'in') res.push(this.val(i));\n this.#dfs(this.right(i), order, res);\n // 후위 순회\n if (order === 'post') res.push(this.val(i));\n }\n\n /* 전위 순회 */\n preOrder(): (number | null)[] {\n const res = [];\n this.#dfs(0, 'pre', res);\n return res;\n }\n\n /* 중위 순회 */\n inOrder(): (number | null)[] {\n const res = [];\n this.#dfs(0, 'in', res);\n return res;\n }\n\n /* 후위 순회 */\n postOrder(): (number | null)[] {\n const res = [];\n this.#dfs(0, 'post', res);\n return res;\n }\n}\n</code></pre> array_binary_tree.dart<pre><code>/* 배열로 표현한 이진 트리 클래스 */\nclass ArrayBinaryTree {\n late List&lt;int?&gt; _tree;\n\n /* 생성자 */\n ArrayBinaryTree(this._tree);\n\n /* 리스트 용량 */\n int size() {\n return _tree.length;\n }\n\n /* 인덱스가 i인 노드의 값 반환 */\n int? val(int i) {\n // 인덱스가 범위를 벗어나면 빈칸을 나타내는 null을 반환한다\n if (i &lt; 0 || i &gt;= size()) {\n return null;\n }\n return _tree[i];\n }\n\n /* 인덱스가 i인 노드의 왼쪽 자식 노드 인덱스 반환 */\n int? left(int i) {\n return 2 * i + 1;\n }\n\n /* 인덱스가 i인 노드의 오른쪽 자식 노드 인덱스 반환 */\n int? right(int i) {\n return 2 * i + 2;\n }\n\n /* 인덱스가 i인 노드의 부모 노드 인덱스 반환 */\n int? parent(int i) {\n return (i - 1) ~/ 2;\n }\n\n /* 레벨 순서 순회 */\n List&lt;int&gt; levelOrder() {\n List&lt;int&gt; res = [];\n for (int i = 0; i &lt; size(); i++) {\n if (val(i) != null) {\n res.add(val(i)!);\n }\n }\n return res;\n }\n\n /* 깊이 우선 순회 */\n void dfs(int i, String order, List&lt;int?&gt; res) {\n // 빈칸이면 반환한다\n if (val(i) == null) {\n return;\n }\n // 전위 순회\n if (order == 'pre') {\n res.add(val(i));\n }\n dfs(left(i)!, order, res);\n // 중위 순회\n if (order == 'in') {\n res.add(val(i));\n }\n dfs(right(i)!, order, res);\n // 후위 순회\n if (order == 'post') {\n res.add(val(i));\n }\n }\n\n /* 전위 순회 */\n List&lt;int?&gt; preOrder() {\n List&lt;int?&gt; res = [];\n dfs(0, 'pre', res);\n return res;\n }\n\n /* 중위 순회 */\n List&lt;int?&gt; inOrder() {\n List&lt;int?&gt; res = [];\n dfs(0, 'in', res);\n return res;\n }\n\n /* 후위 순회 */\n List&lt;int?&gt; postOrder() {\n List&lt;int?&gt; res = [];\n dfs(0, 'post', res);\n return res;\n }\n}\n</code></pre> array_binary_tree.rs<pre><code>/* 배열로 표현한 이진 트리 클래스 */\nstruct ArrayBinaryTree {\n tree: Vec&lt;Option&lt;i32&gt;&gt;,\n}\n\nimpl ArrayBinaryTree {\n /* 생성자 */\n fn new(arr: Vec&lt;Option&lt;i32&gt;&gt;) -&gt; Self {\n Self { tree: arr }\n }\n\n /* 리스트 용량 */\n fn size(&amp;self) -&gt; i32 {\n self.tree.len() as i32\n }\n\n /* 인덱스가 i인 노드의 값 반환 */\n fn val(&amp;self, i: i32) -&gt; Option&lt;i32&gt; {\n // 인덱스가 범위를 벗어나면 빈칸을 나타내는 None을 반환한다\n if i &lt; 0 || i &gt;= self.size() {\n None\n } else {\n self.tree[i as usize]\n }\n }\n\n /* 인덱스가 i인 노드의 왼쪽 자식 노드 인덱스 반환 */\n fn left(&amp;self, i: i32) -&gt; i32 {\n 2 * i + 1\n }\n\n /* 인덱스가 i인 노드의 오른쪽 자식 노드 인덱스 반환 */\n fn right(&amp;self, i: i32) -&gt; i32 {\n 2 * i + 2\n }\n\n /* 인덱스가 i인 노드의 부모 노드 인덱스 반환 */\n fn parent(&amp;self, i: i32) -&gt; i32 {\n (i - 1) / 2\n }\n\n /* 레벨 순서 순회 */\n fn level_order(&amp;self) -&gt; Vec&lt;i32&gt; {\n self.tree.iter().filter_map(|&amp;x| x).collect()\n }\n\n /* 깊이 우선 순회 */\n fn dfs(&amp;self, i: i32, order: &amp;'static str, res: &amp;mut Vec&lt;i32&gt;) {\n if self.val(i).is_none() {\n return;\n }\n let val = self.val(i).unwrap();\n // 전위 순회\n if order == \"pre\" {\n res.push(val);\n }\n self.dfs(self.left(i), order, res);\n // 중위 순회\n if order == \"in\" {\n res.push(val);\n }\n self.dfs(self.right(i), order, res);\n // 후위 순회\n if order == \"post\" {\n res.push(val);\n }\n }\n\n /* 전위 순회 */\n fn pre_order(&amp;self) -&gt; Vec&lt;i32&gt; {\n let mut res = vec![];\n self.dfs(0, \"pre\", &amp;mut res);\n res\n }\n\n /* 중위 순회 */\n fn in_order(&amp;self) -&gt; Vec&lt;i32&gt; {\n let mut res = vec![];\n self.dfs(0, \"in\", &amp;mut res);\n res\n }\n\n /* 후위 순회 */\n fn post_order(&amp;self) -&gt; Vec&lt;i32&gt; {\n let mut res = vec![];\n self.dfs(0, \"post\", &amp;mut res);\n res\n }\n}\n</code></pre> array_binary_tree.c<pre><code>/* 배열로 표현한 이진 트리 구조체 */\ntypedef struct {\n int *tree;\n int size;\n} ArrayBinaryTree;\n\n/* 생성자 */\nArrayBinaryTree *newArrayBinaryTree(int *arr, int arrSize) {\n ArrayBinaryTree *abt = (ArrayBinaryTree *)malloc(sizeof(ArrayBinaryTree));\n abt-&gt;tree = malloc(sizeof(int) * arrSize);\n memcpy(abt-&gt;tree, arr, sizeof(int) * arrSize);\n abt-&gt;size = arrSize;\n return abt;\n}\n\n/* 소멸자 */\nvoid delArrayBinaryTree(ArrayBinaryTree *abt) {\n free(abt-&gt;tree);\n free(abt);\n}\n\n/* 리스트 용량 */\nint size(ArrayBinaryTree *abt) {\n return abt-&gt;size;\n}\n\n/* 인덱스가 i인 노드의 값 반환 */\nint val(ArrayBinaryTree *abt, int i) {\n // 인덱스가 범위를 벗어나면 빈자리를 나타내는 INT_MAX를 반환한다\n if (i &lt; 0 || i &gt;= size(abt))\n return INT_MAX;\n return abt-&gt;tree[i];\n}\n\n/* 레벨 순서 순회 */\nint *levelOrder(ArrayBinaryTree *abt, int *returnSize) {\n int *res = (int *)malloc(sizeof(int) * size(abt));\n int index = 0;\n // 배열을 직접 순회한다\n for (int i = 0; i &lt; size(abt); i++) {\n if (val(abt, i) != INT_MAX)\n res[index++] = val(abt, i);\n }\n *returnSize = index;\n return res;\n}\n\n/* 깊이 우선 순회 */\nvoid dfs(ArrayBinaryTree *abt, int i, char *order, int *res, int *index) {\n // 빈칸이면 반환한다\n if (val(abt, i) == INT_MAX)\n return;\n // 전위 순회\n if (strcmp(order, \"pre\") == 0)\n res[(*index)++] = val(abt, i);\n dfs(abt, left(i), order, res, index);\n // 중위 순회\n if (strcmp(order, \"in\") == 0)\n res[(*index)++] = val(abt, i);\n dfs(abt, right(i), order, res, index);\n // 후위 순회\n if (strcmp(order, \"post\") == 0)\n res[(*index)++] = val(abt, i);\n}\n\n/* 전위 순회 */\nint *preOrder(ArrayBinaryTree *abt, int *returnSize) {\n int *res = (int *)malloc(sizeof(int) * size(abt));\n int index = 0;\n dfs(abt, 0, \"pre\", res, &amp;index);\n *returnSize = index;\n return res;\n}\n\n/* 중위 순회 */\nint *inOrder(ArrayBinaryTree *abt, int *returnSize) {\n int *res = (int *)malloc(sizeof(int) * size(abt));\n int index = 0;\n dfs(abt, 0, \"in\", res, &amp;index);\n *returnSize = index;\n return res;\n}\n\n/* 후위 순회 */\nint *postOrder(ArrayBinaryTree *abt, int *returnSize) {\n int *res = (int *)malloc(sizeof(int) * size(abt));\n int index = 0;\n dfs(abt, 0, \"post\", res, &amp;index);\n *returnSize = index;\n return res;\n}\n</code></pre> array_binary_tree.kt<pre><code>/* 배열로 표현한 이진 트리 클래스 */\nclass ArrayBinaryTree(val tree: MutableList&lt;Int?&gt;) {\n /* 리스트 용량 */\n fun size(): Int {\n return tree.size\n }\n\n /* 인덱스가 i인 노드의 값 반환 */\n fun _val(i: Int): Int? {\n // 인덱스가 범위를 벗어나면 빈칸을 나타내는 null을 반환한다\n if (i &lt; 0 || i &gt;= size()) return null\n return tree[i]\n }\n\n /* 인덱스가 i인 노드의 왼쪽 자식 노드 인덱스 반환 */\n fun left(i: Int): Int {\n return 2 * i + 1\n }\n\n /* 인덱스가 i인 노드의 오른쪽 자식 노드 인덱스 반환 */\n fun right(i: Int): Int {\n return 2 * i + 2\n }\n\n /* 인덱스가 i인 노드의 부모 노드 인덱스 반환 */\n fun parent(i: Int): Int {\n return (i - 1) / 2\n }\n\n /* 레벨 순서 순회 */\n fun levelOrder(): MutableList&lt;Int?&gt; {\n val res = mutableListOf&lt;Int?&gt;()\n // 배열을 직접 순회한다\n for (i in 0..&lt;size()) {\n if (_val(i) != null)\n res.add(_val(i))\n }\n return res\n }\n\n /* 깊이 우선 순회 */\n fun dfs(i: Int, order: String, res: MutableList&lt;Int?&gt;) {\n // 빈칸이면 반환한다\n if (_val(i) == null)\n return\n // 전위 순회\n if (\"pre\" == order)\n res.add(_val(i))\n dfs(left(i), order, res)\n // 중위 순회\n if (\"in\" == order)\n res.add(_val(i))\n dfs(right(i), order, res)\n // 후위 순회\n if (\"post\" == order)\n res.add(_val(i))\n }\n\n /* 전위 순회 */\n fun preOrder(): MutableList&lt;Int?&gt; {\n val res = mutableListOf&lt;Int?&gt;()\n dfs(0, \"pre\", res)\n return res\n }\n\n /* 중위 순회 */\n fun inOrder(): MutableList&lt;Int?&gt; {\n val res = mutableListOf&lt;Int?&gt;()\n dfs(0, \"in\", res)\n return res\n }\n\n /* 후위 순회 */\n fun postOrder(): MutableList&lt;Int?&gt; {\n val res = mutableListOf&lt;Int?&gt;()\n dfs(0, \"post\", res)\n return res\n }\n}\n</code></pre> array_binary_tree.rb<pre><code>### 배열로 표현한 이진 트리 클래스 ###\nclass ArrayBinaryTree\n ### 생성자 ###\n def initialize(arr)\n @tree = arr.to_a\n end\n\n ### 리스트 용량 ###\n def size\n @tree.length\n end\n\n ### 인덱스가 i인 노드의 값 반환 ###\n def val(i)\n # 인덱스가 범위를 벗어나면 빈칸을 나타내는 nil을 반환한다\n return if i &lt; 0 || i &gt;= size\n\n @tree[i]\n end\n\n ### 인덱스가 i인 노드의 왼쪽 자식 노드 인덱스 반환 ###\n def left(i)\n 2 * i + 1\n end\n\n ### 인덱스가 i인 노드의 오른쪽 자식 노드 인덱스 반환 ###\n def right(i)\n 2 * i + 2\n end\n\n ### 인덱스가 i인 노드의 부모 노드 인덱스 반환 ###\n def parent(i)\n (i - 1) / 2\n end\n\n ### 레벨 순서 순회 ###\n def level_order\n @res = []\n\n # 배열을 직접 순회한다\n for i in 0...size\n @res &lt;&lt; val(i) unless val(i).nil?\n end\n\n @res\n end\n\n ### 깊이 우선 순회 ###\n def dfs(i, order)\n return if val(i).nil?\n # 전위 순회\n @res &lt;&lt; val(i) if order == :pre\n dfs(left(i), order)\n # 중위 순회\n @res &lt;&lt; val(i) if order == :in\n dfs(right(i), order)\n # 후위 순회\n @res &lt;&lt; val(i) if order == :post\n end\n\n ### 전위 순회 ###\n def pre_order\n @res = []\n dfs(0, :pre)\n @res\n end\n\n ### 중위 순회 ###\n def in_order\n @res = []\n dfs(0, :in)\n @res\n end\n\n ### 후위 순회 ###\n def post_order\n @res = []\n dfs(0, :post)\n @res\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 7 장   트리","7.3   이진 트리의 배열 표현"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#733","level":2,"title":"7.3.3   장점과 한계","text":"<p>이진 트리의 배열 표현에는 다음 장점이 있다.</p> <ul> <li>배열은 연속된 메모리에 저장되어 캐시 친화적이며, 접근과 순회가 빠르다.</li> <li>포인터를 저장할 필요가 없어 공간을 절약한다.</li> <li>노드에 임의 접근할 수 있다.</li> </ul> <p>하지만 다음과 같은 한계도 있다.</p> <ul> <li>연속된 메모리가 필요하므로 데이터가 지나치게 많은 트리를 저장하기에는 적합하지 않다.</li> <li>노드를 추가하거나 삭제하려면 배열의 삽입과 삭제 연산을 수행해야 하므로 효율이 낮다.</li> <li>트리에 <code>None</code>이 많으면 배열에서 실제 노드 데이터가 차지하는 비율이 낮아 공간 활용률이 떨어진다.</li> </ul>","path":["제 7 장   트리","7.3   이진 트리의 배열 표현"],"tags":[]},{"location":"chapter_tree/avl_tree/","level":1,"title":"7.5   AVL 트리 *","text":"<p>'이진 탐색 트리' 절에서 삽입과 삭제를 여러 번 수행하면 트리가 연결 리스트로 퇴화할 수 있다고 설명했다. 이 경우 모든 연산의 시간 복잡도가 \\(O(\\log n)\\)에서 \\(O(n)\\)으로 저하된다.</p> <p>아래 그림(7-24)의 이진 탐색 트리는 노드를 두 번 삭제하면 연결 리스트로 퇴화한다.</p> <p></p> <p> 그림 7-24   노드 삭제 후 퇴화한 AVL 트리 </p> <p>또한 아래 그림(7-25)의 포화 이진 트리에 노드 두 개를 삽입하면 왼쪽으로 심하게 기울어 탐색의 시간 복잡도도 저하된다.</p> <p></p> <p> 그림 7-25   노드 삽입 후 퇴화한 AVL 트리 </p> <p>1962년 G. M. Adelson-Velsky와 E. M. Landis는 논문 'An algorithm for the organization of information'에서 AVL 트리를 제안했다. 논문은 노드를 계속 추가하거나 삭제해도 트리가 퇴화하지 않게 하는 일련의 연산을 상세히 설명하여, 각 연산의 시간 복잡도를 \\(O(\\log n)\\)으로 유지하도록 했다. 즉, 추가, 삭제, 조회, 수정이 잦은 상황에서도 효율적인 데이터 연산 성능을 유지하므로 활용 가치가 높다.</p>","path":["제 7 장   트리","7.5   AVL 트리 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#751-avl","level":2,"title":"7.5.1   AVL 트리의 주요 용어","text":"<p>AVL 트리는 이진 탐색 트리이면서 균형 이진 트리다. 두 종류의 모든 성질을 함께 만족하므로 균형 이진 탐색 트리(balanced binary search tree)다.</p>","path":["제 7 장   트리","7.5   AVL 트리 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1","level":3,"title":"1.   노드 높이","text":"<p>AVL 트리 연산에는 노드 높이가 필요하므로 노드 클래스에 <code>height</code> 변수를 추가해야 한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby <pre><code>class TreeNode:\n \"\"\"AVL 트리 노드 클래스\"\"\"\n def __init__(self, val: int):\n self.val: int = val # 노드 값\n self.height: int = 0 # 노드 높이\n self.left: TreeNode | None = None # 왼쪽 자식 노드 참조\n self.right: TreeNode | None = None # 오른쪽 자식 노드 참조\n</code></pre> <pre><code>/* AVL 트리 노드 클래스 */\nstruct TreeNode {\n int val{}; // 노드 값\n int height = 0; // 노드 높이\n TreeNode *left{}; // 왼쪽 자식 노드\n TreeNode *right{}; // 오른쪽 자식 노드\n TreeNode() = default;\n explicit TreeNode(int x) : val(x){}\n};\n</code></pre> <pre><code>/* AVL 트리 노드 클래스 */\nclass TreeNode {\n public int val; // 노드 값\n public int height; // 노드 높이\n public TreeNode left; // 왼쪽 자식 노드\n public TreeNode right; // 오른쪽 자식 노드\n public TreeNode(int x) { val = x; }\n}\n</code></pre> <pre><code>/* AVL 트리 노드 클래스 */\nclass TreeNode(int? x) {\n public int? val = x; // 노드 값\n public int height; // 노드 높이\n public TreeNode? left; // 왼쪽 자식 노드 참조\n public TreeNode? right; // 오른쪽 자식 노드 참조\n}\n</code></pre> <pre><code>/* AVL 트리 노드 구조체 */\ntype TreeNode struct {\n Val int // 노드 값\n Height int // 노드 높이\n Left *TreeNode // 왼쪽 자식 노드 참조\n Right *TreeNode // 오른쪽 자식 노드 참조\n}\n</code></pre> <pre><code>/* AVL 트리 노드 클래스 */\nclass TreeNode {\n var val: Int // 노드 값\n var height: Int // 노드 높이\n var left: TreeNode? // 왼쪽 자식 노드\n var right: TreeNode? // 오른쪽 자식 노드\n\n init(x: Int) {\n val = x\n height = 0\n }\n}\n</code></pre> <pre><code>/* AVL 트리 노드 클래스 */\nclass TreeNode {\n val; // 노드 값\n height; //노드 높이\n left; // 왼쪽 자식 노드 포인터\n right; // 오른쪽 자식 노드 포인터\n constructor(val, left, right, height) {\n this.val = val === undefined ? 0 : val;\n this.height = height === undefined ? 0 : height;\n this.left = left === undefined ? null : left;\n this.right = right === undefined ? null : right;\n }\n}\n</code></pre> <pre><code>/* AVL 트리 노드 클래스 */\nclass TreeNode {\n val: number; // 노드 값\n height: number; // 노드 높이\n left: TreeNode | null; // 왼쪽 자식 노드 포인터\n right: TreeNode | null; // 오른쪽 자식 노드 포인터\n constructor(val?: number, height?: number, left?: TreeNode | null, right?: TreeNode | null) {\n this.val = val === undefined ? 0 : val;\n this.height = height === undefined ? 0 : height;\n this.left = left === undefined ? null : left;\n this.right = right === undefined ? null : right;\n }\n}\n</code></pre> <pre><code>/* AVL 트리 노드 클래스 */\nclass TreeNode {\n int val; // 노드 값\n int height; // 노드 높이\n TreeNode? left; // 왼쪽 자식 노드\n TreeNode? right; // 오른쪽 자식 노드\n TreeNode(this.val, [this.height = 0, this.left, this.right]);\n}\n</code></pre> <pre><code>use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* AVL 트리 노드 구조체 */\nstruct TreeNode {\n val: i32, // 노드 값\n height: i32, // 노드 높이\n left: Option&lt;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;, // 왼쪽 자식 노드\n right: Option&lt;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;, // 오른쪽 자식 노드\n}\n\nimpl TreeNode {\n /* 생성자 */\n fn new(val: i32) -&gt; Rc&lt;RefCell&lt;Self&gt;&gt; {\n Rc::new(RefCell::new(Self {\n val,\n height: 0,\n left: None,\n right: None\n }))\n }\n}\n</code></pre> <pre><code>/* AVL 트리 노드 구조체 */\ntypedef struct TreeNode {\n int val;\n int height;\n struct TreeNode *left;\n struct TreeNode *right;\n} TreeNode;\n\n/* 생성자 */\nTreeNode *newTreeNode(int val) {\n TreeNode *node;\n\n node = (TreeNode *)malloc(sizeof(TreeNode));\n node-&gt;val = val;\n node-&gt;height = 0;\n node-&gt;left = NULL;\n node-&gt;right = NULL;\n return node;\n}\n</code></pre> <pre><code>/* AVL 트리 노드 클래스 */\nclass TreeNode(val _val: Int) { // 노드 값\n var height: Int = 0 // 노드 높이\n var left: TreeNode? = null // 왼쪽 자식 노드\n var right: TreeNode? = null // 오른쪽 자식 노드\n}\n</code></pre> <pre><code>### AVL 트리 노드 클래스 ###\nclass TreeNode\n attr_accessor :val # 노드 값\n attr_accessor :height # 노드 높이\n attr_accessor :left # 왼쪽 자식 노드 참조\n attr_accessor :right # 오른쪽 자식 노드 참조\n\n def initialize(val)\n @val = val\n @height = 0\n end\nend\n</code></pre> <p>'노드 높이'는 해당 노드에서 가장 먼 리프까지의 거리, 즉 거치는 '간선' 수다. 특히 리프의 높이는 \\(0\\), 빈 노드의 높이는 \\(-1\\)임에 주의하자. 높이를 조회하고 갱신하는 두 보조 함수를 만든다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py<pre><code>def height(self, node: TreeNode | None) -&gt; int:\n \"\"\"노드 높이 반환\"\"\"\n # 빈 노드의 높이는 -1이고, 리프 노드의 높이는 0이다\n if node is not None:\n return node.height\n return -1\n\ndef update_height(self, node: TreeNode | None):\n \"\"\"노드 높이를 갱신한다\"\"\"\n # 노드 높이는 가장 높은 서브트리의 높이 + 1이다\n node.height = max([self.height(node.left), self.height(node.right)]) + 1\n</code></pre> avl_tree.cpp<pre><code>/* 노드 높이 반환 */\nint height(TreeNode *node) {\n // 빈 노드의 높이는 -1이고, 리프 노드의 높이는 0이다\n return node == nullptr ? -1 : node-&gt;height;\n}\n\n/* 노드 높이를 갱신한다 */\nvoid updateHeight(TreeNode *node) {\n // 노드 높이는 가장 높은 서브트리의 높이 + 1이다\n node-&gt;height = max(height(node-&gt;left), height(node-&gt;right)) + 1;\n}\n</code></pre> avl_tree.java<pre><code>/* 노드 높이 반환 */\nint height(TreeNode node) {\n // 빈 노드의 높이는 -1이고, 리프 노드의 높이는 0이다\n return node == null ? -1 : node.height;\n}\n\n/* 노드 높이를 갱신한다 */\nvoid updateHeight(TreeNode node) {\n // 노드 높이는 가장 높은 서브트리의 높이 + 1이다\n node.height = Math.max(height(node.left), height(node.right)) + 1;\n}\n</code></pre> avl_tree.cs<pre><code>/* 노드 높이 반환 */\nint Height(TreeNode? node) {\n // 빈 노드의 높이는 -1이고, 리프 노드의 높이는 0이다\n return node == null ? -1 : node.height;\n}\n\n/* 노드 높이를 갱신한다 */\nvoid UpdateHeight(TreeNode node) {\n // 노드 높이는 가장 높은 서브트리의 높이 + 1이다\n node.height = Math.Max(Height(node.left), Height(node.right)) + 1;\n}\n</code></pre> avl_tree.go<pre><code>/* 노드 높이 반환 */\nfunc (t *aVLTree) height(node *TreeNode) int {\n // 빈 노드의 높이는 -1이고, 리프 노드의 높이는 0이다\n if node != nil {\n return node.Height\n }\n return -1\n}\n\n/* 노드 높이를 갱신한다 */\nfunc (t *aVLTree) updateHeight(node *TreeNode) {\n lh := t.height(node.Left)\n rh := t.height(node.Right)\n // 노드 높이는 가장 높은 서브트리의 높이 + 1이다\n if lh &gt; rh {\n node.Height = lh + 1\n } else {\n node.Height = rh + 1\n }\n}\n</code></pre> avl_tree.swift<pre><code>/* 노드 높이 반환 */\nfunc height(node: TreeNode?) -&gt; Int {\n // 빈 노드의 높이는 -1이고, 리프 노드의 높이는 0이다\n node?.height ?? -1\n}\n\n/* 노드 높이를 갱신한다 */\nfunc updateHeight(node: TreeNode?) {\n // 노드 높이는 가장 높은 서브트리의 높이 + 1이다\n node?.height = max(height(node: node?.left), height(node: node?.right)) + 1\n}\n</code></pre> avl_tree.js<pre><code>/* 노드 높이 반환 */\nheight(node) {\n // 빈 노드의 높이는 -1이고, 리프 노드의 높이는 0이다\n return node === null ? -1 : node.height;\n}\n\n/* 노드 높이를 갱신한다 */\n#updateHeight(node) {\n // 노드 높이는 가장 높은 서브트리의 높이 + 1이다\n node.height =\n Math.max(this.height(node.left), this.height(node.right)) + 1;\n}\n</code></pre> avl_tree.ts<pre><code>/* 노드 높이 반환 */\nheight(node: TreeNode): number {\n // 빈 노드의 높이는 -1이고, 리프 노드의 높이는 0이다\n return node === null ? -1 : node.height;\n}\n\n/* 노드 높이를 갱신한다 */\nupdateHeight(node: TreeNode): void {\n // 노드 높이는 가장 높은 서브트리의 높이 + 1이다\n node.height =\n Math.max(this.height(node.left), this.height(node.right)) + 1;\n}\n</code></pre> avl_tree.dart<pre><code>/* 노드 높이 반환 */\nint height(TreeNode? node) {\n // 빈 노드의 높이는 -1이고, 리프 노드의 높이는 0이다\n return node == null ? -1 : node.height;\n}\n\n/* 노드 높이를 갱신한다 */\nvoid updateHeight(TreeNode? node) {\n // 노드 높이는 가장 높은 서브트리의 높이 + 1이다\n node!.height = max(height(node.left), height(node.right)) + 1;\n}\n</code></pre> avl_tree.rs<pre><code>/* 노드 높이 반환 */\nfn height(node: OptionTreeNodeRc) -&gt; i32 {\n // 빈 노드의 높이는 -1이고, 리프 노드의 높이는 0이다\n match node {\n Some(node) =&gt; node.borrow().height,\n None =&gt; -1,\n }\n}\n\n/* 노드 높이를 갱신한다 */\nfn update_height(node: OptionTreeNodeRc) {\n if let Some(node) = node {\n let left = node.borrow().left.clone();\n let right = node.borrow().right.clone();\n // 노드 높이는 가장 높은 서브트리의 높이 + 1이다\n node.borrow_mut().height = std::cmp::max(Self::height(left), Self::height(right)) + 1;\n }\n}\n</code></pre> avl_tree.c<pre><code>/* 노드 높이 반환 */\nint height(TreeNode *node) {\n // 빈 노드의 높이는 -1이고, 리프 노드의 높이는 0이다\n if (node != NULL) {\n return node-&gt;height;\n }\n return -1;\n}\n\n/* 노드 높이를 갱신한다 */\nvoid updateHeight(TreeNode *node) {\n int lh = height(node-&gt;left);\n int rh = height(node-&gt;right);\n // 노드 높이는 가장 높은 서브트리의 높이 + 1이다\n if (lh &gt; rh) {\n node-&gt;height = lh + 1;\n } else {\n node-&gt;height = rh + 1;\n }\n}\n</code></pre> avl_tree.kt<pre><code>/* 노드 높이 반환 */\nfun height(node: TreeNode?): Int {\n // 빈 노드의 높이는 -1이고, 리프 노드의 높이는 0이다\n return node?.height ?: -1\n}\n\n/* 노드 높이를 갱신한다 */\nfun updateHeight(node: TreeNode?) {\n // 노드 높이는 가장 높은 서브트리의 높이 + 1이다\n node?.height = max(height(node?.left), height(node?.right)) + 1\n}\n</code></pre> avl_tree.rb<pre><code>### 노드 높이 반환 ###\ndef height(node)\n # 빈 노드의 높이는 -1이고, 리프 노드의 높이는 0이다\n return node.height unless node.nil?\n\n -1\nend\n\n### 노드 높이를 갱신한다 ###\ndef update_height(node)\n # 노드 높이는 가장 높은 서브트리의 높이 + 1이다\n node.height = [height(node.left), height(node.right)].max + 1\nend\n</code></pre>","path":["제 7 장   트리","7.5   AVL 트리 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2","level":3,"title":"2.   노드의 균형 인수","text":"<p>노드의 균형 인수(balance factor)는 왼쪽 서브트리 높이에서 오른쪽 서브트리 높이를 뺀 값이며, 빈 노드의 균형 인수는 \\(0\\)으로 정한다. 이후 편리하게 쓰도록 균형 인수를 구하는 기능도 함수로 만든다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py<pre><code>def balance_factor(self, node: TreeNode | None) -&gt; int:\n \"\"\"균형 인수 반환\"\"\"\n # 빈 노드의 균형 인수는 0이다\n if node is None:\n return 0\n # 노드의 균형 인수 = 왼쪽 서브트리 높이 - 오른쪽 서브트리 높이\n return self.height(node.left) - self.height(node.right)\n</code></pre> avl_tree.cpp<pre><code>/* 균형 인수 반환 */\nint balanceFactor(TreeNode *node) {\n // 빈 노드의 균형 인수는 0이다\n if (node == nullptr)\n return 0;\n // 노드의 균형 인수 = 왼쪽 서브트리 높이 - 오른쪽 서브트리 높이\n return height(node-&gt;left) - height(node-&gt;right);\n}\n</code></pre> avl_tree.java<pre><code>/* 균형 인수 반환 */\nint balanceFactor(TreeNode node) {\n // 빈 노드의 균형 인수는 0이다\n if (node == null)\n return 0;\n // 노드의 균형 인수 = 왼쪽 서브트리 높이 - 오른쪽 서브트리 높이\n return height(node.left) - height(node.right);\n}\n</code></pre> avl_tree.cs<pre><code>/* 균형 인수 반환 */\nint BalanceFactor(TreeNode? node) {\n // 빈 노드의 균형 인수는 0이다\n if (node == null) return 0;\n // 노드의 균형 인수 = 왼쪽 서브트리 높이 - 오른쪽 서브트리 높이\n return Height(node.left) - Height(node.right);\n}\n</code></pre> avl_tree.go<pre><code>/* 균형 인수 반환 */\nfunc (t *aVLTree) balanceFactor(node *TreeNode) int {\n // 빈 노드의 균형 인수는 0이다\n if node == nil {\n return 0\n }\n // 노드의 균형 인수 = 왼쪽 서브트리 높이 - 오른쪽 서브트리 높이\n return t.height(node.Left) - t.height(node.Right)\n}\n</code></pre> avl_tree.swift<pre><code>/* 균형 인수 반환 */\nfunc balanceFactor(node: TreeNode?) -&gt; Int {\n // 빈 노드의 균형 인수는 0이다\n guard let node = node else { return 0 }\n // 노드의 균형 인수 = 왼쪽 서브트리 높이 - 오른쪽 서브트리 높이\n return height(node: node.left) - height(node: node.right)\n}\n</code></pre> avl_tree.js<pre><code>/* 균형 인수 반환 */\nbalanceFactor(node) {\n // 빈 노드의 균형 인수는 0이다\n if (node === null) return 0;\n // 노드의 균형 인수 = 왼쪽 서브트리 높이 - 오른쪽 서브트리 높이\n return this.height(node.left) - this.height(node.right);\n}\n</code></pre> avl_tree.ts<pre><code>/* 균형 인수 반환 */\nbalanceFactor(node: TreeNode): number {\n // 빈 노드의 균형 인수는 0이다\n if (node === null) return 0;\n // 노드의 균형 인수 = 왼쪽 서브트리 높이 - 오른쪽 서브트리 높이\n return this.height(node.left) - this.height(node.right);\n}\n</code></pre> avl_tree.dart<pre><code>/* 균형 인수 반환 */\nint balanceFactor(TreeNode? node) {\n // 빈 노드의 균형 인수는 0이다\n if (node == null) return 0;\n // 노드의 균형 인수 = 왼쪽 서브트리 높이 - 오른쪽 서브트리 높이\n return height(node.left) - height(node.right);\n}\n</code></pre> avl_tree.rs<pre><code>/* 균형 인수 반환 */\nfn balance_factor(node: OptionTreeNodeRc) -&gt; i32 {\n match node {\n // 빈 노드의 균형 인수는 0이다\n None =&gt; 0,\n // 노드의 균형 인수 = 왼쪽 서브트리 높이 - 오른쪽 서브트리 높이\n Some(node) =&gt; {\n Self::height(node.borrow().left.clone()) - Self::height(node.borrow().right.clone())\n }\n }\n}\n</code></pre> avl_tree.c<pre><code>/* 균형 인수 반환 */\nint balanceFactor(TreeNode *node) {\n // 빈 노드의 균형 인수는 0이다\n if (node == NULL) {\n return 0;\n }\n // 노드의 균형 인수 = 왼쪽 서브트리 높이 - 오른쪽 서브트리 높이\n return height(node-&gt;left) - height(node-&gt;right);\n}\n</code></pre> avl_tree.kt<pre><code>/* 균형 인수 반환 */\nfun balanceFactor(node: TreeNode?): Int {\n // 빈 노드의 균형 인수는 0이다\n if (node == null) return 0\n // 노드의 균형 인수 = 왼쪽 서브트리 높이 - 오른쪽 서브트리 높이\n return height(node.left) - height(node.right)\n}\n</code></pre> avl_tree.rb<pre><code>### 균형 인수 반환 ###\ndef balance_factor(node)\n # 빈 노드의 균형 인수는 0이다\n return 0 if node.nil?\n\n # 노드의 균형 인수 = 왼쪽 서브트리 높이 - 오른쪽 서브트리 높이\n height(node.left) - height(node.right)\nend\n</code></pre> <p>참고</p> <p>균형 인수를 \\(f\\)라고 하면 AVL 트리의 모든 노드는 \\(-1 \\le f \\le 1\\)을 만족한다.</p>","path":["제 7 장   트리","7.5   AVL 트리 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#752-avl","level":2,"title":"7.5.2   AVL 트리의 회전","text":"<p>AVL 트리의 특징은 '회전' 연산이다. 이진 트리의 중위 순회 결과를 바꾸지 않으면서 불균형 노드의 균형을 회복한다. 즉, 회전은 '이진 탐색 트리'의 성질을 유지하면서 트리를 다시 '균형 이진 트리'로 만든다.</p> <p>균형 인수의 절댓값이 \\(&gt; 1\\)인 노드를 '불균형 노드'라고 한다. 불균형 상황에 따라 오른쪽 회전, 왼쪽 회전, 오른쪽 후 왼쪽 회전, 왼쪽 후 오른쪽 회전의 네 종류로 나뉜다. 각각 자세히 알아보자.</p>","path":["제 7 장   트리","7.5   AVL 트리 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1_1","level":3,"title":"1.   오른쪽 회전","text":"<p>아래 그림(7-26)에서 노드 아래의 수는 균형 인수다. 아래에서 위로 볼 때 첫 불균형 노드는 '노드 3'이다. 이 노드를 루트로 하는 서브트리에 주목하여 불균형 노드를 <code>node</code>, 왼쪽 자식을 <code>child</code>라고 하고 '오른쪽 회전'을 수행한다. 회전 후 서브트리는 균형을 되찾으며 이진 탐색 트리의 성질도 유지한다.</p> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt; <p></p> <p></p> <p></p> <p></p> <p> 그림 7-26   오른쪽 회전 단계 </p> <p>아래 그림(7-27)처럼 <code>child</code>에 오른쪽 자식 <code>grand_child</code>가 있으면, 오른쪽 회전 중 <code>grand_child</code>를 <code>node</code>의 왼쪽 자식으로 연결하는 단계를 추가해야 한다.</p> <p></p> <p> 그림 7-27   grand_child가 있는 오른쪽 회전 </p> <p>'오른쪽으로 회전한다'는 시각적인 표현이며, 실제로는 노드 포인터를 수정하여 구현한다. 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py<pre><code>def right_rotate(self, node: TreeNode | None) -&gt; TreeNode | None:\n \"\"\"오른쪽 회전 연산\"\"\"\n child = node.left\n grand_child = child.right\n # child를 중심으로 node를 오른쪽으로 회전한다\n child.right = node\n node.left = grand_child\n # 노드 높이를 갱신한다\n self.update_height(node)\n self.update_height(child)\n # 회전한 서브트리의 루트 노드를 반환한다\n return child\n</code></pre> avl_tree.cpp<pre><code>/* 오른쪽 회전 연산 */\nTreeNode *rightRotate(TreeNode *node) {\n TreeNode *child = node-&gt;left;\n TreeNode *grandChild = child-&gt;right;\n // child를 중심으로 node를 오른쪽으로 회전한다\n child-&gt;right = node;\n node-&gt;left = grandChild;\n // 노드 높이를 갱신한다\n updateHeight(node);\n updateHeight(child);\n // 회전한 서브트리의 루트 노드를 반환한다\n return child;\n}\n</code></pre> avl_tree.java<pre><code>/* 오른쪽 회전 연산 */\nTreeNode rightRotate(TreeNode node) {\n TreeNode child = node.left;\n TreeNode grandChild = child.right;\n // child를 중심으로 node를 오른쪽으로 회전한다\n child.right = node;\n node.left = grandChild;\n // 노드 높이를 갱신한다\n updateHeight(node);\n updateHeight(child);\n // 회전한 서브트리의 루트 노드를 반환한다\n return child;\n}\n</code></pre> avl_tree.cs<pre><code>/* 오른쪽 회전 연산 */\nTreeNode? RightRotate(TreeNode? node) {\n TreeNode? child = node?.left;\n TreeNode? grandChild = child?.right;\n // child를 중심으로 node를 오른쪽으로 회전한다\n child.right = node;\n node.left = grandChild;\n // 노드 높이를 갱신한다\n UpdateHeight(node);\n UpdateHeight(child);\n // 회전한 서브트리의 루트 노드를 반환한다\n return child;\n}\n</code></pre> avl_tree.go<pre><code>/* 오른쪽 회전 연산 */\nfunc (t *aVLTree) rightRotate(node *TreeNode) *TreeNode {\n child := node.Left\n grandChild := child.Right\n // child를 중심으로 node를 오른쪽으로 회전한다\n child.Right = node\n node.Left = grandChild\n // 노드 높이를 갱신한다\n t.updateHeight(node)\n t.updateHeight(child)\n // 회전한 서브트리의 루트 노드를 반환한다\n return child\n}\n</code></pre> avl_tree.swift<pre><code>/* 오른쪽 회전 연산 */\nfunc rightRotate(node: TreeNode?) -&gt; TreeNode? {\n let child = node?.left\n let grandChild = child?.right\n // child를 중심으로 node를 오른쪽으로 회전한다\n child?.right = node\n node?.left = grandChild\n // 노드 높이를 갱신한다\n updateHeight(node: node)\n updateHeight(node: child)\n // 회전한 서브트리의 루트 노드를 반환한다\n return child\n}\n</code></pre> avl_tree.js<pre><code>/* 오른쪽 회전 연산 */\n#rightRotate(node) {\n const child = node.left;\n const grandChild = child.right;\n // child를 중심으로 node를 오른쪽으로 회전한다\n child.right = node;\n node.left = grandChild;\n // 노드 높이를 갱신한다\n this.#updateHeight(node);\n this.#updateHeight(child);\n // 회전한 서브트리의 루트 노드를 반환한다\n return child;\n}\n</code></pre> avl_tree.ts<pre><code>/* 오른쪽 회전 연산 */\nrightRotate(node: TreeNode): TreeNode {\n const child = node.left;\n const grandChild = child.right;\n // child를 중심으로 node를 오른쪽으로 회전한다\n child.right = node;\n node.left = grandChild;\n // 노드 높이를 갱신한다\n this.updateHeight(node);\n this.updateHeight(child);\n // 회전한 서브트리의 루트 노드를 반환한다\n return child;\n}\n</code></pre> avl_tree.dart<pre><code>/* 오른쪽 회전 연산 */\nTreeNode? rightRotate(TreeNode? node) {\n TreeNode? child = node!.left;\n TreeNode? grandChild = child!.right;\n // child를 중심으로 node를 오른쪽으로 회전한다\n child.right = node;\n node.left = grandChild;\n // 노드 높이를 갱신한다\n updateHeight(node);\n updateHeight(child);\n // 회전한 서브트리의 루트 노드를 반환한다\n return child;\n}\n</code></pre> avl_tree.rs<pre><code>/* 오른쪽 회전 연산 */\nfn right_rotate(node: OptionTreeNodeRc) -&gt; OptionTreeNodeRc {\n match node {\n Some(node) =&gt; {\n let child = node.borrow().left.clone().unwrap();\n let grand_child = child.borrow().right.clone();\n // child를 중심으로 node를 오른쪽으로 회전한다\n child.borrow_mut().right = Some(node.clone());\n node.borrow_mut().left = grand_child;\n // 노드 높이를 갱신한다\n Self::update_height(Some(node));\n Self::update_height(Some(child.clone()));\n // 회전한 서브트리의 루트 노드를 반환한다\n Some(child)\n }\n None =&gt; None,\n }\n}\n</code></pre> avl_tree.c<pre><code>/* 오른쪽 회전 연산 */\nTreeNode *rightRotate(TreeNode *node) {\n TreeNode *child, *grandChild;\n child = node-&gt;left;\n grandChild = child-&gt;right;\n // child를 중심으로 node를 오른쪽으로 회전한다\n child-&gt;right = node;\n node-&gt;left = grandChild;\n // 노드 높이를 갱신한다\n updateHeight(node);\n updateHeight(child);\n // 회전한 서브트리의 루트 노드를 반환한다\n return child;\n}\n</code></pre> avl_tree.kt<pre><code>/* 오른쪽 회전 연산 */\nfun rightRotate(node: TreeNode?): TreeNode {\n val child = node!!.left\n val grandChild = child!!.right\n // child를 중심으로 node를 오른쪽으로 회전한다\n child.right = node\n node.left = grandChild\n // 노드 높이를 갱신한다\n updateHeight(node)\n updateHeight(child)\n // 회전한 서브트리의 루트 노드를 반환한다\n return child\n}\n</code></pre> avl_tree.rb<pre><code>### 오른쪽 회전 연산 ###\ndef right_rotate(node)\n child = node.left\n grand_child = child.right\n # child를 중심으로 node를 오른쪽으로 회전한다\n child.right = node\n node.left = grand_child\n # 노드 높이를 갱신한다\n update_height(node)\n update_height(child)\n # 회전한 서브트리의 루트 노드를 반환한다\n child\nend\n</code></pre>","path":["제 7 장   트리","7.5   AVL 트리 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2_1","level":3,"title":"2.   왼쪽 회전","text":"<p>앞의 불균형 이진 트리를 '거울에 비춘' 경우에는 아래 그림(7-28)과 같이 '왼쪽 회전'이 필요하다.</p> <p></p> <p> 그림 7-28   왼쪽 회전 </p> <p>마찬가지로 아래 그림(7-29)처럼 <code>child</code>에 왼쪽 자식 <code>grand_child</code>가 있으면, 왼쪽 회전 중 <code>grand_child</code>를 <code>node</code>의 오른쪽 자식으로 연결하는 단계를 추가해야 한다.</p> <p></p> <p> 그림 7-29   grand_child가 있는 왼쪽 회전 </p> <p>오른쪽 회전과 왼쪽 회전은 논리적으로 거울 대칭이며, 각각 해결하는 불균형 상황도 대칭이다. 따라서 오른쪽 회전 코드의 모든 <code>left</code>를 <code>right</code>로, 모든 <code>right</code>를 <code>left</code>로 바꾸면 왼쪽 회전 코드를 얻는다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py<pre><code>def left_rotate(self, node: TreeNode | None) -&gt; TreeNode | None:\n \"\"\"왼쪽 회전 연산\"\"\"\n child = node.right\n grand_child = child.left\n # child를 중심으로 node를 왼쪽으로 회전한다\n child.left = node\n node.right = grand_child\n # 노드 높이를 갱신한다\n self.update_height(node)\n self.update_height(child)\n # 회전한 서브트리의 루트 노드를 반환한다\n return child\n</code></pre> avl_tree.cpp<pre><code>/* 왼쪽 회전 연산 */\nTreeNode *leftRotate(TreeNode *node) {\n TreeNode *child = node-&gt;right;\n TreeNode *grandChild = child-&gt;left;\n // child를 중심으로 node를 왼쪽으로 회전한다\n child-&gt;left = node;\n node-&gt;right = grandChild;\n // 노드 높이를 갱신한다\n updateHeight(node);\n updateHeight(child);\n // 회전한 서브트리의 루트 노드를 반환한다\n return child;\n}\n</code></pre> avl_tree.java<pre><code>/* 왼쪽 회전 연산 */\nTreeNode leftRotate(TreeNode node) {\n TreeNode child = node.right;\n TreeNode grandChild = child.left;\n // child를 중심으로 node를 왼쪽으로 회전한다\n child.left = node;\n node.right = grandChild;\n // 노드 높이를 갱신한다\n updateHeight(node);\n updateHeight(child);\n // 회전한 서브트리의 루트 노드를 반환한다\n return child;\n}\n</code></pre> avl_tree.cs<pre><code>/* 왼쪽 회전 연산 */\nTreeNode? LeftRotate(TreeNode? node) {\n TreeNode? child = node?.right;\n TreeNode? grandChild = child?.left;\n // child를 중심으로 node를 왼쪽으로 회전한다\n child.left = node;\n node.right = grandChild;\n // 노드 높이를 갱신한다\n UpdateHeight(node);\n UpdateHeight(child);\n // 회전한 서브트리의 루트 노드를 반환한다\n return child;\n}\n</code></pre> avl_tree.go<pre><code>/* 왼쪽 회전 연산 */\nfunc (t *aVLTree) leftRotate(node *TreeNode) *TreeNode {\n child := node.Right\n grandChild := child.Left\n // child를 중심으로 node를 왼쪽으로 회전한다\n child.Left = node\n node.Right = grandChild\n // 노드 높이를 갱신한다\n t.updateHeight(node)\n t.updateHeight(child)\n // 회전한 서브트리의 루트 노드를 반환한다\n return child\n}\n</code></pre> avl_tree.swift<pre><code>/* 왼쪽 회전 연산 */\nfunc leftRotate(node: TreeNode?) -&gt; TreeNode? {\n let child = node?.right\n let grandChild = child?.left\n // child를 중심으로 node를 왼쪽으로 회전한다\n child?.left = node\n node?.right = grandChild\n // 노드 높이를 갱신한다\n updateHeight(node: node)\n updateHeight(node: child)\n // 회전한 서브트리의 루트 노드를 반환한다\n return child\n}\n</code></pre> avl_tree.js<pre><code>/* 왼쪽 회전 연산 */\n#leftRotate(node) {\n const child = node.right;\n const grandChild = child.left;\n // child를 중심으로 node를 왼쪽으로 회전한다\n child.left = node;\n node.right = grandChild;\n // 노드 높이를 갱신한다\n this.#updateHeight(node);\n this.#updateHeight(child);\n // 회전한 서브트리의 루트 노드를 반환한다\n return child;\n}\n</code></pre> avl_tree.ts<pre><code>/* 왼쪽 회전 연산 */\nleftRotate(node: TreeNode): TreeNode {\n const child = node.right;\n const grandChild = child.left;\n // child를 중심으로 node를 왼쪽으로 회전한다\n child.left = node;\n node.right = grandChild;\n // 노드 높이를 갱신한다\n this.updateHeight(node);\n this.updateHeight(child);\n // 회전한 서브트리의 루트 노드를 반환한다\n return child;\n}\n</code></pre> avl_tree.dart<pre><code>/* 왼쪽 회전 연산 */\nTreeNode? leftRotate(TreeNode? node) {\n TreeNode? child = node!.right;\n TreeNode? grandChild = child!.left;\n // child를 중심으로 node를 왼쪽으로 회전한다\n child.left = node;\n node.right = grandChild;\n // 노드 높이를 갱신한다\n updateHeight(node);\n updateHeight(child);\n // 회전한 서브트리의 루트 노드를 반환한다\n return child;\n}\n</code></pre> avl_tree.rs<pre><code>/* 왼쪽 회전 연산 */\nfn left_rotate(node: OptionTreeNodeRc) -&gt; OptionTreeNodeRc {\n match node {\n Some(node) =&gt; {\n let child = node.borrow().right.clone().unwrap();\n let grand_child = child.borrow().left.clone();\n // child를 중심으로 node를 왼쪽으로 회전한다\n child.borrow_mut().left = Some(node.clone());\n node.borrow_mut().right = grand_child;\n // 노드 높이를 갱신한다\n Self::update_height(Some(node));\n Self::update_height(Some(child.clone()));\n // 회전한 서브트리의 루트 노드를 반환한다\n Some(child)\n }\n None =&gt; None,\n }\n}\n</code></pre> avl_tree.c<pre><code>/* 왼쪽 회전 연산 */\nTreeNode *leftRotate(TreeNode *node) {\n TreeNode *child, *grandChild;\n child = node-&gt;right;\n grandChild = child-&gt;left;\n // child를 중심으로 node를 왼쪽으로 회전한다\n child-&gt;left = node;\n node-&gt;right = grandChild;\n // 노드 높이를 갱신한다\n updateHeight(node);\n updateHeight(child);\n // 회전한 서브트리의 루트 노드를 반환한다\n return child;\n}\n</code></pre> avl_tree.kt<pre><code>/* 왼쪽 회전 연산 */\nfun leftRotate(node: TreeNode?): TreeNode {\n val child = node!!.right\n val grandChild = child!!.left\n // child를 중심으로 node를 왼쪽으로 회전한다\n child.left = node\n node.right = grandChild\n // 노드 높이를 갱신한다\n updateHeight(node)\n updateHeight(child)\n // 회전한 서브트리의 루트 노드를 반환한다\n return child\n}\n</code></pre> avl_tree.rb<pre><code>### 왼쪽 회전 연산 ###\ndef left_rotate(node)\n child = node.right\n grand_child = child.left\n # child를 중심으로 node를 왼쪽으로 회전한다\n child.left = node\n node.right = grand_child\n # 노드 높이를 갱신한다\n update_height(node)\n update_height(child)\n # 회전한 서브트리의 루트 노드를 반환한다\n child\nend\n</code></pre>","path":["제 7 장   트리","7.5   AVL 트리 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#3","level":3,"title":"3.   왼쪽 회전 후 오른쪽 회전","text":"<p>아래 그림(7-30)의 불균형 노드 3은 왼쪽 회전이나 오른쪽 회전 한 번만으로 균형을 회복할 수 없다. 먼저 <code>child</code>를 '왼쪽 회전'한 뒤 <code>node</code>를 '오른쪽 회전'해야 한다.</p> <p></p> <p> 그림 7-30   왼쪽 회전 후 오른쪽 회전 </p>","path":["제 7 장   트리","7.5   AVL 트리 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#4","level":3,"title":"4.   오른쪽 회전 후 왼쪽 회전","text":"<p>아래 그림(7-31)처럼 앞 상황의 거울 대칭인 경우에는 먼저 <code>child</code>를 '오른쪽 회전'한 뒤 <code>node</code>를 '왼쪽 회전'해야 한다.</p> <p></p> <p> 그림 7-31   오른쪽 회전 후 왼쪽 회전 </p>","path":["제 7 장   트리","7.5   AVL 트리 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#5","level":3,"title":"5.   회전 선택","text":"<p>아래 그림(7-32)의 네 불균형 상황은 앞의 예시들과 각각 대응한다. 순서대로 오른쪽 회전, 왼쪽 후 오른쪽 회전, 오른쪽 후 왼쪽 회전, 왼쪽 회전이 필요하다.</p> <p></p> <p> 그림 7-32   AVL 트리의 네 가지 회전 상황 </p> <p>아래 표(7-3)처럼 불균형 노드의 균형 인수와 더 높은 쪽 자식의 균형 인수 부호를 확인하여 어느 상황에 해당하는지 판단한다.</p> <p> 표 7-3   네 가지 회전의 선택 조건 </p> 불균형 노드의 균형 인수 자식 노드의 균형 인수 사용할 회전 \\(&gt; 1\\) (왼쪽으로 치우친 트리) \\(\\geq 0\\) 오른쪽 회전 \\(&gt; 1\\) (왼쪽으로 치우친 트리) \\(&lt;0\\) 왼쪽 회전 후 오른쪽 회전 \\(&lt; -1\\) (오른쪽으로 치우친 트리) \\(\\leq 0\\) 왼쪽 회전 \\(&lt; -1\\) (오른쪽으로 치우친 트리) \\(&gt;0\\) 오른쪽 회전 후 왼쪽 회전 <p>편리하게 사용하도록 회전 연산을 함수 하나로 묶는다. 이 함수를 사용하면 여러 불균형 상황에 맞게 회전하여 노드의 균형을 복구할 수 있다. 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py<pre><code>def rotate(self, node: TreeNode | None) -&gt; TreeNode | None:\n \"\"\"회전 연산을 수행하여 해당 서브트리의 균형 복원\"\"\"\n # 노드 node의 균형 인수를 구한다\n balance_factor = self.balance_factor(node)\n # 왼쪽으로 치우친 트리\n if balance_factor &gt; 1:\n if self.balance_factor(node.left) &gt;= 0:\n # 오른쪽 회전\n return self.right_rotate(node)\n else:\n # 왼쪽으로 회전한 후 오른쪽으로 회전한다\n node.left = self.left_rotate(node.left)\n return self.right_rotate(node)\n # 오른쪽으로 치우친 트리\n elif balance_factor &lt; -1:\n if self.balance_factor(node.right) &lt;= 0:\n # 왼쪽 회전\n return self.left_rotate(node)\n else:\n # 오른쪽으로 회전한 후 왼쪽으로 회전한다\n node.right = self.right_rotate(node.right)\n return self.left_rotate(node)\n # 균형 트리이므로 회전할 필요 없이 바로 반환한다\n return node\n</code></pre> avl_tree.cpp<pre><code>/* 회전 연산을 수행하여 해당 서브트리의 균형 복원 */\nTreeNode *rotate(TreeNode *node) {\n // 노드 node의 균형 인수를 구한다\n int _balanceFactor = balanceFactor(node);\n // 왼쪽으로 치우친 트리\n if (_balanceFactor &gt; 1) {\n if (balanceFactor(node-&gt;left) &gt;= 0) {\n // 오른쪽 회전\n return rightRotate(node);\n } else {\n // 왼쪽으로 회전한 후 오른쪽으로 회전한다\n node-&gt;left = leftRotate(node-&gt;left);\n return rightRotate(node);\n }\n }\n // 오른쪽으로 치우친 트리\n if (_balanceFactor &lt; -1) {\n if (balanceFactor(node-&gt;right) &lt;= 0) {\n // 왼쪽 회전\n return leftRotate(node);\n } else {\n // 오른쪽으로 회전한 후 왼쪽으로 회전한다\n node-&gt;right = rightRotate(node-&gt;right);\n return leftRotate(node);\n }\n }\n // 균형 트리이므로 회전할 필요 없이 바로 반환한다\n return node;\n}\n</code></pre> avl_tree.java<pre><code>/* 회전 연산을 수행하여 해당 서브트리의 균형 복원 */\nTreeNode rotate(TreeNode node) {\n // 노드 node의 균형 인수를 구한다\n int balanceFactor = balanceFactor(node);\n // 왼쪽으로 치우친 트리\n if (balanceFactor &gt; 1) {\n if (balanceFactor(node.left) &gt;= 0) {\n // 오른쪽 회전\n return rightRotate(node);\n } else {\n // 왼쪽으로 회전한 후 오른쪽으로 회전한다\n node.left = leftRotate(node.left);\n return rightRotate(node);\n }\n }\n // 오른쪽으로 치우친 트리\n if (balanceFactor &lt; -1) {\n if (balanceFactor(node.right) &lt;= 0) {\n // 왼쪽 회전\n return leftRotate(node);\n } else {\n // 오른쪽으로 회전한 후 왼쪽으로 회전한다\n node.right = rightRotate(node.right);\n return leftRotate(node);\n }\n }\n // 균형 트리이므로 회전할 필요 없이 바로 반환한다\n return node;\n}\n</code></pre> avl_tree.cs<pre><code>/* 회전 연산을 수행하여 해당 서브트리의 균형 복원 */\nTreeNode? Rotate(TreeNode? node) {\n // 노드 node의 균형 인수를 구한다\n int balanceFactorInt = BalanceFactor(node);\n // 왼쪽으로 치우친 트리\n if (balanceFactorInt &gt; 1) {\n if (BalanceFactor(node?.left) &gt;= 0) {\n // 오른쪽 회전\n return RightRotate(node);\n } else {\n // 왼쪽으로 회전한 후 오른쪽으로 회전한다\n node!.left = LeftRotate(node!.left);\n return RightRotate(node);\n }\n }\n // 오른쪽으로 치우친 트리\n if (balanceFactorInt &lt; -1) {\n if (BalanceFactor(node?.right) &lt;= 0) {\n // 왼쪽 회전\n return LeftRotate(node);\n } else {\n // 오른쪽으로 회전한 후 왼쪽으로 회전한다\n node!.right = RightRotate(node!.right);\n return LeftRotate(node);\n }\n }\n // 균형 트리이므로 회전할 필요 없이 바로 반환한다\n return node;\n}\n</code></pre> avl_tree.go<pre><code>/* 회전 연산을 수행하여 해당 서브트리의 균형 복원 */\nfunc (t *aVLTree) rotate(node *TreeNode) *TreeNode {\n // 노드 node의 균형 인수를 구한다\n // Go에서는 짧은 변수명을 권장한다. 여기서 bf는 t.balanceFactor를 나타낸다\n bf := t.balanceFactor(node)\n // 왼쪽으로 치우친 트리\n if bf &gt; 1 {\n if t.balanceFactor(node.Left) &gt;= 0 {\n // 오른쪽 회전\n return t.rightRotate(node)\n } else {\n // 왼쪽으로 회전한 후 오른쪽으로 회전한다\n node.Left = t.leftRotate(node.Left)\n return t.rightRotate(node)\n }\n }\n // 오른쪽으로 치우친 트리\n if bf &lt; -1 {\n if t.balanceFactor(node.Right) &lt;= 0 {\n // 왼쪽 회전\n return t.leftRotate(node)\n } else {\n // 오른쪽으로 회전한 후 왼쪽으로 회전한다\n node.Right = t.rightRotate(node.Right)\n return t.leftRotate(node)\n }\n }\n // 균형 트리이므로 회전할 필요 없이 바로 반환한다\n return node\n}\n</code></pre> avl_tree.swift<pre><code>/* 회전 연산을 수행하여 해당 서브트리의 균형 복원 */\nfunc rotate(node: TreeNode?) -&gt; TreeNode? {\n // 노드 node의 균형 인수를 구한다\n let balanceFactor = balanceFactor(node: node)\n // 왼쪽으로 치우친 트리\n if balanceFactor &gt; 1 {\n if self.balanceFactor(node: node?.left) &gt;= 0 {\n // 오른쪽 회전\n return rightRotate(node: node)\n } else {\n // 왼쪽으로 회전한 후 오른쪽으로 회전한다\n node?.left = leftRotate(node: node?.left)\n return rightRotate(node: node)\n }\n }\n // 오른쪽으로 치우친 트리\n if balanceFactor &lt; -1 {\n if self.balanceFactor(node: node?.right) &lt;= 0 {\n // 왼쪽 회전\n return leftRotate(node: node)\n } else {\n // 오른쪽으로 회전한 후 왼쪽으로 회전한다\n node?.right = rightRotate(node: node?.right)\n return leftRotate(node: node)\n }\n }\n // 균형 트리이므로 회전할 필요 없이 바로 반환한다\n return node\n}\n</code></pre> avl_tree.js<pre><code>/* 회전 연산을 수행하여 해당 서브트리의 균형 복원 */\n#rotate(node) {\n // 노드 node의 균형 인수를 구한다\n const balanceFactor = this.balanceFactor(node);\n // 왼쪽으로 치우친 트리\n if (balanceFactor &gt; 1) {\n if (this.balanceFactor(node.left) &gt;= 0) {\n // 오른쪽 회전\n return this.#rightRotate(node);\n } else {\n // 왼쪽으로 회전한 후 오른쪽으로 회전한다\n node.left = this.#leftRotate(node.left);\n return this.#rightRotate(node);\n }\n }\n // 오른쪽으로 치우친 트리\n if (balanceFactor &lt; -1) {\n if (this.balanceFactor(node.right) &lt;= 0) {\n // 왼쪽 회전\n return this.#leftRotate(node);\n } else {\n // 오른쪽으로 회전한 후 왼쪽으로 회전한다\n node.right = this.#rightRotate(node.right);\n return this.#leftRotate(node);\n }\n }\n // 균형 트리이므로 회전할 필요 없이 바로 반환한다\n return node;\n}\n</code></pre> avl_tree.ts<pre><code>/* 회전 연산을 수행하여 해당 서브트리의 균형 복원 */\nrotate(node: TreeNode): TreeNode {\n // 노드 node의 균형 인수를 구한다\n const balanceFactor = this.balanceFactor(node);\n // 왼쪽으로 치우친 트리\n if (balanceFactor &gt; 1) {\n if (this.balanceFactor(node.left) &gt;= 0) {\n // 오른쪽 회전\n return this.rightRotate(node);\n } else {\n // 왼쪽으로 회전한 후 오른쪽으로 회전한다\n node.left = this.leftRotate(node.left);\n return this.rightRotate(node);\n }\n }\n // 오른쪽으로 치우친 트리\n if (balanceFactor &lt; -1) {\n if (this.balanceFactor(node.right) &lt;= 0) {\n // 왼쪽 회전\n return this.leftRotate(node);\n } else {\n // 오른쪽으로 회전한 후 왼쪽으로 회전한다\n node.right = this.rightRotate(node.right);\n return this.leftRotate(node);\n }\n }\n // 균형 트리이므로 회전할 필요 없이 바로 반환한다\n return node;\n}\n</code></pre> avl_tree.dart<pre><code>/* 회전 연산을 수행하여 해당 서브트리의 균형 복원 */\nTreeNode? rotate(TreeNode? node) {\n // 노드 node의 균형 인수를 구한다\n int factor = balanceFactor(node);\n // 왼쪽으로 치우친 트리\n if (factor &gt; 1) {\n if (balanceFactor(node!.left) &gt;= 0) {\n // 오른쪽 회전\n return rightRotate(node);\n } else {\n // 왼쪽으로 회전한 후 오른쪽으로 회전한다\n node.left = leftRotate(node.left);\n return rightRotate(node);\n }\n }\n // 오른쪽으로 치우친 트리\n if (factor &lt; -1) {\n if (balanceFactor(node!.right) &lt;= 0) {\n // 왼쪽 회전\n return leftRotate(node);\n } else {\n // 오른쪽으로 회전한 후 왼쪽으로 회전한다\n node.right = rightRotate(node.right);\n return leftRotate(node);\n }\n }\n // 균형 트리이므로 회전할 필요 없이 바로 반환한다\n return node;\n}\n</code></pre> avl_tree.rs<pre><code>/* 회전 연산을 수행하여 해당 서브트리의 균형 복원 */\nfn rotate(node: OptionTreeNodeRc) -&gt; OptionTreeNodeRc {\n // 노드 node의 균형 인수를 구한다\n let balance_factor = Self::balance_factor(node.clone());\n // 왼쪽으로 치우친 트리\n if balance_factor &gt; 1 {\n let node = node.unwrap();\n if Self::balance_factor(node.borrow().left.clone()) &gt;= 0 {\n // 오른쪽 회전\n Self::right_rotate(Some(node))\n } else {\n // 왼쪽으로 회전한 후 오른쪽으로 회전한다\n let left = node.borrow().left.clone();\n node.borrow_mut().left = Self::left_rotate(left);\n Self::right_rotate(Some(node))\n }\n }\n // 오른쪽으로 치우친 트리\n else if balance_factor &lt; -1 {\n let node = node.unwrap();\n if Self::balance_factor(node.borrow().right.clone()) &lt;= 0 {\n // 왼쪽 회전\n Self::left_rotate(Some(node))\n } else {\n // 오른쪽으로 회전한 후 왼쪽으로 회전한다\n let right = node.borrow().right.clone();\n node.borrow_mut().right = Self::right_rotate(right);\n Self::left_rotate(Some(node))\n }\n } else {\n // 균형 트리이므로 회전할 필요 없이 바로 반환한다\n node\n }\n}\n</code></pre> avl_tree.c<pre><code>/* 회전 연산을 수행하여 해당 서브트리의 균형 복원 */\nTreeNode *rotate(TreeNode *node) {\n // 노드 node의 균형 인수를 구한다\n int bf = balanceFactor(node);\n // 왼쪽으로 치우친 트리\n if (bf &gt; 1) {\n if (balanceFactor(node-&gt;left) &gt;= 0) {\n // 오른쪽 회전\n return rightRotate(node);\n } else {\n // 왼쪽으로 회전한 후 오른쪽으로 회전한다\n node-&gt;left = leftRotate(node-&gt;left);\n return rightRotate(node);\n }\n }\n // 오른쪽으로 치우친 트리\n if (bf &lt; -1) {\n if (balanceFactor(node-&gt;right) &lt;= 0) {\n // 왼쪽 회전\n return leftRotate(node);\n } else {\n // 오른쪽으로 회전한 후 왼쪽으로 회전한다\n node-&gt;right = rightRotate(node-&gt;right);\n return leftRotate(node);\n }\n }\n // 균형 트리이므로 회전할 필요 없이 바로 반환한다\n return node;\n}\n</code></pre> avl_tree.kt<pre><code>/* 회전 연산을 수행하여 해당 서브트리의 균형 복원 */\nfun rotate(node: TreeNode): TreeNode {\n // 노드 node의 균형 인수를 구한다\n val balanceFactor = balanceFactor(node)\n // 왼쪽으로 치우친 트리\n if (balanceFactor &gt; 1) {\n if (balanceFactor(node.left) &gt;= 0) {\n // 오른쪽 회전\n return rightRotate(node)\n } else {\n // 왼쪽으로 회전한 후 오른쪽으로 회전한다\n node.left = leftRotate(node.left)\n return rightRotate(node)\n }\n }\n // 오른쪽으로 치우친 트리\n if (balanceFactor &lt; -1) {\n if (balanceFactor(node.right) &lt;= 0) {\n // 왼쪽 회전\n return leftRotate(node)\n } else {\n // 오른쪽으로 회전한 후 왼쪽으로 회전한다\n node.right = rightRotate(node.right)\n return leftRotate(node)\n }\n }\n // 균형 트리이므로 회전할 필요 없이 바로 반환한다\n return node\n}\n</code></pre> avl_tree.rb<pre><code>### 회전 연산을 수행하여 해당 서브트리의 균형 복원 ###\ndef rotate(node)\n # 노드 node의 균형 인수를 구한다\n balance_factor = balance_factor(node)\n # 왼쪽으로 치우친 트리\n if balance_factor &gt; 1\n if balance_factor(node.left) &gt;= 0\n # 오른쪽 회전\n return right_rotate(node)\n else\n # 왼쪽으로 회전한 후 오른쪽으로 회전한다\n node.left = left_rotate(node.left)\n return right_rotate(node)\n end\n # 오른쪽으로 치우친 트리\n elsif balance_factor &lt; -1\n if balance_factor(node.right) &lt;= 0\n # 왼쪽 회전\n return left_rotate(node)\n else\n # 오른쪽으로 회전한 후 왼쪽으로 회전한다\n node.right = right_rotate(node.right)\n return left_rotate(node)\n end\n end\n # 균형 트리이므로 회전할 필요 없이 바로 반환한다\n node\nend\n</code></pre>","path":["제 7 장   트리","7.5   AVL 트리 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#753-avl","level":2,"title":"7.5.3   AVL 트리의 주요 연산","text":"","path":["제 7 장   트리","7.5   AVL 트리 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1_2","level":3,"title":"1.   노드 삽입","text":"<p>AVL 트리의 삽입은 기본적으로 이진 탐색 트리와 비슷하다. 다만 삽입한 노드에서 루트까지의 경로에 불균형 노드가 여러 개 생길 수 있다는 차이가 있다. 따라서 삽입한 노드부터 아래에서 위로 회전하여 모든 불균형 노드의 균형을 회복해야 한다. 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py<pre><code>def insert(self, val):\n \"\"\"노드 삽입\"\"\"\n self._root = self.insert_helper(self._root, val)\n\ndef insert_helper(self, node: TreeNode | None, val: int) -&gt; TreeNode:\n \"\"\"노드의 재귀적 삽입(보조 메서드)\"\"\"\n if node is None:\n return TreeNode(val)\n # 1. 삽입 위치를 찾아 노드를 삽입한다\n if val &lt; node.val:\n node.left = self.insert_helper(node.left, val)\n elif val &gt; node.val:\n node.right = self.insert_helper(node.right, val)\n else:\n # 중복 노드는 삽입하지 않고 바로 반환한다\n return node\n # 노드 높이를 갱신한다\n self.update_height(node)\n # 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다\n return self.rotate(node)\n</code></pre> avl_tree.cpp<pre><code>/* 노드 삽입 */\nvoid insert(int val) {\n root = insertHelper(root, val);\n}\n\n/* 노드의 재귀적 삽입(보조 메서드) */\nTreeNode *insertHelper(TreeNode *node, int val) {\n if (node == nullptr)\n return new TreeNode(val);\n /* 1. 삽입 위치를 찾아 노드를 삽입한다 */\n if (val &lt; node-&gt;val)\n node-&gt;left = insertHelper(node-&gt;left, val);\n else if (val &gt; node-&gt;val)\n node-&gt;right = insertHelper(node-&gt;right, val);\n else\n return node; // 중복 노드는 삽입하지 않고 바로 반환한다\n updateHeight(node); // 노드 높이를 갱신한다\n /* 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다 */\n node = rotate(node);\n // 서브트리의 루트 노드를 반환한다\n return node;\n}\n</code></pre> avl_tree.java<pre><code>/* 노드 삽입 */\nvoid insert(int val) {\n root = insertHelper(root, val);\n}\n\n/* 노드의 재귀적 삽입(보조 메서드) */\nTreeNode insertHelper(TreeNode node, int val) {\n if (node == null)\n return new TreeNode(val);\n /* 1. 삽입 위치를 찾아 노드를 삽입한다 */\n if (val &lt; node.val)\n node.left = insertHelper(node.left, val);\n else if (val &gt; node.val)\n node.right = insertHelper(node.right, val);\n else\n return node; // 중복 노드는 삽입하지 않고 바로 반환한다\n updateHeight(node); // 노드 높이를 갱신한다\n /* 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다 */\n node = rotate(node);\n // 서브트리의 루트 노드를 반환한다\n return node;\n}\n</code></pre> avl_tree.cs<pre><code>/* 노드 삽입 */\nvoid Insert(int val) {\n root = InsertHelper(root, val);\n}\n\n/* 노드의 재귀적 삽입(보조 메서드) */\nTreeNode? InsertHelper(TreeNode? node, int val) {\n if (node == null) return new TreeNode(val);\n /* 1. 삽입 위치를 찾아 노드를 삽입한다 */\n if (val &lt; node.val)\n node.left = InsertHelper(node.left, val);\n else if (val &gt; node.val)\n node.right = InsertHelper(node.right, val);\n else\n return node; // 중복 노드는 삽입하지 않고 바로 반환한다\n UpdateHeight(node); // 노드 높이를 갱신한다\n /* 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다 */\n node = Rotate(node);\n // 서브트리의 루트 노드를 반환한다\n return node;\n}\n</code></pre> avl_tree.go<pre><code>/* 노드 삽입 */\nfunc (t *aVLTree) insert(val int) {\n t.root = t.insertHelper(t.root, val)\n}\n\n/* 노드의 재귀적 삽입(보조 함수) */\nfunc (t *aVLTree) insertHelper(node *TreeNode, val int) *TreeNode {\n if node == nil {\n return NewTreeNode(val)\n }\n /* 1. 삽입 위치를 찾아 노드를 삽입한다 */\n if val &lt; node.Val.(int) {\n node.Left = t.insertHelper(node.Left, val)\n } else if val &gt; node.Val.(int) {\n node.Right = t.insertHelper(node.Right, val)\n } else {\n // 중복 노드는 삽입하지 않고 바로 반환한다\n return node\n }\n // 노드 높이를 갱신한다\n t.updateHeight(node)\n /* 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다 */\n node = t.rotate(node)\n // 서브트리의 루트 노드를 반환한다\n return node\n}\n</code></pre> avl_tree.swift<pre><code>/* 노드 삽입 */\nfunc insert(val: Int) {\n root = insertHelper(node: root, val: val)\n}\n\n/* 노드의 재귀적 삽입(보조 메서드) */\nfunc insertHelper(node: TreeNode?, val: Int) -&gt; TreeNode? {\n var node = node\n if node == nil {\n return TreeNode(x: val)\n }\n /* 1. 삽입 위치를 찾아 노드를 삽입한다 */\n if val &lt; node!.val {\n node?.left = insertHelper(node: node?.left, val: val)\n } else if val &gt; node!.val {\n node?.right = insertHelper(node: node?.right, val: val)\n } else {\n return node // 중복 노드는 삽입하지 않고 바로 반환한다\n }\n updateHeight(node: node) // 노드 높이를 갱신한다\n /* 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다 */\n node = rotate(node: node)\n // 서브트리의 루트 노드를 반환한다\n return node\n}\n</code></pre> avl_tree.js<pre><code>/* 노드 삽입 */\ninsert(val) {\n this.root = this.#insertHelper(this.root, val);\n}\n\n/* 노드의 재귀적 삽입(보조 메서드) */\n#insertHelper(node, val) {\n if (node === null) return new TreeNode(val);\n /* 1. 삽입 위치를 찾아 노드를 삽입한다 */\n if (val &lt; node.val) node.left = this.#insertHelper(node.left, val);\n else if (val &gt; node.val)\n node.right = this.#insertHelper(node.right, val);\n else return node; // 중복 노드는 삽입하지 않고 바로 반환한다\n this.#updateHeight(node); // 노드 높이를 갱신한다\n /* 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다 */\n node = this.#rotate(node);\n // 서브트리의 루트 노드를 반환한다\n return node;\n}\n</code></pre> avl_tree.ts<pre><code>/* 노드 삽입 */\ninsert(val: number): void {\n this.root = this.insertHelper(this.root, val);\n}\n\n/* 노드의 재귀적 삽입(보조 메서드) */\ninsertHelper(node: TreeNode, val: number): TreeNode {\n if (node === null) return new TreeNode(val);\n /* 1. 삽입 위치를 찾아 노드를 삽입한다 */\n if (val &lt; node.val) {\n node.left = this.insertHelper(node.left, val);\n } else if (val &gt; node.val) {\n node.right = this.insertHelper(node.right, val);\n } else {\n return node; // 중복 노드는 삽입하지 않고 바로 반환한다\n }\n this.updateHeight(node); // 노드 높이를 갱신한다\n /* 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다 */\n node = this.rotate(node);\n // 서브트리의 루트 노드를 반환한다\n return node;\n}\n</code></pre> avl_tree.dart<pre><code>/* 노드 삽입 */\nvoid insert(int val) {\n root = insertHelper(root, val);\n}\n\n/* 노드의 재귀적 삽입(보조 메서드) */\nTreeNode? insertHelper(TreeNode? node, int val) {\n if (node == null) return TreeNode(val);\n /* 1. 삽입 위치를 찾아 노드를 삽입한다 */\n if (val &lt; node.val)\n node.left = insertHelper(node.left, val);\n else if (val &gt; node.val)\n node.right = insertHelper(node.right, val);\n else\n return node; // 중복 노드는 삽입하지 않고 바로 반환한다\n updateHeight(node); // 노드 높이를 갱신한다\n /* 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다 */\n node = rotate(node);\n // 서브트리의 루트 노드를 반환한다\n return node;\n}\n</code></pre> avl_tree.rs<pre><code>/* 노드 삽입 */\nfn insert(&amp;mut self, val: i32) {\n self.root = Self::insert_helper(self.root.clone(), val);\n}\n\n/* 노드의 재귀적 삽입(보조 메서드) */\nfn insert_helper(node: OptionTreeNodeRc, val: i32) -&gt; OptionTreeNodeRc {\n match node {\n Some(mut node) =&gt; {\n /* 1. 삽입 위치를 찾아 노드를 삽입한다 */\n match {\n let node_val = node.borrow().val;\n node_val\n }\n .cmp(&amp;val)\n {\n Ordering::Greater =&gt; {\n let left = node.borrow().left.clone();\n node.borrow_mut().left = Self::insert_helper(left, val);\n }\n Ordering::Less =&gt; {\n let right = node.borrow().right.clone();\n node.borrow_mut().right = Self::insert_helper(right, val);\n }\n Ordering::Equal =&gt; {\n return Some(node); // 중복 노드는 삽입하지 않고 바로 반환한다\n }\n }\n Self::update_height(Some(node.clone())); // 노드 높이를 갱신한다\n\n /* 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다 */\n node = Self::rotate(Some(node)).unwrap();\n // 서브트리의 루트 노드를 반환한다\n Some(node)\n }\n None =&gt; Some(TreeNode::new(val)),\n }\n}\n</code></pre> avl_tree.c<pre><code>/* 노드 삽입 */\nvoid insert(AVLTree *tree, int val) {\n tree-&gt;root = insertHelper(tree-&gt;root, val);\n}\n\n/* 노드의 재귀적 삽입(보조 함수) */\nTreeNode *insertHelper(TreeNode *node, int val) {\n if (node == NULL) {\n return newTreeNode(val);\n }\n /* 1. 삽입 위치를 찾아 노드를 삽입한다 */\n if (val &lt; node-&gt;val) {\n node-&gt;left = insertHelper(node-&gt;left, val);\n } else if (val &gt; node-&gt;val) {\n node-&gt;right = insertHelper(node-&gt;right, val);\n } else {\n // 중복 노드는 삽입하지 않고 바로 반환한다\n return node;\n }\n // 노드 높이를 갱신한다\n updateHeight(node);\n /* 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다 */\n node = rotate(node);\n // 서브트리의 루트 노드를 반환한다\n return node;\n}\n</code></pre> avl_tree.kt<pre><code>/* 노드 삽입 */\nfun insert(_val: Int) {\n root = insertHelper(root, _val)\n}\n\n/* 노드의 재귀적 삽입(보조 메서드) */\nfun insertHelper(n: TreeNode?, _val: Int): TreeNode {\n if (n == null)\n return TreeNode(_val)\n var node = n\n /* 1. 삽입 위치를 찾아 노드를 삽입한다 */\n if (_val &lt; node._val)\n node.left = insertHelper(node.left, _val)\n else if (_val &gt; node._val)\n node.right = insertHelper(node.right, _val)\n else\n return node // 중복 노드는 삽입하지 않고 바로 반환한다\n updateHeight(node) // 노드 높이를 갱신한다\n /* 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다 */\n node = rotate(node)\n // 서브트리의 루트 노드를 반환한다\n return node\n}\n</code></pre> avl_tree.rb<pre><code>### 노드 삽입 ###\ndef insert(val)\n @root = insert_helper(@root, val)\nend\n\n### 노드의 재귀적 삽입(보조 메서드)###\ndef insert_helper(node, val)\n return TreeNode.new(val) if node.nil?\n # 1. 삽입 위치를 찾아 노드를 삽입한다\n if val &lt; node.val\n node.left = insert_helper(node.left, val)\n elsif val &gt; node.val\n node.right = insert_helper(node.right, val)\n else\n # 중복 노드는 삽입하지 않고 바로 반환한다\n return node\n end\n # 노드 높이를 갱신한다\n update_height(node)\n # 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다\n rotate(node)\nend\n</code></pre>","path":["제 7 장   트리","7.5   AVL 트리 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2_2","level":3,"title":"2.   노드 삭제","text":"<p>마찬가지로 이진 탐색 트리의 삭제 방법에 더하여 아래에서 위로 회전하며 모든 불균형 노드의 균형을 회복한다. 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py<pre><code>def remove(self, val: int):\n \"\"\"노드 삭제\"\"\"\n self._root = self.remove_helper(self._root, val)\n\ndef remove_helper(self, node: TreeNode | None, val: int) -&gt; TreeNode | None:\n \"\"\"노드의 재귀적 삭제(보조 메서드)\"\"\"\n if node is None:\n return None\n # 1. 노드를 찾아 삭제한다\n if val &lt; node.val:\n node.left = self.remove_helper(node.left, val)\n elif val &gt; node.val:\n node.right = self.remove_helper(node.right, val)\n else:\n if node.left is None or node.right is None:\n child = node.left or node.right\n # 자식 노드 수 = 0이면 node를 바로 삭제하고 반환한다\n if child is None:\n return None\n # 자식 노드 수 = 1이면 node를 바로 삭제한다\n else:\n node = child\n else:\n # 자식 노드 수 = 2이면 중위 순회의 다음 노드를 삭제하고, 그 노드로 현재 노드를 대체한다\n temp = node.right\n while temp.left is not None:\n temp = temp.left\n node.right = self.remove_helper(node.right, temp.val)\n node.val = temp.val\n # 노드 높이를 갱신한다\n self.update_height(node)\n # 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다\n return self.rotate(node)\n</code></pre> avl_tree.cpp<pre><code>/* 노드 삭제 */\nvoid remove(int val) {\n root = removeHelper(root, val);\n}\n\n/* 노드의 재귀적 삭제(보조 메서드) */\nTreeNode *removeHelper(TreeNode *node, int val) {\n if (node == nullptr)\n return nullptr;\n /* 1. 노드를 찾아 삭제한다 */\n if (val &lt; node-&gt;val)\n node-&gt;left = removeHelper(node-&gt;left, val);\n else if (val &gt; node-&gt;val)\n node-&gt;right = removeHelper(node-&gt;right, val);\n else {\n if (node-&gt;left == nullptr || node-&gt;right == nullptr) {\n TreeNode *child = node-&gt;left != nullptr ? node-&gt;left : node-&gt;right;\n // 자식 노드 수 = 0이면 node를 바로 삭제하고 반환한다\n if (child == nullptr) {\n delete node;\n return nullptr;\n }\n // 자식 노드 수 = 1이면 node를 바로 삭제한다\n else {\n delete node;\n node = child;\n }\n } else {\n // 자식 노드 수 = 2이면 중위 순회의 다음 노드를 삭제하고, 그 노드로 현재 노드를 대체한다\n TreeNode *temp = node-&gt;right;\n while (temp-&gt;left != nullptr) {\n temp = temp-&gt;left;\n }\n int tempVal = temp-&gt;val;\n node-&gt;right = removeHelper(node-&gt;right, temp-&gt;val);\n node-&gt;val = tempVal;\n }\n }\n updateHeight(node); // 노드 높이를 갱신한다\n /* 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다 */\n node = rotate(node);\n // 서브트리의 루트 노드를 반환한다\n return node;\n}\n</code></pre> avl_tree.java<pre><code>/* 노드 삭제 */\nvoid remove(int val) {\n root = removeHelper(root, val);\n}\n\n/* 노드의 재귀적 삭제(보조 메서드) */\nTreeNode removeHelper(TreeNode node, int val) {\n if (node == null)\n return null;\n /* 1. 노드를 찾아 삭제한다 */\n if (val &lt; node.val)\n node.left = removeHelper(node.left, val);\n else if (val &gt; node.val)\n node.right = removeHelper(node.right, val);\n else {\n if (node.left == null || node.right == null) {\n TreeNode child = node.left != null ? node.left : node.right;\n // 자식 노드 수 = 0이면 node를 바로 삭제하고 반환한다\n if (child == null)\n return null;\n // 자식 노드 수 = 1이면 node를 바로 삭제한다\n else\n node = child;\n } else {\n // 자식 노드 수 = 2이면 중위 순회의 다음 노드를 삭제하고, 그 노드로 현재 노드를 대체한다\n TreeNode temp = node.right;\n while (temp.left != null) {\n temp = temp.left;\n }\n node.right = removeHelper(node.right, temp.val);\n node.val = temp.val;\n }\n }\n updateHeight(node); // 노드 높이를 갱신한다\n /* 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다 */\n node = rotate(node);\n // 서브트리의 루트 노드를 반환한다\n return node;\n}\n</code></pre> avl_tree.cs<pre><code>/* 노드 삭제 */\nvoid Remove(int val) {\n root = RemoveHelper(root, val);\n}\n\n/* 노드의 재귀적 삭제(보조 메서드) */\nTreeNode? RemoveHelper(TreeNode? node, int val) {\n if (node == null) return null;\n /* 1. 노드를 찾아 삭제한다 */\n if (val &lt; node.val)\n node.left = RemoveHelper(node.left, val);\n else if (val &gt; node.val)\n node.right = RemoveHelper(node.right, val);\n else {\n if (node.left == null || node.right == null) {\n TreeNode? child = node.left ?? node.right;\n // 자식 노드 수 = 0이면 node를 바로 삭제하고 반환한다\n if (child == null)\n return null;\n // 자식 노드 수 = 1이면 node를 바로 삭제한다\n else\n node = child;\n } else {\n // 자식 노드 수 = 2이면 중위 순회의 다음 노드를 삭제하고, 그 노드로 현재 노드를 대체한다\n TreeNode? temp = node.right;\n while (temp.left != null) {\n temp = temp.left;\n }\n node.right = RemoveHelper(node.right, temp.val!.Value);\n node.val = temp.val;\n }\n }\n UpdateHeight(node); // 노드 높이를 갱신한다\n /* 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다 */\n node = Rotate(node);\n // 서브트리의 루트 노드를 반환한다\n return node;\n}\n</code></pre> avl_tree.go<pre><code>/* 노드 삭제 */\nfunc (t *aVLTree) remove(val int) {\n t.root = t.removeHelper(t.root, val)\n}\n\n/* 노드의 재귀적 삭제(보조 함수) */\nfunc (t *aVLTree) removeHelper(node *TreeNode, val int) *TreeNode {\n if node == nil {\n return nil\n }\n /* 1. 노드를 찾아 삭제한다 */\n if val &lt; node.Val.(int) {\n node.Left = t.removeHelper(node.Left, val)\n } else if val &gt; node.Val.(int) {\n node.Right = t.removeHelper(node.Right, val)\n } else {\n if node.Left == nil || node.Right == nil {\n child := node.Left\n if node.Right != nil {\n child = node.Right\n }\n if child == nil {\n // 자식 노드 수 = 0이면 node를 바로 삭제하고 반환한다\n return nil\n } else {\n // 자식 노드 수 = 1이면 node를 바로 삭제한다\n node = child\n }\n } else {\n // 자식 노드 수 = 2이면 중위 순회의 다음 노드를 삭제하고, 그 노드로 현재 노드를 대체한다\n temp := node.Right\n for temp.Left != nil {\n temp = temp.Left\n }\n node.Right = t.removeHelper(node.Right, temp.Val.(int))\n node.Val = temp.Val\n }\n }\n // 노드 높이를 갱신한다\n t.updateHeight(node)\n /* 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다 */\n node = t.rotate(node)\n // 서브트리의 루트 노드를 반환한다\n return node\n}\n</code></pre> avl_tree.swift<pre><code>/* 노드 삭제 */\nfunc remove(val: Int) {\n root = removeHelper(node: root, val: val)\n}\n\n/* 노드의 재귀적 삭제(보조 메서드) */\nfunc removeHelper(node: TreeNode?, val: Int) -&gt; TreeNode? {\n var node = node\n if node == nil {\n return nil\n }\n /* 1. 노드를 찾아 삭제한다 */\n if val &lt; node!.val {\n node?.left = removeHelper(node: node?.left, val: val)\n } else if val &gt; node!.val {\n node?.right = removeHelper(node: node?.right, val: val)\n } else {\n if node?.left == nil || node?.right == nil {\n let child = node?.left ?? node?.right\n // 자식 노드 수 = 0이면 node를 바로 삭제하고 반환한다\n if child == nil {\n return nil\n }\n // 자식 노드 수 = 1이면 node를 바로 삭제한다\n else {\n node = child\n }\n } else {\n // 자식 노드 수 = 2이면 중위 순회의 다음 노드를 삭제하고, 그 노드로 현재 노드를 대체한다\n var temp = node?.right\n while temp?.left != nil {\n temp = temp?.left\n }\n node?.right = removeHelper(node: node?.right, val: temp!.val)\n node?.val = temp!.val\n }\n }\n updateHeight(node: node) // 노드 높이를 갱신한다\n /* 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다 */\n node = rotate(node: node)\n // 서브트리의 루트 노드를 반환한다\n return node\n}\n</code></pre> avl_tree.js<pre><code>/* 노드 삭제 */\nremove(val) {\n this.root = this.#removeHelper(this.root, val);\n}\n\n/* 노드의 재귀적 삭제(보조 메서드) */\n#removeHelper(node, val) {\n if (node === null) return null;\n /* 1. 노드를 찾아 삭제한다 */\n if (val &lt; node.val) node.left = this.#removeHelper(node.left, val);\n else if (val &gt; node.val)\n node.right = this.#removeHelper(node.right, val);\n else {\n if (node.left === null || node.right === null) {\n const child = node.left !== null ? node.left : node.right;\n // 자식 노드 수 = 0이면 node를 바로 삭제하고 반환한다\n if (child === null) return null;\n // 자식 노드 수 = 1이면 node를 바로 삭제한다\n else node = child;\n } else {\n // 자식 노드 수 = 2이면 중위 순회의 다음 노드를 삭제하고, 그 노드로 현재 노드를 대체한다\n let temp = node.right;\n while (temp.left !== null) {\n temp = temp.left;\n }\n node.right = this.#removeHelper(node.right, temp.val);\n node.val = temp.val;\n }\n }\n this.#updateHeight(node); // 노드 높이를 갱신한다\n /* 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다 */\n node = this.#rotate(node);\n // 서브트리의 루트 노드를 반환한다\n return node;\n}\n</code></pre> avl_tree.ts<pre><code>/* 노드 삭제 */\nremove(val: number): void {\n this.root = this.removeHelper(this.root, val);\n}\n\n/* 노드의 재귀적 삭제(보조 메서드) */\nremoveHelper(node: TreeNode, val: number): TreeNode {\n if (node === null) return null;\n /* 1. 노드를 찾아 삭제한다 */\n if (val &lt; node.val) {\n node.left = this.removeHelper(node.left, val);\n } else if (val &gt; node.val) {\n node.right = this.removeHelper(node.right, val);\n } else {\n if (node.left === null || node.right === null) {\n const child = node.left !== null ? node.left : node.right;\n // 자식 노드 수 = 0이면 node를 바로 삭제하고 반환한다\n if (child === null) {\n return null;\n } else {\n // 자식 노드 수 = 1이면 node를 바로 삭제한다\n node = child;\n }\n } else {\n // 자식 노드 수 = 2이면 중위 순회의 다음 노드를 삭제하고, 그 노드로 현재 노드를 대체한다\n let temp = node.right;\n while (temp.left !== null) {\n temp = temp.left;\n }\n node.right = this.removeHelper(node.right, temp.val);\n node.val = temp.val;\n }\n }\n this.updateHeight(node); // 노드 높이를 갱신한다\n /* 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다 */\n node = this.rotate(node);\n // 서브트리의 루트 노드를 반환한다\n return node;\n}\n</code></pre> avl_tree.dart<pre><code>/* 노드 삭제 */\nvoid remove(int val) {\n root = removeHelper(root, val);\n}\n\n/* 노드의 재귀적 삭제(보조 메서드) */\nTreeNode? removeHelper(TreeNode? node, int val) {\n if (node == null) return null;\n /* 1. 노드를 찾아 삭제한다 */\n if (val &lt; node.val)\n node.left = removeHelper(node.left, val);\n else if (val &gt; node.val)\n node.right = removeHelper(node.right, val);\n else {\n if (node.left == null || node.right == null) {\n TreeNode? child = node.left ?? node.right;\n // 자식 노드 수 = 0이면 node를 바로 삭제하고 반환한다\n if (child == null)\n return null;\n // 자식 노드 수 = 1이면 node를 바로 삭제한다\n else\n node = child;\n } else {\n // 자식 노드 수 = 2이면 중위 순회의 다음 노드를 삭제하고, 그 노드로 현재 노드를 대체한다\n TreeNode? temp = node.right;\n while (temp!.left != null) {\n temp = temp.left;\n }\n node.right = removeHelper(node.right, temp.val);\n node.val = temp.val;\n }\n }\n updateHeight(node); // 노드 높이를 갱신한다\n /* 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다 */\n node = rotate(node);\n // 서브트리의 루트 노드를 반환한다\n return node;\n}\n</code></pre> avl_tree.rs<pre><code>/* 노드 삭제 */\nfn remove(&amp;self, val: i32) {\n Self::remove_helper(self.root.clone(), val);\n}\n\n/* 노드의 재귀적 삭제(보조 메서드) */\nfn remove_helper(node: OptionTreeNodeRc, val: i32) -&gt; OptionTreeNodeRc {\n match node {\n Some(mut node) =&gt; {\n /* 1. 노드를 찾아 삭제한다 */\n if val &lt; node.borrow().val {\n let left = node.borrow().left.clone();\n node.borrow_mut().left = Self::remove_helper(left, val);\n } else if val &gt; node.borrow().val {\n let right = node.borrow().right.clone();\n node.borrow_mut().right = Self::remove_helper(right, val);\n } else if node.borrow().left.is_none() || node.borrow().right.is_none() {\n let child = if node.borrow().left.is_some() {\n node.borrow().left.clone()\n } else {\n node.borrow().right.clone()\n };\n match child {\n // 자식 노드 수 = 0이면 node를 바로 삭제하고 반환한다\n None =&gt; {\n return None;\n }\n // 자식 노드 수 = 1이면 node를 바로 삭제한다\n Some(child) =&gt; node = child,\n }\n } else {\n // 자식 노드 수 = 2이면 중위 순회의 다음 노드를 삭제하고, 그 노드로 현재 노드를 대체한다\n let mut temp = node.borrow().right.clone().unwrap();\n loop {\n let temp_left = temp.borrow().left.clone();\n if temp_left.is_none() {\n break;\n }\n temp = temp_left.unwrap();\n }\n let right = node.borrow().right.clone();\n node.borrow_mut().right = Self::remove_helper(right, temp.borrow().val);\n node.borrow_mut().val = temp.borrow().val;\n }\n Self::update_height(Some(node.clone())); // 노드 높이를 갱신한다\n\n /* 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다 */\n node = Self::rotate(Some(node)).unwrap();\n // 서브트리의 루트 노드를 반환한다\n Some(node)\n }\n None =&gt; None,\n }\n}\n</code></pre> avl_tree.c<pre><code>/* 노드 삭제 */\n// stdio.h를 포함했으므로 여기서는 remove라는 이름을 사용할 수 없다\nvoid removeItem(AVLTree *tree, int val) {\n TreeNode *root = removeHelper(tree-&gt;root, val);\n}\n\n/* 노드의 재귀적 삭제(보조 함수) */\nTreeNode *removeHelper(TreeNode *node, int val) {\n TreeNode *child, *grandChild;\n if (node == NULL) {\n return NULL;\n }\n /* 1. 노드를 찾아 삭제한다 */\n if (val &lt; node-&gt;val) {\n node-&gt;left = removeHelper(node-&gt;left, val);\n } else if (val &gt; node-&gt;val) {\n node-&gt;right = removeHelper(node-&gt;right, val);\n } else {\n if (node-&gt;left == NULL || node-&gt;right == NULL) {\n child = node-&gt;left;\n if (node-&gt;right != NULL) {\n child = node-&gt;right;\n }\n // 자식 노드 수 = 0이면 node를 바로 삭제하고 반환한다\n if (child == NULL) {\n return NULL;\n } else {\n // 자식 노드 수 = 1이면 node를 바로 삭제한다\n node = child;\n }\n } else {\n // 자식 노드 수 = 2이면 중위 순회의 다음 노드를 삭제하고, 그 노드로 현재 노드를 대체한다\n TreeNode *temp = node-&gt;right;\n while (temp-&gt;left != NULL) {\n temp = temp-&gt;left;\n }\n int tempVal = temp-&gt;val;\n node-&gt;right = removeHelper(node-&gt;right, temp-&gt;val);\n node-&gt;val = tempVal;\n }\n }\n // 노드 높이를 갱신한다\n updateHeight(node);\n /* 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다 */\n node = rotate(node);\n // 서브트리의 루트 노드를 반환한다\n return node;\n}\n</code></pre> avl_tree.kt<pre><code>/* 노드 삭제 */\nfun remove(_val: Int) {\n root = removeHelper(root, _val)\n}\n\n/* 노드의 재귀적 삭제(보조 메서드) */\nfun removeHelper(n: TreeNode?, _val: Int): TreeNode? {\n var node = n ?: return null\n /* 1. 노드를 찾아 삭제한다 */\n if (_val &lt; node._val)\n node.left = removeHelper(node.left, _val)\n else if (_val &gt; node._val)\n node.right = removeHelper(node.right, _val)\n else {\n if (node.left == null || node.right == null) {\n val child = if (node.left != null)\n node.left\n else\n node.right\n // 자식 노드 수 = 0이면 node를 바로 삭제하고 반환한다\n if (child == null)\n return null\n // 자식 노드 수 = 1이면 node를 바로 삭제한다\n else\n node = child\n } else {\n // 자식 노드 수 = 2이면 중위 순회의 다음 노드를 삭제하고, 그 노드로 현재 노드를 대체한다\n var temp = node.right\n while (temp!!.left != null) {\n temp = temp.left\n }\n node.right = removeHelper(node.right, temp._val)\n node._val = temp._val\n }\n }\n updateHeight(node) // 노드 높이를 갱신한다\n /* 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다 */\n node = rotate(node)\n // 서브트리의 루트 노드를 반환한다\n return node\n}\n</code></pre> avl_tree.rb<pre><code>### 노드 삭제 ###\ndef remove(val)\n @root = remove_helper(@root, val)\nend\n\n### 노드의 재귀적 삭제(보조 메서드)###\ndef remove_helper(node, val)\n return if node.nil?\n # 1. 노드를 찾아 삭제한다\n if val &lt; node.val\n node.left = remove_helper(node.left, val)\n elsif val &gt; node.val\n node.right = remove_helper(node.right, val)\n else\n if node.left.nil? || node.right.nil?\n child = node.left || node.right\n # 자식 노드 수 = 0이면 node를 바로 삭제하고 반환한다\n return if child.nil?\n # 자식 노드 수 = 1이면 node를 바로 삭제한다\n node = child\n else\n # 자식 노드 수 = 2이면 중위 순회의 다음 노드를 삭제하고, 그 노드로 현재 노드를 대체한다\n temp = node.right\n while !temp.left.nil?\n temp = temp.left\n end\n node.right = remove_helper(node.right, temp.val)\n node.val = temp.val\n end\n end\n # 노드 높이를 갱신한다\n update_height(node)\n # 2. 회전 연산을 수행하여 해당 서브트리의 균형을 복원한다\n rotate(node)\nend\n</code></pre>","path":["제 7 장   트리","7.5   AVL 트리 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#3_1","level":3,"title":"3.   노드 탐색","text":"<p>AVL 트리의 노드 탐색은 이진 탐색 트리와 같으므로 다시 설명하지 않는다.</p>","path":["제 7 장   트리","7.5   AVL 트리 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#754-avl","level":2,"title":"7.5.4   AVL 트리의 대표적인 응용","text":"<ul> <li>대규모 데이터를 구성하고 저장하며, 탐색이 잦고 추가와 삭제가 드문 상황에 적합하다.</li> <li>데이터베이스의 인덱스 시스템을 구축하는 데 사용한다.</li> <li>레드-블랙 트리도 대표적인 균형 이진 탐색 트리다. AVL 트리보다 균형 조건이 느슨하여 삽입과 삭제에 필요한 회전 수가 적고, 노드 추가와 삭제의 평균 효율이 더 높다.</li> </ul>","path":["제 7 장   트리","7.5   AVL 트리 *"],"tags":[]},{"location":"chapter_tree/binary_search_tree/","level":1,"title":"7.4   이진 탐색 트리","text":"<p>아래 그림(7-16)의 이진 탐색 트리(binary search tree)는 다음 조건을 만족한다.</p> <ol> <li>루트에 대해 왼쪽 서브트리의 모든 노드 값 \\(&lt;\\) 루트 값 \\(&lt;\\) 오른쪽 서브트리의 모든 노드 값이다.</li> <li>임의의 노드의 왼쪽과 오른쪽 서브트리도 이진 탐색 트리로, 조건 <code>1.</code>을 만족한다.</li> </ol> <p></p> <p> 그림 7-16   이진 탐색 트리 </p>","path":["제 7 장   트리","7.4   이진 탐색 트리"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#741","level":2,"title":"7.4.1   이진 탐색 트리의 연산","text":"<p>이진 탐색 트리를 <code>BinarySearchTree</code> 클래스로 묶고, 루트 노드를 가리키는 멤버 변수 <code>root</code>를 정의한다.</p>","path":["제 7 장   트리","7.4   이진 탐색 트리"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#1","level":3,"title":"1.   노드 탐색","text":"<p>목표 노드의 값 <code>num</code>이 주어지면 이진 탐색 트리의 성질로 찾을 수 있다. 아래 그림(7-17)과 같이 노드 <code>cur</code>를 두고 루트 <code>root</code>에서 출발하여 <code>cur.val</code>과 <code>num</code>의 대소 관계를 반복해서 비교한다.</p> <ul> <li><code>cur.val &lt; num</code>이면 목표 노드는 <code>cur</code>의 오른쪽 서브트리에 있으므로 <code>cur = cur.right</code>를 수행한다.</li> <li><code>cur.val &gt; num</code>이면 목표 노드는 <code>cur</code>의 왼쪽 서브트리에 있으므로 <code>cur = cur.left</code>를 수행한다.</li> <li><code>cur.val = num</code>이면 목표 노드를 찾았으므로 반복문을 빠져나와 해당 노드를 반환한다.</li> </ul> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt; <p></p> <p></p> <p></p> <p></p> <p> 그림 7-17   이진 탐색 트리의 노드 탐색 예시 </p> <p>이진 탐색 트리의 탐색 원리는 이진 탐색 알고리즘과 같으며, 매번 경우의 절반을 제외한다. 반복 횟수는 최대 트리 높이이므로 트리가 균형을 이루면 \\(O(\\log n)\\) 시간이 걸린다. 예시 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py<pre><code>def search(self, num: int) -&gt; TreeNode | None:\n \"\"\"노드 탐색\"\"\"\n cur = self._root\n # 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while cur is not None:\n # 목표 노드는 cur의 오른쪽 서브트리에 있다\n if cur.val &lt; num:\n cur = cur.right\n # 목표 노드는 cur의 왼쪽 서브트리에 있다\n elif cur.val &gt; num:\n cur = cur.left\n # 목표 노드를 찾았으므로 반복문을 종료한다\n else:\n break\n return cur\n</code></pre> binary_search_tree.cpp<pre><code>/* 노드 탐색 */\nTreeNode *search(int num) {\n TreeNode *cur = root;\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while (cur != nullptr) {\n // 목표 노드는 cur의 오른쪽 서브트리에 있다\n if (cur-&gt;val &lt; num)\n cur = cur-&gt;right;\n // 목표 노드는 cur의 왼쪽 서브트리에 있다\n else if (cur-&gt;val &gt; num)\n cur = cur-&gt;left;\n // 목표 노드를 찾았으므로 반복문을 종료한다\n else\n break;\n }\n // 목표 노드를 반환한다\n return cur;\n}\n</code></pre> binary_search_tree.java<pre><code>/* 노드 탐색 */\nTreeNode search(int num) {\n TreeNode cur = root;\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while (cur != null) {\n // 목표 노드는 cur의 오른쪽 서브트리에 있다\n if (cur.val &lt; num)\n cur = cur.right;\n // 목표 노드는 cur의 왼쪽 서브트리에 있다\n else if (cur.val &gt; num)\n cur = cur.left;\n // 목표 노드를 찾았으므로 반복문을 종료한다\n else\n break;\n }\n // 목표 노드를 반환한다\n return cur;\n}\n</code></pre> binary_search_tree.cs<pre><code>/* 노드 탐색 */\nTreeNode? Search(int num) {\n TreeNode? cur = root;\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while (cur != null) {\n // 목표 노드는 cur의 오른쪽 서브트리에 있다\n if (cur.val &lt; num) cur =\n cur.right;\n // 목표 노드는 cur의 왼쪽 서브트리에 있다\n else if (cur.val &gt; num)\n cur = cur.left;\n // 목표 노드를 찾았으므로 반복문을 종료한다\n else\n break;\n }\n // 목표 노드를 반환한다\n return cur;\n}\n</code></pre> binary_search_tree.go<pre><code>/* 노드 탐색 */\nfunc (bst *binarySearchTree) search(num int) *TreeNode {\n node := bst.root\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n for node != nil {\n if node.Val.(int) &lt; num {\n // 목표 노드는 cur의 오른쪽 서브트리에 있다\n node = node.Right\n } else if node.Val.(int) &gt; num {\n // 목표 노드는 cur의 왼쪽 서브트리에 있다\n node = node.Left\n } else {\n // 목표 노드를 찾았으므로 반복문을 종료한다\n break\n }\n }\n // 목표 노드를 반환한다\n return node\n}\n</code></pre> binary_search_tree.swift<pre><code>/* 노드 탐색 */\nfunc search(num: Int) -&gt; TreeNode? {\n var cur = root\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while cur != nil {\n // 목표 노드는 cur의 오른쪽 서브트리에 있다\n if cur!.val &lt; num {\n cur = cur?.right\n }\n // 목표 노드는 cur의 왼쪽 서브트리에 있다\n else if cur!.val &gt; num {\n cur = cur?.left\n }\n // 목표 노드를 찾았으므로 반복문을 종료한다\n else {\n break\n }\n }\n // 목표 노드를 반환한다\n return cur\n}\n</code></pre> binary_search_tree.js<pre><code>/* 노드 탐색 */\nsearch(num) {\n let cur = this.root;\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while (cur !== null) {\n // 목표 노드는 cur의 오른쪽 서브트리에 있다\n if (cur.val &lt; num) cur = cur.right;\n // 목표 노드는 cur의 왼쪽 서브트리에 있다\n else if (cur.val &gt; num) cur = cur.left;\n // 목표 노드를 찾았으므로 반복문을 종료한다\n else break;\n }\n // 목표 노드를 반환한다\n return cur;\n}\n</code></pre> binary_search_tree.ts<pre><code>/* 노드 탐색 */\nsearch(num: number): TreeNode | null {\n let cur = this.root;\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while (cur !== null) {\n // 목표 노드는 cur의 오른쪽 서브트리에 있다\n if (cur.val &lt; num) cur = cur.right;\n // 목표 노드는 cur의 왼쪽 서브트리에 있다\n else if (cur.val &gt; num) cur = cur.left;\n // 목표 노드를 찾았으므로 반복문을 종료한다\n else break;\n }\n // 목표 노드를 반환한다\n return cur;\n}\n</code></pre> binary_search_tree.dart<pre><code>/* 노드 탐색 */\nTreeNode? search(int _num) {\n TreeNode? cur = _root;\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while (cur != null) {\n // 목표 노드는 cur의 오른쪽 서브트리에 있다\n if (cur.val &lt; _num)\n cur = cur.right;\n // 목표 노드는 cur의 왼쪽 서브트리에 있다\n else if (cur.val &gt; _num)\n cur = cur.left;\n // 목표 노드를 찾았으므로 반복문을 종료한다\n else\n break;\n }\n // 목표 노드를 반환한다\n return cur;\n}\n</code></pre> binary_search_tree.rs<pre><code>/* 노드 탐색 */\npub fn search(&amp;self, num: i32) -&gt; OptionTreeNodeRc {\n let mut cur = self.root.clone();\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while let Some(node) = cur.clone() {\n match num.cmp(&amp;node.borrow().val) {\n // 목표 노드는 cur의 오른쪽 서브트리에 있다\n Ordering::Greater =&gt; cur = node.borrow().right.clone(),\n // 목표 노드는 cur의 왼쪽 서브트리에 있다\n Ordering::Less =&gt; cur = node.borrow().left.clone(),\n // 목표 노드를 찾았으므로 반복문을 종료한다\n Ordering::Equal =&gt; break,\n }\n }\n\n // 목표 노드를 반환한다\n cur\n}\n</code></pre> binary_search_tree.c<pre><code>/* 노드 탐색 */\nTreeNode *search(BinarySearchTree *bst, int num) {\n TreeNode *cur = bst-&gt;root;\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while (cur != NULL) {\n if (cur-&gt;val &lt; num) {\n // 목표 노드는 cur의 오른쪽 서브트리에 있다\n cur = cur-&gt;right;\n } else if (cur-&gt;val &gt; num) {\n // 목표 노드는 cur의 왼쪽 서브트리에 있다\n cur = cur-&gt;left;\n } else {\n // 목표 노드를 찾았으므로 반복문을 종료한다\n break;\n }\n }\n // 목표 노드를 반환한다\n return cur;\n}\n</code></pre> binary_search_tree.kt<pre><code>/* 노드 탐색 */\nfun search(num: Int): TreeNode? {\n var cur = root\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while (cur != null) {\n // 목표 노드는 cur의 오른쪽 서브트리에 있다\n cur = if (cur._val &lt; num)\n cur.right\n // 목표 노드는 cur의 왼쪽 서브트리에 있다\n else if (cur._val &gt; num)\n cur.left\n // 목표 노드를 찾았으므로 반복문을 종료한다\n else\n break\n }\n // 목표 노드를 반환한다\n return cur\n}\n</code></pre> binary_search_tree.rb<pre><code>### 노드 탐색 ###\ndef search(num)\n cur = @root\n\n # 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while !cur.nil?\n # 목표 노드는 cur의 오른쪽 서브트리에 있다\n if cur.val &lt; num\n cur = cur.right\n # 목표 노드는 cur의 왼쪽 서브트리에 있다\n elsif cur.val &gt; num\n cur = cur.left\n # 목표 노드를 찾았으므로 반복문을 종료한다\n else\n break\n end\n end\n\n cur\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 7 장   트리","7.4   이진 탐색 트리"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#2","level":3,"title":"2.   노드 삽입","text":"<p>삽입할 원소 <code>num</code>이 주어졌을 때 '왼쪽 서브트리 &lt; 루트 &lt; 오른쪽 서브트리'라는 성질을 유지하려면 아래 그림(7-18)과 같이 삽입한다.</p> <ol> <li>삽입 위치 찾기: 탐색과 비슷하게 루트에서 출발하여 현재 노드 값과 <code>num</code>의 대소 관계에 따라 아래로 탐색한다. 리프를 지나 <code>None</code>에 도달하면 반복문을 종료한다.</li> <li>그 위치에 노드 삽입: 값이 <code>num</code>인 노드를 초기화하여 <code>None</code> 자리에 놓는다.</li> </ol> <p></p> <p> 그림 7-18   이진 탐색 트리에 노드 삽입 </p> <p>구현 시 다음 두 가지에 주의하자.</p> <ul> <li>중복 노드가 있으면 정의에 어긋나므로 이진 탐색 트리는 중복을 허용하지 않는다. 삽입할 노드가 이미 있으면 삽입하지 않고 바로 반환한다.</li> <li>삽입을 위해 노드 <code>pre</code>에 직전 반복의 노드를 저장해야 한다. 그러면 <code>None</code>에 도달했을 때 그 부모를 얻어 삽입을 완료할 수 있다.</li> </ul> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py<pre><code>def insert(self, num: int):\n \"\"\"노드 삽입\"\"\"\n # 트리가 비어 있으면 루트 노드를 초기화한다\n if self._root is None:\n self._root = TreeNode(num)\n return\n # 반복하여 탐색하다 리프 노드를 지나면 종료한다\n cur, pre = self._root, None\n while cur is not None:\n # 중복 노드를 찾으면 바로 반환한다\n if cur.val == num:\n return\n pre = cur\n # 삽입 위치는 cur의 오른쪽 서브트리에 있다\n if cur.val &lt; num:\n cur = cur.right\n # 삽입 위치는 cur의 왼쪽 서브트리에 있다\n else:\n cur = cur.left\n # 노드 삽입\n node = TreeNode(num)\n if pre.val &lt; num:\n pre.right = node\n else:\n pre.left = node\n</code></pre> binary_search_tree.cpp<pre><code>/* 노드 삽입 */\nvoid insert(int num) {\n // 트리가 비어 있으면 루트 노드를 초기화한다\n if (root == nullptr) {\n root = new TreeNode(num);\n return;\n }\n TreeNode *cur = root, *pre = nullptr;\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while (cur != nullptr) {\n // 중복 노드를 찾으면 바로 반환한다\n if (cur-&gt;val == num)\n return;\n pre = cur;\n // 삽입 위치는 cur의 오른쪽 서브트리에 있다\n if (cur-&gt;val &lt; num)\n cur = cur-&gt;right;\n // 삽입 위치는 cur의 왼쪽 서브트리에 있다\n else\n cur = cur-&gt;left;\n }\n // 노드 삽입\n TreeNode *node = new TreeNode(num);\n if (pre-&gt;val &lt; num)\n pre-&gt;right = node;\n else\n pre-&gt;left = node;\n}\n</code></pre> binary_search_tree.java<pre><code>/* 노드 삽입 */\nvoid insert(int num) {\n // 트리가 비어 있으면 루트 노드를 초기화한다\n if (root == null) {\n root = new TreeNode(num);\n return;\n }\n TreeNode cur = root, pre = null;\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while (cur != null) {\n // 중복 노드를 찾으면 바로 반환한다\n if (cur.val == num)\n return;\n pre = cur;\n // 삽입 위치는 cur의 오른쪽 서브트리에 있다\n if (cur.val &lt; num)\n cur = cur.right;\n // 삽입 위치는 cur의 왼쪽 서브트리에 있다\n else\n cur = cur.left;\n }\n // 노드 삽입\n TreeNode node = new TreeNode(num);\n if (pre.val &lt; num)\n pre.right = node;\n else\n pre.left = node;\n}\n</code></pre> binary_search_tree.cs<pre><code>/* 노드 삽입 */\nvoid Insert(int num) {\n // 트리가 비어 있으면 루트 노드를 초기화한다\n if (root == null) {\n root = new TreeNode(num);\n return;\n }\n TreeNode? cur = root, pre = null;\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while (cur != null) {\n // 중복 노드를 찾으면 바로 반환한다\n if (cur.val == num)\n return;\n pre = cur;\n // 삽입 위치는 cur의 오른쪽 서브트리에 있다\n if (cur.val &lt; num)\n cur = cur.right;\n // 삽입 위치는 cur의 왼쪽 서브트리에 있다\n else\n cur = cur.left;\n }\n\n // 노드 삽입\n TreeNode node = new(num);\n if (pre != null) {\n if (pre.val &lt; num)\n pre.right = node;\n else\n pre.left = node;\n }\n}\n</code></pre> binary_search_tree.go<pre><code>/* 노드 삽입 */\nfunc (bst *binarySearchTree) insert(num int) {\n cur := bst.root\n // 트리가 비어 있으면 루트 노드를 초기화한다\n if cur == nil {\n bst.root = NewTreeNode(num)\n return\n }\n // 삽입할 노드의 부모가 될 노드 위치\n var pre *TreeNode = nil\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n for cur != nil {\n if cur.Val == num {\n return\n }\n pre = cur\n if cur.Val.(int) &lt; num {\n cur = cur.Right\n } else {\n cur = cur.Left\n }\n }\n // 노드 삽입\n node := NewTreeNode(num)\n if pre.Val.(int) &lt; num {\n pre.Right = node\n } else {\n pre.Left = node\n }\n}\n</code></pre> binary_search_tree.swift<pre><code>/* 노드 삽입 */\nfunc insert(num: Int) {\n // 트리가 비어 있으면 루트 노드를 초기화한다\n if root == nil {\n root = TreeNode(x: num)\n return\n }\n var cur = root\n var pre: TreeNode?\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while cur != nil {\n // 중복 노드를 찾으면 바로 반환한다\n if cur!.val == num {\n return\n }\n pre = cur\n // 삽입 위치는 cur의 오른쪽 서브트리에 있다\n if cur!.val &lt; num {\n cur = cur?.right\n }\n // 삽입 위치는 cur의 왼쪽 서브트리에 있다\n else {\n cur = cur?.left\n }\n }\n // 노드 삽입\n let node = TreeNode(x: num)\n if pre!.val &lt; num {\n pre?.right = node\n } else {\n pre?.left = node\n }\n}\n</code></pre> binary_search_tree.js<pre><code>/* 노드 삽입 */\ninsert(num) {\n // 트리가 비어 있으면 루트 노드를 초기화한다\n if (this.root === null) {\n this.root = new TreeNode(num);\n return;\n }\n let cur = this.root,\n pre = null;\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while (cur !== null) {\n // 중복 노드를 찾으면 바로 반환한다\n if (cur.val === num) return;\n pre = cur;\n // 삽입 위치는 cur의 오른쪽 서브트리에 있다\n if (cur.val &lt; num) cur = cur.right;\n // 삽입 위치는 cur의 왼쪽 서브트리에 있다\n else cur = cur.left;\n }\n // 노드 삽입\n const node = new TreeNode(num);\n if (pre.val &lt; num) pre.right = node;\n else pre.left = node;\n}\n</code></pre> binary_search_tree.ts<pre><code>/* 노드 삽입 */\ninsert(num: number): void {\n // 트리가 비어 있으면 루트 노드를 초기화한다\n if (this.root === null) {\n this.root = new TreeNode(num);\n return;\n }\n let cur: TreeNode | null = this.root,\n pre: TreeNode | null = null;\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while (cur !== null) {\n // 중복 노드를 찾으면 바로 반환한다\n if (cur.val === num) return;\n pre = cur;\n // 삽입 위치는 cur의 오른쪽 서브트리에 있다\n if (cur.val &lt; num) cur = cur.right;\n // 삽입 위치는 cur의 왼쪽 서브트리에 있다\n else cur = cur.left;\n }\n // 노드 삽입\n const node = new TreeNode(num);\n if (pre!.val &lt; num) pre!.right = node;\n else pre!.left = node;\n}\n</code></pre> binary_search_tree.dart<pre><code>/* 노드 삽입 */\nvoid insert(int _num) {\n // 트리가 비어 있으면 루트 노드를 초기화한다\n if (_root == null) {\n _root = TreeNode(_num);\n return;\n }\n TreeNode? cur = _root;\n TreeNode? pre = null;\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while (cur != null) {\n // 중복 노드를 찾으면 바로 반환한다\n if (cur.val == _num) return;\n pre = cur;\n // 삽입 위치는 cur의 오른쪽 서브트리에 있다\n if (cur.val &lt; _num)\n cur = cur.right;\n // 삽입 위치는 cur의 왼쪽 서브트리에 있다\n else\n cur = cur.left;\n }\n // 노드 삽입\n TreeNode? node = TreeNode(_num);\n if (pre!.val &lt; _num)\n pre.right = node;\n else\n pre.left = node;\n}\n</code></pre> binary_search_tree.rs<pre><code>/* 노드 삽입 */\npub fn insert(&amp;mut self, num: i32) {\n // 트리가 비어 있으면 루트 노드를 초기화한다\n if self.root.is_none() {\n self.root = Some(TreeNode::new(num));\n return;\n }\n let mut cur = self.root.clone();\n let mut pre = None;\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while let Some(node) = cur.clone() {\n match num.cmp(&amp;node.borrow().val) {\n // 중복 노드를 찾으면 바로 반환한다\n Ordering::Equal =&gt; return,\n // 삽입 위치는 cur의 오른쪽 서브트리에 있다\n Ordering::Greater =&gt; {\n pre = cur.clone();\n cur = node.borrow().right.clone();\n }\n // 삽입 위치는 cur의 왼쪽 서브트리에 있다\n Ordering::Less =&gt; {\n pre = cur.clone();\n cur = node.borrow().left.clone();\n }\n }\n }\n // 노드 삽입\n let pre = pre.unwrap();\n let node = Some(TreeNode::new(num));\n if num &gt; pre.borrow().val {\n pre.borrow_mut().right = node;\n } else {\n pre.borrow_mut().left = node;\n }\n}\n</code></pre> binary_search_tree.c<pre><code>/* 노드 삽입 */\nvoid insert(BinarySearchTree *bst, int num) {\n // 트리가 비어 있으면 루트 노드를 초기화한다\n if (bst-&gt;root == NULL) {\n bst-&gt;root = newTreeNode(num);\n return;\n }\n TreeNode *cur = bst-&gt;root, *pre = NULL;\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while (cur != NULL) {\n // 중복 노드를 찾으면 바로 반환한다\n if (cur-&gt;val == num) {\n return;\n }\n pre = cur;\n if (cur-&gt;val &lt; num) {\n // 삽입 위치는 cur의 오른쪽 서브트리에 있다\n cur = cur-&gt;right;\n } else {\n // 삽입 위치는 cur의 왼쪽 서브트리에 있다\n cur = cur-&gt;left;\n }\n }\n // 노드 삽입\n TreeNode *node = newTreeNode(num);\n if (pre-&gt;val &lt; num) {\n pre-&gt;right = node;\n } else {\n pre-&gt;left = node;\n }\n}\n</code></pre> binary_search_tree.kt<pre><code>/* 노드 삽입 */\nfun insert(num: Int) {\n // 트리가 비어 있으면 루트 노드를 초기화한다\n if (root == null) {\n root = TreeNode(num)\n return\n }\n var cur = root\n var pre: TreeNode? = null\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while (cur != null) {\n // 중복 노드를 찾으면 바로 반환한다\n if (cur._val == num)\n return\n pre = cur\n // 삽입 위치는 cur의 오른쪽 서브트리에 있다\n cur = if (cur._val &lt; num)\n cur.right\n // 삽입 위치는 cur의 왼쪽 서브트리에 있다\n else\n cur.left\n }\n // 노드 삽입\n val node = TreeNode(num)\n if (pre?._val!! &lt; num)\n pre.right = node\n else\n pre.left = node\n}\n</code></pre> binary_search_tree.rb<pre><code>### 노드 삽입 ###\ndef insert(num)\n # 트리가 비어 있으면 루트 노드를 초기화한다\n if @root.nil?\n @root = TreeNode.new(num)\n return\n end\n\n # 반복하여 탐색하다 리프 노드를 지나면 종료한다\n cur, pre = @root, nil\n while !cur.nil?\n # 중복 노드를 찾으면 바로 반환한다\n return if cur.val == num\n\n pre = cur\n # 삽입 위치는 cur의 오른쪽 서브트리에 있다\n if cur.val &lt; num\n cur = cur.right\n # 삽입 위치는 cur의 왼쪽 서브트리에 있다\n else\n cur = cur.left\n end\n end\n\n # 노드 삽입\n node = TreeNode.new(num)\n if pre.val &lt; num\n pre.right = node\n else\n pre.left = node\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>노드 탐색과 마찬가지로 삽입에는 \\(O(\\log n)\\) 시간이 걸린다.</p>","path":["제 7 장   트리","7.4   이진 탐색 트리"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#3","level":3,"title":"3.   노드 삭제","text":"<p>먼저 트리에서 목표 노드를 찾은 뒤 삭제한다. 삽입과 마찬가지로 삭제 후에도 '왼쪽 서브트리 &lt; 루트 &lt; 오른쪽 서브트리'라는 성질을 유지해야 한다. 따라서 목표 노드의 자식 수가 0, 1, 2인 세 경우로 나누어 삭제한다.</p> <p>아래 그림(7-19)과 같이 삭제할 노드의 차수가 \\(0\\)이면 리프 노드이므로 바로 삭제할 수 있다.</p> <p></p> <p> 그림 7-19   이진 탐색 트리의 노드 삭제: 차수 0 </p> <p>아래 그림(7-20)과 같이 차수가 \\(1\\)이면 삭제할 노드를 그 자식으로 바꾸면 된다.</p> <p></p> <p> 그림 7-20   이진 탐색 트리의 노드 삭제: 차수 1 </p> <p>차수가 \\(2\\)이면 바로 삭제할 수 없어 다른 노드로 대체해야 한다. '왼쪽 서브트리 \\(&lt;\\) 루트 \\(&lt;\\) 오른쪽 서브트리' 성질을 유지하려면 오른쪽 서브트리의 최소 노드 또는 왼쪽 서브트리의 최대 노드로 대체할 수 있다.</p> <p>오른쪽 서브트리의 최소 노드(중위 순회에서 다음 노드)를 선택하면 삭제 과정은 아래 그림(7-21)과 같다.</p> <ol> <li>삭제할 노드의 '중위 순회 순서'상 다음 노드를 찾아 <code>tmp</code>라고 한다.</li> <li>삭제할 노드의 값을 <code>tmp</code>의 값으로 덮어쓰고, 트리에서 노드 <code>tmp</code>를 재귀적으로 삭제한다.</li> </ol> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt; <p></p> <p></p> <p></p> <p></p> <p> 그림 7-21   이진 탐색 트리의 노드 삭제: 차수 2 </p> <p>노드 삭제에도 \\(O(\\log n)\\) 시간이 걸린다. 삭제할 노드를 찾는 데 \\(O(\\log n)\\), 중위 순회의 후속 노드를 구하는 데 \\(O(\\log n)\\) 시간이 든다. 예시 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py<pre><code>def remove(self, num: int):\n \"\"\"노드 삭제\"\"\"\n # 트리가 비어 있으면 바로 반환하여 종료한다\n if self._root is None:\n return\n # 반복하여 탐색하다 리프 노드를 지나면 종료한다\n cur, pre = self._root, None\n while cur is not None:\n # 삭제할 노드를 찾았으므로 반복문을 종료한다\n if cur.val == num:\n break\n pre = cur\n # 삭제할 노드는 cur의 오른쪽 서브트리에 있다\n if cur.val &lt; num:\n cur = cur.right\n # 삭제할 노드는 cur의 왼쪽 서브트리에 있다\n else:\n cur = cur.left\n # 삭제할 노드가 없으면 바로 반환한다\n if cur is None:\n return\n\n # 자식 노드 수 = 0 또는 1\n if cur.left is None or cur.right is None:\n # 자식 노드 수 = 0 / 1이면 child = null / 해당 자식 노드이다\n child = cur.left or cur.right\n # 노드 cur를 삭제한다\n if cur != self._root:\n if pre.left == cur:\n pre.left = child\n else:\n pre.right = child\n else:\n # 삭제할 노드가 루트 노드이면 루트 노드를 다시 지정한다\n self._root = child\n # 자식 노드 수 = 2\n else:\n # 중위 순회에서 cur의 다음 노드를 구한다\n tmp: TreeNode = cur.right\n while tmp.left is not None:\n tmp = tmp.left\n # 노드 tmp를 재귀적으로 삭제한다\n self.remove(tmp.val)\n # tmp로 cur를 덮어쓴다\n cur.val = tmp.val\n</code></pre> binary_search_tree.cpp<pre><code>/* 노드 삭제 */\nvoid remove(int num) {\n // 트리가 비어 있으면 바로 반환하여 종료한다\n if (root == nullptr)\n return;\n TreeNode *cur = root, *pre = nullptr;\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while (cur != nullptr) {\n // 삭제할 노드를 찾았으므로 반복문을 종료한다\n if (cur-&gt;val == num)\n break;\n pre = cur;\n // 삭제할 노드는 cur의 오른쪽 서브트리에 있다\n if (cur-&gt;val &lt; num)\n cur = cur-&gt;right;\n // 삭제할 노드는 cur의 왼쪽 서브트리에 있다\n else\n cur = cur-&gt;left;\n }\n // 삭제할 노드가 없으면 바로 반환한다\n if (cur == nullptr)\n return;\n // 자식 노드 수 = 0 또는 1\n if (cur-&gt;left == nullptr || cur-&gt;right == nullptr) {\n // 자식 노드 수가 0 / 1이면 child = nullptr / 해당 자식 노드다\n TreeNode *child = cur-&gt;left != nullptr ? cur-&gt;left : cur-&gt;right;\n // 노드 cur를 삭제한다\n if (cur != root) {\n if (pre-&gt;left == cur)\n pre-&gt;left = child;\n else\n pre-&gt;right = child;\n } else {\n // 삭제할 노드가 루트 노드이면 루트 노드를 다시 지정한다\n root = child;\n }\n // 메모리 해제\n delete cur;\n }\n // 자식 노드 수 = 2\n else {\n // 중위 순회에서 cur의 다음 노드를 구한다\n TreeNode *tmp = cur-&gt;right;\n while (tmp-&gt;left != nullptr) {\n tmp = tmp-&gt;left;\n }\n int tmpVal = tmp-&gt;val;\n // 노드 tmp를 재귀적으로 삭제한다\n remove(tmp-&gt;val);\n // tmp로 cur를 덮어쓴다\n cur-&gt;val = tmpVal;\n }\n}\n</code></pre> binary_search_tree.java<pre><code>/* 노드 삭제 */\nvoid remove(int num) {\n // 트리가 비어 있으면 바로 반환하여 종료한다\n if (root == null)\n return;\n TreeNode cur = root, pre = null;\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while (cur != null) {\n // 삭제할 노드를 찾았으므로 반복문을 종료한다\n if (cur.val == num)\n break;\n pre = cur;\n // 삭제할 노드는 cur의 오른쪽 서브트리에 있다\n if (cur.val &lt; num)\n cur = cur.right;\n // 삭제할 노드는 cur의 왼쪽 서브트리에 있다\n else\n cur = cur.left;\n }\n // 삭제할 노드가 없으면 바로 반환한다\n if (cur == null)\n return;\n // 자식 노드 수 = 0 또는 1\n if (cur.left == null || cur.right == null) {\n // 자식 노드 수 = 0 / 1이면 child = null / 해당 자식 노드이다\n TreeNode child = cur.left != null ? cur.left : cur.right;\n // 노드 cur를 삭제한다\n if (cur != root) {\n if (pre.left == cur)\n pre.left = child;\n else\n pre.right = child;\n } else {\n // 삭제할 노드가 루트 노드이면 루트 노드를 다시 지정한다\n root = child;\n }\n }\n // 자식 노드 수 = 2\n else {\n // 중위 순회에서 cur의 다음 노드를 구한다\n TreeNode tmp = cur.right;\n while (tmp.left != null) {\n tmp = tmp.left;\n }\n // 노드 tmp를 재귀적으로 삭제한다\n remove(tmp.val);\n // tmp로 cur를 덮어쓴다\n cur.val = tmp.val;\n }\n}\n</code></pre> binary_search_tree.cs<pre><code>/* 노드 삭제 */\nvoid Remove(int num) {\n // 트리가 비어 있으면 바로 반환하여 종료한다\n if (root == null)\n return;\n TreeNode? cur = root, pre = null;\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while (cur != null) {\n // 삭제할 노드를 찾았으므로 반복문을 종료한다\n if (cur.val == num)\n break;\n pre = cur;\n // 삭제할 노드는 cur의 오른쪽 서브트리에 있다\n if (cur.val &lt; num)\n cur = cur.right;\n // 삭제할 노드는 cur의 왼쪽 서브트리에 있다\n else\n cur = cur.left;\n }\n // 삭제할 노드가 없으면 바로 반환한다\n if (cur == null)\n return;\n // 자식 노드 수 = 0 또는 1\n if (cur.left == null || cur.right == null) {\n // 자식 노드 수 = 0 / 1이면 child = null / 해당 자식 노드이다\n TreeNode? child = cur.left ?? cur.right;\n // 노드 cur를 삭제한다\n if (cur != root) {\n if (pre!.left == cur)\n pre.left = child;\n else\n pre.right = child;\n } else {\n // 삭제할 노드가 루트 노드이면 루트 노드를 다시 지정한다\n root = child;\n }\n }\n // 자식 노드 수 = 2\n else {\n // 중위 순회에서 cur의 다음 노드를 구한다\n TreeNode? tmp = cur.right;\n while (tmp.left != null) {\n tmp = tmp.left;\n }\n // 노드 tmp를 재귀적으로 삭제한다\n Remove(tmp.val!.Value);\n // tmp로 cur를 덮어쓴다\n cur.val = tmp.val;\n }\n}\n</code></pre> binary_search_tree.go<pre><code>/* 노드 삭제 */\nfunc (bst *binarySearchTree) remove(num int) {\n cur := bst.root\n // 트리가 비어 있으면 바로 반환하여 종료한다\n if cur == nil {\n return\n }\n // 삭제할 노드의 부모 노드 위치\n var pre *TreeNode = nil\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n for cur != nil {\n if cur.Val == num {\n break\n }\n pre = cur\n if cur.Val.(int) &lt; num {\n // 삭제할 노드는 오른쪽 서브트리에 있다\n cur = cur.Right\n } else {\n // 삭제할 노드는 왼쪽 서브트리에 있다\n cur = cur.Left\n }\n }\n // 삭제할 노드가 없으면 바로 반환한다\n if cur == nil {\n return\n }\n // 자식 노드 수가 0 또는 1\n if cur.Left == nil || cur.Right == nil {\n var child *TreeNode = nil\n // 삭제할 노드의 자식 노드를 구한다\n if cur.Left != nil {\n child = cur.Left\n } else {\n child = cur.Right\n }\n // 노드 cur를 삭제한다\n if cur != bst.root {\n if pre.Left == cur {\n pre.Left = child\n } else {\n pre.Right = child\n }\n } else {\n // 삭제할 노드가 루트 노드이면 루트 노드를 다시 지정한다\n bst.root = child\n }\n // 자식 노드 수가 2\n } else {\n // 중위 순회에서 삭제할 노드 cur의 다음 노드를 구한다\n tmp := cur.Right\n for tmp.Left != nil {\n tmp = tmp.Left\n }\n // 노드 tmp를 재귀적으로 삭제한다\n bst.remove(tmp.Val.(int))\n // tmp로 cur를 덮어쓴다\n cur.Val = tmp.Val\n }\n}\n</code></pre> binary_search_tree.swift<pre><code>/* 노드 삭제 */\nfunc remove(num: Int) {\n // 트리가 비어 있으면 바로 반환하여 종료한다\n if root == nil {\n return\n }\n var cur = root\n var pre: TreeNode?\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while cur != nil {\n // 삭제할 노드를 찾았으므로 반복문을 종료한다\n if cur!.val == num {\n break\n }\n pre = cur\n // 삭제할 노드는 cur의 오른쪽 서브트리에 있다\n if cur!.val &lt; num {\n cur = cur?.right\n }\n // 삭제할 노드는 cur의 왼쪽 서브트리에 있다\n else {\n cur = cur?.left\n }\n }\n // 삭제할 노드가 없으면 바로 반환한다\n if cur == nil {\n return\n }\n // 자식 노드 수 = 0 또는 1\n if cur?.left == nil || cur?.right == nil {\n // 자식 노드 수 = 0 / 1이면 child = null / 해당 자식 노드이다\n let child = cur?.left ?? cur?.right\n // 노드 cur를 삭제한다\n if cur !== root {\n if pre?.left === cur {\n pre?.left = child\n } else {\n pre?.right = child\n }\n } else {\n // 삭제할 노드가 루트 노드이면 루트 노드를 다시 지정한다\n root = child\n }\n }\n // 자식 노드 수 = 2\n else {\n // 중위 순회에서 cur의 다음 노드를 구한다\n var tmp = cur?.right\n while tmp?.left != nil {\n tmp = tmp?.left\n }\n // 노드 tmp를 재귀적으로 삭제한다\n remove(num: tmp!.val)\n // tmp로 cur를 덮어쓴다\n cur?.val = tmp!.val\n }\n}\n</code></pre> binary_search_tree.js<pre><code>/* 노드 삭제 */\nremove(num) {\n // 트리가 비어 있으면 바로 반환하여 종료한다\n if (this.root === null) return;\n let cur = this.root,\n pre = null;\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while (cur !== null) {\n // 삭제할 노드를 찾았으므로 반복문을 종료한다\n if (cur.val === num) break;\n pre = cur;\n // 삭제할 노드는 cur의 오른쪽 서브트리에 있다\n if (cur.val &lt; num) cur = cur.right;\n // 삭제할 노드는 cur의 왼쪽 서브트리에 있다\n else cur = cur.left;\n }\n // 삭제할 노드가 없으면 바로 반환한다\n if (cur === null) return;\n // 자식 노드 수 = 0 또는 1\n if (cur.left === null || cur.right === null) {\n // 자식 노드 수 = 0 / 1이면 child = null / 해당 자식 노드이다\n const child = cur.left !== null ? cur.left : cur.right;\n // 노드 cur를 삭제한다\n if (cur !== this.root) {\n if (pre.left === cur) pre.left = child;\n else pre.right = child;\n } else {\n // 삭제할 노드가 루트 노드이면 루트 노드를 다시 지정한다\n this.root = child;\n }\n }\n // 자식 노드 수 = 2\n else {\n // 중위 순회에서 cur의 다음 노드를 구한다\n let tmp = cur.right;\n while (tmp.left !== null) {\n tmp = tmp.left;\n }\n // 노드 tmp를 재귀적으로 삭제한다\n this.remove(tmp.val);\n // tmp로 cur를 덮어쓴다\n cur.val = tmp.val;\n }\n}\n</code></pre> binary_search_tree.ts<pre><code>/* 노드 삭제 */\nremove(num: number): void {\n // 트리가 비어 있으면 바로 반환하여 종료한다\n if (this.root === null) return;\n let cur: TreeNode | null = this.root,\n pre: TreeNode | null = null;\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while (cur !== null) {\n // 삭제할 노드를 찾았으므로 반복문을 종료한다\n if (cur.val === num) break;\n pre = cur;\n // 삭제할 노드는 cur의 오른쪽 서브트리에 있다\n if (cur.val &lt; num) cur = cur.right;\n // 삭제할 노드는 cur의 왼쪽 서브트리에 있다\n else cur = cur.left;\n }\n // 삭제할 노드가 없으면 바로 반환한다\n if (cur === null) return;\n // 자식 노드 수 = 0 또는 1\n if (cur.left === null || cur.right === null) {\n // 자식 노드 수 = 0 / 1이면 child = null / 해당 자식 노드이다\n const child: TreeNode | null =\n cur.left !== null ? cur.left : cur.right;\n // 노드 cur를 삭제한다\n if (cur !== this.root) {\n if (pre!.left === cur) pre!.left = child;\n else pre!.right = child;\n } else {\n // 삭제할 노드가 루트 노드이면 루트 노드를 다시 지정한다\n this.root = child;\n }\n }\n // 자식 노드 수 = 2\n else {\n // 중위 순회에서 cur의 다음 노드를 구한다\n let tmp: TreeNode | null = cur.right;\n while (tmp!.left !== null) {\n tmp = tmp!.left;\n }\n // 노드 tmp를 재귀적으로 삭제한다\n this.remove(tmp!.val);\n // tmp로 cur를 덮어쓴다\n cur.val = tmp!.val;\n }\n}\n</code></pre> binary_search_tree.dart<pre><code>/* 노드 삭제 */\nvoid remove(int _num) {\n // 트리가 비어 있으면 바로 반환하여 종료한다\n if (_root == null) return;\n TreeNode? cur = _root;\n TreeNode? pre = null;\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while (cur != null) {\n // 삭제할 노드를 찾았으므로 반복문을 종료한다\n if (cur.val == _num) break;\n pre = cur;\n // 삭제할 노드는 cur의 오른쪽 서브트리에 있다\n if (cur.val &lt; _num)\n cur = cur.right;\n // 삭제할 노드는 cur의 왼쪽 서브트리에 있다\n else\n cur = cur.left;\n }\n // 삭제할 노드가 없으면 바로 반환한다\n if (cur == null) return;\n // 자식 노드 수 = 0 또는 1\n if (cur.left == null || cur.right == null) {\n // 자식 노드 수 = 0 / 1이면 child = null / 해당 자식 노드이다\n TreeNode? child = cur.left ?? cur.right;\n // 노드 cur를 삭제한다\n if (cur != _root) {\n if (pre!.left == cur)\n pre.left = child;\n else\n pre.right = child;\n } else {\n // 삭제할 노드가 루트 노드이면 루트 노드를 다시 지정한다\n _root = child;\n }\n } else {\n // 자식 노드 수 = 2\n // 중위 순회에서 cur의 다음 노드를 구한다\n TreeNode? tmp = cur.right;\n while (tmp!.left != null) {\n tmp = tmp.left;\n }\n // 노드 tmp를 재귀적으로 삭제한다\n remove(tmp.val);\n // tmp로 cur를 덮어쓴다\n cur.val = tmp.val;\n }\n}\n</code></pre> binary_search_tree.rs<pre><code>/* 노드 삭제 */\npub fn remove(&amp;mut self, num: i32) {\n // 트리가 비어 있으면 바로 반환하여 종료한다\n if self.root.is_none() {\n return;\n }\n let mut cur = self.root.clone();\n let mut pre = None;\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while let Some(node) = cur.clone() {\n match num.cmp(&amp;node.borrow().val) {\n // 삭제할 노드를 찾았으므로 반복문을 종료한다\n Ordering::Equal =&gt; break,\n // 삭제할 노드는 cur의 오른쪽 서브트리에 있다\n Ordering::Greater =&gt; {\n pre = cur.clone();\n cur = node.borrow().right.clone();\n }\n // 삭제할 노드는 cur의 왼쪽 서브트리에 있다\n Ordering::Less =&gt; {\n pre = cur.clone();\n cur = node.borrow().left.clone();\n }\n }\n }\n // 삭제할 노드가 없으면 바로 반환한다\n if cur.is_none() {\n return;\n }\n let cur = cur.unwrap();\n let (left_child, right_child) = (cur.borrow().left.clone(), cur.borrow().right.clone());\n match (left_child.clone(), right_child.clone()) {\n // 자식 노드 수 = 0 또는 1\n (None, None) | (Some(_), None) | (None, Some(_)) =&gt; {\n // 자식 노드 수가 0 / 1이면 child = nullptr / 해당 자식 노드다\n let child = left_child.or(right_child);\n let pre = pre.unwrap();\n // 노드 cur를 삭제한다\n if !Rc::ptr_eq(&amp;cur, self.root.as_ref().unwrap()) {\n let left = pre.borrow().left.clone();\n if left.is_some() &amp;&amp; Rc::ptr_eq(left.as_ref().unwrap(), &amp;cur) {\n pre.borrow_mut().left = child;\n } else {\n pre.borrow_mut().right = child;\n }\n } else {\n // 삭제할 노드가 루트 노드이면 루트 노드를 다시 지정한다\n self.root = child;\n }\n }\n // 자식 노드 수 = 2\n (Some(_), Some(_)) =&gt; {\n // 중위 순회에서 cur의 다음 노드를 구한다\n let mut tmp = cur.borrow().right.clone();\n while let Some(node) = tmp.clone() {\n if node.borrow().left.is_some() {\n tmp = node.borrow().left.clone();\n } else {\n break;\n }\n }\n let tmp_val = tmp.unwrap().borrow().val;\n // 노드 tmp를 재귀적으로 삭제한다\n self.remove(tmp_val);\n // tmp로 cur를 덮어쓴다\n cur.borrow_mut().val = tmp_val;\n }\n }\n}\n</code></pre> binary_search_tree.c<pre><code>/* 노드 삭제 */\n// stdio.h를 포함했으므로 여기서는 remove라는 이름을 사용할 수 없다\nvoid removeItem(BinarySearchTree *bst, int num) {\n // 트리가 비어 있으면 바로 반환하여 종료한다\n if (bst-&gt;root == NULL)\n return;\n TreeNode *cur = bst-&gt;root, *pre = NULL;\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while (cur != NULL) {\n // 삭제할 노드를 찾았으므로 반복문을 종료한다\n if (cur-&gt;val == num)\n break;\n pre = cur;\n if (cur-&gt;val &lt; num) {\n // 삭제할 노드가 root의 오른쪽 서브트리에 있다\n cur = cur-&gt;right;\n } else {\n // 삭제할 노드가 root의 왼쪽 서브트리에 있다\n cur = cur-&gt;left;\n }\n }\n // 삭제할 노드가 없으면 바로 반환한다\n if (cur == NULL)\n return;\n // 삭제할 노드에 자식 노드가 있는지 확인한다\n if (cur-&gt;left == NULL || cur-&gt;right == NULL) {\n /* 자식 노드 수 = 0 또는 1 */\n // 자식 노드 수가 0 / 1이면 child = nullptr / 해당 자식 노드다\n TreeNode *child = cur-&gt;left != NULL ? cur-&gt;left : cur-&gt;right;\n // 노드 cur를 삭제한다\n if (pre-&gt;left == cur) {\n pre-&gt;left = child;\n } else {\n pre-&gt;right = child;\n }\n // 메모리 해제\n free(cur);\n } else {\n /* 자식 노드 수 = 2 */\n // 중위 순회에서 cur의 다음 노드를 구한다\n TreeNode *tmp = cur-&gt;right;\n while (tmp-&gt;left != NULL) {\n tmp = tmp-&gt;left;\n }\n int tmpVal = tmp-&gt;val;\n // 노드 tmp를 재귀적으로 삭제한다\n removeItem(bst, tmp-&gt;val);\n // tmp로 cur를 덮어쓴다\n cur-&gt;val = tmpVal;\n }\n}\n</code></pre> binary_search_tree.kt<pre><code>/* 노드 삭제 */\nfun remove(num: Int) {\n // 트리가 비어 있으면 바로 반환하여 종료한다\n if (root == null)\n return\n var cur = root\n var pre: TreeNode? = null\n // 반복하여 탐색하다 리프 노드를 지나면 종료한다\n while (cur != null) {\n // 삭제할 노드를 찾았으므로 반복문을 종료한다\n if (cur._val == num)\n break\n pre = cur\n // 삭제할 노드는 cur의 오른쪽 서브트리에 있다\n cur = if (cur._val &lt; num)\n cur.right\n // 삭제할 노드는 cur의 왼쪽 서브트리에 있다\n else\n cur.left\n }\n // 삭제할 노드가 없으면 바로 반환한다\n if (cur == null)\n return\n // 자식 노드 수 = 0 또는 1\n if (cur.left == null || cur.right == null) {\n // 자식 노드 수 = 0 / 1이면 child = null / 해당 자식 노드이다\n val child = if (cur.left != null)\n cur.left\n else\n cur.right\n // 노드 cur를 삭제한다\n if (cur != root) {\n if (pre!!.left == cur)\n pre.left = child\n else\n pre.right = child\n } else {\n // 삭제할 노드가 루트 노드이면 루트 노드를 다시 지정한다\n root = child\n }\n // 자식 노드 수 = 2\n } else {\n // 중위 순회에서 cur의 다음 노드를 구한다\n var tmp = cur.right\n while (tmp!!.left != null) {\n tmp = tmp.left\n }\n // 노드 tmp를 재귀적으로 삭제한다\n remove(tmp._val)\n // tmp로 cur를 덮어쓴다\n cur._val = tmp._val\n }\n}\n</code></pre> binary_search_tree.rb<pre><code>### 노드 삭제 ###\ndef remove(num)\n # 트리가 비어 있으면 바로 반환하여 종료한다\n return if @root.nil?\n\n # 반복하여 탐색하다 리프 노드를 지나면 종료한다\n cur, pre = @root, nil\n while !cur.nil?\n # 삭제할 노드를 찾았으므로 반복문을 종료한다\n break if cur.val == num\n\n pre = cur\n # 삭제할 노드는 cur의 오른쪽 서브트리에 있다\n if cur.val &lt; num\n cur = cur.right\n # 삭제할 노드는 cur의 왼쪽 서브트리에 있다\n else\n cur = cur.left\n end\n end\n # 삭제할 노드가 없으면 바로 반환한다\n return if cur.nil?\n\n # 자식 노드 수 = 0 또는 1\n if cur.left.nil? || cur.right.nil?\n # 자식 노드 수 = 0 / 1이면 child = null / 해당 자식 노드이다\n child = cur.left || cur.right\n # 노드 cur를 삭제한다\n if cur != @root\n if pre.left == cur\n pre.left = child\n else\n pre.right = child\n end\n else\n # 삭제할 노드가 루트 노드이면 루트 노드를 다시 지정한다\n @root = child\n end\n # 자식 노드 수 = 2\n else\n # 중위 순회에서 cur의 다음 노드를 구한다\n tmp = cur.right\n while !tmp.left.nil?\n tmp = tmp.left\n end\n # 노드 tmp를 재귀적으로 삭제한다\n remove(tmp.val)\n # tmp로 cur를 덮어쓴다\n cur.val = tmp.val\n end\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 7 장   트리","7.4   이진 탐색 트리"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#4","level":3,"title":"4.   중위 순회 결과의 정렬 성질","text":"<p>아래 그림(7-22)과 같이 이진 트리의 중위 순회는 '왼쪽 \\(\\rightarrow\\) 루트 \\(\\rightarrow\\) 오른쪽' 순서이며, 이진 탐색 트리는 '왼쪽 자식 \\(&lt;\\) 루트 \\(&lt;\\) 오른쪽 자식'의 대소 관계를 만족한다.</p> <p>따라서 중위 순회에서는 항상 다음으로 작은 노드를 먼저 방문한다. 여기서 이진 탐색 트리의 중위 순회 결과는 오름차순이다라는 중요한 성질을 얻는다.</p> <p>이 성질을 이용하면 별도 정렬 없이 \\(O(n)\\) 시간만으로 정렬된 데이터를 얻을 수 있어 매우 효율적이다.</p> <p></p> <p> 그림 7-22   이진 탐색 트리의 중위 순회 결과 </p>","path":["제 7 장   트리","7.4   이진 탐색 트리"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#742","level":2,"title":"7.4.2   이진 탐색 트리의 효율","text":"<p>데이터 집합을 배열이나 이진 탐색 트리에 저장한다고 하자. 아래 표(7-2)를 보면 이진 탐색 트리의 각 연산은 로그 시간 복잡도로 안정적이고 효율적이다. 데이터 추가는 잦고 탐색과 삭제는 드문 상황에서만 배열이 더 효율적이다.</p> <p> 표 7-2   배열과 탐색 트리의 효율 비교 </p> 정렬되지 않은 배열 이진 탐색 트리 원소 탐색 \\(O(n)\\) \\(O(\\log n)\\) 원소 삽입 \\(O(1)\\) \\(O(\\log n)\\) 원소 삭제 \\(O(n)\\) \\(O(\\log n)\\) <p>이상적인 경우 이진 탐색 트리는 '균형'을 이루므로 어떤 노드든 \\(\\log n\\) 회의 반복 안에 찾을 수 있다.</p> <p>하지만 삽입과 삭제를 계속하다 보면 아래 그림(7-23)의 연결 리스트로 퇴화할 수 있다. 이때 각 연산의 시간 복잡도도 \\(O(n)\\)으로 저하된다.</p> <p></p> <p> 그림 7-23   이진 탐색 트리의 퇴화 </p>","path":["제 7 장   트리","7.4   이진 탐색 트리"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#743","level":2,"title":"7.4.3   이진 탐색 트리의 주요 응용","text":"<ul> <li>시스템의 다단계 인덱스로 사용하여 효율적인 탐색, 삽입, 삭제를 구현한다.</li> <li>일부 탐색 알고리즘의 기반 자료 구조로 사용한다.</li> <li>데이터 스트림을 저장하면서 정렬 상태를 유지한다.</li> </ul>","path":["제 7 장   트리","7.4   이진 탐색 트리"],"tags":[]},{"location":"chapter_tree/binary_tree/","level":1,"title":"7.1   이진 트리","text":"<p>이진 트리(binary tree)는 '조상'과 '후손' 사이의 파생 관계를 나타내는 비선형 자료 구조로, '하나를 둘로 나누는' 분할 정복의 논리를 보여 준다. 연결 리스트처럼 기본 단위는 노드이며, 각 노드는 값과 왼쪽 자식 참조, 오른쪽 자식 참조를 포함한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby <pre><code>class TreeNode:\n \"\"\"이진 트리 노드 클래스\"\"\"\n def __init__(self, val: int):\n self.val: int = val # 노드 값\n self.left: TreeNode | None = None # 왼쪽 자식 노드 참조\n self.right: TreeNode | None = None # 오른쪽 자식 노드 참조\n</code></pre> <pre><code>/* 이진 트리 노드 구조체 */\nstruct TreeNode {\n int val; // 노드 값\n TreeNode *left; // 왼쪽 자식 노드 포인터\n TreeNode *right; // 오른쪽 자식 노드 포인터\n TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}\n};\n</code></pre> <pre><code>/* 이진 트리 노드 클래스 */\nclass TreeNode {\n int val; // 노드 값\n TreeNode left; // 왼쪽 자식 노드 참조\n TreeNode right; // 오른쪽 자식 노드 참조\n TreeNode(int x) { val = x; }\n}\n</code></pre> <pre><code>/* 이진 트리 노드 클래스 */\nclass TreeNode(int? x) {\n public int? val = x; // 노드 값\n public TreeNode? left; // 왼쪽 자식 노드 참조\n public TreeNode? right; // 오른쪽 자식 노드 참조\n}\n</code></pre> <pre><code>/* 이진 트리 노드 구조체 */\ntype TreeNode struct {\n Val int\n Left *TreeNode\n Right *TreeNode\n}\n/* 생성자 */\nfunc NewTreeNode(v int) *TreeNode {\n return &amp;TreeNode{\n Left: nil, // 왼쪽 자식 노드 포인터\n Right: nil, // 오른쪽 자식 노드 포인터\n Val: v, // 노드 값\n }\n}\n</code></pre> <pre><code>/* 이진 트리 노드 클래스 */\nclass TreeNode {\n var val: Int // 노드 값\n var left: TreeNode? // 왼쪽 자식 노드 참조\n var right: TreeNode? // 오른쪽 자식 노드 참조\n\n init(x: Int) {\n val = x\n }\n}\n</code></pre> <pre><code>/* 이진 트리 노드 클래스 */\nclass TreeNode {\n val; // 노드 값\n left; // 왼쪽 자식 노드 포인터\n right; // 오른쪽 자식 노드 포인터\n constructor(val, left, right) {\n this.val = val === undefined ? 0 : val;\n this.left = left === undefined ? null : left;\n this.right = right === undefined ? null : right;\n }\n}\n</code></pre> <pre><code>/* 이진 트리 노드 클래스 */\nclass TreeNode {\n val: number;\n left: TreeNode | null;\n right: TreeNode | null;\n\n constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {\n this.val = val === undefined ? 0 : val; // 노드 값\n this.left = left === undefined ? null : left; // 왼쪽 자식 노드 참조\n this.right = right === undefined ? null : right; // 오른쪽 자식 노드 참조\n }\n}\n</code></pre> <pre><code>/* 이진 트리 노드 클래스 */\nclass TreeNode {\n int val; // 노드 값\n TreeNode? left; // 왼쪽 자식 노드 참조\n TreeNode? right; // 오른쪽 자식 노드 참조\n TreeNode(this.val, [this.left, this.right]);\n}\n</code></pre> <pre><code>use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* 이진 트리 노드 구조체 */\nstruct TreeNode {\n val: i32, // 노드 값\n left: Option&lt;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;, // 왼쪽 자식 노드 참조\n right: Option&lt;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;, // 오른쪽 자식 노드 참조\n}\n\nimpl TreeNode {\n /* 생성자 */\n fn new(val: i32) -&gt; Rc&lt;RefCell&lt;Self&gt;&gt; {\n Rc::new(RefCell::new(Self {\n val,\n left: None,\n right: None\n }))\n }\n}\n</code></pre> <pre><code>/* 이진 트리 노드 구조체 */\ntypedef struct TreeNode {\n int val; // 노드 값\n int height; // 노드 높이\n struct TreeNode *left; // 왼쪽 자식 노드 포인터\n struct TreeNode *right; // 오른쪽 자식 노드 포인터\n} TreeNode;\n\n/* 생성자 */\nTreeNode *newTreeNode(int val) {\n TreeNode *node;\n\n node = (TreeNode *)malloc(sizeof(TreeNode));\n node-&gt;val = val;\n node-&gt;height = 0;\n node-&gt;left = NULL;\n node-&gt;right = NULL;\n return node;\n}\n</code></pre> <pre><code>/* 이진 트리 노드 클래스 */\nclass TreeNode(val _val: Int) { // 노드 값\n var left: TreeNode? = null // 왼쪽 자식 노드 참조\n var right: TreeNode? = null // 오른쪽 자식 노드 참조\n}\n</code></pre> <pre><code>### 이진 트리 노드 클래스 ###\nclass TreeNode\n attr_accessor :val # 노드 값\n attr_accessor :left # 왼쪽 자식 노드 참조\n attr_accessor :right # 오른쪽 자식 노드 참조\n\n def initialize(val)\n @val = val\n end\nend\n</code></pre> <p>각 노드는 두 참조(포인터)를 가지며, 각각 왼쪽 자식 노드(left-child node)와 오른쪽 자식 노드(right-child node)를 가리킨다. 해당 노드는 두 자식의 부모 노드(parent node)라고 한다. 어떤 노드의 왼쪽 자식과 그 아래 노드들이 이루는 트리를 그 노드의 왼쪽 서브트리(left subtree)라고 하며, 같은 방식으로 오른쪽 서브트리(right subtree)를 정의한다.</p> <p>이진 트리에서 리프 노드를 제외한 모든 노드에는 자식 노드와 비어 있지 않은 서브트리가 있다. 아래 그림(7-1)에서 '노드 2'를 부모로 보면 왼쪽과 오른쪽 자식은 각각 '노드 4', '노드 5'다. 왼쪽 서브트리는 '노드 4와 그 아래 노드들이 이루는 트리', 오른쪽 서브트리는 '노드 5와 그 아래 노드들이 이루는 트리'다.</p> <p></p> <p> 그림 7-1   부모 노드, 자식 노드, 서브트리 </p>","path":["제 7 장   트리","7.1   이진 트리"],"tags":[]},{"location":"chapter_tree/binary_tree/#711","level":2,"title":"7.1.1   이진 트리의 주요 용어","text":"<p>이진 트리에서 자주 쓰는 용어는 아래 그림(7-2)과 같다.</p> <ul> <li>루트 노드(root node): 이진 트리의 최상위 노드로, 부모가 없다.</li> <li>리프 노드(leaf node): 자식이 없는 노드로, 두 포인터가 모두 <code>None</code>을 가리킨다.</li> <li>간선(edge): 두 노드를 잇는 선으로, 노드 참조(포인터)에 해당한다.</li> <li>노드의 레벨(level): 위에서 아래로 증가하며 루트의 레벨은 1이다.</li> <li>노드의 차수(degree): 자식 노드의 수다. 이진 트리에서는 0, 1, 2 중 하나다.</li> <li>이진 트리의 높이(height): 루트에서 가장 먼 리프까지 거치는 간선 수다.</li> <li>노드의 깊이(depth): 루트에서 해당 노드까지 거치는 간선 수다.</li> <li>노드의 높이(height): 해당 노드에서 가장 먼 리프와 해당 노드 사이에 있는 간선 수다.</li> </ul> <p></p> <p> 그림 7-2   이진 트리의 주요 용어 </p> <p>참고</p> <p>보통 '높이'와 '깊이'를 '거치는 간선 수'로 정의하지만, 일부 문제나 교재는 '거치는 노드 수'로 정의하기도 한다. 그런 경우에는 높이와 깊이에 각각 1을 더해야 한다.</p>","path":["제 7 장   트리","7.1   이진 트리"],"tags":[]},{"location":"chapter_tree/binary_tree/#712","level":2,"title":"7.1.2   이진 트리의 기본 연산","text":"","path":["제 7 장   트리","7.1   이진 트리"],"tags":[]},{"location":"chapter_tree/binary_tree/#1","level":3,"title":"1.   이진 트리 초기화","text":"<p>연결 리스트처럼 먼저 노드를 초기화하고 참조(포인터)를 연결한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree.py<pre><code># 이진 트리 초기화\n# 노드 초기화\nn1 = TreeNode(val=1)\nn2 = TreeNode(val=2)\nn3 = TreeNode(val=3)\nn4 = TreeNode(val=4)\nn5 = TreeNode(val=5)\n# 노드 사이의 참조(포인터) 연결\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n</code></pre> binary_tree.cpp<pre><code>/* 이진 트리 초기화 */\n// 노드 초기화\nTreeNode* n1 = new TreeNode(1);\nTreeNode* n2 = new TreeNode(2);\nTreeNode* n3 = new TreeNode(3);\nTreeNode* n4 = new TreeNode(4);\nTreeNode* n5 = new TreeNode(5);\n// 노드 사이의 참조(포인터) 연결\nn1-&gt;left = n2;\nn1-&gt;right = n3;\nn2-&gt;left = n4;\nn2-&gt;right = n5;\n</code></pre> binary_tree.java<pre><code>// 노드 초기화\nTreeNode n1 = new TreeNode(1);\nTreeNode n2 = new TreeNode(2);\nTreeNode n3 = new TreeNode(3);\nTreeNode n4 = new TreeNode(4);\nTreeNode n5 = new TreeNode(5);\n// 노드 사이의 참조(포인터) 연결\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n</code></pre> binary_tree.cs<pre><code>/* 이진 트리 초기화 */\n// 노드 초기화\nTreeNode n1 = new(1);\nTreeNode n2 = new(2);\nTreeNode n3 = new(3);\nTreeNode n4 = new(4);\nTreeNode n5 = new(5);\n// 노드 사이의 참조(포인터) 연결\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n</code></pre> binary_tree.go<pre><code>/* 이진 트리 초기화 */\n// 노드 초기화\nn1 := NewTreeNode(1)\nn2 := NewTreeNode(2)\nn3 := NewTreeNode(3)\nn4 := NewTreeNode(4)\nn5 := NewTreeNode(5)\n// 노드 사이의 참조(포인터) 연결\nn1.Left = n2\nn1.Right = n3\nn2.Left = n4\nn2.Right = n5\n</code></pre> binary_tree.swift<pre><code>// 노드 초기화\nlet n1 = TreeNode(x: 1)\nlet n2 = TreeNode(x: 2)\nlet n3 = TreeNode(x: 3)\nlet n4 = TreeNode(x: 4)\nlet n5 = TreeNode(x: 5)\n// 노드 사이의 참조(포인터) 연결\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n</code></pre> binary_tree.js<pre><code>/* 이진 트리 초기화 */\n// 노드 초기화\nlet n1 = new TreeNode(1),\n n2 = new TreeNode(2),\n n3 = new TreeNode(3),\n n4 = new TreeNode(4),\n n5 = new TreeNode(5);\n// 노드 사이의 참조(포인터) 연결\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n</code></pre> binary_tree.ts<pre><code>/* 이진 트리 초기화 */\n// 노드 초기화\nlet n1 = new TreeNode(1),\n n2 = new TreeNode(2),\n n3 = new TreeNode(3),\n n4 = new TreeNode(4),\n n5 = new TreeNode(5);\n// 노드 사이의 참조(포인터) 연결\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n</code></pre> binary_tree.dart<pre><code>/* 이진 트리 초기화 */\n// 노드 초기화\nTreeNode n1 = new TreeNode(1);\nTreeNode n2 = new TreeNode(2);\nTreeNode n3 = new TreeNode(3);\nTreeNode n4 = new TreeNode(4);\nTreeNode n5 = new TreeNode(5);\n// 노드 사이의 참조(포인터) 연결\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n</code></pre> binary_tree.rs<pre><code>// 노드 초기화\nlet n1 = TreeNode::new(1);\nlet n2 = TreeNode::new(2);\nlet n3 = TreeNode::new(3);\nlet n4 = TreeNode::new(4);\nlet n5 = TreeNode::new(5);\n// 노드 사이의 참조(포인터) 연결\nn1.borrow_mut().left = Some(n2.clone());\nn1.borrow_mut().right = Some(n3);\nn2.borrow_mut().left = Some(n4);\nn2.borrow_mut().right = Some(n5);\n</code></pre> binary_tree.c<pre><code>/* 이진 트리 초기화 */\n// 노드 초기화\nTreeNode *n1 = newTreeNode(1);\nTreeNode *n2 = newTreeNode(2);\nTreeNode *n3 = newTreeNode(3);\nTreeNode *n4 = newTreeNode(4);\nTreeNode *n5 = newTreeNode(5);\n// 노드 사이의 참조(포인터) 연결\nn1-&gt;left = n2;\nn1-&gt;right = n3;\nn2-&gt;left = n4;\nn2-&gt;right = n5;\n</code></pre> binary_tree.kt<pre><code>// 노드 초기화\nval n1 = TreeNode(1)\nval n2 = TreeNode(2)\nval n3 = TreeNode(3)\nval n4 = TreeNode(4)\nval n5 = TreeNode(5)\n// 노드 사이의 참조(포인터) 연결\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n</code></pre> binary_tree.rb<pre><code># 이진 트리 초기화\n# 노드 초기화\nn1 = TreeNode.new(1)\nn2 = TreeNode.new(2)\nn3 = TreeNode.new(3)\nn4 = TreeNode.new(4)\nn5 = TreeNode.new(5)\n# 노드 사이의 참조(포인터) 연결\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n</code></pre> 실행 시각화 <p> 전체 화면 &gt;</p>","path":["제 7 장   트리","7.1   이진 트리"],"tags":[]},{"location":"chapter_tree/binary_tree/#2","level":3,"title":"2.   노드 삽입과 삭제","text":"<p>연결 리스트와 마찬가지로 이진 트리에서도 포인터를 수정하여 노드를 삽입하고 삭제할 수 있다. 아래 그림(7-3)은 그 예다.</p> <p></p> <p> 그림 7-3   이진 트리의 노드 삽입과 삭제 </p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree.py<pre><code># 노드 삽입과 삭제\np = TreeNode(0)\n# n1 -&gt; n2 사이에 노드 P 삽입\nn1.left = p\np.left = n2\n# 노드 P 삭제\nn1.left = n2\n</code></pre> binary_tree.cpp<pre><code>/* 노드 삽입과 삭제 */\nTreeNode* P = new TreeNode(0);\n// n1 -&gt; n2 사이에 노드 P 삽입\nn1-&gt;left = P;\nP-&gt;left = n2;\n// 노드 P 삭제\nn1-&gt;left = n2;\n// 메모리 해제\ndelete P;\n</code></pre> binary_tree.java<pre><code>TreeNode P = new TreeNode(0);\n// n1 -&gt; n2 사이에 노드 P 삽입\nn1.left = P;\nP.left = n2;\n// 노드 P 삭제\nn1.left = n2;\n</code></pre> binary_tree.cs<pre><code>/* 노드 삽입과 삭제 */\nTreeNode P = new(0);\n// n1 -&gt; n2 사이에 노드 P 삽입\nn1.left = P;\nP.left = n2;\n// 노드 P 삭제\nn1.left = n2;\n</code></pre> binary_tree.go<pre><code>/* 노드 삽입과 삭제 */\n// n1 -&gt; n2 사이에 노드 P 삽입\np := NewTreeNode(0)\nn1.Left = p\np.Left = n2\n// 노드 P 삭제\nn1.Left = n2\n</code></pre> binary_tree.swift<pre><code>let P = TreeNode(x: 0)\n// n1 -&gt; n2 사이에 노드 P 삽입\nn1.left = P\nP.left = n2\n// 노드 P 삭제\nn1.left = n2\n</code></pre> binary_tree.js<pre><code>/* 노드 삽입과 삭제 */\nlet P = new TreeNode(0);\n// n1 -&gt; n2 사이에 노드 P 삽입\nn1.left = P;\nP.left = n2;\n// 노드 P 삭제\nn1.left = n2;\n</code></pre> binary_tree.ts<pre><code>/* 노드 삽입과 삭제 */\nconst P = new TreeNode(0);\n// n1 -&gt; n2 사이에 노드 P 삽입\nn1.left = P;\nP.left = n2;\n// 노드 P 삭제\nn1.left = n2;\n</code></pre> binary_tree.dart<pre><code>/* 노드 삽입과 삭제 */\nTreeNode P = new TreeNode(0);\n// n1 -&gt; n2 사이에 노드 P 삽입\nn1.left = P;\nP.left = n2;\n// 노드 P 삭제\nn1.left = n2;\n</code></pre> binary_tree.rs<pre><code>let p = TreeNode::new(0);\n// n1 -&gt; n2 사이에 노드 P 삽입\nn1.borrow_mut().left = Some(p.clone());\np.borrow_mut().left = Some(n2.clone());\n// 노드 p 삭제\nn1.borrow_mut().left = Some(n2);\n</code></pre> binary_tree.c<pre><code>/* 노드 삽입과 삭제 */\nTreeNode *P = newTreeNode(0);\n// n1 -&gt; n2 사이에 노드 P 삽입\nn1-&gt;left = P;\nP-&gt;left = n2;\n// 노드 P 삭제\nn1-&gt;left = n2;\n// 메모리 해제\nfree(P);\n</code></pre> binary_tree.kt<pre><code>val P = TreeNode(0)\n// n1 -&gt; n2 사이에 노드 P 삽입\nn1.left = P\nP.left = n2\n// 노드 P 삭제\nn1.left = n2\n</code></pre> binary_tree.rb<pre><code># 노드 삽입과 삭제\n_p = TreeNode.new(0)\n# n1 -&gt; n2 사이에 노드 _p 삽입\nn1.left = _p\n_p.left = n2\n# 노드 삭제\nn1.left = n2\n</code></pre> 실행 시각화 <p> 전체 화면 &gt;</p> <p>참고</p> <p>노드 삽입은 이진 트리의 기존 논리 구조를 바꿀 수 있으며, 노드 삭제는 보통 해당 노드와 그 모든 서브트리를 삭제한다는 뜻이다. 따라서 의미 있는 작업을 수행하려면 이진 트리의 삽입과 삭제는 보통 여러 연산을 조합하여 이루어진다.</p>","path":["제 7 장   트리","7.1   이진 트리"],"tags":[]},{"location":"chapter_tree/binary_tree/#713","level":2,"title":"7.1.3   주요 이진 트리 종류","text":"","path":["제 7 장   트리","7.1   이진 트리"],"tags":[]},{"location":"chapter_tree/binary_tree/#1_1","level":3,"title":"1.   포화 이진 트리","text":"<p>아래 그림(7-4)의 포화 이진 트리(perfect binary tree)는 모든 레벨에 노드가 가득 차 있다. 리프 노드의 차수는 \\(0\\), 나머지 노드의 차수는 모두 \\(2\\)다. 높이가 \\(h\\)이면 전체 노드 수는 \\(2^{h+1} - 1\\)로 전형적인 지수 관계를 보이며, 자연에서 흔히 볼 수 있는 세포 분열 현상을 반영한다.</p> <p>참고</p> <p>중국어권에서는 포화 이진 트리를 흔히 满二叉树라고 부른다.</p> <p></p> <p> 그림 7-4   포화 이진 트리 </p>","path":["제 7 장   트리","7.1   이진 트리"],"tags":[]},{"location":"chapter_tree/binary_tree/#2_1","level":3,"title":"2.   완전 이진 트리","text":"<p>아래 그림(7-5)의 완전 이진 트리(complete binary tree)는 맨 아래 레벨만 가득 차 있지 않아도 되며, 그 레벨의 노드는 왼쪽부터 오른쪽으로 빈틈없이 채워져야 한다. 포화 이진 트리도 완전 이진 트리다.</p> <p></p> <p> 그림 7-5   완전 이진 트리 </p>","path":["제 7 장   트리","7.1   이진 트리"],"tags":[]},{"location":"chapter_tree/binary_tree/#3","level":3,"title":"3.   정 이진 트리","text":"<p>아래 그림(7-6)의 정 이진 트리(full binary tree)는 리프를 제외한 모든 노드가 자식 노드를 두 개씩 가진다.</p> <p></p> <p> 그림 7-6   정 이진 트리 </p>","path":["제 7 장   트리","7.1   이진 트리"],"tags":[]},{"location":"chapter_tree/binary_tree/#4","level":3,"title":"4.   균형 이진 트리","text":"<p>아래 그림(7-7)의 균형 이진 트리(balanced binary tree)는 모든 노드에서 왼쪽과 오른쪽 서브트리의 높이 차의 절댓값이 1을 넘지 않는다.</p> <p></p> <p> 그림 7-7   균형 이진 트리 </p>","path":["제 7 장   트리","7.1   이진 트리"],"tags":[]},{"location":"chapter_tree/binary_tree/#714","level":2,"title":"7.1.4   이진 트리의 퇴화","text":"<p>아래 그림(7-8)은 이진 트리의 이상적인 구조와 퇴화한 구조를 보여 준다. 모든 레벨이 가득 차면 '포화 이진 트리'가 되고, 모든 노드가 한쪽으로 치우치면 '연결 리스트'로 퇴화한다.</p> <ul> <li>포화 이진 트리는 이상적인 경우로, 이진 트리의 '분할 정복' 이점을 충분히 발휘할 수 있다.</li> <li>연결 리스트는 다른 극단으로, 모든 연산이 선형 연산이 되어 시간 복잡도가 \\(O(n)\\)으로 저하된다.</li> </ul> <p></p> <p> 그림 7-8   이진 트리의 최선 구조와 최악 구조 </p> <p>아래 표(7-1)와 같이 최선 및 최악 구조에서 이진 트리의 리프 수, 전체 노드 수, 높이 등이 최댓값이나 최솟값에 도달한다.</p> <p> 표 7-1   이진 트리의 최선 구조와 최악 구조 </p> 포화 이진 트리 연결 리스트 \\(i\\) 번째 레벨의 노드 수 \\(2^{i-1}\\) \\(1\\) 높이 \\(h\\)인 트리의 리프 수 \\(2^h\\) \\(1\\) 높이 \\(h\\)인 트리의 전체 노드 수 \\(2^{h+1} - 1\\) \\(h + 1\\) 전체 노드 수가 \\(n\\)인 트리의 높이 \\(\\log_2 (n+1) - 1\\) \\(n - 1\\)","path":["제 7 장   트리","7.1   이진 트리"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/","level":1,"title":"7.2   이진 트리 순회","text":"<p>물리적 구조의 관점에서 트리는 연결 리스트 기반 자료 구조이므로, 포인터로 노드를 하나씩 방문하여 순회한다. 하지만 비선형 자료 구조여서 연결 리스트보다 순회가 복잡하며 탐색 알고리즘이 필요하다.</p> <p>이진 트리의 주요 순회 방식은 레벨 순서 순회, 전위 순회, 중위 순회, 후위 순회 등이다.</p>","path":["제 7 장   트리","7.2   이진 트리 순회"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#721","level":2,"title":"7.2.1   레벨 순서 순회","text":"<p>아래 그림(7-9)과 같이 레벨 순서 순회(level-order traversal)는 위에서 아래로 한 레벨씩 순회하며, 각 레벨에서는 왼쪽부터 오른쪽 순서로 노드를 방문한다.</p> <p>레벨 순서 순회는 본질적으로 너비 우선 순회(breadth-first traversal), 즉 너비 우선 탐색(breadth-first search, BFS)에 속한다. '한 겹씩 바깥으로 확장'하는 층별 순회 방식이다.</p> <p></p> <p> 그림 7-9   이진 트리의 레벨 순서 순회 </p>","path":["제 7 장   트리","7.2   이진 트리 순회"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#1","level":3,"title":"1.   코드 구현","text":"<p>너비 우선 순회는 보통 '큐'로 구현한다. 큐는 '선입선출' 규칙을, 너비 우선 순회는 '한 레벨씩 진행'하는 규칙을 따르며, 바탕에 깔린 생각은 같다. 구현 코드는 다음과 같다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree_bfs.py<pre><code>def level_order(root: TreeNode | None) -&gt; list[int]:\n \"\"\"레벨 순서 순회\"\"\"\n # 큐를 초기화하고 루트 노드를 넣는다\n queue: deque[TreeNode] = deque()\n queue.append(root)\n # 순회 순서를 저장할 리스트를 초기화한다\n res = []\n while queue:\n node: TreeNode = queue.popleft() # 큐에서 꺼낸다\n res.append(node.val) # 노드 값을 저장한다\n if node.left is not None:\n queue.append(node.left) # 왼쪽 자식 노드를 큐에 삽입한다\n if node.right is not None:\n queue.append(node.right) # 오른쪽 자식 노드를 큐에 삽입한다\n return res\n</code></pre> binary_tree_bfs.cpp<pre><code>/* 레벨 순서 순회 */\nvector&lt;int&gt; levelOrder(TreeNode *root) {\n // 큐를 초기화하고 루트 노드를 넣는다\n queue&lt;TreeNode *&gt; queue;\n queue.push(root);\n // 순회 순서를 저장할 리스트를 초기화한다\n vector&lt;int&gt; vec;\n while (!queue.empty()) {\n TreeNode *node = queue.front();\n queue.pop(); // 큐에서 꺼낸다\n vec.push_back(node-&gt;val); // 노드 값을 저장한다\n if (node-&gt;left != nullptr)\n queue.push(node-&gt;left); // 왼쪽 자식 노드를 큐에 삽입한다\n if (node-&gt;right != nullptr)\n queue.push(node-&gt;right); // 오른쪽 자식 노드를 큐에 삽입한다\n }\n return vec;\n}\n</code></pre> binary_tree_bfs.java<pre><code>/* 레벨 순서 순회 */\nList&lt;Integer&gt; levelOrder(TreeNode root) {\n // 큐를 초기화하고 루트 노드를 넣는다\n Queue&lt;TreeNode&gt; queue = new LinkedList&lt;&gt;();\n queue.add(root);\n // 순회 순서를 저장할 리스트를 초기화한다\n List&lt;Integer&gt; list = new ArrayList&lt;&gt;();\n while (!queue.isEmpty()) {\n TreeNode node = queue.poll(); // 큐에서 꺼낸다\n list.add(node.val); // 노드 값을 저장한다\n if (node.left != null)\n queue.offer(node.left); // 왼쪽 자식 노드를 큐에 삽입한다\n if (node.right != null)\n queue.offer(node.right); // 오른쪽 자식 노드를 큐에 삽입한다\n }\n return list;\n}\n</code></pre> binary_tree_bfs.cs<pre><code>/* 레벨 순서 순회 */\nList&lt;int&gt; LevelOrder(TreeNode root) {\n // 큐를 초기화하고 루트 노드를 넣는다\n Queue&lt;TreeNode&gt; queue = new();\n queue.Enqueue(root);\n // 순회 순서를 저장할 리스트를 초기화한다\n List&lt;int&gt; list = [];\n while (queue.Count != 0) {\n TreeNode node = queue.Dequeue(); // 큐에서 꺼낸다\n list.Add(node.val!.Value); // 노드 값을 저장한다\n if (node.left != null)\n queue.Enqueue(node.left); // 왼쪽 자식 노드를 큐에 삽입한다\n if (node.right != null)\n queue.Enqueue(node.right); // 오른쪽 자식 노드를 큐에 삽입한다\n }\n return list;\n}\n</code></pre> binary_tree_bfs.go<pre><code>/* 레벨 순서 순회 */\nfunc levelOrder(root *TreeNode) []any {\n // 큐를 초기화하고 루트 노드를 넣는다\n queue := list.New()\n queue.PushBack(root)\n // 순회 순서를 저장할 슬라이스를 초기화한다\n nums := make([]any, 0)\n for queue.Len() &gt; 0 {\n // 큐에서 꺼낸다\n node := queue.Remove(queue.Front()).(*TreeNode)\n // 노드 값을 저장한다\n nums = append(nums, node.Val)\n if node.Left != nil {\n // 왼쪽 자식 노드를 큐에 삽입한다\n queue.PushBack(node.Left)\n }\n if node.Right != nil {\n // 오른쪽 자식 노드를 큐에 삽입한다\n queue.PushBack(node.Right)\n }\n }\n return nums\n}\n</code></pre> binary_tree_bfs.swift<pre><code>/* 레벨 순서 순회 */\nfunc levelOrder(root: TreeNode) -&gt; [Int] {\n // 큐를 초기화하고 루트 노드를 넣는다\n var queue: [TreeNode] = [root]\n // 순회 순서를 저장할 리스트를 초기화한다\n var list: [Int] = []\n while !queue.isEmpty {\n let node = queue.removeFirst() // 큐에서 꺼낸다\n list.append(node.val) // 노드 값을 저장한다\n if let left = node.left {\n queue.append(left) // 왼쪽 자식 노드를 큐에 삽입한다\n }\n if let right = node.right {\n queue.append(right) // 오른쪽 자식 노드를 큐에 삽입한다\n }\n }\n return list\n}\n</code></pre> binary_tree_bfs.js<pre><code>/* 레벨 순서 순회 */\nfunction levelOrder(root) {\n // 큐를 초기화하고 루트 노드를 넣는다\n const queue = [root];\n // 순회 순서를 저장할 리스트를 초기화한다\n const list = [];\n while (queue.length) {\n let node = queue.shift(); // 큐에서 꺼낸다\n list.push(node.val); // 노드 값을 저장한다\n if (node.left) queue.push(node.left); // 왼쪽 자식 노드를 큐에 삽입한다\n if (node.right) queue.push(node.right); // 오른쪽 자식 노드를 큐에 삽입한다\n }\n return list;\n}\n</code></pre> binary_tree_bfs.ts<pre><code>/* 레벨 순서 순회 */\nfunction levelOrder(root: TreeNode | null): number[] {\n // 큐를 초기화하고 루트 노드를 넣는다\n const queue = [root];\n // 순회 순서를 저장할 리스트를 초기화한다\n const list: number[] = [];\n while (queue.length) {\n let node = queue.shift() as TreeNode; // 큐에서 꺼낸다\n list.push(node.val); // 노드 값을 저장한다\n if (node.left) {\n queue.push(node.left); // 왼쪽 자식 노드를 큐에 삽입한다\n }\n if (node.right) {\n queue.push(node.right); // 오른쪽 자식 노드를 큐에 삽입한다\n }\n }\n return list;\n}\n</code></pre> binary_tree_bfs.dart<pre><code>/* 레벨 순서 순회 */\nList&lt;int&gt; levelOrder(TreeNode? root) {\n // 큐를 초기화하고 루트 노드를 넣는다\n Queue&lt;TreeNode?&gt; queue = Queue();\n queue.add(root);\n // 순회 순서를 저장할 리스트를 초기화한다\n List&lt;int&gt; res = [];\n while (queue.isNotEmpty) {\n TreeNode? node = queue.removeFirst(); // 큐에서 꺼낸다\n res.add(node!.val); // 노드 값을 저장한다\n if (node.left != null) queue.add(node.left); // 왼쪽 자식 노드를 큐에 삽입한다\n if (node.right != null) queue.add(node.right); // 오른쪽 자식 노드를 큐에 삽입한다\n }\n return res;\n}\n</code></pre> binary_tree_bfs.rs<pre><code>/* 레벨 순서 순회 */\nfn level_order(root: &amp;Rc&lt;RefCell&lt;TreeNode&gt;&gt;) -&gt; Vec&lt;i32&gt; {\n // 큐를 초기화하고 루트 노드를 넣는다\n let mut que = VecDeque::new();\n que.push_back(root.clone());\n // 순회 순서를 저장할 리스트를 초기화한다\n let mut vec = Vec::new();\n\n while let Some(node) = que.pop_front() {\n // 큐에서 꺼낸다\n vec.push(node.borrow().val); // 노드 값을 저장한다\n if let Some(left) = node.borrow().left.as_ref() {\n que.push_back(left.clone()); // 왼쪽 자식 노드를 큐에 삽입한다\n }\n if let Some(right) = node.borrow().right.as_ref() {\n que.push_back(right.clone()); // 오른쪽 자식 노드를 큐에 삽입한다\n };\n }\n vec\n}\n</code></pre> binary_tree_bfs.c<pre><code>/* 레벨 순서 순회 */\nint *levelOrder(TreeNode *root, int *size) {\n /* 보조 큐 */\n int front, rear;\n int index, *arr;\n TreeNode *node;\n TreeNode **queue;\n\n /* 보조 큐 */\n queue = (TreeNode **)malloc(sizeof(TreeNode *) * MAX_SIZE);\n // 큐 포인터\n front = 0, rear = 0;\n // 루트 노드를 넣는다\n queue[rear++] = root;\n // 순회 순서를 저장할 리스트를 초기화한다\n /* 보조 배열 */\n arr = (int *)malloc(sizeof(int) * MAX_SIZE);\n // 배열 포인터\n index = 0;\n while (front &lt; rear) {\n // 큐에서 꺼낸다\n node = queue[front++];\n // 노드 값을 저장한다\n arr[index++] = node-&gt;val;\n if (node-&gt;left != NULL) {\n // 왼쪽 자식 노드를 큐에 삽입한다\n queue[rear++] = node-&gt;left;\n }\n if (node-&gt;right != NULL) {\n // 오른쪽 자식 노드를 큐에 삽입한다\n queue[rear++] = node-&gt;right;\n }\n }\n // 배열 길이 값을 갱신한다\n *size = index;\n arr = realloc(arr, sizeof(int) * (*size));\n\n // 보조 배열의 메모리 공간을 해제한다\n free(queue);\n return arr;\n}\n</code></pre> binary_tree_bfs.kt<pre><code>/* 레벨 순서 순회 */\nfun levelOrder(root: TreeNode?): MutableList&lt;Int&gt; {\n // 큐를 초기화하고 루트 노드를 넣는다\n val queue = LinkedList&lt;TreeNode?&gt;()\n queue.add(root)\n // 순회 순서를 저장할 리스트를 초기화한다\n val list = mutableListOf&lt;Int&gt;()\n while (queue.isNotEmpty()) {\n val node = queue.poll() // 큐에서 꺼낸다\n list.add(node?._val!!) // 노드 값을 저장한다\n if (node.left != null)\n queue.offer(node.left) // 왼쪽 자식 노드를 큐에 삽입한다\n if (node.right != null)\n queue.offer(node.right) // 오른쪽 자식 노드를 큐에 삽입한다\n }\n return list\n}\n</code></pre> binary_tree_bfs.rb<pre><code>### 레벨 순서 순회 ###\ndef level_order(root)\n # 큐를 초기화하고 루트 노드를 넣는다\n queue = [root]\n # 순회 순서를 저장할 리스트를 초기화한다\n res = []\n while !queue.empty?\n node = queue.shift # 큐에서 꺼낸다\n res &lt;&lt; node.val # 노드 값을 저장한다\n queue &lt;&lt; node.left unless node.left.nil? # 왼쪽 자식 노드를 큐에 삽입한다\n queue &lt;&lt; node.right unless node.right.nil? # 오른쪽 자식 노드를 큐에 삽입한다\n end\n res\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p>","path":["제 7 장   트리","7.2   이진 트리 순회"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#2","level":3,"title":"2.   복잡도 분석","text":"<ul> <li>시간 복잡도 \\(O(n)\\): 모든 노드를 한 번씩 방문하므로 \\(O(n)\\) 시간이 걸린다. \\(n\\)은 노드 수다.</li> <li>공간 복잡도 \\(O(n)\\): 최악의 경우인 포화 이진 트리에서는 맨 아래 레벨을 순회하기 직전에 큐에 최대 \\((n + 1) / 2\\) 개 노드가 동시에 들어 있어 \\(O(n)\\) 공간을 차지한다.</li> </ul>","path":["제 7 장   트리","7.2   이진 트리 순회"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#722","level":2,"title":"7.2.2   전위, 중위, 후위 순회","text":"<p>전위, 중위, 후위 순회는 모두 깊이 우선 순회(depth-first traversal), 즉 깊이 우선 탐색(depth-first search, DFS)에 속한다. '먼저 끝까지 간 뒤 돌아와 계속하는' 순회 방식이다.</p> <p>아래 그림(7-10)은 이진 트리의 깊이 우선 순회 원리를 보여 준다. 깊이 우선 순회는 트리 전체의 바깥을 한 바퀴 '도는' 것과 같다. 각 노드에서 세 위치를 만나며, 각각 전위, 중위, 후위 순회에 대응한다.</p> <p></p> <p> 그림 7-10   이진 탐색 트리의 전위, 중위, 후위 순회 </p>","path":["제 7 장   트리","7.2   이진 트리 순회"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#1_1","level":3,"title":"1.   코드 구현","text":"<p>깊이 우선 탐색은 보통 재귀로 구현한다.</p> PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree_dfs.py<pre><code>def pre_order(root: TreeNode | None):\n \"\"\"전위 순회\"\"\"\n if root is None:\n return\n # 방문 우선순위: 루트 노드 -&gt; 왼쪽 서브트리 -&gt; 오른쪽 서브트리\n res.append(root.val)\n pre_order(root=root.left)\n pre_order(root=root.right)\n\ndef in_order(root: TreeNode | None):\n \"\"\"중위 순회\"\"\"\n if root is None:\n return\n # 방문 우선순위: 왼쪽 서브트리 -&gt; 루트 노드 -&gt; 오른쪽 서브트리\n in_order(root=root.left)\n res.append(root.val)\n in_order(root=root.right)\n\ndef post_order(root: TreeNode | None):\n \"\"\"후위 순회\"\"\"\n if root is None:\n return\n # 방문 우선순위: 왼쪽 서브트리 -&gt; 오른쪽 서브트리 -&gt; 루트 노드\n post_order(root=root.left)\n post_order(root=root.right)\n res.append(root.val)\n</code></pre> binary_tree_dfs.cpp<pre><code>/* 전위 순회 */\nvoid preOrder(TreeNode *root) {\n if (root == nullptr)\n return;\n // 방문 우선순위: 루트 노드 -&gt; 왼쪽 서브트리 -&gt; 오른쪽 서브트리\n vec.push_back(root-&gt;val);\n preOrder(root-&gt;left);\n preOrder(root-&gt;right);\n}\n\n/* 중위 순회 */\nvoid inOrder(TreeNode *root) {\n if (root == nullptr)\n return;\n // 방문 우선순위: 왼쪽 서브트리 -&gt; 루트 노드 -&gt; 오른쪽 서브트리\n inOrder(root-&gt;left);\n vec.push_back(root-&gt;val);\n inOrder(root-&gt;right);\n}\n\n/* 후위 순회 */\nvoid postOrder(TreeNode *root) {\n if (root == nullptr)\n return;\n // 방문 우선순위: 왼쪽 서브트리 -&gt; 오른쪽 서브트리 -&gt; 루트 노드\n postOrder(root-&gt;left);\n postOrder(root-&gt;right);\n vec.push_back(root-&gt;val);\n}\n</code></pre> binary_tree_dfs.java<pre><code>/* 전위 순회 */\nvoid preOrder(TreeNode root) {\n if (root == null)\n return;\n // 방문 우선순위: 루트 노드 -&gt; 왼쪽 서브트리 -&gt; 오른쪽 서브트리\n list.add(root.val);\n preOrder(root.left);\n preOrder(root.right);\n}\n\n/* 중위 순회 */\nvoid inOrder(TreeNode root) {\n if (root == null)\n return;\n // 방문 우선순위: 왼쪽 서브트리 -&gt; 루트 노드 -&gt; 오른쪽 서브트리\n inOrder(root.left);\n list.add(root.val);\n inOrder(root.right);\n}\n\n/* 후위 순회 */\nvoid postOrder(TreeNode root) {\n if (root == null)\n return;\n // 방문 우선순위: 왼쪽 서브트리 -&gt; 오른쪽 서브트리 -&gt; 루트 노드\n postOrder(root.left);\n postOrder(root.right);\n list.add(root.val);\n}\n</code></pre> binary_tree_dfs.cs<pre><code>/* 전위 순회 */\nvoid PreOrder(TreeNode? root) {\n if (root == null) return;\n // 방문 우선순위: 루트 노드 -&gt; 왼쪽 서브트리 -&gt; 오른쪽 서브트리\n list.Add(root.val!.Value);\n PreOrder(root.left);\n PreOrder(root.right);\n}\n\n/* 중위 순회 */\nvoid InOrder(TreeNode? root) {\n if (root == null) return;\n // 방문 우선순위: 왼쪽 서브트리 -&gt; 루트 노드 -&gt; 오른쪽 서브트리\n InOrder(root.left);\n list.Add(root.val!.Value);\n InOrder(root.right);\n}\n\n/* 후위 순회 */\nvoid PostOrder(TreeNode? root) {\n if (root == null) return;\n // 방문 우선순위: 왼쪽 서브트리 -&gt; 오른쪽 서브트리 -&gt; 루트 노드\n PostOrder(root.left);\n PostOrder(root.right);\n list.Add(root.val!.Value);\n}\n</code></pre> binary_tree_dfs.go<pre><code>/* 전위 순회 */\nfunc preOrder(node *TreeNode) {\n if node == nil {\n return\n }\n // 방문 우선순위: 루트 노드 -&gt; 왼쪽 서브트리 -&gt; 오른쪽 서브트리\n nums = append(nums, node.Val)\n preOrder(node.Left)\n preOrder(node.Right)\n}\n\n/* 중위 순회 */\nfunc inOrder(node *TreeNode) {\n if node == nil {\n return\n }\n // 방문 우선순위: 왼쪽 서브트리 -&gt; 루트 노드 -&gt; 오른쪽 서브트리\n inOrder(node.Left)\n nums = append(nums, node.Val)\n inOrder(node.Right)\n}\n\n/* 후위 순회 */\nfunc postOrder(node *TreeNode) {\n if node == nil {\n return\n }\n // 방문 우선순위: 왼쪽 서브트리 -&gt; 오른쪽 서브트리 -&gt; 루트 노드\n postOrder(node.Left)\n postOrder(node.Right)\n nums = append(nums, node.Val)\n}\n</code></pre> binary_tree_dfs.swift<pre><code>/* 전위 순회 */\nfunc preOrder(root: TreeNode?) {\n guard let root = root else {\n return\n }\n // 방문 우선순위: 루트 노드 -&gt; 왼쪽 서브트리 -&gt; 오른쪽 서브트리\n list.append(root.val)\n preOrder(root: root.left)\n preOrder(root: root.right)\n}\n\n/* 중위 순회 */\nfunc inOrder(root: TreeNode?) {\n guard let root = root else {\n return\n }\n // 방문 우선순위: 왼쪽 서브트리 -&gt; 루트 노드 -&gt; 오른쪽 서브트리\n inOrder(root: root.left)\n list.append(root.val)\n inOrder(root: root.right)\n}\n\n/* 후위 순회 */\nfunc postOrder(root: TreeNode?) {\n guard let root = root else {\n return\n }\n // 방문 우선순위: 왼쪽 서브트리 -&gt; 오른쪽 서브트리 -&gt; 루트 노드\n postOrder(root: root.left)\n postOrder(root: root.right)\n list.append(root.val)\n}\n</code></pre> binary_tree_dfs.js<pre><code>/* 전위 순회 */\nfunction preOrder(root) {\n if (root === null) return;\n // 방문 우선순위: 루트 노드 -&gt; 왼쪽 서브트리 -&gt; 오른쪽 서브트리\n list.push(root.val);\n preOrder(root.left);\n preOrder(root.right);\n}\n\n/* 중위 순회 */\nfunction inOrder(root) {\n if (root === null) return;\n // 방문 우선순위: 왼쪽 서브트리 -&gt; 루트 노드 -&gt; 오른쪽 서브트리\n inOrder(root.left);\n list.push(root.val);\n inOrder(root.right);\n}\n\n/* 후위 순회 */\nfunction postOrder(root) {\n if (root === null) return;\n // 방문 우선순위: 왼쪽 서브트리 -&gt; 오른쪽 서브트리 -&gt; 루트 노드\n postOrder(root.left);\n postOrder(root.right);\n list.push(root.val);\n}\n</code></pre> binary_tree_dfs.ts<pre><code>/* 전위 순회 */\nfunction preOrder(root: TreeNode | null): void {\n if (root === null) {\n return;\n }\n // 방문 우선순위: 루트 노드 -&gt; 왼쪽 서브트리 -&gt; 오른쪽 서브트리\n list.push(root.val);\n preOrder(root.left);\n preOrder(root.right);\n}\n\n/* 중위 순회 */\nfunction inOrder(root: TreeNode | null): void {\n if (root === null) {\n return;\n }\n // 방문 우선순위: 왼쪽 서브트리 -&gt; 루트 노드 -&gt; 오른쪽 서브트리\n inOrder(root.left);\n list.push(root.val);\n inOrder(root.right);\n}\n\n/* 후위 순회 */\nfunction postOrder(root: TreeNode | null): void {\n if (root === null) {\n return;\n }\n // 방문 우선순위: 왼쪽 서브트리 -&gt; 오른쪽 서브트리 -&gt; 루트 노드\n postOrder(root.left);\n postOrder(root.right);\n list.push(root.val);\n}\n</code></pre> binary_tree_dfs.dart<pre><code>/* 전위 순회 */\nvoid preOrder(TreeNode? node) {\n if (node == null) return;\n // 방문 우선순위: 루트 노드 -&gt; 왼쪽 서브트리 -&gt; 오른쪽 서브트리\n list.add(node.val);\n preOrder(node.left);\n preOrder(node.right);\n}\n\n/* 중위 순회 */\nvoid inOrder(TreeNode? node) {\n if (node == null) return;\n // 방문 우선순위: 왼쪽 서브트리 -&gt; 루트 노드 -&gt; 오른쪽 서브트리\n inOrder(node.left);\n list.add(node.val);\n inOrder(node.right);\n}\n\n/* 후위 순회 */\nvoid postOrder(TreeNode? node) {\n if (node == null) return;\n // 방문 우선순위: 왼쪽 서브트리 -&gt; 오른쪽 서브트리 -&gt; 루트 노드\n postOrder(node.left);\n postOrder(node.right);\n list.add(node.val);\n}\n</code></pre> binary_tree_dfs.rs<pre><code>/* 전위 순회 */\nfn pre_order(root: Option&lt;&amp;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;) -&gt; Vec&lt;i32&gt; {\n let mut result = vec![];\n\n fn dfs(root: Option&lt;&amp;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;, res: &amp;mut Vec&lt;i32&gt;) {\n if let Some(node) = root {\n // 방문 우선순위: 루트 노드 -&gt; 왼쪽 서브트리 -&gt; 오른쪽 서브트리\n let node = node.borrow();\n res.push(node.val);\n dfs(node.left.as_ref(), res);\n dfs(node.right.as_ref(), res);\n }\n }\n dfs(root, &amp;mut result);\n\n result\n}\n\n/* 중위 순회 */\nfn in_order(root: Option&lt;&amp;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;) -&gt; Vec&lt;i32&gt; {\n let mut result = vec![];\n\n fn dfs(root: Option&lt;&amp;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;, res: &amp;mut Vec&lt;i32&gt;) {\n if let Some(node) = root {\n // 방문 우선순위: 왼쪽 서브트리 -&gt; 루트 노드 -&gt; 오른쪽 서브트리\n let node = node.borrow();\n dfs(node.left.as_ref(), res);\n res.push(node.val);\n dfs(node.right.as_ref(), res);\n }\n }\n dfs(root, &amp;mut result);\n\n result\n}\n\n/* 후위 순회 */\nfn post_order(root: Option&lt;&amp;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;) -&gt; Vec&lt;i32&gt; {\n let mut result = vec![];\n\n fn dfs(root: Option&lt;&amp;Rc&lt;RefCell&lt;TreeNode&gt;&gt;&gt;, res: &amp;mut Vec&lt;i32&gt;) {\n if let Some(node) = root {\n // 방문 우선순위: 왼쪽 서브트리 -&gt; 오른쪽 서브트리 -&gt; 루트 노드\n let node = node.borrow();\n dfs(node.left.as_ref(), res);\n dfs(node.right.as_ref(), res);\n res.push(node.val);\n }\n }\n\n dfs(root, &amp;mut result);\n\n result\n}\n</code></pre> binary_tree_dfs.c<pre><code>/* 전위 순회 */\nvoid preOrder(TreeNode *root, int *size) {\n if (root == NULL)\n return;\n // 방문 우선순위: 루트 노드 -&gt; 왼쪽 서브트리 -&gt; 오른쪽 서브트리\n arr[(*size)++] = root-&gt;val;\n preOrder(root-&gt;left, size);\n preOrder(root-&gt;right, size);\n}\n\n/* 중위 순회 */\nvoid inOrder(TreeNode *root, int *size) {\n if (root == NULL)\n return;\n // 방문 우선순위: 왼쪽 서브트리 -&gt; 루트 노드 -&gt; 오른쪽 서브트리\n inOrder(root-&gt;left, size);\n arr[(*size)++] = root-&gt;val;\n inOrder(root-&gt;right, size);\n}\n\n/* 후위 순회 */\nvoid postOrder(TreeNode *root, int *size) {\n if (root == NULL)\n return;\n // 방문 우선순위: 왼쪽 서브트리 -&gt; 오른쪽 서브트리 -&gt; 루트 노드\n postOrder(root-&gt;left, size);\n postOrder(root-&gt;right, size);\n arr[(*size)++] = root-&gt;val;\n}\n</code></pre> binary_tree_dfs.kt<pre><code>/* 전위 순회 */\nfun preOrder(root: TreeNode?) {\n if (root == null) return\n // 방문 우선순위: 루트 노드 -&gt; 왼쪽 서브트리 -&gt; 오른쪽 서브트리\n list.add(root._val)\n preOrder(root.left)\n preOrder(root.right)\n}\n\n/* 중위 순회 */\nfun inOrder(root: TreeNode?) {\n if (root == null) return\n // 방문 우선순위: 왼쪽 서브트리 -&gt; 루트 노드 -&gt; 오른쪽 서브트리\n inOrder(root.left)\n list.add(root._val)\n inOrder(root.right)\n}\n\n/* 후위 순회 */\nfun postOrder(root: TreeNode?) {\n if (root == null) return\n // 방문 우선순위: 왼쪽 서브트리 -&gt; 오른쪽 서브트리 -&gt; 루트 노드\n postOrder(root.left)\n postOrder(root.right)\n list.add(root._val)\n}\n</code></pre> binary_tree_dfs.rb<pre><code>### 전위 순회 ###\ndef pre_order(root)\n return if root.nil?\n\n # 방문 우선순위: 루트 노드 -&gt; 왼쪽 서브트리 -&gt; 오른쪽 서브트리\n $res &lt;&lt; root.val\n pre_order(root.left)\n pre_order(root.right)\nend\n\n### 중위 순회 ###\ndef in_order(root)\n return if root.nil?\n\n # 방문 우선순위: 왼쪽 서브트리 -&gt; 루트 노드 -&gt; 오른쪽 서브트리\n in_order(root.left)\n $res &lt;&lt; root.val\n in_order(root.right)\nend\n\n### 후위 순회 ###\ndef post_order(root)\n return if root.nil?\n\n # 방문 우선순위: 왼쪽 서브트리 -&gt; 오른쪽 서브트리 -&gt; 루트 노드\n post_order(root.left)\n post_order(root.right)\n $res &lt;&lt; root.val\nend\n</code></pre> 코드 시각화 <p> 전체 화면 &gt;</p> <p>참고</p> <p>깊이 우선 탐색은 반복으로도 구현할 수 있다. 관심 있는 독자는 직접 알아보자.</p> <p>아래 그림(7-11)은 이진 트리 전위 순회의 재귀 과정을 보여 준다. 서로 반대 방향인 '호출'과 '반환'으로 나뉜다.</p> <ol> <li>'호출'은 새 메서드를 시작하는 것으로, 프로그램은 이 과정에서 다음 노드를 방문한다.</li> <li>'반환'은 함수가 돌아오는 것으로, 현재 노드의 방문이 끝났음을 뜻한다.</li> </ol> &lt;1&gt;&lt;2&gt;&lt;3&gt;&lt;4&gt;&lt;5&gt;&lt;6&gt;&lt;7&gt;&lt;8&gt;&lt;9&gt;&lt;10&gt;&lt;11&gt; <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p></p> <p> 그림 7-11   전위 순회의 재귀 과정 </p>","path":["제 7 장   트리","7.2   이진 트리 순회"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#2_1","level":3,"title":"2.   복잡도 분석","text":"<ul> <li>시간 복잡도 \\(O(n)\\): 모든 노드를 한 번씩 방문하므로 \\(O(n)\\) 시간이 걸린다.</li> <li>공간 복잡도 \\(O(n)\\): 최악의 경우 트리가 연결 리스트로 퇴화하면 재귀 깊이가 \\(n\\)에 이르러 시스템이 \\(O(n)\\)의 스택 프레임 공간을 사용한다.</li> </ul>","path":["제 7 장   트리","7.2   이진 트리 순회"],"tags":[]},{"location":"chapter_tree/exercises/","level":1,"title":"7.7   연습 문제","text":"","path":["제 7 장   트리","7.7   연습 문제"],"tags":[]},{"location":"chapter_tree/exercises/#771","level":2,"title":"7.7.1   개념 확인","text":"","path":["제 7 장   트리","7.7   연습 문제"],"tags":[]},{"location":"chapter_tree/exercises/#1","level":3,"title":"1.   완전 이진 트리, 정 이진 트리, 포화 이진 트리","text":"<p>다음 두 배열은 이진 트리를 레벨 순서로 나타낸 것이다. <code>None</code>은 빈자리를 뜻한다.</p> <ul> <li>트리 A: <code>[1, 2, 3, 4, 5, 6]</code></li> <li>트리 B: <code>[1, 2, 3, None, None, 6, 7]</code></li> </ul> <ol> <li>어느 트리가 완전 이진 트리인가?</li> <li>어느 트리가 모든 비리프 노드에 자식이 두 개씩 있는 정 이진 트리인가?</li> <li>두 트리 중 포화 이진 트리가 있는가? 각각 이유를 설명하라.</li> </ol> 참고 답안 <ol> <li> <p>트리 A는 완전 이진 트리다. 맨 아래 레벨만 가득 차지 않았고, 노드가 왼쪽부터 연속해서 배치되어 있다. 트리 B는 맨 아래 레벨의 왼쪽에 빈자리가 있는데 오른쪽에 노드가 있으므로 완전 이진 트리가 아니다.</p> </li> <li> <p>트리 B는 정 이진 트리다. 노드 1과 노드 3은 자식이 각각 두 개이고, 나머지는 모두 리프 노드다. 트리 A의 노드 3에는 왼쪽 자식 6만 있으므로 정 이진 트리가 아니다.</p> </li> <li> <p>두 트리 모두 맨 아래 레벨이 완전히 채워져 있지 않으므로 포화 이진 트리가 아니다.</p> </li> </ol>","path":["제 7 장   트리","7.7   연습 문제"],"tags":[]},{"location":"chapter_tree/exercises/#2","level":3,"title":"2.   같은 트리의 세 가지 순회 순서","text":"<p>배열 <code>[1, 2, 3, 4, 5, 6, 7]</code>을 레벨 순서로 완전 이진 트리에 저장한다.</p> <ol> <li>이 트리를 그려 보자.</li> <li>전위, 중위, 후위 순회 결과를 각각 적어 보자.</li> <li>중위 순회 결과에서 루트 노드 1의 왼쪽과 오른쪽에 있는 두 수열은 각각 트리의 어느 부분에 대응하는가?</li> </ol> 참고 답안 <ol> <li> <p>트리는 다음과 같다.</p> <pre><code> 1\n / \\\n 2 3\n / \\ / \\\n 4 5 6 7\n</code></pre> </li> <li> <p>전위 순회는 <code>1, 2, 4, 5, 3, 6, 7</code>이다. 중위 순회는 <code>4, 2, 5, 1, 6, 3, 7</code>이다. 후위 순회는 <code>4, 5, 2, 6, 7, 3, 1</code>이다.</p> </li> <li> <p>루트 노드 1의 왼쪽에 있는 <code>4, 2, 5</code>는 왼쪽 서브트리의 중위 순회 결과다. 오른쪽의 <code>6, 3, 7</code>은 오른쪽 서브트리의 중위 순회 결과다.</p> </li> </ol>","path":["제 7 장   트리","7.7   연습 문제"],"tags":[]},{"location":"chapter_tree/exercises/#3","level":3,"title":"3.   두 이진 탐색 트리 비교하기","text":"<p>다음 두 수열을 각각 왼쪽부터 빈 이진 탐색 트리에 차례대로 삽입한다.</p> <ul> <li>수열 A: <code>[4, 2, 6, 1, 3, 5, 7]</code></li> <li>수열 B: <code>[1, 2, 3, 4, 5, 6, 7]</code></li> </ul> <ol> <li>숫자 7을 찾을 때 거치는 노드를 각각 적어 보자.</li> <li>높이를 루트에서 가장 먼 리프까지 지나는 간선 수로 계산할 때, 두 트리의 높이는 각각 얼마인가?</li> <li>앞선 두 질문을 바탕으로 두 트리에서 7을 찾는 효율성이 같은지 판단하라. 트리의 모양과 탐색 경로를 바탕으로 이유를 설명하라.</li> </ol> 참고 답안 <ol> <li> <p>수열 A로 만든 트리의 탐색 경로는 <code>4 → 6 → 7</code>이다. 수열 B로 만든 트리의 탐색 경로는 <code>1 → 2 → 3 → 4 → 5 → 6 → 7</code>이다.</p> </li> <li> <p>첫 번째 트리는 모든 레벨이 가득 차 있고 높이는 2다. 두 번째 트리는 오른쪽 자식만 있으며 높이는 6이다.</p> </li> <li> <p>두 트리에서 7을 찾는 효율성은 다르다. 삽입 순서가 이진 탐색 트리의 모양과 높이를 바꾸기 때문이다. 첫 번째 트리는 노드 3개만 거치지만, 두 번째 트리는 노드 7개를 차례대로 거친다. 트리가 높을수록 최악의 경우 경로를 따라 비교해야 하는 노드가 많아진다.</p> </li> </ol>","path":["제 7 장   트리","7.7   연습 문제"],"tags":[]},{"location":"chapter_tree/exercises/#772","level":2,"title":"7.7.2   프로그래밍 연습","text":"","path":["제 7 장   트리","7.7   연습 문제"],"tags":[]},{"location":"chapter_tree/exercises/#1_1","level":3,"title":"1.   이진 트리의 최대 깊이","text":"<p>이진 트리의 루트 노드 <code>root</code>가 주어진다. 각 노드는 정숫값과 왼쪽 및 오른쪽 자식 노드에 대한 참조를 가진다.</p> <p>루트에서 가장 먼 리프까지 거치는 노드 수를 이진 트리의 최대 깊이라고 정의한다. 이 트리의 최대 깊이를 반환하라. 빈 트리의 최대 깊이는 0이다. 재귀를 사용하여 해결하라.</p> 풀이 힌트 <ol> <li>이 문제에서는 노드 수로 깊이를 계산한다. 루트 하나만 있는 트리의 최대 깊이는 1이다.</li> <li>재귀 함수가 현재 노드를 루트로 하는 서브트리의 최대 깊이를 반환하도록 한다.</li> <li>빈 노드는 0을 반환하고, 비어 있지 않은 노드는 max(depth(left), depth(right)) + 1을 반환한다.</li> </ol> <p>LeetCode</p>","path":["제 7 장   트리","7.7   연습 문제"],"tags":[]},{"location":"chapter_tree/exercises/#2_1","level":3,"title":"2.   이진 트리를 레벨 순서로 순회하기","text":"<p>이진 트리의 루트 노드 <code>root</code>가 주어진다. 큐를 사용하여 위에서 아래로 한 레벨씩 모든 노드를 방문하라. 같은 레벨에서는 왼쪽부터 오른쪽으로 방문한다.</p> <p>2차원 배열을 반환하라. 첫 번째 하위 배열에는 루트가 있는 레벨의 노드 값을, 두 번째에는 다음 레벨의 노드 값을 저장하며 이 방식을 반복한다. 빈 트리라면 빈 배열을 반환하라.</p> 풀이 힌트 <ol> <li>레벨 순서 순회에서는 먼저 들어온 노드를 먼저 방문해야 하므로 큐를 사용한다.</li> <li>각 반복이 시작될 때 큐에 있는 노드들은 정확히 같은 레벨에 속한다.</li> <li>큐의 길이를 먼저 기록한 뒤 그 수만큼 노드를 꺼내고 각 노드의 자식을 넣는다.</li> </ol> <p>LeetCode</p>","path":["제 7 장   트리","7.7   연습 문제"],"tags":[]},{"location":"chapter_tree/exercises/#3-k","level":3,"title":"3.   이진 탐색 트리에서 k번째로 작은 원소","text":"<p>이진 탐색 트리에 총 <code>n</code>개의 노드가 있으며, 노드 값은 모두 다르다. 모든 노드 값을 작은 것부터 나열하고 위치에 1부터 번호를 붙인다.</p> <p>루트 노드 <code>root</code>와 <code>1 &lt;= k &lt;= n</code>을 만족하는 정수 <code>k</code>가 주어질 때, <code>k</code>번째 노드 값을 반환하라. 모든 값을 미리 모으지 말고 중위 순회 도중에 바로 답을 찾아라.</p> 풀이 힌트 <ol> <li>이진 탐색 트리를 중위 순회하면 노드 값을 작은 것부터 방문한다.</li> <li>중위 순회는 왼쪽 서브트리, 현재 노드, 오른쪽 서브트리 순서로 처리한다. 현재 노드를 방문할 때 개수를 1 늘린다.</li> <li>개수가 처음 k가 되었을 때의 노드 값이 답이다. 이후에는 순회를 계속할 필요가 없다.</li> </ol> <p>LeetCode</p>","path":["제 7 장   트리","7.7   연습 문제"],"tags":[]},{"location":"chapter_tree/summary/","level":1,"title":"7.6   요약","text":"","path":["제 7 장   트리","7.6   요약"],"tags":[]},{"location":"chapter_tree/summary/#1","level":3,"title":"1.   핵심 정리","text":"<ul> <li>이진 트리는 '하나를 둘로 나누는' 분할 정복 논리를 보여 주는 비선형 자료 구조다. 각 노드는 값 하나와 왼쪽, 오른쪽 자식을 가리키는 포인터 두 개를 포함한다.</li> <li>어떤 노드의 왼쪽(오른쪽) 자식과 그 아래 노드들이 이루는 트리를 해당 노드의 왼쪽(오른쪽) 서브트리라고 한다.</li> <li>주요 용어에는 루트 노드, 리프 노드, 레벨, 차수, 간선, 높이, 깊이 등이 있다.</li> <li>이진 트리의 초기화, 노드 삽입과 삭제 방법은 연결 리스트와 비슷하다.</li> <li>주요 종류는 포화 이진 트리, 완전 이진 트리, 정 이진 트리, 균형 이진 트리다. 포화 이진 트리가 가장 이상적이며, 연결 리스트는 퇴화한 최악의 상태다.</li> <li>이진 트리는 노드 값과 빈자리를 레벨 순서대로 배열하여 배열로 표현할 수 있다. 부모와 자식의 인덱스 매핑 관계가 포인터를 대신한다.</li> <li>레벨 순서 순회는 '한 겹씩 바깥으로 확장'하는 너비 우선 탐색이며, 보통 큐로 구현한다.</li> <li>전위, 중위, 후위 순회는 '먼저 끝까지 간 뒤 돌아와 계속하는' 깊이 우선 탐색이며, 보통 재귀로 구현한다.</li> <li>이진 탐색 트리는 효율적인 원소 탐색 자료 구조로, 탐색, 삽입, 삭제의 시간 복잡도는 모두 \\(O(\\log n)\\)이다. 연결 리스트로 퇴화하면 각 복잡도가 \\(O(n)\\)으로 저하된다.</li> <li>AVL 트리는 균형 이진 탐색 트리라고도 하며, 회전 연산으로 삽입과 삭제가 반복되어도 균형을 유지한다.</li> <li>AVL 트리의 회전에는 오른쪽 회전, 왼쪽 회전, 오른쪽 후 왼쪽 회전, 왼쪽 후 오른쪽 회전이 있다. 삽입이나 삭제 후 아래에서 위로 회전하여 균형을 복구한다.</li> </ul>","path":["제 7 장   트리","7.6   요약"],"tags":[]},{"location":"chapter_tree/summary/#2-q-a","level":3,"title":"2.   Q &amp; A","text":"<p>Q: 노드가 하나뿐인 이진 트리의 높이와 루트의 깊이는 모두 \\(0\\)인가?</p> <p>그렇다. 높이와 깊이는 보통 '거치는 간선 수'로 정의하기 때문이다.</p> <p>Q: 이진 트리의 삽입과 삭제를 보통 여러 연산의 조합으로 수행한다고 했는데, 여기서 '여러 연산'은 무엇인가? 해당 자원에 속한 자식 노드의 자원을 해제하는 것으로 이해해도 되는가?</p> <p>이진 탐색 트리를 예로 들면 삭제를 세 경우로 나누어 처리하며, 각 경우마다 여러 단계의 노드 연산이 필요하다.</p> <p>Q: 이진 트리의 DFS에는 왜 전위, 중위, 후위라는 세 순서가 있고 각각 어디에 쓰이는가?</p> <p>배열을 정방향이나 역방향으로 순회하듯이 전위, 중위, 후위는 이진 트리를 순회하는 세 방법이며 특정 순서의 결과를 얻는 데 사용한다. 예를 들어 이진 탐색 트리는 <code>왼쪽 자식 값 &lt; 루트 값 &lt; 오른쪽 자식 값</code>을 만족하므로 '왼쪽 \\(\\rightarrow\\) 루트 \\(\\rightarrow\\) 오른쪽' 우선순위로 순회하면 정렬된 노드 순서를 얻는다.</p> <p>Q: 오른쪽 회전은 불균형 노드 <code>node</code>, <code>child</code>, <code>grand_child</code> 사이의 관계를 처리하는데, <code>node</code>와 그 부모의 기존 연결은 관리하지 않아도 되는가? 회전 후 끊어지지 않는가?</p> <p>재귀의 관점에서 보아야 한다. <code>right_rotate(root)</code>는 서브트리의 루트를 전달받고, 마지막의 <code>return child</code>는 회전 후의 서브트리 루트를 반환한다. 새 서브트리 루트와 부모의 연결은 함수가 반환된 뒤 이루어지므로 오른쪽 회전 자체가 관리할 범위가 아니다.</p> <p>Q: C++에서 함수를 <code>private</code>와 <code>public</code>으로 나누는 기준은 무엇인가? <code>height()</code>는 <code>public</code>, <code>updateHeight()</code>는 <code>private</code>에 두는 이유는 무엇인가?</p> <p>주로 메서드의 사용 범위를 기준으로 한다. 클래스 내부에서만 쓰면 <code>private</code>로 설계한다. 예를 들어 <code>updateHeight()</code>는 삽입과 삭제의 한 단계일 뿐이라 사용자가 따로 호출할 의미가 없다. 반면 <code>height()</code>는 <code>vector.size()</code>처럼 노드 높이에 접근하는 기능이므로 편리하게 쓰도록 <code>public</code>으로 둔다.</p> <p>Q: 입력 데이터 집합에서 이진 탐색 트리를 어떻게 구성하는가? 루트 선택이 중요한가?</p> <p>그렇다. 구성 방법은 이진 탐색 트리 코드의 <code>build_tree()</code>에 제시되어 있다. 루트는 보통 입력을 정렬한 뒤 중간 원소로 정하고, 왼쪽과 오른쪽 서브트리를 재귀적으로 구성한다. 이렇게 하면 트리의 균형을 최대한 보장할 수 있다.</p> <p>Q: Java에서 문자열을 비교할 때 반드시 <code>equals()</code>를 사용해야 하는가?</p> <p>Java에서 기본 타입의 <code>==</code>는 두 변수의 값이 같은지 비교한다. 참조 타입에서는 두 방식의 원리가 다르다.</p> <ul> <li><code>==</code>: 두 변수가 같은 객체, 즉 메모리의 같은 위치를 가리키는지 비교한다.</li> <li><code>equals()</code>: 두 객체의 값이 같은지 비교한다.</li> </ul> <p>따라서 값을 비교하려면 <code>equals()</code>를 사용해야 한다. 다만 <code>String a = \"hi\"; String b = \"hi\";</code>로 초기화한 문자열들은 문자열 상수 풀에 저장되어 같은 객체를 가리키므로, 이 경우에는 <code>a == b</code>로 내용이 같은지 비교할 수도 있다.</p> <p>Q: 너비 우선 순회에서 맨 아래 레벨에 도달하기 전 큐의 노드 수는 \\(2^h\\)인가?</p> <p>그렇다. 예를 들어 높이 \\(h = 2\\)인 포화 이진 트리의 전체 노드 수는 \\(n = 7\\)이고, 맨 아래 레벨의 노드 수는 \\(4 = 2^h = (n + 1) / 2\\)다.</p>","path":["제 7 장   트리","7.6   요약"],"tags":[]}]}