mirror of
https://github.com/krahets/hello-algo.git
synced 2026-04-05 03:30:30 +08:00
build
This commit is contained in:
@@ -33,18 +33,18 @@ comments: true
|
||||
| big-$O$ notation | 大 $O$ 记号 | 大 $O$ 記號 |
|
||||
| asymptotic upper bound | 渐近上界 | 漸近上界 |
|
||||
| sign-magnitude | 原码 | 原碼 |
|
||||
| 1’s complement | 反码 | 反碼 |
|
||||
| 2’s complement | 补码 | 補碼 |
|
||||
| 1’s complement | 反码 | 一補數 |
|
||||
| 2’s complement | 补码 | 二補數 |
|
||||
| array | 数组 | 陣列 |
|
||||
| index | 索引 | 索引 |
|
||||
| linked list | 链表 | 鏈結串列 |
|
||||
| linked list node, list node | 链表节点 | 鏈結串列節點 |
|
||||
| head node | 头节点 | 頭節點 |
|
||||
| tail node | 尾节点 | 尾節點 |
|
||||
| list | 列表 | 列表 |
|
||||
| list | 列表 | 串列 |
|
||||
| dynamic array | 动态数组 | 動態陣列 |
|
||||
| hard disk | 硬盘 | 硬碟 |
|
||||
| random-access memory (RAM) | 内存 | 內存 |
|
||||
| random-access memory (RAM) | 内存 | 記憶體 |
|
||||
| cache memory | 缓存 | 快取 |
|
||||
| cache miss | 缓存未命中 | 快取未命中 |
|
||||
| cache hit rate | 缓存命中率 | 快取命中率 |
|
||||
|
||||
@@ -749,7 +749,7 @@ $$
|
||||
|
||||
$T(n)$ 是一次函数,说明其运行时间的增长趋势是线性的,因此它的时间复杂度是线性阶。
|
||||
|
||||
我们将线性阶的时间复杂度记为 $O(n)$ ,这个数学符号称为<u>大($O$ 记号 big-$O$ notation)</u>,表示函数 $T(n)$ 的<u>渐近上界(asymptotic upper bound)</u>。
|
||||
我们将线性阶的时间复杂度记为 $O(n)$ ,这个数学符号称为<u>大 $O$ 记号(big-$O$ notation)</u>,表示函数 $T(n)$ 的<u>渐近上界(asymptotic upper bound)</u>。
|
||||
|
||||
时间复杂度分析本质上是计算“操作数量 $T(n)$”的渐近上界,它具有明确的数学定义。
|
||||
|
||||
|
||||
@@ -8,64 +8,64 @@
|
||||
style="position: absolute; width: auto; height: 26.445%; left: 28.211%; top: 54.145%;">
|
||||
<img src="assets/hero/links.png" alt=""
|
||||
style="position: absolute; width: auto; height: 78.751%; left: 10.545%; top: 7.326%;">
|
||||
<a href="/chapter_introduction/">
|
||||
<a href="chapter_introduction/">
|
||||
<img src="assets/hero/astronaut.png" alt="" style="height: 46.673%; left: 35.413%; top: 24.343%;">
|
||||
<span style="left: 52.244%; top: 20.919%;">初识算法</span>
|
||||
</a>
|
||||
<a href="/chapter_computational_complexity/">
|
||||
<a href="chapter_computational_complexity/">
|
||||
<img src="assets/hero/chapter_computational_complexity.png" alt=""
|
||||
style="height: 12.347%; left: 36.267%; top: 37.653%;">
|
||||
<span style="left: 39.244%; top: 33.919%;">复杂度</span>
|
||||
</a>
|
||||
<a href="/chapter_array_and_linkedlist/">
|
||||
<a href="chapter_array_and_linkedlist/">
|
||||
<img src="assets/hero/chapter_array_and_linkedlist.png" alt=""
|
||||
style="height: 22.242%; left: 73.242%; top: 52.481%;">
|
||||
<span style="left: 90.897%; top: 76.259%;">数组与链表</span>
|
||||
</a>
|
||||
<a href="/chapter_stack_and_queue/">
|
||||
<a href="chapter_stack_and_queue/">
|
||||
<img src="assets/hero/chapter_stack_and_queue.png" alt=""
|
||||
style="height: 14.302%; left: 62.646%; top: 77.875%;">
|
||||
<span style="left: 77.571%; top: 91.25%;">栈与队列</span>
|
||||
</a>
|
||||
<a href="/chapter_hashing/">
|
||||
<a href="chapter_hashing/">
|
||||
<img src="assets/hero/chapter_hashing.png" alt="" style="height: 15.266%; left: 63.281%; top: 27.933%;">
|
||||
<span style="left: 68.862%; top: 46.292%;">哈希表</span>
|
||||
</a>
|
||||
<a href="/chapter_tree/">
|
||||
<a href="chapter_tree/">
|
||||
<img src="assets/hero/chapter_tree.png" alt="" style="height: 19.615%; left: 80.137%; top: 26.678%;">
|
||||
<span style="left: 96.159%; top: 44.8%;">树</span>
|
||||
</a>
|
||||
<a href="/chapter_heap/">
|
||||
<a href="chapter_heap/">
|
||||
<img src="assets/hero/chapter_heap.png" alt="" style="height: 10.566%; left: 77.226%; top: 11.559%;">
|
||||
<span style="left: 88.103%; top: 15.422%;">堆</span>
|
||||
</a>
|
||||
<a href="/chapter_graph/">
|
||||
<a href="chapter_graph/">
|
||||
<img src="assets/hero/chapter_graph.png" alt="" style="height: 16.112%; left: 51.854%; top: 5.575%;">
|
||||
<span style="left: 71.195%; top: 6.503%;">图</span>
|
||||
</a>
|
||||
<a href="/chapter_searching/">
|
||||
<a href="chapter_searching/">
|
||||
<img src="assets/hero/chapter_searching.png" alt="" style="height: 15.149%; left: 18.185%; top: 16.404%;">
|
||||
<span style="left: 14.556%; top: 20.876%;">搜索</span>
|
||||
</a>
|
||||
<a href="/chapter_sorting/">
|
||||
<a href="chapter_sorting/">
|
||||
<img src="assets/hero/chapter_sorting.png" alt="" style="height: 9.574%; left: 25.409%; top: 40.747%;">
|
||||
<span style="left: 28.805%; top: 53.808%;">排序</span>
|
||||
</a>
|
||||
<a href="/chapter_divide_and_conquer/">
|
||||
<a href="chapter_divide_and_conquer/">
|
||||
<img src="assets/hero/chapter_divide_and_conquer.png" alt=""
|
||||
style="height: 18.681%; left: 32.721%; top: 4.816%;">
|
||||
<span style="left: 31.42%; top: 8.679%;">分治</span>
|
||||
</a>
|
||||
<a href="/chapter_backtracking/">
|
||||
<a href="chapter_backtracking/">
|
||||
<img src="assets/hero/chapter_backtracking.png" alt="" style="height: 17.338%; left: 4.875%; top: 32.925%;">
|
||||
<span style="left: 4.742%; top: 50.113%;">回溯</span>
|
||||
</a>
|
||||
<a href="/chapter_dynamic_programming/">
|
||||
<a href="chapter_dynamic_programming/">
|
||||
<img src="assets/hero/chapter_dynamic_programming.png" alt=""
|
||||
style="height: 15.47%; left: 9.406%; top: 57.472%;">
|
||||
<span style="left: 8.561%; top: 75.351%;">动态规划</span>
|
||||
</a>
|
||||
<a href="/chapter_greedy/">
|
||||
<a href="chapter_greedy/">
|
||||
<img src="assets/hero/chapter_greedy.png" alt="" style="height: 14.127%; left: 23.132%; top: 75.803%;">
|
||||
<span style="left: 21.619%; top: 86.85%;">贪心</span>
|
||||
</a>
|
||||
@@ -80,7 +80,7 @@
|
||||
<p style="margin-top: max(-1vh, -2vw); margin-bottom: min(2vh, 3.5vw);">
|
||||
动画图解、一键运行的数据结构与算法教程
|
||||
</p>
|
||||
<a href="/chapter_hello_algo/" class="rounded-button">
|
||||
<a href="chapter_hello_algo/" class="rounded-button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
|
||||
<path
|
||||
@@ -140,7 +140,7 @@
|
||||
<!-- ipad height = 280.6mm -->
|
||||
<!-- iphone height = 160.7mm -->
|
||||
<div class="media-block">
|
||||
<a href="/chapter_paperbook/">
|
||||
<a href="chapter_paperbook/">
|
||||
<div style="height: 8.17%;"></div>
|
||||
<img class="device-on-hover" style="height: 66.83%;" src="assets/hero/cover_render.png" alt="Cover">
|
||||
<div class="text-button">
|
||||
@@ -153,7 +153,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="media-block">
|
||||
<a href="/chapter_hello_algo/">
|
||||
<a href="chapter_hello_algo/">
|
||||
<div style="height: 4.34%;"></div>
|
||||
<img class="device-on-hover" style="height: 66.31%;" src="assets/hero/web_mac_iphone.png" alt="">
|
||||
<div style="height: 4.34%;"></div>
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 4.1 Arrays
|
||||
# 4.1 Array
|
||||
|
||||
An "array" is a linear data structure that operates as a lineup of similar items, stored together in a computer's memory in contiguous spaces. It's like a sequence that maintains organized storage. Each item in this lineup has its unique 'spot' known as an "index". Please refer to the Figure 4-1 to observe how arrays work and grasp these key terms.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 4-1 Array Definition and Storage Method </p>
|
||||
<p align="center"> Figure 4-1 Array definition and storage method </p>
|
||||
|
||||
## 4.1.1 Common Operations on Arrays
|
||||
## 4.1.1 Common operations on arrays
|
||||
|
||||
### 1. Initializing Arrays
|
||||
### 1. Initializing arrays
|
||||
|
||||
Arrays can be initialized in two ways depending on the needs: either without initial values or with specified initial values. When initial values are not specified, most programming languages will set the array elements to $0$:
|
||||
|
||||
@@ -125,13 +125,13 @@ Arrays can be initialized in two ways depending on the needs: either without ini
|
||||
var nums = [_]i32{ 1, 3, 2, 5, 4 };
|
||||
```
|
||||
|
||||
### 2. Accessing Elements
|
||||
### 2. Accessing elements
|
||||
|
||||
Elements in an array are stored in contiguous memory spaces, making it simpler to compute each element's memory address. The formula shown in the Figure below aids in determining an element's memory address, utilizing the array's memory address (specifically, the first element's address) and the element's index. This computation streamlines direct access to the desired element.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 4-2 Memory Address Calculation for Array Elements </p>
|
||||
<p align="center"> Figure 4-2 Memory address calculation for array elements </p>
|
||||
|
||||
As observed in the above illustration, array indexing conventionally begins at $0$. While this might appear counterintuitive, considering counting usually starts at $1$, within the address calculation formula, **an index is essentially an offset from the memory address**. For the first element's address, this offset is $0$, validating its index as $0$.
|
||||
|
||||
@@ -324,13 +324,13 @@ Accessing elements in an array is highly efficient, allowing us to randomly acce
|
||||
<div style="height: 531px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=import%20random%0A%0Adef%20random_access%28nums%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E9%9A%8F%E6%9C%BA%E8%AE%BF%E9%97%AE%E5%85%83%E7%B4%A0%22%22%22%0A%20%20%20%20%23%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5B0,%20len%28nums%29-1%5D%20%E4%B8%AD%E9%9A%8F%E6%9C%BA%E6%8A%BD%E5%8F%96%E4%B8%80%E4%B8%AA%E6%95%B0%E5%AD%97%0A%20%20%20%20random_index%20%3D%20random.randint%280,%20len%28nums%29%20-%201%29%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E5%B9%B6%E8%BF%94%E5%9B%9E%E9%9A%8F%E6%9C%BA%E5%85%83%E7%B4%A0%0A%20%20%20%20random_num%20%3D%20nums%5Brandom_index%5D%0A%20%20%20%20return%20random_num%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20print%28%22%E6%95%B0%E7%BB%84%20nums%20%3D%22,%20nums%29%0A%0A%20%20%20%20%23%20%E9%9A%8F%E6%9C%BA%E8%AE%BF%E9%97%AE%0A%20%20%20%20random_num%3A%20int%20%3D%20random_access%28nums%29%0A%20%20%20%20print%28%22%E5%9C%A8%20nums%20%E4%B8%AD%E8%8E%B7%E5%8F%96%E9%9A%8F%E6%9C%BA%E5%85%83%E7%B4%A0%22,%20random_num%29%0A&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=7&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=import%20random%0A%0Adef%20random_access%28nums%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E9%9A%8F%E6%9C%BA%E8%AE%BF%E9%97%AE%E5%85%83%E7%B4%A0%22%22%22%0A%20%20%20%20%23%20%E5%9C%A8%E5%8C%BA%E9%97%B4%20%5B0,%20len%28nums%29-1%5D%20%E4%B8%AD%E9%9A%8F%E6%9C%BA%E6%8A%BD%E5%8F%96%E4%B8%80%E4%B8%AA%E6%95%B0%E5%AD%97%0A%20%20%20%20random_index%20%3D%20random.randint%280,%20len%28nums%29%20-%201%29%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E5%B9%B6%E8%BF%94%E5%9B%9E%E9%9A%8F%E6%9C%BA%E5%85%83%E7%B4%A0%0A%20%20%20%20random_num%20%3D%20nums%5Brandom_index%5D%0A%20%20%20%20return%20random_num%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20print%28%22%E6%95%B0%E7%BB%84%20nums%20%3D%22,%20nums%29%0A%0A%20%20%20%20%23%20%E9%9A%8F%E6%9C%BA%E8%AE%BF%E9%97%AE%0A%20%20%20%20random_num%3A%20int%20%3D%20random_access%28nums%29%0A%20%20%20%20print%28%22%E5%9C%A8%20nums%20%E4%B8%AD%E8%8E%B7%E5%8F%96%E9%9A%8F%E6%9C%BA%E5%85%83%E7%B4%A0%22,%20random_num%29%0A&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=7&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Full Screen ></a></div>
|
||||
|
||||
### 3. Inserting Elements
|
||||
### 3. Inserting elements
|
||||
|
||||
Array elements are tightly packed in memory, with no space available to accommodate additional data between them. Illustrated in Figure below, inserting an element in the middle of an array requires shifting all subsequent elements back by one position to create room for the new element.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 4-3 Array Element Insertion Example </p>
|
||||
<p align="center"> Figure 4-3 Array element insertion example </p>
|
||||
|
||||
It's important to note that due to the fixed length of an array, inserting an element will unavoidably result in the loss of the last element in the array. Solutions to address this issue will be explored in the "List" chapter.
|
||||
|
||||
@@ -535,13 +535,13 @@ It's important to note that due to the fixed length of an array, inserting an el
|
||||
<div style="height: 495px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20insert%28nums%3A%20list%5Bint%5D,%20num%3A%20int,%20index%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E5%9C%A8%E6%95%B0%E7%BB%84%E7%9A%84%E7%B4%A2%E5%BC%95%20index%20%E5%A4%84%E6%8F%92%E5%85%A5%E5%85%83%E7%B4%A0%20num%22%22%22%0A%20%20%20%20%23%20%E6%8A%8A%E7%B4%A2%E5%BC%95%20index%20%E4%BB%A5%E5%8F%8A%E4%B9%8B%E5%90%8E%E7%9A%84%E6%89%80%E6%9C%89%E5%85%83%E7%B4%A0%E5%90%91%E5%90%8E%E7%A7%BB%E5%8A%A8%E4%B8%80%E4%BD%8D%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%20-%201,%20index,%20-1%29%3A%0A%20%20%20%20%20%20%20%20nums%5Bi%5D%20%3D%20nums%5Bi%20-%201%5D%0A%20%20%20%20%23%20%E5%B0%86%20num%20%E8%B5%8B%E7%BB%99%20index%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0%0A%20%20%20%20nums%5Bindex%5D%20%3D%20num%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20print%28%22%E6%95%B0%E7%BB%84%20nums%20%3D%22,%20nums%29%0A%0A%20%20%20%20%23%20%E6%8F%92%E5%85%A5%E5%85%83%E7%B4%A0%0A%20%20%20%20insert%28nums,%206,%203%29%0A%20%20%20%20print%28%22%E5%9C%A8%E7%B4%A2%E5%BC%95%203%20%E5%A4%84%E6%8F%92%E5%85%A5%E6%95%B0%E5%AD%97%206%20%EF%BC%8C%E5%BE%97%E5%88%B0%20nums%20%3D%22,%20nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20insert%28nums%3A%20list%5Bint%5D,%20num%3A%20int,%20index%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E5%9C%A8%E6%95%B0%E7%BB%84%E7%9A%84%E7%B4%A2%E5%BC%95%20index%20%E5%A4%84%E6%8F%92%E5%85%A5%E5%85%83%E7%B4%A0%20num%22%22%22%0A%20%20%20%20%23%20%E6%8A%8A%E7%B4%A2%E5%BC%95%20index%20%E4%BB%A5%E5%8F%8A%E4%B9%8B%E5%90%8E%E7%9A%84%E6%89%80%E6%9C%89%E5%85%83%E7%B4%A0%E5%90%91%E5%90%8E%E7%A7%BB%E5%8A%A8%E4%B8%80%E4%BD%8D%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%20-%201,%20index,%20-1%29%3A%0A%20%20%20%20%20%20%20%20nums%5Bi%5D%20%3D%20nums%5Bi%20-%201%5D%0A%20%20%20%20%23%20%E5%B0%86%20num%20%E8%B5%8B%E7%BB%99%20index%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0%0A%20%20%20%20nums%5Bindex%5D%20%3D%20num%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20print%28%22%E6%95%B0%E7%BB%84%20nums%20%3D%22,%20nums%29%0A%0A%20%20%20%20%23%20%E6%8F%92%E5%85%A5%E5%85%83%E7%B4%A0%0A%20%20%20%20insert%28nums,%206,%203%29%0A%20%20%20%20print%28%22%E5%9C%A8%E7%B4%A2%E5%BC%95%203%20%E5%A4%84%E6%8F%92%E5%85%A5%E6%95%B0%E5%AD%97%206%20%EF%BC%8C%E5%BE%97%E5%88%B0%20nums%20%3D%22,%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Full Screen ></a></div>
|
||||
|
||||
### 4. Deleting Elements
|
||||
### 4. Deleting elements
|
||||
|
||||
Similarly, as depicted in the Figure 4-4 , to delete an element at index $i$, all elements following index $i$ must be moved forward by one position.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 4-4 Array Element Deletion Example </p>
|
||||
<p align="center"> Figure 4-4 Array element deletion example </p>
|
||||
|
||||
Please note that after deletion, the former last element becomes "meaningless," hence requiring no specific modification.
|
||||
|
||||
@@ -720,11 +720,11 @@ Please note that after deletion, the former last element becomes "meaningless,"
|
||||
|
||||
In summary, the insertion and deletion operations in arrays present the following disadvantages:
|
||||
|
||||
- **High Time Complexity**: Both insertion and deletion in an array have an average time complexity of $O(n)$, where $n$ is the length of the array.
|
||||
- **Loss of Elements**: Due to the fixed length of arrays, elements that exceed the array's capacity are lost during insertion.
|
||||
- **Waste of Memory**: Initializing a longer array and utilizing only the front part results in "meaningless" end elements during insertion, leading to some wasted memory space.
|
||||
- **High time complexity**: Both insertion and deletion in an array have an average time complexity of $O(n)$, where $n$ is the length of the array.
|
||||
- **Loss of elements**: Due to the fixed length of arrays, elements that exceed the array's capacity are lost during insertion.
|
||||
- **Waste of memory**: Initializing a longer array and utilizing only the front part results in "meaningless" end elements during insertion, leading to some wasted memory space.
|
||||
|
||||
### 5. Traversing Arrays
|
||||
### 5. Traversing arrays
|
||||
|
||||
In most programming languages, we can traverse an array either by using indices or by directly iterating over each element:
|
||||
|
||||
@@ -983,7 +983,7 @@ In most programming languages, we can traverse an array either by using indices
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20traverse%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E9%81%8D%E5%8E%86%E6%95%B0%E7%BB%84%22%22%22%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20%23%20%E9%80%9A%E8%BF%87%E7%B4%A2%E5%BC%95%E9%81%8D%E5%8E%86%E6%95%B0%E7%BB%84%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20nums%5Bi%5D%0A%20%20%20%20%23%20%E7%9B%B4%E6%8E%A5%E9%81%8D%E5%8E%86%E6%95%B0%E7%BB%84%E5%85%83%E7%B4%A0%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20num%0A%20%20%20%20%23%20%E5%90%8C%E6%97%B6%E9%81%8D%E5%8E%86%E6%95%B0%E6%8D%AE%E7%B4%A2%E5%BC%95%E5%92%8C%E5%85%83%E7%B4%A0%0A%20%20%20%20for%20i,%20num%20in%20enumerate%28nums%29%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20num%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20print%28%22%E6%95%B0%E7%BB%84%20nums%20%3D%22,%20nums%29%0A%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E6%95%B0%E7%BB%84%0A%20%20%20%20traverse%28nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20traverse%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E9%81%8D%E5%8E%86%E6%95%B0%E7%BB%84%22%22%22%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20%23%20%E9%80%9A%E8%BF%87%E7%B4%A2%E5%BC%95%E9%81%8D%E5%8E%86%E6%95%B0%E7%BB%84%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20nums%5Bi%5D%0A%20%20%20%20%23%20%E7%9B%B4%E6%8E%A5%E9%81%8D%E5%8E%86%E6%95%B0%E7%BB%84%E5%85%83%E7%B4%A0%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20num%0A%20%20%20%20%23%20%E5%90%8C%E6%97%B6%E9%81%8D%E5%8E%86%E6%95%B0%E6%8D%AE%E7%B4%A2%E5%BC%95%E5%92%8C%E5%85%83%E7%B4%A0%0A%20%20%20%20for%20i,%20num%20in%20enumerate%28nums%29%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20num%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20print%28%22%E6%95%B0%E7%BB%84%20nums%20%3D%22,%20nums%29%0A%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E6%95%B0%E7%BB%84%0A%20%20%20%20traverse%28nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Full Screen ></a></div>
|
||||
|
||||
### 6. Finding Elements
|
||||
### 6. Finding elements
|
||||
|
||||
Locating a specific element within an array involves iterating through the array, checking each element to determine if it matches the desired value.
|
||||
|
||||
@@ -1176,7 +1176,7 @@ Because arrays are linear data structures, this operation is commonly referred t
|
||||
<div style="height: 477px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20find%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%9C%A8%E6%95%B0%E7%BB%84%E4%B8%AD%E6%9F%A5%E6%89%BE%E6%8C%87%E5%AE%9A%E5%85%83%E7%B4%A0%22%22%22%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20if%20nums%5Bi%5D%20%3D%3D%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20i%0A%20%20%20%20return%20-1%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20print%28%22%E6%95%B0%E7%BB%84%20nums%20%3D%22,%20nums%29%0A%0A%20%20%20%20%23%20%E6%9F%A5%E6%89%BE%E5%85%83%E7%B4%A0%0A%20%20%20%20index%3A%20int%20%3D%20find%28nums,%203%29%0A%20%20%20%20print%28%22%E5%9C%A8%20nums%20%E4%B8%AD%E6%9F%A5%E6%89%BE%E5%85%83%E7%B4%A0%203%20%EF%BC%8C%E5%BE%97%E5%88%B0%E7%B4%A2%E5%BC%95%20%3D%22,%20index%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20find%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%9C%A8%E6%95%B0%E7%BB%84%E4%B8%AD%E6%9F%A5%E6%89%BE%E6%8C%87%E5%AE%9A%E5%85%83%E7%B4%A0%22%22%22%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20if%20nums%5Bi%5D%20%3D%3D%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20i%0A%20%20%20%20return%20-1%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20print%28%22%E6%95%B0%E7%BB%84%20nums%20%3D%22,%20nums%29%0A%0A%20%20%20%20%23%20%E6%9F%A5%E6%89%BE%E5%85%83%E7%B4%A0%0A%20%20%20%20index%3A%20int%20%3D%20find%28nums,%203%29%0A%20%20%20%20print%28%22%E5%9C%A8%20nums%20%E4%B8%AD%E6%9F%A5%E6%89%BE%E5%85%83%E7%B4%A0%203%20%EF%BC%8C%E5%BE%97%E5%88%B0%E7%B4%A2%E5%BC%95%20%3D%22,%20index%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Full Screen ></a></div>
|
||||
|
||||
### 7. Expanding Arrays
|
||||
### 7. Expanding arrays
|
||||
|
||||
In complex system environments, ensuring the availability of memory space after an array for safe capacity extension becomes challenging. Consequently, in most programming languages, **the length of an array is immutable**.
|
||||
|
||||
@@ -1422,26 +1422,26 @@ To expand an array, it's necessary to create a larger array and then copy the e
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=%23%20%E8%AF%B7%E6%B3%A8%E6%84%8F%EF%BC%8CPython%20%E7%9A%84%20list%20%E6%98%AF%E5%8A%A8%E6%80%81%E6%95%B0%E7%BB%84%EF%BC%8C%E5%8F%AF%E4%BB%A5%E7%9B%B4%E6%8E%A5%E6%89%A9%E5%B1%95%0A%23%20%E4%B8%BA%E4%BA%86%E6%96%B9%E4%BE%BF%E5%AD%A6%E4%B9%A0%EF%BC%8C%E6%9C%AC%E5%87%BD%E6%95%B0%E5%B0%86%20list%20%E7%9C%8B%E4%BD%9C%E9%95%BF%E5%BA%A6%E4%B8%8D%E5%8F%AF%E5%8F%98%E7%9A%84%E6%95%B0%E7%BB%84%0Adef%20extend%28nums%3A%20list%5Bint%5D,%20enlarge%3A%20int%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%22%22%22%E6%89%A9%E5%B1%95%E6%95%B0%E7%BB%84%E9%95%BF%E5%BA%A6%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E4%B8%80%E4%B8%AA%E6%89%A9%E5%B1%95%E9%95%BF%E5%BA%A6%E5%90%8E%E7%9A%84%E6%95%B0%E7%BB%84%0A%20%20%20%20res%20%3D%20%5B0%5D%20*%20%28len%28nums%29%20%2B%20enlarge%29%0A%20%20%20%20%23%20%E5%B0%86%E5%8E%9F%E6%95%B0%E7%BB%84%E4%B8%AD%E7%9A%84%E6%89%80%E6%9C%89%E5%85%83%E7%B4%A0%E5%A4%8D%E5%88%B6%E5%88%B0%E6%96%B0%E6%95%B0%E7%BB%84%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20res%5Bi%5D%20%3D%20nums%5Bi%5D%0A%20%20%20%20%23%20%E8%BF%94%E5%9B%9E%E6%89%A9%E5%B1%95%E5%90%8E%E7%9A%84%E6%96%B0%E6%95%B0%E7%BB%84%0A%20%20%20%20return%20res%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20print%28%22%E6%95%B0%E7%BB%84%20nums%20%3D%22,%20nums%29%0A%0A%20%20%20%20%23%20%E9%95%BF%E5%BA%A6%E6%89%A9%E5%B1%95%0A%20%20%20%20nums%20%3D%20extend%28nums,%203%29%0A%20%20%20%20print%28%22%E5%B0%86%E6%95%B0%E7%BB%84%E9%95%BF%E5%BA%A6%E6%89%A9%E5%B1%95%E8%87%B3%208%20%EF%BC%8C%E5%BE%97%E5%88%B0%20nums%20%3D%22,%20nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=%23%20%E8%AF%B7%E6%B3%A8%E6%84%8F%EF%BC%8CPython%20%E7%9A%84%20list%20%E6%98%AF%E5%8A%A8%E6%80%81%E6%95%B0%E7%BB%84%EF%BC%8C%E5%8F%AF%E4%BB%A5%E7%9B%B4%E6%8E%A5%E6%89%A9%E5%B1%95%0A%23%20%E4%B8%BA%E4%BA%86%E6%96%B9%E4%BE%BF%E5%AD%A6%E4%B9%A0%EF%BC%8C%E6%9C%AC%E5%87%BD%E6%95%B0%E5%B0%86%20list%20%E7%9C%8B%E4%BD%9C%E9%95%BF%E5%BA%A6%E4%B8%8D%E5%8F%AF%E5%8F%98%E7%9A%84%E6%95%B0%E7%BB%84%0Adef%20extend%28nums%3A%20list%5Bint%5D,%20enlarge%3A%20int%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%22%22%22%E6%89%A9%E5%B1%95%E6%95%B0%E7%BB%84%E9%95%BF%E5%BA%A6%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E4%B8%80%E4%B8%AA%E6%89%A9%E5%B1%95%E9%95%BF%E5%BA%A6%E5%90%8E%E7%9A%84%E6%95%B0%E7%BB%84%0A%20%20%20%20res%20%3D%20%5B0%5D%20*%20%28len%28nums%29%20%2B%20enlarge%29%0A%20%20%20%20%23%20%E5%B0%86%E5%8E%9F%E6%95%B0%E7%BB%84%E4%B8%AD%E7%9A%84%E6%89%80%E6%9C%89%E5%85%83%E7%B4%A0%E5%A4%8D%E5%88%B6%E5%88%B0%E6%96%B0%E6%95%B0%E7%BB%84%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20res%5Bi%5D%20%3D%20nums%5Bi%5D%0A%20%20%20%20%23%20%E8%BF%94%E5%9B%9E%E6%89%A9%E5%B1%95%E5%90%8E%E7%9A%84%E6%96%B0%E6%95%B0%E7%BB%84%0A%20%20%20%20return%20res%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20print%28%22%E6%95%B0%E7%BB%84%20nums%20%3D%22,%20nums%29%0A%0A%20%20%20%20%23%20%E9%95%BF%E5%BA%A6%E6%89%A9%E5%B1%95%0A%20%20%20%20nums%20%3D%20extend%28nums,%203%29%0A%20%20%20%20print%28%22%E5%B0%86%E6%95%B0%E7%BB%84%E9%95%BF%E5%BA%A6%E6%89%A9%E5%B1%95%E8%87%B3%208%20%EF%BC%8C%E5%BE%97%E5%88%B0%20nums%20%3D%22,%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Full Screen ></a></div>
|
||||
|
||||
## 4.1.2 Advantages and Limitations of Arrays
|
||||
## 4.1.2 Advantages and limitations of arrays
|
||||
|
||||
Arrays are stored in contiguous memory spaces and consist of elements of the same type. This approach provides substantial prior information that systems can leverage to optimize the efficiency of data structure operations.
|
||||
|
||||
- **High Space Efficiency**: Arrays allocate a contiguous block of memory for data, eliminating the need for additional structural overhead.
|
||||
- **Support for Random Access**: Arrays allow $O(1)$ time access to any element.
|
||||
- **Cache Locality**: When accessing array elements, the computer not only loads them but also caches the surrounding data, utilizing high-speed cache to enchance subsequent operation speeds.
|
||||
- **High space efficiency**: Arrays allocate a contiguous block of memory for data, eliminating the need for additional structural overhead.
|
||||
- **Support for random access**: Arrays allow $O(1)$ time access to any element.
|
||||
- **Cache locality**: When accessing array elements, the computer not only loads them but also caches the surrounding data, utilizing high-speed cache to enchance subsequent operation speeds.
|
||||
|
||||
However, continuous space storage is a double-edged sword, with the following limitations:
|
||||
|
||||
- **Low Efficiency in Insertion and Deletion**: As arrays accumulate many elements, inserting or deleting elements requires shifting a large number of elements.
|
||||
- **Fixed Length**: The length of an array is fixed after initialization. Expanding an array requires copying all data to a new array, incurring significant costs.
|
||||
- **Space Wastage**: If the allocated array size exceeds the what is necessary, the extra space is wasted.
|
||||
- **Low efficiency in insertion and deletion**: As arrays accumulate many elements, inserting or deleting elements requires shifting a large number of elements.
|
||||
- **Fixed length**: The length of an array is fixed after initialization. Expanding an array requires copying all data to a new array, incurring significant costs.
|
||||
- **Space wastage**: If the allocated array size exceeds the what is necessary, the extra space is wasted.
|
||||
|
||||
## 4.1.3 Typical Applications of Arrays
|
||||
## 4.1.3 Typical applications of arrays
|
||||
|
||||
Arrays are fundamental and widely used data structures. They find frequent application in various algorithms and serve in the implementation of complex data structures.
|
||||
|
||||
- **Random Access**: Arrays are ideal for storing data when random sampling is required. By generating a random sequence based on indices, we can achieve random sampling efficiently.
|
||||
- **Sorting and Searching**: Arrays are the most commonly used data structure for sorting and searching algorithms. Techniques like quick sort, merge sort, binary search, etc., are primarily operate on arrays.
|
||||
- **Lookup Tables**: Arrays serve as efficient lookup tables for quick element or relationship retrieval. For instance, mapping characters to ASCII codes becomes seamless by using the ASCII code values as indices and storing corresponding elements in the array.
|
||||
- **Machine Learning**: Within the domain of neural networks, arrays play a pivotal role in executing crucial linear algebra operations involving vectors, matrices, and tensors. Arrays serve as the primary and most extensively used data structure in neural network programming.
|
||||
- **Data Structure Implementation**: Arrays serve as the building blocks for implementing various data structures like stacks, queues, hash tables, heaps, graphs, etc. For instance, the adjacency matrix representation of a graph is essentially a two-dimensional array.
|
||||
- **Random access**: Arrays are ideal for storing data when random sampling is required. By generating a random sequence based on indices, we can achieve random sampling efficiently.
|
||||
- **Sorting and searching**: Arrays are the most commonly used data structure for sorting and searching algorithms. Techniques like quick sort, merge sort, binary search, etc., are primarily operate on arrays.
|
||||
- **Lookup tables**: Arrays serve as efficient lookup tables for quick element or relationship retrieval. For instance, mapping characters to ASCII codes becomes seamless by using the ASCII code values as indices and storing corresponding elements in the array.
|
||||
- **Machine learning**: Within the domain of neural networks, arrays play a pivotal role in executing crucial linear algebra operations involving vectors, matrices, and tensors. Arrays serve as the primary and most extensively used data structure in neural network programming.
|
||||
- **Data structure implementation**: Arrays serve as the building blocks for implementing various data structures like stacks, queues, hash tables, heaps, graphs, etc. For instance, the adjacency matrix representation of a graph is essentially a two-dimensional array.
|
||||
|
||||
@@ -3,9 +3,9 @@ comments: true
|
||||
icon: material/view-list-outline
|
||||
---
|
||||
|
||||
# Chapter 4. Arrays and Linked Lists
|
||||
# Chapter 4. Arrays and linked lists
|
||||
|
||||
{ class="cover-image" }
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
@@ -16,7 +16,7 @@ icon: material/view-list-outline
|
||||
## Chapter Contents
|
||||
|
||||
- [4.1 Array](https://www.hello-algo.com/en/chapter_array_and_linkedlist/array/)
|
||||
- [4.2 Linked List](https://www.hello-algo.com/en/chapter_array_and_linkedlist/linked_list/)
|
||||
- [4.2 Linked list](https://www.hello-algo.com/en/chapter_array_and_linkedlist/linked_list/)
|
||||
- [4.3 List](https://www.hello-algo.com/en/chapter_array_and_linkedlist/list/)
|
||||
- [4.4 Memory and Cache](https://www.hello-algo.com/en/chapter_array_and_linkedlist/ram_and_cache/)
|
||||
- [4.4 Memory and cache](https://www.hello-algo.com/en/chapter_array_and_linkedlist/ram_and_cache/)
|
||||
- [4.5 Summary](https://www.hello-algo.com/en/chapter_array_and_linkedlist/summary/)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 4.2 Linked Lists
|
||||
# 4.2 Linked list
|
||||
|
||||
Memory space is a shared resource among all programs. In a complex system environment, available memory can be dispersed throughout the memory space. We understand that the memory allocated for an array must be continuous. However, for very large arrays, finding a sufficiently large contiguous memory space might be challenging. This is where the flexible advantage of linked lists becomes evident.
|
||||
|
||||
@@ -10,9 +10,9 @@ A "linked list" is a linear data structure in which each element is a node objec
|
||||
|
||||
The design of linked lists allows for their nodes to be distributed across memory locations without requiring contiguous memory addresses.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 4-5 Linked List Definition and Storage Method </p>
|
||||
<p align="center"> Figure 4-5 Linked list definition and storage method </p>
|
||||
|
||||
As shown in the figure, we see that the basic building block of a linked list is the "node" object. Each node comprises two key components: the node's "value" and a "reference" to the next node.
|
||||
|
||||
@@ -26,7 +26,7 @@ As the code below illustrates, a `ListNode` in a linked list, besides holding a
|
||||
|
||||
```python title=""
|
||||
class ListNode:
|
||||
"""Linked List Node Class"""
|
||||
"""Linked list node class"""
|
||||
def __init__(self, val: int):
|
||||
self.val: int = val # Node value
|
||||
self.next: ListNode | None = None # Reference to the next node
|
||||
@@ -35,7 +35,7 @@ As the code below illustrates, a `ListNode` in a linked list, besides holding a
|
||||
=== "C++"
|
||||
|
||||
```cpp title=""
|
||||
/* Linked List Node Structure */
|
||||
/* Linked list node structure */
|
||||
struct ListNode {
|
||||
int val; // Node value
|
||||
ListNode *next; // Pointer to the next node
|
||||
@@ -46,7 +46,7 @@ As the code below illustrates, a `ListNode` in a linked list, besides holding a
|
||||
=== "Java"
|
||||
|
||||
```java title=""
|
||||
/* Linked List Node Class */
|
||||
/* Linked list node class */
|
||||
class ListNode {
|
||||
int val; // Node value
|
||||
ListNode next; // Reference to the next node
|
||||
@@ -57,7 +57,7 @@ As the code below illustrates, a `ListNode` in a linked list, besides holding a
|
||||
=== "C#"
|
||||
|
||||
```csharp title=""
|
||||
/* Linked List Node Class */
|
||||
/* Linked list node class */
|
||||
class ListNode(int x) { // Constructor
|
||||
int val = x; // Node value
|
||||
ListNode? next; // Reference to the next node
|
||||
@@ -67,7 +67,7 @@ As the code below illustrates, a `ListNode` in a linked list, besides holding a
|
||||
=== "Go"
|
||||
|
||||
```go title=""
|
||||
/* Linked List Node Structure */
|
||||
/* Linked list node structure */
|
||||
type ListNode struct {
|
||||
Val int // Node value
|
||||
Next *ListNode // Pointer to the next node
|
||||
@@ -85,7 +85,7 @@ As the code below illustrates, a `ListNode` in a linked list, besides holding a
|
||||
=== "Swift"
|
||||
|
||||
```swift title=""
|
||||
/* Linked List Node Class */
|
||||
/* Linked list node class */
|
||||
class ListNode {
|
||||
var val: Int // Node value
|
||||
var next: ListNode? // Reference to the next node
|
||||
@@ -99,7 +99,7 @@ As the code below illustrates, a `ListNode` in a linked list, besides holding a
|
||||
=== "JS"
|
||||
|
||||
```javascript title=""
|
||||
/* Linked List Node Class */
|
||||
/* Linked list node class */
|
||||
class ListNode {
|
||||
constructor(val, next) {
|
||||
this.val = (val === undefined ? 0 : val); // Node value
|
||||
@@ -111,7 +111,7 @@ As the code below illustrates, a `ListNode` in a linked list, besides holding a
|
||||
=== "TS"
|
||||
|
||||
```typescript title=""
|
||||
/* Linked List Node Class */
|
||||
/* Linked list node class */
|
||||
class ListNode {
|
||||
val: number;
|
||||
next: ListNode | null;
|
||||
@@ -125,7 +125,7 @@ As the code below illustrates, a `ListNode` in a linked list, besides holding a
|
||||
=== "Dart"
|
||||
|
||||
```dart title=""
|
||||
/* 链表节点类 */
|
||||
/* Linked list node class */
|
||||
class ListNode {
|
||||
int val; // Node value
|
||||
ListNode? next; // Reference to the next node
|
||||
@@ -138,7 +138,7 @@ As the code below illustrates, a `ListNode` in a linked list, besides holding a
|
||||
```rust title=""
|
||||
use std::rc::Rc;
|
||||
use std::cell::RefCell;
|
||||
/* Linked List Node Class */
|
||||
/* Linked list node class */
|
||||
#[derive(Debug)]
|
||||
struct ListNode {
|
||||
val: i32, // Node value
|
||||
@@ -149,7 +149,7 @@ As the code below illustrates, a `ListNode` in a linked list, besides holding a
|
||||
=== "C"
|
||||
|
||||
```c title=""
|
||||
/* Linked List Node Structure */
|
||||
/* Linked list node structure */
|
||||
typedef struct ListNode {
|
||||
int val; // Node value
|
||||
struct ListNode *next; // Pointer to the next node
|
||||
@@ -174,7 +174,7 @@ As the code below illustrates, a `ListNode` in a linked list, besides holding a
|
||||
=== "Zig"
|
||||
|
||||
```zig title=""
|
||||
// Linked List Node Class
|
||||
// Linked list node class
|
||||
pub fn ListNode(comptime T: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
@@ -191,9 +191,9 @@ As the code below illustrates, a `ListNode` in a linked list, besides holding a
|
||||
}
|
||||
```
|
||||
|
||||
## 4.2.1 Common Operations on Linked Lists
|
||||
## 4.2.1 Common operations on linked lists
|
||||
|
||||
### 1. Initializing a Linked List
|
||||
### 1. Initializing a linked list
|
||||
|
||||
Constructing a linked list is a two-step process: first, initializing each node object, and second, forming the reference links between the nodes. After initialization, we can traverse all nodes sequentially from the head node by following the `next` reference.
|
||||
|
||||
@@ -410,15 +410,15 @@ Constructing a linked list is a two-step process: first, initializing each node
|
||||
|
||||
The array as a whole is a variable, for instance, the array `nums` includes elements like `nums[0]`, `nums[1]`, and so on, whereas a linked list is made up of several distinct node objects. **We typically refer to a linked list by its head node**, for example, the linked list in the previous code snippet is referred to as `n0`.
|
||||
|
||||
### 2. Inserting a Node
|
||||
### 2. Inserting nodes
|
||||
|
||||
Inserting a node into a linked list is very easy. As shown in the figure, let's assume we aim to insert a new node `P` between two adjacent nodes `n0` and `n1`. **This can be achieved by simply modifying two node references (pointers)**, with a time complexity of $O(1)$.
|
||||
|
||||
By comparison, inserting an element into an array has a time complexity of $O(n)$, which becomes less efficient when dealing with large data volumes.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 4-6 Linked List Node Insertion Example </p>
|
||||
<p align="center"> Figure 4-6 Linked list node insertion example </p>
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -580,15 +580,15 @@ By comparison, inserting an element into an array has a time complexity of $O(n)
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0Adef%20insert%28n0%3A%20ListNode,%20P%3A%20ListNode%29%3A%0A%20%20%20%20%22%22%22%E5%9C%A8%E9%93%BE%E8%A1%A8%E7%9A%84%E8%8A%82%E7%82%B9%20n0%20%E4%B9%8B%E5%90%8E%E6%8F%92%E5%85%A5%E8%8A%82%E7%82%B9%20P%22%22%22%0A%20%20%20%20n1%20%3D%20n0.next%0A%20%20%20%20P.next%20%3D%20n1%0A%20%20%20%20n0.next%20%3D%20P%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%93%BE%E8%A1%A8%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%90%84%E4%B8%AA%E8%8A%82%E7%82%B9%0A%20%20%20%20n0%20%3D%20ListNode%281%29%0A%20%20%20%20n1%20%3D%20ListNode%283%29%0A%20%20%20%20n2%20%3D%20ListNode%282%29%0A%20%20%20%20n3%20%3D%20ListNode%285%29%0A%20%20%20%20n4%20%3D%20ListNode%284%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%0A%20%20%20%20n0.next%20%3D%20n1%0A%20%20%20%20n1.next%20%3D%20n2%0A%20%20%20%20n2.next%20%3D%20n3%0A%20%20%20%20n3.next%20%3D%20n4%0A%0A%20%20%20%20%23%20%E6%8F%92%E5%85%A5%E8%8A%82%E7%82%B9%0A%20%20%20%20p%20%3D%20ListNode%280%29%0A%20%20%20%20insert%28n0,%20p%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=39&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0Adef%20insert%28n0%3A%20ListNode,%20P%3A%20ListNode%29%3A%0A%20%20%20%20%22%22%22%E5%9C%A8%E9%93%BE%E8%A1%A8%E7%9A%84%E8%8A%82%E7%82%B9%20n0%20%E4%B9%8B%E5%90%8E%E6%8F%92%E5%85%A5%E8%8A%82%E7%82%B9%20P%22%22%22%0A%20%20%20%20n1%20%3D%20n0.next%0A%20%20%20%20P.next%20%3D%20n1%0A%20%20%20%20n0.next%20%3D%20P%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%93%BE%E8%A1%A8%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%90%84%E4%B8%AA%E8%8A%82%E7%82%B9%0A%20%20%20%20n0%20%3D%20ListNode%281%29%0A%20%20%20%20n1%20%3D%20ListNode%283%29%0A%20%20%20%20n2%20%3D%20ListNode%282%29%0A%20%20%20%20n3%20%3D%20ListNode%285%29%0A%20%20%20%20n4%20%3D%20ListNode%284%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%0A%20%20%20%20n0.next%20%3D%20n1%0A%20%20%20%20n1.next%20%3D%20n2%0A%20%20%20%20n2.next%20%3D%20n3%0A%20%20%20%20n3.next%20%3D%20n4%0A%0A%20%20%20%20%23%20%E6%8F%92%E5%85%A5%E8%8A%82%E7%82%B9%0A%20%20%20%20p%20%3D%20ListNode%280%29%0A%20%20%20%20insert%28n0,%20p%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=39&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Full Screen ></a></div>
|
||||
|
||||
### 3. Deleting a Node
|
||||
### 3. Deleting nodes
|
||||
|
||||
As shown in the figure, deleting a node from a linked list is also very easy, **involving only the modification of a single node's reference (pointer)**.
|
||||
|
||||
It's important to note that even though node `P` continues to point to `n1` after being deleted, it becomes inaccessible during linked list traversal. This effectively means that `P` is no longer a part of the linked list.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 4-7 Linked List Node Deletion </p>
|
||||
<p align="center"> Figure 4-7 Linked list node deletion </p>
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -796,7 +796,7 @@ It's important to note that even though node `P` continues to point to `n1` afte
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0Adef%20remove%28n0%3A%20ListNode%29%3A%0A%20%20%20%20%22%22%22%E5%88%A0%E9%99%A4%E9%93%BE%E8%A1%A8%E7%9A%84%E8%8A%82%E7%82%B9%20n0%20%E4%B9%8B%E5%90%8E%E7%9A%84%E9%A6%96%E4%B8%AA%E8%8A%82%E7%82%B9%22%22%22%0A%20%20%20%20if%20not%20n0.next%3A%0A%20%20%20%20%20%20%20%20return%0A%20%20%20%20%23%20n0%20-%3E%20P%20-%3E%20n1%0A%20%20%20%20P%20%3D%20n0.next%0A%20%20%20%20n1%20%3D%20P.next%0A%20%20%20%20n0.next%20%3D%20n1%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%93%BE%E8%A1%A8%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%90%84%E4%B8%AA%E8%8A%82%E7%82%B9%0A%20%20%20%20n0%20%3D%20ListNode%281%29%0A%20%20%20%20n1%20%3D%20ListNode%283%29%0A%20%20%20%20n2%20%3D%20ListNode%282%29%0A%20%20%20%20n3%20%3D%20ListNode%285%29%0A%20%20%20%20n4%20%3D%20ListNode%284%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%0A%20%20%20%20n0.next%20%3D%20n1%0A%20%20%20%20n1.next%20%3D%20n2%0A%20%20%20%20n2.next%20%3D%20n3%0A%20%20%20%20n3.next%20%3D%20n4%0A%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%0A%20%20%20%20remove%28n0%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=34&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0Adef%20remove%28n0%3A%20ListNode%29%3A%0A%20%20%20%20%22%22%22%E5%88%A0%E9%99%A4%E9%93%BE%E8%A1%A8%E7%9A%84%E8%8A%82%E7%82%B9%20n0%20%E4%B9%8B%E5%90%8E%E7%9A%84%E9%A6%96%E4%B8%AA%E8%8A%82%E7%82%B9%22%22%22%0A%20%20%20%20if%20not%20n0.next%3A%0A%20%20%20%20%20%20%20%20return%0A%20%20%20%20%23%20n0%20-%3E%20P%20-%3E%20n1%0A%20%20%20%20P%20%3D%20n0.next%0A%20%20%20%20n1%20%3D%20P.next%0A%20%20%20%20n0.next%20%3D%20n1%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%93%BE%E8%A1%A8%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%90%84%E4%B8%AA%E8%8A%82%E7%82%B9%0A%20%20%20%20n0%20%3D%20ListNode%281%29%0A%20%20%20%20n1%20%3D%20ListNode%283%29%0A%20%20%20%20n2%20%3D%20ListNode%282%29%0A%20%20%20%20n3%20%3D%20ListNode%285%29%0A%20%20%20%20n4%20%3D%20ListNode%284%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%0A%20%20%20%20n0.next%20%3D%20n1%0A%20%20%20%20n1.next%20%3D%20n2%0A%20%20%20%20n2.next%20%3D%20n3%0A%20%20%20%20n3.next%20%3D%20n4%0A%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%0A%20%20%20%20remove%28n0%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=34&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Full Screen ></a></div>
|
||||
|
||||
### 4. Accessing Nodes
|
||||
### 4. Accessing nodes
|
||||
|
||||
**Accessing nodes in a linked list is less efficient**. As previously mentioned, any element in an array can be accessed in $O(1)$ time. In contrast, with a linked list, the program involves starting from the head node and sequentially traversing through the nodes until the desired node is found. In other words, to access the $i$-th node in a linked list, the program must iterate through $i - 1$ nodes, resulting in a time complexity of $O(n)$.
|
||||
|
||||
@@ -1005,7 +1005,7 @@ It's important to note that even though node `P` continues to point to `n1` afte
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0Adef%20access%28head%3A%20ListNode,%20index%3A%20int%29%20-%3E%20ListNode%20%7C%20None%3A%0A%20%20%20%20%22%22%22%E8%AE%BF%E9%97%AE%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%B4%A2%E5%BC%95%E4%B8%BA%20index%20%E7%9A%84%E8%8A%82%E7%82%B9%22%22%22%0A%20%20%20%20for%20_%20in%20range%28index%29%3A%0A%20%20%20%20%20%20%20%20if%20not%20head%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20None%0A%20%20%20%20%20%20%20%20head%20%3D%20head.next%0A%20%20%20%20return%20head%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%93%BE%E8%A1%A8%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%90%84%E4%B8%AA%E8%8A%82%E7%82%B9%0A%20%20%20%20n0%20%3D%20ListNode%281%29%0A%20%20%20%20n1%20%3D%20ListNode%283%29%0A%20%20%20%20n2%20%3D%20ListNode%282%29%0A%20%20%20%20n3%20%3D%20ListNode%285%29%0A%20%20%20%20n4%20%3D%20ListNode%284%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%0A%20%20%20%20n0.next%20%3D%20n1%0A%20%20%20%20n1.next%20%3D%20n2%0A%20%20%20%20n2.next%20%3D%20n3%0A%20%20%20%20n3.next%20%3D%20n4%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E8%8A%82%E7%82%B9%0A%20%20%20%20node%20%3D%20access%28n0,%203%29%0A%20%20%20%20print%28%22%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%B4%A2%E5%BC%95%203%20%E5%A4%84%E7%9A%84%E8%8A%82%E7%82%B9%E7%9A%84%E5%80%BC%20%3D%20%7B%7D%22.format%28node.val%29%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=34&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0Adef%20access%28head%3A%20ListNode,%20index%3A%20int%29%20-%3E%20ListNode%20%7C%20None%3A%0A%20%20%20%20%22%22%22%E8%AE%BF%E9%97%AE%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%B4%A2%E5%BC%95%E4%B8%BA%20index%20%E7%9A%84%E8%8A%82%E7%82%B9%22%22%22%0A%20%20%20%20for%20_%20in%20range%28index%29%3A%0A%20%20%20%20%20%20%20%20if%20not%20head%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20None%0A%20%20%20%20%20%20%20%20head%20%3D%20head.next%0A%20%20%20%20return%20head%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%93%BE%E8%A1%A8%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%90%84%E4%B8%AA%E8%8A%82%E7%82%B9%0A%20%20%20%20n0%20%3D%20ListNode%281%29%0A%20%20%20%20n1%20%3D%20ListNode%283%29%0A%20%20%20%20n2%20%3D%20ListNode%282%29%0A%20%20%20%20n3%20%3D%20ListNode%285%29%0A%20%20%20%20n4%20%3D%20ListNode%284%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%0A%20%20%20%20n0.next%20%3D%20n1%0A%20%20%20%20n1.next%20%3D%20n2%0A%20%20%20%20n2.next%20%3D%20n3%0A%20%20%20%20n3.next%20%3D%20n4%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E8%8A%82%E7%82%B9%0A%20%20%20%20node%20%3D%20access%28n0,%203%29%0A%20%20%20%20print%28%22%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%B4%A2%E5%BC%95%203%20%E5%A4%84%E7%9A%84%E8%8A%82%E7%82%B9%E7%9A%84%E5%80%BC%20%3D%20%7B%7D%22.format%28node.val%29%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=34&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Full Screen ></a></div>
|
||||
|
||||
### 5. Finding Nodes
|
||||
### 5. Finding nodes
|
||||
|
||||
Traverse the linked list to locate a node whose value matches `target`, and then output the index of that node within the linked list. This procedure is also an example of linear search. The corresponding code is provided below:
|
||||
|
||||
@@ -1241,11 +1241,11 @@ Traverse the linked list to locate a node whose value matches `target`, and then
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0Adef%20find%28head%3A%20ListNode,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%9C%A8%E9%93%BE%E8%A1%A8%E4%B8%AD%E6%9F%A5%E6%89%BE%E5%80%BC%E4%B8%BA%20target%20%E7%9A%84%E9%A6%96%E4%B8%AA%E8%8A%82%E7%82%B9%22%22%22%0A%20%20%20%20index%20%3D%200%0A%20%20%20%20while%20head%3A%0A%20%20%20%20%20%20%20%20if%20head.val%20%3D%3D%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20index%0A%20%20%20%20%20%20%20%20head%20%3D%20head.next%0A%20%20%20%20%20%20%20%20index%20%2B%3D%201%0A%20%20%20%20return%20-1%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%93%BE%E8%A1%A8%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%90%84%E4%B8%AA%E8%8A%82%E7%82%B9%0A%20%20%20%20n0%20%3D%20ListNode%281%29%0A%20%20%20%20n1%20%3D%20ListNode%283%29%0A%20%20%20%20n2%20%3D%20ListNode%282%29%0A%20%20%20%20n3%20%3D%20ListNode%285%29%0A%20%20%20%20n4%20%3D%20ListNode%284%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%0A%20%20%20%20n0.next%20%3D%20n1%0A%20%20%20%20n1.next%20%3D%20n2%0A%20%20%20%20n2.next%20%3D%20n3%0A%20%20%20%20n3.next%20%3D%20n4%0A%0A%20%20%20%20%23%20%E6%9F%A5%E6%89%BE%E8%8A%82%E7%82%B9%0A%20%20%20%20index%20%3D%20find%28n0,%202%29%0A%20%20%20%20print%28%22%E9%93%BE%E8%A1%A8%E4%B8%AD%E5%80%BC%E4%B8%BA%202%20%E7%9A%84%E8%8A%82%E7%82%B9%E7%9A%84%E7%B4%A2%E5%BC%95%20%3D%20%7B%7D%22.format%28index%29%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=34&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0Adef%20find%28head%3A%20ListNode,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%9C%A8%E9%93%BE%E8%A1%A8%E4%B8%AD%E6%9F%A5%E6%89%BE%E5%80%BC%E4%B8%BA%20target%20%E7%9A%84%E9%A6%96%E4%B8%AA%E8%8A%82%E7%82%B9%22%22%22%0A%20%20%20%20index%20%3D%200%0A%20%20%20%20while%20head%3A%0A%20%20%20%20%20%20%20%20if%20head.val%20%3D%3D%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20index%0A%20%20%20%20%20%20%20%20head%20%3D%20head.next%0A%20%20%20%20%20%20%20%20index%20%2B%3D%201%0A%20%20%20%20return%20-1%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%93%BE%E8%A1%A8%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%90%84%E4%B8%AA%E8%8A%82%E7%82%B9%0A%20%20%20%20n0%20%3D%20ListNode%281%29%0A%20%20%20%20n1%20%3D%20ListNode%283%29%0A%20%20%20%20n2%20%3D%20ListNode%282%29%0A%20%20%20%20n3%20%3D%20ListNode%285%29%0A%20%20%20%20n4%20%3D%20ListNode%284%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%0A%20%20%20%20n0.next%20%3D%20n1%0A%20%20%20%20n1.next%20%3D%20n2%0A%20%20%20%20n2.next%20%3D%20n3%0A%20%20%20%20n3.next%20%3D%20n4%0A%0A%20%20%20%20%23%20%E6%9F%A5%E6%89%BE%E8%8A%82%E7%82%B9%0A%20%20%20%20index%20%3D%20find%28n0,%202%29%0A%20%20%20%20print%28%22%E9%93%BE%E8%A1%A8%E4%B8%AD%E5%80%BC%E4%B8%BA%202%20%E7%9A%84%E8%8A%82%E7%82%B9%E7%9A%84%E7%B4%A2%E5%BC%95%20%3D%20%7B%7D%22.format%28index%29%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=34&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Full Screen ></a></div>
|
||||
|
||||
## 4.2.2 Arrays vs. Linked Lists
|
||||
## 4.2.2 Arrays vs. linked lists
|
||||
|
||||
The Table 4-1 summarizes the characteristics of arrays and linked lists, and it also compares their efficiencies in various operations. Because they utilize opposing storage strategies, their respective properties and operational efficiencies exhibit distinct contrasts.
|
||||
|
||||
<p align="center"> Table 4-1 Efficiency Comparison of Arrays and Linked Lists </p>
|
||||
<p align="center"> Table 4-1 Efficiency comparison of arrays and linked lists </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
@@ -1260,19 +1260,19 @@ The Table 4-1 summarizes the characteristics of arrays and linked lists, and it
|
||||
|
||||
</div>
|
||||
|
||||
## 4.2.3 Common Types of Linked Lists
|
||||
## 4.2.3 Common types of linked lists
|
||||
|
||||
As shown in the figure, there are three common types of linked lists.
|
||||
|
||||
- **Singly Linked List**: This is the standard linked list described earlier. Nodes in a singly linked list include a value and a reference to the next node. The first node is known as the head node, and the last node, which points to null (`None`), is the tail node.
|
||||
- **Circular Linked List**: This is formed when the tail node of a singly linked list points back to the head node, creating a loop. In a circular linked list, any node can function as the head node.
|
||||
- **Doubly Linked List**: In contrast to a singly linked list, a doubly linked list maintains references in two directions. Each node contains references (pointer) to both its successor (the next node) and predecessor (the previous node). Although doubly linked lists offer more flexibility for traversing in either direction, they also consume more memory space.
|
||||
- **Singly linked list**: This is the standard linked list described earlier. Nodes in a singly linked list include a value and a reference to the next node. The first node is known as the head node, and the last node, which points to null (`None`), is the tail node.
|
||||
- **Circular linked list**: This is formed when the tail node of a singly linked list points back to the head node, creating a loop. In a circular linked list, any node can function as the head node.
|
||||
- **Doubly linked list**: In contrast to a singly linked list, a doubly linked list maintains references in two directions. Each node contains references (pointer) to both its successor (the next node) and predecessor (the previous node). Although doubly linked lists offer more flexibility for traversing in either direction, they also consume more memory space.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title=""
|
||||
class ListNode:
|
||||
"""Bidirectional linked list node class""""
|
||||
"""Bidirectional linked list node class"""
|
||||
def __init__(self, val: int):
|
||||
self.val: int = val # Node value
|
||||
self.next: ListNode | None = None # Reference to the successor node
|
||||
@@ -1465,25 +1465,25 @@ As shown in the figure, there are three common types of linked lists.
|
||||
}
|
||||
```
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 4-8 Common Types of Linked Lists </p>
|
||||
<p align="center"> Figure 4-8 Common types of linked lists </p>
|
||||
|
||||
## 4.2.4 Typical Applications of Linked Lists
|
||||
## 4.2.4 Typical applications of linked lists
|
||||
|
||||
Singly linked lists are frequently utilized in implementing stacks, queues, hash tables, and graphs.
|
||||
|
||||
- **Stacks and Queues**: In singly linked lists, if insertions and deletions occur at the same end, it behaves like a stack (last-in-first-out). Conversely, if insertions are at one end and deletions at the other, it functions like a queue (first-in-first-out).
|
||||
- **Hash Tables**: Linked lists are used in chaining, a popular method for resolving hash collisions. Here, all collided elements are grouped into a linked list.
|
||||
- **Stacks and queues**: In singly linked lists, if insertions and deletions occur at the same end, it behaves like a stack (last-in-first-out). Conversely, if insertions are at one end and deletions at the other, it functions like a queue (first-in-first-out).
|
||||
- **Hash tables**: Linked lists are used in chaining, a popular method for resolving hash collisions. Here, all collided elements are grouped into a linked list.
|
||||
- **Graphs**: Adjacency lists, a standard method for graph representation, associate each graph vertex with a linked list. This list contains elements that represent vertices connected to the corresponding vertex.
|
||||
|
||||
Doubly linked lists are ideal for scenarios requiring rapid access to preceding and succeeding elements.
|
||||
|
||||
- **Advanced Data Structures**: In structures like red-black trees and B-trees, accessing a node's parent is essential. This is achieved by incorporating a reference to the parent node in each node, akin to a doubly linked list.
|
||||
- **Browser History**: In web browsers, doubly linked lists facilitate navigating the history of visited pages when users click forward or back.
|
||||
- **LRU Algorithm**: Doubly linked lists are apt for Least Recently Used (LRU) cache eviction algorithms, enabling swift identification of the least recently used data and facilitating fast node addition and removal.
|
||||
- **Advanced data structures**: In structures like red-black trees and B-trees, accessing a node's parent is essential. This is achieved by incorporating a reference to the parent node in each node, akin to a doubly linked list.
|
||||
- **Browser history**: In web browsers, doubly linked lists facilitate navigating the history of visited pages when users click forward or back.
|
||||
- **LRU algorithm**: Doubly linked lists are apt for Least Recently Used (LRU) cache eviction algorithms, enabling swift identification of the least recently used data and facilitating fast node addition and removal.
|
||||
|
||||
Circular linked lists are ideal for applications that require periodic operations, such as resource scheduling in operating systems.
|
||||
|
||||
- **Round-Robin Scheduling Algorithm**: In operating systems, the round-robin scheduling algorithm is a common CPU scheduling method, requiring cycling through a group of processes. Each process is assigned a time slice, and upon expiration, the CPU rotates to the next process. This cyclical operation can be efficiently realized using a circular linked list, allowing for a fair and time-shared system among all processes.
|
||||
- **Data Buffers**: Circular linked lists are also used in data buffers, like in audio and video players, where the data stream is divided into multiple buffer blocks arranged in a circular fashion for seamless playback.
|
||||
- **Round-robin scheduling algorithm**: In operating systems, the round-robin scheduling algorithm is a common CPU scheduling method, requiring cycling through a group of processes. Each process is assigned a time slice, and upon expiration, the CPU rotates to the next process. This cyclical operation can be efficiently realized using a circular linked list, allowing for a fair and time-shared system among all processes.
|
||||
- **Data buffers**: Circular linked lists are also used in data buffers, like in audio and video players, where the data stream is divided into multiple buffer blocks arranged in a circular fashion for seamless playback.
|
||||
|
||||
@@ -15,9 +15,9 @@ To solve this problem, we can implement lists using a "dynamic array." It inheri
|
||||
|
||||
In fact, **many programming languages' standard libraries implement lists using dynamic arrays**, such as Python's `list`, Java's `ArrayList`, C++'s `vector`, and C#'s `List`. In the following discussion, we will consider "list" and "dynamic array" as synonymous concepts.
|
||||
|
||||
## 4.3.1 Common List Operations
|
||||
## 4.3.1 Common list operations
|
||||
|
||||
### 1. Initializing a List
|
||||
### 1. Initializing a list
|
||||
|
||||
We typically use two initialization methods: "without initial values" and "with initial values".
|
||||
|
||||
@@ -145,7 +145,7 @@ We typically use two initialization methods: "without initial values" and "with
|
||||
try nums.appendSlice(&[_]i32{ 1, 3, 2, 5, 4 });
|
||||
```
|
||||
|
||||
### 2. Accessing Elements
|
||||
### 2. Accessing elements
|
||||
|
||||
Lists are essentially arrays, thus they can access and update elements in $O(1)$ time, which is very efficient.
|
||||
|
||||
@@ -270,7 +270,7 @@ Lists are essentially arrays, thus they can access and update elements in $O(1)$
|
||||
nums.items[1] = 0; // Update the element at index 1 to 0
|
||||
```
|
||||
|
||||
### 3. Inserting and Removing Elements
|
||||
### 3. Inserting and removing elements
|
||||
|
||||
Compared to arrays, lists offer more flexibility in adding and removing elements. While adding elements to the end of a list is an $O(1)$ operation, the efficiency of inserting and removing elements elsewhere in the list remains the same as in arrays, with a time complexity of $O(n)$.
|
||||
|
||||
@@ -506,7 +506,7 @@ Compared to arrays, lists offer more flexibility in adding and removing elements
|
||||
_ = nums.orderedRemove(3); // Remove the element at index 3
|
||||
```
|
||||
|
||||
### 4. Iterating the List
|
||||
### 4. Iterating the list
|
||||
|
||||
Similar to arrays, lists can be iterated either by using indices or by directly iterating through each element.
|
||||
|
||||
@@ -695,7 +695,7 @@ Similar to arrays, lists can be iterated either by using indices or by directly
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Concatenating Lists
|
||||
### 5. Concatenating lists
|
||||
|
||||
Given a new list `nums1`, we can append it to the end of the original list.
|
||||
|
||||
@@ -802,7 +802,7 @@ Given a new list `nums1`, we can append it to the end of the original list.
|
||||
try nums.insertSlice(nums.items.len, nums1.items); // Concatenate nums1 to the end of nums
|
||||
```
|
||||
|
||||
### 6. Sorting the List
|
||||
### 6. Sorting the list
|
||||
|
||||
Once the list is sorted, we can employ algorithms commonly used in array-related algorithm problems, such as "binary search" and "two-pointer" algorithms.
|
||||
|
||||
@@ -895,15 +895,15 @@ Once the list is sorted, we can employ algorithms commonly used in array-related
|
||||
std.sort.sort(i32, nums.items, {}, comptime std.sort.asc(i32));
|
||||
```
|
||||
|
||||
## 4.3.2 List Implementation
|
||||
## 4.3.2 List implementation
|
||||
|
||||
Many programming languages come with built-in lists, including Java, C++, Python, etc. Their implementations tend to be intricate, featuring carefully considered settings for various parameters, like initial capacity and expansion factors. Readers who are curious can delve into the source code for further learning.
|
||||
|
||||
To enhance our understanding of how lists work, we will attempt to implement a simplified version of a list, focusing on three crucial design aspects:
|
||||
|
||||
- **Initial Capacity**: Choose a reasonable initial capacity for the array. In this example, we choose 10 as the initial capacity.
|
||||
- **Size Recording**: Declare a variable `size` to record the current number of elements in the list, updating in real-time with element insertion and deletion. With this variable, we can locate the end of the list and determine whether expansion is needed.
|
||||
- **Expansion Mechanism**: If the list reaches full capacity upon an element insertion, an expansion process is required. This involves creating a larger array based on the expansion factor, and then transferring all elements from the current array to the new one. In this example, we stipulate that the array size should double with each expansion.
|
||||
- **Initial capacity**: Choose a reasonable initial capacity for the array. In this example, we choose 10 as the initial capacity.
|
||||
- **Size recording**: Declare a variable `size` to record the current number of elements in the list, updating in real-time with element insertion and deletion. With this variable, we can locate the end of the list and determine whether expansion is needed.
|
||||
- **Expansion mechanism**: If the list reaches full capacity upon an element insertion, an expansion process is required. This involves creating a larger array based on the expansion factor, and then transferring all elements from the current array to the new one. In this example, we stipulate that the array size should double with each expansion.
|
||||
|
||||
=== "Python"
|
||||
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 4.4 Memory and Cache *
|
||||
# 4.4 Memory and cache *
|
||||
|
||||
In the first two sections of this chapter, we explored arrays and linked lists, two fundamental and important data structures, representing "continuous storage" and "dispersed storage" respectively.
|
||||
|
||||
In fact, **the physical structure largely determines the efficiency of a program's use of memory and cache**, which in turn affects the overall performance of the algorithm.
|
||||
|
||||
## 4.4.1 Computer Storage Devices
|
||||
## 4.4.1 Computer storage devices
|
||||
|
||||
There are three types of storage devices in computers: "hard disk," "random-access memory (RAM)," and "cache memory." The following table shows their different roles and performance characteristics in computer systems.
|
||||
|
||||
<p align="center"> Table 4-2 Computer Storage Devices </p>
|
||||
<p align="center"> Table 4-2 Computer storage devices </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
@@ -31,9 +31,9 @@ We can imagine the computer storage system as a pyramid structure shown in the F
|
||||
- **Hard disks are difficult to replace with memory**. Firstly, data in memory is lost after power off, making it unsuitable for long-term data storage; secondly, the cost of memory is dozens of times that of hard disks, making it difficult to popularize in the consumer market.
|
||||
- **It is difficult for caches to have both large capacity and high speed**. As the capacity of L1, L2, L3 caches gradually increases, their physical size becomes larger, increasing the physical distance from the CPU core, leading to increased data transfer time and higher element access latency. Under current technology, a multi-level cache structure is the best balance between capacity, speed, and cost.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 4-9 Computer Storage System </p>
|
||||
<p align="center"> Figure 4-9 Computer storage system </p>
|
||||
|
||||
!!! note
|
||||
|
||||
@@ -43,11 +43,11 @@ Overall, **hard disks are used for long-term storage of large amounts of data, m
|
||||
|
||||
As shown in the Figure 4-10 , during program execution, data is read from the hard disk into memory for CPU computation. The cache can be considered a part of the CPU, **smartly loading data from memory** to provide fast data access to the CPU, significantly enhancing program execution efficiency and reducing reliance on slower memory.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 4-10 Data Flow Between Hard Disk, Memory, and Cache </p>
|
||||
<p align="center"> Figure 4-10 Data flow between hard disk, memory, and cache </p>
|
||||
|
||||
## 4.4.2 Memory Efficiency of Data Structures
|
||||
## 4.4.2 Memory efficiency of data structures
|
||||
|
||||
In terms of memory space utilization, arrays and linked lists have their advantages and limitations.
|
||||
|
||||
@@ -55,7 +55,7 @@ On one hand, **memory is limited and cannot be shared by multiple programs**, so
|
||||
|
||||
On the other hand, during program execution, **as memory is repeatedly allocated and released, the degree of fragmentation of free memory becomes higher**, leading to reduced memory utilization efficiency. Arrays, due to their continuous storage method, are relatively less likely to cause memory fragmentation. In contrast, the elements of a linked list are dispersedly stored, and frequent insertion and deletion operations make memory fragmentation more likely.
|
||||
|
||||
## 4.4.3 Cache Efficiency of Data Structures
|
||||
## 4.4.3 Cache efficiency of data structures
|
||||
|
||||
Although caches are much smaller in space capacity than memory, they are much faster and play a crucial role in program execution speed. Since the cache's capacity is limited and can only store a small part of frequently accessed data, when the CPU tries to access data not in the cache, a "cache miss" occurs, forcing the CPU to load the needed data from slower memory.
|
||||
|
||||
@@ -63,17 +63,17 @@ Clearly, **the fewer the cache misses, the higher the CPU's data read-write effi
|
||||
|
||||
To achieve higher efficiency, caches adopt the following data loading mechanisms.
|
||||
|
||||
- **Cache Lines**: Caches don't store and load data byte by byte but in units of cache lines. Compared to byte-by-byte transfer, the transmission of cache lines is more efficient.
|
||||
- **Prefetch Mechanism**: Processors try to predict data access patterns (such as sequential access, fixed stride jumping access, etc.) and load data into the cache according to specific patterns to improve the hit rate.
|
||||
- **Spatial Locality**: If data is accessed, data nearby is likely to be accessed in the near future. Therefore, when loading certain data, the cache also loads nearby data to improve the hit rate.
|
||||
- **Temporal Locality**: If data is accessed, it's likely to be accessed again in the near future. Caches use this principle to retain recently accessed data to improve the hit rate.
|
||||
- **Cache lines**: Caches don't store and load data byte by byte but in units of cache lines. Compared to byte-by-byte transfer, the transmission of cache lines is more efficient.
|
||||
- **Prefetch mechanism**: Processors try to predict data access patterns (such as sequential access, fixed stride jumping access, etc.) and load data into the cache according to specific patterns to improve the hit rate.
|
||||
- **Spatial locality**: If data is accessed, data nearby is likely to be accessed in the near future. Therefore, when loading certain data, the cache also loads nearby data to improve the hit rate.
|
||||
- **Temporal locality**: If data is accessed, it's likely to be accessed again in the near future. Caches use this principle to retain recently accessed data to improve the hit rate.
|
||||
|
||||
In fact, **arrays and linked lists have different cache utilization efficiencies**, mainly reflected in the following aspects.
|
||||
|
||||
- **Occupied Space**: Linked list elements occupy more space than array elements, resulting in less effective data volume in the cache.
|
||||
- **Cache Lines**: Linked list data is scattered throughout memory, and since caches load "by line," the proportion of loading invalid data is higher.
|
||||
- **Prefetch Mechanism**: The data access pattern of arrays is more "predictable" than that of linked lists, meaning the system is more likely to guess which data will be loaded next.
|
||||
- **Spatial Locality**: Arrays are stored in concentrated memory spaces, so the data near the loaded data is more likely to be accessed next.
|
||||
- **Occupied space**: Linked list elements occupy more space than array elements, resulting in less effective data volume in the cache.
|
||||
- **Cache lines**: Linked list data is scattered throughout memory, and since caches load "by line," the proportion of loading invalid data is higher.
|
||||
- **Prefetch mechanism**: The data access pattern of arrays is more "predictable" than that of linked lists, meaning the system is more likely to guess which data will be loaded next.
|
||||
- **Spatial locality**: Arrays are stored in concentrated memory spaces, so the data near the loaded data is more likely to be accessed next.
|
||||
|
||||
Overall, **arrays have a higher cache hit rate and are generally more efficient in operation than linked lists**. This makes data structures based on arrays more popular in solving algorithmic problems.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ comments: true
|
||||
|
||||
# 4.5 Summary
|
||||
|
||||
### 1. Key Review
|
||||
### 1. Key review
|
||||
|
||||
- Arrays and linked lists are two basic data structures, representing two storage methods in computer memory: contiguous space storage and non-contiguous space storage. Their characteristics complement each other.
|
||||
- Arrays support random access and use less memory; however, they are inefficient in inserting and deleting elements and have a fixed length after initialization.
|
||||
@@ -33,7 +33,7 @@ Linked lists consist of nodes connected by references (pointers), and each node
|
||||
In contrast, array elements must be of the same type, allowing the calculation of offsets to access the corresponding element positions. For example, an array containing both int and long types, with single elements occupying 4 bytes and 8 bytes respectively, cannot use the following formula to calculate offsets, as the array contains elements of two different lengths.
|
||||
|
||||
```shell
|
||||
# Element memory address = Array memory address + Element length * Element index
|
||||
# Element memory address = array memory address + element length * element index
|
||||
```
|
||||
|
||||
**Q**: After deleting a node, is it necessary to set `P.next` to `None`?
|
||||
|
||||
@@ -3,9 +3,9 @@ comments: true
|
||||
icon: material/timer-sand
|
||||
---
|
||||
|
||||
# Chapter 2. Complexity Analysis
|
||||
# Chapter 2. Complexity analysis
|
||||
|
||||
{ class="cover-image" }
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
@@ -15,8 +15,8 @@ icon: material/timer-sand
|
||||
|
||||
## Chapter Contents
|
||||
|
||||
- [2.1 Algorithm Efficiency Assessment](https://www.hello-algo.com/en/chapter_computational_complexity/performance_evaluation/)
|
||||
- [2.2 Iteration and Recursion](https://www.hello-algo.com/en/chapter_computational_complexity/iteration_and_recursion/)
|
||||
- [2.3 Time Complexity](https://www.hello-algo.com/en/chapter_computational_complexity/time_complexity/)
|
||||
- [2.4 Space Complexity](https://www.hello-algo.com/en/chapter_computational_complexity/space_complexity/)
|
||||
- [2.1 Algorithm efficiency assessment](https://www.hello-algo.com/en/chapter_computational_complexity/performance_evaluation/)
|
||||
- [2.2 Iteration and recursion](https://www.hello-algo.com/en/chapter_computational_complexity/iteration_and_recursion/)
|
||||
- [2.3 Time complexity](https://www.hello-algo.com/en/chapter_computational_complexity/time_complexity/)
|
||||
- [2.4 Space complexity](https://www.hello-algo.com/en/chapter_computational_complexity/space_complexity/)
|
||||
- [2.5 Summary](https://www.hello-algo.com/en/chapter_computational_complexity/summary/)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 2.2 Iteration and Recursion
|
||||
# 2.2 Iteration and recursion
|
||||
|
||||
In algorithms, the repeated execution of a task is quite common and is closely related to the analysis of complexity. Therefore, before delving into the concepts of time complexity and space complexity, let's first explore how to implement repetitive tasks in programming. This involves understanding two fundamental programming control structures: iteration and recursion.
|
||||
|
||||
@@ -10,7 +10,7 @@ In algorithms, the repeated execution of a task is quite common and is closely r
|
||||
|
||||
"Iteration" is a control structure for repeatedly performing a task. In iteration, a program repeats a block of code as long as a certain condition is met until this condition is no longer satisfied.
|
||||
|
||||
### 1. For Loops
|
||||
### 1. For loops
|
||||
|
||||
The `for` loop is one of the most common forms of iteration, and **it's particularly suitable when the number of iterations is known in advance**.
|
||||
|
||||
@@ -219,13 +219,13 @@ The following function uses a `for` loop to perform a summation of $1 + 2 + \dot
|
||||
|
||||
The flowchart below represents this sum function.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 2-1 Flowchart of the Sum Function </p>
|
||||
<p align="center"> Figure 2-1 Flowchart of the sum function </p>
|
||||
|
||||
The number of operations in this summation function is proportional to the size of the input data $n$, or in other words, it has a "linear relationship." This "linear relationship" is what time complexity describes. This topic will be discussed in more detail in the next section.
|
||||
|
||||
### 2. While Loops
|
||||
### 2. While loops
|
||||
|
||||
Similar to `for` loops, `while` loops are another approach for implementing iteration. In a `while` loop, the program checks a condition at the beginning of each iteration; if the condition is true, the execution continues, otherwise, the loop ends.
|
||||
|
||||
@@ -728,7 +728,7 @@ For example, in the following code, the condition variable $i$ is updated twice
|
||||
|
||||
Overall, **`for` loops are more concise, while `while` loops are more flexible**. Both can implement iterative structures. Which one to use should be determined based on the specific requirements of the problem.
|
||||
|
||||
### 3. Nested Loops
|
||||
### 3. Nested loops
|
||||
|
||||
We can nest one loop structure within another. Below is an example using `for` loops:
|
||||
|
||||
@@ -983,9 +983,9 @@ We can nest one loop structure within another. Below is an example using `for` l
|
||||
|
||||
The flowchart below represents this nested loop.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 2-2 Flowchart of the Nested Loop </p>
|
||||
<p align="center"> Figure 2-2 Flowchart of the nested loop </p>
|
||||
|
||||
In such cases, the number of operations of the function is proportional to $n^2$, meaning the algorithm's runtime and the size of the input data $n$ has a 'quadratic relationship.'
|
||||
|
||||
@@ -1222,9 +1222,9 @@ Observe the following code, where simply calling the function `recur(n)` can com
|
||||
|
||||
The Figure 2-3 shows the recursive process of this function.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 2-3 Recursive Process of the Sum Function </p>
|
||||
<p align="center"> Figure 2-3 Recursive process of the sum function </p>
|
||||
|
||||
Although iteration and recursion can achieve the same results from a computational standpoint, **they represent two entirely different paradigms of thinking and problem-solving**.
|
||||
|
||||
@@ -1236,7 +1236,7 @@ Let's take the earlier example of the summation function, defined as $f(n) = 1 +
|
||||
- **Iteration**: In this approach, we simulate the summation process within a loop. Starting from $1$ and traversing to $n$, we perform the summation operation in each iteration to eventually compute $f(n)$.
|
||||
- **Recursion**: Here, the problem is broken down into a sub-problem: $f(n) = n + f(n-1)$. This decomposition continues recursively until reaching the base case, $f(1) = 1$, at which point the recursion terminates.
|
||||
|
||||
### 1. Call Stack
|
||||
### 1. Call stack
|
||||
|
||||
Every time a recursive function calls itself, the system allocates memory for the newly initiated function to store local variables, the return address, and other relevant information. This leads to two primary outcomes.
|
||||
|
||||
@@ -1245,18 +1245,18 @@ Every time a recursive function calls itself, the system allocates memory for th
|
||||
|
||||
As shown in the Figure 2-4 , there are $n$ unreturned recursive functions before triggering the termination condition, indicating a **recursion depth of $n$**.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 2-4 Recursion Call Depth </p>
|
||||
<p align="center"> Figure 2-4 Recursion call depth </p>
|
||||
|
||||
In practice, the depth of recursion allowed by programming languages is usually limited, and excessively deep recursion can lead to stack overflow errors.
|
||||
|
||||
### 2. Tail Recursion
|
||||
### 2. Tail recursion
|
||||
|
||||
Interestingly, **if a function performs its recursive call as the very last step before returning,** it can be optimized by the compiler or interpreter to be as space-efficient as iteration. This scenario is known as "tail recursion."
|
||||
|
||||
- **Regular Recursion**: In standard recursion, when the function returns to the previous level, it continues to execute more code, requiring the system to save the context of the previous call.
|
||||
- **Tail Recursion**: Here, the recursive call is the final operation before the function returns. This means that upon returning to the previous level, no further actions are needed, so the system does not need to save the context of the previous level.
|
||||
- **Regular recursion**: In standard recursion, when the function returns to the previous level, it continues to execute more code, requiring the system to save the context of the previous call.
|
||||
- **Tail recursion**: Here, the recursive call is the final operation before the function returns. This means that upon returning to the previous level, no further actions are needed, so the system does not need to save the context of the previous level.
|
||||
|
||||
For example, in calculating $1 + 2 + \dots + n$, we can make the result variable `res` a parameter of the function, thereby achieving tail recursion:
|
||||
|
||||
@@ -1449,18 +1449,18 @@ For example, in calculating $1 + 2 + \dots + n$, we can make the result variable
|
||||
|
||||
The execution process of tail recursion is shown in the following figure. Comparing regular recursion and tail recursion, the point of the summation operation is different.
|
||||
|
||||
- **Regular Recursion**: The summation operation occurs during the "returning" phase, requiring another summation after each layer returns.
|
||||
- **Tail Recursion**: The summation operation occurs during the "calling" phase, and the "returning" phase only involves returning through each layer.
|
||||
- **Regular recursion**: The summation operation occurs during the "returning" phase, requiring another summation after each layer returns.
|
||||
- **Tail recursion**: The summation operation occurs during the "calling" phase, and the "returning" phase only involves returning through each layer.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 2-5 Tail Recursion Process </p>
|
||||
<p align="center"> Figure 2-5 Tail recursion process </p>
|
||||
|
||||
!!! tip
|
||||
|
||||
Note that many compilers or interpreters do not support tail recursion optimization. For example, Python does not support tail recursion optimization by default, so even if the function is in the form of tail recursion, it may still encounter stack overflow issues.
|
||||
|
||||
### 3. Recursion Tree
|
||||
### 3. Recursion tree
|
||||
|
||||
When dealing with algorithms related to "divide and conquer", recursion often offers a more intuitive approach and more readable code than iteration. Take the "Fibonacci sequence" as an example.
|
||||
|
||||
@@ -1691,9 +1691,9 @@ Using the recursive relation, and considering the first two numbers as terminati
|
||||
|
||||
Observing the above code, we see that it recursively calls two functions within itself, **meaning that one call generates two branching calls**. As illustrated below, this continuous recursive calling eventually creates a "recursion tree" with a depth of $n$.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 2-6 Fibonacci Sequence Recursion Tree </p>
|
||||
<p align="center"> Figure 2-6 Fibonacci sequence recursion tree </p>
|
||||
|
||||
Fundamentally, recursion embodies the paradigm of "breaking down a problem into smaller sub-problems." This divide-and-conquer strategy is crucial.
|
||||
|
||||
@@ -1704,7 +1704,7 @@ Fundamentally, recursion embodies the paradigm of "breaking down a problem into
|
||||
|
||||
Summarizing the above content, the following table shows the differences between iteration and recursion in terms of implementation, performance, and applicability.
|
||||
|
||||
<p align="center"> Table: Comparison of Iteration and Recursion Characteristics </p>
|
||||
<p align="center"> Table: Comparison of iteration and recursion characteristics </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 2.1 Algorithm Efficiency Assessment
|
||||
# 2.1 Algorithm efficiency assessment
|
||||
|
||||
In algorithm design, we pursue the following two objectives in sequence.
|
||||
|
||||
@@ -11,14 +11,14 @@ In algorithm design, we pursue the following two objectives in sequence.
|
||||
|
||||
In other words, under the premise of being able to solve the problem, algorithm efficiency has become the main criterion for evaluating the merits of an algorithm, which includes the following two dimensions.
|
||||
|
||||
- **Time Efficiency**: The speed at which an algorithm runs.
|
||||
- **Space Efficiency**: The size of the memory space occupied by an algorithm.
|
||||
- **Time efficiency**: The speed at which an algorithm runs.
|
||||
- **Space efficiency**: The size of the memory space occupied by an algorithm.
|
||||
|
||||
In short, **our goal is to design data structures and algorithms that are both fast and memory-efficient**. Effectively assessing algorithm efficiency is crucial because only then can we compare various algorithms and guide the process of algorithm design and optimization.
|
||||
|
||||
There are mainly two methods of efficiency assessment: actual testing and theoretical estimation.
|
||||
|
||||
## 2.1.1 Actual Testing
|
||||
## 2.1.1 Actual testing
|
||||
|
||||
Suppose we have algorithms `A` and `B`, both capable of solving the same problem, and we need to compare their efficiencies. The most direct method is to use a computer to run these two algorithms and monitor and record their runtime and memory usage. This assessment method reflects the actual situation but has significant limitations.
|
||||
|
||||
@@ -26,7 +26,7 @@ On one hand, **it's difficult to eliminate interference from the testing environ
|
||||
|
||||
On the other hand, **conducting a full test is very resource-intensive**. As the volume of input data changes, the efficiency of the algorithms may vary. For example, with smaller data volumes, algorithm `A` might run faster than `B`, but the opposite might be true with larger data volumes. Therefore, to draw convincing conclusions, we need to test a wide range of input data sizes, which requires significant computational resources.
|
||||
|
||||
## 2.1.2 Theoretical Estimation
|
||||
## 2.1.2 Theoretical estimation
|
||||
|
||||
Due to the significant limitations of actual testing, we can consider evaluating algorithm efficiency solely through calculations. This estimation method is known as "asymptotic complexity analysis," or simply "complexity analysis."
|
||||
|
||||
|
||||
@@ -2,31 +2,31 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 2.4 Space Complexity
|
||||
# 2.4 Space complexity
|
||||
|
||||
"Space complexity" is used to measure the growth trend of the memory space occupied by an algorithm as the amount of data increases. This concept is very similar to time complexity, except that "running time" is replaced with "occupied memory space".
|
||||
|
||||
## 2.4.1 Space Related to Algorithms
|
||||
## 2.4.1 Space related to algorithms
|
||||
|
||||
The memory space used by an algorithm during its execution mainly includes the following types.
|
||||
|
||||
- **Input Space**: Used to store the input data of the algorithm.
|
||||
- **Temporary Space**: Used to store variables, objects, function contexts, and other data during the algorithm's execution.
|
||||
- **Output Space**: Used to store the output data of the algorithm.
|
||||
- **Input space**: Used to store the input data of the algorithm.
|
||||
- **Temporary space**: Used to store variables, objects, function contexts, and other data during the algorithm's execution.
|
||||
- **Output space**: Used to store the output data of the algorithm.
|
||||
|
||||
Generally, the scope of space complexity statistics includes both "Temporary Space" and "Output Space".
|
||||
|
||||
Temporary space can be further divided into three parts.
|
||||
|
||||
- **Temporary Data**: Used to save various constants, variables, objects, etc., during the algorithm's execution.
|
||||
- **Stack Frame Space**: Used to save the context data of the called function. The system creates a stack frame at the top of the stack each time a function is called, and the stack frame space is released after the function returns.
|
||||
- **Instruction Space**: Used to store compiled program instructions, which are usually negligible in actual statistics.
|
||||
- **Temporary data**: Used to save various constants, variables, objects, etc., during the algorithm's execution.
|
||||
- **Stack frame space**: Used to save the context data of the called function. The system creates a stack frame at the top of the stack each time a function is called, and the stack frame space is released after the function returns.
|
||||
- **Instruction space**: Used to store compiled program instructions, which are usually negligible in actual statistics.
|
||||
|
||||
When analyzing the space complexity of a program, **we typically count the Temporary Data, Stack Frame Space, and Output Data**, as shown in the Figure 2-15 .
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 2-15 Space Types Used in Algorithms </p>
|
||||
<p align="center"> Figure 2-15 Space types used in algorithms </p>
|
||||
|
||||
The relevant code is as follows:
|
||||
|
||||
@@ -34,13 +34,13 @@ The relevant code is as follows:
|
||||
|
||||
```python title=""
|
||||
class Node:
|
||||
"""Classes""""
|
||||
"""Classes"""
|
||||
def __init__(self, x: int):
|
||||
self.val: int = x # node value
|
||||
self.next: Node | None = None # reference to the next node
|
||||
|
||||
def function() -> int:
|
||||
""""Functions"""""
|
||||
"""Functions"""
|
||||
# Perform certain operations...
|
||||
return 0
|
||||
|
||||
@@ -277,7 +277,7 @@ The relevant code is as follows:
|
||||
next: Option<Rc<RefCell<Node>>>,
|
||||
}
|
||||
|
||||
/* Creating a Node structure */
|
||||
/* Constructor */
|
||||
impl Node {
|
||||
fn new(val: i32) -> Self {
|
||||
Self { val: val, next: None }
|
||||
@@ -328,7 +328,7 @@ The relevant code is as follows:
|
||||
|
||||
```
|
||||
|
||||
## 2.4.2 Calculation Method
|
||||
## 2.4.2 Calculation method
|
||||
|
||||
The method for calculating space complexity is roughly similar to that of time complexity, with the only change being the shift of the statistical object from "number of operations" to "size of used space".
|
||||
|
||||
@@ -449,10 +449,10 @@ Consider the following code, the term "worst-case" in worst-case space complexit
|
||||
|
||||
```rust title=""
|
||||
fn algorithm(n: i32) {
|
||||
let a = 0; // O(1)
|
||||
let b = [0; 10000]; // O(1)
|
||||
let a = 0; // O(1)
|
||||
let b = [0; 10000]; // O(1)
|
||||
if n > 10 {
|
||||
let nums = vec![0; n as usize]; // O(n)
|
||||
let nums = vec![0; n as usize]; // O(n)
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -490,12 +490,12 @@ Consider the following code, the term "worst-case" in worst-case space complexit
|
||||
return 0
|
||||
|
||||
def loop(n: int):
|
||||
"""Loop O(1)"""""
|
||||
"""Loop O(1)"""
|
||||
for _ in range(n):
|
||||
function()
|
||||
|
||||
def recur(n: int):
|
||||
"""Recursion O(n)"""""
|
||||
"""Recursion O(n)"""
|
||||
if n == 1:
|
||||
return
|
||||
return recur(n - 1)
|
||||
@@ -729,7 +729,7 @@ The time complexity of both `loop()` and `recur()` functions is $O(n)$, but thei
|
||||
- The `loop()` function calls `function()` $n$ times in a loop, where each iteration's `function()` returns and releases its stack frame space, so the space complexity remains $O(1)$.
|
||||
- The recursive function `recur()` will have $n$ instances of unreturned `recur()` existing simultaneously during its execution, thus occupying $O(n)$ stack frame space.
|
||||
|
||||
## 2.4.3 Common Types
|
||||
## 2.4.3 Common types
|
||||
|
||||
Let the size of the input data be $n$, the following chart displays common types of space complexities (arranged from low to high).
|
||||
|
||||
@@ -740,11 +740,11 @@ O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 2-16 Common Types of Space Complexity </p>
|
||||
<p align="center"> Figure 2-16 Common types of space complexity </p>
|
||||
|
||||
### 1. Constant Order $O(1)$ {data-toc-label="1. Constant Order"}
|
||||
### 1. Constant order $O(1)$ {data-toc-label="1. Constant order"}
|
||||
|
||||
Constant order is common in constants, variables, objects that are independent of the size of input data $n$.
|
||||
|
||||
@@ -1139,7 +1139,7 @@ Note that memory occupied by initializing variables or calling functions in a lo
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0Adef%20function%28%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%87%BD%E6%95%B0%22%22%22%0A%20%20%20%20%23%20%E6%89%A7%E8%A1%8C%E6%9F%90%E4%BA%9B%E6%93%8D%E4%BD%9C%0A%20%20%20%20return%200%0A%0Adef%20constant%28n%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E5%B8%B8%E6%95%B0%E9%98%B6%22%22%22%0A%20%20%20%20%23%20%E5%B8%B8%E9%87%8F%E3%80%81%E5%8F%98%E9%87%8F%E3%80%81%E5%AF%B9%E8%B1%A1%E5%8D%A0%E7%94%A8%20O%281%29%20%E7%A9%BA%E9%97%B4%0A%20%20%20%20a%20%3D%200%0A%20%20%20%20nums%20%3D%20%5B0%5D%20*%2010%0A%20%20%20%20node%20%3D%20ListNode%280%29%0A%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%E4%B8%AD%E7%9A%84%E5%8F%98%E9%87%8F%E5%8D%A0%E7%94%A8%20O%281%29%20%E7%A9%BA%E9%97%B4%0A%20%20%20%20for%20_%20in%20range%28n%29%3A%0A%20%20%20%20%20%20%20%20c%20%3D%200%0A%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%E4%B8%AD%E7%9A%84%E5%87%BD%E6%95%B0%E5%8D%A0%E7%94%A8%20O%281%29%20%E7%A9%BA%E9%97%B4%0A%20%20%20%20for%20_%20in%20range%28n%29%3A%0A%20%20%20%20%20%20%20%20function%28%29%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%205%0A%20%20%20%20print%28%22%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E5%A4%A7%E5%B0%8F%20n%20%3D%22,%20n%29%0A%0A%20%20%20%20%23%20%E5%B8%B8%E6%95%B0%E9%98%B6%0A%20%20%20%20constant%28n%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0Adef%20function%28%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%87%BD%E6%95%B0%22%22%22%0A%20%20%20%20%23%20%E6%89%A7%E8%A1%8C%E6%9F%90%E4%BA%9B%E6%93%8D%E4%BD%9C%0A%20%20%20%20return%200%0A%0Adef%20constant%28n%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E5%B8%B8%E6%95%B0%E9%98%B6%22%22%22%0A%20%20%20%20%23%20%E5%B8%B8%E9%87%8F%E3%80%81%E5%8F%98%E9%87%8F%E3%80%81%E5%AF%B9%E8%B1%A1%E5%8D%A0%E7%94%A8%20O%281%29%20%E7%A9%BA%E9%97%B4%0A%20%20%20%20a%20%3D%200%0A%20%20%20%20nums%20%3D%20%5B0%5D%20*%2010%0A%20%20%20%20node%20%3D%20ListNode%280%29%0A%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%E4%B8%AD%E7%9A%84%E5%8F%98%E9%87%8F%E5%8D%A0%E7%94%A8%20O%281%29%20%E7%A9%BA%E9%97%B4%0A%20%20%20%20for%20_%20in%20range%28n%29%3A%0A%20%20%20%20%20%20%20%20c%20%3D%200%0A%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%E4%B8%AD%E7%9A%84%E5%87%BD%E6%95%B0%E5%8D%A0%E7%94%A8%20O%281%29%20%E7%A9%BA%E9%97%B4%0A%20%20%20%20for%20_%20in%20range%28n%29%3A%0A%20%20%20%20%20%20%20%20function%28%29%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%205%0A%20%20%20%20print%28%22%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E5%A4%A7%E5%B0%8F%20n%20%3D%22,%20n%29%0A%0A%20%20%20%20%23%20%E5%B8%B8%E6%95%B0%E9%98%B6%0A%20%20%20%20constant%28n%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Full Screen ></a></div>
|
||||
|
||||
### 2. Linear Order $O(n)$ {data-toc-label="2. Linear Order"}
|
||||
### 2. Linear order $O(n)$ {data-toc-label="2. Linear order"}
|
||||
|
||||
Linear order is common in arrays, linked lists, stacks, queues, etc., where the number of elements is proportional to $n$:
|
||||
|
||||
@@ -1615,11 +1615,11 @@ As shown below, this function's recursive depth is $n$, meaning there are $n$ in
|
||||
<div style="height: 441px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20linear_recur%28n%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E7%BA%BF%E6%80%A7%E9%98%B6%EF%BC%88%E9%80%92%E5%BD%92%E5%AE%9E%E7%8E%B0%EF%BC%89%22%22%22%0A%20%20%20%20print%28%22%E9%80%92%E5%BD%92%20n%20%3D%22,%20n%29%0A%20%20%20%20if%20n%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20return%0A%20%20%20%20linear_recur%28n%20-%201%29%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%205%0A%20%20%20%20print%28%22%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E5%A4%A7%E5%B0%8F%20n%20%3D%22,%20n%29%0A%0A%20%20%20%20%23%20%E7%BA%BF%E6%80%A7%E9%98%B6%0A%20%20%20%20linear_recur%28n%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=25&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20linear_recur%28n%3A%20int%29%3A%0A%20%20%20%20%22%22%22%E7%BA%BF%E6%80%A7%E9%98%B6%EF%BC%88%E9%80%92%E5%BD%92%E5%AE%9E%E7%8E%B0%EF%BC%89%22%22%22%0A%20%20%20%20print%28%22%E9%80%92%E5%BD%92%20n%20%3D%22,%20n%29%0A%20%20%20%20if%20n%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20return%0A%20%20%20%20linear_recur%28n%20-%201%29%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%205%0A%20%20%20%20print%28%22%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E5%A4%A7%E5%B0%8F%20n%20%3D%22,%20n%29%0A%0A%20%20%20%20%23%20%E7%BA%BF%E6%80%A7%E9%98%B6%0A%20%20%20%20linear_recur%28n%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=25&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Full Screen ></a></div>
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 2-17 Recursive Function Generating Linear Order Space Complexity </p>
|
||||
<p align="center"> Figure 2-17 Recursive function generating linear order space complexity </p>
|
||||
|
||||
### 3. Quadratic Order $O(n^2)$ {data-toc-label="3. Quadratic Order"}
|
||||
### 3. Quadratic order $O(n^2)$ {data-toc-label="3. Quadratic order"}
|
||||
|
||||
Quadratic order is common in matrices and graphs, where the number of elements is quadratic to $n$:
|
||||
|
||||
@@ -2062,11 +2062,11 @@ As shown below, the recursive depth of this function is $n$, and in each recursi
|
||||
<div style="height: 459px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20quadratic_recur%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%B9%B3%E6%96%B9%E9%98%B6%EF%BC%88%E9%80%92%E5%BD%92%E5%AE%9E%E7%8E%B0%EF%BC%89%22%22%22%0A%20%20%20%20if%20n%20%3C%3D%200%3A%0A%20%20%20%20%20%20%20%20return%200%0A%20%20%20%20%23%20%E6%95%B0%E7%BB%84%20nums%20%E9%95%BF%E5%BA%A6%E4%B8%BA%20n,%20n-1,%20...,%202,%201%0A%20%20%20%20nums%20%3D%20%5B0%5D%20*%20n%0A%20%20%20%20return%20quadratic_recur%28n%20-%201%29%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%205%0A%20%20%20%20print%28%22%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E5%A4%A7%E5%B0%8F%20n%20%3D%22,%20n%29%0A%0A%20%20%20%20%23%20%E5%B9%B3%E6%96%B9%E9%98%B6%0A%20%20%20%20quadratic_recur%28n%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=28&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20quadratic_recur%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%B9%B3%E6%96%B9%E9%98%B6%EF%BC%88%E9%80%92%E5%BD%92%E5%AE%9E%E7%8E%B0%EF%BC%89%22%22%22%0A%20%20%20%20if%20n%20%3C%3D%200%3A%0A%20%20%20%20%20%20%20%20return%200%0A%20%20%20%20%23%20%E6%95%B0%E7%BB%84%20nums%20%E9%95%BF%E5%BA%A6%E4%B8%BA%20n,%20n-1,%20...,%202,%201%0A%20%20%20%20nums%20%3D%20%5B0%5D%20*%20n%0A%20%20%20%20return%20quadratic_recur%28n%20-%201%29%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%205%0A%20%20%20%20print%28%22%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E5%A4%A7%E5%B0%8F%20n%20%3D%22,%20n%29%0A%0A%20%20%20%20%23%20%E5%B9%B3%E6%96%B9%E9%98%B6%0A%20%20%20%20quadratic_recur%28n%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=28&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Full Screen ></a></div>
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 2-18 Recursive Function Generating Quadratic Order Space Complexity </p>
|
||||
<p align="center"> Figure 2-18 Recursive function generating quadratic order space complexity </p>
|
||||
|
||||
### 4. Exponential Order $O(2^n)$ {data-toc-label="4. Exponential Order"}
|
||||
### 4. Exponential order $O(2^n)$ {data-toc-label="4. Exponential order"}
|
||||
|
||||
Exponential order is common in binary trees. Observe the below image, a "full binary tree" with $n$ levels has $2^n - 1$ nodes, occupying $O(2^n)$ space:
|
||||
|
||||
@@ -2270,17 +2270,17 @@ Exponential order is common in binary trees. Observe the below image, a "full bi
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%20%3D%200%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0Adef%20build_tree%28n%3A%20int%29%20-%3E%20TreeNode%20%7C%20None%3A%0A%20%20%20%20%22%22%22%E6%8C%87%E6%95%B0%E9%98%B6%EF%BC%88%E5%BB%BA%E7%AB%8B%E6%BB%A1%E4%BA%8C%E5%8F%89%E6%A0%91%EF%BC%89%22%22%22%0A%20%20%20%20if%20n%20%3D%3D%200%3A%0A%20%20%20%20%20%20%20%20return%20None%0A%20%20%20%20root%20%3D%20TreeNode%280%29%0A%20%20%20%20root.left%20%3D%20build_tree%28n%20-%201%29%0A%20%20%20%20root.right%20%3D%20build_tree%28n%20-%201%29%0A%20%20%20%20return%20root%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%205%0A%20%20%20%20print%28%22%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E5%A4%A7%E5%B0%8F%20n%20%3D%22,%20n%29%0A%0A%20%20%20%20%23%20%E6%8C%87%E6%95%B0%E9%98%B6%0A%20%20%20%20root%20%3D%20build_tree%28n%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=507&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%20%3D%200%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0Adef%20build_tree%28n%3A%20int%29%20-%3E%20TreeNode%20%7C%20None%3A%0A%20%20%20%20%22%22%22%E6%8C%87%E6%95%B0%E9%98%B6%EF%BC%88%E5%BB%BA%E7%AB%8B%E6%BB%A1%E4%BA%8C%E5%8F%89%E6%A0%91%EF%BC%89%22%22%22%0A%20%20%20%20if%20n%20%3D%3D%200%3A%0A%20%20%20%20%20%20%20%20return%20None%0A%20%20%20%20root%20%3D%20TreeNode%280%29%0A%20%20%20%20root.left%20%3D%20build_tree%28n%20-%201%29%0A%20%20%20%20root.right%20%3D%20build_tree%28n%20-%201%29%0A%20%20%20%20return%20root%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%205%0A%20%20%20%20print%28%22%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E5%A4%A7%E5%B0%8F%20n%20%3D%22,%20n%29%0A%0A%20%20%20%20%23%20%E6%8C%87%E6%95%B0%E9%98%B6%0A%20%20%20%20root%20%3D%20build_tree%28n%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=507&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Full Screen ></a></div>
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 2-19 Full Binary Tree Generating Exponential Order Space Complexity </p>
|
||||
<p align="center"> Figure 2-19 Full binary tree generating exponential order space complexity </p>
|
||||
|
||||
### 5. Logarithmic Order $O(\log n)$ {data-toc-label="5. Logarithmic Order"}
|
||||
### 5. Logarithmic order $O(\log n)$ {data-toc-label="5. Logarithmic order"}
|
||||
|
||||
Logarithmic order is common in divide-and-conquer algorithms. For example, in merge sort, an array of length $n$ is recursively divided in half each round, forming a recursion tree of height $\log n$, using $O(\log n)$ stack frame space.
|
||||
|
||||
Another example is converting a number to a string. Given a positive integer $n$, its number of digits is $\log_{10} n + 1$, corresponding to the length of the string, thus the space complexity is $O(\log_{10} n + 1) = O(\log n)$.
|
||||
|
||||
## 2.4.4 Balancing Time and Space
|
||||
## 2.4.4 Balancing time and space
|
||||
|
||||
Ideally, we aim for both time complexity and space complexity to be optimal. However, in practice, optimizing both simultaneously is often difficult.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ comments: true
|
||||
|
||||
# 2.5 Summary
|
||||
|
||||
### 1. Key Review
|
||||
### 1. Key review
|
||||
|
||||
**Algorithm Efficiency Assessment**
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 2.3 Time Complexity
|
||||
# 2.3 Time complexity
|
||||
|
||||
Time complexity is a concept used to measure how the run time of an algorithm increases with the size of the input data. Understanding time complexity is crucial for accurately assessing the efficiency of an algorithm.
|
||||
|
||||
@@ -204,7 +204,7 @@ $$
|
||||
|
||||
However, in practice, **counting the run time of an algorithm is neither practical nor reasonable**. First, we don't want to tie the estimated time to the running platform, as algorithms need to run on various platforms. Second, it's challenging to know the run time for each type of operation, making the estimation process difficult.
|
||||
|
||||
## 2.3.1 Assessing Time Growth Trend
|
||||
## 2.3.1 Assessing time growth trend
|
||||
|
||||
Time complexity analysis does not count the algorithm's run time, **but rather the growth trend of the run time as the data volume increases**.
|
||||
|
||||
@@ -474,9 +474,9 @@ The following figure shows the time complexities of these three algorithms.
|
||||
- Algorithm `B` involves a print operation looping $n$ times, and its run time grows linearly with $n$. Its time complexity is "linear order."
|
||||
- Algorithm `C` has a print operation looping 1,000,000 times. Although it takes a long time, it is independent of the input data size $n$. Therefore, the time complexity of `C` is the same as `A`, which is "constant order."
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 2-7 Time Growth Trend of Algorithms A, B, and C </p>
|
||||
<p align="center"> Figure 2-7 Time growth trend of algorithms a, b, and c </p>
|
||||
|
||||
Compared to directly counting the run time of an algorithm, what are the characteristics of time complexity analysis?
|
||||
|
||||
@@ -484,7 +484,7 @@ Compared to directly counting the run time of an algorithm, what are the charact
|
||||
- **Time complexity analysis is more straightforward**. Obviously, the running platform and the types of computational operations are irrelevant to the trend of run time growth. Therefore, in time complexity analysis, we can simply treat the execution time of all computational operations as the same "unit time," simplifying the "computational operation run time count" to a "computational operation count." This significantly reduces the complexity of estimation.
|
||||
- **Time complexity has its limitations**. For example, although algorithms `A` and `C` have the same time complexity, their actual run times can be quite different. Similarly, even though algorithm `B` has a higher time complexity than `C`, it is clearly superior when the input data size $n$ is small. In these cases, it's difficult to judge the efficiency of algorithms based solely on time complexity. Nonetheless, despite these issues, complexity analysis remains the most effective and commonly used method for evaluating algorithm efficiency.
|
||||
|
||||
## 2.3.2 Asymptotic Upper Bound
|
||||
## 2.3.2 Asymptotic upper bound
|
||||
|
||||
Consider a function with an input size of $n$:
|
||||
|
||||
@@ -677,17 +677,17 @@ In essence, time complexity analysis is about finding the asymptotic upper bound
|
||||
|
||||
As illustrated below, calculating the asymptotic upper bound involves finding a function $f(n)$ such that, as $n$ approaches infinity, $T(n)$ and $f(n)$ have the same growth order, differing only by a constant factor $c$.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 2-8 Asymptotic Upper Bound of a Function </p>
|
||||
<p align="center"> Figure 2-8 Asymptotic upper bound of a function </p>
|
||||
|
||||
## 2.3.3 Calculation Method
|
||||
## 2.3.3 Calculation method
|
||||
|
||||
While the concept of asymptotic upper bound might seem mathematically dense, you don't need to fully grasp it right away. Let's first understand the method of calculation, which can be practiced and comprehended over time.
|
||||
|
||||
Once $f(n)$ is determined, we obtain the time complexity $O(f(n))$. But how do we determine the asymptotic upper bound $f(n)$? This process generally involves two steps: counting the number of operations and determining the asymptotic upper bound.
|
||||
|
||||
### 1. Step 1: Counting the Number of Operations
|
||||
### 1. Step 1: counting the number of operations
|
||||
|
||||
This step involves going through the code line by line. However, due to the presence of the constant $c$ in $c \cdot f(n)$, **all coefficients and constant terms in $T(n)$ can be ignored**. This principle allows for simplification techniques in counting operations.
|
||||
|
||||
@@ -941,13 +941,13 @@ T(n) & = n^2 + n & \text{Simplified Count (o.O)}
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
### 2. Step 2: Determining the Asymptotic Upper Bound
|
||||
### 2. Step 2: determining the asymptotic upper bound
|
||||
|
||||
**The time complexity is determined by the highest order term in $T(n)$**. This is because, as $n$ approaches infinity, the highest order term dominates, rendering the influence of other terms negligible.
|
||||
|
||||
The following table illustrates examples of different operation counts and their corresponding time complexities. Some exaggerated values are used to emphasize that coefficients cannot alter the order of growth. When $n$ becomes very large, these constants become insignificant.
|
||||
|
||||
<p align="center"> Table: Time Complexity for Different Operation Counts </p>
|
||||
<p align="center"> Table: Time complexity for different operation counts </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
@@ -961,7 +961,7 @@ The following table illustrates examples of different operation counts and their
|
||||
|
||||
</div>
|
||||
|
||||
## 2.3.4 Common Types of Time Complexity
|
||||
## 2.3.4 Common types of time complexity
|
||||
|
||||
Let's consider the input data size as $n$. The common types of time complexities are illustrated below, arranged from lowest to highest:
|
||||
|
||||
@@ -972,11 +972,11 @@ O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!) \newline
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 2-9 Common Types of Time Complexity </p>
|
||||
<p align="center"> Figure 2-9 Common types of time complexity </p>
|
||||
|
||||
### 1. Constant Order $O(1)$ {data-toc-label="1. Constant Order"}
|
||||
### 1. Constant order $O(1)$ {data-toc-label="1. Constant order"}
|
||||
|
||||
Constant order means the number of operations is independent of the input data size $n$. In the following function, although the number of operations `size` might be large, the time complexity remains $O(1)$ as it's unrelated to $n$:
|
||||
|
||||
@@ -1175,7 +1175,7 @@ Constant order means the number of operations is independent of the input data s
|
||||
<div style="height: 459px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20constant%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%B8%B8%E6%95%B0%E9%98%B6%22%22%22%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20size%20%3D%2010%0A%20%20%20%20for%20_%20in%20range%28size%29%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%201%0A%20%20%20%20return%20count%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%208%0A%20%20%20%20print%28%22%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E5%A4%A7%E5%B0%8F%20n%20%3D%22,%20n%29%0A%0A%20%20%20%20count%20%3D%20constant%28n%29%0A%20%20%20%20print%28%22%E5%B8%B8%E6%95%B0%E9%98%B6%E7%9A%84%E6%93%8D%E4%BD%9C%E6%95%B0%E9%87%8F%20%3D%22,%20count%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20constant%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%B8%B8%E6%95%B0%E9%98%B6%22%22%22%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20size%20%3D%2010%0A%20%20%20%20for%20_%20in%20range%28size%29%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%201%0A%20%20%20%20return%20count%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%208%0A%20%20%20%20print%28%22%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E5%A4%A7%E5%B0%8F%20n%20%3D%22,%20n%29%0A%0A%20%20%20%20count%20%3D%20constant%28n%29%0A%20%20%20%20print%28%22%E5%B8%B8%E6%95%B0%E9%98%B6%E7%9A%84%E6%93%8D%E4%BD%9C%E6%95%B0%E9%87%8F%20%3D%22,%20count%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Full Screen ></a></div>
|
||||
|
||||
### 2. Linear Order $O(n)$ {data-toc-label="2. Linear Order"}
|
||||
### 2. Linear order $O(n)$ {data-toc-label="2. Linear order"}
|
||||
|
||||
Linear order indicates the number of operations grows linearly with the input data size $n$. Linear order commonly appears in single-loop structures:
|
||||
|
||||
@@ -1561,7 +1561,7 @@ Operations like array traversal and linked list traversal have a time complexity
|
||||
|
||||
It's important to note that **the input data size $n$ should be determined based on the type of input data**. For example, in the first example, $n$ represents the input data size, while in the second example, the length of the array $n$ is the data size.
|
||||
|
||||
### 3. Quadratic Order $O(n^2)$ {data-toc-label="3. Quadratic Order"}
|
||||
### 3. Quadratic order $O(n^2)$ {data-toc-label="3. Quadratic order"}
|
||||
|
||||
Quadratic order means the number of operations grows quadratically with the input data size $n$. Quadratic order typically appears in nested loops, where both the outer and inner loops have a time complexity of $O(n)$, resulting in an overall complexity of $O(n^2)$:
|
||||
|
||||
@@ -1797,9 +1797,9 @@ Quadratic order means the number of operations grows quadratically with the inpu
|
||||
|
||||
The following image compares constant order, linear order, and quadratic order time complexities.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 2-10 Constant, Linear, and Quadratic Order Time Complexities </p>
|
||||
<p align="center"> Figure 2-10 Constant, linear, and quadratic order time complexities </p>
|
||||
|
||||
For instance, in bubble sort, the outer loop runs $n - 1$ times, and the inner loop runs $n-1$, $n-2$, ..., $2$, $1$ times, averaging $n / 2$ times, resulting in a time complexity of $O((n - 1) n / 2) = O(n^2)$:
|
||||
|
||||
@@ -2127,7 +2127,7 @@ For instance, in bubble sort, the outer loop runs $n - 1$ times, and the inner l
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20bubble_sort%28nums%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%B9%B3%E6%96%B9%E9%98%B6%EF%BC%88%E5%86%92%E6%B3%A1%E6%8E%92%E5%BA%8F%EF%BC%89%22%22%22%0A%20%20%20%20count%20%3D%200%20%20%23%20%E8%AE%A1%E6%95%B0%E5%99%A8%0A%20%20%20%20%23%20%E5%A4%96%E5%BE%AA%E7%8E%AF%EF%BC%9A%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8C%BA%E9%97%B4%E4%B8%BA%20%5B0,%20i%5D%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%20-%201,%200,%20-1%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%86%85%E5%BE%AA%E7%8E%AF%EF%BC%9A%E5%B0%86%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8C%BA%E9%97%B4%20%5B0,%20i%5D%20%E4%B8%AD%E7%9A%84%E6%9C%80%E5%A4%A7%E5%85%83%E7%B4%A0%E4%BA%A4%E6%8D%A2%E8%87%B3%E8%AF%A5%E5%8C%BA%E9%97%B4%E7%9A%84%E6%9C%80%E5%8F%B3%E7%AB%AF%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%28i%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20nums%5Bj%5D%20%3E%20nums%5Bj%20%2B%201%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E4%BA%A4%E6%8D%A2%20nums%5Bj%5D%20%E4%B8%8E%20nums%5Bj%20%2B%201%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20tmp%20%3D%20nums%5Bj%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bj%5D%20%3D%20nums%5Bj%20%2B%201%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bj%20%2B%201%5D%20%3D%20tmp%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20count%20%2B%3D%203%20%20%23%20%E5%85%83%E7%B4%A0%E4%BA%A4%E6%8D%A2%E5%8C%85%E5%90%AB%203%20%E4%B8%AA%E5%8D%95%E5%85%83%E6%93%8D%E4%BD%9C%0A%20%20%20%20return%20count%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%208%0A%20%20%20%20print%28%22%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E5%A4%A7%E5%B0%8F%20n%20%3D%22,%20n%29%0A%0A%20%20%20%20nums%20%3D%20%5Bi%20for%20i%20in%20range%28n,%200,%20-1%29%5D%20%20%23%20%5Bn,%20n-1,%20...,%202,%201%5D%0A%20%20%20%20count%20%3D%20bubble_sort%28nums%29%0A%20%20%20%20print%28%22%E5%B9%B3%E6%96%B9%E9%98%B6%EF%BC%88%E5%86%92%E6%B3%A1%E6%8E%92%E5%BA%8F%EF%BC%89%E7%9A%84%E6%93%8D%E4%BD%9C%E6%95%B0%E9%87%8F%20%3D%22,%20count%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20bubble_sort%28nums%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%B9%B3%E6%96%B9%E9%98%B6%EF%BC%88%E5%86%92%E6%B3%A1%E6%8E%92%E5%BA%8F%EF%BC%89%22%22%22%0A%20%20%20%20count%20%3D%200%20%20%23%20%E8%AE%A1%E6%95%B0%E5%99%A8%0A%20%20%20%20%23%20%E5%A4%96%E5%BE%AA%E7%8E%AF%EF%BC%9A%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8C%BA%E9%97%B4%E4%B8%BA%20%5B0,%20i%5D%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%20-%201,%200,%20-1%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%86%85%E5%BE%AA%E7%8E%AF%EF%BC%9A%E5%B0%86%E6%9C%AA%E6%8E%92%E5%BA%8F%E5%8C%BA%E9%97%B4%20%5B0,%20i%5D%20%E4%B8%AD%E7%9A%84%E6%9C%80%E5%A4%A7%E5%85%83%E7%B4%A0%E4%BA%A4%E6%8D%A2%E8%87%B3%E8%AF%A5%E5%8C%BA%E9%97%B4%E7%9A%84%E6%9C%80%E5%8F%B3%E7%AB%AF%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%28i%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20nums%5Bj%5D%20%3E%20nums%5Bj%20%2B%201%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E4%BA%A4%E6%8D%A2%20nums%5Bj%5D%20%E4%B8%8E%20nums%5Bj%20%2B%201%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20tmp%20%3D%20nums%5Bj%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bj%5D%20%3D%20nums%5Bj%20%2B%201%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bj%20%2B%201%5D%20%3D%20tmp%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20count%20%2B%3D%203%20%20%23%20%E5%85%83%E7%B4%A0%E4%BA%A4%E6%8D%A2%E5%8C%85%E5%90%AB%203%20%E4%B8%AA%E5%8D%95%E5%85%83%E6%93%8D%E4%BD%9C%0A%20%20%20%20return%20count%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%208%0A%20%20%20%20print%28%22%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E5%A4%A7%E5%B0%8F%20n%20%3D%22,%20n%29%0A%0A%20%20%20%20nums%20%3D%20%5Bi%20for%20i%20in%20range%28n,%200,%20-1%29%5D%20%20%23%20%5Bn,%20n-1,%20...,%202,%201%5D%0A%20%20%20%20count%20%3D%20bubble_sort%28nums%29%0A%20%20%20%20print%28%22%E5%B9%B3%E6%96%B9%E9%98%B6%EF%BC%88%E5%86%92%E6%B3%A1%E6%8E%92%E5%BA%8F%EF%BC%89%E7%9A%84%E6%93%8D%E4%BD%9C%E6%95%B0%E9%87%8F%20%3D%22,%20count%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Full Screen ></a></div>
|
||||
|
||||
### 4. Exponential Order $O(2^n)$ {data-toc-label="4. Exponential Order"}
|
||||
### 4. Exponential order $O(2^n)$ {data-toc-label="4. Exponential order"}
|
||||
|
||||
Biological "cell division" is a classic example of exponential order growth: starting with one cell, it becomes two after one division, four after two divisions, and so on, resulting in $2^n$ cells after $n$ divisions.
|
||||
|
||||
@@ -2397,9 +2397,9 @@ The following image and code simulate the cell division process, with a time com
|
||||
<div style="height: 531px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20exponential%28n%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%8C%87%E6%95%B0%E9%98%B6%EF%BC%88%E5%BE%AA%E7%8E%AF%E5%AE%9E%E7%8E%B0%EF%BC%89%22%22%22%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20base%20%3D%201%0A%20%20%20%20%23%20%E7%BB%86%E8%83%9E%E6%AF%8F%E8%BD%AE%E4%B8%80%E5%88%86%E4%B8%BA%E4%BA%8C%EF%BC%8C%E5%BD%A2%E6%88%90%E6%95%B0%E5%88%97%201,%202,%204,%208,%20...,%202%5E%28n-1%29%0A%20%20%20%20for%20_%20in%20range%28n%29%3A%0A%20%20%20%20%20%20%20%20for%20_%20in%20range%28base%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20count%20%2B%3D%201%0A%20%20%20%20%20%20%20%20base%20*%3D%202%0A%20%20%20%20%23%20count%20%3D%201%20%2B%202%20%2B%204%20%2B%208%20%2B%20..%20%2B%202%5E%28n-1%29%20%3D%202%5En%20-%201%0A%20%20%20%20return%20count%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%208%0A%20%20%20%20print%28%22%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E5%A4%A7%E5%B0%8F%20n%20%3D%22,%20n%29%0A%0A%20%20%20%20count%20%3D%20exponential%28n%29%0A%20%20%20%20print%28%22%E6%8C%87%E6%95%B0%E9%98%B6%EF%BC%88%E5%BE%AA%E7%8E%AF%E5%AE%9E%E7%8E%B0%EF%BC%89%E7%9A%84%E6%93%8D%E4%BD%9C%E6%95%B0%E9%87%8F%20%3D%22,%20count%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20exponential%28n%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%8C%87%E6%95%B0%E9%98%B6%EF%BC%88%E5%BE%AA%E7%8E%AF%E5%AE%9E%E7%8E%B0%EF%BC%89%22%22%22%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20base%20%3D%201%0A%20%20%20%20%23%20%E7%BB%86%E8%83%9E%E6%AF%8F%E8%BD%AE%E4%B8%80%E5%88%86%E4%B8%BA%E4%BA%8C%EF%BC%8C%E5%BD%A2%E6%88%90%E6%95%B0%E5%88%97%201,%202,%204,%208,%20...,%202%5E%28n-1%29%0A%20%20%20%20for%20_%20in%20range%28n%29%3A%0A%20%20%20%20%20%20%20%20for%20_%20in%20range%28base%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20count%20%2B%3D%201%0A%20%20%20%20%20%20%20%20base%20*%3D%202%0A%20%20%20%20%23%20count%20%3D%201%20%2B%202%20%2B%204%20%2B%208%20%2B%20..%20%2B%202%5E%28n-1%29%20%3D%202%5En%20-%201%0A%20%20%20%20return%20count%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%208%0A%20%20%20%20print%28%22%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E5%A4%A7%E5%B0%8F%20n%20%3D%22,%20n%29%0A%0A%20%20%20%20count%20%3D%20exponential%28n%29%0A%20%20%20%20print%28%22%E6%8C%87%E6%95%B0%E9%98%B6%EF%BC%88%E5%BE%AA%E7%8E%AF%E5%AE%9E%E7%8E%B0%EF%BC%89%E7%9A%84%E6%93%8D%E4%BD%9C%E6%95%B0%E9%87%8F%20%3D%22,%20count%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Full Screen ></a></div>
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 2-11 Exponential Order Time Complexity </p>
|
||||
<p align="center"> Figure 2-11 Exponential order time complexity </p>
|
||||
|
||||
In practice, exponential order often appears in recursive functions. For example, in the code below, it recursively splits into two halves, stopping after $n$ divisions:
|
||||
|
||||
@@ -2561,7 +2561,7 @@ In practice, exponential order often appears in recursive functions. For example
|
||||
|
||||
Exponential order growth is extremely rapid and is commonly seen in exhaustive search methods (brute force, backtracking, etc.). For large-scale problems, exponential order is unacceptable, often requiring dynamic programming or greedy algorithms as solutions.
|
||||
|
||||
### 5. Logarithmic Order $O(\log n)$ {data-toc-label="5. Logarithmic Order"}
|
||||
### 5. Logarithmic order $O(\log n)$ {data-toc-label="5. Logarithmic order"}
|
||||
|
||||
In contrast to exponential order, logarithmic order reflects situations where "the size is halved each round." Given an input data size $n$, since the size is halved each round, the number of iterations is $\log_2 n$, the inverse function of $2^n$.
|
||||
|
||||
@@ -2772,9 +2772,9 @@ The following image and code simulate the "halving each round" process, with a t
|
||||
<div style="height: 459px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20logarithmic%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%AF%B9%E6%95%B0%E9%98%B6%EF%BC%88%E5%BE%AA%E7%8E%AF%E5%AE%9E%E7%8E%B0%EF%BC%89%22%22%22%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20while%20n%20%3E%201%3A%0A%20%20%20%20%20%20%20%20n%20%3D%20n%20/%202%0A%20%20%20%20%20%20%20%20count%20%2B%3D%201%0A%20%20%20%20return%20count%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%208%0A%20%20%20%20print%28%22%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E5%A4%A7%E5%B0%8F%20n%20%3D%22,%20n%29%0A%0A%20%20%20%20count%20%3D%20logarithmic%28n%29%0A%20%20%20%20print%28%22%E5%AF%B9%E6%95%B0%E9%98%B6%EF%BC%88%E5%BE%AA%E7%8E%AF%E5%AE%9E%E7%8E%B0%EF%BC%89%E7%9A%84%E6%93%8D%E4%BD%9C%E6%95%B0%E9%87%8F%20%3D%22,%20count%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20logarithmic%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%AF%B9%E6%95%B0%E9%98%B6%EF%BC%88%E5%BE%AA%E7%8E%AF%E5%AE%9E%E7%8E%B0%EF%BC%89%22%22%22%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20while%20n%20%3E%201%3A%0A%20%20%20%20%20%20%20%20n%20%3D%20n%20/%202%0A%20%20%20%20%20%20%20%20count%20%2B%3D%201%0A%20%20%20%20return%20count%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%208%0A%20%20%20%20print%28%22%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E5%A4%A7%E5%B0%8F%20n%20%3D%22,%20n%29%0A%0A%20%20%20%20count%20%3D%20logarithmic%28n%29%0A%20%20%20%20print%28%22%E5%AF%B9%E6%95%B0%E9%98%B6%EF%BC%88%E5%BE%AA%E7%8E%AF%E5%AE%9E%E7%8E%B0%EF%BC%89%E7%9A%84%E6%93%8D%E4%BD%9C%E6%95%B0%E9%87%8F%20%3D%22,%20count%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Full Screen ></a></div>
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 2-12 Logarithmic Order Time Complexity </p>
|
||||
<p align="center"> Figure 2-12 Logarithmic order time complexity </p>
|
||||
|
||||
Like exponential order, logarithmic order also frequently appears in recursive functions. The code below forms a recursive tree of height $\log_2 n$:
|
||||
|
||||
@@ -2945,7 +2945,7 @@ Logarithmic order is typical in algorithms based on the divide-and-conquer strat
|
||||
|
||||
This means the base $m$ can be changed without affecting the complexity. Therefore, we often omit the base $m$ and simply denote logarithmic order as $O(\log n)$.
|
||||
|
||||
### 6. Linear-Logarithmic Order $O(n \log n)$ {data-toc-label="6. Linear-Logarithmic Order"}
|
||||
### 6. Linear-logarithmic order $O(n \log n)$ {data-toc-label="6. Linear-logarithmic order"}
|
||||
|
||||
Linear-logarithmic order often appears in nested loops, with the complexities of the two loops being $O(\log n)$ and $O(n)$ respectively. The related code is as follows:
|
||||
|
||||
@@ -3162,13 +3162,13 @@ Linear-logarithmic order often appears in nested loops, with the complexities of
|
||||
|
||||
The image below demonstrates how linear-logarithmic order is generated. Each level of a binary tree has $n$ operations, and the tree has $\log_2 n + 1$ levels, resulting in a time complexity of $O(n \log n)$.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 2-13 Linear-Logarithmic Order Time Complexity </p>
|
||||
<p align="center"> Figure 2-13 Linear-logarithmic order time complexity </p>
|
||||
|
||||
Mainstream sorting algorithms typically have a time complexity of $O(n \log n)$, such as quicksort, mergesort, and heapsort.
|
||||
|
||||
### 7. Factorial Order $O(n!)$ {data-toc-label="7. Factorial Order"}
|
||||
### 7. Factorial order $O(n!)$ {data-toc-label="7. Factorial order"}
|
||||
|
||||
Factorial order corresponds to the mathematical problem of "full permutation." Given $n$ distinct elements, the total number of possible permutations is:
|
||||
|
||||
@@ -3402,13 +3402,13 @@ Factorials are typically implemented using recursion. As shown in the image and
|
||||
<div style="height: 495px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20factorial_recur%28n%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E9%98%B6%E4%B9%98%E9%98%B6%EF%BC%88%E9%80%92%E5%BD%92%E5%AE%9E%E7%8E%B0%EF%BC%89%22%22%22%0A%20%20%20%20if%20n%20%3D%3D%200%3A%0A%20%20%20%20%20%20%20%20return%201%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20%23%20%E4%BB%8E%201%20%E4%B8%AA%E5%88%86%E8%A3%82%E5%87%BA%20n%20%E4%B8%AA%0A%20%20%20%20for%20_%20in%20range%28n%29%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20factorial_recur%28n%20-%201%29%0A%20%20%20%20return%20count%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%204%0A%20%20%20%20print%28%22%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E5%A4%A7%E5%B0%8F%20n%20%3D%22,%20n%29%0A%0A%20%20%20%20count%20%3D%20factorial_recur%28n%29%0A%20%20%20%20print%28%22%E9%98%B6%E4%B9%98%E9%98%B6%EF%BC%88%E9%80%92%E5%BD%92%E5%AE%9E%E7%8E%B0%EF%BC%89%E7%9A%84%E6%93%8D%E4%BD%9C%E6%95%B0%E9%87%8F%20%3D%22,%20count%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20factorial_recur%28n%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E9%98%B6%E4%B9%98%E9%98%B6%EF%BC%88%E9%80%92%E5%BD%92%E5%AE%9E%E7%8E%B0%EF%BC%89%22%22%22%0A%20%20%20%20if%20n%20%3D%3D%200%3A%0A%20%20%20%20%20%20%20%20return%201%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20%23%20%E4%BB%8E%201%20%E4%B8%AA%E5%88%86%E8%A3%82%E5%87%BA%20n%20%E4%B8%AA%0A%20%20%20%20for%20_%20in%20range%28n%29%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20factorial_recur%28n%20-%201%29%0A%20%20%20%20return%20count%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%204%0A%20%20%20%20print%28%22%E8%BE%93%E5%85%A5%E6%95%B0%E6%8D%AE%E5%A4%A7%E5%B0%8F%20n%20%3D%22,%20n%29%0A%0A%20%20%20%20count%20%3D%20factorial_recur%28n%29%0A%20%20%20%20print%28%22%E9%98%B6%E4%B9%98%E9%98%B6%EF%BC%88%E9%80%92%E5%BD%92%E5%AE%9E%E7%8E%B0%EF%BC%89%E7%9A%84%E6%93%8D%E4%BD%9C%E6%95%B0%E9%87%8F%20%3D%22,%20count%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Full Screen ></a></div>
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 2-14 Factorial Order Time Complexity </p>
|
||||
<p align="center"> Figure 2-14 Factorial order time complexity </p>
|
||||
|
||||
Note that factorial order grows even faster than exponential order; it's unacceptable for larger $n$ values.
|
||||
|
||||
## 2.3.5 Worst, Best, and Average Time Complexities
|
||||
## 2.3.5 Worst, best, and average time complexities
|
||||
|
||||
**The time efficiency of an algorithm is often not fixed but depends on the distribution of the input data**. Assume we have an array `nums` of length $n$, consisting of numbers from $1$ to $n$, each appearing only once, but in a randomly shuffled order. The task is to return the index of the element $1$. We can draw the following conclusions:
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 3.2 Basic Data Types
|
||||
# 3.2 Basic data types
|
||||
|
||||
When discussing data in computers, various forms like text, images, videos, voice and 3D models comes to mind. Despite their different organizational forms, they are all composed of various basic data types.
|
||||
|
||||
@@ -22,7 +22,7 @@ The range of values for basic data types depends on the size of the space they o
|
||||
|
||||
The following table lists the space occupied, value range, and default values of various basic data types in Java. While memorizing this table isn't necessary, having a general understanding of it and referencing it when required is recommended.
|
||||
|
||||
<p align="center"> Table 3-1 Space Occupied and Value Range of Basic Data Types </p>
|
||||
<p align="center"> Table 3-1 Space occupied and value range of basic data types </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
|
||||
@@ -2,29 +2,29 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 3.4 Character Encoding *
|
||||
# 3.4 Character encoding *
|
||||
|
||||
In the computer system, all data is stored in binary form, and characters (represented by char) are no exception. To represent characters, we need to develop a "character set" that defines a one-to-one mapping between each character and binary numbers. With the character set, computers can convert binary numbers to characters by looking up the table.
|
||||
|
||||
## 3.4.1 ASCII Character Set
|
||||
## 3.4.1 ASCII character set
|
||||
|
||||
The "ASCII code" is one of the earliest character sets, officially known as the American Standard Code for Information Interchange. It uses 7 binary digits (the lower 7 bits of a byte) to represent a character, allowing for a maximum of 128 different characters. As shown in the Figure 3-6 , ASCII includes uppercase and lowercase English letters, numbers 0 ~ 9, various punctuation marks, and certain control characters (such as newline and tab).
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 3-6 ASCII Code </p>
|
||||
<p align="center"> Figure 3-6 ASCII code </p>
|
||||
|
||||
However, **ASCII can only represent English characters**. With the globalization of computers, a character set called "EASCII" was developed to represent more languages. It expands from the 7-bit structure of ASCII to 8 bits, enabling the representation of 256 characters.
|
||||
|
||||
Globally, various region-specific EASCII character sets have been introduced. The first 128 characters of these sets are consistent with the ASCII, while the remaining 128 characters are defined differently to accommodate the requirements of different languages.
|
||||
|
||||
## 3.4.2 GBK Character Set
|
||||
## 3.4.2 GBK character set
|
||||
|
||||
Later, it was found that **EASCII still could not meet the character requirements of many languages**. For instance, there are nearly a hundred thousand Chinese characters, with several thousand used regularly. In 1980, the Standardization Administration of China released the "GB2312" character set, which included 6763 Chinese characters, essentially fulfilling the computer processing needs for the Chinese language.
|
||||
|
||||
However, GB2312 could not handle some rare and traditional characters. The "GBK" character set expands GB2312 and includes 21886 Chinese characters. In the GBK encoding scheme, ASCII characters are represented with one byte, while Chinese characters use two bytes.
|
||||
|
||||
## 3.4.3 Unicode Character Set
|
||||
## 3.4.3 Unicode character set
|
||||
|
||||
With the rapid evolution of computer technology and a plethora of character sets and encoding standards, numerous problems arose. On the one hand, these character sets generally only defined characters for specific languages and could not function properly in multilingual environments. On the other hand, the existence of multiple character set standards for the same language caused garbled text when information was exchanged between computers using different encoding standards.
|
||||
|
||||
@@ -38,13 +38,13 @@ Unicode is a universal character set that assigns a number (called a "code point
|
||||
|
||||
A straightforward solution to this problem is to store all characters as equal-length encodings. As shown in the Figure 3-7 , each character in "Hello" occupies 1 byte, while each character in "算法" (algorithm) occupies 2 bytes. We could encode all characters in "Hello 算法" as 2 bytes by padding the higher bits with zeros. This method would enable the system to interpret a character every 2 bytes, recovering the content of the phrase.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 3-7 Unicode Encoding Example </p>
|
||||
<p align="center"> Figure 3-7 Unicode encoding example </p>
|
||||
|
||||
However, as ASCII has shown us, encoding English only requires 1 byte. Using the above approach would double the space occupied by English text compared to ASCII encoding, which is a waste of memory space. Therefore, a more efficient Unicode encoding method is needed.
|
||||
|
||||
## 3.4.4 UTF-8 Encoding
|
||||
## 3.4.4 UTF-8 encoding
|
||||
|
||||
Currently, UTF-8 has become the most widely used Unicode encoding method internationally. **It is a variable-length encoding**, using 1 to 4 bytes to represent a character, depending on the complexity of the character. ASCII characters need only 1 byte, Latin and Greek letters require 2 bytes, commonly used Chinese characters need 3 bytes, and some other rare characters need 4 bytes.
|
||||
|
||||
@@ -59,26 +59,26 @@ But why set the highest 2 bits of the remaining bytes to $10$? Actually, this $1
|
||||
|
||||
The reason for using $10$ as a checksum is that, under UTF-8 encoding rules, it's impossible for the highest two bits of a character to be $10$. This can be proven by contradiction: If the highest two bits of a character are $10$, it indicates that the character's length is $1$, corresponding to ASCII. However, the highest bit of an ASCII character should be $0$, which contradicts the assumption.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 3-8 UTF-8 Encoding Example </p>
|
||||
<p align="center"> Figure 3-8 UTF-8 encoding example </p>
|
||||
|
||||
Apart from UTF-8, other common encoding methods include:
|
||||
|
||||
- **UTF-16 Encoding**: Uses 2 or 4 bytes to represent a character. All ASCII characters and commonly used non-English characters are represented with 2 bytes; a few characters require 4 bytes. For 2-byte characters, the UTF-16 encoding equals the Unicode code point.
|
||||
- **UTF-32 Encoding**: Every character uses 4 bytes. This means UTF-32 occupies more space than UTF-8 and UTF-16, especially for texts with a high proportion of ASCII characters.
|
||||
- **UTF-16 encoding**: Uses 2 or 4 bytes to represent a character. All ASCII characters and commonly used non-English characters are represented with 2 bytes; a few characters require 4 bytes. For 2-byte characters, the UTF-16 encoding equals the Unicode code point.
|
||||
- **UTF-32 encoding**: Every character uses 4 bytes. This means UTF-32 occupies more space than UTF-8 and UTF-16, especially for texts with a high proportion of ASCII characters.
|
||||
|
||||
From the perspective of storage space, using UTF-8 to represent English characters is very efficient because it only requires 1 byte; using UTF-16 to encode some non-English characters (such as Chinese) can be more efficient because it only requires 2 bytes, while UTF-8 might need 3 bytes.
|
||||
|
||||
From a compatibility perspective, UTF-8 is the most versatile, with many tools and libraries supporting UTF-8 as a priority.
|
||||
|
||||
## 3.4.5 Character Encoding in Programming Languages
|
||||
## 3.4.5 Character encoding in programming languages
|
||||
|
||||
Historically, many programming languages utilized fixed-length encodings such as UTF-16 or UTF-32 for processing strings during program execution. This allows strings to be handled as arrays, offering several advantages:
|
||||
|
||||
- **Random Access**: Strings encoded in UTF-16 can be accessed randomly with ease. For UTF-8, which is a variable-length encoding, locating the $i^{th}$ character requires traversing the string from the start to the $i^{th}$ position, taking $O(n)$ time.
|
||||
- **Character Counting**: Similar to random access, counting the number of characters in a UTF-16 encoded string is an $O(1)$ operation. However, counting characters in a UTF-8 encoded string requires traversing the entire string.
|
||||
- **String Operations**: Many string operations like splitting, concatenating, inserting, and deleting are easier on UTF-16 encoded strings. These operations generally require additional computation on UTF-8 encoded strings to ensure the validity of the UTF-8 encoding.
|
||||
- **Random access**: Strings encoded in UTF-16 can be accessed randomly with ease. For UTF-8, which is a variable-length encoding, locating the $i^{th}$ character requires traversing the string from the start to the $i^{th}$ position, taking $O(n)$ time.
|
||||
- **Character counting**: Similar to random access, counting the number of characters in a UTF-16 encoded string is an $O(1)$ operation. However, counting characters in a UTF-8 encoded string requires traversing the entire string.
|
||||
- **String operations**: Many string operations like splitting, concatenating, inserting, and deleting are easier on UTF-16 encoded strings. These operations generally require additional computation on UTF-8 encoded strings to ensure the validity of the UTF-8 encoding.
|
||||
|
||||
The design of character encoding schemes in programming languages is an interesting topic involving various factors:
|
||||
|
||||
|
||||
@@ -2,38 +2,38 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 3.1 Classification of Data Structures
|
||||
# 3.1 Classification of data structures
|
||||
|
||||
Common data structures include arrays, linked lists, stacks, queues, hash tables, trees, heaps, and graphs. They can be classified into "logical structure" and "physical structure".
|
||||
|
||||
## 3.1.1 Logical Structure: Linear and Non-Linear
|
||||
## 3.1.1 Logical structure: linear and non-linear
|
||||
|
||||
**The logical structures reveal the logical relationships between data elements**. In arrays and linked lists, data are arranged in a specific sequence, demonstrating the linear relationship between data; while in trees, data are arranged hierarchically from the top down, showing the derived relationship between "ancestors" and "descendants"; and graphs are composed of nodes and edges, reflecting the intricate network relationship.
|
||||
|
||||
As shown in the Figure 3-1 , logical structures can be divided into two major categories: "linear" and "non-linear". Linear structures are more intuitive, indicating data is arranged linearly in logical relationships; non-linear structures, conversely, are arranged non-linearly.
|
||||
|
||||
- **Linear Data Structures**: Arrays, Linked Lists, Stacks, Queues, Hash Tables.
|
||||
- **Non-Linear Data Structures**: Trees, Heaps, Graphs, Hash Tables.
|
||||
- **Linear data structures**: Arrays, Linked Lists, Stacks, Queues, Hash Tables.
|
||||
- **Non-linear data structures**: Trees, Heaps, Graphs, Hash Tables.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 3-1 Linear and Non-Linear Data Structures </p>
|
||||
<p align="center"> Figure 3-1 Linear and non-linear data structures </p>
|
||||
|
||||
Non-linear data structures can be further divided into tree structures and network structures.
|
||||
|
||||
- **Linear Structures**: Arrays, linked lists, queues, stacks, and hash tables, where elements have a one-to-one sequential relationship.
|
||||
- **Tree Structures**: Trees, Heaps, Hash Tables, where elements have a one-to-many relationship.
|
||||
- **Network Structures**: Graphs, where elements have a many-to-many relationships.
|
||||
- **Linear structures**: Arrays, linked lists, queues, stacks, and hash tables, where elements have a one-to-one sequential relationship.
|
||||
- **Tree structures**: Trees, Heaps, Hash Tables, where elements have a one-to-many relationship.
|
||||
- **Network structures**: Graphs, where elements have a many-to-many relationships.
|
||||
|
||||
## 3.1.2 Physical Structure: Contiguous and Dispersed
|
||||
## 3.1.2 Physical structure: contiguous and dispersed
|
||||
|
||||
**During the execution of an algorithm, the data being processed is stored in memory**. The Figure 3-2 shows a computer memory stick where each black square is a physical memory space. We can think of memory as a vast Excel spreadsheet, with each cell capable of storing a certain amount of data.
|
||||
|
||||
**The system accesses the data at the target location by means of a memory address**. As shown in the Figure 3-2 , the computer assigns a unique identifier to each cell in the table according to specific rules, ensuring that each memory space has a unique memory address. With these addresses, the program can access the data stored in memory.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 3-2 Memory Stick, Memory Spaces, Memory Addresses </p>
|
||||
<p align="center"> Figure 3-2 Memory stick, memory spaces, memory addresses </p>
|
||||
|
||||
!!! tip
|
||||
|
||||
@@ -43,9 +43,9 @@ Memory is a shared resource for all programs. When a block of memory is occupied
|
||||
|
||||
As illustrated in the Figure 3-3 , **the physical structure reflects the way data is stored in computer memory** and it can be divided into contiguous space storage (arrays) and non-contiguous space storage (linked lists). The two types of physical structures exhibit complementary characteristics in terms of time efficiency and space efficiency.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 3-3 Contiguous Space Storage and Dispersed Space Storage </p>
|
||||
<p align="center"> Figure 3-3 Contiguous space storage and dispersed space storage </p>
|
||||
|
||||
**It is worth noting that all data structures are implemented based on arrays, linked lists, or a combination of both**. For example, stacks and queues can be implemented using either arrays or linked lists; while implementations of hash tables may involve both arrays and linked lists.
|
||||
- **Array-based implementations**: Stacks, Queues, Hash Tables, Trees, Heaps, Graphs, Matrices, Tensors (arrays with dimensions $\geq 3$).
|
||||
|
||||
@@ -3,9 +3,9 @@ comments: true
|
||||
icon: material/shape-outline
|
||||
---
|
||||
|
||||
# Chapter 3. Data Structures
|
||||
# Chapter 3. Data structures
|
||||
|
||||
{ class="cover-image" }
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
@@ -15,8 +15,8 @@ icon: material/shape-outline
|
||||
|
||||
## Chapter Contents
|
||||
|
||||
- [3.1 Classification of Data Structures](https://www.hello-algo.com/en/chapter_data_structure/classification_of_data_structure/)
|
||||
- [3.2 Fundamental Data Types](https://www.hello-algo.com/en/chapter_data_structure/basic_data_types/)
|
||||
- [3.3 Number Encoding *](https://www.hello-algo.com/en/chapter_data_structure/number_encoding/)
|
||||
- [3.4 Character Encoding *](https://www.hello-algo.com/en/chapter_data_structure/character_encoding/)
|
||||
- [3.1 Classification of data structures](https://www.hello-algo.com/en/chapter_data_structure/classification_of_data_structure/)
|
||||
- [3.2 Fundamental data types](https://www.hello-algo.com/en/chapter_data_structure/basic_data_types/)
|
||||
- [3.3 Number encoding *](https://www.hello-algo.com/en/chapter_data_structure/number_encoding/)
|
||||
- [3.4 Character encoding *](https://www.hello-algo.com/en/chapter_data_structure/character_encoding/)
|
||||
- [3.5 Summary](https://www.hello-algo.com/en/chapter_data_structure/summary/)
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 3.3 Number Encoding *
|
||||
# 3.3 Number encoding *
|
||||
|
||||
!!! note
|
||||
|
||||
In this book, chapters marked with an asterisk '*' are optional readings. If you are short on time or find them challenging, you may skip these initially and return to them after completing the essential chapters.
|
||||
|
||||
## 3.3.1 Integer Encoding
|
||||
## 3.3.1 Integer encoding
|
||||
|
||||
In the table from the previous section, we observed that all integer types can represent one more negative number than positive numbers, such as the `byte` range of $[-128, 127]$. This phenomenon seems counterintuitive, and its underlying reason involves knowledge of sign-magnitude, one's complement, and two's complement encoding.
|
||||
|
||||
@@ -20,9 +20,9 @@ Firstly, it's important to note that **numbers are stored in computers using the
|
||||
|
||||
The following diagram illustrates the conversions among sign-magnitude, one's complement, and two's complement:
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 3-4 Conversions between Sign-Magnitude, One's Complement, and Two's Complement </p>
|
||||
<p align="center"> Figure 3-4 Conversions between sign-magnitude, one's complement, and two's complement </p>
|
||||
|
||||
Although sign-magnitude is the most intuitive, it has limitations. For one, **negative numbers in sign-magnitude cannot be directly used in calculations**. For example, in sign-magnitude, calculating $1 + (-2)$ results in $-3$, which is incorrect.
|
||||
|
||||
@@ -92,7 +92,7 @@ We can now summarize the reason for using two's complement in computers: with tw
|
||||
|
||||
The design of two's complement is quite ingenious, and due to space constraints, we'll stop here. Interested readers are encouraged to explore further.
|
||||
|
||||
## 3.3.2 Floating-Point Number Encoding
|
||||
## 3.3.2 Floating-point number encoding
|
||||
|
||||
You might have noticed something intriguing: despite having the same length of 4 bytes, why does a `float` have a much larger range of values compared to an `int`? This seems counterintuitive, as one would expect the range to shrink for `float` since it needs to represent fractions.
|
||||
|
||||
@@ -129,9 +129,9 @@ $$
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 3-5 Example Calculation of a float in IEEE 754 Standard </p>
|
||||
<p align="center"> Figure 3-5 Example calculation of a float in IEEE 754 standard </p>
|
||||
|
||||
Observing the diagram, given an example data $\mathrm{S} = 0$, $\mathrm{E} = 124$, $\mathrm{N} = 2^{-2} + 2^{-3} = 0.375$, we have:
|
||||
|
||||
@@ -145,7 +145,7 @@ Now we can answer the initial question: **The representation of `float` includes
|
||||
|
||||
As shown in the Table 3-2 , exponent bits $E = 0$ and $E = 255$ have special meanings, **used to represent zero, infinity, $\mathrm{NaN}$, etc.**
|
||||
|
||||
<p align="center"> Table 3-2 Meaning of Exponent Bits </p>
|
||||
<p align="center"> Table 3-2 Meaning of exponent bits </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ comments: true
|
||||
|
||||
# 3.5 Summary
|
||||
|
||||
### 1. Key Review
|
||||
### 1. Key review
|
||||
|
||||
- Data structures can be categorized from two perspectives: logical structure and physical structure. Logical structure describes the logical relationships between data elements, while physical structure describes how data is stored in computer memory.
|
||||
- Common logical structures include linear, tree-like, and network structures. We generally classify data structures into linear (arrays, linked lists, stacks, queues) and non-linear (trees, graphs, heaps) based on their logical structure. The implementation of hash tables may involve both linear and non-linear data structures.
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 6.3 Hash Algorithms
|
||||
# 6.3 Hash algorithms
|
||||
|
||||
The previous two sections introduced the working principle of hash tables and the methods to handle hash collisions. However, both open addressing and chaining can **only ensure that the hash table functions normally when collisions occur, but cannot reduce the frequency of hash collisions**.
|
||||
|
||||
If hash collisions occur too frequently, the performance of the hash table will deteriorate drastically. As shown in the Figure 6-8 , for a chaining hash table, in the ideal case, the key-value pairs are evenly distributed across the buckets, achieving optimal query efficiency; in the worst case, all key-value pairs are stored in the same bucket, degrading the time complexity to $O(n)$.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 6-8 Ideal and Worst Cases of Hash Collisions </p>
|
||||
<p align="center"> Figure 6-8 Ideal and worst cases of hash collisions </p>
|
||||
|
||||
**The distribution of key-value pairs is determined by the hash function**. Recalling the steps of calculating a hash function, first compute the hash value, then modulo it by the array length:
|
||||
|
||||
@@ -22,35 +22,35 @@ Observing the above formula, when the hash table capacity `capacity` is fixed, *
|
||||
|
||||
This means that, to reduce the probability of hash collisions, we should focus on the design of the hash algorithm `hash()`.
|
||||
|
||||
## 6.3.1 Goals of Hash Algorithms
|
||||
## 6.3.1 Goals of hash algorithms
|
||||
|
||||
To achieve a "fast and stable" hash table data structure, hash algorithms should have the following characteristics:
|
||||
|
||||
- **Determinism**: For the same input, the hash algorithm should always produce the same output. Only then can the hash table be reliable.
|
||||
- **High Efficiency**: The process of computing the hash value should be fast enough. The smaller the computational overhead, the more practical the hash table.
|
||||
- **Uniform Distribution**: The hash algorithm should ensure that key-value pairs are evenly distributed in the hash table. The more uniform the distribution, the lower the probability of hash collisions.
|
||||
- **High efficiency**: The process of computing the hash value should be fast enough. The smaller the computational overhead, the more practical the hash table.
|
||||
- **Uniform distribution**: The hash algorithm should ensure that key-value pairs are evenly distributed in the hash table. The more uniform the distribution, the lower the probability of hash collisions.
|
||||
|
||||
In fact, hash algorithms are not only used to implement hash tables but are also widely applied in other fields.
|
||||
|
||||
- **Password Storage**: To protect the security of user passwords, systems usually do not store the plaintext passwords but rather the hash values of the passwords. When a user enters a password, the system calculates the hash value of the input and compares it with the stored hash value. If they match, the password is considered correct.
|
||||
- **Data Integrity Check**: The data sender can calculate the hash value of the data and send it along; the receiver can recalculate the hash value of the received data and compare it with the received hash value. If they match, the data is considered intact.
|
||||
- **Password storage**: To protect the security of user passwords, systems usually do not store the plaintext passwords but rather the hash values of the passwords. When a user enters a password, the system calculates the hash value of the input and compares it with the stored hash value. If they match, the password is considered correct.
|
||||
- **Data integrity check**: The data sender can calculate the hash value of the data and send it along; the receiver can recalculate the hash value of the received data and compare it with the received hash value. If they match, the data is considered intact.
|
||||
|
||||
For cryptographic applications, to prevent reverse engineering such as deducing the original password from the hash value, hash algorithms need higher-level security features.
|
||||
|
||||
- **Unidirectionality**: It should be impossible to deduce any information about the input data from the hash value.
|
||||
- **Collision Resistance**: It should be extremely difficult to find two different inputs that produce the same hash value.
|
||||
- **Avalanche Effect**: Minor changes in the input should lead to significant and unpredictable changes in the output.
|
||||
- **Collision resistance**: It should be extremely difficult to find two different inputs that produce the same hash value.
|
||||
- **Avalanche effect**: Minor changes in the input should lead to significant and unpredictable changes in the output.
|
||||
|
||||
Note that **"Uniform Distribution" and "Collision Resistance" are two separate concepts**. Satisfying uniform distribution does not necessarily mean collision resistance. For example, under random input `key`, the hash function `key % 100` can produce a uniformly distributed output. However, this hash algorithm is too simple, and all `key` with the same last two digits will have the same output, making it easy to deduce a usable `key` from the hash value, thereby cracking the password.
|
||||
|
||||
## 6.3.2 Design of Hash Algorithms
|
||||
## 6.3.2 Design of hash algorithms
|
||||
|
||||
The design of hash algorithms is a complex issue that requires consideration of many factors. However, for some less demanding scenarios, we can also design some simple hash algorithms.
|
||||
|
||||
- **Additive Hash**: Add up the ASCII codes of each character in the input and use the total sum as the hash value.
|
||||
- **Multiplicative Hash**: Utilize the non-correlation of multiplication, multiplying each round by a constant, accumulating the ASCII codes of each character into the hash value.
|
||||
- **XOR Hash**: Accumulate the hash value by XORing each element of the input data.
|
||||
- **Rotating Hash**: Accumulate the ASCII code of each character into a hash value, performing a rotation operation on the hash value before each accumulation.
|
||||
- **Additive hash**: Add up the ASCII codes of each character in the input and use the total sum as the hash value.
|
||||
- **Multiplicative hash**: Utilize the non-correlation of multiplication, multiplying each round by a constant, accumulating the ASCII codes of each character into the hash value.
|
||||
- **XOR hash**: Accumulate the hash value by XORing each element of the input data.
|
||||
- **Rotating hash**: Accumulate the ASCII code of each character into a hash value, performing a rotation operation on the hash value before each accumulation.
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -651,7 +651,7 @@ It is worth noting that if the `key` is guaranteed to be randomly and uniformly
|
||||
|
||||
In summary, we usually choose a prime number as the modulus, and this prime number should be large enough to eliminate periodic patterns as much as possible, enhancing the robustness of the hash algorithm.
|
||||
|
||||
## 6.3.3 Common Hash Algorithms
|
||||
## 6.3.3 Common hash algorithms
|
||||
|
||||
It is not hard to see that the simple hash algorithms mentioned above are quite "fragile" and far from reaching the design goals of hash algorithms. For example, since addition and XOR obey the commutative law, additive hash and XOR hash cannot distinguish strings with the same content but in different order, which may exacerbate hash collisions and cause security issues.
|
||||
|
||||
@@ -663,7 +663,7 @@ Over the past century, hash algorithms have been in a continuous process of upgr
|
||||
- SHA-2 series, especially SHA-256, is one of the most secure hash algorithms to date, with no successful attacks reported, hence commonly used in various security applications and protocols.
|
||||
- SHA-3 has lower implementation costs and higher computational efficiency compared to SHA-2, but its current usage coverage is not as extensive as the SHA-2 series.
|
||||
|
||||
<p align="center"> Table 6-2 Common Hash Algorithms </p>
|
||||
<p align="center"> Table 6-2 Common hash algorithms </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
@@ -677,7 +677,7 @@ Over the past century, hash algorithms have been in a continuous process of upgr
|
||||
|
||||
</div>
|
||||
|
||||
# Hash Values in Data Structures
|
||||
# Hash values in data structures
|
||||
|
||||
We know that the keys in a hash table can be of various data types such as integers, decimals, or strings. Programming languages usually provide built-in hash algorithms for these data types to calculate the bucket indices in the hash table. Taking Python as an example, we can use the `hash()` function to compute the hash values for various data types.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 6.2 Hash Collision
|
||||
# 6.2 Hash collision
|
||||
|
||||
As mentioned in the previous section, **usually the input space of a hash function is much larger than its output space**, making hash collisions theoretically inevitable. For example, if the input space consists of all integers and the output space is the size of the array capacity, multiple integers will inevitably map to the same bucket index.
|
||||
|
||||
@@ -13,24 +13,24 @@ Hash collisions can lead to incorrect query results, severely affecting the usab
|
||||
|
||||
There are mainly two methods for improving the structure of hash tables: "Separate Chaining" and "Open Addressing".
|
||||
|
||||
## 6.2.1 Separate Chaining
|
||||
## 6.2.1 Separate chaining
|
||||
|
||||
In the original hash table, each bucket can store only one key-value pair. "Separate chaining" transforms individual elements into a linked list, with key-value pairs as list nodes, storing all colliding key-value pairs in the same list. The Figure 6-5 shows an example of a hash table with separate chaining.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 6-5 Separate Chaining Hash Table </p>
|
||||
<p align="center"> Figure 6-5 Separate chaining hash table </p>
|
||||
|
||||
The operations of a hash table implemented with separate chaining have changed as follows:
|
||||
|
||||
- **Querying Elements**: Input `key`, pass through the hash function to obtain the bucket index, access the head node of the list, then traverse the list and compare `key` to find the target key-value pair.
|
||||
- **Adding Elements**: First access the list head node via the hash function, then add the node (key-value pair) to the list.
|
||||
- **Deleting Elements**: Access the list head based on the hash function's result, then traverse the list to find and remove the target node.
|
||||
- **Querying elements**: Input `key`, pass through the hash function to obtain the bucket index, access the head node of the list, then traverse the list and compare `key` to find the target key-value pair.
|
||||
- **Adding elements**: First access the list head node via the hash function, then add the node (key-value pair) to the list.
|
||||
- **Deleting elements**: Access the list head based on the hash function's result, then traverse the list to find and remove the target node.
|
||||
|
||||
Separate chaining has the following limitations:
|
||||
|
||||
- **Increased Space Usage**: The linked list contains node pointers, which consume more memory space than arrays.
|
||||
- **Reduced Query Efficiency**: Due to the need for linear traversal of the list to find the corresponding element.
|
||||
- **Increased space usage**: The linked list contains node pointers, which consume more memory space than arrays.
|
||||
- **Reduced query efficiency**: Due to the need for linear traversal of the list to find the corresponding element.
|
||||
|
||||
The code below provides a simple implementation of a separate chaining hash table, with two things to note:
|
||||
|
||||
@@ -1445,32 +1445,32 @@ The code below provides a simple implementation of a separate chaining hash tabl
|
||||
|
||||
It's worth noting that when the list is very long, the query efficiency $O(n)$ is poor. **At this point, the list can be converted to an "AVL tree" or "Red-Black tree"** to optimize the time complexity of the query operation to $O(\log n)$.
|
||||
|
||||
## 6.2.2 Open Addressing
|
||||
## 6.2.2 Open addressing
|
||||
|
||||
"Open addressing" does not introduce additional data structures but uses "multiple probes" to handle hash collisions. The probing methods mainly include linear probing, quadratic probing, and double hashing.
|
||||
|
||||
Let's use linear probing as an example to introduce the mechanism of open addressing hash tables.
|
||||
|
||||
### 1. Linear Probing
|
||||
### 1. Linear probing
|
||||
|
||||
Linear probing uses a fixed-step linear search for probing, differing from ordinary hash tables.
|
||||
|
||||
- **Inserting Elements**: Calculate the bucket index using the hash function. If the bucket already contains an element, linearly traverse forward from the conflict position (usually with a step size of $1$) until an empty bucket is found, then insert the element.
|
||||
- **Searching for Elements**: If a hash collision is found, use the same step size to linearly traverse forward until the corresponding element is found and return `value`; if an empty bucket is encountered, it means the target element is not in the hash table, so return `None`.
|
||||
- **Inserting elements**: Calculate the bucket index using the hash function. If the bucket already contains an element, linearly traverse forward from the conflict position (usually with a step size of $1$) until an empty bucket is found, then insert the element.
|
||||
- **Searching for elements**: If a hash collision is found, use the same step size to linearly traverse forward until the corresponding element is found and return `value`; if an empty bucket is encountered, it means the target element is not in the hash table, so return `None`.
|
||||
|
||||
The Figure 6-6 shows the distribution of key-value pairs in an open addressing (linear probing) hash table. According to this hash function, keys with the same last two digits will be mapped to the same bucket. Through linear probing, they are stored consecutively in that bucket and the buckets below it.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 6-6 Distribution of Key-Value Pairs in Open Addressing (Linear Probing) Hash Table </p>
|
||||
<p align="center"> Figure 6-6 Distribution of key-value pairs in open addressing (linear probing) hash table </p>
|
||||
|
||||
However, **linear probing tends to create "clustering"**. Specifically, the longer a continuous position in the array is occupied, the more likely these positions are to encounter hash collisions, further promoting the growth of these clusters and eventually leading to deterioration in the efficiency of operations.
|
||||
|
||||
It's important to note that **we cannot directly delete elements in an open addressing hash table**. Deleting an element creates an empty bucket `None` in the array. When searching for elements, if linear probing encounters this empty bucket, it will return, making the elements below this bucket inaccessible. The program may incorrectly assume these elements do not exist, as shown in the Figure 6-7 .
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 6-7 Query Issues Caused by Deletion in Open Addressing </p>
|
||||
<p align="center"> Figure 6-7 Query issues caused by deletion in open addressing </p>
|
||||
|
||||
To solve this problem, we can use a "lazy deletion" mechanism: instead of directly removing elements from the hash table, **use a constant `TOMBSTONE` to mark the bucket**. In this mechanism, both `None` and `TOMBSTONE` represent empty buckets and can hold key-value pairs. However, when linear probing encounters `TOMBSTONE`, it should continue traversing since there may still be key-value pairs below it.
|
||||
|
||||
@@ -3090,7 +3090,7 @@ The code below implements an open addressing (linear probing) hash table with la
|
||||
[class]{HashMapOpenAddressing}-[func]{}
|
||||
```
|
||||
|
||||
### 2. Quadratic Probing
|
||||
### 2. Quadratic probing
|
||||
|
||||
Quadratic probing is similar to linear probing and is one of the common strategies of open addressing. When a collision occurs, quadratic probing does not simply skip a fixed number of steps but skips "the square of the number of probes," i.e., $1, 4, 9, \dots$ steps.
|
||||
|
||||
@@ -3104,12 +3104,12 @@ However, quadratic probing is not perfect:
|
||||
- Clustering still exists, i.e., some positions are more likely to be occupied than others.
|
||||
- Due to the growth of squares, quadratic probing may not probe the entire hash table, meaning it might not access empty buckets even if they exist in the hash table.
|
||||
|
||||
### 3. Double Hashing
|
||||
### 3. Double hashing
|
||||
|
||||
As the name suggests, the double hashing method uses multiple hash functions $f_1(x)$, $f_2(x)$, $f_3(x)$, $\dots$ for probing.
|
||||
|
||||
- **Inserting Elements**: If hash function $f_1(x)$ encounters a conflict, try $f_2(x)$, and so on, until an empty position is found and the element is inserted.
|
||||
- **Searching for Elements**: Search in the same order of hash functions until the target element is found and returned; if an empty position is encountered or all hash functions have been tried, it indicates the element is not in the hash table, then return `None`.
|
||||
- **Inserting elements**: If hash function $f_1(x)$ encounters a conflict, try $f_2(x)$, and so on, until an empty position is found and the element is inserted.
|
||||
- **Searching for elements**: Search in the same order of hash functions until the target element is found and returned; if an empty position is encountered or all hash functions have been tried, it indicates the element is not in the hash table, then return `None`.
|
||||
|
||||
Compared to linear probing, double hashing is less prone to clustering but involves additional computation for multiple hash functions.
|
||||
|
||||
@@ -3117,7 +3117,7 @@ Compared to linear probing, double hashing is less prone to clustering but invol
|
||||
|
||||
Please note that open addressing (linear probing, quadratic probing, and double hashing) hash tables all have the issue of "not being able to directly delete elements."
|
||||
|
||||
## 6.2.3 Choice of Programming Languages
|
||||
## 6.2.3 Choice of programming languages
|
||||
|
||||
Various programming languages have adopted different hash table implementation strategies, here are a few examples:
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -3,9 +3,9 @@ comments: true
|
||||
icon: material/table-search
|
||||
---
|
||||
|
||||
# Chapter 6. Hash Table
|
||||
# Chapter 6. Hash table
|
||||
|
||||
{ class="cover-image" }
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
@@ -15,7 +15,7 @@ icon: material/table-search
|
||||
|
||||
## Chapter Contents
|
||||
|
||||
- [6.1 Hash Table](https://www.hello-algo.com/en/chapter_hashing/hash_map/)
|
||||
- [6.2 Hash Collision](https://www.hello-algo.com/en/chapter_hashing/hash_collision/)
|
||||
- [6.3 Hash Algorithm](https://www.hello-algo.com/en/chapter_hashing/hash_algorithm/)
|
||||
- [6.1 Hash table](https://www.hello-algo.com/en/chapter_hashing/hash_map/)
|
||||
- [6.2 Hash collision](https://www.hello-algo.com/en/chapter_hashing/hash_collision/)
|
||||
- [6.3 Hash algorithm](https://www.hello-algo.com/en/chapter_hashing/hash_algorithm/)
|
||||
- [6.4 Summary](https://www.hello-algo.com/en/chapter_hashing/summary/)
|
||||
|
||||
@@ -4,7 +4,7 @@ comments: true
|
||||
|
||||
# 6.4 Summary
|
||||
|
||||
### 1. Key Review
|
||||
### 1. Key review
|
||||
|
||||
- Given an input `key`, a hash table can retrieve the corresponding `value` in $O(1)$ time, which is highly efficient.
|
||||
- Common hash table operations include querying, adding key-value pairs, deleting key-value pairs, and traversing the hash table.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 1.1 Algorithms are Everywhere
|
||||
# 1.1 Algorithms are everywhere
|
||||
|
||||
When we hear the word "algorithm," we naturally think of mathematics. However, many algorithms do not involve complex mathematics but rely more on basic logic, which can be seen everywhere in our daily lives.
|
||||
|
||||
@@ -39,9 +39,9 @@ This essential skill for elementary students, looking up a dictionary, is actual
|
||||
2. Take out a card from the unordered section and insert it into the correct position in the ordered section; after this, the leftmost two cards are in order.
|
||||
3. Continue to repeat step `2.` until all cards are in order.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 1-2 Playing Cards Sorting Process </p>
|
||||
<p align="center"> Figure 1-2 Playing cards sorting process </p>
|
||||
|
||||
The above method of organizing playing cards is essentially the "Insertion Sort" algorithm, which is very efficient for small datasets. Many programming languages' sorting functions include the insertion sort.
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ comments: true
|
||||
icon: material/calculator-variant-outline
|
||||
---
|
||||
|
||||
# Chapter 1. Introduction to Algorithms
|
||||
# Chapter 1. Introduction to algorithms
|
||||
|
||||
{ class="cover-image" }
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
@@ -15,6 +15,6 @@ icon: material/calculator-variant-outline
|
||||
|
||||
## Chapter Contents
|
||||
|
||||
- [1.1 Algorithms are Everywhere](https://www.hello-algo.com/en/chapter_introduction/algorithms_are_everywhere/)
|
||||
- [1.2 What is an Algorithm](https://www.hello-algo.com/en/chapter_introduction/what_is_dsa/)
|
||||
- [1.1 Algorithms are everywhere](https://www.hello-algo.com/en/chapter_introduction/algorithms_are_everywhere/)
|
||||
- [1.2 What is an algorithm](https://www.hello-algo.com/en/chapter_introduction/what_is_dsa/)
|
||||
- [1.3 Summary](https://www.hello-algo.com/en/chapter_introduction/summary/)
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 1.2 What is an Algorithm
|
||||
# 1.2 What is an algorithm
|
||||
|
||||
## 1.2.1 Definition of an Algorithm
|
||||
## 1.2.1 Definition of an algorithm
|
||||
|
||||
An "algorithm" is a set of instructions or steps to solve a specific problem within a finite amount of time. It has the following characteristics:
|
||||
|
||||
@@ -12,7 +12,7 @@ An "algorithm" is a set of instructions or steps to solve a specific problem wit
|
||||
- The algorithm is feasible, meaning it can be completed within a finite number of steps, time, and memory space.
|
||||
- Each step has a definitive meaning. The output is consistently the same under the same inputs and conditions.
|
||||
|
||||
## 1.2.2 Definition of a Data Structure
|
||||
## 1.2.2 Definition of a data structure
|
||||
|
||||
A "data structure" is a way of organizing and storing data in a computer, with the following design goals:
|
||||
|
||||
@@ -25,7 +25,7 @@ A "data structure" is a way of organizing and storing data in a computer, with t
|
||||
- Compared to arrays, linked lists offer more convenience in data addition and deletion but sacrifice data access speed.
|
||||
- Graphs, compared to linked lists, provide richer logical information but require more memory space.
|
||||
|
||||
## 1.2.3 Relationship Between Data Structures and Algorithms
|
||||
## 1.2.3 Relationship between data structures and algorithms
|
||||
|
||||
As shown in the Figure 1-4 , data structures and algorithms are highly related and closely integrated, specifically in the following three aspects:
|
||||
|
||||
@@ -45,7 +45,7 @@ Data structures and algorithms can be likened to a set of building blocks, as il
|
||||
|
||||
The detailed correspondence between the two is shown in the Table 1-1 .
|
||||
|
||||
<p align="center"> Table 1-1 Comparing Data Structures and Algorithms to Building Blocks </p>
|
||||
<p align="center"> Table 1-1 Comparing data structures and algorithms to building blocks </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 0.1 About This Book
|
||||
# 0.1 About this book
|
||||
|
||||
This open-source project aims to create a free, and beginner-friendly crash course on data structures and algorithms.
|
||||
|
||||
@@ -10,7 +10,7 @@ This open-source project aims to create a free, and beginner-friendly crash cour
|
||||
- Run code with just one click, supporting Java, C++, Python, Go, JS, TS, C#, Swift, Rust, Dart, Zig and other languages.
|
||||
- Readers are encouraged to engage with each other in the discussion area for each section, questions and comments are usually answered within two days.
|
||||
|
||||
## 0.1.1 Target Audience
|
||||
## 0.1.1 Target audience
|
||||
|
||||
If you are new to algorithms with limited exposure, or you have accumulated some experience in algorithms, but you only have a vague understanding of data structures and algorithms, and you are constantly jumping between "yep" and "hmm", then this book is for you!
|
||||
|
||||
@@ -22,17 +22,17 @@ If you are an algorithm expert, we look forward to receiving your valuable sugge
|
||||
|
||||
You should know how to write and read simple code in at least one programming language.
|
||||
|
||||
## 0.1.2 Content Structure
|
||||
## 0.1.2 Content structure
|
||||
|
||||
The main content of the book is shown in the following figure.
|
||||
|
||||
- **Complexity Analysis**: explores aspects and methods for evaluating data structures and algorithms. Covers methods of deriving time complexity and space complexity, along with common types and examples.
|
||||
- **Data Structures**: focuses on fundamental data types, classification methods, definitions, pros and cons, common operations, types, applications, and implementation methods of data structures such as array, linked list, stack, queue, hash table, tree, heap, graph, etc.
|
||||
- **Complexity analysis**: explores aspects and methods for evaluating data structures and algorithms. Covers methods of deriving time complexity and space complexity, along with common types and examples.
|
||||
- **Data structures**: focuses on fundamental data types, classification methods, definitions, pros and cons, common operations, types, applications, and implementation methods of data structures such as array, linked list, stack, queue, hash table, tree, heap, graph, etc.
|
||||
- **Algorithms**: defines algorithms, discusses their pros and cons, efficiency, application scenarios, problem-solving steps, and includes sample questions for various algorithms such as search, sorting, divide and conquer, backtracking, dynamic programming, greedy algorithms, and more.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 0-1 Main Content of the Book </p>
|
||||
<p align="center"> Figure 0-1 Main content of the book </p>
|
||||
|
||||
## 0.1.3 Acknowledgements
|
||||
|
||||
|
||||
@@ -15,6 +15,6 @@ icon: material/book-open-outline
|
||||
|
||||
## Chapter Contents
|
||||
|
||||
- [0.1 About This Book](https://www.hello-algo.com/en/chapter_preface/about_the_book/)
|
||||
- [0.2 How to Read](https://www.hello-algo.com/en/chapter_preface/suggestions/)
|
||||
- [0.1 About this book](https://www.hello-algo.com/en/chapter_preface/about_the_book/)
|
||||
- [0.2 How to read](https://www.hello-algo.com/en/chapter_preface/suggestions/)
|
||||
- [0.3 Summary](https://www.hello-algo.com/en/chapter_preface/summary/)
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 0.2 How to Read
|
||||
# 0.2 How to read
|
||||
|
||||
!!! tip
|
||||
|
||||
For the best reading experience, it is recommended that you read through this section.
|
||||
|
||||
## 0.2.1 Writing Conventions
|
||||
## 0.2.1 Writing conventions
|
||||
|
||||
- Chapters marked with '*' after the title are optional and contain relatively challenging content. If you are short on time, it is advisable to skip them.
|
||||
- Technical terms will be in boldface (in the print and PDF versions) or underlined (in the web version), for instance, <u>array</u>. It's advisable to familiarize yourself with these for better comprehension of technical texts.
|
||||
@@ -20,7 +20,7 @@ comments: true
|
||||
=== "Python"
|
||||
|
||||
```python title=""
|
||||
"""Header comments for labeling functions, classes, test samples, etc""""
|
||||
"""Header comments for labeling functions, classes, test samples, etc"""
|
||||
|
||||
# Comments for explaining details
|
||||
|
||||
@@ -184,17 +184,17 @@ comments: true
|
||||
// comments
|
||||
```
|
||||
|
||||
## 0.2.2 Efficient Learning via Animated Illustrations
|
||||
## 0.2.2 Efficient learning via animated illustrations
|
||||
|
||||
Compared with text, videos and pictures have a higher density of information and are more structured, making them easier to understand. In this book, **key and difficult concepts are mainly presented through animations and illustrations**, with text serving as explanations and supplements.
|
||||
|
||||
When encountering content with animations or illustrations as shown in the Figure 0-2 , **prioritize understanding the figure, with text as supplementary**, integrating both for a comprehensive understanding.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 0-2 Animated Illustration Example </p>
|
||||
<p align="center"> Figure 0-2 Animated illustration example </p>
|
||||
|
||||
## 0.2.3 Deepen Understanding through Coding Practice
|
||||
## 0.2.3 Deepen understanding through coding practice
|
||||
|
||||
The source code of this book is hosted on the [GitHub Repository](https://github.com/krahets/hello-algo). As shown in the Figure 0-3 , **the source code comes with test examples and can be executed with just a single click**.
|
||||
|
||||
@@ -202,9 +202,9 @@ If time permits, **it's recommended to type out the code yourself**. If pressed
|
||||
|
||||
Compared to just reading code, writing code often yields more learning. **Learning by doing is the real way to learn.**
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 0-3 Running Code Example </p>
|
||||
<p align="center"> Figure 0-3 Running code example </p>
|
||||
|
||||
Setting up to run the code involves three main steps.
|
||||
|
||||
@@ -220,27 +220,27 @@ git clone https://github.com/krahets/hello-algo.git
|
||||
|
||||
Alternatively, you can also click the "Download ZIP" button at the location shown in the Figure 0-4 to directly download the code as a compressed ZIP file. Then, you can simply extract it locally.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 0-4 Cloning Repository and Downloading Code </p>
|
||||
<p align="center"> Figure 0-4 Cloning repository and downloading code </p>
|
||||
|
||||
**Step 3: Run the source code**. As shown in the Figure 0-5 , for the code block labeled with the file name at the top, we can find the corresponding source code file in the `codes` folder of the repository. These files can be executed with a single click, which will help you save unnecessary debugging time and allow you to focus on learning.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 0-5 Code Block and Corresponding Source Code File </p>
|
||||
<p align="center"> Figure 0-5 Code block and corresponding source code file </p>
|
||||
|
||||
## 0.2.4 Learning Together in Discussion
|
||||
## 0.2.4 Learning together in discussion
|
||||
|
||||
While reading this book, please don't skip over the points that you didn't learn. **Feel free to post your questions in the comment section**. We will be happy to answer them and can usually respond within two days.
|
||||
|
||||
As illustrated in the Figure 0-6 , each chapter features a comment section at the bottom. I encourage you to pay attention to these comments. They not only expose you to others' encountered problems, aiding in identifying knowledge gaps and sparking deeper contemplation, but also invite you to generously contribute by answering fellow readers' inquiries, sharing insights, and fostering mutual improvement.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 0-6 Comment Section Example </p>
|
||||
<p align="center"> Figure 0-6 Comment section example </p>
|
||||
|
||||
## 0.2.5 Algorithm Learning Path
|
||||
## 0.2.5 Algorithm learning path
|
||||
|
||||
Overall, the journey of mastering data structures and algorithms can be divided into three stages:
|
||||
|
||||
@@ -250,6 +250,6 @@ Overall, the journey of mastering data structures and algorithms can be divided
|
||||
|
||||
As shown in the Figure 0-7 , this book mainly covers “Stage 1,” aiming to help you more efficiently embark on Stages 2 and 3.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 0-7 Algorithm Learning Path </p>
|
||||
<p align="center"> Figure 0-7 Algorithm learning path </p>
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 5.3 Double-Ended Queue
|
||||
# 5.3 Double-ended queue
|
||||
|
||||
In a queue, we can only delete elements from the head or add elements to the tail. As shown in the following diagram, a "double-ended queue (deque)" offers more flexibility, allowing the addition or removal of elements at both the head and the tail.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 5-7 Operations in Double-Ended Queue </p>
|
||||
<p align="center"> Figure 5-7 Operations in double-ended queue </p>
|
||||
|
||||
## 5.3.1 Common Operations in Double-Ended Queue
|
||||
## 5.3.1 Common operations in double-ended queue
|
||||
|
||||
The common operations in a double-ended queue are listed below, and the names of specific methods depend on the programming language used.
|
||||
|
||||
<p align="center"> Table 5-3 Efficiency of Double-Ended Queue Operations </p>
|
||||
<p align="center"> Table 5-3 Efficiency of double-ended queue operations </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
@@ -350,11 +350,11 @@ Similarly, we can directly use the double-ended queue classes implemented in pro
|
||||
|
||||
https://pythontutor.com/render.html#code=from%20collections%20import%20deque%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%0A%20%20%20%20deq%20%3D%20deque%28%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E9%98%9F%0A%20%20%20%20deq.append%282%29%20%20%23%20%E6%B7%BB%E5%8A%A0%E8%87%B3%E9%98%9F%E5%B0%BE%0A%20%20%20%20deq.append%285%29%0A%20%20%20%20deq.append%284%29%0A%20%20%20%20deq.appendleft%283%29%20%20%23%20%E6%B7%BB%E5%8A%A0%E8%87%B3%E9%98%9F%E9%A6%96%0A%20%20%20%20deq.appendleft%281%29%0A%20%20%20%20print%28%22%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%20deque%20%3D%22,%20deq%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E5%85%83%E7%B4%A0%0A%20%20%20%20front%20%3D%20deq%5B0%5D%20%20%23%20%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%20front%20%3D%22,%20front%29%0A%20%20%20%20rear%20%3D%20deq%5B-1%5D%20%20%23%20%E9%98%9F%E5%B0%BE%E5%85%83%E7%B4%A0%0A%20%20%20%20print%28%22%E9%98%9F%E5%B0%BE%E5%85%83%E7%B4%A0%20rear%20%3D%22,%20rear%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20pop_front%20%3D%20deq.popleft%28%29%20%20%23%20%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%87%BA%E9%98%9F%E5%85%83%E7%B4%A0%20%20pop_front%20%3D%22,%20pop_front%29%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%87%BA%E9%98%9F%E5%90%8E%20deque%20%3D%22,%20deq%29%0A%20%20%20%20pop_rear%20%3D%20deq.pop%28%29%20%20%23%20%E9%98%9F%E5%B0%BE%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20print%28%22%E9%98%9F%E5%B0%BE%E5%87%BA%E9%98%9F%E5%85%83%E7%B4%A0%20%20pop_rear%20%3D%22,%20pop_rear%29%0A%20%20%20%20print%28%22%E9%98%9F%E5%B0%BE%E5%87%BA%E9%98%9F%E5%90%8E%20deque%20%3D%22,%20deq%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28deq%29%0A%20%20%20%20print%28%22%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20len%28deq%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%20is_empty%29&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
## 5.3.2 Implementing a Double-Ended Queue *
|
||||
## 5.3.2 Implementing a double-ended queue *
|
||||
|
||||
The implementation of a double-ended queue is similar to that of a regular queue, it can be based on either a linked list or an array as the underlying data structure.
|
||||
|
||||
### 1. Implementation Based on Doubly Linked List
|
||||
### 1. Implementation based on doubly linked list
|
||||
|
||||
Recall from the previous section that we used a regular singly linked list to implement a queue, as it conveniently allows for deleting from the head (corresponding to the dequeue operation) and adding new elements after the tail (corresponding to the enqueue operation).
|
||||
|
||||
@@ -2136,7 +2136,7 @@ The implementation code is as follows:
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Implementation Based on Array
|
||||
### 2. Implementation based on array
|
||||
|
||||
As shown in the Figure 5-9 , similar to implementing a queue with an array, we can also use a circular array to implement a double-ended queue.
|
||||
|
||||
@@ -3470,7 +3470,7 @@ The implementation only needs to add methods for "front enqueue" and "rear deque
|
||||
[class]{ArrayDeque}-[func]{}
|
||||
```
|
||||
|
||||
## 5.3.3 Applications of Double-Ended Queue
|
||||
## 5.3.3 Applications of double-ended queue
|
||||
|
||||
The double-ended queue combines the logic of both stacks and queues, **thus, it can implement all their respective use cases while offering greater flexibility**.
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ comments: true
|
||||
icon: material/stack-overflow
|
||||
---
|
||||
|
||||
# Chapter 5. Stack and Queue
|
||||
# Chapter 5. Stack and queue
|
||||
|
||||
{ class="cover-image" }
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
@@ -17,5 +17,5 @@ icon: material/stack-overflow
|
||||
|
||||
- [5.1 Stack](https://www.hello-algo.com/en/chapter_stack_and_queue/stack/)
|
||||
- [5.2 Queue](https://www.hello-algo.com/en/chapter_stack_and_queue/queue/)
|
||||
- [5.3 Double-ended Queue](https://www.hello-algo.com/en/chapter_stack_and_queue/deque/)
|
||||
- [5.3 Double-ended queue](https://www.hello-algo.com/en/chapter_stack_and_queue/deque/)
|
||||
- [5.4 Summary](https://www.hello-algo.com/en/chapter_stack_and_queue/summary/)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -10,15 +10,15 @@ We can compare a stack to a pile of plates on a table. To access the bottom plat
|
||||
|
||||
As shown in the Figure 5-1 , we refer to the top of the pile of elements as the "top of the stack" and the bottom as the "bottom of the stack." The operation of adding elements to the top of the stack is called "push," and the operation of removing the top element is called "pop."
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 5-1 Stack's Last-In-First-Out Rule </p>
|
||||
<p align="center"> Figure 5-1 Stack's last-in-first-out rule </p>
|
||||
|
||||
## 5.1.1 Common Operations on Stack
|
||||
## 5.1.1 Common operations on stack
|
||||
|
||||
The common operations on a stack are shown in the Table 5-1 . The specific method names depend on the programming language used. Here, we use `push()`, `pop()`, and `peek()` as examples.
|
||||
|
||||
<p align="center"> Table 5-1 Efficiency of Stack Operations </p>
|
||||
<p align="center"> Table 5-1 Efficiency of stack operations </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
@@ -323,13 +323,13 @@ Typically, we can directly use the stack class built into the programming langua
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%A0%88%0A%20%20%20%20%23%20Python%20%E6%B2%A1%E6%9C%89%E5%86%85%E7%BD%AE%E7%9A%84%E6%A0%88%E7%B1%BB%EF%BC%8C%E5%8F%AF%E4%BB%A5%E6%8A%8A%20list%20%E5%BD%93%E4%BD%9C%E6%A0%88%E6%9D%A5%E4%BD%BF%E7%94%A8%0A%20%20%20%20stack%20%3D%20%5B%5D%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E6%A0%88%0A%20%20%20%20stack.append%281%29%0A%20%20%20%20stack.append%283%29%0A%20%20%20%20stack.append%282%29%0A%20%20%20%20stack.append%285%29%0A%20%20%20%20stack.append%284%29%0A%20%20%20%20print%28%22%E6%A0%88%20stack%20%3D%22,%20stack%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%0A%20%20%20%20peek%20%3D%20stack%5B-1%5D%0A%20%20%20%20print%28%22%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%20peek%20%3D%22,%20peek%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E6%A0%88%0A%20%20%20%20pop%20%3D%20stack.pop%28%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%85%83%E7%B4%A0%20pop%20%3D%22,%20pop%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%90%8E%20stack%20%3D%22,%20stack%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28stack%29%0A%20%20%20%20print%28%22%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20len%28stack%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E6%A0%88%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%20is_empty%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=2&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%A0%88%0A%20%20%20%20%23%20Python%20%E6%B2%A1%E6%9C%89%E5%86%85%E7%BD%AE%E7%9A%84%E6%A0%88%E7%B1%BB%EF%BC%8C%E5%8F%AF%E4%BB%A5%E6%8A%8A%20list%20%E5%BD%93%E4%BD%9C%E6%A0%88%E6%9D%A5%E4%BD%BF%E7%94%A8%0A%20%20%20%20stack%20%3D%20%5B%5D%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E6%A0%88%0A%20%20%20%20stack.append%281%29%0A%20%20%20%20stack.append%283%29%0A%20%20%20%20stack.append%282%29%0A%20%20%20%20stack.append%285%29%0A%20%20%20%20stack.append%284%29%0A%20%20%20%20print%28%22%E6%A0%88%20stack%20%3D%22,%20stack%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%0A%20%20%20%20peek%20%3D%20stack%5B-1%5D%0A%20%20%20%20print%28%22%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%20peek%20%3D%22,%20peek%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E6%A0%88%0A%20%20%20%20pop%20%3D%20stack.pop%28%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%85%83%E7%B4%A0%20pop%20%3D%22,%20pop%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%90%8E%20stack%20%3D%22,%20stack%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28stack%29%0A%20%20%20%20print%28%22%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20len%28stack%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E6%A0%88%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%20is_empty%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=2&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Full Screen ></a></div>
|
||||
|
||||
## 5.1.2 Implementing a Stack
|
||||
## 5.1.2 Implementing a stack
|
||||
|
||||
To gain a deeper understanding of how a stack operates, let's try implementing a stack class ourselves.
|
||||
|
||||
A stack follows the principle of Last-In-First-Out, which means we can only add or remove elements at the top of the stack. However, both arrays and linked lists allow adding and removing elements at any position, **therefore a stack can be seen as a restricted array or linked list**. In other words, we can "shield" certain irrelevant operations of an array or linked list, aligning their external behavior with the characteristics of a stack.
|
||||
|
||||
### 1. Implementation Based on Linked List
|
||||
### 1. Implementation based on a linked list
|
||||
|
||||
When implementing a stack using a linked list, we can consider the head node of the list as the top of the stack and the tail node as the bottom of the stack.
|
||||
|
||||
@@ -1157,7 +1157,7 @@ Below is an example code for implementing a stack based on a linked list:
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%0Aclass%20LinkedListStack%3A%0A%20%20%20%20%22%22%22%E5%9F%BA%E4%BA%8E%E9%93%BE%E8%A1%A8%E5%AE%9E%E7%8E%B0%E7%9A%84%E6%A0%88%22%22%22%0A%0A%20%20%20%20def%20__init__%28self%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95%22%22%22%0A%20%20%20%20%20%20%20%20self._peek%3A%20ListNode%20%7C%20None%20%3D%20None%0A%20%20%20%20%20%20%20%20self._size%3A%20int%20%3D%200%0A%0A%20%20%20%20def%20size%28self%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%8E%B7%E5%8F%96%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%22%22%22%0A%20%20%20%20%20%20%20%20return%20self._size%0A%0A%20%20%20%20def%20is_empty%28self%29%20-%3E%20bool%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E5%88%A4%E6%96%AD%E6%A0%88%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%22%22%22%0A%20%20%20%20%20%20%20%20return%20not%20self._peek%0A%0A%20%20%20%20def%20push%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E5%85%A5%E6%A0%88%22%22%22%0A%20%20%20%20%20%20%20%20node%20%3D%20ListNode%28val%29%0A%20%20%20%20%20%20%20%20node.next%20%3D%20self._peek%0A%20%20%20%20%20%20%20%20self._peek%20%3D%20node%0A%20%20%20%20%20%20%20%20self._size%20%2B%3D%201%0A%0A%20%20%20%20def%20pop%28self%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E5%87%BA%E6%A0%88%22%22%22%0A%20%20%20%20%20%20%20%20num%20%3D%20self.peek%28%29%0A%20%20%20%20%20%20%20%20self._peek%20%3D%20self._peek.next%0A%20%20%20%20%20%20%20%20self._size%20-%3D%201%0A%20%20%20%20%20%20%20%20return%20num%0A%0A%20%20%20%20def%20peek%28self%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%AE%BF%E9%97%AE%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%22%22%22%0A%20%20%20%20%20%20%20%20if%20self.is_empty%28%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20raise%20IndexError%28%22%E6%A0%88%E4%B8%BA%E7%A9%BA%22%29%0A%20%20%20%20%20%20%20%20return%20self._peek.val%0A%0A%20%20%20%20def%20to_list%28self%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%BD%AC%E5%8C%96%E4%B8%BA%E5%88%97%E8%A1%A8%E7%94%A8%E4%BA%8E%E6%89%93%E5%8D%B0%22%22%22%0A%20%20%20%20%20%20%20%20arr%20%3D%20%5B%5D%0A%20%20%20%20%20%20%20%20node%20%3D%20self._peek%0A%20%20%20%20%20%20%20%20while%20node%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20arr.append%28node.val%29%0A%20%20%20%20%20%20%20%20%20%20%20%20node%20%3D%20node.next%0A%20%20%20%20%20%20%20%20arr.reverse%28%29%0A%20%20%20%20%20%20%20%20return%20arr%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%A0%88%0A%20%20%20%20stack%20%3D%20LinkedListStack%28%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E6%A0%88%0A%20%20%20%20stack.push%281%29%0A%20%20%20%20stack.push%283%29%0A%20%20%20%20stack.push%282%29%0A%20%20%20%20stack.push%285%29%0A%20%20%20%20stack.push%284%29%0A%20%20%20%20print%28%22%E6%A0%88%20stack%20%3D%22,%20stack.to_list%28%29%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%0A%20%20%20%20peek%20%3D%20stack.peek%28%29%0A%20%20%20%20print%28%22%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%20peek%20%3D%22,%20peek%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E6%A0%88%0A%20%20%20%20pop%20%3D%20stack.pop%28%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%85%83%E7%B4%A0%20pop%20%3D%22,%20pop%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%90%8E%20stack%20%3D%22,%20stack.to_list%28%29%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20stack.size%28%29%0A%20%20%20%20print%28%22%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20stack.is_empty%28%29%0A%20%20%20%20print%28%22%E6%A0%88%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%20is_empty%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%0Aclass%20LinkedListStack%3A%0A%20%20%20%20%22%22%22%E5%9F%BA%E4%BA%8E%E9%93%BE%E8%A1%A8%E5%AE%9E%E7%8E%B0%E7%9A%84%E6%A0%88%22%22%22%0A%0A%20%20%20%20def%20__init__%28self%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95%22%22%22%0A%20%20%20%20%20%20%20%20self._peek%3A%20ListNode%20%7C%20None%20%3D%20None%0A%20%20%20%20%20%20%20%20self._size%3A%20int%20%3D%200%0A%0A%20%20%20%20def%20size%28self%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%8E%B7%E5%8F%96%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%22%22%22%0A%20%20%20%20%20%20%20%20return%20self._size%0A%0A%20%20%20%20def%20is_empty%28self%29%20-%3E%20bool%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E5%88%A4%E6%96%AD%E6%A0%88%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%22%22%22%0A%20%20%20%20%20%20%20%20return%20not%20self._peek%0A%0A%20%20%20%20def%20push%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E5%85%A5%E6%A0%88%22%22%22%0A%20%20%20%20%20%20%20%20node%20%3D%20ListNode%28val%29%0A%20%20%20%20%20%20%20%20node.next%20%3D%20self._peek%0A%20%20%20%20%20%20%20%20self._peek%20%3D%20node%0A%20%20%20%20%20%20%20%20self._size%20%2B%3D%201%0A%0A%20%20%20%20def%20pop%28self%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E5%87%BA%E6%A0%88%22%22%22%0A%20%20%20%20%20%20%20%20num%20%3D%20self.peek%28%29%0A%20%20%20%20%20%20%20%20self._peek%20%3D%20self._peek.next%0A%20%20%20%20%20%20%20%20self._size%20-%3D%201%0A%20%20%20%20%20%20%20%20return%20num%0A%0A%20%20%20%20def%20peek%28self%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%AE%BF%E9%97%AE%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%22%22%22%0A%20%20%20%20%20%20%20%20if%20self.is_empty%28%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20raise%20IndexError%28%22%E6%A0%88%E4%B8%BA%E7%A9%BA%22%29%0A%20%20%20%20%20%20%20%20return%20self._peek.val%0A%0A%20%20%20%20def%20to_list%28self%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%BD%AC%E5%8C%96%E4%B8%BA%E5%88%97%E8%A1%A8%E7%94%A8%E4%BA%8E%E6%89%93%E5%8D%B0%22%22%22%0A%20%20%20%20%20%20%20%20arr%20%3D%20%5B%5D%0A%20%20%20%20%20%20%20%20node%20%3D%20self._peek%0A%20%20%20%20%20%20%20%20while%20node%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20arr.append%28node.val%29%0A%20%20%20%20%20%20%20%20%20%20%20%20node%20%3D%20node.next%0A%20%20%20%20%20%20%20%20arr.reverse%28%29%0A%20%20%20%20%20%20%20%20return%20arr%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%A0%88%0A%20%20%20%20stack%20%3D%20LinkedListStack%28%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E6%A0%88%0A%20%20%20%20stack.push%281%29%0A%20%20%20%20stack.push%283%29%0A%20%20%20%20stack.push%282%29%0A%20%20%20%20stack.push%285%29%0A%20%20%20%20stack.push%284%29%0A%20%20%20%20print%28%22%E6%A0%88%20stack%20%3D%22,%20stack.to_list%28%29%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%0A%20%20%20%20peek%20%3D%20stack.peek%28%29%0A%20%20%20%20print%28%22%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%20peek%20%3D%22,%20peek%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E6%A0%88%0A%20%20%20%20pop%20%3D%20stack.pop%28%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%85%83%E7%B4%A0%20pop%20%3D%22,%20pop%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%90%8E%20stack%20%3D%22,%20stack.to_list%28%29%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20stack.size%28%29%0A%20%20%20%20print%28%22%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20stack.is_empty%28%29%0A%20%20%20%20print%28%22%E6%A0%88%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%20is_empty%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Full Screen ></a></div>
|
||||
|
||||
### 2. Implementation Based on Array
|
||||
### 2. Implementation based on an array
|
||||
|
||||
When implementing a stack using an array, we can consider the end of the array as the top of the stack. As shown in the Figure 5-3 , push and pop operations correspond to adding and removing elements at the end of the array, respectively, both with a time complexity of $O(1)$.
|
||||
|
||||
@@ -1817,7 +1817,7 @@ Since the elements to be pushed onto the stack may continuously increase, we can
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20ArrayStack%3A%0A%20%20%20%20%22%22%22%E5%9F%BA%E4%BA%8E%E6%95%B0%E7%BB%84%E5%AE%9E%E7%8E%B0%E7%9A%84%E6%A0%88%22%22%22%0A%0A%20%20%20%20def%20__init__%28self%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95%22%22%22%0A%20%20%20%20%20%20%20%20self._stack%3A%20list%5Bint%5D%20%3D%20%5B%5D%0A%0A%20%20%20%20def%20size%28self%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%8E%B7%E5%8F%96%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%22%22%22%0A%20%20%20%20%20%20%20%20return%20len%28self._stack%29%0A%0A%20%20%20%20def%20is_empty%28self%29%20-%3E%20bool%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E5%88%A4%E6%96%AD%E6%A0%88%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%22%22%22%0A%20%20%20%20%20%20%20%20return%20self._stack%20%3D%3D%20%5B%5D%0A%0A%20%20%20%20def%20push%28self,%20item%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E5%85%A5%E6%A0%88%22%22%22%0A%20%20%20%20%20%20%20%20self._stack.append%28item%29%0A%0A%20%20%20%20def%20pop%28self%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E5%87%BA%E6%A0%88%22%22%22%0A%20%20%20%20%20%20%20%20if%20self.is_empty%28%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20raise%20IndexError%28%22%E6%A0%88%E4%B8%BA%E7%A9%BA%22%29%0A%20%20%20%20%20%20%20%20return%20self._stack.pop%28%29%0A%0A%20%20%20%20def%20peek%28self%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%AE%BF%E9%97%AE%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%22%22%22%0A%20%20%20%20%20%20%20%20if%20self.is_empty%28%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20raise%20IndexError%28%22%E6%A0%88%E4%B8%BA%E7%A9%BA%22%29%0A%20%20%20%20%20%20%20%20return%20self._stack%5B-1%5D%0A%0A%20%20%20%20def%20to_list%28self%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%BF%94%E5%9B%9E%E5%88%97%E8%A1%A8%E7%94%A8%E4%BA%8E%E6%89%93%E5%8D%B0%22%22%22%0A%20%20%20%20%20%20%20%20return%20self._stack%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%A0%88%0A%20%20%20%20stack%20%3D%20ArrayStack%28%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E6%A0%88%0A%20%20%20%20stack.push%281%29%0A%20%20%20%20stack.push%283%29%0A%20%20%20%20stack.push%282%29%0A%20%20%20%20stack.push%285%29%0A%20%20%20%20stack.push%284%29%0A%20%20%20%20print%28%22%E6%A0%88%20stack%20%3D%22,%20stack.to_list%28%29%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%0A%20%20%20%20peek%20%3D%20stack.peek%28%29%0A%20%20%20%20print%28%22%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%20peek%20%3D%22,%20peek%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E6%A0%88%0A%20%20%20%20pop%20%3D%20stack.pop%28%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%85%83%E7%B4%A0%20pop%20%3D%22,%20pop%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%90%8E%20stack%20%3D%22,%20stack.to_list%28%29%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20stack.size%28%29%0A%20%20%20%20print%28%22%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20stack.is_empty%28%29%0A%20%20%20%20print%28%22%E6%A0%88%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%20is_empty%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20ArrayStack%3A%0A%20%20%20%20%22%22%22%E5%9F%BA%E4%BA%8E%E6%95%B0%E7%BB%84%E5%AE%9E%E7%8E%B0%E7%9A%84%E6%A0%88%22%22%22%0A%0A%20%20%20%20def%20__init__%28self%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95%22%22%22%0A%20%20%20%20%20%20%20%20self._stack%3A%20list%5Bint%5D%20%3D%20%5B%5D%0A%0A%20%20%20%20def%20size%28self%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%8E%B7%E5%8F%96%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%22%22%22%0A%20%20%20%20%20%20%20%20return%20len%28self._stack%29%0A%0A%20%20%20%20def%20is_empty%28self%29%20-%3E%20bool%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E5%88%A4%E6%96%AD%E6%A0%88%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%22%22%22%0A%20%20%20%20%20%20%20%20return%20self._stack%20%3D%3D%20%5B%5D%0A%0A%20%20%20%20def%20push%28self,%20item%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E5%85%A5%E6%A0%88%22%22%22%0A%20%20%20%20%20%20%20%20self._stack.append%28item%29%0A%0A%20%20%20%20def%20pop%28self%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E5%87%BA%E6%A0%88%22%22%22%0A%20%20%20%20%20%20%20%20if%20self.is_empty%28%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20raise%20IndexError%28%22%E6%A0%88%E4%B8%BA%E7%A9%BA%22%29%0A%20%20%20%20%20%20%20%20return%20self._stack.pop%28%29%0A%0A%20%20%20%20def%20peek%28self%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%AE%BF%E9%97%AE%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%22%22%22%0A%20%20%20%20%20%20%20%20if%20self.is_empty%28%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20raise%20IndexError%28%22%E6%A0%88%E4%B8%BA%E7%A9%BA%22%29%0A%20%20%20%20%20%20%20%20return%20self._stack%5B-1%5D%0A%0A%20%20%20%20def%20to_list%28self%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%BF%94%E5%9B%9E%E5%88%97%E8%A1%A8%E7%94%A8%E4%BA%8E%E6%89%93%E5%8D%B0%22%22%22%0A%20%20%20%20%20%20%20%20return%20self._stack%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%A0%88%0A%20%20%20%20stack%20%3D%20ArrayStack%28%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E6%A0%88%0A%20%20%20%20stack.push%281%29%0A%20%20%20%20stack.push%283%29%0A%20%20%20%20stack.push%282%29%0A%20%20%20%20stack.push%285%29%0A%20%20%20%20stack.push%284%29%0A%20%20%20%20print%28%22%E6%A0%88%20stack%20%3D%22,%20stack.to_list%28%29%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%0A%20%20%20%20peek%20%3D%20stack.peek%28%29%0A%20%20%20%20print%28%22%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%20peek%20%3D%22,%20peek%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E6%A0%88%0A%20%20%20%20pop%20%3D%20stack.pop%28%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%85%83%E7%B4%A0%20pop%20%3D%22,%20pop%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%90%8E%20stack%20%3D%22,%20stack.to_list%28%29%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20stack.size%28%29%0A%20%20%20%20print%28%22%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20stack.is_empty%28%29%0A%20%20%20%20print%28%22%E6%A0%88%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%20is_empty%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Full Screen ></a></div>
|
||||
|
||||
## 5.1.3 Comparison of the Two Implementations
|
||||
## 5.1.3 Comparison of the two implementations
|
||||
|
||||
**Supported Operations**
|
||||
|
||||
@@ -1842,7 +1842,7 @@ However, since linked list nodes require extra space for storing pointers, **the
|
||||
|
||||
In summary, we cannot simply determine which implementation is more memory-efficient. It requires analysis based on specific circumstances.
|
||||
|
||||
## 5.1.4 Typical Applications of Stack
|
||||
## 5.1.4 Typical applications of stack
|
||||
|
||||
- **Back and forward in browsers, undo and redo in software**. Every time we open a new webpage, the browser pushes the previous page onto the stack, allowing us to go back to the previous page through the back operation, which is essentially a pop operation. To support both back and forward, two stacks are needed to work together.
|
||||
- **Memory management in programs**. Each time a function is called, the system adds a stack frame at the top of the stack to record the function's context information. In recursive functions, the downward recursion phase keeps pushing onto the stack, while the upward backtracking phase keeps popping from the stack.
|
||||
|
||||
@@ -4,7 +4,7 @@ comments: true
|
||||
|
||||
# 5.4 Summary
|
||||
|
||||
### 1. Key Review
|
||||
### 1. Key review
|
||||
|
||||
- Stack is a data structure that follows the Last-In-First-Out (LIFO) principle and can be implemented using arrays or linked lists.
|
||||
- In terms of time efficiency, the array implementation of the stack has a higher average efficiency. However, during expansion, the time complexity for a single push operation can degrade to $O(n)$. In contrast, the linked list implementation of a stack offers more stable efficiency.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% if config.theme.language == 'zh' %}
|
||||
{% set comm = "欢迎在评论区留下你的见解、问题或建议" %}
|
||||
{% set lang = "zh-CN" %}
|
||||
{% elif config.theme.language == 'zh-Hant' %}
|
||||
{% elif config.theme.language == 'zh-hant' %}
|
||||
{% set comm = "歡迎在評論區留下你的見解、問題或建議" %}
|
||||
{% set lang = "zh-TW" %}
|
||||
{% elif config.theme.language == 'en' %}
|
||||
|
||||
53
zh-Hant/docs/chapter_appendix/contribution.md
Normal file
53
zh-Hant/docs/chapter_appendix/contribution.md
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 16.2 一起參與創作
|
||||
|
||||
由於筆者能力有限,書中難免存在一些遺漏和錯誤,請您諒解。如果您發現了筆誤、連結失效、內容缺失、文字歧義、解釋不清晰或行文結構不合理等問題,請協助我們進行修正,以給讀者提供更優質的學習資源。
|
||||
|
||||
所有[撰稿人](https://github.com/krahets/hello-algo/graphs/contributors)的 GitHub ID 將在本書倉庫、網頁版和 PDF 版的主頁上進行展示,以感謝他們對開源社群的無私奉獻。
|
||||
|
||||
!!! success "開源的魅力"
|
||||
|
||||
紙質圖書的兩次印刷的間隔時間往往較久,內容更新非常不方便。
|
||||
|
||||
而在本開源書中,內容更迭的時間被縮短至數日甚至幾個小時。
|
||||
|
||||
### 1. 內容微調
|
||||
|
||||
如圖 16-3 所示,每個頁面的右上角都有“編輯圖示”。您可以按照以下步驟修改文字或程式碼。
|
||||
|
||||
1. 點選“編輯圖示”,如果遇到“需要 Fork 此倉庫”的提示,請同意該操作。
|
||||
2. 修改 Markdown 源檔案內容,檢查內容的正確性,並儘量保持排版格式的統一。
|
||||
3. 在頁面底部填寫修改說明,然後點選“Propose file change”按鈕。頁面跳轉後,點選“Create pull request”按鈕即可發起拉取請求。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 16-3 頁面編輯按鍵 </p>
|
||||
|
||||
圖片無法直接修改,需要透過新建 [Issue](https://github.com/krahets/hello-algo/issues) 或評論留言來描述問題,我們會盡快重新繪製並替換圖片。
|
||||
|
||||
### 2. 內容創作
|
||||
|
||||
如果您有興趣參與此開源專案,包括將程式碼翻譯成其他程式語言、擴展文章內容等,那麼需要實施以下 Pull Request 工作流程。
|
||||
|
||||
1. 登入 GitHub ,將本書的[程式碼倉庫](https://github.com/krahets/hello-algo) Fork 到個人帳號下。
|
||||
2. 進入您的 Fork 倉庫網頁,使用 `git clone` 命令將倉庫克隆至本地。
|
||||
3. 在本地進行內容創作,並進行完整測試,驗證程式碼的正確性。
|
||||
4. 將本地所做更改 Commit ,然後 Push 至遠端倉庫。
|
||||
5. 重新整理倉庫網頁,點選“Create pull request”按鈕即可發起拉取請求。
|
||||
|
||||
### 3. Docker 部署
|
||||
|
||||
在 `hello-algo` 根目錄下,執行以下 Docker 指令碼,即可在 `http://localhost:8000` 訪問本專案:
|
||||
|
||||
```shell
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
使用以下命令即可刪除部署:
|
||||
|
||||
```shell
|
||||
docker-compose down
|
||||
```
|
||||
14
zh-Hant/docs/chapter_appendix/index.md
Normal file
14
zh-Hant/docs/chapter_appendix/index.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/help-circle-outline
|
||||
---
|
||||
|
||||
# 第 16 章 附錄
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
## Chapter Contents
|
||||
|
||||
- [16.1 程式設計環境安裝](https://www.hello-algo.com/en/chapter_appendix/installation/)
|
||||
- [16.2 一起參與創作](https://www.hello-algo.com/en/chapter_appendix/contribution/)
|
||||
- [16.3 術語表](https://www.hello-algo.com/en/chapter_appendix/terminology/)
|
||||
71
zh-Hant/docs/chapter_appendix/installation.md
Normal file
71
zh-Hant/docs/chapter_appendix/installation.md
Normal file
@@ -0,0 +1,71 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 16.1 程式設計環境安裝
|
||||
|
||||
## 16.1.1 安裝 IDE
|
||||
|
||||
推薦使用開源、輕量的 VS Code 作為本地整合開發環境(IDE)。訪問 [VS Code 官網](https://code.visualstudio.com/),根據作業系統選擇相應版本的 VS Code 進行下載和安裝。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 16-1 從官網下載 VS Code </p>
|
||||
|
||||
VS Code 擁有強大的擴展包生態系統,支持大多數程式語言的執行和除錯。以 Python 為例,安裝“Python Extension Pack”擴展包之後,即可進行 Python 程式碼除錯。安裝步驟如圖 16-2 所示。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 16-2 安裝 VS Code 擴展包 </p>
|
||||
|
||||
## 16.1.2 安裝語言環境
|
||||
|
||||
### 1. Python 環境
|
||||
|
||||
1. 下載並安裝 [Miniconda3](https://docs.conda.io/en/latest/miniconda.html) ,需要 Python 3.10 或更新版本。
|
||||
2. 在 VS Code 的擴充功能市場中搜索 `python` ,安裝 Python Extension Pack 。
|
||||
3. (可選)在命令列輸入 `pip install black` ,安裝程式碼格式化工具。
|
||||
|
||||
### 2. C/C++ 環境
|
||||
|
||||
1. Windows 系統需要安裝 [MinGW](https://sourceforge.net/projects/mingw-w64/files/)([配置教程](https://blog.csdn.net/qq_33698226/article/details/129031241));MacOS 自帶 Clang ,無須安裝。
|
||||
2. 在 VS Code 的擴充功能市場中搜索 `c++` ,安裝 C/C++ Extension Pack 。
|
||||
3. (可選)開啟 Settings 頁面,搜尋 `Clang_format_fallback Style` 程式碼格式化選項,設定為 `{ BasedOnStyle: Microsoft, BreakBeforeBraces: Attach }` 。
|
||||
|
||||
### 3. Java 環境
|
||||
|
||||
1. 下載並安裝 [OpenJDK](https://jdk.java.net/18/)(版本需滿足 > JDK 9)。
|
||||
2. 在 VS Code 的擴充功能市場中搜索 `java` ,安裝 Extension Pack for Java 。
|
||||
|
||||
### 4. C# 環境
|
||||
|
||||
1. 下載並安裝 [.Net 8.0](https://dotnet.microsoft.com/en-us/download) 。
|
||||
2. 在 VS Code 的擴充功能市場中搜索 `C# Dev Kit` ,安裝 C# Dev Kit ([配置教程](https://code.visualstudio.com/docs/csharp/get-started))。
|
||||
3. 也可使用 Visual Studio([安裝教程](https://learn.microsoft.com/zh-cn/visualstudio/install/install-visual-studio?view=vs-2022))。
|
||||
|
||||
### 5. Go 環境
|
||||
|
||||
1. 下載並安裝 [go](https://go.dev/dl/) 。
|
||||
2. 在 VS Code 的擴充功能市場中搜索 `go` ,安裝 Go 。
|
||||
3. 按快捷鍵 `Ctrl + Shift + P` 撥出命令欄,輸入 go ,選擇 `Go: Install/Update Tools` ,全部勾選並安裝即可。
|
||||
|
||||
### 6. Swift 環境
|
||||
|
||||
1. 下載並安裝 [Swift](https://www.swift.org/download/) 。
|
||||
2. 在 VS Code 的擴充功能市場中搜索 `swift` ,安裝 [Swift for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=sswg.swift-lang) 。
|
||||
|
||||
### 7. JavaScript 環境
|
||||
|
||||
1. 下載並安裝 [node.js](https://nodejs.org/en/) 。
|
||||
2. 在 VS Code 的擴充功能市場中搜索 `javascript` ,安裝 JavaScript (ES6) code snippets 。
|
||||
3. (可選)在 VS Code 的擴充功能市場中搜索 `Prettier` ,安裝程式碼格式化工具。
|
||||
|
||||
### 8. Dart 環境
|
||||
|
||||
1. 下載並安裝 [Dart](https://dart.dev/get-dart) 。
|
||||
2. 在 VS Code 的擴充功能市場中搜索 `dart` ,安裝 [Dart](https://marketplace.visualstudio.com/items?itemName=Dart-Code.dart-code) 。
|
||||
|
||||
### 9. Rust 環境
|
||||
|
||||
1. 下載並安裝 [Rust](https://www.rust-lang.org/tools/install) 。
|
||||
2. 在 VS Code 的擴充功能市場中搜索 `rust` ,安裝 [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) 。
|
||||
144
zh-Hant/docs/chapter_appendix/terminology.md
Normal file
144
zh-Hant/docs/chapter_appendix/terminology.md
Normal file
@@ -0,0 +1,144 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 16.3 術語表
|
||||
|
||||
表 16-1 列出了書中出現的重要術語,值得注意以下幾點。
|
||||
|
||||
- 建議記住名詞的英文叫法,以便閱讀英文文獻。
|
||||
- 部分名詞在簡體中文和繁體中文下的叫法不同。
|
||||
|
||||
<p align="center"> 表 16-1 資料結構與演算法的重要名詞 </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
| 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$ 记号 | 大 $O$ 記號 |
|
||||
| asymptotic upper bound | 渐近上界 | 漸近上界 |
|
||||
| sign-magnitude | 原码 | 原碼 |
|
||||
| 1’s complement | 反码 | 一補數 |
|
||||
| 2’s complement | 补码 | 二補數 |
|
||||
| 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 | 哈希表 | 雜湊表 |
|
||||
| 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 树 | AVL 樹 |
|
||||
| red-black tree | 红黑树 | 紅黑樹 |
|
||||
| level-order traversal | 层序遍历 | 層序走訪 |
|
||||
| breadth-first traversal | 广度优先遍历 | 廣度優先走訪 |
|
||||
| depth-first traversal | 深度优先遍历 | 深度優先走訪 |
|
||||
| binary search tree | 二叉搜索树 | 二元搜尋樹 |
|
||||
| balanced binary search tree | 平衡二叉搜索树 | 平衡二元搜尋樹 |
|
||||
| balance factor | 平衡因子 | 平衡因子 |
|
||||
| heap | 堆 | 堆積 |
|
||||
| max heap | 大顶堆 | 大頂堆積 |
|
||||
| min heap | 小顶堆 | 小頂堆積 |
|
||||
| priority queue | 优先队列 | 優先佇列 |
|
||||
| heapify | 堆化 | 堆積化 |
|
||||
| top-$k$ problem | Top-$k$ 问题 | 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$ 皇后问题 | $n$ 皇后問題 |
|
||||
| dynamic programming | 动态规划 | 動態規劃 |
|
||||
| initial state | 初始状态 | 初始狀態 |
|
||||
| state-transition equation | 状态转移方程 | 狀態轉移方程 |
|
||||
| knapsack problem | 背包问题 | 背包問題 |
|
||||
| edit distance problem | 编辑距离问题 | 編輯距離問題 |
|
||||
| greedy algorithm | 贪心算法 | 貪婪演算法 |
|
||||
|
||||
</div>
|
||||
1462
zh-Hant/docs/chapter_array_and_linkedlist/array.md
Executable file
1462
zh-Hant/docs/chapter_array_and_linkedlist/array.md
Executable file
File diff suppressed because it is too large
Load Diff
22
zh-Hant/docs/chapter_array_and_linkedlist/index.md
Normal file
22
zh-Hant/docs/chapter_array_and_linkedlist/index.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/view-list-outline
|
||||
---
|
||||
|
||||
# 第 4 章 陣列與鏈結串列
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
資料結構的世界如同一堵厚實的磚牆。
|
||||
|
||||
陣列的磚塊整齊排列,逐個緊貼。鏈結串列的磚塊分散各處,連線的藤蔓自由地穿梭於磚縫之間。
|
||||
|
||||
## Chapter Contents
|
||||
|
||||
- [4.1 陣列](https://www.hello-algo.com/en/chapter_array_and_linkedlist/array/)
|
||||
- [4.2 鏈結串列](https://www.hello-algo.com/en/chapter_array_and_linkedlist/linked_list/)
|
||||
- [4.3 串列](https://www.hello-algo.com/en/chapter_array_and_linkedlist/list/)
|
||||
- [4.4 記憶體與快取 *](https://www.hello-algo.com/en/chapter_array_and_linkedlist/ram_and_cache/)
|
||||
- [4.5 小結](https://www.hello-algo.com/en/chapter_array_and_linkedlist/summary/)
|
||||
1565
zh-Hant/docs/chapter_array_and_linkedlist/linked_list.md
Executable file
1565
zh-Hant/docs/chapter_array_and_linkedlist/linked_list.md
Executable file
File diff suppressed because it is too large
Load Diff
2496
zh-Hant/docs/chapter_array_and_linkedlist/list.md
Executable file
2496
zh-Hant/docs/chapter_array_and_linkedlist/list.md
Executable file
File diff suppressed because one or more lines are too long
84
zh-Hant/docs/chapter_array_and_linkedlist/ram_and_cache.md
Normal file
84
zh-Hant/docs/chapter_array_and_linkedlist/ram_and_cache.md
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# 4.4 記憶體與快取 *
|
||||
|
||||
在本章的前兩節中,我們探討了陣列和鏈結串列這兩種基礎且重要的資料結構,它們分別代表了“連續儲存”和“分散儲存”兩種物理結構。
|
||||
|
||||
實際上,**物理結構在很大程度上決定了程式對記憶體和快取的使用效率**,進而影響演算法程式的整體效能。
|
||||
|
||||
## 4.4.1 計算機儲存裝置
|
||||
|
||||
計算機中包括三種類型的儲存裝置:<u>硬碟(hard disk)</u>、<u>記憶體(random-access memory, RAM)</u>、<u>快取(cache memory)</u>。表 4-2 展示了它們在計算機系統中的不同角色和效能特點。
|
||||
|
||||
<p align="center"> 表 4-2 計算機的儲存裝置 </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
| | 硬碟 | 記憶體 | 快取 |
|
||||
| ------ | ---------------------------------------- | -------------------------------------- | ------------------------------------------------- |
|
||||
| 用途 | 長期儲存資料,包括作業系統、程式、檔案等 | 臨時儲存當前執行的程式和正在處理的資料 | 儲存經常訪問的資料和指令,減少 CPU 訪問記憶體的次數 |
|
||||
| 易失性 | 斷電後資料不會丟失 | 斷電後資料會丟失 | 斷電後資料會丟失 |
|
||||
| 容量 | 較大,TB 級別 | 較小,GB 級別 | 非常小,MB 級別 |
|
||||
| 速度 | 較慢,幾百到幾千 MB/s | 較快,幾十 GB/s | 非常快,幾十到幾百 GB/s |
|
||||
| 價格 | 較便宜,幾毛到幾元 / GB | 較貴,幾十到幾百元 / GB | 非常貴,隨 CPU 打包計價 |
|
||||
|
||||
</div>
|
||||
|
||||
我們可以將計算機儲存系統想象為圖 4-9 所示的金字塔結構。越靠近金字塔頂端的儲存裝置的速度越快、容量越小、成本越高。這種多層級的設計並非偶然,而是計算機科學家和工程師們經過深思熟慮的結果。
|
||||
|
||||
- **硬碟難以被記憶體取代**。首先,記憶體中的資料在斷電後會丟失,因此它不適合長期儲存資料;其次,記憶體的成本是硬碟的幾十倍,這使得它難以在消費者市場普及。
|
||||
- **快取的大容量和高速度難以兼得**。隨著 L1、L2、L3 快取的容量逐步增大,其物理尺寸會變大,與 CPU 核心之間的物理距離會變遠,從而導致資料傳輸時間增加,元素訪問延遲變高。在當前技術下,多層級的快取結構是容量、速度和成本之間的最佳平衡點。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 4-9 計算機儲存系統 </p>
|
||||
|
||||
!!! note
|
||||
|
||||
計算機的儲存層次結構體現了速度、容量和成本三者之間的精妙平衡。實際上,這種權衡普遍存在於所有工業領域,它要求我們在不同的優勢和限制之間找到最佳平衡點。
|
||||
|
||||
總的來說,**硬碟用於長期儲存大量資料,記憶體用於臨時儲存程式執行中正在處理的資料,而快取則用於儲存經常訪問的資料和指令**,以提高程式執行效率。三者共同協作,確保計算機系統高效執行。
|
||||
|
||||
如圖 4-10 所示,在程式執行時,資料會從硬碟中被讀取到記憶體中,供 CPU 計算使用。快取可以看作 CPU 的一部分,**它透過智慧地從記憶體載入資料**,給 CPU 提供高速的資料讀取,從而顯著提升程式的執行效率,減少對較慢的記憶體的依賴。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 4-10 硬碟、記憶體和快取之間的資料流通 </p>
|
||||
|
||||
## 4.4.2 資料結構的記憶體效率
|
||||
|
||||
在記憶體空間利用方面,陣列和鏈結串列各自具有優勢和侷限性。
|
||||
|
||||
一方面,**記憶體是有限的,且同一塊記憶體不能被多個程式共享**,因此我們希望資料結構能夠儘可能高效地利用空間。陣列的元素緊密排列,不需要額外的空間來儲存鏈結串列節點間的引用(指標),因此空間效率更高。然而,陣列需要一次性分配足夠的連續記憶體空間,這可能導致記憶體浪費,陣列擴容也需要額外的時間和空間成本。相比之下,鏈結串列以“節點”為單位進行動態記憶體分配和回收,提供了更大的靈活性。
|
||||
|
||||
另一方面,在程式執行時,**隨著反覆申請與釋放記憶體,空閒記憶體的碎片化程度會越來越高**,從而導致記憶體的利用效率降低。陣列由於其連續的儲存方式,相對不容易導致記憶體碎片化。相反,鏈結串列的元素是分散儲存的,在頻繁的插入與刪除操作中,更容易導致記憶體碎片化。
|
||||
|
||||
## 4.4.3 資料結構的快取效率
|
||||
|
||||
快取雖然在空間容量上遠小於記憶體,但它比記憶體快得多,在程式執行速度上起著至關重要的作用。由於快取的容量有限,只能儲存一小部分頻繁訪問的資料,因此當 CPU 嘗試訪問的資料不在快取中時,就會發生<u>快取未命中(cache miss)</u>,此時 CPU 不得不從速度較慢的記憶體中載入所需資料。
|
||||
|
||||
顯然,**“快取未命中”越少,CPU 讀寫資料的效率就越高**,程式效能也就越好。我們將 CPU 從快取中成功獲取資料的比例稱為<u>快取命中率(cache hit rate)</u>,這個指標通常用來衡量快取效率。
|
||||
|
||||
為了儘可能達到更高的效率,快取會採取以下資料載入機制。
|
||||
|
||||
- **快取行**:快取不是單個位元組地儲存與載入資料,而是以快取行為單位。相比於單個位元組的傳輸,快取行的傳輸形式更加高效。
|
||||
- **預取機制**:處理器會嘗試預測資料訪問模式(例如順序訪問、固定步長跳躍訪問等),並根據特定模式將資料載入至快取之中,從而提升命中率。
|
||||
- **空間區域性**:如果一個數據被訪問,那麼它附近的資料可能近期也會被訪問。因此,快取在載入某一資料時,也會載入其附近的資料,以提高命中率。
|
||||
- **時間區域性**:如果一個數據被訪問,那麼它在不久的將來很可能再次被訪問。快取利用這一原理,透過保留最近訪問過的資料來提高命中率。
|
||||
|
||||
實際上,**陣列和鏈結串列對快取的利用效率是不同的**,主要體現在以下幾個方面。
|
||||
|
||||
- **佔用空間**:鏈結串列元素比陣列元素佔用空間更多,導致快取中容納的有效資料量更少。
|
||||
- **快取行**:鏈結串列資料分散在記憶體各處,而快取是“按行載入”的,因此載入到無效資料的比例更高。
|
||||
- **預取機制**:陣列比鏈結串列的資料訪問模式更具“可預測性”,即系統更容易猜出即將被載入的資料。
|
||||
- **空間區域性**:陣列被儲存在集中的記憶體空間中,因此被載入資料附近的資料更有可能即將被訪問。
|
||||
|
||||
總體而言,**陣列具有更高的快取命中率,因此它在操作效率上通常優於鏈結串列**。這使得在解決演算法問題時,基於陣列實現的資料結構往往更受歡迎。
|
||||
|
||||
需要注意的是,**高快取效率並不意味著陣列在所有情況下都優於鏈結串列**。實際應用中選擇哪種資料結構,應根據具體需求來決定。例如,陣列和鏈結串列都可以實現“堆疊”資料結構(下一章會詳細介紹),但它們適用於不同場景。
|
||||
|
||||
- 在做演算法題時,我們會傾向於選擇基於陣列實現的堆疊,因為它提供了更高的操作效率和隨機訪問的能力,代價僅是需要預先為陣列分配一定的記憶體空間。
|
||||
- 如果資料量非常大、動態性很高、堆疊的預期大小難以估計,那麼基於鏈結串列實現的堆疊更加合適。鏈結串列能夠將大量資料分散儲存於記憶體的不同部分,並且避免了陣列擴容產生的額外開銷。
|
||||
80
zh-Hant/docs/chapter_array_and_linkedlist/summary.md
Normal file
80
zh-Hant/docs/chapter_array_and_linkedlist/summary.md
Normal file
@@ -0,0 +1,80 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 4.5 小結
|
||||
|
||||
### 1. 重點回顧
|
||||
|
||||
- 陣列和鏈結串列是兩種基本的資料結構,分別代表資料在計算機記憶體中的兩種儲存方式:連續空間儲存和分散空間儲存。兩者的特點呈現出互補的特性。
|
||||
- 陣列支持隨機訪問、佔用記憶體較少;但插入和刪除元素效率低,且初始化後長度不可變。
|
||||
- 鏈結串列透過更改引用(指標)實現高效的節點插入與刪除,且可以靈活調整長度;但節點訪問效率低、佔用記憶體較多。常見的鏈結串列型別包括單向鏈結串列、環形鏈結串列、雙向鏈結串列。
|
||||
- 串列是一種支持增刪查改的元素有序集合,通常基於動態陣列實現。它保留了陣列的優勢,同時可以靈活調整長度。
|
||||
- 串列的出現大幅提高了陣列的實用性,但可能導致部分記憶體空間浪費。
|
||||
- 程式執行時,資料主要儲存在記憶體中。陣列可提供更高的記憶體空間效率,而鏈結串列則在記憶體使用上更加靈活。
|
||||
- 快取透過快取行、預取機制以及空間區域性和時間區域性等資料載入機制,為 CPU 提供快速資料訪問,顯著提升程式的執行效率。
|
||||
- 由於陣列具有更高的快取命中率,因此它通常比鏈結串列更高效。在選擇資料結構時,應根據具體需求和場景做出恰當選擇。
|
||||
|
||||
### 2. Q & A
|
||||
|
||||
**Q**:陣列儲存在堆疊上和儲存在堆積上,對時間效率和空間效率是否有影響?
|
||||
|
||||
儲存在堆疊上和堆積上的陣列都被儲存在連續記憶體空間內,資料操作效率基本一致。然而,堆疊和堆積具有各自的特點,從而導致以下不同點。
|
||||
|
||||
1. 分配和釋放效率:堆疊是一塊較小的記憶體,分配由編譯器自動完成;而堆積記憶體相對更大,可以在程式碼中動態分配,更容易碎片化。因此,堆積上的分配和釋放操作通常比堆疊上的慢。
|
||||
2. 大小限制:堆疊記憶體相對較小,堆積的大小一般受限於可用記憶體。因此堆積更加適合儲存大型陣列。
|
||||
3. 靈活性:堆疊上的陣列的大小需要在編譯時確定,而堆積上的陣列的大小可以在執行時動態確定。
|
||||
|
||||
**Q**:為什麼陣列要求相同型別的元素,而在鏈結串列中卻沒有強調相同型別呢?
|
||||
|
||||
鏈結串列由節點組成,節點之間透過引用(指標)連線,各個節點可以儲存不同型別的資料,例如 `int`、`double`、`string`、`object` 等。
|
||||
|
||||
相對地,陣列元素則必須是相同型別的,這樣才能透過計算偏移量來獲取對應元素位置。例如,陣列同時包含 `int` 和 `long` 兩種型別,單個元素分別佔用 4 位元組 和 8 位元組 ,此時就不能用以下公式計算偏移量了,因為陣列中包含了兩種“元素長度”。
|
||||
|
||||
```shell
|
||||
# 元素記憶體位址 = 陣列記憶體位址(首元素記憶體位址) + 元素長度 * 元素索引
|
||||
```
|
||||
|
||||
**Q**:刪除節點 `P` 後,是否需要把 `P.next` 設為 `None` 呢?
|
||||
|
||||
不修改 `P.next` 也可以。從該鏈結串列的角度看,從頭節點走訪到尾節點已經不會遇到 `P` 了。這意味著節點 `P` 已經從鏈結串列中刪除了,此時節點 `P` 指向哪裡都不會對該鏈結串列產生影響。
|
||||
|
||||
從資料結構與演算法(做題)的角度看,不斷開沒有關係,只要保證程式的邏輯是正確的就行。從標準庫的角度看,斷開更加安全、邏輯更加清晰。如果不斷開,假設被刪除節點未被正常回收,那麼它會影響後繼節點的記憶體回收。
|
||||
|
||||
**Q**:在鏈結串列中插入和刪除操作的時間複雜度是 $O(1)$ 。但是增刪之前都需要 $O(n)$ 的時間查詢元素,那為什麼時間複雜度不是 $O(n)$ 呢?
|
||||
|
||||
如果是先查詢元素、再刪除元素,時間複雜度確實是 $O(n)$ 。然而,鏈結串列的 $O(1)$ 增刪的優勢可以在其他應用上得到體現。例如,雙向佇列適合使用鏈結串列實現,我們維護一個指標變數始終指向頭節點、尾節點,每次插入與刪除操作都是 $O(1)$ 。
|
||||
|
||||
**Q**:圖“鏈結串列定義與儲存方式”中,淺藍色的儲存節點指標是佔用一塊記憶體位址嗎?還是和節點值各佔一半呢?
|
||||
|
||||
該示意圖只是定性表示,定量表示需要根據具體情況進行分析。
|
||||
|
||||
- 不同型別的節點值佔用的空間是不同的,比如 `int`、`long`、`double` 和例項物件等。
|
||||
- 指標變數佔用的記憶體空間大小根據所使用的作業系統及編譯環境而定,大多為 8 位元組或 4 位元組。
|
||||
|
||||
**Q**:在串列末尾新增元素是否時時刻刻都為 $O(1)$ ?
|
||||
|
||||
如果新增元素時超出串列長度,則需要先擴容串列再新增。系統會申請一塊新的記憶體,並將原串列的所有元素搬運過去,這時候時間複雜度就會是 $O(n)$ 。
|
||||
|
||||
**Q**:“串列的出現極大地提高了陣列的實用性,但可能導致部分記憶體空間浪費”,這裡的空間浪費是指額外增加的變數如容量、長度、擴容倍數所佔的記憶體嗎?
|
||||
|
||||
這裡的空間浪費主要有兩方面含義:一方面,串列都會設定一個初始長度,我們不一定需要用這麼多;另一方面,為了防止頻繁擴容,擴容一般會乘以一個係數,比如 $\times 1.5$ 。這樣一來,也會出現很多空位,我們通常不能完全填滿它們。
|
||||
|
||||
**Q**:在 Python 中初始化 `n = [1, 2, 3]` 後,這 3 個元素的位址是相連的,但是初始化 `m = [2, 1, 3]` 會發現它們每個元素的 id 並不是連續的,而是分別跟 `n` 中的相同。這些元素的位址不連續,那麼 `m` 還是陣列嗎?
|
||||
|
||||
假如把串列元素換成鏈結串列節點 `n = [n1, n2, n3, n4, n5]` ,通常情況下這 5 個節點物件也分散儲存在記憶體各處。然而,給定一個串列索引,我們仍然可以在 $O(1)$ 時間內獲取節點記憶體位址,從而訪問到對應的節點。這是因為陣列中儲存的是節點的引用,而非節點本身。
|
||||
|
||||
與許多語言不同,Python 中的數字也被包裝為物件,串列中儲存的不是數字本身,而是對數字的引用。因此,我們會發現兩個陣列中的相同數字擁有同一個 id ,並且這些數字的記憶體位址無須連續。
|
||||
|
||||
**Q**:C++ STL 裡面的 `std::list` 已經實現了雙向鏈結串列,但好像一些演算法書上不怎麼直接使用它,是不是因為有什麼侷限性呢?
|
||||
|
||||
一方面,我們往往更青睞使用陣列實現演算法,而只在必要時才使用鏈結串列,主要有兩個原因。
|
||||
|
||||
- 空間開銷:由於每個元素需要兩個額外的指標(一個用於前一個元素,一個用於後一個元素),所以 `std::list` 通常比 `std::vector` 更佔用空間。
|
||||
- 快取不友好:由於資料不是連續存放的,因此 `std::list` 對快取的利用率較低。一般情況下,`std::vector` 的效能會更好。
|
||||
|
||||
另一方面,必要使用鏈結串列的情況主要是二元樹和圖。堆疊和佇列往往會使用程式語言提供的 `stack` 和 `queue` ,而非鏈結串列。
|
||||
|
||||
**Q**:初始化串列 `res = [0] * self.size()` 操作,會導致 `res` 的每個元素引用相同的位址嗎?
|
||||
|
||||
不會。但二維陣列會有這個問題,例如初始化二維串列 `res = [[0] * self.size()]` ,則多次引用了同一個串列 `[0]` 。
|
||||
1951
zh-Hant/docs/chapter_backtracking/backtracking_algorithm.md
Normal file
1951
zh-Hant/docs/chapter_backtracking/backtracking_algorithm.md
Normal file
File diff suppressed because one or more lines are too long
22
zh-Hant/docs/chapter_backtracking/index.md
Normal file
22
zh-Hant/docs/chapter_backtracking/index.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/map-marker-path
|
||||
---
|
||||
|
||||
# 第 13 章 回溯
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
我們如同迷宮中的探索者,在前進的道路上可能會遇到困難。
|
||||
|
||||
回溯的力量讓我們能夠重新開始,不斷嘗試,最終找到通往光明的出口。
|
||||
|
||||
## Chapter Contents
|
||||
|
||||
- [13.1 回溯演算法](https://www.hello-algo.com/en/chapter_backtracking/backtracking_algorithm/)
|
||||
- [13.2 全排列問題](https://www.hello-algo.com/en/chapter_backtracking/permutations_problem/)
|
||||
- [13.3 子集和問題](https://www.hello-algo.com/en/chapter_backtracking/subset_sum_problem/)
|
||||
- [13.4 N 皇后問題](https://www.hello-algo.com/en/chapter_backtracking/n_queens_problem/)
|
||||
- [13.5 小結](https://www.hello-algo.com/en/chapter_backtracking/summary/)
|
||||
732
zh-Hant/docs/chapter_backtracking/n_queens_problem.md
Normal file
732
zh-Hant/docs/chapter_backtracking/n_queens_problem.md
Normal file
@@ -0,0 +1,732 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 13.4 n 皇后問題
|
||||
|
||||
!!! question
|
||||
|
||||
根據國際象棋的規則,皇后可以攻擊與同處一行、一列或一條斜線上的棋子。給定 $n$ 個皇后和一個 $n \times n$ 大小的棋盤,尋找使得所有皇后之間無法相互攻擊的擺放方案。
|
||||
|
||||
如圖 13-15 所示,當 $n = 4$ 時,共可以找到兩個解。從回溯演算法的角度看,$n \times n$ 大小的棋盤共有 $n^2$ 個格子,給出了所有的選擇 `choices` 。在逐個放置皇后的過程中,棋盤狀態在不斷地變化,每個時刻的棋盤就是狀態 `state` 。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 13-15 4 皇后問題的解 </p>
|
||||
|
||||
圖 13-16 展示了本題的三個約束條件:**多個皇后不能在同一行、同一列、同一條對角線上**。值得注意的是,對角線分為主對角線 `\` 和次對角線 `/` 兩種。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 13-16 n 皇后問題的約束條件 </p>
|
||||
|
||||
### 1. 逐行放置策略
|
||||
|
||||
皇后的數量和棋盤的行數都為 $n$ ,因此我們容易得到一個推論:**棋盤每行都允許且只允許放置一個皇后**。
|
||||
|
||||
也就是說,我們可以採取逐行放置策略:從第一行開始,在每行放置一個皇后,直至最後一行結束。
|
||||
|
||||
圖 13-17 所示為 $4$ 皇后問題的逐行放置過程。受畫幅限制,圖 13-17 僅展開了第一行的其中一個搜尋分支,並且將不滿足列約束和對角線約束的方案都進行了剪枝。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 13-17 逐行放置策略 </p>
|
||||
|
||||
從本質上看,**逐行放置策略起到了剪枝的作用**,它避免了同一行出現多個皇后的所有搜尋分支。
|
||||
|
||||
### 2. 列與對角線剪枝
|
||||
|
||||
為了滿足列約束,我們可以利用一個長度為 $n$ 的布林型陣列 `cols` 記錄每一列是否有皇后。在每次決定放置前,我們透過 `cols` 將已有皇后的列進行剪枝,並在回溯中動態更新 `cols` 的狀態。
|
||||
|
||||
那麼,如何處理對角線約束呢?設棋盤中某個格子的行列索引為 $(row, col)$ ,選定矩陣中的某條主對角線,我們發現該對角線上所有格子的行索引減列索引都相等,**即對角線上所有格子的 $row - col$ 為恆定值**。
|
||||
|
||||
也就是說,如果兩個格子滿足 $row_1 - col_1 = row_2 - col_2$ ,則它們一定處在同一條主對角線上。利用該規律,我們可以藉助圖 13-18 所示的陣列 `diags1` 記錄每條主對角線上是否有皇后。
|
||||
|
||||
同理,**次對角線上的所有格子的 $row + col$ 是恆定值**。我們同樣也可以藉助陣列 `diags2` 來處理次對角線約束。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 13-18 處理列約束和對角線約束 </p>
|
||||
|
||||
### 3. 程式碼實現
|
||||
|
||||
請注意,$n$ 維方陣中 $row - col$ 的範圍是 $[-n + 1, n - 1]$ ,$row + col$ 的範圍是 $[0, 2n - 2]$ ,所以主對角線和次對角線的數量都為 $2n - 1$ ,即陣列 `diags1` 和 `diags2` 的長度都為 $2n - 1$ 。
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="n_queens.py"
|
||||
def backtrack(
|
||||
row: int,
|
||||
n: int,
|
||||
state: list[list[str]],
|
||||
res: list[list[list[str]]],
|
||||
cols: list[bool],
|
||||
diags1: list[bool],
|
||||
diags2: list[bool],
|
||||
):
|
||||
"""回溯演算法:n 皇后"""
|
||||
# 當放置完所有行時,記錄解
|
||||
if row == n:
|
||||
res.append([list(row) for row in state])
|
||||
return
|
||||
# 走訪所有列
|
||||
for col in range(n):
|
||||
# 計算該格子對應的主對角線和次對角線
|
||||
diag1 = row - col + n - 1
|
||||
diag2 = row + col
|
||||
# 剪枝:不允許該格子所在列、主對角線、次對角線上存在皇后
|
||||
if not cols[col] and not diags1[diag1] and not diags2[diag2]:
|
||||
# 嘗試:將皇后放置在該格子
|
||||
state[row][col] = "Q"
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = True
|
||||
# 放置下一行
|
||||
backtrack(row + 1, n, state, res, cols, diags1, diags2)
|
||||
# 回退:將該格子恢復為空位
|
||||
state[row][col] = "#"
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = False
|
||||
|
||||
def n_queens(n: int) -> list[list[list[str]]]:
|
||||
"""求解 n 皇后"""
|
||||
# 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
|
||||
state = [["#" for _ in range(n)] for _ in range(n)]
|
||||
cols = [False] * n # 記錄列是否有皇后
|
||||
diags1 = [False] * (2 * n - 1) # 記錄主對角線上是否有皇后
|
||||
diags2 = [False] * (2 * n - 1) # 記錄次對角線上是否有皇后
|
||||
res = []
|
||||
backtrack(0, n, state, res, cols, diags1, diags2)
|
||||
|
||||
return res
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="n_queens.cpp"
|
||||
/* 回溯演算法:n 皇后 */
|
||||
void backtrack(int row, int n, vector<vector<string>> &state, vector<vector<vector<string>>> &res, vector<bool> &cols,
|
||||
vector<bool> &diags1, vector<bool> &diags2) {
|
||||
// 當放置完所有行時,記錄解
|
||||
if (row == n) {
|
||||
res.push_back(state);
|
||||
return;
|
||||
}
|
||||
// 走訪所有列
|
||||
for (int col = 0; col < n; col++) {
|
||||
// 計算該格子對應的主對角線和次對角線
|
||||
int diag1 = row - col + n - 1;
|
||||
int diag2 = row + col;
|
||||
// 剪枝:不允許該格子所在列、主對角線、次對角線上存在皇后
|
||||
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
|
||||
// 嘗試:將皇后放置在該格子
|
||||
state[row][col] = "Q";
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = true;
|
||||
// 放置下一行
|
||||
backtrack(row + 1, n, state, res, cols, diags1, diags2);
|
||||
// 回退:將該格子恢復為空位
|
||||
state[row][col] = "#";
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 求解 n 皇后 */
|
||||
vector<vector<vector<string>>> nQueens(int n) {
|
||||
// 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
|
||||
vector<vector<string>> state(n, vector<string>(n, "#"));
|
||||
vector<bool> cols(n, false); // 記錄列是否有皇后
|
||||
vector<bool> diags1(2 * n - 1, false); // 記錄主對角線上是否有皇后
|
||||
vector<bool> diags2(2 * n - 1, false); // 記錄次對角線上是否有皇后
|
||||
vector<vector<vector<string>>> res;
|
||||
|
||||
backtrack(0, n, state, res, cols, diags1, diags2);
|
||||
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="n_queens.java"
|
||||
/* 回溯演算法:n 皇后 */
|
||||
void backtrack(int row, int n, List<List<String>> state, List<List<List<String>>> res,
|
||||
boolean[] cols, boolean[] diags1, boolean[] diags2) {
|
||||
// 當放置完所有行時,記錄解
|
||||
if (row == n) {
|
||||
List<List<String>> copyState = new ArrayList<>();
|
||||
for (List<String> sRow : state) {
|
||||
copyState.add(new ArrayList<>(sRow));
|
||||
}
|
||||
res.add(copyState);
|
||||
return;
|
||||
}
|
||||
// 走訪所有列
|
||||
for (int col = 0; col < n; col++) {
|
||||
// 計算該格子對應的主對角線和次對角線
|
||||
int diag1 = row - col + n - 1;
|
||||
int diag2 = row + col;
|
||||
// 剪枝:不允許該格子所在列、主對角線、次對角線上存在皇后
|
||||
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
|
||||
// 嘗試:將皇后放置在該格子
|
||||
state.get(row).set(col, "Q");
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = true;
|
||||
// 放置下一行
|
||||
backtrack(row + 1, n, state, res, cols, diags1, diags2);
|
||||
// 回退:將該格子恢復為空位
|
||||
state.get(row).set(col, "#");
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 求解 n 皇后 */
|
||||
List<List<List<String>>> nQueens(int n) {
|
||||
// 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
|
||||
List<List<String>> state = new ArrayList<>();
|
||||
for (int i = 0; i < n; i++) {
|
||||
List<String> row = new ArrayList<>();
|
||||
for (int j = 0; j < n; j++) {
|
||||
row.add("#");
|
||||
}
|
||||
state.add(row);
|
||||
}
|
||||
boolean[] cols = new boolean[n]; // 記錄列是否有皇后
|
||||
boolean[] diags1 = new boolean[2 * n - 1]; // 記錄主對角線上是否有皇后
|
||||
boolean[] diags2 = new boolean[2 * n - 1]; // 記錄次對角線上是否有皇后
|
||||
List<List<List<String>>> res = new ArrayList<>();
|
||||
|
||||
backtrack(0, n, state, res, cols, diags1, diags2);
|
||||
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="n_queens.cs"
|
||||
/* 回溯演算法:n 皇后 */
|
||||
void Backtrack(int row, int n, List<List<string>> state, List<List<List<string>>> res,
|
||||
bool[] cols, bool[] diags1, bool[] diags2) {
|
||||
// 當放置完所有行時,記錄解
|
||||
if (row == n) {
|
||||
List<List<string>> copyState = [];
|
||||
foreach (List<string> sRow in state) {
|
||||
copyState.Add(new List<string>(sRow));
|
||||
}
|
||||
res.Add(copyState);
|
||||
return;
|
||||
}
|
||||
// 走訪所有列
|
||||
for (int col = 0; col < n; col++) {
|
||||
// 計算該格子對應的主對角線和次對角線
|
||||
int diag1 = row - col + n - 1;
|
||||
int diag2 = row + col;
|
||||
// 剪枝:不允許該格子所在列、主對角線、次對角線上存在皇后
|
||||
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
|
||||
// 嘗試:將皇后放置在該格子
|
||||
state[row][col] = "Q";
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = true;
|
||||
// 放置下一行
|
||||
Backtrack(row + 1, n, state, res, cols, diags1, diags2);
|
||||
// 回退:將該格子恢復為空位
|
||||
state[row][col] = "#";
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 求解 n 皇后 */
|
||||
List<List<List<string>>> NQueens(int n) {
|
||||
// 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
|
||||
List<List<string>> state = [];
|
||||
for (int i = 0; i < n; i++) {
|
||||
List<string> row = [];
|
||||
for (int j = 0; j < n; j++) {
|
||||
row.Add("#");
|
||||
}
|
||||
state.Add(row);
|
||||
}
|
||||
bool[] cols = new bool[n]; // 記錄列是否有皇后
|
||||
bool[] diags1 = new bool[2 * n - 1]; // 記錄主對角線上是否有皇后
|
||||
bool[] diags2 = new bool[2 * n - 1]; // 記錄次對角線上是否有皇后
|
||||
List<List<List<string>>> res = [];
|
||||
|
||||
Backtrack(0, n, state, res, cols, diags1, diags2);
|
||||
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="n_queens.go"
|
||||
/* 回溯演算法:n 皇后 */
|
||||
func backtrack(row, n int, state *[][]string, res *[][][]string, cols, diags1, diags2 *[]bool) {
|
||||
// 當放置完所有行時,記錄解
|
||||
if row == n {
|
||||
newState := make([][]string, len(*state))
|
||||
for i, _ := range newState {
|
||||
newState[i] = make([]string, len((*state)[0]))
|
||||
copy(newState[i], (*state)[i])
|
||||
|
||||
}
|
||||
*res = append(*res, newState)
|
||||
}
|
||||
// 走訪所有列
|
||||
for col := 0; col < n; col++ {
|
||||
// 計算該格子對應的主對角線和次對角線
|
||||
diag1 := row - col + n - 1
|
||||
diag2 := row + col
|
||||
// 剪枝:不允許該格子所在列、主對角線、次對角線上存在皇后
|
||||
if !(*cols)[col] && !(*diags1)[diag1] && !(*diags2)[diag2] {
|
||||
// 嘗試:將皇后放置在該格子
|
||||
(*state)[row][col] = "Q"
|
||||
(*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = true, true, true
|
||||
// 放置下一行
|
||||
backtrack(row+1, n, state, res, cols, diags1, diags2)
|
||||
// 回退:將該格子恢復為空位
|
||||
(*state)[row][col] = "#"
|
||||
(*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = false, false, false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 求解 n 皇后 */
|
||||
func nQueens(n int) [][][]string {
|
||||
// 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
|
||||
state := make([][]string, n)
|
||||
for i := 0; i < n; i++ {
|
||||
row := make([]string, n)
|
||||
for i := 0; i < n; i++ {
|
||||
row[i] = "#"
|
||||
}
|
||||
state[i] = row
|
||||
}
|
||||
// 記錄列是否有皇后
|
||||
cols := make([]bool, n)
|
||||
diags1 := make([]bool, 2*n-1)
|
||||
diags2 := make([]bool, 2*n-1)
|
||||
res := make([][][]string, 0)
|
||||
backtrack(0, n, &state, &res, &cols, &diags1, &diags2)
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="n_queens.swift"
|
||||
/* 回溯演算法:n 皇后 */
|
||||
func backtrack(row: Int, n: Int, state: inout [[String]], res: inout [[[String]]], cols: inout [Bool], diags1: inout [Bool], diags2: inout [Bool]) {
|
||||
// 當放置完所有行時,記錄解
|
||||
if row == n {
|
||||
res.append(state)
|
||||
return
|
||||
}
|
||||
// 走訪所有列
|
||||
for col in 0 ..< n {
|
||||
// 計算該格子對應的主對角線和次對角線
|
||||
let diag1 = row - col + n - 1
|
||||
let diag2 = row + col
|
||||
// 剪枝:不允許該格子所在列、主對角線、次對角線上存在皇后
|
||||
if !cols[col] && !diags1[diag1] && !diags2[diag2] {
|
||||
// 嘗試:將皇后放置在該格子
|
||||
state[row][col] = "Q"
|
||||
cols[col] = true
|
||||
diags1[diag1] = true
|
||||
diags2[diag2] = true
|
||||
// 放置下一行
|
||||
backtrack(row: row + 1, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2)
|
||||
// 回退:將該格子恢復為空位
|
||||
state[row][col] = "#"
|
||||
cols[col] = false
|
||||
diags1[diag1] = false
|
||||
diags2[diag2] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 求解 n 皇后 */
|
||||
func nQueens(n: Int) -> [[[String]]] {
|
||||
// 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
|
||||
var state = Array(repeating: Array(repeating: "#", count: n), count: n)
|
||||
var cols = Array(repeating: false, count: n) // 記錄列是否有皇后
|
||||
var diags1 = Array(repeating: false, count: 2 * n - 1) // 記錄主對角線上是否有皇后
|
||||
var diags2 = Array(repeating: false, count: 2 * n - 1) // 記錄次對角線上是否有皇后
|
||||
var res: [[[String]]] = []
|
||||
|
||||
backtrack(row: 0, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2)
|
||||
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="n_queens.js"
|
||||
/* 回溯演算法:n 皇后 */
|
||||
function backtrack(row, n, state, res, cols, diags1, diags2) {
|
||||
// 當放置完所有行時,記錄解
|
||||
if (row === n) {
|
||||
res.push(state.map((row) => row.slice()));
|
||||
return;
|
||||
}
|
||||
// 走訪所有列
|
||||
for (let col = 0; col < n; col++) {
|
||||
// 計算該格子對應的主對角線和次對角線
|
||||
const diag1 = row - col + n - 1;
|
||||
const diag2 = row + col;
|
||||
// 剪枝:不允許該格子所在列、主對角線、次對角線上存在皇后
|
||||
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
|
||||
// 嘗試:將皇后放置在該格子
|
||||
state[row][col] = 'Q';
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = true;
|
||||
// 放置下一行
|
||||
backtrack(row + 1, n, state, res, cols, diags1, diags2);
|
||||
// 回退:將該格子恢復為空位
|
||||
state[row][col] = '#';
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 求解 n 皇后 */
|
||||
function nQueens(n) {
|
||||
// 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
|
||||
const state = Array.from({ length: n }, () => Array(n).fill('#'));
|
||||
const cols = Array(n).fill(false); // 記錄列是否有皇后
|
||||
const diags1 = Array(2 * n - 1).fill(false); // 記錄主對角線上是否有皇后
|
||||
const diags2 = Array(2 * n - 1).fill(false); // 記錄次對角線上是否有皇后
|
||||
const res = [];
|
||||
|
||||
backtrack(0, n, state, res, cols, diags1, diags2);
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="n_queens.ts"
|
||||
/* 回溯演算法:n 皇后 */
|
||||
function backtrack(
|
||||
row: number,
|
||||
n: number,
|
||||
state: string[][],
|
||||
res: string[][][],
|
||||
cols: boolean[],
|
||||
diags1: boolean[],
|
||||
diags2: boolean[]
|
||||
): void {
|
||||
// 當放置完所有行時,記錄解
|
||||
if (row === n) {
|
||||
res.push(state.map((row) => row.slice()));
|
||||
return;
|
||||
}
|
||||
// 走訪所有列
|
||||
for (let col = 0; col < n; col++) {
|
||||
// 計算該格子對應的主對角線和次對角線
|
||||
const diag1 = row - col + n - 1;
|
||||
const diag2 = row + col;
|
||||
// 剪枝:不允許該格子所在列、主對角線、次對角線上存在皇后
|
||||
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
|
||||
// 嘗試:將皇后放置在該格子
|
||||
state[row][col] = 'Q';
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = true;
|
||||
// 放置下一行
|
||||
backtrack(row + 1, n, state, res, cols, diags1, diags2);
|
||||
// 回退:將該格子恢復為空位
|
||||
state[row][col] = '#';
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 求解 n 皇后 */
|
||||
function nQueens(n: number): string[][][] {
|
||||
// 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
|
||||
const state = Array.from({ length: n }, () => Array(n).fill('#'));
|
||||
const cols = Array(n).fill(false); // 記錄列是否有皇后
|
||||
const diags1 = Array(2 * n - 1).fill(false); // 記錄主對角線上是否有皇后
|
||||
const diags2 = Array(2 * n - 1).fill(false); // 記錄次對角線上是否有皇后
|
||||
const res: string[][][] = [];
|
||||
|
||||
backtrack(0, n, state, res, cols, diags1, diags2);
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="n_queens.dart"
|
||||
/* 回溯演算法:n 皇后 */
|
||||
void backtrack(
|
||||
int row,
|
||||
int n,
|
||||
List<List<String>> state,
|
||||
List<List<List<String>>> res,
|
||||
List<bool> cols,
|
||||
List<bool> diags1,
|
||||
List<bool> diags2,
|
||||
) {
|
||||
// 當放置完所有行時,記錄解
|
||||
if (row == n) {
|
||||
List<List<String>> copyState = [];
|
||||
for (List<String> sRow in state) {
|
||||
copyState.add(List.from(sRow));
|
||||
}
|
||||
res.add(copyState);
|
||||
return;
|
||||
}
|
||||
// 走訪所有列
|
||||
for (int col = 0; col < n; col++) {
|
||||
// 計算該格子對應的主對角線和次對角線
|
||||
int diag1 = row - col + n - 1;
|
||||
int diag2 = row + col;
|
||||
// 剪枝:不允許該格子所在列、主對角線、次對角線上存在皇后
|
||||
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
|
||||
// 嘗試:將皇后放置在該格子
|
||||
state[row][col] = "Q";
|
||||
cols[col] = true;
|
||||
diags1[diag1] = true;
|
||||
diags2[diag2] = true;
|
||||
// 放置下一行
|
||||
backtrack(row + 1, n, state, res, cols, diags1, diags2);
|
||||
// 回退:將該格子恢復為空位
|
||||
state[row][col] = "#";
|
||||
cols[col] = false;
|
||||
diags1[diag1] = false;
|
||||
diags2[diag2] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 求解 n 皇后 */
|
||||
List<List<List<String>>> nQueens(int n) {
|
||||
// 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
|
||||
List<List<String>> state = List.generate(n, (index) => List.filled(n, "#"));
|
||||
List<bool> cols = List.filled(n, false); // 記錄列是否有皇后
|
||||
List<bool> diags1 = List.filled(2 * n - 1, false); // 記錄主對角線上是否有皇后
|
||||
List<bool> diags2 = List.filled(2 * n - 1, false); // 記錄次對角線上是否有皇后
|
||||
List<List<List<String>>> res = [];
|
||||
|
||||
backtrack(0, n, state, res, cols, diags1, diags2);
|
||||
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="n_queens.rs"
|
||||
/* 回溯演算法:n 皇后 */
|
||||
fn backtrack(
|
||||
row: usize,
|
||||
n: usize,
|
||||
state: &mut Vec<Vec<String>>,
|
||||
res: &mut Vec<Vec<Vec<String>>>,
|
||||
cols: &mut [bool],
|
||||
diags1: &mut [bool],
|
||||
diags2: &mut [bool],
|
||||
) {
|
||||
// 當放置完所有行時,記錄解
|
||||
if row == n {
|
||||
let mut copy_state: Vec<Vec<String>> = Vec::new();
|
||||
for s_row in state.clone() {
|
||||
copy_state.push(s_row);
|
||||
}
|
||||
res.push(copy_state);
|
||||
return;
|
||||
}
|
||||
// 走訪所有列
|
||||
for col in 0..n {
|
||||
// 計算該格子對應的主對角線和次對角線
|
||||
let diag1 = row + n - 1 - col;
|
||||
let diag2 = row + col;
|
||||
// 剪枝:不允許該格子所在列、主對角線、次對角線上存在皇后
|
||||
if !cols[col] && !diags1[diag1] && !diags2[diag2] {
|
||||
// 嘗試:將皇后放置在該格子
|
||||
state.get_mut(row).unwrap()[col] = "Q".into();
|
||||
(cols[col], diags1[diag1], diags2[diag2]) = (true, true, true);
|
||||
// 放置下一行
|
||||
backtrack(row + 1, n, state, res, cols, diags1, diags2);
|
||||
// 回退:將該格子恢復為空位
|
||||
state.get_mut(row).unwrap()[col] = "#".into();
|
||||
(cols[col], diags1[diag1], diags2[diag2]) = (false, false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 求解 n 皇后 */
|
||||
fn n_queens(n: usize) -> Vec<Vec<Vec<String>>> {
|
||||
// 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
|
||||
let mut state: Vec<Vec<String>> = Vec::new();
|
||||
for _ in 0..n {
|
||||
let mut row: Vec<String> = Vec::new();
|
||||
for _ in 0..n {
|
||||
row.push("#".into());
|
||||
}
|
||||
state.push(row);
|
||||
}
|
||||
let mut cols = vec![false; n]; // 記錄列是否有皇后
|
||||
let mut diags1 = vec![false; 2 * n - 1]; // 記錄主對角線上是否有皇后
|
||||
let mut diags2 = vec![false; 2 * n - 1]; // 記錄次對角線上是否有皇后
|
||||
let mut res: Vec<Vec<Vec<String>>> = Vec::new();
|
||||
|
||||
backtrack(
|
||||
0,
|
||||
n,
|
||||
&mut state,
|
||||
&mut res,
|
||||
&mut cols,
|
||||
&mut diags1,
|
||||
&mut diags2,
|
||||
);
|
||||
|
||||
res
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="n_queens.c"
|
||||
/* 回溯演算法:n 皇后 */
|
||||
void backtrack(int row, int n, char state[MAX_SIZE][MAX_SIZE], char ***res, int *resSize, bool cols[MAX_SIZE],
|
||||
bool diags1[2 * MAX_SIZE - 1], bool diags2[2 * MAX_SIZE - 1]) {
|
||||
// 當放置完所有行時,記錄解
|
||||
if (row == n) {
|
||||
res[*resSize] = (char **)malloc(sizeof(char *) * n);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
res[*resSize][i] = (char *)malloc(sizeof(char) * (n + 1));
|
||||
strcpy(res[*resSize][i], state[i]);
|
||||
}
|
||||
(*resSize)++;
|
||||
return;
|
||||
}
|
||||
// 走訪所有列
|
||||
for (int col = 0; col < n; col++) {
|
||||
// 計算該格子對應的主對角線和次對角線
|
||||
int diag1 = row - col + n - 1;
|
||||
int diag2 = row + col;
|
||||
// 剪枝:不允許該格子所在列、主對角線、次對角線上存在皇后
|
||||
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
|
||||
// 嘗試:將皇后放置在該格子
|
||||
state[row][col] = 'Q';
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = true;
|
||||
// 放置下一行
|
||||
backtrack(row + 1, n, state, res, resSize, cols, diags1, diags2);
|
||||
// 回退:將該格子恢復為空位
|
||||
state[row][col] = '#';
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 求解 n 皇后 */
|
||||
char ***nQueens(int n, int *returnSize) {
|
||||
char state[MAX_SIZE][MAX_SIZE];
|
||||
// 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
|
||||
for (int i = 0; i < n; ++i) {
|
||||
for (int j = 0; j < n; ++j) {
|
||||
state[i][j] = '#';
|
||||
}
|
||||
state[i][n] = '\0';
|
||||
}
|
||||
bool cols[MAX_SIZE] = {false}; // 記錄列是否有皇后
|
||||
bool diags1[2 * MAX_SIZE - 1] = {false}; // 記錄主對角線上是否有皇后
|
||||
bool diags2[2 * MAX_SIZE - 1] = {false}; // 記錄次對角線上是否有皇后
|
||||
|
||||
char ***res = (char ***)malloc(sizeof(char **) * MAX_SIZE);
|
||||
*returnSize = 0;
|
||||
backtrack(0, n, state, res, returnSize, cols, diags1, diags2);
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="n_queens.kt"
|
||||
/* 回溯演算法:n 皇后 */
|
||||
fun backtrack(
|
||||
row: Int,
|
||||
n: Int,
|
||||
state: List<MutableList<String>>,
|
||||
res: MutableList<List<List<String>>?>,
|
||||
cols: BooleanArray,
|
||||
diags1: BooleanArray,
|
||||
diags2: BooleanArray
|
||||
) {
|
||||
// 當放置完所有行時,記錄解
|
||||
if (row == n) {
|
||||
val copyState: MutableList<List<String>> = ArrayList()
|
||||
for (sRow in state) {
|
||||
copyState.add(ArrayList(sRow))
|
||||
}
|
||||
res.add(copyState)
|
||||
return
|
||||
}
|
||||
// 走訪所有列
|
||||
for (col in 0..<n) {
|
||||
// 計算該格子對應的主對角線和次對角線
|
||||
val diag1 = row - col + n - 1
|
||||
val diag2 = row + col
|
||||
// 剪枝:不允許該格子所在列、主對角線、次對角線上存在皇后
|
||||
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
|
||||
// 嘗試:將皇后放置在該格子
|
||||
state[row][col] = "Q"
|
||||
diags2[diag2] = true
|
||||
diags1[diag1] = diags2[diag2]
|
||||
cols[col] = diags1[diag1]
|
||||
// 放置下一行
|
||||
backtrack(row + 1, n, state, res, cols, diags1, diags2)
|
||||
// 回退:將該格子恢復為空位
|
||||
state[row][col] = "#"
|
||||
diags2[diag2] = false
|
||||
diags1[diag1] = diags2[diag2]
|
||||
cols[col] = diags1[diag1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 求解 n 皇后 */
|
||||
fun nQueens(n: Int): List<List<List<String>>?> {
|
||||
// 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位
|
||||
val state: MutableList<MutableList<String>> = ArrayList()
|
||||
for (i in 0..<n) {
|
||||
val row: MutableList<String> = ArrayList()
|
||||
for (j in 0..<n) {
|
||||
row.add("#")
|
||||
}
|
||||
state.add(row)
|
||||
}
|
||||
val cols = BooleanArray(n) // 記錄列是否有皇后
|
||||
val diags1 = BooleanArray(2 * n - 1) // 記錄主對角線上是否有皇后
|
||||
val diags2 = BooleanArray(2 * n - 1) // 記錄次對角線上是否有皇后
|
||||
val res: MutableList<List<List<String>>?> = ArrayList()
|
||||
|
||||
backtrack(0, n, state, res, cols, diags1, diags2)
|
||||
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="n_queens.rb"
|
||||
[class]{}-[func]{backtrack}
|
||||
|
||||
[class]{}-[func]{n_queens}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="n_queens.zig"
|
||||
[class]{}-[func]{backtrack}
|
||||
|
||||
[class]{}-[func]{nQueens}
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20backtrack%28%0A%20%20%20%20row%3A%20int,%0A%20%20%20%20n%3A%20int,%0A%20%20%20%20state%3A%20list%5Blist%5Bstr%5D%5D,%0A%20%20%20%20res%3A%20list%5Blist%5Blist%5Bstr%5D%5D%5D,%0A%20%20%20%20cols%3A%20list%5Bbool%5D,%0A%20%20%20%20diags1%3A%20list%5Bbool%5D,%0A%20%20%20%20diags2%3A%20list%5Bbool%5D,%0A%29%3A%0A%20%20%20%20%22%22%22%E5%9B%9E%E6%BA%AF%E7%AE%97%E6%B3%95%EF%BC%9AN%20%E7%9A%87%E5%90%8E%22%22%22%0A%20%20%20%20%23%20%E5%BD%93%E6%94%BE%E7%BD%AE%E5%AE%8C%E6%89%80%E6%9C%89%E8%A1%8C%E6%97%B6%EF%BC%8C%E8%AE%B0%E5%BD%95%E8%A7%A3%0A%20%20%20%20if%20row%20%3D%3D%20n%3A%0A%20%20%20%20%20%20%20%20res.append%28%5Blist%28row%29%20for%20row%20in%20state%5D%29%0A%20%20%20%20%20%20%20%20return%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E6%89%80%E6%9C%89%E5%88%97%0A%20%20%20%20for%20col%20in%20range%28n%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E8%AE%A1%E7%AE%97%E8%AF%A5%E6%A0%BC%E5%AD%90%E5%AF%B9%E5%BA%94%E7%9A%84%E4%B8%BB%E5%AF%B9%E8%A7%92%E7%BA%BF%E5%92%8C%E6%AC%A1%E5%AF%B9%E8%A7%92%E7%BA%BF%0A%20%20%20%20%20%20%20%20diag1%20%3D%20row%20-%20col%20%2B%20n%20-%201%0A%20%20%20%20%20%20%20%20diag2%20%3D%20row%20%2B%20col%0A%20%20%20%20%20%20%20%20%23%20%E5%89%AA%E6%9E%9D%EF%BC%9A%E4%B8%8D%E5%85%81%E8%AE%B8%E8%AF%A5%E6%A0%BC%E5%AD%90%E6%89%80%E5%9C%A8%E5%88%97%E3%80%81%E4%B8%BB%E5%AF%B9%E8%A7%92%E7%BA%BF%E3%80%81%E6%AC%A1%E5%AF%B9%E8%A7%92%E7%BA%BF%E4%B8%8A%E5%AD%98%E5%9C%A8%E7%9A%87%E5%90%8E%0A%20%20%20%20%20%20%20%20if%20not%20cols%5Bcol%5D%20and%20not%20diags1%5Bdiag1%5D%20and%20not%20diags2%5Bdiag2%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E5%B0%9D%E8%AF%95%EF%BC%9A%E5%B0%86%E7%9A%87%E5%90%8E%E6%94%BE%E7%BD%AE%E5%9C%A8%E8%AF%A5%E6%A0%BC%E5%AD%90%0A%20%20%20%20%20%20%20%20%20%20%20%20state%5Brow%5D%5Bcol%5D%20%3D%20%22Q%22%0A%20%20%20%20%20%20%20%20%20%20%20%20cols%5Bcol%5D%20%3D%20diags1%5Bdiag1%5D%20%3D%20diags2%5Bdiag2%5D%20%3D%20True%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E6%94%BE%E7%BD%AE%E4%B8%8B%E4%B8%80%E8%A1%8C%0A%20%20%20%20%20%20%20%20%20%20%20%20backtrack%28row%20%2B%201,%20n,%20state,%20res,%20cols,%20diags1,%20diags2%29%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E5%9B%9E%E9%80%80%EF%BC%9A%E5%B0%86%E8%AF%A5%E6%A0%BC%E5%AD%90%E6%81%A2%E5%A4%8D%E4%B8%BA%E7%A9%BA%E4%BD%8D%0A%20%20%20%20%20%20%20%20%20%20%20%20state%5Brow%5D%5Bcol%5D%20%3D%20%22%23%22%0A%20%20%20%20%20%20%20%20%20%20%20%20cols%5Bcol%5D%20%3D%20diags1%5Bdiag1%5D%20%3D%20diags2%5Bdiag2%5D%20%3D%20False%0A%0A%0Adef%20n_queens%28n%3A%20int%29%20-%3E%20list%5Blist%5Blist%5Bstr%5D%5D%5D%3A%0A%20%20%20%20%22%22%22%E6%B1%82%E8%A7%A3%20N%20%E7%9A%87%E5%90%8E%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%20n*n%20%E5%A4%A7%E5%B0%8F%E7%9A%84%E6%A3%8B%E7%9B%98%EF%BC%8C%E5%85%B6%E4%B8%AD%20'Q'%20%E4%BB%A3%E8%A1%A8%E7%9A%87%E5%90%8E%EF%BC%8C'%23'%20%E4%BB%A3%E8%A1%A8%E7%A9%BA%E4%BD%8D%0A%20%20%20%20state%20%3D%20%5B%5B%22%23%22%20for%20_%20in%20range%28n%29%5D%20for%20_%20in%20range%28n%29%5D%0A%20%20%20%20cols%20%3D%20%5BFalse%5D%20*%20n%20%20%23%20%E8%AE%B0%E5%BD%95%E5%88%97%E6%98%AF%E5%90%A6%E6%9C%89%E7%9A%87%E5%90%8E%0A%20%20%20%20diags1%20%3D%20%5BFalse%5D%20*%20%282%20*%20n%20-%201%29%20%20%23%20%E8%AE%B0%E5%BD%95%E4%B8%BB%E5%AF%B9%E8%A7%92%E7%BA%BF%E4%B8%8A%E6%98%AF%E5%90%A6%E6%9C%89%E7%9A%87%E5%90%8E%0A%20%20%20%20diags2%20%3D%20%5BFalse%5D%20*%20%282%20*%20n%20-%201%29%20%20%23%20%E8%AE%B0%E5%BD%95%E6%AC%A1%E5%AF%B9%E8%A7%92%E7%BA%BF%E4%B8%8A%E6%98%AF%E5%90%A6%E6%9C%89%E7%9A%87%E5%90%8E%0A%20%20%20%20res%20%3D%20%5B%5D%0A%20%20%20%20backtrack%280,%20n,%20state,%20res,%20cols,%20diags1,%20diags2%29%0A%0A%20%20%20%20return%20res%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%204%0A%20%20%20%20res%20%3D%20n_queens%28n%29%0A%0A%20%20%20%20print%28f%22%E8%BE%93%E5%85%A5%E6%A3%8B%E7%9B%98%E9%95%BF%E5%AE%BD%E4%B8%BA%20%7Bn%7D%22%29%0A%20%20%20%20print%28f%22%E7%9A%87%E5%90%8E%E6%94%BE%E7%BD%AE%E6%96%B9%E6%A1%88%E5%85%B1%E6%9C%89%20%7Blen%28res%29%7D%20%E7%A7%8D%22%29%0A%20%20%20%20for%20state%20in%20res%3A%0A%20%20%20%20%20%20%20%20print%28%22--------------------%22%29%0A%20%20%20%20%20%20%20%20for%20row%20in%20state%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20print%28row%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=61&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20backtrack%28%0A%20%20%20%20row%3A%20int,%0A%20%20%20%20n%3A%20int,%0A%20%20%20%20state%3A%20list%5Blist%5Bstr%5D%5D,%0A%20%20%20%20res%3A%20list%5Blist%5Blist%5Bstr%5D%5D%5D,%0A%20%20%20%20cols%3A%20list%5Bbool%5D,%0A%20%20%20%20diags1%3A%20list%5Bbool%5D,%0A%20%20%20%20diags2%3A%20list%5Bbool%5D,%0A%29%3A%0A%20%20%20%20%22%22%22%E5%9B%9E%E6%BA%AF%E7%AE%97%E6%B3%95%EF%BC%9AN%20%E7%9A%87%E5%90%8E%22%22%22%0A%20%20%20%20%23%20%E5%BD%93%E6%94%BE%E7%BD%AE%E5%AE%8C%E6%89%80%E6%9C%89%E8%A1%8C%E6%97%B6%EF%BC%8C%E8%AE%B0%E5%BD%95%E8%A7%A3%0A%20%20%20%20if%20row%20%3D%3D%20n%3A%0A%20%20%20%20%20%20%20%20res.append%28%5Blist%28row%29%20for%20row%20in%20state%5D%29%0A%20%20%20%20%20%20%20%20return%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E6%89%80%E6%9C%89%E5%88%97%0A%20%20%20%20for%20col%20in%20range%28n%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E8%AE%A1%E7%AE%97%E8%AF%A5%E6%A0%BC%E5%AD%90%E5%AF%B9%E5%BA%94%E7%9A%84%E4%B8%BB%E5%AF%B9%E8%A7%92%E7%BA%BF%E5%92%8C%E6%AC%A1%E5%AF%B9%E8%A7%92%E7%BA%BF%0A%20%20%20%20%20%20%20%20diag1%20%3D%20row%20-%20col%20%2B%20n%20-%201%0A%20%20%20%20%20%20%20%20diag2%20%3D%20row%20%2B%20col%0A%20%20%20%20%20%20%20%20%23%20%E5%89%AA%E6%9E%9D%EF%BC%9A%E4%B8%8D%E5%85%81%E8%AE%B8%E8%AF%A5%E6%A0%BC%E5%AD%90%E6%89%80%E5%9C%A8%E5%88%97%E3%80%81%E4%B8%BB%E5%AF%B9%E8%A7%92%E7%BA%BF%E3%80%81%E6%AC%A1%E5%AF%B9%E8%A7%92%E7%BA%BF%E4%B8%8A%E5%AD%98%E5%9C%A8%E7%9A%87%E5%90%8E%0A%20%20%20%20%20%20%20%20if%20not%20cols%5Bcol%5D%20and%20not%20diags1%5Bdiag1%5D%20and%20not%20diags2%5Bdiag2%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E5%B0%9D%E8%AF%95%EF%BC%9A%E5%B0%86%E7%9A%87%E5%90%8E%E6%94%BE%E7%BD%AE%E5%9C%A8%E8%AF%A5%E6%A0%BC%E5%AD%90%0A%20%20%20%20%20%20%20%20%20%20%20%20state%5Brow%5D%5Bcol%5D%20%3D%20%22Q%22%0A%20%20%20%20%20%20%20%20%20%20%20%20cols%5Bcol%5D%20%3D%20diags1%5Bdiag1%5D%20%3D%20diags2%5Bdiag2%5D%20%3D%20True%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E6%94%BE%E7%BD%AE%E4%B8%8B%E4%B8%80%E8%A1%8C%0A%20%20%20%20%20%20%20%20%20%20%20%20backtrack%28row%20%2B%201,%20n,%20state,%20res,%20cols,%20diags1,%20diags2%29%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E5%9B%9E%E9%80%80%EF%BC%9A%E5%B0%86%E8%AF%A5%E6%A0%BC%E5%AD%90%E6%81%A2%E5%A4%8D%E4%B8%BA%E7%A9%BA%E4%BD%8D%0A%20%20%20%20%20%20%20%20%20%20%20%20state%5Brow%5D%5Bcol%5D%20%3D%20%22%23%22%0A%20%20%20%20%20%20%20%20%20%20%20%20cols%5Bcol%5D%20%3D%20diags1%5Bdiag1%5D%20%3D%20diags2%5Bdiag2%5D%20%3D%20False%0A%0A%0Adef%20n_queens%28n%3A%20int%29%20-%3E%20list%5Blist%5Blist%5Bstr%5D%5D%5D%3A%0A%20%20%20%20%22%22%22%E6%B1%82%E8%A7%A3%20N%20%E7%9A%87%E5%90%8E%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%20n*n%20%E5%A4%A7%E5%B0%8F%E7%9A%84%E6%A3%8B%E7%9B%98%EF%BC%8C%E5%85%B6%E4%B8%AD%20'Q'%20%E4%BB%A3%E8%A1%A8%E7%9A%87%E5%90%8E%EF%BC%8C'%23'%20%E4%BB%A3%E8%A1%A8%E7%A9%BA%E4%BD%8D%0A%20%20%20%20state%20%3D%20%5B%5B%22%23%22%20for%20_%20in%20range%28n%29%5D%20for%20_%20in%20range%28n%29%5D%0A%20%20%20%20cols%20%3D%20%5BFalse%5D%20*%20n%20%20%23%20%E8%AE%B0%E5%BD%95%E5%88%97%E6%98%AF%E5%90%A6%E6%9C%89%E7%9A%87%E5%90%8E%0A%20%20%20%20diags1%20%3D%20%5BFalse%5D%20*%20%282%20*%20n%20-%201%29%20%20%23%20%E8%AE%B0%E5%BD%95%E4%B8%BB%E5%AF%B9%E8%A7%92%E7%BA%BF%E4%B8%8A%E6%98%AF%E5%90%A6%E6%9C%89%E7%9A%87%E5%90%8E%0A%20%20%20%20diags2%20%3D%20%5BFalse%5D%20*%20%282%20*%20n%20-%201%29%20%20%23%20%E8%AE%B0%E5%BD%95%E6%AC%A1%E5%AF%B9%E8%A7%92%E7%BA%BF%E4%B8%8A%E6%98%AF%E5%90%A6%E6%9C%89%E7%9A%87%E5%90%8E%0A%20%20%20%20res%20%3D%20%5B%5D%0A%20%20%20%20backtrack%280,%20n,%20state,%20res,%20cols,%20diags1,%20diags2%29%0A%0A%20%20%20%20return%20res%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%204%0A%20%20%20%20res%20%3D%20n_queens%28n%29%0A%0A%20%20%20%20print%28f%22%E8%BE%93%E5%85%A5%E6%A3%8B%E7%9B%98%E9%95%BF%E5%AE%BD%E4%B8%BA%20%7Bn%7D%22%29%0A%20%20%20%20print%28f%22%E7%9A%87%E5%90%8E%E6%94%BE%E7%BD%AE%E6%96%B9%E6%A1%88%E5%85%B1%E6%9C%89%20%7Blen%28res%29%7D%20%E7%A7%8D%22%29%0A%20%20%20%20for%20state%20in%20res%3A%0A%20%20%20%20%20%20%20%20print%28%22--------------------%22%29%0A%20%20%20%20%20%20%20%20for%20row%20in%20state%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20print%28row%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=61&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
逐行放置 $n$ 次,考慮列約束,則從第一行到最後一行分別有 $n$、$n-1$、$\dots$、$2$、$1$ 個選擇,使用 $O(n!)$ 時間。當記錄解時,需要複製矩陣 `state` 並新增進 `res` ,複製操作使用 $O(n^2)$ 時間。因此,**總體時間複雜度為 $O(n! \cdot n^2)$** 。實際上,根據對角線約束的剪枝也能夠大幅縮小搜尋空間,因而搜尋效率往往優於以上時間複雜度。
|
||||
|
||||
陣列 `state` 使用 $O(n^2)$ 空間,陣列 `cols`、`diags1` 和 `diags2` 皆使用 $O(n)$ 空間。最大遞迴深度為 $n$ ,使用 $O(n)$ 堆疊幀空間。因此,**空間複雜度為 $O(n^2)$** 。
|
||||
1068
zh-Hant/docs/chapter_backtracking/permutations_problem.md
Normal file
1068
zh-Hant/docs/chapter_backtracking/permutations_problem.md
Normal file
File diff suppressed because it is too large
Load Diff
1623
zh-Hant/docs/chapter_backtracking/subset_sum_problem.md
Normal file
1623
zh-Hant/docs/chapter_backtracking/subset_sum_problem.md
Normal file
File diff suppressed because it is too large
Load Diff
27
zh-Hant/docs/chapter_backtracking/summary.md
Normal file
27
zh-Hant/docs/chapter_backtracking/summary.md
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 13.5 小結
|
||||
|
||||
### 1. 重點回顧
|
||||
|
||||
- 回溯演算法本質是窮舉法,透過對解空間進行深度優先走訪來尋找符合條件的解。在搜尋過程中,遇到滿足條件的解則記錄,直至找到所有解或走訪完成後結束。
|
||||
- 回溯演算法的搜尋過程包括嘗試與回退兩個部分。它透過深度優先搜尋來嘗試各種選擇,當遇到不滿足約束條件的情況時,則撤銷上一步的選擇,退回到之前的狀態,並繼續嘗試其他選擇。嘗試與回退是兩個方向相反的操作。
|
||||
- 回溯問題通常包含多個約束條件,它們可用於實現剪枝操作。剪枝可以提前結束不必要的搜尋分支,大幅提升搜尋效率。
|
||||
- 回溯演算法主要可用於解決搜尋問題和約束滿足問題。組合最佳化問題雖然可以用回溯演算法解決,但往往存在效率更高或效果更好的解法。
|
||||
- 全排列問題旨在搜尋給定集合元素的所有可能的排列。我們藉助一個陣列來記錄每個元素是否被選擇,剪掉重複選擇同一元素的搜尋分支,確保每個元素只被選擇一次。
|
||||
- 在全排列問題中,如果集合中存在重複元素,則最終結果會出現重複排列。我們需要約束相等元素在每輪中只能被選擇一次,這通常藉助一個雜湊表來實現。
|
||||
- 子集和問題的目標是在給定集合中找到和為目標值的所有子集。集合不區分元素順序,而搜尋過程會輸出所有順序的結果,產生重複子集。我們在回溯前將資料進行排序,並設定一個變數來指示每一輪的走訪起始點,從而將生成重複子集的搜尋分支進行剪枝。
|
||||
- 對於子集和問題,陣列中的相等元素會產生重複集合。我們利用陣列已排序的前置條件,透過判斷相鄰元素是否相等實現剪枝,從而確保相等元素在每輪中只能被選中一次。
|
||||
- $n$ 皇后問題旨在尋找將 $n$ 個皇后放置到 $n \times n$ 尺寸棋盤上的方案,要求所有皇后兩兩之間無法攻擊對方。該問題的約束條件有行約束、列約束、主對角線和次對角線約束。為滿足行約束,我們採用按行放置的策略,保證每一行放置一個皇后。
|
||||
- 列約束和對角線約束的處理方式類似。對於列約束,我們利用一個陣列來記錄每一列是否有皇后,從而指示選中的格子是否合法。對於對角線約束,我們藉助兩個陣列來分別記錄該主、次對角線上是否存在皇后;難點在於找處在到同一主(副)對角線上格子滿足的行列索引規律。
|
||||
|
||||
### 2. Q & A
|
||||
|
||||
**Q**:怎麼理解回溯和遞迴的關係?
|
||||
|
||||
總的來看,回溯是一種“演算法策略”,而遞迴更像是一個“工具”。
|
||||
|
||||
- 回溯演算法通常基於遞迴實現。然而,回溯是遞迴的應用場景之一,是遞迴在搜尋問題中的應用。
|
||||
- 遞迴的結構體現了“子問題分解”的解題範式,常用於解決分治、回溯、動態規劃(記憶化遞迴)等問題。
|
||||
22
zh-Hant/docs/chapter_computational_complexity/index.md
Normal file
22
zh-Hant/docs/chapter_computational_complexity/index.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/timer-sand
|
||||
---
|
||||
|
||||
# 第 2 章 複雜度分析
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
複雜度分析猶如浩瀚的演算法宇宙中的時空嚮導。
|
||||
|
||||
它帶領我們在時間與空間這兩個維度上深入探索,尋找更優雅的解決方案。
|
||||
|
||||
## Chapter Contents
|
||||
|
||||
- [2.1 演算法效率評估](https://www.hello-algo.com/en/chapter_computational_complexity/performance_evaluation/)
|
||||
- [2.2 迭代與遞迴](https://www.hello-algo.com/en/chapter_computational_complexity/iteration_and_recursion/)
|
||||
- [2.3 時間複雜度](https://www.hello-algo.com/en/chapter_computational_complexity/time_complexity/)
|
||||
- [2.4 空間複雜度](https://www.hello-algo.com/en/chapter_computational_complexity/space_complexity/)
|
||||
- [2.5 小結](https://www.hello-algo.com/en/chapter_computational_complexity/summary/)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,52 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 2.1 演算法效率評估
|
||||
|
||||
在演算法設計中,我們先後追求以下兩個層面的目標。
|
||||
|
||||
1. **找到問題解法**:演算法需要在規定的輸入範圍內可靠地求得問題的正確解。
|
||||
2. **尋求最優解法**:同一個問題可能存在多種解法,我們希望找到儘可能高效的演算法。
|
||||
|
||||
也就是說,在能夠解決問題的前提下,演算法效率已成為衡量演算法優劣的主要評價指標,它包括以下兩個維度。
|
||||
|
||||
- **時間效率**:演算法執行速度的快慢。
|
||||
- **空間效率**:演算法佔用記憶體空間的大小。
|
||||
|
||||
簡而言之,**我們的目標是設計“既快又省”的資料結構與演算法**。而有效地評估演算法效率至關重要,因為只有這樣,我們才能將各種演算法進行對比,進而指導演算法設計與最佳化過程。
|
||||
|
||||
效率評估方法主要分為兩種:實際測試、理論估算。
|
||||
|
||||
## 2.1.1 實際測試
|
||||
|
||||
假設我們現在有演算法 `A` 和演算法 `B` ,它們都能解決同一問題,現在需要對比這兩個演算法的效率。最直接的方法是找一臺計算機,執行這兩個演算法,並監控記錄它們的執行時間和記憶體佔用情況。這種評估方式能夠反映真實情況,但也存在較大的侷限性。
|
||||
|
||||
一方面,**難以排除測試環境的干擾因素**。硬體配置會影響演算法的效能。比如在某臺計算機中,演算法 `A` 的執行時間比演算法 `B` 短;但在另一臺配置不同的計算機中,可能得到相反的測試結果。這意味著我們需要在各種機器上進行測試,統計平均效率,而這是不現實的。
|
||||
|
||||
另一方面,**展開完整測試非常耗費資源**。隨著輸入資料量的變化,演算法會表現出不同的效率。例如,在輸入資料量較小時,演算法 `A` 的執行時間比演算法 `B` 短;而在輸入資料量較大時,測試結果可能恰恰相反。因此,為了得到有說服力的結論,我們需要測試各種規模的輸入資料,而這需要耗費大量的計算資源。
|
||||
|
||||
## 2.1.2 理論估算
|
||||
|
||||
由於實際測試具有較大的侷限性,因此我們可以考慮僅透過一些計算來評估演算法的效率。這種估算方法被稱為<u>漸近複雜度分析(asymptotic complexity analysis)</u>,簡稱<u>複雜度分析</u>。
|
||||
|
||||
複雜度分析能夠體現演算法執行所需的時間和空間資源與輸入資料大小之間的關係。**它描述了隨著輸入資料大小的增加,演算法執行所需時間和空間的增長趨勢**。這個定義有些拗口,我們可以將其分為三個重點來理解。
|
||||
|
||||
- “時間和空間資源”分別對應<u>時間複雜度(time complexity)</u>和<u>空間複雜度(space complexity)</u>。
|
||||
- “隨著輸入資料大小的增加”意味著複雜度反映了演算法執行效率與輸入資料體量之間的關係。
|
||||
- “時間和空間的增長趨勢”表示複雜度分析關注的不是執行時間或佔用空間的具體值,而是時間或空間增長的“快慢”。
|
||||
|
||||
**複雜度分析克服了實際測試方法的弊端**,體現在以下兩個方面。
|
||||
|
||||
- 它獨立於測試環境,分析結果適用於所有執行平臺。
|
||||
- 它可以體現不同資料量下的演算法效率,尤其是在大資料量下的演算法效能。
|
||||
|
||||
!!! tip
|
||||
|
||||
如果你仍對複雜度的概念感到困惑,無須擔心,我們會在後續章節中詳細介紹。
|
||||
|
||||
複雜度分析為我們提供了一把評估演算法效率的“標尺”,使我們可以衡量執行某個演算法所需的時間和空間資源,對比不同演算法之間的效率。
|
||||
|
||||
複雜度是個數學概念,對於初學者可能比較抽象,學習難度相對較高。從這個角度看,複雜度分析可能不太適合作為最先介紹的內容。然而,當我們討論某個資料結構或演算法的特點時,難以避免要分析其執行速度和空間使用情況。
|
||||
|
||||
綜上所述,建議你在深入學習資料結構與演算法之前,**先對複雜度分析建立初步的瞭解,以便能夠完成簡單演算法的複雜度分析**。
|
||||
2384
zh-Hant/docs/chapter_computational_complexity/space_complexity.md
Executable file
2384
zh-Hant/docs/chapter_computational_complexity/space_complexity.md
Executable file
File diff suppressed because it is too large
Load Diff
53
zh-Hant/docs/chapter_computational_complexity/summary.md
Normal file
53
zh-Hant/docs/chapter_computational_complexity/summary.md
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 2.5 小結
|
||||
|
||||
### 1. 重點回顧
|
||||
|
||||
**演算法效率評估**
|
||||
|
||||
- 時間效率和空間效率是衡量演算法優劣的兩個主要評價指標。
|
||||
- 我們可以透過實際測試來評估演算法效率,但難以消除測試環境的影響,且會耗費大量計算資源。
|
||||
- 複雜度分析可以消除實際測試的弊端,分析結果適用於所有執行平臺,並且能夠揭示演算法在不同資料規模下的效率。
|
||||
|
||||
**時間複雜度**
|
||||
|
||||
- 時間複雜度用於衡量演算法執行時間隨資料量增長的趨勢,可以有效評估演算法效率,但在某些情況下可能失效,如在輸入的資料量較小或時間複雜度相同時,無法精確對比演算法效率的優劣。
|
||||
- 最差時間複雜度使用大 $O$ 符號表示,對應函式漸近上界,反映當 $n$ 趨向正無窮時,操作數量 $T(n)$ 的增長級別。
|
||||
- 推算時間複雜度分為兩步,首先統計操作數量,然後判斷漸近上界。
|
||||
- 常見時間複雜度從低到高排列有 $O(1)$、$O(\log n)$、$O(n)$、$O(n \log n)$、$O(n^2)$、$O(2^n)$ 和 $O(n!)$ 等。
|
||||
- 某些演算法的時間複雜度非固定,而是與輸入資料的分佈有關。時間複雜度分為最差、最佳、平均時間複雜度,最佳時間複雜度幾乎不用,因為輸入資料一般需要滿足嚴格條件才能達到最佳情況。
|
||||
- 平均時間複雜度反映演算法在隨機資料輸入下的執行效率,最接近實際應用中的演算法效能。計算平均時間複雜度需要統計輸入資料分佈以及綜合後的數學期望。
|
||||
|
||||
**空間複雜度**
|
||||
|
||||
- 空間複雜度的作用類似於時間複雜度,用於衡量演算法佔用記憶體空間隨資料量增長的趨勢。
|
||||
- 演算法執行過程中的相關記憶體空間可分為輸入空間、暫存空間、輸出空間。通常情況下,輸入空間不納入空間複雜度計算。暫存空間可分為暫存資料、堆疊幀空間和指令空間,其中堆疊幀空間通常僅在遞迴函式中影響空間複雜度。
|
||||
- 我們通常只關注最差空間複雜度,即統計演算法在最差輸入資料和最差執行時刻下的空間複雜度。
|
||||
- 常見空間複雜度從低到高排列有 $O(1)$、$O(\log n)$、$O(n)$、$O(n^2)$ 和 $O(2^n)$ 等。
|
||||
|
||||
### 2. Q & A
|
||||
|
||||
**Q**:尾遞迴的空間複雜度是 $O(1)$ 嗎?
|
||||
|
||||
理論上,尾遞迴函式的空間複雜度可以最佳化至 $O(1)$ 。不過絕大多數程式語言(例如 Java、Python、C++、Go、C# 等)不支持自動最佳化尾遞迴,因此通常認為空間複雜度是 $O(n)$ 。
|
||||
|
||||
**Q**:函式和方法這兩個術語的區別是什麼?
|
||||
|
||||
<u>函式(function)</u>可以被獨立執行,所有參數都以顯式傳遞。<u>方法(method)</u>與一個物件關聯,被隱式傳遞給呼叫它的物件,能夠對類別的例項中包含的資料進行操作。
|
||||
|
||||
下面以幾種常見的程式語言為例來說明。
|
||||
|
||||
- C 語言是程序式程式設計語言,沒有物件導向的概念,所以只有函式。但我們可以透過建立結構體(struct)來模擬物件導向程式設計,與結構體相關聯的函式就相當於其他程式語言中的方法。
|
||||
- Java 和 C# 是物件導向的程式語言,程式碼塊(方法)通常作為某個類別的一部分。靜態方法的行為類似於函式,因為它被繫結在類別上,不能訪問特定的例項變數。
|
||||
- C++ 和 Python 既支持程序式程式設計(函式),也支持物件導向程式設計(方法)。
|
||||
|
||||
**Q**:圖解“常見的空間複雜度型別”反映的是否是佔用空間的絕對大小?
|
||||
|
||||
不是,該圖展示的是空間複雜度,其反映的是增長趨勢,而不是佔用空間的絕對大小。
|
||||
|
||||
假設取 $n = 8$ ,你可能會發現每條曲線的值與函式對應不上。這是因為每條曲線都包含一個常數項,用於將取值範圍壓縮到一個視覺舒適的範圍內。
|
||||
|
||||
在實際中,因為我們通常不知道每個方法的“常數項”複雜度是多少,所以一般無法僅憑複雜度來選擇 $n = 8$ 之下的最優解法。但對於 $n = 8^5$ 就很好選了,這時增長趨勢已經佔主導了。
|
||||
3950
zh-Hant/docs/chapter_computational_complexity/time_complexity.md
Executable file
3950
zh-Hant/docs/chapter_computational_complexity/time_complexity.md
Executable file
File diff suppressed because it is too large
Load Diff
189
zh-Hant/docs/chapter_data_structure/basic_data_types.md
Normal file
189
zh-Hant/docs/chapter_data_structure/basic_data_types.md
Normal file
@@ -0,0 +1,189 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 3.2 基本資料型別
|
||||
|
||||
當談及計算機中的資料時,我們會想到文字、圖片、影片、語音、3D 模型等各種形式。儘管這些資料的組織形式各異,但它們都由各種基本資料型別構成。
|
||||
|
||||
**基本資料型別是 CPU 可以直接進行運算的型別**,在演算法中直接被使用,主要包括以下幾種。
|
||||
|
||||
- 整數型別 `byte`、`short`、`int`、`long` 。
|
||||
- 浮點數型別 `float`、`double` ,用於表示小數。
|
||||
- 字元型別 `char` ,用於表示各種語言的字母、標點符號甚至表情符號等。
|
||||
- 布林型別 `bool` ,用於表示“是”與“否”判斷。
|
||||
|
||||
**基本資料型別以二進位制的形式儲存在計算機中**。一個二進位制位即為 $1$ 位元。在絕大多數現代作業系統中,$1$ 位元組(byte)由 $8$ 位元(bit)組成。
|
||||
|
||||
基本資料型別的取值範圍取決於其佔用的空間大小。下面以 Java 為例。
|
||||
|
||||
- 整數型別 `byte` 佔用 $1$ 位元組 = $8$ 位元 ,可以表示 $2^{8}$ 個數字。
|
||||
- 整數型別 `int` 佔用 $4$ 位元組 = $32$ 位元 ,可以表示 $2^{32}$ 個數字。
|
||||
|
||||
表 3-1 列舉了 Java 中各種基本資料型別的佔用空間、取值範圍和預設值。此表格無須死記硬背,大致理解即可,需要時可以透過查表來回憶。
|
||||
|
||||
<p align="center"> 表 3-1 基本資料型別的佔用空間和取值範圍 </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
| 型別 | 符號 | 佔用空間 | 最小值 | 最大值 | 預設值 |
|
||||
| ------ | -------- | -------- | ------------------------ | ----------------------- | -------------- |
|
||||
| 整數 | `byte` | 1 位元組 | $-2^7$ ($-128$) | $2^7 - 1$ ($127$) | $0$ |
|
||||
| | `short` | 2 位元組 | $-2^{15}$ | $2^{15} - 1$ | $0$ |
|
||||
| | `int` | 4 位元組 | $-2^{31}$ | $2^{31} - 1$ | $0$ |
|
||||
| | `long` | 8 位元組 | $-2^{63}$ | $2^{63} - 1$ | $0$ |
|
||||
| 浮點數 | `float` | 4 位元組 | $1.175 \times 10^{-38}$ | $3.403 \times 10^{38}$ | $0.0\text{f}$ |
|
||||
| | `double` | 8 位元組 | $2.225 \times 10^{-308}$ | $1.798 \times 10^{308}$ | $0.0$ |
|
||||
| 字元 | `char` | 2 位元組 | $0$ | $2^{16} - 1$ | $0$ |
|
||||
| 布林 | `bool` | 1 位元組 | $\text{false}$ | $\text{true}$ | $\text{false}$ |
|
||||
|
||||
</div>
|
||||
|
||||
請注意,表 3-1 針對的是 Java 的基本資料型別的情況。每種程式語言都有各自的資料型別定義,它們的佔用空間、取值範圍和預設值可能會有所不同。
|
||||
|
||||
- 在 Python 中,整數型別 `int` 可以是任意大小,只受限於可用記憶體;浮點數 `float` 是雙精度 64 位;沒有 `char` 型別,單個字元實際上是長度為 1 的字串 `str` 。
|
||||
- C 和 C++ 未明確規定基本資料型別的大小,而因實現和平臺各異。表 3-1 遵循 LP64 [資料模型](https://en.cppreference.com/w/cpp/language/types#Properties),其用於包括 Linux 和 macOS 在內的 Unix 64 位作業系統。
|
||||
- 字元 `char` 的大小在 C 和 C++ 中為 1 位元組,在大多數程式語言中取決於特定的字元編碼方法,詳見“字元編碼”章節。
|
||||
- 即使表示布林量僅需 1 位($0$ 或 $1$),它在記憶體中通常也儲存為 1 位元組。這是因為現代計算機 CPU 通常將 1 位元組作為最小定址記憶體單元。
|
||||
|
||||
那麼,基本資料型別與資料結構之間有什麼關聯呢?我們知道,資料結構是在計算機中組織與儲存資料的方式。這句話的主語是“結構”而非“資料”。
|
||||
|
||||
如果想表示“一排數字”,我們自然會想到使用陣列。這是因為陣列的線性結構可以表示數字的相鄰關係和順序關係,但至於儲存的內容是整數 `int`、小數 `float` 還是字元 `char` ,則與“資料結構”無關。
|
||||
|
||||
換句話說,**基本資料型別提供了資料的“內容型別”,而資料結構提供了資料的“組織方式”**。例如以下程式碼,我們用相同的資料結構(陣列)來儲存與表示不同的基本資料型別,包括 `int`、`float`、`char`、`bool` 等。
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title=""
|
||||
# 使用多種基本資料型別來初始化陣列
|
||||
numbers: list[int] = [0] * 5
|
||||
decimals: list[float] = [0.0] * 5
|
||||
# Python 的字元實際上是長度為 1 的字串
|
||||
characters: list[str] = ['0'] * 5
|
||||
bools: list[bool] = [False] * 5
|
||||
# Python 的串列可以自由儲存各種基本資料型別和物件引用
|
||||
data = [0, 0.0, 'a', False, ListNode(0)]
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title=""
|
||||
// 使用多種基本資料型別來初始化陣列
|
||||
int numbers[5];
|
||||
float decimals[5];
|
||||
char characters[5];
|
||||
bool bools[5];
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title=""
|
||||
// 使用多種基本資料型別來初始化陣列
|
||||
int[] numbers = new int[5];
|
||||
float[] decimals = new float[5];
|
||||
char[] characters = new char[5];
|
||||
boolean[] bools = new boolean[5];
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title=""
|
||||
// 使用多種基本資料型別來初始化陣列
|
||||
int[] numbers = new int[5];
|
||||
float[] decimals = new float[5];
|
||||
char[] characters = new char[5];
|
||||
bool[] bools = new bool[5];
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title=""
|
||||
// 使用多種基本資料型別來初始化陣列
|
||||
var numbers = [5]int{}
|
||||
var decimals = [5]float64{}
|
||||
var characters = [5]byte{}
|
||||
var bools = [5]bool{}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title=""
|
||||
// 使用多種基本資料型別來初始化陣列
|
||||
let numbers = Array(repeating: 0, count: 5)
|
||||
let decimals = Array(repeating: 0.0, count: 5)
|
||||
let characters: [Character] = Array(repeating: "a", count: 5)
|
||||
let bools = Array(repeating: false, count: 5)
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title=""
|
||||
// JavaScript 的陣列可以自由儲存各種基本資料型別和物件
|
||||
const array = [0, 0.0, 'a', false];
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title=""
|
||||
// 使用多種基本資料型別來初始化陣列
|
||||
const numbers: number[] = [];
|
||||
const characters: string[] = [];
|
||||
const bools: boolean[] = [];
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title=""
|
||||
// 使用多種基本資料型別來初始化陣列
|
||||
List<int> numbers = List.filled(5, 0);
|
||||
List<double> decimals = List.filled(5, 0.0);
|
||||
List<String> characters = List.filled(5, 'a');
|
||||
List<bool> bools = List.filled(5, false);
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title=""
|
||||
// 使用多種基本資料型別來初始化陣列
|
||||
let numbers: Vec<i32> = vec![0; 5];
|
||||
let decimals: Vec<f32> = vec![0.0; 5];
|
||||
let characters: Vec<char> = vec!['0'; 5];
|
||||
let bools: Vec<bool> = vec![false; 5];
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title=""
|
||||
// 使用多種基本資料型別來初始化陣列
|
||||
int numbers[10];
|
||||
float decimals[10];
|
||||
char characters[10];
|
||||
bool bools[10];
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title=""
|
||||
// 使用多種基本資料型別來初始化陣列
|
||||
val numbers = IntArray(5)
|
||||
val decinals = FloatArray(5)
|
||||
val characters = CharArray(5)
|
||||
val bools = BooleanArray(5)
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title=""
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title=""
|
||||
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 477px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E4%BD%BF%E7%94%A8%E5%A4%9A%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E6%9D%A5%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20numbers%20%3D%20%5B0%5D%20*%205%0A%20%20%20%20decimals%20%3D%20%5B0.0%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%AD%97%E7%AC%A6%E5%AE%9E%E9%99%85%E4%B8%8A%E6%98%AF%E9%95%BF%E5%BA%A6%E4%B8%BA%201%20%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%0A%20%20%20%20characters%20%3D%20%5B'0'%5D%20*%205%0A%20%20%20%20bools%20%3D%20%5BFalse%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%88%97%E8%A1%A8%E5%8F%AF%E4%BB%A5%E8%87%AA%E7%94%B1%E5%AD%98%E5%82%A8%E5%90%84%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%92%8C%E5%AF%B9%E8%B1%A1%E5%BC%95%E7%94%A8%0A%20%20%20%20data%20%3D%20%5B0,%200.0,%20'a',%20False,%20ListNode%280%29%5D&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=12&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E4%BD%BF%E7%94%A8%E5%A4%9A%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E6%9D%A5%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20numbers%20%3D%20%5B0%5D%20*%205%0A%20%20%20%20decimals%20%3D%20%5B0.0%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%AD%97%E7%AC%A6%E5%AE%9E%E9%99%85%E4%B8%8A%E6%98%AF%E9%95%BF%E5%BA%A6%E4%B8%BA%201%20%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%0A%20%20%20%20characters%20%3D%20%5B'0'%5D%20*%205%0A%20%20%20%20bools%20%3D%20%5BFalse%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%88%97%E8%A1%A8%E5%8F%AF%E4%BB%A5%E8%87%AA%E7%94%B1%E5%AD%98%E5%82%A8%E5%90%84%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%92%8C%E5%AF%B9%E8%B1%A1%E5%BC%95%E7%94%A8%0A%20%20%20%20data%20%3D%20%5B0,%200.0,%20'a',%20False,%20ListNode%280%29%5D&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=12&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
97
zh-Hant/docs/chapter_data_structure/character_encoding.md
Normal file
97
zh-Hant/docs/chapter_data_structure/character_encoding.md
Normal file
@@ -0,0 +1,97 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 3.4 字元編碼 *
|
||||
|
||||
在計算機中,所有資料都是以二進位制數的形式儲存的,字元 `char` 也不例外。為了表示字元,我們需要建立一套“字符集”,規定每個字元和二進位制數之間的一一對應關係。有了字符集之後,計算機就可以透過查表完成二進位制數到字元的轉換。
|
||||
|
||||
## 3.4.1 ASCII 字符集
|
||||
|
||||
<u>ASCII 碼</u>是最早出現的字符集,其全稱為 American Standard Code for Information Interchange(美國標準資訊交換程式碼)。它使用 7 位二進位制數(一個位元組的低 7 位)表示一個字元,最多能夠表示 128 個不同的字元。如圖 3-6 所示,ASCII 碼包括英文字母的大小寫、數字 0 ~ 9、一些標點符號,以及一些控制字元(如換行符和製表符)。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 3-6 ASCII 碼 </p>
|
||||
|
||||
然而,**ASCII 碼僅能夠表示英文**。隨著計算機的全球化,誕生了一種能夠表示更多語言的 <u>EASCII</u> 字符集。它在 ASCII 的 7 位基礎上擴展到 8 位,能夠表示 256 個不同的字元。
|
||||
|
||||
在世界範圍內,陸續出現了一批適用於不同地區的 EASCII 字符集。這些字符集的前 128 個字元統一為 ASCII 碼,後 128 個字元定義不同,以適應不同語言的需求。
|
||||
|
||||
## 3.4.2 GBK 字符集
|
||||
|
||||
後來人們發現,**EASCII 碼仍然無法滿足許多語言的字元數量要求**。比如漢字有近十萬個,光日常使用的就有幾千個。中國國家標準總局於 1980 年釋出了 <u>GB2312</u> 字符集,其收錄了 6763 個漢字,基本滿足了漢字的計算機處理需要。
|
||||
|
||||
然而,GB2312 無法處理部分罕見字和繁體字。<u>GBK</u> 字符集是在 GB2312 的基礎上擴展得到的,它共收錄了 21886 個漢字。在 GBK 的編碼方案中,ASCII 字元使用一個位元組表示,漢字使用兩個位元組表示。
|
||||
|
||||
## 3.4.3 Unicode 字符集
|
||||
|
||||
隨著計算機技術的蓬勃發展,字符集與編碼標準百花齊放,而這帶來了許多問題。一方面,這些字符集一般只定義了特定語言的字元,無法在多語言環境下正常工作。另一方面,同一種語言存在多種字符集標準,如果兩臺計算機使用的是不同的編碼標準,則在資訊傳遞時就會出現亂碼。
|
||||
|
||||
那個時代的研究人員就在想:**如果推出一個足夠完整的字符集,將世界範圍內的所有語言和符號都收錄其中,不就可以解決跨語言環境和亂碼問題了嗎**?在這種想法的驅動下,一個大而全的字符集 Unicode 應運而生。
|
||||
|
||||
<u>Unicode</u> 的中文名稱為“統一碼”,理論上能容納 100 多萬個字元。它致力於將全球範圍內的字元納入統一的字符集之中,提供一種通用的字符集來處理和顯示各種語言文字,減少因為編碼標準不同而產生的亂碼問題。
|
||||
|
||||
自 1991 年釋出以來,Unicode 不斷擴充新的語言與字元。截至 2022 年 9 月,Unicode 已經包含 149186 個字元,包括各種語言的字元、符號甚至表情符號等。在龐大的 Unicode 字符集中,常用的字元佔用 2 位元組,有些生僻的字元佔用 3 位元組甚至 4 位元組。
|
||||
|
||||
Unicode 是一種通用字符集,本質上是給每個字元分配一個編號(稱為“碼點”),**但它並沒有規定在計算機中如何儲存這些字元碼點**。我們不禁會問:當多種長度的 Unicode 碼點同時出現在一個文字中時,系統如何解析字元?例如給定一個長度為 2 位元組的編碼,系統如何確認它是一個 2 位元組的字元還是兩個 1 位元組的字元?
|
||||
|
||||
對於以上問題,**一種直接的解決方案是將所有字元儲存為等長的編碼**。如圖 3-7 所示,“Hello”中的每個字元佔用 1 位元組,“演算法”中的每個字元佔用 2 位元組。我們可以透過高位填 0 將“Hello 演算法”中的所有字元都編碼為 2 位元組長度。這樣系統就可以每隔 2 位元組解析一個字元,恢復這個短語的內容了。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 3-7 Unicode 編碼示例 </p>
|
||||
|
||||
然而 ASCII 碼已經向我們證明,編碼英文只需 1 位元組。若採用上述方案,英文文字佔用空間的大小將會是 ASCII 編碼下的兩倍,非常浪費記憶體空間。因此,我們需要一種更加高效的 Unicode 編碼方法。
|
||||
|
||||
## 3.4.4 UTF-8 編碼
|
||||
|
||||
目前,UTF-8 已成為國際上使用最廣泛的 Unicode 編碼方法。**它是一種可變長度的編碼**,使用 1 到 4 位元組來表示一個字元,根據字元的複雜性而變。ASCII 字元只需 1 位元組,拉丁字母和希臘字母需要 2 位元組,常用的中文字元需要 3 位元組,其他的一些生僻字元需要 4 位元組。
|
||||
|
||||
UTF-8 的編碼規則並不複雜,分為以下兩種情況。
|
||||
|
||||
- 對於長度為 1 位元組的字元,將最高位設定為 $0$ ,其餘 7 位設定為 Unicode 碼點。值得注意的是,ASCII 字元在 Unicode 字符集中佔據了前 128 個碼點。也就是說,**UTF-8 編碼可以向下相容 ASCII 碼**。這意味著我們可以使用 UTF-8 來解析年代久遠的 ASCII 碼文字。
|
||||
- 對於長度為 $n$ 位元組的字元(其中 $n > 1$),將首個位元組的高 $n$ 位都設定為 $1$ ,第 $n + 1$ 位設定為 $0$ ;從第二個位元組開始,將每個位元組的高 2 位都設定為 $10$ ;其餘所有位用於填充字元的 Unicode 碼點。
|
||||
|
||||
圖 3-8 展示了“Hello演算法”對應的 UTF-8 編碼。觀察發現,由於最高 $n$ 位都設定為 $1$ ,因此系統可以透過讀取最高位 $1$ 的個數來解析出字元的長度為 $n$ 。
|
||||
|
||||
但為什麼要將其餘所有位元組的高 2 位都設定為 $10$ 呢?實際上,這個 $10$ 能夠起到校驗符的作用。假設系統從一個錯誤的位元組開始解析文字,位元組頭部的 $10$ 能夠幫助系統快速判斷出異常。
|
||||
|
||||
之所以將 $10$ 當作校驗符,是因為在 UTF-8 編碼規則下,不可能有字元的最高兩位是 $10$ 。這個結論可以用反證法來證明:假設一個字元的最高兩位是 $10$ ,說明該字元的長度為 $1$ ,對應 ASCII 碼。而 ASCII 碼的最高位應該是 $0$ ,與假設矛盾。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 3-8 UTF-8 編碼示例 </p>
|
||||
|
||||
除了 UTF-8 之外,常見的編碼方式還包括以下兩種。
|
||||
|
||||
- **UTF-16 編碼**:使用 2 或 4 位元組來表示一個字元。所有的 ASCII 字元和常用的非英文字元,都用 2 位元組表示;少數字符需要用到 4 位元組表示。對於 2 位元組的字元,UTF-16 編碼與 Unicode 碼點相等。
|
||||
- **UTF-32 編碼**:每個字元都使用 4 位元組。這意味著 UTF-32 比 UTF-8 和 UTF-16 更佔用空間,特別是對於 ASCII 字元佔比較高的文字。
|
||||
|
||||
從儲存空間佔用的角度看,使用 UTF-8 表示英文字元非常高效,因為它僅需 1 位元組;使用 UTF-16 編碼某些非英文字元(例如中文)會更加高效,因為它僅需 2 位元組,而 UTF-8 可能需要 3 位元組。
|
||||
|
||||
從相容性的角度看,UTF-8 的通用性最佳,許多工具和庫優先支持 UTF-8 。
|
||||
|
||||
## 3.4.5 程式語言的字元編碼
|
||||
|
||||
對於以往的大多數程式語言,程式執行中的字串都採用 UTF-16 或 UTF-32 這類等長編碼。在等長編碼下,我們可以將字串看作陣列來處理,這種做法具有以下優點。
|
||||
|
||||
- **隨機訪問**:UTF-16 編碼的字串可以很容易地進行隨機訪問。UTF-8 是一種變長編碼,要想找到第 $i$ 個字元,我們需要從字串的開始處走訪到第 $i$ 個字元,這需要 $O(n)$ 的時間。
|
||||
- **字元計數**:與隨機訪問類似,計算 UTF-16 編碼的字串的長度也是 $O(1)$ 的操作。但是,計算 UTF-8 編碼的字串的長度需要走訪整個字串。
|
||||
- **字串操作**:在 UTF-16 編碼的字串上,很多字串操作(如分割、連線、插入、刪除等)更容易進行。在 UTF-8 編碼的字串上,進行這些操作通常需要額外的計算,以確保不會產生無效的 UTF-8 編碼。
|
||||
|
||||
實際上,程式語言的字元編碼方案設計是一個很有趣的話題,涉及許多因素。
|
||||
|
||||
- Java 的 `String` 型別使用 UTF-16 編碼,每個字元佔用 2 位元組。這是因為 Java 語言設計之初,人們認為 16 位足以表示所有可能的字元。然而,這是一個不正確的判斷。後來 Unicode 規範擴展到了超過 16 位,所以 Java 中的字元現在可能由一對 16 位的值(稱為“代理對”)表示。
|
||||
- JavaScript 和 TypeScript 的字串使用 UTF-16 編碼的原因與 Java 類似。當 1995 年 Netscape 公司首次推出 JavaScript 語言時,Unicode 還處於發展早期,那時候使用 16 位的編碼就足以表示所有的 Unicode 字元了。
|
||||
- C# 使用 UTF-16 編碼,主要是因為 .NET 平臺是由 Microsoft 設計的,而 Microsoft 的很多技術(包括 Windows 作業系統)都廣泛使用 UTF-16 編碼。
|
||||
|
||||
由於以上程式語言對字元數量的低估,它們不得不採取“代理對”的方式來表示超過 16 位長度的 Unicode 字元。這是一個不得已為之的無奈之舉。一方面,包含代理對的字串中,一個字元可能佔用 2 位元組或 4 位元組,從而喪失了等長編碼的優勢。另一方面,處理代理對需要額外增加程式碼,這提高了程式設計的複雜性和除錯難度。
|
||||
|
||||
出於以上原因,部分程式語言提出了一些不同的編碼方案。
|
||||
|
||||
- Python 中的 `str` 使用 Unicode 編碼,並採用一種靈活的字串表示,儲存的字元長度取決於字串中最大的 Unicode 碼點。若字串中全部是 ASCII 字元,則每個字元佔用 1 位元組;如果有字元超出了 ASCII 範圍,但全部在基本多語言平面(BMP)內,則每個字元佔用 2 位元組;如果有超出 BMP 的字元,則每個字元佔用 4 位元組。
|
||||
- Go 語言的 `string` 型別在內部使用 UTF-8 編碼。Go 語言還提供了 `rune` 型別,它用於表示單個 Unicode 碼點。
|
||||
- Rust 語言的 `str` 和 `String` 型別在內部使用 UTF-8 編碼。Rust 也提供了 `char` 型別,用於表示單個 Unicode 碼點。
|
||||
|
||||
需要注意的是,以上討論的都是字串在程式語言中的儲存方式,**這和字串如何在檔案中儲存或在網路中傳輸是不同的問題**。在檔案儲存或網路傳輸中,我們通常會將字串編碼為 UTF-8 格式,以達到最優的相容性和空間效率。
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 3.1 資料結構分類
|
||||
|
||||
常見的資料結構包括陣列、鏈結串列、堆疊、佇列、雜湊表、樹、堆積、圖,它們可以從“邏輯結構”和“物理結構”兩個維度進行分類。
|
||||
|
||||
## 3.1.1 邏輯結構:線性與非線性
|
||||
|
||||
**邏輯結構揭示了資料元素之間的邏輯關係**。在陣列和鏈結串列中,資料按照一定順序排列,體現了資料之間的線性關係;而在樹中,資料從頂部向下按層次排列,表現出“祖先”與“後代”之間的派生關係;圖則由節點和邊構成,反映了複雜的網路關係。
|
||||
|
||||
如圖 3-1 所示,邏輯結構可分為“線性”和“非線性”兩大類。線性結構比較直觀,指資料在邏輯關係上呈線性排列;非線性結構則相反,呈非線性排列。
|
||||
|
||||
- **線性資料結構**:陣列、鏈結串列、堆疊、佇列、雜湊表,元素之間是一對一的順序關係。
|
||||
- **非線性資料結構**:樹、堆積、圖、雜湊表。
|
||||
|
||||
非線性資料結構可以進一步劃分為樹形結構和網狀結構。
|
||||
|
||||
- **樹形結構**:樹、堆積、雜湊表,元素之間是一對多的關係。
|
||||
- **網狀結構**:圖,元素之間是多對多的關係。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 3-1 線性資料結構與非線性資料結構 </p>
|
||||
|
||||
## 3.1.2 物理結構:連續與分散
|
||||
|
||||
**當演算法程式執行時,正在處理的資料主要儲存在記憶體中**。圖 3-2 展示了一個計算機記憶體條,其中每個黑色方塊都包含一塊記憶體空間。我們可以將記憶體想象成一個巨大的 Excel 表格,其中每個單元格都可以儲存一定大小的資料。
|
||||
|
||||
**系統透過記憶體位址來訪問目標位置的資料**。如圖 3-2 所示,計算機根據特定規則為表格中的每個單元格分配編號,確保每個記憶體空間都有唯一的記憶體位址。有了這些位址,程式便可以訪問記憶體中的資料。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 3-2 記憶體條、記憶體空間、記憶體位址 </p>
|
||||
|
||||
!!! tip
|
||||
|
||||
值得說明的是,將記憶體比作 Excel 表格是一個簡化的類比,實際記憶體的工作機制比較複雜,涉及位址空間、記憶體管理、快取機制、虛擬記憶體和物理記憶體等概念。
|
||||
|
||||
記憶體是所有程式的共享資源,當某塊記憶體被某個程式佔用時,則無法被其他程式同時使用了。**因此在資料結構與演算法的設計中,記憶體資源是一個重要的考慮因素**。比如,演算法所佔用的記憶體峰值不應超過系統剩餘空閒記憶體;如果缺少連續大塊的記憶體空間,那麼所選用的資料結構必須能夠儲存在分散的記憶體空間內。
|
||||
|
||||
如圖 3-3 所示,**物理結構反映了資料在計算機記憶體中的儲存方式**,可分為連續空間儲存(陣列)和分散空間儲存(鏈結串列)。物理結構從底層決定了資料的訪問、更新、增刪等操作方法,兩種物理結構在時間效率和空間效率方面呈現出互補的特點。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 3-3 連續空間儲存與分散空間儲存 </p>
|
||||
|
||||
值得說明的是,**所有資料結構都是基於陣列、鏈結串列或二者的組合實現的**。例如,堆疊和佇列既可以使用陣列實現,也可以使用鏈結串列實現;而雜湊表的實現可能同時包含陣列和鏈結串列。
|
||||
|
||||
- **基於陣列可實現**:堆疊、佇列、雜湊表、樹、堆積、圖、矩陣、張量(維度 $\geq 3$ 的陣列)等。
|
||||
- **基於鏈結串列可實現**:堆疊、佇列、雜湊表、樹、堆積、圖等。
|
||||
|
||||
鏈結串列在初始化後,仍可以在程式執行過程中對其長度進行調整,因此也稱“動態資料結構”。陣列在初始化後長度不可變,因此也稱“靜態資料結構”。值得注意的是,陣列可透過重新分配記憶體實現長度變化,從而具備一定的“動態性”。
|
||||
|
||||
!!! tip
|
||||
|
||||
如果你感覺物理結構理解起來有困難,建議先閱讀下一章,然後再回顧本節內容。
|
||||
22
zh-Hant/docs/chapter_data_structure/index.md
Normal file
22
zh-Hant/docs/chapter_data_structure/index.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/shape-outline
|
||||
---
|
||||
|
||||
# 第 3 章 資料結構
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
資料結構如同一副穩固而多樣的框架。
|
||||
|
||||
它為資料的有序組織提供了藍圖,演算法得以在此基礎上生動起來。
|
||||
|
||||
## Chapter Contents
|
||||
|
||||
- [3.1 資料結構分類](https://www.hello-algo.com/en/chapter_data_structure/classification_of_data_structure/)
|
||||
- [3.2 基本資料型別](https://www.hello-algo.com/en/chapter_data_structure/basic_data_types/)
|
||||
- [3.3 數字編碼 *](https://www.hello-algo.com/en/chapter_data_structure/number_encoding/)
|
||||
- [3.4 字元編碼 *](https://www.hello-algo.com/en/chapter_data_structure/character_encoding/)
|
||||
- [3.5 小結](https://www.hello-algo.com/en/chapter_data_structure/summary/)
|
||||
162
zh-Hant/docs/chapter_data_structure/number_encoding.md
Normal file
162
zh-Hant/docs/chapter_data_structure/number_encoding.md
Normal file
@@ -0,0 +1,162 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 3.3 數字編碼 *
|
||||
|
||||
!!! note
|
||||
|
||||
在本書中,標題帶有 * 符號的是選讀章節。如果你時間有限或感到理解困難,可以先跳過,等學完必讀章節後再單獨攻克。
|
||||
|
||||
## 3.3.1 原碼、一補數和二補數
|
||||
|
||||
在上一節的表格中我們發現,所有整數型別能夠表示的負數都比正數多一個,例如 `byte` 的取值範圍是 $[-128, 127]$ 。這個現象比較反直覺,它的內在原因涉及原碼、一補數、二補數的相關知識。
|
||||
|
||||
首先需要指出,**數字是以“二補數”的形式儲存在計算機中的**。在分析這樣做的原因之前,首先給出三者的定義。
|
||||
|
||||
- **原碼**:我們將數字的二進位制表示的最高位視為符號位,其中 $0$ 表示正數,$1$ 表示負數,其餘位表示數字的值。
|
||||
- **一補數**:正數的一補數與其原碼相同,負數的一補數是對其原碼除符號位外的所有位取反。
|
||||
- **二補數**:正數的二補數與其原碼相同,負數的二補數是在其一補數的基礎上加 $1$ 。
|
||||
|
||||
圖 3-4 展示了原碼、一補數和二補數之間的轉換方法。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 3-4 原碼、一補數與二補數之間的相互轉換 </p>
|
||||
|
||||
<u>原碼(sign-magnitude)</u>雖然最直觀,但存在一些侷限性。一方面,**負數的原碼不能直接用於運算**。例如在原碼下計算 $1 + (-2)$ ,得到的結果是 $-3$ ,這顯然是不對的。
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
& 1 + (-2) \newline
|
||||
& \rightarrow 0000 \; 0001 + 1000 \; 0010 \newline
|
||||
& = 1000 \; 0011 \newline
|
||||
& \rightarrow -3
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
為了解決此問題,計算機引入了<u>一補數(1's complement)</u>。如果我們先將原碼轉換為一補數,並在一補數下計算 $1 + (-2)$ ,最後將結果從一補數轉換回原碼,則可得到正確結果 $-1$ 。
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
& 1 + (-2) \newline
|
||||
& \rightarrow 0000 \; 0001 \; \text{(原碼)} + 1000 \; 0010 \; \text{(原碼)} \newline
|
||||
& = 0000 \; 0001 \; \text{(一補數)} + 1111 \; 1101 \; \text{(一補數)} \newline
|
||||
& = 1111 \; 1110 \; \text{(一補數)} \newline
|
||||
& = 1000 \; 0001 \; \text{(原碼)} \newline
|
||||
& \rightarrow -1
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
另一方面,**數字零的原碼有 $+0$ 和 $-0$ 兩種表示方式**。這意味著數字零對應兩個不同的二進位制編碼,這可能會帶來歧義。比如在條件判斷中,如果沒有區分正零和負零,則可能會導致判斷結果出錯。而如果我們想處理正零和負零歧義,則需要引入額外的判斷操作,這可能會降低計算機的運算效率。
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
+0 & \rightarrow 0000 \; 0000 \newline
|
||||
-0 & \rightarrow 1000 \; 0000
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
與原碼一樣,一補數也存在正負零歧義問題,因此計算機進一步引入了<u>二補數(2's complement)</u>。我們先來觀察一下負零的原碼、一補數、二補數的轉換過程:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
-0 \rightarrow \; & 1000 \; 0000 \; \text{(原碼)} \newline
|
||||
= \; & 1111 \; 1111 \; \text{(一補數)} \newline
|
||||
= 1 \; & 0000 \; 0000 \; \text{(二補數)} \newline
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
在負零的一補數基礎上加 $1$ 會產生進位,但 `byte` 型別的長度只有 8 位,因此溢位到第 9 位的 $1$ 會被捨棄。也就是說,**負零的二補數為 $0000 \; 0000$ ,與正零的二補數相同**。這意味著在二補數表示中只存在一個零,正負零歧義從而得到解決。
|
||||
|
||||
還剩最後一個疑惑:`byte` 型別的取值範圍是 $[-128, 127]$ ,多出來的一個負數 $-128$ 是如何得到的呢?我們注意到,區間 $[-127, +127]$ 內的所有整數都有對應的原碼、一補數和二補數,並且原碼和二補數之間可以互相轉換。
|
||||
|
||||
然而,**二補數 $1000 \; 0000$ 是一個例外,它並沒有對應的原碼**。根據轉換方法,我們得到該二補數的原碼為 $0000 \; 0000$ 。這顯然是矛盾的,因為該原碼表示數字 $0$ ,它的二補數應該是自身。計算機規定這個特殊的二補數 $1000 \; 0000$ 代表 $-128$ 。實際上,$(-1) + (-127)$ 在二補數下的計算結果就是 $-128$ 。
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
& (-127) + (-1) \newline
|
||||
& \rightarrow 1111 \; 1111 \; \text{(原碼)} + 1000 \; 0001 \; \text{(原碼)} \newline
|
||||
& = 1000 \; 0000 \; \text{(一補數)} + 1111 \; 1110 \; \text{(一補數)} \newline
|
||||
& = 1000 \; 0001 \; \text{(二補數)} + 1111 \; 1111 \; \text{(二補數)} \newline
|
||||
& = 1000 \; 0000 \; \text{(二補數)} \newline
|
||||
& \rightarrow -128
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
你可能已經發現了,上述所有計算都是加法運算。這暗示著一個重要事實:**計算機內部的硬體電路主要是基於加法運算設計的**。這是因為加法運算相對於其他運算(比如乘法、除法和減法)來說,硬體實現起來更簡單,更容易進行並行化處理,運算速度更快。
|
||||
|
||||
請注意,這並不意味著計算機只能做加法。**透過將加法與一些基本邏輯運算結合,計算機能夠實現各種其他的數學運算**。例如,計算減法 $a - b$ 可以轉換為計算加法 $a + (-b)$ ;計算乘法和除法可以轉換為計算多次加法或減法。
|
||||
|
||||
現在我們可以總結出計算機使用二補數的原因:基於二補數表示,計算機可以用同樣的電路和操作來處理正數和負數的加法,不需要設計特殊的硬體電路來處理減法,並且無須特別處理正負零的歧義問題。這大大簡化了硬體設計,提高了運算效率。
|
||||
|
||||
二補數的設計非常精妙,因篇幅關係我們就先介紹到這裡,建議有興趣的讀者進一步深入瞭解。
|
||||
|
||||
## 3.3.2 浮點數編碼
|
||||
|
||||
細心的你可能會發現:`int` 和 `float` 長度相同,都是 4 位元組 ,但為什麼 `float` 的取值範圍遠大於 `int` ?這非常反直覺,因為按理說 `float` 需要表示小數,取值範圍應該變小才對。
|
||||
|
||||
實際上,**這是因為浮點數 `float` 採用了不同的表示方式**。記一個 32 位元長度的二進位制數為:
|
||||
|
||||
$$
|
||||
b_{31} b_{30} b_{29} \ldots b_2 b_1 b_0
|
||||
$$
|
||||
|
||||
根據 IEEE 754 標準,32-bit 長度的 `float` 由以下三個部分構成。
|
||||
|
||||
- 符號位 $\mathrm{S}$ :佔 1 位 ,對應 $b_{31}$ 。
|
||||
- 指數位 $\mathrm{E}$ :佔 8 位 ,對應 $b_{30} b_{29} \ldots b_{23}$ 。
|
||||
- 分數位 $\mathrm{N}$ :佔 23 位 ,對應 $b_{22} b_{21} \ldots b_0$ 。
|
||||
|
||||
二進位制數 `float` 對應值的計算方法為:
|
||||
|
||||
$$
|
||||
\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
|
||||
$$
|
||||
|
||||
轉化到十進位制下的計算公式為:
|
||||
|
||||
$$
|
||||
\text {val}=(-1)^{\mathrm{S}} \times 2^{\mathrm{E} -127} \times (1 + \mathrm{N})
|
||||
$$
|
||||
|
||||
其中各項的取值範圍為:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\mathrm{S} \in & \{ 0, 1\}, \quad \mathrm{E} \in \{ 1, 2, \dots, 254 \} \newline
|
||||
(1 + \mathrm{N}) = & (1 + \sum_{i=1}^{23} b_{23-i} 2^{-i}) \subset [1, 2 - 2^{-23}]
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 3-5 IEEE 754 標準下的 float 的計算示例 </p>
|
||||
|
||||
觀察圖 3-5 ,給定一個示例資料 $\mathrm{S} = 0$ , $\mathrm{E} = 124$ ,$\mathrm{N} = 2^{-2} + 2^{-3} = 0.375$ ,則有:
|
||||
|
||||
$$
|
||||
\text { val } = (-1)^0 \times 2^{124 - 127} \times (1 + 0.375) = 0.171875
|
||||
$$
|
||||
|
||||
現在我們可以回答最初的問題:**`float` 的表示方式包含指數位,導致其取值範圍遠大於 `int`** 。根據以上計算,`float` 可表示的最大正數為 $2^{254 - 127} \times (2 - 2^{-23}) \approx 3.4 \times 10^{38}$ ,切換符號位便可得到最小負數。
|
||||
|
||||
**儘管浮點數 `float` 擴展了取值範圍,但其副作用是犧牲了精度**。整數型別 `int` 將全部 32 位元用於表示數字,數字是均勻分佈的;而由於指數位的存在,浮點數 `float` 的數值越大,相鄰兩個數字之間的差值就會趨向越大。
|
||||
|
||||
如表 3-2 所示,指數位 $E = 0$ 和 $E = 255$ 具有特殊含義,**用於表示零、無窮大、$\mathrm{NaN}$ 等**。
|
||||
|
||||
<p align="center"> 表 3-2 指數位含義 </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
| 指數位 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}$ | |
|
||||
|
||||
</div>
|
||||
|
||||
值得說明的是,次正規數顯著提升了浮點數的精度。最小正正規數為 $2^{-126}$ ,最小正次正規數為 $2^{-126} \times 2^{-23}$ 。
|
||||
|
||||
雙精度 `double` 也採用類似於 `float` 的表示方法,在此不做贅述。
|
||||
38
zh-Hant/docs/chapter_data_structure/summary.md
Normal file
38
zh-Hant/docs/chapter_data_structure/summary.md
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 3.5 小結
|
||||
|
||||
### 1. 重點回顧
|
||||
|
||||
- 資料結構可以從邏輯結構和物理結構兩個角度進行分類。邏輯結構描述了資料元素之間的邏輯關係,而物理結構描述了資料在計算機記憶體中的儲存方式。
|
||||
- 常見的邏輯結構包括線性、樹狀和網狀等。通常我們根據邏輯結構將資料結構分為線性(陣列、鏈結串列、堆疊、佇列)和非線性(樹、圖、堆積)兩種。雜湊表的實現可能同時包含線性資料結構和非線性資料結構。
|
||||
- 當程式執行時,資料被儲存在計算機記憶體中。每個記憶體空間都擁有對應的記憶體位址,程式透過這些記憶體位址訪問資料。
|
||||
- 物理結構主要分為連續空間儲存(陣列)和分散空間儲存(鏈結串列)。所有資料結構都是由陣列、鏈結串列或兩者的組合實現的。
|
||||
- 計算機中的基本資料型別包括整數 `byte`、`short`、`int`、`long` ,浮點數 `float`、`double` ,字元 `char` 和布林 `bool` 。它們的取值範圍取決於佔用空間大小和表示方式。
|
||||
- 原碼、一補數和二補數是在計算機中編碼數字的三種方法,它們之間可以相互轉換。整數的原碼的最高位是符號位,其餘位是數字的值。
|
||||
- 整數在計算機中是以二補數的形式儲存的。在二補數表示下,計算機可以對正數和負數的加法一視同仁,不需要為減法操作單獨設計特殊的硬體電路,並且不存在正負零歧義的問題。
|
||||
- 浮點數的編碼由 1 位符號位、8 位指數位和 23 位分數位構成。由於存在指數位,因此浮點數的取值範圍遠大於整數,代價是犧牲了精度。
|
||||
- ASCII 碼是最早出現的英文字符集,長度為 1 位元組,共收錄 127 個字元。GBK 字符集是常用的中文字符集,共收錄兩萬多個漢字。Unicode 致力於提供一個完整的字符集標準,收錄世界上各種語言的字元,從而解決由於字元編碼方法不一致而導致的亂碼問題。
|
||||
- UTF-8 是最受歡迎的 Unicode 編碼方法,通用性非常好。它是一種變長的編碼方法,具有很好的擴展性,有效提升了儲存空間的使用效率。UTF-16 和 UTF-32 是等長的編碼方法。在編碼中文時,UTF-16 佔用的空間比 UTF-8 更小。Java 和 C# 等程式語言預設使用 UTF-16 編碼。
|
||||
|
||||
### 2. Q & A
|
||||
|
||||
**Q**:為什麼雜湊表同時包含線性資料結構和非線性資料結構?
|
||||
|
||||
雜湊表底層是陣列,而為了解決雜湊衝突,我們可能會使用“鏈式位址”(後續“雜湊衝突”章節會講):陣列中每個桶指向一個鏈結串列,當鏈結串列長度超過一定閾值時,又可能被轉化為樹(通常為紅黑樹)。
|
||||
|
||||
從儲存的角度來看,雜湊表的底層是陣列,其中每一個桶槽位可能包含一個值,也可能包含一個鏈結串列或一棵樹。因此,雜湊表可能同時包含線性資料結構(陣列、鏈結串列)和非線性資料結構(樹)。
|
||||
|
||||
**Q**:`char` 型別的長度是 1 位元組嗎?
|
||||
|
||||
`char` 型別的長度由程式語言採用的編碼方法決定。例如,Java、JavaScript、TypeScript、C# 都採用 UTF-16 編碼(儲存 Unicode 碼點),因此 `char` 型別的長度為 2 位元組。
|
||||
|
||||
**Q**:基於陣列實現的資料結構也稱“靜態資料結構” 是否有歧義?堆疊也可以進行出堆疊和入堆疊等操作,這些操作都是“動態”的。
|
||||
|
||||
堆疊確實可以實現動態的資料操作,但資料結構仍然是“靜態”(長度不可變)的。儘管基於陣列的資料結構可以動態地新增或刪除元素,但它們的容量是固定的。如果資料量超出了預分配的大小,就需要建立一個新的更大的陣列,並將舊陣列的內容複製到新陣列中。
|
||||
|
||||
**Q**:在構建堆疊(佇列)的時候,未指定它的大小,為什麼它們是“靜態資料結構”呢?
|
||||
|
||||
在高階程式語言中,我們無須人工指定堆疊(佇列)的初始容量,這個工作由類別內部自動完成。例如,Java 的 `ArrayList` 的初始容量通常為 10。另外,擴容操作也是自動實現的。詳見後續的“串列”章節。
|
||||
441
zh-Hant/docs/chapter_divide_and_conquer/binary_search_recur.md
Normal file
441
zh-Hant/docs/chapter_divide_and_conquer/binary_search_recur.md
Normal file
@@ -0,0 +1,441 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 12.2 分治搜尋策略
|
||||
|
||||
我們已經學過,搜尋演算法分為兩大類。
|
||||
|
||||
- **暴力搜尋**:它透過走訪資料結構實現,時間複雜度為 $O(n)$ 。
|
||||
- **自適應搜尋**:它利用特有的資料組織形式或先驗資訊,時間複雜度可達到 $O(\log n)$ 甚至 $O(1)$ 。
|
||||
|
||||
實際上,**時間複雜度為 $O(\log n)$ 的搜尋演算法通常是基於分治策略實現的**,例如二分搜尋和樹。
|
||||
|
||||
- 二分搜尋的每一步都將問題(在陣列中搜索目標元素)分解為一個小問題(在陣列的一半中搜索目標元素),這個過程一直持續到陣列為空或找到目標元素為止。
|
||||
- 樹是分治思想的代表,在二元搜尋樹、AVL 樹、堆積等資料結構中,各種操作的時間複雜度皆為 $O(\log n)$ 。
|
||||
|
||||
二分搜尋的分治策略如下所示。
|
||||
|
||||
- **問題可以分解**:二分搜尋遞迴地將原問題(在陣列中進行查詢)分解為子問題(在陣列的一半中進行查詢),這是透過比較中間元素和目標元素來實現的。
|
||||
- **子問題是獨立的**:在二分搜尋中,每輪只處理一個子問題,它不受其他子問題的影響。
|
||||
- **子問題的解無須合併**:二分搜尋旨在查詢一個特定元素,因此不需要將子問題的解進行合併。當子問題得到解決時,原問題也會同時得到解決。
|
||||
|
||||
分治能夠提升搜尋效率,本質上是因為暴力搜尋每輪只能排除一個選項,**而分治搜尋每輪可以排除一半選項**。
|
||||
|
||||
### 1. 基於分治實現二分搜尋
|
||||
|
||||
在之前的章節中,二分搜尋是基於遞推(迭代)實現的。現在我們基於分治(遞迴)來實現它。
|
||||
|
||||
!!! question
|
||||
|
||||
給定一個長度為 $n$ 的有序陣列 `nums` ,其中所有元素都是唯一的,請查詢元素 `target` 。
|
||||
|
||||
從分治角度,我們將搜尋區間 $[i, j]$ 對應的子問題記為 $f(i, j)$ 。
|
||||
|
||||
以原問題 $f(0, n-1)$ 為起始點,透過以下步驟進行二分搜尋。
|
||||
|
||||
1. 計算搜尋區間 $[i, j]$ 的中點 $m$ ,根據它排除一半搜尋區間。
|
||||
2. 遞迴求解規模減小一半的子問題,可能為 $f(i, m-1)$ 或 $f(m+1, j)$ 。
|
||||
3. 迴圈第 `1.` 步和第 `2.` 步,直至找到 `target` 或區間為空時返回。
|
||||
|
||||
圖 12-4 展示了在陣列中二分搜尋元素 $6$ 的分治過程。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-4 二分搜尋的分治過程 </p>
|
||||
|
||||
在實現程式碼中,我們宣告一個遞迴函式 `dfs()` 來求解問題 $f(i, j)$ :
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="binary_search_recur.py"
|
||||
def dfs(nums: list[int], target: int, i: int, j: int) -> int:
|
||||
"""二分搜尋:問題 f(i, j)"""
|
||||
# 若區間為空,代表無目標元素,則返回 -1
|
||||
if i > j:
|
||||
return -1
|
||||
# 計算中點索引 m
|
||||
m = (i + j) // 2
|
||||
if nums[m] < target:
|
||||
# 遞迴子問題 f(m+1, j)
|
||||
return dfs(nums, target, m + 1, j)
|
||||
elif nums[m] > target:
|
||||
# 遞迴子問題 f(i, m-1)
|
||||
return dfs(nums, target, i, m - 1)
|
||||
else:
|
||||
# 找到目標元素,返回其索引
|
||||
return m
|
||||
|
||||
def binary_search(nums: list[int], target: int) -> int:
|
||||
"""二分搜尋"""
|
||||
n = len(nums)
|
||||
# 求解問題 f(0, n-1)
|
||||
return dfs(nums, target, 0, n - 1)
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="binary_search_recur.cpp"
|
||||
/* 二分搜尋:問題 f(i, j) */
|
||||
int dfs(vector<int> &nums, int target, int i, int j) {
|
||||
// 若區間為空,代表無目標元素,則返回 -1
|
||||
if (i > j) {
|
||||
return -1;
|
||||
}
|
||||
// 計算中點索引 m
|
||||
int m = (i + j) / 2;
|
||||
if (nums[m] < target) {
|
||||
// 遞迴子問題 f(m+1, j)
|
||||
return dfs(nums, target, m + 1, j);
|
||||
} else if (nums[m] > target) {
|
||||
// 遞迴子問題 f(i, m-1)
|
||||
return dfs(nums, target, i, m - 1);
|
||||
} else {
|
||||
// 找到目標元素,返回其索引
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
/* 二分搜尋 */
|
||||
int binarySearch(vector<int> &nums, int target) {
|
||||
int n = nums.size();
|
||||
// 求解問題 f(0, n-1)
|
||||
return dfs(nums, target, 0, n - 1);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="binary_search_recur.java"
|
||||
/* 二分搜尋:問題 f(i, j) */
|
||||
int dfs(int[] nums, int target, int i, int j) {
|
||||
// 若區間為空,代表無目標元素,則返回 -1
|
||||
if (i > j) {
|
||||
return -1;
|
||||
}
|
||||
// 計算中點索引 m
|
||||
int m = (i + j) / 2;
|
||||
if (nums[m] < target) {
|
||||
// 遞迴子問題 f(m+1, j)
|
||||
return dfs(nums, target, m + 1, j);
|
||||
} else if (nums[m] > target) {
|
||||
// 遞迴子問題 f(i, m-1)
|
||||
return dfs(nums, target, i, m - 1);
|
||||
} else {
|
||||
// 找到目標元素,返回其索引
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
/* 二分搜尋 */
|
||||
int binarySearch(int[] nums, int target) {
|
||||
int n = nums.length;
|
||||
// 求解問題 f(0, n-1)
|
||||
return dfs(nums, target, 0, n - 1);
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="binary_search_recur.cs"
|
||||
/* 二分搜尋:問題 f(i, j) */
|
||||
int DFS(int[] nums, int target, int i, int j) {
|
||||
// 若區間為空,代表無目標元素,則返回 -1
|
||||
if (i > j) {
|
||||
return -1;
|
||||
}
|
||||
// 計算中點索引 m
|
||||
int m = (i + j) / 2;
|
||||
if (nums[m] < target) {
|
||||
// 遞迴子問題 f(m+1, j)
|
||||
return DFS(nums, target, m + 1, j);
|
||||
} else if (nums[m] > target) {
|
||||
// 遞迴子問題 f(i, m-1)
|
||||
return DFS(nums, target, i, m - 1);
|
||||
} else {
|
||||
// 找到目標元素,返回其索引
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
/* 二分搜尋 */
|
||||
int BinarySearch(int[] nums, int target) {
|
||||
int n = nums.Length;
|
||||
// 求解問題 f(0, n-1)
|
||||
return DFS(nums, target, 0, n - 1);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="binary_search_recur.go"
|
||||
/* 二分搜尋:問題 f(i, j) */
|
||||
func dfs(nums []int, target, i, j int) int {
|
||||
// 如果區間為空,代表沒有目標元素,則返回 -1
|
||||
if i > j {
|
||||
return -1
|
||||
}
|
||||
// 計算索引中點
|
||||
m := i + ((j - i) >> 1)
|
||||
//判斷中點與目標元素大小
|
||||
if nums[m] < target {
|
||||
// 小於則遞迴右半陣列
|
||||
// 遞迴子問題 f(m+1, j)
|
||||
return dfs(nums, target, m+1, j)
|
||||
} else if nums[m] > target {
|
||||
// 小於則遞迴左半陣列
|
||||
// 遞迴子問題 f(i, m-1)
|
||||
return dfs(nums, target, i, m-1)
|
||||
} else {
|
||||
// 找到目標元素,返回其索引
|
||||
return m
|
||||
}
|
||||
}
|
||||
|
||||
/* 二分搜尋 */
|
||||
func binarySearch(nums []int, target int) int {
|
||||
n := len(nums)
|
||||
return dfs(nums, target, 0, n-1)
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="binary_search_recur.swift"
|
||||
/* 二分搜尋:問題 f(i, j) */
|
||||
func dfs(nums: [Int], target: Int, i: Int, j: Int) -> Int {
|
||||
// 若區間為空,代表無目標元素,則返回 -1
|
||||
if i > j {
|
||||
return -1
|
||||
}
|
||||
// 計算中點索引 m
|
||||
let m = (i + j) / 2
|
||||
if nums[m] < target {
|
||||
// 遞迴子問題 f(m+1, j)
|
||||
return dfs(nums: nums, target: target, i: m + 1, j: j)
|
||||
} else if nums[m] > target {
|
||||
// 遞迴子問題 f(i, m-1)
|
||||
return dfs(nums: nums, target: target, i: i, j: m - 1)
|
||||
} else {
|
||||
// 找到目標元素,返回其索引
|
||||
return m
|
||||
}
|
||||
}
|
||||
|
||||
/* 二分搜尋 */
|
||||
func binarySearch(nums: [Int], target: Int) -> Int {
|
||||
// 求解問題 f(0, n-1)
|
||||
dfs(nums: nums, target: target, i: nums.startIndex, j: nums.endIndex - 1)
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="binary_search_recur.js"
|
||||
/* 二分搜尋:問題 f(i, j) */
|
||||
function dfs(nums, target, i, j) {
|
||||
// 若區間為空,代表無目標元素,則返回 -1
|
||||
if (i > j) {
|
||||
return -1;
|
||||
}
|
||||
// 計算中點索引 m
|
||||
const m = i + ((j - i) >> 1);
|
||||
if (nums[m] < target) {
|
||||
// 遞迴子問題 f(m+1, j)
|
||||
return dfs(nums, target, m + 1, j);
|
||||
} else if (nums[m] > target) {
|
||||
// 遞迴子問題 f(i, m-1)
|
||||
return dfs(nums, target, i, m - 1);
|
||||
} else {
|
||||
// 找到目標元素,返回其索引
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
/* 二分搜尋 */
|
||||
function binarySearch(nums, target) {
|
||||
const n = nums.length;
|
||||
// 求解問題 f(0, n-1)
|
||||
return dfs(nums, target, 0, n - 1);
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="binary_search_recur.ts"
|
||||
/* 二分搜尋:問題 f(i, j) */
|
||||
function dfs(nums: number[], target: number, i: number, j: number): number {
|
||||
// 若區間為空,代表無目標元素,則返回 -1
|
||||
if (i > j) {
|
||||
return -1;
|
||||
}
|
||||
// 計算中點索引 m
|
||||
const m = i + ((j - i) >> 1);
|
||||
if (nums[m] < target) {
|
||||
// 遞迴子問題 f(m+1, j)
|
||||
return dfs(nums, target, m + 1, j);
|
||||
} else if (nums[m] > target) {
|
||||
// 遞迴子問題 f(i, m-1)
|
||||
return dfs(nums, target, i, m - 1);
|
||||
} else {
|
||||
// 找到目標元素,返回其索引
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
/* 二分搜尋 */
|
||||
function binarySearch(nums: number[], target: number): number {
|
||||
const n = nums.length;
|
||||
// 求解問題 f(0, n-1)
|
||||
return dfs(nums, target, 0, n - 1);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="binary_search_recur.dart"
|
||||
/* 二分搜尋:問題 f(i, j) */
|
||||
int dfs(List<int> nums, int target, int i, int j) {
|
||||
// 若區間為空,代表無目標元素,則返回 -1
|
||||
if (i > j) {
|
||||
return -1;
|
||||
}
|
||||
// 計算中點索引 m
|
||||
int m = (i + j) ~/ 2;
|
||||
if (nums[m] < target) {
|
||||
// 遞迴子問題 f(m+1, j)
|
||||
return dfs(nums, target, m + 1, j);
|
||||
} else if (nums[m] > target) {
|
||||
// 遞迴子問題 f(i, m-1)
|
||||
return dfs(nums, target, i, m - 1);
|
||||
} else {
|
||||
// 找到目標元素,返回其索引
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
/* 二分搜尋 */
|
||||
int binarySearch(List<int> nums, int target) {
|
||||
int n = nums.length;
|
||||
// 求解問題 f(0, n-1)
|
||||
return dfs(nums, target, 0, n - 1);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="binary_search_recur.rs"
|
||||
/* 二分搜尋:問題 f(i, j) */
|
||||
fn dfs(nums: &[i32], target: i32, i: i32, j: i32) -> i32 {
|
||||
// 若區間為空,代表無目標元素,則返回 -1
|
||||
if i > j {
|
||||
return -1;
|
||||
}
|
||||
let m: i32 = (i + j) / 2;
|
||||
if nums[m as usize] < target {
|
||||
// 遞迴子問題 f(m+1, j)
|
||||
return dfs(nums, target, m + 1, j);
|
||||
} else if nums[m as usize] > target {
|
||||
// 遞迴子問題 f(i, m-1)
|
||||
return dfs(nums, target, i, m - 1);
|
||||
} else {
|
||||
// 找到目標元素,返回其索引
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
/* 二分搜尋 */
|
||||
fn binary_search(nums: &[i32], target: i32) -> i32 {
|
||||
let n = nums.len() as i32;
|
||||
// 求解問題 f(0, n-1)
|
||||
dfs(nums, target, 0, n - 1)
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="binary_search_recur.c"
|
||||
/* 二分搜尋:問題 f(i, j) */
|
||||
int dfs(int nums[], int target, int i, int j) {
|
||||
// 若區間為空,代表無目標元素,則返回 -1
|
||||
if (i > j) {
|
||||
return -1;
|
||||
}
|
||||
// 計算中點索引 m
|
||||
int m = (i + j) / 2;
|
||||
if (nums[m] < target) {
|
||||
// 遞迴子問題 f(m+1, j)
|
||||
return dfs(nums, target, m + 1, j);
|
||||
} else if (nums[m] > target) {
|
||||
// 遞迴子問題 f(i, m-1)
|
||||
return dfs(nums, target, i, m - 1);
|
||||
} else {
|
||||
// 找到目標元素,返回其索引
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
/* 二分搜尋 */
|
||||
int binarySearch(int nums[], int target, int numsSize) {
|
||||
int n = numsSize;
|
||||
// 求解問題 f(0, n-1)
|
||||
return dfs(nums, target, 0, n - 1);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="binary_search_recur.kt"
|
||||
/* 二分搜尋:問題 f(i, j) */
|
||||
fun dfs(
|
||||
nums: IntArray,
|
||||
target: Int,
|
||||
i: Int,
|
||||
j: Int
|
||||
): Int {
|
||||
// 若區間為空,代表無目標元素,則返回 -1
|
||||
if (i > j) {
|
||||
return -1
|
||||
}
|
||||
// 計算中點索引 m
|
||||
val m = (i + j) / 2
|
||||
return if (nums[m] < target) {
|
||||
// 遞迴子問題 f(m+1, j)
|
||||
dfs(nums, target, m + 1, j)
|
||||
} else if (nums[m] > target) {
|
||||
// 遞迴子問題 f(i, m-1)
|
||||
dfs(nums, target, i, m - 1)
|
||||
} else {
|
||||
// 找到目標元素,返回其索引
|
||||
m
|
||||
}
|
||||
}
|
||||
|
||||
/* 二分搜尋 */
|
||||
fun binarySearch(nums: IntArray, target: Int): Int {
|
||||
val n = nums.size
|
||||
// 求解問題 f(0, n-1)
|
||||
return dfs(nums, target, 0, n - 1)
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="binary_search_recur.rb"
|
||||
[class]{}-[func]{dfs}
|
||||
|
||||
[class]{}-[func]{binary_search}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="binary_search_recur.zig"
|
||||
[class]{}-[func]{dfs}
|
||||
|
||||
[class]{}-[func]{binarySearch}
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20dfs%28nums%3A%20list%5Bint%5D,%20target%3A%20int,%20i%3A%20int,%20j%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%EF%BC%9A%E9%97%AE%E9%A2%98%20f%28i,%20j%29%22%22%22%0A%20%20%20%20%23%20%E8%8B%A5%E5%8C%BA%E9%97%B4%E4%B8%BA%E7%A9%BA%EF%BC%8C%E4%BB%A3%E8%A1%A8%E6%97%A0%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%EF%BC%8C%E5%88%99%E8%BF%94%E5%9B%9E%20-1%0A%20%20%20%20if%20i%20%3E%20j%3A%0A%20%20%20%20%20%20%20%20return%20-1%0A%20%20%20%20%23%20%E8%AE%A1%E7%AE%97%E4%B8%AD%E7%82%B9%E7%B4%A2%E5%BC%95%20m%0A%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20//%202%0A%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%23%20%E9%80%92%E5%BD%92%E5%AD%90%E9%97%AE%E9%A2%98%20f%28m%2B1,%20j%29%0A%20%20%20%20%20%20%20%20return%20dfs%28nums,%20target,%20m%20%2B%201,%20j%29%0A%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%23%20%E9%80%92%E5%BD%92%E5%AD%90%E9%97%AE%E9%A2%98%20f%28i,%20m-1%29%0A%20%20%20%20%20%20%20%20return%20dfs%28nums,%20target,%20i,%20m%20-%201%29%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%89%BE%E5%88%B0%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%EF%BC%8C%E8%BF%94%E5%9B%9E%E5%85%B6%E7%B4%A2%E5%BC%95%0A%20%20%20%20%20%20%20%20return%20m%0A%0Adef%20binary_search%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%22%22%22%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20%23%20%E6%B1%82%E8%A7%A3%E9%97%AE%E9%A2%98%20f%280,%20n-1%29%0A%20%20%20%20return%20dfs%28nums,%20target,%200,%20n%20-%201%29%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20nums%20%3D%20%5B1,%203,%206,%208,%2012,%2015,%2023,%2026,%2031,%2035%5D%0A%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%EF%BC%88%E5%8F%8C%E9%97%AD%E5%8C%BA%E9%97%B4%EF%BC%89%0A%20%20%20%20index%20%3D%20binary_search%28nums,%20target%29%0A%20%20%20%20print%28%22%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%206%20%E7%9A%84%E7%B4%A2%E5%BC%95%20%3D%20%22,%20index%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20dfs%28nums%3A%20list%5Bint%5D,%20target%3A%20int,%20i%3A%20int,%20j%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%EF%BC%9A%E9%97%AE%E9%A2%98%20f%28i,%20j%29%22%22%22%0A%20%20%20%20%23%20%E8%8B%A5%E5%8C%BA%E9%97%B4%E4%B8%BA%E7%A9%BA%EF%BC%8C%E4%BB%A3%E8%A1%A8%E6%97%A0%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%EF%BC%8C%E5%88%99%E8%BF%94%E5%9B%9E%20-1%0A%20%20%20%20if%20i%20%3E%20j%3A%0A%20%20%20%20%20%20%20%20return%20-1%0A%20%20%20%20%23%20%E8%AE%A1%E7%AE%97%E4%B8%AD%E7%82%B9%E7%B4%A2%E5%BC%95%20m%0A%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20//%202%0A%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%23%20%E9%80%92%E5%BD%92%E5%AD%90%E9%97%AE%E9%A2%98%20f%28m%2B1,%20j%29%0A%20%20%20%20%20%20%20%20return%20dfs%28nums,%20target,%20m%20%2B%201,%20j%29%0A%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%23%20%E9%80%92%E5%BD%92%E5%AD%90%E9%97%AE%E9%A2%98%20f%28i,%20m-1%29%0A%20%20%20%20%20%20%20%20return%20dfs%28nums,%20target,%20i,%20m%20-%201%29%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%89%BE%E5%88%B0%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%EF%BC%8C%E8%BF%94%E5%9B%9E%E5%85%B6%E7%B4%A2%E5%BC%95%0A%20%20%20%20%20%20%20%20return%20m%0A%0Adef%20binary_search%28nums%3A%20list%5Bint%5D,%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%22%22%22%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20%23%20%E6%B1%82%E8%A7%A3%E9%97%AE%E9%A2%98%20f%280,%20n-1%29%0A%20%20%20%20return%20dfs%28nums,%20target,%200,%20n%20-%201%29%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20nums%20%3D%20%5B1,%203,%206,%208,%2012,%2015,%2023,%2026,%2031,%2035%5D%0A%0A%20%20%20%20%23%20%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%EF%BC%88%E5%8F%8C%E9%97%AD%E5%8C%BA%E9%97%B4%EF%BC%89%0A%20%20%20%20index%20%3D%20binary_search%28nums,%20target%29%0A%20%20%20%20print%28%22%E7%9B%AE%E6%A0%87%E5%85%83%E7%B4%A0%206%20%E7%9A%84%E7%B4%A2%E5%BC%95%20%3D%20%22,%20index%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
@@ -0,0 +1,539 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 12.3 構建二元樹問題
|
||||
|
||||
!!! question
|
||||
|
||||
給定一棵二元樹的前序走訪 `preorder` 和中序走訪 `inorder` ,請從中構建二元樹,返回二元樹的根節點。假設二元樹中沒有值重複的節點(如圖 12-5 所示)。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-5 構建二元樹的示例資料 </p>
|
||||
|
||||
### 1. 判斷是否為分治問題
|
||||
|
||||
原問題定義為從 `preorder` 和 `inorder` 構建二元樹,是一個典型的分治問題。
|
||||
|
||||
- **問題可以分解**:從分治的角度切入,我們可以將原問題劃分為兩個子問題:構建左子樹、構建右子樹,加上一步操作:初始化根節點。而對於每棵子樹(子問題),我們仍然可以複用以上劃分方法,將其劃分為更小的子樹(子問題),直至達到最小子問題(空子樹)時終止。
|
||||
- **子問題是獨立的**:左子樹和右子樹是相互獨立的,它們之間沒有交集。在構建左子樹時,我們只需關注中序走訪和前序走訪中與左子樹對應的部分。右子樹同理。
|
||||
- **子問題的解可以合併**:一旦得到了左子樹和右子樹(子問題的解),我們就可以將它們連結到根節點上,得到原問題的解。
|
||||
|
||||
### 2. 如何劃分子樹
|
||||
|
||||
根據以上分析,這道題可以使用分治來求解,**但如何透過前序走訪 `preorder` 和中序走訪 `inorder` 來劃分左子樹和右子樹呢**?
|
||||
|
||||
根據定義,`preorder` 和 `inorder` 都可以劃分為三個部分。
|
||||
|
||||
- 前序走訪:`[ 根節點 | 左子樹 | 右子樹 ]` ,例如圖 12-5 的樹對應 `[ 3 | 9 | 2 1 7 ]` 。
|
||||
- 中序走訪:`[ 左子樹 | 根節點 | 右子樹 ]` ,例如圖 12-5 的樹對應 `[ 9 | 3 | 1 2 7 ]` 。
|
||||
|
||||
以上圖資料為例,我們可以透過圖 12-6 所示的步驟得到劃分結果。
|
||||
|
||||
1. 前序走訪的首元素 3 是根節點的值。
|
||||
2. 查詢根節點 3 在 `inorder` 中的索引,利用該索引可將 `inorder` 劃分為 `[ 9 | 3 | 1 2 7 ]` 。
|
||||
3. 根據 `inorder` 的劃分結果,易得左子樹和右子樹的節點數量分別為 1 和 3 ,從而可將 `preorder` 劃分為 `[ 3 | 9 | 2 1 7 ]` 。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-6 在前序走訪和中序走訪中劃分子樹 </p>
|
||||
|
||||
### 3. 基於變數描述子樹區間
|
||||
|
||||
根據以上劃分方法,**我們已經得到根節點、左子樹、右子樹在 `preorder` 和 `inorder` 中的索引區間**。而為了描述這些索引區間,我們需要藉助幾個指標變數。
|
||||
|
||||
- 將當前樹的根節點在 `preorder` 中的索引記為 $i$ 。
|
||||
- 將當前樹的根節點在 `inorder` 中的索引記為 $m$ 。
|
||||
- 將當前樹在 `inorder` 中的索引區間記為 $[l, r]$ 。
|
||||
|
||||
如表 12-1 所示,透過以上變數即可表示根節點在 `preorder` 中的索引,以及子樹在 `inorder` 中的索引區間。
|
||||
|
||||
<p align="center"> 表 12-1 根節點和子樹在前序走訪和中序走訪中的索引 </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
| | 根節點在 `preorder` 中的索引 | 子樹在 `inorder` 中的索引區間 |
|
||||
| ------ | ---------------------------- | ----------------------------- |
|
||||
| 當前樹 | $i$ | $[l, r]$ |
|
||||
| 左子樹 | $i + 1$ | $[l, m-1]$ |
|
||||
| 右子樹 | $i + 1 + (m - l)$ | $[m+1, r]$ |
|
||||
|
||||
</div>
|
||||
|
||||
請注意,右子樹根節點索引中的 $(m-l)$ 的含義是“左子樹的節點數量”,建議結合圖 12-7 理解。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-7 根節點和左右子樹的索引區間表示 </p>
|
||||
|
||||
### 4. 程式碼實現
|
||||
|
||||
為了提升查詢 $m$ 的效率,我們藉助一個雜湊表 `hmap` 來儲存陣列 `inorder` 中元素到索引的對映:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="build_tree.py"
|
||||
def dfs(
|
||||
preorder: list[int],
|
||||
inorder_map: dict[int, int],
|
||||
i: int,
|
||||
l: int,
|
||||
r: int,
|
||||
) -> TreeNode | None:
|
||||
"""構建二元樹:分治"""
|
||||
# 子樹區間為空時終止
|
||||
if r - l < 0:
|
||||
return None
|
||||
# 初始化根節點
|
||||
root = TreeNode(preorder[i])
|
||||
# 查詢 m ,從而劃分左右子樹
|
||||
m = inorder_map[preorder[i]]
|
||||
# 子問題:構建左子樹
|
||||
root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)
|
||||
# 子問題:構建右子樹
|
||||
root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)
|
||||
# 返回根節點
|
||||
return root
|
||||
|
||||
def build_tree(preorder: list[int], inorder: list[int]) -> TreeNode | None:
|
||||
"""構建二元樹"""
|
||||
# 初始化雜湊表,儲存 inorder 元素到索引的對映
|
||||
inorder_map = {val: i for i, val in enumerate(inorder)}
|
||||
root = dfs(preorder, inorder_map, 0, 0, len(inorder) - 1)
|
||||
return root
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="build_tree.cpp"
|
||||
/* 構建二元樹:分治 */
|
||||
TreeNode *dfs(vector<int> &preorder, unordered_map<int, int> &inorderMap, int i, int l, int r) {
|
||||
// 子樹區間為空時終止
|
||||
if (r - l < 0)
|
||||
return NULL;
|
||||
// 初始化根節點
|
||||
TreeNode *root = new TreeNode(preorder[i]);
|
||||
// 查詢 m ,從而劃分左右子樹
|
||||
int m = inorderMap[preorder[i]];
|
||||
// 子問題:構建左子樹
|
||||
root->left = dfs(preorder, inorderMap, i + 1, l, m - 1);
|
||||
// 子問題:構建右子樹
|
||||
root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
|
||||
// 返回根節點
|
||||
return root;
|
||||
}
|
||||
|
||||
/* 構建二元樹 */
|
||||
TreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {
|
||||
// 初始化雜湊表,儲存 inorder 元素到索引的對映
|
||||
unordered_map<int, int> inorderMap;
|
||||
for (int i = 0; i < inorder.size(); i++) {
|
||||
inorderMap[inorder[i]] = i;
|
||||
}
|
||||
TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorder.size() - 1);
|
||||
return root;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="build_tree.java"
|
||||
/* 構建二元樹:分治 */
|
||||
TreeNode dfs(int[] preorder, Map<Integer, Integer> inorderMap, int i, int l, int r) {
|
||||
// 子樹區間為空時終止
|
||||
if (r - l < 0)
|
||||
return null;
|
||||
// 初始化根節點
|
||||
TreeNode root = new TreeNode(preorder[i]);
|
||||
// 查詢 m ,從而劃分左右子樹
|
||||
int m = inorderMap.get(preorder[i]);
|
||||
// 子問題:構建左子樹
|
||||
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);
|
||||
// 子問題:構建右子樹
|
||||
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
|
||||
// 返回根節點
|
||||
return root;
|
||||
}
|
||||
|
||||
/* 構建二元樹 */
|
||||
TreeNode buildTree(int[] preorder, int[] inorder) {
|
||||
// 初始化雜湊表,儲存 inorder 元素到索引的對映
|
||||
Map<Integer, Integer> inorderMap = new HashMap<>();
|
||||
for (int i = 0; i < inorder.length; i++) {
|
||||
inorderMap.put(inorder[i], i);
|
||||
}
|
||||
TreeNode root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);
|
||||
return root;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="build_tree.cs"
|
||||
/* 構建二元樹:分治 */
|
||||
TreeNode? DFS(int[] preorder, Dictionary<int, int> inorderMap, int i, int l, int r) {
|
||||
// 子樹區間為空時終止
|
||||
if (r - l < 0)
|
||||
return null;
|
||||
// 初始化根節點
|
||||
TreeNode root = new(preorder[i]);
|
||||
// 查詢 m ,從而劃分左右子樹
|
||||
int m = inorderMap[preorder[i]];
|
||||
// 子問題:構建左子樹
|
||||
root.left = DFS(preorder, inorderMap, i + 1, l, m - 1);
|
||||
// 子問題:構建右子樹
|
||||
root.right = DFS(preorder, inorderMap, i + 1 + m - l, m + 1, r);
|
||||
// 返回根節點
|
||||
return root;
|
||||
}
|
||||
|
||||
/* 構建二元樹 */
|
||||
TreeNode? BuildTree(int[] preorder, int[] inorder) {
|
||||
// 初始化雜湊表,儲存 inorder 元素到索引的對映
|
||||
Dictionary<int, int> inorderMap = [];
|
||||
for (int i = 0; i < inorder.Length; i++) {
|
||||
inorderMap.TryAdd(inorder[i], i);
|
||||
}
|
||||
TreeNode? root = DFS(preorder, inorderMap, 0, 0, inorder.Length - 1);
|
||||
return root;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="build_tree.go"
|
||||
/* 構建二元樹:分治 */
|
||||
func dfsBuildTree(preorder []int, inorderMap map[int]int, i, l, r int) *TreeNode {
|
||||
// 子樹區間為空時終止
|
||||
if r-l < 0 {
|
||||
return nil
|
||||
}
|
||||
// 初始化根節點
|
||||
root := NewTreeNode(preorder[i])
|
||||
// 查詢 m ,從而劃分左右子樹
|
||||
m := inorderMap[preorder[i]]
|
||||
// 子問題:構建左子樹
|
||||
root.Left = dfsBuildTree(preorder, inorderMap, i+1, l, m-1)
|
||||
// 子問題:構建右子樹
|
||||
root.Right = dfsBuildTree(preorder, inorderMap, i+1+m-l, m+1, r)
|
||||
// 返回根節點
|
||||
return root
|
||||
}
|
||||
|
||||
/* 構建二元樹 */
|
||||
func buildTree(preorder, inorder []int) *TreeNode {
|
||||
// 初始化雜湊表,儲存 inorder 元素到索引的對映
|
||||
inorderMap := make(map[int]int, len(inorder))
|
||||
for i := 0; i < len(inorder); i++ {
|
||||
inorderMap[inorder[i]] = i
|
||||
}
|
||||
|
||||
root := dfsBuildTree(preorder, inorderMap, 0, 0, len(inorder)-1)
|
||||
return root
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="build_tree.swift"
|
||||
/* 構建二元樹:分治 */
|
||||
func dfs(preorder: [Int], inorderMap: [Int: Int], i: Int, l: Int, r: Int) -> TreeNode? {
|
||||
// 子樹區間為空時終止
|
||||
if r - l < 0 {
|
||||
return nil
|
||||
}
|
||||
// 初始化根節點
|
||||
let root = TreeNode(x: preorder[i])
|
||||
// 查詢 m ,從而劃分左右子樹
|
||||
let m = inorderMap[preorder[i]]!
|
||||
// 子問題:構建左子樹
|
||||
root.left = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1, l: l, r: m - 1)
|
||||
// 子問題:構建右子樹
|
||||
root.right = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1 + m - l, l: m + 1, r: r)
|
||||
// 返回根節點
|
||||
return root
|
||||
}
|
||||
|
||||
/* 構建二元樹 */
|
||||
func buildTree(preorder: [Int], inorder: [Int]) -> TreeNode? {
|
||||
// 初始化雜湊表,儲存 inorder 元素到索引的對映
|
||||
let inorderMap = inorder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset }
|
||||
return dfs(preorder: preorder, inorderMap: inorderMap, i: inorder.startIndex, l: inorder.startIndex, r: inorder.endIndex - 1)
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="build_tree.js"
|
||||
/* 構建二元樹:分治 */
|
||||
function dfs(preorder, inorderMap, i, l, r) {
|
||||
// 子樹區間為空時終止
|
||||
if (r - l < 0) return null;
|
||||
// 初始化根節點
|
||||
const root = new TreeNode(preorder[i]);
|
||||
// 查詢 m ,從而劃分左右子樹
|
||||
const m = inorderMap.get(preorder[i]);
|
||||
// 子問題:構建左子樹
|
||||
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);
|
||||
// 子問題:構建右子樹
|
||||
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
|
||||
// 返回根節點
|
||||
return root;
|
||||
}
|
||||
|
||||
/* 構建二元樹 */
|
||||
function buildTree(preorder, inorder) {
|
||||
// 初始化雜湊表,儲存 inorder 元素到索引的對映
|
||||
let inorderMap = new Map();
|
||||
for (let i = 0; i < inorder.length; i++) {
|
||||
inorderMap.set(inorder[i], i);
|
||||
}
|
||||
const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);
|
||||
return root;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="build_tree.ts"
|
||||
/* 構建二元樹:分治 */
|
||||
function dfs(
|
||||
preorder: number[],
|
||||
inorderMap: Map<number, number>,
|
||||
i: number,
|
||||
l: number,
|
||||
r: number
|
||||
): TreeNode | null {
|
||||
// 子樹區間為空時終止
|
||||
if (r - l < 0) return null;
|
||||
// 初始化根節點
|
||||
const root: TreeNode = new TreeNode(preorder[i]);
|
||||
// 查詢 m ,從而劃分左右子樹
|
||||
const m = inorderMap.get(preorder[i]);
|
||||
// 子問題:構建左子樹
|
||||
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);
|
||||
// 子問題:構建右子樹
|
||||
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
|
||||
// 返回根節點
|
||||
return root;
|
||||
}
|
||||
|
||||
/* 構建二元樹 */
|
||||
function buildTree(preorder: number[], inorder: number[]): TreeNode | null {
|
||||
// 初始化雜湊表,儲存 inorder 元素到索引的對映
|
||||
let inorderMap = new Map<number, number>();
|
||||
for (let i = 0; i < inorder.length; i++) {
|
||||
inorderMap.set(inorder[i], i);
|
||||
}
|
||||
const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);
|
||||
return root;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="build_tree.dart"
|
||||
/* 構建二元樹:分治 */
|
||||
TreeNode? dfs(
|
||||
List<int> preorder,
|
||||
Map<int, int> inorderMap,
|
||||
int i,
|
||||
int l,
|
||||
int r,
|
||||
) {
|
||||
// 子樹區間為空時終止
|
||||
if (r - l < 0) {
|
||||
return null;
|
||||
}
|
||||
// 初始化根節點
|
||||
TreeNode? root = TreeNode(preorder[i]);
|
||||
// 查詢 m ,從而劃分左右子樹
|
||||
int m = inorderMap[preorder[i]]!;
|
||||
// 子問題:構建左子樹
|
||||
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);
|
||||
// 子問題:構建右子樹
|
||||
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
|
||||
// 返回根節點
|
||||
return root;
|
||||
}
|
||||
|
||||
/* 構建二元樹 */
|
||||
TreeNode? buildTree(List<int> preorder, List<int> inorder) {
|
||||
// 初始化雜湊表,儲存 inorder 元素到索引的對映
|
||||
Map<int, int> inorderMap = {};
|
||||
for (int i = 0; i < inorder.length; i++) {
|
||||
inorderMap[inorder[i]] = i;
|
||||
}
|
||||
TreeNode? root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);
|
||||
return root;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="build_tree.rs"
|
||||
/* 構建二元樹:分治 */
|
||||
fn dfs(
|
||||
preorder: &[i32],
|
||||
inorder_map: &HashMap<i32, i32>,
|
||||
i: i32,
|
||||
l: i32,
|
||||
r: i32,
|
||||
) -> Option<Rc<RefCell<TreeNode>>> {
|
||||
// 子樹區間為空時終止
|
||||
if r - l < 0 {
|
||||
return None;
|
||||
}
|
||||
// 初始化根節點
|
||||
let root = TreeNode::new(preorder[i as usize]);
|
||||
// 查詢 m ,從而劃分左右子樹
|
||||
let m = inorder_map.get(&preorder[i as usize]).unwrap();
|
||||
// 子問題:構建左子樹
|
||||
root.borrow_mut().left = dfs(preorder, inorder_map, i + 1, l, m - 1);
|
||||
// 子問題:構建右子樹
|
||||
root.borrow_mut().right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r);
|
||||
// 返回根節點
|
||||
Some(root)
|
||||
}
|
||||
|
||||
/* 構建二元樹 */
|
||||
fn build_tree(preorder: &[i32], inorder: &[i32]) -> Option<Rc<RefCell<TreeNode>>> {
|
||||
// 初始化雜湊表,儲存 inorder 元素到索引的對映
|
||||
let mut inorder_map: HashMap<i32, i32> = HashMap::new();
|
||||
for i in 0..inorder.len() {
|
||||
inorder_map.insert(inorder[i], i as i32);
|
||||
}
|
||||
let root = dfs(preorder, &inorder_map, 0, 0, inorder.len() as i32 - 1);
|
||||
root
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="build_tree.c"
|
||||
/* 構建二元樹:分治 */
|
||||
TreeNode *dfs(int *preorder, int *inorderMap, int i, int l, int r, int size) {
|
||||
// 子樹區間為空時終止
|
||||
if (r - l < 0)
|
||||
return NULL;
|
||||
// 初始化根節點
|
||||
TreeNode *root = (TreeNode *)malloc(sizeof(TreeNode));
|
||||
root->val = preorder[i];
|
||||
root->left = NULL;
|
||||
root->right = NULL;
|
||||
// 查詢 m ,從而劃分左右子樹
|
||||
int m = inorderMap[preorder[i]];
|
||||
// 子問題:構建左子樹
|
||||
root->left = dfs(preorder, inorderMap, i + 1, l, m - 1, size);
|
||||
// 子問題:構建右子樹
|
||||
root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r, size);
|
||||
// 返回根節點
|
||||
return root;
|
||||
}
|
||||
|
||||
/* 構建二元樹 */
|
||||
TreeNode *buildTree(int *preorder, int preorderSize, int *inorder, int inorderSize) {
|
||||
// 初始化雜湊表,儲存 inorder 元素到索引的對映
|
||||
int *inorderMap = (int *)malloc(sizeof(int) * MAX_SIZE);
|
||||
for (int i = 0; i < inorderSize; i++) {
|
||||
inorderMap[inorder[i]] = i;
|
||||
}
|
||||
TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorderSize - 1, inorderSize);
|
||||
free(inorderMap);
|
||||
return root;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="build_tree.kt"
|
||||
/* 構建二元樹:分治 */
|
||||
fun dfs(preorder: IntArray, inorderMap: Map<Int?, Int?>, i: Int, l: Int, r: Int): TreeNode? {
|
||||
// 子樹區間為空時終止
|
||||
if (r - l < 0) return null
|
||||
// 初始化根節點
|
||||
val root = TreeNode(preorder[i])
|
||||
// 查詢 m ,從而劃分左右子樹
|
||||
val m = inorderMap[preorder[i]]!!
|
||||
// 子問題:構建左子樹
|
||||
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1)
|
||||
// 子問題:構建右子樹
|
||||
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r)
|
||||
// 返回根節點
|
||||
return root
|
||||
}
|
||||
|
||||
/* 構建二元樹 */
|
||||
fun buildTree(preorder: IntArray, inorder: IntArray): TreeNode? {
|
||||
// 初始化雜湊表,儲存 inorder 元素到索引的對映
|
||||
val inorderMap: MutableMap<Int?, Int?> = HashMap()
|
||||
for (i in inorder.indices) {
|
||||
inorderMap[inorder[i]] = i
|
||||
}
|
||||
val root = dfs(preorder, inorderMap, 0, 0, inorder.size - 1)
|
||||
return root
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="build_tree.rb"
|
||||
[class]{}-[func]{dfs}
|
||||
|
||||
[class]{}-[func]{build_tree}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="build_tree.zig"
|
||||
[class]{}-[func]{dfs}
|
||||
|
||||
[class]{}-[func]{buildTree}
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%20%3D%200%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0Adef%20dfs%28%0A%20%20%20%20preorder%3A%20list%5Bint%5D,%0A%20%20%20%20inorder_map%3A%20dict%5Bint,%20int%5D,%0A%20%20%20%20i%3A%20int,%0A%20%20%20%20l%3A%20int,%0A%20%20%20%20r%3A%20int,%0A%29%20-%3E%20TreeNode%20%7C%20None%3A%0A%20%20%20%20%22%22%22%E6%9E%84%E5%BB%BA%E4%BA%8C%E5%8F%89%E6%A0%91%EF%BC%9A%E5%88%86%E6%B2%BB%22%22%22%0A%20%20%20%20%23%20%E5%AD%90%E6%A0%91%E5%8C%BA%E9%97%B4%E4%B8%BA%E7%A9%BA%E6%97%B6%E7%BB%88%E6%AD%A2%0A%20%20%20%20if%20r%20-%20l%20%3C%200%3A%0A%20%20%20%20%20%20%20%20return%20None%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%A0%B9%E8%8A%82%E7%82%B9%0A%20%20%20%20root%20%3D%20TreeNode%28preorder%5Bi%5D%29%0A%20%20%20%20%23%20%E6%9F%A5%E8%AF%A2%20m%20%EF%BC%8C%E4%BB%8E%E8%80%8C%E5%88%92%E5%88%86%E5%B7%A6%E5%8F%B3%E5%AD%90%E6%A0%91%0A%20%20%20%20m%20%3D%20inorder_map%5Bpreorder%5Bi%5D%5D%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%EF%BC%9A%E6%9E%84%E5%BB%BA%E5%B7%A6%E5%AD%90%E6%A0%91%0A%20%20%20%20root.left%20%3D%20dfs%28preorder,%20inorder_map,%20i%20%2B%201,%20l,%20m%20-%201%29%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%EF%BC%9A%E6%9E%84%E5%BB%BA%E5%8F%B3%E5%AD%90%E6%A0%91%0A%20%20%20%20root.right%20%3D%20dfs%28preorder,%20inorder_map,%20i%20%2B%201%20%2B%20m%20-%20l,%20m%20%2B%201,%20r%29%0A%20%20%20%20%23%20%E8%BF%94%E5%9B%9E%E6%A0%B9%E8%8A%82%E7%82%B9%0A%20%20%20%20return%20root%0A%0A%0Adef%20build_tree%28preorder%3A%20list%5Bint%5D,%20inorder%3A%20list%5Bint%5D%29%20-%3E%20TreeNode%20%7C%20None%3A%0A%20%20%20%20%22%22%22%E6%9E%84%E5%BB%BA%E4%BA%8C%E5%8F%89%E6%A0%91%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%EF%BC%8C%E5%AD%98%E5%82%A8%20inorder%20%E5%85%83%E7%B4%A0%E5%88%B0%E7%B4%A2%E5%BC%95%E7%9A%84%E6%98%A0%E5%B0%84%0A%20%20%20%20inorder_map%20%3D%20%7Bval%3A%20i%20for%20i,%20val%20in%20enumerate%28inorder%29%7D%0A%20%20%20%20root%20%3D%20dfs%28preorder,%20inorder_map,%200,%200,%20len%28inorder%29%20-%201%29%0A%20%20%20%20return%20root%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20preorder%20%3D%20%5B3,%209,%202,%201,%207%5D%0A%20%20%20%20inorder%20%3D%20%5B9,%203,%201,%202,%207%5D%0A%20%20%20%20print%28f%22%E5%89%8D%E5%BA%8F%E9%81%8D%E5%8E%86%20%3D%20%7Bpreorder%7D%22%29%0A%20%20%20%20print%28f%22%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%20%3D%20%7Binorder%7D%22%29%0A%20%20%20%20root%20%3D%20build_tree%28preorder,%20inorder%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=21&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%20%3D%200%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0Adef%20dfs%28%0A%20%20%20%20preorder%3A%20list%5Bint%5D,%0A%20%20%20%20inorder_map%3A%20dict%5Bint,%20int%5D,%0A%20%20%20%20i%3A%20int,%0A%20%20%20%20l%3A%20int,%0A%20%20%20%20r%3A%20int,%0A%29%20-%3E%20TreeNode%20%7C%20None%3A%0A%20%20%20%20%22%22%22%E6%9E%84%E5%BB%BA%E4%BA%8C%E5%8F%89%E6%A0%91%EF%BC%9A%E5%88%86%E6%B2%BB%22%22%22%0A%20%20%20%20%23%20%E5%AD%90%E6%A0%91%E5%8C%BA%E9%97%B4%E4%B8%BA%E7%A9%BA%E6%97%B6%E7%BB%88%E6%AD%A2%0A%20%20%20%20if%20r%20-%20l%20%3C%200%3A%0A%20%20%20%20%20%20%20%20return%20None%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%A0%B9%E8%8A%82%E7%82%B9%0A%20%20%20%20root%20%3D%20TreeNode%28preorder%5Bi%5D%29%0A%20%20%20%20%23%20%E6%9F%A5%E8%AF%A2%20m%20%EF%BC%8C%E4%BB%8E%E8%80%8C%E5%88%92%E5%88%86%E5%B7%A6%E5%8F%B3%E5%AD%90%E6%A0%91%0A%20%20%20%20m%20%3D%20inorder_map%5Bpreorder%5Bi%5D%5D%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%EF%BC%9A%E6%9E%84%E5%BB%BA%E5%B7%A6%E5%AD%90%E6%A0%91%0A%20%20%20%20root.left%20%3D%20dfs%28preorder,%20inorder_map,%20i%20%2B%201,%20l,%20m%20-%201%29%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%EF%BC%9A%E6%9E%84%E5%BB%BA%E5%8F%B3%E5%AD%90%E6%A0%91%0A%20%20%20%20root.right%20%3D%20dfs%28preorder,%20inorder_map,%20i%20%2B%201%20%2B%20m%20-%20l,%20m%20%2B%201,%20r%29%0A%20%20%20%20%23%20%E8%BF%94%E5%9B%9E%E6%A0%B9%E8%8A%82%E7%82%B9%0A%20%20%20%20return%20root%0A%0A%0Adef%20build_tree%28preorder%3A%20list%5Bint%5D,%20inorder%3A%20list%5Bint%5D%29%20-%3E%20TreeNode%20%7C%20None%3A%0A%20%20%20%20%22%22%22%E6%9E%84%E5%BB%BA%E4%BA%8C%E5%8F%89%E6%A0%91%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%EF%BC%8C%E5%AD%98%E5%82%A8%20inorder%20%E5%85%83%E7%B4%A0%E5%88%B0%E7%B4%A2%E5%BC%95%E7%9A%84%E6%98%A0%E5%B0%84%0A%20%20%20%20inorder_map%20%3D%20%7Bval%3A%20i%20for%20i,%20val%20in%20enumerate%28inorder%29%7D%0A%20%20%20%20root%20%3D%20dfs%28preorder,%20inorder_map,%200,%200,%20len%28inorder%29%20-%201%29%0A%20%20%20%20return%20root%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20preorder%20%3D%20%5B3,%209,%202,%201,%207%5D%0A%20%20%20%20inorder%20%3D%20%5B9,%203,%201,%202,%207%5D%0A%20%20%20%20print%28f%22%E5%89%8D%E5%BA%8F%E9%81%8D%E5%8E%86%20%3D%20%7Bpreorder%7D%22%29%0A%20%20%20%20print%28f%22%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%20%3D%20%7Binorder%7D%22%29%0A%20%20%20%20root%20%3D%20build_tree%28preorder,%20inorder%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=21&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
圖 12-8 展示了構建二元樹的遞迴過程,各個節點是在向下“遞”的過程中建立的,而各條邊(引用)是在向上“迴”的過程中建立的。
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<3>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<4>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<5>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<6>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<7>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<8>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<9>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-8 構建二元樹的遞迴過程 </p>
|
||||
|
||||
每個遞迴函式內的前序走訪 `preorder` 和中序走訪 `inorder` 的劃分結果如圖 12-9 所示。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-9 每個遞迴函式中的劃分結果 </p>
|
||||
|
||||
設樹的節點數量為 $n$ ,初始化每一個節點(執行一個遞迴函式 `dfs()` )使用 $O(1)$ 時間。**因此總體時間複雜度為 $O(n)$** 。
|
||||
|
||||
雜湊表儲存 `inorder` 元素到索引的對映,空間複雜度為 $O(n)$ 。在最差情況下,即二元樹退化為鏈結串列時,遞迴深度達到 $n$ ,使用 $O(n)$ 的堆疊幀空間。**因此總體空間複雜度為 $O(n)$** 。
|
||||
101
zh-Hant/docs/chapter_divide_and_conquer/divide_and_conquer.md
Normal file
101
zh-Hant/docs/chapter_divide_and_conquer/divide_and_conquer.md
Normal file
@@ -0,0 +1,101 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 12.1 分治演算法
|
||||
|
||||
<u>分治(divide and conquer)</u>,全稱分而治之,是一種非常重要且常見的演算法策略。分治通常基於遞迴實現,包括“分”和“治”兩個步驟。
|
||||
|
||||
1. **分(劃分階段)**:遞迴地將原問題分解為兩個或多個子問題,直至到達最小子問題時終止。
|
||||
2. **治(合併階段)**:從已知解的最小子問題開始,從底至頂地將子問題的解進行合併,從而構建出原問題的解。
|
||||
|
||||
如圖 12-1 所示,“合併排序”是分治策略的典型應用之一。
|
||||
|
||||
1. **分**:遞迴地將原陣列(原問題)劃分為兩個子陣列(子問題),直到子陣列只剩一個元素(最小子問題)。
|
||||
2. **治**:從底至頂地將有序的子陣列(子問題的解)進行合併,從而得到有序的原陣列(原問題的解)。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-1 合併排序的分治策略 </p>
|
||||
|
||||
## 12.1.1 如何判斷分治問題
|
||||
|
||||
一個問題是否適合使用分治解決,通常可以參考以下幾個判斷依據。
|
||||
|
||||
1. **問題可以分解**:原問題可以分解成規模更小、類似的子問題,以及能夠以相同方式遞迴地進行劃分。
|
||||
2. **子問題是獨立的**:子問題之間沒有重疊,互不依賴,可以獨立解決。
|
||||
3. **子問題的解可以合併**:原問題的解透過合併子問題的解得來。
|
||||
|
||||
顯然,合併排序滿足以上三個判斷依據。
|
||||
|
||||
1. **問題可以分解**:遞迴地將陣列(原問題)劃分為兩個子陣列(子問題)。
|
||||
2. **子問題是獨立的**:每個子陣列都可以獨立地進行排序(子問題可以獨立進行求解)。
|
||||
3. **子問題的解可以合併**:兩個有序子陣列(子問題的解)可以合併為一個有序陣列(原問題的解)。
|
||||
|
||||
## 12.1.2 透過分治提升效率
|
||||
|
||||
**分治不僅可以有效地解決演算法問題,往往還可以提升演算法效率**。在排序演算法中,快速排序、合併排序、堆積排序相較於選擇、冒泡、插入排序更快,就是因為它們應用了分治策略。
|
||||
|
||||
那麼,我們不禁發問:**為什麼分治可以提升演算法效率,其底層邏輯是什麼**?換句話說,將大問題分解為多個子問題、解決子問題、將子問題的解合併為原問題的解,這幾步的效率為什麼比直接解決原問題的效率更高?這個問題可以從操作數量和平行計算兩方面來討論。
|
||||
|
||||
### 1. 操作數量最佳化
|
||||
|
||||
以“泡沫排序”為例,其處理一個長度為 $n$ 的陣列需要 $O(n^2)$ 時間。假設我們按照圖 12-2 所示的方式,將陣列從中點處分為兩個子陣列,則劃分需要 $O(n)$ 時間,排序每個子陣列需要 $O((n / 2)^2)$ 時間,合併兩個子陣列需要 $O(n)$ 時間,總體時間複雜度為:
|
||||
|
||||
$$
|
||||
O(n + (\frac{n}{2})^2 \times 2 + n) = O(\frac{n^2}{2} + 2n)
|
||||
$$
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-2 劃分陣列前後的泡沫排序 </p>
|
||||
|
||||
接下來,我們計算以下不等式,其左邊和右邊分別為劃分前和劃分後的操作總數:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
n^2 & > \frac{n^2}{2} + 2n \newline
|
||||
n^2 - \frac{n^2}{2} - 2n & > 0 \newline
|
||||
n(n - 4) & > 0
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
**這意味著當 $n > 4$ 時,劃分後的操作數量更少,排序效率應該更高**。請注意,劃分後的時間複雜度仍然是平方階 $O(n^2)$ ,只是複雜度中的常數項變小了。
|
||||
|
||||
進一步想,**如果我們把子陣列不斷地再從中點處劃分為兩個子陣列**,直至子陣列只剩一個元素時停止劃分呢?這種思路實際上就是“合併排序”,時間複雜度為 $O(n \log n)$ 。
|
||||
|
||||
再思考,**如果我們多設定幾個劃分點**,將原陣列平均劃分為 $k$ 個子陣列呢?這種情況與“桶排序”非常類似,它非常適合排序海量資料,理論上時間複雜度可以達到 $O(n + k)$ 。
|
||||
|
||||
### 2. 平行計算最佳化
|
||||
|
||||
我們知道,分治生成的子問題是相互獨立的,**因此通常可以並行解決**。也就是說,分治不僅可以降低演算法的時間複雜度,**還有利於作業系統的並行最佳化**。
|
||||
|
||||
並行最佳化在多核或多處理器的環境中尤其有效,因為系統可以同時處理多個子問題,更加充分地利用計算資源,從而顯著減少總體的執行時間。
|
||||
|
||||
比如在圖 12-3 所示的“桶排序”中,我們將海量的資料平均分配到各個桶中,則可所有桶的排序任務分散到各個計算單元,完成後再合併結果。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-3 桶排序的平行計算 </p>
|
||||
|
||||
## 12.1.3 分治常見應用
|
||||
|
||||
一方面,分治可以用來解決許多經典演算法問題。
|
||||
|
||||
- **尋找最近點對**:該演算法首先將點集分成兩部分,然後分別找出兩部分中的最近點對,最後找出跨越兩部分的最近點對。
|
||||
- **大整數乘法**:例如 Karatsuba 演算法,它將大整數乘法分解為幾個較小的整數的乘法和加法。
|
||||
- **矩陣乘法**:例如 Strassen 演算法,它將大矩陣乘法分解為多個小矩陣的乘法和加法。
|
||||
- **河內塔問題**:河內塔問題可以透過遞迴解決,這是典型的分治策略應用。
|
||||
- **求解逆序對**:在一個序列中,如果前面的數字大於後面的數字,那麼這兩個數字構成一個逆序對。求解逆序對問題可以利用分治的思想,藉助合併排序進行求解。
|
||||
|
||||
另一方面,分治在演算法和資料結構的設計中應用得非常廣泛。
|
||||
|
||||
- **二分搜尋**:二分搜尋是將有序陣列從中點索引處分為兩部分,然後根據目標值與中間元素值比較結果,決定排除哪一半區間,並在剩餘區間執行相同的二分操作。
|
||||
- **合併排序**:本節開頭已介紹,不再贅述。
|
||||
- **快速排序**:快速排序是選取一個基準值,然後把陣列分為兩個子陣列,一個子陣列的元素比基準值小,另一子陣列的元素比基準值大,再對這兩部分進行相同的劃分操作,直至子陣列只剩下一個元素。
|
||||
- **桶排序**:桶排序的基本思想是將資料分散到多個桶,然後對每個桶內的元素進行排序,最後將各個桶的元素依次取出,從而得到一個有序陣列。
|
||||
- **樹**:例如二元搜尋樹、AVL 樹、紅黑樹、B 樹、B+ 樹等,它們的查詢、插入和刪除等操作都可以視為分治策略的應用。
|
||||
- **堆積**:堆積是一種特殊的完全二元樹,其各種操作,如插入、刪除和堆積化,實際上都隱含了分治的思想。
|
||||
- **雜湊表**:雖然雜湊表並不直接應用分治,但某些雜湊衝突解決方案間接應用了分治策略,例如,鏈式位址中的長鏈結串列會被轉化為紅黑樹,以提升查詢效率。
|
||||
|
||||
可以看出,**分治是一種“潤物細無聲”的演算法思想**,隱含在各種演算法與資料結構之中。
|
||||
545
zh-Hant/docs/chapter_divide_and_conquer/hanota_problem.md
Normal file
545
zh-Hant/docs/chapter_divide_and_conquer/hanota_problem.md
Normal file
@@ -0,0 +1,545 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 12.4 河內塔問題
|
||||
|
||||
在合併排序和構建二元樹中,我們都是將原問題分解為兩個規模為原問題一半的子問題。然而對於河內塔問題,我們採用不同的分解策略。
|
||||
|
||||
!!! question
|
||||
|
||||
給定三根柱子,記為 `A`、`B` 和 `C` 。起始狀態下,柱子 `A` 上套著 $n$ 個圓盤,它們從上到下按照從小到大的順序排列。我們的任務是要把這 $n$ 個圓盤移到柱子 `C` 上,並保持它們的原有順序不變(如圖 12-10 所示)。在移動圓盤的過程中,需要遵守以下規則。
|
||||
|
||||
1. 圓盤只能從一根柱子頂部拿出,從另一根柱子頂部放入。
|
||||
2. 每次只能移動一個圓盤。
|
||||
3. 小圓盤必須時刻位於大圓盤之上。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-10 河內塔問題示例 </p>
|
||||
|
||||
**我們將規模為 $i$ 的河內塔問題記作 $f(i)$** 。例如 $f(3)$ 代表將 $3$ 個圓盤從 `A` 移動至 `C` 的河內塔問題。
|
||||
|
||||
### 1. 考慮基本情況
|
||||
|
||||
如圖 12-11 所示,對於問題 $f(1)$ ,即當只有一個圓盤時,我們將它直接從 `A` 移動至 `C` 即可。
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-11 規模為 1 的問題的解 </p>
|
||||
|
||||
如圖 12-12 所示,對於問題 $f(2)$ ,即當有兩個圓盤時,**由於要時刻滿足小圓盤在大圓盤之上,因此需要藉助 `B` 來完成移動**。
|
||||
|
||||
1. 先將上面的小圓盤從 `A` 移至 `B` 。
|
||||
2. 再將大圓盤從 `A` 移至 `C` 。
|
||||
3. 最後將小圓盤從 `B` 移至 `C` 。
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<3>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<4>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-12 規模為 2 的問題的解 </p>
|
||||
|
||||
解決問題 $f(2)$ 的過程可總結為:**將兩個圓盤藉助 `B` 從 `A` 移至 `C`** 。其中,`C` 稱為目標柱、`B` 稱為緩衝柱。
|
||||
|
||||
### 2. 子問題分解
|
||||
|
||||
對於問題 $f(3)$ ,即當有三個圓盤時,情況變得稍微複雜了一些。
|
||||
|
||||
因為已知 $f(1)$ 和 $f(2)$ 的解,所以我們可從分治角度思考,**將 `A` 頂部的兩個圓盤看作一個整體**,執行圖 12-13 所示的步驟。這樣三個圓盤就被順利地從 `A` 移至 `C` 了。
|
||||
|
||||
1. 令 `B` 為目標柱、`C` 為緩衝柱,將兩個圓盤從 `A` 移至 `B` 。
|
||||
2. 將 `A` 中剩餘的一個圓盤從 `A` 直接移動至 `C` 。
|
||||
3. 令 `C` 為目標柱、`A` 為緩衝柱,將兩個圓盤從 `B` 移至 `C` 。
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<3>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<4>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-13 規模為 3 的問題的解 </p>
|
||||
|
||||
從本質上看,**我們將問題 $f(3)$ 劃分為兩個子問題 $f(2)$ 和一個子問題 $f(1)$** 。按順序解決這三個子問題之後,原問題隨之得到解決。這說明子問題是獨立的,而且解可以合併。
|
||||
|
||||
至此,我們可總結出圖 12-14 所示的解決河內塔問題的分治策略:將原問題 $f(n)$ 劃分為兩個子問題 $f(n-1)$ 和一個子問題 $f(1)$ ,並按照以下順序解決這三個子問題。
|
||||
|
||||
1. 將 $n-1$ 個圓盤藉助 `C` 從 `A` 移至 `B` 。
|
||||
2. 將剩餘 $1$ 個圓盤從 `A` 直接移至 `C` 。
|
||||
3. 將 $n-1$ 個圓盤藉助 `A` 從 `B` 移至 `C` 。
|
||||
|
||||
對於這兩個子問題 $f(n-1)$ ,**可以透過相同的方式進行遞迴劃分**,直至達到最小子問題 $f(1)$ 。而 $f(1)$ 的解是已知的,只需一次移動操作即可。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-14 解決河內塔問題的分治策略 </p>
|
||||
|
||||
### 3. 程式碼實現
|
||||
|
||||
在程式碼中,我們宣告一個遞迴函式 `dfs(i, src, buf, tar)` ,它的作用是將柱 `src` 頂部的 $i$ 個圓盤藉助緩衝柱 `buf` 移動至目標柱 `tar` :
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="hanota.py"
|
||||
def move(src: list[int], tar: list[int]):
|
||||
"""移動一個圓盤"""
|
||||
# 從 src 頂部拿出一個圓盤
|
||||
pan = src.pop()
|
||||
# 將圓盤放入 tar 頂部
|
||||
tar.append(pan)
|
||||
|
||||
def dfs(i: int, src: list[int], buf: list[int], tar: list[int]):
|
||||
"""求解河內塔問題 f(i)"""
|
||||
# 若 src 只剩下一個圓盤,則直接將其移到 tar
|
||||
if i == 1:
|
||||
move(src, tar)
|
||||
return
|
||||
# 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
|
||||
dfs(i - 1, src, tar, buf)
|
||||
# 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
|
||||
move(src, tar)
|
||||
# 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
|
||||
dfs(i - 1, buf, src, tar)
|
||||
|
||||
def solve_hanota(A: list[int], B: list[int], C: list[int]):
|
||||
"""求解河內塔問題"""
|
||||
n = len(A)
|
||||
# 將 A 頂部 n 個圓盤藉助 B 移到 C
|
||||
dfs(n, A, B, C)
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="hanota.cpp"
|
||||
/* 移動一個圓盤 */
|
||||
void move(vector<int> &src, vector<int> &tar) {
|
||||
// 從 src 頂部拿出一個圓盤
|
||||
int pan = src.back();
|
||||
src.pop_back();
|
||||
// 將圓盤放入 tar 頂部
|
||||
tar.push_back(pan);
|
||||
}
|
||||
|
||||
/* 求解河內塔問題 f(i) */
|
||||
void dfs(int i, vector<int> &src, vector<int> &buf, vector<int> &tar) {
|
||||
// 若 src 只剩下一個圓盤,則直接將其移到 tar
|
||||
if (i == 1) {
|
||||
move(src, tar);
|
||||
return;
|
||||
}
|
||||
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
|
||||
dfs(i - 1, src, tar, buf);
|
||||
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
|
||||
move(src, tar);
|
||||
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
|
||||
dfs(i - 1, buf, src, tar);
|
||||
}
|
||||
|
||||
/* 求解河內塔問題 */
|
||||
void solveHanota(vector<int> &A, vector<int> &B, vector<int> &C) {
|
||||
int n = A.size();
|
||||
// 將 A 頂部 n 個圓盤藉助 B 移到 C
|
||||
dfs(n, A, B, C);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="hanota.java"
|
||||
/* 移動一個圓盤 */
|
||||
void move(List<Integer> src, List<Integer> tar) {
|
||||
// 從 src 頂部拿出一個圓盤
|
||||
Integer pan = src.remove(src.size() - 1);
|
||||
// 將圓盤放入 tar 頂部
|
||||
tar.add(pan);
|
||||
}
|
||||
|
||||
/* 求解河內塔問題 f(i) */
|
||||
void dfs(int i, List<Integer> src, List<Integer> buf, List<Integer> tar) {
|
||||
// 若 src 只剩下一個圓盤,則直接將其移到 tar
|
||||
if (i == 1) {
|
||||
move(src, tar);
|
||||
return;
|
||||
}
|
||||
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
|
||||
dfs(i - 1, src, tar, buf);
|
||||
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
|
||||
move(src, tar);
|
||||
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
|
||||
dfs(i - 1, buf, src, tar);
|
||||
}
|
||||
|
||||
/* 求解河內塔問題 */
|
||||
void solveHanota(List<Integer> A, List<Integer> B, List<Integer> C) {
|
||||
int n = A.size();
|
||||
// 將 A 頂部 n 個圓盤藉助 B 移到 C
|
||||
dfs(n, A, B, C);
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="hanota.cs"
|
||||
/* 移動一個圓盤 */
|
||||
void Move(List<int> src, List<int> tar) {
|
||||
// 從 src 頂部拿出一個圓盤
|
||||
int pan = src[^1];
|
||||
src.RemoveAt(src.Count - 1);
|
||||
// 將圓盤放入 tar 頂部
|
||||
tar.Add(pan);
|
||||
}
|
||||
|
||||
/* 求解河內塔問題 f(i) */
|
||||
void DFS(int i, List<int> src, List<int> buf, List<int> tar) {
|
||||
// 若 src 只剩下一個圓盤,則直接將其移到 tar
|
||||
if (i == 1) {
|
||||
Move(src, tar);
|
||||
return;
|
||||
}
|
||||
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
|
||||
DFS(i - 1, src, tar, buf);
|
||||
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
|
||||
Move(src, tar);
|
||||
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
|
||||
DFS(i - 1, buf, src, tar);
|
||||
}
|
||||
|
||||
/* 求解河內塔問題 */
|
||||
void SolveHanota(List<int> A, List<int> B, List<int> C) {
|
||||
int n = A.Count;
|
||||
// 將 A 頂部 n 個圓盤藉助 B 移到 C
|
||||
DFS(n, A, B, C);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="hanota.go"
|
||||
/* 移動一個圓盤 */
|
||||
func move(src, tar *list.List) {
|
||||
// 從 src 頂部拿出一個圓盤
|
||||
pan := src.Back()
|
||||
// 將圓盤放入 tar 頂部
|
||||
tar.PushBack(pan.Value)
|
||||
// 移除 src 頂部圓盤
|
||||
src.Remove(pan)
|
||||
}
|
||||
|
||||
/* 求解河內塔問題 f(i) */
|
||||
func dfsHanota(i int, src, buf, tar *list.List) {
|
||||
// 若 src 只剩下一個圓盤,則直接將其移到 tar
|
||||
if i == 1 {
|
||||
move(src, tar)
|
||||
return
|
||||
}
|
||||
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
|
||||
dfsHanota(i-1, src, tar, buf)
|
||||
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
|
||||
move(src, tar)
|
||||
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
|
||||
dfsHanota(i-1, buf, src, tar)
|
||||
}
|
||||
|
||||
/* 求解河內塔問題 */
|
||||
func solveHanota(A, B, C *list.List) {
|
||||
n := A.Len()
|
||||
// 將 A 頂部 n 個圓盤藉助 B 移到 C
|
||||
dfsHanota(n, A, B, C)
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="hanota.swift"
|
||||
/* 移動一個圓盤 */
|
||||
func move(src: inout [Int], tar: inout [Int]) {
|
||||
// 從 src 頂部拿出一個圓盤
|
||||
let pan = src.popLast()!
|
||||
// 將圓盤放入 tar 頂部
|
||||
tar.append(pan)
|
||||
}
|
||||
|
||||
/* 求解河內塔問題 f(i) */
|
||||
func dfs(i: Int, src: inout [Int], buf: inout [Int], tar: inout [Int]) {
|
||||
// 若 src 只剩下一個圓盤,則直接將其移到 tar
|
||||
if i == 1 {
|
||||
move(src: &src, tar: &tar)
|
||||
return
|
||||
}
|
||||
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
|
||||
dfs(i: i - 1, src: &src, buf: &tar, tar: &buf)
|
||||
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
|
||||
move(src: &src, tar: &tar)
|
||||
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
|
||||
dfs(i: i - 1, src: &buf, buf: &src, tar: &tar)
|
||||
}
|
||||
|
||||
/* 求解河內塔問題 */
|
||||
func solveHanota(A: inout [Int], B: inout [Int], C: inout [Int]) {
|
||||
let n = A.count
|
||||
// 串列尾部是柱子頂部
|
||||
// 將 src 頂部 n 個圓盤藉助 B 移到 C
|
||||
dfs(i: n, src: &A, buf: &B, tar: &C)
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="hanota.js"
|
||||
/* 移動一個圓盤 */
|
||||
function move(src, tar) {
|
||||
// 從 src 頂部拿出一個圓盤
|
||||
const pan = src.pop();
|
||||
// 將圓盤放入 tar 頂部
|
||||
tar.push(pan);
|
||||
}
|
||||
|
||||
/* 求解河內塔問題 f(i) */
|
||||
function dfs(i, src, buf, tar) {
|
||||
// 若 src 只剩下一個圓盤,則直接將其移到 tar
|
||||
if (i === 1) {
|
||||
move(src, tar);
|
||||
return;
|
||||
}
|
||||
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
|
||||
dfs(i - 1, src, tar, buf);
|
||||
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
|
||||
move(src, tar);
|
||||
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
|
||||
dfs(i - 1, buf, src, tar);
|
||||
}
|
||||
|
||||
/* 求解河內塔問題 */
|
||||
function solveHanota(A, B, C) {
|
||||
const n = A.length;
|
||||
// 將 A 頂部 n 個圓盤藉助 B 移到 C
|
||||
dfs(n, A, B, C);
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="hanota.ts"
|
||||
/* 移動一個圓盤 */
|
||||
function move(src: number[], tar: number[]): void {
|
||||
// 從 src 頂部拿出一個圓盤
|
||||
const pan = src.pop();
|
||||
// 將圓盤放入 tar 頂部
|
||||
tar.push(pan);
|
||||
}
|
||||
|
||||
/* 求解河內塔問題 f(i) */
|
||||
function dfs(i: number, src: number[], buf: number[], tar: number[]): void {
|
||||
// 若 src 只剩下一個圓盤,則直接將其移到 tar
|
||||
if (i === 1) {
|
||||
move(src, tar);
|
||||
return;
|
||||
}
|
||||
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
|
||||
dfs(i - 1, src, tar, buf);
|
||||
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
|
||||
move(src, tar);
|
||||
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
|
||||
dfs(i - 1, buf, src, tar);
|
||||
}
|
||||
|
||||
/* 求解河內塔問題 */
|
||||
function solveHanota(A: number[], B: number[], C: number[]): void {
|
||||
const n = A.length;
|
||||
// 將 A 頂部 n 個圓盤藉助 B 移到 C
|
||||
dfs(n, A, B, C);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="hanota.dart"
|
||||
/* 移動一個圓盤 */
|
||||
void move(List<int> src, List<int> tar) {
|
||||
// 從 src 頂部拿出一個圓盤
|
||||
int pan = src.removeLast();
|
||||
// 將圓盤放入 tar 頂部
|
||||
tar.add(pan);
|
||||
}
|
||||
|
||||
/* 求解河內塔問題 f(i) */
|
||||
void dfs(int i, List<int> src, List<int> buf, List<int> tar) {
|
||||
// 若 src 只剩下一個圓盤,則直接將其移到 tar
|
||||
if (i == 1) {
|
||||
move(src, tar);
|
||||
return;
|
||||
}
|
||||
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
|
||||
dfs(i - 1, src, tar, buf);
|
||||
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
|
||||
move(src, tar);
|
||||
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
|
||||
dfs(i - 1, buf, src, tar);
|
||||
}
|
||||
|
||||
/* 求解河內塔問題 */
|
||||
void solveHanota(List<int> A, List<int> B, List<int> C) {
|
||||
int n = A.length;
|
||||
// 將 A 頂部 n 個圓盤藉助 B 移到 C
|
||||
dfs(n, A, B, C);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="hanota.rs"
|
||||
/* 移動一個圓盤 */
|
||||
fn move_pan(src: &mut Vec<i32>, tar: &mut Vec<i32>) {
|
||||
// 從 src 頂部拿出一個圓盤
|
||||
let pan = src.remove(src.len() - 1);
|
||||
// 將圓盤放入 tar 頂部
|
||||
tar.push(pan);
|
||||
}
|
||||
|
||||
/* 求解河內塔問題 f(i) */
|
||||
fn dfs(i: i32, src: &mut Vec<i32>, buf: &mut Vec<i32>, tar: &mut Vec<i32>) {
|
||||
// 若 src 只剩下一個圓盤,則直接將其移到 tar
|
||||
if i == 1 {
|
||||
move_pan(src, tar);
|
||||
return;
|
||||
}
|
||||
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
|
||||
dfs(i - 1, src, tar, buf);
|
||||
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
|
||||
move_pan(src, tar);
|
||||
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
|
||||
dfs(i - 1, buf, src, tar);
|
||||
}
|
||||
|
||||
/* 求解河內塔問題 */
|
||||
fn solve_hanota(A: &mut Vec<i32>, B: &mut Vec<i32>, C: &mut Vec<i32>) {
|
||||
let n = A.len() as i32;
|
||||
// 將 A 頂部 n 個圓盤藉助 B 移到 C
|
||||
dfs(n, A, B, C);
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="hanota.c"
|
||||
/* 移動一個圓盤 */
|
||||
void move(int *src, int *srcSize, int *tar, int *tarSize) {
|
||||
// 從 src 頂部拿出一個圓盤
|
||||
int pan = src[*srcSize - 1];
|
||||
src[*srcSize - 1] = 0;
|
||||
(*srcSize)--;
|
||||
// 將圓盤放入 tar 頂部
|
||||
tar[*tarSize] = pan;
|
||||
(*tarSize)++;
|
||||
}
|
||||
|
||||
/* 求解河內塔問題 f(i) */
|
||||
void dfs(int i, int *src, int *srcSize, int *buf, int *bufSize, int *tar, int *tarSize) {
|
||||
// 若 src 只剩下一個圓盤,則直接將其移到 tar
|
||||
if (i == 1) {
|
||||
move(src, srcSize, tar, tarSize);
|
||||
return;
|
||||
}
|
||||
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
|
||||
dfs(i - 1, src, srcSize, tar, tarSize, buf, bufSize);
|
||||
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
|
||||
move(src, srcSize, tar, tarSize);
|
||||
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
|
||||
dfs(i - 1, buf, bufSize, src, srcSize, tar, tarSize);
|
||||
}
|
||||
|
||||
/* 求解河內塔問題 */
|
||||
void solveHanota(int *A, int *ASize, int *B, int *BSize, int *C, int *CSize) {
|
||||
// 將 A 頂部 n 個圓盤藉助 B 移到 C
|
||||
dfs(*ASize, A, ASize, B, BSize, C, CSize);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="hanota.kt"
|
||||
/* 移動一個圓盤 */
|
||||
fun move(src: MutableList<Int>, tar: MutableList<Int>) {
|
||||
// 從 src 頂部拿出一個圓盤
|
||||
val pan: Int = src.removeAt(src.size - 1)
|
||||
// 將圓盤放入 tar 頂部
|
||||
tar.add(pan)
|
||||
}
|
||||
|
||||
/* 求解河內塔問題 f(i) */
|
||||
fun dfs(i: Int, src: MutableList<Int>, buf: MutableList<Int>, tar: MutableList<Int>) {
|
||||
// 若 src 只剩下一個圓盤,則直接將其移到 tar
|
||||
if (i == 1) {
|
||||
move(src, tar)
|
||||
return
|
||||
}
|
||||
// 子問題 f(i-1) :將 src 頂部 i-1 個圓盤藉助 tar 移到 buf
|
||||
dfs(i - 1, src, tar, buf)
|
||||
// 子問題 f(1) :將 src 剩餘一個圓盤移到 tar
|
||||
move(src, tar)
|
||||
// 子問題 f(i-1) :將 buf 頂部 i-1 個圓盤藉助 src 移到 tar
|
||||
dfs(i - 1, buf, src, tar)
|
||||
}
|
||||
|
||||
/* 求解河內塔問題 */
|
||||
fun solveHanota(A: MutableList<Int>, B: MutableList<Int>, C: MutableList<Int>) {
|
||||
val n = A.size
|
||||
// 將 A 頂部 n 個圓盤藉助 B 移到 C
|
||||
dfs(n, A, B, C)
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="hanota.rb"
|
||||
[class]{}-[func]{move}
|
||||
|
||||
[class]{}-[func]{dfs}
|
||||
|
||||
[class]{}-[func]{solve_hanota}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="hanota.zig"
|
||||
[class]{}-[func]{move}
|
||||
|
||||
[class]{}-[func]{dfs}
|
||||
|
||||
[class]{}-[func]{solveHanota}
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20move%28src%3A%20list%5Bint%5D,%20tar%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E7%A7%BB%E5%8A%A8%E4%B8%80%E4%B8%AA%E5%9C%86%E7%9B%98%22%22%22%0A%20%20%20%20%23%20%E4%BB%8E%20src%20%E9%A1%B6%E9%83%A8%E6%8B%BF%E5%87%BA%E4%B8%80%E4%B8%AA%E5%9C%86%E7%9B%98%0A%20%20%20%20pan%20%3D%20src.pop%28%29%0A%20%20%20%20%23%20%E5%B0%86%E5%9C%86%E7%9B%98%E6%94%BE%E5%85%A5%20tar%20%E9%A1%B6%E9%83%A8%0A%20%20%20%20tar.append%28pan%29%0A%0A%0Adef%20dfs%28i%3A%20int,%20src%3A%20list%5Bint%5D,%20buf%3A%20list%5Bint%5D,%20tar%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E6%B1%82%E8%A7%A3%E6%B1%89%E8%AF%BA%E5%A1%94%E9%97%AE%E9%A2%98%20f%28i%29%22%22%22%0A%20%20%20%20%23%20%E8%8B%A5%20src%20%E5%8F%AA%E5%89%A9%E4%B8%8B%E4%B8%80%E4%B8%AA%E5%9C%86%E7%9B%98%EF%BC%8C%E5%88%99%E7%9B%B4%E6%8E%A5%E5%B0%86%E5%85%B6%E7%A7%BB%E5%88%B0%20tar%0A%20%20%20%20if%20i%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20move%28src,%20tar%29%0A%20%20%20%20%20%20%20%20return%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%20f%28i-1%29%20%EF%BC%9A%E5%B0%86%20src%20%E9%A1%B6%E9%83%A8%20i-1%20%E4%B8%AA%E5%9C%86%E7%9B%98%E5%80%9F%E5%8A%A9%20tar%20%E7%A7%BB%E5%88%B0%20buf%0A%20%20%20%20dfs%28i%20-%201,%20src,%20tar,%20buf%29%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%20f%281%29%20%EF%BC%9A%E5%B0%86%20src%20%E5%89%A9%E4%BD%99%E4%B8%80%E4%B8%AA%E5%9C%86%E7%9B%98%E7%A7%BB%E5%88%B0%20tar%0A%20%20%20%20move%28src,%20tar%29%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%20f%28i-1%29%20%EF%BC%9A%E5%B0%86%20buf%20%E9%A1%B6%E9%83%A8%20i-1%20%E4%B8%AA%E5%9C%86%E7%9B%98%E5%80%9F%E5%8A%A9%20src%20%E7%A7%BB%E5%88%B0%20tar%0A%20%20%20%20dfs%28i%20-%201,%20buf,%20src,%20tar%29%0A%0A%0Adef%20solve_hanota%28A%3A%20list%5Bint%5D,%20B%3A%20list%5Bint%5D,%20C%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E6%B1%82%E8%A7%A3%E6%B1%89%E8%AF%BA%E5%A1%94%E9%97%AE%E9%A2%98%22%22%22%0A%20%20%20%20n%20%3D%20len%28A%29%0A%20%20%20%20%23%20%E5%B0%86%20A%20%E9%A1%B6%E9%83%A8%20n%20%E4%B8%AA%E5%9C%86%E7%9B%98%E5%80%9F%E5%8A%A9%20B%20%E7%A7%BB%E5%88%B0%20C%0A%20%20%20%20dfs%28n,%20A,%20B,%20C%29%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%97%E8%A1%A8%E5%B0%BE%E9%83%A8%E6%98%AF%E6%9F%B1%E5%AD%90%E9%A1%B6%E9%83%A8%0A%20%20%20%20A%20%3D%20%5B5,%204,%203,%202,%201%5D%0A%20%20%20%20B%20%3D%20%5B%5D%0A%20%20%20%20C%20%3D%20%5B%5D%0A%20%20%20%20print%28%22%E5%88%9D%E5%A7%8B%E7%8A%B6%E6%80%81%E4%B8%8B%EF%BC%9A%22%29%0A%20%20%20%20print%28f%22A%20%3D%20%7BA%7D%22%29%0A%20%20%20%20print%28f%22B%20%3D%20%7BB%7D%22%29%0A%20%20%20%20print%28f%22C%20%3D%20%7BC%7D%22%29%0A%0A%20%20%20%20solve_hanota%28A,%20B,%20C%29%0A%0A%20%20%20%20print%28%22%E5%9C%86%E7%9B%98%E7%A7%BB%E5%8A%A8%E5%AE%8C%E6%88%90%E5%90%8E%EF%BC%9A%22%29%0A%20%20%20%20print%28f%22A%20%3D%20%7BA%7D%22%29%0A%20%20%20%20print%28f%22B%20%3D%20%7BB%7D%22%29%0A%20%20%20%20print%28f%22C%20%3D%20%7BC%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=12&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20move%28src%3A%20list%5Bint%5D,%20tar%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E7%A7%BB%E5%8A%A8%E4%B8%80%E4%B8%AA%E5%9C%86%E7%9B%98%22%22%22%0A%20%20%20%20%23%20%E4%BB%8E%20src%20%E9%A1%B6%E9%83%A8%E6%8B%BF%E5%87%BA%E4%B8%80%E4%B8%AA%E5%9C%86%E7%9B%98%0A%20%20%20%20pan%20%3D%20src.pop%28%29%0A%20%20%20%20%23%20%E5%B0%86%E5%9C%86%E7%9B%98%E6%94%BE%E5%85%A5%20tar%20%E9%A1%B6%E9%83%A8%0A%20%20%20%20tar.append%28pan%29%0A%0A%0Adef%20dfs%28i%3A%20int,%20src%3A%20list%5Bint%5D,%20buf%3A%20list%5Bint%5D,%20tar%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E6%B1%82%E8%A7%A3%E6%B1%89%E8%AF%BA%E5%A1%94%E9%97%AE%E9%A2%98%20f%28i%29%22%22%22%0A%20%20%20%20%23%20%E8%8B%A5%20src%20%E5%8F%AA%E5%89%A9%E4%B8%8B%E4%B8%80%E4%B8%AA%E5%9C%86%E7%9B%98%EF%BC%8C%E5%88%99%E7%9B%B4%E6%8E%A5%E5%B0%86%E5%85%B6%E7%A7%BB%E5%88%B0%20tar%0A%20%20%20%20if%20i%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20move%28src,%20tar%29%0A%20%20%20%20%20%20%20%20return%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%20f%28i-1%29%20%EF%BC%9A%E5%B0%86%20src%20%E9%A1%B6%E9%83%A8%20i-1%20%E4%B8%AA%E5%9C%86%E7%9B%98%E5%80%9F%E5%8A%A9%20tar%20%E7%A7%BB%E5%88%B0%20buf%0A%20%20%20%20dfs%28i%20-%201,%20src,%20tar,%20buf%29%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%20f%281%29%20%EF%BC%9A%E5%B0%86%20src%20%E5%89%A9%E4%BD%99%E4%B8%80%E4%B8%AA%E5%9C%86%E7%9B%98%E7%A7%BB%E5%88%B0%20tar%0A%20%20%20%20move%28src,%20tar%29%0A%20%20%20%20%23%20%E5%AD%90%E9%97%AE%E9%A2%98%20f%28i-1%29%20%EF%BC%9A%E5%B0%86%20buf%20%E9%A1%B6%E9%83%A8%20i-1%20%E4%B8%AA%E5%9C%86%E7%9B%98%E5%80%9F%E5%8A%A9%20src%20%E7%A7%BB%E5%88%B0%20tar%0A%20%20%20%20dfs%28i%20-%201,%20buf,%20src,%20tar%29%0A%0A%0Adef%20solve_hanota%28A%3A%20list%5Bint%5D,%20B%3A%20list%5Bint%5D,%20C%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%E6%B1%82%E8%A7%A3%E6%B1%89%E8%AF%BA%E5%A1%94%E9%97%AE%E9%A2%98%22%22%22%0A%20%20%20%20n%20%3D%20len%28A%29%0A%20%20%20%20%23%20%E5%B0%86%20A%20%E9%A1%B6%E9%83%A8%20n%20%E4%B8%AA%E5%9C%86%E7%9B%98%E5%80%9F%E5%8A%A9%20B%20%E7%A7%BB%E5%88%B0%20C%0A%20%20%20%20dfs%28n,%20A,%20B,%20C%29%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%97%E8%A1%A8%E5%B0%BE%E9%83%A8%E6%98%AF%E6%9F%B1%E5%AD%90%E9%A1%B6%E9%83%A8%0A%20%20%20%20A%20%3D%20%5B5,%204,%203,%202,%201%5D%0A%20%20%20%20B%20%3D%20%5B%5D%0A%20%20%20%20C%20%3D%20%5B%5D%0A%20%20%20%20print%28%22%E5%88%9D%E5%A7%8B%E7%8A%B6%E6%80%81%E4%B8%8B%EF%BC%9A%22%29%0A%20%20%20%20print%28f%22A%20%3D%20%7BA%7D%22%29%0A%20%20%20%20print%28f%22B%20%3D%20%7BB%7D%22%29%0A%20%20%20%20print%28f%22C%20%3D%20%7BC%7D%22%29%0A%0A%20%20%20%20solve_hanota%28A,%20B,%20C%29%0A%0A%20%20%20%20print%28%22%E5%9C%86%E7%9B%98%E7%A7%BB%E5%8A%A8%E5%AE%8C%E6%88%90%E5%90%8E%EF%BC%9A%22%29%0A%20%20%20%20print%28f%22A%20%3D%20%7BA%7D%22%29%0A%20%20%20%20print%28f%22B%20%3D%20%7BB%7D%22%29%0A%20%20%20%20print%28f%22C%20%3D%20%7BC%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=12&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
如圖 12-15 所示,河內塔問題形成一棵高度為 $n$ 的遞迴樹,每個節點代表一個子問題,對應一個開啟的 `dfs()` 函式,**因此時間複雜度為 $O(2^n)$ ,空間複雜度為 $O(n)$** 。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 12-15 河內塔問題的遞迴樹 </p>
|
||||
|
||||
!!! quote
|
||||
|
||||
河內塔問題源自一個古老的傳說。在古印度的一個寺廟裡,僧侶們有三根高大的鑽石柱子,以及 $64$ 個大小不一的金圓盤。僧侶們不斷地移動圓盤,他們相信在最後一個圓盤被正確放置的那一刻,這個世界就會結束。
|
||||
|
||||
然而,即使僧侶們每秒鐘移動一次,總共需要大約 $2^{64} \approx 1.84×10^{19}$ 秒,合約 $5850$ 億年,遠遠超過了現在對宇宙年齡的估計。所以,倘若這個傳說是真的,我們應該不需要擔心世界末日的到來。
|
||||
22
zh-Hant/docs/chapter_divide_and_conquer/index.md
Normal file
22
zh-Hant/docs/chapter_divide_and_conquer/index.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/set-split
|
||||
---
|
||||
|
||||
# 第 12 章 分治
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
難題被逐層拆解,每一次的拆解都使它變得更為簡單。
|
||||
|
||||
分而治之揭示了一個重要的事實:從簡單做起,一切都不再複雜。
|
||||
|
||||
## Chapter Contents
|
||||
|
||||
- [12.1 分治演算法](https://www.hello-algo.com/en/chapter_divide_and_conquer/divide_and_conquer/)
|
||||
- [12.2 分治搜尋策略](https://www.hello-algo.com/en/chapter_divide_and_conquer/binary_search_recur/)
|
||||
- [12.3 構建樹問題](https://www.hello-algo.com/en/chapter_divide_and_conquer/build_binary_tree_problem/)
|
||||
- [12.4 河內塔問題](https://www.hello-algo.com/en/chapter_divide_and_conquer/hanota_problem/)
|
||||
- [12.5 小結](https://www.hello-algo.com/en/chapter_divide_and_conquer/summary/)
|
||||
15
zh-Hant/docs/chapter_divide_and_conquer/summary.md
Normal file
15
zh-Hant/docs/chapter_divide_and_conquer/summary.md
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 12.5 小結
|
||||
|
||||
- 分治是一種常見的演算法設計策略,包括分(劃分)和治(合併)兩個階段,通常基於遞迴實現。
|
||||
- 判斷是否是分治演算法問題的依據包括:問題能否分解、子問題是否獨立、子問題能否合併。
|
||||
- 合併排序是分治策略的典型應用,其遞迴地將陣列劃分為等長的兩個子陣列,直到只剩一個元素時開始逐層合併,從而完成排序。
|
||||
- 引入分治策略往往可以提升演算法效率。一方面,分治策略減少了操作數量;另一方面,分治後有利於系統的並行最佳化。
|
||||
- 分治既可以解決許多演算法問題,也廣泛應用於資料結構與演算法設計中,處處可見其身影。
|
||||
- 相較於暴力搜尋,自適應搜尋效率更高。時間複雜度為 $O(\log n)$ 的搜尋演算法通常是基於分治策略實現的。
|
||||
- 二分搜尋是分治策略的另一個典型應用,它不包含將子問題的解進行合併的步驟。我們可以透過遞迴分治實現二分搜尋。
|
||||
- 在構建二元樹的問題中,構建樹(原問題)可以劃分為構建左子樹和右子樹(子問題),這可以透過劃分前序走訪和中序走訪的索引區間來實現。
|
||||
- 在河內塔問題中,一個規模為 $n$ 的問題可以劃分為兩個規模為 $n-1$ 的子問題和一個規模為 $1$ 的子問題。按順序解決這三個子問題後,原問題隨之得到解決。
|
||||
978
zh-Hant/docs/chapter_dynamic_programming/dp_problem_features.md
Normal file
978
zh-Hant/docs/chapter_dynamic_programming/dp_problem_features.md
Normal file
@@ -0,0 +1,978 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 14.2 動態規劃問題特性
|
||||
|
||||
在上一節中,我們學習了動態規劃是如何透過子問題分解來求解原問題的。實際上,子問題分解是一種通用的演算法思路,在分治、動態規劃、回溯中的側重點不同。
|
||||
|
||||
- 分治演算法遞迴地將原問題劃分為多個相互獨立的子問題,直至最小子問題,並在回溯中合併子問題的解,最終得到原問題的解。
|
||||
- 動態規劃也對問題進行遞迴分解,但與分治演算法的主要區別是,動態規劃中的子問題是相互依賴的,在分解過程中會出現許多重疊子問題。
|
||||
- 回溯演算法在嘗試和回退中窮舉所有可能的解,並透過剪枝避免不必要的搜尋分支。原問題的解由一系列決策步驟構成,我們可以將每個決策步驟之前的子序列看作一個子問題。
|
||||
|
||||
實際上,動態規劃常用來求解最最佳化問題,它們不僅包含重疊子問題,還具有另外兩大特性:最優子結構、無後效性。
|
||||
|
||||
## 14.2.1 最優子結構
|
||||
|
||||
我們對爬樓梯問題稍作改動,使之更加適合展示最優子結構概念。
|
||||
|
||||
!!! question "爬樓梯最小代價"
|
||||
|
||||
給定一個樓梯,你每步可以上 $1$ 階或者 $2$ 階,每一階樓梯上都貼有一個非負整數,表示你在該臺階所需要付出的代價。給定一個非負整數陣列 $cost$ ,其中 $cost[i]$ 表示在第 $i$ 個臺階需要付出的代價,$cost[0]$ 為地面(起始點)。請計算最少需要付出多少代價才能到達頂部?
|
||||
|
||||
如圖 14-6 所示,若第 $1$、$2$、$3$ 階的代價分別為 $1$、$10$、$1$ ,則從地面爬到第 $3$ 階的最小代價為 $2$ 。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 14-6 爬到第 3 階的最小代價 </p>
|
||||
|
||||
設 $dp[i]$ 為爬到第 $i$ 階累計付出的代價,由於第 $i$ 階只可能從 $i - 1$ 階或 $i - 2$ 階走來,因此 $dp[i]$ 只可能等於 $dp[i - 1] + cost[i]$ 或 $dp[i - 2] + cost[i]$ 。為了儘可能減少代價,我們應該選擇兩者中較小的那一個:
|
||||
|
||||
$$
|
||||
dp[i] = \min(dp[i-1], dp[i-2]) + cost[i]
|
||||
$$
|
||||
|
||||
這便可以引出最優子結構的含義:**原問題的最優解是從子問題的最優解構建得來的**。
|
||||
|
||||
本題顯然具有最優子結構:我們從兩個子問題最優解 $dp[i-1]$ 和 $dp[i-2]$ 中挑選出較優的那一個,並用它構建出原問題 $dp[i]$ 的最優解。
|
||||
|
||||
那麼,上一節的爬樓梯題目有沒有最優子結構呢?它的目標是求解方案數量,看似是一個計數問題,但如果換一種問法:“求解最大方案數量”。我們意外地發現,**雖然題目修改前後是等價的,但最優子結構浮現出來了**:第 $n$ 階最大方案數量等於第 $n-1$ 階和第 $n-2$ 階最大方案數量之和。所以說,最優子結構的解釋方式比較靈活,在不同問題中會有不同的含義。
|
||||
|
||||
根據狀態轉移方程,以及初始狀態 $dp[1] = cost[1]$ 和 $dp[2] = cost[2]$ ,我們就可以得到動態規劃程式碼:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="min_cost_climbing_stairs_dp.py"
|
||||
def min_cost_climbing_stairs_dp(cost: list[int]) -> int:
|
||||
"""爬樓梯最小代價:動態規劃"""
|
||||
n = len(cost) - 1
|
||||
if n == 1 or n == 2:
|
||||
return cost[n]
|
||||
# 初始化 dp 表,用於儲存子問題的解
|
||||
dp = [0] * (n + 1)
|
||||
# 初始狀態:預設最小子問題的解
|
||||
dp[1], dp[2] = cost[1], cost[2]
|
||||
# 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for i in range(3, n + 1):
|
||||
dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]
|
||||
return dp[n]
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="min_cost_climbing_stairs_dp.cpp"
|
||||
/* 爬樓梯最小代價:動態規劃 */
|
||||
int minCostClimbingStairsDP(vector<int> &cost) {
|
||||
int n = cost.size() - 1;
|
||||
if (n == 1 || n == 2)
|
||||
return cost[n];
|
||||
// 初始化 dp 表,用於儲存子問題的解
|
||||
vector<int> dp(n + 1);
|
||||
// 初始狀態:預設最小子問題的解
|
||||
dp[1] = cost[1];
|
||||
dp[2] = cost[2];
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for (int i = 3; i <= n; i++) {
|
||||
dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];
|
||||
}
|
||||
return dp[n];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="min_cost_climbing_stairs_dp.java"
|
||||
/* 爬樓梯最小代價:動態規劃 */
|
||||
int minCostClimbingStairsDP(int[] cost) {
|
||||
int n = cost.length - 1;
|
||||
if (n == 1 || n == 2)
|
||||
return cost[n];
|
||||
// 初始化 dp 表,用於儲存子問題的解
|
||||
int[] dp = new int[n + 1];
|
||||
// 初始狀態:預設最小子問題的解
|
||||
dp[1] = cost[1];
|
||||
dp[2] = cost[2];
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for (int i = 3; i <= n; i++) {
|
||||
dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];
|
||||
}
|
||||
return dp[n];
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="min_cost_climbing_stairs_dp.cs"
|
||||
/* 爬樓梯最小代價:動態規劃 */
|
||||
int MinCostClimbingStairsDP(int[] cost) {
|
||||
int n = cost.Length - 1;
|
||||
if (n == 1 || n == 2)
|
||||
return cost[n];
|
||||
// 初始化 dp 表,用於儲存子問題的解
|
||||
int[] dp = new int[n + 1];
|
||||
// 初始狀態:預設最小子問題的解
|
||||
dp[1] = cost[1];
|
||||
dp[2] = cost[2];
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for (int i = 3; i <= n; i++) {
|
||||
dp[i] = Math.Min(dp[i - 1], dp[i - 2]) + cost[i];
|
||||
}
|
||||
return dp[n];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="min_cost_climbing_stairs_dp.go"
|
||||
/* 爬樓梯最小代價:動態規劃 */
|
||||
func minCostClimbingStairsDP(cost []int) int {
|
||||
n := len(cost) - 1
|
||||
if n == 1 || n == 2 {
|
||||
return cost[n]
|
||||
}
|
||||
min := func(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
// 初始化 dp 表,用於儲存子問題的解
|
||||
dp := make([]int, n+1)
|
||||
// 初始狀態:預設最小子問題的解
|
||||
dp[1] = cost[1]
|
||||
dp[2] = cost[2]
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for i := 3; i <= n; i++ {
|
||||
dp[i] = min(dp[i-1], dp[i-2]) + cost[i]
|
||||
}
|
||||
return dp[n]
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="min_cost_climbing_stairs_dp.swift"
|
||||
/* 爬樓梯最小代價:動態規劃 */
|
||||
func minCostClimbingStairsDP(cost: [Int]) -> Int {
|
||||
let n = cost.count - 1
|
||||
if n == 1 || n == 2 {
|
||||
return cost[n]
|
||||
}
|
||||
// 初始化 dp 表,用於儲存子問題的解
|
||||
var dp = Array(repeating: 0, count: n + 1)
|
||||
// 初始狀態:預設最小子問題的解
|
||||
dp[1] = cost[1]
|
||||
dp[2] = cost[2]
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for i in 3 ... n {
|
||||
dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]
|
||||
}
|
||||
return dp[n]
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="min_cost_climbing_stairs_dp.js"
|
||||
/* 爬樓梯最小代價:動態規劃 */
|
||||
function minCostClimbingStairsDP(cost) {
|
||||
const n = cost.length - 1;
|
||||
if (n === 1 || n === 2) {
|
||||
return cost[n];
|
||||
}
|
||||
// 初始化 dp 表,用於儲存子問題的解
|
||||
const dp = new Array(n + 1);
|
||||
// 初始狀態:預設最小子問題的解
|
||||
dp[1] = cost[1];
|
||||
dp[2] = cost[2];
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for (let i = 3; i <= n; i++) {
|
||||
dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];
|
||||
}
|
||||
return dp[n];
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="min_cost_climbing_stairs_dp.ts"
|
||||
/* 爬樓梯最小代價:動態規劃 */
|
||||
function minCostClimbingStairsDP(cost: Array<number>): number {
|
||||
const n = cost.length - 1;
|
||||
if (n === 1 || n === 2) {
|
||||
return cost[n];
|
||||
}
|
||||
// 初始化 dp 表,用於儲存子問題的解
|
||||
const dp = new Array(n + 1);
|
||||
// 初始狀態:預設最小子問題的解
|
||||
dp[1] = cost[1];
|
||||
dp[2] = cost[2];
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for (let i = 3; i <= n; i++) {
|
||||
dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];
|
||||
}
|
||||
return dp[n];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="min_cost_climbing_stairs_dp.dart"
|
||||
/* 爬樓梯最小代價:動態規劃 */
|
||||
int minCostClimbingStairsDP(List<int> cost) {
|
||||
int n = cost.length - 1;
|
||||
if (n == 1 || n == 2) return cost[n];
|
||||
// 初始化 dp 表,用於儲存子問題的解
|
||||
List<int> dp = List.filled(n + 1, 0);
|
||||
// 初始狀態:預設最小子問題的解
|
||||
dp[1] = cost[1];
|
||||
dp[2] = cost[2];
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for (int i = 3; i <= n; i++) {
|
||||
dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];
|
||||
}
|
||||
return dp[n];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="min_cost_climbing_stairs_dp.rs"
|
||||
/* 爬樓梯最小代價:動態規劃 */
|
||||
fn min_cost_climbing_stairs_dp(cost: &[i32]) -> i32 {
|
||||
let n = cost.len() - 1;
|
||||
if n == 1 || n == 2 {
|
||||
return cost[n];
|
||||
}
|
||||
// 初始化 dp 表,用於儲存子問題的解
|
||||
let mut dp = vec![-1; n + 1];
|
||||
// 初始狀態:預設最小子問題的解
|
||||
dp[1] = cost[1];
|
||||
dp[2] = cost[2];
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for i in 3..=n {
|
||||
dp[i] = cmp::min(dp[i - 1], dp[i - 2]) + cost[i];
|
||||
}
|
||||
dp[n]
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="min_cost_climbing_stairs_dp.c"
|
||||
/* 爬樓梯最小代價:動態規劃 */
|
||||
int minCostClimbingStairsDP(int cost[], int costSize) {
|
||||
int n = costSize - 1;
|
||||
if (n == 1 || n == 2)
|
||||
return cost[n];
|
||||
// 初始化 dp 表,用於儲存子問題的解
|
||||
int *dp = calloc(n + 1, sizeof(int));
|
||||
// 初始狀態:預設最小子問題的解
|
||||
dp[1] = cost[1];
|
||||
dp[2] = cost[2];
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for (int i = 3; i <= n; i++) {
|
||||
dp[i] = myMin(dp[i - 1], dp[i - 2]) + cost[i];
|
||||
}
|
||||
int res = dp[n];
|
||||
// 釋放記憶體
|
||||
free(dp);
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="min_cost_climbing_stairs_dp.kt"
|
||||
/* 爬樓梯最小代價:動態規劃 */
|
||||
fun minCostClimbingStairsDP(cost: IntArray): Int {
|
||||
val n = cost.size - 1
|
||||
if (n == 1 || n == 2) return cost[n]
|
||||
// 初始化 dp 表,用於儲存子問題的解
|
||||
val dp = IntArray(n + 1)
|
||||
// 初始狀態:預設最小子問題的解
|
||||
dp[1] = cost[1]
|
||||
dp[2] = cost[2]
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for (i in 3..n) {
|
||||
dp[i] = (min(dp[i - 1].toDouble(), dp[i - 2].toDouble()) + cost[i]).toInt()
|
||||
}
|
||||
return dp[n]
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="min_cost_climbing_stairs_dp.rb"
|
||||
[class]{}-[func]{min_cost_climbing_stairs_dp}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="min_cost_climbing_stairs_dp.zig"
|
||||
// 爬樓梯最小代價:動態規劃
|
||||
fn minCostClimbingStairsDP(comptime cost: []i32) i32 {
|
||||
comptime var n = cost.len - 1;
|
||||
if (n == 1 or n == 2) {
|
||||
return cost[n];
|
||||
}
|
||||
// 初始化 dp 表,用於儲存子問題的解
|
||||
var dp = [_]i32{-1} ** (n + 1);
|
||||
// 初始狀態:預設最小子問題的解
|
||||
dp[1] = cost[1];
|
||||
dp[2] = cost[2];
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for (3..n + 1) |i| {
|
||||
dp[i] = @min(dp[i - 1], dp[i - 2]) + cost[i];
|
||||
}
|
||||
return dp[n];
|
||||
}
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20min_cost_climbing_stairs_dp%28cost%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E7%88%AC%E6%A5%BC%E6%A2%AF%E6%9C%80%E5%B0%8F%E4%BB%A3%E4%BB%B7%EF%BC%9A%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%22%22%22%0A%20%20%20%20n%20%3D%20len%28cost%29%20-%201%0A%20%20%20%20if%20n%20%3D%3D%201%20or%20n%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20return%20cost%5Bn%5D%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%20dp%20%E8%A1%A8%EF%BC%8C%E7%94%A8%E4%BA%8E%E5%AD%98%E5%82%A8%E5%AD%90%E9%97%AE%E9%A2%98%E7%9A%84%E8%A7%A3%0A%20%20%20%20dp%20%3D%20%5B0%5D%20*%20%28n%20%2B%201%29%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E7%8A%B6%E6%80%81%EF%BC%9A%E9%A2%84%E8%AE%BE%E6%9C%80%E5%B0%8F%E5%AD%90%E9%97%AE%E9%A2%98%E7%9A%84%E8%A7%A3%0A%20%20%20%20dp%5B1%5D,%20dp%5B2%5D%20%3D%20cost%5B1%5D,%20cost%5B2%5D%0A%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E4%BB%8E%E8%BE%83%E5%B0%8F%E5%AD%90%E9%97%AE%E9%A2%98%E9%80%90%E6%AD%A5%E6%B1%82%E8%A7%A3%E8%BE%83%E5%A4%A7%E5%AD%90%E9%97%AE%E9%A2%98%0A%20%20%20%20for%20i%20in%20range%283,%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20dp%5Bi%5D%20%3D%20min%28dp%5Bi%20-%201%5D,%20dp%5Bi%20-%202%5D%29%20%2B%20cost%5Bi%5D%0A%20%20%20%20return%20dp%5Bn%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20cost%20%3D%20%5B0,%201,%2010,%201,%201,%201,%2010,%201,%201,%2010,%201%5D%0A%20%20%20%20print%28f%22%E8%BE%93%E5%85%A5%E6%A5%BC%E6%A2%AF%E7%9A%84%E4%BB%A3%E4%BB%B7%E5%88%97%E8%A1%A8%E4%B8%BA%20%7Bcost%7D%22%29%0A%0A%20%20%20%20res%20%3D%20min_cost_climbing_stairs_dp%28cost%29%0A%20%20%20%20print%28f%22%E7%88%AC%E5%AE%8C%E6%A5%BC%E6%A2%AF%E7%9A%84%E6%9C%80%E4%BD%8E%E4%BB%A3%E4%BB%B7%E4%B8%BA%20%7Bres%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20min_cost_climbing_stairs_dp%28cost%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E7%88%AC%E6%A5%BC%E6%A2%AF%E6%9C%80%E5%B0%8F%E4%BB%A3%E4%BB%B7%EF%BC%9A%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%22%22%22%0A%20%20%20%20n%20%3D%20len%28cost%29%20-%201%0A%20%20%20%20if%20n%20%3D%3D%201%20or%20n%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20return%20cost%5Bn%5D%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%20dp%20%E8%A1%A8%EF%BC%8C%E7%94%A8%E4%BA%8E%E5%AD%98%E5%82%A8%E5%AD%90%E9%97%AE%E9%A2%98%E7%9A%84%E8%A7%A3%0A%20%20%20%20dp%20%3D%20%5B0%5D%20*%20%28n%20%2B%201%29%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E7%8A%B6%E6%80%81%EF%BC%9A%E9%A2%84%E8%AE%BE%E6%9C%80%E5%B0%8F%E5%AD%90%E9%97%AE%E9%A2%98%E7%9A%84%E8%A7%A3%0A%20%20%20%20dp%5B1%5D,%20dp%5B2%5D%20%3D%20cost%5B1%5D,%20cost%5B2%5D%0A%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E4%BB%8E%E8%BE%83%E5%B0%8F%E5%AD%90%E9%97%AE%E9%A2%98%E9%80%90%E6%AD%A5%E6%B1%82%E8%A7%A3%E8%BE%83%E5%A4%A7%E5%AD%90%E9%97%AE%E9%A2%98%0A%20%20%20%20for%20i%20in%20range%283,%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20dp%5Bi%5D%20%3D%20min%28dp%5Bi%20-%201%5D,%20dp%5Bi%20-%202%5D%29%20%2B%20cost%5Bi%5D%0A%20%20%20%20return%20dp%5Bn%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20cost%20%3D%20%5B0,%201,%2010,%201,%201,%201,%2010,%201,%201,%2010,%201%5D%0A%20%20%20%20print%28f%22%E8%BE%93%E5%85%A5%E6%A5%BC%E6%A2%AF%E7%9A%84%E4%BB%A3%E4%BB%B7%E5%88%97%E8%A1%A8%E4%B8%BA%20%7Bcost%7D%22%29%0A%0A%20%20%20%20res%20%3D%20min_cost_climbing_stairs_dp%28cost%29%0A%20%20%20%20print%28f%22%E7%88%AC%E5%AE%8C%E6%A5%BC%E6%A2%AF%E7%9A%84%E6%9C%80%E4%BD%8E%E4%BB%A3%E4%BB%B7%E4%B8%BA%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
圖 14-7 展示了以上程式碼的動態規劃過程。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 14-7 爬樓梯最小代價的動態規劃過程 </p>
|
||||
|
||||
本題也可以進行空間最佳化,將一維壓縮至零維,使得空間複雜度從 $O(n)$ 降至 $O(1)$ :
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="min_cost_climbing_stairs_dp.py"
|
||||
def min_cost_climbing_stairs_dp_comp(cost: list[int]) -> int:
|
||||
"""爬樓梯最小代價:空間最佳化後的動態規劃"""
|
||||
n = len(cost) - 1
|
||||
if n == 1 or n == 2:
|
||||
return cost[n]
|
||||
a, b = cost[1], cost[2]
|
||||
for i in range(3, n + 1):
|
||||
a, b = b, min(a, b) + cost[i]
|
||||
return b
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="min_cost_climbing_stairs_dp.cpp"
|
||||
/* 爬樓梯最小代價:空間最佳化後的動態規劃 */
|
||||
int minCostClimbingStairsDPComp(vector<int> &cost) {
|
||||
int n = cost.size() - 1;
|
||||
if (n == 1 || n == 2)
|
||||
return cost[n];
|
||||
int a = cost[1], b = cost[2];
|
||||
for (int i = 3; i <= n; i++) {
|
||||
int tmp = b;
|
||||
b = min(a, tmp) + cost[i];
|
||||
a = tmp;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="min_cost_climbing_stairs_dp.java"
|
||||
/* 爬樓梯最小代價:空間最佳化後的動態規劃 */
|
||||
int minCostClimbingStairsDPComp(int[] cost) {
|
||||
int n = cost.length - 1;
|
||||
if (n == 1 || n == 2)
|
||||
return cost[n];
|
||||
int a = cost[1], b = cost[2];
|
||||
for (int i = 3; i <= n; i++) {
|
||||
int tmp = b;
|
||||
b = Math.min(a, tmp) + cost[i];
|
||||
a = tmp;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="min_cost_climbing_stairs_dp.cs"
|
||||
/* 爬樓梯最小代價:空間最佳化後的動態規劃 */
|
||||
int MinCostClimbingStairsDPComp(int[] cost) {
|
||||
int n = cost.Length - 1;
|
||||
if (n == 1 || n == 2)
|
||||
return cost[n];
|
||||
int a = cost[1], b = cost[2];
|
||||
for (int i = 3; i <= n; i++) {
|
||||
int tmp = b;
|
||||
b = Math.Min(a, tmp) + cost[i];
|
||||
a = tmp;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="min_cost_climbing_stairs_dp.go"
|
||||
/* 爬樓梯最小代價:空間最佳化後的動態規劃 */
|
||||
func minCostClimbingStairsDPComp(cost []int) int {
|
||||
n := len(cost) - 1
|
||||
if n == 1 || n == 2 {
|
||||
return cost[n]
|
||||
}
|
||||
min := func(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
// 初始狀態:預設最小子問題的解
|
||||
a, b := cost[1], cost[2]
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for i := 3; i <= n; i++ {
|
||||
tmp := b
|
||||
b = min(a, tmp) + cost[i]
|
||||
a = tmp
|
||||
}
|
||||
return b
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="min_cost_climbing_stairs_dp.swift"
|
||||
/* 爬樓梯最小代價:空間最佳化後的動態規劃 */
|
||||
func minCostClimbingStairsDPComp(cost: [Int]) -> Int {
|
||||
let n = cost.count - 1
|
||||
if n == 1 || n == 2 {
|
||||
return cost[n]
|
||||
}
|
||||
var (a, b) = (cost[1], cost[2])
|
||||
for i in 3 ... n {
|
||||
(a, b) = (b, min(a, b) + cost[i])
|
||||
}
|
||||
return b
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="min_cost_climbing_stairs_dp.js"
|
||||
/* 爬樓梯最小代價:狀態壓縮後的動態規劃 */
|
||||
function minCostClimbingStairsDPComp(cost) {
|
||||
const n = cost.length - 1;
|
||||
if (n === 1 || n === 2) {
|
||||
return cost[n];
|
||||
}
|
||||
let a = cost[1],
|
||||
b = cost[2];
|
||||
for (let i = 3; i <= n; i++) {
|
||||
const tmp = b;
|
||||
b = Math.min(a, tmp) + cost[i];
|
||||
a = tmp;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="min_cost_climbing_stairs_dp.ts"
|
||||
/* 爬樓梯最小代價:狀態壓縮後的動態規劃 */
|
||||
function minCostClimbingStairsDPComp(cost: Array<number>): number {
|
||||
const n = cost.length - 1;
|
||||
if (n === 1 || n === 2) {
|
||||
return cost[n];
|
||||
}
|
||||
let a = cost[1],
|
||||
b = cost[2];
|
||||
for (let i = 3; i <= n; i++) {
|
||||
const tmp = b;
|
||||
b = Math.min(a, tmp) + cost[i];
|
||||
a = tmp;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="min_cost_climbing_stairs_dp.dart"
|
||||
/* 爬樓梯最小代價:空間最佳化後的動態規劃 */
|
||||
int minCostClimbingStairsDPComp(List<int> cost) {
|
||||
int n = cost.length - 1;
|
||||
if (n == 1 || n == 2) return cost[n];
|
||||
int a = cost[1], b = cost[2];
|
||||
for (int i = 3; i <= n; i++) {
|
||||
int tmp = b;
|
||||
b = min(a, tmp) + cost[i];
|
||||
a = tmp;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="min_cost_climbing_stairs_dp.rs"
|
||||
/* 爬樓梯最小代價:空間最佳化後的動態規劃 */
|
||||
fn min_cost_climbing_stairs_dp_comp(cost: &[i32]) -> i32 {
|
||||
let n = cost.len() - 1;
|
||||
if n == 1 || n == 2 {
|
||||
return cost[n];
|
||||
};
|
||||
let (mut a, mut b) = (cost[1], cost[2]);
|
||||
for i in 3..=n {
|
||||
let tmp = b;
|
||||
b = cmp::min(a, tmp) + cost[i];
|
||||
a = tmp;
|
||||
}
|
||||
b
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="min_cost_climbing_stairs_dp.c"
|
||||
/* 爬樓梯最小代價:空間最佳化後的動態規劃 */
|
||||
int minCostClimbingStairsDPComp(int cost[], int costSize) {
|
||||
int n = costSize - 1;
|
||||
if (n == 1 || n == 2)
|
||||
return cost[n];
|
||||
int a = cost[1], b = cost[2];
|
||||
for (int i = 3; i <= n; i++) {
|
||||
int tmp = b;
|
||||
b = myMin(a, tmp) + cost[i];
|
||||
a = tmp;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="min_cost_climbing_stairs_dp.kt"
|
||||
/* 爬樓梯最小代價:空間最佳化後的動態規劃 */
|
||||
fun minCostClimbingStairsDPComp(cost: IntArray): Int {
|
||||
val n = cost.size - 1
|
||||
if (n == 1 || n == 2) return cost[n]
|
||||
var a = cost[1]
|
||||
var b = cost[2]
|
||||
for (i in 3..n) {
|
||||
val tmp = b
|
||||
b = (min(a.toDouble(), tmp.toDouble()) + cost[i]).toInt()
|
||||
a = tmp
|
||||
}
|
||||
return b
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="min_cost_climbing_stairs_dp.rb"
|
||||
[class]{}-[func]{min_cost_climbing_stairs_dp_comp}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="min_cost_climbing_stairs_dp.zig"
|
||||
// 爬樓梯最小代價:空間最佳化後的動態規劃
|
||||
fn minCostClimbingStairsDPComp(cost: []i32) i32 {
|
||||
var n = cost.len - 1;
|
||||
if (n == 1 or n == 2) {
|
||||
return cost[n];
|
||||
}
|
||||
var a = cost[1];
|
||||
var b = cost[2];
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for (3..n + 1) |i| {
|
||||
var tmp = b;
|
||||
b = @min(a, tmp) + cost[i];
|
||||
a = tmp;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 513px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20min_cost_climbing_stairs_dp_comp%28cost%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E7%88%AC%E6%A5%BC%E6%A2%AF%E6%9C%80%E5%B0%8F%E4%BB%A3%E4%BB%B7%EF%BC%9A%E7%A9%BA%E9%97%B4%E4%BC%98%E5%8C%96%E5%90%8E%E7%9A%84%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%22%22%22%0A%20%20%20%20n%20%3D%20len%28cost%29%20-%201%0A%20%20%20%20if%20n%20%3D%3D%201%20or%20n%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20return%20cost%5Bn%5D%0A%20%20%20%20a,%20b%20%3D%20cost%5B1%5D,%20cost%5B2%5D%0A%20%20%20%20for%20i%20in%20range%283,%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20a,%20b%20%3D%20b,%20min%28a,%20b%29%20%2B%20cost%5Bi%5D%0A%20%20%20%20return%20b%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20cost%20%3D%20%5B0,%201,%2010,%201,%201,%201,%2010,%201,%201,%2010,%201%5D%0A%20%20%20%20print%28f%22%E8%BE%93%E5%85%A5%E6%A5%BC%E6%A2%AF%E7%9A%84%E4%BB%A3%E4%BB%B7%E5%88%97%E8%A1%A8%E4%B8%BA%20%7Bcost%7D%22%29%0A%0A%20%20%20%20res%20%3D%20min_cost_climbing_stairs_dp_comp%28cost%29%0A%20%20%20%20print%28f%22%E7%88%AC%E5%AE%8C%E6%A5%BC%E6%A2%AF%E7%9A%84%E6%9C%80%E4%BD%8E%E4%BB%A3%E4%BB%B7%E4%B8%BA%20%7Bres%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20min_cost_climbing_stairs_dp_comp%28cost%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E7%88%AC%E6%A5%BC%E6%A2%AF%E6%9C%80%E5%B0%8F%E4%BB%A3%E4%BB%B7%EF%BC%9A%E7%A9%BA%E9%97%B4%E4%BC%98%E5%8C%96%E5%90%8E%E7%9A%84%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%22%22%22%0A%20%20%20%20n%20%3D%20len%28cost%29%20-%201%0A%20%20%20%20if%20n%20%3D%3D%201%20or%20n%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20return%20cost%5Bn%5D%0A%20%20%20%20a,%20b%20%3D%20cost%5B1%5D,%20cost%5B2%5D%0A%20%20%20%20for%20i%20in%20range%283,%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20a,%20b%20%3D%20b,%20min%28a,%20b%29%20%2B%20cost%5Bi%5D%0A%20%20%20%20return%20b%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20cost%20%3D%20%5B0,%201,%2010,%201,%201,%201,%2010,%201,%201,%2010,%201%5D%0A%20%20%20%20print%28f%22%E8%BE%93%E5%85%A5%E6%A5%BC%E6%A2%AF%E7%9A%84%E4%BB%A3%E4%BB%B7%E5%88%97%E8%A1%A8%E4%B8%BA%20%7Bcost%7D%22%29%0A%0A%20%20%20%20res%20%3D%20min_cost_climbing_stairs_dp_comp%28cost%29%0A%20%20%20%20print%28f%22%E7%88%AC%E5%AE%8C%E6%A5%BC%E6%A2%AF%E7%9A%84%E6%9C%80%E4%BD%8E%E4%BB%A3%E4%BB%B7%E4%B8%BA%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
## 14.2.2 無後效性
|
||||
|
||||
無後效性是動態規劃能夠有效解決問題的重要特性之一,其定義為:**給定一個確定的狀態,它的未來發展只與當前狀態有關,而與過去經歷的所有狀態無關**。
|
||||
|
||||
以爬樓梯問題為例,給定狀態 $i$ ,它會發展出狀態 $i+1$ 和狀態 $i+2$ ,分別對應跳 $1$ 步和跳 $2$ 步。在做出這兩種選擇時,我們無須考慮狀態 $i$ 之前的狀態,它們對狀態 $i$ 的未來沒有影響。
|
||||
|
||||
然而,如果我們給爬樓梯問題新增一個約束,情況就不一樣了。
|
||||
|
||||
!!! question "帶約束爬樓梯"
|
||||
|
||||
給定一個共有 $n$ 階的樓梯,你每步可以上 $1$ 階或者 $2$ 階,**但不能連續兩輪跳 $1$ 階**,請問有多少種方案可以爬到樓頂?
|
||||
|
||||
如圖 14-8 所示,爬上第 $3$ 階僅剩 $2$ 種可行方案,其中連續三次跳 $1$ 階的方案不滿足約束條件,因此被捨棄。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 14-8 帶約束爬到第 3 階的方案數量 </p>
|
||||
|
||||
在該問題中,如果上一輪是跳 $1$ 階上來的,那麼下一輪就必須跳 $2$ 階。這意味著,**下一步選擇不能由當前狀態(當前所在樓梯階數)獨立決定,還和前一個狀態(上一輪所在樓梯階數)有關**。
|
||||
|
||||
不難發現,此問題已不滿足無後效性,狀態轉移方程 $dp[i] = dp[i-1] + dp[i-2]$ 也失效了,因為 $dp[i-1]$ 代表本輪跳 $1$ 階,但其中包含了許多“上一輪是跳 $1$ 階上來的”方案,而為了滿足約束,我們就不能將 $dp[i-1]$ 直接計入 $dp[i]$ 中。
|
||||
|
||||
為此,我們需要擴展狀態定義:**狀態 $[i, j]$ 表示處在第 $i$ 階並且上一輪跳了 $j$ 階**,其中 $j \in \{1, 2\}$ 。此狀態定義有效地區分了上一輪跳了 $1$ 階還是 $2$ 階,我們可以據此判斷當前狀態是從何而來的。
|
||||
|
||||
- 當上一輪跳了 $1$ 階時,上上一輪只能選擇跳 $2$ 階,即 $dp[i, 1]$ 只能從 $dp[i-1, 2]$ 轉移過來。
|
||||
- 當上一輪跳了 $2$ 階時,上上一輪可選擇跳 $1$ 階或跳 $2$ 階,即 $dp[i, 2]$ 可以從 $dp[i-2, 1]$ 或 $dp[i-2, 2]$ 轉移過來。
|
||||
|
||||
如圖 14-9 所示,在該定義下,$dp[i, j]$ 表示狀態 $[i, j]$ 對應的方案數。此時狀態轉移方程為:
|
||||
|
||||
$$
|
||||
\begin{cases}
|
||||
dp[i, 1] = dp[i-1, 2] \\
|
||||
dp[i, 2] = dp[i-2, 1] + dp[i-2, 2]
|
||||
\end{cases}
|
||||
$$
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 14-9 考慮約束下的遞推關係 </p>
|
||||
|
||||
最終,返回 $dp[n, 1] + dp[n, 2]$ 即可,兩者之和代表爬到第 $n$ 階的方案總數:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="climbing_stairs_constraint_dp.py"
|
||||
def climbing_stairs_constraint_dp(n: int) -> int:
|
||||
"""帶約束爬樓梯:動態規劃"""
|
||||
if n == 1 or n == 2:
|
||||
return 1
|
||||
# 初始化 dp 表,用於儲存子問題的解
|
||||
dp = [[0] * 3 for _ in range(n + 1)]
|
||||
# 初始狀態:預設最小子問題的解
|
||||
dp[1][1], dp[1][2] = 1, 0
|
||||
dp[2][1], dp[2][2] = 0, 1
|
||||
# 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for i in range(3, n + 1):
|
||||
dp[i][1] = dp[i - 1][2]
|
||||
dp[i][2] = dp[i - 2][1] + dp[i - 2][2]
|
||||
return dp[n][1] + dp[n][2]
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="climbing_stairs_constraint_dp.cpp"
|
||||
/* 帶約束爬樓梯:動態規劃 */
|
||||
int climbingStairsConstraintDP(int n) {
|
||||
if (n == 1 || n == 2) {
|
||||
return 1;
|
||||
}
|
||||
// 初始化 dp 表,用於儲存子問題的解
|
||||
vector<vector<int>> dp(n + 1, vector<int>(3, 0));
|
||||
// 初始狀態:預設最小子問題的解
|
||||
dp[1][1] = 1;
|
||||
dp[1][2] = 0;
|
||||
dp[2][1] = 0;
|
||||
dp[2][2] = 1;
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for (int i = 3; i <= n; i++) {
|
||||
dp[i][1] = dp[i - 1][2];
|
||||
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
|
||||
}
|
||||
return dp[n][1] + dp[n][2];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="climbing_stairs_constraint_dp.java"
|
||||
/* 帶約束爬樓梯:動態規劃 */
|
||||
int climbingStairsConstraintDP(int n) {
|
||||
if (n == 1 || n == 2) {
|
||||
return 1;
|
||||
}
|
||||
// 初始化 dp 表,用於儲存子問題的解
|
||||
int[][] dp = new int[n + 1][3];
|
||||
// 初始狀態:預設最小子問題的解
|
||||
dp[1][1] = 1;
|
||||
dp[1][2] = 0;
|
||||
dp[2][1] = 0;
|
||||
dp[2][2] = 1;
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for (int i = 3; i <= n; i++) {
|
||||
dp[i][1] = dp[i - 1][2];
|
||||
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
|
||||
}
|
||||
return dp[n][1] + dp[n][2];
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="climbing_stairs_constraint_dp.cs"
|
||||
/* 帶約束爬樓梯:動態規劃 */
|
||||
int ClimbingStairsConstraintDP(int n) {
|
||||
if (n == 1 || n == 2) {
|
||||
return 1;
|
||||
}
|
||||
// 初始化 dp 表,用於儲存子問題的解
|
||||
int[,] dp = new int[n + 1, 3];
|
||||
// 初始狀態:預設最小子問題的解
|
||||
dp[1, 1] = 1;
|
||||
dp[1, 2] = 0;
|
||||
dp[2, 1] = 0;
|
||||
dp[2, 2] = 1;
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for (int i = 3; i <= n; i++) {
|
||||
dp[i, 1] = dp[i - 1, 2];
|
||||
dp[i, 2] = dp[i - 2, 1] + dp[i - 2, 2];
|
||||
}
|
||||
return dp[n, 1] + dp[n, 2];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="climbing_stairs_constraint_dp.go"
|
||||
/* 帶約束爬樓梯:動態規劃 */
|
||||
func climbingStairsConstraintDP(n int) int {
|
||||
if n == 1 || n == 2 {
|
||||
return 1
|
||||
}
|
||||
// 初始化 dp 表,用於儲存子問題的解
|
||||
dp := make([][3]int, n+1)
|
||||
// 初始狀態:預設最小子問題的解
|
||||
dp[1][1] = 1
|
||||
dp[1][2] = 0
|
||||
dp[2][1] = 0
|
||||
dp[2][2] = 1
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for i := 3; i <= n; i++ {
|
||||
dp[i][1] = dp[i-1][2]
|
||||
dp[i][2] = dp[i-2][1] + dp[i-2][2]
|
||||
}
|
||||
return dp[n][1] + dp[n][2]
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="climbing_stairs_constraint_dp.swift"
|
||||
/* 帶約束爬樓梯:動態規劃 */
|
||||
func climbingStairsConstraintDP(n: Int) -> Int {
|
||||
if n == 1 || n == 2 {
|
||||
return 1
|
||||
}
|
||||
// 初始化 dp 表,用於儲存子問題的解
|
||||
var dp = Array(repeating: Array(repeating: 0, count: 3), count: n + 1)
|
||||
// 初始狀態:預設最小子問題的解
|
||||
dp[1][1] = 1
|
||||
dp[1][2] = 0
|
||||
dp[2][1] = 0
|
||||
dp[2][2] = 1
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for i in 3 ... n {
|
||||
dp[i][1] = dp[i - 1][2]
|
||||
dp[i][2] = dp[i - 2][1] + dp[i - 2][2]
|
||||
}
|
||||
return dp[n][1] + dp[n][2]
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="climbing_stairs_constraint_dp.js"
|
||||
/* 帶約束爬樓梯:動態規劃 */
|
||||
function climbingStairsConstraintDP(n) {
|
||||
if (n === 1 || n === 2) {
|
||||
return 1;
|
||||
}
|
||||
// 初始化 dp 表,用於儲存子問題的解
|
||||
const dp = Array.from(new Array(n + 1), () => new Array(3));
|
||||
// 初始狀態:預設最小子問題的解
|
||||
dp[1][1] = 1;
|
||||
dp[1][2] = 0;
|
||||
dp[2][1] = 0;
|
||||
dp[2][2] = 1;
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for (let i = 3; i <= n; i++) {
|
||||
dp[i][1] = dp[i - 1][2];
|
||||
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
|
||||
}
|
||||
return dp[n][1] + dp[n][2];
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="climbing_stairs_constraint_dp.ts"
|
||||
/* 帶約束爬樓梯:動態規劃 */
|
||||
function climbingStairsConstraintDP(n: number): number {
|
||||
if (n === 1 || n === 2) {
|
||||
return 1;
|
||||
}
|
||||
// 初始化 dp 表,用於儲存子問題的解
|
||||
const dp = Array.from({ length: n + 1 }, () => new Array(3));
|
||||
// 初始狀態:預設最小子問題的解
|
||||
dp[1][1] = 1;
|
||||
dp[1][2] = 0;
|
||||
dp[2][1] = 0;
|
||||
dp[2][2] = 1;
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for (let i = 3; i <= n; i++) {
|
||||
dp[i][1] = dp[i - 1][2];
|
||||
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
|
||||
}
|
||||
return dp[n][1] + dp[n][2];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="climbing_stairs_constraint_dp.dart"
|
||||
/* 帶約束爬樓梯:動態規劃 */
|
||||
int climbingStairsConstraintDP(int n) {
|
||||
if (n == 1 || n == 2) {
|
||||
return 1;
|
||||
}
|
||||
// 初始化 dp 表,用於儲存子問題的解
|
||||
List<List<int>> dp = List.generate(n + 1, (index) => List.filled(3, 0));
|
||||
// 初始狀態:預設最小子問題的解
|
||||
dp[1][1] = 1;
|
||||
dp[1][2] = 0;
|
||||
dp[2][1] = 0;
|
||||
dp[2][2] = 1;
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for (int i = 3; i <= n; i++) {
|
||||
dp[i][1] = dp[i - 1][2];
|
||||
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
|
||||
}
|
||||
return dp[n][1] + dp[n][2];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="climbing_stairs_constraint_dp.rs"
|
||||
/* 帶約束爬樓梯:動態規劃 */
|
||||
fn climbing_stairs_constraint_dp(n: usize) -> i32 {
|
||||
if n == 1 || n == 2 {
|
||||
return 1;
|
||||
};
|
||||
// 初始化 dp 表,用於儲存子問題的解
|
||||
let mut dp = vec![vec![-1; 3]; n + 1];
|
||||
// 初始狀態:預設最小子問題的解
|
||||
dp[1][1] = 1;
|
||||
dp[1][2] = 0;
|
||||
dp[2][1] = 0;
|
||||
dp[2][2] = 1;
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for i in 3..=n {
|
||||
dp[i][1] = dp[i - 1][2];
|
||||
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
|
||||
}
|
||||
dp[n][1] + dp[n][2]
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="climbing_stairs_constraint_dp.c"
|
||||
/* 帶約束爬樓梯:動態規劃 */
|
||||
int climbingStairsConstraintDP(int n) {
|
||||
if (n == 1 || n == 2) {
|
||||
return 1;
|
||||
}
|
||||
// 初始化 dp 表,用於儲存子問題的解
|
||||
int **dp = malloc((n + 1) * sizeof(int *));
|
||||
for (int i = 0; i <= n; i++) {
|
||||
dp[i] = calloc(3, sizeof(int));
|
||||
}
|
||||
// 初始狀態:預設最小子問題的解
|
||||
dp[1][1] = 1;
|
||||
dp[1][2] = 0;
|
||||
dp[2][1] = 0;
|
||||
dp[2][2] = 1;
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for (int i = 3; i <= n; i++) {
|
||||
dp[i][1] = dp[i - 1][2];
|
||||
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
|
||||
}
|
||||
int res = dp[n][1] + dp[n][2];
|
||||
// 釋放記憶體
|
||||
for (int i = 0; i <= n; i++) {
|
||||
free(dp[i]);
|
||||
}
|
||||
free(dp);
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="climbing_stairs_constraint_dp.kt"
|
||||
/* 帶約束爬樓梯:動態規劃 */
|
||||
fun climbingStairsConstraintDP(n: Int): Int {
|
||||
if (n == 1 || n == 2) {
|
||||
return 1
|
||||
}
|
||||
// 初始化 dp 表,用於儲存子問題的解
|
||||
val dp = Array(n + 1) { IntArray(3) }
|
||||
// 初始狀態:預設最小子問題的解
|
||||
dp[1][1] = 1
|
||||
dp[1][2] = 0
|
||||
dp[2][1] = 0
|
||||
dp[2][2] = 1
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for (i in 3..n) {
|
||||
dp[i][1] = dp[i - 1][2]
|
||||
dp[i][2] = dp[i - 2][1] + dp[i - 2][2]
|
||||
}
|
||||
return dp[n][1] + dp[n][2]
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="climbing_stairs_constraint_dp.rb"
|
||||
[class]{}-[func]{climbing_stairs_constraint_dp}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="climbing_stairs_constraint_dp.zig"
|
||||
// 帶約束爬樓梯:動態規劃
|
||||
fn climbingStairsConstraintDP(comptime n: usize) i32 {
|
||||
if (n == 1 or n == 2) {
|
||||
return 1;
|
||||
}
|
||||
// 初始化 dp 表,用於儲存子問題的解
|
||||
var dp = [_][3]i32{ [_]i32{ -1, -1, -1 } } ** (n + 1);
|
||||
// 初始狀態:預設最小子問題的解
|
||||
dp[1][1] = 1;
|
||||
dp[1][2] = 0;
|
||||
dp[2][1] = 0;
|
||||
dp[2][2] = 1;
|
||||
// 狀態轉移:從較小子問題逐步求解較大子問題
|
||||
for (3..n + 1) |i| {
|
||||
dp[i][1] = dp[i - 1][2];
|
||||
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
|
||||
}
|
||||
return dp[n][1] + dp[n][2];
|
||||
}
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20climbing_stairs_constraint_dp%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%B8%A6%E7%BA%A6%E6%9D%9F%E7%88%AC%E6%A5%BC%E6%A2%AF%EF%BC%9A%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%22%22%22%0A%20%20%20%20if%20n%20%3D%3D%201%20or%20n%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20return%201%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%20dp%20%E8%A1%A8%EF%BC%8C%E7%94%A8%E4%BA%8E%E5%AD%98%E5%82%A8%E5%AD%90%E9%97%AE%E9%A2%98%E7%9A%84%E8%A7%A3%0A%20%20%20%20dp%20%3D%20%5B%5B0%5D%20*%203%20for%20_%20in%20range%28n%20%2B%201%29%5D%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E7%8A%B6%E6%80%81%EF%BC%9A%E9%A2%84%E8%AE%BE%E6%9C%80%E5%B0%8F%E5%AD%90%E9%97%AE%E9%A2%98%E7%9A%84%E8%A7%A3%0A%20%20%20%20dp%5B1%5D%5B1%5D,%20dp%5B1%5D%5B2%5D%20%3D%201,%200%0A%20%20%20%20dp%5B2%5D%5B1%5D,%20dp%5B2%5D%5B2%5D%20%3D%200,%201%0A%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E4%BB%8E%E8%BE%83%E5%B0%8F%E5%AD%90%E9%97%AE%E9%A2%98%E9%80%90%E6%AD%A5%E6%B1%82%E8%A7%A3%E8%BE%83%E5%A4%A7%E5%AD%90%E9%97%AE%E9%A2%98%0A%20%20%20%20for%20i%20in%20range%283,%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20dp%5Bi%5D%5B1%5D%20%3D%20dp%5Bi%20-%201%5D%5B2%5D%0A%20%20%20%20%20%20%20%20dp%5Bi%5D%5B2%5D%20%3D%20dp%5Bi%20-%202%5D%5B1%5D%20%2B%20dp%5Bi%20-%202%5D%5B2%5D%0A%20%20%20%20return%20dp%5Bn%5D%5B1%5D%20%2B%20dp%5Bn%5D%5B2%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%209%0A%0A%20%20%20%20res%20%3D%20climbing_stairs_constraint_dp%28n%29%0A%20%20%20%20print%28f%22%E7%88%AC%20%7Bn%7D%20%E9%98%B6%E6%A5%BC%E6%A2%AF%E5%85%B1%E6%9C%89%20%7Bres%7D%20%E7%A7%8D%E6%96%B9%E6%A1%88%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20climbing_stairs_constraint_dp%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%B8%A6%E7%BA%A6%E6%9D%9F%E7%88%AC%E6%A5%BC%E6%A2%AF%EF%BC%9A%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%22%22%22%0A%20%20%20%20if%20n%20%3D%3D%201%20or%20n%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20return%201%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%20dp%20%E8%A1%A8%EF%BC%8C%E7%94%A8%E4%BA%8E%E5%AD%98%E5%82%A8%E5%AD%90%E9%97%AE%E9%A2%98%E7%9A%84%E8%A7%A3%0A%20%20%20%20dp%20%3D%20%5B%5B0%5D%20*%203%20for%20_%20in%20range%28n%20%2B%201%29%5D%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E7%8A%B6%E6%80%81%EF%BC%9A%E9%A2%84%E8%AE%BE%E6%9C%80%E5%B0%8F%E5%AD%90%E9%97%AE%E9%A2%98%E7%9A%84%E8%A7%A3%0A%20%20%20%20dp%5B1%5D%5B1%5D,%20dp%5B1%5D%5B2%5D%20%3D%201,%200%0A%20%20%20%20dp%5B2%5D%5B1%5D,%20dp%5B2%5D%5B2%5D%20%3D%200,%201%0A%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E4%BB%8E%E8%BE%83%E5%B0%8F%E5%AD%90%E9%97%AE%E9%A2%98%E9%80%90%E6%AD%A5%E6%B1%82%E8%A7%A3%E8%BE%83%E5%A4%A7%E5%AD%90%E9%97%AE%E9%A2%98%0A%20%20%20%20for%20i%20in%20range%283,%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20dp%5Bi%5D%5B1%5D%20%3D%20dp%5Bi%20-%201%5D%5B2%5D%0A%20%20%20%20%20%20%20%20dp%5Bi%5D%5B2%5D%20%3D%20dp%5Bi%20-%202%5D%5B1%5D%20%2B%20dp%5Bi%20-%202%5D%5B2%5D%0A%20%20%20%20return%20dp%5Bn%5D%5B1%5D%20%2B%20dp%5Bn%5D%5B2%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%209%0A%0A%20%20%20%20res%20%3D%20climbing_stairs_constraint_dp%28n%29%0A%20%20%20%20print%28f%22%E7%88%AC%20%7Bn%7D%20%E9%98%B6%E6%A5%BC%E6%A2%AF%E5%85%B1%E6%9C%89%20%7Bres%7D%20%E7%A7%8D%E6%96%B9%E6%A1%88%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
在上面的案例中,由於僅需多考慮前面一個狀態,因此我們仍然可以透過擴展狀態定義,使得問題重新滿足無後效性。然而,某些問題具有非常嚴重的“有後效性”。
|
||||
|
||||
!!! question "爬樓梯與障礙生成"
|
||||
|
||||
給定一個共有 $n$ 階的樓梯,你每步可以上 $1$ 階或者 $2$ 階。**規定當爬到第 $i$ 階時,系統自動會在第 $2i$ 階上放上障礙物,之後所有輪都不允許跳到第 $2i$ 階上**。例如,前兩輪分別跳到了第 $2$、$3$ 階上,則之後就不能跳到第 $4$、$6$ 階上。請問有多少種方案可以爬到樓頂?
|
||||
|
||||
在這個問題中,下次跳躍依賴過去所有的狀態,因為每一次跳躍都會在更高的階梯上設定障礙,並影響未來的跳躍。對於這類問題,動態規劃往往難以解決。
|
||||
|
||||
實際上,許多複雜的組合最佳化問題(例如旅行商問題)不滿足無後效性。對於這類問題,我們通常會選擇使用其他方法,例如啟發式搜尋、遺傳演算法、強化學習等,從而在有限時間內得到可用的區域性最優解。
|
||||
1561
zh-Hant/docs/chapter_dynamic_programming/dp_solution_pipeline.md
Normal file
1561
zh-Hant/docs/chapter_dynamic_programming/dp_solution_pipeline.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,996 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 14.6 編輯距離問題
|
||||
|
||||
編輯距離,也稱 Levenshtein 距離,指兩個字串之間互相轉換的最少修改次數,通常用於在資訊檢索和自然語言處理中度量兩個序列的相似度。
|
||||
|
||||
!!! question
|
||||
|
||||
輸入兩個字串 $s$ 和 $t$ ,返回將 $s$ 轉換為 $t$ 所需的最少編輯步數。
|
||||
|
||||
你可以在一個字串中進行三種編輯操作:插入一個字元、刪除一個字元、將字元替換為任意一個字元。
|
||||
|
||||
如圖 14-27 所示,將 `kitten` 轉換為 `sitting` 需要編輯 3 步,包括 2 次替換操作與 1 次新增操作;將 `hello` 轉換為 `algo` 需要 3 步,包括 2 次替換操作和 1 次刪除操作。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 14-27 編輯距離的示例資料 </p>
|
||||
|
||||
**編輯距離問題可以很自然地用決策樹模型來解釋**。字串對應樹節點,一輪決策(一次編輯操作)對應樹的一條邊。
|
||||
|
||||
如圖 14-28 所示,在不限制操作的情況下,每個節點都可以派生出許多條邊,每條邊對應一種操作,這意味著從 `hello` 轉換到 `algo` 有許多種可能的路徑。
|
||||
|
||||
從決策樹的角度看,本題的目標是求解節點 `hello` 和節點 `algo` 之間的最短路徑。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 14-28 基於決策樹模型表示編輯距離問題 </p>
|
||||
|
||||
### 1. 動態規劃思路
|
||||
|
||||
**第一步:思考每輪的決策,定義狀態,從而得到 $dp$ 表**
|
||||
|
||||
每一輪的決策是對字串 $s$ 進行一次編輯操作。
|
||||
|
||||
我們希望在編輯操作的過程中,問題的規模逐漸縮小,這樣才能構建子問題。設字串 $s$ 和 $t$ 的長度分別為 $n$ 和 $m$ ,我們先考慮兩字串尾部的字元 $s[n-1]$ 和 $t[m-1]$ 。
|
||||
|
||||
- 若 $s[n-1]$ 和 $t[m-1]$ 相同,我們可以跳過它們,直接考慮 $s[n-2]$ 和 $t[m-2]$ 。
|
||||
- 若 $s[n-1]$ 和 $t[m-1]$ 不同,我們需要對 $s$ 進行一次編輯(插入、刪除、替換),使得兩字串尾部的字元相同,從而可以跳過它們,考慮規模更小的問題。
|
||||
|
||||
也就是說,我們在字串 $s$ 中進行的每一輪決策(編輯操作),都會使得 $s$ 和 $t$ 中剩餘的待匹配字元發生變化。因此,狀態為當前在 $s$ 和 $t$ 中考慮的第 $i$ 和第 $j$ 個字元,記為 $[i, j]$ 。
|
||||
|
||||
狀態 $[i, j]$ 對應的子問題:**將 $s$ 的前 $i$ 個字元更改為 $t$ 的前 $j$ 個字元所需的最少編輯步數**。
|
||||
|
||||
至此,得到一個尺寸為 $(i+1) \times (j+1)$ 的二維 $dp$ 表。
|
||||
|
||||
**第二步:找出最優子結構,進而推導出狀態轉移方程**
|
||||
|
||||
考慮子問題 $dp[i, j]$ ,其對應的兩個字串的尾部字元為 $s[i-1]$ 和 $t[j-1]$ ,可根據不同編輯操作分為圖 14-29 所示的三種情況。
|
||||
|
||||
1. 在 $s[i-1]$ 之後新增 $t[j-1]$ ,則剩餘子問題 $dp[i, j-1]$ 。
|
||||
2. 刪除 $s[i-1]$ ,則剩餘子問題 $dp[i-1, j]$ 。
|
||||
3. 將 $s[i-1]$ 替換為 $t[j-1]$ ,則剩餘子問題 $dp[i-1, j-1]$ 。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 14-29 編輯距離的狀態轉移 </p>
|
||||
|
||||
根據以上分析,可得最優子結構:$dp[i, j]$ 的最少編輯步數等於 $dp[i, j-1]$、$dp[i-1, j]$、$dp[i-1, j-1]$ 三者中的最少編輯步數,再加上本次的編輯步數 $1$ 。對應的狀態轉移方程為:
|
||||
|
||||
$$
|
||||
dp[i, j] = \min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1
|
||||
$$
|
||||
|
||||
請注意,**當 $s[i-1]$ 和 $t[j-1]$ 相同時,無須編輯當前字元**,這種情況下的狀態轉移方程為:
|
||||
|
||||
$$
|
||||
dp[i, j] = dp[i-1, j-1]
|
||||
$$
|
||||
|
||||
**第三步:確定邊界條件和狀態轉移順序**
|
||||
|
||||
當兩字串都為空時,編輯步數為 $0$ ,即 $dp[0, 0] = 0$ 。當 $s$ 為空但 $t$ 不為空時,最少編輯步數等於 $t$ 的長度,即首行 $dp[0, j] = j$ 。當 $s$ 不為空但 $t$ 為空時,最少編輯步數等於 $s$ 的長度,即首列 $dp[i, 0] = i$ 。
|
||||
|
||||
觀察狀態轉移方程,解 $dp[i, j]$ 依賴左方、上方、左上方的解,因此透過兩層迴圈正序走訪整個 $dp$ 表即可。
|
||||
|
||||
### 2. 程式碼實現
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="edit_distance.py"
|
||||
def edit_distance_dp(s: str, t: str) -> int:
|
||||
"""編輯距離:動態規劃"""
|
||||
n, m = len(s), len(t)
|
||||
dp = [[0] * (m + 1) for _ in range(n + 1)]
|
||||
# 狀態轉移:首行首列
|
||||
for i in range(1, n + 1):
|
||||
dp[i][0] = i
|
||||
for j in range(1, m + 1):
|
||||
dp[0][j] = j
|
||||
# 狀態轉移:其餘行和列
|
||||
for i in range(1, n + 1):
|
||||
for j in range(1, m + 1):
|
||||
if s[i - 1] == t[j - 1]:
|
||||
# 若兩字元相等,則直接跳過此兩字元
|
||||
dp[i][j] = dp[i - 1][j - 1]
|
||||
else:
|
||||
# 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[i][j] = min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1
|
||||
return dp[n][m]
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="edit_distance.cpp"
|
||||
/* 編輯距離:動態規劃 */
|
||||
int editDistanceDP(string s, string t) {
|
||||
int n = s.length(), m = t.length();
|
||||
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
|
||||
// 狀態轉移:首行首列
|
||||
for (int i = 1; i <= n; i++) {
|
||||
dp[i][0] = i;
|
||||
}
|
||||
for (int j = 1; j <= m; j++) {
|
||||
dp[0][j] = j;
|
||||
}
|
||||
// 狀態轉移:其餘行和列
|
||||
for (int i = 1; i <= n; i++) {
|
||||
for (int j = 1; j <= m; j++) {
|
||||
if (s[i - 1] == t[j - 1]) {
|
||||
// 若兩字元相等,則直接跳過此兩字元
|
||||
dp[i][j] = dp[i - 1][j - 1];
|
||||
} else {
|
||||
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return dp[n][m];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="edit_distance.java"
|
||||
/* 編輯距離:動態規劃 */
|
||||
int editDistanceDP(String s, String t) {
|
||||
int n = s.length(), m = t.length();
|
||||
int[][] dp = new int[n + 1][m + 1];
|
||||
// 狀態轉移:首行首列
|
||||
for (int i = 1; i <= n; i++) {
|
||||
dp[i][0] = i;
|
||||
}
|
||||
for (int j = 1; j <= m; j++) {
|
||||
dp[0][j] = j;
|
||||
}
|
||||
// 狀態轉移:其餘行和列
|
||||
for (int i = 1; i <= n; i++) {
|
||||
for (int j = 1; j <= m; j++) {
|
||||
if (s.charAt(i - 1) == t.charAt(j - 1)) {
|
||||
// 若兩字元相等,則直接跳過此兩字元
|
||||
dp[i][j] = dp[i - 1][j - 1];
|
||||
} else {
|
||||
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[i][j] = Math.min(Math.min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return dp[n][m];
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="edit_distance.cs"
|
||||
/* 編輯距離:動態規劃 */
|
||||
int EditDistanceDP(string s, string t) {
|
||||
int n = s.Length, m = t.Length;
|
||||
int[,] dp = new int[n + 1, m + 1];
|
||||
// 狀態轉移:首行首列
|
||||
for (int i = 1; i <= n; i++) {
|
||||
dp[i, 0] = i;
|
||||
}
|
||||
for (int j = 1; j <= m; j++) {
|
||||
dp[0, j] = j;
|
||||
}
|
||||
// 狀態轉移:其餘行和列
|
||||
for (int i = 1; i <= n; i++) {
|
||||
for (int j = 1; j <= m; j++) {
|
||||
if (s[i - 1] == t[j - 1]) {
|
||||
// 若兩字元相等,則直接跳過此兩字元
|
||||
dp[i, j] = dp[i - 1, j - 1];
|
||||
} else {
|
||||
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[i, j] = Math.Min(Math.Min(dp[i, j - 1], dp[i - 1, j]), dp[i - 1, j - 1]) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return dp[n, m];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="edit_distance.go"
|
||||
/* 編輯距離:動態規劃 */
|
||||
func editDistanceDP(s string, t string) int {
|
||||
n := len(s)
|
||||
m := len(t)
|
||||
dp := make([][]int, n+1)
|
||||
for i := 0; i <= n; i++ {
|
||||
dp[i] = make([]int, m+1)
|
||||
}
|
||||
// 狀態轉移:首行首列
|
||||
for i := 1; i <= n; i++ {
|
||||
dp[i][0] = i
|
||||
}
|
||||
for j := 1; j <= m; j++ {
|
||||
dp[0][j] = j
|
||||
}
|
||||
// 狀態轉移:其餘行和列
|
||||
for i := 1; i <= n; i++ {
|
||||
for j := 1; j <= m; j++ {
|
||||
if s[i-1] == t[j-1] {
|
||||
// 若兩字元相等,則直接跳過此兩字元
|
||||
dp[i][j] = dp[i-1][j-1]
|
||||
} else {
|
||||
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[i][j] = MinInt(MinInt(dp[i][j-1], dp[i-1][j]), dp[i-1][j-1]) + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return dp[n][m]
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="edit_distance.swift"
|
||||
/* 編輯距離:動態規劃 */
|
||||
func editDistanceDP(s: String, t: String) -> Int {
|
||||
let n = s.utf8CString.count
|
||||
let m = t.utf8CString.count
|
||||
var dp = Array(repeating: Array(repeating: 0, count: m + 1), count: n + 1)
|
||||
// 狀態轉移:首行首列
|
||||
for i in 1 ... n {
|
||||
dp[i][0] = i
|
||||
}
|
||||
for j in 1 ... m {
|
||||
dp[0][j] = j
|
||||
}
|
||||
// 狀態轉移:其餘行和列
|
||||
for i in 1 ... n {
|
||||
for j in 1 ... m {
|
||||
if s.utf8CString[i - 1] == t.utf8CString[j - 1] {
|
||||
// 若兩字元相等,則直接跳過此兩字元
|
||||
dp[i][j] = dp[i - 1][j - 1]
|
||||
} else {
|
||||
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return dp[n][m]
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="edit_distance.js"
|
||||
/* 編輯距離:動態規劃 */
|
||||
function editDistanceDP(s, t) {
|
||||
const n = s.length,
|
||||
m = t.length;
|
||||
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
||||
// 狀態轉移:首行首列
|
||||
for (let i = 1; i <= n; i++) {
|
||||
dp[i][0] = i;
|
||||
}
|
||||
for (let j = 1; j <= m; j++) {
|
||||
dp[0][j] = j;
|
||||
}
|
||||
// 狀態轉移:其餘行和列
|
||||
for (let i = 1; i <= n; i++) {
|
||||
for (let j = 1; j <= m; j++) {
|
||||
if (s.charAt(i - 1) === t.charAt(j - 1)) {
|
||||
// 若兩字元相等,則直接跳過此兩字元
|
||||
dp[i][j] = dp[i - 1][j - 1];
|
||||
} else {
|
||||
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[i][j] =
|
||||
Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return dp[n][m];
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="edit_distance.ts"
|
||||
/* 編輯距離:動態規劃 */
|
||||
function editDistanceDP(s: string, t: string): number {
|
||||
const n = s.length,
|
||||
m = t.length;
|
||||
const dp = Array.from({ length: n + 1 }, () =>
|
||||
Array.from({ length: m + 1 }, () => 0)
|
||||
);
|
||||
// 狀態轉移:首行首列
|
||||
for (let i = 1; i <= n; i++) {
|
||||
dp[i][0] = i;
|
||||
}
|
||||
for (let j = 1; j <= m; j++) {
|
||||
dp[0][j] = j;
|
||||
}
|
||||
// 狀態轉移:其餘行和列
|
||||
for (let i = 1; i <= n; i++) {
|
||||
for (let j = 1; j <= m; j++) {
|
||||
if (s.charAt(i - 1) === t.charAt(j - 1)) {
|
||||
// 若兩字元相等,則直接跳過此兩字元
|
||||
dp[i][j] = dp[i - 1][j - 1];
|
||||
} else {
|
||||
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[i][j] =
|
||||
Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return dp[n][m];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="edit_distance.dart"
|
||||
/* 編輯距離:動態規劃 */
|
||||
int editDistanceDP(String s, String t) {
|
||||
int n = s.length, m = t.length;
|
||||
List<List<int>> dp = List.generate(n + 1, (_) => List.filled(m + 1, 0));
|
||||
// 狀態轉移:首行首列
|
||||
for (int i = 1; i <= n; i++) {
|
||||
dp[i][0] = i;
|
||||
}
|
||||
for (int j = 1; j <= m; j++) {
|
||||
dp[0][j] = j;
|
||||
}
|
||||
// 狀態轉移:其餘行和列
|
||||
for (int i = 1; i <= n; i++) {
|
||||
for (int j = 1; j <= m; j++) {
|
||||
if (s[i - 1] == t[j - 1]) {
|
||||
// 若兩字元相等,則直接跳過此兩字元
|
||||
dp[i][j] = dp[i - 1][j - 1];
|
||||
} else {
|
||||
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return dp[n][m];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="edit_distance.rs"
|
||||
/* 編輯距離:動態規劃 */
|
||||
fn edit_distance_dp(s: &str, t: &str) -> i32 {
|
||||
let (n, m) = (s.len(), t.len());
|
||||
let mut dp = vec![vec![0; m + 1]; n + 1];
|
||||
// 狀態轉移:首行首列
|
||||
for i in 1..=n {
|
||||
dp[i][0] = i as i32;
|
||||
}
|
||||
for j in 1..m {
|
||||
dp[0][j] = j as i32;
|
||||
}
|
||||
// 狀態轉移:其餘行和列
|
||||
for i in 1..=n {
|
||||
for j in 1..=m {
|
||||
if s.chars().nth(i - 1) == t.chars().nth(j - 1) {
|
||||
// 若兩字元相等,則直接跳過此兩字元
|
||||
dp[i][j] = dp[i - 1][j - 1];
|
||||
} else {
|
||||
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[i][j] =
|
||||
std::cmp::min(std::cmp::min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
dp[n][m]
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="edit_distance.c"
|
||||
/* 編輯距離:動態規劃 */
|
||||
int editDistanceDP(char *s, char *t, int n, int m) {
|
||||
int **dp = malloc((n + 1) * sizeof(int *));
|
||||
for (int i = 0; i <= n; i++) {
|
||||
dp[i] = calloc(m + 1, sizeof(int));
|
||||
}
|
||||
// 狀態轉移:首行首列
|
||||
for (int i = 1; i <= n; i++) {
|
||||
dp[i][0] = i;
|
||||
}
|
||||
for (int j = 1; j <= m; j++) {
|
||||
dp[0][j] = j;
|
||||
}
|
||||
// 狀態轉移:其餘行和列
|
||||
for (int i = 1; i <= n; i++) {
|
||||
for (int j = 1; j <= m; j++) {
|
||||
if (s[i - 1] == t[j - 1]) {
|
||||
// 若兩字元相等,則直接跳過此兩字元
|
||||
dp[i][j] = dp[i - 1][j - 1];
|
||||
} else {
|
||||
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[i][j] = myMin(myMin(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
int res = dp[n][m];
|
||||
// 釋放記憶體
|
||||
for (int i = 0; i <= n; i++) {
|
||||
free(dp[i]);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="edit_distance.kt"
|
||||
/* 編輯距離:動態規劃 */
|
||||
fun editDistanceDP(s: String, t: String): Int {
|
||||
val n = s.length
|
||||
val m = t.length
|
||||
val dp = Array(n + 1) { IntArray(m + 1) }
|
||||
// 狀態轉移:首行首列
|
||||
for (i in 1..n) {
|
||||
dp[i][0] = i
|
||||
}
|
||||
for (j in 1..m) {
|
||||
dp[0][j] = j
|
||||
}
|
||||
// 狀態轉移:其餘行和列
|
||||
for (i in 1..n) {
|
||||
for (j in 1..m) {
|
||||
if (s[i - 1] == t[j - 1]) {
|
||||
// 若兩字元相等,則直接跳過此兩字元
|
||||
dp[i][j] = dp[i - 1][j - 1]
|
||||
} else {
|
||||
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[i][j] =
|
||||
(min(
|
||||
min(dp[i][j - 1].toDouble(), dp[i - 1][j].toDouble()),
|
||||
dp[i - 1][j - 1].toDouble()
|
||||
) + 1).toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
return dp[n][m]
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="edit_distance.rb"
|
||||
[class]{}-[func]{edit_distance_dp}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="edit_distance.zig"
|
||||
// 編輯距離:動態規劃
|
||||
fn editDistanceDP(comptime s: []const u8, comptime t: []const u8) i32 {
|
||||
comptime var n = s.len;
|
||||
comptime var m = t.len;
|
||||
var dp = [_][m + 1]i32{[_]i32{0} ** (m + 1)} ** (n + 1);
|
||||
// 狀態轉移:首行首列
|
||||
for (1..n + 1) |i| {
|
||||
dp[i][0] = @intCast(i);
|
||||
}
|
||||
for (1..m + 1) |j| {
|
||||
dp[0][j] = @intCast(j);
|
||||
}
|
||||
// 狀態轉移:其餘行和列
|
||||
for (1..n + 1) |i| {
|
||||
for (1..m + 1) |j| {
|
||||
if (s[i - 1] == t[j - 1]) {
|
||||
// 若兩字元相等,則直接跳過此兩字元
|
||||
dp[i][j] = dp[i - 1][j - 1];
|
||||
} else {
|
||||
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[i][j] = @min(@min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return dp[n][m];
|
||||
}
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20edit_distance_dp%28s%3A%20str,%20t%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E7%BC%96%E8%BE%91%E8%B7%9D%E7%A6%BB%EF%BC%9A%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%22%22%22%0A%20%20%20%20n,%20m%20%3D%20len%28s%29,%20len%28t%29%0A%20%20%20%20dp%20%3D%20%5B%5B0%5D%20*%20%28m%20%2B%201%29%20for%20_%20in%20range%28n%20%2B%201%29%5D%0A%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E9%A6%96%E8%A1%8C%E9%A6%96%E5%88%97%0A%20%20%20%20for%20i%20in%20range%281,%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20dp%5Bi%5D%5B0%5D%20%3D%20i%0A%20%20%20%20for%20j%20in%20range%281,%20m%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20dp%5B0%5D%5Bj%5D%20%3D%20j%0A%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E5%85%B6%E4%BD%99%E8%A1%8C%E5%92%8C%E5%88%97%0A%20%20%20%20for%20i%20in%20range%281,%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%281,%20m%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20s%5Bi%20-%201%5D%20%3D%3D%20t%5Bj%20-%201%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E4%B8%A4%E5%AD%97%E7%AC%A6%E7%9B%B8%E7%AD%89%EF%BC%8C%E5%88%99%E7%9B%B4%E6%8E%A5%E8%B7%B3%E8%BF%87%E6%AD%A4%E4%B8%A4%E5%AD%97%E7%AC%A6%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20dp%5Bi%5D%5Bj%5D%20%3D%20dp%5Bi%20-%201%5D%5Bj%20-%201%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E6%9C%80%E5%B0%91%E7%BC%96%E8%BE%91%E6%AD%A5%E6%95%B0%20%3D%20%E6%8F%92%E5%85%A5%E3%80%81%E5%88%A0%E9%99%A4%E3%80%81%E6%9B%BF%E6%8D%A2%E8%BF%99%E4%B8%89%E7%A7%8D%E6%93%8D%E4%BD%9C%E7%9A%84%E6%9C%80%E5%B0%91%E7%BC%96%E8%BE%91%E6%AD%A5%E6%95%B0%20%2B%201%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20dp%5Bi%5D%5Bj%5D%20%3D%20min%28dp%5Bi%5D%5Bj%20-%201%5D,%20dp%5Bi%20-%201%5D%5Bj%5D,%20dp%5Bi%20-%201%5D%5Bj%20-%201%5D%29%20%2B%201%0A%20%20%20%20return%20dp%5Bn%5D%5Bm%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20s%20%3D%20%22bag%22%0A%20%20%20%20t%20%3D%20%22pack%22%0A%20%20%20%20n,%20m%20%3D%20len%28s%29,%20len%28t%29%0A%0A%20%20%20%20%23%20%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%0A%20%20%20%20res%20%3D%20edit_distance_dp%28s,%20t%29%0A%20%20%20%20print%28f%22%E5%B0%86%20%7Bs%7D%20%E6%9B%B4%E6%94%B9%E4%B8%BA%20%7Bt%7D%20%E6%9C%80%E5%B0%91%E9%9C%80%E8%A6%81%E7%BC%96%E8%BE%91%20%7Bres%7D%20%E6%AD%A5%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20edit_distance_dp%28s%3A%20str,%20t%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E7%BC%96%E8%BE%91%E8%B7%9D%E7%A6%BB%EF%BC%9A%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%22%22%22%0A%20%20%20%20n,%20m%20%3D%20len%28s%29,%20len%28t%29%0A%20%20%20%20dp%20%3D%20%5B%5B0%5D%20*%20%28m%20%2B%201%29%20for%20_%20in%20range%28n%20%2B%201%29%5D%0A%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E9%A6%96%E8%A1%8C%E9%A6%96%E5%88%97%0A%20%20%20%20for%20i%20in%20range%281,%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20dp%5Bi%5D%5B0%5D%20%3D%20i%0A%20%20%20%20for%20j%20in%20range%281,%20m%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20dp%5B0%5D%5Bj%5D%20%3D%20j%0A%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E5%85%B6%E4%BD%99%E8%A1%8C%E5%92%8C%E5%88%97%0A%20%20%20%20for%20i%20in%20range%281,%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%281,%20m%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20s%5Bi%20-%201%5D%20%3D%3D%20t%5Bj%20-%201%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E4%B8%A4%E5%AD%97%E7%AC%A6%E7%9B%B8%E7%AD%89%EF%BC%8C%E5%88%99%E7%9B%B4%E6%8E%A5%E8%B7%B3%E8%BF%87%E6%AD%A4%E4%B8%A4%E5%AD%97%E7%AC%A6%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20dp%5Bi%5D%5Bj%5D%20%3D%20dp%5Bi%20-%201%5D%5Bj%20-%201%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E6%9C%80%E5%B0%91%E7%BC%96%E8%BE%91%E6%AD%A5%E6%95%B0%20%3D%20%E6%8F%92%E5%85%A5%E3%80%81%E5%88%A0%E9%99%A4%E3%80%81%E6%9B%BF%E6%8D%A2%E8%BF%99%E4%B8%89%E7%A7%8D%E6%93%8D%E4%BD%9C%E7%9A%84%E6%9C%80%E5%B0%91%E7%BC%96%E8%BE%91%E6%AD%A5%E6%95%B0%20%2B%201%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20dp%5Bi%5D%5Bj%5D%20%3D%20min%28dp%5Bi%5D%5Bj%20-%201%5D,%20dp%5Bi%20-%201%5D%5Bj%5D,%20dp%5Bi%20-%201%5D%5Bj%20-%201%5D%29%20%2B%201%0A%20%20%20%20return%20dp%5Bn%5D%5Bm%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20s%20%3D%20%22bag%22%0A%20%20%20%20t%20%3D%20%22pack%22%0A%20%20%20%20n,%20m%20%3D%20len%28s%29,%20len%28t%29%0A%0A%20%20%20%20%23%20%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%0A%20%20%20%20res%20%3D%20edit_distance_dp%28s,%20t%29%0A%20%20%20%20print%28f%22%E5%B0%86%20%7Bs%7D%20%E6%9B%B4%E6%94%B9%E4%B8%BA%20%7Bt%7D%20%E6%9C%80%E5%B0%91%E9%9C%80%E8%A6%81%E7%BC%96%E8%BE%91%20%7Bres%7D%20%E6%AD%A5%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
如圖 14-30 所示,編輯距離問題的狀態轉移過程與背包問題非常類似,都可以看作填寫一個二維網格的過程。
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<3>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<4>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<5>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<6>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<7>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<8>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<9>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<10>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<11>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<12>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<13>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<14>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<15>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 14-30 編輯距離的動態規劃過程 </p>
|
||||
|
||||
### 3. 空間最佳化
|
||||
|
||||
由於 $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]$ ,因此兩種走訪順序都不可取。
|
||||
|
||||
為此,我們可以使用一個變數 `leftup` 來暫存左上方的解 $dp[i-1, j-1]$ ,從而只需考慮左方和上方的解。此時的情況與完全背包問題相同,可使用正序走訪。程式碼如下所示:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="edit_distance.py"
|
||||
def edit_distance_dp_comp(s: str, t: str) -> int:
|
||||
"""編輯距離:空間最佳化後的動態規劃"""
|
||||
n, m = len(s), len(t)
|
||||
dp = [0] * (m + 1)
|
||||
# 狀態轉移:首行
|
||||
for j in range(1, m + 1):
|
||||
dp[j] = j
|
||||
# 狀態轉移:其餘行
|
||||
for i in range(1, n + 1):
|
||||
# 狀態轉移:首列
|
||||
leftup = dp[0] # 暫存 dp[i-1, j-1]
|
||||
dp[0] += 1
|
||||
# 狀態轉移:其餘列
|
||||
for j in range(1, m + 1):
|
||||
temp = dp[j]
|
||||
if s[i - 1] == t[j - 1]:
|
||||
# 若兩字元相等,則直接跳過此兩字元
|
||||
dp[j] = leftup
|
||||
else:
|
||||
# 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[j] = min(dp[j - 1], dp[j], leftup) + 1
|
||||
leftup = temp # 更新為下一輪的 dp[i-1, j-1]
|
||||
return dp[m]
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="edit_distance.cpp"
|
||||
/* 編輯距離:空間最佳化後的動態規劃 */
|
||||
int editDistanceDPComp(string s, string t) {
|
||||
int n = s.length(), m = t.length();
|
||||
vector<int> dp(m + 1, 0);
|
||||
// 狀態轉移:首行
|
||||
for (int j = 1; j <= m; j++) {
|
||||
dp[j] = j;
|
||||
}
|
||||
// 狀態轉移:其餘行
|
||||
for (int i = 1; i <= n; i++) {
|
||||
// 狀態轉移:首列
|
||||
int leftup = dp[0]; // 暫存 dp[i-1, j-1]
|
||||
dp[0] = i;
|
||||
// 狀態轉移:其餘列
|
||||
for (int j = 1; j <= m; j++) {
|
||||
int temp = dp[j];
|
||||
if (s[i - 1] == t[j - 1]) {
|
||||
// 若兩字元相等,則直接跳過此兩字元
|
||||
dp[j] = leftup;
|
||||
} else {
|
||||
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;
|
||||
}
|
||||
leftup = temp; // 更新為下一輪的 dp[i-1, j-1]
|
||||
}
|
||||
}
|
||||
return dp[m];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="edit_distance.java"
|
||||
/* 編輯距離:空間最佳化後的動態規劃 */
|
||||
int editDistanceDPComp(String s, String t) {
|
||||
int n = s.length(), m = t.length();
|
||||
int[] dp = new int[m + 1];
|
||||
// 狀態轉移:首行
|
||||
for (int j = 1; j <= m; j++) {
|
||||
dp[j] = j;
|
||||
}
|
||||
// 狀態轉移:其餘行
|
||||
for (int i = 1; i <= n; i++) {
|
||||
// 狀態轉移:首列
|
||||
int leftup = dp[0]; // 暫存 dp[i-1, j-1]
|
||||
dp[0] = i;
|
||||
// 狀態轉移:其餘列
|
||||
for (int j = 1; j <= m; j++) {
|
||||
int temp = dp[j];
|
||||
if (s.charAt(i - 1) == t.charAt(j - 1)) {
|
||||
// 若兩字元相等,則直接跳過此兩字元
|
||||
dp[j] = leftup;
|
||||
} else {
|
||||
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), leftup) + 1;
|
||||
}
|
||||
leftup = temp; // 更新為下一輪的 dp[i-1, j-1]
|
||||
}
|
||||
}
|
||||
return dp[m];
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="edit_distance.cs"
|
||||
/* 編輯距離:空間最佳化後的動態規劃 */
|
||||
int EditDistanceDPComp(string s, string t) {
|
||||
int n = s.Length, m = t.Length;
|
||||
int[] dp = new int[m + 1];
|
||||
// 狀態轉移:首行
|
||||
for (int j = 1; j <= m; j++) {
|
||||
dp[j] = j;
|
||||
}
|
||||
// 狀態轉移:其餘行
|
||||
for (int i = 1; i <= n; i++) {
|
||||
// 狀態轉移:首列
|
||||
int leftup = dp[0]; // 暫存 dp[i-1, j-1]
|
||||
dp[0] = i;
|
||||
// 狀態轉移:其餘列
|
||||
for (int j = 1; j <= m; j++) {
|
||||
int temp = dp[j];
|
||||
if (s[i - 1] == t[j - 1]) {
|
||||
// 若兩字元相等,則直接跳過此兩字元
|
||||
dp[j] = leftup;
|
||||
} else {
|
||||
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[j] = Math.Min(Math.Min(dp[j - 1], dp[j]), leftup) + 1;
|
||||
}
|
||||
leftup = temp; // 更新為下一輪的 dp[i-1, j-1]
|
||||
}
|
||||
}
|
||||
return dp[m];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="edit_distance.go"
|
||||
/* 編輯距離:空間最佳化後的動態規劃 */
|
||||
func editDistanceDPComp(s string, t string) int {
|
||||
n := len(s)
|
||||
m := len(t)
|
||||
dp := make([]int, m+1)
|
||||
// 狀態轉移:首行
|
||||
for j := 1; j <= m; j++ {
|
||||
dp[j] = j
|
||||
}
|
||||
// 狀態轉移:其餘行
|
||||
for i := 1; i <= n; i++ {
|
||||
// 狀態轉移:首列
|
||||
leftUp := dp[0] // 暫存 dp[i-1, j-1]
|
||||
dp[0] = i
|
||||
// 狀態轉移:其餘列
|
||||
for j := 1; j <= m; j++ {
|
||||
temp := dp[j]
|
||||
if s[i-1] == t[j-1] {
|
||||
// 若兩字元相等,則直接跳過此兩字元
|
||||
dp[j] = leftUp
|
||||
} else {
|
||||
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[j] = MinInt(MinInt(dp[j-1], dp[j]), leftUp) + 1
|
||||
}
|
||||
leftUp = temp // 更新為下一輪的 dp[i-1, j-1]
|
||||
}
|
||||
}
|
||||
return dp[m]
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="edit_distance.swift"
|
||||
/* 編輯距離:空間最佳化後的動態規劃 */
|
||||
func editDistanceDPComp(s: String, t: String) -> Int {
|
||||
let n = s.utf8CString.count
|
||||
let m = t.utf8CString.count
|
||||
var dp = Array(repeating: 0, count: m + 1)
|
||||
// 狀態轉移:首行
|
||||
for j in 1 ... m {
|
||||
dp[j] = j
|
||||
}
|
||||
// 狀態轉移:其餘行
|
||||
for i in 1 ... n {
|
||||
// 狀態轉移:首列
|
||||
var leftup = dp[0] // 暫存 dp[i-1, j-1]
|
||||
dp[0] = i
|
||||
// 狀態轉移:其餘列
|
||||
for j in 1 ... m {
|
||||
let temp = dp[j]
|
||||
if s.utf8CString[i - 1] == t.utf8CString[j - 1] {
|
||||
// 若兩字元相等,則直接跳過此兩字元
|
||||
dp[j] = leftup
|
||||
} else {
|
||||
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1
|
||||
}
|
||||
leftup = temp // 更新為下一輪的 dp[i-1, j-1]
|
||||
}
|
||||
}
|
||||
return dp[m]
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="edit_distance.js"
|
||||
/* 編輯距離:狀態壓縮後的動態規劃 */
|
||||
function editDistanceDPComp(s, t) {
|
||||
const n = s.length,
|
||||
m = t.length;
|
||||
const dp = new Array(m + 1).fill(0);
|
||||
// 狀態轉移:首行
|
||||
for (let j = 1; j <= m; j++) {
|
||||
dp[j] = j;
|
||||
}
|
||||
// 狀態轉移:其餘行
|
||||
for (let i = 1; i <= n; i++) {
|
||||
// 狀態轉移:首列
|
||||
let leftup = dp[0]; // 暫存 dp[i-1, j-1]
|
||||
dp[0] = i;
|
||||
// 狀態轉移:其餘列
|
||||
for (let j = 1; j <= m; j++) {
|
||||
const temp = dp[j];
|
||||
if (s.charAt(i - 1) === t.charAt(j - 1)) {
|
||||
// 若兩字元相等,則直接跳過此兩字元
|
||||
dp[j] = leftup;
|
||||
} else {
|
||||
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;
|
||||
}
|
||||
leftup = temp; // 更新為下一輪的 dp[i-1, j-1]
|
||||
}
|
||||
}
|
||||
return dp[m];
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="edit_distance.ts"
|
||||
/* 編輯距離:狀態壓縮後的動態規劃 */
|
||||
function editDistanceDPComp(s: string, t: string): number {
|
||||
const n = s.length,
|
||||
m = t.length;
|
||||
const dp = new Array(m + 1).fill(0);
|
||||
// 狀態轉移:首行
|
||||
for (let j = 1; j <= m; j++) {
|
||||
dp[j] = j;
|
||||
}
|
||||
// 狀態轉移:其餘行
|
||||
for (let i = 1; i <= n; i++) {
|
||||
// 狀態轉移:首列
|
||||
let leftup = dp[0]; // 暫存 dp[i-1, j-1]
|
||||
dp[0] = i;
|
||||
// 狀態轉移:其餘列
|
||||
for (let j = 1; j <= m; j++) {
|
||||
const temp = dp[j];
|
||||
if (s.charAt(i - 1) === t.charAt(j - 1)) {
|
||||
// 若兩字元相等,則直接跳過此兩字元
|
||||
dp[j] = leftup;
|
||||
} else {
|
||||
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;
|
||||
}
|
||||
leftup = temp; // 更新為下一輪的 dp[i-1, j-1]
|
||||
}
|
||||
}
|
||||
return dp[m];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="edit_distance.dart"
|
||||
/* 編輯距離:空間最佳化後的動態規劃 */
|
||||
int editDistanceDPComp(String s, String t) {
|
||||
int n = s.length, m = t.length;
|
||||
List<int> dp = List.filled(m + 1, 0);
|
||||
// 狀態轉移:首行
|
||||
for (int j = 1; j <= m; j++) {
|
||||
dp[j] = j;
|
||||
}
|
||||
// 狀態轉移:其餘行
|
||||
for (int i = 1; i <= n; i++) {
|
||||
// 狀態轉移:首列
|
||||
int leftup = dp[0]; // 暫存 dp[i-1, j-1]
|
||||
dp[0] = i;
|
||||
// 狀態轉移:其餘列
|
||||
for (int j = 1; j <= m; j++) {
|
||||
int temp = dp[j];
|
||||
if (s[i - 1] == t[j - 1]) {
|
||||
// 若兩字元相等,則直接跳過此兩字元
|
||||
dp[j] = leftup;
|
||||
} else {
|
||||
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;
|
||||
}
|
||||
leftup = temp; // 更新為下一輪的 dp[i-1, j-1]
|
||||
}
|
||||
}
|
||||
return dp[m];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="edit_distance.rs"
|
||||
/* 編輯距離:空間最佳化後的動態規劃 */
|
||||
fn edit_distance_dp_comp(s: &str, t: &str) -> i32 {
|
||||
let (n, m) = (s.len(), t.len());
|
||||
let mut dp = vec![0; m + 1];
|
||||
// 狀態轉移:首行
|
||||
for j in 1..m {
|
||||
dp[j] = j as i32;
|
||||
}
|
||||
// 狀態轉移:其餘行
|
||||
for i in 1..=n {
|
||||
// 狀態轉移:首列
|
||||
let mut leftup = dp[0]; // 暫存 dp[i-1, j-1]
|
||||
dp[0] = i as i32;
|
||||
// 狀態轉移:其餘列
|
||||
for j in 1..=m {
|
||||
let temp = dp[j];
|
||||
if s.chars().nth(i - 1) == t.chars().nth(j - 1) {
|
||||
// 若兩字元相等,則直接跳過此兩字元
|
||||
dp[j] = leftup;
|
||||
} else {
|
||||
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[j] = std::cmp::min(std::cmp::min(dp[j - 1], dp[j]), leftup) + 1;
|
||||
}
|
||||
leftup = temp; // 更新為下一輪的 dp[i-1, j-1]
|
||||
}
|
||||
}
|
||||
dp[m]
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="edit_distance.c"
|
||||
/* 編輯距離:空間最佳化後的動態規劃 */
|
||||
int editDistanceDPComp(char *s, char *t, int n, int m) {
|
||||
int *dp = calloc(m + 1, sizeof(int));
|
||||
// 狀態轉移:首行
|
||||
for (int j = 1; j <= m; j++) {
|
||||
dp[j] = j;
|
||||
}
|
||||
// 狀態轉移:其餘行
|
||||
for (int i = 1; i <= n; i++) {
|
||||
// 狀態轉移:首列
|
||||
int leftup = dp[0]; // 暫存 dp[i-1, j-1]
|
||||
dp[0] = i;
|
||||
// 狀態轉移:其餘列
|
||||
for (int j = 1; j <= m; j++) {
|
||||
int temp = dp[j];
|
||||
if (s[i - 1] == t[j - 1]) {
|
||||
// 若兩字元相等,則直接跳過此兩字元
|
||||
dp[j] = leftup;
|
||||
} else {
|
||||
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[j] = myMin(myMin(dp[j - 1], dp[j]), leftup) + 1;
|
||||
}
|
||||
leftup = temp; // 更新為下一輪的 dp[i-1, j-1]
|
||||
}
|
||||
}
|
||||
int res = dp[m];
|
||||
// 釋放記憶體
|
||||
free(dp);
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="edit_distance.kt"
|
||||
/* 編輯距離:空間最佳化後的動態規劃 */
|
||||
fun editDistanceDPComp(s: String, t: String): Int {
|
||||
val n = s.length
|
||||
val m = t.length
|
||||
val dp = IntArray(m + 1)
|
||||
// 狀態轉移:首行
|
||||
for (j in 1..m) {
|
||||
dp[j] = j
|
||||
}
|
||||
// 狀態轉移:其餘行
|
||||
for (i in 1..n) {
|
||||
// 狀態轉移:首列
|
||||
var leftup = dp[0] // 暫存 dp[i-1, j-1]
|
||||
dp[0] = i
|
||||
// 狀態轉移:其餘列
|
||||
for (j in 1..m) {
|
||||
val temp = dp[j]
|
||||
if (s[i - 1] == t[j - 1]) {
|
||||
// 若兩字元相等,則直接跳過此兩字元
|
||||
dp[j] = leftup
|
||||
} else {
|
||||
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[j] = (min(min(dp[j - 1].toDouble(), dp[j].toDouble()), leftup.toDouble()) + 1).toInt()
|
||||
}
|
||||
leftup = temp // 更新為下一輪的 dp[i-1, j-1]
|
||||
}
|
||||
}
|
||||
return dp[m]
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="edit_distance.rb"
|
||||
[class]{}-[func]{edit_distance_dp_comp}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="edit_distance.zig"
|
||||
// 編輯距離:空間最佳化後的動態規劃
|
||||
fn editDistanceDPComp(comptime s: []const u8, comptime t: []const u8) i32 {
|
||||
comptime var n = s.len;
|
||||
comptime var m = t.len;
|
||||
var dp = [_]i32{0} ** (m + 1);
|
||||
// 狀態轉移:首行
|
||||
for (1..m + 1) |j| {
|
||||
dp[j] = @intCast(j);
|
||||
}
|
||||
// 狀態轉移:其餘行
|
||||
for (1..n + 1) |i| {
|
||||
// 狀態轉移:首列
|
||||
var leftup = dp[0]; // 暫存 dp[i-1, j-1]
|
||||
dp[0] = @intCast(i);
|
||||
// 狀態轉移:其餘列
|
||||
for (1..m + 1) |j| {
|
||||
var temp = dp[j];
|
||||
if (s[i - 1] == t[j - 1]) {
|
||||
// 若兩字元相等,則直接跳過此兩字元
|
||||
dp[j] = leftup;
|
||||
} else {
|
||||
// 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1
|
||||
dp[j] = @min(@min(dp[j - 1], dp[j]), leftup) + 1;
|
||||
}
|
||||
leftup = temp; // 更新為下一輪的 dp[i-1, j-1]
|
||||
}
|
||||
}
|
||||
return dp[m];
|
||||
}
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20edit_distance_dp_comp%28s%3A%20str,%20t%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E7%BC%96%E8%BE%91%E8%B7%9D%E7%A6%BB%EF%BC%9A%E7%A9%BA%E9%97%B4%E4%BC%98%E5%8C%96%E5%90%8E%E7%9A%84%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%22%22%22%0A%20%20%20%20n,%20m%20%3D%20len%28s%29,%20len%28t%29%0A%20%20%20%20dp%20%3D%20%5B0%5D%20*%20%28m%20%2B%201%29%0A%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E9%A6%96%E8%A1%8C%0A%20%20%20%20for%20j%20in%20range%281,%20m%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20dp%5Bj%5D%20%3D%20j%0A%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E5%85%B6%E4%BD%99%E8%A1%8C%0A%20%20%20%20for%20i%20in%20range%281,%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E9%A6%96%E5%88%97%0A%20%20%20%20%20%20%20%20leftup%20%3D%20dp%5B0%5D%20%20%23%20%E6%9A%82%E5%AD%98%20dp%5Bi-1,%20j-1%5D%0A%20%20%20%20%20%20%20%20dp%5B0%5D%20%2B%3D%201%0A%20%20%20%20%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E5%85%B6%E4%BD%99%E5%88%97%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%281,%20m%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20temp%20%3D%20dp%5Bj%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20s%5Bi%20-%201%5D%20%3D%3D%20t%5Bj%20-%201%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E4%B8%A4%E5%AD%97%E7%AC%A6%E7%9B%B8%E7%AD%89%EF%BC%8C%E5%88%99%E7%9B%B4%E6%8E%A5%E8%B7%B3%E8%BF%87%E6%AD%A4%E4%B8%A4%E5%AD%97%E7%AC%A6%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20dp%5Bj%5D%20%3D%20leftup%0A%20%20%20%20%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E6%9C%80%E5%B0%91%E7%BC%96%E8%BE%91%E6%AD%A5%E6%95%B0%20%3D%20%E6%8F%92%E5%85%A5%E3%80%81%E5%88%A0%E9%99%A4%E3%80%81%E6%9B%BF%E6%8D%A2%E8%BF%99%E4%B8%89%E7%A7%8D%E6%93%8D%E4%BD%9C%E7%9A%84%E6%9C%80%E5%B0%91%E7%BC%96%E8%BE%91%E6%AD%A5%E6%95%B0%20%2B%201%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20dp%5Bj%5D%20%3D%20min%28dp%5Bj%20-%201%5D,%20dp%5Bj%5D,%20leftup%29%20%2B%201%0A%20%20%20%20%20%20%20%20%20%20%20%20leftup%20%3D%20temp%20%20%23%20%E6%9B%B4%E6%96%B0%E4%B8%BA%E4%B8%8B%E4%B8%80%E8%BD%AE%E7%9A%84%20dp%5Bi-1,%20j-1%5D%0A%20%20%20%20return%20dp%5Bm%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20s%20%3D%20%22bag%22%0A%20%20%20%20t%20%3D%20%22pack%22%0A%20%20%20%20n,%20m%20%3D%20len%28s%29,%20len%28t%29%0A%0A%20%20%20%20%23%20%E7%A9%BA%E9%97%B4%E4%BC%98%E5%8C%96%E5%90%8E%E7%9A%84%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%0A%20%20%20%20res%20%3D%20edit_distance_dp_comp%28s,%20t%29%0A%20%20%20%20print%28f%22%E5%B0%86%20%7Bs%7D%20%E6%9B%B4%E6%94%B9%E4%B8%BA%20%7Bt%7D%20%E6%9C%80%E5%B0%91%E9%9C%80%E8%A6%81%E7%BC%96%E8%BE%91%20%7Bres%7D%20%E6%AD%A5%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20edit_distance_dp_comp%28s%3A%20str,%20t%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E7%BC%96%E8%BE%91%E8%B7%9D%E7%A6%BB%EF%BC%9A%E7%A9%BA%E9%97%B4%E4%BC%98%E5%8C%96%E5%90%8E%E7%9A%84%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%22%22%22%0A%20%20%20%20n,%20m%20%3D%20len%28s%29,%20len%28t%29%0A%20%20%20%20dp%20%3D%20%5B0%5D%20*%20%28m%20%2B%201%29%0A%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E9%A6%96%E8%A1%8C%0A%20%20%20%20for%20j%20in%20range%281,%20m%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20dp%5Bj%5D%20%3D%20j%0A%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E5%85%B6%E4%BD%99%E8%A1%8C%0A%20%20%20%20for%20i%20in%20range%281,%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E9%A6%96%E5%88%97%0A%20%20%20%20%20%20%20%20leftup%20%3D%20dp%5B0%5D%20%20%23%20%E6%9A%82%E5%AD%98%20dp%5Bi-1,%20j-1%5D%0A%20%20%20%20%20%20%20%20dp%5B0%5D%20%2B%3D%201%0A%20%20%20%20%20%20%20%20%23%20%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%EF%BC%9A%E5%85%B6%E4%BD%99%E5%88%97%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%281,%20m%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20temp%20%3D%20dp%5Bj%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20s%5Bi%20-%201%5D%20%3D%3D%20t%5Bj%20-%201%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E4%B8%A4%E5%AD%97%E7%AC%A6%E7%9B%B8%E7%AD%89%EF%BC%8C%E5%88%99%E7%9B%B4%E6%8E%A5%E8%B7%B3%E8%BF%87%E6%AD%A4%E4%B8%A4%E5%AD%97%E7%AC%A6%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20dp%5Bj%5D%20%3D%20leftup%0A%20%20%20%20%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E6%9C%80%E5%B0%91%E7%BC%96%E8%BE%91%E6%AD%A5%E6%95%B0%20%3D%20%E6%8F%92%E5%85%A5%E3%80%81%E5%88%A0%E9%99%A4%E3%80%81%E6%9B%BF%E6%8D%A2%E8%BF%99%E4%B8%89%E7%A7%8D%E6%93%8D%E4%BD%9C%E7%9A%84%E6%9C%80%E5%B0%91%E7%BC%96%E8%BE%91%E6%AD%A5%E6%95%B0%20%2B%201%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20dp%5Bj%5D%20%3D%20min%28dp%5Bj%20-%201%5D,%20dp%5Bj%5D,%20leftup%29%20%2B%201%0A%20%20%20%20%20%20%20%20%20%20%20%20leftup%20%3D%20temp%20%20%23%20%E6%9B%B4%E6%96%B0%E4%B8%BA%E4%B8%8B%E4%B8%80%E8%BD%AE%E7%9A%84%20dp%5Bi-1,%20j-1%5D%0A%20%20%20%20return%20dp%5Bm%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20s%20%3D%20%22bag%22%0A%20%20%20%20t%20%3D%20%22pack%22%0A%20%20%20%20n,%20m%20%3D%20len%28s%29,%20len%28t%29%0A%0A%20%20%20%20%23%20%E7%A9%BA%E9%97%B4%E4%BC%98%E5%8C%96%E5%90%8E%E7%9A%84%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%0A%20%20%20%20res%20%3D%20edit_distance_dp_comp%28s,%20t%29%0A%20%20%20%20print%28f%22%E5%B0%86%20%7Bs%7D%20%E6%9B%B4%E6%94%B9%E4%B8%BA%20%7Bt%7D%20%E6%9C%80%E5%B0%91%E9%9C%80%E8%A6%81%E7%BC%96%E8%BE%91%20%7Bres%7D%20%E6%AD%A5%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
24
zh-Hant/docs/chapter_dynamic_programming/index.md
Normal file
24
zh-Hant/docs/chapter_dynamic_programming/index.md
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/table-pivot
|
||||
---
|
||||
|
||||
# 第 14 章 動態規劃
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
小溪匯入河流,江河匯入大海。
|
||||
|
||||
動態規劃將小問題的解彙集成大問題的答案,一步步引領我們走向解決問題的彼岸。
|
||||
|
||||
## Chapter Contents
|
||||
|
||||
- [14.1 初探動態規劃](https://www.hello-algo.com/en/chapter_dynamic_programming/intro_to_dynamic_programming/)
|
||||
- [14.2 DP 問題特性](https://www.hello-algo.com/en/chapter_dynamic_programming/dp_problem_features/)
|
||||
- [14.3 DP 解題思路](https://www.hello-algo.com/en/chapter_dynamic_programming/dp_solution_pipeline/)
|
||||
- [14.4 0-1 背包問題](https://www.hello-algo.com/en/chapter_dynamic_programming/knapsack_problem/)
|
||||
- [14.5 完全背包問題](https://www.hello-algo.com/en/chapter_dynamic_programming/unbounded_knapsack_problem/)
|
||||
- [14.6 編輯距離問題](https://www.hello-algo.com/en/chapter_dynamic_programming/edit_distance_problem/)
|
||||
- [14.7 小結](https://www.hello-algo.com/en/chapter_dynamic_programming/summary/)
|
||||
File diff suppressed because it is too large
Load Diff
1488
zh-Hant/docs/chapter_dynamic_programming/knapsack_problem.md
Normal file
1488
zh-Hant/docs/chapter_dynamic_programming/knapsack_problem.md
Normal file
File diff suppressed because it is too large
Load Diff
27
zh-Hant/docs/chapter_dynamic_programming/summary.md
Normal file
27
zh-Hant/docs/chapter_dynamic_programming/summary.md
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 14.7 小結
|
||||
|
||||
- 動態規劃對問題進行分解,並透過儲存子問題的解來規避重複計算,提高計算效率。
|
||||
- 不考慮時間的前提下,所有動態規劃問題都可以用回溯(暴力搜尋)進行求解,但遞迴樹中存在大量的重疊子問題,效率極低。透過引入記憶化串列,可以儲存所有計算過的子問題的解,從而保證重疊子問題只被計算一次。
|
||||
- 記憶化搜尋是一種從頂至底的遞迴式解法,而與之對應的動態規劃是一種從底至頂的遞推式解法,其如同“填寫表格”一樣。由於當前狀態僅依賴某些區域性狀態,因此我們可以消除 $dp$ 表的一個維度,從而降低空間複雜度。
|
||||
- 子問題分解是一種通用的演算法思路,在分治、動態規劃、回溯中具有不同的性質。
|
||||
- 動態規劃問題有三大特性:重疊子問題、最優子結構、無後效性。
|
||||
- 如果原問題的最優解可以從子問題的最優解構建得來,則它就具有最優子結構。
|
||||
- 無後效性指對於一個狀態,其未來發展只與該狀態有關,而與過去經歷的所有狀態無關。許多組合最佳化問題不具有無後效性,無法使用動態規劃快速求解。
|
||||
|
||||
**背包問題**
|
||||
|
||||
- 背包問題是最典型的動態規劃問題之一,具有 0-1 背包、完全背包、多重背包等變種。
|
||||
- 0-1 背包的狀態定義為前 $i$ 個物品在剩餘容量為 $c$ 的背包中的最大價值。根據不放入背包和放入背包兩種決策,可得到最優子結構,並構建出狀態轉移方程。在空間最佳化中,由於每個狀態依賴正上方和左上方的狀態,因此需要倒序走訪串列,避免左上方狀態被覆蓋。
|
||||
- 完全背包問題的每種物品的選取數量無限制,因此選擇放入物品的狀態轉移與 0-1 背包問題不同。由於狀態依賴正上方和正左方的狀態,因此在空間最佳化中應當正序走訪。
|
||||
- 零錢兌換問題是完全背包問題的一個變種。它從求“最大”價值變為求“最小”硬幣數量,因此狀態轉移方程中的 $\max()$ 應改為 $\min()$ 。從追求“不超過”背包容量到追求“恰好”湊出目標金額,因此使用 $amt + 1$ 來表示“無法湊出目標金額”的無效解。
|
||||
- 零錢兌換問題 II 從求“最少硬幣數量”改為求“硬幣組合數量”,狀態轉移方程相應地從 $\min()$ 改為求和運算子。
|
||||
|
||||
**編輯距離問題**
|
||||
|
||||
- 編輯距離(Levenshtein 距離)用於衡量兩個字串之間的相似度,其定義為從一個字串到另一個字串的最少編輯步數,編輯操作包括新增、刪除、替換。
|
||||
- 編輯距離問題的狀態定義為將 $s$ 的前 $i$ 個字元更改為 $t$ 的前 $j$ 個字元所需的最少編輯步數。當 $s[i] \ne t[j]$ 時,具有三種決策:新增、刪除、替換,它們都有相應的剩餘子問題。據此便可以找出最優子結構與構建狀態轉移方程。而當 $s[i] = t[j]$ 時,無須編輯當前字元。
|
||||
- 在編輯距離中,狀態依賴其正上方、正左方、左上方的狀態,因此空間最佳化後正序或倒序走訪都無法正確地進行狀態轉移。為此,我們利用一個變數暫存左上方狀態,從而轉化到與完全背包問題等價的情況,可以在空間最佳化後進行正序走訪。
|
||||
File diff suppressed because it is too large
Load Diff
103
zh-Hant/docs/chapter_graph/graph.md
Normal file
103
zh-Hant/docs/chapter_graph/graph.md
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 9.1 圖
|
||||
|
||||
<u>圖(graph)</u>是一種非線性資料結構,由<u>頂點(vertex)</u>和<u>邊(edge)</u>組成。我們可以將圖 $G$ 抽象地表示為一組頂點 $V$ 和一組邊 $E$ 的集合。以下示例展示了一個包含 5 個頂點和 7 條邊的圖。
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
V & = \{ 1, 2, 3, 4, 5 \} \newline
|
||||
E & = \{ (1,2), (1,3), (1,5), (2,3), (2,4), (2,5), (4,5) \} \newline
|
||||
G & = \{ V, E \} \newline
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
如果將頂點看作節點,將邊看作連線各個節點的引用(指標),我們就可以將圖看作一種從鏈結串列拓展而來的資料結構。如圖 9-1 所示,**相較於線性關係(鏈結串列)和分治關係(樹),網路關係(圖)的自由度更高**,因而更為複雜。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 9-1 鏈結串列、樹、圖之間的關係 </p>
|
||||
|
||||
## 9.1.1 圖的常見型別與術語
|
||||
|
||||
根據邊是否具有方向,可分為<u>無向圖(undirected graph)</u>和<u>有向圖(directed graph)</u>,如圖 9-2 所示。
|
||||
|
||||
- 在無向圖中,邊表示兩頂點之間的“雙向”連線關係,例如微信或 QQ 中的“好友關係”。
|
||||
- 在有向圖中,邊具有方向性,即 $A \rightarrow B$ 和 $A \leftarrow B$ 兩個方向的邊是相互獨立的,例如微博或抖音上的“關注”與“被關注”關係。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 9-2 有向圖與無向圖 </p>
|
||||
|
||||
根據所有頂點是否連通,可分為<u>連通圖(connected graph)</u>和<u>非連通圖(disconnected graph)</u>,如圖 9-3 所示。
|
||||
|
||||
- 對於連通圖,從某個頂點出發,可以到達其餘任意頂點。
|
||||
- 對於非連通圖,從某個頂點出發,至少有一個頂點無法到達。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 9-3 連通圖與非連通圖 </p>
|
||||
|
||||
我們還可以為邊新增“權重”變數,從而得到如圖 9-4 所示的<u>有權圖(weighted graph)</u>。例如在《王者榮耀》等手遊中,系統會根據共同遊戲時間來計算玩家之間的“親密度”,這種親密度網路就可以用有權圖來表示。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 9-4 有權圖與無權圖 </p>
|
||||
|
||||
圖資料結構包含以下常用術語。
|
||||
|
||||
- <u>鄰接(adjacency)</u>:當兩頂點之間存在邊相連時,稱這兩頂點“鄰接”。在圖 9-4 中,頂點 1 的鄰接頂點為頂點 2、3、5。
|
||||
- <u>路徑(path)</u>:從頂點 A 到頂點 B 經過的邊構成的序列被稱為從 A 到 B 的“路徑”。在圖 9-4 中,邊序列 1-5-2-4 是頂點 1 到頂點 4 的一條路徑。
|
||||
- <u>度(degree)</u>:一個頂點擁有的邊數。對於有向圖,<u>入度(in-degree)</u>表示有多少條邊指向該頂點,<u>出度(out-degree)</u>表示有多少條邊從該頂點指出。
|
||||
|
||||
## 9.1.2 圖的表示
|
||||
|
||||
圖的常用表示方式包括“鄰接矩陣”和“鄰接表”。以下使用無向圖進行舉例。
|
||||
|
||||
### 1. 鄰接矩陣
|
||||
|
||||
設圖的頂點數量為 $n$ ,<u>鄰接矩陣(adjacency matrix)</u>使用一個 $n \times n$ 大小的矩陣來表示圖,每一行(列)代表一個頂點,矩陣元素代表邊,用 $1$ 或 $0$ 表示兩個頂點之間是否存在邊。
|
||||
|
||||
如圖 9-5 所示,設鄰接矩陣為 $M$、頂點串列為 $V$ ,那麼矩陣元素 $M[i, j] = 1$ 表示頂點 $V[i]$ 到頂點 $V[j]$ 之間存在邊,反之 $M[i, j] = 0$ 表示兩頂點之間無邊。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 9-5 圖的鄰接矩陣表示 </p>
|
||||
|
||||
鄰接矩陣具有以下特性。
|
||||
|
||||
- 頂點不能與自身相連,因此鄰接矩陣主對角線元素沒有意義。
|
||||
- 對於無向圖,兩個方向的邊等價,此時鄰接矩陣關於主對角線對稱。
|
||||
- 將鄰接矩陣的元素從 $1$ 和 $0$ 替換為權重,則可表示有權圖。
|
||||
|
||||
使用鄰接矩陣表示圖時,我們可以直接訪問矩陣元素以獲取邊,因此增刪查改操作的效率很高,時間複雜度均為 $O(1)$ 。然而,矩陣的空間複雜度為 $O(n^2)$ ,記憶體佔用較多。
|
||||
|
||||
### 2. 鄰接表
|
||||
|
||||
<u>鄰接表(adjacency list)</u>使用 $n$ 個鏈結串列來表示圖,鏈結串列節點表示頂點。第 $i$ 個鏈結串列對應頂點 $i$ ,其中儲存了該頂點的所有鄰接頂點(與該頂點相連的頂點)。圖 9-6 展示了一個使用鄰接表儲存的圖的示例。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 9-6 圖的鄰接表表示 </p>
|
||||
|
||||
鄰接表僅儲存實際存在的邊,而邊的總數通常遠小於 $n^2$ ,因此它更加節省空間。然而,在鄰接表中需要透過走訪鏈結串列來查詢邊,因此其時間效率不如鄰接矩陣。
|
||||
|
||||
觀察圖 9-6 ,**鄰接表結構與雜湊表中的“鏈式位址”非常相似,因此我們也可以採用類似的方法來最佳化效率**。比如當鏈結串列較長時,可以將鏈結串列轉化為 AVL 樹或紅黑樹,從而將時間效率從 $O(n)$ 最佳化至 $O(\log n)$ ;還可以把鏈結串列轉換為雜湊表,從而將時間複雜度降至 $O(1)$ 。
|
||||
|
||||
## 9.1.3 圖的常見應用
|
||||
|
||||
如表 9-1 所示,許多現實系統可以用圖來建模,相應的問題也可以約化為圖計算問題。
|
||||
|
||||
<p align="center"> 表 9-1 現實生活中常見的圖 </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
| | 頂點 | 邊 | 圖計算問題 |
|
||||
| -------- | ---- | -------------------- | ------------ |
|
||||
| 社交網路 | 使用者 | 好友關係 | 潛在好友推薦 |
|
||||
| 地鐵線路 | 站點 | 站點間的連通性 | 最短路線推薦 |
|
||||
| 太陽系 | 星體 | 星體間的萬有引力作用 | 行星軌道計算 |
|
||||
|
||||
</div>
|
||||
2266
zh-Hant/docs/chapter_graph/graph_operations.md
Normal file
2266
zh-Hant/docs/chapter_graph/graph_operations.md
Normal file
File diff suppressed because one or more lines are too long
967
zh-Hant/docs/chapter_graph/graph_traversal.md
Normal file
967
zh-Hant/docs/chapter_graph/graph_traversal.md
Normal file
File diff suppressed because one or more lines are too long
21
zh-Hant/docs/chapter_graph/index.md
Normal file
21
zh-Hant/docs/chapter_graph/index.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/graphql
|
||||
---
|
||||
|
||||
# 第 9 章 圖
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
在生命旅途中,我們就像是一個個節點,被無數看不見的邊相連。
|
||||
|
||||
每一次的相識與相離,都在這張巨大的網路圖中留下獨特的印記。
|
||||
|
||||
## Chapter Contents
|
||||
|
||||
- [9.1 圖](https://www.hello-algo.com/en/chapter_graph/graph/)
|
||||
- [9.2 圖基礎操作](https://www.hello-algo.com/en/chapter_graph/graph_operations/)
|
||||
- [9.3 圖的走訪](https://www.hello-algo.com/en/chapter_graph/graph_traversal/)
|
||||
- [9.4 小結](https://www.hello-algo.com/en/chapter_graph/summary/)
|
||||
35
zh-Hant/docs/chapter_graph/summary.md
Normal file
35
zh-Hant/docs/chapter_graph/summary.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 9.4 小結
|
||||
|
||||
### 1. 重點回顧
|
||||
|
||||
- 圖由頂點和邊組成,可以表示為一組頂點和一組邊構成的集合。
|
||||
- 相較於線性關係(鏈結串列)和分治關係(樹),網路關係(圖)具有更高的自由度,因而更為複雜。
|
||||
- 有向圖的邊具有方向性,連通圖中的任意頂點均可達,有權圖的每條邊都包含權重變數。
|
||||
- 鄰接矩陣利用矩陣來表示圖,每一行(列)代表一個頂點,矩陣元素代表邊,用 $1$ 或 $0$ 表示兩個頂點之間有邊或無邊。鄰接矩陣在增刪查改操作上效率很高,但空間佔用較多。
|
||||
- 鄰接表使用多個鏈結串列來表示圖,第 $i$ 個鏈結串列對應頂點 $i$ ,其中儲存了該頂點的所有鄰接頂點。鄰接表相對於鄰接矩陣更加節省空間,但由於需要走訪鏈結串列來查詢邊,因此時間效率較低。
|
||||
- 當鄰接表中的鏈結串列過長時,可以將其轉換為紅黑樹或雜湊表,從而提升查詢效率。
|
||||
- 從演算法思想的角度分析,鄰接矩陣體現了“以空間換時間”,鄰接表體現了“以時間換空間”。
|
||||
- 圖可用於建模各類現實系統,如社交網路、地鐵線路等。
|
||||
- 樹是圖的一種特例,樹的走訪也是圖的走訪的一種特例。
|
||||
- 圖的廣度優先走訪是一種由近及遠、層層擴張的搜尋方式,通常藉助佇列實現。
|
||||
- 圖的深度優先走訪是一種優先走到底、無路可走時再回溯的搜尋方式,常基於遞迴來實現。
|
||||
|
||||
### 2. Q & A
|
||||
|
||||
**Q**:路徑的定義是頂點序列還是邊序列?
|
||||
|
||||
維基百科上不同語言版本的定義不一致:英文版是“路徑是一個邊序列”,而中文版是“路徑是一個頂點序列”。以下是英文版原文:In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices.
|
||||
|
||||
在本文中,路徑被視為一個邊序列,而不是一個頂點序列。這是因為兩個頂點之間可能存在多條邊連線,此時每條邊都對應一條路徑。
|
||||
|
||||
**Q**:非連通圖中是否會有無法走訪到的點?
|
||||
|
||||
在非連通圖中,從某個頂點出發,至少有一個頂點無法到達。走訪非連通圖需要設定多個起點,以走訪到圖的所有連通分量。
|
||||
|
||||
**Q**:在鄰接表中,“與該頂點相連的所有頂點”的頂點順序是否有要求?
|
||||
|
||||
可以是任意順序。但在實際應用中,可能需要按照指定規則來排序,比如按照頂點新增的次序,或者按照頂點值大小的順序等,這樣有助於快速查詢“帶有某種極值”的頂點。
|
||||
568
zh-Hant/docs/chapter_greedy/fractional_knapsack_problem.md
Normal file
568
zh-Hant/docs/chapter_greedy/fractional_knapsack_problem.md
Normal file
@@ -0,0 +1,568 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 15.2 分數背包問題
|
||||
|
||||
!!! question
|
||||
|
||||
給定 $n$ 個物品,第 $i$ 個物品的重量為 $wgt[i-1]$、價值為 $val[i-1]$ ,和一個容量為 $cap$ 的背包。每個物品只能選擇一次,**但可以選擇物品的一部分,價值根據選擇的重量比例計算**,問在限定背包容量下背包中物品的最大價值。示例如圖 15-3 所示。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-3 分數背包問題的示例資料 </p>
|
||||
|
||||
分數背包問題和 0-1 背包問題整體上非常相似,狀態包含當前物品 $i$ 和容量 $c$ ,目標是求限定背包容量下的最大價值。
|
||||
|
||||
不同點在於,本題允許只選擇物品的一部分。如圖 15-4 所示,**我們可以對物品任意地進行切分,並按照重量比例來計算相應價值**。
|
||||
|
||||
1. 對於物品 $i$ ,它在單位重量下的價值為 $val[i-1] / wgt[i-1]$ ,簡稱單位價值。
|
||||
2. 假設放入一部分物品 $i$ ,重量為 $w$ ,則背包增加的價值為 $w \times val[i-1] / wgt[i-1]$ 。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-4 物品在單位重量下的價值 </p>
|
||||
|
||||
### 1. 貪婪策略確定
|
||||
|
||||
最大化背包內物品總價值,**本質上是最大化單位重量下的物品價值**。由此便可推理出圖 15-5 所示的貪婪策略。
|
||||
|
||||
1. 將物品按照單位價值從高到低進行排序。
|
||||
2. 走訪所有物品,**每輪貪婪地選擇單位價值最高的物品**。
|
||||
3. 若剩餘背包容量不足,則使用當前物品的一部分填滿背包。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-5 分數背包問題的貪婪策略 </p>
|
||||
|
||||
### 2. 程式碼實現
|
||||
|
||||
我們建立了一個物品類別 `Item` ,以便將物品按照單位價值進行排序。迴圈進行貪婪選擇,當背包已滿時跳出並返回解:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="fractional_knapsack.py"
|
||||
class Item:
|
||||
"""物品"""
|
||||
|
||||
def __init__(self, w: int, v: int):
|
||||
self.w = w # 物品重量
|
||||
self.v = v # 物品價值
|
||||
|
||||
def fractional_knapsack(wgt: list[int], val: list[int], cap: int) -> int:
|
||||
"""分數背包:貪婪"""
|
||||
# 建立物品串列,包含兩個屬性:重量、價值
|
||||
items = [Item(w, v) for w, v in zip(wgt, val)]
|
||||
# 按照單位價值 item.v / item.w 從高到低進行排序
|
||||
items.sort(key=lambda item: item.v / item.w, reverse=True)
|
||||
# 迴圈貪婪選擇
|
||||
res = 0
|
||||
for item in items:
|
||||
if item.w <= cap:
|
||||
# 若剩餘容量充足,則將當前物品整個裝進背包
|
||||
res += item.v
|
||||
cap -= item.w
|
||||
else:
|
||||
# 若剩餘容量不足,則將當前物品的一部分裝進背包
|
||||
res += (item.v / item.w) * cap
|
||||
# 已無剩餘容量,因此跳出迴圈
|
||||
break
|
||||
return res
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="fractional_knapsack.cpp"
|
||||
/* 物品 */
|
||||
class Item {
|
||||
public:
|
||||
int w; // 物品重量
|
||||
int v; // 物品價值
|
||||
|
||||
Item(int w, int v) : w(w), v(v) {
|
||||
}
|
||||
};
|
||||
|
||||
/* 分數背包:貪婪 */
|
||||
double fractionalKnapsack(vector<int> &wgt, vector<int> &val, int cap) {
|
||||
// 建立物品串列,包含兩個屬性:重量、價值
|
||||
vector<Item> items;
|
||||
for (int i = 0; i < wgt.size(); i++) {
|
||||
items.push_back(Item(wgt[i], val[i]));
|
||||
}
|
||||
// 按照單位價值 item.v / item.w 從高到低進行排序
|
||||
sort(items.begin(), items.end(), [](Item &a, Item &b) { return (double)a.v / a.w > (double)b.v / b.w; });
|
||||
// 迴圈貪婪選擇
|
||||
double res = 0;
|
||||
for (auto &item : items) {
|
||||
if (item.w <= cap) {
|
||||
// 若剩餘容量充足,則將當前物品整個裝進背包
|
||||
res += item.v;
|
||||
cap -= item.w;
|
||||
} else {
|
||||
// 若剩餘容量不足,則將當前物品的一部分裝進背包
|
||||
res += (double)item.v / item.w * cap;
|
||||
// 已無剩餘容量,因此跳出迴圈
|
||||
break;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="fractional_knapsack.java"
|
||||
/* 物品 */
|
||||
class Item {
|
||||
int w; // 物品重量
|
||||
int v; // 物品價值
|
||||
|
||||
public Item(int w, int v) {
|
||||
this.w = w;
|
||||
this.v = v;
|
||||
}
|
||||
}
|
||||
|
||||
/* 分數背包:貪婪 */
|
||||
double fractionalKnapsack(int[] wgt, int[] val, int cap) {
|
||||
// 建立物品串列,包含兩個屬性:重量、價值
|
||||
Item[] items = new Item[wgt.length];
|
||||
for (int i = 0; i < wgt.length; i++) {
|
||||
items[i] = new Item(wgt[i], val[i]);
|
||||
}
|
||||
// 按照單位價值 item.v / item.w 從高到低進行排序
|
||||
Arrays.sort(items, Comparator.comparingDouble(item -> -((double) item.v / item.w)));
|
||||
// 迴圈貪婪選擇
|
||||
double res = 0;
|
||||
for (Item item : items) {
|
||||
if (item.w <= cap) {
|
||||
// 若剩餘容量充足,則將當前物品整個裝進背包
|
||||
res += item.v;
|
||||
cap -= item.w;
|
||||
} else {
|
||||
// 若剩餘容量不足,則將當前物品的一部分裝進背包
|
||||
res += (double) item.v / item.w * cap;
|
||||
// 已無剩餘容量,因此跳出迴圈
|
||||
break;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="fractional_knapsack.cs"
|
||||
/* 物品 */
|
||||
class Item(int w, int v) {
|
||||
public int w = w; // 物品重量
|
||||
public int v = v; // 物品價值
|
||||
}
|
||||
|
||||
/* 分數背包:貪婪 */
|
||||
double FractionalKnapsack(int[] wgt, int[] val, int cap) {
|
||||
// 建立物品串列,包含兩個屬性:重量、價值
|
||||
Item[] items = new Item[wgt.Length];
|
||||
for (int i = 0; i < wgt.Length; i++) {
|
||||
items[i] = new Item(wgt[i], val[i]);
|
||||
}
|
||||
// 按照單位價值 item.v / item.w 從高到低進行排序
|
||||
Array.Sort(items, (x, y) => (y.v / y.w).CompareTo(x.v / x.w));
|
||||
// 迴圈貪婪選擇
|
||||
double res = 0;
|
||||
foreach (Item item in items) {
|
||||
if (item.w <= cap) {
|
||||
// 若剩餘容量充足,則將當前物品整個裝進背包
|
||||
res += item.v;
|
||||
cap -= item.w;
|
||||
} else {
|
||||
// 若剩餘容量不足,則將當前物品的一部分裝進背包
|
||||
res += (double)item.v / item.w * cap;
|
||||
// 已無剩餘容量,因此跳出迴圈
|
||||
break;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="fractional_knapsack.go"
|
||||
/* 物品 */
|
||||
type Item struct {
|
||||
w int // 物品重量
|
||||
v int // 物品價值
|
||||
}
|
||||
|
||||
/* 分數背包:貪婪 */
|
||||
func fractionalKnapsack(wgt []int, val []int, cap int) float64 {
|
||||
// 建立物品串列,包含兩個屬性:重量、價值
|
||||
items := make([]Item, len(wgt))
|
||||
for i := 0; i < len(wgt); i++ {
|
||||
items[i] = Item{wgt[i], val[i]}
|
||||
}
|
||||
// 按照單位價值 item.v / item.w 從高到低進行排序
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
return float64(items[i].v)/float64(items[i].w) > float64(items[j].v)/float64(items[j].w)
|
||||
})
|
||||
// 迴圈貪婪選擇
|
||||
res := 0.0
|
||||
for _, item := range items {
|
||||
if item.w <= cap {
|
||||
// 若剩餘容量充足,則將當前物品整個裝進背包
|
||||
res += float64(item.v)
|
||||
cap -= item.w
|
||||
} else {
|
||||
// 若剩餘容量不足,則將當前物品的一部分裝進背包
|
||||
res += float64(item.v) / float64(item.w) * float64(cap)
|
||||
// 已無剩餘容量,因此跳出迴圈
|
||||
break
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="fractional_knapsack.swift"
|
||||
/* 物品 */
|
||||
class Item {
|
||||
var w: Int // 物品重量
|
||||
var v: Int // 物品價值
|
||||
|
||||
init(w: Int, v: Int) {
|
||||
self.w = w
|
||||
self.v = v
|
||||
}
|
||||
}
|
||||
|
||||
/* 分數背包:貪婪 */
|
||||
func fractionalKnapsack(wgt: [Int], val: [Int], cap: Int) -> Double {
|
||||
// 建立物品串列,包含兩個屬性:重量、價值
|
||||
var items = zip(wgt, val).map { Item(w: $0, v: $1) }
|
||||
// 按照單位價值 item.v / item.w 從高到低進行排序
|
||||
items.sort { -(Double($0.v) / Double($0.w)) < -(Double($1.v) / Double($1.w)) }
|
||||
// 迴圈貪婪選擇
|
||||
var res = 0.0
|
||||
var cap = cap
|
||||
for item in items {
|
||||
if item.w <= cap {
|
||||
// 若剩餘容量充足,則將當前物品整個裝進背包
|
||||
res += Double(item.v)
|
||||
cap -= item.w
|
||||
} else {
|
||||
// 若剩餘容量不足,則將當前物品的一部分裝進背包
|
||||
res += Double(item.v) / Double(item.w) * Double(cap)
|
||||
// 已無剩餘容量,因此跳出迴圈
|
||||
break
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="fractional_knapsack.js"
|
||||
/* 物品 */
|
||||
class Item {
|
||||
constructor(w, v) {
|
||||
this.w = w; // 物品重量
|
||||
this.v = v; // 物品價值
|
||||
}
|
||||
}
|
||||
|
||||
/* 分數背包:貪婪 */
|
||||
function fractionalKnapsack(wgt, val, cap) {
|
||||
// 建立物品串列,包含兩個屬性:重量、價值
|
||||
const items = wgt.map((w, i) => new Item(w, val[i]));
|
||||
// 按照單位價值 item.v / item.w 從高到低進行排序
|
||||
items.sort((a, b) => b.v / b.w - a.v / a.w);
|
||||
// 迴圈貪婪選擇
|
||||
let res = 0;
|
||||
for (const item of items) {
|
||||
if (item.w <= cap) {
|
||||
// 若剩餘容量充足,則將當前物品整個裝進背包
|
||||
res += item.v;
|
||||
cap -= item.w;
|
||||
} else {
|
||||
// 若剩餘容量不足,則將當前物品的一部分裝進背包
|
||||
res += (item.v / item.w) * cap;
|
||||
// 已無剩餘容量,因此跳出迴圈
|
||||
break;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="fractional_knapsack.ts"
|
||||
/* 物品 */
|
||||
class Item {
|
||||
w: number; // 物品重量
|
||||
v: number; // 物品價值
|
||||
|
||||
constructor(w: number, v: number) {
|
||||
this.w = w;
|
||||
this.v = v;
|
||||
}
|
||||
}
|
||||
|
||||
/* 分數背包:貪婪 */
|
||||
function fractionalKnapsack(wgt: number[], val: number[], cap: number): number {
|
||||
// 建立物品串列,包含兩個屬性:重量、價值
|
||||
const items: Item[] = wgt.map((w, i) => new Item(w, val[i]));
|
||||
// 按照單位價值 item.v / item.w 從高到低進行排序
|
||||
items.sort((a, b) => b.v / b.w - a.v / a.w);
|
||||
// 迴圈貪婪選擇
|
||||
let res = 0;
|
||||
for (const item of items) {
|
||||
if (item.w <= cap) {
|
||||
// 若剩餘容量充足,則將當前物品整個裝進背包
|
||||
res += item.v;
|
||||
cap -= item.w;
|
||||
} else {
|
||||
// 若剩餘容量不足,則將當前物品的一部分裝進背包
|
||||
res += (item.v / item.w) * cap;
|
||||
// 已無剩餘容量,因此跳出迴圈
|
||||
break;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="fractional_knapsack.dart"
|
||||
/* 物品 */
|
||||
class Item {
|
||||
int w; // 物品重量
|
||||
int v; // 物品價值
|
||||
|
||||
Item(this.w, this.v);
|
||||
}
|
||||
|
||||
/* 分數背包:貪婪 */
|
||||
double fractionalKnapsack(List<int> wgt, List<int> val, int cap) {
|
||||
// 建立物品串列,包含兩個屬性:重量、價值
|
||||
List<Item> items = List.generate(wgt.length, (i) => Item(wgt[i], val[i]));
|
||||
// 按照單位價值 item.v / item.w 從高到低進行排序
|
||||
items.sort((a, b) => (b.v / b.w).compareTo(a.v / a.w));
|
||||
// 迴圈貪婪選擇
|
||||
double res = 0;
|
||||
for (Item item in items) {
|
||||
if (item.w <= cap) {
|
||||
// 若剩餘容量充足,則將當前物品整個裝進背包
|
||||
res += item.v;
|
||||
cap -= item.w;
|
||||
} else {
|
||||
// 若剩餘容量不足,則將當前物品的一部分裝進背包
|
||||
res += item.v / item.w * cap;
|
||||
// 已無剩餘容量,因此跳出迴圈
|
||||
break;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="fractional_knapsack.rs"
|
||||
/* 物品 */
|
||||
struct Item {
|
||||
w: i32, // 物品重量
|
||||
v: i32, // 物品價值
|
||||
}
|
||||
|
||||
impl Item {
|
||||
fn new(w: i32, v: i32) -> Self {
|
||||
Self { w, v }
|
||||
}
|
||||
}
|
||||
|
||||
/* 分數背包:貪婪 */
|
||||
fn fractional_knapsack(wgt: &[i32], val: &[i32], mut cap: i32) -> f64 {
|
||||
// 建立物品串列,包含兩個屬性:重量、價值
|
||||
let mut items = wgt
|
||||
.iter()
|
||||
.zip(val.iter())
|
||||
.map(|(&w, &v)| Item::new(w, v))
|
||||
.collect::<Vec<Item>>();
|
||||
// 按照單位價值 item.v / item.w 從高到低進行排序
|
||||
items.sort_by(|a, b| {
|
||||
(b.v as f64 / b.w as f64)
|
||||
.partial_cmp(&(a.v as f64 / a.w as f64))
|
||||
.unwrap()
|
||||
});
|
||||
// 迴圈貪婪選擇
|
||||
let mut res = 0.0;
|
||||
for item in &items {
|
||||
if item.w <= cap {
|
||||
// 若剩餘容量充足,則將當前物品整個裝進背包
|
||||
res += item.v as f64;
|
||||
cap -= item.w;
|
||||
} else {
|
||||
// 若剩餘容量不足,則將當前物品的一部分裝進背包
|
||||
res += item.v as f64 / item.w as f64 * cap as f64;
|
||||
// 已無剩餘容量,因此跳出迴圈
|
||||
break;
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="fractional_knapsack.c"
|
||||
/* 物品 */
|
||||
typedef struct {
|
||||
int w; // 物品重量
|
||||
int v; // 物品價值
|
||||
} Item;
|
||||
|
||||
/* 分數背包:貪婪 */
|
||||
float fractionalKnapsack(int wgt[], int val[], int itemCount, int cap) {
|
||||
// 建立物品串列,包含兩個屬性:重量、價值
|
||||
Item *items = malloc(sizeof(Item) * itemCount);
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
items[i] = (Item){.w = wgt[i], .v = val[i]};
|
||||
}
|
||||
// 按照單位價值 item.v / item.w 從高到低進行排序
|
||||
qsort(items, (size_t)itemCount, sizeof(Item), sortByValueDensity);
|
||||
// 迴圈貪婪選擇
|
||||
float res = 0.0;
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
if (items[i].w <= cap) {
|
||||
// 若剩餘容量充足,則將當前物品整個裝進背包
|
||||
res += items[i].v;
|
||||
cap -= items[i].w;
|
||||
} else {
|
||||
// 若剩餘容量不足,則將當前物品的一部分裝進背包
|
||||
res += (float)cap / items[i].w * items[i].v;
|
||||
cap = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
free(items);
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="fractional_knapsack.kt"
|
||||
/* 物品 */
|
||||
class Item(
|
||||
val w: Int, // 物品
|
||||
val v: Int // 物品價值
|
||||
)
|
||||
|
||||
/* 分數背包:貪婪 */
|
||||
fun fractionalKnapsack(
|
||||
wgt: IntArray,
|
||||
value: IntArray,
|
||||
c: Int
|
||||
): Double {
|
||||
// 建立物品串列,包含兩個屬性:重量、價值
|
||||
var cap = c
|
||||
val items = arrayOfNulls<Item>(wgt.size)
|
||||
for (i in wgt.indices) {
|
||||
items[i] = Item(wgt[i], value[i])
|
||||
}
|
||||
// 按照單位價值 item.v / item.w 從高到低進行排序
|
||||
Arrays.sort(items, Comparator.comparingDouble { item: Item -> -(item.v.toDouble() / item.w) })
|
||||
// 迴圈貪婪選擇
|
||||
var res = 0.0
|
||||
for (item in items) {
|
||||
if (item!!.w <= cap) {
|
||||
// 若剩餘容量充足,則將當前物品整個裝進背包
|
||||
res += item.v.toDouble()
|
||||
cap -= item.w
|
||||
} else {
|
||||
// 若剩餘容量不足,則將當前物品的一部分裝進背包
|
||||
res += item.v.toDouble() / item.w * cap
|
||||
// 已無剩餘容量,因此跳出迴圈
|
||||
break
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
/* 分數背包:貪婪 */
|
||||
fun fractionalKnapsack(
|
||||
wgt: IntArray,
|
||||
value: IntArray,
|
||||
c: Int
|
||||
): Double {
|
||||
// 建立物品串列,包含兩個屬性:重量、價值
|
||||
var cap = c
|
||||
val items = arrayOfNulls<Item>(wgt.size)
|
||||
for (i in wgt.indices) {
|
||||
items[i] = Item(wgt[i], value[i])
|
||||
}
|
||||
// 按照單位價值 item.v / item.w 從高到低進行排序
|
||||
Arrays.sort(items, Comparator.comparingDouble { item: Item -> -(item.v.toDouble() / item.w) })
|
||||
// 迴圈貪婪選擇
|
||||
var res = 0.0
|
||||
for (item in items) {
|
||||
if (item!!.w <= cap) {
|
||||
// 若剩餘容量充足,則將當前物品整個裝進背包
|
||||
res += item.v.toDouble()
|
||||
cap -= item.w
|
||||
} else {
|
||||
// 若剩餘容量不足,則將當前物品的一部分裝進背包
|
||||
res += item.v.toDouble() / item.w * cap
|
||||
// 已無剩餘容量,因此跳出迴圈
|
||||
break
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="fractional_knapsack.rb"
|
||||
[class]{Item}-[func]{}
|
||||
|
||||
[class]{}-[func]{fractional_knapsack}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="fractional_knapsack.zig"
|
||||
[class]{Item}-[func]{}
|
||||
|
||||
[class]{}-[func]{fractionalKnapsack}
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20Item%3A%0A%20%20%20%20%22%22%22%E7%89%A9%E5%93%81%22%22%22%0A%20%20%20%20def%20__init__%28self,%20w%3A%20int,%20v%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.w%20%3D%20w%20%20%23%20%E7%89%A9%E5%93%81%E9%87%8D%E9%87%8F%0A%20%20%20%20%20%20%20%20self.v%20%3D%20v%20%20%23%20%E7%89%A9%E5%93%81%E4%BB%B7%E5%80%BC%0A%0Adef%20fractional_knapsack%28wgt%3A%20list%5Bint%5D,%20val%3A%20list%5Bint%5D,%20cap%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%88%86%E6%95%B0%E8%83%8C%E5%8C%85%EF%BC%9A%E8%B4%AA%E5%BF%83%22%22%22%0A%20%20%20%20%23%20%E5%88%9B%E5%BB%BA%E7%89%A9%E5%93%81%E5%88%97%E8%A1%A8%EF%BC%8C%E5%8C%85%E5%90%AB%E4%B8%A4%E4%B8%AA%E5%B1%9E%E6%80%A7%EF%BC%9A%E9%87%8D%E9%87%8F%E3%80%81%E4%BB%B7%E5%80%BC%0A%20%20%20%20items%20%3D%20%5BItem%28w,%20v%29%20for%20w,%20v%20in%20zip%28wgt,%20val%29%5D%0A%20%20%20%20%23%20%E6%8C%89%E7%85%A7%E5%8D%95%E4%BD%8D%E4%BB%B7%E5%80%BC%20item.v%20/%20item.w%20%E4%BB%8E%E9%AB%98%E5%88%B0%E4%BD%8E%E8%BF%9B%E8%A1%8C%E6%8E%92%E5%BA%8F%0A%20%20%20%20items.sort%28key%3Dlambda%20item%3A%20item.v%20/%20item.w,%20reverse%3DTrue%29%0A%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%E8%B4%AA%E5%BF%83%E9%80%89%E6%8B%A9%0A%20%20%20%20res%20%3D%200%0A%20%20%20%20for%20item%20in%20items%3A%0A%20%20%20%20%20%20%20%20if%20item.w%20%3C%3D%20cap%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E5%89%A9%E4%BD%99%E5%AE%B9%E9%87%8F%E5%85%85%E8%B6%B3%EF%BC%8C%E5%88%99%E5%B0%86%E5%BD%93%E5%89%8D%E7%89%A9%E5%93%81%E6%95%B4%E4%B8%AA%E8%A3%85%E8%BF%9B%E8%83%8C%E5%8C%85%0A%20%20%20%20%20%20%20%20%20%20%20%20res%20%2B%3D%20item.v%0A%20%20%20%20%20%20%20%20%20%20%20%20cap%20-%3D%20item.w%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E5%89%A9%E4%BD%99%E5%AE%B9%E9%87%8F%E4%B8%8D%E8%B6%B3%EF%BC%8C%E5%88%99%E5%B0%86%E5%BD%93%E5%89%8D%E7%89%A9%E5%93%81%E7%9A%84%E4%B8%80%E9%83%A8%E5%88%86%E8%A3%85%E8%BF%9B%E8%83%8C%E5%8C%85%0A%20%20%20%20%20%20%20%20%20%20%20%20res%20%2B%3D%20%28item.v%20/%20item.w%29%20*%20cap%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E5%B7%B2%E6%97%A0%E5%89%A9%E4%BD%99%E5%AE%B9%E9%87%8F%EF%BC%8C%E5%9B%A0%E6%AD%A4%E8%B7%B3%E5%87%BA%E5%BE%AA%E7%8E%AF%0A%20%20%20%20%20%20%20%20%20%20%20%20break%0A%20%20%20%20return%20res%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20wgt%20%3D%20%5B10,%2020,%2030,%2040,%2050%5D%0A%20%20%20%20val%20%3D%20%5B50,%20120,%20150,%20210,%20240%5D%0A%20%20%20%20cap%20%3D%2050%0A%20%20%20%20n%20%3D%20len%28wgt%29%0A%0A%20%20%20%20%23%20%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95%0A%20%20%20%20res%20%3D%20fractional_knapsack%28wgt,%20val,%20cap%29%0A%20%20%20%20print%28f%22%E4%B8%8D%E8%B6%85%E8%BF%87%E8%83%8C%E5%8C%85%E5%AE%B9%E9%87%8F%E7%9A%84%E6%9C%80%E5%A4%A7%E7%89%A9%E5%93%81%E4%BB%B7%E5%80%BC%E4%B8%BA%20%7Bres%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=8&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20Item%3A%0A%20%20%20%20%22%22%22%E7%89%A9%E5%93%81%22%22%22%0A%20%20%20%20def%20__init__%28self,%20w%3A%20int,%20v%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.w%20%3D%20w%20%20%23%20%E7%89%A9%E5%93%81%E9%87%8D%E9%87%8F%0A%20%20%20%20%20%20%20%20self.v%20%3D%20v%20%20%23%20%E7%89%A9%E5%93%81%E4%BB%B7%E5%80%BC%0A%0Adef%20fractional_knapsack%28wgt%3A%20list%5Bint%5D,%20val%3A%20list%5Bint%5D,%20cap%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%88%86%E6%95%B0%E8%83%8C%E5%8C%85%EF%BC%9A%E8%B4%AA%E5%BF%83%22%22%22%0A%20%20%20%20%23%20%E5%88%9B%E5%BB%BA%E7%89%A9%E5%93%81%E5%88%97%E8%A1%A8%EF%BC%8C%E5%8C%85%E5%90%AB%E4%B8%A4%E4%B8%AA%E5%B1%9E%E6%80%A7%EF%BC%9A%E9%87%8D%E9%87%8F%E3%80%81%E4%BB%B7%E5%80%BC%0A%20%20%20%20items%20%3D%20%5BItem%28w,%20v%29%20for%20w,%20v%20in%20zip%28wgt,%20val%29%5D%0A%20%20%20%20%23%20%E6%8C%89%E7%85%A7%E5%8D%95%E4%BD%8D%E4%BB%B7%E5%80%BC%20item.v%20/%20item.w%20%E4%BB%8E%E9%AB%98%E5%88%B0%E4%BD%8E%E8%BF%9B%E8%A1%8C%E6%8E%92%E5%BA%8F%0A%20%20%20%20items.sort%28key%3Dlambda%20item%3A%20item.v%20/%20item.w,%20reverse%3DTrue%29%0A%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%E8%B4%AA%E5%BF%83%E9%80%89%E6%8B%A9%0A%20%20%20%20res%20%3D%200%0A%20%20%20%20for%20item%20in%20items%3A%0A%20%20%20%20%20%20%20%20if%20item.w%20%3C%3D%20cap%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E5%89%A9%E4%BD%99%E5%AE%B9%E9%87%8F%E5%85%85%E8%B6%B3%EF%BC%8C%E5%88%99%E5%B0%86%E5%BD%93%E5%89%8D%E7%89%A9%E5%93%81%E6%95%B4%E4%B8%AA%E8%A3%85%E8%BF%9B%E8%83%8C%E5%8C%85%0A%20%20%20%20%20%20%20%20%20%20%20%20res%20%2B%3D%20item.v%0A%20%20%20%20%20%20%20%20%20%20%20%20cap%20-%3D%20item.w%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E5%89%A9%E4%BD%99%E5%AE%B9%E9%87%8F%E4%B8%8D%E8%B6%B3%EF%BC%8C%E5%88%99%E5%B0%86%E5%BD%93%E5%89%8D%E7%89%A9%E5%93%81%E7%9A%84%E4%B8%80%E9%83%A8%E5%88%86%E8%A3%85%E8%BF%9B%E8%83%8C%E5%8C%85%0A%20%20%20%20%20%20%20%20%20%20%20%20res%20%2B%3D%20%28item.v%20/%20item.w%29%20*%20cap%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E5%B7%B2%E6%97%A0%E5%89%A9%E4%BD%99%E5%AE%B9%E9%87%8F%EF%BC%8C%E5%9B%A0%E6%AD%A4%E8%B7%B3%E5%87%BA%E5%BE%AA%E7%8E%AF%0A%20%20%20%20%20%20%20%20%20%20%20%20break%0A%20%20%20%20return%20res%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20wgt%20%3D%20%5B10,%2020,%2030,%2040,%2050%5D%0A%20%20%20%20val%20%3D%20%5B50,%20120,%20150,%20210,%20240%5D%0A%20%20%20%20cap%20%3D%2050%0A%20%20%20%20n%20%3D%20len%28wgt%29%0A%0A%20%20%20%20%23%20%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95%0A%20%20%20%20res%20%3D%20fractional_knapsack%28wgt,%20val,%20cap%29%0A%20%20%20%20print%28f%22%E4%B8%8D%E8%B6%85%E8%BF%87%E8%83%8C%E5%8C%85%E5%AE%B9%E9%87%8F%E7%9A%84%E6%9C%80%E5%A4%A7%E7%89%A9%E5%93%81%E4%BB%B7%E5%80%BC%E4%B8%BA%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=8&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
除排序之外,在最差情況下,需要走訪整個物品串列,**因此時間複雜度為 $O(n)$** ,其中 $n$ 為物品數量。
|
||||
|
||||
由於初始化了一個 `Item` 物件串列,**因此空間複雜度為 $O(n)$** 。
|
||||
|
||||
### 3. 正確性證明
|
||||
|
||||
採用反證法。假設物品 $x$ 是單位價值最高的物品,使用某演算法求得最大價值為 `res` ,但該解中不包含物品 $x$ 。
|
||||
|
||||
現在從背包中拿出單位重量的任意物品,並替換為單位重量的物品 $x$ 。由於物品 $x$ 的單位價值最高,因此替換後的總價值一定大於 `res` 。**這與 `res` 是最優解矛盾,說明最優解中必須包含物品 $x$** 。
|
||||
|
||||
對於該解中的其他物品,我們也可以構建出上述矛盾。總而言之,**單位價值更大的物品總是更優選擇**,這說明貪婪策略是有效的。
|
||||
|
||||
如圖 15-6 所示,如果將物品重量和物品單位價值分別看作一張二維圖表的橫軸和縱軸,則分數背包問題可轉化為“求在有限橫軸區間下圍成的最大面積”。這個類比可以幫助我們從幾何角度理解貪婪策略的有效性。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-6 分數背包問題的幾何表示 </p>
|
||||
397
zh-Hant/docs/chapter_greedy/greedy_algorithm.md
Normal file
397
zh-Hant/docs/chapter_greedy/greedy_algorithm.md
Normal file
@@ -0,0 +1,397 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 15.1 貪婪演算法
|
||||
|
||||
<u>貪婪演算法(greedy algorithm)</u>是一種常見的解決最佳化問題的演算法,其基本思想是在問題的每個決策階段,都選擇當前看起來最優的選擇,即貪婪地做出區域性最優的決策,以期獲得全域性最優解。貪婪演算法簡潔且高效,在許多實際問題中有著廣泛的應用。
|
||||
|
||||
貪婪演算法和動態規劃都常用於解決最佳化問題。它們之間存在一些相似之處,比如都依賴最優子結構性質,但工作原理不同。
|
||||
|
||||
- 動態規劃會根據之前階段的所有決策來考慮當前決策,並使用過去子問題的解來構建當前子問題的解。
|
||||
- 貪婪演算法不會考慮過去的決策,而是一路向前地進行貪婪選擇,不斷縮小問題範圍,直至問題被解決。
|
||||
|
||||
我們先透過例題“零錢兌換”瞭解貪婪演算法的工作原理。這道題已經在“完全背包問題”章節中介紹過,相信你對它並不陌生。
|
||||
|
||||
!!! question
|
||||
|
||||
給定 $n$ 種硬幣,第 $i$ 種硬幣的面值為 $coins[i - 1]$ ,目標金額為 $amt$ ,每種硬幣可以重複選取,問能夠湊出目標金額的最少硬幣數量。如果無法湊出目標金額,則返回 $-1$ 。
|
||||
|
||||
本題採取的貪婪策略如圖 15-1 所示。給定目標金額,**我們貪婪地選擇不大於且最接近它的硬幣**,不斷迴圈該步驟,直至湊出目標金額為止。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-1 零錢兌換的貪婪策略 </p>
|
||||
|
||||
實現程式碼如下所示:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="coin_change_greedy.py"
|
||||
def coin_change_greedy(coins: list[int], amt: int) -> int:
|
||||
"""零錢兌換:貪婪"""
|
||||
# 假設 coins 串列有序
|
||||
i = len(coins) - 1
|
||||
count = 0
|
||||
# 迴圈進行貪婪選擇,直到無剩餘金額
|
||||
while amt > 0:
|
||||
# 找到小於且最接近剩餘金額的硬幣
|
||||
while i > 0 and coins[i] > amt:
|
||||
i -= 1
|
||||
# 選擇 coins[i]
|
||||
amt -= coins[i]
|
||||
count += 1
|
||||
# 若未找到可行方案,則返回 -1
|
||||
return count if amt == 0 else -1
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="coin_change_greedy.cpp"
|
||||
/* 零錢兌換:貪婪 */
|
||||
int coinChangeGreedy(vector<int> &coins, int amt) {
|
||||
// 假設 coins 串列有序
|
||||
int i = coins.size() - 1;
|
||||
int count = 0;
|
||||
// 迴圈進行貪婪選擇,直到無剩餘金額
|
||||
while (amt > 0) {
|
||||
// 找到小於且最接近剩餘金額的硬幣
|
||||
while (i > 0 && coins[i] > amt) {
|
||||
i--;
|
||||
}
|
||||
// 選擇 coins[i]
|
||||
amt -= coins[i];
|
||||
count++;
|
||||
}
|
||||
// 若未找到可行方案,則返回 -1
|
||||
return amt == 0 ? count : -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="coin_change_greedy.java"
|
||||
/* 零錢兌換:貪婪 */
|
||||
int coinChangeGreedy(int[] coins, int amt) {
|
||||
// 假設 coins 串列有序
|
||||
int i = coins.length - 1;
|
||||
int count = 0;
|
||||
// 迴圈進行貪婪選擇,直到無剩餘金額
|
||||
while (amt > 0) {
|
||||
// 找到小於且最接近剩餘金額的硬幣
|
||||
while (i > 0 && coins[i] > amt) {
|
||||
i--;
|
||||
}
|
||||
// 選擇 coins[i]
|
||||
amt -= coins[i];
|
||||
count++;
|
||||
}
|
||||
// 若未找到可行方案,則返回 -1
|
||||
return amt == 0 ? count : -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="coin_change_greedy.cs"
|
||||
/* 零錢兌換:貪婪 */
|
||||
int CoinChangeGreedy(int[] coins, int amt) {
|
||||
// 假設 coins 串列有序
|
||||
int i = coins.Length - 1;
|
||||
int count = 0;
|
||||
// 迴圈進行貪婪選擇,直到無剩餘金額
|
||||
while (amt > 0) {
|
||||
// 找到小於且最接近剩餘金額的硬幣
|
||||
while (i > 0 && coins[i] > amt) {
|
||||
i--;
|
||||
}
|
||||
// 選擇 coins[i]
|
||||
amt -= coins[i];
|
||||
count++;
|
||||
}
|
||||
// 若未找到可行方案,則返回 -1
|
||||
return amt == 0 ? count : -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="coin_change_greedy.go"
|
||||
/* 零錢兌換:貪婪 */
|
||||
func coinChangeGreedy(coins []int, amt int) int {
|
||||
// 假設 coins 串列有序
|
||||
i := len(coins) - 1
|
||||
count := 0
|
||||
// 迴圈進行貪婪選擇,直到無剩餘金額
|
||||
for amt > 0 {
|
||||
// 找到小於且最接近剩餘金額的硬幣
|
||||
for i > 0 && coins[i] > amt {
|
||||
i--
|
||||
}
|
||||
// 選擇 coins[i]
|
||||
amt -= coins[i]
|
||||
count++
|
||||
}
|
||||
// 若未找到可行方案,則返回 -1
|
||||
if amt != 0 {
|
||||
return -1
|
||||
}
|
||||
return count
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="coin_change_greedy.swift"
|
||||
/* 零錢兌換:貪婪 */
|
||||
func coinChangeGreedy(coins: [Int], amt: Int) -> Int {
|
||||
// 假設 coins 串列有序
|
||||
var i = coins.count - 1
|
||||
var count = 0
|
||||
var amt = amt
|
||||
// 迴圈進行貪婪選擇,直到無剩餘金額
|
||||
while amt > 0 {
|
||||
// 找到小於且最接近剩餘金額的硬幣
|
||||
while i > 0 && coins[i] > amt {
|
||||
i -= 1
|
||||
}
|
||||
// 選擇 coins[i]
|
||||
amt -= coins[i]
|
||||
count += 1
|
||||
}
|
||||
// 若未找到可行方案,則返回 -1
|
||||
return amt == 0 ? count : -1
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="coin_change_greedy.js"
|
||||
/* 零錢兌換:貪婪 */
|
||||
function coinChangeGreedy(coins, amt) {
|
||||
// 假設 coins 陣列有序
|
||||
let i = coins.length - 1;
|
||||
let count = 0;
|
||||
// 迴圈進行貪婪選擇,直到無剩餘金額
|
||||
while (amt > 0) {
|
||||
// 找到小於且最接近剩餘金額的硬幣
|
||||
while (i > 0 && coins[i] > amt) {
|
||||
i--;
|
||||
}
|
||||
// 選擇 coins[i]
|
||||
amt -= coins[i];
|
||||
count++;
|
||||
}
|
||||
// 若未找到可行方案,則返回 -1
|
||||
return amt === 0 ? count : -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="coin_change_greedy.ts"
|
||||
/* 零錢兌換:貪婪 */
|
||||
function coinChangeGreedy(coins: number[], amt: number): number {
|
||||
// 假設 coins 陣列有序
|
||||
let i = coins.length - 1;
|
||||
let count = 0;
|
||||
// 迴圈進行貪婪選擇,直到無剩餘金額
|
||||
while (amt > 0) {
|
||||
// 找到小於且最接近剩餘金額的硬幣
|
||||
while (i > 0 && coins[i] > amt) {
|
||||
i--;
|
||||
}
|
||||
// 選擇 coins[i]
|
||||
amt -= coins[i];
|
||||
count++;
|
||||
}
|
||||
// 若未找到可行方案,則返回 -1
|
||||
return amt === 0 ? count : -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="coin_change_greedy.dart"
|
||||
/* 零錢兌換:貪婪 */
|
||||
int coinChangeGreedy(List<int> coins, int amt) {
|
||||
// 假設 coins 串列有序
|
||||
int i = coins.length - 1;
|
||||
int count = 0;
|
||||
// 迴圈進行貪婪選擇,直到無剩餘金額
|
||||
while (amt > 0) {
|
||||
// 找到小於且最接近剩餘金額的硬幣
|
||||
while (i > 0 && coins[i] > amt) {
|
||||
i--;
|
||||
}
|
||||
// 選擇 coins[i]
|
||||
amt -= coins[i];
|
||||
count++;
|
||||
}
|
||||
// 若未找到可行方案,則返回 -1
|
||||
return amt == 0 ? count : -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="coin_change_greedy.rs"
|
||||
/* 零錢兌換:貪婪 */
|
||||
fn coin_change_greedy(coins: &[i32], mut amt: i32) -> i32 {
|
||||
// 假設 coins 串列有序
|
||||
let mut i = coins.len() - 1;
|
||||
let mut count = 0;
|
||||
// 迴圈進行貪婪選擇,直到無剩餘金額
|
||||
while amt > 0 {
|
||||
// 找到小於且最接近剩餘金額的硬幣
|
||||
while i > 0 && coins[i] > amt {
|
||||
i -= 1;
|
||||
}
|
||||
// 選擇 coins[i]
|
||||
amt -= coins[i];
|
||||
count += 1;
|
||||
}
|
||||
// 若未找到可行方案,則返回 -1
|
||||
if amt == 0 {
|
||||
count
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="coin_change_greedy.c"
|
||||
/* 零錢兌換:貪婪 */
|
||||
int coinChangeGreedy(int *coins, int size, int amt) {
|
||||
// 假設 coins 串列有序
|
||||
int i = size - 1;
|
||||
int count = 0;
|
||||
// 迴圈進行貪婪選擇,直到無剩餘金額
|
||||
while (amt > 0) {
|
||||
// 找到小於且最接近剩餘金額的硬幣
|
||||
while (i > 0 && coins[i] > amt) {
|
||||
i--;
|
||||
}
|
||||
// 選擇 coins[i]
|
||||
amt -= coins[i];
|
||||
count++;
|
||||
}
|
||||
// 若未找到可行方案,則返回 -1
|
||||
return amt == 0 ? count : -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="coin_change_greedy.kt"
|
||||
/* 零錢兌換:貪婪 */
|
||||
fun coinChangeGreedy(coins: IntArray, amt: Int): Int {
|
||||
// 假設 coins 串列有序
|
||||
var am = amt
|
||||
var i = coins.size - 1
|
||||
var count = 0
|
||||
// 迴圈進行貪婪選擇,直到無剩餘金額
|
||||
while (am > 0) {
|
||||
// 找到小於且最接近剩餘金額的硬幣
|
||||
while (i > 0 && coins[i] > am) {
|
||||
i--
|
||||
}
|
||||
// 選擇 coins[i]
|
||||
am -= coins[i]
|
||||
count++
|
||||
}
|
||||
// 若未找到可行方案,則返回 -1
|
||||
return if (am == 0) count else -1
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="coin_change_greedy.rb"
|
||||
[class]{}-[func]{coin_change_greedy}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="coin_change_greedy.zig"
|
||||
[class]{}-[func]{coinChangeGreedy}
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20coin_change_greedy%28coins%3A%20list%5Bint%5D,%20amt%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E9%9B%B6%E9%92%B1%E5%85%91%E6%8D%A2%EF%BC%9A%E8%B4%AA%E5%BF%83%22%22%22%0A%20%20%20%20%23%20%E5%81%87%E8%AE%BE%20coins%20%E5%88%97%E8%A1%A8%E6%9C%89%E5%BA%8F%0A%20%20%20%20i%20%3D%20len%28coins%29%20-%201%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%E8%BF%9B%E8%A1%8C%E8%B4%AA%E5%BF%83%E9%80%89%E6%8B%A9%EF%BC%8C%E7%9B%B4%E5%88%B0%E6%97%A0%E5%89%A9%E4%BD%99%E9%87%91%E9%A2%9D%0A%20%20%20%20while%20amt%20%3E%200%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%89%BE%E5%88%B0%E5%B0%8F%E4%BA%8E%E4%B8%94%E6%9C%80%E6%8E%A5%E8%BF%91%E5%89%A9%E4%BD%99%E9%87%91%E9%A2%9D%E7%9A%84%E7%A1%AC%E5%B8%81%0A%20%20%20%20%20%20%20%20while%20i%20%3E%200%20and%20coins%5Bi%5D%20%3E%20amt%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20-%3D%201%0A%20%20%20%20%20%20%20%20%23%20%E9%80%89%E6%8B%A9%20coins%5Bi%5D%0A%20%20%20%20%20%20%20%20amt%20-%3D%20coins%5Bi%5D%0A%20%20%20%20%20%20%20%20count%20%2B%3D%201%0A%20%20%20%20%23%20%E8%8B%A5%E6%9C%AA%E6%89%BE%E5%88%B0%E5%8F%AF%E8%A1%8C%E6%96%B9%E6%A1%88%EF%BC%8C%E5%88%99%E8%BF%94%E5%9B%9E%20-1%0A%20%20%20%20return%20count%20if%20amt%20%3D%3D%200%20else%20-1%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E8%B4%AA%E5%BF%83%EF%BC%9A%E8%83%BD%E5%A4%9F%E4%BF%9D%E8%AF%81%E6%89%BE%E5%88%B0%E5%85%A8%E5%B1%80%E6%9C%80%E4%BC%98%E8%A7%A3%0A%20%20%20%20coins%20%3D%20%5B1,%205,%2010,%2020,%2050,%20100%5D%0A%20%20%20%20amt%20%3D%20186%0A%20%20%20%20res%20%3D%20coin_change_greedy%28coins,%20amt%29%0A%20%20%20%20print%28f%22%5Cncoins%20%3D%20%7Bcoins%7D,%20amt%20%3D%20%7Bamt%7D%22%29%0A%20%20%20%20print%28f%22%E5%87%91%E5%88%B0%20%7Bamt%7D%20%E6%89%80%E9%9C%80%E7%9A%84%E6%9C%80%E5%B0%91%E7%A1%AC%E5%B8%81%E6%95%B0%E9%87%8F%E4%B8%BA%20%7Bres%7D%22%29%0A%0A%20%20%20%20%23%20%E8%B4%AA%E5%BF%83%EF%BC%9A%E6%97%A0%E6%B3%95%E4%BF%9D%E8%AF%81%E6%89%BE%E5%88%B0%E5%85%A8%E5%B1%80%E6%9C%80%E4%BC%98%E8%A7%A3%0A%20%20%20%20coins%20%3D%20%5B1,%2020,%2050%5D%0A%20%20%20%20amt%20%3D%2060%0A%20%20%20%20res%20%3D%20coin_change_greedy%28coins,%20amt%29%0A%20%20%20%20print%28f%22%5Cncoins%20%3D%20%7Bcoins%7D,%20amt%20%3D%20%7Bamt%7D%22%29%0A%20%20%20%20print%28f%22%E5%87%91%E5%88%B0%20%7Bamt%7D%20%E6%89%80%E9%9C%80%E7%9A%84%E6%9C%80%E5%B0%91%E7%A1%AC%E5%B8%81%E6%95%B0%E9%87%8F%E4%B8%BA%20%7Bres%7D%22%29%0A%20%20%20%20print%28f%22%E5%AE%9E%E9%99%85%E4%B8%8A%E9%9C%80%E8%A6%81%E7%9A%84%E6%9C%80%E5%B0%91%E6%95%B0%E9%87%8F%E4%B8%BA%203%20%EF%BC%8C%E5%8D%B3%2020%20%2B%2020%20%2B%2020%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20coin_change_greedy%28coins%3A%20list%5Bint%5D,%20amt%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E9%9B%B6%E9%92%B1%E5%85%91%E6%8D%A2%EF%BC%9A%E8%B4%AA%E5%BF%83%22%22%22%0A%20%20%20%20%23%20%E5%81%87%E8%AE%BE%20coins%20%E5%88%97%E8%A1%A8%E6%9C%89%E5%BA%8F%0A%20%20%20%20i%20%3D%20len%28coins%29%20-%201%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%E8%BF%9B%E8%A1%8C%E8%B4%AA%E5%BF%83%E9%80%89%E6%8B%A9%EF%BC%8C%E7%9B%B4%E5%88%B0%E6%97%A0%E5%89%A9%E4%BD%99%E9%87%91%E9%A2%9D%0A%20%20%20%20while%20amt%20%3E%200%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%89%BE%E5%88%B0%E5%B0%8F%E4%BA%8E%E4%B8%94%E6%9C%80%E6%8E%A5%E8%BF%91%E5%89%A9%E4%BD%99%E9%87%91%E9%A2%9D%E7%9A%84%E7%A1%AC%E5%B8%81%0A%20%20%20%20%20%20%20%20while%20i%20%3E%200%20and%20coins%5Bi%5D%20%3E%20amt%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20-%3D%201%0A%20%20%20%20%20%20%20%20%23%20%E9%80%89%E6%8B%A9%20coins%5Bi%5D%0A%20%20%20%20%20%20%20%20amt%20-%3D%20coins%5Bi%5D%0A%20%20%20%20%20%20%20%20count%20%2B%3D%201%0A%20%20%20%20%23%20%E8%8B%A5%E6%9C%AA%E6%89%BE%E5%88%B0%E5%8F%AF%E8%A1%8C%E6%96%B9%E6%A1%88%EF%BC%8C%E5%88%99%E8%BF%94%E5%9B%9E%20-1%0A%20%20%20%20return%20count%20if%20amt%20%3D%3D%200%20else%20-1%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E8%B4%AA%E5%BF%83%EF%BC%9A%E8%83%BD%E5%A4%9F%E4%BF%9D%E8%AF%81%E6%89%BE%E5%88%B0%E5%85%A8%E5%B1%80%E6%9C%80%E4%BC%98%E8%A7%A3%0A%20%20%20%20coins%20%3D%20%5B1,%205,%2010,%2020,%2050,%20100%5D%0A%20%20%20%20amt%20%3D%20186%0A%20%20%20%20res%20%3D%20coin_change_greedy%28coins,%20amt%29%0A%20%20%20%20print%28f%22%5Cncoins%20%3D%20%7Bcoins%7D,%20amt%20%3D%20%7Bamt%7D%22%29%0A%20%20%20%20print%28f%22%E5%87%91%E5%88%B0%20%7Bamt%7D%20%E6%89%80%E9%9C%80%E7%9A%84%E6%9C%80%E5%B0%91%E7%A1%AC%E5%B8%81%E6%95%B0%E9%87%8F%E4%B8%BA%20%7Bres%7D%22%29%0A%0A%20%20%20%20%23%20%E8%B4%AA%E5%BF%83%EF%BC%9A%E6%97%A0%E6%B3%95%E4%BF%9D%E8%AF%81%E6%89%BE%E5%88%B0%E5%85%A8%E5%B1%80%E6%9C%80%E4%BC%98%E8%A7%A3%0A%20%20%20%20coins%20%3D%20%5B1,%2020,%2050%5D%0A%20%20%20%20amt%20%3D%2060%0A%20%20%20%20res%20%3D%20coin_change_greedy%28coins,%20amt%29%0A%20%20%20%20print%28f%22%5Cncoins%20%3D%20%7Bcoins%7D,%20amt%20%3D%20%7Bamt%7D%22%29%0A%20%20%20%20print%28f%22%E5%87%91%E5%88%B0%20%7Bamt%7D%20%E6%89%80%E9%9C%80%E7%9A%84%E6%9C%80%E5%B0%91%E7%A1%AC%E5%B8%81%E6%95%B0%E9%87%8F%E4%B8%BA%20%7Bres%7D%22%29%0A%20%20%20%20print%28f%22%E5%AE%9E%E9%99%85%E4%B8%8A%E9%9C%80%E8%A6%81%E7%9A%84%E6%9C%80%E5%B0%91%E6%95%B0%E9%87%8F%E4%B8%BA%203%20%EF%BC%8C%E5%8D%B3%2020%20%2B%2020%20%2B%2020%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
你可能會不由地發出感嘆:So clean !貪婪演算法僅用約十行程式碼就解決了零錢兌換問題。
|
||||
|
||||
## 15.1.1 貪婪演算法的優點與侷限性
|
||||
|
||||
**貪婪演算法不僅操作直接、實現簡單,而且通常效率也很高**。在以上程式碼中,記硬幣最小面值為 $\min(coins)$ ,則貪婪選擇最多迴圈 $amt / \min(coins)$ 次,時間複雜度為 $O(amt / \min(coins))$ 。這比動態規劃解法的時間複雜度 $O(n \times amt)$ 小了一個數量級。
|
||||
|
||||
然而,**對於某些硬幣面值組合,貪婪演算法並不能找到最優解**。圖 15-2 給出了兩個示例。
|
||||
|
||||
- **正例 $coins = [1, 5, 10, 20, 50, 100]$**:在該硬幣組合下,給定任意 $amt$ ,貪婪演算法都可以找到最優解。
|
||||
- **反例 $coins = [1, 20, 50]$**:假設 $amt = 60$ ,貪婪演算法只能找到 $50 + 1 \times 10$ 的兌換組合,共計 $11$ 枚硬幣,但動態規劃可以找到最優解 $20 + 20 + 20$ ,僅需 $3$ 枚硬幣。
|
||||
- **反例 $coins = [1, 49, 50]$**:假設 $amt = 98$ ,貪婪演算法只能找到 $50 + 1 \times 48$ 的兌換組合,共計 $49$ 枚硬幣,但動態規劃可以找到最優解 $49 + 49$ ,僅需 $2$ 枚硬幣。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-2 貪婪演算法無法找出最優解的示例 </p>
|
||||
|
||||
也就是說,對於零錢兌換問題,貪婪演算法無法保證找到全域性最優解,並且有可能找到非常差的解。它更適合用動態規劃解決。
|
||||
|
||||
一般情況下,貪婪演算法的適用情況分以下兩種。
|
||||
|
||||
1. **可以保證找到最優解**:貪婪演算法在這種情況下往往是最優選擇,因為它往往比回溯、動態規劃更高效。
|
||||
2. **可以找到近似最優解**:貪婪演算法在這種情況下也是可用的。對於很多複雜問題來說,尋找全域性最優解非常困難,能以較高效率找到次優解也是非常不錯的。
|
||||
|
||||
## 15.1.2 貪婪演算法特性
|
||||
|
||||
那麼問題來了,什麼樣的問題適合用貪婪演算法求解呢?或者說,貪婪演算法在什麼情況下可以保證找到最優解?
|
||||
|
||||
相較於動態規劃,貪婪演算法的使用條件更加苛刻,其主要關注問題的兩個性質。
|
||||
|
||||
- **貪婪選擇性質**:只有當局部最優選擇始終可以導致全域性最優解時,貪婪演算法才能保證得到最優解。
|
||||
- **最優子結構**:原問題的最優解包含子問題的最優解。
|
||||
|
||||
最優子結構已經在“動態規劃”章節中介紹過,這裡不再贅述。值得注意的是,一些問題的最優子結構並不明顯,但仍然可使用貪婪演算法解決。
|
||||
|
||||
我們主要探究貪婪選擇性質的判斷方法。雖然它的描述看上去比較簡單,**但實際上對於許多問題,證明貪婪選擇性質並非易事**。
|
||||
|
||||
例如零錢兌換問題,我們雖然能夠容易地舉出反例,對貪婪選擇性質進行證偽,但證實的難度較大。如果問:**滿足什麼條件的硬幣組合可以使用貪婪演算法求解**?我們往往只能憑藉直覺或舉例子來給出一個模稜兩可的答案,而難以給出嚴謹的數學證明。
|
||||
|
||||
!!! quote
|
||||
|
||||
有一篇論文給出了一個 $O(n^3)$ 時間複雜度的演算法,用於判斷一個硬幣組合能否使用貪婪演算法找出任意金額的最優解。
|
||||
|
||||
Pearson, D. A polynomial-time algorithm for the change-making problem[J]. Operations Research Letters, 2005, 33(3): 231-234.
|
||||
|
||||
## 15.1.3 貪婪演算法解題步驟
|
||||
|
||||
貪婪問題的解決流程大體可分為以下三步。
|
||||
|
||||
1. **問題分析**:梳理與理解問題特性,包括狀態定義、最佳化目標和約束條件等。這一步在回溯和動態規劃中都有涉及。
|
||||
2. **確定貪婪策略**:確定如何在每一步中做出貪婪選擇。這個策略能夠在每一步減小問題的規模,並最終解決整個問題。
|
||||
3. **正確性證明**:通常需要證明問題具有貪婪選擇性質和最優子結構。這個步驟可能需要用到數學證明,例如歸納法或反證法等。
|
||||
|
||||
確定貪婪策略是求解問題的核心步驟,但實施起來可能並不容易,主要有以下原因。
|
||||
|
||||
- **不同問題的貪婪策略的差異較大**。對於許多問題來說,貪婪策略比較淺顯,我們透過一些大概的思考與嘗試就能得出。而對於一些複雜問題,貪婪策略可能非常隱蔽,這種情況就非常考驗個人的解題經驗與演算法能力了。
|
||||
- **某些貪婪策略具有較強的迷惑性**。當我們滿懷信心設計好貪婪策略,寫出解題程式碼並提交執行,很可能發現部分測試樣例無法透過。這是因為設計的貪婪策略只是“部分正確”的,上文介紹的零錢兌換就是一個典型案例。
|
||||
|
||||
為了保證正確性,我們應該對貪婪策略進行嚴謹的數學證明,**通常需要用到反證法或數學歸納法**。
|
||||
|
||||
然而,正確性證明也很可能不是一件易事。如若沒有頭緒,我們通常會選擇面向測試用例進行程式碼除錯,一步步修改與驗證貪婪策略。
|
||||
|
||||
## 15.1.4 貪婪演算法典型例題
|
||||
|
||||
貪婪演算法常常應用在滿足貪婪選擇性質和最優子結構的最佳化問題中,以下列舉了一些典型的貪婪演算法問題。
|
||||
|
||||
- **硬幣找零問題**:在某些硬幣組合下,貪婪演算法總是可以得到最優解。
|
||||
- **區間排程問題**:假設你有一些任務,每個任務在一段時間內進行,你的目標是完成儘可能多的任務。如果每次都選擇結束時間最早的任務,那麼貪婪演算法就可以得到最優解。
|
||||
- **分數背包問題**:給定一組物品和一個載重量,你的目標是選擇一組物品,使得總重量不超過載重量,且總價值最大。如果每次都選擇價效比最高(價值 / 重量)的物品,那麼貪婪演算法在一些情況下可以得到最優解。
|
||||
- **股票買賣問題**:給定一組股票的歷史價格,你可以進行多次買賣,但如果你已經持有股票,那麼在賣出之前不能再買,目標是獲取最大利潤。
|
||||
- **霍夫曼編碼**:霍夫曼編碼是一種用於無損資料壓縮的貪婪演算法。透過構建霍夫曼樹,每次選擇出現頻率最低的兩個節點合併,最後得到的霍夫曼樹的帶權路徑長度(編碼長度)最小。
|
||||
- **Dijkstra 演算法**:它是一種解決給定源頂點到其餘各頂點的最短路徑問題的貪婪演算法。
|
||||
22
zh-Hant/docs/chapter_greedy/index.md
Normal file
22
zh-Hant/docs/chapter_greedy/index.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/head-heart-outline
|
||||
---
|
||||
|
||||
# 第 15 章 貪婪
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
向日葵朝著太陽轉動,時刻追求自身成長的最大可能。
|
||||
|
||||
貪婪策略在一輪輪的簡單選擇中,逐步導向最佳答案。
|
||||
|
||||
## Chapter Contents
|
||||
|
||||
- [15.1 貪婪演算法](https://www.hello-algo.com/en/chapter_greedy/greedy_algorithm/)
|
||||
- [15.2 分數背包問題](https://www.hello-algo.com/en/chapter_greedy/fractional_knapsack_problem/)
|
||||
- [15.3 最大容量問題](https://www.hello-algo.com/en/chapter_greedy/max_capacity_problem/)
|
||||
- [15.4 最大切分乘積問題](https://www.hello-algo.com/en/chapter_greedy/max_product_cutting_problem/)
|
||||
- [15.5 小結](https://www.hello-algo.com/en/chapter_greedy/summary/)
|
||||
430
zh-Hant/docs/chapter_greedy/max_capacity_problem.md
Normal file
430
zh-Hant/docs/chapter_greedy/max_capacity_problem.md
Normal file
@@ -0,0 +1,430 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 15.3 最大容量問題
|
||||
|
||||
!!! question
|
||||
|
||||
輸入一個陣列 $ht$ ,其中的每個元素代表一個垂直隔板的高度。陣列中的任意兩個隔板,以及它們之間的空間可以組成一個容器。
|
||||
|
||||
容器的容量等於高度和寬度的乘積(面積),其中高度由較短的隔板決定,寬度是兩個隔板的陣列索引之差。
|
||||
|
||||
請在陣列中選擇兩個隔板,使得組成的容器的容量最大,返回最大容量。示例如圖 15-7 所示。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-7 最大容量問題的示例資料 </p>
|
||||
|
||||
容器由任意兩個隔板圍成,**因此本題的狀態為兩個隔板的索引,記為 $[i, j]$** 。
|
||||
|
||||
根據題意,容量等於高度乘以寬度,其中高度由短板決定,寬度是兩隔板的陣列索引之差。設容量為 $cap[i, j]$ ,則可得計算公式:
|
||||
|
||||
$$
|
||||
cap[i, j] = \min(ht[i], ht[j]) \times (j - i)
|
||||
$$
|
||||
|
||||
設陣列長度為 $n$ ,兩個隔板的組合數量(狀態總數)為 $C_n^2 = \frac{n(n - 1)}{2}$ 個。最直接地,**我們可以窮舉所有狀態**,從而求得最大容量,時間複雜度為 $O(n^2)$ 。
|
||||
|
||||
### 1. 貪婪策略確定
|
||||
|
||||
這道題還有更高效率的解法。如圖 15-8 所示,現選取一個狀態 $[i, j]$ ,其滿足索引 $i < j$ 且高度 $ht[i] < ht[j]$ ,即 $i$ 為短板、$j$ 為長板。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-8 初始狀態 </p>
|
||||
|
||||
如圖 15-9 所示,**若此時將長板 $j$ 向短板 $i$ 靠近,則容量一定變小**。
|
||||
|
||||
這是因為在移動長板 $j$ 後,寬度 $j-i$ 肯定變小;而高度由短板決定,因此高度只可能不變( $i$ 仍為短板)或變小(移動後的 $j$ 成為短板)。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-9 向內移動長板後的狀態 </p>
|
||||
|
||||
反向思考,**我們只有向內收縮短板 $i$ ,才有可能使容量變大**。因為雖然寬度一定變小,**但高度可能會變大**(移動後的短板 $i$ 可能會變長)。例如在圖 15-10 中,移動短板後面積變大。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-10 向內移動短板後的狀態 </p>
|
||||
|
||||
由此便可推出本題的貪婪策略:初始化兩指標,使其分列容器兩端,每輪向內收縮短板對應的指標,直至兩指標相遇。
|
||||
|
||||
圖 15-11 展示了貪婪策略的執行過程。
|
||||
|
||||
1. 初始狀態下,指標 $i$ 和 $j$ 分列陣列兩端。
|
||||
2. 計算當前狀態的容量 $cap[i, j]$ ,並更新最大容量。
|
||||
3. 比較板 $i$ 和 板 $j$ 的高度,並將短板向內移動一格。
|
||||
4. 迴圈執行第 `2.` 步和第 `3.` 步,直至 $i$ 和 $j$ 相遇時結束。
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<3>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<4>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<5>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<6>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<7>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<8>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<9>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-11 最大容量問題的貪婪過程 </p>
|
||||
|
||||
### 2. 程式碼實現
|
||||
|
||||
程式碼迴圈最多 $n$ 輪,**因此時間複雜度為 $O(n)$** 。
|
||||
|
||||
變數 $i$、$j$、$res$ 使用常數大小的額外空間,**因此空間複雜度為 $O(1)$** 。
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="max_capacity.py"
|
||||
def max_capacity(ht: list[int]) -> int:
|
||||
"""最大容量:貪婪"""
|
||||
# 初始化 i, j,使其分列陣列兩端
|
||||
i, j = 0, len(ht) - 1
|
||||
# 初始最大容量為 0
|
||||
res = 0
|
||||
# 迴圈貪婪選擇,直至兩板相遇
|
||||
while i < j:
|
||||
# 更新最大容量
|
||||
cap = min(ht[i], ht[j]) * (j - i)
|
||||
res = max(res, cap)
|
||||
# 向內移動短板
|
||||
if ht[i] < ht[j]:
|
||||
i += 1
|
||||
else:
|
||||
j -= 1
|
||||
return res
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="max_capacity.cpp"
|
||||
/* 最大容量:貪婪 */
|
||||
int maxCapacity(vector<int> &ht) {
|
||||
// 初始化 i, j,使其分列陣列兩端
|
||||
int i = 0, j = ht.size() - 1;
|
||||
// 初始最大容量為 0
|
||||
int res = 0;
|
||||
// 迴圈貪婪選擇,直至兩板相遇
|
||||
while (i < j) {
|
||||
// 更新最大容量
|
||||
int cap = min(ht[i], ht[j]) * (j - i);
|
||||
res = max(res, cap);
|
||||
// 向內移動短板
|
||||
if (ht[i] < ht[j]) {
|
||||
i++;
|
||||
} else {
|
||||
j--;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="max_capacity.java"
|
||||
/* 最大容量:貪婪 */
|
||||
int maxCapacity(int[] ht) {
|
||||
// 初始化 i, j,使其分列陣列兩端
|
||||
int i = 0, j = ht.length - 1;
|
||||
// 初始最大容量為 0
|
||||
int res = 0;
|
||||
// 迴圈貪婪選擇,直至兩板相遇
|
||||
while (i < j) {
|
||||
// 更新最大容量
|
||||
int cap = Math.min(ht[i], ht[j]) * (j - i);
|
||||
res = Math.max(res, cap);
|
||||
// 向內移動短板
|
||||
if (ht[i] < ht[j]) {
|
||||
i++;
|
||||
} else {
|
||||
j--;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="max_capacity.cs"
|
||||
/* 最大容量:貪婪 */
|
||||
int MaxCapacity(int[] ht) {
|
||||
// 初始化 i, j,使其分列陣列兩端
|
||||
int i = 0, j = ht.Length - 1;
|
||||
// 初始最大容量為 0
|
||||
int res = 0;
|
||||
// 迴圈貪婪選擇,直至兩板相遇
|
||||
while (i < j) {
|
||||
// 更新最大容量
|
||||
int cap = Math.Min(ht[i], ht[j]) * (j - i);
|
||||
res = Math.Max(res, cap);
|
||||
// 向內移動短板
|
||||
if (ht[i] < ht[j]) {
|
||||
i++;
|
||||
} else {
|
||||
j--;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="max_capacity.go"
|
||||
/* 最大容量:貪婪 */
|
||||
func maxCapacity(ht []int) int {
|
||||
// 初始化 i, j,使其分列陣列兩端
|
||||
i, j := 0, len(ht)-1
|
||||
// 初始最大容量為 0
|
||||
res := 0
|
||||
// 迴圈貪婪選擇,直至兩板相遇
|
||||
for i < j {
|
||||
// 更新最大容量
|
||||
capacity := int(math.Min(float64(ht[i]), float64(ht[j]))) * (j - i)
|
||||
res = int(math.Max(float64(res), float64(capacity)))
|
||||
// 向內移動短板
|
||||
if ht[i] < ht[j] {
|
||||
i++
|
||||
} else {
|
||||
j--
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="max_capacity.swift"
|
||||
/* 最大容量:貪婪 */
|
||||
func maxCapacity(ht: [Int]) -> Int {
|
||||
// 初始化 i, j,使其分列陣列兩端
|
||||
var i = ht.startIndex, j = ht.endIndex - 1
|
||||
// 初始最大容量為 0
|
||||
var res = 0
|
||||
// 迴圈貪婪選擇,直至兩板相遇
|
||||
while i < j {
|
||||
// 更新最大容量
|
||||
let cap = min(ht[i], ht[j]) * (j - i)
|
||||
res = max(res, cap)
|
||||
// 向內移動短板
|
||||
if ht[i] < ht[j] {
|
||||
i += 1
|
||||
} else {
|
||||
j -= 1
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="max_capacity.js"
|
||||
/* 最大容量:貪婪 */
|
||||
function maxCapacity(ht) {
|
||||
// 初始化 i, j,使其分列陣列兩端
|
||||
let i = 0,
|
||||
j = ht.length - 1;
|
||||
// 初始最大容量為 0
|
||||
let res = 0;
|
||||
// 迴圈貪婪選擇,直至兩板相遇
|
||||
while (i < j) {
|
||||
// 更新最大容量
|
||||
const cap = Math.min(ht[i], ht[j]) * (j - i);
|
||||
res = Math.max(res, cap);
|
||||
// 向內移動短板
|
||||
if (ht[i] < ht[j]) {
|
||||
i += 1;
|
||||
} else {
|
||||
j -= 1;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="max_capacity.ts"
|
||||
/* 最大容量:貪婪 */
|
||||
function maxCapacity(ht: number[]): number {
|
||||
// 初始化 i, j,使其分列陣列兩端
|
||||
let i = 0,
|
||||
j = ht.length - 1;
|
||||
// 初始最大容量為 0
|
||||
let res = 0;
|
||||
// 迴圈貪婪選擇,直至兩板相遇
|
||||
while (i < j) {
|
||||
// 更新最大容量
|
||||
const cap: number = Math.min(ht[i], ht[j]) * (j - i);
|
||||
res = Math.max(res, cap);
|
||||
// 向內移動短板
|
||||
if (ht[i] < ht[j]) {
|
||||
i += 1;
|
||||
} else {
|
||||
j -= 1;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="max_capacity.dart"
|
||||
/* 最大容量:貪婪 */
|
||||
int maxCapacity(List<int> ht) {
|
||||
// 初始化 i, j,使其分列陣列兩端
|
||||
int i = 0, j = ht.length - 1;
|
||||
// 初始最大容量為 0
|
||||
int res = 0;
|
||||
// 迴圈貪婪選擇,直至兩板相遇
|
||||
while (i < j) {
|
||||
// 更新最大容量
|
||||
int cap = min(ht[i], ht[j]) * (j - i);
|
||||
res = max(res, cap);
|
||||
// 向內移動短板
|
||||
if (ht[i] < ht[j]) {
|
||||
i++;
|
||||
} else {
|
||||
j--;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="max_capacity.rs"
|
||||
/* 最大容量:貪婪 */
|
||||
fn max_capacity(ht: &[i32]) -> i32 {
|
||||
// 初始化 i, j,使其分列陣列兩端
|
||||
let mut i = 0;
|
||||
let mut j = ht.len() - 1;
|
||||
// 初始最大容量為 0
|
||||
let mut res = 0;
|
||||
// 迴圈貪婪選擇,直至兩板相遇
|
||||
while i < j {
|
||||
// 更新最大容量
|
||||
let cap = std::cmp::min(ht[i], ht[j]) * (j - i) as i32;
|
||||
res = std::cmp::max(res, cap);
|
||||
// 向內移動短板
|
||||
if ht[i] < ht[j] {
|
||||
i += 1;
|
||||
} else {
|
||||
j -= 1;
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="max_capacity.c"
|
||||
/* 最大容量:貪婪 */
|
||||
int maxCapacity(int ht[], int htLength) {
|
||||
// 初始化 i, j,使其分列陣列兩端
|
||||
int i = 0;
|
||||
int j = htLength - 1;
|
||||
// 初始最大容量為 0
|
||||
int res = 0;
|
||||
// 迴圈貪婪選擇,直至兩板相遇
|
||||
while (i < j) {
|
||||
// 更新最大容量
|
||||
int capacity = myMin(ht[i], ht[j]) * (j - i);
|
||||
res = myMax(res, capacity);
|
||||
// 向內移動短板
|
||||
if (ht[i] < ht[j]) {
|
||||
i++;
|
||||
} else {
|
||||
j--;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="max_capacity.kt"
|
||||
/* 最大容量:貪婪 */
|
||||
fun maxCapacity(ht: IntArray): Int {
|
||||
// 初始化 i, j,使其分列陣列兩端
|
||||
var i = 0
|
||||
var j = ht.size - 1
|
||||
// 初始最大容量為 0
|
||||
var res = 0
|
||||
// 迴圈貪婪選擇,直至兩板相遇
|
||||
while (i < j) {
|
||||
// 更新最大容量
|
||||
val cap = (min(ht[i].toDouble(), ht[j].toDouble()) * (j - i)).toInt()
|
||||
res = max(res.toDouble(), cap.toDouble()).toInt()
|
||||
// 向內移動短板
|
||||
if (ht[i] < ht[j]) {
|
||||
i++
|
||||
} else {
|
||||
j--
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="max_capacity.rb"
|
||||
[class]{}-[func]{max_capacity}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="max_capacity.zig"
|
||||
[class]{}-[func]{maxCapacity}
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20max_capacity%28ht%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%EF%BC%9A%E8%B4%AA%E5%BF%83%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%20i,%20j%EF%BC%8C%E4%BD%BF%E5%85%B6%E5%88%86%E5%88%97%E6%95%B0%E7%BB%84%E4%B8%A4%E7%AB%AF%0A%20%20%20%20i,%20j%20%3D%200,%20len%28ht%29%20-%201%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%E4%B8%BA%200%0A%20%20%20%20res%20%3D%200%0A%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%E8%B4%AA%E5%BF%83%E9%80%89%E6%8B%A9%EF%BC%8C%E7%9B%B4%E8%87%B3%E4%B8%A4%E6%9D%BF%E7%9B%B8%E9%81%87%0A%20%20%20%20while%20i%20%3C%20j%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%9B%B4%E6%96%B0%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%0A%20%20%20%20%20%20%20%20cap%20%3D%20min%28ht%5Bi%5D,%20ht%5Bj%5D%29%20*%20%28j%20-%20i%29%0A%20%20%20%20%20%20%20%20res%20%3D%20max%28res,%20cap%29%0A%20%20%20%20%20%20%20%20%23%20%E5%90%91%E5%86%85%E7%A7%BB%E5%8A%A8%E7%9F%AD%E6%9D%BF%0A%20%20%20%20%20%20%20%20if%20ht%5Bi%5D%20%3C%20ht%5Bj%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%2B%3D%201%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20-%3D%201%0A%20%20%20%20return%20res%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20ht%20%3D%20%5B3,%208,%205,%202,%207,%207,%203,%204%5D%0A%0A%20%20%20%20%23%20%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95%0A%20%20%20%20res%20%3D%20max_capacity%28ht%29%0A%20%20%20%20print%28f%22%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%E4%B8%BA%20%7Bres%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20max_capacity%28ht%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%EF%BC%9A%E8%B4%AA%E5%BF%83%22%22%22%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%20i,%20j%EF%BC%8C%E4%BD%BF%E5%85%B6%E5%88%86%E5%88%97%E6%95%B0%E7%BB%84%E4%B8%A4%E7%AB%AF%0A%20%20%20%20i,%20j%20%3D%200,%20len%28ht%29%20-%201%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%E4%B8%BA%200%0A%20%20%20%20res%20%3D%200%0A%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%E8%B4%AA%E5%BF%83%E9%80%89%E6%8B%A9%EF%BC%8C%E7%9B%B4%E8%87%B3%E4%B8%A4%E6%9D%BF%E7%9B%B8%E9%81%87%0A%20%20%20%20while%20i%20%3C%20j%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%9B%B4%E6%96%B0%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%0A%20%20%20%20%20%20%20%20cap%20%3D%20min%28ht%5Bi%5D,%20ht%5Bj%5D%29%20*%20%28j%20-%20i%29%0A%20%20%20%20%20%20%20%20res%20%3D%20max%28res,%20cap%29%0A%20%20%20%20%20%20%20%20%23%20%E5%90%91%E5%86%85%E7%A7%BB%E5%8A%A8%E7%9F%AD%E6%9D%BF%0A%20%20%20%20%20%20%20%20if%20ht%5Bi%5D%20%3C%20ht%5Bj%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%2B%3D%201%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20-%3D%201%0A%20%20%20%20return%20res%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20ht%20%3D%20%5B3,%208,%205,%202,%207,%207,%203,%204%5D%0A%0A%20%20%20%20%23%20%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95%0A%20%20%20%20res%20%3D%20max_capacity%28ht%29%0A%20%20%20%20print%28f%22%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%E4%B8%BA%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
### 3. 正確性證明
|
||||
|
||||
之所以貪婪比窮舉更快,是因為每輪的貪婪選擇都會“跳過”一些狀態。
|
||||
|
||||
比如在狀態 $cap[i, j]$ 下,$i$ 為短板、$j$ 為長板。若貪婪地將短板 $i$ 向內移動一格,會導致圖 15-12 所示的狀態被“跳過”。**這意味著之後無法驗證這些狀態的容量大小**。
|
||||
|
||||
$$
|
||||
cap[i, i+1], cap[i, i+2], \dots, cap[i, j-2], cap[i, j-1]
|
||||
$$
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-12 移動短板導致被跳過的狀態 </p>
|
||||
|
||||
觀察發現,**這些被跳過的狀態實際上就是將長板 $j$ 向內移動的所有狀態**。前面我們已經證明內移長板一定會導致容量變小。也就是說,被跳過的狀態都不可能是最優解,**跳過它們不會導致錯過最優解**。
|
||||
|
||||
以上分析說明,移動短板的操作是“安全”的,貪婪策略是有效的。
|
||||
405
zh-Hant/docs/chapter_greedy/max_product_cutting_problem.md
Normal file
405
zh-Hant/docs/chapter_greedy/max_product_cutting_problem.md
Normal file
@@ -0,0 +1,405 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 15.4 最大切分乘積問題
|
||||
|
||||
!!! question
|
||||
|
||||
給定一個正整數 $n$ ,將其切分為至少兩個正整數的和,求切分後所有整數的乘積最大是多少,如圖 15-13 所示。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-13 最大切分乘積的問題定義 </p>
|
||||
|
||||
假設我們將 $n$ 切分為 $m$ 個整數因子,其中第 $i$ 個因子記為 $n_i$ ,即
|
||||
|
||||
$$
|
||||
n = \sum_{i=1}^{m}n_i
|
||||
$$
|
||||
|
||||
本題的目標是求得所有整數因子的最大乘積,即
|
||||
|
||||
$$
|
||||
\max(\prod_{i=1}^{m}n_i)
|
||||
$$
|
||||
|
||||
我們需要思考的是:切分數量 $m$ 應該多大,每個 $n_i$ 應該是多少?
|
||||
|
||||
### 1. 貪婪策略確定
|
||||
|
||||
根據經驗,兩個整數的乘積往往比它們的加和更大。假設從 $n$ 中分出一個因子 $2$ ,則它們的乘積為 $2(n-2)$ 。我們將該乘積與 $n$ 作比較:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
2(n-2) & \geq n \newline
|
||||
2n - n - 4 & \geq 0 \newline
|
||||
n & \geq 4
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
如圖 15-14 所示,當 $n \geq 4$ 時,切分出一個 $2$ 後乘積會變大,**這說明大於等於 $4$ 的整數都應該被切分**。
|
||||
|
||||
**貪婪策略一**:如果切分方案中包含 $\geq 4$ 的因子,那麼它就應該被繼續切分。最終的切分方案只應出現 $1$、$2$、$3$ 這三種因子。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-14 切分導致乘積變大 </p>
|
||||
|
||||
接下來思考哪個因子是最優的。在 $1$、$2$、$3$ 這三個因子中,顯然 $1$ 是最差的,因為 $1 \times (n-1) < n$ 恆成立,即切分出 $1$ 反而會導致乘積減小。
|
||||
|
||||
如圖 15-15 所示,當 $n = 6$ 時,有 $3 \times 3 > 2 \times 2 \times 2$ 。**這意味著切分出 $3$ 比切分出 $2$ 更優**。
|
||||
|
||||
**貪婪策略二**:在切分方案中,最多隻應存在兩個 $2$ 。因為三個 $2$ 總是可以替換為兩個 $3$ ,從而獲得更大的乘積。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-15 最優切分因子 </p>
|
||||
|
||||
綜上所述,可推理出以下貪婪策略。
|
||||
|
||||
1. 輸入整數 $n$ ,從其不斷地切分出因子 $3$ ,直至餘數為 $0$、$1$、$2$ 。
|
||||
2. 當餘數為 $0$ 時,代表 $n$ 是 $3$ 的倍數,因此不做任何處理。
|
||||
3. 當餘數為 $2$ 時,不繼續劃分,保留。
|
||||
4. 當餘數為 $1$ 時,由於 $2 \times 2 > 1 \times 3$ ,因此應將最後一個 $3$ 替換為 $2$ 。
|
||||
|
||||
### 2. 程式碼實現
|
||||
|
||||
如圖 15-16 所示,我們無須透過迴圈來切分整數,而可以利用向下整除運算得到 $3$ 的個數 $a$ ,用取模運算得到餘數 $b$ ,此時有:
|
||||
|
||||
$$
|
||||
n = 3 a + b
|
||||
$$
|
||||
|
||||
請注意,對於 $n \leq 3$ 的邊界情況,必須拆分出一個 $1$ ,乘積為 $1 \times (n - 1)$ 。
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="max_product_cutting.py"
|
||||
def max_product_cutting(n: int) -> int:
|
||||
"""最大切分乘積:貪婪"""
|
||||
# 當 n <= 3 時,必須切分出一個 1
|
||||
if n <= 3:
|
||||
return 1 * (n - 1)
|
||||
# 貪婪地切分出 3 ,a 為 3 的個數,b 為餘數
|
||||
a, b = n // 3, n % 3
|
||||
if b == 1:
|
||||
# 當餘數為 1 時,將一對 1 * 3 轉化為 2 * 2
|
||||
return int(math.pow(3, a - 1)) * 2 * 2
|
||||
if b == 2:
|
||||
# 當餘數為 2 時,不做處理
|
||||
return int(math.pow(3, a)) * 2
|
||||
# 當餘數為 0 時,不做處理
|
||||
return int(math.pow(3, a))
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="max_product_cutting.cpp"
|
||||
/* 最大切分乘積:貪婪 */
|
||||
int maxProductCutting(int n) {
|
||||
// 當 n <= 3 時,必須切分出一個 1
|
||||
if (n <= 3) {
|
||||
return 1 * (n - 1);
|
||||
}
|
||||
// 貪婪地切分出 3 ,a 為 3 的個數,b 為餘數
|
||||
int a = n / 3;
|
||||
int b = n % 3;
|
||||
if (b == 1) {
|
||||
// 當餘數為 1 時,將一對 1 * 3 轉化為 2 * 2
|
||||
return (int)pow(3, a - 1) * 2 * 2;
|
||||
}
|
||||
if (b == 2) {
|
||||
// 當餘數為 2 時,不做處理
|
||||
return (int)pow(3, a) * 2;
|
||||
}
|
||||
// 當餘數為 0 時,不做處理
|
||||
return (int)pow(3, a);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="max_product_cutting.java"
|
||||
/* 最大切分乘積:貪婪 */
|
||||
int maxProductCutting(int n) {
|
||||
// 當 n <= 3 時,必須切分出一個 1
|
||||
if (n <= 3) {
|
||||
return 1 * (n - 1);
|
||||
}
|
||||
// 貪婪地切分出 3 ,a 為 3 的個數,b 為餘數
|
||||
int a = n / 3;
|
||||
int b = n % 3;
|
||||
if (b == 1) {
|
||||
// 當餘數為 1 時,將一對 1 * 3 轉化為 2 * 2
|
||||
return (int) Math.pow(3, a - 1) * 2 * 2;
|
||||
}
|
||||
if (b == 2) {
|
||||
// 當餘數為 2 時,不做處理
|
||||
return (int) Math.pow(3, a) * 2;
|
||||
}
|
||||
// 當餘數為 0 時,不做處理
|
||||
return (int) Math.pow(3, a);
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="max_product_cutting.cs"
|
||||
/* 最大切分乘積:貪婪 */
|
||||
int MaxProductCutting(int n) {
|
||||
// 當 n <= 3 時,必須切分出一個 1
|
||||
if (n <= 3) {
|
||||
return 1 * (n - 1);
|
||||
}
|
||||
// 貪婪地切分出 3 ,a 為 3 的個數,b 為餘數
|
||||
int a = n / 3;
|
||||
int b = n % 3;
|
||||
if (b == 1) {
|
||||
// 當餘數為 1 時,將一對 1 * 3 轉化為 2 * 2
|
||||
return (int)Math.Pow(3, a - 1) * 2 * 2;
|
||||
}
|
||||
if (b == 2) {
|
||||
// 當餘數為 2 時,不做處理
|
||||
return (int)Math.Pow(3, a) * 2;
|
||||
}
|
||||
// 當餘數為 0 時,不做處理
|
||||
return (int)Math.Pow(3, a);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="max_product_cutting.go"
|
||||
/* 最大切分乘積:貪婪 */
|
||||
func maxProductCutting(n int) int {
|
||||
// 當 n <= 3 時,必須切分出一個 1
|
||||
if n <= 3 {
|
||||
return 1 * (n - 1)
|
||||
}
|
||||
// 貪婪地切分出 3 ,a 為 3 的個數,b 為餘數
|
||||
a := n / 3
|
||||
b := n % 3
|
||||
if b == 1 {
|
||||
// 當餘數為 1 時,將一對 1 * 3 轉化為 2 * 2
|
||||
return int(math.Pow(3, float64(a-1))) * 2 * 2
|
||||
}
|
||||
if b == 2 {
|
||||
// 當餘數為 2 時,不做處理
|
||||
return int(math.Pow(3, float64(a))) * 2
|
||||
}
|
||||
// 當餘數為 0 時,不做處理
|
||||
return int(math.Pow(3, float64(a)))
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="max_product_cutting.swift"
|
||||
/* 最大切分乘積:貪婪 */
|
||||
func maxProductCutting(n: Int) -> Int {
|
||||
// 當 n <= 3 時,必須切分出一個 1
|
||||
if n <= 3 {
|
||||
return 1 * (n - 1)
|
||||
}
|
||||
// 貪婪地切分出 3 ,a 為 3 的個數,b 為餘數
|
||||
let a = n / 3
|
||||
let b = n % 3
|
||||
if b == 1 {
|
||||
// 當餘數為 1 時,將一對 1 * 3 轉化為 2 * 2
|
||||
return pow(3, a - 1) * 2 * 2
|
||||
}
|
||||
if b == 2 {
|
||||
// 當餘數為 2 時,不做處理
|
||||
return pow(3, a) * 2
|
||||
}
|
||||
// 當餘數為 0 時,不做處理
|
||||
return pow(3, a)
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="max_product_cutting.js"
|
||||
/* 最大切分乘積:貪婪 */
|
||||
function maxProductCutting(n) {
|
||||
// 當 n <= 3 時,必須切分出一個 1
|
||||
if (n <= 3) {
|
||||
return 1 * (n - 1);
|
||||
}
|
||||
// 貪婪地切分出 3 ,a 為 3 的個數,b 為餘數
|
||||
let a = Math.floor(n / 3);
|
||||
let b = n % 3;
|
||||
if (b === 1) {
|
||||
// 當餘數為 1 時,將一對 1 * 3 轉化為 2 * 2
|
||||
return Math.pow(3, a - 1) * 2 * 2;
|
||||
}
|
||||
if (b === 2) {
|
||||
// 當餘數為 2 時,不做處理
|
||||
return Math.pow(3, a) * 2;
|
||||
}
|
||||
// 當餘數為 0 時,不做處理
|
||||
return Math.pow(3, a);
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="max_product_cutting.ts"
|
||||
/* 最大切分乘積:貪婪 */
|
||||
function maxProductCutting(n: number): number {
|
||||
// 當 n <= 3 時,必須切分出一個 1
|
||||
if (n <= 3) {
|
||||
return 1 * (n - 1);
|
||||
}
|
||||
// 貪婪地切分出 3 ,a 為 3 的個數,b 為餘數
|
||||
let a: number = Math.floor(n / 3);
|
||||
let b: number = n % 3;
|
||||
if (b === 1) {
|
||||
// 當餘數為 1 時,將一對 1 * 3 轉化為 2 * 2
|
||||
return Math.pow(3, a - 1) * 2 * 2;
|
||||
}
|
||||
if (b === 2) {
|
||||
// 當餘數為 2 時,不做處理
|
||||
return Math.pow(3, a) * 2;
|
||||
}
|
||||
// 當餘數為 0 時,不做處理
|
||||
return Math.pow(3, a);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="max_product_cutting.dart"
|
||||
/* 最大切分乘積:貪婪 */
|
||||
int maxProductCutting(int n) {
|
||||
// 當 n <= 3 時,必須切分出一個 1
|
||||
if (n <= 3) {
|
||||
return 1 * (n - 1);
|
||||
}
|
||||
// 貪婪地切分出 3 ,a 為 3 的個數,b 為餘數
|
||||
int a = n ~/ 3;
|
||||
int b = n % 3;
|
||||
if (b == 1) {
|
||||
// 當餘數為 1 時,將一對 1 * 3 轉化為 2 * 2
|
||||
return (pow(3, a - 1) * 2 * 2).toInt();
|
||||
}
|
||||
if (b == 2) {
|
||||
// 當餘數為 2 時,不做處理
|
||||
return (pow(3, a) * 2).toInt();
|
||||
}
|
||||
// 當餘數為 0 時,不做處理
|
||||
return pow(3, a).toInt();
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="max_product_cutting.rs"
|
||||
/* 最大切分乘積:貪婪 */
|
||||
fn max_product_cutting(n: i32) -> i32 {
|
||||
// 當 n <= 3 時,必須切分出一個 1
|
||||
if n <= 3 {
|
||||
return 1 * (n - 1);
|
||||
}
|
||||
// 貪婪地切分出 3 ,a 為 3 的個數,b 為餘數
|
||||
let a = n / 3;
|
||||
let b = n % 3;
|
||||
if b == 1 {
|
||||
// 當餘數為 1 時,將一對 1 * 3 轉化為 2 * 2
|
||||
3_i32.pow(a as u32 - 1) * 2 * 2
|
||||
} else if b == 2 {
|
||||
// 當餘數為 2 時,不做處理
|
||||
3_i32.pow(a as u32) * 2
|
||||
} else {
|
||||
// 當餘數為 0 時,不做處理
|
||||
3_i32.pow(a as u32)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="max_product_cutting.c"
|
||||
/* 最大切分乘積:貪婪 */
|
||||
int maxProductCutting(int n) {
|
||||
// 當 n <= 3 時,必須切分出一個 1
|
||||
if (n <= 3) {
|
||||
return 1 * (n - 1);
|
||||
}
|
||||
// 貪婪地切分出 3 ,a 為 3 的個數,b 為餘數
|
||||
int a = n / 3;
|
||||
int b = n % 3;
|
||||
if (b == 1) {
|
||||
// 當餘數為 1 時,將一對 1 * 3 轉化為 2 * 2
|
||||
return pow(3, a - 1) * 2 * 2;
|
||||
}
|
||||
if (b == 2) {
|
||||
// 當餘數為 2 時,不做處理
|
||||
return pow(3, a) * 2;
|
||||
}
|
||||
// 當餘數為 0 時,不做處理
|
||||
return pow(3, a);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="max_product_cutting.kt"
|
||||
/* 最大切分乘積:貪婪 */
|
||||
fun maxProductCutting(n: Int): Int {
|
||||
// 當 n <= 3 時,必須切分出一個 1
|
||||
if (n <= 3) {
|
||||
return 1 * (n - 1)
|
||||
}
|
||||
// 貪婪地切分出 3 ,a 為 3 的個數,b 為餘數
|
||||
val a = n / 3
|
||||
val b = n % 3
|
||||
if (b == 1) {
|
||||
// 當餘數為 1 時,將一對 1 * 3 轉化為 2 * 2
|
||||
return 3.0.pow((a - 1).toDouble()).toInt() * 2 * 2
|
||||
}
|
||||
if (b == 2) {
|
||||
// 當餘數為 2 時,不做處理
|
||||
return 3.0.pow(a.toDouble()).toInt() * 2 * 2
|
||||
}
|
||||
// 當餘數為 0 時,不做處理
|
||||
return 3.0.pow(a.toDouble()).toInt()
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="max_product_cutting.rb"
|
||||
[class]{}-[func]{max_product_cutting}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="max_product_cutting.zig"
|
||||
[class]{}-[func]{maxProductCutting}
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=import%20math%0A%0Adef%20max_product_cutting%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%9C%80%E5%A4%A7%E5%88%87%E5%88%86%E4%B9%98%E7%A7%AF%EF%BC%9A%E8%B4%AA%E5%BF%83%22%22%22%0A%20%20%20%20%23%20%E5%BD%93%20n%20%3C%3D%203%20%E6%97%B6%EF%BC%8C%E5%BF%85%E9%A1%BB%E5%88%87%E5%88%86%E5%87%BA%E4%B8%80%E4%B8%AA%201%0A%20%20%20%20if%20n%20%3C%3D%203%3A%0A%20%20%20%20%20%20%20%20return%201%20*%20%28n%20-%201%29%0A%20%20%20%20%23%20%E8%B4%AA%E5%BF%83%E5%9C%B0%E5%88%87%E5%88%86%E5%87%BA%203%20%EF%BC%8Ca%20%E4%B8%BA%203%20%E7%9A%84%E4%B8%AA%E6%95%B0%EF%BC%8Cb%20%E4%B8%BA%E4%BD%99%E6%95%B0%0A%20%20%20%20a,%20b%20%3D%20n%20//%203,%20n%20%25%203%0A%20%20%20%20if%20b%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%BD%93%E4%BD%99%E6%95%B0%E4%B8%BA%201%20%E6%97%B6%EF%BC%8C%E5%B0%86%E4%B8%80%E5%AF%B9%201%20*%203%20%E8%BD%AC%E5%8C%96%E4%B8%BA%202%20*%202%0A%20%20%20%20%20%20%20%20return%20int%28math.pow%283,%20a%20-%201%29%29%20*%202%20*%202%0A%20%20%20%20if%20b%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%BD%93%E4%BD%99%E6%95%B0%E4%B8%BA%202%20%E6%97%B6%EF%BC%8C%E4%B8%8D%E5%81%9A%E5%A4%84%E7%90%86%0A%20%20%20%20%20%20%20%20return%20int%28math.pow%283,%20a%29%29%20*%202%0A%20%20%20%20%23%20%E5%BD%93%E4%BD%99%E6%95%B0%E4%B8%BA%200%20%E6%97%B6%EF%BC%8C%E4%B8%8D%E5%81%9A%E5%A4%84%E7%90%86%0A%20%20%20%20return%20int%28math.pow%283,%20a%29%29%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%2058%0A%0A%20%20%20%20%23%20%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95%0A%20%20%20%20res%20%3D%20max_product_cutting%28n%29%0A%20%20%20%20print%28f%22%E6%9C%80%E5%A4%A7%E5%88%87%E5%88%86%E4%B9%98%E7%A7%AF%E4%B8%BA%20%7Bres%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=import%20math%0A%0Adef%20max_product_cutting%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%9C%80%E5%A4%A7%E5%88%87%E5%88%86%E4%B9%98%E7%A7%AF%EF%BC%9A%E8%B4%AA%E5%BF%83%22%22%22%0A%20%20%20%20%23%20%E5%BD%93%20n%20%3C%3D%203%20%E6%97%B6%EF%BC%8C%E5%BF%85%E9%A1%BB%E5%88%87%E5%88%86%E5%87%BA%E4%B8%80%E4%B8%AA%201%0A%20%20%20%20if%20n%20%3C%3D%203%3A%0A%20%20%20%20%20%20%20%20return%201%20*%20%28n%20-%201%29%0A%20%20%20%20%23%20%E8%B4%AA%E5%BF%83%E5%9C%B0%E5%88%87%E5%88%86%E5%87%BA%203%20%EF%BC%8Ca%20%E4%B8%BA%203%20%E7%9A%84%E4%B8%AA%E6%95%B0%EF%BC%8Cb%20%E4%B8%BA%E4%BD%99%E6%95%B0%0A%20%20%20%20a,%20b%20%3D%20n%20//%203,%20n%20%25%203%0A%20%20%20%20if%20b%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%BD%93%E4%BD%99%E6%95%B0%E4%B8%BA%201%20%E6%97%B6%EF%BC%8C%E5%B0%86%E4%B8%80%E5%AF%B9%201%20*%203%20%E8%BD%AC%E5%8C%96%E4%B8%BA%202%20*%202%0A%20%20%20%20%20%20%20%20return%20int%28math.pow%283,%20a%20-%201%29%29%20*%202%20*%202%0A%20%20%20%20if%20b%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20%23%20%E5%BD%93%E4%BD%99%E6%95%B0%E4%B8%BA%202%20%E6%97%B6%EF%BC%8C%E4%B8%8D%E5%81%9A%E5%A4%84%E7%90%86%0A%20%20%20%20%20%20%20%20return%20int%28math.pow%283,%20a%29%29%20*%202%0A%20%20%20%20%23%20%E5%BD%93%E4%BD%99%E6%95%B0%E4%B8%BA%200%20%E6%97%B6%EF%BC%8C%E4%B8%8D%E5%81%9A%E5%A4%84%E7%90%86%0A%20%20%20%20return%20int%28math.pow%283,%20a%29%29%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%2058%0A%0A%20%20%20%20%23%20%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95%0A%20%20%20%20res%20%3D%20max_product_cutting%28n%29%0A%20%20%20%20print%28f%22%E6%9C%80%E5%A4%A7%E5%88%87%E5%88%86%E4%B9%98%E7%A7%AF%E4%B8%BA%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 15-16 最大切分乘積的計算方法 </p>
|
||||
|
||||
**時間複雜度取決於程式語言的冪運算的實現方法**。以 Python 為例,常用的冪計算函式有三種。
|
||||
|
||||
- 運算子 `**` 和函式 `pow()` 的時間複雜度均為 $O(\log a)$ 。
|
||||
- 函式 `math.pow()` 內部呼叫 C 語言庫的 `pow()` 函式,其執行浮點取冪,時間複雜度為 $O(1)$ 。
|
||||
|
||||
變數 $a$ 和 $b$ 使用常數大小的額外空間,**因此空間複雜度為 $O(1)$** 。
|
||||
|
||||
### 3. 正確性證明
|
||||
|
||||
使用反證法,只分析 $n \geq 3$ 的情況。
|
||||
|
||||
1. **所有因子 $\leq 3$** :假設最優切分方案中存在 $\geq 4$ 的因子 $x$ ,那麼一定可以將其繼續劃分為 $2(x-2)$ ,從而獲得更大的乘積。這與假設矛盾。
|
||||
2. **切分方案不包含 $1$** :假設最優切分方案中存在一個因子 $1$ ,那麼它一定可以合併入另外一個因子中,以獲得更大的乘積。這與假設矛盾。
|
||||
3. **切分方案最多包含兩個 $2$** :假設最優切分方案中包含三個 $2$ ,那麼一定可以替換為兩個 $3$ ,乘積更大。這與假設矛盾。
|
||||
16
zh-Hant/docs/chapter_greedy/summary.md
Normal file
16
zh-Hant/docs/chapter_greedy/summary.md
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 15.5 小結
|
||||
|
||||
- 貪婪演算法通常用於解決最最佳化問題,其原理是在每個決策階段都做出區域性最優的決策,以期獲得全域性最優解。
|
||||
- 貪婪演算法會迭代地做出一個又一個的貪婪選擇,每輪都將問題轉化成一個規模更小的子問題,直到問題被解決。
|
||||
- 貪婪演算法不僅實現簡單,還具有很高的解題效率。相比於動態規劃,貪婪演算法的時間複雜度通常更低。
|
||||
- 在零錢兌換問題中,對於某些硬幣組合,貪婪演算法可以保證找到最優解;對於另外一些硬幣組合則不然,貪婪演算法可能找到很差的解。
|
||||
- 適合用貪婪演算法求解的問題具有兩大性質:貪婪選擇性質和最優子結構。貪婪選擇性質代表貪婪策略的有效性。
|
||||
- 對於某些複雜問題,貪婪選擇性質的證明並不簡單。相對來說,證偽更加容易,例如零錢兌換問題。
|
||||
- 求解貪婪問題主要分為三步:問題分析、確定貪婪策略、正確性證明。其中,確定貪婪策略是核心步驟,正確性證明往往是難點。
|
||||
- 分數背包問題在 0-1 背包的基礎上,允許選擇物品的一部分,因此可使用貪婪演算法求解。貪婪策略的正確性可以使用反證法來證明。
|
||||
- 最大容量問題可使用窮舉法求解,時間複雜度為 $O(n^2)$ 。透過設計貪婪策略,每輪向內移動短板,可將時間複雜度最佳化至 $O(n)$ 。
|
||||
- 在最大切分乘積問題中,我們先後推理出兩個貪婪策略:$\geq 4$ 的整數都應該繼續切分,最優切分因子為 $3$ 。程式碼中包含冪運算,時間複雜度取決於冪運算實現方法,通常為 $O(1)$ 或 $O(\log n)$ 。
|
||||
972
zh-Hant/docs/chapter_hashing/hash_algorithm.md
Normal file
972
zh-Hant/docs/chapter_hashing/hash_algorithm.md
Normal file
@@ -0,0 +1,972 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 6.3 雜湊演算法
|
||||
|
||||
前兩節介紹了雜湊表的工作原理和雜湊衝突的處理方法。然而無論是開放定址還是鏈式位址,**它們只能保證雜湊表可以在發生衝突時正常工作,而無法減少雜湊衝突的發生**。
|
||||
|
||||
如果雜湊衝突過於頻繁,雜湊表的效能則會急劇劣化。如圖 6-8 所示,對於鏈式位址雜湊表,理想情況下鍵值對均勻分佈在各個桶中,達到最佳查詢效率;最差情況下所有鍵值對都儲存到同一個桶中,時間複雜度退化至 $O(n)$ 。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 6-8 雜湊衝突的最佳情況與最差情況 </p>
|
||||
|
||||
**鍵值對的分佈情況由雜湊函式決定**。回憶雜湊函式的計算步驟,先計算雜湊值,再對陣列長度取模:
|
||||
|
||||
```shell
|
||||
index = hash(key) % capacity
|
||||
```
|
||||
|
||||
觀察以上公式,當雜湊表容量 `capacity` 固定時,**雜湊演算法 `hash()` 決定了輸出值**,進而決定了鍵值對在雜湊表中的分佈情況。
|
||||
|
||||
這意味著,為了降低雜湊衝突的發生機率,我們應當將注意力集中在雜湊演算法 `hash()` 的設計上。
|
||||
|
||||
## 6.3.1 雜湊演算法的目標
|
||||
|
||||
為了實現“既快又穩”的雜湊表資料結構,雜湊演算法應具備以下特點。
|
||||
|
||||
- **確定性**:對於相同的輸入,雜湊演算法應始終產生相同的輸出。這樣才能確保雜湊表是可靠的。
|
||||
- **效率高**:計算雜湊值的過程應該足夠快。計算開銷越小,雜湊表的實用性越高。
|
||||
- **均勻分佈**:雜湊演算法應使得鍵值對均勻分佈在雜湊表中。分佈越均勻,雜湊衝突的機率就越低。
|
||||
|
||||
實際上,雜湊演算法除了可以用於實現雜湊表,還廣泛應用於其他領域中。
|
||||
|
||||
- **密碼儲存**:為了保護使用者密碼的安全,系統通常不會直接儲存使用者的明文密碼,而是儲存密碼的雜湊值。當用戶輸入密碼時,系統會對輸入的密碼計算雜湊值,然後與儲存的雜湊值進行比較。如果兩者匹配,那麼密碼就被視為正確。
|
||||
- **資料完整性檢查**:資料傳送方可以計算資料的雜湊值並將其一同傳送;接收方可以重新計算接收到的資料的雜湊值,並與接收到的雜湊值進行比較。如果兩者匹配,那麼資料就被視為完整。
|
||||
|
||||
對於密碼學的相關應用,為了防止從雜湊值推導出原始密碼等逆向工程,雜湊演算法需要具備更高等級的安全特性。
|
||||
|
||||
- **單向性**:無法透過雜湊值反推出關於輸入資料的任何資訊。
|
||||
- **抗碰撞性**:應當極難找到兩個不同的輸入,使得它們的雜湊值相同。
|
||||
- **雪崩效應**:輸入的微小變化應當導致輸出的顯著且不可預測的變化。
|
||||
|
||||
請注意,**“均勻分佈”與“抗碰撞性”是兩個獨立的概念**,滿足均勻分佈不一定滿足抗碰撞性。例如,在隨機輸入 `key` 下,雜湊函式 `key % 100` 可以產生均勻分佈的輸出。然而該雜湊演算法過於簡單,所有後兩位相等的 `key` 的輸出都相同,因此我們可以很容易地從雜湊值反推出可用的 `key` ,從而破解密碼。
|
||||
|
||||
## 6.3.2 雜湊演算法的設計
|
||||
|
||||
雜湊演算法的設計是一個需要考慮許多因素的複雜問題。然而對於某些要求不高的場景,我們也能設計一些簡單的雜湊演算法。
|
||||
|
||||
- **加法雜湊**:對輸入的每個字元的 ASCII 碼進行相加,將得到的總和作為雜湊值。
|
||||
- **乘法雜湊**:利用乘法的不相關性,每輪乘以一個常數,將各個字元的 ASCII 碼累積到雜湊值中。
|
||||
- **互斥或雜湊**:將輸入資料的每個元素透過互斥或操作累積到一個雜湊值中。
|
||||
- **旋轉雜湊**:將每個字元的 ASCII 碼累積到一個雜湊值中,每次累積之前都會對雜湊值進行旋轉操作。
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="simple_hash.py"
|
||||
def add_hash(key: str) -> int:
|
||||
"""加法雜湊"""
|
||||
hash = 0
|
||||
modulus = 1000000007
|
||||
for c in key:
|
||||
hash += ord(c)
|
||||
return hash % modulus
|
||||
|
||||
def mul_hash(key: str) -> int:
|
||||
"""乘法雜湊"""
|
||||
hash = 0
|
||||
modulus = 1000000007
|
||||
for c in key:
|
||||
hash = 31 * hash + ord(c)
|
||||
return hash % modulus
|
||||
|
||||
def xor_hash(key: str) -> int:
|
||||
"""互斥或雜湊"""
|
||||
hash = 0
|
||||
modulus = 1000000007
|
||||
for c in key:
|
||||
hash ^= ord(c)
|
||||
return hash % modulus
|
||||
|
||||
def rot_hash(key: str) -> int:
|
||||
"""旋轉雜湊"""
|
||||
hash = 0
|
||||
modulus = 1000000007
|
||||
for c in key:
|
||||
hash = (hash << 4) ^ (hash >> 28) ^ ord(c)
|
||||
return hash % modulus
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="simple_hash.cpp"
|
||||
/* 加法雜湊 */
|
||||
int addHash(string key) {
|
||||
long long hash = 0;
|
||||
const int MODULUS = 1000000007;
|
||||
for (unsigned char c : key) {
|
||||
hash = (hash + (int)c) % MODULUS;
|
||||
}
|
||||
return (int)hash;
|
||||
}
|
||||
|
||||
/* 乘法雜湊 */
|
||||
int mulHash(string key) {
|
||||
long long hash = 0;
|
||||
const int MODULUS = 1000000007;
|
||||
for (unsigned char c : key) {
|
||||
hash = (31 * hash + (int)c) % MODULUS;
|
||||
}
|
||||
return (int)hash;
|
||||
}
|
||||
|
||||
/* 互斥或雜湊 */
|
||||
int xorHash(string key) {
|
||||
int hash = 0;
|
||||
const int MODULUS = 1000000007;
|
||||
for (unsigned char c : key) {
|
||||
hash ^= (int)c;
|
||||
}
|
||||
return hash & MODULUS;
|
||||
}
|
||||
|
||||
/* 旋轉雜湊 */
|
||||
int rotHash(string key) {
|
||||
long long hash = 0;
|
||||
const int MODULUS = 1000000007;
|
||||
for (unsigned char c : key) {
|
||||
hash = ((hash << 4) ^ (hash >> 28) ^ (int)c) % MODULUS;
|
||||
}
|
||||
return (int)hash;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="simple_hash.java"
|
||||
/* 加法雜湊 */
|
||||
int addHash(String key) {
|
||||
long hash = 0;
|
||||
final int MODULUS = 1000000007;
|
||||
for (char c : key.toCharArray()) {
|
||||
hash = (hash + (int) c) % MODULUS;
|
||||
}
|
||||
return (int) hash;
|
||||
}
|
||||
|
||||
/* 乘法雜湊 */
|
||||
int mulHash(String key) {
|
||||
long hash = 0;
|
||||
final int MODULUS = 1000000007;
|
||||
for (char c : key.toCharArray()) {
|
||||
hash = (31 * hash + (int) c) % MODULUS;
|
||||
}
|
||||
return (int) hash;
|
||||
}
|
||||
|
||||
/* 互斥或雜湊 */
|
||||
int xorHash(String key) {
|
||||
int hash = 0;
|
||||
final int MODULUS = 1000000007;
|
||||
for (char c : key.toCharArray()) {
|
||||
hash ^= (int) c;
|
||||
}
|
||||
return hash & MODULUS;
|
||||
}
|
||||
|
||||
/* 旋轉雜湊 */
|
||||
int rotHash(String key) {
|
||||
long hash = 0;
|
||||
final int MODULUS = 1000000007;
|
||||
for (char c : key.toCharArray()) {
|
||||
hash = ((hash << 4) ^ (hash >> 28) ^ (int) c) % MODULUS;
|
||||
}
|
||||
return (int) hash;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="simple_hash.cs"
|
||||
/* 加法雜湊 */
|
||||
int AddHash(string key) {
|
||||
long hash = 0;
|
||||
const int MODULUS = 1000000007;
|
||||
foreach (char c in key) {
|
||||
hash = (hash + c) % MODULUS;
|
||||
}
|
||||
return (int)hash;
|
||||
}
|
||||
|
||||
/* 乘法雜湊 */
|
||||
int MulHash(string key) {
|
||||
long hash = 0;
|
||||
const int MODULUS = 1000000007;
|
||||
foreach (char c in key) {
|
||||
hash = (31 * hash + c) % MODULUS;
|
||||
}
|
||||
return (int)hash;
|
||||
}
|
||||
|
||||
/* 互斥或雜湊 */
|
||||
int XorHash(string key) {
|
||||
int hash = 0;
|
||||
const int MODULUS = 1000000007;
|
||||
foreach (char c in key) {
|
||||
hash ^= c;
|
||||
}
|
||||
return hash & MODULUS;
|
||||
}
|
||||
|
||||
/* 旋轉雜湊 */
|
||||
int RotHash(string key) {
|
||||
long hash = 0;
|
||||
const int MODULUS = 1000000007;
|
||||
foreach (char c in key) {
|
||||
hash = ((hash << 4) ^ (hash >> 28) ^ c) % MODULUS;
|
||||
}
|
||||
return (int)hash;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="simple_hash.go"
|
||||
/* 加法雜湊 */
|
||||
func addHash(key string) int {
|
||||
var hash int64
|
||||
var modulus int64
|
||||
|
||||
modulus = 1000000007
|
||||
for _, b := range []byte(key) {
|
||||
hash = (hash + int64(b)) % modulus
|
||||
}
|
||||
return int(hash)
|
||||
}
|
||||
|
||||
/* 乘法雜湊 */
|
||||
func mulHash(key string) int {
|
||||
var hash int64
|
||||
var modulus int64
|
||||
|
||||
modulus = 1000000007
|
||||
for _, b := range []byte(key) {
|
||||
hash = (31*hash + int64(b)) % modulus
|
||||
}
|
||||
return int(hash)
|
||||
}
|
||||
|
||||
/* 互斥或雜湊 */
|
||||
func xorHash(key string) int {
|
||||
hash := 0
|
||||
modulus := 1000000007
|
||||
for _, b := range []byte(key) {
|
||||
fmt.Println(int(b))
|
||||
hash ^= int(b)
|
||||
hash = (31*hash + int(b)) % modulus
|
||||
}
|
||||
return hash & modulus
|
||||
}
|
||||
|
||||
/* 旋轉雜湊 */
|
||||
func rotHash(key string) int {
|
||||
var hash int64
|
||||
var modulus int64
|
||||
|
||||
modulus = 1000000007
|
||||
for _, b := range []byte(key) {
|
||||
hash = ((hash << 4) ^ (hash >> 28) ^ int64(b)) % modulus
|
||||
}
|
||||
return int(hash)
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="simple_hash.swift"
|
||||
/* 加法雜湊 */
|
||||
func addHash(key: String) -> Int {
|
||||
var hash = 0
|
||||
let MODULUS = 1_000_000_007
|
||||
for c in key {
|
||||
for scalar in c.unicodeScalars {
|
||||
hash = (hash + Int(scalar.value)) % MODULUS
|
||||
}
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
/* 乘法雜湊 */
|
||||
func mulHash(key: String) -> Int {
|
||||
var hash = 0
|
||||
let MODULUS = 1_000_000_007
|
||||
for c in key {
|
||||
for scalar in c.unicodeScalars {
|
||||
hash = (31 * hash + Int(scalar.value)) % MODULUS
|
||||
}
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
/* 互斥或雜湊 */
|
||||
func xorHash(key: String) -> Int {
|
||||
var hash = 0
|
||||
let MODULUS = 1_000_000_007
|
||||
for c in key {
|
||||
for scalar in c.unicodeScalars {
|
||||
hash ^= Int(scalar.value)
|
||||
}
|
||||
}
|
||||
return hash & MODULUS
|
||||
}
|
||||
|
||||
/* 旋轉雜湊 */
|
||||
func rotHash(key: String) -> Int {
|
||||
var hash = 0
|
||||
let MODULUS = 1_000_000_007
|
||||
for c in key {
|
||||
for scalar in c.unicodeScalars {
|
||||
hash = ((hash << 4) ^ (hash >> 28) ^ Int(scalar.value)) % MODULUS
|
||||
}
|
||||
}
|
||||
return hash
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="simple_hash.js"
|
||||
/* 加法雜湊 */
|
||||
function addHash(key) {
|
||||
let hash = 0;
|
||||
const MODULUS = 1000000007;
|
||||
for (const c of key) {
|
||||
hash = (hash + c.charCodeAt(0)) % MODULUS;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/* 乘法雜湊 */
|
||||
function mulHash(key) {
|
||||
let hash = 0;
|
||||
const MODULUS = 1000000007;
|
||||
for (const c of key) {
|
||||
hash = (31 * hash + c.charCodeAt(0)) % MODULUS;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/* 互斥或雜湊 */
|
||||
function xorHash(key) {
|
||||
let hash = 0;
|
||||
const MODULUS = 1000000007;
|
||||
for (const c of key) {
|
||||
hash ^= c.charCodeAt(0);
|
||||
}
|
||||
return hash & MODULUS;
|
||||
}
|
||||
|
||||
/* 旋轉雜湊 */
|
||||
function rotHash(key) {
|
||||
let hash = 0;
|
||||
const MODULUS = 1000000007;
|
||||
for (const c of key) {
|
||||
hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="simple_hash.ts"
|
||||
/* 加法雜湊 */
|
||||
function addHash(key: string): number {
|
||||
let hash = 0;
|
||||
const MODULUS = 1000000007;
|
||||
for (const c of key) {
|
||||
hash = (hash + c.charCodeAt(0)) % MODULUS;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/* 乘法雜湊 */
|
||||
function mulHash(key: string): number {
|
||||
let hash = 0;
|
||||
const MODULUS = 1000000007;
|
||||
for (const c of key) {
|
||||
hash = (31 * hash + c.charCodeAt(0)) % MODULUS;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/* 互斥或雜湊 */
|
||||
function xorHash(key: string): number {
|
||||
let hash = 0;
|
||||
const MODULUS = 1000000007;
|
||||
for (const c of key) {
|
||||
hash ^= c.charCodeAt(0);
|
||||
}
|
||||
return hash & MODULUS;
|
||||
}
|
||||
|
||||
/* 旋轉雜湊 */
|
||||
function rotHash(key: string): number {
|
||||
let hash = 0;
|
||||
const MODULUS = 1000000007;
|
||||
for (const c of key) {
|
||||
hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="simple_hash.dart"
|
||||
/* 加法雜湊 */
|
||||
int addHash(String key) {
|
||||
int hash = 0;
|
||||
final int MODULUS = 1000000007;
|
||||
for (int i = 0; i < key.length; i++) {
|
||||
hash = (hash + key.codeUnitAt(i)) % MODULUS;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/* 乘法雜湊 */
|
||||
int mulHash(String key) {
|
||||
int hash = 0;
|
||||
final int MODULUS = 1000000007;
|
||||
for (int i = 0; i < key.length; i++) {
|
||||
hash = (31 * hash + key.codeUnitAt(i)) % MODULUS;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/* 互斥或雜湊 */
|
||||
int xorHash(String key) {
|
||||
int hash = 0;
|
||||
final int MODULUS = 1000000007;
|
||||
for (int i = 0; i < key.length; i++) {
|
||||
hash ^= key.codeUnitAt(i);
|
||||
}
|
||||
return hash & MODULUS;
|
||||
}
|
||||
|
||||
/* 旋轉雜湊 */
|
||||
int rotHash(String key) {
|
||||
int hash = 0;
|
||||
final int MODULUS = 1000000007;
|
||||
for (int i = 0; i < key.length; i++) {
|
||||
hash = ((hash << 4) ^ (hash >> 28) ^ key.codeUnitAt(i)) % MODULUS;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="simple_hash.rs"
|
||||
/* 加法雜湊 */
|
||||
fn add_hash(key: &str) -> i32 {
|
||||
let mut hash = 0_i64;
|
||||
const MODULUS: i64 = 1000000007;
|
||||
|
||||
for c in key.chars() {
|
||||
hash = (hash + c as i64) % MODULUS;
|
||||
}
|
||||
|
||||
hash as i32
|
||||
}
|
||||
|
||||
/* 乘法雜湊 */
|
||||
fn mul_hash(key: &str) -> i32 {
|
||||
let mut hash = 0_i64;
|
||||
const MODULUS: i64 = 1000000007;
|
||||
|
||||
for c in key.chars() {
|
||||
hash = (31 * hash + c as i64) % MODULUS;
|
||||
}
|
||||
|
||||
hash as i32
|
||||
}
|
||||
|
||||
/* 互斥或雜湊 */
|
||||
fn xor_hash(key: &str) -> i32 {
|
||||
let mut hash = 0_i64;
|
||||
const MODULUS: i64 = 1000000007;
|
||||
|
||||
for c in key.chars() {
|
||||
hash ^= c as i64;
|
||||
}
|
||||
|
||||
(hash & MODULUS) as i32
|
||||
}
|
||||
|
||||
/* 旋轉雜湊 */
|
||||
fn rot_hash(key: &str) -> i32 {
|
||||
let mut hash = 0_i64;
|
||||
const MODULUS: i64 = 1000000007;
|
||||
|
||||
for c in key.chars() {
|
||||
hash = ((hash << 4) ^ (hash >> 28) ^ c as i64) % MODULUS;
|
||||
}
|
||||
|
||||
hash as i32
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="simple_hash.c"
|
||||
/* 加法雜湊 */
|
||||
int addHash(char *key) {
|
||||
long long hash = 0;
|
||||
const int MODULUS = 1000000007;
|
||||
for (int i = 0; i < strlen(key); i++) {
|
||||
hash = (hash + (unsigned char)key[i]) % MODULUS;
|
||||
}
|
||||
return (int)hash;
|
||||
}
|
||||
|
||||
/* 乘法雜湊 */
|
||||
int mulHash(char *key) {
|
||||
long long hash = 0;
|
||||
const int MODULUS = 1000000007;
|
||||
for (int i = 0; i < strlen(key); i++) {
|
||||
hash = (31 * hash + (unsigned char)key[i]) % MODULUS;
|
||||
}
|
||||
return (int)hash;
|
||||
}
|
||||
|
||||
/* 互斥或雜湊 */
|
||||
int xorHash(char *key) {
|
||||
int hash = 0;
|
||||
const int MODULUS = 1000000007;
|
||||
|
||||
for (int i = 0; i < strlen(key); i++) {
|
||||
hash ^= (unsigned char)key[i];
|
||||
}
|
||||
return hash & MODULUS;
|
||||
}
|
||||
|
||||
/* 旋轉雜湊 */
|
||||
int rotHash(char *key) {
|
||||
long long hash = 0;
|
||||
const int MODULUS = 1000000007;
|
||||
for (int i = 0; i < strlen(key); i++) {
|
||||
hash = ((hash << 4) ^ (hash >> 28) ^ (unsigned char)key[i]) % MODULUS;
|
||||
}
|
||||
|
||||
return (int)hash;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="simple_hash.kt"
|
||||
/* 加法雜湊 */
|
||||
fun addHash(key: String): Int {
|
||||
var hash = 0L
|
||||
for (c in key.toCharArray()) {
|
||||
hash = (hash + c.code) % MODULUS
|
||||
}
|
||||
return hash.toInt()
|
||||
}
|
||||
|
||||
/* 乘法雜湊 */
|
||||
fun mulHash(key: String): Int {
|
||||
var hash = 0L
|
||||
for (c in key.toCharArray()) {
|
||||
hash = (31 * hash + c.code) % MODULUS
|
||||
}
|
||||
return hash.toInt()
|
||||
}
|
||||
|
||||
/* 互斥或雜湊 */
|
||||
fun xorHash(key: String): Int {
|
||||
var hash = 0
|
||||
for (c in key.toCharArray()) {
|
||||
hash = hash xor c.code
|
||||
}
|
||||
return hash and MODULUS
|
||||
}
|
||||
|
||||
/* 旋轉雜湊 */
|
||||
fun rotHash(key: String): Int {
|
||||
var hash = 0L
|
||||
for (c in key.toCharArray()) {
|
||||
hash = ((hash shl 4) xor (hash shr 28) xor c.code.toLong()) % MODULUS
|
||||
}
|
||||
return hash.toInt()
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="simple_hash.rb"
|
||||
[class]{}-[func]{add_hash}
|
||||
|
||||
[class]{}-[func]{mul_hash}
|
||||
|
||||
[class]{}-[func]{xor_hash}
|
||||
|
||||
[class]{}-[func]{rot_hash}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="simple_hash.zig"
|
||||
[class]{}-[func]{addHash}
|
||||
|
||||
[class]{}-[func]{mulHash}
|
||||
|
||||
[class]{}-[func]{xorHash}
|
||||
|
||||
[class]{}-[func]{rotHash}
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20add_hash%28key%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%8A%A0%E6%B3%95%E5%93%88%E5%B8%8C%22%22%22%0A%20%20%20%20hash%20%3D%200%0A%20%20%20%20modulus%20%3D%201000000007%0A%20%20%20%20for%20c%20in%20key%3A%0A%20%20%20%20%20%20%20%20hash%20%2B%3D%20ord%28c%29%0A%20%20%20%20return%20hash%20%25%20modulus%0A%0A%0Adef%20mul_hash%28key%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%B9%98%E6%B3%95%E5%93%88%E5%B8%8C%22%22%22%0A%20%20%20%20hash%20%3D%200%0A%20%20%20%20modulus%20%3D%201000000007%0A%20%20%20%20for%20c%20in%20key%3A%0A%20%20%20%20%20%20%20%20hash%20%3D%2031%20*%20hash%20%2B%20ord%28c%29%0A%20%20%20%20return%20hash%20%25%20modulus%0A%0A%0Adef%20xor_hash%28key%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%BC%82%E6%88%96%E5%93%88%E5%B8%8C%22%22%22%0A%20%20%20%20hash%20%3D%200%0A%20%20%20%20modulus%20%3D%201000000007%0A%20%20%20%20for%20c%20in%20key%3A%0A%20%20%20%20%20%20%20%20hash%20%5E%3D%20ord%28c%29%0A%20%20%20%20return%20hash%20%25%20modulus%0A%0A%0Adef%20rot_hash%28key%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%97%8B%E8%BD%AC%E5%93%88%E5%B8%8C%22%22%22%0A%20%20%20%20hash%20%3D%200%0A%20%20%20%20modulus%20%3D%201000000007%0A%20%20%20%20for%20c%20in%20key%3A%0A%20%20%20%20%20%20%20%20hash%20%3D%20%28hash%20%3C%3C%204%29%20%5E%20%28hash%20%3E%3E%2028%29%20%5E%20ord%28c%29%0A%20%20%20%20return%20hash%20%25%20modulus%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20key%20%3D%20%22Hello%20%E7%AE%97%E6%B3%95%22%0A%0A%20%20%20%20hash%20%3D%20add_hash%28key%29%0A%20%20%20%20print%28f%22%E5%8A%A0%E6%B3%95%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20%7Bhash%7D%22%29%0A%0A%20%20%20%20hash%20%3D%20mul_hash%28key%29%0A%20%20%20%20print%28f%22%E4%B9%98%E6%B3%95%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20%7Bhash%7D%22%29%0A%0A%20%20%20%20hash%20%3D%20xor_hash%28key%29%0A%20%20%20%20print%28f%22%E5%BC%82%E6%88%96%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20%7Bhash%7D%22%29%0A%0A%20%20%20%20hash%20%3D%20rot_hash%28key%29%0A%20%20%20%20print%28f%22%E6%97%8B%E8%BD%AC%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20%7Bhash%7D%22%29%0A&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20add_hash%28key%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%8A%A0%E6%B3%95%E5%93%88%E5%B8%8C%22%22%22%0A%20%20%20%20hash%20%3D%200%0A%20%20%20%20modulus%20%3D%201000000007%0A%20%20%20%20for%20c%20in%20key%3A%0A%20%20%20%20%20%20%20%20hash%20%2B%3D%20ord%28c%29%0A%20%20%20%20return%20hash%20%25%20modulus%0A%0A%0Adef%20mul_hash%28key%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E4%B9%98%E6%B3%95%E5%93%88%E5%B8%8C%22%22%22%0A%20%20%20%20hash%20%3D%200%0A%20%20%20%20modulus%20%3D%201000000007%0A%20%20%20%20for%20c%20in%20key%3A%0A%20%20%20%20%20%20%20%20hash%20%3D%2031%20*%20hash%20%2B%20ord%28c%29%0A%20%20%20%20return%20hash%20%25%20modulus%0A%0A%0Adef%20xor_hash%28key%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%BC%82%E6%88%96%E5%93%88%E5%B8%8C%22%22%22%0A%20%20%20%20hash%20%3D%200%0A%20%20%20%20modulus%20%3D%201000000007%0A%20%20%20%20for%20c%20in%20key%3A%0A%20%20%20%20%20%20%20%20hash%20%5E%3D%20ord%28c%29%0A%20%20%20%20return%20hash%20%25%20modulus%0A%0A%0Adef%20rot_hash%28key%3A%20str%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%97%8B%E8%BD%AC%E5%93%88%E5%B8%8C%22%22%22%0A%20%20%20%20hash%20%3D%200%0A%20%20%20%20modulus%20%3D%201000000007%0A%20%20%20%20for%20c%20in%20key%3A%0A%20%20%20%20%20%20%20%20hash%20%3D%20%28hash%20%3C%3C%204%29%20%5E%20%28hash%20%3E%3E%2028%29%20%5E%20ord%28c%29%0A%20%20%20%20return%20hash%20%25%20modulus%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20key%20%3D%20%22Hello%20%E7%AE%97%E6%B3%95%22%0A%0A%20%20%20%20hash%20%3D%20add_hash%28key%29%0A%20%20%20%20print%28f%22%E5%8A%A0%E6%B3%95%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20%7Bhash%7D%22%29%0A%0A%20%20%20%20hash%20%3D%20mul_hash%28key%29%0A%20%20%20%20print%28f%22%E4%B9%98%E6%B3%95%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20%7Bhash%7D%22%29%0A%0A%20%20%20%20hash%20%3D%20xor_hash%28key%29%0A%20%20%20%20print%28f%22%E5%BC%82%E6%88%96%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20%7Bhash%7D%22%29%0A%0A%20%20%20%20hash%20%3D%20rot_hash%28key%29%0A%20%20%20%20print%28f%22%E6%97%8B%E8%BD%AC%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20%7Bhash%7D%22%29%0A&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
觀察發現,每種雜湊演算法的最後一步都是對大質數 $1000000007$ 取模,以確保雜湊值在合適的範圍內。值得思考的是,為什麼要強調對質數取模,或者說對合數取模的弊端是什麼?這是一個有趣的問題。
|
||||
|
||||
先丟擲結論:**使用大質數作為模數,可以最大化地保證雜湊值的均勻分佈**。因為質數不與其他數字存在公約數,可以減少因取模操作而產生的週期性模式,從而避免雜湊衝突。
|
||||
|
||||
舉個例子,假設我們選擇合數 $9$ 作為模數,它可以被 $3$ 整除,那麼所有可以被 $3$ 整除的 `key` 都會被對映到 $0$、$3$、$6$ 這三個雜湊值。
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\text{modulus} & = 9 \newline
|
||||
\text{key} & = \{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \dots \} \newline
|
||||
\text{hash} & = \{ 0, 3, 6, 0, 3, 6, 0, 3, 6, 0, 3, 6,\dots \}
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
如果輸入 `key` 恰好滿足這種等差數列的資料分佈,那麼雜湊值就會出現聚堆積,從而加重雜湊衝突。現在,假設將 `modulus` 替換為質數 $13$ ,由於 `key` 和 `modulus` 之間不存在公約數,因此輸出的雜湊值的均勻性會明顯提升。
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\text{modulus} & = 13 \newline
|
||||
\text{key} & = \{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \dots \} \newline
|
||||
\text{hash} & = \{ 0, 3, 6, 9, 12, 2, 5, 8, 11, 1, 4, 7, \dots \}
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
值得說明的是,如果能夠保證 `key` 是隨機均勻分佈的,那麼選擇質數或者合數作為模數都可以,它們都能輸出均勻分佈的雜湊值。而當 `key` 的分佈存在某種週期性時,對合數取模更容易出現聚集現象。
|
||||
|
||||
總而言之,我們通常選取質數作為模數,並且這個質數最好足夠大,以儘可能消除週期性模式,提升雜湊演算法的穩健性。
|
||||
|
||||
## 6.3.3 常見雜湊演算法
|
||||
|
||||
不難發現,以上介紹的簡單雜湊演算法都比較“脆弱”,遠遠沒有達到雜湊演算法的設計目標。例如,由於加法和互斥或滿足交換律,因此加法雜湊和互斥或雜湊無法區分內容相同但順序不同的字串,這可能會加劇雜湊衝突,並引起一些安全問題。
|
||||
|
||||
在實際中,我們通常會用一些標準雜湊演算法,例如 MD5、SHA-1、SHA-2 和 SHA-3 等。它們可以將任意長度的輸入資料對映到恆定長度的雜湊值。
|
||||
|
||||
近一個世紀以來,雜湊演算法處在不斷升級與最佳化的過程中。一部分研究人員努力提升雜湊演算法的效能,另一部分研究人員和駭客則致力於尋找雜湊演算法的安全性問題。表 6-2 展示了在實際應用中常見的雜湊演算法。
|
||||
|
||||
- MD5 和 SHA-1 已多次被成功攻擊,因此它們被各類安全應用棄用。
|
||||
- SHA-2 系列中的 SHA-256 是最安全的雜湊演算法之一,仍未出現成功的攻擊案例,因此常用在各類安全應用與協議中。
|
||||
- SHA-3 相較 SHA-2 的實現開銷更低、計算效率更高,但目前使用覆蓋度不如 SHA-2 系列。
|
||||
|
||||
<p align="center"> 表 6-2 常見的雜湊演算法 </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
| | 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 |
|
||||
|
||||
</div>
|
||||
|
||||
## 6.3.4 資料結構的雜湊值
|
||||
|
||||
我們知道,雜湊表的 `key` 可以是整數、小數或字串等資料型別。程式語言通常會為這些資料型別提供內建的雜湊演算法,用於計算雜湊表中的桶索引。以 Python 為例,我們可以呼叫 `hash()` 函式來計算各種資料型別的雜湊值。
|
||||
|
||||
- 整數和布林量的雜湊值就是其本身。
|
||||
- 浮點數和字串的雜湊值計算較為複雜,有興趣的讀者請自行學習。
|
||||
- 元組的雜湊值是對其中每一個元素進行雜湊,然後將這些雜湊值組合起來,得到單一的雜湊值。
|
||||
- 物件的雜湊值基於其記憶體位址生成。透過重寫物件的雜湊方法,可實現基於內容生成雜湊值。
|
||||
|
||||
!!! tip
|
||||
|
||||
請注意,不同程式語言的內建雜湊值計算函式的定義和方法不同。
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="built_in_hash.py"
|
||||
num = 3
|
||||
hash_num = hash(num)
|
||||
# 整數 3 的雜湊值為 3
|
||||
|
||||
bol = True
|
||||
hash_bol = hash(bol)
|
||||
# 布林量 True 的雜湊值為 1
|
||||
|
||||
dec = 3.14159
|
||||
hash_dec = hash(dec)
|
||||
# 小數 3.14159 的雜湊值為 326484311674566659
|
||||
|
||||
str = "Hello 演算法"
|
||||
hash_str = hash(str)
|
||||
# 字串“Hello 演算法”的雜湊值為 4617003410720528961
|
||||
|
||||
tup = (12836, "小哈")
|
||||
hash_tup = hash(tup)
|
||||
# 元組 (12836, '小哈') 的雜湊值為 1029005403108185979
|
||||
|
||||
obj = ListNode(0)
|
||||
hash_obj = hash(obj)
|
||||
# 節點物件 <ListNode object at 0x1058fd810> 的雜湊值為 274267521
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="built_in_hash.cpp"
|
||||
int num = 3;
|
||||
size_t hashNum = hash<int>()(num);
|
||||
// 整數 3 的雜湊值為 3
|
||||
|
||||
bool bol = true;
|
||||
size_t hashBol = hash<bool>()(bol);
|
||||
// 布林量 1 的雜湊值為 1
|
||||
|
||||
double dec = 3.14159;
|
||||
size_t hashDec = hash<double>()(dec);
|
||||
// 小數 3.14159 的雜湊值為 4614256650576692846
|
||||
|
||||
string str = "Hello 演算法";
|
||||
size_t hashStr = hash<string>()(str);
|
||||
// 字串“Hello 演算法”的雜湊值為 15466937326284535026
|
||||
|
||||
// 在 C++ 中,內建 std:hash() 僅提供基本資料型別的雜湊值計算
|
||||
// 陣列、物件的雜湊值計算需要自行實現
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="built_in_hash.java"
|
||||
int num = 3;
|
||||
int hashNum = Integer.hashCode(num);
|
||||
// 整數 3 的雜湊值為 3
|
||||
|
||||
boolean bol = true;
|
||||
int hashBol = Boolean.hashCode(bol);
|
||||
// 布林量 true 的雜湊值為 1231
|
||||
|
||||
double dec = 3.14159;
|
||||
int hashDec = Double.hashCode(dec);
|
||||
// 小數 3.14159 的雜湊值為 -1340954729
|
||||
|
||||
String str = "Hello 演算法";
|
||||
int hashStr = str.hashCode();
|
||||
// 字串“Hello 演算法”的雜湊值為 -727081396
|
||||
|
||||
Object[] arr = { 12836, "小哈" };
|
||||
int hashTup = Arrays.hashCode(arr);
|
||||
// 陣列 [12836, 小哈] 的雜湊值為 1151158
|
||||
|
||||
ListNode obj = new ListNode(0);
|
||||
int hashObj = obj.hashCode();
|
||||
// 節點物件 utils.ListNode@7dc5e7b4 的雜湊值為 2110121908
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="built_in_hash.cs"
|
||||
int num = 3;
|
||||
int hashNum = num.GetHashCode();
|
||||
// 整數 3 的雜湊值為 3;
|
||||
|
||||
bool bol = true;
|
||||
int hashBol = bol.GetHashCode();
|
||||
// 布林量 true 的雜湊值為 1;
|
||||
|
||||
double dec = 3.14159;
|
||||
int hashDec = dec.GetHashCode();
|
||||
// 小數 3.14159 的雜湊值為 -1340954729;
|
||||
|
||||
string str = "Hello 演算法";
|
||||
int hashStr = str.GetHashCode();
|
||||
// 字串“Hello 演算法”的雜湊值為 -586107568;
|
||||
|
||||
object[] arr = [12836, "小哈"];
|
||||
int hashTup = arr.GetHashCode();
|
||||
// 陣列 [12836, 小哈] 的雜湊值為 42931033;
|
||||
|
||||
ListNode obj = new(0);
|
||||
int hashObj = obj.GetHashCode();
|
||||
// 節點物件 0 的雜湊值為 39053774;
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="built_in_hash.go"
|
||||
// Go 未提供內建 hash code 函式
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="built_in_hash.swift"
|
||||
let num = 3
|
||||
let hashNum = num.hashValue
|
||||
// 整數 3 的雜湊值為 9047044699613009734
|
||||
|
||||
let bol = true
|
||||
let hashBol = bol.hashValue
|
||||
// 布林量 true 的雜湊值為 -4431640247352757451
|
||||
|
||||
let dec = 3.14159
|
||||
let hashDec = dec.hashValue
|
||||
// 小數 3.14159 的雜湊值為 -2465384235396674631
|
||||
|
||||
let str = "Hello 演算法"
|
||||
let hashStr = str.hashValue
|
||||
// 字串“Hello 演算法”的雜湊值為 -7850626797806988787
|
||||
|
||||
let arr = [AnyHashable(12836), AnyHashable("小哈")]
|
||||
let hashTup = arr.hashValue
|
||||
// 陣列 [AnyHashable(12836), AnyHashable("小哈")] 的雜湊值為 -2308633508154532996
|
||||
|
||||
let obj = ListNode(x: 0)
|
||||
let hashObj = obj.hashValue
|
||||
// 節點物件 utils.ListNode 的雜湊值為 -2434780518035996159
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="built_in_hash.js"
|
||||
// JavaScript 未提供內建 hash code 函式
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="built_in_hash.ts"
|
||||
// TypeScript 未提供內建 hash code 函式
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="built_in_hash.dart"
|
||||
int num = 3;
|
||||
int hashNum = num.hashCode;
|
||||
// 整數 3 的雜湊值為 34803
|
||||
|
||||
bool bol = true;
|
||||
int hashBol = bol.hashCode;
|
||||
// 布林值 true 的雜湊值為 1231
|
||||
|
||||
double dec = 3.14159;
|
||||
int hashDec = dec.hashCode;
|
||||
// 小數 3.14159 的雜湊值為 2570631074981783
|
||||
|
||||
String str = "Hello 演算法";
|
||||
int hashStr = str.hashCode;
|
||||
// 字串“Hello 演算法”的雜湊值為 468167534
|
||||
|
||||
List arr = [12836, "小哈"];
|
||||
int hashArr = arr.hashCode;
|
||||
// 陣列 [12836, 小哈] 的雜湊值為 976512528
|
||||
|
||||
ListNode obj = new ListNode(0);
|
||||
int hashObj = obj.hashCode;
|
||||
// 節點物件 Instance of 'ListNode' 的雜湊值為 1033450432
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="built_in_hash.rs"
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
let num = 3;
|
||||
let mut num_hasher = DefaultHasher::new();
|
||||
num.hash(&mut num_hasher);
|
||||
let hash_num = num_hasher.finish();
|
||||
// 整數 3 的雜湊值為 568126464209439262
|
||||
|
||||
let bol = true;
|
||||
let mut bol_hasher = DefaultHasher::new();
|
||||
bol.hash(&mut bol_hasher);
|
||||
let hash_bol = bol_hasher.finish();
|
||||
// 布林量 true 的雜湊值為 4952851536318644461
|
||||
|
||||
let dec: f32 = 3.14159;
|
||||
let mut dec_hasher = DefaultHasher::new();
|
||||
dec.to_bits().hash(&mut dec_hasher);
|
||||
let hash_dec = dec_hasher.finish();
|
||||
// 小數 3.14159 的雜湊值為 2566941990314602357
|
||||
|
||||
let str = "Hello 演算法";
|
||||
let mut str_hasher = DefaultHasher::new();
|
||||
str.hash(&mut str_hasher);
|
||||
let hash_str = str_hasher.finish();
|
||||
// 字串“Hello 演算法”的雜湊值為 16092673739211250988
|
||||
|
||||
let arr = (&12836, &"小哈");
|
||||
let mut tup_hasher = DefaultHasher::new();
|
||||
arr.hash(&mut tup_hasher);
|
||||
let hash_tup = tup_hasher.finish();
|
||||
// 元組 (12836, "小哈") 的雜湊值為 1885128010422702749
|
||||
|
||||
let node = ListNode::new(42);
|
||||
let mut hasher = DefaultHasher::new();
|
||||
node.borrow().val.hash(&mut hasher);
|
||||
let hash = hasher.finish();
|
||||
// 節點物件 RefCell { value: ListNode { val: 42, next: None } } 的雜湊值為15387811073369036852
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="built_in_hash.c"
|
||||
// C 未提供內建 hash code 函式
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="built_in_hash.kt"
|
||||
val num = 3
|
||||
val hashNum = num.hashCode()
|
||||
// 整數 3 的雜湊值為 3
|
||||
|
||||
val bol = true
|
||||
val hashBol = bol.hashCode()
|
||||
// 布林量 true 的雜湊值為 1231
|
||||
|
||||
val dec = 3.14159
|
||||
val hashDec = dec.hashCode()
|
||||
// 小數 3.14159 的雜湊值為 -1340954729
|
||||
|
||||
val str = "Hello 演算法"
|
||||
val hashStr = str.hashCode()
|
||||
// 字串“Hello 演算法”的雜湊值為 -727081396
|
||||
|
||||
val arr = arrayOf<Any>(12836, "小哈")
|
||||
val hashTup = arr.hashCode()
|
||||
// 陣列 [12836, 小哈] 的雜湊值為 189568618
|
||||
|
||||
val obj = ListNode(0)
|
||||
val hashObj = obj.hashCode()
|
||||
// 節點物件 utils.ListNode@1d81eb93 的雜湊值為 495053715
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="built_in_hash.rb"
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="built_in_hash.zig"
|
||||
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20num%20%3D%203%0A%20%20%20%20hash_num%20%3D%20hash%28num%29%0A%20%20%20%20%23%20%E6%95%B4%E6%95%B0%203%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%203%0A%0A%20%20%20%20bol%20%3D%20True%0A%20%20%20%20hash_bol%20%3D%20hash%28bol%29%0A%20%20%20%20%23%20%E5%B8%83%E5%B0%94%E9%87%8F%20True%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201%0A%0A%20%20%20%20dec%20%3D%203.14159%0A%20%20%20%20hash_dec%20%3D%20hash%28dec%29%0A%20%20%20%20%23%20%E5%B0%8F%E6%95%B0%203.14159%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20326484311674566659%0A%0A%20%20%20%20str%20%3D%20%22Hello%20%E7%AE%97%E6%B3%95%22%0A%20%20%20%20hash_str%20%3D%20hash%28str%29%0A%20%20%20%20%23%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%80%9CHello%20%E7%AE%97%E6%B3%95%E2%80%9D%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%204617003410720528961%0A%0A%20%20%20%20tup%20%3D%20%2812836,%20%22%E5%B0%8F%E5%93%88%22%29%0A%20%20%20%20hash_tup%20%3D%20hash%28tup%29%0A%20%20%20%20%23%20%E5%85%83%E7%BB%84%20%2812836,%20'%E5%B0%8F%E5%93%88'%29%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201029005403108185979%0A%0A%20%20%20%20obj%20%3D%20ListNode%280%29%0A%20%20%20%20hash_obj%20%3D%20hash%28obj%29%0A%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%AF%B9%E8%B1%A1%20%3CListNode%20object%20at%200x1058fd810%3E%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20274267521&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=19&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20num%20%3D%203%0A%20%20%20%20hash_num%20%3D%20hash%28num%29%0A%20%20%20%20%23%20%E6%95%B4%E6%95%B0%203%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%203%0A%0A%20%20%20%20bol%20%3D%20True%0A%20%20%20%20hash_bol%20%3D%20hash%28bol%29%0A%20%20%20%20%23%20%E5%B8%83%E5%B0%94%E9%87%8F%20True%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201%0A%0A%20%20%20%20dec%20%3D%203.14159%0A%20%20%20%20hash_dec%20%3D%20hash%28dec%29%0A%20%20%20%20%23%20%E5%B0%8F%E6%95%B0%203.14159%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20326484311674566659%0A%0A%20%20%20%20str%20%3D%20%22Hello%20%E7%AE%97%E6%B3%95%22%0A%20%20%20%20hash_str%20%3D%20hash%28str%29%0A%20%20%20%20%23%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%80%9CHello%20%E7%AE%97%E6%B3%95%E2%80%9D%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%204617003410720528961%0A%0A%20%20%20%20tup%20%3D%20%2812836,%20%22%E5%B0%8F%E5%93%88%22%29%0A%20%20%20%20hash_tup%20%3D%20hash%28tup%29%0A%20%20%20%20%23%20%E5%85%83%E7%BB%84%20%2812836,%20'%E5%B0%8F%E5%93%88'%29%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201029005403108185979%0A%0A%20%20%20%20obj%20%3D%20ListNode%280%29%0A%20%20%20%20hash_obj%20%3D%20hash%28obj%29%0A%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%AF%B9%E8%B1%A1%20%3CListNode%20object%20at%200x1058fd810%3E%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20274267521&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=19&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
在許多程式語言中,**只有不可變物件才可作為雜湊表的 `key`** 。假如我們將串列(動態陣列)作為 `key` ,當串列的內容發生變化時,它的雜湊值也隨之改變,我們就無法在雜湊表中查詢到原先的 `value` 了。
|
||||
|
||||
雖然自定義物件(比如鏈結串列節點)的成員變數是可變的,但它是可雜湊的。**這是因為物件的雜湊值通常是基於記憶體位址生成的**,即使物件的內容發生了變化,但它的記憶體位址不變,雜湊值仍然是不變的。
|
||||
|
||||
細心的你可能發現在不同控制檯中執行程式時,輸出的雜湊值是不同的。**這是因為 Python 直譯器在每次啟動時,都會為字串雜湊函式加入一個隨機的鹽(salt)值**。這種做法可以有效防止 HashDoS 攻擊,提升雜湊演算法的安全性。
|
||||
3126
zh-Hant/docs/chapter_hashing/hash_collision.md
Normal file
3126
zh-Hant/docs/chapter_hashing/hash_collision.md
Normal file
File diff suppressed because one or more lines are too long
1890
zh-Hant/docs/chapter_hashing/hash_map.md
Executable file
1890
zh-Hant/docs/chapter_hashing/hash_map.md
Executable file
File diff suppressed because one or more lines are too long
21
zh-Hant/docs/chapter_hashing/index.md
Normal file
21
zh-Hant/docs/chapter_hashing/index.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/table-search
|
||||
---
|
||||
|
||||
# 第 6 章 雜湊表
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
在計算機世界中,雜湊表如同一位聰慧的圖書管理員。
|
||||
|
||||
他知道如何計算索書號,從而可以快速找到目標圖書。
|
||||
|
||||
## Chapter Contents
|
||||
|
||||
- [6.1 雜湊表](https://www.hello-algo.com/en/chapter_hashing/hash_map/)
|
||||
- [6.2 雜湊衝突](https://www.hello-algo.com/en/chapter_hashing/hash_collision/)
|
||||
- [6.3 雜湊演算法](https://www.hello-algo.com/en/chapter_hashing/hash_algorithm/)
|
||||
- [6.4 小結](https://www.hello-algo.com/en/chapter_hashing/summary/)
|
||||
51
zh-Hant/docs/chapter_hashing/summary.md
Normal file
51
zh-Hant/docs/chapter_hashing/summary.md
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 6.4 小結
|
||||
|
||||
### 1. 重點回顧
|
||||
|
||||
- 輸入 `key` ,雜湊表能夠在 $O(1)$ 時間內查詢到 `value` ,效率非常高。
|
||||
- 常見的雜湊表操作包括查詢、新增鍵值對、刪除鍵值對和走訪雜湊表等。
|
||||
- 雜湊函式將 `key` 對映為陣列索引,從而訪問對應桶並獲取 `value` 。
|
||||
- 兩個不同的 `key` 可能在經過雜湊函式後得到相同的陣列索引,導致查詢結果出錯,這種現象被稱為雜湊衝突。
|
||||
- 雜湊表容量越大,雜湊衝突的機率就越低。因此可以透過擴容雜湊表來緩解雜湊衝突。與陣列擴容類似,雜湊表擴容操作的開銷很大。
|
||||
- 負載因子定義為雜湊表中元素數量除以桶數量,反映了雜湊衝突的嚴重程度,常用作觸發雜湊表擴容的條件。
|
||||
- 鏈式位址透過將單個元素轉化為鏈結串列,將所有衝突元素儲存在同一個鏈結串列中。然而,鏈結串列過長會降低查詢效率,可以透過進一步將鏈結串列轉換為紅黑樹來提高效率。
|
||||
- 開放定址透過多次探測來處理雜湊衝突。線性探查使用固定步長,缺點是不能刪除元素,且容易產生聚集。多次雜湊使用多個雜湊函式進行探測,相較線性探查更不易產生聚集,但多個雜湊函式增加了計算量。
|
||||
- 不同程式語言採取了不同的雜湊表實現。例如,Java 的 `HashMap` 使用鏈式位址,而 Python 的 `Dict` 採用開放定址。
|
||||
- 在雜湊表中,我們希望雜湊演算法具有確定性、高效率和均勻分佈的特點。在密碼學中,雜湊演算法還應該具備抗碰撞性和雪崩效應。
|
||||
- 雜湊演算法通常採用大質數作為模數,以最大化地保證雜湊值均勻分佈,減少雜湊衝突。
|
||||
- 常見的雜湊演算法包括 MD5、SHA-1、SHA-2 和 SHA-3 等。MD5 常用於校驗檔案完整性,SHA-2 常用於安全應用與協議。
|
||||
- 程式語言通常會為資料型別提供內建雜湊演算法,用於計算雜湊表中的桶索引。通常情況下,只有不可變物件是可雜湊的。
|
||||
|
||||
### 2. Q & A
|
||||
|
||||
**Q**:雜湊表的時間複雜度在什麼情況下是 $O(n)$ ?
|
||||
|
||||
當雜湊衝突比較嚴重時,雜湊表的時間複雜度會退化至 $O(n)$ 。當雜湊函式設計得比較好、容量設定比較合理、衝突比較平均時,時間複雜度是 $O(1)$ 。我們使用程式語言內建的雜湊表時,通常認為時間複雜度是 $O(1)$ 。
|
||||
|
||||
**Q**:為什麼不使用雜湊函式 $f(x) = x$ 呢?這樣就不會有衝突了。
|
||||
|
||||
在 $f(x) = x$ 雜湊函式下,每個元素對應唯一的桶索引,這與陣列等價。然而,輸入空間通常遠大於輸出空間(陣列長度),因此雜湊函式的最後一步往往是對陣列長度取模。換句話說,雜湊表的目標是將一個較大的狀態空間對映到一個較小的空間,並提供 $O(1)$ 的查詢效率。
|
||||
|
||||
**Q**:雜湊表底層實現是陣列、鏈結串列、二元樹,但為什麼效率可以比它們更高呢?
|
||||
|
||||
首先,雜湊表的時間效率變高,但空間效率變低了。雜湊表有相當一部分記憶體未使用。
|
||||
|
||||
其次,只是在特定使用場景下時間效率變高了。如果一個功能能夠在相同的時間複雜度下使用陣列或鏈結串列實現,那麼通常比雜湊表更快。這是因為雜湊函式計算需要開銷,時間複雜度的常數項更大。
|
||||
|
||||
最後,雜湊表的時間複雜度可能發生劣化。例如在鏈式位址中,我們採取在鏈結串列或紅黑樹中執行查詢操作,仍然有退化至 $O(n)$ 時間的風險。
|
||||
|
||||
**Q**:多次雜湊有不能直接刪除元素的缺陷嗎?標記為已刪除的空間還能再次使用嗎?
|
||||
|
||||
多次雜湊是開放定址的一種,開放定址法都有不能直接刪除元素的缺陷,需要透過標記刪除。標記為已刪除的空間可以再次使用。當將新元素插入雜湊表,並且透過雜湊函式找到標記為已刪除的位置時,該位置可以被新元素使用。這樣做既能保持雜湊表的探測序列不變,又能保證雜湊表的空間使用率。
|
||||
|
||||
**Q**:為什麼線上性探查中,查詢元素的時候會出現雜湊衝突呢?
|
||||
|
||||
查詢的時候透過雜湊函式找到對應的桶和鍵值對,發現 `key` 不匹配,這就代表有雜湊衝突。因此,線性探查法會根據預先設定的步長依次向下查詢,直至找到正確的鍵值對或無法找到跳出為止。
|
||||
|
||||
**Q**:為什麼雜湊表擴容能夠緩解雜湊衝突?
|
||||
|
||||
雜湊函式的最後一步往往是對陣列長度 $n$ 取模(取餘),讓輸出值落在陣列索引範圍內;在擴容後,陣列長度 $n$ 發生變化,而 `key` 對應的索引也可能發生變化。原先落在同一個桶的多個 `key` ,在擴容後可能會被分配到多個桶中,從而實現雜湊衝突的緩解。
|
||||
382
zh-Hant/docs/chapter_heap/build_heap.md
Normal file
382
zh-Hant/docs/chapter_heap/build_heap.md
Normal file
@@ -0,0 +1,382 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 8.2 建堆積操作
|
||||
|
||||
在某些情況下,我們希望使用一個串列的所有元素來構建一個堆積,這個過程被稱為“建堆積操作”。
|
||||
|
||||
## 8.2.1 藉助入堆積操作實現
|
||||
|
||||
我們首先建立一個空堆積,然後走訪串列,依次對每個元素執行“入堆積操作”,即先將元素新增至堆積的尾部,再對該元素執行“從底至頂”堆積化。
|
||||
|
||||
每當一個元素入堆積,堆積的長度就加一。由於節點是從頂到底依次被新增進二元樹的,因此堆積是“自上而下”構建的。
|
||||
|
||||
設元素數量為 $n$ ,每個元素的入堆積操作使用 $O(\log{n})$ 時間,因此該建堆積方法的時間複雜度為 $O(n \log n)$ 。
|
||||
|
||||
## 8.2.2 透過走訪堆積化實現
|
||||
|
||||
實際上,我們可以實現一種更為高效的建堆積方法,共分為兩步。
|
||||
|
||||
1. 將串列所有元素原封不動地新增到堆積中,此時堆積的性質尚未得到滿足。
|
||||
2. 倒序走訪堆積(層序走訪的倒序),依次對每個非葉節點執行“從頂至底堆積化”。
|
||||
|
||||
**每當堆積化一個節點後,以該節點為根節點的子樹就形成一個合法的子堆積**。而由於是倒序走訪,因此堆積是“自下而上”構建的。
|
||||
|
||||
之所以選擇倒序走訪,是因為這樣能夠保證當前節點之下的子樹已經是合法的子堆積,這樣堆積化當前節點才是有效的。
|
||||
|
||||
值得說明的是,**由於葉節點沒有子節點,因此它們天然就是合法的子堆積,無須堆積化**。如以下程式碼所示,最後一個非葉節點是最後一個節點的父節點,我們從它開始倒序走訪並執行堆積化:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="my_heap.py"
|
||||
def __init__(self, nums: list[int]):
|
||||
"""建構子,根據輸入串列建堆積"""
|
||||
# 將串列元素原封不動新增進堆積
|
||||
self.max_heap = nums
|
||||
# 堆積化除葉節點以外的其他所有節點
|
||||
for i in range(self.parent(self.size() - 1), -1, -1):
|
||||
self.sift_down(i)
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="my_heap.cpp"
|
||||
/* 建構子,根據輸入串列建堆積 */
|
||||
MaxHeap(vector<int> nums) {
|
||||
// 將串列元素原封不動新增進堆積
|
||||
maxHeap = nums;
|
||||
// 堆積化除葉節點以外的其他所有節點
|
||||
for (int i = parent(size() - 1); i >= 0; i--) {
|
||||
siftDown(i);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="my_heap.java"
|
||||
/* 建構子,根據輸入串列建堆積 */
|
||||
MaxHeap(List<Integer> nums) {
|
||||
// 將串列元素原封不動新增進堆積
|
||||
maxHeap = new ArrayList<>(nums);
|
||||
// 堆積化除葉節點以外的其他所有節點
|
||||
for (int i = parent(size() - 1); i >= 0; i--) {
|
||||
siftDown(i);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="my_heap.cs"
|
||||
/* 建構子,根據輸入串列建堆積 */
|
||||
MaxHeap(IEnumerable<int> nums) {
|
||||
// 將串列元素原封不動新增進堆積
|
||||
maxHeap = new List<int>(nums);
|
||||
// 堆積化除葉節點以外的其他所有節點
|
||||
var size = Parent(this.Size() - 1);
|
||||
for (int i = size; i >= 0; i--) {
|
||||
SiftDown(i);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="my_heap.go"
|
||||
/* 建構子,根據切片建堆積 */
|
||||
func newMaxHeap(nums []any) *maxHeap {
|
||||
// 將串列元素原封不動新增進堆積
|
||||
h := &maxHeap{data: nums}
|
||||
for i := h.parent(len(h.data) - 1); i >= 0; i-- {
|
||||
// 堆積化除葉節點以外的其他所有節點
|
||||
h.siftDown(i)
|
||||
}
|
||||
return h
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="my_heap.swift"
|
||||
/* 建構子,根據輸入串列建堆積 */
|
||||
init(nums: [Int]) {
|
||||
// 將串列元素原封不動新增進堆積
|
||||
maxHeap = nums
|
||||
// 堆積化除葉節點以外的其他所有節點
|
||||
for i in (0 ... parent(i: size() - 1)).reversed() {
|
||||
siftDown(i: i)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="my_heap.js"
|
||||
/* 建構子,建立空堆積或根據輸入串列建堆積 */
|
||||
constructor(nums) {
|
||||
// 將串列元素原封不動新增進堆積
|
||||
this.#maxHeap = nums === undefined ? [] : [...nums];
|
||||
// 堆積化除葉節點以外的其他所有節點
|
||||
for (let i = this.#parent(this.size() - 1); i >= 0; i--) {
|
||||
this.#siftDown(i);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="my_heap.ts"
|
||||
/* 建構子,建立空堆積或根據輸入串列建堆積 */
|
||||
constructor(nums?: number[]) {
|
||||
// 將串列元素原封不動新增進堆積
|
||||
this.maxHeap = nums === undefined ? [] : [...nums];
|
||||
// 堆積化除葉節點以外的其他所有節點
|
||||
for (let i = this.parent(this.size() - 1); i >= 0; i--) {
|
||||
this.siftDown(i);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="my_heap.dart"
|
||||
/* 建構子,根據輸入串列建堆積 */
|
||||
MaxHeap(List<int> nums) {
|
||||
// 將串列元素原封不動新增進堆積
|
||||
_maxHeap = nums;
|
||||
// 堆積化除葉節點以外的其他所有節點
|
||||
for (int i = _parent(size() - 1); i >= 0; i--) {
|
||||
siftDown(i);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="my_heap.rs"
|
||||
/* 建構子,根據輸入串列建堆積 */
|
||||
fn new(nums: Vec<i32>) -> Self {
|
||||
// 將串列元素原封不動新增進堆積
|
||||
let mut heap = MaxHeap { max_heap: nums };
|
||||
// 堆積化除葉節點以外的其他所有節點
|
||||
for i in (0..=Self::parent(heap.size() - 1)).rev() {
|
||||
heap.sift_down(i);
|
||||
}
|
||||
heap
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="my_heap.c"
|
||||
/* 建構子,根據切片建堆積 */
|
||||
MaxHeap *newMaxHeap(int nums[], int size) {
|
||||
// 所有元素入堆積
|
||||
MaxHeap *maxHeap = (MaxHeap *)malloc(sizeof(MaxHeap));
|
||||
maxHeap->size = size;
|
||||
memcpy(maxHeap->data, nums, size * sizeof(int));
|
||||
for (int i = parent(maxHeap, size - 1); i >= 0; i--) {
|
||||
// 堆積化除葉節點以外的其他所有節點
|
||||
siftDown(maxHeap, i);
|
||||
}
|
||||
return maxHeap;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="my_heap.kt"
|
||||
/* 大頂堆積 */
|
||||
class MaxHeap(nums: List<Int>?) {
|
||||
// 使用串列而非陣列,這樣無須考慮擴容問題
|
||||
// 將串列元素原封不動新增進堆積
|
||||
private val maxHeap = ArrayList(nums!!)
|
||||
|
||||
/* 建構子,根據輸入串列建堆積 */
|
||||
init {
|
||||
// 堆積化除葉節點以外的其他所有節點
|
||||
for (i in parent(size() - 1) downTo 0) {
|
||||
siftDown(i)
|
||||
}
|
||||
}
|
||||
|
||||
/* 獲取左子節點的索引 */
|
||||
private fun left(i: Int): Int {
|
||||
return 2 * i + 1
|
||||
}
|
||||
|
||||
/* 獲取右子節點的索引 */
|
||||
private fun right(i: Int): Int {
|
||||
return 2 * i + 2
|
||||
}
|
||||
|
||||
/* 獲取父節點的索引 */
|
||||
private fun parent(i: Int): Int {
|
||||
return (i - 1) / 2 // 向下整除
|
||||
}
|
||||
|
||||
/* 交換元素 */
|
||||
private fun swap(i: Int, j: Int) {
|
||||
maxHeap[i] = maxHeap[j].also { maxHeap[j] = maxHeap[i] }
|
||||
}
|
||||
|
||||
/* 獲取堆積大小 */
|
||||
fun size(): Int {
|
||||
return maxHeap.size
|
||||
}
|
||||
|
||||
/* 判斷堆積是否為空 */
|
||||
fun isEmpty(): Boolean {
|
||||
/* 判斷堆積是否為空 */
|
||||
return size() == 0
|
||||
}
|
||||
|
||||
/* 訪問堆積頂元素 */
|
||||
fun peek(): Int {
|
||||
return maxHeap[0]
|
||||
}
|
||||
|
||||
/* 元素入堆積 */
|
||||
fun push(value: Int) {
|
||||
// 新增節點
|
||||
maxHeap.add(value)
|
||||
// 從底至頂堆積化
|
||||
siftUp(size() - 1)
|
||||
}
|
||||
|
||||
/* 從節點 i 開始,從底至頂堆積化 */
|
||||
private fun siftUp(it: Int) {
|
||||
// Kotlin的函式參數不可變,因此建立臨時變數
|
||||
var i = it
|
||||
while (true) {
|
||||
// 獲取節點 i 的父節點
|
||||
val p = parent(i)
|
||||
// 當“越過根節點”或“節點無須修復”時,結束堆積化
|
||||
if (p < 0 || maxHeap[i] <= maxHeap[p]) break
|
||||
// 交換兩節點
|
||||
swap(i, p)
|
||||
// 迴圈向上堆積化
|
||||
i = p
|
||||
}
|
||||
}
|
||||
|
||||
/* 元素出堆積 */
|
||||
fun pop(): Int {
|
||||
// 判空處理
|
||||
if (isEmpty()) throw IndexOutOfBoundsException()
|
||||
// 交換根節點與最右葉節點(交換首元素與尾元素)
|
||||
swap(0, size() - 1)
|
||||
// 刪除節點
|
||||
val value = maxHeap.removeAt(size() - 1)
|
||||
// 從頂至底堆積化
|
||||
siftDown(0)
|
||||
// 返回堆積頂元素
|
||||
return value
|
||||
}
|
||||
|
||||
/* 從節點 i 開始,從頂至底堆積化 */
|
||||
private fun siftDown(it: Int) {
|
||||
// Kotlin的函式參數不可變,因此建立臨時變數
|
||||
var i = it
|
||||
while (true) {
|
||||
// 判斷節點 i, l, r 中值最大的節點,記為 ma
|
||||
val l = left(i)
|
||||
val r = right(i)
|
||||
var ma = i
|
||||
if (l < size() && maxHeap[l] > maxHeap[ma]) ma = l
|
||||
if (r < size() && maxHeap[r] > maxHeap[ma]) ma = r
|
||||
// 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出
|
||||
if (ma == i) break
|
||||
// 交換兩節點
|
||||
swap(i, ma)
|
||||
// 迴圈向下堆積化
|
||||
i = ma
|
||||
}
|
||||
}
|
||||
|
||||
/* 列印堆積(二元樹) */
|
||||
fun print() {
|
||||
val queue = PriorityQueue { a: Int, b: Int -> b - a }
|
||||
queue.addAll(maxHeap)
|
||||
printHeap(queue)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="my_heap.rb"
|
||||
[class]{MaxHeap}-[func]{__init__}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="my_heap.zig"
|
||||
// 建構子,根據輸入串列建堆積
|
||||
fn init(self: *Self, allocator: std.mem.Allocator, nums: []const T) !void {
|
||||
if (self.max_heap != null) return;
|
||||
self.max_heap = std.ArrayList(T).init(allocator);
|
||||
// 將串列元素原封不動新增進堆積
|
||||
try self.max_heap.?.appendSlice(nums);
|
||||
// 堆積化除葉節點以外的其他所有節點
|
||||
var i: usize = parent(self.size() - 1) + 1;
|
||||
while (i > 0) : (i -= 1) {
|
||||
try self.siftDown(i - 1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20MaxHeap%3A%0A%20%20%20%20%22%22%22%E5%A4%A7%E9%A1%B6%E5%A0%86%22%22%22%0A%0A%20%20%20%20def%20__init__%28self,%20nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95%EF%BC%8C%E6%A0%B9%E6%8D%AE%E8%BE%93%E5%85%A5%E5%88%97%E8%A1%A8%E5%BB%BA%E5%A0%86%22%22%22%0A%20%20%20%20%20%20%20%20%23%20%E5%B0%86%E5%88%97%E8%A1%A8%E5%85%83%E7%B4%A0%E5%8E%9F%E5%B0%81%E4%B8%8D%E5%8A%A8%E6%B7%BB%E5%8A%A0%E8%BF%9B%E5%A0%86%0A%20%20%20%20%20%20%20%20self.max_heap%20%3D%20nums%0A%20%20%20%20%20%20%20%20%23%20%E5%A0%86%E5%8C%96%E9%99%A4%E5%8F%B6%E8%8A%82%E7%82%B9%E4%BB%A5%E5%A4%96%E7%9A%84%E5%85%B6%E4%BB%96%E6%89%80%E6%9C%89%E8%8A%82%E7%82%B9%0A%20%20%20%20%20%20%20%20for%20i%20in%20range%28self.parent%28self.size%28%29%20-%201%29,%20-1,%20-1%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20self.sift_down%28i%29%0A%0A%20%20%20%20def%20left%28self,%20i%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%8E%B7%E5%8F%96%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E7%9A%84%E7%B4%A2%E5%BC%95%22%22%22%0A%20%20%20%20%20%20%20%20return%202%20*%20i%20%2B%201%0A%0A%20%20%20%20def%20right%28self,%20i%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%8E%B7%E5%8F%96%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E7%9A%84%E7%B4%A2%E5%BC%95%22%22%22%0A%20%20%20%20%20%20%20%20return%202%20*%20i%20%2B%202%0A%0A%20%20%20%20def%20parent%28self,%20i%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%8E%B7%E5%8F%96%E7%88%B6%E8%8A%82%E7%82%B9%E7%9A%84%E7%B4%A2%E5%BC%95%22%22%22%0A%20%20%20%20%20%20%20%20return%20%28i%20-%201%29%20//%202%20%20%23%20%E5%90%91%E4%B8%8B%E6%95%B4%E9%99%A4%0A%0A%20%20%20%20def%20swap%28self,%20i%3A%20int,%20j%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E4%BA%A4%E6%8D%A2%E5%85%83%E7%B4%A0%22%22%22%0A%20%20%20%20%20%20%20%20self.max_heap%5Bi%5D,%20self.max_heap%5Bj%5D%20%3D%20self.max_heap%5Bj%5D,%20self.max_heap%5Bi%5D%0A%0A%20%20%20%20def%20size%28self%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%8E%B7%E5%8F%96%E5%A0%86%E5%A4%A7%E5%B0%8F%22%22%22%0A%20%20%20%20%20%20%20%20return%20len%28self.max_heap%29%0A%0A%20%20%20%20def%20sift_down%28self,%20i%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E4%BB%8E%E8%8A%82%E7%82%B9%20i%20%E5%BC%80%E5%A7%8B%EF%BC%8C%E4%BB%8E%E9%A1%B6%E8%87%B3%E5%BA%95%E5%A0%86%E5%8C%96%22%22%22%0A%20%20%20%20%20%20%20%20while%20True%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E8%8A%82%E7%82%B9%20i,%20l,%20r%20%E4%B8%AD%E5%80%BC%E6%9C%80%E5%A4%A7%E7%9A%84%E8%8A%82%E7%82%B9%EF%BC%8C%E8%AE%B0%E4%B8%BA%20ma%0A%20%20%20%20%20%20%20%20%20%20%20%20l,%20r,%20ma%20%3D%20self.left%28i%29,%20self.right%28i%29,%20i%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20l%20%3C%20self.size%28%29%20and%20self.max_heap%5Bl%5D%20%3E%20self.max_heap%5Bma%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20ma%20%3D%20l%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20r%20%3C%20self.size%28%29%20and%20self.max_heap%5Br%5D%20%3E%20self.max_heap%5Bma%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20ma%20%3D%20r%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E8%8A%82%E7%82%B9%20i%20%E6%9C%80%E5%A4%A7%E6%88%96%E7%B4%A2%E5%BC%95%20l,%20r%20%E8%B6%8A%E7%95%8C%EF%BC%8C%E5%88%99%E6%97%A0%E9%A1%BB%E7%BB%A7%E7%BB%AD%E5%A0%86%E5%8C%96%EF%BC%8C%E8%B7%B3%E5%87%BA%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20ma%20%3D%3D%20i%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20break%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E4%BA%A4%E6%8D%A2%E4%B8%A4%E8%8A%82%E7%82%B9%0A%20%20%20%20%20%20%20%20%20%20%20%20self.swap%28i,%20ma%29%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%E5%90%91%E4%B8%8B%E5%A0%86%E5%8C%96%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20ma%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%A4%A7%E9%A1%B6%E5%A0%86%0A%20%20%20%20max_heap%20%3D%20MaxHeap%28%5B1,%202,%203,%204,%205%5D%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20MaxHeap%3A%0A%20%20%20%20%22%22%22%E5%A4%A7%E9%A1%B6%E5%A0%86%22%22%22%0A%0A%20%20%20%20def%20__init__%28self,%20nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95%EF%BC%8C%E6%A0%B9%E6%8D%AE%E8%BE%93%E5%85%A5%E5%88%97%E8%A1%A8%E5%BB%BA%E5%A0%86%22%22%22%0A%20%20%20%20%20%20%20%20%23%20%E5%B0%86%E5%88%97%E8%A1%A8%E5%85%83%E7%B4%A0%E5%8E%9F%E5%B0%81%E4%B8%8D%E5%8A%A8%E6%B7%BB%E5%8A%A0%E8%BF%9B%E5%A0%86%0A%20%20%20%20%20%20%20%20self.max_heap%20%3D%20nums%0A%20%20%20%20%20%20%20%20%23%20%E5%A0%86%E5%8C%96%E9%99%A4%E5%8F%B6%E8%8A%82%E7%82%B9%E4%BB%A5%E5%A4%96%E7%9A%84%E5%85%B6%E4%BB%96%E6%89%80%E6%9C%89%E8%8A%82%E7%82%B9%0A%20%20%20%20%20%20%20%20for%20i%20in%20range%28self.parent%28self.size%28%29%20-%201%29,%20-1,%20-1%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20self.sift_down%28i%29%0A%0A%20%20%20%20def%20left%28self,%20i%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%8E%B7%E5%8F%96%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E7%9A%84%E7%B4%A2%E5%BC%95%22%22%22%0A%20%20%20%20%20%20%20%20return%202%20*%20i%20%2B%201%0A%0A%20%20%20%20def%20right%28self,%20i%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%8E%B7%E5%8F%96%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E7%9A%84%E7%B4%A2%E5%BC%95%22%22%22%0A%20%20%20%20%20%20%20%20return%202%20*%20i%20%2B%202%0A%0A%20%20%20%20def%20parent%28self,%20i%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%8E%B7%E5%8F%96%E7%88%B6%E8%8A%82%E7%82%B9%E7%9A%84%E7%B4%A2%E5%BC%95%22%22%22%0A%20%20%20%20%20%20%20%20return%20%28i%20-%201%29%20//%202%20%20%23%20%E5%90%91%E4%B8%8B%E6%95%B4%E9%99%A4%0A%0A%20%20%20%20def%20swap%28self,%20i%3A%20int,%20j%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E4%BA%A4%E6%8D%A2%E5%85%83%E7%B4%A0%22%22%22%0A%20%20%20%20%20%20%20%20self.max_heap%5Bi%5D,%20self.max_heap%5Bj%5D%20%3D%20self.max_heap%5Bj%5D,%20self.max_heap%5Bi%5D%0A%0A%20%20%20%20def%20size%28self%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E8%8E%B7%E5%8F%96%E5%A0%86%E5%A4%A7%E5%B0%8F%22%22%22%0A%20%20%20%20%20%20%20%20return%20len%28self.max_heap%29%0A%0A%20%20%20%20def%20sift_down%28self,%20i%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%E4%BB%8E%E8%8A%82%E7%82%B9%20i%20%E5%BC%80%E5%A7%8B%EF%BC%8C%E4%BB%8E%E9%A1%B6%E8%87%B3%E5%BA%95%E5%A0%86%E5%8C%96%22%22%22%0A%20%20%20%20%20%20%20%20while%20True%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E8%8A%82%E7%82%B9%20i,%20l,%20r%20%E4%B8%AD%E5%80%BC%E6%9C%80%E5%A4%A7%E7%9A%84%E8%8A%82%E7%82%B9%EF%BC%8C%E8%AE%B0%E4%B8%BA%20ma%0A%20%20%20%20%20%20%20%20%20%20%20%20l,%20r,%20ma%20%3D%20self.left%28i%29,%20self.right%28i%29,%20i%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20l%20%3C%20self.size%28%29%20and%20self.max_heap%5Bl%5D%20%3E%20self.max_heap%5Bma%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20ma%20%3D%20l%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20r%20%3C%20self.size%28%29%20and%20self.max_heap%5Br%5D%20%3E%20self.max_heap%5Bma%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20ma%20%3D%20r%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8B%A5%E8%8A%82%E7%82%B9%20i%20%E6%9C%80%E5%A4%A7%E6%88%96%E7%B4%A2%E5%BC%95%20l,%20r%20%E8%B6%8A%E7%95%8C%EF%BC%8C%E5%88%99%E6%97%A0%E9%A1%BB%E7%BB%A7%E7%BB%AD%E5%A0%86%E5%8C%96%EF%BC%8C%E8%B7%B3%E5%87%BA%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20ma%20%3D%3D%20i%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20break%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E4%BA%A4%E6%8D%A2%E4%B8%A4%E8%8A%82%E7%82%B9%0A%20%20%20%20%20%20%20%20%20%20%20%20self.swap%28i,%20ma%29%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E5%BE%AA%E7%8E%AF%E5%90%91%E4%B8%8B%E5%A0%86%E5%8C%96%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20ma%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%A4%A7%E9%A1%B6%E5%A0%86%0A%20%20%20%20max_heap%20%3D%20MaxHeap%28%5B1,%202,%203,%204,%205%5D%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
|
||||
|
||||
## 8.2.3 複雜度分析
|
||||
|
||||
下面,我們來嘗試推算第二種建堆積方法的時間複雜度。
|
||||
|
||||
- 假設完全二元樹的節點數量為 $n$ ,則葉節點數量為 $(n + 1) / 2$ ,其中 $/$ 為向下整除。因此需要堆積化的節點數量為 $(n - 1) / 2$ 。
|
||||
- 在從頂至底堆積化的過程中,每個節點最多堆積化到葉節點,因此最大迭代次數為二元樹高度 $\log n$ 。
|
||||
|
||||
將上述兩者相乘,可得到建堆積過程的時間複雜度為 $O(n \log n)$ 。**但這個估算結果並不準確,因為我們沒有考慮到二元樹底層節點數量遠多於頂層節點的性質**。
|
||||
|
||||
接下來我們來進行更為準確的計算。為了降低計算難度,假設給定一個節點數量為 $n$ 、高度為 $h$ 的“完美二元樹”,該假設不會影響計算結果的正確性。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 圖 8-5 完美二元樹的各層節點數量 </p>
|
||||
|
||||
如圖 8-5 所示,節點“從頂至底堆積化”的最大迭代次數等於該節點到葉節點的距離,而該距離正是“節點高度”。因此,我們可以對各層的“節點數量 $\times$ 節點高度”求和,**得到所有節點的堆積化迭代次數的總和**。
|
||||
|
||||
$$
|
||||
T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{(h-1)}\times1
|
||||
$$
|
||||
|
||||
化簡上式需要藉助中學的數列知識,先將 $T(h)$ 乘以 $2$ ,得到:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
T(h) & = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{h-1}\times1 \newline
|
||||
2 T(h) & = 2^1h + 2^2(h-1) + 2^3(h-2) + \dots + 2^{h}\times1 \newline
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
使用錯位相減法,用下式 $2 T(h)$ 減去上式 $T(h)$ ,可得:
|
||||
|
||||
$$
|
||||
2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \dots + 2^{h-1} + 2^h
|
||||
$$
|
||||
|
||||
觀察上式,發現 $T(h)$ 是一個等比數列,可直接使用求和公式,得到時間複雜度為:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
T(h) & = 2 \frac{1 - 2^h}{1 - 2} - h \newline
|
||||
& = 2^{h+1} - h - 2 \newline
|
||||
& = O(2^h)
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
進一步,高度為 $h$ 的完美二元樹的節點數量為 $n = 2^{h+1} - 1$ ,易得複雜度為 $O(2^h) = O(n)$ 。以上推算表明,**輸入串列並建堆積的時間複雜度為 $O(n)$ ,非常高效**。
|
||||
1821
zh-Hant/docs/chapter_heap/heap.md
Normal file
1821
zh-Hant/docs/chapter_heap/heap.md
Normal file
File diff suppressed because one or more lines are too long
21
zh-Hant/docs/chapter_heap/index.md
Normal file
21
zh-Hant/docs/chapter_heap/index.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/family-tree
|
||||
---
|
||||
|
||||
# 第 8 章 堆積
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
堆積就像是山嶽峰巒,層疊起伏、形態各異。
|
||||
|
||||
座座山峰高低錯落,而最高的山峰總是最先映入眼簾。
|
||||
|
||||
## Chapter Contents
|
||||
|
||||
- [8.1 堆積](https://www.hello-algo.com/en/chapter_heap/heap/)
|
||||
- [8.2 建堆積操作](https://www.hello-algo.com/en/chapter_heap/build_heap/)
|
||||
- [8.3 Top-k 問題](https://www.hello-algo.com/en/chapter_heap/top_k/)
|
||||
- [8.4 小結](https://www.hello-algo.com/en/chapter_heap/summary/)
|
||||
21
zh-Hant/docs/chapter_heap/summary.md
Normal file
21
zh-Hant/docs/chapter_heap/summary.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 8.4 小結
|
||||
|
||||
### 1. 重點回顧
|
||||
|
||||
- 堆積是一棵完全二元樹,根據成立條件可分為大頂堆積和小頂堆積。大(小)頂堆積的堆積頂元素是最大(小)的。
|
||||
- 優先佇列的定義是具有出列優先順序的佇列,通常使用堆積來實現。
|
||||
- 堆積的常用操作及其對應的時間複雜度包括:元素入堆積 $O(\log n)$、堆積頂元素出堆積 $O(\log n)$ 和訪問堆積頂元素 $O(1)$ 等。
|
||||
- 完全二元樹非常適合用陣列表示,因此我們通常使用陣列來儲存堆積。
|
||||
- 堆積化操作用於維護堆積的性質,在入堆積和出堆積操作中都會用到。
|
||||
- 輸入 $n$ 個元素並建堆積的時間複雜度可以最佳化至 $O(n)$ ,非常高效。
|
||||
- Top-k 是一個經典演算法問題,可以使用堆積資料結構高效解決,時間複雜度為 $O(n \log k)$ 。
|
||||
|
||||
### 2. Q & A
|
||||
|
||||
**Q**:資料結構的“堆積”與記憶體管理的“堆積”是同一個概念嗎?
|
||||
|
||||
兩者不是同一個概念,只是碰巧都叫“堆積”。計算機系統記憶體中的堆積是動態記憶體分配的一部分,程式在執行時可以使用它來儲存資料。程式可以請求一定量的堆積記憶體,用於儲存如物件和陣列等複雜結構。當這些資料不再需要時,程式需要釋放這些記憶體,以防止記憶體流失。相較於堆疊記憶體,堆積記憶體的管理和使用需要更謹慎,使用不當可能會導致記憶體流失和野指標等問題。
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user