From dda64b52e19e7f9652f486d4636b769e874e9953 Mon Sep 17 00:00:00 2001 From: krahets Date: Fri, 6 Oct 2023 13:31:21 +0800 Subject: [PATCH] build --- chapter_array_and_linkedlist/array.md | 1225 ------ chapter_array_and_linkedlist/list.md | 2117 ----------- .../backtracking_algorithm.md | 1729 --------- chapter_backtracking/n_queens_problem.md | 619 --- chapter_backtracking/permutations_problem.md | 919 ----- chapter_backtracking/subset_sum_problem.md | 1441 ------- .../iteration_and_recursion.md | 1631 -------- .../space_complexity.md | 2031 ---------- .../time_complexity.md | 3328 ----------------- .../binary_search_recur.md | 368 -- .../build_binary_tree_problem.md | 459 --- chapter_divide_and_conquer/hanota_problem.md | 470 --- .../dp_problem_features.md | 808 ---- .../dp_solution_pipeline.md | 1318 ------- .../edit_distance_problem.md | 850 ----- .../intro_to_dynamic_programming.md | 1450 ------- .../knapsack_problem.md | 1247 ------ .../unbounded_knapsack_problem.md | 1985 ---------- chapter_graph/graph_operations.md | 2132 ----------- chapter_graph/graph_traversal.md | 848 ----- chapter_greedy/fractional_knapsack_problem.md | 490 --- chapter_greedy/max_capacity_problem.md | 393 -- chapter_greedy/max_product_cutting_problem.md | 369 -- chapter_hashing/hash_collision.md | 2701 ------------- chapter_hashing/hash_map.md | 1669 --------- chapter_heap/heap.md | 1606 -------- chapter_heap/top_k.md | 307 -- chapter_searching/binary_search.md | 645 ---- chapter_searching/binary_search_edge.md | 438 --- chapter_searching/binary_search_insertion.md | 578 --- .../replace_linear_by_hashing.md | 508 --- chapter_sorting/bubble_sort.md | 566 --- chapter_sorting/bucket_sort.md | 422 --- chapter_sorting/counting_sort.md | 787 ---- chapter_sorting/heap_sort.md | 549 --- chapter_sorting/insertion_sort.md | 269 -- chapter_sorting/merge_sort.md | 641 ---- chapter_sorting/quick_sort.md | 1316 ------- chapter_sorting/radix_sort.md | 697 ---- chapter_sorting/selection_sort.md | 295 -- chapter_stack_and_queue/deque.md | 3249 ---------------- chapter_stack_and_queue/queue.md | 2135 ----------- chapter_stack_and_queue/stack.md | 1721 --------- chapter_tree/array_representation_of_tree.md | 1174 ------ chapter_tree/avl_tree.md | 2465 ------------ chapter_tree/binary_search_tree.md | 1499 -------- chapter_tree/binary_tree_traversal.md | 804 ---- .../javascripts}/katex.js | 0 .../javascripts}/mathjax.js | 0 overrides/partials/comments.html | 12 +- overrides/partials/footer.html | 36 - .../stylesheets}/extra.css | 4 + .../chapter_appendix}/contribution.md | 0 .../chapter_appendix}/index.md | 0 .../chapter_appendix}/installation.md | 0 zh/chapter_array_and_linkedlist/array.md | 630 ++++ .../chapter_array_and_linkedlist}/index.md | 0 .../linked_list.md | 477 +-- zh/chapter_array_and_linkedlist/list.md | 937 +++++ .../chapter_array_and_linkedlist}/summary.md | 0 .../backtracking_algorithm.md | 865 +++++ .../chapter_backtracking}/index.md | 0 zh/chapter_backtracking/n_queens_problem.md | 153 + .../permutations_problem.md | 297 ++ zh/chapter_backtracking/subset_sum_problem.md | 385 ++ .../chapter_backtracking}/summary.md | 0 .../index.md | 0 .../iteration_and_recursion.md | 759 ++++ .../performance_evaluation.md | 0 .../space_complexity.md | 1228 ++++++ .../summary.md | 0 .../time_complexity.md | 1926 ++++++++++ .../basic_data_types.md | 0 .../character_encoding.md | 4 +- .../classification_of_data_structure.md | 0 .../chapter_data_structure}/index.md | 0 .../number_encoding.md | 0 .../chapter_data_structure}/summary.md | 0 .../binary_search_recur.md | 143 + .../build_binary_tree_problem.md | 209 ++ .../divide_and_conquer.md | 0 .../hanota_problem.md | 229 ++ .../chapter_divide_and_conquer}/index.md | 0 .../chapter_divide_and_conquer}/summary.md | 0 .../dp_problem_features.md | 317 ++ .../dp_solution_pipeline.md | 471 +++ .../edit_distance_problem.md | 277 ++ .../chapter_dynamic_programming}/index.md | 0 .../intro_to_dynamic_programming.md | 534 +++ .../knapsack_problem.md | 454 +++ .../chapter_dynamic_programming}/summary.md | 0 .../unbounded_knapsack_problem.md | 631 ++++ {chapter_graph => zh/chapter_graph}/graph.md | 0 zh/chapter_graph/graph_operations.md | 233 ++ zh/chapter_graph/graph_traversal.md | 308 ++ {chapter_graph => zh/chapter_graph}/index.md | 0 .../chapter_graph}/summary.md | 0 .../fractional_knapsack_problem.md | 154 + .../chapter_greedy}/greedy_algorithm.md | 214 +- .../chapter_greedy}/index.md | 0 zh/chapter_greedy/max_capacity_problem.md | 183 + .../max_product_cutting_problem.md | 165 + .../chapter_greedy}/summary.md | 0 .../chapter_hashing}/hash_algorithm.md | 458 +-- zh/chapter_hashing/hash_collision.md | 254 ++ zh/chapter_hashing/hash_map.md | 638 ++++ .../chapter_hashing}/index.md | 0 .../chapter_hashing}/summary.md | 0 .../chapter_heap}/build_heap.md | 127 +- zh/chapter_heap/heap.md | 849 +++++ {chapter_heap => zh/chapter_heap}/index.md | 0 {chapter_heap => zh/chapter_heap}/summary.md | 0 zh/chapter_heap/top_k.md | 149 + .../algorithms_are_everywhere.md | 0 .../chapter_introduction}/index.md | 0 .../chapter_introduction}/summary.md | 0 .../chapter_introduction}/what_is_dsa.md | 0 .../chapter_preface}/about_the_book.md | 0 .../chapter_preface}/index.md | 0 .../chapter_preface}/suggestions.md | 0 .../chapter_preface}/summary.md | 0 .../chapter_reference}/index.md | 0 zh/chapter_searching/binary_search.md | 227 ++ zh/chapter_searching/binary_search_edge.md | 200 + .../binary_search_insertion.md | 237 ++ .../chapter_searching}/index.md | 0 .../replace_linear_by_hashing.md | 191 + .../searching_algorithm_revisited.md | 0 .../chapter_searching}/summary.md | 0 zh/chapter_sorting/bubble_sort.md | 201 + zh/chapter_sorting/bucket_sort.md | 122 + zh/chapter_sorting/counting_sort.md | 226 ++ zh/chapter_sorting/heap_sort.md | 171 + .../chapter_sorting}/index.md | 0 zh/chapter_sorting/insertion_sort.md | 120 + zh/chapter_sorting/merge_sort.md | 176 + zh/chapter_sorting/quick_sort.md | 420 +++ zh/chapter_sorting/radix_sort.md | 163 + zh/chapter_sorting/selection_sort.md | 134 + .../chapter_sorting}/sorting_algorithm.md | 0 .../chapter_sorting}/summary.md | 0 zh/chapter_stack_and_queue/deque.md | 579 +++ .../chapter_stack_and_queue}/index.md | 0 zh/chapter_stack_and_queue/queue.md | 519 +++ zh/chapter_stack_and_queue/stack.md | 529 +++ .../chapter_stack_and_queue}/summary.md | 0 .../array_representation_of_tree.md | 230 ++ zh/chapter_tree/avl_tree.md | 913 +++++ zh/chapter_tree/binary_search_tree.md | 361 ++ .../chapter_tree}/binary_tree.md | 0 zh/chapter_tree/binary_tree_traversal.md | 283 ++ {chapter_tree => zh/chapter_tree}/index.md | 0 {chapter_tree => zh/chapter_tree}/summary.md | 0 index.md => zh/index.md | 0 154 files changed, 19511 insertions(+), 56469 deletions(-) delete mode 100755 chapter_array_and_linkedlist/array.md delete mode 100755 chapter_array_and_linkedlist/list.md delete mode 100644 chapter_backtracking/backtracking_algorithm.md delete mode 100644 chapter_backtracking/n_queens_problem.md delete mode 100644 chapter_backtracking/permutations_problem.md delete mode 100644 chapter_backtracking/subset_sum_problem.md delete mode 100644 chapter_computational_complexity/iteration_and_recursion.md delete mode 100755 chapter_computational_complexity/space_complexity.md delete mode 100755 chapter_computational_complexity/time_complexity.md delete mode 100644 chapter_divide_and_conquer/binary_search_recur.md delete mode 100644 chapter_divide_and_conquer/build_binary_tree_problem.md delete mode 100644 chapter_divide_and_conquer/hanota_problem.md delete mode 100644 chapter_dynamic_programming/dp_problem_features.md delete mode 100644 chapter_dynamic_programming/dp_solution_pipeline.md delete mode 100644 chapter_dynamic_programming/edit_distance_problem.md delete mode 100644 chapter_dynamic_programming/intro_to_dynamic_programming.md delete mode 100644 chapter_dynamic_programming/knapsack_problem.md delete mode 100644 chapter_dynamic_programming/unbounded_knapsack_problem.md delete mode 100644 chapter_graph/graph_operations.md delete mode 100644 chapter_graph/graph_traversal.md delete mode 100644 chapter_greedy/fractional_knapsack_problem.md delete mode 100644 chapter_greedy/max_capacity_problem.md delete mode 100644 chapter_greedy/max_product_cutting_problem.md delete mode 100644 chapter_hashing/hash_collision.md delete mode 100755 chapter_hashing/hash_map.md delete mode 100644 chapter_heap/heap.md delete mode 100644 chapter_heap/top_k.md delete mode 100755 chapter_searching/binary_search.md delete mode 100644 chapter_searching/binary_search_edge.md delete mode 100644 chapter_searching/binary_search_insertion.md delete mode 100755 chapter_searching/replace_linear_by_hashing.md delete mode 100755 chapter_sorting/bubble_sort.md delete mode 100644 chapter_sorting/bucket_sort.md delete mode 100644 chapter_sorting/counting_sort.md delete mode 100644 chapter_sorting/heap_sort.md delete mode 100755 chapter_sorting/insertion_sort.md delete mode 100755 chapter_sorting/merge_sort.md delete mode 100755 chapter_sorting/quick_sort.md delete mode 100644 chapter_sorting/radix_sort.md delete mode 100644 chapter_sorting/selection_sort.md delete mode 100644 chapter_stack_and_queue/deque.md delete mode 100755 chapter_stack_and_queue/queue.md delete mode 100755 chapter_stack_and_queue/stack.md delete mode 100644 chapter_tree/array_representation_of_tree.md delete mode 100644 chapter_tree/avl_tree.md delete mode 100755 chapter_tree/binary_search_tree.md delete mode 100755 chapter_tree/binary_tree_traversal.md rename {javascripts => overrides/javascripts}/katex.js (100%) rename {javascripts => overrides/javascripts}/mathjax.js (100%) delete mode 100644 overrides/partials/footer.html rename {stylesheets => overrides/stylesheets}/extra.css (98%) rename {chapter_appendix => zh/chapter_appendix}/contribution.md (100%) rename {chapter_appendix => zh/chapter_appendix}/index.md (100%) rename {chapter_appendix => zh/chapter_appendix}/installation.md (100%) create mode 100755 zh/chapter_array_and_linkedlist/array.md rename {chapter_array_and_linkedlist => zh/chapter_array_and_linkedlist}/index.md (100%) rename {chapter_array_and_linkedlist => zh/chapter_array_and_linkedlist}/linked_list.md (65%) create mode 100755 zh/chapter_array_and_linkedlist/list.md rename {chapter_array_and_linkedlist => zh/chapter_array_and_linkedlist}/summary.md (100%) create mode 100644 zh/chapter_backtracking/backtracking_algorithm.md rename {chapter_backtracking => zh/chapter_backtracking}/index.md (100%) create mode 100644 zh/chapter_backtracking/n_queens_problem.md create mode 100644 zh/chapter_backtracking/permutations_problem.md create mode 100644 zh/chapter_backtracking/subset_sum_problem.md rename {chapter_backtracking => zh/chapter_backtracking}/summary.md (100%) rename {chapter_computational_complexity => zh/chapter_computational_complexity}/index.md (100%) create mode 100644 zh/chapter_computational_complexity/iteration_and_recursion.md rename {chapter_computational_complexity => zh/chapter_computational_complexity}/performance_evaluation.md (100%) create mode 100755 zh/chapter_computational_complexity/space_complexity.md rename {chapter_computational_complexity => zh/chapter_computational_complexity}/summary.md (100%) create mode 100755 zh/chapter_computational_complexity/time_complexity.md rename {chapter_data_structure => zh/chapter_data_structure}/basic_data_types.md (100%) rename {chapter_data_structure => zh/chapter_data_structure}/character_encoding.md (95%) rename {chapter_data_structure => zh/chapter_data_structure}/classification_of_data_structure.md (100%) rename {chapter_data_structure => zh/chapter_data_structure}/index.md (100%) rename {chapter_data_structure => zh/chapter_data_structure}/number_encoding.md (100%) rename {chapter_data_structure => zh/chapter_data_structure}/summary.md (100%) create mode 100644 zh/chapter_divide_and_conquer/binary_search_recur.md create mode 100644 zh/chapter_divide_and_conquer/build_binary_tree_problem.md rename {chapter_divide_and_conquer => zh/chapter_divide_and_conquer}/divide_and_conquer.md (100%) create mode 100644 zh/chapter_divide_and_conquer/hanota_problem.md rename {chapter_divide_and_conquer => zh/chapter_divide_and_conquer}/index.md (100%) rename {chapter_divide_and_conquer => zh/chapter_divide_and_conquer}/summary.md (100%) create mode 100644 zh/chapter_dynamic_programming/dp_problem_features.md create mode 100644 zh/chapter_dynamic_programming/dp_solution_pipeline.md create mode 100644 zh/chapter_dynamic_programming/edit_distance_problem.md rename {chapter_dynamic_programming => zh/chapter_dynamic_programming}/index.md (100%) create mode 100644 zh/chapter_dynamic_programming/intro_to_dynamic_programming.md create mode 100644 zh/chapter_dynamic_programming/knapsack_problem.md rename {chapter_dynamic_programming => zh/chapter_dynamic_programming}/summary.md (100%) create mode 100644 zh/chapter_dynamic_programming/unbounded_knapsack_problem.md rename {chapter_graph => zh/chapter_graph}/graph.md (100%) create mode 100644 zh/chapter_graph/graph_operations.md create mode 100644 zh/chapter_graph/graph_traversal.md rename {chapter_graph => zh/chapter_graph}/index.md (100%) rename {chapter_graph => zh/chapter_graph}/summary.md (100%) create mode 100644 zh/chapter_greedy/fractional_knapsack_problem.md rename {chapter_greedy => zh/chapter_greedy}/greedy_algorithm.md (57%) rename {chapter_greedy => zh/chapter_greedy}/index.md (100%) create mode 100644 zh/chapter_greedy/max_capacity_problem.md create mode 100644 zh/chapter_greedy/max_product_cutting_problem.md rename {chapter_greedy => zh/chapter_greedy}/summary.md (100%) rename {chapter_hashing => zh/chapter_hashing}/hash_algorithm.md (60%) create mode 100644 zh/chapter_hashing/hash_collision.md create mode 100755 zh/chapter_hashing/hash_map.md rename {chapter_hashing => zh/chapter_hashing}/index.md (100%) rename {chapter_hashing => zh/chapter_hashing}/summary.md (100%) rename {chapter_heap => zh/chapter_heap}/build_heap.md (50%) create mode 100644 zh/chapter_heap/heap.md rename {chapter_heap => zh/chapter_heap}/index.md (100%) rename {chapter_heap => zh/chapter_heap}/summary.md (100%) create mode 100644 zh/chapter_heap/top_k.md rename {chapter_introduction => zh/chapter_introduction}/algorithms_are_everywhere.md (100%) rename {chapter_introduction => zh/chapter_introduction}/index.md (100%) rename {chapter_introduction => zh/chapter_introduction}/summary.md (100%) rename {chapter_introduction => zh/chapter_introduction}/what_is_dsa.md (100%) rename {chapter_preface => zh/chapter_preface}/about_the_book.md (100%) rename {chapter_preface => zh/chapter_preface}/index.md (100%) rename {chapter_preface => zh/chapter_preface}/suggestions.md (100%) rename {chapter_preface => zh/chapter_preface}/summary.md (100%) rename {chapter_reference => zh/chapter_reference}/index.md (100%) create mode 100755 zh/chapter_searching/binary_search.md create mode 100644 zh/chapter_searching/binary_search_edge.md create mode 100644 zh/chapter_searching/binary_search_insertion.md rename {chapter_searching => zh/chapter_searching}/index.md (100%) create mode 100755 zh/chapter_searching/replace_linear_by_hashing.md rename {chapter_searching => zh/chapter_searching}/searching_algorithm_revisited.md (100%) rename {chapter_searching => zh/chapter_searching}/summary.md (100%) create mode 100755 zh/chapter_sorting/bubble_sort.md create mode 100644 zh/chapter_sorting/bucket_sort.md create mode 100644 zh/chapter_sorting/counting_sort.md create mode 100644 zh/chapter_sorting/heap_sort.md rename {chapter_sorting => zh/chapter_sorting}/index.md (100%) create mode 100755 zh/chapter_sorting/insertion_sort.md create mode 100755 zh/chapter_sorting/merge_sort.md create mode 100755 zh/chapter_sorting/quick_sort.md create mode 100644 zh/chapter_sorting/radix_sort.md create mode 100644 zh/chapter_sorting/selection_sort.md rename {chapter_sorting => zh/chapter_sorting}/sorting_algorithm.md (100%) rename {chapter_sorting => zh/chapter_sorting}/summary.md (100%) create mode 100644 zh/chapter_stack_and_queue/deque.md rename {chapter_stack_and_queue => zh/chapter_stack_and_queue}/index.md (100%) create mode 100755 zh/chapter_stack_and_queue/queue.md create mode 100755 zh/chapter_stack_and_queue/stack.md rename {chapter_stack_and_queue => zh/chapter_stack_and_queue}/summary.md (100%) create mode 100644 zh/chapter_tree/array_representation_of_tree.md create mode 100644 zh/chapter_tree/avl_tree.md create mode 100755 zh/chapter_tree/binary_search_tree.md rename {chapter_tree => zh/chapter_tree}/binary_tree.md (100%) create mode 100755 zh/chapter_tree/binary_tree_traversal.md rename {chapter_tree => zh/chapter_tree}/index.md (100%) rename {chapter_tree => zh/chapter_tree}/summary.md (100%) rename index.md => zh/index.md (100%) diff --git a/chapter_array_and_linkedlist/array.md b/chapter_array_and_linkedlist/array.md deleted file mode 100755 index c7e95397f..000000000 --- a/chapter_array_and_linkedlist/array.md +++ /dev/null @@ -1,1225 +0,0 @@ ---- -comments: true ---- - -# 4.1   数组 - -「数组 array」是一种线性数据结构,其将相同类型元素存储在连续的内存空间中。我们将元素在数组中的位置称为该元素的「索引 index」。图 4-1 展示了数组的主要术语和概念。 - -![数组定义与存储方式](array.assets/array_definition.png) - -

图 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 }; - ``` - -=== "Zig" - - ```zig title="array.zig" - // 初始化数组 - var arr = [_]i32{0} ** 5; // { 0, 0, 0, 0, 0 } - var nums = [_]i32{ 1, 3, 2, 5, 4 }; - ``` - -### 2.   访问元素 - -数组元素被存储在连续的内存空间中,这意味着计算数组元素的内存地址非常容易。给定数组内存地址(即首元素内存地址)和某个元素的索引,我们可以使用图 4-2 所示的公式计算得到该元素的内存地址,从而直接访问此元素。 - -![数组元素的内存地址计算](array.assets/array_memory_location_calculation.png) - -

图 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; - } - ``` - -=== "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; - } - ``` - -### 3.   插入元素 - -数组元素在内存中是“紧挨着的”,它们之间没有空间再存放任何数据。如图 4-3 所示,如果想要在数组中间插入一个元素,则需要将该元素之后的所有元素都向后移动一位,之后再把元素赋值给该索引。 - -![数组插入元素示例](array.assets/array_insert_element.png) - -

图 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 sequence(first: nums.count - 1, next: { $0 > index + 1 ? $0 - 1 : nil }) { - 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; - } - ``` - -=== "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; - } - ``` - -### 4.   删除元素 - -同理,如图 4-4 所示,若想要删除索引 $i$ 处的元素,则需要把索引 $i$ 之后的元素都向前移动一位。 - -![数组删除元素示例](array.assets/array_remove_element.png) - -

图 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) { - let count = nums.count - // 把索引 index 之后的所有元素向前移动一位 - for i in sequence(first: index, next: { $0 < count - 1 - 1 ? $0 + 1 : nil }) { - 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]; - } - } - ``` - -=== "Zig" - - ```zig title="array.zig" - // 删除索引 index 处元素 - fn remove(nums: []i32, index: usize) void { - // 把索引 index 之后的所有元素向前移动一位 - var i = index; - while (i < nums.len - 1) : (i += 1) { - nums[i] = nums[i + 1]; - } - } - ``` - -总的来看,数组的插入与删除操作有以下缺点。 - -- **时间复杂度高**:数组的插入和删除的平均时间复杂度均为 $O(n)$ ,其中 $n$ 为数组长度。 -- **丢失元素**:由于数组的长度不可变,因此在插入元素后,超出数组长度范围的元素会丢失。 -- **内存浪费**:我们可以初始化一个比较长的数组,只用前面一部分,这样在插入数据时,丢失的末尾元素都是“无意义”的,但这样做也会造成部分内存空间的浪费。 - -### 5.   遍历数组 - -在大多数编程语言中,我们既可以通过索引遍历数组,也可以直接遍历获取数组中的每个元素。 - -=== "Python" - - ```python title="array.py" - def traverse(nums: list[int]): - """遍历数组""" - count = 0 - # 通过索引遍历数组 - for i in range(len(nums)): - count += 1 - # 直接遍历数组 - for num in nums: - count += 1 - # 同时遍历数据索引和元素 - for i, num in enumerate(nums): - count += 1 - ``` - -=== "C++" - - ```cpp title="array.cpp" - /* 遍历数组 */ - void traverse(int *nums, int size) { - int count = 0; - // 通过索引遍历数组 - for (int i = 0; i < size; i++) { - count++; - } - } - ``` - -=== "Java" - - ```java title="array.java" - /* 遍历数组 */ - void traverse(int[] nums) { - int count = 0; - // 通过索引遍历数组 - for (int i = 0; i < nums.length; i++) { - count++; - } - // 直接遍历数组 - for (int num : nums) { - count++; - } - } - ``` - -=== "C#" - - ```csharp title="array.cs" - /* 遍历数组 */ - void traverse(int[] nums) { - int count = 0; - // 通过索引遍历数组 - for (int i = 0; i < nums.Length; i++) { - count++; - } - // 直接遍历数组 - foreach (int num in nums) { - count++; - } - } - ``` - -=== "Go" - - ```go title="array.go" - /* 遍历数组 */ - func traverse(nums []int) { - count := 0 - // 通过索引遍历数组 - for i := 0; i < len(nums); i++ { - count++ - } - count = 0 - // 直接遍历数组 - for range nums { - count++ - } - } - ``` - -=== "Swift" - - ```swift title="array.swift" - /* 遍历数组 */ - func traverse(nums: [Int]) { - var count = 0 - // 通过索引遍历数组 - for _ in nums.indices { - count += 1 - } - // 直接遍历数组 - for _ in nums { - count += 1 - } - } - ``` - -=== "JS" - - ```javascript title="array.js" - /* 遍历数组 */ - function traverse(nums) { - let count = 0; - // 通过索引遍历数组 - for (let i = 0; i < nums.length; i++) { - count++; - } - // 直接遍历数组 - for (const num of nums) { - count += 1; - } - } - ``` - -=== "TS" - - ```typescript title="array.ts" - /* 遍历数组 */ - function traverse(nums: number[]): void { - let count = 0; - // 通过索引遍历数组 - for (let i = 0; i < nums.length; i++) { - count++; - } - // 直接遍历数组 - for (const num of nums) { - count += 1; - } - } - ``` - -=== "Dart" - - ```dart title="array.dart" - /* 遍历数组元素 */ - void traverse(List nums) { - var count = 0; - // 通过索引遍历数组 - for (var i = 0; i < nums.length; i++) { - count++; - } - // 直接遍历数组 - for (var num in nums) { - count++; - } - // 通过 forEach 方法遍历数组 - nums.forEach((element) { - count++; - }); - } - ``` - -=== "Rust" - - ```rust title="array.rs" - /* 遍历数组 */ - fn traverse(nums: &[i32]) { - let mut _count = 0; - // 通过索引遍历数组 - for _ in 0..nums.len() { - _count += 1; - } - // 直接遍历数组 - for _ in nums { - _count += 1; - } - } - ``` - -=== "C" - - ```c title="array.c" - /* 遍历数组 */ - void traverse(int *nums, int size) { - int count = 0; - // 通过索引遍历数组 - for (int i = 0; i < size; i++) { - count++; - } - } - ``` - -=== "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 += 1; - } - count = 0; - // 直接遍历数组 - for (nums) |_| { - count += 1; - } - } - ``` - -### 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; - } - ``` - -=== "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; - } - ``` - -### 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; - } - ``` - -=== "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; - } - ``` - -## 4.1.2   数组优点与局限性 - -数组存储在连续的内存空间内,且元素类型相同。这种做法包含丰富的先验信息,系统可以利用这些信息来优化数据结构的操作效率。 - -- **空间效率高**: 数组为数据分配了连续的内存块,无须额外的结构开销。 -- **支持随机访问**: 数组允许在 $O(1)$ 时间内访问任何元素。 -- **缓存局部性**: 当访问数组元素时,计算机不仅会加载它,还会缓存其周围的其他数据,从而借助高速缓存来提升后续操作的执行速度。 - -连续空间存储是一把双刃剑,其存在以下缺点。 - -- **插入与删除效率低**:当数组中元素较多时,插入与删除操作需要移动大量的元素。 -- **长度不可变**: 数组在初始化后长度就固定了,扩容数组需要将所有数据复制到新数组,开销很大。 -- **空间浪费**: 如果数组分配的大小超过了实际所需,那么多余的空间就被浪费了。 - -## 4.1.3   数组典型应用 - -数组是一种基础且常见的数据结构,既频繁应用在各类算法之中,也可用于实现各种复杂数据结构。 - -- **随机访问**:如果我们想要随机抽取一些样本,那么可以用数组存储,并生成一个随机序列,根据索引实现样本的随机抽取。 -- **排序和搜索**:数组是排序和搜索算法最常用的数据结构。快速排序、归并排序、二分查找等都主要在数组上进行。 -- **查找表**:当我们需要快速查找一个元素或者需要查找一个元素的对应关系时,可以使用数组作为查找表。假如我们想要实现字符到 ASCII 码的映射,则可以将字符的 ASCII 码值作为索引,对应的元素存放在数组中的对应位置。 -- **机器学习**:神经网络中大量使用了向量、矩阵、张量之间的线性代数运算,这些数据都是以数组的形式构建的。数组是神经网络编程中最常使用的数据结构。 -- **数据结构实现**:数组可以用于实现栈、队列、哈希表、堆、图等数据结构。例如,图的邻接矩阵表示实际上是一个二维数组。 diff --git a/chapter_array_and_linkedlist/list.md b/chapter_array_and_linkedlist/list.md deleted file mode 100755 index f21a968a3..000000000 --- a/chapter_array_and_linkedlist/list.md +++ /dev/null @@ -1,2117 +0,0 @@ ---- -comments: true ---- - -# 4.3   列表 - -**数组长度不可变导致实用性降低**。在实际中,我们可能事先无法确定需要存储多少数据,这使数组长度的选择变得困难。若长度过小,需要在持续添加数据时频繁扩容数组;若长度过大,则会造成内存空间的浪费。 - -为解决此问题,出现了一种被称为「动态数组 dynamic array」的数据结构,即长度可变的数组,也常被称为「列表 list」。列表基于数组实现,继承了数组的优点,并且可以在程序运行过程中动态扩容。我们可以在列表中自由地添加元素,而无须担心超过容量限制。 - -## 4.3.1   列表常用操作 - -### 1.   初始化列表 - -我们通常使用“无初始值”和“有初始值”这两种初始化方法。 - -=== "Python" - - ```python title="list.py" - # 初始化列表 - # 无初始值 - list1: list[int] = [] - # 有初始值 - list: list[int] = [1, 3, 2, 5, 4] - ``` - -=== "C++" - - ```cpp title="list.cpp" - /* 初始化列表 */ - // 需注意,C++ 中 vector 即是本文描述的 list - // 无初始值 - vector list1; - // 有初始值 - vector list = { 1, 3, 2, 5, 4 }; - ``` - -=== "Java" - - ```java title="list.java" - /* 初始化列表 */ - // 无初始值 - List list1 = new ArrayList<>(); - // 有初始值(注意数组的元素类型需为 int[] 的包装类 Integer[]) - Integer[] numbers = new Integer[] { 1, 3, 2, 5, 4 }; - List list = new ArrayList<>(Arrays.asList(numbers)); - ``` - -=== "C#" - - ```csharp title="list.cs" - /* 初始化列表 */ - // 无初始值 - List list1 = new (); - // 有初始值 - int[] numbers = new int[] { 1, 3, 2, 5, 4 }; - List list = numbers.ToList(); - ``` - -=== "Go" - - ```go title="list_test.go" - /* 初始化列表 */ - // 无初始值 - list1 := []int - // 有初始值 - list := []int{1, 3, 2, 5, 4} - ``` - -=== "Swift" - - ```swift title="list.swift" - /* 初始化列表 */ - // 无初始值 - let list1: [Int] = [] - // 有初始值 - var list = [1, 3, 2, 5, 4] - ``` - -=== "JS" - - ```javascript title="list.js" - /* 初始化列表 */ - // 无初始值 - const list1 = []; - // 有初始值 - const list = [1, 3, 2, 5, 4]; - ``` - -=== "TS" - - ```typescript title="list.ts" - /* 初始化列表 */ - // 无初始值 - const list1: number[] = []; - // 有初始值 - const list: number[] = [1, 3, 2, 5, 4]; - ``` - -=== "Dart" - - ```dart title="list.dart" - /* 初始化列表 */ - // 无初始值 - List list1 = []; - // 有初始值 - List list = [1, 3, 2, 5, 4]; - ``` - -=== "Rust" - - ```rust title="list.rs" - /* 初始化列表 */ - // 无初始值 - let list1: Vec = Vec::new(); - // 有初始值 - let list2: Vec = vec![1, 3, 2, 5, 4]; - ``` - -=== "C" - - ```c title="list.c" - // C 未提供内置动态数组 - ``` - -=== "Zig" - - ```zig title="list.zig" - // 初始化列表 - var list = std.ArrayList(i32).init(std.heap.page_allocator); - defer list.deinit(); - try list.appendSlice(&[_]i32{ 1, 3, 2, 5, 4 }); - ``` - -### 2.   访问元素 - -列表本质上是数组,因此可以在 $O(1)$ 时间内访问和更新元素,效率很高。 - -=== "Python" - - ```python title="list.py" - # 访问元素 - num: int = list[1] # 访问索引 1 处的元素 - - # 更新元素 - list[1] = 0 # 将索引 1 处的元素更新为 0 - ``` - -=== "C++" - - ```cpp title="list.cpp" - /* 访问元素 */ - int num = list[1]; // 访问索引 1 处的元素 - - /* 更新元素 */ - list[1] = 0; // 将索引 1 处的元素更新为 0 - ``` - -=== "Java" - - ```java title="list.java" - /* 访问元素 */ - int num = list.get(1); // 访问索引 1 处的元素 - - /* 更新元素 */ - list.set(1, 0); // 将索引 1 处的元素更新为 0 - ``` - -=== "C#" - - ```csharp title="list.cs" - /* 访问元素 */ - int num = list[1]; // 访问索引 1 处的元素 - - /* 更新元素 */ - list[1] = 0; // 将索引 1 处的元素更新为 0 - ``` - -=== "Go" - - ```go title="list_test.go" - /* 访问元素 */ - num := list[1] // 访问索引 1 处的元素 - - /* 更新元素 */ - list[1] = 0 // 将索引 1 处的元素更新为 0 - ``` - -=== "Swift" - - ```swift title="list.swift" - /* 访问元素 */ - let num = list[1] // 访问索引 1 处的元素 - - /* 更新元素 */ - list[1] = 0 // 将索引 1 处的元素更新为 0 - ``` - -=== "JS" - - ```javascript title="list.js" - /* 访问元素 */ - const num = list[1]; // 访问索引 1 处的元素 - - /* 更新元素 */ - list[1] = 0; // 将索引 1 处的元素更新为 0 - ``` - -=== "TS" - - ```typescript title="list.ts" - /* 访问元素 */ - const num: number = list[1]; // 访问索引 1 处的元素 - - /* 更新元素 */ - list[1] = 0; // 将索引 1 处的元素更新为 0 - ``` - -=== "Dart" - - ```dart title="list.dart" - /* 访问元素 */ - int num = list[1]; // 访问索引 1 处的元素 - - /* 更新元素 */ - list[1] = 0; // 将索引 1 处的元素更新为 0 - ``` - -=== "Rust" - - ```rust title="list.rs" - /* 访问元素 */ - let num: i32 = list[1]; // 访问索引 1 处的元素 - /* 更新元素 */ - list[1] = 0; // 将索引 1 处的元素更新为 0 - ``` - -=== "C" - - ```c title="list.c" - // C 未提供内置动态数组 - ``` - -=== "Zig" - - ```zig title="list.zig" - // 访问元素 - var num = list.items[1]; // 访问索引 1 处的元素 - - // 更新元素 - list.items[1] = 0; // 将索引 1 处的元素更新为 0 - ``` - -### 3.   插入与删除元素 - -相较于数组,列表可以自由地添加与删除元素。在列表尾部添加元素的时间复杂度为 $O(1)$ ,但插入和删除元素的效率仍与数组相同,时间复杂度为 $O(n)$ 。 - -=== "Python" - - ```python title="list.py" - # 清空列表 - list.clear() - - # 尾部添加元素 - list.append(1) - list.append(3) - list.append(2) - list.append(5) - list.append(4) - - # 中间插入元素 - list.insert(3, 6) # 在索引 3 处插入数字 6 - - # 删除元素 - list.pop(3) # 删除索引 3 处的元素 - ``` - -=== "C++" - - ```cpp title="list.cpp" - /* 清空列表 */ - list.clear(); - - /* 尾部添加元素 */ - list.push_back(1); - list.push_back(3); - list.push_back(2); - list.push_back(5); - list.push_back(4); - - /* 中间插入元素 */ - list.insert(list.begin() + 3, 6); // 在索引 3 处插入数字 6 - - /* 删除元素 */ - list.erase(list.begin() + 3); // 删除索引 3 处的元素 - ``` - -=== "Java" - - ```java title="list.java" - /* 清空列表 */ - list.clear(); - - /* 尾部添加元素 */ - list.add(1); - list.add(3); - list.add(2); - list.add(5); - list.add(4); - - /* 中间插入元素 */ - list.add(3, 6); // 在索引 3 处插入数字 6 - - /* 删除元素 */ - list.remove(3); // 删除索引 3 处的元素 - ``` - -=== "C#" - - ```csharp title="list.cs" - /* 清空列表 */ - list.Clear(); - - /* 尾部添加元素 */ - list.Add(1); - list.Add(3); - list.Add(2); - list.Add(5); - list.Add(4); - - /* 中间插入元素 */ - list.Insert(3, 6); - - /* 删除元素 */ - list.RemoveAt(3); - ``` - -=== "Go" - - ```go title="list_test.go" - /* 清空列表 */ - list = nil - - /* 尾部添加元素 */ - list = append(list, 1) - list = append(list, 3) - list = append(list, 2) - list = append(list, 5) - list = append(list, 4) - - /* 中间插入元素 */ - list = append(list[:3], append([]int{6}, list[3:]...)...) // 在索引 3 处插入数字 6 - - /* 删除元素 */ - list = append(list[:3], list[4:]...) // 删除索引 3 处的元素 - ``` - -=== "Swift" - - ```swift title="list.swift" - /* 清空列表 */ - list.removeAll() - - /* 尾部添加元素 */ - list.append(1) - list.append(3) - list.append(2) - list.append(5) - list.append(4) - - /* 中间插入元素 */ - list.insert(6, at: 3) // 在索引 3 处插入数字 6 - - /* 删除元素 */ - list.remove(at: 3) // 删除索引 3 处的元素 - ``` - -=== "JS" - - ```javascript title="list.js" - /* 清空列表 */ - list.length = 0; - - /* 尾部添加元素 */ - list.push(1); - list.push(3); - list.push(2); - list.push(5); - list.push(4); - - /* 中间插入元素 */ - list.splice(3, 0, 6); - - /* 删除元素 */ - list.splice(3, 1); - ``` - -=== "TS" - - ```typescript title="list.ts" - /* 清空列表 */ - list.length = 0; - - /* 尾部添加元素 */ - list.push(1); - list.push(3); - list.push(2); - list.push(5); - list.push(4); - - /* 中间插入元素 */ - list.splice(3, 0, 6); - - /* 删除元素 */ - list.splice(3, 1); - ``` - -=== "Dart" - - ```dart title="list.dart" - /* 清空列表 */ - list.clear(); - - /* 尾部添加元素 */ - list.add(1); - list.add(3); - list.add(2); - list.add(5); - list.add(4); - - /* 中间插入元素 */ - list.insert(3, 6); // 在索引 3 处插入数字 6 - - /* 删除元素 */ - list.removeAt(3); // 删除索引 3 处的元素 - ``` - -=== "Rust" - - ```rust title="list.rs" - /* 清空列表 */ - list.clear(); - - /* 尾部添加元素 */ - list.push(1); - list.push(3); - list.push(2); - list.push(5); - list.push(4); - - /* 中间插入元素 */ - list.insert(3, 6); // 在索引 3 处插入数字 6 - - /* 删除元素 */ - list.remove(3); // 删除索引 3 处的元素 - ``` - -=== "C" - - ```c title="list.c" - // C 未提供内置动态数组 - ``` - -=== "Zig" - - ```zig title="list.zig" - // 清空列表 - list.clearRetainingCapacity(); - - // 尾部添加元素 - try list.append(1); - try list.append(3); - try list.append(2); - try list.append(5); - try list.append(4); - - // 中间插入元素 - try list.insert(3, 6); // 在索引 3 处插入数字 6 - - // 删除元素 - _ = list.orderedRemove(3); // 删除索引 3 处的元素 - ``` - -### 4.   遍历列表 - -与数组一样,列表可以根据索引遍历,也可以直接遍历各元素。 - -=== "Python" - - ```python title="list.py" - # 通过索引遍历列表 - count = 0 - for i in range(len(list)): - count += 1 - - # 直接遍历列表元素 - count = 0 - for n in list: - count += 1 - ``` - -=== "C++" - - ```cpp title="list.cpp" - /* 通过索引遍历列表 */ - int count = 0; - for (int i = 0; i < list.size(); i++) { - count++; - } - - /* 直接遍历列表元素 */ - count = 0; - for (int n : list) { - count++; - } - ``` - -=== "Java" - - ```java title="list.java" - /* 通过索引遍历列表 */ - int count = 0; - for (int i = 0; i < list.size(); i++) { - count++; - } - - /* 直接遍历列表元素 */ - count = 0; - for (int n : list) { - count++; - } - ``` - -=== "C#" - - ```csharp title="list.cs" - /* 通过索引遍历列表 */ - int count = 0; - for (int i = 0; i < list.Count; i++) { - count++; - } - - /* 直接遍历列表元素 */ - count = 0; - foreach (int n in list) { - count++; - } - ``` - -=== "Go" - - ```go title="list_test.go" - /* 通过索引遍历列表 */ - count := 0 - for i := 0; i < len(list); i++ { - count++ - } - - /* 直接遍历列表元素 */ - count = 0 - for range list { - count++ - } - ``` - -=== "Swift" - - ```swift title="list.swift" - /* 通过索引遍历列表 */ - var count = 0 - for _ in list.indices { - count += 1 - } - - /* 直接遍历列表元素 */ - count = 0 - for _ in list { - count += 1 - } - ``` - -=== "JS" - - ```javascript title="list.js" - /* 通过索引遍历列表 */ - let count = 0; - for (let i = 0; i < list.length; i++) { - count++; - } - - /* 直接遍历列表元素 */ - count = 0; - for (const n of list) { - count++; - } - ``` - -=== "TS" - - ```typescript title="list.ts" - /* 通过索引遍历列表 */ - let count = 0; - for (let i = 0; i < list.length; i++) { - count++; - } - - /* 直接遍历列表元素 */ - count = 0; - for (const n of list) { - count++; - } - ``` - -=== "Dart" - - ```dart title="list.dart" - /* 通过索引遍历列表 */ - int count = 0; - for (int i = 0; i < list.length; i++) { - count++; - } - - /* 直接遍历列表元素 */ - count = 0; - for (int n in list) { - count++; - } - ``` - -=== "Rust" - - ```rust title="list.rs" - /* 通过索引遍历列表 */ - let mut count = 0; - for (index, value) in list.iter().enumerate() { - count += 1; - } - - /* 直接遍历列表元素 */ - let mut count = 0; - for value in list.iter() { - count += 1; - } - ``` - -=== "C" - - ```c title="list.c" - // C 未提供内置动态数组 - ``` - -=== "Zig" - - ```zig title="list.zig" - // 通过索引遍历列表 - var count: i32 = 0; - var i: i32 = 0; - while (i < list.items.len) : (i += 1) { - count += 1; - } - - // 直接遍历列表元素 - count = 0; - for (list.items) |_| { - count += 1; - } - ``` - -### 5.   拼接列表 - -给定一个新列表 `list1` ,我们可以将该列表拼接到原列表的尾部。 - -=== "Python" - - ```python title="list.py" - # 拼接两个列表 - list1: list[int] = [6, 8, 7, 10, 9] - list += list1 # 将列表 list1 拼接到 list 之后 - ``` - -=== "C++" - - ```cpp title="list.cpp" - /* 拼接两个列表 */ - vector list1 = { 6, 8, 7, 10, 9 }; - // 将列表 list1 拼接到 list 之后 - list.insert(list.end(), list1.begin(), list1.end()); - ``` - -=== "Java" - - ```java title="list.java" - /* 拼接两个列表 */ - List list1 = new ArrayList<>(Arrays.asList(new Integer[] { 6, 8, 7, 10, 9 })); - list.addAll(list1); // 将列表 list1 拼接到 list 之后 - ``` - -=== "C#" - - ```csharp title="list.cs" - /* 拼接两个列表 */ - List list1 = new() { 6, 8, 7, 10, 9 }; - list.AddRange(list1); // 将列表 list1 拼接到 list 之后 - ``` - -=== "Go" - - ```go title="list_test.go" - /* 拼接两个列表 */ - list1 := []int{6, 8, 7, 10, 9} - list = append(list, list1...) // 将列表 list1 拼接到 list 之后 - ``` - -=== "Swift" - - ```swift title="list.swift" - /* 拼接两个列表 */ - let list1 = [6, 8, 7, 10, 9] - list.append(contentsOf: list1) // 将列表 list1 拼接到 list 之后 - ``` - -=== "JS" - - ```javascript title="list.js" - /* 拼接两个列表 */ - const list1 = [6, 8, 7, 10, 9]; - list.push(...list1); // 将列表 list1 拼接到 list 之后 - ``` - -=== "TS" - - ```typescript title="list.ts" - /* 拼接两个列表 */ - const list1: number[] = [6, 8, 7, 10, 9]; - list.push(...list1); // 将列表 list1 拼接到 list 之后 - ``` - -=== "Dart" - - ```dart title="list.dart" - /* 拼接两个列表 */ - List list1 = [6, 8, 7, 10, 9]; - list.addAll(list1); // 将列表 list1 拼接到 list 之后 - ``` - -=== "Rust" - - ```rust title="list.rs" - /* 拼接两个列表 */ - let list1: Vec = vec![6, 8, 7, 10, 9]; - list.extend(list1); - ``` - -=== "C" - - ```c title="list.c" - // C 未提供内置动态数组 - ``` - -=== "Zig" - - ```zig title="list.zig" - // 拼接两个列表 - var list1 = std.ArrayList(i32).init(std.heap.page_allocator); - defer list1.deinit(); - try list1.appendSlice(&[_]i32{ 6, 8, 7, 10, 9 }); - try list.insertSlice(list.items.len, list1.items); // 将列表 list1 拼接到 list 之后 - ``` - -### 6.   排序列表 - -完成列表排序后,我们便可以使用在数组类算法题中经常考察的“二分查找”和“双指针”算法。 - -=== "Python" - - ```python title="list.py" - # 排序列表 - list.sort() # 排序后,列表元素从小到大排列 - ``` - -=== "C++" - - ```cpp title="list.cpp" - /* 排序列表 */ - sort(list.begin(), list.end()); // 排序后,列表元素从小到大排列 - ``` - -=== "Java" - - ```java title="list.java" - /* 排序列表 */ - Collections.sort(list); // 排序后,列表元素从小到大排列 - ``` - -=== "C#" - - ```csharp title="list.cs" - /* 排序列表 */ - list.Sort(); // 排序后,列表元素从小到大排列 - ``` - -=== "Go" - - ```go title="list_test.go" - /* 排序列表 */ - sort.Ints(list) // 排序后,列表元素从小到大排列 - ``` - -=== "Swift" - - ```swift title="list.swift" - /* 排序列表 */ - list.sort() // 排序后,列表元素从小到大排列 - ``` - -=== "JS" - - ```javascript title="list.js" - /* 排序列表 */ - list.sort((a, b) => a - b); // 排序后,列表元素从小到大排列 - ``` - -=== "TS" - - ```typescript title="list.ts" - /* 排序列表 */ - list.sort((a, b) => a - b); // 排序后,列表元素从小到大排列 - ``` - -=== "Dart" - - ```dart title="list.dart" - /* 排序列表 */ - list.sort(); // 排序后,列表元素从小到大排列 - ``` - -=== "Rust" - - ```rust title="list.rs" - /* 排序列表 */ - list.sort(); // 排序后,列表元素从小到大排列 - ``` - -=== "C" - - ```c title="list.c" - // C 未提供内置动态数组 - ``` - -=== "Zig" - - ```zig title="list.zig" - // 排序列表 - std.sort.sort(i32, list.items, {}, comptime std.sort.asc(i32)); - ``` - -## 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.__nums: 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.__nums[index] - - def set(self, num: int, index: int): - """更新元素""" - if index < 0 or index >= self.__size: - raise IndexError("索引越界") - self.__nums[index] = num - - def add(self, num: int): - """尾部添加元素""" - # 元素数量超出容量时,触发扩容机制 - if self.size() == self.capacity(): - self.extend_capacity() - self.__nums[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.__nums[j + 1] = self.__nums[j] - self.__nums[index] = num - # 更新元素数量 - self.__size += 1 - - def remove(self, index: int) -> int: - """删除元素""" - if index < 0 or index >= self.__size: - raise IndexError("索引越界") - num = self.__nums[index] - # 索引 i 之后的元素都向前移动一位 - for j in range(index, self.__size - 1): - self.__nums[j] = self.__nums[j + 1] - # 更新元素数量 - self.__size -= 1 - # 返回被删除元素 - return num - - def extend_capacity(self): - """列表扩容""" - # 新建一个长度为原数组 __extend_ratio 倍的新数组,并将原数组拷贝到新数组 - self.__nums = self.__nums + [0] * self.capacity() * (self.__extend_ratio - 1) - # 更新列表容量 - self.__capacity = len(self.__nums) - - def to_array(self) -> list[int]: - """返回有效长度的列表""" - return self.__nums[: self.__size] - ``` - -=== "C++" - - ```cpp title="my_list.cpp" - /* 列表类简易实现 */ - class MyList { - private: - int *nums; // 数组(存储列表元素) - int numsCapacity = 10; // 列表容量 - int numsSize = 0; // 列表长度(即当前元素数量) - int extendRatio = 2; // 每次列表扩容的倍数 - - public: - /* 构造方法 */ - MyList() { - nums = new int[numsCapacity]; - } - - /* 析构方法 */ - ~MyList() { - delete[] nums; - } - - /* 获取列表长度(即当前元素数量)*/ - int size() { - return numsSize; - } - - /* 获取列表容量 */ - int capacity() { - return numsCapacity; - } - - /* 访问元素 */ - int get(int index) { - // 索引如果越界则抛出异常,下同 - if (index < 0 || index >= size()) - throw out_of_range("索引越界"); - return nums[index]; - } - - /* 更新元素 */ - void set(int index, int num) { - if (index < 0 || index >= size()) - throw out_of_range("索引越界"); - nums[index] = num; - } - - /* 尾部添加元素 */ - void add(int num) { - // 元素数量超出容量时,触发扩容机制 - if (size() == capacity()) - extendCapacity(); - nums[size()] = num; - // 更新元素数量 - numsSize++; - } - - /* 中间插入元素 */ - 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--) { - nums[j + 1] = nums[j]; - } - nums[index] = num; - // 更新元素数量 - numsSize++; - } - - /* 删除元素 */ - int remove(int index) { - if (index < 0 || index >= size()) - throw out_of_range("索引越界"); - int num = nums[index]; - // 索引 i 之后的元素都向前移动一位 - for (int j = index; j < size() - 1; j++) { - nums[j] = nums[j + 1]; - } - // 更新元素数量 - numsSize--; - // 返回被删除元素 - return num; - } - - /* 列表扩容 */ - void extendCapacity() { - // 新建一个长度为原数组 extendRatio 倍的新数组 - int newCapacity = capacity() * extendRatio; - int *tmp = nums; - nums = new int[newCapacity]; - // 将原数组中的所有元素复制到新数组 - for (int i = 0; i < size(); i++) { - nums[i] = tmp[i]; - } - // 释放内存 - delete[] tmp; - numsCapacity = newCapacity; - } - - /* 将列表转换为 Vector 用于打印 */ - vector toVector() { - // 仅转换有效长度范围内的列表元素 - vector vec(size()); - for (int i = 0; i < size(); i++) { - vec[i] = nums[i]; - } - return vec; - } - }; - ``` - -=== "Java" - - ```java title="my_list.java" - /* 列表类简易实现 */ - class MyList { - private int[] nums; // 数组(存储列表元素) - private int capacity = 10; // 列表容量 - private int size = 0; // 列表长度(即当前元素数量) - private int extendRatio = 2; // 每次列表扩容的倍数 - - /* 构造方法 */ - public MyList() { - nums = 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 nums[index]; - } - - /* 更新元素 */ - public void set(int index, int num) { - if (index < 0 || index >= size) - throw new IndexOutOfBoundsException("索引越界"); - nums[index] = num; - } - - /* 尾部添加元素 */ - public void add(int num) { - // 元素数量超出容量时,触发扩容机制 - if (size == capacity()) - extendCapacity(); - nums[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--) { - nums[j + 1] = nums[j]; - } - nums[index] = num; - // 更新元素数量 - size++; - } - - /* 删除元素 */ - public int remove(int index) { - if (index < 0 || index >= size) - throw new IndexOutOfBoundsException("索引越界"); - int num = nums[index]; - // 将索引 index 之后的元素都向前移动一位 - for (int j = index; j < size - 1; j++) { - nums[j] = nums[j + 1]; - } - // 更新元素数量 - size--; - // 返回被删除元素 - return num; - } - - /* 列表扩容 */ - public void extendCapacity() { - // 新建一个长度为原数组 extendRatio 倍的新数组,并将原数组拷贝到新数组 - nums = Arrays.copyOf(nums, capacity() * extendRatio); - // 更新列表容量 - capacity = nums.length; - } - - /* 将列表转换为数组 */ - public int[] toArray() { - int size = size(); - // 仅转换有效长度范围内的列表元素 - int[] nums = new int[size]; - for (int i = 0; i < size; i++) { - nums[i] = get(i); - } - return nums; - } - } - ``` - -=== "C#" - - ```csharp title="my_list.cs" - /* 列表类简易实现 */ - class MyList { - private int[] nums; // 数组(存储列表元素) - private int numsCapacity = 10; // 列表容量 - private int numsSize = 0; // 列表长度(即当前元素数量) - private int extendRatio = 2; // 每次列表扩容的倍数 - - /* 构造方法 */ - public MyList() { - nums = new int[numsCapacity]; - } - - /* 获取列表长度(即当前元素数量)*/ - public int size() { - return numsSize; - } - - /* 获取列表容量 */ - public int capacity() { - return numsCapacity; - } - - /* 访问元素 */ - public int get(int index) { - // 索引如果越界则抛出异常,下同 - if (index < 0 || index >= numsSize) - throw new IndexOutOfRangeException("索引越界"); - return nums[index]; - } - - /* 更新元素 */ - public void set(int index, int num) { - if (index < 0 || index >= numsSize) - throw new IndexOutOfRangeException("索引越界"); - nums[index] = num; - } - - /* 尾部添加元素 */ - public void add(int num) { - // 元素数量超出容量时,触发扩容机制 - if (numsSize == numsCapacity) - extendCapacity(); - nums[numsSize] = num; - // 更新元素数量 - numsSize++; - } - - /* 中间插入元素 */ - public void insert(int index, int num) { - if (index < 0 || index >= numsSize) - throw new IndexOutOfRangeException("索引越界"); - // 元素数量超出容量时,触发扩容机制 - if (numsSize == numsCapacity) - extendCapacity(); - // 将索引 index 以及之后的元素都向后移动一位 - for (int j = numsSize - 1; j >= index; j--) { - nums[j + 1] = nums[j]; - } - nums[index] = num; - // 更新元素数量 - numsSize++; - } - - /* 删除元素 */ - public int remove(int index) { - if (index < 0 || index >= numsSize) - throw new IndexOutOfRangeException("索引越界"); - int num = nums[index]; - // 将索引 index 之后的元素都向前移动一位 - for (int j = index; j < numsSize - 1; j++) { - nums[j] = nums[j + 1]; - } - // 更新元素数量 - numsSize--; - // 返回被删除元素 - return num; - } - - /* 列表扩容 */ - public void extendCapacity() { - // 新建一个长度为 numsCapacity * extendRatio 的数组,并将原数组拷贝到新数组 - Array.Resize(ref nums, numsCapacity * extendRatio); - // 更新列表容量 - numsCapacity = nums.Length; - } - - /* 将列表转换为数组 */ - public int[] toArray() { - // 仅转换有效长度范围内的列表元素 - int[] nums = new int[numsSize]; - for (int i = 0; i < numsSize; i++) { - nums[i] = get(i); - } - return nums; - } - } - ``` - -=== "Go" - - ```go title="my_list.go" - /* 列表类简易实现 */ - type myList struct { - numsCapacity int - nums []int - numsSize int - extendRatio int - } - - /* 构造函数 */ - func newMyList() *myList { - return &myList{ - numsCapacity: 10, // 列表容量 - nums: make([]int, 10), // 数组(存储列表元素) - numsSize: 0, // 列表长度(即当前元素数量) - extendRatio: 2, // 每次列表扩容的倍数 - } - } - - /* 获取列表长度(即当前元素数量) */ - func (l *myList) size() int { - return l.numsSize - } - - /* 获取列表容量 */ - func (l *myList) capacity() int { - return l.numsCapacity - } - - /* 访问元素 */ - func (l *myList) get(index int) int { - // 索引如果越界则抛出异常,下同 - if index < 0 || index >= l.numsSize { - panic("索引越界") - } - return l.nums[index] - } - - /* 更新元素 */ - func (l *myList) set(num, index int) { - if index < 0 || index >= l.numsSize { - panic("索引越界") - } - l.nums[index] = num - } - - /* 尾部添加元素 */ - func (l *myList) add(num int) { - // 元素数量超出容量时,触发扩容机制 - if l.numsSize == l.numsCapacity { - l.extendCapacity() - } - l.nums[l.numsSize] = num - // 更新元素数量 - l.numsSize++ - } - - /* 中间插入元素 */ - func (l *myList) insert(num, index int) { - if index < 0 || index >= l.numsSize { - panic("索引越界") - } - // 元素数量超出容量时,触发扩容机制 - if l.numsSize == l.numsCapacity { - l.extendCapacity() - } - // 将索引 index 以及之后的元素都向后移动一位 - for j := l.numsSize - 1; j >= index; j-- { - l.nums[j+1] = l.nums[j] - } - l.nums[index] = num - // 更新元素数量 - l.numsSize++ - } - - /* 删除元素 */ - func (l *myList) remove(index int) int { - if index < 0 || index >= l.numsSize { - panic("索引越界") - } - num := l.nums[index] - // 索引 i 之后的元素都向前移动一位 - for j := index; j < l.numsSize-1; j++ { - l.nums[j] = l.nums[j+1] - } - // 更新元素数量 - l.numsSize-- - // 返回被删除元素 - return num - } - - /* 列表扩容 */ - func (l *myList) extendCapacity() { - // 新建一个长度为原数组 extendRatio 倍的新数组,并将原数组拷贝到新数组 - l.nums = append(l.nums, make([]int, l.numsCapacity*(l.extendRatio-1))...) - // 更新列表容量 - l.numsCapacity = len(l.nums) - } - - /* 返回有效长度的列表 */ - func (l *myList) toArray() []int { - // 仅转换有效长度范围内的列表元素 - return l.nums[:l.numsSize] - } - ``` - -=== "Swift" - - ```swift title="my_list.swift" - /* 列表类简易实现 */ - class MyList { - private var nums: [Int] // 数组(存储列表元素) - private var _capacity = 10 // 列表容量 - private var _size = 0 // 列表长度(即当前元素数量) - private let extendRatio = 2 // 每次列表扩容的倍数 - - /* 构造方法 */ - init() { - nums = 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 nums[index] - } - - /* 更新元素 */ - func set(index: Int, num: Int) { - if index < 0 || index >= _size { - fatalError("索引越界") - } - nums[index] = num - } - - /* 尾部添加元素 */ - func add(num: Int) { - // 元素数量超出容量时,触发扩容机制 - if _size == _capacity { - extendCapacity() - } - nums[_size] = num - // 更新元素数量 - _size += 1 - } - - /* 中间插入元素 */ - func insert(index: Int, num: Int) { - if index < 0 || index >= _size { - fatalError("索引越界") - } - // 元素数量超出容量时,触发扩容机制 - if _size == _capacity { - extendCapacity() - } - // 将索引 index 以及之后的元素都向后移动一位 - for j in sequence(first: _size - 1, next: { $0 >= index + 1 ? $0 - 1 : nil }) { - nums[j + 1] = nums[j] - } - nums[index] = num - // 更新元素数量 - _size += 1 - } - - /* 删除元素 */ - @discardableResult - func remove(index: Int) -> Int { - if index < 0 || index >= _size { - fatalError("索引越界") - } - let num = nums[index] - // 将索引 index 之后的元素都向前移动一位 - for j in index ..< (_size - 1) { - nums[j] = nums[j + 1] - } - // 更新元素数量 - _size -= 1 - // 返回被删除元素 - return num - } - - /* 列表扩容 */ - func extendCapacity() { - // 新建一个长度为原数组 extendRatio 倍的新数组,并将原数组拷贝到新数组 - nums = nums + Array(repeating: 0, count: _capacity * (extendRatio - 1)) - // 更新列表容量 - _capacity = nums.count - } - - /* 将列表转换为数组 */ - func toArray() -> [Int] { - var nums = Array(repeating: 0, count: _size) - for i in 0 ..< _size { - nums[i] = get(index: i) - } - return nums - } - } - ``` - -=== "JS" - - ```javascript title="my_list.js" - /* 列表类简易实现 */ - class MyList { - #nums = new Array(); // 数组(存储列表元素) - #capacity = 10; // 列表容量 - #size = 0; // 列表长度(即当前元素数量) - #extendRatio = 2; // 每次列表扩容的倍数 - - /* 构造方法 */ - constructor() { - this.#nums = 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.#nums[index]; - } - - /* 更新元素 */ - set(index, num) { - if (index < 0 || index >= this.#size) throw new Error('索引越界'); - this.#nums[index] = num; - } - - /* 尾部添加元素 */ - add(num) { - // 如果长度等于容量,则需要扩容 - if (this.#size === this.#capacity) { - this.extendCapacity(); - } - // 将新元素添加到列表尾部 - this.#nums[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.#nums[j + 1] = this.#nums[j]; - } - // 更新元素数量 - this.#nums[index] = num; - this.#size++; - } - - /* 删除元素 */ - remove(index) { - if (index < 0 || index >= this.#size) throw new Error('索引越界'); - let num = this.#nums[index]; - // 将索引 index 之后的元素都向前移动一位 - for (let j = index; j < this.#size - 1; j++) { - this.#nums[j] = this.#nums[j + 1]; - } - // 更新元素数量 - this.#size--; - // 返回被删除元素 - return num; - } - - /* 列表扩容 */ - extendCapacity() { - // 新建一个长度为原数组 extendRatio 倍的新数组,并将原数组拷贝到新数组 - this.#nums = this.#nums.concat( - new Array(this.capacity() * (this.#extendRatio - 1)) - ); - // 更新列表容量 - this.#capacity = this.#nums.length; - } - - /* 将列表转换为数组 */ - toArray() { - let size = this.size(); - // 仅转换有效长度范围内的列表元素 - const nums = new Array(size); - for (let i = 0; i < size; i++) { - nums[i] = this.get(i); - } - return nums; - } - } - ``` - -=== "TS" - - ```typescript title="my_list.ts" - /* 列表类简易实现 */ - class MyList { - private nums: Array; // 数组(存储列表元素) - private _capacity: number = 10; // 列表容量 - private _size: number = 0; // 列表长度(即当前元素数量) - private extendRatio: number = 2; // 每次列表扩容的倍数 - - /* 构造方法 */ - constructor() { - this.nums = 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.nums[index]; - } - - /* 更新元素 */ - public set(index: number, num: number): void { - if (index < 0 || index >= this._size) throw new Error('索引越界'); - this.nums[index] = num; - } - - /* 尾部添加元素 */ - public add(num: number): void { - // 如果长度等于容量,则需要扩容 - if (this._size === this._capacity) this.extendCapacity(); - // 将新元素添加到列表尾部 - this.nums[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.nums[j + 1] = this.nums[j]; - } - // 更新元素数量 - this.nums[index] = num; - this._size++; - } - - /* 删除元素 */ - public remove(index: number): number { - if (index < 0 || index >= this._size) throw new Error('索引越界'); - let num = this.nums[index]; - // 将索引 index 之后的元素都向前移动一位 - for (let j = index; j < this._size - 1; j++) { - this.nums[j] = this.nums[j + 1]; - } - // 更新元素数量 - this._size--; - // 返回被删除元素 - return num; - } - - /* 列表扩容 */ - public extendCapacity(): void { - // 新建一个长度为 size 的数组,并将原数组拷贝到新数组 - this.nums = this.nums.concat( - new Array(this.capacity() * (this.extendRatio - 1)) - ); - // 更新列表容量 - this._capacity = this.nums.length; - } - - /* 将列表转换为数组 */ - public toArray(): number[] { - let size = this.size(); - // 仅转换有效长度范围内的列表元素 - const nums = new Array(size); - for (let i = 0; i < size; i++) { - nums[i] = this.get(i); - } - return nums; - } - } - ``` - -=== "Dart" - - ```dart title="my_list.dart" - /* 列表类简易实现 */ - class MyList { - late List _nums; // 数组(存储列表元素) - int _capacity = 10; // 列表容量 - int _size = 0; // 列表长度(即当前元素数量) - int _extendRatio = 2; // 每次列表扩容的倍数 - - /* 构造方法 */ - MyList() { - _nums = List.filled(_capacity, 0); - } - - /* 获取列表长度(即当前元素数量)*/ - int size() => _size; - - /* 获取列表容量 */ - int capacity() => _capacity; - - /* 访问元素 */ - int get(int index) { - if (index >= _size) throw RangeError('索引越界'); - return _nums[index]; - } - - /* 更新元素 */ - void set(int index, int num) { - if (index >= _size) throw RangeError('索引越界'); - _nums[index] = num; - } - - /* 尾部添加元素 */ - void add(int num) { - // 元素数量超出容量时,触发扩容机制 - if (_size == _capacity) extendCapacity(); - _nums[_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--) { - _nums[j + 1] = _nums[j]; - } - _nums[index] = num; - // 更新元素数量 - _size++; - } - - /* 删除元素 */ - int remove(int index) { - if (index >= _size) throw RangeError('索引越界'); - int num = _nums[index]; - // 将索引 index 之后的元素都向前移动一位 - for (var j = index; j < _size - 1; j++) { - _nums[j] = _nums[j + 1]; - } - // 更新元素数量 - _size--; - // 返回被删除元素 - return num; - } - - /* 列表扩容 */ - void extendCapacity() { - // 新建一个长度为原数组 _extendRatio 倍的新数组 - final _newNums = List.filled(_capacity * _extendRatio, 0); - // 将原数组拷贝到新数组 - List.copyRange(_newNums, 0, _nums); - // 更新 _nums 的引用 - _nums = _newNums; - // 更新列表容量 - _capacity = _nums.length; - } - - /* 将列表转换为数组 */ - List toArray() { - List nums = []; - for (var i = 0; i < _size; i++) { - nums.add(get(i)); - } - return nums; - } - } - ``` - -=== "Rust" - - ```rust title="my_list.rs" - /* 列表类简易实现 */ - #[allow(dead_code)] - struct MyList { - nums: 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 { - nums: 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 < 0 || index >= self.size {panic!("索引越界")}; - return self.nums[index]; - } - - /* 更新元素 */ - pub fn set(&mut self, index: usize, num: i32) { - if index < 0 || index >= self.size {panic!("索引越界")}; - self.nums[index] = num; - } - - /* 尾部添加元素 */ - pub fn add(&mut self, num: i32) { - // 元素数量超出容量时,触发扩容机制 - if self.size == self.capacity() { - self.extend_capacity(); - } - self.nums[self.size] = num; - // 更新元素数量 - self.size += 1; - } - - /* 中间插入元素 */ - pub fn insert(&mut self, index: usize, num: i32) { - if index < 0 || index >= self.size() {panic!("索引越界")}; - // 元素数量超出容量时,触发扩容机制 - if self.size == self.capacity() { - self.extend_capacity(); - } - // 将索引 index 以及之后的元素都向后移动一位 - for j in (index..self.size).rev() { - self.nums[j + 1] = self.nums[j]; - } - self.nums[index] = num; - // 更新元素数量 - self.size += 1; - } - - /* 删除元素 */ - pub fn remove(&mut self, index: usize) -> i32 { - if index < 0 || index >= self.size() {panic!("索引越界")}; - let num = self.nums[index]; - // 将索引 index 之后的元素都向前移动一位 - for j in (index..self.size - 1) { - self.nums[j] = self.nums[j + 1]; - } - // 更新元素数量 - self.size -= 1; - // 返回被删除元素 - return num; - } - - /* 列表扩容 */ - pub fn extend_capacity(&mut self) { - // 新建一个长度为原数组 extend_ratio 倍的新数组,并将原数组拷贝到新数组 - let new_capacity = self.capacity * self.extend_ratio; - self.nums.resize(new_capacity, 0); - // 更新列表容量 - self.capacity = new_capacity; - } - - /* 将列表转换为数组 */ - pub fn to_array(&mut self) -> Vec { - // 仅转换有效长度范围内的列表元素 - let mut nums = Vec::new(); - for i in 0..self.size { - nums.push(self.get(i)); - } - nums - } - } - ``` - -=== "C" - - ```c title="my_list.c" - /* 列表类简易实现 */ - struct myList { - int *nums; // 数组(存储列表元素) - int capacity; // 列表容量 - int size; // 列表大小 - int extendRatio; // 列表每次扩容的倍数 - }; - - typedef struct myList myList; - - /* 构造函数 */ - myList *newMyList() { - myList *list = malloc(sizeof(myList)); - list->capacity = 10; - list->nums = malloc(sizeof(int) * list->capacity); - list->size = 0; - list->extendRatio = 2; - return list; - } - - /* 析构函数 */ - void delMyList(myList *list) { - free(list->nums); - free(list); - } - - /* 获取列表长度 */ - int size(myList *list) { - return list->size; - } - - /* 获取列表容量 */ - int capacity(myList *list) { - return list->capacity; - } - - /* 访问元素 */ - int get(myList *list, int index) { - assert(index >= 0 && index < list->size); - return list->nums[index]; - } - - /* 更新元素 */ - void set(myList *list, int index, int num) { - assert(index >= 0 && index < list->size); - list->nums[index] = num; - } - - /* 尾部添加元素 */ - void add(myList *list, int num) { - if (size(list) == capacity(list)) { - extendCapacity(list); // 扩容 - } - list->nums[size(list)] = num; - list->size++; - } - - /* 中间插入元素 */ - void insert(myList *list, int index, int num) { - assert(index >= 0 && index < size(list)); - // 元素数量超出容量时,触发扩容机制 - if (size(list) == capacity(list)) { - extendCapacity(list); // 扩容 - } - for (int i = size(list); i > index; --i) { - list->nums[i] = list->nums[i - 1]; - } - list->nums[index] = num; - list->size++; - } - - /* 删除元素 */ - // 注意:stdio.h 占用了 remove 关键词 - int removeNum(myList *list, int index) { - assert(index >= 0 && index < size(list)); - int num = list->nums[index]; - for (int i = index; i < size(list) - 1; i++) { - list->nums[i] = list->nums[i + 1]; - } - list->size--; - return num; - } - - /* 列表扩容 */ - void extendCapacity(myList *list) { - // 先分配空间 - int newCapacity = capacity(list) * list->extendRatio; - int *extend = (int *)malloc(sizeof(int) * newCapacity); - int *temp = list->nums; - - // 拷贝旧数据到新数据 - for (int i = 0; i < size(list); i++) - extend[i] = list->nums[i]; - - // 释放旧数据 - free(temp); - - // 更新新数据 - list->nums = extend; - list->capacity = newCapacity; - } - - /* 将列表转换为 Array 用于打印 */ - int *toArray(myList *list) { - return list->nums; - } - ``` - -=== "Zig" - - ```zig title="my_list.zig" - // 列表类简易实现 - fn MyList(comptime T: type) type { - return struct { - const Self = @This(); - - nums: []T = undefined, // 数组(存储列表元素) - numsCapacity: 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.nums = try self.mem_allocator.alloc(T, self.numsCapacity); - @memset(self.nums, @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.numsCapacity; - } - - // 访问元素 - pub fn get(self: *Self, index: usize) T { - // 索引如果越界则抛出异常,下同 - if (index < 0 or index >= self.size()) @panic("索引越界"); - return self.nums[index]; - } - - // 更新元素 - pub fn set(self: *Self, index: usize, num: T) void { - // 索引如果越界则抛出异常,下同 - if (index < 0 or index >= self.size()) @panic("索引越界"); - self.nums[index] = num; - } - - // 尾部添加元素 - pub fn add(self: *Self, num: T) !void { - // 元素数量超出容量时,触发扩容机制 - if (self.size() == self.capacity()) try self.extendCapacity(); - self.nums[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.nums[j + 1] = self.nums[j]; - } - self.nums[index] = num; - // 更新元素数量 - self.numSize += 1; - } - - // 删除元素 - pub fn remove(self: *Self, index: usize) T { - if (index < 0 or index >= self.size()) @panic("索引越界"); - var num = self.nums[index]; - // 索引 i 之后的元素都向前移动一位 - var j = index; - while (j < self.size() - 1) : (j += 1) { - self.nums[j] = self.nums[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.nums); - self.nums = extend; - // 更新列表容量 - self.numsCapacity = newCapacity; - } - - // 将列表转换为数组 - pub fn toArray(self: *Self) ![]T { - // 仅转换有效长度范围内的列表元素 - var nums = try self.mem_allocator.alloc(T, self.size()); - @memset(nums, @as(T, 0)); - for (nums, 0..) |*num, i| { - num.* = self.get(i); - } - return nums; - } - }; - } - ``` diff --git a/chapter_backtracking/backtracking_algorithm.md b/chapter_backtracking/backtracking_algorithm.md deleted file mode 100644 index b7e0fa514..000000000 --- a/chapter_backtracking/backtracking_algorithm.md +++ /dev/null @@ -1,1729 +0,0 @@ ---- -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" - [class]{}-[func]{preOrder} - ``` - -=== "Zig" - - ```zig title="preorder_traversal_i_compact.zig" - [class]{}-[func]{preOrder} - ``` - -![在前序遍历中搜索节点](backtracking_algorithm.assets/preorder_find_nodes.png) - -

图 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, *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, vector *path, vector *res) { - if (root == NULL) { - return; - } - // 尝试 - vectorPushback(path, root, sizeof(TreeNode)); - if (root->val == 7) { - // 记录解 - vector *newPath = newVector(); - for (int i = 0; i < path->size; i++) { - vectorPushback(newPath, path->data[i], sizeof(int)); - } - vectorPushback(res, newPath, sizeof(vector)); - } - - preOrder(root->left, path, res); - preOrder(root->right, path, res); - - // 回退 - vectorPopback(path); - } - ``` - -=== "Zig" - - ```zig title="preorder_traversal_ii_compact.zig" - [class]{}-[func]{preOrder} - ``` - -在每次“尝试”中,我们通过将当前节点添加进 `path` 来记录路径;而在“回退”前,我们需要将该节点从 `path` 中弹出,**以恢复本次尝试之前的状态**。 - -观察图 13-2 所示的过程,**我们可以将尝试和回退理解为“前进”与“撤销”**,两个操作是互为逆向的。 - -=== "<1>" - ![尝试与回退](backtracking_algorithm.assets/preorder_find_paths_step1.png) - -=== "<2>" - ![preorder_find_paths_step2](backtracking_algorithm.assets/preorder_find_paths_step2.png) - -=== "<3>" - ![preorder_find_paths_step3](backtracking_algorithm.assets/preorder_find_paths_step3.png) - -=== "<4>" - ![preorder_find_paths_step4](backtracking_algorithm.assets/preorder_find_paths_step4.png) - -=== "<5>" - ![preorder_find_paths_step5](backtracking_algorithm.assets/preorder_find_paths_step5.png) - -=== "<6>" - ![preorder_find_paths_step6](backtracking_algorithm.assets/preorder_find_paths_step6.png) - -=== "<7>" - ![preorder_find_paths_step7](backtracking_algorithm.assets/preorder_find_paths_step7.png) - -=== "<8>" - ![preorder_find_paths_step8](backtracking_algorithm.assets/preorder_find_paths_step8.png) - -=== "<9>" - ![preorder_find_paths_step9](backtracking_algorithm.assets/preorder_find_paths_step9.png) - -=== "<10>" - ![preorder_find_paths_step10](backtracking_algorithm.assets/preorder_find_paths_step10.png) - -=== "<11>" - ![preorder_find_paths_step11](backtracking_algorithm.assets/preorder_find_paths_step11.png) - -

图 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, *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, vector *path, vector *res) { - // 剪枝 - if (root == NULL || root->val == 3) { - return; - } - // 尝试 - vectorPushback(path, root, sizeof(TreeNode)); - if (root->val == 7) { - // 记录解 - vector *newPath = newVector(); - for (int i = 0; i < path->size; i++) { - vectorPushback(newPath, path->data[i], sizeof(int)); - } - vectorPushback(res, newPath, sizeof(vector)); - res->depth++; - } - - preOrder(root->left, path, res); - preOrder(root->right, path, res); - - // 回退 - vectorPopback(path); - } - ``` - -=== "Zig" - - ```zig title="preorder_traversal_iii_compact.zig" - [class]{}-[func]{preOrder} - ``` - -剪枝是一个非常形象的名词。如图 13-3 所示,在搜索过程中,**我们“剪掉”了不满足约束条件的搜索分支**,避免许多无意义的尝试,从而提高了搜索效率。 - -![根据约束条件剪枝](backtracking_algorithm.assets/preorder_find_constrained_paths.png) - -

图 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]); - } - } - } - ``` - -=== "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, new List { 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, *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(vector *state) { - return state->size != 0 && ((TreeNode *)(state->data[state->size - 1]))->val == 7; - } - - /* 记录解 */ - void recordSolution(vector *state, vector *res) { - vector *newPath = newVector(); - for (int i = 0; i < state->size; i++) { - vectorPushback(newPath, state->data[i], sizeof(int)); - } - vectorPushback(res, newPath, sizeof(vector)); - } - - /* 判断在当前状态下,该选择是否合法 */ - bool isValid(vector *state, TreeNode *choice) { - return choice != NULL && choice->val != 3; - } - - /* 更新状态 */ - void makeChoice(vector *state, TreeNode *choice) { - vectorPushback(state, choice, sizeof(TreeNode)); - } - - /* 恢复状态 */ - void undoChoice(vector *state, TreeNode *choice) { - vectorPopback(state); - } - - /* 回溯算法:例题三 */ - void backtrack(vector *state, vector *choices, vector *res) { - // 检查是否为解 - if (isSolution(state)) { - // 记录解 - recordSolution(state, res); - return; - } - // 遍历所有选择 - for (int i = 0; i < choices->size; i++) { - TreeNode *choice = choices->data[i]; - // 剪枝:检查选择是否合法 - if (isValid(state, choice)) { - // 尝试:做出选择,更新状态 - makeChoice(state, choice); - // 进行下一轮选择 - vector *nextChoices = newVector(); - vectorPushback(nextChoices, choice->left, sizeof(TreeNode)); - vectorPushback(nextChoices, choice->right, sizeof(TreeNode)); - backtrack(state, nextChoices, res); - // 回退:撤销选择,恢复到之前的状态 - undoChoice(state, choice); - } - } - } - ``` - -=== "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} - ``` - -根据题意,我们在找到值为 $7$ 的节点后应该继续搜索,**因此需要将记录解之后的 `return` 语句删除**。图 13-4 对比了保留或删除 `return` 语句的搜索过程。 - -![保留与删除 return 的搜索过程对比](backtracking_algorithm.assets/backtrack_remove_return_or_not.png) - -

图 13-4   保留与删除 return 的搜索过程对比

- -相比基于前序遍历的代码实现,基于回溯算法框架的代码实现虽然显得啰嗦,但通用性更好。实际上,**许多回溯问题都可以在该框架下解决**。我们只需根据具体问题来定义 `state` 和 `choices` ,并实现框架中的各个方法即可。 - -## 13.1.4   常用术语 - -为了更清晰地分析算法问题,我们总结一下回溯算法中常用术语的含义,并对照例题三给出对应示例。 - -

表 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/chapter_backtracking/n_queens_problem.md b/chapter_backtracking/n_queens_problem.md deleted file mode 100644 index 84897e192..000000000 --- a/chapter_backtracking/n_queens_problem.md +++ /dev/null @@ -1,619 +0,0 @@ ---- -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) - -

图 13-15   4 皇后问题的解

- -图 13-16 展示了本题的三个约束条件:**多个皇后不能在同一行、同一列、同一对角线**。值得注意的是,对角线分为主对角线 `\` 和次对角线 `/` 两种。 - -![n 皇后问题的约束条件](n_queens_problem.assets/n_queens_constraints.png) - -

图 13-16   n 皇后问题的约束条件

- -### 1.   逐行放置策略 - -皇后的数量和棋盘的行数都为 $n$ ,因此我们容易得到一个推论:**棋盘每行都允许且只允许放置一个皇后**。 - -也就是说,我们可以采取逐行放置策略:从第一行开始,在每行放置一个皇后,直至最后一行结束。 - -如图 13-17 所示,为 $4$ 皇后问题的逐行放置过程。受画幅限制,图 13-17 仅展开了第一行的其中一个搜索分支,并且将不满足列约束和对角线约束的方案都进行了剪枝。 - -![逐行放置策略](n_queens_problem.assets/n_queens_placing.png) - -

图 13-17   逐行放置策略

- -本质上看,**逐行放置策略起到了剪枝的作用**,它避免了同一行出现多个皇后的所有搜索分支。 - -### 2.   列与对角线剪枝 - -为了满足列约束,我们可以利用一个长度为 $n$ 的布尔型数组 `cols` 记录每一列是否有皇后。在每次决定放置前,我们通过 `cols` 将已有皇后的列进行剪枝,并在回溯中动态更新 `cols` 的状态。 - -那么,如何处理对角线约束呢?设棋盘中某个格子的行列索引为 $(row, col)$ ,选定矩阵中的某条主对角线,我们发现该对角线上所有格子的行索引减列索引都相等,**即对角线上所有格子的 $row - col$ 为恒定值**。 - -也就是说,如果两个格子满足 $row_1 - col_1 = row_2 - col_2$ ,则它们一定处在同一条主对角线上。利用该规律,我们可以借助图 13-18 所示的数组 `diag1` ,记录每条主对角线上是否有皇后。 - -同理,**次对角线上的所有格子的 $row + col$ 是恒定值**。我们同样也可以借助数组 `diag2` 来处理次对角线约束。 - -![处理列约束和对角线约束](n_queens_problem.assets/n_queens_cols_diagonals.png) - -

图 13-18   处理列约束和对角线约束

- -### 3.   代码实现 - -请注意,$n$ 维方阵中 $row - col$ 的范围是 $[-n + 1, n - 1]$ ,$row + col$ 的范围是 $[0, 2n - 2]$ ,所以主对角线和次对角线的数量都为 $2n - 1$ ,即数组 `diag1` 和 `diag2` 的长度都为 $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 = new List>(); - 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 = new List>(); - for (int i = 0; i < n; i++) { - List row = new List(); - 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 = new List>>(); - - 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 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 - } - } - } - - 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" - [class]{}-[func]{backtrack} - - [class]{}-[func]{nQueens} - ``` - -=== "Zig" - - ```zig title="n_queens.zig" - [class]{}-[func]{backtrack} - - [class]{}-[func]{nQueens} - ``` - -逐行放置 $n$ 次,考虑列约束,则从第一行到最后一行分别有 $n$、$n-1$、$\dots$、$2$、$1$ 个选择,**因此时间复杂度为 $O(n!)$** 。实际上,根据对角线约束的剪枝也能够大幅地缩小搜索空间,因而搜索效率往往优于以上时间复杂度。 - -数组 `state` 使用 $O(n^2)$ 空间,数组 `cols`、`diags1` 和 `diags2` 皆使用 $O(n)$ 空间。最大递归深度为 $n$ ,使用 $O(n)$ 栈帧空间。因此,**空间复杂度为 $O(n^2)$** 。 diff --git a/chapter_backtracking/permutations_problem.md b/chapter_backtracking/permutations_problem.md deleted file mode 100644 index ce5898ee3..000000000 --- a/chapter_backtracking/permutations_problem.md +++ /dev/null @@ -1,919 +0,0 @@ ---- -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) - -

图 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) - -

图 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 = new List>(); - backtrack(new List(), 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(vector *state, vector *choices, vector *selected, vector *res) { - // 当状态长度等于元素数量时,记录解 - if (state->size == choices->size) { - vector *newState = newVector(); - for (int i = 0; i < state->size; i++) { - vectorPushback(newState, state->data[i], sizeof(int)); - } - vectorPushback(res, newState, sizeof(vector)); - return; - } - // 遍历所有选择 - for (int i = 0; i < choices->size; i++) { - int *choice = malloc(sizeof(int)); - *choice = *((int *)(choices->data[i])); - // 剪枝:不允许重复选择元素 且 不允许重复选择相等元素 - bool select = *((bool *)(selected->data[i])); - if (!select) { - // 尝试:做出选择,更新状态 - *((bool *)selected->data[i]) = true; - vectorPushback(state, choice, sizeof(int)); - // 进行下一轮选择 - backtrack(state, choices, selected, res); - // 回退:撤销选择,恢复到之前的状态 - *((bool *)selected->data[i]) = false; - vectorPopback(state); - } - } - } - - /* 全排列 I */ - vector *permutationsI(vector *nums) { - vector *iState = newVector(); - - int select[3] = {false, false, false}; - vector *bSelected = newVector(); - for (int i = 0; i < nums->size; i++) { - vectorPushback(bSelected, &select[i], sizeof(int)); - } - - vector *res = newVector(); - - // 前序遍历 - backtrack(iState, nums, bSelected, res); - return res; - } - ``` - -=== "Zig" - - ```zig title="permutations_i.zig" - [class]{}-[func]{backtrack} - - [class]{}-[func]{permutationsI} - ``` - -## 13.2.2   考虑相等元素的情况 - -!!! question - - 输入一个整数数组,**数组中可能包含重复元素**,返回所有不重复的排列。 - -假设输入数组为 $[1, 1, 2]$ 。为了方便区分两个重复元素 $1$ ,我们将第二个 $1$ 记为 $\hat{1}$ 。 - -如图 13-7 所示,上述方法生成的排列有一半都是重复的。 - -![重复排列](permutations_problem.assets/permutations_ii.png) - -

图 13-7   重复排列

- -那么如何去除重复的排列呢?最直接地,考虑借助一个哈希表,直接对排列结果进行去重。然而这样做不够优雅,**因为生成重复排列的搜索分支是没有必要的,应当被提前识别并剪枝**,这样可以进一步提升算法效率。 - -### 1.   相等元素剪枝 - -观察图 13-8 ,在第一轮中,选择 $1$ 或选择 $\hat{1}$ 是等价的,在这两个选择之下生成的所有排列都是重复的。因此应该把 $\hat{1}$ 剪枝掉。 - -同理,在第一轮选择 $2$ 之后,第二轮选择中的 $1$ 和 $\hat{1}$ 也会产生重复分支,因此也应将第二轮的 $\hat{1}$ 剪枝。 - -本质上看,**我们的目标是在某一轮选择中,保证多个相等的元素仅被选择一次**。 - -![重复排列剪枝](permutations_problem.assets/permutations_ii_pruning.png) - -

图 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; - } - // 遍历所有选择 - ISet 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.RemoveAt(state.Count - 1); - } - } - } - - /* 全排列 II */ - List> permutationsII(int[] nums) { - List> res = new List>(); - backtrack(new List(), 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" - [class]{}-[func]{backtrack} - - [class]{}-[func]{permutationsII} - ``` - -=== "Zig" - - ```zig title="permutations_ii.zig" - [class]{}-[func]{backtrack} - - [class]{}-[func]{permutationsII} - ``` - -假设元素两两之间互不相同,则 $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` 。它记录的是在遍历中哪些元素已被选择过,作用是保证相等元素只被选择一次。 - -图 13-9 展示了两个剪枝条件的生效范围。注意,树中的每个节点代表一个选择,从根节点到叶节点的路径上的各个节点构成一个排列。 - -![两种剪枝条件的作用范围](permutations_problem.assets/permutations_ii_pruning_summary.png) - -

图 13-9   两种剪枝条件的作用范围

diff --git a/chapter_backtracking/subset_sum_problem.md b/chapter_backtracking/subset_sum_problem.md deleted file mode 100644 index b00d00da9..000000000 --- a/chapter_backtracking/subset_sum_problem.md +++ /dev/null @@ -1,1441 +0,0 @@ ---- -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 = new List(); // 状态(子集) - int total = 0; // 子集和 - List> res = new List>(); // 结果列表(子集列表) - 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 stride(from: 0, to: choices.count, by: 1) { - // 剪枝:若子集和超过 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(vector *state, int target, int total, vector *choices, vector *res) { - // 子集和等于 target 时,记录解 - if (total == target) { - vector *tmpVector = newVector(); - for (int i = 0; i < state->size; i++) { - vectorPushback(tmpVector, state->data[i], sizeof(int)); - } - vectorPushback(res, tmpVector, sizeof(vector)); - return; - } - // 遍历所有选择 - for (size_t i = 0; i < choices->size; i++) { - // 剪枝:若子集和超过 target ,则跳过该选择 - if (total + *(int *)(choices->data[i]) > target) { - continue; - } - // 尝试:做出选择,更新元素和 total - vectorPushback(state, choices->data[i], sizeof(int)); - // 进行下一轮选择 - backtrack(state, target, total + *(int *)(choices->data[i]), choices, res); - // 回退:撤销选择,恢复到之前的状态 - vectorPopback(state); - } - } - - /* 求解子集和 I(包含重复子集) */ - vector *subsetSumINaive(vector *nums, int target) { - vector *state = newVector(); // 状态(子集) - int total = 0; // 子集和 - vector *res = newVector(); // 结果列表(子集列表) - backtrack(state, target, total, nums, res); - return res; - } - ``` - -=== "Zig" - - ```zig title="subset_sum_i_naive.zig" - [class]{}-[func]{backtrack} - - [class]{}-[func]{subsetSumINaive} - ``` - -向以上代码输入数组 $[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) - -

图 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) - -

图 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 = new List(); // 状态(子集) - Array.Sort(nums); // 对 nums 进行排序 - int start = 0; // 遍历起始点 - List> res = new List>(); // 结果列表(子集列表) - 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 stride(from: start, to: choices.count, by: 1) { - // 剪枝一:若子集和超过 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(vector *state, int target, vector *choices, int start, vector *res) { - // 子集和等于 target 时,记录解 - if (target == 0) { - vector *tmpVector = newVector(); - for (int i = 0; i < state->size; i++) { - vectorPushback(tmpVector, state->data[i], sizeof(int)); - } - vectorPushback(res, tmpVector, sizeof(vector)); - return; - } - // 遍历所有选择 - // 剪枝二:从 start 开始遍历,避免生成重复子集 - for (int i = start; i < choices->size; i++) { - // 剪枝:若子集和超过 target ,则跳过该选择 - if (target - *(int *)(choices->data[i]) < 0) { - break; - } - // 尝试:做出选择,更新 target, start - vectorPushback(state, choices->data[i], sizeof(int)); - // 进行下一轮选择 - backtrack(state, target - *(int *)(choices->data[i]), choices, i, res); - // 回退:撤销选择,恢复到之前的状态 - vectorPopback(state); - } - } - - /* 求解子集和 I */ - vector *subsetSumI(vector *nums, int target) { - vector *state = newVector(); // 状态(子集) - qsort(nums->data, nums->size, sizeof(int *), comp); // 对 nums 进行排序 - int start = 0; // 子集和 - vector *res = newVector(); // 结果列表(子集列表) - backtrack(state, target, nums, start, res); - return res; - } - ``` - -=== "Zig" - - ```zig title="subset_sum_i.zig" - [class]{}-[func]{backtrack} - - [class]{}-[func]{subsetSumI} - ``` - -如图 13-12 所示,为将数组 $[3, 4, 5]$ 和目标元素 $9$ 输入到以上代码后的整体回溯过程。 - -![子集和 I 回溯过程](subset_sum_problem.assets/subset_sum_i.png) - -

图 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) - -

图 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 = new List(); // 状态(子集) - Array.Sort(nums); // 对 nums 进行排序 - int start = 0; // 遍历起始点 - List> res = new List>(); // 结果列表(子集列表) - 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 stride(from: start, to: choices.count, by: 1) { - // 剪枝一:若子集和超过 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(vector *state, int target, vector *choices, int start, vector *res) { - // 子集和等于 target 时,记录解 - if (target == 0) { - vector *tmpVector = newVector(); - for (int i = 0; i < state->size; i++) { - vectorPushback(tmpVector, state->data[i], sizeof(int)); - } - vectorPushback(res, tmpVector, sizeof(vector)); - return; - } - // 遍历所有选择 - // 剪枝二:从 start 开始遍历,避免生成重复子集 - // 剪枝三:从 start 开始遍历,避免重复选择同一元素 - for (int i = start; i < choices->size; i++) { - // 剪枝一:若子集和超过 target ,则直接结束循环 - // 这是因为数组已排序,后边元素更大,子集和一定超过 target - if (target - *(int *)(choices->data[i]) < 0) { - continue; - } - // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过 - if (i > start && *(int *)(choices->data[i]) == *(int *)(choices->data[i - 1])) { - continue; - } - // 尝试:做出选择,更新 target, start - vectorPushback(state, choices->data[i], sizeof(int)); - // 进行下一轮选择 - backtrack(state, target - *(int *)(choices->data[i]), choices, i + 1, res); - // 回退:撤销选择,恢复到之前的状态 - vectorPopback(state); - } - } - - /* 求解子集和 II */ - vector *subsetSumII(vector *nums, int target) { - vector *state = newVector(); // 状态(子集) - qsort(nums->data, nums->size, sizeof(int *), comp); // 对 nums 进行排序 - int start = 0; // 子集和 - vector *res = newVector(); // 结果列表(子集列表) - backtrack(state, target, nums, start, res); - return res; - } - ``` - -=== "Zig" - - ```zig title="subset_sum_ii.zig" - [class]{}-[func]{backtrack} - - [class]{}-[func]{subsetSumII} - ``` - -图 13-14 展示了数组 $[4, 4, 5]$ 和目标元素 $9$ 的回溯过程,共包含四种剪枝操作。请你将图示与代码注释相结合,理解整个搜索过程,以及每种剪枝操作是如何工作的。 - -![子集和 II 回溯过程](subset_sum_problem.assets/subset_sum_ii.png) - -

图 13-14   子集和 II 回溯过程

diff --git a/chapter_computational_complexity/iteration_and_recursion.md b/chapter_computational_complexity/iteration_and_recursion.md deleted file mode 100644 index 909dfcdd5..000000000 --- a/chapter_computational_complexity/iteration_and_recursion.md +++ /dev/null @@ -1,1631 +0,0 @@ ---- -comments: true -status: new ---- - -# 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; - } - ``` - -=== "Zig" - - ```zig title="iteration.zig" - [class]{}-[func]{forLoop} - ``` - -图 2-1 展示了该求和函数的流程框图。 - -![求和函数的流程框图](iteration_and_recursion.assets/iteration.png) - -

图 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; - } - ``` - -=== "Zig" - - ```zig title="iteration.zig" - [class]{}-[func]{whileLoop} - ``` - -在 `while` 循环中,由于初始化和更新条件变量的步骤是独立在循环结构之外的,**因此它比 `for` 循环的自由度更高**。 - -例如在以下代码中,条件变量 $i$ 每轮进行了两次更新,这种情况就不太方便用 `for` 循环实现。 - -=== "Python" - - ```python title="iteration.py" - def while_loop_ii(n: int) -> int: - """while 循环(两次更新)""" - res = 0 - i = 1 # 初始化条件变量 - # 循环求和 1, 4, ... - 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, ... - 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, ... - 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, 2, 4, 5... - 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, ... - 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, ... - 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, ... - 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, ... - 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, ... - 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, ... - 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, ... - while (i <= n) { - res += i; - // 更新条件变量 - i++; - i *= 2; - } - return res; - } - ``` - -=== "Zig" - - ```zig title="iteration.zig" - [class]{}-[func]{whileLoopII} - ``` - -总的来说,**`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 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(); - } - ``` - -=== "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; - } - ``` - -=== "Zig" - - ```zig title="iteration.zig" - [class]{}-[func]{nestedForLoop} - ``` - -图 2-2 给出了该嵌套循环的流程框图。 - -![嵌套循环的流程框图](iteration_and_recursion.assets/nested_iteration.png) - -

图 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; - } - ``` - -=== "Zig" - - ```zig title="recursion.zig" - [class]{}-[func]{recur} - ``` - -图 2-3 展示了该函数的递归过程。 - -![求和函数的递归过程](iteration_and_recursion.assets/recursion_sum.png) - -

图 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) - -

图 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); - } - ``` - -=== "Zig" - - ```zig title="recursion.zig" - [class]{}-[func]{tailRecur} - ``` - -尾递归的执行过程如图 2-5 所示。对比普通递归和尾递归,求和操作的执行点是不同的。 - -- **普通递归**:求和操作是在“归”的过程中执行的,每层返回后都要再执行一次求和操作。 -- **尾递归**:求和操作是在“递”的过程中执行的,“归”的过程只需层层返回。 - -![尾递归过程](iteration_and_recursion.assets/tail_recursion_sum.png) - -

图 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; - } - ``` - -=== "Zig" - - ```zig title="recursion.zig" - [class]{}-[func]{fib} - ``` - -观察以上代码,我们在函数内递归调用了两个函数,**这意味着从一个调用产生了两个调用分支**。如图 2-6 所示,这样不断递归调用下去,最终将产生一个层数为 $n$ 的「递归树 recursion tree」。 - -![斐波那契数列的递归树](iteration_and_recursion.assets/recursion_tree.png) - -

图 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 Stack(); - 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" - [class]{}-[func]{forLoopRecur} - ``` - -=== "Swift" - - ```swift title="recursion.swift" - /* 使用迭代模拟递归 */ - func forLoopRecur(n: Int) -> Int { - // 使用一个显式的栈来模拟系统调用栈 - var stack: [Int] = [] - var res = 0 - // 递:递归调用 - for i in stride(from: n, to: 0, by: -1) { - // 通过“入栈操作”模拟“递” - 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 = 1; i <= n; 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 = 1; i <= n; 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" - [class]{}-[func]{forLoopRecur} - ``` - -=== "Zig" - - ```zig title="recursion.zig" - [class]{}-[func]{forLoopRecur} - ``` - -观察以上代码,当递归被转换为迭代后,代码变得更加复杂了。尽管迭代和递归在很多情况下可以互相转换,但也不一定值得这样做,有以下两点原因。 - -- 转化后的代码可能更加难以理解,可读性更差。 -- 对于某些复杂问题,模拟系统调用栈的行为可能非常困难。 - -总之,**选择迭代还是递归取决于特定问题的性质**。在编程实践中,权衡两者的优劣并根据情境选择合适的方法是至关重要的。 diff --git a/chapter_computational_complexity/space_complexity.md b/chapter_computational_complexity/space_complexity.md deleted file mode 100755 index d9724e374..000000000 --- a/chapter_computational_complexity/space_complexity.md +++ /dev/null @@ -1,2031 +0,0 @@ ---- -comments: true ---- - -# 2.4   空间复杂度 - -「空间复杂度 space complexity」用于衡量算法占用内存空间随着数据量变大时的增长趋势。这个概念与时间复杂度非常类似,只需将“运行时间”替换为“占用内存空间”。 - -## 2.4.1   算法相关空间 - -算法在运行过程中使用的内存空间主要包括以下几种。 - -- **输入空间**:用于存储算法的输入数据。 -- **暂存空间**:用于存储算法在运行过程中的变量、对象、函数上下文等数据。 -- **输出空间**:用于存储算法的输出数据。 - -一般情况下,空间复杂度的统计范围是“暂存空间”加上“输出空间”。 - -暂存空间可以进一步划分为三个部分。 - -- **暂存数据**:用于保存算法运行过程中的各种常量、变量、对象等。 -- **栈帧空间**:用于保存调用函数的上下文数据。系统在每次调用函数时都会在栈顶部创建一个栈帧,函数返回后,栈帧空间会被释放。 -- **指令空间**:用于保存编译后的程序指令,在实际统计中通常忽略不计。 - -在分析一段程序的空间复杂度时,**我们通常统计暂存数据、栈帧空间和输出数据三部分**。 - -![算法使用的相关空间](space_complexity.assets/space_types.png) - -

图 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 val; - Node next; - Node(int x) { val = x; } - } - - /* 函数 */ - int function() { - // 执行某些操作... - return 0; - } - - int algorithm(int n) { // 输入数据 - const int a = 0; // 暂存数据(常量) - int b = 0; // 暂存数据(变量) - Node node = new Node(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; // 输出数据 - } - ``` - -=== "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) - } - ``` - -=== "Zig" - - ```zig title="" - - ``` - -**在递归函数中,需要注意统计栈帧空间**。例如在以下代码中: - -- 函数 `loop()` 在循环中调用了 $n$ 次 `function()` ,每轮中的 `function()` 都返回并释放了栈帧空间,因此空间复杂度仍为 $O(1)$ 。 -- 递归函数 `recur()` 在运行过程中会同时存在 $n$ 个未返回的 `recur()` ,从而占用 $O(n)$ 的栈帧空间。 - -=== "Python" - - ```python title="" - def function() -> int: - # 执行某些操作 - return 0 - - def loop(n: int): - """循环 O(1)""" - for _ in range(n): - function() - - def recur(n: int) -> 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) */ - void 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); - } - ``` - -=== "Zig" - - ```zig title="" - - ``` - -## 2.4.3   常见类型 - -设输入数据大小为 $n$ ,图 2-16 展示了常见的空间复杂度类型(从低到高排列)。 - -$$ -\begin{aligned} -O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline -\text{常数阶} < \text{对数阶} < \text{线性阶} < \text{平方阶} < \text{指数阶} -\end{aligned} -$$ - -![常见的空间复杂度类型](space_complexity.assets/space_complexity_common_types.png) - -

图 2-16   常见的空间复杂度类型

- -### 1.   常数阶 $O(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 ListNode(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) - ListNode := newNode(0) - // 循环中的变量占用 O(1) 空间 - var c int - for i := 0; i < n; i++ { - c = 0 - } - // 循环中的函数占用 O(1) 空间 - for i := 0; i < n; i++ { - function() - } - fmt.Println(a, b, nums, c, ListNode) - } - ``` - -=== "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(); - } - } - ``` - -=== "Zig" - - ```zig title="space_complexity.zig" - [class]{}-[func]{function} - - // 常数阶 - fn constant(n: i32) void { - // 常量、变量、对象占用 O(1) 空间 - const a: i32 = 0; - var b: i32 = 0; - var nums = [_]i32{0}**10000; - var node = inc.ListNode(i32){.val = 0}; - var i: i32 = 0; - // 循环中的变量占用 O(1) 空间 - while (i < n) : (i += 1) { - var c: i32 = 0; - _ = c; - } - // 循环中的函数占用 O(1) 空间 - i = 0; - while (i < n) : (i += 1) { - _ = function(); - } - _ = a; - _ = b; - _ = nums; - _ = node; - } - ``` - -### 2.   线性阶 $O(n)$ - -线性阶常见于元素数量与 $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 = new(); - for (int i = 0; i < n; i++) { - nodes.Add(new ListNode(i)); - } - // 长度为 n 的哈希表占用 O(n) 空间 - Dictionary map = new(); - 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" - /* 哈希表 */ - struct hashTable { - int key; - int val; - UT_hash_handle hh; // 基于 uthash.h 实现 - }; - - typedef struct hashTable 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); - } - } - ``` - -=== "Zig" - - ```zig title="space_complexity.zig" - // 线性阶 - fn linear(comptime n: i32) !void { - // 长度为 n 的数组占用 O(n) 空间 - var nums = [_]i32{0}**n; - // 长度为 n 的列表占用 O(n) 空间 - var nodes = std.ArrayList(i32).init(std.heap.page_allocator); - defer nodes.deinit(); - var i: i32 = 0; - while (i < n) : (i += 1) { - try nodes.append(i); - } - // 长度为 n 的哈希表占用 O(n) 空间 - var map = std.AutoArrayHashMap(i32, []const u8).init(std.heap.page_allocator); - defer map.deinit(); - var j: i32 = 0; - while (j < n) : (j += 1) { - const string = try std.fmt.allocPrint(std.heap.page_allocator, "{d}", .{j}); - defer std.heap.page_allocator.free(string); - try map.put(i, string); - } - _ = nums; - } - ``` - -如图 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); - } - ``` - -=== "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); - } - ``` - -![递归函数产生的线性阶空间复杂度](space_complexity.assets/space_complexity_recursive_linear.png) - -

图 2-17   递归函数产生的线性阶空间复杂度

- -### 3.   平方阶 $O(n^2)$ - -平方阶常见于矩阵和图,元素数量与 $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 = new(); - for (int i = 0; i < n; i++) { - List tmp = new(); - 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); - } - ``` - -=== "Zig" - - ```zig title="space_complexity.zig" - // 平方阶 - fn quadratic(n: i32) !void { - // 二维列表占用 O(n^2) 空间 - var nodes = std.ArrayList(std.ArrayList(i32)).init(std.heap.page_allocator); - defer nodes.deinit(); - var i: i32 = 0; - while (i < n) : (i += 1) { - var tmp = std.ArrayList(i32).init(std.heap.page_allocator); - defer tmp.deinit(); - var j: i32 = 0; - while (j < n) : (j += 1) { - try tmp.append(0); - } - try nodes.append(tmp); - } - } - ``` - -如图 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; - } - ``` - -=== "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); - } - ``` - -![递归函数产生的平方阶空间复杂度](space_complexity.assets/space_complexity_recursive_quadratic.png) - -

图 2-18   递归函数产生的平方阶空间复杂度

- -### 4.   指数阶 $O(2^n)$ - -指数阶常见于二叉树。观察图 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 TreeNode(0); - root.left = buildTree(n - 1); - root.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; - } - ``` - -=== "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; - } - ``` - -![满二叉树产生的指数阶空间复杂度](space_complexity.assets/space_complexity_exponential.png) - -

图 2-19   满二叉树产生的指数阶空间复杂度

- -### 5.   对数阶 $O(\log n)$ - -对数阶常见于分治算法。例如归并排序,输入长度为 $n$ 的数组,每轮递归将数组从中点划分为两半,形成高度为 $\log n$ 的递归树,使用 $O(\log n)$ 栈帧空间。 - -再例如将数字转化为字符串,输入一个正整数 $n$ ,它的位数为 $\log_{10} n + 1$ ,即对应字符串长度为 $\log_{10} n + 1$ ,因此空间复杂度为 $O(\log_{10} n + 1) = O(\log n)$ 。 - -## 2.4.4   权衡时间与空间 - -理想情况下,我们希望算法的时间复杂度和空间复杂度都能达到最优。然而在实际情况中,同时优化时间复杂度和空间复杂度通常是非常困难的。 - -**降低时间复杂度通常需要以提升空间复杂度为代价,反之亦然**。我们将牺牲内存空间来提升算法运行速度的思路称为“以空间换时间”;反之,则称为“以时间换空间”。 - -选择哪种思路取决于我们更看重哪个方面。在大多数情况下,时间比空间更宝贵,因此“以空间换时间”通常是更常用的策略。当然,在数据量很大的情况下,控制空间复杂度也是非常重要的。 diff --git a/chapter_computational_complexity/time_complexity.md b/chapter_computational_complexity/time_complexity.md deleted file mode 100755 index 5f6275126..000000000 --- a/chapter_computational_complexity/time_complexity.md +++ /dev/null @@ -1,3328 +0,0 @@ ---- -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 - } - } - ``` - -=== "Zig" - - ```zig title="" - - ``` - -根据以上方法,可以得到算法运行时间为 $6n + 12$ ns : - -$$ -1 + 1 + 10 + (1 + 5) \times n = 6n + 12 -$$ - -但实际上,**统计算法的运行时间既不合理也不现实**。首先,我们不希望将预估时间和运行平台绑定,因为算法需要在各种不同的平台上运行。其次,我们很难获知每种操作的运行时间,这给预估过程带来了极大的难度。 - -## 2.3.1   统计时间增长趋势 - -时间复杂度分析统计的不是算法运行时间,**而是算法运行时间随着数据量变大时的增长趋势**。 - -“时间增长趋势”这个概念比较抽象,我们通过一个例子来加以理解。假设输入数据大小为 $n$ ,给定三个算法函数 `A`、`B` 和 `C` : - -=== "Python" - - ```python title="" - # 算法 A 的时间复杂度:常数阶 - def algorithm_A(n: int): - print(0) - # 算法 B 的时间复杂度:线性阶 - def algorithm_B(n: int): - for _ in range(n): - print(0) - # 算法 C 的时间复杂度:常数阶 - def algorithm_C(n: int): - for _ in range(1000000): - print(0) - ``` - -=== "C++" - - ```cpp title="" - // 算法 A 的时间复杂度:常数阶 - void algorithm_A(int n) { - cout << 0 << endl; - } - // 算法 B 的时间复杂度:线性阶 - void algorithm_B(int n) { - for (int i = 0; i < n; i++) { - cout << 0 << endl; - } - } - // 算法 C 的时间复杂度:常数阶 - void algorithm_C(int n) { - for (int i = 0; i < 1000000; i++) { - cout << 0 << endl; - } - } - ``` - -=== "Java" - - ```java title="" - // 算法 A 的时间复杂度:常数阶 - void algorithm_A(int n) { - System.out.println(0); - } - // 算法 B 的时间复杂度:线性阶 - void algorithm_B(int n) { - for (int i = 0; i < n; i++) { - System.out.println(0); - } - } - // 算法 C 的时间复杂度:常数阶 - void algorithm_C(int n) { - for (int i = 0; i < 1000000; i++) { - System.out.println(0); - } - } - ``` - -=== "C#" - - ```csharp title="" - // 算法 A 的时间复杂度:常数阶 - void algorithm_A(int n) { - Console.WriteLine(0); - } - // 算法 B 的时间复杂度:线性阶 - void algorithm_B(int n) { - for (int i = 0; i < n; i++) { - Console.WriteLine(0); - } - } - // 算法 C 的时间复杂度:常数阶 - void algorithm_C(int n) { - for (int i = 0; i < 1000000; i++) { - Console.WriteLine(0); - } - } - ``` - -=== "Go" - - ```go title="" - // 算法 A 的时间复杂度:常数阶 - func algorithm_A(n int) { - fmt.Println(0) - } - // 算法 B 的时间复杂度:线性阶 - func algorithm_B(n int) { - for i := 0; i < n; i++ { - fmt.Println(0) - } - } - // 算法 C 的时间复杂度:常数阶 - func algorithm_C(n int) { - for i := 0; i < 1000000; i++ { - fmt.Println(0) - } - } - ``` - -=== "Swift" - - ```swift title="" - // 算法 A 的时间复杂度:常数阶 - func algorithmA(n: Int) { - print(0) - } - - // 算法 B 的时间复杂度:线性阶 - func algorithmB(n: Int) { - for _ in 0 ..< n { - print(0) - } - } - - // 算法 C 的时间复杂度:常数阶 - func algorithmC(n: Int) { - for _ in 0 ..< 1000000 { - print(0) - } - } - ``` - -=== "JS" - - ```javascript title="" - // 算法 A 的时间复杂度:常数阶 - function algorithm_A(n) { - console.log(0); - } - // 算法 B 的时间复杂度:线性阶 - function algorithm_B(n) { - for (let i = 0; i < n; i++) { - console.log(0); - } - } - // 算法 C 的时间复杂度:常数阶 - function algorithm_C(n) { - for (let i = 0; i < 1000000; i++) { - console.log(0); - } - } - - ``` - -=== "TS" - - ```typescript title="" - // 算法 A 的时间复杂度:常数阶 - function algorithm_A(n: number): void { - console.log(0); - } - // 算法 B 的时间复杂度:线性阶 - function algorithm_B(n: number): void { - for (let i = 0; i < n; i++) { - console.log(0); - } - } - // 算法 C 的时间复杂度:常数阶 - function algorithm_C(n: number): void { - for (let i = 0; i < 1000000; i++) { - console.log(0); - } - } - ``` - -=== "Dart" - - ```dart title="" - // 算法 A 的时间复杂度:常数阶 - void algorithmA(int n) { - print(0); - } - // 算法 B 的时间复杂度:线性阶 - void algorithmB(int n) { - for (int i = 0; i < n; i++) { - print(0); - } - } - // 算法 C 的时间复杂度:常数阶 - void algorithmC(int n) { - for (int i = 0; i < 1000000; i++) { - print(0); - } - } - ``` - -=== "Rust" - - ```rust title="" - // 算法 A 的时间复杂度:常数阶 - fn algorithm_A(n: i32) { - println!("{}", 0); - } - // 算法 B 的时间复杂度:线性阶 - fn algorithm_B(n: i32) { - for _ in 0..n { - println!("{}", 0); - } - } - // 算法 C 的时间复杂度:常数阶 - fn algorithm_C(n: i32) { - for _ in 0..1000000 { - println!("{}", 0); - } - } - ``` - -=== "C" - - ```c title="" - // 算法 A 的时间复杂度:常数阶 - void algorithm_A(int n) { - printf("%d", 0); - } - // 算法 B 的时间复杂度:线性阶 - void algorithm_B(int n) { - for (int i = 0; i < n; i++) { - printf("%d", 0); - } - } - // 算法 C 的时间复杂度:常数阶 - void algorithm_C(int n) { - for (int i = 0; i < 1000000; i++) { - printf("%d", 0); - } - } - ``` - -=== "Zig" - - ```zig title="" - - ``` - -图 2-7 展示了以上三个算法函数的时间复杂度。 - -- 算法 `A` 只有 $1$ 个打印操作,算法运行时间不随着 $n$ 增大而增长。我们称此算法的时间复杂度为“常数阶”。 -- 算法 `B` 中的打印操作需要循环 $n$ 次,算法运行时间随着 $n$ 增大呈线性增长。此算法的时间复杂度被称为“线性阶”。 -- 算法 `C` 中的打印操作需要循环 $1000000$ 次,虽然运行时间很长,但它与输入数据大小 $n$ 无关。因此 `C` 的时间复杂度和 `A` 相同,仍为“常数阶”。 - -![算法 A、B 和 C 的时间增长趋势](time_complexity.assets/time_complexity_simple_example.png) - -

图 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 - } - } - ``` - -=== "Zig" - - ```zig title="" - - ``` - -设算法的操作数量是一个关于输入数据大小 $n$ 的函数,记为 $T(n)$ ,则以上函数的的操作数量为: - -$$ -T(n) = 3 + 2n -$$ - -$T(n)$ 是一次函数,说明其运行时间的增长趋势是线性的,因此它的时间复杂度是线性阶。 - -我们将线性阶的时间复杂度记为 $O(n)$ ,这个数学符号称为「大 $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) - -

图 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); - } - } - } - ``` - -=== "Zig" - - ```zig title="" - - ``` - -以下公式展示了使用上述技巧前后的统计结果,两者推出的时间复杂度都为 $O(n^2)$ 。 - -$$ -\begin{aligned} -T(n) & = 2n(n + 1) + (5n + 1) + 2 & \text{完整统计 (-.-|||)} \newline -& = 2n^2 + 7n + 3 \newline -T(n) & = n^2 + n & \text{偷懒统计 (o.O)} -\end{aligned} -$$ - -### 2.   第二步:判断渐近上界 - -**时间复杂度由多项式 $T(n)$ 中最高阶的项来决定**。这是因为在 $n$ 趋于无穷大时,最高阶的项将发挥主导作用,其他项的影响都可以被忽略。 - -表 2-2 展示了一些例子,其中一些夸张的值是为了强调“系数无法撼动阶数”这一结论。当 $n$ 趋于无穷大时,这些常数变得无足轻重。 - -

表 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) - -

图 2-9   常见的时间复杂度类型

- -### 1.   常数阶 $O(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; - } - ``` - -=== "Zig" - - ```zig title="time_complexity.zig" - // 常数阶 - fn constant(n: i32) i32 { - _ = n; - var count: i32 = 0; - const size: i32 = 100_000; - var i: i32 = 0; - while(i 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; - } - ``` - -=== "Zig" - - ```zig title="time_complexity.zig" - // 线性阶 - fn linear(n: i32) i32 { - var count: i32 = 0; - var i: i32 = 0; - while (i < n) : (i += 1) { - count += 1; - } - return count; - } - ``` - -遍历数组和遍历链表等操作的时间复杂度均为 $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; - } - ``` - -=== "Zig" - - ```zig title="time_complexity.zig" - // 线性阶(遍历数组) - fn arrayTraversal(nums: []i32) i32 { - var count: i32 = 0; - // 循环次数与数组长度成正比 - for (nums) |_| { - count += 1; - } - return count; - } - ``` - -值得注意的是,**输入数据大小 $n$ 需根据输入数据的类型来具体确定**。比如在第一个示例中,变量 $n$ 为输入数据大小;在第二个示例中,数组长度 $n$ 为数据大小。 - -### 3.   平方阶 $O(n^2)$ - -平方阶的操作数量相对于输入数据大小 $n$ 以平方级别增长。平方阶通常出现在嵌套循环中,外层循环和内层循环都为 $O(n)$ ,因此总体为 $O(n^2)$ : - -=== "Python" - - ```python title="time_complexity.py" - def quadratic(n: int) -> int: - """平方阶""" - count = 0 - # 循环次数与数组长度成平方关系 - 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; - // 循环次数与数组长度成平方关系 - 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; - // 循环次数与数组长度成平方关系 - 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; - // 循环次数与数组长度成平方关系 - 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 - // 循环次数与数组长度成平方关系 - 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 - // 循环次数与数组长度成平方关系 - for _ in 0 ..< n { - for _ in 0 ..< n { - count += 1 - } - } - return count - } - ``` - -=== "JS" - - ```javascript title="time_complexity.js" - /* 平方阶 */ - function quadratic(n) { - let count = 0; - // 循环次数与数组长度成平方关系 - 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; - // 循环次数与数组长度成平方关系 - 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; - // 循环次数与数组长度成平方关系 - 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; - // 循环次数与数组长度成平方关系 - for _ in 0..n { - for _ in 0..n { - count += 1; - } - } - count - } - ``` - -=== "C" - - ```c title="time_complexity.c" - /* 平方阶 */ - int quadratic(int n) { - int count = 0; - // 循环次数与数组长度成平方关系 - for (int i = 0; i < n; i++) { - for (int j = 0; j < n; j++) { - count++; - } - } - return count; - } - ``` - -=== "Zig" - - ```zig title="time_complexity.zig" - // 平方阶 - fn quadratic(n: i32) i32 { - var count: i32 = 0; - var i: i32 = 0; - // 循环次数与数组长度成平方关系 - while (i < n) : (i += 1) { - var j: i32 = 0; - while (j < n) : (j += 1) { - count += 1; - } - } - return count; - } - ``` - -图 2-10 对比了常数阶、线性阶和平方阶三种时间复杂度。 - -![常数阶、线性阶和平方阶的时间复杂度](time_complexity.assets/time_complexity_constant_linear_quadratic.png) - -

图 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 stride(from: nums.count - 1, to: 0, by: -1) { - // 内循环:将未排序区间 [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; - } - ``` - -=== "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; - } - ``` - -### 4.   指数阶 $O(2^n)$ - -生物学的“细胞分裂”是指数阶增长的典型例子:初始状态为 $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; - } - ``` - -=== "Zig" - - ```zig title="time_complexity.zig" - // 指数阶(循环实现) - fn exponential(n: i32) i32 { - var count: i32 = 0; - var bas: i32 = 1; - var i: i32 = 0; - // 细胞每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1) - while (i < n) : (i += 1) { - var j: i32 = 0; - while (j < bas) : (j += 1) { - count += 1; - } - bas *= 2; - } - // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 - return count; - } - ``` - -![指数阶的时间复杂度](time_complexity.assets/time_complexity_exponential.png) - -

图 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; - } - ``` - -=== "Zig" - - ```zig title="time_complexity.zig" - // 指数阶(递归实现) - fn expRecur(n: i32) i32 { - if (n == 1) return 1; - return expRecur(n - 1) + expRecur(n - 1) + 1; - } - ``` - -指数阶增长非常迅速,在穷举法(暴力搜索、回溯等)中比较常见。对于数据规模较大的问题,指数阶是不可接受的,通常需要使用动态规划或贪心等算法来解决。 - -### 5.   对数阶 $O(\log n)$ - -与指数阶相反,对数阶反映了“每轮缩减到一半”的情况。设输入数据大小为 $n$ ,由于每轮缩减到一半,因此循环次数是 $\log_2 n$ ,即 $2^n$ 的反函数。 - -图 2-12 和以下代码模拟了“每轮缩减到一半”的过程,时间复杂度为 $O(\log_2 n)$ ,简记为 $O(\log n)$ 。 - -=== "Python" - - ```python title="time_complexity.py" - def logarithmic(n: float) -> int: - """对数阶(循环实现)""" - count = 0 - while n > 1: - n = n / 2 - count += 1 - return count - ``` - -=== "C++" - - ```cpp title="time_complexity.cpp" - /* 对数阶(循环实现) */ - int logarithmic(float n) { - int count = 0; - while (n > 1) { - n = n / 2; - count++; - } - return count; - } - ``` - -=== "Java" - - ```java title="time_complexity.java" - /* 对数阶(循环实现) */ - int logarithmic(float n) { - int count = 0; - while (n > 1) { - n = n / 2; - count++; - } - return count; - } - ``` - -=== "C#" - - ```csharp title="time_complexity.cs" - /* 对数阶(循环实现) */ - int logarithmic(float n) { - int count = 0; - while (n > 1) { - n = n / 2; - count++; - } - return count; - } - ``` - -=== "Go" - - ```go title="time_complexity.go" - /* 对数阶(循环实现)*/ - func logarithmic(n float64) int { - count := 0 - for n > 1 { - n = n / 2 - count++ - } - return count - } - ``` - -=== "Swift" - - ```swift title="time_complexity.swift" - /* 对数阶(循环实现) */ - func logarithmic(n: Double) -> 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(num n) { - int count = 0; - while (n > 1) { - n = n / 2; - count++; - } - return count; - } - ``` - -=== "Rust" - - ```rust title="time_complexity.rs" - /* 对数阶(循环实现) */ - fn logarithmic(mut n: f32) -> i32 { - let mut count = 0; - while n > 1.0 { - n = n / 2.0; - count += 1; - } - count - } - ``` - -=== "C" - - ```c title="time_complexity.c" - /* 对数阶(循环实现) */ - int logarithmic(float n) { - int count = 0; - while (n > 1) { - n = n / 2; - count++; - } - return count; - } - ``` - -=== "Zig" - - ```zig title="time_complexity.zig" - // 对数阶(循环实现) - fn logarithmic(n: f32) i32 { - var count: i32 = 0; - var n_var = n; - while (n_var > 1) - { - n_var = n_var / 2; - count +=1; - } - return count; - } - ``` - -![对数阶的时间复杂度](time_complexity.assets/time_complexity_logarithmic.png) - -

图 2-12   对数阶的时间复杂度

- -与指数阶类似,对数阶也常出现于递归函数中。以下代码形成了一个高度为 $\log_2 n$ 的递归树: - -=== "Python" - - ```python title="time_complexity.py" - def log_recur(n: float) -> int: - """对数阶(递归实现)""" - if n <= 1: - return 0 - return log_recur(n / 2) + 1 - ``` - -=== "C++" - - ```cpp title="time_complexity.cpp" - /* 对数阶(递归实现) */ - int logRecur(float n) { - if (n <= 1) - return 0; - return logRecur(n / 2) + 1; - } - ``` - -=== "Java" - - ```java title="time_complexity.java" - /* 对数阶(递归实现) */ - int logRecur(float n) { - if (n <= 1) - return 0; - return logRecur(n / 2) + 1; - } - ``` - -=== "C#" - - ```csharp title="time_complexity.cs" - /* 对数阶(递归实现) */ - int logRecur(float n) { - if (n <= 1) return 0; - return logRecur(n / 2) + 1; - } - ``` - -=== "Go" - - ```go title="time_complexity.go" - /* 对数阶(递归实现)*/ - func logRecur(n float64) int { - if n <= 1 { - return 0 - } - return logRecur(n/2) + 1 - } - ``` - -=== "Swift" - - ```swift title="time_complexity.swift" - /* 对数阶(递归实现) */ - func logRecur(n: Double) -> 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(num n) { - if (n <= 1) return 0; - return logRecur(n / 2) + 1; - } - ``` - -=== "Rust" - - ```rust title="time_complexity.rs" - /* 对数阶(递归实现) */ - fn log_recur(n: f32) -> i32 { - if n <= 1.0 { - return 0; - } - log_recur(n / 2.0) + 1 - } - ``` - -=== "C" - - ```c title="time_complexity.c" - /* 对数阶(递归实现) */ - int logRecur(float n) { - if (n <= 1) - return 0; - return logRecur(n / 2) + 1; - } - ``` - -=== "Zig" - - ```zig title="time_complexity.zig" - // 对数阶(递归实现) - fn logRecur(n: f32) i32 { - if (n <= 1) return 0; - return logRecur(n / 2) + 1; - } - ``` - -对数阶常出现于基于分治策略的算法中,体现了“一分为多”和“化繁为简”的算法思想。它增长缓慢,是仅次于常数阶的理想的时间复杂度。 - -!!! 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)$ - -线性对数阶常出现于嵌套循环中,两层循环的时间复杂度分别为 $O(\log n)$ 和 $O(n)$ 。相关代码如下: - -=== "Python" - - ```python title="time_complexity.py" - def linear_log_recur(n: float) -> 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(float 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(float 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(float 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 float64) int { - if n <= 1 { - return 1 - } - count := linearLogRecur(n/2) + - linearLogRecur(n/2) - for i := 0.0; i < n; i++ { - count++ - } - return count - } - ``` - -=== "Swift" - - ```swift title="time_complexity.swift" - /* 线性对数阶 */ - func linearLogRecur(n: Double) -> 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(num 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: f32) -> i32 { - if n <= 1.0 { - return 1; - } - let mut count = linear_log_recur(n / 2.0) + - linear_log_recur(n / 2.0); - for _ in 0 ..n as i32 { - count += 1; - } - return count - } - ``` - -=== "C" - - ```c title="time_complexity.c" - /* 线性对数阶 */ - int linearLogRecur(float n) { - if (n <= 1) - return 1; - int count = linearLogRecur(n / 2) + linearLogRecur(n / 2); - for (int i = 0; i < n; i++) { - count++; - } - return count; - } - ``` - -=== "Zig" - - ```zig title="time_complexity.zig" - // 线性对数阶 - fn linearLogRecur(n: f32) i32 { - if (n <= 1) return 1; - var count: i32 = linearLogRecur(n / 2) + - linearLogRecur(n / 2); - var i: f32 = 0; - while (i < n) : (i += 1) { - count += 1; - } - return count; - } - ``` - -图 2-13 展示了线性对数阶的生成方式。二叉树的每一层的操作总数都为 $n$ ,树共有 $\log_2 n + 1$ 层,因此时间复杂度为 $O(n \log n)$ 。 - -![线性对数阶的时间复杂度](time_complexity.assets/time_complexity_logarithmic_linear.png) - -

图 2-13   线性对数阶的时间复杂度

- -主流排序算法的时间复杂度通常为 $O(n \log n)$ ,例如快速排序、归并排序、堆排序等。 - -### 7.   阶乘阶 $O(n!)$ - -阶乘阶对应数学上的“全排列”问题。给定 $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; - } - ``` - -=== "Zig" - - ```zig title="time_complexity.zig" - // 阶乘阶(递归实现) - fn factorialRecur(n: i32) i32 { - if (n == 0) return 1; - var count: i32 = 0; - var i: i32 = 0; - // 从 1 个分裂出 n 个 - while (i < n) : (i += 1) { - count += factorialRecur(n - 1); - } - return count; - } - ``` - -![阶乘阶的时间复杂度](time_complexity.assets/time_complexity_factorial.png) - -

图 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++) { - var index = new Random().Next(i, nums.Length); - var tmp = nums[i]; - var ran = nums[index]; - nums[i] = ran; - nums[index] = tmp; - } - 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; - } - ``` - -=== "Zig" - - ```zig title="worst_best_time_complexity.zig" - // 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 - pub fn randomNumbers(comptime n: usize) [n]i32 { - var nums: [n]i32 = undefined; - // 生成数组 nums = { 1, 2, 3, ..., n } - for (nums) |*num, i| { - num.* = @intCast(i32, i) + 1; - } - // 随机打乱数组元素 - const rand = std.crypto.random; - rand.shuffle(i32, &nums); - return nums; - } - - // 查找数组 nums 中数字 1 所在索引 - pub fn findOne(nums: []i32) i32 { - for (nums) |num, i| { - // 当元素 1 在数组头部时,达到最佳时间复杂度 O(1) - // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n) - if (num == 1) return @intCast(i32, i); - } - return -1; - } - ``` - -值得说明的是,我们在实际中很少使用最佳时间复杂度,因为通常只有在很小概率下才能达到,可能会带来一定的误导性。**而最差时间复杂度更为实用,因为它给出了一个效率安全值**,让我们可以放心地使用算法。 - -从上述示例可以看出,最差或最佳时间复杂度只出现于“特殊的数据分布”,这些情况的出现概率可能很小,并不能真实地反映算法运行效率。相比之下,**平均时间复杂度可以体现算法在随机输入数据下的运行效率**,用 $\Theta$ 记号来表示。 - -对于部分算法,我们可以简单地推算出随机数据分布下的平均情况。比如上述示例,由于输入数组是被打乱的,因此元素 $1$ 出现在任意索引的概率都是相等的,那么算法的平均循环次数就是数组长度的一半 $n / 2$ ,平均时间复杂度为 $\Theta(n / 2) = \Theta(n)$ 。 - -但对于较为复杂的算法,计算平均时间复杂度往往是比较困难的,因为很难分析出在数据分布下的整体数学期望。在这种情况下,我们通常使用最差时间复杂度作为算法效率的评判标准。 - -!!! question "为什么很少看到 $\Theta$ 符号?" - - 可能由于 $O$ 符号过于朗朗上口,我们常常使用它来表示平均时间复杂度。但从严格意义上看,这种做法并不规范。在本书和其他资料中,若遇到类似“平均时间复杂度 $O(n)$”的表述,请将其直接理解为 $\Theta(n)$ 。 diff --git a/chapter_divide_and_conquer/binary_search_recur.md b/chapter_divide_and_conquer/binary_search_recur.md deleted file mode 100644 index e0cc4496f..000000000 --- a/chapter_divide_and_conquer/binary_search_recur.md +++ /dev/null @@ -1,368 +0,0 @@ ---- -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) - -

图 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 { - let n = nums.count - // 求解问题 f(0, n-1) - return dfs(nums: nums, target: target, i: 0, j: n - 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" - [class]{}-[func]{dfs} - - [class]{}-[func]{binarySearch} - ``` - -=== "Zig" - - ```zig title="binary_search_recur.zig" - [class]{}-[func]{dfs} - - [class]{}-[func]{binarySearch} - ``` diff --git a/chapter_divide_and_conquer/build_binary_tree_problem.md b/chapter_divide_and_conquer/build_binary_tree_problem.md deleted file mode 100644 index 81399c23c..000000000 --- a/chapter_divide_and_conquer/build_binary_tree_problem.md +++ /dev/null @@ -1,459 +0,0 @@ ---- -comments: true ---- - -# 12.3   构建二叉树问题 - -!!! question - - 给定一个二叉树的前序遍历 `preorder` 和中序遍历 `inorder` ,请从中构建二叉树,返回二叉树的根节点。 - -![构建二叉树的示例数据](build_binary_tree_problem.assets/build_tree_example.png) - -

图 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) - -

图 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) - -

图 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 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(int[] preorder, int[] inorder) { - // 初始化哈希表,存储 inorder 元素到索引的映射 - Dictionary inorderMap = new Dictionary(); - 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: 0, l: 0, r: inorder.count - 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" - [class]{}-[func]{dfs} - - [class]{}-[func]{buildTree} - ``` - -=== "Zig" - - ```zig title="build_tree.zig" - [class]{}-[func]{dfs} - - [class]{}-[func]{buildTree} - ``` - -图 12-8 展示了构建二叉树的递归过程,各个节点是在向下“递”的过程中建立的,而各条边(即引用)是在向上“归”的过程中建立的。 - -=== "<1>" - ![构建二叉树的递归过程](build_binary_tree_problem.assets/built_tree_step1.png) - -=== "<2>" - ![built_tree_step2](build_binary_tree_problem.assets/built_tree_step2.png) - -=== "<3>" - ![built_tree_step3](build_binary_tree_problem.assets/built_tree_step3.png) - -=== "<4>" - ![built_tree_step4](build_binary_tree_problem.assets/built_tree_step4.png) - -=== "<5>" - ![built_tree_step5](build_binary_tree_problem.assets/built_tree_step5.png) - -=== "<6>" - ![built_tree_step6](build_binary_tree_problem.assets/built_tree_step6.png) - -=== "<7>" - ![built_tree_step7](build_binary_tree_problem.assets/built_tree_step7.png) - -=== "<8>" - ![built_tree_step8](build_binary_tree_problem.assets/built_tree_step8.png) - -=== "<9>" - ![built_tree_step9](build_binary_tree_problem.assets/built_tree_step9.png) - -

图 12-8   构建二叉树的递归过程

- -每个递归函数内的前序遍历 `preorder` 和中序遍历 `inorder` 的划分结果如图 12-9 所示。 - -![每个递归函数中的划分结果](build_binary_tree_problem.assets/built_tree_overall.png) - -

图 12-9   每个递归函数中的划分结果

- -设树的节点数量为 $n$ ,初始化每一个节点(执行一个递归函数 `dfs()` )使用 $O(1)$ 时间。**因此总体时间复杂度为 $O(n)$** 。 - -哈希表存储 `inorder` 元素到索引的映射,空间复杂度为 $O(n)$ 。最差情况下,即二叉树退化为链表时,递归深度达到 $n$ ,使用 $O(n)$ 的栈帧空间。**因此总体空间复杂度为 $O(n)$** 。 diff --git a/chapter_divide_and_conquer/hanota_problem.md b/chapter_divide_and_conquer/hanota_problem.md deleted file mode 100644 index 94d76315d..000000000 --- a/chapter_divide_and_conquer/hanota_problem.md +++ /dev/null @@ -1,470 +0,0 @@ ---- -comments: true ---- - -# 12.4   汉诺塔问题 - -在归并排序和构建二叉树中,我们都是将原问题分解为两个规模为原问题一半的子问题。然而对于汉诺塔问题,我们采用不同的分解策略。 - -!!! question - - 给定三根柱子,记为 `A`、`B` 和 `C` 。起始状态下,柱子 `A` 上套着 $n$ 个圆盘,它们从上到下按照从小到大的顺序排列。我们的任务是要把这 $n$ 个圆盘移到柱子 `C` 上,并保持它们的原有顺序不变。在移动圆盘的过程中,需要遵守以下规则。 - - 1. 圆盘只能从一个柱子顶部拿出,从另一个柱子顶部放入。 - 2. 每次只能移动一个圆盘。 - 3. 小圆盘必须时刻位于大圆盘之上。 - -![汉诺塔问题示例](hanota_problem.assets/hanota_example.png) - -

图 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) - -=== "<2>" - ![hanota_f1_step2](hanota_problem.assets/hanota_f1_step2.png) - -

图 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) - -=== "<2>" - ![hanota_f2_step2](hanota_problem.assets/hanota_f2_step2.png) - -=== "<3>" - ![hanota_f2_step3](hanota_problem.assets/hanota_f2_step3.png) - -=== "<4>" - ![hanota_f2_step4](hanota_problem.assets/hanota_f2_step4.png) - -

图 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) - -=== "<2>" - ![hanota_f3_step2](hanota_problem.assets/hanota_f3_step2.png) - -=== "<3>" - ![hanota_f3_step3](hanota_problem.assets/hanota_f3_step3.png) - -=== "<4>" - ![hanota_f3_step4](hanota_problem.assets/hanota_f3_step4.png) - -

图 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) - -

图 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" - [class]{}-[func]{move} - - [class]{}-[func]{dfs} - - [class]{}-[func]{solveHanota} - ``` - -=== "Zig" - - ```zig title="hanota.zig" - [class]{}-[func]{move} - - [class]{}-[func]{dfs} - - [class]{}-[func]{solveHanota} - ``` - -如图 12-15 所示,汉诺塔问题形成一个高度为 $n$ 的递归树,每个节点代表一个子问题、对应一个开启的 `dfs()` 函数,**因此时间复杂度为 $O(2^n)$ ,空间复杂度为 $O(n)$** 。 - -![汉诺塔问题的递归树](hanota_problem.assets/hanota_recursive_tree.png) - -

图 12-15   汉诺塔问题的递归树

- -!!! quote - - 汉诺塔问题源自一种古老的传说故事。在古印度的一个寺庙里,僧侣们有三根高大的钻石柱子,以及 $64$ 个大小不一的金圆盘。僧侣们不断地移动原盘,他们相信在最后一个圆盘被正确放置的那一刻,这个世界就会结束。 - - 然而,即使僧侣们每秒钟移动一次,总共需要大约 $2^{64} \approx 1.84×10^{19}$ 秒,合约 $5850$ 亿年,远远超过了现在对宇宙年龄的估计。所以,倘若这个传说是真的,我们应该不需要担心世界末日的到来。 diff --git a/chapter_dynamic_programming/dp_problem_features.md b/chapter_dynamic_programming/dp_problem_features.md deleted file mode 100644 index f213b1341..000000000 --- a/chapter_dynamic_programming/dp_problem_features.md +++ /dev/null @@ -1,808 +0,0 @@ ---- -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) - -

图 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] - } - // 初始化 dp 表,用于存储子问题的解 - dp := make([]int, n+1) - // 初始状态:预设最小子问题的解 - dp[1] = cost[1] - dp[2] = cost[2] - // 状态转移:从较小子问题逐步求解较大子问题 - for i := 3; i <= n; i++ { - dp[i] = int(math.Min(float64(dp[i-1]), float64(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 stride(from: 3, through: n, by: 1) { - 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" - [class]{}-[func]{minCostClimbingStairsDP} - ``` - -=== "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]; - } - ``` - -图 14-7 展示了以上代码的动态规划过程。 - -![爬楼梯最小代价的动态规划过程](dp_problem_features.assets/min_cost_cs_dp.png) - -

图 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] - } - // 初始状态:预设最小子问题的解 - a, b := cost[1], cost[2] - // 状态转移:从较小子问题逐步求解较大子问题 - for i := 3; i <= n; i++ { - tmp := b - b = int(math.Min(float64(a), float64(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 stride(from: 3, through: n, by: 1) { - (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" - [class]{}-[func]{minCostClimbingStairsDPComp} - ``` - -=== "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; - } - ``` - -## 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) - -

图 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) - -

图 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 stride(from: 3, through: n, by: 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] - } - ``` - -=== "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" - [class]{}-[func]{climbingStairsConstraintDP} - ``` - -=== "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]; - } - ``` - -在上面的案例中,由于仅需多考虑前面一个状态,我们仍然可以通过扩展状态定义,使得问题重新满足无后效性。然而,某些问题具有非常严重的“有后效性”。 - -!!! question "爬楼梯与障碍生成" - - 给定一个共有 $n$ 阶的楼梯,你每步可以上 $1$ 阶或者 $2$ 阶。**规定当爬到第 $i$ 阶时,系统自动会给第 $2i$ 阶上放上障碍物,之后所有轮都不允许跳到第 $2i$ 阶上**。例如,前两轮分别跳到了第 $2$、$3$ 阶上,则之后就不能跳到第 $4$、$6$ 阶上。请问有多少种方案可以爬到楼顶。 - -在这个问题中,下次跳跃依赖于过去所有的状态,因为每一次跳跃都会在更高的阶梯上设置障碍,并影响未来的跳跃。对于这类问题,动态规划往往难以解决。 - -实际上,许多复杂的组合优化问题(例如旅行商问题)都不满足无后效性。对于这类问题,我们通常会选择使用其他方法,例如启发式搜索、遗传算法、强化学习等,从而在有限时间内得到可用的局部最优解。 diff --git a/chapter_dynamic_programming/dp_solution_pipeline.md b/chapter_dynamic_programming/dp_solution_pipeline.md deleted file mode 100644 index 17e6d2bde..000000000 --- a/chapter_dynamic_programming/dp_solution_pipeline.md +++ /dev/null @@ -1,1318 +0,0 @@ ---- -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) - -

图 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_step1.png) - -

图 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_step2.png) - -

图 14-12   最优子结构与状态转移方程

- -!!! note - - 根据定义好的 $dp$ 表,思考原问题和子问题的关系,找出通过子问题的最优解来构造原问题的最优解的方法,即最优子结构。 - - 一旦我们找到了最优子结构,就可以使用它来构建出状态转移方程。 - -**第三步:确定边界条件和状态转移顺序** - -在本题中,首行的状态只能从其左边的状态得来,首列的状态只能从其上边的状态得来,因此首行 $i = 0$ 和首列 $j = 0$ 是边界条件。 - -如图 14-13 所示,由于每个格子是由其左方格子和上方格子转移而来,因此我们使用采用循环来遍历矩阵,外循环遍历各行、内循环遍历各列。 - -![边界条件与状态转移顺序](dp_solution_pipeline.assets/min_path_sum_solution_step3.png) - -

图 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) 的最小路径代价 - left = min_path_sum_dfs(grid, i - 1, j) - up = 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 left = minPathSumDFS(grid, i - 1, j); - int up = 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 left = minPathSumDFS(grid, i - 1, j); - int up = 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 left = minPathSumDFS(grid, i - 1, j); - int up = 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) 的最小路径代价 - left := minPathSumDFS(grid, i-1, j) - up := 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 left = minPathSumDFS(grid: grid, i: i - 1, j: j) - let up = 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 left = minPathSumDFS(grid, i - 1, j); - const up = 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 left = minPathSumDFS(grid, i - 1, j); - const up = 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 left = minPathSumDFS(grid, i - 1, j); - int up = 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 left = min_path_sum_dfs(grid, i - 1, j); - let up = 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" - [class]{}-[func]{minPathSumDFS} - ``` - -=== "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 left = minPathSumDFS(grid, i - 1, j); - var up = minPathSumDFS(grid, i, j - 1); - // 返回从左上角到 (i, j) 的最小路径代价 - return @min(left, up) + grid[@as(usize, @intCast(i))][@as(usize, @intCast(j))]; - } - ``` - -图 14-14 给出了以 $dp[2, 1]$ 为根节点的递归树,其中包含一些重叠子问题,其数量会随着网格 `grid` 的尺寸变大而急剧增多。 - -本质上看,造成重叠子问题的原因为:**存在多条路径可以从左上角到达某一单元格**。 - -![暴力搜索递归树](dp_solution_pipeline.assets/min_path_sum_dfs.png) - -

图 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] - # 左边和上边单元格的最小路径代价 - left = min_path_sum_dfs_mem(grid, mem, i - 1, j) - up = 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 left = minPathSumDFSMem(grid, mem, i - 1, j); - int up = 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 left = minPathSumDFSMem(grid, mem, i - 1, j); - int up = 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 left = minPathSumDFSMem(grid, mem, i - 1, j); - int up = 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] - } - // 左边和上边单元格的最小路径代价 - left := minPathSumDFSMem(grid, mem, i-1, j) - up := 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 left = minPathSumDFSMem(grid: grid, mem: &mem, i: i - 1, j: j) - let up = 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 left = minPathSumDFSMem(grid, mem, i - 1, j); - const up = 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 left = minPathSumDFSMem(grid, mem, i - 1, j); - const up = 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 left = minPathSumDFSMem(grid, mem, i - 1, j); - int up = 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 left = min_path_sum_dfs_mem(grid, mem, i - 1, j); - let up = 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" - [class]{}-[func]{minPathSumDFSMem} - ``` - -=== "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 left = minPathSumDFSMem(grid, mem, i - 1, j); - var up = 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))]; - } - ``` - -如图 14-15 所示,在引入记忆化后,所有子问题的解只需计算一次,因此时间复杂度取决于状态总数,即网格尺寸 $O(nm)$ 。 - -![记忆化搜索递归树](dp_solution_pipeline.assets/min_path_sum_dfs_mem.png) - -

图 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 stride(from: 1, to: m, by: 1) { - dp[0][j] = dp[0][j - 1] + grid[0][j] - } - // 状态转移:首列 - for i in stride(from: 1, to: n, by: 1) { - dp[i][0] = dp[i - 1][0] + grid[i][0] - } - // 状态转移:其余行列 - for i in stride(from: 1, to: n, by: 1) { - for j in stride(from: 1, to: m, by: 1) { - 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" - [class]{}-[func]{minPathSumDP} - ``` - -=== "Zig" - - ```zig title="min_path_sum.zig" - // 最小路径和:动态规划 - fn minPathSumDP(comptime grid: anytype) i32 { - comptime var n = grid.len; - comptime var m = grid[0].len; - // 初始化 dp 表 - var dp = [_][m]i32{[_]i32{0} ** m} ** n; - dp[0][0] = grid[0][0]; - // 状态转移:首行 - for (1..m) |j| { - dp[0][j] = dp[0][j - 1] + grid[0][j]; - } - // 状态转移:首列 - for (1..n) |i| { - dp[i][0] = dp[i - 1][0] + grid[i][0]; - } - // 状态转移:其余行列 - for (1..n) |i| { - for (1..m) |j| { - dp[i][j] = @min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]; - } - } - return dp[n - 1][m - 1]; - } - ``` - -图 14-16 展示了最小路径和的状态转移过程,其遍历了整个网格,**因此时间复杂度为 $O(nm)$** 。 - -数组 `dp` 大小为 $n \times m$ ,**因此空间复杂度为 $O(nm)$** 。 - -=== "<1>" - ![最小路径和的动态规划过程](dp_solution_pipeline.assets/min_path_sum_dp_step1.png) - -=== "<2>" - ![min_path_sum_dp_step2](dp_solution_pipeline.assets/min_path_sum_dp_step2.png) - -=== "<3>" - ![min_path_sum_dp_step3](dp_solution_pipeline.assets/min_path_sum_dp_step3.png) - -=== "<4>" - ![min_path_sum_dp_step4](dp_solution_pipeline.assets/min_path_sum_dp_step4.png) - -=== "<5>" - ![min_path_sum_dp_step5](dp_solution_pipeline.assets/min_path_sum_dp_step5.png) - -=== "<6>" - ![min_path_sum_dp_step6](dp_solution_pipeline.assets/min_path_sum_dp_step6.png) - -=== "<7>" - ![min_path_sum_dp_step7](dp_solution_pipeline.assets/min_path_sum_dp_step7.png) - -=== "<8>" - ![min_path_sum_dp_step8](dp_solution_pipeline.assets/min_path_sum_dp_step8.png) - -=== "<9>" - ![min_path_sum_dp_step9](dp_solution_pipeline.assets/min_path_sum_dp_step9.png) - -=== "<10>" - ![min_path_sum_dp_step10](dp_solution_pipeline.assets/min_path_sum_dp_step10.png) - -=== "<11>" - ![min_path_sum_dp_step11](dp_solution_pipeline.assets/min_path_sum_dp_step11.png) - -=== "<12>" - ![min_path_sum_dp_step12](dp_solution_pipeline.assets/min_path_sum_dp_step12.png) - -

图 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 stride(from: 1, to: m, by: 1) { - dp[j] = dp[j - 1] + grid[0][j] - } - // 状态转移:其余行 - for i in stride(from: 1, to: n, by: 1) { - // 状态转移:首列 - dp[0] = dp[0] + grid[i][0] - // 状态转移:其余列 - for j in stride(from: 1, to: m, by: 1) { - 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" - [class]{}-[func]{minPathSumDPComp} - ``` - -=== "Zig" - - ```zig title="min_path_sum.zig" - // 最小路径和:空间优化后的动态规划 - fn minPathSumDPComp(comptime grid: anytype) i32 { - comptime var n = grid.len; - comptime var m = grid[0].len; - // 初始化 dp 表 - var dp = [_]i32{0} ** m; - // 状态转移:首行 - dp[0] = grid[0][0]; - for (1..m) |j| { - dp[j] = dp[j - 1] + grid[0][j]; - } - // 状态转移:其余行 - for (1..n) |i| { - // 状态转移:首列 - dp[0] = dp[0] + grid[i][0]; - for (1..m) |j| { - dp[j] = @min(dp[j - 1], dp[j]) + grid[i][j]; - } - } - return dp[m - 1]; - } - ``` diff --git a/chapter_dynamic_programming/edit_distance_problem.md b/chapter_dynamic_programming/edit_distance_problem.md deleted file mode 100644 index 08e4d5a6f..000000000 --- a/chapter_dynamic_programming/edit_distance_problem.md +++ /dev/null @@ -1,850 +0,0 @@ ---- -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) - -

图 14-27   编辑距离的示例数据

- -**编辑距离问题可以很自然地用决策树模型来解释**。字符串对应树节点,一轮决策(一次编辑操作)对应树的一条边。 - -如图 14-28 所示,在不限制操作的情况下,每个节点都可以派生出许多条边,每条边对应一种操作,这意味着从 `hello` 转换到 `algo` 有许多种可能的路径。 - -从决策树的角度看,本题的目标是求解节点 `hello` 和节点 `algo` 之间的最短路径。 - -![基于决策树模型表示编辑距离问题](edit_distance_problem.assets/edit_distance_decision_tree.png) - -

图 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) - -

图 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 stride(from: 1, through: n, by: 1) { - dp[i][0] = i - } - for j in stride(from: 1, through: m, by: 1) { - dp[0][j] = j - } - // 状态转移:其余行列 - for i in stride(from: 1, through: n, by: 1) { - for j in stride(from: 1, through: m, by: 1) { - 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( - 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( - 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" - [class]{}-[func]{editDistanceDP} - ``` - -=== "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]; - } - ``` - -如图 14-30 所示,编辑距离问题的状态转移过程与背包问题非常类似,都可以看作是填写一个二维网格的过程。 - -=== "<1>" - ![编辑距离的动态规划过程](edit_distance_problem.assets/edit_distance_dp_step1.png) - -=== "<2>" - ![edit_distance_dp_step2](edit_distance_problem.assets/edit_distance_dp_step2.png) - -=== "<3>" - ![edit_distance_dp_step3](edit_distance_problem.assets/edit_distance_dp_step3.png) - -=== "<4>" - ![edit_distance_dp_step4](edit_distance_problem.assets/edit_distance_dp_step4.png) - -=== "<5>" - ![edit_distance_dp_step5](edit_distance_problem.assets/edit_distance_dp_step5.png) - -=== "<6>" - ![edit_distance_dp_step6](edit_distance_problem.assets/edit_distance_dp_step6.png) - -=== "<7>" - ![edit_distance_dp_step7](edit_distance_problem.assets/edit_distance_dp_step7.png) - -=== "<8>" - ![edit_distance_dp_step8](edit_distance_problem.assets/edit_distance_dp_step8.png) - -=== "<9>" - ![edit_distance_dp_step9](edit_distance_problem.assets/edit_distance_dp_step9.png) - -=== "<10>" - ![edit_distance_dp_step10](edit_distance_problem.assets/edit_distance_dp_step10.png) - -=== "<11>" - ![edit_distance_dp_step11](edit_distance_problem.assets/edit_distance_dp_step11.png) - -=== "<12>" - ![edit_distance_dp_step12](edit_distance_problem.assets/edit_distance_dp_step12.png) - -=== "<13>" - ![edit_distance_dp_step13](edit_distance_problem.assets/edit_distance_dp_step13.png) - -=== "<14>" - ![edit_distance_dp_step14](edit_distance_problem.assets/edit_distance_dp_step14.png) - -=== "<15>" - ![edit_distance_dp_step15](edit_distance_problem.assets/edit_distance_dp_step15.png) - -

图 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 stride(from: 1, through: m, by: 1) { - dp[j] = j - } - // 状态转移:其余行 - for i in stride(from: 1, through: n, by: 1) { - // 状态转移:首列 - var leftup = dp[0] // 暂存 dp[i-1, j-1] - dp[0] = i - // 状态转移:其余列 - for j in stride(from: 1, through: m, by: 1) { - 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(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(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" - [class]{}-[func]{editDistanceDPComp} - ``` - -=== "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]; - } - ``` diff --git a/chapter_dynamic_programming/intro_to_dynamic_programming.md b/chapter_dynamic_programming/intro_to_dynamic_programming.md deleted file mode 100644 index 4ddef50b7..000000000 --- a/chapter_dynamic_programming/intro_to_dynamic_programming.md +++ /dev/null @@ -1,1450 +0,0 @@ ---- -comments: true ---- - -# 14.1   初探动态规划 - -「动态规划 dynamic programming」是一个重要的算法范式,它将一个问题分解为一系列更小的子问题,并通过存储子问题的解来避免重复计算,从而大幅提升时间效率。 - -在本节中,我们从一个经典例题入手,先给出它的暴力回溯解法,观察其中包含的重叠子问题,再逐步导出更高效的动态规划解法。 - -!!! question "爬楼梯" - - 给定一个共有 $n$ 阶的楼梯,你每步可以上 $1$ 阶或者 $2$ 阶,请问有多少种方案可以爬到楼顶。 - -如图 14-1 所示,对于一个 $3$ 阶楼梯,共有 $3$ 种方案可以爬到楼顶。 - -![爬到第 3 阶的方案数量](intro_to_dynamic_programming.assets/climbing_stairs_example.png) - -

图 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: - break - # 尝试:做出选择,更新状态 - 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) - break; - // 尝试:做出选择,更新状态 - 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) - break; - // 尝试:做出选择,更新状态 - 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) - break; - // 尝试:做出选择,更新状态 - backtrack(choices, state + choice, n, res); - // 回退 - } - } - - /* 爬楼梯:回溯 */ - int climbingStairsBacktrack(int n) { - List choices = new List { 1, 2 }; // 可选择向上爬 1 或 2 阶 - int state = 0; // 从第 0 阶开始爬 - List res = new List { 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 { - break - } - // 尝试:做出选择,更新状态 - 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 { - break - } - 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) break; - // 尝试:做出选择,更新状态 - 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) break; - // 尝试:做出选择,更新状态 - 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) break; - // 尝试:做出选择,更新状态 - 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 { break; } - // 尝试:做出选择,更新状态 - 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) - break; - // 尝试:做出选择,更新状态 - 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; - } - ``` - -=== "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) { - break; - } - // 尝试:做出选择,更新状态 - 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]; - } - ``` - -## 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) - -

图 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); - } - ``` - -=== "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); - } - ``` - -图 14-3 展示了暴力搜索形成的递归树。对于问题 $dp[n]$ ,其递归树的深度为 $n$ ,时间复杂度为 $O(2^n)$ 。指数阶属于爆炸式增长,如果我们输入一个比较大的 $n$ ,则会陷入漫长的等待之中。 - -![爬楼梯对应递归树](intro_to_dynamic_programming.assets/climbing_stairs_dfs_tree.png) - -

图 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; - } - ``` - -=== "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); - } - ``` - -观察图 14-4 ,**经过记忆化处理后,所有重叠子问题都只需被计算一次,时间复杂度被优化至 $O(n)$** ,这是一个巨大的飞跃。 - -![记忆化搜索对应递归树](intro_to_dynamic_programming.assets/climbing_stairs_dfs_memo_tree.png) - -

图 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 stride(from: 3, through: n, by: 1) { - 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; - } - ``` - -=== "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]; - } - ``` - -图 14-5 模拟了以上代码的执行过程。 - -![爬楼梯的动态规划过程](intro_to_dynamic_programming.assets/climbing_stairs_dp.png) - -

图 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 stride(from: 3, through: n, by: 1) { - (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; - } - ``` - -=== "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; - } - ``` - -观察以上代码,由于省去了数组 `dp` 占用的空间,因此空间复杂度从 $O(n)$ 降低至 $O(1)$ 。 - -在动态规划问题中,当前状态往往仅与前面有限个状态有关,这时我们可以只保留必要的状态,通过“降维”来节省内存空间。**这种空间优化技巧被称为“滚动变量”或“滚动数组”**。 diff --git a/chapter_dynamic_programming/knapsack_problem.md b/chapter_dynamic_programming/knapsack_problem.md deleted file mode 100644 index 44615cfa0..000000000 --- a/chapter_dynamic_programming/knapsack_problem.md +++ /dev/null @@ -1,1247 +0,0 @@ ---- -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) - -

图 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" - [class]{}-[func]{knapsackDFS} - ``` - -=== "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); - } - ``` - -如图 14-18 所示,由于每个物品都会产生不选和选两条搜索分支,因此时间复杂度为 $O(2^n)$ 。 - -观察递归树,容易发现其中存在重叠子问题,例如 $dp[1, 10]$ 等。而当物品较多、背包容量较大,尤其是相同重量的物品较多时,重叠子问题的数量将会大幅增多。 - -![0-1 背包的暴力搜索递归树](knapsack_problem.assets/knapsack_dfs.png) - -

图 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" - [class]{}-[func]{knapsackDFSMem} - ``` - -=== "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]; - } - ``` - -图 14-19 展示了在记忆化递归中被剪掉的搜索分支。 - -![0-1 背包的记忆化搜索递归树](knapsack_problem.assets/knapsack_dfs_mem.png) - -

图 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 stride(from: 1, through: n, by: 1) { - for c in stride(from: 1, through: cap, by: 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] - } - ``` - -=== "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" - [class]{}-[func]{knapsackDP} - ``` - -=== "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]; - } - ``` - -如图 14-20 所示,时间复杂度和空间复杂度都由数组 `dp` 大小决定,即 $O(n \times cap)$ 。 - -=== "<1>" - ![0-1 背包的动态规划过程](knapsack_problem.assets/knapsack_dp_step1.png) - -=== "<2>" - ![knapsack_dp_step2](knapsack_problem.assets/knapsack_dp_step2.png) - -=== "<3>" - ![knapsack_dp_step3](knapsack_problem.assets/knapsack_dp_step3.png) - -=== "<4>" - ![knapsack_dp_step4](knapsack_problem.assets/knapsack_dp_step4.png) - -=== "<5>" - ![knapsack_dp_step5](knapsack_problem.assets/knapsack_dp_step5.png) - -=== "<6>" - ![knapsack_dp_step6](knapsack_problem.assets/knapsack_dp_step6.png) - -=== "<7>" - ![knapsack_dp_step7](knapsack_problem.assets/knapsack_dp_step7.png) - -=== "<8>" - ![knapsack_dp_step8](knapsack_problem.assets/knapsack_dp_step8.png) - -=== "<9>" - ![knapsack_dp_step9](knapsack_problem.assets/knapsack_dp_step9.png) - -=== "<10>" - ![knapsack_dp_step10](knapsack_problem.assets/knapsack_dp_step10.png) - -=== "<11>" - ![knapsack_dp_step11](knapsack_problem.assets/knapsack_dp_step11.png) - -=== "<12>" - ![knapsack_dp_step12](knapsack_problem.assets/knapsack_dp_step12.png) - -=== "<13>" - ![knapsack_dp_step13](knapsack_problem.assets/knapsack_dp_step13.png) - -=== "<14>" - ![knapsack_dp_step14](knapsack_problem.assets/knapsack_dp_step14.png) - -

图 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) - -=== "<2>" - ![knapsack_dp_comp_step2](knapsack_problem.assets/knapsack_dp_comp_step2.png) - -=== "<3>" - ![knapsack_dp_comp_step3](knapsack_problem.assets/knapsack_dp_comp_step3.png) - -=== "<4>" - ![knapsack_dp_comp_step4](knapsack_problem.assets/knapsack_dp_comp_step4.png) - -=== "<5>" - ![knapsack_dp_comp_step5](knapsack_problem.assets/knapsack_dp_comp_step5.png) - -=== "<6>" - ![knapsack_dp_comp_step6](knapsack_problem.assets/knapsack_dp_comp_step6.png) - -

图 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 stride(from: 1, through: n, by: 1) { - // 倒序遍历 - for c in stride(from: cap, through: 1, by: -1) { - 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" - [class]{}-[func]{knapsackDPComp} - ``` - -=== "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]; - } - ``` diff --git a/chapter_dynamic_programming/unbounded_knapsack_problem.md b/chapter_dynamic_programming/unbounded_knapsack_problem.md deleted file mode 100644 index 7ff818a13..000000000 --- a/chapter_dynamic_programming/unbounded_knapsack_problem.md +++ /dev/null @@ -1,1985 +0,0 @@ ---- -comments: true ---- - -# 14.5   完全背包问题 - -在本节中,我们先求解另一个常见的背包问题:完全背包,再了解它的一种特例:零钱兑换。 - -## 14.5.1   完全背包 - -!!! question - - 给定 $n$ 个物品,第 $i$ 个物品的重量为 $wgt[i-1]$、价值为 $val[i-1]$ ,和一个容量为 $cap$ 的背包。**每个物品可以重复选取**,问在不超过背包容量下能放入物品的最大价值。 - -![完全背包问题的示例数据](unbounded_knapsack_problem.assets/unbounded_knapsack_example.png) - -

图 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 stride(from: 1, through: n, by: 1) { - for c in stride(from: 1, through: cap, by: 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] - } - ``` - -=== "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" - [class]{}-[func]{unboundedKnapsackDP} - ``` - -=== "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]; - } - ``` - -### 3.   空间优化 - -由于当前状态是从左边和上边的状态转移而来,**因此空间优化后应该对 $dp$ 表中的每一行采取正序遍历**。 - -这个遍历顺序与 0-1 背包正好相反。请借助图 14-23 来理解两者的区别。 - -=== "<1>" - ![完全背包的空间优化后的动态规划过程](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step1.png) - -=== "<2>" - ![unbounded_knapsack_dp_comp_step2](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step2.png) - -=== "<3>" - ![unbounded_knapsack_dp_comp_step3](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step3.png) - -=== "<4>" - ![unbounded_knapsack_dp_comp_step4](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step4.png) - -=== "<5>" - ![unbounded_knapsack_dp_comp_step5](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step5.png) - -=== "<6>" - ![unbounded_knapsack_dp_comp_step6](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step6.png) - -

图 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 stride(from: 1, through: n, by: 1) { - for c in stride(from: 1, through: cap, by: 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] - } - ``` - -=== "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" - [class]{}-[func]{unboundedKnapsackDPComp} - ``` - -=== "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]; - } - ``` - -## 14.5.2   零钱兑换问题 - -背包问题是一大类动态规划问题的代表,其拥有很多的变种,例如零钱兑换问题。 - -!!! question - - 给定 $n$ 种硬币,第 $i$ 种硬币的面值为 $coins[i - 1]$ ,目标金额为 $amt$ ,**每种硬币可以重复选取**,问能够凑出目标金额的最少硬币个数。如果无法凑出目标金额则返回 $-1$ 。 - -![零钱兑换问题的示例数据](unbounded_knapsack_problem.assets/coin_change_example.png) - -

图 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 stride(from: 1, through: amt, by: 1) { - dp[0][a] = MAX - } - // 状态转移:其余行列 - for i in stride(from: 1, through: n, by: 1) { - for a in stride(from: 1, through: amt, by: 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] != 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" - [class]{}-[func]{coinChangeDP} - ``` - -=== "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; - } - } - ``` - -图 14-25 展示了零钱兑换的动态规划过程,和完全背包非常相似。 - -=== "<1>" - ![零钱兑换问题的动态规划过程](unbounded_knapsack_problem.assets/coin_change_dp_step1.png) - -=== "<2>" - ![coin_change_dp_step2](unbounded_knapsack_problem.assets/coin_change_dp_step2.png) - -=== "<3>" - ![coin_change_dp_step3](unbounded_knapsack_problem.assets/coin_change_dp_step3.png) - -=== "<4>" - ![coin_change_dp_step4](unbounded_knapsack_problem.assets/coin_change_dp_step4.png) - -=== "<5>" - ![coin_change_dp_step5](unbounded_knapsack_problem.assets/coin_change_dp_step5.png) - -=== "<6>" - ![coin_change_dp_step6](unbounded_knapsack_problem.assets/coin_change_dp_step6.png) - -=== "<7>" - ![coin_change_dp_step7](unbounded_knapsack_problem.assets/coin_change_dp_step7.png) - -=== "<8>" - ![coin_change_dp_step8](unbounded_knapsack_problem.assets/coin_change_dp_step8.png) - -=== "<9>" - ![coin_change_dp_step9](unbounded_knapsack_problem.assets/coin_change_dp_step9.png) - -=== "<10>" - ![coin_change_dp_step10](unbounded_knapsack_problem.assets/coin_change_dp_step10.png) - -=== "<11>" - ![coin_change_dp_step11](unbounded_knapsack_problem.assets/coin_change_dp_step11.png) - -=== "<12>" - ![coin_change_dp_step12](unbounded_knapsack_problem.assets/coin_change_dp_step12.png) - -=== "<13>" - ![coin_change_dp_step13](unbounded_knapsack_problem.assets/coin_change_dp_step13.png) - -=== "<14>" - ![coin_change_dp_step14](unbounded_knapsack_problem.assets/coin_change_dp_step14.png) - -=== "<15>" - ![coin_change_dp_step15](unbounded_knapsack_problem.assets/coin_change_dp_step15.png) - -

图 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 stride(from: 1, through: n, by: 1) { - for a in stride(from: 1, through: amt, by: 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] != 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" - [class]{}-[func]{coinChangeDPComp} - ``` - -=== "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; - } - } - ``` - -## 14.5.3   零钱兑换问题 II - -!!! question - - 给定 $n$ 种硬币,第 $i$ 种硬币的面值为 $coins[i - 1]$ ,目标金额为 $amt$ ,每种硬币可以重复选取,**问在凑出目标金额的硬币组合数量**。 - -![零钱兑换问题 II 的示例数据](unbounded_knapsack_problem.assets/coin_change_ii_example.png) - -

图 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 stride(from: 0, through: n, by: 1) { - dp[i][0] = 1 - } - // 状态转移 - for i in stride(from: 1, through: n, by: 1) { - for a in stride(from: 1, through: amt, by: 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] - } - ``` - -=== "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" - [class]{}-[func]{coinChangeIIDP} - ``` - -=== "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]; - } - ``` - -### 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 stride(from: 1, through: n, by: 1) { - for a in stride(from: 1, through: amt, by: 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] - } - ``` - -=== "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" - [class]{}-[func]{coinChangeIIDPComp} - ``` - -=== "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]; - } - ``` diff --git a/chapter_graph/graph_operations.md b/chapter_graph/graph_operations.md deleted file mode 100644 index 956015e02..000000000 --- a/chapter_graph/graph_operations.md +++ /dev/null @@ -1,2132 +0,0 @@ ---- -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_initialization.png) - -=== "添加边" - ![adjacency_matrix_add_edge](graph_operations.assets/adjacency_matrix_add_edge.png) - -=== "删除边" - ![adjacency_matrix_remove_edge](graph_operations.assets/adjacency_matrix_remove_edge.png) - -=== "添加顶点" - ![adjacency_matrix_add_vertex](graph_operations.assets/adjacency_matrix_add_vertex.png) - -=== "删除顶点" - ![adjacency_matrix_remove_vertex](graph_operations.assets/adjacency_matrix_remove_vertex.png) - -

图 9-7   邻接矩阵的初始化、增删边、增删顶点

- -以下是基于邻接矩阵表示图的实现代码。 - -=== "Python" - - ```python title="graph_adjacency_matrix.py" - class GraphAdjMat: - """基于邻接矩阵实现的无向图类""" - - # 顶点列表,元素代表“顶点值”,索引代表“顶点索引” - vertices: list[int] = [] - # 邻接矩阵,行列索引对应“顶点索引” - adj_mat: list[list[int]] = [] - - 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 = new List(); - this.adjMat = new List>(); - // 添加顶点 - foreach (int val in vertices) { - addVertex(val); - } - // 添加边 - // 请注意,edges 元素代表顶点索引,即对应 vertices 元素索引 - foreach (int[] e in edges) { - addEdge(e[0], e[1]); - } - } - - /* 获取顶点数量 */ - public int size() { - return vertices.Count; - } - - /* 添加顶点 */ - public void addVertex(int val) { - int n = size(); - // 向顶点列表中添加新顶点的值 - vertices.Add(val); - // 在邻接矩阵中添加一行 - List newRow = new List(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" - /* 基于邻接矩阵实现的无向图类结构 */ - struct graphAdjMat { - int *vertices; // 顶点列表 - unsigned int **adjMat; // 邻接矩阵,元素代表“边”,索引代表“顶点索引” - unsigned int size; // 顶点数量 - unsigned int capacity; // 图容量 - }; - - typedef struct graphAdjMat graphAdjMat; - - /* 添加边 */ - // 参数 i, j 对应 vertices 元素索引 - void addEdge(graphAdjMat *t, int i, int j) { - // 越界检查 - if (i < 0 || j < 0 || i >= t->size || j >= t->size || i == j) { - printf("Out of range in %s:%d\n", __FILE__, __LINE__); - exit(1); - } - // 添加边 - // 参数 i, j 对应 vertices 元素索引 - t->adjMat[i][j] = 1; - t->adjMat[j][i] = 1; - } - - /* 删除边 */ - // 参数 i, j 对应 vertices 元素索引 - void removeEdge(graphAdjMat *t, int i, int j) { - // 越界检查 - if (i < 0 || j < 0 || i >= t->size || j >= t->size || i == j) { - printf("Out of range in %s:%d\n", __FILE__, __LINE__); - exit(1); - } - // 删除边 - // 参数 i, j 对应 vertices 元素索引 - t->adjMat[i][j] = 0; - t->adjMat[j][i] = 0; - } - - /* 添加顶点 */ - void addVertex(graphAdjMat *t, int val) { - // 如果实际使用不大于预设空间,则直接初始化新空间 - if (t->size < t->capacity) { - t->vertices[t->size] = val; // 初始化新顶点值 - for (int i = 0; i < t->size; i++) { - t->adjMat[i][t->size] = 0; // 邻接矩新列阵置0 - } - memset(t->adjMat[t->size], 0, sizeof(unsigned int) * (t->size + 1)); // 将新增行置 0 - t->size++; - return; - } - - // 扩容,申请新的顶点数组 - int *temp = (int *)malloc(sizeof(int) * (t->size * 2)); - memcpy(temp, t->vertices, sizeof(int) * t->size); - temp[t->size] = val; - - // 释放原数组 - free(t->vertices); - t->vertices = temp; - - // 扩容,申请新的二维数组 - unsigned int **tempMat = (unsigned int **)malloc(sizeof(unsigned int *) * t->size * 2); - unsigned int *tempMatLine = (unsigned int *)malloc(sizeof(unsigned int) * (t->size * 2) * (t->size * 2)); - memset(tempMatLine, 0, sizeof(unsigned int) * (t->size * 2) * (t->size * 2)); - for (int k = 0; k < t->size * 2; k++) { - tempMat[k] = tempMatLine + k * (t->size * 2); - } - - for (int i = 0; i < t->size; i++) { - memcpy(tempMat[i], t->adjMat[i], sizeof(unsigned int) * t->size); // 原数据复制到新数组 - } - - for (int i = 0; i < t->size; i++) { - tempMat[i][t->size] = 0; // 将新增列置 0 - } - memset(tempMat[t->size], 0, sizeof(unsigned int) * (t->size + 1)); // 将新增行置 0 - - // 释放原数组 - free(t->adjMat[0]); - free(t->adjMat); - - // 扩容后,指向新地址 - t->adjMat = tempMat; // 指向新的邻接矩阵地址 - t->capacity = t->size * 2; - t->size++; - } - - /* 删除顶点 */ - void removeVertex(graphAdjMat *t, unsigned int index) { - // 越界检查 - if (index < 0 || index >= t->size) { - printf("Out of range in %s:%d\n", __FILE__, __LINE__); - exit(1); - } - for (int i = index; i < t->size - 1; i++) { - t->vertices[i] = t->vertices[i + 1]; // 清除删除的顶点,并将其后所有顶点前移 - } - t->vertices[t->size - 1] = 0; // 将被前移的最后一个顶点置 0 - - // 清除邻接矩阵中删除的列 - for (int i = 0; i < t->size - 1; i++) { - if (i < index) { - for (int j = index; j < t->size - 1; j++) { - t->adjMat[i][j] = t->adjMat[i][j + 1]; // 被删除列后的所有列前移 - } - } else { - memcpy(t->adjMat[i], t->adjMat[i + 1], sizeof(unsigned int) * t->size); // 被删除行的下方所有行上移 - for (int j = index; j < t->size; j++) { - t->adjMat[i][j] = t->adjMat[i][j + 1]; // 被删除列后的所有列前移 - } - } - } - t->size--; - } - - /* 打印顶点与邻接矩阵 */ - void printGraph(graphAdjMat *t) { - if (t->size == 0) { - printf("graph is empty\n"); - return; - } - printf("顶点列表 = ["); - for (int i = 0; i < t->size; i++) { - if (i != t->size - 1) { - printf("%d, ", t->vertices[i]); - } else { - printf("%d", t->vertices[i]); - } - } - printf("]\n"); - printf("邻接矩阵 =\n[\n"); - for (int i = 0; i < t->size; i++) { - printf(" ["); - for (int j = 0; j < t->size; j++) { - if (j != t->size - 1) { - printf("%u, ", t->adjMat[i][j]); - } else { - printf("%u", t->adjMat[i][j]); - } - } - printf("],\n"); - } - printf("]\n"); - } - - /* 构造函数 */ - graphAdjMat *newGraphAjdMat(unsigned int numberVertices, int *vertices, unsigned int **adjMat) { - // 申请内存 - graphAdjMat *newGraph = (graphAdjMat *)malloc(sizeof(graphAdjMat)); // 为图分配内存 - newGraph->vertices = (int *)malloc(sizeof(int) * numberVertices * 2); // 为顶点列表分配内存 - newGraph->adjMat = (unsigned int **)malloc(sizeof(unsigned int *) * numberVertices * 2); // 为邻接矩阵分配二维内存 - unsigned int *temp = (unsigned int *)malloc(sizeof(unsigned int) * numberVertices * 2 * numberVertices * 2); // 为邻接矩阵分配一维内存 - newGraph->size = numberVertices; // 初始化顶点数量 - newGraph->capacity = numberVertices * 2; // 初始化图容量 - - // 配置二维数组 - for (int i = 0; i < numberVertices * 2; i++) { - newGraph->adjMat[i] = temp + i * numberVertices * 2; // 将二维指针指向一维数组 - } - - // 赋值 - memcpy(newGraph->vertices, vertices, sizeof(int) * numberVertices); - for (int i = 0; i < numberVertices; i++) { - memcpy(newGraph->adjMat[i], adjMat[i], sizeof(unsigned int) * numberVertices); // 将传入的邻接矩阵赋值给结构体内邻接矩阵 - } - - // 返回结构体指针 - return newGraph; - } - ``` - -=== "Zig" - - ```zig title="graph_adjacency_matrix.zig" - - ``` - -## 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_initialization.png) - -=== "添加边" - ![adjacency_list_add_edge](graph_operations.assets/adjacency_list_add_edge.png) - -=== "删除边" - ![adjacency_list_remove_edge](graph_operations.assets/adjacency_list_remove_edge.png) - -=== "添加顶点" - ![adjacency_list_add_vertex](graph_operations.assets/adjacency_list_add_vertex.png) - -=== "删除顶点" - ![adjacency_list_remove_vertex](graph_operations.assets/adjacency_list_remove_vertex.png) - -

图 9-8   邻接表的初始化、增删边、增删顶点

- -以下是基于邻接表实现图的代码示例。细心的同学可能注意到,**我们在邻接表中使用 `Vertex` 节点类来表示顶点**,而这样做是有原因的。 - -1. 如果我们选择通过顶点值来区分不同顶点,那么值重复的顶点将无法被区分。 -2. 如果类似邻接矩阵那样,使用顶点列表索引来区分不同顶点。那么,假设我们想要删除索引为 $i$ 的顶点,则需要遍历整个邻接表,将其中 $> i$ 的索引全部减 $1$ ,这样操作效率较低。 -3. 因此我们考虑引入顶点类 `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) { - this.adjList = new Dictionary>(); - // 添加所有顶点和边 - foreach (Vertex[] edge in edges) { - addVertex(edge[0]); - addVertex(edge[1]); - addEdge(edge[0], edge[1]); - } - } - - /* 获取顶点数量 */ - public 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, new List()); - } - - /* 删除顶点 */ - 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 = new List(); - 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(where: { $0 == vet2 }) - adjList[vet2]?.removeAll(where: { $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(where: { $0 == vet }) - } - } - - /* 打印邻接表 */ - public func print() { - Swift.print("邻接表 =") - for pair in adjList { - var tmp: [Int] = [] - for vertex in pair.value { - tmp.append(vertex.val) - } - Swift.print("\(pair.key.val): \(tmp),") - } - } - } - ``` - -=== "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" - /* 基于邻接链表实现的无向图类结构 */ - struct graphAdjList { - Vertex **verticesList; // 邻接表 - unsigned int size; // 顶点数量 - unsigned int capacity; // 顶点容量 - }; - - typedef struct graphAdjList graphAdjList; - - /* 添加边 */ - void addEdge(graphAdjList *t, int i, int j) { - // 越界检查 - if (i < 0 || j < 0 || i == j || i >= t->size || j >= t->size) { - printf("Out of range in %s:%d\n", __FILE__, __LINE__); - return; - } - // 查找欲添加边的顶点 vet1 - vet2 - Vertex *vet1 = t->verticesList[i]; - Vertex *vet2 = t->verticesList[j]; - - // 连接顶点 vet1 - vet2 - pushBack(vet1->linked, vet2); - pushBack(vet2->linked, vet1); - } - - /* 删除边 */ - void removeEdge(graphAdjList *t, int i, int j) { - // 越界检查 - if (i < 0 || j < 0 || i == j || i >= t->size || j >= t->size) { - printf("Out of range in %s:%d\n", __FILE__, __LINE__); - return; - } - - // 查找欲删除边的顶点 vet1 - vet2 - Vertex *vet1 = t->verticesList[i]; - Vertex *vet2 = t->verticesList[j]; - - // 移除待删除边 vet1 - vet2 - removeLink(vet1->linked, vet2); - removeLink(vet2->linked, vet1); - } - - /* 添加顶点 */ - void addVertex(graphAdjList *t, int val) { - // 若大小超过容量,则扩容 - if (t->size >= t->capacity) { - Vertex **tempList = (Vertex **)malloc(sizeof(Vertex *) * 2 * t->capacity); - memcpy(tempList, t->verticesList, sizeof(Vertex *) * t->size); - free(t->verticesList); // 释放原邻接表内存 - t->verticesList = tempList; // 指向新邻接表 - t->capacity = t->capacity * 2; // 容量扩大至2倍 - } - // 申请新顶点内存并将新顶点地址存入顶点列表 - Vertex *newV = newVertex(val); // 建立新顶点 - newV->pos = t->size; // 为新顶点标记下标 - newV->linked = newLinklist(newV); // 为新顶点建立链表 - t->verticesList[t->size] = newV; // 将新顶点加入邻接表 - t->size++; - } - - /* 删除顶点 */ - void removeVertex(graphAdjList *t, unsigned int index) { - // 越界检查 - if (index < 0 || index >= t->size) { - printf("Out of range in %s:%d\n", __FILE__, __LINE__); - exit(1); - } - - Vertex *vet = t->verticesList[index]; // 查找待删节点 - if (vet == 0) { // 若不存在该节点,则返回 - printf("index is:%d\n", index); - printf("Out of range in %s:%d\n", __FILE__, __LINE__); - return; - } - - // 遍历待删除顶点的链表,将所有与待删除结点有关的边删除 - Node *temp = vet->linked->head->next; - while (temp != 0) { - removeLink(temp->val->linked, vet); // 删除与该顶点有关的边 - temp = temp->next; - } - - // 将顶点前移 - for (int i = index; i < t->size - 1; i++) { - t->verticesList[i] = t->verticesList[i + 1]; // 顶点前移 - t->verticesList[i]->pos--; // 所有前移的顶点索引值减1 - } - t->verticesList[t->size - 1] = 0; // 将被删除顶点的位置置 0 - t->size--; - - // 释放内存 - freeVertex(vet); - } - - /* 打印顶点与邻接矩阵 */ - void printGraph(graphAdjList *t) { - printf("邻接表 =\n"); - for (int i = 0; i < t->size; i++) { - Node *n = t->verticesList[i]->linked->head->next; - printf("%d: [", t->verticesList[i]->val); - while (n != 0) { - if (n->next != 0) { - printf("%d, ", n->val->val); - } else { - printf("%d", n->val->val); - } - n = n->next; - } - printf("]\n"); - } - } - - /* 构造函数 */ - graphAdjList *newGraphAdjList(unsigned int verticesCapacity) { - // 申请内存 - graphAdjList *newGraph = (graphAdjList *)malloc(sizeof(graphAdjList)); - // 建立顶点表并分配内存 - newGraph->verticesList = (Vertex **)malloc(sizeof(Vertex *) * verticesCapacity); // 为顶点列表分配内存 - memset(newGraph->verticesList, 0, sizeof(Vertex *) * verticesCapacity); // 顶点列表置 0 - newGraph->size = 0; // 初始化顶点数量 - newGraph->capacity = verticesCapacity; // 初始化顶点容量 - // 返回图指针 - return newGraph; - } - ``` - -=== "Zig" - - ```zig title="graph_adjacency_list.zig" - [class]{GraphAdjList}-[func]{} - ``` - -## 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/chapter_graph/graph_traversal.md b/chapter_graph/graph_traversal.md deleted file mode 100644 index 0d30b114d..000000000 --- a/chapter_graph/graph_traversal.md +++ /dev/null @@ -1,848 +0,0 @@ ---- -comments: true ---- - -# 9.3   图的遍历 - -树代表的是“一对多”的关系,而图则具有更高的自由度,可以表示任意的“多对多”关系。因此,我们可以把树看作是图的一种特例。显然,**树的遍历操作也是图的遍历操作的一种特例**。 - -图和树都需要应用搜索算法来实现遍历操作。图的遍历方式可分为两种:「广度优先遍历 breadth-first traversal」和「深度优先遍历 depth-first traversal」。它们也常被称为「广度优先搜索 breadth-first search」和「深度优先搜索 depth-first search」,简称 BFS 和 DFS 。 - -## 9.3.1   广度优先遍历 - -**广度优先遍历是一种由近及远的遍历方式,从某个节点出发,始终优先访问距离最近的顶点,并一层层向外扩张**。如图 9-9 所示,从左上角顶点出发,先遍历该顶点的所有邻接顶点,然后遍历下一个顶点的所有邻接顶点,以此类推,直至所有顶点访问完毕。 - -![图的广度优先遍历](graph_traversal.assets/graph_bfs.png) - -

图 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]: - """广度优先遍历 BFS""" - # 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点 - # 顶点遍历序列 - 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" - /* 广度优先遍历 BFS */ - // 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点 - 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" - /* 广度优先遍历 BFS */ - // 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点 - 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" - /* 广度优先遍历 BFS */ - // 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点 - List graphBFS(GraphAdjList graph, Vertex startVet) { - // 顶点遍历序列 - List res = new List(); - // 哈希表,用于记录已被访问过的顶点 - HashSet visited = new HashSet() { startVet }; - // 队列用于实现 BFS - Queue que = new Queue(); - 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" - /* 广度优先遍历 BFS */ - // 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点 - 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" - /* 广度优先遍历 BFS */ - // 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点 - 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" - /* 广度优先遍历 BFS */ - // 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点 - 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" - /* 广度优先遍历 BFS */ - // 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点 - 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" - /* 广度优先遍历 BFS */ - 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" - /* 广度优先遍历 BFS */ - // 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点 - 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" - /* 广度优先遍历 */ - // 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点 - Vertex **graphBFS(graphAdjList *t, Vertex *startVet) { - // 顶点遍历序列 - Vertex **res = (Vertex **)malloc(sizeof(Vertex *) * t->size); - memset(res, 0, sizeof(Vertex *) * t->size); - // 队列用于实现 BFS - queue *que = newQueue(t->size); - // 哈希表,用于记录已被访问过的顶点 - hashTable *visited = newHash(t->size); - int resIndex = 0; - queuePush(que, startVet); // 将第一个元素入队 - hashMark(visited, startVet->pos); // 标记第一个入队的顶点 - // 以顶点 vet 为起点,循环直至访问完所有顶点 - while (que->head < que->tail) { - // 遍历该顶点的边链表,将所有与该顶点有连接的,并且未被标记的顶点入队 - Node *n = queueTop(que)->linked->head->next; - while (n != 0) { - // 查询哈希表,若该索引的顶点已入队,则跳过,否则入队并标记 - if (hashQuery(visited, n->val->pos) == 1) { - n = n->next; - continue; // 跳过已被访问过的顶点 - } - queuePush(que, n->val); // 只入队未访问的顶点 - hashMark(visited, n->val->pos); // 标记该顶点已被访问 - } - // 队首元素存入数组 - res[resIndex] = queueTop(que); // 队首顶点加入顶点遍历序列 - resIndex++; - queuePop(que); // 队首元素出队 - } - // 释放内存 - freeQueue(que); - freeHash(visited); - resIndex = 0; - // 返回顶点遍历序列 - return res; - } - ``` - -=== "Zig" - - ```zig title="graph_bfs.zig" - [class]{}-[func]{graphBFS} - ``` - -代码相对抽象,建议对照图 9-10 来加深理解。 - -=== "<1>" - ![图的广度优先遍历步骤](graph_traversal.assets/graph_bfs_step1.png) - -=== "<2>" - ![graph_bfs_step2](graph_traversal.assets/graph_bfs_step2.png) - -=== "<3>" - ![graph_bfs_step3](graph_traversal.assets/graph_bfs_step3.png) - -=== "<4>" - ![graph_bfs_step4](graph_traversal.assets/graph_bfs_step4.png) - -=== "<5>" - ![graph_bfs_step5](graph_traversal.assets/graph_bfs_step5.png) - -=== "<6>" - ![graph_bfs_step6](graph_traversal.assets/graph_bfs_step6.png) - -=== "<7>" - ![graph_bfs_step7](graph_traversal.assets/graph_bfs_step7.png) - -=== "<8>" - ![graph_bfs_step8](graph_traversal.assets/graph_bfs_step8.png) - -=== "<9>" - ![graph_bfs_step9](graph_traversal.assets/graph_bfs_step9.png) - -=== "<10>" - ![graph_bfs_step10](graph_traversal.assets/graph_bfs_step10.png) - -=== "<11>" - ![graph_bfs_step11](graph_traversal.assets/graph_bfs_step11.png) - -

图 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) - -

图 9-11   图的深度优先遍历

- -### 1.   算法实现 - -这种“走到尽头再返回”的算法范式通常基于递归来实现。与广度优先遍历类似,在深度优先遍历中我们也需要借助一个哈希表 `visited` 来记录已被访问的顶点,以避免重复访问顶点。 - -=== "Python" - - ```python title="graph_dfs.py" - def dfs(graph: GraphAdjList, visited: set[Vertex], res: list[Vertex], vet: Vertex): - """深度优先遍历 DFS 辅助函数""" - 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]: - """深度优先遍历 DFS""" - # 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点 - # 顶点遍历序列 - res = [] - # 哈希表,用于记录已被访问过的顶点 - visited = set[Vertex]() - dfs(graph, visited, res, start_vet) - return res - ``` - -=== "C++" - - ```cpp title="graph_dfs.cpp" - /* 深度优先遍历 DFS 辅助函数 */ - 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); - } - } - - /* 深度优先遍历 DFS */ - // 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点 - vector graphDFS(GraphAdjList &graph, Vertex *startVet) { - // 顶点遍历序列 - vector res; - // 哈希表,用于记录已被访问过的顶点 - unordered_set visited; - dfs(graph, visited, res, startVet); - return res; - } - ``` - -=== "Java" - - ```java title="graph_dfs.java" - /* 深度优先遍历 DFS 辅助函数 */ - 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); - } - } - - /* 深度优先遍历 DFS */ - // 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点 - 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" - /* 深度优先遍历 DFS 辅助函数 */ - 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); - } - } - - /* 深度优先遍历 DFS */ - // 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点 - List graphDFS(GraphAdjList graph, Vertex startVet) { - // 顶点遍历序列 - List res = new List(); - // 哈希表,用于记录已被访问过的顶点 - HashSet visited = new HashSet(); - dfs(graph, visited, res, startVet); - return res; - } - ``` - -=== "Go" - - ```go title="graph_dfs.go" - /* 深度优先遍历 DFS 辅助函数 */ - 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) - } - } - } - - /* 深度优先遍历 DFS */ - // 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点 - 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" - /* 深度优先遍历 DFS 辅助函数 */ - 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) - } - } - - /* 深度优先遍历 DFS */ - // 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点 - 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" - /* 深度优先遍历 DFS */ - // 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点 - 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); - } - } - - /* 深度优先遍历 DFS */ - // 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点 - function graphDFS(graph, startVet) { - // 顶点遍历序列 - const res = []; - // 哈希表,用于记录已被访问过的顶点 - const visited = new Set(); - dfs(graph, visited, res, startVet); - return res; - } - ``` - -=== "TS" - - ```typescript title="graph_dfs.ts" - /* 深度优先遍历 DFS 辅助函数 */ - 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); - } - } - - /* 深度优先遍历 DFS */ - // 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点 - 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" - /* 深度优先遍历 DFS 辅助函数 */ - 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); - } - } - - /* 深度优先遍历 DFS */ - List graphDFS(GraphAdjList graph, Vertex startVet) { - // 顶点遍历序列 - List res = []; - // 哈希表,用于记录已被访问过的顶点 - Set visited = {}; - dfs(graph, visited, res, startVet); - return res; - } - ``` - -=== "Rust" - - ```rust title="graph_dfs.rs" - /* 深度优先遍历 DFS 辅助函数 */ - 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); - } - } - } - - /* 深度优先遍历 DFS */ - // 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点 - 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" - /* 深度优先遍历 DFS 辅助函数 */ - int resIndex = 0; - void dfs(graphAdjList *graph, hashTable *visited, Vertex *vet, Vertex **res) { - if (hashQuery(visited, vet->pos) == 1) { - return; // 跳过已被访问过的顶点 - } - hashMark(visited, vet->pos); // 标记顶点并将顶点存入数组 - res[resIndex] = vet; // 将顶点存入数组 - resIndex++; - // 遍历该顶点链表 - Node *n = vet->linked->head->next; - while (n != 0) { - // 递归访问邻接顶点 - dfs(graph, visited, n->val, res); - n = n->next; - } - return; - } - - /* 深度优先遍历 DFS */ - // 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点 - Vertex **graphDFS(graphAdjList *graph, Vertex *startVet) { - // 顶点遍历序列 - Vertex **res = (Vertex **)malloc(sizeof(Vertex *) * graph->size); - memset(res, 0, sizeof(Vertex *) * graph->size); - // 哈希表,用于记录已被访问过的顶点 - hashTable *visited = newHash(graph->size); - dfs(graph, visited, startVet, res); - // 释放哈希表内存并将数组索引归零 - freeHash(visited); - resIndex = 0; - // 返回遍历数组 - return res; - } - ``` - -=== "Zig" - - ```zig title="graph_dfs.zig" - [class]{}-[func]{dfs} - - [class]{}-[func]{graphDFS} - ``` - -深度优先遍历的算法流程如图 9-12 所示。 - -- **直虚线代表向下递推**,表示开启了一个新的递归方法来访问新顶点。 -- **曲虚线代表向上回溯**,表示此递归方法已经返回,回溯到了开启此递归方法的位置。 - -为了加深理解,建议将图示与代码结合起来,在脑中(或者用笔画下来)模拟整个 DFS 过程,包括每个递归方法何时开启、何时返回。 - -=== "<1>" - ![图的深度优先遍历步骤](graph_traversal.assets/graph_dfs_step1.png) - -=== "<2>" - ![graph_dfs_step2](graph_traversal.assets/graph_dfs_step2.png) - -=== "<3>" - ![graph_dfs_step3](graph_traversal.assets/graph_dfs_step3.png) - -=== "<4>" - ![graph_dfs_step4](graph_traversal.assets/graph_dfs_step4.png) - -=== "<5>" - ![graph_dfs_step5](graph_traversal.assets/graph_dfs_step5.png) - -=== "<6>" - ![graph_dfs_step6](graph_traversal.assets/graph_dfs_step6.png) - -=== "<7>" - ![graph_dfs_step7](graph_traversal.assets/graph_dfs_step7.png) - -=== "<8>" - ![graph_dfs_step8](graph_traversal.assets/graph_dfs_step8.png) - -=== "<9>" - ![graph_dfs_step9](graph_traversal.assets/graph_dfs_step9.png) - -=== "<10>" - ![graph_dfs_step10](graph_traversal.assets/graph_dfs_step10.png) - -=== "<11>" - ![graph_dfs_step11](graph_traversal.assets/graph_dfs_step11.png) - -

图 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/chapter_greedy/fractional_knapsack_problem.md b/chapter_greedy/fractional_knapsack_problem.md deleted file mode 100644 index fadcaf2a3..000000000 --- a/chapter_greedy/fractional_knapsack_problem.md +++ /dev/null @@ -1,490 +0,0 @@ ---- -comments: true ---- - -# 15.2   分数背包问题 - -!!! question - - 给定 $n$ 个物品,第 $i$ 个物品的重量为 $wgt[i-1]$、价值为 $val[i-1]$ ,和一个容量为 $cap$ 的背包。每个物品只能选择一次,**但可以选择物品的一部分,价值根据选择的重量比例计算**,问在不超过背包容量下背包中物品的最大价值。 - -![分数背包问题的示例数据](fractional_knapsack_problem.assets/fractional_knapsack_example.png) - -

图 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) - -

图 15-4   物品在单位重量下的价值

- -### 1.   贪心策略确定 - -最大化背包内物品总价值,**本质上是要最大化单位重量下的物品价值**。由此便可推出图 15-5 所示的贪心策略。 - -1. 将物品按照单位价值从高到低进行排序。 -2. 遍历所有物品,**每轮贪心地选择单位价值最高的物品**。 -3. 若剩余背包容量不足,则使用当前物品的一部分填满背包即可。 - -![分数背包的贪心策略](fractional_knapsack_problem.assets/fractional_knapsack_greedy_strategy.png) - -

图 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 { - public int w; // 物品重量 - public 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 从高到低进行排序 - 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(by: { -(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" - /* 物品 */ - struct Item { - int w; // 物品重量 - int v; // 物品价值 - }; - - typedef struct Item 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; - } - ``` - -=== "Zig" - - ```zig title="fractional_knapsack.zig" - [class]{Item}-[func]{} - - [class]{}-[func]{fractionalKnapsack} - ``` - -最差情况下,需要遍历整个物品列表,**因此时间复杂度为 $O(n)$** ,其中 $n$ 为物品数量。 - -由于初始化了一个 `Item` 对象列表,**因此空间复杂度为 $O(n)$** 。 - -### 3.   正确性证明 - -采用反证法。假设物品 $x$ 是单位价值最高的物品,使用某算法求得最大价值为 `res` ,但该解中不包含物品 $x$ 。 - -现在从背包中拿出单位重量的任意物品,并替换为单位重量的物品 $x$ 。由于物品 $x$ 的单位价值最高,因此替换后的总价值一定大于 `res` 。**这与 `res` 是最优解矛盾,说明最优解中必须包含物品 $x$** 。 - -对于该解中的其他物品,我们也可以构建出上述矛盾。总而言之,**单位价值更大的物品总是更优选择**,这说明贪心策略是有效的。 - -如图 15-6 所示,如果将物品重量和物品单位价值分别看作一个 2D 图表的横轴和纵轴,则分数背包问题可被转化为“求在有限横轴区间下的最大围成面积”。这个类比可以帮助我们从几何角度理解贪心策略的有效性。 - -![分数背包问题的几何表示](fractional_knapsack_problem.assets/fractional_knapsack_area_chart.png) - -

图 15-6   分数背包问题的几何表示

diff --git a/chapter_greedy/max_capacity_problem.md b/chapter_greedy/max_capacity_problem.md deleted file mode 100644 index 47d46b158..000000000 --- a/chapter_greedy/max_capacity_problem.md +++ /dev/null @@ -1,393 +0,0 @@ ---- -comments: true ---- - -# 15.3   最大容量问题 - -!!! question - - 输入一个数组 $ht$ ,数组中的每个元素代表一个垂直隔板的高度。数组中的任意两个隔板,以及它们之间的空间可以组成一个容器。 - - 容器的容量等于高度和宽度的乘积(即面积),其中高度由较短的隔板决定,宽度是两个隔板的数组索引之差。 - - 请在数组中选择两个隔板,使得组成的容器的容量最大,返回最大容量。 - -![最大容量问题的示例数据](max_capacity_problem.assets/max_capacity_example.png) - -

图 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) - -

图 15-8   初始状态

- -如图 15-9 所示,**若此时将长板 $j$ 向短板 $i$ 靠近,则容量一定变小**。 - -这是因为在移动长板 $j$ 后,宽度 $j-i$ 肯定变小;而高度由短板决定,因此高度只可能不变( $i$ 仍为短板)或变小(移动后的 $j$ 成为短板)。 - -![向内移动长板后的状态](max_capacity_problem.assets/max_capacity_moving_long_board.png) - -

图 15-9   向内移动长板后的状态

- -反向思考,**我们只有向内收缩短板 $i$ ,才有可能使容量变大**。因为虽然宽度一定变小,**但高度可能会变大**(移动后的短板 $i$ 可能会变长)。例如在图 15-10 中,移动短板后面积变大。 - -![向内移动短板后的状态](max_capacity_problem.assets/max_capacity_moving_short_board.png) - -

图 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) - -=== "<2>" - ![max_capacity_greedy_step2](max_capacity_problem.assets/max_capacity_greedy_step2.png) - -=== "<3>" - ![max_capacity_greedy_step3](max_capacity_problem.assets/max_capacity_greedy_step3.png) - -=== "<4>" - ![max_capacity_greedy_step4](max_capacity_problem.assets/max_capacity_greedy_step4.png) - -=== "<5>" - ![max_capacity_greedy_step5](max_capacity_problem.assets/max_capacity_greedy_step5.png) - -=== "<6>" - ![max_capacity_greedy_step6](max_capacity_problem.assets/max_capacity_greedy_step6.png) - -=== "<7>" - ![max_capacity_greedy_step7](max_capacity_problem.assets/max_capacity_greedy_step7.png) - -=== "<8>" - ![max_capacity_greedy_step8](max_capacity_problem.assets/max_capacity_greedy_step8.png) - -=== "<9>" - ![max_capacity_greedy_step9](max_capacity_problem.assets/max_capacity_greedy_step9.png) - -

图 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 = 0, j = ht.count - 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 = MIN(ht[i], ht[j]) * (j - i); - res = MAX(res, capacity); - // 向内移动短板 - if (ht[i] < ht[j]) { - i++; - } else { - j--; - } - } - return res; - } - ``` - -=== "Zig" - - ```zig title="max_capacity.zig" - [class]{}-[func]{maxCapacity} - ``` - -### 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) - -

图 15-12   移动短板导致被跳过的状态

- -观察发现,**这些被跳过的状态实际上就是将长板 $j$ 向内移动的所有状态**。而在第二步中,我们已经证明内移长板一定会导致容量变小。也就是说,被跳过的状态都不可能是最优解,**跳过它们不会导致错过最优解**。 - -以上的分析说明,**移动短板的操作是“安全”的**,贪心策略是有效的。 diff --git a/chapter_greedy/max_product_cutting_problem.md b/chapter_greedy/max_product_cutting_problem.md deleted file mode 100644 index 570bfdf98..000000000 --- a/chapter_greedy/max_product_cutting_problem.md +++ /dev/null @@ -1,369 +0,0 @@ ---- -comments: true ---- - -# 15.4   最大切分乘积问题 - -!!! question - - 给定一个正整数 $n$ ,将其切分为至少两个正整数的和,求切分后所有整数的乘积最大是多少。 - -![最大切分乘积的问题定义](max_product_cutting_problem.assets/max_product_cutting_definition.png) - -

图 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) - -

图 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) - -

图 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); - } - ``` - -=== "Zig" - - ```zig title="max_product_cutting.zig" - [class]{}-[func]{maxProductCutting} - ``` - -![最大切分乘积的计算方法](max_product_cutting_problem.assets/max_product_cutting_greedy_calculation.png) - -

图 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/chapter_hashing/hash_collision.md b/chapter_hashing/hash_collision.md deleted file mode 100644 index a375d0284..000000000 --- a/chapter_hashing/hash_collision.md +++ /dev/null @@ -1,2701 +0,0 @@ ---- -comments: true ---- - -# 6.2   哈希冲突 - -上节提到,**通常情况下哈希函数的输入空间远大于输出空间**,因此理论上哈希冲突是不可避免的。比如,输入空间为全体整数,输出空间为数组容量大小,则必然有多个整数映射至同一桶索引。 - -哈希冲突会导致查询结果错误,严重影响哈希表的可用性。为解决该问题,我们可以每当遇到哈希冲突时就进行哈希表扩容,直至冲突消失为止。此方法简单粗暴且有效,但效率太低,因为哈希表扩容需要进行大量的数据搬运与哈希值计算。为了提升效率,我们可以采用以下策略。 - -1. 改良哈希表数据结构,**使得哈希表可以在存在哈希冲突时正常工作**。 -2. 仅在必要时,即当哈希冲突比较严重时,才执行扩容操作。 - -哈希表的结构改良方法主要包括“链式地址”和“开放寻址”。 - -## 6.2.1   链式地址 - -在原始哈希表中,每个桶仅能存储一个键值对。「链式地址 separate chaining」将单个元素转换为链表,将键值对作为链表节点,将所有发生冲突的键值对都存储在同一链表中。图 6-5 展示了一个链式地址哈希表的例子。 - -![链式地址哈希表](hash_collision.assets/hash_table_chaining.png) - -

图 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(new List()); - } - } - - /* 哈希函数 */ - private int hashFunc(int key) { - return key % capacity; - } - - /* 负载因子 */ - private 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; - } - } - } - - /* 扩容哈希表 */ - private void extend() { - // 暂存原哈希表 - List> bucketsTmp = buckets; - // 初始化扩容后的新哈希表 - capacity *= extendRatio; - buckets = new List>(capacity); - for (int i = 0; i < capacity; i++) { - buckets.Add(new List()); - } - 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 = new List(); - 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 / 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 _, p := range m.buckets[idx] { - if p.key == key { - p.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 / 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 - } - - /* 扩容哈希表 */ - 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" - /* 基于数组简易实现的链式地址哈希表 */ - struct hashMapChaining { - int size; // 键值对数量 - int capacity; // 哈希表容量 - double loadThres; // 触发扩容的负载因子阈值 - int extendRatio; // 扩容倍数 - Pair *buckets; // 桶数组 - }; - - typedef struct hashMapChaining hashMapChaining; - - /* 初始化桶数组 */ - hashMapChaining *newHashMapChaining() { - // 为哈希表分配空间 - int tableSize = 4; - hashMapChaining *hashmap = (hashMapChaining *)malloc(sizeof(hashMapChaining)); - - // 初始化数组 - hashmap->buckets = (Pair *)malloc(sizeof(Pair) * tableSize); - memset(hashmap->buckets, 0, sizeof(Pair) * tableSize); - - hashmap->capacity = tableSize; - hashmap->size = 0; - hashmap->extendRatio = 2; - hashmap->loadThres = 2.0 / 3.0; - - return hashmap; - } - - /* 销毁哈希表 */ - void delHashMapChaining(hashMapChaining *hashmap) { - for (int i = 0; i < hashmap->capacity; i++) { - Pair *pair = &hashmap->buckets[i]; - Node *node = pair->node; - while (node != NULL) { - Node *temp = node; - node = node->next; - free(temp->val); - free(temp); - } - } - free(hashmap->buckets); - free(hashmap); - } - - /* 哈希函数 */ - int hashFunc(hashMapChaining *hashmap, const int key) { - return key % hashmap->capacity; - } - - /* 负载因子 */ - double loadFactor(hashMapChaining *hashmap) { - return (double)hashmap->size / (double)hashmap->capacity; - } - - /* 添加操作 */ - void put(hashMapChaining *hashmap, const int key, char *val) { - if (loadFactor(hashmap) > hashmap->loadThres) { - extend(hashmap); - } - int index = hashFunc(hashmap, key); - - // 先为新节点分配空间再赋值 - Node *newNode = (Node *)malloc(sizeof(Node)); - memset(newNode, 0, sizeof(Node)); - newNode->key = key; - newNode->val = (char *)malloc(strlen(val) + 1); - strcpy(newNode->val, val); - newNode->val[strlen(val)] = '\0'; - - Pair *pair = &hashmap->buckets[index]; - Node *node = pair->node; - if (node == NULL) { - hashmap->buckets[index].node = newNode; - hashmap->size++; - return; - } - while (node != NULL) { - if (node->key == key) { - // 释放先前分配的内存 - free(node->val); - // 更新节点的值 - node->val = (char *)malloc(strlen(val) + 1); - strcpy(node->val, val); - node->val[strlen(val)] = '\0'; - return; - } - if (node->next == NULL) { - break; - } - node = node->next; - } - node->next = newNode; - hashmap->size++; - } - - /* 删除操作 */ - void removeItem(hashMapChaining *hashmap, int key) { - int index = hashFunc(hashmap, key); - Pair *pair = &hashmap->buckets[index]; - Node *node = pair->node; - // 保存后继的节点 - Node *prev = NULL; - while (node != NULL) { - if (node->key == key) { - // 如果要删除的节点是桶的第一个节点 - if (prev == NULL) { - pair->node = node->next; - } else { - prev->next = node->next; - } - // 释放内存 - free(node->val); - free(node); - hashmap->size--; - return; - } - prev = node; - node = node->next; - } - return; - } - - /* 扩容哈希表 */ - void extend(hashMapChaining *hashmap) { - // 暂存原哈希表 - Pair *oldBuckets = hashmap->buckets; - int oldCapacity = hashmap->capacity; - - // 创建新的哈希表,重新分配一段空间 - hashmap->capacity *= hashmap->extendRatio; - hashmap->buckets = (Pair *)malloc(sizeof(Pair) * hashmap->capacity); - memset(hashmap->buckets, 0, sizeof(Pair) * hashmap->capacity); - hashmap->size = 0; - - // 将原哈希表中的键值对重新哈希到新的哈希表中 - for (int i = 0; i < oldCapacity; i++) { - Node *node = oldBuckets[i].node; - while (node != NULL) { - put(hashmap, node->key, node->val); - node = node->next; - } - } - - // 释放原哈希表的内存 - for (int i = 0; i < oldCapacity; i++) { - Node *node = oldBuckets[i].node; - while (node != NULL) { - Node *temp = node; - node = node->next; - free(temp->val); - free(temp); - } - } - free(oldBuckets); - } - - /* 打印哈希表 */ - void print(hashMapChaining *hashmap) { - for (int i = 0; i < hashmap->capacity; i++) { - printf("["); - Pair *pair = &hashmap->buckets[i]; - Node *node = pair->node; - while (node != NULL) { - if (node->val != NULL) { - printf("%d->%s, ", node->key, node->val); - } - node = node->next; - } - printf("]\n"); - } - return; - } - ``` - -=== "Zig" - - ```zig title="hash_map_chaining.zig" - [class]{HashMapChaining}-[func]{} - ``` - -值得注意的是,当链表很长时,查询效率 $O(n)$ 很差。**此时可以将链表转换为“AVL 树”或“红黑树”**,从而将查询操作的时间复杂度优化至 $O(\log n)$ 。 - -## 6.2.2   开放寻址 - -「开放寻址 open addressing」不引入额外的数据结构,而是通过“多次探测”来处理哈希冲突,探测方式主要包括线性探测、平方探测、多次哈希等。 - -下面将主要以线性探测为例,介绍开放寻址哈希表的工作机制与代码实现。 - -### 1.   线性探测 - -线性探测采用固定步长的线性搜索来进行探测,其操作方法与普通哈希表有所不同。 - -- **插入元素**:通过哈希函数计算桶索引,若发现桶内已有元素,则从冲突位置向后线性遍历(步长通常为 $1$ ),直至找到空桶,将元素插入其中。 -- **查找元素**:若发现哈希冲突,则使用相同步长向后线性遍历,直到找到对应元素,返回 `value` 即可;如果遇到空桶,说明目标元素不在哈希表中,返回 $\text{None}$ 。 - -图 6-6 展示了开放寻址(线性探测)哈希表的键值对分布。根据此哈希函数,最后两位相同的 `key` 都会被映射到相同的桶。而通过线性探测,它们被依次存储在该桶以及之下的桶中。 - -![开放寻址和线性探测](hash_collision.assets/hash_table_linear_probing.png) - -

图 6-6   开放寻址和线性探测

- -然而,**线性探测容易产生“聚集现象”**。具体来说,数组中连续被占用的位置越长,这些连续位置发生哈希冲突的可能性越大,从而进一步促使该位置的聚堆生长,形成恶性循环,最终导致增删查改操作效率劣化。 - -值得注意的是,**我们不能在开放寻址哈希表中直接删除元素**。这是因为删除元素会在数组内产生一个空桶 $\text{None}$ ,而当查询元素时,线性探测到该空桶就会返回,因此在该空桶之下的元素都无法再被访问到,程序可能误判这些元素不存在。 - -![在开放寻址中删除元素导致的查询问题](hash_collision.assets/hash_table_open_addressing_deletion.png) - -

图 6-7   在开放寻址中删除元素导致的查询问题

- -为了解决该问题,我们可以采用「懒删除 lazy deletion」机制:它不直接从哈希表中移除元素,**而是利用一个常量 `TOMBSTONE` 来标记这个桶**。在该机制下,$\text{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 { - private int size; // 键值对数量 - private int capacity = 4; // 哈希表容量 - private double loadThres = 2.0 / 3.0; // 触发扩容的负载因子阈值 - private int extendRatio = 2; // 扩容倍数 - private Pair[] buckets; // 桶数组 - private 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; - // 将键值对从原哈希表搬运至新哈希表 - 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 // 桶数组 - removed pair // 删除标记 - } - - /* 构造方法 */ - func newHashMapOpenAddressing() *hashMapOpenAddressing { - buckets := make([]pair, 4) - return &hashMapOpenAddressing{ - size: 0, - capacity: 4, - loadThres: 2.0 / 3.0, - extendRatio: 2, - buckets: buckets, - removed: pair{ - key: -1, - val: "-1", - }, - } - } - - /* 哈希函数 */ - func (m *hashMapOpenAddressing) hashFunc(key int) int { - return key % m.capacity - } - - /* 负载因子 */ - func (m *hashMapOpenAddressing) loadFactor() float64 { - return float64(m.size) / float64(m.capacity) - } - - /* 查询操作 */ - func (m *hashMapOpenAddressing) get(key int) string { - idx := m.hashFunc(key) - // 线性探测,从 index 开始向后遍历 - for i := 0; i < m.capacity; i++ { - // 计算桶索引,越过尾部返回头部 - j := (idx + 1) % m.capacity - // 若遇到空桶,说明无此 key ,则返回 null - if m.buckets[j] == (pair{}) { - return "" - } - // 若遇到指定 key ,则返回对应 val - if m.buckets[j].key == key && m.buckets[j] != m.removed { - return m.buckets[j].val - } - } - // 若未找到 key 则返回空字符串 - return "" - } - - /* 添加操作 */ - func (m *hashMapOpenAddressing) put(key int, val string) { - // 当负载因子超过阈值时,执行扩容 - if m.loadFactor() > m.loadThres { - m.extend() - } - idx := m.hashFunc(key) - // 线性探测,从 index 开始向后遍历 - for i := 0; i < m.capacity; i++ { - // 计算桶索引,越过尾部返回头部 - j := (idx + i) % m.capacity - // 若遇到空桶、或带有删除标记的桶,则将键值对放入该桶 - if m.buckets[j] == (pair{}) || m.buckets[j] == m.removed { - m.buckets[j] = pair{ - key: key, - val: val, - } - m.size += 1 - return - } - // 若遇到指定 key ,则更新对应 val - if m.buckets[j].key == key { - m.buckets[j].val = val - } - } - } - - /* 删除操作 */ - func (m *hashMapOpenAddressing) remove(key int) { - idx := m.hashFunc(key) - // 遍历桶,从中删除键值对 - // 线性探测,从 index 开始向后遍历 - for i := 0; i < m.capacity; i++ { - // 计算桶索引,越过尾部返回头部 - j := (idx + 1) % m.capacity - // 若遇到空桶,说明无此 key ,则直接返回 - if m.buckets[j] == (pair{}) { - return - } - // 若遇到指定 key ,则标记删除并返回 - if m.buckets[j].key == key { - m.buckets[j] = m.removed - m.size -= 1 - } - } - } - - /* 扩容哈希表 */ - func (m *hashMapOpenAddressing) extend() { - // 暂存原哈希表 - tmpBuckets := make([]pair, len(m.buckets)) - copy(tmpBuckets, m.buckets) - - // 初始化扩容后的新哈希表 - m.capacity *= m.extendRatio - m.buckets = make([]pair, m.capacity) - m.size = 0 - // 将键值对从原哈希表搬运至新哈希表 - for _, p := range tmpBuckets { - if p != (pair{}) && p != m.removed { - m.put(p.key, p.val) - } - } - } - - /* 打印哈希表 */ - func (m *hashMapOpenAddressing) print() { - for _, p := range m.buckets { - if p != (pair{}) { - fmt.Println(strconv.Itoa(p.key) + " -> " + p.val) - } else { - fmt.Println("nil") - } - } - } - ``` - -=== "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 / 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; // 桶数组 - #removed; // 删除标记 - - /* 构造方法 */ - constructor() { - this.#size = 0; - this.#capacity = 4; - this.#loadThres = 2.0 / 3.0; - this.#extendRatio = 2; - this.#buckets = new Array(this.#capacity).fill(null); - this.#removed = new Pair(-1, '-1'); - } - - /* 哈希函数 */ - #hashFunc(key) { - return key % this.#capacity; - } - - /* 负载因子 */ - #loadFactor() { - return this.#size / this.#capacity; - } - - /* 查询操作 */ - get(key) { - const index = this.#hashFunc(key); - // 线性探测,从 index 开始向后遍历 - for (let i = 0; i < this.#capacity; i++) { - // 计算桶索引,越过尾部返回头部 - const j = (index + i) % this.#capacity; - // 若遇到空桶,说明无此 key ,则返回 null - if (this.#buckets[j] === null) return null; - // 若遇到指定 key ,则返回对应 val - if ( - this.#buckets[j].key === key && - this.#buckets[j][key] !== this.#removed.key - ) - return this.#buckets[j].val; - } - return null; - } - - /* 添加操作 */ - put(key, val) { - // 当负载因子超过阈值时,执行扩容 - if (this.#loadFactor() > this.#loadThres) { - this.#extend(); - } - const index = this.#hashFunc(key); - // 线性探测,从 index 开始向后遍历 - for (let i = 0; i < this.#capacity; i++) { - // 计算桶索引,越过尾部返回头部 - let j = (index + i) % this.#capacity; - // 若遇到空桶、或带有删除标记的桶,则将键值对放入该桶 - if ( - this.#buckets[j] === null || - this.#buckets[j][key] === this.#removed.key - ) { - this.#buckets[j] = new Pair(key, val); - this.#size += 1; - return; - } - // 若遇到指定 key ,则更新对应 val - if (this.#buckets[j].key === key) { - this.#buckets[j].val = val; - return; - } - } - } - - /* 删除操作 */ - remove(key) { - const index = this.#hashFunc(key); - // 线性探测,从 index 开始向后遍历 - for (let i = 0; i < this.#capacity; i++) { - // 计算桶索引,越过尾部返回头部 - const j = (index + i) % this.#capacity; - // 若遇到空桶,说明无此 key ,则直接返回 - if (this.#buckets[j] === null) { - return; - } - // 若遇到指定 key ,则标记删除并返回 - if (this.#buckets[j].key === key) { - this.#buckets[j] = this.#removed; - this.#size -= 1; - return; - } - } - } - - /* 扩容哈希表 */ - #extend() { - // 暂存原哈希表 - const bucketsTmp = this.#buckets; - // 初始化扩容后的新哈希表 - this.#capacity *= this.#extendRatio; - this.#buckets = new Array(this.#capacity).fill(null); - this.#size = 0; - // 将键值对从原哈希表搬运至新哈希表 - for (const pair of bucketsTmp) { - if (pair !== null && pair.key !== this.#removed.key) { - this.put(pair.key, pair.val); - } - } - } - - /* 打印哈希表 */ - print() { - for (const pair of this.#buckets) { - if (pair !== null) { - console.log(pair.key + ' -> ' + pair.val); - } else { - console.log('null'); - } - } - } - } - ``` - -=== "TS" - - ```typescript title="hash_map_open_addressing.ts" - /* 开放寻址哈希表 */ - class HashMapOpenAddressing { - #size: number; // 键值对数量 - #capacity: number; // 哈希表容量 - #loadThres: number; // 触发扩容的负载因子阈值 - #extendRatio: number; // 扩容倍数 - #buckets: Pair[]; // 桶数组 - #removed: 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); - this.#removed = new Pair(-1, '-1'); - } - - /* 哈希函数 */ - #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); - // 线性探测,从 index 开始向后遍历 - for (let i = 0; i < this.#capacity; i++) { - // 计算桶索引,越过尾部返回头部 - const j = (index + i) % this.#capacity; - // 若遇到空桶,说明无此 key ,则返回 null - if (this.#buckets[j] === null) return null; - // 若遇到指定 key ,则返回对应 val - if ( - this.#buckets[j].key === key && - this.#buckets[j][key] !== this.#removed.key - ) - return this.#buckets[j].val; - } - return null; - } - - /* 添加操作 */ - put(key: number, val: string): void { - // 当负载因子超过阈值时,执行扩容 - if (this.#loadFactor() > this.#loadThres) { - this.#extend(); - } - const index = this.#hashFunc(key); - // 线性探测,从 index 开始向后遍历 - for (let i = 0; i < this.#capacity; i++) { - // 计算桶索引,越过尾部返回头部 - let j = (index + i) % this.#capacity; - // 若遇到空桶、或带有删除标记的桶,则将键值对放入该桶 - if ( - this.#buckets[j] === null || - this.#buckets[j][key] === this.#removed.key - ) { - this.#buckets[j] = new Pair(key, val); - this.#size += 1; - return; - } - // 若遇到指定 key ,则更新对应 val - if (this.#buckets[j].key === key) { - this.#buckets[j].val = val; - return; - } - } - } - - /* 删除操作 */ - remove(key: number): void { - const index = this.#hashFunc(key); - // 线性探测,从 index 开始向后遍历 - for (let i = 0; i < this.#capacity; i++) { - // 计算桶索引,越过尾部返回头部 - const j = (index + i) % this.#capacity; - // 若遇到空桶,说明无此 key ,则直接返回 - if (this.#buckets[j] === null) { - return; - } - // 若遇到指定 key ,则标记删除并返回 - if (this.#buckets[j].key === key) { - this.#buckets[j] = this.#removed; - this.#size -= 1; - return; - } - } - } - - /* 扩容哈希表 */ - #extend(): void { - // 暂存原哈希表 - const bucketsTmp = this.#buckets; - // 初始化扩容后的新哈希表 - this.#capacity *= this.#extendRatio; - this.#buckets = new Array(this.#capacity).fill(null); - this.#size = 0; - // 将键值对从原哈希表搬运至新哈希表 - for (const pair of bucketsTmp) { - if (pair !== null && pair.key !== this.#removed.key) { - this.put(pair.key, pair.val); - } - } - } - - /* 打印哈希表 */ - print(): void { - for (const pair of this.#buckets) { - if (pair !== null) { - console.log(pair.key + ' -> ' + pair.val); - } else { - console.log('null'); - } - } - } - } - ``` - -=== "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" - [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)$ ,以此类推,直到找到空桶后插入元素。 -- **查找元素**:在相同的哈希函数顺序下进行查找,直到找到目标元素时返回;或当遇到空桶或已尝试所有哈希函数,说明哈希表中不存在该元素,则返回 $\text{None}$ 。 - -与线性探测相比,多次哈希方法不易产生聚集,但多个哈希函数会增加额外的计算量。 - -!!! tip - - 请注意,开放寻址(线性探测、平方探测和多次哈希)哈希表都存在“不能直接删除元素”的问题。 - -## 6.2.3   编程语言的选择 - -各个编程语言采取了不同的哈希表实现策略,以下举几个例子。 - -- Java 采用链式地址。自 JDK 1.8 以来,当 HashMap 内数组长度达到 64 且链表长度达到 8 时,链表会被转换为红黑树以提升查找性能。 -- Python 采用开放寻址。字典 dict 使用伪随机数进行探测。 -- Golang 采用链式地址。Go 规定每个桶最多存储 8 个键值对,超出容量则连接一个溢出桶。当溢出桶过多时,会执行一次特殊的等量扩容操作,以确保性能。 diff --git a/chapter_hashing/hash_map.md b/chapter_hashing/hash_map.md deleted file mode 100755 index 8e09e6724..000000000 --- a/chapter_hashing/hash_map.md +++ /dev/null @@ -1,1669 +0,0 @@ ---- -comments: true ---- - -# 6.1   哈希表 - -「哈希表 hash table」,又称「散列表」,其通过建立键 `key` 与值 `value` 之间的映射,实现高效的元素查询。具体而言,我们向哈希表输入一个键 `key` ,则可以在 $O(1)$ 时间内获取对应的值 `value` 。 - -如图 6-1 所示,给定 $n$ 个学生,每个学生都有“姓名”和“学号”两项数据。假如我们希望实现“输入一个学号,返回对应的姓名”的查询功能,则可以采用图 6-1 所示的哈希表来实现。 - -![哈希表的抽象表示](hash_map.assets/hash_table_lookup.png) - -

图 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) - map.Add(12836, "小哈"); - map.Add(15937, "小啰"); - map.Add(16750, "小算"); - map.Add(13276, "小法"); - map.Add(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 ArrayHashMap(); - /* 添加操作 */ - // 在哈希表中添加键值对 (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 未提供内置哈希表 - ``` - -=== "Zig" - - ```zig title="hash_map.zig" - - ``` - -哈希表有三种常用遍历方式:遍历键值对、遍历键和遍历值。 - -=== "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 - for (auto kv: map) { - cout << kv.first << endl; - } - // 单独遍历值 value - for (auto kv: map) { - cout << kv.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 未提供内置哈希表 - ``` - -=== "Zig" - - ```zig title="hash_map.zig" - - ``` - -## 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) - -

图 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 { - 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(); - 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[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[index] = pair; - } - - /* 删除操作 */ - public void remove(int key) { - int index = hashFunc(key); - // 置为 null ,代表删除 - buckets[index] = null; - } - - /* 获取所有键值对 */ - public List pairSet() { - List pairSet = new(); - foreach (Pair? pair in buckets) { - if (pair != null) - pairSet.Add(pair); - } - return pairSet; - } - - /* 获取所有键 */ - public List keySet() { - List keySet = new(); - foreach (Pair? pair in buckets) { - if (pair != null) - keySet.Add(pair.key); - } - return keySet; - } - - /* 获取所有值 */ - public List valueSet() { - List valueSet = new(); - 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 { - var key: Int - var val: String - - init(key: Int, val: String) { - self.key = key - self.val = val - } - } - - /* 基于数组简易实现的哈希表 */ - class ArrayHashMap { - private var buckets: [Pair?] = [] - - init() { - // 初始化数组,包含 100 个桶 - for _ in 0 ..< 100 { - buckets.append(nil) - } - } - - /* 哈希函数 */ - 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] { - var pairSet: [Pair] = [] - for pair in buckets { - if let pair = pair { - pairSet.append(pair) - } - } - return pairSet - } - - /* 获取所有键 */ - func keySet() -> [Int] { - var keySet: [Int] = [] - for pair in buckets { - if let pair = pair { - keySet.append(pair.key) - } - } - return keySet - } - - /* 获取所有值 */ - func valueSet() -> [String] { - var valueSet: [String] = [] - for pair in buckets { - if let pair = pair { - valueSet.append(pair.val) - } - } - return valueSet - } - - /* 打印哈希表 */ - 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 */ - struct pair { - int key; - char *val; - }; - - typedef struct pair pair; - - /* 基于数组简易实现的哈希表 */ - struct arrayHashMap { - pair *buckets[HASH_MAP_DEFAULT_SIZE]; - }; - - typedef struct arrayHashMap arrayHashMap; - - /* 哈希表初始化函数 */ - arrayHashMap *newArrayHashMap() { - arrayHashMap *map = malloc(sizeof(arrayHashMap)); - return map; - } - - /* 添加操作 */ - void put(arrayHashMap *d, 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); - d->buckets[index] = pair; - } - - /* 删除操作 */ - void removeItem(arrayHashMap *d, const int key) { - int index = hashFunc(key); - free(d->buckets[index]->val); - free(d->buckets[index]); - d->buckets[index] = NULL; - } - - /* 获取所有键值对 */ - void pairSet(arrayHashMap *d, mapSet *set) { - pair *entries; - int i = 0, index = 0; - int total = 0; - - /* 统计有效键值对数量 */ - for (i = 0; i < HASH_MAP_DEFAULT_SIZE; i++) { - if (d->buckets[i] != NULL) { - total++; - } - } - - entries = malloc(sizeof(pair) * total); - for (i = 0; i < HASH_MAP_DEFAULT_SIZE; i++) { - if (d->buckets[i] != NULL) { - entries[index].key = d->buckets[i]->key; - entries[index].val = malloc(strlen(d->buckets[i]->val + 1)); - strcpy(entries[index].val, d->buckets[i]->val); - index++; - } - } - - set->set = entries; - set->len = total; - } - - /* 获取所有键 */ - void keySet(arrayHashMap *d, mapSet *set) { - int *keys; - int i = 0, index = 0; - int total = 0; - - /* 统计有效键值对数量 */ - for (i = 0; i < HASH_MAP_DEFAULT_SIZE; i++) { - if (d->buckets[i] != NULL) { - total++; - } - } - - keys = malloc(total * sizeof(int)); - for (i = 0; i < HASH_MAP_DEFAULT_SIZE; i++) { - if (d->buckets[i] != NULL) { - keys[index] = d->buckets[i]->key; - index++; - } - } - - set->set = keys; - set->len = total; - } - - /* 获取所有值 */ - void valueSet(arrayHashMap *d, mapSet *set) { - char **vals; - int i = 0, index = 0; - int total = 0; - - /* 统计有效键值对数量 */ - for (i = 0; i < HASH_MAP_DEFAULT_SIZE; i++) { - if (d->buckets[i] != NULL) { - total++; - } - } - - vals = malloc(total * sizeof(char *)); - for (i = 0; i < HASH_MAP_DEFAULT_SIZE; i++) { - if (d->buckets[i] != NULL) { - vals[index] = d->buckets[i]->val; - index++; - } - } - - set->set = vals; - set->len = total; - } - - /* 打印哈希表 */ - void print(arrayHashMap *d) { - int i; - mapSet set; - pairSet(d, &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); - } - ``` - -=== "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}); - } - } - }; - } - ``` - -## 6.1.3   哈希冲突与扩容 - -本质上看,哈希函数的作用是将所有 `key` 构成的输入空间映射到数组所有索引构成的输出空间,而输入空间往往远大于输出空间。因此,**理论上一定存在“多个输入对应相同输出”的情况**。 - -对于上述示例中的哈希函数,当输入的 `key` 后两位相同时,哈希函数的输出结果也相同。例如,查询学号为 12836 和 20336 的两个学生时,我们得到: - -```shell -12836 % 100 = 36 -20336 % 100 = 36 -``` - -如图 6-3 所示,两个学号指向了同一个姓名,这显然是不对的。我们将这种多个输入对应同一输出的情况称为「哈希冲突 hash collision」。 - -![哈希冲突示例](hash_map.assets/hash_collision.png) - -

图 6-3   哈希冲突示例

- -容易想到,哈希表容量 $n$ 越大,多个 `key` 被分配到同一个桶中的概率就越低,冲突就越少。因此,**我们可以通过扩容哈希表来减少哈希冲突**。 - -如图 6-4 所示,扩容前键值对 `(136, A)` 和 `(236, D)` 发生冲突,扩容后冲突消失。 - -![哈希表扩容](hash_map.assets/hash_table_reshash.png) - -

图 6-4   哈希表扩容

- -类似于数组扩容,哈希表扩容需将所有键值对从原哈希表迁移至新哈希表,非常耗时。并且由于哈希表容量 `capacity` 改变,我们需要通过哈希函数来重新计算所有键值对的存储位置,这进一步提高了扩容过程的计算开销。为此,编程语言通常会预留足够大的哈希表容量,防止频繁扩容。 - -「负载因子 load factor」是哈希表的一个重要概念,其定义为哈希表的元素数量除以桶数量,用于衡量哈希冲突的严重程度,**也常被作为哈希表扩容的触发条件**。例如在 Java 中,当负载因子超过 $0.75$ 时,系统会将哈希表容量扩展为原先的 $2$ 倍。 diff --git a/chapter_heap/heap.md b/chapter_heap/heap.md deleted file mode 100644 index 4dfca5035..000000000 --- a/chapter_heap/heap.md +++ /dev/null @@ -1,1606 +0,0 @@ ---- -comments: true ---- - -# 8.1   堆 - -「堆 heap」是一种满足特定条件的完全二叉树,主要可分为图 8-1 所示的两种类型。 - -- 「大顶堆 max heap」:任意节点的值 $\geq$ 其子节点的值。 -- 「小顶堆 min heap」:任意节点的值 $\leq$ 其子节点的值。 - -![小顶堆与大顶堆](heap.assets/min_heap_and_max_heap.png) - -

图 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)$ | - -
- -在实际应用中,我们可以直接使用编程语言提供的堆类(或优先队列类)。 - -!!! tip - - 类似于排序算法中的“从小到大排列”和“从大到小排列”,我们可以通过修改 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 PriorityQueue(); - // 初始化大顶堆(使用 lambda 表达式修改 Comparator 即可) - PriorityQueue maxHeap = new PriorityQueue(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(new List<(int, int)> { (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 类 - ``` - -=== "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 类 - ``` - -=== "Zig" - - ```zig title="heap.zig" - - ``` - -## 8.1.2   堆的实现 - -下文实现的是大顶堆。若要将其转换为小顶堆,只需将所有大小逻辑判断取逆(例如,将 $\geq$ 替换为 $\leq$ )。感兴趣的读者可以自行实现。 - -### 1.   堆的存储与表示 - -我们在二叉树章节中学习到,完全二叉树非常适合用数组来表示。由于堆正是一种完全二叉树,**我们将采用数组来存储堆**。 - -当使用数组表示二叉树时,元素代表节点值,索引代表节点在二叉树中的位置。**节点指针通过索引映射公式来实现**。 - -如图 8-2 所示,给定索引 $i$ ,其左子节点索引为 $2i + 1$ ,右子节点索引为 $2i + 2$ ,父节点索引为 $(i - 1) / 2$(向下取整)。当索引越界时,表示空节点或节点不存在。 - -![堆的表示与存储](heap.assets/representation_of_heap.png) - -

图 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 *h, int i) { - return 2 * i + 1; - } - - /* 获取右子节点索引 */ - int right(maxHeap *h, int i) { - return 2 * i + 2; - } - - /* 获取父节点索引 */ - int parent(maxHeap *h, int i) { - return (i - 1) / 2; - } - ``` - -=== "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 *h) { - return h->data[0]; - } - ``` - -=== "Zig" - - ```zig title="my_heap.zig" - // 访问堆顶元素 - fn peek(self: *Self) T { - return self.max_heap.?.items[0]; - } - ``` - -### 3.   元素入堆 - -给定元素 `val` ,我们首先将其添加到堆底。添加之后,由于 val 可能大于堆中其他元素,堆的成立条件可能已被破坏。因此,**需要修复从插入节点到根节点的路径上的各个节点**,这个操作被称为「堆化 heapify」。 - -考虑从入堆节点开始,**从底至顶执行堆化**。如图 8-3 所示,我们比较插入节点与其父节点的值,如果插入节点更大,则将它们交换。然后继续执行此操作,从底至顶修复堆中的各个节点,直至越过根节点或遇到无须交换的节点时结束。 - -=== "<1>" - ![元素入堆步骤](heap.assets/heap_push_step1.png) - -=== "<2>" - ![heap_push_step2](heap.assets/heap_push_step2.png) - -=== "<3>" - ![heap_push_step3](heap.assets/heap_push_step3.png) - -=== "<4>" - ![heap_push_step4](heap.assets/heap_push_step4.png) - -=== "<5>" - ![heap_push_step5](heap.assets/heap_push_step5.png) - -=== "<6>" - ![heap_push_step6](heap.assets/heap_push_step6.png) - -=== "<7>" - ![heap_push_step7](heap.assets/heap_push_step7.png) - -=== "<8>" - ![heap_push_step8](heap.assets/heap_push_step8.png) - -=== "<9>" - ![heap_push_step9](heap.assets/heap_push_step9.png) - -

图 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 *h, int val) { - // 默认情况下,不应该添加这么多节点 - if (h->size == MAX_SIZE) { - printf("heap is full!"); - return; - } - // 添加节点 - h->data[h->size] = val; - h->size++; - - // 从底至顶堆化 - siftUp(h, h->size - 1); - } - - /* 从节点 i 开始,从底至顶堆化 */ - void siftUp(maxHeap *h, int i) { - while (true) { - // 获取节点 i 的父节点 - int p = parent(h, i); - // 当“越过根节点”或“节点无须修复”时,结束堆化 - if (p < 0 || h->data[i] <= h->data[p]) { - break; - } - // 交换两节点 - swap(h, i, p); - // 循环向上堆化 - i = p; - } - } - ``` - -=== "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; - } - } - ``` - -### 4.   堆顶元素出堆 - -堆顶元素是二叉树的根节点,即列表首元素。如果我们直接从列表中删除首元素,那么二叉树中所有节点的索引都会发生变化,这将使得后续使用堆化修复变得困难。为了尽量减少元素索引的变动,我们采用以下操作步骤。 - -1. 交换堆顶元素与堆底元素(即交换根节点与最右叶节点)。 -2. 交换完成后,将堆底从列表中删除(注意,由于已经交换,实际上删除的是原来的堆顶元素)。 -3. 从根节点开始,**从顶至底执行堆化**。 - -如图 8-4 所示,**“从顶至底堆化”的操作方向与“从底至顶堆化”相反**,我们将根节点的值与其两个子节点的值进行比较,将最大的子节点与根节点交换。然后循环执行此操作,直到越过叶节点或遇到无须交换的节点时结束。 - -=== "<1>" - ![堆顶元素出堆步骤](heap.assets/heap_pop_step1.png) - -=== "<2>" - ![heap_pop_step2](heap.assets/heap_pop_step2.png) - -=== "<3>" - ![heap_pop_step3](heap.assets/heap_pop_step3.png) - -=== "<4>" - ![heap_pop_step4](heap.assets/heap_pop_step4.png) - -=== "<5>" - ![heap_pop_step5](heap.assets/heap_pop_step5.png) - -=== "<6>" - ![heap_pop_step6](heap.assets/heap_pop_step6.png) - -=== "<7>" - ![heap_pop_step7](heap.assets/heap_pop_step7.png) - -=== "<8>" - ![heap_pop_step8](heap.assets/heap_pop_step8.png) - -=== "<9>" - ![heap_pop_step9](heap.assets/heap_pop_step9.png) - -=== "<10>" - ![heap_pop_step10](heap.assets/heap_pop_step10.png) - -

图 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 *h) { - // 判空处理 - if (isEmpty(h)) { - printf("heap is empty!"); - return INT_MAX; - } - // 交换根节点与最右叶节点(即交换首元素与尾元素) - swap(h, 0, size(h) - 1); - // 删除节点 - int val = h->data[h->size - 1]; - h->size--; - // 从顶至底堆化 - siftDown(h, 0); - - // 返回堆顶元素 - return val; - } - - /* 从节点 i 开始,从顶至底堆化 */ - void siftDown(maxHeap *h, int i) { - while (true) { - // 判断节点 i, l, r 中值最大的节点,记为 max - int l = left(h, i); - int r = right(h, i); - int max = i; - if (l < size(h) && h->data[l] > h->data[max]) { - max = l; - } - if (r < size(h) && h->data[r] > h->data[max]) { - max = r; - } - // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出 - if (max == i) { - break; - } - // 交换两节点 - swap(h, i, max); - // 循环向下堆化 - i = max; - } - } - ``` - -=== "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; - } - } - ``` - -## 8.1.3   堆常见应用 - -- **优先队列**:堆通常作为实现优先队列的首选数据结构,其入队和出队操作的时间复杂度均为 $O(\log n)$ ,而建队操作为 $O(n)$ ,这些操作都非常高效。 -- **堆排序**:给定一组数据,我们可以用它们建立一个堆,然后不断地执行元素出堆操作,从而得到有序数据。然而,我们通常会使用一种更优雅的方式实现堆排序,详见后续的堆排序章节。 -- **获取最大的 $k$ 个元素**:这是一个经典的算法问题,同时也是一种典型应用,例如选择热度前 10 的新闻作为微博热搜,选取销量前 10 的商品等。 diff --git a/chapter_heap/top_k.md b/chapter_heap/top_k.md deleted file mode 100644 index f03169eb9..000000000 --- a/chapter_heap/top_k.md +++ /dev/null @@ -1,307 +0,0 @@ ---- -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) - -

图 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) - -

图 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) - -=== "<2>" - ![top_k_heap_step2](top_k.assets/top_k_heap_step2.png) - -=== "<3>" - ![top_k_heap_step3](top_k.assets/top_k_heap_step3.png) - -=== "<4>" - ![top_k_heap_step4](top_k.assets/top_k_heap_step4.png) - -=== "<5>" - ![top_k_heap_step5](top_k.assets/top_k_heap_step5.png) - -=== "<6>" - ![top_k_heap_step6](top_k.assets/top_k_heap_step6.png) - -=== "<7>" - ![top_k_heap_step7](top_k.assets/top_k_heap_step7.png) - -=== "<8>" - ![top_k_heap_step8](top_k.assets/top_k_heap_step8.png) - -=== "<9>" - ![top_k_heap_step9](top_k.assets/top_k_heap_step9.png) - -

图 8-8   基于堆寻找最大的 k 个元素

- -总共执行了 $n$ 轮入堆和出堆,堆的最大长度为 $k$ ,因此时间复杂度为 $O(n \log k)$ 。该方法的效率很高,当 $k$ 较小时,时间复杂度趋向 $O(n)$ ;当 $k$ 较大时,时间复杂度不会超过 $O(n \log n)$ 。 - -另外,该方法适用于动态数据流的使用场景。在不断加入数据时,我们可以持续维护堆内的元素,从而实现最大 $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 PriorityQueue(); - // 将数组的前 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 = Array(nums.prefix(k)) - // 从第 k+1 个元素开始,保持堆的长度为 k - for i in stride(from: k, to: nums.count, by: 1) { - // 若当前元素大于堆顶元素,则将堆顶元素出堆、当前元素入堆 - if nums[i] > heap.first! { - heap.removeFirst() - heap.insert(nums[i], at: 0) - } - } - return heap - } - ``` - -=== "JS" - - ```javascript title="top_k.js" - /* 基于堆查找数组中最大的 k 个元素 */ - function topKHeap(nums, k) { - // 使用大顶堆 MaxHeap ,对数组 nums 取相反数 - const invertedNums = nums.map((num) => -num); - // 将数组的前 k 个元素入堆 - const heap = new MaxHeap(invertedNums.slice(0, k)); - // 从第 k+1 个元素开始,保持堆的长度为 k - for (let i = k; i < invertedNums.length; i++) { - // 若当前元素小于堆顶元素,则将堆顶元素出堆、当前元素入堆 - if (invertedNums[i] < heap.peek()) { - heap.pop(); - heap.push(invertedNums[i]); - } - } - // 取出堆中元素 - const maxHeap = heap.getMaxHeap(); - // 对堆中元素取相反数 - const invertedMaxHeap = maxHeap.map((num) => -num); - return invertedMaxHeap; - } - ``` - -=== "TS" - - ```typescript title="top_k.ts" - /* 基于堆查找数组中最大的 k 个元素 */ - function topKHeap(nums: number[], k: number): number[] { - // 将堆中所有元素取反,从而用大顶堆来模拟小顶堆 - const invertedNums = nums.map((num) => -num); - // 将数组的前 k 个元素入堆 - const heap = new MaxHeap(invertedNums.slice(0, k)); - // 从第 k+1 个元素开始,保持堆的长度为 k - for (let i = k; i < invertedNums.length; i++) { - // 若当前元素小于堆顶元素,则将堆顶元素出堆、当前元素入堆 - if (invertedNums[i] < heap.peek()) { - heap.pop(); - heap.push(invertedNums[i]); - } - } - // 取出堆中元素 - const maxHeap = heap.getMaxHeap(); - // 对堆中元素取相反数 - const invertedMaxHeap = maxHeap.map((num) => -num); - return invertedMaxHeap; - } - ``` - -=== "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> { - // Rust 的 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" - [class]{}-[func]{topKHeap} - ``` - -=== "Zig" - - ```zig title="top_k.zig" - [class]{}-[func]{topKHeap} - ``` diff --git a/chapter_searching/binary_search.md b/chapter_searching/binary_search.md deleted file mode 100755 index a41018976..000000000 --- a/chapter_searching/binary_search.md +++ /dev/null @@ -1,645 +0,0 @@ ---- -comments: true ---- - -# 10.1   二分查找 - -「二分查找 binary search」是一种基于分治策略的高效搜索算法。它利用数据的有序性,每轮减少一半搜索范围,直至找到目标元素或搜索区间为空为止。 - -!!! question - - 给定一个长度为 $n$ 的数组 `nums` ,元素按从小到大的顺序排列,数组不包含重复元素。请查找并返回元素 `target` 在该数组中的索引。若数组不包含该元素,则返回 $-1$ 。 - -![二分查找示例数据](binary_search.assets/binary_search_example.png) - -

图 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) - -=== "<2>" - ![binary_search_step2](binary_search.assets/binary_search_step2.png) - -=== "<3>" - ![binary_search_step3](binary_search.assets/binary_search_step3.png) - -=== "<4>" - ![binary_search_step4](binary_search.assets/binary_search_step4.png) - -=== "<5>" - ![binary_search_step5](binary_search.assets/binary_search_step5.png) - -=== "<6>" - ![binary_search_step6](binary_search.assets/binary_search_step6.png) - -=== "<7>" - ![binary_search_step7](binary_search.assets/binary_search_step7.png) - -

图 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 = 0 - var j = nums.count - 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; - } - ``` - -=== "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; - } - ``` - -**时间复杂度 $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 = 0 - var j = nums.count - // 循环,当搜索区间为空时跳出(当 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 - 1; - } 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; - } - ``` - -=== "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; - } - ``` - -如图 10-3 所示,在两种区间表示下,二分查找算法的初始化、循环条件和缩小区间操作皆有所不同。 - -由于“双闭区间”表示中的左右边界都被定义为闭区间,因此指针 $i$ 和 $j$ 缩小区间操作也是对称的。这样更不容易出错,**因此一般建议采用“双闭区间”的写法**。 - -![两种区间定义](binary_search.assets/binary_search_ranges.png) - -

图 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/chapter_searching/binary_search_edge.md b/chapter_searching/binary_search_edge.md deleted file mode 100644 index 9c63d81fa..000000000 --- a/chapter_searching/binary_search_edge.md +++ /dev/null @@ -1,438 +0,0 @@ ---- -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.count || 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; - } - ``` - -=== "Zig" - - ```zig title="binary_search_edge.zig" - [class]{}-[func]{binarySearchLeftEdge} - ``` - -## 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) - -

图 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; - } - ``` - -=== "Zig" - - ```zig title="binary_search_edge.zig" - [class]{}-[func]{binarySearchRightEdge} - ``` - -### 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) - -

图 10-8   将查找边界转化为查找元素

- -代码在此省略,值得注意以下两点。 - -- 给定数组不包含小数,这意味着我们无须关心如何处理相等的情况。 -- 因为该方法引入了小数,所以需要将函数中的变量 `target` 改为浮点数类型。 diff --git a/chapter_searching/binary_search_insertion.md b/chapter_searching/binary_search_insertion.md deleted file mode 100644 index 8ec04f5fe..000000000 --- a/chapter_searching/binary_search_insertion.md +++ /dev/null @@ -1,578 +0,0 @@ ---- -comments: true ---- - -# 10.2   二分查找插入点 - -二分查找不仅可用于搜索目标元素,还具有许多变种问题,比如搜索目标元素的插入位置。 - -## 10.2.1   无重复元素的情况 - -!!! question - - 给定一个长度为 $n$ 的有序数组 `nums` 和一个元素 `target` ,数组不存在重复元素。现将 `target` 插入到数组 `nums` 中,并保持其有序性。若数组中已存在元素 `target` ,则插入到其左方。请返回插入后 `target` 在数组中的索引。 - -![二分查找插入点示例数据](binary_search_insertion.assets/binary_search_insertion_example.png) - -

图 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 { - var i = 0, j = nums.count - 1 // 初始化双闭区间 [0, n-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" - /* 二分查找插入点(存在重复元素) */ - 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 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; - } - ``` - -=== "Zig" - - ```zig title="binary_search_insertion.zig" - [class]{}-[func]{binarySearchInsertionSimple} - ``` - -## 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) - -

图 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) - -=== "<2>" - ![binary_search_insertion_step2](binary_search_insertion.assets/binary_search_insertion_step2.png) - -=== "<3>" - ![binary_search_insertion_step3](binary_search_insertion.assets/binary_search_insertion_step3.png) - -=== "<4>" - ![binary_search_insertion_step4](binary_search_insertion.assets/binary_search_insertion_step4.png) - -=== "<5>" - ![binary_search_insertion_step5](binary_search_insertion.assets/binary_search_insertion_step5.png) - -=== "<6>" - ![binary_search_insertion_step6](binary_search_insertion.assets/binary_search_insertion_step6.png) - -=== "<7>" - ![binary_search_insertion_step7](binary_search_insertion.assets/binary_search_insertion_step7.png) - -=== "<8>" - ![binary_search_insertion_step8](binary_search_insertion.assets/binary_search_insertion_step8.png) - -

图 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 { - var i = 0, j = nums.count - 1 // 初始化双闭区间 [0, n-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; - } - ``` - -=== "Zig" - - ```zig title="binary_search_insertion.zig" - [class]{}-[func]{binarySearchInsertion} - ``` - -!!! tip - - 本节的代码都是“双闭区间”写法。有兴趣的读者可以自行实现“左闭右开”写法。 - -总的来看,二分查找无非就是给指针 $i$ 和 $j$ 分别设定搜索目标,目标可能是一个具体的元素(例如 `target` ),也可能是一个元素范围(例如小于 `target` 的元素)。 - -在不断的循环二分中,指针 $i$ 和 $j$ 都逐渐逼近预先设定的目标。最终,它们或是成功找到答案,或是越过边界后停止。 diff --git a/chapter_searching/replace_linear_by_hashing.md b/chapter_searching/replace_linear_by_hashing.md deleted file mode 100755 index a9640f436..000000000 --- a/chapter_searching/replace_linear_by_hashing.md +++ /dev/null @@ -1,508 +0,0 @@ ---- -comments: true ---- - -# 10.4   哈希优化策略 - -在算法题中,**我们常通过将线性查找替换为哈希查找来降低算法的时间复杂度**。我们借助一个算法题来加深理解。 - -!!! question - - 给定一个整数数组 `nums` 和一个目标元素 `target` ,请在数组中搜索“和”为 `target` 的两个元素,并返回它们的数组索引。返回任意一个解即可。 - -## 10.4.1   线性查找:以时间换空间 - -考虑直接遍历所有可能的组合。如图 10-9 所示,我们开启一个两层循环,在每轮中判断两个整数的和是否为 `target` ,若是则返回它们的索引。 - -![线性查找求解两数之和](replace_linear_by_hashing.assets/two_sum_brute_force.png) - -

图 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 new int[] { i, j }; - } - } - return Array.Empty(); - } - ``` - -=== "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; - } - ``` - -=== "Zig" - - ```zig title="two_sum.zig" - // 方法一:暴力枚举 - fn twoSumBruteForce(nums: []i32, target: i32) ?[2]i32 { - var size: usize = nums.len; - var i: usize = 0; - // 两层循环,时间复杂度 O(n^2) - while (i < size - 1) : (i += 1) { - var j = i + 1; - while (j < size) : (j += 1) { - if (nums[i] + nums[j] == target) { - return [_]i32{@intCast(i), @intCast(j)}; - } - } - } - return null; - } - ``` - -此方法的时间复杂度为 $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) - -=== "<2>" - ![two_sum_hashtable_step2](replace_linear_by_hashing.assets/two_sum_hashtable_step2.png) - -=== "<3>" - ![two_sum_hashtable_step3](replace_linear_by_hashing.assets/two_sum_hashtable_step3.png) - -

图 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 = new(); - // 单层循环,时间复杂度 O(n) - for (int i = 0; i < size; i++) { - if (dic.ContainsKey(target - nums[i])) { - return new int[] { dic[target - nums[i]], i }; - } - dic.Add(nums[i], i); - } - return Array.Empty(); - } - ``` - -=== "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" - /* 哈希表 */ - struct hashTable { - int key; - int val; - UT_hash_handle hh; // 基于 uthash.h 实现 - }; - - typedef struct hashTable 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; - } - ``` - -=== "Zig" - - ```zig title="two_sum.zig" - // 方法二:辅助哈希表 - fn twoSumHashTable(nums: []i32, target: i32) !?[2]i32 { - var size: usize = nums.len; - // 辅助哈希表,空间复杂度 O(n) - var dic = std.AutoHashMap(i32, i32).init(std.heap.page_allocator); - defer dic.deinit(); - var i: usize = 0; - // 单层循环,时间复杂度 O(n) - while (i < size) : (i += 1) { - if (dic.contains(target - nums[i])) { - return [_]i32{dic.get(target - nums[i]).?, @intCast(i)}; - } - try dic.put(nums[i], @intCast(i)); - } - return null; - } - ``` - -此方法通过哈希查找将时间复杂度从 $O(n^2)$ 降低至 $O(n)$ ,大幅提升运行效率。 - -由于需要维护一个额外的哈希表,因此空间复杂度为 $O(n)$ 。**尽管如此,该方法的整体时空效率更为均衡,因此它是本题的最优解法**。 diff --git a/chapter_sorting/bubble_sort.md b/chapter_sorting/bubble_sort.md deleted file mode 100755 index 414b005cd..000000000 --- a/chapter_sorting/bubble_sort.md +++ /dev/null @@ -1,566 +0,0 @@ ---- -comments: true ---- - -# 11.3   冒泡排序 - -「冒泡排序 bubble sort」通过连续地比较与交换相邻元素实现排序。这个过程就像气泡从底部升到顶部一样,因此得名冒泡排序。 - -如图 11-4 所示,冒泡过程可以利用元素交换操作来模拟:从数组最左端开始向右遍历,依次比较相邻元素大小,如果“左元素 > 右元素”就交换它俩。遍历完成后,最大的元素会被移动到数组的最右端。 - -=== "<1>" - ![利用元素交换操作模拟冒泡](bubble_sort.assets/bubble_operation_step1.png) - -=== "<2>" - ![bubble_operation_step2](bubble_sort.assets/bubble_operation_step2.png) - -=== "<3>" - ![bubble_operation_step3](bubble_sort.assets/bubble_operation_step3.png) - -=== "<4>" - ![bubble_operation_step4](bubble_sort.assets/bubble_operation_step4.png) - -=== "<5>" - ![bubble_operation_step5](bubble_sort.assets/bubble_operation_step5.png) - -=== "<6>" - ![bubble_operation_step6](bubble_sort.assets/bubble_operation_step6.png) - -=== "<7>" - ![bubble_operation_step7](bubble_sort.assets/bubble_operation_step7.png) - -

图 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) - -

图 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] - int tmp = nums[j]; - nums[j] = nums[j + 1]; - nums[j + 1] = tmp; - } - } - } - } - ``` - -=== "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 stride(from: nums.count - 1, to: 0, by: -1) { - // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端 - for j in stride(from: 0, to: i, by: 1) { - if nums[j] > nums[j + 1] { - // 交换 nums[j] 与 nums[j + 1] - let tmp = nums[j] - nums[j] = nums[j + 1] - nums[j + 1] = tmp - } - } - } - } - ``` - -=== "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 = 0; i < size - 1; i++) { - // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端 - for (int j = 0; j < size - 1 - i; j++) { - if (nums[j] > nums[j + 1]) { - int temp = nums[j]; - nums[j] = nums[j + 1]; - nums[j + 1] = temp; - } - } - } - } - ``` - -=== "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; - } - } - } - } - ``` - -## 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] - int tmp = nums[j]; - nums[j] = nums[j + 1]; - nums[j + 1] = tmp; - 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 stride(from: nums.count - 1, to: 0, by: -1) { - var flag = false // 初始化标志位 - for j in stride(from: 0, to: i, by: 1) { - 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 - } - } - } - ``` - -=== "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 = 0; i < size - 1; i++) { - bool flag = false; - // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端 - for (int j = 0; j < size - 1 - 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; - } - } - ``` - -=== "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; // 此轮冒泡未交换任何元素,直接跳出 - } - } - ``` - -## 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/chapter_sorting/bucket_sort.md b/chapter_sorting/bucket_sort.md deleted file mode 100644 index 341ecbbcb..000000000 --- a/chapter_sorting/bucket_sort.md +++ /dev/null @@ -1,422 +0,0 @@ ---- -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) - -

图 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 = new List>(); - for (int i = 0; i < k; i++) { - buckets.Add(new List()); - } - // 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 - nums.formIndex(after: &i) - } - } - } - ``` - -=== "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++) { - // 每个桶最多可以分配 k 个元素 - buckets[i] = calloc(ARRAY_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 < ARRAY_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], ARRAY_SIZE, sizeof(float), compare_float); - } - - // 3. 遍历桶合并结果 - for (int i = 0, j = 0; j < k; j++) { - for (int l = 0; l < ARRAY_SIZE; l++) { - if (buckets[j][l] > 0) { - nums[i++] = buckets[j][l]; - } - } - } - - // 释放上述分配的内存 - for (int i = 0; i < k; i++) { - free(buckets[i]); - } - free(buckets); - } - ``` - -=== "Zig" - - ```zig title="bucket_sort.zig" - [class]{}-[func]{bucketSort} - ``` - -## 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) - -

图 11-14   递归划分桶

- -如果我们提前知道商品价格的概率分布,**则可以根据数据概率分布设置每个桶的价格分界线**。值得注意的是,数据分布并不一定需要特意统计,也可以根据数据特点采用某种概率模型进行近似。 - -如图 11-15 所示,我们假设商品价格服从正态分布,这样就可以合理地设定价格区间,从而将商品平均分配到各个桶中。 - -![根据概率分布划分桶](bucket_sort.assets/scatter_in_buckets_distribution.png) - -

图 11-15   根据概率分布划分桶

diff --git a/chapter_sorting/counting_sort.md b/chapter_sorting/counting_sort.md deleted file mode 100644 index bcf0ae42b..000000000 --- a/chapter_sorting/counting_sort.md +++ /dev/null @@ -1,787 +0,0 @@ ---- -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) - -

图 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 stride(from: 0, to: m + 1, by: 1) { - for _ in stride(from: 0, to: counter[num], by: 1) { - 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 = malloc(sizeof(int) * m); - 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; - } - } - } - ``` - -=== "Zig" - - ```zig title="counting_sort.zig" - [class]{}-[func]{countingSortNaive} - ``` - -!!! 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) - -=== "<2>" - ![counting_sort_step2](counting_sort.assets/counting_sort_step2.png) - -=== "<3>" - ![counting_sort_step3](counting_sort.assets/counting_sort_step3.png) - -=== "<4>" - ![counting_sort_step4](counting_sort.assets/counting_sort_step4.png) - -=== "<5>" - ![counting_sort_step5](counting_sort.assets/counting_sort_step5.png) - -=== "<6>" - ![counting_sort_step6](counting_sort.assets/counting_sort_step6.png) - -=== "<7>" - ![counting_sort_step7](counting_sort.assets/counting_sort_step7.png) - -=== "<8>" - ![counting_sort_step8](counting_sort.assets/counting_sort_step8.png) - -

图 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 stride(from: 0, to: m, by: 1) { - counter[i + 1] += counter[i] - } - // 4. 倒序遍历 nums ,将各元素填入结果数组 res - // 初始化数组 res 用于记录结果 - var res = Array(repeating: 0, count: nums.count) - for i in stride(from: nums.count - 1, through: 0, by: -1) { - let num = nums[i] - res[counter[num] - 1] = num // 将 num 放置到对应索引处 - counter[num] -= 1 // 令前缀和自减 1 ,得到下次放置 num 的索引 - } - // 使用结果数组 res 覆盖原数组 nums - for i in stride(from: 0, to: nums.count, by: 1) { - 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 = malloc(sizeof(int) * m); - 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)); - } - ``` - -=== "Zig" - - ```zig title="counting_sort.zig" - [class]{}-[func]{countingSort} - ``` - -## 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/chapter_sorting/heap_sort.md b/chapter_sorting/heap_sort.md deleted file mode 100644 index 799ac9ea1..000000000 --- a/chapter_sorting/heap_sort.md +++ /dev/null @@ -1,549 +0,0 @@ ---- -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) - -=== "<2>" - ![heap_sort_step2](heap_sort.assets/heap_sort_step2.png) - -=== "<3>" - ![heap_sort_step3](heap_sort.assets/heap_sort_step3.png) - -=== "<4>" - ![heap_sort_step4](heap_sort.assets/heap_sort_step4.png) - -=== "<5>" - ![heap_sort_step5](heap_sort.assets/heap_sort_step5.png) - -=== "<6>" - ![heap_sort_step6](heap_sort.assets/heap_sort_step6.png) - -=== "<7>" - ![heap_sort_step7](heap_sort.assets/heap_sort_step7.png) - -=== "<8>" - ![heap_sort_step8](heap_sort.assets/heap_sort_step8.png) - -=== "<9>" - ![heap_sort_step9](heap_sort.assets/heap_sort_step9.png) - -=== "<10>" - ![heap_sort_step10](heap_sort.assets/heap_sort_step10.png) - -=== "<11>" - ![heap_sort_step11](heap_sort.assets/heap_sort_step11.png) - -=== "<12>" - ![heap_sort_step12](heap_sort.assets/heap_sort_step12.png) - -

图 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 stride(from: nums.count - 1, to: 0, by: -1) { - // 交换根节点与最右叶节点(即交换首元素与尾元素) - 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); - } - } - ``` - -=== "Zig" - - ```zig title="heap_sort.zig" - [class]{}-[func]{siftDown} - - [class]{}-[func]{heapSort} - ``` - -## 11.7.2   算法特性 - -- **时间复杂度 $O(n \log n)$、非自适应排序**:建堆操作使用 $O(n)$ 时间。从堆中提取最大元素的时间复杂度为 $O(\log n)$ ,共循环 $n - 1$ 轮。 -- **空间复杂度 $O(1)$、原地排序**:几个指针变量使用 $O(1)$ 空间。元素交换和堆化操作都是在原数组上进行的。 -- **非稳定排序**:在交换堆顶元素和堆底元素时,相等元素的相对位置可能发生变化。 diff --git a/chapter_sorting/insertion_sort.md b/chapter_sorting/insertion_sort.md deleted file mode 100755 index 27e483ccb..000000000 --- a/chapter_sorting/insertion_sort.md +++ /dev/null @@ -1,269 +0,0 @@ ---- -comments: true ---- - -# 11.4   插入排序 - -「插入排序 insertion sort」是一种简单的排序算法,它的工作原理与手动整理一副牌的过程非常相似。 - -具体来说,我们在未排序区间选择一个基准元素,将该元素与其左侧已排序区间的元素逐一比较大小,并将该元素插入到正确的位置。 - -图 11-6 展示了数组插入元素的操作流程。设基准元素为 `base` ,我们需要将从目标索引到 `base` 之间的所有元素向右移动一位,然后再将 `base` 赋值给目标索引。 - -![单次插入操作](insertion_sort.assets/insertion_operation.png) - -

图 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) - -

图 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) { - // 外循环:已排序元素数量为 1, 2, ..., n - for (int i = 1; i < nums.size(); i++) { - int base = nums[i], j = i - 1; - // 内循环:将 base 插入到已排序部分的正确位置 - 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) { - // 外循环:已排序元素数量为 1, 2, ..., n - for (int i = 1; i < nums.length; i++) { - int base = nums[i], j = i - 1; - // 内循环:将 base 插入到已排序部分的正确位置 - 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) { - // 外循环:已排序元素数量为 1, 2, ..., n - for (int i = 1; i < nums.Length; i++) { - int bas = nums[i], j = i - 1; - // 内循环:将 base 插入到已排序部分的正确位置 - 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] - for i := 1; i < len(nums); i++ { - base := nums[i] - j := i - 1 - // 内循环:将 base 插入到已排序部分的正确位置 - 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]) { - // 外循环:已排序元素数量为 1, 2, ..., n - for i in stride(from: 1, to: nums.count, by: 1) { - let base = nums[i] - var j = i - 1 - // 内循环:将 base 插入到已排序部分的正确位置 - 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) { - // 外循环:已排序元素数量为 1, 2, ..., n - for (let i = 1; i < nums.length; i++) { - let base = nums[i], - j = i - 1; - // 内循环:将 base 插入到已排序部分的正确位置 - 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 { - // 外循环:已排序元素数量为 1, 2, ..., n - for (let i = 1; i < nums.length; i++) { - const base = nums[i]; - let j = i - 1; - // 内循环:将 base 插入到已排序部分的正确位置 - 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) { - // 外循环:已排序元素数量为 1, 2, ..., n - for (int i = 1; i < nums.length; i++) { - int base = nums[i], j = i - 1; - // 内循环:将 base 插入到已排序部分的正确位置 - 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]) { - // 外循环:已排序元素数量为 1, 2, ..., n - for i in 1..nums.len() { - let (base, mut j) = (nums[i], (i - 1) as i32); - // 内循环:将 base 插入到已排序部分的正确位置 - 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) { - // 外循环:已排序元素数量为 1, 2, ..., n - for (int i = 1; i < size; i++) { - int base = nums[i], j = i - 1; - // 内循环:将 base 插入到已排序部分的正确位置 - while (j >= 0 && nums[j] > base) { - // 将 nums[j] 向右移动一位 - nums[j + 1] = nums[j]; - j--; - } - // 将 base 赋值到正确位置 - nums[j + 1] = base; - } - } - ``` - -=== "Zig" - - ```zig title="insertion_sort.zig" - // 插入排序 - fn insertionSort(nums: []i32) void { - // 外循环:已排序元素数量为 1, 2, ..., n - var i: usize = 1; - while (i < nums.len) : (i += 1) { - var base = nums[i]; - var j: usize = i; - // 内循环:将 base 插入到已排序部分的正确位置 - while (j >= 1 and nums[j - 1] > base) : (j -= 1) { - nums[j] = nums[j - 1]; // 将 nums[j] 向右移动一位 - } - nums[j] = base; // 将 base 赋值到正确位置 - } - } - ``` - -## 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/chapter_sorting/merge_sort.md b/chapter_sorting/merge_sort.md deleted file mode 100755 index 9e2289c41..000000000 --- a/chapter_sorting/merge_sort.md +++ /dev/null @@ -1,641 +0,0 @@ ---- -comments: true ---- - -# 11.6   归并排序 - -「归并排序 merge sort」是一种基于分治策略的排序算法,包含图 11-10 所示的“划分”和“合并”阶段。 - -1. **划分阶段**:通过递归不断地将数组从中点处分开,将长数组的排序问题转换为短数组的排序问题。 -2. **合并阶段**:当子数组长度为 1 时终止划分,开始合并,持续地将左右两个较短的有序数组合并为一个较长的有序数组,直至结束。 - -![归并排序的划分与合并阶段](merge_sort.assets/merge_sort_overview.png) - -

图 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) - -=== "<2>" - ![merge_sort_step2](merge_sort.assets/merge_sort_step2.png) - -=== "<3>" - ![merge_sort_step3](merge_sort.assets/merge_sort_step3.png) - -=== "<4>" - ![merge_sort_step4](merge_sort.assets/merge_sort_step4.png) - -=== "<5>" - ![merge_sort_step5](merge_sort.assets/merge_sort_step5.png) - -=== "<6>" - ![merge_sort_step6](merge_sort.assets/merge_sort_step6.png) - -=== "<7>" - ![merge_sort_step7](merge_sort.assets/merge_sort_step7.png) - -=== "<8>" - ![merge_sort_step8](merge_sort.assets/merge_sort_step8.png) - -=== "<9>" - ![merge_sort_step9](merge_sort.assets/merge_sort_step9.png) - -=== "<10>" - ![merge_sort_step10](merge_sort.assets/merge_sort_step10.png) - -

图 11-11   归并排序步骤

- -观察发现,归并排序与二叉树后序遍历的递归顺序是一致的。 - -- **后序遍历**:先递归左子树,再递归右子树,最后处理根节点。 -- **归并排序**:先递归左子数组,再递归右子数组,最后处理合并。 - -=== "Python" - - ```python title="merge_sort.py" - def merge(nums: list[int], left: int, mid: int, right: int): - """合并左子数组和右子数组""" - # 左子数组区间 [left, mid] - # 右子数组区间 [mid + 1, right] - # 初始化辅助数组 - tmp = list(nums[left : right + 1]) - # 左子数组的起始索引和结束索引 - left_start = 0 - left_end = mid - left - # 右子数组的起始索引和结束索引 - right_start = mid + 1 - left - right_end = right - left - # i, j 分别指向左子数组、右子数组的首元素 - i = left_start - j = right_start - # 通过覆盖原数组 nums 来合并左子数组和右子数组 - for k in range(left, right + 1): - # 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ - if i > left_end: - nums[k] = tmp[j] - j += 1 - # 否则,若“右子数组已全部合并完”或“左子数组元素 <= 右子数组元素”,则选取左子数组元素,并且 i++ - elif j > right_end or tmp[i] <= tmp[j]: - nums[k] = tmp[i] - i += 1 - # 否则,若“左右子数组都未全部合并完”且“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ - else: - nums[k] = tmp[j] - j += 1 - - 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" - /* 合并左子数组和右子数组 */ - // 左子数组区间 [left, mid] - // 右子数组区间 [mid + 1, right] - void merge(vector &nums, int left, int mid, int right) { - // 初始化辅助数组 - vector tmp(nums.begin() + left, nums.begin() + right + 1); - // 左子数组的起始索引和结束索引 - int leftStart = left - left, leftEnd = mid - left; - // 右子数组的起始索引和结束索引 - int rightStart = mid + 1 - left, rightEnd = right - left; - // i, j 分别指向左子数组、右子数组的首元素 - int i = leftStart, j = rightStart; - // 通过覆盖原数组 nums 来合并左子数组和右子数组 - for (int k = left; k <= right; k++) { - // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ - if (i > leftEnd) - nums[k] = tmp[j++]; - // 否则,若“右子数组已全部合并完”或“左子数组元素 <= 右子数组元素”,则选取左子数组元素,并且 i++ - else if (j > rightEnd || tmp[i] <= tmp[j]) - nums[k] = tmp[i++]; - // 否则,若“左右子数组都未全部合并完”且“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ - else - nums[k] = tmp[j++]; - } - } - - /* 归并排序 */ - 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" - /* 合并左子数组和右子数组 */ - // 左子数组区间 [left, mid] - // 右子数组区间 [mid + 1, right] - void merge(int[] nums, int left, int mid, int right) { - // 初始化辅助数组 - int[] tmp = Arrays.copyOfRange(nums, left, right + 1); - // 左子数组的起始索引和结束索引 - int leftStart = left - left, leftEnd = mid - left; - // 右子数组的起始索引和结束索引 - int rightStart = mid + 1 - left, rightEnd = right - left; - // i, j 分别指向左子数组、右子数组的首元素 - int i = leftStart, j = rightStart; - // 通过覆盖原数组 nums 来合并左子数组和右子数组 - for (int k = left; k <= right; k++) { - // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ - if (i > leftEnd) - nums[k] = tmp[j++]; - // 否则,若“右子数组已全部合并完”或“左子数组元素 <= 右子数组元素”,则选取左子数组元素,并且 i++ - else if (j > rightEnd || tmp[i] <= tmp[j]) - nums[k] = tmp[i++]; - // 否则,若“左右子数组都未全部合并完”且“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ - else - nums[k] = tmp[j++]; - } - } - - /* 归并排序 */ - 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" - /* 合并左子数组和右子数组 */ - // 左子数组区间 [left, mid] - // 右子数组区间 [mid + 1, right] - void merge(int[] nums, int left, int mid, int right) { - // 初始化辅助数组 - int[] tmp = nums[left..(right + 1)]; - // 左子数组的起始索引和结束索引 - int leftStart = left - left, leftEnd = mid - left; - // 右子数组的起始索引和结束索引 - int rightStart = mid + 1 - left, rightEnd = right - left; - // i, j 分别指向左子数组、右子数组的首元素 - int i = leftStart, j = rightStart; - // 通过覆盖原数组 nums 来合并左子数组和右子数组 - for (int k = left; k <= right; k++) { - // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ - if (i > leftEnd) - nums[k] = tmp[j++]; - // 否则,若“右子数组已全部合并完”或“左子数组元素 <= 右子数组元素”,则选取左子数组元素,并且 i++ - else if (j > rightEnd || tmp[i] <= tmp[j]) - nums[k] = tmp[i++]; - // 否则,若“左右子数组都未全部合并完”且“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ - else - nums[k] = tmp[j++]; - } - } - - /* 归并排序 */ - 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" - /* 合并左子数组和右子数组 */ - // 左子数组区间 [left, mid] - // 右子数组区间 [mid + 1, right] - func merge(nums []int, left, mid, right int) { - // 初始化辅助数组 借助 copy 模块 - tmp := make([]int, right-left+1) - for i := left; i <= right; i++ { - tmp[i-left] = nums[i] - } - // 左子数组的起始索引和结束索引 - leftStart, leftEnd := left-left, mid-left - // 右子数组的起始索引和结束索引 - rightStart, rightEnd := mid+1-left, right-left - // i, j 分别指向左子数组、右子数组的首元素 - i, j := leftStart, rightStart - // 通过覆盖原数组 nums 来合并左子数组和右子数组 - for k := left; k <= right; k++ { - // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ - if i > leftEnd { - nums[k] = tmp[j] - j++ - // 否则,若“右子数组已全部合并完”或“左子数组元素 <= 右子数组元素”,则选取左子数组元素,并且 i++ - } else if j > rightEnd || tmp[i] <= tmp[j] { - nums[k] = tmp[i] - i++ - // 否则,若“左右子数组都未全部合并完”且“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ - } else { - nums[k] = tmp[j] - j++ - } - } - } - - /* 归并排序 */ - 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" - /* 合并左子数组和右子数组 */ - // 左子数组区间 [left, mid] - // 右子数组区间 [mid + 1, right] - func merge(nums: inout [Int], left: Int, mid: Int, right: Int) { - // 初始化辅助数组 - let tmp = Array(nums[left ..< (right + 1)]) - // 左子数组的起始索引和结束索引 - let leftStart = left - left - let leftEnd = mid - left - // 右子数组的起始索引和结束索引 - let rightStart = mid + 1 - left - let rightEnd = right - left - // i, j 分别指向左子数组、右子数组的首元素 - var i = leftStart - var j = rightStart - // 通过覆盖原数组 nums 来合并左子数组和右子数组 - for k in left ... right { - // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ - if i > leftEnd { - nums[k] = tmp[j] - j += 1 - } - // 否则,若“右子数组已全部合并完”或“左子数组元素 <= 右子数组元素”,则选取左子数组元素,并且 i++ - else if j > rightEnd || tmp[i] <= tmp[j] { - nums[k] = tmp[i] - i += 1 - } - // 否则,若“左右子数组都未全部合并完”且“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ - else { - nums[k] = tmp[j] - j += 1 - } - } - } - - /* 归并排序 */ - 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" - /* 合并左子数组和右子数组 */ - // 左子数组区间 [left, mid] - // 右子数组区间 [mid + 1, right] - function merge(nums, left, mid, right) { - // 初始化辅助数组 - let tmp = nums.slice(left, right + 1); - // 左子数组的起始索引和结束索引 - let leftStart = left - left, - leftEnd = mid - left; - // 右子数组的起始索引和结束索引 - let rightStart = mid + 1 - left, - rightEnd = right - left; - // i, j 分别指向左子数组、右子数组的首元素 - let i = leftStart, - j = rightStart; - // 通过覆盖原数组 nums 来合并左子数组和右子数组 - for (let k = left; k <= right; k++) { - if (i > leftEnd) { - // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ - nums[k] = tmp[j++]; - } else if (j > rightEnd || tmp[i] <= tmp[j]) { - // 否则,若“右子数组已全部合并完”或“左子数组元素 <= 右子数组元素”,则选取左子数组元素,并且 i++ - nums[k] = tmp[i++]; - } else { - // 否则,若“左右子数组都未全部合并完”且“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ - nums[k] = tmp[j++]; - } - } - } - - /* 归并排序 */ - 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" - /* 合并左子数组和右子数组 */ - // 左子数组区间 [left, mid] - // 右子数组区间 [mid + 1, right] - function merge(nums: number[], left: number, mid: number, right: number): void { - // 初始化辅助数组 - let tmp = nums.slice(left, right + 1); - // 左子数组的起始索引和结束索引 - let leftStart = left - left, - leftEnd = mid - left; - // 右子数组的起始索引和结束索引 - let rightStart = mid + 1 - left, - rightEnd = right - left; - // i, j 分别指向左子数组、右子数组的首元素 - let i = leftStart, - j = rightStart; - // 通过覆盖原数组 nums 来合并左子数组和右子数组 - for (let k = left; k <= right; k++) { - if (i > leftEnd) { - // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ - nums[k] = tmp[j++]; - // 否则,若“右子数组已全部合并完”或“左子数组元素 <= 右子数组元素”,则选取左子数组元素,并且 i++ - } else if (j > rightEnd || tmp[i] <= tmp[j]) { - nums[k] = tmp[i++]; - // 否则,若“左右子数组都未全部合并完”且“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ - } else { - nums[k] = tmp[j++]; - } - } - } - - /* 归并排序 */ - 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" - /* 合并左子数组和右子数组 */ - // 左子数组区间 [left, mid] - // 右子数组区间 [mid + 1, right] - void merge(List nums, int left, int mid, int right) { - // 初始化辅助数组 - List tmp = nums.sublist(left, right + 1); - // 左子数组的起始索引和结束索引 - int leftStart = left - left, leftEnd = mid - left; - // 右子数组的起始索引和结束索引 - int rightStart = mid + 1 - left, rightEnd = right - left; - // i, j 分别指向左子数组、右子数组的首元素 - int i = leftStart, j = rightStart; - // 通过覆盖原数组 nums 来合并左子数组和右子数组 - for (int k = left; k <= right; k++) { - // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ - if (i > leftEnd) - nums[k] = tmp[j++]; - // 否则,若“右子数组已全部合并完”或“左子数组元素 <= 右子数组元素”,则选取左子数组元素,并且 i++ - else if (j > rightEnd || tmp[i] <= tmp[j]) - nums[k] = tmp[i++]; - // 否则,若“左右子数组都未全部合并完”且“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ - else - nums[k] = tmp[j++]; - } - } - - /* 归并排序 */ - 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" - /* 合并左子数组和右子数组 */ - // 左子数组区间 [left, mid] - // 右子数组区间 [mid + 1, right] - fn merge(nums: &mut [i32], left: usize, mid: usize, right: usize) { - // 初始化辅助数组 - let tmp: Vec = nums[left..right + 1].to_vec(); - // 左子数组的起始索引和结束索引 - let (left_start, left_end) = (left - left, mid - left); - // 右子数组的起始索引和结束索引 - let (right_start, right_end) = (mid + 1 - left, right-left); - // i, j 分别指向左子数组、右子数组的首元素 - let (mut l_corrent, mut r_corrent) = (left_start, right_start); - // 通过覆盖原数组 nums 来合并左子数组和右子数组 - for k in left..right + 1 { - // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ - if l_corrent > left_end { - nums[k] = tmp[r_corrent]; - r_corrent += 1; - } - // 否则,若“右子数组已全部合并完”或“左子数组元素 <= 右子数组元素”,则选取左子数组元素,并且 i++ - else if r_corrent > right_end || tmp[l_corrent] <= tmp[r_corrent] { - nums[k] = tmp[l_corrent]; - l_corrent += 1; - } - // 否则,若“左右子数组都未全部合并完”且“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ - else { - nums[k] = tmp[r_corrent]; - r_corrent += 1; - } - } - } - - /* 归并排序 */ - fn merge_sort(left: usize, right: usize, nums: &mut [i32]) { - // 终止条件 - if left >= right { return; } // 当子数组长度为 1 时终止递归 - // 划分阶段 - let mid = (left + right) / 2; // 计算中点 - merge_sort(left, mid, nums); // 递归左子数组 - merge_sort(mid + 1, right, nums); // 递归右子数组 - // 合并阶段 - merge(nums, left, mid, right); - } - ``` - -=== "C" - - ```c title="merge_sort.c" - /* 合并左子数组和右子数组 */ - // 左子数组区间 [left, mid] - // 右子数组区间 [mid + 1, right] - void merge(int *nums, int left, int mid, int right) { - int index; - // 初始化辅助数组 - int tmp[right + 1 - left]; - for (index = left; index < right + 1; index++) { - tmp[index - left] = nums[index]; - } - // 左子数组的起始索引和结束索引 - int leftStart = left - left, leftEnd = mid - left; - // 右子数组的起始索引和结束索引 - int rightStart = mid + 1 - left, rightEnd = right - left; - // i, j 分别指向左子数组、右子数组的首元素 - int i = leftStart, j = rightStart; - // 通过覆盖原数组 nums 来合并左子数组和右子数组 - for (int k = left; k <= right; k++) { - // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ - if (i > leftEnd) - nums[k] = tmp[j++]; - // 否则,若“右子数组已全部合并完”或“左子数组元素 <= 右子数组元素”,则选取左子数组元素,并且 i++ - else if (j > rightEnd || tmp[i] <= tmp[j]) - nums[k] = tmp[i++]; - // 否则,若“左右子数组都未全部合并完”且“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ - else - nums[k] = tmp[j++]; - } - } - - /* 归并排序 */ - 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); - } - ``` - -=== "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); - } - ``` - -实现合并函数 `merge()` 存在以下难点。 - -- **需要特别注意各个变量的含义**。`nums` 的待合并区间为 `[left, right]` ,但由于 `tmp` 仅复制了 `nums` 该区间的元素,因此 `tmp` 对应区间为 `[0, right - left]` 。 -- 在比较 `tmp[i]` 和 `tmp[j]` 的大小时,**还需考虑子数组遍历完成后的索引越界问题**,即 `i > leftEnd` 和 `j > rightEnd` 的情况。索引越界的优先级是最高的,如果左子数组已经被合并完了,那么不需要继续比较,直接合并右子数组元素即可。 - -## 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/chapter_sorting/quick_sort.md b/chapter_sorting/quick_sort.md deleted file mode 100755 index bd2c8256b..000000000 --- a/chapter_sorting/quick_sort.md +++ /dev/null @@ -1,1316 +0,0 @@ ---- -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) - -=== "<2>" - ![pivot_division_step2](quick_sort.assets/pivot_division_step2.png) - -=== "<3>" - ![pivot_division_step3](quick_sort.assets/pivot_division_step3.png) - -=== "<4>" - ![pivot_division_step4](quick_sort.assets/pivot_division_step4.png) - -=== "<5>" - ![pivot_division_step5](quick_sort.assets/pivot_division_step5.png) - -=== "<6>" - ![pivot_division_step6](quick_sort.assets/pivot_division_step6.png) - -=== "<7>" - ![pivot_division_step7](quick_sort.assets/pivot_division_step7.png) - -=== "<8>" - ![pivot_division_step8](quick_sort.assets/pivot_division_step8.png) - -=== "<9>" - ![pivot_division_step9](quick_sort.assets/pivot_division_step9.png) - -

图 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) { - 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; // 返回基准数的索引 - } - ``` - -=== "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 swap(nums: inout [Int], i: Int, j: Int) { - let tmp = nums[i] - nums[i] = nums[j] - nums[j] = tmp - } - - /* 哨兵划分 */ - 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 // 从左向右找首个大于基准数的元素 - } - swap(nums: &nums, i: i, j: j) // 交换这两个元素 - } - swap(nums: &nums, i: i, j: 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; - } - ``` - -=== "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; // 返回基准数的索引 - } - ``` - -## 11.5.1   算法流程 - -快速排序的整体流程如图 11-9 所示。 - -1. 首先,对原数组执行一次“哨兵划分”,得到未排序的左子数组和右子数组。 -2. 然后,对左子数组和右子数组分别递归执行“哨兵划分”。 -3. 持续递归,直至子数组长度为 1 时终止,从而完成整个数组的排序。 - -![快速排序流程](quick_sort.assets/quick_sort_overview.png) - -

图 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); - } - ``` - -=== "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); - } - ``` - -## 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: - """选取三个元素的中位数""" - # 此处使用异或运算来简化代码 - # 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1 - if (nums[left] < nums[mid]) ^ (nums[left] < nums[right]): - return left - elif (nums[mid] < nums[left]) ^ (nums[mid] < nums[right]): - return mid - 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) { - // 此处使用异或运算来简化代码 - // 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1 - if ((nums[left] < nums[mid]) ^ (nums[left] < nums[right])) - return left; - else if ((nums[mid] < nums[left]) ^ (nums[mid] < nums[right])) - return mid; - else - 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) { - // 此处使用异或运算来简化代码 - // 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1 - if ((nums[left] < nums[mid]) ^ (nums[left] < nums[right])) - return left; - else if ((nums[mid] < nums[left]) ^ (nums[mid] < nums[right])) - return mid; - else - 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) { - // 此处使用异或运算来简化代码 - // 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1 - if ((nums[left] < nums[mid]) ^ (nums[left] < nums[right])) - return left; - else if ((nums[mid] < nums[left]) ^ (nums[mid] < nums[right])) - return mid; - else - 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 { - // 此处使用异或运算来简化代码(!= 在这里起到异或的作用) - // 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1 - if (nums[left] < nums[mid]) != (nums[left] < nums[right]) { - return left - } else if (nums[mid] < nums[left]) != (nums[mid] < nums[right]) { - return mid - } - 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 { - if (nums[left] < nums[mid]) != (nums[left] < nums[right]) { - return left - } else if (nums[mid] < nums[left]) != (nums[mid] < nums[right]) { - return mid - } else { - 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) - // 将中位数交换至数组最左端 - swap(nums: &nums, i: left, j: med) - return partition(nums: &nums, left: left, right: right) - } - ``` - -=== "JS" - - ```javascript title="quick_sort.js" - /* 选取三个元素的中位数 */ - medianThree(nums, left, mid, right) { - // 此处使用异或运算来简化代码 - // 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1 - if ((nums[left] < nums[mid]) ^ (nums[left] < nums[right])) return left; - else if ((nums[mid] < nums[left]) ^ (nums[mid] < nums[right])) - return mid; - else 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 { - // 此处使用异或运算来简化代码 - // 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1 - if (Number(nums[left] < nums[mid]) ^ Number(nums[left] < nums[right])) { - return left; - } else if ( - Number(nums[mid] < nums[left]) ^ Number(nums[mid] < nums[right]) - ) { - return mid; - } else { - 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) { - // 此处使用异或运算来简化代码 - // 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1 - if ((nums[left] < nums[mid]) ^ (nums[left] < nums[right])) - return left; - else if ((nums[mid] < nums[left]) ^ (nums[mid] < nums[right])) - return mid; - else - 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 { - // 此处使用异或运算来简化代码 - // 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1 - if (nums[left] < nums[mid]) ^ (nums[left] < nums[right]) { - return left; - } else if (nums[mid] < nums[left]) ^ (nums[mid] < nums[right]) { - return mid; - } - 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) { - // 此处使用异或运算来简化代码 - // 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1 - if ((nums[left] < nums[mid]) ^ (nums[left] < nums[right])) - return left; - else if ((nums[mid] < nums[left]) ^ (nums[mid] < nums[right])) - return mid; - else - return right; - } - - /* 快速排序类(中位基准数优化) */ - // 选取三个元素的中位数 - int medianThree(int nums[], int left, int mid, int right) { - // 此处使用异或运算来简化代码 - // 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1 - if ((nums[left] < nums[mid]) ^ (nums[left] < nums[right])) - return left; - else if ((nums[mid] < nums[left]) ^ (nums[mid] < nums[right])) - return mid; - else - 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; // 返回基准数的索引 - } - ``` - -=== "Zig" - - ```zig title="quick_sort.zig" - // 选取三个元素的中位数 - fn medianThree(nums: []i32, left: usize, mid: usize, right: usize) usize { - // 此处使用异或运算来简化代码 - // 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1 - if ((nums[left] < nums[mid]) != (nums[left] < nums[right])) { - return left; - } else if ((nums[mid] < nums[left]) != (nums[mid] < nums[right])) { - return mid; - } else { - 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; // 返回基准数的索引 - } - ``` - -## 11.5.5   尾递归优化 - -**在某些输入下,快速排序可能占用空间较多**。以完全倒序的输入数组为例,由于每轮哨兵划分后右子数组长度为 $0$ ,递归树的高度会达到 $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] - } - } - } - ``` - -=== "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] - } - } - } - ``` diff --git a/chapter_sorting/radix_sort.md b/chapter_sorting/radix_sort.md deleted file mode 100644 index cbb642444..000000000 --- a/chapter_sorting/radix_sort.md +++ /dev/null @@ -1,697 +0,0 @@ ---- -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) - -

图 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) - let n = nums.count - // 统计 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: n) - for i in stride(from: n - 1, through: 0, by: -1) { - 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); - } - ``` - -=== "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); - } - } - ``` - -!!! 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/chapter_sorting/selection_sort.md b/chapter_sorting/selection_sort.md deleted file mode 100644 index a0817e8df..000000000 --- a/chapter_sorting/selection_sort.md +++ /dev/null @@ -1,295 +0,0 @@ ---- -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) - -=== "<2>" - ![selection_sort_step2](selection_sort.assets/selection_sort_step2.png) - -=== "<3>" - ![selection_sort_step3](selection_sort.assets/selection_sort_step3.png) - -=== "<4>" - ![selection_sort_step4](selection_sort.assets/selection_sort_step4.png) - -=== "<5>" - ![selection_sort_step5](selection_sort.assets/selection_sort_step5.png) - -=== "<6>" - ![selection_sort_step6](selection_sort.assets/selection_sort_step6.png) - -=== "<7>" - ![selection_sort_step7](selection_sort.assets/selection_sort_step7.png) - -=== "<8>" - ![selection_sort_step8](selection_sort.assets/selection_sort_step8.png) - -=== "<9>" - ![selection_sort_step9](selection_sort.assets/selection_sort_step9.png) - -=== "<10>" - ![selection_sort_step10](selection_sort.assets/selection_sort_step10.png) - -=== "<11>" - ![selection_sort_step11](selection_sort.assets/selection_sort_step11.png) - -

图 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]) { - 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; - } - } - ``` - -=== "Zig" - - ```zig title="selection_sort.zig" - [class]{}-[func]{selectionSort} - ``` - -## 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) - -

图 11-3   选择排序非稳定示例

diff --git a/chapter_stack_and_queue/deque.md b/chapter_stack_and_queue/deque.md deleted file mode 100644 index 740611222..000000000 --- a/chapter_stack_and_queue/deque.md +++ /dev/null @@ -1,3249 +0,0 @@ ---- -comments: true ---- - -# 5.3   双向队列 - -在队列中,我们仅能在头部删除或在尾部添加元素。如图 5-7 所示,「双向队列 double-ended queue」提供了更高的灵活性,允许在头部和尾部执行元素的添加或删除操作。 - -![双向队列的操作](deque.assets/deque_operations.png) - -

图 5-7   双向队列的操作

- -## 5.3.1   双向队列常用操作 - -双向队列的常用操作如表 5-3 所示,具体的方法名称需要根据所使用的编程语言来确定。 - -

表 5-3   双向队列操作效率

- -
- -| 方法名 | 描述 | 时间复杂度 | -| ----------- | -------------- | ---------- | -| pushFirst() | 将元素添加至队首 | $O(1)$ | -| pushLast() | 将元素添加至队尾 | $O(1)$ | -| popFirst() | 删除队首元素 | $O(1)$ | -| popLast() | 删除队尾元素 | $O(1)$ | -| peekFirst() | 访问队首元素 | $O(1)$ | -| peekLast() | 访问队尾元素 | $O(1)$ | - -
- -同样地,我们可以直接使用编程语言中已实现的双向队列类。 - -=== "Python" - - ```python title="deque.py" - # 初始化双向队列 - deque: deque[int] = collections.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 LinkedList(); - - /* 元素入队 */ - 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); - console.log("双向队列 deque = ", deque); - - /* 访问元素 */ - const peekFirst = deque[0]; - console.log("队首元素 peekFirst = " + peekFirst); - const peekLast = deque[deque.length - 1]; - console.log("队尾元素 peekLast = " + peekLast); - - /* 元素出队 */ - // 请注意,由于是数组,shift() 方法的时间复杂度为 O(n) - const popFront = deque.shift(); - console.log("队首出队元素 popFront = " + popFront + ",队首出队后 deque = " + deque); - const popBack = deque.pop(); - console.log("队尾出队元素 popBack = " + popBack + ",队尾出队后 deque = " + deque); - - /* 获取双向队列的长度 */ - const size = deque.length; - console.log("双向队列长度 size = " + size); - - /* 判断双向队列是否为空 */ - const isEmpty = size === 0; - console.log("双向队列是否为空 = " + isEmpty); - ``` - -=== "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); - console.log("双向队列 deque = ", deque); - - /* 访问元素 */ - const peekFirst: number = deque[0]; - console.log("队首元素 peekFirst = " + peekFirst); - const peekLast: number = deque[deque.length - 1]; - console.log("队尾元素 peekLast = " + peekLast); - - /* 元素出队 */ - // 请注意,由于是数组,shift() 方法的时间复杂度为 O(n) - const popFront: number = deque.shift() as number; - console.log("队首出队元素 popFront = " + popFront + ",队首出队后 deque = " + deque); - const popBack: number = deque.pop() as number; - console.log("队尾出队元素 popBack = " + popBack + ",队尾出队后 deque = " + deque); - - /* 获取双向队列的长度 */ - const size: number = deque.length; - console.log("双向队列长度 size = " + size); - - /* 判断双向队列是否为空 */ - const isEmpty: boolean = size === 0; - console.log("双向队列是否为空 = " + isEmpty); - ``` - -=== "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;W - ``` - -=== "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 未提供内置双向队列 - ``` - -=== "Zig" - - ```zig title="deque.zig" - - ``` - -## 5.3.2   双向队列实现 * - -双向队列的实现与队列类似,可以选择链表或数组作为底层数据结构。 - -### 1.   基于双向链表的实现 - -回顾上一节内容,我们使用普通单向链表来实现队列,因为它可以方便地删除头节点(对应出队操作)和在尾节点后添加新节点(对应入队操作)。 - -对于双向队列而言,头部和尾部都可以执行入队和出队操作。换句话说,双向队列需要实现另一个对称方向的操作。为此,我们采用“双向链表”作为双向队列的底层数据结构。 - -如图 5-8 所示,我们将双向链表的头节点和尾节点视为双向队列的队首和队尾,同时实现在两端添加和删除节点的功能。 - -=== "LinkedListDeque" - ![基于链表实现双向队列的入队出队操作](deque.assets/linkedlist_deque.png) - -=== "pushLast()" - ![linkedlist_deque_push_last](deque.assets/linkedlist_deque_push_last.png) - -=== "pushFirst()" - ![linkedlist_deque_push_first](deque.assets/linkedlist_deque_push_first.png) - -=== "popLast()" - ![linkedlist_deque_pop_last](deque.assets/linkedlist_deque_pop_last.png) - -=== "popFirst()" - ![linkedlist_deque_pop_first](deque.assets/linkedlist_deque_pop_first.png) - -

图 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 { - public int val; // 节点值 - public ListNode? next; // 后继节点引用 - public ListNode? prev; // 前驱节点引用 - - public ListNode(int val) { - this.val = val; - prev = null; - next = null; - } - } - - /* 基于双向链表实现的双向队列 */ - class LinkedListDeque { - private ListNode? front, rear; // 头节点 front, 尾节点 rear - private int queSize = 0; // 双向队列的长度 - - public LinkedListDeque() { - front = null; - rear = null; - } - - /* 获取双向队列的长度 */ - public int size() { - return queSize; - } - - /* 判断双向队列是否为空 */ - public bool isEmpty() { - return size() == 0; - } - - /* 入队操作 */ - private void push(int num, bool isFront) { - ListNode node = new ListNode(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); - } - - /* 出队操作 */ - private 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 queSize: Int // 双向队列的长度 - - init() { - queSize = 0 - } - - /* 获取双向队列的长度 */ - func size() -> Int { - queSize - } - - /* 判断双向队列是否为空 */ - 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 // 更新尾节点 - } - queSize += 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 // 更新尾节点 - } - queSize -= 1 // 更新队列长度 - return val - } - - /* 队首出队 */ - func popFirst() -> Int { - pop(isFront: true) - } - - /* 队尾出队 */ - func popLast() -> Int { - pop(isFront: false) - } - - /* 访问队首元素 */ - func peekFirst() -> Int? { - isEmpty() ? nil : front?.val - } - - /* 访问队尾元素 */ - func peekLast() -> Int? { - isEmpty() ? nil : 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" - /* 双向链表节点 */ - struct doublyListNode { - int val; // 节点值 - struct doublyListNode *next; // 后继节点 - struct doublyListNode *prev; // 前驱节点 - }; - - typedef struct doublyListNode 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); - } - - /* 基于双向链表实现的双向队列 */ - struct linkedListDeque { - doublyListNode *front, *rear; // 头节点 front ,尾节点 rear - int queSize; // 双向队列的长度 - }; - - typedef struct linkedListDeque 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[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); - } - ``` - -=== "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.png) - -=== "pushLast()" - ![array_deque_push_last](deque.assets/array_deque_push_last.png) - -=== "pushFirst()" - ![array_deque_push_first](deque.assets/array_deque_push_first.png) - -=== "popLast()" - ![array_deque_pop_last](deque.assets/array_deque_pop_last.png) - -=== "popFirst()" - ![array_deque_pop_first](deque.assets/array_deque_pop_first.png) - -

图 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 { - private readonly 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 bool isEmpty() { - return queSize == 0; - } - - /* 计算环形数组索引 */ - private 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 queSize: Int // 双向队列长度 - - /* 构造方法 */ - init(capacity: Int) { - nums = Array(repeating: 0, count: capacity) - front = 0 - queSize = 0 - } - - /* 获取双向队列的容量 */ - func capacity() -> Int { - nums.count - } - - /* 获取双向队列的长度 */ - func size() -> Int { - queSize - } - - /* 判断双向队列是否为空 */ - 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 - queSize += 1 - } - - /* 队尾入队 */ - func pushLast(num: Int) { - if size() == capacity() { - print("双向队列已满") - return - } - // 计算尾指针,指向队尾索引 + 1 - let rear = index(i: front + size()) - // 将 num 添加至队尾 - nums[rear] = num - queSize += 1 - } - - /* 队首出队 */ - func popFirst() -> Int { - let num = peekFirst() - // 队首指针向后移动一位 - front = index(i: front + 1) - queSize -= 1 - return num - } - - /* 队尾出队 */ - func popLast() -> Int { - let num = peekLast() - queSize -= 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] { - // 仅转换有效长度范围内的列表元素 - var res = Array(repeating: 0, count: size()) - for (i, j) in sequence(first: (0, front), next: { $0 < self.size() - 1 ? ($0 + 1, $1 + 1) : nil }) { - res[i] = nums[index(i: j)] - } - return res - } - } - ``` - -=== "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" - /* 基于环形数组实现的双向队列 */ - struct arrayDeque { - int *nums; // 用于存储队列元素的数组 - int front; // 队首指针,指向队首元素 - int queSize; // 尾指针,指向队尾 + 1 - int queCapacity; // 队列容量 - }; - - typedef struct arrayDeque 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); - deque->queCapacity = 0; - } - - /* 获取双向队列的容量 */ - 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; - } - - /* 打印队列 */ - void printArrayDeque(arrayDeque *deque) { - int arr[deque->queSize]; - // 拷贝 - for (int i = 0, j = deque->front; i < deque->queSize; i++, j++) { - arr[i] = deque->nums[j % deque->queCapacity]; - } - printArray(arr, deque->queSize); - } - ``` - -=== "Zig" - - ```zig title="array_deque.zig" - [class]{ArrayDeque}-[func]{} - ``` - -## 5.3.3   双向队列应用 - -双向队列兼具栈与队列的逻辑,**因此它可以实现这两者的所有应用场景,同时提供更高的自由度**。 - -我们知道,软件的“撤销”功能通常使用栈来实现:系统将每次更改操作 `push` 到栈中,然后通过 `pop` 实现撤销。然而,考虑到系统资源的限制,软件通常会限制撤销的步数(例如仅允许保存 $50$ 步)。当栈的长度超过 $50$ 时,软件需要在栈底(即队首)执行删除操作。**但栈无法实现该功能,此时就需要使用双向队列来替代栈**。请注意,“撤销”的核心逻辑仍然遵循栈的先入后出原则,只是双向队列能够更加灵活地实现一些额外逻辑。 diff --git a/chapter_stack_and_queue/queue.md b/chapter_stack_and_queue/queue.md deleted file mode 100755 index d638cf9e0..000000000 --- a/chapter_stack_and_queue/queue.md +++ /dev/null @@ -1,2135 +0,0 @@ ---- -comments: true ---- - -# 5.2   队列 - -「队列 queue」是一种遵循先入先出规则的线性数据结构。顾名思义,队列模拟了排队现象,即新来的人不断加入队列的尾部,而位于队列头部的人逐个离开。 - -如图 5-4 所示,我们将队列的头部称为“队首”,尾部称为“队尾”,将把元素加入队尾的操作称为“入队”,删除队首元素的操作称为“出队”。 - -![队列的先入先出规则](queue.assets/queue_operations.png) - -

图 5-4   队列的先入先出规则

- -## 5.2.1   队列常用操作 - -队列的常见操作如表 5-2 所示。需要注意的是,不同编程语言的方法名称可能会有所不同。我们在此采用与栈相同的方法命名。 - -

表 5-2   队列操作效率

- -
- -| 方法名 | 描述 | 时间复杂度 | -| --------- | -------------------------- | -------- | -| push() | 元素入队,即将元素添加至队尾 | $O(1)$ | -| pop() | 队首元素出队 | $O(1)$ | -| peek() | 访问队首元素 | $O(1)$ | - -
- -我们可以直接使用编程语言中现成的队列类。 - -=== "Python" - - ```python title="queue.py" - # 初始化队列 - # 在 Python 中,我们一般将双向队列类 deque 看作队列使用 - # 虽然 queue.Queue() 是纯正的队列类,但不太好用,因此不建议 - que: deque[int] = collections.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 未提供内置队列 - ``` - -=== "Zig" - - ```zig title="queue.zig" - - ``` - -## 5.2.2   队列实现 - -为了实现队列,我们需要一种数据结构,可以在一端添加元素,并在另一端删除元素。因此,链表和数组都可以用来实现队列。 - -### 1.   基于链表的实现 - -如图 5-5 所示,我们可以将链表的“头节点”和“尾节点”分别视为“队首”和“队尾”,规定队尾仅可添加节点,队首仅可删除节点。 - -=== "LinkedListQueue" - ![基于链表实现队列的入队出队操作](queue.assets/linkedlist_queue.png) - -=== "push()" - ![linkedlist_queue_push](queue.assets/linkedlist_queue_push.png) - -=== "pop()" - ![linkedlist_queue_pop](queue.assets/linkedlist_queue_pop.png) - -

图 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++; - } - - /* 出队 */ - void pop() { - int num = peek(); - // 删除头节点 - ListNode *tmp = front; - front = front->next; - // 释放内存 - delete tmp; - queSize--; - } - - /* 访问队首元素 */ - 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 { - private ListNode? front, rear; // 头节点 front ,尾节点 rear - private 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 ListNode(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 Array.Empty(); - - 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 = 0 - - init() {} - - /* 获取队列的长度 */ - 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" - /* 基于链表实现的队列 */ - struct linkedListQueue { - ListNode *front, *rear; - int queSize; - }; - - typedef struct linkedListQueue linkedListQueue; - - /* 构造函数 */ - linkedListQueue *newLinkedListQueue() { - linkedListQueue *queue = (linkedListQueue *)malloc(sizeof(linkedListQueue)); - queue->front = NULL; - queue->rear = NULL; - queue->queSize = 0; - return queue; - } - - /* 析构函数 */ - void delLinkedListQueue(linkedListQueue *queue) { - // 释放所有节点 - for (int i = 0; i < queue->queSize && queue->front != NULL; i++) { - 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; - } - - /* 出队 */ - void pop(linkedListQueue *queue) { - int num = peek(queue); - ListNode *tmp = queue->front; - queue->front = queue->front->next; - free(tmp); - queue->queSize--; - } - - /* 打印队列 */ - void printLinkedListQueue(linkedListQueue *queue) { - int arr[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); - } - ``` - -=== "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; - } - }; - } - ``` - -### 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.png) - -=== "push()" - ![array_queue_push](queue.assets/array_queue_push.png) - -=== "pop()" - ![array_queue_pop](queue.assets/array_queue_pop.png) - -

图 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++; - } - - /* 出队 */ - void pop() { - int num = peek(); - // 队首指针向后移动一位,若越过尾部则返回到数组头部 - front = (front + 1) % queCapacity; - queSize--; - } - - /* 访问队首元素 */ - 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 { - 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 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 = 0 // 队首指针,指向队首元素 - private var queSize = 0 // 队列长度 - - init(capacity: Int) { - // 初始化数组 - nums = Array(repeating: 0, count: capacity) - } - - /* 获取队列的容量 */ - func capacity() -> Int { - nums.count - } - - /* 获取队列的长度 */ - func size() -> Int { - queSize - } - - /* 判断队列是否为空 */ - func isEmpty() -> Bool { - queSize == 0 - } - - /* 入队 */ - func push(num: Int) { - if size() == capacity() { - print("队列已满") - return - } - // 计算尾指针,指向队尾索引 + 1 - // 通过取余操作,实现 rear 越过数组尾部后回到头部 - let rear = (front + queSize) % capacity() - // 将 num 添加至队尾 - nums[rear] = num - queSize += 1 - } - - /* 出队 */ - @discardableResult - func pop() -> Int { - let num = peek() - // 队首指针向后移动一位,若越过尾部则返回到数组头部 - front = (front + 1) % capacity() - queSize -= 1 - return num - } - - /* 访问队首元素 */ - func peek() -> Int { - if isEmpty() { - fatalError("队列为空") - } - return nums[front] - } - - /* 返回数组 */ - func toArray() -> [Int] { - // 仅转换有效长度范围内的列表元素 - var res = Array(repeating: 0, count: queSize) - for (i, j) in sequence(first: (0, front), next: { $0 < self.queSize - 1 ? ($0 + 1, $1 + 1) : nil }) { - res[i] = nums[j % capacity()] - } - return res - } - } - ``` - -=== "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" - /* 基于环形数组实现的队列 */ - struct arrayQueue { - int *nums; // 用于存储队列元素的数组 - int front; // 队首指针,指向队首元素 - int queSize; // 尾指针,指向队尾 + 1 - int queCapacity; // 队列容量 - }; - - typedef struct arrayQueue 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); - queue->queCapacity = 0; - } - - /* 获取队列的容量 */ - 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++; - } - - /* 出队 */ - void pop(arrayQueue *queue) { - int num = peek(queue); - // 队首指针向后移动一位,若越过尾部则返回到数组头部 - queue->front = (queue->front + 1) % queue->queCapacity; - queue->queSize--; - } - - /* 打印队列 */ - void printArrayQueue(arrayQueue *queue) { - int arr[queue->queSize]; - // 拷贝 - for (int i = 0, j = queue->front; i < queue->queSize; i++, j++) { - arr[i] = queue->nums[j % queue->queCapacity]; - } - printArray(arr, queue->queSize); - } - ``` - -=== "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; - } - }; - } - ``` - -以上实现的队列仍然具有局限性,即其长度不可变。然而,这个问题不难解决,我们可以将数组替换为动态数组,从而引入扩容机制。有兴趣的同学可以尝试自行实现。 - -两种实现的对比结论与栈一致,在此不再赘述。 - -## 5.2.3   队列典型应用 - -- **淘宝订单**。购物者下单后,订单将加入队列中,系统随后会根据顺序依次处理队列中的订单。在双十一期间,短时间内会产生海量订单,高并发成为工程师们需要重点攻克的问题。 -- **各类待办事项**。任何需要实现“先来后到”功能的场景,例如打印机的任务队列、餐厅的出餐队列等。队列在这些场景中可以有效地维护处理顺序。 diff --git a/chapter_stack_and_queue/stack.md b/chapter_stack_and_queue/stack.md deleted file mode 100755 index 7261fed97..000000000 --- a/chapter_stack_and_queue/stack.md +++ /dev/null @@ -1,1721 +0,0 @@ ---- -comments: true ---- - -# 5.1   栈 - -「栈 stack」是一种遵循先入后出的逻辑的线性数据结构。 - -我们可以将栈类比为桌面上的一摞盘子,如果需要拿出底部的盘子,则需要先将上面的盘子依次取出。我们将盘子替换为各种类型的元素(如整数、字符、对象等),就得到了栈数据结构。 - -如图 5-1 所示,我们把堆叠元素的顶部称为“栈顶”,底部称为“栈底”。将把元素添加到栈顶的操作叫做“入栈”,删除栈顶元素的操作叫做“出栈”。 - -![栈的先入后出规则](stack.assets/stack_operations.png) - -

图 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[stack.len() - 1]; - - /* 元素出栈 */ - let pop = stack.pop().unwrap(); - - /* 获取栈的长度 */ - let size = stack.len(); - - /* 判断是否为空 */ - let is_empty = stack.is_empty(); - ``` - -=== "C" - - ```c title="stack.c" - // C 未提供内置栈 - ``` - -=== "Zig" - - ```zig title="stack.zig" - - ``` - -## 5.1.2   栈的实现 - -为了深入了解栈的运行机制,我们来尝试自己实现一个栈类。 - -栈遵循先入后出的原则,因此我们只能在栈顶添加或删除元素。然而,数组和链表都可以在任意位置添加和删除元素,**因此栈可以被视为一种受限制的数组或链表**。换句话说,我们可以“屏蔽”数组或链表的部分无关操作,使其对外表现的逻辑符合栈的特性。 - -### 1.   基于链表的实现 - -使用链表来实现栈时,我们可以将链表的头节点视为栈顶,尾节点视为栈底。 - -如图 5-2 所示,对于入栈操作,我们只需将元素插入链表头部,这种节点插入方法被称为“头插法”。而对于出栈操作,只需将头节点从链表中删除即可。 - -=== "LinkedListStack" - ![基于链表实现栈的入栈出栈操作](stack.assets/linkedlist_stack.png) - -=== "push()" - ![linkedlist_stack_push](stack.assets/linkedlist_stack_push.png) - -=== "pop()" - ![linkedlist_stack_pop](stack.assets/linkedlist_stack_pop.png) - -

图 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++; - } - - /* 出栈 */ - void pop() { - int num = top(); - ListNode *tmp = stackTop; - stackTop = stackTop->next; - // 释放内存 - delete tmp; - stkSize--; - } - - /* 访问栈顶元素 */ - 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 { - private ListNode? stackPeek; // 将头节点作为栈顶 - private 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 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 Exception(); - return stackPeek.val; - } - - /* 将 List 转化为 Array 并返回 */ - public int[] toArray() { - if (stackPeek == null) - return Array.Empty(); - - 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 = 0 // 栈的长度 - - init() {} - - /* 获取栈的长度 */ - 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 sequence(first: res.count - 1, next: { $0 >= 0 + 1 ? $0 - 1 : nil }) { - 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" - /* 基于链表实现的栈 */ - struct linkedListStack { - ListNode *top; // 将头节点作为栈顶 - int size; // 栈的长度 - }; - - typedef struct linkedListStack 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) { - assert(s); - return s->size; - } - - /* 判断栈是否为空 */ - bool isEmpty(linkedListStack *s) { - assert(s); - return size(s) == 0; - } - - /* 访问栈顶元素 */ - int peek(linkedListStack *s) { - assert(s); - assert(size(s) != 0); - return s->top->val; - } - - /* 入栈 */ - void push(linkedListStack *s, int num) { - assert(s); - ListNode *node = (ListNode *)malloc(sizeof(ListNode)); - node->next = s->top; // 更新新加节点指针域 - node->val = num; // 更新新加节点数据域 - s->top = node; // 更新栈顶 - s->size++; // 更新栈大小 - } - - /* 出栈 */ - int pop(linkedListStack *s) { - if (s->size == 0) { - printf("stack is empty.\n"); - return INT_MAX; - } - assert(s); - int val = peek(s); - ListNode *tmp = s->top; - s->top = s->top->next; - // 释放内存 - free(tmp); - s->size--; - return val; - } - ``` - -=== "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; - } - }; - } - ``` - -### 2.   基于数组的实现 - -使用数组实现栈时,我们可以将数组的尾部作为栈顶。如图 5-3 所示,入栈与出栈操作分别对应在数组尾部添加元素与删除元素,时间复杂度都为 $O(1)$ 。 - -=== "ArrayStack" - ![基于数组实现栈的入栈出栈操作](stack.assets/array_stack.png) - -=== "push()" - ![array_stack_push](stack.assets/array_stack_push.png) - -=== "pop()" - ![array_stack_pop](stack.assets/array_stack_pop.png) - -

图 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); - } - - /* 出栈 */ - void pop() { - int oldTop = top(); - stack.pop_back(); - } - - /* 访问栈顶元素 */ - 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 { - private List stack; - public ArrayStack() { - // 初始化列表(动态数组) - stack = new(); - } - - /* 获取栈的长度 */ - 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.ToArray(); - } - } - ``` - -=== "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 { - match self.stack.pop() { - Some(num) => Some(num), - None => None, - } - } - - /* 访问栈顶元素 */ - 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" - /* 基于数组实现的栈 */ - struct arrayStack { - int *data; - int size; - }; - - typedef struct arrayStack arrayStack; - - /* 构造函数 */ - arrayStack *newArrayStack() { - arrayStack *s = malloc(sizeof(arrayStack)); - // 初始化一个大容量,避免扩容 - s->data = malloc(sizeof(int) * MAX_SIZE); - s->size = 0; - return s; - } - - /* 获取栈的长度 */ - int size(arrayStack *s) { - return s->size; - } - - /* 判断栈是否为空 */ - bool isEmpty(arrayStack *s) { - return s->size == 0; - } - - /* 入栈 */ - void push(arrayStack *s, int num) { - if (s->size == MAX_SIZE) { - printf("stack is full.\n"); - return; - } - s->data[s->size] = num; - s->size++; - } - - /* 访问栈顶元素 */ - int peek(arrayStack *s) { - if (s->size == 0) { - printf("stack is empty.\n"); - return INT_MAX; - } - return s->data[s->size - 1]; - } - - /* 出栈 */ - int pop(arrayStack *s) { - if (s->size == 0) { - printf("stack is empty.\n"); - return INT_MAX; - } - int val = peek(s); - s->size--; - return val; - } - ``` - -=== "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.?; - } - }; - } - ``` - -## 5.1.3   两种实现对比 - -**支持操作** - -两种实现都支持栈定义中的各项操作。数组实现额外支持随机访问,但这已超出了栈的定义范畴,因此一般不会用到。 - -**时间效率** - -在基于数组的实现中,入栈和出栈操作都是在预先分配好的连续内存中进行,具有很好的缓存本地性,因此效率较高。然而,如果入栈时超出数组容量,会触发扩容机制,导致该次入栈操作的时间复杂度变为 $O(n)$ 。 - -在链表实现中,链表的扩容非常灵活,不存在上述数组扩容时效率降低的问题。但是,入栈操作需要初始化节点对象并修改指针,因此效率相对较低。不过,如果入栈元素本身就是节点对象,那么可以省去初始化步骤,从而提高效率。 - -综上所述,当入栈与出栈操作的元素是基本数据类型时,例如 `int` 或 `double` ,我们可以得出以下结论。 - -- 基于数组实现的栈在触发扩容时效率会降低,但由于扩容是低频操作,因此平均效率更高。 -- 基于链表实现的栈可以提供更加稳定的效率表现。 - -**空间效率** - -在初始化列表时,系统会为列表分配“初始容量”,该容量可能超过实际需求。并且,扩容机制通常是按照特定倍率(例如 2 倍)进行扩容,扩容后的容量也可能超出实际需求。因此,**基于数组实现的栈可能造成一定的空间浪费**。 - -然而,由于链表节点需要额外存储指针,**因此链表节点占用的空间相对较大**。 - -综上,我们不能简单地确定哪种实现更加节省内存,需要针对具体情况进行分析。 - -## 5.1.4   栈典型应用 - -- **浏览器中的后退与前进、软件中的撤销与反撤销**。每当我们打开新的网页,浏览器就会将上一个网页执行入栈,这样我们就可以通过后退操作回到上一页面。后退操作实际上是在执行出栈。如果要同时支持后退和前进,那么需要两个栈来配合实现。 -- **程序内存管理**。每次调用函数时,系统都会在栈顶添加一个栈帧,用于记录函数的上下文信息。在递归函数中,向下递推阶段会不断执行入栈操作,而向上回溯阶段则会执行出栈操作。 diff --git a/chapter_tree/array_representation_of_tree.md b/chapter_tree/array_representation_of_tree.md deleted file mode 100644 index a3252e68f..000000000 --- a/chapter_tree/array_representation_of_tree.md +++ /dev/null @@ -1,1174 +0,0 @@ ---- -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) - -

图 7-12   完美二叉树的数组表示

- -**映射公式的角色相当于链表中的指针**。给定数组中的任意一个节点,我们都可以通过映射公式来访问它的左(右)子节点。 - -## 7.3.2   表示任意二叉树 - -完美二叉树是一个特例,在二叉树的中间层通常存在许多 $\text{None}$ 。由于层序遍历序列并不包含这些 $\text{None}$ ,因此我们无法仅凭该序列来推测 $\text{None}$ 的数量和分布位置。**这意味着存在多种二叉树结构都符合该层序遍历序列**。 - -如图 7-13 所示,给定一个非完美二叉树,上述的数组表示方法已经失效。 - -![层序遍历序列对应多种二叉树可能性](array_representation_of_tree.assets/array_representation_without_empty.png) - -

图 7-13   层序遍历序列对应多种二叉树可能性

- -为了解决此问题,**我们可以考虑在层序遍历序列中显式地写出所有 $\text{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}; - ``` - -=== "Zig" - - ```zig title="" - - ``` - -![任意类型二叉树的数组表示](array_representation_of_tree.assets/array_representation_with_empty.png) - -

图 7-14   任意类型二叉树的数组表示

- -值得说明的是,**完全二叉树非常适合使用数组来表示**。回顾完全二叉树的定义,$\text{None}$ 只出现在最底层且靠右的位置,**因此所有 $\text{None}$ 一定出现在层序遍历序列的末尾**。 - -这意味着使用数组表示完全二叉树时,可以省略存储所有 $\text{None}$ ,非常方便。图 7-15 给出了一个例子。 - -![完全二叉树的数组表示](array_representation_of_tree.assets/array_representation_complete_binary_tree.png) - -

图 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 (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)); - } - - /* 前序遍历 */ - 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 { - private List tree; - - /* 构造方法 */ - public ArrayBinaryTree(List arr) { - tree = new List(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 = new List(); - // 直接遍历数组 - for (int i = 0; i < size(); i++) { - if (val(i).HasValue) - res.Add(val(i).Value); - } - return res; - } - - /* 深度优先遍历 */ - private 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 = new List(); - dfs(0, "pre", res); - return res; - } - - /* 中序遍历 */ - public List inOrder() { - List res = new List(); - dfs(0, "in", res); - return res; - } - - /* 后序遍历 */ - public List postOrder() { - List res = new List(); - 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 stride(from: 0, to: size(), by: 1) { - 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 (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 (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" - /* 数组表示下的二叉树类 */ - struct arrayBinaryTree { - vector *tree; - }; - - typedef struct arrayBinaryTree arrayBinaryTree; - - /* 构造函数 */ - arrayBinaryTree *newArrayBinaryTree(vector *arr) { - arrayBinaryTree *newABT = malloc(sizeof(arrayBinaryTree)); - newABT->tree = arr; - return newABT; - } - - /* 节点数量 */ - int size(arrayBinaryTree *abt) { - return abt->tree->size; - } - - /* 获取索引为 i 节点的值 */ - int val(arrayBinaryTree *abt, int i) { - // 若索引越界,则返回 INT_MAX ,代表空位 - if (i < 0 || i >= size(abt)) - return INT_MAX; - return *(int *)abt->tree->data[i]; - } - - /* 深度优先遍历 */ - void dfs(arrayBinaryTree *abt, int i, const char *order, vector *res) { - // 若为空位,则返回 - if (val(abt, i) == INT_MAX) - return; - // 前序遍历 - if (strcmp(order, "pre") == 0) { - int tmp = val(abt, i); - vectorPushback(res, &tmp, sizeof(tmp)); - } - dfs(abt, left(i), order, res); - // 中序遍历 - if (strcmp(order, "in") == 0) { - int tmp = val(abt, i); - vectorPushback(res, &tmp, sizeof(tmp)); - } - dfs(abt, right(i), order, res); - // 后序遍历 - if (strcmp(order, "post") == 0) { - int tmp = val(abt, i); - vectorPushback(res, &tmp, sizeof(tmp)); - } - } - - /* 层序遍历 */ - vector *levelOrder(arrayBinaryTree *abt) { - vector *res = newVector(); - // 直接遍历数组 - for (int i = 0; i < size(abt); i++) { - if (val(abt, i) != INT_MAX) { - int tmp = val(abt, i); - vectorPushback(res, &tmp, sizeof(int)); - } - } - return res; - } - - /* 前序遍历 */ - vector *preOrder(arrayBinaryTree *abt) { - vector *res = newVector(); - dfs(abt, 0, "pre", res); - return res; - } - - /* 中序遍历 */ - vector *inOrder(arrayBinaryTree *abt) { - vector *res = newVector(); - dfs(abt, 0, "in", res); - return res; - } - - /* 后序遍历 */ - vector *postOrder(arrayBinaryTree *abt) { - vector *res = newVector(); - dfs(abt, 0, "post", res); - return res; - } - ``` - -=== "Zig" - - ```zig title="array_binary_tree.zig" - [class]{ArrayBinaryTree}-[func]{} - ``` - -## 7.3.3   优势与局限性 - -二叉树的数组表示主要有以下优点。 - -- 数组存储在连续的内存空间中,对缓存友好,访问与遍历速度较快。 -- 不需要存储指针,比较节省空间。 -- 允许随机访问节点。 - -然而,数组表示也存在一些局限性。 - -- 数组存储需要连续内存空间,因此不适合存储数据量过大的树。 -- 增删节点需要通过数组插入与删除操作实现,效率较低。 -- 当二叉树中存在大量 $\text{None}$ 时,数组中包含的节点数据比重较低,空间利用率较低。 diff --git a/chapter_tree/avl_tree.md b/chapter_tree/avl_tree.md deleted file mode 100644 index 8ba5c3e13..000000000 --- a/chapter_tree/avl_tree.md +++ /dev/null @@ -1,2465 +0,0 @@ ---- -comments: true ---- - -# 7.5   AVL 树 * - -在二叉搜索树章节中,我们提到了在多次插入和删除操作后,二叉搜索树可能退化为链表。这种情况下,所有操作的时间复杂度将从 $O(\log n)$ 恶化为 $O(n)$ 。 - -如图 7-24 所示,经过两次删除节点操作,这个二叉搜索树便会退化为链表。 - -![AVL 树在删除节点后发生退化](avl_tree.assets/avltree_degradation_from_removing_node.png) - -

图 7-24   AVL 树在删除节点后发生退化

- -再例如,在图 7-25 的完美二叉树中插入两个节点后,树将严重向左倾斜,查找操作的时间复杂度也随之恶化。 - -![AVL 树在插入节点后发生退化](avl_tree.assets/avltree_degradation_from_inserting_node.png) - -

图 7-25   AVL 树在插入节点后发生退化

- -G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "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 { - public int val; // 节点值 - public int height; // 节点高度 - public TreeNode? left; // 左子节点 - public TreeNode? right; // 右子节点 - public TreeNode(int x) { val = x; } - } - ``` - -=== "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 树节点结构体 */ - struct TreeNode { - int val; - int height; - struct TreeNode *left; - struct TreeNode *right; - }; - - typedef struct TreeNode 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; - } - ``` - -=== "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 == nil ? -1 : node!.height - } - - /* 更新节点高度 */ - 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; - } - } - ``` - -=== "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); - } - ``` - -=== "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) - -=== "<2>" - ![avltree_right_rotate_step2](avl_tree.assets/avltree_right_rotate_step2.png) - -=== "<3>" - ![avltree_right_rotate_step3](avl_tree.assets/avltree_right_rotate_step3.png) - -=== "<4>" - ![avltree_right_rotate_step4](avl_tree.assets/avltree_right_rotate_step4.png) - -

图 7-26   右旋操作步骤

- -如图 7-27 所示,当节点 `child` 有右子节点(记为 `grandChild` )时,需要在右旋中添加一步:将 `grandChild` 作为 `node` 的左子节点。 - -![有 grandChild 的右旋操作](avl_tree.assets/avltree_right_rotate_with_grandchild.png) - -

图 7-27   有 grandChild 的右旋操作

- -“向右旋转”是一种形象化的说法,实际上需要通过修改节点指针来实现,代码如下所示。 - -=== "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; - } - ``` - -=== "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) - -

图 7-28   左旋操作

- -同理,如图 7-29 所示,当节点 `child` 有左子节点(记为 `grandChild` )时,需要在左旋中添加一步:将 `grandChild` 作为 `node` 的右子节点。 - -![有 grandChild 的左旋操作](avl_tree.assets/avltree_left_rotate_with_grandchild.png) - -

图 7-29   有 grandChild 的左旋操作

- -可以观察到,**右旋和左旋操作在逻辑上是镜像对称的,它们分别解决的两种失衡情况也是对称的**。基于对称性,我们只需将右旋的实现代码中的所有的 `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; - } - ``` - -=== "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) - -

图 7-30   先左旋后右旋

- -### 4.   先右旋后左旋 - -如图 7-31 所示,对于上述失衡二叉树的镜像情况,需要先对 `child` 执行“右旋”,然后对 `node` 执行“左旋”。 - -![先右旋后左旋](avl_tree.assets/avltree_right_left_rotate.png) - -

图 7-31   先右旋后左旋

- -### 5.   旋转的选择 - -图 7-32 展示的四种失衡情况与上述案例逐个对应,分别需要采用右旋、左旋、先右后左、先左后右的旋转操作。 - -![AVL 树的四种旋转情况](avl_tree.assets/avltree_rotation_cases.png) - -

图 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; - } - ``` - -=== "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; - } - ``` - -=== "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 != 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; - } - ``` - -=== "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 != nil ? 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 removeNode(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; - } - ``` - -=== "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/chapter_tree/binary_search_tree.md b/chapter_tree/binary_search_tree.md deleted file mode 100755 index cb8343e6b..000000000 --- a/chapter_tree/binary_search_tree.md +++ /dev/null @@ -1,1499 +0,0 @@ ---- -comments: true ---- - -# 7.4   二叉搜索树 - -如图 7-16 所示,「二叉搜索树 binary search tree」满足以下条件。 - -1. 对于根节点,左子树中所有节点的值 $<$ 根节点的值 $<$ 右子树中所有节点的值。 -2. 任意节点的左、右子树也是二叉搜索树,即同样满足条件 `1.` 。 - -![二叉搜索树](binary_search_tree.assets/binary_search_tree.png) - -

图 7-16   二叉搜索树

- -## 7.4.1   二叉搜索树的操作 - -我们将二叉搜索树封装为一个类 `ArrayBinaryTree` ,并声明一个成员变量 `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) - -=== "<2>" - ![bst_search_step2](binary_search_tree.assets/bst_search_step2.png) - -=== "<3>" - ![bst_search_step3](binary_search_tree.assets/bst_search_step3.png) - -=== "<4>" - ![bst_search_step4](binary_search_tree.assets/bst_search_step4.png) - -

图 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) -> Option { - let mut cur = self.root.clone(); - - // 循环查找,越过叶节点后跳出 - while let Some(node) = cur.clone() { - // 目标节点在 cur 的右子树中 - if node.borrow().val < num { - cur = node.borrow().right.clone(); - } - // 目标节点在 cur 的左子树中 - else if node.borrow().val > num { - cur = node.borrow().left.clone(); - } - // 找到目标节点,跳出循环 - else { - 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; - } - ``` - -=== "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; - } - ``` - -### 2.   插入节点 - -给定一个待插入元素 `num` ,为了保持二叉搜索树“左子树 < 根节点 < 右子树”的性质,插入操作流程如图 7-18 所示。 - -1. **查找插入位置**:与查找操作相似,从根节点出发,根据当前节点值和 `num` 的大小关系循环向下搜索,直到越过叶节点(遍历至 $\text{None}$ )时跳出循环。 -2. **在该位置插入节点**:初始化节点 `num` ,将该节点置于 $\text{None}$ 的位置。 - -![在二叉搜索树中插入节点](binary_search_tree.assets/bst_insert.png) - -

图 7-18   在二叉搜索树中插入节点

- -在代码实现中,需要注意以下两点。 - -- 二叉搜索树不允许存在重复节点,否则将违反其定义。因此,若待插入节点在树中已存在,则不执行插入,直接返回。 -- 为了实现插入节点,我们需要借助节点 `pre` 保存上一轮循环的节点。这样在遍历至 $\text{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 TreeNode(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() { - // 找到重复节点,直接返回 - if node.borrow().val == num { - return; - } - // 插入位置在 cur 的右子树中 - pre = cur.clone(); - if node.borrow().val < num { - cur = node.borrow().right.clone(); - } - // 插入位置在 cur 的左子树中 - else { - cur = node.borrow().left.clone(); - } - } - // 插入节点 - let node = TreeNode::new(num); - let pre = pre.unwrap(); - if pre.borrow().val < num { - pre.borrow_mut().right = Some(Rc::clone(&node)); - } else { - pre.borrow_mut().left = Some(Rc::clone(&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; - } - } - ``` - -=== "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; - } - } - ``` - -与查找节点相同,插入节点使用 $O(\log n)$ 时间。 - -### 3.   删除节点 - -先在二叉树中查找到目标节点,再将其从二叉树中删除。 - -与插入节点类似,我们需要保证在删除操作完成后,二叉搜索树的“左子树 < 根节点 < 右子树”的性质仍然满足。 - -因此,我们需要根据目标节点的子节点数量,共分为 0、1 和 2 这三种情况,执行对应的删除节点操作。 - -如图 7-19 所示,当待删除节点的度为 $0$ 时,表示该节点是叶节点,可以直接删除。 - -![在二叉搜索树中删除节点(度为 0 )](binary_search_tree.assets/bst_remove_case1.png) - -

图 7-19   在二叉搜索树中删除节点(度为 0 )

- -如图 7-20 所示,当待删除节点的度为 $1$ 时,将待删除节点替换为其子节点即可。 - -![在二叉搜索树中删除节点(度为 1 )](binary_search_tree.assets/bst_remove_case2.png) - -

图 7-20   在二叉搜索树中删除节点(度为 1 )

- -当待删除节点的度为 $2$ 时,我们无法直接删除它,而需要使用一个节点替换该节点。由于要保持二叉搜索树“左 $<$ 根 $<$ 右”的性质,**因此这个节点可以是右子树的最小节点或左子树的最大节点**。 - -假设我们选择右子树的最小节点(即中序遍历的下一个节点),则删除操作流程如图 7-21 所示。 - -1. 找到待删除节点在“中序遍历序列”中的下一个节点,记为 `tmp` 。 -2. 将 `tmp` 的值覆盖待删除节点的值,并在树中递归删除节点 `tmp` 。 - -=== "<1>" - ![在二叉搜索树中删除节点(度为 2 )](binary_search_tree.assets/bst_remove_case3_step1.png) - -=== "<2>" - ![bst_remove_case3_step2](binary_search_tree.assets/bst_remove_case3_step2.png) - -=== "<3>" - ![bst_remove_case3_step3](binary_search_tree.assets/bst_remove_case3_step3.png) - -=== "<4>" - ![bst_remove_case3_step4](binary_search_tree.assets/bst_remove_case3_step4.png) - -

图 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 != 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; - } - } - ``` - -=== "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 != nil ? 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() { - // 找到待删除节点,跳出循环 - if node.borrow().val == num { - break; - } - // 待删除节点在 cur 的右子树中 - pre = cur.clone(); - if node.borrow().val < num { - cur = node.borrow().right.clone(); - } - // 待删除节点在 cur 的左子树中 - else { - cur = node.borrow().left.clone(); - } - } - // 若无待删除节点,则直接返回 - if cur.is_none() { - return; - } - let cur = cur.unwrap(); - // 子节点数量 = 0 or 1 - if cur.borrow().left.is_none() || cur.borrow().right.is_none() { - // 当子节点数量 = 0 / 1 时, child = nullptr / 该子节点 - let child = cur.borrow().left.clone().or_else(|| cur.borrow().right.clone()); - let pre = pre.unwrap(); - let left = pre.borrow().left.clone().unwrap(); - // 删除节点 cur - if !Rc::ptr_eq(&cur, self.root.as_ref().unwrap()) { - if Rc::ptr_eq(&left, &cur) { - pre.borrow_mut().left = child; - } else { - pre.borrow_mut().right = child; - } - } else { - // 若删除节点为根节点,则重新指定根节点 - self.root = child; - } - } - // 子节点数量 = 2 - else { - // 获取中序遍历中 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 removeNode(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; - } - } else { - /* 子节点数量 = 2 */ - // 获取中序遍历中 cur 的下一个节点 - TreeNode *tmp = cur->right; - while (tmp->left != NULL) { - tmp = tmp->left; - } - int tmpVal = tmp->val; - // 递归删除节点 tmp - removeNode(bst, tmp->val); - // 用 tmp 覆盖 cur - cur->val = tmpVal; - } - } - ``` - -=== "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; - } - } - ``` - -### 4.   中序遍历有序 - -如图 7-22 所示,二叉树的中序遍历遵循“左 $\rightarrow$ 根 $\rightarrow$ 右”的遍历顺序,而二叉搜索树满足“左子节点 $<$ 根节点 $<$ 右子节点”的大小关系。 - -这意味着在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小节点,从而得出一个重要性质:**二叉搜索树的中序遍历序列是升序的**。 - -利用中序遍历升序的性质,我们在二叉搜索树中获取有序数据仅需 $O(n)$ 时间,无须进行额外的排序操作,非常高效。 - -![二叉搜索树的中序遍历序列](binary_search_tree.assets/bst_inorder_traversal.png) - -

图 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) - -

图 7-23   二叉搜索树的退化

- -## 7.4.3   二叉搜索树常见应用 - -- 用作系统中的多级索引,实现高效的查找、插入、删除操作。 -- 作为某些搜索算法的底层数据结构。 -- 用于存储数据流,以保持其有序状态。 diff --git a/chapter_tree/binary_tree_traversal.md b/chapter_tree/binary_tree_traversal.md deleted file mode 100755 index aa93bffc1..000000000 --- a/chapter_tree/binary_tree_traversal.md +++ /dev/null @@ -1,804 +0,0 @@ ---- -comments: true ---- - -# 7.2   二叉树遍历 - -从物理结构的角度来看,树是一种基于链表的数据结构,因此其遍历方式是通过指针逐个访问节点。然而,树是一种非线性数据结构,这使得遍历树比遍历链表更加复杂,需要借助搜索算法来实现。 - -二叉树常见的遍历方式包括层序遍历、前序遍历、中序遍历和后序遍历等。 - -## 7.2.1   层序遍历 - -如图 7-9 所示,「层序遍历 level-order traversal」从顶部到底部逐层遍历二叉树,并在每一层按照从左到右的顺序访问节点。 - -层序遍历本质上属于「广度优先遍历 breadth-first traversal」,它体现了一种“一圈一圈向外扩展”的逐层遍历方式。 - -![二叉树的层序遍历](binary_tree_traversal.assets/binary_tree_bfs.png) - -

图 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 = new(); - while (queue.Count != 0) { - TreeNode node = queue.Dequeue(); // 队列出队 - list.Add(node.val); // 保存节点值 - 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_NODE_SIZE); - // 队列指针 - front = 0, rear = 0; - // 加入根节点 - queue[rear++] = root; - // 初始化一个列表,用于保存遍历序列 - /* 辅助数组 */ - arr = (int *)malloc(sizeof(int) * MAX_NODE_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; - } - ``` - -=== "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; - } - ``` - -### 2.   复杂度分析 - -- **时间复杂度 $O(n)$** :所有节点被访问一次,使用 $O(n)$ 时间,其中 $n$ 为节点数量。 -- **空间复杂度 $O(n)$** :在最差情况下,即满二叉树时,遍历到最底层之前,队列中最多同时存在 $(n + 1) / 2$ 个节点,占用 $O(n)$ 空间。 - -## 7.2.2   前序、中序、后序遍历 - -相应地,前序、中序和后序遍历都属于「深度优先遍历 depth-first traversal」,它体现了一种“先走到尽头,再回溯继续”的遍历方式。 - -图 7-10 展示了对二叉树进行深度优先遍历的工作原理。**深度优先遍历就像是绕着整个二叉树的外围“走”一圈**,在每个节点都会遇到三个位置,分别对应前序遍历、中序遍历和后序遍历。 - -![二叉搜索树的前、中、后序遍历](binary_tree_traversal.assets/binary_tree_dfs.png) - -

图 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); - 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); - } - ``` - -=== "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; - } - ``` - -=== "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); - } - ``` - -!!! note - - 深度优先搜索也可以基于迭代实现,有兴趣的同学可以自行研究。 - -图 7-11 展示了前序遍历二叉树的递归过程,其可分为“递”和“归”两个逆向的部分。 - -1. “递”表示开启新方法,程序在此过程中访问下一个节点。 -2. “归”表示函数返回,代表当前节点已经访问完毕。 - -=== "<1>" - ![前序遍历的递归过程](binary_tree_traversal.assets/preorder_step1.png) - -=== "<2>" - ![preorder_step2](binary_tree_traversal.assets/preorder_step2.png) - -=== "<3>" - ![preorder_step3](binary_tree_traversal.assets/preorder_step3.png) - -=== "<4>" - ![preorder_step4](binary_tree_traversal.assets/preorder_step4.png) - -=== "<5>" - ![preorder_step5](binary_tree_traversal.assets/preorder_step5.png) - -=== "<6>" - ![preorder_step6](binary_tree_traversal.assets/preorder_step6.png) - -=== "<7>" - ![preorder_step7](binary_tree_traversal.assets/preorder_step7.png) - -=== "<8>" - ![preorder_step8](binary_tree_traversal.assets/preorder_step8.png) - -=== "<9>" - ![preorder_step9](binary_tree_traversal.assets/preorder_step9.png) - -=== "<10>" - ![preorder_step10](binary_tree_traversal.assets/preorder_step10.png) - -=== "<11>" - ![preorder_step11](binary_tree_traversal.assets/preorder_step11.png) - -

图 7-11   前序遍历的递归过程

- -### 2.   复杂度分析 - -- **时间复杂度 $O(n)$** :所有节点被访问一次,使用 $O(n)$ 时间。 -- **空间复杂度 $O(n)$** :在最差情况下,即树退化为链表时,递归深度达到 $n$ ,系统占用 $O(n)$ 栈帧空间。 diff --git a/javascripts/katex.js b/overrides/javascripts/katex.js similarity index 100% rename from javascripts/katex.js rename to overrides/javascripts/katex.js diff --git a/javascripts/mathjax.js b/overrides/javascripts/mathjax.js similarity index 100% rename from javascripts/mathjax.js rename to overrides/javascripts/mathjax.js diff --git a/overrides/partials/comments.html b/overrides/partials/comments.html index a183fc080..c8ea9003a 100755 --- a/overrides/partials/comments.html +++ b/overrides/partials/comments.html @@ -1,5 +1,13 @@ {% if page.meta.comments %} -
{{ "欢迎在评论区留下你的见解、疑惑或建议" }}
+ {% if config.theme.language == 'zh' %} + {% set comm = "欢迎在评论区留下你的见解、疑惑或建议" %} + {% set lang = "zh-CN" %} + {% elif config.theme.language == 'en' %} + {% set comm = "Feel free to drop your insights, questions or suggestions" %} + {% set lang = "en" %} + {% endif %} +
{{ comm }}
+