From 8d37c215c83aeea61887640fa4e4070151b95824 Mon Sep 17 00:00:00 2001 From: krahets Date: Sat, 6 Apr 2024 03:02:20 +0800 Subject: [PATCH] build --- docs/chapter_appendix/terminology.md | 8 +- .../time_complexity.md | 2 +- docs/index.html | 34 +- en/docs/chapter_array_and_linkedlist/array.md | 66 +- en/docs/chapter_array_and_linkedlist/index.md | 8 +- .../linked_list.md | 84 +- en/docs/chapter_array_and_linkedlist/list.md | 22 +- .../ram_and_cache.md | 34 +- .../chapter_array_and_linkedlist/summary.md | 4 +- .../chapter_computational_complexity/index.md | 12 +- .../iteration_and_recursion.md | 48 +- .../performance_evaluation.md | 10 +- .../space_complexity.md | 68 +- .../summary.md | 2 +- .../time_complexity.md | 64 +- .../basic_data_types.md | 4 +- .../character_encoding.md | 34 +- .../classification_of_data_structure.md | 28 +- en/docs/chapter_data_structure/index.md | 12 +- .../chapter_data_structure/number_encoding.md | 16 +- en/docs/chapter_data_structure/summary.md | 2 +- en/docs/chapter_hashing/hash_algorithm.md | 36 +- en/docs/chapter_hashing/hash_collision.md | 44 +- en/docs/chapter_hashing/hash_map.md | 16 +- en/docs/chapter_hashing/index.md | 10 +- en/docs/chapter_hashing/summary.md | 2 +- .../algorithms_are_everywhere.md | 6 +- en/docs/chapter_introduction/index.md | 8 +- en/docs/chapter_introduction/what_is_dsa.md | 10 +- en/docs/chapter_preface/about_the_book.md | 14 +- en/docs/chapter_preface/index.md | 4 +- en/docs/chapter_preface/suggestions.md | 38 +- en/docs/chapter_stack_and_queue/deque.md | 18 +- en/docs/chapter_stack_and_queue/index.md | 6 +- en/docs/chapter_stack_and_queue/queue.md | 20 +- en/docs/chapter_stack_and_queue/stack.md | 18 +- en/docs/chapter_stack_and_queue/summary.md | 2 +- overrides/partials/comments.html | 2 +- zh-Hant/docs/chapter_appendix/contribution.md | 53 + zh-Hant/docs/chapter_appendix/index.md | 14 + zh-Hant/docs/chapter_appendix/installation.md | 71 + zh-Hant/docs/chapter_appendix/terminology.md | 144 + .../chapter_array_and_linkedlist/array.md | 1462 ++++++ .../chapter_array_and_linkedlist/index.md | 22 + .../linked_list.md | 1565 +++++++ .../docs/chapter_array_and_linkedlist/list.md | 2496 +++++++++++ .../ram_and_cache.md | 84 + .../chapter_array_and_linkedlist/summary.md | 80 + .../backtracking_algorithm.md | 1951 ++++++++ zh-Hant/docs/chapter_backtracking/index.md | 22 + .../chapter_backtracking/n_queens_problem.md | 732 +++ .../permutations_problem.md | 1068 +++++ .../subset_sum_problem.md | 1623 +++++++ zh-Hant/docs/chapter_backtracking/summary.md | 27 + .../chapter_computational_complexity/index.md | 22 + .../iteration_and_recursion.md | 2066 +++++++++ .../performance_evaluation.md | 52 + .../space_complexity.md | 2384 ++++++++++ .../summary.md | 53 + .../time_complexity.md | 3950 +++++++++++++++++ .../basic_data_types.md | 189 + .../character_encoding.md | 97 + .../classification_of_data_structure.md | 58 + zh-Hant/docs/chapter_data_structure/index.md | 22 + .../chapter_data_structure/number_encoding.md | 162 + .../docs/chapter_data_structure/summary.md | 38 + .../binary_search_recur.md | 441 ++ .../build_binary_tree_problem.md | 539 +++ .../divide_and_conquer.md | 101 + .../hanota_problem.md | 545 +++ .../docs/chapter_divide_and_conquer/index.md | 22 + .../chapter_divide_and_conquer/summary.md | 15 + .../dp_problem_features.md | 978 ++++ .../dp_solution_pipeline.md | 1561 +++++++ .../edit_distance_problem.md | 996 +++++ .../docs/chapter_dynamic_programming/index.md | 24 + .../intro_to_dynamic_programming.md | 1639 +++++++ .../knapsack_problem.md | 1488 +++++++ .../chapter_dynamic_programming/summary.md | 27 + .../unbounded_knapsack_problem.md | 2380 ++++++++++ zh-Hant/docs/chapter_graph/graph.md | 103 + .../docs/chapter_graph/graph_operations.md | 2266 ++++++++++ zh-Hant/docs/chapter_graph/graph_traversal.md | 967 ++++ zh-Hant/docs/chapter_graph/index.md | 21 + zh-Hant/docs/chapter_graph/summary.md | 35 + .../fractional_knapsack_problem.md | 568 +++ .../docs/chapter_greedy/greedy_algorithm.md | 397 ++ zh-Hant/docs/chapter_greedy/index.md | 22 + .../chapter_greedy/max_capacity_problem.md | 430 ++ .../max_product_cutting_problem.md | 405 ++ zh-Hant/docs/chapter_greedy/summary.md | 16 + .../docs/chapter_hashing/hash_algorithm.md | 972 ++++ .../docs/chapter_hashing/hash_collision.md | 3126 +++++++++++++ zh-Hant/docs/chapter_hashing/hash_map.md | 1890 ++++++++ zh-Hant/docs/chapter_hashing/index.md | 21 + zh-Hant/docs/chapter_hashing/summary.md | 51 + zh-Hant/docs/chapter_heap/build_heap.md | 382 ++ zh-Hant/docs/chapter_heap/heap.md | 1821 ++++++++ zh-Hant/docs/chapter_heap/index.md | 21 + zh-Hant/docs/chapter_heap/summary.md | 21 + zh-Hant/docs/chapter_heap/top_k.md | 456 ++ zh-Hant/docs/chapter_hello_algo/index.md | 30 + .../algorithms_are_everywhere.md | 66 + zh-Hant/docs/chapter_introduction/index.md | 20 + zh-Hant/docs/chapter_introduction/summary.md | 13 + .../docs/chapter_introduction/what_is_dsa.md | 65 + .../docs/chapter_preface/about_the_book.md | 56 + zh-Hant/docs/chapter_preface/index.md | 20 + zh-Hant/docs/chapter_preface/suggestions.md | 265 ++ zh-Hant/docs/chapter_preface/summary.md | 12 + zh-Hant/docs/chapter_reference/index.md | 25 + .../docs/chapter_searching/binary_search.md | 721 +++ .../chapter_searching/binary_search_edge.md | 494 +++ .../binary_search_insertion.md | 648 +++ zh-Hant/docs/chapter_searching/index.md | 23 + .../replace_linear_by_hashing.md | 565 +++ .../searching_algorithm_revisited.md | 94 + zh-Hant/docs/chapter_searching/summary.md | 12 + zh-Hant/docs/chapter_sorting/bubble_sort.md | 623 +++ zh-Hant/docs/chapter_sorting/bucket_sort.md | 468 ++ zh-Hant/docs/chapter_sorting/counting_sort.md | 883 ++++ zh-Hant/docs/chapter_sorting/heap_sort.md | 600 +++ zh-Hant/docs/chapter_sorting/index.md | 28 + .../docs/chapter_sorting/insertion_sort.md | 301 ++ zh-Hant/docs/chapter_sorting/merge_sort.md | 708 +++ zh-Hant/docs/chapter_sorting/quick_sort.md | 1419 ++++++ zh-Hant/docs/chapter_sorting/radix_sort.md | 764 ++++ .../docs/chapter_sorting/selection_sort.md | 328 ++ .../docs/chapter_sorting/sorting_algorithm.md | 54 + zh-Hant/docs/chapter_sorting/summary.md | 53 + zh-Hant/docs/chapter_stack_and_queue/deque.md | 3506 +++++++++++++++ zh-Hant/docs/chapter_stack_and_queue/index.md | 21 + zh-Hant/docs/chapter_stack_and_queue/queue.md | 2321 ++++++++++ zh-Hant/docs/chapter_stack_and_queue/stack.md | 1874 ++++++++ .../docs/chapter_stack_and_queue/summary.md | 35 + .../array_representation_of_tree.md | 1280 ++++++ zh-Hant/docs/chapter_tree/avl_tree.md | 2691 +++++++++++ .../docs/chapter_tree/binary_search_tree.md | 1628 +++++++ zh-Hant/docs/chapter_tree/binary_tree.md | 688 +++ .../chapter_tree/binary_tree_traversal.md | 883 ++++ zh-Hant/docs/chapter_tree/index.md | 23 + zh-Hant/docs/chapter_tree/summary.md | 58 + .../docs/index.assets/btn_download_pdf.svg | 1 + .../index.assets/btn_download_pdf_dark.svg | 1 + zh-Hant/docs/index.assets/btn_read_online.svg | 1 + .../index.assets/btn_read_online_dark.svg | 1 + zh-Hant/docs/index.html | 357 ++ zh-Hant/docs/index.md | 9 + 148 files changed, 70398 insertions(+), 408 deletions(-) create mode 100644 zh-Hant/docs/chapter_appendix/contribution.md create mode 100644 zh-Hant/docs/chapter_appendix/index.md create mode 100644 zh-Hant/docs/chapter_appendix/installation.md create mode 100644 zh-Hant/docs/chapter_appendix/terminology.md create mode 100755 zh-Hant/docs/chapter_array_and_linkedlist/array.md create mode 100644 zh-Hant/docs/chapter_array_and_linkedlist/index.md create mode 100755 zh-Hant/docs/chapter_array_and_linkedlist/linked_list.md create mode 100755 zh-Hant/docs/chapter_array_and_linkedlist/list.md create mode 100644 zh-Hant/docs/chapter_array_and_linkedlist/ram_and_cache.md create mode 100644 zh-Hant/docs/chapter_array_and_linkedlist/summary.md create mode 100644 zh-Hant/docs/chapter_backtracking/backtracking_algorithm.md create mode 100644 zh-Hant/docs/chapter_backtracking/index.md create mode 100644 zh-Hant/docs/chapter_backtracking/n_queens_problem.md create mode 100644 zh-Hant/docs/chapter_backtracking/permutations_problem.md create mode 100644 zh-Hant/docs/chapter_backtracking/subset_sum_problem.md create mode 100644 zh-Hant/docs/chapter_backtracking/summary.md create mode 100644 zh-Hant/docs/chapter_computational_complexity/index.md create mode 100644 zh-Hant/docs/chapter_computational_complexity/iteration_and_recursion.md create mode 100644 zh-Hant/docs/chapter_computational_complexity/performance_evaluation.md create mode 100755 zh-Hant/docs/chapter_computational_complexity/space_complexity.md create mode 100644 zh-Hant/docs/chapter_computational_complexity/summary.md create mode 100755 zh-Hant/docs/chapter_computational_complexity/time_complexity.md create mode 100644 zh-Hant/docs/chapter_data_structure/basic_data_types.md create mode 100644 zh-Hant/docs/chapter_data_structure/character_encoding.md create mode 100644 zh-Hant/docs/chapter_data_structure/classification_of_data_structure.md create mode 100644 zh-Hant/docs/chapter_data_structure/index.md create mode 100644 zh-Hant/docs/chapter_data_structure/number_encoding.md create mode 100644 zh-Hant/docs/chapter_data_structure/summary.md create mode 100644 zh-Hant/docs/chapter_divide_and_conquer/binary_search_recur.md create mode 100644 zh-Hant/docs/chapter_divide_and_conquer/build_binary_tree_problem.md create mode 100644 zh-Hant/docs/chapter_divide_and_conquer/divide_and_conquer.md create mode 100644 zh-Hant/docs/chapter_divide_and_conquer/hanota_problem.md create mode 100644 zh-Hant/docs/chapter_divide_and_conquer/index.md create mode 100644 zh-Hant/docs/chapter_divide_and_conquer/summary.md create mode 100644 zh-Hant/docs/chapter_dynamic_programming/dp_problem_features.md create mode 100644 zh-Hant/docs/chapter_dynamic_programming/dp_solution_pipeline.md create mode 100644 zh-Hant/docs/chapter_dynamic_programming/edit_distance_problem.md create mode 100644 zh-Hant/docs/chapter_dynamic_programming/index.md create mode 100644 zh-Hant/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md create mode 100644 zh-Hant/docs/chapter_dynamic_programming/knapsack_problem.md create mode 100644 zh-Hant/docs/chapter_dynamic_programming/summary.md create mode 100644 zh-Hant/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md create mode 100644 zh-Hant/docs/chapter_graph/graph.md create mode 100644 zh-Hant/docs/chapter_graph/graph_operations.md create mode 100644 zh-Hant/docs/chapter_graph/graph_traversal.md create mode 100644 zh-Hant/docs/chapter_graph/index.md create mode 100644 zh-Hant/docs/chapter_graph/summary.md create mode 100644 zh-Hant/docs/chapter_greedy/fractional_knapsack_problem.md create mode 100644 zh-Hant/docs/chapter_greedy/greedy_algorithm.md create mode 100644 zh-Hant/docs/chapter_greedy/index.md create mode 100644 zh-Hant/docs/chapter_greedy/max_capacity_problem.md create mode 100644 zh-Hant/docs/chapter_greedy/max_product_cutting_problem.md create mode 100644 zh-Hant/docs/chapter_greedy/summary.md create mode 100644 zh-Hant/docs/chapter_hashing/hash_algorithm.md create mode 100644 zh-Hant/docs/chapter_hashing/hash_collision.md create mode 100755 zh-Hant/docs/chapter_hashing/hash_map.md create mode 100644 zh-Hant/docs/chapter_hashing/index.md create mode 100644 zh-Hant/docs/chapter_hashing/summary.md create mode 100644 zh-Hant/docs/chapter_heap/build_heap.md create mode 100644 zh-Hant/docs/chapter_heap/heap.md create mode 100644 zh-Hant/docs/chapter_heap/index.md create mode 100644 zh-Hant/docs/chapter_heap/summary.md create mode 100644 zh-Hant/docs/chapter_heap/top_k.md create mode 100644 zh-Hant/docs/chapter_hello_algo/index.md create mode 100644 zh-Hant/docs/chapter_introduction/algorithms_are_everywhere.md create mode 100644 zh-Hant/docs/chapter_introduction/index.md create mode 100644 zh-Hant/docs/chapter_introduction/summary.md create mode 100644 zh-Hant/docs/chapter_introduction/what_is_dsa.md create mode 100644 zh-Hant/docs/chapter_preface/about_the_book.md create mode 100644 zh-Hant/docs/chapter_preface/index.md create mode 100644 zh-Hant/docs/chapter_preface/suggestions.md create mode 100644 zh-Hant/docs/chapter_preface/summary.md create mode 100644 zh-Hant/docs/chapter_reference/index.md create mode 100755 zh-Hant/docs/chapter_searching/binary_search.md create mode 100644 zh-Hant/docs/chapter_searching/binary_search_edge.md create mode 100644 zh-Hant/docs/chapter_searching/binary_search_insertion.md create mode 100644 zh-Hant/docs/chapter_searching/index.md create mode 100755 zh-Hant/docs/chapter_searching/replace_linear_by_hashing.md create mode 100644 zh-Hant/docs/chapter_searching/searching_algorithm_revisited.md create mode 100644 zh-Hant/docs/chapter_searching/summary.md create mode 100755 zh-Hant/docs/chapter_sorting/bubble_sort.md create mode 100644 zh-Hant/docs/chapter_sorting/bucket_sort.md create mode 100644 zh-Hant/docs/chapter_sorting/counting_sort.md create mode 100644 zh-Hant/docs/chapter_sorting/heap_sort.md create mode 100644 zh-Hant/docs/chapter_sorting/index.md create mode 100755 zh-Hant/docs/chapter_sorting/insertion_sort.md create mode 100755 zh-Hant/docs/chapter_sorting/merge_sort.md create mode 100755 zh-Hant/docs/chapter_sorting/quick_sort.md create mode 100644 zh-Hant/docs/chapter_sorting/radix_sort.md create mode 100644 zh-Hant/docs/chapter_sorting/selection_sort.md create mode 100644 zh-Hant/docs/chapter_sorting/sorting_algorithm.md create mode 100644 zh-Hant/docs/chapter_sorting/summary.md create mode 100644 zh-Hant/docs/chapter_stack_and_queue/deque.md create mode 100644 zh-Hant/docs/chapter_stack_and_queue/index.md create mode 100755 zh-Hant/docs/chapter_stack_and_queue/queue.md create mode 100755 zh-Hant/docs/chapter_stack_and_queue/stack.md create mode 100644 zh-Hant/docs/chapter_stack_and_queue/summary.md create mode 100644 zh-Hant/docs/chapter_tree/array_representation_of_tree.md create mode 100644 zh-Hant/docs/chapter_tree/avl_tree.md create mode 100755 zh-Hant/docs/chapter_tree/binary_search_tree.md create mode 100644 zh-Hant/docs/chapter_tree/binary_tree.md create mode 100755 zh-Hant/docs/chapter_tree/binary_tree_traversal.md create mode 100644 zh-Hant/docs/chapter_tree/index.md create mode 100644 zh-Hant/docs/chapter_tree/summary.md create mode 100644 zh-Hant/docs/index.assets/btn_download_pdf.svg create mode 100644 zh-Hant/docs/index.assets/btn_download_pdf_dark.svg create mode 100644 zh-Hant/docs/index.assets/btn_read_online.svg create mode 100644 zh-Hant/docs/index.assets/btn_read_online_dark.svg create mode 100644 zh-Hant/docs/index.html create mode 100644 zh-Hant/docs/index.md diff --git a/docs/chapter_appendix/terminology.md b/docs/chapter_appendix/terminology.md index 96b982e32..cfab3b30b 100644 --- a/docs/chapter_appendix/terminology.md +++ b/docs/chapter_appendix/terminology.md @@ -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 | 缓存命中率 | 快取命中率 | diff --git a/docs/chapter_computational_complexity/time_complexity.md b/docs/chapter_computational_complexity/time_complexity.md index d22cf5ac2..25f8a9c8a 100755 --- a/docs/chapter_computational_complexity/time_complexity.md +++ b/docs/chapter_computational_complexity/time_complexity.md @@ -749,7 +749,7 @@ $$ $T(n)$ 是一次函数,说明其运行时间的增长趋势是线性的,因此它的时间复杂度是线性阶。 -我们将线性阶的时间复杂度记为 $O(n)$ ,这个数学符号称为大($O$ 记号 big-$O$ notation),表示函数 $T(n)$ 的渐近上界(asymptotic upper bound)。 +我们将线性阶的时间复杂度记为 $O(n)$ ,这个数学符号称为大 $O$ 记号(big-$O$ notation),表示函数 $T(n)$ 的渐近上界(asymptotic upper bound)。 时间复杂度分析本质上是计算“操作数量 $T(n)$”的渐近上界,它具有明确的数学定义。 diff --git a/docs/index.html b/docs/index.html index ecfd75d8a..53aa17fcf 100644 --- a/docs/index.html +++ b/docs/index.html @@ -8,64 +8,64 @@ style="position: absolute; width: auto; height: 26.445%; left: 28.211%; top: 54.145%;"> - + 初识算法 - + 复杂度 - + 数组与链表 - + 栈与队列 - + 哈希表 - + - + - + - + 搜索 - + 排序 - + 分治 - + 回溯 - + 动态规划 - + 贪心 @@ -80,7 +80,7 @@

动画图解、一键运行的数据结构与算法教程

- +
- +
Cover
- +
diff --git a/en/docs/chapter_array_and_linkedlist/array.md b/en/docs/chapter_array_and_linkedlist/array.md index 7f7bf2647..b41ad461d 100755 --- a/en/docs/chapter_array_and_linkedlist/array.md +++ b/en/docs/chapter_array_and_linkedlist/array.md @@ -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. -![Array Definition and Storage Method](array.assets/array_definition.png){ class="animation-figure" } +![Array definition and storage method](array.assets/array_definition.png){ class="animation-figure" } -

Figure 4-1   Array Definition and Storage Method

+

Figure 4-1   Array definition and storage method

-## 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. -![Memory Address Calculation for Array Elements](array.assets/array_memory_location_calculation.png){ class="animation-figure" } +![Memory address calculation for array elements](array.assets/array_memory_location_calculation.png){ class="animation-figure" } -

Figure 4-2   Memory Address Calculation for Array Elements

+

Figure 4-2   Memory address calculation for array elements

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
Full Screen >
-### 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. -![Array Element Insertion Example](array.assets/array_insert_element.png){ class="animation-figure" } +![Array element insertion example](array.assets/array_insert_element.png){ class="animation-figure" } -

Figure 4-3   Array Element Insertion Example

+

Figure 4-3   Array element insertion example

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
Full Screen >
-### 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. -![Array Element Deletion Example](array.assets/array_remove_element.png){ class="animation-figure" } +![Array element deletion example](array.assets/array_remove_element.png){ class="animation-figure" } -

Figure 4-4   Array Element Deletion Example

+

Figure 4-4   Array element deletion example

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
Full Screen >
-### 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
Full Screen >
-### 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
Full Screen >
-## 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. diff --git a/en/docs/chapter_array_and_linkedlist/index.md b/en/docs/chapter_array_and_linkedlist/index.md index 138ed3cc5..77bd560c5 100644 --- a/en/docs/chapter_array_and_linkedlist/index.md +++ b/en/docs/chapter_array_and_linkedlist/index.md @@ -3,9 +3,9 @@ comments: true icon: material/view-list-outline --- -# Chapter 4.   Arrays and Linked Lists +# Chapter 4.   Arrays and linked lists -![Arrays and Linked Lists](../assets/covers/chapter_array_and_linkedlist.jpg){ class="cover-image" } +![Arrays and linked lists](../assets/covers/chapter_array_and_linkedlist.jpg){ 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/) diff --git a/en/docs/chapter_array_and_linkedlist/linked_list.md b/en/docs/chapter_array_and_linkedlist/linked_list.md index 120c333fe..bae9b71a8 100755 --- a/en/docs/chapter_array_and_linkedlist/linked_list.md +++ b/en/docs/chapter_array_and_linkedlist/linked_list.md @@ -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. -![Linked List Definition and Storage Method](linked_list.assets/linkedlist_definition.png){ class="animation-figure" } +![Linked list definition and storage method](linked_list.assets/linkedlist_definition.png){ class="animation-figure" } -

Figure 4-5   Linked List Definition and Storage Method

+

Figure 4-5   Linked list definition and storage method

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. -![Linked List Node Insertion Example](linked_list.assets/linkedlist_insert_node.png){ class="animation-figure" } +![Linked list node insertion example](linked_list.assets/linkedlist_insert_node.png){ class="animation-figure" } -

Figure 4-6   Linked List Node Insertion Example

+

Figure 4-6   Linked list node insertion example

=== "Python" @@ -580,15 +580,15 @@ By comparison, inserting an element into an array has a time complexity of $O(n)
Full Screen >
-### 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. -![Linked List Node Deletion](linked_list.assets/linkedlist_remove_node.png){ class="animation-figure" } +![Linked list node deletion](linked_list.assets/linkedlist_remove_node.png){ class="animation-figure" } -

Figure 4-7   Linked List Node Deletion

+

Figure 4-7   Linked list node deletion

=== "Python" @@ -796,7 +796,7 @@ It's important to note that even though node `P` continues to point to `n1` afte
Full Screen >
-### 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
Full Screen >
-### 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
Full Screen >
-## 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. -

Table 4-1   Efficiency Comparison of Arrays and Linked Lists

+

Table 4-1   Efficiency comparison of arrays and linked lists

@@ -1260,19 +1260,19 @@ The Table 4-1 summarizes the characteristics of arrays and linked lists, and it
-## 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. } ``` -![Common Types of Linked Lists](linked_list.assets/linkedlist_common_types.png){ class="animation-figure" } +![Common types of linked lists](linked_list.assets/linkedlist_common_types.png){ class="animation-figure" } -

Figure 4-8   Common Types of Linked Lists

+

Figure 4-8   Common types of linked lists

-## 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. diff --git a/en/docs/chapter_array_and_linkedlist/list.md b/en/docs/chapter_array_and_linkedlist/list.md index 5b06dd370..98b629741 100755 --- a/en/docs/chapter_array_and_linkedlist/list.md +++ b/en/docs/chapter_array_and_linkedlist/list.md @@ -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" diff --git a/en/docs/chapter_array_and_linkedlist/ram_and_cache.md b/en/docs/chapter_array_and_linkedlist/ram_and_cache.md index cab66850e..7bd83efff 100644 --- a/en/docs/chapter_array_and_linkedlist/ram_and_cache.md +++ b/en/docs/chapter_array_and_linkedlist/ram_and_cache.md @@ -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. -

Table 4-2   Computer Storage Devices

+

Table 4-2   Computer storage devices

@@ -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. -![Computer Storage System](ram_and_cache.assets/storage_pyramid.png){ class="animation-figure" } +![Computer storage system](ram_and_cache.assets/storage_pyramid.png){ class="animation-figure" } -

Figure 4-9   Computer Storage System

+

Figure 4-9   Computer storage system

!!! 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. -![Data Flow Between Hard Disk, Memory, and Cache](ram_and_cache.assets/computer_storage_devices.png){ class="animation-figure" } +![Data flow between hard disk, memory, and cache](ram_and_cache.assets/computer_storage_devices.png){ class="animation-figure" } -

Figure 4-10   Data Flow Between Hard Disk, Memory, and Cache

+

Figure 4-10   Data flow between hard disk, memory, and cache

-## 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. diff --git a/en/docs/chapter_array_and_linkedlist/summary.md b/en/docs/chapter_array_and_linkedlist/summary.md index f47ffdc0e..91def0485 100644 --- a/en/docs/chapter_array_and_linkedlist/summary.md +++ b/en/docs/chapter_array_and_linkedlist/summary.md @@ -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`? diff --git a/en/docs/chapter_computational_complexity/index.md b/en/docs/chapter_computational_complexity/index.md index 4889d18d9..271507406 100644 --- a/en/docs/chapter_computational_complexity/index.md +++ b/en/docs/chapter_computational_complexity/index.md @@ -3,9 +3,9 @@ comments: true icon: material/timer-sand --- -# Chapter 2.   Complexity Analysis +# Chapter 2.   Complexity analysis -![complexity_analysis](../assets/covers/chapter_complexity_analysis.jpg){ class="cover-image" } +![Complexity analysis](../assets/covers/chapter_complexity_analysis.jpg){ 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/) diff --git a/en/docs/chapter_computational_complexity/iteration_and_recursion.md b/en/docs/chapter_computational_complexity/iteration_and_recursion.md index f4505974b..2c247982c 100644 --- a/en/docs/chapter_computational_complexity/iteration_and_recursion.md +++ b/en/docs/chapter_computational_complexity/iteration_and_recursion.md @@ -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. -![Flowchart of the Sum Function](iteration_and_recursion.assets/iteration.png){ class="animation-figure" } +![Flowchart of the sum function](iteration_and_recursion.assets/iteration.png){ class="animation-figure" } -

Figure 2-1   Flowchart of the Sum Function

+

Figure 2-1   Flowchart of the sum function

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. -![Flowchart of the Nested Loop](iteration_and_recursion.assets/nested_iteration.png){ class="animation-figure" } +![Flowchart of the nested loop](iteration_and_recursion.assets/nested_iteration.png){ class="animation-figure" } -

Figure 2-2   Flowchart of the Nested Loop

+

Figure 2-2   Flowchart of the nested loop

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. -![Recursive Process of the Sum Function](iteration_and_recursion.assets/recursion_sum.png){ class="animation-figure" } +![Recursive process of the sum function](iteration_and_recursion.assets/recursion_sum.png){ class="animation-figure" } -

Figure 2-3   Recursive Process of the Sum Function

+

Figure 2-3   Recursive process of the sum function

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$**. -![Recursion Call Depth](iteration_and_recursion.assets/recursion_sum_depth.png){ class="animation-figure" } +![Recursion call depth](iteration_and_recursion.assets/recursion_sum_depth.png){ class="animation-figure" } -

Figure 2-4   Recursion Call Depth

+

Figure 2-4   Recursion call depth

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. -![Tail Recursion Process](iteration_and_recursion.assets/tail_recursion_sum.png){ class="animation-figure" } +![Tail recursion process](iteration_and_recursion.assets/tail_recursion_sum.png){ class="animation-figure" } -

Figure 2-5   Tail Recursion Process

+

Figure 2-5   Tail recursion process

!!! 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$. -![Fibonacci Sequence Recursion Tree](iteration_and_recursion.assets/recursion_tree.png){ class="animation-figure" } +![Fibonacci sequence recursion tree](iteration_and_recursion.assets/recursion_tree.png){ class="animation-figure" } -

Figure 2-6   Fibonacci Sequence Recursion Tree

+

Figure 2-6   Fibonacci sequence recursion tree

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. -

Table: Comparison of Iteration and Recursion Characteristics

+

Table: Comparison of iteration and recursion characteristics

diff --git a/en/docs/chapter_computational_complexity/performance_evaluation.md b/en/docs/chapter_computational_complexity/performance_evaluation.md index 360747f2b..e0408ddda 100644 --- a/en/docs/chapter_computational_complexity/performance_evaluation.md +++ b/en/docs/chapter_computational_complexity/performance_evaluation.md @@ -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." diff --git a/en/docs/chapter_computational_complexity/space_complexity.md b/en/docs/chapter_computational_complexity/space_complexity.md index 8368bbfe5..00e80844b 100644 --- a/en/docs/chapter_computational_complexity/space_complexity.md +++ b/en/docs/chapter_computational_complexity/space_complexity.md @@ -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 . -![Space Types Used in Algorithms](space_complexity.assets/space_types.png){ class="animation-figure" } +![Space types used in algorithms](space_complexity.assets/space_types.png){ class="animation-figure" } -

Figure 2-15   Space Types Used in Algorithms

+

Figure 2-15   Space types used in algorithms

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>>, } - /* 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} $$ -![Common Types of Space Complexity](space_complexity.assets/space_complexity_common_types.png){ class="animation-figure" } +![Common types of space complexity](space_complexity.assets/space_complexity_common_types.png){ class="animation-figure" } -

Figure 2-16   Common Types of Space Complexity

+

Figure 2-16   Common types of space complexity

-### 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
-### 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
-![Recursive Function Generating Linear Order Space Complexity](space_complexity.assets/space_complexity_recursive_linear.png){ class="animation-figure" } +![Recursive function generating linear order space complexity](space_complexity.assets/space_complexity_recursive_linear.png){ class="animation-figure" } -

Figure 2-17   Recursive Function Generating Linear Order Space Complexity

+

Figure 2-17   Recursive function generating linear order space complexity

-### 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
-![Recursive Function Generating Quadratic Order Space Complexity](space_complexity.assets/space_complexity_recursive_quadratic.png){ class="animation-figure" } +![Recursive function generating quadratic order space complexity](space_complexity.assets/space_complexity_recursive_quadratic.png){ class="animation-figure" } -

Figure 2-18   Recursive Function Generating Quadratic Order Space Complexity

+

Figure 2-18   Recursive function generating quadratic order space complexity

-### 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
-![Full Binary Tree Generating Exponential Order Space Complexity](space_complexity.assets/space_complexity_exponential.png){ class="animation-figure" } +![Full binary tree generating exponential order space complexity](space_complexity.assets/space_complexity_exponential.png){ class="animation-figure" } -

Figure 2-19   Full Binary Tree Generating Exponential Order Space Complexity

+

Figure 2-19   Full binary tree generating exponential order space complexity

-### 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. diff --git a/en/docs/chapter_computational_complexity/summary.md b/en/docs/chapter_computational_complexity/summary.md index 74f3d218e..7660f1739 100644 --- a/en/docs/chapter_computational_complexity/summary.md +++ b/en/docs/chapter_computational_complexity/summary.md @@ -4,7 +4,7 @@ comments: true # 2.5   Summary -### 1.   Key Review +### 1.   Key review **Algorithm Efficiency Assessment** diff --git a/en/docs/chapter_computational_complexity/time_complexity.md b/en/docs/chapter_computational_complexity/time_complexity.md index 95065b088..86775f380 100644 --- a/en/docs/chapter_computational_complexity/time_complexity.md +++ b/en/docs/chapter_computational_complexity/time_complexity.md @@ -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." -![Time Growth Trend of Algorithms A, B, and C](time_complexity.assets/time_complexity_simple_example.png){ class="animation-figure" } +![Time growth trend of algorithms a, b, and c](time_complexity.assets/time_complexity_simple_example.png){ class="animation-figure" } -

Figure 2-7   Time Growth Trend of Algorithms A, B, and C

+

Figure 2-7   Time growth trend of algorithms a, b, and c

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$. -![Asymptotic Upper Bound of a Function](time_complexity.assets/asymptotic_upper_bound.png){ class="animation-figure" } +![Asymptotic upper bound of a function](time_complexity.assets/asymptotic_upper_bound.png){ class="animation-figure" } -

Figure 2-8   Asymptotic Upper Bound of a Function

+

Figure 2-8   Asymptotic upper bound of a function

-## 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. -

Table: Time Complexity for Different Operation Counts

+

Table: Time complexity for different operation counts

@@ -961,7 +961,7 @@ The following table illustrates examples of different operation counts and their
-## 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} $$ -![Common Types of Time Complexity](time_complexity.assets/time_complexity_common_types.png){ class="animation-figure" } +![Common types of time complexity](time_complexity.assets/time_complexity_common_types.png){ class="animation-figure" } -

Figure 2-9   Common Types of Time Complexity

+

Figure 2-9   Common types of time complexity

-### 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
-### 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. -![Constant, Linear, and Quadratic Order Time Complexities](time_complexity.assets/time_complexity_constant_linear_quadratic.png){ class="animation-figure" } +![Constant, linear, and quadratic order time complexities](time_complexity.assets/time_complexity_constant_linear_quadratic.png){ class="animation-figure" } -

Figure 2-10   Constant, Linear, and Quadratic Order Time Complexities

+

Figure 2-10   Constant, linear, and quadratic order time complexities

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
-### 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
-![Exponential Order Time Complexity](time_complexity.assets/time_complexity_exponential.png){ class="animation-figure" } +![Exponential order time complexity](time_complexity.assets/time_complexity_exponential.png){ class="animation-figure" } -

Figure 2-11   Exponential Order Time Complexity

+

Figure 2-11   Exponential order time complexity

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
-![Logarithmic Order Time Complexity](time_complexity.assets/time_complexity_logarithmic.png){ class="animation-figure" } +![Logarithmic order time complexity](time_complexity.assets/time_complexity_logarithmic.png){ class="animation-figure" } -

Figure 2-12   Logarithmic Order Time Complexity

+

Figure 2-12   Logarithmic order time complexity

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)$. -![Linear-Logarithmic Order Time Complexity](time_complexity.assets/time_complexity_logarithmic_linear.png){ class="animation-figure" } +![Linear-logarithmic order time complexity](time_complexity.assets/time_complexity_logarithmic_linear.png){ class="animation-figure" } -

Figure 2-13   Linear-Logarithmic Order Time Complexity

+

Figure 2-13   Linear-logarithmic order time complexity

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
-![Factorial Order Time Complexity](time_complexity.assets/time_complexity_factorial.png){ class="animation-figure" } +![Factorial order time complexity](time_complexity.assets/time_complexity_factorial.png){ class="animation-figure" } -

Figure 2-14   Factorial Order Time Complexity

+

Figure 2-14   Factorial order time complexity

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: diff --git a/en/docs/chapter_data_structure/basic_data_types.md b/en/docs/chapter_data_structure/basic_data_types.md index ad9577863..ed51cf820 100644 --- a/en/docs/chapter_data_structure/basic_data_types.md +++ b/en/docs/chapter_data_structure/basic_data_types.md @@ -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. -

Table 3-1   Space Occupied and Value Range of Basic Data Types

+

Table 3-1   Space occupied and value range of basic data types

diff --git a/en/docs/chapter_data_structure/character_encoding.md b/en/docs/chapter_data_structure/character_encoding.md index 37cc0610a..4f770baac 100644 --- a/en/docs/chapter_data_structure/character_encoding.md +++ b/en/docs/chapter_data_structure/character_encoding.md @@ -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). -![ASCII Code](character_encoding.assets/ascii_table.png){ class="animation-figure" } +![ASCII code](character_encoding.assets/ascii_table.png){ class="animation-figure" } -

Figure 3-6   ASCII Code

+

Figure 3-6   ASCII code

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. -![Unicode Encoding Example](character_encoding.assets/unicode_hello_algo.png){ class="animation-figure" } +![Unicode encoding example](character_encoding.assets/unicode_hello_algo.png){ class="animation-figure" } -

Figure 3-7   Unicode Encoding Example

+

Figure 3-7   Unicode encoding example

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. -![UTF-8 Encoding Example](character_encoding.assets/utf-8_hello_algo.png){ class="animation-figure" } +![UTF-8 encoding example](character_encoding.assets/utf-8_hello_algo.png){ class="animation-figure" } -

Figure 3-8   UTF-8 Encoding Example

+

Figure 3-8   UTF-8 encoding example

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: diff --git a/en/docs/chapter_data_structure/classification_of_data_structure.md b/en/docs/chapter_data_structure/classification_of_data_structure.md index dd5c3fa19..7d6b26ceb 100644 --- a/en/docs/chapter_data_structure/classification_of_data_structure.md +++ b/en/docs/chapter_data_structure/classification_of_data_structure.md @@ -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. -![Linear and Non-Linear Data Structures](classification_of_data_structure.assets/classification_logic_structure.png){ class="animation-figure" } +![Linear and non-linear data structures](classification_of_data_structure.assets/classification_logic_structure.png){ class="animation-figure" } -

Figure 3-1   Linear and Non-Linear Data Structures

+

Figure 3-1   Linear and non-linear data structures

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. -![Memory Stick, Memory Spaces, Memory Addresses](classification_of_data_structure.assets/computer_memory_location.png){ class="animation-figure" } +![Memory stick, memory spaces, memory addresses](classification_of_data_structure.assets/computer_memory_location.png){ class="animation-figure" } -

Figure 3-2   Memory Stick, Memory Spaces, Memory Addresses

+

Figure 3-2   Memory stick, memory spaces, memory addresses

!!! 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. -![Contiguous Space Storage and Dispersed Space Storage](classification_of_data_structure.assets/classification_phisical_structure.png){ class="animation-figure" } +![Contiguous space storage and dispersed space storage](classification_of_data_structure.assets/classification_phisical_structure.png){ class="animation-figure" } -

Figure 3-3   Contiguous Space Storage and Dispersed Space Storage

+

Figure 3-3   Contiguous space storage and dispersed space storage

**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$). diff --git a/en/docs/chapter_data_structure/index.md b/en/docs/chapter_data_structure/index.md index 94c454dd6..b3a84f25e 100644 --- a/en/docs/chapter_data_structure/index.md +++ b/en/docs/chapter_data_structure/index.md @@ -3,9 +3,9 @@ comments: true icon: material/shape-outline --- -# Chapter 3.   Data Structures +# Chapter 3.   Data structures -![Data Structures](../assets/covers/chapter_data_structure.jpg){ class="cover-image" } +![Data structures](../assets/covers/chapter_data_structure.jpg){ 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/) diff --git a/en/docs/chapter_data_structure/number_encoding.md b/en/docs/chapter_data_structure/number_encoding.md index f4336a745..de7fe0dd2 100644 --- a/en/docs/chapter_data_structure/number_encoding.md +++ b/en/docs/chapter_data_structure/number_encoding.md @@ -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: -![Conversions between Sign-Magnitude, One's Complement, and Two's Complement](number_encoding.assets/1s_2s_complement.png){ class="animation-figure" } +![Conversions between sign-magnitude, one's complement, and two's complement](number_encoding.assets/1s_2s_complement.png){ class="animation-figure" } -

Figure 3-4   Conversions between Sign-Magnitude, One's Complement, and Two's Complement

+

Figure 3-4   Conversions between sign-magnitude, one's complement, and two's complement

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} $$ -![Example Calculation of a float in IEEE 754 Standard](number_encoding.assets/ieee_754_float.png){ class="animation-figure" } +![Example calculation of a float in IEEE 754 standard](number_encoding.assets/ieee_754_float.png){ class="animation-figure" } -

Figure 3-5   Example Calculation of a float in IEEE 754 Standard

+

Figure 3-5   Example calculation of a float in IEEE 754 standard

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.** -

Table 3-2   Meaning of Exponent Bits

+

Table 3-2   Meaning of exponent bits

diff --git a/en/docs/chapter_data_structure/summary.md b/en/docs/chapter_data_structure/summary.md index 58f1b338b..0da453092 100644 --- a/en/docs/chapter_data_structure/summary.md +++ b/en/docs/chapter_data_structure/summary.md @@ -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. diff --git a/en/docs/chapter_hashing/hash_algorithm.md b/en/docs/chapter_hashing/hash_algorithm.md index 8cb0f8a9e..ff685a9d1 100644 --- a/en/docs/chapter_hashing/hash_algorithm.md +++ b/en/docs/chapter_hashing/hash_algorithm.md @@ -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)$. -![Ideal and Worst Cases of Hash Collisions](hash_algorithm.assets/hash_collision_best_worst_condition.png){ class="animation-figure" } +![Ideal and worst cases of hash collisions](hash_algorithm.assets/hash_collision_best_worst_condition.png){ class="animation-figure" } -

Figure 6-8   Ideal and Worst Cases of Hash Collisions

+

Figure 6-8   Ideal and worst cases of hash collisions

**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. -

Table 6-2   Common Hash Algorithms

+

Table 6-2   Common hash algorithms

@@ -677,7 +677,7 @@ Over the past century, hash algorithms have been in a continuous process of upgr
-# 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. diff --git a/en/docs/chapter_hashing/hash_collision.md b/en/docs/chapter_hashing/hash_collision.md index a9a9eaa60..0924e0023 100644 --- a/en/docs/chapter_hashing/hash_collision.md +++ b/en/docs/chapter_hashing/hash_collision.md @@ -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. -![Separate Chaining Hash Table](hash_collision.assets/hash_table_chaining.png){ class="animation-figure" } +![Separate chaining hash table](hash_collision.assets/hash_table_chaining.png){ class="animation-figure" } -

Figure 6-5   Separate Chaining Hash Table

+

Figure 6-5   Separate chaining hash table

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. -![Distribution of Key-Value Pairs in Open Addressing (Linear Probing) Hash Table](hash_collision.assets/hash_table_linear_probing.png){ class="animation-figure" } +![Distribution of key-value pairs in open addressing (linear probing) hash table](hash_collision.assets/hash_table_linear_probing.png){ class="animation-figure" } -

Figure 6-6   Distribution of Key-Value Pairs in Open Addressing (Linear Probing) Hash Table

+

Figure 6-6   Distribution of key-value pairs in open addressing (linear probing) hash table

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 . -![Query Issues Caused by Deletion in Open Addressing](hash_collision.assets/hash_table_open_addressing_deletion.png){ class="animation-figure" } +![Query issues caused by deletion in open addressing](hash_collision.assets/hash_table_open_addressing_deletion.png){ class="animation-figure" } -

Figure 6-7   Query Issues Caused by Deletion in Open Addressing

+

Figure 6-7   Query issues caused by deletion in open addressing

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: diff --git a/en/docs/chapter_hashing/hash_map.md b/en/docs/chapter_hashing/hash_map.md index 9c31db5bd..6a0f5a02d 100755 --- a/en/docs/chapter_hashing/hash_map.md +++ b/en/docs/chapter_hashing/hash_map.md @@ -2,7 +2,7 @@ comments: true --- -# 6.1   Hash Table +# 6.1   Hash table A "hash table", also known as a "hash map", achieves efficient element querying by establishing a mapping between keys and values. Specifically, when we input a `key` into the hash table, we can retrieve the corresponding `value` in $O(1)$ time. @@ -14,11 +14,11 @@ As shown in the Figure 6-1 , given $n$ students, each with two pieces of data: " Apart from hash tables, arrays and linked lists can also be used to implement querying functions. Their efficiency is compared in the Table 6-1 . -- **Adding Elements**: Simply add the element to the end of the array (or linked list), using $O(1)$ time. -- **Querying Elements**: Since the array (or linked list) is unordered, it requires traversing all the elements, using $O(n)$ time. -- **Deleting Elements**: First, locate the element, then delete it from the array (or linked list), using $O(n)$ time. +- **Adding elements**: Simply add the element to the end of the array (or linked list), using $O(1)$ time. +- **Querying elements**: Since the array (or linked list) is unordered, it requires traversing all the elements, using $O(n)$ time. +- **Deleting elements**: First, locate the element, then delete it from the array (or linked list), using $O(n)$ time. -

Table 6-1   Comparison of Element Query Efficiency

+

Table 6-1   Comparison of element query efficiency

@@ -32,7 +32,7 @@ Apart from hash tables, arrays and linked lists can also be used to implement qu Observations reveal that **the time complexity for adding, deleting, and querying in a hash table is $O(1)$**, which is highly efficient. -## 6.1.1   Common Operations of Hash Table +## 6.1.1   Common operations of hash table Common operations of a hash table include initialization, querying, adding key-value pairs, and deleting key-value pairs, etc. Example code is as follows: @@ -496,7 +496,7 @@ There are three common ways to traverse a hash table: traversing key-value pairs
-## 6.1.2   Simple Implementation of Hash Table +## 6.1.2   Simple implementation of hash table First, let's consider the simplest case: **implementing a hash table using just an array**. In the hash table, each empty slot in the array is called a "bucket", and each bucket can store one key-value pair. Therefore, the query operation involves finding the bucket corresponding to the `key` and retrieving the `value` from it. @@ -1819,7 +1819,7 @@ The following code implements a simple hash table. Here, we encapsulate `key` an
-## 6.1.3   Hash Collision and Resizing +## 6.1.3   Hash collision and resizing Fundamentally, the role of the hash function is to map the entire input space of all keys to the output space of all array indices. However, the input space is often much larger than the output space. Therefore, **theoretically, there must be situations where "multiple inputs correspond to the same output"**. diff --git a/en/docs/chapter_hashing/index.md b/en/docs/chapter_hashing/index.md index 78c1284c5..0e9bd5f36 100644 --- a/en/docs/chapter_hashing/index.md +++ b/en/docs/chapter_hashing/index.md @@ -3,9 +3,9 @@ comments: true icon: material/table-search --- -# Chapter 6.   Hash Table +# Chapter 6.   Hash table -![Hash Table](../assets/covers/chapter_hashing.jpg){ class="cover-image" } +![Hash table](../assets/covers/chapter_hashing.jpg){ 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/) diff --git a/en/docs/chapter_hashing/summary.md b/en/docs/chapter_hashing/summary.md index ec68c7859..780892101 100644 --- a/en/docs/chapter_hashing/summary.md +++ b/en/docs/chapter_hashing/summary.md @@ -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. diff --git a/en/docs/chapter_introduction/algorithms_are_everywhere.md b/en/docs/chapter_introduction/algorithms_are_everywhere.md index 111362b36..53ac82d77 100644 --- a/en/docs/chapter_introduction/algorithms_are_everywhere.md +++ b/en/docs/chapter_introduction/algorithms_are_everywhere.md @@ -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. -![Playing Cards Sorting Process](algorithms_are_everywhere.assets/playing_cards_sorting.png){ class="animation-figure" } +![Playing cards sorting process](algorithms_are_everywhere.assets/playing_cards_sorting.png){ class="animation-figure" } -

Figure 1-2   Playing Cards Sorting Process

+

Figure 1-2   Playing cards sorting process

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. diff --git a/en/docs/chapter_introduction/index.md b/en/docs/chapter_introduction/index.md index 553205b82..6274b38a0 100644 --- a/en/docs/chapter_introduction/index.md +++ b/en/docs/chapter_introduction/index.md @@ -3,9 +3,9 @@ comments: true icon: material/calculator-variant-outline --- -# Chapter 1.   Introduction to Algorithms +# Chapter 1.   Introduction to algorithms -![A first look at the algorithm](../assets/covers/chapter_introduction.jpg){ class="cover-image" } +![Introduction to algorithms](../assets/covers/chapter_introduction.jpg){ 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/) diff --git a/en/docs/chapter_introduction/what_is_dsa.md b/en/docs/chapter_introduction/what_is_dsa.md index 2fb032f9c..d4a533ce9 100644 --- a/en/docs/chapter_introduction/what_is_dsa.md +++ b/en/docs/chapter_introduction/what_is_dsa.md @@ -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 . -

Table 1-1   Comparing Data Structures and Algorithms to Building Blocks

+

Table 1-1   Comparing data structures and algorithms to building blocks

diff --git a/en/docs/chapter_preface/about_the_book.md b/en/docs/chapter_preface/about_the_book.md index 7b94c3955..e2cba7191 100644 --- a/en/docs/chapter_preface/about_the_book.md +++ b/en/docs/chapter_preface/about_the_book.md @@ -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. -![Main Content of the Book](about_the_book.assets/hello_algo_mindmap.png){ class="animation-figure" } +![Main content of the book](about_the_book.assets/hello_algo_mindmap.png){ class="animation-figure" } -

Figure 0-1   Main Content of the Book

+

Figure 0-1   Main content of the book

## 0.1.3   Acknowledgements diff --git a/en/docs/chapter_preface/index.md b/en/docs/chapter_preface/index.md index 34a61d829..5d285a50c 100644 --- a/en/docs/chapter_preface/index.md +++ b/en/docs/chapter_preface/index.md @@ -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/) diff --git a/en/docs/chapter_preface/suggestions.md b/en/docs/chapter_preface/suggestions.md index 113ca2094..a5886afe4 100644 --- a/en/docs/chapter_preface/suggestions.md +++ b/en/docs/chapter_preface/suggestions.md @@ -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, array. 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. -![Animated Illustration Example](../index.assets/animation.gif){ class="animation-figure" } +![Animated illustration example](../index.assets/animation.gif){ class="animation-figure" } -

Figure 0-2   Animated Illustration Example

+

Figure 0-2   Animated illustration example

-## 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.** -![Running Code Example](../index.assets/running_code.gif){ class="animation-figure" } +![Running code example](../index.assets/running_code.gif){ class="animation-figure" } -

Figure 0-3   Running Code Example

+

Figure 0-3   Running code example

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. -![Cloning Repository and Downloading Code](suggestions.assets/download_code.png){ class="animation-figure" } +![Cloning repository and downloading code](suggestions.assets/download_code.png){ class="animation-figure" } -

Figure 0-4   Cloning Repository and Downloading Code

+

Figure 0-4   Cloning repository and downloading code

**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. -![Code Block and Corresponding Source Code File](suggestions.assets/code_md_to_repo.png){ class="animation-figure" } +![Code block and corresponding source code file](suggestions.assets/code_md_to_repo.png){ class="animation-figure" } -

Figure 0-5   Code Block and Corresponding Source Code File

+

Figure 0-5   Code block and corresponding source code file

-## 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. -![Comment Section Example](../index.assets/comment.gif){ class="animation-figure" } +![Comment section example](../index.assets/comment.gif){ class="animation-figure" } -

Figure 0-6   Comment Section Example

+

Figure 0-6   Comment section example

-## 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. -![Algorithm Learning Path](suggestions.assets/learning_route.png){ class="animation-figure" } +![Algorithm learning path](suggestions.assets/learning_route.png){ class="animation-figure" } -

Figure 0-7   Algorithm Learning Path

+

Figure 0-7   Algorithm learning path

diff --git a/en/docs/chapter_stack_and_queue/deque.md b/en/docs/chapter_stack_and_queue/deque.md index 5238a0b05..c8123ce64 100644 --- a/en/docs/chapter_stack_and_queue/deque.md +++ b/en/docs/chapter_stack_and_queue/deque.md @@ -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. -![Operations in Double-Ended Queue](deque.assets/deque_operations.png){ class="animation-figure" } +![Operations in double-ended queue](deque.assets/deque_operations.png){ class="animation-figure" } -

Figure 5-7   Operations in Double-Ended Queue

+

Figure 5-7   Operations in double-ended queue

-## 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. -

Table 5-3   Efficiency of Double-Ended Queue Operations

+

Table 5-3   Efficiency of double-ended queue operations

@@ -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**. diff --git a/en/docs/chapter_stack_and_queue/index.md b/en/docs/chapter_stack_and_queue/index.md index 34cb5130f..746f3ed54 100644 --- a/en/docs/chapter_stack_and_queue/index.md +++ b/en/docs/chapter_stack_and_queue/index.md @@ -3,9 +3,9 @@ comments: true icon: material/stack-overflow --- -# Chapter 5.   Stack and Queue +# Chapter 5.   Stack and queue -![Stack and Queue](../assets/covers/chapter_stack_and_queue.jpg){ class="cover-image" } +![Stack and queue](../assets/covers/chapter_stack_and_queue.jpg){ 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/) diff --git a/en/docs/chapter_stack_and_queue/queue.md b/en/docs/chapter_stack_and_queue/queue.md index c90075361..31e8f359d 100755 --- a/en/docs/chapter_stack_and_queue/queue.md +++ b/en/docs/chapter_stack_and_queue/queue.md @@ -8,15 +8,15 @@ comments: true As shown in the Figure 5-4 , we call the front of the queue the "head" and the back the "tail." The operation of adding elements to the rear of the queue is termed "enqueue," and the operation of removing elements from the front is termed "dequeue." -![Queue's First-In-First-Out Rule](queue.assets/queue_operations.png){ class="animation-figure" } +![Queue's first-in-first-out rule](queue.assets/queue_operations.png){ class="animation-figure" } -

Figure 5-4   Queue's First-In-First-Out Rule

+

Figure 5-4   Queue's first-in-first-out rule

-## 5.2.1   Common Operations on Queue +## 5.2.1   Common operations on queue The common operations on a queue are shown in the Table 5-2 . Note that method names may vary across different programming languages. Here, we use the same naming convention as that used for stacks. -

Table 5-2   Efficiency of Queue Operations

+

Table 5-2   Efficiency of queue operations

@@ -329,11 +329,11 @@ We can directly use the ready-made queue classes in programming languages:
-## 5.2.2   Implementing a Queue +## 5.2.2   Implementing a queue To implement a queue, we need a data structure that allows adding elements at one end and removing them at the other. Both linked lists and arrays meet this requirement. -### 1.   Implementation Based on a Linked List +### 1.   Implementation based on a linked list As shown in the Figure 5-5 , we can consider the "head node" and "tail node" of a linked list as the "front" and "rear" of the queue, respectively. It is stipulated that nodes can only be added at the rear and removed at the front. @@ -1296,7 +1296,7 @@ Below is the code for implementing a queue using a linked list:
-### 2.   Implementation Based on an Array +### 2.   Implementation based on an array Deleting the first element in an array has a time complexity of $O(n)$, which would make the dequeue operation inefficient. However, this problem can be cleverly avoided as follows. @@ -2289,7 +2289,7 @@ The above implementation of the queue still has its limitations: its length is f The comparison of the two implementations is consistent with that of the stack and is not repeated here. -## 5.2.3   Typical Applications of Queue +## 5.2.3   Typical applications of queue -- **Amazon Orders**. After shoppers place orders, these orders join a queue, and the system processes them in order. During events like Singles' Day, a massive number of orders are generated in a short time, making high concurrency a key challenge for engineers. -- **Various To-Do Lists**. Any scenario requiring a "first-come, first-served" functionality, such as a printer's task queue or a restaurant's food delivery queue, can effectively maintain the order of processing with a queue. +- **Amazon orders**: After shoppers place orders, these orders join a queue, and the system processes them in order. During events like Singles' Day, a massive number of orders are generated in a short time, making high concurrency a key challenge for engineers. +- **Various to-do lists**: Any scenario requiring a "first-come, first-served" functionality, such as a printer's task queue or a restaurant's food delivery queue, can effectively maintain the order of processing with a queue. diff --git a/en/docs/chapter_stack_and_queue/stack.md b/en/docs/chapter_stack_and_queue/stack.md index 4a95910c9..161015a7a 100755 --- a/en/docs/chapter_stack_and_queue/stack.md +++ b/en/docs/chapter_stack_and_queue/stack.md @@ -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." -![Stack's Last-In-First-Out Rule](stack.assets/stack_operations.png){ class="animation-figure" } +![Stack's last-in-first-out rule](stack.assets/stack_operations.png){ class="animation-figure" } -

Figure 5-1   Stack's Last-In-First-Out Rule

+

Figure 5-1   Stack's last-in-first-out rule

-## 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. -

Table 5-1   Efficiency of Stack Operations

+

Table 5-1   Efficiency of stack operations

@@ -323,13 +323,13 @@ Typically, we can directly use the stack class built into the programming langua
-## 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:
-### 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
-## 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. diff --git a/en/docs/chapter_stack_and_queue/summary.md b/en/docs/chapter_stack_and_queue/summary.md index 818277bf8..ad4d9c7d1 100644 --- a/en/docs/chapter_stack_and_queue/summary.md +++ b/en/docs/chapter_stack_and_queue/summary.md @@ -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. diff --git a/overrides/partials/comments.html b/overrides/partials/comments.html index 6f02cdf8c..75caa1ada 100755 --- a/overrides/partials/comments.html +++ b/overrides/partials/comments.html @@ -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' %} diff --git a/zh-Hant/docs/chapter_appendix/contribution.md b/zh-Hant/docs/chapter_appendix/contribution.md new file mode 100644 index 000000000..48df90d30 --- /dev/null +++ b/zh-Hant/docs/chapter_appendix/contribution.md @@ -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”按鈕即可發起拉取請求。 + +![頁面編輯按鍵](contribution.assets/edit_markdown.png){ class="animation-figure" } + +

圖 16-3   頁面編輯按鍵

+ +圖片無法直接修改,需要透過新建 [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 +``` diff --git a/zh-Hant/docs/chapter_appendix/index.md b/zh-Hant/docs/chapter_appendix/index.md new file mode 100644 index 000000000..f5149e85c --- /dev/null +++ b/zh-Hant/docs/chapter_appendix/index.md @@ -0,0 +1,14 @@ +--- +comments: true +icon: material/help-circle-outline +--- + +# 第 16 章   附錄 + +![附錄](../assets/covers/chapter_appendix.jpg){ 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/) diff --git a/zh-Hant/docs/chapter_appendix/installation.md b/zh-Hant/docs/chapter_appendix/installation.md new file mode 100644 index 000000000..1bfd028aa --- /dev/null +++ b/zh-Hant/docs/chapter_appendix/installation.md @@ -0,0 +1,71 @@ +--- +comments: true +--- + +# 16.1   程式設計環境安裝 + +## 16.1.1   安裝 IDE + +推薦使用開源、輕量的 VS Code 作為本地整合開發環境(IDE)。訪問 [VS Code 官網](https://code.visualstudio.com/),根據作業系統選擇相應版本的 VS Code 進行下載和安裝。 + +![從官網下載 VS Code](installation.assets/vscode_installation.png){ class="animation-figure" } + +

圖 16-1   從官網下載 VS Code

+ +VS Code 擁有強大的擴展包生態系統,支持大多數程式語言的執行和除錯。以 Python 為例,安裝“Python Extension Pack”擴展包之後,即可進行 Python 程式碼除錯。安裝步驟如圖 16-2 所示。 + +![安裝 VS Code 擴展包](installation.assets/vscode_extension_installation.png){ class="animation-figure" } + +

圖 16-2   安裝 VS Code 擴展包

+ +## 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) 。 diff --git a/zh-Hant/docs/chapter_appendix/terminology.md b/zh-Hant/docs/chapter_appendix/terminology.md new file mode 100644 index 000000000..2d26c8466 --- /dev/null +++ b/zh-Hant/docs/chapter_appendix/terminology.md @@ -0,0 +1,144 @@ +--- +comments: true +--- + +# 16.3   術語表 + +表 16-1 列出了書中出現的重要術語,值得注意以下幾點。 + +- 建議記住名詞的英文叫法,以便閱讀英文文獻。 +- 部分名詞在簡體中文和繁體中文下的叫法不同。 + +

表 16-1   資料結構與演算法的重要名詞

+ +
+ +| 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 | 贪心算法 | 貪婪演算法 | + +
diff --git a/zh-Hant/docs/chapter_array_and_linkedlist/array.md b/zh-Hant/docs/chapter_array_and_linkedlist/array.md new file mode 100755 index 000000000..5147d4729 --- /dev/null +++ b/zh-Hant/docs/chapter_array_and_linkedlist/array.md @@ -0,0 +1,1462 @@ +--- +comments: true +--- + +# 4.1   陣列 + +陣列(array)是一種線性資料結構,其將相同型別的元素儲存在連續的記憶體空間中。我們將元素在陣列中的位置稱為該元素的索引(index)。圖 4-1 展示了陣列的主要概念和儲存方式。 + +![陣列定義與儲存方式](array.assets/array_definition.png){ class="animation-figure" } + +

圖 4-1   陣列定義與儲存方式

+ +## 4.1.1   陣列常用操作 + +### 1.   初始化陣列 + +我們可以根據需求選用陣列的兩種初始化方式:無初始值、給定初始值。在未指定初始值的情況下,大多數程式語言會將陣列元素初始化為 $0$ : + +=== "Python" + + ```python title="array.py" + # 初始化陣列 + arr: list[int] = [0] * 5 # [ 0, 0, 0, 0, 0 ] + nums: list[int] = [1, 3, 2, 5, 4] + ``` + +=== "C++" + + ```cpp title="array.cpp" + /* 初始化陣列 */ + // 儲存在堆疊上 + int arr[5]; + int nums[5] = { 1, 3, 2, 5, 4 }; + // 儲存在堆積上(需要手動釋放空間) + int* arr1 = new int[5]; + int* nums1 = new int[5] { 1, 3, 2, 5, 4 }; + ``` + +=== "Java" + + ```java title="array.java" + /* 初始化陣列 */ + int[] arr = new int[5]; // { 0, 0, 0, 0, 0 } + int[] nums = { 1, 3, 2, 5, 4 }; + ``` + +=== "C#" + + ```csharp title="array.cs" + /* 初始化陣列 */ + int[] arr = new int[5]; // [ 0, 0, 0, 0, 0 ] + int[] nums = [1, 3, 2, 5, 4]; + ``` + +=== "Go" + + ```go title="array.go" + /* 初始化陣列 */ + var arr [5]int + // 在 Go 中,指定長度時([5]int)為陣列,不指定長度時([]int)為切片 + // 由於 Go 的陣列被設計為在編譯期確定長度,因此只能使用常數來指定長度 + // 為了方便實現擴容 extend() 方法,以下將切片(Slice)看作陣列(Array) + nums := []int{1, 3, 2, 5, 4} + ``` + +=== "Swift" + + ```swift title="array.swift" + /* 初始化陣列 */ + let arr = Array(repeating: 0, count: 5) // [0, 0, 0, 0, 0] + let nums = [1, 3, 2, 5, 4] + ``` + +=== "JS" + + ```javascript title="array.js" + /* 初始化陣列 */ + var arr = new Array(5).fill(0); + var nums = [1, 3, 2, 5, 4]; + ``` + +=== "TS" + + ```typescript title="array.ts" + /* 初始化陣列 */ + let arr: number[] = new Array(5).fill(0); + let nums: number[] = [1, 3, 2, 5, 4]; + ``` + +=== "Dart" + + ```dart title="array.dart" + /* 初始化陣列 */ + List arr = List.filled(5, 0); // [0, 0, 0, 0, 0] + List nums = [1, 3, 2, 5, 4]; + ``` + +=== "Rust" + + ```rust title="array.rs" + /* 初始化陣列 */ + let arr: Vec = vec![0; 5]; // [0, 0, 0, 0, 0] + let nums: Vec = vec![1, 3, 2, 5, 4]; + ``` + +=== "C" + + ```c title="array.c" + /* 初始化陣列 */ + int arr[5] = { 0 }; // { 0, 0, 0, 0, 0 } + int nums[5] = { 1, 3, 2, 5, 4 }; + ``` + +=== "Kotlin" + + ```kotlin title="array.kt" + /* 初始化陣列 */ + var arr = IntArray(5) // { 0, 0, 0, 0, 0 } + var nums = intArrayOf(1, 3, 2, 5, 4) + ``` + +=== "Ruby" + + ```ruby title="array.rb" + # 初始化陣列 + arr = Array.new(5, 0) + nums = [1, 3, 2, 5, 4] + ``` + +=== "Zig" + + ```zig title="array.zig" + // 初始化陣列 + var arr = [_]i32{0} ** 5; // { 0, 0, 0, 0, 0 } + var nums = [_]i32{ 1, 3, 2, 5, 4 }; + ``` + +??? pythontutor "視覺化執行" + +
+ + +### 2.   訪問元素 + +陣列元素被儲存在連續的記憶體空間中,這意味著計算陣列元素的記憶體位址非常容易。給定陣列記憶體位址(首元素記憶體位址)和某個元素的索引,我們可以使用圖 4-2 所示的公式計算得到該元素的記憶體位址,從而直接訪問該元素。 + +![陣列元素的記憶體位址計算](array.assets/array_memory_location_calculation.png){ class="animation-figure" } + +

圖 4-2   陣列元素的記憶體位址計算

+ +觀察圖 4-2 ,我們發現陣列首個元素的索引為 $0$ ,這似乎有些反直覺,因為從 $1$ 開始計數會更自然。但從位址計算公式的角度看,**索引本質上是記憶體位址的偏移量**。首個元素的位址偏移量是 $0$ ,因此它的索引為 $0$ 是合理的。 + +在陣列中訪問元素非常高效,我們可以在 $O(1)$ 時間內隨機訪問陣列中的任意一個元素。 + +=== "Python" + + ```python title="array.py" + def random_access(nums: list[int]) -> int: + """隨機訪問元素""" + # 在區間 [0, len(nums)-1] 中隨機抽取一個數字 + random_index = random.randint(0, len(nums) - 1) + # 獲取並返回隨機元素 + random_num = nums[random_index] + return random_num + ``` + +=== "C++" + + ```cpp title="array.cpp" + /* 隨機訪問元素 */ + int randomAccess(int *nums, int size) { + // 在區間 [0, size) 中隨機抽取一個數字 + int randomIndex = rand() % size; + // 獲取並返回隨機元素 + int randomNum = nums[randomIndex]; + return randomNum; + } + ``` + +=== "Java" + + ```java title="array.java" + /* 隨機訪問元素 */ + int randomAccess(int[] nums) { + // 在區間 [0, nums.length) 中隨機抽取一個數字 + int randomIndex = ThreadLocalRandom.current().nextInt(0, nums.length); + // 獲取並返回隨機元素 + int randomNum = nums[randomIndex]; + return randomNum; + } + ``` + +=== "C#" + + ```csharp title="array.cs" + /* 隨機訪問元素 */ + int RandomAccess(int[] nums) { + Random random = new(); + // 在區間 [0, nums.Length) 中隨機抽取一個數字 + int randomIndex = random.Next(nums.Length); + // 獲取並返回隨機元素 + int randomNum = nums[randomIndex]; + return randomNum; + } + ``` + +=== "Go" + + ```go title="array.go" + /* 隨機訪問元素 */ + func randomAccess(nums []int) (randomNum int) { + // 在區間 [0, nums.length) 中隨機抽取一個數字 + randomIndex := rand.Intn(len(nums)) + // 獲取並返回隨機元素 + randomNum = nums[randomIndex] + return + } + ``` + +=== "Swift" + + ```swift title="array.swift" + /* 隨機訪問元素 */ + func randomAccess(nums: [Int]) -> Int { + // 在區間 [0, nums.count) 中隨機抽取一個數字 + let randomIndex = nums.indices.randomElement()! + // 獲取並返回隨機元素 + let randomNum = nums[randomIndex] + return randomNum + } + ``` + +=== "JS" + + ```javascript title="array.js" + /* 隨機訪問元素 */ + function randomAccess(nums) { + // 在區間 [0, nums.length) 中隨機抽取一個數字 + const random_index = Math.floor(Math.random() * nums.length); + // 獲取並返回隨機元素 + const random_num = nums[random_index]; + return random_num; + } + ``` + +=== "TS" + + ```typescript title="array.ts" + /* 隨機訪問元素 */ + function randomAccess(nums: number[]): number { + // 在區間 [0, nums.length) 中隨機抽取一個數字 + const random_index = Math.floor(Math.random() * nums.length); + // 獲取並返回隨機元素 + const random_num = nums[random_index]; + return random_num; + } + ``` + +=== "Dart" + + ```dart title="array.dart" + /* 隨機訪問元素 */ + int randomAccess(List nums) { + // 在區間 [0, nums.length) 中隨機抽取一個數字 + int randomIndex = Random().nextInt(nums.length); + // 獲取並返回隨機元素 + int randomNum = nums[randomIndex]; + return randomNum; + } + ``` + +=== "Rust" + + ```rust title="array.rs" + /* 隨機訪問元素 */ + fn random_access(nums: &[i32]) -> i32 { + // 在區間 [0, nums.len()) 中隨機抽取一個數字 + let random_index = rand::thread_rng().gen_range(0..nums.len()); + // 獲取並返回隨機元素 + let random_num = nums[random_index]; + random_num + } + ``` + +=== "C" + + ```c title="array.c" + /* 隨機訪問元素 */ + int randomAccess(int *nums, int size) { + // 在區間 [0, size) 中隨機抽取一個數字 + int randomIndex = rand() % size; + // 獲取並返回隨機元素 + int randomNum = nums[randomIndex]; + return randomNum; + } + ``` + +=== "Kotlin" + + ```kotlin title="array.kt" + /* 隨機訪問元素 */ + fun randomAccess(nums: IntArray): Int { + // 在區間 [0, nums.size) 中隨機抽取一個數字 + val randomIndex = ThreadLocalRandom.current().nextInt(0, nums.size) + // 獲取並返回隨機元素 + val randomNum = nums[randomIndex] + return randomNum + } + ``` + +=== "Ruby" + + ```ruby title="array.rb" + ### 隨機訪問元素 ### + def random_access(nums) + # 在區間 [0, nums.length) 中隨機抽取一個數字 + random_index = Random.rand(0...nums.length) + + # 獲取並返回隨機元素 + nums[random_index] + end + ``` + +=== "Zig" + + ```zig title="array.zig" + // 隨機訪問元素 + fn randomAccess(nums: []i32) i32 { + // 在區間 [0, nums.len) 中隨機抽取一個整數 + var randomIndex = std.crypto.random.intRangeLessThan(usize, 0, nums.len); + // 獲取並返回隨機元素 + var randomNum = nums[randomIndex]; + return randomNum; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +### 3.   插入元素 + +陣列元素在記憶體中是“緊挨著的”,它們之間沒有空間再存放任何資料。如圖 4-3 所示,如果想在陣列中間插入一個元素,則需要將該元素之後的所有元素都向後移動一位,之後再把元素賦值給該索引。 + +![陣列插入元素示例](array.assets/array_insert_element.png){ class="animation-figure" } + +

圖 4-3   陣列插入元素示例

+ +值得注意的是,由於陣列的長度是固定的,因此插入一個元素必定會導致陣列尾部元素“丟失”。我們將這個問題的解決方案留在“串列”章節中討論。 + +=== "Python" + + ```python title="array.py" + def insert(nums: list[int], num: int, index: int): + """在陣列的索引 index 處插入元素 num""" + # 把索引 index 以及之後的所有元素向後移動一位 + for i in range(len(nums) - 1, index, -1): + nums[i] = nums[i - 1] + # 將 num 賦給 index 處的元素 + nums[index] = num + ``` + +=== "C++" + + ```cpp title="array.cpp" + /* 在陣列的索引 index 處插入元素 num */ + void insert(int *nums, int size, int num, int index) { + // 把索引 index 以及之後的所有元素向後移動一位 + for (int i = size - 1; i > index; i--) { + nums[i] = nums[i - 1]; + } + // 將 num 賦給 index 處的元素 + nums[index] = num; + } + ``` + +=== "Java" + + ```java title="array.java" + /* 在陣列的索引 index 處插入元素 num */ + void insert(int[] nums, int num, int index) { + // 把索引 index 以及之後的所有元素向後移動一位 + for (int i = nums.length - 1; i > index; i--) { + nums[i] = nums[i - 1]; + } + // 將 num 賦給 index 處的元素 + nums[index] = num; + } + ``` + +=== "C#" + + ```csharp title="array.cs" + /* 在陣列的索引 index 處插入元素 num */ + void Insert(int[] nums, int num, int index) { + // 把索引 index 以及之後的所有元素向後移動一位 + for (int i = nums.Length - 1; i > index; i--) { + nums[i] = nums[i - 1]; + } + // 將 num 賦給 index 處的元素 + nums[index] = num; + } + ``` + +=== "Go" + + ```go title="array.go" + /* 在陣列的索引 index 處插入元素 num */ + func insert(nums []int, num int, index int) { + // 把索引 index 以及之後的所有元素向後移動一位 + for i := len(nums) - 1; i > index; i-- { + nums[i] = nums[i-1] + } + // 將 num 賦給 index 處的元素 + nums[index] = num + } + ``` + +=== "Swift" + + ```swift title="array.swift" + /* 在陣列的索引 index 處插入元素 num */ + func insert(nums: inout [Int], num: Int, index: Int) { + // 把索引 index 以及之後的所有元素向後移動一位 + for i in nums.indices.dropFirst(index).reversed() { + nums[i] = nums[i - 1] + } + // 將 num 賦給 index 處的元素 + nums[index] = num + } + ``` + +=== "JS" + + ```javascript title="array.js" + /* 在陣列的索引 index 處插入元素 num */ + function insert(nums, num, index) { + // 把索引 index 以及之後的所有元素向後移動一位 + for (let i = nums.length - 1; i > index; i--) { + nums[i] = nums[i - 1]; + } + // 將 num 賦給 index 處的元素 + nums[index] = num; + } + ``` + +=== "TS" + + ```typescript title="array.ts" + /* 在陣列的索引 index 處插入元素 num */ + function insert(nums: number[], num: number, index: number): void { + // 把索引 index 以及之後的所有元素向後移動一位 + for (let i = nums.length - 1; i > index; i--) { + nums[i] = nums[i - 1]; + } + // 將 num 賦給 index 處的元素 + nums[index] = num; + } + ``` + +=== "Dart" + + ```dart title="array.dart" + /* 在陣列的索引 index 處插入元素 _num */ + void insert(List nums, int _num, int index) { + // 把索引 index 以及之後的所有元素向後移動一位 + for (var i = nums.length - 1; i > index; i--) { + nums[i] = nums[i - 1]; + } + // 將 _num 賦給 index 處元素 + nums[index] = _num; + } + ``` + +=== "Rust" + + ```rust title="array.rs" + /* 在陣列的索引 index 處插入元素 num */ + fn insert(nums: &mut Vec, num: i32, index: usize) { + // 把索引 index 以及之後的所有元素向後移動一位 + for i in (index + 1..nums.len()).rev() { + nums[i] = nums[i - 1]; + } + // 將 num 賦給 index 處的元素 + nums[index] = num; + } + ``` + +=== "C" + + ```c title="array.c" + /* 在陣列的索引 index 處插入元素 num */ + void insert(int *nums, int size, int num, int index) { + // 把索引 index 以及之後的所有元素向後移動一位 + for (int i = size - 1; i > index; i--) { + nums[i] = nums[i - 1]; + } + // 將 num 賦給 index 處的元素 + nums[index] = num; + } + ``` + +=== "Kotlin" + + ```kotlin title="array.kt" + /* 在陣列的索引 index 處插入元素 num */ + fun insert(nums: IntArray, num: Int, index: Int) { + // 把索引 index 以及之後的所有元素向後移動一位 + for (i in nums.size - 1 downTo index + 1) { + nums[i] = nums[i - 1] + } + // 將 num 賦給 index 處的元素 + nums[index] = num + } + ``` + +=== "Ruby" + + ```ruby title="array.rb" + ### 在陣列的索引 index 處插入元素 num ### + def insert(nums, num, index) + # 把索引 index 以及之後的所有元素向後移動一位 + for i in (nums.length - 1).downto(index + 1) + nums[i] = nums[i - 1] + end + + # 將 num 賦給 index 處的元素 + nums[index] = num + end + ``` + +=== "Zig" + + ```zig title="array.zig" + // 在陣列的索引 index 處插入元素 num + fn insert(nums: []i32, num: i32, index: usize) void { + // 把索引 index 以及之後的所有元素向後移動一位 + var i = nums.len - 1; + while (i > index) : (i -= 1) { + nums[i] = nums[i - 1]; + } + // 將 num 賦給 index 處的元素 + nums[index] = num; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +### 4.   刪除元素 + +同理,如圖 4-4 所示,若想刪除索引 $i$ 處的元素,則需要把索引 $i$ 之後的元素都向前移動一位。 + +![陣列刪除元素示例](array.assets/array_remove_element.png){ class="animation-figure" } + +

圖 4-4   陣列刪除元素示例

+ +請注意,刪除元素完成後,原先末尾的元素變得“無意義”了,所以我們無須特意去修改它。 + +=== "Python" + + ```python title="array.py" + def remove(nums: list[int], index: int): + """刪除索引 index 處的元素""" + # 把索引 index 之後的所有元素向前移動一位 + for i in range(index, len(nums) - 1): + nums[i] = nums[i + 1] + ``` + +=== "C++" + + ```cpp title="array.cpp" + /* 刪除索引 index 處的元素 */ + void remove(int *nums, int size, int index) { + // 把索引 index 之後的所有元素向前移動一位 + for (int i = index; i < size - 1; i++) { + nums[i] = nums[i + 1]; + } + } + ``` + +=== "Java" + + ```java title="array.java" + /* 刪除索引 index 處的元素 */ + void remove(int[] nums, int index) { + // 把索引 index 之後的所有元素向前移動一位 + for (int i = index; i < nums.length - 1; i++) { + nums[i] = nums[i + 1]; + } + } + ``` + +=== "C#" + + ```csharp title="array.cs" + /* 刪除索引 index 處的元素 */ + void Remove(int[] nums, int index) { + // 把索引 index 之後的所有元素向前移動一位 + for (int i = index; i < nums.Length - 1; i++) { + nums[i] = nums[i + 1]; + } + } + ``` + +=== "Go" + + ```go title="array.go" + /* 刪除索引 index 處的元素 */ + func remove(nums []int, index int) { + // 把索引 index 之後的所有元素向前移動一位 + for i := index; i < len(nums)-1; i++ { + nums[i] = nums[i+1] + } + } + ``` + +=== "Swift" + + ```swift title="array.swift" + /* 刪除索引 index 處的元素 */ + func remove(nums: inout [Int], index: Int) { + // 把索引 index 之後的所有元素向前移動一位 + for i in nums.indices.dropFirst(index).dropLast() { + nums[i] = nums[i + 1] + } + } + ``` + +=== "JS" + + ```javascript title="array.js" + /* 刪除索引 index 處的元素 */ + function remove(nums, index) { + // 把索引 index 之後的所有元素向前移動一位 + for (let i = index; i < nums.length - 1; i++) { + nums[i] = nums[i + 1]; + } + } + ``` + +=== "TS" + + ```typescript title="array.ts" + /* 刪除索引 index 處的元素 */ + function remove(nums: number[], index: number): void { + // 把索引 index 之後的所有元素向前移動一位 + for (let i = index; i < nums.length - 1; i++) { + nums[i] = nums[i + 1]; + } + } + ``` + +=== "Dart" + + ```dart title="array.dart" + /* 刪除索引 index 處的元素 */ + void remove(List nums, int index) { + // 把索引 index 之後的所有元素向前移動一位 + for (var i = index; i < nums.length - 1; i++) { + nums[i] = nums[i + 1]; + } + } + ``` + +=== "Rust" + + ```rust title="array.rs" + /* 刪除索引 index 處的元素 */ + fn remove(nums: &mut Vec, index: usize) { + // 把索引 index 之後的所有元素向前移動一位 + for i in index..nums.len() - 1 { + nums[i] = nums[i + 1]; + } + } + ``` + +=== "C" + + ```c title="array.c" + /* 刪除索引 index 處的元素 */ + // 注意:stdio.h 佔用了 remove 關鍵詞 + void removeItem(int *nums, int size, int index) { + // 把索引 index 之後的所有元素向前移動一位 + for (int i = index; i < size - 1; i++) { + nums[i] = nums[i + 1]; + } + } + ``` + +=== "Kotlin" + + ```kotlin title="array.kt" + /* 刪除索引 index 處的元素 */ + fun remove(nums: IntArray, index: Int) { + // 把索引 index 之後的所有元素向前移動一位 + for (i in index..
+ + +總的來看,陣列的插入與刪除操作有以下缺點。 + +- **時間複雜度高**:陣列的插入和刪除的平均時間複雜度均為 $O(n)$ ,其中 $n$ 為陣列長度。 +- **丟失元素**:由於陣列的長度不可變,因此在插入元素後,超出陣列長度範圍的元素會丟失。 +- **記憶體浪費**:我們可以初始化一個比較長的陣列,只用前面一部分,這樣在插入資料時,丟失的末尾元素都是“無意義”的,但這樣做會造成部分記憶體空間浪費。 + +### 5.   走訪陣列 + +在大多數程式語言中,我們既可以透過索引走訪陣列,也可以直接走訪獲取陣列中的每個元素: + +=== "Python" + + ```python title="array.py" + def traverse(nums: list[int]): + """走訪陣列""" + count = 0 + # 透過索引走訪陣列 + for i in range(len(nums)): + count += nums[i] + # 直接走訪陣列元素 + for num in nums: + count += num + # 同時走訪資料索引和元素 + for i, num in enumerate(nums): + count += nums[i] + count += num + ``` + +=== "C++" + + ```cpp title="array.cpp" + /* 走訪陣列 */ + void traverse(int *nums, int size) { + int count = 0; + // 透過索引走訪陣列 + for (int i = 0; i < size; i++) { + count += nums[i]; + } + } + ``` + +=== "Java" + + ```java title="array.java" + /* 走訪陣列 */ + void traverse(int[] nums) { + int count = 0; + // 透過索引走訪陣列 + for (int i = 0; i < nums.length; i++) { + count += nums[i]; + } + // 直接走訪陣列元素 + for (int num : nums) { + count += num; + } + } + ``` + +=== "C#" + + ```csharp title="array.cs" + /* 走訪陣列 */ + void Traverse(int[] nums) { + int count = 0; + // 透過索引走訪陣列 + for (int i = 0; i < nums.Length; i++) { + count += nums[i]; + } + // 直接走訪陣列元素 + foreach (int num in nums) { + count += num; + } + } + ``` + +=== "Go" + + ```go title="array.go" + /* 走訪陣列 */ + func traverse(nums []int) { + count := 0 + // 透過索引走訪陣列 + for i := 0; i < len(nums); i++ { + count += nums[i] + } + count = 0 + // 直接走訪陣列元素 + for _, num := range nums { + count += num + } + // 同時走訪資料索引和元素 + for i, num := range nums { + count += nums[i] + count += num + } + } + ``` + +=== "Swift" + + ```swift title="array.swift" + /* 走訪陣列 */ + func traverse(nums: [Int]) { + var count = 0 + // 透過索引走訪陣列 + for i in nums.indices { + count += nums[i] + } + // 直接走訪陣列元素 + for num in nums { + count += num + } + // 同時走訪資料索引和元素 + for (i, num) in nums.enumerated() { + count += nums[i] + count += num + } + } + ``` + +=== "JS" + + ```javascript title="array.js" + /* 走訪陣列 */ + function traverse(nums) { + let count = 0; + // 透過索引走訪陣列 + for (let i = 0; i < nums.length; i++) { + count += nums[i]; + } + // 直接走訪陣列元素 + for (const num of nums) { + count += num; + } + } + ``` + +=== "TS" + + ```typescript title="array.ts" + /* 走訪陣列 */ + function traverse(nums: number[]): void { + let count = 0; + // 透過索引走訪陣列 + for (let i = 0; i < nums.length; i++) { + count += nums[i]; + } + // 直接走訪陣列元素 + for (const num of nums) { + count += num; + } + } + ``` + +=== "Dart" + + ```dart title="array.dart" + /* 走訪陣列元素 */ + void traverse(List nums) { + int count = 0; + // 透過索引走訪陣列 + for (var i = 0; i < nums.length; i++) { + count += nums[i]; + } + // 直接走訪陣列元素 + for (int _num in nums) { + count += _num; + } + // 透過 forEach 方法走訪陣列 + nums.forEach((_num) { + count += _num; + }); + } + ``` + +=== "Rust" + + ```rust title="array.rs" + /* 走訪陣列 */ + fn traverse(nums: &[i32]) { + let mut _count = 0; + // 透過索引走訪陣列 + for i in 0..nums.len() { + _count += nums[i]; + } + // 直接走訪陣列元素 + for num in nums { + _count += num; + } + } + ``` + +=== "C" + + ```c title="array.c" + /* 走訪陣列 */ + void traverse(int *nums, int size) { + int count = 0; + // 透過索引走訪陣列 + for (int i = 0; i < size; i++) { + count += nums[i]; + } + } + ``` + +=== "Kotlin" + + ```kotlin title="array.kt" + /* 走訪陣列 */ + fun traverse(nums: IntArray) { + var count = 0 + // 透過索引走訪陣列 + for (i in nums.indices) { + count += nums[i] + } + // 直接走訪陣列元素 + for (j: Int in nums) { + count += j + } + } + ``` + +=== "Ruby" + + ```ruby title="array.rb" + ### 走訪陣列 ### + def traverse(nums) + count = 0 + + # 透過索引走訪陣列 + for i in 0...nums.length + count += nums[i] + end + + # 直接走訪陣列元素 + for num in nums + count += num + end + end + ``` + +=== "Zig" + + ```zig title="array.zig" + // 走訪陣列 + fn traverse(nums: []i32) void { + var count: i32 = 0; + // 透過索引走訪陣列 + var i: i32 = 0; + while (i < nums.len) : (i += 1) { + count += nums[i]; + } + count = 0; + // 直接走訪陣列元素 + for (nums) |num| { + count += num; + } + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +### 6.   查詢元素 + +在陣列中查詢指定元素需要走訪陣列,每輪判斷元素值是否匹配,若匹配則輸出對應索引。 + +因為陣列是線性資料結構,所以上述查詢操作被稱為“線性查詢”。 + +=== "Python" + + ```python title="array.py" + def find(nums: list[int], target: int) -> int: + """在陣列中查詢指定元素""" + for i in range(len(nums)): + if nums[i] == target: + return i + return -1 + ``` + +=== "C++" + + ```cpp title="array.cpp" + /* 在陣列中查詢指定元素 */ + int find(int *nums, int size, int target) { + for (int i = 0; i < size; i++) { + if (nums[i] == target) + return i; + } + return -1; + } + ``` + +=== "Java" + + ```java title="array.java" + /* 在陣列中查詢指定元素 */ + int find(int[] nums, int target) { + for (int i = 0; i < nums.length; i++) { + if (nums[i] == target) + return i; + } + return -1; + } + ``` + +=== "C#" + + ```csharp title="array.cs" + /* 在陣列中查詢指定元素 */ + int Find(int[] nums, int target) { + for (int i = 0; i < nums.Length; i++) { + if (nums[i] == target) + return i; + } + return -1; + } + ``` + +=== "Go" + + ```go title="array.go" + /* 在陣列中查詢指定元素 */ + func find(nums []int, target int) (index int) { + index = -1 + for i := 0; i < len(nums); i++ { + if nums[i] == target { + index = i + break + } + } + return + } + ``` + +=== "Swift" + + ```swift title="array.swift" + /* 在陣列中查詢指定元素 */ + func find(nums: [Int], target: Int) -> Int { + for i in nums.indices { + if nums[i] == target { + return i + } + } + return -1 + } + ``` + +=== "JS" + + ```javascript title="array.js" + /* 在陣列中查詢指定元素 */ + function find(nums, target) { + for (let i = 0; i < nums.length; i++) { + if (nums[i] === target) return i; + } + return -1; + } + ``` + +=== "TS" + + ```typescript title="array.ts" + /* 在陣列中查詢指定元素 */ + function find(nums: number[], target: number): number { + for (let i = 0; i < nums.length; i++) { + if (nums[i] === target) { + return i; + } + } + return -1; + } + ``` + +=== "Dart" + + ```dart title="array.dart" + /* 在陣列中查詢指定元素 */ + int find(List nums, int target) { + for (var i = 0; i < nums.length; i++) { + if (nums[i] == target) return i; + } + return -1; + } + ``` + +=== "Rust" + + ```rust title="array.rs" + /* 在陣列中查詢指定元素 */ + fn find(nums: &[i32], target: i32) -> Option { + for i in 0..nums.len() { + if nums[i] == target { + return Some(i); + } + } + None + } + ``` + +=== "C" + + ```c title="array.c" + /* 在陣列中查詢指定元素 */ + int find(int *nums, int size, int target) { + for (int i = 0; i < size; i++) { + if (nums[i] == target) + return i; + } + return -1; + } + ``` + +=== "Kotlin" + + ```kotlin title="array.kt" + /* 在陣列中查詢指定元素 */ + fun find(nums: IntArray, target: Int): Int { + for (i in nums.indices) { + if (nums[i] == target) return i + } + return -1 + } + ``` + +=== "Ruby" + + ```ruby title="array.rb" + ### 在陣列中查詢指定元素 ### + def find(nums, target) + for i in 0...nums.length + return i if nums[i] == target + end + + -1 + end + ``` + +=== "Zig" + + ```zig title="array.zig" + // 在陣列中查詢指定元素 + fn find(nums: []i32, target: i32) i32 { + for (nums, 0..) |num, i| { + if (num == target) return @intCast(i); + } + return -1; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +### 7.   擴容陣列 + +在複雜的系統環境中,程式難以保證陣列之後的記憶體空間是可用的,從而無法安全地擴展陣列容量。因此在大多數程式語言中,**陣列的長度是不可變的**。 + +如果我們希望擴容陣列,則需重新建立一個更大的陣列,然後把原陣列元素依次複製到新陣列。這是一個 $O(n)$ 的操作,在陣列很大的情況下非常耗時。程式碼如下所示: + +=== "Python" + + ```python title="array.py" + def extend(nums: list[int], enlarge: int) -> list[int]: + """擴展陣列長度""" + # 初始化一個擴展長度後的陣列 + res = [0] * (len(nums) + enlarge) + # 將原陣列中的所有元素複製到新陣列 + for i in range(len(nums)): + res[i] = nums[i] + # 返回擴展後的新陣列 + return res + ``` + +=== "C++" + + ```cpp title="array.cpp" + /* 擴展陣列長度 */ + int *extend(int *nums, int size, int enlarge) { + // 初始化一個擴展長度後的陣列 + int *res = new int[size + enlarge]; + // 將原陣列中的所有元素複製到新陣列 + for (int i = 0; i < size; i++) { + res[i] = nums[i]; + } + // 釋放記憶體 + delete[] nums; + // 返回擴展後的新陣列 + return res; + } + ``` + +=== "Java" + + ```java title="array.java" + /* 擴展陣列長度 */ + int[] extend(int[] nums, int enlarge) { + // 初始化一個擴展長度後的陣列 + int[] res = new int[nums.length + enlarge]; + // 將原陣列中的所有元素複製到新陣列 + for (int i = 0; i < nums.length; i++) { + res[i] = nums[i]; + } + // 返回擴展後的新陣列 + return res; + } + ``` + +=== "C#" + + ```csharp title="array.cs" + /* 擴展陣列長度 */ + int[] Extend(int[] nums, int enlarge) { + // 初始化一個擴展長度後的陣列 + int[] res = new int[nums.Length + enlarge]; + // 將原陣列中的所有元素複製到新陣列 + for (int i = 0; i < nums.Length; i++) { + res[i] = nums[i]; + } + // 返回擴展後的新陣列 + return res; + } + ``` + +=== "Go" + + ```go title="array.go" + /* 擴展陣列長度 */ + func extend(nums []int, enlarge int) []int { + // 初始化一個擴展長度後的陣列 + res := make([]int, len(nums)+enlarge) + // 將原陣列中的所有元素複製到新陣列 + for i, num := range nums { + res[i] = num + } + // 返回擴展後的新陣列 + return res + } + ``` + +=== "Swift" + + ```swift title="array.swift" + /* 擴展陣列長度 */ + func extend(nums: [Int], enlarge: Int) -> [Int] { + // 初始化一個擴展長度後的陣列 + var res = Array(repeating: 0, count: nums.count + enlarge) + // 將原陣列中的所有元素複製到新陣列 + for i in nums.indices { + res[i] = nums[i] + } + // 返回擴展後的新陣列 + return res + } + ``` + +=== "JS" + + ```javascript title="array.js" + /* 擴展陣列長度 */ + // 請注意,JavaScript 的 Array 是動態陣列,可以直接擴展 + // 為了方便學習,本函式將 Array 看作長度不可變的陣列 + function extend(nums, enlarge) { + // 初始化一個擴展長度後的陣列 + const res = new Array(nums.length + enlarge).fill(0); + // 將原陣列中的所有元素複製到新陣列 + for (let i = 0; i < nums.length; i++) { + res[i] = nums[i]; + } + // 返回擴展後的新陣列 + return res; + } + ``` + +=== "TS" + + ```typescript title="array.ts" + /* 擴展陣列長度 */ + // 請注意,TypeScript 的 Array 是動態陣列,可以直接擴展 + // 為了方便學習,本函式將 Array 看作長度不可變的陣列 + function extend(nums: number[], enlarge: number): number[] { + // 初始化一個擴展長度後的陣列 + const res = new Array(nums.length + enlarge).fill(0); + // 將原陣列中的所有元素複製到新陣列 + for (let i = 0; i < nums.length; i++) { + res[i] = nums[i]; + } + // 返回擴展後的新陣列 + return res; + } + ``` + +=== "Dart" + + ```dart title="array.dart" + /* 擴展陣列長度 */ + List extend(List nums, int enlarge) { + // 初始化一個擴展長度後的陣列 + List res = List.filled(nums.length + enlarge, 0); + // 將原陣列中的所有元素複製到新陣列 + for (var i = 0; i < nums.length; i++) { + res[i] = nums[i]; + } + // 返回擴展後的新陣列 + return res; + } + ``` + +=== "Rust" + + ```rust title="array.rs" + /* 擴展陣列長度 */ + fn extend(nums: Vec, enlarge: usize) -> Vec { + // 初始化一個擴展長度後的陣列 + let mut res: Vec = vec![0; nums.len() + enlarge]; + // 將原陣列中的所有元素複製到新 + for i in 0..nums.len() { + res[i] = nums[i]; + } + // 返回擴展後的新陣列 + res + } + ``` + +=== "C" + + ```c title="array.c" + /* 擴展陣列長度 */ + int *extend(int *nums, int size, int enlarge) { + // 初始化一個擴展長度後的陣列 + int *res = (int *)malloc(sizeof(int) * (size + enlarge)); + // 將原陣列中的所有元素複製到新陣列 + for (int i = 0; i < size; i++) { + res[i] = nums[i]; + } + // 初始化擴展後的空間 + for (int i = size; i < size + enlarge; i++) { + res[i] = 0; + } + // 返回擴展後的新陣列 + return res; + } + ``` + +=== "Kotlin" + + ```kotlin title="array.kt" + /* 擴展陣列長度 */ + fun extend(nums: IntArray, enlarge: Int): IntArray { + // 初始化一個擴展長度後的陣列 + val res = IntArray(nums.size + enlarge) + // 將原陣列中的所有元素複製到新陣列 + for (i in nums.indices) { + res[i] = nums[i] + } + // 返回擴展後的新陣列 + return res + } + ``` + +=== "Ruby" + + ```ruby title="array.rb" + ### 擴展陣列長度 ### + # 請注意,Ruby 的 Array 是動態陣列,可以直接擴展 + # 為了方便學習,本函式將 Array 看作長度不可變的陣列 + def extend(nums, enlarge) + # 初始化一個擴展長度後的陣列 + res = Array.new(nums.length + enlarge, 0) + + # 將原陣列中的所有元素複製到新陣列 + for i in 0...nums.length + res[i] = nums[i] + end + + # 返回擴展後的新陣列 + res + end + ``` + +=== "Zig" + + ```zig title="array.zig" + // 擴展陣列長度 + fn extend(mem_allocator: std.mem.Allocator, nums: []i32, enlarge: usize) ![]i32 { + // 初始化一個擴展長度後的陣列 + var res = try mem_allocator.alloc(i32, nums.len + enlarge); + @memset(res, 0); + // 將原陣列中的所有元素複製到新陣列 + std.mem.copy(i32, res, nums); + // 返回擴展後的新陣列 + return res; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 4.1.2   陣列的優點與侷限性 + +陣列儲存在連續的記憶體空間內,且元素型別相同。這種做法包含豐富的先驗資訊,系統可以利用這些資訊來最佳化資料結構的操作效率。 + +- **空間效率高**:陣列為資料分配了連續的記憶體塊,無須額外的結構開銷。 +- **支持隨機訪問**:陣列允許在 $O(1)$ 時間內訪問任何元素。 +- **快取區域性**:當訪問陣列元素時,計算機不僅會載入它,還會快取其周圍的其他資料,從而藉助高速快取來提升後續操作的執行速度。 + +連續空間儲存是一把雙刃劍,其存在以下侷限性。 + +- **插入與刪除效率低**:當陣列中元素較多時,插入與刪除操作需要移動大量的元素。 +- **長度不可變**:陣列在初始化後長度就固定了,擴容陣列需要將所有資料複製到新陣列,開銷很大。 +- **空間浪費**:如果陣列分配的大小超過實際所需,那麼多餘的空間就被浪費了。 + +## 4.1.3   陣列典型應用 + +陣列是一種基礎且常見的資料結構,既頻繁應用在各類演算法之中,也可用於實現各種複雜資料結構。 + +- **隨機訪問**:如果我們想隨機抽取一些樣本,那麼可以用陣列儲存,並生成一個隨機序列,根據索引實現隨機抽樣。 +- **排序和搜尋**:陣列是排序和搜尋演算法最常用的資料結構。快速排序、合併排序、二分搜尋等都主要在陣列上進行。 +- **查詢表**:當需要快速查詢一個元素或其對應關係時,可以使用陣列作為查詢表。假如我們想實現字元到 ASCII 碼的對映,則可以將字元的 ASCII 碼值作為索引,對應的元素存放在陣列中的對應位置。 +- **機器學習**:神經網路中大量使用了向量、矩陣、張量之間的線性代數運算,這些資料都是以陣列的形式構建的。陣列是神經網路程式設計中最常使用的資料結構。 +- **資料結構實現**:陣列可以用於實現堆疊、佇列、雜湊表、堆積、圖等資料結構。例如,圖的鄰接矩陣表示實際上是一個二維陣列。 diff --git a/zh-Hant/docs/chapter_array_and_linkedlist/index.md b/zh-Hant/docs/chapter_array_and_linkedlist/index.md new file mode 100644 index 000000000..1b2e6c964 --- /dev/null +++ b/zh-Hant/docs/chapter_array_and_linkedlist/index.md @@ -0,0 +1,22 @@ +--- +comments: true +icon: material/view-list-outline +--- + +# 第 4 章   陣列與鏈結串列 + +![陣列與鏈結串列](../assets/covers/chapter_array_and_linkedlist.jpg){ 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/) diff --git a/zh-Hant/docs/chapter_array_and_linkedlist/linked_list.md b/zh-Hant/docs/chapter_array_and_linkedlist/linked_list.md new file mode 100755 index 000000000..aae4bb749 --- /dev/null +++ b/zh-Hant/docs/chapter_array_and_linkedlist/linked_list.md @@ -0,0 +1,1565 @@ +--- +comments: true +--- + +# 4.2   鏈結串列 + +記憶體空間是所有程式的公共資源,在一個複雜的系統執行環境下,空閒的記憶體空間可能散落在記憶體各處。我們知道,儲存陣列的記憶體空間必須是連續的,而當陣列非常大時,記憶體可能無法提供如此大的連續空間。此時鏈結串列的靈活性優勢就體現出來了。 + +鏈結串列(linked list)是一種線性資料結構,其中的每個元素都是一個節點物件,各個節點透過“引用”相連線。引用記錄了下一個節點的記憶體位址,透過它可以從當前節點訪問到下一個節點。 + +鏈結串列的設計使得各個節點可以分散儲存在記憶體各處,它們的記憶體位址無須連續。 + +![鏈結串列定義與儲存方式](linked_list.assets/linkedlist_definition.png){ class="animation-figure" } + +

圖 4-5   鏈結串列定義與儲存方式

+ +觀察圖 4-5 ,鏈結串列的組成單位是節點(node)物件。每個節點都包含兩項資料:節點的“值”和指向下一節點的“引用”。 + +- 鏈結串列的首個節點被稱為“頭節點”,最後一個節點被稱為“尾節點”。 +- 尾節點指向的是“空”,它在 Java、C++ 和 Python 中分別被記為 `null`、`nullptr` 和 `None` 。 +- 在 C、C++、Go 和 Rust 等支持指標的語言中,上述“引用”應被替換為“指標”。 + +如以下程式碼所示,鏈結串列節點 `ListNode` 除了包含值,還需額外儲存一個引用(指標)。因此在相同資料量下,**鏈結串列比陣列佔用更多的記憶體空間**。 + +=== "Python" + + ```python title="" + class ListNode: + """鏈結串列節點類別""" + def __init__(self, val: int): + self.val: int = val # 節點值 + self.next: ListNode | None = None # 指向下一節點的引用 + ``` + +=== "C++" + + ```cpp title="" + /* 鏈結串列節點結構體 */ + struct ListNode { + int val; // 節點值 + ListNode *next; // 指向下一節點的指標 + ListNode(int x) : val(x), next(nullptr) {} // 建構子 + }; + ``` + +=== "Java" + + ```java title="" + /* 鏈結串列節點類別 */ + class ListNode { + int val; // 節點值 + ListNode next; // 指向下一節點的引用 + ListNode(int x) { val = x; } // 建構子 + } + ``` + +=== "C#" + + ```csharp title="" + /* 鏈結串列節點類別 */ + class ListNode(int x) { //建構子 + int val = x; // 節點值 + ListNode? next; // 指向下一節點的引用 + } + ``` + +=== "Go" + + ```go title="" + /* 鏈結串列節點結構體 */ + type ListNode struct { + Val int // 節點值 + Next *ListNode // 指向下一節點的指標 + } + + // NewListNode 建構子,建立一個新的鏈結串列 + func NewListNode(val int) *ListNode { + return &ListNode{ + Val: val, + Next: nil, + } + } + ``` + +=== "Swift" + + ```swift title="" + /* 鏈結串列節點類別 */ + class ListNode { + var val: Int // 節點值 + var next: ListNode? // 指向下一節點的引用 + + init(x: Int) { // 建構子 + val = x + } + } + ``` + +=== "JS" + + ```javascript title="" + /* 鏈結串列節點類別 */ + class ListNode { + constructor(val, next) { + this.val = (val === undefined ? 0 : val); // 節點值 + this.next = (next === undefined ? null : next); // 指向下一節點的引用 + } + } + ``` + +=== "TS" + + ```typescript title="" + /* 鏈結串列節點類別 */ + class ListNode { + val: number; + next: ListNode | null; + constructor(val?: number, next?: ListNode | null) { + this.val = val === undefined ? 0 : val; // 節點值 + this.next = next === undefined ? null : next; // 指向下一節點的引用 + } + } + ``` + +=== "Dart" + + ```dart title="" + /* 鏈結串列節點類別 */ + class ListNode { + int val; // 節點值 + ListNode? next; // 指向下一節點的引用 + ListNode(this.val, [this.next]); // 建構子 + } + ``` + +=== "Rust" + + ```rust title="" + use std::rc::Rc; + use std::cell::RefCell; + /* 鏈結串列節點類別 */ + #[derive(Debug)] + struct ListNode { + val: i32, // 節點值 + next: Option>>, // 指向下一節點的指標 + } + ``` + +=== "C" + + ```c title="" + /* 鏈結串列節點結構體 */ + typedef struct ListNode { + int val; // 節點值 + struct ListNode *next; // 指向下一節點的指標 + } ListNode; + + /* 建構子 */ + ListNode *newListNode(int val) { + ListNode *node; + node = (ListNode *) malloc(sizeof(ListNode)); + node->val = val; + node->next = NULL; + return node; + } + ``` + +=== "Kotlin" + + ```kotlin title="" + /* 鏈結串列節點類別 */ + // 建構子 + class ListNode(x: Int) { + val _val: Int = x // 節點值 + val next: ListNode? = null // 指向下一個節點的引用 + } + ``` + +=== "Ruby" + + ```ruby title="" + # 鏈結串列節點類別 + class ListNode + attr_accessor :val # 節點值 + attr_accessor :next # 指向下一節點的引用 + + def initialize(val=0, next_node=nil) + @val = val + @next = next_node + end + end + ``` + +=== "Zig" + + ```zig title="" + // 鏈結串列節點類別 + pub fn ListNode(comptime T: type) type { + return struct { + const Self = @This(); + + val: T = 0, // 節點值 + next: ?*Self = null, // 指向下一節點的指標 + + // 建構子 + pub fn init(self: *Self, x: i32) void { + self.val = x; + self.next = null; + } + }; + } + ``` + +## 4.2.1   鏈結串列常用操作 + +### 1.   初始化鏈結串列 + +建立鏈結串列分為兩步,第一步是初始化各個節點物件,第二步是構建節點之間的引用關係。初始化完成後,我們就可以從鏈結串列的頭節點出發,透過引用指向 `next` 依次訪問所有節點。 + +=== "Python" + + ```python title="linked_list.py" + # 初始化鏈結串列 1 -> 3 -> 2 -> 5 -> 4 + # 初始化各個節點 + n0 = ListNode(1) + n1 = ListNode(3) + n2 = ListNode(2) + n3 = ListNode(5) + n4 = ListNode(4) + # 構建節點之間的引用 + n0.next = n1 + n1.next = n2 + n2.next = n3 + n3.next = n4 + ``` + +=== "C++" + + ```cpp title="linked_list.cpp" + /* 初始化鏈結串列 1 -> 3 -> 2 -> 5 -> 4 */ + // 初始化各個節點 + ListNode* n0 = new ListNode(1); + ListNode* n1 = new ListNode(3); + ListNode* n2 = new ListNode(2); + ListNode* n3 = new ListNode(5); + ListNode* n4 = new ListNode(4); + // 構建節點之間的引用 + n0->next = n1; + n1->next = n2; + n2->next = n3; + n3->next = n4; + ``` + +=== "Java" + + ```java title="linked_list.java" + /* 初始化鏈結串列 1 -> 3 -> 2 -> 5 -> 4 */ + // 初始化各個節點 + ListNode n0 = new ListNode(1); + ListNode n1 = new ListNode(3); + ListNode n2 = new ListNode(2); + ListNode n3 = new ListNode(5); + ListNode n4 = new ListNode(4); + // 構建節點之間的引用 + n0.next = n1; + n1.next = n2; + n2.next = n3; + n3.next = n4; + ``` + +=== "C#" + + ```csharp title="linked_list.cs" + /* 初始化鏈結串列 1 -> 3 -> 2 -> 5 -> 4 */ + // 初始化各個節點 + ListNode n0 = new(1); + ListNode n1 = new(3); + ListNode n2 = new(2); + ListNode n3 = new(5); + ListNode n4 = new(4); + // 構建節點之間的引用 + n0.next = n1; + n1.next = n2; + n2.next = n3; + n3.next = n4; + ``` + +=== "Go" + + ```go title="linked_list.go" + /* 初始化鏈結串列 1 -> 3 -> 2 -> 5 -> 4 */ + // 初始化各個節點 + n0 := NewListNode(1) + n1 := NewListNode(3) + n2 := NewListNode(2) + n3 := NewListNode(5) + n4 := NewListNode(4) + // 構建節點之間的引用 + n0.Next = n1 + n1.Next = n2 + n2.Next = n3 + n3.Next = n4 + ``` + +=== "Swift" + + ```swift title="linked_list.swift" + /* 初始化鏈結串列 1 -> 3 -> 2 -> 5 -> 4 */ + // 初始化各個節點 + let n0 = ListNode(x: 1) + let n1 = ListNode(x: 3) + let n2 = ListNode(x: 2) + let n3 = ListNode(x: 5) + let n4 = ListNode(x: 4) + // 構建節點之間的引用 + n0.next = n1 + n1.next = n2 + n2.next = n3 + n3.next = n4 + ``` + +=== "JS" + + ```javascript title="linked_list.js" + /* 初始化鏈結串列 1 -> 3 -> 2 -> 5 -> 4 */ + // 初始化各個節點 + const n0 = new ListNode(1); + const n1 = new ListNode(3); + const n2 = new ListNode(2); + const n3 = new ListNode(5); + const n4 = new ListNode(4); + // 構建節點之間的引用 + n0.next = n1; + n1.next = n2; + n2.next = n3; + n3.next = n4; + ``` + +=== "TS" + + ```typescript title="linked_list.ts" + /* 初始化鏈結串列 1 -> 3 -> 2 -> 5 -> 4 */ + // 初始化各個節點 + const n0 = new ListNode(1); + const n1 = new ListNode(3); + const n2 = new ListNode(2); + const n3 = new ListNode(5); + const n4 = new ListNode(4); + // 構建節點之間的引用 + n0.next = n1; + n1.next = n2; + n2.next = n3; + n3.next = n4; + ``` + +=== "Dart" + + ```dart title="linked_list.dart" + /* 初始化鏈結串列 1 -> 3 -> 2 -> 5 -> 4 */\ + // 初始化各個節點 + ListNode n0 = ListNode(1); + ListNode n1 = ListNode(3); + ListNode n2 = ListNode(2); + ListNode n3 = ListNode(5); + ListNode n4 = ListNode(4); + // 構建節點之間的引用 + n0.next = n1; + n1.next = n2; + n2.next = n3; + n3.next = n4; + ``` + +=== "Rust" + + ```rust title="linked_list.rs" + /* 初始化鏈結串列 1 -> 3 -> 2 -> 5 -> 4 */ + // 初始化各個節點 + let n0 = Rc::new(RefCell::new(ListNode { val: 1, next: None })); + let n1 = Rc::new(RefCell::new(ListNode { val: 3, next: None })); + let n2 = Rc::new(RefCell::new(ListNode { val: 2, next: None })); + let n3 = Rc::new(RefCell::new(ListNode { val: 5, next: None })); + let n4 = Rc::new(RefCell::new(ListNode { val: 4, next: None })); + + // 構建節點之間的引用 + n0.borrow_mut().next = Some(n1.clone()); + n1.borrow_mut().next = Some(n2.clone()); + n2.borrow_mut().next = Some(n3.clone()); + n3.borrow_mut().next = Some(n4.clone()); + ``` + +=== "C" + + ```c title="linked_list.c" + /* 初始化鏈結串列 1 -> 3 -> 2 -> 5 -> 4 */ + // 初始化各個節點 + ListNode* n0 = newListNode(1); + ListNode* n1 = newListNode(3); + ListNode* n2 = newListNode(2); + ListNode* n3 = newListNode(5); + ListNode* n4 = newListNode(4); + // 構建節點之間的引用 + n0->next = n1; + n1->next = n2; + n2->next = n3; + n3->next = n4; + ``` + +=== "Kotlin" + + ```kotlin title="linked_list.kt" + /* 初始化鏈結串列 1 -> 3 -> 2 -> 5 -> 4 */ + // 初始化各個節點 + val n0 = ListNode(1) + val n1 = ListNode(3) + val n2 = ListNode(2) + val n3 = ListNode(5) + val n4 = ListNode(4) + // 構建節點之間的引用 + n0.next = n1; + n1.next = n2; + n2.next = n3; + n3.next = n4; + ``` + +=== "Ruby" + + ```ruby title="linked_list.rb" + # 初始化鏈結串列 1 -> 3 -> 2 -> 5 -> 4 + # 初始化各個節點 + n0 = ListNode.new(1) + n1 = ListNode.new(3) + n2 = ListNode.new(2) + n3 = ListNode.new(5) + n4 = ListNode.new(4) + # 構建節點之間的引用 + n0.next = n1 + n1.next = n2 + n2.next = n3 + n3.next = n4 + ``` + +=== "Zig" + + ```zig title="linked_list.zig" + // 初始化鏈結串列 + // 初始化各個節點 + var n0 = inc.ListNode(i32){.val = 1}; + var n1 = inc.ListNode(i32){.val = 3}; + var n2 = inc.ListNode(i32){.val = 2}; + var n3 = inc.ListNode(i32){.val = 5}; + var n4 = inc.ListNode(i32){.val = 4}; + // 構建節點之間的引用 + n0.next = &n1; + n1.next = &n2; + n2.next = &n3; + n3.next = &n4; + ``` + +??? pythontutor "視覺化執行" + +
+ + +陣列整體是一個變數,比如陣列 `nums` 包含元素 `nums[0]` 和 `nums[1]` 等,而鏈結串列是由多個獨立的節點物件組成的。**我們通常將頭節點當作鏈結串列的代稱**,比如以上程式碼中的鏈結串列可記作鏈結串列 `n0` 。 + +### 2.   插入節點 + +在鏈結串列中插入節點非常容易。如圖 4-6 所示,假設我們想在相鄰的兩個節點 `n0` 和 `n1` 之間插入一個新節點 `P` ,**則只需改變兩個節點引用(指標)即可**,時間複雜度為 $O(1)$ 。 + +相比之下,在陣列中插入元素的時間複雜度為 $O(n)$ ,在大資料量下的效率較低。 + +![鏈結串列插入節點示例](linked_list.assets/linkedlist_insert_node.png){ class="animation-figure" } + +

圖 4-6   鏈結串列插入節點示例

+ +=== "Python" + + ```python title="linked_list.py" + def insert(n0: ListNode, P: ListNode): + """在鏈結串列的節點 n0 之後插入節點 P""" + n1 = n0.next + P.next = n1 + n0.next = P + ``` + +=== "C++" + + ```cpp title="linked_list.cpp" + /* 在鏈結串列的節點 n0 之後插入節點 P */ + void insert(ListNode *n0, ListNode *P) { + ListNode *n1 = n0->next; + P->next = n1; + n0->next = P; + } + ``` + +=== "Java" + + ```java title="linked_list.java" + /* 在鏈結串列的節點 n0 之後插入節點 P */ + void insert(ListNode n0, ListNode P) { + ListNode n1 = n0.next; + P.next = n1; + n0.next = P; + } + ``` + +=== "C#" + + ```csharp title="linked_list.cs" + /* 在鏈結串列的節點 n0 之後插入節點 P */ + void Insert(ListNode n0, ListNode P) { + ListNode? n1 = n0.next; + P.next = n1; + n0.next = P; + } + ``` + +=== "Go" + + ```go title="linked_list.go" + /* 在鏈結串列的節點 n0 之後插入節點 P */ + func insertNode(n0 *ListNode, P *ListNode) { + n1 := n0.Next + P.Next = n1 + n0.Next = P + } + ``` + +=== "Swift" + + ```swift title="linked_list.swift" + /* 在鏈結串列的節點 n0 之後插入節點 P */ + func insert(n0: ListNode, P: ListNode) { + let n1 = n0.next + P.next = n1 + n0.next = P + } + ``` + +=== "JS" + + ```javascript title="linked_list.js" + /* 在鏈結串列的節點 n0 之後插入節點 P */ + function insert(n0, P) { + const n1 = n0.next; + P.next = n1; + n0.next = P; + } + ``` + +=== "TS" + + ```typescript title="linked_list.ts" + /* 在鏈結串列的節點 n0 之後插入節點 P */ + function insert(n0: ListNode, P: ListNode): void { + const n1 = n0.next; + P.next = n1; + n0.next = P; + } + ``` + +=== "Dart" + + ```dart title="linked_list.dart" + /* 在鏈結串列的節點 n0 之後插入節點 P */ + void insert(ListNode n0, ListNode P) { + ListNode? n1 = n0.next; + P.next = n1; + n0.next = P; + } + ``` + +=== "Rust" + + ```rust title="linked_list.rs" + /* 在鏈結串列的節點 n0 之後插入節點 P */ + #[allow(non_snake_case)] + pub fn insert(n0: &Rc>>, P: Rc>>) { + let n1 = n0.borrow_mut().next.take(); + P.borrow_mut().next = n1; + n0.borrow_mut().next = Some(P); + } + ``` + +=== "C" + + ```c title="linked_list.c" + /* 在鏈結串列的節點 n0 之後插入節點 P */ + void insert(ListNode *n0, ListNode *P) { + ListNode *n1 = n0->next; + P->next = n1; + n0->next = P; + } + ``` + +=== "Kotlin" + + ```kotlin title="linked_list.kt" + /* 在鏈結串列的節點 n0 之後插入節點p */ + fun insert(n0: ListNode?, p: ListNode?) { + val n1 = n0?.next + p?.next = n1 + n0?.next = p + } + ``` + +=== "Ruby" + + ```ruby title="linked_list.rb" + ### 在鏈結串列的節點 n0 之後插入節點 _p ### + # Ruby 的 `p` 是一個內建函式, `P` 是一個常數,所以可以使用 `_p` 代替 + def insert(n0, _p) + n1 = n0.next + _p.next = n1 + n0.next = _p + end + ``` + +=== "Zig" + + ```zig title="linked_list.zig" + // 在鏈結串列的節點 n0 之後插入節點 P + fn insert(n0: ?*inc.ListNode(i32), P: ?*inc.ListNode(i32)) void { + var n1 = n0.?.next; + P.?.next = n1; + n0.?.next = P; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +### 3.   刪除節點 + +如圖 4-7 所示,在鏈結串列中刪除節點也非常方便,**只需改變一個節點的引用(指標)即可**。 + +請注意,儘管在刪除操作完成後節點 `P` 仍然指向 `n1` ,但實際上走訪此鏈結串列已經無法訪問到 `P` ,這意味著 `P` 已經不再屬於該鏈結串列了。 + +![鏈結串列刪除節點](linked_list.assets/linkedlist_remove_node.png){ class="animation-figure" } + +

圖 4-7   鏈結串列刪除節點

+ +=== "Python" + + ```python title="linked_list.py" + def remove(n0: ListNode): + """刪除鏈結串列的節點 n0 之後的首個節點""" + if not n0.next: + return + # n0 -> P -> n1 + P = n0.next + n1 = P.next + n0.next = n1 + ``` + +=== "C++" + + ```cpp title="linked_list.cpp" + /* 刪除鏈結串列的節點 n0 之後的首個節點 */ + void remove(ListNode *n0) { + if (n0->next == nullptr) + return; + // n0 -> P -> n1 + ListNode *P = n0->next; + ListNode *n1 = P->next; + n0->next = n1; + // 釋放記憶體 + delete P; + } + ``` + +=== "Java" + + ```java title="linked_list.java" + /* 刪除鏈結串列的節點 n0 之後的首個節點 */ + void remove(ListNode n0) { + if (n0.next == null) + return; + // n0 -> P -> n1 + ListNode P = n0.next; + ListNode n1 = P.next; + n0.next = n1; + } + ``` + +=== "C#" + + ```csharp title="linked_list.cs" + /* 刪除鏈結串列的節點 n0 之後的首個節點 */ + void Remove(ListNode n0) { + if (n0.next == null) + return; + // n0 -> P -> n1 + ListNode P = n0.next; + ListNode? n1 = P.next; + n0.next = n1; + } + ``` + +=== "Go" + + ```go title="linked_list.go" + /* 刪除鏈結串列的節點 n0 之後的首個節點 */ + func removeItem(n0 *ListNode) { + if n0.Next == nil { + return + } + // n0 -> P -> n1 + P := n0.Next + n1 := P.Next + n0.Next = n1 + } + ``` + +=== "Swift" + + ```swift title="linked_list.swift" + /* 刪除鏈結串列的節點 n0 之後的首個節點 */ + func remove(n0: ListNode) { + if n0.next == nil { + return + } + // n0 -> P -> n1 + let P = n0.next + let n1 = P?.next + n0.next = n1 + } + ``` + +=== "JS" + + ```javascript title="linked_list.js" + /* 刪除鏈結串列的節點 n0 之後的首個節點 */ + function remove(n0) { + if (!n0.next) return; + // n0 -> P -> n1 + const P = n0.next; + const n1 = P.next; + n0.next = n1; + } + ``` + +=== "TS" + + ```typescript title="linked_list.ts" + /* 刪除鏈結串列的節點 n0 之後的首個節點 */ + function remove(n0: ListNode): void { + if (!n0.next) { + return; + } + // n0 -> P -> n1 + const P = n0.next; + const n1 = P.next; + n0.next = n1; + } + ``` + +=== "Dart" + + ```dart title="linked_list.dart" + /* 刪除鏈結串列的節點 n0 之後的首個節點 */ + void remove(ListNode n0) { + if (n0.next == null) return; + // n0 -> P -> n1 + ListNode P = n0.next!; + ListNode? n1 = P.next; + n0.next = n1; + } + ``` + +=== "Rust" + + ```rust title="linked_list.rs" + /* 刪除鏈結串列的節點 n0 之後的首個節點 */ + #[allow(non_snake_case)] + pub fn remove(n0: &Rc>>) { + if n0.borrow().next.is_none() { + return; + }; + // n0 -> P -> n1 + let P = n0.borrow_mut().next.take(); + if let Some(node) = P { + let n1 = node.borrow_mut().next.take(); + n0.borrow_mut().next = n1; + } + } + ``` + +=== "C" + + ```c title="linked_list.c" + /* 刪除鏈結串列的節點 n0 之後的首個節點 */ + // 注意:stdio.h 佔用了 remove 關鍵詞 + void removeItem(ListNode *n0) { + if (!n0->next) + return; + // n0 -> P -> n1 + ListNode *P = n0->next; + ListNode *n1 = P->next; + n0->next = n1; + // 釋放記憶體 + free(P); + } + ``` + +=== "Kotlin" + + ```kotlin title="linked_list.kt" + /* 刪除鏈結串列的節點 n0 之後的首個節點 */ + fun remove(n0: ListNode?) { + val p = n0?.next + val n1 = p?.next + n0?.next = n1 + } + ``` + +=== "Ruby" + + ```ruby title="linked_list.rb" + ### 刪除鏈結串列的節點 n0 之後的首個節點 ### + def remove(n0) + return if n0.next.nil? + + # n0 -> remove_node -> n1 + remove_node = n0.next + n1 = remove_node.next + n0.next = n1 + end + ``` + +=== "Zig" + + ```zig title="linked_list.zig" + // 刪除鏈結串列的節點 n0 之後的首個節點 + fn remove(n0: ?*inc.ListNode(i32)) void { + if (n0.?.next == null) return; + // n0 -> P -> n1 + var P = n0.?.next; + var n1 = P.?.next; + n0.?.next = n1; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +### 4.   訪問節點 + +**在鏈結串列中訪問節點的效率較低**。如上一節所述,我們可以在 $O(1)$ 時間下訪問陣列中的任意元素。鏈結串列則不然,程式需要從頭節點出發,逐個向後走訪,直至找到目標節點。也就是說,訪問鏈結串列的第 $i$ 個節點需要迴圈 $i - 1$ 輪,時間複雜度為 $O(n)$ 。 + +=== "Python" + + ```python title="linked_list.py" + def access(head: ListNode, index: int) -> ListNode | None: + """訪問鏈結串列中索引為 index 的節點""" + for _ in range(index): + if not head: + return None + head = head.next + return head + ``` + +=== "C++" + + ```cpp title="linked_list.cpp" + /* 訪問鏈結串列中索引為 index 的節點 */ + ListNode *access(ListNode *head, int index) { + for (int i = 0; i < index; i++) { + if (head == nullptr) + return nullptr; + head = head->next; + } + return head; + } + ``` + +=== "Java" + + ```java title="linked_list.java" + /* 訪問鏈結串列中索引為 index 的節點 */ + ListNode access(ListNode head, int index) { + for (int i = 0; i < index; i++) { + if (head == null) + return null; + head = head.next; + } + return head; + } + ``` + +=== "C#" + + ```csharp title="linked_list.cs" + /* 訪問鏈結串列中索引為 index 的節點 */ + ListNode? Access(ListNode? head, int index) { + for (int i = 0; i < index; i++) { + if (head == null) + return null; + head = head.next; + } + return head; + } + ``` + +=== "Go" + + ```go title="linked_list.go" + /* 訪問鏈結串列中索引為 index 的節點 */ + func access(head *ListNode, index int) *ListNode { + for i := 0; i < index; i++ { + if head == nil { + return nil + } + head = head.Next + } + return head + } + ``` + +=== "Swift" + + ```swift title="linked_list.swift" + /* 訪問鏈結串列中索引為 index 的節點 */ + func access(head: ListNode, index: Int) -> ListNode? { + var head: ListNode? = head + for _ in 0 ..< index { + if head == nil { + return nil + } + head = head?.next + } + return head + } + ``` + +=== "JS" + + ```javascript title="linked_list.js" + /* 訪問鏈結串列中索引為 index 的節點 */ + function access(head, index) { + for (let i = 0; i < index; i++) { + if (!head) { + return null; + } + head = head.next; + } + return head; + } + ``` + +=== "TS" + + ```typescript title="linked_list.ts" + /* 訪問鏈結串列中索引為 index 的節點 */ + function access(head: ListNode | null, index: number): ListNode | null { + for (let i = 0; i < index; i++) { + if (!head) { + return null; + } + head = head.next; + } + return head; + } + ``` + +=== "Dart" + + ```dart title="linked_list.dart" + /* 訪問鏈結串列中索引為 index 的節點 */ + ListNode? access(ListNode? head, int index) { + for (var i = 0; i < index; i++) { + if (head == null) return null; + head = head.next; + } + return head; + } + ``` + +=== "Rust" + + ```rust title="linked_list.rs" + /* 訪問鏈結串列中索引為 index 的節點 */ + pub fn access(head: Rc>>, index: i32) -> Rc>> { + if index <= 0 { + return head; + }; + if let Some(node) = &head.borrow().next { + return access(node.clone(), index - 1); + } + + return head; + } + ``` + +=== "C" + + ```c title="linked_list.c" + /* 訪問鏈結串列中索引為 index 的節點 */ + ListNode *access(ListNode *head, int index) { + for (int i = 0; i < index; i++) { + if (head == NULL) + return NULL; + head = head->next; + } + return head; + } + ``` + +=== "Kotlin" + + ```kotlin title="linked_list.kt" + /* 訪問鏈結串列中索引為 index 的節點 */ + fun access(head: ListNode?, index: Int): ListNode? { + var h = head + for (i in 0..
+ + +### 5.   查詢節點 + +走訪鏈結串列,查詢其中值為 `target` 的節點,輸出該節點在鏈結串列中的索引。此過程也屬於線性查詢。程式碼如下所示: + +=== "Python" + + ```python title="linked_list.py" + def find(head: ListNode, target: int) -> int: + """在鏈結串列中查詢值為 target 的首個節點""" + index = 0 + while head: + if head.val == target: + return index + head = head.next + index += 1 + return -1 + ``` + +=== "C++" + + ```cpp title="linked_list.cpp" + /* 在鏈結串列中查詢值為 target 的首個節點 */ + int find(ListNode *head, int target) { + int index = 0; + while (head != nullptr) { + if (head->val == target) + return index; + head = head->next; + index++; + } + return -1; + } + ``` + +=== "Java" + + ```java title="linked_list.java" + /* 在鏈結串列中查詢值為 target 的首個節點 */ + int find(ListNode head, int target) { + int index = 0; + while (head != null) { + if (head.val == target) + return index; + head = head.next; + index++; + } + return -1; + } + ``` + +=== "C#" + + ```csharp title="linked_list.cs" + /* 在鏈結串列中查詢值為 target 的首個節點 */ + int Find(ListNode? head, int target) { + int index = 0; + while (head != null) { + if (head.val == target) + return index; + head = head.next; + index++; + } + return -1; + } + ``` + +=== "Go" + + ```go title="linked_list.go" + /* 在鏈結串列中查詢值為 target 的首個節點 */ + func findNode(head *ListNode, target int) int { + index := 0 + for head != nil { + if head.Val == target { + return index + } + head = head.Next + index++ + } + return -1 + } + ``` + +=== "Swift" + + ```swift title="linked_list.swift" + /* 在鏈結串列中查詢值為 target 的首個節點 */ + func find(head: ListNode, target: Int) -> Int { + var head: ListNode? = head + var index = 0 + while head != nil { + if head?.val == target { + return index + } + head = head?.next + index += 1 + } + return -1 + } + ``` + +=== "JS" + + ```javascript title="linked_list.js" + /* 在鏈結串列中查詢值為 target 的首個節點 */ + function find(head, target) { + let index = 0; + while (head !== null) { + if (head.val === target) { + return index; + } + head = head.next; + index += 1; + } + return -1; + } + ``` + +=== "TS" + + ```typescript title="linked_list.ts" + /* 在鏈結串列中查詢值為 target 的首個節點 */ + function find(head: ListNode | null, target: number): number { + let index = 0; + while (head !== null) { + if (head.val === target) { + return index; + } + head = head.next; + index += 1; + } + return -1; + } + ``` + +=== "Dart" + + ```dart title="linked_list.dart" + /* 在鏈結串列中查詢值為 target 的首個節點 */ + int find(ListNode? head, int target) { + int index = 0; + while (head != null) { + if (head.val == target) { + return index; + } + head = head.next; + index++; + } + return -1; + } + ``` + +=== "Rust" + + ```rust title="linked_list.rs" + /* 在鏈結串列中查詢值為 target 的首個節點 */ + pub fn find(head: Rc>>, target: T, index: i32) -> i32 { + if head.borrow().val == target { + return index; + }; + if let Some(node) = &head.borrow_mut().next { + return find(node.clone(), target, index + 1); + } + return -1; + } + ``` + +=== "C" + + ```c title="linked_list.c" + /* 在鏈結串列中查詢值為 target 的首個節點 */ + int find(ListNode *head, int target) { + int index = 0; + while (head) { + if (head->val == target) + return index; + head = head->next; + index++; + } + return -1; + } + ``` + +=== "Kotlin" + + ```kotlin title="linked_list.kt" + /* 在鏈結串列中查詢值為 target 的首個節點 */ + fun find(head: ListNode?, target: Int): Int { + var index = 0 + var h = head + while (h != null) { + if (h.value == target) return index + h = h.next + index++ + } + return -1 + } + ``` + +=== "Ruby" + + ```ruby title="linked_list.rb" + ### 在鏈結串列中查詢值為 target 的首個節點 ### + def find(head, target) + index = 0 + while head + return index if head.val == target + head = head.next + index += 1 + end + + -1 + end + ``` + +=== "Zig" + + ```zig title="linked_list.zig" + // 在鏈結串列中查詢值為 target 的首個節點 + fn find(node: ?*inc.ListNode(i32), target: i32) i32 { + var head = node; + var index: i32 = 0; + while (head != null) { + if (head.?.val == target) return index; + head = head.?.next; + index += 1; + } + return -1; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 4.2.2   陣列 vs. 鏈結串列 + +表 4-1 總結了陣列和鏈結串列的各項特點並對比了操作效率。由於它們採用兩種相反的儲存策略,因此各種性質和操作效率也呈現對立的特點。 + +

表 4-1   陣列與鏈結串列的效率對比

+ +
+ +| | 陣列 | 鏈結串列 | +| -------- | ------------------------------ | -------------- | +| 儲存方式 | 連續記憶體空間 | 分散記憶體空間 | +| 容量擴展 | 長度不可變 | 可靈活擴展 | +| 記憶體效率 | 元素佔用記憶體少、但可能浪費空間 | 元素佔用記憶體多 | +| 訪問元素 | $O(1)$ | $O(n)$ | +| 新增元素 | $O(n)$ | $O(1)$ | +| 刪除元素 | $O(n)$ | $O(1)$ | + +
+ +## 4.2.3   常見鏈結串列型別 + +如圖 4-8 所示,常見的鏈結串列型別包括三種。 + +- **單向鏈結串列**:即前面介紹的普通鏈結串列。單向鏈結串列的節點包含值和指向下一節點的引用兩項資料。我們將首個節點稱為頭節點,將最後一個節點稱為尾節點,尾節點指向空 `None` 。 +- **環形鏈結串列**:如果我們令單向鏈結串列的尾節點指向頭節點(首尾相接),則得到一個環形鏈結串列。在環形鏈結串列中,任意節點都可以視作頭節點。 +- **雙向鏈結串列**:與單向鏈結串列相比,雙向鏈結串列記錄了兩個方向的引用。雙向鏈結串列的節點定義同時包含指向後繼節點(下一個節點)和前驅節點(上一個節點)的引用(指標)。相較於單向鏈結串列,雙向鏈結串列更具靈活性,可以朝兩個方向走訪鏈結串列,但相應地也需要佔用更多的記憶體空間。 + +=== "Python" + + ```python title="" + class ListNode: + """雙向鏈結串列節點類別""" + def __init__(self, val: int): + self.val: int = val # 節點值 + self.next: ListNode | None = None # 指向後繼節點的引用 + self.prev: ListNode | None = None # 指向前驅節點的引用 + ``` + +=== "C++" + + ```cpp title="" + /* 雙向鏈結串列節點結構體 */ + struct ListNode { + int val; // 節點值 + ListNode *next; // 指向後繼節點的指標 + ListNode *prev; // 指向前驅節點的指標 + ListNode(int x) : val(x), next(nullptr), prev(nullptr) {} // 建構子 + }; + ``` + +=== "Java" + + ```java title="" + /* 雙向鏈結串列節點類別 */ + class ListNode { + int val; // 節點值 + ListNode next; // 指向後繼節點的引用 + ListNode prev; // 指向前驅節點的引用 + ListNode(int x) { val = x; } // 建構子 + } + ``` + +=== "C#" + + ```csharp title="" + /* 雙向鏈結串列節點類別 */ + class ListNode(int x) { // 建構子 + int val = x; // 節點值 + ListNode next; // 指向後繼節點的引用 + ListNode prev; // 指向前驅節點的引用 + } + ``` + +=== "Go" + + ```go title="" + /* 雙向鏈結串列節點結構體 */ + type DoublyListNode struct { + Val int // 節點值 + Next *DoublyListNode // 指向後繼節點的指標 + Prev *DoublyListNode // 指向前驅節點的指標 + } + + // NewDoublyListNode 初始化 + func NewDoublyListNode(val int) *DoublyListNode { + return &DoublyListNode{ + Val: val, + Next: nil, + Prev: nil, + } + } + ``` + +=== "Swift" + + ```swift title="" + /* 雙向鏈結串列節點類別 */ + class ListNode { + var val: Int // 節點值 + var next: ListNode? // 指向後繼節點的引用 + var prev: ListNode? // 指向前驅節點的引用 + + init(x: Int) { // 建構子 + val = x + } + } + ``` + +=== "JS" + + ```javascript title="" + /* 雙向鏈結串列節點類別 */ + class ListNode { + constructor(val, next, prev) { + this.val = val === undefined ? 0 : val; // 節點值 + this.next = next === undefined ? null : next; // 指向後繼節點的引用 + this.prev = prev === undefined ? null : prev; // 指向前驅節點的引用 + } + } + ``` + +=== "TS" + + ```typescript title="" + /* 雙向鏈結串列節點類別 */ + class ListNode { + val: number; + next: ListNode | null; + prev: ListNode | null; + constructor(val?: number, next?: ListNode | null, prev?: ListNode | null) { + this.val = val === undefined ? 0 : val; // 節點值 + this.next = next === undefined ? null : next; // 指向後繼節點的引用 + this.prev = prev === undefined ? null : prev; // 指向前驅節點的引用 + } + } + ``` + +=== "Dart" + + ```dart title="" + /* 雙向鏈結串列節點類別 */ + class ListNode { + int val; // 節點值 + ListNode next; // 指向後繼節點的引用 + ListNode prev; // 指向前驅節點的引用 + ListNode(this.val, [this.next, this.prev]); // 建構子 + } + ``` + +=== "Rust" + + ```rust title="" + use std::rc::Rc; + use std::cell::RefCell; + + /* 雙向鏈結串列節點型別 */ + #[derive(Debug)] + struct ListNode { + val: i32, // 節點值 + next: Option>>, // 指向後繼節點的指標 + prev: Option>>, // 指向前驅節點的指標 + } + + /* 建構子 */ + impl ListNode { + fn new(val: i32) -> Self { + ListNode { + val, + next: None, + prev: None, + } + } + } + ``` + +=== "C" + + ```c title="" + /* 雙向鏈結串列節點結構體 */ + typedef struct ListNode { + int val; // 節點值 + struct ListNode *next; // 指向後繼節點的指標 + struct ListNode *prev; // 指向前驅節點的指標 + } ListNode; + + /* 建構子 */ + ListNode *newListNode(int val) { + ListNode *node; + node = (ListNode *) malloc(sizeof(ListNode)); + node->val = val; + node->next = NULL; + node->prev = NULL; + return node; + } + ``` + +=== "Kotlin" + + ```kotlin title="" + /* 雙向鏈結串列節點類別 */ + // 建構子 + class ListNode(x: Int) { + val _val: Int = x // 節點值 + val next: ListNode? = null // 指向後繼節點的引用 + val prev: ListNode? = null // 指向前驅節點的引用 + } + ``` + +=== "Ruby" + + ```ruby title="" + # 雙向鏈結串列節點類別 + class ListNode + attr_accessor :val # 節點值 + attr_accessor :next # 指向後繼節點的引用 + attr_accessor :prev # 指向前驅節點的引用 + + def initialize(val=0, next_node=nil, prev_node=nil) + @val = val + @next = next_node + @prev = prev_node + end + end + ``` + +=== "Zig" + + ```zig title="" + // 雙向鏈結串列節點類別 + pub fn ListNode(comptime T: type) type { + return struct { + const Self = @This(); + + val: T = 0, // 節點值 + next: ?*Self = null, // 指向後繼節點的指標 + prev: ?*Self = null, // 指向前驅節點的指標 + + // 建構子 + pub fn init(self: *Self, x: i32) void { + self.val = x; + self.next = null; + self.prev = null; + } + }; + } + ``` + +![常見鏈結串列種類](linked_list.assets/linkedlist_common_types.png){ class="animation-figure" } + +

圖 4-8   常見鏈結串列種類

+ +## 4.2.4   鏈結串列典型應用 + +單向鏈結串列通常用於實現堆疊、佇列、雜湊表和圖等資料結構。 + +- **堆疊與佇列**:當插入和刪除操作都在鏈結串列的一端進行時,它表現出先進後出的特性,對應堆疊;當插入操作在鏈結串列的一端進行,刪除操作在鏈結串列的另一端進行,它表現出先進先出的特性,對應佇列。 +- **雜湊表**:鏈式位址是解決雜湊衝突的主流方案之一,在該方案中,所有衝突的元素都會被放到一個鏈結串列中。 +- **圖**:鄰接表是表示圖的一種常用方式,其中圖的每個頂點都與一個鏈結串列相關聯,鏈結串列中的每個元素都代表與該頂點相連的其他頂點。 + +雙向鏈結串列常用於需要快速查詢前一個和後一個元素的場景。 + +- **高階資料結構**:比如在紅黑樹、B 樹中,我們需要訪問節點的父節點,這可以透過在節點中儲存一個指向父節點的引用來實現,類似於雙向鏈結串列。 +- **瀏覽器歷史**:在網頁瀏覽器中,當用戶點選前進或後退按鈕時,瀏覽器需要知道使用者訪問過的前一個和後一個網頁。雙向鏈結串列的特性使得這種操作變得簡單。 +- **LRU 演算法**:在快取淘汰(LRU)演算法中,我們需要快速找到最近最少使用的資料,以及支持快速新增和刪除節點。這時候使用雙向鏈結串列就非常合適。 + +環形鏈結串列常用於需要週期性操作的場景,比如作業系統的資源排程。 + +- **時間片輪轉排程演算法**:在作業系統中,時間片輪轉排程演算法是一種常見的 CPU 排程演算法,它需要對一組程序進行迴圈。每個程序被賦予一個時間片,當時間片用完時,CPU 將切換到下一個程序。這種迴圈操作可以透過環形鏈結串列來實現。 +- **資料緩衝區**:在某些資料緩衝區的實現中,也可能會使用環形鏈結串列。比如在音訊、影片播放器中,資料流可能會被分成多個緩衝塊並放入一個環形鏈結串列,以便實現無縫播放。 diff --git a/zh-Hant/docs/chapter_array_and_linkedlist/list.md b/zh-Hant/docs/chapter_array_and_linkedlist/list.md new file mode 100755 index 000000000..4bdaa41dc --- /dev/null +++ b/zh-Hant/docs/chapter_array_and_linkedlist/list.md @@ -0,0 +1,2496 @@ +--- +comments: true +--- + +# 4.3   串列 + +串列(list)是一個抽象的資料結構概念,它表示元素的有序集合,支持元素訪問、修改、新增、刪除和走訪等操作,無須使用者考慮容量限制的問題。串列可以基於鏈結串列或陣列實現。 + +- 鏈結串列天然可以看作一個串列,其支持元素增刪查改操作,並且可以靈活動態擴容。 +- 陣列也支持元素增刪查改,但由於其長度不可變,因此只能看作一個具有長度限制的串列。 + +當使用陣列實現串列時,**長度不可變的性質會導致串列的實用性降低**。這是因為我們通常無法事先確定需要儲存多少資料,從而難以選擇合適的串列長度。若長度過小,則很可能無法滿足使用需求;若長度過大,則會造成記憶體空間浪費。 + +為解決此問題,我們可以使用動態陣列(dynamic array)來實現串列。它繼承了陣列的各項優點,並且可以在程式執行過程中進行動態擴容。 + +實際上,**許多程式語言中的標準庫提供的串列是基於動態陣列實現的**,例如 Python 中的 `list` 、Java 中的 `ArrayList` 、C++ 中的 `vector` 和 C# 中的 `List` 等。在接下來的討論中,我們將把“串列”和“動態陣列”視為等同的概念。 + +## 4.3.1   串列常用操作 + +### 1.   初始化串列 + +我們通常使用“無初始值”和“有初始值”這兩種初始化方法: + +=== "Python" + + ```python title="list.py" + # 初始化串列 + # 無初始值 + nums1: list[int] = [] + # 有初始值 + nums: list[int] = [1, 3, 2, 5, 4] + ``` + +=== "C++" + + ```cpp title="list.cpp" + /* 初始化串列 */ + // 需注意,C++ 中 vector 即是本文描述的 nums + // 無初始值 + vector nums1; + // 有初始值 + vector nums = { 1, 3, 2, 5, 4 }; + ``` + +=== "Java" + + ```java title="list.java" + /* 初始化串列 */ + // 無初始值 + List nums1 = new ArrayList<>(); + // 有初始值(注意陣列的元素型別需為 int[] 的包裝類別 Integer[]) + Integer[] numbers = new Integer[] { 1, 3, 2, 5, 4 }; + List nums = new ArrayList<>(Arrays.asList(numbers)); + ``` + +=== "C#" + + ```csharp title="list.cs" + /* 初始化串列 */ + // 無初始值 + List nums1 = []; + // 有初始值 + int[] numbers = [1, 3, 2, 5, 4]; + List nums = [.. numbers]; + ``` + +=== "Go" + + ```go title="list_test.go" + /* 初始化串列 */ + // 無初始值 + nums1 := []int{} + // 有初始值 + nums := []int{1, 3, 2, 5, 4} + ``` + +=== "Swift" + + ```swift title="list.swift" + /* 初始化串列 */ + // 無初始值 + let nums1: [Int] = [] + // 有初始值 + var nums = [1, 3, 2, 5, 4] + ``` + +=== "JS" + + ```javascript title="list.js" + /* 初始化串列 */ + // 無初始值 + const nums1 = []; + // 有初始值 + const nums = [1, 3, 2, 5, 4]; + ``` + +=== "TS" + + ```typescript title="list.ts" + /* 初始化串列 */ + // 無初始值 + const nums1: number[] = []; + // 有初始值 + const nums: number[] = [1, 3, 2, 5, 4]; + ``` + +=== "Dart" + + ```dart title="list.dart" + /* 初始化串列 */ + // 無初始值 + List nums1 = []; + // 有初始值 + List nums = [1, 3, 2, 5, 4]; + ``` + +=== "Rust" + + ```rust title="list.rs" + /* 初始化串列 */ + // 無初始值 + let nums1: Vec = Vec::new(); + // 有初始值 + let nums: Vec = vec![1, 3, 2, 5, 4]; + ``` + +=== "C" + + ```c title="list.c" + // C 未提供內建動態陣列 + ``` + +=== "Kotlin" + + ```kotlin title="list.kt" + /* 初始化串列 */ + // 無初始值 + var nums1 = listOf() + // 有初始值 + var numbers = arrayOf(1, 3, 2, 5, 4) + var nums = numbers.toMutableList() + ``` + +=== "Ruby" + + ```ruby title="list.rb" + # 初始化串列 + # 無初始值 + nums1 = [] + # 有初始值 + nums = [1, 3, 2, 5, 4] + ``` + +=== "Zig" + + ```zig title="list.zig" + // 初始化串列 + var nums = std.ArrayList(i32).init(std.heap.page_allocator); + defer nums.deinit(); + try nums.appendSlice(&[_]i32{ 1, 3, 2, 5, 4 }); + ``` + +??? pythontutor "視覺化執行" + +
+ + +### 2.   訪問元素 + +串列本質上是陣列,因此可以在 $O(1)$ 時間內訪問和更新元素,效率很高。 + +=== "Python" + + ```python title="list.py" + # 訪問元素 + num: int = nums[1] # 訪問索引 1 處的元素 + + # 更新元素 + nums[1] = 0 # 將索引 1 處的元素更新為 0 + ``` + +=== "C++" + + ```cpp title="list.cpp" + /* 訪問元素 */ + int num = nums[1]; // 訪問索引 1 處的元素 + + /* 更新元素 */ + nums[1] = 0; // 將索引 1 處的元素更新為 0 + ``` + +=== "Java" + + ```java title="list.java" + /* 訪問元素 */ + int num = nums.get(1); // 訪問索引 1 處的元素 + + /* 更新元素 */ + nums.set(1, 0); // 將索引 1 處的元素更新為 0 + ``` + +=== "C#" + + ```csharp title="list.cs" + /* 訪問元素 */ + int num = nums[1]; // 訪問索引 1 處的元素 + + /* 更新元素 */ + nums[1] = 0; // 將索引 1 處的元素更新為 0 + ``` + +=== "Go" + + ```go title="list_test.go" + /* 訪問元素 */ + num := nums[1] // 訪問索引 1 處的元素 + + /* 更新元素 */ + nums[1] = 0 // 將索引 1 處的元素更新為 0 + ``` + +=== "Swift" + + ```swift title="list.swift" + /* 訪問元素 */ + let num = nums[1] // 訪問索引 1 處的元素 + + /* 更新元素 */ + nums[1] = 0 // 將索引 1 處的元素更新為 0 + ``` + +=== "JS" + + ```javascript title="list.js" + /* 訪問元素 */ + const num = nums[1]; // 訪問索引 1 處的元素 + + /* 更新元素 */ + nums[1] = 0; // 將索引 1 處的元素更新為 0 + ``` + +=== "TS" + + ```typescript title="list.ts" + /* 訪問元素 */ + const num: number = nums[1]; // 訪問索引 1 處的元素 + + /* 更新元素 */ + nums[1] = 0; // 將索引 1 處的元素更新為 0 + ``` + +=== "Dart" + + ```dart title="list.dart" + /* 訪問元素 */ + int num = nums[1]; // 訪問索引 1 處的元素 + + /* 更新元素 */ + nums[1] = 0; // 將索引 1 處的元素更新為 0 + ``` + +=== "Rust" + + ```rust title="list.rs" + /* 訪問元素 */ + let num: i32 = nums[1]; // 訪問索引 1 處的元素 + /* 更新元素 */ + nums[1] = 0; // 將索引 1 處的元素更新為 0 + ``` + +=== "C" + + ```c title="list.c" + // C 未提供內建動態陣列 + ``` + +=== "Kotlin" + + ```kotlin title="list.kt" + /* 訪問元素 */ + val num = nums[1] // 訪問索引 1 處的元素 + /* 更新元素 */ + nums[1] = 0 // 將索引 1 處的元素更新為 0 + ``` + +=== "Ruby" + + ```ruby title="list.rb" + # 訪問元素 + num = nums[1] # 訪問索引 1 處的元素 + # 更新元素 + nums[1] = 0 # 將索引 1 處的元素更新為 0 + ``` + +=== "Zig" + + ```zig title="list.zig" + // 訪問元素 + var num = nums.items[1]; // 訪問索引 1 處的元素 + + // 更新元素 + nums.items[1] = 0; // 將索引 1 處的元素更新為 0 + ``` + +??? pythontutor "視覺化執行" + +
+ + +### 3.   插入與刪除元素 + +相較於陣列,串列可以自由地新增與刪除元素。在串列尾部新增元素的時間複雜度為 $O(1)$ ,但插入和刪除元素的效率仍與陣列相同,時間複雜度為 $O(n)$ 。 + +=== "Python" + + ```python title="list.py" + # 清空串列 + nums.clear() + + # 在尾部新增元素 + nums.append(1) + nums.append(3) + nums.append(2) + nums.append(5) + nums.append(4) + + # 在中間插入元素 + nums.insert(3, 6) # 在索引 3 處插入數字 6 + + # 刪除元素 + nums.pop(3) # 刪除索引 3 處的元素 + ``` + +=== "C++" + + ```cpp title="list.cpp" + /* 清空串列 */ + nums.clear(); + + /* 在尾部新增元素 */ + nums.push_back(1); + nums.push_back(3); + nums.push_back(2); + nums.push_back(5); + nums.push_back(4); + + /* 在中間插入元素 */ + nums.insert(nums.begin() + 3, 6); // 在索引 3 處插入數字 6 + + /* 刪除元素 */ + nums.erase(nums.begin() + 3); // 刪除索引 3 處的元素 + ``` + +=== "Java" + + ```java title="list.java" + /* 清空串列 */ + nums.clear(); + + /* 在尾部新增元素 */ + nums.add(1); + nums.add(3); + nums.add(2); + nums.add(5); + nums.add(4); + + /* 在中間插入元素 */ + nums.add(3, 6); // 在索引 3 處插入數字 6 + + /* 刪除元素 */ + nums.remove(3); // 刪除索引 3 處的元素 + ``` + +=== "C#" + + ```csharp title="list.cs" + /* 清空串列 */ + nums.Clear(); + + /* 在尾部新增元素 */ + nums.Add(1); + nums.Add(3); + nums.Add(2); + nums.Add(5); + nums.Add(4); + + /* 在中間插入元素 */ + nums.Insert(3, 6); + + /* 刪除元素 */ + nums.RemoveAt(3); + ``` + +=== "Go" + + ```go title="list_test.go" + /* 清空串列 */ + nums = nil + + /* 在尾部新增元素 */ + nums = append(nums, 1) + nums = append(nums, 3) + nums = append(nums, 2) + nums = append(nums, 5) + nums = append(nums, 4) + + /* 在中間插入元素 */ + nums = append(nums[:3], append([]int{6}, nums[3:]...)...) // 在索引 3 處插入數字 6 + + /* 刪除元素 */ + nums = append(nums[:3], nums[4:]...) // 刪除索引 3 處的元素 + ``` + +=== "Swift" + + ```swift title="list.swift" + /* 清空串列 */ + nums.removeAll() + + /* 在尾部新增元素 */ + nums.append(1) + nums.append(3) + nums.append(2) + nums.append(5) + nums.append(4) + + /* 在中間插入元素 */ + nums.insert(6, at: 3) // 在索引 3 處插入數字 6 + + /* 刪除元素 */ + nums.remove(at: 3) // 刪除索引 3 處的元素 + ``` + +=== "JS" + + ```javascript title="list.js" + /* 清空串列 */ + nums.length = 0; + + /* 在尾部新增元素 */ + nums.push(1); + nums.push(3); + nums.push(2); + nums.push(5); + nums.push(4); + + /* 在中間插入元素 */ + nums.splice(3, 0, 6); + + /* 刪除元素 */ + nums.splice(3, 1); + ``` + +=== "TS" + + ```typescript title="list.ts" + /* 清空串列 */ + nums.length = 0; + + /* 在尾部新增元素 */ + nums.push(1); + nums.push(3); + nums.push(2); + nums.push(5); + nums.push(4); + + /* 在中間插入元素 */ + nums.splice(3, 0, 6); + + /* 刪除元素 */ + nums.splice(3, 1); + ``` + +=== "Dart" + + ```dart title="list.dart" + /* 清空串列 */ + nums.clear(); + + /* 在尾部新增元素 */ + nums.add(1); + nums.add(3); + nums.add(2); + nums.add(5); + nums.add(4); + + /* 在中間插入元素 */ + nums.insert(3, 6); // 在索引 3 處插入數字 6 + + /* 刪除元素 */ + nums.removeAt(3); // 刪除索引 3 處的元素 + ``` + +=== "Rust" + + ```rust title="list.rs" + /* 清空串列 */ + nums.clear(); + + /* 在尾部新增元素 */ + nums.push(1); + nums.push(3); + nums.push(2); + nums.push(5); + nums.push(4); + + /* 在中間插入元素 */ + nums.insert(3, 6); // 在索引 3 處插入數字 6 + + /* 刪除元素 */ + nums.remove(3); // 刪除索引 3 處的元素 + ``` + +=== "C" + + ```c title="list.c" + // C 未提供內建動態陣列 + ``` + +=== "Kotlin" + + ```kotlin title="list.kt" + /* 清空串列 */ + nums.clear(); + + /* 在尾部新增元素 */ + nums.add(1); + nums.add(3); + nums.add(2); + nums.add(5); + nums.add(4); + + /* 在中間插入元素 */ + nums.add(3, 6); // 在索引 3 處插入數字 6 + + /* 刪除元素 */ + nums.remove(3); // 刪除索引 3 處的元素 + ``` + +=== "Ruby" + + ```ruby title="list.rb" + # 清空串列 + nums.clear + + # 在尾部新增元素 + nums << 1 + nums << 3 + nums << 2 + nums << 5 + nums << 4 + + # 在中間插入元素 + nums.insert(3, 6) # 在索引 3 處插入數字 6 + + # 刪除元素 + nums.delete_at(3) # 刪除索引 3 處的元素 + ``` + +=== "Zig" + + ```zig title="list.zig" + // 清空串列 + nums.clearRetainingCapacity(); + + // 在尾部新增元素 + try nums.append(1); + try nums.append(3); + try nums.append(2); + try nums.append(5); + try nums.append(4); + + // 在中間插入元素 + try nums.insert(3, 6); // 在索引 3 處插入數字 6 + + // 刪除元素 + _ = nums.orderedRemove(3); // 刪除索引 3 處的元素 + ``` + +??? pythontutor "視覺化執行" + +
+ + +### 4.   走訪串列 + +與陣列一樣,串列可以根據索引走訪,也可以直接走訪各元素。 + +=== "Python" + + ```python title="list.py" + # 透過索引走訪串列 + count = 0 + for i in range(len(nums)): + count += nums[i] + + # 直接走訪串列元素 + for num in nums: + count += num + ``` + +=== "C++" + + ```cpp title="list.cpp" + /* 透過索引走訪串列 */ + int count = 0; + for (int i = 0; i < nums.size(); i++) { + count += nums[i]; + } + + /* 直接走訪串列元素 */ + count = 0; + for (int num : nums) { + count += num; + } + ``` + +=== "Java" + + ```java title="list.java" + /* 透過索引走訪串列 */ + int count = 0; + for (int i = 0; i < nums.size(); i++) { + count += nums.get(i); + } + + /* 直接走訪串列元素 */ + for (int num : nums) { + count += num; + } + ``` + +=== "C#" + + ```csharp title="list.cs" + /* 透過索引走訪串列 */ + int count = 0; + for (int i = 0; i < nums.Count; i++) { + count += nums[i]; + } + + /* 直接走訪串列元素 */ + count = 0; + foreach (int num in nums) { + count += num; + } + ``` + +=== "Go" + + ```go title="list_test.go" + /* 透過索引走訪串列 */ + count := 0 + for i := 0; i < len(nums); i++ { + count += nums[i] + } + + /* 直接走訪串列元素 */ + count = 0 + for _, num := range nums { + count += num + } + ``` + +=== "Swift" + + ```swift title="list.swift" + /* 透過索引走訪串列 */ + var count = 0 + for i in nums.indices { + count += nums[i] + } + + /* 直接走訪串列元素 */ + count = 0 + for num in nums { + count += num + } + ``` + +=== "JS" + + ```javascript title="list.js" + /* 透過索引走訪串列 */ + let count = 0; + for (let i = 0; i < nums.length; i++) { + count += nums[i]; + } + + /* 直接走訪串列元素 */ + count = 0; + for (const num of nums) { + count += num; + } + ``` + +=== "TS" + + ```typescript title="list.ts" + /* 透過索引走訪串列 */ + let count = 0; + for (let i = 0; i < nums.length; i++) { + count += nums[i]; + } + + /* 直接走訪串列元素 */ + count = 0; + for (const num of nums) { + count += num; + } + ``` + +=== "Dart" + + ```dart title="list.dart" + /* 透過索引走訪串列 */ + int count = 0; + for (var i = 0; i < nums.length; i++) { + count += nums[i]; + } + + /* 直接走訪串列元素 */ + count = 0; + for (var num in nums) { + count += num; + } + ``` + +=== "Rust" + + ```rust title="list.rs" + // 透過索引走訪串列 + let mut _count = 0; + for i in 0..nums.len() { + _count += nums[i]; + } + + // 直接走訪串列元素 + _count = 0; + for num in &nums { + _count += num; + } + ``` + +=== "C" + + ```c title="list.c" + // C 未提供內建動態陣列 + ``` + +=== "Kotlin" + + ```kotlin title="list.kt" + /* 透過索引走訪串列 */ + var count = 0 + for (i in nums.indices) { + count += nums[i] + } + + /* 直接走訪串列元素 */ + for (num in nums) { + count += num + } + ``` + +=== "Ruby" + + ```ruby title="list.rb" + # 透過索引走訪串列 + count = 0 + for i in 0...nums.length + count += nums[i] + end + + # 直接走訪串列元素 + count = 0 + for num in nums + count += num + end + ``` + +=== "Zig" + + ```zig title="list.zig" + // 透過索引走訪串列 + var count: i32 = 0; + var i: i32 = 0; + while (i < nums.items.len) : (i += 1) { + count += nums[i]; + } + + // 直接走訪串列元素 + count = 0; + for (nums.items) |num| { + count += num; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +### 5.   拼接串列 + +給定一個新串列 `nums1` ,我們可以將其拼接到原串列的尾部。 + +=== "Python" + + ```python title="list.py" + # 拼接兩個串列 + nums1: list[int] = [6, 8, 7, 10, 9] + nums += nums1 # 將串列 nums1 拼接到 nums 之後 + ``` + +=== "C++" + + ```cpp title="list.cpp" + /* 拼接兩個串列 */ + vector nums1 = { 6, 8, 7, 10, 9 }; + // 將串列 nums1 拼接到 nums 之後 + nums.insert(nums.end(), nums1.begin(), nums1.end()); + ``` + +=== "Java" + + ```java title="list.java" + /* 拼接兩個串列 */ + List nums1 = new ArrayList<>(Arrays.asList(new Integer[] { 6, 8, 7, 10, 9 })); + nums.addAll(nums1); // 將串列 nums1 拼接到 nums 之後 + ``` + +=== "C#" + + ```csharp title="list.cs" + /* 拼接兩個串列 */ + List nums1 = [6, 8, 7, 10, 9]; + nums.AddRange(nums1); // 將串列 nums1 拼接到 nums 之後 + ``` + +=== "Go" + + ```go title="list_test.go" + /* 拼接兩個串列 */ + nums1 := []int{6, 8, 7, 10, 9} + nums = append(nums, nums1...) // 將串列 nums1 拼接到 nums 之後 + ``` + +=== "Swift" + + ```swift title="list.swift" + /* 拼接兩個串列 */ + let nums1 = [6, 8, 7, 10, 9] + nums.append(contentsOf: nums1) // 將串列 nums1 拼接到 nums 之後 + ``` + +=== "JS" + + ```javascript title="list.js" + /* 拼接兩個串列 */ + const nums1 = [6, 8, 7, 10, 9]; + nums.push(...nums1); // 將串列 nums1 拼接到 nums 之後 + ``` + +=== "TS" + + ```typescript title="list.ts" + /* 拼接兩個串列 */ + const nums1: number[] = [6, 8, 7, 10, 9]; + nums.push(...nums1); // 將串列 nums1 拼接到 nums 之後 + ``` + +=== "Dart" + + ```dart title="list.dart" + /* 拼接兩個串列 */ + List nums1 = [6, 8, 7, 10, 9]; + nums.addAll(nums1); // 將串列 nums1 拼接到 nums 之後 + ``` + +=== "Rust" + + ```rust title="list.rs" + /* 拼接兩個串列 */ + let nums1: Vec = vec![6, 8, 7, 10, 9]; + nums.extend(nums1); + ``` + +=== "C" + + ```c title="list.c" + // C 未提供內建動態陣列 + ``` + +=== "Kotlin" + + ```kotlin title="list.kt" + /* 拼接兩個串列 */ + val nums1 = intArrayOf(6, 8, 7, 10, 9).toMutableList() + nums.addAll(nums1) // 將串列 nums1 拼接到 nums 之後 + ``` + +=== "Ruby" + + ```ruby title="list.rb" + # 拼接兩個串列 + nums1 = [6, 8, 7, 10, 9] + nums += nums1 + ``` + +=== "Zig" + + ```zig title="list.zig" + // 拼接兩個串列 + var nums1 = std.ArrayList(i32).init(std.heap.page_allocator); + defer nums1.deinit(); + try nums1.appendSlice(&[_]i32{ 6, 8, 7, 10, 9 }); + try nums.insertSlice(nums.items.len, nums1.items); // 將串列 nums1 拼接到 nums 之後 + ``` + +??? pythontutor "視覺化執行" + +
+ + +### 6.   排序串列 + +完成串列排序後,我們便可以使用在陣列類別演算法題中經常考查的“二分搜尋”和“雙指標”演算法。 + +=== "Python" + + ```python title="list.py" + # 排序串列 + nums.sort() # 排序後,串列元素從小到大排列 + ``` + +=== "C++" + + ```cpp title="list.cpp" + /* 排序串列 */ + sort(nums.begin(), nums.end()); // 排序後,串列元素從小到大排列 + ``` + +=== "Java" + + ```java title="list.java" + /* 排序串列 */ + Collections.sort(nums); // 排序後,串列元素從小到大排列 + ``` + +=== "C#" + + ```csharp title="list.cs" + /* 排序串列 */ + nums.Sort(); // 排序後,串列元素從小到大排列 + ``` + +=== "Go" + + ```go title="list_test.go" + /* 排序串列 */ + sort.Ints(nums) // 排序後,串列元素從小到大排列 + ``` + +=== "Swift" + + ```swift title="list.swift" + /* 排序串列 */ + nums.sort() // 排序後,串列元素從小到大排列 + ``` + +=== "JS" + + ```javascript title="list.js" + /* 排序串列 */ + nums.sort((a, b) => a - b); // 排序後,串列元素從小到大排列 + ``` + +=== "TS" + + ```typescript title="list.ts" + /* 排序串列 */ + nums.sort((a, b) => a - b); // 排序後,串列元素從小到大排列 + ``` + +=== "Dart" + + ```dart title="list.dart" + /* 排序串列 */ + nums.sort(); // 排序後,串列元素從小到大排列 + ``` + +=== "Rust" + + ```rust title="list.rs" + /* 排序串列 */ + nums.sort(); // 排序後,串列元素從小到大排列 + ``` + +=== "C" + + ```c title="list.c" + // C 未提供內建動態陣列 + ``` + +=== "Kotlin" + + ```kotlin title="list.kt" + /* 排序串列 */ + nums.sort() // 排序後,串列元素從小到大排列 + ``` + +=== "Ruby" + + ```ruby title="list.rb" + # 排序串列 + nums = nums.sort { |a, b| a <=> b } # 排序後,串列元素從小到大排列 + ``` + +=== "Zig" + + ```zig title="list.zig" + // 排序串列 + std.sort.sort(i32, nums.items, {}, comptime std.sort.asc(i32)); + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 4.3.2   串列實現 + +許多程式語言內建了串列,例如 Java、C++、Python 等。它們的實現比較複雜,各個參數的設定也非常考究,例如初始容量、擴容倍數等。感興趣的讀者可以查閱原始碼進行學習。 + +為了加深對串列工作原理的理解,我們嘗試實現一個簡易版串列,包括以下三個重點設計。 + +- **初始容量**:選取一個合理的陣列初始容量。在本示例中,我們選擇 10 作為初始容量。 +- **數量記錄**:宣告一個變數 `size` ,用於記錄串列當前元素數量,並隨著元素插入和刪除實時更新。根據此變數,我們可以定位串列尾部,以及判斷是否需要擴容。 +- **擴容機制**:若插入元素時串列容量已滿,則需要進行擴容。先根據擴容倍數建立一個更大的陣列,再將當前陣列的所有元素依次移動至新陣列。在本示例中,我們規定每次將陣列擴容至之前的 2 倍。 + +=== "Python" + + ```python title="my_list.py" + class MyList: + """串列類別""" + + def __init__(self): + """建構子""" + self._capacity: int = 10 # 串列容量 + self._arr: list[int] = [0] * self._capacity # 陣列(儲存串列元素) + self._size: int = 0 # 串列長度(當前元素數量) + self._extend_ratio: int = 2 # 每次串列擴容的倍數 + + def size(self) -> int: + """獲取串列長度(當前元素數量)""" + return self._size + + def capacity(self) -> int: + """獲取串列容量""" + return self._capacity + + def get(self, index: int) -> int: + """訪問元素""" + # 索引如果越界,則丟擲異常,下同 + if index < 0 or index >= self._size: + raise IndexError("索引越界") + return self._arr[index] + + def set(self, num: int, index: int): + """更新元素""" + if index < 0 or index >= self._size: + raise IndexError("索引越界") + self._arr[index] = num + + def add(self, num: int): + """在尾部新增元素""" + # 元素數量超出容量時,觸發擴容機制 + if self.size() == self.capacity(): + self.extend_capacity() + self._arr[self._size] = num + self._size += 1 + + def insert(self, num: int, index: int): + """在中間插入元素""" + if index < 0 or index >= self._size: + raise IndexError("索引越界") + # 元素數量超出容量時,觸發擴容機制 + if self._size == self.capacity(): + self.extend_capacity() + # 將索引 index 以及之後的元素都向後移動一位 + for j in range(self._size - 1, index - 1, -1): + self._arr[j + 1] = self._arr[j] + self._arr[index] = num + # 更新元素數量 + self._size += 1 + + def remove(self, index: int) -> int: + """刪除元素""" + if index < 0 or index >= self._size: + raise IndexError("索引越界") + num = self._arr[index] + # 將索引 index 之後的元素都向前移動一位 + for j in range(index, self._size - 1): + self._arr[j] = self._arr[j + 1] + # 更新元素數量 + self._size -= 1 + # 返回被刪除的元素 + return num + + def extend_capacity(self): + """串列擴容""" + # 新建一個長度為原陣列 _extend_ratio 倍的新陣列,並將原陣列複製到新陣列 + self._arr = self._arr + [0] * self.capacity() * (self._extend_ratio - 1) + # 更新串列容量 + self._capacity = len(self._arr) + + def to_array(self) -> list[int]: + """返回有效長度的串列""" + return self._arr[: self._size] + ``` + +=== "C++" + + ```cpp title="my_list.cpp" + /* 串列類別 */ + class MyList { + private: + int *arr; // 陣列(儲存串列元素) + int arrCapacity = 10; // 串列容量 + int arrSize = 0; // 串列長度(當前元素數量) + int extendRatio = 2; // 每次串列擴容的倍數 + + public: + /* 建構子 */ + MyList() { + arr = new int[arrCapacity]; + } + + /* 析構方法 */ + ~MyList() { + delete[] arr; + } + + /* 獲取串列長度(當前元素數量)*/ + int size() { + return arrSize; + } + + /* 獲取串列容量 */ + int capacity() { + return arrCapacity; + } + + /* 訪問元素 */ + int get(int index) { + // 索引如果越界,則丟擲異常,下同 + if (index < 0 || index >= size()) + throw out_of_range("索引越界"); + return arr[index]; + } + + /* 更新元素 */ + void set(int index, int num) { + if (index < 0 || index >= size()) + throw out_of_range("索引越界"); + arr[index] = num; + } + + /* 在尾部新增元素 */ + void add(int num) { + // 元素數量超出容量時,觸發擴容機制 + if (size() == capacity()) + extendCapacity(); + arr[size()] = num; + // 更新元素數量 + arrSize++; + } + + /* 在中間插入元素 */ + void insert(int index, int num) { + if (index < 0 || index >= size()) + throw out_of_range("索引越界"); + // 元素數量超出容量時,觸發擴容機制 + if (size() == capacity()) + extendCapacity(); + // 將索引 index 以及之後的元素都向後移動一位 + for (int j = size() - 1; j >= index; j--) { + arr[j + 1] = arr[j]; + } + arr[index] = num; + // 更新元素數量 + arrSize++; + } + + /* 刪除元素 */ + int remove(int index) { + if (index < 0 || index >= size()) + throw out_of_range("索引越界"); + int num = arr[index]; + // 將索引 index 之後的元素都向前移動一位 + for (int j = index; j < size() - 1; j++) { + arr[j] = arr[j + 1]; + } + // 更新元素數量 + arrSize--; + // 返回被刪除的元素 + return num; + } + + /* 串列擴容 */ + void extendCapacity() { + // 新建一個長度為原陣列 extendRatio 倍的新陣列 + int newCapacity = capacity() * extendRatio; + int *tmp = arr; + arr = new int[newCapacity]; + // 將原陣列中的所有元素複製到新陣列 + for (int i = 0; i < size(); i++) { + arr[i] = tmp[i]; + } + // 釋放記憶體 + delete[] tmp; + arrCapacity = newCapacity; + } + + /* 將串列轉換為 Vector 用於列印 */ + vector toVector() { + // 僅轉換有效長度範圍內的串列元素 + vector vec(size()); + for (int i = 0; i < size(); i++) { + vec[i] = arr[i]; + } + return vec; + } + }; + ``` + +=== "Java" + + ```java title="my_list.java" + /* 串列類別 */ + class MyList { + private int[] arr; // 陣列(儲存串列元素) + private int capacity = 10; // 串列容量 + private int size = 0; // 串列長度(當前元素數量) + private int extendRatio = 2; // 每次串列擴容的倍數 + + /* 建構子 */ + public MyList() { + arr = new int[capacity]; + } + + /* 獲取串列長度(當前元素數量) */ + public int size() { + return size; + } + + /* 獲取串列容量 */ + public int capacity() { + return capacity; + } + + /* 訪問元素 */ + public int get(int index) { + // 索引如果越界,則丟擲異常,下同 + if (index < 0 || index >= size) + throw new IndexOutOfBoundsException("索引越界"); + return arr[index]; + } + + /* 更新元素 */ + public void set(int index, int num) { + if (index < 0 || index >= size) + throw new IndexOutOfBoundsException("索引越界"); + arr[index] = num; + } + + /* 在尾部新增元素 */ + public void add(int num) { + // 元素數量超出容量時,觸發擴容機制 + if (size == capacity()) + extendCapacity(); + arr[size] = num; + // 更新元素數量 + size++; + } + + /* 在中間插入元素 */ + public void insert(int index, int num) { + if (index < 0 || index >= size) + throw new IndexOutOfBoundsException("索引越界"); + // 元素數量超出容量時,觸發擴容機制 + if (size == capacity()) + extendCapacity(); + // 將索引 index 以及之後的元素都向後移動一位 + for (int j = size - 1; j >= index; j--) { + arr[j + 1] = arr[j]; + } + arr[index] = num; + // 更新元素數量 + size++; + } + + /* 刪除元素 */ + public int remove(int index) { + if (index < 0 || index >= size) + throw new IndexOutOfBoundsException("索引越界"); + int num = arr[index]; + // 將將索引 index 之後的元素都向前移動一位 + for (int j = index; j < size - 1; j++) { + arr[j] = arr[j + 1]; + } + // 更新元素數量 + size--; + // 返回被刪除的元素 + return num; + } + + /* 串列擴容 */ + public void extendCapacity() { + // 新建一個長度為原陣列 extendRatio 倍的新陣列,並將原陣列複製到新陣列 + arr = Arrays.copyOf(arr, capacity() * extendRatio); + // 更新串列容量 + capacity = arr.length; + } + + /* 將串列轉換為陣列 */ + public int[] toArray() { + int size = size(); + // 僅轉換有效長度範圍內的串列元素 + int[] arr = new int[size]; + for (int i = 0; i < size; i++) { + arr[i] = get(i); + } + return arr; + } + } + ``` + +=== "C#" + + ```csharp title="my_list.cs" + /* 串列類別 */ + class MyList { + private int[] arr; // 陣列(儲存串列元素) + private int arrCapacity = 10; // 串列容量 + private int arrSize = 0; // 串列長度(當前元素數量) + private readonly int extendRatio = 2; // 每次串列擴容的倍數 + + /* 建構子 */ + public MyList() { + arr = new int[arrCapacity]; + } + + /* 獲取串列長度(當前元素數量)*/ + public int Size() { + return arrSize; + } + + /* 獲取串列容量 */ + public int Capacity() { + return arrCapacity; + } + + /* 訪問元素 */ + public int Get(int index) { + // 索引如果越界,則丟擲異常,下同 + if (index < 0 || index >= arrSize) + throw new IndexOutOfRangeException("索引越界"); + return arr[index]; + } + + /* 更新元素 */ + public void Set(int index, int num) { + if (index < 0 || index >= arrSize) + throw new IndexOutOfRangeException("索引越界"); + arr[index] = num; + } + + /* 在尾部新增元素 */ + public void Add(int num) { + // 元素數量超出容量時,觸發擴容機制 + if (arrSize == arrCapacity) + ExtendCapacity(); + arr[arrSize] = num; + // 更新元素數量 + arrSize++; + } + + /* 在中間插入元素 */ + public void Insert(int index, int num) { + if (index < 0 || index >= arrSize) + throw new IndexOutOfRangeException("索引越界"); + // 元素數量超出容量時,觸發擴容機制 + if (arrSize == arrCapacity) + ExtendCapacity(); + // 將索引 index 以及之後的元素都向後移動一位 + for (int j = arrSize - 1; j >= index; j--) { + arr[j + 1] = arr[j]; + } + arr[index] = num; + // 更新元素數量 + arrSize++; + } + + /* 刪除元素 */ + public int Remove(int index) { + if (index < 0 || index >= arrSize) + throw new IndexOutOfRangeException("索引越界"); + int num = arr[index]; + // 將將索引 index 之後的元素都向前移動一位 + for (int j = index; j < arrSize - 1; j++) { + arr[j] = arr[j + 1]; + } + // 更新元素數量 + arrSize--; + // 返回被刪除的元素 + return num; + } + + /* 串列擴容 */ + public void ExtendCapacity() { + // 新建一個長度為 arrCapacity * extendRatio 的陣列,並將原陣列複製到新陣列 + Array.Resize(ref arr, arrCapacity * extendRatio); + // 更新串列容量 + arrCapacity = arr.Length; + } + + /* 將串列轉換為陣列 */ + public int[] ToArray() { + // 僅轉換有效長度範圍內的串列元素 + int[] arr = new int[arrSize]; + for (int i = 0; i < arrSize; i++) { + arr[i] = Get(i); + } + return arr; + } + } + ``` + +=== "Go" + + ```go title="my_list.go" + /* 串列類別 */ + type myList struct { + arrCapacity int + arr []int + arrSize int + extendRatio int + } + + /* 建構子 */ + func newMyList() *myList { + return &myList{ + arrCapacity: 10, // 串列容量 + arr: make([]int, 10), // 陣列(儲存串列元素) + arrSize: 0, // 串列長度(當前元素數量) + extendRatio: 2, // 每次串列擴容的倍數 + } + } + + /* 獲取串列長度(當前元素數量) */ + func (l *myList) size() int { + return l.arrSize + } + + /* 獲取串列容量 */ + func (l *myList) capacity() int { + return l.arrCapacity + } + + /* 訪問元素 */ + func (l *myList) get(index int) int { + // 索引如果越界,則丟擲異常,下同 + if index < 0 || index >= l.arrSize { + panic("索引越界") + } + return l.arr[index] + } + + /* 更新元素 */ + func (l *myList) set(num, index int) { + if index < 0 || index >= l.arrSize { + panic("索引越界") + } + l.arr[index] = num + } + + /* 在尾部新增元素 */ + func (l *myList) add(num int) { + // 元素數量超出容量時,觸發擴容機制 + if l.arrSize == l.arrCapacity { + l.extendCapacity() + } + l.arr[l.arrSize] = num + // 更新元素數量 + l.arrSize++ + } + + /* 在中間插入元素 */ + func (l *myList) insert(num, index int) { + if index < 0 || index >= l.arrSize { + panic("索引越界") + } + // 元素數量超出容量時,觸發擴容機制 + if l.arrSize == l.arrCapacity { + l.extendCapacity() + } + // 將索引 index 以及之後的元素都向後移動一位 + for j := l.arrSize - 1; j >= index; j-- { + l.arr[j+1] = l.arr[j] + } + l.arr[index] = num + // 更新元素數量 + l.arrSize++ + } + + /* 刪除元素 */ + func (l *myList) remove(index int) int { + if index < 0 || index >= l.arrSize { + panic("索引越界") + } + num := l.arr[index] + // 將索引 index 之後的元素都向前移動一位 + for j := index; j < l.arrSize-1; j++ { + l.arr[j] = l.arr[j+1] + } + // 更新元素數量 + l.arrSize-- + // 返回被刪除的元素 + return num + } + + /* 串列擴容 */ + func (l *myList) extendCapacity() { + // 新建一個長度為原陣列 extendRatio 倍的新陣列,並將原陣列複製到新陣列 + l.arr = append(l.arr, make([]int, l.arrCapacity*(l.extendRatio-1))...) + // 更新串列容量 + l.arrCapacity = len(l.arr) + } + + /* 返回有效長度的串列 */ + func (l *myList) toArray() []int { + // 僅轉換有效長度範圍內的串列元素 + return l.arr[:l.arrSize] + } + ``` + +=== "Swift" + + ```swift title="my_list.swift" + /* 串列類別 */ + class MyList { + private var arr: [Int] // 陣列(儲存串列元素) + private var _capacity: Int // 串列容量 + private var _size: Int // 串列長度(當前元素數量) + private let extendRatio: Int // 每次串列擴容的倍數 + + /* 建構子 */ + init() { + _capacity = 10 + _size = 0 + extendRatio = 2 + arr = Array(repeating: 0, count: _capacity) + } + + /* 獲取串列長度(當前元素數量)*/ + func size() -> Int { + _size + } + + /* 獲取串列容量 */ + func capacity() -> Int { + _capacity + } + + /* 訪問元素 */ + func get(index: Int) -> Int { + // 索引如果越界則丟擲錯誤,下同 + if index < 0 || index >= size() { + fatalError("索引越界") + } + return arr[index] + } + + /* 更新元素 */ + func set(index: Int, num: Int) { + if index < 0 || index >= size() { + fatalError("索引越界") + } + arr[index] = num + } + + /* 在尾部新增元素 */ + func add(num: Int) { + // 元素數量超出容量時,觸發擴容機制 + if size() == capacity() { + extendCapacity() + } + arr[size()] = num + // 更新元素數量 + _size += 1 + } + + /* 在中間插入元素 */ + func insert(index: Int, num: Int) { + if index < 0 || index >= size() { + fatalError("索引越界") + } + // 元素數量超出容量時,觸發擴容機制 + if size() == capacity() { + extendCapacity() + } + // 將索引 index 以及之後的元素都向後移動一位 + for j in (index ..< size()).reversed() { + arr[j + 1] = arr[j] + } + arr[index] = num + // 更新元素數量 + _size += 1 + } + + /* 刪除元素 */ + @discardableResult + func remove(index: Int) -> Int { + if index < 0 || index >= size() { + fatalError("索引越界") + } + let num = arr[index] + // 將將索引 index 之後的元素都向前移動一位 + for j in index ..< (size() - 1) { + arr[j] = arr[j + 1] + } + // 更新元素數量 + _size -= 1 + // 返回被刪除的元素 + return num + } + + /* 串列擴容 */ + func extendCapacity() { + // 新建一個長度為原陣列 extendRatio 倍的新陣列,並將原陣列複製到新陣列 + arr = arr + Array(repeating: 0, count: capacity() * (extendRatio - 1)) + // 更新串列容量 + _capacity = arr.count + } + + /* 將串列轉換為陣列 */ + func toArray() -> [Int] { + Array(arr.prefix(size())) + } + } + ``` + +=== "JS" + + ```javascript title="my_list.js" + /* 串列類別 */ + class MyList { + #arr = new Array(); // 陣列(儲存串列元素) + #capacity = 10; // 串列容量 + #size = 0; // 串列長度(當前元素數量) + #extendRatio = 2; // 每次串列擴容的倍數 + + /* 建構子 */ + constructor() { + this.#arr = new Array(this.#capacity); + } + + /* 獲取串列長度(當前元素數量)*/ + size() { + return this.#size; + } + + /* 獲取串列容量 */ + capacity() { + return this.#capacity; + } + + /* 訪問元素 */ + get(index) { + // 索引如果越界,則丟擲異常,下同 + if (index < 0 || index >= this.#size) throw new Error('索引越界'); + return this.#arr[index]; + } + + /* 更新元素 */ + set(index, num) { + if (index < 0 || index >= this.#size) throw new Error('索引越界'); + this.#arr[index] = num; + } + + /* 在尾部新增元素 */ + add(num) { + // 如果長度等於容量,則需要擴容 + if (this.#size === this.#capacity) { + this.extendCapacity(); + } + // 將新元素新增到串列尾部 + this.#arr[this.#size] = num; + this.#size++; + } + + /* 在中間插入元素 */ + insert(index, num) { + if (index < 0 || index >= this.#size) throw new Error('索引越界'); + // 元素數量超出容量時,觸發擴容機制 + if (this.#size === this.#capacity) { + this.extendCapacity(); + } + // 將索引 index 以及之後的元素都向後移動一位 + for (let j = this.#size - 1; j >= index; j--) { + this.#arr[j + 1] = this.#arr[j]; + } + // 更新元素數量 + this.#arr[index] = num; + this.#size++; + } + + /* 刪除元素 */ + remove(index) { + if (index < 0 || index >= this.#size) throw new Error('索引越界'); + let num = this.#arr[index]; + // 將將索引 index 之後的元素都向前移動一位 + for (let j = index; j < this.#size - 1; j++) { + this.#arr[j] = this.#arr[j + 1]; + } + // 更新元素數量 + this.#size--; + // 返回被刪除的元素 + return num; + } + + /* 串列擴容 */ + extendCapacity() { + // 新建一個長度為原陣列 extendRatio 倍的新陣列,並將原陣列複製到新陣列 + this.#arr = this.#arr.concat( + new Array(this.capacity() * (this.#extendRatio - 1)) + ); + // 更新串列容量 + this.#capacity = this.#arr.length; + } + + /* 將串列轉換為陣列 */ + toArray() { + let size = this.size(); + // 僅轉換有效長度範圍內的串列元素 + const arr = new Array(size); + for (let i = 0; i < size; i++) { + arr[i] = this.get(i); + } + return arr; + } + } + ``` + +=== "TS" + + ```typescript title="my_list.ts" + /* 串列類別 */ + class MyList { + private arr: Array; // 陣列(儲存串列元素) + private _capacity: number = 10; // 串列容量 + private _size: number = 0; // 串列長度(當前元素數量) + private extendRatio: number = 2; // 每次串列擴容的倍數 + + /* 建構子 */ + constructor() { + this.arr = new Array(this._capacity); + } + + /* 獲取串列長度(當前元素數量)*/ + public size(): number { + return this._size; + } + + /* 獲取串列容量 */ + public capacity(): number { + return this._capacity; + } + + /* 訪問元素 */ + public get(index: number): number { + // 索引如果越界,則丟擲異常,下同 + if (index < 0 || index >= this._size) throw new Error('索引越界'); + return this.arr[index]; + } + + /* 更新元素 */ + public set(index: number, num: number): void { + if (index < 0 || index >= this._size) throw new Error('索引越界'); + this.arr[index] = num; + } + + /* 在尾部新增元素 */ + public add(num: number): void { + // 如果長度等於容量,則需要擴容 + if (this._size === this._capacity) this.extendCapacity(); + // 將新元素新增到串列尾部 + this.arr[this._size] = num; + this._size++; + } + + /* 在中間插入元素 */ + public insert(index: number, num: number): void { + if (index < 0 || index >= this._size) throw new Error('索引越界'); + // 元素數量超出容量時,觸發擴容機制 + if (this._size === this._capacity) { + this.extendCapacity(); + } + // 將索引 index 以及之後的元素都向後移動一位 + for (let j = this._size - 1; j >= index; j--) { + this.arr[j + 1] = this.arr[j]; + } + // 更新元素數量 + this.arr[index] = num; + this._size++; + } + + /* 刪除元素 */ + public remove(index: number): number { + if (index < 0 || index >= this._size) throw new Error('索引越界'); + let num = this.arr[index]; + // 將將索引 index 之後的元素都向前移動一位 + for (let j = index; j < this._size - 1; j++) { + this.arr[j] = this.arr[j + 1]; + } + // 更新元素數量 + this._size--; + // 返回被刪除的元素 + return num; + } + + /* 串列擴容 */ + public extendCapacity(): void { + // 新建一個長度為 size 的陣列,並將原陣列複製到新陣列 + this.arr = this.arr.concat( + new Array(this.capacity() * (this.extendRatio - 1)) + ); + // 更新串列容量 + this._capacity = this.arr.length; + } + + /* 將串列轉換為陣列 */ + public toArray(): number[] { + let size = this.size(); + // 僅轉換有效長度範圍內的串列元素 + const arr = new Array(size); + for (let i = 0; i < size; i++) { + arr[i] = this.get(i); + } + return arr; + } + } + ``` + +=== "Dart" + + ```dart title="my_list.dart" + /* 串列類別 */ + class MyList { + late List _arr; // 陣列(儲存串列元素) + int _capacity = 10; // 串列容量 + int _size = 0; // 串列長度(當前元素數量) + int _extendRatio = 2; // 每次串列擴容的倍數 + + /* 建構子 */ + MyList() { + _arr = List.filled(_capacity, 0); + } + + /* 獲取串列長度(當前元素數量)*/ + int size() => _size; + + /* 獲取串列容量 */ + int capacity() => _capacity; + + /* 訪問元素 */ + int get(int index) { + if (index >= _size) throw RangeError('索引越界'); + return _arr[index]; + } + + /* 更新元素 */ + void set(int index, int _num) { + if (index >= _size) throw RangeError('索引越界'); + _arr[index] = _num; + } + + /* 在尾部新增元素 */ + void add(int _num) { + // 元素數量超出容量時,觸發擴容機制 + if (_size == _capacity) extendCapacity(); + _arr[_size] = _num; + // 更新元素數量 + _size++; + } + + /* 在中間插入元素 */ + void insert(int index, int _num) { + if (index >= _size) throw RangeError('索引越界'); + // 元素數量超出容量時,觸發擴容機制 + if (_size == _capacity) extendCapacity(); + // 將索引 index 以及之後的元素都向後移動一位 + for (var j = _size - 1; j >= index; j--) { + _arr[j + 1] = _arr[j]; + } + _arr[index] = _num; + // 更新元素數量 + _size++; + } + + /* 刪除元素 */ + int remove(int index) { + if (index >= _size) throw RangeError('索引越界'); + int _num = _arr[index]; + // 將將索引 index 之後的元素都向前移動一位 + for (var j = index; j < _size - 1; j++) { + _arr[j] = _arr[j + 1]; + } + // 更新元素數量 + _size--; + // 返回被刪除的元素 + return _num; + } + + /* 串列擴容 */ + void extendCapacity() { + // 新建一個長度為原陣列 _extendRatio 倍的新陣列 + final _newNums = List.filled(_capacity * _extendRatio, 0); + // 將原陣列複製到新陣列 + List.copyRange(_newNums, 0, _arr); + // 更新 _arr 的引用 + _arr = _newNums; + // 更新串列容量 + _capacity = _arr.length; + } + + /* 將串列轉換為陣列 */ + List toArray() { + List arr = []; + for (var i = 0; i < _size; i++) { + arr.add(get(i)); + } + return arr; + } + } + ``` + +=== "Rust" + + ```rust title="my_list.rs" + /* 串列類別 */ + #[allow(dead_code)] + struct MyList { + arr: Vec, // 陣列(儲存串列元素) + capacity: usize, // 串列容量 + size: usize, // 串列長度(當前元素數量) + extend_ratio: usize, // 每次串列擴容的倍數 + } + + #[allow(unused, unused_comparisons)] + impl MyList { + /* 建構子 */ + pub fn new(capacity: usize) -> Self { + let mut vec = Vec::new(); + vec.resize(capacity, 0); + Self { + arr: vec, + capacity, + size: 0, + extend_ratio: 2, + } + } + + /* 獲取串列長度(當前元素數量)*/ + pub fn size(&self) -> usize { + return self.size; + } + + /* 獲取串列容量 */ + pub fn capacity(&self) -> usize { + return self.capacity; + } + + /* 訪問元素 */ + pub fn get(&self, index: usize) -> i32 { + // 索引如果越界,則丟擲異常,下同 + if index >= self.size { + panic!("索引越界") + }; + return self.arr[index]; + } + + /* 更新元素 */ + pub fn set(&mut self, index: usize, num: i32) { + if index >= self.size { + panic!("索引越界") + }; + self.arr[index] = num; + } + + /* 在尾部新增元素 */ + pub fn add(&mut self, num: i32) { + // 元素數量超出容量時,觸發擴容機制 + if self.size == self.capacity() { + self.extend_capacity(); + } + self.arr[self.size] = num; + // 更新元素數量 + self.size += 1; + } + + /* 在中間插入元素 */ + pub fn insert(&mut self, index: usize, num: i32) { + if index >= self.size() { + panic!("索引越界") + }; + // 元素數量超出容量時,觸發擴容機制 + if self.size == self.capacity() { + self.extend_capacity(); + } + // 將索引 index 以及之後的元素都向後移動一位 + for j in (index..self.size).rev() { + self.arr[j + 1] = self.arr[j]; + } + self.arr[index] = num; + // 更新元素數量 + self.size += 1; + } + + /* 刪除元素 */ + pub fn remove(&mut self, index: usize) -> i32 { + if index >= self.size() { + panic!("索引越界") + }; + let num = self.arr[index]; + // 將將索引 index 之後的元素都向前移動一位 + for j in (index..self.size - 1) { + self.arr[j] = self.arr[j + 1]; + } + // 更新元素數量 + self.size -= 1; + // 返回被刪除的元素 + return num; + } + + /* 串列擴容 */ + pub fn extend_capacity(&mut self) { + // 新建一個長度為原陣列 extend_ratio 倍的新陣列,並將原陣列複製到新陣列 + let new_capacity = self.capacity * self.extend_ratio; + self.arr.resize(new_capacity, 0); + // 更新串列容量 + self.capacity = new_capacity; + } + + /* 將串列轉換為陣列 */ + pub fn to_array(&mut self) -> Vec { + // 僅轉換有效長度範圍內的串列元素 + let mut arr = Vec::new(); + for i in 0..self.size { + arr.push(self.get(i)); + } + arr + } + } + ``` + +=== "C" + + ```c title="my_list.c" + /* 串列類別 */ + typedef struct { + int *arr; // 陣列(儲存串列元素) + int capacity; // 串列容量 + int size; // 串列大小 + int extendRatio; // 串列每次擴容的倍數 + } MyList; + + /* 建構子 */ + MyList *newMyList() { + MyList *nums = malloc(sizeof(MyList)); + nums->capacity = 10; + nums->arr = malloc(sizeof(int) * nums->capacity); + nums->size = 0; + nums->extendRatio = 2; + return nums; + } + + /* 析構函式 */ + void delMyList(MyList *nums) { + free(nums->arr); + free(nums); + } + + /* 獲取串列長度 */ + int size(MyList *nums) { + return nums->size; + } + + /* 獲取串列容量 */ + int capacity(MyList *nums) { + return nums->capacity; + } + + /* 訪問元素 */ + int get(MyList *nums, int index) { + assert(index >= 0 && index < nums->size); + return nums->arr[index]; + } + + /* 更新元素 */ + void set(MyList *nums, int index, int num) { + assert(index >= 0 && index < nums->size); + nums->arr[index] = num; + } + + /* 在尾部新增元素 */ + void add(MyList *nums, int num) { + if (size(nums) == capacity(nums)) { + extendCapacity(nums); // 擴容 + } + nums->arr[size(nums)] = num; + nums->size++; + } + + /* 在中間插入元素 */ + void insert(MyList *nums, int index, int num) { + assert(index >= 0 && index < size(nums)); + // 元素數量超出容量時,觸發擴容機制 + if (size(nums) == capacity(nums)) { + extendCapacity(nums); // 擴容 + } + for (int i = size(nums); i > index; --i) { + nums->arr[i] = nums->arr[i - 1]; + } + nums->arr[index] = num; + nums->size++; + } + + /* 刪除元素 */ + // 注意:stdio.h 佔用了 remove 關鍵詞 + int removeItem(MyList *nums, int index) { + assert(index >= 0 && index < size(nums)); + int num = nums->arr[index]; + for (int i = index; i < size(nums) - 1; i++) { + nums->arr[i] = nums->arr[i + 1]; + } + nums->size--; + return num; + } + + /* 串列擴容 */ + void extendCapacity(MyList *nums) { + // 先分配空間 + int newCapacity = capacity(nums) * nums->extendRatio; + int *extend = (int *)malloc(sizeof(int) * newCapacity); + int *temp = nums->arr; + + // 複製舊資料到新資料 + for (int i = 0; i < size(nums); i++) + extend[i] = nums->arr[i]; + + // 釋放舊資料 + free(temp); + + // 更新新資料 + nums->arr = extend; + nums->capacity = newCapacity; + } + + /* 將串列轉換為 Array 用於列印 */ + int *toArray(MyList *nums) { + return nums->arr; + } + ``` + +=== "Kotlin" + + ```kotlin title="my_list.kt" + /* 串列類別 */ + class MyList { + private var arr: IntArray = intArrayOf() // 陣列(儲存串列元素) + private var capacity = 10 // 串列容量 + private var size = 0 // 串列長度(當前元素數量) + private var extendRatio = 2 // 每次串列擴容的倍數 + + /* 建構子 */ + init { + arr = IntArray(capacity) + } + + /* 獲取串列長度(當前元素數量) */ + fun size(): Int { + return size + } + + /* 獲取串列容量 */ + fun capacity(): Int { + return capacity + } + + /* 訪問元素 */ + fun get(index: Int): Int { + // 索引如果越界,則丟擲異常,下同 + if (index < 0 || index >= size) + throw IndexOutOfBoundsException() + return arr[index] + } + + /* 更新元素 */ + fun set(index: Int, num: Int) { + if (index < 0 || index >= size) + throw IndexOutOfBoundsException("索引越界") + arr[index] = num + } + + /* 在尾部新增元素 */ + fun add(num: Int) { + // 元素數量超出容量時,觸發擴容機制 + if (size == capacity()) + extendCapacity() + arr[size] = num + // 更新元素數量 + size++ + } + + /* 在中間插入元素 */ + fun insert(index: Int, num: Int) { + if (index < 0 || index >= size) + throw IndexOutOfBoundsException("索引越界") + // 元素數量超出容量時,觸發擴容機制 + if (size == capacity()) + extendCapacity() + // 將索引 index 以及之後的元素都向後移動一位 + for (j in size - 1 downTo index) + arr[j + 1] = arr[j] + arr[index] = num + // 更新元素數量 + size++ + } + + /* 刪除元素 */ + fun remove(index: Int): Int { + if (index < 0 || index >= size) + throw IndexOutOfBoundsException("索引越界") + val num: Int = arr[index] + // 將將索引 index 之後的元素都向前移動一位 + for (j in index..= size + @arr[index] + end + + ### 訪問元素 ### + def set(index, num) + raise IndexError, "索引越界" if index < 0 || index >= size + @arr[index] = num + end + + ### 在尾部新增元素 ### + def add(num) + # 元素數量超出容量時,觸發擴容機制 + extend_capacity if size == capacity + @arr[size] = num + + # 更新元素數量 + @size += 1 + end + + ### 在中間插入元素 ### + def insert(index, num) + raise IndexError, "索引越界" if index < 0 || index >= size + + # 元素數量超出容量時,觸發擴容機制 + extend_capacity if size == capacity + + # 將索引 index 以及之後的元素都向後移動一位 + for j in (size - 1).downto(index) + @arr[j + 1] = @arr[j] + end + @arr[index] = num + + # 更新元素數量 + @size += 1 + end + + ### 刪除元素 ### + def remove(index) + raise IndexError, "索引越界" if index < 0 || index >= size + num = @arr[index] + + # 將將索引 index 之後的元素都向前移動一位 + for j in index...size + @arr[j] = @arr[j + 1] + end + + # 更新元素數量 + @size -= 1 + + # 返回被刪除的元素 + num + end + + ### 串列擴容 ### + def extend_capacity + # 新建一個長度為原陣列 extend_ratio 倍的新陣列,並將原陣列複製到新陣列 + arr = @arr.dup + Array.new(capacity * (@extend_ratio - 1)) + # 更新串列容量 + @capacity = arr.length + end + + ### 將串列轉換為陣列 ### + def to_array + sz = size + # 僅轉換有效長度範圍內的串列元素 + arr = Array.new(sz) + for i in 0...sz + arr[i] = get(i) + end + arr + end + end + ``` + +=== "Zig" + + ```zig title="my_list.zig" + // 串列類別 + fn MyList(comptime T: type) type { + return struct { + const Self = @This(); + + arr: []T = undefined, // 陣列(儲存串列元素) + arrCapacity: usize = 10, // 串列容量 + numSize: usize = 0, // 串列長度(當前元素數量) + extendRatio: usize = 2, // 每次串列擴容的倍數 + mem_arena: ?std.heap.ArenaAllocator = null, + mem_allocator: std.mem.Allocator = undefined, // 記憶體分配器 + + // 建構子(分配記憶體+初始化串列) + pub fn init(self: *Self, allocator: std.mem.Allocator) !void { + if (self.mem_arena == null) { + self.mem_arena = std.heap.ArenaAllocator.init(allocator); + self.mem_allocator = self.mem_arena.?.allocator(); + } + self.arr = try self.mem_allocator.alloc(T, self.arrCapacity); + @memset(self.arr, @as(T, 0)); + } + + // 析構函式(釋放記憶體) + pub fn deinit(self: *Self) void { + if (self.mem_arena == null) return; + self.mem_arena.?.deinit(); + } + + // 獲取串列長度(當前元素數量) + pub fn size(self: *Self) usize { + return self.numSize; + } + + // 獲取串列容量 + pub fn capacity(self: *Self) usize { + return self.arrCapacity; + } + + // 訪問元素 + pub fn get(self: *Self, index: usize) T { + // 索引如果越界,則丟擲異常,下同 + if (index < 0 or index >= self.size()) @panic("索引越界"); + return self.arr[index]; + } + + // 更新元素 + pub fn set(self: *Self, index: usize, num: T) void { + // 索引如果越界,則丟擲異常,下同 + if (index < 0 or index >= self.size()) @panic("索引越界"); + self.arr[index] = num; + } + + // 在尾部新增元素 + pub fn add(self: *Self, num: T) !void { + // 元素數量超出容量時,觸發擴容機制 + if (self.size() == self.capacity()) try self.extendCapacity(); + self.arr[self.size()] = num; + // 更新元素數量 + self.numSize += 1; + } + + // 在中間插入元素 + pub fn insert(self: *Self, index: usize, num: T) !void { + if (index < 0 or index >= self.size()) @panic("索引越界"); + // 元素數量超出容量時,觸發擴容機制 + if (self.size() == self.capacity()) try self.extendCapacity(); + // 將索引 index 以及之後的元素都向後移動一位 + var j = self.size() - 1; + while (j >= index) : (j -= 1) { + self.arr[j + 1] = self.arr[j]; + } + self.arr[index] = num; + // 更新元素數量 + self.numSize += 1; + } + + // 刪除元素 + pub fn remove(self: *Self, index: usize) T { + if (index < 0 or index >= self.size()) @panic("索引越界"); + var num = self.arr[index]; + // 將索引 index 之後的元素都向前移動一位 + var j = index; + while (j < self.size() - 1) : (j += 1) { + self.arr[j] = self.arr[j + 1]; + } + // 更新元素數量 + self.numSize -= 1; + // 返回被刪除的元素 + return num; + } + + // 串列擴容 + pub fn extendCapacity(self: *Self) !void { + // 新建一個長度為 size * extendRatio 的陣列,並將原陣列複製到新陣列 + var newCapacity = self.capacity() * self.extendRatio; + var extend = try self.mem_allocator.alloc(T, newCapacity); + @memset(extend, @as(T, 0)); + // 將原陣列中的所有元素複製到新陣列 + std.mem.copy(T, extend, self.arr); + self.arr = extend; + // 更新串列容量 + self.arrCapacity = newCapacity; + } + + // 將串列轉換為陣列 + pub fn toArray(self: *Self) ![]T { + // 僅轉換有效長度範圍內的串列元素 + var arr = try self.mem_allocator.alloc(T, self.size()); + @memset(arr, @as(T, 0)); + for (arr, 0..) |*num, i| { + num.* = self.get(i); + } + return arr; + } + }; + } + ``` + +??? pythontutor "視覺化執行" + +
+ diff --git a/zh-Hant/docs/chapter_array_and_linkedlist/ram_and_cache.md b/zh-Hant/docs/chapter_array_and_linkedlist/ram_and_cache.md new file mode 100644 index 000000000..a577d1220 --- /dev/null +++ b/zh-Hant/docs/chapter_array_and_linkedlist/ram_and_cache.md @@ -0,0 +1,84 @@ +--- +comments: true +status: new +--- + +# 4.4   記憶體與快取 * + +在本章的前兩節中,我們探討了陣列和鏈結串列這兩種基礎且重要的資料結構,它們分別代表了“連續儲存”和“分散儲存”兩種物理結構。 + +實際上,**物理結構在很大程度上決定了程式對記憶體和快取的使用效率**,進而影響演算法程式的整體效能。 + +## 4.4.1   計算機儲存裝置 + +計算機中包括三種類型的儲存裝置:硬碟(hard disk)記憶體(random-access memory, RAM)快取(cache memory)。表 4-2 展示了它們在計算機系統中的不同角色和效能特點。 + +

表 4-2   計算機的儲存裝置

+ +
+ +| | 硬碟 | 記憶體 | 快取 | +| ------ | ---------------------------------------- | -------------------------------------- | ------------------------------------------------- | +| 用途 | 長期儲存資料,包括作業系統、程式、檔案等 | 臨時儲存當前執行的程式和正在處理的資料 | 儲存經常訪問的資料和指令,減少 CPU 訪問記憶體的次數 | +| 易失性 | 斷電後資料不會丟失 | 斷電後資料會丟失 | 斷電後資料會丟失 | +| 容量 | 較大,TB 級別 | 較小,GB 級別 | 非常小,MB 級別 | +| 速度 | 較慢,幾百到幾千 MB/s | 較快,幾十 GB/s | 非常快,幾十到幾百 GB/s | +| 價格 | 較便宜,幾毛到幾元 / GB | 較貴,幾十到幾百元 / GB | 非常貴,隨 CPU 打包計價 | + +
+ +我們可以將計算機儲存系統想象為圖 4-9 所示的金字塔結構。越靠近金字塔頂端的儲存裝置的速度越快、容量越小、成本越高。這種多層級的設計並非偶然,而是計算機科學家和工程師們經過深思熟慮的結果。 + +- **硬碟難以被記憶體取代**。首先,記憶體中的資料在斷電後會丟失,因此它不適合長期儲存資料;其次,記憶體的成本是硬碟的幾十倍,這使得它難以在消費者市場普及。 +- **快取的大容量和高速度難以兼得**。隨著 L1、L2、L3 快取的容量逐步增大,其物理尺寸會變大,與 CPU 核心之間的物理距離會變遠,從而導致資料傳輸時間增加,元素訪問延遲變高。在當前技術下,多層級的快取結構是容量、速度和成本之間的最佳平衡點。 + +![計算機儲存系統](ram_and_cache.assets/storage_pyramid.png){ class="animation-figure" } + +

圖 4-9   計算機儲存系統

+ +!!! note + + 計算機的儲存層次結構體現了速度、容量和成本三者之間的精妙平衡。實際上,這種權衡普遍存在於所有工業領域,它要求我們在不同的優勢和限制之間找到最佳平衡點。 + +總的來說,**硬碟用於長期儲存大量資料,記憶體用於臨時儲存程式執行中正在處理的資料,而快取則用於儲存經常訪問的資料和指令**,以提高程式執行效率。三者共同協作,確保計算機系統高效執行。 + +如圖 4-10 所示,在程式執行時,資料會從硬碟中被讀取到記憶體中,供 CPU 計算使用。快取可以看作 CPU 的一部分,**它透過智慧地從記憶體載入資料**,給 CPU 提供高速的資料讀取,從而顯著提升程式的執行效率,減少對較慢的記憶體的依賴。 + +![硬碟、記憶體和快取之間的資料流通](ram_and_cache.assets/computer_storage_devices.png){ class="animation-figure" } + +

圖 4-10   硬碟、記憶體和快取之間的資料流通

+ +## 4.4.2   資料結構的記憶體效率 + +在記憶體空間利用方面,陣列和鏈結串列各自具有優勢和侷限性。 + +一方面,**記憶體是有限的,且同一塊記憶體不能被多個程式共享**,因此我們希望資料結構能夠儘可能高效地利用空間。陣列的元素緊密排列,不需要額外的空間來儲存鏈結串列節點間的引用(指標),因此空間效率更高。然而,陣列需要一次性分配足夠的連續記憶體空間,這可能導致記憶體浪費,陣列擴容也需要額外的時間和空間成本。相比之下,鏈結串列以“節點”為單位進行動態記憶體分配和回收,提供了更大的靈活性。 + +另一方面,在程式執行時,**隨著反覆申請與釋放記憶體,空閒記憶體的碎片化程度會越來越高**,從而導致記憶體的利用效率降低。陣列由於其連續的儲存方式,相對不容易導致記憶體碎片化。相反,鏈結串列的元素是分散儲存的,在頻繁的插入與刪除操作中,更容易導致記憶體碎片化。 + +## 4.4.3   資料結構的快取效率 + +快取雖然在空間容量上遠小於記憶體,但它比記憶體快得多,在程式執行速度上起著至關重要的作用。由於快取的容量有限,只能儲存一小部分頻繁訪問的資料,因此當 CPU 嘗試訪問的資料不在快取中時,就會發生快取未命中(cache miss),此時 CPU 不得不從速度較慢的記憶體中載入所需資料。 + +顯然,**“快取未命中”越少,CPU 讀寫資料的效率就越高**,程式效能也就越好。我們將 CPU 從快取中成功獲取資料的比例稱為快取命中率(cache hit rate),這個指標通常用來衡量快取效率。 + +為了儘可能達到更高的效率,快取會採取以下資料載入機制。 + +- **快取行**:快取不是單個位元組地儲存與載入資料,而是以快取行為單位。相比於單個位元組的傳輸,快取行的傳輸形式更加高效。 +- **預取機制**:處理器會嘗試預測資料訪問模式(例如順序訪問、固定步長跳躍訪問等),並根據特定模式將資料載入至快取之中,從而提升命中率。 +- **空間區域性**:如果一個數據被訪問,那麼它附近的資料可能近期也會被訪問。因此,快取在載入某一資料時,也會載入其附近的資料,以提高命中率。 +- **時間區域性**:如果一個數據被訪問,那麼它在不久的將來很可能再次被訪問。快取利用這一原理,透過保留最近訪問過的資料來提高命中率。 + +實際上,**陣列和鏈結串列對快取的利用效率是不同的**,主要體現在以下幾個方面。 + +- **佔用空間**:鏈結串列元素比陣列元素佔用空間更多,導致快取中容納的有效資料量更少。 +- **快取行**:鏈結串列資料分散在記憶體各處,而快取是“按行載入”的,因此載入到無效資料的比例更高。 +- **預取機制**:陣列比鏈結串列的資料訪問模式更具“可預測性”,即系統更容易猜出即將被載入的資料。 +- **空間區域性**:陣列被儲存在集中的記憶體空間中,因此被載入資料附近的資料更有可能即將被訪問。 + +總體而言,**陣列具有更高的快取命中率,因此它在操作效率上通常優於鏈結串列**。這使得在解決演算法問題時,基於陣列實現的資料結構往往更受歡迎。 + +需要注意的是,**高快取效率並不意味著陣列在所有情況下都優於鏈結串列**。實際應用中選擇哪種資料結構,應根據具體需求來決定。例如,陣列和鏈結串列都可以實現“堆疊”資料結構(下一章會詳細介紹),但它們適用於不同場景。 + +- 在做演算法題時,我們會傾向於選擇基於陣列實現的堆疊,因為它提供了更高的操作效率和隨機訪問的能力,代價僅是需要預先為陣列分配一定的記憶體空間。 +- 如果資料量非常大、動態性很高、堆疊的預期大小難以估計,那麼基於鏈結串列實現的堆疊更加合適。鏈結串列能夠將大量資料分散儲存於記憶體的不同部分,並且避免了陣列擴容產生的額外開銷。 diff --git a/zh-Hant/docs/chapter_array_and_linkedlist/summary.md b/zh-Hant/docs/chapter_array_and_linkedlist/summary.md new file mode 100644 index 000000000..120c088e7 --- /dev/null +++ b/zh-Hant/docs/chapter_array_and_linkedlist/summary.md @@ -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]` 。 diff --git a/zh-Hant/docs/chapter_backtracking/backtracking_algorithm.md b/zh-Hant/docs/chapter_backtracking/backtracking_algorithm.md new file mode 100644 index 000000000..5fa8b4850 --- /dev/null +++ b/zh-Hant/docs/chapter_backtracking/backtracking_algorithm.md @@ -0,0 +1,1951 @@ +--- +comments: true +--- + +# 13.1   回溯演算法 + +回溯演算法(backtracking algorithm)是一種透過窮舉來解決問題的方法,它的核心思想是從一個初始狀態出發,暴力搜尋所有可能的解決方案,當遇到正確的解則將其記錄,直到找到解或者嘗試了所有可能的選擇都無法找到解為止。 + +回溯演算法通常採用“深度優先搜尋”來走訪解空間。在“二元樹”章節中,我們提到前序、中序和後序走訪都屬於深度優先搜尋。接下來,我們利用前序走訪構造一個回溯問題,逐步瞭解回溯演算法的工作原理。 + +!!! question "例題一" + + 給定一棵二元樹,搜尋並記錄所有值為 $7$ 的節點,請返回節點串列。 + +對於此題,我們前序走訪這棵樹,並判斷當前節點的值是否為 $7$ ,若是,則將該節點的值加入結果串列 `res` 之中。相關過程實現如圖 13-1 和以下程式碼所示: + +=== "Python" + + ```python title="preorder_traversal_i_compact.py" + def pre_order(root: TreeNode): + """前序走訪:例題一""" + if root is None: + return + if root.val == 7: + # 記錄解 + res.append(root) + pre_order(root.left) + pre_order(root.right) + ``` + +=== "C++" + + ```cpp title="preorder_traversal_i_compact.cpp" + /* 前序走訪:例題一 */ + void preOrder(TreeNode *root) { + if (root == nullptr) { + return; + } + if (root->val == 7) { + // 記錄解 + res.push_back(root); + } + preOrder(root->left); + preOrder(root->right); + } + ``` + +=== "Java" + + ```java title="preorder_traversal_i_compact.java" + /* 前序走訪:例題一 */ + void preOrder(TreeNode root) { + if (root == null) { + return; + } + if (root.val == 7) { + // 記錄解 + res.add(root); + } + preOrder(root.left); + preOrder(root.right); + } + ``` + +=== "C#" + + ```csharp title="preorder_traversal_i_compact.cs" + /* 前序走訪:例題一 */ + void PreOrder(TreeNode? root) { + if (root == null) { + return; + } + if (root.val == 7) { + // 記錄解 + res.Add(root); + } + PreOrder(root.left); + PreOrder(root.right); + } + ``` + +=== "Go" + + ```go title="preorder_traversal_i_compact.go" + /* 前序走訪:例題一 */ + func preOrderI(root *TreeNode, res *[]*TreeNode) { + if root == nil { + return + } + if (root.Val).(int) == 7 { + // 記錄解 + *res = append(*res, root) + } + preOrderI(root.Left, res) + preOrderI(root.Right, res) + } + ``` + +=== "Swift" + + ```swift title="preorder_traversal_i_compact.swift" + /* 前序走訪:例題一 */ + func preOrder(root: TreeNode?) { + guard let root = root else { + return + } + if root.val == 7 { + // 記錄解 + res.append(root) + } + preOrder(root: root.left) + preOrder(root: root.right) + } + ``` + +=== "JS" + + ```javascript title="preorder_traversal_i_compact.js" + /* 前序走訪:例題一 */ + function preOrder(root, res) { + if (root === null) { + return; + } + if (root.val === 7) { + // 記錄解 + res.push(root); + } + preOrder(root.left, res); + preOrder(root.right, res); + } + ``` + +=== "TS" + + ```typescript title="preorder_traversal_i_compact.ts" + /* 前序走訪:例題一 */ + function preOrder(root: TreeNode | null, res: TreeNode[]): void { + if (root === null) { + return; + } + if (root.val === 7) { + // 記錄解 + res.push(root); + } + preOrder(root.left, res); + preOrder(root.right, res); + } + ``` + +=== "Dart" + + ```dart title="preorder_traversal_i_compact.dart" + /* 前序走訪:例題一 */ + void preOrder(TreeNode? root, List res) { + if (root == null) { + return; + } + if (root.val == 7) { + // 記錄解 + res.add(root); + } + preOrder(root.left, res); + preOrder(root.right, res); + } + ``` + +=== "Rust" + + ```rust title="preorder_traversal_i_compact.rs" + /* 前序走訪:例題一 */ + fn pre_order(res: &mut Vec>>, root: Option>>) { + if root.is_none() { + return; + } + if let Some(node) = root { + if node.borrow().val == 7 { + // 記錄解 + res.push(node.clone()); + } + pre_order(res, node.borrow().left.clone()); + pre_order(res, node.borrow().right.clone()); + } + } + ``` + +=== "C" + + ```c title="preorder_traversal_i_compact.c" + /* 前序走訪:例題一 */ + void preOrder(TreeNode *root) { + if (root == NULL) { + return; + } + if (root->val == 7) { + // 記錄解 + res[resSize++] = root; + } + preOrder(root->left); + preOrder(root->right); + } + ``` + +=== "Kotlin" + + ```kotlin title="preorder_traversal_i_compact.kt" + /* 前序走訪:例題一 */ + fun preOrder(root: TreeNode?) { + if (root == null) { + return + } + if (root.value == 7) { + // 記錄解 + res!!.add(root) + } + preOrder(root.left) + preOrder(root.right) + } + ``` + +=== "Ruby" + + ```ruby title="preorder_traversal_i_compact.rb" + [class]{}-[func]{pre_order} + ``` + +=== "Zig" + + ```zig title="preorder_traversal_i_compact.zig" + [class]{}-[func]{preOrder} + ``` + +??? pythontutor "視覺化執行" + +
+ + +![在前序走訪中搜索節點](backtracking_algorithm.assets/preorder_find_nodes.png){ class="animation-figure" } + +

圖 13-1   在前序走訪中搜索節點

+ +## 13.1.1   嘗試與回退 + +**之所以稱之為回溯演算法,是因為該演算法在搜尋解空間時會採用“嘗試”與“回退”的策略**。當演算法在搜尋過程中遇到某個狀態無法繼續前進或無法得到滿足條件的解時,它會撤銷上一步的選擇,退回到之前的狀態,並嘗試其他可能的選擇。 + +對於例題一,訪問每個節點都代表一次“嘗試”,而越過葉節點或返回父節點的 `return` 則表示“回退”。 + +值得說明的是,**回退並不僅僅包括函式返回**。為解釋這一點,我們對例題一稍作拓展。 + +!!! question "例題二" + + 在二元樹中搜索所有值為 $7$ 的節點,**請返回根節點到這些節點的路徑**。 + +在例題一程式碼的基礎上,我們需要藉助一個串列 `path` 記錄訪問過的節點路徑。當訪問到值為 $7$ 的節點時,則複製 `path` 並新增進結果串列 `res` 。走訪完成後,`res` 中儲存的就是所有的解。程式碼如下所示: + +=== "Python" + + ```python title="preorder_traversal_ii_compact.py" + def pre_order(root: TreeNode): + """前序走訪:例題二""" + if root is None: + return + # 嘗試 + path.append(root) + if root.val == 7: + # 記錄解 + res.append(list(path)) + pre_order(root.left) + pre_order(root.right) + # 回退 + path.pop() + ``` + +=== "C++" + + ```cpp title="preorder_traversal_ii_compact.cpp" + /* 前序走訪:例題二 */ + void preOrder(TreeNode *root) { + if (root == nullptr) { + return; + } + // 嘗試 + path.push_back(root); + if (root->val == 7) { + // 記錄解 + res.push_back(path); + } + preOrder(root->left); + preOrder(root->right); + // 回退 + path.pop_back(); + } + ``` + +=== "Java" + + ```java title="preorder_traversal_ii_compact.java" + /* 前序走訪:例題二 */ + void preOrder(TreeNode root) { + if (root == null) { + return; + } + // 嘗試 + path.add(root); + if (root.val == 7) { + // 記錄解 + res.add(new ArrayList<>(path)); + } + preOrder(root.left); + preOrder(root.right); + // 回退 + path.remove(path.size() - 1); + } + ``` + +=== "C#" + + ```csharp title="preorder_traversal_ii_compact.cs" + /* 前序走訪:例題二 */ + void PreOrder(TreeNode? root) { + if (root == null) { + return; + } + // 嘗試 + path.Add(root); + if (root.val == 7) { + // 記錄解 + res.Add(new List(path)); + } + PreOrder(root.left); + PreOrder(root.right); + // 回退 + path.RemoveAt(path.Count - 1); + } + ``` + +=== "Go" + + ```go title="preorder_traversal_ii_compact.go" + /* 前序走訪:例題二 */ + func preOrderII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) { + if root == nil { + return + } + // 嘗試 + *path = append(*path, root) + if root.Val.(int) == 7 { + // 記錄解 + *res = append(*res, append([]*TreeNode{}, *path...)) + } + preOrderII(root.Left, res, path) + preOrderII(root.Right, res, path) + // 回退 + *path = (*path)[:len(*path)-1] + } + ``` + +=== "Swift" + + ```swift title="preorder_traversal_ii_compact.swift" + /* 前序走訪:例題二 */ + func preOrder(root: TreeNode?) { + guard let root = root else { + return + } + // 嘗試 + path.append(root) + if root.val == 7 { + // 記錄解 + res.append(path) + } + preOrder(root: root.left) + preOrder(root: root.right) + // 回退 + path.removeLast() + } + ``` + +=== "JS" + + ```javascript title="preorder_traversal_ii_compact.js" + /* 前序走訪:例題二 */ + function preOrder(root, path, res) { + if (root === null) { + return; + } + // 嘗試 + path.push(root); + if (root.val === 7) { + // 記錄解 + res.push([...path]); + } + preOrder(root.left, path, res); + preOrder(root.right, path, res); + // 回退 + path.pop(); + } + ``` + +=== "TS" + + ```typescript title="preorder_traversal_ii_compact.ts" + /* 前序走訪:例題二 */ + function preOrder( + root: TreeNode | null, + path: TreeNode[], + res: TreeNode[][] + ): void { + if (root === null) { + return; + } + // 嘗試 + path.push(root); + if (root.val === 7) { + // 記錄解 + res.push([...path]); + } + preOrder(root.left, path, res); + preOrder(root.right, path, res); + // 回退 + path.pop(); + } + ``` + +=== "Dart" + + ```dart title="preorder_traversal_ii_compact.dart" + /* 前序走訪:例題二 */ + void preOrder( + TreeNode? root, + List path, + List> res, + ) { + if (root == null) { + return; + } + + // 嘗試 + path.add(root); + if (root.val == 7) { + // 記錄解 + res.add(List.from(path)); + } + preOrder(root.left, path, res); + preOrder(root.right, path, res); + // 回退 + path.removeLast(); + } + ``` + +=== "Rust" + + ```rust title="preorder_traversal_ii_compact.rs" + /* 前序走訪:例題二 */ + fn pre_order( + res: &mut Vec>>>, + path: &mut Vec>>, + root: Option>>, + ) { + if root.is_none() { + return; + } + if let Some(node) = root { + // 嘗試 + path.push(node.clone()); + if node.borrow().val == 7 { + // 記錄解 + res.push(path.clone()); + } + pre_order(res, path, node.borrow().left.clone()); + pre_order(res, path, node.borrow().right.clone()); + // 回退 + path.remove(path.len() - 1); + } + } + ``` + +=== "C" + + ```c title="preorder_traversal_ii_compact.c" + /* 前序走訪:例題二 */ + void preOrder(TreeNode *root) { + if (root == NULL) { + return; + } + // 嘗試 + path[pathSize++] = root; + if (root->val == 7) { + // 記錄解 + for (int i = 0; i < pathSize; ++i) { + res[resSize][i] = path[i]; + } + resSize++; + } + preOrder(root->left); + preOrder(root->right); + // 回退 + pathSize--; + } + ``` + +=== "Kotlin" + + ```kotlin title="preorder_traversal_ii_compact.kt" + /* 前序走訪:例題二 */ + fun preOrder(root: TreeNode?) { + if (root == null) { + return + } + // 嘗試 + path!!.add(root) + if (root.value == 7) { + // 記錄解 + res!!.add(ArrayList(path!!)) + } + preOrder(root.left) + preOrder(root.right) + // 回退 + path!!.removeAt(path!!.size - 1) + } + ``` + +=== "Ruby" + + ```ruby title="preorder_traversal_ii_compact.rb" + [class]{}-[func]{pre_order} + ``` + +=== "Zig" + + ```zig title="preorder_traversal_ii_compact.zig" + [class]{}-[func]{preOrder} + ``` + +??? pythontutor "視覺化執行" + +
+ + +在每次“嘗試”中,我們透過將當前節點新增進 `path` 來記錄路徑;而在“回退”前,我們需要將該節點從 `path` 中彈出,**以恢復本次嘗試之前的狀態**。 + +觀察圖 13-2 所示的過程,**我們可以將嘗試和回退理解為“前進”與“撤銷”**,兩個操作互為逆向。 + +=== "<1>" + ![嘗試與回退](backtracking_algorithm.assets/preorder_find_paths_step1.png){ class="animation-figure" } + +=== "<2>" + ![preorder_find_paths_step2](backtracking_algorithm.assets/preorder_find_paths_step2.png){ class="animation-figure" } + +=== "<3>" + ![preorder_find_paths_step3](backtracking_algorithm.assets/preorder_find_paths_step3.png){ class="animation-figure" } + +=== "<4>" + ![preorder_find_paths_step4](backtracking_algorithm.assets/preorder_find_paths_step4.png){ class="animation-figure" } + +=== "<5>" + ![preorder_find_paths_step5](backtracking_algorithm.assets/preorder_find_paths_step5.png){ class="animation-figure" } + +=== "<6>" + ![preorder_find_paths_step6](backtracking_algorithm.assets/preorder_find_paths_step6.png){ class="animation-figure" } + +=== "<7>" + ![preorder_find_paths_step7](backtracking_algorithm.assets/preorder_find_paths_step7.png){ class="animation-figure" } + +=== "<8>" + ![preorder_find_paths_step8](backtracking_algorithm.assets/preorder_find_paths_step8.png){ class="animation-figure" } + +=== "<9>" + ![preorder_find_paths_step9](backtracking_algorithm.assets/preorder_find_paths_step9.png){ class="animation-figure" } + +=== "<10>" + ![preorder_find_paths_step10](backtracking_algorithm.assets/preorder_find_paths_step10.png){ class="animation-figure" } + +=== "<11>" + ![preorder_find_paths_step11](backtracking_algorithm.assets/preorder_find_paths_step11.png){ class="animation-figure" } + +

圖 13-2   嘗試與回退

+ +## 13.1.2   剪枝 + +複雜的回溯問題通常包含一個或多個約束條件,**約束條件通常可用於“剪枝”**。 + +!!! question "例題三" + + 在二元樹中搜索所有值為 $7$ 的節點,請返回根節點到這些節點的路徑,**並要求路徑中不包含值為 $3$ 的節點**。 + +為了滿足以上約束條件,**我們需要新增剪枝操作**:在搜尋過程中,若遇到值為 $3$ 的節點,則提前返回,不再繼續搜尋。程式碼如下所示: + +=== "Python" + + ```python title="preorder_traversal_iii_compact.py" + def pre_order(root: TreeNode): + """前序走訪:例題三""" + # 剪枝 + if root is None or root.val == 3: + return + # 嘗試 + path.append(root) + if root.val == 7: + # 記錄解 + res.append(list(path)) + pre_order(root.left) + pre_order(root.right) + # 回退 + path.pop() + ``` + +=== "C++" + + ```cpp title="preorder_traversal_iii_compact.cpp" + /* 前序走訪:例題三 */ + void preOrder(TreeNode *root) { + // 剪枝 + if (root == nullptr || root->val == 3) { + return; + } + // 嘗試 + path.push_back(root); + if (root->val == 7) { + // 記錄解 + res.push_back(path); + } + preOrder(root->left); + preOrder(root->right); + // 回退 + path.pop_back(); + } + ``` + +=== "Java" + + ```java title="preorder_traversal_iii_compact.java" + /* 前序走訪:例題三 */ + void preOrder(TreeNode root) { + // 剪枝 + if (root == null || root.val == 3) { + return; + } + // 嘗試 + path.add(root); + if (root.val == 7) { + // 記錄解 + res.add(new ArrayList<>(path)); + } + preOrder(root.left); + preOrder(root.right); + // 回退 + path.remove(path.size() - 1); + } + ``` + +=== "C#" + + ```csharp title="preorder_traversal_iii_compact.cs" + /* 前序走訪:例題三 */ + void PreOrder(TreeNode? root) { + // 剪枝 + if (root == null || root.val == 3) { + return; + } + // 嘗試 + path.Add(root); + if (root.val == 7) { + // 記錄解 + res.Add(new List(path)); + } + PreOrder(root.left); + PreOrder(root.right); + // 回退 + path.RemoveAt(path.Count - 1); + } + ``` + +=== "Go" + + ```go title="preorder_traversal_iii_compact.go" + /* 前序走訪:例題三 */ + func preOrderIII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) { + // 剪枝 + if root == nil || root.Val == 3 { + return + } + // 嘗試 + *path = append(*path, root) + if root.Val.(int) == 7 { + // 記錄解 + *res = append(*res, append([]*TreeNode{}, *path...)) + } + preOrderIII(root.Left, res, path) + preOrderIII(root.Right, res, path) + // 回退 + *path = (*path)[:len(*path)-1] + } + ``` + +=== "Swift" + + ```swift title="preorder_traversal_iii_compact.swift" + /* 前序走訪:例題三 */ + func preOrder(root: TreeNode?) { + // 剪枝 + guard let root = root, root.val != 3 else { + return + } + // 嘗試 + path.append(root) + if root.val == 7 { + // 記錄解 + res.append(path) + } + preOrder(root: root.left) + preOrder(root: root.right) + // 回退 + path.removeLast() + } + ``` + +=== "JS" + + ```javascript title="preorder_traversal_iii_compact.js" + /* 前序走訪:例題三 */ + function preOrder(root, path, res) { + // 剪枝 + if (root === null || root.val === 3) { + return; + } + // 嘗試 + path.push(root); + if (root.val === 7) { + // 記錄解 + res.push([...path]); + } + preOrder(root.left, path, res); + preOrder(root.right, path, res); + // 回退 + path.pop(); + } + ``` + +=== "TS" + + ```typescript title="preorder_traversal_iii_compact.ts" + /* 前序走訪:例題三 */ + function preOrder( + root: TreeNode | null, + path: TreeNode[], + res: TreeNode[][] + ): void { + // 剪枝 + if (root === null || root.val === 3) { + return; + } + // 嘗試 + path.push(root); + if (root.val === 7) { + // 記錄解 + res.push([...path]); + } + preOrder(root.left, path, res); + preOrder(root.right, path, res); + // 回退 + path.pop(); + } + ``` + +=== "Dart" + + ```dart title="preorder_traversal_iii_compact.dart" + /* 前序走訪:例題三 */ + void preOrder( + TreeNode? root, + List path, + List> res, + ) { + if (root == null || root.val == 3) { + return; + } + + // 嘗試 + path.add(root); + if (root.val == 7) { + // 記錄解 + res.add(List.from(path)); + } + preOrder(root.left, path, res); + preOrder(root.right, path, res); + // 回退 + path.removeLast(); + } + ``` + +=== "Rust" + + ```rust title="preorder_traversal_iii_compact.rs" + /* 前序走訪:例題三 */ + fn pre_order( + res: &mut Vec>>>, + path: &mut Vec>>, + root: Option>>, + ) { + // 剪枝 + if root.is_none() || root.as_ref().unwrap().borrow().val == 3 { + return; + } + if let Some(node) = root { + // 嘗試 + path.push(node.clone()); + if node.borrow().val == 7 { + // 記錄解 + res.push(path.clone()); + } + pre_order(res, path, node.borrow().left.clone()); + pre_order(res, path, node.borrow().right.clone()); + // 回退 + path.remove(path.len() - 1); + } + } + ``` + +=== "C" + + ```c title="preorder_traversal_iii_compact.c" + /* 前序走訪:例題三 */ + void preOrder(TreeNode *root) { + // 剪枝 + if (root == NULL || root->val == 3) { + return; + } + // 嘗試 + path[pathSize++] = root; + if (root->val == 7) { + // 記錄解 + for (int i = 0; i < pathSize; i++) { + res[resSize][i] = path[i]; + } + resSize++; + } + preOrder(root->left); + preOrder(root->right); + // 回退 + pathSize--; + } + ``` + +=== "Kotlin" + + ```kotlin title="preorder_traversal_iii_compact.kt" + /* 前序走訪:例題三 */ + fun preOrder(root: TreeNode?) { + // 剪枝 + if (root == null || root.value == 3) { + return + } + // 嘗試 + path!!.add(root) + if (root.value == 7) { + // 記錄解 + res!!.add(ArrayList(path!!)) + } + preOrder(root.left) + preOrder(root.right) + // 回退 + path!!.removeAt(path!!.size - 1) + } + ``` + +=== "Ruby" + + ```ruby title="preorder_traversal_iii_compact.rb" + [class]{}-[func]{pre_order} + ``` + +=== "Zig" + + ```zig title="preorder_traversal_iii_compact.zig" + [class]{}-[func]{preOrder} + ``` + +??? pythontutor "視覺化執行" + +
+ + +“剪枝”是一個非常形象的名詞。如圖 13-3 所示,在搜尋過程中,**我們“剪掉”了不滿足約束條件的搜尋分支**,避免許多無意義的嘗試,從而提高了搜尋效率。 + +![根據約束條件剪枝](backtracking_algorithm.assets/preorder_find_constrained_paths.png){ class="animation-figure" } + +

圖 13-3   根據約束條件剪枝

+ +## 13.1.3   框架程式碼 + +接下來,我們嘗試將回溯的“嘗試、回退、剪枝”的主體框架提煉出來,提升程式碼的通用性。 + +在以下框架程式碼中,`state` 表示問題的當前狀態,`choices` 表示當前狀態下可以做出的選擇: + +=== "Python" + + ```python title="" + def backtrack(state: State, choices: list[choice], res: list[state]): + """回溯演算法框架""" + # 判斷是否為解 + if is_solution(state): + # 記錄解 + record_solution(state, res) + # 不再繼續搜尋 + return + # 走訪所有選擇 + for choice in choices: + # 剪枝:判斷選擇是否合法 + if is_valid(state, choice): + # 嘗試:做出選擇,更新狀態 + make_choice(state, choice) + backtrack(state, choices, res) + # 回退:撤銷選擇,恢復到之前的狀態 + undo_choice(state, choice) + ``` + +=== "C++" + + ```cpp title="" + /* 回溯演算法框架 */ + void backtrack(State *state, vector &choices, vector &res) { + // 判斷是否為解 + if (isSolution(state)) { + // 記錄解 + recordSolution(state, res); + // 不再繼續搜尋 + return; + } + // 走訪所有選擇 + for (Choice choice : choices) { + // 剪枝:判斷選擇是否合法 + if (isValid(state, choice)) { + // 嘗試:做出選擇,更新狀態 + makeChoice(state, choice); + backtrack(state, choices, res); + // 回退:撤銷選擇,恢復到之前的狀態 + undoChoice(state, choice); + } + } + } + ``` + +=== "Java" + + ```java title="" + /* 回溯演算法框架 */ + void backtrack(State state, List choices, List res) { + // 判斷是否為解 + if (isSolution(state)) { + // 記錄解 + recordSolution(state, res); + // 不再繼續搜尋 + return; + } + // 走訪所有選擇 + for (Choice choice : choices) { + // 剪枝:判斷選擇是否合法 + if (isValid(state, choice)) { + // 嘗試:做出選擇,更新狀態 + makeChoice(state, choice); + backtrack(state, choices, res); + // 回退:撤銷選擇,恢復到之前的狀態 + undoChoice(state, choice); + } + } + } + ``` + +=== "C#" + + ```csharp title="" + /* 回溯演算法框架 */ + void Backtrack(State state, List choices, List res) { + // 判斷是否為解 + if (IsSolution(state)) { + // 記錄解 + RecordSolution(state, res); + // 不再繼續搜尋 + return; + } + // 走訪所有選擇 + foreach (Choice choice in choices) { + // 剪枝:判斷選擇是否合法 + if (IsValid(state, choice)) { + // 嘗試:做出選擇,更新狀態 + MakeChoice(state, choice); + Backtrack(state, choices, res); + // 回退:撤銷選擇,恢復到之前的狀態 + UndoChoice(state, choice); + } + } + } + ``` + +=== "Go" + + ```go title="" + /* 回溯演算法框架 */ + func backtrack(state *State, choices []Choice, res *[]State) { + // 判斷是否為解 + if isSolution(state) { + // 記錄解 + recordSolution(state, res) + // 不再繼續搜尋 + return + } + // 走訪所有選擇 + for _, choice := range choices { + // 剪枝:判斷選擇是否合法 + if isValid(state, choice) { + // 嘗試:做出選擇,更新狀態 + makeChoice(state, choice) + backtrack(state, choices, res) + // 回退:撤銷選擇,恢復到之前的狀態 + undoChoice(state, choice) + } + } + } + ``` + +=== "Swift" + + ```swift title="" + /* 回溯演算法框架 */ + func backtrack(state: inout State, choices: [Choice], res: inout [State]) { + // 判斷是否為解 + if isSolution(state: state) { + // 記錄解 + recordSolution(state: state, res: &res) + // 不再繼續搜尋 + return + } + // 走訪所有選擇 + for choice in choices { + // 剪枝:判斷選擇是否合法 + if isValid(state: state, choice: choice) { + // 嘗試:做出選擇,更新狀態 + makeChoice(state: &state, choice: choice) + backtrack(state: &state, choices: choices, res: &res) + // 回退:撤銷選擇,恢復到之前的狀態 + undoChoice(state: &state, choice: choice) + } + } + } + ``` + +=== "JS" + + ```javascript title="" + /* 回溯演算法框架 */ + function backtrack(state, choices, res) { + // 判斷是否為解 + if (isSolution(state)) { + // 記錄解 + recordSolution(state, res); + // 不再繼續搜尋 + return; + } + // 走訪所有選擇 + for (let choice of choices) { + // 剪枝:判斷選擇是否合法 + if (isValid(state, choice)) { + // 嘗試:做出選擇,更新狀態 + makeChoice(state, choice); + backtrack(state, choices, res); + // 回退:撤銷選擇,恢復到之前的狀態 + undoChoice(state, choice); + } + } + } + ``` + +=== "TS" + + ```typescript title="" + /* 回溯演算法框架 */ + function backtrack(state: State, choices: Choice[], res: State[]): void { + // 判斷是否為解 + if (isSolution(state)) { + // 記錄解 + recordSolution(state, res); + // 不再繼續搜尋 + return; + } + // 走訪所有選擇 + for (let choice of choices) { + // 剪枝:判斷選擇是否合法 + if (isValid(state, choice)) { + // 嘗試:做出選擇,更新狀態 + makeChoice(state, choice); + backtrack(state, choices, res); + // 回退:撤銷選擇,恢復到之前的狀態 + undoChoice(state, choice); + } + } + } + ``` + +=== "Dart" + + ```dart title="" + /* 回溯演算法框架 */ + void backtrack(State state, List, List res) { + // 判斷是否為解 + if (isSolution(state)) { + // 記錄解 + recordSolution(state, res); + // 不再繼續搜尋 + return; + } + // 走訪所有選擇 + for (Choice choice in choices) { + // 剪枝:判斷選擇是否合法 + if (isValid(state, choice)) { + // 嘗試:做出選擇,更新狀態 + makeChoice(state, choice); + backtrack(state, choices, res); + // 回退:撤銷選擇,恢復到之前的狀態 + undoChoice(state, choice); + } + } + } + ``` + +=== "Rust" + + ```rust title="" + /* 回溯演算法框架 */ + fn backtrack(state: &mut State, choices: &Vec, res: &mut Vec) { + // 判斷是否為解 + if is_solution(state) { + // 記錄解 + record_solution(state, res); + // 不再繼續搜尋 + return; + } + // 走訪所有選擇 + for choice in choices { + // 剪枝:判斷選擇是否合法 + if is_valid(state, choice) { + // 嘗試:做出選擇,更新狀態 + make_choice(state, choice); + backtrack(state, choices, res); + // 回退:撤銷選擇,恢復到之前的狀態 + undo_choice(state, choice); + } + } + } + ``` + +=== "C" + + ```c title="" + /* 回溯演算法框架 */ + void backtrack(State *state, Choice *choices, int numChoices, State *res, int numRes) { + // 判斷是否為解 + if (isSolution(state)) { + // 記錄解 + recordSolution(state, res, numRes); + // 不再繼續搜尋 + return; + } + // 走訪所有選擇 + for (int i = 0; i < numChoices; i++) { + // 剪枝:判斷選擇是否合法 + if (isValid(state, &choices[i])) { + // 嘗試:做出選擇,更新狀態 + makeChoice(state, &choices[i]); + backtrack(state, choices, numChoices, res, numRes); + // 回退:撤銷選擇,恢復到之前的狀態 + undoChoice(state, &choices[i]); + } + } + } + ``` + +=== "Kotlin" + + ```kotlin title="" + /* 回溯演算法框架 */ + fun backtrack(state: State?, choices: List, res: List?) { + // 判斷是否為解 + if (isSolution(state)) { + // 記錄解 + recordSolution(state, res) + // 不再繼續搜尋 + return + } + // 走訪所有選擇 + for (choice in choices) { + // 剪枝:判斷選擇是否合法 + if (isValid(state, choice)) { + // 嘗試:做出選擇,更新狀態 + makeChoice(state, choice) + backtrack(state, choices, res) + // 回退:撤銷選擇,恢復到之前的狀態 + undoChoice(state, choice) + } + } + } + ``` + +=== "Ruby" + + ```ruby title="" + + ``` + +=== "Zig" + + ```zig title="" + + ``` + +接下來,我們基於框架程式碼來解決例題三。狀態 `state` 為節點走訪路徑,選擇 `choices` 為當前節點的左子節點和右子節點,結果 `res` 是路徑串列: + +=== "Python" + + ```python title="preorder_traversal_iii_template.py" + def is_solution(state: list[TreeNode]) -> bool: + """判斷當前狀態是否為解""" + return state and state[-1].val == 7 + + def record_solution(state: list[TreeNode], res: list[list[TreeNode]]): + """記錄解""" + res.append(list(state)) + + def is_valid(state: list[TreeNode], choice: TreeNode) -> bool: + """判斷在當前狀態下,該選擇是否合法""" + return choice is not None and choice.val != 3 + + def make_choice(state: list[TreeNode], choice: TreeNode): + """更新狀態""" + state.append(choice) + + def undo_choice(state: list[TreeNode], choice: TreeNode): + """恢復狀態""" + state.pop() + + def backtrack( + state: list[TreeNode], choices: list[TreeNode], res: list[list[TreeNode]] + ): + """回溯演算法:例題三""" + # 檢查是否為解 + if is_solution(state): + # 記錄解 + record_solution(state, res) + # 走訪所有選擇 + for choice in choices: + # 剪枝:檢查選擇是否合法 + if is_valid(state, choice): + # 嘗試:做出選擇,更新狀態 + make_choice(state, choice) + # 進行下一輪選擇 + backtrack(state, [choice.left, choice.right], res) + # 回退:撤銷選擇,恢復到之前的狀態 + undo_choice(state, choice) + ``` + +=== "C++" + + ```cpp title="preorder_traversal_iii_template.cpp" + /* 判斷當前狀態是否為解 */ + bool isSolution(vector &state) { + return !state.empty() && state.back()->val == 7; + } + + /* 記錄解 */ + void recordSolution(vector &state, vector> &res) { + res.push_back(state); + } + + /* 判斷在當前狀態下,該選擇是否合法 */ + bool isValid(vector &state, TreeNode *choice) { + return choice != nullptr && choice->val != 3; + } + + /* 更新狀態 */ + void makeChoice(vector &state, TreeNode *choice) { + state.push_back(choice); + } + + /* 恢復狀態 */ + void undoChoice(vector &state, TreeNode *choice) { + state.pop_back(); + } + + /* 回溯演算法:例題三 */ + void backtrack(vector &state, vector &choices, vector> &res) { + // 檢查是否為解 + if (isSolution(state)) { + // 記錄解 + recordSolution(state, res); + } + // 走訪所有選擇 + for (TreeNode *choice : choices) { + // 剪枝:檢查選擇是否合法 + if (isValid(state, choice)) { + // 嘗試:做出選擇,更新狀態 + makeChoice(state, choice); + // 進行下一輪選擇 + vector nextChoices{choice->left, choice->right}; + backtrack(state, nextChoices, res); + // 回退:撤銷選擇,恢復到之前的狀態 + undoChoice(state, choice); + } + } + } + ``` + +=== "Java" + + ```java title="preorder_traversal_iii_template.java" + /* 判斷當前狀態是否為解 */ + boolean isSolution(List state) { + return !state.isEmpty() && state.get(state.size() - 1).val == 7; + } + + /* 記錄解 */ + void recordSolution(List state, List> res) { + res.add(new ArrayList<>(state)); + } + + /* 判斷在當前狀態下,該選擇是否合法 */ + boolean isValid(List state, TreeNode choice) { + return choice != null && choice.val != 3; + } + + /* 更新狀態 */ + void makeChoice(List state, TreeNode choice) { + state.add(choice); + } + + /* 恢復狀態 */ + void undoChoice(List state, TreeNode choice) { + state.remove(state.size() - 1); + } + + /* 回溯演算法:例題三 */ + void backtrack(List state, List choices, List> res) { + // 檢查是否為解 + if (isSolution(state)) { + // 記錄解 + recordSolution(state, res); + } + // 走訪所有選擇 + for (TreeNode choice : choices) { + // 剪枝:檢查選擇是否合法 + if (isValid(state, choice)) { + // 嘗試:做出選擇,更新狀態 + makeChoice(state, choice); + // 進行下一輪選擇 + backtrack(state, Arrays.asList(choice.left, choice.right), res); + // 回退:撤銷選擇,恢復到之前的狀態 + undoChoice(state, choice); + } + } + } + ``` + +=== "C#" + + ```csharp title="preorder_traversal_iii_template.cs" + /* 判斷當前狀態是否為解 */ + bool IsSolution(List state) { + return state.Count != 0 && state[^1].val == 7; + } + + /* 記錄解 */ + void RecordSolution(List state, List> res) { + res.Add(new List(state)); + } + + /* 判斷在當前狀態下,該選擇是否合法 */ + bool IsValid(List state, TreeNode choice) { + return choice != null && choice.val != 3; + } + + /* 更新狀態 */ + void MakeChoice(List state, TreeNode choice) { + state.Add(choice); + } + + /* 恢復狀態 */ + void UndoChoice(List state, TreeNode choice) { + state.RemoveAt(state.Count - 1); + } + + /* 回溯演算法:例題三 */ + void Backtrack(List state, List choices, List> res) { + // 檢查是否為解 + if (IsSolution(state)) { + // 記錄解 + RecordSolution(state, res); + } + // 走訪所有選擇 + foreach (TreeNode choice in choices) { + // 剪枝:檢查選擇是否合法 + if (IsValid(state, choice)) { + // 嘗試:做出選擇,更新狀態 + MakeChoice(state, choice); + // 進行下一輪選擇 + Backtrack(state, [choice.left!, choice.right!], res); + // 回退:撤銷選擇,恢復到之前的狀態 + UndoChoice(state, choice); + } + } + } + ``` + +=== "Go" + + ```go title="preorder_traversal_iii_template.go" + /* 判斷當前狀態是否為解 */ + func isSolution(state *[]*TreeNode) bool { + return len(*state) != 0 && (*state)[len(*state)-1].Val == 7 + } + + /* 記錄解 */ + func recordSolution(state *[]*TreeNode, res *[][]*TreeNode) { + *res = append(*res, append([]*TreeNode{}, *state...)) + } + + /* 判斷在當前狀態下,該選擇是否合法 */ + func isValid(state *[]*TreeNode, choice *TreeNode) bool { + return choice != nil && choice.Val != 3 + } + + /* 更新狀態 */ + func makeChoice(state *[]*TreeNode, choice *TreeNode) { + *state = append(*state, choice) + } + + /* 恢復狀態 */ + func undoChoice(state *[]*TreeNode, choice *TreeNode) { + *state = (*state)[:len(*state)-1] + } + + /* 回溯演算法:例題三 */ + func backtrackIII(state *[]*TreeNode, choices *[]*TreeNode, res *[][]*TreeNode) { + // 檢查是否為解 + if isSolution(state) { + // 記錄解 + recordSolution(state, res) + } + // 走訪所有選擇 + for _, choice := range *choices { + // 剪枝:檢查選擇是否合法 + if isValid(state, choice) { + // 嘗試:做出選擇,更新狀態 + makeChoice(state, choice) + // 進行下一輪選擇 + temp := make([]*TreeNode, 0) + temp = append(temp, choice.Left, choice.Right) + backtrackIII(state, &temp, res) + // 回退:撤銷選擇,恢復到之前的狀態 + undoChoice(state, choice) + } + } + } + ``` + +=== "Swift" + + ```swift title="preorder_traversal_iii_template.swift" + /* 判斷當前狀態是否為解 */ + func isSolution(state: [TreeNode]) -> Bool { + !state.isEmpty && state.last!.val == 7 + } + + /* 記錄解 */ + func recordSolution(state: [TreeNode], res: inout [[TreeNode]]) { + res.append(state) + } + + /* 判斷在當前狀態下,該選擇是否合法 */ + func isValid(state: [TreeNode], choice: TreeNode?) -> Bool { + choice != nil && choice!.val != 3 + } + + /* 更新狀態 */ + func makeChoice(state: inout [TreeNode], choice: TreeNode) { + state.append(choice) + } + + /* 恢復狀態 */ + func undoChoice(state: inout [TreeNode], choice: TreeNode) { + state.removeLast() + } + + /* 回溯演算法:例題三 */ + func backtrack(state: inout [TreeNode], choices: [TreeNode], res: inout [[TreeNode]]) { + // 檢查是否為解 + if isSolution(state: state) { + recordSolution(state: state, res: &res) + } + // 走訪所有選擇 + for choice in choices { + // 剪枝:檢查選擇是否合法 + if isValid(state: state, choice: choice) { + // 嘗試:做出選擇,更新狀態 + makeChoice(state: &state, choice: choice) + // 進行下一輪選擇 + backtrack(state: &state, choices: [choice.left, choice.right].compactMap { $0 }, res: &res) + // 回退:撤銷選擇,恢復到之前的狀態 + undoChoice(state: &state, choice: choice) + } + } + } + ``` + +=== "JS" + + ```javascript title="preorder_traversal_iii_template.js" + /* 判斷當前狀態是否為解 */ + function isSolution(state) { + return state && state[state.length - 1]?.val === 7; + } + + /* 記錄解 */ + function recordSolution(state, res) { + res.push([...state]); + } + + /* 判斷在當前狀態下,該選擇是否合法 */ + function isValid(state, choice) { + return choice !== null && choice.val !== 3; + } + + /* 更新狀態 */ + function makeChoice(state, choice) { + state.push(choice); + } + + /* 恢復狀態 */ + function undoChoice(state) { + state.pop(); + } + + /* 回溯演算法:例題三 */ + function backtrack(state, choices, res) { + // 檢查是否為解 + if (isSolution(state)) { + // 記錄解 + recordSolution(state, res); + } + // 走訪所有選擇 + for (const choice of choices) { + // 剪枝:檢查選擇是否合法 + if (isValid(state, choice)) { + // 嘗試:做出選擇,更新狀態 + makeChoice(state, choice); + // 進行下一輪選擇 + backtrack(state, [choice.left, choice.right], res); + // 回退:撤銷選擇,恢復到之前的狀態 + undoChoice(state); + } + } + } + ``` + +=== "TS" + + ```typescript title="preorder_traversal_iii_template.ts" + /* 判斷當前狀態是否為解 */ + function isSolution(state: TreeNode[]): boolean { + return state && state[state.length - 1]?.val === 7; + } + + /* 記錄解 */ + function recordSolution(state: TreeNode[], res: TreeNode[][]): void { + res.push([...state]); + } + + /* 判斷在當前狀態下,該選擇是否合法 */ + function isValid(state: TreeNode[], choice: TreeNode): boolean { + return choice !== null && choice.val !== 3; + } + + /* 更新狀態 */ + function makeChoice(state: TreeNode[], choice: TreeNode): void { + state.push(choice); + } + + /* 恢復狀態 */ + function undoChoice(state: TreeNode[]): void { + state.pop(); + } + + /* 回溯演算法:例題三 */ + function backtrack( + state: TreeNode[], + choices: TreeNode[], + res: TreeNode[][] + ): void { + // 檢查是否為解 + if (isSolution(state)) { + // 記錄解 + recordSolution(state, res); + } + // 走訪所有選擇 + for (const choice of choices) { + // 剪枝:檢查選擇是否合法 + if (isValid(state, choice)) { + // 嘗試:做出選擇,更新狀態 + makeChoice(state, choice); + // 進行下一輪選擇 + backtrack(state, [choice.left, choice.right], res); + // 回退:撤銷選擇,恢復到之前的狀態 + undoChoice(state); + } + } + } + ``` + +=== "Dart" + + ```dart title="preorder_traversal_iii_template.dart" + /* 判斷當前狀態是否為解 */ + bool isSolution(List state) { + return state.isNotEmpty && state.last.val == 7; + } + + /* 記錄解 */ + void recordSolution(List state, List> res) { + res.add(List.from(state)); + } + + /* 判斷在當前狀態下,該選擇是否合法 */ + bool isValid(List state, TreeNode? choice) { + return choice != null && choice.val != 3; + } + + /* 更新狀態 */ + void makeChoice(List state, TreeNode? choice) { + state.add(choice!); + } + + /* 恢復狀態 */ + void undoChoice(List state, TreeNode? choice) { + state.removeLast(); + } + + /* 回溯演算法:例題三 */ + void backtrack( + List state, + List choices, + List> res, + ) { + // 檢查是否為解 + if (isSolution(state)) { + // 記錄解 + recordSolution(state, res); + } + // 走訪所有選擇 + for (TreeNode? choice in choices) { + // 剪枝:檢查選擇是否合法 + if (isValid(state, choice)) { + // 嘗試:做出選擇,更新狀態 + makeChoice(state, choice); + // 進行下一輪選擇 + backtrack(state, [choice!.left, choice.right], res); + // 回退:撤銷選擇,恢復到之前的狀態 + undoChoice(state, choice); + } + } + } + ``` + +=== "Rust" + + ```rust title="preorder_traversal_iii_template.rs" + /* 判斷當前狀態是否為解 */ + fn is_solution(state: &mut Vec>>) -> bool { + return !state.is_empty() && state.get(state.len() - 1).unwrap().borrow().val == 7; + } + + /* 記錄解 */ + fn record_solution( + state: &mut Vec>>, + res: &mut Vec>>>, + ) { + res.push(state.clone()); + } + + /* 判斷在當前狀態下,該選擇是否合法 */ + fn is_valid(_: &mut Vec>>, choice: Rc>) -> bool { + return choice.borrow().val != 3; + } + + /* 更新狀態 */ + fn make_choice(state: &mut Vec>>, choice: Rc>) { + state.push(choice); + } + + /* 恢復狀態 */ + fn undo_choice(state: &mut Vec>>, _: Rc>) { + state.remove(state.len() - 1); + } + + /* 回溯演算法:例題三 */ + fn backtrack( + state: &mut Vec>>, + choices: &mut Vec>>, + res: &mut Vec>>>, + ) { + // 檢查是否為解 + if is_solution(state) { + // 記錄解 + record_solution(state, res); + } + // 走訪所有選擇 + for choice in choices { + // 剪枝:檢查選擇是否合法 + if is_valid(state, choice.clone()) { + // 嘗試:做出選擇,更新狀態 + make_choice(state, choice.clone()); + // 進行下一輪選擇 + backtrack( + state, + &mut vec![ + choice.borrow().left.clone().unwrap(), + choice.borrow().right.clone().unwrap(), + ], + res, + ); + // 回退:撤銷選擇,恢復到之前的狀態 + undo_choice(state, choice.clone()); + } + } + } + ``` + +=== "C" + + ```c title="preorder_traversal_iii_template.c" + /* 判斷當前狀態是否為解 */ + bool isSolution(void) { + return pathSize > 0 && path[pathSize - 1]->val == 7; + } + + /* 記錄解 */ + void recordSolution(void) { + for (int i = 0; i < pathSize; i++) { + res[resSize][i] = path[i]; + } + resSize++; + } + + /* 判斷在當前狀態下,該選擇是否合法 */ + bool isValid(TreeNode *choice) { + return choice != NULL && choice->val != 3; + } + + /* 更新狀態 */ + void makeChoice(TreeNode *choice) { + path[pathSize++] = choice; + } + + /* 恢復狀態 */ + void undoChoice(void) { + pathSize--; + } + + /* 回溯演算法:例題三 */ + void backtrack(TreeNode *choices[2]) { + // 檢查是否為解 + if (isSolution()) { + // 記錄解 + recordSolution(); + } + // 走訪所有選擇 + for (int i = 0; i < 2; i++) { + TreeNode *choice = choices[i]; + // 剪枝:檢查選擇是否合法 + if (isValid(choice)) { + // 嘗試:做出選擇,更新狀態 + makeChoice(choice); + // 進行下一輪選擇 + TreeNode *nextChoices[2] = {choice->left, choice->right}; + backtrack(nextChoices); + // 回退:撤銷選擇,恢復到之前的狀態 + undoChoice(); + } + } + } + ``` + +=== "Kotlin" + + ```kotlin title="preorder_traversal_iii_template.kt" + /* 判斷當前狀態是否為解 */ + fun isSolution(state: List): Boolean { + return state.isNotEmpty() && state[state.size - 1]?.value == 7 + } + + /* 記錄解 */ + fun recordSolution(state: MutableList?, res: MutableList?>) { + res.add(state?.let { ArrayList(it) }) + } + + /* 判斷在當前狀態下,該選擇是否合法 */ + fun isValid(state: List?, choice: TreeNode?): Boolean { + return choice != null && choice.value != 3 + } + + /* 更新狀態 */ + fun makeChoice(state: MutableList, choice: TreeNode?) { + state.add(choice) + } + + /* 恢復狀態 */ + fun undoChoice(state: MutableList, choice: TreeNode?) { + state.removeLast() + } + + /* 回溯演算法:例題三 */ + fun backtrack( + state: MutableList, + choices: List, + res: MutableList?> + ) { + // 檢查是否為解 + if (isSolution(state)) { + // 記錄解 + recordSolution(state, res) + } + // 走訪所有選擇 + for (choice in choices) { + // 剪枝:檢查選擇是否合法 + if (isValid(state, choice)) { + // 嘗試:做出選擇,更新狀態 + makeChoice(state, choice) + // 進行下一輪選擇 + backtrack(state, listOf(choice!!.left, choice.right), res) + // 回退:撤銷選擇,恢復到之前的狀態 + undoChoice(state, choice) + } + } + } + ``` + +=== "Ruby" + + ```ruby title="preorder_traversal_iii_template.rb" + [class]{}-[func]{is_solution} + + [class]{}-[func]{record_solution} + + [class]{}-[func]{is_valid} + + [class]{}-[func]{make_choice} + + [class]{}-[func]{undo_choice} + + [class]{}-[func]{backtrack} + ``` + +=== "Zig" + + ```zig title="preorder_traversal_iii_template.zig" + [class]{}-[func]{isSolution} + + [class]{}-[func]{recordSolution} + + [class]{}-[func]{isValid} + + [class]{}-[func]{makeChoice} + + [class]{}-[func]{undoChoice} + + [class]{}-[func]{backtrack} + ``` + +??? pythontutor "視覺化執行" + +
+ + +根據題意,我們在找到值為 $7$ 的節點後應該繼續搜尋,**因此需要將記錄解之後的 `return` 語句刪除**。圖 13-4 對比了保留或刪除 `return` 語句的搜尋過程。 + +![保留與刪除 return 的搜尋過程對比](backtracking_algorithm.assets/backtrack_remove_return_or_not.png){ class="animation-figure" } + +

圖 13-4   保留與刪除 return 的搜尋過程對比

+ +相比基於前序走訪的程式碼實現,基於回溯演算法框架的程式碼實現雖然顯得囉唆,但通用性更好。實際上,**許多回溯問題可以在該框架下解決**。我們只需根據具體問題來定義 `state` 和 `choices` ,並實現框架中的各個方法即可。 + +## 13.1.4   常用術語 + +為了更清晰地分析演算法問題,我們總結一下回溯演算法中常用術語的含義,並對照例題三給出對應示例,如表 13-1 所示。 + +

表 13-1   常見的回溯演算法術語

+ +
+ +| 名詞 | 定義 | 例題三 | +| ---------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------- | +| 解(solution) | 解是滿足問題特定條件的答案,可能有一個或多個 | 根節點到節點 $7$ 的滿足約束條件的所有路徑 | +| 約束條件(constraint) | 約束條件是問題中限制解的可行性的條件,通常用於剪枝 | 路徑中不包含節點 $3$ | +| 狀態(state) | 狀態表示問題在某一時刻的情況,包括已經做出的選擇 | 當前已訪問的節點路徑,即 `path` 節點串列 | +| 嘗試(attempt) | 嘗試是根據可用選擇來探索解空間的過程,包括做出選擇,更新狀態,檢查是否為解 | 遞迴訪問左(右)子節點,將節點新增進 `path` ,判斷節點的值是否為 $7$ | +| 回退(backtracking) | 回退指遇到不滿足約束條件的狀態時,撤銷前面做出的選擇,回到上一個狀態 | 當越過葉節點、結束節點訪問、遇到值為 $3$ 的節點時終止搜尋,函式返回 | +| 剪枝(pruning) | 剪枝是根據問題特性和約束條件避免無意義的搜尋路徑的方法,可提高搜尋效率 | 當遇到值為 $3$ 的節點時,則不再繼續搜尋 | + +
+ +!!! tip + + 問題、解、狀態等概念是通用的,在分治、回溯、動態規劃、貪婪等演算法中都有涉及。 + +## 13.1.5   優點與侷限性 + +回溯演算法本質上是一種深度優先搜尋演算法,它嘗試所有可能的解決方案直到找到滿足條件的解。這種方法的優點在於能夠找到所有可能的解決方案,而且在合理的剪枝操作下,具有很高的效率。 + +然而,在處理大規模或者複雜問題時,**回溯演算法的執行效率可能難以接受**。 + +- **時間**:回溯演算法通常需要走訪狀態空間的所有可能,時間複雜度可以達到指數階或階乘階。 +- **空間**:在遞迴呼叫中需要儲存當前的狀態(例如路徑、用於剪枝的輔助變數等),當深度很大時,空間需求可能會變得很大。 + +即便如此,**回溯演算法仍然是某些搜尋問題和約束滿足問題的最佳解決方案**。對於這些問題,由於無法預測哪些選擇可生成有效的解,因此我們必須對所有可能的選擇進行走訪。在這種情況下,**關鍵是如何最佳化效率**,常見的效率最佳化方法有兩種。 + +- **剪枝**:避免搜尋那些肯定不會產生解的路徑,從而節省時間和空間。 +- **啟發式搜尋**:在搜尋過程中引入一些策略或者估計值,從而優先搜尋最有可能產生有效解的路徑。 + +## 13.1.6   回溯典型例題 + +回溯演算法可用於解決許多搜尋問題、約束滿足問題和組合最佳化問題。 + +**搜尋問題**:這類問題的目標是找到滿足特定條件的解決方案。 + +- 全排列問題:給定一個集合,求出其所有可能的排列組合。 +- 子集和問題:給定一個集合和一個目標和,找到集合中所有和為目標和的子集。 +- 河內塔問題:給定三根柱子和一系列大小不同的圓盤,要求將所有圓盤從一根柱子移動到另一根柱子,每次只能移動一個圓盤,且不能將大圓盤放在小圓盤上。 + +**約束滿足問題**:這類問題的目標是找到滿足所有約束條件的解。 + +- $n$ 皇后:在 $n \times n$ 的棋盤上放置 $n$ 個皇后,使得它們互不攻擊。 +- 數獨:在 $9 \times 9$ 的網格中填入數字 $1$ ~ $9$ ,使得每行、每列和每個 $3 \times 3$ 子網格中的數字不重複。 +- 圖著色問題:給定一個無向圖,用最少的顏色給圖的每個頂點著色,使得相鄰頂點顏色不同。 + +**組合最佳化問題**:這類問題的目標是在一個組合空間中找到滿足某些條件的最優解。 + +- 0-1 背包問題:給定一組物品和一個背包,每個物品有一定的價值和重量,要求在背包容量限制內,選擇物品使得總價值最大。 +- 旅行商問題:在一個圖中,從一個點出發,訪問所有其他點恰好一次後返回起點,求最短路徑。 +- 最大團問題:給定一個無向圖,找到最大的完全子圖,即子圖中的任意兩個頂點之間都有邊相連。 + +請注意,對於許多組合最佳化問題,回溯不是最優解決方案。 + +- 0-1 背包問題通常使用動態規劃解決,以達到更高的時間效率。 +- 旅行商是一個著名的 NP-Hard 問題,常用解法有遺傳演算法和蟻群演算法等。 +- 最大團問題是圖論中的一個經典問題,可用貪婪演算法等啟發式演算法來解決。 diff --git a/zh-Hant/docs/chapter_backtracking/index.md b/zh-Hant/docs/chapter_backtracking/index.md new file mode 100644 index 000000000..d11076166 --- /dev/null +++ b/zh-Hant/docs/chapter_backtracking/index.md @@ -0,0 +1,22 @@ +--- +comments: true +icon: material/map-marker-path +--- + +# 第 13 章   回溯 + +![回溯](../assets/covers/chapter_backtracking.jpg){ 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/) diff --git a/zh-Hant/docs/chapter_backtracking/n_queens_problem.md b/zh-Hant/docs/chapter_backtracking/n_queens_problem.md new file mode 100644 index 000000000..4ae1cba5b --- /dev/null +++ b/zh-Hant/docs/chapter_backtracking/n_queens_problem.md @@ -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` 。 + +![4 皇后問題的解](n_queens_problem.assets/solution_4_queens.png){ class="animation-figure" } + +

圖 13-15   4 皇后問題的解

+ +圖 13-16 展示了本題的三個約束條件:**多個皇后不能在同一行、同一列、同一條對角線上**。值得注意的是,對角線分為主對角線 `\` 和次對角線 `/` 兩種。 + +![n 皇后問題的約束條件](n_queens_problem.assets/n_queens_constraints.png){ class="animation-figure" } + +

圖 13-16   n 皇后問題的約束條件

+ +### 1.   逐行放置策略 + +皇后的數量和棋盤的行數都為 $n$ ,因此我們容易得到一個推論:**棋盤每行都允許且只允許放置一個皇后**。 + +也就是說,我們可以採取逐行放置策略:從第一行開始,在每行放置一個皇后,直至最後一行結束。 + +圖 13-17 所示為 $4$ 皇后問題的逐行放置過程。受畫幅限制,圖 13-17 僅展開了第一行的其中一個搜尋分支,並且將不滿足列約束和對角線約束的方案都進行了剪枝。 + +![逐行放置策略](n_queens_problem.assets/n_queens_placing.png){ class="animation-figure" } + +

圖 13-17   逐行放置策略

+ +從本質上看,**逐行放置策略起到了剪枝的作用**,它避免了同一行出現多個皇后的所有搜尋分支。 + +### 2.   列與對角線剪枝 + +為了滿足列約束,我們可以利用一個長度為 $n$ 的布林型陣列 `cols` 記錄每一列是否有皇后。在每次決定放置前,我們透過 `cols` 將已有皇后的列進行剪枝,並在回溯中動態更新 `cols` 的狀態。 + +那麼,如何處理對角線約束呢?設棋盤中某個格子的行列索引為 $(row, col)$ ,選定矩陣中的某條主對角線,我們發現該對角線上所有格子的行索引減列索引都相等,**即對角線上所有格子的 $row - col$ 為恆定值**。 + +也就是說,如果兩個格子滿足 $row_1 - col_1 = row_2 - col_2$ ,則它們一定處在同一條主對角線上。利用該規律,我們可以藉助圖 13-18 所示的陣列 `diags1` 記錄每條主對角線上是否有皇后。 + +同理,**次對角線上的所有格子的 $row + col$ 是恆定值**。我們同樣也可以藉助陣列 `diags2` 來處理次對角線約束。 + +![處理列約束和對角線約束](n_queens_problem.assets/n_queens_cols_diagonals.png){ class="animation-figure" } + +

圖 13-18   處理列約束和對角線約束

+ +### 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> &state, vector>> &res, vector &cols, + vector &diags1, vector &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>> nQueens(int n) { + // 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位 + vector> state(n, vector(n, "#")); + vector cols(n, false); // 記錄列是否有皇后 + vector diags1(2 * n - 1, false); // 記錄主對角線上是否有皇后 + vector diags2(2 * n - 1, false); // 記錄次對角線上是否有皇后 + vector>> 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> state, List>> res, + boolean[] cols, boolean[] diags1, boolean[] diags2) { + // 當放置完所有行時,記錄解 + if (row == n) { + List> copyState = new ArrayList<>(); + for (List 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>> nQueens(int n) { + // 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位 + List> state = new ArrayList<>(); + for (int i = 0; i < n; i++) { + List 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>> 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> state, List>> res, + bool[] cols, bool[] diags1, bool[] diags2) { + // 當放置完所有行時,記錄解 + if (row == n) { + List> copyState = []; + foreach (List sRow in state) { + copyState.Add(new List(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>> NQueens(int n) { + // 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位 + List> state = []; + for (int i = 0; i < n; i++) { + List 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>> 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> state, + List>> res, + List cols, + List diags1, + List diags2, + ) { + // 當放置完所有行時,記錄解 + if (row == n) { + List> copyState = []; + for (List 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>> nQueens(int n) { + // 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位 + List> state = List.generate(n, (index) => List.filled(n, "#")); + List cols = List.filled(n, false); // 記錄列是否有皇后 + List diags1 = List.filled(2 * n - 1, false); // 記錄主對角線上是否有皇后 + List diags2 = List.filled(2 * n - 1, false); // 記錄次對角線上是否有皇后 + List>> 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>, + res: &mut Vec>>, + cols: &mut [bool], + diags1: &mut [bool], + diags2: &mut [bool], + ) { + // 當放置完所有行時,記錄解 + if row == n { + let mut copy_state: Vec> = 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>> { + // 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位 + let mut state: Vec> = Vec::new(); + for _ in 0..n { + let mut row: Vec = 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::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>, + res: MutableList>?>, + cols: BooleanArray, + diags1: BooleanArray, + diags2: BooleanArray + ) { + // 當放置完所有行時,記錄解 + if (row == n) { + val copyState: MutableList> = ArrayList() + for (sRow in state) { + copyState.add(ArrayList(sRow)) + } + res.add(copyState) + return + } + // 走訪所有列 + for (col in 0..>?> { + // 初始化 n*n 大小的棋盤,其中 'Q' 代表皇后,'#' 代表空位 + val state: MutableList> = ArrayList() + for (i in 0.. = ArrayList() + for (j in 0..>?> = 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 "視覺化執行" + +
+ + +逐行放置 $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)$** 。 diff --git a/zh-Hant/docs/chapter_backtracking/permutations_problem.md b/zh-Hant/docs/chapter_backtracking/permutations_problem.md new file mode 100644 index 000000000..d3a835003 --- /dev/null +++ b/zh-Hant/docs/chapter_backtracking/permutations_problem.md @@ -0,0 +1,1068 @@ +--- +comments: true +--- + +# 13.2   全排列問題 + +全排列問題是回溯演算法的一個典型應用。它的定義是在給定一個集合(如一個陣列或字串)的情況下,找出其中元素的所有可能的排列。 + +表 13-2 列舉了幾個示例資料,包括輸入陣列和對應的所有排列。 + +

表 13-2   全排列示例

+ +
+ +| 輸入陣列 | 所有排列 | +| :---------- | :----------------------------------------------------------------- | +| $[1]$ | $[1]$ | +| $[1, 2]$ | $[1, 2], [2, 1]$ | +| $[1, 2, 3]$ | $[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]$ | + +
+ +## 13.2.1   無相等元素的情況 + +!!! question + + 輸入一個整數陣列,其中不包含重複元素,返回所有可能的排列。 + +從回溯演算法的角度看,**我們可以把生成排列的過程想象成一系列選擇的結果**。假設輸入陣列為 $[1, 2, 3]$ ,如果我們先選擇 $1$ ,再選擇 $3$ ,最後選擇 $2$ ,則獲得排列 $[1, 3, 2]$ 。回退表示撤銷一個選擇,之後繼續嘗試其他選擇。 + +從回溯程式碼的角度看,候選集合 `choices` 是輸入陣列中的所有元素,狀態 `state` 是直至目前已被選擇的元素。請注意,每個元素只允許被選擇一次,**因此 `state` 中的所有元素都應該是唯一的**。 + +如圖 13-5 所示,我們可以將搜尋過程展開成一棵遞迴樹,樹中的每個節點代表當前狀態 `state` 。從根節點開始,經過三輪選擇後到達葉節點,每個葉節點都對應一個排列。 + +![全排列的遞迴樹](permutations_problem.assets/permutations_i.png){ class="animation-figure" } + +

圖 13-5   全排列的遞迴樹

+ +### 1.   重複選擇剪枝 + +為了實現每個元素只被選擇一次,我們考慮引入一個布林型陣列 `selected` ,其中 `selected[i]` 表示 `choices[i]` 是否已被選擇,並基於它實現以下剪枝操作。 + +- 在做出選擇 `choice[i]` 後,我們就將 `selected[i]` 賦值為 $\text{True}$ ,代表它已被選擇。 +- 走訪選擇串列 `choices` 時,跳過所有已被選擇的節點,即剪枝。 + +如圖 13-6 所示,假設我們第一輪選擇 1 ,第二輪選擇 3 ,第三輪選擇 2 ,則需要在第二輪剪掉元素 1 的分支,在第三輪剪掉元素 1 和元素 3 的分支。 + +![全排列剪枝示例](permutations_problem.assets/permutations_i_pruning.png){ class="animation-figure" } + +

圖 13-6   全排列剪枝示例

+ +觀察圖 13-6 發現,該剪枝操作將搜尋空間大小從 $O(n^n)$ 減小至 $O(n!)$ 。 + +### 2.   程式碼實現 + +想清楚以上資訊之後,我們就可以在框架程式碼中做“完形填空”了。為了縮短整體程式碼,我們不單獨實現框架程式碼中的各個函式,而是將它們展開在 `backtrack()` 函式中: + +=== "Python" + + ```python title="permutations_i.py" + def backtrack( + state: list[int], choices: list[int], selected: list[bool], res: list[list[int]] + ): + """回溯演算法:全排列 I""" + # 當狀態長度等於元素數量時,記錄解 + if len(state) == len(choices): + res.append(list(state)) + return + # 走訪所有選擇 + for i, choice in enumerate(choices): + # 剪枝:不允許重複選擇元素 + if not selected[i]: + # 嘗試:做出選擇,更新狀態 + selected[i] = True + state.append(choice) + # 進行下一輪選擇 + backtrack(state, choices, selected, res) + # 回退:撤銷選擇,恢復到之前的狀態 + selected[i] = False + state.pop() + + def permutations_i(nums: list[int]) -> list[list[int]]: + """全排列 I""" + res = [] + backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res) + return res + ``` + +=== "C++" + + ```cpp title="permutations_i.cpp" + /* 回溯演算法:全排列 I */ + void backtrack(vector &state, const vector &choices, vector &selected, vector> &res) { + // 當狀態長度等於元素數量時,記錄解 + if (state.size() == choices.size()) { + res.push_back(state); + return; + } + // 走訪所有選擇 + for (int i = 0; i < choices.size(); i++) { + int choice = choices[i]; + // 剪枝:不允許重複選擇元素 + if (!selected[i]) { + // 嘗試:做出選擇,更新狀態 + selected[i] = true; + state.push_back(choice); + // 進行下一輪選擇 + backtrack(state, choices, selected, res); + // 回退:撤銷選擇,恢復到之前的狀態 + selected[i] = false; + state.pop_back(); + } + } + } + + /* 全排列 I */ + vector> permutationsI(vector nums) { + vector state; + vector selected(nums.size(), false); + vector> res; + backtrack(state, nums, selected, res); + return res; + } + ``` + +=== "Java" + + ```java title="permutations_i.java" + /* 回溯演算法:全排列 I */ + void backtrack(List state, int[] choices, boolean[] selected, List> res) { + // 當狀態長度等於元素數量時,記錄解 + if (state.size() == choices.length) { + res.add(new ArrayList(state)); + return; + } + // 走訪所有選擇 + for (int i = 0; i < choices.length; i++) { + int choice = choices[i]; + // 剪枝:不允許重複選擇元素 + if (!selected[i]) { + // 嘗試:做出選擇,更新狀態 + selected[i] = true; + state.add(choice); + // 進行下一輪選擇 + backtrack(state, choices, selected, res); + // 回退:撤銷選擇,恢復到之前的狀態 + selected[i] = false; + state.remove(state.size() - 1); + } + } + } + + /* 全排列 I */ + List> permutationsI(int[] nums) { + List> res = new ArrayList>(); + backtrack(new ArrayList(), nums, new boolean[nums.length], res); + return res; + } + ``` + +=== "C#" + + ```csharp title="permutations_i.cs" + /* 回溯演算法:全排列 I */ + void Backtrack(List state, int[] choices, bool[] selected, List> res) { + // 當狀態長度等於元素數量時,記錄解 + if (state.Count == choices.Length) { + res.Add(new List(state)); + return; + } + // 走訪所有選擇 + for (int i = 0; i < choices.Length; i++) { + int choice = choices[i]; + // 剪枝:不允許重複選擇元素 + if (!selected[i]) { + // 嘗試:做出選擇,更新狀態 + selected[i] = true; + state.Add(choice); + // 進行下一輪選擇 + Backtrack(state, choices, selected, res); + // 回退:撤銷選擇,恢復到之前的狀態 + selected[i] = false; + state.RemoveAt(state.Count - 1); + } + } + } + + /* 全排列 I */ + List> PermutationsI(int[] nums) { + List> res = []; + Backtrack([], nums, new bool[nums.Length], res); + return res; + } + ``` + +=== "Go" + + ```go title="permutations_i.go" + /* 回溯演算法:全排列 I */ + func backtrackI(state *[]int, choices *[]int, selected *[]bool, res *[][]int) { + // 當狀態長度等於元素數量時,記錄解 + if len(*state) == len(*choices) { + newState := append([]int{}, *state...) + *res = append(*res, newState) + } + // 走訪所有選擇 + for i := 0; i < len(*choices); i++ { + choice := (*choices)[i] + // 剪枝:不允許重複選擇元素 + if !(*selected)[i] { + // 嘗試:做出選擇,更新狀態 + (*selected)[i] = true + *state = append(*state, choice) + // 進行下一輪選擇 + backtrackI(state, choices, selected, res) + // 回退:撤銷選擇,恢復到之前的狀態 + (*selected)[i] = false + *state = (*state)[:len(*state)-1] + } + } + } + + /* 全排列 I */ + func permutationsI(nums []int) [][]int { + res := make([][]int, 0) + state := make([]int, 0) + selected := make([]bool, len(nums)) + backtrackI(&state, &nums, &selected, &res) + return res + } + ``` + +=== "Swift" + + ```swift title="permutations_i.swift" + /* 回溯演算法:全排列 I */ + func backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) { + // 當狀態長度等於元素數量時,記錄解 + if state.count == choices.count { + res.append(state) + return + } + // 走訪所有選擇 + for (i, choice) in choices.enumerated() { + // 剪枝:不允許重複選擇元素 + if !selected[i] { + // 嘗試:做出選擇,更新狀態 + selected[i] = true + state.append(choice) + // 進行下一輪選擇 + backtrack(state: &state, choices: choices, selected: &selected, res: &res) + // 回退:撤銷選擇,恢復到之前的狀態 + selected[i] = false + state.removeLast() + } + } + } + + /* 全排列 I */ + func permutationsI(nums: [Int]) -> [[Int]] { + var state: [Int] = [] + var selected = Array(repeating: false, count: nums.count) + var res: [[Int]] = [] + backtrack(state: &state, choices: nums, selected: &selected, res: &res) + return res + } + ``` + +=== "JS" + + ```javascript title="permutations_i.js" + /* 回溯演算法:全排列 I */ + function backtrack(state, choices, selected, res) { + // 當狀態長度等於元素數量時,記錄解 + if (state.length === choices.length) { + res.push([...state]); + return; + } + // 走訪所有選擇 + choices.forEach((choice, i) => { + // 剪枝:不允許重複選擇元素 + if (!selected[i]) { + // 嘗試:做出選擇,更新狀態 + selected[i] = true; + state.push(choice); + // 進行下一輪選擇 + backtrack(state, choices, selected, res); + // 回退:撤銷選擇,恢復到之前的狀態 + selected[i] = false; + state.pop(); + } + }); + } + + /* 全排列 I */ + function permutationsI(nums) { + const res = []; + backtrack([], nums, Array(nums.length).fill(false), res); + return res; + } + ``` + +=== "TS" + + ```typescript title="permutations_i.ts" + /* 回溯演算法:全排列 I */ + function backtrack( + state: number[], + choices: number[], + selected: boolean[], + res: number[][] + ): void { + // 當狀態長度等於元素數量時,記錄解 + if (state.length === choices.length) { + res.push([...state]); + return; + } + // 走訪所有選擇 + choices.forEach((choice, i) => { + // 剪枝:不允許重複選擇元素 + if (!selected[i]) { + // 嘗試:做出選擇,更新狀態 + selected[i] = true; + state.push(choice); + // 進行下一輪選擇 + backtrack(state, choices, selected, res); + // 回退:撤銷選擇,恢復到之前的狀態 + selected[i] = false; + state.pop(); + } + }); + } + + /* 全排列 I */ + function permutationsI(nums: number[]): number[][] { + const res: number[][] = []; + backtrack([], nums, Array(nums.length).fill(false), res); + return res; + } + ``` + +=== "Dart" + + ```dart title="permutations_i.dart" + /* 回溯演算法:全排列 I */ + void backtrack( + List state, + List choices, + List selected, + List> res, + ) { + // 當狀態長度等於元素數量時,記錄解 + if (state.length == choices.length) { + res.add(List.from(state)); + return; + } + // 走訪所有選擇 + for (int i = 0; i < choices.length; i++) { + int choice = choices[i]; + // 剪枝:不允許重複選擇元素 + if (!selected[i]) { + // 嘗試:做出選擇,更新狀態 + selected[i] = true; + state.add(choice); + // 進行下一輪選擇 + backtrack(state, choices, selected, res); + // 回退:撤銷選擇,恢復到之前的狀態 + selected[i] = false; + state.removeLast(); + } + } + } + + /* 全排列 I */ + List> permutationsI(List nums) { + List> res = []; + backtrack([], nums, List.filled(nums.length, false), res); + return res; + } + ``` + +=== "Rust" + + ```rust title="permutations_i.rs" + /* 回溯演算法:全排列 I */ + fn backtrack(mut state: Vec, choices: &[i32], selected: &mut [bool], res: &mut Vec>) { + // 當狀態長度等於元素數量時,記錄解 + if state.len() == choices.len() { + res.push(state); + return; + } + // 走訪所有選擇 + for i in 0..choices.len() { + let choice = choices[i]; + // 剪枝:不允許重複選擇元素 + if !selected[i] { + // 嘗試:做出選擇,更新狀態 + selected[i] = true; + state.push(choice); + // 進行下一輪選擇 + backtrack(state.clone(), choices, selected, res); + // 回退:撤銷選擇,恢復到之前的狀態 + selected[i] = false; + state.remove(state.len() - 1); + } + } + } + + /* 全排列 I */ + fn permutations_i(nums: &mut [i32]) -> Vec> { + let mut res = Vec::new(); // 狀態(子集) + backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res); + res + } + ``` + +=== "C" + + ```c title="permutations_i.c" + /* 回溯演算法:全排列 I */ + void backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) { + // 當狀態長度等於元素數量時,記錄解 + if (stateSize == choicesSize) { + res[*resSize] = (int *)malloc(choicesSize * sizeof(int)); + for (int i = 0; i < choicesSize; i++) { + res[*resSize][i] = state[i]; + } + (*resSize)++; + return; + } + // 走訪所有選擇 + for (int i = 0; i < choicesSize; i++) { + int choice = choices[i]; + // 剪枝:不允許重複選擇元素 + if (!selected[i]) { + // 嘗試:做出選擇,更新狀態 + selected[i] = true; + state[stateSize] = choice; + // 進行下一輪選擇 + backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize); + // 回退:撤銷選擇,恢復到之前的狀態 + selected[i] = false; + } + } + } + + /* 全排列 I */ + int **permutationsI(int *nums, int numsSize, int *returnSize) { + int *state = (int *)malloc(numsSize * sizeof(int)); + bool *selected = (bool *)malloc(numsSize * sizeof(bool)); + for (int i = 0; i < numsSize; i++) { + selected[i] = false; + } + int **res = (int **)malloc(MAX_SIZE * sizeof(int *)); + *returnSize = 0; + + backtrack(state, 0, nums, numsSize, selected, res, returnSize); + + free(state); + free(selected); + + return res; + } + ``` + +=== "Kotlin" + + ```kotlin title="permutations_i.kt" + /* 回溯演算法:全排列 I */ + fun backtrack( + state: MutableList, + choices: IntArray, + selected: BooleanArray, + res: MutableList?> + ) { + // 當狀態長度等於元素數量時,記錄解 + if (state.size == choices.size) { + res.add(ArrayList(state)) + return + } + // 走訪所有選擇 + for (i in choices.indices) { + val choice = choices[i] + // 剪枝:不允許重複選擇元素 + if (!selected[i]) { + // 嘗試:做出選擇,更新狀態 + selected[i] = true + state.add(choice) + // 進行下一輪選擇 + backtrack(state, choices, selected, res) + // 回退:撤銷選擇,恢復到之前的狀態 + selected[i] = false + state.removeAt(state.size - 1) + } + } + } + + /* 全排列 I */ + fun permutationsI(nums: IntArray): List?> { + val res: MutableList?> = ArrayList() + backtrack(ArrayList(), nums, BooleanArray(nums.size), res) + return res + } + ``` + +=== "Ruby" + + ```ruby title="permutations_i.rb" + [class]{}-[func]{backtrack} + + [class]{}-[func]{permutations_i} + ``` + +=== "Zig" + + ```zig title="permutations_i.zig" + [class]{}-[func]{backtrack} + + [class]{}-[func]{permutationsI} + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 13.2.2   考慮相等元素的情況 + +!!! question + + 輸入一個整數陣列,**陣列中可能包含重複元素**,返回所有不重複的排列。 + +假設輸入陣列為 $[1, 1, 2]$ 。為了方便區分兩個重複元素 $1$ ,我們將第二個 $1$ 記為 $\hat{1}$ 。 + +如圖 13-7 所示,上述方法生成的排列有一半是重複的。 + +![重複排列](permutations_problem.assets/permutations_ii.png){ class="animation-figure" } + +

圖 13-7   重複排列

+ +那麼如何去除重複的排列呢?最直接地,考慮藉助一個雜湊表,直接對排列結果進行去重。然而這樣做不夠優雅,**因為生成重複排列的搜尋分支沒有必要,應當提前識別並剪枝**,這樣可以進一步提升演算法效率。 + +### 1.   相等元素剪枝 + +觀察圖 13-8 ,在第一輪中,選擇 $1$ 或選擇 $\hat{1}$ 是等價的,在這兩個選擇之下生成的所有排列都是重複的。因此應該把 $\hat{1}$ 剪枝。 + +同理,在第一輪選擇 $2$ 之後,第二輪選擇中的 $1$ 和 $\hat{1}$ 也會產生重複分支,因此也應將第二輪的 $\hat{1}$ 剪枝。 + +從本質上看,**我們的目標是在某一輪選擇中,保證多個相等的元素僅被選擇一次**。 + +![重複排列剪枝](permutations_problem.assets/permutations_ii_pruning.png){ class="animation-figure" } + +

圖 13-8   重複排列剪枝

+ +### 2.   程式碼實現 + +在上一題的程式碼的基礎上,我們考慮在每一輪選擇中開啟一個雜湊表 `duplicated` ,用於記錄該輪中已經嘗試過的元素,並將重複元素剪枝: + +=== "Python" + + ```python title="permutations_ii.py" + def backtrack( + state: list[int], choices: list[int], selected: list[bool], res: list[list[int]] + ): + """回溯演算法:全排列 II""" + # 當狀態長度等於元素數量時,記錄解 + if len(state) == len(choices): + res.append(list(state)) + return + # 走訪所有選擇 + duplicated = set[int]() + for i, choice in enumerate(choices): + # 剪枝:不允許重複選擇元素 且 不允許重複選擇相等元素 + if not selected[i] and choice not in duplicated: + # 嘗試:做出選擇,更新狀態 + duplicated.add(choice) # 記錄選擇過的元素值 + selected[i] = True + state.append(choice) + # 進行下一輪選擇 + backtrack(state, choices, selected, res) + # 回退:撤銷選擇,恢復到之前的狀態 + selected[i] = False + state.pop() + + def permutations_ii(nums: list[int]) -> list[list[int]]: + """全排列 II""" + res = [] + backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res) + return res + ``` + +=== "C++" + + ```cpp title="permutations_ii.cpp" + /* 回溯演算法:全排列 II */ + void backtrack(vector &state, const vector &choices, vector &selected, vector> &res) { + // 當狀態長度等於元素數量時,記錄解 + if (state.size() == choices.size()) { + res.push_back(state); + return; + } + // 走訪所有選擇 + unordered_set duplicated; + for (int i = 0; i < choices.size(); i++) { + int choice = choices[i]; + // 剪枝:不允許重複選擇元素 且 不允許重複選擇相等元素 + if (!selected[i] && duplicated.find(choice) == duplicated.end()) { + // 嘗試:做出選擇,更新狀態 + duplicated.emplace(choice); // 記錄選擇過的元素值 + selected[i] = true; + state.push_back(choice); + // 進行下一輪選擇 + backtrack(state, choices, selected, res); + // 回退:撤銷選擇,恢復到之前的狀態 + selected[i] = false; + state.pop_back(); + } + } + } + + /* 全排列 II */ + vector> permutationsII(vector nums) { + vector state; + vector selected(nums.size(), false); + vector> res; + backtrack(state, nums, selected, res); + return res; + } + ``` + +=== "Java" + + ```java title="permutations_ii.java" + /* 回溯演算法:全排列 II */ + void backtrack(List state, int[] choices, boolean[] selected, List> res) { + // 當狀態長度等於元素數量時,記錄解 + if (state.size() == choices.length) { + res.add(new ArrayList(state)); + return; + } + // 走訪所有選擇 + Set duplicated = new HashSet(); + for (int i = 0; i < choices.length; i++) { + int choice = choices[i]; + // 剪枝:不允許重複選擇元素 且 不允許重複選擇相等元素 + if (!selected[i] && !duplicated.contains(choice)) { + // 嘗試:做出選擇,更新狀態 + duplicated.add(choice); // 記錄選擇過的元素值 + selected[i] = true; + state.add(choice); + // 進行下一輪選擇 + backtrack(state, choices, selected, res); + // 回退:撤銷選擇,恢復到之前的狀態 + selected[i] = false; + state.remove(state.size() - 1); + } + } + } + + /* 全排列 II */ + List> permutationsII(int[] nums) { + List> res = new ArrayList>(); + backtrack(new ArrayList(), nums, new boolean[nums.length], res); + return res; + } + ``` + +=== "C#" + + ```csharp title="permutations_ii.cs" + /* 回溯演算法:全排列 II */ + void Backtrack(List state, int[] choices, bool[] selected, List> res) { + // 當狀態長度等於元素數量時,記錄解 + if (state.Count == choices.Length) { + res.Add(new List(state)); + return; + } + // 走訪所有選擇 + HashSet duplicated = []; + for (int i = 0; i < choices.Length; i++) { + int choice = choices[i]; + // 剪枝:不允許重複選擇元素 且 不允許重複選擇相等元素 + if (!selected[i] && !duplicated.Contains(choice)) { + // 嘗試:做出選擇,更新狀態 + duplicated.Add(choice); // 記錄選擇過的元素值 + selected[i] = true; + state.Add(choice); + // 進行下一輪選擇 + Backtrack(state, choices, selected, res); + // 回退:撤銷選擇,恢復到之前的狀態 + selected[i] = false; + state.RemoveAt(state.Count - 1); + } + } + } + + /* 全排列 II */ + List> PermutationsII(int[] nums) { + List> res = []; + Backtrack([], nums, new bool[nums.Length], res); + return res; + } + ``` + +=== "Go" + + ```go title="permutations_ii.go" + /* 回溯演算法:全排列 II */ + func backtrackII(state *[]int, choices *[]int, selected *[]bool, res *[][]int) { + // 當狀態長度等於元素數量時,記錄解 + if len(*state) == len(*choices) { + newState := append([]int{}, *state...) + *res = append(*res, newState) + } + // 走訪所有選擇 + duplicated := make(map[int]struct{}, 0) + for i := 0; i < len(*choices); i++ { + choice := (*choices)[i] + // 剪枝:不允許重複選擇元素 且 不允許重複選擇相等元素 + if _, ok := duplicated[choice]; !ok && !(*selected)[i] { + // 嘗試:做出選擇,更新狀態 + // 記錄選擇過的元素值 + duplicated[choice] = struct{}{} + (*selected)[i] = true + *state = append(*state, choice) + // 進行下一輪選擇 + backtrackI(state, choices, selected, res) + // 回退:撤銷選擇,恢復到之前的狀態 + (*selected)[i] = false + *state = (*state)[:len(*state)-1] + } + } + } + + /* 全排列 II */ + func permutationsII(nums []int) [][]int { + res := make([][]int, 0) + state := make([]int, 0) + selected := make([]bool, len(nums)) + backtrackII(&state, &nums, &selected, &res) + return res + } + ``` + +=== "Swift" + + ```swift title="permutations_ii.swift" + /* 回溯演算法:全排列 II */ + func backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) { + // 當狀態長度等於元素數量時,記錄解 + if state.count == choices.count { + res.append(state) + return + } + // 走訪所有選擇 + var duplicated: Set = [] + for (i, choice) in choices.enumerated() { + // 剪枝:不允許重複選擇元素 且 不允許重複選擇相等元素 + if !selected[i], !duplicated.contains(choice) { + // 嘗試:做出選擇,更新狀態 + duplicated.insert(choice) // 記錄選擇過的元素值 + selected[i] = true + state.append(choice) + // 進行下一輪選擇 + backtrack(state: &state, choices: choices, selected: &selected, res: &res) + // 回退:撤銷選擇,恢復到之前的狀態 + selected[i] = false + state.removeLast() + } + } + } + + /* 全排列 II */ + func permutationsII(nums: [Int]) -> [[Int]] { + var state: [Int] = [] + var selected = Array(repeating: false, count: nums.count) + var res: [[Int]] = [] + backtrack(state: &state, choices: nums, selected: &selected, res: &res) + return res + } + ``` + +=== "JS" + + ```javascript title="permutations_ii.js" + /* 回溯演算法:全排列 II */ + function backtrack(state, choices, selected, res) { + // 當狀態長度等於元素數量時,記錄解 + if (state.length === choices.length) { + res.push([...state]); + return; + } + // 走訪所有選擇 + const duplicated = new Set(); + choices.forEach((choice, i) => { + // 剪枝:不允許重複選擇元素 且 不允許重複選擇相等元素 + if (!selected[i] && !duplicated.has(choice)) { + // 嘗試:做出選擇,更新狀態 + duplicated.add(choice); // 記錄選擇過的元素值 + selected[i] = true; + state.push(choice); + // 進行下一輪選擇 + backtrack(state, choices, selected, res); + // 回退:撤銷選擇,恢復到之前的狀態 + selected[i] = false; + state.pop(); + } + }); + } + + /* 全排列 II */ + function permutationsII(nums) { + const res = []; + backtrack([], nums, Array(nums.length).fill(false), res); + return res; + } + ``` + +=== "TS" + + ```typescript title="permutations_ii.ts" + /* 回溯演算法:全排列 II */ + function backtrack( + state: number[], + choices: number[], + selected: boolean[], + res: number[][] + ): void { + // 當狀態長度等於元素數量時,記錄解 + if (state.length === choices.length) { + res.push([...state]); + return; + } + // 走訪所有選擇 + const duplicated = new Set(); + choices.forEach((choice, i) => { + // 剪枝:不允許重複選擇元素 且 不允許重複選擇相等元素 + if (!selected[i] && !duplicated.has(choice)) { + // 嘗試:做出選擇,更新狀態 + duplicated.add(choice); // 記錄選擇過的元素值 + selected[i] = true; + state.push(choice); + // 進行下一輪選擇 + backtrack(state, choices, selected, res); + // 回退:撤銷選擇,恢復到之前的狀態 + selected[i] = false; + state.pop(); + } + }); + } + + /* 全排列 II */ + function permutationsII(nums: number[]): number[][] { + const res: number[][] = []; + backtrack([], nums, Array(nums.length).fill(false), res); + return res; + } + ``` + +=== "Dart" + + ```dart title="permutations_ii.dart" + /* 回溯演算法:全排列 II */ + void backtrack( + List state, + List choices, + List selected, + List> res, + ) { + // 當狀態長度等於元素數量時,記錄解 + if (state.length == choices.length) { + res.add(List.from(state)); + return; + } + // 走訪所有選擇 + Set duplicated = {}; + for (int i = 0; i < choices.length; i++) { + int choice = choices[i]; + // 剪枝:不允許重複選擇元素 且 不允許重複選擇相等元素 + if (!selected[i] && !duplicated.contains(choice)) { + // 嘗試:做出選擇,更新狀態 + duplicated.add(choice); // 記錄選擇過的元素值 + selected[i] = true; + state.add(choice); + // 進行下一輪選擇 + backtrack(state, choices, selected, res); + // 回退:撤銷選擇,恢復到之前的狀態 + selected[i] = false; + state.removeLast(); + } + } + } + + /* 全排列 II */ + List> permutationsII(List nums) { + List> res = []; + backtrack([], nums, List.filled(nums.length, false), res); + return res; + } + ``` + +=== "Rust" + + ```rust title="permutations_ii.rs" + /* 回溯演算法:全排列 II */ + fn backtrack(mut state: Vec, choices: &[i32], selected: &mut [bool], res: &mut Vec>) { + // 當狀態長度等於元素數量時,記錄解 + if state.len() == choices.len() { + res.push(state); + return; + } + // 走訪所有選擇 + let mut duplicated = HashSet::::new(); + for i in 0..choices.len() { + let choice = choices[i]; + // 剪枝:不允許重複選擇元素 且 不允許重複選擇相等元素 + if !selected[i] && !duplicated.contains(&choice) { + // 嘗試:做出選擇,更新狀態 + duplicated.insert(choice); // 記錄選擇過的元素值 + selected[i] = true; + state.push(choice); + // 進行下一輪選擇 + backtrack(state.clone(), choices, selected, res); + // 回退:撤銷選擇,恢復到之前的狀態 + selected[i] = false; + state.remove(state.len() - 1); + } + } + } + + /* 全排列 II */ + fn permutations_ii(nums: &mut [i32]) -> Vec> { + let mut res = Vec::new(); + backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res); + res + } + ``` + +=== "C" + + ```c title="permutations_ii.c" + /* 回溯演算法:全排列 II */ + void backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) { + // 當狀態長度等於元素數量時,記錄解 + if (stateSize == choicesSize) { + res[*resSize] = (int *)malloc(choicesSize * sizeof(int)); + for (int i = 0; i < choicesSize; i++) { + res[*resSize][i] = state[i]; + } + (*resSize)++; + return; + } + // 走訪所有選擇 + bool duplicated[MAX_SIZE] = {false}; + for (int i = 0; i < choicesSize; i++) { + int choice = choices[i]; + // 剪枝:不允許重複選擇元素 且 不允許重複選擇相等元素 + if (!selected[i] && !duplicated[choice]) { + // 嘗試:做出選擇,更新狀態 + duplicated[choice] = true; // 記錄選擇過的元素值 + selected[i] = true; + state[stateSize] = choice; + // 進行下一輪選擇 + backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize); + // 回退:撤銷選擇,恢復到之前的狀態 + selected[i] = false; + } + } + } + + /* 全排列 II */ + int **permutationsII(int *nums, int numsSize, int *returnSize) { + int *state = (int *)malloc(numsSize * sizeof(int)); + bool *selected = (bool *)malloc(numsSize * sizeof(bool)); + for (int i = 0; i < numsSize; i++) { + selected[i] = false; + } + int **res = (int **)malloc(MAX_SIZE * sizeof(int *)); + *returnSize = 0; + + backtrack(state, 0, nums, numsSize, selected, res, returnSize); + + free(state); + free(selected); + + return res; + } + ``` + +=== "Kotlin" + + ```kotlin title="permutations_ii.kt" + /* 回溯演算法:全排列 II */ + fun backtrack( + state: MutableList, + choices: IntArray, + selected: BooleanArray, + res: MutableList?> + ) { + // 當狀態長度等於元素數量時,記錄解 + if (state.size == choices.size) { + res.add(ArrayList(state)) + return + } + // 走訪所有選擇 + val duplicated: MutableSet = HashSet() + for (i in choices.indices) { + val choice = choices[i] + // 剪枝:不允許重複選擇元素 且 不允許重複選擇相等元素 + if (!selected[i] && !duplicated.contains(choice)) { + // 嘗試:做出選擇,更新狀態 + duplicated.add(choice) // 記錄選擇過的元素值 + selected[i] = true + state.add(choice) + // 進行下一輪選擇 + backtrack(state, choices, selected, res) + // 回退:撤銷選擇,恢復到之前的狀態 + selected[i] = false + state.removeAt(state.size - 1) + } + } + } + + /* 全排列 II */ + fun permutationsII(nums: IntArray): MutableList?> { + val res: MutableList?> = ArrayList() + backtrack(ArrayList(), nums, BooleanArray(nums.size), res) + return res + } + ``` + +=== "Ruby" + + ```ruby title="permutations_ii.rb" + [class]{}-[func]{backtrack} + + [class]{}-[func]{permutations_ii} + ``` + +=== "Zig" + + ```zig title="permutations_ii.zig" + [class]{}-[func]{backtrack} + + [class]{}-[func]{permutationsII} + ``` + +??? pythontutor "視覺化執行" + +
+ + +假設元素兩兩之間互不相同,則 $n$ 個元素共有 $n!$ 種排列(階乘);在記錄結果時,需要複製長度為 $n$ 的串列,使用 $O(n)$ 時間。**因此時間複雜度為 $O(n!n)$** 。 + +最大遞迴深度為 $n$ ,使用 $O(n)$ 堆疊幀空間。`selected` 使用 $O(n)$ 空間。同一時刻最多共有 $n$ 個 `duplicated` ,使用 $O(n^2)$ 空間。**因此空間複雜度為 $O(n^2)$** 。 + +### 3.   兩種剪枝對比 + +請注意,雖然 `selected` 和 `duplicated` 都用於剪枝,但兩者的目標不同。 + +- **重複選擇剪枝**:整個搜尋過程中只有一個 `selected` 。它記錄的是當前狀態中包含哪些元素,其作用是避免某個元素在 `state` 中重複出現。 +- **相等元素剪枝**:每輪選擇(每個呼叫的 `backtrack` 函式)都包含一個 `duplicated` 。它記錄的是在本輪走訪(`for` 迴圈)中哪些元素已被選擇過,其作用是保證相等元素只被選擇一次。 + +圖 13-9 展示了兩個剪枝條件的生效範圍。注意,樹中的每個節點代表一個選擇,從根節點到葉節點的路徑上的各個節點構成一個排列。 + +![兩種剪枝條件的作用範圍](permutations_problem.assets/permutations_ii_pruning_summary.png){ class="animation-figure" } + +

圖 13-9   兩種剪枝條件的作用範圍

diff --git a/zh-Hant/docs/chapter_backtracking/subset_sum_problem.md b/zh-Hant/docs/chapter_backtracking/subset_sum_problem.md new file mode 100644 index 000000000..896c67041 --- /dev/null +++ b/zh-Hant/docs/chapter_backtracking/subset_sum_problem.md @@ -0,0 +1,1623 @@ +--- +comments: true +--- + +# 13.3   子集和問題 + +## 13.3.1   無重複元素的情況 + +!!! question + + 給定一個正整數陣列 `nums` 和一個目標正整數 `target` ,請找出所有可能的組合,使得組合中的元素和等於 `target` 。給定陣列無重複元素,每個元素可以被選取多次。請以串列形式返回這些組合,串列中不應包含重複組合。 + +例如,輸入集合 $\{3, 4, 5\}$ 和目標整數 $9$ ,解為 $\{3, 3, 3\}, \{4, 5\}$ 。需要注意以下兩點。 + +- 輸入集合中的元素可以被無限次重複選取。 +- 子集不區分元素順序,比如 $\{4, 5\}$ 和 $\{5, 4\}$ 是同一個子集。 + +### 1.   參考全排列解法 + +類似於全排列問題,我們可以把子集的生成過程想象成一系列選擇的結果,並在選擇過程中實時更新“元素和”,當元素和等於 `target` 時,就將子集記錄至結果串列。 + +而與全排列問題不同的是,**本題集合中的元素可以被無限次選取**,因此無須藉助 `selected` 布林串列來記錄元素是否已被選擇。我們可以對全排列程式碼進行小幅修改,初步得到解題程式碼: + +=== "Python" + + ```python title="subset_sum_i_naive.py" + def backtrack( + state: list[int], + target: int, + total: int, + choices: list[int], + res: list[list[int]], + ): + """回溯演算法:子集和 I""" + # 子集和等於 target 時,記錄解 + if total == target: + res.append(list(state)) + return + # 走訪所有選擇 + for i in range(len(choices)): + # 剪枝:若子集和超過 target ,則跳過該選擇 + if total + choices[i] > target: + continue + # 嘗試:做出選擇,更新元素和 total + state.append(choices[i]) + # 進行下一輪選擇 + backtrack(state, target, total + choices[i], choices, res) + # 回退:撤銷選擇,恢復到之前的狀態 + state.pop() + + def subset_sum_i_naive(nums: list[int], target: int) -> list[list[int]]: + """求解子集和 I(包含重複子集)""" + state = [] # 狀態(子集) + total = 0 # 子集和 + res = [] # 結果串列(子集串列) + backtrack(state, target, total, nums, res) + return res + ``` + +=== "C++" + + ```cpp title="subset_sum_i_naive.cpp" + /* 回溯演算法:子集和 I */ + void backtrack(vector &state, int target, int total, vector &choices, vector> &res) { + // 子集和等於 target 時,記錄解 + if (total == target) { + res.push_back(state); + return; + } + // 走訪所有選擇 + for (size_t i = 0; i < choices.size(); i++) { + // 剪枝:若子集和超過 target ,則跳過該選擇 + if (total + choices[i] > target) { + continue; + } + // 嘗試:做出選擇,更新元素和 total + state.push_back(choices[i]); + // 進行下一輪選擇 + backtrack(state, target, total + choices[i], choices, res); + // 回退:撤銷選擇,恢復到之前的狀態 + state.pop_back(); + } + } + + /* 求解子集和 I(包含重複子集) */ + vector> subsetSumINaive(vector &nums, int target) { + vector state; // 狀態(子集) + int total = 0; // 子集和 + vector> res; // 結果串列(子集串列) + backtrack(state, target, total, nums, res); + return res; + } + ``` + +=== "Java" + + ```java title="subset_sum_i_naive.java" + /* 回溯演算法:子集和 I */ + void backtrack(List state, int target, int total, int[] choices, List> res) { + // 子集和等於 target 時,記錄解 + if (total == target) { + res.add(new ArrayList<>(state)); + return; + } + // 走訪所有選擇 + for (int i = 0; i < choices.length; i++) { + // 剪枝:若子集和超過 target ,則跳過該選擇 + if (total + choices[i] > target) { + continue; + } + // 嘗試:做出選擇,更新元素和 total + state.add(choices[i]); + // 進行下一輪選擇 + backtrack(state, target, total + choices[i], choices, res); + // 回退:撤銷選擇,恢復到之前的狀態 + state.remove(state.size() - 1); + } + } + + /* 求解子集和 I(包含重複子集) */ + List> subsetSumINaive(int[] nums, int target) { + List state = new ArrayList<>(); // 狀態(子集) + int total = 0; // 子集和 + List> res = new ArrayList<>(); // 結果串列(子集串列) + backtrack(state, target, total, nums, res); + return res; + } + ``` + +=== "C#" + + ```csharp title="subset_sum_i_naive.cs" + /* 回溯演算法:子集和 I */ + void Backtrack(List state, int target, int total, int[] choices, List> res) { + // 子集和等於 target 時,記錄解 + if (total == target) { + res.Add(new List(state)); + return; + } + // 走訪所有選擇 + for (int i = 0; i < choices.Length; i++) { + // 剪枝:若子集和超過 target ,則跳過該選擇 + if (total + choices[i] > target) { + continue; + } + // 嘗試:做出選擇,更新元素和 total + state.Add(choices[i]); + // 進行下一輪選擇 + Backtrack(state, target, total + choices[i], choices, res); + // 回退:撤銷選擇,恢復到之前的狀態 + state.RemoveAt(state.Count - 1); + } + } + + /* 求解子集和 I(包含重複子集) */ + List> SubsetSumINaive(int[] nums, int target) { + List state = []; // 狀態(子集) + int total = 0; // 子集和 + List> res = []; // 結果串列(子集串列) + Backtrack(state, target, total, nums, res); + return res; + } + ``` + +=== "Go" + + ```go title="subset_sum_i_naive.go" + /* 回溯演算法:子集和 I */ + func backtrackSubsetSumINaive(total, target int, state, choices *[]int, res *[][]int) { + // 子集和等於 target 時,記錄解 + if target == total { + newState := append([]int{}, *state...) + *res = append(*res, newState) + return + } + // 走訪所有選擇 + for i := 0; i < len(*choices); i++ { + // 剪枝:若子集和超過 target ,則跳過該選擇 + if total+(*choices)[i] > target { + continue + } + // 嘗試:做出選擇,更新元素和 total + *state = append(*state, (*choices)[i]) + // 進行下一輪選擇 + backtrackSubsetSumINaive(total+(*choices)[i], target, state, choices, res) + // 回退:撤銷選擇,恢復到之前的狀態 + *state = (*state)[:len(*state)-1] + } + } + + /* 求解子集和 I(包含重複子集) */ + func subsetSumINaive(nums []int, target int) [][]int { + state := make([]int, 0) // 狀態(子集) + total := 0 // 子集和 + res := make([][]int, 0) // 結果串列(子集串列) + backtrackSubsetSumINaive(total, target, &state, &nums, &res) + return res + } + ``` + +=== "Swift" + + ```swift title="subset_sum_i_naive.swift" + /* 回溯演算法:子集和 I */ + func backtrack(state: inout [Int], target: Int, total: Int, choices: [Int], res: inout [[Int]]) { + // 子集和等於 target 時,記錄解 + if total == target { + res.append(state) + return + } + // 走訪所有選擇 + for i in choices.indices { + // 剪枝:若子集和超過 target ,則跳過該選擇 + if total + choices[i] > target { + continue + } + // 嘗試:做出選擇,更新元素和 total + state.append(choices[i]) + // 進行下一輪選擇 + backtrack(state: &state, target: target, total: total + choices[i], choices: choices, res: &res) + // 回退:撤銷選擇,恢復到之前的狀態 + state.removeLast() + } + } + + /* 求解子集和 I(包含重複子集) */ + func subsetSumINaive(nums: [Int], target: Int) -> [[Int]] { + var state: [Int] = [] // 狀態(子集) + let total = 0 // 子集和 + var res: [[Int]] = [] // 結果串列(子集串列) + backtrack(state: &state, target: target, total: total, choices: nums, res: &res) + return res + } + ``` + +=== "JS" + + ```javascript title="subset_sum_i_naive.js" + /* 回溯演算法:子集和 I */ + function backtrack(state, target, total, choices, res) { + // 子集和等於 target 時,記錄解 + if (total === target) { + res.push([...state]); + return; + } + // 走訪所有選擇 + for (let i = 0; i < choices.length; i++) { + // 剪枝:若子集和超過 target ,則跳過該選擇 + if (total + choices[i] > target) { + continue; + } + // 嘗試:做出選擇,更新元素和 total + state.push(choices[i]); + // 進行下一輪選擇 + backtrack(state, target, total + choices[i], choices, res); + // 回退:撤銷選擇,恢復到之前的狀態 + state.pop(); + } + } + + /* 求解子集和 I(包含重複子集) */ + function subsetSumINaive(nums, target) { + const state = []; // 狀態(子集) + const total = 0; // 子集和 + const res = []; // 結果串列(子集串列) + backtrack(state, target, total, nums, res); + return res; + } + ``` + +=== "TS" + + ```typescript title="subset_sum_i_naive.ts" + /* 回溯演算法:子集和 I */ + function backtrack( + state: number[], + target: number, + total: number, + choices: number[], + res: number[][] + ): void { + // 子集和等於 target 時,記錄解 + if (total === target) { + res.push([...state]); + return; + } + // 走訪所有選擇 + for (let i = 0; i < choices.length; i++) { + // 剪枝:若子集和超過 target ,則跳過該選擇 + if (total + choices[i] > target) { + continue; + } + // 嘗試:做出選擇,更新元素和 total + state.push(choices[i]); + // 進行下一輪選擇 + backtrack(state, target, total + choices[i], choices, res); + // 回退:撤銷選擇,恢復到之前的狀態 + state.pop(); + } + } + + /* 求解子集和 I(包含重複子集) */ + function subsetSumINaive(nums: number[], target: number): number[][] { + const state = []; // 狀態(子集) + const total = 0; // 子集和 + const res = []; // 結果串列(子集串列) + backtrack(state, target, total, nums, res); + return res; + } + ``` + +=== "Dart" + + ```dart title="subset_sum_i_naive.dart" + /* 回溯演算法:子集和 I */ + void backtrack( + List state, + int target, + int total, + List choices, + List> res, + ) { + // 子集和等於 target 時,記錄解 + if (total == target) { + res.add(List.from(state)); + return; + } + // 走訪所有選擇 + for (int i = 0; i < choices.length; i++) { + // 剪枝:若子集和超過 target ,則跳過該選擇 + if (total + choices[i] > target) { + continue; + } + // 嘗試:做出選擇,更新元素和 total + state.add(choices[i]); + // 進行下一輪選擇 + backtrack(state, target, total + choices[i], choices, res); + // 回退:撤銷選擇,恢復到之前的狀態 + state.removeLast(); + } + } + + /* 求解子集和 I(包含重複子集) */ + List> subsetSumINaive(List nums, int target) { + List state = []; // 狀態(子集) + int total = 0; // 元素和 + List> res = []; // 結果串列(子集串列) + backtrack(state, target, total, nums, res); + return res; + } + ``` + +=== "Rust" + + ```rust title="subset_sum_i_naive.rs" + /* 回溯演算法:子集和 I */ + fn backtrack( + mut state: Vec, + target: i32, + total: i32, + choices: &[i32], + res: &mut Vec>, + ) { + // 子集和等於 target 時,記錄解 + if total == target { + res.push(state); + return; + } + // 走訪所有選擇 + for i in 0..choices.len() { + // 剪枝:若子集和超過 target ,則跳過該選擇 + if total + choices[i] > target { + continue; + } + // 嘗試:做出選擇,更新元素和 total + state.push(choices[i]); + // 進行下一輪選擇 + backtrack(state.clone(), target, total + choices[i], choices, res); + // 回退:撤銷選擇,恢復到之前的狀態 + state.pop(); + } + } + + /* 求解子集和 I(包含重複子集) */ + fn subset_sum_i_naive(nums: &[i32], target: i32) -> Vec> { + let state = Vec::new(); // 狀態(子集) + let total = 0; // 子集和 + let mut res = Vec::new(); // 結果串列(子集串列) + backtrack(state, target, total, nums, &mut res); + res + } + ``` + +=== "C" + + ```c title="subset_sum_i_naive.c" + /* 回溯演算法:子集和 I */ + void backtrack(int target, int total, int *choices, int choicesSize) { + // 子集和等於 target 時,記錄解 + if (total == target) { + for (int i = 0; i < stateSize; i++) { + res[resSize][i] = state[i]; + } + resColSizes[resSize++] = stateSize; + return; + } + // 走訪所有選擇 + for (int i = 0; i < choicesSize; i++) { + // 剪枝:若子集和超過 target ,則跳過該選擇 + if (total + choices[i] > target) { + continue; + } + // 嘗試:做出選擇,更新元素和 total + state[stateSize++] = choices[i]; + // 進行下一輪選擇 + backtrack(target, total + choices[i], choices, choicesSize); + // 回退:撤銷選擇,恢復到之前的狀態 + stateSize--; + } + } + + /* 求解子集和 I(包含重複子集) */ + void subsetSumINaive(int *nums, int numsSize, int target) { + resSize = 0; // 初始化解的數量為0 + backtrack(target, 0, nums, numsSize); + } + ``` + +=== "Kotlin" + + ```kotlin title="subset_sum_i_naive.kt" + /* 回溯演算法:子集和 I */ + fun backtrack( + state: MutableList, + target: Int, + total: Int, + choices: IntArray, + res: MutableList?> + ) { + // 子集和等於 target 時,記錄解 + if (total == target) { + res.add(ArrayList(state)) + return + } + // 走訪所有選擇 + for (i in choices.indices) { + // 剪枝:若子集和超過 target ,則跳過該選擇 + if (total + choices[i] > target) { + continue + } + // 嘗試:做出選擇,更新元素和 total + state.add(choices[i]) + // 進行下一輪選擇 + backtrack(state, target, total + choices[i], choices, res) + // 回退:撤銷選擇,恢復到之前的狀態 + state.removeAt(state.size - 1) + } + } + + /* 求解子集和 I(包含重複子集) */ + fun subsetSumINaive(nums: IntArray, target: Int): List?> { + val state: MutableList = ArrayList() // 狀態(子集) + val total = 0 // 子集和 + val res: MutableList?> = ArrayList() // 結果串列(子集串列) + backtrack(state, target, total, nums, res) + return res + } + ``` + +=== "Ruby" + + ```ruby title="subset_sum_i_naive.rb" + [class]{}-[func]{backtrack} + + [class]{}-[func]{subset_sum_i_naive} + ``` + +=== "Zig" + + ```zig title="subset_sum_i_naive.zig" + [class]{}-[func]{backtrack} + + [class]{}-[func]{subsetSumINaive} + ``` + +??? pythontutor "視覺化執行" + +
+ + +向以上程式碼輸入陣列 $[3, 4, 5]$ 和目標元素 $9$ ,輸出結果為 $[3, 3, 3], [4, 5], [5, 4]$ 。**雖然成功找出了所有和為 $9$ 的子集,但其中存在重複的子集 $[4, 5]$ 和 $[5, 4]$** 。 + +這是因為搜尋過程是區分選擇順序的,然而子集不區分選擇順序。如圖 13-10 所示,先選 $4$ 後選 $5$ 與先選 $5$ 後選 $4$ 是不同的分支,但對應同一個子集。 + +![子集搜尋與越界剪枝](subset_sum_problem.assets/subset_sum_i_naive.png){ class="animation-figure" } + +

圖 13-10   子集搜尋與越界剪枝

+ +為了去除重複子集,**一種直接的思路是對結果串列進行去重**。但這個方法效率很低,有兩方面原因。 + +- 當陣列元素較多,尤其是當 `target` 較大時,搜尋過程會產生大量的重複子集。 +- 比較子集(陣列)的異同非常耗時,需要先排序陣列,再比較陣列中每個元素的異同。 + +### 2.   重複子集剪枝 + +**我們考慮在搜尋過程中透過剪枝進行去重**。觀察圖 13-11 ,重複子集是在以不同順序選擇陣列元素時產生的,例如以下情況。 + +1. 當第一輪和第二輪分別選擇 $3$ 和 $4$ 時,會生成包含這兩個元素的所有子集,記為 $[3, 4, \dots]$ 。 +2. 之後,當第一輪選擇 $4$ 時,**則第二輪應該跳過 $3$** ,因為該選擇產生的子集 $[4, 3, \dots]$ 和第 `1.` 步中生成的子集完全重複。 + +在搜尋過程中,每一層的選擇都是從左到右被逐個嘗試的,因此越靠右的分支被剪掉的越多。 + +1. 前兩輪選擇 $3$ 和 $5$ ,生成子集 $[3, 5, \dots]$ 。 +2. 前兩輪選擇 $4$ 和 $5$ ,生成子集 $[4, 5, \dots]$ 。 +3. 若第一輪選擇 $5$ ,**則第二輪應該跳過 $3$ 和 $4$** ,因為子集 $[5, 3, \dots]$ 和 $[5, 4, \dots]$ 與第 `1.` 步和第 `2.` 步中描述的子集完全重複。 + +![不同選擇順序導致的重複子集](subset_sum_problem.assets/subset_sum_i_pruning.png){ class="animation-figure" } + +

圖 13-11   不同選擇順序導致的重複子集

+ +總結來看,給定輸入陣列 $[x_1, x_2, \dots, x_n]$ ,設搜尋過程中的選擇序列為 $[x_{i_1}, x_{i_2}, \dots, x_{i_m}]$ ,則該選擇序列需要滿足 $i_1 \leq i_2 \leq \dots \leq i_m$ ,**不滿足該條件的選擇序列都會造成重複,應當剪枝**。 + +### 3.   程式碼實現 + +為實現該剪枝,我們初始化變數 `start` ,用於指示走訪起始點。**當做出選擇 $x_{i}$ 後,設定下一輪從索引 $i$ 開始走訪**。這樣做就可以讓選擇序列滿足 $i_1 \leq i_2 \leq \dots \leq i_m$ ,從而保證子集唯一。 + +除此之外,我們還對程式碼進行了以下兩項最佳化。 + +- 在開啟搜尋前,先將陣列 `nums` 排序。在走訪所有選擇時,**當子集和超過 `target` 時直接結束迴圈**,因為後邊的元素更大,其子集和一定超過 `target` 。 +- 省去元素和變數 `total` ,**透過在 `target` 上執行減法來統計元素和**,當 `target` 等於 $0$ 時記錄解。 + +=== "Python" + + ```python title="subset_sum_i.py" + def backtrack( + state: list[int], target: int, choices: list[int], start: int, res: list[list[int]] + ): + """回溯演算法:子集和 I""" + # 子集和等於 target 時,記錄解 + if target == 0: + res.append(list(state)) + return + # 走訪所有選擇 + # 剪枝二:從 start 開始走訪,避免生成重複子集 + for i in range(start, len(choices)): + # 剪枝一:若子集和超過 target ,則直接結束迴圈 + # 這是因為陣列已排序,後邊元素更大,子集和一定超過 target + if target - choices[i] < 0: + break + # 嘗試:做出選擇,更新 target, start + state.append(choices[i]) + # 進行下一輪選擇 + backtrack(state, target - choices[i], choices, i, res) + # 回退:撤銷選擇,恢復到之前的狀態 + state.pop() + + def subset_sum_i(nums: list[int], target: int) -> list[list[int]]: + """求解子集和 I""" + state = [] # 狀態(子集) + nums.sort() # 對 nums 進行排序 + start = 0 # 走訪起始點 + res = [] # 結果串列(子集串列) + backtrack(state, target, nums, start, res) + return res + ``` + +=== "C++" + + ```cpp title="subset_sum_i.cpp" + /* 回溯演算法:子集和 I */ + void backtrack(vector &state, int target, vector &choices, int start, vector> &res) { + // 子集和等於 target 時,記錄解 + if (target == 0) { + res.push_back(state); + return; + } + // 走訪所有選擇 + // 剪枝二:從 start 開始走訪,避免生成重複子集 + for (int i = start; i < choices.size(); i++) { + // 剪枝一:若子集和超過 target ,則直接結束迴圈 + // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target + if (target - choices[i] < 0) { + break; + } + // 嘗試:做出選擇,更新 target, start + state.push_back(choices[i]); + // 進行下一輪選擇 + backtrack(state, target - choices[i], choices, i, res); + // 回退:撤銷選擇,恢復到之前的狀態 + state.pop_back(); + } + } + + /* 求解子集和 I */ + vector> subsetSumI(vector &nums, int target) { + vector state; // 狀態(子集) + sort(nums.begin(), nums.end()); // 對 nums 進行排序 + int start = 0; // 走訪起始點 + vector> res; // 結果串列(子集串列) + backtrack(state, target, nums, start, res); + return res; + } + ``` + +=== "Java" + + ```java title="subset_sum_i.java" + /* 回溯演算法:子集和 I */ + void backtrack(List state, int target, int[] choices, int start, List> res) { + // 子集和等於 target 時,記錄解 + if (target == 0) { + res.add(new ArrayList<>(state)); + return; + } + // 走訪所有選擇 + // 剪枝二:從 start 開始走訪,避免生成重複子集 + for (int i = start; i < choices.length; i++) { + // 剪枝一:若子集和超過 target ,則直接結束迴圈 + // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target + if (target - choices[i] < 0) { + break; + } + // 嘗試:做出選擇,更新 target, start + state.add(choices[i]); + // 進行下一輪選擇 + backtrack(state, target - choices[i], choices, i, res); + // 回退:撤銷選擇,恢復到之前的狀態 + state.remove(state.size() - 1); + } + } + + /* 求解子集和 I */ + List> subsetSumI(int[] nums, int target) { + List state = new ArrayList<>(); // 狀態(子集) + Arrays.sort(nums); // 對 nums 進行排序 + int start = 0; // 走訪起始點 + List> res = new ArrayList<>(); // 結果串列(子集串列) + backtrack(state, target, nums, start, res); + return res; + } + ``` + +=== "C#" + + ```csharp title="subset_sum_i.cs" + /* 回溯演算法:子集和 I */ + void Backtrack(List state, int target, int[] choices, int start, List> res) { + // 子集和等於 target 時,記錄解 + if (target == 0) { + res.Add(new List(state)); + return; + } + // 走訪所有選擇 + // 剪枝二:從 start 開始走訪,避免生成重複子集 + for (int i = start; i < choices.Length; i++) { + // 剪枝一:若子集和超過 target ,則直接結束迴圈 + // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target + if (target - choices[i] < 0) { + break; + } + // 嘗試:做出選擇,更新 target, start + state.Add(choices[i]); + // 進行下一輪選擇 + Backtrack(state, target - choices[i], choices, i, res); + // 回退:撤銷選擇,恢復到之前的狀態 + state.RemoveAt(state.Count - 1); + } + } + + /* 求解子集和 I */ + List> SubsetSumI(int[] nums, int target) { + List state = []; // 狀態(子集) + Array.Sort(nums); // 對 nums 進行排序 + int start = 0; // 走訪起始點 + List> res = []; // 結果串列(子集串列) + Backtrack(state, target, nums, start, res); + return res; + } + ``` + +=== "Go" + + ```go title="subset_sum_i.go" + /* 回溯演算法:子集和 I */ + func backtrackSubsetSumI(start, target int, state, choices *[]int, res *[][]int) { + // 子集和等於 target 時,記錄解 + if target == 0 { + newState := append([]int{}, *state...) + *res = append(*res, newState) + return + } + // 走訪所有選擇 + // 剪枝二:從 start 開始走訪,避免生成重複子集 + for i := start; i < len(*choices); i++ { + // 剪枝一:若子集和超過 target ,則直接結束迴圈 + // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target + if target-(*choices)[i] < 0 { + break + } + // 嘗試:做出選擇,更新 target, start + *state = append(*state, (*choices)[i]) + // 進行下一輪選擇 + backtrackSubsetSumI(i, target-(*choices)[i], state, choices, res) + // 回退:撤銷選擇,恢復到之前的狀態 + *state = (*state)[:len(*state)-1] + } + } + + /* 求解子集和 I */ + func subsetSumI(nums []int, target int) [][]int { + state := make([]int, 0) // 狀態(子集) + sort.Ints(nums) // 對 nums 進行排序 + start := 0 // 走訪起始點 + res := make([][]int, 0) // 結果串列(子集串列) + backtrackSubsetSumI(start, target, &state, &nums, &res) + return res + } + ``` + +=== "Swift" + + ```swift title="subset_sum_i.swift" + /* 回溯演算法:子集和 I */ + func backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) { + // 子集和等於 target 時,記錄解 + if target == 0 { + res.append(state) + return + } + // 走訪所有選擇 + // 剪枝二:從 start 開始走訪,避免生成重複子集 + for i in choices.indices.dropFirst(start) { + // 剪枝一:若子集和超過 target ,則直接結束迴圈 + // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target + if target - choices[i] < 0 { + break + } + // 嘗試:做出選擇,更新 target, start + state.append(choices[i]) + // 進行下一輪選擇 + backtrack(state: &state, target: target - choices[i], choices: choices, start: i, res: &res) + // 回退:撤銷選擇,恢復到之前的狀態 + state.removeLast() + } + } + + /* 求解子集和 I */ + func subsetSumI(nums: [Int], target: Int) -> [[Int]] { + var state: [Int] = [] // 狀態(子集) + let nums = nums.sorted() // 對 nums 進行排序 + let start = 0 // 走訪起始點 + var res: [[Int]] = [] // 結果串列(子集串列) + backtrack(state: &state, target: target, choices: nums, start: start, res: &res) + return res + } + ``` + +=== "JS" + + ```javascript title="subset_sum_i.js" + /* 回溯演算法:子集和 I */ + function backtrack(state, target, choices, start, res) { + // 子集和等於 target 時,記錄解 + if (target === 0) { + res.push([...state]); + return; + } + // 走訪所有選擇 + // 剪枝二:從 start 開始走訪,避免生成重複子集 + for (let i = start; i < choices.length; i++) { + // 剪枝一:若子集和超過 target ,則直接結束迴圈 + // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target + if (target - choices[i] < 0) { + break; + } + // 嘗試:做出選擇,更新 target, start + state.push(choices[i]); + // 進行下一輪選擇 + backtrack(state, target - choices[i], choices, i, res); + // 回退:撤銷選擇,恢復到之前的狀態 + state.pop(); + } + } + + /* 求解子集和 I */ + function subsetSumI(nums, target) { + const state = []; // 狀態(子集) + nums.sort((a, b) => a - b); // 對 nums 進行排序 + const start = 0; // 走訪起始點 + const res = []; // 結果串列(子集串列) + backtrack(state, target, nums, start, res); + return res; + } + ``` + +=== "TS" + + ```typescript title="subset_sum_i.ts" + /* 回溯演算法:子集和 I */ + function backtrack( + state: number[], + target: number, + choices: number[], + start: number, + res: number[][] + ): void { + // 子集和等於 target 時,記錄解 + if (target === 0) { + res.push([...state]); + return; + } + // 走訪所有選擇 + // 剪枝二:從 start 開始走訪,避免生成重複子集 + for (let i = start; i < choices.length; i++) { + // 剪枝一:若子集和超過 target ,則直接結束迴圈 + // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target + if (target - choices[i] < 0) { + break; + } + // 嘗試:做出選擇,更新 target, start + state.push(choices[i]); + // 進行下一輪選擇 + backtrack(state, target - choices[i], choices, i, res); + // 回退:撤銷選擇,恢復到之前的狀態 + state.pop(); + } + } + + /* 求解子集和 I */ + function subsetSumI(nums: number[], target: number): number[][] { + const state = []; // 狀態(子集) + nums.sort((a, b) => a - b); // 對 nums 進行排序 + const start = 0; // 走訪起始點 + const res = []; // 結果串列(子集串列) + backtrack(state, target, nums, start, res); + return res; + } + ``` + +=== "Dart" + + ```dart title="subset_sum_i.dart" + /* 回溯演算法:子集和 I */ + void backtrack( + List state, + int target, + List choices, + int start, + List> res, + ) { + // 子集和等於 target 時,記錄解 + if (target == 0) { + res.add(List.from(state)); + return; + } + // 走訪所有選擇 + // 剪枝二:從 start 開始走訪,避免生成重複子集 + for (int i = start; i < choices.length; i++) { + // 剪枝一:若子集和超過 target ,則直接結束迴圈 + // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target + if (target - choices[i] < 0) { + break; + } + // 嘗試:做出選擇,更新 target, start + state.add(choices[i]); + // 進行下一輪選擇 + backtrack(state, target - choices[i], choices, i, res); + // 回退:撤銷選擇,恢復到之前的狀態 + state.removeLast(); + } + } + + /* 求解子集和 I */ + List> subsetSumI(List nums, int target) { + List state = []; // 狀態(子集) + nums.sort(); // 對 nums 進行排序 + int start = 0; // 走訪起始點 + List> res = []; // 結果串列(子集串列) + backtrack(state, target, nums, start, res); + return res; + } + ``` + +=== "Rust" + + ```rust title="subset_sum_i.rs" + /* 回溯演算法:子集和 I */ + fn backtrack( + mut state: Vec, + target: i32, + choices: &[i32], + start: usize, + res: &mut Vec>, + ) { + // 子集和等於 target 時,記錄解 + if target == 0 { + res.push(state); + return; + } + // 走訪所有選擇 + // 剪枝二:從 start 開始走訪,避免生成重複子集 + for i in start..choices.len() { + // 剪枝一:若子集和超過 target ,則直接結束迴圈 + // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target + if target - choices[i] < 0 { + break; + } + // 嘗試:做出選擇,更新 target, start + state.push(choices[i]); + // 進行下一輪選擇 + backtrack(state.clone(), target - choices[i], choices, i, res); + // 回退:撤銷選擇,恢復到之前的狀態 + state.pop(); + } + } + + /* 求解子集和 I */ + fn subset_sum_i(nums: &mut [i32], target: i32) -> Vec> { + let state = Vec::new(); // 狀態(子集) + nums.sort(); // 對 nums 進行排序 + let start = 0; // 走訪起始點 + let mut res = Vec::new(); // 結果串列(子集串列) + backtrack(state, target, nums, start, &mut res); + res + } + ``` + +=== "C" + + ```c title="subset_sum_i.c" + /* 回溯演算法:子集和 I */ + void backtrack(int target, int *choices, int choicesSize, int start) { + // 子集和等於 target 時,記錄解 + if (target == 0) { + for (int i = 0; i < stateSize; ++i) { + res[resSize][i] = state[i]; + } + resColSizes[resSize++] = stateSize; + return; + } + // 走訪所有選擇 + // 剪枝二:從 start 開始走訪,避免生成重複子集 + for (int i = start; i < choicesSize; i++) { + // 剪枝一:若子集和超過 target ,則直接結束迴圈 + // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target + if (target - choices[i] < 0) { + break; + } + // 嘗試:做出選擇,更新 target, start + state[stateSize] = choices[i]; + stateSize++; + // 進行下一輪選擇 + backtrack(target - choices[i], choices, choicesSize, i); + // 回退:撤銷選擇,恢復到之前的狀態 + stateSize--; + } + } + + /* 求解子集和 I */ + void subsetSumI(int *nums, int numsSize, int target) { + qsort(nums, numsSize, sizeof(int), cmp); // 對 nums 進行排序 + int start = 0; // 走訪起始點 + backtrack(target, nums, numsSize, start); + } + ``` + +=== "Kotlin" + + ```kotlin title="subset_sum_i.kt" + /* 回溯演算法:子集和 I */ + fun backtrack( + state: MutableList, + target: Int, + choices: IntArray, + start: Int, + res: MutableList?> + ) { + // 子集和等於 target 時,記錄解 + if (target == 0) { + res.add(ArrayList(state)) + return + } + // 走訪所有選擇 + // 剪枝二:從 start 開始走訪,避免生成重複子集 + for (i in start..?> { + val state: MutableList = ArrayList() // 狀態(子集) + Arrays.sort(nums) // 對 nums 進行排序 + val start = 0 // 走訪起始點 + val res: MutableList?> = ArrayList() // 結果串列(子集串列) + backtrack(state, target, nums, start, res) + return res + } + ``` + +=== "Ruby" + + ```ruby title="subset_sum_i.rb" + [class]{}-[func]{backtrack} + + [class]{}-[func]{subset_sum_i} + ``` + +=== "Zig" + + ```zig title="subset_sum_i.zig" + [class]{}-[func]{backtrack} + + [class]{}-[func]{subsetSumI} + ``` + +??? pythontutor "視覺化執行" + +
+ + +圖 13-12 所示為將陣列 $[3, 4, 5]$ 和目標元素 $9$ 輸入以上程式碼後的整體回溯過程。 + +![子集和 I 回溯過程](subset_sum_problem.assets/subset_sum_i.png){ class="animation-figure" } + +

圖 13-12   子集和 I 回溯過程

+ +## 13.3.2   考慮重複元素的情況 + +!!! question + + 給定一個正整數陣列 `nums` 和一個目標正整數 `target` ,請找出所有可能的組合,使得組合中的元素和等於 `target` 。**給定陣列可能包含重複元素,每個元素只可被選擇一次**。請以串列形式返回這些組合,串列中不應包含重複組合。 + +相比於上題,**本題的輸入陣列可能包含重複元素**,這引入了新的問題。例如,給定陣列 $[4, \hat{4}, 5]$ 和目標元素 $9$ ,則現有程式碼的輸出結果為 $[4, 5], [\hat{4}, 5]$ ,出現了重複子集。 + +**造成這種重複的原因是相等元素在某輪中被多次選擇**。在圖 13-13 中,第一輪共有三個選擇,其中兩個都為 $4$ ,會產生兩個重複的搜尋分支,從而輸出重複子集;同理,第二輪的兩個 $4$ 也會產生重複子集。 + +![相等元素導致的重複子集](subset_sum_problem.assets/subset_sum_ii_repeat.png){ class="animation-figure" } + +

圖 13-13   相等元素導致的重複子集

+ +### 1.   相等元素剪枝 + +為解決此問題,**我們需要限制相等元素在每一輪中只能被選擇一次**。實現方式比較巧妙:由於陣列是已排序的,因此相等元素都是相鄰的。這意味著在某輪選擇中,若當前元素與其左邊元素相等,則說明它已經被選擇過,因此直接跳過當前元素。 + +與此同時,**本題規定每個陣列元素只能被選擇一次**。幸運的是,我們也可以利用變數 `start` 來滿足該約束:當做出選擇 $x_{i}$ 後,設定下一輪從索引 $i + 1$ 開始向後走訪。這樣既能去除重複子集,也能避免重複選擇元素。 + +### 2.   程式碼實現 + +=== "Python" + + ```python title="subset_sum_ii.py" + def backtrack( + state: list[int], target: int, choices: list[int], start: int, res: list[list[int]] + ): + """回溯演算法:子集和 II""" + # 子集和等於 target 時,記錄解 + if target == 0: + res.append(list(state)) + return + # 走訪所有選擇 + # 剪枝二:從 start 開始走訪,避免生成重複子集 + # 剪枝三:從 start 開始走訪,避免重複選擇同一元素 + for i in range(start, len(choices)): + # 剪枝一:若子集和超過 target ,則直接結束迴圈 + # 這是因為陣列已排序,後邊元素更大,子集和一定超過 target + if target - choices[i] < 0: + break + # 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過 + if i > start and choices[i] == choices[i - 1]: + continue + # 嘗試:做出選擇,更新 target, start + state.append(choices[i]) + # 進行下一輪選擇 + backtrack(state, target - choices[i], choices, i + 1, res) + # 回退:撤銷選擇,恢復到之前的狀態 + state.pop() + + def subset_sum_ii(nums: list[int], target: int) -> list[list[int]]: + """求解子集和 II""" + state = [] # 狀態(子集) + nums.sort() # 對 nums 進行排序 + start = 0 # 走訪起始點 + res = [] # 結果串列(子集串列) + backtrack(state, target, nums, start, res) + return res + ``` + +=== "C++" + + ```cpp title="subset_sum_ii.cpp" + /* 回溯演算法:子集和 II */ + void backtrack(vector &state, int target, vector &choices, int start, vector> &res) { + // 子集和等於 target 時,記錄解 + if (target == 0) { + res.push_back(state); + return; + } + // 走訪所有選擇 + // 剪枝二:從 start 開始走訪,避免生成重複子集 + // 剪枝三:從 start 開始走訪,避免重複選擇同一元素 + for (int i = start; i < choices.size(); i++) { + // 剪枝一:若子集和超過 target ,則直接結束迴圈 + // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target + if (target - choices[i] < 0) { + break; + } + // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過 + if (i > start && choices[i] == choices[i - 1]) { + continue; + } + // 嘗試:做出選擇,更新 target, start + state.push_back(choices[i]); + // 進行下一輪選擇 + backtrack(state, target - choices[i], choices, i + 1, res); + // 回退:撤銷選擇,恢復到之前的狀態 + state.pop_back(); + } + } + + /* 求解子集和 II */ + vector> subsetSumII(vector &nums, int target) { + vector state; // 狀態(子集) + sort(nums.begin(), nums.end()); // 對 nums 進行排序 + int start = 0; // 走訪起始點 + vector> res; // 結果串列(子集串列) + backtrack(state, target, nums, start, res); + return res; + } + ``` + +=== "Java" + + ```java title="subset_sum_ii.java" + /* 回溯演算法:子集和 II */ + void backtrack(List state, int target, int[] choices, int start, List> res) { + // 子集和等於 target 時,記錄解 + if (target == 0) { + res.add(new ArrayList<>(state)); + return; + } + // 走訪所有選擇 + // 剪枝二:從 start 開始走訪,避免生成重複子集 + // 剪枝三:從 start 開始走訪,避免重複選擇同一元素 + for (int i = start; i < choices.length; i++) { + // 剪枝一:若子集和超過 target ,則直接結束迴圈 + // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target + if (target - choices[i] < 0) { + break; + } + // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過 + if (i > start && choices[i] == choices[i - 1]) { + continue; + } + // 嘗試:做出選擇,更新 target, start + state.add(choices[i]); + // 進行下一輪選擇 + backtrack(state, target - choices[i], choices, i + 1, res); + // 回退:撤銷選擇,恢復到之前的狀態 + state.remove(state.size() - 1); + } + } + + /* 求解子集和 II */ + List> subsetSumII(int[] nums, int target) { + List state = new ArrayList<>(); // 狀態(子集) + Arrays.sort(nums); // 對 nums 進行排序 + int start = 0; // 走訪起始點 + List> res = new ArrayList<>(); // 結果串列(子集串列) + backtrack(state, target, nums, start, res); + return res; + } + ``` + +=== "C#" + + ```csharp title="subset_sum_ii.cs" + /* 回溯演算法:子集和 II */ + void Backtrack(List state, int target, int[] choices, int start, List> res) { + // 子集和等於 target 時,記錄解 + if (target == 0) { + res.Add(new List(state)); + return; + } + // 走訪所有選擇 + // 剪枝二:從 start 開始走訪,避免生成重複子集 + // 剪枝三:從 start 開始走訪,避免重複選擇同一元素 + for (int i = start; i < choices.Length; i++) { + // 剪枝一:若子集和超過 target ,則直接結束迴圈 + // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target + if (target - choices[i] < 0) { + break; + } + // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過 + if (i > start && choices[i] == choices[i - 1]) { + continue; + } + // 嘗試:做出選擇,更新 target, start + state.Add(choices[i]); + // 進行下一輪選擇 + Backtrack(state, target - choices[i], choices, i + 1, res); + // 回退:撤銷選擇,恢復到之前的狀態 + state.RemoveAt(state.Count - 1); + } + } + + /* 求解子集和 II */ + List> SubsetSumII(int[] nums, int target) { + List state = []; // 狀態(子集) + Array.Sort(nums); // 對 nums 進行排序 + int start = 0; // 走訪起始點 + List> res = []; // 結果串列(子集串列) + Backtrack(state, target, nums, start, res); + return res; + } + ``` + +=== "Go" + + ```go title="subset_sum_ii.go" + /* 回溯演算法:子集和 II */ + func backtrackSubsetSumII(start, target int, state, choices *[]int, res *[][]int) { + // 子集和等於 target 時,記錄解 + if target == 0 { + newState := append([]int{}, *state...) + *res = append(*res, newState) + return + } + // 走訪所有選擇 + // 剪枝二:從 start 開始走訪,避免生成重複子集 + // 剪枝三:從 start 開始走訪,避免重複選擇同一元素 + for i := start; i < len(*choices); i++ { + // 剪枝一:若子集和超過 target ,則直接結束迴圈 + // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target + if target-(*choices)[i] < 0 { + break + } + // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過 + if i > start && (*choices)[i] == (*choices)[i-1] { + continue + } + // 嘗試:做出選擇,更新 target, start + *state = append(*state, (*choices)[i]) + // 進行下一輪選擇 + backtrackSubsetSumII(i+1, target-(*choices)[i], state, choices, res) + // 回退:撤銷選擇,恢復到之前的狀態 + *state = (*state)[:len(*state)-1] + } + } + + /* 求解子集和 II */ + func subsetSumII(nums []int, target int) [][]int { + state := make([]int, 0) // 狀態(子集) + sort.Ints(nums) // 對 nums 進行排序 + start := 0 // 走訪起始點 + res := make([][]int, 0) // 結果串列(子集串列) + backtrackSubsetSumII(start, target, &state, &nums, &res) + return res + } + ``` + +=== "Swift" + + ```swift title="subset_sum_ii.swift" + /* 回溯演算法:子集和 II */ + func backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) { + // 子集和等於 target 時,記錄解 + if target == 0 { + res.append(state) + return + } + // 走訪所有選擇 + // 剪枝二:從 start 開始走訪,避免生成重複子集 + // 剪枝三:從 start 開始走訪,避免重複選擇同一元素 + for i in choices.indices.dropFirst(start) { + // 剪枝一:若子集和超過 target ,則直接結束迴圈 + // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target + if target - choices[i] < 0 { + break + } + // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過 + if i > start, choices[i] == choices[i - 1] { + continue + } + // 嘗試:做出選擇,更新 target, start + state.append(choices[i]) + // 進行下一輪選擇 + backtrack(state: &state, target: target - choices[i], choices: choices, start: i + 1, res: &res) + // 回退:撤銷選擇,恢復到之前的狀態 + state.removeLast() + } + } + + /* 求解子集和 II */ + func subsetSumII(nums: [Int], target: Int) -> [[Int]] { + var state: [Int] = [] // 狀態(子集) + let nums = nums.sorted() // 對 nums 進行排序 + let start = 0 // 走訪起始點 + var res: [[Int]] = [] // 結果串列(子集串列) + backtrack(state: &state, target: target, choices: nums, start: start, res: &res) + return res + } + ``` + +=== "JS" + + ```javascript title="subset_sum_ii.js" + /* 回溯演算法:子集和 II */ + function backtrack(state, target, choices, start, res) { + // 子集和等於 target 時,記錄解 + if (target === 0) { + res.push([...state]); + return; + } + // 走訪所有選擇 + // 剪枝二:從 start 開始走訪,避免生成重複子集 + // 剪枝三:從 start 開始走訪,避免重複選擇同一元素 + for (let i = start; i < choices.length; i++) { + // 剪枝一:若子集和超過 target ,則直接結束迴圈 + // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target + if (target - choices[i] < 0) { + break; + } + // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過 + if (i > start && choices[i] === choices[i - 1]) { + continue; + } + // 嘗試:做出選擇,更新 target, start + state.push(choices[i]); + // 進行下一輪選擇 + backtrack(state, target - choices[i], choices, i + 1, res); + // 回退:撤銷選擇,恢復到之前的狀態 + state.pop(); + } + } + + /* 求解子集和 II */ + function subsetSumII(nums, target) { + const state = []; // 狀態(子集) + nums.sort((a, b) => a - b); // 對 nums 進行排序 + const start = 0; // 走訪起始點 + const res = []; // 結果串列(子集串列) + backtrack(state, target, nums, start, res); + return res; + } + ``` + +=== "TS" + + ```typescript title="subset_sum_ii.ts" + /* 回溯演算法:子集和 II */ + function backtrack( + state: number[], + target: number, + choices: number[], + start: number, + res: number[][] + ): void { + // 子集和等於 target 時,記錄解 + if (target === 0) { + res.push([...state]); + return; + } + // 走訪所有選擇 + // 剪枝二:從 start 開始走訪,避免生成重複子集 + // 剪枝三:從 start 開始走訪,避免重複選擇同一元素 + for (let i = start; i < choices.length; i++) { + // 剪枝一:若子集和超過 target ,則直接結束迴圈 + // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target + if (target - choices[i] < 0) { + break; + } + // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過 + if (i > start && choices[i] === choices[i - 1]) { + continue; + } + // 嘗試:做出選擇,更新 target, start + state.push(choices[i]); + // 進行下一輪選擇 + backtrack(state, target - choices[i], choices, i + 1, res); + // 回退:撤銷選擇,恢復到之前的狀態 + state.pop(); + } + } + + /* 求解子集和 II */ + function subsetSumII(nums: number[], target: number): number[][] { + const state = []; // 狀態(子集) + nums.sort((a, b) => a - b); // 對 nums 進行排序 + const start = 0; // 走訪起始點 + const res = []; // 結果串列(子集串列) + backtrack(state, target, nums, start, res); + return res; + } + ``` + +=== "Dart" + + ```dart title="subset_sum_ii.dart" + /* 回溯演算法:子集和 II */ + void backtrack( + List state, + int target, + List choices, + int start, + List> res, + ) { + // 子集和等於 target 時,記錄解 + if (target == 0) { + res.add(List.from(state)); + return; + } + // 走訪所有選擇 + // 剪枝二:從 start 開始走訪,避免生成重複子集 + // 剪枝三:從 start 開始走訪,避免重複選擇同一元素 + for (int i = start; i < choices.length; i++) { + // 剪枝一:若子集和超過 target ,則直接結束迴圈 + // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target + if (target - choices[i] < 0) { + break; + } + // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過 + if (i > start && choices[i] == choices[i - 1]) { + continue; + } + // 嘗試:做出選擇,更新 target, start + state.add(choices[i]); + // 進行下一輪選擇 + backtrack(state, target - choices[i], choices, i + 1, res); + // 回退:撤銷選擇,恢復到之前的狀態 + state.removeLast(); + } + } + + /* 求解子集和 II */ + List> subsetSumII(List nums, int target) { + List state = []; // 狀態(子集) + nums.sort(); // 對 nums 進行排序 + int start = 0; // 走訪起始點 + List> res = []; // 結果串列(子集串列) + backtrack(state, target, nums, start, res); + return res; + } + ``` + +=== "Rust" + + ```rust title="subset_sum_ii.rs" + /* 回溯演算法:子集和 II */ + fn backtrack( + mut state: Vec, + target: i32, + choices: &[i32], + start: usize, + res: &mut Vec>, + ) { + // 子集和等於 target 時,記錄解 + if target == 0 { + res.push(state); + return; + } + // 走訪所有選擇 + // 剪枝二:從 start 開始走訪,避免生成重複子集 + // 剪枝三:從 start 開始走訪,避免重複選擇同一元素 + for i in start..choices.len() { + // 剪枝一:若子集和超過 target ,則直接結束迴圈 + // 這是因為陣列已排序,後邊元素更大,子集和一定超過 target + if target - choices[i] < 0 { + break; + } + // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過 + if i > start && choices[i] == choices[i - 1] { + continue; + } + // 嘗試:做出選擇,更新 target, start + state.push(choices[i]); + // 進行下一輪選擇 + backtrack(state.clone(), target - choices[i], choices, i, res); + // 回退:撤銷選擇,恢復到之前的狀態 + state.pop(); + } + } + + /* 求解子集和 II */ + fn subset_sum_ii(nums: &mut [i32], target: i32) -> Vec> { + let state = Vec::new(); // 狀態(子集) + nums.sort(); // 對 nums 進行排序 + let start = 0; // 走訪起始點 + let mut res = Vec::new(); // 結果串列(子集串列) + backtrack(state, target, nums, start, &mut res); + res + } + ``` + +=== "C" + + ```c title="subset_sum_ii.c" + /* 回溯演算法:子集和 II */ + void backtrack(int target, int *choices, int choicesSize, int start) { + // 子集和等於 target 時,記錄解 + if (target == 0) { + for (int i = 0; i < stateSize; i++) { + res[resSize][i] = state[i]; + } + resColSizes[resSize++] = stateSize; + return; + } + // 走訪所有選擇 + // 剪枝二:從 start 開始走訪,避免生成重複子集 + // 剪枝三:從 start 開始走訪,避免重複選擇同一元素 + for (int i = start; i < choicesSize; i++) { + // 剪枝一:若子集和超過 target ,則直接跳過 + if (target - choices[i] < 0) { + continue; + } + // 剪枝四:如果該元素與左邊元素相等,說明該搜尋分支重複,直接跳過 + if (i > start && choices[i] == choices[i - 1]) { + continue; + } + // 嘗試:做出選擇,更新 target, start + state[stateSize] = choices[i]; + stateSize++; + // 進行下一輪選擇 + backtrack(target - choices[i], choices, choicesSize, i + 1); + // 回退:撤銷選擇,恢復到之前的狀態 + stateSize--; + } + } + + /* 求解子集和 II */ + void subsetSumII(int *nums, int numsSize, int target) { + // 對 nums 進行排序 + qsort(nums, numsSize, sizeof(int), cmp); + // 開始回溯 + backtrack(target, nums, numsSize, 0); + } + ``` + +=== "Kotlin" + + ```kotlin title="subset_sum_ii.kt" + /* 回溯演算法:子集和 II */ + fun backtrack( + state: MutableList, + target: Int, + choices: IntArray, + start: Int, + res: MutableList?> + ) { + // 子集和等於 target 時,記錄解 + if (target == 0) { + res.add(ArrayList(state)) + return + } + // 走訪所有選擇 + // 剪枝二:從 start 開始走訪,避免生成重複子集 + // 剪枝三:從 start 開始走訪,避免重複選擇同一元素 + for (i in start.. start && choices[i] == choices[i - 1]) { + continue + } + // 嘗試:做出選擇,更新 target, start + state.add(choices[i]) + // 進行下一輪選擇 + backtrack(state, target - choices[i], choices, i + 1, res) + // 回退:撤銷選擇,恢復到之前的狀態 + state.removeAt(state.size - 1) + } + } + + /* 求解子集和 II */ + fun subsetSumII(nums: IntArray, target: Int): List?> { + val state: MutableList = ArrayList() // 狀態(子集) + Arrays.sort(nums) // 對 nums 進行排序 + val start = 0 // 走訪起始點 + val res: MutableList?> = ArrayList() // 結果串列(子集串列) + backtrack(state, target, nums, start, res) + return res + } + ``` + +=== "Ruby" + + ```ruby title="subset_sum_ii.rb" + [class]{}-[func]{backtrack} + + [class]{}-[func]{subset_sum_ii} + ``` + +=== "Zig" + + ```zig title="subset_sum_ii.zig" + [class]{}-[func]{backtrack} + + [class]{}-[func]{subsetSumII} + ``` + +??? pythontutor "視覺化執行" + +
+ + +圖 13-14 展示了陣列 $[4, 4, 5]$ 和目標元素 $9$ 的回溯過程,共包含四種剪枝操作。請你將圖示與程式碼註釋相結合,理解整個搜尋過程,以及每種剪枝操作是如何工作的。 + +![子集和 II 回溯過程](subset_sum_problem.assets/subset_sum_ii.png){ class="animation-figure" } + +

圖 13-14   子集和 II 回溯過程

diff --git a/zh-Hant/docs/chapter_backtracking/summary.md b/zh-Hant/docs/chapter_backtracking/summary.md new file mode 100644 index 000000000..4cb7c6eab --- /dev/null +++ b/zh-Hant/docs/chapter_backtracking/summary.md @@ -0,0 +1,27 @@ +--- +comments: true +--- + +# 13.5   小結 + +### 1.   重點回顧 + +- 回溯演算法本質是窮舉法,透過對解空間進行深度優先走訪來尋找符合條件的解。在搜尋過程中,遇到滿足條件的解則記錄,直至找到所有解或走訪完成後結束。 +- 回溯演算法的搜尋過程包括嘗試與回退兩個部分。它透過深度優先搜尋來嘗試各種選擇,當遇到不滿足約束條件的情況時,則撤銷上一步的選擇,退回到之前的狀態,並繼續嘗試其他選擇。嘗試與回退是兩個方向相反的操作。 +- 回溯問題通常包含多個約束條件,它們可用於實現剪枝操作。剪枝可以提前結束不必要的搜尋分支,大幅提升搜尋效率。 +- 回溯演算法主要可用於解決搜尋問題和約束滿足問題。組合最佳化問題雖然可以用回溯演算法解決,但往往存在效率更高或效果更好的解法。 +- 全排列問題旨在搜尋給定集合元素的所有可能的排列。我們藉助一個陣列來記錄每個元素是否被選擇,剪掉重複選擇同一元素的搜尋分支,確保每個元素只被選擇一次。 +- 在全排列問題中,如果集合中存在重複元素,則最終結果會出現重複排列。我們需要約束相等元素在每輪中只能被選擇一次,這通常藉助一個雜湊表來實現。 +- 子集和問題的目標是在給定集合中找到和為目標值的所有子集。集合不區分元素順序,而搜尋過程會輸出所有順序的結果,產生重複子集。我們在回溯前將資料進行排序,並設定一個變數來指示每一輪的走訪起始點,從而將生成重複子集的搜尋分支進行剪枝。 +- 對於子集和問題,陣列中的相等元素會產生重複集合。我們利用陣列已排序的前置條件,透過判斷相鄰元素是否相等實現剪枝,從而確保相等元素在每輪中只能被選中一次。 +- $n$ 皇后問題旨在尋找將 $n$ 個皇后放置到 $n \times n$ 尺寸棋盤上的方案,要求所有皇后兩兩之間無法攻擊對方。該問題的約束條件有行約束、列約束、主對角線和次對角線約束。為滿足行約束,我們採用按行放置的策略,保證每一行放置一個皇后。 +- 列約束和對角線約束的處理方式類似。對於列約束,我們利用一個陣列來記錄每一列是否有皇后,從而指示選中的格子是否合法。對於對角線約束,我們藉助兩個陣列來分別記錄該主、次對角線上是否存在皇后;難點在於找處在到同一主(副)對角線上格子滿足的行列索引規律。 + +### 2.   Q & A + +**Q**:怎麼理解回溯和遞迴的關係? + +總的來看,回溯是一種“演算法策略”,而遞迴更像是一個“工具”。 + +- 回溯演算法通常基於遞迴實現。然而,回溯是遞迴的應用場景之一,是遞迴在搜尋問題中的應用。 +- 遞迴的結構體現了“子問題分解”的解題範式,常用於解決分治、回溯、動態規劃(記憶化遞迴)等問題。 diff --git a/zh-Hant/docs/chapter_computational_complexity/index.md b/zh-Hant/docs/chapter_computational_complexity/index.md new file mode 100644 index 000000000..b3f32664c --- /dev/null +++ b/zh-Hant/docs/chapter_computational_complexity/index.md @@ -0,0 +1,22 @@ +--- +comments: true +icon: material/timer-sand +--- + +# 第 2 章   複雜度分析 + +![複雜度分析](../assets/covers/chapter_complexity_analysis.jpg){ 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/) diff --git a/zh-Hant/docs/chapter_computational_complexity/iteration_and_recursion.md b/zh-Hant/docs/chapter_computational_complexity/iteration_and_recursion.md new file mode 100644 index 000000000..ad21651ee --- /dev/null +++ b/zh-Hant/docs/chapter_computational_complexity/iteration_and_recursion.md @@ -0,0 +1,2066 @@ +--- +comments: true +--- + +# 2.2   迭代與遞迴 + +在演算法中,重複執行某個任務是很常見的,它與複雜度分析息息相關。因此,在介紹時間複雜度和空間複雜度之前,我們先來了解如何在程式中實現重複執行任務,即兩種基本的程式控制結構:迭代、遞迴。 + +## 2.2.1   迭代 + +迭代(iteration)是一種重複執行某個任務的控制結構。在迭代中,程式會在滿足一定的條件下重複執行某段程式碼,直到這個條件不再滿足。 + +### 1.   for 迴圈 + +`for` 迴圈是最常見的迭代形式之一,**適合在預先知道迭代次數時使用**。 + +以下函式基於 `for` 迴圈實現了求和 $1 + 2 + \dots + n$ ,求和結果使用變數 `res` 記錄。需要注意的是,Python 中 `range(a, b)` 對應的區間是“左閉右開”的,對應的走訪範圍為 $a, a + 1, \dots, b-1$ : + +=== "Python" + + ```python title="iteration.py" + def for_loop(n: int) -> int: + """for 迴圈""" + res = 0 + # 迴圈求和 1, 2, ..., n-1, n + for i in range(1, n + 1): + res += i + return res + ``` + +=== "C++" + + ```cpp title="iteration.cpp" + /* for 迴圈 */ + int forLoop(int n) { + int res = 0; + // 迴圈求和 1, 2, ..., n-1, n + for (int i = 1; i <= n; ++i) { + res += i; + } + return res; + } + ``` + +=== "Java" + + ```java title="iteration.java" + /* for 迴圈 */ + int forLoop(int n) { + int res = 0; + // 迴圈求和 1, 2, ..., n-1, n + for (int i = 1; i <= n; i++) { + res += i; + } + return res; + } + ``` + +=== "C#" + + ```csharp title="iteration.cs" + /* for 迴圈 */ + int ForLoop(int n) { + int res = 0; + // 迴圈求和 1, 2, ..., n-1, n + for (int i = 1; i <= n; i++) { + res += i; + } + return res; + } + ``` + +=== "Go" + + ```go title="iteration.go" + /* for 迴圈 */ + func forLoop(n int) int { + res := 0 + // 迴圈求和 1, 2, ..., n-1, n + for i := 1; i <= n; i++ { + res += i + } + return res + } + ``` + +=== "Swift" + + ```swift title="iteration.swift" + /* for 迴圈 */ + func forLoop(n: Int) -> Int { + var res = 0 + // 迴圈求和 1, 2, ..., n-1, n + for i in 1 ... n { + res += i + } + return res + } + ``` + +=== "JS" + + ```javascript title="iteration.js" + /* for 迴圈 */ + function forLoop(n) { + let res = 0; + // 迴圈求和 1, 2, ..., n-1, n + for (let i = 1; i <= n; i++) { + res += i; + } + return res; + } + ``` + +=== "TS" + + ```typescript title="iteration.ts" + /* for 迴圈 */ + function forLoop(n: number): number { + let res = 0; + // 迴圈求和 1, 2, ..., n-1, n + for (let i = 1; i <= n; i++) { + res += i; + } + return res; + } + ``` + +=== "Dart" + + ```dart title="iteration.dart" + /* for 迴圈 */ + int forLoop(int n) { + int res = 0; + // 迴圈求和 1, 2, ..., n-1, n + for (int i = 1; i <= n; i++) { + res += i; + } + return res; + } + ``` + +=== "Rust" + + ```rust title="iteration.rs" + /* for 迴圈 */ + fn for_loop(n: i32) -> i32 { + let mut res = 0; + // 迴圈求和 1, 2, ..., n-1, n + for i in 1..=n { + res += i; + } + res + } + ``` + +=== "C" + + ```c title="iteration.c" + /* for 迴圈 */ + int forLoop(int n) { + int res = 0; + // 迴圈求和 1, 2, ..., n-1, n + for (int i = 1; i <= n; i++) { + res += i; + } + return res; + } + ``` + +=== "Kotlin" + + ```kotlin title="iteration.kt" + /* for 迴圈 */ + fun forLoop(n: Int): Int { + var res = 0 + // 迴圈求和 1, 2, ..., n-1, n + for (i in 1..n) { + res += i + } + return res + } + ``` + +=== "Ruby" + + ```ruby title="iteration.rb" + ### for 迴圈 ### + def for_loop(n) + res = 0 + + # 迴圈求和 1, 2, ..., n-1, n + for i in 1..n + res += i + end + + res + end + ``` + +=== "Zig" + + ```zig title="iteration.zig" + // for 迴圈 + fn forLoop(n: usize) i32 { + var res: i32 = 0; + // 迴圈求和 1, 2, ..., n-1, n + for (1..n+1) |i| { + res = res + @as(i32, @intCast(i)); + } + return res; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +圖 2-1 是該求和函式的流程框圖。 + +![求和函式的流程框圖](iteration_and_recursion.assets/iteration.png){ class="animation-figure" } + +

圖 2-1   求和函式的流程框圖

+ +此求和函式的操作數量與輸入資料大小 $n$ 成正比,或者說成“線性關係”。實際上,**時間複雜度描述的就是這個“線性關係”**。相關內容將會在下一節中詳細介紹。 + +### 2.   while 迴圈 + +與 `for` 迴圈類似,`while` 迴圈也是一種實現迭代的方法。在 `while` 迴圈中,程式每輪都會先檢查條件,如果條件為真,則繼續執行,否則就結束迴圈。 + +下面我們用 `while` 迴圈來實現求和 $1 + 2 + \dots + n$ : + +=== "Python" + + ```python title="iteration.py" + def while_loop(n: int) -> int: + """while 迴圈""" + res = 0 + i = 1 # 初始化條件變數 + # 迴圈求和 1, 2, ..., n-1, n + while i <= n: + res += i + i += 1 # 更新條件變數 + return res + ``` + +=== "C++" + + ```cpp title="iteration.cpp" + /* while 迴圈 */ + int whileLoop(int n) { + int res = 0; + int i = 1; // 初始化條件變數 + // 迴圈求和 1, 2, ..., n-1, n + while (i <= n) { + res += i; + i++; // 更新條件變數 + } + return res; + } + ``` + +=== "Java" + + ```java title="iteration.java" + /* while 迴圈 */ + int whileLoop(int n) { + int res = 0; + int i = 1; // 初始化條件變數 + // 迴圈求和 1, 2, ..., n-1, n + while (i <= n) { + res += i; + i++; // 更新條件變數 + } + return res; + } + ``` + +=== "C#" + + ```csharp title="iteration.cs" + /* while 迴圈 */ + int WhileLoop(int n) { + int res = 0; + int i = 1; // 初始化條件變數 + // 迴圈求和 1, 2, ..., n-1, n + while (i <= n) { + res += i; + i += 1; // 更新條件變數 + } + return res; + } + ``` + +=== "Go" + + ```go title="iteration.go" + /* while 迴圈 */ + func whileLoop(n int) int { + res := 0 + // 初始化條件變數 + i := 1 + // 迴圈求和 1, 2, ..., n-1, n + for i <= n { + res += i + // 更新條件變數 + i++ + } + return res + } + ``` + +=== "Swift" + + ```swift title="iteration.swift" + /* while 迴圈 */ + func whileLoop(n: Int) -> Int { + var res = 0 + var i = 1 // 初始化條件變數 + // 迴圈求和 1, 2, ..., n-1, n + while i <= n { + res += i + i += 1 // 更新條件變數 + } + return res + } + ``` + +=== "JS" + + ```javascript title="iteration.js" + /* while 迴圈 */ + function whileLoop(n) { + let res = 0; + let i = 1; // 初始化條件變數 + // 迴圈求和 1, 2, ..., n-1, n + while (i <= n) { + res += i; + i++; // 更新條件變數 + } + return res; + } + ``` + +=== "TS" + + ```typescript title="iteration.ts" + /* while 迴圈 */ + function whileLoop(n: number): number { + let res = 0; + let i = 1; // 初始化條件變數 + // 迴圈求和 1, 2, ..., n-1, n + while (i <= n) { + res += i; + i++; // 更新條件變數 + } + return res; + } + ``` + +=== "Dart" + + ```dart title="iteration.dart" + /* while 迴圈 */ + int whileLoop(int n) { + int res = 0; + int i = 1; // 初始化條件變數 + // 迴圈求和 1, 2, ..., n-1, n + while (i <= n) { + res += i; + i++; // 更新條件變數 + } + return res; + } + ``` + +=== "Rust" + + ```rust title="iteration.rs" + /* while 迴圈 */ + fn while_loop(n: i32) -> i32 { + let mut res = 0; + let mut i = 1; // 初始化條件變數 + + // 迴圈求和 1, 2, ..., n-1, n + while i <= n { + res += i; + i += 1; // 更新條件變數 + } + res + } + ``` + +=== "C" + + ```c title="iteration.c" + /* while 迴圈 */ + int whileLoop(int n) { + int res = 0; + int i = 1; // 初始化條件變數 + // 迴圈求和 1, 2, ..., n-1, n + while (i <= n) { + res += i; + i++; // 更新條件變數 + } + return res; + } + ``` + +=== "Kotlin" + + ```kotlin title="iteration.kt" + /* while 迴圈 */ + fun whileLoop(n: Int): Int { + var res = 0 + var i = 1 // 初始化條件變數 + // 迴圈求和 1, 2, ..., n-1, n + while (i <= n) { + res += i + i++ // 更新條件變數 + } + return res + } + ``` + +=== "Ruby" + + ```ruby title="iteration.rb" + ### while 迴圈 ### + def while_loop(n) + res = 0 + i = 1 # 初始化條件變數 + + # 迴圈求和 1, 2, ..., n-1, n + while i <= n + res += i + i += 1 # 更新條件變數 + end + + res + end + ``` + +=== "Zig" + + ```zig title="iteration.zig" + // while 迴圈 + fn whileLoop(n: i32) i32 { + var res: i32 = 0; + var i: i32 = 1; // 初始化條件變數 + // 迴圈求和 1, 2, ..., n-1, n + while (i <= n) { + res += @intCast(i); + i += 1; + } + return res; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +**`while` 迴圈比 `for` 迴圈的自由度更高**。在 `while` 迴圈中,我們可以自由地設計條件變數的初始化和更新步驟。 + +例如在以下程式碼中,條件變數 $i$ 每輪進行兩次更新,這種情況就不太方便用 `for` 迴圈實現: + +=== "Python" + + ```python title="iteration.py" + def while_loop_ii(n: int) -> int: + """while 迴圈(兩次更新)""" + res = 0 + i = 1 # 初始化條件變數 + # 迴圈求和 1, 4, 10, ... + while i <= n: + res += i + # 更新條件變數 + i += 1 + i *= 2 + return res + ``` + +=== "C++" + + ```cpp title="iteration.cpp" + /* while 迴圈(兩次更新) */ + int whileLoopII(int n) { + int res = 0; + int i = 1; // 初始化條件變數 + // 迴圈求和 1, 4, 10, ... + while (i <= n) { + res += i; + // 更新條件變數 + i++; + i *= 2; + } + return res; + } + ``` + +=== "Java" + + ```java title="iteration.java" + /* while 迴圈(兩次更新) */ + int whileLoopII(int n) { + int res = 0; + int i = 1; // 初始化條件變數 + // 迴圈求和 1, 4, 10, ... + while (i <= n) { + res += i; + // 更新條件變數 + i++; + i *= 2; + } + return res; + } + ``` + +=== "C#" + + ```csharp title="iteration.cs" + /* while 迴圈(兩次更新) */ + int WhileLoopII(int n) { + int res = 0; + int i = 1; // 初始化條件變數 + // 迴圈求和 1, 4, 10, ... + while (i <= n) { + res += i; + // 更新條件變數 + i += 1; + i *= 2; + } + return res; + } + ``` + +=== "Go" + + ```go title="iteration.go" + /* while 迴圈(兩次更新) */ + func whileLoopII(n int) int { + res := 0 + // 初始化條件變數 + i := 1 + // 迴圈求和 1, 4, 10, ... + for i <= n { + res += i + // 更新條件變數 + i++ + i *= 2 + } + return res + } + ``` + +=== "Swift" + + ```swift title="iteration.swift" + /* while 迴圈(兩次更新) */ + func whileLoopII(n: Int) -> Int { + var res = 0 + var i = 1 // 初始化條件變數 + // 迴圈求和 1, 4, 10, ... + while i <= n { + res += i + // 更新條件變數 + i += 1 + i *= 2 + } + return res + } + ``` + +=== "JS" + + ```javascript title="iteration.js" + /* while 迴圈(兩次更新) */ + function whileLoopII(n) { + let res = 0; + let i = 1; // 初始化條件變數 + // 迴圈求和 1, 4, 10, ... + while (i <= n) { + res += i; + // 更新條件變數 + i++; + i *= 2; + } + return res; + } + ``` + +=== "TS" + + ```typescript title="iteration.ts" + /* while 迴圈(兩次更新) */ + function whileLoopII(n: number): number { + let res = 0; + let i = 1; // 初始化條件變數 + // 迴圈求和 1, 4, 10, ... + while (i <= n) { + res += i; + // 更新條件變數 + i++; + i *= 2; + } + return res; + } + ``` + +=== "Dart" + + ```dart title="iteration.dart" + /* while 迴圈(兩次更新) */ + int whileLoopII(int n) { + int res = 0; + int i = 1; // 初始化條件變數 + // 迴圈求和 1, 4, 10, ... + while (i <= n) { + res += i; + // 更新條件變數 + i++; + i *= 2; + } + return res; + } + ``` + +=== "Rust" + + ```rust title="iteration.rs" + /* while 迴圈(兩次更新) */ + fn while_loop_ii(n: i32) -> i32 { + let mut res = 0; + let mut i = 1; // 初始化條件變數 + + // 迴圈求和 1, 4, 10, ... + while i <= n { + res += i; + // 更新條件變數 + i += 1; + i *= 2; + } + res + } + ``` + +=== "C" + + ```c title="iteration.c" + /* while 迴圈(兩次更新) */ + int whileLoopII(int n) { + int res = 0; + int i = 1; // 初始化條件變數 + // 迴圈求和 1, 4, 10, ... + while (i <= n) { + res += i; + // 更新條件變數 + i++; + i *= 2; + } + return res; + } + ``` + +=== "Kotlin" + + ```kotlin title="iteration.kt" + /* while 迴圈(兩次更新) */ + fun whileLoopII(n: Int): Int { + var res = 0 + var i = 1 // 初始化條件變數 + // 迴圈求和 1, 4, 10, ... + while (i <= n) { + res += i + // 更新條件變數 + i++ + i *= 2 + } + return res + } + ``` + +=== "Ruby" + + ```ruby title="iteration.rb" + ### while 迴圈(兩次更新)### + def while_loop_ii(n) + res = 0 + i = 1 # 初始化條件變數 + + # 迴圈求和 1, 4, 10, ... + while i <= n + res += i + # 更新條件變數 + i += 1 + i *= 2 + end + + res + end + ``` + +=== "Zig" + + ```zig title="iteration.zig" + // while 迴圈(兩次更新) + fn whileLoopII(n: i32) i32 { + var res: i32 = 0; + var i: i32 = 1; // 初始化條件變數 + // 迴圈求和 1, 4, 10, ... + while (i <= n) { + res += @intCast(i); + // 更新條件變數 + i += 1; + i *= 2; + } + return res; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +總的來說,**`for` 迴圈的程式碼更加緊湊,`while` 迴圈更加靈活**,兩者都可以實現迭代結構。選擇使用哪一個應該根據特定問題的需求來決定。 + +### 3.   巢狀迴圈 + +我們可以在一個迴圈結構內巢狀另一個迴圈結構,下面以 `for` 迴圈為例: + +=== "Python" + + ```python title="iteration.py" + def nested_for_loop(n: int) -> str: + """雙層 for 迴圈""" + res = "" + # 迴圈 i = 1, 2, ..., n-1, n + for i in range(1, n + 1): + # 迴圈 j = 1, 2, ..., n-1, n + for j in range(1, n + 1): + res += f"({i}, {j}), " + return res + ``` + +=== "C++" + + ```cpp title="iteration.cpp" + /* 雙層 for 迴圈 */ + string nestedForLoop(int n) { + ostringstream res; + // 迴圈 i = 1, 2, ..., n-1, n + for (int i = 1; i <= n; ++i) { + // 迴圈 j = 1, 2, ..., n-1, n + for (int j = 1; j <= n; ++j) { + res << "(" << i << ", " << j << "), "; + } + } + return res.str(); + } + ``` + +=== "Java" + + ```java title="iteration.java" + /* 雙層 for 迴圈 */ + String nestedForLoop(int n) { + StringBuilder res = new StringBuilder(); + // 迴圈 i = 1, 2, ..., n-1, n + for (int i = 1; i <= n; i++) { + // 迴圈 j = 1, 2, ..., n-1, n + for (int j = 1; j <= n; j++) { + res.append("(" + i + ", " + j + "), "); + } + } + return res.toString(); + } + ``` + +=== "C#" + + ```csharp title="iteration.cs" + /* 雙層 for 迴圈 */ + string NestedForLoop(int n) { + StringBuilder res = new(); + // 迴圈 i = 1, 2, ..., n-1, n + for (int i = 1; i <= n; i++) { + // 迴圈 j = 1, 2, ..., n-1, n + for (int j = 1; j <= n; j++) { + res.Append($"({i}, {j}), "); + } + } + return res.ToString(); + } + ``` + +=== "Go" + + ```go title="iteration.go" + /* 雙層 for 迴圈 */ + func nestedForLoop(n int) string { + res := "" + // 迴圈 i = 1, 2, ..., n-1, n + for i := 1; i <= n; i++ { + for j := 1; j <= n; j++ { + // 迴圈 j = 1, 2, ..., n-1, n + res += fmt.Sprintf("(%d, %d), ", i, j) + } + } + return res + } + ``` + +=== "Swift" + + ```swift title="iteration.swift" + /* 雙層 for 迴圈 */ + func nestedForLoop(n: Int) -> String { + var res = "" + // 迴圈 i = 1, 2, ..., n-1, n + for i in 1 ... n { + // 迴圈 j = 1, 2, ..., n-1, n + for j in 1 ... n { + res.append("(\(i), \(j)), ") + } + } + return res + } + ``` + +=== "JS" + + ```javascript title="iteration.js" + /* 雙層 for 迴圈 */ + function nestedForLoop(n) { + let res = ''; + // 迴圈 i = 1, 2, ..., n-1, n + for (let i = 1; i <= n; i++) { + // 迴圈 j = 1, 2, ..., n-1, n + for (let j = 1; j <= n; j++) { + res += `(${i}, ${j}), `; + } + } + return res; + } + ``` + +=== "TS" + + ```typescript title="iteration.ts" + /* 雙層 for 迴圈 */ + function nestedForLoop(n: number): string { + let res = ''; + // 迴圈 i = 1, 2, ..., n-1, n + for (let i = 1; i <= n; i++) { + // 迴圈 j = 1, 2, ..., n-1, n + for (let j = 1; j <= n; j++) { + res += `(${i}, ${j}), `; + } + } + return res; + } + ``` + +=== "Dart" + + ```dart title="iteration.dart" + /* 雙層 for 迴圈 */ + String nestedForLoop(int n) { + String res = ""; + // 迴圈 i = 1, 2, ..., n-1, n + for (int i = 1; i <= n; i++) { + // 迴圈 j = 1, 2, ..., n-1, n + for (int j = 1; j <= n; j++) { + res += "($i, $j), "; + } + } + return res; + } + ``` + +=== "Rust" + + ```rust title="iteration.rs" + /* 雙層 for 迴圈 */ + fn nested_for_loop(n: i32) -> String { + let mut res = vec![]; + // 迴圈 i = 1, 2, ..., n-1, n + for i in 1..=n { + // 迴圈 j = 1, 2, ..., n-1, n + for j in 1..=n { + res.push(format!("({}, {}), ", i, j)); + } + } + res.join("") + } + ``` + +=== "C" + + ```c title="iteration.c" + /* 雙層 for 迴圈 */ + char *nestedForLoop(int n) { + // n * n 為對應點數量,"(i, j), " 對應字串長最大為 6+10*2,加上最後一個空字元 \0 的額外空間 + int size = n * n * 26 + 1; + char *res = malloc(size * sizeof(char)); + // 迴圈 i = 1, 2, ..., n-1, n + for (int i = 1; i <= n; i++) { + // 迴圈 j = 1, 2, ..., n-1, n + for (int j = 1; j <= n; j++) { + char tmp[26]; + snprintf(tmp, sizeof(tmp), "(%d, %d), ", i, j); + strncat(res, tmp, size - strlen(res) - 1); + } + } + return res; + } + ``` + +=== "Kotlin" + + ```kotlin title="iteration.kt" + /* 雙層 for 迴圈 */ + fun nestedForLoop(n: Int): String { + val res = StringBuilder() + // 迴圈 i = 1, 2, ..., n-1, n + for (i in 1..n) { + // 迴圈 j = 1, 2, ..., n-1, n + for (j in 1..n) { + res.append(" ($i, $j), ") + } + } + return res.toString() + } + ``` + +=== "Ruby" + + ```ruby title="iteration.rb" + ### 雙層 for 迴圈 ### + def nested_for_loop(n) + res = "" + + # 迴圈 i = 1, 2, ..., n-1, n + for i in 1..n + # 迴圈 j = 1, 2, ..., n-1, n + for j in 1..n + res += "(#{i}, #{j}), " + end + end + + res + end + ``` + +=== "Zig" + + ```zig title="iteration.zig" + // 雙層 for 迴圈 + fn nestedForLoop(allocator: Allocator, n: usize) ![]const u8 { + var res = std.ArrayList(u8).init(allocator); + defer res.deinit(); + var buffer: [20]u8 = undefined; + // 迴圈 i = 1, 2, ..., n-1, n + for (1..n+1) |i| { + // 迴圈 j = 1, 2, ..., n-1, n + for (1..n+1) |j| { + var _str = try std.fmt.bufPrint(&buffer, "({d}, {d}), ", .{i, j}); + try res.appendSlice(_str); + } + } + return res.toOwnedSlice(); + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +圖 2-2 是該巢狀迴圈的流程框圖。 + +![巢狀迴圈的流程框圖](iteration_and_recursion.assets/nested_iteration.png){ class="animation-figure" } + +

圖 2-2   巢狀迴圈的流程框圖

+ +在這種情況下,函式的操作數量與 $n^2$ 成正比,或者說演算法執行時間和輸入資料大小 $n$ 成“平方關係”。 + +我們可以繼續新增巢狀迴圈,每一次巢狀都是一次“升維”,將會使時間複雜度提高至“立方關係”“四次方關係”,以此類推。 + +## 2.2.2   遞迴 + + 遞迴(recursion)是一種演算法策略,透過函式呼叫自身來解決問題。它主要包含兩個階段。 + +1. **遞**:程式不斷深入地呼叫自身,通常傳入更小或更簡化的參數,直到達到“終止條件”。 +2. **迴**:觸發“終止條件”後,程式從最深層的遞迴函式開始逐層返回,匯聚每一層的結果。 + +而從實現的角度看,遞迴程式碼主要包含三個要素。 + +1. **終止條件**:用於決定什麼時候由“遞”轉“迴”。 +2. **遞迴呼叫**:對應“遞”,函式呼叫自身,通常輸入更小或更簡化的參數。 +3. **返回結果**:對應“迴”,將當前遞迴層級的結果返回至上一層。 + +觀察以下程式碼,我們只需呼叫函式 `recur(n)` ,就可以完成 $1 + 2 + \dots + n$ 的計算: + +=== "Python" + + ```python title="recursion.py" + def recur(n: int) -> int: + """遞迴""" + # 終止條件 + if n == 1: + return 1 + # 遞:遞迴呼叫 + res = recur(n - 1) + # 迴:返回結果 + return n + res + ``` + +=== "C++" + + ```cpp title="recursion.cpp" + /* 遞迴 */ + int recur(int n) { + // 終止條件 + if (n == 1) + return 1; + // 遞:遞迴呼叫 + int res = recur(n - 1); + // 迴:返回結果 + return n + res; + } + ``` + +=== "Java" + + ```java title="recursion.java" + /* 遞迴 */ + int recur(int n) { + // 終止條件 + if (n == 1) + return 1; + // 遞:遞迴呼叫 + int res = recur(n - 1); + // 迴:返回結果 + return n + res; + } + ``` + +=== "C#" + + ```csharp title="recursion.cs" + /* 遞迴 */ + int Recur(int n) { + // 終止條件 + if (n == 1) + return 1; + // 遞:遞迴呼叫 + int res = Recur(n - 1); + // 迴:返回結果 + return n + res; + } + ``` + +=== "Go" + + ```go title="recursion.go" + /* 遞迴 */ + func recur(n int) int { + // 終止條件 + if n == 1 { + return 1 + } + // 遞:遞迴呼叫 + res := recur(n - 1) + // 迴:返回結果 + return n + res + } + ``` + +=== "Swift" + + ```swift title="recursion.swift" + /* 遞迴 */ + func recur(n: Int) -> Int { + // 終止條件 + if n == 1 { + return 1 + } + // 遞:遞迴呼叫 + let res = recur(n: n - 1) + // 迴:返回結果 + return n + res + } + ``` + +=== "JS" + + ```javascript title="recursion.js" + /* 遞迴 */ + function recur(n) { + // 終止條件 + if (n === 1) return 1; + // 遞:遞迴呼叫 + const res = recur(n - 1); + // 迴:返回結果 + return n + res; + } + ``` + +=== "TS" + + ```typescript title="recursion.ts" + /* 遞迴 */ + function recur(n: number): number { + // 終止條件 + if (n === 1) return 1; + // 遞:遞迴呼叫 + const res = recur(n - 1); + // 迴:返回結果 + return n + res; + } + ``` + +=== "Dart" + + ```dart title="recursion.dart" + /* 遞迴 */ + int recur(int n) { + // 終止條件 + if (n == 1) return 1; + // 遞:遞迴呼叫 + int res = recur(n - 1); + // 迴:返回結果 + return n + res; + } + ``` + +=== "Rust" + + ```rust title="recursion.rs" + /* 遞迴 */ + fn recur(n: i32) -> i32 { + // 終止條件 + if n == 1 { + return 1; + } + // 遞:遞迴呼叫 + let res = recur(n - 1); + // 迴:返回結果 + n + res + } + ``` + +=== "C" + + ```c title="recursion.c" + /* 遞迴 */ + int recur(int n) { + // 終止條件 + if (n == 1) + return 1; + // 遞:遞迴呼叫 + int res = recur(n - 1); + // 迴:返回結果 + return n + res; + } + ``` + +=== "Kotlin" + + ```kotlin title="recursion.kt" + /* 遞迴 */ + fun recur(n: Int): Int { + // 終止條件 + if (n == 1) + return 1 + // 遞: 遞迴呼叫 + val res = recur(n - 1) + // 迴: 返回結果 + return n + res + } + ``` + +=== "Ruby" + + ```ruby title="recursion.rb" + ### 遞迴 ### + def recur(n) + # 終止條件 + return 1 if n == 1 + # 遞:遞迴呼叫 + res = recur(n - 1) + # 迴:返回結果 + n + res + end + ``` + +=== "Zig" + + ```zig title="recursion.zig" + // 遞迴函式 + fn recur(n: i32) i32 { + // 終止條件 + if (n == 1) { + return 1; + } + // 遞:遞迴呼叫 + var res: i32 = recur(n - 1); + // 迴:返回結果 + return n + res; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +圖 2-3 展示了該函式的遞迴過程。 + +![求和函式的遞迴過程](iteration_and_recursion.assets/recursion_sum.png){ class="animation-figure" } + +

圖 2-3   求和函式的遞迴過程

+ +雖然從計算角度看,迭代與遞迴可以得到相同的結果,**但它們代表了兩種完全不同的思考和解決問題的範式**。 + +- **迭代**:“自下而上”地解決問題。從最基礎的步驟開始,然後不斷重複或累加這些步驟,直到任務完成。 +- **遞迴**:“自上而下”地解決問題。將原問題分解為更小的子問題,這些子問題和原問題具有相同的形式。接下來將子問題繼續分解為更小的子問題,直到基本情況時停止(基本情況的解是已知的)。 + +以上述求和函式為例,設問題 $f(n) = 1 + 2 + \dots + n$ 。 + +- **迭代**:在迴圈中模擬求和過程,從 $1$ 走訪到 $n$ ,每輪執行求和操作,即可求得 $f(n)$ 。 +- **遞迴**:將問題分解為子問題 $f(n) = n + f(n-1)$ ,不斷(遞迴地)分解下去,直至基本情況 $f(1) = 1$ 時終止。 + +### 1.   呼叫堆疊 + +遞迴函式每次呼叫自身時,系統都會為新開啟的函式分配記憶體,以儲存區域性變數、呼叫位址和其他資訊等。這將導致兩方面的結果。 + +- 函式的上下文資料都儲存在稱為“堆疊幀空間”的記憶體區域中,直至函式返回後才會被釋放。因此,**遞迴通常比迭代更加耗費記憶體空間**。 +- 遞迴呼叫函式會產生額外的開銷。**因此遞迴通常比迴圈的時間效率更低**。 + +如圖 2-4 所示,在觸發終止條件前,同時存在 $n$ 個未返回的遞迴函式,**遞迴深度為 $n$** 。 + +![遞迴呼叫深度](iteration_and_recursion.assets/recursion_sum_depth.png){ class="animation-figure" } + +

圖 2-4   遞迴呼叫深度

+ +在實際中,程式語言允許的遞迴深度通常是有限的,過深的遞迴可能導致堆疊溢位錯誤。 + +### 2.   尾遞迴 + +有趣的是,**如果函式在返回前的最後一步才進行遞迴呼叫**,則該函式可以被編譯器或直譯器最佳化,使其在空間效率上與迭代相當。這種情況被稱為尾遞迴(tail recursion)。 + +- **普通遞迴**:當函式返回到上一層級的函式後,需要繼續執行程式碼,因此系統需要儲存上一層呼叫的上下文。 +- **尾遞迴**:遞迴呼叫是函式返回前的最後一個操作,這意味著函式返回到上一層級後,無須繼續執行其他操作,因此系統無須儲存上一層函式的上下文。 + +以計算 $1 + 2 + \dots + n$ 為例,我們可以將結果變數 `res` 設為函式參數,從而實現尾遞迴: + +=== "Python" + + ```python title="recursion.py" + def tail_recur(n, res): + """尾遞迴""" + # 終止條件 + if n == 0: + return res + # 尾遞迴呼叫 + return tail_recur(n - 1, res + n) + ``` + +=== "C++" + + ```cpp title="recursion.cpp" + /* 尾遞迴 */ + int tailRecur(int n, int res) { + // 終止條件 + if (n == 0) + return res; + // 尾遞迴呼叫 + return tailRecur(n - 1, res + n); + } + ``` + +=== "Java" + + ```java title="recursion.java" + /* 尾遞迴 */ + int tailRecur(int n, int res) { + // 終止條件 + if (n == 0) + return res; + // 尾遞迴呼叫 + return tailRecur(n - 1, res + n); + } + ``` + +=== "C#" + + ```csharp title="recursion.cs" + /* 尾遞迴 */ + int TailRecur(int n, int res) { + // 終止條件 + if (n == 0) + return res; + // 尾遞迴呼叫 + return TailRecur(n - 1, res + n); + } + ``` + +=== "Go" + + ```go title="recursion.go" + /* 尾遞迴 */ + func tailRecur(n int, res int) int { + // 終止條件 + if n == 0 { + return res + } + // 尾遞迴呼叫 + return tailRecur(n-1, res+n) + } + ``` + +=== "Swift" + + ```swift title="recursion.swift" + /* 尾遞迴 */ + func tailRecur(n: Int, res: Int) -> Int { + // 終止條件 + if n == 0 { + return res + } + // 尾遞迴呼叫 + return tailRecur(n: n - 1, res: res + n) + } + ``` + +=== "JS" + + ```javascript title="recursion.js" + /* 尾遞迴 */ + function tailRecur(n, res) { + // 終止條件 + if (n === 0) return res; + // 尾遞迴呼叫 + return tailRecur(n - 1, res + n); + } + ``` + +=== "TS" + + ```typescript title="recursion.ts" + /* 尾遞迴 */ + function tailRecur(n: number, res: number): number { + // 終止條件 + if (n === 0) return res; + // 尾遞迴呼叫 + return tailRecur(n - 1, res + n); + } + ``` + +=== "Dart" + + ```dart title="recursion.dart" + /* 尾遞迴 */ + int tailRecur(int n, int res) { + // 終止條件 + if (n == 0) return res; + // 尾遞迴呼叫 + return tailRecur(n - 1, res + n); + } + ``` + +=== "Rust" + + ```rust title="recursion.rs" + /* 尾遞迴 */ + fn tail_recur(n: i32, res: i32) -> i32 { + // 終止條件 + if n == 0 { + return res; + } + // 尾遞迴呼叫 + tail_recur(n - 1, res + n) + } + ``` + +=== "C" + + ```c title="recursion.c" + /* 尾遞迴 */ + int tailRecur(int n, int res) { + // 終止條件 + if (n == 0) + return res; + // 尾遞迴呼叫 + return tailRecur(n - 1, res + n); + } + ``` + +=== "Kotlin" + + ```kotlin title="recursion.kt" + /* 尾遞迴 */ + tailrec fun tailRecur(n: Int, res: Int): Int { + // 新增 tailrec 關鍵詞,以開啟尾遞迴最佳化 + // 終止條件 + if (n == 0) + return res + // 尾遞迴呼叫 + return tailRecur(n - 1, res + n) + } + ``` + +=== "Ruby" + + ```ruby title="recursion.rb" + ### 尾遞迴 ### + def tail_recur(n, res) + # 終止條件 + return res if n == 0 + # 尾遞迴呼叫 + tail_recur(n - 1, res + n) + end + ``` + +=== "Zig" + + ```zig title="recursion.zig" + // 尾遞迴函式 + fn tailRecur(n: i32, res: i32) i32 { + // 終止條件 + if (n == 0) { + return res; + } + // 尾遞迴呼叫 + return tailRecur(n - 1, res + n); + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +尾遞迴的執行過程如圖 2-5 所示。對比普通遞迴和尾遞迴,兩者的求和操作的執行點是不同的。 + +- **普通遞迴**:求和操作是在“迴”的過程中執行的,每層返回後都要再執行一次求和操作。 +- **尾遞迴**:求和操作是在“遞”的過程中執行的,“迴”的過程只需層層返回。 + +![尾遞迴過程](iteration_and_recursion.assets/tail_recursion_sum.png){ class="animation-figure" } + +

圖 2-5   尾遞迴過程

+ +!!! tip + + 請注意,許多編譯器或直譯器並不支持尾遞迴最佳化。例如,Python 預設不支持尾遞迴最佳化,因此即使函式是尾遞迴形式,仍然可能會遇到堆疊溢位問題。 + +### 3.   遞迴樹 + +當處理與“分治”相關的演算法問題時,遞迴往往比迭代的思路更加直觀、程式碼更加易讀。以“費波那契數列”為例。 + +!!! question + + 給定一個費波那契數列 $0, 1, 1, 2, 3, 5, 8, 13, \dots$ ,求該數列的第 $n$ 個數字。 + +設費波那契數列的第 $n$ 個數字為 $f(n)$ ,易得兩個結論。 + +- 數列的前兩個數字為 $f(1) = 0$ 和 $f(2) = 1$ 。 +- 數列中的每個數字是前兩個數字的和,即 $f(n) = f(n - 1) + f(n - 2)$ 。 + +按照遞推關係進行遞迴呼叫,將前兩個數字作為終止條件,便可寫出遞迴程式碼。呼叫 `fib(n)` 即可得到費波那契數列的第 $n$ 個數字: + +=== "Python" + + ```python title="recursion.py" + def fib(n: int) -> int: + """費波那契數列:遞迴""" + # 終止條件 f(1) = 0, f(2) = 1 + if n == 1 or n == 2: + return n - 1 + # 遞迴呼叫 f(n) = f(n-1) + f(n-2) + res = fib(n - 1) + fib(n - 2) + # 返回結果 f(n) + return res + ``` + +=== "C++" + + ```cpp title="recursion.cpp" + /* 費波那契數列:遞迴 */ + int fib(int n) { + // 終止條件 f(1) = 0, f(2) = 1 + if (n == 1 || n == 2) + return n - 1; + // 遞迴呼叫 f(n) = f(n-1) + f(n-2) + int res = fib(n - 1) + fib(n - 2); + // 返回結果 f(n) + return res; + } + ``` + +=== "Java" + + ```java title="recursion.java" + /* 費波那契數列:遞迴 */ + int fib(int n) { + // 終止條件 f(1) = 0, f(2) = 1 + if (n == 1 || n == 2) + return n - 1; + // 遞迴呼叫 f(n) = f(n-1) + f(n-2) + int res = fib(n - 1) + fib(n - 2); + // 返回結果 f(n) + return res; + } + ``` + +=== "C#" + + ```csharp title="recursion.cs" + /* 費波那契數列:遞迴 */ + int Fib(int n) { + // 終止條件 f(1) = 0, f(2) = 1 + if (n == 1 || n == 2) + return n - 1; + // 遞迴呼叫 f(n) = f(n-1) + f(n-2) + int res = Fib(n - 1) + Fib(n - 2); + // 返回結果 f(n) + return res; + } + ``` + +=== "Go" + + ```go title="recursion.go" + /* 費波那契數列:遞迴 */ + func fib(n int) int { + // 終止條件 f(1) = 0, f(2) = 1 + if n == 1 || n == 2 { + return n - 1 + } + // 遞迴呼叫 f(n) = f(n-1) + f(n-2) + res := fib(n-1) + fib(n-2) + // 返回結果 f(n) + return res + } + ``` + +=== "Swift" + + ```swift title="recursion.swift" + /* 費波那契數列:遞迴 */ + func fib(n: Int) -> Int { + // 終止條件 f(1) = 0, f(2) = 1 + if n == 1 || n == 2 { + return n - 1 + } + // 遞迴呼叫 f(n) = f(n-1) + f(n-2) + let res = fib(n: n - 1) + fib(n: n - 2) + // 返回結果 f(n) + return res + } + ``` + +=== "JS" + + ```javascript title="recursion.js" + /* 費波那契數列:遞迴 */ + function fib(n) { + // 終止條件 f(1) = 0, f(2) = 1 + if (n === 1 || n === 2) return n - 1; + // 遞迴呼叫 f(n) = f(n-1) + f(n-2) + const res = fib(n - 1) + fib(n - 2); + // 返回結果 f(n) + return res; + } + ``` + +=== "TS" + + ```typescript title="recursion.ts" + /* 費波那契數列:遞迴 */ + function fib(n: number): number { + // 終止條件 f(1) = 0, f(2) = 1 + if (n === 1 || n === 2) return n - 1; + // 遞迴呼叫 f(n) = f(n-1) + f(n-2) + const res = fib(n - 1) + fib(n - 2); + // 返回結果 f(n) + return res; + } + ``` + +=== "Dart" + + ```dart title="recursion.dart" + /* 費波那契數列:遞迴 */ + int fib(int n) { + // 終止條件 f(1) = 0, f(2) = 1 + if (n == 1 || n == 2) return n - 1; + // 遞迴呼叫 f(n) = f(n-1) + f(n-2) + int res = fib(n - 1) + fib(n - 2); + // 返回結果 f(n) + return res; + } + ``` + +=== "Rust" + + ```rust title="recursion.rs" + /* 費波那契數列:遞迴 */ + fn fib(n: i32) -> i32 { + // 終止條件 f(1) = 0, f(2) = 1 + if n == 1 || n == 2 { + return n - 1; + } + // 遞迴呼叫 f(n) = f(n-1) + f(n-2) + let res = fib(n - 1) + fib(n - 2); + // 返回結果 + res + } + ``` + +=== "C" + + ```c title="recursion.c" + /* 費波那契數列:遞迴 */ + int fib(int n) { + // 終止條件 f(1) = 0, f(2) = 1 + if (n == 1 || n == 2) + return n - 1; + // 遞迴呼叫 f(n) = f(n-1) + f(n-2) + int res = fib(n - 1) + fib(n - 2); + // 返回結果 f(n) + return res; + } + ``` + +=== "Kotlin" + + ```kotlin title="recursion.kt" + /* 費波那契數列:遞迴 */ + fun fib(n: Int): Int { + // 終止條件 f(1) = 0, f(2) = 1 + if (n == 1 || n == 2) + return n - 1 + // 遞迴呼叫 f(n) = f(n-1) + f(n-2) + val res = fib(n - 1) + fib(n - 2) + // 返回結果 f(n) + return res + } + ``` + +=== "Ruby" + + ```ruby title="recursion.rb" + ### 費波那契數列:遞迴 ### + def fib(n) + # 終止條件 f(1) = 0, f(2) = 1 + return n - 1 if n == 1 || n == 2 + # 遞迴呼叫 f(n) = f(n-1) + f(n-2) + res = fib(n - 1) + fib(n - 2) + # 返回結果 f(n) + res + end + ``` + +=== "Zig" + + ```zig title="recursion.zig" + // 費波那契數列 + fn fib(n: i32) i32 { + // 終止條件 f(1) = 0, f(2) = 1 + if (n == 1 or n == 2) { + return n - 1; + } + // 遞迴呼叫 f(n) = f(n-1) + f(n-2) + var res: i32 = fib(n - 1) + fib(n - 2); + // 返回結果 f(n) + return res; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +觀察以上程式碼,我們在函式內遞迴呼叫了兩個函式,**這意味著從一個呼叫產生了兩個呼叫分支**。如圖 2-6 所示,這樣不斷遞迴呼叫下去,最終將產生一棵層數為 $n$ 的遞迴樹(recursion tree)。 + +![費波那契數列的遞迴樹](iteration_and_recursion.assets/recursion_tree.png){ class="animation-figure" } + +

圖 2-6   費波那契數列的遞迴樹

+ +從本質上看,遞迴體現了“將問題分解為更小子問題”的思維範式,這種分治策略至關重要。 + +- 從演算法角度看,搜尋、排序、回溯、分治、動態規劃等許多重要演算法策略直接或間接地應用了這種思維方式。 +- 從資料結構角度看,遞迴天然適合處理鏈結串列、樹和圖的相關問題,因為它們非常適合用分治思想進行分析。 + +## 2.2.3   兩者對比 + +總結以上內容,如表 2-1 所示,迭代和遞迴在實現、效能和適用性上有所不同。 + +

表 2-1   迭代與遞迴特點對比

+ +
+ +| | 迭代 | 遞迴 | +| -------- | -------------------------------------- | ------------------------------------------------------------ | +| 實現方式 | 迴圈結構 | 函式呼叫自身 | +| 時間效率 | 效率通常較高,無函式呼叫開銷 | 每次函式呼叫都會產生開銷 | +| 記憶體使用 | 通常使用固定大小的記憶體空間 | 累積函式呼叫可能使用大量的堆疊幀空間 | +| 適用問題 | 適用於簡單迴圈任務,程式碼直觀、可讀性好 | 適用於子問題分解,如樹、圖、分治、回溯等,程式碼結構簡潔、清晰 | + +
+ +!!! tip + + 如果感覺以下內容理解困難,可以在讀完“堆疊”章節後再來複習。 + +那麼,迭代和遞迴具有什麼內在關聯呢?以上述遞迴函式為例,求和操作在遞迴的“迴”階段進行。這意味著最初被呼叫的函式實際上是最後完成其求和操作的,**這種工作機制與堆疊的“先入後出”原則異曲同工**。 + +事實上,“呼叫堆疊”和“堆疊幀空間”這類遞迴術語已經暗示了遞迴與堆疊之間的密切關係。 + +1. **遞**:當函式被呼叫時,系統會在“呼叫堆疊”上為該函式分配新的堆疊幀,用於儲存函式的區域性變數、參數、返回位址等資料。 +2. **迴**:當函式完成執行並返回時,對應的堆疊幀會被從“呼叫堆疊”上移除,恢復之前函式的執行環境。 + +因此,**我們可以使用一個顯式的堆疊來模擬呼叫堆疊的行為**,從而將遞迴轉化為迭代形式: + +=== "Python" + + ```python title="recursion.py" + def for_loop_recur(n: int) -> int: + """使用迭代模擬遞迴""" + # 使用一個顯式的堆疊來模擬系統呼叫堆疊 + stack = [] + res = 0 + # 遞:遞迴呼叫 + for i in range(n, 0, -1): + # 透過“入堆疊操作”模擬“遞” + stack.append(i) + # 迴:返回結果 + while stack: + # 透過“出堆疊操作”模擬“迴” + res += stack.pop() + # res = 1+2+3+...+n + return res + ``` + +=== "C++" + + ```cpp title="recursion.cpp" + /* 使用迭代模擬遞迴 */ + int forLoopRecur(int n) { + // 使用一個顯式的堆疊來模擬系統呼叫堆疊 + stack stack; + int res = 0; + // 遞:遞迴呼叫 + for (int i = n; i > 0; i--) { + // 透過“入堆疊操作”模擬“遞” + stack.push(i); + } + // 迴:返回結果 + while (!stack.empty()) { + // 透過“出堆疊操作”模擬“迴” + res += stack.top(); + stack.pop(); + } + // res = 1+2+3+...+n + return res; + } + ``` + +=== "Java" + + ```java title="recursion.java" + /* 使用迭代模擬遞迴 */ + int forLoopRecur(int n) { + // 使用一個顯式的堆疊來模擬系統呼叫堆疊 + Stack stack = new Stack<>(); + int res = 0; + // 遞:遞迴呼叫 + for (int i = n; i > 0; i--) { + // 透過“入堆疊操作”模擬“遞” + stack.push(i); + } + // 迴:返回結果 + while (!stack.isEmpty()) { + // 透過“出堆疊操作”模擬“迴” + res += stack.pop(); + } + // res = 1+2+3+...+n + return res; + } + ``` + +=== "C#" + + ```csharp title="recursion.cs" + /* 使用迭代模擬遞迴 */ + int ForLoopRecur(int n) { + // 使用一個顯式的堆疊來模擬系統呼叫堆疊 + Stack stack = new(); + int res = 0; + // 遞:遞迴呼叫 + for (int i = n; i > 0; i--) { + // 透過“入堆疊操作”模擬“遞” + stack.Push(i); + } + // 迴:返回結果 + while (stack.Count > 0) { + // 透過“出堆疊操作”模擬“迴” + res += stack.Pop(); + } + // res = 1+2+3+...+n + return res; + } + ``` + +=== "Go" + + ```go title="recursion.go" + /* 使用迭代模擬遞迴 */ + func forLoopRecur(n int) int { + // 使用一個顯式的堆疊來模擬系統呼叫堆疊 + stack := list.New() + res := 0 + // 遞:遞迴呼叫 + for i := n; i > 0; i-- { + // 透過“入堆疊操作”模擬“遞” + stack.PushBack(i) + } + // 迴:返回結果 + for stack.Len() != 0 { + // 透過“出堆疊操作”模擬“迴” + res += stack.Back().Value.(int) + stack.Remove(stack.Back()) + } + // res = 1+2+3+...+n + return res + } + ``` + +=== "Swift" + + ```swift title="recursion.swift" + /* 使用迭代模擬遞迴 */ + func forLoopRecur(n: Int) -> Int { + // 使用一個顯式的堆疊來模擬系統呼叫堆疊 + var stack: [Int] = [] + var res = 0 + // 遞:遞迴呼叫 + for i in (1 ... n).reversed() { + // 透過“入堆疊操作”模擬“遞” + stack.append(i) + } + // 迴:返回結果 + while !stack.isEmpty { + // 透過“出堆疊操作”模擬“迴” + res += stack.removeLast() + } + // res = 1+2+3+...+n + return res + } + ``` + +=== "JS" + + ```javascript title="recursion.js" + /* 使用迭代模擬遞迴 */ + function forLoopRecur(n) { + // 使用一個顯式的堆疊來模擬系統呼叫堆疊 + const stack = []; + let res = 0; + // 遞:遞迴呼叫 + for (let i = n; i > 0; i--) { + // 透過“入堆疊操作”模擬“遞” + stack.push(i); + } + // 迴:返回結果 + while (stack.length) { + // 透過“出堆疊操作”模擬“迴” + res += stack.pop(); + } + // res = 1+2+3+...+n + return res; + } + ``` + +=== "TS" + + ```typescript title="recursion.ts" + /* 使用迭代模擬遞迴 */ + function forLoopRecur(n: number): number { + // 使用一個顯式的堆疊來模擬系統呼叫堆疊 + const stack: number[] = []; + let res: number = 0; + // 遞:遞迴呼叫 + for (let i = n; i > 0; i--) { + // 透過“入堆疊操作”模擬“遞” + stack.push(i); + } + // 迴:返回結果 + while (stack.length) { + // 透過“出堆疊操作”模擬“迴” + res += stack.pop(); + } + // res = 1+2+3+...+n + return res; + } + ``` + +=== "Dart" + + ```dart title="recursion.dart" + /* 使用迭代模擬遞迴 */ + int forLoopRecur(int n) { + // 使用一個顯式的堆疊來模擬系統呼叫堆疊 + List stack = []; + int res = 0; + // 遞:遞迴呼叫 + for (int i = n; i > 0; i--) { + // 透過“入堆疊操作”模擬“遞” + stack.add(i); + } + // 迴:返回結果 + while (!stack.isEmpty) { + // 透過“出堆疊操作”模擬“迴” + res += stack.removeLast(); + } + // res = 1+2+3+...+n + return res; + } + ``` + +=== "Rust" + + ```rust title="recursion.rs" + /* 使用迭代模擬遞迴 */ + fn for_loop_recur(n: i32) -> i32 { + // 使用一個顯式的堆疊來模擬系統呼叫堆疊 + let mut stack = Vec::new(); + let mut res = 0; + // 遞:遞迴呼叫 + for i in (1..=n).rev() { + // 透過“入堆疊操作”模擬“遞” + stack.push(i); + } + // 迴:返回結果 + while !stack.is_empty() { + // 透過“出堆疊操作”模擬“迴” + res += stack.pop().unwrap(); + } + // res = 1+2+3+...+n + res + } + ``` + +=== "C" + + ```c title="recursion.c" + /* 使用迭代模擬遞迴 */ + int forLoopRecur(int n) { + int stack[1000]; // 藉助一個大陣列來模擬堆疊 + int top = -1; // 堆疊頂索引 + int res = 0; + // 遞:遞迴呼叫 + for (int i = n; i > 0; i--) { + // 透過“入堆疊操作”模擬“遞” + stack[1 + top++] = i; + } + // 迴:返回結果 + while (top >= 0) { + // 透過“出堆疊操作”模擬“迴” + res += stack[top--]; + } + // res = 1+2+3+...+n + return res; + } + ``` + +=== "Kotlin" + + ```kotlin title="recursion.kt" + /* 使用迭代模擬遞迴 */ + fun forLoopRecur(n: Int): Int { + // 使用一個顯式的堆疊來模擬系統呼叫堆疊 + val stack = Stack() + var res = 0 + // 遞: 遞迴呼叫 + for (i in n downTo 0) { + stack.push(i) + } + // 迴: 返回結果 + while (stack.isNotEmpty()) { + // 透過“出堆疊操作”模擬“迴” + res += stack.pop() + } + // res = 1+2+3+...+n + return res + } + ``` + +=== "Ruby" + + ```ruby title="recursion.rb" + ### 使用迭代模擬遞迴 ### + def for_loop_recur(n) + # 使用一個顯式的堆疊來模擬系統呼叫堆疊 + stack = [] + res = 0 + + # 遞:遞迴呼叫 + for i in n.downto(0) + # 透過“入堆疊操作”模擬“遞” + stack << i + end + # 迴:返回結果 + while !stack.empty? + res += stack.pop + end + + # res = 1+2+3+...+n + res + end + ``` + +=== "Zig" + + ```zig title="recursion.zig" + // 使用迭代模擬遞迴 + fn forLoopRecur(comptime n: i32) i32 { + // 使用一個顯式的堆疊來模擬系統呼叫堆疊 + var stack: [n]i32 = undefined; + var res: i32 = 0; + // 遞:遞迴呼叫 + var i: usize = n; + while (i > 0) { + stack[i - 1] = @intCast(i); + i -= 1; + } + // 迴:返回結果 + var index: usize = n; + while (index > 0) { + index -= 1; + res += stack[index]; + } + // res = 1+2+3+...+n + return res; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +觀察以上程式碼,當遞迴轉化為迭代後,程式碼變得更加複雜了。儘管迭代和遞迴在很多情況下可以互相轉化,但不一定值得這樣做,有以下兩點原因。 + +- 轉化後的程式碼可能更加難以理解,可讀性更差。 +- 對於某些複雜問題,模擬系統呼叫堆疊的行為可能非常困難。 + +總之,**選擇迭代還是遞迴取決於特定問題的性質**。在程式設計實踐中,權衡兩者的優劣並根據情境選擇合適的方法至關重要。 diff --git a/zh-Hant/docs/chapter_computational_complexity/performance_evaluation.md b/zh-Hant/docs/chapter_computational_complexity/performance_evaluation.md new file mode 100644 index 000000000..d453ffa5c --- /dev/null +++ b/zh-Hant/docs/chapter_computational_complexity/performance_evaluation.md @@ -0,0 +1,52 @@ +--- +comments: true +--- + +# 2.1   演算法效率評估 + +在演算法設計中,我們先後追求以下兩個層面的目標。 + +1. **找到問題解法**:演算法需要在規定的輸入範圍內可靠地求得問題的正確解。 +2. **尋求最優解法**:同一個問題可能存在多種解法,我們希望找到儘可能高效的演算法。 + +也就是說,在能夠解決問題的前提下,演算法效率已成為衡量演算法優劣的主要評價指標,它包括以下兩個維度。 + +- **時間效率**:演算法執行速度的快慢。 +- **空間效率**:演算法佔用記憶體空間的大小。 + +簡而言之,**我們的目標是設計“既快又省”的資料結構與演算法**。而有效地評估演算法效率至關重要,因為只有這樣,我們才能將各種演算法進行對比,進而指導演算法設計與最佳化過程。 + +效率評估方法主要分為兩種:實際測試、理論估算。 + +## 2.1.1   實際測試 + +假設我們現在有演算法 `A` 和演算法 `B` ,它們都能解決同一問題,現在需要對比這兩個演算法的效率。最直接的方法是找一臺計算機,執行這兩個演算法,並監控記錄它們的執行時間和記憶體佔用情況。這種評估方式能夠反映真實情況,但也存在較大的侷限性。 + +一方面,**難以排除測試環境的干擾因素**。硬體配置會影響演算法的效能。比如在某臺計算機中,演算法 `A` 的執行時間比演算法 `B` 短;但在另一臺配置不同的計算機中,可能得到相反的測試結果。這意味著我們需要在各種機器上進行測試,統計平均效率,而這是不現實的。 + +另一方面,**展開完整測試非常耗費資源**。隨著輸入資料量的變化,演算法會表現出不同的效率。例如,在輸入資料量較小時,演算法 `A` 的執行時間比演算法 `B` 短;而在輸入資料量較大時,測試結果可能恰恰相反。因此,為了得到有說服力的結論,我們需要測試各種規模的輸入資料,而這需要耗費大量的計算資源。 + +## 2.1.2   理論估算 + +由於實際測試具有較大的侷限性,因此我們可以考慮僅透過一些計算來評估演算法的效率。這種估算方法被稱為漸近複雜度分析(asymptotic complexity analysis),簡稱複雜度分析。 + +複雜度分析能夠體現演算法執行所需的時間和空間資源與輸入資料大小之間的關係。**它描述了隨著輸入資料大小的增加,演算法執行所需時間和空間的增長趨勢**。這個定義有些拗口,我們可以將其分為三個重點來理解。 + +- “時間和空間資源”分別對應時間複雜度(time complexity)空間複雜度(space complexity)。 +- “隨著輸入資料大小的增加”意味著複雜度反映了演算法執行效率與輸入資料體量之間的關係。 +- “時間和空間的增長趨勢”表示複雜度分析關注的不是執行時間或佔用空間的具體值,而是時間或空間增長的“快慢”。 + +**複雜度分析克服了實際測試方法的弊端**,體現在以下兩個方面。 + +- 它獨立於測試環境,分析結果適用於所有執行平臺。 +- 它可以體現不同資料量下的演算法效率,尤其是在大資料量下的演算法效能。 + +!!! tip + + 如果你仍對複雜度的概念感到困惑,無須擔心,我們會在後續章節中詳細介紹。 + +複雜度分析為我們提供了一把評估演算法效率的“標尺”,使我們可以衡量執行某個演算法所需的時間和空間資源,對比不同演算法之間的效率。 + +複雜度是個數學概念,對於初學者可能比較抽象,學習難度相對較高。從這個角度看,複雜度分析可能不太適合作為最先介紹的內容。然而,當我們討論某個資料結構或演算法的特點時,難以避免要分析其執行速度和空間使用情況。 + +綜上所述,建議你在深入學習資料結構與演算法之前,**先對複雜度分析建立初步的瞭解,以便能夠完成簡單演算法的複雜度分析**。 diff --git a/zh-Hant/docs/chapter_computational_complexity/space_complexity.md b/zh-Hant/docs/chapter_computational_complexity/space_complexity.md new file mode 100755 index 000000000..e8782be80 --- /dev/null +++ b/zh-Hant/docs/chapter_computational_complexity/space_complexity.md @@ -0,0 +1,2384 @@ +--- +comments: true +--- + +# 2.4   空間複雜度 + +空間複雜度(space complexity)用於衡量演算法佔用記憶體空間隨著資料量變大時的增長趨勢。這個概念與時間複雜度非常類似,只需將“執行時間”替換為“佔用記憶體空間”。 + +## 2.4.1   演算法相關空間 + +演算法在執行過程中使用的記憶體空間主要包括以下幾種。 + +- **輸入空間**:用於儲存演算法的輸入資料。 +- **暫存空間**:用於儲存演算法在執行過程中的變數、物件、函式上下文等資料。 +- **輸出空間**:用於儲存演算法的輸出資料。 + +一般情況下,空間複雜度的統計範圍是“暫存空間”加上“輸出空間”。 + +暫存空間可以進一步劃分為三個部分。 + +- **暫存資料**:用於儲存演算法執行過程中的各種常數、變數、物件等。 +- **堆疊幀空間**:用於儲存呼叫函式的上下文資料。系統在每次呼叫函式時都會在堆疊頂部建立一個堆疊幀,函式返回後,堆疊幀空間會被釋放。 +- **指令空間**:用於儲存編譯後的程式指令,在實際統計中通常忽略不計。 + +在分析一段程式的空間複雜度時,**我們通常統計暫存資料、堆疊幀空間和輸出資料三部分**,如圖 2-15 所示。 + +![演算法使用的相關空間](space_complexity.assets/space_types.png){ class="animation-figure" } + +

圖 2-15   演算法使用的相關空間

+ +相關程式碼如下: + +=== "Python" + + ```python title="" + class Node: + """類別""" + def __init__(self, x: int): + self.val: int = x # 節點值 + self.next: Node | None = None # 指向下一節點的引用 + + def function() -> int: + """函式""" + # 執行某些操作... + return 0 + + def algorithm(n) -> int: # 輸入資料 + A = 0 # 暫存資料(常數,一般用大寫字母表示) + b = 0 # 暫存資料(變數) + node = Node(0) # 暫存資料(物件) + c = function() # 堆疊幀空間(呼叫函式) + return A + b + c # 輸出資料 + ``` + +=== "C++" + + ```cpp title="" + /* 結構體 */ + struct Node { + int val; + Node *next; + Node(int x) : val(x), next(nullptr) {} + }; + + /* 函式 */ + int func() { + // 執行某些操作... + return 0; + } + + int algorithm(int n) { // 輸入資料 + const int a = 0; // 暫存資料(常數) + int b = 0; // 暫存資料(變數) + Node* node = new Node(0); // 暫存資料(物件) + int c = func(); // 堆疊幀空間(呼叫函式) + return a + b + c; // 輸出資料 + } + ``` + +=== "Java" + + ```java title="" + /* 類別 */ + class Node { + int val; + Node next; + Node(int x) { val = x; } + } + + /* 函式 */ + int function() { + // 執行某些操作... + return 0; + } + + int algorithm(int n) { // 輸入資料 + final int a = 0; // 暫存資料(常數) + int b = 0; // 暫存資料(變數) + Node node = new Node(0); // 暫存資料(物件) + int c = function(); // 堆疊幀空間(呼叫函式) + return a + b + c; // 輸出資料 + } + ``` + +=== "C#" + + ```csharp title="" + /* 類別 */ + class Node(int x) { + int val = x; + Node next; + } + + /* 函式 */ + int Function() { + // 執行某些操作... + return 0; + } + + int Algorithm(int n) { // 輸入資料 + const int a = 0; // 暫存資料(常數) + int b = 0; // 暫存資料(變數) + Node node = new(0); // 暫存資料(物件) + int c = Function(); // 堆疊幀空間(呼叫函式) + return a + b + c; // 輸出資料 + } + ``` + +=== "Go" + + ```go title="" + /* 結構體 */ + type node struct { + val int + next *node + } + + /* 建立 node 結構體 */ + func newNode(val int) *node { + return &node{val: val} + } + + /* 函式 */ + func function() int { + // 執行某些操作... + return 0 + } + + func algorithm(n int) int { // 輸入資料 + const a = 0 // 暫存資料(常數) + b := 0 // 暫存資料(變數) + newNode(0) // 暫存資料(物件) + c := function() // 堆疊幀空間(呼叫函式) + return a + b + c // 輸出資料 + } + ``` + +=== "Swift" + + ```swift title="" + /* 類別 */ + class Node { + var val: Int + var next: Node? + + init(x: Int) { + val = x + } + } + + /* 函式 */ + func function() -> Int { + // 執行某些操作... + return 0 + } + + func algorithm(n: Int) -> Int { // 輸入資料 + let a = 0 // 暫存資料(常數) + var b = 0 // 暫存資料(變數) + let node = Node(x: 0) // 暫存資料(物件) + let c = function() // 堆疊幀空間(呼叫函式) + return a + b + c // 輸出資料 + } + ``` + +=== "JS" + + ```javascript title="" + /* 類別 */ + class Node { + val; + next; + constructor(val) { + this.val = val === undefined ? 0 : val; // 節點值 + this.next = null; // 指向下一節點的引用 + } + } + + /* 函式 */ + function constFunc() { + // 執行某些操作 + return 0; + } + + function algorithm(n) { // 輸入資料 + const a = 0; // 暫存資料(常數) + let b = 0; // 暫存資料(變數) + const node = new Node(0); // 暫存資料(物件) + const c = constFunc(); // 堆疊幀空間(呼叫函式) + return a + b + c; // 輸出資料 + } + ``` + +=== "TS" + + ```typescript title="" + /* 類別 */ + class Node { + val: number; + next: Node | null; + constructor(val?: number) { + this.val = val === undefined ? 0 : val; // 節點值 + this.next = null; // 指向下一節點的引用 + } + } + + /* 函式 */ + function constFunc(): number { + // 執行某些操作 + return 0; + } + + function algorithm(n: number): number { // 輸入資料 + const a = 0; // 暫存資料(常數) + let b = 0; // 暫存資料(變數) + const node = new Node(0); // 暫存資料(物件) + const c = constFunc(); // 堆疊幀空間(呼叫函式) + return a + b + c; // 輸出資料 + } + ``` + +=== "Dart" + + ```dart title="" + /* 類別 */ + class Node { + int val; + Node next; + Node(this.val, [this.next]); + } + + /* 函式 */ + int function() { + // 執行某些操作... + return 0; + } + + int algorithm(int n) { // 輸入資料 + const int a = 0; // 暫存資料(常數) + int b = 0; // 暫存資料(變數) + Node node = Node(0); // 暫存資料(物件) + int c = function(); // 堆疊幀空間(呼叫函式) + return a + b + c; // 輸出資料 + } + ``` + +=== "Rust" + + ```rust title="" + use std::rc::Rc; + use std::cell::RefCell; + + /* 結構體 */ + struct Node { + val: i32, + next: Option>>, + } + + /* 建立 Node 結構體 */ + impl Node { + fn new(val: i32) -> Self { + Self { val: val, next: None } + } + } + + /* 函式 */ + fn function() -> i32 { + // 執行某些操作... + return 0; + } + + fn algorithm(n: i32) -> i32 { // 輸入資料 + const a: i32 = 0; // 暫存資料(常數) + let mut b = 0; // 暫存資料(變數) + let node = Node::new(0); // 暫存資料(物件) + let c = function(); // 堆疊幀空間(呼叫函式) + return a + b + c; // 輸出資料 + } + ``` + +=== "C" + + ```c title="" + /* 函式 */ + int func() { + // 執行某些操作... + return 0; + } + + int algorithm(int n) { // 輸入資料 + const int a = 0; // 暫存資料(常數) + int b = 0; // 暫存資料(變數) + int c = func(); // 堆疊幀空間(呼叫函式) + return a + b + c; // 輸出資料 + } + ``` + +=== "Kotlin" + + ```kotlin title="" + /* 類別 */ + class Node(var _val: Int) { + var next: Node? = null + } + + /* 函式 */ + fun function(): Int { + // 執行某些操作... + return 0 + } + + fun algorithm(n: Int): Int { // 輸入資料 + val a = 0 // 暫存資料(常數) + var b = 0 // 暫存資料(變數) + val node = Node(0) // 暫存資料(物件) + val c = function() // 堆疊幀空間(呼叫函式) + return a + b + c // 輸出資料 + } + ``` + +=== "Ruby" + + ```ruby title="" + ### 類別 ### + class Node + attr_accessor :val # 節點值 + attr_accessor :next # 指向下一節點的引用 + + def initialize(x) + @val = x + end + end + + ### 函式 ### + def function + # 執行某些操作... + 0 + end + + ### 演算法 ### + def algorithm(n) # 輸入資料 + a = 0 # 暫存資料(常數) + b = 0 # 暫存資料(變數) + node = Node.new(0) # 暫存資料(物件) + c = function # 堆疊幀空間(呼叫函式) + a + b + c # 輸出資料 + end + ``` + +=== "Zig" + + ```zig title="" + + ``` + +## 2.4.2   推算方法 + +空間複雜度的推算方法與時間複雜度大致相同,只需將統計物件從“操作數量”轉為“使用空間大小”。 + +而與時間複雜度不同的是,**我們通常只關注最差空間複雜度**。這是因為記憶體空間是一項硬性要求,我們必須確保在所有輸入資料下都有足夠的記憶體空間預留。 + +觀察以下程式碼,最差空間複雜度中的“最差”有兩層含義。 + +1. **以最差輸入資料為準**:當 $n < 10$ 時,空間複雜度為 $O(1)$ ;但當 $n > 10$ 時,初始化的陣列 `nums` 佔用 $O(n)$ 空間,因此最差空間複雜度為 $O(n)$ 。 +2. **以演算法執行中的峰值記憶體為準**:例如,程式在執行最後一行之前,佔用 $O(1)$ 空間;當初始化陣列 `nums` 時,程式佔用 $O(n)$ 空間,因此最差空間複雜度為 $O(n)$ 。 + +=== "Python" + + ```python title="" + def algorithm(n: int): + a = 0 # O(1) + b = [0] * 10000 # O(1) + if n > 10: + nums = [0] * n # O(n) + ``` + +=== "C++" + + ```cpp title="" + void algorithm(int n) { + int a = 0; // O(1) + vector b(10000); // O(1) + if (n > 10) + vector nums(n); // O(n) + } + ``` + +=== "Java" + + ```java title="" + void algorithm(int n) { + int a = 0; // O(1) + int[] b = new int[10000]; // O(1) + if (n > 10) + int[] nums = new int[n]; // O(n) + } + ``` + +=== "C#" + + ```csharp title="" + void Algorithm(int n) { + int a = 0; // O(1) + int[] b = new int[10000]; // O(1) + if (n > 10) { + int[] nums = new int[n]; // O(n) + } + } + ``` + +=== "Go" + + ```go title="" + func algorithm(n int) { + a := 0 // O(1) + b := make([]int, 10000) // O(1) + var nums []int + if n > 10 { + nums := make([]int, n) // O(n) + } + fmt.Println(a, b, nums) + } + ``` + +=== "Swift" + + ```swift title="" + func algorithm(n: Int) { + let a = 0 // O(1) + let b = Array(repeating: 0, count: 10000) // O(1) + if n > 10 { + let nums = Array(repeating: 0, count: n) // O(n) + } + } + ``` + +=== "JS" + + ```javascript title="" + function algorithm(n) { + const a = 0; // O(1) + const b = new Array(10000); // O(1) + if (n > 10) { + const nums = new Array(n); // O(n) + } + } + ``` + +=== "TS" + + ```typescript title="" + function algorithm(n: number): void { + const a = 0; // O(1) + const b = new Array(10000); // O(1) + if (n > 10) { + const nums = new Array(n); // O(n) + } + } + ``` + +=== "Dart" + + ```dart title="" + void algorithm(int n) { + int a = 0; // O(1) + List b = List.filled(10000, 0); // O(1) + if (n > 10) { + List nums = List.filled(n, 0); // O(n) + } + } + ``` + +=== "Rust" + + ```rust title="" + fn algorithm(n: i32) { + let a = 0; // O(1) + let b = [0; 10000]; // O(1) + if n > 10 { + let nums = vec![0; n as usize]; // O(n) + } + } + ``` + +=== "C" + + ```c title="" + void algorithm(int n) { + int a = 0; // O(1) + int b[10000]; // O(1) + if (n > 10) + int nums[n] = {0}; // O(n) + } + ``` + +=== "Kotlin" + + ```kotlin title="" + fun algorithm(n: Int) { + val a = 0 // O(1) + val b = IntArray(10000) // O(1) + if (n > 10) { + val nums = IntArray(n) // O(n) + } + } + ``` + +=== "Ruby" + + ```ruby title="" + def algorithm(n) + a = 0 # O(1) + b = Array.new(10000) # O(1) + nums = Array.new(n) if n > 10 # O(n) + end + ``` + +=== "Zig" + + ```zig title="" + + ``` + +**在遞迴函式中,需要注意統計堆疊幀空間**。觀察以下程式碼: + +=== "Python" + + ```python title="" + def function() -> int: + # 執行某些操作 + return 0 + + def loop(n: int): + """迴圈的空間複雜度為 O(1)""" + for _ in range(n): + function() + + def recur(n: int): + """遞迴的空間複雜度為 O(n)""" + if n == 1: + return + return recur(n - 1) + ``` + +=== "C++" + + ```cpp title="" + int func() { + // 執行某些操作 + return 0; + } + /* 迴圈的空間複雜度為 O(1) */ + void loop(int n) { + for (int i = 0; i < n; i++) { + func(); + } + } + /* 遞迴的空間複雜度為 O(n) */ + void recur(int n) { + if (n == 1) return; + return recur(n - 1); + } + ``` + +=== "Java" + + ```java title="" + int function() { + // 執行某些操作 + return 0; + } + /* 迴圈的空間複雜度為 O(1) */ + void loop(int n) { + for (int i = 0; i < n; i++) { + function(); + } + } + /* 遞迴的空間複雜度為 O(n) */ + void recur(int n) { + if (n == 1) return; + return recur(n - 1); + } + ``` + +=== "C#" + + ```csharp title="" + int Function() { + // 執行某些操作 + return 0; + } + /* 迴圈的空間複雜度為 O(1) */ + void Loop(int n) { + for (int i = 0; i < n; i++) { + Function(); + } + } + /* 遞迴的空間複雜度為 O(n) */ + int Recur(int n) { + if (n == 1) return 1; + return Recur(n - 1); + } + ``` + +=== "Go" + + ```go title="" + func function() int { + // 執行某些操作 + return 0 + } + + /* 迴圈的空間複雜度為 O(1) */ + func loop(n int) { + for i := 0; i < n; i++ { + function() + } + } + + /* 遞迴的空間複雜度為 O(n) */ + func recur(n int) { + if n == 1 { + return + } + recur(n - 1) + } + ``` + +=== "Swift" + + ```swift title="" + @discardableResult + func function() -> Int { + // 執行某些操作 + return 0 + } + + /* 迴圈的空間複雜度為 O(1) */ + func loop(n: Int) { + for _ in 0 ..< n { + function() + } + } + + /* 遞迴的空間複雜度為 O(n) */ + func recur(n: Int) { + if n == 1 { + return + } + recur(n: n - 1) + } + ``` + +=== "JS" + + ```javascript title="" + function constFunc() { + // 執行某些操作 + return 0; + } + /* 迴圈的空間複雜度為 O(1) */ + function loop(n) { + for (let i = 0; i < n; i++) { + constFunc(); + } + } + /* 遞迴的空間複雜度為 O(n) */ + function recur(n) { + if (n === 1) return; + return recur(n - 1); + } + ``` + +=== "TS" + + ```typescript title="" + function constFunc(): number { + // 執行某些操作 + return 0; + } + /* 迴圈的空間複雜度為 O(1) */ + function loop(n: number): void { + for (let i = 0; i < n; i++) { + constFunc(); + } + } + /* 遞迴的空間複雜度為 O(n) */ + function recur(n: number): void { + if (n === 1) return; + return recur(n - 1); + } + ``` + +=== "Dart" + + ```dart title="" + int function() { + // 執行某些操作 + return 0; + } + /* 迴圈的空間複雜度為 O(1) */ + void loop(int n) { + for (int i = 0; i < n; i++) { + function(); + } + } + /* 遞迴的空間複雜度為 O(n) */ + void recur(int n) { + if (n == 1) return; + return recur(n - 1); + } + ``` + +=== "Rust" + + ```rust title="" + fn function() -> i32 { + // 執行某些操作 + return 0; + } + /* 迴圈的空間複雜度為 O(1) */ + fn loop(n: i32) { + for i in 0..n { + function(); + } + } + /* 遞迴的空間複雜度為 O(n) */ + fn recur(n: i32) { + if n == 1 { + return; + } + recur(n - 1); + } + ``` + +=== "C" + + ```c title="" + int func() { + // 執行某些操作 + return 0; + } + /* 迴圈的空間複雜度為 O(1) */ + void loop(int n) { + for (int i = 0; i < n; i++) { + func(); + } + } + /* 遞迴的空間複雜度為 O(n) */ + void recur(int n) { + if (n == 1) return; + return recur(n - 1); + } + ``` + +=== "Kotlin" + + ```kotlin title="" + fun function(): Int { + // 執行某些操作 + return 0 + } + /* 迴圈的空間複雜度為 O(1) */ + fun loop(n: Int) { + for (i in 0.. 圖 2-16   常見的空間複雜度型別

+ +### 1.   常數階 $O(1)$ {data-toc-label="1.   常數階"} + +常數階常見於數量與輸入資料大小 $n$ 無關的常數、變數、物件。 + +需要注意的是,在迴圈中初始化變數或呼叫函式而佔用的記憶體,在進入下一迴圈後就會被釋放,因此不會累積佔用空間,空間複雜度仍為 $O(1)$ : + +=== "Python" + + ```python title="space_complexity.py" + def function() -> int: + """函式""" + # 執行某些操作 + return 0 + + def constant(n: int): + """常數階""" + # 常數、變數、物件佔用 O(1) 空間 + a = 0 + nums = [0] * 10000 + node = ListNode(0) + # 迴圈中的變數佔用 O(1) 空間 + for _ in range(n): + c = 0 + # 迴圈中的函式佔用 O(1) 空間 + for _ in range(n): + function() + ``` + +=== "C++" + + ```cpp title="space_complexity.cpp" + /* 函式 */ + int func() { + // 執行某些操作 + return 0; + } + + /* 常數階 */ + void constant(int n) { + // 常數、變數、物件佔用 O(1) 空間 + const int a = 0; + int b = 0; + vector nums(10000); + ListNode node(0); + // 迴圈中的變數佔用 O(1) 空間 + for (int i = 0; i < n; i++) { + int c = 0; + } + // 迴圈中的函式佔用 O(1) 空間 + for (int i = 0; i < n; i++) { + func(); + } + } + ``` + +=== "Java" + + ```java title="space_complexity.java" + /* 函式 */ + int function() { + // 執行某些操作 + return 0; + } + + /* 常數階 */ + void constant(int n) { + // 常數、變數、物件佔用 O(1) 空間 + final int a = 0; + int b = 0; + int[] nums = new int[10000]; + ListNode node = new ListNode(0); + // 迴圈中的變數佔用 O(1) 空間 + for (int i = 0; i < n; i++) { + int c = 0; + } + // 迴圈中的函式佔用 O(1) 空間 + for (int i = 0; i < n; i++) { + function(); + } + } + ``` + +=== "C#" + + ```csharp title="space_complexity.cs" + /* 函式 */ + int Function() { + // 執行某些操作 + return 0; + } + + /* 常數階 */ + void Constant(int n) { + // 常數、變數、物件佔用 O(1) 空間 + int a = 0; + int b = 0; + int[] nums = new int[10000]; + ListNode node = new(0); + // 迴圈中的變數佔用 O(1) 空間 + for (int i = 0; i < n; i++) { + int c = 0; + } + // 迴圈中的函式佔用 O(1) 空間 + for (int i = 0; i < n; i++) { + Function(); + } + } + ``` + +=== "Go" + + ```go title="space_complexity.go" + /* 函式 */ + func function() int { + // 執行某些操作... + return 0 + } + + /* 常數階 */ + func spaceConstant(n int) { + // 常數、變數、物件佔用 O(1) 空間 + const a = 0 + b := 0 + nums := make([]int, 10000) + node := newNode(0) + // 迴圈中的變數佔用 O(1) 空間 + var c int + for i := 0; i < n; i++ { + c = 0 + } + // 迴圈中的函式佔用 O(1) 空間 + for i := 0; i < n; i++ { + function() + } + b += 0 + c += 0 + nums[0] = 0 + node.val = 0 + } + ``` + +=== "Swift" + + ```swift title="space_complexity.swift" + /* 函式 */ + @discardableResult + func function() -> Int { + // 執行某些操作 + return 0 + } + + /* 常數階 */ + func constant(n: Int) { + // 常數、變數、物件佔用 O(1) 空間 + let a = 0 + var b = 0 + let nums = Array(repeating: 0, count: 10000) + let node = ListNode(x: 0) + // 迴圈中的變數佔用 O(1) 空間 + for _ in 0 ..< n { + let c = 0 + } + // 迴圈中的函式佔用 O(1) 空間 + for _ in 0 ..< n { + function() + } + } + ``` + +=== "JS" + + ```javascript title="space_complexity.js" + /* 函式 */ + function constFunc() { + // 執行某些操作 + return 0; + } + + /* 常數階 */ + function constant(n) { + // 常數、變數、物件佔用 O(1) 空間 + const a = 0; + const b = 0; + const nums = new Array(10000); + const node = new ListNode(0); + // 迴圈中的變數佔用 O(1) 空間 + for (let i = 0; i < n; i++) { + const c = 0; + } + // 迴圈中的函式佔用 O(1) 空間 + for (let i = 0; i < n; i++) { + constFunc(); + } + } + ``` + +=== "TS" + + ```typescript title="space_complexity.ts" + /* 函式 */ + function constFunc(): number { + // 執行某些操作 + return 0; + } + + /* 常數階 */ + function constant(n: number): void { + // 常數、變數、物件佔用 O(1) 空間 + const a = 0; + const b = 0; + const nums = new Array(10000); + const node = new ListNode(0); + // 迴圈中的變數佔用 O(1) 空間 + for (let i = 0; i < n; i++) { + const c = 0; + } + // 迴圈中的函式佔用 O(1) 空間 + for (let i = 0; i < n; i++) { + constFunc(); + } + } + ``` + +=== "Dart" + + ```dart title="space_complexity.dart" + /* 函式 */ + int function() { + // 執行某些操作 + return 0; + } + + /* 常數階 */ + void constant(int n) { + // 常數、變數、物件佔用 O(1) 空間 + final int a = 0; + int b = 0; + List nums = List.filled(10000, 0); + ListNode node = ListNode(0); + // 迴圈中的變數佔用 O(1) 空間 + for (var i = 0; i < n; i++) { + int c = 0; + } + // 迴圈中的函式佔用 O(1) 空間 + for (var i = 0; i < n; i++) { + function(); + } + } + ``` + +=== "Rust" + + ```rust title="space_complexity.rs" + /* 函式 */ + fn function() -> i32 { + // 執行某些操作 + return 0; + } + + /* 常數階 */ + #[allow(unused)] + fn constant(n: i32) { + // 常數、變數、物件佔用 O(1) 空間 + const A: i32 = 0; + let b = 0; + let nums = vec![0; 10000]; + let node = ListNode::new(0); + // 迴圈中的變數佔用 O(1) 空間 + for i in 0..n { + let c = 0; + } + // 迴圈中的函式佔用 O(1) 空間 + for i in 0..n { + function(); + } + } + ``` + +=== "C" + + ```c title="space_complexity.c" + /* 函式 */ + int func() { + // 執行某些操作 + return 0; + } + + /* 常數階 */ + void constant(int n) { + // 常數、變數、物件佔用 O(1) 空間 + const int a = 0; + int b = 0; + int nums[1000]; + ListNode *node = newListNode(0); + free(node); + // 迴圈中的變數佔用 O(1) 空間 + for (int i = 0; i < n; i++) { + int c = 0; + } + // 迴圈中的函式佔用 O(1) 空間 + for (int i = 0; i < n; i++) { + func(); + } + } + ``` + +=== "Kotlin" + + ```kotlin title="space_complexity.kt" + /* 函式 */ + fun function(): Int { + // 執行某些操作 + return 0 + } + + /* 常數階 */ + fun constant(n: Int) { + // 常數、變數、物件佔用 O(1) 空間 + val a = 0 + var b = 0 + val nums = Array(10000) { 0 } + val node = ListNode(0) + // 迴圈中的變數佔用 O(1) 空間 + for (i in 0..
+ + +### 2.   線性階 $O(n)$ {data-toc-label="2.   線性階"} + +線性階常見於元素數量與 $n$ 成正比的陣列、鏈結串列、堆疊、佇列等: + +=== "Python" + + ```python title="space_complexity.py" + def linear(n: int): + """線性階""" + # 長度為 n 的串列佔用 O(n) 空間 + nums = [0] * n + # 長度為 n 的雜湊表佔用 O(n) 空間 + hmap = dict[int, str]() + for i in range(n): + hmap[i] = str(i) + ``` + +=== "C++" + + ```cpp title="space_complexity.cpp" + /* 線性階 */ + void linear(int n) { + // 長度為 n 的陣列佔用 O(n) 空間 + vector nums(n); + // 長度為 n 的串列佔用 O(n) 空間 + vector nodes; + for (int i = 0; i < n; i++) { + nodes.push_back(ListNode(i)); + } + // 長度為 n 的雜湊表佔用 O(n) 空間 + unordered_map map; + for (int i = 0; i < n; i++) { + map[i] = to_string(i); + } + } + ``` + +=== "Java" + + ```java title="space_complexity.java" + /* 線性階 */ + void linear(int n) { + // 長度為 n 的陣列佔用 O(n) 空間 + int[] nums = new int[n]; + // 長度為 n 的串列佔用 O(n) 空間 + List nodes = new ArrayList<>(); + for (int i = 0; i < n; i++) { + nodes.add(new ListNode(i)); + } + // 長度為 n 的雜湊表佔用 O(n) 空間 + Map map = new HashMap<>(); + for (int i = 0; i < n; i++) { + map.put(i, String.valueOf(i)); + } + } + ``` + +=== "C#" + + ```csharp title="space_complexity.cs" + /* 線性階 */ + void Linear(int n) { + // 長度為 n 的陣列佔用 O(n) 空間 + int[] nums = new int[n]; + // 長度為 n 的串列佔用 O(n) 空間 + List nodes = []; + for (int i = 0; i < n; i++) { + nodes.Add(new ListNode(i)); + } + // 長度為 n 的雜湊表佔用 O(n) 空間 + Dictionary map = []; + for (int i = 0; i < n; i++) { + map.Add(i, i.ToString()); + } + } + ``` + +=== "Go" + + ```go title="space_complexity.go" + /* 線性階 */ + func spaceLinear(n int) { + // 長度為 n 的陣列佔用 O(n) 空間 + _ = make([]int, n) + // 長度為 n 的串列佔用 O(n) 空間 + var nodes []*node + for i := 0; i < n; i++ { + nodes = append(nodes, newNode(i)) + } + // 長度為 n 的雜湊表佔用 O(n) 空間 + m := make(map[int]string, n) + for i := 0; i < n; i++ { + m[i] = strconv.Itoa(i) + } + } + ``` + +=== "Swift" + + ```swift title="space_complexity.swift" + /* 線性階 */ + func linear(n: Int) { + // 長度為 n 的陣列佔用 O(n) 空間 + let nums = Array(repeating: 0, count: n) + // 長度為 n 的串列佔用 O(n) 空間 + let nodes = (0 ..< n).map { ListNode(x: $0) } + // 長度為 n 的雜湊表佔用 O(n) 空間 + let map = Dictionary(uniqueKeysWithValues: (0 ..< n).map { ($0, "\($0)") }) + } + ``` + +=== "JS" + + ```javascript title="space_complexity.js" + /* 線性階 */ + function linear(n) { + // 長度為 n 的陣列佔用 O(n) 空間 + const nums = new Array(n); + // 長度為 n 的串列佔用 O(n) 空間 + const nodes = []; + for (let i = 0; i < n; i++) { + nodes.push(new ListNode(i)); + } + // 長度為 n 的雜湊表佔用 O(n) 空間 + const map = new Map(); + for (let i = 0; i < n; i++) { + map.set(i, i.toString()); + } + } + ``` + +=== "TS" + + ```typescript title="space_complexity.ts" + /* 線性階 */ + function linear(n: number): void { + // 長度為 n 的陣列佔用 O(n) 空間 + const nums = new Array(n); + // 長度為 n 的串列佔用 O(n) 空間 + const nodes: ListNode[] = []; + for (let i = 0; i < n; i++) { + nodes.push(new ListNode(i)); + } + // 長度為 n 的雜湊表佔用 O(n) 空間 + const map = new Map(); + for (let i = 0; i < n; i++) { + map.set(i, i.toString()); + } + } + ``` + +=== "Dart" + + ```dart title="space_complexity.dart" + /* 線性階 */ + void linear(int n) { + // 長度為 n 的陣列佔用 O(n) 空間 + List nums = List.filled(n, 0); + // 長度為 n 的串列佔用 O(n) 空間 + List nodes = []; + for (var i = 0; i < n; i++) { + nodes.add(ListNode(i)); + } + // 長度為 n 的雜湊表佔用 O(n) 空間 + Map map = HashMap(); + for (var i = 0; i < n; i++) { + map.putIfAbsent(i, () => i.toString()); + } + } + ``` + +=== "Rust" + + ```rust title="space_complexity.rs" + /* 線性階 */ + #[allow(unused)] + fn linear(n: i32) { + // 長度為 n 的陣列佔用 O(n) 空間 + let mut nums = vec![0; n as usize]; + // 長度為 n 的串列佔用 O(n) 空間 + let mut nodes = Vec::new(); + for i in 0..n { + nodes.push(ListNode::new(i)) + } + // 長度為 n 的雜湊表佔用 O(n) 空間 + let mut map = HashMap::new(); + for i in 0..n { + map.insert(i, i.to_string()); + } + } + ``` + +=== "C" + + ```c title="space_complexity.c" + /* 雜湊表 */ + typedef struct { + int key; + int val; + UT_hash_handle hh; // 基於 uthash.h 實現 + } HashTable; + + /* 線性階 */ + void linear(int n) { + // 長度為 n 的陣列佔用 O(n) 空間 + int *nums = malloc(sizeof(int) * n); + free(nums); + + // 長度為 n 的串列佔用 O(n) 空間 + ListNode **nodes = malloc(sizeof(ListNode *) * n); + for (int i = 0; i < n; i++) { + nodes[i] = newListNode(i); + } + // 記憶體釋放 + for (int i = 0; i < n; i++) { + free(nodes[i]); + } + free(nodes); + + // 長度為 n 的雜湊表佔用 O(n) 空間 + HashTable *h = NULL; + for (int i = 0; i < n; i++) { + HashTable *tmp = malloc(sizeof(HashTable)); + tmp->key = i; + tmp->val = i; + HASH_ADD_INT(h, key, tmp); + } + + // 記憶體釋放 + HashTable *curr, *tmp; + HASH_ITER(hh, h, curr, tmp) { + HASH_DEL(h, curr); + free(curr); + } + } + ``` + +=== "Kotlin" + + ```kotlin title="space_complexity.kt" + /* 線性階 */ + fun linear(n: Int) { + // 長度為 n 的陣列佔用 O(n) 空間 + val nums = Array(n) { 0 } + // 長度為 n 的串列佔用 O(n) 空間 + val nodes = mutableListOf() + for (i in 0..() + for (i in 0..
+ + +如圖 2-17 所示,此函式的遞迴深度為 $n$ ,即同時存在 $n$ 個未返回的 `linear_recur()` 函式,使用 $O(n)$ 大小的堆疊幀空間: + +=== "Python" + + ```python title="space_complexity.py" + def linear_recur(n: int): + """線性階(遞迴實現)""" + print("遞迴 n =", n) + if n == 1: + return + linear_recur(n - 1) + ``` + +=== "C++" + + ```cpp title="space_complexity.cpp" + /* 線性階(遞迴實現) */ + void linearRecur(int n) { + cout << "遞迴 n = " << n << endl; + if (n == 1) + return; + linearRecur(n - 1); + } + ``` + +=== "Java" + + ```java title="space_complexity.java" + /* 線性階(遞迴實現) */ + void linearRecur(int n) { + System.out.println("遞迴 n = " + n); + if (n == 1) + return; + linearRecur(n - 1); + } + ``` + +=== "C#" + + ```csharp title="space_complexity.cs" + /* 線性階(遞迴實現) */ + void LinearRecur(int n) { + Console.WriteLine("遞迴 n = " + n); + if (n == 1) return; + LinearRecur(n - 1); + } + ``` + +=== "Go" + + ```go title="space_complexity.go" + /* 線性階(遞迴實現) */ + func spaceLinearRecur(n int) { + fmt.Println("遞迴 n =", n) + if n == 1 { + return + } + spaceLinearRecur(n - 1) + } + ``` + +=== "Swift" + + ```swift title="space_complexity.swift" + /* 線性階(遞迴實現) */ + func linearRecur(n: Int) { + print("遞迴 n = \(n)") + if n == 1 { + return + } + linearRecur(n: n - 1) + } + ``` + +=== "JS" + + ```javascript title="space_complexity.js" + /* 線性階(遞迴實現) */ + function linearRecur(n) { + console.log(`遞迴 n = ${n}`); + if (n === 1) return; + linearRecur(n - 1); + } + ``` + +=== "TS" + + ```typescript title="space_complexity.ts" + /* 線性階(遞迴實現) */ + function linearRecur(n: number): void { + console.log(`遞迴 n = ${n}`); + if (n === 1) return; + linearRecur(n - 1); + } + ``` + +=== "Dart" + + ```dart title="space_complexity.dart" + /* 線性階(遞迴實現) */ + void linearRecur(int n) { + print('遞迴 n = $n'); + if (n == 1) return; + linearRecur(n - 1); + } + ``` + +=== "Rust" + + ```rust title="space_complexity.rs" + /* 線性階(遞迴實現) */ + fn linear_recur(n: i32) { + println!("遞迴 n = {}", n); + if n == 1 { + return; + }; + linear_recur(n - 1); + } + ``` + +=== "C" + + ```c title="space_complexity.c" + /* 線性階(遞迴實現) */ + void linearRecur(int n) { + printf("遞迴 n = %d\r\n", n); + if (n == 1) + return; + linearRecur(n - 1); + } + ``` + +=== "Kotlin" + + ```kotlin title="space_complexity.kt" + /* 線性階(遞迴實現) */ + fun linearRecur(n: Int) { + println("遞迴 n = $n") + if (n == 1) + return + linearRecur(n - 1) + } + ``` + +=== "Ruby" + + ```ruby title="space_complexity.rb" + ### 線性階(遞迴實現)### + def linear_recur(n) + puts "遞迴 n = #{n}" + return if n == 1 + linear_recur(n - 1) + end + ``` + +=== "Zig" + + ```zig title="space_complexity.zig" + // 線性階(遞迴實現) + fn linearRecur(comptime n: i32) void { + std.debug.print("遞迴 n = {}\n", .{n}); + if (n == 1) return; + linearRecur(n - 1); + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +![遞迴函式產生的線性階空間複雜度](space_complexity.assets/space_complexity_recursive_linear.png){ class="animation-figure" } + +

圖 2-17   遞迴函式產生的線性階空間複雜度

+ +### 3.   平方階 $O(n^2)$ {data-toc-label="3.   平方階"} + +平方階常見於矩陣和圖,元素數量與 $n$ 成平方關係: + +=== "Python" + + ```python title="space_complexity.py" + def quadratic(n: int): + """平方階""" + # 二維串列佔用 O(n^2) 空間 + num_matrix = [[0] * n for _ in range(n)] + ``` + +=== "C++" + + ```cpp title="space_complexity.cpp" + /* 平方階 */ + void quadratic(int n) { + // 二維串列佔用 O(n^2) 空間 + vector> numMatrix; + for (int i = 0; i < n; i++) { + vector tmp; + for (int j = 0; j < n; j++) { + tmp.push_back(0); + } + numMatrix.push_back(tmp); + } + } + ``` + +=== "Java" + + ```java title="space_complexity.java" + /* 平方階 */ + void quadratic(int n) { + // 矩陣佔用 O(n^2) 空間 + int[][] numMatrix = new int[n][n]; + // 二維串列佔用 O(n^2) 空間 + List> numList = new ArrayList<>(); + for (int i = 0; i < n; i++) { + List tmp = new ArrayList<>(); + for (int j = 0; j < n; j++) { + tmp.add(0); + } + numList.add(tmp); + } + } + ``` + +=== "C#" + + ```csharp title="space_complexity.cs" + /* 平方階 */ + void Quadratic(int n) { + // 矩陣佔用 O(n^2) 空間 + int[,] numMatrix = new int[n, n]; + // 二維串列佔用 O(n^2) 空間 + List> numList = []; + for (int i = 0; i < n; i++) { + List tmp = []; + for (int j = 0; j < n; j++) { + tmp.Add(0); + } + numList.Add(tmp); + } + } + ``` + +=== "Go" + + ```go title="space_complexity.go" + /* 平方階 */ + func spaceQuadratic(n int) { + // 矩陣佔用 O(n^2) 空間 + numMatrix := make([][]int, n) + for i := 0; i < n; i++ { + numMatrix[i] = make([]int, n) + } + } + ``` + +=== "Swift" + + ```swift title="space_complexity.swift" + /* 平方階 */ + func quadratic(n: Int) { + // 二維串列佔用 O(n^2) 空間 + let numList = Array(repeating: Array(repeating: 0, count: n), count: n) + } + ``` + +=== "JS" + + ```javascript title="space_complexity.js" + /* 平方階 */ + function quadratic(n) { + // 矩陣佔用 O(n^2) 空間 + const numMatrix = Array(n) + .fill(null) + .map(() => Array(n).fill(null)); + // 二維串列佔用 O(n^2) 空間 + const numList = []; + for (let i = 0; i < n; i++) { + const tmp = []; + for (let j = 0; j < n; j++) { + tmp.push(0); + } + numList.push(tmp); + } + } + ``` + +=== "TS" + + ```typescript title="space_complexity.ts" + /* 平方階 */ + function quadratic(n: number): void { + // 矩陣佔用 O(n^2) 空間 + const numMatrix = Array(n) + .fill(null) + .map(() => Array(n).fill(null)); + // 二維串列佔用 O(n^2) 空間 + const numList = []; + for (let i = 0; i < n; i++) { + const tmp = []; + for (let j = 0; j < n; j++) { + tmp.push(0); + } + numList.push(tmp); + } + } + ``` + +=== "Dart" + + ```dart title="space_complexity.dart" + /* 平方階 */ + void quadratic(int n) { + // 矩陣佔用 O(n^2) 空間 + List> numMatrix = List.generate(n, (_) => List.filled(n, 0)); + // 二維串列佔用 O(n^2) 空間 + List> numList = []; + for (var i = 0; i < n; i++) { + List tmp = []; + for (int j = 0; j < n; j++) { + tmp.add(0); + } + numList.add(tmp); + } + } + ``` + +=== "Rust" + + ```rust title="space_complexity.rs" + /* 平方階 */ + #[allow(unused)] + fn quadratic(n: i32) { + // 矩陣佔用 O(n^2) 空間 + let num_matrix = vec![vec![0; n as usize]; n as usize]; + // 二維串列佔用 O(n^2) 空間 + let mut num_list = Vec::new(); + for i in 0..n { + let mut tmp = Vec::new(); + for j in 0..n { + tmp.push(0); + } + num_list.push(tmp); + } + } + ``` + +=== "C" + + ```c title="space_complexity.c" + /* 平方階 */ + void quadratic(int n) { + // 二維串列佔用 O(n^2) 空間 + int **numMatrix = malloc(sizeof(int *) * n); + for (int i = 0; i < n; i++) { + int *tmp = malloc(sizeof(int) * n); + for (int j = 0; j < n; j++) { + tmp[j] = 0; + } + numMatrix[i] = tmp; + } + + // 記憶體釋放 + for (int i = 0; i < n; i++) { + free(numMatrix[i]); + } + free(numMatrix); + } + ``` + +=== "Kotlin" + + ```kotlin title="space_complexity.kt" + /* 平方階 */ + fun quadratic(n: Int) { + // 矩陣佔用 O(n^2) 空間 + val numMatrix: Array?> = arrayOfNulls(n) + // 二維串列佔用 O(n^2) 空間 + val numList: MutableList> = arrayListOf() + for (i in 0..() + for (j in 0..
+ + +如圖 2-18 所示,該函式的遞迴深度為 $n$ ,在每個遞迴函式中都初始化了一個陣列,長度分別為 $n$、$n-1$、$\dots$、$2$、$1$ ,平均長度為 $n / 2$ ,因此總體佔用 $O(n^2)$ 空間: + +=== "Python" + + ```python title="space_complexity.py" + def quadratic_recur(n: int) -> int: + """平方階(遞迴實現)""" + if n <= 0: + return 0 + # 陣列 nums 長度為 n, n-1, ..., 2, 1 + nums = [0] * n + return quadratic_recur(n - 1) + ``` + +=== "C++" + + ```cpp title="space_complexity.cpp" + /* 平方階(遞迴實現) */ + int quadraticRecur(int n) { + if (n <= 0) + return 0; + vector nums(n); + cout << "遞迴 n = " << n << " 中的 nums 長度 = " << nums.size() << endl; + return quadraticRecur(n - 1); + } + ``` + +=== "Java" + + ```java title="space_complexity.java" + /* 平方階(遞迴實現) */ + int quadraticRecur(int n) { + if (n <= 0) + return 0; + // 陣列 nums 長度為 n, n-1, ..., 2, 1 + int[] nums = new int[n]; + System.out.println("遞迴 n = " + n + " 中的 nums 長度 = " + nums.length); + return quadraticRecur(n - 1); + } + ``` + +=== "C#" + + ```csharp title="space_complexity.cs" + /* 平方階(遞迴實現) */ + int QuadraticRecur(int n) { + if (n <= 0) return 0; + int[] nums = new int[n]; + Console.WriteLine("遞迴 n = " + n + " 中的 nums 長度 = " + nums.Length); + return QuadraticRecur(n - 1); + } + ``` + +=== "Go" + + ```go title="space_complexity.go" + /* 平方階(遞迴實現) */ + func spaceQuadraticRecur(n int) int { + if n <= 0 { + return 0 + } + nums := make([]int, n) + fmt.Printf("遞迴 n = %d 中的 nums 長度 = %d \n", n, len(nums)) + return spaceQuadraticRecur(n - 1) + } + ``` + +=== "Swift" + + ```swift title="space_complexity.swift" + /* 平方階(遞迴實現) */ + @discardableResult + func quadraticRecur(n: Int) -> Int { + if n <= 0 { + return 0 + } + // 陣列 nums 長度為 n, n-1, ..., 2, 1 + let nums = Array(repeating: 0, count: n) + print("遞迴 n = \(n) 中的 nums 長度 = \(nums.count)") + return quadraticRecur(n: n - 1) + } + ``` + +=== "JS" + + ```javascript title="space_complexity.js" + /* 平方階(遞迴實現) */ + function quadraticRecur(n) { + if (n <= 0) return 0; + const nums = new Array(n); + console.log(`遞迴 n = ${n} 中的 nums 長度 = ${nums.length}`); + return quadraticRecur(n - 1); + } + ``` + +=== "TS" + + ```typescript title="space_complexity.ts" + /* 平方階(遞迴實現) */ + function quadraticRecur(n: number): number { + if (n <= 0) return 0; + const nums = new Array(n); + console.log(`遞迴 n = ${n} 中的 nums 長度 = ${nums.length}`); + return quadraticRecur(n - 1); + } + ``` + +=== "Dart" + + ```dart title="space_complexity.dart" + /* 平方階(遞迴實現) */ + int quadraticRecur(int n) { + if (n <= 0) return 0; + List nums = List.filled(n, 0); + print('遞迴 n = $n 中的 nums 長度 = ${nums.length}'); + return quadraticRecur(n - 1); + } + ``` + +=== "Rust" + + ```rust title="space_complexity.rs" + /* 平方階(遞迴實現) */ + fn quadratic_recur(n: i32) -> i32 { + if n <= 0 { + return 0; + }; + // 陣列 nums 長度為 n, n-1, ..., 2, 1 + let nums = vec![0; n as usize]; + println!("遞迴 n = {} 中的 nums 長度 = {}", n, nums.len()); + return quadratic_recur(n - 1); + } + ``` + +=== "C" + + ```c title="space_complexity.c" + /* 平方階(遞迴實現) */ + int quadraticRecur(int n) { + if (n <= 0) + return 0; + int *nums = malloc(sizeof(int) * n); + printf("遞迴 n = %d 中的 nums 長度 = %d\r\n", n, n); + int res = quadraticRecur(n - 1); + free(nums); + return res; + } + ``` + +=== "Kotlin" + + ```kotlin title="space_complexity.kt" + /* 平方階(遞迴實現) */ + tailrec fun quadraticRecur(n: Int): Int { + if (n <= 0) + return 0 + // 陣列 nums 長度為 n, n-1, ..., 2, 1 + val nums = Array(n) { 0 } + println("遞迴 n = $n 中的 nums 長度 = ${nums.size}") + return quadraticRecur(n - 1) + } + ``` + +=== "Ruby" + + ```ruby title="space_complexity.rb" + ### 平方階(遞迴實現)### + def quadratic_recur(n) + return 0 unless n > 0 + + # 陣列 nums 長度為 n, n-1, ..., 2, 1 + nums = Array.new(n, 0) + quadratic_recur(n - 1) + end + ``` + +=== "Zig" + + ```zig title="space_complexity.zig" + // 平方階(遞迴實現) + fn quadraticRecur(comptime n: i32) i32 { + if (n <= 0) return 0; + var nums = [_]i32{0}**n; + std.debug.print("遞迴 n = {} 中的 nums 長度 = {}\n", .{n, nums.len}); + return quadraticRecur(n - 1); + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +![遞迴函式產生的平方階空間複雜度](space_complexity.assets/space_complexity_recursive_quadratic.png){ class="animation-figure" } + +

圖 2-18   遞迴函式產生的平方階空間複雜度

+ +### 4.   指數階 $O(2^n)$ {data-toc-label="4.   指數階"} + +指數階常見於二元樹。觀察圖 2-19 ,層數為 $n$ 的“滿二元樹”的節點數量為 $2^n - 1$ ,佔用 $O(2^n)$ 空間: + +=== "Python" + + ```python title="space_complexity.py" + def build_tree(n: int) -> TreeNode | None: + """指數階(建立滿二元樹)""" + if n == 0: + return None + root = TreeNode(0) + root.left = build_tree(n - 1) + root.right = build_tree(n - 1) + return root + ``` + +=== "C++" + + ```cpp title="space_complexity.cpp" + /* 指數階(建立滿二元樹) */ + TreeNode *buildTree(int n) { + if (n == 0) + return nullptr; + TreeNode *root = new TreeNode(0); + root->left = buildTree(n - 1); + root->right = buildTree(n - 1); + return root; + } + ``` + +=== "Java" + + ```java title="space_complexity.java" + /* 指數階(建立滿二元樹) */ + TreeNode buildTree(int n) { + if (n == 0) + return null; + TreeNode root = new TreeNode(0); + root.left = buildTree(n - 1); + root.right = buildTree(n - 1); + return root; + } + ``` + +=== "C#" + + ```csharp title="space_complexity.cs" + /* 指數階(建立滿二元樹) */ + TreeNode? BuildTree(int n) { + if (n == 0) return null; + TreeNode root = new(0) { + left = BuildTree(n - 1), + right = BuildTree(n - 1) + }; + return root; + } + ``` + +=== "Go" + + ```go title="space_complexity.go" + /* 指數階(建立滿二元樹) */ + func buildTree(n int) *TreeNode { + if n == 0 { + return nil + } + root := NewTreeNode(0) + root.Left = buildTree(n - 1) + root.Right = buildTree(n - 1) + return root + } + ``` + +=== "Swift" + + ```swift title="space_complexity.swift" + /* 指數階(建立滿二元樹) */ + func buildTree(n: Int) -> TreeNode? { + if n == 0 { + return nil + } + let root = TreeNode(x: 0) + root.left = buildTree(n: n - 1) + root.right = buildTree(n: n - 1) + return root + } + ``` + +=== "JS" + + ```javascript title="space_complexity.js" + /* 指數階(建立滿二元樹) */ + function buildTree(n) { + if (n === 0) return null; + const root = new TreeNode(0); + root.left = buildTree(n - 1); + root.right = buildTree(n - 1); + return root; + } + ``` + +=== "TS" + + ```typescript title="space_complexity.ts" + /* 指數階(建立滿二元樹) */ + function buildTree(n: number): TreeNode | null { + if (n === 0) return null; + const root = new TreeNode(0); + root.left = buildTree(n - 1); + root.right = buildTree(n - 1); + return root; + } + ``` + +=== "Dart" + + ```dart title="space_complexity.dart" + /* 指數階(建立滿二元樹) */ + TreeNode? buildTree(int n) { + if (n == 0) return null; + TreeNode root = TreeNode(0); + root.left = buildTree(n - 1); + root.right = buildTree(n - 1); + return root; + } + ``` + +=== "Rust" + + ```rust title="space_complexity.rs" + /* 指數階(建立滿二元樹) */ + fn build_tree(n: i32) -> Option>> { + if n == 0 { + return None; + }; + let root = TreeNode::new(0); + root.borrow_mut().left = build_tree(n - 1); + root.borrow_mut().right = build_tree(n - 1); + return Some(root); + } + ``` + +=== "C" + + ```c title="space_complexity.c" + /* 指數階(建立滿二元樹) */ + TreeNode *buildTree(int n) { + if (n == 0) + return NULL; + TreeNode *root = newTreeNode(0); + root->left = buildTree(n - 1); + root->right = buildTree(n - 1); + return root; + } + ``` + +=== "Kotlin" + + ```kotlin title="space_complexity.kt" + /* 指數階(建立滿二元樹) */ + fun buildTree(n: Int): TreeNode? { + if (n == 0) + return null + val root = TreeNode(0) + root.left = buildTree(n - 1) + root.right = buildTree(n - 1) + return root + } + ``` + +=== "Ruby" + + ```ruby title="space_complexity.rb" + ### 指數階(建立滿二元樹)### + def build_tree(n) + return if n == 0 + + TreeNode.new.tap do |root| + root.left = build_tree(n - 1) + root.right = build_tree(n - 1) + end + end + ``` + +=== "Zig" + + ```zig title="space_complexity.zig" + // 指數階(建立滿二元樹) + fn buildTree(mem_allocator: std.mem.Allocator, n: i32) !?*inc.TreeNode(i32) { + if (n == 0) return null; + const root = try mem_allocator.create(inc.TreeNode(i32)); + root.init(0); + root.left = try buildTree(mem_allocator, n - 1); + root.right = try buildTree(mem_allocator, n - 1); + return root; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +![滿二元樹產生的指數階空間複雜度](space_complexity.assets/space_complexity_exponential.png){ class="animation-figure" } + +

圖 2-19   滿二元樹產生的指數階空間複雜度

+ +### 5.   對數階 $O(\log n)$ {data-toc-label="5.   對數階"} + +對數階常見於分治演算法。例如合併排序,輸入長度為 $n$ 的陣列,每輪遞迴將陣列從中點處劃分為兩半,形成高度為 $\log n$ 的遞迴樹,使用 $O(\log n)$ 堆疊幀空間。 + +再例如將數字轉化為字串,輸入一個正整數 $n$ ,它的位數為 $\lfloor \log_{10} n \rfloor + 1$ ,即對應字串長度為 $\lfloor \log_{10} n \rfloor + 1$ ,因此空間複雜度為 $O(\log_{10} n + 1) = O(\log n)$ 。 + +## 2.4.4   權衡時間與空間 + +理想情況下,我們希望演算法的時間複雜度和空間複雜度都能達到最優。然而在實際情況中,同時最佳化時間複雜度和空間複雜度通常非常困難。 + +**降低時間複雜度通常需要以提升空間複雜度為代價,反之亦然**。我們將犧牲記憶體空間來提升演算法執行速度的思路稱為“以空間換時間”;反之,則稱為“以時間換空間”。 + +選擇哪種思路取決於我們更看重哪個方面。在大多數情況下,時間比空間更寶貴,因此“以空間換時間”通常是更常用的策略。當然,在資料量很大的情況下,控制空間複雜度也非常重要。 diff --git a/zh-Hant/docs/chapter_computational_complexity/summary.md b/zh-Hant/docs/chapter_computational_complexity/summary.md new file mode 100644 index 000000000..cf876543f --- /dev/null +++ b/zh-Hant/docs/chapter_computational_complexity/summary.md @@ -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**:函式和方法這兩個術語的區別是什麼? + +函式(function)可以被獨立執行,所有參數都以顯式傳遞。方法(method)與一個物件關聯,被隱式傳遞給呼叫它的物件,能夠對類別的例項中包含的資料進行操作。 + +下面以幾種常見的程式語言為例來說明。 + +- C 語言是程序式程式設計語言,沒有物件導向的概念,所以只有函式。但我們可以透過建立結構體(struct)來模擬物件導向程式設計,與結構體相關聯的函式就相當於其他程式語言中的方法。 +- Java 和 C# 是物件導向的程式語言,程式碼塊(方法)通常作為某個類別的一部分。靜態方法的行為類似於函式,因為它被繫結在類別上,不能訪問特定的例項變數。 +- C++ 和 Python 既支持程序式程式設計(函式),也支持物件導向程式設計(方法)。 + +**Q**:圖解“常見的空間複雜度型別”反映的是否是佔用空間的絕對大小? + +不是,該圖展示的是空間複雜度,其反映的是增長趨勢,而不是佔用空間的絕對大小。 + +假設取 $n = 8$ ,你可能會發現每條曲線的值與函式對應不上。這是因為每條曲線都包含一個常數項,用於將取值範圍壓縮到一個視覺舒適的範圍內。 + +在實際中,因為我們通常不知道每個方法的“常數項”複雜度是多少,所以一般無法僅憑複雜度來選擇 $n = 8$ 之下的最優解法。但對於 $n = 8^5$ 就很好選了,這時增長趨勢已經佔主導了。 diff --git a/zh-Hant/docs/chapter_computational_complexity/time_complexity.md b/zh-Hant/docs/chapter_computational_complexity/time_complexity.md new file mode 100755 index 000000000..087bc9752 --- /dev/null +++ b/zh-Hant/docs/chapter_computational_complexity/time_complexity.md @@ -0,0 +1,3950 @@ +--- +comments: true +--- + +# 2.3   時間複雜度 + +執行時間可以直觀且準確地反映演算法的效率。如果我們想準確預估一段程式碼的執行時間,應該如何操作呢? + +1. **確定執行平臺**,包括硬體配置、程式語言、系統環境等,這些因素都會影響程式碼的執行效率。 +2. **評估各種計算操作所需的執行時間**,例如加法操作 `+` 需要 1 ns ,乘法操作 `*` 需要 10 ns ,列印操作 `print()` 需要 5 ns 等。 +3. **統計程式碼中所有的計算操作**,並將所有操作的執行時間求和,從而得到執行時間。 + +例如在以下程式碼中,輸入資料大小為 $n$ : + +=== "Python" + + ```python title="" + # 在某執行平臺下 + def algorithm(n: int): + a = 2 # 1 ns + a = a + 1 # 1 ns + a = a * 2 # 10 ns + # 迴圈 n 次 + for _ in range(n): # 1 ns + print(0) # 5 ns + ``` + +=== "C++" + + ```cpp title="" + // 在某執行平臺下 + void algorithm(int n) { + int a = 2; // 1 ns + a = a + 1; // 1 ns + a = a * 2; // 10 ns + // 迴圈 n 次 + for (int i = 0; i < n; i++) { // 1 ns ,每輪都要執行 i++ + cout << 0 << endl; // 5 ns + } + } + ``` + +=== "Java" + + ```java title="" + // 在某執行平臺下 + void algorithm(int n) { + int a = 2; // 1 ns + a = a + 1; // 1 ns + a = a * 2; // 10 ns + // 迴圈 n 次 + for (int i = 0; i < n; i++) { // 1 ns ,每輪都要執行 i++ + System.out.println(0); // 5 ns + } + } + ``` + +=== "C#" + + ```csharp title="" + // 在某執行平臺下 + void Algorithm(int n) { + int a = 2; // 1 ns + a = a + 1; // 1 ns + a = a * 2; // 10 ns + // 迴圈 n 次 + for (int i = 0; i < n; i++) { // 1 ns ,每輪都要執行 i++ + Console.WriteLine(0); // 5 ns + } + } + ``` + +=== "Go" + + ```go title="" + // 在某執行平臺下 + func algorithm(n int) { + a := 2 // 1 ns + a = a + 1 // 1 ns + a = a * 2 // 10 ns + // 迴圈 n 次 + for i := 0; i < n; i++ { // 1 ns + fmt.Println(a) // 5 ns + } + } + ``` + +=== "Swift" + + ```swift title="" + // 在某執行平臺下 + func algorithm(n: Int) { + var a = 2 // 1 ns + a = a + 1 // 1 ns + a = a * 2 // 10 ns + // 迴圈 n 次 + for _ in 0 ..< n { // 1 ns + print(0) // 5 ns + } + } + ``` + +=== "JS" + + ```javascript title="" + // 在某執行平臺下 + function algorithm(n) { + var a = 2; // 1 ns + a = a + 1; // 1 ns + a = a * 2; // 10 ns + // 迴圈 n 次 + for(let i = 0; i < n; i++) { // 1 ns ,每輪都要執行 i++ + console.log(0); // 5 ns + } + } + ``` + +=== "TS" + + ```typescript title="" + // 在某執行平臺下 + function algorithm(n: number): void { + var a: number = 2; // 1 ns + a = a + 1; // 1 ns + a = a * 2; // 10 ns + // 迴圈 n 次 + for(let i = 0; i < n; i++) { // 1 ns ,每輪都要執行 i++ + console.log(0); // 5 ns + } + } + ``` + +=== "Dart" + + ```dart title="" + // 在某執行平臺下 + void algorithm(int n) { + int a = 2; // 1 ns + a = a + 1; // 1 ns + a = a * 2; // 10 ns + // 迴圈 n 次 + for (int i = 0; i < n; i++) { // 1 ns ,每輪都要執行 i++ + print(0); // 5 ns + } + } + ``` + +=== "Rust" + + ```rust title="" + // 在某執行平臺下 + fn algorithm(n: i32) { + let mut a = 2; // 1 ns + a = a + 1; // 1 ns + a = a * 2; // 10 ns + // 迴圈 n 次 + for _ in 0..n { // 1 ns ,每輪都要執行 i++ + println!("{}", 0); // 5 ns + } + } + ``` + +=== "C" + + ```c title="" + // 在某執行平臺下 + void algorithm(int n) { + int a = 2; // 1 ns + a = a + 1; // 1 ns + a = a * 2; // 10 ns + // 迴圈 n 次 + for (int i = 0; i < n; i++) { // 1 ns ,每輪都要執行 i++ + printf("%d", 0); // 5 ns + } + } + ``` + +=== "Kotlin" + + ```kotlin title="" + // 在某執行平臺下 + fun algorithm(n: Int) { + var a = 2 // 1 ns + a = a + 1 // 1 ns + a = a * 2 // 10 ns + // 迴圈 n 次 + for (i in 0.. 圖 2-7   演算法 A、B 和 C 的時間增長趨勢

+ +相較於直接統計演算法的執行時間,時間複雜度分析有哪些特點呢? + +- **時間複雜度能夠有效評估演算法效率**。例如,演算法 `B` 的執行時間呈線性增長,在 $n > 1$ 時比演算法 `A` 更慢,在 $n > 1000000$ 時比演算法 `C` 更慢。事實上,只要輸入資料大小 $n$ 足夠大,複雜度為“常數階”的演算法一定優於“線性階”的演算法,這正是時間增長趨勢的含義。 +- **時間複雜度的推算方法更簡便**。顯然,執行平臺和計算操作型別都與演算法執行時間的增長趨勢無關。因此在時間複雜度分析中,我們可以簡單地將所有計算操作的執行時間視為相同的“單位時間”,從而將“計算操作執行時間統計”簡化為“計算操作數量統計”,這樣一來估算難度就大大降低了。 +- **時間複雜度也存在一定的侷限性**。例如,儘管演算法 `A` 和 `C` 的時間複雜度相同,但實際執行時間差別很大。同樣,儘管演算法 `B` 的時間複雜度比 `C` 高,但在輸入資料大小 $n$ 較小時,演算法 `B` 明顯優於演算法 `C` 。在這些情況下,我們很難僅憑時間複雜度判斷演算法效率的高低。當然,儘管存在上述問題,複雜度分析仍然是評判演算法效率最有效且常用的方法。 + +## 2.3.2   函式漸近上界 + +給定一個輸入大小為 $n$ 的函式: + +=== "Python" + + ```python title="" + def algorithm(n: int): + a = 1 # +1 + a = a + 1 # +1 + a = a * 2 # +1 + # 迴圈 n 次 + for i in range(n): # +1 + print(0) # +1 + ``` + +=== "C++" + + ```cpp title="" + void algorithm(int n) { + int a = 1; // +1 + a = a + 1; // +1 + a = a * 2; // +1 + // 迴圈 n 次 + for (int i = 0; i < n; i++) { // +1(每輪都執行 i ++) + cout << 0 << endl; // +1 + } + } + ``` + +=== "Java" + + ```java title="" + void algorithm(int n) { + int a = 1; // +1 + a = a + 1; // +1 + a = a * 2; // +1 + // 迴圈 n 次 + for (int i = 0; i < n; i++) { // +1(每輪都執行 i ++) + System.out.println(0); // +1 + } + } + ``` + +=== "C#" + + ```csharp title="" + void Algorithm(int n) { + int a = 1; // +1 + a = a + 1; // +1 + a = a * 2; // +1 + // 迴圈 n 次 + for (int i = 0; i < n; i++) { // +1(每輪都執行 i ++) + Console.WriteLine(0); // +1 + } + } + ``` + +=== "Go" + + ```go title="" + func algorithm(n int) { + a := 1 // +1 + a = a + 1 // +1 + a = a * 2 // +1 + // 迴圈 n 次 + for i := 0; i < n; i++ { // +1 + fmt.Println(a) // +1 + } + } + ``` + +=== "Swift" + + ```swift title="" + func algorithm(n: Int) { + var a = 1 // +1 + a = a + 1 // +1 + a = a * 2 // +1 + // 迴圈 n 次 + for _ in 0 ..< n { // +1 + print(0) // +1 + } + } + ``` + +=== "JS" + + ```javascript title="" + function algorithm(n) { + var a = 1; // +1 + a += 1; // +1 + a *= 2; // +1 + // 迴圈 n 次 + for(let i = 0; i < n; i++){ // +1(每輪都執行 i ++) + console.log(0); // +1 + } + } + ``` + +=== "TS" + + ```typescript title="" + function algorithm(n: number): void{ + var a: number = 1; // +1 + a += 1; // +1 + a *= 2; // +1 + // 迴圈 n 次 + for(let i = 0; i < n; i++){ // +1(每輪都執行 i ++) + console.log(0); // +1 + } + } + ``` + +=== "Dart" + + ```dart title="" + void algorithm(int n) { + int a = 1; // +1 + a = a + 1; // +1 + a = a * 2; // +1 + // 迴圈 n 次 + for (int i = 0; i < n; i++) { // +1(每輪都執行 i ++) + print(0); // +1 + } + } + ``` + +=== "Rust" + + ```rust title="" + fn algorithm(n: i32) { + let mut a = 1; // +1 + a = a + 1; // +1 + a = a * 2; // +1 + + // 迴圈 n 次 + for _ in 0..n { // +1(每輪都執行 i ++) + println!("{}", 0); // +1 + } + } + ``` + +=== "C" + + ```c title="" + void algorithm(int n) { + int a = 1; // +1 + a = a + 1; // +1 + a = a * 2; // +1 + // 迴圈 n 次 + for (int i = 0; i < n; i++) { // +1(每輪都執行 i ++) + printf("%d", 0); // +1 + } + } + ``` + +=== "Kotlin" + + ```kotlin title="" + fun algorithm(n: Int) { + var a = 1 // +1 + a = a + 1 // +1 + a = a * 2 // +1 + // 迴圈 n 次 + for (i in 0..大 $O$ 記號(big-$O$ notation),表示函式 $T(n)$ 的漸近上界(asymptotic upper bound)。 + +時間複雜度分析本質上是計算“操作數量 $T(n)$”的漸近上界,它具有明確的數學定義。 + +!!! abstract "函式漸近上界" + + 若存在正實數 $c$ 和實數 $n_0$ ,使得對於所有的 $n > n_0$ ,均有 $T(n) \leq c \cdot f(n)$ ,則可認為 $f(n)$ 給出了 $T(n)$ 的一個漸近上界,記為 $T(n) = O(f(n))$ 。 + +如圖 2-8 所示,計算漸近上界就是尋找一個函式 $f(n)$ ,使得當 $n$ 趨向於無窮大時,$T(n)$ 和 $f(n)$ 處於相同的增長級別,僅相差一個常數項 $c$ 的倍數。 + +![函式的漸近上界](time_complexity.assets/asymptotic_upper_bound.png){ class="animation-figure" } + +

圖 2-8   函式的漸近上界

+ +## 2.3.3   推算方法 + +漸近上界的數學味兒有點重,如果你感覺沒有完全理解,也無須擔心。我們可以先掌握推算方法,在不斷的實踐中,就可以逐漸領悟其數學意義。 + +根據定義,確定 $f(n)$ 之後,我們便可得到時間複雜度 $O(f(n))$ 。那麼如何確定漸近上界 $f(n)$ 呢?總體分為兩步:首先統計操作數量,然後判斷漸近上界。 + +### 1.   第一步:統計操作數量 + +針對程式碼,逐行從上到下計算即可。然而,由於上述 $c \cdot f(n)$ 中的常數項 $c$ 可以取任意大小,**因此操作數量 $T(n)$ 中的各種係數、常數項都可以忽略**。根據此原則,可以總結出以下計數簡化技巧。 + +1. **忽略 $T(n)$ 中的常數項**。因為它們都與 $n$ 無關,所以對時間複雜度不產生影響。 +2. **省略所有係數**。例如,迴圈 $2n$ 次、$5n + 1$ 次等,都可以簡化記為 $n$ 次,因為 $n$ 前面的係數對時間複雜度沒有影響。 +3. **迴圈巢狀時使用乘法**。總操作數量等於外層迴圈和內層迴圈操作數量之積,每一層迴圈依然可以分別套用第 `1.` 點和第 `2.` 點的技巧。 + +給定一個函式,我們可以用上述技巧來統計操作數量: + +=== "Python" + + ```python title="" + def algorithm(n: int): + a = 1 # +0(技巧 1) + a = a + n # +0(技巧 1) + # +n(技巧 2) + for i in range(5 * n + 1): + print(0) + # +n*n(技巧 3) + for i in range(2 * n): + for j in range(n + 1): + print(0) + ``` + +=== "C++" + + ```cpp title="" + void algorithm(int n) { + int a = 1; // +0(技巧 1) + a = a + n; // +0(技巧 1) + // +n(技巧 2) + for (int i = 0; i < 5 * n + 1; i++) { + cout << 0 << endl; + } + // +n*n(技巧 3) + for (int i = 0; i < 2 * n; i++) { + for (int j = 0; j < n + 1; j++) { + cout << 0 << endl; + } + } + } + ``` + +=== "Java" + + ```java title="" + void algorithm(int n) { + int a = 1; // +0(技巧 1) + a = a + n; // +0(技巧 1) + // +n(技巧 2) + for (int i = 0; i < 5 * n + 1; i++) { + System.out.println(0); + } + // +n*n(技巧 3) + for (int i = 0; i < 2 * n; i++) { + for (int j = 0; j < n + 1; j++) { + System.out.println(0); + } + } + } + ``` + +=== "C#" + + ```csharp title="" + void Algorithm(int n) { + int a = 1; // +0(技巧 1) + a = a + n; // +0(技巧 1) + // +n(技巧 2) + for (int i = 0; i < 5 * n + 1; i++) { + Console.WriteLine(0); + } + // +n*n(技巧 3) + for (int i = 0; i < 2 * n; i++) { + for (int j = 0; j < n + 1; j++) { + Console.WriteLine(0); + } + } + } + ``` + +=== "Go" + + ```go title="" + func algorithm(n int) { + a := 1 // +0(技巧 1) + a = a + n // +0(技巧 1) + // +n(技巧 2) + for i := 0; i < 5 * n + 1; i++ { + fmt.Println(0) + } + // +n*n(技巧 3) + for i := 0; i < 2 * n; i++ { + for j := 0; j < n + 1; j++ { + fmt.Println(0) + } + } + } + ``` + +=== "Swift" + + ```swift title="" + func algorithm(n: Int) { + var a = 1 // +0(技巧 1) + a = a + n // +0(技巧 1) + // +n(技巧 2) + for _ in 0 ..< (5 * n + 1) { + print(0) + } + // +n*n(技巧 3) + for _ in 0 ..< (2 * n) { + for _ in 0 ..< (n + 1) { + print(0) + } + } + } + ``` + +=== "JS" + + ```javascript title="" + function algorithm(n) { + let a = 1; // +0(技巧 1) + a = a + n; // +0(技巧 1) + // +n(技巧 2) + for (let i = 0; i < 5 * n + 1; i++) { + console.log(0); + } + // +n*n(技巧 3) + for (let i = 0; i < 2 * n; i++) { + for (let j = 0; j < n + 1; j++) { + console.log(0); + } + } + } + ``` + +=== "TS" + + ```typescript title="" + function algorithm(n: number): void { + let a = 1; // +0(技巧 1) + a = a + n; // +0(技巧 1) + // +n(技巧 2) + for (let i = 0; i < 5 * n + 1; i++) { + console.log(0); + } + // +n*n(技巧 3) + for (let i = 0; i < 2 * n; i++) { + for (let j = 0; j < n + 1; j++) { + console.log(0); + } + } + } + ``` + +=== "Dart" + + ```dart title="" + void algorithm(int n) { + int a = 1; // +0(技巧 1) + a = a + n; // +0(技巧 1) + // +n(技巧 2) + for (int i = 0; i < 5 * n + 1; i++) { + print(0); + } + // +n*n(技巧 3) + for (int i = 0; i < 2 * n; i++) { + for (int j = 0; j < n + 1; j++) { + print(0); + } + } + } + ``` + +=== "Rust" + + ```rust title="" + fn algorithm(n: i32) { + let mut a = 1; // +0(技巧 1) + a = a + n; // +0(技巧 1) + + // +n(技巧 2) + for i in 0..(5 * n + 1) { + println!("{}", 0); + } + + // +n*n(技巧 3) + for i in 0..(2 * n) { + for j in 0..(n + 1) { + println!("{}", 0); + } + } + } + ``` + +=== "C" + + ```c title="" + void algorithm(int n) { + int a = 1; // +0(技巧 1) + a = a + n; // +0(技巧 1) + // +n(技巧 2) + for (int i = 0; i < 5 * n + 1; i++) { + printf("%d", 0); + } + // +n*n(技巧 3) + for (int i = 0; i < 2 * n; i++) { + for (int j = 0; j < n + 1; j++) { + printf("%d", 0); + } + } + } + ``` + +=== "Kotlin" + + ```kotlin title="" + fun algorithm(n: Int) { + var a = 1 // +0(技巧 1) + a = a + n // +0(技巧 1) + // +n(技巧 2) + for (i in 0..<5 * n + 1) { + println(0) + } + // +n*n(技巧 3) + for (i in 0..<2 * n) { + for (j in 0.. 表 2-2   不同操作數量對應的時間複雜度

+ +
+ +| 操作數量 $T(n)$ | 時間複雜度 $O(f(n))$ | +| ---------------------- | -------------------- | +| $100000$ | $O(1)$ | +| $3n + 2$ | $O(n)$ | +| $2n^2 + 3n + 2$ | $O(n^2)$ | +| $n^3 + 10000n^2$ | $O(n^3)$ | +| $2^n + 10000n^{10000}$ | $O(2^n)$ | + +
+ +## 2.3.4   常見型別 + +設輸入資料大小為 $n$ ,常見的時間複雜度型別如圖 2-9 所示(按照從低到高的順序排列)。 + +$$ +\begin{aligned} +O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!) \newline +\text{常數階} < \text{對數階} < \text{線性階} < \text{線性對數階} < \text{平方階} < \text{指數階} < \text{階乘階} +\end{aligned} +$$ + +![常見的時間複雜度型別](time_complexity.assets/time_complexity_common_types.png){ class="animation-figure" } + +

圖 2-9   常見的時間複雜度型別

+ +### 1.   常數階 $O(1)$ {data-toc-label="1.   常數階"} + +常數階的操作數量與輸入資料大小 $n$ 無關,即不隨著 $n$ 的變化而變化。 + +在以下函式中,儘管操作數量 `size` 可能很大,但由於其與輸入資料大小 $n$ 無關,因此時間複雜度仍為 $O(1)$ : + +=== "Python" + + ```python title="time_complexity.py" + def constant(n: int) -> int: + """常數階""" + count = 0 + size = 100000 + for _ in range(size): + count += 1 + return count + ``` + +=== "C++" + + ```cpp title="time_complexity.cpp" + /* 常數階 */ + int constant(int n) { + int count = 0; + int size = 100000; + for (int i = 0; i < size; i++) + count++; + return count; + } + ``` + +=== "Java" + + ```java title="time_complexity.java" + /* 常數階 */ + int constant(int n) { + int count = 0; + int size = 100000; + for (int i = 0; i < size; i++) + count++; + return count; + } + ``` + +=== "C#" + + ```csharp title="time_complexity.cs" + /* 常數階 */ + int Constant(int n) { + int count = 0; + int size = 100000; + for (int i = 0; i < size; i++) + count++; + return count; + } + ``` + +=== "Go" + + ```go title="time_complexity.go" + /* 常數階 */ + func constant(n int) int { + count := 0 + size := 100000 + for i := 0; i < size; i++ { + count++ + } + return count + } + ``` + +=== "Swift" + + ```swift title="time_complexity.swift" + /* 常數階 */ + func constant(n: Int) -> Int { + var count = 0 + let size = 100_000 + for _ in 0 ..< size { + count += 1 + } + return count + } + ``` + +=== "JS" + + ```javascript title="time_complexity.js" + /* 常數階 */ + function constant(n) { + let count = 0; + const size = 100000; + for (let i = 0; i < size; i++) count++; + return count; + } + ``` + +=== "TS" + + ```typescript title="time_complexity.ts" + /* 常數階 */ + function constant(n: number): number { + let count = 0; + const size = 100000; + for (let i = 0; i < size; i++) count++; + return count; + } + ``` + +=== "Dart" + + ```dart title="time_complexity.dart" + /* 常數階 */ + int constant(int n) { + int count = 0; + int size = 100000; + for (var i = 0; i < size; i++) { + count++; + } + return count; + } + ``` + +=== "Rust" + + ```rust title="time_complexity.rs" + /* 常數階 */ + fn constant(n: i32) -> i32 { + _ = n; + let mut count = 0; + let size = 100_000; + for _ in 0..size { + count += 1; + } + count + } + ``` + +=== "C" + + ```c title="time_complexity.c" + /* 常數階 */ + int constant(int n) { + int count = 0; + int size = 100000; + int i = 0; + for (int i = 0; i < size; i++) { + count++; + } + return count; + } + ``` + +=== "Kotlin" + + ```kotlin title="time_complexity.kt" + /* 常數階 */ + fun constant(n: Int): Int { + var count = 0 + val size = 10_0000 + for (i in 0..
+ + +### 2.   線性階 $O(n)$ {data-toc-label="2.   線性階"} + +線性階的操作數量相對於輸入資料大小 $n$ 以線性級別增長。線性階通常出現在單層迴圈中: + +=== "Python" + + ```python title="time_complexity.py" + def linear(n: int) -> int: + """線性階""" + count = 0 + for _ in range(n): + count += 1 + return count + ``` + +=== "C++" + + ```cpp title="time_complexity.cpp" + /* 線性階 */ + int linear(int n) { + int count = 0; + for (int i = 0; i < n; i++) + count++; + return count; + } + ``` + +=== "Java" + + ```java title="time_complexity.java" + /* 線性階 */ + int linear(int n) { + int count = 0; + for (int i = 0; i < n; i++) + count++; + return count; + } + ``` + +=== "C#" + + ```csharp title="time_complexity.cs" + /* 線性階 */ + int Linear(int n) { + int count = 0; + for (int i = 0; i < n; i++) + count++; + return count; + } + ``` + +=== "Go" + + ```go title="time_complexity.go" + /* 線性階 */ + func linear(n int) int { + count := 0 + for i := 0; i < n; i++ { + count++ + } + return count + } + ``` + +=== "Swift" + + ```swift title="time_complexity.swift" + /* 線性階 */ + func linear(n: Int) -> Int { + var count = 0 + for _ in 0 ..< n { + count += 1 + } + return count + } + ``` + +=== "JS" + + ```javascript title="time_complexity.js" + /* 線性階 */ + function linear(n) { + let count = 0; + for (let i = 0; i < n; i++) count++; + return count; + } + ``` + +=== "TS" + + ```typescript title="time_complexity.ts" + /* 線性階 */ + function linear(n: number): number { + let count = 0; + for (let i = 0; i < n; i++) count++; + return count; + } + ``` + +=== "Dart" + + ```dart title="time_complexity.dart" + /* 線性階 */ + int linear(int n) { + int count = 0; + for (var i = 0; i < n; i++) { + count++; + } + return count; + } + ``` + +=== "Rust" + + ```rust title="time_complexity.rs" + /* 線性階 */ + fn linear(n: i32) -> i32 { + let mut count = 0; + for _ in 0..n { + count += 1; + } + count + } + ``` + +=== "C" + + ```c title="time_complexity.c" + /* 線性階 */ + int linear(int n) { + int count = 0; + for (int i = 0; i < n; i++) { + count++; + } + return count; + } + ``` + +=== "Kotlin" + + ```kotlin title="time_complexity.kt" + /* 線性階 */ + fun linear(n: Int): Int { + var count = 0 + // 迴圈次數與陣列長度成正比 + for (i in 0..
+ + +走訪陣列和走訪鏈結串列等操作的時間複雜度均為 $O(n)$ ,其中 $n$ 為陣列或鏈結串列的長度: + +=== "Python" + + ```python title="time_complexity.py" + def array_traversal(nums: list[int]) -> int: + """線性階(走訪陣列)""" + count = 0 + # 迴圈次數與陣列長度成正比 + for num in nums: + count += 1 + return count + ``` + +=== "C++" + + ```cpp title="time_complexity.cpp" + /* 線性階(走訪陣列) */ + int arrayTraversal(vector &nums) { + int count = 0; + // 迴圈次數與陣列長度成正比 + for (int num : nums) { + count++; + } + return count; + } + ``` + +=== "Java" + + ```java title="time_complexity.java" + /* 線性階(走訪陣列) */ + int arrayTraversal(int[] nums) { + int count = 0; + // 迴圈次數與陣列長度成正比 + for (int num : nums) { + count++; + } + return count; + } + ``` + +=== "C#" + + ```csharp title="time_complexity.cs" + /* 線性階(走訪陣列) */ + int ArrayTraversal(int[] nums) { + int count = 0; + // 迴圈次數與陣列長度成正比 + foreach (int num in nums) { + count++; + } + return count; + } + ``` + +=== "Go" + + ```go title="time_complexity.go" + /* 線性階(走訪陣列) */ + func arrayTraversal(nums []int) int { + count := 0 + // 迴圈次數與陣列長度成正比 + for range nums { + count++ + } + return count + } + ``` + +=== "Swift" + + ```swift title="time_complexity.swift" + /* 線性階(走訪陣列) */ + func arrayTraversal(nums: [Int]) -> Int { + var count = 0 + // 迴圈次數與陣列長度成正比 + for _ in nums { + count += 1 + } + return count + } + ``` + +=== "JS" + + ```javascript title="time_complexity.js" + /* 線性階(走訪陣列) */ + function arrayTraversal(nums) { + let count = 0; + // 迴圈次數與陣列長度成正比 + for (let i = 0; i < nums.length; i++) { + count++; + } + return count; + } + ``` + +=== "TS" + + ```typescript title="time_complexity.ts" + /* 線性階(走訪陣列) */ + function arrayTraversal(nums: number[]): number { + let count = 0; + // 迴圈次數與陣列長度成正比 + for (let i = 0; i < nums.length; i++) { + count++; + } + return count; + } + ``` + +=== "Dart" + + ```dart title="time_complexity.dart" + /* 線性階(走訪陣列) */ + int arrayTraversal(List nums) { + int count = 0; + // 迴圈次數與陣列長度成正比 + for (var _num in nums) { + count++; + } + return count; + } + ``` + +=== "Rust" + + ```rust title="time_complexity.rs" + /* 線性階(走訪陣列) */ + fn array_traversal(nums: &[i32]) -> i32 { + let mut count = 0; + // 迴圈次數與陣列長度成正比 + for _ in nums { + count += 1; + } + count + } + ``` + +=== "C" + + ```c title="time_complexity.c" + /* 線性階(走訪陣列) */ + int arrayTraversal(int *nums, int n) { + int count = 0; + // 迴圈次數與陣列長度成正比 + for (int i = 0; i < n; i++) { + count++; + } + return count; + } + ``` + +=== "Kotlin" + + ```kotlin title="time_complexity.kt" + /* 線性階(走訪陣列) */ + fun arrayTraversal(nums: IntArray): Int { + var count = 0 + // 迴圈次數與陣列長度成正比 + for (num in nums) { + count++ + } + return count + } + ``` + +=== "Ruby" + + ```ruby title="time_complexity.rb" + ### 線性階(走訪陣列)### + def array_traversal(nums) + count = 0 + + # 迴圈次數與陣列長度成正比 + for num in nums + count += 1 + end + + count + end + ``` + +=== "Zig" + + ```zig title="time_complexity.zig" + // 線性階(走訪陣列) + fn arrayTraversal(nums: []i32) i32 { + var count: i32 = 0; + // 迴圈次數與陣列長度成正比 + for (nums) |_| { + count += 1; + } + return count; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +值得注意的是,**輸入資料大小 $n$ 需根據輸入資料的型別來具體確定**。比如在第一個示例中,變數 $n$ 為輸入資料大小;在第二個示例中,陣列長度 $n$ 為資料大小。 + +### 3.   平方階 $O(n^2)$ {data-toc-label="3.   平方階"} + +平方階的操作數量相對於輸入資料大小 $n$ 以平方級別增長。平方階通常出現在巢狀迴圈中,外層迴圈和內層迴圈的時間複雜度都為 $O(n)$ ,因此總體的時間複雜度為 $O(n^2)$ : + +=== "Python" + + ```python title="time_complexity.py" + def quadratic(n: int) -> int: + """平方階""" + count = 0 + # 迴圈次數與資料大小 n 成平方關係 + for i in range(n): + for j in range(n): + count += 1 + return count + ``` + +=== "C++" + + ```cpp title="time_complexity.cpp" + /* 平方階 */ + int quadratic(int n) { + int count = 0; + // 迴圈次數與資料大小 n 成平方關係 + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + count++; + } + } + return count; + } + ``` + +=== "Java" + + ```java title="time_complexity.java" + /* 平方階 */ + int quadratic(int n) { + int count = 0; + // 迴圈次數與資料大小 n 成平方關係 + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + count++; + } + } + return count; + } + ``` + +=== "C#" + + ```csharp title="time_complexity.cs" + /* 平方階 */ + int Quadratic(int n) { + int count = 0; + // 迴圈次數與資料大小 n 成平方關係 + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + count++; + } + } + return count; + } + ``` + +=== "Go" + + ```go title="time_complexity.go" + /* 平方階 */ + func quadratic(n int) int { + count := 0 + // 迴圈次數與資料大小 n 成平方關係 + for i := 0; i < n; i++ { + for j := 0; j < n; j++ { + count++ + } + } + return count + } + ``` + +=== "Swift" + + ```swift title="time_complexity.swift" + /* 平方階 */ + func quadratic(n: Int) -> Int { + var count = 0 + // 迴圈次數與資料大小 n 成平方關係 + for _ in 0 ..< n { + for _ in 0 ..< n { + count += 1 + } + } + return count + } + ``` + +=== "JS" + + ```javascript title="time_complexity.js" + /* 平方階 */ + function quadratic(n) { + let count = 0; + // 迴圈次數與資料大小 n 成平方關係 + for (let i = 0; i < n; i++) { + for (let j = 0; j < n; j++) { + count++; + } + } + return count; + } + ``` + +=== "TS" + + ```typescript title="time_complexity.ts" + /* 平方階 */ + function quadratic(n: number): number { + let count = 0; + // 迴圈次數與資料大小 n 成平方關係 + for (let i = 0; i < n; i++) { + for (let j = 0; j < n; j++) { + count++; + } + } + return count; + } + ``` + +=== "Dart" + + ```dart title="time_complexity.dart" + /* 平方階 */ + int quadratic(int n) { + int count = 0; + // 迴圈次數與資料大小 n 成平方關係 + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + count++; + } + } + return count; + } + ``` + +=== "Rust" + + ```rust title="time_complexity.rs" + /* 平方階 */ + fn quadratic(n: i32) -> i32 { + let mut count = 0; + // 迴圈次數與資料大小 n 成平方關係 + for _ in 0..n { + for _ in 0..n { + count += 1; + } + } + count + } + ``` + +=== "C" + + ```c title="time_complexity.c" + /* 平方階 */ + int quadratic(int n) { + int count = 0; + // 迴圈次數與資料大小 n 成平方關係 + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + count++; + } + } + return count; + } + ``` + +=== "Kotlin" + + ```kotlin title="time_complexity.kt" + /* 平方階 */ + fun quadratic(n: Int): Int { + var count = 0 + // 迴圈次數與資料大小 n 成平方關係 + for (i in 0..
+
全螢幕觀看 >
+ +圖 2-10 對比了常數階、線性階和平方階三種時間複雜度。 + +![常數階、線性階和平方階的時間複雜度](time_complexity.assets/time_complexity_constant_linear_quadratic.png){ class="animation-figure" } + +

圖 2-10   常數階、線性階和平方階的時間複雜度

+ +以泡沫排序為例,外層迴圈執行 $n - 1$ 次,內層迴圈執行 $n-1$、$n-2$、$\dots$、$2$、$1$ 次,平均為 $n / 2$ 次,因此時間複雜度為 $O((n - 1) n / 2) = O(n^2)$ : + +=== "Python" + + ```python title="time_complexity.py" + def bubble_sort(nums: list[int]) -> int: + """平方階(泡沫排序)""" + count = 0 # 計數器 + # 外迴圈:未排序區間為 [0, i] + for i in range(len(nums) - 1, 0, -1): + # 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for j in range(i): + if nums[j] > nums[j + 1]: + # 交換 nums[j] 與 nums[j + 1] + tmp: int = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = tmp + count += 3 # 元素交換包含 3 個單元操作 + return count + ``` + +=== "C++" + + ```cpp title="time_complexity.cpp" + /* 平方階(泡沫排序) */ + int bubbleSort(vector &nums) { + int count = 0; // 計數器 + // 外迴圈:未排序區間為 [0, i] + for (int i = nums.size() - 1; i > 0; i--) { + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交換 nums[j] 與 nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // 元素交換包含 3 個單元操作 + } + } + } + return count; + } + ``` + +=== "Java" + + ```java title="time_complexity.java" + /* 平方階(泡沫排序) */ + int bubbleSort(int[] nums) { + int count = 0; // 計數器 + // 外迴圈:未排序區間為 [0, i] + for (int i = nums.length - 1; i > 0; i--) { + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交換 nums[j] 與 nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // 元素交換包含 3 個單元操作 + } + } + } + return count; + } + ``` + +=== "C#" + + ```csharp title="time_complexity.cs" + /* 平方階(泡沫排序) */ + int BubbleSort(int[] nums) { + int count = 0; // 計數器 + // 外迴圈:未排序區間為 [0, i] + for (int i = nums.Length - 1; i > 0; i--) { + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交換 nums[j] 與 nums[j + 1] + (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]); + count += 3; // 元素交換包含 3 個單元操作 + } + } + } + return count; + } + ``` + +=== "Go" + + ```go title="time_complexity.go" + /* 平方階(泡沫排序) */ + func bubbleSort(nums []int) int { + count := 0 // 計數器 + // 外迴圈:未排序區間為 [0, i] + for i := len(nums) - 1; i > 0; i-- { + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for j := 0; j < i; j++ { + if nums[j] > nums[j+1] { + // 交換 nums[j] 與 nums[j + 1] + tmp := nums[j] + nums[j] = nums[j+1] + nums[j+1] = tmp + count += 3 // 元素交換包含 3 個單元操作 + } + } + } + return count + } + ``` + +=== "Swift" + + ```swift title="time_complexity.swift" + /* 平方階(泡沫排序) */ + func bubbleSort(nums: inout [Int]) -> Int { + var count = 0 // 計數器 + // 外迴圈:未排序區間為 [0, i] + for i in nums.indices.dropFirst().reversed() { + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for j in 0 ..< i { + if nums[j] > nums[j + 1] { + // 交換 nums[j] 與 nums[j + 1] + let tmp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = tmp + count += 3 // 元素交換包含 3 個單元操作 + } + } + } + return count + } + ``` + +=== "JS" + + ```javascript title="time_complexity.js" + /* 平方階(泡沫排序) */ + function bubbleSort(nums) { + let count = 0; // 計數器 + // 外迴圈:未排序區間為 [0, i] + for (let i = nums.length - 1; i > 0; i--) { + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for (let j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交換 nums[j] 與 nums[j + 1] + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // 元素交換包含 3 個單元操作 + } + } + } + return count; + } + ``` + +=== "TS" + + ```typescript title="time_complexity.ts" + /* 平方階(泡沫排序) */ + function bubbleSort(nums: number[]): number { + let count = 0; // 計數器 + // 外迴圈:未排序區間為 [0, i] + for (let i = nums.length - 1; i > 0; i--) { + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for (let j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交換 nums[j] 與 nums[j + 1] + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // 元素交換包含 3 個單元操作 + } + } + } + return count; + } + ``` + +=== "Dart" + + ```dart title="time_complexity.dart" + /* 平方階(泡沫排序) */ + int bubbleSort(List nums) { + int count = 0; // 計數器 + // 外迴圈:未排序區間為 [0, i] + for (var i = nums.length - 1; i > 0; i--) { + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for (var j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交換 nums[j] 與 nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // 元素交換包含 3 個單元操作 + } + } + } + return count; + } + ``` + +=== "Rust" + + ```rust title="time_complexity.rs" + /* 平方階(泡沫排序) */ + fn bubble_sort(nums: &mut [i32]) -> i32 { + let mut count = 0; // 計數器 + + // 外迴圈:未排序區間為 [0, i] + for i in (1..nums.len()).rev() { + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for j in 0..i { + if nums[j] > nums[j + 1] { + // 交換 nums[j] 與 nums[j + 1] + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // 元素交換包含 3 個單元操作 + } + } + } + count + } + ``` + +=== "C" + + ```c title="time_complexity.c" + /* 平方階(泡沫排序) */ + int bubbleSort(int *nums, int n) { + int count = 0; // 計數器 + // 外迴圈:未排序區間為 [0, i] + for (int i = n - 1; i > 0; i--) { + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交換 nums[j] 與 nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // 元素交換包含 3 個單元操作 + } + } + } + return count; + } + ``` + +=== "Kotlin" + + ```kotlin title="time_complexity.kt" + /* 平方階(泡沫排序) */ + fun bubbleSort(nums: IntArray): Int { + var count = 0 + // 外迴圈:未排序區間為 [0, i] + for (i in nums.size - 1 downTo 1) { + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for (j in 0.. nums[j + 1]) { + // 交換 nums[j] 與 nums[j + 1] + nums[j] = nums[j + 1].also { nums[j + 1] = nums[j] } + count += 3 // 元素交換包含 3 個單元操作 + } + } + } + return count + } + ``` + +=== "Ruby" + + ```ruby title="time_complexity.rb" + ### 平方階(泡沫排序)### + def bubble_sort(nums) + count = 0 # 計數器 + + # 外迴圈:未排序區間為 [0, i] + for i in (nums.length - 1).downto(0) + # 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for j in 0...i + if nums[j] > nums[j + 1] + # 交換 nums[j] 與 nums[j + 1] + tmp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = tmp + count += 3 # 元素交換包含 3 個單元操作 + end + end + end + + count + end + ``` + +=== "Zig" + + ```zig title="time_complexity.zig" + // 平方階(泡沫排序) + fn bubbleSort(nums: []i32) i32 { + var count: i32 = 0; // 計數器 + // 外迴圈:未排序區間為 [0, i] + var i: i32 = @as(i32, @intCast(nums.len)) - 1; + while (i > 0) : (i -= 1) { + var j: usize = 0; + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + while (j < i) : (j += 1) { + if (nums[j] > nums[j + 1]) { + // 交換 nums[j] 與 nums[j + 1] + var tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // 元素交換包含 3 個單元操作 + } + } + } + return count; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +### 4.   指數階 $O(2^n)$ {data-toc-label="4.   指數階"} + +生物學的“細胞分裂”是指數階增長的典型例子:初始狀態為 $1$ 個細胞,分裂一輪後變為 $2$ 個,分裂兩輪後變為 $4$ 個,以此類推,分裂 $n$ 輪後有 $2^n$ 個細胞。 + +圖 2-11 和以下程式碼模擬了細胞分裂的過程,時間複雜度為 $O(2^n)$ : + +=== "Python" + + ```python title="time_complexity.py" + def exponential(n: int) -> int: + """指數階(迴圈實現)""" + count = 0 + base = 1 + # 細胞每輪一分為二,形成數列 1, 2, 4, 8, ..., 2^(n-1) + for _ in range(n): + for _ in range(base): + count += 1 + base *= 2 + # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count + ``` + +=== "C++" + + ```cpp title="time_complexity.cpp" + /* 指數階(迴圈實現) */ + int exponential(int n) { + int count = 0, base = 1; + // 細胞每輪一分為二,形成數列 1, 2, 4, 8, ..., 2^(n-1) + for (int i = 0; i < n; i++) { + for (int j = 0; j < base; j++) { + count++; + } + base *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count; + } + ``` + +=== "Java" + + ```java title="time_complexity.java" + /* 指數階(迴圈實現) */ + int exponential(int n) { + int count = 0, base = 1; + // 細胞每輪一分為二,形成數列 1, 2, 4, 8, ..., 2^(n-1) + for (int i = 0; i < n; i++) { + for (int j = 0; j < base; j++) { + count++; + } + base *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count; + } + ``` + +=== "C#" + + ```csharp title="time_complexity.cs" + /* 指數階(迴圈實現) */ + int Exponential(int n) { + int count = 0, bas = 1; + // 細胞每輪一分為二,形成數列 1, 2, 4, 8, ..., 2^(n-1) + for (int i = 0; i < n; i++) { + for (int j = 0; j < bas; j++) { + count++; + } + bas *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count; + } + ``` + +=== "Go" + + ```go title="time_complexity.go" + /* 指數階(迴圈實現)*/ + func exponential(n int) int { + count, base := 0, 1 + // 細胞每輪一分為二,形成數列 1, 2, 4, 8, ..., 2^(n-1) + for i := 0; i < n; i++ { + for j := 0; j < base; j++ { + count++ + } + base *= 2 + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count + } + ``` + +=== "Swift" + + ```swift title="time_complexity.swift" + /* 指數階(迴圈實現) */ + func exponential(n: Int) -> Int { + var count = 0 + var base = 1 + // 細胞每輪一分為二,形成數列 1, 2, 4, 8, ..., 2^(n-1) + for _ in 0 ..< n { + for _ in 0 ..< base { + count += 1 + } + base *= 2 + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count + } + ``` + +=== "JS" + + ```javascript title="time_complexity.js" + /* 指數階(迴圈實現) */ + function exponential(n) { + let count = 0, + base = 1; + // 細胞每輪一分為二,形成數列 1, 2, 4, 8, ..., 2^(n-1) + for (let i = 0; i < n; i++) { + for (let j = 0; j < base; j++) { + count++; + } + base *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count; + } + ``` + +=== "TS" + + ```typescript title="time_complexity.ts" + /* 指數階(迴圈實現) */ + function exponential(n: number): number { + let count = 0, + base = 1; + // 細胞每輪一分為二,形成數列 1, 2, 4, 8, ..., 2^(n-1) + for (let i = 0; i < n; i++) { + for (let j = 0; j < base; j++) { + count++; + } + base *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count; + } + ``` + +=== "Dart" + + ```dart title="time_complexity.dart" + /* 指數階(迴圈實現) */ + int exponential(int n) { + int count = 0, base = 1; + // 細胞每輪一分為二,形成數列 1, 2, 4, 8, ..., 2^(n-1) + for (var i = 0; i < n; i++) { + for (var j = 0; j < base; j++) { + count++; + } + base *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count; + } + ``` + +=== "Rust" + + ```rust title="time_complexity.rs" + /* 指數階(迴圈實現) */ + fn exponential(n: i32) -> i32 { + let mut count = 0; + let mut base = 1; + // 細胞每輪一分為二,形成數列 1, 2, 4, 8, ..., 2^(n-1) + for _ in 0..n { + for _ in 0..base { + count += 1 + } + base *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + count + } + ``` + +=== "C" + + ```c title="time_complexity.c" + /* 指數階(迴圈實現) */ + int exponential(int n) { + int count = 0; + int bas = 1; + // 細胞每輪一分為二,形成數列 1, 2, 4, 8, ..., 2^(n-1) + for (int i = 0; i < n; i++) { + for (int j = 0; j < bas; j++) { + count++; + } + bas *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count; + } + ``` + +=== "Kotlin" + + ```kotlin title="time_complexity.kt" + /* 指數階(迴圈實現) */ + fun exponential(n: Int): Int { + var count = 0 + // 細胞每輪一分為二,形成數列 1, 2, 4, 8, ..., 2^(n-1) + var base = 1 + for (i in 0..
+
全螢幕觀看 >
+ +![指數階的時間複雜度](time_complexity.assets/time_complexity_exponential.png){ class="animation-figure" } + +

圖 2-11   指數階的時間複雜度

+ +在實際演算法中,指數階常出現於遞迴函式中。例如在以下程式碼中,其遞迴地一分為二,經過 $n$ 次分裂後停止: + +=== "Python" + + ```python title="time_complexity.py" + def exp_recur(n: int) -> int: + """指數階(遞迴實現)""" + if n == 1: + return 1 + return exp_recur(n - 1) + exp_recur(n - 1) + 1 + ``` + +=== "C++" + + ```cpp title="time_complexity.cpp" + /* 指數階(遞迴實現) */ + int expRecur(int n) { + if (n == 1) + return 1; + return expRecur(n - 1) + expRecur(n - 1) + 1; + } + ``` + +=== "Java" + + ```java title="time_complexity.java" + /* 指數階(遞迴實現) */ + int expRecur(int n) { + if (n == 1) + return 1; + return expRecur(n - 1) + expRecur(n - 1) + 1; + } + ``` + +=== "C#" + + ```csharp title="time_complexity.cs" + /* 指數階(遞迴實現) */ + int ExpRecur(int n) { + if (n == 1) return 1; + return ExpRecur(n - 1) + ExpRecur(n - 1) + 1; + } + ``` + +=== "Go" + + ```go title="time_complexity.go" + /* 指數階(遞迴實現)*/ + func expRecur(n int) int { + if n == 1 { + return 1 + } + return expRecur(n-1) + expRecur(n-1) + 1 + } + ``` + +=== "Swift" + + ```swift title="time_complexity.swift" + /* 指數階(遞迴實現) */ + func expRecur(n: Int) -> Int { + if n == 1 { + return 1 + } + return expRecur(n: n - 1) + expRecur(n: n - 1) + 1 + } + ``` + +=== "JS" + + ```javascript title="time_complexity.js" + /* 指數階(遞迴實現) */ + function expRecur(n) { + if (n === 1) return 1; + return expRecur(n - 1) + expRecur(n - 1) + 1; + } + ``` + +=== "TS" + + ```typescript title="time_complexity.ts" + /* 指數階(遞迴實現) */ + function expRecur(n: number): number { + if (n === 1) return 1; + return expRecur(n - 1) + expRecur(n - 1) + 1; + } + ``` + +=== "Dart" + + ```dart title="time_complexity.dart" + /* 指數階(遞迴實現) */ + int expRecur(int n) { + if (n == 1) return 1; + return expRecur(n - 1) + expRecur(n - 1) + 1; + } + ``` + +=== "Rust" + + ```rust title="time_complexity.rs" + /* 指數階(遞迴實現) */ + fn exp_recur(n: i32) -> i32 { + if n == 1 { + return 1; + } + exp_recur(n - 1) + exp_recur(n - 1) + 1 + } + ``` + +=== "C" + + ```c title="time_complexity.c" + /* 指數階(遞迴實現) */ + int expRecur(int n) { + if (n == 1) + return 1; + return expRecur(n - 1) + expRecur(n - 1) + 1; + } + ``` + +=== "Kotlin" + + ```kotlin title="time_complexity.kt" + /* 指數階(遞迴實現) */ + fun expRecur(n: Int): Int { + if (n == 1) { + return 1 + } + return expRecur(n - 1) + expRecur(n - 1) + 1 + } + ``` + +=== "Ruby" + + ```ruby title="time_complexity.rb" + ### 指數階(遞迴實現)### + def exp_recur(n) + return 1 if n == 1 + exp_recur(n - 1) + exp_recur(n - 1) + 1 + end + ``` + +=== "Zig" + + ```zig title="time_complexity.zig" + // 指數階(遞迴實現) + fn expRecur(n: i32) i32 { + if (n == 1) return 1; + return expRecur(n - 1) + expRecur(n - 1) + 1; + } + ``` + +??? pythontutor "視覺化執行" + +
+
全螢幕觀看 >
+ +指數階增長非常迅速,在窮舉法(暴力搜尋、回溯等)中比較常見。對於資料規模較大的問題,指數階是不可接受的,通常需要使用動態規劃或貪婪演算法等來解決。 + +### 5.   對數階 $O(\log n)$ {data-toc-label="5.   對數階"} + +與指數階相反,對數階反映了“每輪縮減到一半”的情況。設輸入資料大小為 $n$ ,由於每輪縮減到一半,因此迴圈次數是 $\log_2 n$ ,即 $2^n$ 的反函式。 + +圖 2-12 和以下程式碼模擬了“每輪縮減到一半”的過程,時間複雜度為 $O(\log_2 n)$ ,簡記為 $O(\log n)$ : + +=== "Python" + + ```python title="time_complexity.py" + def logarithmic(n: int) -> int: + """對數階(迴圈實現)""" + count = 0 + while n > 1: + n = n / 2 + count += 1 + return count + ``` + +=== "C++" + + ```cpp title="time_complexity.cpp" + /* 對數階(迴圈實現) */ + int logarithmic(int n) { + int count = 0; + while (n > 1) { + n = n / 2; + count++; + } + return count; + } + ``` + +=== "Java" + + ```java title="time_complexity.java" + /* 對數階(迴圈實現) */ + int logarithmic(int n) { + int count = 0; + while (n > 1) { + n = n / 2; + count++; + } + return count; + } + ``` + +=== "C#" + + ```csharp title="time_complexity.cs" + /* 對數階(迴圈實現) */ + int Logarithmic(int n) { + int count = 0; + while (n > 1) { + n /= 2; + count++; + } + return count; + } + ``` + +=== "Go" + + ```go title="time_complexity.go" + /* 對數階(迴圈實現)*/ + func logarithmic(n int) int { + count := 0 + for n > 1 { + n = n / 2 + count++ + } + return count + } + ``` + +=== "Swift" + + ```swift title="time_complexity.swift" + /* 對數階(迴圈實現) */ + func logarithmic(n: Int) -> Int { + var count = 0 + var n = n + while n > 1 { + n = n / 2 + count += 1 + } + return count + } + ``` + +=== "JS" + + ```javascript title="time_complexity.js" + /* 對數階(迴圈實現) */ + function logarithmic(n) { + let count = 0; + while (n > 1) { + n = n / 2; + count++; + } + return count; + } + ``` + +=== "TS" + + ```typescript title="time_complexity.ts" + /* 對數階(迴圈實現) */ + function logarithmic(n: number): number { + let count = 0; + while (n > 1) { + n = n / 2; + count++; + } + return count; + } + ``` + +=== "Dart" + + ```dart title="time_complexity.dart" + /* 對數階(迴圈實現) */ + int logarithmic(int n) { + int count = 0; + while (n > 1) { + n = n ~/ 2; + count++; + } + return count; + } + ``` + +=== "Rust" + + ```rust title="time_complexity.rs" + /* 對數階(迴圈實現) */ + fn logarithmic(mut n: i32) -> i32 { + let mut count = 0; + while n > 1 { + n = n / 2; + count += 1; + } + count + } + ``` + +=== "C" + + ```c title="time_complexity.c" + /* 對數階(迴圈實現) */ + int logarithmic(int n) { + int count = 0; + while (n > 1) { + n = n / 2; + count++; + } + return count; + } + ``` + +=== "Kotlin" + + ```kotlin title="time_complexity.kt" + /* 對數階(迴圈實現) */ + fun logarithmic(n: Int): Int { + var n1 = n + var count = 0 + while (n1 > 1) { + n1 /= 2 + count++ + } + return count + } + ``` + +=== "Ruby" + + ```ruby title="time_complexity.rb" + ### 對數階(迴圈實現)### + def logarithmic(n) + count = 0 + + while n > 1 + n /= 2 + count += 1 + end + + count + end + ``` + +=== "Zig" + + ```zig title="time_complexity.zig" + // 對數階(迴圈實現) + fn logarithmic(n: i32) i32 { + var count: i32 = 0; + var n_var = n; + while (n_var > 1) + { + n_var = n_var / 2; + count +=1; + } + return count; + } + ``` + +??? pythontutor "視覺化執行" + +
+
全螢幕觀看 >
+ +![對數階的時間複雜度](time_complexity.assets/time_complexity_logarithmic.png){ class="animation-figure" } + +

圖 2-12   對數階的時間複雜度

+ +與指數階類似,對數階也常出現於遞迴函式中。以下程式碼形成了一棵高度為 $\log_2 n$ 的遞迴樹: + +=== "Python" + + ```python title="time_complexity.py" + def log_recur(n: int) -> int: + """對數階(遞迴實現)""" + if n <= 1: + return 0 + return log_recur(n / 2) + 1 + ``` + +=== "C++" + + ```cpp title="time_complexity.cpp" + /* 對數階(遞迴實現) */ + int logRecur(int n) { + if (n <= 1) + return 0; + return logRecur(n / 2) + 1; + } + ``` + +=== "Java" + + ```java title="time_complexity.java" + /* 對數階(遞迴實現) */ + int logRecur(int n) { + if (n <= 1) + return 0; + return logRecur(n / 2) + 1; + } + ``` + +=== "C#" + + ```csharp title="time_complexity.cs" + /* 對數階(遞迴實現) */ + int LogRecur(int n) { + if (n <= 1) return 0; + return LogRecur(n / 2) + 1; + } + ``` + +=== "Go" + + ```go title="time_complexity.go" + /* 對數階(遞迴實現)*/ + func logRecur(n int) int { + if n <= 1 { + return 0 + } + return logRecur(n/2) + 1 + } + ``` + +=== "Swift" + + ```swift title="time_complexity.swift" + /* 對數階(遞迴實現) */ + func logRecur(n: Int) -> Int { + if n <= 1 { + return 0 + } + return logRecur(n: n / 2) + 1 + } + ``` + +=== "JS" + + ```javascript title="time_complexity.js" + /* 對數階(遞迴實現) */ + function logRecur(n) { + if (n <= 1) return 0; + return logRecur(n / 2) + 1; + } + ``` + +=== "TS" + + ```typescript title="time_complexity.ts" + /* 對數階(遞迴實現) */ + function logRecur(n: number): number { + if (n <= 1) return 0; + return logRecur(n / 2) + 1; + } + ``` + +=== "Dart" + + ```dart title="time_complexity.dart" + /* 對數階(遞迴實現) */ + int logRecur(int n) { + if (n <= 1) return 0; + return logRecur(n ~/ 2) + 1; + } + ``` + +=== "Rust" + + ```rust title="time_complexity.rs" + /* 對數階(遞迴實現) */ + fn log_recur(n: i32) -> i32 { + if n <= 1 { + return 0; + } + log_recur(n / 2) + 1 + } + ``` + +=== "C" + + ```c title="time_complexity.c" + /* 對數階(遞迴實現) */ + int logRecur(int n) { + if (n <= 1) + return 0; + return logRecur(n / 2) + 1; + } + ``` + +=== "Kotlin" + + ```kotlin title="time_complexity.kt" + /* 對數階(遞迴實現) */ + fun logRecur(n: Int): Int { + if (n <= 1) + return 0 + return logRecur(n / 2) + 1 + } + ``` + +=== "Ruby" + + ```ruby title="time_complexity.rb" + ### 對數階(遞迴實現)### + def log_recur(n) + return 0 unless n > 1 + log_recur(n / 2) + 1 + end + ``` + +=== "Zig" + + ```zig title="time_complexity.zig" + // 對數階(遞迴實現) + fn logRecur(n: i32) i32 { + if (n <= 1) return 0; + return logRecur(n / 2) + 1; + } + ``` + +??? pythontutor "視覺化執行" + +
+
全螢幕觀看 >
+ +對數階常出現於基於分治策略的演算法中,體現了“一分為多”和“化繁為簡”的演算法思想。它增長緩慢,是僅次於常數階的理想的時間複雜度。 + +!!! tip "$O(\log n)$ 的底數是多少?" + + 準確來說,“一分為 $m$”對應的時間複雜度是 $O(\log_m n)$ 。而透過對數換底公式,我們可以得到具有不同底數、相等的時間複雜度: + + $$ + O(\log_m n) = O(\log_k n / \log_k m) = O(\log_k n) + $$ + + 也就是說,底數 $m$ 可以在不影響複雜度的前提下轉換。因此我們通常會省略底數 $m$ ,將對數階直接記為 $O(\log n)$ 。 + +### 6.   線性對數階 $O(n \log n)$ {data-toc-label="6.   線性對數階"} + +線性對數階常出現於巢狀迴圈中,兩層迴圈的時間複雜度分別為 $O(\log n)$ 和 $O(n)$ 。相關程式碼如下: + +=== "Python" + + ```python title="time_complexity.py" + def linear_log_recur(n: int) -> int: + """線性對數階""" + if n <= 1: + return 1 + count: int = linear_log_recur(n // 2) + linear_log_recur(n // 2) + for _ in range(n): + count += 1 + return count + ``` + +=== "C++" + + ```cpp title="time_complexity.cpp" + /* 線性對數階 */ + int linearLogRecur(int n) { + if (n <= 1) + return 1; + int count = linearLogRecur(n / 2) + linearLogRecur(n / 2); + for (int i = 0; i < n; i++) { + count++; + } + return count; + } + ``` + +=== "Java" + + ```java title="time_complexity.java" + /* 線性對數階 */ + int linearLogRecur(int n) { + if (n <= 1) + return 1; + int count = linearLogRecur(n / 2) + linearLogRecur(n / 2); + for (int i = 0; i < n; i++) { + count++; + } + return count; + } + ``` + +=== "C#" + + ```csharp title="time_complexity.cs" + /* 線性對數階 */ + int LinearLogRecur(int n) { + if (n <= 1) return 1; + int count = LinearLogRecur(n / 2) + LinearLogRecur(n / 2); + for (int i = 0; i < n; i++) { + count++; + } + return count; + } + ``` + +=== "Go" + + ```go title="time_complexity.go" + /* 線性對數階 */ + func linearLogRecur(n int) int { + if n <= 1 { + return 1 + } + count := linearLogRecur(n/2) + linearLogRecur(n/2) + for i := 0; i < n; i++ { + count++ + } + return count + } + ``` + +=== "Swift" + + ```swift title="time_complexity.swift" + /* 線性對數階 */ + func linearLogRecur(n: Int) -> Int { + if n <= 1 { + return 1 + } + var count = linearLogRecur(n: n / 2) + linearLogRecur(n: n / 2) + for _ in stride(from: 0, to: n, by: 1) { + count += 1 + } + return count + } + ``` + +=== "JS" + + ```javascript title="time_complexity.js" + /* 線性對數階 */ + function linearLogRecur(n) { + if (n <= 1) return 1; + let count = linearLogRecur(n / 2) + linearLogRecur(n / 2); + for (let i = 0; i < n; i++) { + count++; + } + return count; + } + ``` + +=== "TS" + + ```typescript title="time_complexity.ts" + /* 線性對數階 */ + function linearLogRecur(n: number): number { + if (n <= 1) return 1; + let count = linearLogRecur(n / 2) + linearLogRecur(n / 2); + for (let i = 0; i < n; i++) { + count++; + } + return count; + } + ``` + +=== "Dart" + + ```dart title="time_complexity.dart" + /* 線性對數階 */ + int linearLogRecur(int n) { + if (n <= 1) return 1; + int count = linearLogRecur(n ~/ 2) + linearLogRecur(n ~/ 2); + for (var i = 0; i < n; i++) { + count++; + } + return count; + } + ``` + +=== "Rust" + + ```rust title="time_complexity.rs" + /* 線性對數階 */ + fn linear_log_recur(n: i32) -> i32 { + if n <= 1 { + return 1; + } + let mut count = linear_log_recur(n / 2) + linear_log_recur(n / 2); + for _ in 0..n as i32 { + count += 1; + } + return count; + } + ``` + +=== "C" + + ```c title="time_complexity.c" + /* 線性對數階 */ + int linearLogRecur(int n) { + if (n <= 1) + return 1; + int count = linearLogRecur(n / 2) + linearLogRecur(n / 2); + for (int i = 0; i < n; i++) { + count++; + } + return count; + } + ``` + +=== "Kotlin" + + ```kotlin title="time_complexity.kt" + /* 線性對數階 */ + fun linearLogRecur(n: Int): Int { + if (n <= 1) + return 1 + var count = linearLogRecur(n / 2) + linearLogRecur(n / 2) + for (i in 0.. 1 + + count = linear_log_recur(n / 2) + linear_log_recur(n / 2) + (0...n).each { count += 1 } + + count + end + ``` + +=== "Zig" + + ```zig title="time_complexity.zig" + // 線性對數階 + fn linearLogRecur(n: i32) i32 { + if (n <= 1) return 1; + var count: i32 = linearLogRecur(n / 2) + linearLogRecur(n / 2); + var i: i32 = 0; + while (i < n) : (i += 1) { + count += 1; + } + return count; + } + ``` + +??? pythontutor "視覺化執行" + +
+
全螢幕觀看 >
+ +圖 2-13 展示了線性對數階的生成方式。二元樹的每一層的操作總數都為 $n$ ,樹共有 $\log_2 n + 1$ 層,因此時間複雜度為 $O(n \log n)$ 。 + +![線性對數階的時間複雜度](time_complexity.assets/time_complexity_logarithmic_linear.png){ class="animation-figure" } + +

圖 2-13   線性對數階的時間複雜度

+ +主流排序演算法的時間複雜度通常為 $O(n \log n)$ ,例如快速排序、合併排序、堆積排序等。 + +### 7.   階乘階 $O(n!)$ {data-toc-label="7.   階乘階"} + +階乘階對應數學上的“全排列”問題。給定 $n$ 個互不重複的元素,求其所有可能的排列方案,方案數量為: + +$$ +n! = n \times (n - 1) \times (n - 2) \times \dots \times 2 \times 1 +$$ + +階乘通常使用遞迴實現。如圖 2-14 和以下程式碼所示,第一層分裂出 $n$ 個,第二層分裂出 $n - 1$ 個,以此類推,直至第 $n$ 層時停止分裂: + +=== "Python" + + ```python title="time_complexity.py" + def factorial_recur(n: int) -> int: + """階乘階(遞迴實現)""" + if n == 0: + return 1 + count = 0 + # 從 1 個分裂出 n 個 + for _ in range(n): + count += factorial_recur(n - 1) + return count + ``` + +=== "C++" + + ```cpp title="time_complexity.cpp" + /* 階乘階(遞迴實現) */ + int factorialRecur(int n) { + if (n == 0) + return 1; + int count = 0; + // 從 1 個分裂出 n 個 + for (int i = 0; i < n; i++) { + count += factorialRecur(n - 1); + } + return count; + } + ``` + +=== "Java" + + ```java title="time_complexity.java" + /* 階乘階(遞迴實現) */ + int factorialRecur(int n) { + if (n == 0) + return 1; + int count = 0; + // 從 1 個分裂出 n 個 + for (int i = 0; i < n; i++) { + count += factorialRecur(n - 1); + } + return count; + } + ``` + +=== "C#" + + ```csharp title="time_complexity.cs" + /* 階乘階(遞迴實現) */ + int FactorialRecur(int n) { + if (n == 0) return 1; + int count = 0; + // 從 1 個分裂出 n 個 + for (int i = 0; i < n; i++) { + count += FactorialRecur(n - 1); + } + return count; + } + ``` + +=== "Go" + + ```go title="time_complexity.go" + /* 階乘階(遞迴實現) */ + func factorialRecur(n int) int { + if n == 0 { + return 1 + } + count := 0 + // 從 1 個分裂出 n 個 + for i := 0; i < n; i++ { + count += factorialRecur(n - 1) + } + return count + } + ``` + +=== "Swift" + + ```swift title="time_complexity.swift" + /* 階乘階(遞迴實現) */ + func factorialRecur(n: Int) -> Int { + if n == 0 { + return 1 + } + var count = 0 + // 從 1 個分裂出 n 個 + for _ in 0 ..< n { + count += factorialRecur(n: n - 1) + } + return count + } + ``` + +=== "JS" + + ```javascript title="time_complexity.js" + /* 階乘階(遞迴實現) */ + function factorialRecur(n) { + if (n === 0) return 1; + let count = 0; + // 從 1 個分裂出 n 個 + for (let i = 0; i < n; i++) { + count += factorialRecur(n - 1); + } + return count; + } + ``` + +=== "TS" + + ```typescript title="time_complexity.ts" + /* 階乘階(遞迴實現) */ + function factorialRecur(n: number): number { + if (n === 0) return 1; + let count = 0; + // 從 1 個分裂出 n 個 + for (let i = 0; i < n; i++) { + count += factorialRecur(n - 1); + } + return count; + } + ``` + +=== "Dart" + + ```dart title="time_complexity.dart" + /* 階乘階(遞迴實現) */ + int factorialRecur(int n) { + if (n == 0) return 1; + int count = 0; + // 從 1 個分裂出 n 個 + for (var i = 0; i < n; i++) { + count += factorialRecur(n - 1); + } + return count; + } + ``` + +=== "Rust" + + ```rust title="time_complexity.rs" + /* 階乘階(遞迴實現) */ + fn factorial_recur(n: i32) -> i32 { + if n == 0 { + return 1; + } + let mut count = 0; + // 從 1 個分裂出 n 個 + for _ in 0..n { + count += factorial_recur(n - 1); + } + count + } + ``` + +=== "C" + + ```c title="time_complexity.c" + /* 階乘階(遞迴實現) */ + int factorialRecur(int n) { + if (n == 0) + return 1; + int count = 0; + for (int i = 0; i < n; i++) { + count += factorialRecur(n - 1); + } + return count; + } + ``` + +=== "Kotlin" + + ```kotlin title="time_complexity.kt" + /* 階乘階(遞迴實現) */ + fun factorialRecur(n: Int): Int { + if (n == 0) + return 1 + var count = 0 + // 從 1 個分裂出 n 個 + for (i in 0..
+
全螢幕觀看 >
+ +![階乘階的時間複雜度](time_complexity.assets/time_complexity_factorial.png){ class="animation-figure" } + +

圖 2-14   階乘階的時間複雜度

+ +請注意,因為當 $n \geq 4$ 時恆有 $n! > 2^n$ ,所以階乘階比指數階增長得更快,在 $n$ 較大時也是不可接受的。 + +## 2.3.5   最差、最佳、平均時間複雜度 + +**演算法的時間效率往往不是固定的,而是與輸入資料的分佈有關**。假設輸入一個長度為 $n$ 的陣列 `nums` ,其中 `nums` 由從 $1$ 至 $n$ 的數字組成,每個數字只出現一次;但元素順序是隨機打亂的,任務目標是返回元素 $1$ 的索引。我們可以得出以下結論。 + +- 當 `nums = [?, ?, ..., 1]` ,即當末尾元素是 $1$ 時,需要完整走訪陣列,**達到最差時間複雜度 $O(n)$** 。 +- 當 `nums = [1, ?, ?, ...]` ,即當首個元素為 $1$ 時,無論陣列多長都不需要繼續走訪,**達到最佳時間複雜度 $\Omega(1)$** 。 + +“最差時間複雜度”對應函式漸近上界,使用大 $O$ 記號表示。相應地,“最佳時間複雜度”對應函式漸近下界,用 $\Omega$ 記號表示: + +=== "Python" + + ```python title="worst_best_time_complexity.py" + def random_numbers(n: int) -> list[int]: + """生成一個陣列,元素為: 1, 2, ..., n ,順序被打亂""" + # 生成陣列 nums =: 1, 2, 3, ..., n + nums = [i for i in range(1, n + 1)] + # 隨機打亂陣列元素 + random.shuffle(nums) + return nums + + def find_one(nums: list[int]) -> int: + """查詢陣列 nums 中數字 1 所在索引""" + for i in range(len(nums)): + # 當元素 1 在陣列頭部時,達到最佳時間複雜度 O(1) + # 當元素 1 在陣列尾部時,達到最差時間複雜度 O(n) + if nums[i] == 1: + return i + return -1 + ``` + +=== "C++" + + ```cpp title="worst_best_time_complexity.cpp" + /* 生成一個陣列,元素為 { 1, 2, ..., n },順序被打亂 */ + vector randomNumbers(int n) { + vector nums(n); + // 生成陣列 nums = { 1, 2, 3, ..., n } + for (int i = 0; i < n; i++) { + nums[i] = i + 1; + } + // 使用系統時間生成隨機種子 + unsigned seed = chrono::system_clock::now().time_since_epoch().count(); + // 隨機打亂陣列元素 + shuffle(nums.begin(), nums.end(), default_random_engine(seed)); + return nums; + } + + /* 查詢陣列 nums 中數字 1 所在索引 */ + int findOne(vector &nums) { + for (int i = 0; i < nums.size(); i++) { + // 當元素 1 在陣列頭部時,達到最佳時間複雜度 O(1) + // 當元素 1 在陣列尾部時,達到最差時間複雜度 O(n) + if (nums[i] == 1) + return i; + } + return -1; + } + ``` + +=== "Java" + + ```java title="worst_best_time_complexity.java" + /* 生成一個陣列,元素為 { 1, 2, ..., n },順序被打亂 */ + int[] randomNumbers(int n) { + Integer[] nums = new Integer[n]; + // 生成陣列 nums = { 1, 2, 3, ..., n } + for (int i = 0; i < n; i++) { + nums[i] = i + 1; + } + // 隨機打亂陣列元素 + Collections.shuffle(Arrays.asList(nums)); + // Integer[] -> int[] + int[] res = new int[n]; + for (int i = 0; i < n; i++) { + res[i] = nums[i]; + } + return res; + } + + /* 查詢陣列 nums 中數字 1 所在索引 */ + int findOne(int[] nums) { + for (int i = 0; i < nums.length; i++) { + // 當元素 1 在陣列頭部時,達到最佳時間複雜度 O(1) + // 當元素 1 在陣列尾部時,達到最差時間複雜度 O(n) + if (nums[i] == 1) + return i; + } + return -1; + } + ``` + +=== "C#" + + ```csharp title="worst_best_time_complexity.cs" + /* 生成一個陣列,元素為 { 1, 2, ..., n },順序被打亂 */ + int[] RandomNumbers(int n) { + int[] nums = new int[n]; + // 生成陣列 nums = { 1, 2, 3, ..., n } + for (int i = 0; i < n; i++) { + nums[i] = i + 1; + } + + // 隨機打亂陣列元素 + for (int i = 0; i < nums.Length; i++) { + int index = new Random().Next(i, nums.Length); + (nums[i], nums[index]) = (nums[index], nums[i]); + } + return nums; + } + + /* 查詢陣列 nums 中數字 1 所在索引 */ + int FindOne(int[] nums) { + for (int i = 0; i < nums.Length; i++) { + // 當元素 1 在陣列頭部時,達到最佳時間複雜度 O(1) + // 當元素 1 在陣列尾部時,達到最差時間複雜度 O(n) + if (nums[i] == 1) + return i; + } + return -1; + } + ``` + +=== "Go" + + ```go title="worst_best_time_complexity.go" + /* 生成一個陣列,元素為 { 1, 2, ..., n },順序被打亂 */ + func randomNumbers(n int) []int { + nums := make([]int, n) + // 生成陣列 nums = { 1, 2, 3, ..., n } + for i := 0; i < n; i++ { + nums[i] = i + 1 + } + // 隨機打亂陣列元素 + rand.Shuffle(len(nums), func(i, j int) { + nums[i], nums[j] = nums[j], nums[i] + }) + return nums + } + + /* 查詢陣列 nums 中數字 1 所在索引 */ + func findOne(nums []int) int { + for i := 0; i < len(nums); i++ { + // 當元素 1 在陣列頭部時,達到最佳時間複雜度 O(1) + // 當元素 1 在陣列尾部時,達到最差時間複雜度 O(n) + if nums[i] == 1 { + return i + } + } + return -1 + } + ``` + +=== "Swift" + + ```swift title="worst_best_time_complexity.swift" + /* 生成一個陣列,元素為 { 1, 2, ..., n },順序被打亂 */ + func randomNumbers(n: Int) -> [Int] { + // 生成陣列 nums = { 1, 2, 3, ..., n } + var nums = Array(1 ... n) + // 隨機打亂陣列元素 + nums.shuffle() + return nums + } + + /* 查詢陣列 nums 中數字 1 所在索引 */ + func findOne(nums: [Int]) -> Int { + for i in nums.indices { + // 當元素 1 在陣列頭部時,達到最佳時間複雜度 O(1) + // 當元素 1 在陣列尾部時,達到最差時間複雜度 O(n) + if nums[i] == 1 { + return i + } + } + return -1 + } + ``` + +=== "JS" + + ```javascript title="worst_best_time_complexity.js" + /* 生成一個陣列,元素為 { 1, 2, ..., n },順序被打亂 */ + function randomNumbers(n) { + const nums = Array(n); + // 生成陣列 nums = { 1, 2, 3, ..., n } + for (let i = 0; i < n; i++) { + nums[i] = i + 1; + } + // 隨機打亂陣列元素 + for (let i = 0; i < n; i++) { + const r = Math.floor(Math.random() * (i + 1)); + const temp = nums[i]; + nums[i] = nums[r]; + nums[r] = temp; + } + return nums; + } + + /* 查詢陣列 nums 中數字 1 所在索引 */ + function findOne(nums) { + for (let i = 0; i < nums.length; i++) { + // 當元素 1 在陣列頭部時,達到最佳時間複雜度 O(1) + // 當元素 1 在陣列尾部時,達到最差時間複雜度 O(n) + if (nums[i] === 1) { + return i; + } + } + return -1; + } + ``` + +=== "TS" + + ```typescript title="worst_best_time_complexity.ts" + /* 生成一個陣列,元素為 { 1, 2, ..., n },順序被打亂 */ + function randomNumbers(n: number): number[] { + const nums = Array(n); + // 生成陣列 nums = { 1, 2, 3, ..., n } + for (let i = 0; i < n; i++) { + nums[i] = i + 1; + } + // 隨機打亂陣列元素 + for (let i = 0; i < n; i++) { + const r = Math.floor(Math.random() * (i + 1)); + const temp = nums[i]; + nums[i] = nums[r]; + nums[r] = temp; + } + return nums; + } + + /* 查詢陣列 nums 中數字 1 所在索引 */ + function findOne(nums: number[]): number { + for (let i = 0; i < nums.length; i++) { + // 當元素 1 在陣列頭部時,達到最佳時間複雜度 O(1) + // 當元素 1 在陣列尾部時,達到最差時間複雜度 O(n) + if (nums[i] === 1) { + return i; + } + } + return -1; + } + ``` + +=== "Dart" + + ```dart title="worst_best_time_complexity.dart" + /* 生成一個陣列,元素為 { 1, 2, ..., n },順序被打亂 */ + List randomNumbers(int n) { + final nums = List.filled(n, 0); + // 生成陣列 nums = { 1, 2, 3, ..., n } + for (var i = 0; i < n; i++) { + nums[i] = i + 1; + } + // 隨機打亂陣列元素 + nums.shuffle(); + + return nums; + } + + /* 查詢陣列 nums 中數字 1 所在索引 */ + int findOne(List nums) { + for (var i = 0; i < nums.length; i++) { + // 當元素 1 在陣列頭部時,達到最佳時間複雜度 O(1) + // 當元素 1 在陣列尾部時,達到最差時間複雜度 O(n) + if (nums[i] == 1) return i; + } + + return -1; + } + ``` + +=== "Rust" + + ```rust title="worst_best_time_complexity.rs" + /* 生成一個陣列,元素為 { 1, 2, ..., n },順序被打亂 */ + fn random_numbers(n: i32) -> Vec { + // 生成陣列 nums = { 1, 2, 3, ..., n } + let mut nums = (1..=n).collect::>(); + // 隨機打亂陣列元素 + nums.shuffle(&mut thread_rng()); + nums + } + + /* 查詢陣列 nums 中數字 1 所在索引 */ + fn find_one(nums: &[i32]) -> Option { + for i in 0..nums.len() { + // 當元素 1 在陣列頭部時,達到最佳時間複雜度 O(1) + // 當元素 1 在陣列尾部時,達到最差時間複雜度 O(n) + if nums[i] == 1 { + return Some(i); + } + } + None + } + ``` + +=== "C" + + ```c title="worst_best_time_complexity.c" + /* 生成一個陣列,元素為 { 1, 2, ..., n },順序被打亂 */ + int *randomNumbers(int n) { + // 分配堆積區記憶體(建立一維可變長陣列:陣列中元素數量為 n ,元素型別為 int ) + int *nums = (int *)malloc(n * sizeof(int)); + // 生成陣列 nums = { 1, 2, 3, ..., n } + for (int i = 0; i < n; i++) { + nums[i] = i + 1; + } + // 隨機打亂陣列元素 + for (int i = n - 1; i > 0; i--) { + int j = rand() % (i + 1); + int temp = nums[i]; + nums[i] = nums[j]; + nums[j] = temp; + } + return nums; + } + + /* 查詢陣列 nums 中數字 1 所在索引 */ + int findOne(int *nums, int n) { + for (int i = 0; i < n; i++) { + // 當元素 1 在陣列頭部時,達到最佳時間複雜度 O(1) + // 當元素 1 在陣列尾部時,達到最差時間複雜度 O(n) + if (nums[i] == 1) + return i; + } + return -1; + } + ``` + +=== "Kotlin" + + ```kotlin title="worst_best_time_complexity.kt" + /* 生成一個陣列,元素為 { 1, 2, ..., n },順序被打亂 */ + fun randomNumbers(n: Int): Array { + val nums = IntArray(n) + // 生成陣列 nums = { 1, 2, 3, ..., n } + for (i in 0.. int[] + val res = arrayOfNulls(n) + for (i in 0..): Int { + for (i in nums.indices) { + // 當元素 1 在陣列頭部時,達到最佳時間複雜度 O(1) + // 當元素 1 在陣列尾部時,達到最差時間複雜度 O(n) + if (nums[i] == 1) + return i + } + return -1 + } + ``` + +=== "Ruby" + + ```ruby title="worst_best_time_complexity.rb" + ### 生成一個陣列,元素為: 1, 2, ..., n ,順序被打亂 ### + def random_numbers(n) + # 生成陣列 nums =: 1, 2, 3, ..., n + nums = Array.new(n) { |i| i + 1 } + # 隨機打亂陣列元素 + nums.shuffle! + end + + ### 查詢陣列 nums 中數字 1 所在索引 ### + def find_one(nums) + for i in 0...nums.length + # 當元素 1 在陣列頭部時,達到最佳時間複雜度 O(1) + # 當元素 1 在陣列尾部時,達到最差時間複雜度 O(n) + return i if nums[i] == 1 + end + + -1 + end + ``` + +=== "Zig" + + ```zig title="worst_best_time_complexity.zig" + // 生成一個陣列,元素為 { 1, 2, ..., n },順序被打亂 + fn randomNumbers(comptime n: usize) [n]i32 { + var nums: [n]i32 = undefined; + // 生成陣列 nums = { 1, 2, 3, ..., n } + for (&nums, 0..) |*num, i| { + num.* = @as(i32, @intCast(i)) + 1; + } + // 隨機打亂陣列元素 + const rand = std.crypto.random; + rand.shuffle(i32, &nums); + return nums; + } + + // 查詢陣列 nums 中數字 1 所在索引 + fn findOne(nums: []i32) i32 { + for (nums, 0..) |num, i| { + // 當元素 1 在陣列頭部時,達到最佳時間複雜度 O(1) + // 當元素 1 在陣列尾部時,達到最差時間複雜度 O(n) + if (num == 1) return @intCast(i); + } + return -1; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +值得說明的是,我們在實際中很少使用最佳時間複雜度,因為通常只有在很小機率下才能達到,可能會帶來一定的誤導性。**而最差時間複雜度更為實用,因為它給出了一個效率安全值**,讓我們可以放心地使用演算法。 + +從上述示例可以看出,最差時間複雜度和最佳時間複雜度只出現於“特殊的資料分佈”,這些情況的出現機率可能很小,並不能真實地反映演算法執行效率。相比之下,**平均時間複雜度可以體現演算法在隨機輸入資料下的執行效率**,用 $\Theta$ 記號來表示。 + +對於部分演算法,我們可以簡單地推算出隨機資料分佈下的平均情況。比如上述示例,由於輸入陣列是被打亂的,因此元素 $1$ 出現在任意索引的機率都是相等的,那麼演算法的平均迴圈次數就是陣列長度的一半 $n / 2$ ,平均時間複雜度為 $\Theta(n / 2) = \Theta(n)$ 。 + +但對於較為複雜的演算法,計算平均時間複雜度往往比較困難,因為很難分析出在資料分佈下的整體數學期望。在這種情況下,我們通常使用最差時間複雜度作為演算法效率的評判標準。 + +!!! question "為什麼很少看到 $\Theta$ 符號?" + + 可能由於 $O$ 符號過於朗朗上口,因此我們常常使用它來表示平均時間複雜度。但從嚴格意義上講,這種做法並不規範。在本書和其他資料中,若遇到類似“平均時間複雜度 $O(n)$”的表述,請將其直接理解為 $\Theta(n)$ 。 diff --git a/zh-Hant/docs/chapter_data_structure/basic_data_types.md b/zh-Hant/docs/chapter_data_structure/basic_data_types.md new file mode 100644 index 000000000..70e34f432 --- /dev/null +++ b/zh-Hant/docs/chapter_data_structure/basic_data_types.md @@ -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 中各種基本資料型別的佔用空間、取值範圍和預設值。此表格無須死記硬背,大致理解即可,需要時可以透過查表來回憶。 + +

表 3-1   基本資料型別的佔用空間和取值範圍

+ +
+ +| 型別 | 符號 | 佔用空間 | 最小值 | 最大值 | 預設值 | +| ------ | -------- | -------- | ------------------------ | ----------------------- | -------------- | +| 整數 | `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}$ | + +
+ +請注意,表 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 numbers = List.filled(5, 0); + List decimals = List.filled(5, 0.0); + List characters = List.filled(5, 'a'); + List bools = List.filled(5, false); + ``` + +=== "Rust" + + ```rust title="" + // 使用多種基本資料型別來初始化陣列 + let numbers: Vec = vec![0; 5]; + let decimals: Vec = vec![0.0; 5]; + let characters: Vec = vec!['0'; 5]; + let bools: Vec = 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 "視覺化執行" + +
+ diff --git a/zh-Hant/docs/chapter_data_structure/character_encoding.md b/zh-Hant/docs/chapter_data_structure/character_encoding.md new file mode 100644 index 000000000..f60c2a5ea --- /dev/null +++ b/zh-Hant/docs/chapter_data_structure/character_encoding.md @@ -0,0 +1,97 @@ +--- +comments: true +--- + +# 3.4   字元編碼 * + +在計算機中,所有資料都是以二進位制數的形式儲存的,字元 `char` 也不例外。為了表示字元,我們需要建立一套“字符集”,規定每個字元和二進位制數之間的一一對應關係。有了字符集之後,計算機就可以透過查表完成二進位制數到字元的轉換。 + +## 3.4.1   ASCII 字符集 + +ASCII 碼是最早出現的字符集,其全稱為 American Standard Code for Information Interchange(美國標準資訊交換程式碼)。它使用 7 位二進位制數(一個位元組的低 7 位)表示一個字元,最多能夠表示 128 個不同的字元。如圖 3-6 所示,ASCII 碼包括英文字母的大小寫、數字 0 ~ 9、一些標點符號,以及一些控制字元(如換行符和製表符)。 + +![ASCII 碼](character_encoding.assets/ascii_table.png){ class="animation-figure" } + +

圖 3-6   ASCII 碼

+ +然而,**ASCII 碼僅能夠表示英文**。隨著計算機的全球化,誕生了一種能夠表示更多語言的 EASCII 字符集。它在 ASCII 的 7 位基礎上擴展到 8 位,能夠表示 256 個不同的字元。 + +在世界範圍內,陸續出現了一批適用於不同地區的 EASCII 字符集。這些字符集的前 128 個字元統一為 ASCII 碼,後 128 個字元定義不同,以適應不同語言的需求。 + +## 3.4.2   GBK 字符集 + +後來人們發現,**EASCII 碼仍然無法滿足許多語言的字元數量要求**。比如漢字有近十萬個,光日常使用的就有幾千個。中國國家標準總局於 1980 年釋出了 GB2312 字符集,其收錄了 6763 個漢字,基本滿足了漢字的計算機處理需要。 + +然而,GB2312 無法處理部分罕見字和繁體字。GBK 字符集是在 GB2312 的基礎上擴展得到的,它共收錄了 21886 個漢字。在 GBK 的編碼方案中,ASCII 字元使用一個位元組表示,漢字使用兩個位元組表示。 + +## 3.4.3   Unicode 字符集 + +隨著計算機技術的蓬勃發展,字符集與編碼標準百花齊放,而這帶來了許多問題。一方面,這些字符集一般只定義了特定語言的字元,無法在多語言環境下正常工作。另一方面,同一種語言存在多種字符集標準,如果兩臺計算機使用的是不同的編碼標準,則在資訊傳遞時就會出現亂碼。 + +那個時代的研究人員就在想:**如果推出一個足夠完整的字符集,將世界範圍內的所有語言和符號都收錄其中,不就可以解決跨語言環境和亂碼問題了嗎**?在這種想法的驅動下,一個大而全的字符集 Unicode 應運而生。 + +Unicode 的中文名稱為“統一碼”,理論上能容納 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 位元組解析一個字元,恢復這個短語的內容了。 + +![Unicode 編碼示例](character_encoding.assets/unicode_hello_algo.png){ class="animation-figure" } + +

圖 3-7   Unicode 編碼示例

+ +然而 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$ ,與假設矛盾。 + +![UTF-8 編碼示例](character_encoding.assets/utf-8_hello_algo.png){ class="animation-figure" } + +

圖 3-8   UTF-8 編碼示例

+ +除了 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 格式,以達到最優的相容性和空間效率。 diff --git a/zh-Hant/docs/chapter_data_structure/classification_of_data_structure.md b/zh-Hant/docs/chapter_data_structure/classification_of_data_structure.md new file mode 100644 index 000000000..ce5a93810 --- /dev/null +++ b/zh-Hant/docs/chapter_data_structure/classification_of_data_structure.md @@ -0,0 +1,58 @@ +--- +comments: true +--- + +# 3.1   資料結構分類 + +常見的資料結構包括陣列、鏈結串列、堆疊、佇列、雜湊表、樹、堆積、圖,它們可以從“邏輯結構”和“物理結構”兩個維度進行分類。 + +## 3.1.1   邏輯結構:線性與非線性 + +**邏輯結構揭示了資料元素之間的邏輯關係**。在陣列和鏈結串列中,資料按照一定順序排列,體現了資料之間的線性關係;而在樹中,資料從頂部向下按層次排列,表現出“祖先”與“後代”之間的派生關係;圖則由節點和邊構成,反映了複雜的網路關係。 + +如圖 3-1 所示,邏輯結構可分為“線性”和“非線性”兩大類。線性結構比較直觀,指資料在邏輯關係上呈線性排列;非線性結構則相反,呈非線性排列。 + +- **線性資料結構**:陣列、鏈結串列、堆疊、佇列、雜湊表,元素之間是一對一的順序關係。 +- **非線性資料結構**:樹、堆積、圖、雜湊表。 + +非線性資料結構可以進一步劃分為樹形結構和網狀結構。 + +- **樹形結構**:樹、堆積、雜湊表,元素之間是一對多的關係。 +- **網狀結構**:圖,元素之間是多對多的關係。 + +![線性資料結構與非線性資料結構](classification_of_data_structure.assets/classification_logic_structure.png){ class="animation-figure" } + +

圖 3-1   線性資料結構與非線性資料結構

+ +## 3.1.2   物理結構:連續與分散 + +**當演算法程式執行時,正在處理的資料主要儲存在記憶體中**。圖 3-2 展示了一個計算機記憶體條,其中每個黑色方塊都包含一塊記憶體空間。我們可以將記憶體想象成一個巨大的 Excel 表格,其中每個單元格都可以儲存一定大小的資料。 + +**系統透過記憶體位址來訪問目標位置的資料**。如圖 3-2 所示,計算機根據特定規則為表格中的每個單元格分配編號,確保每個記憶體空間都有唯一的記憶體位址。有了這些位址,程式便可以訪問記憶體中的資料。 + +![記憶體條、記憶體空間、記憶體位址](classification_of_data_structure.assets/computer_memory_location.png){ class="animation-figure" } + +

圖 3-2   記憶體條、記憶體空間、記憶體位址

+ +!!! tip + + 值得說明的是,將記憶體比作 Excel 表格是一個簡化的類比,實際記憶體的工作機制比較複雜,涉及位址空間、記憶體管理、快取機制、虛擬記憶體和物理記憶體等概念。 + +記憶體是所有程式的共享資源,當某塊記憶體被某個程式佔用時,則無法被其他程式同時使用了。**因此在資料結構與演算法的設計中,記憶體資源是一個重要的考慮因素**。比如,演算法所佔用的記憶體峰值不應超過系統剩餘空閒記憶體;如果缺少連續大塊的記憶體空間,那麼所選用的資料結構必須能夠儲存在分散的記憶體空間內。 + +如圖 3-3 所示,**物理結構反映了資料在計算機記憶體中的儲存方式**,可分為連續空間儲存(陣列)和分散空間儲存(鏈結串列)。物理結構從底層決定了資料的訪問、更新、增刪等操作方法,兩種物理結構在時間效率和空間效率方面呈現出互補的特點。 + +![連續空間儲存與分散空間儲存](classification_of_data_structure.assets/classification_phisical_structure.png){ class="animation-figure" } + +

圖 3-3   連續空間儲存與分散空間儲存

+ +值得說明的是,**所有資料結構都是基於陣列、鏈結串列或二者的組合實現的**。例如,堆疊和佇列既可以使用陣列實現,也可以使用鏈結串列實現;而雜湊表的實現可能同時包含陣列和鏈結串列。 + +- **基於陣列可實現**:堆疊、佇列、雜湊表、樹、堆積、圖、矩陣、張量(維度 $\geq 3$ 的陣列)等。 +- **基於鏈結串列可實現**:堆疊、佇列、雜湊表、樹、堆積、圖等。 + +鏈結串列在初始化後,仍可以在程式執行過程中對其長度進行調整,因此也稱“動態資料結構”。陣列在初始化後長度不可變,因此也稱“靜態資料結構”。值得注意的是,陣列可透過重新分配記憶體實現長度變化,從而具備一定的“動態性”。 + +!!! tip + + 如果你感覺物理結構理解起來有困難,建議先閱讀下一章,然後再回顧本節內容。 diff --git a/zh-Hant/docs/chapter_data_structure/index.md b/zh-Hant/docs/chapter_data_structure/index.md new file mode 100644 index 000000000..8ec9b5b93 --- /dev/null +++ b/zh-Hant/docs/chapter_data_structure/index.md @@ -0,0 +1,22 @@ +--- +comments: true +icon: material/shape-outline +--- + +# 第 3 章   資料結構 + +![資料結構](../assets/covers/chapter_data_structure.jpg){ 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/) diff --git a/zh-Hant/docs/chapter_data_structure/number_encoding.md b/zh-Hant/docs/chapter_data_structure/number_encoding.md new file mode 100644 index 000000000..ee8e1945c --- /dev/null +++ b/zh-Hant/docs/chapter_data_structure/number_encoding.md @@ -0,0 +1,162 @@ +--- +comments: true +--- + +# 3.3   數字編碼 * + +!!! note + + 在本書中,標題帶有 * 符號的是選讀章節。如果你時間有限或感到理解困難,可以先跳過,等學完必讀章節後再單獨攻克。 + +## 3.3.1   原碼、一補數和二補數 + +在上一節的表格中我們發現,所有整數型別能夠表示的負數都比正數多一個,例如 `byte` 的取值範圍是 $[-128, 127]$ 。這個現象比較反直覺,它的內在原因涉及原碼、一補數、二補數的相關知識。 + +首先需要指出,**數字是以“二補數”的形式儲存在計算機中的**。在分析這樣做的原因之前,首先給出三者的定義。 + +- **原碼**:我們將數字的二進位制表示的最高位視為符號位,其中 $0$ 表示正數,$1$ 表示負數,其餘位表示數字的值。 +- **一補數**:正數的一補數與其原碼相同,負數的一補數是對其原碼除符號位外的所有位取反。 +- **二補數**:正數的二補數與其原碼相同,負數的二補數是在其一補數的基礎上加 $1$ 。 + +圖 3-4 展示了原碼、一補數和二補數之間的轉換方法。 + +![原碼、一補數與二補數之間的相互轉換](number_encoding.assets/1s_2s_complement.png){ class="animation-figure" } + +

圖 3-4   原碼、一補數與二補數之間的相互轉換

+ +原碼(sign-magnitude)雖然最直觀,但存在一些侷限性。一方面,**負數的原碼不能直接用於運算**。例如在原碼下計算 $1 + (-2)$ ,得到的結果是 $-3$ ,這顯然是不對的。 + +$$ +\begin{aligned} +& 1 + (-2) \newline +& \rightarrow 0000 \; 0001 + 1000 \; 0010 \newline +& = 1000 \; 0011 \newline +& \rightarrow -3 +\end{aligned} +$$ + +為了解決此問題,計算機引入了一補數(1's complement)。如果我們先將原碼轉換為一補數,並在一補數下計算 $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} +$$ + +與原碼一樣,一補數也存在正負零歧義問題,因此計算機進一步引入了二補數(2's complement)。我們先來觀察一下負零的原碼、一補數、二補數的轉換過程: + +$$ +\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} +$$ + +![IEEE 754 標準下的 float 的計算示例](number_encoding.assets/ieee_754_float.png){ class="animation-figure" } + +

圖 3-5   IEEE 754 標準下的 float 的計算示例

+ +觀察圖 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}$ 等**。 + +

表 3-2   指數位含義

+ +
+ +| 指數位 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}$ | | + +
+ +值得說明的是,次正規數顯著提升了浮點數的精度。最小正正規數為 $2^{-126}$ ,最小正次正規數為 $2^{-126} \times 2^{-23}$ 。 + +雙精度 `double` 也採用類似於 `float` 的表示方法,在此不做贅述。 diff --git a/zh-Hant/docs/chapter_data_structure/summary.md b/zh-Hant/docs/chapter_data_structure/summary.md new file mode 100644 index 000000000..98f674b9d --- /dev/null +++ b/zh-Hant/docs/chapter_data_structure/summary.md @@ -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。另外,擴容操作也是自動實現的。詳見後續的“串列”章節。 diff --git a/zh-Hant/docs/chapter_divide_and_conquer/binary_search_recur.md b/zh-Hant/docs/chapter_divide_and_conquer/binary_search_recur.md new file mode 100644 index 000000000..f7425d84f --- /dev/null +++ b/zh-Hant/docs/chapter_divide_and_conquer/binary_search_recur.md @@ -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$ 的分治過程。 + +![二分搜尋的分治過程](binary_search_recur.assets/binary_search_recur.png){ class="animation-figure" } + +

圖 12-4   二分搜尋的分治過程

+ +在實現程式碼中,我們宣告一個遞迴函式 `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 &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 &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 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 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 "視覺化執行" + +
+ diff --git a/zh-Hant/docs/chapter_divide_and_conquer/build_binary_tree_problem.md b/zh-Hant/docs/chapter_divide_and_conquer/build_binary_tree_problem.md new file mode 100644 index 000000000..a52e4d6b3 --- /dev/null +++ b/zh-Hant/docs/chapter_divide_and_conquer/build_binary_tree_problem.md @@ -0,0 +1,539 @@ +--- +comments: true +--- + +# 12.3   構建二元樹問題 + +!!! question + + 給定一棵二元樹的前序走訪 `preorder` 和中序走訪 `inorder` ,請從中構建二元樹,返回二元樹的根節點。假設二元樹中沒有值重複的節點(如圖 12-5 所示)。 + +![構建二元樹的示例資料](build_binary_tree_problem.assets/build_tree_example.png){ class="animation-figure" } + +

圖 12-5   構建二元樹的示例資料

+ +### 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 ]` 。 + +![在前序走訪和中序走訪中劃分子樹](build_binary_tree_problem.assets/build_tree_preorder_inorder_division.png){ class="animation-figure" } + +

圖 12-6   在前序走訪和中序走訪中劃分子樹

+ +### 3.   基於變數描述子樹區間 + +根據以上劃分方法,**我們已經得到根節點、左子樹、右子樹在 `preorder` 和 `inorder` 中的索引區間**。而為了描述這些索引區間,我們需要藉助幾個指標變數。 + +- 將當前樹的根節點在 `preorder` 中的索引記為 $i$ 。 +- 將當前樹的根節點在 `inorder` 中的索引記為 $m$ 。 +- 將當前樹在 `inorder` 中的索引區間記為 $[l, r]$ 。 + +如表 12-1 所示,透過以上變數即可表示根節點在 `preorder` 中的索引,以及子樹在 `inorder` 中的索引區間。 + +

表 12-1   根節點和子樹在前序走訪和中序走訪中的索引

+ +
+ +| | 根節點在 `preorder` 中的索引 | 子樹在 `inorder` 中的索引區間 | +| ------ | ---------------------------- | ----------------------------- | +| 當前樹 | $i$ | $[l, r]$ | +| 左子樹 | $i + 1$ | $[l, m-1]$ | +| 右子樹 | $i + 1 + (m - l)$ | $[m+1, r]$ | + +
+ +請注意,右子樹根節點索引中的 $(m-l)$ 的含義是“左子樹的節點數量”,建議結合圖 12-7 理解。 + +![根節點和左右子樹的索引區間表示](build_binary_tree_problem.assets/build_tree_division_pointers.png){ class="animation-figure" } + +

圖 12-7   根節點和左右子樹的索引區間表示

+ +### 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 &preorder, unordered_map &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 &preorder, vector &inorder) { + // 初始化雜湊表,儲存 inorder 元素到索引的對映 + unordered_map 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 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 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 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 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, + 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(); + 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 preorder, + Map 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 preorder, List inorder) { + // 初始化雜湊表,儲存 inorder 元素到索引的對映 + Map 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, + i: i32, + l: i32, + r: i32, + ) -> Option>> { + // 子樹區間為空時終止 + 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>> { + // 初始化雜湊表,儲存 inorder 元素到索引的對映 + let mut inorder_map: HashMap = 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, 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 = 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 "視覺化執行" + +
+ + +圖 12-8 展示了構建二元樹的遞迴過程,各個節點是在向下“遞”的過程中建立的,而各條邊(引用)是在向上“迴”的過程中建立的。 + +=== "<1>" + ![構建二元樹的遞迴過程](build_binary_tree_problem.assets/built_tree_step1.png){ class="animation-figure" } + +=== "<2>" + ![built_tree_step2](build_binary_tree_problem.assets/built_tree_step2.png){ class="animation-figure" } + +=== "<3>" + ![built_tree_step3](build_binary_tree_problem.assets/built_tree_step3.png){ class="animation-figure" } + +=== "<4>" + ![built_tree_step4](build_binary_tree_problem.assets/built_tree_step4.png){ class="animation-figure" } + +=== "<5>" + ![built_tree_step5](build_binary_tree_problem.assets/built_tree_step5.png){ class="animation-figure" } + +=== "<6>" + ![built_tree_step6](build_binary_tree_problem.assets/built_tree_step6.png){ class="animation-figure" } + +=== "<7>" + ![built_tree_step7](build_binary_tree_problem.assets/built_tree_step7.png){ class="animation-figure" } + +=== "<8>" + ![built_tree_step8](build_binary_tree_problem.assets/built_tree_step8.png){ class="animation-figure" } + +=== "<9>" + ![built_tree_step9](build_binary_tree_problem.assets/built_tree_step9.png){ class="animation-figure" } + +

圖 12-8   構建二元樹的遞迴過程

+ +每個遞迴函式內的前序走訪 `preorder` 和中序走訪 `inorder` 的劃分結果如圖 12-9 所示。 + +![每個遞迴函式中的劃分結果](build_binary_tree_problem.assets/built_tree_overall.png){ class="animation-figure" } + +

圖 12-9   每個遞迴函式中的劃分結果

+ +設樹的節點數量為 $n$ ,初始化每一個節點(執行一個遞迴函式 `dfs()` )使用 $O(1)$ 時間。**因此總體時間複雜度為 $O(n)$** 。 + +雜湊表儲存 `inorder` 元素到索引的對映,空間複雜度為 $O(n)$ 。在最差情況下,即二元樹退化為鏈結串列時,遞迴深度達到 $n$ ,使用 $O(n)$ 的堆疊幀空間。**因此總體空間複雜度為 $O(n)$** 。 diff --git a/zh-Hant/docs/chapter_divide_and_conquer/divide_and_conquer.md b/zh-Hant/docs/chapter_divide_and_conquer/divide_and_conquer.md new file mode 100644 index 000000000..b19026737 --- /dev/null +++ b/zh-Hant/docs/chapter_divide_and_conquer/divide_and_conquer.md @@ -0,0 +1,101 @@ +--- +comments: true +--- + +# 12.1   分治演算法 + +分治(divide and conquer),全稱分而治之,是一種非常重要且常見的演算法策略。分治通常基於遞迴實現,包括“分”和“治”兩個步驟。 + +1. **分(劃分階段)**:遞迴地將原問題分解為兩個或多個子問題,直至到達最小子問題時終止。 +2. **治(合併階段)**:從已知解的最小子問題開始,從底至頂地將子問題的解進行合併,從而構建出原問題的解。 + +如圖 12-1 所示,“合併排序”是分治策略的典型應用之一。 + +1. **分**:遞迴地將原陣列(原問題)劃分為兩個子陣列(子問題),直到子陣列只剩一個元素(最小子問題)。 +2. **治**:從底至頂地將有序的子陣列(子問題的解)進行合併,從而得到有序的原陣列(原問題的解)。 + +![合併排序的分治策略](divide_and_conquer.assets/divide_and_conquer_merge_sort.png){ class="animation-figure" } + +

圖 12-1   合併排序的分治策略

+ +## 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) +$$ + +![劃分陣列前後的泡沫排序](divide_and_conquer.assets/divide_and_conquer_bubble_sort.png){ class="animation-figure" } + +

圖 12-2   劃分陣列前後的泡沫排序

+ +接下來,我們計算以下不等式,其左邊和右邊分別為劃分前和劃分後的操作總數: + +$$ +\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 所示的“桶排序”中,我們將海量的資料平均分配到各個桶中,則可所有桶的排序任務分散到各個計算單元,完成後再合併結果。 + +![桶排序的平行計算](divide_and_conquer.assets/divide_and_conquer_parallel_computing.png){ class="animation-figure" } + +

圖 12-3   桶排序的平行計算

+ +## 12.1.3   分治常見應用 + +一方面,分治可以用來解決許多經典演算法問題。 + +- **尋找最近點對**:該演算法首先將點集分成兩部分,然後分別找出兩部分中的最近點對,最後找出跨越兩部分的最近點對。 +- **大整數乘法**:例如 Karatsuba 演算法,它將大整數乘法分解為幾個較小的整數的乘法和加法。 +- **矩陣乘法**:例如 Strassen 演算法,它將大矩陣乘法分解為多個小矩陣的乘法和加法。 +- **河內塔問題**:河內塔問題可以透過遞迴解決,這是典型的分治策略應用。 +- **求解逆序對**:在一個序列中,如果前面的數字大於後面的數字,那麼這兩個數字構成一個逆序對。求解逆序對問題可以利用分治的思想,藉助合併排序進行求解。 + +另一方面,分治在演算法和資料結構的設計中應用得非常廣泛。 + +- **二分搜尋**:二分搜尋是將有序陣列從中點索引處分為兩部分,然後根據目標值與中間元素值比較結果,決定排除哪一半區間,並在剩餘區間執行相同的二分操作。 +- **合併排序**:本節開頭已介紹,不再贅述。 +- **快速排序**:快速排序是選取一個基準值,然後把陣列分為兩個子陣列,一個子陣列的元素比基準值小,另一子陣列的元素比基準值大,再對這兩部分進行相同的劃分操作,直至子陣列只剩下一個元素。 +- **桶排序**:桶排序的基本思想是將資料分散到多個桶,然後對每個桶內的元素進行排序,最後將各個桶的元素依次取出,從而得到一個有序陣列。 +- **樹**:例如二元搜尋樹、AVL 樹、紅黑樹、B 樹、B+ 樹等,它們的查詢、插入和刪除等操作都可以視為分治策略的應用。 +- **堆積**:堆積是一種特殊的完全二元樹,其各種操作,如插入、刪除和堆積化,實際上都隱含了分治的思想。 +- **雜湊表**:雖然雜湊表並不直接應用分治,但某些雜湊衝突解決方案間接應用了分治策略,例如,鏈式位址中的長鏈結串列會被轉化為紅黑樹,以提升查詢效率。 + +可以看出,**分治是一種“潤物細無聲”的演算法思想**,隱含在各種演算法與資料結構之中。 diff --git a/zh-Hant/docs/chapter_divide_and_conquer/hanota_problem.md b/zh-Hant/docs/chapter_divide_and_conquer/hanota_problem.md new file mode 100644 index 000000000..ae7b498c6 --- /dev/null +++ b/zh-Hant/docs/chapter_divide_and_conquer/hanota_problem.md @@ -0,0 +1,545 @@ +--- +comments: true +--- + +# 12.4   河內塔問題 + +在合併排序和構建二元樹中,我們都是將原問題分解為兩個規模為原問題一半的子問題。然而對於河內塔問題,我們採用不同的分解策略。 + +!!! question + + 給定三根柱子,記為 `A`、`B` 和 `C` 。起始狀態下,柱子 `A` 上套著 $n$ 個圓盤,它們從上到下按照從小到大的順序排列。我們的任務是要把這 $n$ 個圓盤移到柱子 `C` 上,並保持它們的原有順序不變(如圖 12-10 所示)。在移動圓盤的過程中,需要遵守以下規則。 + + 1. 圓盤只能從一根柱子頂部拿出,從另一根柱子頂部放入。 + 2. 每次只能移動一個圓盤。 + 3. 小圓盤必須時刻位於大圓盤之上。 + +![河內塔問題示例](hanota_problem.assets/hanota_example.png){ class="animation-figure" } + +

圖 12-10   河內塔問題示例

+ +**我們將規模為 $i$ 的河內塔問題記作 $f(i)$** 。例如 $f(3)$ 代表將 $3$ 個圓盤從 `A` 移動至 `C` 的河內塔問題。 + +### 1.   考慮基本情況 + +如圖 12-11 所示,對於問題 $f(1)$ ,即當只有一個圓盤時,我們將它直接從 `A` 移動至 `C` 即可。 + +=== "<1>" + ![規模為 1 的問題的解](hanota_problem.assets/hanota_f1_step1.png){ class="animation-figure" } + +=== "<2>" + ![hanota_f1_step2](hanota_problem.assets/hanota_f1_step2.png){ class="animation-figure" } + +

圖 12-11   規模為 1 的問題的解

+ +如圖 12-12 所示,對於問題 $f(2)$ ,即當有兩個圓盤時,**由於要時刻滿足小圓盤在大圓盤之上,因此需要藉助 `B` 來完成移動**。 + +1. 先將上面的小圓盤從 `A` 移至 `B` 。 +2. 再將大圓盤從 `A` 移至 `C` 。 +3. 最後將小圓盤從 `B` 移至 `C` 。 + +=== "<1>" + ![規模為 2 的問題的解](hanota_problem.assets/hanota_f2_step1.png){ class="animation-figure" } + +=== "<2>" + ![hanota_f2_step2](hanota_problem.assets/hanota_f2_step2.png){ class="animation-figure" } + +=== "<3>" + ![hanota_f2_step3](hanota_problem.assets/hanota_f2_step3.png){ class="animation-figure" } + +=== "<4>" + ![hanota_f2_step4](hanota_problem.assets/hanota_f2_step4.png){ class="animation-figure" } + +

圖 12-12   規模為 2 的問題的解

+ +解決問題 $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>" + ![規模為 3 的問題的解](hanota_problem.assets/hanota_f3_step1.png){ class="animation-figure" } + +=== "<2>" + ![hanota_f3_step2](hanota_problem.assets/hanota_f3_step2.png){ class="animation-figure" } + +=== "<3>" + ![hanota_f3_step3](hanota_problem.assets/hanota_f3_step3.png){ class="animation-figure" } + +=== "<4>" + ![hanota_f3_step4](hanota_problem.assets/hanota_f3_step4.png){ class="animation-figure" } + +

圖 12-13   規模為 3 的問題的解

+ +從本質上看,**我們將問題 $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)$ 的解是已知的,只需一次移動操作即可。 + +![解決河內塔問題的分治策略](hanota_problem.assets/hanota_divide_and_conquer.png){ class="animation-figure" } + +

圖 12-14   解決河內塔問題的分治策略

+ +### 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 &src, vector &tar) { + // 從 src 頂部拿出一個圓盤 + int pan = src.back(); + src.pop_back(); + // 將圓盤放入 tar 頂部 + tar.push_back(pan); + } + + /* 求解河內塔問題 f(i) */ + void dfs(int i, vector &src, vector &buf, vector &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 &A, vector &B, vector &C) { + int n = A.size(); + // 將 A 頂部 n 個圓盤藉助 B 移到 C + dfs(n, A, B, C); + } + ``` + +=== "Java" + + ```java title="hanota.java" + /* 移動一個圓盤 */ + void move(List src, List tar) { + // 從 src 頂部拿出一個圓盤 + Integer pan = src.remove(src.size() - 1); + // 將圓盤放入 tar 頂部 + tar.add(pan); + } + + /* 求解河內塔問題 f(i) */ + void dfs(int i, List src, List buf, List 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 A, List B, List C) { + int n = A.size(); + // 將 A 頂部 n 個圓盤藉助 B 移到 C + dfs(n, A, B, C); + } + ``` + +=== "C#" + + ```csharp title="hanota.cs" + /* 移動一個圓盤 */ + void Move(List src, List tar) { + // 從 src 頂部拿出一個圓盤 + int pan = src[^1]; + src.RemoveAt(src.Count - 1); + // 將圓盤放入 tar 頂部 + tar.Add(pan); + } + + /* 求解河內塔問題 f(i) */ + void DFS(int i, List src, List buf, List 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 A, List B, List 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 src, List tar) { + // 從 src 頂部拿出一個圓盤 + int pan = src.removeLast(); + // 將圓盤放入 tar 頂部 + tar.add(pan); + } + + /* 求解河內塔問題 f(i) */ + void dfs(int i, List src, List buf, List 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 A, List B, List 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, tar: &mut Vec) { + // 從 src 頂部拿出一個圓盤 + let pan = src.remove(src.len() - 1); + // 將圓盤放入 tar 頂部 + tar.push(pan); + } + + /* 求解河內塔問題 f(i) */ + fn dfs(i: i32, src: &mut Vec, buf: &mut Vec, tar: &mut Vec) { + // 若 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, B: &mut Vec, C: &mut Vec) { + 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, tar: MutableList) { + // 從 src 頂部拿出一個圓盤 + val pan: Int = src.removeAt(src.size - 1) + // 將圓盤放入 tar 頂部 + tar.add(pan) + } + + /* 求解河內塔問題 f(i) */ + fun dfs(i: Int, src: MutableList, buf: MutableList, tar: MutableList) { + // 若 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, B: MutableList, C: MutableList) { + 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 "視覺化執行" + +
+ + +如圖 12-15 所示,河內塔問題形成一棵高度為 $n$ 的遞迴樹,每個節點代表一個子問題,對應一個開啟的 `dfs()` 函式,**因此時間複雜度為 $O(2^n)$ ,空間複雜度為 $O(n)$** 。 + +![河內塔問題的遞迴樹](hanota_problem.assets/hanota_recursive_tree.png){ class="animation-figure" } + +

圖 12-15   河內塔問題的遞迴樹

+ +!!! quote + + 河內塔問題源自一個古老的傳說。在古印度的一個寺廟裡,僧侶們有三根高大的鑽石柱子,以及 $64$ 個大小不一的金圓盤。僧侶們不斷地移動圓盤,他們相信在最後一個圓盤被正確放置的那一刻,這個世界就會結束。 + + 然而,即使僧侶們每秒鐘移動一次,總共需要大約 $2^{64} \approx 1.84×10^{19}$ 秒,合約 $5850$ 億年,遠遠超過了現在對宇宙年齡的估計。所以,倘若這個傳說是真的,我們應該不需要擔心世界末日的到來。 diff --git a/zh-Hant/docs/chapter_divide_and_conquer/index.md b/zh-Hant/docs/chapter_divide_and_conquer/index.md new file mode 100644 index 000000000..2f7838d22 --- /dev/null +++ b/zh-Hant/docs/chapter_divide_and_conquer/index.md @@ -0,0 +1,22 @@ +--- +comments: true +icon: material/set-split +--- + +# 第 12 章   分治 + +![分治](../assets/covers/chapter_divide_and_conquer.jpg){ 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/) diff --git a/zh-Hant/docs/chapter_divide_and_conquer/summary.md b/zh-Hant/docs/chapter_divide_and_conquer/summary.md new file mode 100644 index 000000000..8e2afdad6 --- /dev/null +++ b/zh-Hant/docs/chapter_divide_and_conquer/summary.md @@ -0,0 +1,15 @@ +--- +comments: true +--- + +# 12.5   小結 + +- 分治是一種常見的演算法設計策略,包括分(劃分)和治(合併)兩個階段,通常基於遞迴實現。 +- 判斷是否是分治演算法問題的依據包括:問題能否分解、子問題是否獨立、子問題能否合併。 +- 合併排序是分治策略的典型應用,其遞迴地將陣列劃分為等長的兩個子陣列,直到只剩一個元素時開始逐層合併,從而完成排序。 +- 引入分治策略往往可以提升演算法效率。一方面,分治策略減少了操作數量;另一方面,分治後有利於系統的並行最佳化。 +- 分治既可以解決許多演算法問題,也廣泛應用於資料結構與演算法設計中,處處可見其身影。 +- 相較於暴力搜尋,自適應搜尋效率更高。時間複雜度為 $O(\log n)$ 的搜尋演算法通常是基於分治策略實現的。 +- 二分搜尋是分治策略的另一個典型應用,它不包含將子問題的解進行合併的步驟。我們可以透過遞迴分治實現二分搜尋。 +- 在構建二元樹的問題中,構建樹(原問題)可以劃分為構建左子樹和右子樹(子問題),這可以透過劃分前序走訪和中序走訪的索引區間來實現。 +- 在河內塔問題中,一個規模為 $n$ 的問題可以劃分為兩個規模為 $n-1$ 的子問題和一個規模為 $1$ 的子問題。按順序解決這三個子問題後,原問題隨之得到解決。 diff --git a/zh-Hant/docs/chapter_dynamic_programming/dp_problem_features.md b/zh-Hant/docs/chapter_dynamic_programming/dp_problem_features.md new file mode 100644 index 000000000..2a0e15ec8 --- /dev/null +++ b/zh-Hant/docs/chapter_dynamic_programming/dp_problem_features.md @@ -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$ 。 + +![爬到第 3 階的最小代價](dp_problem_features.assets/min_cost_cs_example.png){ class="animation-figure" } + +

圖 14-6   爬到第 3 階的最小代價

+ +設 $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 &cost) { + int n = cost.size() - 1; + if (n == 1 || n == 2) + return cost[n]; + // 初始化 dp 表,用於儲存子問題的解 + vector 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 { + 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 cost) { + int n = cost.length - 1; + if (n == 1 || n == 2) return cost[n]; + // 初始化 dp 表,用於儲存子問題的解 + List 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 "視覺化執行" + +
+ + +圖 14-7 展示了以上程式碼的動態規劃過程。 + +![爬樓梯最小代價的動態規劃過程](dp_problem_features.assets/min_cost_cs_dp.png){ class="animation-figure" } + +

圖 14-7   爬樓梯最小代價的動態規劃過程

+ +本題也可以進行空間最佳化,將一維壓縮至零維,使得空間複雜度從 $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 &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 { + 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 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 "視覺化執行" + +
+ + +## 14.2.2   無後效性 + +無後效性是動態規劃能夠有效解決問題的重要特性之一,其定義為:**給定一個確定的狀態,它的未來發展只與當前狀態有關,而與過去經歷的所有狀態無關**。 + +以爬樓梯問題為例,給定狀態 $i$ ,它會發展出狀態 $i+1$ 和狀態 $i+2$ ,分別對應跳 $1$ 步和跳 $2$ 步。在做出這兩種選擇時,我們無須考慮狀態 $i$ 之前的狀態,它們對狀態 $i$ 的未來沒有影響。 + +然而,如果我們給爬樓梯問題新增一個約束,情況就不一樣了。 + +!!! question "帶約束爬樓梯" + + 給定一個共有 $n$ 階的樓梯,你每步可以上 $1$ 階或者 $2$ 階,**但不能連續兩輪跳 $1$ 階**,請問有多少種方案可以爬到樓頂? + +如圖 14-8 所示,爬上第 $3$ 階僅剩 $2$ 種可行方案,其中連續三次跳 $1$ 階的方案不滿足約束條件,因此被捨棄。 + +![帶約束爬到第 3 階的方案數量](dp_problem_features.assets/climbing_stairs_constraint_example.png){ class="animation-figure" } + +

圖 14-8   帶約束爬到第 3 階的方案數量

+ +在該問題中,如果上一輪是跳 $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} +$$ + +![考慮約束下的遞推關係](dp_problem_features.assets/climbing_stairs_constraint_state_transfer.png){ class="animation-figure" } + +

圖 14-9   考慮約束下的遞推關係

+ +最終,返回 $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> dp(n + 1, vector(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> 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 "視覺化執行" + +
+ + +在上面的案例中,由於僅需多考慮前面一個狀態,因此我們仍然可以透過擴展狀態定義,使得問題重新滿足無後效性。然而,某些問題具有非常嚴重的“有後效性”。 + +!!! question "爬樓梯與障礙生成" + + 給定一個共有 $n$ 階的樓梯,你每步可以上 $1$ 階或者 $2$ 階。**規定當爬到第 $i$ 階時,系統自動會在第 $2i$ 階上放上障礙物,之後所有輪都不允許跳到第 $2i$ 階上**。例如,前兩輪分別跳到了第 $2$、$3$ 階上,則之後就不能跳到第 $4$、$6$ 階上。請問有多少種方案可以爬到樓頂? + +在這個問題中,下次跳躍依賴過去所有的狀態,因為每一次跳躍都會在更高的階梯上設定障礙,並影響未來的跳躍。對於這類問題,動態規劃往往難以解決。 + +實際上,許多複雜的組合最佳化問題(例如旅行商問題)不滿足無後效性。對於這類問題,我們通常會選擇使用其他方法,例如啟發式搜尋、遺傳演算法、強化學習等,從而在有限時間內得到可用的區域性最優解。 diff --git a/zh-Hant/docs/chapter_dynamic_programming/dp_solution_pipeline.md b/zh-Hant/docs/chapter_dynamic_programming/dp_solution_pipeline.md new file mode 100644 index 000000000..d85cc7475 --- /dev/null +++ b/zh-Hant/docs/chapter_dynamic_programming/dp_solution_pipeline.md @@ -0,0 +1,1561 @@ +--- +comments: true +--- + +# 14.3   動態規劃解題思路 + +上兩節介紹了動態規劃問題的主要特徵,接下來我們一起探究兩個更加實用的問題。 + +1. 如何判斷一個問題是不是動態規劃問題? +2. 求解動態規劃問題該從何處入手,完整步驟是什麼? + +## 14.3.1   問題判斷 + +總的來說,如果一個問題包含重疊子問題、最優子結構,並滿足無後效性,那麼它通常適合用動態規劃求解。然而,我們很難從問題描述中直接提取出這些特性。因此我們通常會放寬條件,**先觀察問題是否適合使用回溯(窮舉)解決**。 + +**適合用回溯解決的問題通常滿足“決策樹模型”**,這種問題可以使用樹形結構來描述,其中每一個節點代表一個決策,每一條路徑代表一個決策序列。 + +換句話說,如果問題包含明確的決策概念,並且解是透過一系列決策產生的,那麼它就滿足決策樹模型,通常可以使用回溯來解決。 + +在此基礎上,動態規劃問題還有一些判斷的“加分項”。 + +- 問題包含最大(小)或最多(少)等最最佳化描述。 +- 問題的狀態能夠使用一個串列、多維矩陣或樹來表示,並且一個狀態與其周圍的狀態存在遞推關係。 + +相應地,也存在一些“減分項”。 + +- 問題的目標是找出所有可能的解決方案,而不是找出最優解。 +- 問題描述中有明顯的排列組合的特徵,需要返回具體的多個方案。 + +如果一個問題滿足決策樹模型,並具有較為明顯的“加分項”,我們就可以假設它是一個動態規劃問題,並在求解過程中驗證它。 + +## 14.3.2   問題求解步驟 + +動態規劃的解題流程會因問題的性質和難度而有所不同,但通常遵循以下步驟:描述決策,定義狀態,建立 $dp$ 表,推導狀態轉移方程,確定邊界條件等。 + +為了更形象地展示解題步驟,我們使用一個經典問題“最小路徑和”來舉例。 + +!!! question + + 給定一個 $n \times m$ 的二維網格 `grid` ,網格中的每個單元格包含一個非負整數,表示該單元格的代價。機器人以左上角單元格為起始點,每次只能向下或者向右移動一步,直至到達右下角單元格。請返回從左上角到右下角的最小路徑和。 + +圖 14-10 展示了一個例子,給定網格的最小路徑和為 $13$ 。 + +![最小路徑和示例資料](dp_solution_pipeline.assets/min_path_sum_example.png){ class="animation-figure" } + +

圖 14-10   最小路徑和示例資料

+ +**第一步:思考每輪的決策,定義狀態,從而得到 $dp$ 表** + +本題的每一輪的決策就是從當前格子向下或向右走一步。設當前格子的行列索引為 $[i, j]$ ,則向下或向右走一步後,索引變為 $[i+1, j]$ 或 $[i, j+1]$ 。因此,狀態應包含行索引和列索引兩個變數,記為 $[i, j]$ 。 + +狀態 $[i, j]$ 對應的子問題為:從起始點 $[0, 0]$ 走到 $[i, j]$ 的最小路徑和,解記為 $dp[i, j]$ 。 + +至此,我們就得到了圖 14-11 所示的二維 $dp$ 矩陣,其尺寸與輸入網格 $grid$ 相同。 + +![狀態定義與 dp 表](dp_solution_pipeline.assets/min_path_sum_solution_state_definition.png){ class="animation-figure" } + +

圖 14-11   狀態定義與 dp 表

+ +!!! note + + 動態規劃和回溯過程可以描述為一個決策序列,而狀態由所有決策變數構成。它應當包含描述解題進度的所有變數,其包含了足夠的資訊,能夠用來推導出下一個狀態。 + + 每個狀態都對應一個子問題,我們會定義一個 $dp$ 表來儲存所有子問題的解,狀態的每個獨立變數都是 $dp$ 表的一個維度。從本質上看,$dp$ 表是狀態和子問題的解之間的對映。 + +**第二步:找出最優子結構,進而推導出狀態轉移方程** + +對於狀態 $[i, j]$ ,它只能從上邊格子 $[i-1, j]$ 和左邊格子 $[i, j-1]$ 轉移而來。因此最優子結構為:到達 $[i, j]$ 的最小路徑和由 $[i, j-1]$ 的最小路徑和與 $[i-1, j]$ 的最小路徑和中較小的那一個決定。 + +根據以上分析,可推出圖 14-12 所示的狀態轉移方程: + +$$ +dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j] +$$ + +![最優子結構與狀態轉移方程](dp_solution_pipeline.assets/min_path_sum_solution_state_transition.png){ class="animation-figure" } + +

圖 14-12   最優子結構與狀態轉移方程

+ +!!! note + + 根據定義好的 $dp$ 表,思考原問題和子問題的關係,找出透過子問題的最優解來構造原問題的最優解的方法,即最優子結構。 + + 一旦我們找到了最優子結構,就可以使用它來構建出狀態轉移方程。 + +**第三步:確定邊界條件和狀態轉移順序** + +在本題中,處在首行的狀態只能從其左邊的狀態得來,處在首列的狀態只能從其上邊的狀態得來,因此首行 $i = 0$ 和首列 $j = 0$ 是邊界條件。 + +如圖 14-13 所示,由於每個格子是由其左方格子和上方格子轉移而來,因此我們使用迴圈來走訪矩陣,外迴圈走訪各行,內迴圈走訪各列。 + +![邊界條件與狀態轉移順序](dp_solution_pipeline.assets/min_path_sum_solution_initial_state.png){ class="animation-figure" } + +

圖 14-13   邊界條件與狀態轉移順序

+ +!!! note + + 邊界條件在動態規劃中用於初始化 $dp$ 表,在搜尋中用於剪枝。 + + 狀態轉移順序的核心是要保證在計算當前問題的解時,所有它依賴的更小子問題的解都已經被正確地計算出來。 + +根據以上分析,我們已經可以直接寫出動態規劃程式碼。然而子問題分解是一種從頂至底的思想,因此按照“暴力搜尋 $\rightarrow$ 記憶化搜尋 $\rightarrow$ 動態規劃”的順序實現更加符合思維習慣。 + +### 1.   方法一:暴力搜尋 + +從狀態 $[i, j]$ 開始搜尋,不斷分解為更小的狀態 $[i-1, j]$ 和 $[i, j-1]$ ,遞迴函式包括以下要素。 + +- **遞迴參數**:狀態 $[i, j]$ 。 +- **返回值**:從 $[0, 0]$ 到 $[i, j]$ 的最小路徑和 $dp[i, j]$ 。 +- **終止條件**:當 $i = 0$ 且 $j = 0$ 時,返回代價 $grid[0, 0]$ 。 +- **剪枝**:當 $i < 0$ 時或 $j < 0$ 時索引越界,此時返回代價 $+\infty$ ,代表不可行。 + +實現程式碼如下: + +=== "Python" + + ```python title="min_path_sum.py" + def min_path_sum_dfs(grid: list[list[int]], i: int, j: int) -> int: + """最小路徑和:暴力搜尋""" + # 若為左上角單元格,則終止搜尋 + if i == 0 and j == 0: + return grid[0][0] + # 若行列索引越界,則返回 +∞ 代價 + if i < 0 or j < 0: + return inf + # 計算從左上角到 (i-1, j) 和 (i, j-1) 的最小路徑代價 + up = min_path_sum_dfs(grid, i - 1, j) + left = min_path_sum_dfs(grid, i, j - 1) + # 返回從左上角到 (i, j) 的最小路徑代價 + return min(left, up) + grid[i][j] + ``` + +=== "C++" + + ```cpp title="min_path_sum.cpp" + /* 最小路徑和:暴力搜尋 */ + int minPathSumDFS(vector> &grid, int i, int j) { + // 若為左上角單元格,則終止搜尋 + if (i == 0 && j == 0) { + return grid[0][0]; + } + // 若行列索引越界,則返回 +∞ 代價 + if (i < 0 || j < 0) { + return INT_MAX; + } + // 計算從左上角到 (i-1, j) 和 (i, j-1) 的最小路徑代價 + int up = minPathSumDFS(grid, i - 1, j); + int left = minPathSumDFS(grid, i, j - 1); + // 返回從左上角到 (i, j) 的最小路徑代價 + return min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX; + } + ``` + +=== "Java" + + ```java title="min_path_sum.java" + /* 最小路徑和:暴力搜尋 */ + int minPathSumDFS(int[][] grid, int i, int j) { + // 若為左上角單元格,則終止搜尋 + if (i == 0 && j == 0) { + return grid[0][0]; + } + // 若行列索引越界,則返回 +∞ 代價 + if (i < 0 || j < 0) { + return Integer.MAX_VALUE; + } + // 計算從左上角到 (i-1, j) 和 (i, j-1) 的最小路徑代價 + int up = minPathSumDFS(grid, i - 1, j); + int left = minPathSumDFS(grid, i, j - 1); + // 返回從左上角到 (i, j) 的最小路徑代價 + return Math.min(left, up) + grid[i][j]; + } + ``` + +=== "C#" + + ```csharp title="min_path_sum.cs" + /* 最小路徑和:暴力搜尋 */ + int MinPathSumDFS(int[][] grid, int i, int j) { + // 若為左上角單元格,則終止搜尋 + if (i == 0 && j == 0) { + return grid[0][0]; + } + // 若行列索引越界,則返回 +∞ 代價 + if (i < 0 || j < 0) { + return int.MaxValue; + } + // 計算從左上角到 (i-1, j) 和 (i, j-1) 的最小路徑代價 + int up = MinPathSumDFS(grid, i - 1, j); + int left = MinPathSumDFS(grid, i, j - 1); + // 返回從左上角到 (i, j) 的最小路徑代價 + return Math.Min(left, up) + grid[i][j]; + } + ``` + +=== "Go" + + ```go title="min_path_sum.go" + /* 最小路徑和:暴力搜尋 */ + func minPathSumDFS(grid [][]int, i, j int) int { + // 若為左上角單元格,則終止搜尋 + if i == 0 && j == 0 { + return grid[0][0] + } + // 若行列索引越界,則返回 +∞ 代價 + if i < 0 || j < 0 { + return math.MaxInt + } + // 計算從左上角到 (i-1, j) 和 (i, j-1) 的最小路徑代價 + up := minPathSumDFS(grid, i-1, j) + left := minPathSumDFS(grid, i, j-1) + // 返回從左上角到 (i, j) 的最小路徑代價 + return int(math.Min(float64(left), float64(up))) + grid[i][j] + } + ``` + +=== "Swift" + + ```swift title="min_path_sum.swift" + /* 最小路徑和:暴力搜尋 */ + func minPathSumDFS(grid: [[Int]], i: Int, j: Int) -> Int { + // 若為左上角單元格,則終止搜尋 + if i == 0, j == 0 { + return grid[0][0] + } + // 若行列索引越界,則返回 +∞ 代價 + if i < 0 || j < 0 { + return .max + } + // 計算從左上角到 (i-1, j) 和 (i, j-1) 的最小路徑代價 + let up = minPathSumDFS(grid: grid, i: i - 1, j: j) + let left = minPathSumDFS(grid: grid, i: i, j: j - 1) + // 返回從左上角到 (i, j) 的最小路徑代價 + return min(left, up) + grid[i][j] + } + ``` + +=== "JS" + + ```javascript title="min_path_sum.js" + /* 最小路徑和:暴力搜尋 */ + function minPathSumDFS(grid, i, j) { + // 若為左上角單元格,則終止搜尋 + if (i === 0 && j === 0) { + return grid[0][0]; + } + // 若行列索引越界,則返回 +∞ 代價 + if (i < 0 || j < 0) { + return Infinity; + } + // 計算從左上角到 (i-1, j) 和 (i, j-1) 的最小路徑代價 + const up = minPathSumDFS(grid, i - 1, j); + const left = minPathSumDFS(grid, i, j - 1); + // 返回從左上角到 (i, j) 的最小路徑代價 + return Math.min(left, up) + grid[i][j]; + } + ``` + +=== "TS" + + ```typescript title="min_path_sum.ts" + /* 最小路徑和:暴力搜尋 */ + function minPathSumDFS( + grid: Array>, + i: number, + j: number + ): number { + // 若為左上角單元格,則終止搜尋 + if (i === 0 && j == 0) { + return grid[0][0]; + } + // 若行列索引越界,則返回 +∞ 代價 + if (i < 0 || j < 0) { + return Infinity; + } + // 計算從左上角到 (i-1, j) 和 (i, j-1) 的最小路徑代價 + const up = minPathSumDFS(grid, i - 1, j); + const left = minPathSumDFS(grid, i, j - 1); + // 返回從左上角到 (i, j) 的最小路徑代價 + return Math.min(left, up) + grid[i][j]; + } + ``` + +=== "Dart" + + ```dart title="min_path_sum.dart" + /* 最小路徑和:暴力搜尋 */ + int minPathSumDFS(List> grid, int i, int j) { + // 若為左上角單元格,則終止搜尋 + if (i == 0 && j == 0) { + return grid[0][0]; + } + // 若行列索引越界,則返回 +∞ 代價 + if (i < 0 || j < 0) { + // 在 Dart 中,int 型別是固定範圍的整數,不存在表示“無窮大”的值 + return BigInt.from(2).pow(31).toInt(); + } + // 計算從左上角到 (i-1, j) 和 (i, j-1) 的最小路徑代價 + int up = minPathSumDFS(grid, i - 1, j); + int left = minPathSumDFS(grid, i, j - 1); + // 返回從左上角到 (i, j) 的最小路徑代價 + return min(left, up) + grid[i][j]; + } + ``` + +=== "Rust" + + ```rust title="min_path_sum.rs" + /* 最小路徑和:暴力搜尋 */ + fn min_path_sum_dfs(grid: &Vec>, i: i32, j: i32) -> i32 { + // 若為左上角單元格,則終止搜尋 + if i == 0 && j == 0 { + return grid[0][0]; + } + // 若行列索引越界,則返回 +∞ 代價 + if i < 0 || j < 0 { + return i32::MAX; + } + // 計算從左上角到 (i-1, j) 和 (i, j-1) 的最小路徑代價 + let up = min_path_sum_dfs(grid, i - 1, j); + let left = min_path_sum_dfs(grid, i, j - 1); + // 返回從左上角到 (i, j) 的最小路徑代價 + std::cmp::min(left, up) + grid[i as usize][j as usize] + } + ``` + +=== "C" + + ```c title="min_path_sum.c" + /* 最小路徑和:暴力搜尋 */ + int minPathSumDFS(int grid[MAX_SIZE][MAX_SIZE], int i, int j) { + // 若為左上角單元格,則終止搜尋 + if (i == 0 && j == 0) { + return grid[0][0]; + } + // 若行列索引越界,則返回 +∞ 代價 + if (i < 0 || j < 0) { + return INT_MAX; + } + // 計算從左上角到 (i-1, j) 和 (i, j-1) 的最小路徑代價 + int up = minPathSumDFS(grid, i - 1, j); + int left = minPathSumDFS(grid, i, j - 1); + // 返回從左上角到 (i, j) 的最小路徑代價 + return myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX; + } + ``` + +=== "Kotlin" + + ```kotlin title="min_path_sum.kt" + /* 最小路徑和:暴力搜尋 */ + fun minPathSumDFS( + grid: Array>, + i: Int, + j: Int + ): Int { + // 若為左上角單元格,則終止搜尋 + if (i == 0 && j == 0) { + return grid[0][0] + } + // 若行列索引越界,則返回 +∞ 代價 + if (i < 0 || j < 0) { + return Int.MAX_VALUE + } + // 計算從左上角到 (i-1, j) 和 (i, j-1) 的最小路徑代價 + val up = minPathSumDFS(grid, i - 1, j) + val left = minPathSumDFS(grid, i, j - 1) + // 返回從左上角到 (i, j) 的最小路徑代價 + return (min(left.toDouble(), up.toDouble()) + grid[i][j]).toInt() + } + ``` + +=== "Ruby" + + ```ruby title="min_path_sum.rb" + [class]{}-[func]{min_path_sum_dfs} + ``` + +=== "Zig" + + ```zig title="min_path_sum.zig" + // 最小路徑和:暴力搜尋 + fn minPathSumDFS(grid: anytype, i: i32, j: i32) i32 { + // 若為左上角單元格,則終止搜尋 + if (i == 0 and j == 0) { + return grid[0][0]; + } + // 若行列索引越界,則返回 +∞ 代價 + if (i < 0 or j < 0) { + return std.math.maxInt(i32); + } + // 計算從左上角到 (i-1, j) 和 (i, j-1) 的最小路徑代價 + var up = minPathSumDFS(grid, i - 1, j); + var left = minPathSumDFS(grid, i, j - 1); + // 返回從左上角到 (i, j) 的最小路徑代價 + return @min(left, up) + grid[@as(usize, @intCast(i))][@as(usize, @intCast(j))]; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +圖 14-14 給出了以 $dp[2, 1]$ 為根節點的遞迴樹,其中包含一些重疊子問題,其數量會隨著網格 `grid` 的尺寸變大而急劇增多。 + +從本質上看,造成重疊子問題的原因為:**存在多條路徑可以從左上角到達某一單元格**。 + +![暴力搜尋遞迴樹](dp_solution_pipeline.assets/min_path_sum_dfs.png){ class="animation-figure" } + +

圖 14-14   暴力搜尋遞迴樹

+ +每個狀態都有向下和向右兩種選擇,從左上角走到右下角總共需要 $m + n - 2$ 步,所以最差時間複雜度為 $O(2^{m + n})$ 。請注意,這種計算方式未考慮臨近網格邊界的情況,當到達網路邊界時只剩下一種選擇,因此實際的路徑數量會少一些。 + +### 2.   方法二:記憶化搜尋 + +我們引入一個和網格 `grid` 相同尺寸的記憶串列 `mem` ,用於記錄各個子問題的解,並將重疊子問題進行剪枝: + +=== "Python" + + ```python title="min_path_sum.py" + def min_path_sum_dfs_mem( + grid: list[list[int]], mem: list[list[int]], i: int, j: int + ) -> int: + """最小路徑和:記憶化搜尋""" + # 若為左上角單元格,則終止搜尋 + if i == 0 and j == 0: + return grid[0][0] + # 若行列索引越界,則返回 +∞ 代價 + if i < 0 or j < 0: + return inf + # 若已有記錄,則直接返回 + if mem[i][j] != -1: + return mem[i][j] + # 左邊和上邊單元格的最小路徑代價 + up = min_path_sum_dfs_mem(grid, mem, i - 1, j) + left = min_path_sum_dfs_mem(grid, mem, i, j - 1) + # 記錄並返回左上角到 (i, j) 的最小路徑代價 + mem[i][j] = min(left, up) + grid[i][j] + return mem[i][j] + ``` + +=== "C++" + + ```cpp title="min_path_sum.cpp" + /* 最小路徑和:記憶化搜尋 */ + int minPathSumDFSMem(vector> &grid, vector> &mem, int i, int j) { + // 若為左上角單元格,則終止搜尋 + if (i == 0 && j == 0) { + return grid[0][0]; + } + // 若行列索引越界,則返回 +∞ 代價 + if (i < 0 || j < 0) { + return INT_MAX; + } + // 若已有記錄,則直接返回 + if (mem[i][j] != -1) { + return mem[i][j]; + } + // 左邊和上邊單元格的最小路徑代價 + int up = minPathSumDFSMem(grid, mem, i - 1, j); + int left = minPathSumDFSMem(grid, mem, i, j - 1); + // 記錄並返回左上角到 (i, j) 的最小路徑代價 + mem[i][j] = min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX; + return mem[i][j]; + } + ``` + +=== "Java" + + ```java title="min_path_sum.java" + /* 最小路徑和:記憶化搜尋 */ + int minPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) { + // 若為左上角單元格,則終止搜尋 + if (i == 0 && j == 0) { + return grid[0][0]; + } + // 若行列索引越界,則返回 +∞ 代價 + if (i < 0 || j < 0) { + return Integer.MAX_VALUE; + } + // 若已有記錄,則直接返回 + if (mem[i][j] != -1) { + return mem[i][j]; + } + // 左邊和上邊單元格的最小路徑代價 + int up = minPathSumDFSMem(grid, mem, i - 1, j); + int left = minPathSumDFSMem(grid, mem, i, j - 1); + // 記錄並返回左上角到 (i, j) 的最小路徑代價 + mem[i][j] = Math.min(left, up) + grid[i][j]; + return mem[i][j]; + } + ``` + +=== "C#" + + ```csharp title="min_path_sum.cs" + /* 最小路徑和:記憶化搜尋 */ + int MinPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) { + // 若為左上角單元格,則終止搜尋 + if (i == 0 && j == 0) { + return grid[0][0]; + } + // 若行列索引越界,則返回 +∞ 代價 + if (i < 0 || j < 0) { + return int.MaxValue; + } + // 若已有記錄,則直接返回 + if (mem[i][j] != -1) { + return mem[i][j]; + } + // 左邊和上邊單元格的最小路徑代價 + int up = MinPathSumDFSMem(grid, mem, i - 1, j); + int left = MinPathSumDFSMem(grid, mem, i, j - 1); + // 記錄並返回左上角到 (i, j) 的最小路徑代價 + mem[i][j] = Math.Min(left, up) + grid[i][j]; + return mem[i][j]; + } + ``` + +=== "Go" + + ```go title="min_path_sum.go" + /* 最小路徑和:記憶化搜尋 */ + func minPathSumDFSMem(grid, mem [][]int, i, j int) int { + // 若為左上角單元格,則終止搜尋 + if i == 0 && j == 0 { + return grid[0][0] + } + // 若行列索引越界,則返回 +∞ 代價 + if i < 0 || j < 0 { + return math.MaxInt + } + // 若已有記錄,則直接返回 + if mem[i][j] != -1 { + return mem[i][j] + } + // 左邊和上邊單元格的最小路徑代價 + up := minPathSumDFSMem(grid, mem, i-1, j) + left := minPathSumDFSMem(grid, mem, i, j-1) + // 記錄並返回左上角到 (i, j) 的最小路徑代價 + mem[i][j] = int(math.Min(float64(left), float64(up))) + grid[i][j] + return mem[i][j] + } + ``` + +=== "Swift" + + ```swift title="min_path_sum.swift" + /* 最小路徑和:記憶化搜尋 */ + func minPathSumDFSMem(grid: [[Int]], mem: inout [[Int]], i: Int, j: Int) -> Int { + // 若為左上角單元格,則終止搜尋 + if i == 0, j == 0 { + return grid[0][0] + } + // 若行列索引越界,則返回 +∞ 代價 + if i < 0 || j < 0 { + return .max + } + // 若已有記錄,則直接返回 + if mem[i][j] != -1 { + return mem[i][j] + } + // 左邊和上邊單元格的最小路徑代價 + let up = minPathSumDFSMem(grid: grid, mem: &mem, i: i - 1, j: j) + let left = minPathSumDFSMem(grid: grid, mem: &mem, i: i, j: j - 1) + // 記錄並返回左上角到 (i, j) 的最小路徑代價 + mem[i][j] = min(left, up) + grid[i][j] + return mem[i][j] + } + ``` + +=== "JS" + + ```javascript title="min_path_sum.js" + /* 最小路徑和:記憶化搜尋 */ + function minPathSumDFSMem(grid, mem, i, j) { + // 若為左上角單元格,則終止搜尋 + if (i === 0 && j === 0) { + return grid[0][0]; + } + // 若行列索引越界,則返回 +∞ 代價 + if (i < 0 || j < 0) { + return Infinity; + } + // 若已有記錄,則直接返回 + if (mem[i][j] !== -1) { + return mem[i][j]; + } + // 左邊和上邊單元格的最小路徑代價 + const up = minPathSumDFSMem(grid, mem, i - 1, j); + const left = minPathSumDFSMem(grid, mem, i, j - 1); + // 記錄並返回左上角到 (i, j) 的最小路徑代價 + mem[i][j] = Math.min(left, up) + grid[i][j]; + return mem[i][j]; + } + ``` + +=== "TS" + + ```typescript title="min_path_sum.ts" + /* 最小路徑和:記憶化搜尋 */ + function minPathSumDFSMem( + grid: Array>, + mem: Array>, + i: number, + j: number + ): number { + // 若為左上角單元格,則終止搜尋 + if (i === 0 && j === 0) { + return grid[0][0]; + } + // 若行列索引越界,則返回 +∞ 代價 + if (i < 0 || j < 0) { + return Infinity; + } + // 若已有記錄,則直接返回 + if (mem[i][j] != -1) { + return mem[i][j]; + } + // 左邊和上邊單元格的最小路徑代價 + const up = minPathSumDFSMem(grid, mem, i - 1, j); + const left = minPathSumDFSMem(grid, mem, i, j - 1); + // 記錄並返回左上角到 (i, j) 的最小路徑代價 + mem[i][j] = Math.min(left, up) + grid[i][j]; + return mem[i][j]; + } + ``` + +=== "Dart" + + ```dart title="min_path_sum.dart" + /* 最小路徑和:記憶化搜尋 */ + int minPathSumDFSMem(List> grid, List> mem, int i, int j) { + // 若為左上角單元格,則終止搜尋 + if (i == 0 && j == 0) { + return grid[0][0]; + } + // 若行列索引越界,則返回 +∞ 代價 + if (i < 0 || j < 0) { + // 在 Dart 中,int 型別是固定範圍的整數,不存在表示“無窮大”的值 + return BigInt.from(2).pow(31).toInt(); + } + // 若已有記錄,則直接返回 + if (mem[i][j] != -1) { + return mem[i][j]; + } + // 左邊和上邊單元格的最小路徑代價 + int up = minPathSumDFSMem(grid, mem, i - 1, j); + int left = minPathSumDFSMem(grid, mem, i, j - 1); + // 記錄並返回左上角到 (i, j) 的最小路徑代價 + mem[i][j] = min(left, up) + grid[i][j]; + return mem[i][j]; + } + ``` + +=== "Rust" + + ```rust title="min_path_sum.rs" + /* 最小路徑和:記憶化搜尋 */ + fn min_path_sum_dfs_mem(grid: &Vec>, mem: &mut Vec>, i: i32, j: i32) -> i32 { + // 若為左上角單元格,則終止搜尋 + if i == 0 && j == 0 { + return grid[0][0]; + } + // 若行列索引越界,則返回 +∞ 代價 + if i < 0 || j < 0 { + return i32::MAX; + } + // 若已有記錄,則直接返回 + if mem[i as usize][j as usize] != -1 { + return mem[i as usize][j as usize]; + } + // 左邊和上邊單元格的最小路徑代價 + let up = min_path_sum_dfs_mem(grid, mem, i - 1, j); + let left = min_path_sum_dfs_mem(grid, mem, i, j - 1); + // 記錄並返回左上角到 (i, j) 的最小路徑代價 + mem[i as usize][j as usize] = std::cmp::min(left, up) + grid[i as usize][j as usize]; + mem[i as usize][j as usize] + } + ``` + +=== "C" + + ```c title="min_path_sum.c" + /* 最小路徑和:記憶化搜尋 */ + int minPathSumDFSMem(int grid[MAX_SIZE][MAX_SIZE], int mem[MAX_SIZE][MAX_SIZE], int i, int j) { + // 若為左上角單元格,則終止搜尋 + if (i == 0 && j == 0) { + return grid[0][0]; + } + // 若行列索引越界,則返回 +∞ 代價 + if (i < 0 || j < 0) { + return INT_MAX; + } + // 若已有記錄,則直接返回 + if (mem[i][j] != -1) { + return mem[i][j]; + } + // 左邊和上邊單元格的最小路徑代價 + int up = minPathSumDFSMem(grid, mem, i - 1, j); + int left = minPathSumDFSMem(grid, mem, i, j - 1); + // 記錄並返回左上角到 (i, j) 的最小路徑代價 + mem[i][j] = myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX; + return mem[i][j]; + } + ``` + +=== "Kotlin" + + ```kotlin title="min_path_sum.kt" + /* 最小路徑和:記憶化搜尋 */ + fun minPathSumDFSMem( + grid: Array>, + mem: Array>, + i: Int, + j: Int + ): Int { + // 若為左上角單元格,則終止搜尋 + if (i == 0 && j == 0) { + return grid[0][0] + } + // 若行列索引越界,則返回 +∞ 代價 + if (i < 0 || j < 0) { + return Int.MAX_VALUE + } + // 若已有記錄,則直接返回 + if (mem[i][j] != -1) { + return mem[i][j] + } + // 左邊和上邊單元格的最小路徑代價 + val up = minPathSumDFSMem(grid, mem, i - 1, j) + val left = minPathSumDFSMem(grid, mem, i, j - 1) + // 記錄並返回左上角到 (i, j) 的最小路徑代價 + mem[i][j] = (min(left.toDouble(), up.toDouble()) + grid[i][j]).toInt() + return mem[i][j] + } + ``` + +=== "Ruby" + + ```ruby title="min_path_sum.rb" + [class]{}-[func]{min_path_sum_dfs_mem} + ``` + +=== "Zig" + + ```zig title="min_path_sum.zig" + // 最小路徑和:記憶化搜尋 + fn minPathSumDFSMem(grid: anytype, mem: anytype, i: i32, j: i32) i32 { + // 若為左上角單元格,則終止搜尋 + if (i == 0 and j == 0) { + return grid[0][0]; + } + // 若行列索引越界,則返回 +∞ 代價 + if (i < 0 or j < 0) { + return std.math.maxInt(i32); + } + // 若已有記錄,則直接返回 + if (mem[@as(usize, @intCast(i))][@as(usize, @intCast(j))] != -1) { + return mem[@as(usize, @intCast(i))][@as(usize, @intCast(j))]; + } + // 計算從左上角到 (i-1, j) 和 (i, j-1) 的最小路徑代價 + var up = minPathSumDFSMem(grid, mem, i - 1, j); + var left = minPathSumDFSMem(grid, mem, i, j - 1); + // 返回從左上角到 (i, j) 的最小路徑代價 + // 記錄並返回左上角到 (i, j) 的最小路徑代價 + mem[@as(usize, @intCast(i))][@as(usize, @intCast(j))] = @min(left, up) + grid[@as(usize, @intCast(i))][@as(usize, @intCast(j))]; + return mem[@as(usize, @intCast(i))][@as(usize, @intCast(j))]; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +如圖 14-15 所示,在引入記憶化後,所有子問題的解只需計算一次,因此時間複雜度取決於狀態總數,即網格尺寸 $O(nm)$ 。 + +![記憶化搜尋遞迴樹](dp_solution_pipeline.assets/min_path_sum_dfs_mem.png){ class="animation-figure" } + +

圖 14-15   記憶化搜尋遞迴樹

+ +### 3.   方法三:動態規劃 + +基於迭代實現動態規劃解法,程式碼如下所示: + +=== "Python" + + ```python title="min_path_sum.py" + def min_path_sum_dp(grid: list[list[int]]) -> int: + """最小路徑和:動態規劃""" + n, m = len(grid), len(grid[0]) + # 初始化 dp 表 + dp = [[0] * m for _ in range(n)] + dp[0][0] = grid[0][0] + # 狀態轉移:首行 + for j in range(1, m): + dp[0][j] = dp[0][j - 1] + grid[0][j] + # 狀態轉移:首列 + for i in range(1, n): + dp[i][0] = dp[i - 1][0] + grid[i][0] + # 狀態轉移:其餘行和列 + for i in range(1, n): + for j in range(1, m): + dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j] + return dp[n - 1][m - 1] + ``` + +=== "C++" + + ```cpp title="min_path_sum.cpp" + /* 最小路徑和:動態規劃 */ + int minPathSumDP(vector> &grid) { + int n = grid.size(), m = grid[0].size(); + // 初始化 dp 表 + vector> dp(n, vector(m)); + dp[0][0] = grid[0][0]; + // 狀態轉移:首行 + for (int j = 1; j < m; j++) { + dp[0][j] = dp[0][j - 1] + grid[0][j]; + } + // 狀態轉移:首列 + for (int i = 1; i < n; i++) { + dp[i][0] = dp[i - 1][0] + grid[i][0]; + } + // 狀態轉移:其餘行和列 + for (int i = 1; i < n; i++) { + for (int j = 1; j < m; j++) { + dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]; + } + } + return dp[n - 1][m - 1]; + } + ``` + +=== "Java" + + ```java title="min_path_sum.java" + /* 最小路徑和:動態規劃 */ + int minPathSumDP(int[][] grid) { + int n = grid.length, m = grid[0].length; + // 初始化 dp 表 + int[][] dp = new int[n][m]; + dp[0][0] = grid[0][0]; + // 狀態轉移:首行 + for (int j = 1; j < m; j++) { + dp[0][j] = dp[0][j - 1] + grid[0][j]; + } + // 狀態轉移:首列 + for (int i = 1; i < n; i++) { + dp[i][0] = dp[i - 1][0] + grid[i][0]; + } + // 狀態轉移:其餘行和列 + for (int i = 1; i < n; i++) { + for (int j = 1; j < m; j++) { + dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]; + } + } + return dp[n - 1][m - 1]; + } + ``` + +=== "C#" + + ```csharp title="min_path_sum.cs" + /* 最小路徑和:動態規劃 */ + int MinPathSumDP(int[][] grid) { + int n = grid.Length, m = grid[0].Length; + // 初始化 dp 表 + int[,] dp = new int[n, m]; + dp[0, 0] = grid[0][0]; + // 狀態轉移:首行 + for (int j = 1; j < m; j++) { + dp[0, j] = dp[0, j - 1] + grid[0][j]; + } + // 狀態轉移:首列 + for (int i = 1; i < n; i++) { + dp[i, 0] = dp[i - 1, 0] + grid[i][0]; + } + // 狀態轉移:其餘行和列 + for (int i = 1; i < n; i++) { + for (int j = 1; j < m; j++) { + dp[i, j] = Math.Min(dp[i, j - 1], dp[i - 1, j]) + grid[i][j]; + } + } + return dp[n - 1, m - 1]; + } + ``` + +=== "Go" + + ```go title="min_path_sum.go" + /* 最小路徑和:動態規劃 */ + func minPathSumDP(grid [][]int) int { + n, m := len(grid), len(grid[0]) + // 初始化 dp 表 + dp := make([][]int, n) + for i := 0; i < n; i++ { + dp[i] = make([]int, m) + } + dp[0][0] = grid[0][0] + // 狀態轉移:首行 + for j := 1; j < m; j++ { + dp[0][j] = dp[0][j-1] + grid[0][j] + } + // 狀態轉移:首列 + for i := 1; i < n; i++ { + dp[i][0] = dp[i-1][0] + grid[i][0] + } + // 狀態轉移:其餘行和列 + for i := 1; i < n; i++ { + for j := 1; j < m; j++ { + dp[i][j] = int(math.Min(float64(dp[i][j-1]), float64(dp[i-1][j]))) + grid[i][j] + } + } + return dp[n-1][m-1] + } + ``` + +=== "Swift" + + ```swift title="min_path_sum.swift" + /* 最小路徑和:動態規劃 */ + func minPathSumDP(grid: [[Int]]) -> Int { + let n = grid.count + let m = grid[0].count + // 初始化 dp 表 + var dp = Array(repeating: Array(repeating: 0, count: m), count: n) + dp[0][0] = grid[0][0] + // 狀態轉移:首行 + for j in 1 ..< m { + dp[0][j] = dp[0][j - 1] + grid[0][j] + } + // 狀態轉移:首列 + for i in 1 ..< n { + dp[i][0] = dp[i - 1][0] + grid[i][0] + } + // 狀態轉移:其餘行和列 + for i in 1 ..< n { + for j in 1 ..< m { + dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j] + } + } + return dp[n - 1][m - 1] + } + ``` + +=== "JS" + + ```javascript title="min_path_sum.js" + /* 最小路徑和:動態規劃 */ + function minPathSumDP(grid) { + const n = grid.length, + m = grid[0].length; + // 初始化 dp 表 + const dp = Array.from({ length: n }, () => + Array.from({ length: m }, () => 0) + ); + dp[0][0] = grid[0][0]; + // 狀態轉移:首行 + for (let j = 1; j < m; j++) { + dp[0][j] = dp[0][j - 1] + grid[0][j]; + } + // 狀態轉移:首列 + for (let i = 1; i < n; i++) { + dp[i][0] = dp[i - 1][0] + grid[i][0]; + } + // 狀態轉移:其餘行和列 + for (let i = 1; i < n; i++) { + for (let j = 1; j < m; j++) { + dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]; + } + } + return dp[n - 1][m - 1]; + } + ``` + +=== "TS" + + ```typescript title="min_path_sum.ts" + /* 最小路徑和:動態規劃 */ + function minPathSumDP(grid: Array>): number { + const n = grid.length, + m = grid[0].length; + // 初始化 dp 表 + const dp = Array.from({ length: n }, () => + Array.from({ length: m }, () => 0) + ); + dp[0][0] = grid[0][0]; + // 狀態轉移:首行 + for (let j = 1; j < m; j++) { + dp[0][j] = dp[0][j - 1] + grid[0][j]; + } + // 狀態轉移:首列 + for (let i = 1; i < n; i++) { + dp[i][0] = dp[i - 1][0] + grid[i][0]; + } + // 狀態轉移:其餘行和列 + for (let i = 1; i < n; i++) { + for (let j: number = 1; j < m; j++) { + dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]; + } + } + return dp[n - 1][m - 1]; + } + ``` + +=== "Dart" + + ```dart title="min_path_sum.dart" + /* 最小路徑和:動態規劃 */ + int minPathSumDP(List> grid) { + int n = grid.length, m = grid[0].length; + // 初始化 dp 表 + List> dp = List.generate(n, (i) => List.filled(m, 0)); + dp[0][0] = grid[0][0]; + // 狀態轉移:首行 + for (int j = 1; j < m; j++) { + dp[0][j] = dp[0][j - 1] + grid[0][j]; + } + // 狀態轉移:首列 + for (int i = 1; i < n; i++) { + dp[i][0] = dp[i - 1][0] + grid[i][0]; + } + // 狀態轉移:其餘行和列 + for (int i = 1; i < n; i++) { + for (int j = 1; j < m; j++) { + dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]; + } + } + return dp[n - 1][m - 1]; + } + ``` + +=== "Rust" + + ```rust title="min_path_sum.rs" + /* 最小路徑和:動態規劃 */ + fn min_path_sum_dp(grid: &Vec>) -> i32 { + let (n, m) = (grid.len(), grid[0].len()); + // 初始化 dp 表 + let mut dp = vec![vec![0; m]; n]; + dp[0][0] = grid[0][0]; + // 狀態轉移:首行 + for j in 1..m { + dp[0][j] = dp[0][j - 1] + grid[0][j]; + } + // 狀態轉移:首列 + for i in 1..n { + dp[i][0] = dp[i - 1][0] + grid[i][0]; + } + // 狀態轉移:其餘行和列 + for i in 1..n { + for j in 1..m { + dp[i][j] = std::cmp::min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]; + } + } + dp[n - 1][m - 1] + } + ``` + +=== "C" + + ```c title="min_path_sum.c" + /* 最小路徑和:動態規劃 */ + int minPathSumDP(int grid[MAX_SIZE][MAX_SIZE], int n, int m) { + // 初始化 dp 表 + int **dp = malloc(n * sizeof(int *)); + for (int i = 0; i < n; i++) { + dp[i] = calloc(m, sizeof(int)); + } + dp[0][0] = grid[0][0]; + // 狀態轉移:首行 + for (int j = 1; j < m; j++) { + dp[0][j] = dp[0][j - 1] + grid[0][j]; + } + // 狀態轉移:首列 + for (int i = 1; i < n; i++) { + dp[i][0] = dp[i - 1][0] + grid[i][0]; + } + // 狀態轉移:其餘行和列 + for (int i = 1; i < n; i++) { + for (int j = 1; j < m; j++) { + dp[i][j] = myMin(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]; + } + } + int res = dp[n - 1][m - 1]; + // 釋放記憶體 + for (int i = 0; i < n; i++) { + free(dp[i]); + } + return res; + } + ``` + +=== "Kotlin" + + ```kotlin title="min_path_sum.kt" + /* 最小路徑和:動態規劃 */ + fun minPathSumDP(grid: Array>): Int { + val n = grid.size + val m = grid[0].size + // 初始化 dp 表 + val dp = Array(n) { IntArray(m) } + dp[0][0] = grid[0][0] + // 狀態轉移:首行 + for (j in 1..
+
全螢幕觀看 >
+ +圖 14-16 展示了最小路徑和的狀態轉移過程,其走訪了整個網格,**因此時間複雜度為 $O(nm)$** 。 + +陣列 `dp` 大小為 $n \times m$ ,**因此空間複雜度為 $O(nm)$** 。 + +=== "<1>" + ![最小路徑和的動態規劃過程](dp_solution_pipeline.assets/min_path_sum_dp_step1.png){ class="animation-figure" } + +=== "<2>" + ![min_path_sum_dp_step2](dp_solution_pipeline.assets/min_path_sum_dp_step2.png){ class="animation-figure" } + +=== "<3>" + ![min_path_sum_dp_step3](dp_solution_pipeline.assets/min_path_sum_dp_step3.png){ class="animation-figure" } + +=== "<4>" + ![min_path_sum_dp_step4](dp_solution_pipeline.assets/min_path_sum_dp_step4.png){ class="animation-figure" } + +=== "<5>" + ![min_path_sum_dp_step5](dp_solution_pipeline.assets/min_path_sum_dp_step5.png){ class="animation-figure" } + +=== "<6>" + ![min_path_sum_dp_step6](dp_solution_pipeline.assets/min_path_sum_dp_step6.png){ class="animation-figure" } + +=== "<7>" + ![min_path_sum_dp_step7](dp_solution_pipeline.assets/min_path_sum_dp_step7.png){ class="animation-figure" } + +=== "<8>" + ![min_path_sum_dp_step8](dp_solution_pipeline.assets/min_path_sum_dp_step8.png){ class="animation-figure" } + +=== "<9>" + ![min_path_sum_dp_step9](dp_solution_pipeline.assets/min_path_sum_dp_step9.png){ class="animation-figure" } + +=== "<10>" + ![min_path_sum_dp_step10](dp_solution_pipeline.assets/min_path_sum_dp_step10.png){ class="animation-figure" } + +=== "<11>" + ![min_path_sum_dp_step11](dp_solution_pipeline.assets/min_path_sum_dp_step11.png){ class="animation-figure" } + +=== "<12>" + ![min_path_sum_dp_step12](dp_solution_pipeline.assets/min_path_sum_dp_step12.png){ class="animation-figure" } + +

圖 14-16   最小路徑和的動態規劃過程

+ +### 4.   空間最佳化 + +由於每個格子只與其左邊和上邊的格子有關,因此我們可以只用一個單行陣列來實現 $dp$ 表。 + +請注意,因為陣列 `dp` 只能表示一行的狀態,所以我們無法提前初始化首列狀態,而是在走訪每行時更新它: + +=== "Python" + + ```python title="min_path_sum.py" + def min_path_sum_dp_comp(grid: list[list[int]]) -> int: + """最小路徑和:空間最佳化後的動態規劃""" + n, m = len(grid), len(grid[0]) + # 初始化 dp 表 + dp = [0] * m + # 狀態轉移:首行 + dp[0] = grid[0][0] + for j in range(1, m): + dp[j] = dp[j - 1] + grid[0][j] + # 狀態轉移:其餘行 + for i in range(1, n): + # 狀態轉移:首列 + dp[0] = dp[0] + grid[i][0] + # 狀態轉移:其餘列 + for j in range(1, m): + dp[j] = min(dp[j - 1], dp[j]) + grid[i][j] + return dp[m - 1] + ``` + +=== "C++" + + ```cpp title="min_path_sum.cpp" + /* 最小路徑和:空間最佳化後的動態規劃 */ + int minPathSumDPComp(vector> &grid) { + int n = grid.size(), m = grid[0].size(); + // 初始化 dp 表 + vector dp(m); + // 狀態轉移:首行 + dp[0] = grid[0][0]; + for (int j = 1; j < m; j++) { + dp[j] = dp[j - 1] + grid[0][j]; + } + // 狀態轉移:其餘行 + for (int i = 1; i < n; i++) { + // 狀態轉移:首列 + dp[0] = dp[0] + grid[i][0]; + // 狀態轉移:其餘列 + for (int j = 1; j < m; j++) { + dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]; + } + } + return dp[m - 1]; + } + ``` + +=== "Java" + + ```java title="min_path_sum.java" + /* 最小路徑和:空間最佳化後的動態規劃 */ + int minPathSumDPComp(int[][] grid) { + int n = grid.length, m = grid[0].length; + // 初始化 dp 表 + int[] dp = new int[m]; + // 狀態轉移:首行 + dp[0] = grid[0][0]; + for (int j = 1; j < m; j++) { + dp[j] = dp[j - 1] + grid[0][j]; + } + // 狀態轉移:其餘行 + for (int i = 1; i < n; i++) { + // 狀態轉移:首列 + dp[0] = dp[0] + grid[i][0]; + // 狀態轉移:其餘列 + for (int j = 1; j < m; j++) { + dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j]; + } + } + return dp[m - 1]; + } + ``` + +=== "C#" + + ```csharp title="min_path_sum.cs" + /* 最小路徑和:空間最佳化後的動態規劃 */ + int MinPathSumDPComp(int[][] grid) { + int n = grid.Length, m = grid[0].Length; + // 初始化 dp 表 + int[] dp = new int[m]; + dp[0] = grid[0][0]; + // 狀態轉移:首行 + for (int j = 1; j < m; j++) { + dp[j] = dp[j - 1] + grid[0][j]; + } + // 狀態轉移:其餘行 + for (int i = 1; i < n; i++) { + // 狀態轉移:首列 + dp[0] = dp[0] + grid[i][0]; + // 狀態轉移:其餘列 + for (int j = 1; j < m; j++) { + dp[j] = Math.Min(dp[j - 1], dp[j]) + grid[i][j]; + } + } + return dp[m - 1]; + } + ``` + +=== "Go" + + ```go title="min_path_sum.go" + /* 最小路徑和:空間最佳化後的動態規劃 */ + func minPathSumDPComp(grid [][]int) int { + n, m := len(grid), len(grid[0]) + // 初始化 dp 表 + dp := make([]int, m) + // 狀態轉移:首行 + dp[0] = grid[0][0] + for j := 1; j < m; j++ { + dp[j] = dp[j-1] + grid[0][j] + } + // 狀態轉移:其餘行和列 + for i := 1; i < n; i++ { + // 狀態轉移:首列 + dp[0] = dp[0] + grid[i][0] + // 狀態轉移:其餘列 + for j := 1; j < m; j++ { + dp[j] = int(math.Min(float64(dp[j-1]), float64(dp[j]))) + grid[i][j] + } + } + return dp[m-1] + } + ``` + +=== "Swift" + + ```swift title="min_path_sum.swift" + /* 最小路徑和:空間最佳化後的動態規劃 */ + func minPathSumDPComp(grid: [[Int]]) -> Int { + let n = grid.count + let m = grid[0].count + // 初始化 dp 表 + var dp = Array(repeating: 0, count: m) + // 狀態轉移:首行 + dp[0] = grid[0][0] + for j in 1 ..< m { + dp[j] = dp[j - 1] + grid[0][j] + } + // 狀態轉移:其餘行 + for i in 1 ..< n { + // 狀態轉移:首列 + dp[0] = dp[0] + grid[i][0] + // 狀態轉移:其餘列 + for j in 1 ..< m { + dp[j] = min(dp[j - 1], dp[j]) + grid[i][j] + } + } + return dp[m - 1] + } + ``` + +=== "JS" + + ```javascript title="min_path_sum.js" + /* 最小路徑和:狀態壓縮後的動態規劃 */ + function minPathSumDPComp(grid) { + const n = grid.length, + m = grid[0].length; + // 初始化 dp 表 + const dp = new Array(m); + // 狀態轉移:首行 + dp[0] = grid[0][0]; + for (let j = 1; j < m; j++) { + dp[j] = dp[j - 1] + grid[0][j]; + } + // 狀態轉移:其餘行 + for (let i = 1; i < n; i++) { + // 狀態轉移:首列 + dp[0] = dp[0] + grid[i][0]; + // 狀態轉移:其餘列 + for (let j = 1; j < m; j++) { + dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j]; + } + } + return dp[m - 1]; + } + ``` + +=== "TS" + + ```typescript title="min_path_sum.ts" + /* 最小路徑和:狀態壓縮後的動態規劃 */ + function minPathSumDPComp(grid: Array>): number { + const n = grid.length, + m = grid[0].length; + // 初始化 dp 表 + const dp = new Array(m); + // 狀態轉移:首行 + dp[0] = grid[0][0]; + for (let j = 1; j < m; j++) { + dp[j] = dp[j - 1] + grid[0][j]; + } + // 狀態轉移:其餘行 + for (let i = 1; i < n; i++) { + // 狀態轉移:首列 + dp[0] = dp[0] + grid[i][0]; + // 狀態轉移:其餘列 + for (let j = 1; j < m; j++) { + dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j]; + } + } + return dp[m - 1]; + } + ``` + +=== "Dart" + + ```dart title="min_path_sum.dart" + /* 最小路徑和:空間最佳化後的動態規劃 */ + int minPathSumDPComp(List> grid) { + int n = grid.length, m = grid[0].length; + // 初始化 dp 表 + List dp = List.filled(m, 0); + dp[0] = grid[0][0]; + for (int j = 1; j < m; j++) { + dp[j] = dp[j - 1] + grid[0][j]; + } + // 狀態轉移:其餘行 + for (int i = 1; i < n; i++) { + // 狀態轉移:首列 + dp[0] = dp[0] + grid[i][0]; + // 狀態轉移:其餘列 + for (int j = 1; j < m; j++) { + dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]; + } + } + return dp[m - 1]; + } + ``` + +=== "Rust" + + ```rust title="min_path_sum.rs" + /* 最小路徑和:空間最佳化後的動態規劃 */ + fn min_path_sum_dp_comp(grid: &Vec>) -> i32 { + let (n, m) = (grid.len(), grid[0].len()); + // 初始化 dp 表 + let mut dp = vec![0; m]; + // 狀態轉移:首行 + dp[0] = grid[0][0]; + for j in 1..m { + dp[j] = dp[j - 1] + grid[0][j]; + } + // 狀態轉移:其餘行 + for i in 1..n { + // 狀態轉移:首列 + dp[0] = dp[0] + grid[i][0]; + // 狀態轉移:其餘列 + for j in 1..m { + dp[j] = std::cmp::min(dp[j - 1], dp[j]) + grid[i][j]; + } + } + dp[m - 1] + } + ``` + +=== "C" + + ```c title="min_path_sum.c" + /* 最小路徑和:空間最佳化後的動態規劃 */ + int minPathSumDPComp(int grid[MAX_SIZE][MAX_SIZE], int n, int m) { + // 初始化 dp 表 + int *dp = calloc(m, sizeof(int)); + // 狀態轉移:首行 + dp[0] = grid[0][0]; + for (int j = 1; j < m; j++) { + dp[j] = dp[j - 1] + grid[0][j]; + } + // 狀態轉移:其餘行 + for (int i = 1; i < n; i++) { + // 狀態轉移:首列 + dp[0] = dp[0] + grid[i][0]; + // 狀態轉移:其餘列 + for (int j = 1; j < m; j++) { + dp[j] = myMin(dp[j - 1], dp[j]) + grid[i][j]; + } + } + int res = dp[m - 1]; + // 釋放記憶體 + free(dp); + return res; + } + ``` + +=== "Kotlin" + + ```kotlin title="min_path_sum.kt" + /* 最小路徑和:空間最佳化後的動態規劃 */ + fun minPathSumDPComp(grid: Array>): Int { + val n = grid.size + val m = grid[0].size + // 初始化 dp 表 + val dp = IntArray(m) + // 狀態轉移:首行 + dp[0] = grid[0][0] + for (j in 1.. + diff --git a/zh-Hant/docs/chapter_dynamic_programming/edit_distance_problem.md b/zh-Hant/docs/chapter_dynamic_programming/edit_distance_problem.md new file mode 100644 index 000000000..0c0298850 --- /dev/null +++ b/zh-Hant/docs/chapter_dynamic_programming/edit_distance_problem.md @@ -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 次刪除操作。 + +![編輯距離的示例資料](edit_distance_problem.assets/edit_distance_example.png){ class="animation-figure" } + +

圖 14-27   編輯距離的示例資料

+ +**編輯距離問題可以很自然地用決策樹模型來解釋**。字串對應樹節點,一輪決策(一次編輯操作)對應樹的一條邊。 + +如圖 14-28 所示,在不限制操作的情況下,每個節點都可以派生出許多條邊,每條邊對應一種操作,這意味著從 `hello` 轉換到 `algo` 有許多種可能的路徑。 + +從決策樹的角度看,本題的目標是求解節點 `hello` 和節點 `algo` 之間的最短路徑。 + +![基於決策樹模型表示編輯距離問題](edit_distance_problem.assets/edit_distance_decision_tree.png){ class="animation-figure" } + +

圖 14-28   基於決策樹模型表示編輯距離問題

+ +### 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]$ 。 + +![編輯距離的狀態轉移](edit_distance_problem.assets/edit_distance_state_transfer.png){ class="animation-figure" } + +

圖 14-29   編輯距離的狀態轉移

+ +根據以上分析,可得最優子結構:$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> dp(n + 1, vector(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> 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 "視覺化執行" + +
+ + +如圖 14-30 所示,編輯距離問題的狀態轉移過程與背包問題非常類似,都可以看作填寫一個二維網格的過程。 + +=== "<1>" + ![編輯距離的動態規劃過程](edit_distance_problem.assets/edit_distance_dp_step1.png){ class="animation-figure" } + +=== "<2>" + ![edit_distance_dp_step2](edit_distance_problem.assets/edit_distance_dp_step2.png){ class="animation-figure" } + +=== "<3>" + ![edit_distance_dp_step3](edit_distance_problem.assets/edit_distance_dp_step3.png){ class="animation-figure" } + +=== "<4>" + ![edit_distance_dp_step4](edit_distance_problem.assets/edit_distance_dp_step4.png){ class="animation-figure" } + +=== "<5>" + ![edit_distance_dp_step5](edit_distance_problem.assets/edit_distance_dp_step5.png){ class="animation-figure" } + +=== "<6>" + ![edit_distance_dp_step6](edit_distance_problem.assets/edit_distance_dp_step6.png){ class="animation-figure" } + +=== "<7>" + ![edit_distance_dp_step7](edit_distance_problem.assets/edit_distance_dp_step7.png){ class="animation-figure" } + +=== "<8>" + ![edit_distance_dp_step8](edit_distance_problem.assets/edit_distance_dp_step8.png){ class="animation-figure" } + +=== "<9>" + ![edit_distance_dp_step9](edit_distance_problem.assets/edit_distance_dp_step9.png){ class="animation-figure" } + +=== "<10>" + ![edit_distance_dp_step10](edit_distance_problem.assets/edit_distance_dp_step10.png){ class="animation-figure" } + +=== "<11>" + ![edit_distance_dp_step11](edit_distance_problem.assets/edit_distance_dp_step11.png){ class="animation-figure" } + +=== "<12>" + ![edit_distance_dp_step12](edit_distance_problem.assets/edit_distance_dp_step12.png){ class="animation-figure" } + +=== "<13>" + ![edit_distance_dp_step13](edit_distance_problem.assets/edit_distance_dp_step13.png){ class="animation-figure" } + +=== "<14>" + ![edit_distance_dp_step14](edit_distance_problem.assets/edit_distance_dp_step14.png){ class="animation-figure" } + +=== "<15>" + ![edit_distance_dp_step15](edit_distance_problem.assets/edit_distance_dp_step15.png){ class="animation-figure" } + +

圖 14-30   編輯距離的動態規劃過程

+ +### 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 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 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 "視覺化執行" + +
+ diff --git a/zh-Hant/docs/chapter_dynamic_programming/index.md b/zh-Hant/docs/chapter_dynamic_programming/index.md new file mode 100644 index 000000000..9d365ff0d --- /dev/null +++ b/zh-Hant/docs/chapter_dynamic_programming/index.md @@ -0,0 +1,24 @@ +--- +comments: true +icon: material/table-pivot +--- + +# 第 14 章   動態規劃 + +![動態規劃](../assets/covers/chapter_dynamic_programming.jpg){ 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/) diff --git a/zh-Hant/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md b/zh-Hant/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md new file mode 100644 index 000000000..1b2c87484 --- /dev/null +++ b/zh-Hant/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md @@ -0,0 +1,1639 @@ +--- +comments: true +--- + +# 14.1   初探動態規劃 + +動態規劃(dynamic programming)是一個重要的演算法範式,它將一個問題分解為一系列更小的子問題,並透過儲存子問題的解來避免重複計算,從而大幅提升時間效率。 + +在本節中,我們從一個經典例題入手,先給出它的暴力回溯解法,觀察其中包含的重疊子問題,再逐步導出更高效的動態規劃解法。 + +!!! question "爬樓梯" + + 給定一個共有 $n$ 階的樓梯,你每步可以上 $1$ 階或者 $2$ 階,請問有多少種方案可以爬到樓頂? + +如圖 14-1 所示,對於一個 $3$ 階樓梯,共有 $3$ 種方案可以爬到樓頂。 + +![爬到第 3 階的方案數量](intro_to_dynamic_programming.assets/climbing_stairs_example.png){ class="animation-figure" } + +

圖 14-1   爬到第 3 階的方案數量

+ +本題的目標是求解方案數量,**我們可以考慮透過回溯來窮舉所有可能性**。具體來說,將爬樓梯想象為一個多輪選擇的過程:從地面出發,每輪選擇上 $1$ 階或 $2$ 階,每當到達樓梯頂部時就將方案數量加 $1$ ,當越過樓梯頂部時就將其剪枝。程式碼如下所示: + +=== "Python" + + ```python title="climbing_stairs_backtrack.py" + def backtrack(choices: list[int], state: int, n: int, res: list[int]) -> int: + """回溯""" + # 當爬到第 n 階時,方案數量加 1 + if state == n: + res[0] += 1 + # 走訪所有選擇 + for choice in choices: + # 剪枝:不允許越過第 n 階 + if state + choice > n: + continue + # 嘗試:做出選擇,更新狀態 + backtrack(choices, state + choice, n, res) + # 回退 + + def climbing_stairs_backtrack(n: int) -> int: + """爬樓梯:回溯""" + choices = [1, 2] # 可選擇向上爬 1 階或 2 階 + state = 0 # 從第 0 階開始爬 + res = [0] # 使用 res[0] 記錄方案數量 + backtrack(choices, state, n, res) + return res[0] + ``` + +=== "C++" + + ```cpp title="climbing_stairs_backtrack.cpp" + /* 回溯 */ + void backtrack(vector &choices, int state, int n, vector &res) { + // 當爬到第 n 階時,方案數量加 1 + if (state == n) + res[0]++; + // 走訪所有選擇 + for (auto &choice : choices) { + // 剪枝:不允許越過第 n 階 + if (state + choice > n) + continue; + // 嘗試:做出選擇,更新狀態 + backtrack(choices, state + choice, n, res); + // 回退 + } + } + + /* 爬樓梯:回溯 */ + int climbingStairsBacktrack(int n) { + vector choices = {1, 2}; // 可選擇向上爬 1 階或 2 階 + int state = 0; // 從第 0 階開始爬 + vector res = {0}; // 使用 res[0] 記錄方案數量 + backtrack(choices, state, n, res); + return res[0]; + } + ``` + +=== "Java" + + ```java title="climbing_stairs_backtrack.java" + /* 回溯 */ + void backtrack(List choices, int state, int n, List res) { + // 當爬到第 n 階時,方案數量加 1 + if (state == n) + res.set(0, res.get(0) + 1); + // 走訪所有選擇 + for (Integer choice : choices) { + // 剪枝:不允許越過第 n 階 + if (state + choice > n) + continue; + // 嘗試:做出選擇,更新狀態 + backtrack(choices, state + choice, n, res); + // 回退 + } + } + + /* 爬樓梯:回溯 */ + int climbingStairsBacktrack(int n) { + List choices = Arrays.asList(1, 2); // 可選擇向上爬 1 階或 2 階 + int state = 0; // 從第 0 階開始爬 + List res = new ArrayList<>(); + res.add(0); // 使用 res[0] 記錄方案數量 + backtrack(choices, state, n, res); + return res.get(0); + } + ``` + +=== "C#" + + ```csharp title="climbing_stairs_backtrack.cs" + /* 回溯 */ + void Backtrack(List choices, int state, int n, List res) { + // 當爬到第 n 階時,方案數量加 1 + if (state == n) + res[0]++; + // 走訪所有選擇 + foreach (int choice in choices) { + // 剪枝:不允許越過第 n 階 + if (state + choice > n) + continue; + // 嘗試:做出選擇,更新狀態 + Backtrack(choices, state + choice, n, res); + // 回退 + } + } + + /* 爬樓梯:回溯 */ + int ClimbingStairsBacktrack(int n) { + List choices = [1, 2]; // 可選擇向上爬 1 階或 2 階 + int state = 0; // 從第 0 階開始爬 + List res = [0]; // 使用 res[0] 記錄方案數量 + Backtrack(choices, state, n, res); + return res[0]; + } + ``` + +=== "Go" + + ```go title="climbing_stairs_backtrack.go" + /* 回溯 */ + func backtrack(choices []int, state, n int, res []int) { + // 當爬到第 n 階時,方案數量加 1 + if state == n { + res[0] = res[0] + 1 + } + // 走訪所有選擇 + for _, choice := range choices { + // 剪枝:不允許越過第 n 階 + if state+choice > n { + continue + } + // 嘗試:做出選擇,更新狀態 + backtrack(choices, state+choice, n, res) + // 回退 + } + } + + /* 爬樓梯:回溯 */ + func climbingStairsBacktrack(n int) int { + // 可選擇向上爬 1 階或 2 階 + choices := []int{1, 2} + // 從第 0 階開始爬 + state := 0 + res := make([]int, 1) + // 使用 res[0] 記錄方案數量 + res[0] = 0 + backtrack(choices, state, n, res) + return res[0] + } + ``` + +=== "Swift" + + ```swift title="climbing_stairs_backtrack.swift" + /* 回溯 */ + func backtrack(choices: [Int], state: Int, n: Int, res: inout [Int]) { + // 當爬到第 n 階時,方案數量加 1 + if state == n { + res[0] += 1 + } + // 走訪所有選擇 + for choice in choices { + // 剪枝:不允許越過第 n 階 + if state + choice > n { + continue + } + backtrack(choices: choices, state: state + choice, n: n, res: &res) + } + } + + /* 爬樓梯:回溯 */ + func climbingStairsBacktrack(n: Int) -> Int { + let choices = [1, 2] // 可選擇向上爬 1 階或 2 階 + let state = 0 // 從第 0 階開始爬 + var res: [Int] = [] + res.append(0) // 使用 res[0] 記錄方案數量 + backtrack(choices: choices, state: state, n: n, res: &res) + return res[0] + } + ``` + +=== "JS" + + ```javascript title="climbing_stairs_backtrack.js" + /* 回溯 */ + function backtrack(choices, state, n, res) { + // 當爬到第 n 階時,方案數量加 1 + if (state === n) res.set(0, res.get(0) + 1); + // 走訪所有選擇 + for (const choice of choices) { + // 剪枝:不允許越過第 n 階 + if (state + choice > n) continue; + // 嘗試:做出選擇,更新狀態 + backtrack(choices, state + choice, n, res); + // 回退 + } + } + + /* 爬樓梯:回溯 */ + function climbingStairsBacktrack(n) { + const choices = [1, 2]; // 可選擇向上爬 1 階或 2 階 + const state = 0; // 從第 0 階開始爬 + const res = new Map(); + res.set(0, 0); // 使用 res[0] 記錄方案數量 + backtrack(choices, state, n, res); + return res.get(0); + } + ``` + +=== "TS" + + ```typescript title="climbing_stairs_backtrack.ts" + /* 回溯 */ + function backtrack( + choices: number[], + state: number, + n: number, + res: Map<0, any> + ): void { + // 當爬到第 n 階時,方案數量加 1 + if (state === n) res.set(0, res.get(0) + 1); + // 走訪所有選擇 + for (const choice of choices) { + // 剪枝:不允許越過第 n 階 + if (state + choice > n) continue; + // 嘗試:做出選擇,更新狀態 + backtrack(choices, state + choice, n, res); + // 回退 + } + } + + /* 爬樓梯:回溯 */ + function climbingStairsBacktrack(n: number): number { + const choices = [1, 2]; // 可選擇向上爬 1 階或 2 階 + const state = 0; // 從第 0 階開始爬 + const res = new Map(); + res.set(0, 0); // 使用 res[0] 記錄方案數量 + backtrack(choices, state, n, res); + return res.get(0); + } + ``` + +=== "Dart" + + ```dart title="climbing_stairs_backtrack.dart" + /* 回溯 */ + void backtrack(List choices, int state, int n, List res) { + // 當爬到第 n 階時,方案數量加 1 + if (state == n) { + res[0]++; + } + // 走訪所有選擇 + for (int choice in choices) { + // 剪枝:不允許越過第 n 階 + if (state + choice > n) continue; + // 嘗試:做出選擇,更新狀態 + backtrack(choices, state + choice, n, res); + // 回退 + } + } + + /* 爬樓梯:回溯 */ + int climbingStairsBacktrack(int n) { + List choices = [1, 2]; // 可選擇向上爬 1 階或 2 階 + int state = 0; // 從第 0 階開始爬 + List res = []; + res.add(0); // 使用 res[0] 記錄方案數量 + backtrack(choices, state, n, res); + return res[0]; + } + ``` + +=== "Rust" + + ```rust title="climbing_stairs_backtrack.rs" + /* 回溯 */ + fn backtrack(choices: &[i32], state: i32, n: i32, res: &mut [i32]) { + // 當爬到第 n 階時,方案數量加 1 + if state == n { + res[0] = res[0] + 1; + } + // 走訪所有選擇 + for &choice in choices { + // 剪枝:不允許越過第 n 階 + if state + choice > n { + continue; + } + // 嘗試:做出選擇,更新狀態 + backtrack(choices, state + choice, n, res); + // 回退 + } + } + + /* 爬樓梯:回溯 */ + fn climbing_stairs_backtrack(n: usize) -> i32 { + let choices = vec![1, 2]; // 可選擇向上爬 1 階或 2 階 + let state = 0; // 從第 0 階開始爬 + let mut res = Vec::new(); + res.push(0); // 使用 res[0] 記錄方案數量 + backtrack(&choices, state, n as i32, &mut res); + res[0] + } + ``` + +=== "C" + + ```c title="climbing_stairs_backtrack.c" + /* 回溯 */ + void backtrack(int *choices, int state, int n, int *res, int len) { + // 當爬到第 n 階時,方案數量加 1 + if (state == n) + res[0]++; + // 走訪所有選擇 + for (int i = 0; i < len; i++) { + int choice = choices[i]; + // 剪枝:不允許越過第 n 階 + if (state + choice > n) + continue; + // 嘗試:做出選擇,更新狀態 + backtrack(choices, state + choice, n, res, len); + // 回退 + } + } + + /* 爬樓梯:回溯 */ + int climbingStairsBacktrack(int n) { + int choices[2] = {1, 2}; // 可選擇向上爬 1 階或 2 階 + int state = 0; // 從第 0 階開始爬 + int *res = (int *)malloc(sizeof(int)); + *res = 0; // 使用 res[0] 記錄方案數量 + int len = sizeof(choices) / sizeof(int); + backtrack(choices, state, n, res, len); + int result = *res; + free(res); + return result; + } + ``` + +=== "Kotlin" + + ```kotlin title="climbing_stairs_backtrack.kt" + /* 回溯 */ + fun backtrack( + choices: List, + state: Int, + n: Int, + res: MutableList + ) { + // 當爬到第 n 階時,方案數量加 1 + if (state == n) res[0] = res[0] + 1 + // 走訪所有選擇 + for (choice in choices) { + // 剪枝:不允許越過第 n 階 + if (state + choice > n) continue + // 嘗試:做出選擇,更新狀態 + backtrack(choices, state + choice, n, res) + // 回退 + } + } + + /* 爬樓梯:回溯 */ + fun climbingStairsBacktrack(n: Int): Int { + val choices = mutableListOf(1, 2) // 可選擇向上爬 1 階或 2 階 + val state = 0 // 從第 0 階開始爬 + val res = ArrayList() + res.add(0) // 使用 res[0] 記錄方案數量 + backtrack(choices, state, n, res) + return res[0] + } + ``` + +=== "Ruby" + + ```ruby title="climbing_stairs_backtrack.rb" + [class]{}-[func]{backtrack} + + [class]{}-[func]{climbing_stairs_backtrack} + ``` + +=== "Zig" + + ```zig title="climbing_stairs_backtrack.zig" + // 回溯 + fn backtrack(choices: []i32, state: i32, n: i32, res: std.ArrayList(i32)) void { + // 當爬到第 n 階時,方案數量加 1 + if (state == n) { + res.items[0] = res.items[0] + 1; + } + // 走訪所有選擇 + for (choices) |choice| { + // 剪枝:不允許越過第 n 階 + if (state + choice > n) { + continue; + } + // 嘗試:做出選擇,更新狀態 + backtrack(choices, state + choice, n, res); + // 回退 + } + } + + // 爬樓梯:回溯 + fn climbingStairsBacktrack(n: usize) !i32 { + var choices = [_]i32{ 1, 2 }; // 可選擇向上爬 1 階或 2 階 + var state: i32 = 0; // 從第 0 階開始爬 + var res = std.ArrayList(i32).init(std.heap.page_allocator); + defer res.deinit(); + try res.append(0); // 使用 res[0] 記錄方案數量 + backtrack(&choices, state, @intCast(n), res); + return res.items[0]; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 14.1.1   方法一:暴力搜尋 + +回溯演算法通常並不顯式地對問題進行拆解,而是將求解問題看作一系列決策步驟,透過試探和剪枝,搜尋所有可能的解。 + +我們可以嘗試從問題分解的角度分析這道題。設爬到第 $i$ 階共有 $dp[i]$ 種方案,那麼 $dp[i]$ 就是原問題,其子問題包括: + +$$ +dp[i-1], dp[i-2], \dots, dp[2], dp[1] +$$ + +由於每輪只能上 $1$ 階或 $2$ 階,因此當我們站在第 $i$ 階樓梯上時,上一輪只可能站在第 $i - 1$ 階或第 $i - 2$ 階上。換句話說,我們只能從第 $i -1$ 階或第 $i - 2$ 階邁向第 $i$ 階。 + +由此便可得出一個重要推論:**爬到第 $i - 1$ 階的方案數加上爬到第 $i - 2$ 階的方案數就等於爬到第 $i$ 階的方案數**。公式如下: + +$$ +dp[i] = dp[i-1] + dp[i-2] +$$ + +這意味著在爬樓梯問題中,各個子問題之間存在遞推關係,**原問題的解可以由子問題的解構建得來**。圖 14-2 展示了該遞推關係。 + +![方案數量遞推關係](intro_to_dynamic_programming.assets/climbing_stairs_state_transfer.png){ class="animation-figure" } + +

圖 14-2   方案數量遞推關係

+ +我們可以根據遞推公式得到暴力搜尋解法。以 $dp[n]$ 為起始點,**遞迴地將一個較大問題拆解為兩個較小問題的和**,直至到達最小子問題 $dp[1]$ 和 $dp[2]$ 時返回。其中,最小子問題的解是已知的,即 $dp[1] = 1$、$dp[2] = 2$ ,表示爬到第 $1$、$2$ 階分別有 $1$、$2$ 種方案。 + +觀察以下程式碼,它和標準回溯程式碼都屬於深度優先搜尋,但更加簡潔: + +=== "Python" + + ```python title="climbing_stairs_dfs.py" + def dfs(i: int) -> int: + """搜尋""" + # 已知 dp[1] 和 dp[2] ,返回之 + if i == 1 or i == 2: + return i + # dp[i] = dp[i-1] + dp[i-2] + count = dfs(i - 1) + dfs(i - 2) + return count + + def climbing_stairs_dfs(n: int) -> int: + """爬樓梯:搜尋""" + return dfs(n) + ``` + +=== "C++" + + ```cpp title="climbing_stairs_dfs.cpp" + /* 搜尋 */ + int dfs(int i) { + // 已知 dp[1] 和 dp[2] ,返回之 + if (i == 1 || i == 2) + return i; + // dp[i] = dp[i-1] + dp[i-2] + int count = dfs(i - 1) + dfs(i - 2); + return count; + } + + /* 爬樓梯:搜尋 */ + int climbingStairsDFS(int n) { + return dfs(n); + } + ``` + +=== "Java" + + ```java title="climbing_stairs_dfs.java" + /* 搜尋 */ + int dfs(int i) { + // 已知 dp[1] 和 dp[2] ,返回之 + if (i == 1 || i == 2) + return i; + // dp[i] = dp[i-1] + dp[i-2] + int count = dfs(i - 1) + dfs(i - 2); + return count; + } + + /* 爬樓梯:搜尋 */ + int climbingStairsDFS(int n) { + return dfs(n); + } + ``` + +=== "C#" + + ```csharp title="climbing_stairs_dfs.cs" + /* 搜尋 */ + int DFS(int i) { + // 已知 dp[1] 和 dp[2] ,返回之 + if (i == 1 || i == 2) + return i; + // dp[i] = dp[i-1] + dp[i-2] + int count = DFS(i - 1) + DFS(i - 2); + return count; + } + + /* 爬樓梯:搜尋 */ + int ClimbingStairsDFS(int n) { + return DFS(n); + } + ``` + +=== "Go" + + ```go title="climbing_stairs_dfs.go" + /* 搜尋 */ + func dfs(i int) int { + // 已知 dp[1] 和 dp[2] ,返回之 + if i == 1 || i == 2 { + return i + } + // dp[i] = dp[i-1] + dp[i-2] + count := dfs(i-1) + dfs(i-2) + return count + } + + /* 爬樓梯:搜尋 */ + func climbingStairsDFS(n int) int { + return dfs(n) + } + ``` + +=== "Swift" + + ```swift title="climbing_stairs_dfs.swift" + /* 搜尋 */ + func dfs(i: Int) -> Int { + // 已知 dp[1] 和 dp[2] ,返回之 + if i == 1 || i == 2 { + return i + } + // dp[i] = dp[i-1] + dp[i-2] + let count = dfs(i: i - 1) + dfs(i: i - 2) + return count + } + + /* 爬樓梯:搜尋 */ + func climbingStairsDFS(n: Int) -> Int { + dfs(i: n) + } + ``` + +=== "JS" + + ```javascript title="climbing_stairs_dfs.js" + /* 搜尋 */ + function dfs(i) { + // 已知 dp[1] 和 dp[2] ,返回之 + if (i === 1 || i === 2) return i; + // dp[i] = dp[i-1] + dp[i-2] + const count = dfs(i - 1) + dfs(i - 2); + return count; + } + + /* 爬樓梯:搜尋 */ + function climbingStairsDFS(n) { + return dfs(n); + } + ``` + +=== "TS" + + ```typescript title="climbing_stairs_dfs.ts" + /* 搜尋 */ + function dfs(i: number): number { + // 已知 dp[1] 和 dp[2] ,返回之 + if (i === 1 || i === 2) return i; + // dp[i] = dp[i-1] + dp[i-2] + const count = dfs(i - 1) + dfs(i - 2); + return count; + } + + /* 爬樓梯:搜尋 */ + function climbingStairsDFS(n: number): number { + return dfs(n); + } + ``` + +=== "Dart" + + ```dart title="climbing_stairs_dfs.dart" + /* 搜尋 */ + int dfs(int i) { + // 已知 dp[1] 和 dp[2] ,返回之 + if (i == 1 || i == 2) return i; + // dp[i] = dp[i-1] + dp[i-2] + int count = dfs(i - 1) + dfs(i - 2); + return count; + } + + /* 爬樓梯:搜尋 */ + int climbingStairsDFS(int n) { + return dfs(n); + } + ``` + +=== "Rust" + + ```rust title="climbing_stairs_dfs.rs" + /* 搜尋 */ + fn dfs(i: usize) -> i32 { + // 已知 dp[1] 和 dp[2] ,返回之 + if i == 1 || i == 2 { + return i as i32; + } + // dp[i] = dp[i-1] + dp[i-2] + let count = dfs(i - 1) + dfs(i - 2); + count + } + + /* 爬樓梯:搜尋 */ + fn climbing_stairs_dfs(n: usize) -> i32 { + dfs(n) + } + ``` + +=== "C" + + ```c title="climbing_stairs_dfs.c" + /* 搜尋 */ + int dfs(int i) { + // 已知 dp[1] 和 dp[2] ,返回之 + if (i == 1 || i == 2) + return i; + // dp[i] = dp[i-1] + dp[i-2] + int count = dfs(i - 1) + dfs(i - 2); + return count; + } + + /* 爬樓梯:搜尋 */ + int climbingStairsDFS(int n) { + return dfs(n); + } + ``` + +=== "Kotlin" + + ```kotlin title="climbing_stairs_dfs.kt" + /* 搜尋 */ + fun dfs(i: Int): Int { + // 已知 dp[1] 和 dp[2] ,返回之 + if (i == 1 || i == 2) return i + // dp[i] = dp[i-1] + dp[i-2] + val count = dfs(i - 1) + dfs(i - 2) + return count + } + + /* 爬樓梯:搜尋 */ + fun climbingStairsDFS(n: Int): Int { + return dfs(n) + } + ``` + +=== "Ruby" + + ```ruby title="climbing_stairs_dfs.rb" + [class]{}-[func]{dfs} + + [class]{}-[func]{climbing_stairs_dfs} + ``` + +=== "Zig" + + ```zig title="climbing_stairs_dfs.zig" + // 搜尋 + fn dfs(i: usize) i32 { + // 已知 dp[1] 和 dp[2] ,返回之 + if (i == 1 or i == 2) { + return @intCast(i); + } + // dp[i] = dp[i-1] + dp[i-2] + var count = dfs(i - 1) + dfs(i - 2); + return count; + } + + // 爬樓梯:搜尋 + fn climbingStairsDFS(comptime n: usize) i32 { + return dfs(n); + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +圖 14-3 展示了暴力搜尋形成的遞迴樹。對於問題 $dp[n]$ ,其遞迴樹的深度為 $n$ ,時間複雜度為 $O(2^n)$ 。指數階屬於爆炸式增長,如果我們輸入一個比較大的 $n$ ,則會陷入漫長的等待之中。 + +![爬樓梯對應遞迴樹](intro_to_dynamic_programming.assets/climbing_stairs_dfs_tree.png){ class="animation-figure" } + +

圖 14-3   爬樓梯對應遞迴樹

+ +觀察圖 14-3 ,**指數階的時間複雜度是“重疊子問題”導致的**。例如 $dp[9]$ 被分解為 $dp[8]$ 和 $dp[7]$ ,$dp[8]$ 被分解為 $dp[7]$ 和 $dp[6]$ ,兩者都包含子問題 $dp[7]$ 。 + +以此類推,子問題中包含更小的重疊子問題,子子孫孫無窮盡也。絕大部分計算資源都浪費在這些重疊的子問題上。 + +## 14.1.2   方法二:記憶化搜尋 + +為了提升演算法效率,**我們希望所有的重疊子問題都只被計算一次**。為此,我們宣告一個陣列 `mem` 來記錄每個子問題的解,並在搜尋過程中將重疊子問題剪枝。 + +1. 當首次計算 $dp[i]$ 時,我們將其記錄至 `mem[i]` ,以便之後使用。 +2. 當再次需要計算 $dp[i]$ 時,我們便可直接從 `mem[i]` 中獲取結果,從而避免重複計算該子問題。 + +程式碼如下所示: + +=== "Python" + + ```python title="climbing_stairs_dfs_mem.py" + def dfs(i: int, mem: list[int]) -> int: + """記憶化搜尋""" + # 已知 dp[1] 和 dp[2] ,返回之 + if i == 1 or i == 2: + return i + # 若存在記錄 dp[i] ,則直接返回之 + if mem[i] != -1: + return mem[i] + # dp[i] = dp[i-1] + dp[i-2] + count = dfs(i - 1, mem) + dfs(i - 2, mem) + # 記錄 dp[i] + mem[i] = count + return count + + def climbing_stairs_dfs_mem(n: int) -> int: + """爬樓梯:記憶化搜尋""" + # mem[i] 記錄爬到第 i 階的方案總數,-1 代表無記錄 + mem = [-1] * (n + 1) + return dfs(n, mem) + ``` + +=== "C++" + + ```cpp title="climbing_stairs_dfs_mem.cpp" + /* 記憶化搜尋 */ + int dfs(int i, vector &mem) { + // 已知 dp[1] 和 dp[2] ,返回之 + if (i == 1 || i == 2) + return i; + // 若存在記錄 dp[i] ,則直接返回之 + if (mem[i] != -1) + return mem[i]; + // dp[i] = dp[i-1] + dp[i-2] + int count = dfs(i - 1, mem) + dfs(i - 2, mem); + // 記錄 dp[i] + mem[i] = count; + return count; + } + + /* 爬樓梯:記憶化搜尋 */ + int climbingStairsDFSMem(int n) { + // mem[i] 記錄爬到第 i 階的方案總數,-1 代表無記錄 + vector mem(n + 1, -1); + return dfs(n, mem); + } + ``` + +=== "Java" + + ```java title="climbing_stairs_dfs_mem.java" + /* 記憶化搜尋 */ + int dfs(int i, int[] mem) { + // 已知 dp[1] 和 dp[2] ,返回之 + if (i == 1 || i == 2) + return i; + // 若存在記錄 dp[i] ,則直接返回之 + if (mem[i] != -1) + return mem[i]; + // dp[i] = dp[i-1] + dp[i-2] + int count = dfs(i - 1, mem) + dfs(i - 2, mem); + // 記錄 dp[i] + mem[i] = count; + return count; + } + + /* 爬樓梯:記憶化搜尋 */ + int climbingStairsDFSMem(int n) { + // mem[i] 記錄爬到第 i 階的方案總數,-1 代表無記錄 + int[] mem = new int[n + 1]; + Arrays.fill(mem, -1); + return dfs(n, mem); + } + ``` + +=== "C#" + + ```csharp title="climbing_stairs_dfs_mem.cs" + /* 記憶化搜尋 */ + int DFS(int i, int[] mem) { + // 已知 dp[1] 和 dp[2] ,返回之 + if (i == 1 || i == 2) + return i; + // 若存在記錄 dp[i] ,則直接返回之 + if (mem[i] != -1) + return mem[i]; + // dp[i] = dp[i-1] + dp[i-2] + int count = DFS(i - 1, mem) + DFS(i - 2, mem); + // 記錄 dp[i] + mem[i] = count; + return count; + } + + /* 爬樓梯:記憶化搜尋 */ + int ClimbingStairsDFSMem(int n) { + // mem[i] 記錄爬到第 i 階的方案總數,-1 代表無記錄 + int[] mem = new int[n + 1]; + Array.Fill(mem, -1); + return DFS(n, mem); + } + ``` + +=== "Go" + + ```go title="climbing_stairs_dfs_mem.go" + /* 記憶化搜尋 */ + func dfsMem(i int, mem []int) int { + // 已知 dp[1] 和 dp[2] ,返回之 + if i == 1 || i == 2 { + return i + } + // 若存在記錄 dp[i] ,則直接返回之 + if mem[i] != -1 { + return mem[i] + } + // dp[i] = dp[i-1] + dp[i-2] + count := dfsMem(i-1, mem) + dfsMem(i-2, mem) + // 記錄 dp[i] + mem[i] = count + return count + } + + /* 爬樓梯:記憶化搜尋 */ + func climbingStairsDFSMem(n int) int { + // mem[i] 記錄爬到第 i 階的方案總數,-1 代表無記錄 + mem := make([]int, n+1) + for i := range mem { + mem[i] = -1 + } + return dfsMem(n, mem) + } + ``` + +=== "Swift" + + ```swift title="climbing_stairs_dfs_mem.swift" + /* 記憶化搜尋 */ + func dfs(i: Int, mem: inout [Int]) -> Int { + // 已知 dp[1] 和 dp[2] ,返回之 + if i == 1 || i == 2 { + return i + } + // 若存在記錄 dp[i] ,則直接返回之 + if mem[i] != -1 { + return mem[i] + } + // dp[i] = dp[i-1] + dp[i-2] + let count = dfs(i: i - 1, mem: &mem) + dfs(i: i - 2, mem: &mem) + // 記錄 dp[i] + mem[i] = count + return count + } + + /* 爬樓梯:記憶化搜尋 */ + func climbingStairsDFSMem(n: Int) -> Int { + // mem[i] 記錄爬到第 i 階的方案總數,-1 代表無記錄 + var mem = Array(repeating: -1, count: n + 1) + return dfs(i: n, mem: &mem) + } + ``` + +=== "JS" + + ```javascript title="climbing_stairs_dfs_mem.js" + /* 記憶化搜尋 */ + function dfs(i, mem) { + // 已知 dp[1] 和 dp[2] ,返回之 + if (i === 1 || i === 2) return i; + // 若存在記錄 dp[i] ,則直接返回之 + if (mem[i] != -1) return mem[i]; + // dp[i] = dp[i-1] + dp[i-2] + const count = dfs(i - 1, mem) + dfs(i - 2, mem); + // 記錄 dp[i] + mem[i] = count; + return count; + } + + /* 爬樓梯:記憶化搜尋 */ + function climbingStairsDFSMem(n) { + // mem[i] 記錄爬到第 i 階的方案總數,-1 代表無記錄 + const mem = new Array(n + 1).fill(-1); + return dfs(n, mem); + } + ``` + +=== "TS" + + ```typescript title="climbing_stairs_dfs_mem.ts" + /* 記憶化搜尋 */ + function dfs(i: number, mem: number[]): number { + // 已知 dp[1] 和 dp[2] ,返回之 + if (i === 1 || i === 2) return i; + // 若存在記錄 dp[i] ,則直接返回之 + if (mem[i] != -1) return mem[i]; + // dp[i] = dp[i-1] + dp[i-2] + const count = dfs(i - 1, mem) + dfs(i - 2, mem); + // 記錄 dp[i] + mem[i] = count; + return count; + } + + /* 爬樓梯:記憶化搜尋 */ + function climbingStairsDFSMem(n: number): number { + // mem[i] 記錄爬到第 i 階的方案總數,-1 代表無記錄 + const mem = new Array(n + 1).fill(-1); + return dfs(n, mem); + } + ``` + +=== "Dart" + + ```dart title="climbing_stairs_dfs_mem.dart" + /* 記憶化搜尋 */ + int dfs(int i, List mem) { + // 已知 dp[1] 和 dp[2] ,返回之 + if (i == 1 || i == 2) return i; + // 若存在記錄 dp[i] ,則直接返回之 + if (mem[i] != -1) return mem[i]; + // dp[i] = dp[i-1] + dp[i-2] + int count = dfs(i - 1, mem) + dfs(i - 2, mem); + // 記錄 dp[i] + mem[i] = count; + return count; + } + + /* 爬樓梯:記憶化搜尋 */ + int climbingStairsDFSMem(int n) { + // mem[i] 記錄爬到第 i 階的方案總數,-1 代表無記錄 + List mem = List.filled(n + 1, -1); + return dfs(n, mem); + } + ``` + +=== "Rust" + + ```rust title="climbing_stairs_dfs_mem.rs" + /* 記憶化搜尋 */ + fn dfs(i: usize, mem: &mut [i32]) -> i32 { + // 已知 dp[1] 和 dp[2] ,返回之 + if i == 1 || i == 2 { + return i as i32; + } + // 若存在記錄 dp[i] ,則直接返回之 + if mem[i] != -1 { + return mem[i]; + } + // dp[i] = dp[i-1] + dp[i-2] + let count = dfs(i - 1, mem) + dfs(i - 2, mem); + // 記錄 dp[i] + mem[i] = count; + count + } + + /* 爬樓梯:記憶化搜尋 */ + fn climbing_stairs_dfs_mem(n: usize) -> i32 { + // mem[i] 記錄爬到第 i 階的方案總數,-1 代表無記錄 + let mut mem = vec![-1; n + 1]; + dfs(n, &mut mem) + } + ``` + +=== "C" + + ```c title="climbing_stairs_dfs_mem.c" + /* 記憶化搜尋 */ + int dfs(int i, int *mem) { + // 已知 dp[1] 和 dp[2] ,返回之 + if (i == 1 || i == 2) + return i; + // 若存在記錄 dp[i] ,則直接返回之 + if (mem[i] != -1) + return mem[i]; + // dp[i] = dp[i-1] + dp[i-2] + int count = dfs(i - 1, mem) + dfs(i - 2, mem); + // 記錄 dp[i] + mem[i] = count; + return count; + } + + /* 爬樓梯:記憶化搜尋 */ + int climbingStairsDFSMem(int n) { + // mem[i] 記錄爬到第 i 階的方案總數,-1 代表無記錄 + int *mem = (int *)malloc((n + 1) * sizeof(int)); + for (int i = 0; i <= n; i++) { + mem[i] = -1; + } + int result = dfs(n, mem); + free(mem); + return result; + } + ``` + +=== "Kotlin" + + ```kotlin title="climbing_stairs_dfs_mem.kt" + /* 記憶化搜尋 */ + fun dfs(i: Int, mem: IntArray): Int { + // 已知 dp[1] 和 dp[2] ,返回之 + if (i == 1 || i == 2) return i + // 若存在記錄 dp[i] ,則直接返回之 + if (mem[i] != -1) return mem[i] + // dp[i] = dp[i-1] + dp[i-2] + val count = dfs(i - 1, mem) + dfs(i - 2, mem) + // 記錄 dp[i] + mem[i] = count + return count + } + + /* 爬樓梯:記憶化搜尋 */ + fun climbingStairsDFSMem(n: Int): Int { + // mem[i] 記錄爬到第 i 階的方案總數,-1 代表無記錄 + val mem = IntArray(n + 1) + Arrays.fill(mem, -1) + return dfs(n, mem) + } + ``` + +=== "Ruby" + + ```ruby title="climbing_stairs_dfs_mem.rb" + [class]{}-[func]{dfs} + + [class]{}-[func]{climbing_stairs_dfs_mem} + ``` + +=== "Zig" + + ```zig title="climbing_stairs_dfs_mem.zig" + // 記憶化搜尋 + fn dfs(i: usize, mem: []i32) i32 { + // 已知 dp[1] 和 dp[2] ,返回之 + if (i == 1 or i == 2) { + return @intCast(i); + } + // 若存在記錄 dp[i] ,則直接返回之 + if (mem[i] != -1) { + return mem[i]; + } + // dp[i] = dp[i-1] + dp[i-2] + var count = dfs(i - 1, mem) + dfs(i - 2, mem); + // 記錄 dp[i] + mem[i] = count; + return count; + } + + // 爬樓梯:記憶化搜尋 + fn climbingStairsDFSMem(comptime n: usize) i32 { + // mem[i] 記錄爬到第 i 階的方案總數,-1 代表無記錄 + var mem = [_]i32{ -1 } ** (n + 1); + return dfs(n, &mem); + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +觀察圖 14-4 ,**經過記憶化處理後,所有重疊子問題都只需計算一次,時間複雜度最佳化至 $O(n)$** ,這是一個巨大的飛躍。 + +![記憶化搜尋對應遞迴樹](intro_to_dynamic_programming.assets/climbing_stairs_dfs_memo_tree.png){ class="animation-figure" } + +

圖 14-4   記憶化搜尋對應遞迴樹

+ +## 14.1.3   方法三:動態規劃 + +**記憶化搜尋是一種“從頂至底”的方法**:我們從原問題(根節點)開始,遞迴地將較大子問題分解為較小子問題,直至解已知的最小子問題(葉節點)。之後,透過回溯逐層收集子問題的解,構建出原問題的解。 + +與之相反,**動態規劃是一種“從底至頂”的方法**:從最小子問題的解開始,迭代地構建更大子問題的解,直至得到原問題的解。 + +由於動態規劃不包含回溯過程,因此只需使用迴圈迭代實現,無須使用遞迴。在以下程式碼中,我們初始化一個陣列 `dp` 來儲存子問題的解,它起到了與記憶化搜尋中陣列 `mem` 相同的記錄作用: + +=== "Python" + + ```python title="climbing_stairs_dp.py" + def climbing_stairs_dp(n: int) -> int: + """爬樓梯:動態規劃""" + if n == 1 or n == 2: + return n + # 初始化 dp 表,用於儲存子問題的解 + dp = [0] * (n + 1) + # 初始狀態:預設最小子問題的解 + dp[1], dp[2] = 1, 2 + # 狀態轉移:從較小子問題逐步求解較大子問題 + for i in range(3, n + 1): + dp[i] = dp[i - 1] + dp[i - 2] + return dp[n] + ``` + +=== "C++" + + ```cpp title="climbing_stairs_dp.cpp" + /* 爬樓梯:動態規劃 */ + int climbingStairsDP(int n) { + if (n == 1 || n == 2) + return n; + // 初始化 dp 表,用於儲存子問題的解 + vector dp(n + 1); + // 初始狀態:預設最小子問題的解 + dp[1] = 1; + dp[2] = 2; + // 狀態轉移:從較小子問題逐步求解較大子問題 + for (int i = 3; i <= n; i++) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + return dp[n]; + } + ``` + +=== "Java" + + ```java title="climbing_stairs_dp.java" + /* 爬樓梯:動態規劃 */ + int climbingStairsDP(int n) { + if (n == 1 || n == 2) + return n; + // 初始化 dp 表,用於儲存子問題的解 + int[] dp = new int[n + 1]; + // 初始狀態:預設最小子問題的解 + dp[1] = 1; + dp[2] = 2; + // 狀態轉移:從較小子問題逐步求解較大子問題 + for (int i = 3; i <= n; i++) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + return dp[n]; + } + ``` + +=== "C#" + + ```csharp title="climbing_stairs_dp.cs" + /* 爬樓梯:動態規劃 */ + int ClimbingStairsDP(int n) { + if (n == 1 || n == 2) + return n; + // 初始化 dp 表,用於儲存子問題的解 + int[] dp = new int[n + 1]; + // 初始狀態:預設最小子問題的解 + dp[1] = 1; + dp[2] = 2; + // 狀態轉移:從較小子問題逐步求解較大子問題 + for (int i = 3; i <= n; i++) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + return dp[n]; + } + ``` + +=== "Go" + + ```go title="climbing_stairs_dp.go" + /* 爬樓梯:動態規劃 */ + func climbingStairsDP(n int) int { + if n == 1 || n == 2 { + return n + } + // 初始化 dp 表,用於儲存子問題的解 + dp := make([]int, n+1) + // 初始狀態:預設最小子問題的解 + dp[1] = 1 + dp[2] = 2 + // 狀態轉移:從較小子問題逐步求解較大子問題 + for i := 3; i <= n; i++ { + dp[i] = dp[i-1] + dp[i-2] + } + return dp[n] + } + ``` + +=== "Swift" + + ```swift title="climbing_stairs_dp.swift" + /* 爬樓梯:動態規劃 */ + func climbingStairsDP(n: Int) -> Int { + if n == 1 || n == 2 { + return n + } + // 初始化 dp 表,用於儲存子問題的解 + var dp = Array(repeating: 0, count: n + 1) + // 初始狀態:預設最小子問題的解 + dp[1] = 1 + dp[2] = 2 + // 狀態轉移:從較小子問題逐步求解較大子問題 + for i in 3 ... n { + dp[i] = dp[i - 1] + dp[i - 2] + } + return dp[n] + } + ``` + +=== "JS" + + ```javascript title="climbing_stairs_dp.js" + /* 爬樓梯:動態規劃 */ + function climbingStairsDP(n) { + if (n === 1 || n === 2) return n; + // 初始化 dp 表,用於儲存子問題的解 + const dp = new Array(n + 1).fill(-1); + // 初始狀態:預設最小子問題的解 + dp[1] = 1; + dp[2] = 2; + // 狀態轉移:從較小子問題逐步求解較大子問題 + for (let i = 3; i <= n; i++) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + return dp[n]; + } + ``` + +=== "TS" + + ```typescript title="climbing_stairs_dp.ts" + /* 爬樓梯:動態規劃 */ + function climbingStairsDP(n: number): number { + if (n === 1 || n === 2) return n; + // 初始化 dp 表,用於儲存子問題的解 + const dp = new Array(n + 1).fill(-1); + // 初始狀態:預設最小子問題的解 + dp[1] = 1; + dp[2] = 2; + // 狀態轉移:從較小子問題逐步求解較大子問題 + for (let i = 3; i <= n; i++) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + return dp[n]; + } + ``` + +=== "Dart" + + ```dart title="climbing_stairs_dp.dart" + /* 爬樓梯:動態規劃 */ + int climbingStairsDP(int n) { + if (n == 1 || n == 2) return n; + // 初始化 dp 表,用於儲存子問題的解 + List dp = List.filled(n + 1, 0); + // 初始狀態:預設最小子問題的解 + dp[1] = 1; + dp[2] = 2; + // 狀態轉移:從較小子問題逐步求解較大子問題 + for (int i = 3; i <= n; i++) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + return dp[n]; + } + ``` + +=== "Rust" + + ```rust title="climbing_stairs_dp.rs" + /* 爬樓梯:動態規劃 */ + fn climbing_stairs_dp(n: usize) -> i32 { + // 已知 dp[1] 和 dp[2] ,返回之 + if n == 1 || n == 2 { + return n as i32; + } + // 初始化 dp 表,用於儲存子問題的解 + let mut dp = vec![-1; n + 1]; + // 初始狀態:預設最小子問題的解 + dp[1] = 1; + dp[2] = 2; + // 狀態轉移:從較小子問題逐步求解較大子問題 + for i in 3..=n { + dp[i] = dp[i - 1] + dp[i - 2]; + } + dp[n] + } + ``` + +=== "C" + + ```c title="climbing_stairs_dp.c" + /* 爬樓梯:動態規劃 */ + int climbingStairsDP(int n) { + if (n == 1 || n == 2) + return n; + // 初始化 dp 表,用於儲存子問題的解 + int *dp = (int *)malloc((n + 1) * sizeof(int)); + // 初始狀態:預設最小子問題的解 + dp[1] = 1; + dp[2] = 2; + // 狀態轉移:從較小子問題逐步求解較大子問題 + for (int i = 3; i <= n; i++) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + int result = dp[n]; + free(dp); + return result; + } + ``` + +=== "Kotlin" + + ```kotlin title="climbing_stairs_dp.kt" + /* 爬樓梯:動態規劃 */ + fun climbingStairsDP(n: Int): Int { + if (n == 1 || n == 2) return n + // 初始化 dp 表,用於儲存子問題的解 + val dp = IntArray(n + 1) + // 初始狀態:預設最小子問題的解 + dp[1] = 1 + dp[2] = 2 + // 狀態轉移:從較小子問題逐步求解較大子問題 + for (i in 3..n) { + dp[i] = dp[i - 1] + dp[i - 2] + } + return dp[n] + } + ``` + +=== "Ruby" + + ```ruby title="climbing_stairs_dp.rb" + [class]{}-[func]{climbing_stairs_dp} + ``` + +=== "Zig" + + ```zig title="climbing_stairs_dp.zig" + // 爬樓梯:動態規劃 + fn climbingStairsDP(comptime n: usize) i32 { + // 已知 dp[1] 和 dp[2] ,返回之 + if (n == 1 or n == 2) { + return @intCast(n); + } + // 初始化 dp 表,用於儲存子問題的解 + var dp = [_]i32{-1} ** (n + 1); + // 初始狀態:預設最小子問題的解 + dp[1] = 1; + dp[2] = 2; + // 狀態轉移:從較小子問題逐步求解較大子問題 + for (3..n + 1) |i| { + dp[i] = dp[i - 1] + dp[i - 2]; + } + return dp[n]; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +圖 14-5 模擬了以上程式碼的執行過程。 + +![爬樓梯的動態規劃過程](intro_to_dynamic_programming.assets/climbing_stairs_dp.png){ class="animation-figure" } + +

圖 14-5   爬樓梯的動態規劃過程

+ +與回溯演算法一樣,動態規劃也使用“狀態”概念來表示問題求解的特定階段,每個狀態都對應一個子問題以及相應的區域性最優解。例如,爬樓梯問題的狀態定義為當前所在樓梯階數 $i$ 。 + +根據以上內容,我們可以總結出動態規劃的常用術語。 + +- 將陣列 `dp` 稱為 dp 表,$dp[i]$ 表示狀態 $i$ 對應子問題的解。 +- 將最小子問題對應的狀態(第 $1$ 階和第 $2$ 階樓梯)稱為初始狀態。 +- 將遞推公式 $dp[i] = dp[i-1] + dp[i-2]$ 稱為狀態轉移方程。 + +## 14.1.4   空間最佳化 + +細心的讀者可能發現了,**由於 $dp[i]$ 只與 $dp[i-1]$ 和 $dp[i-2]$ 有關,因此我們無須使用一個陣列 `dp` 來儲存所有子問題的解**,而只需兩個變數滾動前進即可。程式碼如下所示: + +=== "Python" + + ```python title="climbing_stairs_dp.py" + def climbing_stairs_dp_comp(n: int) -> int: + """爬樓梯:空間最佳化後的動態規劃""" + if n == 1 or n == 2: + return n + a, b = 1, 2 + for _ in range(3, n + 1): + a, b = b, a + b + return b + ``` + +=== "C++" + + ```cpp title="climbing_stairs_dp.cpp" + /* 爬樓梯:空間最佳化後的動態規劃 */ + int climbingStairsDPComp(int n) { + if (n == 1 || n == 2) + return n; + int a = 1, b = 2; + for (int i = 3; i <= n; i++) { + int tmp = b; + b = a + b; + a = tmp; + } + return b; + } + ``` + +=== "Java" + + ```java title="climbing_stairs_dp.java" + /* 爬樓梯:空間最佳化後的動態規劃 */ + int climbingStairsDPComp(int n) { + if (n == 1 || n == 2) + return n; + int a = 1, b = 2; + for (int i = 3; i <= n; i++) { + int tmp = b; + b = a + b; + a = tmp; + } + return b; + } + ``` + +=== "C#" + + ```csharp title="climbing_stairs_dp.cs" + /* 爬樓梯:空間最佳化後的動態規劃 */ + int ClimbingStairsDPComp(int n) { + if (n == 1 || n == 2) + return n; + int a = 1, b = 2; + for (int i = 3; i <= n; i++) { + int tmp = b; + b = a + b; + a = tmp; + } + return b; + } + ``` + +=== "Go" + + ```go title="climbing_stairs_dp.go" + /* 爬樓梯:空間最佳化後的動態規劃 */ + func climbingStairsDPComp(n int) int { + if n == 1 || n == 2 { + return n + } + a, b := 1, 2 + // 狀態轉移:從較小子問題逐步求解較大子問題 + for i := 3; i <= n; i++ { + a, b = b, a+b + } + return b + } + ``` + +=== "Swift" + + ```swift title="climbing_stairs_dp.swift" + /* 爬樓梯:空間最佳化後的動態規劃 */ + func climbingStairsDPComp(n: Int) -> Int { + if n == 1 || n == 2 { + return n + } + var a = 1 + var b = 2 + for _ in 3 ... n { + (a, b) = (b, a + b) + } + return b + } + ``` + +=== "JS" + + ```javascript title="climbing_stairs_dp.js" + /* 爬樓梯:空間最佳化後的動態規劃 */ + function climbingStairsDPComp(n) { + if (n === 1 || n === 2) return n; + let a = 1, + b = 2; + for (let i = 3; i <= n; i++) { + const tmp = b; + b = a + b; + a = tmp; + } + return b; + } + ``` + +=== "TS" + + ```typescript title="climbing_stairs_dp.ts" + /* 爬樓梯:空間最佳化後的動態規劃 */ + function climbingStairsDPComp(n: number): number { + if (n === 1 || n === 2) return n; + let a = 1, + b = 2; + for (let i = 3; i <= n; i++) { + const tmp = b; + b = a + b; + a = tmp; + } + return b; + } + ``` + +=== "Dart" + + ```dart title="climbing_stairs_dp.dart" + /* 爬樓梯:空間最佳化後的動態規劃 */ + int climbingStairsDPComp(int n) { + if (n == 1 || n == 2) return n; + int a = 1, b = 2; + for (int i = 3; i <= n; i++) { + int tmp = b; + b = a + b; + a = tmp; + } + return b; + } + ``` + +=== "Rust" + + ```rust title="climbing_stairs_dp.rs" + /* 爬樓梯:空間最佳化後的動態規劃 */ + fn climbing_stairs_dp_comp(n: usize) -> i32 { + if n == 1 || n == 2 { + return n as i32; + } + let (mut a, mut b) = (1, 2); + for _ in 3..=n { + let tmp = b; + b = a + b; + a = tmp; + } + b + } + ``` + +=== "C" + + ```c title="climbing_stairs_dp.c" + /* 爬樓梯:空間最佳化後的動態規劃 */ + int climbingStairsDPComp(int n) { + if (n == 1 || n == 2) + return n; + int a = 1, b = 2; + for (int i = 3; i <= n; i++) { + int tmp = b; + b = a + b; + a = tmp; + } + return b; + } + ``` + +=== "Kotlin" + + ```kotlin title="climbing_stairs_dp.kt" + /* 爬樓梯:空間最佳化後的動態規劃 */ + fun climbingStairsDPComp(n: Int): Int { + if (n == 1 || n == 2) return n + var a = 1 + var b = 2 + for (i in 3..n) { + val tmp = b + b += a + a = tmp + } + return b + } + ``` + +=== "Ruby" + + ```ruby title="climbing_stairs_dp.rb" + [class]{}-[func]{climbing_stairs_dp_comp} + ``` + +=== "Zig" + + ```zig title="climbing_stairs_dp.zig" + // 爬樓梯:空間最佳化後的動態規劃 + fn climbingStairsDPComp(comptime n: usize) i32 { + if (n == 1 or n == 2) { + return @intCast(n); + } + var a: i32 = 1; + var b: i32 = 2; + for (3..n + 1) |_| { + var tmp = b; + b = a + b; + a = tmp; + } + return b; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +觀察以上程式碼,由於省去了陣列 `dp` 佔用的空間,因此空間複雜度從 $O(n)$ 降至 $O(1)$ 。 + +在動態規劃問題中,當前狀態往往僅與前面有限個狀態有關,這時我們可以只保留必要的狀態,透過“降維”來節省記憶體空間。**這種空間最佳化技巧被稱為“滾動變數”或“滾動陣列”**。 diff --git a/zh-Hant/docs/chapter_dynamic_programming/knapsack_problem.md b/zh-Hant/docs/chapter_dynamic_programming/knapsack_problem.md new file mode 100644 index 000000000..6ff7f7c2b --- /dev/null +++ b/zh-Hant/docs/chapter_dynamic_programming/knapsack_problem.md @@ -0,0 +1,1488 @@ +--- +comments: true +--- + +# 14.4   0-1 背包問題 + +背包問題是一個非常好的動態規劃入門題目,是動態規劃中最常見的問題形式。其具有很多變種,例如 0-1 背包問題、完全背包問題、多重背包問題等。 + +在本節中,我們先來求解最常見的 0-1 背包問題。 + +!!! question + + 給定 $n$ 個物品,第 $i$ 個物品的重量為 $wgt[i-1]$、價值為 $val[i-1]$ ,和一個容量為 $cap$ 的背包。每個物品只能選擇一次,問在限定背包容量下能放入物品的最大價值。 + +觀察圖 14-17 ,由於物品編號 $i$ 從 $1$ 開始計數,陣列索引從 $0$ 開始計數,因此物品 $i$ 對應重量 $wgt[i-1]$ 和價值 $val[i-1]$ 。 + +![0-1 背包的示例資料](knapsack_problem.assets/knapsack_example.png){ class="animation-figure" } + +

圖 14-17   0-1 背包的示例資料

+ +我們可以將 0-1 背包問題看作一個由 $n$ 輪決策組成的過程,對於每個物體都有不放入和放入兩種決策,因此該問題滿足決策樹模型。 + +該問題的目標是求解“在限定背包容量下能放入物品的最大價值”,因此較大機率是一個動態規劃問題。 + +**第一步:思考每輪的決策,定義狀態,從而得到 $dp$ 表** + +對於每個物品來說,不放入背包,背包容量不變;放入背包,背包容量減小。由此可得狀態定義:當前物品編號 $i$ 和剩餘背包容量 $c$ ,記為 $[i, c]$ 。 + +狀態 $[i, c]$ 對應的子問題為:**前 $i$ 個物品在剩餘容量為 $c$ 的背包中的最大價值**,記為 $dp[i, c]$ 。 + +待求解的是 $dp[n, cap]$ ,因此需要一個尺寸為 $(n+1) \times (cap+1)$ 的二維 $dp$ 表。 + +**第二步:找出最優子結構,進而推導出狀態轉移方程** + +當我們做出物品 $i$ 的決策後,剩餘的是前 $i-1$ 個物品的決策,可分為以下兩種情況。 + +- **不放入物品 $i$** :背包容量不變,狀態變化為 $[i-1, c]$ 。 +- **放入物品 $i$** :背包容量減少 $wgt[i-1]$ ,價值增加 $val[i-1]$ ,狀態變化為 $[i-1, c-wgt[i-1]]$ 。 + +上述分析向我們揭示了本題的最優子結構:**最大價值 $dp[i, c]$ 等於不放入物品 $i$ 和放入物品 $i$ 兩種方案中價值更大的那一個**。由此可推導出狀態轉移方程: + +$$ +dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1]) +$$ + +需要注意的是,若當前物品重量 $wgt[i - 1]$ 超出剩餘背包容量 $c$ ,則只能選擇不放入背包。 + +**第三步:確定邊界條件和狀態轉移順序** + +當無物品或無剩餘背包容量時最大價值為 $0$ ,即首列 $dp[i, 0]$ 和首行 $dp[0, c]$ 都等於 $0$ 。 + +當前狀態 $[i, c]$ 從上方的狀態 $[i-1, c]$ 和左上方的狀態 $[i-1, c-wgt[i-1]]$ 轉移而來,因此透過兩層迴圈正序走訪整個 $dp$ 表即可。 + +根據以上分析,我們接下來按順序實現暴力搜尋、記憶化搜尋、動態規劃解法。 + +### 1.   方法一:暴力搜尋 + +搜尋程式碼包含以下要素。 + +- **遞迴參數**:狀態 $[i, c]$ 。 +- **返回值**:子問題的解 $dp[i, c]$ 。 +- **終止條件**:當物品編號越界 $i = 0$ 或背包剩餘容量為 $0$ 時,終止遞迴並返回價值 $0$ 。 +- **剪枝**:若當前物品重量超出背包剩餘容量,則只能選擇不放入背包。 + +=== "Python" + + ```python title="knapsack.py" + def knapsack_dfs(wgt: list[int], val: list[int], i: int, c: int) -> int: + """0-1 背包:暴力搜尋""" + # 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if i == 0 or c == 0: + return 0 + # 若超過背包容量,則只能選擇不放入背包 + if wgt[i - 1] > c: + return knapsack_dfs(wgt, val, i - 1, c) + # 計算不放入和放入物品 i 的最大價值 + no = knapsack_dfs(wgt, val, i - 1, c) + yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1] + # 返回兩種方案中價值更大的那一個 + return max(no, yes) + ``` + +=== "C++" + + ```cpp title="knapsack.cpp" + /* 0-1 背包:暴力搜尋 */ + int knapsackDFS(vector &wgt, vector &val, int i, int c) { + // 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if (i == 0 || c == 0) { + return 0; + } + // 若超過背包容量,則只能選擇不放入背包 + if (wgt[i - 1] > c) { + return knapsackDFS(wgt, val, i - 1, c); + } + // 計算不放入和放入物品 i 的最大價值 + int no = knapsackDFS(wgt, val, i - 1, c); + int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 返回兩種方案中價值更大的那一個 + return max(no, yes); + } + ``` + +=== "Java" + + ```java title="knapsack.java" + /* 0-1 背包:暴力搜尋 */ + int knapsackDFS(int[] wgt, int[] val, int i, int c) { + // 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if (i == 0 || c == 0) { + return 0; + } + // 若超過背包容量,則只能選擇不放入背包 + if (wgt[i - 1] > c) { + return knapsackDFS(wgt, val, i - 1, c); + } + // 計算不放入和放入物品 i 的最大價值 + int no = knapsackDFS(wgt, val, i - 1, c); + int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 返回兩種方案中價值更大的那一個 + return Math.max(no, yes); + } + ``` + +=== "C#" + + ```csharp title="knapsack.cs" + /* 0-1 背包:暴力搜尋 */ + int KnapsackDFS(int[] weight, int[] val, int i, int c) { + // 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if (i == 0 || c == 0) { + return 0; + } + // 若超過背包容量,則只能選擇不放入背包 + if (weight[i - 1] > c) { + return KnapsackDFS(weight, val, i - 1, c); + } + // 計算不放入和放入物品 i 的最大價值 + int no = KnapsackDFS(weight, val, i - 1, c); + int yes = KnapsackDFS(weight, val, i - 1, c - weight[i - 1]) + val[i - 1]; + // 返回兩種方案中價值更大的那一個 + return Math.Max(no, yes); + } + ``` + +=== "Go" + + ```go title="knapsack.go" + /* 0-1 背包:暴力搜尋 */ + func knapsackDFS(wgt, val []int, i, c int) int { + // 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if i == 0 || c == 0 { + return 0 + } + // 若超過背包容量,則只能選擇不放入背包 + if wgt[i-1] > c { + return knapsackDFS(wgt, val, i-1, c) + } + // 計算不放入和放入物品 i 的最大價值 + no := knapsackDFS(wgt, val, i-1, c) + yes := knapsackDFS(wgt, val, i-1, c-wgt[i-1]) + val[i-1] + // 返回兩種方案中價值更大的那一個 + return int(math.Max(float64(no), float64(yes))) + } + ``` + +=== "Swift" + + ```swift title="knapsack.swift" + /* 0-1 背包:暴力搜尋 */ + func knapsackDFS(wgt: [Int], val: [Int], i: Int, c: Int) -> Int { + // 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if i == 0 || c == 0 { + return 0 + } + // 若超過背包容量,則只能選擇不放入背包 + if wgt[i - 1] > c { + return knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c) + } + // 計算不放入和放入物品 i 的最大價值 + let no = knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c) + let yes = knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c - wgt[i - 1]) + val[i - 1] + // 返回兩種方案中價值更大的那一個 + return max(no, yes) + } + ``` + +=== "JS" + + ```javascript title="knapsack.js" + /* 0-1 背包:暴力搜尋 */ + function knapsackDFS(wgt, val, i, c) { + // 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if (i === 0 || c === 0) { + return 0; + } + // 若超過背包容量,則只能選擇不放入背包 + if (wgt[i - 1] > c) { + return knapsackDFS(wgt, val, i - 1, c); + } + // 計算不放入和放入物品 i 的最大價值 + const no = knapsackDFS(wgt, val, i - 1, c); + const yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 返回兩種方案中價值更大的那一個 + return Math.max(no, yes); + } + ``` + +=== "TS" + + ```typescript title="knapsack.ts" + /* 0-1 背包:暴力搜尋 */ + function knapsackDFS( + wgt: Array, + val: Array, + i: number, + c: number + ): number { + // 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if (i === 0 || c === 0) { + return 0; + } + // 若超過背包容量,則只能選擇不放入背包 + if (wgt[i - 1] > c) { + return knapsackDFS(wgt, val, i - 1, c); + } + // 計算不放入和放入物品 i 的最大價值 + const no = knapsackDFS(wgt, val, i - 1, c); + const yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 返回兩種方案中價值更大的那一個 + return Math.max(no, yes); + } + ``` + +=== "Dart" + + ```dart title="knapsack.dart" + /* 0-1 背包:暴力搜尋 */ + int knapsackDFS(List wgt, List val, int i, int c) { + // 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if (i == 0 || c == 0) { + return 0; + } + // 若超過背包容量,則只能選擇不放入背包 + if (wgt[i - 1] > c) { + return knapsackDFS(wgt, val, i - 1, c); + } + // 計算不放入和放入物品 i 的最大價值 + int no = knapsackDFS(wgt, val, i - 1, c); + int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 返回兩種方案中價值更大的那一個 + return max(no, yes); + } + ``` + +=== "Rust" + + ```rust title="knapsack.rs" + /* 0-1 背包:暴力搜尋 */ + fn knapsack_dfs(wgt: &[i32], val: &[i32], i: usize, c: usize) -> i32 { + // 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if i == 0 || c == 0 { + return 0; + } + // 若超過背包容量,則只能選擇不放入背包 + if wgt[i - 1] > c as i32 { + return knapsack_dfs(wgt, val, i - 1, c); + } + // 計算不放入和放入物品 i 的最大價值 + let no = knapsack_dfs(wgt, val, i - 1, c); + let yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1] as usize) + val[i - 1]; + // 返回兩種方案中價值更大的那一個 + std::cmp::max(no, yes) + } + ``` + +=== "C" + + ```c title="knapsack.c" + /* 0-1 背包:暴力搜尋 */ + int knapsackDFS(int wgt[], int val[], int i, int c) { + // 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if (i == 0 || c == 0) { + return 0; + } + // 若超過背包容量,則只能選擇不放入背包 + if (wgt[i - 1] > c) { + return knapsackDFS(wgt, val, i - 1, c); + } + // 計算不放入和放入物品 i 的最大價值 + int no = knapsackDFS(wgt, val, i - 1, c); + int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 返回兩種方案中價值更大的那一個 + return myMax(no, yes); + } + ``` + +=== "Kotlin" + + ```kotlin title="knapsack.kt" + /* 0-1 背包:暴力搜尋 */ + fun knapsackDFS( + wgt: IntArray, + value: IntArray, + i: Int, + c: Int + ): Int { + // 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if (i == 0 || c == 0) { + return 0 + } + // 若超過背包容量,則只能選擇不放入背包 + if (wgt[i - 1] > c) { + return knapsackDFS(wgt, value, i - 1, c) + } + // 計算不放入和放入物品 i 的最大價值 + val no = knapsackDFS(wgt, value, i - 1, c) + val yes = knapsackDFS(wgt, value, i - 1, c - wgt[i - 1]) + value[i - 1] + // 返回兩種方案中價值更大的那一個 + return max(no.toDouble(), yes.toDouble()).toInt() + } + ``` + +=== "Ruby" + + ```ruby title="knapsack.rb" + [class]{}-[func]{knapsack_dfs} + ``` + +=== "Zig" + + ```zig title="knapsack.zig" + // 0-1 背包:暴力搜尋 + fn knapsackDFS(wgt: []i32, val: []i32, i: usize, c: usize) i32 { + // 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if (i == 0 or c == 0) { + return 0; + } + // 若超過背包容量,則只能選擇不放入背包 + if (wgt[i - 1] > c) { + return knapsackDFS(wgt, val, i - 1, c); + } + // 計算不放入和放入物品 i 的最大價值 + var no = knapsackDFS(wgt, val, i - 1, c); + var yes = knapsackDFS(wgt, val, i - 1, c - @as(usize, @intCast(wgt[i - 1]))) + val[i - 1]; + // 返回兩種方案中價值更大的那一個 + return @max(no, yes); + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +如圖 14-18 所示,由於每個物品都會產生不選和選兩條搜尋分支,因此時間複雜度為 $O(2^n)$ 。 + +觀察遞迴樹,容易發現其中存在重疊子問題,例如 $dp[1, 10]$ 等。而當物品較多、背包容量較大,尤其是相同重量的物品較多時,重疊子問題的數量將會大幅增多。 + +![0-1 背包問題的暴力搜尋遞迴樹](knapsack_problem.assets/knapsack_dfs.png){ class="animation-figure" } + +

圖 14-18   0-1 背包問題的暴力搜尋遞迴樹

+ +### 2.   方法二:記憶化搜尋 + +為了保證重疊子問題只被計算一次,我們藉助記憶串列 `mem` 來記錄子問題的解,其中 `mem[i][c]` 對應 $dp[i, c]$ 。 + +引入記憶化之後,**時間複雜度取決於子問題數量**,也就是 $O(n \times cap)$ 。實現程式碼如下: + +=== "Python" + + ```python title="knapsack.py" + def knapsack_dfs_mem( + wgt: list[int], val: list[int], mem: list[list[int]], i: int, c: int + ) -> int: + """0-1 背包:記憶化搜尋""" + # 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if i == 0 or c == 0: + return 0 + # 若已有記錄,則直接返回 + if mem[i][c] != -1: + return mem[i][c] + # 若超過背包容量,則只能選擇不放入背包 + if wgt[i - 1] > c: + return knapsack_dfs_mem(wgt, val, mem, i - 1, c) + # 計算不放入和放入物品 i 的最大價值 + no = knapsack_dfs_mem(wgt, val, mem, i - 1, c) + yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1] + # 記錄並返回兩種方案中價值更大的那一個 + mem[i][c] = max(no, yes) + return mem[i][c] + ``` + +=== "C++" + + ```cpp title="knapsack.cpp" + /* 0-1 背包:記憶化搜尋 */ + int knapsackDFSMem(vector &wgt, vector &val, vector> &mem, int i, int c) { + // 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if (i == 0 || c == 0) { + return 0; + } + // 若已有記錄,則直接返回 + if (mem[i][c] != -1) { + return mem[i][c]; + } + // 若超過背包容量,則只能選擇不放入背包 + if (wgt[i - 1] > c) { + return knapsackDFSMem(wgt, val, mem, i - 1, c); + } + // 計算不放入和放入物品 i 的最大價值 + int no = knapsackDFSMem(wgt, val, mem, i - 1, c); + int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 記錄並返回兩種方案中價值更大的那一個 + mem[i][c] = max(no, yes); + return mem[i][c]; + } + ``` + +=== "Java" + + ```java title="knapsack.java" + /* 0-1 背包:記憶化搜尋 */ + int knapsackDFSMem(int[] wgt, int[] val, int[][] mem, int i, int c) { + // 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if (i == 0 || c == 0) { + return 0; + } + // 若已有記錄,則直接返回 + if (mem[i][c] != -1) { + return mem[i][c]; + } + // 若超過背包容量,則只能選擇不放入背包 + if (wgt[i - 1] > c) { + return knapsackDFSMem(wgt, val, mem, i - 1, c); + } + // 計算不放入和放入物品 i 的最大價值 + int no = knapsackDFSMem(wgt, val, mem, i - 1, c); + int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 記錄並返回兩種方案中價值更大的那一個 + mem[i][c] = Math.max(no, yes); + return mem[i][c]; + } + ``` + +=== "C#" + + ```csharp title="knapsack.cs" + /* 0-1 背包:記憶化搜尋 */ + int KnapsackDFSMem(int[] weight, int[] val, int[][] mem, int i, int c) { + // 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if (i == 0 || c == 0) { + return 0; + } + // 若已有記錄,則直接返回 + if (mem[i][c] != -1) { + return mem[i][c]; + } + // 若超過背包容量,則只能選擇不放入背包 + if (weight[i - 1] > c) { + return KnapsackDFSMem(weight, val, mem, i - 1, c); + } + // 計算不放入和放入物品 i 的最大價值 + int no = KnapsackDFSMem(weight, val, mem, i - 1, c); + int yes = KnapsackDFSMem(weight, val, mem, i - 1, c - weight[i - 1]) + val[i - 1]; + // 記錄並返回兩種方案中價值更大的那一個 + mem[i][c] = Math.Max(no, yes); + return mem[i][c]; + } + ``` + +=== "Go" + + ```go title="knapsack.go" + /* 0-1 背包:記憶化搜尋 */ + func knapsackDFSMem(wgt, val []int, mem [][]int, i, c int) int { + // 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if i == 0 || c == 0 { + return 0 + } + // 若已有記錄,則直接返回 + if mem[i][c] != -1 { + return mem[i][c] + } + // 若超過背包容量,則只能選擇不放入背包 + if wgt[i-1] > c { + return knapsackDFSMem(wgt, val, mem, i-1, c) + } + // 計算不放入和放入物品 i 的最大價值 + no := knapsackDFSMem(wgt, val, mem, i-1, c) + yes := knapsackDFSMem(wgt, val, mem, i-1, c-wgt[i-1]) + val[i-1] + // 返回兩種方案中價值更大的那一個 + mem[i][c] = int(math.Max(float64(no), float64(yes))) + return mem[i][c] + } + ``` + +=== "Swift" + + ```swift title="knapsack.swift" + /* 0-1 背包:記憶化搜尋 */ + func knapsackDFSMem(wgt: [Int], val: [Int], mem: inout [[Int]], i: Int, c: Int) -> Int { + // 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if i == 0 || c == 0 { + return 0 + } + // 若已有記錄,則直接返回 + if mem[i][c] != -1 { + return mem[i][c] + } + // 若超過背包容量,則只能選擇不放入背包 + if wgt[i - 1] > c { + return knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c) + } + // 計算不放入和放入物品 i 的最大價值 + let no = knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c) + let yes = knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c - wgt[i - 1]) + val[i - 1] + // 記錄並返回兩種方案中價值更大的那一個 + mem[i][c] = max(no, yes) + return mem[i][c] + } + ``` + +=== "JS" + + ```javascript title="knapsack.js" + /* 0-1 背包:記憶化搜尋 */ + function knapsackDFSMem(wgt, val, mem, i, c) { + // 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if (i === 0 || c === 0) { + return 0; + } + // 若已有記錄,則直接返回 + if (mem[i][c] !== -1) { + return mem[i][c]; + } + // 若超過背包容量,則只能選擇不放入背包 + if (wgt[i - 1] > c) { + return knapsackDFSMem(wgt, val, mem, i - 1, c); + } + // 計算不放入和放入物品 i 的最大價值 + const no = knapsackDFSMem(wgt, val, mem, i - 1, c); + const yes = + knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 記錄並返回兩種方案中價值更大的那一個 + mem[i][c] = Math.max(no, yes); + return mem[i][c]; + } + ``` + +=== "TS" + + ```typescript title="knapsack.ts" + /* 0-1 背包:記憶化搜尋 */ + function knapsackDFSMem( + wgt: Array, + val: Array, + mem: Array>, + i: number, + c: number + ): number { + // 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if (i === 0 || c === 0) { + return 0; + } + // 若已有記錄,則直接返回 + if (mem[i][c] !== -1) { + return mem[i][c]; + } + // 若超過背包容量,則只能選擇不放入背包 + if (wgt[i - 1] > c) { + return knapsackDFSMem(wgt, val, mem, i - 1, c); + } + // 計算不放入和放入物品 i 的最大價值 + const no = knapsackDFSMem(wgt, val, mem, i - 1, c); + const yes = + knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 記錄並返回兩種方案中價值更大的那一個 + mem[i][c] = Math.max(no, yes); + return mem[i][c]; + } + ``` + +=== "Dart" + + ```dart title="knapsack.dart" + /* 0-1 背包:記憶化搜尋 */ + int knapsackDFSMem( + List wgt, + List val, + List> mem, + int i, + int c, + ) { + // 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if (i == 0 || c == 0) { + return 0; + } + // 若已有記錄,則直接返回 + if (mem[i][c] != -1) { + return mem[i][c]; + } + // 若超過背包容量,則只能選擇不放入背包 + if (wgt[i - 1] > c) { + return knapsackDFSMem(wgt, val, mem, i - 1, c); + } + // 計算不放入和放入物品 i 的最大價值 + int no = knapsackDFSMem(wgt, val, mem, i - 1, c); + int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 記錄並返回兩種方案中價值更大的那一個 + mem[i][c] = max(no, yes); + return mem[i][c]; + } + ``` + +=== "Rust" + + ```rust title="knapsack.rs" + /* 0-1 背包:記憶化搜尋 */ + fn knapsack_dfs_mem(wgt: &[i32], val: &[i32], mem: &mut Vec>, i: usize, c: usize) -> i32 { + // 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if i == 0 || c == 0 { + return 0; + } + // 若已有記錄,則直接返回 + if mem[i][c] != -1 { + return mem[i][c]; + } + // 若超過背包容量,則只能選擇不放入背包 + if wgt[i - 1] > c as i32 { + return knapsack_dfs_mem(wgt, val, mem, i - 1, c); + } + // 計算不放入和放入物品 i 的最大價值 + let no = knapsack_dfs_mem(wgt, val, mem, i - 1, c); + let yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1] as usize) + val[i - 1]; + // 記錄並返回兩種方案中價值更大的那一個 + mem[i][c] = std::cmp::max(no, yes); + mem[i][c] + } + ``` + +=== "C" + + ```c title="knapsack.c" + /* 0-1 背包:記憶化搜尋 */ + int knapsackDFSMem(int wgt[], int val[], int memCols, int **mem, int i, int c) { + // 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if (i == 0 || c == 0) { + return 0; + } + // 若已有記錄,則直接返回 + if (mem[i][c] != -1) { + return mem[i][c]; + } + // 若超過背包容量,則只能選擇不放入背包 + if (wgt[i - 1] > c) { + return knapsackDFSMem(wgt, val, memCols, mem, i - 1, c); + } + // 計算不放入和放入物品 i 的最大價值 + int no = knapsackDFSMem(wgt, val, memCols, mem, i - 1, c); + int yes = knapsackDFSMem(wgt, val, memCols, mem, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 記錄並返回兩種方案中價值更大的那一個 + mem[i][c] = myMax(no, yes); + return mem[i][c]; + } + ``` + +=== "Kotlin" + + ```kotlin title="knapsack.kt" + /* 0-1 背包:記憶化搜尋 */ + fun knapsackDFSMem( + wgt: IntArray, + value: IntArray, + mem: Array, + i: Int, + c: Int + ): Int { + // 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if (i == 0 || c == 0) { + return 0 + } + // 若已有記錄,則直接返回 + if (mem[i][c] != -1) { + return mem[i][c] + } + // 若超過背包容量,則只能選擇不放入背包 + if (wgt[i - 1] > c) { + return knapsackDFSMem(wgt, value, mem, i - 1, c) + } + // 計算不放入和放入物品 i 的最大價值 + val no = knapsackDFSMem(wgt, value, mem, i - 1, c) + val yes = knapsackDFSMem(wgt, value, mem, i - 1, c - wgt[i - 1]) + value[i - 1] + // 記錄並返回兩種方案中價值更大的那一個 + mem[i][c] = max(no.toDouble(), yes.toDouble()).toInt() + return mem[i][c] + } + ``` + +=== "Ruby" + + ```ruby title="knapsack.rb" + [class]{}-[func]{knapsack_dfs_mem} + ``` + +=== "Zig" + + ```zig title="knapsack.zig" + // 0-1 背包:記憶化搜尋 + fn knapsackDFSMem(wgt: []i32, val: []i32, mem: anytype, i: usize, c: usize) i32 { + // 若已選完所有物品或背包無剩餘容量,則返回價值 0 + if (i == 0 or c == 0) { + return 0; + } + // 若已有記錄,則直接返回 + if (mem[i][c] != -1) { + return mem[i][c]; + } + // 若超過背包容量,則只能選擇不放入背包 + if (wgt[i - 1] > c) { + return knapsackDFSMem(wgt, val, mem, i - 1, c); + } + // 計算不放入和放入物品 i 的最大價值 + var no = knapsackDFSMem(wgt, val, mem, i - 1, c); + var yes = knapsackDFSMem(wgt, val, mem, i - 1, c - @as(usize, @intCast(wgt[i - 1]))) + val[i - 1]; + // 記錄並返回兩種方案中價值更大的那一個 + mem[i][c] = @max(no, yes); + return mem[i][c]; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +圖 14-19 展示了在記憶化搜尋中被剪掉的搜尋分支。 + +![0-1 背包問題的記憶化搜尋遞迴樹](knapsack_problem.assets/knapsack_dfs_mem.png){ class="animation-figure" } + +

圖 14-19   0-1 背包問題的記憶化搜尋遞迴樹

+ +### 3.   方法三:動態規劃 + +動態規劃實質上就是在狀態轉移中填充 $dp$ 表的過程,程式碼如下所示: + +=== "Python" + + ```python title="knapsack.py" + def knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int: + """0-1 背包:動態規劃""" + n = len(wgt) + # 初始化 dp 表 + dp = [[0] * (cap + 1) for _ in range(n + 1)] + # 狀態轉移 + for i in range(1, n + 1): + for c in range(1, cap + 1): + if wgt[i - 1] > c: + # 若超過背包容量,則不選物品 i + dp[i][c] = dp[i - 1][c] + else: + # 不選和選物品 i 這兩種方案的較大值 + dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]) + return dp[n][cap] + ``` + +=== "C++" + + ```cpp title="knapsack.cpp" + /* 0-1 背包:動態規劃 */ + int knapsackDP(vector &wgt, vector &val, int cap) { + int n = wgt.size(); + // 初始化 dp 表 + vector> dp(n + 1, vector(cap + 1, 0)); + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[i][c] = dp[i - 1][c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[n][cap]; + } + ``` + +=== "Java" + + ```java title="knapsack.java" + /* 0-1 背包:動態規劃 */ + int knapsackDP(int[] wgt, int[] val, int cap) { + int n = wgt.length; + // 初始化 dp 表 + int[][] dp = new int[n + 1][cap + 1]; + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[i][c] = dp[i - 1][c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[i][c] = Math.max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[n][cap]; + } + ``` + +=== "C#" + + ```csharp title="knapsack.cs" + /* 0-1 背包:動態規劃 */ + int KnapsackDP(int[] weight, int[] val, int cap) { + int n = weight.Length; + // 初始化 dp 表 + int[,] dp = new int[n + 1, cap + 1]; + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (weight[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[i, c] = dp[i - 1, c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[i, c] = Math.Max(dp[i - 1, c - weight[i - 1]] + val[i - 1], dp[i - 1, c]); + } + } + } + return dp[n, cap]; + } + ``` + +=== "Go" + + ```go title="knapsack.go" + /* 0-1 背包:動態規劃 */ + func knapsackDP(wgt, val []int, cap int) int { + n := len(wgt) + // 初始化 dp 表 + dp := make([][]int, n+1) + for i := 0; i <= n; i++ { + dp[i] = make([]int, cap+1) + } + // 狀態轉移 + for i := 1; i <= n; i++ { + for c := 1; c <= cap; c++ { + if wgt[i-1] > c { + // 若超過背包容量,則不選物品 i + dp[i][c] = dp[i-1][c] + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[i][c] = int(math.Max(float64(dp[i-1][c]), float64(dp[i-1][c-wgt[i-1]]+val[i-1]))) + } + } + } + return dp[n][cap] + } + ``` + +=== "Swift" + + ```swift title="knapsack.swift" + /* 0-1 背包:動態規劃 */ + func knapsackDP(wgt: [Int], val: [Int], cap: Int) -> Int { + let n = wgt.count + // 初始化 dp 表 + var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1) + // 狀態轉移 + for i in 1 ... n { + for c in 1 ... cap { + if wgt[i - 1] > c { + // 若超過背包容量,則不選物品 i + dp[i][c] = dp[i - 1][c] + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]) + } + } + } + return dp[n][cap] + } + ``` + +=== "JS" + + ```javascript title="knapsack.js" + /* 0-1 背包:動態規劃 */ + function knapsackDP(wgt, val, cap) { + const n = wgt.length; + // 初始化 dp 表 + const dp = Array(n + 1) + .fill(0) + .map(() => Array(cap + 1).fill(0)); + // 狀態轉移 + for (let i = 1; i <= n; i++) { + for (let c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[i][c] = dp[i - 1][c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[i][c] = Math.max( + dp[i - 1][c], + dp[i - 1][c - wgt[i - 1]] + val[i - 1] + ); + } + } + } + return dp[n][cap]; + } + ``` + +=== "TS" + + ```typescript title="knapsack.ts" + /* 0-1 背包:動態規劃 */ + function knapsackDP( + wgt: Array, + val: Array, + cap: number + ): number { + const n = wgt.length; + // 初始化 dp 表 + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: cap + 1 }, () => 0) + ); + // 狀態轉移 + for (let i = 1; i <= n; i++) { + for (let c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[i][c] = dp[i - 1][c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[i][c] = Math.max( + dp[i - 1][c], + dp[i - 1][c - wgt[i - 1]] + val[i - 1] + ); + } + } + } + return dp[n][cap]; + } + ``` + +=== "Dart" + + ```dart title="knapsack.dart" + /* 0-1 背包:動態規劃 */ + int knapsackDP(List wgt, List val, int cap) { + int n = wgt.length; + // 初始化 dp 表 + List> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0)); + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[i][c] = dp[i - 1][c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[n][cap]; + } + ``` + +=== "Rust" + + ```rust title="knapsack.rs" + /* 0-1 背包:動態規劃 */ + fn knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 { + let n = wgt.len(); + // 初始化 dp 表 + let mut dp = vec![vec![0; cap + 1]; n + 1]; + // 狀態轉移 + for i in 1..=n { + for c in 1..=cap { + if wgt[i - 1] > c as i32 { + // 若超過背包容量,則不選物品 i + dp[i][c] = dp[i - 1][c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[i][c] = std::cmp::max( + dp[i - 1][c], + dp[i - 1][c - wgt[i - 1] as usize] + val[i - 1], + ); + } + } + } + dp[n][cap] + } + ``` + +=== "C" + + ```c title="knapsack.c" + /* 0-1 背包:動態規劃 */ + int knapsackDP(int wgt[], int val[], int cap, int wgtSize) { + int n = wgtSize; + // 初始化 dp 表 + int **dp = malloc((n + 1) * sizeof(int *)); + for (int i = 0; i <= n; i++) { + dp[i] = calloc(cap + 1, sizeof(int)); + } + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[i][c] = dp[i - 1][c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[i][c] = myMax(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]); + } + } + } + int res = dp[n][cap]; + // 釋放記憶體 + for (int i = 0; i <= n; i++) { + free(dp[i]); + } + return res; + } + ``` + +=== "Kotlin" + + ```kotlin title="knapsack.kt" + /* 0-1 背包:動態規劃 */ + fun knapsackDP( + wgt: IntArray, + value: IntArray, + cap: Int + ): Int { + val n = wgt.size + // 初始化 dp 表 + val dp = Array(n + 1) { IntArray(cap + 1) } + // 狀態轉移 + for (i in 1..n) { + for (c in 1..cap) { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[i][c] = dp[i - 1][c] + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[i][c] = max(dp[i - 1][c].toDouble(), (dp[i - 1][c - wgt[i - 1]] + value[i - 1]).toDouble()) + .toInt() + } + } + } + return dp[n][cap] + } + ``` + +=== "Ruby" + + ```ruby title="knapsack.rb" + [class]{}-[func]{knapsack_dp} + ``` + +=== "Zig" + + ```zig title="knapsack.zig" + // 0-1 背包:動態規劃 + fn knapsackDP(comptime wgt: []i32, val: []i32, comptime cap: usize) i32 { + comptime var n = wgt.len; + // 初始化 dp 表 + var dp = [_][cap + 1]i32{[_]i32{0} ** (cap + 1)} ** (n + 1); + // 狀態轉移 + for (1..n + 1) |i| { + for (1..cap + 1) |c| { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[i][c] = dp[i - 1][c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[i][c] = @max(dp[i - 1][c], dp[i - 1][c - @as(usize, @intCast(wgt[i - 1]))] + val[i - 1]); + } + } + } + return dp[n][cap]; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +如圖 14-20 所示,時間複雜度和空間複雜度都由陣列 `dp` 大小決定,即 $O(n \times cap)$ 。 + +=== "<1>" + ![0-1 背包問題的動態規劃過程](knapsack_problem.assets/knapsack_dp_step1.png){ class="animation-figure" } + +=== "<2>" + ![knapsack_dp_step2](knapsack_problem.assets/knapsack_dp_step2.png){ class="animation-figure" } + +=== "<3>" + ![knapsack_dp_step3](knapsack_problem.assets/knapsack_dp_step3.png){ class="animation-figure" } + +=== "<4>" + ![knapsack_dp_step4](knapsack_problem.assets/knapsack_dp_step4.png){ class="animation-figure" } + +=== "<5>" + ![knapsack_dp_step5](knapsack_problem.assets/knapsack_dp_step5.png){ class="animation-figure" } + +=== "<6>" + ![knapsack_dp_step6](knapsack_problem.assets/knapsack_dp_step6.png){ class="animation-figure" } + +=== "<7>" + ![knapsack_dp_step7](knapsack_problem.assets/knapsack_dp_step7.png){ class="animation-figure" } + +=== "<8>" + ![knapsack_dp_step8](knapsack_problem.assets/knapsack_dp_step8.png){ class="animation-figure" } + +=== "<9>" + ![knapsack_dp_step9](knapsack_problem.assets/knapsack_dp_step9.png){ class="animation-figure" } + +=== "<10>" + ![knapsack_dp_step10](knapsack_problem.assets/knapsack_dp_step10.png){ class="animation-figure" } + +=== "<11>" + ![knapsack_dp_step11](knapsack_problem.assets/knapsack_dp_step11.png){ class="animation-figure" } + +=== "<12>" + ![knapsack_dp_step12](knapsack_problem.assets/knapsack_dp_step12.png){ class="animation-figure" } + +=== "<13>" + ![knapsack_dp_step13](knapsack_problem.assets/knapsack_dp_step13.png){ class="animation-figure" } + +=== "<14>" + ![knapsack_dp_step14](knapsack_problem.assets/knapsack_dp_step14.png){ class="animation-figure" } + +

圖 14-20   0-1 背包問題的動態規劃過程

+ +### 4.   空間最佳化 + +由於每個狀態都只與其上一行的狀態有關,因此我們可以使用兩個陣列滾動前進,將空間複雜度從 $O(n^2)$ 降至 $O(n)$ 。 + +進一步思考,我們能否僅用一個陣列實現空間最佳化呢?觀察可知,每個狀態都是由正上方或左上方的格子轉移過來的。假設只有一個陣列,當開始走訪第 $i$ 行時,該陣列儲存的仍然是第 $i-1$ 行的狀態。 + +- 如果採取正序走訪,那麼走訪到 $dp[i, j]$ 時,左上方 $dp[i-1, 1]$ ~ $dp[i-1, j-1]$ 值可能已經被覆蓋,此時就無法得到正確的狀態轉移結果。 +- 如果採取倒序走訪,則不會發生覆蓋問題,狀態轉移可以正確進行。 + +圖 14-21 展示了在單個陣列下從第 $i = 1$ 行轉換至第 $i = 2$ 行的過程。請思考正序走訪和倒序走訪的區別。 + +=== "<1>" + ![0-1 背包的空間最佳化後的動態規劃過程](knapsack_problem.assets/knapsack_dp_comp_step1.png){ class="animation-figure" } + +=== "<2>" + ![knapsack_dp_comp_step2](knapsack_problem.assets/knapsack_dp_comp_step2.png){ class="animation-figure" } + +=== "<3>" + ![knapsack_dp_comp_step3](knapsack_problem.assets/knapsack_dp_comp_step3.png){ class="animation-figure" } + +=== "<4>" + ![knapsack_dp_comp_step4](knapsack_problem.assets/knapsack_dp_comp_step4.png){ class="animation-figure" } + +=== "<5>" + ![knapsack_dp_comp_step5](knapsack_problem.assets/knapsack_dp_comp_step5.png){ class="animation-figure" } + +=== "<6>" + ![knapsack_dp_comp_step6](knapsack_problem.assets/knapsack_dp_comp_step6.png){ class="animation-figure" } + +

圖 14-21   0-1 背包的空間最佳化後的動態規劃過程

+ +在程式碼實現中,我們僅需將陣列 `dp` 的第一維 $i$ 直接刪除,並且把內迴圈更改為倒序走訪即可: + +=== "Python" + + ```python title="knapsack.py" + def knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int: + """0-1 背包:空間最佳化後的動態規劃""" + n = len(wgt) + # 初始化 dp 表 + dp = [0] * (cap + 1) + # 狀態轉移 + for i in range(1, n + 1): + # 倒序走訪 + for c in range(cap, 0, -1): + if wgt[i - 1] > c: + # 若超過背包容量,則不選物品 i + dp[c] = dp[c] + else: + # 不選和選物品 i 這兩種方案的較大值 + dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]) + return dp[cap] + ``` + +=== "C++" + + ```cpp title="knapsack.cpp" + /* 0-1 背包:空間最佳化後的動態規劃 */ + int knapsackDPComp(vector &wgt, vector &val, int cap) { + int n = wgt.size(); + // 初始化 dp 表 + vector dp(cap + 1, 0); + // 狀態轉移 + for (int i = 1; i <= n; i++) { + // 倒序走訪 + for (int c = cap; c >= 1; c--) { + if (wgt[i - 1] <= c) { + // 不選和選物品 i 這兩種方案的較大值 + dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } + ``` + +=== "Java" + + ```java title="knapsack.java" + /* 0-1 背包:空間最佳化後的動態規劃 */ + int knapsackDPComp(int[] wgt, int[] val, int cap) { + int n = wgt.length; + // 初始化 dp 表 + int[] dp = new int[cap + 1]; + // 狀態轉移 + for (int i = 1; i <= n; i++) { + // 倒序走訪 + for (int c = cap; c >= 1; c--) { + if (wgt[i - 1] <= c) { + // 不選和選物品 i 這兩種方案的較大值 + dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } + ``` + +=== "C#" + + ```csharp title="knapsack.cs" + /* 0-1 背包:空間最佳化後的動態規劃 */ + int KnapsackDPComp(int[] weight, int[] val, int cap) { + int n = weight.Length; + // 初始化 dp 表 + int[] dp = new int[cap + 1]; + // 狀態轉移 + for (int i = 1; i <= n; i++) { + // 倒序走訪 + for (int c = cap; c > 0; c--) { + if (weight[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[c] = dp[c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[c] = Math.Max(dp[c], dp[c - weight[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } + ``` + +=== "Go" + + ```go title="knapsack.go" + /* 0-1 背包:空間最佳化後的動態規劃 */ + func knapsackDPComp(wgt, val []int, cap int) int { + n := len(wgt) + // 初始化 dp 表 + dp := make([]int, cap+1) + // 狀態轉移 + for i := 1; i <= n; i++ { + // 倒序走訪 + for c := cap; c >= 1; c-- { + if wgt[i-1] <= c { + // 不選和選物品 i 這兩種方案的較大值 + dp[c] = int(math.Max(float64(dp[c]), float64(dp[c-wgt[i-1]]+val[i-1]))) + } + } + } + return dp[cap] + } + ``` + +=== "Swift" + + ```swift title="knapsack.swift" + /* 0-1 背包:空間最佳化後的動態規劃 */ + func knapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int { + let n = wgt.count + // 初始化 dp 表 + var dp = Array(repeating: 0, count: cap + 1) + // 狀態轉移 + for i in 1 ... n { + // 倒序走訪 + for c in (1 ... cap).reversed() { + if wgt[i - 1] <= c { + // 不選和選物品 i 這兩種方案的較大值 + dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]) + } + } + } + return dp[cap] + } + ``` + +=== "JS" + + ```javascript title="knapsack.js" + /* 0-1 背包:狀態壓縮後的動態規劃 */ + function knapsackDPComp(wgt, val, cap) { + const n = wgt.length; + // 初始化 dp 表 + const dp = Array(cap + 1).fill(0); + // 狀態轉移 + for (let i = 1; i <= n; i++) { + // 倒序走訪 + for (let c = cap; c >= 1; c--) { + if (wgt[i - 1] <= c) { + // 不選和選物品 i 這兩種方案的較大值 + dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } + ``` + +=== "TS" + + ```typescript title="knapsack.ts" + /* 0-1 背包:狀態壓縮後的動態規劃 */ + function knapsackDPComp( + wgt: Array, + val: Array, + cap: number + ): number { + const n = wgt.length; + // 初始化 dp 表 + const dp = Array(cap + 1).fill(0); + // 狀態轉移 + for (let i = 1; i <= n; i++) { + // 倒序走訪 + for (let c = cap; c >= 1; c--) { + if (wgt[i - 1] <= c) { + // 不選和選物品 i 這兩種方案的較大值 + dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } + ``` + +=== "Dart" + + ```dart title="knapsack.dart" + /* 0-1 背包:空間最佳化後的動態規劃 */ + int knapsackDPComp(List wgt, List val, int cap) { + int n = wgt.length; + // 初始化 dp 表 + List dp = List.filled(cap + 1, 0); + // 狀態轉移 + for (int i = 1; i <= n; i++) { + // 倒序走訪 + for (int c = cap; c >= 1; c--) { + if (wgt[i - 1] <= c) { + // 不選和選物品 i 這兩種方案的較大值 + dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } + ``` + +=== "Rust" + + ```rust title="knapsack.rs" + /* 0-1 背包:空間最佳化後的動態規劃 */ + fn knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 { + let n = wgt.len(); + // 初始化 dp 表 + let mut dp = vec![0; cap + 1]; + // 狀態轉移 + for i in 1..=n { + // 倒序走訪 + for c in (1..=cap).rev() { + if wgt[i - 1] <= c as i32 { + // 不選和選物品 i 這兩種方案的較大值 + dp[c] = std::cmp::max(dp[c], dp[c - wgt[i - 1] as usize] + val[i - 1]); + } + } + } + dp[cap] + } + ``` + +=== "C" + + ```c title="knapsack.c" + /* 0-1 背包:空間最佳化後的動態規劃 */ + int knapsackDPComp(int wgt[], int val[], int cap, int wgtSize) { + int n = wgtSize; + // 初始化 dp 表 + int *dp = calloc(cap + 1, sizeof(int)); + // 狀態轉移 + for (int i = 1; i <= n; i++) { + // 倒序走訪 + for (int c = cap; c >= 1; c--) { + if (wgt[i - 1] <= c) { + // 不選和選物品 i 這兩種方案的較大值 + dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + int res = dp[cap]; + // 釋放記憶體 + free(dp); + return res; + } + ``` + +=== "Kotlin" + + ```kotlin title="knapsack.kt" + /* 0-1 背包:空間最佳化後的動態規劃 */ + fun knapsackDPComp( + wgt: IntArray, + value: IntArray, + cap: Int + ): Int { + val n = wgt.size + // 初始化 dp 表 + val dp = IntArray(cap + 1) + // 狀態轉移 + for (i in 1..n) { + // 倒序走訪 + for (c in cap downTo 1) { + if (wgt[i - 1] <= c) { + // 不選和選物品 i 這兩種方案的較大值 + dp[c] = + max(dp[c].toDouble(), (dp[c - wgt[i - 1]] + value[i - 1]).toDouble()).toInt() + } + } + } + return dp[cap] + } + ``` + +=== "Ruby" + + ```ruby title="knapsack.rb" + [class]{}-[func]{knapsack_dp_comp} + ``` + +=== "Zig" + + ```zig title="knapsack.zig" + // 0-1 背包:空間最佳化後的動態規劃 + fn knapsackDPComp(wgt: []i32, val: []i32, comptime cap: usize) i32 { + var n = wgt.len; + // 初始化 dp 表 + var dp = [_]i32{0} ** (cap + 1); + // 狀態轉移 + for (1..n + 1) |i| { + // 倒序走訪 + var c = cap; + while (c > 0) : (c -= 1) { + if (wgt[i - 1] < c) { + // 不選和選物品 i 這兩種方案的較大值 + dp[c] = @max(dp[c], dp[c - @as(usize, @intCast(wgt[i - 1]))] + val[i - 1]); + } + } + } + return dp[cap]; + } + ``` + +??? pythontutor "視覺化執行" + +
+ diff --git a/zh-Hant/docs/chapter_dynamic_programming/summary.md b/zh-Hant/docs/chapter_dynamic_programming/summary.md new file mode 100644 index 000000000..4c2e020c1 --- /dev/null +++ b/zh-Hant/docs/chapter_dynamic_programming/summary.md @@ -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]$ 時,無須編輯當前字元。 +- 在編輯距離中,狀態依賴其正上方、正左方、左上方的狀態,因此空間最佳化後正序或倒序走訪都無法正確地進行狀態轉移。為此,我們利用一個變數暫存左上方狀態,從而轉化到與完全背包問題等價的情況,可以在空間最佳化後進行正序走訪。 diff --git a/zh-Hant/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md b/zh-Hant/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md new file mode 100644 index 000000000..18ba4226b --- /dev/null +++ b/zh-Hant/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md @@ -0,0 +1,2380 @@ +--- +comments: true +--- + +# 14.5   完全背包問題 + +在本節中,我們先求解另一個常見的背包問題:完全背包,再瞭解它的一種特例:零錢兌換。 + +## 14.5.1   完全背包問題 + +!!! question + + 給定 $n$ 個物品,第 $i$ 個物品的重量為 $wgt[i-1]$、價值為 $val[i-1]$ ,和一個容量為 $cap$ 的背包。**每個物品可以重複選取**,問在限定背包容量下能放入物品的最大價值。示例如圖 14-22 所示。 + +![完全背包問題的示例資料](unbounded_knapsack_problem.assets/unbounded_knapsack_example.png){ class="animation-figure" } + +

圖 14-22   完全背包問題的示例資料

+ +### 1.   動態規劃思路 + +完全背包問題和 0-1 背包問題非常相似,**區別僅在於不限制物品的選擇次數**。 + +- 在 0-1 背包問題中,每種物品只有一個,因此將物品 $i$ 放入背包後,只能從前 $i-1$ 個物品中選擇。 +- 在完全背包問題中,每種物品的數量是無限的,因此將物品 $i$ 放入背包後,**仍可以從前 $i$ 個物品中選擇**。 + +在完全背包問題的規定下,狀態 $[i, c]$ 的變化分為兩種情況。 + +- **不放入物品 $i$** :與 0-1 背包問題相同,轉移至 $[i-1, c]$ 。 +- **放入物品 $i$** :與 0-1 背包問題不同,轉移至 $[i, c-wgt[i-1]]$ 。 + +從而狀態轉移方程變為: + +$$ +dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1]) +$$ + +### 2.   程式碼實現 + +對比兩道題目的程式碼,狀態轉移中有一處從 $i-1$ 變為 $i$ ,其餘完全一致: + +=== "Python" + + ```python title="unbounded_knapsack.py" + def unbounded_knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int: + """完全背包:動態規劃""" + n = len(wgt) + # 初始化 dp 表 + dp = [[0] * (cap + 1) for _ in range(n + 1)] + # 狀態轉移 + for i in range(1, n + 1): + for c in range(1, cap + 1): + if wgt[i - 1] > c: + # 若超過背包容量,則不選物品 i + dp[i][c] = dp[i - 1][c] + else: + # 不選和選物品 i 這兩種方案的較大值 + dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]) + return dp[n][cap] + ``` + +=== "C++" + + ```cpp title="unbounded_knapsack.cpp" + /* 完全背包:動態規劃 */ + int unboundedKnapsackDP(vector &wgt, vector &val, int cap) { + int n = wgt.size(); + // 初始化 dp 表 + vector> dp(n + 1, vector(cap + 1, 0)); + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[i][c] = dp[i - 1][c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[n][cap]; + } + ``` + +=== "Java" + + ```java title="unbounded_knapsack.java" + /* 完全背包:動態規劃 */ + int unboundedKnapsackDP(int[] wgt, int[] val, int cap) { + int n = wgt.length; + // 初始化 dp 表 + int[][] dp = new int[n + 1][cap + 1]; + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[i][c] = dp[i - 1][c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[i][c] = Math.max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[n][cap]; + } + ``` + +=== "C#" + + ```csharp title="unbounded_knapsack.cs" + /* 完全背包:動態規劃 */ + int UnboundedKnapsackDP(int[] wgt, int[] val, int cap) { + int n = wgt.Length; + // 初始化 dp 表 + int[,] dp = new int[n + 1, cap + 1]; + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[i, c] = dp[i - 1, c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[i, c] = Math.Max(dp[i - 1, c], dp[i, c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[n, cap]; + } + ``` + +=== "Go" + + ```go title="unbounded_knapsack.go" + /* 完全背包:動態規劃 */ + func unboundedKnapsackDP(wgt, val []int, cap int) int { + n := len(wgt) + // 初始化 dp 表 + dp := make([][]int, n+1) + for i := 0; i <= n; i++ { + dp[i] = make([]int, cap+1) + } + // 狀態轉移 + for i := 1; i <= n; i++ { + for c := 1; c <= cap; c++ { + if wgt[i-1] > c { + // 若超過背包容量,則不選物品 i + dp[i][c] = dp[i-1][c] + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[i][c] = int(math.Max(float64(dp[i-1][c]), float64(dp[i][c-wgt[i-1]]+val[i-1]))) + } + } + } + return dp[n][cap] + } + ``` + +=== "Swift" + + ```swift title="unbounded_knapsack.swift" + /* 完全背包:動態規劃 */ + func unboundedKnapsackDP(wgt: [Int], val: [Int], cap: Int) -> Int { + let n = wgt.count + // 初始化 dp 表 + var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1) + // 狀態轉移 + for i in 1 ... n { + for c in 1 ... cap { + if wgt[i - 1] > c { + // 若超過背包容量,則不選物品 i + dp[i][c] = dp[i - 1][c] + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]) + } + } + } + return dp[n][cap] + } + ``` + +=== "JS" + + ```javascript title="unbounded_knapsack.js" + /* 完全背包:動態規劃 */ + function unboundedKnapsackDP(wgt, val, cap) { + const n = wgt.length; + // 初始化 dp 表 + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: cap + 1 }, () => 0) + ); + // 狀態轉移 + for (let i = 1; i <= n; i++) { + for (let c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[i][c] = dp[i - 1][c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[i][c] = Math.max( + dp[i - 1][c], + dp[i][c - wgt[i - 1]] + val[i - 1] + ); + } + } + } + return dp[n][cap]; + } + ``` + +=== "TS" + + ```typescript title="unbounded_knapsack.ts" + /* 完全背包:動態規劃 */ + function unboundedKnapsackDP( + wgt: Array, + val: Array, + cap: number + ): number { + const n = wgt.length; + // 初始化 dp 表 + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: cap + 1 }, () => 0) + ); + // 狀態轉移 + for (let i = 1; i <= n; i++) { + for (let c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[i][c] = dp[i - 1][c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[i][c] = Math.max( + dp[i - 1][c], + dp[i][c - wgt[i - 1]] + val[i - 1] + ); + } + } + } + return dp[n][cap]; + } + ``` + +=== "Dart" + + ```dart title="unbounded_knapsack.dart" + /* 完全背包:動態規劃 */ + int unboundedKnapsackDP(List wgt, List val, int cap) { + int n = wgt.length; + // 初始化 dp 表 + List> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0)); + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[i][c] = dp[i - 1][c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[n][cap]; + } + ``` + +=== "Rust" + + ```rust title="unbounded_knapsack.rs" + /* 完全背包:動態規劃 */ + fn unbounded_knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 { + let n = wgt.len(); + // 初始化 dp 表 + let mut dp = vec![vec![0; cap + 1]; n + 1]; + // 狀態轉移 + for i in 1..=n { + for c in 1..=cap { + if wgt[i - 1] > c as i32 { + // 若超過背包容量,則不選物品 i + dp[i][c] = dp[i - 1][c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[i][c] = std::cmp::max(dp[i - 1][c], dp[i][c - wgt[i - 1] as usize] + val[i - 1]); + } + } + } + return dp[n][cap]; + } + ``` + +=== "C" + + ```c title="unbounded_knapsack.c" + /* 完全背包:動態規劃 */ + int unboundedKnapsackDP(int wgt[], int val[], int cap, int wgtSize) { + int n = wgtSize; + // 初始化 dp 表 + int **dp = malloc((n + 1) * sizeof(int *)); + for (int i = 0; i <= n; i++) { + dp[i] = calloc(cap + 1, sizeof(int)); + } + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[i][c] = dp[i - 1][c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[i][c] = myMax(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]); + } + } + } + int res = dp[n][cap]; + // 釋放記憶體 + for (int i = 0; i <= n; i++) { + free(dp[i]); + } + return res; + } + ``` + +=== "Kotlin" + + ```kotlin title="unbounded_knapsack.kt" + /* 完全背包:動態規劃 */ + fun unboundedKnapsackDP( + wgt: IntArray, + value: IntArray, + cap: Int + ): Int { + val n = wgt.size + // 初始化 dp 表 + val dp = Array(n + 1) { IntArray(cap + 1) } + // 狀態轉移 + for (i in 1..n) { + for (c in 1..cap) { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[i][c] = dp[i - 1][c] + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[i][c] = max(dp[i - 1][c].toDouble(), (dp[i][c - wgt[i - 1]] + value[i - 1]).toDouble()) + .toInt() + } + } + } + return dp[n][cap] + } + ``` + +=== "Ruby" + + ```ruby title="unbounded_knapsack.rb" + [class]{}-[func]{unbounded_knapsack_dp} + ``` + +=== "Zig" + + ```zig title="unbounded_knapsack.zig" + // 完全背包:動態規劃 + fn unboundedKnapsackDP(comptime wgt: []i32, val: []i32, comptime cap: usize) i32 { + comptime var n = wgt.len; + // 初始化 dp 表 + var dp = [_][cap + 1]i32{[_]i32{0} ** (cap + 1)} ** (n + 1); + // 狀態轉移 + for (1..n + 1) |i| { + for (1..cap + 1) |c| { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[i][c] = dp[i - 1][c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[i][c] = @max(dp[i - 1][c], dp[i][c - @as(usize, @intCast(wgt[i - 1]))] + val[i - 1]); + } + } + } + return dp[n][cap]; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +### 3.   空間最佳化 + +由於當前狀態是從左邊和上邊的狀態轉移而來的,**因此空間最佳化後應該對 $dp$ 表中的每一行進行正序走訪**。 + +這個走訪順序與 0-1 背包正好相反。請藉助圖 14-23 來理解兩者的區別。 + +=== "<1>" + ![完全背包問題在空間最佳化後的動態規劃過程](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step1.png){ class="animation-figure" } + +=== "<2>" + ![unbounded_knapsack_dp_comp_step2](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step2.png){ class="animation-figure" } + +=== "<3>" + ![unbounded_knapsack_dp_comp_step3](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step3.png){ class="animation-figure" } + +=== "<4>" + ![unbounded_knapsack_dp_comp_step4](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step4.png){ class="animation-figure" } + +=== "<5>" + ![unbounded_knapsack_dp_comp_step5](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step5.png){ class="animation-figure" } + +=== "<6>" + ![unbounded_knapsack_dp_comp_step6](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step6.png){ class="animation-figure" } + +

圖 14-23   完全背包問題在空間最佳化後的動態規劃過程

+ +程式碼實現比較簡單,僅需將陣列 `dp` 的第一維刪除: + +=== "Python" + + ```python title="unbounded_knapsack.py" + def unbounded_knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int: + """完全背包:空間最佳化後的動態規劃""" + n = len(wgt) + # 初始化 dp 表 + dp = [0] * (cap + 1) + # 狀態轉移 + for i in range(1, n + 1): + # 正序走訪 + for c in range(1, cap + 1): + if wgt[i - 1] > c: + # 若超過背包容量,則不選物品 i + dp[c] = dp[c] + else: + # 不選和選物品 i 這兩種方案的較大值 + dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]) + return dp[cap] + ``` + +=== "C++" + + ```cpp title="unbounded_knapsack.cpp" + /* 完全背包:空間最佳化後的動態規劃 */ + int unboundedKnapsackDPComp(vector &wgt, vector &val, int cap) { + int n = wgt.size(); + // 初始化 dp 表 + vector dp(cap + 1, 0); + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[c] = dp[c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } + ``` + +=== "Java" + + ```java title="unbounded_knapsack.java" + /* 完全背包:空間最佳化後的動態規劃 */ + int unboundedKnapsackDPComp(int[] wgt, int[] val, int cap) { + int n = wgt.length; + // 初始化 dp 表 + int[] dp = new int[cap + 1]; + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[c] = dp[c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } + ``` + +=== "C#" + + ```csharp title="unbounded_knapsack.cs" + /* 完全背包:空間最佳化後的動態規劃 */ + int UnboundedKnapsackDPComp(int[] wgt, int[] val, int cap) { + int n = wgt.Length; + // 初始化 dp 表 + int[] dp = new int[cap + 1]; + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[c] = dp[c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[c] = Math.Max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } + ``` + +=== "Go" + + ```go title="unbounded_knapsack.go" + /* 完全背包:空間最佳化後的動態規劃 */ + func unboundedKnapsackDPComp(wgt, val []int, cap int) int { + n := len(wgt) + // 初始化 dp 表 + dp := make([]int, cap+1) + // 狀態轉移 + for i := 1; i <= n; i++ { + for c := 1; c <= cap; c++ { + if wgt[i-1] > c { + // 若超過背包容量,則不選物品 i + dp[c] = dp[c] + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[c] = int(math.Max(float64(dp[c]), float64(dp[c-wgt[i-1]]+val[i-1]))) + } + } + } + return dp[cap] + } + ``` + +=== "Swift" + + ```swift title="unbounded_knapsack.swift" + /* 完全背包:空間最佳化後的動態規劃 */ + func unboundedKnapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int { + let n = wgt.count + // 初始化 dp 表 + var dp = Array(repeating: 0, count: cap + 1) + // 狀態轉移 + for i in 1 ... n { + for c in 1 ... cap { + if wgt[i - 1] > c { + // 若超過背包容量,則不選物品 i + dp[c] = dp[c] + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]) + } + } + } + return dp[cap] + } + ``` + +=== "JS" + + ```javascript title="unbounded_knapsack.js" + /* 完全背包:狀態壓縮後的動態規劃 */ + function unboundedKnapsackDPComp(wgt, val, cap) { + const n = wgt.length; + // 初始化 dp 表 + const dp = Array.from({ length: cap + 1 }, () => 0); + // 狀態轉移 + for (let i = 1; i <= n; i++) { + for (let c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[c] = dp[c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } + ``` + +=== "TS" + + ```typescript title="unbounded_knapsack.ts" + /* 完全背包:狀態壓縮後的動態規劃 */ + function unboundedKnapsackDPComp( + wgt: Array, + val: Array, + cap: number + ): number { + const n = wgt.length; + // 初始化 dp 表 + const dp = Array.from({ length: cap + 1 }, () => 0); + // 狀態轉移 + for (let i = 1; i <= n; i++) { + for (let c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[c] = dp[c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } + ``` + +=== "Dart" + + ```dart title="unbounded_knapsack.dart" + /* 完全背包:空間最佳化後的動態規劃 */ + int unboundedKnapsackDPComp(List wgt, List val, int cap) { + int n = wgt.length; + // 初始化 dp 表 + List dp = List.filled(cap + 1, 0); + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[c] = dp[c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } + ``` + +=== "Rust" + + ```rust title="unbounded_knapsack.rs" + /* 完全背包:空間最佳化後的動態規劃 */ + fn unbounded_knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 { + let n = wgt.len(); + // 初始化 dp 表 + let mut dp = vec![0; cap + 1]; + // 狀態轉移 + for i in 1..=n { + for c in 1..=cap { + if wgt[i - 1] > c as i32 { + // 若超過背包容量,則不選物品 i + dp[c] = dp[c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[c] = std::cmp::max(dp[c], dp[c - wgt[i - 1] as usize] + val[i - 1]); + } + } + } + dp[cap] + } + ``` + +=== "C" + + ```c title="unbounded_knapsack.c" + /* 完全背包:空間最佳化後的動態規劃 */ + int unboundedKnapsackDPComp(int wgt[], int val[], int cap, int wgtSize) { + int n = wgtSize; + // 初始化 dp 表 + int *dp = calloc(cap + 1, sizeof(int)); + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[c] = dp[c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + int res = dp[cap]; + // 釋放記憶體 + free(dp); + return res; + } + ``` + +=== "Kotlin" + + ```kotlin title="unbounded_knapsack.kt" + /* 完全背包:空間最佳化後的動態規劃 */ + fun unboundedKnapsackDPComp( + wgt: IntArray, + value: IntArray, + cap: Int + ): Int { + val n = wgt.size + // 初始化 dp 表 + val dp = IntArray(cap + 1) + // 狀態轉移 + for (i in 1..n) { + for (c in 1..cap) { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[c] = dp[c] + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[c] = + max(dp[c].toDouble(), (dp[c - wgt[i - 1]] + value[i - 1]).toDouble()).toInt() + } + } + } + return dp[cap] + } + ``` + +=== "Ruby" + + ```ruby title="unbounded_knapsack.rb" + [class]{}-[func]{unbounded_knapsack_dp_comp} + ``` + +=== "Zig" + + ```zig title="unbounded_knapsack.zig" + // 完全背包:空間最佳化後的動態規劃 + fn unboundedKnapsackDPComp(comptime wgt: []i32, val: []i32, comptime cap: usize) i32 { + comptime var n = wgt.len; + // 初始化 dp 表 + var dp = [_]i32{0} ** (cap + 1); + // 狀態轉移 + for (1..n + 1) |i| { + for (1..cap + 1) |c| { + if (wgt[i - 1] > c) { + // 若超過背包容量,則不選物品 i + dp[c] = dp[c]; + } else { + // 不選和選物品 i 這兩種方案的較大值 + dp[c] = @max(dp[c], dp[c - @as(usize, @intCast(wgt[i - 1]))] + val[i - 1]); + } + } + } + return dp[cap]; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 14.5.2   零錢兌換問題 + +背包問題是一大類動態規劃問題的代表,其擁有很多變種,例如零錢兌換問題。 + +!!! question + + 給定 $n$ 種硬幣,第 $i$ 種硬幣的面值為 $coins[i - 1]$ ,目標金額為 $amt$ ,**每種硬幣可以重複選取**,問能夠湊出目標金額的最少硬幣數量。如果無法湊出目標金額,則返回 $-1$ 。示例如圖 14-24 所示。 + +![零錢兌換問題的示例資料](unbounded_knapsack_problem.assets/coin_change_example.png){ class="animation-figure" } + +

圖 14-24   零錢兌換問題的示例資料

+ +### 1.   動態規劃思路 + +**零錢兌換可以看作完全背包問題的一種特殊情況**,兩者具有以下關聯與不同點。 + +- 兩道題可以相互轉換,“物品”對應“硬幣”、“物品重量”對應“硬幣面值”、“背包容量”對應“目標金額”。 +- 最佳化目標相反,完全背包問題是要最大化物品價值,零錢兌換問題是要最小化硬幣數量。 +- 完全背包問題是求“不超過”背包容量下的解,零錢兌換是求“恰好”湊到目標金額的解。 + +**第一步:思考每輪的決策,定義狀態,從而得到 $dp$ 表** + +狀態 $[i, a]$ 對應的子問題為:**前 $i$ 種硬幣能夠湊出金額 $a$ 的最少硬幣數量**,記為 $dp[i, a]$ 。 + +二維 $dp$ 表的尺寸為 $(n+1) \times (amt+1)$ 。 + +**第二步:找出最優子結構,進而推導出狀態轉移方程** + +本題與完全背包問題的狀態轉移方程存在以下兩點差異。 + +- 本題要求最小值,因此需將運算子 $\max()$ 更改為 $\min()$ 。 +- 最佳化主體是硬幣數量而非商品價值,因此在選中硬幣時執行 $+1$ 即可。 + +$$ +dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) +$$ + +**第三步:確定邊界條件和狀態轉移順序** + +當目標金額為 $0$ 時,湊出它的最少硬幣數量為 $0$ ,即首列所有 $dp[i, 0]$ 都等於 $0$ 。 + +當無硬幣時,**無法湊出任意 $> 0$ 的目標金額**,即是無效解。為使狀態轉移方程中的 $\min()$ 函式能夠識別並過濾無效解,我們考慮使用 $+ \infty$ 來表示它們,即令首行所有 $dp[0, a]$ 都等於 $+ \infty$ 。 + +### 2.   程式碼實現 + +大多數程式語言並未提供 $+ \infty$ 變數,只能使用整型 `int` 的最大值來代替。而這又會導致大數越界:狀態轉移方程中的 $+ 1$ 操作可能發生溢位。 + +為此,我們採用數字 $amt + 1$ 來表示無效解,因為湊出 $amt$ 的硬幣數量最多為 $amt$ 。最後返回前,判斷 $dp[n, amt]$ 是否等於 $amt + 1$ ,若是則返回 $-1$ ,代表無法湊出目標金額。程式碼如下所示: + +=== "Python" + + ```python title="coin_change.py" + def coin_change_dp(coins: list[int], amt: int) -> int: + """零錢兌換:動態規劃""" + n = len(coins) + MAX = amt + 1 + # 初始化 dp 表 + dp = [[0] * (amt + 1) for _ in range(n + 1)] + # 狀態轉移:首行首列 + for a in range(1, amt + 1): + dp[0][a] = MAX + # 狀態轉移:其餘行和列 + for i in range(1, n + 1): + for a in range(1, amt + 1): + if coins[i - 1] > a: + # 若超過目標金額,則不選硬幣 i + dp[i][a] = dp[i - 1][a] + else: + # 不選和選硬幣 i 這兩種方案的較小值 + dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1) + return dp[n][amt] if dp[n][amt] != MAX else -1 + ``` + +=== "C++" + + ```cpp title="coin_change.cpp" + /* 零錢兌換:動態規劃 */ + int coinChangeDP(vector &coins, int amt) { + int n = coins.size(); + int MAX = amt + 1; + // 初始化 dp 表 + vector> dp(n + 1, vector(amt + 1, 0)); + // 狀態轉移:首行首列 + for (int a = 1; a <= amt; a++) { + dp[0][a] = MAX; + } + // 狀態轉移:其餘行和列 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[i][a] = dp[i - 1][a]; + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1); + } + } + } + return dp[n][amt] != MAX ? dp[n][amt] : -1; + } + ``` + +=== "Java" + + ```java title="coin_change.java" + /* 零錢兌換:動態規劃 */ + int coinChangeDP(int[] coins, int amt) { + int n = coins.length; + int MAX = amt + 1; + // 初始化 dp 表 + int[][] dp = new int[n + 1][amt + 1]; + // 狀態轉移:首行首列 + for (int a = 1; a <= amt; a++) { + dp[0][a] = MAX; + } + // 狀態轉移:其餘行和列 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[i][a] = dp[i - 1][a]; + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1); + } + } + } + return dp[n][amt] != MAX ? dp[n][amt] : -1; + } + ``` + +=== "C#" + + ```csharp title="coin_change.cs" + /* 零錢兌換:動態規劃 */ + int CoinChangeDP(int[] coins, int amt) { + int n = coins.Length; + int MAX = amt + 1; + // 初始化 dp 表 + int[,] dp = new int[n + 1, amt + 1]; + // 狀態轉移:首行首列 + for (int a = 1; a <= amt; a++) { + dp[0, a] = MAX; + } + // 狀態轉移:其餘行和列 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[i, a] = dp[i - 1, a]; + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[i, a] = Math.Min(dp[i - 1, a], dp[i, a - coins[i - 1]] + 1); + } + } + } + return dp[n, amt] != MAX ? dp[n, amt] : -1; + } + ``` + +=== "Go" + + ```go title="coin_change.go" + /* 零錢兌換:動態規劃 */ + func coinChangeDP(coins []int, amt int) int { + n := len(coins) + max := amt + 1 + // 初始化 dp 表 + dp := make([][]int, n+1) + for i := 0; i <= n; i++ { + dp[i] = make([]int, amt+1) + } + // 狀態轉移:首行首列 + for a := 1; a <= amt; a++ { + dp[0][a] = max + } + // 狀態轉移:其餘行和列 + for i := 1; i <= n; i++ { + for a := 1; a <= amt; a++ { + if coins[i-1] > a { + // 若超過目標金額,則不選硬幣 i + dp[i][a] = dp[i-1][a] + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[i][a] = int(math.Min(float64(dp[i-1][a]), float64(dp[i][a-coins[i-1]]+1))) + } + } + } + if dp[n][amt] != max { + return dp[n][amt] + } + return -1 + } + ``` + +=== "Swift" + + ```swift title="coin_change.swift" + /* 零錢兌換:動態規劃 */ + func coinChangeDP(coins: [Int], amt: Int) -> Int { + let n = coins.count + let MAX = amt + 1 + // 初始化 dp 表 + var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1) + // 狀態轉移:首行首列 + for a in 1 ... amt { + dp[0][a] = MAX + } + // 狀態轉移:其餘行和列 + for i in 1 ... n { + for a in 1 ... amt { + if coins[i - 1] > a { + // 若超過目標金額,則不選硬幣 i + dp[i][a] = dp[i - 1][a] + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1) + } + } + } + return dp[n][amt] != MAX ? dp[n][amt] : -1 + } + ``` + +=== "JS" + + ```javascript title="coin_change.js" + /* 零錢兌換:動態規劃 */ + function coinChangeDP(coins, amt) { + const n = coins.length; + const MAX = amt + 1; + // 初始化 dp 表 + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: amt + 1 }, () => 0) + ); + // 狀態轉移:首行首列 + for (let a = 1; a <= amt; a++) { + dp[0][a] = MAX; + } + // 狀態轉移:其餘行和列 + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[i][a] = dp[i - 1][a]; + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1); + } + } + } + return dp[n][amt] !== MAX ? dp[n][amt] : -1; + } + ``` + +=== "TS" + + ```typescript title="coin_change.ts" + /* 零錢兌換:動態規劃 */ + function coinChangeDP(coins: Array, amt: number): number { + const n = coins.length; + const MAX = amt + 1; + // 初始化 dp 表 + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: amt + 1 }, () => 0) + ); + // 狀態轉移:首行首列 + for (let a = 1; a <= amt; a++) { + dp[0][a] = MAX; + } + // 狀態轉移:其餘行和列 + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[i][a] = dp[i - 1][a]; + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1); + } + } + } + return dp[n][amt] !== MAX ? dp[n][amt] : -1; + } + ``` + +=== "Dart" + + ```dart title="coin_change.dart" + /* 零錢兌換:動態規劃 */ + int coinChangeDP(List coins, int amt) { + int n = coins.length; + int MAX = amt + 1; + // 初始化 dp 表 + List> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0)); + // 狀態轉移:首行首列 + for (int a = 1; a <= amt; a++) { + dp[0][a] = MAX; + } + // 狀態轉移:其餘行和列 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[i][a] = dp[i - 1][a]; + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1); + } + } + } + return dp[n][amt] != MAX ? dp[n][amt] : -1; + } + ``` + +=== "Rust" + + ```rust title="coin_change.rs" + /* 零錢兌換:動態規劃 */ + fn coin_change_dp(coins: &[i32], amt: usize) -> i32 { + let n = coins.len(); + let max = amt + 1; + // 初始化 dp 表 + let mut dp = vec![vec![0; amt + 1]; n + 1]; + // 狀態轉移:首行首列 + for a in 1..=amt { + dp[0][a] = max; + } + // 狀態轉移:其餘行和列 + for i in 1..=n { + for a in 1..=amt { + if coins[i - 1] > a as i32 { + // 若超過目標金額,則不選硬幣 i + dp[i][a] = dp[i - 1][a]; + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[i][a] = std::cmp::min(dp[i - 1][a], dp[i][a - coins[i - 1] as usize] + 1); + } + } + } + if dp[n][amt] != max { + return dp[n][amt] as i32; + } else { + -1 + } + } + ``` + +=== "C" + + ```c title="coin_change.c" + /* 零錢兌換:動態規劃 */ + int coinChangeDP(int coins[], int amt, int coinsSize) { + int n = coinsSize; + int MAX = amt + 1; + // 初始化 dp 表 + int **dp = malloc((n + 1) * sizeof(int *)); + for (int i = 0; i <= n; i++) { + dp[i] = calloc(amt + 1, sizeof(int)); + } + // 狀態轉移:首行首列 + for (int a = 1; a <= amt; a++) { + dp[0][a] = MAX; + } + // 狀態轉移:其餘行和列 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[i][a] = dp[i - 1][a]; + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[i][a] = myMin(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1); + } + } + } + int res = dp[n][amt] != MAX ? dp[n][amt] : -1; + // 釋放記憶體 + for (int i = 0; i <= n; i++) { + free(dp[i]); + } + free(dp); + return res; + } + ``` + +=== "Kotlin" + + ```kotlin title="coin_change.kt" + /* 零錢兌換:動態規劃 */ + fun coinChangeDP(coins: IntArray, amt: Int): Int { + val n = coins.size + val MAX = amt + 1 + // 初始化 dp 表 + val dp = Array(n + 1) { IntArray(amt + 1) } + // 狀態轉移:首行首列 + for (a in 1..amt) { + dp[0][a] = MAX + } + // 狀態轉移:其餘行和列 + for (i in 1..n) { + for (a in 1..amt) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[i][a] = dp[i - 1][a] + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[i][a] = min(dp[i - 1][a].toDouble(), (dp[i][a - coins[i - 1]] + 1).toDouble()) + .toInt() + } + } + } + return if (dp[n][amt] != MAX) dp[n][amt] else -1 + } + ``` + +=== "Ruby" + + ```ruby title="coin_change.rb" + [class]{}-[func]{coin_change_dp} + ``` + +=== "Zig" + + ```zig title="coin_change.zig" + // 零錢兌換:動態規劃 + fn coinChangeDP(comptime coins: []i32, comptime amt: usize) i32 { + comptime var n = coins.len; + comptime var max = amt + 1; + // 初始化 dp 表 + var dp = [_][amt + 1]i32{[_]i32{0} ** (amt + 1)} ** (n + 1); + // 狀態轉移:首行首列 + for (1..amt + 1) |a| { + dp[0][a] = max; + } + // 狀態轉移:其餘行和列 + for (1..n + 1) |i| { + for (1..amt + 1) |a| { + if (coins[i - 1] > @as(i32, @intCast(a))) { + // 若超過目標金額,則不選硬幣 i + dp[i][a] = dp[i - 1][a]; + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[i][a] = @min(dp[i - 1][a], dp[i][a - @as(usize, @intCast(coins[i - 1]))] + 1); + } + } + } + if (dp[n][amt] != max) { + return @intCast(dp[n][amt]); + } else { + return -1; + } + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +圖 14-25 展示了零錢兌換的動態規劃過程,和完全背包問題非常相似。 + +=== "<1>" + ![零錢兌換問題的動態規劃過程](unbounded_knapsack_problem.assets/coin_change_dp_step1.png){ class="animation-figure" } + +=== "<2>" + ![coin_change_dp_step2](unbounded_knapsack_problem.assets/coin_change_dp_step2.png){ class="animation-figure" } + +=== "<3>" + ![coin_change_dp_step3](unbounded_knapsack_problem.assets/coin_change_dp_step3.png){ class="animation-figure" } + +=== "<4>" + ![coin_change_dp_step4](unbounded_knapsack_problem.assets/coin_change_dp_step4.png){ class="animation-figure" } + +=== "<5>" + ![coin_change_dp_step5](unbounded_knapsack_problem.assets/coin_change_dp_step5.png){ class="animation-figure" } + +=== "<6>" + ![coin_change_dp_step6](unbounded_knapsack_problem.assets/coin_change_dp_step6.png){ class="animation-figure" } + +=== "<7>" + ![coin_change_dp_step7](unbounded_knapsack_problem.assets/coin_change_dp_step7.png){ class="animation-figure" } + +=== "<8>" + ![coin_change_dp_step8](unbounded_knapsack_problem.assets/coin_change_dp_step8.png){ class="animation-figure" } + +=== "<9>" + ![coin_change_dp_step9](unbounded_knapsack_problem.assets/coin_change_dp_step9.png){ class="animation-figure" } + +=== "<10>" + ![coin_change_dp_step10](unbounded_knapsack_problem.assets/coin_change_dp_step10.png){ class="animation-figure" } + +=== "<11>" + ![coin_change_dp_step11](unbounded_knapsack_problem.assets/coin_change_dp_step11.png){ class="animation-figure" } + +=== "<12>" + ![coin_change_dp_step12](unbounded_knapsack_problem.assets/coin_change_dp_step12.png){ class="animation-figure" } + +=== "<13>" + ![coin_change_dp_step13](unbounded_knapsack_problem.assets/coin_change_dp_step13.png){ class="animation-figure" } + +=== "<14>" + ![coin_change_dp_step14](unbounded_knapsack_problem.assets/coin_change_dp_step14.png){ class="animation-figure" } + +=== "<15>" + ![coin_change_dp_step15](unbounded_knapsack_problem.assets/coin_change_dp_step15.png){ class="animation-figure" } + +

圖 14-25   零錢兌換問題的動態規劃過程

+ +### 3.   空間最佳化 + +零錢兌換的空間最佳化的處理方式和完全背包問題一致: + +=== "Python" + + ```python title="coin_change.py" + def coin_change_dp_comp(coins: list[int], amt: int) -> int: + """零錢兌換:空間最佳化後的動態規劃""" + n = len(coins) + MAX = amt + 1 + # 初始化 dp 表 + dp = [MAX] * (amt + 1) + dp[0] = 0 + # 狀態轉移 + for i in range(1, n + 1): + # 正序走訪 + for a in range(1, amt + 1): + if coins[i - 1] > a: + # 若超過目標金額,則不選硬幣 i + dp[a] = dp[a] + else: + # 不選和選硬幣 i 這兩種方案的較小值 + dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1) + return dp[amt] if dp[amt] != MAX else -1 + ``` + +=== "C++" + + ```cpp title="coin_change.cpp" + /* 零錢兌換:空間最佳化後的動態規劃 */ + int coinChangeDPComp(vector &coins, int amt) { + int n = coins.size(); + int MAX = amt + 1; + // 初始化 dp 表 + vector dp(amt + 1, MAX); + dp[0] = 0; + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[a] = dp[a]; + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1); + } + } + } + return dp[amt] != MAX ? dp[amt] : -1; + } + ``` + +=== "Java" + + ```java title="coin_change.java" + /* 零錢兌換:空間最佳化後的動態規劃 */ + int coinChangeDPComp(int[] coins, int amt) { + int n = coins.length; + int MAX = amt + 1; + // 初始化 dp 表 + int[] dp = new int[amt + 1]; + Arrays.fill(dp, MAX); + dp[0] = 0; + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[a] = dp[a]; + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1); + } + } + } + return dp[amt] != MAX ? dp[amt] : -1; + } + ``` + +=== "C#" + + ```csharp title="coin_change.cs" + /* 零錢兌換:空間最佳化後的動態規劃 */ + int CoinChangeDPComp(int[] coins, int amt) { + int n = coins.Length; + int MAX = amt + 1; + // 初始化 dp 表 + int[] dp = new int[amt + 1]; + Array.Fill(dp, MAX); + dp[0] = 0; + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[a] = dp[a]; + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[a] = Math.Min(dp[a], dp[a - coins[i - 1]] + 1); + } + } + } + return dp[amt] != MAX ? dp[amt] : -1; + } + ``` + +=== "Go" + + ```go title="coin_change.go" + /* 零錢兌換:動態規劃 */ + func coinChangeDPComp(coins []int, amt int) int { + n := len(coins) + max := amt + 1 + // 初始化 dp 表 + dp := make([]int, amt+1) + for i := 1; i <= amt; i++ { + dp[i] = max + } + // 狀態轉移 + for i := 1; i <= n; i++ { + // 倒序走訪 + for a := 1; a <= amt; a++ { + if coins[i-1] > a { + // 若超過目標金額,則不選硬幣 i + dp[a] = dp[a] + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[a] = int(math.Min(float64(dp[a]), float64(dp[a-coins[i-1]]+1))) + } + } + } + if dp[amt] != max { + return dp[amt] + } + return -1 + } + ``` + +=== "Swift" + + ```swift title="coin_change.swift" + /* 零錢兌換:空間最佳化後的動態規劃 */ + func coinChangeDPComp(coins: [Int], amt: Int) -> Int { + let n = coins.count + let MAX = amt + 1 + // 初始化 dp 表 + var dp = Array(repeating: MAX, count: amt + 1) + dp[0] = 0 + // 狀態轉移 + for i in 1 ... n { + for a in 1 ... amt { + if coins[i - 1] > a { + // 若超過目標金額,則不選硬幣 i + dp[a] = dp[a] + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1) + } + } + } + return dp[amt] != MAX ? dp[amt] : -1 + } + ``` + +=== "JS" + + ```javascript title="coin_change.js" + /* 零錢兌換:狀態壓縮後的動態規劃 */ + function coinChangeDPComp(coins, amt) { + const n = coins.length; + const MAX = amt + 1; + // 初始化 dp 表 + const dp = Array.from({ length: amt + 1 }, () => MAX); + dp[0] = 0; + // 狀態轉移 + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[a] = dp[a]; + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1); + } + } + } + return dp[amt] !== MAX ? dp[amt] : -1; + } + ``` + +=== "TS" + + ```typescript title="coin_change.ts" + /* 零錢兌換:狀態壓縮後的動態規劃 */ + function coinChangeDPComp(coins: Array, amt: number): number { + const n = coins.length; + const MAX = amt + 1; + // 初始化 dp 表 + const dp = Array.from({ length: amt + 1 }, () => MAX); + dp[0] = 0; + // 狀態轉移 + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[a] = dp[a]; + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1); + } + } + } + return dp[amt] !== MAX ? dp[amt] : -1; + } + ``` + +=== "Dart" + + ```dart title="coin_change.dart" + /* 零錢兌換:空間最佳化後的動態規劃 */ + int coinChangeDPComp(List coins, int amt) { + int n = coins.length; + int MAX = amt + 1; + // 初始化 dp 表 + List dp = List.filled(amt + 1, MAX); + dp[0] = 0; + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[a] = dp[a]; + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1); + } + } + } + return dp[amt] != MAX ? dp[amt] : -1; + } + ``` + +=== "Rust" + + ```rust title="coin_change.rs" + /* 零錢兌換:空間最佳化後的動態規劃 */ + fn coin_change_dp_comp(coins: &[i32], amt: usize) -> i32 { + let n = coins.len(); + let max = amt + 1; + // 初始化 dp 表 + let mut dp = vec![0; amt + 1]; + dp.fill(max); + dp[0] = 0; + // 狀態轉移 + for i in 1..=n { + for a in 1..=amt { + if coins[i - 1] > a as i32 { + // 若超過目標金額,則不選硬幣 i + dp[a] = dp[a]; + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[a] = std::cmp::min(dp[a], dp[a - coins[i - 1] as usize] + 1); + } + } + } + if dp[amt] != max { + return dp[amt] as i32; + } else { + -1 + } + } + ``` + +=== "C" + + ```c title="coin_change.c" + /* 零錢兌換:空間最佳化後的動態規劃 */ + int coinChangeDPComp(int coins[], int amt, int coinsSize) { + int n = coinsSize; + int MAX = amt + 1; + // 初始化 dp 表 + int *dp = calloc(amt + 1, sizeof(int)); + dp[0] = 0; + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[a] = dp[a]; + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[a] = myMin(dp[a], dp[a - coins[i - 1]] + 1); + } + } + } + int res = dp[amt] != MAX ? dp[amt] : -1; + // 釋放記憶體 + free(dp); + return res; + } + ``` + +=== "Kotlin" + + ```kotlin title="coin_change.kt" + /* 零錢兌換:空間最佳化後的動態規劃 */ + fun coinChangeDPComp(coins: IntArray, amt: Int): Int { + val n = coins.size + val MAX = amt + 1 + // 初始化 dp 表 + val dp = IntArray(amt + 1) + Arrays.fill(dp, MAX) + dp[0] = 0 + // 狀態轉移 + for (i in 1..n) { + for (a in 1..amt) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[a] = dp[a] + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[a] = min(dp[a].toDouble(), (dp[a - coins[i - 1]] + 1).toDouble()).toInt() + } + } + } + return if (dp[amt] != MAX) dp[amt] else -1 + } + ``` + +=== "Ruby" + + ```ruby title="coin_change.rb" + [class]{}-[func]{coin_change_dp_comp} + ``` + +=== "Zig" + + ```zig title="coin_change.zig" + // 零錢兌換:空間最佳化後的動態規劃 + fn coinChangeDPComp(comptime coins: []i32, comptime amt: usize) i32 { + comptime var n = coins.len; + comptime var max = amt + 1; + // 初始化 dp 表 + var dp = [_]i32{0} ** (amt + 1); + @memset(&dp, max); + dp[0] = 0; + // 狀態轉移 + for (1..n + 1) |i| { + for (1..amt + 1) |a| { + if (coins[i - 1] > @as(i32, @intCast(a))) { + // 若超過目標金額,則不選硬幣 i + dp[a] = dp[a]; + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[a] = @min(dp[a], dp[a - @as(usize, @intCast(coins[i - 1]))] + 1); + } + } + } + if (dp[amt] != max) { + return @intCast(dp[amt]); + } else { + return -1; + } + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 14.5.3   零錢兌換問題 II + +!!! question + + 給定 $n$ 種硬幣,第 $i$ 種硬幣的面值為 $coins[i - 1]$ ,目標金額為 $amt$ ,每種硬幣可以重複選取,**問湊出目標金額的硬幣組合數量**。示例如圖 14-26 所示。 + +![零錢兌換問題 II 的示例資料](unbounded_knapsack_problem.assets/coin_change_ii_example.png){ class="animation-figure" } + +

圖 14-26   零錢兌換問題 II 的示例資料

+ +### 1.   動態規劃思路 + +相比於上一題,本題目標是求組合數量,因此子問題變為:**前 $i$ 種硬幣能夠湊出金額 $a$ 的組合數量**。而 $dp$ 表仍然是尺寸為 $(n+1) \times (amt + 1)$ 的二維矩陣。 + +當前狀態的組合數量等於不選當前硬幣與選當前硬幣這兩種決策的組合數量之和。狀態轉移方程為: + +$$ +dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]] +$$ + +當目標金額為 $0$ 時,無須選擇任何硬幣即可湊出目標金額,因此應將首列所有 $dp[i, 0]$ 都初始化為 $1$ 。當無硬幣時,無法湊出任何 $>0$ 的目標金額,因此首行所有 $dp[0, a]$ 都等於 $0$ 。 + +### 2.   程式碼實現 + +=== "Python" + + ```python title="coin_change_ii.py" + def coin_change_ii_dp(coins: list[int], amt: int) -> int: + """零錢兌換 II:動態規劃""" + n = len(coins) + # 初始化 dp 表 + dp = [[0] * (amt + 1) for _ in range(n + 1)] + # 初始化首列 + for i in range(n + 1): + dp[i][0] = 1 + # 狀態轉移 + for i in range(1, n + 1): + for a in range(1, amt + 1): + if coins[i - 1] > a: + # 若超過目標金額,則不選硬幣 i + dp[i][a] = dp[i - 1][a] + else: + # 不選和選硬幣 i 這兩種方案之和 + dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]] + return dp[n][amt] + ``` + +=== "C++" + + ```cpp title="coin_change_ii.cpp" + /* 零錢兌換 II:動態規劃 */ + int coinChangeIIDP(vector &coins, int amt) { + int n = coins.size(); + // 初始化 dp 表 + vector> dp(n + 1, vector(amt + 1, 0)); + // 初始化首列 + for (int i = 0; i <= n; i++) { + dp[i][0] = 1; + } + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[i][a] = dp[i - 1][a]; + } else { + // 不選和選硬幣 i 這兩種方案之和 + dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]; + } + } + } + return dp[n][amt]; + } + ``` + +=== "Java" + + ```java title="coin_change_ii.java" + /* 零錢兌換 II:動態規劃 */ + int coinChangeIIDP(int[] coins, int amt) { + int n = coins.length; + // 初始化 dp 表 + int[][] dp = new int[n + 1][amt + 1]; + // 初始化首列 + for (int i = 0; i <= n; i++) { + dp[i][0] = 1; + } + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[i][a] = dp[i - 1][a]; + } else { + // 不選和選硬幣 i 這兩種方案之和 + dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]; + } + } + } + return dp[n][amt]; + } + ``` + +=== "C#" + + ```csharp title="coin_change_ii.cs" + /* 零錢兌換 II:動態規劃 */ + int CoinChangeIIDP(int[] coins, int amt) { + int n = coins.Length; + // 初始化 dp 表 + int[,] dp = new int[n + 1, amt + 1]; + // 初始化首列 + for (int i = 0; i <= n; i++) { + dp[i, 0] = 1; + } + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[i, a] = dp[i - 1, a]; + } else { + // 不選和選硬幣 i 這兩種方案之和 + dp[i, a] = dp[i - 1, a] + dp[i, a - coins[i - 1]]; + } + } + } + return dp[n, amt]; + } + ``` + +=== "Go" + + ```go title="coin_change_ii.go" + /* 零錢兌換 II:動態規劃 */ + func coinChangeIIDP(coins []int, amt int) int { + n := len(coins) + // 初始化 dp 表 + dp := make([][]int, n+1) + for i := 0; i <= n; i++ { + dp[i] = make([]int, amt+1) + } + // 初始化首列 + for i := 0; i <= n; i++ { + dp[i][0] = 1 + } + // 狀態轉移:其餘行和列 + for i := 1; i <= n; i++ { + for a := 1; a <= amt; a++ { + if coins[i-1] > a { + // 若超過目標金額,則不選硬幣 i + dp[i][a] = dp[i-1][a] + } else { + // 不選和選硬幣 i 這兩種方案之和 + dp[i][a] = dp[i-1][a] + dp[i][a-coins[i-1]] + } + } + } + return dp[n][amt] + } + ``` + +=== "Swift" + + ```swift title="coin_change_ii.swift" + /* 零錢兌換 II:動態規劃 */ + func coinChangeIIDP(coins: [Int], amt: Int) -> Int { + let n = coins.count + // 初始化 dp 表 + var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1) + // 初始化首列 + for i in 0 ... n { + dp[i][0] = 1 + } + // 狀態轉移 + for i in 1 ... n { + for a in 1 ... amt { + if coins[i - 1] > a { + // 若超過目標金額,則不選硬幣 i + dp[i][a] = dp[i - 1][a] + } else { + // 不選和選硬幣 i 這兩種方案之和 + dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]] + } + } + } + return dp[n][amt] + } + ``` + +=== "JS" + + ```javascript title="coin_change_ii.js" + /* 零錢兌換 II:動態規劃 */ + function coinChangeIIDP(coins, amt) { + const n = coins.length; + // 初始化 dp 表 + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: amt + 1 }, () => 0) + ); + // 初始化首列 + for (let i = 0; i <= n; i++) { + dp[i][0] = 1; + } + // 狀態轉移 + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[i][a] = dp[i - 1][a]; + } else { + // 不選和選硬幣 i 這兩種方案之和 + dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]; + } + } + } + return dp[n][amt]; + } + ``` + +=== "TS" + + ```typescript title="coin_change_ii.ts" + /* 零錢兌換 II:動態規劃 */ + function coinChangeIIDP(coins: Array, amt: number): number { + const n = coins.length; + // 初始化 dp 表 + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: amt + 1 }, () => 0) + ); + // 初始化首列 + for (let i = 0; i <= n; i++) { + dp[i][0] = 1; + } + // 狀態轉移 + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[i][a] = dp[i - 1][a]; + } else { + // 不選和選硬幣 i 這兩種方案之和 + dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]; + } + } + } + return dp[n][amt]; + } + ``` + +=== "Dart" + + ```dart title="coin_change_ii.dart" + /* 零錢兌換 II:動態規劃 */ + int coinChangeIIDP(List coins, int amt) { + int n = coins.length; + // 初始化 dp 表 + List> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0)); + // 初始化首列 + for (int i = 0; i <= n; i++) { + dp[i][0] = 1; + } + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[i][a] = dp[i - 1][a]; + } else { + // 不選和選硬幣 i 這兩種方案之和 + dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]; + } + } + } + return dp[n][amt]; + } + ``` + +=== "Rust" + + ```rust title="coin_change_ii.rs" + /* 零錢兌換 II:動態規劃 */ + fn coin_change_ii_dp(coins: &[i32], amt: usize) -> i32 { + let n = coins.len(); + // 初始化 dp 表 + let mut dp = vec![vec![0; amt + 1]; n + 1]; + // 初始化首列 + for i in 0..=n { + dp[i][0] = 1; + } + // 狀態轉移 + for i in 1..=n { + for a in 1..=amt { + if coins[i - 1] > a as i32 { + // 若超過目標金額,則不選硬幣 i + dp[i][a] = dp[i - 1][a]; + } else { + // 不選和選硬幣 i 這兩種方案之和 + dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1] as usize]; + } + } + } + dp[n][amt] + } + ``` + +=== "C" + + ```c title="coin_change_ii.c" + /* 零錢兌換 II:動態規劃 */ + int coinChangeIIDP(int coins[], int amt, int coinsSize) { + int n = coinsSize; + // 初始化 dp 表 + int **dp = malloc((n + 1) * sizeof(int *)); + for (int i = 0; i <= n; i++) { + dp[i] = calloc(amt + 1, sizeof(int)); + } + // 初始化首列 + for (int i = 0; i <= n; i++) { + dp[i][0] = 1; + } + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[i][a] = dp[i - 1][a]; + } else { + // 不選和選硬幣 i 這兩種方案之和 + dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]; + } + } + } + int res = dp[n][amt]; + // 釋放記憶體 + for (int i = 0; i <= n; i++) { + free(dp[i]); + } + free(dp); + return res; + } + ``` + +=== "Kotlin" + + ```kotlin title="coin_change_ii.kt" + /* 零錢兌換 II:動態規劃 */ + fun coinChangeIIDP(coins: IntArray, amt: Int): Int { + val n = coins.size + // 初始化 dp 表 + val dp = Array(n + 1) { IntArray(amt + 1) } + // 初始化首列 + for (i in 0..n) { + dp[i][0] = 1 + } + // 狀態轉移 + for (i in 1..n) { + for (a in 1..amt) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[i][a] = dp[i - 1][a] + } else { + // 不選和選硬幣 i 這兩種方案之和 + dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]] + } + } + } + return dp[n][amt] + } + ``` + +=== "Ruby" + + ```ruby title="coin_change_ii.rb" + [class]{}-[func]{coin_change_ii_dp} + ``` + +=== "Zig" + + ```zig title="coin_change_ii.zig" + // 零錢兌換 II:動態規劃 + fn coinChangeIIDP(comptime coins: []i32, comptime amt: usize) i32 { + comptime var n = coins.len; + // 初始化 dp 表 + var dp = [_][amt + 1]i32{[_]i32{0} ** (amt + 1)} ** (n + 1); + // 初始化首列 + for (0..n + 1) |i| { + dp[i][0] = 1; + } + // 狀態轉移 + for (1..n + 1) |i| { + for (1..amt + 1) |a| { + if (coins[i - 1] > @as(i32, @intCast(a))) { + // 若超過目標金額,則不選硬幣 i + dp[i][a] = dp[i - 1][a]; + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[i][a] = dp[i - 1][a] + dp[i][a - @as(usize, @intCast(coins[i - 1]))]; + } + } + } + return dp[n][amt]; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +### 3.   空間最佳化 + +空間最佳化處理方式相同,刪除硬幣維度即可: + +=== "Python" + + ```python title="coin_change_ii.py" + def coin_change_ii_dp_comp(coins: list[int], amt: int) -> int: + """零錢兌換 II:空間最佳化後的動態規劃""" + n = len(coins) + # 初始化 dp 表 + dp = [0] * (amt + 1) + dp[0] = 1 + # 狀態轉移 + for i in range(1, n + 1): + # 正序走訪 + for a in range(1, amt + 1): + if coins[i - 1] > a: + # 若超過目標金額,則不選硬幣 i + dp[a] = dp[a] + else: + # 不選和選硬幣 i 這兩種方案之和 + dp[a] = dp[a] + dp[a - coins[i - 1]] + return dp[amt] + ``` + +=== "C++" + + ```cpp title="coin_change_ii.cpp" + /* 零錢兌換 II:空間最佳化後的動態規劃 */ + int coinChangeIIDPComp(vector &coins, int amt) { + int n = coins.size(); + // 初始化 dp 表 + vector dp(amt + 1, 0); + dp[0] = 1; + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[a] = dp[a]; + } else { + // 不選和選硬幣 i 這兩種方案之和 + dp[a] = dp[a] + dp[a - coins[i - 1]]; + } + } + } + return dp[amt]; + } + ``` + +=== "Java" + + ```java title="coin_change_ii.java" + /* 零錢兌換 II:空間最佳化後的動態規劃 */ + int coinChangeIIDPComp(int[] coins, int amt) { + int n = coins.length; + // 初始化 dp 表 + int[] dp = new int[amt + 1]; + dp[0] = 1; + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[a] = dp[a]; + } else { + // 不選和選硬幣 i 這兩種方案之和 + dp[a] = dp[a] + dp[a - coins[i - 1]]; + } + } + } + return dp[amt]; + } + ``` + +=== "C#" + + ```csharp title="coin_change_ii.cs" + /* 零錢兌換 II:空間最佳化後的動態規劃 */ + int CoinChangeIIDPComp(int[] coins, int amt) { + int n = coins.Length; + // 初始化 dp 表 + int[] dp = new int[amt + 1]; + dp[0] = 1; + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[a] = dp[a]; + } else { + // 不選和選硬幣 i 這兩種方案之和 + dp[a] = dp[a] + dp[a - coins[i - 1]]; + } + } + } + return dp[amt]; + } + ``` + +=== "Go" + + ```go title="coin_change_ii.go" + /* 零錢兌換 II:空間最佳化後的動態規劃 */ + func coinChangeIIDPComp(coins []int, amt int) int { + n := len(coins) + // 初始化 dp 表 + dp := make([]int, amt+1) + dp[0] = 1 + // 狀態轉移 + for i := 1; i <= n; i++ { + // 倒序走訪 + for a := 1; a <= amt; a++ { + if coins[i-1] > a { + // 若超過目標金額,則不選硬幣 i + dp[a] = dp[a] + } else { + // 不選和選硬幣 i 這兩種方案之和 + dp[a] = dp[a] + dp[a-coins[i-1]] + } + } + } + return dp[amt] + } + ``` + +=== "Swift" + + ```swift title="coin_change_ii.swift" + /* 零錢兌換 II:空間最佳化後的動態規劃 */ + func coinChangeIIDPComp(coins: [Int], amt: Int) -> Int { + let n = coins.count + // 初始化 dp 表 + var dp = Array(repeating: 0, count: amt + 1) + dp[0] = 1 + // 狀態轉移 + for i in 1 ... n { + for a in 1 ... amt { + if coins[i - 1] > a { + // 若超過目標金額,則不選硬幣 i + dp[a] = dp[a] + } else { + // 不選和選硬幣 i 這兩種方案之和 + dp[a] = dp[a] + dp[a - coins[i - 1]] + } + } + } + return dp[amt] + } + ``` + +=== "JS" + + ```javascript title="coin_change_ii.js" + /* 零錢兌換 II:狀態壓縮後的動態規劃 */ + function coinChangeIIDPComp(coins, amt) { + const n = coins.length; + // 初始化 dp 表 + const dp = Array.from({ length: amt + 1 }, () => 0); + dp[0] = 1; + // 狀態轉移 + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[a] = dp[a]; + } else { + // 不選和選硬幣 i 這兩種方案之和 + dp[a] = dp[a] + dp[a - coins[i - 1]]; + } + } + } + return dp[amt]; + } + ``` + +=== "TS" + + ```typescript title="coin_change_ii.ts" + /* 零錢兌換 II:狀態壓縮後的動態規劃 */ + function coinChangeIIDPComp(coins: Array, amt: number): number { + const n = coins.length; + // 初始化 dp 表 + const dp = Array.from({ length: amt + 1 }, () => 0); + dp[0] = 1; + // 狀態轉移 + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[a] = dp[a]; + } else { + // 不選和選硬幣 i 這兩種方案之和 + dp[a] = dp[a] + dp[a - coins[i - 1]]; + } + } + } + return dp[amt]; + } + ``` + +=== "Dart" + + ```dart title="coin_change_ii.dart" + /* 零錢兌換 II:空間最佳化後的動態規劃 */ + int coinChangeIIDPComp(List coins, int amt) { + int n = coins.length; + // 初始化 dp 表 + List dp = List.filled(amt + 1, 0); + dp[0] = 1; + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[a] = dp[a]; + } else { + // 不選和選硬幣 i 這兩種方案之和 + dp[a] = dp[a] + dp[a - coins[i - 1]]; + } + } + } + return dp[amt]; + } + ``` + +=== "Rust" + + ```rust title="coin_change_ii.rs" + /* 零錢兌換 II:空間最佳化後的動態規劃 */ + fn coin_change_ii_dp_comp(coins: &[i32], amt: usize) -> i32 { + let n = coins.len(); + // 初始化 dp 表 + let mut dp = vec![0; amt + 1]; + dp[0] = 1; + // 狀態轉移 + for i in 1..=n { + for a in 1..=amt { + if coins[i - 1] > a as i32 { + // 若超過目標金額,則不選硬幣 i + dp[a] = dp[a]; + } else { + // 不選和選硬幣 i 這兩種方案之和 + dp[a] = dp[a] + dp[a - coins[i - 1] as usize]; + } + } + } + dp[amt] + } + ``` + +=== "C" + + ```c title="coin_change_ii.c" + /* 零錢兌換 II:空間最佳化後的動態規劃 */ + int coinChangeIIDPComp(int coins[], int amt, int coinsSize) { + int n = coinsSize; + // 初始化 dp 表 + int *dp = calloc(amt + 1, sizeof(int)); + dp[0] = 1; + // 狀態轉移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[a] = dp[a]; + } else { + // 不選和選硬幣 i 這兩種方案之和 + dp[a] = dp[a] + dp[a - coins[i - 1]]; + } + } + } + int res = dp[amt]; + // 釋放記憶體 + free(dp); + return res; + } + ``` + +=== "Kotlin" + + ```kotlin title="coin_change_ii.kt" + /* 零錢兌換 II:空間最佳化後的動態規劃 */ + fun coinChangeIIDPComp(coins: IntArray, amt: Int): Int { + val n = coins.size + // 初始化 dp 表 + val dp = IntArray(amt + 1) + dp[0] = 1 + // 狀態轉移 + for (i in 1..n) { + for (a in 1..amt) { + if (coins[i - 1] > a) { + // 若超過目標金額,則不選硬幣 i + dp[a] = dp[a] + } else { + // 不選和選硬幣 i 這兩種方案之和 + dp[a] = dp[a] + dp[a - coins[i - 1]] + } + } + } + return dp[amt] + } + ``` + +=== "Ruby" + + ```ruby title="coin_change_ii.rb" + [class]{}-[func]{coin_change_ii_dp_comp} + ``` + +=== "Zig" + + ```zig title="coin_change_ii.zig" + // 零錢兌換 II:空間最佳化後的動態規劃 + fn coinChangeIIDPComp(comptime coins: []i32, comptime amt: usize) i32 { + comptime var n = coins.len; + // 初始化 dp 表 + var dp = [_]i32{0} ** (amt + 1); + dp[0] = 1; + // 狀態轉移 + for (1..n + 1) |i| { + for (1..amt + 1) |a| { + if (coins[i - 1] > @as(i32, @intCast(a))) { + // 若超過目標金額,則不選硬幣 i + dp[a] = dp[a]; + } else { + // 不選和選硬幣 i 這兩種方案的較小值 + dp[a] = dp[a] + dp[a - @as(usize, @intCast(coins[i - 1]))]; + } + } + } + return dp[amt]; + } + ``` + +??? pythontutor "視覺化執行" + +
+ diff --git a/zh-Hant/docs/chapter_graph/graph.md b/zh-Hant/docs/chapter_graph/graph.md new file mode 100644 index 000000000..886c3b591 --- /dev/null +++ b/zh-Hant/docs/chapter_graph/graph.md @@ -0,0 +1,103 @@ +--- +comments: true +--- + +# 9.1   圖 + +圖(graph)是一種非線性資料結構,由頂點(vertex)邊(edge)組成。我們可以將圖 $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 所示,**相較於線性關係(鏈結串列)和分治關係(樹),網路關係(圖)的自由度更高**,因而更為複雜。 + +![鏈結串列、樹、圖之間的關係](graph.assets/linkedlist_tree_graph.png){ class="animation-figure" } + +

圖 9-1   鏈結串列、樹、圖之間的關係

+ +## 9.1.1   圖的常見型別與術語 + +根據邊是否具有方向,可分為無向圖(undirected graph)有向圖(directed graph),如圖 9-2 所示。 + +- 在無向圖中,邊表示兩頂點之間的“雙向”連線關係,例如微信或 QQ 中的“好友關係”。 +- 在有向圖中,邊具有方向性,即 $A \rightarrow B$ 和 $A \leftarrow B$ 兩個方向的邊是相互獨立的,例如微博或抖音上的“關注”與“被關注”關係。 + +![有向圖與無向圖](graph.assets/directed_graph.png){ class="animation-figure" } + +

圖 9-2   有向圖與無向圖

+ +根據所有頂點是否連通,可分為連通圖(connected graph)非連通圖(disconnected graph),如圖 9-3 所示。 + +- 對於連通圖,從某個頂點出發,可以到達其餘任意頂點。 +- 對於非連通圖,從某個頂點出發,至少有一個頂點無法到達。 + +![連通圖與非連通圖](graph.assets/connected_graph.png){ class="animation-figure" } + +

圖 9-3   連通圖與非連通圖

+ +我們還可以為邊新增“權重”變數,從而得到如圖 9-4 所示的有權圖(weighted graph)。例如在《王者榮耀》等手遊中,系統會根據共同遊戲時間來計算玩家之間的“親密度”,這種親密度網路就可以用有權圖來表示。 + +![有權圖與無權圖](graph.assets/weighted_graph.png){ class="animation-figure" } + +

圖 9-4   有權圖與無權圖

+ +圖資料結構包含以下常用術語。 + +- 鄰接(adjacency):當兩頂點之間存在邊相連時,稱這兩頂點“鄰接”。在圖 9-4 中,頂點 1 的鄰接頂點為頂點 2、3、5。 +- 路徑(path):從頂點 A 到頂點 B 經過的邊構成的序列被稱為從 A 到 B 的“路徑”。在圖 9-4 中,邊序列 1-5-2-4 是頂點 1 到頂點 4 的一條路徑。 +- 度(degree):一個頂點擁有的邊數。對於有向圖,入度(in-degree)表示有多少條邊指向該頂點,出度(out-degree)表示有多少條邊從該頂點指出。 + +## 9.1.2   圖的表示 + +圖的常用表示方式包括“鄰接矩陣”和“鄰接表”。以下使用無向圖進行舉例。 + +### 1.   鄰接矩陣 + +設圖的頂點數量為 $n$ ,鄰接矩陣(adjacency matrix)使用一個 $n \times n$ 大小的矩陣來表示圖,每一行(列)代表一個頂點,矩陣元素代表邊,用 $1$ 或 $0$ 表示兩個頂點之間是否存在邊。 + +如圖 9-5 所示,設鄰接矩陣為 $M$、頂點串列為 $V$ ,那麼矩陣元素 $M[i, j] = 1$ 表示頂點 $V[i]$ 到頂點 $V[j]$ 之間存在邊,反之 $M[i, j] = 0$ 表示兩頂點之間無邊。 + +![圖的鄰接矩陣表示](graph.assets/adjacency_matrix.png){ class="animation-figure" } + +

圖 9-5   圖的鄰接矩陣表示

+ +鄰接矩陣具有以下特性。 + +- 頂點不能與自身相連,因此鄰接矩陣主對角線元素沒有意義。 +- 對於無向圖,兩個方向的邊等價,此時鄰接矩陣關於主對角線對稱。 +- 將鄰接矩陣的元素從 $1$ 和 $0$ 替換為權重,則可表示有權圖。 + +使用鄰接矩陣表示圖時,我們可以直接訪問矩陣元素以獲取邊,因此增刪查改操作的效率很高,時間複雜度均為 $O(1)$ 。然而,矩陣的空間複雜度為 $O(n^2)$ ,記憶體佔用較多。 + +### 2.   鄰接表 + +鄰接表(adjacency list)使用 $n$ 個鏈結串列來表示圖,鏈結串列節點表示頂點。第 $i$ 個鏈結串列對應頂點 $i$ ,其中儲存了該頂點的所有鄰接頂點(與該頂點相連的頂點)。圖 9-6 展示了一個使用鄰接表儲存的圖的示例。 + +![圖的鄰接表表示](graph.assets/adjacency_list.png){ class="animation-figure" } + +

圖 9-6   圖的鄰接表表示

+ +鄰接表僅儲存實際存在的邊,而邊的總數通常遠小於 $n^2$ ,因此它更加節省空間。然而,在鄰接表中需要透過走訪鏈結串列來查詢邊,因此其時間效率不如鄰接矩陣。 + +觀察圖 9-6 ,**鄰接表結構與雜湊表中的“鏈式位址”非常相似,因此我們也可以採用類似的方法來最佳化效率**。比如當鏈結串列較長時,可以將鏈結串列轉化為 AVL 樹或紅黑樹,從而將時間效率從 $O(n)$ 最佳化至 $O(\log n)$ ;還可以把鏈結串列轉換為雜湊表,從而將時間複雜度降至 $O(1)$ 。 + +## 9.1.3   圖的常見應用 + +如表 9-1 所示,許多現實系統可以用圖來建模,相應的問題也可以約化為圖計算問題。 + +

表 9-1   現實生活中常見的圖

+ +
+ +| | 頂點 | 邊 | 圖計算問題 | +| -------- | ---- | -------------------- | ------------ | +| 社交網路 | 使用者 | 好友關係 | 潛在好友推薦 | +| 地鐵線路 | 站點 | 站點間的連通性 | 最短路線推薦 | +| 太陽系 | 星體 | 星體間的萬有引力作用 | 行星軌道計算 | + +
diff --git a/zh-Hant/docs/chapter_graph/graph_operations.md b/zh-Hant/docs/chapter_graph/graph_operations.md new file mode 100644 index 000000000..39fe82a77 --- /dev/null +++ b/zh-Hant/docs/chapter_graph/graph_operations.md @@ -0,0 +1,2266 @@ +--- +comments: true +--- + +# 9.2   圖的基礎操作 + +圖的基礎操作可分為對“邊”的操作和對“頂點”的操作。在“鄰接矩陣”和“鄰接表”兩種表示方法下,實現方式有所不同。 + +## 9.2.1   基於鄰接矩陣的實現 + +給定一個頂點數量為 $n$ 的無向圖,則各種操作的實現方式如圖 9-7 所示。 + +- **新增或刪除邊**:直接在鄰接矩陣中修改指定的邊即可,使用 $O(1)$ 時間。而由於是無向圖,因此需要同時更新兩個方向的邊。 +- **新增頂點**:在鄰接矩陣的尾部新增一行一列,並全部填 $0$ 即可,使用 $O(n)$ 時間。 +- **刪除頂點**:在鄰接矩陣中刪除一行一列。當刪除首行首列時達到最差情況,需要將 $(n-1)^2$ 個元素“向左上移動”,從而使用 $O(n^2)$ 時間。 +- **初始化**:傳入 $n$ 個頂點,初始化長度為 $n$ 的頂點串列 `vertices` ,使用 $O(n)$ 時間;初始化 $n \times n$ 大小的鄰接矩陣 `adjMat` ,使用 $O(n^2)$ 時間。 + +=== "初始化鄰接矩陣" + ![鄰接矩陣的初始化、增刪邊、增刪頂點](graph_operations.assets/adjacency_matrix_step1_initialization.png){ class="animation-figure" } + +=== "新增邊" + ![adjacency_matrix_add_edge](graph_operations.assets/adjacency_matrix_step2_add_edge.png){ class="animation-figure" } + +=== "刪除邊" + ![adjacency_matrix_remove_edge](graph_operations.assets/adjacency_matrix_step3_remove_edge.png){ class="animation-figure" } + +=== "新增頂點" + ![adjacency_matrix_add_vertex](graph_operations.assets/adjacency_matrix_step4_add_vertex.png){ class="animation-figure" } + +=== "刪除頂點" + ![adjacency_matrix_remove_vertex](graph_operations.assets/adjacency_matrix_step5_remove_vertex.png){ class="animation-figure" } + +

圖 9-7   鄰接矩陣的初始化、增刪邊、增刪頂點

+ +以下是基於鄰接矩陣表示圖的實現程式碼: + +=== "Python" + + ```python title="graph_adjacency_matrix.py" + class GraphAdjMat: + """基於鄰接矩陣實現的無向圖類別""" + + def __init__(self, vertices: list[int], edges: list[list[int]]): + """建構子""" + # 頂點串列,元素代表“頂點值”,索引代表“頂點索引” + self.vertices: list[int] = [] + # 鄰接矩陣,行列索引對應“頂點索引” + self.adj_mat: list[list[int]] = [] + # 新增頂點 + for val in vertices: + self.add_vertex(val) + # 新增邊 + # 請注意,edges 元素代表頂點索引,即對應 vertices 元素索引 + for e in edges: + self.add_edge(e[0], e[1]) + + def size(self) -> int: + """獲取頂點數量""" + return len(self.vertices) + + def add_vertex(self, val: int): + """新增頂點""" + n = self.size() + # 向頂點串列中新增新頂點的值 + self.vertices.append(val) + # 在鄰接矩陣中新增一行 + new_row = [0] * n + self.adj_mat.append(new_row) + # 在鄰接矩陣中新增一列 + for row in self.adj_mat: + row.append(0) + + def remove_vertex(self, index: int): + """刪除頂點""" + if index >= self.size(): + raise IndexError() + # 在頂點串列中移除索引 index 的頂點 + self.vertices.pop(index) + # 在鄰接矩陣中刪除索引 index 的行 + self.adj_mat.pop(index) + # 在鄰接矩陣中刪除索引 index 的列 + for row in self.adj_mat: + row.pop(index) + + def add_edge(self, i: int, j: int): + """新增邊""" + # 參數 i, j 對應 vertices 元素索引 + # 索引越界與相等處理 + if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j: + raise IndexError() + # 在無向圖中,鄰接矩陣關於主對角線對稱,即滿足 (i, j) == (j, i) + self.adj_mat[i][j] = 1 + self.adj_mat[j][i] = 1 + + def remove_edge(self, i: int, j: int): + """刪除邊""" + # 參數 i, j 對應 vertices 元素索引 + # 索引越界與相等處理 + if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j: + raise IndexError() + self.adj_mat[i][j] = 0 + self.adj_mat[j][i] = 0 + + def print(self): + """列印鄰接矩陣""" + print("頂點串列 =", self.vertices) + print("鄰接矩陣 =") + print_matrix(self.adj_mat) + ``` + +=== "C++" + + ```cpp title="graph_adjacency_matrix.cpp" + /* 基於鄰接矩陣實現的無向圖類別 */ + class GraphAdjMat { + vector vertices; // 頂點串列,元素代表“頂點值”,索引代表“頂點索引” + vector> adjMat; // 鄰接矩陣,行列索引對應“頂點索引” + + public: + /* 建構子 */ + GraphAdjMat(const vector &vertices, const vector> &edges) { + // 新增頂點 + for (int val : vertices) { + addVertex(val); + } + // 新增邊 + // 請注意,edges 元素代表頂點索引,即對應 vertices 元素索引 + for (const vector &edge : edges) { + addEdge(edge[0], edge[1]); + } + } + + /* 獲取頂點數量 */ + int size() const { + return vertices.size(); + } + + /* 新增頂點 */ + void addVertex(int val) { + int n = size(); + // 向頂點串列中新增新頂點的值 + vertices.push_back(val); + // 在鄰接矩陣中新增一行 + adjMat.emplace_back(vector(n, 0)); + // 在鄰接矩陣中新增一列 + for (vector &row : adjMat) { + row.push_back(0); + } + } + + /* 刪除頂點 */ + void removeVertex(int index) { + if (index >= size()) { + throw out_of_range("頂點不存在"); + } + // 在頂點串列中移除索引 index 的頂點 + vertices.erase(vertices.begin() + index); + // 在鄰接矩陣中刪除索引 index 的行 + adjMat.erase(adjMat.begin() + index); + // 在鄰接矩陣中刪除索引 index 的列 + for (vector &row : adjMat) { + row.erase(row.begin() + index); + } + } + + /* 新增邊 */ + // 參數 i, j 對應 vertices 元素索引 + void addEdge(int i, int j) { + // 索引越界與相等處理 + if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) { + throw out_of_range("頂點不存在"); + } + // 在無向圖中,鄰接矩陣關於主對角線對稱,即滿足 (i, j) == (j, i) + adjMat[i][j] = 1; + adjMat[j][i] = 1; + } + + /* 刪除邊 */ + // 參數 i, j 對應 vertices 元素索引 + void removeEdge(int i, int j) { + // 索引越界與相等處理 + if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) { + throw out_of_range("頂點不存在"); + } + adjMat[i][j] = 0; + adjMat[j][i] = 0; + } + + /* 列印鄰接矩陣 */ + void print() { + cout << "頂點串列 = "; + printVector(vertices); + cout << "鄰接矩陣 =" << endl; + printVectorMatrix(adjMat); + } + }; + ``` + +=== "Java" + + ```java title="graph_adjacency_matrix.java" + /* 基於鄰接矩陣實現的無向圖類別 */ + class GraphAdjMat { + List vertices; // 頂點串列,元素代表“頂點值”,索引代表“頂點索引” + List> adjMat; // 鄰接矩陣,行列索引對應“頂點索引” + + /* 建構子 */ + public GraphAdjMat(int[] vertices, int[][] edges) { + this.vertices = new ArrayList<>(); + this.adjMat = new ArrayList<>(); + // 新增頂點 + for (int val : vertices) { + addVertex(val); + } + // 新增邊 + // 請注意,edges 元素代表頂點索引,即對應 vertices 元素索引 + for (int[] e : edges) { + addEdge(e[0], e[1]); + } + } + + /* 獲取頂點數量 */ + public int size() { + return vertices.size(); + } + + /* 新增頂點 */ + public void addVertex(int val) { + int n = size(); + // 向頂點串列中新增新頂點的值 + vertices.add(val); + // 在鄰接矩陣中新增一行 + List newRow = new ArrayList<>(n); + for (int j = 0; j < n; j++) { + newRow.add(0); + } + adjMat.add(newRow); + // 在鄰接矩陣中新增一列 + for (List row : adjMat) { + row.add(0); + } + } + + /* 刪除頂點 */ + public void removeVertex(int index) { + if (index >= size()) + throw new IndexOutOfBoundsException(); + // 在頂點串列中移除索引 index 的頂點 + vertices.remove(index); + // 在鄰接矩陣中刪除索引 index 的行 + adjMat.remove(index); + // 在鄰接矩陣中刪除索引 index 的列 + for (List row : adjMat) { + row.remove(index); + } + } + + /* 新增邊 */ + // 參數 i, j 對應 vertices 元素索引 + public void addEdge(int i, int j) { + // 索引越界與相等處理 + if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) + throw new IndexOutOfBoundsException(); + // 在無向圖中,鄰接矩陣關於主對角線對稱,即滿足 (i, j) == (j, i) + adjMat.get(i).set(j, 1); + adjMat.get(j).set(i, 1); + } + + /* 刪除邊 */ + // 參數 i, j 對應 vertices 元素索引 + public void removeEdge(int i, int j) { + // 索引越界與相等處理 + if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) + throw new IndexOutOfBoundsException(); + adjMat.get(i).set(j, 0); + adjMat.get(j).set(i, 0); + } + + /* 列印鄰接矩陣 */ + public void print() { + System.out.print("頂點串列 = "); + System.out.println(vertices); + System.out.println("鄰接矩陣 ="); + PrintUtil.printMatrix(adjMat); + } + } + ``` + +=== "C#" + + ```csharp title="graph_adjacency_matrix.cs" + /* 基於鄰接矩陣實現的無向圖類別 */ + class GraphAdjMat { + List vertices; // 頂點串列,元素代表“頂點值”,索引代表“頂點索引” + List> adjMat; // 鄰接矩陣,行列索引對應“頂點索引” + + /* 建構子 */ + public GraphAdjMat(int[] vertices, int[][] edges) { + this.vertices = []; + this.adjMat = []; + // 新增頂點 + foreach (int val in vertices) { + AddVertex(val); + } + // 新增邊 + // 請注意,edges 元素代表頂點索引,即對應 vertices 元素索引 + foreach (int[] e in edges) { + AddEdge(e[0], e[1]); + } + } + + /* 獲取頂點數量 */ + int Size() { + return vertices.Count; + } + + /* 新增頂點 */ + public void AddVertex(int val) { + int n = Size(); + // 向頂點串列中新增新頂點的值 + vertices.Add(val); + // 在鄰接矩陣中新增一行 + List newRow = new(n); + for (int j = 0; j < n; j++) { + newRow.Add(0); + } + adjMat.Add(newRow); + // 在鄰接矩陣中新增一列 + foreach (List row in adjMat) { + row.Add(0); + } + } + + /* 刪除頂點 */ + public void RemoveVertex(int index) { + if (index >= Size()) + throw new IndexOutOfRangeException(); + // 在頂點串列中移除索引 index 的頂點 + vertices.RemoveAt(index); + // 在鄰接矩陣中刪除索引 index 的行 + adjMat.RemoveAt(index); + // 在鄰接矩陣中刪除索引 index 的列 + foreach (List row in adjMat) { + row.RemoveAt(index); + } + } + + /* 新增邊 */ + // 參數 i, j 對應 vertices 元素索引 + public void AddEdge(int i, int j) { + // 索引越界與相等處理 + if (i < 0 || j < 0 || i >= Size() || j >= Size() || i == j) + throw new IndexOutOfRangeException(); + // 在無向圖中,鄰接矩陣關於主對角線對稱,即滿足 (i, j) == (j, i) + adjMat[i][j] = 1; + adjMat[j][i] = 1; + } + + /* 刪除邊 */ + // 參數 i, j 對應 vertices 元素索引 + public void RemoveEdge(int i, int j) { + // 索引越界與相等處理 + if (i < 0 || j < 0 || i >= Size() || j >= Size() || i == j) + throw new IndexOutOfRangeException(); + adjMat[i][j] = 0; + adjMat[j][i] = 0; + } + + /* 列印鄰接矩陣 */ + public void Print() { + Console.Write("頂點串列 = "); + PrintUtil.PrintList(vertices); + Console.WriteLine("鄰接矩陣 ="); + PrintUtil.PrintMatrix(adjMat); + } + } + ``` + +=== "Go" + + ```go title="graph_adjacency_matrix.go" + /* 基於鄰接矩陣實現的無向圖類別 */ + type graphAdjMat struct { + // 頂點串列,元素代表“頂點值”,索引代表“頂點索引” + vertices []int + // 鄰接矩陣,行列索引對應“頂點索引” + adjMat [][]int + } + + /* 建構子 */ + func newGraphAdjMat(vertices []int, edges [][]int) *graphAdjMat { + // 新增頂點 + n := len(vertices) + adjMat := make([][]int, n) + for i := range adjMat { + adjMat[i] = make([]int, n) + } + // 初始化圖 + g := &graphAdjMat{ + vertices: vertices, + adjMat: adjMat, + } + // 新增邊 + // 請注意,edges 元素代表頂點索引,即對應 vertices 元素索引 + for i := range edges { + g.addEdge(edges[i][0], edges[i][1]) + } + return g + } + + /* 獲取頂點數量 */ + func (g *graphAdjMat) size() int { + return len(g.vertices) + } + + /* 新增頂點 */ + func (g *graphAdjMat) addVertex(val int) { + n := g.size() + // 向頂點串列中新增新頂點的值 + g.vertices = append(g.vertices, val) + // 在鄰接矩陣中新增一行 + newRow := make([]int, n) + g.adjMat = append(g.adjMat, newRow) + // 在鄰接矩陣中新增一列 + for i := range g.adjMat { + g.adjMat[i] = append(g.adjMat[i], 0) + } + } + + /* 刪除頂點 */ + func (g *graphAdjMat) removeVertex(index int) { + if index >= g.size() { + return + } + // 在頂點串列中移除索引 index 的頂點 + g.vertices = append(g.vertices[:index], g.vertices[index+1:]...) + // 在鄰接矩陣中刪除索引 index 的行 + g.adjMat = append(g.adjMat[:index], g.adjMat[index+1:]...) + // 在鄰接矩陣中刪除索引 index 的列 + for i := range g.adjMat { + g.adjMat[i] = append(g.adjMat[i][:index], g.adjMat[i][index+1:]...) + } + } + + /* 新增邊 */ + // 參數 i, j 對應 vertices 元素索引 + func (g *graphAdjMat) addEdge(i, j int) { + // 索引越界與相等處理 + if i < 0 || j < 0 || i >= g.size() || j >= g.size() || i == j { + fmt.Errorf("%s", "Index Out Of Bounds Exception") + } + // 在無向圖中,鄰接矩陣關於主對角線對稱,即滿足 (i, j) == (j, i) + g.adjMat[i][j] = 1 + g.adjMat[j][i] = 1 + } + + /* 刪除邊 */ + // 參數 i, j 對應 vertices 元素索引 + func (g *graphAdjMat) removeEdge(i, j int) { + // 索引越界與相等處理 + if i < 0 || j < 0 || i >= g.size() || j >= g.size() || i == j { + fmt.Errorf("%s", "Index Out Of Bounds Exception") + } + g.adjMat[i][j] = 0 + g.adjMat[j][i] = 0 + } + + /* 列印鄰接矩陣 */ + func (g *graphAdjMat) print() { + fmt.Printf("\t頂點串列 = %v\n", g.vertices) + fmt.Printf("\t鄰接矩陣 = \n") + for i := range g.adjMat { + fmt.Printf("\t\t\t%v\n", g.adjMat[i]) + } + } + ``` + +=== "Swift" + + ```swift title="graph_adjacency_matrix.swift" + /* 基於鄰接矩陣實現的無向圖類別 */ + class GraphAdjMat { + private var vertices: [Int] // 頂點串列,元素代表“頂點值”,索引代表“頂點索引” + private var adjMat: [[Int]] // 鄰接矩陣,行列索引對應“頂點索引” + + /* 建構子 */ + init(vertices: [Int], edges: [[Int]]) { + self.vertices = [] + adjMat = [] + // 新增頂點 + for val in vertices { + addVertex(val: val) + } + // 新增邊 + // 請注意,edges 元素代表頂點索引,即對應 vertices 元素索引 + for e in edges { + addEdge(i: e[0], j: e[1]) + } + } + + /* 獲取頂點數量 */ + func size() -> Int { + vertices.count + } + + /* 新增頂點 */ + func addVertex(val: Int) { + let n = size() + // 向頂點串列中新增新頂點的值 + vertices.append(val) + // 在鄰接矩陣中新增一行 + let newRow = Array(repeating: 0, count: n) + adjMat.append(newRow) + // 在鄰接矩陣中新增一列 + for i in adjMat.indices { + adjMat[i].append(0) + } + } + + /* 刪除頂點 */ + func removeVertex(index: Int) { + if index >= size() { + fatalError("越界") + } + // 在頂點串列中移除索引 index 的頂點 + vertices.remove(at: index) + // 在鄰接矩陣中刪除索引 index 的行 + adjMat.remove(at: index) + // 在鄰接矩陣中刪除索引 index 的列 + for i in adjMat.indices { + adjMat[i].remove(at: index) + } + } + + /* 新增邊 */ + // 參數 i, j 對應 vertices 元素索引 + func addEdge(i: Int, j: Int) { + // 索引越界與相等處理 + if i < 0 || j < 0 || i >= size() || j >= size() || i == j { + fatalError("越界") + } + // 在無向圖中,鄰接矩陣關於主對角線對稱,即滿足 (i, j) == (j, i) + adjMat[i][j] = 1 + adjMat[j][i] = 1 + } + + /* 刪除邊 */ + // 參數 i, j 對應 vertices 元素索引 + func removeEdge(i: Int, j: Int) { + // 索引越界與相等處理 + if i < 0 || j < 0 || i >= size() || j >= size() || i == j { + fatalError("越界") + } + adjMat[i][j] = 0 + adjMat[j][i] = 0 + } + + /* 列印鄰接矩陣 */ + func print() { + Swift.print("頂點串列 = ", terminator: "") + Swift.print(vertices) + Swift.print("鄰接矩陣 =") + PrintUtil.printMatrix(matrix: adjMat) + } + } + ``` + +=== "JS" + + ```javascript title="graph_adjacency_matrix.js" + /* 基於鄰接矩陣實現的無向圖類別 */ + class GraphAdjMat { + vertices; // 頂點串列,元素代表“頂點值”,索引代表“頂點索引” + adjMat; // 鄰接矩陣,行列索引對應“頂點索引” + + /* 建構子 */ + constructor(vertices, edges) { + this.vertices = []; + this.adjMat = []; + // 新增頂點 + for (const val of vertices) { + this.addVertex(val); + } + // 新增邊 + // 請注意,edges 元素代表頂點索引,即對應 vertices 元素索引 + for (const e of edges) { + this.addEdge(e[0], e[1]); + } + } + + /* 獲取頂點數量 */ + size() { + return this.vertices.length; + } + + /* 新增頂點 */ + addVertex(val) { + const n = this.size(); + // 向頂點串列中新增新頂點的值 + this.vertices.push(val); + // 在鄰接矩陣中新增一行 + const newRow = []; + for (let j = 0; j < n; j++) { + newRow.push(0); + } + this.adjMat.push(newRow); + // 在鄰接矩陣中新增一列 + for (const row of this.adjMat) { + row.push(0); + } + } + + /* 刪除頂點 */ + removeVertex(index) { + if (index >= this.size()) { + throw new RangeError('Index Out Of Bounds Exception'); + } + // 在頂點串列中移除索引 index 的頂點 + this.vertices.splice(index, 1); + + // 在鄰接矩陣中刪除索引 index 的行 + this.adjMat.splice(index, 1); + // 在鄰接矩陣中刪除索引 index 的列 + for (const row of this.adjMat) { + row.splice(index, 1); + } + } + + /* 新增邊 */ + // 參數 i, j 對應 vertices 元素索引 + addEdge(i, j) { + // 索引越界與相等處理 + if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) { + throw new RangeError('Index Out Of Bounds Exception'); + } + // 在無向圖中,鄰接矩陣關於主對角線對稱,即滿足 (i, j) === (j, i) + this.adjMat[i][j] = 1; + this.adjMat[j][i] = 1; + } + + /* 刪除邊 */ + // 參數 i, j 對應 vertices 元素索引 + removeEdge(i, j) { + // 索引越界與相等處理 + if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) { + throw new RangeError('Index Out Of Bounds Exception'); + } + this.adjMat[i][j] = 0; + this.adjMat[j][i] = 0; + } + + /* 列印鄰接矩陣 */ + print() { + console.log('頂點串列 = ', this.vertices); + console.log('鄰接矩陣 =', this.adjMat); + } + } + ``` + +=== "TS" + + ```typescript title="graph_adjacency_matrix.ts" + /* 基於鄰接矩陣實現的無向圖類別 */ + class GraphAdjMat { + vertices: number[]; // 頂點串列,元素代表“頂點值”,索引代表“頂點索引” + adjMat: number[][]; // 鄰接矩陣,行列索引對應“頂點索引” + + /* 建構子 */ + constructor(vertices: number[], edges: number[][]) { + this.vertices = []; + this.adjMat = []; + // 新增頂點 + for (const val of vertices) { + this.addVertex(val); + } + // 新增邊 + // 請注意,edges 元素代表頂點索引,即對應 vertices 元素索引 + for (const e of edges) { + this.addEdge(e[0], e[1]); + } + } + + /* 獲取頂點數量 */ + size(): number { + return this.vertices.length; + } + + /* 新增頂點 */ + addVertex(val: number): void { + const n: number = this.size(); + // 向頂點串列中新增新頂點的值 + this.vertices.push(val); + // 在鄰接矩陣中新增一行 + const newRow: number[] = []; + for (let j: number = 0; j < n; j++) { + newRow.push(0); + } + this.adjMat.push(newRow); + // 在鄰接矩陣中新增一列 + for (const row of this.adjMat) { + row.push(0); + } + } + + /* 刪除頂點 */ + removeVertex(index: number): void { + if (index >= this.size()) { + throw new RangeError('Index Out Of Bounds Exception'); + } + // 在頂點串列中移除索引 index 的頂點 + this.vertices.splice(index, 1); + + // 在鄰接矩陣中刪除索引 index 的行 + this.adjMat.splice(index, 1); + // 在鄰接矩陣中刪除索引 index 的列 + for (const row of this.adjMat) { + row.splice(index, 1); + } + } + + /* 新增邊 */ + // 參數 i, j 對應 vertices 元素索引 + addEdge(i: number, j: number): void { + // 索引越界與相等處理 + if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) { + throw new RangeError('Index Out Of Bounds Exception'); + } + // 在無向圖中,鄰接矩陣關於主對角線對稱,即滿足 (i, j) === (j, i) + this.adjMat[i][j] = 1; + this.adjMat[j][i] = 1; + } + + /* 刪除邊 */ + // 參數 i, j 對應 vertices 元素索引 + removeEdge(i: number, j: number): void { + // 索引越界與相等處理 + if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) { + throw new RangeError('Index Out Of Bounds Exception'); + } + this.adjMat[i][j] = 0; + this.adjMat[j][i] = 0; + } + + /* 列印鄰接矩陣 */ + print(): void { + console.log('頂點串列 = ', this.vertices); + console.log('鄰接矩陣 =', this.adjMat); + } + } + ``` + +=== "Dart" + + ```dart title="graph_adjacency_matrix.dart" + /* 基於鄰接矩陣實現的無向圖類別 */ + class GraphAdjMat { + List vertices = []; // 頂點元素,元素代表“頂點值”,索引代表“頂點索引” + List> adjMat = []; //鄰接矩陣,行列索引對應“頂點索引” + + /* 建構子 */ + GraphAdjMat(List vertices, List> edges) { + this.vertices = []; + this.adjMat = []; + // 新增頂點 + for (int val in vertices) { + addVertex(val); + } + // 新增邊 + // 請注意,edges 元素代表頂點索引,即對應 vertices 元素索引 + for (List e in edges) { + addEdge(e[0], e[1]); + } + } + + /* 獲取頂點數量 */ + int size() { + return vertices.length; + } + + /* 新增頂點 */ + void addVertex(int val) { + int n = size(); + // 向頂點串列中新增新頂點的值 + vertices.add(val); + // 在鄰接矩陣中新增一行 + List newRow = List.filled(n, 0, growable: true); + adjMat.add(newRow); + // 在鄰接矩陣中新增一列 + for (List row in adjMat) { + row.add(0); + } + } + + /* 刪除頂點 */ + void removeVertex(int index) { + if (index >= size()) { + throw IndexError; + } + // 在頂點串列中移除索引 index 的頂點 + vertices.removeAt(index); + // 在鄰接矩陣中刪除索引 index 的行 + adjMat.removeAt(index); + // 在鄰接矩陣中刪除索引 index 的列 + for (List row in adjMat) { + row.removeAt(index); + } + } + + /* 新增邊 */ + // 參數 i, j 對應 vertices 元素索引 + void addEdge(int i, int j) { + // 索引越界與相等處理 + if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) { + throw IndexError; + } + // 在無向圖中,鄰接矩陣關於主對角線對稱,即滿足 (i, j) == (j, i) + adjMat[i][j] = 1; + adjMat[j][i] = 1; + } + + /* 刪除邊 */ + // 參數 i, j 對應 vertices 元素索引 + void removeEdge(int i, int j) { + // 索引越界與相等處理 + if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) { + throw IndexError; + } + adjMat[i][j] = 0; + adjMat[j][i] = 0; + } + + /* 列印鄰接矩陣 */ + void printAdjMat() { + print("頂點串列 = $vertices"); + print("鄰接矩陣 = "); + printMatrix(adjMat); + } + } + ``` + +=== "Rust" + + ```rust title="graph_adjacency_matrix.rs" + /* 基於鄰接矩陣實現的無向圖型別 */ + pub struct GraphAdjMat { + // 頂點串列,元素代表“頂點值”,索引代表“頂點索引” + pub vertices: Vec, + // 鄰接矩陣,行列索引對應“頂點索引” + pub adj_mat: Vec>, + } + + impl GraphAdjMat { + /* 建構子 */ + pub fn new(vertices: Vec, edges: Vec<[usize; 2]>) -> Self { + let mut graph = GraphAdjMat { + vertices: vec![], + adj_mat: vec![], + }; + // 新增頂點 + for val in vertices { + graph.add_vertex(val); + } + // 新增邊 + // 請注意,edges 元素代表頂點索引,即對應 vertices 元素索引 + for edge in edges { + graph.add_edge(edge[0], edge[1]) + } + + graph + } + + /* 獲取頂點數量 */ + pub fn size(&self) -> usize { + self.vertices.len() + } + + /* 新增頂點 */ + pub fn add_vertex(&mut self, val: i32) { + let n = self.size(); + // 向頂點串列中新增新頂點的值 + self.vertices.push(val); + // 在鄰接矩陣中新增一行 + self.adj_mat.push(vec![0; n]); + // 在鄰接矩陣中新增一列 + for row in &mut self.adj_mat { + row.push(0); + } + } + + /* 刪除頂點 */ + pub fn remove_vertex(&mut self, index: usize) { + if index >= self.size() { + panic!("index error") + } + // 在頂點串列中移除索引 index 的頂點 + self.vertices.remove(index); + // 在鄰接矩陣中刪除索引 index 的行 + self.adj_mat.remove(index); + // 在鄰接矩陣中刪除索引 index 的列 + for row in &mut self.adj_mat { + row.remove(index); + } + } + + /* 新增邊 */ + pub fn add_edge(&mut self, i: usize, j: usize) { + // 參數 i, j 對應 vertices 元素索引 + // 索引越界與相等處理 + if i >= self.size() || j >= self.size() || i == j { + panic!("index error") + } + // 在無向圖中,鄰接矩陣關於主對角線對稱,即滿足 (i, j) == (j, i) + self.adj_mat[i][j] = 1; + self.adj_mat[j][i] = 1; + } + + /* 刪除邊 */ + // 參數 i, j 對應 vertices 元素索引 + pub fn remove_edge(&mut self, i: usize, j: usize) { + // 參數 i, j 對應 vertices 元素索引 + // 索引越界與相等處理 + if i >= self.size() || j >= self.size() || i == j { + panic!("index error") + } + self.adj_mat[i][j] = 0; + self.adj_mat[j][i] = 0; + } + + /* 列印鄰接矩陣 */ + pub fn print(&self) { + println!("頂點串列 = {:?}", self.vertices); + println!("鄰接矩陣 ="); + println!("["); + for row in &self.adj_mat { + println!(" {:?},", row); + } + println!("]") + } + } + ``` + +=== "C" + + ```c title="graph_adjacency_matrix.c" + /* 基於鄰接矩陣實現的無向圖結構體 */ + typedef struct { + int vertices[MAX_SIZE]; + int adjMat[MAX_SIZE][MAX_SIZE]; + int size; + } GraphAdjMat; + + /* 建構子 */ + GraphAdjMat *newGraphAdjMat() { + GraphAdjMat *graph = (GraphAdjMat *)malloc(sizeof(GraphAdjMat)); + graph->size = 0; + for (int i = 0; i < MAX_SIZE; i++) { + for (int j = 0; j < MAX_SIZE; j++) { + graph->adjMat[i][j] = 0; + } + } + return graph; + } + + /* 析構函式 */ + void delGraphAdjMat(GraphAdjMat *graph) { + free(graph); + } + + /* 新增頂點 */ + void addVertex(GraphAdjMat *graph, int val) { + if (graph->size == MAX_SIZE) { + fprintf(stderr, "圖的頂點數量已達最大值\n"); + return; + } + // 新增第 n 個頂點,並將第 n 行和列置零 + int n = graph->size; + graph->vertices[n] = val; + for (int i = 0; i <= n; i++) { + graph->adjMat[n][i] = graph->adjMat[i][n] = 0; + } + graph->size++; + } + + /* 刪除頂點 */ + void removeVertex(GraphAdjMat *graph, int index) { + if (index < 0 || index >= graph->size) { + fprintf(stderr, "頂點索引越界\n"); + return; + } + // 在頂點串列中移除索引 index 的頂點 + for (int i = index; i < graph->size - 1; i++) { + graph->vertices[i] = graph->vertices[i + 1]; + } + // 在鄰接矩陣中刪除索引 index 的行 + for (int i = index; i < graph->size - 1; i++) { + for (int j = 0; j < graph->size; j++) { + graph->adjMat[i][j] = graph->adjMat[i + 1][j]; + } + } + // 在鄰接矩陣中刪除索引 index 的列 + for (int i = 0; i < graph->size; i++) { + for (int j = index; j < graph->size - 1; j++) { + graph->adjMat[i][j] = graph->adjMat[i][j + 1]; + } + } + graph->size--; + } + + /* 新增邊 */ + // 參數 i, j 對應 vertices 元素索引 + void addEdge(GraphAdjMat *graph, int i, int j) { + if (i < 0 || j < 0 || i >= graph->size || j >= graph->size || i == j) { + fprintf(stderr, "邊索引越界或相等\n"); + return; + } + graph->adjMat[i][j] = 1; + graph->adjMat[j][i] = 1; + } + + /* 刪除邊 */ + // 參數 i, j 對應 vertices 元素索引 + void removeEdge(GraphAdjMat *graph, int i, int j) { + if (i < 0 || j < 0 || i >= graph->size || j >= graph->size || i == j) { + fprintf(stderr, "邊索引越界或相等\n"); + return; + } + graph->adjMat[i][j] = 0; + graph->adjMat[j][i] = 0; + } + + /* 列印鄰接矩陣 */ + void printGraphAdjMat(GraphAdjMat *graph) { + printf("頂點串列 = "); + printArray(graph->vertices, graph->size); + printf("鄰接矩陣 =\n"); + for (int i = 0; i < graph->size; i++) { + printArray(graph->adjMat[i], graph->size); + } + } + ``` + +=== "Kotlin" + + ```kotlin title="graph_adjacency_matrix.kt" + /* 基於鄰接矩陣實現的無向圖類別 */ + class GraphAdjMat(vertices: IntArray, edges: Array) { + val vertices: MutableList = ArrayList() // 頂點串列,元素代表“頂點值”,索引代表“頂點索引” + val adjMat: MutableList> = ArrayList() // 鄰接矩陣,行列索引對應“頂點索引” + + /* 建構子 */ + init { + // 新增頂點 + for (vertex in vertices) { + addVertex(vertex) + } + // 新增邊 + // 請注意,edges 元素代表頂點索引,即對應 vertices 元素索引 + for (edge in edges) { + addEdge(edge[0], edge[1]) + } + } + + /* 獲取頂點數量 */ + fun size(): Int { + return vertices.size + } + + /* 新增頂點 */ + fun addVertex(value: Int) { + val n = size() + // 向頂點串列中新增新頂點的值 + vertices.add(value) + // 在鄰接矩陣中新增一行 + val newRow: MutableList = mutableListOf() + for (j in 0..= size()) throw IndexOutOfBoundsException() + // 在頂點串列中移除索引 index 的頂點 + vertices.removeAt(index) + // 在鄰接矩陣中刪除索引 index 的行 + adjMat.removeAt(index) + // 在鄰接矩陣中刪除索引 index 的列 + for (row in adjMat) { + row.removeAt(index) + } + } + + /* 新增邊 */ + // 參數 i, j 對應 vertices 元素索引 + fun addEdge(i: Int, j: Int) { + // 索引越界與相等處理 + if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) throw java.lang.IndexOutOfBoundsException() + // 在無向圖中,鄰接矩陣關於主對角線對稱,即滿足 (i, j) == (j, i) + adjMat[i][j] = 1; + adjMat[j][i] = 1; + } + + /* 刪除邊 */ + // 參數 i, j 對應 vertices 元素索引 + fun removeEdge(i: Int, j: Int) { + // 索引越界與相等處理 + if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) throw java.lang.IndexOutOfBoundsException() + adjMat[i][j] = 0; + adjMat[j][i] = 0; + } + + /* 列印鄰接矩陣 */ + fun print() { + print("頂點串列 = ") + println(vertices); + println("鄰接矩陣 ="); + printMatrix(adjMat) + } + } + ``` + +=== "Ruby" + + ```ruby title="graph_adjacency_matrix.rb" + [class]{GraphAdjMat}-[func]{} + ``` + +=== "Zig" + + ```zig title="graph_adjacency_matrix.zig" + [class]{GraphAdjMat}-[func]{} + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 9.2.2   基於鄰接表的實現 + +設無向圖的頂點總數為 $n$、邊總數為 $m$ ,則可根據圖 9-8 所示的方法實現各種操作。 + +- **新增邊**:在頂點對應鏈結串列的末尾新增邊即可,使用 $O(1)$ 時間。因為是無向圖,所以需要同時新增兩個方向的邊。 +- **刪除邊**:在頂點對應鏈結串列中查詢並刪除指定邊,使用 $O(m)$ 時間。在無向圖中,需要同時刪除兩個方向的邊。 +- **新增頂點**:在鄰接表中新增一個鏈結串列,並將新增頂點作為鏈結串列頭節點,使用 $O(1)$ 時間。 +- **刪除頂點**:需走訪整個鄰接表,刪除包含指定頂點的所有邊,使用 $O(n + m)$ 時間。 +- **初始化**:在鄰接表中建立 $n$ 個頂點和 $2m$ 條邊,使用 $O(n + m)$ 時間。 + +=== "初始化鄰接表" + ![鄰接表的初始化、增刪邊、增刪頂點](graph_operations.assets/adjacency_list_step1_initialization.png){ class="animation-figure" } + +=== "新增邊" + ![adjacency_list_add_edge](graph_operations.assets/adjacency_list_step2_add_edge.png){ class="animation-figure" } + +=== "刪除邊" + ![adjacency_list_remove_edge](graph_operations.assets/adjacency_list_step3_remove_edge.png){ class="animation-figure" } + +=== "新增頂點" + ![adjacency_list_add_vertex](graph_operations.assets/adjacency_list_step4_add_vertex.png){ class="animation-figure" } + +=== "刪除頂點" + ![adjacency_list_remove_vertex](graph_operations.assets/adjacency_list_step5_remove_vertex.png){ class="animation-figure" } + +

圖 9-8   鄰接表的初始化、增刪邊、增刪頂點

+ +以下是鄰接表的程式碼實現。對比圖 9-8 ,實際程式碼有以下不同。 + +- 為了方便新增與刪除頂點,以及簡化程式碼,我們使用串列(動態陣列)來代替鏈結串列。 +- 使用雜湊表來儲存鄰接表,`key` 為頂點例項,`value` 為該頂點的鄰接頂點串列(鏈結串列)。 + +另外,我們在鄰接表中使用 `Vertex` 類別來表示頂點,這樣做的原因是:如果與鄰接矩陣一樣,用串列索引來區分不同頂點,那麼假設要刪除索引為 $i$ 的頂點,則需走訪整個鄰接表,將所有大於 $i$ 的索引全部減 $1$ ,效率很低。而如果每個頂點都是唯一的 `Vertex` 例項,刪除某一頂點之後就無須改動其他頂點了。 + +=== "Python" + + ```python title="graph_adjacency_list.py" + class GraphAdjList: + """基於鄰接表實現的無向圖類別""" + + def __init__(self, edges: list[list[Vertex]]): + """建構子""" + # 鄰接表,key:頂點,value:該頂點的所有鄰接頂點 + self.adj_list = dict[Vertex, list[Vertex]]() + # 新增所有頂點和邊 + for edge in edges: + self.add_vertex(edge[0]) + self.add_vertex(edge[1]) + self.add_edge(edge[0], edge[1]) + + def size(self) -> int: + """獲取頂點數量""" + return len(self.adj_list) + + def add_edge(self, vet1: Vertex, vet2: Vertex): + """新增邊""" + if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2: + raise ValueError() + # 新增邊 vet1 - vet2 + self.adj_list[vet1].append(vet2) + self.adj_list[vet2].append(vet1) + + def remove_edge(self, vet1: Vertex, vet2: Vertex): + """刪除邊""" + if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2: + raise ValueError() + # 刪除邊 vet1 - vet2 + self.adj_list[vet1].remove(vet2) + self.adj_list[vet2].remove(vet1) + + def add_vertex(self, vet: Vertex): + """新增頂點""" + if vet in self.adj_list: + return + # 在鄰接表中新增一個新鏈結串列 + self.adj_list[vet] = [] + + def remove_vertex(self, vet: Vertex): + """刪除頂點""" + if vet not in self.adj_list: + raise ValueError() + # 在鄰接表中刪除頂點 vet 對應的鏈結串列 + self.adj_list.pop(vet) + # 走訪其他頂點的鏈結串列,刪除所有包含 vet 的邊 + for vertex in self.adj_list: + if vet in self.adj_list[vertex]: + self.adj_list[vertex].remove(vet) + + def print(self): + """列印鄰接表""" + print("鄰接表 =") + for vertex in self.adj_list: + tmp = [v.val for v in self.adj_list[vertex]] + print(f"{vertex.val}: {tmp},") + ``` + +=== "C++" + + ```cpp title="graph_adjacency_list.cpp" + /* 基於鄰接表實現的無向圖類別 */ + class GraphAdjList { + public: + // 鄰接表,key:頂點,value:該頂點的所有鄰接頂點 + unordered_map> adjList; + + /* 在 vector 中刪除指定節點 */ + void remove(vector &vec, Vertex *vet) { + for (int i = 0; i < vec.size(); i++) { + if (vec[i] == vet) { + vec.erase(vec.begin() + i); + break; + } + } + } + + /* 建構子 */ + GraphAdjList(const vector> &edges) { + // 新增所有頂點和邊 + for (const vector &edge : edges) { + addVertex(edge[0]); + addVertex(edge[1]); + addEdge(edge[0], edge[1]); + } + } + + /* 獲取頂點數量 */ + int size() { + return adjList.size(); + } + + /* 新增邊 */ + void addEdge(Vertex *vet1, Vertex *vet2) { + if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2) + throw invalid_argument("不存在頂點"); + // 新增邊 vet1 - vet2 + adjList[vet1].push_back(vet2); + adjList[vet2].push_back(vet1); + } + + /* 刪除邊 */ + void removeEdge(Vertex *vet1, Vertex *vet2) { + if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2) + throw invalid_argument("不存在頂點"); + // 刪除邊 vet1 - vet2 + remove(adjList[vet1], vet2); + remove(adjList[vet2], vet1); + } + + /* 新增頂點 */ + void addVertex(Vertex *vet) { + if (adjList.count(vet)) + return; + // 在鄰接表中新增一個新鏈結串列 + adjList[vet] = vector(); + } + + /* 刪除頂點 */ + void removeVertex(Vertex *vet) { + if (!adjList.count(vet)) + throw invalid_argument("不存在頂點"); + // 在鄰接表中刪除頂點 vet 對應的鏈結串列 + adjList.erase(vet); + // 走訪其他頂點的鏈結串列,刪除所有包含 vet 的邊 + for (auto &adj : adjList) { + remove(adj.second, vet); + } + } + + /* 列印鄰接表 */ + void print() { + cout << "鄰接表 =" << endl; + for (auto &adj : adjList) { + const auto &key = adj.first; + const auto &vec = adj.second; + cout << key->val << ": "; + printVector(vetsToVals(vec)); + } + } + }; + ``` + +=== "Java" + + ```java title="graph_adjacency_list.java" + /* 基於鄰接表實現的無向圖類別 */ + class GraphAdjList { + // 鄰接表,key:頂點,value:該頂點的所有鄰接頂點 + Map> adjList; + + /* 建構子 */ + public GraphAdjList(Vertex[][] edges) { + this.adjList = new HashMap<>(); + // 新增所有頂點和邊 + for (Vertex[] edge : edges) { + addVertex(edge[0]); + addVertex(edge[1]); + addEdge(edge[0], edge[1]); + } + } + + /* 獲取頂點數量 */ + public int size() { + return adjList.size(); + } + + /* 新增邊 */ + public void addEdge(Vertex vet1, Vertex vet2) { + if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2) + throw new IllegalArgumentException(); + // 新增邊 vet1 - vet2 + adjList.get(vet1).add(vet2); + adjList.get(vet2).add(vet1); + } + + /* 刪除邊 */ + public void removeEdge(Vertex vet1, Vertex vet2) { + if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2) + throw new IllegalArgumentException(); + // 刪除邊 vet1 - vet2 + adjList.get(vet1).remove(vet2); + adjList.get(vet2).remove(vet1); + } + + /* 新增頂點 */ + public void addVertex(Vertex vet) { + if (adjList.containsKey(vet)) + return; + // 在鄰接表中新增一個新鏈結串列 + adjList.put(vet, new ArrayList<>()); + } + + /* 刪除頂點 */ + public void removeVertex(Vertex vet) { + if (!adjList.containsKey(vet)) + throw new IllegalArgumentException(); + // 在鄰接表中刪除頂點 vet 對應的鏈結串列 + adjList.remove(vet); + // 走訪其他頂點的鏈結串列,刪除所有包含 vet 的邊 + for (List list : adjList.values()) { + list.remove(vet); + } + } + + /* 列印鄰接表 */ + public void print() { + System.out.println("鄰接表 ="); + for (Map.Entry> pair : adjList.entrySet()) { + List tmp = new ArrayList<>(); + for (Vertex vertex : pair.getValue()) + tmp.add(vertex.val); + System.out.println(pair.getKey().val + ": " + tmp + ","); + } + } + } + ``` + +=== "C#" + + ```csharp title="graph_adjacency_list.cs" + /* 基於鄰接表實現的無向圖類別 */ + class GraphAdjList { + // 鄰接表,key:頂點,value:該頂點的所有鄰接頂點 + public Dictionary> adjList; + + /* 建構子 */ + public GraphAdjList(Vertex[][] edges) { + adjList = []; + // 新增所有頂點和邊 + foreach (Vertex[] edge in edges) { + AddVertex(edge[0]); + AddVertex(edge[1]); + AddEdge(edge[0], edge[1]); + } + } + + /* 獲取頂點數量 */ + int Size() { + return adjList.Count; + } + + /* 新增邊 */ + public void AddEdge(Vertex vet1, Vertex vet2) { + if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2) + throw new InvalidOperationException(); + // 新增邊 vet1 - vet2 + adjList[vet1].Add(vet2); + adjList[vet2].Add(vet1); + } + + /* 刪除邊 */ + public void RemoveEdge(Vertex vet1, Vertex vet2) { + if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2) + throw new InvalidOperationException(); + // 刪除邊 vet1 - vet2 + adjList[vet1].Remove(vet2); + adjList[vet2].Remove(vet1); + } + + /* 新增頂點 */ + public void AddVertex(Vertex vet) { + if (adjList.ContainsKey(vet)) + return; + // 在鄰接表中新增一個新鏈結串列 + adjList.Add(vet, []); + } + + /* 刪除頂點 */ + public void RemoveVertex(Vertex vet) { + if (!adjList.ContainsKey(vet)) + throw new InvalidOperationException(); + // 在鄰接表中刪除頂點 vet 對應的鏈結串列 + adjList.Remove(vet); + // 走訪其他頂點的鏈結串列,刪除所有包含 vet 的邊 + foreach (List list in adjList.Values) { + list.Remove(vet); + } + } + + /* 列印鄰接表 */ + public void Print() { + Console.WriteLine("鄰接表 ="); + foreach (KeyValuePair> pair in adjList) { + List tmp = []; + foreach (Vertex vertex in pair.Value) + tmp.Add(vertex.val); + Console.WriteLine(pair.Key.val + ": [" + string.Join(", ", tmp) + "],"); + } + } + } + ``` + +=== "Go" + + ```go title="graph_adjacency_list.go" + /* 基於鄰接表實現的無向圖類別 */ + type graphAdjList struct { + // 鄰接表,key:頂點,value:該頂點的所有鄰接頂點 + adjList map[Vertex][]Vertex + } + + /* 建構子 */ + func newGraphAdjList(edges [][]Vertex) *graphAdjList { + g := &graphAdjList{ + adjList: make(map[Vertex][]Vertex), + } + // 新增所有頂點和邊 + for _, edge := range edges { + g.addVertex(edge[0]) + g.addVertex(edge[1]) + g.addEdge(edge[0], edge[1]) + } + return g + } + + /* 獲取頂點數量 */ + func (g *graphAdjList) size() int { + return len(g.adjList) + } + + /* 新增邊 */ + func (g *graphAdjList) addEdge(vet1 Vertex, vet2 Vertex) { + _, ok1 := g.adjList[vet1] + _, ok2 := g.adjList[vet2] + if !ok1 || !ok2 || vet1 == vet2 { + panic("error") + } + // 新增邊 vet1 - vet2, 新增匿名 struct{}, + g.adjList[vet1] = append(g.adjList[vet1], vet2) + g.adjList[vet2] = append(g.adjList[vet2], vet1) + } + + /* 刪除邊 */ + func (g *graphAdjList) removeEdge(vet1 Vertex, vet2 Vertex) { + _, ok1 := g.adjList[vet1] + _, ok2 := g.adjList[vet2] + if !ok1 || !ok2 || vet1 == vet2 { + panic("error") + } + // 刪除邊 vet1 - vet2 + g.adjList[vet1] = DeleteSliceElms(g.adjList[vet1], vet2) + g.adjList[vet2] = DeleteSliceElms(g.adjList[vet2], vet1) + } + + /* 新增頂點 */ + func (g *graphAdjList) addVertex(vet Vertex) { + _, ok := g.adjList[vet] + if ok { + return + } + // 在鄰接表中新增一個新鏈結串列 + g.adjList[vet] = make([]Vertex, 0) + } + + /* 刪除頂點 */ + func (g *graphAdjList) removeVertex(vet Vertex) { + _, ok := g.adjList[vet] + if !ok { + panic("error") + } + // 在鄰接表中刪除頂點 vet 對應的鏈結串列 + delete(g.adjList, vet) + // 走訪其他頂點的鏈結串列,刪除所有包含 vet 的邊 + for v, list := range g.adjList { + g.adjList[v] = DeleteSliceElms(list, vet) + } + } + + /* 列印鄰接表 */ + func (g *graphAdjList) print() { + var builder strings.Builder + fmt.Printf("鄰接表 = \n") + for k, v := range g.adjList { + builder.WriteString("\t\t" + strconv.Itoa(k.Val) + ": ") + for _, vet := range v { + builder.WriteString(strconv.Itoa(vet.Val) + " ") + } + fmt.Println(builder.String()) + builder.Reset() + } + } + ``` + +=== "Swift" + + ```swift title="graph_adjacency_list.swift" + /* 基於鄰接表實現的無向圖類別 */ + class GraphAdjList { + // 鄰接表,key:頂點,value:該頂點的所有鄰接頂點 + public private(set) var adjList: [Vertex: [Vertex]] + + /* 建構子 */ + public init(edges: [[Vertex]]) { + adjList = [:] + // 新增所有頂點和邊 + for edge in edges { + addVertex(vet: edge[0]) + addVertex(vet: edge[1]) + addEdge(vet1: edge[0], vet2: edge[1]) + } + } + + /* 獲取頂點數量 */ + public func size() -> Int { + adjList.count + } + + /* 新增邊 */ + public func addEdge(vet1: Vertex, vet2: Vertex) { + if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 { + fatalError("參數錯誤") + } + // 新增邊 vet1 - vet2 + adjList[vet1]?.append(vet2) + adjList[vet2]?.append(vet1) + } + + /* 刪除邊 */ + public func removeEdge(vet1: Vertex, vet2: Vertex) { + if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 { + fatalError("參數錯誤") + } + // 刪除邊 vet1 - vet2 + adjList[vet1]?.removeAll { $0 == vet2 } + adjList[vet2]?.removeAll { $0 == vet1 } + } + + /* 新增頂點 */ + public func addVertex(vet: Vertex) { + if adjList[vet] != nil { + return + } + // 在鄰接表中新增一個新鏈結串列 + adjList[vet] = [] + } + + /* 刪除頂點 */ + public func removeVertex(vet: Vertex) { + if adjList[vet] == nil { + fatalError("參數錯誤") + } + // 在鄰接表中刪除頂點 vet 對應的鏈結串列 + adjList.removeValue(forKey: vet) + // 走訪其他頂點的鏈結串列,刪除所有包含 vet 的邊 + for key in adjList.keys { + adjList[key]?.removeAll { $0 == vet } + } + } + + /* 列印鄰接表 */ + public func print() { + Swift.print("鄰接表 =") + for (vertex, list) in adjList { + let list = list.map { $0.val } + Swift.print("\(vertex.val): \(list),") + } + } + } + ``` + +=== "JS" + + ```javascript title="graph_adjacency_list.js" + /* 基於鄰接表實現的無向圖類別 */ + class GraphAdjList { + // 鄰接表,key:頂點,value:該頂點的所有鄰接頂點 + adjList; + + /* 建構子 */ + constructor(edges) { + this.adjList = new Map(); + // 新增所有頂點和邊 + for (const edge of edges) { + this.addVertex(edge[0]); + this.addVertex(edge[1]); + this.addEdge(edge[0], edge[1]); + } + } + + /* 獲取頂點數量 */ + size() { + return this.adjList.size; + } + + /* 新增邊 */ + addEdge(vet1, vet2) { + if ( + !this.adjList.has(vet1) || + !this.adjList.has(vet2) || + vet1 === vet2 + ) { + throw new Error('Illegal Argument Exception'); + } + // 新增邊 vet1 - vet2 + this.adjList.get(vet1).push(vet2); + this.adjList.get(vet2).push(vet1); + } + + /* 刪除邊 */ + removeEdge(vet1, vet2) { + if ( + !this.adjList.has(vet1) || + !this.adjList.has(vet2) || + vet1 === vet2 + ) { + throw new Error('Illegal Argument Exception'); + } + // 刪除邊 vet1 - vet2 + this.adjList.get(vet1).splice(this.adjList.get(vet1).indexOf(vet2), 1); + this.adjList.get(vet2).splice(this.adjList.get(vet2).indexOf(vet1), 1); + } + + /* 新增頂點 */ + addVertex(vet) { + if (this.adjList.has(vet)) return; + // 在鄰接表中新增一個新鏈結串列 + this.adjList.set(vet, []); + } + + /* 刪除頂點 */ + removeVertex(vet) { + if (!this.adjList.has(vet)) { + throw new Error('Illegal Argument Exception'); + } + // 在鄰接表中刪除頂點 vet 對應的鏈結串列 + this.adjList.delete(vet); + // 走訪其他頂點的鏈結串列,刪除所有包含 vet 的邊 + for (const set of this.adjList.values()) { + const index = set.indexOf(vet); + if (index > -1) { + set.splice(index, 1); + } + } + } + + /* 列印鄰接表 */ + print() { + console.log('鄰接表 ='); + for (const [key, value] of this.adjList) { + const tmp = []; + for (const vertex of value) { + tmp.push(vertex.val); + } + console.log(key.val + ': ' + tmp.join()); + } + } + } + ``` + +=== "TS" + + ```typescript title="graph_adjacency_list.ts" + /* 基於鄰接表實現的無向圖類別 */ + class GraphAdjList { + // 鄰接表,key:頂點,value:該頂點的所有鄰接頂點 + adjList: Map; + + /* 建構子 */ + constructor(edges: Vertex[][]) { + this.adjList = new Map(); + // 新增所有頂點和邊 + for (const edge of edges) { + this.addVertex(edge[0]); + this.addVertex(edge[1]); + this.addEdge(edge[0], edge[1]); + } + } + + /* 獲取頂點數量 */ + size(): number { + return this.adjList.size; + } + + /* 新增邊 */ + addEdge(vet1: Vertex, vet2: Vertex): void { + if ( + !this.adjList.has(vet1) || + !this.adjList.has(vet2) || + vet1 === vet2 + ) { + throw new Error('Illegal Argument Exception'); + } + // 新增邊 vet1 - vet2 + this.adjList.get(vet1).push(vet2); + this.adjList.get(vet2).push(vet1); + } + + /* 刪除邊 */ + removeEdge(vet1: Vertex, vet2: Vertex): void { + if ( + !this.adjList.has(vet1) || + !this.adjList.has(vet2) || + vet1 === vet2 + ) { + throw new Error('Illegal Argument Exception'); + } + // 刪除邊 vet1 - vet2 + this.adjList.get(vet1).splice(this.adjList.get(vet1).indexOf(vet2), 1); + this.adjList.get(vet2).splice(this.adjList.get(vet2).indexOf(vet1), 1); + } + + /* 新增頂點 */ + addVertex(vet: Vertex): void { + if (this.adjList.has(vet)) return; + // 在鄰接表中新增一個新鏈結串列 + this.adjList.set(vet, []); + } + + /* 刪除頂點 */ + removeVertex(vet: Vertex): void { + if (!this.adjList.has(vet)) { + throw new Error('Illegal Argument Exception'); + } + // 在鄰接表中刪除頂點 vet 對應的鏈結串列 + this.adjList.delete(vet); + // 走訪其他頂點的鏈結串列,刪除所有包含 vet 的邊 + for (const set of this.adjList.values()) { + const index: number = set.indexOf(vet); + if (index > -1) { + set.splice(index, 1); + } + } + } + + /* 列印鄰接表 */ + print(): void { + console.log('鄰接表 ='); + for (const [key, value] of this.adjList.entries()) { + const tmp = []; + for (const vertex of value) { + tmp.push(vertex.val); + } + console.log(key.val + ': ' + tmp.join()); + } + } + } + ``` + +=== "Dart" + + ```dart title="graph_adjacency_list.dart" + /* 基於鄰接表實現的無向圖類別 */ + class GraphAdjList { + // 鄰接表,key:頂點,value:該頂點的所有鄰接頂點 + Map> adjList = {}; + + /* 建構子 */ + GraphAdjList(List> edges) { + for (List edge in edges) { + addVertex(edge[0]); + addVertex(edge[1]); + addEdge(edge[0], edge[1]); + } + } + + /* 獲取頂點數量 */ + int size() { + return adjList.length; + } + + /* 新增邊 */ + void addEdge(Vertex vet1, Vertex vet2) { + if (!adjList.containsKey(vet1) || + !adjList.containsKey(vet2) || + vet1 == vet2) { + throw ArgumentError; + } + // 新增邊 vet1 - vet2 + adjList[vet1]!.add(vet2); + adjList[vet2]!.add(vet1); + } + + /* 刪除邊 */ + void removeEdge(Vertex vet1, Vertex vet2) { + if (!adjList.containsKey(vet1) || + !adjList.containsKey(vet2) || + vet1 == vet2) { + throw ArgumentError; + } + // 刪除邊 vet1 - vet2 + adjList[vet1]!.remove(vet2); + adjList[vet2]!.remove(vet1); + } + + /* 新增頂點 */ + void addVertex(Vertex vet) { + if (adjList.containsKey(vet)) return; + // 在鄰接表中新增一個新鏈結串列 + adjList[vet] = []; + } + + /* 刪除頂點 */ + void removeVertex(Vertex vet) { + if (!adjList.containsKey(vet)) { + throw ArgumentError; + } + // 在鄰接表中刪除頂點 vet 對應的鏈結串列 + adjList.remove(vet); + // 走訪其他頂點的鏈結串列,刪除所有包含 vet 的邊 + adjList.forEach((key, value) { + value.remove(vet); + }); + } + + /* 列印鄰接表 */ + void printAdjList() { + print("鄰接表 ="); + adjList.forEach((key, value) { + List tmp = []; + for (Vertex vertex in value) { + tmp.add(vertex.val); + } + print("${key.val}: $tmp,"); + }); + } + } + ``` + +=== "Rust" + + ```rust title="graph_adjacency_list.rs" + /* 基於鄰接表實現的無向圖型別 */ + pub struct GraphAdjList { + // 鄰接表,key:頂點,value:該頂點的所有鄰接頂點 + pub adj_list: HashMap>, + } + + impl GraphAdjList { + /* 建構子 */ + pub fn new(edges: Vec<[Vertex; 2]>) -> Self { + let mut graph = GraphAdjList { + adj_list: HashMap::new(), + }; + // 新增所有頂點和邊 + for edge in edges { + graph.add_vertex(edge[0]); + graph.add_vertex(edge[1]); + graph.add_edge(edge[0], edge[1]); + } + + graph + } + + /* 獲取頂點數量 */ + #[allow(unused)] + pub fn size(&self) -> usize { + self.adj_list.len() + } + + /* 新增邊 */ + pub fn add_edge(&mut self, vet1: Vertex, vet2: Vertex) { + if !self.adj_list.contains_key(&vet1) || !self.adj_list.contains_key(&vet2) || vet1 == vet2 + { + panic!("value error"); + } + // 新增邊 vet1 - vet2 + self.adj_list.get_mut(&vet1).unwrap().push(vet2); + self.adj_list.get_mut(&vet2).unwrap().push(vet1); + } + + /* 刪除邊 */ + #[allow(unused)] + pub fn remove_edge(&mut self, vet1: Vertex, vet2: Vertex) { + if !self.adj_list.contains_key(&vet1) || !self.adj_list.contains_key(&vet2) || vet1 == vet2 + { + panic!("value error"); + } + // 刪除邊 vet1 - vet2 + self.adj_list + .get_mut(&vet1) + .unwrap() + .retain(|&vet| vet != vet2); + self.adj_list + .get_mut(&vet2) + .unwrap() + .retain(|&vet| vet != vet1); + } + + /* 新增頂點 */ + pub fn add_vertex(&mut self, vet: Vertex) { + if self.adj_list.contains_key(&vet) { + return; + } + // 在鄰接表中新增一個新鏈結串列 + self.adj_list.insert(vet, vec![]); + } + + /* 刪除頂點 */ + #[allow(unused)] + pub fn remove_vertex(&mut self, vet: Vertex) { + if !self.adj_list.contains_key(&vet) { + panic!("value error"); + } + // 在鄰接表中刪除頂點 vet 對應的鏈結串列 + self.adj_list.remove(&vet); + // 走訪其他頂點的鏈結串列,刪除所有包含 vet 的邊 + for list in self.adj_list.values_mut() { + list.retain(|&v| v != vet); + } + } + + /* 列印鄰接表 */ + pub fn print(&self) { + println!("鄰接表 ="); + for (vertex, list) in &self.adj_list { + let list = list.iter().map(|vertex| vertex.val).collect::>(); + println!("{}: {:?},", vertex.val, list); + } + } + } + ``` + +=== "C" + + ```c title="graph_adjacency_list.c" + /* 節點結構體 */ + typedef struct AdjListNode { + Vertex *vertex; // 頂點 + struct AdjListNode *next; // 後繼節點 + } AdjListNode; + + /* 查詢頂點對應的節點 */ + AdjListNode *findNode(GraphAdjList *graph, Vertex *vet) { + for (int i = 0; i < graph->size; i++) { + if (graph->heads[i]->vertex == vet) { + return graph->heads[i]; + } + } + return NULL; + } + + /* 新增邊輔助函式 */ + void addEdgeHelper(AdjListNode *head, Vertex *vet) { + AdjListNode *node = (AdjListNode *)malloc(sizeof(AdjListNode)); + node->vertex = vet; + // 頭插法 + node->next = head->next; + head->next = node; + } + + /* 刪除邊輔助函式 */ + void removeEdgeHelper(AdjListNode *head, Vertex *vet) { + AdjListNode *pre = head; + AdjListNode *cur = head->next; + // 在鏈結串列中搜索 vet 對應節點 + while (cur != NULL && cur->vertex != vet) { + pre = cur; + cur = cur->next; + } + if (cur == NULL) + return; + // 將 vet 對應節點從鏈結串列中刪除 + pre->next = cur->next; + // 釋放記憶體 + free(cur); + } + + /* 基於鄰接表實現的無向圖類別 */ + typedef struct { + AdjListNode *heads[MAX_SIZE]; // 節點陣列 + int size; // 節點數量 + } GraphAdjList; + + /* 建構子 */ + GraphAdjList *newGraphAdjList() { + GraphAdjList *graph = (GraphAdjList *)malloc(sizeof(GraphAdjList)); + if (!graph) { + return NULL; + } + graph->size = 0; + for (int i = 0; i < MAX_SIZE; i++) { + graph->heads[i] = NULL; + } + return graph; + } + + /* 析構函式 */ + void delGraphAdjList(GraphAdjList *graph) { + for (int i = 0; i < graph->size; i++) { + AdjListNode *cur = graph->heads[i]; + while (cur != NULL) { + AdjListNode *next = cur->next; + if (cur != graph->heads[i]) { + free(cur); + } + cur = next; + } + free(graph->heads[i]->vertex); + free(graph->heads[i]); + } + free(graph); + } + + /* 查詢頂點對應的節點 */ + AdjListNode *findNode(GraphAdjList *graph, Vertex *vet) { + for (int i = 0; i < graph->size; i++) { + if (graph->heads[i]->vertex == vet) { + return graph->heads[i]; + } + } + return NULL; + } + + /* 新增邊 */ + void addEdge(GraphAdjList *graph, Vertex *vet1, Vertex *vet2) { + AdjListNode *head1 = findNode(graph, vet1); + AdjListNode *head2 = findNode(graph, vet2); + assert(head1 != NULL && head2 != NULL && head1 != head2); + // 新增邊 vet1 - vet2 + addEdgeHelper(head1, vet2); + addEdgeHelper(head2, vet1); + } + + /* 刪除邊 */ + void removeEdge(GraphAdjList *graph, Vertex *vet1, Vertex *vet2) { + AdjListNode *head1 = findNode(graph, vet1); + AdjListNode *head2 = findNode(graph, vet2); + assert(head1 != NULL && head2 != NULL); + // 刪除邊 vet1 - vet2 + removeEdgeHelper(head1, head2->vertex); + removeEdgeHelper(head2, head1->vertex); + } + + /* 新增頂點 */ + void addVertex(GraphAdjList *graph, Vertex *vet) { + assert(graph != NULL && graph->size < MAX_SIZE); + AdjListNode *head = (AdjListNode *)malloc(sizeof(AdjListNode)); + head->vertex = vet; + head->next = NULL; + // 在鄰接表中新增一個新鏈結串列 + graph->heads[graph->size++] = head; + } + + /* 刪除頂點 */ + void removeVertex(GraphAdjList *graph, Vertex *vet) { + AdjListNode *node = findNode(graph, vet); + assert(node != NULL); + // 在鄰接表中刪除頂點 vet 對應的鏈結串列 + AdjListNode *cur = node, *pre = NULL; + while (cur) { + pre = cur; + cur = cur->next; + free(pre); + } + // 走訪其他頂點的鏈結串列,刪除所有包含 vet 的邊 + for (int i = 0; i < graph->size; i++) { + cur = graph->heads[i]; + pre = NULL; + while (cur) { + pre = cur; + cur = cur->next; + if (cur && cur->vertex == vet) { + pre->next = cur->next; + free(cur); + break; + } + } + } + // 將該頂點之後的頂點向前移動,以填補空缺 + int i; + for (i = 0; i < graph->size; i++) { + if (graph->heads[i] == node) + break; + } + for (int j = i; j < graph->size - 1; j++) { + graph->heads[j] = graph->heads[j + 1]; + } + graph->size--; + free(vet); + } + ``` + +=== "Kotlin" + + ```kotlin title="graph_adjacency_list.kt" + /* 基於鄰接表實現的無向圖類別 */ + class GraphAdjList(edges: Array>) { + // 鄰接表,key:頂點,value:該頂點的所有鄰接頂點 + val adjList: MutableMap> = HashMap() + + /* 建構子 */ + init { + // 新增所有頂點和邊 + for (edge in edges) { + addVertex(edge[0]!!); + addVertex(edge[1]!!); + addEdge(edge[0]!!, edge[1]!!); + } + } + + /* 獲取頂點數量 */ + fun size(): Int { + return adjList.size + } + + /* 新增邊 */ + fun addEdge(vet1: Vertex, vet2: Vertex) { + if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2) + throw IllegalArgumentException() + // 新增邊 vet1 - vet2 + adjList[vet1]?.add(vet2) + adjList[vet2]?.add(vet1); + } + + /* 刪除邊 */ + fun removeEdge(vet1: Vertex, vet2: Vertex) { + if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2) + throw IllegalArgumentException() + // 刪除邊 vet1 - vet2 + adjList[vet1]?.remove(vet2); + adjList[vet2]?.remove(vet1); + } + + /* 新增頂點 */ + fun addVertex(vet: Vertex) { + if (adjList.containsKey(vet)) + return + // 在鄰接表中新增一個新鏈結串列 + adjList[vet] = mutableListOf() + } + + /* 刪除頂點 */ + fun removeVertex(vet: Vertex) { + if (!adjList.containsKey(vet)) + throw IllegalArgumentException() + // 在鄰接表中刪除頂點 vet 對應的鏈結串列 + adjList.remove(vet); + // 走訪其他頂點的鏈結串列,刪除所有包含 vet 的邊 + for (list in adjList.values) { + list.remove(vet) + } + } + + /* 列印鄰接表 */ + fun print() { + println("鄰接表 =") + for (pair in adjList.entries) { + val tmp = ArrayList() + for (vertex in pair.value) { + tmp.add(vertex.value) + } + println("${pair.key.value}: $tmp,") + } + } + } + ``` + +=== "Ruby" + + ```ruby title="graph_adjacency_list.rb" + [class]{GraphAdjList}-[func]{} + ``` + +=== "Zig" + + ```zig title="graph_adjacency_list.zig" + [class]{GraphAdjList}-[func]{} + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 9.2.3   效率對比 + +設圖中共有 $n$ 個頂點和 $m$ 條邊,表 9-2 對比了鄰接矩陣和鄰接表的時間效率和空間效率。 + +

表 9-2   鄰接矩陣與鄰接表對比

+ +
+ +| | 鄰接矩陣 | 鄰接表(鏈結串列) | 鄰接表(雜湊表) | +| ------------ | -------- | -------------- | ---------------- | +| 判斷是否鄰接 | $O(1)$ | $O(m)$ | $O(1)$ | +| 新增邊 | $O(1)$ | $O(1)$ | $O(1)$ | +| 刪除邊 | $O(1)$ | $O(m)$ | $O(1)$ | +| 新增頂點 | $O(n)$ | $O(1)$ | $O(1)$ | +| 刪除頂點 | $O(n^2)$ | $O(n + m)$ | $O(n)$ | +| 記憶體空間佔用 | $O(n^2)$ | $O(n + m)$ | $O(n + m)$ | + +
+ +觀察表 9-2 ,似乎鄰接表(雜湊表)的時間效率與空間效率最優。但實際上,在鄰接矩陣中操作邊的效率更高,只需一次陣列訪問或賦值操作即可。綜合來看,鄰接矩陣體現了“以空間換時間”的原則,而鄰接表體現了“以時間換空間”的原則。 diff --git a/zh-Hant/docs/chapter_graph/graph_traversal.md b/zh-Hant/docs/chapter_graph/graph_traversal.md new file mode 100644 index 000000000..27b51c314 --- /dev/null +++ b/zh-Hant/docs/chapter_graph/graph_traversal.md @@ -0,0 +1,967 @@ +--- +comments: true +--- + +# 9.3   圖的走訪 + +樹代表的是“一對多”的關係,而圖則具有更高的自由度,可以表示任意的“多對多”關係。因此,我們可以把樹看作圖的一種特例。顯然,**樹的走訪操作也是圖的走訪操作的一種特例**。 + +圖和樹都需要應用搜索演算法來實現走訪操作。圖的走訪方式也可分為兩種:廣度優先走訪深度優先走訪。 + +## 9.3.1   廣度優先走訪 + +**廣度優先走訪是一種由近及遠的走訪方式,從某個節點出發,始終優先訪問距離最近的頂點,並一層層向外擴張**。如圖 9-9 所示,從左上角頂點出發,首先走訪該頂點的所有鄰接頂點,然後走訪下一個頂點的所有鄰接頂點,以此類推,直至所有頂點訪問完畢。 + +![圖的廣度優先走訪](graph_traversal.assets/graph_bfs.png){ class="animation-figure" } + +

圖 9-9   圖的廣度優先走訪

+ +### 1.   演算法實現 + +BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入先出”的性質,這與 BFS 的“由近及遠”的思想異曲同工。 + +1. 將走訪起始頂點 `startVet` 加入列列,並開啟迴圈。 +2. 在迴圈的每輪迭代中,彈出佇列首頂點並記錄訪問,然後將該頂點的所有鄰接頂點加入到佇列尾部。 +3. 迴圈步驟 `2.` ,直到所有頂點被訪問完畢後結束。 + +為了防止重複走訪頂點,我們需要藉助一個雜湊表 `visited` 來記錄哪些節點已被訪問。 + +=== "Python" + + ```python title="graph_bfs.py" + def graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]: + """廣度優先走訪""" + # 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點 + # 頂點走訪序列 + res = [] + # 雜湊表,用於記錄已被訪問過的頂點 + visited = set[Vertex]([start_vet]) + # 佇列用於實現 BFS + que = deque[Vertex]([start_vet]) + # 以頂點 vet 為起點,迴圈直至訪問完所有頂點 + while len(que) > 0: + vet = que.popleft() # 佇列首頂點出隊 + res.append(vet) # 記錄訪問頂點 + # 走訪該頂點的所有鄰接頂點 + for adj_vet in graph.adj_list[vet]: + if adj_vet in visited: + continue # 跳過已被訪問的頂點 + que.append(adj_vet) # 只入列未訪問的頂點 + visited.add(adj_vet) # 標記該頂點已被訪問 + # 返回頂點走訪序列 + return res + ``` + +=== "C++" + + ```cpp title="graph_bfs.cpp" + /* 廣度優先走訪 */ + // 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點 + vector graphBFS(GraphAdjList &graph, Vertex *startVet) { + // 頂點走訪序列 + vector res; + // 雜湊表,用於記錄已被訪問過的頂點 + unordered_set visited = {startVet}; + // 佇列用於實現 BFS + queue que; + que.push(startVet); + // 以頂點 vet 為起點,迴圈直至訪問完所有頂點 + while (!que.empty()) { + Vertex *vet = que.front(); + que.pop(); // 佇列首頂點出隊 + res.push_back(vet); // 記錄訪問頂點 + // 走訪該頂點的所有鄰接頂點 + for (auto adjVet : graph.adjList[vet]) { + if (visited.count(adjVet)) + continue; // 跳過已被訪問的頂點 + que.push(adjVet); // 只入列未訪問的頂點 + visited.emplace(adjVet); // 標記該頂點已被訪問 + } + } + // 返回頂點走訪序列 + return res; + } + ``` + +=== "Java" + + ```java title="graph_bfs.java" + /* 廣度優先走訪 */ + // 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點 + List graphBFS(GraphAdjList graph, Vertex startVet) { + // 頂點走訪序列 + List res = new ArrayList<>(); + // 雜湊表,用於記錄已被訪問過的頂點 + Set visited = new HashSet<>(); + visited.add(startVet); + // 佇列用於實現 BFS + Queue que = new LinkedList<>(); + que.offer(startVet); + // 以頂點 vet 為起點,迴圈直至訪問完所有頂點 + while (!que.isEmpty()) { + Vertex vet = que.poll(); // 佇列首頂點出隊 + res.add(vet); // 記錄訪問頂點 + // 走訪該頂點的所有鄰接頂點 + for (Vertex adjVet : graph.adjList.get(vet)) { + if (visited.contains(adjVet)) + continue; // 跳過已被訪問的頂點 + que.offer(adjVet); // 只入列未訪問的頂點 + visited.add(adjVet); // 標記該頂點已被訪問 + } + } + // 返回頂點走訪序列 + return res; + } + ``` + +=== "C#" + + ```csharp title="graph_bfs.cs" + /* 廣度優先走訪 */ + // 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點 + List GraphBFS(GraphAdjList graph, Vertex startVet) { + // 頂點走訪序列 + List res = []; + // 雜湊表,用於記錄已被訪問過的頂點 + HashSet visited = [startVet]; + // 佇列用於實現 BFS + Queue que = new(); + que.Enqueue(startVet); + // 以頂點 vet 為起點,迴圈直至訪問完所有頂點 + while (que.Count > 0) { + Vertex vet = que.Dequeue(); // 佇列首頂點出隊 + res.Add(vet); // 記錄訪問頂點 + foreach (Vertex adjVet in graph.adjList[vet]) { + if (visited.Contains(adjVet)) { + continue; // 跳過已被訪問的頂點 + } + que.Enqueue(adjVet); // 只入列未訪問的頂點 + visited.Add(adjVet); // 標記該頂點已被訪問 + } + } + + // 返回頂點走訪序列 + return res; + } + ``` + +=== "Go" + + ```go title="graph_bfs.go" + /* 廣度優先走訪 */ + // 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點 + func graphBFS(g *graphAdjList, startVet Vertex) []Vertex { + // 頂點走訪序列 + res := make([]Vertex, 0) + // 雜湊表,用於記錄已被訪問過的頂點 + visited := make(map[Vertex]struct{}) + visited[startVet] = struct{}{} + // 佇列用於實現 BFS, 使用切片模擬佇列 + queue := make([]Vertex, 0) + queue = append(queue, startVet) + // 以頂點 vet 為起點,迴圈直至訪問完所有頂點 + for len(queue) > 0 { + // 佇列首頂點出隊 + vet := queue[0] + queue = queue[1:] + // 記錄訪問頂點 + res = append(res, vet) + // 走訪該頂點的所有鄰接頂點 + for _, adjVet := range g.adjList[vet] { + _, isExist := visited[adjVet] + // 只入列未訪問的頂點 + if !isExist { + queue = append(queue, adjVet) + visited[adjVet] = struct{}{} + } + } + } + // 返回頂點走訪序列 + return res + } + ``` + +=== "Swift" + + ```swift title="graph_bfs.swift" + /* 廣度優先走訪 */ + // 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點 + func graphBFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] { + // 頂點走訪序列 + var res: [Vertex] = [] + // 雜湊表,用於記錄已被訪問過的頂點 + var visited: Set = [startVet] + // 佇列用於實現 BFS + var que: [Vertex] = [startVet] + // 以頂點 vet 為起點,迴圈直至訪問完所有頂點 + while !que.isEmpty { + let vet = que.removeFirst() // 佇列首頂點出隊 + res.append(vet) // 記錄訪問頂點 + // 走訪該頂點的所有鄰接頂點 + for adjVet in graph.adjList[vet] ?? [] { + if visited.contains(adjVet) { + continue // 跳過已被訪問的頂點 + } + que.append(adjVet) // 只入列未訪問的頂點 + visited.insert(adjVet) // 標記該頂點已被訪問 + } + } + // 返回頂點走訪序列 + return res + } + ``` + +=== "JS" + + ```javascript title="graph_bfs.js" + /* 廣度優先走訪 */ + // 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點 + function graphBFS(graph, startVet) { + // 頂點走訪序列 + const res = []; + // 雜湊表,用於記錄已被訪問過的頂點 + const visited = new Set(); + visited.add(startVet); + // 佇列用於實現 BFS + const que = [startVet]; + // 以頂點 vet 為起點,迴圈直至訪問完所有頂點 + while (que.length) { + const vet = que.shift(); // 佇列首頂點出隊 + res.push(vet); // 記錄訪問頂點 + // 走訪該頂點的所有鄰接頂點 + for (const adjVet of graph.adjList.get(vet) ?? []) { + if (visited.has(adjVet)) { + continue; // 跳過已被訪問的頂點 + } + que.push(adjVet); // 只入列未訪問的頂點 + visited.add(adjVet); // 標記該頂點已被訪問 + } + } + // 返回頂點走訪序列 + return res; + } + ``` + +=== "TS" + + ```typescript title="graph_bfs.ts" + /* 廣度優先走訪 */ + // 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點 + function graphBFS(graph: GraphAdjList, startVet: Vertex): Vertex[] { + // 頂點走訪序列 + const res: Vertex[] = []; + // 雜湊表,用於記錄已被訪問過的頂點 + const visited: Set = new Set(); + visited.add(startVet); + // 佇列用於實現 BFS + const que = [startVet]; + // 以頂點 vet 為起點,迴圈直至訪問完所有頂點 + while (que.length) { + const vet = que.shift(); // 佇列首頂點出隊 + res.push(vet); // 記錄訪問頂點 + // 走訪該頂點的所有鄰接頂點 + for (const adjVet of graph.adjList.get(vet) ?? []) { + if (visited.has(adjVet)) { + continue; // 跳過已被訪問的頂點 + } + que.push(adjVet); // 只入列未訪問 + visited.add(adjVet); // 標記該頂點已被訪問 + } + } + // 返回頂點走訪序列 + return res; + } + ``` + +=== "Dart" + + ```dart title="graph_bfs.dart" + /* 廣度優先走訪 */ + List graphBFS(GraphAdjList graph, Vertex startVet) { + // 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點 + // 頂點走訪序列 + List res = []; + // 雜湊表,用於記錄已被訪問過的頂點 + Set visited = {}; + visited.add(startVet); + // 佇列用於實現 BFS + Queue que = Queue(); + que.add(startVet); + // 以頂點 vet 為起點,迴圈直至訪問完所有頂點 + while (que.isNotEmpty) { + Vertex vet = que.removeFirst(); // 佇列首頂點出隊 + res.add(vet); // 記錄訪問頂點 + // 走訪該頂點的所有鄰接頂點 + for (Vertex adjVet in graph.adjList[vet]!) { + if (visited.contains(adjVet)) { + continue; // 跳過已被訪問的頂點 + } + que.add(adjVet); // 只入列未訪問的頂點 + visited.add(adjVet); // 標記該頂點已被訪問 + } + } + // 返回頂點走訪序列 + return res; + } + ``` + +=== "Rust" + + ```rust title="graph_bfs.rs" + /* 廣度優先走訪 */ + // 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點 + fn graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> Vec { + // 頂點走訪序列 + let mut res = vec![]; + // 雜湊表,用於記錄已被訪問過的頂點 + let mut visited = HashSet::new(); + visited.insert(start_vet); + // 佇列用於實現 BFS + let mut que = VecDeque::new(); + que.push_back(start_vet); + // 以頂點 vet 為起點,迴圈直至訪問完所有頂點 + while !que.is_empty() { + let vet = que.pop_front().unwrap(); // 佇列首頂點出隊 + res.push(vet); // 記錄訪問頂點 + + // 走訪該頂點的所有鄰接頂點 + if let Some(adj_vets) = graph.adj_list.get(&vet) { + for &adj_vet in adj_vets { + if visited.contains(&adj_vet) { + continue; // 跳過已被訪問的頂點 + } + que.push_back(adj_vet); // 只入列未訪問的頂點 + visited.insert(adj_vet); // 標記該頂點已被訪問 + } + } + } + // 返回頂點走訪序列 + res + } + ``` + +=== "C" + + ```c title="graph_bfs.c" + /* 節點佇列結構體 */ + typedef struct { + Vertex *vertices[MAX_SIZE]; + int front, rear, size; + } Queue; + + /* 建構子 */ + Queue *newQueue() { + Queue *q = (Queue *)malloc(sizeof(Queue)); + q->front = q->rear = q->size = 0; + return q; + } + + /* 判斷佇列是否為空 */ + int isEmpty(Queue *q) { + return q->size == 0; + } + + /* 入列操作 */ + void enqueue(Queue *q, Vertex *vet) { + q->vertices[q->rear] = vet; + q->rear = (q->rear + 1) % MAX_SIZE; + q->size++; + } + + /* 出列操作 */ + Vertex *dequeue(Queue *q) { + Vertex *vet = q->vertices[q->front]; + q->front = (q->front + 1) % MAX_SIZE; + q->size--; + return vet; + } + + /* 檢查頂點是否已被訪問 */ + int isVisited(Vertex **visited, int size, Vertex *vet) { + // 走訪查詢節點,使用 O(n) 時間 + for (int i = 0; i < size; i++) { + if (visited[i] == vet) + return 1; + } + return 0; + } + + /* 廣度優先走訪 */ + // 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點 + void graphBFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize, Vertex **visited, int *visitedSize) { + // 佇列用於實現 BFS + Queue *queue = newQueue(); + enqueue(queue, startVet); + visited[(*visitedSize)++] = startVet; + // 以頂點 vet 為起點,迴圈直至訪問完所有頂點 + while (!isEmpty(queue)) { + Vertex *vet = dequeue(queue); // 佇列首頂點出隊 + res[(*resSize)++] = vet; // 記錄訪問頂點 + // 走訪該頂點的所有鄰接頂點 + AdjListNode *node = findNode(graph, vet); + while (node != NULL) { + // 跳過已被訪問的頂點 + if (!isVisited(visited, *visitedSize, node->vertex)) { + enqueue(queue, node->vertex); // 只入列未訪問的頂點 + visited[(*visitedSize)++] = node->vertex; // 標記該頂點已被訪問 + } + node = node->next; + } + } + // 釋放記憶體 + free(queue); + } + ``` + +=== "Kotlin" + + ```kotlin title="graph_bfs.kt" + /* 廣度優先走訪 */ + // 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點 + fun graphBFS(graph: GraphAdjList, startVet: Vertex): List { + // 頂點走訪序列 + val res: MutableList = ArrayList() + // 雜湊表,用於記錄已被訪問過的頂點 + val visited: MutableSet = HashSet() + visited.add(startVet) + // 佇列用於實現 BFS + val que: Queue = LinkedList() + que.offer(startVet) + // 以頂點 vet 為起點,迴圈直至訪問完所有頂點 + while (!que.isEmpty()) { + val vet = que.poll() // 佇列首頂點出隊 + res.add(vet) // 記錄訪問頂點 + // 走訪該頂點的所有鄰接頂點 + for (adjVet in graph.adjList[vet]!!) { + if (visited.contains(adjVet)) continue // 跳過已被訪問的頂點 + + que.offer(adjVet) // 只入列未訪問的頂點 + visited.add(adjVet) // 標記該頂點已被訪問 + } + } + // 返回頂點走訪序列 + return res + } + ``` + +=== "Ruby" + + ```ruby title="graph_bfs.rb" + [class]{}-[func]{graph_bfs} + ``` + +=== "Zig" + + ```zig title="graph_bfs.zig" + [class]{}-[func]{graphBFS} + ``` + +??? pythontutor "視覺化執行" + +
+ + +程式碼相對抽象,建議對照圖 9-10 來加深理解。 + +=== "<1>" + ![圖的廣度優先走訪步驟](graph_traversal.assets/graph_bfs_step1.png){ class="animation-figure" } + +=== "<2>" + ![graph_bfs_step2](graph_traversal.assets/graph_bfs_step2.png){ class="animation-figure" } + +=== "<3>" + ![graph_bfs_step3](graph_traversal.assets/graph_bfs_step3.png){ class="animation-figure" } + +=== "<4>" + ![graph_bfs_step4](graph_traversal.assets/graph_bfs_step4.png){ class="animation-figure" } + +=== "<5>" + ![graph_bfs_step5](graph_traversal.assets/graph_bfs_step5.png){ class="animation-figure" } + +=== "<6>" + ![graph_bfs_step6](graph_traversal.assets/graph_bfs_step6.png){ class="animation-figure" } + +=== "<7>" + ![graph_bfs_step7](graph_traversal.assets/graph_bfs_step7.png){ class="animation-figure" } + +=== "<8>" + ![graph_bfs_step8](graph_traversal.assets/graph_bfs_step8.png){ class="animation-figure" } + +=== "<9>" + ![graph_bfs_step9](graph_traversal.assets/graph_bfs_step9.png){ class="animation-figure" } + +=== "<10>" + ![graph_bfs_step10](graph_traversal.assets/graph_bfs_step10.png){ class="animation-figure" } + +=== "<11>" + ![graph_bfs_step11](graph_traversal.assets/graph_bfs_step11.png){ class="animation-figure" } + +

圖 9-10   圖的廣度優先走訪步驟

+ +!!! question "廣度優先走訪的序列是否唯一?" + + 不唯一。廣度優先走訪只要求按“由近及遠”的順序走訪,**而多個相同距離的頂點的走訪順序允許被任意打亂**。以圖 9-10 為例,頂點 $1$、$3$ 的訪問順序可以交換,頂點 $2$、$4$、$6$ 的訪問順序也可以任意交換。 + +### 2.   複雜度分析 + +**時間複雜度**:所有頂點都會入列並出隊一次,使用 $O(|V|)$ 時間;在走訪鄰接頂點的過程中,由於是無向圖,因此所有邊都會被訪問 $2$ 次,使用 $O(2|E|)$ 時間;總體使用 $O(|V| + |E|)$ 時間。 + +**空間複雜度**:串列 `res` ,雜湊表 `visited` ,佇列 `que` 中的頂點數量最多為 $|V|$ ,使用 $O(|V|)$ 空間。 + +## 9.3.2   深度優先走訪 + +**深度優先走訪是一種優先走到底、無路可走再回頭的走訪方式**。如圖 9-11 所示,從左上角頂點出發,訪問當前頂點的某個鄰接頂點,直到走到盡頭時返回,再繼續走到盡頭並返回,以此類推,直至所有頂點走訪完成。 + +![圖的深度優先走訪](graph_traversal.assets/graph_dfs.png){ class="animation-figure" } + +

圖 9-11   圖的深度優先走訪

+ +### 1.   演算法實現 + +這種“走到盡頭再返回”的演算法範式通常基於遞迴來實現。與廣度優先走訪類似,在深度優先走訪中,我們也需要藉助一個雜湊表 `visited` 來記錄已被訪問的頂點,以避免重複訪問頂點。 + +=== "Python" + + ```python title="graph_dfs.py" + def dfs(graph: GraphAdjList, visited: set[Vertex], res: list[Vertex], vet: Vertex): + """深度優先走訪輔助函式""" + res.append(vet) # 記錄訪問頂點 + visited.add(vet) # 標記該頂點已被訪問 + # 走訪該頂點的所有鄰接頂點 + for adjVet in graph.adj_list[vet]: + if adjVet in visited: + continue # 跳過已被訪問的頂點 + # 遞迴訪問鄰接頂點 + dfs(graph, visited, res, adjVet) + + def graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]: + """深度優先走訪""" + # 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點 + # 頂點走訪序列 + res = [] + # 雜湊表,用於記錄已被訪問過的頂點 + visited = set[Vertex]() + dfs(graph, visited, res, start_vet) + return res + ``` + +=== "C++" + + ```cpp title="graph_dfs.cpp" + /* 深度優先走訪輔助函式 */ + void dfs(GraphAdjList &graph, unordered_set &visited, vector &res, Vertex *vet) { + res.push_back(vet); // 記錄訪問頂點 + visited.emplace(vet); // 標記該頂點已被訪問 + // 走訪該頂點的所有鄰接頂點 + for (Vertex *adjVet : graph.adjList[vet]) { + if (visited.count(adjVet)) + continue; // 跳過已被訪問的頂點 + // 遞迴訪問鄰接頂點 + dfs(graph, visited, res, adjVet); + } + } + + /* 深度優先走訪 */ + // 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點 + vector graphDFS(GraphAdjList &graph, Vertex *startVet) { + // 頂點走訪序列 + vector res; + // 雜湊表,用於記錄已被訪問過的頂點 + unordered_set visited; + dfs(graph, visited, res, startVet); + return res; + } + ``` + +=== "Java" + + ```java title="graph_dfs.java" + /* 深度優先走訪輔助函式 */ + void dfs(GraphAdjList graph, Set visited, List res, Vertex vet) { + res.add(vet); // 記錄訪問頂點 + visited.add(vet); // 標記該頂點已被訪問 + // 走訪該頂點的所有鄰接頂點 + for (Vertex adjVet : graph.adjList.get(vet)) { + if (visited.contains(adjVet)) + continue; // 跳過已被訪問的頂點 + // 遞迴訪問鄰接頂點 + dfs(graph, visited, res, adjVet); + } + } + + /* 深度優先走訪 */ + // 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點 + List graphDFS(GraphAdjList graph, Vertex startVet) { + // 頂點走訪序列 + List res = new ArrayList<>(); + // 雜湊表,用於記錄已被訪問過的頂點 + Set visited = new HashSet<>(); + dfs(graph, visited, res, startVet); + return res; + } + ``` + +=== "C#" + + ```csharp title="graph_dfs.cs" + /* 深度優先走訪輔助函式 */ + void DFS(GraphAdjList graph, HashSet visited, List res, Vertex vet) { + res.Add(vet); // 記錄訪問頂點 + visited.Add(vet); // 標記該頂點已被訪問 + // 走訪該頂點的所有鄰接頂點 + foreach (Vertex adjVet in graph.adjList[vet]) { + if (visited.Contains(adjVet)) { + continue; // 跳過已被訪問的頂點 + } + // 遞迴訪問鄰接頂點 + DFS(graph, visited, res, adjVet); + } + } + + /* 深度優先走訪 */ + // 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點 + List GraphDFS(GraphAdjList graph, Vertex startVet) { + // 頂點走訪序列 + List res = []; + // 雜湊表,用於記錄已被訪問過的頂點 + HashSet visited = []; + DFS(graph, visited, res, startVet); + return res; + } + ``` + +=== "Go" + + ```go title="graph_dfs.go" + /* 深度優先走訪輔助函式 */ + func dfs(g *graphAdjList, visited map[Vertex]struct{}, res *[]Vertex, vet Vertex) { + // append 操作會返回新的的引用,必須讓原引用重新賦值為新slice的引用 + *res = append(*res, vet) + visited[vet] = struct{}{} + // 走訪該頂點的所有鄰接頂點 + for _, adjVet := range g.adjList[vet] { + _, isExist := visited[adjVet] + // 遞迴訪問鄰接頂點 + if !isExist { + dfs(g, visited, res, adjVet) + } + } + } + + /* 深度優先走訪 */ + // 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點 + func graphDFS(g *graphAdjList, startVet Vertex) []Vertex { + // 頂點走訪序列 + res := make([]Vertex, 0) + // 雜湊表,用於記錄已被訪問過的頂點 + visited := make(map[Vertex]struct{}) + dfs(g, visited, &res, startVet) + // 返回頂點走訪序列 + return res + } + ``` + +=== "Swift" + + ```swift title="graph_dfs.swift" + /* 深度優先走訪輔助函式 */ + func dfs(graph: GraphAdjList, visited: inout Set, res: inout [Vertex], vet: Vertex) { + res.append(vet) // 記錄訪問頂點 + visited.insert(vet) // 標記該頂點已被訪問 + // 走訪該頂點的所有鄰接頂點 + for adjVet in graph.adjList[vet] ?? [] { + if visited.contains(adjVet) { + continue // 跳過已被訪問的頂點 + } + // 遞迴訪問鄰接頂點 + dfs(graph: graph, visited: &visited, res: &res, vet: adjVet) + } + } + + /* 深度優先走訪 */ + // 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點 + func graphDFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] { + // 頂點走訪序列 + var res: [Vertex] = [] + // 雜湊表,用於記錄已被訪問過的頂點 + var visited: Set = [] + dfs(graph: graph, visited: &visited, res: &res, vet: startVet) + return res + } + ``` + +=== "JS" + + ```javascript title="graph_dfs.js" + /* 深度優先走訪 */ + // 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點 + function dfs(graph, visited, res, vet) { + res.push(vet); // 記錄訪問頂點 + visited.add(vet); // 標記該頂點已被訪問 + // 走訪該頂點的所有鄰接頂點 + for (const adjVet of graph.adjList.get(vet)) { + if (visited.has(adjVet)) { + continue; // 跳過已被訪問的頂點 + } + // 遞迴訪問鄰接頂點 + dfs(graph, visited, res, adjVet); + } + } + + /* 深度優先走訪 */ + // 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點 + function graphDFS(graph, startVet) { + // 頂點走訪序列 + const res = []; + // 雜湊表,用於記錄已被訪問過的頂點 + const visited = new Set(); + dfs(graph, visited, res, startVet); + return res; + } + ``` + +=== "TS" + + ```typescript title="graph_dfs.ts" + /* 深度優先走訪輔助函式 */ + function dfs( + graph: GraphAdjList, + visited: Set, + res: Vertex[], + vet: Vertex + ): void { + res.push(vet); // 記錄訪問頂點 + visited.add(vet); // 標記該頂點已被訪問 + // 走訪該頂點的所有鄰接頂點 + for (const adjVet of graph.adjList.get(vet)) { + if (visited.has(adjVet)) { + continue; // 跳過已被訪問的頂點 + } + // 遞迴訪問鄰接頂點 + dfs(graph, visited, res, adjVet); + } + } + + /* 深度優先走訪 */ + // 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點 + function graphDFS(graph: GraphAdjList, startVet: Vertex): Vertex[] { + // 頂點走訪序列 + const res: Vertex[] = []; + // 雜湊表,用於記錄已被訪問過的頂點 + const visited: Set = new Set(); + dfs(graph, visited, res, startVet); + return res; + } + ``` + +=== "Dart" + + ```dart title="graph_dfs.dart" + /* 深度優先走訪輔助函式 */ + void dfs( + GraphAdjList graph, + Set visited, + List res, + Vertex vet, + ) { + res.add(vet); // 記錄訪問頂點 + visited.add(vet); // 標記該頂點已被訪問 + // 走訪該頂點的所有鄰接頂點 + for (Vertex adjVet in graph.adjList[vet]!) { + if (visited.contains(adjVet)) { + continue; // 跳過已被訪問的頂點 + } + // 遞迴訪問鄰接頂點 + dfs(graph, visited, res, adjVet); + } + } + + /* 深度優先走訪 */ + List graphDFS(GraphAdjList graph, Vertex startVet) { + // 頂點走訪序列 + List res = []; + // 雜湊表,用於記錄已被訪問過的頂點 + Set visited = {}; + dfs(graph, visited, res, startVet); + return res; + } + ``` + +=== "Rust" + + ```rust title="graph_dfs.rs" + /* 深度優先走訪輔助函式 */ + fn dfs(graph: &GraphAdjList, visited: &mut HashSet, res: &mut Vec, vet: Vertex) { + res.push(vet); // 記錄訪問頂點 + visited.insert(vet); // 標記該頂點已被訪問 + // 走訪該頂點的所有鄰接頂點 + if let Some(adj_vets) = graph.adj_list.get(&vet) { + for &adj_vet in adj_vets { + if visited.contains(&adj_vet) { + continue; // 跳過已被訪問的頂點 + } + // 遞迴訪問鄰接頂點 + dfs(graph, visited, res, adj_vet); + } + } + } + + /* 深度優先走訪 */ + // 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點 + fn graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> Vec { + // 頂點走訪序列 + let mut res = vec![]; + // 雜湊表,用於記錄已被訪問過的頂點 + let mut visited = HashSet::new(); + dfs(&graph, &mut visited, &mut res, start_vet); + + res + } + ``` + +=== "C" + + ```c title="graph_dfs.c" + /* 檢查頂點是否已被訪問 */ + int isVisited(Vertex **res, int size, Vertex *vet) { + // 走訪查詢節點,使用 O(n) 時間 + for (int i = 0; i < size; i++) { + if (res[i] == vet) { + return 1; + } + } + return 0; + } + + /* 深度優先走訪輔助函式 */ + void dfs(GraphAdjList *graph, Vertex **res, int *resSize, Vertex *vet) { + // 記錄訪問頂點 + res[(*resSize)++] = vet; + // 走訪該頂點的所有鄰接頂點 + AdjListNode *node = findNode(graph, vet); + while (node != NULL) { + // 跳過已被訪問的頂點 + if (!isVisited(res, *resSize, node->vertex)) { + // 遞迴訪問鄰接頂點 + dfs(graph, res, resSize, node->vertex); + } + node = node->next; + } + } + + /* 深度優先走訪 */ + // 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點 + void graphDFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize) { + dfs(graph, res, resSize, startVet); + } + ``` + +=== "Kotlin" + + ```kotlin title="graph_dfs.kt" + /* 深度優先走訪輔助函式 */ + fun dfs( + graph: GraphAdjList, + visited: MutableSet, + res: MutableList, + vet: Vertex? + ) { + res.add(vet) // 記錄訪問頂點 + visited.add(vet) // 標記該頂點已被訪問 + // 走訪該頂點的所有鄰接頂點 + for (adjVet in graph.adjList[vet]!!) { + if (visited.contains(adjVet)) continue // 跳過已被訪問的頂點 + // 遞迴訪問鄰接頂點 + dfs(graph, visited, res, adjVet) + } + } + + /* 深度優先走訪 */ + // 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點 + fun graphDFS( + graph: GraphAdjList, + startVet: Vertex? + ): List { + // 頂點走訪序列 + val res: MutableList = ArrayList() + // 雜湊表,用於記錄已被訪問過的頂點 + val visited: MutableSet = HashSet() + dfs(graph, visited, res, startVet) + return res + } + ``` + +=== "Ruby" + + ```ruby title="graph_dfs.rb" + [class]{}-[func]{dfs} + + [class]{}-[func]{graph_dfs} + ``` + +=== "Zig" + + ```zig title="graph_dfs.zig" + [class]{}-[func]{dfs} + + [class]{}-[func]{graphDFS} + ``` + +??? pythontutor "視覺化執行" + +
+ + +深度優先走訪的演算法流程如圖 9-12 所示。 + +- **直虛線代表向下遞推**,表示開啟了一個新的遞迴方法來訪問新頂點。 +- **曲虛線代表向上回溯**,表示此遞迴方法已經返回,回溯到了開啟此方法的位置。 + +為了加深理解,建議將圖 9-12 與程式碼結合起來,在腦中模擬(或者用筆畫下來)整個 DFS 過程,包括每個遞迴方法何時開啟、何時返回。 + +=== "<1>" + ![圖的深度優先走訪步驟](graph_traversal.assets/graph_dfs_step1.png){ class="animation-figure" } + +=== "<2>" + ![graph_dfs_step2](graph_traversal.assets/graph_dfs_step2.png){ class="animation-figure" } + +=== "<3>" + ![graph_dfs_step3](graph_traversal.assets/graph_dfs_step3.png){ class="animation-figure" } + +=== "<4>" + ![graph_dfs_step4](graph_traversal.assets/graph_dfs_step4.png){ class="animation-figure" } + +=== "<5>" + ![graph_dfs_step5](graph_traversal.assets/graph_dfs_step5.png){ class="animation-figure" } + +=== "<6>" + ![graph_dfs_step6](graph_traversal.assets/graph_dfs_step6.png){ class="animation-figure" } + +=== "<7>" + ![graph_dfs_step7](graph_traversal.assets/graph_dfs_step7.png){ class="animation-figure" } + +=== "<8>" + ![graph_dfs_step8](graph_traversal.assets/graph_dfs_step8.png){ class="animation-figure" } + +=== "<9>" + ![graph_dfs_step9](graph_traversal.assets/graph_dfs_step9.png){ class="animation-figure" } + +=== "<10>" + ![graph_dfs_step10](graph_traversal.assets/graph_dfs_step10.png){ class="animation-figure" } + +=== "<11>" + ![graph_dfs_step11](graph_traversal.assets/graph_dfs_step11.png){ class="animation-figure" } + +

圖 9-12   圖的深度優先走訪步驟

+ +!!! question "深度優先走訪的序列是否唯一?" + + 與廣度優先走訪類似,深度優先走訪序列的順序也不是唯一的。給定某頂點,先往哪個方向探索都可以,即鄰接頂點的順序可以任意打亂,都是深度優先走訪。 + + 以樹的走訪為例,“根 $\rightarrow$ 左 $\rightarrow$ 右”“左 $\rightarrow$ 根 $\rightarrow$ 右”“左 $\rightarrow$ 右 $\rightarrow$ 根”分別對應前序、中序、後序走訪,它們展示了三種走訪優先順序,然而這三者都屬於深度優先走訪。 + +### 2.   複雜度分析 + +**時間複雜度**:所有頂點都會被訪問 $1$ 次,使用 $O(|V|)$ 時間;所有邊都會被訪問 $2$ 次,使用 $O(2|E|)$ 時間;總體使用 $O(|V| + |E|)$ 時間。 + +**空間複雜度**:串列 `res` ,雜湊表 `visited` 頂點數量最多為 $|V|$ ,遞迴深度最大為 $|V|$ ,因此使用 $O(|V|)$ 空間。 diff --git a/zh-Hant/docs/chapter_graph/index.md b/zh-Hant/docs/chapter_graph/index.md new file mode 100644 index 000000000..7232b25c4 --- /dev/null +++ b/zh-Hant/docs/chapter_graph/index.md @@ -0,0 +1,21 @@ +--- +comments: true +icon: material/graphql +--- + +# 第 9 章   圖 + +![圖](../assets/covers/chapter_graph.jpg){ 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/) diff --git a/zh-Hant/docs/chapter_graph/summary.md b/zh-Hant/docs/chapter_graph/summary.md new file mode 100644 index 000000000..2358114f9 --- /dev/null +++ b/zh-Hant/docs/chapter_graph/summary.md @@ -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**:在鄰接表中,“與該頂點相連的所有頂點”的頂點順序是否有要求? + +可以是任意順序。但在實際應用中,可能需要按照指定規則來排序,比如按照頂點新增的次序,或者按照頂點值大小的順序等,這樣有助於快速查詢“帶有某種極值”的頂點。 diff --git a/zh-Hant/docs/chapter_greedy/fractional_knapsack_problem.md b/zh-Hant/docs/chapter_greedy/fractional_knapsack_problem.md new file mode 100644 index 000000000..060e4fd5f --- /dev/null +++ b/zh-Hant/docs/chapter_greedy/fractional_knapsack_problem.md @@ -0,0 +1,568 @@ +--- +comments: true +--- + +# 15.2   分數背包問題 + +!!! question + + 給定 $n$ 個物品,第 $i$ 個物品的重量為 $wgt[i-1]$、價值為 $val[i-1]$ ,和一個容量為 $cap$ 的背包。每個物品只能選擇一次,**但可以選擇物品的一部分,價值根據選擇的重量比例計算**,問在限定背包容量下背包中物品的最大價值。示例如圖 15-3 所示。 + +![分數背包問題的示例資料](fractional_knapsack_problem.assets/fractional_knapsack_example.png){ class="animation-figure" } + +

圖 15-3   分數背包問題的示例資料

+ +分數背包問題和 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]$ 。 + +![物品在單位重量下的價值](fractional_knapsack_problem.assets/fractional_knapsack_unit_value.png){ class="animation-figure" } + +

圖 15-4   物品在單位重量下的價值

+ +### 1.   貪婪策略確定 + +最大化背包內物品總價值,**本質上是最大化單位重量下的物品價值**。由此便可推理出圖 15-5 所示的貪婪策略。 + +1. 將物品按照單位價值從高到低進行排序。 +2. 走訪所有物品,**每輪貪婪地選擇單位價值最高的物品**。 +3. 若剩餘背包容量不足,則使用當前物品的一部分填滿背包。 + +![分數背包問題的貪婪策略](fractional_knapsack_problem.assets/fractional_knapsack_greedy_strategy.png){ class="animation-figure" } + +

圖 15-5   分數背包問題的貪婪策略

+ +### 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 &wgt, vector &val, int cap) { + // 建立物品串列,包含兩個屬性:重量、價值 + vector 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 wgt, List val, int cap) { + // 建立物品串列,包含兩個屬性:重量、價值 + List 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::>(); + // 按照單位價值 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(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(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 "視覺化執行" + +
+ + +除排序之外,在最差情況下,需要走訪整個物品串列,**因此時間複雜度為 $O(n)$** ,其中 $n$ 為物品數量。 + +由於初始化了一個 `Item` 物件串列,**因此空間複雜度為 $O(n)$** 。 + +### 3.   正確性證明 + +採用反證法。假設物品 $x$ 是單位價值最高的物品,使用某演算法求得最大價值為 `res` ,但該解中不包含物品 $x$ 。 + +現在從背包中拿出單位重量的任意物品,並替換為單位重量的物品 $x$ 。由於物品 $x$ 的單位價值最高,因此替換後的總價值一定大於 `res` 。**這與 `res` 是最優解矛盾,說明最優解中必須包含物品 $x$** 。 + +對於該解中的其他物品,我們也可以構建出上述矛盾。總而言之,**單位價值更大的物品總是更優選擇**,這說明貪婪策略是有效的。 + +如圖 15-6 所示,如果將物品重量和物品單位價值分別看作一張二維圖表的橫軸和縱軸,則分數背包問題可轉化為“求在有限橫軸區間下圍成的最大面積”。這個類比可以幫助我們從幾何角度理解貪婪策略的有效性。 + +![分數背包問題的幾何表示](fractional_knapsack_problem.assets/fractional_knapsack_area_chart.png){ class="animation-figure" } + +

圖 15-6   分數背包問題的幾何表示

diff --git a/zh-Hant/docs/chapter_greedy/greedy_algorithm.md b/zh-Hant/docs/chapter_greedy/greedy_algorithm.md new file mode 100644 index 000000000..5aab16b48 --- /dev/null +++ b/zh-Hant/docs/chapter_greedy/greedy_algorithm.md @@ -0,0 +1,397 @@ +--- +comments: true +--- + +# 15.1   貪婪演算法 + +貪婪演算法(greedy algorithm)是一種常見的解決最佳化問題的演算法,其基本思想是在問題的每個決策階段,都選擇當前看起來最優的選擇,即貪婪地做出區域性最優的決策,以期獲得全域性最優解。貪婪演算法簡潔且高效,在許多實際問題中有著廣泛的應用。 + +貪婪演算法和動態規劃都常用於解決最佳化問題。它們之間存在一些相似之處,比如都依賴最優子結構性質,但工作原理不同。 + +- 動態規劃會根據之前階段的所有決策來考慮當前決策,並使用過去子問題的解來構建當前子問題的解。 +- 貪婪演算法不會考慮過去的決策,而是一路向前地進行貪婪選擇,不斷縮小問題範圍,直至問題被解決。 + +我們先透過例題“零錢兌換”瞭解貪婪演算法的工作原理。這道題已經在“完全背包問題”章節中介紹過,相信你對它並不陌生。 + +!!! question + + 給定 $n$ 種硬幣,第 $i$ 種硬幣的面值為 $coins[i - 1]$ ,目標金額為 $amt$ ,每種硬幣可以重複選取,問能夠湊出目標金額的最少硬幣數量。如果無法湊出目標金額,則返回 $-1$ 。 + +本題採取的貪婪策略如圖 15-1 所示。給定目標金額,**我們貪婪地選擇不大於且最接近它的硬幣**,不斷迴圈該步驟,直至湊出目標金額為止。 + +![零錢兌換的貪婪策略](greedy_algorithm.assets/coin_change_greedy_strategy.png){ class="animation-figure" } + +

圖 15-1   零錢兌換的貪婪策略

+ +實現程式碼如下所示: + +=== "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 &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 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 "視覺化執行" + +
+ + +你可能會不由地發出感嘆: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$ 枚硬幣。 + +![貪婪演算法無法找出最優解的示例](greedy_algorithm.assets/coin_change_greedy_vs_dp.png){ class="animation-figure" } + +

圖 15-2   貪婪演算法無法找出最優解的示例

+ +也就是說,對於零錢兌換問題,貪婪演算法無法保證找到全域性最優解,並且有可能找到非常差的解。它更適合用動態規劃解決。 + +一般情況下,貪婪演算法的適用情況分以下兩種。 + +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 演算法**:它是一種解決給定源頂點到其餘各頂點的最短路徑問題的貪婪演算法。 diff --git a/zh-Hant/docs/chapter_greedy/index.md b/zh-Hant/docs/chapter_greedy/index.md new file mode 100644 index 000000000..524fa5760 --- /dev/null +++ b/zh-Hant/docs/chapter_greedy/index.md @@ -0,0 +1,22 @@ +--- +comments: true +icon: material/head-heart-outline +--- + +# 第 15 章   貪婪 + +![貪婪](../assets/covers/chapter_greedy.jpg){ 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/) diff --git a/zh-Hant/docs/chapter_greedy/max_capacity_problem.md b/zh-Hant/docs/chapter_greedy/max_capacity_problem.md new file mode 100644 index 000000000..920923bcf --- /dev/null +++ b/zh-Hant/docs/chapter_greedy/max_capacity_problem.md @@ -0,0 +1,430 @@ +--- +comments: true +--- + +# 15.3   最大容量問題 + +!!! question + + 輸入一個陣列 $ht$ ,其中的每個元素代表一個垂直隔板的高度。陣列中的任意兩個隔板,以及它們之間的空間可以組成一個容器。 + + 容器的容量等於高度和寬度的乘積(面積),其中高度由較短的隔板決定,寬度是兩個隔板的陣列索引之差。 + + 請在陣列中選擇兩個隔板,使得組成的容器的容量最大,返回最大容量。示例如圖 15-7 所示。 + +![最大容量問題的示例資料](max_capacity_problem.assets/max_capacity_example.png){ class="animation-figure" } + +

圖 15-7   最大容量問題的示例資料

+ +容器由任意兩個隔板圍成,**因此本題的狀態為兩個隔板的索引,記為 $[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$ 為長板。 + +![初始狀態](max_capacity_problem.assets/max_capacity_initial_state.png){ class="animation-figure" } + +

圖 15-8   初始狀態

+ +如圖 15-9 所示,**若此時將長板 $j$ 向短板 $i$ 靠近,則容量一定變小**。 + +這是因為在移動長板 $j$ 後,寬度 $j-i$ 肯定變小;而高度由短板決定,因此高度只可能不變( $i$ 仍為短板)或變小(移動後的 $j$ 成為短板)。 + +![向內移動長板後的狀態](max_capacity_problem.assets/max_capacity_moving_long_board.png){ class="animation-figure" } + +

圖 15-9   向內移動長板後的狀態

+ +反向思考,**我們只有向內收縮短板 $i$ ,才有可能使容量變大**。因為雖然寬度一定變小,**但高度可能會變大**(移動後的短板 $i$ 可能會變長)。例如在圖 15-10 中,移動短板後面積變大。 + +![向內移動短板後的狀態](max_capacity_problem.assets/max_capacity_moving_short_board.png){ class="animation-figure" } + +

圖 15-10   向內移動短板後的狀態

+ +由此便可推出本題的貪婪策略:初始化兩指標,使其分列容器兩端,每輪向內收縮短板對應的指標,直至兩指標相遇。 + +圖 15-11 展示了貪婪策略的執行過程。 + +1. 初始狀態下,指標 $i$ 和 $j$ 分列陣列兩端。 +2. 計算當前狀態的容量 $cap[i, j]$ ,並更新最大容量。 +3. 比較板 $i$ 和 板 $j$ 的高度,並將短板向內移動一格。 +4. 迴圈執行第 `2.` 步和第 `3.` 步,直至 $i$ 和 $j$ 相遇時結束。 + +=== "<1>" + ![最大容量問題的貪婪過程](max_capacity_problem.assets/max_capacity_greedy_step1.png){ class="animation-figure" } + +=== "<2>" + ![max_capacity_greedy_step2](max_capacity_problem.assets/max_capacity_greedy_step2.png){ class="animation-figure" } + +=== "<3>" + ![max_capacity_greedy_step3](max_capacity_problem.assets/max_capacity_greedy_step3.png){ class="animation-figure" } + +=== "<4>" + ![max_capacity_greedy_step4](max_capacity_problem.assets/max_capacity_greedy_step4.png){ class="animation-figure" } + +=== "<5>" + ![max_capacity_greedy_step5](max_capacity_problem.assets/max_capacity_greedy_step5.png){ class="animation-figure" } + +=== "<6>" + ![max_capacity_greedy_step6](max_capacity_problem.assets/max_capacity_greedy_step6.png){ class="animation-figure" } + +=== "<7>" + ![max_capacity_greedy_step7](max_capacity_problem.assets/max_capacity_greedy_step7.png){ class="animation-figure" } + +=== "<8>" + ![max_capacity_greedy_step8](max_capacity_problem.assets/max_capacity_greedy_step8.png){ class="animation-figure" } + +=== "<9>" + ![max_capacity_greedy_step9](max_capacity_problem.assets/max_capacity_greedy_step9.png){ class="animation-figure" } + +

圖 15-11   最大容量問題的貪婪過程

+ +### 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 &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 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 "視覺化執行" + +
+ + +### 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] +$$ + +![移動短板導致被跳過的狀態](max_capacity_problem.assets/max_capacity_skipped_states.png){ class="animation-figure" } + +

圖 15-12   移動短板導致被跳過的狀態

+ +觀察發現,**這些被跳過的狀態實際上就是將長板 $j$ 向內移動的所有狀態**。前面我們已經證明內移長板一定會導致容量變小。也就是說,被跳過的狀態都不可能是最優解,**跳過它們不會導致錯過最優解**。 + +以上分析說明,移動短板的操作是“安全”的,貪婪策略是有效的。 diff --git a/zh-Hant/docs/chapter_greedy/max_product_cutting_problem.md b/zh-Hant/docs/chapter_greedy/max_product_cutting_problem.md new file mode 100644 index 000000000..c42808d09 --- /dev/null +++ b/zh-Hant/docs/chapter_greedy/max_product_cutting_problem.md @@ -0,0 +1,405 @@ +--- +comments: true +--- + +# 15.4   最大切分乘積問題 + +!!! question + + 給定一個正整數 $n$ ,將其切分為至少兩個正整數的和,求切分後所有整數的乘積最大是多少,如圖 15-13 所示。 + +![最大切分乘積的問題定義](max_product_cutting_problem.assets/max_product_cutting_definition.png){ class="animation-figure" } + +

圖 15-13   最大切分乘積的問題定義

+ +假設我們將 $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$ 這三種因子。 + +![切分導致乘積變大](max_product_cutting_problem.assets/max_product_cutting_greedy_infer1.png){ class="animation-figure" } + +

圖 15-14   切分導致乘積變大

+ +接下來思考哪個因子是最優的。在 $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$ ,從而獲得更大的乘積。 + +![最優切分因子](max_product_cutting_problem.assets/max_product_cutting_greedy_infer2.png){ class="animation-figure" } + +

圖 15-15   最優切分因子

+ +綜上所述,可推理出以下貪婪策略。 + +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 "視覺化執行" + +
+ + +![最大切分乘積的計算方法](max_product_cutting_problem.assets/max_product_cutting_greedy_calculation.png){ class="animation-figure" } + +

圖 15-16   最大切分乘積的計算方法

+ +**時間複雜度取決於程式語言的冪運算的實現方法**。以 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$ ,乘積更大。這與假設矛盾。 diff --git a/zh-Hant/docs/chapter_greedy/summary.md b/zh-Hant/docs/chapter_greedy/summary.md new file mode 100644 index 000000000..3d1f3b71c --- /dev/null +++ b/zh-Hant/docs/chapter_greedy/summary.md @@ -0,0 +1,16 @@ +--- +comments: true +--- + +# 15.5   小結 + +- 貪婪演算法通常用於解決最最佳化問題,其原理是在每個決策階段都做出區域性最優的決策,以期獲得全域性最優解。 +- 貪婪演算法會迭代地做出一個又一個的貪婪選擇,每輪都將問題轉化成一個規模更小的子問題,直到問題被解決。 +- 貪婪演算法不僅實現簡單,還具有很高的解題效率。相比於動態規劃,貪婪演算法的時間複雜度通常更低。 +- 在零錢兌換問題中,對於某些硬幣組合,貪婪演算法可以保證找到最優解;對於另外一些硬幣組合則不然,貪婪演算法可能找到很差的解。 +- 適合用貪婪演算法求解的問題具有兩大性質:貪婪選擇性質和最優子結構。貪婪選擇性質代表貪婪策略的有效性。 +- 對於某些複雜問題,貪婪選擇性質的證明並不簡單。相對來說,證偽更加容易,例如零錢兌換問題。 +- 求解貪婪問題主要分為三步:問題分析、確定貪婪策略、正確性證明。其中,確定貪婪策略是核心步驟,正確性證明往往是難點。 +- 分數背包問題在 0-1 背包的基礎上,允許選擇物品的一部分,因此可使用貪婪演算法求解。貪婪策略的正確性可以使用反證法來證明。 +- 最大容量問題可使用窮舉法求解,時間複雜度為 $O(n^2)$ 。透過設計貪婪策略,每輪向內移動短板,可將時間複雜度最佳化至 $O(n)$ 。 +- 在最大切分乘積問題中,我們先後推理出兩個貪婪策略:$\geq 4$ 的整數都應該繼續切分,最優切分因子為 $3$ 。程式碼中包含冪運算,時間複雜度取決於冪運算實現方法,通常為 $O(1)$ 或 $O(\log n)$ 。 diff --git a/zh-Hant/docs/chapter_hashing/hash_algorithm.md b/zh-Hant/docs/chapter_hashing/hash_algorithm.md new file mode 100644 index 000000000..9a34a5350 --- /dev/null +++ b/zh-Hant/docs/chapter_hashing/hash_algorithm.md @@ -0,0 +1,972 @@ +--- +comments: true +--- + +# 6.3   雜湊演算法 + +前兩節介紹了雜湊表的工作原理和雜湊衝突的處理方法。然而無論是開放定址還是鏈式位址,**它們只能保證雜湊表可以在發生衝突時正常工作,而無法減少雜湊衝突的發生**。 + +如果雜湊衝突過於頻繁,雜湊表的效能則會急劇劣化。如圖 6-8 所示,對於鏈式位址雜湊表,理想情況下鍵值對均勻分佈在各個桶中,達到最佳查詢效率;最差情況下所有鍵值對都儲存到同一個桶中,時間複雜度退化至 $O(n)$ 。 + +![雜湊衝突的最佳情況與最差情況](hash_algorithm.assets/hash_collision_best_worst_condition.png){ class="animation-figure" } + +

圖 6-8   雜湊衝突的最佳情況與最差情況

+ +**鍵值對的分佈情況由雜湊函式決定**。回憶雜湊函式的計算步驟,先計算雜湊值,再對陣列長度取模: + +```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 "視覺化執行" + +
+ + +觀察發現,每種雜湊演算法的最後一步都是對大質數 $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 系列。 + +

表 6-2   常見的雜湊演算法

+ +
+ +| | 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 | + +
+ +## 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) + # 節點物件 的雜湊值為 274267521 + ``` + +=== "C++" + + ```cpp title="built_in_hash.cpp" + int num = 3; + size_t hashNum = hash()(num); + // 整數 3 的雜湊值為 3 + + bool bol = true; + size_t hashBol = hash()(bol); + // 布林量 1 的雜湊值為 1 + + double dec = 3.14159; + size_t hashDec = hash()(dec); + // 小數 3.14159 的雜湊值為 4614256650576692846 + + string str = "Hello 演算法"; + size_t hashStr = hash()(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(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 "視覺化執行" + +
+ + +在許多程式語言中,**只有不可變物件才可作為雜湊表的 `key`** 。假如我們將串列(動態陣列)作為 `key` ,當串列的內容發生變化時,它的雜湊值也隨之改變,我們就無法在雜湊表中查詢到原先的 `value` 了。 + +雖然自定義物件(比如鏈結串列節點)的成員變數是可變的,但它是可雜湊的。**這是因為物件的雜湊值通常是基於記憶體位址生成的**,即使物件的內容發生了變化,但它的記憶體位址不變,雜湊值仍然是不變的。 + +細心的你可能發現在不同控制檯中執行程式時,輸出的雜湊值是不同的。**這是因為 Python 直譯器在每次啟動時,都會為字串雜湊函式加入一個隨機的鹽(salt)值**。這種做法可以有效防止 HashDoS 攻擊,提升雜湊演算法的安全性。 diff --git a/zh-Hant/docs/chapter_hashing/hash_collision.md b/zh-Hant/docs/chapter_hashing/hash_collision.md new file mode 100644 index 000000000..b2a6d09d6 --- /dev/null +++ b/zh-Hant/docs/chapter_hashing/hash_collision.md @@ -0,0 +1,3126 @@ +--- +comments: true +--- + +# 6.2   雜湊衝突 + +上一節提到,**通常情況下雜湊函式的輸入空間遠大於輸出空間**,因此理論上雜湊衝突是不可避免的。比如,輸入空間為全體整數,輸出空間為陣列容量大小,則必然有多個整數對映至同一桶索引。 + +雜湊衝突會導致查詢結果錯誤,嚴重影響雜湊表的可用性。為了解決該問題,每當遇到雜湊衝突時,我們就進行雜湊表擴容,直至衝突消失為止。此方法簡單粗暴且有效,但效率太低,因為雜湊表擴容需要進行大量的資料搬運與雜湊值計算。為了提升效率,我們可以採用以下策略。 + +1. 改良雜湊表資料結構,**使得雜湊表可以在出現雜湊衝突時正常工作**。 +2. 僅在必要時,即當雜湊衝突比較嚴重時,才執行擴容操作。 + +雜湊表的結構改良方法主要包括“鏈式位址”和“開放定址”。 + +## 6.2.1   鏈式位址 + +在原始雜湊表中,每個桶僅能儲存一個鍵值對。鏈式位址(separate chaining)將單個元素轉換為鏈結串列,將鍵值對作為鏈結串列節點,將所有發生衝突的鍵值對都儲存在同一鏈結串列中。圖 6-5 展示了一個鏈式位址雜湊表的例子。 + +![鏈式位址雜湊表](hash_collision.assets/hash_table_chaining.png){ class="animation-figure" } + +

圖 6-5   鏈式位址雜湊表

+ +基於鏈式位址實現的雜湊表的操作方法發生了以下變化。 + +- **查詢元素**:輸入 `key` ,經過雜湊函式得到桶索引,即可訪問鏈結串列頭節點,然後走訪鏈結串列並對比 `key` 以查詢目標鍵值對。 +- **新增元素**:首先透過雜湊函式訪問鏈結串列頭節點,然後將節點(鍵值對)新增到鏈結串列中。 +- **刪除元素**:根據雜湊函式的結果訪問鏈結串列頭部,接著走訪鏈結串列以查詢目標節點並將其刪除。 + +鏈式位址存在以下侷限性。 + +- **佔用空間增大**:鏈結串列包含節點指標,它相比陣列更加耗費記憶體空間。 +- **查詢效率降低**:因為需要線性走訪鏈結串列來查詢對應元素。 + +以下程式碼給出了鏈式位址雜湊表的簡單實現,需要注意兩點。 + +- 使用串列(動態陣列)代替鏈結串列,從而簡化程式碼。在這種設定下,雜湊表(陣列)包含多個桶,每個桶都是一個串列。 +- 以下實現包含雜湊表擴容方法。當負載因子超過 $\frac{2}{3}$ 時,我們將雜湊表擴容至原先的 $2$ 倍。 + +=== "Python" + + ```python title="hash_map_chaining.py" + class HashMapChaining: + """鏈式位址雜湊表""" + + def __init__(self): + """建構子""" + self.size = 0 # 鍵值對數量 + self.capacity = 4 # 雜湊表容量 + self.load_thres = 2.0 / 3.0 # 觸發擴容的負載因子閾值 + self.extend_ratio = 2 # 擴容倍數 + self.buckets = [[] for _ in range(self.capacity)] # 桶陣列 + + def hash_func(self, key: int) -> int: + """雜湊函式""" + return key % self.capacity + + def load_factor(self) -> float: + """負載因子""" + return self.size / self.capacity + + def get(self, key: int) -> str | None: + """查詢操作""" + index = self.hash_func(key) + bucket = self.buckets[index] + # 走訪桶,若找到 key ,則返回對應 val + for pair in bucket: + if pair.key == key: + return pair.val + # 若未找到 key ,則返回 None + return None + + def put(self, key: int, val: str): + """新增操作""" + # 當負載因子超過閾值時,執行擴容 + if self.load_factor() > self.load_thres: + self.extend() + index = self.hash_func(key) + bucket = self.buckets[index] + # 走訪桶,若遇到指定 key ,則更新對應 val 並返回 + for pair in bucket: + if pair.key == key: + pair.val = val + return + # 若無該 key ,則將鍵值對新增至尾部 + pair = Pair(key, val) + bucket.append(pair) + self.size += 1 + + def remove(self, key: int): + """刪除操作""" + index = self.hash_func(key) + bucket = self.buckets[index] + # 走訪桶,從中刪除鍵值對 + for pair in bucket: + if pair.key == key: + bucket.remove(pair) + self.size -= 1 + break + + def extend(self): + """擴容雜湊表""" + # 暫存原雜湊表 + buckets = self.buckets + # 初始化擴容後的新雜湊表 + self.capacity *= self.extend_ratio + self.buckets = [[] for _ in range(self.capacity)] + self.size = 0 + # 將鍵值對從原雜湊表搬運至新雜湊表 + for bucket in buckets: + for pair in bucket: + self.put(pair.key, pair.val) + + def print(self): + """列印雜湊表""" + for bucket in self.buckets: + res = [] + for pair in bucket: + res.append(str(pair.key) + " -> " + pair.val) + print(res) + ``` + +=== "C++" + + ```cpp title="hash_map_chaining.cpp" + /* 鏈式位址雜湊表 */ + class HashMapChaining { + private: + int size; // 鍵值對數量 + int capacity; // 雜湊表容量 + double loadThres; // 觸發擴容的負載因子閾值 + int extendRatio; // 擴容倍數 + vector> buckets; // 桶陣列 + + public: + /* 建構子 */ + HashMapChaining() : size(0), capacity(4), loadThres(2.0 / 3.0), extendRatio(2) { + buckets.resize(capacity); + } + + /* 析構方法 */ + ~HashMapChaining() { + for (auto &bucket : buckets) { + for (Pair *pair : bucket) { + // 釋放記憶體 + delete pair; + } + } + } + + /* 雜湊函式 */ + int hashFunc(int key) { + return key % capacity; + } + + /* 負載因子 */ + double loadFactor() { + return (double)size / (double)capacity; + } + + /* 查詢操作 */ + string get(int key) { + int index = hashFunc(key); + // 走訪桶,若找到 key ,則返回對應 val + for (Pair *pair : buckets[index]) { + if (pair->key == key) { + return pair->val; + } + } + // 若未找到 key ,則返回空字串 + return ""; + } + + /* 新增操作 */ + void put(int key, string val) { + // 當負載因子超過閾值時,執行擴容 + if (loadFactor() > loadThres) { + extend(); + } + int index = hashFunc(key); + // 走訪桶,若遇到指定 key ,則更新對應 val 並返回 + for (Pair *pair : buckets[index]) { + if (pair->key == key) { + pair->val = val; + return; + } + } + // 若無該 key ,則將鍵值對新增至尾部 + buckets[index].push_back(new Pair(key, val)); + size++; + } + + /* 刪除操作 */ + void remove(int key) { + int index = hashFunc(key); + auto &bucket = buckets[index]; + // 走訪桶,從中刪除鍵值對 + for (int i = 0; i < bucket.size(); i++) { + if (bucket[i]->key == key) { + Pair *tmp = bucket[i]; + bucket.erase(bucket.begin() + i); // 從中刪除鍵值對 + delete tmp; // 釋放記憶體 + size--; + return; + } + } + } + + /* 擴容雜湊表 */ + void extend() { + // 暫存原雜湊表 + vector> bucketsTmp = buckets; + // 初始化擴容後的新雜湊表 + capacity *= extendRatio; + buckets.clear(); + buckets.resize(capacity); + size = 0; + // 將鍵值對從原雜湊表搬運至新雜湊表 + for (auto &bucket : bucketsTmp) { + for (Pair *pair : bucket) { + put(pair->key, pair->val); + // 釋放記憶體 + delete pair; + } + } + } + + /* 列印雜湊表 */ + void print() { + for (auto &bucket : buckets) { + cout << "["; + for (Pair *pair : bucket) { + cout << pair->key << " -> " << pair->val << ", "; + } + cout << "]\n"; + } + } + }; + ``` + +=== "Java" + + ```java title="hash_map_chaining.java" + /* 鏈式位址雜湊表 */ + class HashMapChaining { + int size; // 鍵值對數量 + int capacity; // 雜湊表容量 + double loadThres; // 觸發擴容的負載因子閾值 + int extendRatio; // 擴容倍數 + List> buckets; // 桶陣列 + + /* 建構子 */ + public HashMapChaining() { + size = 0; + capacity = 4; + loadThres = 2.0 / 3.0; + extendRatio = 2; + buckets = new ArrayList<>(capacity); + for (int i = 0; i < capacity; i++) { + buckets.add(new ArrayList<>()); + } + } + + /* 雜湊函式 */ + int hashFunc(int key) { + return key % capacity; + } + + /* 負載因子 */ + double loadFactor() { + return (double) size / capacity; + } + + /* 查詢操作 */ + String get(int key) { + int index = hashFunc(key); + List bucket = buckets.get(index); + // 走訪桶,若找到 key ,則返回對應 val + for (Pair pair : bucket) { + if (pair.key == key) { + return pair.val; + } + } + // 若未找到 key ,則返回 null + return null; + } + + /* 新增操作 */ + void put(int key, String val) { + // 當負載因子超過閾值時,執行擴容 + if (loadFactor() > loadThres) { + extend(); + } + int index = hashFunc(key); + List bucket = buckets.get(index); + // 走訪桶,若遇到指定 key ,則更新對應 val 並返回 + for (Pair pair : bucket) { + if (pair.key == key) { + pair.val = val; + return; + } + } + // 若無該 key ,則將鍵值對新增至尾部 + Pair pair = new Pair(key, val); + bucket.add(pair); + size++; + } + + /* 刪除操作 */ + void remove(int key) { + int index = hashFunc(key); + List bucket = buckets.get(index); + // 走訪桶,從中刪除鍵值對 + for (Pair pair : bucket) { + if (pair.key == key) { + bucket.remove(pair); + size--; + break; + } + } + } + + /* 擴容雜湊表 */ + void extend() { + // 暫存原雜湊表 + List> bucketsTmp = buckets; + // 初始化擴容後的新雜湊表 + capacity *= extendRatio; + buckets = new ArrayList<>(capacity); + for (int i = 0; i < capacity; i++) { + buckets.add(new ArrayList<>()); + } + size = 0; + // 將鍵值對從原雜湊表搬運至新雜湊表 + for (List bucket : bucketsTmp) { + for (Pair pair : bucket) { + put(pair.key, pair.val); + } + } + } + + /* 列印雜湊表 */ + void print() { + for (List bucket : buckets) { + List res = new ArrayList<>(); + for (Pair pair : bucket) { + res.add(pair.key + " -> " + pair.val); + } + System.out.println(res); + } + } + } + ``` + +=== "C#" + + ```csharp title="hash_map_chaining.cs" + /* 鏈式位址雜湊表 */ + class HashMapChaining { + int size; // 鍵值對數量 + int capacity; // 雜湊表容量 + double loadThres; // 觸發擴容的負載因子閾值 + int extendRatio; // 擴容倍數 + List> buckets; // 桶陣列 + + /* 建構子 */ + public HashMapChaining() { + size = 0; + capacity = 4; + loadThres = 2.0 / 3.0; + extendRatio = 2; + buckets = new List>(capacity); + for (int i = 0; i < capacity; i++) { + buckets.Add([]); + } + } + + /* 雜湊函式 */ + int HashFunc(int key) { + return key % capacity; + } + + /* 負載因子 */ + double LoadFactor() { + return (double)size / capacity; + } + + /* 查詢操作 */ + public string? Get(int key) { + int index = HashFunc(key); + // 走訪桶,若找到 key ,則返回對應 val + foreach (Pair pair in buckets[index]) { + if (pair.key == key) { + return pair.val; + } + } + // 若未找到 key ,則返回 null + return null; + } + + /* 新增操作 */ + public void Put(int key, string val) { + // 當負載因子超過閾值時,執行擴容 + if (LoadFactor() > loadThres) { + Extend(); + } + int index = HashFunc(key); + // 走訪桶,若遇到指定 key ,則更新對應 val 並返回 + foreach (Pair pair in buckets[index]) { + if (pair.key == key) { + pair.val = val; + return; + } + } + // 若無該 key ,則將鍵值對新增至尾部 + buckets[index].Add(new Pair(key, val)); + size++; + } + + /* 刪除操作 */ + public void Remove(int key) { + int index = HashFunc(key); + // 走訪桶,從中刪除鍵值對 + foreach (Pair pair in buckets[index].ToList()) { + if (pair.key == key) { + buckets[index].Remove(pair); + size--; + break; + } + } + } + + /* 擴容雜湊表 */ + void Extend() { + // 暫存原雜湊表 + List> bucketsTmp = buckets; + // 初始化擴容後的新雜湊表 + capacity *= extendRatio; + buckets = new List>(capacity); + for (int i = 0; i < capacity; i++) { + buckets.Add([]); + } + size = 0; + // 將鍵值對從原雜湊表搬運至新雜湊表 + foreach (List bucket in bucketsTmp) { + foreach (Pair pair in bucket) { + Put(pair.key, pair.val); + } + } + } + + /* 列印雜湊表 */ + public void Print() { + foreach (List bucket in buckets) { + List res = []; + foreach (Pair pair in bucket) { + res.Add(pair.key + " -> " + pair.val); + } + foreach (string kv in res) { + Console.WriteLine(kv); + } + } + } + } + ``` + +=== "Go" + + ```go title="hash_map_chaining.go" + /* 鏈式位址雜湊表 */ + type hashMapChaining struct { + size int // 鍵值對數量 + capacity int // 雜湊表容量 + loadThres float64 // 觸發擴容的負載因子閾值 + extendRatio int // 擴容倍數 + buckets [][]pair // 桶陣列 + } + + /* 建構子 */ + func newHashMapChaining() *hashMapChaining { + buckets := make([][]pair, 4) + for i := 0; i < 4; i++ { + buckets[i] = make([]pair, 0) + } + return &hashMapChaining{ + size: 0, + capacity: 4, + loadThres: 2.0 / 3.0, + extendRatio: 2, + buckets: buckets, + } + } + + /* 雜湊函式 */ + func (m *hashMapChaining) hashFunc(key int) int { + return key % m.capacity + } + + /* 負載因子 */ + func (m *hashMapChaining) loadFactor() float64 { + return float64(m.size) / float64(m.capacity) + } + + /* 查詢操作 */ + func (m *hashMapChaining) get(key int) string { + idx := m.hashFunc(key) + bucket := m.buckets[idx] + // 走訪桶,若找到 key ,則返回對應 val + for _, p := range bucket { + if p.key == key { + return p.val + } + } + // 若未找到 key ,則返回空字串 + return "" + } + + /* 新增操作 */ + func (m *hashMapChaining) put(key int, val string) { + // 當負載因子超過閾值時,執行擴容 + if m.loadFactor() > m.loadThres { + m.extend() + } + idx := m.hashFunc(key) + // 走訪桶,若遇到指定 key ,則更新對應 val 並返回 + for i := range m.buckets[idx] { + if m.buckets[idx][i].key == key { + m.buckets[idx][i].val = val + return + } + } + // 若無該 key ,則將鍵值對新增至尾部 + p := pair{ + key: key, + val: val, + } + m.buckets[idx] = append(m.buckets[idx], p) + m.size += 1 + } + + /* 刪除操作 */ + func (m *hashMapChaining) remove(key int) { + idx := m.hashFunc(key) + // 走訪桶,從中刪除鍵值對 + for i, p := range m.buckets[idx] { + if p.key == key { + // 切片刪除 + m.buckets[idx] = append(m.buckets[idx][:i], m.buckets[idx][i+1:]...) + m.size -= 1 + break + } + } + } + + /* 擴容雜湊表 */ + func (m *hashMapChaining) extend() { + // 暫存原雜湊表 + tmpBuckets := make([][]pair, len(m.buckets)) + for i := 0; i < len(m.buckets); i++ { + tmpBuckets[i] = make([]pair, len(m.buckets[i])) + copy(tmpBuckets[i], m.buckets[i]) + } + // 初始化擴容後的新雜湊表 + m.capacity *= m.extendRatio + m.buckets = make([][]pair, m.capacity) + for i := 0; i < m.capacity; i++ { + m.buckets[i] = make([]pair, 0) + } + m.size = 0 + // 將鍵值對從原雜湊表搬運至新雜湊表 + for _, bucket := range tmpBuckets { + for _, p := range bucket { + m.put(p.key, p.val) + } + } + } + + /* 列印雜湊表 */ + func (m *hashMapChaining) print() { + var builder strings.Builder + + for _, bucket := range m.buckets { + builder.WriteString("[") + for _, p := range bucket { + builder.WriteString(strconv.Itoa(p.key) + " -> " + p.val + " ") + } + builder.WriteString("]") + fmt.Println(builder.String()) + builder.Reset() + } + } + ``` + +=== "Swift" + + ```swift title="hash_map_chaining.swift" + /* 鏈式位址雜湊表 */ + class HashMapChaining { + var size: Int // 鍵值對數量 + var capacity: Int // 雜湊表容量 + var loadThres: Double // 觸發擴容的負載因子閾值 + var extendRatio: Int // 擴容倍數 + var buckets: [[Pair]] // 桶陣列 + + /* 建構子 */ + init() { + size = 0 + capacity = 4 + loadThres = 2.0 / 3.0 + extendRatio = 2 + buckets = Array(repeating: [], count: capacity) + } + + /* 雜湊函式 */ + func hashFunc(key: Int) -> Int { + key % capacity + } + + /* 負載因子 */ + func loadFactor() -> Double { + Double(size) / Double(capacity) + } + + /* 查詢操作 */ + func get(key: Int) -> String? { + let index = hashFunc(key: key) + let bucket = buckets[index] + // 走訪桶,若找到 key ,則返回對應 val + for pair in bucket { + if pair.key == key { + return pair.val + } + } + // 若未找到 key ,則返回 nil + return nil + } + + /* 新增操作 */ + func put(key: Int, val: String) { + // 當負載因子超過閾值時,執行擴容 + if loadFactor() > loadThres { + extend() + } + let index = hashFunc(key: key) + let bucket = buckets[index] + // 走訪桶,若遇到指定 key ,則更新對應 val 並返回 + for pair in bucket { + if pair.key == key { + pair.val = val + return + } + } + // 若無該 key ,則將鍵值對新增至尾部 + let pair = Pair(key: key, val: val) + buckets[index].append(pair) + size += 1 + } + + /* 刪除操作 */ + func remove(key: Int) { + let index = hashFunc(key: key) + let bucket = buckets[index] + // 走訪桶,從中刪除鍵值對 + for (pairIndex, pair) in bucket.enumerated() { + if pair.key == key { + buckets[index].remove(at: pairIndex) + size -= 1 + break + } + } + } + + /* 擴容雜湊表 */ + func extend() { + // 暫存原雜湊表 + let bucketsTmp = buckets + // 初始化擴容後的新雜湊表 + capacity *= extendRatio + buckets = Array(repeating: [], count: capacity) + size = 0 + // 將鍵值對從原雜湊表搬運至新雜湊表 + for bucket in bucketsTmp { + for pair in bucket { + put(key: pair.key, val: pair.val) + } + } + } + + /* 列印雜湊表 */ + func print() { + for bucket in buckets { + let res = bucket.map { "\($0.key) -> \($0.val)" } + Swift.print(res) + } + } + } + ``` + +=== "JS" + + ```javascript title="hash_map_chaining.js" + /* 鏈式位址雜湊表 */ + class HashMapChaining { + #size; // 鍵值對數量 + #capacity; // 雜湊表容量 + #loadThres; // 觸發擴容的負載因子閾值 + #extendRatio; // 擴容倍數 + #buckets; // 桶陣列 + + /* 建構子 */ + constructor() { + this.#size = 0; + this.#capacity = 4; + this.#loadThres = 2.0 / 3.0; + this.#extendRatio = 2; + this.#buckets = new Array(this.#capacity).fill(null).map((x) => []); + } + + /* 雜湊函式 */ + #hashFunc(key) { + return key % this.#capacity; + } + + /* 負載因子 */ + #loadFactor() { + return this.#size / this.#capacity; + } + + /* 查詢操作 */ + get(key) { + const index = this.#hashFunc(key); + const bucket = this.#buckets[index]; + // 走訪桶,若找到 key ,則返回對應 val + for (const pair of bucket) { + if (pair.key === key) { + return pair.val; + } + } + // 若未找到 key ,則返回 null + return null; + } + + /* 新增操作 */ + put(key, val) { + // 當負載因子超過閾值時,執行擴容 + if (this.#loadFactor() > this.#loadThres) { + this.#extend(); + } + const index = this.#hashFunc(key); + const bucket = this.#buckets[index]; + // 走訪桶,若遇到指定 key ,則更新對應 val 並返回 + for (const pair of bucket) { + if (pair.key === key) { + pair.val = val; + return; + } + } + // 若無該 key ,則將鍵值對新增至尾部 + const pair = new Pair(key, val); + bucket.push(pair); + this.#size++; + } + + /* 刪除操作 */ + remove(key) { + const index = this.#hashFunc(key); + let bucket = this.#buckets[index]; + // 走訪桶,從中刪除鍵值對 + for (let i = 0; i < bucket.length; i++) { + if (bucket[i].key === key) { + bucket.splice(i, 1); + this.#size--; + break; + } + } + } + + /* 擴容雜湊表 */ + #extend() { + // 暫存原雜湊表 + const bucketsTmp = this.#buckets; + // 初始化擴容後的新雜湊表 + this.#capacity *= this.#extendRatio; + this.#buckets = new Array(this.#capacity).fill(null).map((x) => []); + this.#size = 0; + // 將鍵值對從原雜湊表搬運至新雜湊表 + for (const bucket of bucketsTmp) { + for (const pair of bucket) { + this.put(pair.key, pair.val); + } + } + } + + /* 列印雜湊表 */ + print() { + for (const bucket of this.#buckets) { + let res = []; + for (const pair of bucket) { + res.push(pair.key + ' -> ' + pair.val); + } + console.log(res); + } + } + } + ``` + +=== "TS" + + ```typescript title="hash_map_chaining.ts" + /* 鏈式位址雜湊表 */ + class HashMapChaining { + #size: number; // 鍵值對數量 + #capacity: number; // 雜湊表容量 + #loadThres: number; // 觸發擴容的負載因子閾值 + #extendRatio: number; // 擴容倍數 + #buckets: Pair[][]; // 桶陣列 + + /* 建構子 */ + constructor() { + this.#size = 0; + this.#capacity = 4; + this.#loadThres = 2.0 / 3.0; + this.#extendRatio = 2; + this.#buckets = new Array(this.#capacity).fill(null).map((x) => []); + } + + /* 雜湊函式 */ + #hashFunc(key: number): number { + return key % this.#capacity; + } + + /* 負載因子 */ + #loadFactor(): number { + return this.#size / this.#capacity; + } + + /* 查詢操作 */ + get(key: number): string | null { + const index = this.#hashFunc(key); + const bucket = this.#buckets[index]; + // 走訪桶,若找到 key ,則返回對應 val + for (const pair of bucket) { + if (pair.key === key) { + return pair.val; + } + } + // 若未找到 key ,則返回 null + return null; + } + + /* 新增操作 */ + put(key: number, val: string): void { + // 當負載因子超過閾值時,執行擴容 + if (this.#loadFactor() > this.#loadThres) { + this.#extend(); + } + const index = this.#hashFunc(key); + const bucket = this.#buckets[index]; + // 走訪桶,若遇到指定 key ,則更新對應 val 並返回 + for (const pair of bucket) { + if (pair.key === key) { + pair.val = val; + return; + } + } + // 若無該 key ,則將鍵值對新增至尾部 + const pair = new Pair(key, val); + bucket.push(pair); + this.#size++; + } + + /* 刪除操作 */ + remove(key: number): void { + const index = this.#hashFunc(key); + let bucket = this.#buckets[index]; + // 走訪桶,從中刪除鍵值對 + for (let i = 0; i < bucket.length; i++) { + if (bucket[i].key === key) { + bucket.splice(i, 1); + this.#size--; + break; + } + } + } + + /* 擴容雜湊表 */ + #extend(): void { + // 暫存原雜湊表 + const bucketsTmp = this.#buckets; + // 初始化擴容後的新雜湊表 + this.#capacity *= this.#extendRatio; + this.#buckets = new Array(this.#capacity).fill(null).map((x) => []); + this.#size = 0; + // 將鍵值對從原雜湊表搬運至新雜湊表 + for (const bucket of bucketsTmp) { + for (const pair of bucket) { + this.put(pair.key, pair.val); + } + } + } + + /* 列印雜湊表 */ + print(): void { + for (const bucket of this.#buckets) { + let res = []; + for (const pair of bucket) { + res.push(pair.key + ' -> ' + pair.val); + } + console.log(res); + } + } + } + ``` + +=== "Dart" + + ```dart title="hash_map_chaining.dart" + /* 鏈式位址雜湊表 */ + class HashMapChaining { + late int size; // 鍵值對數量 + late int capacity; // 雜湊表容量 + late double loadThres; // 觸發擴容的負載因子閾值 + late int extendRatio; // 擴容倍數 + late List> buckets; // 桶陣列 + + /* 建構子 */ + HashMapChaining() { + size = 0; + capacity = 4; + loadThres = 2.0 / 3.0; + extendRatio = 2; + buckets = List.generate(capacity, (_) => []); + } + + /* 雜湊函式 */ + int hashFunc(int key) { + return key % capacity; + } + + /* 負載因子 */ + double loadFactor() { + return size / capacity; + } + + /* 查詢操作 */ + String? get(int key) { + int index = hashFunc(key); + List bucket = buckets[index]; + // 走訪桶,若找到 key ,則返回對應 val + for (Pair pair in bucket) { + if (pair.key == key) { + return pair.val; + } + } + // 若未找到 key ,則返回 null + return null; + } + + /* 新增操作 */ + void put(int key, String val) { + // 當負載因子超過閾值時,執行擴容 + if (loadFactor() > loadThres) { + extend(); + } + int index = hashFunc(key); + List bucket = buckets[index]; + // 走訪桶,若遇到指定 key ,則更新對應 val 並返回 + for (Pair pair in bucket) { + if (pair.key == key) { + pair.val = val; + return; + } + } + // 若無該 key ,則將鍵值對新增至尾部 + Pair pair = Pair(key, val); + bucket.add(pair); + size++; + } + + /* 刪除操作 */ + void remove(int key) { + int index = hashFunc(key); + List bucket = buckets[index]; + // 走訪桶,從中刪除鍵值對 + for (Pair pair in bucket) { + if (pair.key == key) { + bucket.remove(pair); + size--; + break; + } + } + } + + /* 擴容雜湊表 */ + void extend() { + // 暫存原雜湊表 + List> bucketsTmp = buckets; + // 初始化擴容後的新雜湊表 + capacity *= extendRatio; + buckets = List.generate(capacity, (_) => []); + size = 0; + // 將鍵值對從原雜湊表搬運至新雜湊表 + for (List bucket in bucketsTmp) { + for (Pair pair in bucket) { + put(pair.key, pair.val); + } + } + } + + /* 列印雜湊表 */ + void printHashMap() { + for (List bucket in buckets) { + List res = []; + for (Pair pair in bucket) { + res.add("${pair.key} -> ${pair.val}"); + } + print(res); + } + } + } + ``` + +=== "Rust" + + ```rust title="hash_map_chaining.rs" + /* 鏈式位址雜湊表 */ + struct HashMapChaining { + size: i32, + capacity: i32, + load_thres: f32, + extend_ratio: i32, + buckets: Vec>, + } + + impl HashMapChaining { + /* 建構子 */ + fn new() -> Self { + Self { + size: 0, + capacity: 4, + load_thres: 2.0 / 3.0, + extend_ratio: 2, + buckets: vec![vec![]; 4], + } + } + + /* 雜湊函式 */ + fn hash_func(&self, key: i32) -> usize { + key as usize % self.capacity as usize + } + + /* 負載因子 */ + fn load_factor(&self) -> f32 { + self.size as f32 / self.capacity as f32 + } + + /* 刪除操作 */ + fn remove(&mut self, key: i32) -> Option { + let index = self.hash_func(key); + let bucket = &mut self.buckets[index]; + + // 走訪桶,從中刪除鍵值對 + for i in 0..bucket.len() { + if bucket[i].key == key { + let pair = bucket.remove(i); + self.size -= 1; + return Some(pair.val); + } + } + + // 若未找到 key ,則返回 None + None + } + + /* 擴容雜湊表 */ + fn extend(&mut self) { + // 暫存原雜湊表 + let buckets_tmp = std::mem::replace(&mut self.buckets, vec![]); + + // 初始化擴容後的新雜湊表 + self.capacity *= self.extend_ratio; + self.buckets = vec![Vec::new(); self.capacity as usize]; + self.size = 0; + + // 將鍵值對從原雜湊表搬運至新雜湊表 + for bucket in buckets_tmp { + for pair in bucket { + self.put(pair.key, pair.val); + } + } + } + + /* 列印雜湊表 */ + fn print(&self) { + for bucket in &self.buckets { + let mut res = Vec::new(); + for pair in bucket { + res.push(format!("{} -> {}", pair.key, pair.val)); + } + println!("{:?}", res); + } + } + + /* 新增操作 */ + fn put(&mut self, key: i32, val: String) { + // 當負載因子超過閾值時,執行擴容 + if self.load_factor() > self.load_thres { + self.extend(); + } + + let index = self.hash_func(key); + let bucket = &mut self.buckets[index]; + + // 走訪桶,若遇到指定 key ,則更新對應 val 並返回 + for pair in bucket { + if pair.key == key { + pair.val = val.clone(); + return; + } + } + let bucket = &mut self.buckets[index]; + + // 若無該 key ,則將鍵值對新增至尾部 + let pair = Pair { + key, + val: val.clone(), + }; + bucket.push(pair); + self.size += 1; + } + + /* 查詢操作 */ + fn get(&self, key: i32) -> Option<&str> { + let index = self.hash_func(key); + let bucket = &self.buckets[index]; + + // 走訪桶,若找到 key ,則返回對應 val + for pair in bucket { + if pair.key == key { + return Some(&pair.val); + } + } + + // 若未找到 key ,則返回 None + None + } + } + ``` + +=== "C" + + ```c title="hash_map_chaining.c" + /* 鏈結串列節點 */ + typedef struct Node { + Pair *pair; + struct Node *next; + } Node; + + /* 鏈式位址雜湊表 */ + typedef struct { + int size; // 鍵值對數量 + int capacity; // 雜湊表容量 + double loadThres; // 觸發擴容的負載因子閾值 + int extendRatio; // 擴容倍數 + Node **buckets; // 桶陣列 + } HashMapChaining; + + /* 建構子 */ + HashMapChaining *newHashMapChaining() { + HashMapChaining *hashMap = (HashMapChaining *)malloc(sizeof(HashMapChaining)); + hashMap->size = 0; + hashMap->capacity = 4; + hashMap->loadThres = 2.0 / 3.0; + hashMap->extendRatio = 2; + hashMap->buckets = (Node **)malloc(hashMap->capacity * sizeof(Node *)); + for (int i = 0; i < hashMap->capacity; i++) { + hashMap->buckets[i] = NULL; + } + return hashMap; + } + + /* 析構函式 */ + void delHashMapChaining(HashMapChaining *hashMap) { + for (int i = 0; i < hashMap->capacity; i++) { + Node *cur = hashMap->buckets[i]; + while (cur) { + Node *tmp = cur; + cur = cur->next; + free(tmp->pair); + free(tmp); + } + } + free(hashMap->buckets); + free(hashMap); + } + + /* 雜湊函式 */ + int hashFunc(HashMapChaining *hashMap, int key) { + return key % hashMap->capacity; + } + + /* 負載因子 */ + double loadFactor(HashMapChaining *hashMap) { + return (double)hashMap->size / (double)hashMap->capacity; + } + + /* 查詢操作 */ + char *get(HashMapChaining *hashMap, int key) { + int index = hashFunc(hashMap, key); + // 走訪桶,若找到 key ,則返回對應 val + Node *cur = hashMap->buckets[index]; + while (cur) { + if (cur->pair->key == key) { + return cur->pair->val; + } + cur = cur->next; + } + return ""; // 若未找到 key ,則返回空字串 + } + + /* 新增操作 */ + void put(HashMapChaining *hashMap, int key, const char *val) { + // 當負載因子超過閾值時,執行擴容 + if (loadFactor(hashMap) > hashMap->loadThres) { + extend(hashMap); + } + int index = hashFunc(hashMap, key); + // 走訪桶,若遇到指定 key ,則更新對應 val 並返回 + Node *cur = hashMap->buckets[index]; + while (cur) { + if (cur->pair->key == key) { + strcpy(cur->pair->val, val); // 若遇到指定 key ,則更新對應 val 並返回 + return; + } + cur = cur->next; + } + // 若無該 key ,則將鍵值對新增至鏈結串列頭部 + Pair *newPair = (Pair *)malloc(sizeof(Pair)); + newPair->key = key; + strcpy(newPair->val, val); + Node *newNode = (Node *)malloc(sizeof(Node)); + newNode->pair = newPair; + newNode->next = hashMap->buckets[index]; + hashMap->buckets[index] = newNode; + hashMap->size++; + } + + /* 擴容雜湊表 */ + void extend(HashMapChaining *hashMap) { + // 暫存原雜湊表 + int oldCapacity = hashMap->capacity; + Node **oldBuckets = hashMap->buckets; + // 初始化擴容後的新雜湊表 + hashMap->capacity *= hashMap->extendRatio; + hashMap->buckets = (Node **)malloc(hashMap->capacity * sizeof(Node *)); + for (int i = 0; i < hashMap->capacity; i++) { + hashMap->buckets[i] = NULL; + } + hashMap->size = 0; + // 將鍵值對從原雜湊表搬運至新雜湊表 + for (int i = 0; i < oldCapacity; i++) { + Node *cur = oldBuckets[i]; + while (cur) { + put(hashMap, cur->pair->key, cur->pair->val); + Node *temp = cur; + cur = cur->next; + // 釋放記憶體 + free(temp->pair); + free(temp); + } + } + + free(oldBuckets); + } + + /* 刪除操作 */ + void removeItem(HashMapChaining *hashMap, int key) { + int index = hashFunc(hashMap, key); + Node *cur = hashMap->buckets[index]; + Node *pre = NULL; + while (cur) { + if (cur->pair->key == key) { + // 從中刪除鍵值對 + if (pre) { + pre->next = cur->next; + } else { + hashMap->buckets[index] = cur->next; + } + // 釋放記憶體 + free(cur->pair); + free(cur); + hashMap->size--; + return; + } + pre = cur; + cur = cur->next; + } + } + + /* 列印雜湊表 */ + void print(HashMapChaining *hashMap) { + for (int i = 0; i < hashMap->capacity; i++) { + Node *cur = hashMap->buckets[i]; + printf("["); + while (cur) { + printf("%d -> %s, ", cur->pair->key, cur->pair->val); + cur = cur->next; + } + printf("]\n"); + } + } + ``` + +=== "Kotlin" + + ```kotlin title="hash_map_chaining.kt" + /* 鏈式位址雜湊表 */ + class HashMapChaining() { + var size: Int // 鍵值對數量 + var capacity: Int // 雜湊表容量 + val loadThres: Double // 觸發擴容的負載因子閾值 + val extendRatio: Int // 擴容倍數 + var buckets: MutableList> // 桶陣列 + + /* 建構子 */ + init { + size = 0 + capacity = 4 + loadThres = 2.0 / 3.0 + extendRatio = 2 + buckets = ArrayList(capacity) + for (i in 0.. loadThres) { + extend() + } + val index = hashFunc(key) + val bucket = buckets[index] + // 走訪桶,若遇到指定 key ,則更新對應 val 並返回 + for (pair in bucket) { + if (pair.key == key) { + pair.value = value + return + } + } + // 若無該 key ,則將鍵值對新增至尾部 + val pair = Pair(key, value) + bucket.add(pair) + size++ + } + + /* 刪除操作 */ + fun remove(key: Int) { + val index = hashFunc(key) + val bucket = buckets[index] + // 走訪桶,從中刪除鍵值對 + for (pair in bucket) { + if (pair.key == key) { + bucket.remove(pair) + size-- + break + } + } + } + + /* 擴容雜湊表 */ + fun extend() { + // 暫存原雜湊表 + val bucketsTmp = buckets + // 初始化擴容後的新雜湊表 + capacity *= extendRatio + // mutablelist 無固定大小 + buckets = mutableListOf() + for (i in 0..() + for (pair in bucket) { + val k = pair.key + val v = pair.value + res.add("$k -> $v") + } + println(res) + } + } + } + ``` + +=== "Ruby" + + ```ruby title="hash_map_chaining.rb" + [class]{HashMapChaining}-[func]{} + ``` + +=== "Zig" + + ```zig title="hash_map_chaining.zig" + [class]{HashMapChaining}-[func]{} + ``` + +??? pythontutor "視覺化執行" + +
+ + +值得注意的是,當鏈結串列很長時,查詢效率 $O(n)$ 很差。**此時可以將鏈結串列轉換為“AVL 樹”或“紅黑樹”**,從而將查詢操作的時間複雜度最佳化至 $O(\log n)$ 。 + +## 6.2.2   開放定址 + +開放定址(open addressing)不引入額外的資料結構,而是透過“多次探測”來處理雜湊衝突,探測方式主要包括線性探查、平方探測和多次雜湊等。 + +下面以線性探查為例,介紹開放定址雜湊表的工作機制。 + +### 1.   線性探查 + +線性探查採用固定步長的線性搜尋來進行探測,其操作方法與普通雜湊表有所不同。 + +- **插入元素**:透過雜湊函式計算桶索引,若發現桶內已有元素,則從衝突位置向後線性走訪(步長通常為 $1$ ),直至找到空桶,將元素插入其中。 +- **查詢元素**:若發現雜湊衝突,則使用相同步長向後進行線性走訪,直到找到對應元素,返回 `value` 即可;如果遇到空桶,說明目標元素不在雜湊表中,返回 `None` 。 + +圖 6-6 展示了開放定址(線性探查)雜湊表的鍵值對分佈。根據此雜湊函式,最後兩位相同的 `key` 都會被對映到相同的桶。而透過線性探查,它們被依次儲存在該桶以及之下的桶中。 + +![開放定址(線性探查)雜湊表的鍵值對分佈](hash_collision.assets/hash_table_linear_probing.png){ class="animation-figure" } + +

圖 6-6   開放定址(線性探查)雜湊表的鍵值對分佈

+ +然而,**線性探查容易產生“聚集現象”**。具體來說,陣列中連續被佔用的位置越長,這些連續位置發生雜湊衝突的可能性越大,從而進一步促使該位置的聚堆積生長,形成惡性迴圈,最終導致增刪查改操作效率劣化。 + +值得注意的是,**我們不能在開放定址雜湊表中直接刪除元素**。這是因為刪除元素會在陣列內產生一個空桶 `None` ,而當查詢元素時,線性探查到該空桶就會返回,因此在該空桶之下的元素都無法再被訪問到,程式可能誤判這些元素不存在,如圖 6-7 所示。 + +![在開放定址中刪除元素導致的查詢問題](hash_collision.assets/hash_table_open_addressing_deletion.png){ class="animation-figure" } + +

圖 6-7   在開放定址中刪除元素導致的查詢問題

+ +為了解決該問題,我們可以採用懶刪除(lazy deletion)機制:它不直接從雜湊表中移除元素,**而是利用一個常數 `TOMBSTONE` 來標記這個桶**。在該機制下,`None` 和 `TOMBSTONE` 都代表空桶,都可以放置鍵值對。但不同的是,線性探查到 `TOMBSTONE` 時應該繼續走訪,因為其之下可能還存在鍵值對。 + +然而,**懶刪除可能會加速雜湊表的效能退化**。這是因為每次刪除操作都會產生一個刪除標記,隨著 `TOMBSTONE` 的增加,搜尋時間也會增加,因為線性探查可能需要跳過多個 `TOMBSTONE` 才能找到目標元素。 + +為此,考慮線上性探查中記錄遇到的首個 `TOMBSTONE` 的索引,並將搜尋到的目標元素與該 `TOMBSTONE` 交換位置。這樣做的好處是當每次查詢或新增元素時,元素會被移動至距離理想位置(探測起始點)更近的桶,從而最佳化查詢效率。 + +以下程式碼實現了一個包含懶刪除的開放定址(線性探查)雜湊表。為了更加充分地使用雜湊表的空間,我們將雜湊表看作一個“環形陣列”,當越過陣列尾部時,回到頭部繼續走訪。 + +=== "Python" + + ```python title="hash_map_open_addressing.py" + class HashMapOpenAddressing: + """開放定址雜湊表""" + + def __init__(self): + """建構子""" + self.size = 0 # 鍵值對數量 + self.capacity = 4 # 雜湊表容量 + self.load_thres = 2.0 / 3.0 # 觸發擴容的負載因子閾值 + self.extend_ratio = 2 # 擴容倍數 + self.buckets: list[Pair | None] = [None] * self.capacity # 桶陣列 + self.TOMBSTONE = Pair(-1, "-1") # 刪除標記 + + def hash_func(self, key: int) -> int: + """雜湊函式""" + return key % self.capacity + + def load_factor(self) -> float: + """負載因子""" + return self.size / self.capacity + + def find_bucket(self, key: int) -> int: + """搜尋 key 對應的桶索引""" + index = self.hash_func(key) + first_tombstone = -1 + # 線性探查,當遇到空桶時跳出 + while self.buckets[index] is not None: + # 若遇到 key ,返回對應的桶索引 + if self.buckets[index].key == key: + # 若之前遇到了刪除標記,則將鍵值對移動至該索引處 + if first_tombstone != -1: + self.buckets[first_tombstone] = self.buckets[index] + self.buckets[index] = self.TOMBSTONE + return first_tombstone # 返回移動後的桶索引 + return index # 返回桶索引 + # 記錄遇到的首個刪除標記 + if first_tombstone == -1 and self.buckets[index] is self.TOMBSTONE: + first_tombstone = index + # 計算桶索引,越過尾部則返回頭部 + index = (index + 1) % self.capacity + # 若 key 不存在,則返回新增點的索引 + return index if first_tombstone == -1 else first_tombstone + + def get(self, key: int) -> str: + """查詢操作""" + # 搜尋 key 對應的桶索引 + index = self.find_bucket(key) + # 若找到鍵值對,則返回對應 val + if self.buckets[index] not in [None, self.TOMBSTONE]: + return self.buckets[index].val + # 若鍵值對不存在,則返回 None + return None + + def put(self, key: int, val: str): + """新增操作""" + # 當負載因子超過閾值時,執行擴容 + if self.load_factor() > self.load_thres: + self.extend() + # 搜尋 key 對應的桶索引 + index = self.find_bucket(key) + # 若找到鍵值對,則覆蓋 val 並返回 + if self.buckets[index] not in [None, self.TOMBSTONE]: + self.buckets[index].val = val + return + # 若鍵值對不存在,則新增該鍵值對 + self.buckets[index] = Pair(key, val) + self.size += 1 + + def remove(self, key: int): + """刪除操作""" + # 搜尋 key 對應的桶索引 + index = self.find_bucket(key) + # 若找到鍵值對,則用刪除標記覆蓋它 + if self.buckets[index] not in [None, self.TOMBSTONE]: + self.buckets[index] = self.TOMBSTONE + self.size -= 1 + + def extend(self): + """擴容雜湊表""" + # 暫存原雜湊表 + buckets_tmp = self.buckets + # 初始化擴容後的新雜湊表 + self.capacity *= self.extend_ratio + self.buckets = [None] * self.capacity + self.size = 0 + # 將鍵值對從原雜湊表搬運至新雜湊表 + for pair in buckets_tmp: + if pair not in [None, self.TOMBSTONE]: + self.put(pair.key, pair.val) + + def print(self): + """列印雜湊表""" + for pair in self.buckets: + if pair is None: + print("None") + elif pair is self.TOMBSTONE: + print("TOMBSTONE") + else: + print(pair.key, "->", pair.val) + ``` + +=== "C++" + + ```cpp title="hash_map_open_addressing.cpp" + /* 開放定址雜湊表 */ + class HashMapOpenAddressing { + private: + int size; // 鍵值對數量 + int capacity = 4; // 雜湊表容量 + const double loadThres = 2.0 / 3.0; // 觸發擴容的負載因子閾值 + const int extendRatio = 2; // 擴容倍數 + vector buckets; // 桶陣列 + Pair *TOMBSTONE = new Pair(-1, "-1"); // 刪除標記 + + public: + /* 建構子 */ + HashMapOpenAddressing() : size(0), buckets(capacity, nullptr) { + } + + /* 析構方法 */ + ~HashMapOpenAddressing() { + for (Pair *pair : buckets) { + if (pair != nullptr && pair != TOMBSTONE) { + delete pair; + } + } + delete TOMBSTONE; + } + + /* 雜湊函式 */ + int hashFunc(int key) { + return key % capacity; + } + + /* 負載因子 */ + double loadFactor() { + return (double)size / capacity; + } + + /* 搜尋 key 對應的桶索引 */ + int findBucket(int key) { + int index = hashFunc(key); + int firstTombstone = -1; + // 線性探查,當遇到空桶時跳出 + while (buckets[index] != nullptr) { + // 若遇到 key ,返回對應的桶索引 + if (buckets[index]->key == key) { + // 若之前遇到了刪除標記,則將鍵值對移動至該索引處 + if (firstTombstone != -1) { + buckets[firstTombstone] = buckets[index]; + buckets[index] = TOMBSTONE; + return firstTombstone; // 返回移動後的桶索引 + } + return index; // 返回桶索引 + } + // 記錄遇到的首個刪除標記 + if (firstTombstone == -1 && buckets[index] == TOMBSTONE) { + firstTombstone = index; + } + // 計算桶索引,越過尾部則返回頭部 + index = (index + 1) % capacity; + } + // 若 key 不存在,則返回新增點的索引 + return firstTombstone == -1 ? index : firstTombstone; + } + + /* 查詢操作 */ + string get(int key) { + // 搜尋 key 對應的桶索引 + int index = findBucket(key); + // 若找到鍵值對,則返回對應 val + if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) { + return buckets[index]->val; + } + // 若鍵值對不存在,則返回空字串 + return ""; + } + + /* 新增操作 */ + void put(int key, string val) { + // 當負載因子超過閾值時,執行擴容 + if (loadFactor() > loadThres) { + extend(); + } + // 搜尋 key 對應的桶索引 + int index = findBucket(key); + // 若找到鍵值對,則覆蓋 val 並返回 + if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) { + buckets[index]->val = val; + return; + } + // 若鍵值對不存在,則新增該鍵值對 + buckets[index] = new Pair(key, val); + size++; + } + + /* 刪除操作 */ + void remove(int key) { + // 搜尋 key 對應的桶索引 + int index = findBucket(key); + // 若找到鍵值對,則用刪除標記覆蓋它 + if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) { + delete buckets[index]; + buckets[index] = TOMBSTONE; + size--; + } + } + + /* 擴容雜湊表 */ + void extend() { + // 暫存原雜湊表 + vector bucketsTmp = buckets; + // 初始化擴容後的新雜湊表 + capacity *= extendRatio; + buckets = vector(capacity, nullptr); + size = 0; + // 將鍵值對從原雜湊表搬運至新雜湊表 + for (Pair *pair : bucketsTmp) { + if (pair != nullptr && pair != TOMBSTONE) { + put(pair->key, pair->val); + delete pair; + } + } + } + + /* 列印雜湊表 */ + void print() { + for (Pair *pair : buckets) { + if (pair == nullptr) { + cout << "nullptr" << endl; + } else if (pair == TOMBSTONE) { + cout << "TOMBSTONE" << endl; + } else { + cout << pair->key << " -> " << pair->val << endl; + } + } + } + }; + ``` + +=== "Java" + + ```java title="hash_map_open_addressing.java" + /* 開放定址雜湊表 */ + class HashMapOpenAddressing { + private int size; // 鍵值對數量 + private int capacity = 4; // 雜湊表容量 + private final double loadThres = 2.0 / 3.0; // 觸發擴容的負載因子閾值 + private final int extendRatio = 2; // 擴容倍數 + private Pair[] buckets; // 桶陣列 + private final Pair TOMBSTONE = new Pair(-1, "-1"); // 刪除標記 + + /* 建構子 */ + public HashMapOpenAddressing() { + size = 0; + buckets = new Pair[capacity]; + } + + /* 雜湊函式 */ + private int hashFunc(int key) { + return key % capacity; + } + + /* 負載因子 */ + private double loadFactor() { + return (double) size / capacity; + } + + /* 搜尋 key 對應的桶索引 */ + private int findBucket(int key) { + int index = hashFunc(key); + int firstTombstone = -1; + // 線性探查,當遇到空桶時跳出 + while (buckets[index] != null) { + // 若遇到 key ,返回對應的桶索引 + if (buckets[index].key == key) { + // 若之前遇到了刪除標記,則將鍵值對移動至該索引處 + if (firstTombstone != -1) { + buckets[firstTombstone] = buckets[index]; + buckets[index] = TOMBSTONE; + return firstTombstone; // 返回移動後的桶索引 + } + return index; // 返回桶索引 + } + // 記錄遇到的首個刪除標記 + if (firstTombstone == -1 && buckets[index] == TOMBSTONE) { + firstTombstone = index; + } + // 計算桶索引,越過尾部則返回頭部 + index = (index + 1) % capacity; + } + // 若 key 不存在,則返回新增點的索引 + return firstTombstone == -1 ? index : firstTombstone; + } + + /* 查詢操作 */ + public String get(int key) { + // 搜尋 key 對應的桶索引 + int index = findBucket(key); + // 若找到鍵值對,則返回對應 val + if (buckets[index] != null && buckets[index] != TOMBSTONE) { + return buckets[index].val; + } + // 若鍵值對不存在,則返回 null + return null; + } + + /* 新增操作 */ + public void put(int key, String val) { + // 當負載因子超過閾值時,執行擴容 + if (loadFactor() > loadThres) { + extend(); + } + // 搜尋 key 對應的桶索引 + int index = findBucket(key); + // 若找到鍵值對,則覆蓋 val 並返回 + if (buckets[index] != null && buckets[index] != TOMBSTONE) { + buckets[index].val = val; + return; + } + // 若鍵值對不存在,則新增該鍵值對 + buckets[index] = new Pair(key, val); + size++; + } + + /* 刪除操作 */ + public void remove(int key) { + // 搜尋 key 對應的桶索引 + int index = findBucket(key); + // 若找到鍵值對,則用刪除標記覆蓋它 + if (buckets[index] != null && buckets[index] != TOMBSTONE) { + buckets[index] = TOMBSTONE; + size--; + } + } + + /* 擴容雜湊表 */ + private void extend() { + // 暫存原雜湊表 + Pair[] bucketsTmp = buckets; + // 初始化擴容後的新雜湊表 + capacity *= extendRatio; + buckets = new Pair[capacity]; + size = 0; + // 將鍵值對從原雜湊表搬運至新雜湊表 + for (Pair pair : bucketsTmp) { + if (pair != null && pair != TOMBSTONE) { + put(pair.key, pair.val); + } + } + } + + /* 列印雜湊表 */ + public void print() { + for (Pair pair : buckets) { + if (pair == null) { + System.out.println("null"); + } else if (pair == TOMBSTONE) { + System.out.println("TOMBSTONE"); + } else { + System.out.println(pair.key + " -> " + pair.val); + } + } + } + } + ``` + +=== "C#" + + ```csharp title="hash_map_open_addressing.cs" + /* 開放定址雜湊表 */ + class HashMapOpenAddressing { + int size; // 鍵值對數量 + int capacity = 4; // 雜湊表容量 + double loadThres = 2.0 / 3.0; // 觸發擴容的負載因子閾值 + int extendRatio = 2; // 擴容倍數 + Pair[] buckets; // 桶陣列 + Pair TOMBSTONE = new(-1, "-1"); // 刪除標記 + + /* 建構子 */ + public HashMapOpenAddressing() { + size = 0; + buckets = new Pair[capacity]; + } + + /* 雜湊函式 */ + int HashFunc(int key) { + return key % capacity; + } + + /* 負載因子 */ + double LoadFactor() { + return (double)size / capacity; + } + + /* 搜尋 key 對應的桶索引 */ + int FindBucket(int key) { + int index = HashFunc(key); + int firstTombstone = -1; + // 線性探查,當遇到空桶時跳出 + while (buckets[index] != null) { + // 若遇到 key ,返回對應的桶索引 + if (buckets[index].key == key) { + // 若之前遇到了刪除標記,則將鍵值對移動至該索引處 + if (firstTombstone != -1) { + buckets[firstTombstone] = buckets[index]; + buckets[index] = TOMBSTONE; + return firstTombstone; // 返回移動後的桶索引 + } + return index; // 返回桶索引 + } + // 記錄遇到的首個刪除標記 + if (firstTombstone == -1 && buckets[index] == TOMBSTONE) { + firstTombstone = index; + } + // 計算桶索引,越過尾部則返回頭部 + index = (index + 1) % capacity; + } + // 若 key 不存在,則返回新增點的索引 + return firstTombstone == -1 ? index : firstTombstone; + } + + /* 查詢操作 */ + public string? Get(int key) { + // 搜尋 key 對應的桶索引 + int index = FindBucket(key); + // 若找到鍵值對,則返回對應 val + if (buckets[index] != null && buckets[index] != TOMBSTONE) { + return buckets[index].val; + } + // 若鍵值對不存在,則返回 null + return null; + } + + /* 新增操作 */ + public void Put(int key, string val) { + // 當負載因子超過閾值時,執行擴容 + if (LoadFactor() > loadThres) { + Extend(); + } + // 搜尋 key 對應的桶索引 + int index = FindBucket(key); + // 若找到鍵值對,則覆蓋 val 並返回 + if (buckets[index] != null && buckets[index] != TOMBSTONE) { + buckets[index].val = val; + return; + } + // 若鍵值對不存在,則新增該鍵值對 + buckets[index] = new Pair(key, val); + size++; + } + + /* 刪除操作 */ + public void Remove(int key) { + // 搜尋 key 對應的桶索引 + int index = FindBucket(key); + // 若找到鍵值對,則用刪除標記覆蓋它 + if (buckets[index] != null && buckets[index] != TOMBSTONE) { + buckets[index] = TOMBSTONE; + size--; + } + } + + /* 擴容雜湊表 */ + void Extend() { + // 暫存原雜湊表 + Pair[] bucketsTmp = buckets; + // 初始化擴容後的新雜湊表 + capacity *= extendRatio; + buckets = new Pair[capacity]; + size = 0; + // 將鍵值對從原雜湊表搬運至新雜湊表 + foreach (Pair pair in bucketsTmp) { + if (pair != null && pair != TOMBSTONE) { + Put(pair.key, pair.val); + } + } + } + + /* 列印雜湊表 */ + public void Print() { + foreach (Pair pair in buckets) { + if (pair == null) { + Console.WriteLine("null"); + } else if (pair == TOMBSTONE) { + Console.WriteLine("TOMBSTONE"); + } else { + Console.WriteLine(pair.key + " -> " + pair.val); + } + } + } + } + ``` + +=== "Go" + + ```go title="hash_map_open_addressing.go" + /* 開放定址雜湊表 */ + type hashMapOpenAddressing struct { + size int // 鍵值對數量 + capacity int // 雜湊表容量 + loadThres float64 // 觸發擴容的負載因子閾值 + extendRatio int // 擴容倍數 + buckets []*pair // 桶陣列 + TOMBSTONE *pair // 刪除標記 + } + + /* 建構子 */ + func newHashMapOpenAddressing() *hashMapOpenAddressing { + return &hashMapOpenAddressing{ + size: 0, + capacity: 4, + loadThres: 2.0 / 3.0, + extendRatio: 2, + buckets: make([]*pair, 4), + TOMBSTONE: &pair{-1, "-1"}, + } + } + + /* 雜湊函式 */ + func (h *hashMapOpenAddressing) hashFunc(key int) int { + return key % h.capacity // 根據鍵計算雜湊值 + } + + /* 負載因子 */ + func (h *hashMapOpenAddressing) loadFactor() float64 { + return float64(h.size) / float64(h.capacity) // 計算當前負載因子 + } + + /* 搜尋 key 對應的桶索引 */ + func (h *hashMapOpenAddressing) findBucket(key int) int { + index := h.hashFunc(key) // 獲取初始索引 + firstTombstone := -1 // 記錄遇到的第一個TOMBSTONE的位置 + for h.buckets[index] != nil { + if h.buckets[index].key == key { + if firstTombstone != -1 { + // 若之前遇到了刪除標記,則將鍵值對移動至該索引處 + h.buckets[firstTombstone] = h.buckets[index] + h.buckets[index] = h.TOMBSTONE + return firstTombstone // 返回移動後的桶索引 + } + return index // 返回找到的索引 + } + if firstTombstone == -1 && h.buckets[index] == h.TOMBSTONE { + firstTombstone = index // 記錄遇到的首個刪除標記的位置 + } + index = (index + 1) % h.capacity // 線性探查,越過尾部則返回頭部 + } + // 若 key 不存在,則返回新增點的索引 + if firstTombstone != -1 { + return firstTombstone + } + return index + } + + /* 查詢操作 */ + func (h *hashMapOpenAddressing) get(key int) string { + index := h.findBucket(key) // 搜尋 key 對應的桶索引 + if h.buckets[index] != nil && h.buckets[index] != h.TOMBSTONE { + return h.buckets[index].val // 若找到鍵值對,則返回對應 val + } + return "" // 若鍵值對不存在,則返回 "" + } + + /* 新增操作 */ + func (h *hashMapOpenAddressing) put(key int, val string) { + if h.loadFactor() > h.loadThres { + h.extend() // 當負載因子超過閾值時,執行擴容 + } + index := h.findBucket(key) // 搜尋 key 對應的桶索引 + if h.buckets[index] == nil || h.buckets[index] == h.TOMBSTONE { + h.buckets[index] = &pair{key, val} // 若鍵值對不存在,則新增該鍵值對 + h.size++ + } else { + h.buckets[index].val = val // 若找到鍵值對,則覆蓋 val + } + } + + /* 刪除操作 */ + func (h *hashMapOpenAddressing) remove(key int) { + index := h.findBucket(key) // 搜尋 key 對應的桶索引 + if h.buckets[index] != nil && h.buckets[index] != h.TOMBSTONE { + h.buckets[index] = h.TOMBSTONE // 若找到鍵值對,則用刪除標記覆蓋它 + h.size-- + } + } + + /* 擴容雜湊表 */ + func (h *hashMapOpenAddressing) extend() { + oldBuckets := h.buckets // 暫存原雜湊表 + h.capacity *= h.extendRatio // 更新容量 + h.buckets = make([]*pair, h.capacity) // 初始化擴容後的新雜湊表 + h.size = 0 // 重置大小 + // 將鍵值對從原雜湊表搬運至新雜湊表 + for _, pair := range oldBuckets { + if pair != nil && pair != h.TOMBSTONE { + h.put(pair.key, pair.val) + } + } + } + + /* 列印雜湊表 */ + func (h *hashMapOpenAddressing) print() { + for _, pair := range h.buckets { + if pair == nil { + fmt.Println("nil") + } else if pair == h.TOMBSTONE { + fmt.Println("TOMBSTONE") + } else { + fmt.Printf("%d -> %s\n", pair.key, pair.val) + } + } + } + ``` + +=== "Swift" + + ```swift title="hash_map_open_addressing.swift" + /* 開放定址雜湊表 */ + class HashMapOpenAddressing { + var size: Int // 鍵值對數量 + var capacity: Int // 雜湊表容量 + var loadThres: Double // 觸發擴容的負載因子閾值 + var extendRatio: Int // 擴容倍數 + var buckets: [Pair?] // 桶陣列 + var TOMBSTONE: Pair // 刪除標記 + + /* 建構子 */ + init() { + size = 0 + capacity = 4 + loadThres = 2.0 / 3.0 + extendRatio = 2 + buckets = Array(repeating: nil, count: capacity) + TOMBSTONE = Pair(key: -1, val: "-1") + } + + /* 雜湊函式 */ + func hashFunc(key: Int) -> Int { + key % capacity + } + + /* 負載因子 */ + func loadFactor() -> Double { + Double(size) / Double(capacity) + } + + /* 搜尋 key 對應的桶索引 */ + func findBucket(key: Int) -> Int { + var index = hashFunc(key: key) + var firstTombstone = -1 + // 線性探查,當遇到空桶時跳出 + while buckets[index] != nil { + // 若遇到 key ,返回對應的桶索引 + if buckets[index]!.key == key { + // 若之前遇到了刪除標記,則將鍵值對移動至該索引處 + if firstTombstone != -1 { + buckets[firstTombstone] = buckets[index] + buckets[index] = TOMBSTONE + return firstTombstone // 返回移動後的桶索引 + } + return index // 返回桶索引 + } + // 記錄遇到的首個刪除標記 + if firstTombstone == -1 && buckets[index] == TOMBSTONE { + firstTombstone = index + } + // 計算桶索引,越過尾部則返回頭部 + index = (index + 1) % capacity + } + // 若 key 不存在,則返回新增點的索引 + return firstTombstone == -1 ? index : firstTombstone + } + + /* 查詢操作 */ + func get(key: Int) -> String? { + // 搜尋 key 對應的桶索引 + let index = findBucket(key: key) + // 若找到鍵值對,則返回對應 val + if buckets[index] != nil, buckets[index] != TOMBSTONE { + return buckets[index]!.val + } + // 若鍵值對不存在,則返回 null + return nil + } + + /* 新增操作 */ + func put(key: Int, val: String) { + // 當負載因子超過閾值時,執行擴容 + if loadFactor() > loadThres { + extend() + } + // 搜尋 key 對應的桶索引 + let index = findBucket(key: key) + // 若找到鍵值對,則覆蓋 val 並返回 + if buckets[index] != nil, buckets[index] != TOMBSTONE { + buckets[index]!.val = val + return + } + // 若鍵值對不存在,則新增該鍵值對 + buckets[index] = Pair(key: key, val: val) + size += 1 + } + + /* 刪除操作 */ + func remove(key: Int) { + // 搜尋 key 對應的桶索引 + let index = findBucket(key: key) + // 若找到鍵值對,則用刪除標記覆蓋它 + if buckets[index] != nil, buckets[index] != TOMBSTONE { + buckets[index] = TOMBSTONE + size -= 1 + } + } + + /* 擴容雜湊表 */ + func extend() { + // 暫存原雜湊表 + let bucketsTmp = buckets + // 初始化擴容後的新雜湊表 + capacity *= extendRatio + buckets = Array(repeating: nil, count: capacity) + size = 0 + // 將鍵值對從原雜湊表搬運至新雜湊表 + for pair in bucketsTmp { + if let pair, pair != TOMBSTONE { + put(key: pair.key, val: pair.val) + } + } + } + + /* 列印雜湊表 */ + func print() { + for pair in buckets { + if pair == nil { + Swift.print("null") + } else if pair == TOMBSTONE { + Swift.print("TOMBSTONE") + } else { + Swift.print("\(pair!.key) -> \(pair!.val)") + } + } + } + } + ``` + +=== "JS" + + ```javascript title="hash_map_open_addressing.js" + /* 開放定址雜湊表 */ + class HashMapOpenAddressing { + #size; // 鍵值對數量 + #capacity; // 雜湊表容量 + #loadThres; // 觸發擴容的負載因子閾值 + #extendRatio; // 擴容倍數 + #buckets; // 桶陣列 + #TOMBSTONE; // 刪除標記 + + /* 建構子 */ + constructor() { + this.#size = 0; // 鍵值對數量 + this.#capacity = 4; // 雜湊表容量 + this.#loadThres = 2.0 / 3.0; // 觸發擴容的負載因子閾值 + this.#extendRatio = 2; // 擴容倍數 + this.#buckets = Array(this.#capacity).fill(null); // 桶陣列 + this.#TOMBSTONE = new Pair(-1, '-1'); // 刪除標記 + } + + /* 雜湊函式 */ + #hashFunc(key) { + return key % this.#capacity; + } + + /* 負載因子 */ + #loadFactor() { + return this.#size / this.#capacity; + } + + /* 搜尋 key 對應的桶索引 */ + #findBucket(key) { + let index = this.#hashFunc(key); + let firstTombstone = -1; + // 線性探查,當遇到空桶時跳出 + while (this.#buckets[index] !== null) { + // 若遇到 key ,返回對應的桶索引 + if (this.#buckets[index].key === key) { + // 若之前遇到了刪除標記,則將鍵值對移動至該索引處 + if (firstTombstone !== -1) { + this.#buckets[firstTombstone] = this.#buckets[index]; + this.#buckets[index] = this.#TOMBSTONE; + return firstTombstone; // 返回移動後的桶索引 + } + return index; // 返回桶索引 + } + // 記錄遇到的首個刪除標記 + if ( + firstTombstone === -1 && + this.#buckets[index] === this.#TOMBSTONE + ) { + firstTombstone = index; + } + // 計算桶索引,越過尾部則返回頭部 + index = (index + 1) % this.#capacity; + } + // 若 key 不存在,則返回新增點的索引 + return firstTombstone === -1 ? index : firstTombstone; + } + + /* 查詢操作 */ + get(key) { + // 搜尋 key 對應的桶索引 + const index = this.#findBucket(key); + // 若找到鍵值對,則返回對應 val + if ( + this.#buckets[index] !== null && + this.#buckets[index] !== this.#TOMBSTONE + ) { + return this.#buckets[index].val; + } + // 若鍵值對不存在,則返回 null + return null; + } + + /* 新增操作 */ + put(key, val) { + // 當負載因子超過閾值時,執行擴容 + if (this.#loadFactor() > this.#loadThres) { + this.#extend(); + } + // 搜尋 key 對應的桶索引 + const index = this.#findBucket(key); + // 若找到鍵值對,則覆蓋 val 並返回 + if ( + this.#buckets[index] !== null && + this.#buckets[index] !== this.#TOMBSTONE + ) { + this.#buckets[index].val = val; + return; + } + // 若鍵值對不存在,則新增該鍵值對 + this.#buckets[index] = new Pair(key, val); + this.#size++; + } + + /* 刪除操作 */ + remove(key) { + // 搜尋 key 對應的桶索引 + const index = this.#findBucket(key); + // 若找到鍵值對,則用刪除標記覆蓋它 + if ( + this.#buckets[index] !== null && + this.#buckets[index] !== this.#TOMBSTONE + ) { + this.#buckets[index] = this.#TOMBSTONE; + this.#size--; + } + } + + /* 擴容雜湊表 */ + #extend() { + // 暫存原雜湊表 + const bucketsTmp = this.#buckets; + // 初始化擴容後的新雜湊表 + this.#capacity *= this.#extendRatio; + this.#buckets = Array(this.#capacity).fill(null); + this.#size = 0; + // 將鍵值對從原雜湊表搬運至新雜湊表 + for (const pair of bucketsTmp) { + if (pair !== null && pair !== this.#TOMBSTONE) { + this.put(pair.key, pair.val); + } + } + } + + /* 列印雜湊表 */ + print() { + for (const pair of this.#buckets) { + if (pair === null) { + console.log('null'); + } else if (pair === this.#TOMBSTONE) { + console.log('TOMBSTONE'); + } else { + console.log(pair.key + ' -> ' + pair.val); + } + } + } + } + ``` + +=== "TS" + + ```typescript title="hash_map_open_addressing.ts" + /* 開放定址雜湊表 */ + class HashMapOpenAddressing { + private size: number; // 鍵值對數量 + private capacity: number; // 雜湊表容量 + private loadThres: number; // 觸發擴容的負載因子閾值 + private extendRatio: number; // 擴容倍數 + private buckets: Array; // 桶陣列 + private TOMBSTONE: Pair; // 刪除標記 + + /* 建構子 */ + constructor() { + this.size = 0; // 鍵值對數量 + this.capacity = 4; // 雜湊表容量 + this.loadThres = 2.0 / 3.0; // 觸發擴容的負載因子閾值 + this.extendRatio = 2; // 擴容倍數 + this.buckets = Array(this.capacity).fill(null); // 桶陣列 + this.TOMBSTONE = new Pair(-1, '-1'); // 刪除標記 + } + + /* 雜湊函式 */ + private hashFunc(key: number): number { + return key % this.capacity; + } + + /* 負載因子 */ + private loadFactor(): number { + return this.size / this.capacity; + } + + /* 搜尋 key 對應的桶索引 */ + private findBucket(key: number): number { + let index = this.hashFunc(key); + let firstTombstone = -1; + // 線性探查,當遇到空桶時跳出 + while (this.buckets[index] !== null) { + // 若遇到 key ,返回對應的桶索引 + if (this.buckets[index]!.key === key) { + // 若之前遇到了刪除標記,則將鍵值對移動至該索引處 + if (firstTombstone !== -1) { + this.buckets[firstTombstone] = this.buckets[index]; + this.buckets[index] = this.TOMBSTONE; + return firstTombstone; // 返回移動後的桶索引 + } + return index; // 返回桶索引 + } + // 記錄遇到的首個刪除標記 + if ( + firstTombstone === -1 && + this.buckets[index] === this.TOMBSTONE + ) { + firstTombstone = index; + } + // 計算桶索引,越過尾部則返回頭部 + index = (index + 1) % this.capacity; + } + // 若 key 不存在,則返回新增點的索引 + return firstTombstone === -1 ? index : firstTombstone; + } + + /* 查詢操作 */ + get(key: number): string | null { + // 搜尋 key 對應的桶索引 + const index = this.findBucket(key); + // 若找到鍵值對,則返回對應 val + if ( + this.buckets[index] !== null && + this.buckets[index] !== this.TOMBSTONE + ) { + return this.buckets[index]!.val; + } + // 若鍵值對不存在,則返回 null + return null; + } + + /* 新增操作 */ + put(key: number, val: string): void { + // 當負載因子超過閾值時,執行擴容 + if (this.loadFactor() > this.loadThres) { + this.extend(); + } + // 搜尋 key 對應的桶索引 + const index = this.findBucket(key); + // 若找到鍵值對,則覆蓋 val 並返回 + if ( + this.buckets[index] !== null && + this.buckets[index] !== this.TOMBSTONE + ) { + this.buckets[index]!.val = val; + return; + } + // 若鍵值對不存在,則新增該鍵值對 + this.buckets[index] = new Pair(key, val); + this.size++; + } + + /* 刪除操作 */ + remove(key: number): void { + // 搜尋 key 對應的桶索引 + const index = this.findBucket(key); + // 若找到鍵值對,則用刪除標記覆蓋它 + if ( + this.buckets[index] !== null && + this.buckets[index] !== this.TOMBSTONE + ) { + this.buckets[index] = this.TOMBSTONE; + this.size--; + } + } + + /* 擴容雜湊表 */ + private extend(): void { + // 暫存原雜湊表 + const bucketsTmp = this.buckets; + // 初始化擴容後的新雜湊表 + this.capacity *= this.extendRatio; + this.buckets = Array(this.capacity).fill(null); + this.size = 0; + // 將鍵值對從原雜湊表搬運至新雜湊表 + for (const pair of bucketsTmp) { + if (pair !== null && pair !== this.TOMBSTONE) { + this.put(pair.key, pair.val); + } + } + } + + /* 列印雜湊表 */ + print(): void { + for (const pair of this.buckets) { + if (pair === null) { + console.log('null'); + } else if (pair === this.TOMBSTONE) { + console.log('TOMBSTONE'); + } else { + console.log(pair.key + ' -> ' + pair.val); + } + } + } + } + ``` + +=== "Dart" + + ```dart title="hash_map_open_addressing.dart" + /* 開放定址雜湊表 */ + class HashMapOpenAddressing { + late int _size; // 鍵值對數量 + int _capacity = 4; // 雜湊表容量 + double _loadThres = 2.0 / 3.0; // 觸發擴容的負載因子閾值 + int _extendRatio = 2; // 擴容倍數 + late List _buckets; // 桶陣列 + Pair _TOMBSTONE = Pair(-1, "-1"); // 刪除標記 + + /* 建構子 */ + HashMapOpenAddressing() { + _size = 0; + _buckets = List.generate(_capacity, (index) => null); + } + + /* 雜湊函式 */ + int hashFunc(int key) { + return key % _capacity; + } + + /* 負載因子 */ + double loadFactor() { + return _size / _capacity; + } + + /* 搜尋 key 對應的桶索引 */ + int findBucket(int key) { + int index = hashFunc(key); + int firstTombstone = -1; + // 線性探查,當遇到空桶時跳出 + while (_buckets[index] != null) { + // 若遇到 key ,返回對應的桶索引 + if (_buckets[index]!.key == key) { + // 若之前遇到了刪除標記,則將鍵值對移動至該索引處 + if (firstTombstone != -1) { + _buckets[firstTombstone] = _buckets[index]; + _buckets[index] = _TOMBSTONE; + return firstTombstone; // 返回移動後的桶索引 + } + return index; // 返回桶索引 + } + // 記錄遇到的首個刪除標記 + if (firstTombstone == -1 && _buckets[index] == _TOMBSTONE) { + firstTombstone = index; + } + // 計算桶索引,越過尾部則返回頭部 + index = (index + 1) % _capacity; + } + // 若 key 不存在,則返回新增點的索引 + return firstTombstone == -1 ? index : firstTombstone; + } + + /* 查詢操作 */ + String? get(int key) { + // 搜尋 key 對應的桶索引 + int index = findBucket(key); + // 若找到鍵值對,則返回對應 val + if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) { + return _buckets[index]!.val; + } + // 若鍵值對不存在,則返回 null + return null; + } + + /* 新增操作 */ + void put(int key, String val) { + // 當負載因子超過閾值時,執行擴容 + if (loadFactor() > _loadThres) { + extend(); + } + // 搜尋 key 對應的桶索引 + int index = findBucket(key); + // 若找到鍵值對,則覆蓋 val 並返回 + if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) { + _buckets[index]!.val = val; + return; + } + // 若鍵值對不存在,則新增該鍵值對 + _buckets[index] = new Pair(key, val); + _size++; + } + + /* 刪除操作 */ + void remove(int key) { + // 搜尋 key 對應的桶索引 + int index = findBucket(key); + // 若找到鍵值對,則用刪除標記覆蓋它 + if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) { + _buckets[index] = _TOMBSTONE; + _size--; + } + } + + /* 擴容雜湊表 */ + void extend() { + // 暫存原雜湊表 + List bucketsTmp = _buckets; + // 初始化擴容後的新雜湊表 + _capacity *= _extendRatio; + _buckets = List.generate(_capacity, (index) => null); + _size = 0; + // 將鍵值對從原雜湊表搬運至新雜湊表 + for (Pair? pair in bucketsTmp) { + if (pair != null && pair != _TOMBSTONE) { + put(pair.key, pair.val); + } + } + } + + /* 列印雜湊表 */ + void printHashMap() { + for (Pair? pair in _buckets) { + if (pair == null) { + print("null"); + } else if (pair == _TOMBSTONE) { + print("TOMBSTONE"); + } else { + print("${pair.key} -> ${pair.val}"); + } + } + } + } + ``` + +=== "Rust" + + ```rust title="hash_map_open_addressing.rs" + /* 開放定址雜湊表 */ + struct HashMapOpenAddressing { + size: usize, // 鍵值對數量 + capacity: usize, // 雜湊表容量 + load_thres: f64, // 觸發擴容的負載因子閾值 + extend_ratio: usize, // 擴容倍數 + buckets: Vec>, // 桶陣列 + TOMBSTONE: Option, // 刪除標記 + } + + impl HashMapOpenAddressing { + /* 建構子 */ + fn new() -> Self { + Self { + size: 0, + capacity: 4, + load_thres: 2.0 / 3.0, + extend_ratio: 2, + buckets: vec![None; 4], + TOMBSTONE: Some(Pair { + key: -1, + val: "-1".to_string(), + }), + } + } + + /* 雜湊函式 */ + fn hash_func(&self, key: i32) -> usize { + (key % self.capacity as i32) as usize + } + + /* 負載因子 */ + fn load_factor(&self) -> f64 { + self.size as f64 / self.capacity as f64 + } + + /* 搜尋 key 對應的桶索引 */ + fn find_bucket(&mut self, key: i32) -> usize { + let mut index = self.hash_func(key); + let mut first_tombstone = -1; + // 線性探查,當遇到空桶時跳出 + while self.buckets[index].is_some() { + // 若遇到 key,返回對應的桶索引 + if self.buckets[index].as_ref().unwrap().key == key { + // 若之前遇到了刪除標記,則將建值對移動至該索引 + if first_tombstone != -1 { + self.buckets[first_tombstone as usize] = self.buckets[index].take(); + self.buckets[index] = self.TOMBSTONE.clone(); + return first_tombstone as usize; // 返回移動後的桶索引 + } + return index; // 返回桶索引 + } + // 記錄遇到的首個刪除標記 + if first_tombstone == -1 && self.buckets[index] == self.TOMBSTONE { + first_tombstone = index as i32; + } + // 計算桶索引,越過尾部則返回頭部 + index = (index + 1) % self.capacity; + } + // 若 key 不存在,則返回新增點的索引 + if first_tombstone == -1 { + index + } else { + first_tombstone as usize + } + } + + /* 查詢操作 */ + fn get(&mut self, key: i32) -> Option<&str> { + // 搜尋 key 對應的桶索引 + let index = self.find_bucket(key); + // 若找到鍵值對,則返回對應 val + if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE { + return self.buckets[index].as_ref().map(|pair| &pair.val as &str); + } + // 若鍵值對不存在,則返回 null + None + } + + /* 新增操作 */ + fn put(&mut self, key: i32, val: String) { + // 當負載因子超過閾值時,執行擴容 + if self.load_factor() > self.load_thres { + self.extend(); + } + // 搜尋 key 對應的桶索引 + let index = self.find_bucket(key); + // 若找到鍵值對,則覆蓋 val 並返回 + if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE { + self.buckets[index].as_mut().unwrap().val = val; + return; + } + // 若鍵值對不存在,則新增該鍵值對 + self.buckets[index] = Some(Pair { key, val }); + self.size += 1; + } + + /* 刪除操作 */ + fn remove(&mut self, key: i32) { + // 搜尋 key 對應的桶索引 + let index = self.find_bucket(key); + // 若找到鍵值對,則用刪除標記覆蓋它 + if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE { + self.buckets[index] = self.TOMBSTONE.clone(); + self.size -= 1; + } + } + + /* 擴容雜湊表 */ + fn extend(&mut self) { + // 暫存原雜湊表 + let buckets_tmp = self.buckets.clone(); + // 初始化擴容後的新雜湊表 + self.capacity *= self.extend_ratio; + self.buckets = vec![None; self.capacity]; + self.size = 0; + + // 將鍵值對從原雜湊表搬運至新雜湊表 + for pair in buckets_tmp { + if pair.is_none() || pair == self.TOMBSTONE { + continue; + } + let pair = pair.unwrap(); + + self.put(pair.key, pair.val); + } + } + /* 列印雜湊表 */ + fn print(&self) { + for pair in &self.buckets { + if pair.is_none() { + println!("null"); + } else if pair == &self.TOMBSTONE { + println!("TOMBSTONE"); + } else { + let pair = pair.as_ref().unwrap(); + println!("{} -> {}", pair.key, pair.val); + } + } + } + } + ``` + +=== "C" + + ```c title="hash_map_open_addressing.c" + /* 開放定址雜湊表 */ + typedef struct { + int size; // 鍵值對數量 + int capacity; // 雜湊表容量 + double loadThres; // 觸發擴容的負載因子閾值 + int extendRatio; // 擴容倍數 + Pair **buckets; // 桶陣列 + Pair *TOMBSTONE; // 刪除標記 + } HashMapOpenAddressing; + + /* 建構子 */ + HashMapOpenAddressing *newHashMapOpenAddressing() { + HashMapOpenAddressing *hashMap = (HashMapOpenAddressing *)malloc(sizeof(HashMapOpenAddressing)); + hashMap->size = 0; + hashMap->capacity = 4; + hashMap->loadThres = 2.0 / 3.0; + hashMap->extendRatio = 2; + hashMap->buckets = (Pair **)malloc(sizeof(Pair *) * hashMap->capacity); + hashMap->TOMBSTONE = (Pair *)malloc(sizeof(Pair)); + hashMap->TOMBSTONE->key = -1; + hashMap->TOMBSTONE->val = "-1"; + + return hashMap; + } + + /* 析構函式 */ + void delHashMapOpenAddressing(HashMapOpenAddressing *hashMap) { + for (int i = 0; i < hashMap->capacity; i++) { + Pair *pair = hashMap->buckets[i]; + if (pair != NULL && pair != hashMap->TOMBSTONE) { + free(pair->val); + free(pair); + } + } + } + + /* 雜湊函式 */ + int hashFunc(HashMapOpenAddressing *hashMap, int key) { + return key % hashMap->capacity; + } + + /* 負載因子 */ + double loadFactor(HashMapOpenAddressing *hashMap) { + return (double)hashMap->size / (double)hashMap->capacity; + } + + /* 搜尋 key 對應的桶索引 */ + int findBucket(HashMapOpenAddressing *hashMap, int key) { + int index = hashFunc(hashMap, key); + int firstTombstone = -1; + // 線性探查,當遇到空桶時跳出 + while (hashMap->buckets[index] != NULL) { + // 若遇到 key ,返回對應的桶索引 + if (hashMap->buckets[index]->key == key) { + // 若之前遇到了刪除標記,則將鍵值對移動至該索引處 + if (firstTombstone != -1) { + hashMap->buckets[firstTombstone] = hashMap->buckets[index]; + hashMap->buckets[index] = hashMap->TOMBSTONE; + return firstTombstone; // 返回移動後的桶索引 + } + return index; // 返回桶索引 + } + // 記錄遇到的首個刪除標記 + if (firstTombstone == -1 && hashMap->buckets[index] == hashMap->TOMBSTONE) { + firstTombstone = index; + } + // 計算桶索引,越過尾部則返回頭部 + index = (index + 1) % hashMap->capacity; + } + // 若 key 不存在,則返回新增點的索引 + return firstTombstone == -1 ? index : firstTombstone; + } + + /* 查詢操作 */ + char *get(HashMapOpenAddressing *hashMap, int key) { + // 搜尋 key 對應的桶索引 + int index = findBucket(hashMap, key); + // 若找到鍵值對,則返回對應 val + if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) { + return hashMap->buckets[index]->val; + } + // 若鍵值對不存在,則返回空字串 + return ""; + } + + /* 新增操作 */ + void put(HashMapOpenAddressing *hashMap, int key, char *val) { + // 當負載因子超過閾值時,執行擴容 + if (loadFactor(hashMap) > hashMap->loadThres) { + extend(hashMap); + } + // 搜尋 key 對應的桶索引 + int index = findBucket(hashMap, key); + // 若找到鍵值對,則覆蓋 val 並返回 + if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) { + free(hashMap->buckets[index]->val); + hashMap->buckets[index]->val = (char *)malloc(sizeof(strlen(val) + 1)); + strcpy(hashMap->buckets[index]->val, val); + hashMap->buckets[index]->val[strlen(val)] = '\0'; + return; + } + // 若鍵值對不存在,則新增該鍵值對 + Pair *pair = (Pair *)malloc(sizeof(Pair)); + pair->key = key; + pair->val = (char *)malloc(sizeof(strlen(val) + 1)); + strcpy(pair->val, val); + pair->val[strlen(val)] = '\0'; + + hashMap->buckets[index] = pair; + hashMap->size++; + } + + /* 刪除操作 */ + void removeItem(HashMapOpenAddressing *hashMap, int key) { + // 搜尋 key 對應的桶索引 + int index = findBucket(hashMap, key); + // 若找到鍵值對,則用刪除標記覆蓋它 + if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) { + Pair *pair = hashMap->buckets[index]; + free(pair->val); + free(pair); + hashMap->buckets[index] = hashMap->TOMBSTONE; + hashMap->size--; + } + } + + /* 擴容雜湊表 */ + void extend(HashMapOpenAddressing *hashMap) { + // 暫存原雜湊表 + Pair **bucketsTmp = hashMap->buckets; + int oldCapacity = hashMap->capacity; + // 初始化擴容後的新雜湊表 + hashMap->capacity *= hashMap->extendRatio; + hashMap->buckets = (Pair **)malloc(sizeof(Pair *) * hashMap->capacity); + hashMap->size = 0; + // 將鍵值對從原雜湊表搬運至新雜湊表 + for (int i = 0; i < oldCapacity; i++) { + Pair *pair = bucketsTmp[i]; + if (pair != NULL && pair != hashMap->TOMBSTONE) { + put(hashMap, pair->key, pair->val); + free(pair->val); + free(pair); + } + } + free(bucketsTmp); + } + + /* 列印雜湊表 */ + void print(HashMapOpenAddressing *hashMap) { + for (int i = 0; i < hashMap->capacity; i++) { + Pair *pair = hashMap->buckets[i]; + if (pair == NULL) { + printf("NULL\n"); + } else if (pair == hashMap->TOMBSTONE) { + printf("TOMBSTONE\n"); + } else { + printf("%d -> %s\n", pair->key, pair->val); + } + } + } + ``` + +=== "Kotlin" + + ```kotlin title="hash_map_open_addressing.kt" + /* 開放定址雜湊表 */ + class HashMapOpenAddressing { + private var size: Int = 0 // 鍵值對數量 + private var capacity = 4 // 雜湊表容量 + private val loadThres: Double = 2.0 / 3.0 // 觸發擴容的負載因子閾值 + private val extendRatio = 2 // 擴容倍數 + private var buckets: Array // 桶陣列 + private val TOMBSTONE = Pair(-1, "-1") // 刪除標記 + + /* 建構子 */ + init { + buckets = arrayOfNulls(capacity) + } + + /* 雜湊函式 */ + fun hashFunc(key: Int): Int { + return key % capacity + } + + /* 負載因子 */ + fun loadFactor(): Double { + return (size / capacity).toDouble() + } + + /* 搜尋 key 對應的桶索引 */ + fun findBucket(key: Int): Int { + var index = hashFunc(key) + var firstTombstone = -1 + // 線性探查,當遇到空桶時跳出 + while (buckets[index] != null) { + // 若遇到 key ,返回對應的桶索引 + if (buckets[index]?.key == key) { + // 若之前遇到了刪除標記,則將鍵值對移動至該索引處 + if (firstTombstone != -1) { + buckets[firstTombstone] = buckets[index] + buckets[index] = TOMBSTONE + return firstTombstone // 返回移動後的桶索引 + } + return index // 返回桶索引 + } + // 記錄遇到的首個刪除標記 + if (firstTombstone == -1 && buckets[index] == TOMBSTONE) { + firstTombstone = index + } + // 計算桶索引,越過尾部則返回頭部 + index = (index + 1) % capacity + } + // 若 key 不存在,則返回新增點的索引 + return if (firstTombstone == -1) index else firstTombstone + } + + /* 查詢操作 */ + fun get(key: Int): String? { + // 搜尋 key 對應的桶索引 + val index = findBucket(key) + // 若找到鍵值對,則返回對應 val + if (buckets[index] != null && buckets[index] != TOMBSTONE) { + return buckets[index]?.value + } + // 若鍵值對不存在,則返回 null + return null + } + + /* 新增操作 */ + fun put(key: Int, value: String) { + // 當負載因子超過閾值時,執行擴容 + if (loadFactor() > loadThres) { + extend() + } + // 搜尋 key 對應的桶索引 + val index = findBucket(key) + // 若找到鍵值對,則覆蓋 val 並返回 + if (buckets[index] != null && buckets[index] != TOMBSTONE) { + buckets[index]!!.value = value + return + } + // 若鍵值對不存在,則新增該鍵值對 + buckets[index] = Pair(key, value) + size++ + } + + /* 刪除操作 */ + fun remove(key: Int) { + // 搜尋 key 對應的桶索引 + val index = findBucket(key) + // 若找到鍵值對,則用刪除標記覆蓋它 + if (buckets[index] != null && buckets[index] != TOMBSTONE) { + buckets[index] = TOMBSTONE + size-- + } + } + + /* 擴容雜湊表 */ + fun extend() { + // 暫存原雜湊表 + val bucketsTmp = buckets + // 初始化擴容後的新雜湊表 + capacity *= extendRatio + buckets = arrayOfNulls(capacity) + size = 0 + // 將鍵值對從原雜湊表搬運至新雜湊表 + for (pair in bucketsTmp) { + if (pair != null && pair != TOMBSTONE) { + put(pair.key, pair.value) + } + } + } + + /* 列印雜湊表 */ + fun print() { + for (pair in buckets) { + if (pair == null) { + println("null") + } else if (pair == TOMBSTONE) { + println("TOMESTOME") + } else { + println("${pair.key} -> ${pair.value}") + } + } + } + } + ``` + +=== "Ruby" + + ```ruby title="hash_map_open_addressing.rb" + [class]{HashMapOpenAddressing}-[func]{} + ``` + +=== "Zig" + + ```zig title="hash_map_open_addressing.zig" + [class]{HashMapOpenAddressing}-[func]{} + ``` + +### 2.   平方探測 + +平方探測與線性探查類似,都是開放定址的常見策略之一。當發生衝突時,平方探測不是簡單地跳過一個固定的步數,而是跳過“探測次數的平方”的步數,即 $1, 4, 9, \dots$ 步。 + +平方探測主要具有以下優勢。 + +- 平方探測透過跳過探測次數平方的距離,試圖緩解線性探查的聚集效應。 +- 平方探測會跳過更大的距離來尋找空位置,有助於資料分佈得更加均勻。 + +然而,平方探測並不是完美的。 + +- 仍然存在聚集現象,即某些位置比其他位置更容易被佔用。 +- 由於平方的增長,平方探測可能不會探測整個雜湊表,這意味著即使雜湊表中有空桶,平方探測也可能無法訪問到它。 + +### 3.   多次雜湊 + +顧名思義,多次雜湊方法使用多個雜湊函式 $f_1(x)$、$f_2(x)$、$f_3(x)$、$\dots$ 進行探測。 + +- **插入元素**:若雜湊函式 $f_1(x)$ 出現衝突,則嘗試 $f_2(x)$ ,以此類推,直到找到空位後插入元素。 +- **查詢元素**:在相同的雜湊函式順序下進行查詢,直到找到目標元素時返回;若遇到空位或已嘗試所有雜湊函式,說明雜湊表中不存在該元素,則返回 `None` 。 + +與線性探查相比,多次雜湊方法不易產生聚集,但多個雜湊函式會帶來額外的計算量。 + +!!! tip + + 請注意,開放定址(線性探查、平方探測和多次雜湊)雜湊表都存在“不能直接刪除元素”的問題。 + +## 6.2.3   程式語言的選擇 + +各種程式語言採取了不同的雜湊表實現策略,下面舉幾個例子。 + +- Python 採用開放定址。字典 `dict` 使用偽隨機數進行探測。 +- Java 採用鏈式位址。自 JDK 1.8 以來,當 `HashMap` 內陣列長度達到 64 且鏈結串列長度達到 8 時,鏈結串列會轉換為紅黑樹以提升查詢效能。 +- Go 採用鏈式位址。Go 規定每個桶最多儲存 8 個鍵值對,超出容量則連線一個溢位桶;當溢位桶過多時,會執行一次特殊的等量擴容操作,以確保效能。 diff --git a/zh-Hant/docs/chapter_hashing/hash_map.md b/zh-Hant/docs/chapter_hashing/hash_map.md new file mode 100755 index 000000000..70b61fc0c --- /dev/null +++ b/zh-Hant/docs/chapter_hashing/hash_map.md @@ -0,0 +1,1890 @@ +--- +comments: true +--- + +# 6.1   雜湊表 + +雜湊表(hash table),又稱散列表,它透過建立鍵 `key` 與值 `value` 之間的對映,實現高效的元素查詢。具體而言,我們向雜湊表中輸入一個鍵 `key` ,則可以在 $O(1)$ 時間內獲取對應的值 `value` 。 + +如圖 6-1 所示,給定 $n$ 個學生,每個學生都有“姓名”和“學號”兩項資料。假如我們希望實現“輸入一個學號,返回對應的姓名”的查詢功能,則可以採用圖 6-1 所示的雜湊表來實現。 + +![雜湊表的抽象表示](hash_map.assets/hash_table_lookup.png){ class="animation-figure" } + +

圖 6-1   雜湊表的抽象表示

+ +除雜湊表外,陣列和鏈結串列也可以實現查詢功能,它們的效率對比如表 6-1 所示。 + +- **新增元素**:僅需將元素新增至陣列(鏈結串列)的尾部即可,使用 $O(1)$ 時間。 +- **查詢元素**:由於陣列(鏈結串列)是亂序的,因此需要走訪其中的所有元素,使用 $O(n)$ 時間。 +- **刪除元素**:需要先查詢到元素,再從陣列(鏈結串列)中刪除,使用 $O(n)$ 時間。 + +

表 6-1   元素查詢效率對比

+ +
+ +| | 陣列 | 鏈結串列 | 雜湊表 | +| -------- | ------ | ------ | ------ | +| 查詢元素 | $O(n)$ | $O(n)$ | $O(1)$ | +| 新增元素 | $O(1)$ | $O(1)$ | $O(1)$ | +| 刪除元素 | $O(n)$ | $O(n)$ | $O(1)$ | + +
+ +觀察發現,**在雜湊表中進行增刪查改的時間複雜度都是 $O(1)$** ,非常高效。 + +## 6.1.1   雜湊表常用操作 + +雜湊表的常見操作包括:初始化、查詢操作、新增鍵值對和刪除鍵值對等,示例程式碼如下: + +=== "Python" + + ```python title="hash_map.py" + # 初始化雜湊表 + hmap: dict = {} + + # 新增操作 + # 在雜湊表中新增鍵值對 (key, value) + hmap[12836] = "小哈" + hmap[15937] = "小囉" + hmap[16750] = "小算" + hmap[13276] = "小法" + hmap[10583] = "小鴨" + + # 查詢操作 + # 向雜湊表中輸入鍵 key ,得到值 value + name: str = hmap[15937] + + # 刪除操作 + # 在雜湊表中刪除鍵值對 (key, value) + hmap.pop(10583) + ``` + +=== "C++" + + ```cpp title="hash_map.cpp" + /* 初始化雜湊表 */ + unordered_map map; + + /* 新增操作 */ + // 在雜湊表中新增鍵值對 (key, value) + map[12836] = "小哈"; + map[15937] = "小囉"; + map[16750] = "小算"; + map[13276] = "小法"; + map[10583] = "小鴨"; + + /* 查詢操作 */ + // 向雜湊表中輸入鍵 key ,得到值 value + string name = map[15937]; + + /* 刪除操作 */ + // 在雜湊表中刪除鍵值對 (key, value) + map.erase(10583); + ``` + +=== "Java" + + ```java title="hash_map.java" + /* 初始化雜湊表 */ + Map map = new HashMap<>(); + + /* 新增操作 */ + // 在雜湊表中新增鍵值對 (key, value) + map.put(12836, "小哈"); + map.put(15937, "小囉"); + map.put(16750, "小算"); + map.put(13276, "小法"); + map.put(10583, "小鴨"); + + /* 查詢操作 */ + // 向雜湊表中輸入鍵 key ,得到值 value + String name = map.get(15937); + + /* 刪除操作 */ + // 在雜湊表中刪除鍵值對 (key, value) + map.remove(10583); + ``` + +=== "C#" + + ```csharp title="hash_map.cs" + /* 初始化雜湊表 */ + Dictionary map = new() { + /* 新增操作 */ + // 在雜湊表中新增鍵值對 (key, value) + { 12836, "小哈" }, + { 15937, "小囉" }, + { 16750, "小算" }, + { 13276, "小法" }, + { 10583, "小鴨" } + }; + + /* 查詢操作 */ + // 向雜湊表中輸入鍵 key ,得到值 value + string name = map[15937]; + + /* 刪除操作 */ + // 在雜湊表中刪除鍵值對 (key, value) + map.Remove(10583); + ``` + +=== "Go" + + ```go title="hash_map_test.go" + /* 初始化雜湊表 */ + hmap := make(map[int]string) + + /* 新增操作 */ + // 在雜湊表中新增鍵值對 (key, value) + hmap[12836] = "小哈" + hmap[15937] = "小囉" + hmap[16750] = "小算" + hmap[13276] = "小法" + hmap[10583] = "小鴨" + + /* 查詢操作 */ + // 向雜湊表中輸入鍵 key ,得到值 value + name := hmap[15937] + + /* 刪除操作 */ + // 在雜湊表中刪除鍵值對 (key, value) + delete(hmap, 10583) + ``` + +=== "Swift" + + ```swift title="hash_map.swift" + /* 初始化雜湊表 */ + var map: [Int: String] = [:] + + /* 新增操作 */ + // 在雜湊表中新增鍵值對 (key, value) + map[12836] = "小哈" + map[15937] = "小囉" + map[16750] = "小算" + map[13276] = "小法" + map[10583] = "小鴨" + + /* 查詢操作 */ + // 向雜湊表中輸入鍵 key ,得到值 value + let name = map[15937]! + + /* 刪除操作 */ + // 在雜湊表中刪除鍵值對 (key, value) + map.removeValue(forKey: 10583) + ``` + +=== "JS" + + ```javascript title="hash_map.js" + /* 初始化雜湊表 */ + const map = new Map(); + /* 新增操作 */ + // 在雜湊表中新增鍵值對 (key, value) + map.set(12836, '小哈'); + map.set(15937, '小囉'); + map.set(16750, '小算'); + map.set(13276, '小法'); + map.set(10583, '小鴨'); + + /* 查詢操作 */ + // 向雜湊表中輸入鍵 key ,得到值 value + let name = map.get(15937); + + /* 刪除操作 */ + // 在雜湊表中刪除鍵值對 (key, value) + map.delete(10583); + ``` + +=== "TS" + + ```typescript title="hash_map.ts" + /* 初始化雜湊表 */ + const map = new Map(); + /* 新增操作 */ + // 在雜湊表中新增鍵值對 (key, value) + map.set(12836, '小哈'); + map.set(15937, '小囉'); + map.set(16750, '小算'); + map.set(13276, '小法'); + map.set(10583, '小鴨'); + console.info('\n新增完成後,雜湊表為\nKey -> Value'); + console.info(map); + + /* 查詢操作 */ + // 向雜湊表中輸入鍵 key ,得到值 value + let name = map.get(15937); + console.info('\n輸入學號 15937 ,查詢到姓名 ' + name); + + /* 刪除操作 */ + // 在雜湊表中刪除鍵值對 (key, value) + map.delete(10583); + console.info('\n刪除 10583 後,雜湊表為\nKey -> Value'); + console.info(map); + ``` + +=== "Dart" + + ```dart title="hash_map.dart" + /* 初始化雜湊表 */ + Map map = {}; + + /* 新增操作 */ + // 在雜湊表中新增鍵值對 (key, value) + map[12836] = "小哈"; + map[15937] = "小囉"; + map[16750] = "小算"; + map[13276] = "小法"; + map[10583] = "小鴨"; + + /* 查詢操作 */ + // 向雜湊表中輸入鍵 key ,得到值 value + String name = map[15937]; + + /* 刪除操作 */ + // 在雜湊表中刪除鍵值對 (key, value) + map.remove(10583); + ``` + +=== "Rust" + + ```rust title="hash_map.rs" + use std::collections::HashMap; + + /* 初始化雜湊表 */ + let mut map: HashMap = HashMap::new(); + + /* 新增操作 */ + // 在雜湊表中新增鍵值對 (key, value) + map.insert(12836, "小哈".to_string()); + map.insert(15937, "小囉".to_string()); + map.insert(16750, "小算".to_string()); + map.insert(13279, "小法".to_string()); + map.insert(10583, "小鴨".to_string()); + + /* 查詢操作 */ + // 向雜湊表中輸入鍵 key ,得到值 value + let _name: Option<&String> = map.get(&15937); + + /* 刪除操作 */ + // 在雜湊表中刪除鍵值對 (key, value) + let _removed_value: Option = map.remove(&10583); + ``` + +=== "C" + + ```c title="hash_map.c" + // C 未提供內建雜湊表 + ``` + +=== "Kotlin" + + ```kotlin title="hash_map.kt" + /* 初始化雜湊表 */ + val map = HashMap() + + /* 新增操作 */ + // 在雜湊表中新增鍵值對 (key, value) + map[12836] = "小哈" + map[15937] = "小囉" + map[16750] = "小算" + map[13276] = "小法" + map[10583] = "小鴨" + + /* 查詢操作 */ + // 向雜湊表中輸入鍵 key ,得到值 value + val name = map[15937] + + /* 刪除操作 */ + // 在雜湊表中刪除鍵值對 (key, value) + map.remove(10583) + ``` + +=== "Ruby" + + ```ruby title="hash_map.rb" + + ``` + +=== "Zig" + + ```zig title="hash_map.zig" + + ``` + +??? pythontutor "視覺化執行" + +
+ + +雜湊表有三種常用的走訪方式:走訪鍵值對、走訪鍵和走訪值。示例程式碼如下: + +=== "Python" + + ```python title="hash_map.py" + # 走訪雜湊表 + # 走訪鍵值對 key->value + for key, value in hmap.items(): + print(key, "->", value) + # 單獨走訪鍵 key + for key in hmap.keys(): + print(key) + # 單獨走訪值 value + for value in hmap.values(): + print(value) + ``` + +=== "C++" + + ```cpp title="hash_map.cpp" + /* 走訪雜湊表 */ + // 走訪鍵值對 key->value + for (auto kv: map) { + cout << kv.first << " -> " << kv.second << endl; + } + // 使用迭代器走訪 key->value + for (auto iter = map.begin(); iter != map.end(); iter++) { + cout << iter->first << "->" << iter->second << endl; + } + ``` + +=== "Java" + + ```java title="hash_map.java" + /* 走訪雜湊表 */ + // 走訪鍵值對 key->value + for (Map.Entry kv: map.entrySet()) { + System.out.println(kv.getKey() + " -> " + kv.getValue()); + } + // 單獨走訪鍵 key + for (int key: map.keySet()) { + System.out.println(key); + } + // 單獨走訪值 value + for (String val: map.values()) { + System.out.println(val); + } + ``` + +=== "C#" + + ```csharp title="hash_map.cs" + /* 走訪雜湊表 */ + // 走訪鍵值對 Key->Value + foreach (var kv in map) { + Console.WriteLine(kv.Key + " -> " + kv.Value); + } + // 單獨走訪鍵 key + foreach (int key in map.Keys) { + Console.WriteLine(key); + } + // 單獨走訪值 value + foreach (string val in map.Values) { + Console.WriteLine(val); + } + ``` + +=== "Go" + + ```go title="hash_map_test.go" + /* 走訪雜湊表 */ + // 走訪鍵值對 key->value + for key, value := range hmap { + fmt.Println(key, "->", value) + } + // 單獨走訪鍵 key + for key := range hmap { + fmt.Println(key) + } + // 單獨走訪值 value + for _, value := range hmap { + fmt.Println(value) + } + ``` + +=== "Swift" + + ```swift title="hash_map.swift" + /* 走訪雜湊表 */ + // 走訪鍵值對 Key->Value + for (key, value) in map { + print("\(key) -> \(value)") + } + // 單獨走訪鍵 Key + for key in map.keys { + print(key) + } + // 單獨走訪值 Value + for value in map.values { + print(value) + } + ``` + +=== "JS" + + ```javascript title="hash_map.js" + /* 走訪雜湊表 */ + console.info('\n走訪鍵值對 Key->Value'); + for (const [k, v] of map.entries()) { + console.info(k + ' -> ' + v); + } + console.info('\n單獨走訪鍵 Key'); + for (const k of map.keys()) { + console.info(k); + } + console.info('\n單獨走訪值 Value'); + for (const v of map.values()) { + console.info(v); + } + ``` + +=== "TS" + + ```typescript title="hash_map.ts" + /* 走訪雜湊表 */ + console.info('\n走訪鍵值對 Key->Value'); + for (const [k, v] of map.entries()) { + console.info(k + ' -> ' + v); + } + console.info('\n單獨走訪鍵 Key'); + for (const k of map.keys()) { + console.info(k); + } + console.info('\n單獨走訪值 Value'); + for (const v of map.values()) { + console.info(v); + } + ``` + +=== "Dart" + + ```dart title="hash_map.dart" + /* 走訪雜湊表 */ + // 走訪鍵值對 Key->Value + map.forEach((key, value) { + print('$key -> $value'); + }); + + // 單獨走訪鍵 Key + map.keys.forEach((key) { + print(key); + }); + + // 單獨走訪值 Value + map.values.forEach((value) { + print(value); + }); + ``` + +=== "Rust" + + ```rust title="hash_map.rs" + /* 走訪雜湊表 */ + // 走訪鍵值對 Key->Value + for (key, value) in &map { + println!("{key} -> {value}"); + } + + // 單獨走訪鍵 Key + for key in map.keys() { + println!("{key}"); + } + + // 單獨走訪值 Value + for value in map.values() { + println!("{value}"); + } + ``` + +=== "C" + + ```c title="hash_map.c" + // C 未提供內建雜湊表 + ``` + +=== "Kotlin" + + ```kotlin title="hash_map.kt" + /* 走訪雜湊表 */ + // 走訪鍵值對 key->value + for ((key, value) in map) { + println("$key -> $value") + } + // 單獨走訪鍵 key + for (key in map.keys) { + println(key) + } + // 單獨走訪值 value + for (_val in map.values) { + println(_val) + } + ``` + +=== "Ruby" + + ```ruby title="hash_map.rb" + + ``` + +=== "Zig" + + ```zig title="hash_map.zig" + + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 6.1.2   雜湊表簡單實現 + +我們先考慮最簡單的情況,**僅用一個陣列來實現雜湊表**。在雜湊表中,我們將陣列中的每個空位稱為桶(bucket),每個桶可儲存一個鍵值對。因此,查詢操作就是找到 `key` 對應的桶,並在桶中獲取 `value` 。 + +那麼,如何基於 `key` 定位對應的桶呢?這是透過雜湊函式(hash function)實現的。雜湊函式的作用是將一個較大的輸入空間對映到一個較小的輸出空間。在雜湊表中,輸入空間是所有 `key` ,輸出空間是所有桶(陣列索引)。換句話說,輸入一個 `key` ,**我們可以透過雜湊函式得到該 `key` 對應的鍵值對在陣列中的儲存位置**。 + +輸入一個 `key` ,雜湊函式的計算過程分為以下兩步。 + +1. 透過某種雜湊演算法 `hash()` 計算得到雜湊值。 +2. 將雜湊值對桶數量(陣列長度)`capacity` 取模,從而獲取該 `key` 對應的陣列索引 `index` 。 + +```shell +index = hash(key) % capacity +``` + +隨後,我們就可以利用 `index` 在雜湊表中訪問對應的桶,從而獲取 `value` 。 + +設陣列長度 `capacity = 100`、雜湊演算法 `hash(key) = key` ,易得雜湊函式為 `key % 100` 。圖 6-2 以 `key` 學號和 `value` 姓名為例,展示了雜湊函式的工作原理。 + +![雜湊函式工作原理](hash_map.assets/hash_function.png){ class="animation-figure" } + +

圖 6-2   雜湊函式工作原理

+ +以下程式碼實現了一個簡單雜湊表。其中,我們將 `key` 和 `value` 封裝成一個類別 `Pair` ,以表示鍵值對。 + +=== "Python" + + ```python title="array_hash_map.py" + class Pair: + """鍵值對""" + + def __init__(self, key: int, val: str): + self.key = key + self.val = val + + class ArrayHashMap: + """基於陣列實現的雜湊表""" + + def __init__(self): + """建構子""" + # 初始化陣列,包含 100 個桶 + self.buckets: list[Pair | None] = [None] * 100 + + def hash_func(self, key: int) -> int: + """雜湊函式""" + index = key % 100 + return index + + def get(self, key: int) -> str: + """查詢操作""" + index: int = self.hash_func(key) + pair: Pair = self.buckets[index] + if pair is None: + return None + return pair.val + + def put(self, key: int, val: str): + """新增操作""" + pair = Pair(key, val) + index: int = self.hash_func(key) + self.buckets[index] = pair + + def remove(self, key: int): + """刪除操作""" + index: int = self.hash_func(key) + # 置為 None ,代表刪除 + self.buckets[index] = None + + def entry_set(self) -> list[Pair]: + """獲取所有鍵值對""" + result: list[Pair] = [] + for pair in self.buckets: + if pair is not None: + result.append(pair) + return result + + def key_set(self) -> list[int]: + """獲取所有鍵""" + result = [] + for pair in self.buckets: + if pair is not None: + result.append(pair.key) + return result + + def value_set(self) -> list[str]: + """獲取所有值""" + result = [] + for pair in self.buckets: + if pair is not None: + result.append(pair.val) + return result + + def print(self): + """列印雜湊表""" + for pair in self.buckets: + if pair is not None: + print(pair.key, "->", pair.val) + ``` + +=== "C++" + + ```cpp title="array_hash_map.cpp" + /* 鍵值對 */ + struct Pair { + public: + int key; + string val; + Pair(int key, string val) { + this->key = key; + this->val = val; + } + }; + + /* 基於陣列實現的雜湊表 */ + class ArrayHashMap { + private: + vector buckets; + + public: + ArrayHashMap() { + // 初始化陣列,包含 100 個桶 + buckets = vector(100); + } + + ~ArrayHashMap() { + // 釋放記憶體 + for (const auto &bucket : buckets) { + delete bucket; + } + buckets.clear(); + } + + /* 雜湊函式 */ + int hashFunc(int key) { + int index = key % 100; + return index; + } + + /* 查詢操作 */ + string get(int key) { + int index = hashFunc(key); + Pair *pair = buckets[index]; + if (pair == nullptr) + return ""; + return pair->val; + } + + /* 新增操作 */ + void put(int key, string val) { + Pair *pair = new Pair(key, val); + int index = hashFunc(key); + buckets[index] = pair; + } + + /* 刪除操作 */ + void remove(int key) { + int index = hashFunc(key); + // 釋放記憶體並置為 nullptr + delete buckets[index]; + buckets[index] = nullptr; + } + + /* 獲取所有鍵值對 */ + vector pairSet() { + vector pairSet; + for (Pair *pair : buckets) { + if (pair != nullptr) { + pairSet.push_back(pair); + } + } + return pairSet; + } + + /* 獲取所有鍵 */ + vector keySet() { + vector keySet; + for (Pair *pair : buckets) { + if (pair != nullptr) { + keySet.push_back(pair->key); + } + } + return keySet; + } + + /* 獲取所有值 */ + vector valueSet() { + vector valueSet; + for (Pair *pair : buckets) { + if (pair != nullptr) { + valueSet.push_back(pair->val); + } + } + return valueSet; + } + + /* 列印雜湊表 */ + void print() { + for (Pair *kv : pairSet()) { + cout << kv->key << " -> " << kv->val << endl; + } + } + }; + ``` + +=== "Java" + + ```java title="array_hash_map.java" + /* 鍵值對 */ + class Pair { + public int key; + public String val; + + public Pair(int key, String val) { + this.key = key; + this.val = val; + } + } + + /* 基於陣列實現的雜湊表 */ + class ArrayHashMap { + private List buckets; + + public ArrayHashMap() { + // 初始化陣列,包含 100 個桶 + buckets = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + buckets.add(null); + } + } + + /* 雜湊函式 */ + private int hashFunc(int key) { + int index = key % 100; + return index; + } + + /* 查詢操作 */ + public String get(int key) { + int index = hashFunc(key); + Pair pair = buckets.get(index); + if (pair == null) + return null; + return pair.val; + } + + /* 新增操作 */ + public void put(int key, String val) { + Pair pair = new Pair(key, val); + int index = hashFunc(key); + buckets.set(index, pair); + } + + /* 刪除操作 */ + public void remove(int key) { + int index = hashFunc(key); + // 置為 null ,代表刪除 + buckets.set(index, null); + } + + /* 獲取所有鍵值對 */ + public List pairSet() { + List pairSet = new ArrayList<>(); + for (Pair pair : buckets) { + if (pair != null) + pairSet.add(pair); + } + return pairSet; + } + + /* 獲取所有鍵 */ + public List keySet() { + List keySet = new ArrayList<>(); + for (Pair pair : buckets) { + if (pair != null) + keySet.add(pair.key); + } + return keySet; + } + + /* 獲取所有值 */ + public List valueSet() { + List valueSet = new ArrayList<>(); + for (Pair pair : buckets) { + if (pair != null) + valueSet.add(pair.val); + } + return valueSet; + } + + /* 列印雜湊表 */ + public void print() { + for (Pair kv : pairSet()) { + System.out.println(kv.key + " -> " + kv.val); + } + } + } + ``` + +=== "C#" + + ```csharp title="array_hash_map.cs" + /* 鍵值對 int->string */ + class Pair(int key, string val) { + public int key = key; + public string val = val; + } + + /* 基於陣列實現的雜湊表 */ + class ArrayHashMap { + List buckets; + public ArrayHashMap() { + // 初始化陣列,包含 100 個桶 + buckets = []; + for (int i = 0; i < 100; i++) { + buckets.Add(null); + } + } + + /* 雜湊函式 */ + int HashFunc(int key) { + int index = key % 100; + return index; + } + + /* 查詢操作 */ + public string? Get(int key) { + int index = HashFunc(key); + Pair? pair = buckets[index]; + if (pair == null) return null; + return pair.val; + } + + /* 新增操作 */ + public void Put(int key, string val) { + Pair pair = new(key, val); + int index = HashFunc(key); + buckets[index] = pair; + } + + /* 刪除操作 */ + public void Remove(int key) { + int index = HashFunc(key); + // 置為 null ,代表刪除 + buckets[index] = null; + } + + /* 獲取所有鍵值對 */ + public List PairSet() { + List pairSet = []; + foreach (Pair? pair in buckets) { + if (pair != null) + pairSet.Add(pair); + } + return pairSet; + } + + /* 獲取所有鍵 */ + public List KeySet() { + List keySet = []; + foreach (Pair? pair in buckets) { + if (pair != null) + keySet.Add(pair.key); + } + return keySet; + } + + /* 獲取所有值 */ + public List ValueSet() { + List valueSet = []; + foreach (Pair? pair in buckets) { + if (pair != null) + valueSet.Add(pair.val); + } + return valueSet; + } + + /* 列印雜湊表 */ + public void Print() { + foreach (Pair kv in PairSet()) { + Console.WriteLine(kv.key + " -> " + kv.val); + } + } + } + ``` + +=== "Go" + + ```go title="array_hash_map.go" + /* 鍵值對 */ + type pair struct { + key int + val string + } + + /* 基於陣列實現的雜湊表 */ + type arrayHashMap struct { + buckets []*pair + } + + /* 初始化雜湊表 */ + func newArrayHashMap() *arrayHashMap { + // 初始化陣列,包含 100 個桶 + buckets := make([]*pair, 100) + return &arrayHashMap{buckets: buckets} + } + + /* 雜湊函式 */ + func (a *arrayHashMap) hashFunc(key int) int { + index := key % 100 + return index + } + + /* 查詢操作 */ + func (a *arrayHashMap) get(key int) string { + index := a.hashFunc(key) + pair := a.buckets[index] + if pair == nil { + return "Not Found" + } + return pair.val + } + + /* 新增操作 */ + func (a *arrayHashMap) put(key int, val string) { + pair := &pair{key: key, val: val} + index := a.hashFunc(key) + a.buckets[index] = pair + } + + /* 刪除操作 */ + func (a *arrayHashMap) remove(key int) { + index := a.hashFunc(key) + // 置為 nil ,代表刪除 + a.buckets[index] = nil + } + + /* 獲取所有鍵對 */ + func (a *arrayHashMap) pairSet() []*pair { + var pairs []*pair + for _, pair := range a.buckets { + if pair != nil { + pairs = append(pairs, pair) + } + } + return pairs + } + + /* 獲取所有鍵 */ + func (a *arrayHashMap) keySet() []int { + var keys []int + for _, pair := range a.buckets { + if pair != nil { + keys = append(keys, pair.key) + } + } + return keys + } + + /* 獲取所有值 */ + func (a *arrayHashMap) valueSet() []string { + var values []string + for _, pair := range a.buckets { + if pair != nil { + values = append(values, pair.val) + } + } + return values + } + + /* 列印雜湊表 */ + func (a *arrayHashMap) print() { + for _, pair := range a.buckets { + if pair != nil { + fmt.Println(pair.key, "->", pair.val) + } + } + } + ``` + +=== "Swift" + + ```swift title="array_hash_map.swift" + /* 鍵值對 */ + class Pair: Equatable { + public var key: Int + public var val: String + + public init(key: Int, val: String) { + self.key = key + self.val = val + } + + public static func == (lhs: Pair, rhs: Pair) -> Bool { + lhs.key == rhs.key && lhs.val == rhs.val + } + } + + /* 基於陣列實現的雜湊表 */ + class ArrayHashMap { + private var buckets: [Pair?] + + init() { + // 初始化陣列,包含 100 個桶 + buckets = Array(repeating: nil, count: 100) + } + + /* 雜湊函式 */ + private func hashFunc(key: Int) -> Int { + let index = key % 100 + return index + } + + /* 查詢操作 */ + func get(key: Int) -> String? { + let index = hashFunc(key: key) + let pair = buckets[index] + return pair?.val + } + + /* 新增操作 */ + func put(key: Int, val: String) { + let pair = Pair(key: key, val: val) + let index = hashFunc(key: key) + buckets[index] = pair + } + + /* 刪除操作 */ + func remove(key: Int) { + let index = hashFunc(key: key) + // 置為 nil ,代表刪除 + buckets[index] = nil + } + + /* 獲取所有鍵值對 */ + func pairSet() -> [Pair] { + buckets.compactMap { $0 } + } + + /* 獲取所有鍵 */ + func keySet() -> [Int] { + buckets.compactMap { $0?.key } + } + + /* 獲取所有值 */ + func valueSet() -> [String] { + buckets.compactMap { $0?.val } + } + + /* 列印雜湊表 */ + func print() { + for pair in pairSet() { + Swift.print("\(pair.key) -> \(pair.val)") + } + } + } + ``` + +=== "JS" + + ```javascript title="array_hash_map.js" + /* 鍵值對 Number -> String */ + class Pair { + constructor(key, val) { + this.key = key; + this.val = val; + } + } + + /* 基於陣列實現的雜湊表 */ + class ArrayHashMap { + #buckets; + constructor() { + // 初始化陣列,包含 100 個桶 + this.#buckets = new Array(100).fill(null); + } + + /* 雜湊函式 */ + #hashFunc(key) { + return key % 100; + } + + /* 查詢操作 */ + get(key) { + let index = this.#hashFunc(key); + let pair = this.#buckets[index]; + if (pair === null) return null; + return pair.val; + } + + /* 新增操作 */ + set(key, val) { + let index = this.#hashFunc(key); + this.#buckets[index] = new Pair(key, val); + } + + /* 刪除操作 */ + delete(key) { + let index = this.#hashFunc(key); + // 置為 null ,代表刪除 + this.#buckets[index] = null; + } + + /* 獲取所有鍵值對 */ + entries() { + let arr = []; + for (let i = 0; i < this.#buckets.length; i++) { + if (this.#buckets[i]) { + arr.push(this.#buckets[i]); + } + } + return arr; + } + + /* 獲取所有鍵 */ + keys() { + let arr = []; + for (let i = 0; i < this.#buckets.length; i++) { + if (this.#buckets[i]) { + arr.push(this.#buckets[i].key); + } + } + return arr; + } + + /* 獲取所有值 */ + values() { + let arr = []; + for (let i = 0; i < this.#buckets.length; i++) { + if (this.#buckets[i]) { + arr.push(this.#buckets[i].val); + } + } + return arr; + } + + /* 列印雜湊表 */ + print() { + let pairSet = this.entries(); + for (const pair of pairSet) { + console.info(`${pair.key} -> ${pair.val}`); + } + } + } + ``` + +=== "TS" + + ```typescript title="array_hash_map.ts" + /* 鍵值對 Number -> String */ + class Pair { + public key: number; + public val: string; + + constructor(key: number, val: string) { + this.key = key; + this.val = val; + } + } + + /* 基於陣列實現的雜湊表 */ + class ArrayHashMap { + private readonly buckets: (Pair | null)[]; + + constructor() { + // 初始化陣列,包含 100 個桶 + this.buckets = new Array(100).fill(null); + } + + /* 雜湊函式 */ + private hashFunc(key: number): number { + return key % 100; + } + + /* 查詢操作 */ + public get(key: number): string | null { + let index = this.hashFunc(key); + let pair = this.buckets[index]; + if (pair === null) return null; + return pair.val; + } + + /* 新增操作 */ + public set(key: number, val: string) { + let index = this.hashFunc(key); + this.buckets[index] = new Pair(key, val); + } + + /* 刪除操作 */ + public delete(key: number) { + let index = this.hashFunc(key); + // 置為 null ,代表刪除 + this.buckets[index] = null; + } + + /* 獲取所有鍵值對 */ + public entries(): (Pair | null)[] { + let arr: (Pair | null)[] = []; + for (let i = 0; i < this.buckets.length; i++) { + if (this.buckets[i]) { + arr.push(this.buckets[i]); + } + } + return arr; + } + + /* 獲取所有鍵 */ + public keys(): (number | undefined)[] { + let arr: (number | undefined)[] = []; + for (let i = 0; i < this.buckets.length; i++) { + if (this.buckets[i]) { + arr.push(this.buckets[i].key); + } + } + return arr; + } + + /* 獲取所有值 */ + public values(): (string | undefined)[] { + let arr: (string | undefined)[] = []; + for (let i = 0; i < this.buckets.length; i++) { + if (this.buckets[i]) { + arr.push(this.buckets[i].val); + } + } + return arr; + } + + /* 列印雜湊表 */ + public print() { + let pairSet = this.entries(); + for (const pair of pairSet) { + console.info(`${pair.key} -> ${pair.val}`); + } + } + } + ``` + +=== "Dart" + + ```dart title="array_hash_map.dart" + /* 鍵值對 */ + class Pair { + int key; + String val; + Pair(this.key, this.val); + } + + /* 基於陣列實現的雜湊表 */ + class ArrayHashMap { + late List _buckets; + + ArrayHashMap() { + // 初始化陣列,包含 100 個桶 + _buckets = List.filled(100, null); + } + + /* 雜湊函式 */ + int _hashFunc(int key) { + final int index = key % 100; + return index; + } + + /* 查詢操作 */ + String? get(int key) { + final int index = _hashFunc(key); + final Pair? pair = _buckets[index]; + if (pair == null) { + return null; + } + return pair.val; + } + + /* 新增操作 */ + void put(int key, String val) { + final Pair pair = Pair(key, val); + final int index = _hashFunc(key); + _buckets[index] = pair; + } + + /* 刪除操作 */ + void remove(int key) { + final int index = _hashFunc(key); + _buckets[index] = null; + } + + /* 獲取所有鍵值對 */ + List pairSet() { + List pairSet = []; + for (final Pair? pair in _buckets) { + if (pair != null) { + pairSet.add(pair); + } + } + return pairSet; + } + + /* 獲取所有鍵 */ + List keySet() { + List keySet = []; + for (final Pair? pair in _buckets) { + if (pair != null) { + keySet.add(pair.key); + } + } + return keySet; + } + + /* 獲取所有值 */ + List values() { + List valueSet = []; + for (final Pair? pair in _buckets) { + if (pair != null) { + valueSet.add(pair.val); + } + } + return valueSet; + } + + /* 列印雜湊表 */ + void printHashMap() { + for (final Pair kv in pairSet()) { + print("${kv.key} -> ${kv.val}"); + } + } + } + ``` + +=== "Rust" + + ```rust title="array_hash_map.rs" + /* 鍵值對 */ + #[derive(Debug, Clone, PartialEq)] + pub struct Pair { + pub key: i32, + pub val: String, + } + + /* 基於陣列實現的雜湊表 */ + pub struct ArrayHashMap { + buckets: Vec>, + } + + impl ArrayHashMap { + pub fn new() -> ArrayHashMap { + // 初始化陣列,包含 100 個桶 + Self { + buckets: vec![None; 100], + } + } + + /* 雜湊函式 */ + fn hash_func(&self, key: i32) -> usize { + key as usize % 100 + } + + /* 查詢操作 */ + pub fn get(&self, key: i32) -> Option<&String> { + let index = self.hash_func(key); + self.buckets[index].as_ref().map(|pair| &pair.val) + } + + /* 新增操作 */ + pub fn put(&mut self, key: i32, val: &str) { + let index = self.hash_func(key); + self.buckets[index] = Some(Pair { + key, + val: val.to_string(), + }); + } + + /* 刪除操作 */ + pub fn remove(&mut self, key: i32) { + let index = self.hash_func(key); + // 置為 None ,代表刪除 + self.buckets[index] = None; + } + + /* 獲取所有鍵值對 */ + pub fn entry_set(&self) -> Vec<&Pair> { + self.buckets + .iter() + .filter_map(|pair| pair.as_ref()) + .collect() + } + + /* 獲取所有鍵 */ + pub fn key_set(&self) -> Vec<&i32> { + self.buckets + .iter() + .filter_map(|pair| pair.as_ref().map(|pair| &pair.key)) + .collect() + } + + /* 獲取所有值 */ + pub fn value_set(&self) -> Vec<&String> { + self.buckets + .iter() + .filter_map(|pair| pair.as_ref().map(|pair| &pair.val)) + .collect() + } + + /* 列印雜湊表 */ + pub fn print(&self) { + for pair in self.entry_set() { + println!("{} -> {}", pair.key, pair.val); + } + } + } + ``` + +=== "C" + + ```c title="array_hash_map.c" + /* 鍵值對 int->string */ + typedef struct { + int key; + char *val; + } Pair; + + /* 基於陣列實現的雜湊表 */ + typedef struct { + Pair *buckets[HASHTABLE_CAPACITY]; + } ArrayHashMap; + + /* 建構子 */ + ArrayHashMap *newArrayHashMap() { + ArrayHashMap *hmap = malloc(sizeof(ArrayHashMap)); + return hmap; + } + + /* 析構函式 */ + void delArrayHashMap(ArrayHashMap *hmap) { + for (int i = 0; i < HASHTABLE_CAPACITY; i++) { + if (hmap->buckets[i] != NULL) { + free(hmap->buckets[i]->val); + free(hmap->buckets[i]); + } + } + free(hmap); + } + + /* 新增操作 */ + void put(ArrayHashMap *hmap, const int key, const char *val) { + Pair *Pair = malloc(sizeof(Pair)); + Pair->key = key; + Pair->val = malloc(strlen(val) + 1); + strcpy(Pair->val, val); + + int index = hashFunc(key); + hmap->buckets[index] = Pair; + } + + /* 刪除操作 */ + void removeItem(ArrayHashMap *hmap, const int key) { + int index = hashFunc(key); + free(hmap->buckets[index]->val); + free(hmap->buckets[index]); + hmap->buckets[index] = NULL; + } + + /* 獲取所有鍵值對 */ + void pairSet(ArrayHashMap *hmap, MapSet *set) { + Pair *entries; + int i = 0, index = 0; + int total = 0; + /* 統計有效鍵值對數量 */ + for (i = 0; i < HASHTABLE_CAPACITY; i++) { + if (hmap->buckets[i] != NULL) { + total++; + } + } + entries = malloc(sizeof(Pair) * total); + for (i = 0; i < HASHTABLE_CAPACITY; i++) { + if (hmap->buckets[i] != NULL) { + entries[index].key = hmap->buckets[i]->key; + entries[index].val = malloc(strlen(hmap->buckets[i]->val) + 1); + strcpy(entries[index].val, hmap->buckets[i]->val); + index++; + } + } + set->set = entries; + set->len = total; + } + + /* 獲取所有鍵 */ + void keySet(ArrayHashMap *hmap, MapSet *set) { + int *keys; + int i = 0, index = 0; + int total = 0; + /* 統計有效鍵值對數量 */ + for (i = 0; i < HASHTABLE_CAPACITY; i++) { + if (hmap->buckets[i] != NULL) { + total++; + } + } + keys = malloc(total * sizeof(int)); + for (i = 0; i < HASHTABLE_CAPACITY; i++) { + if (hmap->buckets[i] != NULL) { + keys[index] = hmap->buckets[i]->key; + index++; + } + } + set->set = keys; + set->len = total; + } + + /* 獲取所有值 */ + void valueSet(ArrayHashMap *hmap, MapSet *set) { + char **vals; + int i = 0, index = 0; + int total = 0; + /* 統計有效鍵值對數量 */ + for (i = 0; i < HASHTABLE_CAPACITY; i++) { + if (hmap->buckets[i] != NULL) { + total++; + } + } + vals = malloc(total * sizeof(char *)); + for (i = 0; i < HASHTABLE_CAPACITY; i++) { + if (hmap->buckets[i] != NULL) { + vals[index] = hmap->buckets[i]->val; + index++; + } + } + set->set = vals; + set->len = total; + } + + /* 列印雜湊表 */ + void print(ArrayHashMap *hmap) { + int i; + MapSet set; + pairSet(hmap, &set); + Pair *entries = (Pair *)set.set; + for (i = 0; i < set.len; i++) { + printf("%d -> %s\n", entries[i].key, entries[i].val); + } + free(set.set); + } + ``` + +=== "Kotlin" + + ```kotlin title="array_hash_map.kt" + /* 鍵值對 */ + class Pair( + var key: Int, + var value: String + ) + + /* 基於陣列實現的雜湊表 */ + class ArrayHashMap { + private val buckets = arrayOfNulls(100) + + init { + // 初始化陣列,包含 100 個桶 + for (i in 0..<100) { + buckets[i] = null + } + } + + /* 雜湊函式 */ + fun hashFunc(key: Int): Int { + val index = key % 100 + return index + } + + /* 查詢操作 */ + fun get(key: Int): String? { + val index = hashFunc(key) + val pair = buckets[index] ?: return null + return pair.value + } + + /* 新增操作 */ + fun put(key: Int, value: String) { + val pair = Pair(key, value) + val index = hashFunc(key) + buckets[index] = pair + } + + /* 刪除操作 */ + fun remove(key: Int) { + val index = hashFunc(key) + // 置為 null ,代表刪除 + buckets[index] = null + } + + /* 獲取所有鍵值對 */ + fun pairSet(): MutableList { + val pairSet = ArrayList() + for (pair in buckets) { + if (pair != null) pairSet.add(pair) + } + return pairSet + } + + /* 獲取所有鍵 */ + fun keySet(): MutableList { + val keySet = ArrayList() + for (pair in buckets) { + if (pair != null) keySet.add(pair.key) + } + return keySet + } + + /* 獲取所有值 */ + fun valueSet(): MutableList { + val valueSet = ArrayList() + for (pair in buckets) { + pair?.let { valueSet.add(it.value) } + } + return valueSet + } + + /* 列印雜湊表 */ + fun print() { + for (kv in pairSet()) { + val key = kv.key + val value = kv.value + println("${key}->${value}") + } + } + } + + /* 基於陣列實現的雜湊表 */ + class ArrayHashMap { + private val buckets = arrayOfNulls(100) + + init { + // 初始化陣列,包含 100 個桶 + for (i in 0..<100) { + buckets[i] = null + } + } + + /* 雜湊函式 */ + fun hashFunc(key: Int): Int { + val index = key % 100 + return index + } + + /* 查詢操作 */ + fun get(key: Int): String? { + val index = hashFunc(key) + val pair = buckets[index] ?: return null + return pair.value + } + + /* 新增操作 */ + fun put(key: Int, value: String) { + val pair = Pair(key, value) + val index = hashFunc(key) + buckets[index] = pair + } + + /* 刪除操作 */ + fun remove(key: Int) { + val index = hashFunc(key) + // 置為 null ,代表刪除 + buckets[index] = null + } + + /* 獲取所有鍵值對 */ + fun pairSet(): MutableList { + val pairSet = ArrayList() + for (pair in buckets) { + if (pair != null) pairSet.add(pair) + } + return pairSet + } + + /* 獲取所有鍵 */ + fun keySet(): MutableList { + val keySet = ArrayList() + for (pair in buckets) { + if (pair != null) keySet.add(pair.key) + } + return keySet + } + + /* 獲取所有值 */ + fun valueSet(): MutableList { + val valueSet = ArrayList() + for (pair in buckets) { + pair?.let { valueSet.add(it.value) } + } + return valueSet + } + + /* 列印雜湊表 */ + fun print() { + for (kv in pairSet()) { + val key = kv.key + val value = kv.value + println("${key}->${value}") + } + } + } + ``` + +=== "Ruby" + + ```ruby title="array_hash_map.rb" + [class]{Pair}-[func]{} + + [class]{ArrayHashMap}-[func]{} + ``` + +=== "Zig" + + ```zig title="array_hash_map.zig" + // 鍵值對 + const Pair = struct { + key: usize = undefined, + val: []const u8 = undefined, + + pub fn init(key: usize, val: []const u8) Pair { + return Pair { + .key = key, + .val = val, + }; + } + }; + + // 基於陣列實現的雜湊表 + fn ArrayHashMap(comptime T: type) type { + return struct { + bucket: ?std.ArrayList(?T) = null, + mem_allocator: std.mem.Allocator = undefined, + + const Self = @This(); + + // 建構子 + pub fn init(self: *Self, allocator: std.mem.Allocator) !void { + self.mem_allocator = allocator; + // 初始化一個長度為 100 的桶(陣列) + self.bucket = std.ArrayList(?T).init(self.mem_allocator); + var i: i32 = 0; + while (i < 100) : (i += 1) { + try self.bucket.?.append(null); + } + } + + // 析構函式 + pub fn deinit(self: *Self) void { + if (self.bucket != null) self.bucket.?.deinit(); + } + + // 雜湊函式 + fn hashFunc(key: usize) usize { + var index = key % 100; + return index; + } + + // 查詢操作 + pub fn get(self: *Self, key: usize) []const u8 { + var index = hashFunc(key); + var pair = self.bucket.?.items[index]; + return pair.?.val; + } + + // 新增操作 + pub fn put(self: *Self, key: usize, val: []const u8) !void { + var pair = Pair.init(key, val); + var index = hashFunc(key); + self.bucket.?.items[index] = pair; + } + + // 刪除操作 + pub fn remove(self: *Self, key: usize) !void { + var index = hashFunc(key); + // 置為 null ,代表刪除 + self.bucket.?.items[index] = null; + } + + // 獲取所有鍵值對 + pub fn pairSet(self: *Self) !std.ArrayList(T) { + var entry_set = std.ArrayList(T).init(self.mem_allocator); + for (self.bucket.?.items) |item| { + if (item == null) continue; + try entry_set.append(item.?); + } + return entry_set; + } + + // 獲取所有鍵 + pub fn keySet(self: *Self) !std.ArrayList(usize) { + var key_set = std.ArrayList(usize).init(self.mem_allocator); + for (self.bucket.?.items) |item| { + if (item == null) continue; + try key_set.append(item.?.key); + } + return key_set; + } + + // 獲取所有值 + pub fn valueSet(self: *Self) !std.ArrayList([]const u8) { + var value_set = std.ArrayList([]const u8).init(self.mem_allocator); + for (self.bucket.?.items) |item| { + if (item == null) continue; + try value_set.append(item.?.val); + } + return value_set; + } + + // 列印雜湊表 + pub fn print(self: *Self) !void { + var entry_set = try self.pairSet(); + defer entry_set.deinit(); + for (entry_set.items) |item| { + std.debug.print("{} -> {s}\n", .{item.key, item.val}); + } + } + }; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 6.1.3   雜湊衝突與擴容 + +從本質上看,雜湊函式的作用是將所有 `key` 構成的輸入空間對映到陣列所有索引構成的輸出空間,而輸入空間往往遠大於輸出空間。因此,**理論上一定存在“多個輸入對應相同輸出”的情況**。 + +對於上述示例中的雜湊函式,當輸入的 `key` 後兩位相同時,雜湊函式的輸出結果也相同。例如,查詢學號為 12836 和 20336 的兩個學生時,我們得到: + +```shell +12836 % 100 = 36 +20336 % 100 = 36 +``` + +如圖 6-3 所示,兩個學號指向了同一個姓名,這顯然是不對的。我們將這種多個輸入對應同一輸出的情況稱為雜湊衝突(hash collision)。 + +![雜湊衝突示例](hash_map.assets/hash_collision.png){ class="animation-figure" } + +

圖 6-3   雜湊衝突示例

+ +容易想到,雜湊表容量 $n$ 越大,多個 `key` 被分配到同一個桶中的機率就越低,衝突就越少。因此,**我們可以透過擴容雜湊表來減少雜湊衝突**。 + +如圖 6-4 所示,擴容前鍵值對 `(136, A)` 和 `(236, D)` 發生衝突,擴容後衝突消失。 + +![雜湊表擴容](hash_map.assets/hash_table_reshash.png){ class="animation-figure" } + +

圖 6-4   雜湊表擴容

+ +類似於陣列擴容,雜湊表擴容需將所有鍵值對從原雜湊表遷移至新雜湊表,非常耗時;並且由於雜湊表容量 `capacity` 改變,我們需要透過雜湊函式來重新計算所有鍵值對的儲存位置,這進一步增加了擴容過程的計算開銷。為此,程式語言通常會預留足夠大的雜湊表容量,防止頻繁擴容。 + +負載因子(load factor)是雜湊表的一個重要概念,其定義為雜湊表的元素數量除以桶數量,用於衡量雜湊衝突的嚴重程度,**也常作為雜湊表擴容的觸發條件**。例如在 Java 中,當負載因子超過 $0.75$ 時,系統會將雜湊表擴容至原先的 $2$ 倍。 diff --git a/zh-Hant/docs/chapter_hashing/index.md b/zh-Hant/docs/chapter_hashing/index.md new file mode 100644 index 000000000..baa9ff089 --- /dev/null +++ b/zh-Hant/docs/chapter_hashing/index.md @@ -0,0 +1,21 @@ +--- +comments: true +icon: material/table-search +--- + +# 第 6 章   雜湊表 + +![雜湊表](../assets/covers/chapter_hashing.jpg){ 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/) diff --git a/zh-Hant/docs/chapter_hashing/summary.md b/zh-Hant/docs/chapter_hashing/summary.md new file mode 100644 index 000000000..5052b15fc --- /dev/null +++ b/zh-Hant/docs/chapter_hashing/summary.md @@ -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` ,在擴容後可能會被分配到多個桶中,從而實現雜湊衝突的緩解。 diff --git a/zh-Hant/docs/chapter_heap/build_heap.md b/zh-Hant/docs/chapter_heap/build_heap.md new file mode 100644 index 000000000..fbbf647a9 --- /dev/null +++ b/zh-Hant/docs/chapter_heap/build_heap.md @@ -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 nums) { + // 將串列元素原封不動新增進堆積 + maxHeap = nums; + // 堆積化除葉節點以外的其他所有節點 + for (int i = parent(size() - 1); i >= 0; i--) { + siftDown(i); + } + } + ``` + +=== "Java" + + ```java title="my_heap.java" + /* 建構子,根據輸入串列建堆積 */ + MaxHeap(List nums) { + // 將串列元素原封不動新增進堆積 + maxHeap = new ArrayList<>(nums); + // 堆積化除葉節點以外的其他所有節點 + for (int i = parent(size() - 1); i >= 0; i--) { + siftDown(i); + } + } + ``` + +=== "C#" + + ```csharp title="my_heap.cs" + /* 建構子,根據輸入串列建堆積 */ + MaxHeap(IEnumerable nums) { + // 將串列元素原封不動新增進堆積 + maxHeap = new List(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 nums) { + // 將串列元素原封不動新增進堆積 + _maxHeap = nums; + // 堆積化除葉節點以外的其他所有節點 + for (int i = _parent(size() - 1); i >= 0; i--) { + siftDown(i); + } + } + ``` + +=== "Rust" + + ```rust title="my_heap.rs" + /* 建構子,根據輸入串列建堆積 */ + fn new(nums: Vec) -> 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?) { + // 使用串列而非陣列,這樣無須考慮擴容問題 + // 將串列元素原封不動新增進堆積 + 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 "視覺化執行" + +
+ + +## 8.2.3   複雜度分析 + +下面,我們來嘗試推算第二種建堆積方法的時間複雜度。 + +- 假設完全二元樹的節點數量為 $n$ ,則葉節點數量為 $(n + 1) / 2$ ,其中 $/$ 為向下整除。因此需要堆積化的節點數量為 $(n - 1) / 2$ 。 +- 在從頂至底堆積化的過程中,每個節點最多堆積化到葉節點,因此最大迭代次數為二元樹高度 $\log n$ 。 + +將上述兩者相乘,可得到建堆積過程的時間複雜度為 $O(n \log n)$ 。**但這個估算結果並不準確,因為我們沒有考慮到二元樹底層節點數量遠多於頂層節點的性質**。 + +接下來我們來進行更為準確的計算。為了降低計算難度,假設給定一個節點數量為 $n$ 、高度為 $h$ 的“完美二元樹”,該假設不會影響計算結果的正確性。 + +![完美二元樹的各層節點數量](build_heap.assets/heapify_operations_count.png){ class="animation-figure" } + +

圖 8-5   完美二元樹的各層節點數量

+ +如圖 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)$ ,非常高效**。 diff --git a/zh-Hant/docs/chapter_heap/heap.md b/zh-Hant/docs/chapter_heap/heap.md new file mode 100644 index 000000000..91d71b220 --- /dev/null +++ b/zh-Hant/docs/chapter_heap/heap.md @@ -0,0 +1,1821 @@ +--- +comments: true +--- + +# 8.1   堆積 + +堆積(heap)是一種滿足特定條件的完全二元樹,主要可分為兩種型別,如圖 8-1 所示。 + +- 小頂堆積(min heap):任意節點的值 $\leq$ 其子節點的值。 +- 大頂堆積(max heap):任意節點的值 $\geq$ 其子節點的值。 + +![小頂堆積與大頂堆積](heap.assets/min_heap_and_max_heap.png){ class="animation-figure" } + +

圖 8-1   小頂堆積與大頂堆積

+ +堆積作為完全二元樹的一個特例,具有以下特性。 + +- 最底層節點靠左填充,其他層的節點都被填滿。 +- 我們將二元樹的根節點稱為“堆積頂”,將底層最靠右的節點稱為“堆積底”。 +- 對於大頂堆積(小頂堆積),堆積頂元素(根節點)的值是最大(最小)的。 + +## 8.1.1   堆積的常用操作 + +需要指出的是,許多程式語言提供的是優先佇列(priority queue),這是一種抽象的資料結構,定義為具有優先順序排序的佇列。 + +實際上,**堆積通常用於實現優先佇列,大頂堆積相當於元素按從大到小的順序出列的優先佇列**。從使用角度來看,我們可以將“優先佇列”和“堆積”看作等價的資料結構。因此,本書對兩者不做特別區分,統一稱作“堆積”。 + +堆積的常用操作見表 8-1 ,方法名需要根據程式語言來確定。 + +

表 8-1   堆積的操作效率

+ +
+ +| 方法名 | 描述 | 時間複雜度 | +| ----------- | ------------------------------------------------ | ----------- | +| `push()` | 元素入堆積 | $O(\log n)$ | +| `pop()` | 堆積頂元素出堆積 | $O(\log n)$ | +| `peek()` | 訪問堆積頂元素(對於大 / 小頂堆積分別為最大 / 小值) | $O(1)$ | +| `size()` | 獲取堆積的元素數量 | $O(1)$ | +| `isEmpty()` | 判斷堆積是否為空 | $O(1)$ | + +
+ +在實際應用中,我們可以直接使用程式語言提供的堆積類別(或優先佇列類別)。 + +類似於排序演算法中的“從小到大排列”和“從大到小排列”,我們可以透過設定一個 `flag` 或修改 `Comparator` 實現“小頂堆積”與“大頂堆積”之間的轉換。程式碼如下所示: + +=== "Python" + + ```python title="heap.py" + # 初始化小頂堆積 + min_heap, flag = [], 1 + # 初始化大頂堆積 + max_heap, flag = [], -1 + + # Python 的 heapq 模組預設實現小頂堆積 + # 考慮將“元素取負”後再入堆積,這樣就可以將大小關係顛倒,從而實現大頂堆積 + # 在本示例中,flag = 1 時對應小頂堆積,flag = -1 時對應大頂堆積 + + # 元素入堆積 + heapq.heappush(max_heap, flag * 1) + heapq.heappush(max_heap, flag * 3) + heapq.heappush(max_heap, flag * 2) + heapq.heappush(max_heap, flag * 5) + heapq.heappush(max_heap, flag * 4) + + # 獲取堆積頂元素 + peek: int = flag * max_heap[0] # 5 + + # 堆積頂元素出堆積 + # 出堆積元素會形成一個從大到小的序列 + val = flag * heapq.heappop(max_heap) # 5 + val = flag * heapq.heappop(max_heap) # 4 + val = flag * heapq.heappop(max_heap) # 3 + val = flag * heapq.heappop(max_heap) # 2 + val = flag * heapq.heappop(max_heap) # 1 + + # 獲取堆積大小 + size: int = len(max_heap) + + # 判斷堆積是否為空 + is_empty: bool = not max_heap + + # 輸入串列並建堆積 + min_heap: list[int] = [1, 3, 2, 5, 4] + heapq.heapify(min_heap) + ``` + +=== "C++" + + ```cpp title="heap.cpp" + /* 初始化堆積 */ + // 初始化小頂堆積 + priority_queue, greater> minHeap; + // 初始化大頂堆積 + priority_queue, less> maxHeap; + + /* 元素入堆積 */ + maxHeap.push(1); + maxHeap.push(3); + maxHeap.push(2); + maxHeap.push(5); + maxHeap.push(4); + + /* 獲取堆積頂元素 */ + int peek = maxHeap.top(); // 5 + + /* 堆積頂元素出堆積 */ + // 出堆積元素會形成一個從大到小的序列 + maxHeap.pop(); // 5 + maxHeap.pop(); // 4 + maxHeap.pop(); // 3 + maxHeap.pop(); // 2 + maxHeap.pop(); // 1 + + /* 獲取堆積大小 */ + int size = maxHeap.size(); + + /* 判斷堆積是否為空 */ + bool isEmpty = maxHeap.empty(); + + /* 輸入串列並建堆積 */ + vector input{1, 3, 2, 5, 4}; + priority_queue, greater> minHeap(input.begin(), input.end()); + ``` + +=== "Java" + + ```java title="heap.java" + /* 初始化堆積 */ + // 初始化小頂堆積 + Queue minHeap = new PriorityQueue<>(); + // 初始化大頂堆積(使用 lambda 表示式修改 Comparator 即可) + Queue maxHeap = new PriorityQueue<>((a, b) -> b - a); + + /* 元素入堆積 */ + maxHeap.offer(1); + maxHeap.offer(3); + maxHeap.offer(2); + maxHeap.offer(5); + maxHeap.offer(4); + + /* 獲取堆積頂元素 */ + int peek = maxHeap.peek(); // 5 + + /* 堆積頂元素出堆積 */ + // 出堆積元素會形成一個從大到小的序列 + peek = maxHeap.poll(); // 5 + peek = maxHeap.poll(); // 4 + peek = maxHeap.poll(); // 3 + peek = maxHeap.poll(); // 2 + peek = maxHeap.poll(); // 1 + + /* 獲取堆積大小 */ + int size = maxHeap.size(); + + /* 判斷堆積是否為空 */ + boolean isEmpty = maxHeap.isEmpty(); + + /* 輸入串列並建堆積 */ + minHeap = new PriorityQueue<>(Arrays.asList(1, 3, 2, 5, 4)); + ``` + +=== "C#" + + ```csharp title="heap.cs" + /* 初始化堆積 */ + // 初始化小頂堆積 + PriorityQueue minHeap = new(); + // 初始化大頂堆積(使用 lambda 表示式修改 Comparator 即可) + PriorityQueue maxHeap = new(Comparer.Create((x, y) => y - x)); + + /* 元素入堆積 */ + maxHeap.Enqueue(1, 1); + maxHeap.Enqueue(3, 3); + maxHeap.Enqueue(2, 2); + maxHeap.Enqueue(5, 5); + maxHeap.Enqueue(4, 4); + + /* 獲取堆積頂元素 */ + int peek = maxHeap.Peek();//5 + + /* 堆積頂元素出堆積 */ + // 出堆積元素會形成一個從大到小的序列 + peek = maxHeap.Dequeue(); // 5 + peek = maxHeap.Dequeue(); // 4 + peek = maxHeap.Dequeue(); // 3 + peek = maxHeap.Dequeue(); // 2 + peek = maxHeap.Dequeue(); // 1 + + /* 獲取堆積大小 */ + int size = maxHeap.Count; + + /* 判斷堆積是否為空 */ + bool isEmpty = maxHeap.Count == 0; + + /* 輸入串列並建堆積 */ + minHeap = new PriorityQueue([(1, 1), (3, 3), (2, 2), (5, 5), (4, 4)]); + ``` + +=== "Go" + + ```go title="heap.go" + // Go 語言中可以透過實現 heap.Interface 來構建整數大頂堆積 + // 實現 heap.Interface 需要同時實現 sort.Interface + type intHeap []any + + // Push heap.Interface 的方法,實現推入元素到堆積 + func (h *intHeap) Push(x any) { + // Push 和 Pop 使用 pointer receiver 作為參數 + // 因為它們不僅會對切片的內容進行調整,還會修改切片的長度。 + *h = append(*h, x.(int)) + } + + // Pop heap.Interface 的方法,實現彈出堆積頂元素 + func (h *intHeap) Pop() any { + // 待出堆積元素存放在最後 + last := (*h)[len(*h)-1] + *h = (*h)[:len(*h)-1] + return last + } + + // Len sort.Interface 的方法 + func (h *intHeap) Len() int { + return len(*h) + } + + // Less sort.Interface 的方法 + func (h *intHeap) Less(i, j int) bool { + // 如果實現小頂堆積,則需要調整為小於號 + return (*h)[i].(int) > (*h)[j].(int) + } + + // Swap sort.Interface 的方法 + func (h *intHeap) Swap(i, j int) { + (*h)[i], (*h)[j] = (*h)[j], (*h)[i] + } + + // Top 獲取堆積頂元素 + func (h *intHeap) Top() any { + return (*h)[0] + } + + /* Driver Code */ + func TestHeap(t *testing.T) { + /* 初始化堆積 */ + // 初始化大頂堆積 + maxHeap := &intHeap{} + heap.Init(maxHeap) + /* 元素入堆積 */ + // 呼叫 heap.Interface 的方法,來新增元素 + heap.Push(maxHeap, 1) + heap.Push(maxHeap, 3) + heap.Push(maxHeap, 2) + heap.Push(maxHeap, 4) + heap.Push(maxHeap, 5) + + /* 獲取堆積頂元素 */ + top := maxHeap.Top() + fmt.Printf("堆積頂元素為 %d\n", top) + + /* 堆積頂元素出堆積 */ + // 呼叫 heap.Interface 的方法,來移除元素 + heap.Pop(maxHeap) // 5 + heap.Pop(maxHeap) // 4 + heap.Pop(maxHeap) // 3 + heap.Pop(maxHeap) // 2 + heap.Pop(maxHeap) // 1 + + /* 獲取堆積大小 */ + size := len(*maxHeap) + fmt.Printf("堆積元素數量為 %d\n", size) + + /* 判斷堆積是否為空 */ + isEmpty := len(*maxHeap) == 0 + fmt.Printf("堆積是否為空 %t\n", isEmpty) + } + ``` + +=== "Swift" + + ```swift title="heap.swift" + /* 初始化堆積 */ + // Swift 的 Heap 型別同時支持最大堆積和最小堆積,且需要引入 swift-collections + var heap = Heap() + + /* 元素入堆積 */ + heap.insert(1) + heap.insert(3) + heap.insert(2) + heap.insert(5) + heap.insert(4) + + /* 獲取堆積頂元素 */ + var peek = heap.max()! + + /* 堆積頂元素出堆積 */ + peek = heap.removeMax() // 5 + peek = heap.removeMax() // 4 + peek = heap.removeMax() // 3 + peek = heap.removeMax() // 2 + peek = heap.removeMax() // 1 + + /* 獲取堆積大小 */ + let size = heap.count + + /* 判斷堆積是否為空 */ + let isEmpty = heap.isEmpty + + /* 輸入串列並建堆積 */ + let heap2 = Heap([1, 3, 2, 5, 4]) + ``` + +=== "JS" + + ```javascript title="heap.js" + // JavaScript 未提供內建 Heap 類別 + ``` + +=== "TS" + + ```typescript title="heap.ts" + // TypeScript 未提供內建 Heap 類別 + ``` + +=== "Dart" + + ```dart title="heap.dart" + // Dart 未提供內建 Heap 類別 + ``` + +=== "Rust" + + ```rust title="heap.rs" + use std::collections::BinaryHeap; + use std::cmp::Reverse; + + /* 初始化堆積 */ + // 初始化小頂堆積 + let mut min_heap = BinaryHeap::>::new(); + // 初始化大頂堆積 + let mut max_heap = BinaryHeap::new(); + + /* 元素入堆積 */ + max_heap.push(1); + max_heap.push(3); + max_heap.push(2); + max_heap.push(5); + max_heap.push(4); + + /* 獲取堆積頂元素 */ + let peek = max_heap.peek().unwrap(); // 5 + + /* 堆積頂元素出堆積 */ + // 出堆積元素會形成一個從大到小的序列 + let peek = max_heap.pop().unwrap(); // 5 + let peek = max_heap.pop().unwrap(); // 4 + let peek = max_heap.pop().unwrap(); // 3 + let peek = max_heap.pop().unwrap(); // 2 + let peek = max_heap.pop().unwrap(); // 1 + + /* 獲取堆積大小 */ + let size = max_heap.len(); + + /* 判斷堆積是否為空 */ + let is_empty = max_heap.is_empty(); + + /* 輸入串列並建堆積 */ + let min_heap = BinaryHeap::from(vec![Reverse(1), Reverse(3), Reverse(2), Reverse(5), Reverse(4)]); + ``` + +=== "C" + + ```c title="heap.c" + // C 未提供內建 Heap 類別 + ``` + +=== "Kotlin" + + ```kotlin title="heap.kt" + /* 初始化堆積 */ + // 初始化小頂堆積 + var minHeap = PriorityQueue() + // 初始化大頂堆積(使用 lambda 表示式修改 Comparator 即可) + val maxHeap = PriorityQueue { a: Int, b: Int -> b - a } + + /* 元素入堆積 */ + maxHeap.offer(1) + maxHeap.offer(3) + maxHeap.offer(2) + maxHeap.offer(5) + maxHeap.offer(4) + + /* 獲取堆積頂元素 */ + var peek = maxHeap.peek() // 5 + + /* 堆積頂元素出堆積 */ + // 出堆積元素會形成一個從大到小的序列 + peek = maxHeap.poll() // 5 + peek = maxHeap.poll() // 4 + peek = maxHeap.poll() // 3 + peek = maxHeap.poll() // 2 + peek = maxHeap.poll() // 1 + + /* 獲取堆積大小 */ + val size = maxHeap.size + + /* 判斷堆積是否為空 */ + val isEmpty = maxHeap.isEmpty() + + /* 輸入串列並建堆積 */ + minHeap = PriorityQueue(mutableListOf(1, 3, 2, 5, 4)) + ``` + +=== "Ruby" + + ```ruby title="heap.rb" + + ``` + +=== "Zig" + + ```zig title="heap.zig" + + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 8.1.2   堆積的實現 + +下文實現的是大頂堆積。若要將其轉換為小頂堆積,只需將所有大小邏輯判斷取逆(例如,將 $\geq$ 替換為 $\leq$ )。感興趣的讀者可以自行實現。 + +### 1.   堆積的儲存與表示 + +“二元樹”章節講過,完全二元樹非常適合用陣列來表示。由於堆積正是一種完全二元樹,**因此我們將採用陣列來儲存堆積**。 + +當使用陣列表示二元樹時,元素代表節點值,索引代表節點在二元樹中的位置。**節點指標透過索引對映公式來實現**。 + +如圖 8-2 所示,給定索引 $i$ ,其左子節點的索引為 $2i + 1$ ,右子節點的索引為 $2i + 2$ ,父節點的索引為 $(i - 1) / 2$(向下整除)。當索引越界時,表示空節點或節點不存在。 + +![堆積的表示與儲存](heap.assets/representation_of_heap.png){ class="animation-figure" } + +

圖 8-2   堆積的表示與儲存

+ +我們可以將索引對映公式封裝成函式,方便後續使用: + +=== "Python" + + ```python title="my_heap.py" + def left(self, i: int) -> int: + """獲取左子節點的索引""" + return 2 * i + 1 + + def right(self, i: int) -> int: + """獲取右子節點的索引""" + return 2 * i + 2 + + def parent(self, i: int) -> int: + """獲取父節點的索引""" + return (i - 1) // 2 # 向下整除 + ``` + +=== "C++" + + ```cpp title="my_heap.cpp" + /* 獲取左子節點的索引 */ + int left(int i) { + return 2 * i + 1; + } + + /* 獲取右子節點的索引 */ + int right(int i) { + return 2 * i + 2; + } + + /* 獲取父節點的索引 */ + int parent(int i) { + return (i - 1) / 2; // 向下整除 + } + ``` + +=== "Java" + + ```java title="my_heap.java" + /* 獲取左子節點的索引 */ + int left(int i) { + return 2 * i + 1; + } + + /* 獲取右子節點的索引 */ + int right(int i) { + return 2 * i + 2; + } + + /* 獲取父節點的索引 */ + int parent(int i) { + return (i - 1) / 2; // 向下整除 + } + ``` + +=== "C#" + + ```csharp title="my_heap.cs" + /* 獲取左子節點的索引 */ + int Left(int i) { + return 2 * i + 1; + } + + /* 獲取右子節點的索引 */ + int Right(int i) { + return 2 * i + 2; + } + + /* 獲取父節點的索引 */ + int Parent(int i) { + return (i - 1) / 2; // 向下整除 + } + ``` + +=== "Go" + + ```go title="my_heap.go" + /* 獲取左子節點的索引 */ + func (h *maxHeap) left(i int) int { + return 2*i + 1 + } + + /* 獲取右子節點的索引 */ + func (h *maxHeap) right(i int) int { + return 2*i + 2 + } + + /* 獲取父節點的索引 */ + func (h *maxHeap) parent(i int) int { + // 向下整除 + return (i - 1) / 2 + } + ``` + +=== "Swift" + + ```swift title="my_heap.swift" + /* 獲取左子節點的索引 */ + func left(i: Int) -> Int { + 2 * i + 1 + } + + /* 獲取右子節點的索引 */ + func right(i: Int) -> Int { + 2 * i + 2 + } + + /* 獲取父節點的索引 */ + func parent(i: Int) -> Int { + (i - 1) / 2 // 向下整除 + } + ``` + +=== "JS" + + ```javascript title="my_heap.js" + /* 獲取左子節點的索引 */ + #left(i) { + return 2 * i + 1; + } + + /* 獲取右子節點的索引 */ + #right(i) { + return 2 * i + 2; + } + + /* 獲取父節點的索引 */ + #parent(i) { + return Math.floor((i - 1) / 2); // 向下整除 + } + ``` + +=== "TS" + + ```typescript title="my_heap.ts" + /* 獲取左子節點的索引 */ + left(i: number): number { + return 2 * i + 1; + } + + /* 獲取右子節點的索引 */ + right(i: number): number { + return 2 * i + 2; + } + + /* 獲取父節點的索引 */ + parent(i: number): number { + return Math.floor((i - 1) / 2); // 向下整除 + } + ``` + +=== "Dart" + + ```dart title="my_heap.dart" + /* 獲取左子節點的索引 */ + int _left(int i) { + return 2 * i + 1; + } + + /* 獲取右子節點的索引 */ + int _right(int i) { + return 2 * i + 2; + } + + /* 獲取父節點的索引 */ + int _parent(int i) { + return (i - 1) ~/ 2; // 向下整除 + } + ``` + +=== "Rust" + + ```rust title="my_heap.rs" + /* 獲取左子節點的索引 */ + fn left(i: usize) -> usize { + 2 * i + 1 + } + + /* 獲取右子節點的索引 */ + fn right(i: usize) -> usize { + 2 * i + 2 + } + + /* 獲取父節點的索引 */ + fn parent(i: usize) -> usize { + (i - 1) / 2 // 向下整除 + } + ``` + +=== "C" + + ```c title="my_heap.c" + /* 獲取左子節點的索引 */ + int left(MaxHeap *maxHeap, int i) { + return 2 * i + 1; + } + + /* 獲取右子節點的索引 */ + int right(MaxHeap *maxHeap, int i) { + return 2 * i + 2; + } + + /* 獲取父節點的索引 */ + int parent(MaxHeap *maxHeap, int i) { + return (i - 1) / 2; + } + ``` + +=== "Kotlin" + + ```kotlin title="my_heap.kt" + /* 獲取左子節點的索引 */ + fun left(i: Int): Int { + return 2 * i + 1 + } + + /* 獲取右子節點的索引 */ + fun right(i: Int): Int { + return 2 * i + 2 + } + + /* 獲取父節點的索引 */ + fun parent(i: Int): Int { + return (i - 1) / 2 // 向下整除 + } + ``` + +=== "Ruby" + + ```ruby title="my_heap.rb" + [class]{MaxHeap}-[func]{left} + + [class]{MaxHeap}-[func]{right} + + [class]{MaxHeap}-[func]{parent} + ``` + +=== "Zig" + + ```zig title="my_heap.zig" + // 獲取左子節點的索引 + fn left(i: usize) usize { + return 2 * i + 1; + } + + // 獲取右子節點的索引 + fn right(i: usize) usize { + return 2 * i + 2; + } + + // 獲取父節點的索引 + fn parent(i: usize) usize { + // return (i - 1) / 2; // 向下整除 + return @divFloor(i - 1, 2); + } + ``` + +### 2.   訪問堆積頂元素 + +堆積頂元素即為二元樹的根節點,也就是串列的首個元素: + +=== "Python" + + ```python title="my_heap.py" + def peek(self) -> int: + """訪問堆積頂元素""" + return self.max_heap[0] + ``` + +=== "C++" + + ```cpp title="my_heap.cpp" + /* 訪問堆積頂元素 */ + int peek() { + return maxHeap[0]; + } + ``` + +=== "Java" + + ```java title="my_heap.java" + /* 訪問堆積頂元素 */ + int peek() { + return maxHeap.get(0); + } + ``` + +=== "C#" + + ```csharp title="my_heap.cs" + /* 訪問堆積頂元素 */ + int Peek() { + return maxHeap[0]; + } + ``` + +=== "Go" + + ```go title="my_heap.go" + /* 訪問堆積頂元素 */ + func (h *maxHeap) peek() any { + return h.data[0] + } + ``` + +=== "Swift" + + ```swift title="my_heap.swift" + /* 訪問堆積頂元素 */ + func peek() -> Int { + maxHeap[0] + } + ``` + +=== "JS" + + ```javascript title="my_heap.js" + /* 訪問堆積頂元素 */ + peek() { + return this.#maxHeap[0]; + } + ``` + +=== "TS" + + ```typescript title="my_heap.ts" + /* 訪問堆積頂元素 */ + peek(): number { + return this.maxHeap[0]; + } + ``` + +=== "Dart" + + ```dart title="my_heap.dart" + /* 訪問堆積頂元素 */ + int peek() { + return _maxHeap[0]; + } + ``` + +=== "Rust" + + ```rust title="my_heap.rs" + /* 訪問堆積頂元素 */ + fn peek(&self) -> Option { + self.max_heap.first().copied() + } + ``` + +=== "C" + + ```c title="my_heap.c" + /* 訪問堆積頂元素 */ + int peek(MaxHeap *maxHeap) { + return maxHeap->data[0]; + } + ``` + +=== "Kotlin" + + ```kotlin title="my_heap.kt" + /* 訪問堆積頂元素 */ + fun peek(): Int { + return maxHeap[0] + } + ``` + +=== "Ruby" + + ```ruby title="my_heap.rb" + [class]{MaxHeap}-[func]{peek} + ``` + +=== "Zig" + + ```zig title="my_heap.zig" + // 訪問堆積頂元素 + fn peek(self: *Self) T { + return self.max_heap.?.items[0]; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +### 3.   元素入堆積 + +給定元素 `val` ,我們首先將其新增到堆積底。新增之後,由於 `val` 可能大於堆積中其他元素,堆積的成立條件可能已被破壞,**因此需要修復從插入節點到根節點的路徑上的各個節點**,這個操作被稱為堆積化(heapify)。 + +考慮從入堆積節點開始,**從底至頂執行堆積化**。如圖 8-3 所示,我們比較插入節點與其父節點的值,如果插入節點更大,則將它們交換。然後繼續執行此操作,從底至頂修復堆積中的各個節點,直至越過根節點或遇到無須交換的節點時結束。 + +=== "<1>" + ![元素入堆積步驟](heap.assets/heap_push_step1.png){ class="animation-figure" } + +=== "<2>" + ![heap_push_step2](heap.assets/heap_push_step2.png){ class="animation-figure" } + +=== "<3>" + ![heap_push_step3](heap.assets/heap_push_step3.png){ class="animation-figure" } + +=== "<4>" + ![heap_push_step4](heap.assets/heap_push_step4.png){ class="animation-figure" } + +=== "<5>" + ![heap_push_step5](heap.assets/heap_push_step5.png){ class="animation-figure" } + +=== "<6>" + ![heap_push_step6](heap.assets/heap_push_step6.png){ class="animation-figure" } + +=== "<7>" + ![heap_push_step7](heap.assets/heap_push_step7.png){ class="animation-figure" } + +=== "<8>" + ![heap_push_step8](heap.assets/heap_push_step8.png){ class="animation-figure" } + +=== "<9>" + ![heap_push_step9](heap.assets/heap_push_step9.png){ class="animation-figure" } + +

圖 8-3   元素入堆積步驟

+ +設節點總數為 $n$ ,則樹的高度為 $O(\log n)$ 。由此可知,堆積化操作的迴圈輪數最多為 $O(\log n)$ ,**元素入堆積操作的時間複雜度為 $O(\log n)$** 。程式碼如下所示: + +=== "Python" + + ```python title="my_heap.py" + def push(self, val: int): + """元素入堆積""" + # 新增節點 + self.max_heap.append(val) + # 從底至頂堆積化 + self.sift_up(self.size() - 1) + + def sift_up(self, i: int): + """從節點 i 開始,從底至頂堆積化""" + while True: + # 獲取節點 i 的父節點 + p = self.parent(i) + # 當“越過根節點”或“節點無須修復”時,結束堆積化 + if p < 0 or self.max_heap[i] <= self.max_heap[p]: + break + # 交換兩節點 + self.swap(i, p) + # 迴圈向上堆積化 + i = p + ``` + +=== "C++" + + ```cpp title="my_heap.cpp" + /* 元素入堆積 */ + void push(int val) { + // 新增節點 + maxHeap.push_back(val); + // 從底至頂堆積化 + siftUp(size() - 1); + } + + /* 從節點 i 開始,從底至頂堆積化 */ + void siftUp(int i) { + while (true) { + // 獲取節點 i 的父節點 + int p = parent(i); + // 當“越過根節點”或“節點無須修復”時,結束堆積化 + if (p < 0 || maxHeap[i] <= maxHeap[p]) + break; + // 交換兩節點 + swap(maxHeap[i], maxHeap[p]); + // 迴圈向上堆積化 + i = p; + } + } + ``` + +=== "Java" + + ```java title="my_heap.java" + /* 元素入堆積 */ + void push(int val) { + // 新增節點 + maxHeap.add(val); + // 從底至頂堆積化 + siftUp(size() - 1); + } + + /* 從節點 i 開始,從底至頂堆積化 */ + void siftUp(int i) { + while (true) { + // 獲取節點 i 的父節點 + int p = parent(i); + // 當“越過根節點”或“節點無須修復”時,結束堆積化 + if (p < 0 || maxHeap.get(i) <= maxHeap.get(p)) + break; + // 交換兩節點 + swap(i, p); + // 迴圈向上堆積化 + i = p; + } + } + ``` + +=== "C#" + + ```csharp title="my_heap.cs" + /* 元素入堆積 */ + void Push(int val) { + // 新增節點 + maxHeap.Add(val); + // 從底至頂堆積化 + SiftUp(Size() - 1); + } + + /* 從節點 i 開始,從底至頂堆積化 */ + void SiftUp(int i) { + while (true) { + // 獲取節點 i 的父節點 + int p = Parent(i); + // 若“越過根節點”或“節點無須修復”,則結束堆積化 + if (p < 0 || maxHeap[i] <= maxHeap[p]) + break; + // 交換兩節點 + Swap(i, p); + // 迴圈向上堆積化 + i = p; + } + } + ``` + +=== "Go" + + ```go title="my_heap.go" + /* 元素入堆積 */ + func (h *maxHeap) push(val any) { + // 新增節點 + h.data = append(h.data, val) + // 從底至頂堆積化 + h.siftUp(len(h.data) - 1) + } + + /* 從節點 i 開始,從底至頂堆積化 */ + func (h *maxHeap) siftUp(i int) { + for true { + // 獲取節點 i 的父節點 + p := h.parent(i) + // 當“越過根節點”或“節點無須修復”時,結束堆積化 + if p < 0 || h.data[i].(int) <= h.data[p].(int) { + break + } + // 交換兩節點 + h.swap(i, p) + // 迴圈向上堆積化 + i = p + } + } + ``` + +=== "Swift" + + ```swift title="my_heap.swift" + /* 元素入堆積 */ + func push(val: Int) { + // 新增節點 + maxHeap.append(val) + // 從底至頂堆積化 + siftUp(i: size() - 1) + } + + /* 從節點 i 開始,從底至頂堆積化 */ + func siftUp(i: Int) { + var i = i + while true { + // 獲取節點 i 的父節點 + let p = parent(i: i) + // 當“越過根節點”或“節點無須修復”時,結束堆積化 + if p < 0 || maxHeap[i] <= maxHeap[p] { + break + } + // 交換兩節點 + swap(i: i, j: p) + // 迴圈向上堆積化 + i = p + } + } + ``` + +=== "JS" + + ```javascript title="my_heap.js" + /* 元素入堆積 */ + push(val) { + // 新增節點 + this.#maxHeap.push(val); + // 從底至頂堆積化 + this.#siftUp(this.size() - 1); + } + + /* 從節點 i 開始,從底至頂堆積化 */ + #siftUp(i) { + while (true) { + // 獲取節點 i 的父節點 + const p = this.#parent(i); + // 當“越過根節點”或“節點無須修復”時,結束堆積化 + if (p < 0 || this.#maxHeap[i] <= this.#maxHeap[p]) break; + // 交換兩節點 + this.#swap(i, p); + // 迴圈向上堆積化 + i = p; + } + } + ``` + +=== "TS" + + ```typescript title="my_heap.ts" + /* 元素入堆積 */ + push(val: number): void { + // 新增節點 + this.maxHeap.push(val); + // 從底至頂堆積化 + this.siftUp(this.size() - 1); + } + + /* 從節點 i 開始,從底至頂堆積化 */ + siftUp(i: number): void { + while (true) { + // 獲取節點 i 的父節點 + const p = this.parent(i); + // 當“越過根節點”或“節點無須修復”時,結束堆積化 + if (p < 0 || this.maxHeap[i] <= this.maxHeap[p]) break; + // 交換兩節點 + this.swap(i, p); + // 迴圈向上堆積化 + i = p; + } + } + ``` + +=== "Dart" + + ```dart title="my_heap.dart" + /* 元素入堆積 */ + void push(int val) { + // 新增節點 + _maxHeap.add(val); + // 從底至頂堆積化 + siftUp(size() - 1); + } + + /* 從節點 i 開始,從底至頂堆積化 */ + void siftUp(int i) { + while (true) { + // 獲取節點 i 的父節點 + int p = _parent(i); + // 當“越過根節點”或“節點無須修復”時,結束堆積化 + if (p < 0 || _maxHeap[i] <= _maxHeap[p]) { + break; + } + // 交換兩節點 + _swap(i, p); + // 迴圈向上堆積化 + i = p; + } + } + ``` + +=== "Rust" + + ```rust title="my_heap.rs" + /* 元素入堆積 */ + fn push(&mut self, val: i32) { + // 新增節點 + self.max_heap.push(val); + // 從底至頂堆積化 + self.sift_up(self.size() - 1); + } + + /* 從節點 i 開始,從底至頂堆積化 */ + fn sift_up(&mut self, mut i: usize) { + loop { + // 節點 i 已經是堆積頂節點了,結束堆積化 + if i == 0 { + break; + } + // 獲取節點 i 的父節點 + let p = Self::parent(i); + // 當“節點無須修復”時,結束堆積化 + if self.max_heap[i] <= self.max_heap[p] { + break; + } + // 交換兩節點 + self.swap(i, p); + // 迴圈向上堆積化 + i = p; + } + } + ``` + +=== "C" + + ```c title="my_heap.c" + /* 元素入堆積 */ + void push(MaxHeap *maxHeap, int val) { + // 預設情況下,不應該新增這麼多節點 + if (maxHeap->size == MAX_SIZE) { + printf("heap is full!"); + return; + } + // 新增節點 + maxHeap->data[maxHeap->size] = val; + maxHeap->size++; + + // 從底至頂堆積化 + siftUp(maxHeap, maxHeap->size - 1); + } + + /* 從節點 i 開始,從底至頂堆積化 */ + void siftUp(MaxHeap *maxHeap, int i) { + while (true) { + // 獲取節點 i 的父節點 + int p = parent(maxHeap, i); + // 當“越過根節點”或“節點無須修復”時,結束堆積化 + if (p < 0 || maxHeap->data[i] <= maxHeap->data[p]) { + break; + } + // 交換兩節點 + swap(maxHeap, i, p); + // 迴圈向上堆積化 + i = p; + } + } + ``` + +=== "Kotlin" + + ```kotlin title="my_heap.kt" + /* 元素入堆積 */ + fun push(value: Int) { + // 新增節點 + maxHeap.add(value) + // 從底至頂堆積化 + siftUp(size() - 1) + } + + /* 從節點 i 開始,從底至頂堆積化 */ + 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 + } + } + ``` + +=== "Ruby" + + ```ruby title="my_heap.rb" + [class]{MaxHeap}-[func]{push} + + [class]{MaxHeap}-[func]{sift_up} + ``` + +=== "Zig" + + ```zig title="my_heap.zig" + // 元素入堆積 + fn push(self: *Self, val: T) !void { + // 新增節點 + try self.max_heap.?.append(val); + // 從底至頂堆積化 + try self.siftUp(self.size() - 1); + } + + // 從節點 i 開始,從底至頂堆積化 + fn siftUp(self: *Self, i_: usize) !void { + var i = i_; + while (true) { + // 獲取節點 i 的父節點 + var p = parent(i); + // 當“越過根節點”或“節點無須修復”時,結束堆積化 + if (p < 0 or self.max_heap.?.items[i] <= self.max_heap.?.items[p]) break; + // 交換兩節點 + try self.swap(i, p); + // 迴圈向上堆積化 + i = p; + } + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +### 4.   堆積頂元素出堆積 + +堆積頂元素是二元樹的根節點,即串列首元素。如果我們直接從串列中刪除首元素,那麼二元樹中所有節點的索引都會發生變化,這將使得後續使用堆積化進行修復變得困難。為了儘量減少元素索引的變動,我們採用以下操作步驟。 + +1. 交換堆積頂元素與堆積底元素(交換根節點與最右葉節點)。 +2. 交換完成後,將堆積底從串列中刪除(注意,由於已經交換,因此實際上刪除的是原來的堆積頂元素)。 +3. 從根節點開始,**從頂至底執行堆積化**。 + +如圖 8-4 所示,**“從頂至底堆積化”的操作方向與“從底至頂堆積化”相反**,我們將根節點的值與其兩個子節點的值進行比較,將最大的子節點與根節點交換。然後迴圈執行此操作,直到越過葉節點或遇到無須交換的節點時結束。 + +=== "<1>" + ![堆積頂元素出堆積步驟](heap.assets/heap_pop_step1.png){ class="animation-figure" } + +=== "<2>" + ![heap_pop_step2](heap.assets/heap_pop_step2.png){ class="animation-figure" } + +=== "<3>" + ![heap_pop_step3](heap.assets/heap_pop_step3.png){ class="animation-figure" } + +=== "<4>" + ![heap_pop_step4](heap.assets/heap_pop_step4.png){ class="animation-figure" } + +=== "<5>" + ![heap_pop_step5](heap.assets/heap_pop_step5.png){ class="animation-figure" } + +=== "<6>" + ![heap_pop_step6](heap.assets/heap_pop_step6.png){ class="animation-figure" } + +=== "<7>" + ![heap_pop_step7](heap.assets/heap_pop_step7.png){ class="animation-figure" } + +=== "<8>" + ![heap_pop_step8](heap.assets/heap_pop_step8.png){ class="animation-figure" } + +=== "<9>" + ![heap_pop_step9](heap.assets/heap_pop_step9.png){ class="animation-figure" } + +=== "<10>" + ![heap_pop_step10](heap.assets/heap_pop_step10.png){ class="animation-figure" } + +

圖 8-4   堆積頂元素出堆積步驟

+ +與元素入堆積操作相似,堆積頂元素出堆積操作的時間複雜度也為 $O(\log n)$ 。程式碼如下所示: + +=== "Python" + + ```python title="my_heap.py" + def pop(self) -> int: + """元素出堆積""" + # 判空處理 + if self.is_empty(): + raise IndexError("堆積為空") + # 交換根節點與最右葉節點(交換首元素與尾元素) + self.swap(0, self.size() - 1) + # 刪除節點 + val = self.max_heap.pop() + # 從頂至底堆積化 + self.sift_down(0) + # 返回堆積頂元素 + return val + + def sift_down(self, i: int): + """從節點 i 開始,從頂至底堆積化""" + while True: + # 判斷節點 i, l, r 中值最大的節點,記為 ma + l, r, ma = self.left(i), self.right(i), i + if l < self.size() and self.max_heap[l] > self.max_heap[ma]: + ma = l + if r < self.size() and self.max_heap[r] > self.max_heap[ma]: + ma = r + # 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出 + if ma == i: + break + # 交換兩節點 + self.swap(i, ma) + # 迴圈向下堆積化 + i = ma + ``` + +=== "C++" + + ```cpp title="my_heap.cpp" + /* 元素出堆積 */ + void pop() { + // 判空處理 + if (isEmpty()) { + throw out_of_range("堆積為空"); + } + // 交換根節點與最右葉節點(交換首元素與尾元素) + swap(maxHeap[0], maxHeap[size() - 1]); + // 刪除節點 + maxHeap.pop_back(); + // 從頂至底堆積化 + siftDown(0); + } + + /* 從節點 i 開始,從頂至底堆積化 */ + void siftDown(int i) { + while (true) { + // 判斷節點 i, l, r 中值最大的節點,記為 ma + int l = left(i), r = right(i), 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(maxHeap[i], maxHeap[ma]); + // 迴圈向下堆積化 + i = ma; + } + } + ``` + +=== "Java" + + ```java title="my_heap.java" + /* 元素出堆積 */ + int pop() { + // 判空處理 + if (isEmpty()) + throw new IndexOutOfBoundsException(); + // 交換根節點與最右葉節點(交換首元素與尾元素) + swap(0, size() - 1); + // 刪除節點 + int val = maxHeap.remove(size() - 1); + // 從頂至底堆積化 + siftDown(0); + // 返回堆積頂元素 + return val; + } + + /* 從節點 i 開始,從頂至底堆積化 */ + void siftDown(int i) { + while (true) { + // 判斷節點 i, l, r 中值最大的節點,記為 ma + int l = left(i), r = right(i), ma = i; + if (l < size() && maxHeap.get(l) > maxHeap.get(ma)) + ma = l; + if (r < size() && maxHeap.get(r) > maxHeap.get(ma)) + ma = r; + // 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出 + if (ma == i) + break; + // 交換兩節點 + swap(i, ma); + // 迴圈向下堆積化 + i = ma; + } + } + ``` + +=== "C#" + + ```csharp title="my_heap.cs" + /* 元素出堆積 */ + int Pop() { + // 判空處理 + if (IsEmpty()) + throw new IndexOutOfRangeException(); + // 交換根節點與最右葉節點(交換首元素與尾元素) + Swap(0, Size() - 1); + // 刪除節點 + int val = maxHeap.Last(); + maxHeap.RemoveAt(Size() - 1); + // 從頂至底堆積化 + SiftDown(0); + // 返回堆積頂元素 + return val; + } + + /* 從節點 i 開始,從頂至底堆積化 */ + void SiftDown(int i) { + while (true) { + // 判斷節點 i, l, r 中值最大的節點,記為 ma + int l = Left(i), r = Right(i), ma = i; + if (l < Size() && maxHeap[l] > maxHeap[ma]) + ma = l; + if (r < Size() && maxHeap[r] > maxHeap[ma]) + ma = r; + // 若“節點 i 最大”或“越過葉節點”,則結束堆積化 + if (ma == i) break; + // 交換兩節點 + Swap(i, ma); + // 迴圈向下堆積化 + i = ma; + } + } + ``` + +=== "Go" + + ```go title="my_heap.go" + /* 元素出堆積 */ + func (h *maxHeap) pop() any { + // 判空處理 + if h.isEmpty() { + fmt.Println("error") + return nil + } + // 交換根節點與最右葉節點(交換首元素與尾元素) + h.swap(0, h.size()-1) + // 刪除節點 + val := h.data[len(h.data)-1] + h.data = h.data[:len(h.data)-1] + // 從頂至底堆積化 + h.siftDown(0) + + // 返回堆積頂元素 + return val + } + + /* 從節點 i 開始,從頂至底堆積化 */ + func (h *maxHeap) siftDown(i int) { + for true { + // 判斷節點 i, l, r 中值最大的節點,記為 max + l, r, max := h.left(i), h.right(i), i + if l < h.size() && h.data[l].(int) > h.data[max].(int) { + max = l + } + if r < h.size() && h.data[r].(int) > h.data[max].(int) { + max = r + } + // 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出 + if max == i { + break + } + // 交換兩節點 + h.swap(i, max) + // 迴圈向下堆積化 + i = max + } + } + ``` + +=== "Swift" + + ```swift title="my_heap.swift" + /* 元素出堆積 */ + func pop() -> Int { + // 判空處理 + if isEmpty() { + fatalError("堆積為空") + } + // 交換根節點與最右葉節點(交換首元素與尾元素) + swap(i: 0, j: size() - 1) + // 刪除節點 + let val = maxHeap.remove(at: size() - 1) + // 從頂至底堆積化 + siftDown(i: 0) + // 返回堆積頂元素 + return val + } + + /* 從節點 i 開始,從頂至底堆積化 */ + func siftDown(i: Int) { + var i = i + while true { + // 判斷節點 i, l, r 中值最大的節點,記為 ma + let l = left(i: i) + let r = right(i: 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: i, j: ma) + // 迴圈向下堆積化 + i = ma + } + } + ``` + +=== "JS" + + ```javascript title="my_heap.js" + /* 元素出堆積 */ + pop() { + // 判空處理 + if (this.isEmpty()) throw new Error('堆積為空'); + // 交換根節點與最右葉節點(交換首元素與尾元素) + this.#swap(0, this.size() - 1); + // 刪除節點 + const val = this.#maxHeap.pop(); + // 從頂至底堆積化 + this.#siftDown(0); + // 返回堆積頂元素 + return val; + } + + /* 從節點 i 開始,從頂至底堆積化 */ + #siftDown(i) { + while (true) { + // 判斷節點 i, l, r 中值最大的節點,記為 ma + const l = this.#left(i), + r = this.#right(i); + let ma = i; + if (l < this.size() && this.#maxHeap[l] > this.#maxHeap[ma]) ma = l; + if (r < this.size() && this.#maxHeap[r] > this.#maxHeap[ma]) ma = r; + // 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出 + if (ma === i) break; + // 交換兩節點 + this.#swap(i, ma); + // 迴圈向下堆積化 + i = ma; + } + } + ``` + +=== "TS" + + ```typescript title="my_heap.ts" + /* 元素出堆積 */ + pop(): number { + // 判空處理 + if (this.isEmpty()) throw new RangeError('Heap is empty.'); + // 交換根節點與最右葉節點(交換首元素與尾元素) + this.swap(0, this.size() - 1); + // 刪除節點 + const val = this.maxHeap.pop(); + // 從頂至底堆積化 + this.siftDown(0); + // 返回堆積頂元素 + return val; + } + + /* 從節點 i 開始,從頂至底堆積化 */ + siftDown(i: number): void { + while (true) { + // 判斷節點 i, l, r 中值最大的節點,記為 ma + const l = this.left(i), + r = this.right(i); + let ma = i; + if (l < this.size() && this.maxHeap[l] > this.maxHeap[ma]) ma = l; + if (r < this.size() && this.maxHeap[r] > this.maxHeap[ma]) ma = r; + // 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出 + if (ma === i) break; + // 交換兩節點 + this.swap(i, ma); + // 迴圈向下堆積化 + i = ma; + } + } + ``` + +=== "Dart" + + ```dart title="my_heap.dart" + /* 元素出堆積 */ + int pop() { + // 判空處理 + if (isEmpty()) throw Exception('堆積為空'); + // 交換根節點與最右葉節點(交換首元素與尾元素) + _swap(0, size() - 1); + // 刪除節點 + int val = _maxHeap.removeLast(); + // 從頂至底堆積化 + siftDown(0); + // 返回堆積頂元素 + return val; + } + + /* 從節點 i 開始,從頂至底堆積化 */ + void siftDown(int i) { + while (true) { + // 判斷節點 i, l, r 中值最大的節點,記為 ma + int l = _left(i); + int r = _right(i); + int 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; + } + } + ``` + +=== "Rust" + + ```rust title="my_heap.rs" + /* 元素出堆積 */ + fn pop(&mut self) -> i32 { + // 判空處理 + if self.is_empty() { + panic!("index out of bounds"); + } + // 交換根節點與最右葉節點(交換首元素與尾元素) + self.swap(0, self.size() - 1); + // 刪除節點 + let val = self.max_heap.remove(self.size() - 1); + // 從頂至底堆積化 + self.sift_down(0); + // 返回堆積頂元素 + val + } + + /* 從節點 i 開始,從頂至底堆積化 */ + fn sift_down(&mut self, mut i: usize) { + loop { + // 判斷節點 i, l, r 中值最大的節點,記為 ma + let (l, r, mut ma) = (Self::left(i), Self::right(i), i); + if l < self.size() && self.max_heap[l] > self.max_heap[ma] { + ma = l; + } + if r < self.size() && self.max_heap[r] > self.max_heap[ma] { + ma = r; + } + // 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出 + if ma == i { + break; + } + // 交換兩節點 + self.swap(i, ma); + // 迴圈向下堆積化 + i = ma; + } + } + ``` + +=== "C" + + ```c title="my_heap.c" + /* 元素出堆積 */ + int pop(MaxHeap *maxHeap) { + // 判空處理 + if (isEmpty(maxHeap)) { + printf("heap is empty!"); + return INT_MAX; + } + // 交換根節點與最右葉節點(交換首元素與尾元素) + swap(maxHeap, 0, size(maxHeap) - 1); + // 刪除節點 + int val = maxHeap->data[maxHeap->size - 1]; + maxHeap->size--; + // 從頂至底堆積化 + siftDown(maxHeap, 0); + + // 返回堆積頂元素 + return val; + } + + /* 從節點 i 開始,從頂至底堆積化 */ + void siftDown(MaxHeap *maxHeap, int i) { + while (true) { + // 判斷節點 i, l, r 中值最大的節點,記為 max + int l = left(maxHeap, i); + int r = right(maxHeap, i); + int max = i; + if (l < size(maxHeap) && maxHeap->data[l] > maxHeap->data[max]) { + max = l; + } + if (r < size(maxHeap) && maxHeap->data[r] > maxHeap->data[max]) { + max = r; + } + // 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出 + if (max == i) { + break; + } + // 交換兩節點 + swap(maxHeap, i, max); + // 迴圈向下堆積化 + i = max; + } + } + ``` + +=== "Kotlin" + + ```kotlin title="my_heap.kt" + /* 元素出堆積 */ + fun pop(): Int { + // 判空處理 + if (isEmpty()) throw IndexOutOfBoundsException() + // 交換根節點與最右葉節點(交換首元素與尾元素) + swap(0, size() - 1) + // 刪除節點 + val value = maxHeap.removeAt(size() - 1) + // 從頂至底堆積化 + siftDown(0) + // 返回堆積頂元素 + return value + } + + /* 從節點 i 開始,從頂至底堆積化 */ + 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 + } + } + ``` + +=== "Ruby" + + ```ruby title="my_heap.rb" + [class]{MaxHeap}-[func]{pop} + + [class]{MaxHeap}-[func]{sift_down} + ``` + +=== "Zig" + + ```zig title="my_heap.zig" + // 元素出堆積 + fn pop(self: *Self) !T { + // 判斷處理 + if (self.isEmpty()) unreachable; + // 交換根節點與最右葉節點(交換首元素與尾元素) + try self.swap(0, self.size() - 1); + // 刪除節點 + var val = self.max_heap.?.pop(); + // 從頂至底堆積化 + try self.siftDown(0); + // 返回堆積頂元素 + return val; + } + + // 從節點 i 開始,從頂至底堆積化 + fn siftDown(self: *Self, i_: usize) !void { + var i = i_; + while (true) { + // 判斷節點 i, l, r 中值最大的節點,記為 ma + var l = left(i); + var r = right(i); + var ma = i; + if (l < self.size() and self.max_heap.?.items[l] > self.max_heap.?.items[ma]) ma = l; + if (r < self.size() and self.max_heap.?.items[r] > self.max_heap.?.items[ma]) ma = r; + // 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出 + if (ma == i) break; + // 交換兩節點 + try self.swap(i, ma); + // 迴圈向下堆積化 + i = ma; + } + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 8.1.3   堆積的常見應用 + +- **優先佇列**:堆積通常作為實現優先佇列的首選資料結構,其入列和出列操作的時間複雜度均為 $O(\log n)$ ,而建隊操作為 $O(n)$ ,這些操作都非常高效。 +- **堆積排序**:給定一組資料,我們可以用它們建立一個堆積,然後不斷地執行元素出堆積操作,從而得到有序資料。然而,我們通常會使用一種更優雅的方式實現堆積排序,詳見“堆積排序”章節。 +- **獲取最大的 $k$ 個元素**:這是一個經典的演算法問題,同時也是一種典型應用,例如選擇熱度前 10 的新聞作為微博熱搜,選取銷量前 10 的商品等。 diff --git a/zh-Hant/docs/chapter_heap/index.md b/zh-Hant/docs/chapter_heap/index.md new file mode 100644 index 000000000..c2d898fb4 --- /dev/null +++ b/zh-Hant/docs/chapter_heap/index.md @@ -0,0 +1,21 @@ +--- +comments: true +icon: material/family-tree +--- + +# 第 8 章   堆積 + +![堆積](../assets/covers/chapter_heap.jpg){ 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/) diff --git a/zh-Hant/docs/chapter_heap/summary.md b/zh-Hant/docs/chapter_heap/summary.md new file mode 100644 index 000000000..282d45dd2 --- /dev/null +++ b/zh-Hant/docs/chapter_heap/summary.md @@ -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**:資料結構的“堆積”與記憶體管理的“堆積”是同一個概念嗎? + +兩者不是同一個概念,只是碰巧都叫“堆積”。計算機系統記憶體中的堆積是動態記憶體分配的一部分,程式在執行時可以使用它來儲存資料。程式可以請求一定量的堆積記憶體,用於儲存如物件和陣列等複雜結構。當這些資料不再需要時,程式需要釋放這些記憶體,以防止記憶體流失。相較於堆疊記憶體,堆積記憶體的管理和使用需要更謹慎,使用不當可能會導致記憶體流失和野指標等問題。 diff --git a/zh-Hant/docs/chapter_heap/top_k.md b/zh-Hant/docs/chapter_heap/top_k.md new file mode 100644 index 000000000..bb3e07ebb --- /dev/null +++ b/zh-Hant/docs/chapter_heap/top_k.md @@ -0,0 +1,456 @@ +--- +comments: true +--- + +# 8.3   Top-k 問題 + +!!! question + + 給定一個長度為 $n$ 的無序陣列 `nums` ,請返回陣列中最大的 $k$ 個元素。 + +對於該問題,我們先介紹兩種思路比較直接的解法,再介紹效率更高的堆積解法。 + +## 8.3.1   方法一:走訪選擇 + +我們可以進行圖 8-6 所示的 $k$ 輪走訪,分別在每輪中提取第 $1$、$2$、$\dots$、$k$ 大的元素,時間複雜度為 $O(nk)$ 。 + +此方法只適用於 $k \ll n$ 的情況,因為當 $k$ 與 $n$ 比較接近時,其時間複雜度趨向於 $O(n^2)$ ,非常耗時。 + +![走訪尋找最大的 k 個元素](top_k.assets/top_k_traversal.png){ class="animation-figure" } + +

圖 8-6   走訪尋找最大的 k 個元素

+ +!!! tip + + 當 $k = n$ 時,我們可以得到完整的有序序列,此時等價於“選擇排序”演算法。 + +## 8.3.2   方法二:排序 + +如圖 8-7 所示,我們可以先對陣列 `nums` 進行排序,再返回最右邊的 $k$ 個元素,時間複雜度為 $O(n \log n)$ 。 + +顯然,該方法“超額”完成任務了,因為我們只需找出最大的 $k$ 個元素即可,而不需要排序其他元素。 + +![排序尋找最大的 k 個元素](top_k.assets/top_k_sorting.png){ class="animation-figure" } + +

圖 8-7   排序尋找最大的 k 個元素

+ +## 8.3.3   方法三:堆積 + +我們可以基於堆積更加高效地解決 Top-k 問題,流程如圖 8-8 所示。 + +1. 初始化一個小頂堆積,其堆積頂元素最小。 +2. 先將陣列的前 $k$ 個元素依次入堆積。 +3. 從第 $k + 1$ 個元素開始,若當前元素大於堆積頂元素,則將堆積頂元素出堆積,並將當前元素入堆積。 +4. 走訪完成後,堆積中儲存的就是最大的 $k$ 個元素。 + +=== "<1>" + ![基於堆積尋找最大的 k 個元素](top_k.assets/top_k_heap_step1.png){ class="animation-figure" } + +=== "<2>" + ![top_k_heap_step2](top_k.assets/top_k_heap_step2.png){ class="animation-figure" } + +=== "<3>" + ![top_k_heap_step3](top_k.assets/top_k_heap_step3.png){ class="animation-figure" } + +=== "<4>" + ![top_k_heap_step4](top_k.assets/top_k_heap_step4.png){ class="animation-figure" } + +=== "<5>" + ![top_k_heap_step5](top_k.assets/top_k_heap_step5.png){ class="animation-figure" } + +=== "<6>" + ![top_k_heap_step6](top_k.assets/top_k_heap_step6.png){ class="animation-figure" } + +=== "<7>" + ![top_k_heap_step7](top_k.assets/top_k_heap_step7.png){ class="animation-figure" } + +=== "<8>" + ![top_k_heap_step8](top_k.assets/top_k_heap_step8.png){ class="animation-figure" } + +=== "<9>" + ![top_k_heap_step9](top_k.assets/top_k_heap_step9.png){ class="animation-figure" } + +

圖 8-8   基於堆積尋找最大的 k 個元素

+ +示例程式碼如下: + +=== "Python" + + ```python title="top_k.py" + def top_k_heap(nums: list[int], k: int) -> list[int]: + """基於堆積查詢陣列中最大的 k 個元素""" + # 初始化小頂堆積 + heap = [] + # 將陣列的前 k 個元素入堆積 + for i in range(k): + heapq.heappush(heap, nums[i]) + # 從第 k+1 個元素開始,保持堆積的長度為 k + for i in range(k, len(nums)): + # 若當前元素大於堆積頂元素,則將堆積頂元素出堆積、當前元素入堆積 + if nums[i] > heap[0]: + heapq.heappop(heap) + heapq.heappush(heap, nums[i]) + return heap + ``` + +=== "C++" + + ```cpp title="top_k.cpp" + /* 基於堆積查詢陣列中最大的 k 個元素 */ + priority_queue, greater> topKHeap(vector &nums, int k) { + // 初始化小頂堆積 + priority_queue, greater> heap; + // 將陣列的前 k 個元素入堆積 + for (int i = 0; i < k; i++) { + heap.push(nums[i]); + } + // 從第 k+1 個元素開始,保持堆積的長度為 k + for (int i = k; i < nums.size(); i++) { + // 若當前元素大於堆積頂元素,則將堆積頂元素出堆積、當前元素入堆積 + if (nums[i] > heap.top()) { + heap.pop(); + heap.push(nums[i]); + } + } + return heap; + } + ``` + +=== "Java" + + ```java title="top_k.java" + /* 基於堆積查詢陣列中最大的 k 個元素 */ + Queue topKHeap(int[] nums, int k) { + // 初始化小頂堆積 + Queue heap = new PriorityQueue(); + // 將陣列的前 k 個元素入堆積 + for (int i = 0; i < k; i++) { + heap.offer(nums[i]); + } + // 從第 k+1 個元素開始,保持堆積的長度為 k + for (int i = k; i < nums.length; i++) { + // 若當前元素大於堆積頂元素,則將堆積頂元素出堆積、當前元素入堆積 + if (nums[i] > heap.peek()) { + heap.poll(); + heap.offer(nums[i]); + } + } + return heap; + } + ``` + +=== "C#" + + ```csharp title="top_k.cs" + /* 基於堆積查詢陣列中最大的 k 個元素 */ + PriorityQueue TopKHeap(int[] nums, int k) { + // 初始化小頂堆積 + PriorityQueue heap = new(); + // 將陣列的前 k 個元素入堆積 + for (int i = 0; i < k; i++) { + heap.Enqueue(nums[i], nums[i]); + } + // 從第 k+1 個元素開始,保持堆積的長度為 k + for (int i = k; i < nums.Length; i++) { + // 若當前元素大於堆積頂元素,則將堆積頂元素出堆積、當前元素入堆積 + if (nums[i] > heap.Peek()) { + heap.Dequeue(); + heap.Enqueue(nums[i], nums[i]); + } + } + return heap; + } + ``` + +=== "Go" + + ```go title="top_k.go" + /* 基於堆積查詢陣列中最大的 k 個元素 */ + func topKHeap(nums []int, k int) *minHeap { + // 初始化小頂堆積 + h := &minHeap{} + heap.Init(h) + // 將陣列的前 k 個元素入堆積 + for i := 0; i < k; i++ { + heap.Push(h, nums[i]) + } + // 從第 k+1 個元素開始,保持堆積的長度為 k + for i := k; i < len(nums); i++ { + // 若當前元素大於堆積頂元素,則將堆積頂元素出堆積、當前元素入堆積 + if nums[i] > h.Top().(int) { + heap.Pop(h) + heap.Push(h, nums[i]) + } + } + return h + } + ``` + +=== "Swift" + + ```swift title="top_k.swift" + /* 基於堆積查詢陣列中最大的 k 個元素 */ + func topKHeap(nums: [Int], k: Int) -> [Int] { + // 初始化一個小頂堆積,並將前 k 個元素建堆積 + var heap = Heap(nums.prefix(k)) + // 從第 k+1 個元素開始,保持堆積的長度為 k + for i in nums.indices.dropFirst(k) { + // 若當前元素大於堆積頂元素,則將堆積頂元素出堆積、當前元素入堆積 + if nums[i] > heap.min()! { + _ = heap.removeMin() + heap.insert(nums[i]) + } + } + return heap.unordered + } + ``` + +=== "JS" + + ```javascript title="top_k.js" + /* 元素入堆積 */ + function pushMinHeap(maxHeap, val) { + // 元素取反 + maxHeap.push(-val); + } + + /* 元素出堆積 */ + function popMinHeap(maxHeap) { + // 元素取反 + return -maxHeap.pop(); + } + + /* 訪問堆積頂元素 */ + function peekMinHeap(maxHeap) { + // 元素取反 + return -maxHeap.peek(); + } + + /* 取出堆積中元素 */ + function getMinHeap(maxHeap) { + // 元素取反 + return maxHeap.getMaxHeap().map((num) => -num); + } + + /* 基於堆積查詢陣列中最大的 k 個元素 */ + function topKHeap(nums, k) { + // 初始化小頂堆積 + // 請注意:我們將堆積中所有元素取反,從而用大頂堆積來模擬小頂堆積 + const maxHeap = new MaxHeap([]); + // 將陣列的前 k 個元素入堆積 + for (let i = 0; i < k; i++) { + pushMinHeap(maxHeap, nums[i]); + } + // 從第 k+1 個元素開始,保持堆積的長度為 k + for (let i = k; i < nums.length; i++) { + // 若當前元素大於堆積頂元素,則將堆積頂元素出堆積、當前元素入堆積 + if (nums[i] > peekMinHeap(maxHeap)) { + popMinHeap(maxHeap); + pushMinHeap(maxHeap, nums[i]); + } + } + // 返回堆積中元素 + return getMinHeap(maxHeap); + } + ``` + +=== "TS" + + ```typescript title="top_k.ts" + /* 元素入堆積 */ + function pushMinHeap(maxHeap: MaxHeap, val: number): void { + // 元素取反 + maxHeap.push(-val); + } + + /* 元素出堆積 */ + function popMinHeap(maxHeap: MaxHeap): number { + // 元素取反 + return -maxHeap.pop(); + } + + /* 訪問堆積頂元素 */ + function peekMinHeap(maxHeap: MaxHeap): number { + // 元素取反 + return -maxHeap.peek(); + } + + /* 取出堆積中元素 */ + function getMinHeap(maxHeap: MaxHeap): number[] { + // 元素取反 + return maxHeap.getMaxHeap().map((num: number) => -num); + } + + /* 基於堆積查詢陣列中最大的 k 個元素 */ + function topKHeap(nums: number[], k: number): number[] { + // 初始化小頂堆積 + // 請注意:我們將堆積中所有元素取反,從而用大頂堆積來模擬小頂堆積 + const maxHeap = new MaxHeap([]); + // 將陣列的前 k 個元素入堆積 + for (let i = 0; i < k; i++) { + pushMinHeap(maxHeap, nums[i]); + } + // 從第 k+1 個元素開始,保持堆積的長度為 k + for (let i = k; i < nums.length; i++) { + // 若當前元素大於堆積頂元素,則將堆積頂元素出堆積、當前元素入堆積 + if (nums[i] > peekMinHeap(maxHeap)) { + popMinHeap(maxHeap); + pushMinHeap(maxHeap, nums[i]); + } + } + // 返回堆積中元素 + return getMinHeap(maxHeap); + } + ``` + +=== "Dart" + + ```dart title="top_k.dart" + /* 基於堆積查詢陣列中最大的 k 個元素 */ + MinHeap topKHeap(List nums, int k) { + // 初始化小頂堆積,將陣列的前 k 個元素入堆積 + MinHeap heap = MinHeap(nums.sublist(0, k)); + // 從第 k+1 個元素開始,保持堆積的長度為 k + for (int i = k; i < nums.length; i++) { + // 若當前元素大於堆積頂元素,則將堆積頂元素出堆積、當前元素入堆積 + if (nums[i] > heap.peek()) { + heap.pop(); + heap.push(nums[i]); + } + } + return heap; + } + ``` + +=== "Rust" + + ```rust title="top_k.rs" + /* 基於堆積查詢陣列中最大的 k 個元素 */ + fn top_k_heap(nums: Vec, k: usize) -> BinaryHeap> { + // BinaryHeap 是大頂堆積,使用 Reverse 將元素取反,從而實現小頂堆積 + let mut heap = BinaryHeap::>::new(); + // 將陣列的前 k 個元素入堆積 + for &num in nums.iter().take(k) { + heap.push(Reverse(num)); + } + // 從第 k+1 個元素開始,保持堆積的長度為 k + for &num in nums.iter().skip(k) { + // 若當前元素大於堆積頂元素,則將堆積頂元素出堆積、當前元素入堆積 + if num > heap.peek().unwrap().0 { + heap.pop(); + heap.push(Reverse(num)); + } + } + heap + } + ``` + +=== "C" + + ```c title="top_k.c" + /* 元素入堆積 */ + void pushMinHeap(MaxHeap *maxHeap, int val) { + // 元素取反 + push(maxHeap, -val); + } + + /* 元素出堆積 */ + int popMinHeap(MaxHeap *maxHeap) { + // 元素取反 + return -pop(maxHeap); + } + + /* 訪問堆積頂元素 */ + int peekMinHeap(MaxHeap *maxHeap) { + // 元素取反 + return -peek(maxHeap); + } + + /* 取出堆積中元素 */ + int *getMinHeap(MaxHeap *maxHeap) { + // 將堆積中所有元素取反並存入 res 陣列 + int *res = (int *)malloc(maxHeap->size * sizeof(int)); + for (int i = 0; i < maxHeap->size; i++) { + res[i] = -maxHeap->data[i]; + } + return res; + } + + /* 取出堆積中元素 */ + int *getMinHeap(MaxHeap *maxHeap) { + // 將堆積中所有元素取反並存入 res 陣列 + int *res = (int *)malloc(maxHeap->size * sizeof(int)); + for (int i = 0; i < maxHeap->size; i++) { + res[i] = -maxHeap->data[i]; + } + return res; + } + + // 基於堆積查詢陣列中最大的 k 個元素的函式 + int *topKHeap(int *nums, int sizeNums, int k) { + // 初始化小頂堆積 + // 請注意:我們將堆積中所有元素取反,從而用大頂堆積來模擬小頂堆積 + int *empty = (int *)malloc(0); + MaxHeap *maxHeap = newMaxHeap(empty, 0); + // 將陣列的前 k 個元素入堆積 + for (int i = 0; i < k; i++) { + pushMinHeap(maxHeap, nums[i]); + } + // 從第 k+1 個元素開始,保持堆積的長度為 k + for (int i = k; i < sizeNums; i++) { + // 若當前元素大於堆積頂元素,則將堆積頂元素出堆積、當前元素入堆積 + if (nums[i] > peekMinHeap(maxHeap)) { + popMinHeap(maxHeap); + pushMinHeap(maxHeap, nums[i]); + } + } + int *res = getMinHeap(maxHeap); + // 釋放記憶體 + delMaxHeap(maxHeap); + return res; + } + ``` + +=== "Kotlin" + + ```kotlin title="top_k.kt" + /* 基於堆積查詢陣列中最大的 k 個元素 */ + fun topKHeap(nums: IntArray, k: Int): Queue { + // 初始化小頂堆積 + val heap = PriorityQueue() + // 將陣列的前 k 個元素入堆積 + for (i in 0.. heap.peek()) { + heap.poll() + heap.offer(nums[i]) + } + } + return heap + } + ``` + +=== "Ruby" + + ```ruby title="top_k.rb" + [class]{}-[func]{top_k_heap} + ``` + +=== "Zig" + + ```zig title="top_k.zig" + [class]{}-[func]{topKHeap} + ``` + +??? pythontutor "視覺化執行" + +
+ + +總共執行了 $n$ 輪入堆積和出堆積,堆積的最大長度為 $k$ ,因此時間複雜度為 $O(n \log k)$ 。該方法的效率很高,當 $k$ 較小時,時間複雜度趨向 $O(n)$ ;當 $k$ 較大時,時間複雜度不會超過 $O(n \log n)$ 。 + +另外,該方法適用於動態資料流的使用場景。在不斷加入資料時,我們可以持續維護堆積內的元素,從而實現最大的 $k$ 個元素的動態更新。 diff --git a/zh-Hant/docs/chapter_hello_algo/index.md b/zh-Hant/docs/chapter_hello_algo/index.md new file mode 100644 index 000000000..ac04b5769 --- /dev/null +++ b/zh-Hant/docs/chapter_hello_algo/index.md @@ -0,0 +1,30 @@ +--- +comments: true +icon: material/rocket-launch-outline +--- + +# 序 + +幾年前,我在力扣上分享了“劍指 Offer”系列題解,受到了許多讀者的鼓勵和支持。在與讀者交流期間,我最常被問的一個問題是“如何入門演算法”。逐漸地,我對這個問題產生了濃厚的興趣。 + +兩眼一抹黑地刷題似乎是最受歡迎的方法,簡單、直接且有效。然而刷題就如同玩“掃雷”遊戲,自學能力強的人能夠順利將地雷逐個排掉,而基礎不足的人很可能被炸得滿頭是包,並在挫折中步步退縮。通讀教材也是一種常見做法,但對於面向求職的人來說,畢業論文、投遞簡歷、準備筆試和面試已經消耗了大部分精力,啃厚重的書往往變成了一項艱鉅的挑戰。 + +如果你也面臨類似的困擾,那麼很幸運這本書“找”到了你。本書是我對這個問題給出的答案,即使不是最優解,也至少是一次積極的嘗試。本書雖然不足以讓你直接拿到 Offer,但會引導你探索資料結構與演算法的“知識地圖”,帶你瞭解不同“地雷”的形狀、大小和分佈位置,讓你掌握各種“排雷方法”。有了這些本領,相信你可以更加自如地刷題和閱讀文獻,逐步構建起完整的知識體系。 + +我深深贊同費曼教授所言:“Knowledge isn't free. You have to pay attention.”從這個意義上看,這本書並非完全“免費”。為了不辜負你為本書所付出的寶貴“注意力”,我會竭盡所能,投入最大的“注意力”來完成本書的創作。 + +本人自知學疏才淺,書中內容雖然已經過一段時間的打磨,但一定仍有許多錯誤,懇請各位老師和同學批評指正。 + +![Hello 演算法](../assets/covers/chapter_hello_algo.jpg){ class="cover-image" } + +
+

Hello,演算法!

+
+ +計算機的出現給世界帶來了巨大變革,它憑藉高速的計算能力和出色的可程式設計性,成為了執行演算法與處理資料的理想媒介。無論是電子遊戲的逼真畫面、自動駕駛的智慧決策,還是 AlphaGo 的精彩棋局、ChatGPT 的自然互動,這些應用都是演算法在計算機上的精妙演繹。 + +事實上,在計算機問世之前,演算法和資料結構就已經存在於世界的各個角落。早期的演算法相對簡單,例如古代的計數方法和工具製作步驟等。隨著文明的進步,演算法逐漸變得更加精細和複雜。從巧奪天工的匠人技藝、到解放生產力的工業產品、再到宇宙執行的科學規律,幾乎每一件平凡或令人驚歎的事物背後,都隱藏著精妙的演算法思想。 + +同樣,資料結構無處不在:大到社會網路,小到地鐵線路,許多系統都可以建模為“圖”;大到一個國家,小到一個家庭,社會的主要組織形式呈現出“樹”的特徵;冬天的衣服就像“堆疊”,最先穿上的最後才能脫下;羽毛球筒則如同“佇列”,一端放入、另一端取出;字典就像一個“雜湊表”,能夠快速查詢目標詞條。 + +本書旨在透過清晰易懂的動畫圖解和可執行的程式碼示例,使讀者理解演算法和資料結構的核心概念,並能夠透過程式設計來實現它們。在此基礎上,本書致力於揭示演算法在複雜世界中的生動體現,展現演算法之美。希望本書能夠幫助到你! diff --git a/zh-Hant/docs/chapter_introduction/algorithms_are_everywhere.md b/zh-Hant/docs/chapter_introduction/algorithms_are_everywhere.md new file mode 100644 index 000000000..dc19916bd --- /dev/null +++ b/zh-Hant/docs/chapter_introduction/algorithms_are_everywhere.md @@ -0,0 +1,66 @@ +--- +comments: true +--- + +# 1.1   演算法無處不在 + +當我們聽到“演算法”這個詞時,很自然地會想到數學。然而實際上,許多演算法並不涉及複雜數學,而是更多地依賴基本邏輯,這些邏輯在我們的日常生活中處處可見。 + +在正式探討演算法之前,有一個有趣的事實值得分享:**你已經在不知不覺中學會了許多演算法,並習慣將它們應用到日常生活中了**。下面我將舉幾個具體的例子來證實這一點。 + +**例一:查字典**。在字典裡,每個漢字都對應一個拼音,而字典是按照拼音字母順序排列的。假設我們需要查詢一個拼音首字母為 $r$ 的字,通常會按照圖 1-1 所示的方式實現。 + +1. 翻開字典約一半的頁數,檢視該頁的首字母是什麼,假設首字母為 $m$ 。 +2. 由於在拼音字母表中 $r$ 位於 $m$ 之後,所以排除字典前半部分,查詢範圍縮小到後半部分。 +3. 不斷重複步驟 `1.` 和 步驟 `2.` ,直至找到拼音首字母為 $r$ 的頁碼為止。 + +=== "<1>" + ![查字典步驟](algorithms_are_everywhere.assets/binary_search_dictionary_step1.png){ class="animation-figure" } + +=== "<2>" + ![binary_search_dictionary_step2](algorithms_are_everywhere.assets/binary_search_dictionary_step2.png){ class="animation-figure" } + +=== "<3>" + ![binary_search_dictionary_step3](algorithms_are_everywhere.assets/binary_search_dictionary_step3.png){ class="animation-figure" } + +=== "<4>" + ![binary_search_dictionary_step4](algorithms_are_everywhere.assets/binary_search_dictionary_step4.png){ class="animation-figure" } + +=== "<5>" + ![binary_search_dictionary_step5](algorithms_are_everywhere.assets/binary_search_dictionary_step5.png){ class="animation-figure" } + +

圖 1-1   查字典步驟

+ +查字典這個小學生必備技能,實際上就是著名的“二分搜尋”演算法。從資料結構的角度,我們可以把字典視為一個已排序的“陣列”;從演算法的角度,我們可以將上述查字典的一系列操作看作“二分搜尋”。 + +**例二:整理撲克**。我們在打牌時,每局都需要整理手中的撲克牌,使其從小到大排列,實現流程如圖 1-2 所示。 + +1. 將撲克牌劃分為“有序”和“無序”兩部分,並假設初始狀態下最左 1 張撲克牌已經有序。 +2. 在無序部分抽出一張撲克牌,插入至有序部分的正確位置;完成後最左 2 張撲克已經有序。 +3. 不斷迴圈步驟 `2.` ,每一輪將一張撲克牌從無序部分插入至有序部分,直至所有撲克牌都有序。 + +![撲克排序步驟](algorithms_are_everywhere.assets/playing_cards_sorting.png){ class="animation-figure" } + +

圖 1-2   撲克排序步驟

+ +上述整理撲克牌的方法本質上是“插入排序”演算法,它在處理小型資料集時非常高效。許多程式語言的排序庫函式中都有插入排序的身影。 + +**例三:貨幣找零**。假設我們在超市購買了 $69$ 元的商品,給了收銀員 $100$ 元,則收銀員需要找我們 $31$ 元。他會很自然地完成如圖 1-3 所示的思考。 + +1. 可選項是比 $31$ 元面值更小的貨幣,包括 $1$ 元、$5$ 元、$10$ 元、$20$ 元。 +2. 從可選項中拿出最大的 $20$ 元,剩餘 $31 - 20 = 11$ 元。 +3. 從剩餘可選項中拿出最大的 $10$ 元,剩餘 $11 - 10 = 1$ 元。 +4. 從剩餘可選項中拿出最大的 $1$ 元,剩餘 $1 - 1 = 0$ 元。 +5. 完成找零,方案為 $20 + 10 + 1 = 31$ 元。 + +![貨幣找零過程](algorithms_are_everywhere.assets/greedy_change.png){ class="animation-figure" } + +

圖 1-3   貨幣找零過程

+ +在以上步驟中,我們每一步都採取當前看來最好的選擇(儘可能用大面額的貨幣),最終得到了可行的找零方案。從資料結構與演算法的角度看,這種方法本質上是“貪婪”演算法。 + +小到烹飪一道菜,大到星際航行,幾乎所有問題的解決都離不開演算法。計算機的出現使得我們能夠透過程式設計將資料結構儲存在記憶體中,同時編寫程式碼呼叫 CPU 和 GPU 執行演算法。這樣一來,我們就能把生活中的問題轉移到計算機上,以更高效的方式解決各種複雜問題。 + +!!! tip + + 如果你對資料結構、演算法、陣列和二分搜尋等概念仍感到一知半解,請繼續往下閱讀,本書將引導你邁入資料結構與演算法的知識殿堂。 diff --git a/zh-Hant/docs/chapter_introduction/index.md b/zh-Hant/docs/chapter_introduction/index.md new file mode 100644 index 000000000..193a1a54b --- /dev/null +++ b/zh-Hant/docs/chapter_introduction/index.md @@ -0,0 +1,20 @@ +--- +comments: true +icon: material/calculator-variant-outline +--- + +# 第 1 章   初識演算法 + +![初識演算法](../assets/covers/chapter_introduction.jpg){ class="cover-image" } + +!!! abstract + + 一位少女翩翩起舞,與資料交織在一起,裙襬上飄揚著演算法的旋律。 + + 她邀請你共舞,請緊跟她的步伐,踏入充滿邏輯與美感的演算法世界。 + +## Chapter Contents + +- [1.1   演算法無處不在](https://www.hello-algo.com/en/chapter_introduction/algorithms_are_everywhere/) +- [1.2   演算法是什麼](https://www.hello-algo.com/en/chapter_introduction/what_is_dsa/) +- [1.3   小結](https://www.hello-algo.com/en/chapter_introduction/summary/) diff --git a/zh-Hant/docs/chapter_introduction/summary.md b/zh-Hant/docs/chapter_introduction/summary.md new file mode 100644 index 000000000..abc02ce79 --- /dev/null +++ b/zh-Hant/docs/chapter_introduction/summary.md @@ -0,0 +1,13 @@ +--- +comments: true +--- + +# 1.3   小結 + +- 演算法在日常生活中無處不在,並不是遙不可及的高深知識。實際上,我們已經在不知不覺中學會了許多演算法,用以解決生活中的大小問題。 +- 查字典的原理與二分搜尋演算法相一致。二分搜尋演算法體現了分而治之的重要演算法思想。 +- 整理撲克的過程與插入排序演算法非常類似。插入排序演算法適合排序小型資料集。 +- 貨幣找零的步驟本質上是貪婪演算法,每一步都採取當前看來最好的選擇。 +- 演算法是在有限時間內解決特定問題的一組指令或操作步驟,而資料結構是計算機中組織和儲存資料的方式。 +- 資料結構與演算法緊密相連。資料結構是演算法的基石,而演算法是資料結構發揮作用的舞臺。 +- 我們可以將資料結構與演算法類比為拼裝積木,積木代表資料,積木的形狀和連線方式等代表資料結構,拼裝積木的步驟則對應演算法。 diff --git a/zh-Hant/docs/chapter_introduction/what_is_dsa.md b/zh-Hant/docs/chapter_introduction/what_is_dsa.md new file mode 100644 index 000000000..51d0f4972 --- /dev/null +++ b/zh-Hant/docs/chapter_introduction/what_is_dsa.md @@ -0,0 +1,65 @@ +--- +comments: true +--- + +# 1.2   演算法是什麼 + +## 1.2.1   演算法定義 + +演算法(algorithm)是在有限時間內解決特定問題的一組指令或操作步驟,它具有以下特性。 + +- 問題是明確的,包含清晰的輸入和輸出定義。 +- 具有可行性,能夠在有限步驟、時間和記憶體空間下完成。 +- 各步驟都有確定的含義,在相同的輸入和執行條件下,輸出始終相同。 + +## 1.2.2   資料結構定義 + +資料結構(data structure)是計算機中組織和儲存資料的方式,具有以下設計目標。 + +- 空間佔用儘量少,以節省計算機記憶體。 +- 資料操作儘可能快速,涵蓋資料訪問、新增、刪除、更新等。 +- 提供簡潔的資料表示和邏輯資訊,以便演算法高效執行。 + +**資料結構設計是一個充滿權衡的過程**。如果想在某方面取得提升,往往需要在另一方面作出妥協。下面舉兩個例子。 + +- 鏈結串列相較於陣列,在資料新增和刪除操作上更加便捷,但犧牲了資料訪問速度。 +- 圖相較於鏈結串列,提供了更豐富的邏輯資訊,但需要佔用更大的記憶體空間。 + +## 1.2.3   資料結構與演算法的關係 + +如圖 1-4 所示,資料結構與演算法高度相關、緊密結合,具體表現在以下三個方面。 + +- 資料結構是演算法的基石。資料結構為演算法提供了結構化儲存的資料,以及操作資料的方法。 +- 演算法是資料結構發揮作用的舞臺。資料結構本身僅儲存資料資訊,結合演算法才能解決特定問題。 +- 演算法通常可以基於不同的資料結構實現,但執行效率可能相差很大,選擇合適的資料結構是關鍵。 + +![資料結構與演算法的關係](what_is_dsa.assets/relationship_between_data_structure_and_algorithm.png){ class="animation-figure" } + +

圖 1-4   資料結構與演算法的關係

+ +資料結構與演算法猶如圖 1-5 所示的拼裝積木。一套積木,除了包含許多零件之外,還附有詳細的組裝說明書。我們按照說明書一步步操作,就能組裝出精美的積木模型。 + +![拼裝積木](what_is_dsa.assets/assembling_blocks.png){ class="animation-figure" } + +

圖 1-5   拼裝積木

+ +兩者的詳細對應關係如表 1-1 所示。 + +

表 1-1   將資料結構與演算法類比為拼裝積木

+ +
+ +| 資料結構與演算法 | 拼裝積木 | +| -------------- | ---------------------------------------- | +| 輸入資料 | 未拼裝的積木 | +| 資料結構 | 積木組織形式,包括形狀、大小、連線方式等 | +| 演算法 | 把積木拼成目標形態的一系列操作步驟 | +| 輸出資料 | 積木模型 | + +
+ +值得說明的是,資料結構與演算法是獨立於程式語言的。正因如此,本書得以提供基於多種程式語言的實現。 + +!!! tip "約定俗成的簡稱" + + 在實際討論時,我們通常會將“資料結構與演算法”簡稱為“演算法”。比如眾所周知的 LeetCode 演算法題目,實際上同時考查資料結構和演算法兩方面的知識。 diff --git a/zh-Hant/docs/chapter_preface/about_the_book.md b/zh-Hant/docs/chapter_preface/about_the_book.md new file mode 100644 index 000000000..f7433d8c0 --- /dev/null +++ b/zh-Hant/docs/chapter_preface/about_the_book.md @@ -0,0 +1,56 @@ +--- +comments: true +--- + +# 0.1   關於本書 + +本專案旨在建立一本開源、免費、對新手友好的資料結構與演算法入門教程。 + +- 全書採用動畫圖解,結構化地講解資料結構與演算法知識,內容清晰易懂,學習曲線平滑。 +- 演算法源程式碼皆可一鍵執行,支持 Python、C++、Java、C#、Go、Swift、JavaScript、TypeScript、Dart、Rust、C 和 Zig 等語言。 +- 鼓勵讀者在線上章節評論區互幫互助、共同進步,提問與評論通常可在兩日內得到回覆。 + +## 0.1.1   讀者物件 + +若你是演算法初學者,從未接觸過演算法,或者已經有一些刷題經驗,對資料結構與演算法有模糊的認識,在會與不會之間反覆橫跳,那麼本書正是為你量身定製的! + +如果你已經積累一定的刷題量,熟悉大部分題型,那麼本書可助你回顧與梳理演算法知識體系,倉庫源程式碼可以當作“刷題工具庫”或“演算法字典”來使用。 + +若你是演算法“大神”,我們期待收到你的寶貴建議,或者[一起參與創作](https://www.hello-algo.com/chapter_appendix/contribution/)。 + +!!! success "前置條件" + + 你需要至少具備任一語言的程式設計基礎,能夠閱讀和編寫簡單程式碼。 + +## 0.1.2   內容結構 + +本書的主要內容如圖 0-1 所示。 + +- **複雜度分析**:資料結構和演算法的評價維度與方法。時間複雜度和空間複雜度的推算方法、常見型別、示例等。 +- **資料結構**:基本資料型別和資料結構的分類方法。陣列、鏈結串列、堆疊、佇列、雜湊表、樹、堆積、圖等資料結構的定義、優缺點、常用操作、常見型別、典型應用、實現方法等。 +- **演算法**:搜尋、排序、分治、回溯、動態規劃、貪婪等演算法的定義、優缺點、效率、應用場景、解題步驟和示例問題等。 + +![本書主要內容](about_the_book.assets/hello_algo_mindmap.png){ class="animation-figure" } + +

圖 0-1   本書主要內容

+ +## 0.1.3   致謝 + +本書在開源社群眾多貢獻者的共同努力下不斷完善。感謝每一位投入時間與精力的撰稿人,他們是(按照 GitHub 自動生成的順序):krahets、codingonion、nuomi1、Gonglja、Reanon、justin-tse、danielsss、hpstory、S-N-O-R-L-A-X、night-cruise、msk397、gvenusleo、RiverTwilight、gyt95、zhuoqinyue、Zuoxun、Xia-Sang、mingXta、FangYuan33、GN-Yu、IsChristina、xBLACKICEx、guowei-gong、Cathay-Chen、mgisr、JoseHung、qualifier1024、pengchzn、Guanngxu、longsizhuo、L-Super、what-is-me、yuan0221、lhxsm、Slone123c、WSL0809、longranger2、theNefelibatas、xiongsp、JeffersonHuang、hongyun-robot、K3v123、yuelinxin、a16su、gaofer、malone6、Wonderdch、xjr7670、DullSword、Horbin-Magician、NI-SW、reeswell、XC-Zero、XiaChuerwu、yd-j、iron-irax、huawuque404、MolDuM、Nigh、KorsChen、foursevenlove、52coder、bubble9um、youshaoXG、curly210102、gltianwen、fanchenggang、Transmigration-zhou、FloranceYeh、FreddieLi、ShiMaRing、lipusheng、Javesun99、JackYang-hellobobo、shanghai-Jerry、0130w、Keynman、psychelzh、logan-qiu、ZnYang2018、MwumLi、1ch0、Phoenix0415、qingpeng9802、Richard-Zhang1019、QiLOL、Suremotoo、Turing-1024-Lee、Evilrabbit520、GaochaoZhu、ZJKung、linzeyan、hezhizhen、ZongYangL、beintentional、czruby、coderlef、dshlstarr、szu17dmy、fbigm、gledfish、hts0000、boloboloda、iStig、jiaxianhua、wenjianmin、keshida、kilikilikid、lclc6、lwbaptx、liuxjerry、lucaswangdev、lyl625760、chadyi、noobcodemaker、selear、siqyka、syd168、4yDX3906、tao363、wangwang105、weibk、yabo083、yi427、yishangzhang、zhouLion、baagod、ElaBosak233、xb534、luluxia、yanedie、thomasq0、YangXuanyi 和 th1nk3r-ing 。 + +本書的程式碼審閱工作由 codingonion、Gonglja、gvenusleo、hpstory、justin-tse、krahets、night-cruise、nuomi1 和 Reanon 完成(按照首字母順序排列)。感謝他們付出的時間與精力,正是他們確保了各語言程式碼的規範與統一。 + +在本書的創作過程中,我得到了許多人的幫助。 + +- 感謝我在公司的導師李汐博士,在一次暢談中你鼓勵我“快行動起來”,堅定了我寫這本書的決心; +- 感謝我的女朋友泡泡作為本書的首位讀者,從演算法小白的角度提出許多寶貴建議,使得本書更適合新手閱讀; +- 感謝騰寶、琦寶、飛寶為本書起了一個富有創意的名字,喚起大家寫下第一行程式碼“Hello World!”的美好回憶; +- 感謝校銓在智慧財產權方面提供的專業幫助,這對本開源書的完善起到了重要作用; +- 感謝蘇潼為本書設計了精美的封面和 logo ,並在我的強迫症的驅使下多次耐心修改; +- 感謝 @squidfunk 提供的排版建議,以及他開發的開源文件主題 [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material/tree/master) 。 + +在寫作過程中,我閱讀了許多關於資料結構與演算法的教材和文章。這些作品為本書提供了優秀的範本,確保了本書內容的準確性與品質。在此感謝所有老師和前輩的傑出貢獻! + +本書倡導手腦並用的學習方式,在這一點上我深受[《動手學深度學習》](https://github.com/d2l-ai/d2l-zh)的啟發。在此向各位讀者強烈推薦這本優秀的著作。 + +**衷心感謝我的父母,正是你們一直以來的支持與鼓勵,讓我有機會做這件富有趣味的事**。 diff --git a/zh-Hant/docs/chapter_preface/index.md b/zh-Hant/docs/chapter_preface/index.md new file mode 100644 index 000000000..f0cb3a0d9 --- /dev/null +++ b/zh-Hant/docs/chapter_preface/index.md @@ -0,0 +1,20 @@ +--- +comments: true +icon: material/book-open-outline +--- + +# 第 0 章   前言 + +![前言](../assets/covers/chapter_preface.jpg){ class="cover-image" } + +!!! abstract + + 演算法猶如美妙的交響樂,每一行程式碼都像韻律般流淌。 + + 願這本書在你的腦海中輕輕響起,留下獨特而深刻的旋律。 + +## Chapter Contents + +- [0.1   關於本書](https://www.hello-algo.com/en/chapter_preface/about_the_book/) +- [0.2   如何使用本書](https://www.hello-algo.com/en/chapter_preface/suggestions/) +- [0.3   小結](https://www.hello-algo.com/en/chapter_preface/summary/) diff --git a/zh-Hant/docs/chapter_preface/suggestions.md b/zh-Hant/docs/chapter_preface/suggestions.md new file mode 100644 index 000000000..f9e8b8f75 --- /dev/null +++ b/zh-Hant/docs/chapter_preface/suggestions.md @@ -0,0 +1,265 @@ +--- +comments: true +--- + +# 0.2   如何使用本書 + +!!! tip + + 為了獲得最佳的閱讀體驗,建議你通讀本節內容。 + +## 0.2.1   行文風格約定 + +- 標題後標註 `*` 的是選讀章節,內容相對困難。如果你的時間有限,可以先跳過。 +- 專業術語會使用黑體(紙質版和 PDF 版)或新增下劃線(網頁版),例如陣列(array)。建議記住它們,以便閱讀文獻。 +- 重點內容和總結性語句會 **加粗**,這類文字值得特別關注。 +- 有特指含義的詞句會使用“引號”標註,以避免歧義。 +- 當涉及程式語言之間不一致的名詞時,本書均以 Python 為準,例如使用 `None` 來表示“空”。 +- 本書部分放棄了程式語言的註釋規範,以換取更加緊湊的內容排版。註釋主要分為三種類型:標題註釋、內容註釋、多行註釋。 + +=== "Python" + + ```python title="" + """標題註釋,用於標註函式、類別、測試樣例等""" + + # 內容註釋,用於詳解程式碼 + + """ + 多行 + 註釋 + """ + ``` + +=== "C++" + + ```cpp title="" + /* 標題註釋,用於標註函式、類別、測試樣例等 */ + + // 內容註釋,用於詳解程式碼 + + /** + * 多行 + * 註釋 + */ + ``` + +=== "Java" + + ```java title="" + /* 標題註釋,用於標註函式、類別、測試樣例等 */ + + // 內容註釋,用於詳解程式碼 + + /** + * 多行 + * 註釋 + */ + ``` + +=== "C#" + + ```csharp title="" + /* 標題註釋,用於標註函式、類別、測試樣例等 */ + + // 內容註釋,用於詳解程式碼 + + /** + * 多行 + * 註釋 + */ + ``` + +=== "Go" + + ```go title="" + /* 標題註釋,用於標註函式、類別、測試樣例等 */ + + // 內容註釋,用於詳解程式碼 + + /** + * 多行 + * 註釋 + */ + ``` + +=== "Swift" + + ```swift title="" + /* 標題註釋,用於標註函式、類別、測試樣例等 */ + + // 內容註釋,用於詳解程式碼 + + /** + * 多行 + * 註釋 + */ + ``` + +=== "JS" + + ```javascript title="" + /* 標題註釋,用於標註函式、類別、測試樣例等 */ + + // 內容註釋,用於詳解程式碼 + + /** + * 多行 + * 註釋 + */ + ``` + +=== "TS" + + ```typescript title="" + /* 標題註釋,用於標註函式、類別、測試樣例等 */ + + // 內容註釋,用於詳解程式碼 + + /** + * 多行 + * 註釋 + */ + ``` + +=== "Dart" + + ```dart title="" + /* 標題註釋,用於標註函式、類別、測試樣例等 */ + + // 內容註釋,用於詳解程式碼 + + /** + * 多行 + * 註釋 + */ + ``` + +=== "Rust" + + ```rust title="" + /* 標題註釋,用於標註函式、類別、測試樣例等 */ + + // 內容註釋,用於詳解程式碼 + + /** + * 多行 + * 註釋 + */ + ``` + +=== "C" + + ```c title="" + /* 標題註釋,用於標註函式、類別、測試樣例等 */ + + // 內容註釋,用於詳解程式碼 + + /** + * 多行 + * 註釋 + */ + ``` + +=== "Kotlin" + + ```kotlin title="" + /* 標題註釋,用於標註函式、類別、測試樣例等 */ + + // 內容註釋,用於詳解程式碼 + + /** + * 多行 + * 註釋 + */ + ``` + +=== "Ruby" + + ```ruby title="" + + ``` + +=== "Zig" + + ```zig title="" + // 標題註釋,用於標註函式、類別、測試樣例等 + + // 內容註釋,用於詳解程式碼 + + // 多行 + // 註釋 + ``` + +## 0.2.2   在動畫圖解中高效學習 + +相較於文字,影片和圖片具有更高的資訊密度和結構化程度,更易於理解。在本書中,**重點和難點知識將主要透過動畫以圖解形式展示**,而文字則作為解釋與補充。 + +如果你在閱讀本書時,發現某段內容提供瞭如圖 0-2 所示的動畫圖解,**請以圖為主、以文字為輔**,綜合兩者來理解內容。 + +![動畫圖解示例](../index.assets/animation.gif){ class="animation-figure" } + +

圖 0-2   動畫圖解示例

+ +## 0.2.3   在程式碼實踐中加深理解 + +本書的配套程式碼託管在 [GitHub 倉庫](https://github.com/krahets/hello-algo)。如圖 0-3 所示,**源程式碼附有測試樣例,可一鍵執行**。 + +如果時間允許,**建議你參照程式碼自行敲一遍**。如果學習時間有限,請至少通讀並執行所有程式碼。 + +與閱讀程式碼相比,編寫程式碼的過程往往能帶來更多收穫。**動手學,才是真的學**。 + +![執行程式碼示例](../index.assets/running_code.gif){ class="animation-figure" } + +

圖 0-3   執行程式碼示例

+ +執行程式碼的前置工作主要分為三步。 + +**第一步:安裝本地程式設計環境**。請參照附錄所示的[教程](https://www.hello-algo.com/chapter_appendix/installation/)進行安裝,如果已安裝,則可跳過此步驟。 + +**第二步:克隆或下載程式碼倉庫**。前往 [GitHub 倉庫](https://github.com/krahets/hello-algo)。如果已經安裝 [Git](https://git-scm.com/downloads) ,可以透過以下命令克隆本倉庫: + +```shell +git clone https://github.com/krahets/hello-algo.git +``` + +當然,你也可以在圖 0-4 所示的位置,點選“Download ZIP”按鈕直接下載程式碼壓縮包,然後在本地解壓即可。 + +![克隆倉庫與下載程式碼](suggestions.assets/download_code.png){ class="animation-figure" } + +

圖 0-4   克隆倉庫與下載程式碼

+ +**第三步:執行源程式碼**。如圖 0-5 所示,對於頂部標有檔案名稱的程式碼塊,我們可以在倉庫的 `codes` 檔案夾內找到對應的源程式碼檔案。源程式碼檔案可一鍵執行,將幫助你節省不必要的除錯時間,讓你能夠專注於學習內容。 + +![程式碼塊與對應的源程式碼檔案](suggestions.assets/code_md_to_repo.png){ class="animation-figure" } + +

圖 0-5   程式碼塊與對應的源程式碼檔案

+ +除了本地執行程式碼,**網頁版還支持 Python 程式碼的視覺化執行**(基於 [pythontutor](https://pythontutor.com/) 實現)。如圖 0-6 所示,你可以點選程式碼塊下方的“視覺化執行”來展開檢視,觀察演算法程式碼的執行過程;也可以點選“全屏觀看”,以獲得更好的閱覽體驗。 + +![Python 程式碼的視覺化執行](suggestions.assets/pythontutor_example.png){ class="animation-figure" } + +

圖 0-6   Python 程式碼的視覺化執行

+ +## 0.2.4   在提問討論中共同成長 + +在閱讀本書時,請不要輕易跳過那些沒學明白的知識點。**歡迎在評論區提出你的問題**,我和小夥伴們將竭誠為你解答,一般情況下可在兩天內回覆。 + +如圖 0-7 所示,網頁版每個章節的底部都配有評論區。希望你能多關注評論區的內容。一方面,你可以瞭解大家遇到的問題,從而查漏補缺,激發更深入的思考。另一方面,期待你能慷慨地回答其他小夥伴的問題,分享你的見解,幫助他人進步。 + +![評論區示例](../index.assets/comment.gif){ class="animation-figure" } + +

圖 0-7   評論區示例

+ +## 0.2.5   演算法學習路線 + +從總體上看,我們可以將學習資料結構與演算法的過程劃分為三個階段。 + +1. **階段一:演算法入門**。我們需要熟悉各種資料結構的特點和用法,學習不同演算法的原理、流程、用途和效率等方面的內容。 +2. **階段二:刷演算法題**。建議從熱門題目開刷,先積累至少 100 道題目,熟悉主流的演算法問題。初次刷題時,“知識遺忘”可能是一個挑戰,但請放心,這是很正常的。我們可以按照“艾賓浩斯遺忘曲線”來複習題目,通常在進行 3~5 輪的重複後,就能將其牢記在心。推薦的題單和刷題計劃請見此 [GitHub 倉庫](https://github.com/krahets/LeetCode-Book)。 +3. **階段三:搭建知識體系**。在學習方面,我們可以閱讀演算法專欄文章、解題框架和演算法教材,以不斷豐富知識體系。在刷題方面,可以嘗試採用進階刷題策略,如按專題分類、一題多解、一解多題等,相關的刷題心得可以在各個社群找到。 + +如圖 0-8 所示,本書內容主要涵蓋“階段一”,旨在幫助你更高效地展開階段二和階段三的學習。 + +![演算法學習路線](suggestions.assets/learning_route.png){ class="animation-figure" } + +

圖 0-8   演算法學習路線

diff --git a/zh-Hant/docs/chapter_preface/summary.md b/zh-Hant/docs/chapter_preface/summary.md new file mode 100644 index 000000000..1d371e534 --- /dev/null +++ b/zh-Hant/docs/chapter_preface/summary.md @@ -0,0 +1,12 @@ +--- +comments: true +--- + +# 0.3   小結 + +- 本書的主要受眾是演算法初學者。如果你已有一定基礎,本書能幫助你系統回顧演算法知識,書中源程式碼也可作為“刷題工具庫”使用。 +- 書中內容主要包括複雜度分析、資料結構和演算法三部分,涵蓋了該領域的大部分主題。 +- 對於演算法新手,在初學階段閱讀一本入門書至關重要,可以少走許多彎路。 +- 書中的動畫圖解通常用於介紹重點和難點知識。閱讀本書時,應給予這些內容更多關注。 +- 實踐乃學習程式設計之最佳途徑。強烈建議執行源程式碼並親自敲程式碼。 +- 本書網頁版的每個章節都設有評論區,歡迎隨時分享你的疑惑與見解。 diff --git a/zh-Hant/docs/chapter_reference/index.md b/zh-Hant/docs/chapter_reference/index.md new file mode 100644 index 000000000..b75d606cb --- /dev/null +++ b/zh-Hant/docs/chapter_reference/index.md @@ -0,0 +1,25 @@ +--- +icon: material/bookshelf +--- + +# 參考文獻 + +[1] Thomas H. Cormen, et al. Introduction to Algorithms (3rd Edition). + +[2] Aditya Bhargava. Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People (1st Edition). + +[3] Robert Sedgewick, et al. Algorithms (4th Edition). + +[4] 嚴蔚敏. 資料結構(C 語言版). + +[5] 鄧俊輝. 資料結構(C++ 語言版,第三版). + +[6] 馬克 艾倫 維斯著,陳越譯. 資料結構與演算法分析:Java語言描述(第三版). + +[7] 程傑. 大話資料結構. + +[8] 王爭. 資料結構與演算法之美. + +[9] Gayle Laakmann McDowell. Cracking the Coding Interview: 189 Programming Questions and Solutions (6th Edition). + +[10] Aston Zhang, et al. Dive into Deep Learning. diff --git a/zh-Hant/docs/chapter_searching/binary_search.md b/zh-Hant/docs/chapter_searching/binary_search.md new file mode 100755 index 000000000..286acaf73 --- /dev/null +++ b/zh-Hant/docs/chapter_searching/binary_search.md @@ -0,0 +1,721 @@ +--- +comments: true +--- + +# 10.1   二分搜尋 + +二分搜尋(binary search)是一種基於分治策略的高效搜尋演算法。它利用資料的有序性,每輪縮小一半搜尋範圍,直至找到目標元素或搜尋區間為空為止。 + +!!! question + + 給定一個長度為 $n$ 的陣列 `nums` ,元素按從小到大的順序排列且不重複。請查詢並返回元素 `target` 在該陣列中的索引。若陣列不包含該元素,則返回 $-1$ 。示例如圖 10-1 所示。 + +![二分搜尋示例資料](binary_search.assets/binary_search_example.png){ class="animation-figure" } + +

圖 10-1   二分搜尋示例資料

+ +如圖 10-2 所示,我們先初始化指標 $i = 0$ 和 $j = n - 1$ ,分別指向陣列首元素和尾元素,代表搜尋區間 $[0, n - 1]$ 。請注意,中括號表示閉區間,其包含邊界值本身。 + +接下來,迴圈執行以下兩步。 + +1. 計算中點索引 $m = \lfloor {(i + j) / 2} \rfloor$ ,其中 $\lfloor \: \rfloor$ 表示向下取整操作。 +2. 判斷 `nums[m]` 和 `target` 的大小關係,分為以下三種情況。 + 1. 當 `nums[m] < target` 時,說明 `target` 在區間 $[m + 1, j]$ 中,因此執行 $i = m + 1$ 。 + 2. 當 `nums[m] > target` 時,說明 `target` 在區間 $[i, m - 1]$ 中,因此執行 $j = m - 1$ 。 + 3. 當 `nums[m] = target` 時,說明找到 `target` ,因此返回索引 $m$ 。 + +若陣列不包含目標元素,搜尋區間最終會縮小為空。此時返回 $-1$ 。 + +=== "<1>" + ![二分搜尋流程](binary_search.assets/binary_search_step1.png){ class="animation-figure" } + +=== "<2>" + ![binary_search_step2](binary_search.assets/binary_search_step2.png){ class="animation-figure" } + +=== "<3>" + ![binary_search_step3](binary_search.assets/binary_search_step3.png){ class="animation-figure" } + +=== "<4>" + ![binary_search_step4](binary_search.assets/binary_search_step4.png){ class="animation-figure" } + +=== "<5>" + ![binary_search_step5](binary_search.assets/binary_search_step5.png){ class="animation-figure" } + +=== "<6>" + ![binary_search_step6](binary_search.assets/binary_search_step6.png){ class="animation-figure" } + +=== "<7>" + ![binary_search_step7](binary_search.assets/binary_search_step7.png){ class="animation-figure" } + +

圖 10-2   二分搜尋流程

+ +值得注意的是,由於 $i$ 和 $j$ 都是 `int` 型別,**因此 $i + j$ 可能會超出 `int` 型別的取值範圍**。為了避免大數越界,我們通常採用公式 $m = \lfloor {i + (j - i) / 2} \rfloor$ 來計算中點。 + +程式碼如下所示: + +=== "Python" + + ```python title="binary_search.py" + def binary_search(nums: list[int], target: int) -> int: + """二分搜尋(雙閉區間)""" + # 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素 + i, j = 0, len(nums) - 1 + # 迴圈,當搜尋區間為空時跳出(當 i > j 時為空) + while i <= j: + # 理論上 Python 的數字可以無限大(取決於記憶體大小),無須考慮大數越界問題 + m = (i + j) // 2 # 計算中點索引 m + if nums[m] < target: + i = m + 1 # 此情況說明 target 在區間 [m+1, j] 中 + elif nums[m] > target: + j = m - 1 # 此情況說明 target 在區間 [i, m-1] 中 + else: + return m # 找到目標元素,返回其索引 + return -1 # 未找到目標元素,返回 -1 + ``` + +=== "C++" + + ```cpp title="binary_search.cpp" + /* 二分搜尋(雙閉區間) */ + int binarySearch(vector &nums, int target) { + // 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素 + int i = 0, j = nums.size() - 1; + // 迴圈,當搜尋區間為空時跳出(當 i > j 時為空) + while (i <= j) { + int m = i + (j - i) / 2; // 計算中點索引 m + if (nums[m] < target) // 此情況說明 target 在區間 [m+1, j] 中 + i = m + 1; + else if (nums[m] > target) // 此情況說明 target 在區間 [i, m-1] 中 + j = m - 1; + else // 找到目標元素,返回其索引 + return m; + } + // 未找到目標元素,返回 -1 + return -1; + } + ``` + +=== "Java" + + ```java title="binary_search.java" + /* 二分搜尋(雙閉區間) */ + int binarySearch(int[] nums, int target) { + // 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素 + int i = 0, j = nums.length - 1; + // 迴圈,當搜尋區間為空時跳出(當 i > j 時為空) + while (i <= j) { + int m = i + (j - i) / 2; // 計算中點索引 m + if (nums[m] < target) // 此情況說明 target 在區間 [m+1, j] 中 + i = m + 1; + else if (nums[m] > target) // 此情況說明 target 在區間 [i, m-1] 中 + j = m - 1; + else // 找到目標元素,返回其索引 + return m; + } + // 未找到目標元素,返回 -1 + return -1; + } + ``` + +=== "C#" + + ```csharp title="binary_search.cs" + /* 二分搜尋(雙閉區間) */ + int BinarySearch(int[] nums, int target) { + // 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素 + int i = 0, j = nums.Length - 1; + // 迴圈,當搜尋區間為空時跳出(當 i > j 時為空) + while (i <= j) { + int m = i + (j - i) / 2; // 計算中點索引 m + if (nums[m] < target) // 此情況說明 target 在區間 [m+1, j] 中 + i = m + 1; + else if (nums[m] > target) // 此情況說明 target 在區間 [i, m-1] 中 + j = m - 1; + else // 找到目標元素,返回其索引 + return m; + } + // 未找到目標元素,返回 -1 + return -1; + } + ``` + +=== "Go" + + ```go title="binary_search.go" + /* 二分搜尋(雙閉區間) */ + func binarySearch(nums []int, target int) int { + // 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素 + i, j := 0, len(nums)-1 + // 迴圈,當搜尋區間為空時跳出(當 i > j 時為空) + for i <= j { + m := i + (j-i)/2 // 計算中點索引 m + if nums[m] < target { // 此情況說明 target 在區間 [m+1, j] 中 + i = m + 1 + } else if nums[m] > target { // 此情況說明 target 在區間 [i, m-1] 中 + j = m - 1 + } else { // 找到目標元素,返回其索引 + return m + } + } + // 未找到目標元素,返回 -1 + return -1 + } + ``` + +=== "Swift" + + ```swift title="binary_search.swift" + /* 二分搜尋(雙閉區間) */ + func binarySearch(nums: [Int], target: Int) -> Int { + // 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素 + var i = nums.startIndex + var j = nums.endIndex - 1 + // 迴圈,當搜尋區間為空時跳出(當 i > j 時為空) + while i <= j { + let m = i + (j - i) / 2 // 計算中點索引 m + if nums[m] < target { // 此情況說明 target 在區間 [m+1, j] 中 + i = m + 1 + } else if nums[m] > target { // 此情況說明 target 在區間 [i, m-1] 中 + j = m - 1 + } else { // 找到目標元素,返回其索引 + return m + } + } + // 未找到目標元素,返回 -1 + return -1 + } + ``` + +=== "JS" + + ```javascript title="binary_search.js" + /* 二分搜尋(雙閉區間) */ + function binarySearch(nums, target) { + // 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素 + let i = 0, + j = nums.length - 1; + // 迴圈,當搜尋區間為空時跳出(當 i > j 時為空) + while (i <= j) { + // 計算中點索引 m ,使用 parseInt() 向下取整 + const m = parseInt(i + (j - i) / 2); + if (nums[m] < target) + // 此情況說明 target 在區間 [m+1, j] 中 + i = m + 1; + else if (nums[m] > target) + // 此情況說明 target 在區間 [i, m-1] 中 + j = m - 1; + else return m; // 找到目標元素,返回其索引 + } + // 未找到目標元素,返回 -1 + return -1; + } + ``` + +=== "TS" + + ```typescript title="binary_search.ts" + /* 二分搜尋(雙閉區間) */ + function binarySearch(nums: number[], target: number): number { + // 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素 + let i = 0, + j = nums.length - 1; + // 迴圈,當搜尋區間為空時跳出(當 i > j 時為空) + while (i <= j) { + // 計算中點索引 m + const m = Math.floor(i + (j - i) / 2); + if (nums[m] < target) { + // 此情況說明 target 在區間 [m+1, j] 中 + i = m + 1; + } else if (nums[m] > target) { + // 此情況說明 target 在區間 [i, m-1] 中 + j = m - 1; + } else { + // 找到目標元素,返回其索引 + return m; + } + } + return -1; // 未找到目標元素,返回 -1 + } + ``` + +=== "Dart" + + ```dart title="binary_search.dart" + /* 二分搜尋(雙閉區間) */ + int binarySearch(List nums, int target) { + // 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素 + int i = 0, j = nums.length - 1; + // 迴圈,當搜尋區間為空時跳出(當 i > j 時為空) + while (i <= j) { + int m = i + (j - i) ~/ 2; // 計算中點索引 m + if (nums[m] < target) { + // 此情況說明 target 在區間 [m+1, j] 中 + i = m + 1; + } else if (nums[m] > target) { + // 此情況說明 target 在區間 [i, m-1] 中 + j = m - 1; + } else { + // 找到目標元素,返回其索引 + return m; + } + } + // 未找到目標元素,返回 -1 + return -1; + } + ``` + +=== "Rust" + + ```rust title="binary_search.rs" + /* 二分搜尋(雙閉區間) */ + fn binary_search(nums: &[i32], target: i32) -> i32 { + // 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素 + let mut i = 0; + let mut j = nums.len() as i32 - 1; + // 迴圈,當搜尋區間為空時跳出(當 i > j 時為空) + while i <= j { + let m = i + (j - i) / 2; // 計算中點索引 m + if nums[m as usize] < target { + // 此情況說明 target 在區間 [m+1, j] 中 + i = m + 1; + } else if nums[m as usize] > target { + // 此情況說明 target 在區間 [i, m-1] 中 + j = m - 1; + } else { + // 找到目標元素,返回其索引 + return m; + } + } + // 未找到目標元素,返回 -1 + return -1; + } + ``` + +=== "C" + + ```c title="binary_search.c" + /* 二分搜尋(雙閉區間) */ + int binarySearch(int *nums, int len, int target) { + // 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素 + int i = 0, j = len - 1; + // 迴圈,當搜尋區間為空時跳出(當 i > j 時為空) + while (i <= j) { + int m = i + (j - i) / 2; // 計算中點索引 m + if (nums[m] < target) // 此情況說明 target 在區間 [m+1, j] 中 + i = m + 1; + else if (nums[m] > target) // 此情況說明 target 在區間 [i, m-1] 中 + j = m - 1; + else // 找到目標元素,返回其索引 + return m; + } + // 未找到目標元素,返回 -1 + return -1; + } + ``` + +=== "Kotlin" + + ```kotlin title="binary_search.kt" + /* 二分搜尋(雙閉區間) */ + fun binarySearch(nums: IntArray, target: Int): Int { + // 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素 + var i = 0 + var j = nums.size - 1 + // 迴圈,當搜尋區間為空時跳出(當 i > j 時為空) + while (i <= j) { + val m = i + (j - i) / 2 // 計算中點索引 m + if (nums[m] < target) // 此情況說明 target 在區間 [m+1, j] 中 + i = m + 1 + else if (nums[m] > target) // 此情況說明 target 在區間 [i, m-1] 中 + j = m - 1 + else // 找到目標元素,返回其索引 + return m + } + // 未找到目標元素,返回 -1 + return -1 + } + ``` + +=== "Ruby" + + ```ruby title="binary_search.rb" + [class]{}-[func]{binary_search} + ``` + +=== "Zig" + + ```zig title="binary_search.zig" + // 二分搜尋(雙閉區間) + fn binarySearch(comptime T: type, nums: std.ArrayList(T), target: T) T { + // 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素 + var i: usize = 0; + var j: usize = nums.items.len - 1; + // 迴圈,當搜尋區間為空時跳出(當 i > j 時為空) + while (i <= j) { + var m = i + (j - i) / 2; // 計算中點索引 m + if (nums.items[m] < target) { // 此情況說明 target 在區間 [m+1, j] 中 + i = m + 1; + } else if (nums.items[m] > target) { // 此情況說明 target 在區間 [i, m-1] 中 + j = m - 1; + } else { // 找到目標元素,返回其索引 + return @intCast(m); + } + } + // 未找到目標元素,返回 -1 + return -1; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +**時間複雜度為 $O(\log n)$** :在二分迴圈中,區間每輪縮小一半,因此迴圈次數為 $\log_2 n$ 。 + +**空間複雜度為 $O(1)$** :指標 $i$ 和 $j$ 使用常數大小空間。 + +## 10.1.1   區間表示方法 + +除了上述雙閉區間外,常見的區間表示還有“左閉右開”區間,定義為 $[0, n)$ ,即左邊界包含自身,右邊界不包含自身。在該表示下,區間 $[i, j)$ 在 $i = j$ 時為空。 + +我們可以基於該表示實現具有相同功能的二分搜尋演算法: + +=== "Python" + + ```python title="binary_search.py" + def binary_search_lcro(nums: list[int], target: int) -> int: + """二分搜尋(左閉右開區間)""" + # 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1 + i, j = 0, len(nums) + # 迴圈,當搜尋區間為空時跳出(當 i = j 時為空) + while i < j: + m = (i + j) // 2 # 計算中點索引 m + if nums[m] < target: + i = m + 1 # 此情況說明 target 在區間 [m+1, j) 中 + elif nums[m] > target: + j = m # 此情況說明 target 在區間 [i, m) 中 + else: + return m # 找到目標元素,返回其索引 + return -1 # 未找到目標元素,返回 -1 + ``` + +=== "C++" + + ```cpp title="binary_search.cpp" + /* 二分搜尋(左閉右開區間) */ + int binarySearchLCRO(vector &nums, int target) { + // 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1 + int i = 0, j = nums.size(); + // 迴圈,當搜尋區間為空時跳出(當 i = j 時為空) + while (i < j) { + int m = i + (j - i) / 2; // 計算中點索引 m + if (nums[m] < target) // 此情況說明 target 在區間 [m+1, j) 中 + i = m + 1; + else if (nums[m] > target) // 此情況說明 target 在區間 [i, m) 中 + j = m; + else // 找到目標元素,返回其索引 + return m; + } + // 未找到目標元素,返回 -1 + return -1; + } + ``` + +=== "Java" + + ```java title="binary_search.java" + /* 二分搜尋(左閉右開區間) */ + int binarySearchLCRO(int[] nums, int target) { + // 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1 + int i = 0, j = nums.length; + // 迴圈,當搜尋區間為空時跳出(當 i = j 時為空) + while (i < j) { + int m = i + (j - i) / 2; // 計算中點索引 m + if (nums[m] < target) // 此情況說明 target 在區間 [m+1, j) 中 + i = m + 1; + else if (nums[m] > target) // 此情況說明 target 在區間 [i, m) 中 + j = m; + else // 找到目標元素,返回其索引 + return m; + } + // 未找到目標元素,返回 -1 + return -1; + } + ``` + +=== "C#" + + ```csharp title="binary_search.cs" + /* 二分搜尋(左閉右開區間) */ + int BinarySearchLCRO(int[] nums, int target) { + // 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1 + int i = 0, j = nums.Length; + // 迴圈,當搜尋區間為空時跳出(當 i = j 時為空) + while (i < j) { + int m = i + (j - i) / 2; // 計算中點索引 m + if (nums[m] < target) // 此情況說明 target 在區間 [m+1, j) 中 + i = m + 1; + else if (nums[m] > target) // 此情況說明 target 在區間 [i, m) 中 + j = m; + else // 找到目標元素,返回其索引 + return m; + } + // 未找到目標元素,返回 -1 + return -1; + } + ``` + +=== "Go" + + ```go title="binary_search.go" + /* 二分搜尋(左閉右開區間) */ + func binarySearchLCRO(nums []int, target int) int { + // 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1 + i, j := 0, len(nums) + // 迴圈,當搜尋區間為空時跳出(當 i = j 時為空) + for i < j { + m := i + (j-i)/2 // 計算中點索引 m + if nums[m] < target { // 此情況說明 target 在區間 [m+1, j) 中 + i = m + 1 + } else if nums[m] > target { // 此情況說明 target 在區間 [i, m) 中 + j = m + } else { // 找到目標元素,返回其索引 + return m + } + } + // 未找到目標元素,返回 -1 + return -1 + } + ``` + +=== "Swift" + + ```swift title="binary_search.swift" + /* 二分搜尋(左閉右開區間) */ + func binarySearchLCRO(nums: [Int], target: Int) -> Int { + // 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1 + var i = nums.startIndex + var j = nums.endIndex + // 迴圈,當搜尋區間為空時跳出(當 i = j 時為空) + while i < j { + let m = i + (j - i) / 2 // 計算中點索引 m + if nums[m] < target { // 此情況說明 target 在區間 [m+1, j) 中 + i = m + 1 + } else if nums[m] > target { // 此情況說明 target 在區間 [i, m) 中 + j = m + } else { // 找到目標元素,返回其索引 + return m + } + } + // 未找到目標元素,返回 -1 + return -1 + } + ``` + +=== "JS" + + ```javascript title="binary_search.js" + /* 二分搜尋(左閉右開區間) */ + function binarySearchLCRO(nums, target) { + // 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1 + let i = 0, + j = nums.length; + // 迴圈,當搜尋區間為空時跳出(當 i = j 時為空) + while (i < j) { + // 計算中點索引 m ,使用 parseInt() 向下取整 + const m = parseInt(i + (j - i) / 2); + if (nums[m] < target) + // 此情況說明 target 在區間 [m+1, j) 中 + i = m + 1; + else if (nums[m] > target) + // 此情況說明 target 在區間 [i, m) 中 + j = m; + // 找到目標元素,返回其索引 + else return m; + } + // 未找到目標元素,返回 -1 + return -1; + } + ``` + +=== "TS" + + ```typescript title="binary_search.ts" + /* 二分搜尋(左閉右開區間) */ + function binarySearchLCRO(nums: number[], target: number): number { + // 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1 + let i = 0, + j = nums.length; + // 迴圈,當搜尋區間為空時跳出(當 i = j 時為空) + while (i < j) { + // 計算中點索引 m + const m = Math.floor(i + (j - i) / 2); + if (nums[m] < target) { + // 此情況說明 target 在區間 [m+1, j) 中 + i = m + 1; + } else if (nums[m] > target) { + // 此情況說明 target 在區間 [i, m) 中 + j = m; + } else { + // 找到目標元素,返回其索引 + return m; + } + } + return -1; // 未找到目標元素,返回 -1 + } + ``` + +=== "Dart" + + ```dart title="binary_search.dart" + /* 二分搜尋(左閉右開區間) */ + int binarySearchLCRO(List nums, int target) { + // 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1 + int i = 0, j = nums.length; + // 迴圈,當搜尋區間為空時跳出(當 i = j 時為空) + while (i < j) { + int m = i + (j - i) ~/ 2; // 計算中點索引 m + if (nums[m] < target) { + // 此情況說明 target 在區間 [m+1, j) 中 + i = m + 1; + } else if (nums[m] > target) { + // 此情況說明 target 在區間 [i, m) 中 + j = m; + } else { + // 找到目標元素,返回其索引 + return m; + } + } + // 未找到目標元素,返回 -1 + return -1; + } + ``` + +=== "Rust" + + ```rust title="binary_search.rs" + /* 二分搜尋(左閉右開區間) */ + fn binary_search_lcro(nums: &[i32], target: i32) -> i32 { + // 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1 + let mut i = 0; + let mut j = nums.len() as i32; + // 迴圈,當搜尋區間為空時跳出(當 i = j 時為空) + while i < j { + let m = i + (j - i) / 2; // 計算中點索引 m + if nums[m as usize] < target { + // 此情況說明 target 在區間 [m+1, j) 中 + i = m + 1; + } else if nums[m as usize] > target { + // 此情況說明 target 在區間 [i, m) 中 + j = m; + } else { + // 找到目標元素,返回其索引 + return m; + } + } + // 未找到目標元素,返回 -1 + return -1; + } + ``` + +=== "C" + + ```c title="binary_search.c" + /* 二分搜尋(左閉右開區間) */ + int binarySearchLCRO(int *nums, int len, int target) { + // 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1 + int i = 0, j = len; + // 迴圈,當搜尋區間為空時跳出(當 i = j 時為空) + while (i < j) { + int m = i + (j - i) / 2; // 計算中點索引 m + if (nums[m] < target) // 此情況說明 target 在區間 [m+1, j) 中 + i = m + 1; + else if (nums[m] > target) // 此情況說明 target 在區間 [i, m) 中 + j = m; + else // 找到目標元素,返回其索引 + return m; + } + // 未找到目標元素,返回 -1 + return -1; + } + ``` + +=== "Kotlin" + + ```kotlin title="binary_search.kt" + /* 二分搜尋(左閉右開區間) */ + fun binarySearchLCRO(nums: IntArray, target: Int): Int { + // 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1 + var i = 0 + var j = nums.size + // 迴圈,當搜尋區間為空時跳出(當 i = j 時為空) + while (i < j) { + val m = i + (j - i) / 2 // 計算中點索引 m + if (nums[m] < target) // 此情況說明 target 在區間 [m+1, j) 中 + i = m + 1 + else if (nums[m] > target) // 此情況說明 target 在區間 [i, m) 中 + j = m + else // 找到目標元素,返回其索引 + return m + } + // 未找到目標元素,返回 -1 + return -1 + } + ``` + +=== "Ruby" + + ```ruby title="binary_search.rb" + [class]{}-[func]{binary_search_lcro} + ``` + +=== "Zig" + + ```zig title="binary_search.zig" + // 二分搜尋(左閉右開區間) + fn binarySearchLCRO(comptime T: type, nums: std.ArrayList(T), target: T) T { + // 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1 + var i: usize = 0; + var j: usize = nums.items.len; + // 迴圈,當搜尋區間為空時跳出(當 i = j 時為空) + while (i <= j) { + var m = i + (j - i) / 2; // 計算中點索引 m + if (nums.items[m] < target) { // 此情況說明 target 在區間 [m+1, j) 中 + i = m + 1; + } else if (nums.items[m] > target) { // 此情況說明 target 在區間 [i, m) 中 + j = m; + } else { // 找到目標元素,返回其索引 + return @intCast(m); + } + } + // 未找到目標元素,返回 -1 + return -1; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +如圖 10-3 所示,在兩種區間表示下,二分搜尋演算法的初始化、迴圈條件和縮小區間操作皆有所不同。 + +由於“雙閉區間”表示中的左右邊界都被定義為閉區間,因此透過指標 $i$ 和指標 $j$ 縮小區間的操作也是對稱的。這樣更不容易出錯,**因此一般建議採用“雙閉區間”的寫法**。 + +![兩種區間定義](binary_search.assets/binary_search_ranges.png){ class="animation-figure" } + +

圖 10-3   兩種區間定義

+ +## 10.1.2   優點與侷限性 + +二分搜尋在時間和空間方面都有較好的效能。 + +- 二分搜尋的時間效率高。在大資料量下,對數階的時間複雜度具有顯著優勢。例如,當資料大小 $n = 2^{20}$ 時,線性查詢需要 $2^{20} = 1048576$ 輪迴圈,而二分搜尋僅需 $\log_2 2^{20} = 20$ 輪迴圈。 +- 二分搜尋無須額外空間。相較於需要藉助額外空間的搜尋演算法(例如雜湊查詢),二分搜尋更加節省空間。 + +然而,二分搜尋並非適用於所有情況,主要有以下原因。 + +- 二分搜尋僅適用於有序資料。若輸入資料無序,為了使用二分搜尋而專門進行排序,得不償失。因為排序演算法的時間複雜度通常為 $O(n \log n)$ ,比線性查詢和二分搜尋都更高。對於頻繁插入元素的場景,為保持陣列有序性,需要將元素插入到特定位置,時間複雜度為 $O(n)$ ,也是非常昂貴的。 +- 二分搜尋僅適用於陣列。二分搜尋需要跳躍式(非連續地)訪問元素,而在鏈結串列中執行跳躍式訪問的效率較低,因此不適合應用在鏈結串列或基於鏈結串列實現的資料結構。 +- 小資料量下,線性查詢效能更佳。線上性查詢中,每輪只需 1 次判斷操作;而在二分搜尋中,需要 1 次加法、1 次除法、1 ~ 3 次判斷操作、1 次加法(減法),共 4 ~ 6 個單元操作;因此,當資料量 $n$ 較小時,線性查詢反而比二分搜尋更快。 diff --git a/zh-Hant/docs/chapter_searching/binary_search_edge.md b/zh-Hant/docs/chapter_searching/binary_search_edge.md new file mode 100644 index 000000000..ae1af133a --- /dev/null +++ b/zh-Hant/docs/chapter_searching/binary_search_edge.md @@ -0,0 +1,494 @@ +--- +comments: true +--- + +# 10.3   二分搜尋邊界 + +## 10.3.1   查詢左邊界 + +!!! question + + 給定一個長度為 $n$ 的有序陣列 `nums` ,其中可能包含重複元素。請返回陣列中最左一個元素 `target` 的索引。若陣列中不包含該元素,則返回 $-1$ 。 + +回憶二分搜尋插入點的方法,搜尋完成後 $i$ 指向最左一個 `target` ,**因此查詢插入點本質上是在查詢最左一個 `target` 的索引**。 + +考慮透過查詢插入點的函式實現查詢左邊界。請注意,陣列中可能不包含 `target` ,這種情況可能導致以下兩種結果。 + +- 插入點的索引 $i$ 越界。 +- 元素 `nums[i]` 與 `target` 不相等。 + +當遇到以上兩種情況時,直接返回 $-1$ 即可。程式碼如下所示: + +=== "Python" + + ```python title="binary_search_edge.py" + def binary_search_left_edge(nums: list[int], target: int) -> int: + """二分搜尋最左一個 target""" + # 等價於查詢 target 的插入點 + i = binary_search_insertion(nums, target) + # 未找到 target ,返回 -1 + if i == len(nums) or nums[i] != target: + return -1 + # 找到 target ,返回索引 i + return i + ``` + +=== "C++" + + ```cpp title="binary_search_edge.cpp" + /* 二分搜尋最左一個 target */ + int binarySearchLeftEdge(vector &nums, int target) { + // 等價於查詢 target 的插入點 + int i = binarySearchInsertion(nums, target); + // 未找到 target ,返回 -1 + if (i == nums.size() || nums[i] != target) { + return -1; + } + // 找到 target ,返回索引 i + return i; + } + ``` + +=== "Java" + + ```java title="binary_search_edge.java" + /* 二分搜尋最左一個 target */ + int binarySearchLeftEdge(int[] nums, int target) { + // 等價於查詢 target 的插入點 + int i = binary_search_insertion.binarySearchInsertion(nums, target); + // 未找到 target ,返回 -1 + if (i == nums.length || nums[i] != target) { + return -1; + } + // 找到 target ,返回索引 i + return i; + } + ``` + +=== "C#" + + ```csharp title="binary_search_edge.cs" + /* 二分搜尋最左一個 target */ + int BinarySearchLeftEdge(int[] nums, int target) { + // 等價於查詢 target 的插入點 + int i = binary_search_insertion.BinarySearchInsertion(nums, target); + // 未找到 target ,返回 -1 + if (i == nums.Length || nums[i] != target) { + return -1; + } + // 找到 target ,返回索引 i + return i; + } + ``` + +=== "Go" + + ```go title="binary_search_edge.go" + /* 二分搜尋最左一個 target */ + func binarySearchLeftEdge(nums []int, target int) int { + // 等價於查詢 target 的插入點 + i := binarySearchInsertion(nums, target) + // 未找到 target ,返回 -1 + if i == len(nums) || nums[i] != target { + return -1 + } + // 找到 target ,返回索引 i + return i + } + ``` + +=== "Swift" + + ```swift title="binary_search_edge.swift" + /* 二分搜尋最左一個 target */ + func binarySearchLeftEdge(nums: [Int], target: Int) -> Int { + // 等價於查詢 target 的插入點 + let i = binarySearchInsertion(nums: nums, target: target) + // 未找到 target ,返回 -1 + if i == nums.endIndex || nums[i] != target { + return -1 + } + // 找到 target ,返回索引 i + return i + } + ``` + +=== "JS" + + ```javascript title="binary_search_edge.js" + /* 二分搜尋最左一個 target */ + function binarySearchLeftEdge(nums, target) { + // 等價於查詢 target 的插入點 + const i = binarySearchInsertion(nums, target); + // 未找到 target ,返回 -1 + if (i === nums.length || nums[i] !== target) { + return -1; + } + // 找到 target ,返回索引 i + return i; + } + ``` + +=== "TS" + + ```typescript title="binary_search_edge.ts" + /* 二分搜尋最左一個 target */ + function binarySearchLeftEdge(nums: Array, target: number): number { + // 等價於查詢 target 的插入點 + const i = binarySearchInsertion(nums, target); + // 未找到 target ,返回 -1 + if (i === nums.length || nums[i] !== target) { + return -1; + } + // 找到 target ,返回索引 i + return i; + } + ``` + +=== "Dart" + + ```dart title="binary_search_edge.dart" + /* 二分搜尋最左一個 target */ + int binarySearchLeftEdge(List nums, int target) { + // 等價於查詢 target 的插入點 + int i = binarySearchInsertion(nums, target); + // 未找到 target ,返回 -1 + if (i == nums.length || nums[i] != target) { + return -1; + } + // 找到 target ,返回索引 i + return i; + } + ``` + +=== "Rust" + + ```rust title="binary_search_edge.rs" + /* 二分搜尋最左一個 target */ + fn binary_search_left_edge(nums: &[i32], target: i32) -> i32 { + // 等價於查詢 target 的插入點 + let i = binary_search_insertion(nums, target); + // 未找到 target ,返回 -1 + if i == nums.len() as i32 || nums[i as usize] != target { + return -1; + } + // 找到 target ,返回索引 i + i + } + ``` + +=== "C" + + ```c title="binary_search_edge.c" + /* 二分搜尋最左一個 target */ + int binarySearchLeftEdge(int *nums, int numSize, int target) { + // 等價於查詢 target 的插入點 + int i = binarySearchInsertion(nums, numSize, target); + // 未找到 target ,返回 -1 + if (i == numSize || nums[i] != target) { + return -1; + } + // 找到 target ,返回索引 i + return i; + } + ``` + +=== "Kotlin" + + ```kotlin title="binary_search_edge.kt" + /* 二分搜尋最左一個 target */ + fun binarySearchLeftEdge(nums: IntArray, target: Int): Int { + // 等價於查詢 target 的插入點 + val i = binarySearchInsertion(nums, target) + // 未找到 target ,返回 -1 + if (i == nums.size || nums[i] != target) { + return -1 + } + // 找到 target ,返回索引 i + return i + } + ``` + +=== "Ruby" + + ```ruby title="binary_search_edge.rb" + [class]{}-[func]{binary_search_left_edge} + ``` + +=== "Zig" + + ```zig title="binary_search_edge.zig" + [class]{}-[func]{binarySearchLeftEdge} + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 10.3.2   查詢右邊界 + +那麼如何查詢最右一個 `target` 呢?最直接的方式是修改程式碼,替換在 `nums[m] == target` 情況下的指標收縮操作。程式碼在此省略,有興趣的讀者可以自行實現。 + +下面我們介紹兩種更加取巧的方法。 + +### 1.   複用查詢左邊界 + +實際上,我們可以利用查詢最左元素的函式來查詢最右元素,具體方法為:**將查詢最右一個 `target` 轉化為查詢最左一個 `target + 1`**。 + +如圖 10-7 所示,查詢完成後,指標 $i$ 指向最左一個 `target + 1`(如果存在),而 $j$ 指向最右一個 `target` ,**因此返回 $j$ 即可**。 + +![將查詢右邊界轉化為查詢左邊界](binary_search_edge.assets/binary_search_right_edge_by_left_edge.png){ class="animation-figure" } + +

圖 10-7   將查詢右邊界轉化為查詢左邊界

+ +請注意,返回的插入點是 $i$ ,因此需要將其減 $1$ ,從而獲得 $j$ : + +=== "Python" + + ```python title="binary_search_edge.py" + def binary_search_right_edge(nums: list[int], target: int) -> int: + """二分搜尋最右一個 target""" + # 轉化為查詢最左一個 target + 1 + i = binary_search_insertion(nums, target + 1) + # j 指向最右一個 target ,i 指向首個大於 target 的元素 + j = i - 1 + # 未找到 target ,返回 -1 + if j == -1 or nums[j] != target: + return -1 + # 找到 target ,返回索引 j + return j + ``` + +=== "C++" + + ```cpp title="binary_search_edge.cpp" + /* 二分搜尋最右一個 target */ + int binarySearchRightEdge(vector &nums, int target) { + // 轉化為查詢最左一個 target + 1 + int i = binarySearchInsertion(nums, target + 1); + // j 指向最右一個 target ,i 指向首個大於 target 的元素 + int j = i - 1; + // 未找到 target ,返回 -1 + if (j == -1 || nums[j] != target) { + return -1; + } + // 找到 target ,返回索引 j + return j; + } + ``` + +=== "Java" + + ```java title="binary_search_edge.java" + /* 二分搜尋最右一個 target */ + int binarySearchRightEdge(int[] nums, int target) { + // 轉化為查詢最左一個 target + 1 + int i = binary_search_insertion.binarySearchInsertion(nums, target + 1); + // j 指向最右一個 target ,i 指向首個大於 target 的元素 + int j = i - 1; + // 未找到 target ,返回 -1 + if (j == -1 || nums[j] != target) { + return -1; + } + // 找到 target ,返回索引 j + return j; + } + ``` + +=== "C#" + + ```csharp title="binary_search_edge.cs" + /* 二分搜尋最右一個 target */ + int BinarySearchRightEdge(int[] nums, int target) { + // 轉化為查詢最左一個 target + 1 + int i = binary_search_insertion.BinarySearchInsertion(nums, target + 1); + // j 指向最右一個 target ,i 指向首個大於 target 的元素 + int j = i - 1; + // 未找到 target ,返回 -1 + if (j == -1 || nums[j] != target) { + return -1; + } + // 找到 target ,返回索引 j + return j; + } + ``` + +=== "Go" + + ```go title="binary_search_edge.go" + /* 二分搜尋最右一個 target */ + func binarySearchRightEdge(nums []int, target int) int { + // 轉化為查詢最左一個 target + 1 + i := binarySearchInsertion(nums, target+1) + // j 指向最右一個 target ,i 指向首個大於 target 的元素 + j := i - 1 + // 未找到 target ,返回 -1 + if j == -1 || nums[j] != target { + return -1 + } + // 找到 target ,返回索引 j + return j + } + ``` + +=== "Swift" + + ```swift title="binary_search_edge.swift" + /* 二分搜尋最右一個 target */ + func binarySearchRightEdge(nums: [Int], target: Int) -> Int { + // 轉化為查詢最左一個 target + 1 + let i = binarySearchInsertion(nums: nums, target: target + 1) + // j 指向最右一個 target ,i 指向首個大於 target 的元素 + let j = i - 1 + // 未找到 target ,返回 -1 + if j == -1 || nums[j] != target { + return -1 + } + // 找到 target ,返回索引 j + return j + } + ``` + +=== "JS" + + ```javascript title="binary_search_edge.js" + /* 二分搜尋最右一個 target */ + function binarySearchRightEdge(nums, target) { + // 轉化為查詢最左一個 target + 1 + const i = binarySearchInsertion(nums, target + 1); + // j 指向最右一個 target ,i 指向首個大於 target 的元素 + const j = i - 1; + // 未找到 target ,返回 -1 + if (j === -1 || nums[j] !== target) { + return -1; + } + // 找到 target ,返回索引 j + return j; + } + ``` + +=== "TS" + + ```typescript title="binary_search_edge.ts" + /* 二分搜尋最右一個 target */ + function binarySearchRightEdge(nums: Array, target: number): number { + // 轉化為查詢最左一個 target + 1 + const i = binarySearchInsertion(nums, target + 1); + // j 指向最右一個 target ,i 指向首個大於 target 的元素 + const j = i - 1; + // 未找到 target ,返回 -1 + if (j === -1 || nums[j] !== target) { + return -1; + } + // 找到 target ,返回索引 j + return j; + } + ``` + +=== "Dart" + + ```dart title="binary_search_edge.dart" + /* 二分搜尋最右一個 target */ + int binarySearchRightEdge(List nums, int target) { + // 轉化為查詢最左一個 target + 1 + int i = binarySearchInsertion(nums, target + 1); + // j 指向最右一個 target ,i 指向首個大於 target 的元素 + int j = i - 1; + // 未找到 target ,返回 -1 + if (j == -1 || nums[j] != target) { + return -1; + } + // 找到 target ,返回索引 j + return j; + } + ``` + +=== "Rust" + + ```rust title="binary_search_edge.rs" + /* 二分搜尋最右一個 target */ + fn binary_search_right_edge(nums: &[i32], target: i32) -> i32 { + // 轉化為查詢最左一個 target + 1 + let i = binary_search_insertion(nums, target + 1); + // j 指向最右一個 target ,i 指向首個大於 target 的元素 + let j = i - 1; + // 未找到 target ,返回 -1 + if j == -1 || nums[j as usize] != target { + return -1; + } + // 找到 target ,返回索引 j + j + } + ``` + +=== "C" + + ```c title="binary_search_edge.c" + /* 二分搜尋最右一個 target */ + int binarySearchRightEdge(int *nums, int numSize, int target) { + // 轉化為查詢最左一個 target + 1 + int i = binarySearchInsertion(nums, numSize, target + 1); + // j 指向最右一個 target ,i 指向首個大於 target 的元素 + int j = i - 1; + // 未找到 target ,返回 -1 + if (j == -1 || nums[j] != target) { + return -1; + } + // 找到 target ,返回索引 j + return j; + } + ``` + +=== "Kotlin" + + ```kotlin title="binary_search_edge.kt" + /* 二分搜尋最右一個 target */ + fun binarySearchRightEdge(nums: IntArray, target: Int): Int { + // 轉化為查詢最左一個 target + 1 + val i = binarySearchInsertion(nums, target + 1) + // j 指向最右一個 target ,i 指向首個大於 target 的元素 + val j = i - 1 + // 未找到 target ,返回 -1 + if (j == -1 || nums[j] != target) { + return -1 + } + // 找到 target ,返回索引 j + return j + } + ``` + +=== "Ruby" + + ```ruby title="binary_search_edge.rb" + [class]{}-[func]{binary_search_right_edge} + ``` + +=== "Zig" + + ```zig title="binary_search_edge.zig" + [class]{}-[func]{binarySearchRightEdge} + ``` + +??? pythontutor "視覺化執行" + +
+ + +### 2.   轉化為查詢元素 + +我們知道,當陣列不包含 `target` 時,最終 $i$ 和 $j$ 會分別指向首個大於、小於 `target` 的元素。 + +因此,如圖 10-8 所示,我們可以構造一個陣列中不存在的元素,用於查詢左右邊界。 + +- 查詢最左一個 `target` :可以轉化為查詢 `target - 0.5` ,並返回指標 $i$ 。 +- 查詢最右一個 `target` :可以轉化為查詢 `target + 0.5` ,並返回指標 $j$ 。 + +![將查詢邊界轉化為查詢元素](binary_search_edge.assets/binary_search_edge_by_element.png){ class="animation-figure" } + +

圖 10-8   將查詢邊界轉化為查詢元素

+ +程式碼在此省略,以下兩點值得注意。 + +- 給定陣列不包含小數,這意味著我們無須關心如何處理相等的情況。 +- 因為該方法引入了小數,所以需要將函式中的變數 `target` 改為浮點數型別(Python 無須改動)。 diff --git a/zh-Hant/docs/chapter_searching/binary_search_insertion.md b/zh-Hant/docs/chapter_searching/binary_search_insertion.md new file mode 100644 index 000000000..c49bce901 --- /dev/null +++ b/zh-Hant/docs/chapter_searching/binary_search_insertion.md @@ -0,0 +1,648 @@ +--- +comments: true +--- + +# 10.2   二分搜尋插入點 + +二分搜尋不僅可用於搜尋目標元素,還可用於解決許多變種問題,比如搜尋目標元素的插入位置。 + +## 10.2.1   無重複元素的情況 + +!!! question + + 給定一個長度為 $n$ 的有序陣列 `nums` 和一個元素 `target` ,陣列不存在重複元素。現將 `target` 插入陣列 `nums` 中,並保持其有序性。若陣列中已存在元素 `target` ,則插入到其左方。請返回插入後 `target` 在陣列中的索引。示例如圖 10-4 所示。 + +![二分搜尋插入點示例資料](binary_search_insertion.assets/binary_search_insertion_example.png){ class="animation-figure" } + +

圖 10-4   二分搜尋插入點示例資料

+ +如果想複用上一節的二分搜尋程式碼,則需要回答以下兩個問題。 + +**問題一**:當陣列中包含 `target` 時,插入點的索引是否是該元素的索引? + +題目要求將 `target` 插入到相等元素的左邊,這意味著新插入的 `target` 替換了原來 `target` 的位置。也就是說,**當陣列包含 `target` 時,插入點的索引就是該 `target` 的索引**。 + +**問題二**:當陣列中不存在 `target` 時,插入點是哪個元素的索引? + +進一步思考二分搜尋過程:當 `nums[m] < target` 時 $i$ 移動,這意味著指標 $i$ 在向大於等於 `target` 的元素靠近。同理,指標 $j$ 始終在向小於等於 `target` 的元素靠近。 + +因此二分結束時一定有:$i$ 指向首個大於 `target` 的元素,$j$ 指向首個小於 `target` 的元素。**易得當陣列不包含 `target` 時,插入索引為 $i$** 。程式碼如下所示: + +=== "Python" + + ```python title="binary_search_insertion.py" + def binary_search_insertion_simple(nums: list[int], target: int) -> int: + """二分搜尋插入點(無重複元素)""" + i, j = 0, len(nums) - 1 # 初始化雙閉區間 [0, n-1] + while i <= j: + m = (i + j) // 2 # 計算中點索引 m + if nums[m] < target: + i = m + 1 # target 在區間 [m+1, j] 中 + elif nums[m] > target: + j = m - 1 # target 在區間 [i, m-1] 中 + else: + return m # 找到 target ,返回插入點 m + # 未找到 target ,返回插入點 i + return i + ``` + +=== "C++" + + ```cpp title="binary_search_insertion.cpp" + /* 二分搜尋插入點(無重複元素) */ + int binarySearchInsertionSimple(vector &nums, int target) { + int i = 0, j = nums.size() - 1; // 初始化雙閉區間 [0, n-1] + while (i <= j) { + int m = i + (j - i) / 2; // 計算中點索引 m + if (nums[m] < target) { + i = m + 1; // target 在區間 [m+1, j] 中 + } else if (nums[m] > target) { + j = m - 1; // target 在區間 [i, m-1] 中 + } else { + return m; // 找到 target ,返回插入點 m + } + } + // 未找到 target ,返回插入點 i + return i; + } + ``` + +=== "Java" + + ```java title="binary_search_insertion.java" + /* 二分搜尋插入點(無重複元素) */ + int binarySearchInsertionSimple(int[] nums, int target) { + int i = 0, j = nums.length - 1; // 初始化雙閉區間 [0, n-1] + while (i <= j) { + int m = i + (j - i) / 2; // 計算中點索引 m + if (nums[m] < target) { + i = m + 1; // target 在區間 [m+1, j] 中 + } else if (nums[m] > target) { + j = m - 1; // target 在區間 [i, m-1] 中 + } else { + return m; // 找到 target ,返回插入點 m + } + } + // 未找到 target ,返回插入點 i + return i; + } + ``` + +=== "C#" + + ```csharp title="binary_search_insertion.cs" + /* 二分搜尋插入點(無重複元素) */ + int BinarySearchInsertionSimple(int[] nums, int target) { + int i = 0, j = nums.Length - 1; // 初始化雙閉區間 [0, n-1] + while (i <= j) { + int m = i + (j - i) / 2; // 計算中點索引 m + if (nums[m] < target) { + i = m + 1; // target 在區間 [m+1, j] 中 + } else if (nums[m] > target) { + j = m - 1; // target 在區間 [i, m-1] 中 + } else { + return m; // 找到 target ,返回插入點 m + } + } + // 未找到 target ,返回插入點 i + return i; + } + ``` + +=== "Go" + + ```go title="binary_search_insertion.go" + /* 二分搜尋插入點(無重複元素) */ + func binarySearchInsertionSimple(nums []int, target int) int { + // 初始化雙閉區間 [0, n-1] + i, j := 0, len(nums)-1 + for i <= j { + // 計算中點索引 m + m := i + (j-i)/2 + if nums[m] < target { + // target 在區間 [m+1, j] 中 + i = m + 1 + } else if nums[m] > target { + // target 在區間 [i, m-1] 中 + j = m - 1 + } else { + // 找到 target ,返回插入點 m + return m + } + } + // 未找到 target ,返回插入點 i + return i + } + ``` + +=== "Swift" + + ```swift title="binary_search_insertion.swift" + /* 二分搜尋插入點(無重複元素) */ + func binarySearchInsertionSimple(nums: [Int], target: Int) -> Int { + // 初始化雙閉區間 [0, n-1] + var i = nums.startIndex + var j = nums.endIndex - 1 + while i <= j { + let m = i + (j - i) / 2 // 計算中點索引 m + if nums[m] < target { + i = m + 1 // target 在區間 [m+1, j] 中 + } else if nums[m] > target { + j = m - 1 // target 在區間 [i, m-1] 中 + } else { + return m // 找到 target ,返回插入點 m + } + } + // 未找到 target ,返回插入點 i + return i + } + ``` + +=== "JS" + + ```javascript title="binary_search_insertion.js" + /* 二分搜尋插入點(無重複元素) */ + function binarySearchInsertionSimple(nums, target) { + let i = 0, + j = nums.length - 1; // 初始化雙閉區間 [0, n-1] + while (i <= j) { + const m = Math.floor(i + (j - i) / 2); // 計算中點索引 m, 使用 Math.floor() 向下取整 + if (nums[m] < target) { + i = m + 1; // target 在區間 [m+1, j] 中 + } else if (nums[m] > target) { + j = m - 1; // target 在區間 [i, m-1] 中 + } else { + return m; // 找到 target ,返回插入點 m + } + } + // 未找到 target ,返回插入點 i + return i; + } + ``` + +=== "TS" + + ```typescript title="binary_search_insertion.ts" + /* 二分搜尋插入點(無重複元素) */ + function binarySearchInsertionSimple( + nums: Array, + target: number + ): number { + let i = 0, + j = nums.length - 1; // 初始化雙閉區間 [0, n-1] + while (i <= j) { + const m = Math.floor(i + (j - i) / 2); // 計算中點索引 m, 使用 Math.floor() 向下取整 + if (nums[m] < target) { + i = m + 1; // target 在區間 [m+1, j] 中 + } else if (nums[m] > target) { + j = m - 1; // target 在區間 [i, m-1] 中 + } else { + return m; // 找到 target ,返回插入點 m + } + } + // 未找到 target ,返回插入點 i + return i; + } + ``` + +=== "Dart" + + ```dart title="binary_search_insertion.dart" + /* 二分搜尋插入點(無重複元素) */ + int binarySearchInsertionSimple(List nums, int target) { + int i = 0, j = nums.length - 1; // 初始化雙閉區間 [0, n-1] + while (i <= j) { + int m = i + (j - i) ~/ 2; // 計算中點索引 m + if (nums[m] < target) { + i = m + 1; // target 在區間 [m+1, j] 中 + } else if (nums[m] > target) { + j = m - 1; // target 在區間 [i, m-1] 中 + } else { + return m; // 找到 target ,返回插入點 m + } + } + // 未找到 target ,返回插入點 i + return i; + } + ``` + +=== "Rust" + + ```rust title="binary_search_insertion.rs" + /* 二分搜尋插入點(無重複元素) */ + fn binary_search_insertion_simple(nums: &[i32], target: i32) -> i32 { + let (mut i, mut j) = (0, nums.len() as i32 - 1); // 初始化雙閉區間 [0, n-1] + while i <= j { + let m = i + (j - i) / 2; // 計算中點索引 m + if nums[m as usize] < target { + i = m + 1; // target 在區間 [m+1, j] 中 + } else if nums[m as usize] > target { + j = m - 1; // target 在區間 [i, m-1] 中 + } else { + return m; + } + } + // 未找到 target ,返回插入點 i + i + } + ``` + +=== "C" + + ```c title="binary_search_insertion.c" + /* 二分搜尋插入點(無重複元素) */ + int binarySearchInsertionSimple(int *nums, int numSize, int target) { + int i = 0, j = numSize - 1; // 初始化雙閉區間 [0, n-1] + while (i <= j) { + int m = i + (j - i) / 2; // 計算中點索引 m + if (nums[m] < target) { + i = m + 1; // target 在區間 [m+1, j] 中 + } else if (nums[m] > target) { + j = m - 1; // target 在區間 [i, m-1] 中 + } else { + return m; // 找到 target ,返回插入點 m + } + } + // 未找到 target ,返回插入點 i + return i; + } + ``` + +=== "Kotlin" + + ```kotlin title="binary_search_insertion.kt" + /* 二分搜尋插入點(無重複元素) */ + fun binarySearchInsertionSimple(nums: IntArray, target: Int): Int { + var i = 0 + var j = nums.size - 1 // 初始化雙閉區間 [0, n-1] + while (i <= j) { + val m = i + (j - i) / 2 // 計算中點索引 m + if (nums[m] < target) { + i = m + 1 // target 在區間 [m+1, j] 中 + } else if (nums[m] > target) { + j = m - 1 // target 在區間 [i, m-1] 中 + } else { + return m // 找到 target ,返回插入點 m + } + } + // 未找到 target ,返回插入點 i + return i + } + ``` + +=== "Ruby" + + ```ruby title="binary_search_insertion.rb" + [class]{}-[func]{binary_search_insertion_simple} + ``` + +=== "Zig" + + ```zig title="binary_search_insertion.zig" + [class]{}-[func]{binarySearchInsertionSimple} + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 10.2.2   存在重複元素的情況 + +!!! question + + 在上一題的基礎上,規定陣列可能包含重複元素,其餘不變。 + +假設陣列中存在多個 `target` ,則普通二分搜尋只能返回其中一個 `target` 的索引,**而無法確定該元素的左邊和右邊還有多少 `target`**。 + +題目要求將目標元素插入到最左邊,**所以我們需要查詢陣列中最左一個 `target` 的索引**。初步考慮透過圖 10-5 所示的步驟實現。 + +1. 執行二分搜尋,得到任意一個 `target` 的索引,記為 $k$ 。 +2. 從索引 $k$ 開始,向左進行線性走訪,當找到最左邊的 `target` 時返回。 + +![線性查詢重複元素的插入點](binary_search_insertion.assets/binary_search_insertion_naive.png){ class="animation-figure" } + +

圖 10-5   線性查詢重複元素的插入點

+ +此方法雖然可用,但其包含線性查詢,因此時間複雜度為 $O(n)$ 。當陣列中存在很多重複的 `target` 時,該方法效率很低。 + +現考慮拓展二分搜尋程式碼。如圖 10-6 所示,整體流程保持不變,每輪先計算中點索引 $m$ ,再判斷 `target` 和 `nums[m]` 的大小關係,分為以下幾種情況。 + +- 當 `nums[m] < target` 或 `nums[m] > target` 時,說明還沒有找到 `target` ,因此採用普通二分搜尋的縮小區間操作,**從而使指標 $i$ 和 $j$ 向 `target` 靠近**。 +- 當 `nums[m] == target` 時,說明小於 `target` 的元素在區間 $[i, m - 1]$ 中,因此採用 $j = m - 1$ 來縮小區間,**從而使指標 $j$ 向小於 `target` 的元素靠近**。 + +迴圈完成後,$i$ 指向最左邊的 `target` ,$j$ 指向首個小於 `target` 的元素,**因此索引 $i$ 就是插入點**。 + +=== "<1>" + ![二分搜尋重複元素的插入點的步驟](binary_search_insertion.assets/binary_search_insertion_step1.png){ class="animation-figure" } + +=== "<2>" + ![binary_search_insertion_step2](binary_search_insertion.assets/binary_search_insertion_step2.png){ class="animation-figure" } + +=== "<3>" + ![binary_search_insertion_step3](binary_search_insertion.assets/binary_search_insertion_step3.png){ class="animation-figure" } + +=== "<4>" + ![binary_search_insertion_step4](binary_search_insertion.assets/binary_search_insertion_step4.png){ class="animation-figure" } + +=== "<5>" + ![binary_search_insertion_step5](binary_search_insertion.assets/binary_search_insertion_step5.png){ class="animation-figure" } + +=== "<6>" + ![binary_search_insertion_step6](binary_search_insertion.assets/binary_search_insertion_step6.png){ class="animation-figure" } + +=== "<7>" + ![binary_search_insertion_step7](binary_search_insertion.assets/binary_search_insertion_step7.png){ class="animation-figure" } + +=== "<8>" + ![binary_search_insertion_step8](binary_search_insertion.assets/binary_search_insertion_step8.png){ class="animation-figure" } + +

圖 10-6   二分搜尋重複元素的插入點的步驟

+ +觀察以下程式碼,判斷分支 `nums[m] > target` 和 `nums[m] == target` 的操作相同,因此兩者可以合併。 + +即便如此,我們仍然可以將判斷條件保持展開,因為其邏輯更加清晰、可讀性更好。 + +=== "Python" + + ```python title="binary_search_insertion.py" + def binary_search_insertion(nums: list[int], target: int) -> int: + """二分搜尋插入點(存在重複元素)""" + i, j = 0, len(nums) - 1 # 初始化雙閉區間 [0, n-1] + while i <= j: + m = (i + j) // 2 # 計算中點索引 m + if nums[m] < target: + i = m + 1 # target 在區間 [m+1, j] 中 + elif nums[m] > target: + j = m - 1 # target 在區間 [i, m-1] 中 + else: + j = m - 1 # 首個小於 target 的元素在區間 [i, m-1] 中 + # 返回插入點 i + return i + ``` + +=== "C++" + + ```cpp title="binary_search_insertion.cpp" + /* 二分搜尋插入點(存在重複元素) */ + int binarySearchInsertion(vector &nums, int target) { + int i = 0, j = nums.size() - 1; // 初始化雙閉區間 [0, n-1] + while (i <= j) { + int m = i + (j - i) / 2; // 計算中點索引 m + if (nums[m] < target) { + i = m + 1; // target 在區間 [m+1, j] 中 + } else if (nums[m] > target) { + j = m - 1; // target 在區間 [i, m-1] 中 + } else { + j = m - 1; // 首個小於 target 的元素在區間 [i, m-1] 中 + } + } + // 返回插入點 i + return i; + } + ``` + +=== "Java" + + ```java title="binary_search_insertion.java" + /* 二分搜尋插入點(存在重複元素) */ + int binarySearchInsertion(int[] nums, int target) { + int i = 0, j = nums.length - 1; // 初始化雙閉區間 [0, n-1] + while (i <= j) { + int m = i + (j - i) / 2; // 計算中點索引 m + if (nums[m] < target) { + i = m + 1; // target 在區間 [m+1, j] 中 + } else if (nums[m] > target) { + j = m - 1; // target 在區間 [i, m-1] 中 + } else { + j = m - 1; // 首個小於 target 的元素在區間 [i, m-1] 中 + } + } + // 返回插入點 i + return i; + } + ``` + +=== "C#" + + ```csharp title="binary_search_insertion.cs" + /* 二分搜尋插入點(存在重複元素) */ + int BinarySearchInsertion(int[] nums, int target) { + int i = 0, j = nums.Length - 1; // 初始化雙閉區間 [0, n-1] + while (i <= j) { + int m = i + (j - i) / 2; // 計算中點索引 m + if (nums[m] < target) { + i = m + 1; // target 在區間 [m+1, j] 中 + } else if (nums[m] > target) { + j = m - 1; // target 在區間 [i, m-1] 中 + } else { + j = m - 1; // 首個小於 target 的元素在區間 [i, m-1] 中 + } + } + // 返回插入點 i + return i; + } + ``` + +=== "Go" + + ```go title="binary_search_insertion.go" + /* 二分搜尋插入點(存在重複元素) */ + func binarySearchInsertion(nums []int, target int) int { + // 初始化雙閉區間 [0, n-1] + i, j := 0, len(nums)-1 + for i <= j { + // 計算中點索引 m + m := i + (j-i)/2 + if nums[m] < target { + // target 在區間 [m+1, j] 中 + i = m + 1 + } else if nums[m] > target { + // target 在區間 [i, m-1] 中 + j = m - 1 + } else { + // 首個小於 target 的元素在區間 [i, m-1] 中 + j = m - 1 + } + } + // 返回插入點 i + return i + } + ``` + +=== "Swift" + + ```swift title="binary_search_insertion.swift" + /* 二分搜尋插入點(存在重複元素) */ + func binarySearchInsertion(nums: [Int], target: Int) -> Int { + // 初始化雙閉區間 [0, n-1] + var i = nums.startIndex + var j = nums.endIndex - 1 + while i <= j { + let m = i + (j - i) / 2 // 計算中點索引 m + if nums[m] < target { + i = m + 1 // target 在區間 [m+1, j] 中 + } else if nums[m] > target { + j = m - 1 // target 在區間 [i, m-1] 中 + } else { + j = m - 1 // 首個小於 target 的元素在區間 [i, m-1] 中 + } + } + // 返回插入點 i + return i + } + ``` + +=== "JS" + + ```javascript title="binary_search_insertion.js" + /* 二分搜尋插入點(存在重複元素) */ + function binarySearchInsertion(nums, target) { + let i = 0, + j = nums.length - 1; // 初始化雙閉區間 [0, n-1] + while (i <= j) { + const m = Math.floor(i + (j - i) / 2); // 計算中點索引 m, 使用 Math.floor() 向下取整 + if (nums[m] < target) { + i = m + 1; // target 在區間 [m+1, j] 中 + } else if (nums[m] > target) { + j = m - 1; // target 在區間 [i, m-1] 中 + } else { + j = m - 1; // 首個小於 target 的元素在區間 [i, m-1] 中 + } + } + // 返回插入點 i + return i; + } + ``` + +=== "TS" + + ```typescript title="binary_search_insertion.ts" + /* 二分搜尋插入點(存在重複元素) */ + function binarySearchInsertion(nums: Array, target: number): number { + let i = 0, + j = nums.length - 1; // 初始化雙閉區間 [0, n-1] + while (i <= j) { + const m = Math.floor(i + (j - i) / 2); // 計算中點索引 m, 使用 Math.floor() 向下取整 + if (nums[m] < target) { + i = m + 1; // target 在區間 [m+1, j] 中 + } else if (nums[m] > target) { + j = m - 1; // target 在區間 [i, m-1] 中 + } else { + j = m - 1; // 首個小於 target 的元素在區間 [i, m-1] 中 + } + } + // 返回插入點 i + return i; + } + ``` + +=== "Dart" + + ```dart title="binary_search_insertion.dart" + /* 二分搜尋插入點(存在重複元素) */ + int binarySearchInsertion(List nums, int target) { + int i = 0, j = nums.length - 1; // 初始化雙閉區間 [0, n-1] + while (i <= j) { + int m = i + (j - i) ~/ 2; // 計算中點索引 m + if (nums[m] < target) { + i = m + 1; // target 在區間 [m+1, j] 中 + } else if (nums[m] > target) { + j = m - 1; // target 在區間 [i, m-1] 中 + } else { + j = m - 1; // 首個小於 target 的元素在區間 [i, m-1] 中 + } + } + // 返回插入點 i + return i; + } + ``` + +=== "Rust" + + ```rust title="binary_search_insertion.rs" + /* 二分搜尋插入點(存在重複元素) */ + pub fn binary_search_insertion(nums: &[i32], target: i32) -> i32 { + let (mut i, mut j) = (0, nums.len() as i32 - 1); // 初始化雙閉區間 [0, n-1] + while i <= j { + let m = i + (j - i) / 2; // 計算中點索引 m + if nums[m as usize] < target { + i = m + 1; // target 在區間 [m+1, j] 中 + } else if nums[m as usize] > target { + j = m - 1; // target 在區間 [i, m-1] 中 + } else { + j = m - 1; // 首個小於 target 的元素在區間 [i, m-1] 中 + } + } + // 返回插入點 i + i + } + ``` + +=== "C" + + ```c title="binary_search_insertion.c" + /* 二分搜尋插入點(存在重複元素) */ + int binarySearchInsertion(int *nums, int numSize, int target) { + int i = 0, j = numSize - 1; // 初始化雙閉區間 [0, n-1] + while (i <= j) { + int m = i + (j - i) / 2; // 計算中點索引 m + if (nums[m] < target) { + i = m + 1; // target 在區間 [m+1, j] 中 + } else if (nums[m] > target) { + j = m - 1; // target 在區間 [i, m-1] 中 + } else { + j = m - 1; // 首個小於 target 的元素在區間 [i, m-1] 中 + } + } + // 返回插入點 i + return i; + } + ``` + +=== "Kotlin" + + ```kotlin title="binary_search_insertion.kt" + /* 二分搜尋插入點(存在重複元素) */ + fun binarySearchInsertion(nums: IntArray, target: Int): Int { + var i = 0 + var j = nums.size - 1 // 初始化雙閉區間 [0, n-1] + while (i <= j) { + val m = i + (j - i) / 2 // 計算中點索引 m + if (nums[m] < target) { + i = m + 1 // target 在區間 [m+1, j] 中 + } else if (nums[m] > target) { + j = m - 1 // target 在區間 [i, m-1] 中 + } else { + j = m - 1 // 首個小於 target 的元素在區間 [i, m-1] 中 + } + } + // 返回插入點 i + return i + } + ``` + +=== "Ruby" + + ```ruby title="binary_search_insertion.rb" + [class]{}-[func]{binary_search_insertion} + ``` + +=== "Zig" + + ```zig title="binary_search_insertion.zig" + [class]{}-[func]{binarySearchInsertion} + ``` + +??? pythontutor "視覺化執行" + +
+ + +!!! tip + + 本節的程式碼都是“雙閉區間”寫法。有興趣的讀者可以自行實現“左閉右開”寫法。 + +總的來看,二分搜尋無非就是給指標 $i$ 和 $j$ 分別設定搜尋目標,目標可能是一個具體的元素(例如 `target` ),也可能是一個元素範圍(例如小於 `target` 的元素)。 + +在不斷的迴圈二分中,指標 $i$ 和 $j$ 都逐漸逼近預先設定的目標。最終,它們或是成功找到答案,或是越過邊界後停止。 diff --git a/zh-Hant/docs/chapter_searching/index.md b/zh-Hant/docs/chapter_searching/index.md new file mode 100644 index 000000000..b7f289ca3 --- /dev/null +++ b/zh-Hant/docs/chapter_searching/index.md @@ -0,0 +1,23 @@ +--- +comments: true +icon: material/text-search +--- + +# 第 10 章   搜尋 + +![搜尋](../assets/covers/chapter_searching.jpg){ class="cover-image" } + +!!! abstract + + 搜尋是一場未知的冒險,我們或許需要走遍神秘空間的每個角落,又或許可以快速鎖定目標。 + + 在這場尋覓之旅中,每一次探索都可能得到一個未曾料想的答案。 + +## Chapter Contents + +- [10.1   二分搜尋](https://www.hello-algo.com/en/chapter_searching/binary_search/) +- [10.2   二分搜尋插入點](https://www.hello-algo.com/en/chapter_searching/binary_search_insertion/) +- [10.3   二分搜尋邊界](https://www.hello-algo.com/en/chapter_searching/binary_search_edge/) +- [10.4   雜湊最佳化策略](https://www.hello-algo.com/en/chapter_searching/replace_linear_by_hashing/) +- [10.5   重識搜尋演算法](https://www.hello-algo.com/en/chapter_searching/searching_algorithm_revisited/) +- [10.6   小結](https://www.hello-algo.com/en/chapter_searching/summary/) diff --git a/zh-Hant/docs/chapter_searching/replace_linear_by_hashing.md b/zh-Hant/docs/chapter_searching/replace_linear_by_hashing.md new file mode 100755 index 000000000..765048d87 --- /dev/null +++ b/zh-Hant/docs/chapter_searching/replace_linear_by_hashing.md @@ -0,0 +1,565 @@ +--- +comments: true +--- + +# 10.4   雜湊最佳化策略 + +在演算法題中,**我們常透過將線性查詢替換為雜湊查詢來降低演算法的時間複雜度**。我們藉助一個演算法題來加深理解。 + +!!! question + + 給定一個整數陣列 `nums` 和一個目標元素 `target` ,請在陣列中搜索“和”為 `target` 的兩個元素,並返回它們的陣列索引。返回任意一個解即可。 + +## 10.4.1   線性查詢:以時間換空間 + +考慮直接走訪所有可能的組合。如圖 10-9 所示,我們開啟一個兩層迴圈,在每輪中判斷兩個整數的和是否為 `target` ,若是,則返回它們的索引。 + +![線性查詢求解兩數之和](replace_linear_by_hashing.assets/two_sum_brute_force.png){ class="animation-figure" } + +

圖 10-9   線性查詢求解兩數之和

+ +程式碼如下所示: + +=== "Python" + + ```python title="two_sum.py" + def two_sum_brute_force(nums: list[int], target: int) -> list[int]: + """方法一:暴力列舉""" + # 兩層迴圈,時間複雜度為 O(n^2) + for i in range(len(nums) - 1): + for j in range(i + 1, len(nums)): + if nums[i] + nums[j] == target: + return [i, j] + return [] + ``` + +=== "C++" + + ```cpp title="two_sum.cpp" + /* 方法一:暴力列舉 */ + vector twoSumBruteForce(vector &nums, int target) { + int size = nums.size(); + // 兩層迴圈,時間複雜度為 O(n^2) + for (int i = 0; i < size - 1; i++) { + for (int j = i + 1; j < size; j++) { + if (nums[i] + nums[j] == target) + return {i, j}; + } + } + return {}; + } + ``` + +=== "Java" + + ```java title="two_sum.java" + /* 方法一:暴力列舉 */ + int[] twoSumBruteForce(int[] nums, int target) { + int size = nums.length; + // 兩層迴圈,時間複雜度為 O(n^2) + for (int i = 0; i < size - 1; i++) { + for (int j = i + 1; j < size; j++) { + if (nums[i] + nums[j] == target) + return new int[] { i, j }; + } + } + return new int[0]; + } + ``` + +=== "C#" + + ```csharp title="two_sum.cs" + /* 方法一:暴力列舉 */ + int[] TwoSumBruteForce(int[] nums, int target) { + int size = nums.Length; + // 兩層迴圈,時間複雜度為 O(n^2) + for (int i = 0; i < size - 1; i++) { + for (int j = i + 1; j < size; j++) { + if (nums[i] + nums[j] == target) + return [i, j]; + } + } + return []; + } + ``` + +=== "Go" + + ```go title="two_sum.go" + /* 方法一:暴力列舉 */ + func twoSumBruteForce(nums []int, target int) []int { + size := len(nums) + // 兩層迴圈,時間複雜度為 O(n^2) + for i := 0; i < size-1; i++ { + for j := i + 1; i < size; j++ { + if nums[i]+nums[j] == target { + return []int{i, j} + } + } + } + return nil + } + ``` + +=== "Swift" + + ```swift title="two_sum.swift" + /* 方法一:暴力列舉 */ + func twoSumBruteForce(nums: [Int], target: Int) -> [Int] { + // 兩層迴圈,時間複雜度為 O(n^2) + for i in nums.indices.dropLast() { + for j in nums.indices.dropFirst(i + 1) { + if nums[i] + nums[j] == target { + return [i, j] + } + } + } + return [0] + } + ``` + +=== "JS" + + ```javascript title="two_sum.js" + /* 方法一:暴力列舉 */ + function twoSumBruteForce(nums, target) { + const n = nums.length; + // 兩層迴圈,時間複雜度為 O(n^2) + for (let i = 0; i < n; i++) { + for (let j = i + 1; j < n; j++) { + if (nums[i] + nums[j] === target) { + return [i, j]; + } + } + } + return []; + } + ``` + +=== "TS" + + ```typescript title="two_sum.ts" + /* 方法一:暴力列舉 */ + function twoSumBruteForce(nums: number[], target: number): number[] { + const n = nums.length; + // 兩層迴圈,時間複雜度為 O(n^2) + for (let i = 0; i < n; i++) { + for (let j = i + 1; j < n; j++) { + if (nums[i] + nums[j] === target) { + return [i, j]; + } + } + } + return []; + } + ``` + +=== "Dart" + + ```dart title="two_sum.dart" + /* 方法一: 暴力列舉 */ + List twoSumBruteForce(List nums, int target) { + int size = nums.length; + // 兩層迴圈,時間複雜度為 O(n^2) + for (var i = 0; i < size - 1; i++) { + for (var j = i + 1; j < size; j++) { + if (nums[i] + nums[j] == target) return [i, j]; + } + } + return [0]; + } + ``` + +=== "Rust" + + ```rust title="two_sum.rs" + /* 方法一:暴力列舉 */ + pub fn two_sum_brute_force(nums: &Vec, target: i32) -> Option> { + let size = nums.len(); + // 兩層迴圈,時間複雜度為 O(n^2) + for i in 0..size - 1 { + for j in i + 1..size { + if nums[i] + nums[j] == target { + return Some(vec![i as i32, j as i32]); + } + } + } + None + } + ``` + +=== "C" + + ```c title="two_sum.c" + /* 方法一:暴力列舉 */ + int *twoSumBruteForce(int *nums, int numsSize, int target, int *returnSize) { + for (int i = 0; i < numsSize; ++i) { + for (int j = i + 1; j < numsSize; ++j) { + if (nums[i] + nums[j] == target) { + int *res = malloc(sizeof(int) * 2); + res[0] = i, res[1] = j; + *returnSize = 2; + return res; + } + } + } + *returnSize = 0; + return NULL; + } + ``` + +=== "Kotlin" + + ```kotlin title="two_sum.kt" + /* 方法一:暴力列舉 */ + fun twoSumBruteForce(nums: IntArray, target: Int): IntArray { + val size = nums.size + // 兩層迴圈,時間複雜度為 O(n^2) + for (i in 0.. + + +此方法的時間複雜度為 $O(n^2)$ ,空間複雜度為 $O(1)$ ,在大資料量下非常耗時。 + +## 10.4.2   雜湊查詢:以空間換時間 + +考慮藉助一個雜湊表,鍵值對分別為陣列元素和元素索引。迴圈走訪陣列,每輪執行圖 10-10 所示的步驟。 + +1. 判斷數字 `target - nums[i]` 是否在雜湊表中,若是,則直接返回這兩個元素的索引。 +2. 將鍵值對 `nums[i]` 和索引 `i` 新增進雜湊表。 + +=== "<1>" + ![輔助雜湊表求解兩數之和](replace_linear_by_hashing.assets/two_sum_hashtable_step1.png){ class="animation-figure" } + +=== "<2>" + ![two_sum_hashtable_step2](replace_linear_by_hashing.assets/two_sum_hashtable_step2.png){ class="animation-figure" } + +=== "<3>" + ![two_sum_hashtable_step3](replace_linear_by_hashing.assets/two_sum_hashtable_step3.png){ class="animation-figure" } + +

圖 10-10   輔助雜湊表求解兩數之和

+ +實現程式碼如下所示,僅需單層迴圈即可: + +=== "Python" + + ```python title="two_sum.py" + def two_sum_hash_table(nums: list[int], target: int) -> list[int]: + """方法二:輔助雜湊表""" + # 輔助雜湊表,空間複雜度為 O(n) + dic = {} + # 單層迴圈,時間複雜度為 O(n) + for i in range(len(nums)): + if target - nums[i] in dic: + return [dic[target - nums[i]], i] + dic[nums[i]] = i + return [] + ``` + +=== "C++" + + ```cpp title="two_sum.cpp" + /* 方法二:輔助雜湊表 */ + vector twoSumHashTable(vector &nums, int target) { + int size = nums.size(); + // 輔助雜湊表,空間複雜度為 O(n) + unordered_map dic; + // 單層迴圈,時間複雜度為 O(n) + for (int i = 0; i < size; i++) { + if (dic.find(target - nums[i]) != dic.end()) { + return {dic[target - nums[i]], i}; + } + dic.emplace(nums[i], i); + } + return {}; + } + ``` + +=== "Java" + + ```java title="two_sum.java" + /* 方法二:輔助雜湊表 */ + int[] twoSumHashTable(int[] nums, int target) { + int size = nums.length; + // 輔助雜湊表,空間複雜度為 O(n) + Map dic = new HashMap<>(); + // 單層迴圈,時間複雜度為 O(n) + for (int i = 0; i < size; i++) { + if (dic.containsKey(target - nums[i])) { + return new int[] { dic.get(target - nums[i]), i }; + } + dic.put(nums[i], i); + } + return new int[0]; + } + ``` + +=== "C#" + + ```csharp title="two_sum.cs" + /* 方法二:輔助雜湊表 */ + int[] TwoSumHashTable(int[] nums, int target) { + int size = nums.Length; + // 輔助雜湊表,空間複雜度為 O(n) + Dictionary dic = []; + // 單層迴圈,時間複雜度為 O(n) + for (int i = 0; i < size; i++) { + if (dic.ContainsKey(target - nums[i])) { + return [dic[target - nums[i]], i]; + } + dic.Add(nums[i], i); + } + return []; + } + ``` + +=== "Go" + + ```go title="two_sum.go" + /* 方法二:輔助雜湊表 */ + func twoSumHashTable(nums []int, target int) []int { + // 輔助雜湊表,空間複雜度為 O(n) + hashTable := map[int]int{} + // 單層迴圈,時間複雜度為 O(n) + for idx, val := range nums { + if preIdx, ok := hashTable[target-val]; ok { + return []int{preIdx, idx} + } + hashTable[val] = idx + } + return nil + } + ``` + +=== "Swift" + + ```swift title="two_sum.swift" + /* 方法二:輔助雜湊表 */ + func twoSumHashTable(nums: [Int], target: Int) -> [Int] { + // 輔助雜湊表,空間複雜度為 O(n) + var dic: [Int: Int] = [:] + // 單層迴圈,時間複雜度為 O(n) + for i in nums.indices { + if let j = dic[target - nums[i]] { + return [j, i] + } + dic[nums[i]] = i + } + return [0] + } + ``` + +=== "JS" + + ```javascript title="two_sum.js" + /* 方法二:輔助雜湊表 */ + function twoSumHashTable(nums, target) { + // 輔助雜湊表,空間複雜度為 O(n) + let m = {}; + // 單層迴圈,時間複雜度為 O(n) + for (let i = 0; i < nums.length; i++) { + if (m[target - nums[i]] !== undefined) { + return [m[target - nums[i]], i]; + } else { + m[nums[i]] = i; + } + } + return []; + } + ``` + +=== "TS" + + ```typescript title="two_sum.ts" + /* 方法二:輔助雜湊表 */ + function twoSumHashTable(nums: number[], target: number): number[] { + // 輔助雜湊表,空間複雜度為 O(n) + let m: Map = new Map(); + // 單層迴圈,時間複雜度為 O(n) + for (let i = 0; i < nums.length; i++) { + let index = m.get(target - nums[i]); + if (index !== undefined) { + return [index, i]; + } else { + m.set(nums[i], i); + } + } + return []; + } + ``` + +=== "Dart" + + ```dart title="two_sum.dart" + /* 方法二: 輔助雜湊表 */ + List twoSumHashTable(List nums, int target) { + int size = nums.length; + // 輔助雜湊表,空間複雜度為 O(n) + Map dic = HashMap(); + // 單層迴圈,時間複雜度為 O(n) + for (var i = 0; i < size; i++) { + if (dic.containsKey(target - nums[i])) { + return [dic[target - nums[i]]!, i]; + } + dic.putIfAbsent(nums[i], () => i); + } + return [0]; + } + ``` + +=== "Rust" + + ```rust title="two_sum.rs" + /* 方法二:輔助雜湊表 */ + pub fn two_sum_hash_table(nums: &Vec, target: i32) -> Option> { + // 輔助雜湊表,空間複雜度為 O(n) + let mut dic = HashMap::new(); + // 單層迴圈,時間複雜度為 O(n) + for (i, num) in nums.iter().enumerate() { + match dic.get(&(target - num)) { + Some(v) => return Some(vec![*v as i32, i as i32]), + None => dic.insert(num, i as i32), + }; + } + None + } + ``` + +=== "C" + + ```c title="two_sum.c" + /* 雜湊表 */ + typedef struct { + int key; + int val; + UT_hash_handle hh; // 基於 uthash.h 實現 + } HashTable; + + /* 雜湊表查詢 */ + HashTable *find(HashTable *h, int key) { + HashTable *tmp; + HASH_FIND_INT(h, &key, tmp); + return tmp; + } + + /* 雜湊表元素插入 */ + void insert(HashTable *h, int key, int val) { + HashTable *t = find(h, key); + if (t == NULL) { + HashTable *tmp = malloc(sizeof(HashTable)); + tmp->key = key, tmp->val = val; + HASH_ADD_INT(h, key, tmp); + } else { + t->val = val; + } + } + + /* 方法二:輔助雜湊表 */ + int *twoSumHashTable(int *nums, int numsSize, int target, int *returnSize) { + HashTable *hashtable = NULL; + for (int i = 0; i < numsSize; i++) { + HashTable *t = find(hashtable, target - nums[i]); + if (t != NULL) { + int *res = malloc(sizeof(int) * 2); + res[0] = t->val, res[1] = i; + *returnSize = 2; + return res; + } + insert(hashtable, nums[i], i); + } + *returnSize = 0; + return NULL; + } + ``` + +=== "Kotlin" + + ```kotlin title="two_sum.kt" + /* 方法二:輔助雜湊表 */ + fun twoSumHashTable(nums: IntArray, target: Int): IntArray { + val size = nums.size + // 輔助雜湊表,空間複雜度為 O(n) + val dic = HashMap() + // 單層迴圈,時間複雜度為 O(n) + for (i in 0.. + + +此方法透過雜湊查詢將時間複雜度從 $O(n^2)$ 降至 $O(n)$ ,大幅提升執行效率。 + +由於需要維護一個額外的雜湊表,因此空間複雜度為 $O(n)$ 。**儘管如此,該方法的整體時空效率更為均衡,因此它是本題的最優解法**。 diff --git a/zh-Hant/docs/chapter_searching/searching_algorithm_revisited.md b/zh-Hant/docs/chapter_searching/searching_algorithm_revisited.md new file mode 100644 index 000000000..67854c866 --- /dev/null +++ b/zh-Hant/docs/chapter_searching/searching_algorithm_revisited.md @@ -0,0 +1,94 @@ +--- +comments: true +--- + +# 10.5   重識搜尋演算法 + +搜尋演算法(searching algorithm)用於在資料結構(例如陣列、鏈結串列、樹或圖)中搜索一個或一組滿足特定條件的元素。 + +搜尋演算法可根據實現思路分為以下兩類。 + +- **透過走訪資料結構來定位目標元素**,例如陣列、鏈結串列、樹和圖的走訪等。 +- **利用資料組織結構或資料包含的先驗資訊,實現高效元素查詢**,例如二分搜尋、雜湊查詢和二元搜尋樹查詢等。 + +不難發現,這些知識點都已在前面的章節中介紹過,因此搜尋演算法對於我們來說並不陌生。在本節中,我們將從更加系統的視角切入,重新審視搜尋演算法。 + +## 10.5.1   暴力搜尋 + +暴力搜尋透過走訪資料結構的每個元素來定位目標元素。 + +- “線性搜尋”適用於陣列和鏈結串列等線性資料結構。它從資料結構的一端開始,逐個訪問元素,直到找到目標元素或到達另一端仍沒有找到目標元素為止。 +- “廣度優先搜尋”和“深度優先搜尋”是圖和樹的兩種走訪策略。廣度優先搜尋從初始節點開始逐層搜尋,由近及遠地訪問各個節點。深度優先搜尋從初始節點開始,沿著一條路徑走到頭,再回溯並嘗試其他路徑,直到走訪完整個資料結構。 + +暴力搜尋的優點是簡單且通用性好,**無須對資料做預處理和藉助額外的資料結構**。 + +然而,**此類演算法的時間複雜度為 $O(n)$** ,其中 $n$ 為元素數量,因此在資料量較大的情況下效能較差。 + +## 10.5.2   自適應搜尋 + +自適應搜尋利用資料的特有屬性(例如有序性)來最佳化搜尋過程,從而更高效地定位目標元素。 + +- “二分搜尋”利用資料的有序性實現高效查詢,僅適用於陣列。 +- “雜湊查詢”利用雜湊表將搜尋資料和目標資料建立為鍵值對對映,從而實現查詢操作。 +- “樹查詢”在特定的樹結構(例如二元搜尋樹)中,基於比較節點值來快速排除節點,從而定位目標元素。 + +此類演算法的優點是效率高,**時間複雜度可達到 $O(\log n)$ 甚至 $O(1)$** 。 + +然而,**使用這些演算法往往需要對資料進行預處理**。例如,二分搜尋需要預先對陣列進行排序,雜湊查詢和樹查詢都需要藉助額外的資料結構,維護這些資料結構也需要額外的時間和空間開銷。 + +!!! tip + + 自適應搜尋演算法常被稱為查詢演算法,**主要用於在特定資料結構中快速檢索目標元素**。 + +## 10.5.3   搜尋方法選取 + +給定大小為 $n$ 的一組資料,我們可以使用線性搜尋、二分搜尋、樹查詢、雜湊查詢等多種方法從中搜索目標元素。各個方法的工作原理如圖 10-11 所示。 + +![多種搜尋策略](searching_algorithm_revisited.assets/searching_algorithms.png){ class="animation-figure" } + +

圖 10-11   多種搜尋策略

+ +上述幾種方法的操作效率與特性如表 10-1 所示。 + +

表 10-1   查詢演算法效率對比

+ +
+ +| | 線性搜尋 | 二分搜尋 | 樹查詢 | 雜湊查詢 | +| ------------ | -------- | ------------------ | ------------------ | --------------- | +| 查詢元素 | $O(n)$ | $O(\log n)$ | $O(\log n)$ | $O(1)$ | +| 插入元素 | $O(1)$ | $O(n)$ | $O(\log n)$ | $O(1)$ | +| 刪除元素 | $O(n)$ | $O(n)$ | $O(\log n)$ | $O(1)$ | +| 額外空間 | $O(1)$ | $O(1)$ | $O(n)$ | $O(n)$ | +| 資料預處理 | / | 排序 $O(n \log n)$ | 建樹 $O(n \log n)$ | 建雜湊表 $O(n)$ | +| 資料是否有序 | 無序 | 有序 | 有序 | 無序 | + +
+ +搜尋演算法的選擇還取決於資料體量、搜尋效能要求、資料查詢與更新頻率等。 + +**線性搜尋** + +- 通用性較好,無須任何資料預處理操作。假如我們僅需查詢一次資料,那麼其他三種方法的資料預處理的時間比線性搜尋的時間還要更長。 +- 適用於體量較小的資料,此情況下時間複雜度對效率影響較小。 +- 適用於資料更新頻率較高的場景,因為該方法不需要對資料進行任何額外維護。 + +**二分搜尋** + +- 適用於大資料量的情況,效率表現穩定,最差時間複雜度為 $O(\log n)$ 。 +- 資料量不能過大,因為儲存陣列需要連續的記憶體空間。 +- 不適用於高頻增刪資料的場景,因為維護有序陣列的開銷較大。 + +**雜湊查詢** + +- 適合對查詢效能要求很高的場景,平均時間複雜度為 $O(1)$ 。 +- 不適合需要有序資料或範圍查詢的場景,因為雜湊表無法維護資料的有序性。 +- 對雜湊函式和雜湊衝突處理策略的依賴性較高,具有較大的效能劣化風險。 +- 不適合資料量過大的情況,因為雜湊表需要額外空間來最大程度地減少衝突,從而提供良好的查詢效能。 + +**樹查詢** + +- 適用於海量資料,因為樹節點在記憶體中是分散儲存的。 +- 適合需要維護有序資料或範圍查詢的場景。 +- 在持續增刪節點的過程中,二元搜尋樹可能產生傾斜,時間複雜度劣化至 $O(n)$ 。 +- 若使用 AVL 樹或紅黑樹,則各項操作可在 $O(\log n)$ 效率下穩定執行,但維護樹平衡的操作會增加額外的開銷。 diff --git a/zh-Hant/docs/chapter_searching/summary.md b/zh-Hant/docs/chapter_searching/summary.md new file mode 100644 index 000000000..271ca66cd --- /dev/null +++ b/zh-Hant/docs/chapter_searching/summary.md @@ -0,0 +1,12 @@ +--- +comments: true +--- + +# 10.6   小結 + +- 二分搜尋依賴資料的有序性,透過迴圈逐步縮減一半搜尋區間來進行查詢。它要求輸入資料有序,且僅適用於陣列或基於陣列實現的資料結構。 +- 暴力搜尋透過走訪資料結構來定位資料。線性搜尋適用於陣列和鏈結串列,廣度優先搜尋和深度優先搜尋適用於圖和樹。此類演算法通用性好,無須對資料進行預處理,但時間複雜度 $O(n)$ 較高。 +- 雜湊查詢、樹查詢和二分搜尋屬於高效搜尋方法,可在特定資料結構中快速定位目標元素。此類演算法效率高,時間複雜度可達 $O(\log n)$ 甚至 $O(1)$ ,但通常需要藉助額外資料結構。 +- 實際中,我們需要對資料體量、搜尋效能要求、資料查詢和更新頻率等因素進行具體分析,從而選擇合適的搜尋方法。 +- 線性搜尋適用於小型或頻繁更新的資料;二分搜尋適用於大型、排序的資料;雜湊查詢適用於對查詢效率要求較高且無須範圍查詢的資料;樹查詢適用於需要維護順序和支持範圍查詢的大型動態資料。 +- 用雜湊查詢替換線性查詢是一種常用的最佳化執行時間的策略,可將時間複雜度從 $O(n)$ 降至 $O(1)$ 。 diff --git a/zh-Hant/docs/chapter_sorting/bubble_sort.md b/zh-Hant/docs/chapter_sorting/bubble_sort.md new file mode 100755 index 000000000..b413fffdf --- /dev/null +++ b/zh-Hant/docs/chapter_sorting/bubble_sort.md @@ -0,0 +1,623 @@ +--- +comments: true +--- + +# 11.3   泡沫排序 + +泡沫排序(bubble sort)透過連續地比較與交換相鄰元素實現排序。這個過程就像氣泡從底部升到頂部一樣,因此得名泡沫排序。 + +如圖 11-4 所示,冒泡過程可以利用元素交換操作來模擬:從陣列最左端開始向右走訪,依次比較相鄰元素大小,如果“左元素 > 右元素”就交換二者。走訪完成後,最大的元素會被移動到陣列的最右端。 + +=== "<1>" + ![利用元素交換操作模擬冒泡](bubble_sort.assets/bubble_operation_step1.png){ class="animation-figure" } + +=== "<2>" + ![bubble_operation_step2](bubble_sort.assets/bubble_operation_step2.png){ class="animation-figure" } + +=== "<3>" + ![bubble_operation_step3](bubble_sort.assets/bubble_operation_step3.png){ class="animation-figure" } + +=== "<4>" + ![bubble_operation_step4](bubble_sort.assets/bubble_operation_step4.png){ class="animation-figure" } + +=== "<5>" + ![bubble_operation_step5](bubble_sort.assets/bubble_operation_step5.png){ class="animation-figure" } + +=== "<6>" + ![bubble_operation_step6](bubble_sort.assets/bubble_operation_step6.png){ class="animation-figure" } + +=== "<7>" + ![bubble_operation_step7](bubble_sort.assets/bubble_operation_step7.png){ class="animation-figure" } + +

圖 11-4   利用元素交換操作模擬冒泡

+ +## 11.3.1   演算法流程 + +設陣列的長度為 $n$ ,泡沫排序的步驟如圖 11-5 所示。 + +1. 首先,對 $n$ 個元素執行“冒泡”,**將陣列的最大元素交換至正確位置**。 +2. 接下來,對剩餘 $n - 1$ 個元素執行“冒泡”,**將第二大元素交換至正確位置**。 +3. 以此類推,經過 $n - 1$ 輪“冒泡”後,**前 $n - 1$ 大的元素都被交換至正確位置**。 +4. 僅剩的一個元素必定是最小元素,無須排序,因此陣列排序完成。 + +![泡沫排序流程](bubble_sort.assets/bubble_sort_overview.png){ class="animation-figure" } + +

圖 11-5   泡沫排序流程

+ +示例程式碼如下: + +=== "Python" + + ```python title="bubble_sort.py" + def bubble_sort(nums: list[int]): + """泡沫排序""" + n = len(nums) + # 外迴圈:未排序區間為 [0, i] + for i in range(n - 1, 0, -1): + # 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for j in range(i): + if nums[j] > nums[j + 1]: + # 交換 nums[j] 與 nums[j + 1] + nums[j], nums[j + 1] = nums[j + 1], nums[j] + ``` + +=== "C++" + + ```cpp title="bubble_sort.cpp" + /* 泡沫排序 */ + void bubbleSort(vector &nums) { + // 外迴圈:未排序區間為 [0, i] + for (int i = nums.size() - 1; i > 0; i--) { + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交換 nums[j] 與 nums[j + 1] + // 這裡使用了 std::swap() 函式 + swap(nums[j], nums[j + 1]); + } + } + } + } + ``` + +=== "Java" + + ```java title="bubble_sort.java" + /* 泡沫排序 */ + void bubbleSort(int[] nums) { + // 外迴圈:未排序區間為 [0, i] + for (int i = nums.length - 1; i > 0; i--) { + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交換 nums[j] 與 nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + } + } + } + } + ``` + +=== "C#" + + ```csharp title="bubble_sort.cs" + /* 泡沫排序 */ + void BubbleSort(int[] nums) { + // 外迴圈:未排序區間為 [0, i] + for (int i = nums.Length - 1; i > 0; i--) { + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交換 nums[j] 與 nums[j + 1] + (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]); + } + } + } + } + ``` + +=== "Go" + + ```go title="bubble_sort.go" + /* 泡沫排序 */ + func bubbleSort(nums []int) { + // 外迴圈:未排序區間為 [0, i] + for i := len(nums) - 1; i > 0; i-- { + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for j := 0; j < i; j++ { + if nums[j] > nums[j+1] { + // 交換 nums[j] 與 nums[j + 1] + nums[j], nums[j+1] = nums[j+1], nums[j] + } + } + } + } + ``` + +=== "Swift" + + ```swift title="bubble_sort.swift" + /* 泡沫排序 */ + func bubbleSort(nums: inout [Int]) { + // 外迴圈:未排序區間為 [0, i] + for i in nums.indices.dropFirst().reversed() { + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for j in 0 ..< i { + if nums[j] > nums[j + 1] { + // 交換 nums[j] 與 nums[j + 1] + nums.swapAt(j, j + 1) + } + } + } + } + ``` + +=== "JS" + + ```javascript title="bubble_sort.js" + /* 泡沫排序 */ + function bubbleSort(nums) { + // 外迴圈:未排序區間為 [0, i] + for (let i = nums.length - 1; i > 0; i--) { + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for (let j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交換 nums[j] 與 nums[j + 1] + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + } + } + } + } + ``` + +=== "TS" + + ```typescript title="bubble_sort.ts" + /* 泡沫排序 */ + function bubbleSort(nums: number[]): void { + // 外迴圈:未排序區間為 [0, i] + for (let i = nums.length - 1; i > 0; i--) { + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for (let j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交換 nums[j] 與 nums[j + 1] + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + } + } + } + } + ``` + +=== "Dart" + + ```dart title="bubble_sort.dart" + /* 泡沫排序 */ + void bubbleSort(List nums) { + // 外迴圈:未排序區間為 [0, i] + for (int i = nums.length - 1; i > 0; i--) { + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交換 nums[j] 與 nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + } + } + } + } + ``` + +=== "Rust" + + ```rust title="bubble_sort.rs" + /* 泡沫排序 */ + fn bubble_sort(nums: &mut [i32]) { + // 外迴圈:未排序區間為 [0, i] + for i in (1..nums.len()).rev() { + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for j in 0..i { + if nums[j] > nums[j + 1] { + // 交換 nums[j] 與 nums[j + 1] + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + } + } + } + } + ``` + +=== "C" + + ```c title="bubble_sort.c" + /* 泡沫排序 */ + void bubbleSort(int nums[], int size) { + // 外迴圈:未排序區間為 [0, i] + for (int i = size - 1; i > 0; i--) { + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + int temp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = temp; + } + } + } + } + ``` + +=== "Kotlin" + + ```kotlin title="bubble_sort.kt" + /* 泡沫排序 */ + fun bubbleSort(nums: IntArray) { + // 外迴圈:未排序區間為 [0, i] + for (i in nums.size - 1 downTo 1) { + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for (j in 0.. nums[j + 1]) { + // 交換 nums[j] 與 nums[j + 1] + nums[j] = nums[j+1].also { nums[j+1] = nums[j] } + } + } + } + } + ``` + +=== "Ruby" + + ```ruby title="bubble_sort.rb" + [class]{}-[func]{bubble_sort} + ``` + +=== "Zig" + + ```zig title="bubble_sort.zig" + // 泡沫排序 + fn bubbleSort(nums: []i32) void { + // 外迴圈:未排序區間為 [0, i] + var i: usize = nums.len - 1; + while (i > 0) : (i -= 1) { + var j: usize = 0; + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + while (j < i) : (j += 1) { + if (nums[j] > nums[j + 1]) { + // 交換 nums[j] 與 nums[j + 1] + var tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + } + } + } + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 11.3.2   效率最佳化 + +我們發現,如果某輪“冒泡”中沒有執行任何交換操作,說明陣列已經完成排序,可直接返回結果。因此,可以增加一個標誌位 `flag` 來監測這種情況,一旦出現就立即返回。 + +經過最佳化,泡沫排序的最差時間複雜度和平均時間複雜度仍為 $O(n^2)$ ;但當輸入陣列完全有序時,可達到最佳時間複雜度 $O(n)$ 。 + +=== "Python" + + ```python title="bubble_sort.py" + def bubble_sort_with_flag(nums: list[int]): + """泡沫排序(標誌最佳化)""" + n = len(nums) + # 外迴圈:未排序區間為 [0, i] + for i in range(n - 1, 0, -1): + flag = False # 初始化標誌位 + # 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for j in range(i): + if nums[j] > nums[j + 1]: + # 交換 nums[j] 與 nums[j + 1] + nums[j], nums[j + 1] = nums[j + 1], nums[j] + flag = True # 記錄交換元素 + if not flag: + break # 此輪“冒泡”未交換任何元素,直接跳出 + ``` + +=== "C++" + + ```cpp title="bubble_sort.cpp" + /* 泡沫排序(標誌最佳化)*/ + void bubbleSortWithFlag(vector &nums) { + // 外迴圈:未排序區間為 [0, i] + for (int i = nums.size() - 1; i > 0; i--) { + bool flag = false; // 初始化標誌位 + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交換 nums[j] 與 nums[j + 1] + // 這裡使用了 std::swap() 函式 + swap(nums[j], nums[j + 1]); + flag = true; // 記錄交換元素 + } + } + if (!flag) + break; // 此輪“冒泡”未交換任何元素,直接跳出 + } + } + ``` + +=== "Java" + + ```java title="bubble_sort.java" + /* 泡沫排序(標誌最佳化) */ + void bubbleSortWithFlag(int[] nums) { + // 外迴圈:未排序區間為 [0, i] + for (int i = nums.length - 1; i > 0; i--) { + boolean flag = false; // 初始化標誌位 + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交換 nums[j] 與 nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + flag = true; // 記錄交換元素 + } + } + if (!flag) + break; // 此輪“冒泡”未交換任何元素,直接跳出 + } + } + ``` + +=== "C#" + + ```csharp title="bubble_sort.cs" + /* 泡沫排序(標誌最佳化)*/ + void BubbleSortWithFlag(int[] nums) { + // 外迴圈:未排序區間為 [0, i] + for (int i = nums.Length - 1; i > 0; i--) { + bool flag = false; // 初始化標誌位 + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交換 nums[j] 與 nums[j + 1] + (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]); + flag = true; // 記錄交換元素 + } + } + if (!flag) break; // 此輪“冒泡”未交換任何元素,直接跳出 + } + } + ``` + +=== "Go" + + ```go title="bubble_sort.go" + /* 泡沫排序(標誌最佳化)*/ + func bubbleSortWithFlag(nums []int) { + // 外迴圈:未排序區間為 [0, i] + for i := len(nums) - 1; i > 0; i-- { + flag := false // 初始化標誌位 + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for j := 0; j < i; j++ { + if nums[j] > nums[j+1] { + // 交換 nums[j] 與 nums[j + 1] + nums[j], nums[j+1] = nums[j+1], nums[j] + flag = true // 記錄交換元素 + } + } + if flag == false { // 此輪“冒泡”未交換任何元素,直接跳出 + break + } + } + } + ``` + +=== "Swift" + + ```swift title="bubble_sort.swift" + /* 泡沫排序(標誌最佳化)*/ + func bubbleSortWithFlag(nums: inout [Int]) { + // 外迴圈:未排序區間為 [0, i] + for i in nums.indices.dropFirst().reversed() { + var flag = false // 初始化標誌位 + for j in 0 ..< i { + if nums[j] > nums[j + 1] { + // 交換 nums[j] 與 nums[j + 1] + nums.swapAt(j, j + 1) + flag = true // 記錄交換元素 + } + } + if !flag { // 此輪“冒泡”未交換任何元素,直接跳出 + break + } + } + } + ``` + +=== "JS" + + ```javascript title="bubble_sort.js" + /* 泡沫排序(標誌最佳化)*/ + function bubbleSortWithFlag(nums) { + // 外迴圈:未排序區間為 [0, i] + for (let i = nums.length - 1; i > 0; i--) { + let flag = false; // 初始化標誌位 + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for (let j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交換 nums[j] 與 nums[j + 1] + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + flag = true; // 記錄交換元素 + } + } + if (!flag) break; // 此輪“冒泡”未交換任何元素,直接跳出 + } + } + ``` + +=== "TS" + + ```typescript title="bubble_sort.ts" + /* 泡沫排序(標誌最佳化)*/ + function bubbleSortWithFlag(nums: number[]): void { + // 外迴圈:未排序區間為 [0, i] + for (let i = nums.length - 1; i > 0; i--) { + let flag = false; // 初始化標誌位 + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for (let j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交換 nums[j] 與 nums[j + 1] + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + flag = true; // 記錄交換元素 + } + } + if (!flag) break; // 此輪“冒泡”未交換任何元素,直接跳出 + } + } + ``` + +=== "Dart" + + ```dart title="bubble_sort.dart" + /* 泡沫排序(標誌最佳化)*/ + void bubbleSortWithFlag(List nums) { + // 外迴圈:未排序區間為 [0, i] + for (int i = nums.length - 1; i > 0; i--) { + bool flag = false; // 初始化標誌位 + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交換 nums[j] 與 nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + flag = true; // 記錄交換元素 + } + } + if (!flag) break; // 此輪“冒泡”未交換任何元素,直接跳出 + } + } + ``` + +=== "Rust" + + ```rust title="bubble_sort.rs" + /* 泡沫排序(標誌最佳化) */ + fn bubble_sort_with_flag(nums: &mut [i32]) { + // 外迴圈:未排序區間為 [0, i] + for i in (1..nums.len()).rev() { + let mut flag = false; // 初始化標誌位 + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for j in 0..i { + if nums[j] > nums[j + 1] { + // 交換 nums[j] 與 nums[j + 1] + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + flag = true; // 記錄交換元素 + } + } + if !flag { + break; // 此輪“冒泡”未交換任何元素,直接跳出 + }; + } + } + ``` + +=== "C" + + ```c title="bubble_sort.c" + /* 泡沫排序(標誌最佳化)*/ + void bubbleSortWithFlag(int nums[], int size) { + // 外迴圈:未排序區間為 [0, i] + for (int i = size - 1; i > 0; i--) { + bool flag = false; + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + int temp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = temp; + flag = true; + } + } + if (!flag) + break; + } + } + ``` + +=== "Kotlin" + + ```kotlin title="bubble_sort.kt" + /* 泡沫排序(標誌最佳化) */ + fun bubbleSortWithFlag(nums: IntArray) { + // 外迴圈:未排序區間為 [0, i] + for (i in nums.size - 1 downTo 1) { + var flag = false // 初始化標誌位 + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + for (j in 0.. nums[j + 1]) { + // 交換 nums[j] 與 nums[j + 1] + nums[j] = nums[j + 1].also { nums[j] = nums[j + 1] } + flag = true // 記錄交換元素 + } + } + if (!flag) break // 此輪“冒泡”未交換任何元素,直接跳出 + } + } + ``` + +=== "Ruby" + + ```ruby title="bubble_sort.rb" + [class]{}-[func]{bubble_sort_with_flag} + ``` + +=== "Zig" + + ```zig title="bubble_sort.zig" + // 泡沫排序(標誌最佳化) + fn bubbleSortWithFlag(nums: []i32) void { + // 外迴圈:未排序區間為 [0, i] + var i: usize = nums.len - 1; + while (i > 0) : (i -= 1) { + var flag = false; // 初始化標誌位 + var j: usize = 0; + // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 + while (j < i) : (j += 1) { + if (nums[j] > nums[j + 1]) { + // 交換 nums[j] 與 nums[j + 1] + var tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + flag = true; + } + } + if (!flag) break; // 此輪“冒泡”未交換任何元素,直接跳出 + } + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 11.3.3   演算法特性 + +- **時間複雜度為 $O(n^2)$、自適應排序**:各輪“冒泡”走訪的陣列長度依次為 $n - 1$、$n - 2$、$\dots$、$2$、$1$ ,總和為 $(n - 1) n / 2$ 。在引入 `flag` 最佳化後,最佳時間複雜度可達到 $O(n)$ 。 +- **空間複雜度為 $O(1)$、原地排序**:指標 $i$ 和 $j$ 使用常數大小的額外空間。 +- **穩定排序**:由於在“冒泡”中遇到相等元素不交換。 diff --git a/zh-Hant/docs/chapter_sorting/bucket_sort.md b/zh-Hant/docs/chapter_sorting/bucket_sort.md new file mode 100644 index 000000000..3cf8a9615 --- /dev/null +++ b/zh-Hant/docs/chapter_sorting/bucket_sort.md @@ -0,0 +1,468 @@ +--- +comments: true +--- + +# 11.8   桶排序 + +前述幾種排序演算法都屬於“基於比較的排序演算法”,它們透過比較元素間的大小來實現排序。此類排序演算法的時間複雜度無法超越 $O(n \log n)$ 。接下來,我們將探討幾種“非比較排序演算法”,它們的時間複雜度可以達到線性階。 + +桶排序(bucket sort)是分治策略的一個典型應用。它透過設定一些具有大小順序的桶,每個桶對應一個數據範圍,將資料平均分配到各個桶中;然後,在每個桶內部分別執行排序;最終按照桶的順序將所有資料合併。 + +## 11.8.1   演算法流程 + +考慮一個長度為 $n$ 的陣列,其元素是範圍 $[0, 1)$ 內的浮點數。桶排序的流程如圖 11-13 所示。 + +1. 初始化 $k$ 個桶,將 $n$ 個元素分配到 $k$ 個桶中。 +2. 對每個桶分別執行排序(這裡採用程式語言的內建排序函式)。 +3. 按照桶從小到大的順序合併結果。 + +![桶排序演算法流程](bucket_sort.assets/bucket_sort_overview.png){ class="animation-figure" } + +

圖 11-13   桶排序演算法流程

+ +程式碼如下所示: + +=== "Python" + + ```python title="bucket_sort.py" + def bucket_sort(nums: list[float]): + """桶排序""" + # 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素 + k = len(nums) // 2 + buckets = [[] for _ in range(k)] + # 1. 將陣列元素分配到各個桶中 + for num in nums: + # 輸入資料範圍為 [0, 1),使用 num * k 對映到索引範圍 [0, k-1] + i = int(num * k) + # 將 num 新增進桶 i + buckets[i].append(num) + # 2. 對各個桶執行排序 + for bucket in buckets: + # 使用內建排序函式,也可以替換成其他排序演算法 + bucket.sort() + # 3. 走訪桶合併結果 + i = 0 + for bucket in buckets: + for num in bucket: + nums[i] = num + i += 1 + ``` + +=== "C++" + + ```cpp title="bucket_sort.cpp" + /* 桶排序 */ + void bucketSort(vector &nums) { + // 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素 + int k = nums.size() / 2; + vector> buckets(k); + // 1. 將陣列元素分配到各個桶中 + for (float num : nums) { + // 輸入資料範圍為 [0, 1),使用 num * k 對映到索引範圍 [0, k-1] + int i = num * k; + // 將 num 新增進桶 bucket_idx + buckets[i].push_back(num); + } + // 2. 對各個桶執行排序 + for (vector &bucket : buckets) { + // 使用內建排序函式,也可以替換成其他排序演算法 + sort(bucket.begin(), bucket.end()); + } + // 3. 走訪桶合併結果 + int i = 0; + for (vector &bucket : buckets) { + for (float num : bucket) { + nums[i++] = num; + } + } + } + ``` + +=== "Java" + + ```java title="bucket_sort.java" + /* 桶排序 */ + void bucketSort(float[] nums) { + // 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素 + int k = nums.length / 2; + List> buckets = new ArrayList<>(); + for (int i = 0; i < k; i++) { + buckets.add(new ArrayList<>()); + } + // 1. 將陣列元素分配到各個桶中 + for (float num : nums) { + // 輸入資料範圍為 [0, 1),使用 num * k 對映到索引範圍 [0, k-1] + int i = (int) (num * k); + // 將 num 新增進桶 i + buckets.get(i).add(num); + } + // 2. 對各個桶執行排序 + for (List bucket : buckets) { + // 使用內建排序函式,也可以替換成其他排序演算法 + Collections.sort(bucket); + } + // 3. 走訪桶合併結果 + int i = 0; + for (List bucket : buckets) { + for (float num : bucket) { + nums[i++] = num; + } + } + } + ``` + +=== "C#" + + ```csharp title="bucket_sort.cs" + /* 桶排序 */ + void BucketSort(float[] nums) { + // 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素 + int k = nums.Length / 2; + List> buckets = []; + for (int i = 0; i < k; i++) { + buckets.Add([]); + } + // 1. 將陣列元素分配到各個桶中 + foreach (float num in nums) { + // 輸入資料範圍為 [0, 1),使用 num * k 對映到索引範圍 [0, k-1] + int i = (int)(num * k); + // 將 num 新增進桶 i + buckets[i].Add(num); + } + // 2. 對各個桶執行排序 + foreach (List bucket in buckets) { + // 使用內建排序函式,也可以替換成其他排序演算法 + bucket.Sort(); + } + // 3. 走訪桶合併結果 + int j = 0; + foreach (List bucket in buckets) { + foreach (float num in bucket) { + nums[j++] = num; + } + } + } + ``` + +=== "Go" + + ```go title="bucket_sort.go" + /* 桶排序 */ + func bucketSort(nums []float64) { + // 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素 + k := len(nums) / 2 + buckets := make([][]float64, k) + for i := 0; i < k; i++ { + buckets[i] = make([]float64, 0) + } + // 1. 將陣列元素分配到各個桶中 + for _, num := range nums { + // 輸入資料範圍為 [0, 1),使用 num * k 對映到索引範圍 [0, k-1] + i := int(num * float64(k)) + // 將 num 新增進桶 i + buckets[i] = append(buckets[i], num) + } + // 2. 對各個桶執行排序 + for i := 0; i < k; i++ { + // 使用內建切片排序函式,也可以替換成其他排序演算法 + sort.Float64s(buckets[i]) + } + // 3. 走訪桶合併結果 + i := 0 + for _, bucket := range buckets { + for _, num := range bucket { + nums[i] = num + i++ + } + } + } + ``` + +=== "Swift" + + ```swift title="bucket_sort.swift" + /* 桶排序 */ + func bucketSort(nums: inout [Double]) { + // 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素 + let k = nums.count / 2 + var buckets = (0 ..< k).map { _ in [Double]() } + // 1. 將陣列元素分配到各個桶中 + for num in nums { + // 輸入資料範圍為 [0, 1),使用 num * k 對映到索引範圍 [0, k-1] + let i = Int(num * Double(k)) + // 將 num 新增進桶 i + buckets[i].append(num) + } + // 2. 對各個桶執行排序 + for i in buckets.indices { + // 使用內建排序函式,也可以替換成其他排序演算法 + buckets[i].sort() + } + // 3. 走訪桶合併結果 + var i = nums.startIndex + for bucket in buckets { + for num in bucket { + nums[i] = num + i += 1 + } + } + } + ``` + +=== "JS" + + ```javascript title="bucket_sort.js" + /* 桶排序 */ + function bucketSort(nums) { + // 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素 + const k = nums.length / 2; + const buckets = []; + for (let i = 0; i < k; i++) { + buckets.push([]); + } + // 1. 將陣列元素分配到各個桶中 + for (const num of nums) { + // 輸入資料範圍為 [0, 1),使用 num * k 對映到索引範圍 [0, k-1] + const i = Math.floor(num * k); + // 將 num 新增進桶 i + buckets[i].push(num); + } + // 2. 對各個桶執行排序 + for (const bucket of buckets) { + // 使用內建排序函式,也可以替換成其他排序演算法 + bucket.sort((a, b) => a - b); + } + // 3. 走訪桶合併結果 + let i = 0; + for (const bucket of buckets) { + for (const num of bucket) { + nums[i++] = num; + } + } + } + ``` + +=== "TS" + + ```typescript title="bucket_sort.ts" + /* 桶排序 */ + function bucketSort(nums: number[]): void { + // 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素 + const k = nums.length / 2; + const buckets: number[][] = []; + for (let i = 0; i < k; i++) { + buckets.push([]); + } + // 1. 將陣列元素分配到各個桶中 + for (const num of nums) { + // 輸入資料範圍為 [0, 1),使用 num * k 對映到索引範圍 [0, k-1] + const i = Math.floor(num * k); + // 將 num 新增進桶 i + buckets[i].push(num); + } + // 2. 對各個桶執行排序 + for (const bucket of buckets) { + // 使用內建排序函式,也可以替換成其他排序演算法 + bucket.sort((a, b) => a - b); + } + // 3. 走訪桶合併結果 + let i = 0; + for (const bucket of buckets) { + for (const num of bucket) { + nums[i++] = num; + } + } + } + ``` + +=== "Dart" + + ```dart title="bucket_sort.dart" + /* 桶排序 */ + void bucketSort(List nums) { + // 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素 + int k = nums.length ~/ 2; + List> buckets = List.generate(k, (index) => []); + + // 1. 將陣列元素分配到各個桶中 + for (double _num in nums) { + // 輸入資料範圍為 [0, 1),使用 _num * k 對映到索引範圍 [0, k-1] + int i = (_num * k).toInt(); + // 將 _num 新增進桶 bucket_idx + buckets[i].add(_num); + } + // 2. 對各個桶執行排序 + for (List bucket in buckets) { + bucket.sort(); + } + // 3. 走訪桶合併結果 + int i = 0; + for (List bucket in buckets) { + for (double _num in bucket) { + nums[i++] = _num; + } + } + } + ``` + +=== "Rust" + + ```rust title="bucket_sort.rs" + /* 桶排序 */ + fn bucket_sort(nums: &mut [f64]) { + // 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素 + let k = nums.len() / 2; + let mut buckets = vec![vec![]; k]; + // 1. 將陣列元素分配到各個桶中 + for &mut num in &mut *nums { + // 輸入資料範圍為 [0, 1),使用 num * k 對映到索引範圍 [0, k-1] + let i = (num * k as f64) as usize; + // 將 num 新增進桶 i + buckets[i].push(num); + } + // 2. 對各個桶執行排序 + for bucket in &mut buckets { + // 使用內建排序函式,也可以替換成其他排序演算法 + bucket.sort_by(|a, b| a.partial_cmp(b).unwrap()); + } + // 3. 走訪桶合併結果 + let mut i = 0; + for bucket in &mut buckets { + for &mut num in bucket { + nums[i] = num; + i += 1; + } + } + } + ``` + +=== "C" + + ```c title="bucket_sort.c" + /* 桶排序 */ + void bucketSort(float nums[], int size) { + // 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素 + int k = size / 2; + float **buckets = calloc(k, sizeof(float *)); + for (int i = 0; i < k; i++) { + // 每個桶最多可以分配 size 個元素 + buckets[i] = calloc(size, sizeof(float)); + } + + // 1. 將陣列元素分配到各個桶中 + for (int i = 0; i < size; i++) { + // 輸入資料範圍為 [0, 1),使用 num * k 對映到索引範圍 [0, k-1] + int bucket_idx = nums[i] * k; + int j = 0; + // 如果桶中有資料且資料小於當前值 nums[i], 要將其放到當前桶的後面,相當於 cpp 中的 push_back + while (buckets[bucket_idx][j] > 0 && buckets[bucket_idx][j] < nums[i]) { + j++; + } + float temp = nums[i]; + while (j < size && buckets[bucket_idx][j] > 0) { + swap(&temp, &buckets[bucket_idx][j]); + j++; + } + buckets[bucket_idx][j] = temp; + } + + // 2. 對各個桶執行排序 + for (int i = 0; i < k; i++) { + qsort(buckets[i], size, sizeof(float), compare_float); + } + + // 3. 走訪桶合併結果 + for (int i = 0, j = 0; j < k; j++) { + for (int l = 0; l < size; l++) { + if (buckets[j][l] > 0) { + nums[i++] = buckets[j][l]; + } + } + } + + // 釋放上述分配的記憶體 + for (int i = 0; i < k; i++) { + free(buckets[i]); + } + free(buckets); + } + ``` + +=== "Kotlin" + + ```kotlin title="bucket_sort.kt" + /* 桶排序 */ + fun bucketSort(nums: FloatArray) { + // 初始化 k = n/2 個桶,預期向每個桶分配 2 個元素 + val k = nums.size / 2 + val buckets = ArrayList>() + for (i in 0.. + + +## 11.8.2   演算法特性 + +桶排序適用於處理體量很大的資料。例如,輸入資料包含 100 萬個元素,由於空間限制,系統記憶體無法一次性載入所有資料。此時,可以將資料分成 1000 個桶,然後分別對每個桶進行排序,最後將結果合併。 + +- **時間複雜度為 $O(n + k)$** :假設元素在各個桶內平均分佈,那麼每個桶內的元素數量為 $\frac{n}{k}$ 。假設排序單個桶使用 $O(\frac{n}{k} \log\frac{n}{k})$ 時間,則排序所有桶使用 $O(n \log\frac{n}{k})$ 時間。**當桶數量 $k$ 比較大時,時間複雜度則趨向於 $O(n)$** 。合併結果時需要走訪所有桶和元素,花費 $O(n + k)$ 時間。 +- **自適應排序**:在最差情況下,所有資料被分配到一個桶中,且排序該桶使用 $O(n^2)$ 時間。 +- **空間複雜度為 $O(n + k)$、非原地排序**:需要藉助 $k$ 個桶和總共 $n$ 個元素的額外空間。 +- 桶排序是否穩定取決於排序桶內元素的演算法是否穩定。 + +## 11.8.3   如何實現平均分配 + +桶排序的時間複雜度理論上可以達到 $O(n)$ ,**關鍵在於將元素均勻分配到各個桶中**,因為實際資料往往不是均勻分佈的。例如,我們想要將淘寶上的所有商品按價格範圍平均分配到 10 個桶中,但商品價格分佈不均,低於 100 元的非常多,高於 1000 元的非常少。若將價格區間平均劃分為 10 個,各個桶中的商品數量差距會非常大。 + +為實現平均分配,我們可以先設定一條大致的分界線,將資料粗略地分到 3 個桶中。**分配完畢後,再將商品較多的桶繼續劃分為 3 個桶,直至所有桶中的元素數量大致相等**。 + +如圖 11-14 所示,這種方法本質上是建立一棵遞迴樹,目標是讓葉節點的值儘可能平均。當然,不一定要每輪將資料劃分為 3 個桶,具體劃分方式可根據資料特點靈活選擇。 + +![遞迴劃分桶](bucket_sort.assets/scatter_in_buckets_recursively.png){ class="animation-figure" } + +

圖 11-14   遞迴劃分桶

+ +如果我們提前知道商品價格的機率分佈,**則可以根據資料機率分佈設定每個桶的價格分界線**。值得注意的是,資料分佈並不一定需要特意統計,也可以根據資料特點採用某種機率模型進行近似。 + +如圖 11-15 所示,我們假設商品價格服從正態分佈,這樣就可以合理地設定價格區間,從而將商品平均分配到各個桶中。 + +![根據機率分佈劃分桶](bucket_sort.assets/scatter_in_buckets_distribution.png){ class="animation-figure" } + +

圖 11-15   根據機率分佈劃分桶

diff --git a/zh-Hant/docs/chapter_sorting/counting_sort.md b/zh-Hant/docs/chapter_sorting/counting_sort.md new file mode 100644 index 000000000..83665b8ea --- /dev/null +++ b/zh-Hant/docs/chapter_sorting/counting_sort.md @@ -0,0 +1,883 @@ +--- +comments: true +--- + +# 11.9   計數排序 + +計數排序(counting sort)透過統計元素數量來實現排序,通常應用於整數陣列。 + +## 11.9.1   簡單實現 + +先來看一個簡單的例子。給定一個長度為 $n$ 的陣列 `nums` ,其中的元素都是“非負整數”,計數排序的整體流程如圖 11-16 所示。 + +1. 走訪陣列,找出其中的最大數字,記為 $m$ ,然後建立一個長度為 $m + 1$ 的輔助陣列 `counter` 。 +2. **藉助 `counter` 統計 `nums` 中各數字的出現次數**,其中 `counter[num]` 對應數字 `num` 的出現次數。統計方法很簡單,只需走訪 `nums`(設當前數字為 `num`),每輪將 `counter[num]` 增加 $1$ 即可。 +3. **由於 `counter` 的各個索引天然有序,因此相當於所有數字已經排序好了**。接下來,我們走訪 `counter` ,根據各數字出現次數從小到大的順序填入 `nums` 即可。 + +![計數排序流程](counting_sort.assets/counting_sort_overview.png){ class="animation-figure" } + +

圖 11-16   計數排序流程

+ +程式碼如下所示: + +=== "Python" + + ```python title="counting_sort.py" + def counting_sort_naive(nums: list[int]): + """計數排序""" + # 簡單實現,無法用於排序物件 + # 1. 統計陣列最大元素 m + m = 0 + for num in nums: + m = max(m, num) + # 2. 統計各數字的出現次數 + # counter[num] 代表 num 的出現次數 + counter = [0] * (m + 1) + for num in nums: + counter[num] += 1 + # 3. 走訪 counter ,將各元素填入原陣列 nums + i = 0 + for num in range(m + 1): + for _ in range(counter[num]): + nums[i] = num + i += 1 + ``` + +=== "C++" + + ```cpp title="counting_sort.cpp" + /* 計數排序 */ + // 簡單實現,無法用於排序物件 + void countingSortNaive(vector &nums) { + // 1. 統計陣列最大元素 m + int m = 0; + for (int num : nums) { + m = max(m, num); + } + // 2. 統計各數字的出現次數 + // counter[num] 代表 num 的出現次數 + vector counter(m + 1, 0); + for (int num : nums) { + counter[num]++; + } + // 3. 走訪 counter ,將各元素填入原陣列 nums + int i = 0; + for (int num = 0; num < m + 1; num++) { + for (int j = 0; j < counter[num]; j++, i++) { + nums[i] = num; + } + } + } + ``` + +=== "Java" + + ```java title="counting_sort.java" + /* 計數排序 */ + // 簡單實現,無法用於排序物件 + void countingSortNaive(int[] nums) { + // 1. 統計陣列最大元素 m + int m = 0; + for (int num : nums) { + m = Math.max(m, num); + } + // 2. 統計各數字的出現次數 + // counter[num] 代表 num 的出現次數 + int[] counter = new int[m + 1]; + for (int num : nums) { + counter[num]++; + } + // 3. 走訪 counter ,將各元素填入原陣列 nums + int i = 0; + for (int num = 0; num < m + 1; num++) { + for (int j = 0; j < counter[num]; j++, i++) { + nums[i] = num; + } + } + } + ``` + +=== "C#" + + ```csharp title="counting_sort.cs" + /* 計數排序 */ + // 簡單實現,無法用於排序物件 + void CountingSortNaive(int[] nums) { + // 1. 統計陣列最大元素 m + int m = 0; + foreach (int num in nums) { + m = Math.Max(m, num); + } + // 2. 統計各數字的出現次數 + // counter[num] 代表 num 的出現次數 + int[] counter = new int[m + 1]; + foreach (int num in nums) { + counter[num]++; + } + // 3. 走訪 counter ,將各元素填入原陣列 nums + int i = 0; + for (int num = 0; num < m + 1; num++) { + for (int j = 0; j < counter[num]; j++, i++) { + nums[i] = num; + } + } + } + ``` + +=== "Go" + + ```go title="counting_sort.go" + /* 計數排序 */ + // 簡單實現,無法用於排序物件 + func countingSortNaive(nums []int) { + // 1. 統計陣列最大元素 m + m := 0 + for _, num := range nums { + if num > m { + m = num + } + } + // 2. 統計各數字的出現次數 + // counter[num] 代表 num 的出現次數 + counter := make([]int, m+1) + for _, num := range nums { + counter[num]++ + } + // 3. 走訪 counter ,將各元素填入原陣列 nums + for i, num := 0, 0; num < m+1; num++ { + for j := 0; j < counter[num]; j++ { + nums[i] = num + i++ + } + } + } + ``` + +=== "Swift" + + ```swift title="counting_sort.swift" + /* 計數排序 */ + // 簡單實現,無法用於排序物件 + func countingSortNaive(nums: inout [Int]) { + // 1. 統計陣列最大元素 m + let m = nums.max()! + // 2. 統計各數字的出現次數 + // counter[num] 代表 num 的出現次數 + var counter = Array(repeating: 0, count: m + 1) + for num in nums { + counter[num] += 1 + } + // 3. 走訪 counter ,將各元素填入原陣列 nums + var i = 0 + for num in 0 ..< m + 1 { + for _ in 0 ..< counter[num] { + nums[i] = num + i += 1 + } + } + } + ``` + +=== "JS" + + ```javascript title="counting_sort.js" + /* 計數排序 */ + // 簡單實現,無法用於排序物件 + function countingSortNaive(nums) { + // 1. 統計陣列最大元素 m + let m = 0; + for (const num of nums) { + m = Math.max(m, num); + } + // 2. 統計各數字的出現次數 + // counter[num] 代表 num 的出現次數 + const counter = new Array(m + 1).fill(0); + for (const num of nums) { + counter[num]++; + } + // 3. 走訪 counter ,將各元素填入原陣列 nums + let i = 0; + for (let num = 0; num < m + 1; num++) { + for (let j = 0; j < counter[num]; j++, i++) { + nums[i] = num; + } + } + } + ``` + +=== "TS" + + ```typescript title="counting_sort.ts" + /* 計數排序 */ + // 簡單實現,無法用於排序物件 + function countingSortNaive(nums: number[]): void { + // 1. 統計陣列最大元素 m + let m = 0; + for (const num of nums) { + m = Math.max(m, num); + } + // 2. 統計各數字的出現次數 + // counter[num] 代表 num 的出現次數 + const counter: number[] = new Array(m + 1).fill(0); + for (const num of nums) { + counter[num]++; + } + // 3. 走訪 counter ,將各元素填入原陣列 nums + let i = 0; + for (let num = 0; num < m + 1; num++) { + for (let j = 0; j < counter[num]; j++, i++) { + nums[i] = num; + } + } + } + ``` + +=== "Dart" + + ```dart title="counting_sort.dart" + /* 計數排序 */ + // 簡單實現,無法用於排序物件 + void countingSortNaive(List nums) { + // 1. 統計陣列最大元素 m + int m = 0; + for (int _num in nums) { + m = max(m, _num); + } + // 2. 統計各數字的出現次數 + // counter[_num] 代表 _num 的出現次數 + List counter = List.filled(m + 1, 0); + for (int _num in nums) { + counter[_num]++; + } + // 3. 走訪 counter ,將各元素填入原陣列 nums + int i = 0; + for (int _num = 0; _num < m + 1; _num++) { + for (int j = 0; j < counter[_num]; j++, i++) { + nums[i] = _num; + } + } + } + ``` + +=== "Rust" + + ```rust title="counting_sort.rs" + /* 計數排序 */ + // 簡單實現,無法用於排序物件 + fn counting_sort_naive(nums: &mut [i32]) { + // 1. 統計陣列最大元素 m + let m = *nums.into_iter().max().unwrap(); + // 2. 統計各數字的出現次數 + // counter[num] 代表 num 的出現次數 + let mut counter = vec![0; m as usize + 1]; + for &num in &*nums { + counter[num as usize] += 1; + } + // 3. 走訪 counter ,將各元素填入原陣列 nums + let mut i = 0; + for num in 0..m + 1 { + for _ in 0..counter[num as usize] { + nums[i] = num; + i += 1; + } + } + } + ``` + +=== "C" + + ```c title="counting_sort.c" + /* 計數排序 */ + // 簡單實現,無法用於排序物件 + void countingSortNaive(int nums[], int size) { + // 1. 統計陣列最大元素 m + int m = 0; + for (int i = 0; i < size; i++) { + if (nums[i] > m) { + m = nums[i]; + } + } + // 2. 統計各數字的出現次數 + // counter[num] 代表 num 的出現次數 + int *counter = calloc(m + 1, sizeof(int)); + for (int i = 0; i < size; i++) { + counter[nums[i]]++; + } + // 3. 走訪 counter ,將各元素填入原陣列 nums + int i = 0; + for (int num = 0; num < m + 1; num++) { + for (int j = 0; j < counter[num]; j++, i++) { + nums[i] = num; + } + } + // 4. 釋放記憶體 + free(counter); + } + ``` + +=== "Kotlin" + + ```kotlin title="counting_sort.kt" + /* 計數排序 */ + // 簡單實現,無法用於排序物件 + fun countingSortNaive(nums: IntArray) { + // 1. 統計陣列最大元素 m + var m = 0 + for (num in nums) { + m = max(m.toDouble(), num.toDouble()).toInt() + } + // 2. 統計各數字的出現次數 + // counter[num] 代表 num 的出現次數 + val counter = IntArray(m + 1) + for (num in nums) { + counter[num]++ + } + // 3. 走訪 counter ,將各元素填入原陣列 nums + var i = 0 + for (num in 0.. + + +!!! note "計數排序與桶排序的關聯" + + 從桶排序的角度看,我們可以將計數排序中的計數陣列 `counter` 的每個索引視為一個桶,將統計數量的過程看作將各個元素分配到對應的桶中。本質上,計數排序是桶排序在整型資料下的一個特例。 + +## 11.9.2   完整實現 + +細心的讀者可能發現了,**如果輸入資料是物件,上述步驟 `3.` 就失效了**。假設輸入資料是商品物件,我們想按照商品價格(類別的成員變數)對商品進行排序,而上述演算法只能給出價格的排序結果。 + +那麼如何才能得到原資料的排序結果呢?我們首先計算 `counter` 的“前綴和”。顧名思義,索引 `i` 處的前綴和 `prefix[i]` 等於陣列前 `i` 個元素之和: + +$$ +\text{prefix}[i] = \sum_{j=0}^i \text{counter[j]} +$$ + +**前綴和具有明確的意義,`prefix[num] - 1` 代表元素 `num` 在結果陣列 `res` 中最後一次出現的索引**。這個資訊非常關鍵,因為它告訴我們各個元素應該出現在結果陣列的哪個位置。接下來,我們倒序走訪原陣列 `nums` 的每個元素 `num` ,在每輪迭代中執行以下兩步。 + +1. 將 `num` 填入陣列 `res` 的索引 `prefix[num] - 1` 處。 +2. 令前綴和 `prefix[num]` 減小 $1$ ,從而得到下次放置 `num` 的索引。 + +走訪完成後,陣列 `res` 中就是排序好的結果,最後使用 `res` 覆蓋原陣列 `nums` 即可。圖 11-17 展示了完整的計數排序流程。 + +=== "<1>" + ![計數排序步驟](counting_sort.assets/counting_sort_step1.png){ class="animation-figure" } + +=== "<2>" + ![counting_sort_step2](counting_sort.assets/counting_sort_step2.png){ class="animation-figure" } + +=== "<3>" + ![counting_sort_step3](counting_sort.assets/counting_sort_step3.png){ class="animation-figure" } + +=== "<4>" + ![counting_sort_step4](counting_sort.assets/counting_sort_step4.png){ class="animation-figure" } + +=== "<5>" + ![counting_sort_step5](counting_sort.assets/counting_sort_step5.png){ class="animation-figure" } + +=== "<6>" + ![counting_sort_step6](counting_sort.assets/counting_sort_step6.png){ class="animation-figure" } + +=== "<7>" + ![counting_sort_step7](counting_sort.assets/counting_sort_step7.png){ class="animation-figure" } + +=== "<8>" + ![counting_sort_step8](counting_sort.assets/counting_sort_step8.png){ class="animation-figure" } + +

圖 11-17   計數排序步驟

+ +計數排序的實現程式碼如下所示: + +=== "Python" + + ```python title="counting_sort.py" + def counting_sort(nums: list[int]): + """計數排序""" + # 完整實現,可排序物件,並且是穩定排序 + # 1. 統計陣列最大元素 m + m = max(nums) + # 2. 統計各數字的出現次數 + # counter[num] 代表 num 的出現次數 + counter = [0] * (m + 1) + for num in nums: + counter[num] += 1 + # 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引” + # 即 counter[num]-1 是 num 在 res 中最後一次出現的索引 + for i in range(m): + counter[i + 1] += counter[i] + # 4. 倒序走訪 nums ,將各元素填入結果陣列 res + # 初始化陣列 res 用於記錄結果 + n = len(nums) + res = [0] * n + for i in range(n - 1, -1, -1): + num = nums[i] + res[counter[num] - 1] = num # 將 num 放置到對應索引處 + counter[num] -= 1 # 令前綴和自減 1 ,得到下次放置 num 的索引 + # 使用結果陣列 res 覆蓋原陣列 nums + for i in range(n): + nums[i] = res[i] + ``` + +=== "C++" + + ```cpp title="counting_sort.cpp" + /* 計數排序 */ + // 完整實現,可排序物件,並且是穩定排序 + void countingSort(vector &nums) { + // 1. 統計陣列最大元素 m + int m = 0; + for (int num : nums) { + m = max(m, num); + } + // 2. 統計各數字的出現次數 + // counter[num] 代表 num 的出現次數 + vector counter(m + 1, 0); + for (int num : nums) { + counter[num]++; + } + // 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引” + // 即 counter[num]-1 是 num 在 res 中最後一次出現的索引 + for (int i = 0; i < m; i++) { + counter[i + 1] += counter[i]; + } + // 4. 倒序走訪 nums ,將各元素填入結果陣列 res + // 初始化陣列 res 用於記錄結果 + int n = nums.size(); + vector res(n); + for (int i = n - 1; i >= 0; i--) { + int num = nums[i]; + res[counter[num] - 1] = num; // 將 num 放置到對應索引處 + counter[num]--; // 令前綴和自減 1 ,得到下次放置 num 的索引 + } + // 使用結果陣列 res 覆蓋原陣列 nums + nums = res; + } + ``` + +=== "Java" + + ```java title="counting_sort.java" + /* 計數排序 */ + // 完整實現,可排序物件,並且是穩定排序 + void countingSort(int[] nums) { + // 1. 統計陣列最大元素 m + int m = 0; + for (int num : nums) { + m = Math.max(m, num); + } + // 2. 統計各數字的出現次數 + // counter[num] 代表 num 的出現次數 + int[] counter = new int[m + 1]; + for (int num : nums) { + counter[num]++; + } + // 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引” + // 即 counter[num]-1 是 num 在 res 中最後一次出現的索引 + for (int i = 0; i < m; i++) { + counter[i + 1] += counter[i]; + } + // 4. 倒序走訪 nums ,將各元素填入結果陣列 res + // 初始化陣列 res 用於記錄結果 + int n = nums.length; + int[] res = new int[n]; + for (int i = n - 1; i >= 0; i--) { + int num = nums[i]; + res[counter[num] - 1] = num; // 將 num 放置到對應索引處 + counter[num]--; // 令前綴和自減 1 ,得到下次放置 num 的索引 + } + // 使用結果陣列 res 覆蓋原陣列 nums + for (int i = 0; i < n; i++) { + nums[i] = res[i]; + } + } + ``` + +=== "C#" + + ```csharp title="counting_sort.cs" + /* 計數排序 */ + // 完整實現,可排序物件,並且是穩定排序 + void CountingSort(int[] nums) { + // 1. 統計陣列最大元素 m + int m = 0; + foreach (int num in nums) { + m = Math.Max(m, num); + } + // 2. 統計各數字的出現次數 + // counter[num] 代表 num 的出現次數 + int[] counter = new int[m + 1]; + foreach (int num in nums) { + counter[num]++; + } + // 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引” + // 即 counter[num]-1 是 num 在 res 中最後一次出現的索引 + for (int i = 0; i < m; i++) { + counter[i + 1] += counter[i]; + } + // 4. 倒序走訪 nums ,將各元素填入結果陣列 res + // 初始化陣列 res 用於記錄結果 + int n = nums.Length; + int[] res = new int[n]; + for (int i = n - 1; i >= 0; i--) { + int num = nums[i]; + res[counter[num] - 1] = num; // 將 num 放置到對應索引處 + counter[num]--; // 令前綴和自減 1 ,得到下次放置 num 的索引 + } + // 使用結果陣列 res 覆蓋原陣列 nums + for (int i = 0; i < n; i++) { + nums[i] = res[i]; + } + } + ``` + +=== "Go" + + ```go title="counting_sort.go" + /* 計數排序 */ + // 完整實現,可排序物件,並且是穩定排序 + func countingSort(nums []int) { + // 1. 統計陣列最大元素 m + m := 0 + for _, num := range nums { + if num > m { + m = num + } + } + // 2. 統計各數字的出現次數 + // counter[num] 代表 num 的出現次數 + counter := make([]int, m+1) + for _, num := range nums { + counter[num]++ + } + // 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引” + // 即 counter[num]-1 是 num 在 res 中最後一次出現的索引 + for i := 0; i < m; i++ { + counter[i+1] += counter[i] + } + // 4. 倒序走訪 nums ,將各元素填入結果陣列 res + // 初始化陣列 res 用於記錄結果 + n := len(nums) + res := make([]int, n) + for i := n - 1; i >= 0; i-- { + num := nums[i] + // 將 num 放置到對應索引處 + res[counter[num]-1] = num + // 令前綴和自減 1 ,得到下次放置 num 的索引 + counter[num]-- + } + // 使用結果陣列 res 覆蓋原陣列 nums + copy(nums, res) + } + ``` + +=== "Swift" + + ```swift title="counting_sort.swift" + /* 計數排序 */ + // 完整實現,可排序物件,並且是穩定排序 + func countingSort(nums: inout [Int]) { + // 1. 統計陣列最大元素 m + let m = nums.max()! + // 2. 統計各數字的出現次數 + // counter[num] 代表 num 的出現次數 + var counter = Array(repeating: 0, count: m + 1) + for num in nums { + counter[num] += 1 + } + // 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引” + // 即 counter[num]-1 是 num 在 res 中最後一次出現的索引 + for i in 0 ..< m { + counter[i + 1] += counter[i] + } + // 4. 倒序走訪 nums ,將各元素填入結果陣列 res + // 初始化陣列 res 用於記錄結果 + var res = Array(repeating: 0, count: nums.count) + for i in nums.indices.reversed() { + let num = nums[i] + res[counter[num] - 1] = num // 將 num 放置到對應索引處 + counter[num] -= 1 // 令前綴和自減 1 ,得到下次放置 num 的索引 + } + // 使用結果陣列 res 覆蓋原陣列 nums + for i in nums.indices { + nums[i] = res[i] + } + } + ``` + +=== "JS" + + ```javascript title="counting_sort.js" + /* 計數排序 */ + // 完整實現,可排序物件,並且是穩定排序 + function countingSort(nums) { + // 1. 統計陣列最大元素 m + let m = 0; + for (const num of nums) { + m = Math.max(m, num); + } + // 2. 統計各數字的出現次數 + // counter[num] 代表 num 的出現次數 + const counter = new Array(m + 1).fill(0); + for (const num of nums) { + counter[num]++; + } + // 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引” + // 即 counter[num]-1 是 num 在 res 中最後一次出現的索引 + for (let i = 0; i < m; i++) { + counter[i + 1] += counter[i]; + } + // 4. 倒序走訪 nums ,將各元素填入結果陣列 res + // 初始化陣列 res 用於記錄結果 + const n = nums.length; + const res = new Array(n); + for (let i = n - 1; i >= 0; i--) { + const num = nums[i]; + res[counter[num] - 1] = num; // 將 num 放置到對應索引處 + counter[num]--; // 令前綴和自減 1 ,得到下次放置 num 的索引 + } + // 使用結果陣列 res 覆蓋原陣列 nums + for (let i = 0; i < n; i++) { + nums[i] = res[i]; + } + } + ``` + +=== "TS" + + ```typescript title="counting_sort.ts" + /* 計數排序 */ + // 完整實現,可排序物件,並且是穩定排序 + function countingSort(nums: number[]): void { + // 1. 統計陣列最大元素 m + let m = 0; + for (const num of nums) { + m = Math.max(m, num); + } + // 2. 統計各數字的出現次數 + // counter[num] 代表 num 的出現次數 + const counter: number[] = new Array(m + 1).fill(0); + for (const num of nums) { + counter[num]++; + } + // 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引” + // 即 counter[num]-1 是 num 在 res 中最後一次出現的索引 + for (let i = 0; i < m; i++) { + counter[i + 1] += counter[i]; + } + // 4. 倒序走訪 nums ,將各元素填入結果陣列 res + // 初始化陣列 res 用於記錄結果 + const n = nums.length; + const res: number[] = new Array(n); + for (let i = n - 1; i >= 0; i--) { + const num = nums[i]; + res[counter[num] - 1] = num; // 將 num 放置到對應索引處 + counter[num]--; // 令前綴和自減 1 ,得到下次放置 num 的索引 + } + // 使用結果陣列 res 覆蓋原陣列 nums + for (let i = 0; i < n; i++) { + nums[i] = res[i]; + } + } + ``` + +=== "Dart" + + ```dart title="counting_sort.dart" + /* 計數排序 */ + // 完整實現,可排序物件,並且是穩定排序 + void countingSort(List nums) { + // 1. 統計陣列最大元素 m + int m = 0; + for (int _num in nums) { + m = max(m, _num); + } + // 2. 統計各數字的出現次數 + // counter[_num] 代表 _num 的出現次數 + List counter = List.filled(m + 1, 0); + for (int _num in nums) { + counter[_num]++; + } + // 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引” + // 即 counter[_num]-1 是 _num 在 res 中最後一次出現的索引 + for (int i = 0; i < m; i++) { + counter[i + 1] += counter[i]; + } + // 4. 倒序走訪 nums ,將各元素填入結果陣列 res + // 初始化陣列 res 用於記錄結果 + int n = nums.length; + List res = List.filled(n, 0); + for (int i = n - 1; i >= 0; i--) { + int _num = nums[i]; + res[counter[_num] - 1] = _num; // 將 _num 放置到對應索引處 + counter[_num]--; // 令前綴和自減 1 ,得到下次放置 _num 的索引 + } + // 使用結果陣列 res 覆蓋原陣列 nums + nums.setAll(0, res); + } + ``` + +=== "Rust" + + ```rust title="counting_sort.rs" + /* 計數排序 */ + // 完整實現,可排序物件,並且是穩定排序 + fn counting_sort(nums: &mut [i32]) { + // 1. 統計陣列最大元素 m + let m = *nums.into_iter().max().unwrap(); + // 2. 統計各數字的出現次數 + // counter[num] 代表 num 的出現次數 + let mut counter = vec![0; m as usize + 1]; + for &num in &*nums { + counter[num as usize] += 1; + } + // 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引” + // 即 counter[num]-1 是 num 在 res 中最後一次出現的索引 + for i in 0..m as usize { + counter[i + 1] += counter[i]; + } + // 4. 倒序走訪 nums ,將各元素填入結果陣列 res + // 初始化陣列 res 用於記錄結果 + let n = nums.len(); + let mut res = vec![0; n]; + for i in (0..n).rev() { + let num = nums[i]; + res[counter[num as usize] - 1] = num; // 將 num 放置到對應索引處 + counter[num as usize] -= 1; // 令前綴和自減 1 ,得到下次放置 num 的索引 + } + // 使用結果陣列 res 覆蓋原陣列 nums + for i in 0..n { + nums[i] = res[i]; + } + } + ``` + +=== "C" + + ```c title="counting_sort.c" + /* 計數排序 */ + // 完整實現,可排序物件,並且是穩定排序 + void countingSort(int nums[], int size) { + // 1. 統計陣列最大元素 m + int m = 0; + for (int i = 0; i < size; i++) { + if (nums[i] > m) { + m = nums[i]; + } + } + // 2. 統計各數字的出現次數 + // counter[num] 代表 num 的出現次數 + int *counter = calloc(m, sizeof(int)); + for (int i = 0; i < size; i++) { + counter[nums[i]]++; + } + // 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引” + // 即 counter[num]-1 是 num 在 res 中最後一次出現的索引 + for (int i = 0; i < m; i++) { + counter[i + 1] += counter[i]; + } + // 4. 倒序走訪 nums ,將各元素填入結果陣列 res + // 初始化陣列 res 用於記錄結果 + int *res = malloc(sizeof(int) * size); + for (int i = size - 1; i >= 0; i--) { + int num = nums[i]; + res[counter[num] - 1] = num; // 將 num 放置到對應索引處 + counter[num]--; // 令前綴和自減 1 ,得到下次放置 num 的索引 + } + // 使用結果陣列 res 覆蓋原陣列 nums + memcpy(nums, res, size * sizeof(int)); + // 5. 釋放記憶體 + free(counter); + } + ``` + +=== "Kotlin" + + ```kotlin title="counting_sort.kt" + /* 計數排序 */ + // 完整實現,可排序物件,並且是穩定排序 + fun countingSort(nums: IntArray) { + // 1. 統計陣列最大元素 m + var m = 0 + for (num in nums) { + m = max(m.toDouble(), num.toDouble()).toInt() + } + // 2. 統計各數字的出現次數 + // counter[num] 代表 num 的出現次數 + val counter = IntArray(m + 1) + for (num in nums) { + counter[num]++ + } + // 3. 求 counter 的前綴和,將“出現次數”轉換為“尾索引” + // 即 counter[num]-1 是 num 在 res 中最後一次出現的索引 + for (i in 0.. + + +## 11.9.3   演算法特性 + +- **時間複雜度為 $O(n + m)$** :涉及走訪 `nums` 和走訪 `counter` ,都使用線性時間。一般情況下 $n \gg m$ ,時間複雜度趨於 $O(n)$ 。 +- **空間複雜度為 $O(n + m)$、非原地排序**:藉助了長度分別為 $n$ 和 $m$ 的陣列 `res` 和 `counter` 。 +- **穩定排序**:由於向 `res` 中填充元素的順序是“從右向左”的,因此倒序走訪 `nums` 可以避免改變相等元素之間的相對位置,從而實現穩定排序。實際上,正序走訪 `nums` 也可以得到正確的排序結果,但結果是非穩定的。 + +## 11.9.4   侷限性 + +看到這裡,你也許會覺得計數排序非常巧妙,僅透過統計數量就可以實現高效的排序。然而,使用計數排序的前置條件相對較為嚴格。 + +**計數排序只適用於非負整數**。若想將其用於其他型別的資料,需要確保這些資料可以轉換為非負整數,並且在轉換過程中不能改變各個元素之間的相對大小關係。例如,對於包含負數的整數陣列,可以先給所有數字加上一個常數,將全部數字轉化為正數,排序完成後再轉換回去。 + +**計數排序適用於資料量大但資料範圍較小的情況**。比如,在上述示例中 $m$ 不能太大,否則會佔用過多空間。而當 $n \ll m$ 時,計數排序使用 $O(m)$ 時間,可能比 $O(n \log n)$ 的排序演算法還要慢。 diff --git a/zh-Hant/docs/chapter_sorting/heap_sort.md b/zh-Hant/docs/chapter_sorting/heap_sort.md new file mode 100644 index 000000000..920293948 --- /dev/null +++ b/zh-Hant/docs/chapter_sorting/heap_sort.md @@ -0,0 +1,600 @@ +--- +comments: true +--- + +# 11.7   堆積排序 + +!!! tip + + 閱讀本節前,請確保已學完“堆積“章節。 + +堆積排序(heap sort)是一種基於堆積資料結構實現的高效排序演算法。我們可以利用已經學過的“建堆積操作”和“元素出堆積操作”實現堆積排序。 + +1. 輸入陣列並建立小頂堆積,此時最小元素位於堆積頂。 +2. 不斷執行出堆積操作,依次記錄出堆積元素,即可得到從小到大排序的序列。 + +以上方法雖然可行,但需要藉助一個額外陣列來儲存彈出的元素,比較浪費空間。在實際中,我們通常使用一種更加優雅的實現方式。 + +## 11.7.1   演算法流程 + +設陣列的長度為 $n$ ,堆積排序的流程如圖 11-12 所示。 + +1. 輸入陣列並建立大頂堆積。完成後,最大元素位於堆積頂。 +2. 將堆積頂元素(第一個元素)與堆積底元素(最後一個元素)交換。完成交換後,堆積的長度減 $1$ ,已排序元素數量加 $1$ 。 +3. 從堆積頂元素開始,從頂到底執行堆積化操作(sift down)。完成堆積化後,堆積的性質得到修復。 +4. 迴圈執行第 `2.` 步和第 `3.` 步。迴圈 $n - 1$ 輪後,即可完成陣列排序。 + +!!! tip + + 實際上,元素出堆積操作中也包含第 `2.` 步和第 `3.` 步,只是多了一個彈出元素的步驟。 + +=== "<1>" + ![堆積排序步驟](heap_sort.assets/heap_sort_step1.png){ class="animation-figure" } + +=== "<2>" + ![heap_sort_step2](heap_sort.assets/heap_sort_step2.png){ class="animation-figure" } + +=== "<3>" + ![heap_sort_step3](heap_sort.assets/heap_sort_step3.png){ class="animation-figure" } + +=== "<4>" + ![heap_sort_step4](heap_sort.assets/heap_sort_step4.png){ class="animation-figure" } + +=== "<5>" + ![heap_sort_step5](heap_sort.assets/heap_sort_step5.png){ class="animation-figure" } + +=== "<6>" + ![heap_sort_step6](heap_sort.assets/heap_sort_step6.png){ class="animation-figure" } + +=== "<7>" + ![heap_sort_step7](heap_sort.assets/heap_sort_step7.png){ class="animation-figure" } + +=== "<8>" + ![heap_sort_step8](heap_sort.assets/heap_sort_step8.png){ class="animation-figure" } + +=== "<9>" + ![heap_sort_step9](heap_sort.assets/heap_sort_step9.png){ class="animation-figure" } + +=== "<10>" + ![heap_sort_step10](heap_sort.assets/heap_sort_step10.png){ class="animation-figure" } + +=== "<11>" + ![heap_sort_step11](heap_sort.assets/heap_sort_step11.png){ class="animation-figure" } + +=== "<12>" + ![heap_sort_step12](heap_sort.assets/heap_sort_step12.png){ class="animation-figure" } + +

圖 11-12   堆積排序步驟

+ +在程式碼實現中,我們使用了與“堆積”章節相同的從頂至底堆積化 `sift_down()` 函式。值得注意的是,由於堆積的長度會隨著提取最大元素而減小,因此我們需要給 `sift_down()` 函式新增一個長度參數 $n$ ,用於指定堆積的當前有效長度。程式碼如下所示: + +=== "Python" + + ```python title="heap_sort.py" + def sift_down(nums: list[int], n: int, i: int): + """堆積的長度為 n ,從節點 i 開始,從頂至底堆積化""" + while True: + # 判斷節點 i, l, r 中值最大的節點,記為 ma + l = 2 * i + 1 + r = 2 * i + 2 + ma = i + if l < n and nums[l] > nums[ma]: + ma = l + if r < n and nums[r] > nums[ma]: + ma = r + # 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出 + if ma == i: + break + # 交換兩節點 + nums[i], nums[ma] = nums[ma], nums[i] + # 迴圈向下堆積化 + i = ma + + def heap_sort(nums: list[int]): + """堆積排序""" + # 建堆積操作:堆積化除葉節點以外的其他所有節點 + for i in range(len(nums) // 2 - 1, -1, -1): + sift_down(nums, len(nums), i) + # 從堆積中提取最大元素,迴圈 n-1 輪 + for i in range(len(nums) - 1, 0, -1): + # 交換根節點與最右葉節點(交換首元素與尾元素) + nums[0], nums[i] = nums[i], nums[0] + # 以根節點為起點,從頂至底進行堆積化 + sift_down(nums, i, 0) + ``` + +=== "C++" + + ```cpp title="heap_sort.cpp" + /* 堆積的長度為 n ,從節點 i 開始,從頂至底堆積化 */ + void siftDown(vector &nums, int n, int i) { + while (true) { + // 判斷節點 i, l, r 中值最大的節點,記為 ma + int l = 2 * i + 1; + int r = 2 * i + 2; + int ma = i; + if (l < n && nums[l] > nums[ma]) + ma = l; + if (r < n && nums[r] > nums[ma]) + ma = r; + // 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出 + if (ma == i) { + break; + } + // 交換兩節點 + swap(nums[i], nums[ma]); + // 迴圈向下堆積化 + i = ma; + } + } + + /* 堆積排序 */ + void heapSort(vector &nums) { + // 建堆積操作:堆積化除葉節點以外的其他所有節點 + for (int i = nums.size() / 2 - 1; i >= 0; --i) { + siftDown(nums, nums.size(), i); + } + // 從堆積中提取最大元素,迴圈 n-1 輪 + for (int i = nums.size() - 1; i > 0; --i) { + // 交換根節點與最右葉節點(交換首元素與尾元素) + swap(nums[0], nums[i]); + // 以根節點為起點,從頂至底進行堆積化 + siftDown(nums, i, 0); + } + } + ``` + +=== "Java" + + ```java title="heap_sort.java" + /* 堆積的長度為 n ,從節點 i 開始,從頂至底堆積化 */ + void siftDown(int[] nums, int n, int i) { + while (true) { + // 判斷節點 i, l, r 中值最大的節點,記為 ma + int l = 2 * i + 1; + int r = 2 * i + 2; + int ma = i; + if (l < n && nums[l] > nums[ma]) + ma = l; + if (r < n && nums[r] > nums[ma]) + ma = r; + // 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出 + if (ma == i) + break; + // 交換兩節點 + int temp = nums[i]; + nums[i] = nums[ma]; + nums[ma] = temp; + // 迴圈向下堆積化 + i = ma; + } + } + + /* 堆積排序 */ + void heapSort(int[] nums) { + // 建堆積操作:堆積化除葉節點以外的其他所有節點 + for (int i = nums.length / 2 - 1; i >= 0; i--) { + siftDown(nums, nums.length, i); + } + // 從堆積中提取最大元素,迴圈 n-1 輪 + for (int i = nums.length - 1; i > 0; i--) { + // 交換根節點與最右葉節點(交換首元素與尾元素) + int tmp = nums[0]; + nums[0] = nums[i]; + nums[i] = tmp; + // 以根節點為起點,從頂至底進行堆積化 + siftDown(nums, i, 0); + } + } + ``` + +=== "C#" + + ```csharp title="heap_sort.cs" + /* 堆積的長度為 n ,從節點 i 開始,從頂至底堆積化 */ + void SiftDown(int[] nums, int n, int i) { + while (true) { + // 判斷節點 i, l, r 中值最大的節點,記為 ma + int l = 2 * i + 1; + int r = 2 * i + 2; + int ma = i; + if (l < n && nums[l] > nums[ma]) + ma = l; + if (r < n && nums[r] > nums[ma]) + ma = r; + // 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出 + if (ma == i) + break; + // 交換兩節點 + (nums[ma], nums[i]) = (nums[i], nums[ma]); + // 迴圈向下堆積化 + i = ma; + } + } + + /* 堆積排序 */ + void HeapSort(int[] nums) { + // 建堆積操作:堆積化除葉節點以外的其他所有節點 + for (int i = nums.Length / 2 - 1; i >= 0; i--) { + SiftDown(nums, nums.Length, i); + } + // 從堆積中提取最大元素,迴圈 n-1 輪 + for (int i = nums.Length - 1; i > 0; i--) { + // 交換根節點與最右葉節點(交換首元素與尾元素) + (nums[i], nums[0]) = (nums[0], nums[i]); + // 以根節點為起點,從頂至底進行堆積化 + SiftDown(nums, i, 0); + } + } + ``` + +=== "Go" + + ```go title="heap_sort.go" + /* 堆積的長度為 n ,從節點 i 開始,從頂至底堆積化 */ + func siftDown(nums *[]int, n, i int) { + for true { + // 判斷節點 i, l, r 中值最大的節點,記為 ma + l := 2*i + 1 + r := 2*i + 2 + ma := i + if l < n && (*nums)[l] > (*nums)[ma] { + ma = l + } + if r < n && (*nums)[r] > (*nums)[ma] { + ma = r + } + // 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出 + if ma == i { + break + } + // 交換兩節點 + (*nums)[i], (*nums)[ma] = (*nums)[ma], (*nums)[i] + // 迴圈向下堆積化 + i = ma + } + } + + /* 堆積排序 */ + func heapSort(nums *[]int) { + // 建堆積操作:堆積化除葉節點以外的其他所有節點 + for i := len(*nums)/2 - 1; i >= 0; i-- { + siftDown(nums, len(*nums), i) + } + // 從堆積中提取最大元素,迴圈 n-1 輪 + for i := len(*nums) - 1; i > 0; i-- { + // 交換根節點與最右葉節點(交換首元素與尾元素) + (*nums)[0], (*nums)[i] = (*nums)[i], (*nums)[0] + // 以根節點為起點,從頂至底進行堆積化 + siftDown(nums, i, 0) + } + } + ``` + +=== "Swift" + + ```swift title="heap_sort.swift" + /* 堆積的長度為 n ,從節點 i 開始,從頂至底堆積化 */ + func siftDown(nums: inout [Int], n: Int, i: Int) { + var i = i + while true { + // 判斷節點 i, l, r 中值最大的節點,記為 ma + let l = 2 * i + 1 + let r = 2 * i + 2 + var ma = i + if l < n, nums[l] > nums[ma] { + ma = l + } + if r < n, nums[r] > nums[ma] { + ma = r + } + // 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出 + if ma == i { + break + } + // 交換兩節點 + nums.swapAt(i, ma) + // 迴圈向下堆積化 + i = ma + } + } + + /* 堆積排序 */ + func heapSort(nums: inout [Int]) { + // 建堆積操作:堆積化除葉節點以外的其他所有節點 + for i in stride(from: nums.count / 2 - 1, through: 0, by: -1) { + siftDown(nums: &nums, n: nums.count, i: i) + } + // 從堆積中提取最大元素,迴圈 n-1 輪 + for i in nums.indices.dropFirst().reversed() { + // 交換根節點與最右葉節點(交換首元素與尾元素) + nums.swapAt(0, i) + // 以根節點為起點,從頂至底進行堆積化 + siftDown(nums: &nums, n: i, i: 0) + } + } + ``` + +=== "JS" + + ```javascript title="heap_sort.js" + /* 堆積的長度為 n ,從節點 i 開始,從頂至底堆積化 */ + function siftDown(nums, n, i) { + while (true) { + // 判斷節點 i, l, r 中值最大的節點,記為 ma + let l = 2 * i + 1; + let r = 2 * i + 2; + let ma = i; + if (l < n && nums[l] > nums[ma]) { + ma = l; + } + if (r < n && nums[r] > nums[ma]) { + ma = r; + } + // 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出 + if (ma === i) { + break; + } + // 交換兩節點 + [nums[i], nums[ma]] = [nums[ma], nums[i]]; + // 迴圈向下堆積化 + i = ma; + } + } + + /* 堆積排序 */ + function heapSort(nums) { + // 建堆積操作:堆積化除葉節點以外的其他所有節點 + for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) { + siftDown(nums, nums.length, i); + } + // 從堆積中提取最大元素,迴圈 n-1 輪 + for (let i = nums.length - 1; i > 0; i--) { + // 交換根節點與最右葉節點(交換首元素與尾元素) + [nums[0], nums[i]] = [nums[i], nums[0]]; + // 以根節點為起點,從頂至底進行堆積化 + siftDown(nums, i, 0); + } + } + ``` + +=== "TS" + + ```typescript title="heap_sort.ts" + /* 堆積的長度為 n ,從節點 i 開始,從頂至底堆積化 */ + function siftDown(nums: number[], n: number, i: number): void { + while (true) { + // 判斷節點 i, l, r 中值最大的節點,記為 ma + let l = 2 * i + 1; + let r = 2 * i + 2; + let ma = i; + if (l < n && nums[l] > nums[ma]) { + ma = l; + } + if (r < n && nums[r] > nums[ma]) { + ma = r; + } + // 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出 + if (ma === i) { + break; + } + // 交換兩節點 + [nums[i], nums[ma]] = [nums[ma], nums[i]]; + // 迴圈向下堆積化 + i = ma; + } + } + + /* 堆積排序 */ + function heapSort(nums: number[]): void { + // 建堆積操作:堆積化除葉節點以外的其他所有節點 + for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) { + siftDown(nums, nums.length, i); + } + // 從堆積中提取最大元素,迴圈 n-1 輪 + for (let i = nums.length - 1; i > 0; i--) { + // 交換根節點與最右葉節點(交換首元素與尾元素) + [nums[0], nums[i]] = [nums[i], nums[0]]; + // 以根節點為起點,從頂至底進行堆積化 + siftDown(nums, i, 0); + } + } + ``` + +=== "Dart" + + ```dart title="heap_sort.dart" + /* 堆積的長度為 n ,從節點 i 開始,從頂至底堆積化 */ + void siftDown(List nums, int n, int i) { + while (true) { + // 判斷節點 i, l, r 中值最大的節點,記為 ma + int l = 2 * i + 1; + int r = 2 * i + 2; + int ma = i; + if (l < n && nums[l] > nums[ma]) ma = l; + if (r < n && nums[r] > nums[ma]) ma = r; + // 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出 + if (ma == i) break; + // 交換兩節點 + int temp = nums[i]; + nums[i] = nums[ma]; + nums[ma] = temp; + // 迴圈向下堆積化 + i = ma; + } + } + + /* 堆積排序 */ + void heapSort(List nums) { + // 建堆積操作:堆積化除葉節點以外的其他所有節點 + for (int i = nums.length ~/ 2 - 1; i >= 0; i--) { + siftDown(nums, nums.length, i); + } + // 從堆積中提取最大元素,迴圈 n-1 輪 + for (int i = nums.length - 1; i > 0; i--) { + // 交換根節點與最右葉節點(交換首元素與尾元素) + int tmp = nums[0]; + nums[0] = nums[i]; + nums[i] = tmp; + // 以根節點為起點,從頂至底進行堆積化 + siftDown(nums, i, 0); + } + } + ``` + +=== "Rust" + + ```rust title="heap_sort.rs" + /* 堆積的長度為 n ,從節點 i 開始,從頂至底堆積化 */ + fn sift_down(nums: &mut [i32], n: usize, mut i: usize) { + loop { + // 判斷節點 i, l, r 中值最大的節點,記為 ma + let l = 2 * i + 1; + let r = 2 * i + 2; + let mut ma = i; + if l < n && nums[l] > nums[ma] { + ma = l; + } + if r < n && nums[r] > nums[ma] { + ma = r; + } + // 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出 + if ma == i { + break; + } + // 交換兩節點 + let temp = nums[i]; + nums[i] = nums[ma]; + nums[ma] = temp; + // 迴圈向下堆積化 + i = ma; + } + } + + /* 堆積排序 */ + fn heap_sort(nums: &mut [i32]) { + // 建堆積操作:堆積化除葉節點以外的其他所有節點 + for i in (0..=nums.len() / 2 - 1).rev() { + sift_down(nums, nums.len(), i); + } + // 從堆積中提取最大元素,迴圈 n-1 輪 + for i in (1..=nums.len() - 1).rev() { + // 交換根節點與最右葉節點(交換首元素與尾元素) + let tmp = nums[0]; + nums[0] = nums[i]; + nums[i] = tmp; + // 以根節點為起點,從頂至底進行堆積化 + sift_down(nums, i, 0); + } + } + ``` + +=== "C" + + ```c title="heap_sort.c" + /* 堆積的長度為 n ,從節點 i 開始,從頂至底堆積化 */ + void siftDown(int nums[], int n, int i) { + while (1) { + // 判斷節點 i, l, r 中值最大的節點,記為 ma + int l = 2 * i + 1; + int r = 2 * i + 2; + int ma = i; + if (l < n && nums[l] > nums[ma]) + ma = l; + if (r < n && nums[r] > nums[ma]) + ma = r; + // 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出 + if (ma == i) { + break; + } + // 交換兩節點 + int temp = nums[i]; + nums[i] = nums[ma]; + nums[ma] = temp; + // 迴圈向下堆積化 + i = ma; + } + } + + /* 堆積排序 */ + void heapSort(int nums[], int n) { + // 建堆積操作:堆積化除葉節點以外的其他所有節點 + for (int i = n / 2 - 1; i >= 0; --i) { + siftDown(nums, n, i); + } + // 從堆積中提取最大元素,迴圈 n-1 輪 + for (int i = n - 1; i > 0; --i) { + // 交換根節點與最右葉節點(交換首元素與尾元素) + int tmp = nums[0]; + nums[0] = nums[i]; + nums[i] = tmp; + // 以根節點為起點,從頂至底進行堆積化 + siftDown(nums, i, 0); + } + } + ``` + +=== "Kotlin" + + ```kotlin title="heap_sort.kt" + /* 堆積的長度為 n ,從節點 i 開始,從頂至底堆積化 */ + fun siftDown(nums: IntArray, n: Int, li: Int) { + var i = li + while (true) { + // 判斷節點 i, l, r 中值最大的節點,記為 ma + val l = 2 * i + 1 + val r = 2 * i + 2 + var ma = i + if (l < n && nums[l] > nums[ma]) ma = l + if (r < n && nums[r] > nums[ma]) ma = r + // 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出 + if (ma == i) break + // 交換兩節點 + nums[i] = nums[ma].also { nums[ma] = nums[i] } + // 迴圈向下堆積化 + i = ma + } + } + + /* 堆積排序 */ + fun heapSort(nums: IntArray) { + // 建堆積操作:堆積化除葉節點以外的其他所有節點 + for (i in nums.size / 2 - 1 downTo 0) { + siftDown(nums, nums.size, i) + } + // 從堆積中提取最大元素,迴圈 n-1 輪 + for (i in nums.size - 1 downTo 1) { + // 交換根節點與最右葉節點(交換首元素與尾元素) + nums[0] = nums[i].also { nums[i] = nums[0] } + // 以根節點為起點,從頂至底進行堆積化 + siftDown(nums, i, 0) + } + } + ``` + +=== "Ruby" + + ```ruby title="heap_sort.rb" + [class]{}-[func]{sift_down} + + [class]{}-[func]{heap_sort} + ``` + +=== "Zig" + + ```zig title="heap_sort.zig" + [class]{}-[func]{siftDown} + + [class]{}-[func]{heapSort} + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 11.7.2   演算法特性 + +- **時間複雜度為 $O(n \log n)$、非自適應排序**:建堆積操作使用 $O(n)$ 時間。從堆積中提取最大元素的時間複雜度為 $O(\log n)$ ,共迴圈 $n - 1$ 輪。 +- **空間複雜度為 $O(1)$、原地排序**:幾個指標變數使用 $O(1)$ 空間。元素交換和堆積化操作都是在原陣列上進行的。 +- **非穩定排序**:在交換堆積頂元素和堆積底元素時,相等元素的相對位置可能發生變化。 diff --git a/zh-Hant/docs/chapter_sorting/index.md b/zh-Hant/docs/chapter_sorting/index.md new file mode 100644 index 000000000..6b3c735d5 --- /dev/null +++ b/zh-Hant/docs/chapter_sorting/index.md @@ -0,0 +1,28 @@ +--- +comments: true +icon: material/sort-ascending +--- + +# 第 11 章   排序 + +![排序](../assets/covers/chapter_sorting.jpg){ class="cover-image" } + +!!! abstract + + 排序猶如一把將混亂變為秩序的魔法鑰匙,使我們能以更高效的方式理解與處理資料。 + + 無論是簡單的升序,還是複雜的分類排列,排序都向我們展示了資料的和諧美感。 + +## Chapter Contents + +- [11.1   排序演算法](https://www.hello-algo.com/en/chapter_sorting/sorting_algorithm/) +- [11.2   選擇排序](https://www.hello-algo.com/en/chapter_sorting/selection_sort/) +- [11.3   泡沫排序](https://www.hello-algo.com/en/chapter_sorting/bubble_sort/) +- [11.4   插入排序](https://www.hello-algo.com/en/chapter_sorting/insertion_sort/) +- [11.5   快速排序](https://www.hello-algo.com/en/chapter_sorting/quick_sort/) +- [11.6   合併排序](https://www.hello-algo.com/en/chapter_sorting/merge_sort/) +- [11.7   堆積排序](https://www.hello-algo.com/en/chapter_sorting/heap_sort/) +- [11.8   桶排序](https://www.hello-algo.com/en/chapter_sorting/bucket_sort/) +- [11.9   計數排序](https://www.hello-algo.com/en/chapter_sorting/counting_sort/) +- [11.10   基數排序](https://www.hello-algo.com/en/chapter_sorting/radix_sort/) +- [11.11   小結](https://www.hello-algo.com/en/chapter_sorting/summary/) diff --git a/zh-Hant/docs/chapter_sorting/insertion_sort.md b/zh-Hant/docs/chapter_sorting/insertion_sort.md new file mode 100755 index 000000000..e1981665b --- /dev/null +++ b/zh-Hant/docs/chapter_sorting/insertion_sort.md @@ -0,0 +1,301 @@ +--- +comments: true +--- + +# 11.4   插入排序 + +插入排序(insertion sort)是一種簡單的排序演算法,它的工作原理與手動整理一副牌的過程非常相似。 + +具體來說,我們在未排序區間選擇一個基準元素,將該元素與其左側已排序區間的元素逐一比較大小,並將該元素插入到正確的位置。 + +圖 11-6 展示了陣列插入元素的操作流程。設基準元素為 `base` ,我們需要將從目標索引到 `base` 之間的所有元素向右移動一位,然後將 `base` 賦值給目標索引。 + +![單次插入操作](insertion_sort.assets/insertion_operation.png){ class="animation-figure" } + +

圖 11-6   單次插入操作

+ +## 11.4.1   演算法流程 + +插入排序的整體流程如圖 11-7 所示。 + +1. 初始狀態下,陣列的第 1 個元素已完成排序。 +2. 選取陣列的第 2 個元素作為 `base` ,將其插入到正確位置後,**陣列的前 2 個元素已排序**。 +3. 選取第 3 個元素作為 `base` ,將其插入到正確位置後,**陣列的前 3 個元素已排序**。 +4. 以此類推,在最後一輪中,選取最後一個元素作為 `base` ,將其插入到正確位置後,**所有元素均已排序**。 + +![插入排序流程](insertion_sort.assets/insertion_sort_overview.png){ class="animation-figure" } + +

圖 11-7   插入排序流程

+ +示例程式碼如下: + +=== "Python" + + ```python title="insertion_sort.py" + def insertion_sort(nums: list[int]): + """插入排序""" + # 外迴圈:已排序區間為 [0, i-1] + for i in range(1, len(nums)): + base = nums[i] + j = i - 1 + # 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置 + while j >= 0 and nums[j] > base: + nums[j + 1] = nums[j] # 將 nums[j] 向右移動一位 + j -= 1 + nums[j + 1] = base # 將 base 賦值到正確位置 + ``` + +=== "C++" + + ```cpp title="insertion_sort.cpp" + /* 插入排序 */ + void insertionSort(vector &nums) { + // 外迴圈:已排序區間為 [0, i-1] + for (int i = 1; i < nums.size(); i++) { + int base = nums[i], j = i - 1; + // 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置 + while (j >= 0 && nums[j] > base) { + nums[j + 1] = nums[j]; // 將 nums[j] 向右移動一位 + j--; + } + nums[j + 1] = base; // 將 base 賦值到正確位置 + } + } + ``` + +=== "Java" + + ```java title="insertion_sort.java" + /* 插入排序 */ + void insertionSort(int[] nums) { + // 外迴圈:已排序區間為 [0, i-1] + for (int i = 1; i < nums.length; i++) { + int base = nums[i], j = i - 1; + // 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置 + while (j >= 0 && nums[j] > base) { + nums[j + 1] = nums[j]; // 將 nums[j] 向右移動一位 + j--; + } + nums[j + 1] = base; // 將 base 賦值到正確位置 + } + } + ``` + +=== "C#" + + ```csharp title="insertion_sort.cs" + /* 插入排序 */ + void InsertionSort(int[] nums) { + // 外迴圈:已排序區間為 [0, i-1] + for (int i = 1; i < nums.Length; i++) { + int bas = nums[i], j = i - 1; + // 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置 + while (j >= 0 && nums[j] > bas) { + nums[j + 1] = nums[j]; // 將 nums[j] 向右移動一位 + j--; + } + nums[j + 1] = bas; // 將 base 賦值到正確位置 + } + } + ``` + +=== "Go" + + ```go title="insertion_sort.go" + /* 插入排序 */ + func insertionSort(nums []int) { + // 外迴圈:已排序區間為 [0, i-1] + for i := 1; i < len(nums); i++ { + base := nums[i] + j := i - 1 + // 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置 + for j >= 0 && nums[j] > base { + nums[j+1] = nums[j] // 將 nums[j] 向右移動一位 + j-- + } + nums[j+1] = base // 將 base 賦值到正確位置 + } + } + ``` + +=== "Swift" + + ```swift title="insertion_sort.swift" + /* 插入排序 */ + func insertionSort(nums: inout [Int]) { + // 外迴圈:已排序區間為 [0, i-1] + for i in nums.indices.dropFirst() { + let base = nums[i] + var j = i - 1 + // 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置 + while j >= 0, nums[j] > base { + nums[j + 1] = nums[j] // 將 nums[j] 向右移動一位 + j -= 1 + } + nums[j + 1] = base // 將 base 賦值到正確位置 + } + } + ``` + +=== "JS" + + ```javascript title="insertion_sort.js" + /* 插入排序 */ + function insertionSort(nums) { + // 外迴圈:已排序區間為 [0, i-1] + for (let i = 1; i < nums.length; i++) { + let base = nums[i], + j = i - 1; + // 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置 + while (j >= 0 && nums[j] > base) { + nums[j + 1] = nums[j]; // 將 nums[j] 向右移動一位 + j--; + } + nums[j + 1] = base; // 將 base 賦值到正確位置 + } + } + ``` + +=== "TS" + + ```typescript title="insertion_sort.ts" + /* 插入排序 */ + function insertionSort(nums: number[]): void { + // 外迴圈:已排序區間為 [0, i-1] + for (let i = 1; i < nums.length; i++) { + const base = nums[i]; + let j = i - 1; + // 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置 + while (j >= 0 && nums[j] > base) { + nums[j + 1] = nums[j]; // 將 nums[j] 向右移動一位 + j--; + } + nums[j + 1] = base; // 將 base 賦值到正確位置 + } + } + ``` + +=== "Dart" + + ```dart title="insertion_sort.dart" + /* 插入排序 */ + void insertionSort(List nums) { + // 外迴圈:已排序區間為 [0, i-1] + for (int i = 1; i < nums.length; i++) { + int base = nums[i], j = i - 1; + // 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置 + while (j >= 0 && nums[j] > base) { + nums[j + 1] = nums[j]; // 將 nums[j] 向右移動一位 + j--; + } + nums[j + 1] = base; // 將 base 賦值到正確位置 + } + } + ``` + +=== "Rust" + + ```rust title="insertion_sort.rs" + /* 插入排序 */ + fn insertion_sort(nums: &mut [i32]) { + // 外迴圈:已排序區間為 [0, i-1] + for i in 1..nums.len() { + let (base, mut j) = (nums[i], (i - 1) as i32); + // 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置 + while j >= 0 && nums[j as usize] > base { + nums[(j + 1) as usize] = nums[j as usize]; // 將 nums[j] 向右移動一位 + j -= 1; + } + nums[(j + 1) as usize] = base; // 將 base 賦值到正確位置 + } + } + ``` + +=== "C" + + ```c title="insertion_sort.c" + /* 插入排序 */ + void insertionSort(int nums[], int size) { + // 外迴圈:已排序區間為 [0, i-1] + for (int i = 1; i < size; i++) { + int base = nums[i], j = i - 1; + // 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置 + while (j >= 0 && nums[j] > base) { + // 將 nums[j] 向右移動一位 + nums[j + 1] = nums[j]; + j--; + } + // 將 base 賦值到正確位置 + nums[j + 1] = base; + } + } + ``` + +=== "Kotlin" + + ```kotlin title="insertion_sort.kt" + /* 插入排序 */ + fun insertionSort(nums: IntArray) { + //外迴圈: 已排序元素為 1, 2, ..., n + for (i in nums.indices) { + val base = nums[i] + var j = i - 1 + // 內迴圈: 將 base 插入到已排序部分的正確位置 + while (j >= 0 && nums[j] > base) { + nums[j + 1] = nums[j] // 將 nums[j] 向右移動一位 + j-- + } + nums[j + 1] = base // 將 base 賦值到正確位置 + } + } + ``` + +=== "Ruby" + + ```ruby title="insertion_sort.rb" + [class]{}-[func]{insertion_sort} + ``` + +=== "Zig" + + ```zig title="insertion_sort.zig" + // 插入排序 + fn insertionSort(nums: []i32) void { + // 外迴圈:已排序區間為 [0, i-1] + var i: usize = 1; + while (i < nums.len) : (i += 1) { + var base = nums[i]; + var j: usize = i; + // 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置 + while (j >= 1 and nums[j - 1] > base) : (j -= 1) { + nums[j] = nums[j - 1]; // 將 nums[j] 向右移動一位 + } + nums[j] = base; // 將 base 賦值到正確位置 + } + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 11.4.2   演算法特性 + +- **時間複雜度為 $O(n^2)$、自適應排序**:在最差情況下,每次插入操作分別需要迴圈 $n - 1$、$n-2$、$\dots$、$2$、$1$ 次,求和得到 $(n - 1) n / 2$ ,因此時間複雜度為 $O(n^2)$ 。在遇到有序資料時,插入操作會提前終止。當輸入陣列完全有序時,插入排序達到最佳時間複雜度 $O(n)$ 。 +- **空間複雜度為 $O(1)$、原地排序**:指標 $i$ 和 $j$ 使用常數大小的額外空間。 +- **穩定排序**:在插入操作過程中,我們會將元素插入到相等元素的右側,不會改變它們的順序。 + +## 11.4.3   插入排序的優勢 + +插入排序的時間複雜度為 $O(n^2)$ ,而我們即將學習的快速排序的時間複雜度為 $O(n \log n)$ 。儘管插入排序的時間複雜度更高,**但在資料量較小的情況下,插入排序通常更快**。 + +這個結論與線性查詢和二分搜尋的適用情況的結論類似。快速排序這類 $O(n \log n)$ 的演算法屬於基於分治策略的排序演算法,往往包含更多單元計算操作。而在資料量較小時,$n^2$ 和 $n \log n$ 的數值比較接近,複雜度不佔主導地位,每輪中的單元操作數量起到決定性作用。 + +實際上,許多程式語言(例如 Java)的內建排序函式採用了插入排序,大致思路為:對於長陣列,採用基於分治策略的排序演算法,例如快速排序;對於短陣列,直接使用插入排序。 + +雖然泡沫排序、選擇排序和插入排序的時間複雜度都為 $O(n^2)$ ,但在實際情況中,**插入排序的使用頻率顯著高於泡沫排序和選擇排序**,主要有以下原因。 + +- 泡沫排序基於元素交換實現,需要藉助一個臨時變數,共涉及 3 個單元操作;插入排序基於元素賦值實現,僅需 1 個單元操作。因此,**泡沫排序的計算開銷通常比插入排序更高**。 +- 選擇排序在任何情況下的時間複雜度都為 $O(n^2)$ 。**如果給定一組部分有序的資料,插入排序通常比選擇排序效率更高**。 +- 選擇排序不穩定,無法應用於多級排序。 diff --git a/zh-Hant/docs/chapter_sorting/merge_sort.md b/zh-Hant/docs/chapter_sorting/merge_sort.md new file mode 100755 index 000000000..3d059745a --- /dev/null +++ b/zh-Hant/docs/chapter_sorting/merge_sort.md @@ -0,0 +1,708 @@ +--- +comments: true +--- + +# 11.6   合併排序 + +合併排序(merge sort)是一種基於分治策略的排序演算法,包含圖 11-10 所示的“劃分”和“合併”階段。 + +1. **劃分階段**:透過遞迴不斷地將陣列從中點處分開,將長陣列的排序問題轉換為短陣列的排序問題。 +2. **合併階段**:當子陣列長度為 1 時終止劃分,開始合併,持續地將左右兩個較短的有序陣列合併為一個較長的有序陣列,直至結束。 + +![合併排序的劃分與合併階段](merge_sort.assets/merge_sort_overview.png){ class="animation-figure" } + +

圖 11-10   合併排序的劃分與合併階段

+ +## 11.6.1   演算法流程 + +如圖 11-11 所示,“劃分階段”從頂至底遞迴地將陣列從中點切分為兩個子陣列。 + +1. 計算陣列中點 `mid` ,遞迴劃分左子陣列(區間 `[left, mid]` )和右子陣列(區間 `[mid + 1, right]` )。 +2. 遞迴執行步驟 `1.` ,直至子陣列區間長度為 1 時終止。 + +“合併階段”從底至頂地將左子陣列和右子陣列合併為一個有序陣列。需要注意的是,從長度為 1 的子陣列開始合併,合併階段中的每個子陣列都是有序的。 + +=== "<1>" + ![合併排序步驟](merge_sort.assets/merge_sort_step1.png){ class="animation-figure" } + +=== "<2>" + ![merge_sort_step2](merge_sort.assets/merge_sort_step2.png){ class="animation-figure" } + +=== "<3>" + ![merge_sort_step3](merge_sort.assets/merge_sort_step3.png){ class="animation-figure" } + +=== "<4>" + ![merge_sort_step4](merge_sort.assets/merge_sort_step4.png){ class="animation-figure" } + +=== "<5>" + ![merge_sort_step5](merge_sort.assets/merge_sort_step5.png){ class="animation-figure" } + +=== "<6>" + ![merge_sort_step6](merge_sort.assets/merge_sort_step6.png){ class="animation-figure" } + +=== "<7>" + ![merge_sort_step7](merge_sort.assets/merge_sort_step7.png){ class="animation-figure" } + +=== "<8>" + ![merge_sort_step8](merge_sort.assets/merge_sort_step8.png){ class="animation-figure" } + +=== "<9>" + ![merge_sort_step9](merge_sort.assets/merge_sort_step9.png){ class="animation-figure" } + +=== "<10>" + ![merge_sort_step10](merge_sort.assets/merge_sort_step10.png){ class="animation-figure" } + +

圖 11-11   合併排序步驟

+ +觀察發現,合併排序與二元樹後序走訪的遞迴順序是一致的。 + +- **後序走訪**:先遞迴左子樹,再遞迴右子樹,最後處理根節點。 +- **合併排序**:先遞迴左子陣列,再遞迴右子陣列,最後處理合併。 + +合併排序的實現如以下程式碼所示。請注意,`nums` 的待合併區間為 `[left, right]` ,而 `tmp` 的對應區間為 `[0, right - left]` 。 + +=== "Python" + + ```python title="merge_sort.py" + def merge(nums: list[int], left: int, mid: int, right: int): + """合併左子陣列和右子陣列""" + # 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right] + # 建立一個臨時陣列 tmp ,用於存放合併後的結果 + tmp = [0] * (right - left + 1) + # 初始化左子陣列和右子陣列的起始索引 + i, j, k = left, mid + 1, 0 + # 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中 + while i <= mid and j <= right: + if nums[i] <= nums[j]: + tmp[k] = nums[i] + i += 1 + else: + tmp[k] = nums[j] + j += 1 + k += 1 + # 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中 + while i <= mid: + tmp[k] = nums[i] + i += 1 + k += 1 + while j <= right: + tmp[k] = nums[j] + j += 1 + k += 1 + # 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間 + for k in range(0, len(tmp)): + nums[left + k] = tmp[k] + + def merge_sort(nums: list[int], left: int, right: int): + """合併排序""" + # 終止條件 + if left >= right: + return # 當子陣列長度為 1 時終止遞迴 + # 劃分階段 + mid = (left + right) // 2 # 計算中點 + merge_sort(nums, left, mid) # 遞迴左子陣列 + merge_sort(nums, mid + 1, right) # 遞迴右子陣列 + # 合併階段 + merge(nums, left, mid, right) + ``` + +=== "C++" + + ```cpp title="merge_sort.cpp" + /* 合併左子陣列和右子陣列 */ + void merge(vector &nums, int left, int mid, int right) { + // 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right] + // 建立一個臨時陣列 tmp ,用於存放合併後的結果 + vector tmp(right - left + 1); + // 初始化左子陣列和右子陣列的起始索引 + int i = left, j = mid + 1, k = 0; + // 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中 + while (i <= mid && j <= right) { + if (nums[i] <= nums[j]) + tmp[k++] = nums[i++]; + else + tmp[k++] = nums[j++]; + } + // 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中 + while (i <= mid) { + tmp[k++] = nums[i++]; + } + while (j <= right) { + tmp[k++] = nums[j++]; + } + // 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間 + for (k = 0; k < tmp.size(); k++) { + nums[left + k] = tmp[k]; + } + } + + /* 合併排序 */ + void mergeSort(vector &nums, int left, int right) { + // 終止條件 + if (left >= right) + return; // 當子陣列長度為 1 時終止遞迴 + // 劃分階段 + int mid = (left + right) / 2; // 計算中點 + mergeSort(nums, left, mid); // 遞迴左子陣列 + mergeSort(nums, mid + 1, right); // 遞迴右子陣列 + // 合併階段 + merge(nums, left, mid, right); + } + ``` + +=== "Java" + + ```java title="merge_sort.java" + /* 合併左子陣列和右子陣列 */ + void merge(int[] nums, int left, int mid, int right) { + // 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right] + // 建立一個臨時陣列 tmp ,用於存放合併後的結果 + int[] tmp = new int[right - left + 1]; + // 初始化左子陣列和右子陣列的起始索引 + int i = left, j = mid + 1, k = 0; + // 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中 + while (i <= mid && j <= right) { + if (nums[i] <= nums[j]) + tmp[k++] = nums[i++]; + else + tmp[k++] = nums[j++]; + } + // 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中 + while (i <= mid) { + tmp[k++] = nums[i++]; + } + while (j <= right) { + tmp[k++] = nums[j++]; + } + // 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間 + for (k = 0; k < tmp.length; k++) { + nums[left + k] = tmp[k]; + } + } + + /* 合併排序 */ + void mergeSort(int[] nums, int left, int right) { + // 終止條件 + if (left >= right) + return; // 當子陣列長度為 1 時終止遞迴 + // 劃分階段 + int mid = (left + right) / 2; // 計算中點 + mergeSort(nums, left, mid); // 遞迴左子陣列 + mergeSort(nums, mid + 1, right); // 遞迴右子陣列 + // 合併階段 + merge(nums, left, mid, right); + } + ``` + +=== "C#" + + ```csharp title="merge_sort.cs" + /* 合併左子陣列和右子陣列 */ + void Merge(int[] nums, int left, int mid, int right) { + // 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right] + // 建立一個臨時陣列 tmp ,用於存放合併後的結果 + int[] tmp = new int[right - left + 1]; + // 初始化左子陣列和右子陣列的起始索引 + int i = left, j = mid + 1, k = 0; + // 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中 + while (i <= mid && j <= right) { + if (nums[i] <= nums[j]) + tmp[k++] = nums[i++]; + else + tmp[k++] = nums[j++]; + } + // 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中 + while (i <= mid) { + tmp[k++] = nums[i++]; + } + while (j <= right) { + tmp[k++] = nums[j++]; + } + // 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間 + for (k = 0; k < tmp.Length; ++k) { + nums[left + k] = tmp[k]; + } + } + + /* 合併排序 */ + void MergeSort(int[] nums, int left, int right) { + // 終止條件 + if (left >= right) return; // 當子陣列長度為 1 時終止遞迴 + // 劃分階段 + int mid = (left + right) / 2; // 計算中點 + MergeSort(nums, left, mid); // 遞迴左子陣列 + MergeSort(nums, mid + 1, right); // 遞迴右子陣列 + // 合併階段 + Merge(nums, left, mid, right); + } + ``` + +=== "Go" + + ```go title="merge_sort.go" + /* 合併左子陣列和右子陣列 */ + func merge(nums []int, left, mid, right int) { + // 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right] + // 建立一個臨時陣列 tmp ,用於存放合併後的結果 + tmp := make([]int, right-left+1) + // 初始化左子陣列和右子陣列的起始索引 + i, j, k := left, mid+1, 0 + // 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中 + for i <= mid && j <= right { + if nums[i] <= nums[j] { + tmp[k] = nums[i] + i++ + } else { + tmp[k] = nums[j] + j++ + } + k++ + } + // 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中 + for i <= mid { + tmp[k] = nums[i] + i++ + k++ + } + for j <= right { + tmp[k] = nums[j] + j++ + k++ + } + // 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間 + for k := 0; k < len(tmp); k++ { + nums[left+k] = tmp[k] + } + } + + /* 合併排序 */ + func mergeSort(nums []int, left, right int) { + // 終止條件 + if left >= right { + return + } + // 劃分階段 + mid := (left + right) / 2 + mergeSort(nums, left, mid) + mergeSort(nums, mid+1, right) + // 合併階段 + merge(nums, left, mid, right) + } + ``` + +=== "Swift" + + ```swift title="merge_sort.swift" + /* 合併左子陣列和右子陣列 */ + func merge(nums: inout [Int], left: Int, mid: Int, right: Int) { + // 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right] + // 建立一個臨時陣列 tmp ,用於存放合併後的結果 + var tmp = Array(repeating: 0, count: right - left + 1) + // 初始化左子陣列和右子陣列的起始索引 + var i = left, j = mid + 1, k = 0 + // 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中 + while i <= mid, j <= right { + if nums[i] <= nums[j] { + tmp[k] = nums[i] + i += 1 + } else { + tmp[k] = nums[j] + j += 1 + } + k += 1 + } + // 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中 + while i <= mid { + tmp[k] = nums[i] + i += 1 + k += 1 + } + while j <= right { + tmp[k] = nums[j] + j += 1 + k += 1 + } + // 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間 + for k in tmp.indices { + nums[left + k] = tmp[k] + } + } + + /* 合併排序 */ + func mergeSort(nums: inout [Int], left: Int, right: Int) { + // 終止條件 + if left >= right { // 當子陣列長度為 1 時終止遞迴 + return + } + // 劃分階段 + let mid = (left + right) / 2 // 計算中點 + mergeSort(nums: &nums, left: left, right: mid) // 遞迴左子陣列 + mergeSort(nums: &nums, left: mid + 1, right: right) // 遞迴右子陣列 + // 合併階段 + merge(nums: &nums, left: left, mid: mid, right: right) + } + ``` + +=== "JS" + + ```javascript title="merge_sort.js" + /* 合併左子陣列和右子陣列 */ + function merge(nums, left, mid, right) { + // 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right] + // 建立一個臨時陣列 tmp ,用於存放合併後的結果 + const tmp = new Array(right - left + 1); + // 初始化左子陣列和右子陣列的起始索引 + let i = left, + j = mid + 1, + k = 0; + // 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中 + while (i <= mid && j <= right) { + if (nums[i] <= nums[j]) { + tmp[k++] = nums[i++]; + } else { + tmp[k++] = nums[j++]; + } + } + // 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中 + while (i <= mid) { + tmp[k++] = nums[i++]; + } + while (j <= right) { + tmp[k++] = nums[j++]; + } + // 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間 + for (k = 0; k < tmp.length; k++) { + nums[left + k] = tmp[k]; + } + } + + /* 合併排序 */ + function mergeSort(nums, left, right) { + // 終止條件 + if (left >= right) return; // 當子陣列長度為 1 時終止遞迴 + // 劃分階段 + let mid = Math.floor((left + right) / 2); // 計算中點 + mergeSort(nums, left, mid); // 遞迴左子陣列 + mergeSort(nums, mid + 1, right); // 遞迴右子陣列 + // 合併階段 + merge(nums, left, mid, right); + } + ``` + +=== "TS" + + ```typescript title="merge_sort.ts" + /* 合併左子陣列和右子陣列 */ + function merge(nums: number[], left: number, mid: number, right: number): void { + // 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right] + // 建立一個臨時陣列 tmp ,用於存放合併後的結果 + const tmp = new Array(right - left + 1); + // 初始化左子陣列和右子陣列的起始索引 + let i = left, + j = mid + 1, + k = 0; + // 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中 + while (i <= mid && j <= right) { + if (nums[i] <= nums[j]) { + tmp[k++] = nums[i++]; + } else { + tmp[k++] = nums[j++]; + } + } + // 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中 + while (i <= mid) { + tmp[k++] = nums[i++]; + } + while (j <= right) { + tmp[k++] = nums[j++]; + } + // 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間 + for (k = 0; k < tmp.length; k++) { + nums[left + k] = tmp[k]; + } + } + + /* 合併排序 */ + function mergeSort(nums: number[], left: number, right: number): void { + // 終止條件 + if (left >= right) return; // 當子陣列長度為 1 時終止遞迴 + // 劃分階段 + let mid = Math.floor((left + right) / 2); // 計算中點 + mergeSort(nums, left, mid); // 遞迴左子陣列 + mergeSort(nums, mid + 1, right); // 遞迴右子陣列 + // 合併階段 + merge(nums, left, mid, right); + } + ``` + +=== "Dart" + + ```dart title="merge_sort.dart" + /* 合併左子陣列和右子陣列 */ + void merge(List nums, int left, int mid, int right) { + // 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right] + // 建立一個臨時陣列 tmp ,用於存放合併後的結果 + List tmp = List.filled(right - left + 1, 0); + // 初始化左子陣列和右子陣列的起始索引 + int i = left, j = mid + 1, k = 0; + // 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中 + while (i <= mid && j <= right) { + if (nums[i] <= nums[j]) + tmp[k++] = nums[i++]; + else + tmp[k++] = nums[j++]; + } + // 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中 + while (i <= mid) { + tmp[k++] = nums[i++]; + } + while (j <= right) { + tmp[k++] = nums[j++]; + } + // 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間 + for (k = 0; k < tmp.length; k++) { + nums[left + k] = tmp[k]; + } + } + + /* 合併排序 */ + void mergeSort(List nums, int left, int right) { + // 終止條件 + if (left >= right) return; // 當子陣列長度為 1 時終止遞迴 + // 劃分階段 + int mid = (left + right) ~/ 2; // 計算中點 + mergeSort(nums, left, mid); // 遞迴左子陣列 + mergeSort(nums, mid + 1, right); // 遞迴右子陣列 + // 合併階段 + merge(nums, left, mid, right); + } + ``` + +=== "Rust" + + ```rust title="merge_sort.rs" + /* 合併左子陣列和右子陣列 */ + fn merge(nums: &mut [i32], left: usize, mid: usize, right: usize) { + // 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right] + // 建立一個臨時陣列 tmp ,用於存放合併後的結果 + let tmp_size = right - left + 1; + let mut tmp = vec![0; tmp_size]; + // 初始化左子陣列和右子陣列的起始索引 + let (mut i, mut j, mut k) = (left, mid + 1, 0); + // 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中 + while i <= mid && j <= right { + if nums[i] <= nums[j] { + tmp[k] = nums[i]; + i += 1; + } else { + tmp[k] = nums[j]; + j += 1; + } + k += 1; + } + // 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中 + while i <= mid { + tmp[k] = nums[i]; + k += 1; + i += 1; + } + while j <= right { + tmp[k] = nums[j]; + k += 1; + j += 1; + } + // 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間 + for k in 0..tmp_size { + nums[left + k] = tmp[k]; + } + } + + /* 合併排序 */ + fn merge_sort(nums: &mut [i32], left: usize, right: usize) { + // 終止條件 + if left >= right { + return; // 當子陣列長度為 1 時終止遞迴 + } + + // 劃分階段 + let mid = (left + right) / 2; // 計算中點 + merge_sort(nums, left, mid); // 遞迴左子陣列 + merge_sort(nums, mid + 1, right); // 遞迴右子陣列 + + // 合併階段 + merge(nums, left, mid, right); + } + ``` + +=== "C" + + ```c title="merge_sort.c" + /* 合併左子陣列和右子陣列 */ + void merge(int *nums, int left, int mid, int right) { + // 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right] + // 建立一個臨時陣列 tmp ,用於存放合併後的結果 + int tmpSize = right - left + 1; + int *tmp = (int *)malloc(tmpSize * sizeof(int)); + // 初始化左子陣列和右子陣列的起始索引 + int i = left, j = mid + 1, k = 0; + // 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中 + while (i <= mid && j <= right) { + if (nums[i] <= nums[j]) { + tmp[k++] = nums[i++]; + } else { + tmp[k++] = nums[j++]; + } + } + // 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中 + while (i <= mid) { + tmp[k++] = nums[i++]; + } + while (j <= right) { + tmp[k++] = nums[j++]; + } + // 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間 + for (k = 0; k < tmpSize; ++k) { + nums[left + k] = tmp[k]; + } + // 釋放記憶體 + free(tmp); + } + + /* 合併排序 */ + void mergeSort(int *nums, int left, int right) { + // 終止條件 + if (left >= right) + return; // 當子陣列長度為 1 時終止遞迴 + // 劃分階段 + int mid = (left + right) / 2; // 計算中點 + mergeSort(nums, left, mid); // 遞迴左子陣列 + mergeSort(nums, mid + 1, right); // 遞迴右子陣列 + // 合併階段 + merge(nums, left, mid, right); + } + ``` + +=== "Kotlin" + + ```kotlin title="merge_sort.kt" + /* 合併左子陣列和右子陣列 */ + fun merge(nums: IntArray, left: Int, mid: Int, right: Int) { + // 左子陣列區間為 [left, mid], 右子陣列區間為 [mid+1, right] + // 建立一個臨時陣列 tmp ,用於存放合併後的結果 + val tmp = IntArray(right - left + 1) + // 初始化左子陣列和右子陣列的起始索引 + var i = left + var j = mid + 1 + var k = 0 + // 當左右子陣列都還有元素時,進行比較並將較小的元素複製到臨時陣列中 + while (i <= mid && j <= right) { + if (nums[i] <= nums[j]) tmp[k++] = nums[i++] + else tmp[k++] = nums[j++] + } + // 將左子陣列和右子陣列的剩餘元素複製到臨時陣列中 + while (i <= mid) { + tmp[k++] = nums[i++] + } + while (j <= right) { + tmp[k++] = nums[j++] + } + // 將臨時陣列 tmp 中的元素複製回原陣列 nums 的對應區間 + for (l in tmp.indices) { + nums[left + l] = tmp[l] + } + } + + /* 合併排序 */ + fun mergeSort(nums: IntArray, left: Int, right: Int) { + // 終止條件 + if (left >= right) return // 當子陣列長度為 1 時終止遞迴 + // 劃分階段 + val mid = (left + right) / 2 // 計算中點 + mergeSort(nums, left, mid) // 遞迴左子陣列 + mergeSort(nums, mid + 1, right) // 遞迴右子陣列 + // 合併階段 + merge(nums, left, mid, right) + } + ``` + +=== "Ruby" + + ```ruby title="merge_sort.rb" + [class]{}-[func]{merge} + + [class]{}-[func]{merge_sort} + ``` + +=== "Zig" + + ```zig title="merge_sort.zig" + // 合併左子陣列和右子陣列 + // 左子陣列區間 [left, mid] + // 右子陣列區間 [mid + 1, right] + fn merge(nums: []i32, left: usize, mid: usize, right: usize) !void { + // 初始化輔助陣列 + var mem_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer mem_arena.deinit(); + const mem_allocator = mem_arena.allocator(); + var tmp = try mem_allocator.alloc(i32, right + 1 - left); + std.mem.copy(i32, tmp, nums[left..right+1]); + // 左子陣列的起始索引和結束索引 + var leftStart = left - left; + var leftEnd = mid - left; + // 右子陣列的起始索引和結束索引 + var rightStart = mid + 1 - left; + var rightEnd = right - left; + // i, j 分別指向左子陣列、右子陣列的首元素 + var i = leftStart; + var j = rightStart; + // 透過覆蓋原陣列 nums 來合併左子陣列和右子陣列 + var k = left; + while (k <= right) : (k += 1) { + // 若“左子陣列已全部合併完”,則選取右子陣列元素,並且 j++ + if (i > leftEnd) { + nums[k] = tmp[j]; + j += 1; + // 否則,若“右子陣列已全部合併完”或“左子陣列元素 <= 右子陣列元素”,則選取左子陣列元素,並且 i++ + } else if (j > rightEnd or tmp[i] <= tmp[j]) { + nums[k] = tmp[i]; + i += 1; + // 否則,若“左右子陣列都未全部合併完”且“左子陣列元素 > 右子陣列元素”,則選取右子陣列元素,並且 j++ + } else { + nums[k] = tmp[j]; + j += 1; + } + } + } + + // 合併排序 + fn mergeSort(nums: []i32, left: usize, right: usize) !void { + // 終止條件 + if (left >= right) return; // 當子陣列長度為 1 時終止遞迴 + // 劃分階段 + var mid = (left + right) / 2; // 計算中點 + try mergeSort(nums, left, mid); // 遞迴左子陣列 + try mergeSort(nums, mid + 1, right); // 遞迴右子陣列 + // 合併階段 + try merge(nums, left, mid, right); + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 11.6.2   演算法特性 + +- **時間複雜度為 $O(n \log n)$、非自適應排序**:劃分產生高度為 $\log n$ 的遞迴樹,每層合併的總操作數量為 $n$ ,因此總體時間複雜度為 $O(n \log n)$ 。 +- **空間複雜度為 $O(n)$、非原地排序**:遞迴深度為 $\log n$ ,使用 $O(\log n)$ 大小的堆疊幀空間。合併操作需要藉助輔助陣列實現,使用 $O(n)$ 大小的額外空間。 +- **穩定排序**:在合併過程中,相等元素的次序保持不變。 + +## 11.6.3   鏈結串列排序 + +對於鏈結串列,合併排序相較於其他排序演算法具有顯著優勢,**可以將鏈結串列排序任務的空間複雜度最佳化至 $O(1)$** 。 + +- **劃分階段**:可以使用“迭代”替代“遞迴”來實現鏈結串列劃分工作,從而省去遞迴使用的堆疊幀空間。 +- **合併階段**:在鏈結串列中,節點增刪操作僅需改變引用(指標)即可實現,因此合併階段(將兩個短有序鏈結串列合併為一個長有序鏈結串列)無須建立額外鏈結串列。 + +具體實現細節比較複雜,有興趣的讀者可以查閱相關資料進行學習。 diff --git a/zh-Hant/docs/chapter_sorting/quick_sort.md b/zh-Hant/docs/chapter_sorting/quick_sort.md new file mode 100755 index 000000000..05c2178d2 --- /dev/null +++ b/zh-Hant/docs/chapter_sorting/quick_sort.md @@ -0,0 +1,1419 @@ +--- +comments: true +--- + +# 11.5   快速排序 + +快速排序(quick sort)是一種基於分治策略的排序演算法,執行高效,應用廣泛。 + +快速排序的核心操作是“哨兵劃分”,其目標是:選擇陣列中的某個元素作為“基準數”,將所有小於基準數的元素移到其左側,而大於基準數的元素移到其右側。具體來說,哨兵劃分的流程如圖 11-8 所示。 + +1. 選取陣列最左端元素作為基準數,初始化兩個指標 `i` 和 `j` 分別指向陣列的兩端。 +2. 設定一個迴圈,在每輪中使用 `i`(`j`)分別尋找第一個比基準數大(小)的元素,然後交換這兩個元素。 +3. 迴圈執行步驟 `2.` ,直到 `i` 和 `j` 相遇時停止,最後將基準數交換至兩個子陣列的分界線。 + +=== "<1>" + ![哨兵劃分步驟](quick_sort.assets/pivot_division_step1.png){ class="animation-figure" } + +=== "<2>" + ![pivot_division_step2](quick_sort.assets/pivot_division_step2.png){ class="animation-figure" } + +=== "<3>" + ![pivot_division_step3](quick_sort.assets/pivot_division_step3.png){ class="animation-figure" } + +=== "<4>" + ![pivot_division_step4](quick_sort.assets/pivot_division_step4.png){ class="animation-figure" } + +=== "<5>" + ![pivot_division_step5](quick_sort.assets/pivot_division_step5.png){ class="animation-figure" } + +=== "<6>" + ![pivot_division_step6](quick_sort.assets/pivot_division_step6.png){ class="animation-figure" } + +=== "<7>" + ![pivot_division_step7](quick_sort.assets/pivot_division_step7.png){ class="animation-figure" } + +=== "<8>" + ![pivot_division_step8](quick_sort.assets/pivot_division_step8.png){ class="animation-figure" } + +=== "<9>" + ![pivot_division_step9](quick_sort.assets/pivot_division_step9.png){ class="animation-figure" } + +

圖 11-8   哨兵劃分步驟

+ +哨兵劃分完成後,原陣列被劃分成三部分:左子陣列、基準數、右子陣列,且滿足“左子陣列任意元素 $\leq$ 基準數 $\leq$ 右子陣列任意元素”。因此,我們接下來只需對這兩個子陣列進行排序。 + +!!! note "快速排序的分治策略" + + 哨兵劃分的實質是將一個較長陣列的排序問題簡化為兩個較短陣列的排序問題。 + +=== "Python" + + ```python title="quick_sort.py" + def partition(self, nums: list[int], left: int, right: int) -> int: + """哨兵劃分""" + # 以 nums[left] 為基準數 + i, j = left, right + while i < j: + while i < j and nums[j] >= nums[left]: + j -= 1 # 從右向左找首個小於基準數的元素 + while i < j and nums[i] <= nums[left]: + i += 1 # 從左向右找首個大於基準數的元素 + # 元素交換 + nums[i], nums[j] = nums[j], nums[i] + # 將基準數交換至兩子陣列的分界線 + nums[i], nums[left] = nums[left], nums[i] + return i # 返回基準數的索引 + ``` + +=== "C++" + + ```cpp title="quick_sort.cpp" + /* 元素交換 */ + void swap(vector &nums, int i, int j) { + int tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } + + /* 哨兵劃分 */ + int partition(vector &nums, int left, int right) { + // 以 nums[left] 為基準數 + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) + j--; // 從右向左找首個小於基準數的元素 + while (i < j && nums[i] <= nums[left]) + i++; // 從左向右找首個大於基準數的元素 + swap(nums, i, j); // 交換這兩個元素 + } + swap(nums, i, left); // 將基準數交換至兩子陣列的分界線 + return i; // 返回基準數的索引 + } + ``` + +=== "Java" + + ```java title="quick_sort.java" + /* 元素交換 */ + void swap(int[] nums, int i, int j) { + int tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } + + /* 哨兵劃分 */ + int partition(int[] nums, int left, int right) { + // 以 nums[left] 為基準數 + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) + j--; // 從右向左找首個小於基準數的元素 + while (i < j && nums[i] <= nums[left]) + i++; // 從左向右找首個大於基準數的元素 + swap(nums, i, j); // 交換這兩個元素 + } + swap(nums, i, left); // 將基準數交換至兩子陣列的分界線 + return i; // 返回基準數的索引 + } + ``` + +=== "C#" + + ```csharp title="quick_sort.cs" + /* 元素交換 */ + void Swap(int[] nums, int i, int j) { + (nums[j], nums[i]) = (nums[i], nums[j]); + } + + /* 哨兵劃分 */ + int Partition(int[] nums, int left, int right) { + // 以 nums[left] 為基準數 + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) + j--; // 從右向左找首個小於基準數的元素 + while (i < j && nums[i] <= nums[left]) + i++; // 從左向右找首個大於基準數的元素 + Swap(nums, i, j); // 交換這兩個元素 + } + Swap(nums, i, left); // 將基準數交換至兩子陣列的分界線 + return i; // 返回基準數的索引 + } + ``` + +=== "Go" + + ```go title="quick_sort.go" + /* 哨兵劃分 */ + func (q *quickSort) partition(nums []int, left, right int) int { + // 以 nums[left] 為基準數 + i, j := left, right + for i < j { + for i < j && nums[j] >= nums[left] { + j-- // 從右向左找首個小於基準數的元素 + } + for i < j && nums[i] <= nums[left] { + i++ // 從左向右找首個大於基準數的元素 + } + // 元素交換 + nums[i], nums[j] = nums[j], nums[i] + } + // 將基準數交換至兩子陣列的分界線 + nums[i], nums[left] = nums[left], nums[i] + return i // 返回基準數的索引 + } + ``` + +=== "Swift" + + ```swift title="quick_sort.swift" + /* 哨兵劃分 */ + func partition(nums: inout [Int], left: Int, right: Int) -> Int { + // 以 nums[left] 為基準數 + var i = left + var j = right + while i < j { + while i < j, nums[j] >= nums[left] { + j -= 1 // 從右向左找首個小於基準數的元素 + } + while i < j, nums[i] <= nums[left] { + i += 1 // 從左向右找首個大於基準數的元素 + } + nums.swapAt(i, j) // 交換這兩個元素 + } + nums.swapAt(i, left) // 將基準數交換至兩子陣列的分界線 + return i // 返回基準數的索引 + } + ``` + +=== "JS" + + ```javascript title="quick_sort.js" + /* 元素交換 */ + swap(nums, i, j) { + let tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } + + /* 哨兵劃分 */ + partition(nums, left, right) { + // 以 nums[left] 為基準數 + let i = left, + j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) { + j -= 1; // 從右向左找首個小於基準數的元素 + } + while (i < j && nums[i] <= nums[left]) { + i += 1; // 從左向右找首個大於基準數的元素 + } + // 元素交換 + this.swap(nums, i, j); // 交換這兩個元素 + } + this.swap(nums, i, left); // 將基準數交換至兩子陣列的分界線 + return i; // 返回基準數的索引 + } + ``` + +=== "TS" + + ```typescript title="quick_sort.ts" + /* 元素交換 */ + swap(nums: number[], i: number, j: number): void { + let tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } + + /* 哨兵劃分 */ + partition(nums: number[], left: number, right: number): number { + // 以 nums[left] 為基準數 + let i = left, + j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) { + j -= 1; // 從右向左找首個小於基準數的元素 + } + while (i < j && nums[i] <= nums[left]) { + i += 1; // 從左向右找首個大於基準數的元素 + } + // 元素交換 + this.swap(nums, i, j); // 交換這兩個元素 + } + this.swap(nums, i, left); // 將基準數交換至兩子陣列的分界線 + return i; // 返回基準數的索引 + } + ``` + +=== "Dart" + + ```dart title="quick_sort.dart" + /* 元素交換 */ + void _swap(List nums, int i, int j) { + int tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } + + /* 哨兵劃分 */ + int _partition(List nums, int left, int right) { + // 以 nums[left] 為基準數 + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) j--; // 從右向左找首個小於基準數的元素 + while (i < j && nums[i] <= nums[left]) i++; // 從左向右找首個大於基準數的元素 + _swap(nums, i, j); // 交換這兩個元素 + } + _swap(nums, i, left); // 將基準數交換至兩子陣列的分界線 + return i; // 返回基準數的索引 + } + ``` + +=== "Rust" + + ```rust title="quick_sort.rs" + /* 哨兵劃分 */ + fn partition(nums: &mut [i32], left: usize, right: usize) -> usize { + // 以 nums[left] 為基準數 + let (mut i, mut j) = (left, right); + while i < j { + while i < j && nums[j] >= nums[left] { + j -= 1; // 從右向左找首個小於基準數的元素 + } + while i < j && nums[i] <= nums[left] { + i += 1; // 從左向右找首個大於基準數的元素 + } + nums.swap(i, j); // 交換這兩個元素 + } + nums.swap(i, left); // 將基準數交換至兩子陣列的分界線 + i // 返回基準數的索引 + } + ``` + +=== "C" + + ```c title="quick_sort.c" + /* 元素交換 */ + void swap(int nums[], int i, int j) { + int tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } + + /* 快速排序類別 */ + // 快速排序類別-哨兵劃分 + int partition(int nums[], int left, int right) { + // 以 nums[left] 為基準數 + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) { + // 從右向左找首個小於基準數的元素 + j--; + } + while (i < j && nums[i] <= nums[left]) { + // 從左向右找首個大於基準數的元素 + i++; + } + // 交換這兩個元素 + swap(nums, i, j); + } + // 將基準數交換至兩子陣列的分界線 + swap(nums, i, left); + // 返回基準數的索引 + return i; + } + ``` + +=== "Kotlin" + + ```kotlin title="quick_sort.kt" + /* 元素交換 */ + fun swap(nums: IntArray, i: Int, j: Int) { + nums[i] = nums[j].also { nums[j] = nums[i] } + } + + /* 哨兵劃分 */ + fun partition(nums: IntArray, left: Int, right: Int): Int { + // 以 nums[left] 為基準數 + var i = left + var j = right + while (i < j) { + while (i < j && nums[j] >= nums[left]) + j-- // 從右向左找首個小於基準數的元素 + while (i < j && nums[i] <= nums[left]) + i++ // 從左向右找首個大於基準數的元素 + swap(nums, i, j) // 交換這兩個元素 + } + swap(nums, i, left) // 將基準數交換至兩子陣列的分界線 + return i // 返回基準數的索引 + } + ``` + +=== "Ruby" + + ```ruby title="quick_sort.rb" + [class]{QuickSort}-[func]{partition} + ``` + +=== "Zig" + + ```zig title="quick_sort.zig" + // 元素交換 + fn swap(nums: []i32, i: usize, j: usize) void { + var tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } + + // 哨兵劃分 + fn partition(nums: []i32, left: usize, right: usize) usize { + // 以 nums[left] 為基準數 + var i = left; + var j = right; + while (i < j) { + while (i < j and nums[j] >= nums[left]) j -= 1; // 從右向左找首個小於基準數的元素 + while (i < j and nums[i] <= nums[left]) i += 1; // 從左向右找首個大於基準數的元素 + swap(nums, i, j); // 交換這兩個元素 + } + swap(nums, i, left); // 將基準數交換至兩子陣列的分界線 + return i; // 返回基準數的索引 + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 11.5.1   演算法流程 + +快速排序的整體流程如圖 11-9 所示。 + +1. 首先,對原陣列執行一次“哨兵劃分”,得到未排序的左子陣列和右子陣列。 +2. 然後,對左子陣列和右子陣列分別遞迴執行“哨兵劃分”。 +3. 持續遞迴,直至子陣列長度為 1 時終止,從而完成整個陣列的排序。 + +![快速排序流程](quick_sort.assets/quick_sort_overview.png){ class="animation-figure" } + +

圖 11-9   快速排序流程

+ +=== "Python" + + ```python title="quick_sort.py" + def quick_sort(self, nums: list[int], left: int, right: int): + """快速排序""" + # 子陣列長度為 1 時終止遞迴 + if left >= right: + return + # 哨兵劃分 + pivot = self.partition(nums, left, right) + # 遞迴左子陣列、右子陣列 + self.quick_sort(nums, left, pivot - 1) + self.quick_sort(nums, pivot + 1, right) + ``` + +=== "C++" + + ```cpp title="quick_sort.cpp" + /* 快速排序 */ + void quickSort(vector &nums, int left, int right) { + // 子陣列長度為 1 時終止遞迴 + if (left >= right) + return; + // 哨兵劃分 + int pivot = partition(nums, left, right); + // 遞迴左子陣列、右子陣列 + quickSort(nums, left, pivot - 1); + quickSort(nums, pivot + 1, right); + } + ``` + +=== "Java" + + ```java title="quick_sort.java" + /* 快速排序 */ + void quickSort(int[] nums, int left, int right) { + // 子陣列長度為 1 時終止遞迴 + if (left >= right) + return; + // 哨兵劃分 + int pivot = partition(nums, left, right); + // 遞迴左子陣列、右子陣列 + quickSort(nums, left, pivot - 1); + quickSort(nums, pivot + 1, right); + } + ``` + +=== "C#" + + ```csharp title="quick_sort.cs" + /* 快速排序 */ + void QuickSort(int[] nums, int left, int right) { + // 子陣列長度為 1 時終止遞迴 + if (left >= right) + return; + // 哨兵劃分 + int pivot = Partition(nums, left, right); + // 遞迴左子陣列、右子陣列 + QuickSort(nums, left, pivot - 1); + QuickSort(nums, pivot + 1, right); + } + ``` + +=== "Go" + + ```go title="quick_sort.go" + /* 快速排序 */ + func (q *quickSort) quickSort(nums []int, left, right int) { + // 子陣列長度為 1 時終止遞迴 + if left >= right { + return + } + // 哨兵劃分 + pivot := q.partition(nums, left, right) + // 遞迴左子陣列、右子陣列 + q.quickSort(nums, left, pivot-1) + q.quickSort(nums, pivot+1, right) + } + ``` + +=== "Swift" + + ```swift title="quick_sort.swift" + /* 快速排序 */ + func quickSort(nums: inout [Int], left: Int, right: Int) { + // 子陣列長度為 1 時終止遞迴 + if left >= right { + return + } + // 哨兵劃分 + let pivot = partition(nums: &nums, left: left, right: right) + // 遞迴左子陣列、右子陣列 + quickSort(nums: &nums, left: left, right: pivot - 1) + quickSort(nums: &nums, left: pivot + 1, right: right) + } + ``` + +=== "JS" + + ```javascript title="quick_sort.js" + /* 快速排序 */ + quickSort(nums, left, right) { + // 子陣列長度為 1 時終止遞迴 + if (left >= right) return; + // 哨兵劃分 + const pivot = this.partition(nums, left, right); + // 遞迴左子陣列、右子陣列 + this.quickSort(nums, left, pivot - 1); + this.quickSort(nums, pivot + 1, right); + } + ``` + +=== "TS" + + ```typescript title="quick_sort.ts" + /* 快速排序 */ + quickSort(nums: number[], left: number, right: number): void { + // 子陣列長度為 1 時終止遞迴 + if (left >= right) { + return; + } + // 哨兵劃分 + const pivot = this.partition(nums, left, right); + // 遞迴左子陣列、右子陣列 + this.quickSort(nums, left, pivot - 1); + this.quickSort(nums, pivot + 1, right); + } + ``` + +=== "Dart" + + ```dart title="quick_sort.dart" + /* 快速排序 */ + void quickSort(List nums, int left, int right) { + // 子陣列長度為 1 時終止遞迴 + if (left >= right) return; + // 哨兵劃分 + int pivot = _partition(nums, left, right); + // 遞迴左子陣列、右子陣列 + quickSort(nums, left, pivot - 1); + quickSort(nums, pivot + 1, right); + } + ``` + +=== "Rust" + + ```rust title="quick_sort.rs" + /* 快速排序 */ + pub fn quick_sort(left: i32, right: i32, nums: &mut [i32]) { + // 子陣列長度為 1 時終止遞迴 + if left >= right { + return; + } + // 哨兵劃分 + let pivot = Self::partition(nums, left as usize, right as usize) as i32; + // 遞迴左子陣列、右子陣列 + Self::quick_sort(left, pivot - 1, nums); + Self::quick_sort(pivot + 1, right, nums); + } + ``` + +=== "C" + + ```c title="quick_sort.c" + /* 快速排序類別 */ + // 快速排序類別-哨兵劃分 + int partition(int nums[], int left, int right) { + // 以 nums[left] 為基準數 + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) { + // 從右向左找首個小於基準數的元素 + j--; + } + while (i < j && nums[i] <= nums[left]) { + // 從左向右找首個大於基準數的元素 + i++; + } + // 交換這兩個元素 + swap(nums, i, j); + } + // 將基準數交換至兩子陣列的分界線 + swap(nums, i, left); + // 返回基準數的索引 + return i; + } + + // 快速排序類別-快速排序 + void quickSort(int nums[], int left, int right) { + // 子陣列長度為 1 時終止遞迴 + if (left >= right) { + return; + } + // 哨兵劃分 + int pivot = partition(nums, left, right); + // 遞迴左子陣列、右子陣列 + quickSort(nums, left, pivot - 1); + quickSort(nums, pivot + 1, right); + } + ``` + +=== "Kotlin" + + ```kotlin title="quick_sort.kt" + /* 快速排序 */ + fun quickSort(nums: IntArray, left: Int, right: Int) { + // 子陣列長度為 1 時終止遞迴 + if (left >= right) return + // 哨兵劃分 + val pivot = partition(nums, left, right) + // 遞迴左子陣列、右子陣列 + quickSort(nums, left, pivot - 1) + quickSort(nums, pivot + 1, right) + } + ``` + +=== "Ruby" + + ```ruby title="quick_sort.rb" + [class]{QuickSort}-[func]{quick_sort} + ``` + +=== "Zig" + + ```zig title="quick_sort.zig" + // 快速排序 + fn quickSort(nums: []i32, left: usize, right: usize) void { + // 子陣列長度為 1 時終止遞迴 + if (left >= right) return; + // 哨兵劃分 + var pivot = partition(nums, left, right); + // 遞迴左子陣列、右子陣列 + quickSort(nums, left, pivot - 1); + quickSort(nums, pivot + 1, right); + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 11.5.2   演算法特性 + +- **時間複雜度為 $O(n \log n)$、自適應排序**:在平均情況下,哨兵劃分的遞迴層數為 $\log n$ ,每層中的總迴圈數為 $n$ ,總體使用 $O(n \log n)$ 時間。在最差情況下,每輪哨兵劃分操作都將長度為 $n$ 的陣列劃分為長度為 $0$ 和 $n - 1$ 的兩個子陣列,此時遞迴層數達到 $n$ ,每層中的迴圈數為 $n$ ,總體使用 $O(n^2)$ 時間。 +- **空間複雜度為 $O(n)$、原地排序**:在輸入陣列完全倒序的情況下,達到最差遞迴深度 $n$ ,使用 $O(n)$ 堆疊幀空間。排序操作是在原陣列上進行的,未藉助額外陣列。 +- **非穩定排序**:在哨兵劃分的最後一步,基準數可能會被交換至相等元素的右側。 + +## 11.5.3   快速排序為什麼快 + +從名稱上就能看出,快速排序在效率方面應該具有一定的優勢。儘管快速排序的平均時間複雜度與“合併排序”和“堆積排序”相同,但通常快速排序的效率更高,主要有以下原因。 + +- **出現最差情況的機率很低**:雖然快速排序的最差時間複雜度為 $O(n^2)$ ,沒有合併排序穩定,但在絕大多數情況下,快速排序能在 $O(n \log n)$ 的時間複雜度下執行。 +- **快取使用效率高**:在執行哨兵劃分操作時,系統可將整個子陣列載入到快取,因此訪問元素的效率較高。而像“堆積排序”這類演算法需要跳躍式訪問元素,從而缺乏這一特性。 +- **複雜度的常數係數小**:在上述三種演算法中,快速排序的比較、賦值、交換等操作的總數量最少。這與“插入排序”比“泡沫排序”更快的原因類似。 + +## 11.5.4   基準數最佳化 + +**快速排序在某些輸入下的時間效率可能降低**。舉一個極端例子,假設輸入陣列是完全倒序的,由於我們選擇最左端元素作為基準數,那麼在哨兵劃分完成後,基準數被交換至陣列最右端,導致左子陣列長度為 $n - 1$、右子陣列長度為 $0$ 。如此遞迴下去,每輪哨兵劃分後都有一個子陣列的長度為 $0$ ,分治策略失效,快速排序退化為“泡沫排序”的近似形式。 + +為了儘量避免這種情況發生,**我們可以最佳化哨兵劃分中的基準數的選取策略**。例如,我們可以隨機選取一個元素作為基準數。然而,如果運氣不佳,每次都選到不理想的基準數,效率仍然不盡如人意。 + +需要注意的是,程式語言通常生成的是“偽隨機數”。如果我們針對偽隨機數序列構建一個特定的測試樣例,那麼快速排序的效率仍然可能劣化。 + +為了進一步改進,我們可以在陣列中選取三個候選元素(通常為陣列的首、尾、中點元素),**並將這三個候選元素的中位數作為基準數**。這樣一來,基準數“既不太小也不太大”的機率將大幅提升。當然,我們還可以選取更多候選元素,以進一步提高演算法的穩健性。採用這種方法後,時間複雜度劣化至 $O(n^2)$ 的機率大大降低。 + +示例程式碼如下: + +=== "Python" + + ```python title="quick_sort.py" + def median_three(self, nums: list[int], left: int, mid: int, right: int) -> int: + """選取三個候選元素的中位數""" + l, m, r = nums[left], nums[mid], nums[right] + if (l <= m <= r) or (r <= m <= l): + return mid # m 在 l 和 r 之間 + if (m <= l <= r) or (r <= l <= m): + return left # l 在 m 和 r 之間 + return right + + def partition(self, nums: list[int], left: int, right: int) -> int: + """哨兵劃分(三數取中值)""" + # 以 nums[left] 為基準數 + med = self.median_three(nums, left, (left + right) // 2, right) + # 將中位數交換至陣列最左端 + nums[left], nums[med] = nums[med], nums[left] + # 以 nums[left] 為基準數 + i, j = left, right + while i < j: + while i < j and nums[j] >= nums[left]: + j -= 1 # 從右向左找首個小於基準數的元素 + while i < j and nums[i] <= nums[left]: + i += 1 # 從左向右找首個大於基準數的元素 + # 元素交換 + nums[i], nums[j] = nums[j], nums[i] + # 將基準數交換至兩子陣列的分界線 + nums[i], nums[left] = nums[left], nums[i] + return i # 返回基準數的索引 + ``` + +=== "C++" + + ```cpp title="quick_sort.cpp" + /* 選取三個候選元素的中位數 */ + int medianThree(vector &nums, int left, int mid, int right) { + int l = nums[left], m = nums[mid], r = nums[right]; + if ((l <= m && m <= r) || (r <= m && m <= l)) + return mid; // m 在 l 和 r 之間 + if ((m <= l && l <= r) || (r <= l && l <= m)) + return left; // l 在 m 和 r 之間 + return right; + } + + /* 哨兵劃分(三數取中值) */ + int partition(vector &nums, int left, int right) { + // 選取三個候選元素的中位數 + int med = medianThree(nums, left, (left + right) / 2, right); + // 將中位數交換至陣列最左端 + swap(nums, left, med); + // 以 nums[left] 為基準數 + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) + j--; // 從右向左找首個小於基準數的元素 + while (i < j && nums[i] <= nums[left]) + i++; // 從左向右找首個大於基準數的元素 + swap(nums, i, j); // 交換這兩個元素 + } + swap(nums, i, left); // 將基準數交換至兩子陣列的分界線 + return i; // 返回基準數的索引 + } + ``` + +=== "Java" + + ```java title="quick_sort.java" + /* 選取三個候選元素的中位數 */ + int medianThree(int[] nums, int left, int mid, int right) { + int l = nums[left], m = nums[mid], r = nums[right]; + if ((l <= m && m <= r) || (r <= m && m <= l)) + return mid; // m 在 l 和 r 之間 + if ((m <= l && l <= r) || (r <= l && l <= m)) + return left; // l 在 m 和 r 之間 + return right; + } + + /* 哨兵劃分(三數取中值) */ + int partition(int[] nums, int left, int right) { + // 選取三個候選元素的中位數 + int med = medianThree(nums, left, (left + right) / 2, right); + // 將中位數交換至陣列最左端 + swap(nums, left, med); + // 以 nums[left] 為基準數 + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) + j--; // 從右向左找首個小於基準數的元素 + while (i < j && nums[i] <= nums[left]) + i++; // 從左向右找首個大於基準數的元素 + swap(nums, i, j); // 交換這兩個元素 + } + swap(nums, i, left); // 將基準數交換至兩子陣列的分界線 + return i; // 返回基準數的索引 + } + ``` + +=== "C#" + + ```csharp title="quick_sort.cs" + /* 選取三個候選元素的中位數 */ + int MedianThree(int[] nums, int left, int mid, int right) { + int l = nums[left], m = nums[mid], r = nums[right]; + if ((l <= m && m <= r) || (r <= m && m <= l)) + return mid; // m 在 l 和 r 之間 + if ((m <= l && l <= r) || (r <= l && l <= m)) + return left; // l 在 m 和 r 之間 + return right; + } + + /* 哨兵劃分(三數取中值) */ + int Partition(int[] nums, int left, int right) { + // 選取三個候選元素的中位數 + int med = MedianThree(nums, left, (left + right) / 2, right); + // 將中位數交換至陣列最左端 + Swap(nums, left, med); + // 以 nums[left] 為基準數 + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) + j--; // 從右向左找首個小於基準數的元素 + while (i < j && nums[i] <= nums[left]) + i++; // 從左向右找首個大於基準數的元素 + Swap(nums, i, j); // 交換這兩個元素 + } + Swap(nums, i, left); // 將基準數交換至兩子陣列的分界線 + return i; // 返回基準數的索引 + } + ``` + +=== "Go" + + ```go title="quick_sort.go" + /* 選取三個候選元素的中位數 */ + func (q *quickSortMedian) medianThree(nums []int, left, mid, right int) int { + l, m, r := nums[left], nums[mid], nums[right] + if (l <= m && m <= r) || (r <= m && m <= l) { + return mid // m 在 l 和 r 之間 + } + if (m <= l && l <= r) || (r <= l && l <= m) { + return left // l 在 m 和 r 之間 + } + return right + } + + /* 哨兵劃分(三數取中值)*/ + func (q *quickSortMedian) partition(nums []int, left, right int) int { + // 以 nums[left] 為基準數 + med := q.medianThree(nums, left, (left+right)/2, right) + // 將中位數交換至陣列最左端 + nums[left], nums[med] = nums[med], nums[left] + // 以 nums[left] 為基準數 + i, j := left, right + for i < j { + for i < j && nums[j] >= nums[left] { + j-- //從右向左找首個小於基準數的元素 + } + for i < j && nums[i] <= nums[left] { + i++ //從左向右找首個大於基準數的元素 + } + //元素交換 + nums[i], nums[j] = nums[j], nums[i] + } + //將基準數交換至兩子陣列的分界線 + nums[i], nums[left] = nums[left], nums[i] + return i //返回基準數的索引 + } + ``` + +=== "Swift" + + ```swift title="quick_sort.swift" + /* 選取三個候選元素的中位數 */ + func medianThree(nums: [Int], left: Int, mid: Int, right: Int) -> Int { + let l = nums[left] + let m = nums[mid] + let r = nums[right] + if (l <= m && m <= r) || (r <= m && m <= l) { + return mid // m 在 l 和 r 之間 + } + if (m <= l && l <= r) || (r <= l && l <= m) { + return left // l 在 m 和 r 之間 + } + return right + } + + /* 哨兵劃分(三數取中值) */ + func partitionMedian(nums: inout [Int], left: Int, right: Int) -> Int { + // 選取三個候選元素的中位數 + let med = medianThree(nums: nums, left: left, mid: (left + right) / 2, right: right) + // 將中位數交換至陣列最左端 + nums.swapAt(left, med) + return partition(nums: &nums, left: left, right: right) + } + ``` + +=== "JS" + + ```javascript title="quick_sort.js" + /* 選取三個候選元素的中位數 */ + medianThree(nums, left, mid, right) { + let l = nums[left], + m = nums[mid], + r = nums[right]; + // m 在 l 和 r 之間 + if ((l <= m && m <= r) || (r <= m && m <= l)) return mid; + // l 在 m 和 r 之間 + if ((m <= l && l <= r) || (r <= l && l <= m)) return left; + return right; + } + + /* 哨兵劃分(三數取中值) */ + partition(nums, left, right) { + // 選取三個候選元素的中位數 + let med = this.medianThree( + nums, + left, + Math.floor((left + right) / 2), + right + ); + // 將中位數交換至陣列最左端 + this.swap(nums, left, med); + // 以 nums[left] 為基準數 + let i = left, + j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) j--; // 從右向左找首個小於基準數的元素 + while (i < j && nums[i] <= nums[left]) i++; // 從左向右找首個大於基準數的元素 + this.swap(nums, i, j); // 交換這兩個元素 + } + this.swap(nums, i, left); // 將基準數交換至兩子陣列的分界線 + return i; // 返回基準數的索引 + } + ``` + +=== "TS" + + ```typescript title="quick_sort.ts" + /* 選取三個候選元素的中位數 */ + medianThree( + nums: number[], + left: number, + mid: number, + right: number + ): number { + let l = nums[left], + m = nums[mid], + r = nums[right]; + // m 在 l 和 r 之間 + if ((l <= m && m <= r) || (r <= m && m <= l)) return mid; + // l 在 m 和 r 之間 + if ((m <= l && l <= r) || (r <= l && l <= m)) return left; + return right; + } + + /* 哨兵劃分(三數取中值) */ + partition(nums: number[], left: number, right: number): number { + // 選取三個候選元素的中位數 + let med = this.medianThree( + nums, + left, + Math.floor((left + right) / 2), + right + ); + // 將中位數交換至陣列最左端 + this.swap(nums, left, med); + // 以 nums[left] 為基準數 + let i = left, + j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) { + j--; // 從右向左找首個小於基準數的元素 + } + while (i < j && nums[i] <= nums[left]) { + i++; // 從左向右找首個大於基準數的元素 + } + this.swap(nums, i, j); // 交換這兩個元素 + } + this.swap(nums, i, left); // 將基準數交換至兩子陣列的分界線 + return i; // 返回基準數的索引 + } + ``` + +=== "Dart" + + ```dart title="quick_sort.dart" + /* 選取三個候選元素的中位數 */ + int _medianThree(List nums, int left, int mid, int right) { + int l = nums[left], m = nums[mid], r = nums[right]; + if ((l <= m && m <= r) || (r <= m && m <= l)) + return mid; // m 在 l 和 r 之間 + if ((m <= l && l <= r) || (r <= l && l <= m)) + return left; // l 在 m 和 r 之間 + return right; + } + + /* 哨兵劃分(三數取中值) */ + int _partition(List nums, int left, int right) { + // 選取三個候選元素的中位數 + int med = _medianThree(nums, left, (left + right) ~/ 2, right); + // 將中位數交換至陣列最左端 + _swap(nums, left, med); + // 以 nums[left] 為基準數 + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) j--; // 從右向左找首個小於基準數的元素 + while (i < j && nums[i] <= nums[left]) i++; // 從左向右找首個大於基準數的元素 + _swap(nums, i, j); // 交換這兩個元素 + } + _swap(nums, i, left); // 將基準數交換至兩子陣列的分界線 + return i; // 返回基準數的索引 + } + ``` + +=== "Rust" + + ```rust title="quick_sort.rs" + /* 選取三個候選元素的中位數 */ + fn median_three(nums: &mut [i32], left: usize, mid: usize, right: usize) -> usize { + let (l, m, r) = (nums[left], nums[mid], nums[right]); + if (l <= m && m <= r) || (r <= m && m <= l) { + return mid; // m 在 l 和 r 之間 + } + if (m <= l && l <= r) || (r <= l && l <= m) { + return left; // l 在 m 和 r 之間 + } + right + } + + /* 哨兵劃分(三數取中值) */ + fn partition(nums: &mut [i32], left: usize, right: usize) -> usize { + // 選取三個候選元素的中位數 + let med = Self::median_three(nums, left, (left + right) / 2, right); + // 將中位數交換至陣列最左端 + nums.swap(left, med); + // 以 nums[left] 為基準數 + let (mut i, mut j) = (left, right); + while i < j { + while i < j && nums[j] >= nums[left] { + j -= 1; // 從右向左找首個小於基準數的元素 + } + while i < j && nums[i] <= nums[left] { + i += 1; // 從左向右找首個大於基準數的元素 + } + nums.swap(i, j); // 交換這兩個元素 + } + nums.swap(i, left); // 將基準數交換至兩子陣列的分界線 + i // 返回基準數的索引 + } + ``` + +=== "C" + + ```c title="quick_sort.c" + /* 快速排序類別(中位基準數最佳化) */ + // 選取三個候選元素的中位數 + int medianThree(int nums[], int left, int mid, int right) { + int l = nums[left], m = nums[mid], r = nums[right]; + if ((l <= m && m <= r) || (r <= m && m <= l)) + return mid; // m 在 l 和 r 之間 + if ((m <= l && l <= r) || (r <= l && l <= m)) + return left; // l 在 m 和 r 之間 + return right; + } + + /* 哨兵劃分(三數取中值) */ + int partitionMedian(int nums[], int left, int right) { + // 選取三個候選元素的中位數 + int med = medianThree(nums, left, (left + right) / 2, right); + // 將中位數交換至陣列最左端 + swap(nums, left, med); + // 以 nums[left] 為基準數 + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) + j--; // 從右向左找首個小於基準數的元素 + while (i < j && nums[i] <= nums[left]) + i++; // 從左向右找首個大於基準數的元素 + swap(nums, i, j); // 交換這兩個元素 + } + swap(nums, i, left); // 將基準數交換至兩子陣列的分界線 + return i; // 返回基準數的索引 + } + ``` + +=== "Kotlin" + + ```kotlin title="quick_sort.kt" + /* 選取三個候選元素的中位數 */ + fun medianThree(nums: IntArray, left: Int, mid: Int, right: Int): Int { + val l = nums[left] + val m = nums[mid] + val r = nums[right] + if ((m in l..r) || (m in r..l)) + return mid // m 在 l 和 r 之間 + if ((l in m..r) || (l in r..m)) + return left // l 在 m 和 r 之間 + return right + } + + /* 哨兵劃分 */ + fun partition(nums: IntArray, left: Int, right: Int): Int { + // 以 nums[left] 為基準數 + var i = left + var j = right + while (i < j) { + while (i < j && nums[j] >= nums[left]) + j-- // 從右向左找首個小於基準數的元素 + while (i < j && nums[i] <= nums[left]) + i++ // 從左向右找首個大於基準數的元素 + swap(nums, i, j) // 交換這兩個元素 + } + swap(nums, i, left) // 將基準數交換至兩子陣列的分界線 + return i // 返回基準數的索引 + } + ``` + +=== "Ruby" + + ```ruby title="quick_sort.rb" + [class]{QuickSortMedian}-[func]{median_three} + + [class]{QuickSortMedian}-[func]{partition} + ``` + +=== "Zig" + + ```zig title="quick_sort.zig" + // 選取三個候選元素的中位數 + fn medianThree(nums: []i32, left: usize, mid: usize, right: usize) usize { + var l = nums[left]; + var m = nums[mid]; + var r = nums[right]; + if ((l <= m && m <= r) || (r <= m && m <= l)) + return mid; // m 在 l 和 r 之間 + if ((m <= l && l <= r) || (r <= l && l <= m)) + return left; // l 在 m 和 r 之間 + return right; + } + + // 哨兵劃分(三數取中值) + fn partition(nums: []i32, left: usize, right: usize) usize { + // 選取三個候選元素的中位數 + var med = medianThree(nums, left, (left + right) / 2, right); + // 將中位數交換至陣列最左端 + swap(nums, left, med); + // 以 nums[left] 為基準數 + var i = left; + var j = right; + while (i < j) { + while (i < j and nums[j] >= nums[left]) j -= 1; // 從右向左找首個小於基準數的元素 + while (i < j and nums[i] <= nums[left]) i += 1; // 從左向右找首個大於基準數的元素 + swap(nums, i, j); // 交換這兩個元素 + } + swap(nums, i, left); // 將基準數交換至兩子陣列的分界線 + return i; // 返回基準數的索引 + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 11.5.5   尾遞迴最佳化 + +**在某些輸入下,快速排序可能佔用空間較多**。以完全有序的輸入陣列為例,設遞迴中的子陣列長度為 $m$ ,每輪哨兵劃分操作都將產生長度為 $0$ 的左子陣列和長度為 $m - 1$ 的右子陣列,這意味著每一層遞迴呼叫減少的問題規模非常小(只減少一個元素),遞迴樹的高度會達到 $n - 1$ ,此時需要佔用 $O(n)$ 大小的堆疊幀空間。 + +為了防止堆疊幀空間的累積,我們可以在每輪哨兵排序完成後,比較兩個子陣列的長度,**僅對較短的子陣列進行遞迴**。由於較短子陣列的長度不會超過 $n / 2$ ,因此這種方法能確保遞迴深度不超過 $\log n$ ,從而將最差空間複雜度最佳化至 $O(\log n)$ 。程式碼如下所示: + +=== "Python" + + ```python title="quick_sort.py" + def quick_sort(self, nums: list[int], left: int, right: int): + """快速排序(尾遞迴最佳化)""" + # 子陣列長度為 1 時終止 + while left < right: + # 哨兵劃分操作 + pivot = self.partition(nums, left, right) + # 對兩個子陣列中較短的那個執行快速排序 + if pivot - left < right - pivot: + self.quick_sort(nums, left, pivot - 1) # 遞迴排序左子陣列 + left = pivot + 1 # 剩餘未排序區間為 [pivot + 1, right] + else: + self.quick_sort(nums, pivot + 1, right) # 遞迴排序右子陣列 + right = pivot - 1 # 剩餘未排序區間為 [left, pivot - 1] + ``` + +=== "C++" + + ```cpp title="quick_sort.cpp" + /* 快速排序(尾遞迴最佳化) */ + void quickSort(vector &nums, int left, int right) { + // 子陣列長度為 1 時終止 + while (left < right) { + // 哨兵劃分操作 + int pivot = partition(nums, left, right); + // 對兩個子陣列中較短的那個執行快速排序 + if (pivot - left < right - pivot) { + quickSort(nums, left, pivot - 1); // 遞迴排序左子陣列 + left = pivot + 1; // 剩餘未排序區間為 [pivot + 1, right] + } else { + quickSort(nums, pivot + 1, right); // 遞迴排序右子陣列 + right = pivot - 1; // 剩餘未排序區間為 [left, pivot - 1] + } + } + } + ``` + +=== "Java" + + ```java title="quick_sort.java" + /* 快速排序(尾遞迴最佳化) */ + void quickSort(int[] nums, int left, int right) { + // 子陣列長度為 1 時終止 + while (left < right) { + // 哨兵劃分操作 + int pivot = partition(nums, left, right); + // 對兩個子陣列中較短的那個執行快速排序 + if (pivot - left < right - pivot) { + quickSort(nums, left, pivot - 1); // 遞迴排序左子陣列 + left = pivot + 1; // 剩餘未排序區間為 [pivot + 1, right] + } else { + quickSort(nums, pivot + 1, right); // 遞迴排序右子陣列 + right = pivot - 1; // 剩餘未排序區間為 [left, pivot - 1] + } + } + } + ``` + +=== "C#" + + ```csharp title="quick_sort.cs" + /* 快速排序(尾遞迴最佳化) */ + void QuickSort(int[] nums, int left, int right) { + // 子陣列長度為 1 時終止 + while (left < right) { + // 哨兵劃分操作 + int pivot = Partition(nums, left, right); + // 對兩個子陣列中較短的那個執行快速排序 + if (pivot - left < right - pivot) { + QuickSort(nums, left, pivot - 1); // 遞迴排序左子陣列 + left = pivot + 1; // 剩餘未排序區間為 [pivot + 1, right] + } else { + QuickSort(nums, pivot + 1, right); // 遞迴排序右子陣列 + right = pivot - 1; // 剩餘未排序區間為 [left, pivot - 1] + } + } + } + ``` + +=== "Go" + + ```go title="quick_sort.go" + /* 快速排序(尾遞迴最佳化)*/ + func (q *quickSortTailCall) quickSort(nums []int, left, right int) { + // 子陣列長度為 1 時終止 + for left < right { + // 哨兵劃分操作 + pivot := q.partition(nums, left, right) + // 對兩個子陣列中較短的那個執行快速排序 + if pivot-left < right-pivot { + q.quickSort(nums, left, pivot-1) // 遞迴排序左子陣列 + left = pivot + 1 // 剩餘未排序區間為 [pivot + 1, right] + } else { + q.quickSort(nums, pivot+1, right) // 遞迴排序右子陣列 + right = pivot - 1 // 剩餘未排序區間為 [left, pivot - 1] + } + } + } + ``` + +=== "Swift" + + ```swift title="quick_sort.swift" + /* 快速排序(尾遞迴最佳化) */ + func quickSortTailCall(nums: inout [Int], left: Int, right: Int) { + var left = left + var right = right + // 子陣列長度為 1 時終止 + while left < right { + // 哨兵劃分操作 + let pivot = partition(nums: &nums, left: left, right: right) + // 對兩個子陣列中較短的那個執行快速排序 + if (pivot - left) < (right - pivot) { + quickSortTailCall(nums: &nums, left: left, right: pivot - 1) // 遞迴排序左子陣列 + left = pivot + 1 // 剩餘未排序區間為 [pivot + 1, right] + } else { + quickSortTailCall(nums: &nums, left: pivot + 1, right: right) // 遞迴排序右子陣列 + right = pivot - 1 // 剩餘未排序區間為 [left, pivot - 1] + } + } + } + ``` + +=== "JS" + + ```javascript title="quick_sort.js" + /* 快速排序(尾遞迴最佳化) */ + quickSort(nums, left, right) { + // 子陣列長度為 1 時終止 + while (left < right) { + // 哨兵劃分操作 + let pivot = this.partition(nums, left, right); + // 對兩個子陣列中較短的那個執行快速排序 + if (pivot - left < right - pivot) { + this.quickSort(nums, left, pivot - 1); // 遞迴排序左子陣列 + left = pivot + 1; // 剩餘未排序區間為 [pivot + 1, right] + } else { + this.quickSort(nums, pivot + 1, right); // 遞迴排序右子陣列 + right = pivot - 1; // 剩餘未排序區間為 [left, pivot - 1] + } + } + } + ``` + +=== "TS" + + ```typescript title="quick_sort.ts" + /* 快速排序(尾遞迴最佳化) */ + quickSort(nums: number[], left: number, right: number): void { + // 子陣列長度為 1 時終止 + while (left < right) { + // 哨兵劃分操作 + let pivot = this.partition(nums, left, right); + // 對兩個子陣列中較短的那個執行快速排序 + if (pivot - left < right - pivot) { + this.quickSort(nums, left, pivot - 1); // 遞迴排序左子陣列 + left = pivot + 1; // 剩餘未排序區間為 [pivot + 1, right] + } else { + this.quickSort(nums, pivot + 1, right); // 遞迴排序右子陣列 + right = pivot - 1; // 剩餘未排序區間為 [left, pivot - 1] + } + } + } + ``` + +=== "Dart" + + ```dart title="quick_sort.dart" + /* 快速排序(尾遞迴最佳化) */ + void quickSort(List nums, int left, int right) { + // 子陣列長度為 1 時終止 + while (left < right) { + // 哨兵劃分操作 + int pivot = _partition(nums, left, right); + // 對兩個子陣列中較短的那個執行快速排序 + if (pivot - left < right - pivot) { + quickSort(nums, left, pivot - 1); // 遞迴排序左子陣列 + left = pivot + 1; // 剩餘未排序區間為 [pivot + 1, right] + } else { + quickSort(nums, pivot + 1, right); // 遞迴排序右子陣列 + right = pivot - 1; // 剩餘未排序區間為 [left, pivot - 1] + } + } + } + ``` + +=== "Rust" + + ```rust title="quick_sort.rs" + /* 快速排序(尾遞迴最佳化) */ + pub fn quick_sort(mut left: i32, mut right: i32, nums: &mut [i32]) { + // 子陣列長度為 1 時終止 + while left < right { + // 哨兵劃分操作 + let pivot = Self::partition(nums, left as usize, right as usize) as i32; + // 對兩個子陣列中較短的那個執行快速排序 + if pivot - left < right - pivot { + Self::quick_sort(left, pivot - 1, nums); // 遞迴排序左子陣列 + left = pivot + 1; // 剩餘未排序區間為 [pivot + 1, right] + } else { + Self::quick_sort(pivot + 1, right, nums); // 遞迴排序右子陣列 + right = pivot - 1; // 剩餘未排序區間為 [left, pivot - 1] + } + } + } + ``` + +=== "C" + + ```c title="quick_sort.c" + /* 快速排序類別(尾遞迴最佳化) */ + // 快速排序(尾遞迴最佳化) + void quickSortTailCall(int nums[], int left, int right) { + // 子陣列長度為 1 時終止 + while (left < right) { + // 哨兵劃分操作 + int pivot = partition(nums, left, right); + // 對兩個子陣列中較短的那個執行快速排序 + if (pivot - left < right - pivot) { + quickSortTailCall(nums, left, pivot - 1); // 遞迴排序左子陣列 + left = pivot + 1; // 剩餘未排序區間為 [pivot + 1, right] + } else { + quickSortTailCall(nums, pivot + 1, right); // 遞迴排序右子陣列 + right = pivot - 1; // 剩餘未排序區間為 [left, pivot - 1] + } + } + } + ``` + +=== "Kotlin" + + ```kotlin title="quick_sort.kt" + /* 快速排序 */ + fun quickSort(nums: IntArray, left: Int, right: Int) { + // 子陣列長度為 1 時終止遞迴 + if (left >= right) return + // 哨兵劃分 + val pivot = partition(nums, left, right) + // 遞迴左子陣列、右子陣列 + quickSort(nums, left, pivot - 1) + quickSort(nums, pivot + 1, right) + } + ``` + +=== "Ruby" + + ```ruby title="quick_sort.rb" + [class]{QuickSortTailCall}-[func]{quick_sort} + ``` + +=== "Zig" + + ```zig title="quick_sort.zig" + // 快速排序(尾遞迴最佳化) + fn quickSort(nums: []i32, left_: usize, right_: usize) void { + var left = left_; + var right = right_; + // 子陣列長度為 1 時終止遞迴 + while (left < right) { + // 哨兵劃分操作 + var pivot = partition(nums, left, right); + // 對兩個子陣列中較短的那個執行快速排序 + if (pivot - left < right - pivot) { + quickSort(nums, left, pivot - 1); // 遞迴排序左子陣列 + left = pivot + 1; // 剩餘未排序區間為 [pivot + 1, right] + } else { + quickSort(nums, pivot + 1, right); // 遞迴排序右子陣列 + right = pivot - 1; // 剩餘未排序區間為 [left, pivot - 1] + } + } + } + ``` + +??? pythontutor "視覺化執行" + +
+ diff --git a/zh-Hant/docs/chapter_sorting/radix_sort.md b/zh-Hant/docs/chapter_sorting/radix_sort.md new file mode 100644 index 000000000..8be8251ad --- /dev/null +++ b/zh-Hant/docs/chapter_sorting/radix_sort.md @@ -0,0 +1,764 @@ +--- +comments: true +--- + +# 11.10   基數排序 + +上一節介紹了計數排序,它適用於資料量 $n$ 較大但資料範圍 $m$ 較小的情況。假設我們需要對 $n = 10^6$ 個學號進行排序,而學號是一個 $8$ 位數字,這意味著資料範圍 $m = 10^8$ 非常大,使用計數排序需要分配大量記憶體空間,而基數排序可以避免這種情況。 + +基數排序(radix sort)的核心思想與計數排序一致,也透過統計個數來實現排序。在此基礎上,基數排序利用數字各位之間的遞進關係,依次對每一位進行排序,從而得到最終的排序結果。 + +## 11.10.1   演算法流程 + +以學號資料為例,假設數字的最低位是第 $1$ 位,最高位是第 $8$ 位,基數排序的流程如圖 11-18 所示。 + +1. 初始化位數 $k = 1$ 。 +2. 對學號的第 $k$ 位執行“計數排序”。完成後,資料會根據第 $k$ 位從小到大排序。 +3. 將 $k$ 增加 $1$ ,然後返回步驟 `2.` 繼續迭代,直到所有位都排序完成後結束。 + +![基數排序演算法流程](radix_sort.assets/radix_sort_overview.png){ class="animation-figure" } + +

圖 11-18   基數排序演算法流程

+ +下面剖析程式碼實現。對於一個 $d$ 進位制的數字 $x$ ,要獲取其第 $k$ 位 $x_k$ ,可以使用以下計算公式: + +$$ +x_k = \lfloor\frac{x}{d^{k-1}}\rfloor \bmod d +$$ + +其中 $\lfloor a \rfloor$ 表示對浮點數 $a$ 向下取整,而 $\bmod \: d$ 表示對 $d$ 取模(取餘)。對於學號資料,$d = 10$ 且 $k \in [1, 8]$ 。 + +此外,我們需要小幅改動計數排序程式碼,使之可以根據數字的第 $k$ 位進行排序: + +=== "Python" + + ```python title="radix_sort.py" + def digit(num: int, exp: int) -> int: + """獲取元素 num 的第 k 位,其中 exp = 10^(k-1)""" + # 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算 + return (num // exp) % 10 + + def counting_sort_digit(nums: list[int], exp: int): + """計數排序(根據 nums 第 k 位排序)""" + # 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列 + counter = [0] * 10 + n = len(nums) + # 統計 0~9 各數字的出現次數 + for i in range(n): + d = digit(nums[i], exp) # 獲取 nums[i] 第 k 位,記為 d + counter[d] += 1 # 統計數字 d 的出現次數 + # 求前綴和,將“出現個數”轉換為“陣列索引” + for i in range(1, 10): + counter[i] += counter[i - 1] + # 倒序走訪,根據桶內統計結果,將各元素填入 res + res = [0] * n + for i in range(n - 1, -1, -1): + d = digit(nums[i], exp) + j = counter[d] - 1 # 獲取 d 在陣列中的索引 j + res[j] = nums[i] # 將當前元素填入索引 j + counter[d] -= 1 # 將 d 的數量減 1 + # 使用結果覆蓋原陣列 nums + for i in range(n): + nums[i] = res[i] + + def radix_sort(nums: list[int]): + """基數排序""" + # 獲取陣列的最大元素,用於判斷最大位數 + m = max(nums) + # 按照從低位到高位的順序走訪 + exp = 1 + while exp <= m: + # 對陣列元素的第 k 位執行計數排序 + # k = 1 -> exp = 1 + # k = 2 -> exp = 10 + # 即 exp = 10^(k-1) + counting_sort_digit(nums, exp) + exp *= 10 + ``` + +=== "C++" + + ```cpp title="radix_sort.cpp" + /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */ + int digit(int num, int exp) { + // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算 + return (num / exp) % 10; + } + + /* 計數排序(根據 nums 第 k 位排序) */ + void countingSortDigit(vector &nums, int exp) { + // 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列 + vector counter(10, 0); + int n = nums.size(); + // 統計 0~9 各數字的出現次數 + for (int i = 0; i < n; i++) { + int d = digit(nums[i], exp); // 獲取 nums[i] 第 k 位,記為 d + counter[d]++; // 統計數字 d 的出現次數 + } + // 求前綴和,將“出現個數”轉換為“陣列索引” + for (int i = 1; i < 10; i++) { + counter[i] += counter[i - 1]; + } + // 倒序走訪,根據桶內統計結果,將各元素填入 res + vector res(n, 0); + for (int i = n - 1; i >= 0; i--) { + int d = digit(nums[i], exp); + int j = counter[d] - 1; // 獲取 d 在陣列中的索引 j + res[j] = nums[i]; // 將當前元素填入索引 j + counter[d]--; // 將 d 的數量減 1 + } + // 使用結果覆蓋原陣列 nums + for (int i = 0; i < n; i++) + nums[i] = res[i]; + } + + /* 基數排序 */ + void radixSort(vector &nums) { + // 獲取陣列的最大元素,用於判斷最大位數 + int m = *max_element(nums.begin(), nums.end()); + // 按照從低位到高位的順序走訪 + for (int exp = 1; exp <= m; exp *= 10) + // 對陣列元素的第 k 位執行計數排序 + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // 即 exp = 10^(k-1) + countingSortDigit(nums, exp); + } + ``` + +=== "Java" + + ```java title="radix_sort.java" + /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */ + int digit(int num, int exp) { + // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算 + return (num / exp) % 10; + } + + /* 計數排序(根據 nums 第 k 位排序) */ + void countingSortDigit(int[] nums, int exp) { + // 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列 + int[] counter = new int[10]; + int n = nums.length; + // 統計 0~9 各數字的出現次數 + for (int i = 0; i < n; i++) { + int d = digit(nums[i], exp); // 獲取 nums[i] 第 k 位,記為 d + counter[d]++; // 統計數字 d 的出現次數 + } + // 求前綴和,將“出現個數”轉換為“陣列索引” + for (int i = 1; i < 10; i++) { + counter[i] += counter[i - 1]; + } + // 倒序走訪,根據桶內統計結果,將各元素填入 res + int[] res = new int[n]; + for (int i = n - 1; i >= 0; i--) { + int d = digit(nums[i], exp); + int j = counter[d] - 1; // 獲取 d 在陣列中的索引 j + res[j] = nums[i]; // 將當前元素填入索引 j + counter[d]--; // 將 d 的數量減 1 + } + // 使用結果覆蓋原陣列 nums + for (int i = 0; i < n; i++) + nums[i] = res[i]; + } + + /* 基數排序 */ + void radixSort(int[] nums) { + // 獲取陣列的最大元素,用於判斷最大位數 + int m = Integer.MIN_VALUE; + for (int num : nums) + if (num > m) + m = num; + // 按照從低位到高位的順序走訪 + for (int exp = 1; exp <= m; exp *= 10) + // 對陣列元素的第 k 位執行計數排序 + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // 即 exp = 10^(k-1) + countingSortDigit(nums, exp); + } + ``` + +=== "C#" + + ```csharp title="radix_sort.cs" + /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */ + int Digit(int num, int exp) { + // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算 + return (num / exp) % 10; + } + + /* 計數排序(根據 nums 第 k 位排序) */ + void CountingSortDigit(int[] nums, int exp) { + // 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列 + int[] counter = new int[10]; + int n = nums.Length; + // 統計 0~9 各數字的出現次數 + for (int i = 0; i < n; i++) { + int d = Digit(nums[i], exp); // 獲取 nums[i] 第 k 位,記為 d + counter[d]++; // 統計數字 d 的出現次數 + } + // 求前綴和,將“出現個數”轉換為“陣列索引” + for (int i = 1; i < 10; i++) { + counter[i] += counter[i - 1]; + } + // 倒序走訪,根據桶內統計結果,將各元素填入 res + int[] res = new int[n]; + for (int i = n - 1; i >= 0; i--) { + int d = Digit(nums[i], exp); + int j = counter[d] - 1; // 獲取 d 在陣列中的索引 j + res[j] = nums[i]; // 將當前元素填入索引 j + counter[d]--; // 將 d 的數量減 1 + } + // 使用結果覆蓋原陣列 nums + for (int i = 0; i < n; i++) { + nums[i] = res[i]; + } + } + + /* 基數排序 */ + void RadixSort(int[] nums) { + // 獲取陣列的最大元素,用於判斷最大位數 + int m = int.MinValue; + foreach (int num in nums) { + if (num > m) m = num; + } + // 按照從低位到高位的順序走訪 + for (int exp = 1; exp <= m; exp *= 10) { + // 對陣列元素的第 k 位執行計數排序 + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // 即 exp = 10^(k-1) + CountingSortDigit(nums, exp); + } + } + ``` + +=== "Go" + + ```go title="radix_sort.go" + /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */ + func digit(num, exp int) int { + // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算 + return (num / exp) % 10 + } + + /* 計數排序(根據 nums 第 k 位排序) */ + func countingSortDigit(nums []int, exp int) { + // 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列 + counter := make([]int, 10) + n := len(nums) + // 統計 0~9 各數字的出現次數 + for i := 0; i < n; i++ { + d := digit(nums[i], exp) // 獲取 nums[i] 第 k 位,記為 d + counter[d]++ // 統計數字 d 的出現次數 + } + // 求前綴和,將“出現個數”轉換為“陣列索引” + for i := 1; i < 10; i++ { + counter[i] += counter[i-1] + } + // 倒序走訪,根據桶內統計結果,將各元素填入 res + res := make([]int, n) + for i := n - 1; i >= 0; i-- { + d := digit(nums[i], exp) + j := counter[d] - 1 // 獲取 d 在陣列中的索引 j + res[j] = nums[i] // 將當前元素填入索引 j + counter[d]-- // 將 d 的數量減 1 + } + // 使用結果覆蓋原陣列 nums + for i := 0; i < n; i++ { + nums[i] = res[i] + } + } + + /* 基數排序 */ + func radixSort(nums []int) { + // 獲取陣列的最大元素,用於判斷最大位數 + max := math.MinInt + for _, num := range nums { + if num > max { + max = num + } + } + // 按照從低位到高位的順序走訪 + for exp := 1; max >= exp; exp *= 10 { + // 對陣列元素的第 k 位執行計數排序 + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // 即 exp = 10^(k-1) + countingSortDigit(nums, exp) + } + } + ``` + +=== "Swift" + + ```swift title="radix_sort.swift" + /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */ + func digit(num: Int, exp: Int) -> Int { + // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算 + (num / exp) % 10 + } + + /* 計數排序(根據 nums 第 k 位排序) */ + func countingSortDigit(nums: inout [Int], exp: Int) { + // 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列 + var counter = Array(repeating: 0, count: 10) + // 統計 0~9 各數字的出現次數 + for i in nums.indices { + let d = digit(num: nums[i], exp: exp) // 獲取 nums[i] 第 k 位,記為 d + counter[d] += 1 // 統計數字 d 的出現次數 + } + // 求前綴和,將“出現個數”轉換為“陣列索引” + for i in 1 ..< 10 { + counter[i] += counter[i - 1] + } + // 倒序走訪,根據桶內統計結果,將各元素填入 res + var res = Array(repeating: 0, count: nums.count) + for i in nums.indices.reversed() { + let d = digit(num: nums[i], exp: exp) + let j = counter[d] - 1 // 獲取 d 在陣列中的索引 j + res[j] = nums[i] // 將當前元素填入索引 j + counter[d] -= 1 // 將 d 的數量減 1 + } + // 使用結果覆蓋原陣列 nums + for i in nums.indices { + nums[i] = res[i] + } + } + + /* 基數排序 */ + func radixSort(nums: inout [Int]) { + // 獲取陣列的最大元素,用於判斷最大位數 + var m = Int.min + for num in nums { + if num > m { + m = num + } + } + // 按照從低位到高位的順序走訪 + for exp in sequence(first: 1, next: { m >= ($0 * 10) ? $0 * 10 : nil }) { + // 對陣列元素的第 k 位執行計數排序 + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // 即 exp = 10^(k-1) + countingSortDigit(nums: &nums, exp: exp) + } + } + ``` + +=== "JS" + + ```javascript title="radix_sort.js" + /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */ + function digit(num, exp) { + // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算 + return Math.floor(num / exp) % 10; + } + + /* 計數排序(根據 nums 第 k 位排序) */ + function countingSortDigit(nums, exp) { + // 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列 + const counter = new Array(10).fill(0); + const n = nums.length; + // 統計 0~9 各數字的出現次數 + for (let i = 0; i < n; i++) { + const d = digit(nums[i], exp); // 獲取 nums[i] 第 k 位,記為 d + counter[d]++; // 統計數字 d 的出現次數 + } + // 求前綴和,將“出現個數”轉換為“陣列索引” + for (let i = 1; i < 10; i++) { + counter[i] += counter[i - 1]; + } + // 倒序走訪,根據桶內統計結果,將各元素填入 res + const res = new Array(n).fill(0); + for (let i = n - 1; i >= 0; i--) { + const d = digit(nums[i], exp); + const j = counter[d] - 1; // 獲取 d 在陣列中的索引 j + res[j] = nums[i]; // 將當前元素填入索引 j + counter[d]--; // 將 d 的數量減 1 + } + // 使用結果覆蓋原陣列 nums + for (let i = 0; i < n; i++) { + nums[i] = res[i]; + } + } + + /* 基數排序 */ + function radixSort(nums) { + // 獲取陣列的最大元素,用於判斷最大位數 + let m = Number.MIN_VALUE; + for (const num of nums) { + if (num > m) { + m = num; + } + } + // 按照從低位到高位的順序走訪 + for (let exp = 1; exp <= m; exp *= 10) { + // 對陣列元素的第 k 位執行計數排序 + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // 即 exp = 10^(k-1) + countingSortDigit(nums, exp); + } + } + ``` + +=== "TS" + + ```typescript title="radix_sort.ts" + /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */ + function digit(num: number, exp: number): number { + // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算 + return Math.floor(num / exp) % 10; + } + + /* 計數排序(根據 nums 第 k 位排序) */ + function countingSortDigit(nums: number[], exp: number): void { + // 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列 + const counter = new Array(10).fill(0); + const n = nums.length; + // 統計 0~9 各數字的出現次數 + for (let i = 0; i < n; i++) { + const d = digit(nums[i], exp); // 獲取 nums[i] 第 k 位,記為 d + counter[d]++; // 統計數字 d 的出現次數 + } + // 求前綴和,將“出現個數”轉換為“陣列索引” + for (let i = 1; i < 10; i++) { + counter[i] += counter[i - 1]; + } + // 倒序走訪,根據桶內統計結果,將各元素填入 res + const res = new Array(n).fill(0); + for (let i = n - 1; i >= 0; i--) { + const d = digit(nums[i], exp); + const j = counter[d] - 1; // 獲取 d 在陣列中的索引 j + res[j] = nums[i]; // 將當前元素填入索引 j + counter[d]--; // 將 d 的數量減 1 + } + // 使用結果覆蓋原陣列 nums + for (let i = 0; i < n; i++) { + nums[i] = res[i]; + } + } + + /* 基數排序 */ + function radixSort(nums: number[]): void { + // 獲取陣列的最大元素,用於判斷最大位數 + let m = Number.MIN_VALUE; + for (const num of nums) { + if (num > m) { + m = num; + } + } + // 按照從低位到高位的順序走訪 + for (let exp = 1; exp <= m; exp *= 10) { + // 對陣列元素的第 k 位執行計數排序 + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // 即 exp = 10^(k-1) + countingSortDigit(nums, exp); + } + } + ``` + +=== "Dart" + + ```dart title="radix_sort.dart" + /* 獲取元素 _num 的第 k 位,其中 exp = 10^(k-1) */ + int digit(int _num, int exp) { + // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算 + return (_num ~/ exp) % 10; + } + + /* 計數排序(根據 nums 第 k 位排序) */ + void countingSortDigit(List nums, int exp) { + // 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列 + List counter = List.filled(10, 0); + int n = nums.length; + // 統計 0~9 各數字的出現次數 + for (int i = 0; i < n; i++) { + int d = digit(nums[i], exp); // 獲取 nums[i] 第 k 位,記為 d + counter[d]++; // 統計數字 d 的出現次數 + } + // 求前綴和,將“出現個數”轉換為“陣列索引” + for (int i = 1; i < 10; i++) { + counter[i] += counter[i - 1]; + } + // 倒序走訪,根據桶內統計結果,將各元素填入 res + List res = List.filled(n, 0); + for (int i = n - 1; i >= 0; i--) { + int d = digit(nums[i], exp); + int j = counter[d] - 1; // 獲取 d 在陣列中的索引 j + res[j] = nums[i]; // 將當前元素填入索引 j + counter[d]--; // 將 d 的數量減 1 + } + // 使用結果覆蓋原陣列 nums + for (int i = 0; i < n; i++) nums[i] = res[i]; + } + + /* 基數排序 */ + void radixSort(List nums) { + // 獲取陣列的最大元素,用於判斷最大位數 + // dart 中 int 的長度是 64 位的 + int m = -1 << 63; + for (int _num in nums) if (_num > m) m = _num; + // 按照從低位到高位的順序走訪 + for (int exp = 1; exp <= m; exp *= 10) + // 對陣列元素的第 k 位執行計數排序 + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // 即 exp = 10^(k-1) + countingSortDigit(nums, exp); + } + ``` + +=== "Rust" + + ```rust title="radix_sort.rs" + /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */ + fn digit(num: i32, exp: i32) -> usize { + // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算 + return ((num / exp) % 10) as usize; + } + + /* 計數排序(根據 nums 第 k 位排序) */ + fn counting_sort_digit(nums: &mut [i32], exp: i32) { + // 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列 + let mut counter = [0; 10]; + let n = nums.len(); + // 統計 0~9 各數字的出現次數 + for i in 0..n { + let d = digit(nums[i], exp); // 獲取 nums[i] 第 k 位,記為 d + counter[d] += 1; // 統計數字 d 的出現次數 + } + // 求前綴和,將“出現個數”轉換為“陣列索引” + for i in 1..10 { + counter[i] += counter[i - 1]; + } + // 倒序走訪,根據桶內統計結果,將各元素填入 res + let mut res = vec![0; n]; + for i in (0..n).rev() { + let d = digit(nums[i], exp); + let j = counter[d] - 1; // 獲取 d 在陣列中的索引 j + res[j] = nums[i]; // 將當前元素填入索引 j + counter[d] -= 1; // 將 d 的數量減 1 + } + // 使用結果覆蓋原陣列 nums + for i in 0..n { + nums[i] = res[i]; + } + } + + /* 基數排序 */ + fn radix_sort(nums: &mut [i32]) { + // 獲取陣列的最大元素,用於判斷最大位數 + let m = *nums.into_iter().max().unwrap(); + // 按照從低位到高位的順序走訪 + let mut exp = 1; + while exp <= m { + counting_sort_digit(nums, exp); + exp *= 10; + } + } + ``` + +=== "C" + + ```c title="radix_sort.c" + /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */ + int digit(int num, int exp) { + // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算 + return (num / exp) % 10; + } + + /* 計數排序(根據 nums 第 k 位排序) */ + void countingSortDigit(int nums[], int size, int exp) { + // 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列 + int *counter = (int *)malloc((sizeof(int) * 10)); + // 統計 0~9 各數字的出現次數 + for (int i = 0; i < size; i++) { + // 獲取 nums[i] 第 k 位,記為 d + int d = digit(nums[i], exp); + // 統計數字 d 的出現次數 + counter[d]++; + } + // 求前綴和,將“出現個數”轉換為“陣列索引” + for (int i = 1; i < 10; i++) { + counter[i] += counter[i - 1]; + } + // 倒序走訪,根據桶內統計結果,將各元素填入 res + int *res = (int *)malloc(sizeof(int) * size); + for (int i = size - 1; i >= 0; i--) { + int d = digit(nums[i], exp); + int j = counter[d] - 1; // 獲取 d 在陣列中的索引 j + res[j] = nums[i]; // 將當前元素填入索引 j + counter[d]--; // 將 d 的數量減 1 + } + // 使用結果覆蓋原陣列 nums + for (int i = 0; i < size; i++) { + nums[i] = res[i]; + } + } + + /* 基數排序 */ + void radixSort(int nums[], int size) { + // 獲取陣列的最大元素,用於判斷最大位數 + int max = INT32_MIN; + for (size_t i = 0; i < size - 1; i++) { + if (nums[i] > max) { + max = nums[i]; + } + } + // 按照從低位到高位的順序走訪 + for (int exp = 1; max >= exp; exp *= 10) + // 對陣列元素的第 k 位執行計數排序 + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // 即 exp = 10^(k-1) + countingSortDigit(nums, size, exp); + } + ``` + +=== "Kotlin" + + ```kotlin title="radix_sort.kt" + /* 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) */ + fun digit(num: Int, exp: Int): Int { + // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算 + return (num / exp) % 10 + } + + /* 計數排序(根據 nums 第 k 位排序) */ + fun countingSortDigit(nums: IntArray, exp: Int) { + // 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列 + val counter = IntArray(10) + val n = nums.size + // 統計 0~9 各數字的出現次數 + for (i in 0.. m) m = num + var exp = 1 + // 按照從低位到高位的順序走訪 + while (exp <= m) { + // 對陣列元素的第 k 位執行計數排序 + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // 即 exp = 10^(k-1) + countingSortDigit(nums, exp) + exp *= 10 + } + } + ``` + +=== "Ruby" + + ```ruby title="radix_sort.rb" + [class]{}-[func]{digit} + + [class]{}-[func]{counting_sort_digit} + + [class]{}-[func]{radix_sort} + ``` + +=== "Zig" + + ```zig title="radix_sort.zig" + // 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) + fn digit(num: i32, exp: i32) i32 { + // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算 + return @mod(@divFloor(num, exp), 10); + } + + // 計數排序(根據 nums 第 k 位排序) + fn countingSortDigit(nums: []i32, exp: i32) !void { + // 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列 + var mem_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + // defer mem_arena.deinit(); + const mem_allocator = mem_arena.allocator(); + var counter = try mem_allocator.alloc(usize, 10); + @memset(counter, 0); + var n = nums.len; + // 統計 0~9 各數字的出現次數 + for (nums) |num| { + var d: u32 = @bitCast(digit(num, exp)); // 獲取 nums[i] 第 k 位,記為 d + counter[d] += 1; // 統計數字 d 的出現次數 + } + // 求前綴和,將“出現個數”轉換為“陣列索引” + var i: usize = 1; + while (i < 10) : (i += 1) { + counter[i] += counter[i - 1]; + } + // 倒序走訪,根據桶內統計結果,將各元素填入 res + var res = try mem_allocator.alloc(i32, n); + i = n - 1; + while (i >= 0) : (i -= 1) { + var d: u32 = @bitCast(digit(nums[i], exp)); + var j = counter[d] - 1; // 獲取 d 在陣列中的索引 j + res[j] = nums[i]; // 將當前元素填入索引 j + counter[d] -= 1; // 將 d 的數量減 1 + if (i == 0) break; + } + // 使用結果覆蓋原陣列 nums + i = 0; + while (i < n) : (i += 1) { + nums[i] = res[i]; + } + } + + // 基數排序 + fn radixSort(nums: []i32) !void { + // 獲取陣列的最大元素,用於判斷最大位數 + var m: i32 = std.math.minInt(i32); + for (nums) |num| { + if (num > m) m = num; + } + // 按照從低位到高位的順序走訪 + var exp: i32 = 1; + while (exp <= m) : (exp *= 10) { + // 對陣列元素的第 k 位執行計數排序 + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // 即 exp = 10^(k-1) + try countingSortDigit(nums, exp); + } + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +!!! question "為什麼從最低位開始排序?" + + 在連續的排序輪次中,後一輪排序會覆蓋前一輪排序的結果。舉例來說,如果第一輪排序結果 $a < b$ ,而第二輪排序結果 $a > b$ ,那麼第二輪的結果將取代第一輪的結果。由於數字的高位優先順序高於低位,因此應該先排序低位再排序高位。 + +## 11.10.2   演算法特性 + +相較於計數排序,基數排序適用於數值範圍較大的情況,**但前提是資料必須可以表示為固定位數的格式,且位數不能過大**。例如,浮點數不適合使用基數排序,因為其位數 $k$ 過大,可能導致時間複雜度 $O(nk) \gg O(n^2)$ 。 + +- **時間複雜度為 $O(nk)$**:設資料量為 $n$、資料為 $d$ 進位制、最大位數為 $k$ ,則對某一位執行計數排序使用 $O(n + d)$ 時間,排序所有 $k$ 位使用 $O((n + d)k)$ 時間。通常情況下,$d$ 和 $k$ 都相對較小,時間複雜度趨向 $O(n)$ 。 +- **空間複雜度為 $O(n + d)$、非原地排序**:與計數排序相同,基數排序需要藉助長度為 $n$ 和 $d$ 的陣列 `res` 和 `counter` 。 +- **穩定排序**:當計數排序穩定時,基數排序也穩定;當計數排序不穩定時,基數排序無法保證得到正確的排序結果。 diff --git a/zh-Hant/docs/chapter_sorting/selection_sort.md b/zh-Hant/docs/chapter_sorting/selection_sort.md new file mode 100644 index 000000000..f9749f39f --- /dev/null +++ b/zh-Hant/docs/chapter_sorting/selection_sort.md @@ -0,0 +1,328 @@ +--- +comments: true +--- + +# 11.2   選擇排序 + +選擇排序(selection sort)的工作原理非常簡單:開啟一個迴圈,每輪從未排序區間選擇最小的元素,將其放到已排序區間的末尾。 + +設陣列的長度為 $n$ ,選擇排序的演算法流程如圖 11-2 所示。 + +1. 初始狀態下,所有元素未排序,即未排序(索引)區間為 $[0, n-1]$ 。 +2. 選取區間 $[0, n-1]$ 中的最小元素,將其與索引 $0$ 處的元素交換。完成後,陣列前 1 個元素已排序。 +3. 選取區間 $[1, n-1]$ 中的最小元素,將其與索引 $1$ 處的元素交換。完成後,陣列前 2 個元素已排序。 +4. 以此類推。經過 $n - 1$ 輪選擇與交換後,陣列前 $n - 1$ 個元素已排序。 +5. 僅剩的一個元素必定是最大元素,無須排序,因此陣列排序完成。 + +=== "<1>" + ![選擇排序步驟](selection_sort.assets/selection_sort_step1.png){ class="animation-figure" } + +=== "<2>" + ![selection_sort_step2](selection_sort.assets/selection_sort_step2.png){ class="animation-figure" } + +=== "<3>" + ![selection_sort_step3](selection_sort.assets/selection_sort_step3.png){ class="animation-figure" } + +=== "<4>" + ![selection_sort_step4](selection_sort.assets/selection_sort_step4.png){ class="animation-figure" } + +=== "<5>" + ![selection_sort_step5](selection_sort.assets/selection_sort_step5.png){ class="animation-figure" } + +=== "<6>" + ![selection_sort_step6](selection_sort.assets/selection_sort_step6.png){ class="animation-figure" } + +=== "<7>" + ![selection_sort_step7](selection_sort.assets/selection_sort_step7.png){ class="animation-figure" } + +=== "<8>" + ![selection_sort_step8](selection_sort.assets/selection_sort_step8.png){ class="animation-figure" } + +=== "<9>" + ![selection_sort_step9](selection_sort.assets/selection_sort_step9.png){ class="animation-figure" } + +=== "<10>" + ![selection_sort_step10](selection_sort.assets/selection_sort_step10.png){ class="animation-figure" } + +=== "<11>" + ![selection_sort_step11](selection_sort.assets/selection_sort_step11.png){ class="animation-figure" } + +

圖 11-2   選擇排序步驟

+ +在程式碼中,我們用 $k$ 來記錄未排序區間內的最小元素: + +=== "Python" + + ```python title="selection_sort.py" + def selection_sort(nums: list[int]): + """選擇排序""" + n = len(nums) + # 外迴圈:未排序區間為 [i, n-1] + for i in range(n - 1): + # 內迴圈:找到未排序區間內的最小元素 + k = i + for j in range(i + 1, n): + if nums[j] < nums[k]: + k = j # 記錄最小元素的索引 + # 將該最小元素與未排序區間的首個元素交換 + nums[i], nums[k] = nums[k], nums[i] + ``` + +=== "C++" + + ```cpp title="selection_sort.cpp" + /* 選擇排序 */ + void selectionSort(vector &nums) { + int n = nums.size(); + // 外迴圈:未排序區間為 [i, n-1] + for (int i = 0; i < n - 1; i++) { + // 內迴圈:找到未排序區間內的最小元素 + int k = i; + for (int j = i + 1; j < n; j++) { + if (nums[j] < nums[k]) + k = j; // 記錄最小元素的索引 + } + // 將該最小元素與未排序區間的首個元素交換 + swap(nums[i], nums[k]); + } + } + ``` + +=== "Java" + + ```java title="selection_sort.java" + /* 選擇排序 */ + void selectionSort(int[] nums) { + int n = nums.length; + // 外迴圈:未排序區間為 [i, n-1] + for (int i = 0; i < n - 1; i++) { + // 內迴圈:找到未排序區間內的最小元素 + int k = i; + for (int j = i + 1; j < n; j++) { + if (nums[j] < nums[k]) + k = j; // 記錄最小元素的索引 + } + // 將該最小元素與未排序區間的首個元素交換 + int temp = nums[i]; + nums[i] = nums[k]; + nums[k] = temp; + } + } + ``` + +=== "C#" + + ```csharp title="selection_sort.cs" + /* 選擇排序 */ + void SelectionSort(int[] nums) { + int n = nums.Length; + // 外迴圈:未排序區間為 [i, n-1] + for (int i = 0; i < n - 1; i++) { + // 內迴圈:找到未排序區間內的最小元素 + int k = i; + for (int j = i + 1; j < n; j++) { + if (nums[j] < nums[k]) + k = j; // 記錄最小元素的索引 + } + // 將該最小元素與未排序區間的首個元素交換 + (nums[k], nums[i]) = (nums[i], nums[k]); + } + } + ``` + +=== "Go" + + ```go title="selection_sort.go" + /* 選擇排序 */ + func selectionSort(nums []int) { + n := len(nums) + // 外迴圈:未排序區間為 [i, n-1] + for i := 0; i < n-1; i++ { + // 內迴圈:找到未排序區間內的最小元素 + k := i + for j := i + 1; j < n; j++ { + if nums[j] < nums[k] { + // 記錄最小元素的索引 + k = j + } + } + // 將該最小元素與未排序區間的首個元素交換 + nums[i], nums[k] = nums[k], nums[i] + + } + } + ``` + +=== "Swift" + + ```swift title="selection_sort.swift" + /* 選擇排序 */ + func selectionSort(nums: inout [Int]) { + // 外迴圈:未排序區間為 [i, n-1] + for i in nums.indices.dropLast() { + // 內迴圈:找到未排序區間內的最小元素 + var k = i + for j in nums.indices.dropFirst(i + 1) { + if nums[j] < nums[k] { + k = j // 記錄最小元素的索引 + } + } + // 將該最小元素與未排序區間的首個元素交換 + nums.swapAt(i, k) + } + } + ``` + +=== "JS" + + ```javascript title="selection_sort.js" + /* 選擇排序 */ + function selectionSort(nums) { + let n = nums.length; + // 外迴圈:未排序區間為 [i, n-1] + for (let i = 0; i < n - 1; i++) { + // 內迴圈:找到未排序區間內的最小元素 + let k = i; + for (let j = i + 1; j < n; j++) { + if (nums[j] < nums[k]) { + k = j; // 記錄最小元素的索引 + } + } + // 將該最小元素與未排序區間的首個元素交換 + [nums[i], nums[k]] = [nums[k], nums[i]]; + } + } + ``` + +=== "TS" + + ```typescript title="selection_sort.ts" + /* 選擇排序 */ + function selectionSort(nums: number[]): void { + let n = nums.length; + // 外迴圈:未排序區間為 [i, n-1] + for (let i = 0; i < n - 1; i++) { + // 內迴圈:找到未排序區間內的最小元素 + let k = i; + for (let j = i + 1; j < n; j++) { + if (nums[j] < nums[k]) { + k = j; // 記錄最小元素的索引 + } + } + // 將該最小元素與未排序區間的首個元素交換 + [nums[i], nums[k]] = [nums[k], nums[i]]; + } + } + ``` + +=== "Dart" + + ```dart title="selection_sort.dart" + /* 選擇排序 */ + void selectionSort(List nums) { + int n = nums.length; + // 外迴圈:未排序區間為 [i, n-1] + for (int i = 0; i < n - 1; i++) { + // 內迴圈:找到未排序區間內的最小元素 + int k = i; + for (int j = i + 1; j < n; j++) { + if (nums[j] < nums[k]) k = j; // 記錄最小元素的索引 + } + // 將該最小元素與未排序區間的首個元素交換 + int temp = nums[i]; + nums[i] = nums[k]; + nums[k] = temp; + } + } + ``` + +=== "Rust" + + ```rust title="selection_sort.rs" + /* 選擇排序 */ + fn selection_sort(nums: &mut [i32]) { + if nums.is_empty() { + return; + } + let n = nums.len(); + // 外迴圈:未排序區間為 [i, n-1] + for i in 0..n - 1 { + // 內迴圈:找到未排序區間內的最小元素 + let mut k = i; + for j in i + 1..n { + if nums[j] < nums[k] { + k = j; // 記錄最小元素的索引 + } + } + // 將該最小元素與未排序區間的首個元素交換 + nums.swap(i, k); + } + } + ``` + +=== "C" + + ```c title="selection_sort.c" + /* 選擇排序 */ + void selectionSort(int nums[], int n) { + // 外迴圈:未排序區間為 [i, n-1] + for (int i = 0; i < n - 1; i++) { + // 內迴圈:找到未排序區間內的最小元素 + int k = i; + for (int j = i + 1; j < n; j++) { + if (nums[j] < nums[k]) + k = j; // 記錄最小元素的索引 + } + // 將該最小元素與未排序區間的首個元素交換 + int temp = nums[i]; + nums[i] = nums[k]; + nums[k] = temp; + } + } + ``` + +=== "Kotlin" + + ```kotlin title="selection_sort.kt" + /* 選擇排序 */ + fun selectionSort(nums: IntArray) { + val n = nums.size + // 外迴圈:未排序區間為 [i, n-1] + for (i in 0.. + + +## 11.2.1   演算法特性 + +- **時間複雜度為 $O(n^2)$、非自適應排序**:外迴圈共 $n - 1$ 輪,第一輪的未排序區間長度為 $n$ ,最後一輪的未排序區間長度為 $2$ ,即各輪外迴圈分別包含 $n$、$n - 1$、$\dots$、$3$、$2$ 輪內迴圈,求和為 $\frac{(n - 1)(n + 2)}{2}$ 。 +- **空間複雜度為 $O(1)$、原地排序**:指標 $i$ 和 $j$ 使用常數大小的額外空間。 +- **非穩定排序**:如圖 11-3 所示,元素 `nums[i]` 有可能被交換至與其相等的元素的右邊,導致兩者的相對順序發生改變。 + +![選擇排序非穩定示例](selection_sort.assets/selection_sort_instability.png){ class="animation-figure" } + +

圖 11-3   選擇排序非穩定示例

diff --git a/zh-Hant/docs/chapter_sorting/sorting_algorithm.md b/zh-Hant/docs/chapter_sorting/sorting_algorithm.md new file mode 100644 index 000000000..5079e06c8 --- /dev/null +++ b/zh-Hant/docs/chapter_sorting/sorting_algorithm.md @@ -0,0 +1,54 @@ +--- +comments: true +--- + +# 11.1   排序演算法 + +排序演算法(sorting algorithm)用於對一組資料按照特定順序進行排列。排序演算法有著廣泛的應用,因為有序資料通常能夠被更高效地查詢、分析和處理。 + +如圖 11-1 所示,排序演算法中的資料型別可以是整數、浮點數、字元或字串等。排序的判斷規則可根據需求設定,如數字大小、字元 ASCII 碼順序或自定義規則。 + +![資料型別和判斷規則示例](sorting_algorithm.assets/sorting_examples.png){ class="animation-figure" } + +

圖 11-1   資料型別和判斷規則示例

+ +## 11.1.1   評價維度 + +**執行效率**:我們期望排序演算法的時間複雜度儘量低,且總體操作數量較少(時間複雜度中的常數項變小)。對於大資料量的情況,執行效率顯得尤為重要。 + +**就地性**:顧名思義,原地排序透過在原陣列上直接操作實現排序,無須藉助額外的輔助陣列,從而節省記憶體。通常情況下,原地排序的資料搬運操作較少,執行速度也更快。 + +**穩定性**:穩定排序在完成排序後,相等元素在陣列中的相對順序不發生改變。 + +穩定排序是多級排序場景的必要條件。假設我們有一個儲存學生資訊的表格,第 1 列和第 2 列分別是姓名和年齡。在這種情況下,非穩定排序可能導致輸入資料的有序性喪失: + +```shell +# 輸入資料是按照姓名排序好的 +# (name, age) + ('A', 19) + ('B', 18) + ('C', 21) + ('D', 19) + ('E', 23) + +# 假設使用非穩定排序演算法按年齡排序串列, +# 結果中 ('D', 19) 和 ('A', 19) 的相對位置改變, +# 輸入資料按姓名排序的性質丟失 + ('B', 18) + ('D', 19) + ('A', 19) + ('C', 21) + ('E', 23) +``` + +**自適應性**:自適應排序的時間複雜度會受輸入資料的影響,即最佳時間複雜度、最差時間複雜度、平均時間複雜度並不完全相等。 + +自適應性需要根據具體情況來評估。如果最差時間複雜度差於平均時間複雜度,說明排序演算法在某些資料下效能可能劣化,因此被視為負面屬性;而如果最佳時間複雜度優於平均時間複雜度,則被視為正面屬性。 + +**是否基於比較**:基於比較的排序依賴比較運算子($<$、$=$、$>$)來判斷元素的相對順序,從而排序整個陣列,理論最優時間複雜度為 $O(n \log n)$ 。而非比較排序不使用比較運算子,時間複雜度可達 $O(n)$ ,但其通用性相對較差。 + +## 11.1.2   理想排序演算法 + +**執行快、原地、穩定、正向自適應、通用性好**。顯然,迄今為止尚未發現兼具以上所有特性的排序演算法。因此,在選擇排序演算法時,需要根據具體的資料特點和問題需求來決定。 + +接下來,我們將共同學習各種排序演算法,並基於上述評價維度對各個排序演算法的優缺點進行分析。 diff --git a/zh-Hant/docs/chapter_sorting/summary.md b/zh-Hant/docs/chapter_sorting/summary.md new file mode 100644 index 000000000..72f128f63 --- /dev/null +++ b/zh-Hant/docs/chapter_sorting/summary.md @@ -0,0 +1,53 @@ +--- +comments: true +--- + +# 11.11   小結 + +### 1.   重點回顧 + +- 泡沫排序透過交換相鄰元素來實現排序。透過新增一個標誌位來實現提前返回,我們可以將泡沫排序的最佳時間複雜度最佳化到 $O(n)$ 。 +- 插入排序每輪將未排序區間內的元素插入到已排序區間的正確位置,從而完成排序。雖然插入排序的時間複雜度為 $O(n^2)$ ,但由於單元操作相對較少,因此在小資料量的排序任務中非常受歡迎。 +- 快速排序基於哨兵劃分操作實現排序。在哨兵劃分中,有可能每次都選取到最差的基準數,導致時間複雜度劣化至 $O(n^2)$ 。引入中位數基準數或隨機基準數可以降低這種劣化的機率。尾遞迴方法可以有效地減少遞迴深度,將空間複雜度最佳化到 $O(\log n)$ 。 +- 合併排序包括劃分和合並兩個階段,典型地體現了分治策略。在合併排序中,排序陣列需要建立輔助陣列,空間複雜度為 $O(n)$ ;然而排序鏈結串列的空間複雜度可以最佳化至 $O(1)$ 。 +- 桶排序包含三個步驟:資料分桶、桶內排序和合並結果。它同樣體現了分治策略,適用於資料體量很大的情況。桶排序的關鍵在於對資料進行平均分配。 +- 計數排序是桶排序的一個特例,它透過統計資料出現的次數來實現排序。計數排序適用於資料量大但資料範圍有限的情況,並且要求資料能夠轉換為正整數。 +- 基數排序透過逐位排序來實現資料排序,要求資料能夠表示為固定位數的數字。 +- 總的來說,我們希望找到一種排序演算法,具有高效率、穩定、原地以及正向自適應性等優點。然而,正如其他資料結構和演算法一樣,沒有一種排序演算法能夠同時滿足所有這些條件。在實際應用中,我們需要根據資料的特性來選擇合適的排序演算法。 +- 圖 11-19 對比了主流排序演算法的效率、穩定性、就地性和自適應性等。 + +![排序演算法對比](summary.assets/sorting_algorithms_comparison.png){ class="animation-figure" } + +

圖 11-19   排序演算法對比

+ +### 2.   Q & A + +**Q**:排序演算法穩定性在什麼情況下是必需的? + +在現實中,我們有可能基於物件的某個屬性進行排序。例如,學生有姓名和身高兩個屬性,我們希望實現一個多級排序:先按照姓名進行排序,得到 `(A, 180) (B, 185) (C, 170) (D, 170)` ;再對身高進行排序。由於排序演算法不穩定,因此可能得到 `(D, 170) (C, 170) (A, 180) (B, 185)` 。 + +可以發現,學生 D 和 C 的位置發生了交換,姓名的有序性被破壞了,而這是我們不希望看到的。 + +**Q**:哨兵劃分中“從右往左查詢”與“從左往右查詢”的順序可以交換嗎? + +不行,當我們以最左端元素為基準數時,必須先“從右往左查詢”再“從左往右查詢”。這個結論有些反直覺,我們來剖析一下原因。 + +哨兵劃分 `partition()` 的最後一步是交換 `nums[left]` 和 `nums[i]` 。完成交換後,基準數左邊的元素都 `<=` 基準數,**這就要求最後一步交換前 `nums[left] >= nums[i]` 必須成立**。假設我們先“從左往右查詢”,那麼如果找不到比基準數更大的元素,**則會在 `i == j` 時跳出迴圈,此時可能 `nums[j] == nums[i] > nums[left]`**。也就是說,此時最後一步交換操作會把一個比基準數更大的元素交換至陣列最左端,導致哨兵劃分失敗。 + +舉個例子,給定陣列 `[0, 0, 0, 0, 1]` ,如果先“從左向右查詢”,哨兵劃分後陣列為 `[1, 0, 0, 0, 0]` ,這個結果是不正確的。 + +再深入思考一下,如果我們選擇 `nums[right]` 為基準數,那麼正好反過來,必須先“從左往右查詢”。 + +**Q**:關於尾遞迴最佳化,為什麼選短的陣列能保證遞迴深度不超過 $\log n$ ? + +遞迴深度就是當前未返回的遞迴方法的數量。每輪哨兵劃分我們將原陣列劃分為兩個子陣列。在尾遞迴最佳化後,向下遞迴的子陣列長度最大為原陣列長度的一半。假設最差情況,一直為一半長度,那麼最終的遞迴深度就是 $\log n$ 。 + +回顧原始的快速排序,我們有可能會連續地遞迴長度較大的陣列,最差情況下為 $n$、$n - 1$、$\dots$、$2$、$1$ ,遞迴深度為 $n$ 。尾遞迴最佳化可以避免這種情況出現。 + +**Q**:當陣列中所有元素都相等時,快速排序的時間複雜度是 $O(n^2)$ 嗎?該如何處理這種退化情況? + +是的。對於這種情況,可以考慮透過哨兵劃分將陣列劃分為三個部分:小於、等於、大於基準數。僅向下遞迴小於和大於的兩部分。在該方法下,輸入元素全部相等的陣列,僅一輪哨兵劃分即可完成排序。 + +**Q**:桶排序的最差時間複雜度為什麼是 $O(n^2)$ ? + +最差情況下,所有元素被分至同一個桶中。如果我們採用一個 $O(n^2)$ 演算法來排序這些元素,則時間複雜度為 $O(n^2)$ 。 diff --git a/zh-Hant/docs/chapter_stack_and_queue/deque.md b/zh-Hant/docs/chapter_stack_and_queue/deque.md new file mode 100644 index 000000000..c99f0a538 --- /dev/null +++ b/zh-Hant/docs/chapter_stack_and_queue/deque.md @@ -0,0 +1,3506 @@ +--- +comments: true +--- + +# 5.3   雙向佇列 + +在佇列中,我們僅能刪除頭部元素或在尾部新增元素。如圖 5-7 所示,雙向佇列(double-ended queue)提供了更高的靈活性,允許在頭部和尾部執行元素的新增或刪除操作。 + +![雙向佇列的操作](deque.assets/deque_operations.png){ class="animation-figure" } + +

圖 5-7   雙向佇列的操作

+ +## 5.3.1   雙向佇列常用操作 + +雙向佇列的常用操作如表 5-3 所示,具體的方法名稱需要根據所使用的程式語言來確定。 + +

表 5-3   雙向佇列操作效率

+ +
+ +| 方法名 | 描述 | 時間複雜度 | +| -------------- | ---------------- | ---------- | +| `push_first()` | 將元素新增至佇列首 | $O(1)$ | +| `push_last()` | 將元素新增至佇列尾 | $O(1)$ | +| `pop_first()` | 刪除佇列首元素 | $O(1)$ | +| `pop_last()` | 刪除佇列尾元素 | $O(1)$ | +| `peek_first()` | 訪問佇列首元素 | $O(1)$ | +| `peek_last()` | 訪問佇列尾元素 | $O(1)$ | + +
+ +同樣地,我們可以直接使用程式語言中已實現的雙向佇列類別: + +=== "Python" + + ```python title="deque.py" + from collections import deque + + # 初始化雙向佇列 + deque: deque[int] = deque() + + # 元素入列 + deque.append(2) # 新增至佇列尾 + deque.append(5) + deque.append(4) + deque.appendleft(3) # 新增至佇列首 + deque.appendleft(1) + + # 訪問元素 + front: int = deque[0] # 佇列首元素 + rear: int = deque[-1] # 佇列尾元素 + + # 元素出列 + pop_front: int = deque.popleft() # 佇列首元素出列 + pop_rear: int = deque.pop() # 佇列尾元素出列 + + # 獲取雙向佇列的長度 + size: int = len(deque) + + # 判斷雙向佇列是否為空 + is_empty: bool = len(deque) == 0 + ``` + +=== "C++" + + ```cpp title="deque.cpp" + /* 初始化雙向佇列 */ + deque deque; + + /* 元素入列 */ + deque.push_back(2); // 新增至佇列尾 + deque.push_back(5); + deque.push_back(4); + deque.push_front(3); // 新增至佇列首 + deque.push_front(1); + + /* 訪問元素 */ + int front = deque.front(); // 佇列首元素 + int back = deque.back(); // 佇列尾元素 + + /* 元素出列 */ + deque.pop_front(); // 佇列首元素出列 + deque.pop_back(); // 佇列尾元素出列 + + /* 獲取雙向佇列的長度 */ + int size = deque.size(); + + /* 判斷雙向佇列是否為空 */ + bool empty = deque.empty(); + ``` + +=== "Java" + + ```java title="deque.java" + /* 初始化雙向佇列 */ + Deque deque = new LinkedList<>(); + + /* 元素入列 */ + deque.offerLast(2); // 新增至佇列尾 + deque.offerLast(5); + deque.offerLast(4); + deque.offerFirst(3); // 新增至佇列首 + deque.offerFirst(1); + + /* 訪問元素 */ + int peekFirst = deque.peekFirst(); // 佇列首元素 + int peekLast = deque.peekLast(); // 佇列尾元素 + + /* 元素出列 */ + int popFirst = deque.pollFirst(); // 佇列首元素出列 + int popLast = deque.pollLast(); // 佇列尾元素出列 + + /* 獲取雙向佇列的長度 */ + int size = deque.size(); + + /* 判斷雙向佇列是否為空 */ + boolean isEmpty = deque.isEmpty(); + ``` + +=== "C#" + + ```csharp title="deque.cs" + /* 初始化雙向佇列 */ + // 在 C# 中,將鏈結串列 LinkedList 看作雙向佇列來使用 + LinkedList deque = new(); + + /* 元素入列 */ + deque.AddLast(2); // 新增至佇列尾 + deque.AddLast(5); + deque.AddLast(4); + deque.AddFirst(3); // 新增至佇列首 + deque.AddFirst(1); + + /* 訪問元素 */ + int peekFirst = deque.First.Value; // 佇列首元素 + int peekLast = deque.Last.Value; // 佇列尾元素 + + /* 元素出列 */ + deque.RemoveFirst(); // 佇列首元素出列 + deque.RemoveLast(); // 佇列尾元素出列 + + /* 獲取雙向佇列的長度 */ + int size = deque.Count; + + /* 判斷雙向佇列是否為空 */ + bool isEmpty = deque.Count == 0; + ``` + +=== "Go" + + ```go title="deque_test.go" + /* 初始化雙向佇列 */ + // 在 Go 中,將 list 作為雙向佇列使用 + deque := list.New() + + /* 元素入列 */ + deque.PushBack(2) // 新增至佇列尾 + deque.PushBack(5) + deque.PushBack(4) + deque.PushFront(3) // 新增至佇列首 + deque.PushFront(1) + + /* 訪問元素 */ + front := deque.Front() // 佇列首元素 + rear := deque.Back() // 佇列尾元素 + + /* 元素出列 */ + deque.Remove(front) // 佇列首元素出列 + deque.Remove(rear) // 佇列尾元素出列 + + /* 獲取雙向佇列的長度 */ + size := deque.Len() + + /* 判斷雙向佇列是否為空 */ + isEmpty := deque.Len() == 0 + ``` + +=== "Swift" + + ```swift title="deque.swift" + /* 初始化雙向佇列 */ + // Swift 沒有內建的雙向佇列類別,可以把 Array 當作雙向佇列來使用 + var deque: [Int] = [] + + /* 元素入列 */ + deque.append(2) // 新增至佇列尾 + deque.append(5) + deque.append(4) + deque.insert(3, at: 0) // 新增至佇列首 + deque.insert(1, at: 0) + + /* 訪問元素 */ + let peekFirst = deque.first! // 佇列首元素 + let peekLast = deque.last! // 佇列尾元素 + + /* 元素出列 */ + // 使用 Array 模擬時 popFirst 的複雜度為 O(n) + let popFirst = deque.removeFirst() // 佇列首元素出列 + let popLast = deque.removeLast() // 佇列尾元素出列 + + /* 獲取雙向佇列的長度 */ + let size = deque.count + + /* 判斷雙向佇列是否為空 */ + let isEmpty = deque.isEmpty + ``` + +=== "JS" + + ```javascript title="deque.js" + /* 初始化雙向佇列 */ + // JavaScript 沒有內建的雙端佇列,只能把 Array 當作雙端佇列來使用 + const deque = []; + + /* 元素入列 */ + deque.push(2); + deque.push(5); + deque.push(4); + // 請注意,由於是陣列,unshift() 方法的時間複雜度為 O(n) + deque.unshift(3); + deque.unshift(1); + + /* 訪問元素 */ + const peekFirst = deque[0]; + const peekLast = deque[deque.length - 1]; + + /* 元素出列 */ + // 請注意,由於是陣列,shift() 方法的時間複雜度為 O(n) + const popFront = deque.shift(); + const popBack = deque.pop(); + + /* 獲取雙向佇列的長度 */ + const size = deque.length; + + /* 判斷雙向佇列是否為空 */ + const isEmpty = size === 0; + ``` + +=== "TS" + + ```typescript title="deque.ts" + /* 初始化雙向佇列 */ + // TypeScript 沒有內建的雙端佇列,只能把 Array 當作雙端佇列來使用 + const deque: number[] = []; + + /* 元素入列 */ + deque.push(2); + deque.push(5); + deque.push(4); + // 請注意,由於是陣列,unshift() 方法的時間複雜度為 O(n) + deque.unshift(3); + deque.unshift(1); + + /* 訪問元素 */ + const peekFirst: number = deque[0]; + const peekLast: number = deque[deque.length - 1]; + + /* 元素出列 */ + // 請注意,由於是陣列,shift() 方法的時間複雜度為 O(n) + const popFront: number = deque.shift() as number; + const popBack: number = deque.pop() as number; + + /* 獲取雙向佇列的長度 */ + const size: number = deque.length; + + /* 判斷雙向佇列是否為空 */ + const isEmpty: boolean = size === 0; + ``` + +=== "Dart" + + ```dart title="deque.dart" + /* 初始化雙向佇列 */ + // 在 Dart 中,Queue 被定義為雙向佇列 + Queue deque = Queue(); + + /* 元素入列 */ + deque.addLast(2); // 新增至佇列尾 + deque.addLast(5); + deque.addLast(4); + deque.addFirst(3); // 新增至佇列首 + deque.addFirst(1); + + /* 訪問元素 */ + int peekFirst = deque.first; // 佇列首元素 + int peekLast = deque.last; // 佇列尾元素 + + /* 元素出列 */ + int popFirst = deque.removeFirst(); // 佇列首元素出列 + int popLast = deque.removeLast(); // 佇列尾元素出列 + + /* 獲取雙向佇列的長度 */ + int size = deque.length; + + /* 判斷雙向佇列是否為空 */ + bool isEmpty = deque.isEmpty; + ``` + +=== "Rust" + + ```rust title="deque.rs" + /* 初始化雙向佇列 */ + let mut deque: VecDeque = VecDeque::new(); + + /* 元素入列 */ + deque.push_back(2); // 新增至佇列尾 + deque.push_back(5); + deque.push_back(4); + deque.push_front(3); // 新增至佇列首 + deque.push_front(1); + + /* 訪問元素 */ + if let Some(front) = deque.front() { // 佇列首元素 + } + if let Some(rear) = deque.back() { // 佇列尾元素 + } + + /* 元素出列 */ + if let Some(pop_front) = deque.pop_front() { // 佇列首元素出列 + } + if let Some(pop_rear) = deque.pop_back() { // 佇列尾元素出列 + } + + /* 獲取雙向佇列的長度 */ + let size = deque.len(); + + /* 判斷雙向佇列是否為空 */ + let is_empty = deque.is_empty(); + ``` + +=== "C" + + ```c title="deque.c" + // C 未提供內建雙向佇列 + ``` + +=== "Kotlin" + + ```kotlin title="deque.kt" + /* 初始化雙向佇列 */ + val deque = LinkedList() + + /* 元素入列 */ + deque.offerLast(2) // 新增至佇列尾 + deque.offerLast(5) + deque.offerLast(4) + deque.offerFirst(3) // 新增至佇列首 + deque.offerFirst(1) + + /* 訪問元素 */ + val peekFirst = deque.peekFirst() // 佇列首元素 + val peekLast = deque.peekLast() // 佇列尾元素 + + /* 元素出列 */ + val popFirst = deque.pollFirst() // 佇列首元素出列 + val popLast = deque.pollLast() // 佇列尾元素出列 + + /* 獲取雙向佇列的長度 */ + val size = deque.size + + /* 判斷雙向佇列是否為空 */ + val isEmpty = deque.isEmpty() + ``` + +=== "Ruby" + + ```ruby title="deque.rb" + + ``` + +=== "Zig" + + ```zig title="deque.zig" + + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 5.3.2   雙向佇列實現 * + +雙向佇列的實現與佇列類似,可以選擇鏈結串列或陣列作為底層資料結構。 + +### 1.   基於雙向鏈結串列的實現 + +回顧上一節內容,我們使用普通單向鏈結串列來實現佇列,因為它可以方便地刪除頭節點(對應出列操作)和在尾節點後新增新節點(對應入列操作)。 + +對於雙向佇列而言,頭部和尾部都可以執行入列和出列操作。換句話說,雙向佇列需要實現另一個對稱方向的操作。為此,我們採用“雙向鏈結串列”作為雙向佇列的底層資料結構。 + +如圖 5-8 所示,我們將雙向鏈結串列的頭節點和尾節點視為雙向佇列的佇列首和佇列尾,同時實現在兩端新增和刪除節點的功能。 + +=== "LinkedListDeque" + ![基於鏈結串列實現雙向佇列的入列出列操作](deque.assets/linkedlist_deque_step1.png){ class="animation-figure" } + +=== "push_last()" + ![linkedlist_deque_push_last](deque.assets/linkedlist_deque_step2_push_last.png){ class="animation-figure" } + +=== "push_first()" + ![linkedlist_deque_push_first](deque.assets/linkedlist_deque_step3_push_first.png){ class="animation-figure" } + +=== "pop_last()" + ![linkedlist_deque_pop_last](deque.assets/linkedlist_deque_step4_pop_last.png){ class="animation-figure" } + +=== "pop_first()" + ![linkedlist_deque_pop_first](deque.assets/linkedlist_deque_step5_pop_first.png){ class="animation-figure" } + +

圖 5-8   基於鏈結串列實現雙向佇列的入列出列操作

+ +實現程式碼如下所示: + +=== "Python" + + ```python title="linkedlist_deque.py" + class ListNode: + """雙向鏈結串列節點""" + + def __init__(self, val: int): + """建構子""" + self.val: int = val + self.next: ListNode | None = None # 後繼節點引用 + self.prev: ListNode | None = None # 前驅節點引用 + + class LinkedListDeque: + """基於雙向鏈結串列實現的雙向佇列""" + + def __init__(self): + """建構子""" + self._front: ListNode | None = None # 頭節點 front + self._rear: ListNode | None = None # 尾節點 rear + self._size: int = 0 # 雙向佇列的長度 + + def size(self) -> int: + """獲取雙向佇列的長度""" + return self._size + + def is_empty(self) -> bool: + """判斷雙向佇列是否為空""" + return self.size() == 0 + + def push(self, num: int, is_front: bool): + """入列操作""" + node = ListNode(num) + # 若鏈結串列為空,則令 front 和 rear 都指向 node + if self.is_empty(): + self._front = self._rear = node + # 佇列首入列操作 + elif is_front: + # 將 node 新增至鏈結串列頭部 + self._front.prev = node + node.next = self._front + self._front = node # 更新頭節點 + # 佇列尾入列操作 + else: + # 將 node 新增至鏈結串列尾部 + self._rear.next = node + node.prev = self._rear + self._rear = node # 更新尾節點 + self._size += 1 # 更新佇列長度 + + def push_first(self, num: int): + """佇列首入列""" + self.push(num, True) + + def push_last(self, num: int): + """佇列尾入列""" + self.push(num, False) + + def pop(self, is_front: bool) -> int: + """出列操作""" + if self.is_empty(): + raise IndexError("雙向佇列為空") + # 佇列首出列操作 + if is_front: + val: int = self._front.val # 暫存頭節點值 + # 刪除頭節點 + fnext: ListNode | None = self._front.next + if fnext != None: + fnext.prev = None + self._front.next = None + self._front = fnext # 更新頭節點 + # 佇列尾出列操作 + else: + val: int = self._rear.val # 暫存尾節點值 + # 刪除尾節點 + rprev: ListNode | None = self._rear.prev + if rprev != None: + rprev.next = None + self._rear.prev = None + self._rear = rprev # 更新尾節點 + self._size -= 1 # 更新佇列長度 + return val + + def pop_first(self) -> int: + """佇列首出列""" + return self.pop(True) + + def pop_last(self) -> int: + """佇列尾出列""" + return self.pop(False) + + def peek_first(self) -> int: + """訪問佇列首元素""" + if self.is_empty(): + raise IndexError("雙向佇列為空") + return self._front.val + + def peek_last(self) -> int: + """訪問佇列尾元素""" + if self.is_empty(): + raise IndexError("雙向佇列為空") + return self._rear.val + + def to_array(self) -> list[int]: + """返回陣列用於列印""" + node = self._front + res = [0] * self.size() + for i in range(self.size()): + res[i] = node.val + node = node.next + return res + ``` + +=== "C++" + + ```cpp title="linkedlist_deque.cpp" + /* 雙向鏈結串列節點 */ + struct DoublyListNode { + int val; // 節點值 + DoublyListNode *next; // 後繼節點指標 + DoublyListNode *prev; // 前驅節點指標 + DoublyListNode(int val) : val(val), prev(nullptr), next(nullptr) { + } + }; + + /* 基於雙向鏈結串列實現的雙向佇列 */ + class LinkedListDeque { + private: + DoublyListNode *front, *rear; // 頭節點 front ,尾節點 rear + int queSize = 0; // 雙向佇列的長度 + + public: + /* 建構子 */ + LinkedListDeque() : front(nullptr), rear(nullptr) { + } + + /* 析構方法 */ + ~LinkedListDeque() { + // 走訪鏈結串列刪除節點,釋放記憶體 + DoublyListNode *pre, *cur = front; + while (cur != nullptr) { + pre = cur; + cur = cur->next; + delete pre; + } + } + + /* 獲取雙向佇列的長度 */ + int size() { + return queSize; + } + + /* 判斷雙向佇列是否為空 */ + bool isEmpty() { + return size() == 0; + } + + /* 入列操作 */ + void push(int num, bool isFront) { + DoublyListNode *node = new DoublyListNode(num); + // 若鏈結串列為空,則令 front 和 rear 都指向 node + if (isEmpty()) + front = rear = node; + // 佇列首入列操作 + else if (isFront) { + // 將 node 新增至鏈結串列頭部 + front->prev = node; + node->next = front; + front = node; // 更新頭節點 + // 佇列尾入列操作 + } else { + // 將 node 新增至鏈結串列尾部 + rear->next = node; + node->prev = rear; + rear = node; // 更新尾節點 + } + queSize++; // 更新佇列長度 + } + + /* 佇列首入列 */ + void pushFirst(int num) { + push(num, true); + } + + /* 佇列尾入列 */ + void pushLast(int num) { + push(num, false); + } + + /* 出列操作 */ + int pop(bool isFront) { + if (isEmpty()) + throw out_of_range("佇列為空"); + int val; + // 佇列首出列操作 + if (isFront) { + val = front->val; // 暫存頭節點值 + // 刪除頭節點 + DoublyListNode *fNext = front->next; + if (fNext != nullptr) { + fNext->prev = nullptr; + front->next = nullptr; + } + delete front; + front = fNext; // 更新頭節點 + // 佇列尾出列操作 + } else { + val = rear->val; // 暫存尾節點值 + // 刪除尾節點 + DoublyListNode *rPrev = rear->prev; + if (rPrev != nullptr) { + rPrev->next = nullptr; + rear->prev = nullptr; + } + delete rear; + rear = rPrev; // 更新尾節點 + } + queSize--; // 更新佇列長度 + return val; + } + + /* 佇列首出列 */ + int popFirst() { + return pop(true); + } + + /* 佇列尾出列 */ + int popLast() { + return pop(false); + } + + /* 訪問佇列首元素 */ + int peekFirst() { + if (isEmpty()) + throw out_of_range("雙向佇列為空"); + return front->val; + } + + /* 訪問佇列尾元素 */ + int peekLast() { + if (isEmpty()) + throw out_of_range("雙向佇列為空"); + return rear->val; + } + + /* 返回陣列用於列印 */ + vector toVector() { + DoublyListNode *node = front; + vector res(size()); + for (int i = 0; i < res.size(); i++) { + res[i] = node->val; + node = node->next; + } + return res; + } + }; + ``` + +=== "Java" + + ```java title="linkedlist_deque.java" + /* 雙向鏈結串列節點 */ + class ListNode { + int val; // 節點值 + ListNode next; // 後繼節點引用 + ListNode prev; // 前驅節點引用 + + ListNode(int val) { + this.val = val; + prev = next = null; + } + } + + /* 基於雙向鏈結串列實現的雙向佇列 */ + class LinkedListDeque { + private ListNode front, rear; // 頭節點 front ,尾節點 rear + private int queSize = 0; // 雙向佇列的長度 + + public LinkedListDeque() { + front = rear = null; + } + + /* 獲取雙向佇列的長度 */ + public int size() { + return queSize; + } + + /* 判斷雙向佇列是否為空 */ + public boolean isEmpty() { + return size() == 0; + } + + /* 入列操作 */ + private void push(int num, boolean isFront) { + ListNode node = new ListNode(num); + // 若鏈結串列為空,則令 front 和 rear 都指向 node + if (isEmpty()) + front = rear = node; + // 佇列首入列操作 + else if (isFront) { + // 將 node 新增至鏈結串列頭部 + front.prev = node; + node.next = front; + front = node; // 更新頭節點 + // 佇列尾入列操作 + } else { + // 將 node 新增至鏈結串列尾部 + rear.next = node; + node.prev = rear; + rear = node; // 更新尾節點 + } + queSize++; // 更新佇列長度 + } + + /* 佇列首入列 */ + public void pushFirst(int num) { + push(num, true); + } + + /* 佇列尾入列 */ + public void pushLast(int num) { + push(num, false); + } + + /* 出列操作 */ + private int pop(boolean isFront) { + if (isEmpty()) + throw new IndexOutOfBoundsException(); + int val; + // 佇列首出列操作 + if (isFront) { + val = front.val; // 暫存頭節點值 + // 刪除頭節點 + ListNode fNext = front.next; + if (fNext != null) { + fNext.prev = null; + front.next = null; + } + front = fNext; // 更新頭節點 + // 佇列尾出列操作 + } else { + val = rear.val; // 暫存尾節點值 + // 刪除尾節點 + ListNode rPrev = rear.prev; + if (rPrev != null) { + rPrev.next = null; + rear.prev = null; + } + rear = rPrev; // 更新尾節點 + } + queSize--; // 更新佇列長度 + return val; + } + + /* 佇列首出列 */ + public int popFirst() { + return pop(true); + } + + /* 佇列尾出列 */ + public int popLast() { + return pop(false); + } + + /* 訪問佇列首元素 */ + public int peekFirst() { + if (isEmpty()) + throw new IndexOutOfBoundsException(); + return front.val; + } + + /* 訪問佇列尾元素 */ + public int peekLast() { + if (isEmpty()) + throw new IndexOutOfBoundsException(); + return rear.val; + } + + /* 返回陣列用於列印 */ + public int[] toArray() { + ListNode node = front; + int[] res = new int[size()]; + for (int i = 0; i < res.length; i++) { + res[i] = node.val; + node = node.next; + } + return res; + } + } + ``` + +=== "C#" + + ```csharp title="linkedlist_deque.cs" + /* 雙向鏈結串列節點 */ + class ListNode(int val) { + public int val = val; // 節點值 + public ListNode? next = null; // 後繼節點引用 + public ListNode? prev = null; // 前驅節點引用 + } + + /* 基於雙向鏈結串列實現的雙向佇列 */ + class LinkedListDeque { + ListNode? front, rear; // 頭節點 front, 尾節點 rear + int queSize = 0; // 雙向佇列的長度 + + public LinkedListDeque() { + front = null; + rear = null; + } + + /* 獲取雙向佇列的長度 */ + public int Size() { + return queSize; + } + + /* 判斷雙向佇列是否為空 */ + public bool IsEmpty() { + return Size() == 0; + } + + /* 入列操作 */ + void Push(int num, bool isFront) { + ListNode node = new(num); + // 若鏈結串列為空,則令 front 和 rear 都指向 node + if (IsEmpty()) { + front = node; + rear = node; + } + // 佇列首入列操作 + else if (isFront) { + // 將 node 新增至鏈結串列頭部 + front!.prev = node; + node.next = front; + front = node; // 更新頭節點 + } + // 佇列尾入列操作 + else { + // 將 node 新增至鏈結串列尾部 + rear!.next = node; + node.prev = rear; + rear = node; // 更新尾節點 + } + + queSize++; // 更新佇列長度 + } + + /* 佇列首入列 */ + public void PushFirst(int num) { + Push(num, true); + } + + /* 佇列尾入列 */ + public void PushLast(int num) { + Push(num, false); + } + + /* 出列操作 */ + int? Pop(bool isFront) { + if (IsEmpty()) + throw new Exception(); + int? val; + // 佇列首出列操作 + if (isFront) { + val = front?.val; // 暫存頭節點值 + // 刪除頭節點 + ListNode? fNext = front?.next; + if (fNext != null) { + fNext.prev = null; + front!.next = null; + } + front = fNext; // 更新頭節點 + } + // 佇列尾出列操作 + else { + val = rear?.val; // 暫存尾節點值 + // 刪除尾節點 + ListNode? rPrev = rear?.prev; + if (rPrev != null) { + rPrev.next = null; + rear!.prev = null; + } + rear = rPrev; // 更新尾節點 + } + + queSize--; // 更新佇列長度 + return val; + } + + /* 佇列首出列 */ + public int? PopFirst() { + return Pop(true); + } + + /* 佇列尾出列 */ + public int? PopLast() { + return Pop(false); + } + + /* 訪問佇列首元素 */ + public int? PeekFirst() { + if (IsEmpty()) + throw new Exception(); + return front?.val; + } + + /* 訪問佇列尾元素 */ + public int? PeekLast() { + if (IsEmpty()) + throw new Exception(); + return rear?.val; + } + + /* 返回陣列用於列印 */ + public int?[] ToArray() { + ListNode? node = front; + int?[] res = new int?[Size()]; + for (int i = 0; i < res.Length; i++) { + res[i] = node?.val; + node = node?.next; + } + + return res; + } + } + ``` + +=== "Go" + + ```go title="linkedlist_deque.go" + /* 基於雙向鏈結串列實現的雙向佇列 */ + type linkedListDeque struct { + // 使用內建包 list + data *list.List + } + + /* 初始化雙端佇列 */ + func newLinkedListDeque() *linkedListDeque { + return &linkedListDeque{ + data: list.New(), + } + } + + /* 佇列首元素入列 */ + func (s *linkedListDeque) pushFirst(value any) { + s.data.PushFront(value) + } + + /* 佇列尾元素入列 */ + func (s *linkedListDeque) pushLast(value any) { + s.data.PushBack(value) + } + + /* 佇列首元素出列 */ + func (s *linkedListDeque) popFirst() any { + if s.isEmpty() { + return nil + } + e := s.data.Front() + s.data.Remove(e) + return e.Value + } + + /* 佇列尾元素出列 */ + func (s *linkedListDeque) popLast() any { + if s.isEmpty() { + return nil + } + e := s.data.Back() + s.data.Remove(e) + return e.Value + } + + /* 訪問佇列首元素 */ + func (s *linkedListDeque) peekFirst() any { + if s.isEmpty() { + return nil + } + e := s.data.Front() + return e.Value + } + + /* 訪問佇列尾元素 */ + func (s *linkedListDeque) peekLast() any { + if s.isEmpty() { + return nil + } + e := s.data.Back() + return e.Value + } + + /* 獲取佇列的長度 */ + func (s *linkedListDeque) size() int { + return s.data.Len() + } + + /* 判斷佇列是否為空 */ + func (s *linkedListDeque) isEmpty() bool { + return s.data.Len() == 0 + } + + /* 獲取 List 用於列印 */ + func (s *linkedListDeque) toList() *list.List { + return s.data + } + ``` + +=== "Swift" + + ```swift title="linkedlist_deque.swift" + /* 雙向鏈結串列節點 */ + class ListNode { + var val: Int // 節點值 + var next: ListNode? // 後繼節點引用 + weak var prev: ListNode? // 前驅節點引用 + + init(val: Int) { + self.val = val + } + } + + /* 基於雙向鏈結串列實現的雙向佇列 */ + class LinkedListDeque { + private var front: ListNode? // 頭節點 front + private var rear: ListNode? // 尾節點 rear + private var _size: Int // 雙向佇列的長度 + + init() { + _size = 0 + } + + /* 獲取雙向佇列的長度 */ + func size() -> Int { + _size + } + + /* 判斷雙向佇列是否為空 */ + func isEmpty() -> Bool { + size() == 0 + } + + /* 入列操作 */ + private func push(num: Int, isFront: Bool) { + let node = ListNode(val: num) + // 若鏈結串列為空,則令 front 和 rear 都指向 node + if isEmpty() { + front = node + rear = node + } + // 佇列首入列操作 + else if isFront { + // 將 node 新增至鏈結串列頭部 + front?.prev = node + node.next = front + front = node // 更新頭節點 + } + // 佇列尾入列操作 + else { + // 將 node 新增至鏈結串列尾部 + rear?.next = node + node.prev = rear + rear = node // 更新尾節點 + } + _size += 1 // 更新佇列長度 + } + + /* 佇列首入列 */ + func pushFirst(num: Int) { + push(num: num, isFront: true) + } + + /* 佇列尾入列 */ + func pushLast(num: Int) { + push(num: num, isFront: false) + } + + /* 出列操作 */ + private func pop(isFront: Bool) -> Int { + if isEmpty() { + fatalError("雙向佇列為空") + } + let val: Int + // 佇列首出列操作 + if isFront { + val = front!.val // 暫存頭節點值 + // 刪除頭節點 + let fNext = front?.next + if fNext != nil { + fNext?.prev = nil + front?.next = nil + } + front = fNext // 更新頭節點 + } + // 佇列尾出列操作 + else { + val = rear!.val // 暫存尾節點值 + // 刪除尾節點 + let rPrev = rear?.prev + if rPrev != nil { + rPrev?.next = nil + rear?.prev = nil + } + rear = rPrev // 更新尾節點 + } + _size -= 1 // 更新佇列長度 + return val + } + + /* 佇列首出列 */ + func popFirst() -> Int { + pop(isFront: true) + } + + /* 佇列尾出列 */ + func popLast() -> Int { + pop(isFront: false) + } + + /* 訪問佇列首元素 */ + func peekFirst() -> Int { + if isEmpty() { + fatalError("雙向佇列為空") + } + return front!.val + } + + /* 訪問佇列尾元素 */ + func peekLast() -> Int { + if isEmpty() { + fatalError("雙向佇列為空") + } + return rear!.val + } + + /* 返回陣列用於列印 */ + func toArray() -> [Int] { + var node = front + var res = Array(repeating: 0, count: size()) + for i in res.indices { + res[i] = node!.val + node = node?.next + } + return res + } + } + ``` + +=== "JS" + + ```javascript title="linkedlist_deque.js" + /* 雙向鏈結串列節點 */ + class ListNode { + prev; // 前驅節點引用 (指標) + next; // 後繼節點引用 (指標) + val; // 節點值 + + constructor(val) { + this.val = val; + this.next = null; + this.prev = null; + } + } + + /* 基於雙向鏈結串列實現的雙向佇列 */ + class LinkedListDeque { + #front; // 頭節點 front + #rear; // 尾節點 rear + #queSize; // 雙向佇列的長度 + + constructor() { + this.#front = null; + this.#rear = null; + this.#queSize = 0; + } + + /* 佇列尾入列操作 */ + pushLast(val) { + const node = new ListNode(val); + // 若鏈結串列為空,則令 front 和 rear 都指向 node + if (this.#queSize === 0) { + this.#front = node; + this.#rear = node; + } else { + // 將 node 新增至鏈結串列尾部 + this.#rear.next = node; + node.prev = this.#rear; + this.#rear = node; // 更新尾節點 + } + this.#queSize++; + } + + /* 佇列首入列操作 */ + pushFirst(val) { + const node = new ListNode(val); + // 若鏈結串列為空,則令 front 和 rear 都指向 node + if (this.#queSize === 0) { + this.#front = node; + this.#rear = node; + } else { + // 將 node 新增至鏈結串列頭部 + this.#front.prev = node; + node.next = this.#front; + this.#front = node; // 更新頭節點 + } + this.#queSize++; + } + + /* 佇列尾出列操作 */ + popLast() { + if (this.#queSize === 0) { + return null; + } + const value = this.#rear.val; // 儲存尾節點值 + // 刪除尾節點 + let temp = this.#rear.prev; + if (temp !== null) { + temp.next = null; + this.#rear.prev = null; + } + this.#rear = temp; // 更新尾節點 + this.#queSize--; + return value; + } + + /* 佇列首出列操作 */ + popFirst() { + if (this.#queSize === 0) { + return null; + } + const value = this.#front.val; // 儲存尾節點值 + // 刪除頭節點 + let temp = this.#front.next; + if (temp !== null) { + temp.prev = null; + this.#front.next = null; + } + this.#front = temp; // 更新頭節點 + this.#queSize--; + return value; + } + + /* 訪問佇列尾元素 */ + peekLast() { + return this.#queSize === 0 ? null : this.#rear.val; + } + + /* 訪問佇列首元素 */ + peekFirst() { + return this.#queSize === 0 ? null : this.#front.val; + } + + /* 獲取雙向佇列的長度 */ + size() { + return this.#queSize; + } + + /* 判斷雙向佇列是否為空 */ + isEmpty() { + return this.#queSize === 0; + } + + /* 列印雙向佇列 */ + print() { + const arr = []; + let temp = this.#front; + while (temp !== null) { + arr.push(temp.val); + temp = temp.next; + } + console.log('[' + arr.join(', ') + ']'); + } + } + ``` + +=== "TS" + + ```typescript title="linkedlist_deque.ts" + /* 雙向鏈結串列節點 */ + class ListNode { + prev: ListNode; // 前驅節點引用 (指標) + next: ListNode; // 後繼節點引用 (指標) + val: number; // 節點值 + + constructor(val: number) { + this.val = val; + this.next = null; + this.prev = null; + } + } + + /* 基於雙向鏈結串列實現的雙向佇列 */ + class LinkedListDeque { + private front: ListNode; // 頭節點 front + private rear: ListNode; // 尾節點 rear + private queSize: number; // 雙向佇列的長度 + + constructor() { + this.front = null; + this.rear = null; + this.queSize = 0; + } + + /* 佇列尾入列操作 */ + pushLast(val: number): void { + const node: ListNode = new ListNode(val); + // 若鏈結串列為空,則令 front 和 rear 都指向 node + if (this.queSize === 0) { + this.front = node; + this.rear = node; + } else { + // 將 node 新增至鏈結串列尾部 + this.rear.next = node; + node.prev = this.rear; + this.rear = node; // 更新尾節點 + } + this.queSize++; + } + + /* 佇列首入列操作 */ + pushFirst(val: number): void { + const node: ListNode = new ListNode(val); + // 若鏈結串列為空,則令 front 和 rear 都指向 node + if (this.queSize === 0) { + this.front = node; + this.rear = node; + } else { + // 將 node 新增至鏈結串列頭部 + this.front.prev = node; + node.next = this.front; + this.front = node; // 更新頭節點 + } + this.queSize++; + } + + /* 佇列尾出列操作 */ + popLast(): number { + if (this.queSize === 0) { + return null; + } + const value: number = this.rear.val; // 儲存尾節點值 + // 刪除尾節點 + let temp: ListNode = this.rear.prev; + if (temp !== null) { + temp.next = null; + this.rear.prev = null; + } + this.rear = temp; // 更新尾節點 + this.queSize--; + return value; + } + + /* 佇列首出列操作 */ + popFirst(): number { + if (this.queSize === 0) { + return null; + } + const value: number = this.front.val; // 儲存尾節點值 + // 刪除頭節點 + let temp: ListNode = this.front.next; + if (temp !== null) { + temp.prev = null; + this.front.next = null; + } + this.front = temp; // 更新頭節點 + this.queSize--; + return value; + } + + /* 訪問佇列尾元素 */ + peekLast(): number { + return this.queSize === 0 ? null : this.rear.val; + } + + /* 訪問佇列首元素 */ + peekFirst(): number { + return this.queSize === 0 ? null : this.front.val; + } + + /* 獲取雙向佇列的長度 */ + size(): number { + return this.queSize; + } + + /* 判斷雙向佇列是否為空 */ + isEmpty(): boolean { + return this.queSize === 0; + } + + /* 列印雙向佇列 */ + print(): void { + const arr: number[] = []; + let temp: ListNode = this.front; + while (temp !== null) { + arr.push(temp.val); + temp = temp.next; + } + console.log('[' + arr.join(', ') + ']'); + } + } + ``` + +=== "Dart" + + ```dart title="linkedlist_deque.dart" + /* 雙向鏈結串列節點 */ + class ListNode { + int val; // 節點值 + ListNode? next; // 後繼節點引用 + ListNode? prev; // 前驅節點引用 + + ListNode(this.val, {this.next, this.prev}); + } + + /* 基於雙向鏈結串列實現的雙向對列 */ + class LinkedListDeque { + late ListNode? _front; // 頭節點 _front + late ListNode? _rear; // 尾節點 _rear + int _queSize = 0; // 雙向佇列的長度 + + LinkedListDeque() { + this._front = null; + this._rear = null; + } + + /* 獲取雙向佇列長度 */ + int size() { + return this._queSize; + } + + /* 判斷雙向佇列是否為空 */ + bool isEmpty() { + return size() == 0; + } + + /* 入列操作 */ + void push(int _num, bool isFront) { + final ListNode node = ListNode(_num); + if (isEmpty()) { + // 若鏈結串列為空,則令 _front 和 _rear 都指向 node + _front = _rear = node; + } else if (isFront) { + // 佇列首入列操作 + // 將 node 新增至鏈結串列頭部 + _front!.prev = node; + node.next = _front; + _front = node; // 更新頭節點 + } else { + // 佇列尾入列操作 + // 將 node 新增至鏈結串列尾部 + _rear!.next = node; + node.prev = _rear; + _rear = node; // 更新尾節點 + } + _queSize++; // 更新佇列長度 + } + + /* 佇列首入列 */ + void pushFirst(int _num) { + push(_num, true); + } + + /* 佇列尾入列 */ + void pushLast(int _num) { + push(_num, false); + } + + /* 出列操作 */ + int? pop(bool isFront) { + // 若佇列為空,直接返回 null + if (isEmpty()) { + return null; + } + final int val; + if (isFront) { + // 佇列首出列操作 + val = _front!.val; // 暫存頭節點值 + // 刪除頭節點 + ListNode? fNext = _front!.next; + if (fNext != null) { + fNext.prev = null; + _front!.next = null; + } + _front = fNext; // 更新頭節點 + } else { + // 佇列尾出列操作 + val = _rear!.val; // 暫存尾節點值 + // 刪除尾節點 + ListNode? rPrev = _rear!.prev; + if (rPrev != null) { + rPrev.next = null; + _rear!.prev = null; + } + _rear = rPrev; // 更新尾節點 + } + _queSize--; // 更新佇列長度 + return val; + } + + /* 佇列首出列 */ + int? popFirst() { + return pop(true); + } + + /* 佇列尾出列 */ + int? popLast() { + return pop(false); + } + + /* 訪問佇列首元素 */ + int? peekFirst() { + return _front?.val; + } + + /* 訪問佇列尾元素 */ + int? peekLast() { + return _rear?.val; + } + + /* 返回陣列用於列印 */ + List toArray() { + ListNode? node = _front; + final List res = []; + for (int i = 0; i < _queSize; i++) { + res.add(node!.val); + node = node.next; + } + return res; + } + } + ``` + +=== "Rust" + + ```rust title="linkedlist_deque.rs" + /* 雙向鏈結串列節點 */ + pub struct ListNode { + pub val: T, // 節點值 + pub next: Option>>>, // 後繼節點指標 + pub prev: Option>>>, // 前驅節點指標 + } + + impl ListNode { + pub fn new(val: T) -> Rc>> { + Rc::new(RefCell::new(ListNode { + val, + next: None, + prev: None, + })) + } + } + + /* 基於雙向鏈結串列實現的雙向佇列 */ + #[allow(dead_code)] + pub struct LinkedListDeque { + front: Option>>>, // 頭節點 front + rear: Option>>>, // 尾節點 rear + que_size: usize, // 雙向佇列的長度 + } + + impl LinkedListDeque { + pub fn new() -> Self { + Self { + front: None, + rear: None, + que_size: 0, + } + } + + /* 獲取雙向佇列的長度 */ + pub fn size(&self) -> usize { + return self.que_size; + } + + /* 判斷雙向佇列是否為空 */ + pub fn is_empty(&self) -> bool { + return self.size() == 0; + } + + /* 入列操作 */ + pub fn push(&mut self, num: T, is_front: bool) { + let node = ListNode::new(num); + // 佇列首入列操作 + if is_front { + match self.front.take() { + // 若鏈結串列為空,則令 front 和 rear 都指向 node + None => { + self.rear = Some(node.clone()); + self.front = Some(node); + } + // 將 node 新增至鏈結串列頭部 + Some(old_front) => { + old_front.borrow_mut().prev = Some(node.clone()); + node.borrow_mut().next = Some(old_front); + self.front = Some(node); // 更新頭節點 + } + } + } + // 佇列尾入列操作 + else { + match self.rear.take() { + // 若鏈結串列為空,則令 front 和 rear 都指向 node + None => { + self.front = Some(node.clone()); + self.rear = Some(node); + } + // 將 node 新增至鏈結串列尾部 + Some(old_rear) => { + old_rear.borrow_mut().next = Some(node.clone()); + node.borrow_mut().prev = Some(old_rear); + self.rear = Some(node); // 更新尾節點 + } + } + } + self.que_size += 1; // 更新佇列長度 + } + + /* 佇列首入列 */ + pub fn push_first(&mut self, num: T) { + self.push(num, true); + } + + /* 佇列尾入列 */ + pub fn push_last(&mut self, num: T) { + self.push(num, false); + } + + /* 出列操作 */ + pub fn pop(&mut self, is_front: bool) -> Option { + // 若佇列為空,直接返回 None + if self.is_empty() { + return None; + }; + // 佇列首出列操作 + if is_front { + self.front.take().map(|old_front| { + match old_front.borrow_mut().next.take() { + Some(new_front) => { + new_front.borrow_mut().prev.take(); + self.front = Some(new_front); // 更新頭節點 + } + None => { + self.rear.take(); + } + } + self.que_size -= 1; // 更新佇列長度 + Rc::try_unwrap(old_front).ok().unwrap().into_inner().val + }) + } + // 佇列尾出列操作 + else { + self.rear.take().map(|old_rear| { + match old_rear.borrow_mut().prev.take() { + Some(new_rear) => { + new_rear.borrow_mut().next.take(); + self.rear = Some(new_rear); // 更新尾節點 + } + None => { + self.front.take(); + } + } + self.que_size -= 1; // 更新佇列長度 + Rc::try_unwrap(old_rear).ok().unwrap().into_inner().val + }) + } + } + + /* 佇列首出列 */ + pub fn pop_first(&mut self) -> Option { + return self.pop(true); + } + + /* 佇列尾出列 */ + pub fn pop_last(&mut self) -> Option { + return self.pop(false); + } + + /* 訪問佇列首元素 */ + pub fn peek_first(&self) -> Option<&Rc>>> { + self.front.as_ref() + } + + /* 訪問佇列尾元素 */ + pub fn peek_last(&self) -> Option<&Rc>>> { + self.rear.as_ref() + } + + /* 返回陣列用於列印 */ + pub fn to_array(&self, head: Option<&Rc>>>) -> Vec { + if let Some(node) = head { + let mut nums = self.to_array(node.borrow().next.as_ref()); + nums.insert(0, node.borrow().val); + return nums; + } + return Vec::new(); + } + } + ``` + +=== "C" + + ```c title="linkedlist_deque.c" + /* 雙向鏈結串列節點 */ + typedef struct DoublyListNode { + int val; // 節點值 + struct DoublyListNode *next; // 後繼節點 + struct DoublyListNode *prev; // 前驅節點 + } DoublyListNode; + + /* 建構子 */ + DoublyListNode *newDoublyListNode(int num) { + DoublyListNode *new = (DoublyListNode *)malloc(sizeof(DoublyListNode)); + new->val = num; + new->next = NULL; + new->prev = NULL; + return new; + } + + /* 析構函式 */ + void delDoublyListNode(DoublyListNode *node) { + free(node); + } + + /* 基於雙向鏈結串列實現的雙向佇列 */ + typedef struct { + DoublyListNode *front, *rear; // 頭節點 front ,尾節點 rear + int queSize; // 雙向佇列的長度 + } LinkedListDeque; + + /* 建構子 */ + LinkedListDeque *newLinkedListDeque() { + LinkedListDeque *deque = (LinkedListDeque *)malloc(sizeof(LinkedListDeque)); + deque->front = NULL; + deque->rear = NULL; + deque->queSize = 0; + return deque; + } + + /* 析構函式 */ + void delLinkedListdeque(LinkedListDeque *deque) { + // 釋放所有節點 + for (int i = 0; i < deque->queSize && deque->front != NULL; i++) { + DoublyListNode *tmp = deque->front; + deque->front = deque->front->next; + free(tmp); + } + // 釋放 deque 結構體 + free(deque); + } + + /* 獲取佇列的長度 */ + int size(LinkedListDeque *deque) { + return deque->queSize; + } + + /* 判斷佇列是否為空 */ + bool empty(LinkedListDeque *deque) { + return (size(deque) == 0); + } + + /* 入列 */ + void push(LinkedListDeque *deque, int num, bool isFront) { + DoublyListNode *node = newDoublyListNode(num); + // 若鏈結串列為空,則令 front 和 rear 都指向node + if (empty(deque)) { + deque->front = deque->rear = node; + } + // 佇列首入列操作 + else if (isFront) { + // 將 node 新增至鏈結串列頭部 + deque->front->prev = node; + node->next = deque->front; + deque->front = node; // 更新頭節點 + } + // 佇列尾入列操作 + else { + // 將 node 新增至鏈結串列尾部 + deque->rear->next = node; + node->prev = deque->rear; + deque->rear = node; + } + deque->queSize++; // 更新佇列長度 + } + + /* 佇列首入列 */ + void pushFirst(LinkedListDeque *deque, int num) { + push(deque, num, true); + } + + /* 佇列尾入列 */ + void pushLast(LinkedListDeque *deque, int num) { + push(deque, num, false); + } + + /* 訪問佇列首元素 */ + int peekFirst(LinkedListDeque *deque) { + assert(size(deque) && deque->front); + return deque->front->val; + } + + /* 訪問佇列尾元素 */ + int peekLast(LinkedListDeque *deque) { + assert(size(deque) && deque->rear); + return deque->rear->val; + } + + /* 出列 */ + int pop(LinkedListDeque *deque, bool isFront) { + if (empty(deque)) + return -1; + int val; + // 佇列首出列操作 + if (isFront) { + val = peekFirst(deque); // 暫存頭節點值 + DoublyListNode *fNext = deque->front->next; + if (fNext) { + fNext->prev = NULL; + deque->front->next = NULL; + delDoublyListNode(deque->front); + } + deque->front = fNext; // 更新頭節點 + } + // 佇列尾出列操作 + else { + val = peekLast(deque); // 暫存尾節點值 + DoublyListNode *rPrev = deque->rear->prev; + if (rPrev) { + rPrev->next = NULL; + deque->rear->prev = NULL; + delDoublyListNode(deque->rear); + } + deque->rear = rPrev; // 更新尾節點 + } + deque->queSize--; // 更新佇列長度 + return val; + } + + /* 佇列首出列 */ + int popFirst(LinkedListDeque *deque) { + return pop(deque, true); + } + + /* 佇列尾出列 */ + int popLast(LinkedListDeque *deque) { + return pop(deque, false); + } + + /* 列印佇列 */ + void printLinkedListDeque(LinkedListDeque *deque) { + int *arr = malloc(sizeof(int) * deque->queSize); + // 複製鏈結串列中的資料到陣列 + int i; + DoublyListNode *node; + for (i = 0, node = deque->front; i < deque->queSize; i++) { + arr[i] = node->val; + node = node->next; + } + printArray(arr, deque->queSize); + free(arr); + } + ``` + +=== "Kotlin" + + ```kotlin title="linkedlist_deque.kt" + /* 雙向鏈結串列節點 */ + class ListNode(var value: Int) { + // 節點值 + var next: ListNode? = null // 後繼節點引用 + var prev: ListNode? = null // 前驅節點引用 + } + + /* 基於雙向鏈結串列實現的雙向佇列 */ + class LinkedListDeque { + private var front: ListNode? = null // 頭節點 front ,尾節點 rear + private var rear: ListNode? = null + private var queSize = 0 // 雙向佇列的長度 + + /* 獲取雙向佇列的長度 */ + fun size(): Int { + return queSize + } + + /* 判斷雙向佇列是否為空 */ + fun isEmpty(): Boolean { + return size() == 0 + } + + /* 入列操作 */ + fun push(num: Int, isFront: Boolean) { + val node = ListNode(num) + // 若鏈結串列為空,則令 front 和 rear 都指向 node + if (isEmpty()) { + rear = node + front = rear + // 佇列首入列操作 + } else if (isFront) { + // 將 node 新增至鏈結串列頭部 + front?.prev = node + node.next = front + front = node // 更新頭節點 + // 佇列尾入列操作 + } else { + // 將 node 新增至鏈結串列尾部 + rear?.next = node + node.prev = rear + rear = node // 更新尾節點 + } + queSize++ // 更新佇列長度 + } + + /* 佇列首入列 */ + fun pushFirst(num: Int) { + push(num, true) + } + + /* 佇列尾入列 */ + fun pushLast(num: Int) { + push(num, false) + } + + /* 出列操作 */ + fun pop(isFront: Boolean): Int { + if (isEmpty()) throw IndexOutOfBoundsException() + + val value: Int + // 佇列首出列操作 + if (isFront) { + value = front!!.value // 暫存頭節點值 + // 刪除頭節點 + val fNext = front!!.next + if (fNext != null) { + fNext.prev = null + front!!.next = null + } + front = fNext // 更新頭節點 + // 佇列尾出列操作 + } else { + value = rear!!.value // 暫存尾節點值 + // 刪除尾節點 + val rPrev = rear!!.prev + if (rPrev != null) { + rPrev.next = null + rear!!.prev = null + } + rear = rPrev // 更新尾節點 + } + queSize-- // 更新佇列長度 + return value + } + + /* 佇列首出列 */ + fun popFirst(): Int { + return pop(true) + } + + /* 佇列尾出列 */ + fun popLast(): Int { + return pop(false) + } + + /* 訪問佇列首元素 */ + fun peekFirst(): Int { + if (isEmpty()) { + throw IndexOutOfBoundsException() + + } + return front!!.value + } + + /* 訪問佇列尾元素 */ + fun peekLast(): Int { + if (isEmpty()) throw IndexOutOfBoundsException() + return rear!!.value + } + + /* 返回陣列用於列印 */ + fun toArray(): IntArray { + var node = front + val res = IntArray(size()) + for (i in res.indices) { + res[i] = node!!.value + node = node.next + } + return res + } + } + ``` + +=== "Ruby" + + ```ruby title="linkedlist_deque.rb" + [class]{ListNode}-[func]{} + + [class]{LinkedListDeque}-[func]{} + ``` + +=== "Zig" + + ```zig title="linkedlist_deque.zig" + // 雙向鏈結串列節點 + fn ListNode(comptime T: type) type { + return struct { + const Self = @This(); + + val: T = undefined, // 節點值 + next: ?*Self = null, // 後繼節點指標 + prev: ?*Self = null, // 前驅節點指標 + + // Initialize a list node with specific value + pub fn init(self: *Self, x: i32) void { + self.val = x; + self.next = null; + self.prev = null; + } + }; + } + + // 基於雙向鏈結串列實現的雙向佇列 + fn LinkedListDeque(comptime T: type) type { + return struct { + const Self = @This(); + + front: ?*ListNode(T) = null, // 頭節點 front + rear: ?*ListNode(T) = null, // 尾節點 rear + que_size: usize = 0, // 雙向佇列的長度 + mem_arena: ?std.heap.ArenaAllocator = null, + mem_allocator: std.mem.Allocator = undefined, // 記憶體分配器 + + // 建構子(分配記憶體+初始化佇列) + pub fn init(self: *Self, allocator: std.mem.Allocator) !void { + if (self.mem_arena == null) { + self.mem_arena = std.heap.ArenaAllocator.init(allocator); + self.mem_allocator = self.mem_arena.?.allocator(); + } + self.front = null; + self.rear = null; + self.que_size = 0; + } + + // 析構函式(釋放記憶體) + pub fn deinit(self: *Self) void { + if (self.mem_arena == null) return; + self.mem_arena.?.deinit(); + } + + // 獲取雙向佇列的長度 + pub fn size(self: *Self) usize { + return self.que_size; + } + + // 判斷雙向佇列是否為空 + pub fn isEmpty(self: *Self) bool { + return self.size() == 0; + } + + // 入列操作 + pub fn push(self: *Self, num: T, is_front: bool) !void { + var node = try self.mem_allocator.create(ListNode(T)); + node.init(num); + // 若鏈結串列為空,則令 front 和 rear 都指向 node + if (self.isEmpty()) { + self.front = node; + self.rear = node; + // 佇列首入列操作 + } else if (is_front) { + // 將 node 新增至鏈結串列頭部 + self.front.?.prev = node; + node.next = self.front; + self.front = node; // 更新頭節點 + // 佇列尾入列操作 + } else { + // 將 node 新增至鏈結串列尾部 + self.rear.?.next = node; + node.prev = self.rear; + self.rear = node; // 更新尾節點 + } + self.que_size += 1; // 更新佇列長度 + } + + // 佇列首入列 + pub fn pushFirst(self: *Self, num: T) !void { + try self.push(num, true); + } + + // 佇列尾入列 + pub fn pushLast(self: *Self, num: T) !void { + try self.push(num, false); + } + + // 出列操作 + pub fn pop(self: *Self, is_front: bool) T { + if (self.isEmpty()) @panic("雙向佇列為空"); + var val: T = undefined; + // 佇列首出列操作 + if (is_front) { + val = self.front.?.val; // 暫存頭節點值 + // 刪除頭節點 + var fNext = self.front.?.next; + if (fNext != null) { + fNext.?.prev = null; + self.front.?.next = null; + } + self.front = fNext; // 更新頭節點 + // 佇列尾出列操作 + } else { + val = self.rear.?.val; // 暫存尾節點值 + // 刪除尾節點 + var rPrev = self.rear.?.prev; + if (rPrev != null) { + rPrev.?.next = null; + self.rear.?.prev = null; + } + self.rear = rPrev; // 更新尾節點 + } + self.que_size -= 1; // 更新佇列長度 + return val; + } + + // 佇列首出列 + pub fn popFirst(self: *Self) T { + return self.pop(true); + } + + // 佇列尾出列 + pub fn popLast(self: *Self) T { + return self.pop(false); + } + + // 訪問佇列首元素 + pub fn peekFirst(self: *Self) T { + if (self.isEmpty()) @panic("雙向佇列為空"); + return self.front.?.val; + } + + // 訪問佇列尾元素 + pub fn peekLast(self: *Self) T { + if (self.isEmpty()) @panic("雙向佇列為空"); + return self.rear.?.val; + } + + // 返回陣列用於列印 + pub fn toArray(self: *Self) ![]T { + var node = self.front; + var res = try self.mem_allocator.alloc(T, self.size()); + @memset(res, @as(T, 0)); + var i: usize = 0; + while (i < res.len) : (i += 1) { + res[i] = node.?.val; + node = node.?.next; + } + return res; + } + }; + } + ``` + +### 2.   基於陣列的實現 + +如圖 5-9 所示,與基於陣列實現佇列類似,我們也可以使用環形陣列來實現雙向佇列。 + +=== "ArrayDeque" + ![基於陣列實現雙向佇列的入列出列操作](deque.assets/array_deque_step1.png){ class="animation-figure" } + +=== "push_last()" + ![array_deque_push_last](deque.assets/array_deque_step2_push_last.png){ class="animation-figure" } + +=== "push_first()" + ![array_deque_push_first](deque.assets/array_deque_step3_push_first.png){ class="animation-figure" } + +=== "pop_last()" + ![array_deque_pop_last](deque.assets/array_deque_step4_pop_last.png){ class="animation-figure" } + +=== "pop_first()" + ![array_deque_pop_first](deque.assets/array_deque_step5_pop_first.png){ class="animation-figure" } + +

圖 5-9   基於陣列實現雙向佇列的入列出列操作

+ +在佇列的實現基礎上,僅需增加“佇列首入列”和“佇列尾出列”的方法: + +=== "Python" + + ```python title="array_deque.py" + class ArrayDeque: + """基於環形陣列實現的雙向佇列""" + + def __init__(self, capacity: int): + """建構子""" + self._nums: list[int] = [0] * capacity + self._front: int = 0 + self._size: int = 0 + + def capacity(self) -> int: + """獲取雙向佇列的容量""" + return len(self._nums) + + def size(self) -> int: + """獲取雙向佇列的長度""" + return self._size + + def is_empty(self) -> bool: + """判斷雙向佇列是否為空""" + return self._size == 0 + + def index(self, i: int) -> int: + """計算環形陣列索引""" + # 透過取餘操作實現陣列首尾相連 + # 當 i 越過陣列尾部後,回到頭部 + # 當 i 越過陣列頭部後,回到尾部 + return (i + self.capacity()) % self.capacity() + + def push_first(self, num: int): + """佇列首入列""" + if self._size == self.capacity(): + print("雙向佇列已滿") + return + # 佇列首指標向左移動一位 + # 透過取餘操作實現 front 越過陣列頭部後回到尾部 + self._front = self.index(self._front - 1) + # 將 num 新增至佇列首 + self._nums[self._front] = num + self._size += 1 + + def push_last(self, num: int): + """佇列尾入列""" + if self._size == self.capacity(): + print("雙向佇列已滿") + return + # 計算佇列尾指標,指向佇列尾索引 + 1 + rear = self.index(self._front + self._size) + # 將 num 新增至佇列尾 + self._nums[rear] = num + self._size += 1 + + def pop_first(self) -> int: + """佇列首出列""" + num = self.peek_first() + # 佇列首指標向後移動一位 + self._front = self.index(self._front + 1) + self._size -= 1 + return num + + def pop_last(self) -> int: + """佇列尾出列""" + num = self.peek_last() + self._size -= 1 + return num + + def peek_first(self) -> int: + """訪問佇列首元素""" + if self.is_empty(): + raise IndexError("雙向佇列為空") + return self._nums[self._front] + + def peek_last(self) -> int: + """訪問佇列尾元素""" + if self.is_empty(): + raise IndexError("雙向佇列為空") + # 計算尾元素索引 + last = self.index(self._front + self._size - 1) + return self._nums[last] + + def to_array(self) -> list[int]: + """返回陣列用於列印""" + # 僅轉換有效長度範圍內的串列元素 + res = [] + for i in range(self._size): + res.append(self._nums[self.index(self._front + i)]) + return res + ``` + +=== "C++" + + ```cpp title="array_deque.cpp" + /* 基於環形陣列實現的雙向佇列 */ + class ArrayDeque { + private: + vector nums; // 用於儲存雙向佇列元素的陣列 + int front; // 佇列首指標,指向佇列首元素 + int queSize; // 雙向佇列長度 + + public: + /* 建構子 */ + ArrayDeque(int capacity) { + nums.resize(capacity); + front = queSize = 0; + } + + /* 獲取雙向佇列的容量 */ + int capacity() { + return nums.size(); + } + + /* 獲取雙向佇列的長度 */ + int size() { + return queSize; + } + + /* 判斷雙向佇列是否為空 */ + bool isEmpty() { + return queSize == 0; + } + + /* 計算環形陣列索引 */ + int index(int i) { + // 透過取餘操作實現陣列首尾相連 + // 當 i 越過陣列尾部後,回到頭部 + // 當 i 越過陣列頭部後,回到尾部 + return (i + capacity()) % capacity(); + } + + /* 佇列首入列 */ + void pushFirst(int num) { + if (queSize == capacity()) { + cout << "雙向佇列已滿" << endl; + return; + } + // 佇列首指標向左移動一位 + // 透過取餘操作實現 front 越過陣列頭部後回到尾部 + front = index(front - 1); + // 將 num 新增至佇列首 + nums[front] = num; + queSize++; + } + + /* 佇列尾入列 */ + void pushLast(int num) { + if (queSize == capacity()) { + cout << "雙向佇列已滿" << endl; + return; + } + // 計算佇列尾指標,指向佇列尾索引 + 1 + int rear = index(front + queSize); + // 將 num 新增至佇列尾 + nums[rear] = num; + queSize++; + } + + /* 佇列首出列 */ + int popFirst() { + int num = peekFirst(); + // 佇列首指標向後移動一位 + front = index(front + 1); + queSize--; + return num; + } + + /* 佇列尾出列 */ + int popLast() { + int num = peekLast(); + queSize--; + return num; + } + + /* 訪問佇列首元素 */ + int peekFirst() { + if (isEmpty()) + throw out_of_range("雙向佇列為空"); + return nums[front]; + } + + /* 訪問佇列尾元素 */ + int peekLast() { + if (isEmpty()) + throw out_of_range("雙向佇列為空"); + // 計算尾元素索引 + int last = index(front + queSize - 1); + return nums[last]; + } + + /* 返回陣列用於列印 */ + vector toVector() { + // 僅轉換有效長度範圍內的串列元素 + vector res(queSize); + for (int i = 0, j = front; i < queSize; i++, j++) { + res[i] = nums[index(j)]; + } + return res; + } + }; + ``` + +=== "Java" + + ```java title="array_deque.java" + /* 基於環形陣列實現的雙向佇列 */ + class ArrayDeque { + private int[] nums; // 用於儲存雙向佇列元素的陣列 + private int front; // 佇列首指標,指向佇列首元素 + private int queSize; // 雙向佇列長度 + + /* 建構子 */ + public ArrayDeque(int capacity) { + this.nums = new int[capacity]; + front = queSize = 0; + } + + /* 獲取雙向佇列的容量 */ + public int capacity() { + return nums.length; + } + + /* 獲取雙向佇列的長度 */ + public int size() { + return queSize; + } + + /* 判斷雙向佇列是否為空 */ + public boolean isEmpty() { + return queSize == 0; + } + + /* 計算環形陣列索引 */ + private int index(int i) { + // 透過取餘操作實現陣列首尾相連 + // 當 i 越過陣列尾部後,回到頭部 + // 當 i 越過陣列頭部後,回到尾部 + return (i + capacity()) % capacity(); + } + + /* 佇列首入列 */ + public void pushFirst(int num) { + if (queSize == capacity()) { + System.out.println("雙向佇列已滿"); + return; + } + // 佇列首指標向左移動一位 + // 透過取餘操作實現 front 越過陣列頭部後回到尾部 + front = index(front - 1); + // 將 num 新增至佇列首 + nums[front] = num; + queSize++; + } + + /* 佇列尾入列 */ + public void pushLast(int num) { + if (queSize == capacity()) { + System.out.println("雙向佇列已滿"); + return; + } + // 計算佇列尾指標,指向佇列尾索引 + 1 + int rear = index(front + queSize); + // 將 num 新增至佇列尾 + nums[rear] = num; + queSize++; + } + + /* 佇列首出列 */ + public int popFirst() { + int num = peekFirst(); + // 佇列首指標向後移動一位 + front = index(front + 1); + queSize--; + return num; + } + + /* 佇列尾出列 */ + public int popLast() { + int num = peekLast(); + queSize--; + return num; + } + + /* 訪問佇列首元素 */ + public int peekFirst() { + if (isEmpty()) + throw new IndexOutOfBoundsException(); + return nums[front]; + } + + /* 訪問佇列尾元素 */ + public int peekLast() { + if (isEmpty()) + throw new IndexOutOfBoundsException(); + // 計算尾元素索引 + int last = index(front + queSize - 1); + return nums[last]; + } + + /* 返回陣列用於列印 */ + public int[] toArray() { + // 僅轉換有效長度範圍內的串列元素 + int[] res = new int[queSize]; + for (int i = 0, j = front; i < queSize; i++, j++) { + res[i] = nums[index(j)]; + } + return res; + } + } + ``` + +=== "C#" + + ```csharp title="array_deque.cs" + /* 基於環形陣列實現的雙向佇列 */ + class ArrayDeque { + int[] nums; // 用於儲存雙向佇列元素的陣列 + int front; // 佇列首指標,指向佇列首元素 + int queSize; // 雙向佇列長度 + + /* 建構子 */ + public ArrayDeque(int capacity) { + nums = new int[capacity]; + front = queSize = 0; + } + + /* 獲取雙向佇列的容量 */ + int Capacity() { + return nums.Length; + } + + /* 獲取雙向佇列的長度 */ + public int Size() { + return queSize; + } + + /* 判斷雙向佇列是否為空 */ + public bool IsEmpty() { + return queSize == 0; + } + + /* 計算環形陣列索引 */ + int Index(int i) { + // 透過取餘操作實現陣列首尾相連 + // 當 i 越過陣列尾部後,回到頭部 + // 當 i 越過陣列頭部後,回到尾部 + return (i + Capacity()) % Capacity(); + } + + /* 佇列首入列 */ + public void PushFirst(int num) { + if (queSize == Capacity()) { + Console.WriteLine("雙向佇列已滿"); + return; + } + // 佇列首指標向左移動一位 + // 透過取餘操作實現 front 越過陣列頭部後回到尾部 + front = Index(front - 1); + // 將 num 新增至佇列首 + nums[front] = num; + queSize++; + } + + /* 佇列尾入列 */ + public void PushLast(int num) { + if (queSize == Capacity()) { + Console.WriteLine("雙向佇列已滿"); + return; + } + // 計算佇列尾指標,指向佇列尾索引 + 1 + int rear = Index(front + queSize); + // 將 num 新增至佇列尾 + nums[rear] = num; + queSize++; + } + + /* 佇列首出列 */ + public int PopFirst() { + int num = PeekFirst(); + // 佇列首指標向後移動一位 + front = Index(front + 1); + queSize--; + return num; + } + + /* 佇列尾出列 */ + public int PopLast() { + int num = PeekLast(); + queSize--; + return num; + } + + /* 訪問佇列首元素 */ + public int PeekFirst() { + if (IsEmpty()) { + throw new InvalidOperationException(); + } + return nums[front]; + } + + /* 訪問佇列尾元素 */ + public int PeekLast() { + if (IsEmpty()) { + throw new InvalidOperationException(); + } + // 計算尾元素索引 + int last = Index(front + queSize - 1); + return nums[last]; + } + + /* 返回陣列用於列印 */ + public int[] ToArray() { + // 僅轉換有效長度範圍內的串列元素 + int[] res = new int[queSize]; + for (int i = 0, j = front; i < queSize; i++, j++) { + res[i] = nums[Index(j)]; + } + return res; + } + } + ``` + +=== "Go" + + ```go title="array_deque.go" + /* 基於環形陣列實現的雙向佇列 */ + type arrayDeque struct { + nums []int // 用於儲存雙向佇列元素的陣列 + front int // 佇列首指標,指向佇列首元素 + queSize int // 雙向佇列長度 + queCapacity int // 佇列容量(即最大容納元素數量) + } + + /* 初始化佇列 */ + func newArrayDeque(queCapacity int) *arrayDeque { + return &arrayDeque{ + nums: make([]int, queCapacity), + queCapacity: queCapacity, + front: 0, + queSize: 0, + } + } + + /* 獲取雙向佇列的長度 */ + func (q *arrayDeque) size() int { + return q.queSize + } + + /* 判斷雙向佇列是否為空 */ + func (q *arrayDeque) isEmpty() bool { + return q.queSize == 0 + } + + /* 計算環形陣列索引 */ + func (q *arrayDeque) index(i int) int { + // 透過取餘操作實現陣列首尾相連 + // 當 i 越過陣列尾部後,回到頭部 + // 當 i 越過陣列頭部後,回到尾部 + return (i + q.queCapacity) % q.queCapacity + } + + /* 佇列首入列 */ + func (q *arrayDeque) pushFirst(num int) { + if q.queSize == q.queCapacity { + fmt.Println("雙向佇列已滿") + return + } + // 佇列首指標向左移動一位 + // 透過取餘操作實現 front 越過陣列頭部後回到尾部 + q.front = q.index(q.front - 1) + // 將 num 新增至佇列首 + q.nums[q.front] = num + q.queSize++ + } + + /* 佇列尾入列 */ + func (q *arrayDeque) pushLast(num int) { + if q.queSize == q.queCapacity { + fmt.Println("雙向佇列已滿") + return + } + // 計算佇列尾指標,指向佇列尾索引 + 1 + rear := q.index(q.front + q.queSize) + // 將 num 新增至佇列尾 + q.nums[rear] = num + q.queSize++ + } + + /* 佇列首出列 */ + func (q *arrayDeque) popFirst() any { + num := q.peekFirst() + // 佇列首指標向後移動一位 + q.front = q.index(q.front + 1) + q.queSize-- + return num + } + + /* 佇列尾出列 */ + func (q *arrayDeque) popLast() any { + num := q.peekLast() + q.queSize-- + return num + } + + /* 訪問佇列首元素 */ + func (q *arrayDeque) peekFirst() any { + if q.isEmpty() { + return nil + } + return q.nums[q.front] + } + + /* 訪問佇列尾元素 */ + func (q *arrayDeque) peekLast() any { + if q.isEmpty() { + return nil + } + // 計算尾元素索引 + last := q.index(q.front + q.queSize - 1) + return q.nums[last] + } + + /* 獲取 Slice 用於列印 */ + func (q *arrayDeque) toSlice() []int { + // 僅轉換有效長度範圍內的串列元素 + res := make([]int, q.queSize) + for i, j := 0, q.front; i < q.queSize; i++ { + res[i] = q.nums[q.index(j)] + j++ + } + return res + } + ``` + +=== "Swift" + + ```swift title="array_deque.swift" + /* 基於環形陣列實現的雙向佇列 */ + class ArrayDeque { + private var nums: [Int] // 用於儲存雙向佇列元素的陣列 + private var front: Int // 佇列首指標,指向佇列首元素 + private var _size: Int // 雙向佇列長度 + + /* 建構子 */ + init(capacity: Int) { + nums = Array(repeating: 0, count: capacity) + front = 0 + _size = 0 + } + + /* 獲取雙向佇列的容量 */ + func capacity() -> Int { + nums.count + } + + /* 獲取雙向佇列的長度 */ + func size() -> Int { + _size + } + + /* 判斷雙向佇列是否為空 */ + func isEmpty() -> Bool { + size() == 0 + } + + /* 計算環形陣列索引 */ + private func index(i: Int) -> Int { + // 透過取餘操作實現陣列首尾相連 + // 當 i 越過陣列尾部後,回到頭部 + // 當 i 越過陣列頭部後,回到尾部 + (i + capacity()) % capacity() + } + + /* 佇列首入列 */ + func pushFirst(num: Int) { + if size() == capacity() { + print("雙向佇列已滿") + return + } + // 佇列首指標向左移動一位 + // 透過取餘操作實現 front 越過陣列頭部後回到尾部 + front = index(i: front - 1) + // 將 num 新增至佇列首 + nums[front] = num + _size += 1 + } + + /* 佇列尾入列 */ + func pushLast(num: Int) { + if size() == capacity() { + print("雙向佇列已滿") + return + } + // 計算佇列尾指標,指向佇列尾索引 + 1 + let rear = index(i: front + size()) + // 將 num 新增至佇列尾 + nums[rear] = num + _size += 1 + } + + /* 佇列首出列 */ + func popFirst() -> Int { + let num = peekFirst() + // 佇列首指標向後移動一位 + front = index(i: front + 1) + _size -= 1 + return num + } + + /* 佇列尾出列 */ + func popLast() -> Int { + let num = peekLast() + _size -= 1 + return num + } + + /* 訪問佇列首元素 */ + func peekFirst() -> Int { + if isEmpty() { + fatalError("雙向佇列為空") + } + return nums[front] + } + + /* 訪問佇列尾元素 */ + func peekLast() -> Int { + if isEmpty() { + fatalError("雙向佇列為空") + } + // 計算尾元素索引 + let last = index(i: front + size() - 1) + return nums[last] + } + + /* 返回陣列用於列印 */ + func toArray() -> [Int] { + // 僅轉換有效長度範圍內的串列元素 + (front ..< front + size()).map { nums[index(i: $0)] } + } + } + ``` + +=== "JS" + + ```javascript title="array_deque.js" + /* 基於環形陣列實現的雙向佇列 */ + class ArrayDeque { + #nums; // 用於儲存雙向佇列元素的陣列 + #front; // 佇列首指標,指向佇列首元素 + #queSize; // 雙向佇列長度 + + /* 建構子 */ + constructor(capacity) { + this.#nums = new Array(capacity); + this.#front = 0; + this.#queSize = 0; + } + + /* 獲取雙向佇列的容量 */ + capacity() { + return this.#nums.length; + } + + /* 獲取雙向佇列的長度 */ + size() { + return this.#queSize; + } + + /* 判斷雙向佇列是否為空 */ + isEmpty() { + return this.#queSize === 0; + } + + /* 計算環形陣列索引 */ + index(i) { + // 透過取餘操作實現陣列首尾相連 + // 當 i 越過陣列尾部後,回到頭部 + // 當 i 越過陣列頭部後,回到尾部 + return (i + this.capacity()) % this.capacity(); + } + + /* 佇列首入列 */ + pushFirst(num) { + if (this.#queSize === this.capacity()) { + console.log('雙向佇列已滿'); + return; + } + // 佇列首指標向左移動一位 + // 透過取餘操作實現 front 越過陣列頭部後回到尾部 + this.#front = this.index(this.#front - 1); + // 將 num 新增至佇列首 + this.#nums[this.#front] = num; + this.#queSize++; + } + + /* 佇列尾入列 */ + pushLast(num) { + if (this.#queSize === this.capacity()) { + console.log('雙向佇列已滿'); + return; + } + // 計算佇列尾指標,指向佇列尾索引 + 1 + const rear = this.index(this.#front + this.#queSize); + // 將 num 新增至佇列尾 + this.#nums[rear] = num; + this.#queSize++; + } + + /* 佇列首出列 */ + popFirst() { + const num = this.peekFirst(); + // 佇列首指標向後移動一位 + this.#front = this.index(this.#front + 1); + this.#queSize--; + return num; + } + + /* 佇列尾出列 */ + popLast() { + const num = this.peekLast(); + this.#queSize--; + return num; + } + + /* 訪問佇列首元素 */ + peekFirst() { + if (this.isEmpty()) throw new Error('The Deque Is Empty.'); + return this.#nums[this.#front]; + } + + /* 訪問佇列尾元素 */ + peekLast() { + if (this.isEmpty()) throw new Error('The Deque Is Empty.'); + // 計算尾元素索引 + const last = this.index(this.#front + this.#queSize - 1); + return this.#nums[last]; + } + + /* 返回陣列用於列印 */ + toArray() { + // 僅轉換有效長度範圍內的串列元素 + const res = []; + for (let i = 0, j = this.#front; i < this.#queSize; i++, j++) { + res[i] = this.#nums[this.index(j)]; + } + return res; + } + } + ``` + +=== "TS" + + ```typescript title="array_deque.ts" + /* 基於環形陣列實現的雙向佇列 */ + class ArrayDeque { + private nums: number[]; // 用於儲存雙向佇列元素的陣列 + private front: number; // 佇列首指標,指向佇列首元素 + private queSize: number; // 雙向佇列長度 + + /* 建構子 */ + constructor(capacity: number) { + this.nums = new Array(capacity); + this.front = 0; + this.queSize = 0; + } + + /* 獲取雙向佇列的容量 */ + capacity(): number { + return this.nums.length; + } + + /* 獲取雙向佇列的長度 */ + size(): number { + return this.queSize; + } + + /* 判斷雙向佇列是否為空 */ + isEmpty(): boolean { + return this.queSize === 0; + } + + /* 計算環形陣列索引 */ + index(i: number): number { + // 透過取餘操作實現陣列首尾相連 + // 當 i 越過陣列尾部後,回到頭部 + // 當 i 越過陣列頭部後,回到尾部 + return (i + this.capacity()) % this.capacity(); + } + + /* 佇列首入列 */ + pushFirst(num: number): void { + if (this.queSize === this.capacity()) { + console.log('雙向佇列已滿'); + return; + } + // 佇列首指標向左移動一位 + // 透過取餘操作實現 front 越過陣列頭部後回到尾部 + this.front = this.index(this.front - 1); + // 將 num 新增至佇列首 + this.nums[this.front] = num; + this.queSize++; + } + + /* 佇列尾入列 */ + pushLast(num: number): void { + if (this.queSize === this.capacity()) { + console.log('雙向佇列已滿'); + return; + } + // 計算佇列尾指標,指向佇列尾索引 + 1 + const rear: number = this.index(this.front + this.queSize); + // 將 num 新增至佇列尾 + this.nums[rear] = num; + this.queSize++; + } + + /* 佇列首出列 */ + popFirst(): number { + const num: number = this.peekFirst(); + // 佇列首指標向後移動一位 + this.front = this.index(this.front + 1); + this.queSize--; + return num; + } + + /* 佇列尾出列 */ + popLast(): number { + const num: number = this.peekLast(); + this.queSize--; + return num; + } + + /* 訪問佇列首元素 */ + peekFirst(): number { + if (this.isEmpty()) throw new Error('The Deque Is Empty.'); + return this.nums[this.front]; + } + + /* 訪問佇列尾元素 */ + peekLast(): number { + if (this.isEmpty()) throw new Error('The Deque Is Empty.'); + // 計算尾元素索引 + const last = this.index(this.front + this.queSize - 1); + return this.nums[last]; + } + + /* 返回陣列用於列印 */ + toArray(): number[] { + // 僅轉換有效長度範圍內的串列元素 + const res: number[] = []; + for (let i = 0, j = this.front; i < this.queSize; i++, j++) { + res[i] = this.nums[this.index(j)]; + } + return res; + } + } + ``` + +=== "Dart" + + ```dart title="array_deque.dart" + /* 基於環形陣列實現的雙向佇列 */ + class ArrayDeque { + late List _nums; // 用於儲存雙向佇列元素的陣列 + late int _front; // 佇列首指標,指向佇列首元素 + late int _queSize; // 雙向佇列長度 + + /* 建構子 */ + ArrayDeque(int capacity) { + this._nums = List.filled(capacity, 0); + this._front = this._queSize = 0; + } + + /* 獲取雙向佇列的容量 */ + int capacity() { + return _nums.length; + } + + /* 獲取雙向佇列的長度 */ + int size() { + return _queSize; + } + + /* 判斷雙向佇列是否為空 */ + bool isEmpty() { + return _queSize == 0; + } + + /* 計算環形陣列索引 */ + int index(int i) { + // 透過取餘操作實現陣列首尾相連 + // 當 i 越過陣列尾部後,回到頭部 + // 當 i 越過陣列頭部後,回到尾部 + return (i + capacity()) % capacity(); + } + + /* 佇列首入列 */ + void pushFirst(int _num) { + if (_queSize == capacity()) { + throw Exception("雙向佇列已滿"); + } + // 佇列首指標向左移動一位 + // 透過取餘操作實現 _front 越過陣列頭部後回到尾部 + _front = index(_front - 1); + // 將 _num 新增至佇列首 + _nums[_front] = _num; + _queSize++; + } + + /* 佇列尾入列 */ + void pushLast(int _num) { + if (_queSize == capacity()) { + throw Exception("雙向佇列已滿"); + } + // 計算佇列尾指標,指向佇列尾索引 + 1 + int rear = index(_front + _queSize); + // 將 _num 新增至佇列尾 + _nums[rear] = _num; + _queSize++; + } + + /* 佇列首出列 */ + int popFirst() { + int _num = peekFirst(); + // 佇列首指標向右移動一位 + _front = index(_front + 1); + _queSize--; + return _num; + } + + /* 佇列尾出列 */ + int popLast() { + int _num = peekLast(); + _queSize--; + return _num; + } + + /* 訪問佇列首元素 */ + int peekFirst() { + if (isEmpty()) { + throw Exception("雙向佇列為空"); + } + return _nums[_front]; + } + + /* 訪問佇列尾元素 */ + int peekLast() { + if (isEmpty()) { + throw Exception("雙向佇列為空"); + } + // 計算尾元素索引 + int last = index(_front + _queSize - 1); + return _nums[last]; + } + + /* 返回陣列用於列印 */ + List toArray() { + // 僅轉換有效長度範圍內的串列元素 + List res = List.filled(_queSize, 0); + for (int i = 0, j = _front; i < _queSize; i++, j++) { + res[i] = _nums[index(j)]; + } + return res; + } + } + ``` + +=== "Rust" + + ```rust title="array_deque.rs" + /* 基於環形陣列實現的雙向佇列 */ + struct ArrayDeque { + nums: Vec, // 用於儲存雙向佇列元素的陣列 + front: usize, // 佇列首指標,指向佇列首元素 + que_size: usize, // 雙向佇列長度 + } + + impl ArrayDeque { + /* 建構子 */ + pub fn new(capacity: usize) -> Self { + Self { + nums: vec![0; capacity], + front: 0, + que_size: 0, + } + } + + /* 獲取雙向佇列的容量 */ + pub fn capacity(&self) -> usize { + self.nums.len() + } + + /* 獲取雙向佇列的長度 */ + pub fn size(&self) -> usize { + self.que_size + } + + /* 判斷雙向佇列是否為空 */ + pub fn is_empty(&self) -> bool { + self.que_size == 0 + } + + /* 計算環形陣列索引 */ + fn index(&self, i: i32) -> usize { + // 透過取餘操作實現陣列首尾相連 + // 當 i 越過陣列尾部後,回到頭部 + // 當 i 越過陣列頭部後,回到尾部 + return ((i + self.capacity() as i32) % self.capacity() as i32) as usize; + } + + /* 佇列首入列 */ + pub fn push_first(&mut self, num: i32) { + if self.que_size == self.capacity() { + println!("雙向佇列已滿"); + return; + } + // 佇列首指標向左移動一位 + // 透過取餘操作實現 front 越過陣列頭部後回到尾部 + self.front = self.index(self.front as i32 - 1); + // 將 num 新增至佇列首 + self.nums[self.front] = num; + self.que_size += 1; + } + + /* 佇列尾入列 */ + pub fn push_last(&mut self, num: i32) { + if self.que_size == self.capacity() { + println!("雙向佇列已滿"); + return; + } + // 計算佇列尾指標,指向佇列尾索引 + 1 + let rear = self.index(self.front as i32 + self.que_size as i32); + // 將 num 新增至佇列尾 + self.nums[rear] = num; + self.que_size += 1; + } + + /* 佇列首出列 */ + fn pop_first(&mut self) -> i32 { + let num = self.peek_first(); + // 佇列首指標向後移動一位 + self.front = self.index(self.front as i32 + 1); + self.que_size -= 1; + num + } + + /* 佇列尾出列 */ + fn pop_last(&mut self) -> i32 { + let num = self.peek_last(); + self.que_size -= 1; + num + } + + /* 訪問佇列首元素 */ + fn peek_first(&self) -> i32 { + if self.is_empty() { + panic!("雙向佇列為空") + }; + self.nums[self.front] + } + + /* 訪問佇列尾元素 */ + fn peek_last(&self) -> i32 { + if self.is_empty() { + panic!("雙向佇列為空") + }; + // 計算尾元素索引 + let last = self.index(self.front as i32 + self.que_size as i32 - 1); + self.nums[last] + } + + /* 返回陣列用於列印 */ + fn to_array(&self) -> Vec { + // 僅轉換有效長度範圍內的串列元素 + let mut res = vec![0; self.que_size]; + let mut j = self.front; + for i in 0..self.que_size { + res[i] = self.nums[self.index(j as i32)]; + j += 1; + } + res + } + } + ``` + +=== "C" + + ```c title="array_deque.c" + /* 基於環形陣列實現的雙向佇列 */ + typedef struct { + int *nums; // 用於儲存佇列元素的陣列 + int front; // 佇列首指標,指向佇列首元素 + int queSize; // 尾指標,指向佇列尾 + 1 + int queCapacity; // 佇列容量 + } ArrayDeque; + + /* 建構子 */ + ArrayDeque *newArrayDeque(int capacity) { + ArrayDeque *deque = (ArrayDeque *)malloc(sizeof(ArrayDeque)); + // 初始化陣列 + deque->queCapacity = capacity; + deque->nums = (int *)malloc(sizeof(int) * deque->queCapacity); + deque->front = deque->queSize = 0; + return deque; + } + + /* 析構函式 */ + void delArrayDeque(ArrayDeque *deque) { + free(deque->nums); + free(deque); + } + + /* 獲取雙向佇列的容量 */ + int capacity(ArrayDeque *deque) { + return deque->queCapacity; + } + + /* 獲取雙向佇列的長度 */ + int size(ArrayDeque *deque) { + return deque->queSize; + } + + /* 判斷雙向佇列是否為空 */ + bool empty(ArrayDeque *deque) { + return deque->queSize == 0; + } + + /* 計算環形陣列索引 */ + int dequeIndex(ArrayDeque *deque, int i) { + // 透過取餘操作實現陣列首尾相連 + // 當 i 越過陣列尾部時,回到頭部 + // 當 i 越過陣列頭部後,回到尾部 + return ((i + capacity(deque)) % capacity(deque)); + } + + /* 佇列首入列 */ + void pushFirst(ArrayDeque *deque, int num) { + if (deque->queSize == capacity(deque)) { + printf("雙向佇列已滿\r\n"); + return; + } + // 佇列首指標向左移動一位 + // 透過取餘操作實現 front 越過陣列頭部回到尾部 + deque->front = dequeIndex(deque, deque->front - 1); + // 將 num 新增到佇列首 + deque->nums[deque->front] = num; + deque->queSize++; + } + + /* 佇列尾入列 */ + void pushLast(ArrayDeque *deque, int num) { + if (deque->queSize == capacity(deque)) { + printf("雙向佇列已滿\r\n"); + return; + } + // 計算佇列尾指標,指向佇列尾索引 + 1 + int rear = dequeIndex(deque, deque->front + deque->queSize); + // 將 num 新增至佇列尾 + deque->nums[rear] = num; + deque->queSize++; + } + + /* 訪問佇列首元素 */ + int peekFirst(ArrayDeque *deque) { + // 訪問異常:雙向佇列為空 + assert(empty(deque) == 0); + return deque->nums[deque->front]; + } + + /* 訪問佇列尾元素 */ + int peekLast(ArrayDeque *deque) { + // 訪問異常:雙向佇列為空 + assert(empty(deque) == 0); + int last = dequeIndex(deque, deque->front + deque->queSize - 1); + return deque->nums[last]; + } + + /* 佇列首出列 */ + int popFirst(ArrayDeque *deque) { + int num = peekFirst(deque); + // 佇列首指標向後移動一位 + deque->front = dequeIndex(deque, deque->front + 1); + deque->queSize--; + return num; + } + + /* 佇列尾出列 */ + int popLast(ArrayDeque *deque) { + int num = peekLast(deque); + deque->queSize--; + return num; + } + ``` + +=== "Kotlin" + + ```kotlin title="array_deque.kt" + /* 基於環形陣列實現的雙向佇列 */ + class ArrayDeque(capacity: Int) { + private var nums = IntArray(capacity) // 用於儲存雙向佇列元素的陣列 + private var front = 0 // 佇列首指標,指向佇列首元素 + private var queSize = 0 // 雙向佇列長度 + + /* 獲取雙向佇列的容量 */ + fun capacity(): Int { + return nums.size + } + + /* 獲取雙向佇列的長度 */ + fun size(): Int { + return queSize + } + + /* 判斷雙向佇列是否為空 */ + fun isEmpty(): Boolean { + return queSize == 0 + } + + /* 計算環形陣列索引 */ + private fun index(i: Int): Int { + // 透過取餘操作實現陣列首尾相連 + // 當 i 越過陣列尾部後,回到頭部 + // 當 i 越過陣列頭部後,回到尾部 + return (i + capacity()) % capacity() + } + + /* 佇列首入列 */ + fun pushFirst(num: Int) { + if (queSize == capacity()) { + println("雙向佇列已滿") + return + } + // 佇列首指標向左移動一位 + // 透過取餘操作實現 front 越過陣列頭部後回到尾部 + front = index(front - 1) + // 將 num 新增至佇列首 + nums[front] = num + queSize++ + } + + /* 佇列尾入列 */ + fun pushLast(num: Int) { + if (queSize == capacity()) { + println("雙向佇列已滿") + return + } + // 計算佇列尾指標,指向佇列尾索引 + 1 + val rear = index(front + queSize) + // 將 num 新增至佇列尾 + nums[rear] = num + queSize++ + } + + /* 佇列首出列 */ + fun popFirst(): Int { + val num = peekFirst() + // 佇列首指標向後移動一位 + front = index(front + 1) + queSize-- + return num + } + + /* 訪問佇列尾元素 */ + fun popLast(): Int { + val num = peekLast() + queSize-- + return num + } + + /* 訪問佇列首元素 */ + fun peekFirst(): Int { + if (isEmpty()) throw IndexOutOfBoundsException() + return nums[front] + } + + /* 訪問佇列尾元素 */ + fun peekLast(): Int { + if (isEmpty()) throw IndexOutOfBoundsException() + // 計算尾元素索引 + val last = index(front + queSize - 1) + return nums[last] + } + + /* 返回陣列用於列印 */ + fun toArray(): IntArray { + // 僅轉換有效長度範圍內的串列元素 + val res = IntArray(queSize) + var i = 0 + var j = front + while (i < queSize) { + res[i] = nums[index(j)] + i++ + j++ + } + return res + } + } + ``` + +=== "Ruby" + + ```ruby title="array_deque.rb" + [class]{ArrayDeque}-[func]{} + ``` + +=== "Zig" + + ```zig title="array_deque.zig" + [class]{ArrayDeque}-[func]{} + ``` + +## 5.3.3   雙向佇列應用 + +雙向佇列兼具堆疊與佇列的邏輯,**因此它可以實現這兩者的所有應用場景,同時提供更高的自由度**。 + +我們知道,軟體的“撤銷”功能通常使用堆疊來實現:系統將每次更改操作 `push` 到堆疊中,然後透過 `pop` 實現撤銷。然而,考慮到系統資源的限制,軟體通常會限制撤銷的步數(例如僅允許儲存 $50$ 步)。當堆疊的長度超過 $50$ 時,軟體需要在堆疊底(佇列首)執行刪除操作。**但堆疊無法實現該功能,此時就需要使用雙向佇列來替代堆疊**。請注意,“撤銷”的核心邏輯仍然遵循堆疊的先入後出原則,只是雙向佇列能夠更加靈活地實現一些額外邏輯。 diff --git a/zh-Hant/docs/chapter_stack_and_queue/index.md b/zh-Hant/docs/chapter_stack_and_queue/index.md new file mode 100644 index 000000000..0bcbc0c19 --- /dev/null +++ b/zh-Hant/docs/chapter_stack_and_queue/index.md @@ -0,0 +1,21 @@ +--- +comments: true +icon: material/stack-overflow +--- + +# 第 5 章   堆疊與佇列 + +![堆疊與佇列](../assets/covers/chapter_stack_and_queue.jpg){ class="cover-image" } + +!!! abstract + + 堆疊如同疊貓貓,而佇列就像貓貓排隊。 + + 兩者分別代表先入後出和先入先出的邏輯關係。 + +## Chapter Contents + +- [5.1   堆疊](https://www.hello-algo.com/en/chapter_stack_and_queue/stack/) +- [5.2   佇列](https://www.hello-algo.com/en/chapter_stack_and_queue/queue/) +- [5.3   雙向佇列](https://www.hello-algo.com/en/chapter_stack_and_queue/deque/) +- [5.4   小結](https://www.hello-algo.com/en/chapter_stack_and_queue/summary/) diff --git a/zh-Hant/docs/chapter_stack_and_queue/queue.md b/zh-Hant/docs/chapter_stack_and_queue/queue.md new file mode 100755 index 000000000..af3248c05 --- /dev/null +++ b/zh-Hant/docs/chapter_stack_and_queue/queue.md @@ -0,0 +1,2321 @@ +--- +comments: true +--- + +# 5.2   佇列 + +佇列(queue)是一種遵循先入先出規則的線性資料結構。顧名思義,佇列模擬了排隊現象,即新來的人不斷加入佇列尾部,而位於佇列頭部的人逐個離開。 + +如圖 5-4 所示,我們將佇列頭部稱為“佇列首”,尾部稱為“佇列尾”,將把元素加入列尾的操作稱為“入列”,刪除佇列首元素的操作稱為“出列”。 + +![佇列的先入先出規則](queue.assets/queue_operations.png){ class="animation-figure" } + +

圖 5-4   佇列的先入先出規則

+ +## 5.2.1   佇列常用操作 + +佇列的常見操作如表 5-2 所示。需要注意的是,不同程式語言的方法名稱可能會有所不同。我們在此採用與堆疊相同的方法命名。 + +

表 5-2   佇列操作效率

+ +
+ +| 方法名 | 描述 | 時間複雜度 | +| -------- | ---------------------------- | ---------- | +| `push()` | 元素入列,即將元素新增至佇列尾 | $O(1)$ | +| `pop()` | 佇列首元素出列 | $O(1)$ | +| `peek()` | 訪問佇列首元素 | $O(1)$ | + +
+ +我們可以直接使用程式語言中現成的佇列類別: + +=== "Python" + + ```python title="queue.py" + from collections import deque + + # 初始化佇列 + # 在 Python 中,我們一般將雙向佇列類別 deque 當作佇列使用 + # 雖然 queue.Queue() 是純正的佇列類別,但不太好用,因此不推薦 + que: deque[int] = deque() + + # 元素入列 + que.append(1) + que.append(3) + que.append(2) + que.append(5) + que.append(4) + + # 訪問佇列首元素 + front: int = que[0] + + # 元素出列 + pop: int = que.popleft() + + # 獲取佇列的長度 + size: int = len(que) + + # 判斷佇列是否為空 + is_empty: bool = len(que) == 0 + ``` + +=== "C++" + + ```cpp title="queue.cpp" + /* 初始化佇列 */ + queue queue; + + /* 元素入列 */ + queue.push(1); + queue.push(3); + queue.push(2); + queue.push(5); + queue.push(4); + + /* 訪問佇列首元素 */ + int front = queue.front(); + + /* 元素出列 */ + queue.pop(); + + /* 獲取佇列的長度 */ + int size = queue.size(); + + /* 判斷佇列是否為空 */ + bool empty = queue.empty(); + ``` + +=== "Java" + + ```java title="queue.java" + /* 初始化佇列 */ + Queue queue = new LinkedList<>(); + + /* 元素入列 */ + queue.offer(1); + queue.offer(3); + queue.offer(2); + queue.offer(5); + queue.offer(4); + + /* 訪問佇列首元素 */ + int peek = queue.peek(); + + /* 元素出列 */ + int pop = queue.poll(); + + /* 獲取佇列的長度 */ + int size = queue.size(); + + /* 判斷佇列是否為空 */ + boolean isEmpty = queue.isEmpty(); + ``` + +=== "C#" + + ```csharp title="queue.cs" + /* 初始化佇列 */ + Queue queue = new(); + + /* 元素入列 */ + queue.Enqueue(1); + queue.Enqueue(3); + queue.Enqueue(2); + queue.Enqueue(5); + queue.Enqueue(4); + + /* 訪問佇列首元素 */ + int peek = queue.Peek(); + + /* 元素出列 */ + int pop = queue.Dequeue(); + + /* 獲取佇列的長度 */ + int size = queue.Count; + + /* 判斷佇列是否為空 */ + bool isEmpty = queue.Count == 0; + ``` + +=== "Go" + + ```go title="queue_test.go" + /* 初始化佇列 */ + // 在 Go 中,將 list 作為佇列來使用 + queue := list.New() + + /* 元素入列 */ + queue.PushBack(1) + queue.PushBack(3) + queue.PushBack(2) + queue.PushBack(5) + queue.PushBack(4) + + /* 訪問佇列首元素 */ + peek := queue.Front() + + /* 元素出列 */ + pop := queue.Front() + queue.Remove(pop) + + /* 獲取佇列的長度 */ + size := queue.Len() + + /* 判斷佇列是否為空 */ + isEmpty := queue.Len() == 0 + ``` + +=== "Swift" + + ```swift title="queue.swift" + /* 初始化佇列 */ + // Swift 沒有內建的佇列類別,可以把 Array 當作佇列來使用 + var queue: [Int] = [] + + /* 元素入列 */ + queue.append(1) + queue.append(3) + queue.append(2) + queue.append(5) + queue.append(4) + + /* 訪問佇列首元素 */ + let peek = queue.first! + + /* 元素出列 */ + // 由於是陣列,因此 removeFirst 的複雜度為 O(n) + let pool = queue.removeFirst() + + /* 獲取佇列的長度 */ + let size = queue.count + + /* 判斷佇列是否為空 */ + let isEmpty = queue.isEmpty + ``` + +=== "JS" + + ```javascript title="queue.js" + /* 初始化佇列 */ + // JavaScript 沒有內建的佇列,可以把 Array 當作佇列來使用 + const queue = []; + + /* 元素入列 */ + queue.push(1); + queue.push(3); + queue.push(2); + queue.push(5); + queue.push(4); + + /* 訪問佇列首元素 */ + const peek = queue[0]; + + /* 元素出列 */ + // 底層是陣列,因此 shift() 方法的時間複雜度為 O(n) + const pop = queue.shift(); + + /* 獲取佇列的長度 */ + const size = queue.length; + + /* 判斷佇列是否為空 */ + const empty = queue.length === 0; + ``` + +=== "TS" + + ```typescript title="queue.ts" + /* 初始化佇列 */ + // TypeScript 沒有內建的佇列,可以把 Array 當作佇列來使用 + const queue: number[] = []; + + /* 元素入列 */ + queue.push(1); + queue.push(3); + queue.push(2); + queue.push(5); + queue.push(4); + + /* 訪問佇列首元素 */ + const peek = queue[0]; + + /* 元素出列 */ + // 底層是陣列,因此 shift() 方法的時間複雜度為 O(n) + const pop = queue.shift(); + + /* 獲取佇列的長度 */ + const size = queue.length; + + /* 判斷佇列是否為空 */ + const empty = queue.length === 0; + ``` + +=== "Dart" + + ```dart title="queue.dart" + /* 初始化佇列 */ + // 在 Dart 中,佇列類別 Qeque 是雙向佇列,也可作為佇列使用 + Queue queue = Queue(); + + /* 元素入列 */ + queue.add(1); + queue.add(3); + queue.add(2); + queue.add(5); + queue.add(4); + + /* 訪問佇列首元素 */ + int peek = queue.first; + + /* 元素出列 */ + int pop = queue.removeFirst(); + + /* 獲取佇列的長度 */ + int size = queue.length; + + /* 判斷佇列是否為空 */ + bool isEmpty = queue.isEmpty; + ``` + +=== "Rust" + + ```rust title="queue.rs" + /* 初始化雙向佇列 */ + // 在 Rust 中使用雙向佇列作為普通佇列來使用 + let mut deque: VecDeque = VecDeque::new(); + + /* 元素入列 */ + deque.push_back(1); + deque.push_back(3); + deque.push_back(2); + deque.push_back(5); + deque.push_back(4); + + /* 訪問佇列首元素 */ + if let Some(front) = deque.front() { + } + + /* 元素出列 */ + if let Some(pop) = deque.pop_front() { + } + + /* 獲取佇列的長度 */ + let size = deque.len(); + + /* 判斷佇列是否為空 */ + let is_empty = deque.is_empty(); + ``` + +=== "C" + + ```c title="queue.c" + // C 未提供內建佇列 + ``` + +=== "Kotlin" + + ```kotlin title="queue.kt" + /* 初始化佇列 */ + val queue = LinkedList() + + /* 元素入列 */ + queue.offer(1) + queue.offer(3) + queue.offer(2) + queue.offer(5) + queue.offer(4) + + /* 訪問佇列首元素 */ + val peek = queue.peek() + + /* 元素出列 */ + val pop = queue.poll() + + /* 獲取佇列的長度 */ + val size = queue.size + + /* 判斷佇列是否為空 */ + val isEmpty = queue.isEmpty() + ``` + +=== "Ruby" + + ```ruby title="queue.rb" + + ``` + +=== "Zig" + + ```zig title="queue.zig" + + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 5.2.2   佇列實現 + +為了實現佇列,我們需要一種資料結構,可以在一端新增元素,並在另一端刪除元素,鏈結串列和陣列都符合要求。 + +### 1.   基於鏈結串列的實現 + +如圖 5-5 所示,我們可以將鏈結串列的“頭節點”和“尾節點”分別視為“佇列首”和“佇列尾”,規定佇列尾僅可新增節點,佇列首僅可刪除節點。 + +=== "LinkedListQueue" + ![基於鏈結串列實現佇列的入列出列操作](queue.assets/linkedlist_queue_step1.png){ class="animation-figure" } + +=== "push()" + ![linkedlist_queue_push](queue.assets/linkedlist_queue_step2_push.png){ class="animation-figure" } + +=== "pop()" + ![linkedlist_queue_pop](queue.assets/linkedlist_queue_step3_pop.png){ class="animation-figure" } + +

圖 5-5   基於鏈結串列實現佇列的入列出列操作

+ +以下是用鏈結串列實現佇列的程式碼: + +=== "Python" + + ```python title="linkedlist_queue.py" + class LinkedListQueue: + """基於鏈結串列實現的佇列""" + + def __init__(self): + """建構子""" + self._front: ListNode | None = None # 頭節點 front + self._rear: ListNode | None = None # 尾節點 rear + self._size: int = 0 + + def size(self) -> int: + """獲取佇列的長度""" + return self._size + + def is_empty(self) -> bool: + """判斷佇列是否為空""" + return not self._front + + def push(self, num: int): + """入列""" + # 在尾節點後新增 num + node = ListNode(num) + # 如果佇列為空,則令頭、尾節點都指向該節點 + if self._front is None: + self._front = node + self._rear = node + # 如果佇列不為空,則將該節點新增到尾節點後 + else: + self._rear.next = node + self._rear = node + self._size += 1 + + def pop(self) -> int: + """出列""" + num = self.peek() + # 刪除頭節點 + self._front = self._front.next + self._size -= 1 + return num + + def peek(self) -> int: + """訪問佇列首元素""" + if self.is_empty(): + raise IndexError("佇列為空") + return self._front.val + + def to_list(self) -> list[int]: + """轉化為串列用於列印""" + queue = [] + temp = self._front + while temp: + queue.append(temp.val) + temp = temp.next + return queue + ``` + +=== "C++" + + ```cpp title="linkedlist_queue.cpp" + /* 基於鏈結串列實現的佇列 */ + class LinkedListQueue { + private: + ListNode *front, *rear; // 頭節點 front ,尾節點 rear + int queSize; + + public: + LinkedListQueue() { + front = nullptr; + rear = nullptr; + queSize = 0; + } + + ~LinkedListQueue() { + // 走訪鏈結串列刪除節點,釋放記憶體 + freeMemoryLinkedList(front); + } + + /* 獲取佇列的長度 */ + int size() { + return queSize; + } + + /* 判斷佇列是否為空 */ + bool isEmpty() { + return queSize == 0; + } + + /* 入列 */ + void push(int num) { + // 在尾節點後新增 num + ListNode *node = new ListNode(num); + // 如果佇列為空,則令頭、尾節點都指向該節點 + if (front == nullptr) { + front = node; + rear = node; + } + // 如果佇列不為空,則將該節點新增到尾節點後 + else { + rear->next = node; + rear = node; + } + queSize++; + } + + /* 出列 */ + int pop() { + int num = peek(); + // 刪除頭節點 + ListNode *tmp = front; + front = front->next; + // 釋放記憶體 + delete tmp; + queSize--; + return num; + } + + /* 訪問佇列首元素 */ + int peek() { + if (size() == 0) + throw out_of_range("佇列為空"); + return front->val; + } + + /* 將鏈結串列轉化為 Vector 並返回 */ + vector toVector() { + ListNode *node = front; + vector res(size()); + for (int i = 0; i < res.size(); i++) { + res[i] = node->val; + node = node->next; + } + return res; + } + }; + ``` + +=== "Java" + + ```java title="linkedlist_queue.java" + /* 基於鏈結串列實現的佇列 */ + class LinkedListQueue { + private ListNode front, rear; // 頭節點 front ,尾節點 rear + private int queSize = 0; + + public LinkedListQueue() { + front = null; + rear = null; + } + + /* 獲取佇列的長度 */ + public int size() { + return queSize; + } + + /* 判斷佇列是否為空 */ + public boolean isEmpty() { + return size() == 0; + } + + /* 入列 */ + public void push(int num) { + // 在尾節點後新增 num + ListNode node = new ListNode(num); + // 如果佇列為空,則令頭、尾節點都指向該節點 + if (front == null) { + front = node; + rear = node; + // 如果佇列不為空,則將該節點新增到尾節點後 + } else { + rear.next = node; + rear = node; + } + queSize++; + } + + /* 出列 */ + public int pop() { + int num = peek(); + // 刪除頭節點 + front = front.next; + queSize--; + return num; + } + + /* 訪問佇列首元素 */ + public int peek() { + if (isEmpty()) + throw new IndexOutOfBoundsException(); + return front.val; + } + + /* 將鏈結串列轉化為 Array 並返回 */ + public int[] toArray() { + ListNode node = front; + int[] res = new int[size()]; + for (int i = 0; i < res.length; i++) { + res[i] = node.val; + node = node.next; + } + return res; + } + } + ``` + +=== "C#" + + ```csharp title="linkedlist_queue.cs" + /* 基於鏈結串列實現的佇列 */ + class LinkedListQueue { + ListNode? front, rear; // 頭節點 front ,尾節點 rear + int queSize = 0; + + public LinkedListQueue() { + front = null; + rear = null; + } + + /* 獲取佇列的長度 */ + public int Size() { + return queSize; + } + + /* 判斷佇列是否為空 */ + public bool IsEmpty() { + return Size() == 0; + } + + /* 入列 */ + public void Push(int num) { + // 在尾節點後新增 num + ListNode node = new(num); + // 如果佇列為空,則令頭、尾節點都指向該節點 + if (front == null) { + front = node; + rear = node; + // 如果佇列不為空,則將該節點新增到尾節點後 + } else if (rear != null) { + rear.next = node; + rear = node; + } + queSize++; + } + + /* 出列 */ + public int Pop() { + int num = Peek(); + // 刪除頭節點 + front = front?.next; + queSize--; + return num; + } + + /* 訪問佇列首元素 */ + public int Peek() { + if (IsEmpty()) + throw new Exception(); + return front!.val; + } + + /* 將鏈結串列轉化為 Array 並返回 */ + public int[] ToArray() { + if (front == null) + return []; + + ListNode? node = front; + int[] res = new int[Size()]; + for (int i = 0; i < res.Length; i++) { + res[i] = node!.val; + node = node.next; + } + return res; + } + } + ``` + +=== "Go" + + ```go title="linkedlist_queue.go" + /* 基於鏈結串列實現的佇列 */ + type linkedListQueue struct { + // 使用內建包 list 來實現佇列 + data *list.List + } + + /* 初始化佇列 */ + func newLinkedListQueue() *linkedListQueue { + return &linkedListQueue{ + data: list.New(), + } + } + + /* 入列 */ + func (s *linkedListQueue) push(value any) { + s.data.PushBack(value) + } + + /* 出列 */ + func (s *linkedListQueue) pop() any { + if s.isEmpty() { + return nil + } + e := s.data.Front() + s.data.Remove(e) + return e.Value + } + + /* 訪問佇列首元素 */ + func (s *linkedListQueue) peek() any { + if s.isEmpty() { + return nil + } + e := s.data.Front() + return e.Value + } + + /* 獲取佇列的長度 */ + func (s *linkedListQueue) size() int { + return s.data.Len() + } + + /* 判斷佇列是否為空 */ + func (s *linkedListQueue) isEmpty() bool { + return s.data.Len() == 0 + } + + /* 獲取 List 用於列印 */ + func (s *linkedListQueue) toList() *list.List { + return s.data + } + ``` + +=== "Swift" + + ```swift title="linkedlist_queue.swift" + /* 基於鏈結串列實現的佇列 */ + class LinkedListQueue { + private var front: ListNode? // 頭節點 + private var rear: ListNode? // 尾節點 + private var _size: Int + + init() { + _size = 0 + } + + /* 獲取佇列的長度 */ + func size() -> Int { + _size + } + + /* 判斷佇列是否為空 */ + func isEmpty() -> Bool { + size() == 0 + } + + /* 入列 */ + func push(num: Int) { + // 在尾節點後新增 num + let node = ListNode(x: num) + // 如果佇列為空,則令頭、尾節點都指向該節點 + if front == nil { + front = node + rear = node + } + // 如果佇列不為空,則將該節點新增到尾節點後 + else { + rear?.next = node + rear = node + } + _size += 1 + } + + /* 出列 */ + @discardableResult + func pop() -> Int { + let num = peek() + // 刪除頭節點 + front = front?.next + _size -= 1 + return num + } + + /* 訪問佇列首元素 */ + func peek() -> Int { + if isEmpty() { + fatalError("佇列為空") + } + return front!.val + } + + /* 將鏈結串列轉化為 Array 並返回 */ + func toArray() -> [Int] { + var node = front + var res = Array(repeating: 0, count: size()) + for i in res.indices { + res[i] = node!.val + node = node?.next + } + return res + } + } + ``` + +=== "JS" + + ```javascript title="linkedlist_queue.js" + /* 基於鏈結串列實現的佇列 */ + class LinkedListQueue { + #front; // 頭節點 #front + #rear; // 尾節點 #rear + #queSize = 0; + + constructor() { + this.#front = null; + this.#rear = null; + } + + /* 獲取佇列的長度 */ + get size() { + return this.#queSize; + } + + /* 判斷佇列是否為空 */ + isEmpty() { + return this.size === 0; + } + + /* 入列 */ + push(num) { + // 在尾節點後新增 num + const node = new ListNode(num); + // 如果佇列為空,則令頭、尾節點都指向該節點 + if (!this.#front) { + this.#front = node; + this.#rear = node; + // 如果佇列不為空,則將該節點新增到尾節點後 + } else { + this.#rear.next = node; + this.#rear = node; + } + this.#queSize++; + } + + /* 出列 */ + pop() { + const num = this.peek(); + // 刪除頭節點 + this.#front = this.#front.next; + this.#queSize--; + return num; + } + + /* 訪問佇列首元素 */ + peek() { + if (this.size === 0) throw new Error('佇列為空'); + return this.#front.val; + } + + /* 將鏈結串列轉化為 Array 並返回 */ + toArray() { + let node = this.#front; + const res = new Array(this.size); + for (let i = 0; i < res.length; i++) { + res[i] = node.val; + node = node.next; + } + return res; + } + } + ``` + +=== "TS" + + ```typescript title="linkedlist_queue.ts" + /* 基於鏈結串列實現的佇列 */ + class LinkedListQueue { + private front: ListNode | null; // 頭節點 front + private rear: ListNode | null; // 尾節點 rear + private queSize: number = 0; + + constructor() { + this.front = null; + this.rear = null; + } + + /* 獲取佇列的長度 */ + get size(): number { + return this.queSize; + } + + /* 判斷佇列是否為空 */ + isEmpty(): boolean { + return this.size === 0; + } + + /* 入列 */ + push(num: number): void { + // 在尾節點後新增 num + const node = new ListNode(num); + // 如果佇列為空,則令頭、尾節點都指向該節點 + if (!this.front) { + this.front = node; + this.rear = node; + // 如果佇列不為空,則將該節點新增到尾節點後 + } else { + this.rear!.next = node; + this.rear = node; + } + this.queSize++; + } + + /* 出列 */ + pop(): number { + const num = this.peek(); + if (!this.front) throw new Error('佇列為空'); + // 刪除頭節點 + this.front = this.front.next; + this.queSize--; + return num; + } + + /* 訪問佇列首元素 */ + peek(): number { + if (this.size === 0) throw new Error('佇列為空'); + return this.front!.val; + } + + /* 將鏈結串列轉化為 Array 並返回 */ + toArray(): number[] { + let node = this.front; + const res = new Array(this.size); + for (let i = 0; i < res.length; i++) { + res[i] = node!.val; + node = node!.next; + } + return res; + } + } + ``` + +=== "Dart" + + ```dart title="linkedlist_queue.dart" + /* 基於鏈結串列實現的佇列 */ + class LinkedListQueue { + ListNode? _front; // 頭節點 _front + ListNode? _rear; // 尾節點 _rear + int _queSize = 0; // 佇列長度 + + LinkedListQueue() { + _front = null; + _rear = null; + } + + /* 獲取佇列的長度 */ + int size() { + return _queSize; + } + + /* 判斷佇列是否為空 */ + bool isEmpty() { + return _queSize == 0; + } + + /* 入列 */ + void push(int _num) { + // 在尾節點後新增 _num + final node = ListNode(_num); + // 如果佇列為空,則令頭、尾節點都指向該節點 + if (_front == null) { + _front = node; + _rear = node; + } else { + // 如果佇列不為空,則將該節點新增到尾節點後 + _rear!.next = node; + _rear = node; + } + _queSize++; + } + + /* 出列 */ + int pop() { + final int _num = peek(); + // 刪除頭節點 + _front = _front!.next; + _queSize--; + return _num; + } + + /* 訪問佇列首元素 */ + int peek() { + if (_queSize == 0) { + throw Exception('佇列為空'); + } + return _front!.val; + } + + /* 將鏈結串列轉化為 Array 並返回 */ + List toArray() { + ListNode? node = _front; + final List queue = []; + while (node != null) { + queue.add(node.val); + node = node.next; + } + return queue; + } + } + ``` + +=== "Rust" + + ```rust title="linkedlist_queue.rs" + /* 基於鏈結串列實現的佇列 */ + #[allow(dead_code)] + pub struct LinkedListQueue { + front: Option>>>, // 頭節點 front + rear: Option>>>, // 尾節點 rear + que_size: usize, // 佇列的長度 + } + + impl LinkedListQueue { + pub fn new() -> Self { + Self { + front: None, + rear: None, + que_size: 0, + } + } + + /* 獲取佇列的長度 */ + pub fn size(&self) -> usize { + return self.que_size; + } + + /* 判斷佇列是否為空 */ + pub fn is_empty(&self) -> bool { + return self.size() == 0; + } + + /* 入列 */ + pub fn push(&mut self, num: T) { + // 在尾節點後新增 num + let new_rear = ListNode::new(num); + match self.rear.take() { + // 如果佇列不為空,則將該節點新增到尾節點後 + Some(old_rear) => { + old_rear.borrow_mut().next = Some(new_rear.clone()); + self.rear = Some(new_rear); + } + // 如果佇列為空,則令頭、尾節點都指向該節點 + None => { + self.front = Some(new_rear.clone()); + self.rear = Some(new_rear); + } + } + self.que_size += 1; + } + + /* 出列 */ + pub fn pop(&mut self) -> Option { + self.front.take().map(|old_front| { + match old_front.borrow_mut().next.take() { + Some(new_front) => { + self.front = Some(new_front); + } + None => { + self.rear.take(); + } + } + self.que_size -= 1; + Rc::try_unwrap(old_front).ok().unwrap().into_inner().val + }) + } + + /* 訪問佇列首元素 */ + pub fn peek(&self) -> Option<&Rc>>> { + self.front.as_ref() + } + + /* 將鏈結串列轉化為 Array 並返回 */ + pub fn to_array(&self, head: Option<&Rc>>>) -> Vec { + if let Some(node) = head { + let mut nums = self.to_array(node.borrow().next.as_ref()); + nums.insert(0, node.borrow().val); + return nums; + } + return Vec::new(); + } + } + ``` + +=== "C" + + ```c title="linkedlist_queue.c" + /* 基於鏈結串列實現的佇列 */ + typedef struct { + ListNode *front, *rear; + int queSize; + } LinkedListQueue; + + /* 建構子 */ + LinkedListQueue *newLinkedListQueue() { + LinkedListQueue *queue = (LinkedListQueue *)malloc(sizeof(LinkedListQueue)); + queue->front = NULL; + queue->rear = NULL; + queue->queSize = 0; + return queue; + } + + /* 析構函式 */ + void delLinkedListQueue(LinkedListQueue *queue) { + // 釋放所有節點 + while (queue->front != NULL) { + ListNode *tmp = queue->front; + queue->front = queue->front->next; + free(tmp); + } + // 釋放 queue 結構體 + free(queue); + } + + /* 獲取佇列的長度 */ + int size(LinkedListQueue *queue) { + return queue->queSize; + } + + /* 判斷佇列是否為空 */ + bool empty(LinkedListQueue *queue) { + return (size(queue) == 0); + } + + /* 入列 */ + void push(LinkedListQueue *queue, int num) { + // 尾節點處新增 node + ListNode *node = newListNode(num); + // 如果佇列為空,則令頭、尾節點都指向該節點 + if (queue->front == NULL) { + queue->front = node; + queue->rear = node; + } + // 如果佇列不為空,則將該節點新增到尾節點後 + else { + queue->rear->next = node; + queue->rear = node; + } + queue->queSize++; + } + + /* 訪問佇列首元素 */ + int peek(LinkedListQueue *queue) { + assert(size(queue) && queue->front); + return queue->front->val; + } + + /* 出列 */ + int pop(LinkedListQueue *queue) { + int num = peek(queue); + ListNode *tmp = queue->front; + queue->front = queue->front->next; + free(tmp); + queue->queSize--; + return num; + } + + /* 列印佇列 */ + void printLinkedListQueue(LinkedListQueue *queue) { + int *arr = malloc(sizeof(int) * queue->queSize); + // 複製鏈結串列中的資料到陣列 + int i; + ListNode *node; + for (i = 0, node = queue->front; i < queue->queSize; i++) { + arr[i] = node->val; + node = node->next; + } + printArray(arr, queue->queSize); + free(arr); + } + ``` + +=== "Kotlin" + + ```kotlin title="linkedlist_queue.kt" + /* 基於鏈結串列實現的佇列 */ + class LinkedListQueue( + // 頭節點 front ,尾節點 rear + private var front: ListNode? = null, + private var rear: ListNode? = null, + private var queSize: Int = 0 + ) { + + /* 獲取佇列的長度 */ + fun size(): Int { + return queSize + } + + /* 判斷佇列是否為空 */ + fun isEmpty(): Boolean { + return size() == 0 + } + + /* 入列 */ + fun push(num: Int) { + // 在尾節點後新增 num + val node = ListNode(num) + // 如果佇列為空,則令頭、尾節點都指向該節點 + if (front == null) { + front = node + rear = node + // 如果佇列不為空,則將該節點新增到尾節點後 + } else { + rear?.next = node + rear = node + } + queSize++ + } + + /* 出列 */ + fun pop(): Int { + val num = peek() + // 刪除頭節點 + front = front?.next + queSize-- + return num + } + + /* 訪問佇列首元素 */ + fun peek(): Int { + if (isEmpty()) throw IndexOutOfBoundsException() + return front!!.value + } + + /* 將鏈結串列轉化為 Array 並返回 */ + fun toArray(): IntArray { + var node = front + val res = IntArray(size()) + for (i in res.indices) { + res[i] = node!!.value + node = node.next + } + return res + } + } + ``` + +=== "Ruby" + + ```ruby title="linkedlist_queue.rb" + [class]{LinkedListQueue}-[func]{} + ``` + +=== "Zig" + + ```zig title="linkedlist_queue.zig" + // 基於鏈結串列實現的佇列 + fn LinkedListQueue(comptime T: type) type { + return struct { + const Self = @This(); + + front: ?*inc.ListNode(T) = null, // 頭節點 front + rear: ?*inc.ListNode(T) = null, // 尾節點 rear + que_size: usize = 0, // 佇列的長度 + mem_arena: ?std.heap.ArenaAllocator = null, + mem_allocator: std.mem.Allocator = undefined, // 記憶體分配器 + + // 建構子(分配記憶體+初始化佇列) + pub fn init(self: *Self, allocator: std.mem.Allocator) !void { + if (self.mem_arena == null) { + self.mem_arena = std.heap.ArenaAllocator.init(allocator); + self.mem_allocator = self.mem_arena.?.allocator(); + } + self.front = null; + self.rear = null; + self.que_size = 0; + } + + // 析構函式(釋放記憶體) + pub fn deinit(self: *Self) void { + if (self.mem_arena == null) return; + self.mem_arena.?.deinit(); + } + + // 獲取佇列的長度 + pub fn size(self: *Self) usize { + return self.que_size; + } + + // 判斷佇列是否為空 + pub fn isEmpty(self: *Self) bool { + return self.size() == 0; + } + + // 訪問佇列首元素 + pub fn peek(self: *Self) T { + if (self.size() == 0) @panic("佇列為空"); + return self.front.?.val; + } + + // 入列 + pub fn push(self: *Self, num: T) !void { + // 在尾節點後新增 num + var node = try self.mem_allocator.create(inc.ListNode(T)); + node.init(num); + // 如果佇列為空,則令頭、尾節點都指向該節點 + if (self.front == null) { + self.front = node; + self.rear = node; + // 如果佇列不為空,則將該節點新增到尾節點後 + } else { + self.rear.?.next = node; + self.rear = node; + } + self.que_size += 1; + } + + // 出列 + pub fn pop(self: *Self) T { + var num = self.peek(); + // 刪除頭節點 + self.front = self.front.?.next; + self.que_size -= 1; + return num; + } + + // 將鏈結串列轉換為陣列 + pub fn toArray(self: *Self) ![]T { + var node = self.front; + var res = try self.mem_allocator.alloc(T, self.size()); + @memset(res, @as(T, 0)); + var i: usize = 0; + while (i < res.len) : (i += 1) { + res[i] = node.?.val; + node = node.?.next; + } + return res; + } + }; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +### 2.   基於陣列的實現 + +在陣列中刪除首元素的時間複雜度為 $O(n)$ ,這會導致出列操作效率較低。然而,我們可以採用以下巧妙方法來避免這個問題。 + +我們可以使用一個變數 `front` 指向佇列首元素的索引,並維護一個變數 `size` 用於記錄佇列長度。定義 `rear = front + size` ,這個公式計算出的 `rear` 指向佇列尾元素之後的下一個位置。 + +基於此設計,**陣列中包含元素的有效區間為 `[front, rear - 1]`**,各種操作的實現方法如圖 5-6 所示。 + +- 入列操作:將輸入元素賦值給 `rear` 索引處,並將 `size` 增加 1 。 +- 出列操作:只需將 `front` 增加 1 ,並將 `size` 減少 1 。 + +可以看到,入列和出列操作都只需進行一次操作,時間複雜度均為 $O(1)$ 。 + +=== "ArrayQueue" + ![基於陣列實現佇列的入列出列操作](queue.assets/array_queue_step1.png){ class="animation-figure" } + +=== "push()" + ![array_queue_push](queue.assets/array_queue_step2_push.png){ class="animation-figure" } + +=== "pop()" + ![array_queue_pop](queue.assets/array_queue_step3_pop.png){ class="animation-figure" } + +

圖 5-6   基於陣列實現佇列的入列出列操作

+ +你可能會發現一個問題:在不斷進行入列和出列的過程中,`front` 和 `rear` 都在向右移動,**當它們到達陣列尾部時就無法繼續移動了**。為了解決此問題,我們可以將陣列視為首尾相接的“環形陣列”。 + +對於環形陣列,我們需要讓 `front` 或 `rear` 在越過陣列尾部時,直接回到陣列頭部繼續走訪。這種週期性規律可以透過“取餘操作”來實現,程式碼如下所示: + +=== "Python" + + ```python title="array_queue.py" + class ArrayQueue: + """基於環形陣列實現的佇列""" + + def __init__(self, size: int): + """建構子""" + self._nums: list[int] = [0] * size # 用於儲存佇列元素的陣列 + self._front: int = 0 # 佇列首指標,指向佇列首元素 + self._size: int = 0 # 佇列長度 + + def capacity(self) -> int: + """獲取佇列的容量""" + return len(self._nums) + + def size(self) -> int: + """獲取佇列的長度""" + return self._size + + def is_empty(self) -> bool: + """判斷佇列是否為空""" + return self._size == 0 + + def push(self, num: int): + """入列""" + if self._size == self.capacity(): + raise IndexError("佇列已滿") + # 計算佇列尾指標,指向佇列尾索引 + 1 + # 透過取餘操作實現 rear 越過陣列尾部後回到頭部 + rear: int = (self._front + self._size) % self.capacity() + # 將 num 新增至佇列尾 + self._nums[rear] = num + self._size += 1 + + def pop(self) -> int: + """出列""" + num: int = self.peek() + # 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部 + self._front = (self._front + 1) % self.capacity() + self._size -= 1 + return num + + def peek(self) -> int: + """訪問佇列首元素""" + if self.is_empty(): + raise IndexError("佇列為空") + return self._nums[self._front] + + def to_list(self) -> list[int]: + """返回串列用於列印""" + res = [0] * self.size() + j: int = self._front + for i in range(self.size()): + res[i] = self._nums[(j % self.capacity())] + j += 1 + return res + ``` + +=== "C++" + + ```cpp title="array_queue.cpp" + /* 基於環形陣列實現的佇列 */ + class ArrayQueue { + private: + int *nums; // 用於儲存佇列元素的陣列 + int front; // 佇列首指標,指向佇列首元素 + int queSize; // 佇列長度 + int queCapacity; // 佇列容量 + + public: + ArrayQueue(int capacity) { + // 初始化陣列 + nums = new int[capacity]; + queCapacity = capacity; + front = queSize = 0; + } + + ~ArrayQueue() { + delete[] nums; + } + + /* 獲取佇列的容量 */ + int capacity() { + return queCapacity; + } + + /* 獲取佇列的長度 */ + int size() { + return queSize; + } + + /* 判斷佇列是否為空 */ + bool isEmpty() { + return size() == 0; + } + + /* 入列 */ + void push(int num) { + if (queSize == queCapacity) { + cout << "佇列已滿" << endl; + return; + } + // 計算佇列尾指標,指向佇列尾索引 + 1 + // 透過取餘操作實現 rear 越過陣列尾部後回到頭部 + int rear = (front + queSize) % queCapacity; + // 將 num 新增至佇列尾 + nums[rear] = num; + queSize++; + } + + /* 出列 */ + int pop() { + int num = peek(); + // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部 + front = (front + 1) % queCapacity; + queSize--; + return num; + } + + /* 訪問佇列首元素 */ + int peek() { + if (isEmpty()) + throw out_of_range("佇列為空"); + return nums[front]; + } + + /* 將陣列轉化為 Vector 並返回 */ + vector toVector() { + // 僅轉換有效長度範圍內的串列元素 + vector arr(queSize); + for (int i = 0, j = front; i < queSize; i++, j++) { + arr[i] = nums[j % queCapacity]; + } + return arr; + } + }; + ``` + +=== "Java" + + ```java title="array_queue.java" + /* 基於環形陣列實現的佇列 */ + class ArrayQueue { + private int[] nums; // 用於儲存佇列元素的陣列 + private int front; // 佇列首指標,指向佇列首元素 + private int queSize; // 佇列長度 + + public ArrayQueue(int capacity) { + nums = new int[capacity]; + front = queSize = 0; + } + + /* 獲取佇列的容量 */ + public int capacity() { + return nums.length; + } + + /* 獲取佇列的長度 */ + public int size() { + return queSize; + } + + /* 判斷佇列是否為空 */ + public boolean isEmpty() { + return queSize == 0; + } + + /* 入列 */ + public void push(int num) { + if (queSize == capacity()) { + System.out.println("佇列已滿"); + return; + } + // 計算佇列尾指標,指向佇列尾索引 + 1 + // 透過取餘操作實現 rear 越過陣列尾部後回到頭部 + int rear = (front + queSize) % capacity(); + // 將 num 新增至佇列尾 + nums[rear] = num; + queSize++; + } + + /* 出列 */ + public int pop() { + int num = peek(); + // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部 + front = (front + 1) % capacity(); + queSize--; + return num; + } + + /* 訪問佇列首元素 */ + public int peek() { + if (isEmpty()) + throw new IndexOutOfBoundsException(); + return nums[front]; + } + + /* 返回陣列 */ + public int[] toArray() { + // 僅轉換有效長度範圍內的串列元素 + int[] res = new int[queSize]; + for (int i = 0, j = front; i < queSize; i++, j++) { + res[i] = nums[j % capacity()]; + } + return res; + } + } + ``` + +=== "C#" + + ```csharp title="array_queue.cs" + /* 基於環形陣列實現的佇列 */ + class ArrayQueue { + int[] nums; // 用於儲存佇列元素的陣列 + int front; // 佇列首指標,指向佇列首元素 + int queSize; // 佇列長度 + + public ArrayQueue(int capacity) { + nums = new int[capacity]; + front = queSize = 0; + } + + /* 獲取佇列的容量 */ + int Capacity() { + return nums.Length; + } + + /* 獲取佇列的長度 */ + public int Size() { + return queSize; + } + + /* 判斷佇列是否為空 */ + public bool IsEmpty() { + return queSize == 0; + } + + /* 入列 */ + public void Push(int num) { + if (queSize == Capacity()) { + Console.WriteLine("佇列已滿"); + return; + } + // 計算佇列尾指標,指向佇列尾索引 + 1 + // 透過取餘操作實現 rear 越過陣列尾部後回到頭部 + int rear = (front + queSize) % Capacity(); + // 將 num 新增至佇列尾 + nums[rear] = num; + queSize++; + } + + /* 出列 */ + public int Pop() { + int num = Peek(); + // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部 + front = (front + 1) % Capacity(); + queSize--; + return num; + } + + /* 訪問佇列首元素 */ + public int Peek() { + if (IsEmpty()) + throw new Exception(); + return nums[front]; + } + + /* 返回陣列 */ + public int[] ToArray() { + // 僅轉換有效長度範圍內的串列元素 + int[] res = new int[queSize]; + for (int i = 0, j = front; i < queSize; i++, j++) { + res[i] = nums[j % this.Capacity()]; + } + return res; + } + } + ``` + +=== "Go" + + ```go title="array_queue.go" + /* 基於環形陣列實現的佇列 */ + type arrayQueue struct { + nums []int // 用於儲存佇列元素的陣列 + front int // 佇列首指標,指向佇列首元素 + queSize int // 佇列長度 + queCapacity int // 佇列容量(即最大容納元素數量) + } + + /* 初始化佇列 */ + func newArrayQueue(queCapacity int) *arrayQueue { + return &arrayQueue{ + nums: make([]int, queCapacity), + queCapacity: queCapacity, + front: 0, + queSize: 0, + } + } + + /* 獲取佇列的長度 */ + func (q *arrayQueue) size() int { + return q.queSize + } + + /* 判斷佇列是否為空 */ + func (q *arrayQueue) isEmpty() bool { + return q.queSize == 0 + } + + /* 入列 */ + func (q *arrayQueue) push(num int) { + // 當 rear == queCapacity 表示佇列已滿 + if q.queSize == q.queCapacity { + return + } + // 計算佇列尾指標,指向佇列尾索引 + 1 + // 透過取餘操作實現 rear 越過陣列尾部後回到頭部 + rear := (q.front + q.queSize) % q.queCapacity + // 將 num 新增至佇列尾 + q.nums[rear] = num + q.queSize++ + } + + /* 出列 */ + func (q *arrayQueue) pop() any { + num := q.peek() + // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部 + q.front = (q.front + 1) % q.queCapacity + q.queSize-- + return num + } + + /* 訪問佇列首元素 */ + func (q *arrayQueue) peek() any { + if q.isEmpty() { + return nil + } + return q.nums[q.front] + } + + /* 獲取 Slice 用於列印 */ + func (q *arrayQueue) toSlice() []int { + rear := (q.front + q.queSize) + if rear >= q.queCapacity { + rear %= q.queCapacity + return append(q.nums[q.front:], q.nums[:rear]...) + } + return q.nums[q.front:rear] + } + ``` + +=== "Swift" + + ```swift title="array_queue.swift" + /* 基於環形陣列實現的佇列 */ + class ArrayQueue { + private var nums: [Int] // 用於儲存佇列元素的陣列 + private var front: Int // 佇列首指標,指向佇列首元素 + private var _size: Int // 佇列長度 + + init(capacity: Int) { + // 初始化陣列 + nums = Array(repeating: 0, count: capacity) + front = 0 + _size = 0 + } + + /* 獲取佇列的容量 */ + func capacity() -> Int { + nums.count + } + + /* 獲取佇列的長度 */ + func size() -> Int { + _size + } + + /* 判斷佇列是否為空 */ + func isEmpty() -> Bool { + size() == 0 + } + + /* 入列 */ + func push(num: Int) { + if size() == capacity() { + print("佇列已滿") + return + } + // 計算佇列尾指標,指向佇列尾索引 + 1 + // 透過取餘操作實現 rear 越過陣列尾部後回到頭部 + let rear = (front + size()) % capacity() + // 將 num 新增至佇列尾 + nums[rear] = num + _size += 1 + } + + /* 出列 */ + @discardableResult + func pop() -> Int { + let num = peek() + // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部 + front = (front + 1) % capacity() + _size -= 1 + return num + } + + /* 訪問佇列首元素 */ + func peek() -> Int { + if isEmpty() { + fatalError("佇列為空") + } + return nums[front] + } + + /* 返回陣列 */ + func toArray() -> [Int] { + // 僅轉換有效長度範圍內的串列元素 + (front ..< front + size()).map { nums[$0 % capacity()] } + } + } + ``` + +=== "JS" + + ```javascript title="array_queue.js" + /* 基於環形陣列實現的佇列 */ + class ArrayQueue { + #nums; // 用於儲存佇列元素的陣列 + #front = 0; // 佇列首指標,指向佇列首元素 + #queSize = 0; // 佇列長度 + + constructor(capacity) { + this.#nums = new Array(capacity); + } + + /* 獲取佇列的容量 */ + get capacity() { + return this.#nums.length; + } + + /* 獲取佇列的長度 */ + get size() { + return this.#queSize; + } + + /* 判斷佇列是否為空 */ + isEmpty() { + return this.#queSize === 0; + } + + /* 入列 */ + push(num) { + if (this.size === this.capacity) { + console.log('佇列已滿'); + return; + } + // 計算佇列尾指標,指向佇列尾索引 + 1 + // 透過取餘操作實現 rear 越過陣列尾部後回到頭部 + const rear = (this.#front + this.size) % this.capacity; + // 將 num 新增至佇列尾 + this.#nums[rear] = num; + this.#queSize++; + } + + /* 出列 */ + pop() { + const num = this.peek(); + // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部 + this.#front = (this.#front + 1) % this.capacity; + this.#queSize--; + return num; + } + + /* 訪問佇列首元素 */ + peek() { + if (this.isEmpty()) throw new Error('佇列為空'); + return this.#nums[this.#front]; + } + + /* 返回 Array */ + toArray() { + // 僅轉換有效長度範圍內的串列元素 + const arr = new Array(this.size); + for (let i = 0, j = this.#front; i < this.size; i++, j++) { + arr[i] = this.#nums[j % this.capacity]; + } + return arr; + } + } + ``` + +=== "TS" + + ```typescript title="array_queue.ts" + /* 基於環形陣列實現的佇列 */ + class ArrayQueue { + private nums: number[]; // 用於儲存佇列元素的陣列 + private front: number; // 佇列首指標,指向佇列首元素 + private queSize: number; // 佇列長度 + + constructor(capacity: number) { + this.nums = new Array(capacity); + this.front = this.queSize = 0; + } + + /* 獲取佇列的容量 */ + get capacity(): number { + return this.nums.length; + } + + /* 獲取佇列的長度 */ + get size(): number { + return this.queSize; + } + + /* 判斷佇列是否為空 */ + isEmpty(): boolean { + return this.queSize === 0; + } + + /* 入列 */ + push(num: number): void { + if (this.size === this.capacity) { + console.log('佇列已滿'); + return; + } + // 計算佇列尾指標,指向佇列尾索引 + 1 + // 透過取餘操作實現 rear 越過陣列尾部後回到頭部 + const rear = (this.front + this.queSize) % this.capacity; + // 將 num 新增至佇列尾 + this.nums[rear] = num; + this.queSize++; + } + + /* 出列 */ + pop(): number { + const num = this.peek(); + // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部 + this.front = (this.front + 1) % this.capacity; + this.queSize--; + return num; + } + + /* 訪問佇列首元素 */ + peek(): number { + if (this.isEmpty()) throw new Error('佇列為空'); + return this.nums[this.front]; + } + + /* 返回 Array */ + toArray(): number[] { + // 僅轉換有效長度範圍內的串列元素 + const arr = new Array(this.size); + for (let i = 0, j = this.front; i < this.size; i++, j++) { + arr[i] = this.nums[j % this.capacity]; + } + return arr; + } + } + ``` + +=== "Dart" + + ```dart title="array_queue.dart" + /* 基於環形陣列實現的佇列 */ + class ArrayQueue { + late List _nums; // 用於儲存佇列元素的陣列 + late int _front; // 佇列首指標,指向佇列首元素 + late int _queSize; // 佇列長度 + + ArrayQueue(int capacity) { + _nums = List.filled(capacity, 0); + _front = _queSize = 0; + } + + /* 獲取佇列的容量 */ + int capaCity() { + return _nums.length; + } + + /* 獲取佇列的長度 */ + int size() { + return _queSize; + } + + /* 判斷佇列是否為空 */ + bool isEmpty() { + return _queSize == 0; + } + + /* 入列 */ + void push(int _num) { + if (_queSize == capaCity()) { + throw Exception("佇列已滿"); + } + // 計算佇列尾指標,指向佇列尾索引 + 1 + // 透過取餘操作實現 rear 越過陣列尾部後回到頭部 + int rear = (_front + _queSize) % capaCity(); + // 將 _num 新增至佇列尾 + _nums[rear] = _num; + _queSize++; + } + + /* 出列 */ + int pop() { + int _num = peek(); + // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部 + _front = (_front + 1) % capaCity(); + _queSize--; + return _num; + } + + /* 訪問佇列首元素 */ + int peek() { + if (isEmpty()) { + throw Exception("佇列為空"); + } + return _nums[_front]; + } + + /* 返回 Array */ + List toArray() { + // 僅轉換有效長度範圍內的串列元素 + final List res = List.filled(_queSize, 0); + for (int i = 0, j = _front; i < _queSize; i++, j++) { + res[i] = _nums[j % capaCity()]; + } + return res; + } + } + ``` + +=== "Rust" + + ```rust title="array_queue.rs" + /* 基於環形陣列實現的佇列 */ + struct ArrayQueue { + nums: Vec, // 用於儲存佇列元素的陣列 + front: i32, // 佇列首指標,指向佇列首元素 + que_size: i32, // 佇列長度 + que_capacity: i32, // 佇列容量 + } + + impl ArrayQueue { + /* 建構子 */ + fn new(capacity: i32) -> ArrayQueue { + ArrayQueue { + nums: vec![0; capacity as usize], + front: 0, + que_size: 0, + que_capacity: capacity, + } + } + + /* 獲取佇列的容量 */ + fn capacity(&self) -> i32 { + self.que_capacity + } + + /* 獲取佇列的長度 */ + fn size(&self) -> i32 { + self.que_size + } + + /* 判斷佇列是否為空 */ + fn is_empty(&self) -> bool { + self.que_size == 0 + } + + /* 入列 */ + fn push(&mut self, num: i32) { + if self.que_size == self.capacity() { + println!("佇列已滿"); + return; + } + // 計算佇列尾指標,指向佇列尾索引 + 1 + // 透過取餘操作實現 rear 越過陣列尾部後回到頭部 + let rear = (self.front + self.que_size) % self.que_capacity; + // 將 num 新增至佇列尾 + self.nums[rear as usize] = num; + self.que_size += 1; + } + + /* 出列 */ + fn pop(&mut self) -> i32 { + let num = self.peek(); + // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部 + self.front = (self.front + 1) % self.que_capacity; + self.que_size -= 1; + num + } + + /* 訪問佇列首元素 */ + fn peek(&self) -> i32 { + if self.is_empty() { + panic!("index out of bounds"); + } + self.nums[self.front as usize] + } + + /* 返回陣列 */ + fn to_vector(&self) -> Vec { + let cap = self.que_capacity; + let mut j = self.front; + let mut arr = vec![0; self.que_size as usize]; + for i in 0..self.que_size { + arr[i as usize] = self.nums[(j % cap) as usize]; + j += 1; + } + arr + } + } + ``` + +=== "C" + + ```c title="array_queue.c" + /* 基於環形陣列實現的佇列 */ + typedef struct { + int *nums; // 用於儲存佇列元素的陣列 + int front; // 佇列首指標,指向佇列首元素 + int queSize; // 尾指標,指向佇列尾 + 1 + int queCapacity; // 佇列容量 + } ArrayQueue; + + /* 建構子 */ + ArrayQueue *newArrayQueue(int capacity) { + ArrayQueue *queue = (ArrayQueue *)malloc(sizeof(ArrayQueue)); + // 初始化陣列 + queue->queCapacity = capacity; + queue->nums = (int *)malloc(sizeof(int) * queue->queCapacity); + queue->front = queue->queSize = 0; + return queue; + } + + /* 析構函式 */ + void delArrayQueue(ArrayQueue *queue) { + free(queue->nums); + free(queue); + } + + /* 獲取佇列的容量 */ + int capacity(ArrayQueue *queue) { + return queue->queCapacity; + } + + /* 獲取佇列的長度 */ + int size(ArrayQueue *queue) { + return queue->queSize; + } + + /* 判斷佇列是否為空 */ + bool empty(ArrayQueue *queue) { + return queue->queSize == 0; + } + + /* 訪問佇列首元素 */ + int peek(ArrayQueue *queue) { + assert(size(queue) != 0); + return queue->nums[queue->front]; + } + + /* 入列 */ + void push(ArrayQueue *queue, int num) { + if (size(queue) == capacity(queue)) { + printf("佇列已滿\r\n"); + return; + } + // 計算佇列尾指標,指向佇列尾索引 + 1 + // 透過取餘操作實現 rear 越過陣列尾部後回到頭部 + int rear = (queue->front + queue->queSize) % queue->queCapacity; + // 將 num 新增至佇列尾 + queue->nums[rear] = num; + queue->queSize++; + } + + /* 出列 */ + int pop(ArrayQueue *queue) { + int num = peek(queue); + // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部 + queue->front = (queue->front + 1) % queue->queCapacity; + queue->queSize--; + return num; + } + ``` + +=== "Kotlin" + + ```kotlin title="array_queue.kt" + /* 基於環形陣列實現的佇列 */ + class ArrayQueue(capacity: Int) { + private val nums = IntArray(capacity) // 用於儲存佇列元素的陣列 + private var front = 0 // 佇列首指標,指向佇列首元素 + private var queSize = 0 // 佇列長度 + + /* 獲取佇列的容量 */ + fun capacity(): Int { + return nums.size + } + + /* 獲取佇列的長度 */ + fun size(): Int { + return queSize + } + + /* 判斷佇列是否為空 */ + fun isEmpty(): Boolean { + return queSize == 0 + } + + /* 入列 */ + fun push(num: Int) { + if (queSize == capacity()) { + println("佇列已滿") + return + } + // 計算佇列尾指標,指向佇列尾索引 + 1 + // 透過取餘操作實現 rear 越過陣列尾部後回到頭部 + val rear = (front + queSize) % capacity() + // 將 num 新增至佇列尾 + nums[rear] = num + queSize++ + } + + /* 出列 */ + fun pop(): Int { + val num = peek() + // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部 + front = (front + 1) % capacity() + queSize-- + return num + } + + /* 訪問佇列首元素 */ + fun peek(): Int { + if (isEmpty()) throw IndexOutOfBoundsException() + return nums[front] + } + + /* 返回陣列 */ + fun toArray(): IntArray { + // 僅轉換有效長度範圍內的串列元素 + val res = IntArray(queSize) + var i = 0 + var j = front + while (i < queSize) { + res[i] = nums[j % capacity()] + i++ + j++ + } + return res + } + } + ``` + +=== "Ruby" + + ```ruby title="array_queue.rb" + [class]{ArrayQueue}-[func]{} + ``` + +=== "Zig" + + ```zig title="array_queue.zig" + // 基於環形陣列實現的佇列 + fn ArrayQueue(comptime T: type) type { + return struct { + const Self = @This(); + + nums: []T = undefined, // 用於儲存佇列元素的陣列 + cap: usize = 0, // 佇列容量 + front: usize = 0, // 佇列首指標,指向佇列首元素 + queSize: usize = 0, // 尾指標,指向佇列尾 + 1 + mem_arena: ?std.heap.ArenaAllocator = null, + mem_allocator: std.mem.Allocator = undefined, // 記憶體分配器 + + // 建構子(分配記憶體+初始化陣列) + pub fn init(self: *Self, allocator: std.mem.Allocator, cap: usize) !void { + if (self.mem_arena == null) { + self.mem_arena = std.heap.ArenaAllocator.init(allocator); + self.mem_allocator = self.mem_arena.?.allocator(); + } + self.cap = cap; + self.nums = try self.mem_allocator.alloc(T, self.cap); + @memset(self.nums, @as(T, 0)); + } + + // 析構函式(釋放記憶體) + pub fn deinit(self: *Self) void { + if (self.mem_arena == null) return; + self.mem_arena.?.deinit(); + } + + // 獲取佇列的容量 + pub fn capacity(self: *Self) usize { + return self.cap; + } + + // 獲取佇列的長度 + pub fn size(self: *Self) usize { + return self.queSize; + } + + // 判斷佇列是否為空 + pub fn isEmpty(self: *Self) bool { + return self.queSize == 0; + } + + // 入列 + pub fn push(self: *Self, num: T) !void { + if (self.size() == self.capacity()) { + std.debug.print("佇列已滿\n", .{}); + return; + } + // 計算佇列尾指標,指向佇列尾索引 + 1 + // 透過取餘操作實現 rear 越過陣列尾部後回到頭部 + var rear = (self.front + self.queSize) % self.capacity(); + // 在尾節點後新增 num + self.nums[rear] = num; + self.queSize += 1; + } + + // 出列 + pub fn pop(self: *Self) T { + var num = self.peek(); + // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部 + self.front = (self.front + 1) % self.capacity(); + self.queSize -= 1; + return num; + } + + // 訪問佇列首元素 + pub fn peek(self: *Self) T { + if (self.isEmpty()) @panic("佇列為空"); + return self.nums[self.front]; + } + + // 返回陣列 + pub fn toArray(self: *Self) ![]T { + // 僅轉換有效長度範圍內的串列元素 + var res = try self.mem_allocator.alloc(T, self.size()); + @memset(res, @as(T, 0)); + var i: usize = 0; + var j: usize = self.front; + while (i < self.size()) : ({ i += 1; j += 1; }) { + res[i] = self.nums[j % self.capacity()]; + } + return res; + } + }; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +以上實現的佇列仍然具有侷限性:其長度不可變。然而,這個問題不難解決,我們可以將陣列替換為動態陣列,從而引入擴容機制。有興趣的讀者可以嘗試自行實現。 + +兩種實現的對比結論與堆疊一致,在此不再贅述。 + +## 5.2.3   佇列典型應用 + +- **淘寶訂單**。購物者下單後,訂單將加入列列中,系統隨後會根據順序處理佇列中的訂單。在雙十一期間,短時間內會產生海量訂單,高併發成為工程師們需要重點攻克的問題。 +- **各類待辦事項**。任何需要實現“先來後到”功能的場景,例如印表機的任務佇列、餐廳的出餐佇列等,佇列在這些場景中可以有效地維護處理順序。 diff --git a/zh-Hant/docs/chapter_stack_and_queue/stack.md b/zh-Hant/docs/chapter_stack_and_queue/stack.md new file mode 100755 index 000000000..dc723fb99 --- /dev/null +++ b/zh-Hant/docs/chapter_stack_and_queue/stack.md @@ -0,0 +1,1874 @@ +--- +comments: true +--- + +# 5.1   堆疊 + +堆疊(stack)是一種遵循先入後出邏輯的線性資料結構。 + +我們可以將堆疊類比為桌面上的一疊盤子,如果想取出底部的盤子,則需要先將上面的盤子依次移走。我們將盤子替換為各種型別的元素(如整數、字元、物件等),就得到了堆疊這種資料結構。 + +如圖 5-1 所示,我們把堆積疊元素的頂部稱為“堆疊頂”,底部稱為“堆疊底”。將把元素新增到堆疊頂的操作叫作“入堆疊”,刪除堆疊頂元素的操作叫作“出堆疊”。 + +![堆疊的先入後出規則](stack.assets/stack_operations.png){ class="animation-figure" } + +

圖 5-1   堆疊的先入後出規則

+ +## 5.1.1   堆疊的常用操作 + +堆疊的常用操作如表 5-1 所示,具體的方法名需要根據所使用的程式語言來確定。在此,我們以常見的 `push()`、`pop()`、`peek()` 命名為例。 + +

表 5-1   堆疊的操作效率

+ +
+ +| 方法 | 描述 | 時間複雜度 | +| -------- | ---------------------- | ---------- | +| `push()` | 元素入堆疊(新增至堆疊頂) | $O(1)$ | +| `pop()` | 堆疊頂元素出堆疊 | $O(1)$ | +| `peek()` | 訪問堆疊頂元素 | $O(1)$ | + +
+ +通常情況下,我們可以直接使用程式語言內建的堆疊類別。然而,某些語言可能沒有專門提供堆疊類別,這時我們可以將該語言的“陣列”或“鏈結串列”當作堆疊來使用,並在程式邏輯上忽略與堆疊無關的操作。 + +=== "Python" + + ```python title="stack.py" + # 初始化堆疊 + # Python 沒有內建的堆疊類別,可以把 list 當作堆疊來使用 + stack: list[int] = [] + + # 元素入堆疊 + stack.append(1) + stack.append(3) + stack.append(2) + stack.append(5) + stack.append(4) + + # 訪問堆疊頂元素 + peek: int = stack[-1] + + # 元素出堆疊 + pop: int = stack.pop() + + # 獲取堆疊的長度 + size: int = len(stack) + + # 判斷是否為空 + is_empty: bool = len(stack) == 0 + ``` + +=== "C++" + + ```cpp title="stack.cpp" + /* 初始化堆疊 */ + stack stack; + + /* 元素入堆疊 */ + stack.push(1); + stack.push(3); + stack.push(2); + stack.push(5); + stack.push(4); + + /* 訪問堆疊頂元素 */ + int top = stack.top(); + + /* 元素出堆疊 */ + stack.pop(); // 無返回值 + + /* 獲取堆疊的長度 */ + int size = stack.size(); + + /* 判斷是否為空 */ + bool empty = stack.empty(); + ``` + +=== "Java" + + ```java title="stack.java" + /* 初始化堆疊 */ + Stack stack = new Stack<>(); + + /* 元素入堆疊 */ + stack.push(1); + stack.push(3); + stack.push(2); + stack.push(5); + stack.push(4); + + /* 訪問堆疊頂元素 */ + int peek = stack.peek(); + + /* 元素出堆疊 */ + int pop = stack.pop(); + + /* 獲取堆疊的長度 */ + int size = stack.size(); + + /* 判斷是否為空 */ + boolean isEmpty = stack.isEmpty(); + ``` + +=== "C#" + + ```csharp title="stack.cs" + /* 初始化堆疊 */ + Stack stack = new(); + + /* 元素入堆疊 */ + stack.Push(1); + stack.Push(3); + stack.Push(2); + stack.Push(5); + stack.Push(4); + + /* 訪問堆疊頂元素 */ + int peek = stack.Peek(); + + /* 元素出堆疊 */ + int pop = stack.Pop(); + + /* 獲取堆疊的長度 */ + int size = stack.Count; + + /* 判斷是否為空 */ + bool isEmpty = stack.Count == 0; + ``` + +=== "Go" + + ```go title="stack_test.go" + /* 初始化堆疊 */ + // 在 Go 中,推薦將 Slice 當作堆疊來使用 + var stack []int + + /* 元素入堆疊 */ + stack = append(stack, 1) + stack = append(stack, 3) + stack = append(stack, 2) + stack = append(stack, 5) + stack = append(stack, 4) + + /* 訪問堆疊頂元素 */ + peek := stack[len(stack)-1] + + /* 元素出堆疊 */ + pop := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + /* 獲取堆疊的長度 */ + size := len(stack) + + /* 判斷是否為空 */ + isEmpty := len(stack) == 0 + ``` + +=== "Swift" + + ```swift title="stack.swift" + /* 初始化堆疊 */ + // Swift 沒有內建的堆疊類別,可以把 Array 當作堆疊來使用 + var stack: [Int] = [] + + /* 元素入堆疊 */ + stack.append(1) + stack.append(3) + stack.append(2) + stack.append(5) + stack.append(4) + + /* 訪問堆疊頂元素 */ + let peek = stack.last! + + /* 元素出堆疊 */ + let pop = stack.removeLast() + + /* 獲取堆疊的長度 */ + let size = stack.count + + /* 判斷是否為空 */ + let isEmpty = stack.isEmpty + ``` + +=== "JS" + + ```javascript title="stack.js" + /* 初始化堆疊 */ + // JavaScript 沒有內建的堆疊類別,可以把 Array 當作堆疊來使用 + const stack = []; + + /* 元素入堆疊 */ + stack.push(1); + stack.push(3); + stack.push(2); + stack.push(5); + stack.push(4); + + /* 訪問堆疊頂元素 */ + const peek = stack[stack.length-1]; + + /* 元素出堆疊 */ + const pop = stack.pop(); + + /* 獲取堆疊的長度 */ + const size = stack.length; + + /* 判斷是否為空 */ + const is_empty = stack.length === 0; + ``` + +=== "TS" + + ```typescript title="stack.ts" + /* 初始化堆疊 */ + // TypeScript 沒有內建的堆疊類別,可以把 Array 當作堆疊來使用 + const stack: number[] = []; + + /* 元素入堆疊 */ + stack.push(1); + stack.push(3); + stack.push(2); + stack.push(5); + stack.push(4); + + /* 訪問堆疊頂元素 */ + const peek = stack[stack.length - 1]; + + /* 元素出堆疊 */ + const pop = stack.pop(); + + /* 獲取堆疊的長度 */ + const size = stack.length; + + /* 判斷是否為空 */ + const is_empty = stack.length === 0; + ``` + +=== "Dart" + + ```dart title="stack.dart" + /* 初始化堆疊 */ + // Dart 沒有內建的堆疊類別,可以把 List 當作堆疊來使用 + List stack = []; + + /* 元素入堆疊 */ + stack.add(1); + stack.add(3); + stack.add(2); + stack.add(5); + stack.add(4); + + /* 訪問堆疊頂元素 */ + int peek = stack.last; + + /* 元素出堆疊 */ + int pop = stack.removeLast(); + + /* 獲取堆疊的長度 */ + int size = stack.length; + + /* 判斷是否為空 */ + bool isEmpty = stack.isEmpty; + ``` + +=== "Rust" + + ```rust title="stack.rs" + /* 初始化堆疊 */ + // 把 Vec 當作堆疊來使用 + let mut stack: Vec = Vec::new(); + + /* 元素入堆疊 */ + stack.push(1); + stack.push(3); + stack.push(2); + stack.push(5); + stack.push(4); + + /* 訪問堆疊頂元素 */ + let top = stack.last().unwrap(); + + /* 元素出堆疊 */ + let pop = stack.pop().unwrap(); + + /* 獲取堆疊的長度 */ + let size = stack.len(); + + /* 判斷是否為空 */ + let is_empty = stack.is_empty(); + ``` + +=== "C" + + ```c title="stack.c" + // C 未提供內建堆疊 + ``` + +=== "Kotlin" + + ```kotlin title="stack.kt" + /* 初始化堆疊 */ + val stack = Stack() + + /* 元素入堆疊 */ + stack.push(1) + stack.push(3) + stack.push(2) + stack.push(5) + stack.push(4) + + /* 訪問堆疊頂元素 */ + val peek = stack.peek() + + /* 元素出堆疊 */ + val pop = stack.pop() + + /* 獲取堆疊的長度 */ + val size = stack.size + + /* 判斷是否為空 */ + val isEmpty = stack.isEmpty() + ``` + +=== "Ruby" + + ```ruby title="stack.rb" + + ``` + +=== "Zig" + + ```zig title="stack.zig" + + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 5.1.2   堆疊的實現 + +為了深入瞭解堆疊的執行機制,我們來嘗試自己實現一個堆疊類別。 + +堆疊遵循先入後出的原則,因此我們只能在堆疊頂新增或刪除元素。然而,陣列和鏈結串列都可以在任意位置新增和刪除元素,**因此堆疊可以視為一種受限制的陣列或鏈結串列**。換句話說,我們可以“遮蔽”陣列或鏈結串列的部分無關操作,使其對外表現的邏輯符合堆疊的特性。 + +### 1.   基於鏈結串列的實現 + +使用鏈結串列實現堆疊時,我們可以將鏈結串列的頭節點視為堆疊頂,尾節點視為堆疊底。 + +如圖 5-2 所示,對於入堆疊操作,我們只需將元素插入鏈結串列頭部,這種節點插入方法被稱為“頭插法”。而對於出堆疊操作,只需將頭節點從鏈結串列中刪除即可。 + +=== "LinkedListStack" + ![基於鏈結串列實現堆疊的入堆疊出堆疊操作](stack.assets/linkedlist_stack_step1.png){ class="animation-figure" } + +=== "push()" + ![linkedlist_stack_push](stack.assets/linkedlist_stack_step2_push.png){ class="animation-figure" } + +=== "pop()" + ![linkedlist_stack_pop](stack.assets/linkedlist_stack_step3_pop.png){ class="animation-figure" } + +

圖 5-2   基於鏈結串列實現堆疊的入堆疊出堆疊操作

+ +以下是基於鏈結串列實現堆疊的示例程式碼: + +=== "Python" + + ```python title="linkedlist_stack.py" + class LinkedListStack: + """基於鏈結串列實現的堆疊""" + + def __init__(self): + """建構子""" + self._peek: ListNode | None = None + self._size: int = 0 + + def size(self) -> int: + """獲取堆疊的長度""" + return self._size + + def is_empty(self) -> bool: + """判斷堆疊是否為空""" + return not self._peek + + def push(self, val: int): + """入堆疊""" + node = ListNode(val) + node.next = self._peek + self._peek = node + self._size += 1 + + def pop(self) -> int: + """出堆疊""" + num = self.peek() + self._peek = self._peek.next + self._size -= 1 + return num + + def peek(self) -> int: + """訪問堆疊頂元素""" + if self.is_empty(): + raise IndexError("堆疊為空") + return self._peek.val + + def to_list(self) -> list[int]: + """轉化為串列用於列印""" + arr = [] + node = self._peek + while node: + arr.append(node.val) + node = node.next + arr.reverse() + return arr + ``` + +=== "C++" + + ```cpp title="linkedlist_stack.cpp" + /* 基於鏈結串列實現的堆疊 */ + class LinkedListStack { + private: + ListNode *stackTop; // 將頭節點作為堆疊頂 + int stkSize; // 堆疊的長度 + + public: + LinkedListStack() { + stackTop = nullptr; + stkSize = 0; + } + + ~LinkedListStack() { + // 走訪鏈結串列刪除節點,釋放記憶體 + freeMemoryLinkedList(stackTop); + } + + /* 獲取堆疊的長度 */ + int size() { + return stkSize; + } + + /* 判斷堆疊是否為空 */ + bool isEmpty() { + return size() == 0; + } + + /* 入堆疊 */ + void push(int num) { + ListNode *node = new ListNode(num); + node->next = stackTop; + stackTop = node; + stkSize++; + } + + /* 出堆疊 */ + int pop() { + int num = top(); + ListNode *tmp = stackTop; + stackTop = stackTop->next; + // 釋放記憶體 + delete tmp; + stkSize--; + return num; + } + + /* 訪問堆疊頂元素 */ + int top() { + if (isEmpty()) + throw out_of_range("堆疊為空"); + return stackTop->val; + } + + /* 將 List 轉化為 Array 並返回 */ + vector toVector() { + ListNode *node = stackTop; + vector res(size()); + for (int i = res.size() - 1; i >= 0; i--) { + res[i] = node->val; + node = node->next; + } + return res; + } + }; + ``` + +=== "Java" + + ```java title="linkedlist_stack.java" + /* 基於鏈結串列實現的堆疊 */ + class LinkedListStack { + private ListNode stackPeek; // 將頭節點作為堆疊頂 + private int stkSize = 0; // 堆疊的長度 + + public LinkedListStack() { + stackPeek = null; + } + + /* 獲取堆疊的長度 */ + public int size() { + return stkSize; + } + + /* 判斷堆疊是否為空 */ + public boolean isEmpty() { + return size() == 0; + } + + /* 入堆疊 */ + public void push(int num) { + ListNode node = new ListNode(num); + node.next = stackPeek; + stackPeek = node; + stkSize++; + } + + /* 出堆疊 */ + public int pop() { + int num = peek(); + stackPeek = stackPeek.next; + stkSize--; + return num; + } + + /* 訪問堆疊頂元素 */ + public int peek() { + if (isEmpty()) + throw new IndexOutOfBoundsException(); + return stackPeek.val; + } + + /* 將 List 轉化為 Array 並返回 */ + public int[] toArray() { + ListNode node = stackPeek; + int[] res = new int[size()]; + for (int i = res.length - 1; i >= 0; i--) { + res[i] = node.val; + node = node.next; + } + return res; + } + } + ``` + +=== "C#" + + ```csharp title="linkedlist_stack.cs" + /* 基於鏈結串列實現的堆疊 */ + class LinkedListStack { + ListNode? stackPeek; // 將頭節點作為堆疊頂 + int stkSize = 0; // 堆疊的長度 + + public LinkedListStack() { + stackPeek = null; + } + + /* 獲取堆疊的長度 */ + public int Size() { + return stkSize; + } + + /* 判斷堆疊是否為空 */ + public bool IsEmpty() { + return Size() == 0; + } + + /* 入堆疊 */ + public void Push(int num) { + ListNode node = new(num) { + next = stackPeek + }; + stackPeek = node; + stkSize++; + } + + /* 出堆疊 */ + public int Pop() { + int num = Peek(); + stackPeek = stackPeek!.next; + stkSize--; + return num; + } + + /* 訪問堆疊頂元素 */ + public int Peek() { + if (IsEmpty()) + throw new Exception(); + return stackPeek!.val; + } + + /* 將 List 轉化為 Array 並返回 */ + public int[] ToArray() { + if (stackPeek == null) + return []; + + ListNode? node = stackPeek; + int[] res = new int[Size()]; + for (int i = res.Length - 1; i >= 0; i--) { + res[i] = node!.val; + node = node.next; + } + return res; + } + } + ``` + +=== "Go" + + ```go title="linkedlist_stack.go" + /* 基於鏈結串列實現的堆疊 */ + type linkedListStack struct { + // 使用內建包 list 來實現堆疊 + data *list.List + } + + /* 初始化堆疊 */ + func newLinkedListStack() *linkedListStack { + return &linkedListStack{ + data: list.New(), + } + } + + /* 入堆疊 */ + func (s *linkedListStack) push(value int) { + s.data.PushBack(value) + } + + /* 出堆疊 */ + func (s *linkedListStack) pop() any { + if s.isEmpty() { + return nil + } + e := s.data.Back() + s.data.Remove(e) + return e.Value + } + + /* 訪問堆疊頂元素 */ + func (s *linkedListStack) peek() any { + if s.isEmpty() { + return nil + } + e := s.data.Back() + return e.Value + } + + /* 獲取堆疊的長度 */ + func (s *linkedListStack) size() int { + return s.data.Len() + } + + /* 判斷堆疊是否為空 */ + func (s *linkedListStack) isEmpty() bool { + return s.data.Len() == 0 + } + + /* 獲取 List 用於列印 */ + func (s *linkedListStack) toList() *list.List { + return s.data + } + ``` + +=== "Swift" + + ```swift title="linkedlist_stack.swift" + /* 基於鏈結串列實現的堆疊 */ + class LinkedListStack { + private var _peek: ListNode? // 將頭節點作為堆疊頂 + private var _size: Int // 堆疊的長度 + + init() { + _size = 0 + } + + /* 獲取堆疊的長度 */ + func size() -> Int { + _size + } + + /* 判斷堆疊是否為空 */ + func isEmpty() -> Bool { + size() == 0 + } + + /* 入堆疊 */ + func push(num: Int) { + let node = ListNode(x: num) + node.next = _peek + _peek = node + _size += 1 + } + + /* 出堆疊 */ + @discardableResult + func pop() -> Int { + let num = peek() + _peek = _peek?.next + _size -= 1 + return num + } + + /* 訪問堆疊頂元素 */ + func peek() -> Int { + if isEmpty() { + fatalError("堆疊為空") + } + return _peek!.val + } + + /* 將 List 轉化為 Array 並返回 */ + func toArray() -> [Int] { + var node = _peek + var res = Array(repeating: 0, count: size()) + for i in res.indices.reversed() { + res[i] = node!.val + node = node?.next + } + return res + } + } + ``` + +=== "JS" + + ```javascript title="linkedlist_stack.js" + /* 基於鏈結串列實現的堆疊 */ + class LinkedListStack { + #stackPeek; // 將頭節點作為堆疊頂 + #stkSize = 0; // 堆疊的長度 + + constructor() { + this.#stackPeek = null; + } + + /* 獲取堆疊的長度 */ + get size() { + return this.#stkSize; + } + + /* 判斷堆疊是否為空 */ + isEmpty() { + return this.size === 0; + } + + /* 入堆疊 */ + push(num) { + const node = new ListNode(num); + node.next = this.#stackPeek; + this.#stackPeek = node; + this.#stkSize++; + } + + /* 出堆疊 */ + pop() { + const num = this.peek(); + this.#stackPeek = this.#stackPeek.next; + this.#stkSize--; + return num; + } + + /* 訪問堆疊頂元素 */ + peek() { + if (!this.#stackPeek) throw new Error('堆疊為空'); + return this.#stackPeek.val; + } + + /* 將鏈結串列轉化為 Array 並返回 */ + toArray() { + let node = this.#stackPeek; + const res = new Array(this.size); + for (let i = res.length - 1; i >= 0; i--) { + res[i] = node.val; + node = node.next; + } + return res; + } + } + ``` + +=== "TS" + + ```typescript title="linkedlist_stack.ts" + /* 基於鏈結串列實現的堆疊 */ + class LinkedListStack { + private stackPeek: ListNode | null; // 將頭節點作為堆疊頂 + private stkSize: number = 0; // 堆疊的長度 + + constructor() { + this.stackPeek = null; + } + + /* 獲取堆疊的長度 */ + get size(): number { + return this.stkSize; + } + + /* 判斷堆疊是否為空 */ + isEmpty(): boolean { + return this.size === 0; + } + + /* 入堆疊 */ + push(num: number): void { + const node = new ListNode(num); + node.next = this.stackPeek; + this.stackPeek = node; + this.stkSize++; + } + + /* 出堆疊 */ + pop(): number { + const num = this.peek(); + if (!this.stackPeek) throw new Error('堆疊為空'); + this.stackPeek = this.stackPeek.next; + this.stkSize--; + return num; + } + + /* 訪問堆疊頂元素 */ + peek(): number { + if (!this.stackPeek) throw new Error('堆疊為空'); + return this.stackPeek.val; + } + + /* 將鏈結串列轉化為 Array 並返回 */ + toArray(): number[] { + let node = this.stackPeek; + const res = new Array(this.size); + for (let i = res.length - 1; i >= 0; i--) { + res[i] = node!.val; + node = node!.next; + } + return res; + } + } + ``` + +=== "Dart" + + ```dart title="linkedlist_stack.dart" + /* 基於鏈結串列類別實現的堆疊 */ + class LinkedListStack { + ListNode? _stackPeek; // 將頭節點作為堆疊頂 + int _stkSize = 0; // 堆疊的長度 + + LinkedListStack() { + _stackPeek = null; + } + + /* 獲取堆疊的長度 */ + int size() { + return _stkSize; + } + + /* 判斷堆疊是否為空 */ + bool isEmpty() { + return _stkSize == 0; + } + + /* 入堆疊 */ + void push(int _num) { + final ListNode node = ListNode(_num); + node.next = _stackPeek; + _stackPeek = node; + _stkSize++; + } + + /* 出堆疊 */ + int pop() { + final int _num = peek(); + _stackPeek = _stackPeek!.next; + _stkSize--; + return _num; + } + + /* 訪問堆疊頂元素 */ + int peek() { + if (_stackPeek == null) { + throw Exception("堆疊為空"); + } + return _stackPeek!.val; + } + + /* 將鏈結串列轉化為 List 並返回 */ + List toList() { + ListNode? node = _stackPeek; + List list = []; + while (node != null) { + list.add(node.val); + node = node.next; + } + list = list.reversed.toList(); + return list; + } + } + ``` + +=== "Rust" + + ```rust title="linkedlist_stack.rs" + /* 基於鏈結串列實現的堆疊 */ + #[allow(dead_code)] + pub struct LinkedListStack { + stack_peek: Option>>>, // 將頭節點作為堆疊頂 + stk_size: usize, // 堆疊的長度 + } + + impl LinkedListStack { + pub fn new() -> Self { + Self { + stack_peek: None, + stk_size: 0, + } + } + + /* 獲取堆疊的長度 */ + pub fn size(&self) -> usize { + return self.stk_size; + } + + /* 判斷堆疊是否為空 */ + pub fn is_empty(&self) -> bool { + return self.size() == 0; + } + + /* 入堆疊 */ + pub fn push(&mut self, num: T) { + let node = ListNode::new(num); + node.borrow_mut().next = self.stack_peek.take(); + self.stack_peek = Some(node); + self.stk_size += 1; + } + + /* 出堆疊 */ + pub fn pop(&mut self) -> Option { + self.stack_peek.take().map(|old_head| { + match old_head.borrow_mut().next.take() { + Some(new_head) => { + self.stack_peek = Some(new_head); + } + None => { + self.stack_peek = None; + } + } + self.stk_size -= 1; + Rc::try_unwrap(old_head).ok().unwrap().into_inner().val + }) + } + + /* 訪問堆疊頂元素 */ + pub fn peek(&self) -> Option<&Rc>>> { + self.stack_peek.as_ref() + } + + /* 將 List 轉化為 Array 並返回 */ + pub fn to_array(&self, head: Option<&Rc>>>) -> Vec { + if let Some(node) = head { + let mut nums = self.to_array(node.borrow().next.as_ref()); + nums.push(node.borrow().val); + return nums; + } + return Vec::new(); + } + } + ``` + +=== "C" + + ```c title="linkedlist_stack.c" + /* 基於鏈結串列實現的堆疊 */ + typedef struct { + ListNode *top; // 將頭節點作為堆疊頂 + int size; // 堆疊的長度 + } LinkedListStack; + + /* 建構子 */ + LinkedListStack *newLinkedListStack() { + LinkedListStack *s = malloc(sizeof(LinkedListStack)); + s->top = NULL; + s->size = 0; + return s; + } + + /* 析構函式 */ + void delLinkedListStack(LinkedListStack *s) { + while (s->top) { + ListNode *n = s->top->next; + free(s->top); + s->top = n; + } + free(s); + } + + /* 獲取堆疊的長度 */ + int size(LinkedListStack *s) { + return s->size; + } + + /* 判斷堆疊是否為空 */ + bool isEmpty(LinkedListStack *s) { + return size(s) == 0; + } + + /* 入堆疊 */ + void push(LinkedListStack *s, int num) { + ListNode *node = (ListNode *)malloc(sizeof(ListNode)); + node->next = s->top; // 更新新加節點指標域 + node->val = num; // 更新新加節點資料域 + s->top = node; // 更新堆疊頂 + s->size++; // 更新堆疊大小 + } + + /* 訪問堆疊頂元素 */ + int peek(LinkedListStack *s) { + if (s->size == 0) { + printf("堆疊為空\n"); + return INT_MAX; + } + return s->top->val; + } + + /* 出堆疊 */ + int pop(LinkedListStack *s) { + int val = peek(s); + ListNode *tmp = s->top; + s->top = s->top->next; + // 釋放記憶體 + free(tmp); + s->size--; + return val; + } + ``` + +=== "Kotlin" + + ```kotlin title="linkedlist_stack.kt" + /* 基於鏈結串列實現的堆疊 */ + class LinkedListStack( + private var stackPeek: ListNode? = null, // 將頭節點作為堆疊頂 + private var stkSize: Int = 0 // 堆疊的長度 + ) { + + /* 獲取堆疊的長度 */ + fun size(): Int { + return stkSize + } + + /* 判斷堆疊是否為空 */ + fun isEmpty(): Boolean { + return size() == 0 + } + + /* 入堆疊 */ + fun push(num: Int) { + val node = ListNode(num) + node.next = stackPeek + stackPeek = node + stkSize++ + } + + /* 出堆疊 */ + fun pop(): Int? { + val num = peek() + stackPeek = stackPeek?.next + stkSize--; + return num + } + + /* 訪問堆疊頂元素 */ + fun peek(): Int? { + if (isEmpty()) throw IndexOutOfBoundsException() + return stackPeek?.value + } + + /* 將 List 轉化為 Array 並返回 */ + fun toArray(): IntArray { + var node = stackPeek + val res = IntArray(size()) + for (i in res.size - 1 downTo 0) { + res[i] = node?.value!! + node = node.next + } + return res + } + } + ``` + +=== "Ruby" + + ```ruby title="linkedlist_stack.rb" + [class]{LinkedListStack}-[func]{} + ``` + +=== "Zig" + + ```zig title="linkedlist_stack.zig" + // 基於鏈結串列實現的堆疊 + fn LinkedListStack(comptime T: type) type { + return struct { + const Self = @This(); + + stack_top: ?*inc.ListNode(T) = null, // 將頭節點作為堆疊頂 + stk_size: usize = 0, // 堆疊的長度 + mem_arena: ?std.heap.ArenaAllocator = null, + mem_allocator: std.mem.Allocator = undefined, // 記憶體分配器 + + // 建構子(分配記憶體+初始化堆疊) + pub fn init(self: *Self, allocator: std.mem.Allocator) !void { + if (self.mem_arena == null) { + self.mem_arena = std.heap.ArenaAllocator.init(allocator); + self.mem_allocator = self.mem_arena.?.allocator(); + } + self.stack_top = null; + self.stk_size = 0; + } + + // 析構函式(釋放記憶體) + pub fn deinit(self: *Self) void { + if (self.mem_arena == null) return; + self.mem_arena.?.deinit(); + } + + // 獲取堆疊的長度 + pub fn size(self: *Self) usize { + return self.stk_size; + } + + // 判斷堆疊是否為空 + pub fn isEmpty(self: *Self) bool { + return self.size() == 0; + } + + // 訪問堆疊頂元素 + pub fn peek(self: *Self) T { + if (self.size() == 0) @panic("堆疊為空"); + return self.stack_top.?.val; + } + + // 入堆疊 + pub fn push(self: *Self, num: T) !void { + var node = try self.mem_allocator.create(inc.ListNode(T)); + node.init(num); + node.next = self.stack_top; + self.stack_top = node; + self.stk_size += 1; + } + + // 出堆疊 + pub fn pop(self: *Self) T { + var num = self.peek(); + self.stack_top = self.stack_top.?.next; + self.stk_size -= 1; + return num; + } + + // 將堆疊轉換為陣列 + pub fn toArray(self: *Self) ![]T { + var node = self.stack_top; + var res = try self.mem_allocator.alloc(T, self.size()); + @memset(res, @as(T, 0)); + var i: usize = 0; + while (i < res.len) : (i += 1) { + res[res.len - i - 1] = node.?.val; + node = node.?.next; + } + return res; + } + }; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +### 2.   基於陣列的實現 + +使用陣列實現堆疊時,我們可以將陣列的尾部作為堆疊頂。如圖 5-3 所示,入堆疊與出堆疊操作分別對應在陣列尾部新增元素與刪除元素,時間複雜度都為 $O(1)$ 。 + +=== "ArrayStack" + ![基於陣列實現堆疊的入堆疊出堆疊操作](stack.assets/array_stack_step1.png){ class="animation-figure" } + +=== "push()" + ![array_stack_push](stack.assets/array_stack_step2_push.png){ class="animation-figure" } + +=== "pop()" + ![array_stack_pop](stack.assets/array_stack_step3_pop.png){ class="animation-figure" } + +

圖 5-3   基於陣列實現堆疊的入堆疊出堆疊操作

+ +由於入堆疊的元素可能會源源不斷地增加,因此我們可以使用動態陣列,這樣就無須自行處理陣列擴容問題。以下為示例程式碼: + +=== "Python" + + ```python title="array_stack.py" + class ArrayStack: + """基於陣列實現的堆疊""" + + def __init__(self): + """建構子""" + self._stack: list[int] = [] + + def size(self) -> int: + """獲取堆疊的長度""" + return len(self._stack) + + def is_empty(self) -> bool: + """判斷堆疊是否為空""" + return self._stack == [] + + def push(self, item: int): + """入堆疊""" + self._stack.append(item) + + def pop(self) -> int: + """出堆疊""" + if self.is_empty(): + raise IndexError("堆疊為空") + return self._stack.pop() + + def peek(self) -> int: + """訪問堆疊頂元素""" + if self.is_empty(): + raise IndexError("堆疊為空") + return self._stack[-1] + + def to_list(self) -> list[int]: + """返回串列用於列印""" + return self._stack + ``` + +=== "C++" + + ```cpp title="array_stack.cpp" + /* 基於陣列實現的堆疊 */ + class ArrayStack { + private: + vector stack; + + public: + /* 獲取堆疊的長度 */ + int size() { + return stack.size(); + } + + /* 判斷堆疊是否為空 */ + bool isEmpty() { + return stack.size() == 0; + } + + /* 入堆疊 */ + void push(int num) { + stack.push_back(num); + } + + /* 出堆疊 */ + int pop() { + int num = top(); + stack.pop_back(); + return num; + } + + /* 訪問堆疊頂元素 */ + int top() { + if (isEmpty()) + throw out_of_range("堆疊為空"); + return stack.back(); + } + + /* 返回 Vector */ + vector toVector() { + return stack; + } + }; + ``` + +=== "Java" + + ```java title="array_stack.java" + /* 基於陣列實現的堆疊 */ + class ArrayStack { + private ArrayList stack; + + public ArrayStack() { + // 初始化串列(動態陣列) + stack = new ArrayList<>(); + } + + /* 獲取堆疊的長度 */ + public int size() { + return stack.size(); + } + + /* 判斷堆疊是否為空 */ + public boolean isEmpty() { + return size() == 0; + } + + /* 入堆疊 */ + public void push(int num) { + stack.add(num); + } + + /* 出堆疊 */ + public int pop() { + if (isEmpty()) + throw new IndexOutOfBoundsException(); + return stack.remove(size() - 1); + } + + /* 訪問堆疊頂元素 */ + public int peek() { + if (isEmpty()) + throw new IndexOutOfBoundsException(); + return stack.get(size() - 1); + } + + /* 將 List 轉化為 Array 並返回 */ + public Object[] toArray() { + return stack.toArray(); + } + } + ``` + +=== "C#" + + ```csharp title="array_stack.cs" + /* 基於陣列實現的堆疊 */ + class ArrayStack { + List stack; + public ArrayStack() { + // 初始化串列(動態陣列) + stack = []; + } + + /* 獲取堆疊的長度 */ + public int Size() { + return stack.Count; + } + + /* 判斷堆疊是否為空 */ + public bool IsEmpty() { + return Size() == 0; + } + + /* 入堆疊 */ + public void Push(int num) { + stack.Add(num); + } + + /* 出堆疊 */ + public int Pop() { + if (IsEmpty()) + throw new Exception(); + var val = Peek(); + stack.RemoveAt(Size() - 1); + return val; + } + + /* 訪問堆疊頂元素 */ + public int Peek() { + if (IsEmpty()) + throw new Exception(); + return stack[Size() - 1]; + } + + /* 將 List 轉化為 Array 並返回 */ + public int[] ToArray() { + return [.. stack]; + } + } + ``` + +=== "Go" + + ```go title="array_stack.go" + /* 基於陣列實現的堆疊 */ + type arrayStack struct { + data []int // 資料 + } + + /* 初始化堆疊 */ + func newArrayStack() *arrayStack { + return &arrayStack{ + // 設定堆疊的長度為 0,容量為 16 + data: make([]int, 0, 16), + } + } + + /* 堆疊的長度 */ + func (s *arrayStack) size() int { + return len(s.data) + } + + /* 堆疊是否為空 */ + func (s *arrayStack) isEmpty() bool { + return s.size() == 0 + } + + /* 入堆疊 */ + func (s *arrayStack) push(v int) { + // 切片會自動擴容 + s.data = append(s.data, v) + } + + /* 出堆疊 */ + func (s *arrayStack) pop() any { + val := s.peek() + s.data = s.data[:len(s.data)-1] + return val + } + + /* 獲取堆疊頂元素 */ + func (s *arrayStack) peek() any { + if s.isEmpty() { + return nil + } + val := s.data[len(s.data)-1] + return val + } + + /* 獲取 Slice 用於列印 */ + func (s *arrayStack) toSlice() []int { + return s.data + } + ``` + +=== "Swift" + + ```swift title="array_stack.swift" + /* 基於陣列實現的堆疊 */ + class ArrayStack { + private var stack: [Int] + + init() { + // 初始化串列(動態陣列) + stack = [] + } + + /* 獲取堆疊的長度 */ + func size() -> Int { + stack.count + } + + /* 判斷堆疊是否為空 */ + func isEmpty() -> Bool { + stack.isEmpty + } + + /* 入堆疊 */ + func push(num: Int) { + stack.append(num) + } + + /* 出堆疊 */ + @discardableResult + func pop() -> Int { + if isEmpty() { + fatalError("堆疊為空") + } + return stack.removeLast() + } + + /* 訪問堆疊頂元素 */ + func peek() -> Int { + if isEmpty() { + fatalError("堆疊為空") + } + return stack.last! + } + + /* 將 List 轉化為 Array 並返回 */ + func toArray() -> [Int] { + stack + } + } + ``` + +=== "JS" + + ```javascript title="array_stack.js" + /* 基於陣列實現的堆疊 */ + class ArrayStack { + #stack; + constructor() { + this.#stack = []; + } + + /* 獲取堆疊的長度 */ + get size() { + return this.#stack.length; + } + + /* 判斷堆疊是否為空 */ + isEmpty() { + return this.#stack.length === 0; + } + + /* 入堆疊 */ + push(num) { + this.#stack.push(num); + } + + /* 出堆疊 */ + pop() { + if (this.isEmpty()) throw new Error('堆疊為空'); + return this.#stack.pop(); + } + + /* 訪問堆疊頂元素 */ + top() { + if (this.isEmpty()) throw new Error('堆疊為空'); + return this.#stack[this.#stack.length - 1]; + } + + /* 返回 Array */ + toArray() { + return this.#stack; + } + } + ``` + +=== "TS" + + ```typescript title="array_stack.ts" + /* 基於陣列實現的堆疊 */ + class ArrayStack { + private stack: number[]; + constructor() { + this.stack = []; + } + + /* 獲取堆疊的長度 */ + get size(): number { + return this.stack.length; + } + + /* 判斷堆疊是否為空 */ + isEmpty(): boolean { + return this.stack.length === 0; + } + + /* 入堆疊 */ + push(num: number): void { + this.stack.push(num); + } + + /* 出堆疊 */ + pop(): number | undefined { + if (this.isEmpty()) throw new Error('堆疊為空'); + return this.stack.pop(); + } + + /* 訪問堆疊頂元素 */ + top(): number | undefined { + if (this.isEmpty()) throw new Error('堆疊為空'); + return this.stack[this.stack.length - 1]; + } + + /* 返回 Array */ + toArray() { + return this.stack; + } + } + ``` + +=== "Dart" + + ```dart title="array_stack.dart" + /* 基於陣列實現的堆疊 */ + class ArrayStack { + late List _stack; + ArrayStack() { + _stack = []; + } + + /* 獲取堆疊的長度 */ + int size() { + return _stack.length; + } + + /* 判斷堆疊是否為空 */ + bool isEmpty() { + return _stack.isEmpty; + } + + /* 入堆疊 */ + void push(int _num) { + _stack.add(_num); + } + + /* 出堆疊 */ + int pop() { + if (isEmpty()) { + throw Exception("堆疊為空"); + } + return _stack.removeLast(); + } + + /* 訪問堆疊頂元素 */ + int peek() { + if (isEmpty()) { + throw Exception("堆疊為空"); + } + return _stack.last; + } + + /* 將堆疊轉化為 Array 並返回 */ + List toArray() => _stack; + } + ``` + +=== "Rust" + + ```rust title="array_stack.rs" + /* 基於陣列實現的堆疊 */ + struct ArrayStack { + stack: Vec, + } + + impl ArrayStack { + /* 初始化堆疊 */ + fn new() -> ArrayStack { + ArrayStack:: { + stack: Vec::::new(), + } + } + + /* 獲取堆疊的長度 */ + fn size(&self) -> usize { + self.stack.len() + } + + /* 判斷堆疊是否為空 */ + fn is_empty(&self) -> bool { + self.size() == 0 + } + + /* 入堆疊 */ + fn push(&mut self, num: T) { + self.stack.push(num); + } + + /* 出堆疊 */ + fn pop(&mut self) -> Option { + self.stack.pop() + } + + /* 訪問堆疊頂元素 */ + fn peek(&self) -> Option<&T> { + if self.is_empty() { + panic!("堆疊為空") + }; + self.stack.last() + } + + /* 返回 &Vec */ + fn to_array(&self) -> &Vec { + &self.stack + } + } + ``` + +=== "C" + + ```c title="array_stack.c" + /* 基於陣列實現的堆疊 */ + typedef struct { + int *data; + int size; + } ArrayStack; + + /* 建構子 */ + ArrayStack *newArrayStack() { + ArrayStack *stack = malloc(sizeof(ArrayStack)); + // 初始化一個大容量,避免擴容 + stack->data = malloc(sizeof(int) * MAX_SIZE); + stack->size = 0; + return stack; + } + + /* 析構函式 */ + void delArrayStack(ArrayStack *stack) { + free(stack->data); + free(stack); + } + + /* 獲取堆疊的長度 */ + int size(ArrayStack *stack) { + return stack->size; + } + + /* 判斷堆疊是否為空 */ + bool isEmpty(ArrayStack *stack) { + return stack->size == 0; + } + + /* 入堆疊 */ + void push(ArrayStack *stack, int num) { + if (stack->size == MAX_SIZE) { + printf("堆疊已滿\n"); + return; + } + stack->data[stack->size] = num; + stack->size++; + } + + /* 訪問堆疊頂元素 */ + int peek(ArrayStack *stack) { + if (stack->size == 0) { + printf("堆疊為空\n"); + return INT_MAX; + } + return stack->data[stack->size - 1]; + } + + /* 出堆疊 */ + int pop(ArrayStack *stack) { + int val = peek(stack); + stack->size--; + return val; + } + ``` + +=== "Kotlin" + + ```kotlin title="array_stack.kt" + /* 基於陣列實現的堆疊 */ + class ArrayStack { + // 初始化串列(動態陣列) + private val stack = ArrayList() + + /* 獲取堆疊的長度 */ + fun size(): Int { + return stack.size + } + + /* 判斷堆疊是否為空 */ + fun isEmpty(): Boolean { + return size() == 0 + } + + /* 入堆疊 */ + fun push(num: Int) { + stack.add(num) + } + + /* 出堆疊 */ + fun pop(): Int { + if (isEmpty()) throw IndexOutOfBoundsException() + return stack.removeAt(size() - 1) + } + + /* 訪問堆疊頂元素 */ + fun peek(): Int { + if (isEmpty()) throw IndexOutOfBoundsException() + return stack[size() - 1] + } + + /* 將 List 轉化為 Array 並返回 */ + fun toArray(): Array { + return stack.toArray() + } + } + ``` + +=== "Ruby" + + ```ruby title="array_stack.rb" + [class]{ArrayStack}-[func]{} + ``` + +=== "Zig" + + ```zig title="array_stack.zig" + // 基於陣列實現的堆疊 + fn ArrayStack(comptime T: type) type { + return struct { + const Self = @This(); + + stack: ?std.ArrayList(T) = null, + + // 建構子(分配記憶體+初始化堆疊) + pub fn init(self: *Self, allocator: std.mem.Allocator) void { + if (self.stack == null) { + self.stack = std.ArrayList(T).init(allocator); + } + } + + // 析構方法(釋放記憶體) + pub fn deinit(self: *Self) void { + if (self.stack == null) return; + self.stack.?.deinit(); + } + + // 獲取堆疊的長度 + pub fn size(self: *Self) usize { + return self.stack.?.items.len; + } + + // 判斷堆疊是否為空 + pub fn isEmpty(self: *Self) bool { + return self.size() == 0; + } + + // 訪問堆疊頂元素 + pub fn peek(self: *Self) T { + if (self.isEmpty()) @panic("堆疊為空"); + return self.stack.?.items[self.size() - 1]; + } + + // 入堆疊 + pub fn push(self: *Self, num: T) !void { + try self.stack.?.append(num); + } + + // 出堆疊 + pub fn pop(self: *Self) T { + var num = self.stack.?.pop(); + return num; + } + + // 返回 ArrayList + pub fn toList(self: *Self) std.ArrayList(T) { + return self.stack.?; + } + }; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 5.1.3   兩種實現對比 + +**支持操作** + +兩種實現都支持堆疊定義中的各項操作。陣列實現額外支持隨機訪問,但這已超出了堆疊的定義範疇,因此一般不會用到。 + +**時間效率** + +在基於陣列的實現中,入堆疊和出堆疊操作都在預先分配好的連續記憶體中進行,具有很好的快取本地性,因此效率較高。然而,如果入堆疊時超出陣列容量,會觸發擴容機制,導致該次入堆疊操作的時間複雜度變為 $O(n)$ 。 + +在基於鏈結串列的實現中,鏈結串列的擴容非常靈活,不存在上述陣列擴容時效率降低的問題。但是,入堆疊操作需要初始化節點物件並修改指標,因此效率相對較低。不過,如果入堆疊元素本身就是節點物件,那麼可以省去初始化步驟,從而提高效率。 + +綜上所述,當入堆疊與出堆疊操作的元素是基本資料型別時,例如 `int` 或 `double` ,我們可以得出以下結論。 + +- 基於陣列實現的堆疊在觸發擴容時效率會降低,但由於擴容是低頻操作,因此平均效率更高。 +- 基於鏈結串列實現的堆疊可以提供更加穩定的效率表現。 + +**空間效率** + +在初始化串列時,系統會為串列分配“初始容量”,該容量可能超出實際需求;並且,擴容機制通常是按照特定倍率(例如 2 倍)進行擴容的,擴容後的容量也可能超出實際需求。因此,**基於陣列實現的堆疊可能造成一定的空間浪費**。 + +然而,由於鏈結串列節點需要額外儲存指標,**因此鏈結串列節點佔用的空間相對較大**。 + +綜上,我們不能簡單地確定哪種實現更加節省記憶體,需要針對具體情況進行分析。 + +## 5.1.4   堆疊的典型應用 + +- **瀏覽器中的後退與前進、軟體中的撤銷與反撤銷**。每當我們開啟新的網頁,瀏覽器就會對上一個網頁執行入堆疊,這樣我們就可以通過後退操作回到上一個網頁。後退操作實際上是在執行出堆疊。如果要同時支持後退和前進,那麼需要兩個堆疊來配合實現。 +- **程式記憶體管理**。每次呼叫函式時,系統都會在堆疊頂新增一個堆疊幀,用於記錄函式的上下文資訊。在遞迴函式中,向下遞推階段會不斷執行入堆疊操作,而向上回溯階段則會不斷執行出堆疊操作。 diff --git a/zh-Hant/docs/chapter_stack_and_queue/summary.md b/zh-Hant/docs/chapter_stack_and_queue/summary.md new file mode 100644 index 000000000..41833ea94 --- /dev/null +++ b/zh-Hant/docs/chapter_stack_and_queue/summary.md @@ -0,0 +1,35 @@ +--- +comments: true +--- + +# 5.4   小結 + +### 1.   重點回顧 + +- 堆疊是一種遵循先入後出原則的資料結構,可透過陣列或鏈結串列來實現。 +- 在時間效率方面,堆疊的陣列實現具有較高的平均效率,但在擴容過程中,單次入堆疊操作的時間複雜度會劣化至 $O(n)$ 。相比之下,堆疊的鏈結串列實現具有更為穩定的效率表現。 +- 在空間效率方面,堆疊的陣列實現可能導致一定程度的空間浪費。但需要注意的是,鏈結串列節點所佔用的記憶體空間比陣列元素更大。 +- 佇列是一種遵循先入先出原則的資料結構,同樣可以透過陣列或鏈結串列來實現。在時間效率和空間效率的對比上,佇列的結論與前述堆疊的結論相似。 +- 雙向佇列是一種具有更高自由度的佇列,它允許在兩端進行元素的新增和刪除操作。 + +### 2.   Q & A + +**Q**:瀏覽器的前進後退是否是雙向鏈結串列實現? + +瀏覽器的前進後退功能本質上是“堆疊”的體現。當用戶訪問一個新頁面時,該頁面會被新增到堆疊頂;當用戶點選後退按鈕時,該頁面會從堆疊頂彈出。使用雙向佇列可以方便地實現一些額外操作,這個在“雙向佇列”章節有提到。 + +**Q**:在出堆疊後,是否需要釋放出堆疊節點的記憶體? + +如果後續仍需要使用彈出節點,則不需要釋放記憶體。若之後不需要用到,`Java` 和 `Python` 等語言擁有自動垃圾回收機制,因此不需要手動釋放記憶體;在 `C` 和 `C++` 中需要手動釋放記憶體。 + +**Q**:雙向佇列像是兩個堆疊拼接在了一起,它的用途是什麼? + +雙向佇列就像是堆疊和佇列的組合或兩個堆疊拼在了一起。它表現的是堆疊 + 佇列的邏輯,因此可以實現堆疊與佇列的所有應用,並且更加靈活。 + +**Q**:撤銷(undo)和反撤銷(redo)具體是如何實現的? + +使用兩個堆疊,堆疊 `A` 用於撤銷,堆疊 `B` 用於反撤銷。 + +1. 每當使用者執行一個操作,將這個操作壓入堆疊 `A` ,並清空堆疊 `B` 。 +2. 當用戶執行“撤銷”時,從堆疊 `A` 中彈出最近的操作,並將其壓入堆疊 `B` 。 +3. 當用戶執行“反撤銷”時,從堆疊 `B` 中彈出最近的操作,並將其壓入堆疊 `A` 。 diff --git a/zh-Hant/docs/chapter_tree/array_representation_of_tree.md b/zh-Hant/docs/chapter_tree/array_representation_of_tree.md new file mode 100644 index 000000000..02a94980f --- /dev/null +++ b/zh-Hant/docs/chapter_tree/array_representation_of_tree.md @@ -0,0 +1,1280 @@ +--- +comments: true +--- + +# 7.3   二元樹陣列表示 + +在鏈結串列表示下,二元樹的儲存單元為節點 `TreeNode` ,節點之間透過指標相連線。上一節介紹了鏈結串列表示下的二元樹的各項基本操作。 + +那麼,我們能否用陣列來表示二元樹呢?答案是肯定的。 + +## 7.3.1   表示完美二元樹 + +先分析一個簡單案例。給定一棵完美二元樹,我們將所有節點按照層序走訪的順序儲存在一個陣列中,則每個節點都對應唯一的陣列索引。 + +根據層序走訪的特性,我們可以推導出父節點索引與子節點索引之間的“對映公式”:**若某節點的索引為 $i$ ,則該節點的左子節點索引為 $2i + 1$ ,右子節點索引為 $2i + 2$** 。圖 7-12 展示了各個節點索引之間的對映關係。 + +![完美二元樹的陣列表示](array_representation_of_tree.assets/array_representation_binary_tree.png){ class="animation-figure" } + +

圖 7-12   完美二元樹的陣列表示

+ +**對映公式的角色相當於鏈結串列中的節點引用(指標)**。給定陣列中的任意一個節點,我們都可以透過對映公式來訪問它的左(右)子節點。 + +## 7.3.2   表示任意二元樹 + +完美二元樹是一個特例,在二元樹的中間層通常存在許多 `None` 。由於層序走訪序列並不包含這些 `None` ,因此我們無法僅憑該序列來推測 `None` 的數量和分佈位置。**這意味著存在多種二元樹結構都符合該層序走訪序列**。 + +如圖 7-13 所示,給定一棵非完美二元樹,上述陣列表示方法已經失效。 + +![層序走訪序列對應多種二元樹可能性](array_representation_of_tree.assets/array_representation_without_empty.png){ class="animation-figure" } + +

圖 7-13   層序走訪序列對應多種二元樹可能性

+ +為了解決此問題,**我們可以考慮在層序走訪序列中顯式地寫出所有 `None`** 。如圖 7-14 所示,這樣處理後,層序走訪序列就可以唯一表示二元樹了。示例程式碼如下: + +=== "Python" + + ```python title="" + # 二元樹的陣列表示 + # 使用 None 來表示空位 + tree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15] + ``` + +=== "C++" + + ```cpp title="" + /* 二元樹的陣列表示 */ + // 使用 int 最大值 INT_MAX 標記空位 + vector tree = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15}; + ``` + +=== "Java" + + ```java title="" + /* 二元樹的陣列表示 */ + // 使用 int 的包裝類別 Integer ,就可以使用 null 來標記空位 + Integer[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 }; + ``` + +=== "C#" + + ```csharp title="" + /* 二元樹的陣列表示 */ + // 使用 int? 可空型別 ,就可以使用 null 來標記空位 + int?[] tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15]; + ``` + +=== "Go" + + ```go title="" + /* 二元樹的陣列表示 */ + // 使用 any 型別的切片, 就可以使用 nil 來標記空位 + tree := []any{1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15} + ``` + +=== "Swift" + + ```swift title="" + /* 二元樹的陣列表示 */ + // 使用 Int? 可空型別 ,就可以使用 nil 來標記空位 + let tree: [Int?] = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15] + ``` + +=== "JS" + + ```javascript title="" + /* 二元樹的陣列表示 */ + // 使用 null 來表示空位 + let tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15]; + ``` + +=== "TS" + + ```typescript title="" + /* 二元樹的陣列表示 */ + // 使用 null 來表示空位 + let tree: (number | null)[] = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15]; + ``` + +=== "Dart" + + ```dart title="" + /* 二元樹的陣列表示 */ + // 使用 int? 可空型別 ,就可以使用 null 來標記空位 + List tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15]; + ``` + +=== "Rust" + + ```rust title="" + /* 二元樹的陣列表示 */ + // 使用 None 來標記空位 + let tree = [Some(1), Some(2), Some(3), Some(4), None, Some(6), Some(7), Some(8), Some(9), None, None, Some(12), None, None, Some(15)]; + ``` + +=== "C" + + ```c title="" + /* 二元樹的陣列表示 */ + // 使用 int 最大值標記空位,因此要求節點值不能為 INT_MAX + int tree[] = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15}; + ``` + +=== "Kotlin" + + ```kotlin title="" + /* 二元樹的陣列表示 */ + // 使用 null 來表示空位 + val tree = mutableListOf( 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 ) + ``` + +=== "Ruby" + + ```ruby title="" + + ``` + +=== "Zig" + + ```zig title="" + + ``` + +![任意型別二元樹的陣列表示](array_representation_of_tree.assets/array_representation_with_empty.png){ class="animation-figure" } + +

圖 7-14   任意型別二元樹的陣列表示

+ +值得說明的是,**完全二元樹非常適合使用陣列來表示**。回顧完全二元樹的定義,`None` 只出現在最底層且靠右的位置,**因此所有 `None` 一定出現在層序走訪序列的末尾**。 + +這意味著使用陣列表示完全二元樹時,可以省略儲存所有 `None` ,非常方便。圖 7-15 給出了一個例子。 + +![完全二元樹的陣列表示](array_representation_of_tree.assets/array_representation_complete_binary_tree.png){ class="animation-figure" } + +

圖 7-15   完全二元樹的陣列表示

+ +以下程式碼實現了一棵基於陣列表示的二元樹,包括以下幾種操作。 + +- 給定某節點,獲取它的值、左(右)子節點、父節點。 +- 獲取前序走訪、中序走訪、後序走訪、層序走訪序列。 + +=== "Python" + + ```python title="array_binary_tree.py" + class ArrayBinaryTree: + """陣列表示下的二元樹類別""" + + def __init__(self, arr: list[int | None]): + """建構子""" + self._tree = list(arr) + + def size(self): + """串列容量""" + return len(self._tree) + + def val(self, i: int) -> int: + """獲取索引為 i 節點的值""" + # 若索引越界,則返回 None ,代表空位 + if i < 0 or i >= self.size(): + return None + return self._tree[i] + + def left(self, i: int) -> int | None: + """獲取索引為 i 節點的左子節點的索引""" + return 2 * i + 1 + + def right(self, i: int) -> int | None: + """獲取索引為 i 節點的右子節點的索引""" + return 2 * i + 2 + + def parent(self, i: int) -> int | None: + """獲取索引為 i 節點的父節點的索引""" + return (i - 1) // 2 + + def level_order(self) -> list[int]: + """層序走訪""" + self.res = [] + # 直接走訪陣列 + for i in range(self.size()): + if self.val(i) is not None: + self.res.append(self.val(i)) + return self.res + + def dfs(self, i: int, order: str): + """深度優先走訪""" + if self.val(i) is None: + return + # 前序走訪 + if order == "pre": + self.res.append(self.val(i)) + self.dfs(self.left(i), order) + # 中序走訪 + if order == "in": + self.res.append(self.val(i)) + self.dfs(self.right(i), order) + # 後序走訪 + if order == "post": + self.res.append(self.val(i)) + + def pre_order(self) -> list[int]: + """前序走訪""" + self.res = [] + self.dfs(0, order="pre") + return self.res + + def in_order(self) -> list[int]: + """中序走訪""" + self.res = [] + self.dfs(0, order="in") + return self.res + + def post_order(self) -> list[int]: + """後序走訪""" + self.res = [] + self.dfs(0, order="post") + return self.res + ``` + +=== "C++" + + ```cpp title="array_binary_tree.cpp" + /* 陣列表示下的二元樹類別 */ + class ArrayBinaryTree { + public: + /* 建構子 */ + ArrayBinaryTree(vector arr) { + tree = arr; + } + + /* 串列容量 */ + int size() { + return tree.size(); + } + + /* 獲取索引為 i 節點的值 */ + int val(int i) { + // 若索引越界,則返回 INT_MAX ,代表空位 + if (i < 0 || i >= size()) + return INT_MAX; + return tree[i]; + } + + /* 獲取索引為 i 節點的左子節點的索引 */ + int left(int i) { + return 2 * i + 1; + } + + /* 獲取索引為 i 節點的右子節點的索引 */ + int right(int i) { + return 2 * i + 2; + } + + /* 獲取索引為 i 節點的父節點的索引 */ + int parent(int i) { + return (i - 1) / 2; + } + + /* 層序走訪 */ + vector levelOrder() { + vector res; + // 直接走訪陣列 + for (int i = 0; i < size(); i++) { + if (val(i) != INT_MAX) + res.push_back(val(i)); + } + return res; + } + + /* 前序走訪 */ + vector preOrder() { + vector res; + dfs(0, "pre", res); + return res; + } + + /* 中序走訪 */ + vector inOrder() { + vector res; + dfs(0, "in", res); + return res; + } + + /* 後序走訪 */ + vector postOrder() { + vector res; + dfs(0, "post", res); + return res; + } + + private: + vector tree; + + /* 深度優先走訪 */ + void dfs(int i, string order, vector &res) { + // 若為空位,則返回 + if (val(i) == INT_MAX) + return; + // 前序走訪 + if (order == "pre") + res.push_back(val(i)); + dfs(left(i), order, res); + // 中序走訪 + if (order == "in") + res.push_back(val(i)); + dfs(right(i), order, res); + // 後序走訪 + if (order == "post") + res.push_back(val(i)); + } + }; + ``` + +=== "Java" + + ```java title="array_binary_tree.java" + /* 陣列表示下的二元樹類別 */ + class ArrayBinaryTree { + private List tree; + + /* 建構子 */ + public ArrayBinaryTree(List arr) { + tree = new ArrayList<>(arr); + } + + /* 串列容量 */ + public int size() { + return tree.size(); + } + + /* 獲取索引為 i 節點的值 */ + public Integer val(int i) { + // 若索引越界,則返回 null ,代表空位 + if (i < 0 || i >= size()) + return null; + return tree.get(i); + } + + /* 獲取索引為 i 節點的左子節點的索引 */ + public Integer left(int i) { + return 2 * i + 1; + } + + /* 獲取索引為 i 節點的右子節點的索引 */ + public Integer right(int i) { + return 2 * i + 2; + } + + /* 獲取索引為 i 節點的父節點的索引 */ + public Integer parent(int i) { + return (i - 1) / 2; + } + + /* 層序走訪 */ + public List levelOrder() { + List res = new ArrayList<>(); + // 直接走訪陣列 + for (int i = 0; i < size(); i++) { + if (val(i) != null) + res.add(val(i)); + } + return res; + } + + /* 深度優先走訪 */ + private void dfs(Integer i, String order, List res) { + // 若為空位,則返回 + if (val(i) == null) + return; + // 前序走訪 + if ("pre".equals(order)) + res.add(val(i)); + dfs(left(i), order, res); + // 中序走訪 + if ("in".equals(order)) + res.add(val(i)); + dfs(right(i), order, res); + // 後序走訪 + if ("post".equals(order)) + res.add(val(i)); + } + + /* 前序走訪 */ + public List preOrder() { + List res = new ArrayList<>(); + dfs(0, "pre", res); + return res; + } + + /* 中序走訪 */ + public List inOrder() { + List res = new ArrayList<>(); + dfs(0, "in", res); + return res; + } + + /* 後序走訪 */ + public List postOrder() { + List res = new ArrayList<>(); + dfs(0, "post", res); + return res; + } + } + ``` + +=== "C#" + + ```csharp title="array_binary_tree.cs" + /* 陣列表示下的二元樹類別 */ + class ArrayBinaryTree(List arr) { + List tree = new(arr); + + /* 串列容量 */ + public int Size() { + return tree.Count; + } + + /* 獲取索引為 i 節點的值 */ + public int? Val(int i) { + // 若索引越界,則返回 null ,代表空位 + if (i < 0 || i >= Size()) + return null; + return tree[i]; + } + + /* 獲取索引為 i 節點的左子節點的索引 */ + public int Left(int i) { + return 2 * i + 1; + } + + /* 獲取索引為 i 節點的右子節點的索引 */ + public int Right(int i) { + return 2 * i + 2; + } + + /* 獲取索引為 i 節點的父節點的索引 */ + public int Parent(int i) { + return (i - 1) / 2; + } + + /* 層序走訪 */ + public List LevelOrder() { + List res = []; + // 直接走訪陣列 + for (int i = 0; i < Size(); i++) { + if (Val(i).HasValue) + res.Add(Val(i)!.Value); + } + return res; + } + + /* 深度優先走訪 */ + void DFS(int i, string order, List res) { + // 若為空位,則返回 + if (!Val(i).HasValue) + return; + // 前序走訪 + if (order == "pre") + res.Add(Val(i)!.Value); + DFS(Left(i), order, res); + // 中序走訪 + if (order == "in") + res.Add(Val(i)!.Value); + DFS(Right(i), order, res); + // 後序走訪 + if (order == "post") + res.Add(Val(i)!.Value); + } + + /* 前序走訪 */ + public List PreOrder() { + List res = []; + DFS(0, "pre", res); + return res; + } + + /* 中序走訪 */ + public List InOrder() { + List res = []; + DFS(0, "in", res); + return res; + } + + /* 後序走訪 */ + public List PostOrder() { + List res = []; + DFS(0, "post", res); + return res; + } + } + ``` + +=== "Go" + + ```go title="array_binary_tree.go" + /* 陣列表示下的二元樹類別 */ + type arrayBinaryTree struct { + tree []any + } + + /* 建構子 */ + func newArrayBinaryTree(arr []any) *arrayBinaryTree { + return &arrayBinaryTree{ + tree: arr, + } + } + + /* 串列容量 */ + func (abt *arrayBinaryTree) size() int { + return len(abt.tree) + } + + /* 獲取索引為 i 節點的值 */ + func (abt *arrayBinaryTree) val(i int) any { + // 若索引越界,則返回 null ,代表空位 + if i < 0 || i >= abt.size() { + return nil + } + return abt.tree[i] + } + + /* 獲取索引為 i 節點的左子節點的索引 */ + func (abt *arrayBinaryTree) left(i int) int { + return 2*i + 1 + } + + /* 獲取索引為 i 節點的右子節點的索引 */ + func (abt *arrayBinaryTree) right(i int) int { + return 2*i + 2 + } + + /* 獲取索引為 i 節點的父節點的索引 */ + func (abt *arrayBinaryTree) parent(i int) int { + return (i - 1) / 2 + } + + /* 層序走訪 */ + func (abt *arrayBinaryTree) levelOrder() []any { + var res []any + // 直接走訪陣列 + for i := 0; i < abt.size(); i++ { + if abt.val(i) != nil { + res = append(res, abt.val(i)) + } + } + return res + } + + /* 深度優先走訪 */ + func (abt *arrayBinaryTree) dfs(i int, order string, res *[]any) { + // 若為空位,則返回 + if abt.val(i) == nil { + return + } + // 前序走訪 + if order == "pre" { + *res = append(*res, abt.val(i)) + } + abt.dfs(abt.left(i), order, res) + // 中序走訪 + if order == "in" { + *res = append(*res, abt.val(i)) + } + abt.dfs(abt.right(i), order, res) + // 後序走訪 + if order == "post" { + *res = append(*res, abt.val(i)) + } + } + + /* 前序走訪 */ + func (abt *arrayBinaryTree) preOrder() []any { + var res []any + abt.dfs(0, "pre", &res) + return res + } + + /* 中序走訪 */ + func (abt *arrayBinaryTree) inOrder() []any { + var res []any + abt.dfs(0, "in", &res) + return res + } + + /* 後序走訪 */ + func (abt *arrayBinaryTree) postOrder() []any { + var res []any + abt.dfs(0, "post", &res) + return res + } + ``` + +=== "Swift" + + ```swift title="array_binary_tree.swift" + /* 陣列表示下的二元樹類別 */ + class ArrayBinaryTree { + private var tree: [Int?] + + /* 建構子 */ + init(arr: [Int?]) { + tree = arr + } + + /* 串列容量 */ + func size() -> Int { + tree.count + } + + /* 獲取索引為 i 節點的值 */ + func val(i: Int) -> Int? { + // 若索引越界,則返回 null ,代表空位 + if i < 0 || i >= size() { + return nil + } + return tree[i] + } + + /* 獲取索引為 i 節點的左子節點的索引 */ + func left(i: Int) -> Int { + 2 * i + 1 + } + + /* 獲取索引為 i 節點的右子節點的索引 */ + func right(i: Int) -> Int { + 2 * i + 2 + } + + /* 獲取索引為 i 節點的父節點的索引 */ + func parent(i: Int) -> Int { + (i - 1) / 2 + } + + /* 層序走訪 */ + func levelOrder() -> [Int] { + var res: [Int] = [] + // 直接走訪陣列 + for i in 0 ..< size() { + if let val = val(i: i) { + res.append(val) + } + } + return res + } + + /* 深度優先走訪 */ + private func dfs(i: Int, order: String, res: inout [Int]) { + // 若為空位,則返回 + guard let val = val(i: i) else { + return + } + // 前序走訪 + if order == "pre" { + res.append(val) + } + dfs(i: left(i: i), order: order, res: &res) + // 中序走訪 + if order == "in" { + res.append(val) + } + dfs(i: right(i: i), order: order, res: &res) + // 後序走訪 + if order == "post" { + res.append(val) + } + } + + /* 前序走訪 */ + func preOrder() -> [Int] { + var res: [Int] = [] + dfs(i: 0, order: "pre", res: &res) + return res + } + + /* 中序走訪 */ + func inOrder() -> [Int] { + var res: [Int] = [] + dfs(i: 0, order: "in", res: &res) + return res + } + + /* 後序走訪 */ + func postOrder() -> [Int] { + var res: [Int] = [] + dfs(i: 0, order: "post", res: &res) + return res + } + } + ``` + +=== "JS" + + ```javascript title="array_binary_tree.js" + /* 陣列表示下的二元樹類別 */ + class ArrayBinaryTree { + #tree; + + /* 建構子 */ + constructor(arr) { + this.#tree = arr; + } + + /* 串列容量 */ + size() { + return this.#tree.length; + } + + /* 獲取索引為 i 節點的值 */ + val(i) { + // 若索引越界,則返回 null ,代表空位 + if (i < 0 || i >= this.size()) return null; + return this.#tree[i]; + } + + /* 獲取索引為 i 節點的左子節點的索引 */ + left(i) { + return 2 * i + 1; + } + + /* 獲取索引為 i 節點的右子節點的索引 */ + right(i) { + return 2 * i + 2; + } + + /* 獲取索引為 i 節點的父節點的索引 */ + parent(i) { + return Math.floor((i - 1) / 2); // 向下整除 + } + + /* 層序走訪 */ + levelOrder() { + let res = []; + // 直接走訪陣列 + for (let i = 0; i < this.size(); i++) { + if (this.val(i) !== null) res.push(this.val(i)); + } + return res; + } + + /* 深度優先走訪 */ + #dfs(i, order, res) { + // 若為空位,則返回 + if (this.val(i) === null) return; + // 前序走訪 + if (order === 'pre') res.push(this.val(i)); + this.#dfs(this.left(i), order, res); + // 中序走訪 + if (order === 'in') res.push(this.val(i)); + this.#dfs(this.right(i), order, res); + // 後序走訪 + if (order === 'post') res.push(this.val(i)); + } + + /* 前序走訪 */ + preOrder() { + const res = []; + this.#dfs(0, 'pre', res); + return res; + } + + /* 中序走訪 */ + inOrder() { + const res = []; + this.#dfs(0, 'in', res); + return res; + } + + /* 後序走訪 */ + postOrder() { + const res = []; + this.#dfs(0, 'post', res); + return res; + } + } + ``` + +=== "TS" + + ```typescript title="array_binary_tree.ts" + /* 陣列表示下的二元樹類別 */ + class ArrayBinaryTree { + #tree: (number | null)[]; + + /* 建構子 */ + constructor(arr: (number | null)[]) { + this.#tree = arr; + } + + /* 串列容量 */ + size(): number { + return this.#tree.length; + } + + /* 獲取索引為 i 節點的值 */ + val(i: number): number | null { + // 若索引越界,則返回 null ,代表空位 + if (i < 0 || i >= this.size()) return null; + return this.#tree[i]; + } + + /* 獲取索引為 i 節點的左子節點的索引 */ + left(i: number): number { + return 2 * i + 1; + } + + /* 獲取索引為 i 節點的右子節點的索引 */ + right(i: number): number { + return 2 * i + 2; + } + + /* 獲取索引為 i 節點的父節點的索引 */ + parent(i: number): number { + return Math.floor((i - 1) / 2); // 向下整除 + } + + /* 層序走訪 */ + levelOrder(): number[] { + let res = []; + // 直接走訪陣列 + for (let i = 0; i < this.size(); i++) { + if (this.val(i) !== null) res.push(this.val(i)); + } + return res; + } + + /* 深度優先走訪 */ + #dfs(i: number, order: Order, res: (number | null)[]): void { + // 若為空位,則返回 + if (this.val(i) === null) return; + // 前序走訪 + if (order === 'pre') res.push(this.val(i)); + this.#dfs(this.left(i), order, res); + // 中序走訪 + if (order === 'in') res.push(this.val(i)); + this.#dfs(this.right(i), order, res); + // 後序走訪 + if (order === 'post') res.push(this.val(i)); + } + + /* 前序走訪 */ + preOrder(): (number | null)[] { + const res = []; + this.#dfs(0, 'pre', res); + return res; + } + + /* 中序走訪 */ + inOrder(): (number | null)[] { + const res = []; + this.#dfs(0, 'in', res); + return res; + } + + /* 後序走訪 */ + postOrder(): (number | null)[] { + const res = []; + this.#dfs(0, 'post', res); + return res; + } + } + ``` + +=== "Dart" + + ```dart title="array_binary_tree.dart" + /* 陣列表示下的二元樹類別 */ + class ArrayBinaryTree { + late List _tree; + + /* 建構子 */ + ArrayBinaryTree(this._tree); + + /* 串列容量 */ + int size() { + return _tree.length; + } + + /* 獲取索引為 i 節點的值 */ + int? val(int i) { + // 若索引越界,則返回 null ,代表空位 + if (i < 0 || i >= size()) { + return null; + } + return _tree[i]; + } + + /* 獲取索引為 i 節點的左子節點的索引 */ + int? left(int i) { + return 2 * i + 1; + } + + /* 獲取索引為 i 節點的右子節點的索引 */ + int? right(int i) { + return 2 * i + 2; + } + + /* 獲取索引為 i 節點的父節點的索引 */ + int? parent(int i) { + return (i - 1) ~/ 2; + } + + /* 層序走訪 */ + List levelOrder() { + List res = []; + for (int i = 0; i < size(); i++) { + if (val(i) != null) { + res.add(val(i)!); + } + } + return res; + } + + /* 深度優先走訪 */ + void dfs(int i, String order, List res) { + // 若為空位,則返回 + if (val(i) == null) { + return; + } + // 前序走訪 + if (order == 'pre') { + res.add(val(i)); + } + dfs(left(i)!, order, res); + // 中序走訪 + if (order == 'in') { + res.add(val(i)); + } + dfs(right(i)!, order, res); + // 後序走訪 + if (order == 'post') { + res.add(val(i)); + } + } + + /* 前序走訪 */ + List preOrder() { + List res = []; + dfs(0, 'pre', res); + return res; + } + + /* 中序走訪 */ + List inOrder() { + List res = []; + dfs(0, 'in', res); + return res; + } + + /* 後序走訪 */ + List postOrder() { + List res = []; + dfs(0, 'post', res); + return res; + } + } + ``` + +=== "Rust" + + ```rust title="array_binary_tree.rs" + /* 陣列表示下的二元樹類別 */ + struct ArrayBinaryTree { + tree: Vec>, + } + + impl ArrayBinaryTree { + /* 建構子 */ + fn new(arr: Vec>) -> Self { + Self { tree: arr } + } + + /* 串列容量 */ + fn size(&self) -> i32 { + self.tree.len() as i32 + } + + /* 獲取索引為 i 節點的值 */ + fn val(&self, i: i32) -> Option { + // 若索引越界,則返回 None ,代表空位 + if i < 0 || i >= self.size() { + None + } else { + self.tree[i as usize] + } + } + + /* 獲取索引為 i 節點的左子節點的索引 */ + fn left(&self, i: i32) -> i32 { + 2 * i + 1 + } + + /* 獲取索引為 i 節點的右子節點的索引 */ + fn right(&self, i: i32) -> i32 { + 2 * i + 2 + } + + /* 獲取索引為 i 節點的父節點的索引 */ + fn parent(&self, i: i32) -> i32 { + (i - 1) / 2 + } + + /* 層序走訪 */ + fn level_order(&self) -> Vec { + let mut res = vec![]; + // 直接走訪陣列 + for i in 0..self.size() { + if let Some(val) = self.val(i) { + res.push(val) + } + } + res + } + + /* 深度優先走訪 */ + fn dfs(&self, i: i32, order: &str, res: &mut Vec) { + if self.val(i).is_none() { + return; + } + let val = self.val(i).unwrap(); + // 前序走訪 + if order == "pre" { + res.push(val); + } + self.dfs(self.left(i), order, res); + // 中序走訪 + if order == "in" { + res.push(val); + } + self.dfs(self.right(i), order, res); + // 後序走訪 + if order == "post" { + res.push(val); + } + } + + /* 前序走訪 */ + fn pre_order(&self) -> Vec { + let mut res = vec![]; + self.dfs(0, "pre", &mut res); + res + } + + /* 中序走訪 */ + fn in_order(&self) -> Vec { + let mut res = vec![]; + self.dfs(0, "in", &mut res); + res + } + + /* 後序走訪 */ + fn post_order(&self) -> Vec { + let mut res = vec![]; + self.dfs(0, "post", &mut res); + res + } + } + ``` + +=== "C" + + ```c title="array_binary_tree.c" + /* 陣列表示下的二元樹結構體 */ + typedef struct { + int *tree; + int size; + } ArrayBinaryTree; + + /* 建構子 */ + ArrayBinaryTree *newArrayBinaryTree(int *arr, int arrSize) { + ArrayBinaryTree *abt = (ArrayBinaryTree *)malloc(sizeof(ArrayBinaryTree)); + abt->tree = malloc(sizeof(int) * arrSize); + memcpy(abt->tree, arr, sizeof(int) * arrSize); + abt->size = arrSize; + return abt; + } + + /* 析構函式 */ + void delArrayBinaryTree(ArrayBinaryTree *abt) { + free(abt->tree); + free(abt); + } + + /* 串列容量 */ + int size(ArrayBinaryTree *abt) { + return abt->size; + } + + /* 獲取索引為 i 節點的值 */ + int val(ArrayBinaryTree *abt, int i) { + // 若索引越界,則返回 INT_MAX ,代表空位 + if (i < 0 || i >= size(abt)) + return INT_MAX; + return abt->tree[i]; + } + + /* 層序走訪 */ + int *levelOrder(ArrayBinaryTree *abt, int *returnSize) { + int *res = (int *)malloc(sizeof(int) * size(abt)); + int index = 0; + // 直接走訪陣列 + for (int i = 0; i < size(abt); i++) { + if (val(abt, i) != INT_MAX) + res[index++] = val(abt, i); + } + *returnSize = index; + return res; + } + + /* 深度優先走訪 */ + void dfs(ArrayBinaryTree *abt, int i, char *order, int *res, int *index) { + // 若為空位,則返回 + if (val(abt, i) == INT_MAX) + return; + // 前序走訪 + if (strcmp(order, "pre") == 0) + res[(*index)++] = val(abt, i); + dfs(abt, left(i), order, res, index); + // 中序走訪 + if (strcmp(order, "in") == 0) + res[(*index)++] = val(abt, i); + dfs(abt, right(i), order, res, index); + // 後序走訪 + if (strcmp(order, "post") == 0) + res[(*index)++] = val(abt, i); + } + + /* 前序走訪 */ + int *preOrder(ArrayBinaryTree *abt, int *returnSize) { + int *res = (int *)malloc(sizeof(int) * size(abt)); + int index = 0; + dfs(abt, 0, "pre", res, &index); + *returnSize = index; + return res; + } + + /* 中序走訪 */ + int *inOrder(ArrayBinaryTree *abt, int *returnSize) { + int *res = (int *)malloc(sizeof(int) * size(abt)); + int index = 0; + dfs(abt, 0, "in", res, &index); + *returnSize = index; + return res; + } + + /* 後序走訪 */ + int *postOrder(ArrayBinaryTree *abt, int *returnSize) { + int *res = (int *)malloc(sizeof(int) * size(abt)); + int index = 0; + dfs(abt, 0, "post", res, &index); + *returnSize = index; + return res; + } + ``` + +=== "Kotlin" + + ```kotlin title="array_binary_tree.kt" + /* 陣列表示下的二元樹類別 */ + class ArrayBinaryTree(val tree: List) { + /* 串列容量 */ + fun size(): Int { + return tree.size + } + + /* 獲取索引為 i 節點的值 */ + fun value(i: Int): Int? { + // 若索引越界,則返回 null ,代表空位 + if (i < 0 || i >= size()) return null + return tree[i] + } + + /* 獲取索引為 i 節點的左子節點的索引 */ + fun left(i: Int): Int { + return 2 * i + 1 + } + + /* 獲取索引為 i 節點的右子節點的索引 */ + fun right(i: Int): Int { + return 2 * i + 2 + } + + /* 獲取索引為 i 節點的父節點的索引 */ + fun parent(i: Int): Int { + return (i - 1) / 2 + } + + /* 層序走訪 */ + fun levelOrder(): List { + val res = ArrayList() + // 直接走訪陣列 + for (i in 0..) { + // 若為空位,則返回 + if (value(i) == null) return + // 前序走訪 + if ("pre" == order) res.add(value(i)) + dfs(left(i), order, res) + // 中序走訪 + if ("in" == order) res.add(value(i)) + dfs(right(i), order, res) + // 後序走訪 + if ("post" == order) res.add(value(i)) + } + + /* 前序走訪 */ + fun preOrder(): List { + val res = ArrayList() + dfs(0, "pre", res) + return res + } + + /* 中序走訪 */ + fun inOrder(): List { + val res = ArrayList() + dfs(0, "in", res) + return res + } + + /* 後序走訪 */ + fun postOrder(): List { + val res = ArrayList() + dfs(0, "post", res) + return res + } + } + ``` + +=== "Ruby" + + ```ruby title="array_binary_tree.rb" + [class]{ArrayBinaryTree}-[func]{} + ``` + +=== "Zig" + + ```zig title="array_binary_tree.zig" + [class]{ArrayBinaryTree}-[func]{} + ``` + +??? pythontutor "視覺化執行" + +
+ + +## 7.3.3   優點與侷限性 + +二元樹的陣列表示主要有以下優點。 + +- 陣列儲存在連續的記憶體空間中,對快取友好,訪問與走訪速度較快。 +- 不需要儲存指標,比較節省空間。 +- 允許隨機訪問節點。 + +然而,陣列表示也存在一些侷限性。 + +- 陣列儲存需要連續記憶體空間,因此不適合儲存資料量過大的樹。 +- 增刪節點需要透過陣列插入與刪除操作實現,效率較低。 +- 當二元樹中存在大量 `None` 時,陣列中包含的節點資料比重較低,空間利用率較低。 diff --git a/zh-Hant/docs/chapter_tree/avl_tree.md b/zh-Hant/docs/chapter_tree/avl_tree.md new file mode 100644 index 000000000..9adb6432c --- /dev/null +++ b/zh-Hant/docs/chapter_tree/avl_tree.md @@ -0,0 +1,2691 @@ +--- +comments: true +--- + +# 7.5   AVL 樹 * + +在“二元搜尋樹”章節中我們提到,在多次插入和刪除操作後,二元搜尋樹可能退化為鏈結串列。在這種情況下,所有操作的時間複雜度將從 $O(\log n)$ 劣化為 $O(n)$ 。 + +如圖 7-24 所示,經過兩次刪除節點操作,這棵二元搜尋樹便會退化為鏈結串列。 + +![AVL 樹在刪除節點後發生退化](avl_tree.assets/avltree_degradation_from_removing_node.png){ class="animation-figure" } + +

圖 7-24   AVL 樹在刪除節點後發生退化

+ +再例如,在圖 7-25 所示的完美二元樹中插入兩個節點後,樹將嚴重向左傾斜,查詢操作的時間複雜度也隨之劣化。 + +![AVL 樹在插入節點後發生退化](avl_tree.assets/avltree_degradation_from_inserting_node.png){ class="animation-figure" } + +

圖 7-25   AVL 樹在插入節點後發生退化

+ +1962 年 G. M. Adelson-Velsky 和 E. M. Landis 在論文“An algorithm for the organization of information”中提出了 AVL 樹。論文中詳細描述了一系列操作,確保在持續新增和刪除節點後,AVL 樹不會退化,從而使得各種操作的時間複雜度保持在 $O(\log n)$ 級別。換句話說,在需要頻繁進行增刪查改操作的場景中,AVL 樹能始終保持高效的資料操作效能,具有很好的應用價值。 + +## 7.5.1   AVL 樹常見術語 + +AVL 樹既是二元搜尋樹,也是平衡二元樹,同時滿足這兩類二元樹的所有性質,因此是一種平衡二元搜尋樹(balanced binary search tree)。 + +### 1.   節點高度 + +由於 AVL 樹的相關操作需要獲取節點高度,因此我們需要為節點類別新增 `height` 變數: + +=== "Python" + + ```python title="" + class TreeNode: + """AVL 樹節點類別""" + def __init__(self, val: int): + self.val: int = val # 節點值 + self.height: int = 0 # 節點高度 + self.left: TreeNode | None = None # 左子節點引用 + self.right: TreeNode | None = None # 右子節點引用 + ``` + +=== "C++" + + ```cpp title="" + /* AVL 樹節點類別 */ + struct TreeNode { + int val{}; // 節點值 + int height = 0; // 節點高度 + TreeNode *left{}; // 左子節點 + TreeNode *right{}; // 右子節點 + TreeNode() = default; + explicit TreeNode(int x) : val(x){} + }; + ``` + +=== "Java" + + ```java title="" + /* AVL 樹節點類別 */ + class TreeNode { + public int val; // 節點值 + public int height; // 節點高度 + public TreeNode left; // 左子節點 + public TreeNode right; // 右子節點 + public TreeNode(int x) { val = x; } + } + ``` + +=== "C#" + + ```csharp title="" + /* AVL 樹節點類別 */ + class TreeNode(int? x) { + public int? val = x; // 節點值 + public int height; // 節點高度 + public TreeNode? left; // 左子節點引用 + public TreeNode? right; // 右子節點引用 + } + ``` + +=== "Go" + + ```go title="" + /* AVL 樹節點結構體 */ + type TreeNode struct { + Val int // 節點值 + Height int // 節點高度 + Left *TreeNode // 左子節點引用 + Right *TreeNode // 右子節點引用 + } + ``` + +=== "Swift" + + ```swift title="" + /* AVL 樹節點類別 */ + class TreeNode { + var val: Int // 節點值 + var height: Int // 節點高度 + var left: TreeNode? // 左子節點 + var right: TreeNode? // 右子節點 + + init(x: Int) { + val = x + height = 0 + } + } + ``` + +=== "JS" + + ```javascript title="" + /* AVL 樹節點類別 */ + class TreeNode { + val; // 節點值 + height; //節點高度 + left; // 左子節點指標 + right; // 右子節點指標 + constructor(val, left, right, height) { + this.val = val === undefined ? 0 : val; + this.height = height === undefined ? 0 : height; + this.left = left === undefined ? null : left; + this.right = right === undefined ? null : right; + } + } + ``` + +=== "TS" + + ```typescript title="" + /* AVL 樹節點類別 */ + class TreeNode { + val: number; // 節點值 + height: number; // 節點高度 + left: TreeNode | null; // 左子節點指標 + right: TreeNode | null; // 右子節點指標 + constructor(val?: number, height?: number, left?: TreeNode | null, right?: TreeNode | null) { + this.val = val === undefined ? 0 : val; + this.height = height === undefined ? 0 : height; + this.left = left === undefined ? null : left; + this.right = right === undefined ? null : right; + } + } + ``` + +=== "Dart" + + ```dart title="" + /* AVL 樹節點類別 */ + class TreeNode { + int val; // 節點值 + int height; // 節點高度 + TreeNode? left; // 左子節點 + TreeNode? right; // 右子節點 + TreeNode(this.val, [this.height = 0, this.left, this.right]); + } + ``` + +=== "Rust" + + ```rust title="" + use std::rc::Rc; + use std::cell::RefCell; + + /* AVL 樹節點結構體 */ + struct TreeNode { + val: i32, // 節點值 + height: i32, // 節點高度 + left: Option>>, // 左子節點 + right: Option>>, // 右子節點 + } + + impl TreeNode { + /* 建構子 */ + fn new(val: i32) -> Rc> { + Rc::new(RefCell::new(Self { + val, + height: 0, + left: None, + right: None + })) + } + } + ``` + +=== "C" + + ```c title="" + /* AVL 樹節點結構體 */ + TreeNode struct TreeNode { + int val; + int height; + struct TreeNode *left; + struct TreeNode *right; + } TreeNode; + + /* 建構子 */ + TreeNode *newTreeNode(int val) { + TreeNode *node; + + node = (TreeNode *)malloc(sizeof(TreeNode)); + node->val = val; + node->height = 0; + node->left = NULL; + node->right = NULL; + return node; + } + ``` + +=== "Kotlin" + + ```kotlin title="" + /* AVL 樹節點類別 */ + class TreeNode(val _val: Int) { // 節點值 + val height: Int = 0 // 節點高度 + val left: TreeNode? = null // 左子節點 + val right: TreeNode? = null // 右子節點 + } + ``` + +=== "Ruby" + + ```ruby title="" + + ``` + +=== "Zig" + + ```zig title="" + + ``` + +“節點高度”是指從該節點到它的最遠葉節點的距離,即所經過的“邊”的數量。需要特別注意的是,葉節點的高度為 $0$ ,而空節點的高度為 $-1$ 。我們將建立兩個工具函式,分別用於獲取和更新節點的高度: + +=== "Python" + + ```python title="avl_tree.py" + def height(self, node: TreeNode | None) -> int: + """獲取節點高度""" + # 空節點高度為 -1 ,葉節點高度為 0 + if node is not None: + return node.height + return -1 + + def update_height(self, node: TreeNode | None): + """更新節點高度""" + # 節點高度等於最高子樹高度 + 1 + node.height = max([self.height(node.left), self.height(node.right)]) + 1 + ``` + +=== "C++" + + ```cpp title="avl_tree.cpp" + /* 獲取節點高度 */ + int height(TreeNode *node) { + // 空節點高度為 -1 ,葉節點高度為 0 + return node == nullptr ? -1 : node->height; + } + + /* 更新節點高度 */ + void updateHeight(TreeNode *node) { + // 節點高度等於最高子樹高度 + 1 + node->height = max(height(node->left), height(node->right)) + 1; + } + ``` + +=== "Java" + + ```java title="avl_tree.java" + /* 獲取節點高度 */ + int height(TreeNode node) { + // 空節點高度為 -1 ,葉節點高度為 0 + return node == null ? -1 : node.height; + } + + /* 更新節點高度 */ + void updateHeight(TreeNode node) { + // 節點高度等於最高子樹高度 + 1 + node.height = Math.max(height(node.left), height(node.right)) + 1; + } + ``` + +=== "C#" + + ```csharp title="avl_tree.cs" + /* 獲取節點高度 */ + int Height(TreeNode? node) { + // 空節點高度為 -1 ,葉節點高度為 0 + return node == null ? -1 : node.height; + } + + /* 更新節點高度 */ + void UpdateHeight(TreeNode node) { + // 節點高度等於最高子樹高度 + 1 + node.height = Math.Max(Height(node.left), Height(node.right)) + 1; + } + ``` + +=== "Go" + + ```go title="avl_tree.go" + /* 獲取節點高度 */ + func (t *aVLTree) height(node *TreeNode) int { + // 空節點高度為 -1 ,葉節點高度為 0 + if node != nil { + return node.Height + } + return -1 + } + + /* 更新節點高度 */ + func (t *aVLTree) updateHeight(node *TreeNode) { + lh := t.height(node.Left) + rh := t.height(node.Right) + // 節點高度等於最高子樹高度 + 1 + if lh > rh { + node.Height = lh + 1 + } else { + node.Height = rh + 1 + } + } + ``` + +=== "Swift" + + ```swift title="avl_tree.swift" + /* 獲取節點高度 */ + func height(node: TreeNode?) -> Int { + // 空節點高度為 -1 ,葉節點高度為 0 + node?.height ?? -1 + } + + /* 更新節點高度 */ + func updateHeight(node: TreeNode?) { + // 節點高度等於最高子樹高度 + 1 + node?.height = max(height(node: node?.left), height(node: node?.right)) + 1 + } + ``` + +=== "JS" + + ```javascript title="avl_tree.js" + /* 獲取節點高度 */ + height(node) { + // 空節點高度為 -1 ,葉節點高度為 0 + return node === null ? -1 : node.height; + } + + /* 更新節點高度 */ + #updateHeight(node) { + // 節點高度等於最高子樹高度 + 1 + node.height = + Math.max(this.height(node.left), this.height(node.right)) + 1; + } + ``` + +=== "TS" + + ```typescript title="avl_tree.ts" + /* 獲取節點高度 */ + height(node: TreeNode): number { + // 空節點高度為 -1 ,葉節點高度為 0 + return node === null ? -1 : node.height; + } + + /* 更新節點高度 */ + updateHeight(node: TreeNode): void { + // 節點高度等於最高子樹高度 + 1 + node.height = + Math.max(this.height(node.left), this.height(node.right)) + 1; + } + ``` + +=== "Dart" + + ```dart title="avl_tree.dart" + /* 獲取節點高度 */ + int height(TreeNode? node) { + // 空節點高度為 -1 ,葉節點高度為 0 + return node == null ? -1 : node.height; + } + + /* 更新節點高度 */ + void updateHeight(TreeNode? node) { + // 節點高度等於最高子樹高度 + 1 + node!.height = max(height(node.left), height(node.right)) + 1; + } + ``` + +=== "Rust" + + ```rust title="avl_tree.rs" + /* 獲取節點高度 */ + fn height(node: OptionTreeNodeRc) -> i32 { + // 空節點高度為 -1 ,葉節點高度為 0 + match node { + Some(node) => node.borrow().height, + None => -1, + } + } + + /* 更新節點高度 */ + fn update_height(node: OptionTreeNodeRc) { + if let Some(node) = node { + let left = node.borrow().left.clone(); + let right = node.borrow().right.clone(); + // 節點高度等於最高子樹高度 + 1 + node.borrow_mut().height = std::cmp::max(Self::height(left), Self::height(right)) + 1; + } + } + ``` + +=== "C" + + ```c title="avl_tree.c" + /* 獲取節點高度 */ + int height(TreeNode *node) { + // 空節點高度為 -1 ,葉節點高度為 0 + if (node != NULL) { + return node->height; + } + return -1; + } + + /* 更新節點高度 */ + void updateHeight(TreeNode *node) { + int lh = height(node->left); + int rh = height(node->right); + // 節點高度等於最高子樹高度 + 1 + if (lh > rh) { + node->height = lh + 1; + } else { + node->height = rh + 1; + } + } + ``` + +=== "Kotlin" + + ```kotlin title="avl_tree.kt" + /* 獲取節點高度 */ + fun height(node: TreeNode?): Int { + // 空節點高度為 -1 ,葉節點高度為 0 + return node?.height ?: -1 + } + + /* 更新節點高度 */ + fun updateHeight(node: TreeNode?) { + // 節點高度等於最高子樹高度 + 1 + node?.height = (max(height(node?.left).toDouble(), height(node?.right).toDouble()) + 1).toInt() + } + ``` + +=== "Ruby" + + ```ruby title="avl_tree.rb" + [class]{AVLTree}-[func]{height} + + [class]{AVLTree}-[func]{update_height} + ``` + +=== "Zig" + + ```zig title="avl_tree.zig" + // 獲取節點高度 + fn height(self: *Self, node: ?*inc.TreeNode(T)) i32 { + _ = self; + // 空節點高度為 -1 ,葉節點高度為 0 + return if (node == null) -1 else node.?.height; + } + + // 更新節點高度 + fn updateHeight(self: *Self, node: ?*inc.TreeNode(T)) void { + // 節點高度等於最高子樹高度 + 1 + node.?.height = @max(self.height(node.?.left), self.height(node.?.right)) + 1; + } + ``` + +### 2.   節點平衡因子 + +節點的平衡因子(balance factor)定義為節點左子樹的高度減去右子樹的高度,同時規定空節點的平衡因子為 $0$ 。我們同樣將獲取節點平衡因子的功能封裝成函式,方便後續使用: + +=== "Python" + + ```python title="avl_tree.py" + def balance_factor(self, node: TreeNode | None) -> int: + """獲取平衡因子""" + # 空節點平衡因子為 0 + if node is None: + return 0 + # 節點平衡因子 = 左子樹高度 - 右子樹高度 + return self.height(node.left) - self.height(node.right) + ``` + +=== "C++" + + ```cpp title="avl_tree.cpp" + /* 獲取平衡因子 */ + int balanceFactor(TreeNode *node) { + // 空節點平衡因子為 0 + if (node == nullptr) + return 0; + // 節點平衡因子 = 左子樹高度 - 右子樹高度 + return height(node->left) - height(node->right); + } + ``` + +=== "Java" + + ```java title="avl_tree.java" + /* 獲取平衡因子 */ + int balanceFactor(TreeNode node) { + // 空節點平衡因子為 0 + if (node == null) + return 0; + // 節點平衡因子 = 左子樹高度 - 右子樹高度 + return height(node.left) - height(node.right); + } + ``` + +=== "C#" + + ```csharp title="avl_tree.cs" + /* 獲取平衡因子 */ + int BalanceFactor(TreeNode? node) { + // 空節點平衡因子為 0 + if (node == null) return 0; + // 節點平衡因子 = 左子樹高度 - 右子樹高度 + return Height(node.left) - Height(node.right); + } + ``` + +=== "Go" + + ```go title="avl_tree.go" + /* 獲取平衡因子 */ + func (t *aVLTree) balanceFactor(node *TreeNode) int { + // 空節點平衡因子為 0 + if node == nil { + return 0 + } + // 節點平衡因子 = 左子樹高度 - 右子樹高度 + return t.height(node.Left) - t.height(node.Right) + } + ``` + +=== "Swift" + + ```swift title="avl_tree.swift" + /* 獲取平衡因子 */ + func balanceFactor(node: TreeNode?) -> Int { + // 空節點平衡因子為 0 + guard let node = node else { return 0 } + // 節點平衡因子 = 左子樹高度 - 右子樹高度 + return height(node: node.left) - height(node: node.right) + } + ``` + +=== "JS" + + ```javascript title="avl_tree.js" + /* 獲取平衡因子 */ + balanceFactor(node) { + // 空節點平衡因子為 0 + if (node === null) return 0; + // 節點平衡因子 = 左子樹高度 - 右子樹高度 + return this.height(node.left) - this.height(node.right); + } + ``` + +=== "TS" + + ```typescript title="avl_tree.ts" + /* 獲取平衡因子 */ + balanceFactor(node: TreeNode): number { + // 空節點平衡因子為 0 + if (node === null) return 0; + // 節點平衡因子 = 左子樹高度 - 右子樹高度 + return this.height(node.left) - this.height(node.right); + } + ``` + +=== "Dart" + + ```dart title="avl_tree.dart" + /* 獲取平衡因子 */ + int balanceFactor(TreeNode? node) { + // 空節點平衡因子為 0 + if (node == null) return 0; + // 節點平衡因子 = 左子樹高度 - 右子樹高度 + return height(node.left) - height(node.right); + } + ``` + +=== "Rust" + + ```rust title="avl_tree.rs" + /* 獲取平衡因子 */ + fn balance_factor(node: OptionTreeNodeRc) -> i32 { + match node { + // 空節點平衡因子為 0 + None => 0, + // 節點平衡因子 = 左子樹高度 - 右子樹高度 + Some(node) => { + Self::height(node.borrow().left.clone()) - Self::height(node.borrow().right.clone()) + } + } + } + ``` + +=== "C" + + ```c title="avl_tree.c" + /* 獲取平衡因子 */ + int balanceFactor(TreeNode *node) { + // 空節點平衡因子為 0 + if (node == NULL) { + return 0; + } + // 節點平衡因子 = 左子樹高度 - 右子樹高度 + return height(node->left) - height(node->right); + } + ``` + +=== "Kotlin" + + ```kotlin title="avl_tree.kt" + /* 獲取平衡因子 */ + fun balanceFactor(node: TreeNode?): Int { + // 空節點平衡因子為 0 + if (node == null) return 0 + // 節點平衡因子 = 左子樹高度 - 右子樹高度 + return height(node.left) - height(node.right) + } + ``` + +=== "Ruby" + + ```ruby title="avl_tree.rb" + [class]{AVLTree}-[func]{balance_factor} + ``` + +=== "Zig" + + ```zig title="avl_tree.zig" + // 獲取平衡因子 + fn balanceFactor(self: *Self, node: ?*inc.TreeNode(T)) i32 { + // 空節點平衡因子為 0 + if (node == null) return 0; + // 節點平衡因子 = 左子樹高度 - 右子樹高度 + return self.height(node.?.left) - self.height(node.?.right); + } + ``` + +!!! note + + 設平衡因子為 $f$ ,則一棵 AVL 樹的任意節點的平衡因子皆滿足 $-1 \le f \le 1$ 。 + +## 7.5.2   AVL 樹旋轉 + +AVL 樹的特點在於“旋轉”操作,它能夠在不影響二元樹的中序走訪序列的前提下,使失衡節點重新恢復平衡。換句話說,**旋轉操作既能保持“二元搜尋樹”的性質,也能使樹重新變為“平衡二元樹”**。 + +我們將平衡因子絕對值 $> 1$ 的節點稱為“失衡節點”。根據節點失衡情況的不同,旋轉操作分為四種:右旋、左旋、先右旋後左旋、先左旋後右旋。下面詳細介紹這些旋轉操作。 + +### 1.   右旋 + +如圖 7-26 所示,節點下方為平衡因子。從底至頂看,二元樹中首個失衡節點是“節點 3”。我們關注以該失衡節點為根節點的子樹,將該節點記為 `node` ,其左子節點記為 `child` ,執行“右旋”操作。完成右旋後,子樹恢復平衡,並且仍然保持二元搜尋樹的性質。 + +=== "<1>" + ![右旋操作步驟](avl_tree.assets/avltree_right_rotate_step1.png){ class="animation-figure" } + +=== "<2>" + ![avltree_right_rotate_step2](avl_tree.assets/avltree_right_rotate_step2.png){ class="animation-figure" } + +=== "<3>" + ![avltree_right_rotate_step3](avl_tree.assets/avltree_right_rotate_step3.png){ class="animation-figure" } + +=== "<4>" + ![avltree_right_rotate_step4](avl_tree.assets/avltree_right_rotate_step4.png){ class="animation-figure" } + +

圖 7-26   右旋操作步驟

+ +如圖 7-27 所示,當節點 `child` 有右子節點(記為 `grand_child` )時,需要在右旋中新增一步:將 `grand_child` 作為 `node` 的左子節點。 + +![有 grand_child 的右旋操作](avl_tree.assets/avltree_right_rotate_with_grandchild.png){ class="animation-figure" } + +

圖 7-27   有 grand_child 的右旋操作

+ +“向右旋轉”是一種形象化的說法,實際上需要透過修改節點指標來實現,程式碼如下所示: + +=== "Python" + + ```python title="avl_tree.py" + def right_rotate(self, node: TreeNode | None) -> TreeNode | None: + """右旋操作""" + child = node.left + grand_child = child.right + # 以 child 為原點,將 node 向右旋轉 + child.right = node + node.left = grand_child + # 更新節點高度 + self.update_height(node) + self.update_height(child) + # 返回旋轉後子樹的根節點 + return child + ``` + +=== "C++" + + ```cpp title="avl_tree.cpp" + /* 右旋操作 */ + TreeNode *rightRotate(TreeNode *node) { + TreeNode *child = node->left; + TreeNode *grandChild = child->right; + // 以 child 為原點,將 node 向右旋轉 + child->right = node; + node->left = grandChild; + // 更新節點高度 + updateHeight(node); + updateHeight(child); + // 返回旋轉後子樹的根節點 + return child; + } + ``` + +=== "Java" + + ```java title="avl_tree.java" + /* 右旋操作 */ + TreeNode rightRotate(TreeNode node) { + TreeNode child = node.left; + TreeNode grandChild = child.right; + // 以 child 為原點,將 node 向右旋轉 + child.right = node; + node.left = grandChild; + // 更新節點高度 + updateHeight(node); + updateHeight(child); + // 返回旋轉後子樹的根節點 + return child; + } + ``` + +=== "C#" + + ```csharp title="avl_tree.cs" + /* 右旋操作 */ + TreeNode? RightRotate(TreeNode? node) { + TreeNode? child = node?.left; + TreeNode? grandChild = child?.right; + // 以 child 為原點,將 node 向右旋轉 + child.right = node; + node.left = grandChild; + // 更新節點高度 + UpdateHeight(node); + UpdateHeight(child); + // 返回旋轉後子樹的根節點 + return child; + } + ``` + +=== "Go" + + ```go title="avl_tree.go" + /* 右旋操作 */ + func (t *aVLTree) rightRotate(node *TreeNode) *TreeNode { + child := node.Left + grandChild := child.Right + // 以 child 為原點,將 node 向右旋轉 + child.Right = node + node.Left = grandChild + // 更新節點高度 + t.updateHeight(node) + t.updateHeight(child) + // 返回旋轉後子樹的根節點 + return child + } + ``` + +=== "Swift" + + ```swift title="avl_tree.swift" + /* 右旋操作 */ + func rightRotate(node: TreeNode?) -> TreeNode? { + let child = node?.left + let grandChild = child?.right + // 以 child 為原點,將 node 向右旋轉 + child?.right = node + node?.left = grandChild + // 更新節點高度 + updateHeight(node: node) + updateHeight(node: child) + // 返回旋轉後子樹的根節點 + return child + } + ``` + +=== "JS" + + ```javascript title="avl_tree.js" + /* 右旋操作 */ + #rightRotate(node) { + const child = node.left; + const grandChild = child.right; + // 以 child 為原點,將 node 向右旋轉 + child.right = node; + node.left = grandChild; + // 更新節點高度 + this.#updateHeight(node); + this.#updateHeight(child); + // 返回旋轉後子樹的根節點 + return child; + } + ``` + +=== "TS" + + ```typescript title="avl_tree.ts" + /* 右旋操作 */ + rightRotate(node: TreeNode): TreeNode { + const child = node.left; + const grandChild = child.right; + // 以 child 為原點,將 node 向右旋轉 + child.right = node; + node.left = grandChild; + // 更新節點高度 + this.updateHeight(node); + this.updateHeight(child); + // 返回旋轉後子樹的根節點 + return child; + } + ``` + +=== "Dart" + + ```dart title="avl_tree.dart" + /* 右旋操作 */ + TreeNode? rightRotate(TreeNode? node) { + TreeNode? child = node!.left; + TreeNode? grandChild = child!.right; + // 以 child 為原點,將 node 向右旋轉 + child.right = node; + node.left = grandChild; + // 更新節點高度 + updateHeight(node); + updateHeight(child); + // 返回旋轉後子樹的根節點 + return child; + } + ``` + +=== "Rust" + + ```rust title="avl_tree.rs" + /* 右旋操作 */ + fn right_rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc { + match node { + Some(node) => { + let child = node.borrow().left.clone().unwrap(); + let grand_child = child.borrow().right.clone(); + // 以 child 為原點,將 node 向右旋轉 + child.borrow_mut().right = Some(node.clone()); + node.borrow_mut().left = grand_child; + // 更新節點高度 + Self::update_height(Some(node)); + Self::update_height(Some(child.clone())); + // 返回旋轉後子樹的根節點 + Some(child) + } + None => None, + } + } + ``` + +=== "C" + + ```c title="avl_tree.c" + /* 右旋操作 */ + TreeNode *rightRotate(TreeNode *node) { + TreeNode *child, *grandChild; + child = node->left; + grandChild = child->right; + // 以 child 為原點,將 node 向右旋轉 + child->right = node; + node->left = grandChild; + // 更新節點高度 + updateHeight(node); + updateHeight(child); + // 返回旋轉後子樹的根節點 + return child; + } + ``` + +=== "Kotlin" + + ```kotlin title="avl_tree.kt" + /* 右旋操作 */ + fun rightRotate(node: TreeNode?): TreeNode { + val child = node!!.left + val grandChild = child!!.right + // 以 child 為原點,將 node 向右旋轉 + child.right = node + node.left = grandChild + // 更新節點高度 + updateHeight(node) + updateHeight(child) + // 返回旋轉後子樹的根節點 + return child + } + ``` + +=== "Ruby" + + ```ruby title="avl_tree.rb" + [class]{AVLTree}-[func]{right_rotate} + ``` + +=== "Zig" + + ```zig title="avl_tree.zig" + // 右旋操作 + fn rightRotate(self: *Self, node: ?*inc.TreeNode(T)) ?*inc.TreeNode(T) { + var child = node.?.left; + var grandChild = child.?.right; + // 以 child 為原點,將 node 向右旋轉 + child.?.right = node; + node.?.left = grandChild; + // 更新節點高度 + self.updateHeight(node); + self.updateHeight(child); + // 返回旋轉後子樹的根節點 + return child; + } + ``` + +### 2.   左旋 + +相應地,如果考慮上述失衡二元樹的“映象”,則需要執行圖 7-28 所示的“左旋”操作。 + +![左旋操作](avl_tree.assets/avltree_left_rotate.png){ class="animation-figure" } + +

圖 7-28   左旋操作

+ +同理,如圖 7-29 所示,當節點 `child` 有左子節點(記為 `grand_child` )時,需要在左旋中新增一步:將 `grand_child` 作為 `node` 的右子節點。 + +![有 grand_child 的左旋操作](avl_tree.assets/avltree_left_rotate_with_grandchild.png){ class="animation-figure" } + +

圖 7-29   有 grand_child 的左旋操作

+ +可以觀察到,**右旋和左旋操作在邏輯上是映象對稱的,它們分別解決的兩種失衡情況也是對稱的**。基於對稱性,我們只需將右旋的實現程式碼中的所有的 `left` 替換為 `right` ,將所有的 `right` 替換為 `left` ,即可得到左旋的實現程式碼: + +=== "Python" + + ```python title="avl_tree.py" + def left_rotate(self, node: TreeNode | None) -> TreeNode | None: + """左旋操作""" + child = node.right + grand_child = child.left + # 以 child 為原點,將 node 向左旋轉 + child.left = node + node.right = grand_child + # 更新節點高度 + self.update_height(node) + self.update_height(child) + # 返回旋轉後子樹的根節點 + return child + ``` + +=== "C++" + + ```cpp title="avl_tree.cpp" + /* 左旋操作 */ + TreeNode *leftRotate(TreeNode *node) { + TreeNode *child = node->right; + TreeNode *grandChild = child->left; + // 以 child 為原點,將 node 向左旋轉 + child->left = node; + node->right = grandChild; + // 更新節點高度 + updateHeight(node); + updateHeight(child); + // 返回旋轉後子樹的根節點 + return child; + } + ``` + +=== "Java" + + ```java title="avl_tree.java" + /* 左旋操作 */ + TreeNode leftRotate(TreeNode node) { + TreeNode child = node.right; + TreeNode grandChild = child.left; + // 以 child 為原點,將 node 向左旋轉 + child.left = node; + node.right = grandChild; + // 更新節點高度 + updateHeight(node); + updateHeight(child); + // 返回旋轉後子樹的根節點 + return child; + } + ``` + +=== "C#" + + ```csharp title="avl_tree.cs" + /* 左旋操作 */ + TreeNode? LeftRotate(TreeNode? node) { + TreeNode? child = node?.right; + TreeNode? grandChild = child?.left; + // 以 child 為原點,將 node 向左旋轉 + child.left = node; + node.right = grandChild; + // 更新節點高度 + UpdateHeight(node); + UpdateHeight(child); + // 返回旋轉後子樹的根節點 + return child; + } + ``` + +=== "Go" + + ```go title="avl_tree.go" + /* 左旋操作 */ + func (t *aVLTree) leftRotate(node *TreeNode) *TreeNode { + child := node.Right + grandChild := child.Left + // 以 child 為原點,將 node 向左旋轉 + child.Left = node + node.Right = grandChild + // 更新節點高度 + t.updateHeight(node) + t.updateHeight(child) + // 返回旋轉後子樹的根節點 + return child + } + ``` + +=== "Swift" + + ```swift title="avl_tree.swift" + /* 左旋操作 */ + func leftRotate(node: TreeNode?) -> TreeNode? { + let child = node?.right + let grandChild = child?.left + // 以 child 為原點,將 node 向左旋轉 + child?.left = node + node?.right = grandChild + // 更新節點高度 + updateHeight(node: node) + updateHeight(node: child) + // 返回旋轉後子樹的根節點 + return child + } + ``` + +=== "JS" + + ```javascript title="avl_tree.js" + /* 左旋操作 */ + #leftRotate(node) { + const child = node.right; + const grandChild = child.left; + // 以 child 為原點,將 node 向左旋轉 + child.left = node; + node.right = grandChild; + // 更新節點高度 + this.#updateHeight(node); + this.#updateHeight(child); + // 返回旋轉後子樹的根節點 + return child; + } + ``` + +=== "TS" + + ```typescript title="avl_tree.ts" + /* 左旋操作 */ + leftRotate(node: TreeNode): TreeNode { + const child = node.right; + const grandChild = child.left; + // 以 child 為原點,將 node 向左旋轉 + child.left = node; + node.right = grandChild; + // 更新節點高度 + this.updateHeight(node); + this.updateHeight(child); + // 返回旋轉後子樹的根節點 + return child; + } + ``` + +=== "Dart" + + ```dart title="avl_tree.dart" + /* 左旋操作 */ + TreeNode? leftRotate(TreeNode? node) { + TreeNode? child = node!.right; + TreeNode? grandChild = child!.left; + // 以 child 為原點,將 node 向左旋轉 + child.left = node; + node.right = grandChild; + // 更新節點高度 + updateHeight(node); + updateHeight(child); + // 返回旋轉後子樹的根節點 + return child; + } + ``` + +=== "Rust" + + ```rust title="avl_tree.rs" + /* 左旋操作 */ + fn left_rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc { + match node { + Some(node) => { + let child = node.borrow().right.clone().unwrap(); + let grand_child = child.borrow().left.clone(); + // 以 child 為原點,將 node 向左旋轉 + child.borrow_mut().left = Some(node.clone()); + node.borrow_mut().right = grand_child; + // 更新節點高度 + Self::update_height(Some(node)); + Self::update_height(Some(child.clone())); + // 返回旋轉後子樹的根節點 + Some(child) + } + None => None, + } + } + ``` + +=== "C" + + ```c title="avl_tree.c" + /* 左旋操作 */ + TreeNode *leftRotate(TreeNode *node) { + TreeNode *child, *grandChild; + child = node->right; + grandChild = child->left; + // 以 child 為原點,將 node 向左旋轉 + child->left = node; + node->right = grandChild; + // 更新節點高度 + updateHeight(node); + updateHeight(child); + // 返回旋轉後子樹的根節點 + return child; + } + ``` + +=== "Kotlin" + + ```kotlin title="avl_tree.kt" + /* 左旋操作 */ + fun leftRotate(node: TreeNode?): TreeNode { + val child = node!!.right + val grandChild = child!!.left + // 以 child 為原點,將 node 向左旋轉 + child.left = node + node.right = grandChild + // 更新節點高度 + updateHeight(node) + updateHeight(child) + // 返回旋轉後子樹的根節點 + return child + } + ``` + +=== "Ruby" + + ```ruby title="avl_tree.rb" + [class]{AVLTree}-[func]{left_rotate} + ``` + +=== "Zig" + + ```zig title="avl_tree.zig" + // 左旋操作 + fn leftRotate(self: *Self, node: ?*inc.TreeNode(T)) ?*inc.TreeNode(T) { + var child = node.?.right; + var grandChild = child.?.left; + // 以 child 為原點,將 node 向左旋轉 + child.?.left = node; + node.?.right = grandChild; + // 更新節點高度 + self.updateHeight(node); + self.updateHeight(child); + // 返回旋轉後子樹的根節點 + return child; + } + ``` + +### 3.   先左旋後右旋 + +對於圖 7-30 中的失衡節點 3 ,僅使用左旋或右旋都無法使子樹恢復平衡。此時需要先對 `child` 執行“左旋”,再對 `node` 執行“右旋”。 + +![先左旋後右旋](avl_tree.assets/avltree_left_right_rotate.png){ class="animation-figure" } + +

圖 7-30   先左旋後右旋

+ +### 4.   先右旋後左旋 + +如圖 7-31 所示,對於上述失衡二元樹的映象情況,需要先對 `child` 執行“右旋”,再對 `node` 執行“左旋”。 + +![先右旋後左旋](avl_tree.assets/avltree_right_left_rotate.png){ class="animation-figure" } + +

圖 7-31   先右旋後左旋

+ +### 5.   旋轉的選擇 + +圖 7-32 展示的四種失衡情況與上述案例逐個對應,分別需要採用右旋、先左旋後右旋、先右旋後左旋、左旋的操作。 + +![AVL 樹的四種旋轉情況](avl_tree.assets/avltree_rotation_cases.png){ class="animation-figure" } + +

圖 7-32   AVL 樹的四種旋轉情況

+ +如下表所示,我們透過判斷失衡節點的平衡因子以及較高一側子節點的平衡因子的正負號,來確定失衡節點屬於圖 7-32 中的哪種情況。 + +

表 7-3   四種旋轉情況的選擇條件

+ +
+ +| 失衡節點的平衡因子 | 子節點的平衡因子 | 應採用的旋轉方法 | +| ------------------ | ---------------- | ---------------- | +| $> 1$ (左偏樹) | $\geq 0$ | 右旋 | +| $> 1$ (左偏樹) | $<0$ | 先左旋後右旋 | +| $< -1$ (右偏樹) | $\leq 0$ | 左旋 | +| $< -1$ (右偏樹) | $>0$ | 先右旋後左旋 | + +
+ +為了便於使用,我們將旋轉操作封裝成一個函式。**有了這個函式,我們就能對各種失衡情況進行旋轉,使失衡節點重新恢復平衡**。程式碼如下所示: + +=== "Python" + + ```python title="avl_tree.py" + def rotate(self, node: TreeNode | None) -> TreeNode | None: + """執行旋轉操作,使該子樹重新恢復平衡""" + # 獲取節點 node 的平衡因子 + balance_factor = self.balance_factor(node) + # 左偏樹 + if balance_factor > 1: + if self.balance_factor(node.left) >= 0: + # 右旋 + return self.right_rotate(node) + else: + # 先左旋後右旋 + node.left = self.left_rotate(node.left) + return self.right_rotate(node) + # 右偏樹 + elif balance_factor < -1: + if self.balance_factor(node.right) <= 0: + # 左旋 + return self.left_rotate(node) + else: + # 先右旋後左旋 + node.right = self.right_rotate(node.right) + return self.left_rotate(node) + # 平衡樹,無須旋轉,直接返回 + return node + ``` + +=== "C++" + + ```cpp title="avl_tree.cpp" + /* 執行旋轉操作,使該子樹重新恢復平衡 */ + TreeNode *rotate(TreeNode *node) { + // 獲取節點 node 的平衡因子 + int _balanceFactor = balanceFactor(node); + // 左偏樹 + if (_balanceFactor > 1) { + if (balanceFactor(node->left) >= 0) { + // 右旋 + return rightRotate(node); + } else { + // 先左旋後右旋 + node->left = leftRotate(node->left); + return rightRotate(node); + } + } + // 右偏樹 + if (_balanceFactor < -1) { + if (balanceFactor(node->right) <= 0) { + // 左旋 + return leftRotate(node); + } else { + // 先右旋後左旋 + node->right = rightRotate(node->right); + return leftRotate(node); + } + } + // 平衡樹,無須旋轉,直接返回 + return node; + } + ``` + +=== "Java" + + ```java title="avl_tree.java" + /* 執行旋轉操作,使該子樹重新恢復平衡 */ + TreeNode rotate(TreeNode node) { + // 獲取節點 node 的平衡因子 + int balanceFactor = balanceFactor(node); + // 左偏樹 + if (balanceFactor > 1) { + if (balanceFactor(node.left) >= 0) { + // 右旋 + return rightRotate(node); + } else { + // 先左旋後右旋 + node.left = leftRotate(node.left); + return rightRotate(node); + } + } + // 右偏樹 + if (balanceFactor < -1) { + if (balanceFactor(node.right) <= 0) { + // 左旋 + return leftRotate(node); + } else { + // 先右旋後左旋 + node.right = rightRotate(node.right); + return leftRotate(node); + } + } + // 平衡樹,無須旋轉,直接返回 + return node; + } + ``` + +=== "C#" + + ```csharp title="avl_tree.cs" + /* 執行旋轉操作,使該子樹重新恢復平衡 */ + TreeNode? Rotate(TreeNode? node) { + // 獲取節點 node 的平衡因子 + int balanceFactorInt = BalanceFactor(node); + // 左偏樹 + if (balanceFactorInt > 1) { + if (BalanceFactor(node?.left) >= 0) { + // 右旋 + return RightRotate(node); + } else { + // 先左旋後右旋 + node!.left = LeftRotate(node!.left); + return RightRotate(node); + } + } + // 右偏樹 + if (balanceFactorInt < -1) { + if (BalanceFactor(node?.right) <= 0) { + // 左旋 + return LeftRotate(node); + } else { + // 先右旋後左旋 + node!.right = RightRotate(node!.right); + return LeftRotate(node); + } + } + // 平衡樹,無須旋轉,直接返回 + return node; + } + ``` + +=== "Go" + + ```go title="avl_tree.go" + /* 執行旋轉操作,使該子樹重新恢復平衡 */ + func (t *aVLTree) rotate(node *TreeNode) *TreeNode { + // 獲取節點 node 的平衡因子 + // Go 推薦短變數,這裡 bf 指代 t.balanceFactor + bf := t.balanceFactor(node) + // 左偏樹 + if bf > 1 { + if t.balanceFactor(node.Left) >= 0 { + // 右旋 + return t.rightRotate(node) + } else { + // 先左旋後右旋 + node.Left = t.leftRotate(node.Left) + return t.rightRotate(node) + } + } + // 右偏樹 + if bf < -1 { + if t.balanceFactor(node.Right) <= 0 { + // 左旋 + return t.leftRotate(node) + } else { + // 先右旋後左旋 + node.Right = t.rightRotate(node.Right) + return t.leftRotate(node) + } + } + // 平衡樹,無須旋轉,直接返回 + return node + } + ``` + +=== "Swift" + + ```swift title="avl_tree.swift" + /* 執行旋轉操作,使該子樹重新恢復平衡 */ + func rotate(node: TreeNode?) -> TreeNode? { + // 獲取節點 node 的平衡因子 + let balanceFactor = balanceFactor(node: node) + // 左偏樹 + if balanceFactor > 1 { + if self.balanceFactor(node: node?.left) >= 0 { + // 右旋 + return rightRotate(node: node) + } else { + // 先左旋後右旋 + node?.left = leftRotate(node: node?.left) + return rightRotate(node: node) + } + } + // 右偏樹 + if balanceFactor < -1 { + if self.balanceFactor(node: node?.right) <= 0 { + // 左旋 + return leftRotate(node: node) + } else { + // 先右旋後左旋 + node?.right = rightRotate(node: node?.right) + return leftRotate(node: node) + } + } + // 平衡樹,無須旋轉,直接返回 + return node + } + ``` + +=== "JS" + + ```javascript title="avl_tree.js" + /* 執行旋轉操作,使該子樹重新恢復平衡 */ + #rotate(node) { + // 獲取節點 node 的平衡因子 + const balanceFactor = this.balanceFactor(node); + // 左偏樹 + if (balanceFactor > 1) { + if (this.balanceFactor(node.left) >= 0) { + // 右旋 + return this.#rightRotate(node); + } else { + // 先左旋後右旋 + node.left = this.#leftRotate(node.left); + return this.#rightRotate(node); + } + } + // 右偏樹 + if (balanceFactor < -1) { + if (this.balanceFactor(node.right) <= 0) { + // 左旋 + return this.#leftRotate(node); + } else { + // 先右旋後左旋 + node.right = this.#rightRotate(node.right); + return this.#leftRotate(node); + } + } + // 平衡樹,無須旋轉,直接返回 + return node; + } + ``` + +=== "TS" + + ```typescript title="avl_tree.ts" + /* 執行旋轉操作,使該子樹重新恢復平衡 */ + rotate(node: TreeNode): TreeNode { + // 獲取節點 node 的平衡因子 + const balanceFactor = this.balanceFactor(node); + // 左偏樹 + if (balanceFactor > 1) { + if (this.balanceFactor(node.left) >= 0) { + // 右旋 + return this.rightRotate(node); + } else { + // 先左旋後右旋 + node.left = this.leftRotate(node.left); + return this.rightRotate(node); + } + } + // 右偏樹 + if (balanceFactor < -1) { + if (this.balanceFactor(node.right) <= 0) { + // 左旋 + return this.leftRotate(node); + } else { + // 先右旋後左旋 + node.right = this.rightRotate(node.right); + return this.leftRotate(node); + } + } + // 平衡樹,無須旋轉,直接返回 + return node; + } + ``` + +=== "Dart" + + ```dart title="avl_tree.dart" + /* 執行旋轉操作,使該子樹重新恢復平衡 */ + TreeNode? rotate(TreeNode? node) { + // 獲取節點 node 的平衡因子 + int factor = balanceFactor(node); + // 左偏樹 + if (factor > 1) { + if (balanceFactor(node!.left) >= 0) { + // 右旋 + return rightRotate(node); + } else { + // 先左旋後右旋 + node.left = leftRotate(node.left); + return rightRotate(node); + } + } + // 右偏樹 + if (factor < -1) { + if (balanceFactor(node!.right) <= 0) { + // 左旋 + return leftRotate(node); + } else { + // 先右旋後左旋 + node.right = rightRotate(node.right); + return leftRotate(node); + } + } + // 平衡樹,無須旋轉,直接返回 + return node; + } + ``` + +=== "Rust" + + ```rust title="avl_tree.rs" + /* 執行旋轉操作,使該子樹重新恢復平衡 */ + fn rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc { + // 獲取節點 node 的平衡因子 + let balance_factor = Self::balance_factor(node.clone()); + // 左偏樹 + if balance_factor > 1 { + let node = node.unwrap(); + if Self::balance_factor(node.borrow().left.clone()) >= 0 { + // 右旋 + Self::right_rotate(Some(node)) + } else { + // 先左旋後右旋 + let left = node.borrow().left.clone(); + node.borrow_mut().left = Self::left_rotate(left); + Self::right_rotate(Some(node)) + } + } + // 右偏樹 + else if balance_factor < -1 { + let node = node.unwrap(); + if Self::balance_factor(node.borrow().right.clone()) <= 0 { + // 左旋 + Self::left_rotate(Some(node)) + } else { + // 先右旋後左旋 + let right = node.borrow().right.clone(); + node.borrow_mut().right = Self::right_rotate(right); + Self::left_rotate(Some(node)) + } + } else { + // 平衡樹,無須旋轉,直接返回 + node + } + } + ``` + +=== "C" + + ```c title="avl_tree.c" + /* 執行旋轉操作,使該子樹重新恢復平衡 */ + TreeNode *rotate(TreeNode *node) { + // 獲取節點 node 的平衡因子 + int bf = balanceFactor(node); + // 左偏樹 + if (bf > 1) { + if (balanceFactor(node->left) >= 0) { + // 右旋 + return rightRotate(node); + } else { + // 先左旋後右旋 + node->left = leftRotate(node->left); + return rightRotate(node); + } + } + // 右偏樹 + if (bf < -1) { + if (balanceFactor(node->right) <= 0) { + // 左旋 + return leftRotate(node); + } else { + // 先右旋後左旋 + node->right = rightRotate(node->right); + return leftRotate(node); + } + } + // 平衡樹,無須旋轉,直接返回 + return node; + } + ``` + +=== "Kotlin" + + ```kotlin title="avl_tree.kt" + /* 執行旋轉操作,使該子樹重新恢復平衡 */ + fun rotate(node: TreeNode): TreeNode { + // 獲取節點 node 的平衡因子 + val balanceFactor = balanceFactor(node) + // 左偏樹 + if (balanceFactor > 1) { + if (balanceFactor(node.left) >= 0) { + // 右旋 + return rightRotate(node) + } else { + // 先左旋後右旋 + node.left = leftRotate(node.left) + return rightRotate(node) + } + } + // 右偏樹 + if (balanceFactor < -1) { + if (balanceFactor(node.right) <= 0) { + // 左旋 + return leftRotate(node) + } else { + // 先右旋後左旋 + node.right = rightRotate(node.right) + return leftRotate(node) + } + } + // 平衡樹,無須旋轉,直接返回 + return node + } + ``` + +=== "Ruby" + + ```ruby title="avl_tree.rb" + [class]{AVLTree}-[func]{rotate} + ``` + +=== "Zig" + + ```zig title="avl_tree.zig" + // 執行旋轉操作,使該子樹重新恢復平衡 + fn rotate(self: *Self, node: ?*inc.TreeNode(T)) ?*inc.TreeNode(T) { + // 獲取節點 node 的平衡因子 + var balance_factor = self.balanceFactor(node); + // 左偏樹 + if (balance_factor > 1) { + if (self.balanceFactor(node.?.left) >= 0) { + // 右旋 + return self.rightRotate(node); + } else { + // 先左旋後右旋 + node.?.left = self.leftRotate(node.?.left); + return self.rightRotate(node); + } + } + // 右偏樹 + if (balance_factor < -1) { + if (self.balanceFactor(node.?.right) <= 0) { + // 左旋 + return self.leftRotate(node); + } else { + // 先右旋後左旋 + node.?.right = self.rightRotate(node.?.right); + return self.leftRotate(node); + } + } + // 平衡樹,無須旋轉,直接返回 + return node; + } + ``` + +## 7.5.3   AVL 樹常用操作 + +### 1.   插入節點 + +AVL 樹的節點插入操作與二元搜尋樹在主體上類似。唯一的區別在於,在 AVL 樹中插入節點後,從該節點到根節點的路徑上可能會出現一系列失衡節點。因此,**我們需要從這個節點開始,自底向上執行旋轉操作,使所有失衡節點恢復平衡**。程式碼如下所示: + +=== "Python" + + ```python title="avl_tree.py" + def insert(self, val): + """插入節點""" + self._root = self.insert_helper(self._root, val) + + def insert_helper(self, node: TreeNode | None, val: int) -> TreeNode: + """遞迴插入節點(輔助方法)""" + if node is None: + return TreeNode(val) + # 1. 查詢插入位置並插入節點 + if val < node.val: + node.left = self.insert_helper(node.left, val) + elif val > node.val: + node.right = self.insert_helper(node.right, val) + else: + # 重複節點不插入,直接返回 + return node + # 更新節點高度 + self.update_height(node) + # 2. 執行旋轉操作,使該子樹重新恢復平衡 + return self.rotate(node) + ``` + +=== "C++" + + ```cpp title="avl_tree.cpp" + /* 插入節點 */ + void insert(int val) { + root = insertHelper(root, val); + } + + /* 遞迴插入節點(輔助方法) */ + TreeNode *insertHelper(TreeNode *node, int val) { + if (node == nullptr) + return new TreeNode(val); + /* 1. 查詢插入位置並插入節點 */ + if (val < node->val) + node->left = insertHelper(node->left, val); + else if (val > node->val) + node->right = insertHelper(node->right, val); + else + return node; // 重複節點不插入,直接返回 + updateHeight(node); // 更新節點高度 + /* 2. 執行旋轉操作,使該子樹重新恢復平衡 */ + node = rotate(node); + // 返回子樹的根節點 + return node; + } + ``` + +=== "Java" + + ```java title="avl_tree.java" + /* 插入節點 */ + void insert(int val) { + root = insertHelper(root, val); + } + + /* 遞迴插入節點(輔助方法) */ + TreeNode insertHelper(TreeNode node, int val) { + if (node == null) + return new TreeNode(val); + /* 1. 查詢插入位置並插入節點 */ + if (val < node.val) + node.left = insertHelper(node.left, val); + else if (val > node.val) + node.right = insertHelper(node.right, val); + else + return node; // 重複節點不插入,直接返回 + updateHeight(node); // 更新節點高度 + /* 2. 執行旋轉操作,使該子樹重新恢復平衡 */ + node = rotate(node); + // 返回子樹的根節點 + return node; + } + ``` + +=== "C#" + + ```csharp title="avl_tree.cs" + /* 插入節點 */ + void Insert(int val) { + root = InsertHelper(root, val); + } + + /* 遞迴插入節點(輔助方法) */ + TreeNode? InsertHelper(TreeNode? node, int val) { + if (node == null) return new TreeNode(val); + /* 1. 查詢插入位置並插入節點 */ + if (val < node.val) + node.left = InsertHelper(node.left, val); + else if (val > node.val) + node.right = InsertHelper(node.right, val); + else + return node; // 重複節點不插入,直接返回 + UpdateHeight(node); // 更新節點高度 + /* 2. 執行旋轉操作,使該子樹重新恢復平衡 */ + node = Rotate(node); + // 返回子樹的根節點 + return node; + } + ``` + +=== "Go" + + ```go title="avl_tree.go" + /* 插入節點 */ + func (t *aVLTree) insert(val int) { + t.root = t.insertHelper(t.root, val) + } + + /* 遞迴插入節點(輔助函式) */ + func (t *aVLTree) insertHelper(node *TreeNode, val int) *TreeNode { + if node == nil { + return NewTreeNode(val) + } + /* 1. 查詢插入位置並插入節點 */ + if val < node.Val.(int) { + node.Left = t.insertHelper(node.Left, val) + } else if val > node.Val.(int) { + node.Right = t.insertHelper(node.Right, val) + } else { + // 重複節點不插入,直接返回 + return node + } + // 更新節點高度 + t.updateHeight(node) + /* 2. 執行旋轉操作,使該子樹重新恢復平衡 */ + node = t.rotate(node) + // 返回子樹的根節點 + return node + } + ``` + +=== "Swift" + + ```swift title="avl_tree.swift" + /* 插入節點 */ + func insert(val: Int) { + root = insertHelper(node: root, val: val) + } + + /* 遞迴插入節點(輔助方法) */ + func insertHelper(node: TreeNode?, val: Int) -> TreeNode? { + var node = node + if node == nil { + return TreeNode(x: val) + } + /* 1. 查詢插入位置並插入節點 */ + if val < node!.val { + node?.left = insertHelper(node: node?.left, val: val) + } else if val > node!.val { + node?.right = insertHelper(node: node?.right, val: val) + } else { + return node // 重複節點不插入,直接返回 + } + updateHeight(node: node) // 更新節點高度 + /* 2. 執行旋轉操作,使該子樹重新恢復平衡 */ + node = rotate(node: node) + // 返回子樹的根節點 + return node + } + ``` + +=== "JS" + + ```javascript title="avl_tree.js" + /* 插入節點 */ + insert(val) { + this.root = this.#insertHelper(this.root, val); + } + + /* 遞迴插入節點(輔助方法) */ + #insertHelper(node, val) { + if (node === null) return new TreeNode(val); + /* 1. 查詢插入位置並插入節點 */ + if (val < node.val) node.left = this.#insertHelper(node.left, val); + else if (val > node.val) + node.right = this.#insertHelper(node.right, val); + else return node; // 重複節點不插入,直接返回 + this.#updateHeight(node); // 更新節點高度 + /* 2. 執行旋轉操作,使該子樹重新恢復平衡 */ + node = this.#rotate(node); + // 返回子樹的根節點 + return node; + } + ``` + +=== "TS" + + ```typescript title="avl_tree.ts" + /* 插入節點 */ + insert(val: number): void { + this.root = this.insertHelper(this.root, val); + } + + /* 遞迴插入節點(輔助方法) */ + insertHelper(node: TreeNode, val: number): TreeNode { + if (node === null) return new TreeNode(val); + /* 1. 查詢插入位置並插入節點 */ + if (val < node.val) { + node.left = this.insertHelper(node.left, val); + } else if (val > node.val) { + node.right = this.insertHelper(node.right, val); + } else { + return node; // 重複節點不插入,直接返回 + } + this.updateHeight(node); // 更新節點高度 + /* 2. 執行旋轉操作,使該子樹重新恢復平衡 */ + node = this.rotate(node); + // 返回子樹的根節點 + return node; + } + ``` + +=== "Dart" + + ```dart title="avl_tree.dart" + /* 插入節點 */ + void insert(int val) { + root = insertHelper(root, val); + } + + /* 遞迴插入節點(輔助方法) */ + TreeNode? insertHelper(TreeNode? node, int val) { + if (node == null) return TreeNode(val); + /* 1. 查詢插入位置並插入節點 */ + if (val < node.val) + node.left = insertHelper(node.left, val); + else if (val > node.val) + node.right = insertHelper(node.right, val); + else + return node; // 重複節點不插入,直接返回 + updateHeight(node); // 更新節點高度 + /* 2. 執行旋轉操作,使該子樹重新恢復平衡 */ + node = rotate(node); + // 返回子樹的根節點 + return node; + } + ``` + +=== "Rust" + + ```rust title="avl_tree.rs" + /* 插入節點 */ + fn insert(&mut self, val: i32) { + self.root = Self::insert_helper(self.root.clone(), val); + } + + /* 遞迴插入節點(輔助方法) */ + fn insert_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc { + match node { + Some(mut node) => { + /* 1. 查詢插入位置並插入節點 */ + match { + let node_val = node.borrow().val; + node_val + } + .cmp(&val) + { + Ordering::Greater => { + let left = node.borrow().left.clone(); + node.borrow_mut().left = Self::insert_helper(left, val); + } + Ordering::Less => { + let right = node.borrow().right.clone(); + node.borrow_mut().right = Self::insert_helper(right, val); + } + Ordering::Equal => { + return Some(node); // 重複節點不插入,直接返回 + } + } + Self::update_height(Some(node.clone())); // 更新節點高度 + + /* 2. 執行旋轉操作,使該子樹重新恢復平衡 */ + node = Self::rotate(Some(node)).unwrap(); + // 返回子樹的根節點 + Some(node) + } + None => Some(TreeNode::new(val)), + } + } + ``` + +=== "C" + + ```c title="avl_tree.c" + /* 插入節點 */ + void insert(AVLTree *tree, int val) { + tree->root = insertHelper(tree->root, val); + } + + /* 遞迴插入節點(輔助函式) */ + TreeNode *insertHelper(TreeNode *node, int val) { + if (node == NULL) { + return newTreeNode(val); + } + /* 1. 查詢插入位置並插入節點 */ + if (val < node->val) { + node->left = insertHelper(node->left, val); + } else if (val > node->val) { + node->right = insertHelper(node->right, val); + } else { + // 重複節點不插入,直接返回 + return node; + } + // 更新節點高度 + updateHeight(node); + /* 2. 執行旋轉操作,使該子樹重新恢復平衡 */ + node = rotate(node); + // 返回子樹的根節點 + return node; + } + ``` + +=== "Kotlin" + + ```kotlin title="avl_tree.kt" + /* 插入節點 */ + fun insert(value: Int) { + root = insertHelper(root, value) + } + + /* 遞迴插入節點(輔助方法) */ + fun insertHelper(n: TreeNode?, value: Int): TreeNode { + if (n == null) + return TreeNode(value) + var node = n + /* 1. 查詢插入位置並插入節點 */ + if (value < node.value) node.left = insertHelper(node.left, value) + else if (value > node.value) node.right = insertHelper(node.right, value) + else return node // 重複節點不插入,直接返回 + + updateHeight(node) // 更新節點高度 + /* 2. 執行旋轉操作,使該子樹重新恢復平衡 */ + node = rotate(node) + // 返回子樹的根節點 + return node + } + ``` + +=== "Ruby" + + ```ruby title="avl_tree.rb" + [class]{AVLTree}-[func]{insert} + + [class]{AVLTree}-[func]{insert_helper} + ``` + +=== "Zig" + + ```zig title="avl_tree.zig" + // 插入節點 + fn insert(self: *Self, val: T) !void { + self.root = (try self.insertHelper(self.root, val)).?; + } + + // 遞迴插入節點(輔助方法) + fn insertHelper(self: *Self, node_: ?*inc.TreeNode(T), val: T) !?*inc.TreeNode(T) { + var node = node_; + if (node == null) { + var tmp_node = try self.mem_allocator.create(inc.TreeNode(T)); + tmp_node.init(val); + return tmp_node; + } + // 1. 查詢插入位置並插入節點 + if (val < node.?.val) { + node.?.left = try self.insertHelper(node.?.left, val); + } else if (val > node.?.val) { + node.?.right = try self.insertHelper(node.?.right, val); + } else { + return node; // 重複節點不插入,直接返回 + } + self.updateHeight(node); // 更新節點高度 + // 2. 執行旋轉操作,使該子樹重新恢復平衡 + node = self.rotate(node); + // 返回子樹的根節點 + return node; + } + ``` + +### 2.   刪除節點 + +類似地,在二元搜尋樹的刪除節點方法的基礎上,需要從底至頂執行旋轉操作,使所有失衡節點恢復平衡。程式碼如下所示: + +=== "Python" + + ```python title="avl_tree.py" + def remove(self, val: int): + """刪除節點""" + self._root = self.remove_helper(self._root, val) + + def remove_helper(self, node: TreeNode | None, val: int) -> TreeNode | None: + """遞迴刪除節點(輔助方法)""" + if node is None: + return None + # 1. 查詢節點並刪除 + if val < node.val: + node.left = self.remove_helper(node.left, val) + elif val > node.val: + node.right = self.remove_helper(node.right, val) + else: + if node.left is None or node.right is None: + child = node.left or node.right + # 子節點數量 = 0 ,直接刪除 node 並返回 + if child is None: + return None + # 子節點數量 = 1 ,直接刪除 node + else: + node = child + else: + # 子節點數量 = 2 ,則將中序走訪的下個節點刪除,並用該節點替換當前節點 + temp = node.right + while temp.left is not None: + temp = temp.left + node.right = self.remove_helper(node.right, temp.val) + node.val = temp.val + # 更新節點高度 + self.update_height(node) + # 2. 執行旋轉操作,使該子樹重新恢復平衡 + return self.rotate(node) + ``` + +=== "C++" + + ```cpp title="avl_tree.cpp" + /* 刪除節點 */ + void remove(int val) { + root = removeHelper(root, val); + } + + /* 遞迴刪除節點(輔助方法) */ + TreeNode *removeHelper(TreeNode *node, int val) { + if (node == nullptr) + return nullptr; + /* 1. 查詢節點並刪除 */ + if (val < node->val) + node->left = removeHelper(node->left, val); + else if (val > node->val) + node->right = removeHelper(node->right, val); + else { + if (node->left == nullptr || node->right == nullptr) { + TreeNode *child = node->left != nullptr ? node->left : node->right; + // 子節點數量 = 0 ,直接刪除 node 並返回 + if (child == nullptr) { + delete node; + return nullptr; + } + // 子節點數量 = 1 ,直接刪除 node + else { + delete node; + node = child; + } + } else { + // 子節點數量 = 2 ,則將中序走訪的下個節點刪除,並用該節點替換當前節點 + TreeNode *temp = node->right; + while (temp->left != nullptr) { + temp = temp->left; + } + int tempVal = temp->val; + node->right = removeHelper(node->right, temp->val); + node->val = tempVal; + } + } + updateHeight(node); // 更新節點高度 + /* 2. 執行旋轉操作,使該子樹重新恢復平衡 */ + node = rotate(node); + // 返回子樹的根節點 + return node; + } + ``` + +=== "Java" + + ```java title="avl_tree.java" + /* 刪除節點 */ + void remove(int val) { + root = removeHelper(root, val); + } + + /* 遞迴刪除節點(輔助方法) */ + TreeNode removeHelper(TreeNode node, int val) { + if (node == null) + return null; + /* 1. 查詢節點並刪除 */ + if (val < node.val) + node.left = removeHelper(node.left, val); + else if (val > node.val) + node.right = removeHelper(node.right, val); + else { + if (node.left == null || node.right == null) { + TreeNode child = node.left != null ? node.left : node.right; + // 子節點數量 = 0 ,直接刪除 node 並返回 + if (child == null) + return null; + // 子節點數量 = 1 ,直接刪除 node + else + node = child; + } else { + // 子節點數量 = 2 ,則將中序走訪的下個節點刪除,並用該節點替換當前節點 + TreeNode temp = node.right; + while (temp.left != null) { + temp = temp.left; + } + node.right = removeHelper(node.right, temp.val); + node.val = temp.val; + } + } + updateHeight(node); // 更新節點高度 + /* 2. 執行旋轉操作,使該子樹重新恢復平衡 */ + node = rotate(node); + // 返回子樹的根節點 + return node; + } + ``` + +=== "C#" + + ```csharp title="avl_tree.cs" + /* 刪除節點 */ + void Remove(int val) { + root = RemoveHelper(root, val); + } + + /* 遞迴刪除節點(輔助方法) */ + TreeNode? RemoveHelper(TreeNode? node, int val) { + if (node == null) return null; + /* 1. 查詢節點並刪除 */ + if (val < node.val) + node.left = RemoveHelper(node.left, val); + else if (val > node.val) + node.right = RemoveHelper(node.right, val); + else { + if (node.left == null || node.right == null) { + TreeNode? child = node.left ?? node.right; + // 子節點數量 = 0 ,直接刪除 node 並返回 + if (child == null) + return null; + // 子節點數量 = 1 ,直接刪除 node + else + node = child; + } else { + // 子節點數量 = 2 ,則將中序走訪的下個節點刪除,並用該節點替換當前節點 + TreeNode? temp = node.right; + while (temp.left != null) { + temp = temp.left; + } + node.right = RemoveHelper(node.right, temp.val!.Value); + node.val = temp.val; + } + } + UpdateHeight(node); // 更新節點高度 + /* 2. 執行旋轉操作,使該子樹重新恢復平衡 */ + node = Rotate(node); + // 返回子樹的根節點 + return node; + } + ``` + +=== "Go" + + ```go title="avl_tree.go" + /* 刪除節點 */ + func (t *aVLTree) remove(val int) { + t.root = t.removeHelper(t.root, val) + } + + /* 遞迴刪除節點(輔助函式) */ + func (t *aVLTree) removeHelper(node *TreeNode, val int) *TreeNode { + if node == nil { + return nil + } + /* 1. 查詢節點並刪除 */ + if val < node.Val.(int) { + node.Left = t.removeHelper(node.Left, val) + } else if val > node.Val.(int) { + node.Right = t.removeHelper(node.Right, val) + } else { + if node.Left == nil || node.Right == nil { + child := node.Left + if node.Right != nil { + child = node.Right + } + if child == nil { + // 子節點數量 = 0 ,直接刪除 node 並返回 + return nil + } else { + // 子節點數量 = 1 ,直接刪除 node + node = child + } + } else { + // 子節點數量 = 2 ,則將中序走訪的下個節點刪除,並用該節點替換當前節點 + temp := node.Right + for temp.Left != nil { + temp = temp.Left + } + node.Right = t.removeHelper(node.Right, temp.Val.(int)) + node.Val = temp.Val + } + } + // 更新節點高度 + t.updateHeight(node) + /* 2. 執行旋轉操作,使該子樹重新恢復平衡 */ + node = t.rotate(node) + // 返回子樹的根節點 + return node + } + ``` + +=== "Swift" + + ```swift title="avl_tree.swift" + /* 刪除節點 */ + func remove(val: Int) { + root = removeHelper(node: root, val: val) + } + + /* 遞迴刪除節點(輔助方法) */ + func removeHelper(node: TreeNode?, val: Int) -> TreeNode? { + var node = node + if node == nil { + return nil + } + /* 1. 查詢節點並刪除 */ + if val < node!.val { + node?.left = removeHelper(node: node?.left, val: val) + } else if val > node!.val { + node?.right = removeHelper(node: node?.right, val: val) + } else { + if node?.left == nil || node?.right == nil { + let child = node?.left ?? node?.right + // 子節點數量 = 0 ,直接刪除 node 並返回 + if child == nil { + return nil + } + // 子節點數量 = 1 ,直接刪除 node + else { + node = child + } + } else { + // 子節點數量 = 2 ,則將中序走訪的下個節點刪除,並用該節點替換當前節點 + var temp = node?.right + while temp?.left != nil { + temp = temp?.left + } + node?.right = removeHelper(node: node?.right, val: temp!.val) + node?.val = temp!.val + } + } + updateHeight(node: node) // 更新節點高度 + /* 2. 執行旋轉操作,使該子樹重新恢復平衡 */ + node = rotate(node: node) + // 返回子樹的根節點 + return node + } + ``` + +=== "JS" + + ```javascript title="avl_tree.js" + /* 刪除節點 */ + remove(val) { + this.root = this.#removeHelper(this.root, val); + } + + /* 遞迴刪除節點(輔助方法) */ + #removeHelper(node, val) { + if (node === null) return null; + /* 1. 查詢節點並刪除 */ + if (val < node.val) node.left = this.#removeHelper(node.left, val); + else if (val > node.val) + node.right = this.#removeHelper(node.right, val); + else { + if (node.left === null || node.right === null) { + const child = node.left !== null ? node.left : node.right; + // 子節點數量 = 0 ,直接刪除 node 並返回 + if (child === null) return null; + // 子節點數量 = 1 ,直接刪除 node + else node = child; + } else { + // 子節點數量 = 2 ,則將中序走訪的下個節點刪除,並用該節點替換當前節點 + let temp = node.right; + while (temp.left !== null) { + temp = temp.left; + } + node.right = this.#removeHelper(node.right, temp.val); + node.val = temp.val; + } + } + this.#updateHeight(node); // 更新節點高度 + /* 2. 執行旋轉操作,使該子樹重新恢復平衡 */ + node = this.#rotate(node); + // 返回子樹的根節點 + return node; + } + ``` + +=== "TS" + + ```typescript title="avl_tree.ts" + /* 刪除節點 */ + remove(val: number): void { + this.root = this.removeHelper(this.root, val); + } + + /* 遞迴刪除節點(輔助方法) */ + removeHelper(node: TreeNode, val: number): TreeNode { + if (node === null) return null; + /* 1. 查詢節點並刪除 */ + if (val < node.val) { + node.left = this.removeHelper(node.left, val); + } else if (val > node.val) { + node.right = this.removeHelper(node.right, val); + } else { + if (node.left === null || node.right === null) { + const child = node.left !== null ? node.left : node.right; + // 子節點數量 = 0 ,直接刪除 node 並返回 + if (child === null) { + return null; + } else { + // 子節點數量 = 1 ,直接刪除 node + node = child; + } + } else { + // 子節點數量 = 2 ,則將中序走訪的下個節點刪除,並用該節點替換當前節點 + let temp = node.right; + while (temp.left !== null) { + temp = temp.left; + } + node.right = this.removeHelper(node.right, temp.val); + node.val = temp.val; + } + } + this.updateHeight(node); // 更新節點高度 + /* 2. 執行旋轉操作,使該子樹重新恢復平衡 */ + node = this.rotate(node); + // 返回子樹的根節點 + return node; + } + ``` + +=== "Dart" + + ```dart title="avl_tree.dart" + /* 刪除節點 */ + void remove(int val) { + root = removeHelper(root, val); + } + + /* 遞迴刪除節點(輔助方法) */ + TreeNode? removeHelper(TreeNode? node, int val) { + if (node == null) return null; + /* 1. 查詢節點並刪除 */ + if (val < node.val) + node.left = removeHelper(node.left, val); + else if (val > node.val) + node.right = removeHelper(node.right, val); + else { + if (node.left == null || node.right == null) { + TreeNode? child = node.left ?? node.right; + // 子節點數量 = 0 ,直接刪除 node 並返回 + if (child == null) + return null; + // 子節點數量 = 1 ,直接刪除 node + else + node = child; + } else { + // 子節點數量 = 2 ,則將中序走訪的下個節點刪除,並用該節點替換當前節點 + TreeNode? temp = node.right; + while (temp!.left != null) { + temp = temp.left; + } + node.right = removeHelper(node.right, temp.val); + node.val = temp.val; + } + } + updateHeight(node); // 更新節點高度 + /* 2. 執行旋轉操作,使該子樹重新恢復平衡 */ + node = rotate(node); + // 返回子樹的根節點 + return node; + } + ``` + +=== "Rust" + + ```rust title="avl_tree.rs" + /* 刪除節點 */ + fn remove(&self, val: i32) { + Self::remove_helper(self.root.clone(), val); + } + + /* 遞迴刪除節點(輔助方法) */ + fn remove_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc { + match node { + Some(mut node) => { + /* 1. 查詢節點並刪除 */ + if val < node.borrow().val { + let left = node.borrow().left.clone(); + node.borrow_mut().left = Self::remove_helper(left, val); + } else if val > node.borrow().val { + let right = node.borrow().right.clone(); + node.borrow_mut().right = Self::remove_helper(right, val); + } else if node.borrow().left.is_none() || node.borrow().right.is_none() { + let child = if node.borrow().left.is_some() { + node.borrow().left.clone() + } else { + node.borrow().right.clone() + }; + match child { + // 子節點數量 = 0 ,直接刪除 node 並返回 + None => { + return None; + } + // 子節點數量 = 1 ,直接刪除 node + Some(child) => node = child, + } + } else { + // 子節點數量 = 2 ,則將中序走訪的下個節點刪除,並用該節點替換當前節點 + let mut temp = node.borrow().right.clone().unwrap(); + loop { + let temp_left = temp.borrow().left.clone(); + if temp_left.is_none() { + break; + } + temp = temp_left.unwrap(); + } + let right = node.borrow().right.clone(); + node.borrow_mut().right = Self::remove_helper(right, temp.borrow().val); + node.borrow_mut().val = temp.borrow().val; + } + Self::update_height(Some(node.clone())); // 更新節點高度 + + /* 2. 執行旋轉操作,使該子樹重新恢復平衡 */ + node = Self::rotate(Some(node)).unwrap(); + // 返回子樹的根節點 + Some(node) + } + None => None, + } + } + ``` + +=== "C" + + ```c title="avl_tree.c" + /* 刪除節點 */ + // 由於引入了 stdio.h ,此處無法使用 remove 關鍵詞 + void removeItem(AVLTree *tree, int val) { + TreeNode *root = removeHelper(tree->root, val); + } + + /* 遞迴刪除節點(輔助函式) */ + TreeNode *removeHelper(TreeNode *node, int val) { + TreeNode *child, *grandChild; + if (node == NULL) { + return NULL; + } + /* 1. 查詢節點並刪除 */ + if (val < node->val) { + node->left = removeHelper(node->left, val); + } else if (val > node->val) { + node->right = removeHelper(node->right, val); + } else { + if (node->left == NULL || node->right == NULL) { + child = node->left; + if (node->right != NULL) { + child = node->right; + } + // 子節點數量 = 0 ,直接刪除 node 並返回 + if (child == NULL) { + return NULL; + } else { + // 子節點數量 = 1 ,直接刪除 node + node = child; + } + } else { + // 子節點數量 = 2 ,則將中序走訪的下個節點刪除,並用該節點替換當前節點 + TreeNode *temp = node->right; + while (temp->left != NULL) { + temp = temp->left; + } + int tempVal = temp->val; + node->right = removeHelper(node->right, temp->val); + node->val = tempVal; + } + } + // 更新節點高度 + updateHeight(node); + /* 2. 執行旋轉操作,使該子樹重新恢復平衡 */ + node = rotate(node); + // 返回子樹的根節點 + return node; + } + ``` + +=== "Kotlin" + + ```kotlin title="avl_tree.kt" + /* 刪除節點 */ + fun remove(value: Int) { + root = removeHelper(root, value) + } + + /* 遞迴刪除節點(輔助方法) */ + fun removeHelper(n: TreeNode?, value: Int): TreeNode? { + var node = n ?: return null + /* 1. 查詢節點並刪除 */ + if (value < node.value) node.left = removeHelper(node.left, value) + else if (value > node.value) node.right = removeHelper(node.right, value) + else { + if (node.left == null || node.right == null) { + val child = if (node.left != null) node.left else node.right + // 子節點數量 = 0 ,直接刪除 node 並返回 + if (child == null) return null + else node = child + } else { + // 子節點數量 = 2 ,則將中序走訪的下個節點刪除,並用該節點替換當前節點 + var temp = node.right + while (temp!!.left != null) { + temp = temp.left + } + node.right = removeHelper(node.right, temp.value) + node.value = temp.value + } + } + updateHeight(node) // 更新節點高度 + /* 2. 執行旋轉操作,使該子樹重新恢復平衡 */ + node = rotate(node) + // 返回子樹的根節點 + return node + } + ``` + +=== "Ruby" + + ```ruby title="avl_tree.rb" + [class]{AVLTree}-[func]{remove} + + [class]{AVLTree}-[func]{remove_helper} + ``` + +=== "Zig" + + ```zig title="avl_tree.zig" + // 刪除節點 + fn remove(self: *Self, val: T) void { + self.root = self.removeHelper(self.root, val).?; + } + + // 遞迴刪除節點(輔助方法) + fn removeHelper(self: *Self, node_: ?*inc.TreeNode(T), val: T) ?*inc.TreeNode(T) { + var node = node_; + if (node == null) return null; + // 1. 查詢節點並刪除 + if (val < node.?.val) { + node.?.left = self.removeHelper(node.?.left, val); + } else if (val > node.?.val) { + node.?.right = self.removeHelper(node.?.right, val); + } else { + if (node.?.left == null or node.?.right == null) { + var child = if (node.?.left != null) node.?.left else node.?.right; + // 子節點數量 = 0 ,直接刪除 node 並返回 + if (child == null) { + return null; + // 子節點數量 = 1 ,直接刪除 node + } else { + node = child; + } + } else { + // 子節點數量 = 2 ,則將中序走訪的下個節點刪除,並用該節點替換當前節點 + var temp = node.?.right; + while (temp.?.left != null) { + temp = temp.?.left; + } + node.?.right = self.removeHelper(node.?.right, temp.?.val); + node.?.val = temp.?.val; + } + } + self.updateHeight(node); // 更新節點高度 + // 2. 執行旋轉操作,使該子樹重新恢復平衡 + node = self.rotate(node); + // 返回子樹的根節點 + return node; + } + ``` + +### 3.   查詢節點 + +AVL 樹的節點查詢操作與二元搜尋樹一致,在此不再贅述。 + +## 7.5.4   AVL 樹典型應用 + +- 組織和儲存大型資料,適用於高頻查詢、低頻增刪的場景。 +- 用於構建資料庫中的索引系統。 +- 紅黑樹也是一種常見的平衡二元搜尋樹。相較於 AVL 樹,紅黑樹的平衡條件更寬鬆,插入與刪除節點所需的旋轉操作更少,節點增刪操作的平均效率更高。 diff --git a/zh-Hant/docs/chapter_tree/binary_search_tree.md b/zh-Hant/docs/chapter_tree/binary_search_tree.md new file mode 100755 index 000000000..1f1a9ba3f --- /dev/null +++ b/zh-Hant/docs/chapter_tree/binary_search_tree.md @@ -0,0 +1,1628 @@ +--- +comments: true +--- + +# 7.4   二元搜尋樹 + +如圖 7-16 所示,二元搜尋樹(binary search tree)滿足以下條件。 + +1. 對於根節點,左子樹中所有節點的值 $<$ 根節點的值 $<$ 右子樹中所有節點的值。 +2. 任意節點的左、右子樹也是二元搜尋樹,即同樣滿足條件 `1.` 。 + +![二元搜尋樹](binary_search_tree.assets/binary_search_tree.png){ class="animation-figure" } + +

圖 7-16   二元搜尋樹

+ +## 7.4.1   二元搜尋樹的操作 + +我們將二元搜尋樹封裝為一個類別 `BinarySearchTree` ,並宣告一個成員變數 `root` ,指向樹的根節點。 + +### 1.   查詢節點 + +給定目標節點值 `num` ,可以根據二元搜尋樹的性質來查詢。如圖 7-17 所示,我們宣告一個節點 `cur` ,從二元樹的根節點 `root` 出發,迴圈比較節點值 `cur.val` 和 `num` 之間的大小關係。 + +- 若 `cur.val < num` ,說明目標節點在 `cur` 的右子樹中,因此執行 `cur = cur.right` 。 +- 若 `cur.val > num` ,說明目標節點在 `cur` 的左子樹中,因此執行 `cur = cur.left` 。 +- 若 `cur.val = num` ,說明找到目標節點,跳出迴圈並返回該節點。 + +=== "<1>" + ![二元搜尋樹查詢節點示例](binary_search_tree.assets/bst_search_step1.png){ class="animation-figure" } + +=== "<2>" + ![bst_search_step2](binary_search_tree.assets/bst_search_step2.png){ class="animation-figure" } + +=== "<3>" + ![bst_search_step3](binary_search_tree.assets/bst_search_step3.png){ class="animation-figure" } + +=== "<4>" + ![bst_search_step4](binary_search_tree.assets/bst_search_step4.png){ class="animation-figure" } + +

圖 7-17   二元搜尋樹查詢節點示例

+ +二元搜尋樹的查詢操作與二分搜尋演算法的工作原理一致,都是每輪排除一半情況。迴圈次數最多為二元樹的高度,當二元樹平衡時,使用 $O(\log n)$ 時間。示例程式碼如下: + +=== "Python" + + ```python title="binary_search_tree.py" + def search(self, num: int) -> TreeNode | None: + """查詢節點""" + cur = self._root + # 迴圈查詢,越過葉節點後跳出 + while cur is not None: + # 目標節點在 cur 的右子樹中 + if cur.val < num: + cur = cur.right + # 目標節點在 cur 的左子樹中 + elif cur.val > num: + cur = cur.left + # 找到目標節點,跳出迴圈 + else: + break + return cur + ``` + +=== "C++" + + ```cpp title="binary_search_tree.cpp" + /* 查詢節點 */ + TreeNode *search(int num) { + TreeNode *cur = root; + // 迴圈查詢,越過葉節點後跳出 + while (cur != nullptr) { + // 目標節點在 cur 的右子樹中 + if (cur->val < num) + cur = cur->right; + // 目標節點在 cur 的左子樹中 + else if (cur->val > num) + cur = cur->left; + // 找到目標節點,跳出迴圈 + else + break; + } + // 返回目標節點 + return cur; + } + ``` + +=== "Java" + + ```java title="binary_search_tree.java" + /* 查詢節點 */ + TreeNode search(int num) { + TreeNode cur = root; + // 迴圈查詢,越過葉節點後跳出 + while (cur != null) { + // 目標節點在 cur 的右子樹中 + if (cur.val < num) + cur = cur.right; + // 目標節點在 cur 的左子樹中 + else if (cur.val > num) + cur = cur.left; + // 找到目標節點,跳出迴圈 + else + break; + } + // 返回目標節點 + return cur; + } + ``` + +=== "C#" + + ```csharp title="binary_search_tree.cs" + /* 查詢節點 */ + TreeNode? Search(int num) { + TreeNode? cur = root; + // 迴圈查詢,越過葉節點後跳出 + while (cur != null) { + // 目標節點在 cur 的右子樹中 + if (cur.val < num) cur = + cur.right; + // 目標節點在 cur 的左子樹中 + else if (cur.val > num) + cur = cur.left; + // 找到目標節點,跳出迴圈 + else + break; + } + // 返回目標節點 + return cur; + } + ``` + +=== "Go" + + ```go title="binary_search_tree.go" + /* 查詢節點 */ + func (bst *binarySearchTree) search(num int) *TreeNode { + node := bst.root + // 迴圈查詢,越過葉節點後跳出 + for node != nil { + if node.Val.(int) < num { + // 目標節點在 cur 的右子樹中 + node = node.Right + } else if node.Val.(int) > num { + // 目標節點在 cur 的左子樹中 + node = node.Left + } else { + // 找到目標節點,跳出迴圈 + break + } + } + // 返回目標節點 + return node + } + ``` + +=== "Swift" + + ```swift title="binary_search_tree.swift" + /* 查詢節點 */ + func search(num: Int) -> TreeNode? { + var cur = root + // 迴圈查詢,越過葉節點後跳出 + while cur != nil { + // 目標節點在 cur 的右子樹中 + if cur!.val < num { + cur = cur?.right + } + // 目標節點在 cur 的左子樹中 + else if cur!.val > num { + cur = cur?.left + } + // 找到目標節點,跳出迴圈 + else { + break + } + } + // 返回目標節點 + return cur + } + ``` + +=== "JS" + + ```javascript title="binary_search_tree.js" + /* 查詢節點 */ + search(num) { + let cur = this.root; + // 迴圈查詢,越過葉節點後跳出 + while (cur !== null) { + // 目標節點在 cur 的右子樹中 + if (cur.val < num) cur = cur.right; + // 目標節點在 cur 的左子樹中 + else if (cur.val > num) cur = cur.left; + // 找到目標節點,跳出迴圈 + else break; + } + // 返回目標節點 + return cur; + } + ``` + +=== "TS" + + ```typescript title="binary_search_tree.ts" + /* 查詢節點 */ + search(num: number): TreeNode | null { + let cur = this.root; + // 迴圈查詢,越過葉節點後跳出 + while (cur !== null) { + // 目標節點在 cur 的右子樹中 + if (cur.val < num) cur = cur.right; + // 目標節點在 cur 的左子樹中 + else if (cur.val > num) cur = cur.left; + // 找到目標節點,跳出迴圈 + else break; + } + // 返回目標節點 + return cur; + } + ``` + +=== "Dart" + + ```dart title="binary_search_tree.dart" + /* 查詢節點 */ + TreeNode? search(int _num) { + TreeNode? cur = _root; + // 迴圈查詢,越過葉節點後跳出 + while (cur != null) { + // 目標節點在 cur 的右子樹中 + if (cur.val < _num) + cur = cur.right; + // 目標節點在 cur 的左子樹中 + else if (cur.val > _num) + cur = cur.left; + // 找到目標節點,跳出迴圈 + else + break; + } + // 返回目標節點 + return cur; + } + ``` + +=== "Rust" + + ```rust title="binary_search_tree.rs" + /* 查詢節點 */ + pub fn search(&self, num: i32) -> OptionTreeNodeRc { + let mut cur = self.root.clone(); + // 迴圈查詢,越過葉節點後跳出 + while let Some(node) = cur.clone() { + match num.cmp(&node.borrow().val) { + // 目標節點在 cur 的右子樹中 + Ordering::Greater => cur = node.borrow().right.clone(), + // 目標節點在 cur 的左子樹中 + Ordering::Less => cur = node.borrow().left.clone(), + // 找到目標節點,跳出迴圈 + Ordering::Equal => break, + } + } + + // 返回目標節點 + cur + } + ``` + +=== "C" + + ```c title="binary_search_tree.c" + /* 查詢節點 */ + TreeNode *search(BinarySearchTree *bst, int num) { + TreeNode *cur = bst->root; + // 迴圈查詢,越過葉節點後跳出 + while (cur != NULL) { + if (cur->val < num) { + // 目標節點在 cur 的右子樹中 + cur = cur->right; + } else if (cur->val > num) { + // 目標節點在 cur 的左子樹中 + cur = cur->left; + } else { + // 找到目標節點,跳出迴圈 + break; + } + } + // 返回目標節點 + return cur; + } + ``` + +=== "Kotlin" + + ```kotlin title="binary_search_tree.kt" + /* 查詢節點 */ + fun search(num: Int): TreeNode? { + var cur = root + // 迴圈查詢,越過葉節點後跳出 + while (cur != null) { + // 目標節點在 cur 的右子樹中 + cur = if (cur.value < num) cur.right + // 目標節點在 cur 的左子樹中 + else if (cur.value > num) cur.left + // 找到目標節點,跳出迴圈 + else break + } + // 返回目標節點 + return cur + } + ``` + +=== "Ruby" + + ```ruby title="binary_search_tree.rb" + [class]{BinarySearchTree}-[func]{search} + ``` + +=== "Zig" + + ```zig title="binary_search_tree.zig" + // 查詢節點 + fn search(self: *Self, num: T) ?*inc.TreeNode(T) { + var cur = self.root; + // 迴圈查詢,越過葉節點後跳出 + while (cur != null) { + // 目標節點在 cur 的右子樹中 + if (cur.?.val < num) { + cur = cur.?.right; + // 目標節點在 cur 的左子樹中 + } else if (cur.?.val > num) { + cur = cur.?.left; + // 找到目標節點,跳出迴圈 + } else { + break; + } + } + // 返回目標節點 + return cur; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +### 2.   插入節點 + +給定一個待插入元素 `num` ,為了保持二元搜尋樹“左子樹 < 根節點 < 右子樹”的性質,插入操作流程如圖 7-18 所示。 + +1. **查詢插入位置**:與查詢操作相似,從根節點出發,根據當前節點值和 `num` 的大小關係迴圈向下搜尋,直到越過葉節點(走訪至 `None` )時跳出迴圈。 +2. **在該位置插入節點**:初始化節點 `num` ,將該節點置於 `None` 的位置。 + +![在二元搜尋樹中插入節點](binary_search_tree.assets/bst_insert.png){ class="animation-figure" } + +

圖 7-18   在二元搜尋樹中插入節點

+ +在程式碼實現中,需要注意以下兩點。 + +- 二元搜尋樹不允許存在重複節點,否則將違反其定義。因此,若待插入節點在樹中已存在,則不執行插入,直接返回。 +- 為了實現插入節點,我們需要藉助節點 `pre` 儲存上一輪迴圈的節點。這樣在走訪至 `None` 時,我們可以獲取到其父節點,從而完成節點插入操作。 + +=== "Python" + + ```python title="binary_search_tree.py" + def insert(self, num: int): + """插入節點""" + # 若樹為空,則初始化根節點 + if self._root is None: + self._root = TreeNode(num) + return + # 迴圈查詢,越過葉節點後跳出 + cur, pre = self._root, None + while cur is not None: + # 找到重複節點,直接返回 + if cur.val == num: + return + pre = cur + # 插入位置在 cur 的右子樹中 + if cur.val < num: + cur = cur.right + # 插入位置在 cur 的左子樹中 + else: + cur = cur.left + # 插入節點 + node = TreeNode(num) + if pre.val < num: + pre.right = node + else: + pre.left = node + ``` + +=== "C++" + + ```cpp title="binary_search_tree.cpp" + /* 插入節點 */ + void insert(int num) { + // 若樹為空,則初始化根節點 + if (root == nullptr) { + root = new TreeNode(num); + return; + } + TreeNode *cur = root, *pre = nullptr; + // 迴圈查詢,越過葉節點後跳出 + while (cur != nullptr) { + // 找到重複節點,直接返回 + if (cur->val == num) + return; + pre = cur; + // 插入位置在 cur 的右子樹中 + if (cur->val < num) + cur = cur->right; + // 插入位置在 cur 的左子樹中 + else + cur = cur->left; + } + // 插入節點 + TreeNode *node = new TreeNode(num); + if (pre->val < num) + pre->right = node; + else + pre->left = node; + } + ``` + +=== "Java" + + ```java title="binary_search_tree.java" + /* 插入節點 */ + void insert(int num) { + // 若樹為空,則初始化根節點 + if (root == null) { + root = new TreeNode(num); + return; + } + TreeNode cur = root, pre = null; + // 迴圈查詢,越過葉節點後跳出 + while (cur != null) { + // 找到重複節點,直接返回 + if (cur.val == num) + return; + pre = cur; + // 插入位置在 cur 的右子樹中 + if (cur.val < num) + cur = cur.right; + // 插入位置在 cur 的左子樹中 + else + cur = cur.left; + } + // 插入節點 + TreeNode node = new TreeNode(num); + if (pre.val < num) + pre.right = node; + else + pre.left = node; + } + ``` + +=== "C#" + + ```csharp title="binary_search_tree.cs" + /* 插入節點 */ + void Insert(int num) { + // 若樹為空,則初始化根節點 + if (root == null) { + root = new TreeNode(num); + return; + } + TreeNode? cur = root, pre = null; + // 迴圈查詢,越過葉節點後跳出 + while (cur != null) { + // 找到重複節點,直接返回 + if (cur.val == num) + return; + pre = cur; + // 插入位置在 cur 的右子樹中 + if (cur.val < num) + cur = cur.right; + // 插入位置在 cur 的左子樹中 + else + cur = cur.left; + } + + // 插入節點 + TreeNode node = new(num); + if (pre != null) { + if (pre.val < num) + pre.right = node; + else + pre.left = node; + } + } + ``` + +=== "Go" + + ```go title="binary_search_tree.go" + /* 插入節點 */ + func (bst *binarySearchTree) insert(num int) { + cur := bst.root + // 若樹為空,則初始化根節點 + if cur == nil { + bst.root = NewTreeNode(num) + return + } + // 待插入節點之前的節點位置 + var pre *TreeNode = nil + // 迴圈查詢,越過葉節點後跳出 + for cur != nil { + if cur.Val == num { + return + } + pre = cur + if cur.Val.(int) < num { + cur = cur.Right + } else { + cur = cur.Left + } + } + // 插入節點 + node := NewTreeNode(num) + if pre.Val.(int) < num { + pre.Right = node + } else { + pre.Left = node + } + } + ``` + +=== "Swift" + + ```swift title="binary_search_tree.swift" + /* 插入節點 */ + func insert(num: Int) { + // 若樹為空,則初始化根節點 + if root == nil { + root = TreeNode(x: num) + return + } + var cur = root + var pre: TreeNode? + // 迴圈查詢,越過葉節點後跳出 + while cur != nil { + // 找到重複節點,直接返回 + if cur!.val == num { + return + } + pre = cur + // 插入位置在 cur 的右子樹中 + if cur!.val < num { + cur = cur?.right + } + // 插入位置在 cur 的左子樹中 + else { + cur = cur?.left + } + } + // 插入節點 + let node = TreeNode(x: num) + if pre!.val < num { + pre?.right = node + } else { + pre?.left = node + } + } + ``` + +=== "JS" + + ```javascript title="binary_search_tree.js" + /* 插入節點 */ + insert(num) { + // 若樹為空,則初始化根節點 + if (this.root === null) { + this.root = new TreeNode(num); + return; + } + let cur = this.root, + pre = null; + // 迴圈查詢,越過葉節點後跳出 + while (cur !== null) { + // 找到重複節點,直接返回 + if (cur.val === num) return; + pre = cur; + // 插入位置在 cur 的右子樹中 + if (cur.val < num) cur = cur.right; + // 插入位置在 cur 的左子樹中 + else cur = cur.left; + } + // 插入節點 + const node = new TreeNode(num); + if (pre.val < num) pre.right = node; + else pre.left = node; + } + ``` + +=== "TS" + + ```typescript title="binary_search_tree.ts" + /* 插入節點 */ + insert(num: number): void { + // 若樹為空,則初始化根節點 + if (this.root === null) { + this.root = new TreeNode(num); + return; + } + let cur: TreeNode | null = this.root, + pre: TreeNode | null = null; + // 迴圈查詢,越過葉節點後跳出 + while (cur !== null) { + // 找到重複節點,直接返回 + if (cur.val === num) return; + pre = cur; + // 插入位置在 cur 的右子樹中 + if (cur.val < num) cur = cur.right; + // 插入位置在 cur 的左子樹中 + else cur = cur.left; + } + // 插入節點 + const node = new TreeNode(num); + if (pre!.val < num) pre!.right = node; + else pre!.left = node; + } + ``` + +=== "Dart" + + ```dart title="binary_search_tree.dart" + /* 插入節點 */ + void insert(int _num) { + // 若樹為空,則初始化根節點 + if (_root == null) { + _root = TreeNode(_num); + return; + } + TreeNode? cur = _root; + TreeNode? pre = null; + // 迴圈查詢,越過葉節點後跳出 + while (cur != null) { + // 找到重複節點,直接返回 + if (cur.val == _num) return; + pre = cur; + // 插入位置在 cur 的右子樹中 + if (cur.val < _num) + cur = cur.right; + // 插入位置在 cur 的左子樹中 + else + cur = cur.left; + } + // 插入節點 + TreeNode? node = TreeNode(_num); + if (pre!.val < _num) + pre.right = node; + else + pre.left = node; + } + ``` + +=== "Rust" + + ```rust title="binary_search_tree.rs" + /* 插入節點 */ + pub fn insert(&mut self, num: i32) { + // 若樹為空,則初始化根節點 + if self.root.is_none() { + self.root = Some(TreeNode::new(num)); + return; + } + let mut cur = self.root.clone(); + let mut pre = None; + // 迴圈查詢,越過葉節點後跳出 + while let Some(node) = cur.clone() { + match num.cmp(&node.borrow().val) { + // 找到重複節點,直接返回 + Ordering::Equal => return, + // 插入位置在 cur 的右子樹中 + Ordering::Greater => { + pre = cur.clone(); + cur = node.borrow().right.clone(); + } + // 插入位置在 cur 的左子樹中 + Ordering::Less => { + pre = cur.clone(); + cur = node.borrow().left.clone(); + } + } + } + // 插入節點 + let pre = pre.unwrap(); + let node = Some(TreeNode::new(num)); + if num > pre.borrow().val { + pre.borrow_mut().right = node; + } else { + pre.borrow_mut().left = node; + } + } + ``` + +=== "C" + + ```c title="binary_search_tree.c" + /* 插入節點 */ + void insert(BinarySearchTree *bst, int num) { + // 若樹為空,則初始化根節點 + if (bst->root == NULL) { + bst->root = newTreeNode(num); + return; + } + TreeNode *cur = bst->root, *pre = NULL; + // 迴圈查詢,越過葉節點後跳出 + while (cur != NULL) { + // 找到重複節點,直接返回 + if (cur->val == num) { + return; + } + pre = cur; + if (cur->val < num) { + // 插入位置在 cur 的右子樹中 + cur = cur->right; + } else { + // 插入位置在 cur 的左子樹中 + cur = cur->left; + } + } + // 插入節點 + TreeNode *node = newTreeNode(num); + if (pre->val < num) { + pre->right = node; + } else { + pre->left = node; + } + } + ``` + +=== "Kotlin" + + ```kotlin title="binary_search_tree.kt" + /* 插入節點 */ + fun insert(num: Int) { + // 若樹為空,則初始化根節點 + if (root == null) { + root = TreeNode(num) + return + } + var cur = root + var pre: TreeNode? = null + // 迴圈查詢,越過葉節點後跳出 + while (cur != null) { + // 找到重複節點,直接返回 + if (cur.value == num) return + pre = cur + // 插入位置在 cur 的右子樹中 + cur = if (cur.value < num) cur.right + // 插入位置在 cur 的左子樹中 + else cur.left + } + // 插入節點 + val node = TreeNode(num) + if (pre?.value!! < num) pre.right = node + else pre.left = node + } + ``` + +=== "Ruby" + + ```ruby title="binary_search_tree.rb" + [class]{BinarySearchTree}-[func]{insert} + ``` + +=== "Zig" + + ```zig title="binary_search_tree.zig" + // 插入節點 + fn insert(self: *Self, num: T) !void { + // 若樹為空,則初始化根節點 + if (self.root == null) { + self.root = try self.mem_allocator.create(inc.TreeNode(T)); + return; + } + var cur = self.root; + var pre: ?*inc.TreeNode(T) = null; + // 迴圈查詢,越過葉節點後跳出 + while (cur != null) { + // 找到重複節點,直接返回 + if (cur.?.val == num) return; + pre = cur; + // 插入位置在 cur 的右子樹中 + if (cur.?.val < num) { + cur = cur.?.right; + // 插入位置在 cur 的左子樹中 + } else { + cur = cur.?.left; + } + } + // 插入節點 + var node = try self.mem_allocator.create(inc.TreeNode(T)); + node.init(num); + if (pre.?.val < num) { + pre.?.right = node; + } else { + pre.?.left = node; + } + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +與查詢節點相同,插入節點使用 $O(\log n)$ 時間。 + +### 3.   刪除節點 + +先在二元樹中查詢到目標節點,再將其刪除。與插入節點類似,我們需要保證在刪除操作完成後,二元搜尋樹的“左子樹 < 根節點 < 右子樹”的性質仍然滿足。因此,我們根據目標節點的子節點數量,分 0、1 和 2 三種情況,執行對應的刪除節點操作。 + +如圖 7-19 所示,當待刪除節點的度為 $0$ 時,表示該節點是葉節點,可以直接刪除。 + +![在二元搜尋樹中刪除節點(度為 0 )](binary_search_tree.assets/bst_remove_case1.png){ class="animation-figure" } + +

圖 7-19   在二元搜尋樹中刪除節點(度為 0 )

+ +如圖 7-20 所示,當待刪除節點的度為 $1$ 時,將待刪除節點替換為其子節點即可。 + +![在二元搜尋樹中刪除節點(度為 1 )](binary_search_tree.assets/bst_remove_case2.png){ class="animation-figure" } + +

圖 7-20   在二元搜尋樹中刪除節點(度為 1 )

+ +當待刪除節點的度為 $2$ 時,我們無法直接刪除它,而需要使用一個節點替換該節點。由於要保持二元搜尋樹“左子樹 $<$ 根節點 $<$ 右子樹”的性質,**因此這個節點可以是右子樹的最小節點或左子樹的最大節點**。 + +假設我們選擇右子樹的最小節點(中序走訪的下一個節點),則刪除操作流程如圖 7-21 所示。 + +1. 找到待刪除節點在“中序走訪序列”中的下一個節點,記為 `tmp` 。 +2. 用 `tmp` 的值覆蓋待刪除節點的值,並在樹中遞迴刪除節點 `tmp` 。 + +=== "<1>" + ![在二元搜尋樹中刪除節點(度為 2 )](binary_search_tree.assets/bst_remove_case3_step1.png){ class="animation-figure" } + +=== "<2>" + ![bst_remove_case3_step2](binary_search_tree.assets/bst_remove_case3_step2.png){ class="animation-figure" } + +=== "<3>" + ![bst_remove_case3_step3](binary_search_tree.assets/bst_remove_case3_step3.png){ class="animation-figure" } + +=== "<4>" + ![bst_remove_case3_step4](binary_search_tree.assets/bst_remove_case3_step4.png){ class="animation-figure" } + +

圖 7-21   在二元搜尋樹中刪除節點(度為 2 )

+ +刪除節點操作同樣使用 $O(\log n)$ 時間,其中查詢待刪除節點需要 $O(\log n)$ 時間,獲取中序走訪後繼節點需要 $O(\log n)$ 時間。示例程式碼如下: + +=== "Python" + + ```python title="binary_search_tree.py" + def remove(self, num: int): + """刪除節點""" + # 若樹為空,直接提前返回 + if self._root is None: + return + # 迴圈查詢,越過葉節點後跳出 + cur, pre = self._root, None + while cur is not None: + # 找到待刪除節點,跳出迴圈 + if cur.val == num: + break + pre = cur + # 待刪除節點在 cur 的右子樹中 + if cur.val < num: + cur = cur.right + # 待刪除節點在 cur 的左子樹中 + else: + cur = cur.left + # 若無待刪除節點,則直接返回 + if cur is None: + return + + # 子節點數量 = 0 or 1 + if cur.left is None or cur.right is None: + # 當子節點數量 = 0 / 1 時, child = null / 該子節點 + child = cur.left or cur.right + # 刪除節點 cur + if cur != self._root: + if pre.left == cur: + pre.left = child + else: + pre.right = child + else: + # 若刪除節點為根節點,則重新指定根節點 + self._root = child + # 子節點數量 = 2 + else: + # 獲取中序走訪中 cur 的下一個節點 + tmp: TreeNode = cur.right + while tmp.left is not None: + tmp = tmp.left + # 遞迴刪除節點 tmp + self.remove(tmp.val) + # 用 tmp 覆蓋 cur + cur.val = tmp.val + ``` + +=== "C++" + + ```cpp title="binary_search_tree.cpp" + /* 刪除節點 */ + void remove(int num) { + // 若樹為空,直接提前返回 + if (root == nullptr) + return; + TreeNode *cur = root, *pre = nullptr; + // 迴圈查詢,越過葉節點後跳出 + while (cur != nullptr) { + // 找到待刪除節點,跳出迴圈 + if (cur->val == num) + break; + pre = cur; + // 待刪除節點在 cur 的右子樹中 + if (cur->val < num) + cur = cur->right; + // 待刪除節點在 cur 的左子樹中 + else + cur = cur->left; + } + // 若無待刪除節點,則直接返回 + if (cur == nullptr) + return; + // 子節點數量 = 0 or 1 + if (cur->left == nullptr || cur->right == nullptr) { + // 當子節點數量 = 0 / 1 時, child = nullptr / 該子節點 + TreeNode *child = cur->left != nullptr ? cur->left : cur->right; + // 刪除節點 cur + if (cur != root) { + if (pre->left == cur) + pre->left = child; + else + pre->right = child; + } else { + // 若刪除節點為根節點,則重新指定根節點 + root = child; + } + // 釋放記憶體 + delete cur; + } + // 子節點數量 = 2 + else { + // 獲取中序走訪中 cur 的下一個節點 + TreeNode *tmp = cur->right; + while (tmp->left != nullptr) { + tmp = tmp->left; + } + int tmpVal = tmp->val; + // 遞迴刪除節點 tmp + remove(tmp->val); + // 用 tmp 覆蓋 cur + cur->val = tmpVal; + } + } + ``` + +=== "Java" + + ```java title="binary_search_tree.java" + /* 刪除節點 */ + void remove(int num) { + // 若樹為空,直接提前返回 + if (root == null) + return; + TreeNode cur = root, pre = null; + // 迴圈查詢,越過葉節點後跳出 + while (cur != null) { + // 找到待刪除節點,跳出迴圈 + if (cur.val == num) + break; + pre = cur; + // 待刪除節點在 cur 的右子樹中 + if (cur.val < num) + cur = cur.right; + // 待刪除節點在 cur 的左子樹中 + else + cur = cur.left; + } + // 若無待刪除節點,則直接返回 + if (cur == null) + return; + // 子節點數量 = 0 or 1 + if (cur.left == null || cur.right == null) { + // 當子節點數量 = 0 / 1 時, child = null / 該子節點 + TreeNode child = cur.left != null ? cur.left : cur.right; + // 刪除節點 cur + if (cur != root) { + if (pre.left == cur) + pre.left = child; + else + pre.right = child; + } else { + // 若刪除節點為根節點,則重新指定根節點 + root = child; + } + } + // 子節點數量 = 2 + else { + // 獲取中序走訪中 cur 的下一個節點 + TreeNode tmp = cur.right; + while (tmp.left != null) { + tmp = tmp.left; + } + // 遞迴刪除節點 tmp + remove(tmp.val); + // 用 tmp 覆蓋 cur + cur.val = tmp.val; + } + } + ``` + +=== "C#" + + ```csharp title="binary_search_tree.cs" + /* 刪除節點 */ + void Remove(int num) { + // 若樹為空,直接提前返回 + if (root == null) + return; + TreeNode? cur = root, pre = null; + // 迴圈查詢,越過葉節點後跳出 + while (cur != null) { + // 找到待刪除節點,跳出迴圈 + if (cur.val == num) + break; + pre = cur; + // 待刪除節點在 cur 的右子樹中 + if (cur.val < num) + cur = cur.right; + // 待刪除節點在 cur 的左子樹中 + else + cur = cur.left; + } + // 若無待刪除節點,則直接返回 + if (cur == null) + return; + // 子節點數量 = 0 or 1 + if (cur.left == null || cur.right == null) { + // 當子節點數量 = 0 / 1 時, child = null / 該子節點 + TreeNode? child = cur.left ?? cur.right; + // 刪除節點 cur + if (cur != root) { + if (pre!.left == cur) + pre.left = child; + else + pre.right = child; + } else { + // 若刪除節點為根節點,則重新指定根節點 + root = child; + } + } + // 子節點數量 = 2 + else { + // 獲取中序走訪中 cur 的下一個節點 + TreeNode? tmp = cur.right; + while (tmp.left != null) { + tmp = tmp.left; + } + // 遞迴刪除節點 tmp + Remove(tmp.val!.Value); + // 用 tmp 覆蓋 cur + cur.val = tmp.val; + } + } + ``` + +=== "Go" + + ```go title="binary_search_tree.go" + /* 刪除節點 */ + func (bst *binarySearchTree) remove(num int) { + cur := bst.root + // 若樹為空,直接提前返回 + if cur == nil { + return + } + // 待刪除節點之前的節點位置 + var pre *TreeNode = nil + // 迴圈查詢,越過葉節點後跳出 + for cur != nil { + if cur.Val == num { + break + } + pre = cur + if cur.Val.(int) < num { + // 待刪除節點在右子樹中 + cur = cur.Right + } else { + // 待刪除節點在左子樹中 + cur = cur.Left + } + } + // 若無待刪除節點,則直接返回 + if cur == nil { + return + } + // 子節點數為 0 或 1 + if cur.Left == nil || cur.Right == nil { + var child *TreeNode = nil + // 取出待刪除節點的子節點 + if cur.Left != nil { + child = cur.Left + } else { + child = cur.Right + } + // 刪除節點 cur + if cur != bst.root { + if pre.Left == cur { + pre.Left = child + } else { + pre.Right = child + } + } else { + // 若刪除節點為根節點,則重新指定根節點 + bst.root = child + } + // 子節點數為 2 + } else { + // 獲取中序走訪中待刪除節點 cur 的下一個節點 + tmp := cur.Right + for tmp.Left != nil { + tmp = tmp.Left + } + // 遞迴刪除節點 tmp + bst.remove(tmp.Val.(int)) + // 用 tmp 覆蓋 cur + cur.Val = tmp.Val + } + } + ``` + +=== "Swift" + + ```swift title="binary_search_tree.swift" + /* 刪除節點 */ + func remove(num: Int) { + // 若樹為空,直接提前返回 + if root == nil { + return + } + var cur = root + var pre: TreeNode? + // 迴圈查詢,越過葉節點後跳出 + while cur != nil { + // 找到待刪除節點,跳出迴圈 + if cur!.val == num { + break + } + pre = cur + // 待刪除節點在 cur 的右子樹中 + if cur!.val < num { + cur = cur?.right + } + // 待刪除節點在 cur 的左子樹中 + else { + cur = cur?.left + } + } + // 若無待刪除節點,則直接返回 + if cur == nil { + return + } + // 子節點數量 = 0 or 1 + if cur?.left == nil || cur?.right == nil { + // 當子節點數量 = 0 / 1 時, child = null / 該子節點 + let child = cur?.left ?? cur?.right + // 刪除節點 cur + if cur !== root { + if pre?.left === cur { + pre?.left = child + } else { + pre?.right = child + } + } else { + // 若刪除節點為根節點,則重新指定根節點 + root = child + } + } + // 子節點數量 = 2 + else { + // 獲取中序走訪中 cur 的下一個節點 + var tmp = cur?.right + while tmp?.left != nil { + tmp = tmp?.left + } + // 遞迴刪除節點 tmp + remove(num: tmp!.val) + // 用 tmp 覆蓋 cur + cur?.val = tmp!.val + } + } + ``` + +=== "JS" + + ```javascript title="binary_search_tree.js" + /* 刪除節點 */ + remove(num) { + // 若樹為空,直接提前返回 + if (this.root === null) return; + let cur = this.root, + pre = null; + // 迴圈查詢,越過葉節點後跳出 + while (cur !== null) { + // 找到待刪除節點,跳出迴圈 + if (cur.val === num) break; + pre = cur; + // 待刪除節點在 cur 的右子樹中 + if (cur.val < num) cur = cur.right; + // 待刪除節點在 cur 的左子樹中 + else cur = cur.left; + } + // 若無待刪除節點,則直接返回 + if (cur === null) return; + // 子節點數量 = 0 or 1 + if (cur.left === null || cur.right === null) { + // 當子節點數量 = 0 / 1 時, child = null / 該子節點 + const child = cur.left !== null ? cur.left : cur.right; + // 刪除節點 cur + if (cur !== this.root) { + if (pre.left === cur) pre.left = child; + else pre.right = child; + } else { + // 若刪除節點為根節點,則重新指定根節點 + this.root = child; + } + } + // 子節點數量 = 2 + else { + // 獲取中序走訪中 cur 的下一個節點 + let tmp = cur.right; + while (tmp.left !== null) { + tmp = tmp.left; + } + // 遞迴刪除節點 tmp + this.remove(tmp.val); + // 用 tmp 覆蓋 cur + cur.val = tmp.val; + } + } + ``` + +=== "TS" + + ```typescript title="binary_search_tree.ts" + /* 刪除節點 */ + remove(num: number): void { + // 若樹為空,直接提前返回 + if (this.root === null) return; + let cur: TreeNode | null = this.root, + pre: TreeNode | null = null; + // 迴圈查詢,越過葉節點後跳出 + while (cur !== null) { + // 找到待刪除節點,跳出迴圈 + if (cur.val === num) break; + pre = cur; + // 待刪除節點在 cur 的右子樹中 + if (cur.val < num) cur = cur.right; + // 待刪除節點在 cur 的左子樹中 + else cur = cur.left; + } + // 若無待刪除節點,則直接返回 + if (cur === null) return; + // 子節點數量 = 0 or 1 + if (cur.left === null || cur.right === null) { + // 當子節點數量 = 0 / 1 時, child = null / 該子節點 + const child: TreeNode | null = + cur.left !== null ? cur.left : cur.right; + // 刪除節點 cur + if (cur !== this.root) { + if (pre!.left === cur) pre!.left = child; + else pre!.right = child; + } else { + // 若刪除節點為根節點,則重新指定根節點 + this.root = child; + } + } + // 子節點數量 = 2 + else { + // 獲取中序走訪中 cur 的下一個節點 + let tmp: TreeNode | null = cur.right; + while (tmp!.left !== null) { + tmp = tmp!.left; + } + // 遞迴刪除節點 tmp + this.remove(tmp!.val); + // 用 tmp 覆蓋 cur + cur.val = tmp!.val; + } + } + ``` + +=== "Dart" + + ```dart title="binary_search_tree.dart" + /* 刪除節點 */ + void remove(int _num) { + // 若樹為空,直接提前返回 + if (_root == null) return; + TreeNode? cur = _root; + TreeNode? pre = null; + // 迴圈查詢,越過葉節點後跳出 + while (cur != null) { + // 找到待刪除節點,跳出迴圈 + if (cur.val == _num) break; + pre = cur; + // 待刪除節點在 cur 的右子樹中 + if (cur.val < _num) + cur = cur.right; + // 待刪除節點在 cur 的左子樹中 + else + cur = cur.left; + } + // 若無待刪除節點,直接返回 + if (cur == null) return; + // 子節點數量 = 0 or 1 + if (cur.left == null || cur.right == null) { + // 當子節點數量 = 0 / 1 時, child = null / 該子節點 + TreeNode? child = cur.left ?? cur.right; + // 刪除節點 cur + if (cur != _root) { + if (pre!.left == cur) + pre.left = child; + else + pre.right = child; + } else { + // 若刪除節點為根節點,則重新指定根節點 + _root = child; + } + } else { + // 子節點數量 = 2 + // 獲取中序走訪中 cur 的下一個節點 + TreeNode? tmp = cur.right; + while (tmp!.left != null) { + tmp = tmp.left; + } + // 遞迴刪除節點 tmp + remove(tmp.val); + // 用 tmp 覆蓋 cur + cur.val = tmp.val; + } + } + ``` + +=== "Rust" + + ```rust title="binary_search_tree.rs" + /* 刪除節點 */ + pub fn remove(&mut self, num: i32) { + // 若樹為空,直接提前返回 + if self.root.is_none() { + return; + } + let mut cur = self.root.clone(); + let mut pre = None; + // 迴圈查詢,越過葉節點後跳出 + while let Some(node) = cur.clone() { + match num.cmp(&node.borrow().val) { + // 找到待刪除節點,跳出迴圈 + Ordering::Equal => break, + // 待刪除節點在 cur 的右子樹中 + Ordering::Greater => { + pre = cur.clone(); + cur = node.borrow().right.clone(); + } + // 待刪除節點在 cur 的左子樹中 + Ordering::Less => { + pre = cur.clone(); + cur = node.borrow().left.clone(); + } + } + } + // 若無待刪除節點,則直接返回 + if cur.is_none() { + return; + } + let cur = cur.unwrap(); + let (left_child, right_child) = (cur.borrow().left.clone(), cur.borrow().right.clone()); + match (left_child.clone(), right_child.clone()) { + // 子節點數量 = 0 or 1 + (None, None) | (Some(_), None) | (None, Some(_)) => { + // 當子節點數量 = 0 / 1 時, child = nullptr / 該子節點 + let child = left_child.or(right_child); + let pre = pre.unwrap(); + // 刪除節點 cur + if !Rc::ptr_eq(&cur, self.root.as_ref().unwrap()) { + let left = pre.borrow().left.clone(); + if left.is_some() && Rc::ptr_eq(&left.as_ref().unwrap(), &cur) { + pre.borrow_mut().left = child; + } else { + pre.borrow_mut().right = child; + } + } else { + // 若刪除節點為根節點,則重新指定根節點 + self.root = child; + } + } + // 子節點數量 = 2 + (Some(_), Some(_)) => { + // 獲取中序走訪中 cur 的下一個節點 + let mut tmp = cur.borrow().right.clone(); + while let Some(node) = tmp.clone() { + if node.borrow().left.is_some() { + tmp = node.borrow().left.clone(); + } else { + break; + } + } + let tmpval = tmp.unwrap().borrow().val; + // 遞迴刪除節點 tmp + self.remove(tmpval); + // 用 tmp 覆蓋 cur + cur.borrow_mut().val = tmpval; + } + } + } + ``` + +=== "C" + + ```c title="binary_search_tree.c" + /* 刪除節點 */ + // 由於引入了 stdio.h ,此處無法使用 remove 關鍵詞 + void removeItem(BinarySearchTree *bst, int num) { + // 若樹為空,直接提前返回 + if (bst->root == NULL) + return; + TreeNode *cur = bst->root, *pre = NULL; + // 迴圈查詢,越過葉節點後跳出 + while (cur != NULL) { + // 找到待刪除節點,跳出迴圈 + if (cur->val == num) + break; + pre = cur; + if (cur->val < num) { + // 待刪除節點在 root 的右子樹中 + cur = cur->right; + } else { + // 待刪除節點在 root 的左子樹中 + cur = cur->left; + } + } + // 若無待刪除節點,則直接返回 + if (cur == NULL) + return; + // 判斷待刪除節點是否存在子節點 + if (cur->left == NULL || cur->right == NULL) { + /* 子節點數量 = 0 or 1 */ + // 當子節點數量 = 0 / 1 時, child = nullptr / 該子節點 + TreeNode *child = cur->left != NULL ? cur->left : cur->right; + // 刪除節點 cur + if (pre->left == cur) { + pre->left = child; + } else { + pre->right = child; + } + // 釋放記憶體 + free(cur); + } else { + /* 子節點數量 = 2 */ + // 獲取中序走訪中 cur 的下一個節點 + TreeNode *tmp = cur->right; + while (tmp->left != NULL) { + tmp = tmp->left; + } + int tmpVal = tmp->val; + // 遞迴刪除節點 tmp + removeItem(bst, tmp->val); + // 用 tmp 覆蓋 cur + cur->val = tmpVal; + } + } + ``` + +=== "Kotlin" + + ```kotlin title="binary_search_tree.kt" + /* 刪除節點 */ + fun remove(num: Int) { + // 若樹為空,直接提前返回 + if (root == null) return + var cur = root + var pre: TreeNode? = null + // 迴圈查詢,越過葉節點後跳出 + while (cur != null) { + // 找到待刪除節點,跳出迴圈 + if (cur.value == num) break + pre = cur + // 待刪除節點在 cur 的右子樹中 + cur = if (cur.value < num) cur.right + // 待刪除節點在 cur 的左子樹中 + else cur.left + } + // 若無待刪除節點,則直接返回 + if (cur == null) return + // 子節點數量 = 0 or 1 + if (cur.left == null || cur.right == null) { + // 當子節點數量 = 0 / 1 時, child = null / 該子節點 + val child = if (cur.left != null) cur.left else cur.right + // 刪除節點 cur + if (cur != root) { + if (pre!!.left == cur) pre.left = child + else pre.right = child + } else { + // 若刪除節點為根節點,則重新指定根節點 + root = child + } + // 子節點數量 = 2 + } else { + // 獲取中序走訪中 cur 的下一個節點 + var tmp = cur.right + while (tmp!!.left != null) { + tmp = tmp.left + } + // 遞迴刪除節點 tmp + remove(tmp.value) + // 用 tmp 覆蓋 cur + cur.value = tmp.value + } + } + ``` + +=== "Ruby" + + ```ruby title="binary_search_tree.rb" + [class]{BinarySearchTree}-[func]{remove} + ``` + +=== "Zig" + + ```zig title="binary_search_tree.zig" + // 刪除節點 + fn remove(self: *Self, num: T) void { + // 若樹為空,直接提前返回 + if (self.root == null) return; + var cur = self.root; + var pre: ?*inc.TreeNode(T) = null; + // 迴圈查詢,越過葉節點後跳出 + while (cur != null) { + // 找到待刪除節點,跳出迴圈 + if (cur.?.val == num) break; + pre = cur; + // 待刪除節點在 cur 的右子樹中 + if (cur.?.val < num) { + cur = cur.?.right; + // 待刪除節點在 cur 的左子樹中 + } else { + cur = cur.?.left; + } + } + // 若無待刪除節點,則直接返回 + if (cur == null) return; + // 子節點數量 = 0 or 1 + if (cur.?.left == null or cur.?.right == null) { + // 當子節點數量 = 0 / 1 時, child = null / 該子節點 + var child = if (cur.?.left != null) cur.?.left else cur.?.right; + // 刪除節點 cur + if (pre.?.left == cur) { + pre.?.left = child; + } else { + pre.?.right = child; + } + // 子節點數量 = 2 + } else { + // 獲取中序走訪中 cur 的下一個節點 + var tmp = cur.?.right; + while (tmp.?.left != null) { + tmp = tmp.?.left; + } + var tmp_val = tmp.?.val; + // 遞迴刪除節點 tmp + self.remove(tmp.?.val); + // 用 tmp 覆蓋 cur + cur.?.val = tmp_val; + } + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +### 4.   中序走訪有序 + +如圖 7-22 所示,二元樹的中序走訪遵循“左 $\rightarrow$ 根 $\rightarrow$ 右”的走訪順序,而二元搜尋樹滿足“左子節點 $<$ 根節點 $<$ 右子節點”的大小關係。 + +這意味著在二元搜尋樹中進行中序走訪時,總是會優先走訪下一個最小節點,從而得出一個重要性質:**二元搜尋樹的中序走訪序列是升序的**。 + +利用中序走訪升序的性質,我們在二元搜尋樹中獲取有序資料僅需 $O(n)$ 時間,無須進行額外的排序操作,非常高效。 + +![二元搜尋樹的中序走訪序列](binary_search_tree.assets/bst_inorder_traversal.png){ class="animation-figure" } + +

圖 7-22   二元搜尋樹的中序走訪序列

+ +## 7.4.2   二元搜尋樹的效率 + +給定一組資料,我們考慮使用陣列或二元搜尋樹儲存。觀察表 7-2 ,二元搜尋樹的各項操作的時間複雜度都是對數階,具有穩定且高效的效能。只有在高頻新增、低頻查詢刪除資料的場景下,陣列比二元搜尋樹的效率更高。 + +

表 7-2   陣列與搜尋樹的效率對比

+ +
+ +| | 無序陣列 | 二元搜尋樹 | +| -------- | -------- | ----------- | +| 查詢元素 | $O(n)$ | $O(\log n)$ | +| 插入元素 | $O(1)$ | $O(\log n)$ | +| 刪除元素 | $O(n)$ | $O(\log n)$ | + +
+ +在理想情況下,二元搜尋樹是“平衡”的,這樣就可以在 $\log n$ 輪迴圈內查詢任意節點。 + +然而,如果我們在二元搜尋樹中不斷地插入和刪除節點,可能導致二元樹退化為圖 7-23 所示的鏈結串列,這時各種操作的時間複雜度也會退化為 $O(n)$ 。 + +![二元搜尋樹退化](binary_search_tree.assets/bst_degradation.png){ class="animation-figure" } + +

圖 7-23   二元搜尋樹退化

+ +## 7.4.3   二元搜尋樹常見應用 + +- 用作系統中的多級索引,實現高效的查詢、插入、刪除操作。 +- 作為某些搜尋演算法的底層資料結構。 +- 用於儲存資料流,以保持其有序狀態。 diff --git a/zh-Hant/docs/chapter_tree/binary_tree.md b/zh-Hant/docs/chapter_tree/binary_tree.md new file mode 100644 index 000000000..efb244f31 --- /dev/null +++ b/zh-Hant/docs/chapter_tree/binary_tree.md @@ -0,0 +1,688 @@ +--- +comments: true +--- + +# 7.1   二元樹 + +二元樹(binary tree)是一種非線性資料結構,代表“祖先”與“後代”之間的派生關係,體現了“一分為二”的分治邏輯。與鏈結串列類似,二元樹的基本單元是節點,每個節點包含值、左子節點引用和右子節點引用。 + +=== "Python" + + ```python title="" + class TreeNode: + """二元樹節點類別""" + def __init__(self, val: int): + self.val: int = val # 節點值 + self.left: TreeNode | None = None # 左子節點引用 + self.right: TreeNode | None = None # 右子節點引用 + ``` + +=== "C++" + + ```cpp title="" + /* 二元樹節點結構體 */ + struct TreeNode { + int val; // 節點值 + TreeNode *left; // 左子節點指標 + TreeNode *right; // 右子節點指標 + TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} + }; + ``` + +=== "Java" + + ```java title="" + /* 二元樹節點類別 */ + class TreeNode { + int val; // 節點值 + TreeNode left; // 左子節點引用 + TreeNode right; // 右子節點引用 + TreeNode(int x) { val = x; } + } + ``` + +=== "C#" + + ```csharp title="" + /* 二元樹節點類別 */ + class TreeNode(int? x) { + public int? val = x; // 節點值 + public TreeNode? left; // 左子節點引用 + public TreeNode? right; // 右子節點引用 + } + ``` + +=== "Go" + + ```go title="" + /* 二元樹節點結構體 */ + type TreeNode struct { + Val int + Left *TreeNode + Right *TreeNode + } + /* 建構子 */ + func NewTreeNode(v int) *TreeNode { + return &TreeNode{ + Left: nil, // 左子節點指標 + Right: nil, // 右子節點指標 + Val: v, // 節點值 + } + } + ``` + +=== "Swift" + + ```swift title="" + /* 二元樹節點類別 */ + class TreeNode { + var val: Int // 節點值 + var left: TreeNode? // 左子節點引用 + var right: TreeNode? // 右子節點引用 + + init(x: Int) { + val = x + } + } + ``` + +=== "JS" + + ```javascript title="" + /* 二元樹節點類別 */ + class TreeNode { + val; // 節點值 + left; // 左子節點指標 + right; // 右子節點指標 + constructor(val, left, right) { + this.val = val === undefined ? 0 : val; + this.left = left === undefined ? null : left; + this.right = right === undefined ? null : right; + } + } + ``` + +=== "TS" + + ```typescript title="" + /* 二元樹節點類別 */ + class TreeNode { + val: number; + left: TreeNode | null; + right: TreeNode | null; + + constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) { + this.val = val === undefined ? 0 : val; // 節點值 + this.left = left === undefined ? null : left; // 左子節點引用 + this.right = right === undefined ? null : right; // 右子節點引用 + } + } + ``` + +=== "Dart" + + ```dart title="" + /* 二元樹節點類別 */ + class TreeNode { + int val; // 節點值 + TreeNode? left; // 左子節點引用 + TreeNode? right; // 右子節點引用 + TreeNode(this.val, [this.left, this.right]); + } + ``` + +=== "Rust" + + ```rust title="" + use std::rc::Rc; + use std::cell::RefCell; + + /* 二元樹節點結構體 */ + struct TreeNode { + val: i32, // 節點值 + left: Option>>, // 左子節點引用 + right: Option>>, // 右子節點引用 + } + + impl TreeNode { + /* 建構子 */ + fn new(val: i32) -> Rc> { + Rc::new(RefCell::new(Self { + val, + left: None, + right: None + })) + } + } + ``` + +=== "C" + + ```c title="" + /* 二元樹節點結構體 */ + typedef struct TreeNode { + int val; // 節點值 + int height; // 節點高度 + struct TreeNode *left; // 左子節點指標 + struct TreeNode *right; // 右子節點指標 + } TreeNode; + + /* 建構子 */ + TreeNode *newTreeNode(int val) { + TreeNode *node; + + node = (TreeNode *)malloc(sizeof(TreeNode)); + node->val = val; + node->height = 0; + node->left = NULL; + node->right = NULL; + return node; + } + ``` + +=== "Kotlin" + + ```kotlin title="" + /* 二元樹節點類別 */ + class TreeNode(val _val: Int) { // 節點值 + val left: TreeNode? = null // 左子節點引用 + val right: TreeNode? = null // 右子節點引用 + } + ``` + +=== "Ruby" + + ```ruby title="" + + ``` + +=== "Zig" + + ```zig title="" + + ``` + +每個節點都有兩個引用(指標),分別指向左子節點(left-child node)右子節點(right-child node),該節點被稱為這兩個子節點的父節點(parent node)。當給定一個二元樹的節點時,我們將該節點的左子節點及其以下節點形成的樹稱為該節點的左子樹(left subtree),同理可得右子樹(right subtree)。 + +**在二元樹中,除葉節點外,其他所有節點都包含子節點和非空子樹**。如圖 7-1 所示,如果將“節點 2”視為父節點,則其左子節點和右子節點分別是“節點 4”和“節點 5”,左子樹是“節點 4 及其以下節點形成的樹”,右子樹是“節點 5 及其以下節點形成的樹”。 + +![父節點、子節點、子樹](binary_tree.assets/binary_tree_definition.png){ class="animation-figure" } + +

圖 7-1   父節點、子節點、子樹

+ +## 7.1.1   二元樹常見術語 + +二元樹的常用術語如圖 7-2 所示。 + +- 根節點(root node):位於二元樹頂層的節點,沒有父節點。 +- 葉節點(leaf node):沒有子節點的節點,其兩個指標均指向 `None` 。 +- 邊(edge):連線兩個節點的線段,即節點引用(指標)。 +- 節點所在的層(level):從頂至底遞增,根節點所在層為 1 。 +- 節點的度(degree):節點的子節點的數量。在二元樹中,度的取值範圍是 0、1、2 。 +- 二元樹的高度(height):從根節點到最遠葉節點所經過的邊的數量。 +- 節點的深度(depth):從根節點到該節點所經過的邊的數量。 +- 節點的高度(height):從距離該節點最遠的葉節點到該節點所經過的邊的數量。 + +![二元樹的常用術語](binary_tree.assets/binary_tree_terminology.png){ class="animation-figure" } + +

圖 7-2   二元樹的常用術語

+ +!!! tip + + 請注意,我們通常將“高度”和“深度”定義為“經過的邊的數量”,但有些題目或教材可能會將其定義為“經過的節點的數量”。在這種情況下,高度和深度都需要加 1 。 + +## 7.1.2   二元樹基本操作 + +### 1.   初始化二元樹 + +與鏈結串列類似,首先初始化節點,然後構建引用(指標)。 + +=== "Python" + + ```python title="binary_tree.py" + # 初始化二元樹 + # 初始化節點 + n1 = TreeNode(val=1) + n2 = TreeNode(val=2) + n3 = TreeNode(val=3) + n4 = TreeNode(val=4) + n5 = TreeNode(val=5) + # 構建節點之間的引用(指標) + n1.left = n2 + n1.right = n3 + n2.left = n4 + n2.right = n5 + ``` + +=== "C++" + + ```cpp title="binary_tree.cpp" + /* 初始化二元樹 */ + // 初始化節點 + TreeNode* n1 = new TreeNode(1); + TreeNode* n2 = new TreeNode(2); + TreeNode* n3 = new TreeNode(3); + TreeNode* n4 = new TreeNode(4); + TreeNode* n5 = new TreeNode(5); + // 構建節點之間的引用(指標) + n1->left = n2; + n1->right = n3; + n2->left = n4; + n2->right = n5; + ``` + +=== "Java" + + ```java title="binary_tree.java" + // 初始化節點 + TreeNode n1 = new TreeNode(1); + TreeNode n2 = new TreeNode(2); + TreeNode n3 = new TreeNode(3); + TreeNode n4 = new TreeNode(4); + TreeNode n5 = new TreeNode(5); + // 構建節點之間的引用(指標) + n1.left = n2; + n1.right = n3; + n2.left = n4; + n2.right = n5; + ``` + +=== "C#" + + ```csharp title="binary_tree.cs" + /* 初始化二元樹 */ + // 初始化節點 + TreeNode n1 = new(1); + TreeNode n2 = new(2); + TreeNode n3 = new(3); + TreeNode n4 = new(4); + TreeNode n5 = new(5); + // 構建節點之間的引用(指標) + n1.left = n2; + n1.right = n3; + n2.left = n4; + n2.right = n5; + ``` + +=== "Go" + + ```go title="binary_tree.go" + /* 初始化二元樹 */ + // 初始化節點 + n1 := NewTreeNode(1) + n2 := NewTreeNode(2) + n3 := NewTreeNode(3) + n4 := NewTreeNode(4) + n5 := NewTreeNode(5) + // 構建節點之間的引用(指標) + n1.Left = n2 + n1.Right = n3 + n2.Left = n4 + n2.Right = n5 + ``` + +=== "Swift" + + ```swift title="binary_tree.swift" + // 初始化節點 + let n1 = TreeNode(x: 1) + let n2 = TreeNode(x: 2) + let n3 = TreeNode(x: 3) + let n4 = TreeNode(x: 4) + let n5 = TreeNode(x: 5) + // 構建節點之間的引用(指標) + n1.left = n2 + n1.right = n3 + n2.left = n4 + n2.right = n5 + ``` + +=== "JS" + + ```javascript title="binary_tree.js" + /* 初始化二元樹 */ + // 初始化節點 + let n1 = new TreeNode(1), + n2 = new TreeNode(2), + n3 = new TreeNode(3), + n4 = new TreeNode(4), + n5 = new TreeNode(5); + // 構建節點之間的引用(指標) + n1.left = n2; + n1.right = n3; + n2.left = n4; + n2.right = n5; + ``` + +=== "TS" + + ```typescript title="binary_tree.ts" + /* 初始化二元樹 */ + // 初始化節點 + let n1 = new TreeNode(1), + n2 = new TreeNode(2), + n3 = new TreeNode(3), + n4 = new TreeNode(4), + n5 = new TreeNode(5); + // 構建節點之間的引用(指標) + n1.left = n2; + n1.right = n3; + n2.left = n4; + n2.right = n5; + ``` + +=== "Dart" + + ```dart title="binary_tree.dart" + /* 初始化二元樹 */ + // 初始化節點 + TreeNode n1 = new TreeNode(1); + TreeNode n2 = new TreeNode(2); + TreeNode n3 = new TreeNode(3); + TreeNode n4 = new TreeNode(4); + TreeNode n5 = new TreeNode(5); + // 構建節點之間的引用(指標) + n1.left = n2; + n1.right = n3; + n2.left = n4; + n2.right = n5; + ``` + +=== "Rust" + + ```rust title="binary_tree.rs" + // 初始化節點 + let n1 = TreeNode::new(1); + let n2 = TreeNode::new(2); + let n3 = TreeNode::new(3); + let n4 = TreeNode::new(4); + let n5 = TreeNode::new(5); + // 構建節點之間的引用(指標) + n1.borrow_mut().left = Some(n2.clone()); + n1.borrow_mut().right = Some(n3); + n2.borrow_mut().left = Some(n4); + n2.borrow_mut().right = Some(n5); + ``` + +=== "C" + + ```c title="binary_tree.c" + /* 初始化二元樹 */ + // 初始化節點 + TreeNode *n1 = newTreeNode(1); + TreeNode *n2 = newTreeNode(2); + TreeNode *n3 = newTreeNode(3); + TreeNode *n4 = newTreeNode(4); + TreeNode *n5 = newTreeNode(5); + // 構建節點之間的引用(指標) + n1->left = n2; + n1->right = n3; + n2->left = n4; + n2->right = n5; + ``` + +=== "Kotlin" + + ```kotlin title="binary_tree.kt" + // 初始化節點 + val n1 = TreeNode(1) + val n2 = TreeNode(2) + val n3 = TreeNode(3) + val n4 = TreeNode(4) + val n5 = TreeNode(5) + // 構建節點之間的引用(指標) + n1.left = n2 + n1.right = n3 + n2.left = n4 + n2.right = n5 + ``` + +=== "Ruby" + + ```ruby title="binary_tree.rb" + + ``` + +=== "Zig" + + ```zig title="binary_tree.zig" + + ``` + +??? pythontutor "視覺化執行" + +
+ + +### 2.   插入與刪除節點 + +與鏈結串列類似,在二元樹中插入與刪除節點可以透過修改指標來實現。圖 7-3 給出了一個示例。 + +![在二元樹中插入與刪除節點](binary_tree.assets/binary_tree_add_remove.png){ class="animation-figure" } + +

圖 7-3   在二元樹中插入與刪除節點

+ +=== "Python" + + ```python title="binary_tree.py" + # 插入與刪除節點 + p = TreeNode(0) + # 在 n1 -> n2 中間插入節點 P + n1.left = p + p.left = n2 + # 刪除節點 P + n1.left = n2 + ``` + +=== "C++" + + ```cpp title="binary_tree.cpp" + /* 插入與刪除節點 */ + TreeNode* P = new TreeNode(0); + // 在 n1 -> n2 中間插入節點 P + n1->left = P; + P->left = n2; + // 刪除節點 P + n1->left = n2; + ``` + +=== "Java" + + ```java title="binary_tree.java" + TreeNode P = new TreeNode(0); + // 在 n1 -> n2 中間插入節點 P + n1.left = P; + P.left = n2; + // 刪除節點 P + n1.left = n2; + ``` + +=== "C#" + + ```csharp title="binary_tree.cs" + /* 插入與刪除節點 */ + TreeNode P = new(0); + // 在 n1 -> n2 中間插入節點 P + n1.left = P; + P.left = n2; + // 刪除節點 P + n1.left = n2; + ``` + +=== "Go" + + ```go title="binary_tree.go" + /* 插入與刪除節點 */ + // 在 n1 -> n2 中間插入節點 P + p := NewTreeNode(0) + n1.Left = p + p.Left = n2 + // 刪除節點 P + n1.Left = n2 + ``` + +=== "Swift" + + ```swift title="binary_tree.swift" + let P = TreeNode(x: 0) + // 在 n1 -> n2 中間插入節點 P + n1.left = P + P.left = n2 + // 刪除節點 P + n1.left = n2 + ``` + +=== "JS" + + ```javascript title="binary_tree.js" + /* 插入與刪除節點 */ + let P = new TreeNode(0); + // 在 n1 -> n2 中間插入節點 P + n1.left = P; + P.left = n2; + // 刪除節點 P + n1.left = n2; + ``` + +=== "TS" + + ```typescript title="binary_tree.ts" + /* 插入與刪除節點 */ + const P = new TreeNode(0); + // 在 n1 -> n2 中間插入節點 P + n1.left = P; + P.left = n2; + // 刪除節點 P + n1.left = n2; + ``` + +=== "Dart" + + ```dart title="binary_tree.dart" + /* 插入與刪除節點 */ + TreeNode P = new TreeNode(0); + // 在 n1 -> n2 中間插入節點 P + n1.left = P; + P.left = n2; + // 刪除節點 P + n1.left = n2; + ``` + +=== "Rust" + + ```rust title="binary_tree.rs" + let p = TreeNode::new(0); + // 在 n1 -> n2 中間插入節點 P + n1.borrow_mut().left = Some(p.clone()); + p.borrow_mut().left = Some(n2.clone()); + // 刪除節點 p + n1.borrow_mut().left = Some(n2); + ``` + +=== "C" + + ```c title="binary_tree.c" + /* 插入與刪除節點 */ + TreeNode *P = newTreeNode(0); + // 在 n1 -> n2 中間插入節點 P + n1->left = P; + P->left = n2; + // 刪除節點 P + n1->left = n2; + ``` + +=== "Kotlin" + + ```kotlin title="binary_tree.kt" + val P = TreeNode(0) + // 在 n1 -> n2 中間插入節點 P + n1.left = P + P.left = n2 + // 刪除節點 P + n1.left = n2 + ``` + +=== "Ruby" + + ```ruby title="binary_tree.rb" + + ``` + +=== "Zig" + + ```zig title="binary_tree.zig" + + ``` + +??? pythontutor "視覺化執行" + +
+ + +!!! note + + 需要注意的是,插入節點可能會改變二元樹的原有邏輯結構,而刪除節點通常意味著刪除該節點及其所有子樹。因此,在二元樹中,插入與刪除通常是由一套操作配合完成的,以實現有實際意義的操作。 + +## 7.1.3   常見二元樹型別 + +### 1.   完美二元樹 + +如圖 7-4 所示,完美二元樹(perfect binary tree)所有層的節點都被完全填滿。在完美二元樹中,葉節點的度為 $0$ ,其餘所有節點的度都為 $2$ ;若樹的高度為 $h$ ,則節點總數為 $2^{h+1} - 1$ ,呈現標準的指數級關係,反映了自然界中常見的細胞分裂現象。 + +!!! tip + + 請注意,在中文社群中,完美二元樹常被稱為滿二元樹。 + +![完美二元樹](binary_tree.assets/perfect_binary_tree.png){ class="animation-figure" } + +

圖 7-4   完美二元樹

+ +### 2.   完全二元樹 + +如圖 7-5 所示,完全二元樹(complete binary tree)只有最底層的節點未被填滿,且最底層節點儘量靠左填充。 + +![完全二元樹](binary_tree.assets/complete_binary_tree.png){ class="animation-figure" } + +

圖 7-5   完全二元樹

+ +### 3.   完滿二元樹 + +如圖 7-6 所示,完滿二元樹(full binary tree)除了葉節點之外,其餘所有節點都有兩個子節點。 + +![完滿二元樹](binary_tree.assets/full_binary_tree.png){ class="animation-figure" } + +

圖 7-6   完滿二元樹

+ +### 4.   平衡二元樹 + +如圖 7-7 所示,平衡二元樹(balanced binary tree)中任意節點的左子樹和右子樹的高度之差的絕對值不超過 1 。 + +![平衡二元樹](binary_tree.assets/balanced_binary_tree.png){ class="animation-figure" } + +

圖 7-7   平衡二元樹

+ +## 7.1.4   二元樹的退化 + +圖 7-8 展示了二元樹的理想結構與退化結構。當二元樹的每層節點都被填滿時,達到“完美二元樹”;而當所有節點都偏向一側時,二元樹退化為“鏈結串列”。 + +- 完美二元樹是理想情況,可以充分發揮二元樹“分治”的優勢。 +- 鏈結串列則是另一個極端,各項操作都變為線性操作,時間複雜度退化至 $O(n)$ 。 + +![二元樹的最佳結構與最差結構](binary_tree.assets/binary_tree_best_worst_cases.png){ class="animation-figure" } + +

圖 7-8   二元樹的最佳結構與最差結構

+ +如表 7-1 所示,在最佳結構和最差結構下,二元樹的葉節點數量、節點總數、高度等達到極大值或極小值。 + +

表 7-1   二元樹的最佳結構與最差結構

+ +
+ +| | 完美二元樹 | 鏈結串列 | +| --------------------------- | ------------------ | ------- | +| 第 $i$ 層的節點數量 | $2^{i-1}$ | $1$ | +| 高度為 $h$ 的樹的葉節點數量 | $2^h$ | $1$ | +| 高度為 $h$ 的樹的節點總數 | $2^{h+1} - 1$ | $h + 1$ | +| 節點總數為 $n$ 的樹的高度 | $\log_2 (n+1) - 1$ | $n - 1$ | + +
diff --git a/zh-Hant/docs/chapter_tree/binary_tree_traversal.md b/zh-Hant/docs/chapter_tree/binary_tree_traversal.md new file mode 100755 index 000000000..ca2cf11ad --- /dev/null +++ b/zh-Hant/docs/chapter_tree/binary_tree_traversal.md @@ -0,0 +1,883 @@ +--- +comments: true +--- + +# 7.2   二元樹走訪 + +從物理結構的角度來看,樹是一種基於鏈結串列的資料結構,因此其走訪方式是透過指標逐個訪問節點。然而,樹是一種非線性資料結構,這使得走訪樹比走訪鏈結串列更加複雜,需要藉助搜尋演算法來實現。 + +二元樹常見的走訪方式包括層序走訪、前序走訪、中序走訪和後序走訪等。 + +## 7.2.1   層序走訪 + +如圖 7-9 所示,層序走訪(level-order traversal)從頂部到底部逐層走訪二元樹,並在每一層按照從左到右的順序訪問節點。 + +層序走訪本質上屬於廣度優先走訪(breadth-first traversal),也稱廣度優先搜尋(breadth-first search, BFS),它體現了一種“一圈一圈向外擴展”的逐層走訪方式。 + +![二元樹的層序走訪](binary_tree_traversal.assets/binary_tree_bfs.png){ class="animation-figure" } + +

圖 7-9   二元樹的層序走訪

+ +### 1.   程式碼實現 + +廣度優先走訪通常藉助“佇列”來實現。佇列遵循“先進先出”的規則,而廣度優先走訪則遵循“逐層推進”的規則,兩者背後的思想是一致的。實現程式碼如下: + +=== "Python" + + ```python title="binary_tree_bfs.py" + def level_order(root: TreeNode | None) -> list[int]: + """層序走訪""" + # 初始化佇列,加入根節點 + queue: deque[TreeNode] = deque() + queue.append(root) + # 初始化一個串列,用於儲存走訪序列 + res = [] + while queue: + node: TreeNode = queue.popleft() # 隊列出隊 + res.append(node.val) # 儲存節點值 + if node.left is not None: + queue.append(node.left) # 左子節點入列 + if node.right is not None: + queue.append(node.right) # 右子節點入列 + return res + ``` + +=== "C++" + + ```cpp title="binary_tree_bfs.cpp" + /* 層序走訪 */ + vector levelOrder(TreeNode *root) { + // 初始化佇列,加入根節點 + queue queue; + queue.push(root); + // 初始化一個串列,用於儲存走訪序列 + vector vec; + while (!queue.empty()) { + TreeNode *node = queue.front(); + queue.pop(); // 隊列出隊 + vec.push_back(node->val); // 儲存節點值 + if (node->left != nullptr) + queue.push(node->left); // 左子節點入列 + if (node->right != nullptr) + queue.push(node->right); // 右子節點入列 + } + return vec; + } + ``` + +=== "Java" + + ```java title="binary_tree_bfs.java" + /* 層序走訪 */ + List levelOrder(TreeNode root) { + // 初始化佇列,加入根節點 + Queue queue = new LinkedList<>(); + queue.add(root); + // 初始化一個串列,用於儲存走訪序列 + List list = new ArrayList<>(); + while (!queue.isEmpty()) { + TreeNode node = queue.poll(); // 隊列出隊 + list.add(node.val); // 儲存節點值 + if (node.left != null) + queue.offer(node.left); // 左子節點入列 + if (node.right != null) + queue.offer(node.right); // 右子節點入列 + } + return list; + } + ``` + +=== "C#" + + ```csharp title="binary_tree_bfs.cs" + /* 層序走訪 */ + List LevelOrder(TreeNode root) { + // 初始化佇列,加入根節點 + Queue queue = new(); + queue.Enqueue(root); + // 初始化一個串列,用於儲存走訪序列 + List list = []; + while (queue.Count != 0) { + TreeNode node = queue.Dequeue(); // 隊列出隊 + list.Add(node.val!.Value); // 儲存節點值 + if (node.left != null) + queue.Enqueue(node.left); // 左子節點入列 + if (node.right != null) + queue.Enqueue(node.right); // 右子節點入列 + } + return list; + } + ``` + +=== "Go" + + ```go title="binary_tree_bfs.go" + /* 層序走訪 */ + func levelOrder(root *TreeNode) []any { + // 初始化佇列,加入根節點 + queue := list.New() + queue.PushBack(root) + // 初始化一個切片,用於儲存走訪序列 + nums := make([]any, 0) + for queue.Len() > 0 { + // 隊列出隊 + node := queue.Remove(queue.Front()).(*TreeNode) + // 儲存節點值 + nums = append(nums, node.Val) + if node.Left != nil { + // 左子節點入列 + queue.PushBack(node.Left) + } + if node.Right != nil { + // 右子節點入列 + queue.PushBack(node.Right) + } + } + return nums + } + ``` + +=== "Swift" + + ```swift title="binary_tree_bfs.swift" + /* 層序走訪 */ + func levelOrder(root: TreeNode) -> [Int] { + // 初始化佇列,加入根節點 + var queue: [TreeNode] = [root] + // 初始化一個串列,用於儲存走訪序列 + var list: [Int] = [] + while !queue.isEmpty { + let node = queue.removeFirst() // 隊列出隊 + list.append(node.val) // 儲存節點值 + if let left = node.left { + queue.append(left) // 左子節點入列 + } + if let right = node.right { + queue.append(right) // 右子節點入列 + } + } + return list + } + ``` + +=== "JS" + + ```javascript title="binary_tree_bfs.js" + /* 層序走訪 */ + function levelOrder(root) { + // 初始化佇列,加入根節點 + const queue = [root]; + // 初始化一個串列,用於儲存走訪序列 + const list = []; + while (queue.length) { + let node = queue.shift(); // 隊列出隊 + list.push(node.val); // 儲存節點值 + if (node.left) queue.push(node.left); // 左子節點入列 + if (node.right) queue.push(node.right); // 右子節點入列 + } + return list; + } + ``` + +=== "TS" + + ```typescript title="binary_tree_bfs.ts" + /* 層序走訪 */ + function levelOrder(root: TreeNode | null): number[] { + // 初始化佇列,加入根節點 + const queue = [root]; + // 初始化一個串列,用於儲存走訪序列 + const list: number[] = []; + while (queue.length) { + let node = queue.shift() as TreeNode; // 隊列出隊 + list.push(node.val); // 儲存節點值 + if (node.left) { + queue.push(node.left); // 左子節點入列 + } + if (node.right) { + queue.push(node.right); // 右子節點入列 + } + } + return list; + } + ``` + +=== "Dart" + + ```dart title="binary_tree_bfs.dart" + /* 層序走訪 */ + List levelOrder(TreeNode? root) { + // 初始化佇列,加入根節點 + Queue queue = Queue(); + queue.add(root); + // 初始化一個串列,用於儲存走訪序列 + List res = []; + while (queue.isNotEmpty) { + TreeNode? node = queue.removeFirst(); // 隊列出隊 + res.add(node!.val); // 儲存節點值 + if (node.left != null) queue.add(node.left); // 左子節點入列 + if (node.right != null) queue.add(node.right); // 右子節點入列 + } + return res; + } + ``` + +=== "Rust" + + ```rust title="binary_tree_bfs.rs" + /* 層序走訪 */ + fn level_order(root: &Rc>) -> Vec { + // 初始化佇列,加入根節點 + let mut que = VecDeque::new(); + que.push_back(Rc::clone(&root)); + // 初始化一個串列,用於儲存走訪序列 + let mut vec = Vec::new(); + + while let Some(node) = que.pop_front() { + // 隊列出隊 + vec.push(node.borrow().val); // 儲存節點值 + if let Some(left) = node.borrow().left.as_ref() { + que.push_back(Rc::clone(left)); // 左子節點入列 + } + if let Some(right) = node.borrow().right.as_ref() { + que.push_back(Rc::clone(right)); // 右子節點入列 + }; + } + vec + } + ``` + +=== "C" + + ```c title="binary_tree_bfs.c" + /* 層序走訪 */ + int *levelOrder(TreeNode *root, int *size) { + /* 輔助佇列 */ + int front, rear; + int index, *arr; + TreeNode *node; + TreeNode **queue; + + /* 輔助佇列 */ + queue = (TreeNode **)malloc(sizeof(TreeNode *) * MAX_SIZE); + // 佇列指標 + front = 0, rear = 0; + // 加入根節點 + queue[rear++] = root; + // 初始化一個串列,用於儲存走訪序列 + /* 輔助陣列 */ + arr = (int *)malloc(sizeof(int) * MAX_SIZE); + // 陣列指標 + index = 0; + while (front < rear) { + // 隊列出隊 + node = queue[front++]; + // 儲存節點值 + arr[index++] = node->val; + if (node->left != NULL) { + // 左子節點入列 + queue[rear++] = node->left; + } + if (node->right != NULL) { + // 右子節點入列 + queue[rear++] = node->right; + } + } + // 更新陣列長度的值 + *size = index; + arr = realloc(arr, sizeof(int) * (*size)); + + // 釋放輔助陣列空間 + free(queue); + return arr; + } + ``` + +=== "Kotlin" + + ```kotlin title="binary_tree_bfs.kt" + /* 層序走訪 */ + fun levelOrder(root: TreeNode?): MutableList { + // 初始化佇列,加入根節點 + val queue = LinkedList() + queue.add(root) + // 初始化一個串列,用於儲存走訪序列 + val list = ArrayList() + while (!queue.isEmpty()) { + val node = queue.poll() // 隊列出隊 + list.add(node?.value!!) // 儲存節點值 + if (node.left != null) queue.offer(node.left) // 左子節點入列 + + if (node.right != null) queue.offer(node.right) // 右子節點入列 + } + return list + } + ``` + +=== "Ruby" + + ```ruby title="binary_tree_bfs.rb" + [class]{}-[func]{level_order} + ``` + +=== "Zig" + + ```zig title="binary_tree_bfs.zig" + // 層序走訪 + fn levelOrder(comptime T: type, mem_allocator: std.mem.Allocator, root: *inc.TreeNode(T)) !std.ArrayList(T) { + // 初始化佇列,加入根節點 + const L = std.TailQueue(*inc.TreeNode(T)); + var queue = L{}; + var root_node = try mem_allocator.create(L.Node); + root_node.data = root; + queue.append(root_node); + // 初始化一個串列,用於儲存走訪序列 + var list = std.ArrayList(T).init(std.heap.page_allocator); + while (queue.len > 0) { + var queue_node = queue.popFirst().?; // 隊列出隊 + var node = queue_node.data; + try list.append(node.val); // 儲存節點值 + if (node.left != null) { + var tmp_node = try mem_allocator.create(L.Node); + tmp_node.data = node.left.?; + queue.append(tmp_node); // 左子節點入列 + } + if (node.right != null) { + var tmp_node = try mem_allocator.create(L.Node); + tmp_node.data = node.right.?; + queue.append(tmp_node); // 右子節點入列 + } + } + return list; + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +### 2.   複雜度分析 + +- **時間複雜度為 $O(n)$** :所有節點被訪問一次,使用 $O(n)$ 時間,其中 $n$ 為節點數量。 +- **空間複雜度為 $O(n)$** :在最差情況下,即滿二元樹時,走訪到最底層之前,佇列中最多同時存在 $(n + 1) / 2$ 個節點,佔用 $O(n)$ 空間。 + +## 7.2.2   前序、中序、後序走訪 + +相應地,前序、中序和後序走訪都屬於深度優先走訪(depth-first traversal),也稱深度優先搜尋(depth-first search, DFS),它體現了一種“先走到盡頭,再回溯繼續”的走訪方式。 + +圖 7-10 展示了對二元樹進行深度優先走訪的工作原理。**深度優先走訪就像是繞著整棵二元樹的外圍“走”一圈**,在每個節點都會遇到三個位置,分別對應前序走訪、中序走訪和後序走訪。 + +![二元搜尋樹的前序、中序、後序走訪](binary_tree_traversal.assets/binary_tree_dfs.png){ class="animation-figure" } + +

圖 7-10   二元搜尋樹的前序、中序、後序走訪

+ +### 1.   程式碼實現 + +深度優先搜尋通常基於遞迴實現: + +=== "Python" + + ```python title="binary_tree_dfs.py" + def pre_order(root: TreeNode | None): + """前序走訪""" + if root is None: + return + # 訪問優先順序:根節點 -> 左子樹 -> 右子樹 + res.append(root.val) + pre_order(root=root.left) + pre_order(root=root.right) + + def in_order(root: TreeNode | None): + """中序走訪""" + if root is None: + return + # 訪問優先順序:左子樹 -> 根節點 -> 右子樹 + in_order(root=root.left) + res.append(root.val) + in_order(root=root.right) + + def post_order(root: TreeNode | None): + """後序走訪""" + if root is None: + return + # 訪問優先順序:左子樹 -> 右子樹 -> 根節點 + post_order(root=root.left) + post_order(root=root.right) + res.append(root.val) + ``` + +=== "C++" + + ```cpp title="binary_tree_dfs.cpp" + /* 前序走訪 */ + void preOrder(TreeNode *root) { + if (root == nullptr) + return; + // 訪問優先順序:根節點 -> 左子樹 -> 右子樹 + vec.push_back(root->val); + preOrder(root->left); + preOrder(root->right); + } + + /* 中序走訪 */ + void inOrder(TreeNode *root) { + if (root == nullptr) + return; + // 訪問優先順序:左子樹 -> 根節點 -> 右子樹 + inOrder(root->left); + vec.push_back(root->val); + inOrder(root->right); + } + + /* 後序走訪 */ + void postOrder(TreeNode *root) { + if (root == nullptr) + return; + // 訪問優先順序:左子樹 -> 右子樹 -> 根節點 + postOrder(root->left); + postOrder(root->right); + vec.push_back(root->val); + } + ``` + +=== "Java" + + ```java title="binary_tree_dfs.java" + /* 前序走訪 */ + void preOrder(TreeNode root) { + if (root == null) + return; + // 訪問優先順序:根節點 -> 左子樹 -> 右子樹 + list.add(root.val); + preOrder(root.left); + preOrder(root.right); + } + + /* 中序走訪 */ + void inOrder(TreeNode root) { + if (root == null) + return; + // 訪問優先順序:左子樹 -> 根節點 -> 右子樹 + inOrder(root.left); + list.add(root.val); + inOrder(root.right); + } + + /* 後序走訪 */ + void postOrder(TreeNode root) { + if (root == null) + return; + // 訪問優先順序:左子樹 -> 右子樹 -> 根節點 + postOrder(root.left); + postOrder(root.right); + list.add(root.val); + } + ``` + +=== "C#" + + ```csharp title="binary_tree_dfs.cs" + /* 前序走訪 */ + void PreOrder(TreeNode? root) { + if (root == null) return; + // 訪問優先順序:根節點 -> 左子樹 -> 右子樹 + list.Add(root.val!.Value); + PreOrder(root.left); + PreOrder(root.right); + } + + /* 中序走訪 */ + void InOrder(TreeNode? root) { + if (root == null) return; + // 訪問優先順序:左子樹 -> 根節點 -> 右子樹 + InOrder(root.left); + list.Add(root.val!.Value); + InOrder(root.right); + } + + /* 後序走訪 */ + void PostOrder(TreeNode? root) { + if (root == null) return; + // 訪問優先順序:左子樹 -> 右子樹 -> 根節點 + PostOrder(root.left); + PostOrder(root.right); + list.Add(root.val!.Value); + } + ``` + +=== "Go" + + ```go title="binary_tree_dfs.go" + /* 前序走訪 */ + func preOrder(node *TreeNode) { + if node == nil { + return + } + // 訪問優先順序:根節點 -> 左子樹 -> 右子樹 + nums = append(nums, node.Val) + preOrder(node.Left) + preOrder(node.Right) + } + + /* 中序走訪 */ + func inOrder(node *TreeNode) { + if node == nil { + return + } + // 訪問優先順序:左子樹 -> 根節點 -> 右子樹 + inOrder(node.Left) + nums = append(nums, node.Val) + inOrder(node.Right) + } + + /* 後序走訪 */ + func postOrder(node *TreeNode) { + if node == nil { + return + } + // 訪問優先順序:左子樹 -> 右子樹 -> 根節點 + postOrder(node.Left) + postOrder(node.Right) + nums = append(nums, node.Val) + } + ``` + +=== "Swift" + + ```swift title="binary_tree_dfs.swift" + /* 前序走訪 */ + func preOrder(root: TreeNode?) { + guard let root = root else { + return + } + // 訪問優先順序:根節點 -> 左子樹 -> 右子樹 + list.append(root.val) + preOrder(root: root.left) + preOrder(root: root.right) + } + + /* 中序走訪 */ + func inOrder(root: TreeNode?) { + guard let root = root else { + return + } + // 訪問優先順序:左子樹 -> 根節點 -> 右子樹 + inOrder(root: root.left) + list.append(root.val) + inOrder(root: root.right) + } + + /* 後序走訪 */ + func postOrder(root: TreeNode?) { + guard let root = root else { + return + } + // 訪問優先順序:左子樹 -> 右子樹 -> 根節點 + postOrder(root: root.left) + postOrder(root: root.right) + list.append(root.val) + } + ``` + +=== "JS" + + ```javascript title="binary_tree_dfs.js" + /* 前序走訪 */ + function preOrder(root) { + if (root === null) return; + // 訪問優先順序:根節點 -> 左子樹 -> 右子樹 + list.push(root.val); + preOrder(root.left); + preOrder(root.right); + } + + /* 中序走訪 */ + function inOrder(root) { + if (root === null) return; + // 訪問優先順序:左子樹 -> 根節點 -> 右子樹 + inOrder(root.left); + list.push(root.val); + inOrder(root.right); + } + + /* 後序走訪 */ + function postOrder(root) { + if (root === null) return; + // 訪問優先順序:左子樹 -> 右子樹 -> 根節點 + postOrder(root.left); + postOrder(root.right); + list.push(root.val); + } + ``` + +=== "TS" + + ```typescript title="binary_tree_dfs.ts" + /* 前序走訪 */ + function preOrder(root: TreeNode | null): void { + if (root === null) { + return; + } + // 訪問優先順序:根節點 -> 左子樹 -> 右子樹 + list.push(root.val); + preOrder(root.left); + preOrder(root.right); + } + + /* 中序走訪 */ + function inOrder(root: TreeNode | null): void { + if (root === null) { + return; + } + // 訪問優先順序:左子樹 -> 根節點 -> 右子樹 + inOrder(root.left); + list.push(root.val); + inOrder(root.right); + } + + /* 後序走訪 */ + function postOrder(root: TreeNode | null): void { + if (root === null) { + return; + } + // 訪問優先順序:左子樹 -> 右子樹 -> 根節點 + postOrder(root.left); + postOrder(root.right); + list.push(root.val); + } + ``` + +=== "Dart" + + ```dart title="binary_tree_dfs.dart" + /* 前序走訪 */ + void preOrder(TreeNode? node) { + if (node == null) return; + // 訪問優先順序:根節點 -> 左子樹 -> 右子樹 + list.add(node.val); + preOrder(node.left); + preOrder(node.right); + } + + /* 中序走訪 */ + void inOrder(TreeNode? node) { + if (node == null) return; + // 訪問優先順序:左子樹 -> 根節點 -> 右子樹 + inOrder(node.left); + list.add(node.val); + inOrder(node.right); + } + + /* 後序走訪 */ + void postOrder(TreeNode? node) { + if (node == null) return; + // 訪問優先順序:左子樹 -> 右子樹 -> 根節點 + postOrder(node.left); + postOrder(node.right); + list.add(node.val); + } + ``` + +=== "Rust" + + ```rust title="binary_tree_dfs.rs" + /* 前序走訪 */ + fn pre_order(root: Option<&Rc>>) -> Vec { + let mut result = vec![]; + + if let Some(node) = root { + // 訪問優先順序:根節點 -> 左子樹 -> 右子樹 + result.push(node.borrow().val); + result.append(&mut pre_order(node.borrow().left.as_ref())); + result.append(&mut pre_order(node.borrow().right.as_ref())); + } + result + } + + /* 中序走訪 */ + fn in_order(root: Option<&Rc>>) -> Vec { + let mut result = vec![]; + + if let Some(node) = root { + // 訪問優先順序:左子樹 -> 根節點 -> 右子樹 + result.append(&mut in_order(node.borrow().left.as_ref())); + result.push(node.borrow().val); + result.append(&mut in_order(node.borrow().right.as_ref())); + } + result + } + + /* 後序走訪 */ + fn post_order(root: Option<&Rc>>) -> Vec { + let mut result = vec![]; + + if let Some(node) = root { + // 訪問優先順序:左子樹 -> 右子樹 -> 根節點 + result.append(&mut post_order(node.borrow().left.as_ref())); + result.append(&mut post_order(node.borrow().right.as_ref())); + result.push(node.borrow().val); + } + result + } + ``` + +=== "C" + + ```c title="binary_tree_dfs.c" + /* 前序走訪 */ + void preOrder(TreeNode *root, int *size) { + if (root == NULL) + return; + // 訪問優先順序:根節點 -> 左子樹 -> 右子樹 + arr[(*size)++] = root->val; + preOrder(root->left, size); + preOrder(root->right, size); + } + + /* 中序走訪 */ + void inOrder(TreeNode *root, int *size) { + if (root == NULL) + return; + // 訪問優先順序:左子樹 -> 根節點 -> 右子樹 + inOrder(root->left, size); + arr[(*size)++] = root->val; + inOrder(root->right, size); + } + + /* 後序走訪 */ + void postOrder(TreeNode *root, int *size) { + if (root == NULL) + return; + // 訪問優先順序:左子樹 -> 右子樹 -> 根節點 + postOrder(root->left, size); + postOrder(root->right, size); + arr[(*size)++] = root->val; + } + ``` + +=== "Kotlin" + + ```kotlin title="binary_tree_dfs.kt" + /* 前序走訪 */ + fun preOrder(root: TreeNode?) { + if (root == null) return + // 訪問優先順序:根節點 -> 左子樹 -> 右子樹 + list.add(root.value) + preOrder(root.left) + preOrder(root.right) + } + + /* 中序走訪 */ + fun inOrder(root: TreeNode?) { + if (root == null) return + // 訪問優先順序:左子樹 -> 根節點 -> 右子樹 + inOrder(root.left) + list.add(root.value) + inOrder(root.right) + } + + /* 後序走訪 */ + fun postOrder(root: TreeNode?) { + if (root == null) return + // 訪問優先順序:左子樹 -> 右子樹 -> 根節點 + postOrder(root.left) + postOrder(root.right) + list.add(root.value) + } + ``` + +=== "Ruby" + + ```ruby title="binary_tree_dfs.rb" + [class]{}-[func]{pre_order} + + [class]{}-[func]{in_order} + + [class]{}-[func]{post_order} + ``` + +=== "Zig" + + ```zig title="binary_tree_dfs.zig" + // 前序走訪 + fn preOrder(comptime T: type, root: ?*inc.TreeNode(T)) !void { + if (root == null) return; + // 訪問優先順序:根節點 -> 左子樹 -> 右子樹 + try list.append(root.?.val); + try preOrder(T, root.?.left); + try preOrder(T, root.?.right); + } + + // 中序走訪 + fn inOrder(comptime T: type, root: ?*inc.TreeNode(T)) !void { + if (root == null) return; + // 訪問優先順序:左子樹 -> 根節點 -> 右子樹 + try inOrder(T, root.?.left); + try list.append(root.?.val); + try inOrder(T, root.?.right); + } + + // 後序走訪 + fn postOrder(comptime T: type, root: ?*inc.TreeNode(T)) !void { + if (root == null) return; + // 訪問優先順序:左子樹 -> 右子樹 -> 根節點 + try postOrder(T, root.?.left); + try postOrder(T, root.?.right); + try list.append(root.?.val); + } + ``` + +??? pythontutor "視覺化執行" + +
+ + +!!! tip + + 深度優先搜尋也可以基於迭代實現,有興趣的讀者可以自行研究。 + +圖 7-11 展示了前序走訪二元樹的遞迴過程,其可分為“遞”和“迴”兩個逆向的部分。 + +1. “遞”表示開啟新方法,程式在此過程中訪問下一個節點。 +2. “迴”表示函式返回,代表當前節點已經訪問完畢。 + +=== "<1>" + ![前序走訪的遞迴過程](binary_tree_traversal.assets/preorder_step1.png){ class="animation-figure" } + +=== "<2>" + ![preorder_step2](binary_tree_traversal.assets/preorder_step2.png){ class="animation-figure" } + +=== "<3>" + ![preorder_step3](binary_tree_traversal.assets/preorder_step3.png){ class="animation-figure" } + +=== "<4>" + ![preorder_step4](binary_tree_traversal.assets/preorder_step4.png){ class="animation-figure" } + +=== "<5>" + ![preorder_step5](binary_tree_traversal.assets/preorder_step5.png){ class="animation-figure" } + +=== "<6>" + ![preorder_step6](binary_tree_traversal.assets/preorder_step6.png){ class="animation-figure" } + +=== "<7>" + ![preorder_step7](binary_tree_traversal.assets/preorder_step7.png){ class="animation-figure" } + +=== "<8>" + ![preorder_step8](binary_tree_traversal.assets/preorder_step8.png){ class="animation-figure" } + +=== "<9>" + ![preorder_step9](binary_tree_traversal.assets/preorder_step9.png){ class="animation-figure" } + +=== "<10>" + ![preorder_step10](binary_tree_traversal.assets/preorder_step10.png){ class="animation-figure" } + +=== "<11>" + ![preorder_step11](binary_tree_traversal.assets/preorder_step11.png){ class="animation-figure" } + +

圖 7-11   前序走訪的遞迴過程

+ +### 2.   複雜度分析 + +- **時間複雜度為 $O(n)$** :所有節點被訪問一次,使用 $O(n)$ 時間。 +- **空間複雜度為 $O(n)$** :在最差情況下,即樹退化為鏈結串列時,遞迴深度達到 $n$ ,系統佔用 $O(n)$ 堆疊幀空間。 diff --git a/zh-Hant/docs/chapter_tree/index.md b/zh-Hant/docs/chapter_tree/index.md new file mode 100644 index 000000000..c23d03749 --- /dev/null +++ b/zh-Hant/docs/chapter_tree/index.md @@ -0,0 +1,23 @@ +--- +comments: true +icon: material/graph-outline +--- + +# 第 7 章   樹 + +![樹](../assets/covers/chapter_tree.jpg){ class="cover-image" } + +!!! abstract + + 參天大樹充滿生命力,根深葉茂,分枝扶疏。 + + 它為我們展現了資料分治的生動形態。 + +## Chapter Contents + +- [7.1   二元樹](https://www.hello-algo.com/en/chapter_tree/binary_tree/) +- [7.2   二元樹走訪](https://www.hello-algo.com/en/chapter_tree/binary_tree_traversal/) +- [7.3   二元樹陣列表示](https://www.hello-algo.com/en/chapter_tree/array_representation_of_tree/) +- [7.4   二元搜尋樹](https://www.hello-algo.com/en/chapter_tree/binary_search_tree/) +- [7.5   AVL *](https://www.hello-algo.com/en/chapter_tree/avl_tree/) +- [7.6   小結](https://www.hello-algo.com/en/chapter_tree/summary/) diff --git a/zh-Hant/docs/chapter_tree/summary.md b/zh-Hant/docs/chapter_tree/summary.md new file mode 100644 index 000000000..d6b3914de --- /dev/null +++ b/zh-Hant/docs/chapter_tree/summary.md @@ -0,0 +1,58 @@ +--- +comments: true +--- + +# 7.6   小結 + +### 1.   重點回顧 + +- 二元樹是一種非線性資料結構,體現“一分為二”的分治邏輯。每個二元樹節點包含一個值以及兩個指標,分別指向其左子節點和右子節點。 +- 對於二元樹中的某個節點,其左(右)子節點及其以下形成的樹被稱為該節點的左(右)子樹。 +- 二元樹的相關術語包括根節點、葉節點、層、度、邊、高度和深度等。 +- 二元樹的初始化、節點插入和節點刪除操作與鏈結串列操作方法類似。 +- 常見的二元樹型別有完美二元樹、完全二元樹、完滿二元樹和平衡二元樹。完美二元樹是最理想的狀態,而鏈結串列是退化後的最差狀態。 +- 二元樹可以用陣列表示,方法是將節點值和空位按層序走訪順序排列,並根據父節點與子節點之間的索引對映關係來實現指標。 +- 二元樹的層序走訪是一種廣度優先搜尋方法,它體現了“一圈一圈向外擴展”的逐層走訪方式,通常透過佇列來實現。 +- 前序、中序、後序走訪皆屬於深度優先搜尋,它們體現了“先走到盡頭,再回溯繼續”的走訪方式,通常使用遞迴來實現。 +- 二元搜尋樹是一種高效的元素查詢資料結構,其查詢、插入和刪除操作的時間複雜度均為 $O(\log n)$ 。當二元搜尋樹退化為鏈結串列時,各項時間複雜度會劣化至 $O(n)$ 。 +- AVL 樹,也稱平衡二元搜尋樹,它透過旋轉操作確保在不斷插入和刪除節點後樹仍然保持平衡。 +- AVL 樹的旋轉操作包括右旋、左旋、先右旋再左旋、先左旋再右旋。在插入或刪除節點後,AVL 樹會從底向頂執行旋轉操作,使樹重新恢復平衡。 + +### 2.   Q & A + +**Q**:對於只有一個節點的二元樹,樹的高度和根節點的深度都是 $0$ 嗎? + +是的,因為高度和深度通常定義為“經過的邊的數量”。 + +**Q**:二元樹中的插入與刪除一般由一套操作配合完成,這裡的“一套操作”指什麼呢?可以理解為資源的子節點的資源釋放嗎? + +拿二元搜尋樹來舉例,刪除節點操作要分三種情況處理,其中每種情況都需要進行多個步驟的節點操作。 + +**Q**:為什麼 DFS 走訪二元樹有前、中、後三種順序,分別有什麼用呢? + +與順序和逆序走訪陣列類似,前序、中序、後序走訪是三種二元樹走訪方法,我們可以使用它們得到一個特定順序的走訪結果。例如在二元搜尋樹中,由於節點大小滿足 `左子節點值 < 根節點值 < 右子節點值` ,因此我們只要按照“左 $\rightarrow$ 根 $\rightarrow$ 右”的優先順序走訪樹,就可以獲得有序的節點序列。 + +**Q**:右旋操作是處理失衡節點 `node`、`child`、`grand_child` 之間的關係,那 `node` 的父節點和 `node` 原來的連線不需要維護嗎?右旋操作後豈不是斷掉了? + +我們需要從遞迴的視角來看這個問題。右旋操作 `right_rotate(root)` 傳入的是子樹的根節點,最終 `return child` 返回旋轉之後的子樹的根節點。子樹的根節點和其父節點的連線是在該函式返回後完成的,不屬於右旋操作的維護範圍。 + +**Q**:在 C++ 中,函式被劃分到 `private` 和 `public` 中,這方面有什麼考量嗎?為什麼要將 `height()` 函式和 `updateHeight()` 函式分別放在 `public` 和 `private` 中呢? + +主要看方法的使用範圍,如果方法只在類別內部使用,那麼就設計為 `private` 。例如,使用者單獨呼叫 `updateHeight()` 是沒有意義的,它只是插入、刪除操作中的一步。而 `height()` 是訪問節點高度,類似於 `vector.size()` ,因此設定成 `public` 以便使用。 + +**Q**:如何從一組輸入資料構建一棵二元搜尋樹?根節點的選擇是不是很重要? + +是的,構建樹的方法已在二元搜尋樹程式碼中的 `build_tree()` 方法中給出。至於根節點的選擇,我們通常會將輸入資料排序,然後將中點元素作為根節點,再遞迴地構建左右子樹。這樣做可以最大程度保證樹的平衡性。 + +**Q**:在 Java 中,字串對比是否一定要用 `equals()` 方法? + +在 Java 中,對於基本資料型別,`==` 用於對比兩個變數的值是否相等。對於引用型別,兩種符號的工作原理是不同的。 + +- `==` :用來比較兩個變數是否指向同一個物件,即它們在記憶體中的位置是否相同。 +- `equals()`:用來對比兩個物件的值是否相等。 + +因此,如果要對比值,我們應該使用 `equals()` 。然而,透過 `String a = "hi"; String b = "hi";` 初始化的字串都儲存在字串常數池中,它們指向同一個物件,因此也可以用 `a == b` 來比較兩個字串的內容。 + +**Q**:廣度優先走訪到最底層之前,佇列中的節點數量是 $2^h$ 嗎? + +是的,例如高度 $h = 2$ 的滿二元樹,其節點總數 $n = 7$ ,則底層節點數量 $4 = 2^h = (n + 1) / 2$ 。 diff --git a/zh-Hant/docs/index.assets/btn_download_pdf.svg b/zh-Hant/docs/index.assets/btn_download_pdf.svg new file mode 100644 index 000000000..2d52ece69 --- /dev/null +++ b/zh-Hant/docs/index.assets/btn_download_pdf.svg @@ -0,0 +1 @@ +下載PDF \ No newline at end of file diff --git a/zh-Hant/docs/index.assets/btn_download_pdf_dark.svg b/zh-Hant/docs/index.assets/btn_download_pdf_dark.svg new file mode 100644 index 000000000..71ae42ec0 --- /dev/null +++ b/zh-Hant/docs/index.assets/btn_download_pdf_dark.svg @@ -0,0 +1 @@ +下載PDF \ No newline at end of file diff --git a/zh-Hant/docs/index.assets/btn_read_online.svg b/zh-Hant/docs/index.assets/btn_read_online.svg new file mode 100644 index 000000000..7ff5c3f26 --- /dev/null +++ b/zh-Hant/docs/index.assets/btn_read_online.svg @@ -0,0 +1 @@ +線上閱讀 \ No newline at end of file diff --git a/zh-Hant/docs/index.assets/btn_read_online_dark.svg b/zh-Hant/docs/index.assets/btn_read_online_dark.svg new file mode 100644 index 000000000..4bc3134b5 --- /dev/null +++ b/zh-Hant/docs/index.assets/btn_read_online_dark.svg @@ -0,0 +1 @@ +線上閱讀 \ No newline at end of file diff --git a/zh-Hant/docs/index.html b/zh-Hant/docs/index.html new file mode 100644 index 000000000..01c3b858a --- /dev/null +++ b/zh-Hant/docs/index.html @@ -0,0 +1,357 @@ + +
+ + + + + +
+ +
+

+ 動畫圖解、一鍵執行的資料結構與演算法教程 +

+ + + + + 開始閱讀 + + + + + + + 程式碼倉庫 + +
+ +
+ + + +
+
+
+ + +
+
+ Preview +
+ + + + + + + + + + + + +
+

500 幅動畫圖解、12 種程式語言程式碼、3000 條社群問答,助你快速入門資料結構與演算法

+
+
+ + +
+ +
+ + +
+
+

推薦語

+
+
+

“一本通俗易懂的資料結構與演算法入門書,引導讀者手腦並用地學習,強烈推薦演算法初學者閱讀。”

+

—— 鄧俊輝,清華大學計算機系教授

+
+
+

“如果我當年學資料結構與演算法的時候有《Hello 演算法》,學起來應該會簡單 10 倍!”

+

—— 李沐,亞馬遜資深首席科學家

+
+
+
+
+ + +
+
+
+
+
+
+ + + +

動畫圖解

+
+

內容清晰易懂,學習曲線平滑

+

"A picture is worth a thousand words."
“一圖勝千言”

+
+
+ Animation example +
+ +
+ Running code example +
+
+
+ + + +

一鍵執行

+
+

十餘種程式語言,程式碼視覺化執行

+

"Talk is cheap. Show me the code."
“少吹牛,看程式碼”

+
+
+
+ +
+
+
+
+ + + +

互助學習

+
+

歡迎討論與提問,讀者間攜手共進

+

"Learning by teaching."
“教學相長”

+
+
+ Comments example +
+ +
+
+ + +
+
+ +
+

作者

+ +
+ + + + + +
+

貢獻者

+

本書在開源社群 140 多位貢獻者的共同努力下不斷完善,感謝他們付出的時間與精力!

+ + Contributors + +
+
+
\ No newline at end of file diff --git a/zh-Hant/docs/index.md b/zh-Hant/docs/index.md new file mode 100644 index 000000000..402b9b2af --- /dev/null +++ b/zh-Hant/docs/index.md @@ -0,0 +1,9 @@ +--- +comments: false +glightbox: false +hide: + - footer + - toc + - edit + - navigation +---